[proxy,client] Add --external-ip-map flag for the embedded client (#8185)

* [proxy] Add --external-ip-map flag for the embedded client

The proxy behind a NAT with a forwarded fixed --wg-port could not advertise
its public address as an ICE candidate, so peers fell back to relay. Expose
the agent's external IP map through embed.Options and a proxy flag
(NB_PROXY_EXTERNAL_IP_MAP).

* [proxy] Validate --external-ip-map at startup

The engine drops every external IP mapping when one fails to parse, so a
typo or a space after a comma silently disabled the feature. Move the
agent's validator to shared/externalipmap, run it from the proxy at
startup, and trim entries first.

* [client,proxy] Require an external IP first in --external-ip-map

The engine reads the first component as the external IP, so a mapping like
iface/10.0.0.5 passed validation and then dropped every mapping.
This commit is contained in:
Nicolas Frati
2026-10-15 14:26:50 +02:00
committed by GitHub
parent 547dd1f177
commit 357d8a2a7c
8 changed files with 160 additions and 75 deletions
+20
View File
@@ -6,6 +6,7 @@ import (
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
@@ -13,6 +14,7 @@ import (
"github.com/spf13/cobra"
"golang.org/x/crypto/acme"
"github.com/netbirdio/netbird/shared/externalipmap"
"github.com/netbirdio/netbird/shared/management/domain"
"github.com/netbirdio/netbird/shared/profiling"
@@ -74,6 +76,7 @@ var (
certLockMethod string
wildcardCertDir string
wgPort uint16
externalIPMap []string
proxyProtocol bool
preSharedKey string
supportsCustomPorts bool
@@ -117,6 +120,7 @@ func init() {
rootCmd.Flags().StringVar(&certLockMethod, "cert-lock-method", envStringOrDefault("NB_PROXY_CERT_LOCK_METHOD", "auto"), "Certificate lock method for cross-replica coordination: auto, flock, or k8s-lease")
rootCmd.Flags().StringVar(&wildcardCertDir, "wildcard-cert-dir", envStringOrDefault("NB_PROXY_WILDCARD_CERT_DIR", ""), "Directory containing wildcard certificate pairs (<name>.crt/<name>.key). Wildcard patterns are extracted from SANs automatically")
rootCmd.Flags().Uint16Var(&wgPort, "wg-port", envUint16OrDefault("NB_PROXY_WG_PORT", 0), "WireGuard listen port (0 = random). Fixed port only works with single-account deployments")
rootCmd.Flags().StringSliceVar(&externalIPMap, "external-ip-map", envStringSliceOrDefault("NB_PROXY_EXTERNAL_IP_MAP", nil), "Comma-separated external IPs to advertise as ICE candidates, as IP or IP/internal-IP-or-interface (e.g. '203.0.113.5/10.0.0.5'). Requires a fixed --wg-port forwarded on the NAT")
rootCmd.Flags().BoolVar(&proxyProtocol, "proxy-protocol", envBoolOrDefault("NB_PROXY_PROXY_PROTOCOL", false), "Enable PROXY protocol on TCP listeners to preserve client IPs behind L4 proxies")
rootCmd.Flags().StringVar(&preSharedKey, "preshared-key", envStringOrDefault("NB_PROXY_PRESHARED_KEY", ""), "Define a pre-shared key for the tunnel between proxy and peers")
rootCmd.Flags().BoolVar(&supportsCustomPorts, "supports-custom-ports", envBoolOrDefault("NB_PROXY_SUPPORTS_CUSTOM_PORTS", true), "Whether the proxy can bind arbitrary ports for UDP/TCP passthrough")
@@ -221,6 +225,13 @@ func runServer(cmd *cobra.Command, args []string) error {
return fmt.Errorf("invalid --trusted-proxies: %w", err)
}
for i := range externalIPMap {
externalIPMap[i] = strings.TrimSpace(externalIPMap[i])
}
if err := externalipmap.Validate(externalIPMap); err != nil {
return fmt.Errorf("invalid --external-ip-map: %w", err)
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
defer stop()
@@ -248,6 +259,7 @@ func runServer(cmd *cobra.Command, args []string) error {
CertLockMethod: nbacme.CertLockMethod(certLockMethod),
WildcardCertDir: wildcardCertDir,
WireguardPort: wgPort,
NATExternalIPs: externalIPMap,
Performance: perf,
ProxyProtocol: proxyProtocol,
PreSharedKey: preSharedKey,
@@ -286,6 +298,14 @@ func envStringOrDefault(key string, def string) string {
return v
}
func envStringSliceOrDefault(key string, def []string) []string {
v, exists := os.LookupEnv(key)
if !exists || v == "" {
return def
}
return strings.Split(v, ",")
}
func envUint16OrDefault(key string, def uint16) uint16 {
v, exists := os.LookupEnv(key)
if !exists {
+4
View File
@@ -144,6 +144,8 @@ type ClientConfig struct {
WGPort uint16
PreSharedKey string
Performance embed.Performance
// NATExternalIPs mirrors embed.Options.NATExternalIPs.
NATExternalIPs []string
// BlockInbound mirrors embed.Options.BlockInbound. Set to true on the
// standalone proxy where the embedded client never accepts inbound;
// set to false on the private/embedded proxy so the engine creates
@@ -402,6 +404,7 @@ func (n *NetBird) createClientEntry(ctx context.Context, accountID types.Account
WireguardPort: &wgPort,
PreSharedKey: n.clientCfg.PreSharedKey,
Performance: n.clientCfg.Performance,
NATExternalIPs: n.clientCfg.NATExternalIPs,
}
logEmbedOptions(n.logger, accountID, serviceID, publicKey.String(), embedOpts)
client, err := embed.New(embedOpts)
@@ -914,6 +917,7 @@ func logEmbedOptions(logger *log.Logger, accountID types.AccountID, serviceID ty
"management_url": opts.ManagementURL,
"log_level": opts.LogLevel,
"wg_port": wgPort,
"nat_external_ips": opts.NATExternalIPs,
"mtu": mtu,
"block_inbound": opts.BlockInbound,
"block_lan_access": opts.BlockLANAccess,
+4
View File
@@ -89,6 +89,9 @@ type Config struct {
// WireguardPort is the UDP port for the embedded NetBird tunnel.
// Zero asks the OS for a random port.
WireguardPort uint16
// NATExternalIPs are external IPs advertised as ICE candidates by the
// embedded tunnel. Requires a fixed WireguardPort forwarded on the NAT.
NATExternalIPs []string
// ProxyProtocol enables PROXY protocol (v1/v2) on TCP listeners.
ProxyProtocol bool
// PreSharedKey is the WireGuard pre-shared key used between the
@@ -160,6 +163,7 @@ func New(ctx context.Context, cfg Config) *Server {
ForwardedProto: cfg.ForwardedProto,
TrustedProxies: cfg.TrustedProxies,
WireguardPort: cfg.WireguardPort,
NATExternalIPs: cfg.NATExternalIPs,
ProxyProtocol: cfg.ProxyProtocol,
PreSharedKey: cfg.PreSharedKey,
Performance: cfg.Performance,
+8 -4
View File
@@ -203,6 +203,9 @@ type Server struct {
// single-account deployments; multiple accounts will fail to bind
// the same port.
WireguardPort uint16
// NATExternalIPs maps external IPs advertised as ICE candidates by every
// embedded client. Requires a fixed WireguardPort forwarded on the NAT.
NATExternalIPs []string
// Performance configures the tunnel pool/batch sizes for every
// embedded client this proxy spawns.
Performance embed.Performance
@@ -581,10 +584,11 @@ func (s *Server) initManagementClient() error {
// inbound listeners.
func (s *Server) initNetBirdClient() error {
s.netbird = roundtrip.NewNetBird(s.ctx, s.ID, s.ProxyURL, roundtrip.ClientConfig{
MgmtAddr: s.ManagementAddress,
WGPort: s.WireguardPort,
PreSharedKey: s.PreSharedKey,
Performance: s.Performance,
MgmtAddr: s.ManagementAddress,
WGPort: s.WireguardPort,
PreSharedKey: s.PreSharedKey,
Performance: s.Performance,
NATExternalIPs: s.NATExternalIPs,
// On --private the embedded client serves per-account inbound
// listeners and must apply management's ACL: keep BlockInbound off
// so the engine creates the ACL manager. On the standalone proxy