280 lines
13 KiB
Markdown
280 lines
13 KiB
Markdown
|
|
# 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):
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"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`:
|
||
|
|
```sh
|
||
|
|
#!/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`):
|
||
|
|
```sh
|
||
|
|
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:
|
||
|
|
|
||
|
|
```js
|
||
|
|
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
|