DEV Community

Cover image for How Two Processes Fighting Over One WireGuard Interface Broke Our NetBird Mesh in Kubernetes
Jagoum
Jagoum

Posted on

How Two Processes Fighting Over One WireGuard Interface Broke Our NetBird Mesh in Kubernetes

How Two Processes Fighting Over One WireGuard Interface Broke Our NetBird Mesh in Kubernetes

We run NetBird as a WireGuard mesh overlay across our OpenStack Kubernetes cluster. Three router pods (DaemonSet) advertise K8s service and pod CIDRs to remote agents over the mesh. One proxy pod forwards agent traffic to K8s ClusterIPs.

Last week, all three routers went unstable — relays disconnected, peers dropped to 0/0, and the engine entered a restart loop with 256+ restarts. Here's how we found and fixed it.

The Symptoms

Management: Connected
Signal: Connected  
Relays: 0/1 Available    ← only 1 relay, none connected
Networks: -              ← CIDRs not advertised
Peers: 0/0 Connected     ← nobody can reach us
Enter fullscreen mode Exit fullscreen mode

Meanwhile, the proxy pod on the same cluster was rock-solid:

Management: Connected
Relays: 3/3 Available
Peers: 7/14 Connected
Enter fullscreen mode Exit fullscreen mode

Both were running the same NetBird version (0.73.2), same setup key, same management URL. But the routers were dying and the proxy wasn't.

Investigation: Layer by Layer

Layer 1: Version Mismatch (Red Herring)

The original symptom was failed to handle new offer: relay connection is not established and relay WebSocket connections dying with EOF every ~1.6 seconds. We'd recently upgraded the routers from 0.73.2 to 0.74.2, but the self-hosted relay server was still 0.73.x.

Fix: Downgraded routers back to 0.73.2. The relay connection storms stopped. But the instability remained.

Layer 2: Stale WireGuard Interfaces

After the downgrade, two routers couldn't bind port 51820 — the old WireGuard interface (wt0) persisted on the node from the previous pod. We deleted the stale interfaces:

kubectl exec -n netbird-system <pod> -- ip link delete wt0
kubectl delete pod -n netbird-system <pod>
Enter fullscreen mode Exit fullscreen mode

Workers 2 and 3 recovered. Worker-1 didn't.

Layer 3: The Dual-Process Bug

Worker-1's logs told the real story:

2026-07-14T14:50:14.631Z netbird WireGuard interface monitor: interface wt0 deleted, restarting engine
2026-07-14T14:50:15.354Z netbird stopped Netbird Engine
2026-07-14T14:50:16.098Z netbird new WireGuard interface wt0 has been created
2026-07-14T14:50:18.307Z netbird WireGuard interface monitor: interface wt0 deleted, restarting engine
Enter fullscreen mode Exit fullscreen mode

The WGIfaceMonitor was detecting wt0 deletions every few seconds, triggering engine restarts in a loop.

The root cause: The DaemonSet had no explicit command, so it used the image's default entrypoint. That entrypoint does this:

main() {
  "${NETBIRD_BIN}" service run &    # Process 1: daemon mode
  wait_for_daemon_startup 30
  connect                           # Process 2: "netbird up" (foreground)
  wait "${service_pids[@]}"
}
Enter fullscreen mode Exit fullscreen mode

Two independent NetBird engines were running in the same container, both managing separate WireGuard interfaces, both fighting for the same management gRPC connection. The service run daemon would create wt0, the foreground netbird up process would see the conflict and interfere, the monitor would detect the interference as a deletion, and the engine would restart.

The proxy pod was stable because it explicitly sets command: ["netbird", "service", "run"] — a single process.

Layer 4: Cilium WireGuard Port Conflict

Compounding the problem: Cilium has enable-wireguard: true, which creates cilium_wg0 on every node. The kernel WireGuard module binds UDP port 51820 with no visible userspace process:

$ ss -ulnp | grep 51820
UNCONN  0  0  0.0.0.0:51820  ← kernel socket, no process owner
Enter fullscreen mode Exit fullscreen mode

NetBird's default WireGuard port is also 51820. With the kernel socket holding the port, NetBird falls back to random ports — which works but adds instability when combined with the dual-process fight.

Layer 5: Missing Hardening

The proxy had these env vars set but the routers didn't:

  • NB_DISABLE_NAT_MAPPER=true — disables UPnP discovery (useless on cloud VMs, clutters logs)
  • NB_DISABLE_EBPF_WG_PROXY=true — disables eBPF proxy that can hold ports across restarts

The Fix

Match the proxy's configuration. One command change + two env vars:

containers:
- name: netbird
  command: ["netbird", "service", "run"]   # ← explicit single process
  env:
  - name: NB_SETUP_KEY
    valueFrom:
      secretKeyRef:
        key: setup_key
        name: netbird-router-setup-key
  - name: NB_WIREGUARD_PORT
    value: "51820"
  - name: NB_MANAGEMENT_URL
    value: "https://netbird.vpn.skyengpro.app:443"
  - name: NB_HOSTNAME
    value: "$(NODE_NAME)"
  - name: NB_DISABLE_NAT_MAPPER           # ← new
    value: "true"
  - name: NB_DISABLE_EBPF_WG_PROXY        # ← new
    value: "true"
Enter fullscreen mode Exit fullscreen mode

The liveness probe also needed updating to only check for the single process:

livenessProbe:
  exec:
    command: ["sh", "-c", "pgrep -f 'netbird service' >/dev/null || kill -0 1"]
Enter fullscreen mode Exit fullscreen mode

After the Fix

All three routers went from broken to fully stable:

Worker-1 Worker-2 Worker-3
Before 0/0 peers, 256 restarts 0/0 peers, 264 restarts 3-5/13 peers, flapping
After 7/13 peers, 0 restarts 7/13 peers, 0 restarts 7/13 peers, 0 restarts

Management ✅ Signal ✅ Relays: 3/3 ✅ Networks: 10.20.96.0/19 ✅

Lessons Learned

1. The proxy was your best reference implementation. It was the only stable component all along. When debugging, compare working vs broken configs line by line.

2. Image entrypoints can surprise you. We didn't set a command in the DaemonSet, so we got the image's default entrypoint — which runs two processes. Always check docker inspect <image> or read the Dockerfile before deploying.

3. Kernel WireGuard modules don't show in ss -ulnp. The cilium_wg0 socket was invisible because the kernel module owns it, not a userspace process. This caused confusing "port 51820 is in use" messages with no visible process.

4. HostNetwork pods share the host's WireGuard module state. Stale wt0 interfaces survive pod deletion. When rolling out new pods, you may need to clean up old interfaces first.

5. Don't mix service run and foreground netbird up. They're two independent engines that fight over the same interface name and management connection. Pick one.

TL;DR

The DaemonSet's default entrypoint started two NetBird engines in the same container. They fought over the wt0 WireGuard interface, causing an infinite restart loop. The fix: explicitly set command: ["netbird", "service", "run"] and add NB_DISABLE_NAT_MAPPER=true + NB_DISABLE_EBPF_WG_PROXY=true.


If you're running NetBird in Kubernetes, check your DaemonSet entrypoint. If it doesn't explicitly set command, you might have the same dual-process bug hiding in plain sight.

Top comments (0)