Files
luci-app-zt-gateway/docs/diagnostics/zerotier-wiblan-missing-iprule-2026-07-14.md

125 lines
3.6 KiB
Markdown
Raw Permalink Normal View History

# ZeroTier Gateway Routing Fixes
## Date: 2026-07-14
## Problem
After switching to the Amsterdam node, traffic from WiBLAN AP clients
(10.11.13.0/24) was not routed through the Amsterdam exit gateway
(10.11.12.3). The table 100 default route pointed correctly to
Amsterdam, but traffic never reached it.
## Root Cause
The `ip rule` directing WIBLAN subnet traffic to policy table 100 was
missing:
```
100: from 10.11.13.0/24 lookup 100
```
Without this rule, WIBLAN client traffic fell through to the main
routing table (priority 32766) which routed via WAN, bypassing the
ZeroTier tunnel entirely.
### Why it was missing
1. **Hotplug script omission**: The hotplug `ifup` case in
`zt-gateway-setup` installed table 100/101 routes and the fwmark
drain rule, but never installed the `from 10.11.13.0/24` source
rule.
2. **No UCI persistence**: `setup-routing` installed the rule at
runtime but its UCI persistence block only wrote route sections,
not a `network rule` section. So netifd couldn't restore it on
boot.
3. **Switch script gap**: `zt-gateway-switch` (`do_force`/`do_graceful`)
assumed the rule already existed and never verified or re-added it.
## Fixes Applied
### 1. Hotplug script (`zt-gateway-setup` heredoc)
Added to the `ifup` case after the table routes:
```sh
ip rule add from "$ZTG_WIBLAN_CIDR" table "$ZTG_TABLE_MAIN" priority 100 2>/dev/null || \
ip rule replace from "$ZTG_WIBLAN_CIDR" table "$ZTG_TABLE_MAIN" priority 100
```
### 2. UCI persistence (`setup-routing`)
Added a `network rule` UCI section so netifd restores the rule on boot:
```sh
uci -q set "network.zt_wiblan_rule=rule"
uci -q set "network.zt_wiblan_rule.src=${WIBLAN_CIDR}"
uci -q set "network.zt_wiblan_rule.lookup=${TABLE_MAIN}"
uci -q set "network.zt_wiblan_rule.priority=100"
```
### 3. Switch script (`zt-gateway-switch`)
Added rule verification to both `do_force()` and `do_graceful()`:
```sh
ip rule add from "$WIBLAN_CIDR" table "$TABLE_MAIN" priority 100 2>/dev/null || \
ip rule replace from "$WIBLAN_CIDR" table "$TABLE_MAIN" priority 100
```
## Additional Bugs Fixed
### BRIDGE_PORTS auto-detection failure (busybox)
The `awk` pipeline over `ip -o link show` failed silently on busybox
because busybox awk mishandles `exit` inside compound `if` blocks in
`-F` pipelines. Fixed by:
1. Trying UCI config first: `uci -q get zt-gateway.global.bridge_ports`
2. Falling back to `/proc/net/dev`: `awk -F': ' '/^zt/{print $1; exit}'`
3. Validating the result exists: `ip link show "$candidate"`
### Route dev parameter
`setup-routing` used `dev $BRIDGE_PORTS` (raw ZT interface) for
default routes, but the ZT interface has no IP after being enslaved
to the bridge. Changed to `dev $BRIDGE` (the bridge device that
holds the 10.11.12.x/23 subnet IP).
### `ip rule replace` busybox incompatibility
Busybox `ip` does not support `ip rule replace`. Replaced all
occurrences with:
```sh
ip rule del ... 2>/dev/null || true
ip rule add ...
```
### Deploy script stdin starvation
The `mise run deploy:install` script used `find | sort | while read`
but `ssh`/`scp` inside the loop consumed stdin from the pipe,
starving the `while read` after the first file. Fixed by adding
`</dev/null` to `ssh` and `scp` commands in the loop.
## Verification Commands
```bash
# Check ip rule exists
ip rule show | grep "lookup 100"
# Expected: 100: from 10.11.13.0/24 lookup 100
# Check table 100 routes
ip route show table 100
# Expected: default via <active_gw_ip> dev br-zt
# Check UCI rule persisted
uci show network | grep zt_wiblan_rule
# Expected: network.zt_wiblan_rule=rule, src, lookup, priority
# Ping exit gateway via bridge
ping -c 2 -I br-zt <active_gw_ip>
```