Add IP and pasted certificate support

This commit is contained in:
i
2026-07-22 20:18:59 +08:00
parent f09231786f
commit d3bf52f884
2 changed files with 105 additions and 15 deletions
+8 -3
View File
@@ -9,7 +9,7 @@
每台 Relay 服务器需满足: 每台 Relay 服务器需满足:
- Linux,≥ 1 CPU / 1 GB RAM - Linux,≥ 1 CPU / 1 GB RAM
- 公网 IP,且有域名解析指向该 IP - 公网 IP,且有域名解析指向该 IP;脚本里的 Relay 地址也可以直接填写 IP
- 已安装 Docker(含 `docker compose`) - 已安装 Docker(含 `docker compose`)
- 以下端口按实际配置放行(脚本会在最后动态列出): - 以下端口按实际配置放行(脚本会在最后动态列出):
- `80/tcp` — Let's Encrypt HTTP challenge(仅自动证书模式需要) - `80/tcp` — Let's Encrypt HTTP challenge(仅自动证书模式需要)
@@ -71,7 +71,8 @@ Step 3.N · Relay Server #N(每台重复)
1) Let's Encrypt — 自动签发,需开放 80/tcp 1) Let's Encrypt — 自动签发,需开放 80/tcp
2) 已有证书 — 输入宿主机目录及容器内路径(均可自定义) 2) 已有证书 — 输入宿主机目录及容器内路径(均可自定义)
3) 自签证书 — 脚本调用 openssl 生成,支持完整 Subject 字段、 3) 自签证书 — 脚本调用 openssl 生成,支持完整 Subject 字段、
额外 SAN(DNS/IP)、有效期、RSA/ECC 及密钥长度选择 额外 SAN(DNS/IP)、有效期、RSA/ECC 及密钥长度选择
4) 粘贴证书 — 直接粘贴 PEM 证书和私钥内容,由脚本写入 certs 目录
Step 4 · Main Server config.yaml Snippet Step 4 · Main Server config.yaml Snippet
→ 打印带占位符的 config.yaml 片段(不输出真实域名和密钥) → 打印带占位符的 config.yaml 片段(不输出真实域名和密钥)
@@ -89,7 +90,7 @@ Step 6 · Firewall Configuration(可选,可跳过)
脚本支持在已有部署上重新运行以修改配置。Step 3 中先询问输出目录,若该目录下存在 `relay.env`,脚本会自动解析并将以下字段作为默认值填入后续输入框,直接回车即保留原值,输入新值则覆盖: 脚本支持在已有部署上重新运行以修改配置。Step 3 中先询问输出目录,若该目录下存在 `relay.env`,脚本会自动解析并将以下字段作为默认值填入后续输入框,直接回车即保留原值,输入新值则覆盖:
- 域名、监听端口、日志级别 - 域名或 IP、监听端口、日志级别
- STUN 开关及端口 - STUN 开关及端口
- TLS 模式、LE 邮箱、证书路径 - TLS 模式、LE 邮箱、证书路径
@@ -170,6 +171,10 @@ curl -v https://relay.example.com/
证书输出到 `<输出目录>/certs/`,私钥权限自动设为 `600`。若系统未安装 `openssl`,脚本会打印等效命令供手动执行。 证书输出到 `<输出目录>/certs/`,私钥权限自动设为 `600`。若系统未安装 `openssl`,脚本会打印等效命令供手动执行。
**粘贴证书**
选择该模式后,脚本会提示分别粘贴证书 PEM 和私钥 PEM,保存到 `<输出目录>/certs/fullchain.pem` 与 `<输出目录>/certs/privkey.pem`,并自动挂载到容器中。
## 主服务器 config.yaml 配置 ## 主服务器 config.yaml 配置
Step 4 输出带占位符的配置片段,需在主服务器上手动填入真实域名和密钥: Step 4 输出带占位符的配置片段,需在主服务器上手动填入真实域名和密钥:
+97 -12
View File
@@ -101,6 +101,38 @@ validate_domain() {
[[ "$1" =~ ^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$ ]] [[ "$1" =~ ^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*\.[a-zA-Z]{2,}$ ]]
} }
validate_ipv4() {
local IFS=.
local octet
local -a octets
read -ra octets <<< "$1"
[[ "${#octets[@]}" -eq 4 ]] || return 1
for octet in "${octets[@]}"; do
[[ "$octet" =~ ^[0-9]+$ ]] || return 1
(( octet >= 0 && octet <= 255 )) || return 1
done
}
validate_domain_or_ip() {
validate_domain "$1" || validate_ipv4 "$1"
}
read_multiline_block() {
# read_multiline_block <var_name> <prompt> [terminator]
local var="$1" prompt="$2" terminator="${3:-END}"
local line block=""
echo -e "${BOLD}${prompt}${RESET}"
echo " Paste the content below. Finish with a line containing only: ${terminator}"
while IFS= read -r line; do
[[ "$line" == "$terminator" ]] && break
block+="$line"$'\n'
done
printf -v "$var" '%s' "$block"
}
# ── Arg parsing ─────────────────────────────────────────────────────────────── # ── Arg parsing ───────────────────────────────────────────────────────────────
DRY_RUN=false DRY_RUN=false
OUTPUT_DIR="." OUTPUT_DIR="."
@@ -120,9 +152,9 @@ mkdir -p "$OUTPUT_DIR"
# ── Banner ──────────────────────────────────────────────────────────────────── # ── Banner ────────────────────────────────────────────────────────────────────
echo -e "${BOLD}${CYAN}" echo -e "${BOLD}${CYAN}"
echo "╔══════════════════════════════════════════════════════════╗" echo "============================================================"
echo "║ NetBird External Relay Server Setup Wizard ║" echo " NetBird External Relay Server Setup Wizard"
echo "╚══════════════════════════════════════════════════════════╝${RESET}" echo "============================================================${RESET}"
echo "" echo ""
echo "This script generates relay.env and docker-compose.yml" echo "This script generates relay.env and docker-compose.yml"
echo "for one or more NetBird relay servers." echo "for one or more NetBird relay servers."
@@ -210,9 +242,9 @@ for (( i=1; i<=RELAY_COUNT; i++ )); do
# ── Domain ──────────────────────────────────────────────────────────────── # ── Domain ────────────────────────────────────────────────────────────────
while true; do while true; do
ask DOMAIN " Domain name (e.g. relay-us.example.com)" "${EX_DOMAIN}" ask DOMAIN " Domain name or IP (e.g. relay-us.example.com / 203.0.113.10)" "${EX_DOMAIN}"
if validate_domain "$DOMAIN"; then break if validate_domain_or_ip "$DOMAIN"; then break
else warn " That doesn't look like a valid domain. Try again."; fi else warn " That doesn't look like a valid domain or IPv4 address. Try again."; fi
done done
RELAY_DOMAINS+=("$DOMAIN") RELAY_DOMAINS+=("$DOMAIN")
@@ -237,7 +269,14 @@ for (( i=1; i<=RELAY_COUNT; i++ )); do
echo " 1) Let's Encrypt (automatic — server needs port 80/tcp open)" echo " 1) Let's Encrypt (automatic — server needs port 80/tcp open)"
echo " 2) Existing certificates (wildcard / own CA)" echo " 2) Existing certificates (wildcard / own CA)"
echo " 3) Self-signed certificate (generated by this script)" echo " 3) Self-signed certificate (generated by this script)"
ask TLS_MODE " Choose [1/2/3]" "${EX_TLS_MODE:-1}" echo " 4) Paste certificate and private key"
ask TLS_MODE " Choose [1/2/3/4]" "${EX_TLS_MODE:-1}"
if [[ "$TLS_MODE" == "1" ]] && validate_ipv4 "$DOMAIN"; then
warn " Let's Encrypt does not issue certificates for raw IP addresses."
warn " Choose mode 2, 3, or 4 for IP-based deployments."
ask TLS_MODE " Re-choose TLS mode [2/3/4]" "3"
fi
if [[ "$TLS_MODE" == "1" ]]; then if [[ "$TLS_MODE" == "1" ]]; then
ask LE_EMAIL " Let's Encrypt email" "${EX_LE_EMAIL}" ask LE_EMAIL " Let's Encrypt email" "${EX_LE_EMAIL}"
@@ -255,6 +294,15 @@ for (( i=1; i<=RELAY_COUNT; i++ )); do
ask CERT_FILE " Cert file path inside container" "${EX_CERT_FILE:-/certs/fullchain.pem}" ask CERT_FILE " Cert file path inside container" "${EX_CERT_FILE:-/certs/fullchain.pem}"
ask KEY_FILE " Key file path inside container" "${EX_KEY_FILE:-/certs/privkey.pem}" ask KEY_FILE " Key file path inside container" "${EX_KEY_FILE:-/certs/privkey.pem}"
LE_EMAIL=""; LE_DATA_DIR="" LE_EMAIL=""; LE_DATA_DIR=""
elif [[ "$TLS_MODE" == "4" ]]; then
echo ""
echo " ── Pasted certificate details ──"
CERT_HOST_PATH="${RELAY_DIR}/certs"
CERT_FILE="/certs/fullchain.pem"
KEY_FILE="/certs/privkey.pem"
LE_EMAIL=""; LE_DATA_DIR=""
read_multiline_block PASTED_CERT " Paste the certificate PEM" "END_CERT"
read_multiline_block PASTED_KEY " Paste the private key PEM" "END_KEY"
else else
echo "" echo ""
echo " ── Self-signed certificate details ──" echo " ── Self-signed certificate details ──"
@@ -266,10 +314,18 @@ for (( i=1; i<=RELAY_COUNT; i++ )); do
ask SS_ST " State / Province (ST)" "" ask SS_ST " State / Province (ST)" ""
ask SS_L " Locality / City (L)" "" ask SS_L " Locality / City (L)" ""
SS_SANS="DNS:${DOMAIN}" if validate_ipv4 "$DOMAIN"; then
SS_SANS="IP:${DOMAIN}"
else
SS_SANS="DNS:${DOMAIN}"
fi
echo "" echo ""
echo " Subject Alternative Names (SANs):" echo " Subject Alternative Names (SANs):"
echo " DNS:${DOMAIN} is included automatically." if validate_ipv4 "$DOMAIN"; then
echo " IP:${DOMAIN} is included automatically."
else
echo " DNS:${DOMAIN} is included automatically."
fi
echo " Add extra SANs one by one (IP:x.x.x.x or DNS:other.example.com)." echo " Add extra SANs one by one (IP:x.x.x.x or DNS:other.example.com)."
echo " Press Enter on an empty line when done." echo " Press Enter on an empty line when done."
while true; do while true; do
@@ -318,14 +374,22 @@ for (( i=1; i<=RELAY_COUNT; i++ )); do
KEY_FILE="/certs/privkey.pem" KEY_FILE="/certs/privkey.pem"
LE_EMAIL=""; LE_DATA_DIR="" LE_EMAIL=""; LE_DATA_DIR=""
fi fi
# For self-signed mode, certs live inside the relay dir # For generated or pasted certs, certs live inside the relay dir
[[ "$TLS_MODE" == "3" ]] && CERT_HOST_PATH="${RELAY_DIR}/certs" [[ "$TLS_MODE" == "3" || "$TLS_MODE" == "4" ]] && CERT_HOST_PATH="${RELAY_DIR}/certs"
RELAY_DIRS+=("$RELAY_DIR") RELAY_DIRS+=("$RELAY_DIR")
RELAY_LISTEN_PORTS+=("$LISTEN_PORT") RELAY_LISTEN_PORTS+=("$LISTEN_PORT")
RELAY_STUN_PORTS_LIST+=("$STUN_PORTS") RELAY_STUN_PORTS_LIST+=("$STUN_PORTS")
RELAY_TLS_MODES+=("$TLS_MODE") RELAY_TLS_MODES+=("$TLS_MODE")
RELAY_ENABLE_STUN+=("$ENABLE_STUN") RELAY_ENABLE_STUN+=("$ENABLE_STUN")
if [[ "$TLS_MODE" == "3" ]]; then
TLS_LABEL="Self-signed certificate"
elif [[ "$TLS_MODE" == "4" ]]; then
TLS_LABEL="Pasted certificate"
else
TLS_LABEL="Existing certificates"
fi
# ── Build relay.env ─────────────────────────────────────────────────────── # ── Build relay.env ───────────────────────────────────────────────────────
ENV_FILE="${RELAY_DIR}/relay.env" ENV_FILE="${RELAY_DIR}/relay.env"
COMPOSE_FILE="${RELAY_DIR}/docker-compose.yml" COMPOSE_FILE="${RELAY_DIR}/docker-compose.yml"
@@ -349,7 +413,7 @@ NB_LETSENCRYPT_DATA_DIR=${LE_DATA_DIR}
" "
else else
ENV_CONTENT+=" ENV_CONTENT+="
# TLS — $([ "$TLS_MODE" == "3" ] && echo "Self-signed certificate" || echo "Existing certificates") # TLS — ${TLS_LABEL}
NB_TLS_CERT_FILE=${CERT_FILE} NB_TLS_CERT_FILE=${CERT_FILE}
NB_TLS_KEY_FILE=${KEY_FILE} NB_TLS_KEY_FILE=${KEY_FILE}
" "
@@ -442,6 +506,18 @@ volumes:
echo " -subj \"${SS_SUBJ}\" \\" echo " -subj \"${SS_SUBJ}\" \\"
echo " -addext \"subjectAltName=${SS_SANS}\"" echo " -addext \"subjectAltName=${SS_SANS}\""
fi fi
if [[ "$TLS_MODE" == "4" ]]; then
echo ""
echo -e "${BOLD}▶ Pasted certificate (would write):${RESET}"
echo "────────────────────────────────────────"
echo "mkdir -p ${CERT_HOST_PATH}"
echo "cat > ${CERT_HOST_PATH}/fullchain.pem <<'EOF_CERT'"
echo "$PASTED_CERT"
echo "EOF_CERT"
echo "cat > ${CERT_HOST_PATH}/privkey.pem <<'EOF_KEY'"
echo "$PASTED_KEY"
echo "EOF_KEY"
fi
else else
mkdir -p "$RELAY_DIR" mkdir -p "$RELAY_DIR"
printf '%s' "$ENV_CONTENT" > "$ENV_FILE" printf '%s' "$ENV_CONTENT" > "$ENV_FILE"
@@ -492,6 +568,15 @@ volumes:
info " SANs : ${SS_SANS}" info " SANs : ${SS_SANS}"
info " Valid for : ${SS_DAYS} days" info " Valid for : ${SS_DAYS} days"
fi fi
if [[ "$TLS_MODE" == "4" ]]; then
info " Writing pasted certificate ..."
mkdir -p "${CERT_HOST_PATH}"
printf '%s' "${PASTED_CERT}" > "${CERT_HOST_PATH}/fullchain.pem"
printf '%s' "${PASTED_KEY}" > "${CERT_HOST_PATH}/privkey.pem"
chmod 600 "${CERT_HOST_PATH}/privkey.pem"
success " Certificate : ${CERT_HOST_PATH}/fullchain.pem"
success " Private key : ${CERT_HOST_PATH}/privkey.pem"
fi
fi fi
fi fi