Skip to content

OpenThread Border Router: Fixing Headaches with FW and Routing - #4803

Draft
tannerdsilva wants to merge 19 commits into
home-assistant:masterfrom
tannerdsilva:otbr-routing-corrections
Draft

OpenThread Border Router: Fixing Headaches with FW and Routing#4803
tannerdsilva wants to merge 19 commits into
home-assistant:masterfrom
tannerdsilva:otbr-routing-corrections

Conversation

@tannerdsilva

@tannerdsilva tannerdsilva commented Aug 26, 2026

Copy link
Copy Markdown

What this is for

I run this add-on on a live commercial deployment: a single-floor office around 35,000 sq ft with 450+ Thread devices, six border routers, and two switching racks on one shared L2. At that size, whatever breaks breaks loudly, and it keeps breaking until you fix it. Neither problem this PR fixes was something I read about. Both happened to me, at scale, and both are why the changes here exist.

Routing loop

On a multi-BR network sharing one broadcast domain, a border router can install the broad fc00::/7 ULA route. That swallows the whole ULA space. Devices end up stranded on stale prefixes, and the mesh starts routing in circles.

mDNS leak loop

Current OTBR releases leak the agent's mDNS advertisements out of both interfaces, wpan0 and the backbone. On a shared fabric with a second border router, that wraps around. An advertisement leaves one BR's wpan0, crosses the mesh, shows up at the next BR, and comes back out its backbone. The network ends up forwarding its own discovery traffic in a loop.

What changed

  • Deterministic OMR prefix. custom_omr_prefix allows all border routers to operate in the same prefix, which removes the trigger for the catch-all route. Patches 0003 and 0004 make the ULA prefix reproducible and persisted.
  • Catch-all route removed. Patch 0005 puts the broad fc00::/7 route behind a compile-time flag. Patch 0006 tightens the Routing Manager to match exact /64s and publish the real OMR prefix instead of ::/0.
  • Firewall rebuilt. The nftables table ip6 otbr (seven chains) replaces the minimal ip6tables rules: default-deny in both forwarding directions, host input and output protection, TREL isolation, scope-gated multicast, ipset-to-nftables sync, and Docker's DOCKER-USER chain. The scope gate is what contains mDNS. Link-local multicast, where mDNS lives (ff02::fb), is dropped at the boundary in both directions, so an advertisement can't reach another border router and loop.
  • Safety services. otbr-route-guard, otbr-ipset-sync, and otbr-wpan-sysctl cover the edge cases that only show up under load.
  • Thread 1.4. DHCPv6 Prefix Delegation, Multi-AIL detection, peer-BR tracking.

A note on the firewall setting

I want to be upfront about the firewall toggle in the app's config frontend. I didn't know what to do with it. Even with that enabled setting in place, the old behavior fell short of actually being enough, and it was hard to see what the firewall was even supposed to be blocking or managing. So this PR rebuilds that layer from the ground up rather than trying to patch the toggle. If anyone can point me at how the firewall rules should best be sliced out for the config UI, I'm glad to re-apply it properly on top of this.

Tested where it lives

The network that exposed these bugs is the same one validating the fix. Both loops reproduced at that scale, and both are gone there. The image also builds cleanly from a fresh host. Validating against 450+ nodes across six border routers is a proof most OTBR deployments never get to run.

… leadership weight recognition across the mesh
- COPY 0003-disable-fc00-route.patch and 0004-routing-manager-corrections.patch
  into the build context (they were applied but never copied: latent build break).
- Sync openthread-core-ha-config-posix.h with ts-otbr: enable
  DISABLE_BROAD_ULA_ROUTE, MULTI_AIL_DETECTION, TRACK_PEER_BR_INFO, USE_HEAP.

Routing patches remain byte-identical to ts-otbr (0005==0003, 0006==0004).
…arding)

Replace blanket multicast/broadcast drop in both forward chains with
scope-gated forwarding per Thread 1.3+ spec: only admin-local scope
(ff04::/16) and above cross the BR boundary; mesh-local (ff02/ff03) and
broadcast never leak. Add mcast-fwd-scope set. Align MLE/TMF control-plane
allow, SRP/ND handling, and forward-chain priority (mangle->filter).

Byte-identical to ts-otbr working tree.
…c, wpan-sysctl)

Bring the runtime hardening services into the fork, byte-identical to
ts-otbr: otbr-route-guard (deletes RA-learned OMR routes), otbr-ipset-sync
(continuous ipset->nft set sync), otbr-wpan-sysctl (IPv6 hardening on wpan0).
Wire otbr-ipset-sync and otbr-wpan-sysctl into user/contents.d to match
ts-otbr.
Enable the RA-route defense service so it actually starts at boot,
keeping the s6 service wiring in sync with ts-otbr.
Remove upgrade_threshold and downgrade_threshold from the add-on config
schema, defaults, and frontend translations. These expose Thread router
upgrade/downgrade targets that are not needed for this submission.
Remove upgrade_threshold and downgrade_threshold from the DOCS.md
configuration table and the CHANGELOG docs line, matching the earlier
removal of the options from the config UI.
… script

The upgrade_threshold and downgrade_threshold options were removed from the
config schema and UI, so the bashio blocks that applied them are unreachable.
Delete the dead ot-ctl routerupgradethreshold / routerdowngradethreshold
handling.

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @tannerdsilva

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant

Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@Apollon77

Apollon77 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

@tannerdsilva For the fc00::/7 route issue, did you try simply disabling OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE ?

... see also openthread/openthread#13562

Also @agners @puddly

@Apollon77

Copy link
Copy Markdown
Contributor

@tannerdsilva and for the firewall topic maybe openthread/ot-br-posix#3325 is the best solution

@tannerdsilva

tannerdsilva commented Aug 28, 2026

Copy link
Copy Markdown
Author

There's a lot in this. As I'm looking over the diff, I'm seeing things like the "suggested ULA prefix" upstream patch. Things like this are where I started (in speculation), not where I ended (in applied success). Should be removed.

I'll shake that out tonight, while also investigating the above concerns.

In my opinion, most painful part of this was the border router mDNS leak into thread interface. I have found this is super problematic because it causes "inverted L2 uplinks" and entropy on Thread in general. Unfortunately, it's entropy that can't be power cycled away at all - only time heals. My environment also required an elevated leader weight to resolve a complete offline deadlock after the mDNS loop was capped.

My fork started by addressing the fc00::/7 route issue because I knew I had undefined Thread issues, and this seemed like a valid angle to investigate. I then went "scorched earth" on all traffic after seeing no improvement in the routing patches alone. The fc00::/7 patches stayed in because it does seem like an incremental and valid technical improvement to an issue that I didn't discover (therefore couldn't feel entitled to omit it on my own).

@tannerdsilva

Copy link
Copy Markdown
Author

IMO: the consistent OMR prefix is important here. I can't say why, other than this:

  1. My own trial and error evals make me believe that this enables much stronger meshing with multi-BR networks.

  2. I have discovered that Apple border routers share OMR prefix across all BR in a network.

I think the easiest way to bring existing installations forward on this is to hash a private-range IPv6 OMR prefix from some public data field of the thread credentials (maybe hashing the network name). I will update this PR with this logic because I believe this is a "no downside, only upside" improvement for all network configurations of all sizes.

@Apollon77 as described, this static omr prefix with multiple BR is causing my visualization to break as we discussed in DM.

…to one

- Bump build pins to ot-br-posix main tip 4e5cfca4 (includes home-assistant#3325
  in-process opt-in nftables backend as the new base)
- Drop patches now upstream: 0001 (SO_REUSEADDR), 0002 (NAT64 IPv4
  options/security hardening)
- Drop speculative ULA-prefix patches (0003/0004) and the broad-ULA-route
  patch (0005) plus orphaned duplicates
- Keep 0006 routing-manager corrections, re-based for the new upstream
  NetworkDataContainsUlaRoute() logic (kUlaPrefix /7->/64, publish real
  OMR prefix, never publish ::/0)
- Replace 0005 with OPENTHREAD_POSIX_CONFIG_INSTALL_EXTERNAL_ROUTES_ENABLE=0
  in the project config header (the upstream #13562 mechanism)
- Build validated on clean Ubuntu 24.04 host (amd64, beta+stable, 0 errors)
Build both beta and stable with -DOTBR_NFTABLES=ON: the upstream home-assistant#3325
in-process backend (libnftnl/libmnl) owns the ingress filter and NAT44
and forces OT_FIREWALL off, so there is no legacy ipset producer. Add
libnftnl-dev and libmnl-dev to both builder stages.

NOTE: runtime wiring in otbr-agent/run is a follow-up commit. Deploying
this commit alone would leave the add-on shell firewall without its ipset
source.
With -DOTBR_NFTABLES=ON the agent owns unicast ingress into the Thread
interface and NAT44 in-process (OT_FIREWALL is compiled out, so the
classic ipsets are never populated). Adapt otbr-agent/run:

- Detect the backend via the build marker under the OTBR install prefix
  (/opt/otbr-{stable,beta}/share/otbr/nftables-backend), env override wins
- Skip legacy ipset creation and ipset->nft sync in backend mode
- Add-on 'ip6 otbr' forward chains end in 'return', so unicast ingress
  falls through to the agent's inet otbr forward_ingress (the add-on chain
  evaluates first at the same hook/priority; a terminal accept/drop would
  terminate the hook and bypass the backend)
- Keep host input/output protection, scope-gated multicast / mDNS
  containment, TREL isolation and DOCKER-USER hole; replace the now-empty
  ingress allow/deny set matches with rate-limited generic accepts
- Skip the add-on NAT64 table in backend mode (agent owns NAT44 masquerade)
- firewall toggle now slices the add-on layer only
- otbr-ipset-sync exits early in backend mode (nothing to sync)

Image-verified: marker=1 under the prefix, detection=1, backend symbol
present, scripts parse.
When custom_omr_prefix is not set, hash the Thread network name into a
ULA /64 (fd00::/8) and apply it via br omrconfig custom. Every border
router on the same mesh sees the same network name and therefore derives
the same OMR prefix, so multi-BR deployments converge on a single stable
prefix without per-BR configuration (matching Apple border-router
behavior). custom_omr_prefix remains the frontend-input override, and a
configurable priority still applies via custom_omr_priority.

The derivation is deterministic (verified: same name -> same /64, unique
per name, empty name fails cleanly), applies through the existing
ot-ctl br omrconfig path, and retries until the agent reports the
network name. DOCS.md/CHANGELOG/translations updated accordingly.
- CHANGELOG: replace stale 4.0.0 build bullets that still described the
  removed ULA-prefix / broad-ULA-route / SO_REUSEADDR / NAT64 patches with
  the actual rebased state (single 0006 patch, upstream items dropped,
  external-route install disabled via the config header, home-assistant#3325 backend and
  deterministic OMR notes)
- Add the refreshed companion documentation (otbr-pr-docs) aligned with the
  final state of the change: PR_BODY, uplink theory, multicast forwarding,
  thread-research architecture overview, DOCS and CHANGELOG mirrors

The docs now describe the home-assistant#3325 in-process backend plus the retained add-on
layer, the deterministic network-name-derived OMR prefix, and the commit
chain behind the PR.
@tannerdsilva

Copy link
Copy Markdown
Author

Ok I think that concludes the development work for now. Working to validate these changes in my environment today...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants