Files
netbird-relay/setup-relay.sh
T
2026-03-18 20:59:43 +08:00

734 lines
32 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
}
parse_existing_env() {
# parse_existing_env <relay.env path>
# Sets EX_* variables from an existing relay.env file.
local envfile="$1"
[[ -f "$envfile" ]] || return 1
_get() { grep -m1 "^${1}=" "$envfile" 2>/dev/null | cut -d= -f2- || true; }
EX_LOG_LEVEL="$(_get NB_LOG_LEVEL)"
EX_AUTH_SECRET="$(_get NB_AUTH_SECRET)"
EX_LE_EMAIL="$(_get NB_LETSENCRYPT_EMAIL)"
EX_CERT_FILE="$(_get NB_TLS_CERT_FILE)"
EX_KEY_FILE="$(_get NB_TLS_KEY_FILE)"
EX_ENABLE_STUN="$(_get NB_ENABLE_STUN)"
EX_STUN_PORTS="$(_get NB_STUN_PORTS)"
# Parse domain and port from NB_EXPOSED_ADDRESS=rels://domain:port
local exposed; exposed="$(_get NB_EXPOSED_ADDRESS)"
EX_DOMAIN="${exposed#rels://}"; EX_DOMAIN="${EX_DOMAIN%%:*}"
EX_LISTEN_PORT="${exposed##*:}"
# Detect TLS mode
local le_domains; le_domains="$(_get NB_LETSENCRYPT_DOMAINS)"
if [[ -n "$le_domains" ]]; then
EX_TLS_MODE="1"
elif [[ -n "$EX_CERT_FILE" ]]; then
EX_TLS_MODE="2" # could be self-signed, but treat as existing cert
else
EX_TLS_MODE="1" # fallback
fi
}
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 ""
# Try to pre-read an existing secret from /opt/netbird-relay/relay.env
_PREREAD_SECRET=""
if [[ -f "/opt/netbird-relay/relay.env" ]]; then
_PREREAD_SECRET="$(grep -m1 '^NB_AUTH_SECRET=' /opt/netbird-relay/relay.env 2>/dev/null | cut -d= -f2- || true)"
fi
if [[ -n "$_PREREAD_SECRET" ]]; then
info "Found existing secret in /opt/netbird-relay/relay.env"
ask AUTH_SECRET "Keep existing secret or enter a new one" "$_PREREAD_SECRET"
success "Using secret."
elif 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
ask AUTH_SECRET "Paste your existing shared secret" ""
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}"
# ── Output directory (ask first so we can read existing config) ───────────
ask RELAY_DIR " Output directory for this relay's files" "/opt/netbird-relay"
# ── Load existing config as defaults if relay.env is present ─────────────
EX_DOMAIN=""; EX_LISTEN_PORT="443"; EX_LOG_LEVEL="info"
EX_AUTH_SECRET=""; EX_LE_EMAIL=""; EX_CERT_FILE=""; EX_KEY_FILE=""
EX_ENABLE_STUN="true"; EX_STUN_PORTS="3478"; EX_TLS_MODE="1"
_EXISTING_ENV="${RELAY_DIR}/relay.env"
if parse_existing_env "$_EXISTING_ENV"; then
info " Found existing config in ${_EXISTING_ENV} — using as defaults."
fi
# ── 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
done
RELAY_DOMAINS+=("$DOMAIN")
# ── Listen & exposed ports ────────────────────────────────────────────────
ask LISTEN_PORT " HTTPS listen port" "${EX_LISTEN_PORT:-443}"
ask LOG_LEVEL " Log level (debug/info/warn/error)" "${EX_LOG_LEVEL:-info}"
# ── STUN ──────────────────────────────────────────────────────────────────
echo ""
_STUN_DEFAULT="y"; [[ "${EX_ENABLE_STUN}" == "false" ]] && _STUN_DEFAULT="n"
if ask_yn " Enable embedded STUN server?" "$_STUN_DEFAULT"; then
ENABLE_STUN=true
ask STUN_PORTS " STUN port(s) — comma-separated for multiple (e.g. 3478,3479)" "${EX_STUN_PORTS:-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)"
echo " 3) Self-signed certificate (generated by this script)"
ask TLS_MODE " Choose [1/2/3]" "${EX_TLS_MODE:-1}"
if [[ "$TLS_MODE" == "1" ]]; then
ask LE_EMAIL " Let's Encrypt email" "${EX_LE_EMAIL}"
LE_DATA_DIR="/data/letsencrypt"
CERT_HOST_PATH=""; CERT_FILE=""; KEY_FILE=""
elif [[ "$TLS_MODE" == "2" ]]; then
# Derive existing host path from cert file path stored in env (best-effort)
_EX_CERT_HOST=""
[[ -n "$EX_CERT_FILE" ]] && _EX_CERT_HOST="$(
grep -m1 'volumes:' -A5 "${RELAY_DIR}/docker-compose.yml" 2>/dev/null \
| grep -m1 ':/certs:ro\|:/certs ' \
| awk -F: '{print $1}' | sed 's/^ *- *//' || true
)"
ask CERT_HOST_PATH " Host path to certs directory" "${_EX_CERT_HOST:-/opt/1panel/www/sites/${DOMAIN}/ssl}"
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=""
else
echo ""
echo " ── Self-signed certificate details ──"
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)" ""
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
ask SS_DAYS " Certificate validity (days)" "3650"
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
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=""
CERT_FILE="/certs/fullchain.pem"
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"
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+="
# TLS — $([ "$TLS_MODE" == "3" ] && echo "Self-signed certificate" || echo "Existing certificates")
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"
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
else
mkdir -p "$RELAY_DIR"
printf '%s' "$ENV_CONTENT" > "$ENV_FILE"
printf '%s' "$COMPOSE_CONTENT" > "$COMPOSE_FILE"
chmod 600 "$ENV_FILE"
success " Written: ${ENV_FILE}"
success " Written: ${COMPOSE_FILE}"
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
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"
elif [[ "$TM" == "3" ]]; then
echo " Note: Self-signed cert is at ${DIR}/certs/"
echo " Clients must trust this CA or skip TLS verification."
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! 🎉"
# ═════════════════════════════════════════════════════════════════════════════
# STEP 6 – Firewall (optional)
# ═════════════════════════════════════════════════════════════════════════════
header "Step 6 · Firewall Configuration (optional)"
$DRY_RUN && { warn "DRY-RUN: skipping firewall step."; exit 0; }
if ! ask_yn "Configure firewall rules now?" "y"; then
info "Skipped. Remember to open the ports listed above manually."
exit 0
fi
# ── Detect firewall ───────────────────────────────────────────────────────────
detect_firewall() {
if systemctl is-active --quiet firewalld 2>/dev/null; then
echo "firewalld"
elif systemctl is-active --quiet ufw 2>/dev/null || command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
echo "ufw"
elif command -v nft &>/dev/null && nft list ruleset 2>/dev/null | grep -q "table"; then
echo "nftables"
elif command -v iptables &>/dev/null; then
echo "iptables"
else
echo "none"
fi
}
FW="$(detect_firewall)"
case "$FW" in
firewalld) info "Detected: firewalld" ;;
ufw) info "Detected: ufw" ;;
nftables) info "Detected: nftables" ;;
iptables) info "Detected: iptables" ;;
none) warn "No supported firewall detected (firewalld / ufw / nftables / iptables)."
info "Open the ports listed in Step 5 manually."
exit 0 ;;
esac
# ── Collect all ports to open across all relays ───────────────────────────────
declare -a FW_TCP=()
declare -a FW_UDP=()
for i in "${!RELAY_DOMAINS[@]}"; do
LP="${RELAY_LISTEN_PORTS[$i]}"
TM="${RELAY_TLS_MODES[$i]}"
ES="${RELAY_ENABLE_STUN[$i]}"
SP="${RELAY_STUN_PORTS_LIST[$i]}"
# TCP: relay port
FW_TCP+=("$LP")
# TCP: port 80 for Let's Encrypt
[[ "$TM" == "1" ]] && FW_TCP+=("80")
# UDP: STUN ports
if [[ "$ES" == "true" ]] && [[ -n "$SP" ]]; then
IFS=',' read -ra _SP <<< "$SP"
for _P in "${_SP[@]}"; do
FW_UDP+=("${_P// /}")
done
fi
done
# Deduplicate
mapfile -t FW_TCP < <(printf '%s\n' "${FW_TCP[@]}" | sort -un)
mapfile -t FW_UDP < <(printf '%s\n' "${FW_UDP[@]}" | sort -un)
echo ""
echo -e "${BOLD}Ports to open:${RESET}"
for p in "${FW_TCP[@]}"; do echo " ${p}/tcp"; done
for p in "${FW_UDP[@]}"; do echo " ${p}/udp"; done
echo ""
if ! ask_yn "Apply these rules?" "y"; then
info "Skipped."
exit 0
fi
# ── Apply rules ───────────────────────────────────────────────────────────────
apply_firewall_rules() {
local fw="$1"
case "$fw" in
firewalld)
for p in "${FW_TCP[@]}"; do
firewall-cmd --permanent --add-port="${p}/tcp" && \
success " firewalld: opened ${p}/tcp" || \
error " firewalld: failed to open ${p}/tcp"
done
for p in "${FW_UDP[@]}"; do
firewall-cmd --permanent --add-port="${p}/udp" && \
success " firewalld: opened ${p}/udp" || \
error " firewalld: failed to open ${p}/udp"
done
firewall-cmd --reload && success " firewalld: reloaded" || error " firewalld: reload failed"
;;
ufw)
for p in "${FW_TCP[@]}"; do
ufw allow "${p}/tcp" && \
success " ufw: allowed ${p}/tcp" || \
error " ufw: failed to allow ${p}/tcp"
done
for p in "${FW_UDP[@]}"; do
ufw allow "${p}/udp" && \
success " ufw: allowed ${p}/udp" || \
error " ufw: failed to allow ${p}/udp"
done
;;
nftables)
# Add rules to the first inet/ip filter input chain found, or create one
_NFT_TABLE="filter"
_NFT_CHAIN="input"
# Check if table/chain exist
if ! nft list chain inet "${_NFT_TABLE}" "${_NFT_CHAIN}" &>/dev/null; then
nft add table inet "${_NFT_TABLE}"
nft add chain inet "${_NFT_TABLE}" "${_NFT_CHAIN}" \
'{ type filter hook input priority 0 ; policy accept ; }'
info " nftables: created table inet ${_NFT_TABLE} chain ${_NFT_CHAIN}"
fi
for p in "${FW_TCP[@]}"; do
nft add rule inet "${_NFT_TABLE}" "${_NFT_CHAIN}" tcp dport "${p}" accept && \
success " nftables: accepted ${p}/tcp" || \
error " nftables: failed ${p}/tcp"
done
for p in "${FW_UDP[@]}"; do
nft add rule inet "${_NFT_TABLE}" "${_NFT_CHAIN}" udp dport "${p}" accept && \
success " nftables: accepted ${p}/udp" || \
error " nftables: failed ${p}/udp"
done
# Persist
if command -v nft &>/dev/null; then
if [[ -d /etc/nftables.d ]]; then
nft list ruleset > /etc/nftables.d/netbird-relay.nft && \
success " nftables: saved to /etc/nftables.d/netbird-relay.nft"
elif [[ -f /etc/nftables.conf ]]; then
nft list ruleset > /etc/nftables.conf && \
success " nftables: saved to /etc/nftables.conf"
else
warn " nftables: rules applied but not persisted — save manually with:"
warn " nft list ruleset > /etc/nftables.conf"
fi
fi
;;
iptables)
for p in "${FW_TCP[@]}"; do
iptables -C INPUT -p tcp --dport "${p}" -j ACCEPT 2>/dev/null || {
iptables -A INPUT -p tcp --dport "${p}" -j ACCEPT && \
success " iptables: opened ${p}/tcp" || \
error " iptables: failed to open ${p}/tcp"
}
done
for p in "${FW_UDP[@]}"; do
iptables -C INPUT -p udp --dport "${p}" -j ACCEPT 2>/dev/null || {
iptables -A INPUT -p udp --dport "${p}" -j ACCEPT && \
success " iptables: opened ${p}/udp" || \
error " iptables: failed to open ${p}/udp"
}
done
# Persist
if command -v netfilter-persistent &>/dev/null; then
netfilter-persistent save && success " iptables: rules persisted via netfilter-persistent"
elif command -v iptables-save &>/dev/null; then
if [[ -f /etc/iptables/rules.v4 ]]; then
iptables-save > /etc/iptables/rules.v4 && \
success " iptables: saved to /etc/iptables/rules.v4"
else
warn " iptables: rules applied but not persisted — save manually with:"
warn " iptables-save > /etc/iptables/rules.v4"
fi
fi
;;
esac
}
if [[ "$EUID" -ne 0 ]]; then
warn "Not running as root — firewall commands may fail."
warn "Re-run with sudo if needed."
fi
apply_firewall_rules "$FW"
echo ""
success "Firewall rules applied. 🎉"