From 47bcd7cbb306b2a6bc6972552a1c5903ca67996d Mon Sep 17 00:00:00 2001 From: mikkeldamsgaard Date: Sun, 12 Apr 2026 17:48:41 +0200 Subject: [PATCH] docs(netbird): document STUN networking setup (#67) Explain why the STUN service needs a separate Kubernetes Service (UDP cannot traverse HTTP ingress) and document three networking options: separate LoadBalancer, shared static IP, and NodePort. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 6 +++ charts/netbird/README.md | 75 +++++++++++++++++++++++++++++- charts/netbird/templates/NOTES.txt | 6 +++ charts/netbird/values.yaml | 8 +++- 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c34ab3..5c25da0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## Unreleased +### Added + +- **netbird**: Document STUN networking setup in README — explains why STUN + needs a separate service (UDP), and covers options for LoadBalancer, + shared static IP, and NodePort configurations (#67). + ## [0.4.0] — 2026-04-09 ### Changed diff --git a/charts/netbird/README.md b/charts/netbird/README.md index c7f8028..dc94f97 100644 --- a/charts/netbird/README.md +++ b/charts/netbird/README.md @@ -202,6 +202,78 @@ dashboard: - netbird.example.com ``` +## STUN Networking + +NetBird's embedded STUN server uses **UDP port 3478**, which standard HTTP +ingress controllers cannot proxy. The chart therefore creates a dedicated +Kubernetes Service (`server.stunService`) for STUN traffic, separate from +the HTTP ingress. + +NetBird clients derive the STUN URI from `server.config.exposedAddress` — +the hostname in `exposedAddress` is combined with port 3478 to form +`stun::3478`. This means the STUN hostname **must resolve to an +IP that reaches the STUN service**. + +### Option 1: Separate LoadBalancer (default) + +The chart defaults to `server.stunService.type: LoadBalancer`, which +provisions a dedicated external IP for UDP traffic. Because this IP +differs from the ingress controller IP, you need a DNS record that points +to the STUN LoadBalancer: + +``` +netbird.example.com → Ingress IP (HTTP / gRPC / Relay) +stun.netbird.example.com → STUN LB IP (UDP 3478) +``` + +Retrieve the STUN external IP after deployment: + +```bash +kubectl get svc -server-stun -n \ + -o jsonpath='{.status.loadBalancer.ingress[0].ip}' +``` + +If you use a separate hostname for STUN you will also need to configure a +custom STUN URI in the NetBird server config so that clients connect to the +correct address. + +### Option 2: Shared static IP (single DNS entry) + +On cloud providers that support static IP assignment you can give the +**same IP** to both the ingress controller and the STUN LoadBalancer. A +single DNS record then serves both HTTP and UDP traffic: + +```yaml +server: + stunService: + type: LoadBalancer + port: 3478 + annotations: + # GKE example: + networking.gke.io/load-balancer-ip-refs: "my-static-ip" + # AWS NLB with Elastic IP: + service.beta.kubernetes.io/aws-load-balancer-eip-allocations: "eipalloc-xxx" +``` + +Ensure your ingress controller's external Service also uses the same +static IP so that `exposedAddress` resolves to one address for all +protocols. + +### Option 3: NodePort + +Expose STUN on a fixed port across all cluster nodes. Useful when a cloud +LoadBalancer is not available or when nodes already have public IPs: + +```yaml +server: + stunService: + type: NodePort + port: 3478 +``` + +Point DNS at one or more node IPs. Clients will connect on the allocated +NodePort (check `kubectl get svc` for the assigned port). + ## Personal Access Token (PAT) Seeding The chart can optionally seed the database with a Personal Access Token @@ -685,7 +757,8 @@ ADFS) can be tested manually: └──────────────────────────────────────────────────────────┘ │ STUN Service :3478/UDP - (LoadBalancer) + (LoadBalancer — separate IP, + cannot use HTTP Ingress) ``` ## Upstream Source diff --git a/charts/netbird/templates/NOTES.txt b/charts/netbird/templates/NOTES.txt index 16cc957..6923f5f 100644 --- a/charts/netbird/templates/NOTES.txt +++ b/charts/netbird/templates/NOTES.txt @@ -4,6 +4,12 @@ Components: Server (Management + Signal + Relay + STUN): Service: {{ include "netbird.server.fullname" . }}:{{ .Values.server.service.port }} STUN Service: {{ include "netbird.server.fullname" . }}-stun:{{ .Values.server.stunService.port }} ({{ .Values.server.stunService.type }}) + NOTE: STUN uses UDP and cannot go through HTTP ingress. Ensure DNS for + your exposedAddress resolves to an IP that reaches this service. +{{- if eq .Values.server.stunService.type "LoadBalancer" }} + Retrieve the STUN external IP: + kubectl get svc {{ include "netbird.server.fullname" . }}-stun -n {{ .Release.Namespace }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}' +{{- end }} {{- if .Values.server.ingress.enabled }} Server HTTP Ingress (API + OAuth2): diff --git a/charts/netbird/values.yaml b/charts/netbird/values.yaml index 12e96fd..d6b1e14 100644 --- a/charts/netbird/values.yaml +++ b/charts/netbird/values.yaml @@ -306,7 +306,9 @@ server: exposedAddress: "" # -- List of UDP ports for the embedded STUN server. - # These ports must also be exposed via the stunService. + # These ports must also be exposed via the stunService. STUN uses UDP + # which cannot be proxied by standard HTTP ingress controllers — see + # the "STUN Networking" section in README.md for setup options. stunPorts: - 3478 @@ -407,6 +409,10 @@ server: port: 80 # -- Dedicated service for the STUN/TURN UDP port. + # STUN uses UDP which cannot go through a standard HTTP ingress, so this + # service gets its own external IP by default (LoadBalancer). To use a + # single DNS entry, assign a shared static IP via annotations. See the + # "STUN Networking" section in README.md for detailed options. stunService: type: LoadBalancer port: 3478