Files
luci-app-zt-gateway/.omp/plans/add-setup-screen.md
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

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:

  1. Detects what's already configured vs. what's missing
  2. Lets the user run each setup step individually or all at once
  3. Shows real-time status after each step
  4. 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 from zerotier-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

  1. Find ZT interface: zerotier-cli listnetworks → parse <dev> column
  2. If no ZT interface found → error "ZeroTier not joined to any network"
  3. Create UCI device: uci set network.zt_bridge=bridge; uci set network.zt_bridge.name=$BRIDGE; uci set network.zt_bridge.bridge_empty=1
  4. Create UCI interface: uci set network.brzt=interface; uci set network.brzt.proto=static; uci set network.brzt.device=$BRIDGE
  5. Set IP from UCI config (read global section for WIBLAN CIDR, derive gateway IP)
  6. Create UCI zerotier interface: uci set network.zerotier=interface; uci set network.zerotier.proto=none; uci set network.zerotier.device=$ZT_IFACE
  7. uci commit network
  8. Bring up: ifup brzt; ifup zerotier
  9. Runtime fallback: if ifup doesn'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

  1. echo 1 > /proc/sys/net/ipv4/ip_forward
  2. Persist: add net.ipv4.ip_forward=1 to /etc/sysctl.d/99-zt-gateway.conf + sysctl -p
  3. ip rule del from $WIBLAN_CIDR table $TABLE_MAIN 2>/dev/null || true
  4. ip rule add from $WIBLAN_CIDR table $TABLE_MAIN priority 100
  5. Read active_gateway from UCI → get IP from gateway config
  6. ip route replace default via $GW_IP dev $BRIDGE table $TABLE_MAIN
  7. ip route replace $GW_IP dev $BRIDGE (host route)
  8. 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

  1. On page load: Call zt-gateway.setup { action: 'status' } to get current state
  2. Render checklist: Each step shows ✓ (green) or ✗ (gray) with a [Setup] button
  3. [Setup] button: Calls zt-gateway.setup { action: 'setup-bridge' } (etc.), then refreshes status
  4. [Run All]: Calls zt-gateway.setup { action: 'setup-all' }, then refreshes status
  5. WiFi Guide: Shows the UCI config snippet to add (read-only, with copy button)
  6. 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

  1. Deploy to router: HOST=root@10.11.12.254 mise run deploy:install
  2. Open UI: Navigate to Services > ZeroTier Gateway
  3. Setup panel: Should show checklist with current state (most items ✓ since router is pre-configured)
  4. Fresh test: Deploy to a clean OpenWrt container (docker compose up openwrt-luci), verify setup panel shows all ✗
  5. Run setup: Click [Run All], verify all items turn ✓
  6. Switch test: After setup, select a gateway and switch — should succeed
  7. Reboot test: After setup, reboot — verify routes survive

7. Implementation Order

  1. Create zt-gateway-setup script with status command only
  2. Add setup ubus method to backend
  3. Add ACL for setup method
  4. Add setup panel to UI (read-only status display)
  5. Implement setup-bridge in script
  6. Implement setup-routing in script
  7. Implement setup-hotplug in script
  8. Implement setup-persistence in script
  9. Implement setup-dhcp in script
  10. Implement setup-all in script
  11. Wire [Setup] buttons in UI to call backend
  12. Add WiFi AP guide section
  13. Update UCI config with new options
  14. Update deploy task and docs
  15. Test end-to-end