Malar Invention 1e4a46c4bf setup wizard: fix routing, DHCP, WiFi AP, firewall, and deploy task
Setup script fixes:
- _cidr_to_mask: pad to 4 octets (/24 -> 255.255.255.0)
- UCI quoting: remove embedded shell quotes from uci set calls
- Bridge ports: auto-detect zt* interface instead of hardcoding ztabc0
- Bridge netmask: default to /23 (255.255.254.0) for ZT+WIBLAN
- DHCP/WiFi AP: reference interface name (zt_wiblan) not device name (br_zt)
- Firewall zone: add zt_wiblan to LAN zone for nftables fw4
- ZT IP persistence: ensure ZT-assigned IP stays on interface for ARP
- Exit gateway routing: table 100/101 route via exit gateway, not self
- New setup-wifi-ap subcommand for WIBLAN WiFi AP

UBUS handler:
- Add setup-wifi-ap to validation regex and error message

Deploy task:
- Auto-discover files from root/ and htdocs/ instead of hardcoded list
- Clear LuCI cache before restarting services

Documentation:
- New docs/SETUP-GATEWAY.md with architecture, config, pitfalls, checklist
- Updated docs/INSTALL.md with deploy task and setup wizard sections
- Updated docs/PROGRESS.md with session log and learnings
2026-07-13 11:11:19 +05:30
2026-07-12 23:42:54 +05:30
2026-07-12 23:42:54 +05:30
2026-07-12 23:42:54 +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.

Files

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

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:

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

UCI config

/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.

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.

License

Apache-2.0

Description
LuCI app for OpenWRT that switches ZeroTier gateways
Readme 59 KiB
Languages
Shell 48.9%
JavaScript 27.2%
UnrealScript 22.6%
Makefile 1.3%