From d3bf52f884ed4d7f9676da5436786f1e2f0fc2d0 Mon Sep 17 00:00:00 2001 From: Arkylin Date: Wed, 22 Jul 2026 20:18:59 +0800 Subject: [PATCH] Add IP and pasted certificate support --- README.md | 11 +++-- setup-relay.sh | 109 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 105 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 78898c5..bc12720 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ 每台 Relay 服务器需满足: - Linux,≥ 1 CPU / 1 GB RAM -- 公网 IP,且有域名解析指向该 IP +- 公网 IP,且有域名解析指向该 IP;脚本里的 Relay 地址也可以直接填写 IP - 已安装 Docker(含 `docker compose`) - 以下端口按实际配置放行(脚本会在最后动态列出): - `80/tcp` — Let's Encrypt HTTP challenge(仅自动证书模式需要) @@ -71,7 +71,8 @@ Step 3.N · Relay Server #N(每台重复) 1) Let's Encrypt — 自动签发,需开放 80/tcp 2) 已有证书 — 输入宿主机目录及容器内路径(均可自定义) 3) 自签证书 — 脚本调用 openssl 生成,支持完整 Subject 字段、 - 额外 SAN(DNS/IP)、有效期、RSA/ECC 及密钥长度选择 + 额外 SAN(DNS/IP)、有效期、RSA/ECC 及密钥长度选择 + 4) 粘贴证书 — 直接粘贴 PEM 证书和私钥内容,由脚本写入 certs 目录 Step 4 · Main Server config.yaml Snippet → 打印带占位符的 config.yaml 片段(不输出真实域名和密钥) @@ -89,7 +90,7 @@ Step 6 · Firewall Configuration(可选,可跳过) 脚本支持在已有部署上重新运行以修改配置。Step 3 中先询问输出目录,若该目录下存在 `relay.env`,脚本会自动解析并将以下字段作为默认值填入后续输入框,直接回车即保留原值,输入新值则覆盖: -- 域名、监听端口、日志级别 +- 域名或 IP、监听端口、日志级别 - STUN 开关及端口 - TLS 模式、LE 邮箱、证书路径 @@ -170,6 +171,10 @@ curl -v https://relay.example.com/ 证书输出到 `<输出目录>/certs/`,私钥权限自动设为 `600`。若系统未安装 `openssl`,脚本会打印等效命令供手动执行。 +**粘贴证书** + +选择该模式后,脚本会提示分别粘贴证书 PEM 和私钥 PEM,保存到 `<输出目录>/certs/fullchain.pem` 与 `<输出目录>/certs/privkey.pem`,并自动挂载到容器中。 + ## 主服务器 config.yaml 配置 Step 4 输出带占位符的配置片段,需在主服务器上手动填入真实域名和密钥: diff --git a/setup-relay.sh b/setup-relay.sh index 4ab61e4..88e3ad7 100644 --- a/setup-relay.sh +++ b/setup-relay.sh @@ -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,}$ ]] } +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 [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 ─────────────────────────────────────────────────────────────── DRY_RUN=false OUTPUT_DIR="." @@ -120,9 +152,9 @@ mkdir -p "$OUTPUT_DIR" # ── Banner ──────────────────────────────────────────────────────────────────── echo -e "${BOLD}${CYAN}" -echo "╔══════════════════════════════════════════════════════════╗" -echo "║ NetBird External Relay Server Setup Wizard ║" -echo "╚══════════════════════════════════════════════════════════╝${RESET}" +echo "============================================================" +echo " NetBird External Relay Server Setup Wizard" +echo "============================================================${RESET}" echo "" echo "This script generates relay.env and docker-compose.yml" echo "for one or more NetBird relay servers." @@ -210,9 +242,9 @@ for (( i=1; i<=RELAY_COUNT; i++ )); do # ── Domain ──────────────────────────────────────────────────────────────── while true; do - ask DOMAIN " Domain name (e.g. relay-us.example.com)" "${EX_DOMAIN}" - if validate_domain "$DOMAIN"; then break - else warn " That doesn't look like a valid domain. Try again."; fi + ask DOMAIN " Domain name or IP (e.g. relay-us.example.com / 203.0.113.10)" "${EX_DOMAIN}" + if validate_domain_or_ip "$DOMAIN"; then break + else warn " That doesn't look like a valid domain or IPv4 address. Try again."; fi done 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 " 2) Existing certificates (wildcard / own CA)" 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 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 KEY_FILE " Key file path inside container" "${EX_KEY_FILE:-/certs/privkey.pem}" 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 echo "" 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_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 " 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 " Press Enter on an empty line when done." while true; do @@ -318,14 +374,22 @@ for (( i=1; i<=RELAY_COUNT; i++ )); do KEY_FILE="/certs/privkey.pem" LE_EMAIL=""; LE_DATA_DIR="" fi - # For self-signed mode, certs live inside the relay dir - [[ "$TLS_MODE" == "3" ]] && CERT_HOST_PATH="${RELAY_DIR}/certs" + # For generated or pasted certs, certs live inside the relay dir + [[ "$TLS_MODE" == "3" || "$TLS_MODE" == "4" ]] && CERT_HOST_PATH="${RELAY_DIR}/certs" RELAY_DIRS+=("$RELAY_DIR") RELAY_LISTEN_PORTS+=("$LISTEN_PORT") RELAY_STUN_PORTS_LIST+=("$STUN_PORTS") RELAY_TLS_MODES+=("$TLS_MODE") 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 ─────────────────────────────────────────────────────── ENV_FILE="${RELAY_DIR}/relay.env" COMPOSE_FILE="${RELAY_DIR}/docker-compose.yml" @@ -349,7 +413,7 @@ NB_LETSENCRYPT_DATA_DIR=${LE_DATA_DIR} " else ENV_CONTENT+=" -# TLS — $([ "$TLS_MODE" == "3" ] && echo "Self-signed certificate" || echo "Existing certificates") +# TLS — ${TLS_LABEL} NB_TLS_CERT_FILE=${CERT_FILE} NB_TLS_KEY_FILE=${KEY_FILE} " @@ -442,6 +506,18 @@ volumes: echo " -subj \"${SS_SUBJ}\" \\" echo " -addext \"subjectAltName=${SS_SANS}\"" 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 mkdir -p "$RELAY_DIR" printf '%s' "$ENV_CONTENT" > "$ENV_FILE" @@ -492,6 +568,15 @@ volumes: info " SANs : ${SS_SANS}" info " Valid for : ${SS_DAYS} days" 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