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
13 KiB
Plan: One-Time Setup Screen for luci-app-zt-gateway
Problem
When luci-app-zt-gateway is installed on a fresh OpenWrt router, the switch script fails with Cannot find device "br-zt" because the prerequisite networking (bridge, routing, hotplug, DHCP) doesn't exist yet. There's no guided setup — the user must manually configure everything via CLI.
Goal
Add a Setup section to the overview UI that:
- Detects what's already configured vs. what's missing
- Lets the user run each setup step individually or all at once
- Shows real-time status after each step
- For WiFi AP, displays the UCI config to add (user confirms)
Production Router State (reference)
The router at root@10.11.12.254 shows the target state:
| Component | UCI/Config | Runtime |
|---|---|---|
| br-zt bridge | config device type bridge name br-zt |
ip link show br-zt |
| brzt interface | config interface brzt proto static device br-zt ipaddr 10.11.12.254 |
ip addr show br-zt |
| ZT interface | config interface zerotier proto none device ztk4jpk77j |
ip link show ztk4jpk77j |
| ZT enslavement | hotplug script | brctl show br-zt |
| Policy rule | config rule src 10.11.13.0/24 lookup 100 priority 100 |
ip rule show |
| Table 100 default | config route interface brzt target 0.0.0.0 gateway 10.11.12.3 table 100 |
ip route show table 100 |
| Host route | config route interface brzt target 10.11.12.3 |
ip route show |
| WIBLAN return | config route interface brzt target 10.11.13.0 table 100 |
— |
| DHCP | config dhcp brzt interface brzt start 257 limit 254 |
— |
| WIBLAN AP | config wifi-iface wifinet0 device radio0 ssid WIBLAN network brzt |
— |
| IP forwarding | sysctl | cat /proc/sys/net/ipv4/ip_forward → 1 |
| Hotplug | /etc/hotplug.d/net/99-zerotier-bridge |
— |
| rc.local | routes in /etc/rc.local |
— |
Architecture
1. Setup Script: root/usr/sbin/zt-gateway-setup
A new POSIX shell script with subcommands. Idempotent — running any step twice is safe.
Usage: zt-gateway-setup <command>
Commands:
status Show setup status for all components
setup-bridge Create br-zt bridge and detect ZT interface
setup-routing Configure ip rules, table 100, ip forwarding
setup-hotplug Write /etc/hotplug.d/net/99-zerotier-bridge
setup-persistence Write rc.local + UCI network routes
setup-dhcp Configure DHCP for WIBLAN subnet
setup-all Run all setup steps in order
Environment variables (same pattern as zt-gateway-switch):
ZTG_BRIDGE(default:br-zt)ZTG_WIBLAN_CIDR(default:10.11.13.0/24)ZTG_TABLE_MAIN(default:100)ZTG_ZT_NETWORK_ID(default: auto-detect fromzerotier-cli listnetworks)
status output (JSON, for ubus consumption):
{
"zt_client": true,
"zt_network_joined": true,
"zt_interface": "ztk4jpk77j",
"bridge_exists": true,
"bridge_has_zt": true,
"bridge_has_address": true,
"routing_ready": true,
"ip_forwarding": true,
"hotplug_script": true,
"persistence": true,
"dhcp_configured": true
}
Key implementation details:
setup-bridge
- Find ZT interface:
zerotier-cli listnetworks→ parse<dev>column - If no ZT interface found → error "ZeroTier not joined to any network"
- Create UCI device:
uci set network.zt_bridge=bridge; uci set network.zt_bridge.name=$BRIDGE; uci set network.zt_bridge.bridge_empty=1 - Create UCI interface:
uci set network.brzt=interface; uci set network.brzt.proto=static; uci set network.brzt.device=$BRIDGE - Set IP from UCI config (read
globalsection for WIBLAN CIDR, derive gateway IP) - Create UCI zerotier interface:
uci set network.zerotier=interface; uci set network.zerotier.proto=none; uci set network.zerotier.device=$ZT_IFACE uci commit network- Bring up:
ifup brzt; ifup zerotier - Runtime fallback: if
ifupdoesn't enslave, do it manually:ip link add name $BRIDGE type bridge 2>/dev/null || true ip link set $BRIDGE up ip link set $ZT_IFACE master $BRIDGE
setup-routing
echo 1 > /proc/sys/net/ipv4/ip_forward- Persist: add
net.ipv4.ip_forward=1to/etc/sysctl.d/99-zt-gateway.conf+sysctl -p ip rule del from $WIBLAN_CIDR table $TABLE_MAIN 2>/dev/null || trueip rule add from $WIBLAN_CIDR table $TABLE_MAIN priority 100- Read
active_gatewayfrom UCI → get IP from gateway config ip route replace default via $GW_IP dev $BRIDGE table $TABLE_MAINip route replace $GW_IP dev $BRIDGE(host route)ip route replace $WIBLAN_CIDR dev $BRIDGE table 1(MWAN return)
setup-hotplug
Write /etc/hotplug.d/net/99-zerotier-bridge:
#!/bin/sh
# Auto-generated by zt-gateway-setup — do not edit manually.
# Re-enslaves ZT interface to br-zt and restores routes on reconnect.
[ "$INTERFACE" = "$ZT_IFACE" ] && [ "$ACTION" = "add" ] && {
ip link set $ZT_IFACE master $BRIDGE 2>/dev/null || brctl addif $BRIDGE $ZT_IFACE 2>/dev/null
ip route del $ZT_SUBNET dev $ZT_IFACE 2>/dev/null
ip route replace $WIBLAN_CIDR dev $BRIDGE table 1 2>/dev/null
ip route replace $GW_IP dev $BRIDGE 2>/dev/null
ip route replace default via $GW_IP dev $BRIDGE table $TABLE_MAIN 2>/dev/null
}
The IPs are hardcoded (matching how persist_all uses sed to update them on switch).
setup-persistence
Write rc.local entries and UCI network routes (already handled by persist_all in the switch script, but setup-persistence creates the initial entries).
setup-dhcp
Write to /etc/config/dhcp (NOT /etc/config/network):
uci set dhcp.brzt=dhcp
uci set dhcp.brzt.interface='brzt'
uci set dhcp.brzt.start=257
uci set dhcp.brzt.limit=254
uci set dhcp.brzt.leasetime='12h'
uci add_list dhcp.brzt.dhcp_option='3,$WIBLAN_GW' # gateway
uci add_list dhcp.brzt.dhcp_option='6,8.8.8.8,1.1.1.1' # DNS
uci commit dhcp
The gateway IP is the router's own IP on br-zt (read from UCI network.brzt.ipaddr after bridge setup).
2. Backend: root/usr/share/rpcd/ucode/zt-gateway.uc
Add a new setup ubus method:
setup: {
args: {
action: 'string' // 'status' | 'setup-bridge' | 'setup-routing' | ...
},
call: function(req) {
const action = req.args?.action || 'status';
const result = system_output(`/usr/sbin/zt-gateway-setup ${shell_quote(action)}`);
// Parse JSON output, return structured result
}
}
Also update the ACL in root/usr/share/rpcd/acl.d/luci-app-zt-gateway.json to allow the setup method.
3. UI: htdocs/luci-static/resources/view/zt-gateway/overview.js
Add a Setup section to the overview page. When setup is incomplete, show it prominently at the top. When complete, collapse it or hide it.
Setup Panel Layout
┌─────────────────────────────────────────────────────────┐
│ Setup [Status] │
├─────────────────────────────────────────────────────────┤
│ │
│ ☑ ZeroTier client installed │
│ ☑ Network "whiteblossom" joined (ztk4jpk77j) │
│ │
│ ☐ Bridge (br-zt) [Setup] │
│ Creates br-zt and enslaves ZT interface │
│ │
│ ☐ Routing (ip rules + table 100) [Setup] │
│ Configures policy routing for WIBLAN traffic │
│ │
│ ☐ Hotplug script [Setup] │
│ Restores routes when ZT reconnects │
│ │
│ ☐ Boot persistence [Setup] │
│ Routes survive reboot │
│ │
│ ☐ DHCP for WIBLAN [Setup] │
│ Assigns IPs to WiFi clients (10.11.13.0/24) │
│ │
│ ───────────────────────────────────────────────────── │
│ WiFi AP (WIBLAN) [Guide] │
│ Add to /etc/config/wireless: │
│ ┌─────────────────────────────────────────────┐ │
│ │ config wifi-iface 'wifinetN' │ │
│ │ option device 'radio0' │ │
│ │ option mode 'ap' │ │
│ │ option ssid 'WIBLAN' │ │
│ │ option encryption 'psk2' │ │
│ │ option key '<your-password>' │ │
│ │ option network 'brzt' │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ [Run All Setup Steps] │
│ │
└─────────────────────────────────────────────────────────┘
UI Behavior
- On page load: Call
zt-gateway.setup { action: 'status' }to get current state - Render checklist: Each step shows ✓ (green) or ✗ (gray) with a [Setup] button
- [Setup] button: Calls
zt-gateway.setup { action: 'setup-bridge' }(etc.), then refreshes status - [Run All]: Calls
zt-gateway.setup { action: 'setup-all' }, then refreshes status - WiFi Guide: Shows the UCI config snippet to add (read-only, with copy button)
- After all steps complete: Collapse the setup section, show normal overview
Status Detection Logic (in zt-gateway-setup status)
| Check | How |
|---|---|
| ZT client installed | command -v zerotier-cli |
| ZT network joined | zerotier-cli listnetworks 2>/dev/null | grep -q OK |
| ZT interface name | Parse zerotier-cli listnetworks → <dev> column |
| Bridge exists | ip link show $BRIDGE 2>/dev/null |
| Bridge has ZT | brctl show $BRIDGE 2>/dev/null | grep -q $ZT_IFACE |
| Bridge has address | ip -o -4 addr show dev $BRIDGE 2>/dev/null |
| IP forwarding | cat /proc/sys/net/ipv4/ip_forward |
| Policy rule | ip rule show | grep "from $WIBLAN_CIDR.*lookup $TABLE_MAIN" |
| Table 100 default | ip route show table $TABLE_MAIN | grep -q "default via" |
| Hotplug script | [ -x /etc/hotplug.d/net/99-zerotier-bridge ] |
| Persistence | Check rc.local has routes |
| DHCP | uci get dhcp.brzt.interface 2>/dev/null |
4. Files to Create/Modify
| File | Action | Purpose |
|---|---|---|
root/usr/sbin/zt-gateway-setup |
Create | Setup script with subcommands |
root/usr/share/rpcd/ucode/zt-gateway.uc |
Modify | Add setup ubus method |
root/usr/share/rpcd/acl.d/luci-app-zt-gateway.json |
Modify | Add setup to ubus read/write ACL |
htdocs/luci-static/resources/view/zt-gateway/overview.js |
Modify | Add setup panel section |
root/etc/config/zt-gateway |
Modify | Add wiblan_subnet and zt_network_id options to global |
docs/INSTALL.md |
Modify | Document setup screen in installation guide |
5. UCI Config Additions
Add to config global in root/etc/config/zt-gateway:
option wiblan_subnet '10.11.13.0/24'
option wiblan_gateway '10.11.12.254'
option wiblan_dhcp_start '257'
option wiblan_dhcp_limit '254'
option zt_network_id ''
These allow the setup script and UI to derive all IP addresses from config rather than hardcoding.
6. Verification
- Deploy to router:
HOST=root@10.11.12.254 mise run deploy:install - Open UI: Navigate to Services > ZeroTier Gateway
- Setup panel: Should show checklist with current state (most items ✓ since router is pre-configured)
- Fresh test: Deploy to a clean OpenWrt container (
docker compose up openwrt-luci), verify setup panel shows all ✗ - Run setup: Click [Run All], verify all items turn ✓
- Switch test: After setup, select a gateway and switch — should succeed
- Reboot test: After setup, reboot — verify routes survive
7. Implementation Order
- Create
zt-gateway-setupscript withstatuscommand only - Add
setupubus method to backend - Add ACL for
setupmethod - Add setup panel to UI (read-only status display)
- Implement
setup-bridgein script - Implement
setup-routingin script - Implement
setup-hotplugin script - Implement
setup-persistencein script - Implement
setup-dhcpin script - Implement
setup-allin script - Wire [Setup] buttons in UI to call backend
- Add WiFi AP guide section
- Update UCI config with new options
- Update deploy task and docs
- Test end-to-end