diff --git a/infrastructure_files/getting-started.sh b/infrastructure_files/getting-started.sh index 25599997c..cd83836c4 100755 --- a/infrastructure_files/getting-started.sh +++ b/infrastructure_files/getting-started.sh @@ -91,16 +91,17 @@ read_reverse_proxy_type() { 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 " [6] Traefik TCP Proxy (single port 443 + STUN)" > /dev/stderr echo "" > /dev/stderr - echo -n "Enter choice [0-5] (default: 0): " > /dev/stderr + echo -n "Enter choice [0-6] (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 + if [[ ! "$CHOICE" =~ ^[0-6]$ ]]; then + echo "Invalid choice. Please enter a number between 0 and 6." > /dev/stderr read_reverse_proxy_type return fi @@ -140,6 +141,35 @@ read_traefik_certresolver() { return 0 } +read_traefik_tcp_acme_email() { + echo "" > /dev/stderr + echo "Enter your email for Let's Encrypt certificate notifications." > /dev/stderr + echo -n "Email address: " > /dev/stderr + read -r EMAIL < /dev/tty + if [[ -z "$EMAIL" ]]; then + echo "Email is required for Let's Encrypt." > /dev/stderr + read_traefik_tcp_acme_email + return + fi + echo "$EMAIL" + return 0 +} + +read_enable_proxy() { + echo "" > /dev/stderr + echo "Do you want to enable the NetBird Proxy service?" > /dev/stderr + echo "The proxy exposes internal NetBird network resources to the internet." > /dev/stderr + echo -n "Enable proxy? [y/N]: " > /dev/stderr + read -r CHOICE < /dev/tty + + if [[ "$CHOICE" =~ ^[Yy]$ ]]; then + echo "true" + else + echo "false" + fi + return 0 +} + read_port_binding_preference() { echo "" > /dev/stderr echo "Should container ports be bound to localhost only (127.0.0.1)?" > /dev/stderr @@ -206,6 +236,30 @@ wait_management() { return 0 } +wait_management_traefik() { + set +e + echo -n "Waiting for Management server to become ready" + counter=1 + while true; do + # Check the embedded IdP endpoint through Traefik + if curl -sk -f -o /dev/null "$NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN/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 traefik + $DOCKER_COMPOSE_COMMAND logs --tail=20 management + fi + echo -n " ." + sleep 2 + counter=$((counter + 1)) + done + echo " done" + set -e + return 0 +} + wait_management_direct() { set +e local upstream_host=$(get_upstream_host) @@ -263,6 +317,14 @@ initialize_default_values() { RELAY_HOST_PORT="8084" BIND_LOCALHOST_ONLY="true" EXTERNAL_PROXY_NETWORK="" + + # Traefik TCP proxy configuration + TRAEFIK_IMAGE="traefik:v3.4" + TRAEFIK_TCP_ACME_EMAIL="" + + # NetBird Proxy configuration + ENABLE_PROXY="false" + PROXY_TOKEN="" return 0 } @@ -293,8 +355,17 @@ configure_reverse_proxy() { TRAEFIK_CERTRESOLVER=$(read_traefik_certresolver) fi + # Handle Traefik TCP proxy prompts + if [[ "$REVERSE_PROXY_TYPE" == "6" ]]; then + TRAEFIK_TCP_ACME_EMAIL=$(read_traefik_tcp_acme_email) + + # Prompt for NetBird Proxy configuration + ENABLE_PROXY=$(read_enable_proxy) + # Note: PROXY_TOKEN will be auto-generated after Management starts + fi + # Handle port binding for external proxy options (2-5) - if [[ "$REVERSE_PROXY_TYPE" -ge 2 ]]; then + if [[ "$REVERSE_PROXY_TYPE" -ge 2 && "$REVERSE_PROXY_TYPE" -le 5 ]]; then BIND_LOCALHOST_ONLY=$(read_port_binding_preference) fi @@ -313,7 +384,7 @@ check_existing_installation() { 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 management.json relay.env nginx-netbird.conf caddyfile-netbird.txt npm-advanced-config.txt" + echo " rm -f docker-compose.yml Caddyfile dashboard.env management.json relay.env nginx-netbird.conf caddyfile-netbird.txt npm-advanced-config.txt traefik.yml traefik-dynamic.yml proxy.env" echo "Be aware that this will remove all data from the database, and you will have to reconfigure the dashboard." exit 1 fi @@ -347,6 +418,17 @@ generate_configuration_files() { 5) render_docker_compose_exposed_ports > docker-compose.yml ;; + 6) + render_docker_compose_traefik_tcp > docker-compose.yml + render_traefik_static_config > traefik.yml + render_traefik_dynamic_config > traefik-dynamic.yml + if [[ "$ENABLE_PROXY" == "true" ]]; then + # Create placeholder proxy.env so docker-compose can validate + # This will be overwritten with the actual token after Management starts + echo "# Placeholder - will be updated with token after Management starts" > proxy.env + echo "NB_PROXY_TOKEN=placeholder" >> proxy.env + fi + ;; *) echo "Invalid reverse proxy type: $REVERSE_PROXY_TYPE" > /dev/stderr exit 1 @@ -402,6 +484,50 @@ start_services_and_show_instructions() { echo "" echo "NetBird containers are running. Configure NPM as shown above, then access:" echo " $NETBIRD_HTTP_PROTOCOL://$NETBIRD_DOMAIN" + elif [[ "$REVERSE_PROXY_TYPE" == "6" ]]; then + # Traefik TCP Proxy - two-phase startup if proxy is enabled + echo -e "$MSG_STARTING_SERVICES" + + if [[ "$ENABLE_PROXY" == "true" ]]; then + # Phase 1: Start core services (without proxy) + echo "Starting core services..." + $DOCKER_COMPOSE_COMMAND up -d traefik dashboard signal relay management + + sleep 3 + wait_management_traefik + + # Phase 2: Create proxy token and start proxy + echo "" + echo "Creating proxy access token..." + # Use docker exec with bash to run the token command directly + # (bypassing the entrypoint which adds 'management' as first arg) + PROXY_TOKEN=$($DOCKER_COMPOSE_COMMAND exec -T management \ + bash -c '/go/bin/netbird-mgmt token create --name "default-proxy" --config /etc/netbird/management.json' 2>/dev/null | grep "^Token:" | awk '{print $2}') + + if [[ -z "$PROXY_TOKEN" ]]; then + echo "ERROR: Failed to create proxy token. Check management logs." > /dev/stderr + $DOCKER_COMPOSE_COMMAND logs --tail=20 management + exit 1 + fi + + echo "Proxy token created successfully." + + # Generate proxy.env with the token + render_proxy_env > proxy.env + + # Start proxy service + echo "Starting proxy service..." + $DOCKER_COMPOSE_COMMAND up -d proxy + else + # No proxy - start all services at once + $DOCKER_COMPOSE_COMMAND up -d + + sleep 3 + wait_management_traefik + fi + + echo -e "$MSG_DONE" + print_post_setup_instructions else # External proxies (nginx, external Caddy, other) - need manual config first print_post_setup_instructions @@ -547,6 +673,29 @@ EOF return 0 } +render_proxy_env() { + cat < /dev/stderr ;; diff --git a/management/Dockerfile.multistage b/management/Dockerfile.multistage new file mode 100644 index 000000000..619f84615 --- /dev/null +++ b/management/Dockerfile.multistage @@ -0,0 +1,17 @@ +FROM golang:1.25-bookworm AS builder +WORKDIR /app + +# Install build dependencies +RUN apt-get update && apt-get install -y gcc libc6-dev && rm -rf /var/lib/apt/lists/* + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=1 GOOS=linux go build -ldflags="-s -w" -o netbird-mgmt ./management + +FROM ubuntu:24.04 +RUN apt update && apt install -y ca-certificates && rm -fr /var/cache/apt +ENTRYPOINT [ "/go/bin/netbird-mgmt","management"] +CMD ["--log-file", "console"] +COPY --from=builder /app/netbird-mgmt /go/bin/netbird-mgmt diff --git a/management/server/peer.go b/management/server/peer.go index 5101a5133..b7b6d913b 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -756,11 +756,9 @@ func (am *DefaultAccountManager) AddPeer(ctx context.Context, accountID, setupKe } } - if !peer.ProxyMeta.Embedded { - err = transaction.AddPeerToAllGroup(ctx, accountID, newPeer.ID) - if err != nil { - return fmt.Errorf("failed adding peer to All group: %w", err) - } + err = transaction.AddPeerToAllGroup(ctx, accountID, newPeer.ID) + if err != nil { + return fmt.Errorf("failed adding peer to All group: %w", err) } switch { diff --git a/proxy/cmd/proxy/cmd/root.go b/proxy/cmd/proxy/cmd/root.go index ebb0ff71d..00f41aa50 100644 --- a/proxy/cmd/proxy/cmd/root.go +++ b/proxy/cmd/proxy/cmd/root.go @@ -39,9 +39,10 @@ var ( addr string proxyDomain string certDir string - acmeCerts bool - acmeAddr string - acmeDir string + acmeCerts bool + acmeAddr string + acmeDir string + acmeChallengeType string debugEndpoint bool debugEndpointAddr string healthAddr string @@ -72,9 +73,10 @@ func init() { rootCmd.Flags().StringVar(&addr, "addr", envStringOrDefault("NB_PROXY_ADDRESS", ":443"), "Reverse proxy address to listen on") rootCmd.Flags().StringVar(&proxyDomain, "domain", envStringOrDefault("NB_PROXY_DOMAIN", ""), "The Domain at which this proxy will be reached. e.g., netbird.example.com") rootCmd.Flags().StringVar(&certDir, "cert-dir", envStringOrDefault("NB_PROXY_CERTIFICATE_DIRECTORY", "./certs"), "Directory to store certificates") - rootCmd.Flags().BoolVar(&acmeCerts, "acme-certs", envBoolOrDefault("NB_PROXY_ACME_CERTIFICATES", false), "Generate ACME certificates using HTTP-01 challenges") - rootCmd.Flags().StringVar(&acmeAddr, "acme-addr", envStringOrDefault("NB_PROXY_ACME_ADDRESS", ":80"), "HTTP address for ACME HTTP-01 challenges") + rootCmd.Flags().BoolVar(&acmeCerts, "acme-certs", envBoolOrDefault("NB_PROXY_ACME_CERTIFICATES", false), "Generate ACME certificates automatically") + rootCmd.Flags().StringVar(&acmeAddr, "acme-addr", envStringOrDefault("NB_PROXY_ACME_ADDRESS", ":80"), "HTTP address for ACME HTTP-01 challenges (only used when acme-challenge-type is http-01)") rootCmd.Flags().StringVar(&acmeDir, "acme-dir", envStringOrDefault("NB_PROXY_ACME_DIRECTORY", acme.LetsEncryptURL), "URL of ACME challenge directory") + rootCmd.Flags().StringVar(&acmeChallengeType, "acme-challenge-type", envStringOrDefault("NB_PROXY_ACME_CHALLENGE_TYPE", "tls-alpn-01"), "ACME challenge type: tls-alpn-01 (default, port 443 only) or http-01 (requires port 80)") rootCmd.Flags().BoolVar(&debugEndpoint, "debug-endpoint", envBoolOrDefault("NB_PROXY_DEBUG_ENDPOINT", false), "Enable debug HTTP endpoint") rootCmd.Flags().StringVar(&debugEndpointAddr, "debug-endpoint-addr", envStringOrDefault("NB_PROXY_DEBUG_ENDPOINT_ADDRESS", "localhost:8444"), "Address for the debug HTTP endpoint") rootCmd.Flags().StringVar(&healthAddr, "health-addr", envStringOrDefault("NB_PROXY_HEALTH_ADDRESS", "localhost:8080"), "Address for the health probe endpoint (liveness/readiness/startup)") @@ -151,6 +153,7 @@ func runServer(cmd *cobra.Command, args []string) error { GenerateACMECertificates: acmeCerts, ACMEChallengeAddress: acmeAddr, ACMEDirectory: acmeDir, + ACMEChallengeType: acmeChallengeType, DebugEndpointEnabled: debugEndpoint, DebugEndpointAddress: debugEndpointAddr, HealthAddress: healthAddr, diff --git a/proxy/server.go b/proxy/server.go index 62c408d31..096e1be5c 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -77,6 +77,9 @@ type Server struct { GenerateACMECertificates bool ACMEChallengeAddress string ACMEDirectory string + // ACMEChallengeType specifies the ACME challenge type: "http-01" or "tls-alpn-01". + // Defaults to "tls-alpn-01" if not specified. + ACMEChallengeType string // CertLockMethod controls how ACME certificate locks are coordinated // across replicas. Default: CertLockAuto (detect environment). CertLockMethod acme.CertLockMethod @@ -205,17 +208,28 @@ func (s *Server) ListenAndServe(ctx context.Context, addr string) (err error) { // When generating ACME certificates, start a challenge server. tlsConfig := &tls.Config{} if s.GenerateACMECertificates { - s.Logger.WithField("acme_server", s.ACMEDirectory).Debug("ACME certificates enabled, configuring certificate manager") - s.acme = acme.NewManager(s.CertificateDirectory, s.ACMEDirectory, s, s.Logger, s.CertLockMethod) - s.http = &http.Server{ - Addr: s.ACMEChallengeAddress, - Handler: s.acme.HTTPHandler(nil), + // Default to TLS-ALPN-01 challenge if not specified + if s.ACMEChallengeType == "" { + s.ACMEChallengeType = "tls-alpn-01" } - go func() { - if err := s.http.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { - s.Logger.WithError(err).Error("ACME HTTP-01 challenge server failed") + s.Logger.WithFields(log.Fields{ + "acme_server": s.ACMEDirectory, + "challenge_type": s.ACMEChallengeType, + }).Debug("ACME certificates enabled, configuring certificate manager") + s.acme = acme.NewManager(s.CertificateDirectory, s.ACMEDirectory, s, s.Logger, s.CertLockMethod) + + // Only start HTTP server for HTTP-01 challenge type + if s.ACMEChallengeType == "http-01" { + s.http = &http.Server{ + Addr: s.ACMEChallengeAddress, + Handler: s.acme.HTTPHandler(nil), } - }() + go func() { + if err := s.http.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + s.Logger.WithError(err).Error("ACME HTTP-01 challenge server failed") + } + }() + } tlsConfig = s.acme.TLSConfig() // ServerName needs to be set to allow for ACME to work correctly @@ -223,8 +237,9 @@ func (s *Server) ListenAndServe(ctx context.Context, addr string) (err error) { tlsConfig.ServerName = s.ProxyURL s.Logger.WithFields(log.Fields{ - "ServerName": s.ProxyURL, - }).Debug("started ACME challenge server") + "ServerName": s.ProxyURL, + "challenge_type": s.ACMEChallengeType, + }).Debug("ACME certificate manager configured") } else { s.Logger.Debug("ACME certificates disabled, using static certificates with file watching") certPath := filepath.Join(s.CertificateDirectory, s.CertificateFile)