From b352ab84c082c51368e587b83787fed2d4591fee Mon Sep 17 00:00:00 2001 From: shuuri-labs <61762328+shuuri-labs@users.noreply.github.com> Date: Fri, 16 Jan 2026 17:42:28 +0100 Subject: [PATCH] Feat/quickstart reverse proxy assistant (#5100) * add external reverse proxy config steps to quickstart script * remove generated files * - Remove 'press enter' prompt from post-traefik config since traefik requires no manual config - Improve npm flow (ask users for docker network, user container names in config) * fixes for npm flow * nginx flow fixes * caddy flow fixes * Consolidate NPM_NETWORK, NGINX_NETWORK, CADDY_NETWORK into single EXTERNAL_PROXY_NETWORK variable. Add read_proxy_docker_network() function that prompts for Docker network for options 2-4 (Nginx, NPM, Caddy). Generated configs now use container names when a Docker network is specified. * fix https for traefik * fix sonar code smells * fix sonar smell (add return to render_dashboard_env) * added tls instructions to nginx flow * removed unused bind_addr variable from quickstart.sh * Refactor getting-started.sh for improved maintainability Break down large functions into focused, single-responsibility components: - Split init_environment() into 6 initialization functions - Split print_post_setup_instructions() into 6 proxy-specific functions - Add section headers for better code organization - Fix 3 code smell issues (unused bind_addr variables) - Add TLS certificate documentation for Nginx - Link reverse proxy names to docs sections Reduces largest function from 205 to ~90 lines while maintaining single-file distribution. No functional changes. * - Remove duplicate network display logic in Traefik instructions - Use upstream_host instead of bind_addr for NPM forward hostname - Use upstream_host instead of bind_addr in manual proxy route examples - Prevents displaying invalid 0.0.0.0 as connection target in setup instructions * add wait_management_direct to caddy flow to ensure script waits until containers are running/passing healthchecks before reporting 'done!' --- infrastructure_files/getting-started.sh | 1153 ++++++++++++++++++++++- 1 file changed, 1136 insertions(+), 17 deletions(-) diff --git a/infrastructure_files/getting-started.sh b/infrastructure_files/getting-started.sh index 5a9488fad..8676840a6 100755 --- a/infrastructure_files/getting-started.sh +++ b/infrastructure_files/getting-started.sh @@ -9,6 +9,16 @@ set -e # Sed pattern to strip base64 padding characters SED_STRIP_PADDING='s/=//g' +# Constants for repeated string literals +readonly MSG_STARTING_SERVICES="\nStarting NetBird services\n" +readonly MSG_DONE="\nDone!\n" +readonly MSG_NEXT_STEPS="Next steps:" +readonly MSG_SEPARATOR="==========================================" + +############################################ +# Utility Functions +############################################ + check_docker_compose() { if command -v docker-compose &> /dev/null then @@ -82,6 +92,106 @@ get_turn_external_ip() { return 0 } +read_reverse_proxy_type() { + echo "" > /dev/stderr + echo "Which reverse proxy will you use?" > /dev/stderr + echo " [0] Built-in Caddy (recommended - automatic TLS)" > /dev/stderr + echo " [1] Traefik (labels added to containers)" > /dev/stderr + echo " [2] Nginx (generates config template)" > /dev/stderr + echo " [3] Nginx Proxy Manager (generates config + instructions)" > /dev/stderr + echo " [4] External Caddy (generates Caddyfile snippet)" > /dev/stderr + echo " [5] Other/Manual (displays setup documentation)" > /dev/stderr + echo "" > /dev/stderr + echo -n "Enter choice [0-5] (default: 0): " > /dev/stderr + read -r CHOICE < /dev/tty + + if [[ -z "$CHOICE" ]]; then + CHOICE="0" + fi + + if [[ ! "$CHOICE" =~ ^[0-5]$ ]]; then + echo "Invalid choice. Please enter a number between 0 and 5." > /dev/stderr + read_reverse_proxy_type + return + fi + + echo "$CHOICE" + return 0 +} + +read_traefik_network() { + echo "" > /dev/stderr + echo "If you have an existing Traefik instance, enter its external network name." > /dev/stderr + echo -n "External network (leave empty to create 'netbird' network): " > /dev/stderr + read -r NETWORK < /dev/tty + echo "$NETWORK" + return 0 +} + +read_traefik_entrypoint() { + echo "" > /dev/stderr + echo "Enter the name of your Traefik HTTPS entrypoint." > /dev/stderr + echo -n "HTTPS entrypoint name (default: websecure): " > /dev/stderr + read -r ENTRYPOINT < /dev/tty + if [[ -z "$ENTRYPOINT" ]]; then + ENTRYPOINT="websecure" + fi + echo "$ENTRYPOINT" + return 0 +} + +read_traefik_certresolver() { + echo "" > /dev/stderr + echo "Enter the name of your Traefik certificate resolver (for automatic TLS)." > /dev/stderr + echo "Leave empty if you handle TLS termination elsewhere or use a wildcard cert." > /dev/stderr + echo -n "Certificate resolver name (e.g., letsencrypt): " > /dev/stderr + read -r RESOLVER < /dev/tty + echo "$RESOLVER" + return 0 +} + +read_port_binding_preference() { + echo "" > /dev/stderr + echo "Should container ports be bound to localhost only (127.0.0.1)?" > /dev/stderr + echo "Choose 'yes' if your reverse proxy runs on the same host (more secure)." > /dev/stderr + echo -n "Bind to localhost only? [Y/n]: " > /dev/stderr + read -r CHOICE < /dev/tty + + if [[ "$CHOICE" =~ ^[Nn]$ ]]; then + echo "false" + else + echo "true" + fi + return 0 +} + +read_proxy_docker_network() { + local proxy_name="$1" + echo "" > /dev/stderr + echo "Is ${proxy_name} running in Docker?" > /dev/stderr + echo "If yes, enter the Docker network ${proxy_name} is on (NetBird will join it)." > /dev/stderr + echo -n "Docker network (leave empty if not in Docker): " > /dev/stderr + read -r NETWORK < /dev/tty + echo "$NETWORK" + return 0 +} + +get_bind_address() { + if [[ "$BIND_LOCALHOST_ONLY" == "true" ]]; then + echo "127.0.0.1" + else + echo "0.0.0.0" + fi + return 0 +} + +get_upstream_host() { + # Always return 127.0.0.1 for health checks and upstream targets + # Cannot use 0.0.0.0 as a connection target + echo "127.0.0.1" + return 0 +} + wait_management() { set +e echo -n "Waiting for Management server to become ready" @@ -106,7 +216,35 @@ wait_management() { return 0 } -init_environment() { +wait_management_direct() { + set +e + local upstream_host=$(get_upstream_host) + echo -n "Waiting for Management server to become ready" + counter=1 + while true; do + # Check the embedded IdP endpoint directly (no reverse proxy) + if curl -sk -f -o /dev/null "http://${upstream_host}:${MANAGEMENT_HOST_PORT}/oauth2/.well-known/openid-configuration" 2>/dev/null; then + break + fi + if [[ $counter -eq 60 ]]; then + echo "" + echo "Taking too long. Checking logs..." + $DOCKER_COMPOSE_COMMAND logs --tail=20 management + fi + echo -n " ." + sleep 2 + counter=$((counter + 1)) + done + echo " done" + set -e + return 0 +} + +############################################ +# Initialization and Configuration +############################################ + +initialize_default_values() { CADDY_SECURE_DOMAIN="" NETBIRD_PORT=80 NETBIRD_HTTP_PROTOCOL="http" @@ -120,6 +258,22 @@ init_environment() { TURN_MAX_PORT=65535 TURN_EXTERNAL_IP_CONFIG=$(get_turn_external_ip) + # Reverse proxy configuration + REVERSE_PROXY_TYPE="0" + TRAEFIK_EXTERNAL_NETWORK="" + TRAEFIK_ENTRYPOINT="websecure" + TRAEFIK_CERTRESOLVER="" + DASHBOARD_HOST_PORT="8080" + MANAGEMENT_HOST_PORT="8081" + SIGNAL_HOST_PORT="8083" + SIGNAL_GRPC_PORT="10000" + RELAY_HOST_PORT="8084" + BIND_LOCALHOST_ONLY="true" + EXTERNAL_PROXY_NETWORK="" + return 0 +} + +configure_domain() { if ! check_nb_domain "$NETBIRD_DOMAIN"; then NETBIRD_DOMAIN=$(read_nb_domain) fi @@ -132,41 +286,169 @@ init_environment() { NETBIRD_HTTP_PROTOCOL="https" NETBIRD_RELAY_PROTO="rels" fi + return 0 +} - check_jq +configure_reverse_proxy() { + # Prompt for reverse proxy type + REVERSE_PROXY_TYPE=$(read_reverse_proxy_type) - DOCKER_COMPOSE_COMMAND=$(check_docker_compose) + # Handle Traefik-specific prompts + if [[ "$REVERSE_PROXY_TYPE" == "1" ]]; then + TRAEFIK_EXTERNAL_NETWORK=$(read_traefik_network) + TRAEFIK_ENTRYPOINT=$(read_traefik_entrypoint) + TRAEFIK_CERTRESOLVER=$(read_traefik_certresolver) + fi + # Handle port binding for external proxy options (2-5) + if [[ "$REVERSE_PROXY_TYPE" -ge 2 ]]; then + BIND_LOCALHOST_ONLY=$(read_port_binding_preference) + fi + + # Handle Docker network prompts for external proxies (options 2-4) + case "$REVERSE_PROXY_TYPE" in + 2) EXTERNAL_PROXY_NETWORK=$(read_proxy_docker_network "Nginx") ;; + 3) EXTERNAL_PROXY_NETWORK=$(read_proxy_docker_network "Nginx Proxy Manager") ;; + 4) EXTERNAL_PROXY_NETWORK=$(read_proxy_docker_network "Caddy") ;; + *) ;; # No network prompt for other options + esac + return 0 +} + +check_existing_installation() { if [[ -f management.json ]]; then echo "Generated files already exist, if you want to reinitialize the environment, please remove them first." echo "You can use the following commands:" echo " $DOCKER_COMPOSE_COMMAND down --volumes # to remove all containers and volumes" - echo " rm -f docker-compose.yml Caddyfile dashboard.env turnserver.conf management.json relay.env" + echo " rm -f docker-compose.yml Caddyfile dashboard.env turnserver.conf management.json relay.env nginx-netbird.conf caddyfile-netbird.txt npm-advanced-config.txt" echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard." exit 1 fi + return 0 +} +generate_configuration_files() { echo Rendering initial files... - render_docker_compose > docker-compose.yml - render_caddyfile > Caddyfile + + # Render docker-compose and proxy config based on selection + case "$REVERSE_PROXY_TYPE" in + 0) + render_docker_compose > docker-compose.yml + render_caddyfile > Caddyfile + ;; + 1) + render_docker_compose_traefik > docker-compose.yml + ;; + 2) + render_docker_compose_exposed_ports > docker-compose.yml + render_nginx_conf > nginx-netbird.conf + ;; + 3) + render_docker_compose_exposed_ports > docker-compose.yml + render_npm_advanced_config > npm-advanced-config.txt + ;; + 4) + render_docker_compose_exposed_ports > docker-compose.yml + render_external_caddyfile > caddyfile-netbird.txt + ;; + 5) + render_docker_compose_exposed_ports > docker-compose.yml + ;; + *) + echo "Invalid reverse proxy type: $REVERSE_PROXY_TYPE" > /dev/stderr + exit 1 + ;; + esac + + # Common files for all configurations render_dashboard_env > dashboard.env render_management_json > management.json render_turn_server_conf > turnserver.conf render_relay_env > relay.env - - echo -e "\nStarting NetBird services\n" - $DOCKER_COMPOSE_COMMAND up -d - - # Wait for management (and embedded IdP) to be ready - sleep 3 - wait_management - - echo -e "\nDone!\n" - echo "You can access the NetBird dashboard at $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN" - echo "Follow the onboarding steps to set up your NetBird instance." return 0 } +start_services_and_show_instructions() { + # For built-in Caddy and Traefik, start containers immediately + # For NPM, start containers first (NPM needs services running to create proxy) + # For other external proxies, show instructions first and wait for user confirmation + if [[ "$REVERSE_PROXY_TYPE" == "0" ]]; then + # Built-in Caddy - handles everything automatically + echo -e "$MSG_STARTING_SERVICES" + $DOCKER_COMPOSE_COMMAND up -d + + sleep 3 + wait_management + + echo -e "$MSG_DONE" + print_post_setup_instructions + elif [[ "$REVERSE_PROXY_TYPE" == "1" ]]; then + # Traefik - start containers first, then show instructions + # Traefik discovers services via Docker labels, so containers must be running + echo -e "$MSG_STARTING_SERVICES" + $DOCKER_COMPOSE_COMMAND up -d + + sleep 3 + wait_management_direct + + echo -e "$MSG_DONE" + print_post_setup_instructions + echo "" + echo "NetBird containers are running. Once Traefik is connected, access the dashboard at:" + echo " $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN" + elif [[ "$REVERSE_PROXY_TYPE" == "3" ]]; then + # NPM - start containers first, then show instructions + # NPM requires backend services to be running before creating proxy hosts + echo -e "$MSG_STARTING_SERVICES" + $DOCKER_COMPOSE_COMMAND up -d + + sleep 3 + wait_management_direct + + echo -e "$MSG_DONE" + print_post_setup_instructions + echo "" + echo "NetBird containers are running. Configure NPM as shown above, then access:" + echo " $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN" + else + # External proxies (nginx, external Caddy, other) - need manual config first + print_post_setup_instructions + + echo "" + echo -n "Press Enter when your reverse proxy is configured (or Ctrl+C to exit)... " + read -r < /dev/tty + + echo -e "$MSG_STARTING_SERVICES" + $DOCKER_COMPOSE_COMMAND up -d + + sleep 3 + wait_management_direct + + echo -e "$MSG_DONE" + echo "NetBird is now running. Access the dashboard at:" + echo " $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN" + fi + return 0 +} + +init_environment() { + initialize_default_values + configure_domain + configure_reverse_proxy + + check_jq + DOCKER_COMPOSE_COMMAND=$(check_docker_compose) + + check_existing_installation + generate_configuration_files + start_services_and_show_instructions + return 0 +} + +############################################ +# Configuration File Renderers +############################################ + render_caddyfile() { cat < ${upstream_host}:${RELAY_HOST_PORT}" + echo " (HTTP with WebSocket upgrade)" + echo "" + echo " /ws-proxy/signal* -> ${upstream_host}:${SIGNAL_HOST_PORT}" + echo " (HTTP with WebSocket upgrade)" + echo "" + echo " /signalexchange.SignalExchange/* -> ${upstream_host}:${SIGNAL_GRPC_PORT}" + echo " (gRPC/h2c - plaintext HTTP/2)" + echo "" + echo " /api/* -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" + echo " (HTTP)" + echo "" + echo " /ws-proxy/management* -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" + echo " (HTTP with WebSocket upgrade)" + echo "" + echo " /management.ManagementService/* -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" + echo " (gRPC/h2c - plaintext HTTP/2)" + echo "" + echo " /oauth2/* -> ${upstream_host}:${MANAGEMENT_HOST_PORT}" + echo " (HTTP - embedded IdP)" + echo "" + echo " /* -> ${upstream_host}:${DASHBOARD_HOST_PORT}" + echo " (HTTP - catch-all for dashboard)" + echo "" + echo "IMPORTANT: gRPC routes require HTTP/2 (h2c) upstream support." + echo "Long-running connections need extended timeouts (recommend 1 day)." + return 0 +} + +print_post_setup_instructions() { + case "$REVERSE_PROXY_TYPE" in + 0) + print_caddy_instructions + ;; + 1) + print_traefik_instructions + ;; + 2) + print_nginx_instructions + ;; + 3) + print_npm_instructions + ;; + 4) + print_external_caddy_instructions + ;; + 5) + print_manual_instructions + ;; + *) + echo "Unknown reverse proxy type: $REVERSE_PROXY_TYPE" > /dev/stderr + ;; + esac + return 0 +} + init_environment