2026-03-18 20:35:51 +08:00
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# NetBird External Relay Server Setup Script
|
|
|
|
|
|
# Interactively generates relay.env and docker-compose.yml for one or more
|
|
|
|
|
|
# relay servers. Run this script ON each relay server, or use --dry-run to
|
|
|
|
|
|
# preview the generated files locally.
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
|
|
# ── Colours ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
|
|
|
|
|
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
|
|
|
|
|
|
|
|
|
|
|
|
info() { echo -e "${CYAN}[INFO]${RESET} $*"; }
|
|
|
|
|
|
success() { echo -e "${GREEN}[OK]${RESET} $*"; }
|
|
|
|
|
|
warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
|
|
|
|
|
|
error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
|
|
|
|
|
|
header() { echo -e "\n${BOLD}${CYAN}══ $* ══${RESET}"; }
|
|
|
|
|
|
|
|
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
ask() {
|
|
|
|
|
|
# ask <var_name> <prompt> [default]
|
|
|
|
|
|
local var="$1" prompt="$2" default="${3:-}"
|
|
|
|
|
|
local display_default=""
|
|
|
|
|
|
[[ -n "$default" ]] && display_default=" [${default}]"
|
|
|
|
|
|
while true; do
|
|
|
|
|
|
read -rp "$(echo -e "${BOLD}${prompt}${display_default}: ${RESET}")" value
|
|
|
|
|
|
value="${value:-$default}"
|
|
|
|
|
|
if [[ -n "$value" ]]; then
|
|
|
|
|
|
printf -v "$var" '%s' "$value"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
warn "This field is required."
|
|
|
|
|
|
done
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ask_yn() {
|
|
|
|
|
|
# ask_yn <prompt> <default: y|n> → returns 0=yes 1=no
|
|
|
|
|
|
local prompt="$1" default="${2:-y}"
|
|
|
|
|
|
local opts="[Y/n]"; [[ "$default" == "n" ]] && opts="[y/N]"
|
|
|
|
|
|
read -rp "$(echo -e "${BOLD}${prompt} ${opts}: ${RESET}")" reply
|
|
|
|
|
|
reply="${reply:-$default}"
|
|
|
|
|
|
[[ "${reply,,}" == "y" ]]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ask_secret() {
|
|
|
|
|
|
local var="$1" prompt="$2"
|
|
|
|
|
|
while true; do
|
|
|
|
|
|
read -rsp "$(echo -e "${BOLD}${prompt}: ${RESET}")" value; echo
|
|
|
|
|
|
if [[ -n "$value" ]]; then
|
|
|
|
|
|
printf -v "$var" '%s' "$value"
|
|
|
|
|
|
return
|
|
|
|
|
|
fi
|
|
|
|
|
|
warn "Secret cannot be empty."
|
|
|
|
|
|
done
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
generate_secret() {
|
|
|
|
|
|
if command -v openssl &>/dev/null; then
|
|
|
|
|
|
openssl rand -base64 32
|
|
|
|
|
|
else
|
|
|
|
|
|
head -c 32 /dev/urandom | base64
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
validate_domain() {
|
|
|
|
|
|
# Very basic domain sanity check
|
|
|
|
|
|
[[ "$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,}$ ]]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# ── Arg parsing ───────────────────────────────────────────────────────────────
|
|
|
|
|
|
DRY_RUN=false
|
|
|
|
|
|
OUTPUT_DIR="."
|
|
|
|
|
|
for arg in "$@"; do
|
|
|
|
|
|
case "$arg" in
|
|
|
|
|
|
--dry-run) DRY_RUN=true ;;
|
|
|
|
|
|
--output=*) OUTPUT_DIR="${arg#--output=}" ;;
|
|
|
|
|
|
-h|--help)
|
|
|
|
|
|
echo "Usage: $0 [--dry-run] [--output=<dir>]"
|
|
|
|
|
|
echo " --dry-run Print generated files to stdout instead of writing them"
|
|
|
|
|
|
echo " --output=DIR Write files to DIR instead of current directory"
|
|
|
|
|
|
exit 0 ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
mkdir -p "$OUTPUT_DIR"
|
|
|
|
|
|
|
|
|
|
|
|
# ── Banner ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
echo -e "${BOLD}${CYAN}"
|
|
|
|
|
|
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."
|
|
|
|
|
|
$DRY_RUN && warn "DRY-RUN mode: files will be printed, not written."
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# STEP 1 – Auth secret
|
|
|
|
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
header "Step 1 · Authentication Secret"
|
|
|
|
|
|
echo "All relay servers AND your main NetBird server must share the same secret."
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
|
|
if ask_yn "Generate a new random secret automatically?" "y"; then
|
|
|
|
|
|
AUTH_SECRET="$(generate_secret)"
|
|
|
|
|
|
success "Generated secret: ${BOLD}${AUTH_SECRET}${RESET}"
|
|
|
|
|
|
warn "Save this — you'll need it for every relay and your main server config."
|
|
|
|
|
|
else
|
2026-03-18 20:53:24 +08:00
|
|
|
|
ask AUTH_SECRET "Paste your existing shared secret" ""
|
2026-03-18 20:35:51 +08:00
|
|
|
|
success "Using supplied secret."
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# STEP 2 – Number of relay servers
|
|
|
|
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
header "Step 2 · How many relay servers?"
|
|
|
|
|
|
ask RELAY_COUNT "Number of relay servers to configure" "1"
|
|
|
|
|
|
if ! [[ "$RELAY_COUNT" =~ ^[1-9][0-9]*$ ]]; then
|
|
|
|
|
|
error "Please enter a positive integer."; exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# STEP 3 – Per-relay configuration
|
|
|
|
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
declare -a RELAY_DOMAINS=()
|
|
|
|
|
|
declare -a RELAY_DIRS=()
|
|
|
|
|
|
declare -a RELAY_LISTEN_PORTS=()
|
|
|
|
|
|
declare -a RELAY_STUN_PORTS_LIST=()
|
|
|
|
|
|
declare -a RELAY_TLS_MODES=()
|
|
|
|
|
|
declare -a RELAY_ENABLE_STUN=()
|
|
|
|
|
|
|
|
|
|
|
|
for (( i=1; i<=RELAY_COUNT; i++ )); do
|
|
|
|
|
|
|
|
|
|
|
|
header "Step 3.$i · Relay Server #${i}"
|
|
|
|
|
|
|
|
|
|
|
|
# ── Domain ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
while true; do
|
|
|
|
|
|
ask DOMAIN " Domain name (e.g. relay-us.example.com)" ""
|
|
|
|
|
|
if validate_domain "$DOMAIN"; then break
|
|
|
|
|
|
else warn " That doesn't look like a valid domain. Try again."; fi
|
|
|
|
|
|
done
|
|
|
|
|
|
RELAY_DOMAINS+=("$DOMAIN")
|
|
|
|
|
|
|
|
|
|
|
|
# ── Listen & exposed ports ────────────────────────────────────────────────
|
|
|
|
|
|
ask LISTEN_PORT " HTTPS listen port" "443"
|
|
|
|
|
|
ask LOG_LEVEL " Log level (debug/info/warn/error)" "info"
|
|
|
|
|
|
|
|
|
|
|
|
# ── STUN ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
if ask_yn " Enable embedded STUN server?" "y"; then
|
|
|
|
|
|
ENABLE_STUN=true
|
|
|
|
|
|
ask STUN_PORTS " STUN port(s) — comma-separated for multiple (e.g. 3478,3479)" "3478"
|
|
|
|
|
|
else
|
|
|
|
|
|
ENABLE_STUN=false
|
|
|
|
|
|
STUN_PORTS=""
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ── TLS mode ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo " TLS mode:"
|
|
|
|
|
|
echo " 1) Let's Encrypt (automatic — server needs port 80/tcp open)"
|
|
|
|
|
|
echo " 2) Existing certificates (wildcard / own CA)"
|
2026-03-18 20:50:09 +08:00
|
|
|
|
echo " 3) Self-signed certificate (generated by this script)"
|
|
|
|
|
|
ask TLS_MODE " Choose [1/2/3]" "1"
|
2026-03-18 20:35:51 +08:00
|
|
|
|
|
|
|
|
|
|
if [[ "$TLS_MODE" == "1" ]]; then
|
|
|
|
|
|
ask LE_EMAIL " Let's Encrypt email" ""
|
|
|
|
|
|
LE_DATA_DIR="/data/letsencrypt"
|
2026-03-18 20:50:09 +08:00
|
|
|
|
CERT_HOST_PATH=""; CERT_FILE=""; KEY_FILE=""; SELFSIGN_DAYS=""
|
|
|
|
|
|
elif [[ "$TLS_MODE" == "2" ]]; then
|
2026-03-18 20:35:51 +08:00
|
|
|
|
ask CERT_HOST_PATH " Host path to certs directory" "/opt/1panel/www/sites/${DOMAIN}/ssl"
|
|
|
|
|
|
ask CERT_FILE " Cert file path inside container" "/certs/fullchain.pem"
|
|
|
|
|
|
ask KEY_FILE " Key file path inside container" "/certs/privkey.pem"
|
2026-03-18 20:50:09 +08:00
|
|
|
|
LE_EMAIL=""; LE_DATA_DIR=""; SELFSIGN_DAYS=""
|
|
|
|
|
|
else
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo " ── Self-signed certificate details ──"
|
|
|
|
|
|
|
|
|
|
|
|
# Subject fields
|
|
|
|
|
|
ask SS_CN " Common Name (CN)" "${DOMAIN}"
|
|
|
|
|
|
ask SS_O " Organization (O)" ""
|
|
|
|
|
|
ask SS_OU " Organizational Unit (OU)" ""
|
|
|
|
|
|
ask SS_C " Country (C, 2-letter ISO)" ""
|
|
|
|
|
|
ask SS_ST " State / Province (ST)" ""
|
|
|
|
|
|
ask SS_L " Locality / City (L)" ""
|
|
|
|
|
|
|
|
|
|
|
|
# SANs — domain always included, allow adding more
|
|
|
|
|
|
SS_SANS="DNS:${DOMAIN}"
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo " Subject Alternative Names (SANs):"
|
|
|
|
|
|
echo " DNS:${DOMAIN} is included automatically."
|
|
|
|
|
|
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
|
|
|
|
|
|
read -rp "$(echo -e "${BOLD} Extra SAN (or Enter to finish): ${RESET}")" _san
|
|
|
|
|
|
[[ -z "$_san" ]] && break
|
|
|
|
|
|
SS_SANS+=",${_san}"
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
# Validity
|
|
|
|
|
|
ask SS_DAYS " Certificate validity (days)" "3650"
|
|
|
|
|
|
|
|
|
|
|
|
# Key type
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo " Key type:"
|
|
|
|
|
|
echo " 1) RSA"
|
|
|
|
|
|
echo " 2) ECC (ECDSA)"
|
|
|
|
|
|
ask SS_KEY_TYPE " Choose [1/2]" "1"
|
|
|
|
|
|
|
|
|
|
|
|
if [[ "$SS_KEY_TYPE" == "1" ]]; then
|
|
|
|
|
|
echo " RSA key size:"
|
|
|
|
|
|
echo " 1) 2048 bit"
|
|
|
|
|
|
echo " 2) 4096 bit"
|
|
|
|
|
|
ask SS_KEY_SIZE " Choose [1/2]" "2"
|
|
|
|
|
|
[[ "$SS_KEY_SIZE" == "1" ]] && SS_NEWKEY="rsa:2048" || SS_NEWKEY="rsa:4096"
|
|
|
|
|
|
else
|
|
|
|
|
|
echo " ECC curve:"
|
|
|
|
|
|
echo " 1) P-256 (prime256v1)"
|
|
|
|
|
|
echo " 2) P-384 (secp384r1)"
|
|
|
|
|
|
echo " 3) P-521 (secp521r1)"
|
|
|
|
|
|
ask SS_CURVE " Choose [1/2/3]" "1"
|
|
|
|
|
|
case "$SS_CURVE" in
|
|
|
|
|
|
1) SS_NEWKEY="ec" ; SS_PKEYOPT="ec_paramgen_curve:P-256" ;;
|
|
|
|
|
|
2) SS_NEWKEY="ec" ; SS_PKEYOPT="ec_paramgen_curve:P-384" ;;
|
|
|
|
|
|
3) SS_NEWKEY="ec" ; SS_PKEYOPT="ec_paramgen_curve:P-521" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# Build subject string (only include non-empty fields)
|
|
|
|
|
|
SS_SUBJ=""
|
|
|
|
|
|
[[ -n "$SS_C" ]] && SS_SUBJ+="/C=${SS_C}"
|
|
|
|
|
|
[[ -n "$SS_ST" ]] && SS_SUBJ+="/ST=${SS_ST}"
|
|
|
|
|
|
[[ -n "$SS_L" ]] && SS_SUBJ+="/L=${SS_L}"
|
|
|
|
|
|
[[ -n "$SS_O" ]] && SS_SUBJ+="/O=${SS_O}"
|
|
|
|
|
|
[[ -n "$SS_OU" ]] && SS_SUBJ+="/OU=${SS_OU}"
|
|
|
|
|
|
SS_SUBJ+="/CN=${SS_CN}"
|
|
|
|
|
|
|
|
|
|
|
|
CERT_HOST_PATH="" # filled in after RELAY_DIR is known
|
|
|
|
|
|
CERT_FILE="/certs/fullchain.pem"
|
|
|
|
|
|
KEY_FILE="/certs/privkey.pem"
|
2026-03-18 20:35:51 +08:00
|
|
|
|
LE_EMAIL=""; LE_DATA_DIR=""
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ── Output directory ──────────────────────────────────────────────────────
|
2026-03-18 20:53:24 +08:00
|
|
|
|
_DEFAULT_RELAY_DIR="/opt/netbird-relay"
|
|
|
|
|
|
# If the default dir already has relay files, surface it as the default so
|
|
|
|
|
|
# the user can confirm with Enter rather than retyping the path.
|
|
|
|
|
|
if [[ -f "${_DEFAULT_RELAY_DIR}/relay.env" || -f "${_DEFAULT_RELAY_DIR}/docker-compose.yml" ]]; then
|
|
|
|
|
|
warn " ${_DEFAULT_RELAY_DIR} already contains relay files (will be overwritten if confirmed)."
|
|
|
|
|
|
fi
|
|
|
|
|
|
ask RELAY_DIR " Output directory for this relay's files" "${_DEFAULT_RELAY_DIR}"
|
2026-03-18 20:50:09 +08:00
|
|
|
|
# For self-signed mode, certs live inside the relay dir
|
|
|
|
|
|
[[ "$TLS_MODE" == "3" ]] && CERT_HOST_PATH="${RELAY_DIR}/certs"
|
2026-03-18 20:35:51 +08:00
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
|
|
# ── Build relay.env ───────────────────────────────────────────────────────
|
|
|
|
|
|
ENV_FILE="${RELAY_DIR}/relay.env"
|
|
|
|
|
|
COMPOSE_FILE="${RELAY_DIR}/docker-compose.yml"
|
|
|
|
|
|
|
|
|
|
|
|
ENV_CONTENT="# NetBird Relay — ${DOMAIN}
|
|
|
|
|
|
# Generated by setup-relay.sh on $(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
|
|
|
|
|
# ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
NB_LOG_LEVEL=${LOG_LEVEL}
|
|
|
|
|
|
NB_LISTEN_ADDRESS=:${LISTEN_PORT}
|
|
|
|
|
|
NB_EXPOSED_ADDRESS=rels://${DOMAIN}:${LISTEN_PORT}
|
|
|
|
|
|
NB_AUTH_SECRET=${AUTH_SECRET}
|
|
|
|
|
|
"
|
|
|
|
|
|
|
|
|
|
|
|
if [[ "$TLS_MODE" == "1" ]]; then
|
|
|
|
|
|
ENV_CONTENT+="
|
|
|
|
|
|
# TLS — Let's Encrypt (automatic certificate provisioning)
|
|
|
|
|
|
NB_LETSENCRYPT_DOMAINS=${DOMAIN}
|
|
|
|
|
|
NB_LETSENCRYPT_EMAIL=${LE_EMAIL}
|
|
|
|
|
|
NB_LETSENCRYPT_DATA_DIR=${LE_DATA_DIR}
|
|
|
|
|
|
"
|
|
|
|
|
|
else
|
|
|
|
|
|
ENV_CONTENT+="
|
2026-03-18 20:50:09 +08:00
|
|
|
|
# TLS — $([ "$TLS_MODE" == "3" ] && echo "Self-signed certificate" || echo "Existing certificates")
|
2026-03-18 20:35:51 +08:00
|
|
|
|
NB_TLS_CERT_FILE=${CERT_FILE}
|
|
|
|
|
|
NB_TLS_KEY_FILE=${KEY_FILE}
|
|
|
|
|
|
"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
if $ENABLE_STUN; then
|
|
|
|
|
|
ENV_CONTENT+="
|
|
|
|
|
|
# Embedded STUN
|
|
|
|
|
|
NB_ENABLE_STUN=true
|
|
|
|
|
|
NB_STUN_PORTS=${STUN_PORTS}
|
|
|
|
|
|
"
|
|
|
|
|
|
else
|
|
|
|
|
|
ENV_CONTENT+="
|
|
|
|
|
|
# Embedded STUN disabled
|
|
|
|
|
|
NB_ENABLE_STUN=false
|
|
|
|
|
|
"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ── Build STUN port mappings ───────────────────────────────────────────────
|
|
|
|
|
|
STUN_PORT_LINES=""
|
|
|
|
|
|
if $ENABLE_STUN && [[ -n "$STUN_PORTS" ]]; then
|
|
|
|
|
|
IFS=',' read -ra SPORT_ARRAY <<< "$STUN_PORTS"
|
|
|
|
|
|
for SP in "${SPORT_ARRAY[@]}"; do
|
|
|
|
|
|
SP="${SP// /}"
|
|
|
|
|
|
STUN_PORT_LINES+=" - '${SP}:${SP}/udp'"$'\n'
|
|
|
|
|
|
done
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ── Build volume section ───────────────────────────────────────────────────
|
|
|
|
|
|
if [[ "$TLS_MODE" == "1" ]]; then
|
|
|
|
|
|
VOLUME_LINES=" - relay_data:/data"
|
|
|
|
|
|
else
|
|
|
|
|
|
VOLUME_LINES=" - ${CERT_HOST_PATH}:$(dirname "${CERT_FILE}"):ro
|
|
|
|
|
|
- relay_data:/data"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# ── Build docker-compose.yml ──────────────────────────────────────────────
|
|
|
|
|
|
if [[ "$TLS_MODE" == "1" ]]; then
|
|
|
|
|
|
LE_PORT_LINE=" - '80:80'"$'\n'
|
|
|
|
|
|
else
|
|
|
|
|
|
LE_PORT_LINE=""
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
COMPOSE_CONTENT="# NetBird Relay — ${DOMAIN}
|
|
|
|
|
|
# Generated by setup-relay.sh on $(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
|
|
|
|
|
# ---------------------------------------------------------------
|
|
|
|
|
|
services:
|
|
|
|
|
|
relay:
|
|
|
|
|
|
image: netbirdio/relay:latest
|
|
|
|
|
|
container_name: netbird-relay
|
|
|
|
|
|
restart: unless-stopped
|
|
|
|
|
|
ports:
|
|
|
|
|
|
- '${LISTEN_PORT}:${LISTEN_PORT}'
|
|
|
|
|
|
${LE_PORT_LINE}${STUN_PORT_LINES} env_file:
|
|
|
|
|
|
- relay.env
|
|
|
|
|
|
volumes:
|
|
|
|
|
|
${VOLUME_LINES}
|
|
|
|
|
|
logging:
|
|
|
|
|
|
driver: \"json-file\"
|
|
|
|
|
|
options:
|
|
|
|
|
|
max-size: \"500m\"
|
|
|
|
|
|
max-file: \"2\"
|
|
|
|
|
|
|
|
|
|
|
|
volumes:
|
|
|
|
|
|
relay_data:
|
|
|
|
|
|
"
|
|
|
|
|
|
|
|
|
|
|
|
# ── Write or print ────────────────────────────────────────────────────────
|
|
|
|
|
|
if $DRY_RUN; then
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo -e "${BOLD}▶ ${ENV_FILE}${RESET}"
|
|
|
|
|
|
echo "────────────────────────────────────────"
|
|
|
|
|
|
echo "$ENV_CONTENT"
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo -e "${BOLD}▶ ${COMPOSE_FILE}${RESET}"
|
|
|
|
|
|
echo "────────────────────────────────────────"
|
|
|
|
|
|
echo "$COMPOSE_CONTENT"
|
2026-03-18 20:50:09 +08:00
|
|
|
|
if [[ "$TLS_MODE" == "3" ]]; then
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo -e "${BOLD}▶ Self-signed cert (would run):${RESET}"
|
|
|
|
|
|
echo "────────────────────────────────────────"
|
|
|
|
|
|
echo "mkdir -p ${CERT_HOST_PATH}"
|
|
|
|
|
|
if [[ "$SS_KEY_TYPE" == "1" ]]; then
|
|
|
|
|
|
echo "openssl req -x509 -newkey ${SS_NEWKEY} -sha256 -days ${SS_DAYS} -nodes \\"
|
|
|
|
|
|
else
|
|
|
|
|
|
echo "openssl req -x509 -newkey ${SS_NEWKEY} -pkeyopt ${SS_PKEYOPT} -sha256 -days ${SS_DAYS} -nodes \\"
|
|
|
|
|
|
fi
|
|
|
|
|
|
echo " -keyout ${CERT_HOST_PATH}/privkey.pem \\"
|
|
|
|
|
|
echo " -out ${CERT_HOST_PATH}/fullchain.pem \\"
|
|
|
|
|
|
echo " -subj \"${SS_SUBJ}\" \\"
|
|
|
|
|
|
echo " -addext \"subjectAltName=${SS_SANS}\""
|
|
|
|
|
|
fi
|
2026-03-18 20:35:51 +08:00
|
|
|
|
else
|
|
|
|
|
|
mkdir -p "$RELAY_DIR"
|
2026-03-18 20:50:09 +08:00
|
|
|
|
printf '%s' "$ENV_CONTENT" > "$ENV_FILE"
|
2026-03-18 20:35:51 +08:00
|
|
|
|
printf '%s' "$COMPOSE_CONTENT" > "$COMPOSE_FILE"
|
2026-03-18 20:50:09 +08:00
|
|
|
|
chmod 600 "$ENV_FILE"
|
2026-03-18 20:35:51 +08:00
|
|
|
|
success " Written: ${ENV_FILE}"
|
|
|
|
|
|
success " Written: ${COMPOSE_FILE}"
|
2026-03-18 20:50:09 +08:00
|
|
|
|
|
|
|
|
|
|
if [[ "$TLS_MODE" == "3" ]]; then
|
|
|
|
|
|
if ! command -v openssl &>/dev/null; then
|
|
|
|
|
|
error " openssl not found — cannot generate self-signed certificate."
|
|
|
|
|
|
error " Install openssl and re-run, or run the following manually:"
|
|
|
|
|
|
if [[ "$SS_KEY_TYPE" == "1" ]]; then
|
|
|
|
|
|
error " openssl req -x509 -newkey ${SS_NEWKEY} -sha256 -days ${SS_DAYS} -nodes \\"
|
|
|
|
|
|
else
|
|
|
|
|
|
error " openssl req -x509 -newkey ${SS_NEWKEY} -pkeyopt ${SS_PKEYOPT} -sha256 -days ${SS_DAYS} -nodes \\"
|
|
|
|
|
|
fi
|
|
|
|
|
|
error " -keyout ${CERT_HOST_PATH}/privkey.pem \\"
|
|
|
|
|
|
error " -out ${CERT_HOST_PATH}/fullchain.pem \\"
|
|
|
|
|
|
error " -subj \"${SS_SUBJ}\" \\"
|
|
|
|
|
|
error " -addext \"subjectAltName=${SS_SANS}\""
|
|
|
|
|
|
else
|
|
|
|
|
|
info " Generating self-signed certificate ..."
|
|
|
|
|
|
mkdir -p "${CERT_HOST_PATH}"
|
|
|
|
|
|
if [[ "$SS_KEY_TYPE" == "1" ]]; then
|
|
|
|
|
|
openssl req -x509 \
|
|
|
|
|
|
-newkey "${SS_NEWKEY}" \
|
|
|
|
|
|
-sha256 -days "${SS_DAYS}" -nodes \
|
|
|
|
|
|
-keyout "${CERT_HOST_PATH}/privkey.pem" \
|
|
|
|
|
|
-out "${CERT_HOST_PATH}/fullchain.pem" \
|
|
|
|
|
|
-subj "${SS_SUBJ}" \
|
|
|
|
|
|
-addext "subjectAltName=${SS_SANS}" \
|
|
|
|
|
|
2>/dev/null
|
|
|
|
|
|
else
|
|
|
|
|
|
openssl req -x509 \
|
|
|
|
|
|
-newkey "${SS_NEWKEY}" -pkeyopt "${SS_PKEYOPT}" \
|
|
|
|
|
|
-sha256 -days "${SS_DAYS}" -nodes \
|
|
|
|
|
|
-keyout "${CERT_HOST_PATH}/privkey.pem" \
|
|
|
|
|
|
-out "${CERT_HOST_PATH}/fullchain.pem" \
|
|
|
|
|
|
-subj "${SS_SUBJ}" \
|
|
|
|
|
|
-addext "subjectAltName=${SS_SANS}" \
|
|
|
|
|
|
2>/dev/null
|
|
|
|
|
|
fi
|
|
|
|
|
|
chmod 600 "${CERT_HOST_PATH}/privkey.pem"
|
|
|
|
|
|
success " Certificate : ${CERT_HOST_PATH}/fullchain.pem"
|
|
|
|
|
|
success " Private key : ${CERT_HOST_PATH}/privkey.pem"
|
|
|
|
|
|
info " Subject : ${SS_SUBJ}"
|
|
|
|
|
|
info " SANs : ${SS_SANS}"
|
|
|
|
|
|
info " Valid for : ${SS_DAYS} days"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
2026-03-18 20:35:51 +08:00
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
done # end per-relay loop
|
|
|
|
|
|
|
|
|
|
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# STEP 4 – Main server snippet
|
|
|
|
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
header "Step 4 · Main Server Configuration Snippet"
|
|
|
|
|
|
|
|
|
|
|
|
echo "Add the following relay entries to your main NetBird server's"
|
|
|
|
|
|
echo "management.json (or equivalent config), replacing any existing ones:"
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo -e "${BOLD}Relay URLs:${RESET}"
|
|
|
|
|
|
for i in "${!RELAY_DOMAINS[@]}"; do
|
|
|
|
|
|
echo " rels://${RELAY_DOMAINS[$i]}:${RELAY_LISTEN_PORTS[$i]}"
|
|
|
|
|
|
done
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo -e "${BOLD}STUN URLs:${RESET}"
|
|
|
|
|
|
for i in "${!RELAY_DOMAINS[@]}"; do
|
|
|
|
|
|
if [[ "${RELAY_ENABLE_STUN[$i]}" == "true" ]] && [[ -n "${RELAY_STUN_PORTS_LIST[$i]}" ]]; then
|
|
|
|
|
|
IFS=',' read -ra _SP <<< "${RELAY_STUN_PORTS_LIST[$i]}"
|
|
|
|
|
|
for _P in "${_SP[@]}"; do
|
|
|
|
|
|
_P="${_P// /}"
|
|
|
|
|
|
echo " stun:${RELAY_DOMAINS[$i]}:${_P}"
|
|
|
|
|
|
done
|
|
|
|
|
|
fi
|
|
|
|
|
|
done
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo -e "${BOLD}Shared secret:${RESET} ${AUTH_SECRET}"
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
# STEP 5 – Next steps
|
|
|
|
|
|
# ═════════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
header "Step 5 · Next Steps"
|
|
|
|
|
|
|
|
|
|
|
|
for i in "${!RELAY_DOMAINS[@]}"; do
|
|
|
|
|
|
D="${RELAY_DOMAINS[$i]}"
|
|
|
|
|
|
DIR="${RELAY_DIRS[$i]}"
|
|
|
|
|
|
LP="${RELAY_LISTEN_PORTS[$i]}"
|
|
|
|
|
|
TM="${RELAY_TLS_MODES[$i]}"
|
|
|
|
|
|
ES="${RELAY_ENABLE_STUN[$i]}"
|
|
|
|
|
|
SP="${RELAY_STUN_PORTS_LIST[$i]}"
|
|
|
|
|
|
|
|
|
|
|
|
echo -e "${BOLD}Relay: ${D}${RESET}"
|
|
|
|
|
|
echo " 1. Copy ${DIR}/ to the relay server"
|
|
|
|
|
|
echo " 2. On the relay server:"
|
|
|
|
|
|
echo " cd ${DIR}"
|
|
|
|
|
|
echo " docker compose up -d"
|
|
|
|
|
|
echo " docker compose logs -f"
|
|
|
|
|
|
if [[ "$TM" == "1" ]]; then
|
|
|
|
|
|
echo " 3. Trigger TLS cert (Let's Encrypt):"
|
|
|
|
|
|
echo " curl -v https://${D}/"
|
|
|
|
|
|
echo " Expect: 404 page not found + valid LE cert"
|
2026-03-18 20:50:09 +08:00
|
|
|
|
elif [[ "$TM" == "3" ]]; then
|
|
|
|
|
|
echo " Note: Self-signed cert is at ${DIR}/certs/"
|
|
|
|
|
|
echo " Clients must trust this CA or skip TLS verification."
|
2026-03-18 20:35:51 +08:00
|
|
|
|
fi
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo -e " ${BOLD}Firewall ports to open:${RESET}"
|
|
|
|
|
|
[[ "$TM" == "1" ]] && echo " 80/tcp — Let's Encrypt HTTP challenge"
|
|
|
|
|
|
echo " ${LP}/tcp — Relay (HTTPS)"
|
|
|
|
|
|
if [[ "$ES" == "true" ]] && [[ -n "$SP" ]]; then
|
|
|
|
|
|
IFS=',' read -ra _SP <<< "$SP"
|
|
|
|
|
|
for _P in "${_SP[@]}"; do
|
|
|
|
|
|
_P="${_P// /}"
|
|
|
|
|
|
echo " ${_P}/udp — STUN"
|
|
|
|
|
|
done
|
|
|
|
|
|
fi
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
done
|
|
|
|
|
|
success "Setup complete! 🎉"
|