SLZB on-device OTBR + HA: Thread nodes unreachable (ENETUNREACH) — stale OMR prefix in RAs [solved]

[Guide/Solved] SLZB dual-radio on-device OTBR + HA: Thread nodes unreachable (ENETUNREACH) — root cause was a stale OMR prefix in Router Advertisements

TL;DR: If you run a SMLIGHT SLZB coordinator in the experimental “Thread+OTBR running on device” mode and your Matter-over-Thread devices randomly fail to commission (Android says “Device requires a Thread border router” despite everything looking correct), OTA updates stall, and the Matter Server logs ENETUNREACH — check whether the Route Information Option (RIO) in the border router’s Router Advertisements actually matches the OMR prefix your Thread nodes live on. In my case the RA advertised a stale OMR prefix left over from a previous Thread dataset, so LAN hosts never got a route to the real mesh. Reported to SMLIGHT, confirmed, and fixed in a beta firmware (fix scheduled for the next stable release). Full diagnosis path, commands, and a temporary workaround below.


My setup

  • Home Assistant OS on an HP EliteDesk mini (Ethernet only, no Bluetooth adapter)
  • SMLIGHT SLZB-MRW10U (Ethernet/PoE): Radio 1 = Z-Wave (Z-Wave JS UI), Radio 2 = Thread+OTBR running on device (experimental mode), OTBR REST API on port 8080
  • Matter Server add-on, BLE commissioning via ESPHome Bluetooth proxies (M5Stack ATOM Lite + Olimex ESP32-POE-ISO)
  • Thread devices: IKEA BILRESA, MYGGBETT, KAJPLATS (Matter over Thread)
  • Router: carrier LTE gateway (no IPv6 magic, plain SLAAC on the LAN)

Symptoms (they look unrelated at first!)

  1. Android commissioning always failed with “Device requires a Thread border router” — even though credentials were synced, the border router was visible in the HA Thread panel, and _meshcop._udp was discoverable from the phone (verified with a service browser app).
  2. Commissioning via the HA web UI + manual pairing code worked, but took minutes (endless Sigma1/CASE retransmissions in the Matter Server debug log).
  3. Firmware OTA updates to sleepy Thread devices kept stalling and being cancelled (BDX transfer errors).
  4. Eventually the Matter Server log filled with:
    WARN PeerConnection @1:17 udp://[fd3a:....]:5540 General connection error
    (retry in 2m): [network-unreachable] send ENETUNREACH fd3a:...
    

Diagnosis

Step 1 — check the routing table on the HA host:

ip -6 route show | grep -E 'fd'

Result: the host had the on-link ULA prefix from the BR’s RA (SLAAC worked, the host even had an address from it) — but no route to the Thread mesh/OMR prefix where the actual nodes live. That’s why every connection attempt died locally with ENETUNREACH.

Step 2 — prove causality with a manual route:

# find the BR's link-local address
ip neigh show | grep <BR-IPv4>          # get its MAC
ip -6 neigh show dev eno1               # match MAC -> fe80::... address

# add the route manually
ip -6 route add <OMR-prefix>/64 via fe80::<BR-link-local> dev eno1

Connectivity to all Thread nodes was restored instantly. So the mesh, the BR and the nodes were all fine — only the route advertisement was broken.

Step 3 — capture the Router Advertisements:

tcpdump -i eno1 -vv 'icmp6 and ip6[40] == 134' -c 3

And here was the surprise. The RA did contain a Route Information Option — but it advertised a different prefix than the one my nodes were using:

  • RIO advertised: fd85:....::/64 (stale)
  • Nodes actually lived on: fd3a:....::/64

Background: I had re-formed the Thread network earlier (renamed it and generated a fresh dataset via the OTBR “Update TLVs” page, because my two sites both used the default OpenThread-ESP name — don’t do that, give every Thread network a unique name!). The OTBR’s Routing Manager apparently kept the OMR prefix persisted from the previous dataset and never switched the RIO to the new one. Reboots didn’t help.

This also elegantly explains symptom #1: Google Play Services validate reachability of the Thread mesh before commissioning — with no usable route, the phone reported “no border router” even though mDNS and credentials were perfect.

Temporary workaround (survives HA restarts)

configuration.yaml:

shell_command:
  add_thread_route: "ip -6 route add <OMR-prefix>/64 via fe80::<BR-link-local> dev eno1 || true"

Automation:

alias: "Thread route fix (BR RIO workaround)"
triggers:
  - trigger: homeassistant
    event: start
actions:
  - delay: "00:02:00"
  - action: shell_command.add_thread_route
mode: single

The || true keeps the return code clean when the route already exists.

Resolution

I reported it to SMLIGHT support with the tcpdump capture and the manual-route causality proof. They responded quickly and sent a beta firmware. After flashing it, the mesh renumbered itself onto a prefix consistent with the advertised RIO (all nodes picked up new addresses on their own within hours, sleepy devices included), routes are now learned automatically from RAs, stalled OTAs completed, and the log has been silent since. SMLIGHT confirmed the fix will be part of the next regular firmware release.

Lessons learned

  1. Give every Thread network a unique name and fresh keys — never leave the default (OpenThread-ESP), especially if you run more than one site. Identical names/ExtPANs confuse Google Play Services credential storage badly.
  2. When Thread devices are “visible but unreachable”, check ip -6 route before blaming the devices — a missing/stale RIO produces exactly this split-brain state.
  3. tcpdump 'icmp6 and ip6[40] == 134' is your friend — one capture told me more than a week of guessing.
  4. HAOS is ready to consume RIOs out of the box (accept_ra_rt_info_max_plen=64) — if the route isn’t there, the border router isn’t advertising it correctly.
  5. A manual ip -6 route add is a perfectly good band-aid, but wire it to an automation on HA start, because it won’t survive reboots.

Hope this saves someone the two weeks it cost me. Happy to answer questions — and big thanks to SMLIGHT for the fast turnaround once the packet capture was on the table.