diff --git a/go.mod b/go.mod index ff9105761..4a8bc3f2b 100644 --- a/go.mod +++ b/go.mod @@ -83,6 +83,7 @@ require ( github.com/pion/stun/v3 v3.1.0 github.com/pion/transport/v3 v3.1.1 github.com/pion/turn/v3 v3.0.1 + github.com/pires/go-proxyproto v0.11.0 github.com/pkg/sftp v1.13.9 github.com/prometheus/client_golang v1.23.2 github.com/quic-go/quic-go v0.55.0 diff --git a/go.sum b/go.sum index 23a12ff68..2a9ad6d70 100644 --- a/go.sum +++ b/go.sum @@ -474,6 +474,8 @@ github.com/pion/turn/v3 v3.0.1 h1:wLi7BTQr6/Q20R0vt/lHbjv6y4GChFtC33nkYbasoT8= github.com/pion/turn/v3 v3.0.1/go.mod h1:MrJDKgqryDyWy1/4NT9TWfXWGMC7UHT6pJIv1+gMeNE= github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc= github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8= +github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= +github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/infrastructure_files/getting-started.sh b/infrastructure_files/getting-started.sh index d8a9e9ad6..dc5d53504 100755 --- a/infrastructure_files/getting-started.sh +++ b/infrastructure_files/getting-started.sh @@ -329,6 +329,9 @@ initialize_default_values() { BIND_LOCALHOST_ONLY="true" EXTERNAL_PROXY_NETWORK="" + # Traefik static IP within the internal bridge network + TRAEFIK_IP="172.30.0.10" + # NetBird Proxy configuration ENABLE_PROXY="false" PROXY_DOMAIN="" @@ -393,7 +396,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 dashboard.env config.yaml proxy.env nginx-netbird.conf caddyfile-netbird.txt npm-advanced-config.txt" + echo " rm -f docker-compose.yml dashboard.env config.yaml proxy.env traefik-dynamic.yaml 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 @@ -412,6 +415,8 @@ generate_configuration_files() { # This will be overwritten with the actual token after netbird-server starts echo "# Placeholder - will be updated with token after netbird-server starts" > proxy.env echo "NB_PROXY_TOKEN=placeholder" >> proxy.env + # TCP ServersTransport for PROXY protocol v2 to the proxy backend + render_traefik_dynamic > traefik-dynamic.yaml fi ;; 1) @@ -559,10 +564,14 @@ init_environment() { ############################################ render_docker_compose_traefik_builtin() { - # Generate proxy service section if enabled + # Generate proxy service section and Traefik dynamic config if enabled local proxy_service="" local proxy_volumes="" + local traefik_file_provider="" + local traefik_dynamic_volume="" if [[ "$ENABLE_PROXY" == "true" ]]; then + traefik_file_provider=' - "--providers.file.filename=/etc/traefik/dynamic.yaml"' + traefik_dynamic_volume=" - ./traefik-dynamic.yaml:/etc/traefik/dynamic.yaml:ro" proxy_service=" # NetBird Proxy - exposes internal resources to the internet proxy: @@ -570,7 +579,7 @@ render_docker_compose_traefik_builtin() { container_name: netbird-proxy # Hairpin NAT fix: route domain back to traefik's static IP within Docker extra_hosts: - - \"$NETBIRD_DOMAIN:172.30.0.10\" + - \"$NETBIRD_DOMAIN:$TRAEFIK_IP\" ports: - 51820:51820/udp restart: unless-stopped @@ -590,6 +599,7 @@ render_docker_compose_traefik_builtin() { - traefik.tcp.routers.proxy-passthrough.service=proxy-tls - traefik.tcp.routers.proxy-passthrough.priority=1 - traefik.tcp.services.proxy-tls.loadbalancer.server.port=8443 + - traefik.tcp.services.proxy-tls.loadbalancer.serverstransport=pp-v2@file logging: driver: \"json-file\" options: @@ -609,7 +619,7 @@ services: restart: unless-stopped networks: netbird: - ipv4_address: 172.30.0.10 + ipv4_address: $TRAEFIK_IP command: # Logging - "--log.level=INFO" @@ -636,12 +646,14 @@ services: # gRPC transport settings - "--serverstransport.forwardingtimeouts.responseheadertimeout=0s" - "--serverstransport.forwardingtimeouts.idleconntimeout=0s" +$traefik_file_provider ports: - '443:443' - '80:80' volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - netbird_traefik_letsencrypt:/letsencrypt +$traefik_dynamic_volume logging: driver: "json-file" options: @@ -751,6 +763,10 @@ server: cliRedirectURIs: - "http://localhost:53000/" + reverseProxy: + trustedHTTPProxies: + - "$TRAEFIK_IP/32" + store: engine: "sqlite" encryptionKey: "$DATASTORE_ENCRYPTION_KEY" @@ -780,6 +796,17 @@ EOF return 0 } +render_traefik_dynamic() { + cat <<'EOF' +tcp: + serversTransports: + pp-v2: + proxyProtocol: + version: 2 +EOF + return 0 +} + render_proxy_env() { cat < 0 { + ppListener.ConnPolicy = s.proxyProtocolPolicy + } else { + s.Logger.Warn("PROXY protocol enabled without trusted proxies; any source may send PROXY headers") + } + s.Logger.Info("PROXY protocol enabled on listener") + return ppListener +} + +// proxyProtocolPolicy returns whether to require, skip, or reject the PROXY +// header based on whether the connection source is in TrustedProxies. +func (s *Server) proxyProtocolPolicy(opts proxyproto.ConnPolicyOptions) (proxyproto.Policy, error) { + // No logging on reject to prevent abuse + tcpAddr, ok := opts.Upstream.(*net.TCPAddr) + if !ok { + return proxyproto.REJECT, nil + } + addr, ok := netip.AddrFromSlice(tcpAddr.IP) + if !ok { + return proxyproto.REJECT, nil + } + addr = addr.Unmap() + + // called per accept + for _, prefix := range s.TrustedProxies { + if prefix.Contains(addr) { + return proxyproto.REQUIRE, nil + } + } + return proxyproto.IGNORE, nil +} + const ( + defaultHealthAddr = "localhost:8080" + defaultDebugAddr = "localhost:8444" + + // proxyProtoHeaderTimeout is the deadline for reading the PROXY protocol + // header after accepting a connection. + proxyProtoHeaderTimeout = 5 * time.Second + // shutdownPreStopDelay is the time to wait after receiving a shutdown signal // before draining connections. This allows the load balancer to propagate // the endpoint removal. @@ -647,7 +725,7 @@ func (s *Server) protoToMapping(mapping *proto.ProxyMapping) proxy.Mapping { // If addr is empty, it defaults to localhost:8444 for security. func debugEndpointAddr(addr string) string { if addr == "" { - return "localhost:8444" + return defaultDebugAddr } return addr }