[GH-ISSUE #5801] Feature Request: Management server pushes NetBird routes to local gateway routers via TR-064 #11780

Open
opened 2026-08-05 01:30:58 -04:00 by saavagebueno · 3 comments
Owner

Originally created by @renne on GitHub (Apr 5, 2026).
Original GitHub issue: https://github.com/netbirdio/netbird/issues/5801

Originally assigned to: @jnfrati on GitHub.

There is no management-plane webhook (see #1596, #4315) to trigger external route updates when NetBird routes change, so keeping gateway routes in sync currently requires a polling workaround.

Describe the solution you'd like

The implementation would use the LANIPRoute:1 TR-064 service to reconcile routes:

Action Purpose
GetLANIPRouteNumberOfEntries Read current count of static routes
GetGenericLANIPRouteEntry(NewIndex) Read existing routes by index (for reconciliation)
AddLANIPRoute(NewDestIPAddress, NewDestSubnetMask, NewGatewayIPAddress, …) Inject a new route
DeleteLANIPRoute(NewDestIPAddress, NewDestSubnetMask) Remove a stale route

This could be implemented as:

  • A built-in integration in the management server (similar to the existing Generic HTTP Event Streaming integration), configured with the TR-064 endpoint URL and credentials, and triggered on route create/delete events.
  • Or as a webhook on management-plane events (route added/removed, peer joined group) — which would also unblock the general case tracked in #1596.

Why TR-064?

Describe alternatives you've considered

  • Polling loop (current workaround): a small Python service polls GET /api/routes, diffs against the router's known routes (read via GetLANIPRouteNumberOfEntries + GetGenericLANIPRouteEntry), and calls TR-064 to sync. Works, but requires running extra infrastructure and introduces latency.
  • Generic HTTP Event Streaming (existing): only covers network activity/audit events, not management lifecycle events like route creation — so it cannot trigger TR-064 calls today.

Additional context

Originally created by @renne on GitHub (Apr 5, 2026). Original GitHub issue: https://github.com/netbirdio/netbird/issues/5801 Originally assigned to: @jnfrati on GitHub. ### Is your feature request related to a problem? There is no management-plane webhook (see #1596, #4315) to trigger external route updates when NetBird routes change, so keeping gateway routes in sync currently requires a polling workaround. ### Describe the solution you'd like The implementation would use the `LANIPRoute:1` TR-064 service to reconcile routes: | Action | Purpose | |--------|---------| | `GetLANIPRouteNumberOfEntries` | Read current count of static routes | | `GetGenericLANIPRouteEntry(NewIndex)` | Read existing routes by index (for reconciliation) | | `AddLANIPRoute(NewDestIPAddress, NewDestSubnetMask, NewGatewayIPAddress, …)` | Inject a new route | | `DeleteLANIPRoute(NewDestIPAddress, NewDestSubnetMask)` | Remove a stale route | This could be implemented as: - A **built-in integration** in the management server (similar to the existing Generic HTTP Event Streaming integration), configured with the TR-064 endpoint URL and credentials, and triggered on route create/delete events. - Or as a **webhook on management-plane events** (route added/removed, peer joined group) — which would also unblock the general case tracked in #1596. ### Why TR-064? ### Describe alternatives you've considered - **Polling loop** (current workaround): a small Python service polls `GET /api/routes`, diffs against the router's known routes (read via `GetLANIPRouteNumberOfEntries` + `GetGenericLANIPRouteEntry`), and calls TR-064 to sync. Works, but requires running extra infrastructure and introduces latency. - **Generic HTTP Event Streaming** (existing): only covers network activity/audit events, not management lifecycle events like route creation — so it cannot trigger TR-064 calls today. ### Additional context - TR-064 spec: https://avm.de/service/schnittstellen/ - `fritzconnection` Python library: https://fritzconnection.readthedocs.io/ - Related: #1596 (custom event streaming), #4315 (peer up/down webhooks)
saavagebueno added the feature-request label 2026-08-05 01:30:58 -04:00
Author
Owner

@renne commented on GitHub (Apr 6, 2026):

Implementation Notes from a Working Reference Implementation

I've built a working TR-064 Fritz!Box route-injection daemon (renne/netbird-tr064) that does exactly what this issue describes. Here are empirically-verified implementation details and corrections to the information in the original issue.


Service/Action Names in the Original Issue Are Wrong

The original issue references LANIPRoute:1 with actions like GetLANIPRouteNumberOfEntries, AddLANIPRoute, DeleteLANIPRoutethese do not exist on Fritz!Box.

The correct service and actions are:

What Value
Service urn:dslforum-org:service:Layer3Forwarding:1
Control URL /upnp/control/layer3forwarding (varies — must be discovered)
List count GetForwardNumberOfEntriesNewForwardNumberOfEntries
Read entry GetGenericForwardingEntry(NewForwardingIndex=N) → all fields
Add entry AddForwardingEntry(NewType, NewDestIPAddress, NewDestSubnetMask, NewSourceIPAddress, NewSourceSubnetMask, NewForwardingPolicy, NewForwardingMetric, NewInterface, NewGatewayIPAddress)
Enable SetForwardingEntryEnable(NewDestIPAddress, NewDestSubnetMask, NewEnable=1)
Delete DeleteForwardingEntry(NewDestIPAddress, NewDestSubnetMask)

⚠️ Critical Pitfall: Wrong Namespace for Service Discovery

Fritz!Box uses urn:dslforum-org:device-1-0 (not urn:schemas-upnp-org:device-1-0) as the XML namespace in tr64desc.xml. Using the wrong namespace causes silent HTTP 500 errors — no meaningful error message, just a failure. You must use:

NS = {"d": "urn:dslforum-org:device-1-0"}
tree.findall(".//d:service", NS)

⚠️ Critical Pitfall: Two-Step Route Addition

Fritz!Box returns UPnP error 501 if you include NewEnable in AddForwardingEntry. Routes must be added in two steps:

  1. AddForwardingEntry(...)without NewEnable — creates the route in a disabled state
  2. SetForwardingEntryEnable(NewDestIPAddress, NewDestSubnetMask, NewEnable=1) — enables it

If you skip step 2, the route is created but inactive (and won't appear in Fritz!Box web UI under active routes).


⚠️ Critical Pitfall: Ghost Routes

DeleteForwardingEntry does not remove entries — it zeroes their fields to 0.0.0.0. These "ghost" entries:

  • Survive Fritz!Box reboots
  • Block re-addition of the same route (UPnP error 701: ValueAlreadySpecified)
  • Accumulate silently over time

Mitigation: On daemon startup, call DeleteForwardingEntry(0.0.0.0, 0.0.0.0) in a loop until you get a SOAP fault — this removes all ghost entries. Then GetGenericForwardingEntry returns NewEnable=0 for actually-disabled entries, which must be filtered separately from ghost routes (all-zeros destination = ghost; non-zero destination with NewEnable=0 = legitimately disabled).


Required Parameters for AddForwardingEntry

Empirically verified required fields:

<NewType>Host</NewType>                          <!-- NOT "ip-net", "static", or "route" -->
<NewDestIPAddress>10.64.0.0</NewDestIPAddress>
<NewDestSubnetMask>255.192.0.0</NewDestSubnetMask>
<NewSourceIPAddress>0.0.0.0</NewSourceIPAddress>
<NewSourceSubnetMask>0.0.0.0</NewSourceSubnetMask>
<NewForwardingPolicy>None</NewForwardingPolicy>
<NewForwardingMetric>0</NewForwardingMetric>
<NewInterface>LanHostConfigManagement1</NewInterface>
<NewGatewayIPAddress>192.168.178.8</NewGatewayIPAddress>
<!-- NewEnable must NOT be included here — see two-step note above -->

NewType must be "Host". Using "ip-net", "static", or "route" causes a SOAP fault.


CGNAT Range (100.64.0.0/10) Is Accepted

Both Fritz!Box 7530 AX and Fritz!Box 7690 (Fritz!OS 8.20) accept 100.64.0.0/10 and subnets thereof (e.g. 100.91.0.0/16) as static routes via TR-064. Earlier reports of rejection were false positives caused by the wrong-namespace bug described above. Netbird's default overlay range is fine to inject directly.


Authentication

HTTP Digest Auth works for Fritz!OS 8.x. HTTP Basic Auth is available as a fallback for older firmwares. Use the Fritz!Box web UI credentials (not a separate TR-064 user unless explicitly configured).


Architecture Notes (for Netbird-native implementation)

  • No push/webhook from Netbird — polling /api/routes is required (or watch the management server events if implementing inside Netbird itself)
  • Routes with masquerade=true don't need Fritz!Box static routes — the routing peer does source NAT and the return path is handled. Only masquerade=false routes need Fritz!Box static routes so LAN hosts can route back.
  • The overlay subnet (100.64.x.x/16 or similar) itself needs a static route in Fritz!Box if LAN machines should reach Netbird peers directly (i.e., without going through the routing peer's NAT)
  • Fritz!Box supports ~50 static routes maximum — implement a capacity guard

Working reference implementation with full source: https://github.com/renne/netbird-tr064

<!-- gh-comment-id:4193636662 --> @renne commented on GitHub (Apr 6, 2026): ## Implementation Notes from a Working Reference Implementation I've built a working TR-064 Fritz!Box route-injection daemon ([renne/netbird-tr064](https://github.com/renne/netbird-tr064)) that does exactly what this issue describes. Here are empirically-verified implementation details and corrections to the information in the original issue. --- ### ❌ Service/Action Names in the Original Issue Are Wrong The original issue references `LANIPRoute:1` with actions like `GetLANIPRouteNumberOfEntries`, `AddLANIPRoute`, `DeleteLANIPRoute` — **these do not exist on Fritz!Box**. The correct service and actions are: | What | Value | |------|-------| | **Service** | `urn:dslforum-org:service:Layer3Forwarding:1` | | **Control URL** | `/upnp/control/layer3forwarding` (varies — must be discovered) | | **List count** | `GetForwardNumberOfEntries` → `NewForwardNumberOfEntries` | | **Read entry** | `GetGenericForwardingEntry(NewForwardingIndex=N)` → all fields | | **Add entry** | `AddForwardingEntry(NewType, NewDestIPAddress, NewDestSubnetMask, NewSourceIPAddress, NewSourceSubnetMask, NewForwardingPolicy, NewForwardingMetric, NewInterface, NewGatewayIPAddress)` | | **Enable** | `SetForwardingEntryEnable(NewDestIPAddress, NewDestSubnetMask, NewEnable=1)` | | **Delete** | `DeleteForwardingEntry(NewDestIPAddress, NewDestSubnetMask)` | --- ### ⚠️ Critical Pitfall: Wrong Namespace for Service Discovery Fritz!Box uses `urn:dslforum-org:device-1-0` (not `urn:schemas-upnp-org:device-1-0`) as the XML namespace in `tr64desc.xml`. Using the wrong namespace causes **silent HTTP 500 errors** — no meaningful error message, just a failure. You must use: ```python NS = {"d": "urn:dslforum-org:device-1-0"} tree.findall(".//d:service", NS) ``` --- ### ⚠️ Critical Pitfall: Two-Step Route Addition Fritz!Box returns **UPnP error 501** if you include `NewEnable` in `AddForwardingEntry`. Routes must be added in two steps: 1. `AddForwardingEntry(...)` — **without** `NewEnable` — creates the route in a disabled state 2. `SetForwardingEntryEnable(NewDestIPAddress, NewDestSubnetMask, NewEnable=1)` — enables it If you skip step 2, the route is created but inactive (and won't appear in Fritz!Box web UI under active routes). --- ### ⚠️ Critical Pitfall: Ghost Routes `DeleteForwardingEntry` does **not** remove entries — it zeroes their fields to `0.0.0.0`. These "ghost" entries: - Survive Fritz!Box reboots - Block re-addition of the same route (UPnP error 701: `ValueAlreadySpecified`) - Accumulate silently over time **Mitigation**: On daemon startup, call `DeleteForwardingEntry(0.0.0.0, 0.0.0.0)` in a loop until you get a SOAP fault — this removes all ghost entries. Then `GetGenericForwardingEntry` returns `NewEnable=0` for actually-disabled entries, which must be filtered separately from ghost routes (all-zeros destination = ghost; non-zero destination with `NewEnable=0` = legitimately disabled). --- ### ✅ Required Parameters for `AddForwardingEntry` Empirically verified required fields: ```xml <NewType>Host</NewType> <!-- NOT "ip-net", "static", or "route" --> <NewDestIPAddress>10.64.0.0</NewDestIPAddress> <NewDestSubnetMask>255.192.0.0</NewDestSubnetMask> <NewSourceIPAddress>0.0.0.0</NewSourceIPAddress> <NewSourceSubnetMask>0.0.0.0</NewSourceSubnetMask> <NewForwardingPolicy>None</NewForwardingPolicy> <NewForwardingMetric>0</NewForwardingMetric> <NewInterface>LanHostConfigManagement1</NewInterface> <NewGatewayIPAddress>192.168.178.8</NewGatewayIPAddress> <!-- NewEnable must NOT be included here — see two-step note above --> ``` `NewType` must be `"Host"`. Using `"ip-net"`, `"static"`, or `"route"` causes a SOAP fault. --- ### ✅ CGNAT Range (100.64.0.0/10) Is Accepted Both Fritz!Box 7530 AX and Fritz!Box 7690 (Fritz!OS 8.20) **accept** `100.64.0.0/10` and subnets thereof (e.g. `100.91.0.0/16`) as static routes via TR-064. Earlier reports of rejection were false positives caused by the wrong-namespace bug described above. Netbird's default overlay range is fine to inject directly. --- ### ✅ Authentication HTTP Digest Auth works for Fritz!OS 8.x. HTTP Basic Auth is available as a fallback for older firmwares. Use the Fritz!Box web UI credentials (not a separate TR-064 user unless explicitly configured). --- ### Architecture Notes (for Netbird-native implementation) - **No push/webhook from Netbird** — polling `/api/routes` is required (or watch the management server events if implementing inside Netbird itself) - Routes with `masquerade=true` don't need Fritz!Box static routes — the routing peer does source NAT and the return path is handled. Only `masquerade=false` routes need Fritz!Box static routes so LAN hosts can route back. - The overlay subnet (`100.64.x.x/16` or similar) itself needs a static route in Fritz!Box if LAN machines should reach Netbird peers directly (i.e., without going through the routing peer's NAT) - Fritz!Box supports ~50 static routes maximum — implement a capacity guard --- Working reference implementation with full source: **https://github.com/renne/netbird-tr064**
Author
Owner

@jnfrati commented on GitHub (Apr 7, 2026):

Hey @renne ! Thanks for reaching out with this feature request, I'm afraid this might be a bit too niche to the Fritz community as I couldn't find many routers that use TR-064, is the independent daemon not enough for the use case? Does this solve any problem beyond "native support" for the protocol?

<!-- gh-comment-id:4197780868 --> @jnfrati commented on GitHub (Apr 7, 2026): Hey @renne ! Thanks for reaching out with this feature request, I'm afraid this might be a bit too niche to the Fritz community as I couldn't find many routers that use TR-064, is the independent daemon not enough for the use case? Does this solve any problem beyond "native support" for the protocol?
Author
Owner

@renne commented on GitHub (Apr 7, 2026):

Hi @jnfrati The independent daemon is a single-point-of-failure and needs additional administration. It does not work in real-time but with a polling delay. I also couldn't find a way to determine the currently active subnet-router in a stable way (Fritz!Boxes can only handle on route per target-subnet). My current approach is to check which subnet-routers are currently available, sort by metric in the Netbird Management server and then sort by order in config.yaml. This is a statistic match, but not 100% reliable.

A more generic approach would be to add some kind of plugin-API for custom router-connectors or at least a script-hook to the Nebird client.

<!-- gh-comment-id:4199599183 --> @renne commented on GitHub (Apr 7, 2026): Hi @jnfrati The independent daemon is a single-point-of-failure and needs additional administration. It does not work in real-time but with a polling delay. I also couldn't find a way to determine the currently active subnet-router in a stable way (Fritz!Boxes can only handle on route per target-subnet). My current approach is to check which subnet-routers are currently available, sort by metric in the Netbird Management server and then sort by order in config.yaml. This is a statistic match, but not 100% reliable. A more generic approach would be to add some kind of plugin-API for custom router-connectors or at least a script-hook to the Nebird client.
Sign in to join this conversation.
No Label feature-request
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: DYNR/netbird#11780