2026-06-19 00:03:09 +05:30
|
|
|
# luci-app-zt-gateway
|
|
|
|
|
|
|
|
|
|
A LuCI application for OpenWRT that lets users switch which remote ZeroTier node
|
|
|
|
|
acts as the internet exit gateway for WIBLAN clients (`10.11.13.0/24`).
|
|
|
|
|
|
|
|
|
|
Users select a region/gateway, choose a switch mode, and the app reconfigures
|
|
|
|
|
kernel routing, conntrack, hotplug scripts, and UCI network routes with minimal
|
|
|
|
|
disruption.
|
|
|
|
|
|
|
|
|
|
## Switch modes
|
|
|
|
|
|
|
|
|
|
- **Force** — instant cutover (~1-2s disruption); existing connections break and
|
|
|
|
|
clients auto-reconnect through the new gateway. Flushes conntrack for the
|
|
|
|
|
WIBLAN subnet.
|
|
|
|
|
- **Graceful drain** — zero disruption; existing connections keep using the old
|
|
|
|
|
gateway via fwmark + a drain routing table, while new connections go through
|
|
|
|
|
the new gateway immediately. Auto-falls-back to a force switch after a
|
|
|
|
|
configurable timeout if drain does not complete.
|
|
|
|
|
|
feat: implement ZeroTier exit gateway switcher
Adds the luci-app-zt-gateway package: a LuCI app + rpcd/ucode backend +
shell switch script that reconfigures which remote ZeroTier node acts as
the internet exit gateway for WIBLAN clients (10.11.13.0/24).
- Makefile (luci.mk, arch-independent)
- UCI config skeleton with three sample gateways
- rpcd ACL + menu entry
- zt-gateway.uc rpcd backend exposing status / switch / health /
drain_status / cancel_drain ubus methods
- zt-gateway-switch shell script implementing force + graceful modes:
* force: pre-flight ping, atomic route replace, conntrack flush,
UCI/hotplug/rc.local persistence
* graceful: dual-table drain using CONNMARK fwmark 0x100 at
priority 99, background drain monitor with two-consecutive-zero
completion and timeout-forced fallback to force
- LuCI overview.js: gateway radio list, mode select, drain progress
panel, cancel-drain button, health polling
- Docker test harness (docker-compose + Dockerfile.router +
router/gw entrypoints) exercising the switch script against real
iproute2/iptables/conntrack on two simulated exit nodes
Verified against the harness: force switch, graceful drain to natural
completion, pre-flight blocking of unreachable gateways (force + graceful),
and drain-timeout forced fallback.
2026-06-19 02:51:21 +05:30
|
|
|
## Files
|
2026-06-19 00:03:09 +05:30
|
|
|
|
feat: implement ZeroTier exit gateway switcher
Adds the luci-app-zt-gateway package: a LuCI app + rpcd/ucode backend +
shell switch script that reconfigures which remote ZeroTier node acts as
the internet exit gateway for WIBLAN clients (10.11.13.0/24).
- Makefile (luci.mk, arch-independent)
- UCI config skeleton with three sample gateways
- rpcd ACL + menu entry
- zt-gateway.uc rpcd backend exposing status / switch / health /
drain_status / cancel_drain ubus methods
- zt-gateway-switch shell script implementing force + graceful modes:
* force: pre-flight ping, atomic route replace, conntrack flush,
UCI/hotplug/rc.local persistence
* graceful: dual-table drain using CONNMARK fwmark 0x100 at
priority 99, background drain monitor with two-consecutive-zero
completion and timeout-forced fallback to force
- LuCI overview.js: gateway radio list, mode select, drain progress
panel, cancel-drain button, health polling
- Docker test harness (docker-compose + Dockerfile.router +
router/gw entrypoints) exercising the switch script against real
iproute2/iptables/conntrack on two simulated exit nodes
Verified against the harness: force switch, graceful drain to natural
completion, pre-flight blocking of unreachable gateways (force + graceful),
and drain-timeout forced fallback.
2026-06-19 02:51:21 +05:30
|
|
|
```
|
|
|
|
|
luci-app-zt-gateway/
|
|
|
|
|
├── Makefile # OpenWRT build (luci.mk)
|
|
|
|
|
├── htdocs/luci-static/resources/view/zt-gateway/overview.js # LuCI UI
|
|
|
|
|
├── root/etc/config/zt-gateway # UCI config: gateway registry + state
|
|
|
|
|
├── root/usr/sbin/zt-gateway-switch # shell: actual switch logic
|
|
|
|
|
├── root/usr/share/rpcd/acl.d/luci-app-zt-gateway.json # ACL
|
|
|
|
|
├── root/usr/share/rpcd/ucode/zt-gateway.uc # rpcd backend (ubus)
|
|
|
|
|
└── root/usr/share/luci/menu.d/luci-app-zt-gateway.json # menu entry
|
|
|
|
|
└── docker-compose.yml # local Docker test harness
|
|
|
|
|
└── Dockerfile.router # test image (archlinux + tools)
|
|
|
|
|
└── docker/router-entrypoint.sh # brings up br-zt + baseline routing
|
|
|
|
|
└── docker/gw-entrypoint.sh # simulated ZT exit node startup
|
|
|
|
|
```
|
2026-06-19 00:03:09 +05:30
|
|
|
|
feat: implement ZeroTier exit gateway switcher
Adds the luci-app-zt-gateway package: a LuCI app + rpcd/ucode backend +
shell switch script that reconfigures which remote ZeroTier node acts as
the internet exit gateway for WIBLAN clients (10.11.13.0/24).
- Makefile (luci.mk, arch-independent)
- UCI config skeleton with three sample gateways
- rpcd ACL + menu entry
- zt-gateway.uc rpcd backend exposing status / switch / health /
drain_status / cancel_drain ubus methods
- zt-gateway-switch shell script implementing force + graceful modes:
* force: pre-flight ping, atomic route replace, conntrack flush,
UCI/hotplug/rc.local persistence
* graceful: dual-table drain using CONNMARK fwmark 0x100 at
priority 99, background drain monitor with two-consecutive-zero
completion and timeout-forced fallback to force
- LuCI overview.js: gateway radio list, mode select, drain progress
panel, cancel-drain button, health polling
- Docker test harness (docker-compose + Dockerfile.router +
router/gw entrypoints) exercising the switch script against real
iproute2/iptables/conntrack on two simulated exit nodes
Verified against the harness: force switch, graceful drain to natural
completion, pre-flight blocking of unreachable gateways (force + graceful),
and drain-timeout forced fallback.
2026-06-19 02:51:21 +05:30
|
|
|
The `zt-gateway-switch` script is callable directly from SSH for testing; the
|
|
|
|
|
rpcd/ucode backend wraps it for the LuCI UI and exposes these ubus methods
|
|
|
|
|
under the `zt-gateway` object:
|
2026-06-19 00:03:09 +05:30
|
|
|
|
feat: implement ZeroTier exit gateway switcher
Adds the luci-app-zt-gateway package: a LuCI app + rpcd/ucode backend +
shell switch script that reconfigures which remote ZeroTier node acts as
the internet exit gateway for WIBLAN clients (10.11.13.0/24).
- Makefile (luci.mk, arch-independent)
- UCI config skeleton with three sample gateways
- rpcd ACL + menu entry
- zt-gateway.uc rpcd backend exposing status / switch / health /
drain_status / cancel_drain ubus methods
- zt-gateway-switch shell script implementing force + graceful modes:
* force: pre-flight ping, atomic route replace, conntrack flush,
UCI/hotplug/rc.local persistence
* graceful: dual-table drain using CONNMARK fwmark 0x100 at
priority 99, background drain monitor with two-consecutive-zero
completion and timeout-forced fallback to force
- LuCI overview.js: gateway radio list, mode select, drain progress
panel, cancel-drain button, health polling
- Docker test harness (docker-compose + Dockerfile.router +
router/gw entrypoints) exercising the switch script against real
iproute2/iptables/conntrack on two simulated exit nodes
Verified against the harness: force switch, graceful drain to natural
completion, pre-flight blocking of unreachable gateways (force + graceful),
and drain-timeout forced fallback.
2026-06-19 02:51:21 +05:30
|
|
|
| Method | Args | Description |
|
|
|
|
|
| -------------- | ----------------------------- | ---------------------------------------------- |
|
|
|
|
|
| `status` | — | active gateway, settings, gateway registry |
|
|
|
|
|
| `switch` | `{ region, mode }` | execute a force or graceful switch |
|
|
|
|
|
| `health` | `{ region }` | ping a gateway over br-zt, return latency |
|
|
|
|
|
| `drain_status` | — | graceful drain progress + remaining flows |
|
|
|
|
|
| `cancel_drain` | — | cancel drain and force-switch to the new gw |
|
2026-06-19 00:03:09 +05:30
|
|
|
|
feat: implement ZeroTier exit gateway switcher
Adds the luci-app-zt-gateway package: a LuCI app + rpcd/ucode backend +
shell switch script that reconfigures which remote ZeroTier node acts as
the internet exit gateway for WIBLAN clients (10.11.13.0/24).
- Makefile (luci.mk, arch-independent)
- UCI config skeleton with three sample gateways
- rpcd ACL + menu entry
- zt-gateway.uc rpcd backend exposing status / switch / health /
drain_status / cancel_drain ubus methods
- zt-gateway-switch shell script implementing force + graceful modes:
* force: pre-flight ping, atomic route replace, conntrack flush,
UCI/hotplug/rc.local persistence
* graceful: dual-table drain using CONNMARK fwmark 0x100 at
priority 99, background drain monitor with two-consecutive-zero
completion and timeout-forced fallback to force
- LuCI overview.js: gateway radio list, mode select, drain progress
panel, cancel-drain button, health polling
- Docker test harness (docker-compose + Dockerfile.router +
router/gw entrypoints) exercising the switch script against real
iproute2/iptables/conntrack on two simulated exit nodes
Verified against the harness: force switch, graceful drain to natural
completion, pre-flight blocking of unreachable gateways (force + graceful),
and drain-timeout forced fallback.
2026-06-19 02:51:21 +05:30
|
|
|
## UCI config
|
2026-06-19 00:03:09 +05:30
|
|
|
|
feat: implement ZeroTier exit gateway switcher
Adds the luci-app-zt-gateway package: a LuCI app + rpcd/ucode backend +
shell switch script that reconfigures which remote ZeroTier node acts as
the internet exit gateway for WIBLAN clients (10.11.13.0/24).
- Makefile (luci.mk, arch-independent)
- UCI config skeleton with three sample gateways
- rpcd ACL + menu entry
- zt-gateway.uc rpcd backend exposing status / switch / health /
drain_status / cancel_drain ubus methods
- zt-gateway-switch shell script implementing force + graceful modes:
* force: pre-flight ping, atomic route replace, conntrack flush,
UCI/hotplug/rc.local persistence
* graceful: dual-table drain using CONNMARK fwmark 0x100 at
priority 99, background drain monitor with two-consecutive-zero
completion and timeout-forced fallback to force
- LuCI overview.js: gateway radio list, mode select, drain progress
panel, cancel-drain button, health polling
- Docker test harness (docker-compose + Dockerfile.router +
router/gw entrypoints) exercising the switch script against real
iproute2/iptables/conntrack on two simulated exit nodes
Verified against the harness: force switch, graceful drain to natural
completion, pre-flight blocking of unreachable gateways (force + graceful),
and drain-timeout forced fallback.
2026-06-19 02:51:21 +05:30
|
|
|
`/etc/config/zt-gateway` holds the gateway registry and active state:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
config global 'global'
|
|
|
|
|
option active_gateway 'amsterdam' # region key of active gateway
|
|
|
|
|
option switch_mode 'force' # 'force' | 'graceful'
|
|
|
|
|
option drain_timeout '600' # graceful drain timeout (seconds)
|
|
|
|
|
option health_interval '60'
|
|
|
|
|
option auto_failback '0'
|
|
|
|
|
|
|
|
|
|
config gateway
|
|
|
|
|
option region 'amsterdam'
|
|
|
|
|
option label 'Amsterdam (ocirosea641)'
|
|
|
|
|
option ip '10.11.12.3'
|
|
|
|
|
option default '1'
|
|
|
|
|
option health_check 'ping'
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Build & install
|
|
|
|
|
|
|
|
|
|
Build against the OpenWRT SDK with `luci.mk` (architecture-independent —
|
|
|
|
|
`LUCI_PKGARCH:=all`). Full instructions are in
|
|
|
|
|
[`.omp/plans/zerotier-gateway-switching.md`](.omp/plans/zerotier-gateway-switching.md).
|
|
|
|
|
|
|
|
|
|
A manual `opkg-build` cheat sheet, the `apk` install path for OpenWRT 25.12+,
|
|
|
|
|
and post-install cache clearing are all documented there too.
|
|
|
|
|
|
|
|
|
|
## Local test harness (Docker)
|
|
|
|
|
|
|
|
|
|
End-to-end verification without a physical router is possible with the bundled
|
|
|
|
|
`docker-compose.yml` + `Dockerfile.router` + `docker/*-entrypoint.sh`. The
|
|
|
|
|
harness emulates two bridge networks (WIBLAN clients at `10.99.13.0/24` and
|
|
|
|
|
ZeroTier exit gateways at `10.99.12.0/24`) plus two simulated exit nodes, and
|
|
|
|
|
exercises `zt-gateway-switch` against real `iproute2`, `iptables`, and the
|
|
|
|
|
host kernel's conntrack table.
|
|
|
|
|
|
|
|
|
|
Run it with `docker compose up -d --build`, then `docker exec openwrt-router
|
|
|
|
|
zt-gateway-switch <ip> <mode> [drain_timeout]`.
|
|
|
|
|
|
|
|
|
|
## Verified behavior
|
|
|
|
|
|
|
|
|
|
The following scenarios have been exercised against the Docker harness:
|
|
|
|
|
|
|
|
|
|
- Force switch: `ip route show table 100` reflects the new gateway, host route
|
|
|
|
|
and mwan3 table 1 return route preserved.
|
|
|
|
|
- Graceful drain: table 100 → new gateway, table 101 → old gateway, fwmark
|
|
|
|
|
`0x100` rule at priority 99 inserted above the priority-100 src rule,
|
|
|
|
|
ESTABLISHED WIBLAN connections keep flowing through the old gateway until
|
|
|
|
|
they naturally age out, after which the mangle rules, drain table, and
|
|
|
|
|
pidfiles are cleaned up automatically.
|
|
|
|
|
- Pre-flight: switching to an unreachable gateway aborts with exit code 2 and
|
|
|
|
|
leaves routes untouched (verified for both `force` and `graceful` modes).
|
|
|
|
|
- Drain timeout: a forced-fallback fires after the configured timeout,
|
|
|
|
|
preserving the new gateway in table 100 and tearing down drain state.
|
|
|
|
|
|
|
|
|
|
## Persistence model
|
|
|
|
|
|
|
|
|
|
The gateway IP is recorded in five places on the router; the switch script
|
|
|
|
|
updates them all atomically:
|
|
|
|
|
|
|
|
|
|
| Location | What |
|
|
|
|
|
| --------------------------------- | -------------------------------------- |
|
|
|
|
|
| Kernel: host route | `<gw_ip> dev br-zt` |
|
|
|
|
|
| Kernel: table 100 default | `default via <gw_ip> dev br-zt table 100` |
|
|
|
|
|
| UCI: `/etc/config/network` | `zt_gateway_host` + `zt_gateway_default` routes |
|
|
|
|
|
| Hotplug: `99-zerotier-bridge` | host route + table 100 route + mwan3 table 1 return |
|
|
|
|
|
| Boot: `/etc/rc.local` | all three routes |
|
|
|
|
|
|
|
|
|
|
The hotplug and rc.local scripts are rewritten via a dot-escaped `sed` replace
|
|
|
|
|
so a reboot boots against the new gateway.
|
2026-06-19 00:03:09 +05:30
|
|
|
|
|
|
|
|
## License
|
|
|
|
|
|
|
|
|
|
Apache-2.0
|