From 4f4922bc3d7bac7581a1dcd5ad3a1002d69d62df Mon Sep 17 00:00:00 2001 From: emrcbrn Date: Mon, 3 Aug 2026 11:46:05 +0200 Subject: [PATCH 1/2] Document the dual-stack IPv6 gateway-IP issue for self-hosted On dual-stack hosts whose NetBird domain has an AAAA record, peers that connect over IPv6 are recorded with the Docker bridge gateway (e.g. 172.30.0.1) as their Public IP, because Docker's userland proxy re-originates the connection from the gateway before any reverse proxy sees it. Header and trustedHTTPProxies settings cannot fix it. - Quickstart: add an "IPv6 and dual-stack hosts" subsection under Troubleshoot (symptom, cause, the enable_ipv6 + ip6tables fix pair, and the AAAA-removal alternative), plus a short pointer on the domain prerequisite. Distinguishes this from overlay IPv6. - External reverse proxy: note that the gateway-IP symptom is not a header problem, linking to the fix. - Self-hosted dashboard troubleshooting: symptom-phrased entry linking to the fix. Ref: NET-1461 Co-Authored-By: Claude Opus 4.8 --- .../selfhosted/external-reverse-proxy.mdx | 4 ++ .../selfhosted/selfhosted-quickstart.mdx | 44 ++++++++++++++++++- .../selfhosted/troubleshooting/dashboard.mdx | 6 +++ 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/src/pages/selfhosted/external-reverse-proxy.mdx b/src/pages/selfhosted/external-reverse-proxy.mdx index 915ce9af2..6a3876a2a 100644 --- a/src/pages/selfhosted/external-reverse-proxy.mdx +++ b/src/pages/selfhosted/external-reverse-proxy.mdx @@ -8,6 +8,10 @@ This is not to be confused with the NetBird reverse proxy service that launched Not all reverse proxies are supported as NetBird uses *gRPC* for various components. Your reverse proxy must support HTTP/2 and gRPC proxying. + +If the dashboard shows a peer's **Public IP** as the Docker network's gateway address (for example `172.30.0.1`), this is not a header or `trustedHTTPProxies` problem. On a dual-stack host, IPv6 traffic is re-originated from the bridge gateway before your reverse proxy sees it, so `X-Forwarded-For` and `X-Real-IP` cannot recover the real address. See [IPv6 and dual-stack hosts](/selfhosted/selfhosted-quickstart#i-pv6-and-dual-stack-hosts) for the fix. + + Starting with **v0.65.0**, new NetBird installations use a **combined container** (`netbirdio/netbird-server`) that merges management, signal, and relay into a single service. This simplifies reverse proxy configuration because all traffic routes to one backend. The templates on this page cover both setups: - **[Combined container (v0.65.0+)](#combined-container-setup-v0-65-0)** -- the default for new installations. All backend services run in a single container on one port. diff --git a/src/pages/selfhosted/selfhosted-quickstart.mdx b/src/pages/selfhosted/selfhosted-quickstart.mdx index bd6f73ba8..6887e63ea 100644 --- a/src/pages/selfhosted/selfhosted-quickstart.mdx +++ b/src/pages/selfhosted/selfhosted-quickstart.mdx @@ -11,7 +11,7 @@ This is the quickest way to try self-hosted NetBird. It should take around 5 min ## Infrastructure requirements - A Linux VM with at least **1CPU** and **2GB** of memory. - The VM must be publicly accessible on **TCP ports 80 and 443**, and **UDP port 3478**. -- A **public domain** name that resolves to the VM's public IP address (e.g. `netbird.example.com`). +- A **public domain** name that resolves to the VM's public IP address (e.g. `netbird.example.com`). On a dual-stack host, see [IPv6 and dual-stack hosts](#i-pv6-and-dual-stack-hosts) below. ## Software requirements @@ -192,6 +192,48 @@ Once your NetBird instance is running, refer to these guides for ongoing mainten Check that the connector is properly configured in **Settings** → **Identity Providers**. Ensure the redirect URL is correctly configured in your IdP. +### IPv6 and dual-stack hosts + +On a host that is dual-stack and whose NetBird domain also has an `AAAA` record, peers that connect over IPv6 can be recorded with the Docker bridge gateway address (for example `172.30.0.1`) as their **Public IP** in the dashboard, shown with Region **Unknown**. The same peer shows its real public IP when it connects over IPv4, so the value appears to flip between logins. + +This happens because the getting-started script creates an IPv4-only Docker network. IPv4 traffic reaches the containers through iptables NAT, which preserves the client's source IP, but IPv6 traffic has no path into the bridge and is handled by Docker's userland proxy. That proxy re-originates the connection over IPv4 from the bridge gateway, so management records the gateway address. It happens before any reverse proxy sees the request, so `X-Forwarded-For`, `X-Real-IP`, and `trustedHTTPProxies` settings cannot correct it. + +To fix it, give the Docker network a real IPv6 path. Apply both steps together: + +1. Enable IPv6 on the `netbird` network in `docker-compose.yml`, with a private (ULA) subnet: + +```yaml +networks: + netbird: + enable_ipv6: true + ipam: + config: + - subnet: fd00:b14d::/64 +``` + +2. Make sure Docker's `ip6tables` is active. It is enabled by default on Docker Engine 27 and later. On older engines, enable it in `/etc/docker/daemon.json` and restart Docker: + +```json +{ + "ipv6": true, + "ip6tables": true +} +``` + + +Enabling `enable_ipv6` without `ip6tables` can expose containers directly over IPv6, with no NAT and no Docker-managed filtering. Always apply the two together. + + +If you do not need IPv6 reachability, there is a simpler alternative: remove the `AAAA` record for your NetBird domain so every peer connects over IPv4. + + +On hosts where IPv6 is disabled in the kernel (the `ipv6.disable=1` boot parameter), creating an IPv6-enabled network fails at `docker compose up`. Use the `AAAA`-removal alternative on those hosts. + + + +This section is about the Docker network the NetBird server runs on, not the IPv6 addresses peers receive inside the NetBird network. For the latter, see [IPv6 Overlay Addressing](/manage/settings/ipv6). + + For more troubleshooting help, see the [Troubleshooting guide](/selfhosted/troubleshooting). --- diff --git a/src/pages/selfhosted/troubleshooting/dashboard.mdx b/src/pages/selfhosted/troubleshooting/dashboard.mdx index 742cc1444..251218ddb 100644 --- a/src/pages/selfhosted/troubleshooting/dashboard.mdx +++ b/src/pages/selfhosted/troubleshooting/dashboard.mdx @@ -28,3 +28,9 @@ Problems with the self-hosted dashboard. For other areas, start from [Troublesho 3. **Management can't validate the token.** Review `docker compose logs management` for token-validation errors. **Confirm**: Re-run the action. It succeeds, and the Management logs show no auth errors. + +## Peer Public IP shows a Docker or internal IP + +**Symptom**: A peer's **Public IP** in the dashboard shows the Docker bridge gateway (for example `172.30.0.1`) with Region **Unknown**, and it changes back to the real public IP on the next login. + +**Cause and fix**: On a dual-stack host whose NetBird domain has an `AAAA` record, IPv6 connections reach the containers through Docker's userland proxy, which rewrites the source to the bridge gateway before any reverse proxy sees it. Give the Docker network a real IPv6 path, or remove the `AAAA` record. See [IPv6 and dual-stack hosts](/selfhosted/selfhosted-quickstart#i-pv6-and-dual-stack-hosts) for the full fix. From ba6fc25be047ac73966c9b04ea8b27ac95cd9025 Mon Sep 17 00:00:00 2001 From: emrcbrn Date: Mon, 3 Aug 2026 12:27:30 +0200 Subject: [PATCH 2/2] Add network recreation and version-specific ip6tables steps (CodeRabbit) - The fix applies to a running deployment, and Docker only reads network options when it first creates a network, so add a step to recreate the netbird network (docker compose down/up), with a warning not to remove data volumes. - Make the daemon.json guidance version-specific: ip6tables is default on Docker Engine 27+, and older engines need "experimental": true. Drop the daemon-level "ipv6": true, which only affects the default bridge and is not needed here. Co-Authored-By: Claude Opus 4.8 --- src/pages/selfhosted/selfhosted-quickstart.mdx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/pages/selfhosted/selfhosted-quickstart.mdx b/src/pages/selfhosted/selfhosted-quickstart.mdx index 6887e63ea..10014cb1c 100644 --- a/src/pages/selfhosted/selfhosted-quickstart.mdx +++ b/src/pages/selfhosted/selfhosted-quickstart.mdx @@ -198,7 +198,7 @@ On a host that is dual-stack and whose NetBird domain also has an `AAAA` record, This happens because the getting-started script creates an IPv4-only Docker network. IPv4 traffic reaches the containers through iptables NAT, which preserves the client's source IP, but IPv6 traffic has no path into the bridge and is handled by Docker's userland proxy. That proxy re-originates the connection over IPv4 from the bridge gateway, so management records the gateway address. It happens before any reverse proxy sees the request, so `X-Forwarded-For`, `X-Real-IP`, and `trustedHTTPProxies` settings cannot correct it. -To fix it, give the Docker network a real IPv6 path. Apply both steps together: +To fix it, give the Docker network a real IPv6 path. Apply these steps together: 1. Enable IPv6 on the `netbird` network in `docker-compose.yml`, with a private (ULA) subnet: @@ -211,15 +211,24 @@ networks: - subnet: fd00:b14d::/64 ``` -2. Make sure Docker's `ip6tables` is active. It is enabled by default on Docker Engine 27 and later. On older engines, enable it in `/etc/docker/daemon.json` and restart Docker: +2. Make sure Docker's `ip6tables` support is active, so Docker installs the IPv6 NAT and filter rules for the network. On **Docker Engine 27 and later** it is enabled by default, so no change is needed. On **older engines** it was experimental, so enable it in `/etc/docker/daemon.json` and restart Docker: ```json { - "ipv6": true, + "experimental": true, "ip6tables": true } ``` +3. Recreate the `netbird` network so Docker applies the new options. Docker reads a network's settings only when it first creates the network, so an already-running deployment keeps its old IPv4-only network until you recreate it. From your NetBird directory: + +```shell +docker compose down +docker compose up -d +``` + +`docker compose down` removes the containers and networks but keeps your named volumes, so no data is lost. Do not add the `-v` flag, which would delete the data volumes. + Enabling `enable_ipv6` without `ip6tables` can expose containers directly over IPv6, with no NAT and no Docker-managed filtering. Always apply the two together.