Syncs NetBird routes into gateway routers via TR-064 SOAP, enabling automatic static-route injection for Fritz!Box and other TR-064-compliant devices — no firmware changes required.
This service was created as a workaround pending native Fritz!OS support (see netbirdio/netbird#5801).
- Uses the Networks API (
GET /api/networks→/resources+/routers) to fetch the current route set from your NetBird management server (or cloud API) everypoll_intervalseconds. - Compares the current NetBird route set against the static routes already present in each configured router.
- Skips routes with
masquerade=true— the subnet router NATes overlay traffic, so no static route is needed on the gateway router. - Adds missing routes via
AddLANIPRouteand removes orphaned routes viaDeleteLANIPRoute. - Uses an ownership rule: only routes whose gateway address matches one of
the configured peer LAN IPs are ever touched — foreign routes (e.g. added by
WireGuard VPN or manually) are never deleted.
If a desired destination is already covered by a foreign route, a
WARNINGis logged and the route is skipped. - Metric-aware failover: for each CIDR, the daemon picks the online peer
with the lowest NetBird
metricas the active gateway (mirroring NetBird client peer-selection). Config order breaks ties. If that peer goes offline, the route is re-pointed to the next best peer within onepoll_interval. If all peers are offline, the route is removed to avoid a silent blackhole.
Because NetBird has no management-plane webhooks (#1596, #4315), polling is currently the only viable approach.
TR-064 must be enabled on your Fritz!Box:
- Open the Fritz!Box web interface → Home Network → Network → Network Settings
- Scroll down to Remote Access for Applications
- Enable Allow access for applications (activates TR-064 on port 49000)
For static route management the Fritz!Box user needs no special permissions beyond basic authentication; the default admin account works.
mkdir -p /srv/docker/netbird/netbird-tr064
cp config.example.yaml /srv/docker/netbird/netbird-tr064/config.yaml
# Edit config.yaml with your token, router URL, and credentialsAdd to your existing NetBird Docker Compose stack:
services:
netbird-tr064:
image: ghcr.io/renne/netbird-tr064:latest
restart: unless-stopped
volumes:
- ./netbird-tr064/config.yaml:/config/config.yaml:ro
networks:
- netbird
depends_on:
- netbird-managementSee docker-compose.example.yml for a full example.
netbird:
management_url: "http://netbird-management:80"
# Cloud alternative: https://api.netbird.io
api_token: "nbp_..."
sync:
poll_interval: 60 # seconds between reconciliation passes
only_enabled: true # skip routes marked disabled in NetBird
routers:
- name: "fritzbox"
backend: tr064
url: "http://192.168.178.1:49000"
username: "admin"
password: "secret"
peers:
"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx": "192.168.178.x" # primary routing peer
# "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy": "192.168.178.y" # secondary (HA failover)| Field | Required | Default | Description |
|---|---|---|---|
netbird.management_url |
Yes | — | NetBird management base URL |
netbird.api_token |
Yes | — | NetBird API token (nbp_…) |
sync.poll_interval |
No | 60 |
Seconds between polls |
sync.only_enabled |
No | true |
Skip disabled routes |
routers[].name |
Yes | — | Friendly name for logging |
routers[].backend |
Yes | — | Backend type; currently only tr064 |
routers[].url |
Yes | — | TR-064 base URL (e.g. http://192.168.178.1:49000) |
routers[].username |
Yes | — | Router admin username |
routers[].password |
Yes | — | Router admin password |
routers[].peers |
Yes | — | Map of peer_id → LAN IP for routing peers; defines next-hop addresses. The online peer with the lowest NetBird metric is selected; config order breaks ties. |
routers[].exclude_subnets |
No | [] |
CIDRs to skip and protect: never injected even if present in NetBird, and existing routes matching them are never deleted. Use for subnets covered by a Fritz!Box VPN tunnel. See VPN route limitation. |
Each router requires a peers map linking NetBird peer IDs to their LAN IP
addresses on that router's network.
Finding a peer ID: run netbird status on the routing peer device — the peer
ID is shown at the top. It is also visible in the NetBird management dashboard
under Peers.
Stable IP requirement: the LAN IP must not change, because the Fritz!Box uses it as the static route next-hop. Use either a static IP configured on the peer itself, or a DHCP reservation (fixed lease by MAC address) on the Fritz!Box.
High availability: list multiple peers. The daemon selects the online peer with
the lowest NetBird routing metric as the active gateway — matching the same
peer-selection logic used by NetBird clients (GET /api/networks/{id}/routers
metric field, lower = preferred). Config order breaks ties. If the active peer
goes offline, the next-best peer takes over within one poll_interval. If all
peers are offline, the route is removed.
TODO: The
peersmap shall be removed once the NetBird management API exposes per-peer LAN IP addresses. Thepeersmap must be maintained manually for now — research confirmed that no current API path returns a routing peer's LAN IP (GET /api/peersreturns the overlay mesh IP and WAN/public IP only;GET /api/routesreturns routed CIDRs and peer IDs but no next-hop LAN address). Tracked upstream: netbirdio/netbird#5810
Fritz!Box WireGuard and IPsec VPN tunnels automatically install kernel routes for
their remote subnets. Those routes collide with the NetBird routes this daemon
would inject (e.g. both want 192.168.178.0/24 via different gateways).
The exclude_subnets list is how you tell the daemon to skip those CIDRs entirely.
⚠️ This list must be maintained manually — forever.The TR-064 protocol provides no way to read VPN-installed routes:
Fritz!Box VPN type TR-064 exposure WireGuard (FRITZ!OS 7.50+) Zero TR-064 API — configured via UI only IPsec site-to-site Kernel-internal routes, invisible to all SOAP services IPsec roadwarrior ( X_AVM-DE_AppSetup:1)Write-only credential push; no route read action exists Whenever you add, remove, or change a WireGuard or IPsec tunnel on a Fritz!Box, update
exclude_subnetsinconfig.yamlaccordingly and restart the daemon.
exclude_subnets has two distinct effects on every sync cycle:
- Injection suppression — any NetBird-advertised route whose CIDR is covered by an excluded entry is never added to the Fritz!Box.
- Deletion protection — any existing Fritz!Box static route whose CIDR is covered by an excluded entry is never deleted by the daemon, even if it is absent from NetBird.
This makes exclude_subnets the right place for subnets that belong to a Fritz!Box VPN
tunnel (avoid collision + avoid deletion of the VPN anchor).
| Variable | Default | Description |
|---|---|---|
CONFIG_PATH |
/config/config.yaml |
Path to config file |
LOG_LEVEL |
INFO |
Python logging level (DEBUG, INFO, WARNING) |
Add as many routers as needed under the routers list.
Each router is synced independently in every poll cycle.
routers:
- name: "fritzbox-site-a"
backend: tr064
url: "http://10.0.0.1:49000"
username: "admin"
password: "secret1"
peers:
"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx": "10.0.0.x"
- name: "fritzbox-site-b"
backend: tr064
url: "http://192.168.178.1:49000"
username: "admin"
password: "secret2"
peers:
"yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy": "192.168.178.x"The RouterBackend ABC lives in src/backends/base.py.
Implement three methods and register the backend in src/backends/__init__.py:
class RouterBackend(ABC):
def get_routes(self) -> set[tuple[str, str, str]]: ... # {(dest_ip, mask, gateway_ip), …}
def add_route(self, dest: str, mask: str, gateway: str) -> None: ...
def delete_route(self, dest: str, mask: str) -> None: ...Register in src/backends/__init__.py:
BACKENDS = {
"tr064": TR064Backend,
"myvendor": MyVendorBackend,
}| Device | FRITZ!OS |
|---|---|
| FRITZ!Box 7530 AX | 8.20 |
| FRITZ!Box 7690 | 8.20 |
TR-064 (Broadband Forum) is implemented by Fritz!Box, Teltonika, and many other consumer and SMB routers. The TR-064 backend in this project uses only:
Layer3Forwarding:1— static route management- HTTP Digest Auth (RFC 2617) with Basic Auth fallback
requests+xml.etree.ElementTree— no vendor-specific library
- netbirdio/netbird#5810 —
Feature request: expose per-peer LAN IP (
Meta.NetworkAddresses) inGET /api/peers(blockspeersmap removal) - netbirdio/netbird#5801 — Feature request: management server TR-064 route injection
- netbirdio/netbird#669 — Feature request: native Fritz!OS NetBird client