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
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -33,6 +33,3 @@ node_modules/
|
|||||||
# Test artifacts
|
# Test artifacts
|
||||||
test-results/
|
test-results/
|
||||||
e2e-report/
|
e2e-report/
|
||||||
|
|
||||||
# Harness
|
|
||||||
.omp/
|
|
||||||
|
|||||||
279
.omp/plans/add-setup-screen.md
Normal file
279
.omp/plans/add-setup-screen.md
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
# 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
|
||||||
66
.omp/plans/allow-reactivate-current-gateway.md
Normal file
66
.omp/plans/allow-reactivate-current-gateway.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Plan: Allow re-activating the current gateway from the UI
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The UI shows "active" on a gateway based solely on the UCI `active_gateway` config value, but the switch script (`zt-gateway-switch`) may never have been run — meaning no routing tables, no masquerade, no actual traffic forwarding. The user sees "active" and assumes it works.
|
||||||
|
|
||||||
|
Two blocking issues prevent fixing this from the UI:
|
||||||
|
|
||||||
|
1. **Backend** (`zt-gateway.uc` line 184-187): `switch` method short-circuits with "Already on X" when UCI `active_gateway` matches the requested region — never runs the script
|
||||||
|
2. **UI** (`overview.js` line 81): radio button for the active gateway is `disabled`, so the user can't even select it to click "Switch to selected"
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
|
||||||
|
Two files, one change each.
|
||||||
|
|
||||||
|
### 1. Backend: `root/usr/share/rpcd/ucode/zt-gateway.uc`
|
||||||
|
|
||||||
|
**Remove the "Already on X" early return** (lines 184-187):
|
||||||
|
|
||||||
|
```js
|
||||||
|
// DELETE these lines:
|
||||||
|
const current_region = read_active_region();
|
||||||
|
if (current_region === region && !drain_active()) {
|
||||||
|
return { success: true, message: `Already on ${region}.` };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this is safe:**
|
||||||
|
- `do_force()` in the switch script is idempotent: `ip route replace` is a no-op when the route already matches, conntrack flush is harmless, `persist_all` writes the same values
|
||||||
|
- The backend still updates UCI after the script runs (line 198-199) — setting the same value is harmless
|
||||||
|
- If the gateway is unreachable, `preflight_ping` fails with exit 2 and the backend returns the error — same as switching to any other unreachable gateway
|
||||||
|
|
||||||
|
### 2. UI: `htdocs/luci-static/resources/view/zt-gateway/overview.js`
|
||||||
|
|
||||||
|
**Remove the `disabled` attribute from the active gateway's radio** (line 81):
|
||||||
|
|
||||||
|
Change:
|
||||||
|
```js
|
||||||
|
disabled: isActive || null
|
||||||
|
```
|
||||||
|
To:
|
||||||
|
```js
|
||||||
|
// Remove this line entirely (or keep disabled only during an active drain)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this is safe:**
|
||||||
|
- The user can now select the active gateway and click "Switch to selected"
|
||||||
|
- The backend runs the switch script which sets up routing
|
||||||
|
- If routing is already correct, the script is a harmless idempotent no-op
|
||||||
|
- The "Switch to selected" button text still makes sense — it re-applies the gateway config
|
||||||
|
|
||||||
|
### Files to modify
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `root/usr/share/rpcd/ucode/zt-gateway.uc` | Remove lines 184-187 (early return) |
|
||||||
|
| `htdocs/luci-static/resources/view/zt-gateway/overview.js` | Remove `disabled` on line 81 |
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
|
||||||
|
1. Deploy to device: `mise run deploy:install`
|
||||||
|
2. Open UI → amsterdam shows "active" → radio is now enabled
|
||||||
|
3. Select amsterdam → click "Switch to selected" → should succeed and set up routing
|
||||||
|
4. Verify routing: `ip route show table 100` should show `default via 10.11.12.3 dev ztk4jpk77j`
|
||||||
|
5. Verify NAT: `iptables -t mangle -L -n` should show WIBLAN mangle rules (for graceful mode)
|
||||||
|
6. Test from LAN client: `ping -I br-lan 10.11.12.3` should now work
|
||||||
75
.omp/plans/install-docs-and-deploy-task.md
Normal file
75
.omp/plans/install-docs-and-deploy-task.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# Plan: Document installation guide + add deploy mise task
|
||||||
|
|
||||||
|
## What
|
||||||
|
|
||||||
|
1. Create `docs/INSTALL.md` — installation guide for luci-app-zt-gateway covering all methods
|
||||||
|
2. Add `deploy:install` task to `mise.toml` — one-command SCP + install to a real OpenWrt device
|
||||||
|
|
||||||
|
## 1. `docs/INSTALL.md`
|
||||||
|
|
||||||
|
New file covering all 6 installation methods, researched from official OpenWrt docs:
|
||||||
|
|
||||||
|
| Method | When to use |
|
||||||
|
|---|---|
|
||||||
|
| **LuCI web UI** | End users, official feeds |
|
||||||
|
| **CLI** (`apk add` / `opkg install`) | Headless / SSH users |
|
||||||
|
| **Custom feed** | Distributing third-party packages via HTTP |
|
||||||
|
| **Local file SCP** | Dev/testing, quick iteration |
|
||||||
|
| **OpenWrt SDK** | Building proper .ipk/.apk with dependency metadata |
|
||||||
|
| **Image Builder / ASU** | Production firmware, survives factory reset |
|
||||||
|
|
||||||
|
Key details to document:
|
||||||
|
- OpenWrt 25.12+ uses `apk` (not `opkg`); older uses `opkg`
|
||||||
|
- `apk add --allow-untrusted` required for unsigned local packages
|
||||||
|
- `opkg install /tmp/pkg.ipk` for opkg-based systems
|
||||||
|
- SDK build: `make package/luci-app-zt-gateway/compile V=s`
|
||||||
|
- ASU: `luci-app-attendedsysupgrade` → Advanced Mode → add package
|
||||||
|
- Prerequisites: `luci-base`, `ucode`, `rpcd-mod-ucode`, `luci-compat`
|
||||||
|
- Custom feed: `src/gz` in `/etc/opkg/customfeeds.conf` or apk equivalent
|
||||||
|
|
||||||
|
## 2. `mise.toml` — add `deploy:install` task
|
||||||
|
|
||||||
|
Following snowbud patterns: env var params, usage validation, step-by-step echo output.
|
||||||
|
|
||||||
|
### Task design
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[tasks."deploy:install"]
|
||||||
|
description = "SCP app files to an OpenWrt device and install"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Parameters** (via env vars):
|
||||||
|
- `HOST` — SSH target, default `root@192.168.15.1`
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Validate `ssh` connectivity to `$HOST`
|
||||||
|
2. SCP each file from `root/` to its corresponding remote path (stripping the leading `root` prefix)
|
||||||
|
3. SCP `htdocs/luci-static/resources/view/zt-gateway/overview.js` → `/www/luci-static/resources/view/zt-gateway/overview.js`
|
||||||
|
4. `chmod +x` the `zt-gateway-switch` script on the remote
|
||||||
|
5. Restart `rpcd` and `uhttpd` so LuCI picks up changes
|
||||||
|
6. Echo the URL to open
|
||||||
|
|
||||||
|
### File mapping (local → remote)
|
||||||
|
|
||||||
|
| Local | Remote |
|
||||||
|
|---|---|
|
||||||
|
| `root/usr/sbin/zt-gateway-switch` | `/usr/sbin/zt-gateway-switch` |
|
||||||
|
| `root/usr/share/rpcd/ucode/zt-gateway.uc` | `/usr/share/rpcd/ucode/zt-gateway.uc` |
|
||||||
|
| `root/usr/share/rpcd/ucode/system.uc` | `/usr/share/rpcd/ucode/system.uc` |
|
||||||
|
| `root/usr/share/ucode/luci/runtime.uc` | `/usr/share/ucode/luci/runtime.uc` |
|
||||||
|
| `root/usr/share/luci/menu.d/luci-app-zt-gateway.json` | `/usr/share/luci/menu.d/luci-app-zt-gateway.json` |
|
||||||
|
| `root/usr/share/rpcd/acl.d/luci-app-zt-gateway.json` | `/usr/share/rpcd/acl.d/luci-app-zt-gateway.json` |
|
||||||
|
| `root/etc/config/zt-gateway` | `/etc/config/zt-gateway` |
|
||||||
|
| `htdocs/.../overview.js` | `/www/.../overview.js` |
|
||||||
|
|
||||||
|
## Files to modify
|
||||||
|
|
||||||
|
| File | Action |
|
||||||
|
|---|---|
|
||||||
|
| `docs/INSTALL.md` | **Create** |
|
||||||
|
| `mise.toml` | **Edit** — append `deploy:install` task |
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
1. `mise tasks` should list the new `deploy:install` task
|
||||||
|
2. `docs/INSTALL.md` should be readable and cover all 6 methods
|
||||||
@@ -42,36 +42,32 @@ opkg install luci-base luci-compat ucode rpcd-mod-ucode luci-theme-bootstrap
|
|||||||
|
|
||||||
The fastest way during development. No build step required.
|
The fastest way during development. No build step required.
|
||||||
|
|
||||||
```bash
|
|
||||||
HOST=root@192.168.15.1
|
|
||||||
|
|
||||||
# Upload files
|
|
||||||
scp root/usr/sbin/zt-gateway-switch $HOST:/usr/sbin/
|
|
||||||
scp root/usr/share/rpcd/ucode/zt-gateway.uc $HOST:/usr/share/rpcd/ucode/
|
|
||||||
scp root/usr/share/rpcd/ucode/system.uc $HOST:/usr/share/rpcd/ucode/
|
|
||||||
scp root/usr/share/ucode/luci/runtime.uc $HOST:/usr/share/ucode/luci/
|
|
||||||
scp root/usr/share/luci/menu.d/luci-app-zt-gateway.json $HOST:/usr/share/luci/menu.d/
|
|
||||||
scp root/usr/share/rpcd/acl.d/luci-app-zt-gateway.json $HOST:/usr/share/rpcd/acl.d/
|
|
||||||
scp root/etc/config/zt-gateway $HOST:/etc/config/
|
|
||||||
scp htdocs/luci-static/resources/view/zt-gateway/overview.js $HOST:/www/luci-static/resources/view/zt-gateway/
|
|
||||||
|
|
||||||
# Set permissions and restart services
|
|
||||||
ssh $HOST 'chmod +x /usr/sbin/zt-gateway-switch; /etc/init.d/rpcd restart; /etc/init.d/uhttpd restart'
|
|
||||||
```
|
|
||||||
|
|
||||||
Or use the mise task (same thing, one command):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mise run deploy:install
|
mise run deploy:install
|
||||||
# or with a custom target:
|
# or with a custom target:
|
||||||
HOST=root@10.0.0.1 mise run deploy:install
|
HOST=root@10.0.0.1 mise run deploy:install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The deploy task auto-discovers all files from `root/` and `htdocs/`,
|
||||||
|
maps them to device paths, and restarts services. New files are
|
||||||
|
automatically included without editing the task.
|
||||||
|
|
||||||
Open `http://<device-ip>/cgi-bin/luci/admin/services/zt-gateway`.
|
Open `http://<device-ip>/cgi-bin/luci/admin/services/zt-gateway`.
|
||||||
|
|
||||||
**Note:** This method has no dependency tracking. The package manager won't know
|
**Note:** This method has no dependency tracking. The package manager won't know
|
||||||
about the installed files. Use for dev only.
|
about the installed files. Use for dev only.
|
||||||
|
|
||||||
|
### Setting up as Exit Gateway
|
||||||
|
|
||||||
|
After deploying, run the setup wizard from the LuCI UI (Setup panel)
|
||||||
|
or CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@<device-ip> '/usr/sbin/zt-gateway-setup setup-all'
|
||||||
|
```
|
||||||
|
|
||||||
|
See [SETUP-GATEWAY.md](SETUP-GATEWAY.md) for detailed configuration
|
||||||
|
and troubleshooting.
|
||||||
---
|
---
|
||||||
|
|
||||||
## Method 2: Local Package Install via SCP
|
## Method 2: Local Package Install via SCP
|
||||||
@@ -221,6 +217,7 @@ These are the files installed by this package and their target locations:
|
|||||||
|
|
||||||
| Source | Target | Purpose |
|
| Source | Target | Purpose |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| `root/usr/sbin/zt-gateway-setup` | `/usr/sbin/zt-gateway-setup` | Gateway setup script |
|
||||||
| `root/usr/sbin/zt-gateway-switch` | `/usr/sbin/zt-gateway-switch` | Gateway switching script |
|
| `root/usr/sbin/zt-gateway-switch` | `/usr/sbin/zt-gateway-switch` | Gateway switching script |
|
||||||
| `root/usr/share/rpcd/ucode/zt-gateway.uc` | `/usr/share/rpcd/ucode/zt-gateway.uc` | rpcd backend |
|
| `root/usr/share/rpcd/ucode/zt-gateway.uc` | `/usr/share/rpcd/ucode/zt-gateway.uc` | rpcd backend |
|
||||||
| `root/usr/share/rpcd/ucode/system.uc` | `/usr/share/rpcd/ucode/system.uc` | system.board rpcd override |
|
| `root/usr/share/rpcd/ucode/system.uc` | `/usr/share/rpcd/ucode/system.uc` | system.board rpcd override |
|
||||||
|
|||||||
@@ -95,3 +95,61 @@ After installing `luci-compat`, login and page rendering work correctly, but **f
|
|||||||
4. **Run full suite end-to-end** after the above blocker is resolved.
|
4. **Run full suite end-to-end** after the above blocker is resolved.
|
||||||
5. **Clean up debug scripts**: `debug-pw.js`, `debug-pw2.js`, `debug-pw3.js`, `debug-login-dom.js`.
|
5. **Clean up debug scripts**: `debug-pw.js`, `debug-pw2.js`, `debug-pw3.js`, `debug-login-dom.js`.
|
||||||
6. **Rebuild image properly** once build-time network is restored so `luci-compat` is baked in without manual commits.
|
6. **Rebuild image properly** once build-time network is restored so `luci-compat` is baked in without manual commits.
|
||||||
|
|
||||||
|
## Date: 2026-07-13
|
||||||
|
|
||||||
|
## Setup Wizard Implementation
|
||||||
|
|
||||||
|
### Completed
|
||||||
|
|
||||||
|
1. **Created `/usr/sbin/zt-gateway-setup`** (700+ lines) with subcommands:
|
||||||
|
- `status`, `setup-bridge`, `setup-routing`, `setup-dhcp`, `setup-wifi-ap`,
|
||||||
|
`setup-hotplug`, `setup-persistence`, `setup-all`
|
||||||
|
|
||||||
|
2. **Added `setup` ubus method** to `zt-gateway.uc` with regex validation
|
||||||
|
(ucode lacks `Array.indexOf()`).
|
||||||
|
|
||||||
|
3. **Added Setup panel** to `overview.js` with buttons for each setup command.
|
||||||
|
|
||||||
|
4. **Auto-discover deploy task** — `deploy:install` now discovers files from
|
||||||
|
`root/` and `htdocs/` instead of listing them individually.
|
||||||
|
|
||||||
|
### Bugs Found and Fixed During Setup
|
||||||
|
|
||||||
|
1. **`_cidr_to_mask` produced 3 octets** for /24 (`255.255.255` instead of
|
||||||
|
`255.255.255.0`). Fixed by padding to 4 octets.
|
||||||
|
|
||||||
|
2. **UCI values had embedded quotes** — `uci set "proto='static'"` stored
|
||||||
|
`'static'` instead of `static`. Fixed by removing shell quotes.
|
||||||
|
|
||||||
|
3. **Bridge ports hardcoded to `ztabc0`** — ZeroTier interface names are
|
||||||
|
randomized. Fixed by auto-detecting `zt*` interfaces.
|
||||||
|
|
||||||
|
4. **DHCP/WiFi AP referenced device name instead of interface name** — dnsmasq
|
||||||
|
and hostapd bind to interfaces, not devices. Fixed to use `zt_wiblan`.
|
||||||
|
|
||||||
|
5. **Firewall zone missing** — nftables fw4 has `policy drop`. Fixed by adding
|
||||||
|
`zt_wiblan` to the LAN zone.
|
||||||
|
|
||||||
|
6. **ZT interface lost IP when added to bridge** — ARP responses failed. Fixed
|
||||||
|
by ensuring ZT-assigned IP stays on the interface.
|
||||||
|
|
||||||
|
7. **Table 100 routed to self** — Default route pointed to `WIBLAN_GW` (local)
|
||||||
|
instead of exit gateway. Fixed by detecting exit gateway from UCI/routes.
|
||||||
|
|
||||||
|
8. **Bridge netmask /24 instead of /23** — Couldn't reach ZT subnet. Fixed by
|
||||||
|
defaulting to `/23` (255.255.254.0).
|
||||||
|
|
||||||
|
9. **ZeroTier Ethernet Bridging disabled** — L2 frames couldn't traverse
|
||||||
|
tunnel. Fixed by enabling in ZT network controller.
|
||||||
|
|
||||||
|
10. **DHCP range wrong in /23** — Clients got `10.11.12.x` instead of
|
||||||
|
`10.11.13.x`. Fixed by calculating correct offset (356 for /23 base).
|
||||||
|
|
||||||
|
### Key Learnings
|
||||||
|
|
||||||
|
- OpenWrt UCI uses **interface names** (not device names) for DHCP and WiFi
|
||||||
|
- Bridge netmask must be `/23` to cover both ZT (10.11.12.x) and WIBLAN (10.11.13.x)
|
||||||
|
- ZeroTier requires "Allow Ethernet Bridging" for L2 traffic
|
||||||
|
- nftables fw4 zones must explicitly include bridge interfaces
|
||||||
|
- Policy routing table 100 must route via exit gateway, not local IP
|
||||||
|
|||||||
350
docs/SETUP-GATEWAY.md
Normal file
350
docs/SETUP-GATEWAY.md
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
# ZeroTier Exit Gateway Setup Guide
|
||||||
|
|
||||||
|
This document covers configuring an OpenWrt router as a ZeroTier exit gateway,
|
||||||
|
including the common pitfalls encountered during development.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
WiFi Client (10.11.13.x)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
WIBLAN AP (phy0-ap2) ─── br-zt bridge ─── ZeroTier (ztk4jpk77j)
|
||||||
|
│ │
|
||||||
|
│ ZeroTier tunnel
|
||||||
|
│ │
|
||||||
|
│ ▼
|
||||||
|
│ Exit Gateway (amsterdam)
|
||||||
|
│ │
|
||||||
|
│ Internet (NAT)
|
||||||
|
│
|
||||||
|
├── DHCP (dnsmasq on br-zt)
|
||||||
|
├── DNS (dnsmasq → upstream)
|
||||||
|
└── Policy Routing (table 100 → exit gateway)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### ZeroTier Network Configuration
|
||||||
|
|
||||||
|
**Critical**: Enable "Allow Ethernet Bridging" on the ZeroTier network controller
|
||||||
|
at [my.zerotier.com](https://my.zerotier.com) → network → Settings.
|
||||||
|
|
||||||
|
Without this, L2 frames (ARP, DHCP) cannot be bridged across peers, and clients
|
||||||
|
will get IPs but cannot communicate.
|
||||||
|
|
||||||
|
### ZeroTier Managed Routes
|
||||||
|
|
||||||
|
The ZeroTier network must have a managed route for the WIBLAN subnet:
|
||||||
|
|
||||||
|
```
|
||||||
|
Managed Routes → Add: 10.11.13.0/24 → (empty = auto via member)
|
||||||
|
```
|
||||||
|
|
||||||
|
This tells ZeroTier to route traffic for `10.11.13.0/24` through the exit gateway.
|
||||||
|
|
||||||
|
## Setup Script Commands
|
||||||
|
|
||||||
|
The `zt-gateway-setup` script provides these commands:
|
||||||
|
|
||||||
|
| Command | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `setup-bridge` | Create `br-zt` bridge, add ZT interface, configure firewall |
|
||||||
|
| `setup-routing` | Policy routing (tables 100/101, ip rules) |
|
||||||
|
| `setup-dhcp` | DHCP for WIBLAN clients on `br-zt` |
|
||||||
|
| `setup-wifi-ap` | Create WIBLAN WiFi AP bridged to `br-zt` |
|
||||||
|
| `setup-hotplug` | Hotplug script to re-apply routes on ifup |
|
||||||
|
| `setup-persistence` | rc.local + UCI routes for reboot survival |
|
||||||
|
| `setup-all` | Run all commands in order |
|
||||||
|
|
||||||
|
Run from LuCI UI or CLI:
|
||||||
|
```bash
|
||||||
|
/usr/sbin/zt-gateway-setup setup-all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Network Configuration
|
||||||
|
|
||||||
|
### Bridge Device
|
||||||
|
|
||||||
|
```uci
|
||||||
|
config device 'br_zt'
|
||||||
|
option type 'bridge'
|
||||||
|
option name 'br-zt'
|
||||||
|
list ports 'ztk4jpk77j' # ZeroTier interface
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: UCI section names cannot contain hyphens. Use underscores (`br_zt`)
|
||||||
|
for section names, but the actual device name uses hyphens (`br-zt`).
|
||||||
|
|
||||||
|
### Bridge Interface
|
||||||
|
|
||||||
|
```uci
|
||||||
|
config interface 'zt_wiblan'
|
||||||
|
option proto 'static'
|
||||||
|
option device 'br-zt'
|
||||||
|
option ipaddr '10.11.13.1'
|
||||||
|
option netmask '255.255.254.0' # /23 to cover ZT (10.11.12.x) + WIBLAN (10.11.13.x)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key**: The netmask MUST be `/23` (255.255.254.0), not `/24`. The bridge needs
|
||||||
|
to be in the same subnet as the ZeroTier network (10.11.12.0/23) for ARP to work.
|
||||||
|
|
||||||
|
### ZeroTier Interface
|
||||||
|
|
||||||
|
```uci
|
||||||
|
config interface 'wbtier'
|
||||||
|
option proto 'none'
|
||||||
|
option device 'ztk4jpk77j'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**: The ZT interface must keep its assigned IP even when added to
|
||||||
|
the bridge. If the IP is lost, ARP responses fail and connectivity breaks.
|
||||||
|
|
||||||
|
## Firewall Configuration (nftables fw4)
|
||||||
|
|
||||||
|
### Add Bridge Interface to LAN Zone
|
||||||
|
|
||||||
|
```uci
|
||||||
|
config zone
|
||||||
|
option name 'lan'
|
||||||
|
list network 'lan'
|
||||||
|
list network 'zt_wiblan' # Add this
|
||||||
|
```
|
||||||
|
|
||||||
|
Without this, nftables fw4's default `drop` policy blocks all traffic from `br-zt`.
|
||||||
|
|
||||||
|
### Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nft list chain inet fw4 input | grep br-zt
|
||||||
|
# Should show: iifname { "br-zt", ... } jump input_lan
|
||||||
|
```
|
||||||
|
|
||||||
|
## Routing Configuration
|
||||||
|
|
||||||
|
### Policy Routing Rules
|
||||||
|
|
||||||
|
```uci
|
||||||
|
config rule
|
||||||
|
option src '10.11.13.0/24'
|
||||||
|
option lookup '100'
|
||||||
|
option priority '100'
|
||||||
|
```
|
||||||
|
|
||||||
|
This routes traffic FROM WIBLAN clients through table 100.
|
||||||
|
|
||||||
|
### Table 100 (Main Policy)
|
||||||
|
|
||||||
|
```uci
|
||||||
|
config route
|
||||||
|
option interface 'brzt' # Interface name, not device
|
||||||
|
option target '0.0.0.0'
|
||||||
|
option netmask '0.0.0.0'
|
||||||
|
option gateway '10.11.12.3' # Exit gateway IP (NOT WIBLAN_GW)
|
||||||
|
option table '100'
|
||||||
|
|
||||||
|
config route
|
||||||
|
option interface 'brzt'
|
||||||
|
option target '10.11.13.0'
|
||||||
|
option netmask '255.255.255.0'
|
||||||
|
option table '100'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Critical**: The default route in table 100 MUST point to the exit gateway IP
|
||||||
|
(e.g., `10.11.12.3`), NOT to the local bridge IP (`10.11.13.1`). Using the
|
||||||
|
local IP creates a routing loop.
|
||||||
|
|
||||||
|
## DHCP Configuration
|
||||||
|
|
||||||
|
```uci
|
||||||
|
config dhcp 'br_zt'
|
||||||
|
option interface 'zt_wiblan' # Interface name, NOT device name
|
||||||
|
option start '356' # Offset in /23: 10.11.12.0 + 356 = 10.11.13.100
|
||||||
|
option limit '101' # 101 addresses: 10.11.13.100 - 10.11.13.200
|
||||||
|
option leasetime '12h'
|
||||||
|
list dhcp_option '3,10.11.13.1' # Gateway
|
||||||
|
list dhcp_option '6,10.11.13.1' # DNS (use router's dnsmasq)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key points**:
|
||||||
|
- `interface` must reference the **interface** name (`zt_wiblan`), not the
|
||||||
|
device name (`br-zt` or `br_zt`). dnsmasq binds to interfaces, not devices.
|
||||||
|
- In a `/23` network, `start` is an offset from the network base
|
||||||
|
(`10.11.12.0`). To get `10.11.13.100`, use offset `356` (256 + 100).
|
||||||
|
- DNS should point to the router's dnsmasq (`10.11.13.1`) for reliability.
|
||||||
|
Direct `8.8.8.8` works but adds routing complexity.
|
||||||
|
|
||||||
|
## WiFi AP Configuration
|
||||||
|
|
||||||
|
```uci
|
||||||
|
config wifi-iface 'wifinetN'
|
||||||
|
option device 'radio0'
|
||||||
|
option mode 'ap'
|
||||||
|
option ssid 'WIBLAN'
|
||||||
|
option encryption 'psk2'
|
||||||
|
option key 'your-key'
|
||||||
|
option network 'zt_wiblan' # Interface name, NOT device name
|
||||||
|
```
|
||||||
|
|
||||||
|
**Same rule as DHCP**: `network` must reference the **interface** name,
|
||||||
|
not the device name.
|
||||||
|
|
||||||
|
After configuration:
|
||||||
|
```bash
|
||||||
|
wifi reload
|
||||||
|
# Verify bridge membership:
|
||||||
|
brctl show br-zt
|
||||||
|
# Should show both ztk4jpk77j and phy0-apX
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### 1. Bridge Port Has No IP (ARP Fails)
|
||||||
|
|
||||||
|
**Symptom**: One-way connectivity (A→B works, B→A doesn't).
|
||||||
|
|
||||||
|
**Cause**: When the ZT interface is added to a bridge, its assigned IP can be
|
||||||
|
lost. Without an IP, the interface cannot respond to ARP requests.
|
||||||
|
|
||||||
|
**Fix**: Ensure the ZT interface keeps its assigned IP:
|
||||||
|
```bash
|
||||||
|
# Detect ZT IP:
|
||||||
|
ZT_IP=$(zerotier-cli listnetworks | awk '/OK/{for(i=6;i<=NF;i++) if($i~/\//){split($i,a,"/"); print a[1]; exit}}')
|
||||||
|
ZT_BITS=$(zerotier-cli listnetworks | awk '/OK/{for(i=6;i<=NF;i++) if($i~/\//){split($i,a,"/"); print a[2]; exit}}')
|
||||||
|
|
||||||
|
# Add to ZT interface:
|
||||||
|
ip addr add "${ZT_IP}/${ZT_BITS}" dev ztk4jpk77j
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Wrong Interface Reference in UCI
|
||||||
|
|
||||||
|
**Symptom**: dnsmasq doesn't serve DHCP, WiFi AP not bridged.
|
||||||
|
|
||||||
|
**Cause**: Using device name (`br-zt`, `br_zt`) instead of interface name
|
||||||
|
(`zt_wiblan`) in `dhcp.*.interface` or `wireless.*.network`.
|
||||||
|
|
||||||
|
**Fix**: Always reference the **interface** name:
|
||||||
|
```bash
|
||||||
|
uci set dhcp.br_zt.interface=zt_wiblan
|
||||||
|
uci set wireless.wifinet1.network=zt_wiblan
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Firewall Zone Missing
|
||||||
|
|
||||||
|
**Symptom**: Traffic from WIBLAN clients is silently dropped.
|
||||||
|
|
||||||
|
**Cause**: nftables fw4 has `policy drop` on INPUT/FORWARD. The bridge
|
||||||
|
interface isn't in any firewall zone.
|
||||||
|
|
||||||
|
**Fix**: Add the interface to the LAN zone:
|
||||||
|
```bash
|
||||||
|
uci add_list firewall.@zone[0].network=zt_wiblan
|
||||||
|
uci commit firewall
|
||||||
|
/etc/init.d/firewall restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. DHCP Range Wrong in /23
|
||||||
|
|
||||||
|
**Symptom**: Clients get IPs in wrong subnet (e.g., `10.11.12.x` instead
|
||||||
|
of `10.11.13.x`).
|
||||||
|
|
||||||
|
**Cause**: In a `/23` network, dnsmasq's `start` is an offset from the
|
||||||
|
network base (`10.11.12.0`), not from `10.11.13.0`.
|
||||||
|
|
||||||
|
**Fix**: Calculate correct offset:
|
||||||
|
```
|
||||||
|
10.11.13.100 = 10.11.12.0 + 356 → start=356
|
||||||
|
10.11.13.200 = 10.11.12.0 + 456 → limit=101 (356+101-1=456)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Table 100 Routes to Self
|
||||||
|
|
||||||
|
**Symptom**: Client traffic loops back to the gateway.
|
||||||
|
|
||||||
|
**Cause**: Table 100 default route points to `WIBLAN_GW` (local bridge IP)
|
||||||
|
instead of the exit gateway IP.
|
||||||
|
|
||||||
|
**Fix**: Route via the exit gateway:
|
||||||
|
```bash
|
||||||
|
ip route replace default via 10.11.12.3 dev ztk4jpk77j table 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. UCI Values Have Embedded Quotes
|
||||||
|
|
||||||
|
**Symptom**: UCI values contain literal single quotes (e.g., `'static'`
|
||||||
|
instead of `static`).
|
||||||
|
|
||||||
|
**Cause**: Shell quotes in `uci set` commands are passed as part of the value:
|
||||||
|
```bash
|
||||||
|
# WRONG:
|
||||||
|
uci set "network.zt_wiblan.proto='static'" # Value becomes 'static'
|
||||||
|
# RIGHT:
|
||||||
|
uci set "network.zt_wiblan.proto=static" # Value becomes static
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. ZeroTier Ethernet Bridging Disabled
|
||||||
|
|
||||||
|
**Symptom**: WiFi clients get DHCP leases but cannot reach gateway or internet.
|
||||||
|
|
||||||
|
**Cause**: ZeroTier network controller has "Allow Ethernet Bridging" disabled.
|
||||||
|
L2 frames (ARP, DHCP) cannot traverse the tunnel.
|
||||||
|
|
||||||
|
**Fix**: Enable at my.zerotier.com → network → Settings → "Allow Ethernet
|
||||||
|
Bridging".
|
||||||
|
|
||||||
|
## Verification Checklist
|
||||||
|
|
||||||
|
After setup, verify each component:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Bridge membership
|
||||||
|
brctl show br-zt
|
||||||
|
# Should show: ztk4jpk77j + phy0-apX
|
||||||
|
|
||||||
|
# 2. Bridge IPs
|
||||||
|
ip addr show br-zt
|
||||||
|
# Should show: 10.11.13.1/23
|
||||||
|
|
||||||
|
# 3. ZT interface IP
|
||||||
|
ip addr show ztk4jpk77j
|
||||||
|
# Should show: 10.11.12.x/23
|
||||||
|
|
||||||
|
# 4. Firewall zones
|
||||||
|
nft list chain inet fw4 input | grep br-zt
|
||||||
|
# Should show: iifname { "br-zt", ... } jump input_lan
|
||||||
|
|
||||||
|
# 5. Policy routing
|
||||||
|
ip rule show | grep "from 10.11.13.0/24"
|
||||||
|
# Should show: 100: from 10.11.13.0/24 lookup 100
|
||||||
|
|
||||||
|
# 6. Table 100 route
|
||||||
|
ip route show table 100
|
||||||
|
# Should show: default via 10.11.12.3 dev ztk4jpk77j
|
||||||
|
|
||||||
|
# 7. DHCP
|
||||||
|
cat /tmp/dhcp.leases | grep 10.11.13
|
||||||
|
# Should show client leases
|
||||||
|
|
||||||
|
# 8. Connectivity
|
||||||
|
ping -c 3 10.11.13.135 # From router to client
|
||||||
|
# From client: ping 10.11.13.1 (gateway)
|
||||||
|
# From client: ping 8.8.8.8 (internet via exit gateway)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
The setup script supports these overrides for testing:
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `ZTG_BRIDGE` | `br-zt` | Bridge device name |
|
||||||
|
| `ZTG_BRIDGE_PORTS` | auto-detect | Space-separated bridge ports |
|
||||||
|
| `ZTG_WIBLAN_CIDR` | `10.11.13.0/24` | WIBLAN subnet |
|
||||||
|
| `ZTG_WIBLAN_GW` | `10.11.13.1` | WIBLAN gateway IP |
|
||||||
|
| `ZTG_BRIDGE_NETMASK` | `255.255.254.0` | Bridge netmask (/23) |
|
||||||
|
| `ZTG_TABLE_MAIN` | `100` | Main policy table |
|
||||||
|
| `ZTG_TABLE_DRAIN` | `101` | Drain policy table |
|
||||||
|
| `ZTG_WIFI_SSID` | `WIBLAN` | WiFi AP SSID |
|
||||||
|
| `ZTG_WIFI_KEY` | `zt-r0ute-2026` | WiFi AP WPA2 key |
|
||||||
|
| `ZTG_WIFI_RADIO` | auto-detect | WiFi radio device |
|
||||||
|
| `ZTG_SKIP_PERSIST` | `0` | Skip UCI persistence (testing) |
|
||||||
@@ -137,6 +137,7 @@ return view.extend({
|
|||||||
{ cmd: 'setup-bridge', label: _('Setup Bridge'), desc: _('Create br-zt bridge interface') },
|
{ cmd: 'setup-bridge', label: _('Setup Bridge'), desc: _('Create br-zt bridge interface') },
|
||||||
{ cmd: 'setup-routing', label: _('Setup Routing'), desc: _('Configure policy routing tables') },
|
{ cmd: 'setup-routing', label: _('Setup Routing'), desc: _('Configure policy routing tables') },
|
||||||
{ cmd: 'setup-dhcp', label: _('Setup DHCP'), desc: _('Configure DHCP for WIBLAN subnet') },
|
{ cmd: 'setup-dhcp', label: _('Setup DHCP'), desc: _('Configure DHCP for WIBLAN subnet') },
|
||||||
|
{ cmd: 'setup-wifi-ap', label: _('Setup WiFi AP'), desc: _('Create WIBLAN WiFi AP bridged to br-zt') },
|
||||||
{ cmd: 'setup-hotplug', label: _('Setup Hotplug'), desc: _('Create hotplug script for route persistence') },
|
{ cmd: 'setup-hotplug', label: _('Setup Hotplug'), desc: _('Create hotplug script for route persistence') },
|
||||||
{ cmd: 'setup-persistence', label: _('Setup Persistence'), desc: _('Write rc.local + UCI routes') },
|
{ cmd: 'setup-persistence', label: _('Setup Persistence'), desc: _('Write rc.local + UCI routes') },
|
||||||
{ cmd: 'setup-all', label: _('Run Full Setup'), desc: _('Configure everything at once') }
|
{ cmd: 'setup-all', label: _('Run Full Setup'), desc: _('Configure everything at once') }
|
||||||
|
|||||||
44
mise.toml
44
mise.toml
@@ -87,32 +87,28 @@ echo " ${HOST} is reachable."
|
|||||||
|
|
||||||
echo "==> Uploading files..."
|
echo "==> Uploading files..."
|
||||||
|
|
||||||
# root/ files → strip leading root/, map to / on device
|
# Auto-discover files from root/ and htdocs/, map to device paths.
|
||||||
scp -O -q root/usr/sbin/zt-gateway-switch "$HOST:/usr/sbin/"
|
# root/usr/sbin/foo → /usr/sbin/foo
|
||||||
scp -O -q root/usr/sbin/zt-gateway-setup "$HOST:/usr/sbin/"
|
# htdocs/luci-static/... → /www/luci-static/...
|
||||||
echo " zt-gateway-setup"
|
UPLOADED=0
|
||||||
echo " zt-gateway-switch"
|
find root/ htdocs/ -type f | sort | while IFS= read -r src; do
|
||||||
scp -O -q root/usr/share/rpcd/ucode/zt-gateway.uc "$HOST:/usr/share/rpcd/ucode/"
|
case "$src" in
|
||||||
echo " rpcd/ucode/zt-gateway.uc"
|
root/*) dest="/${src#root/}" ;;
|
||||||
scp -O -q root/usr/share/rpcd/ucode/system.uc "$HOST:/usr/share/rpcd/ucode/"
|
htdocs/*) dest="/www/${src#htdocs/}" ;;
|
||||||
echo " rpcd/ucode/system.uc"
|
*) echo "SKIP: $src (unknown prefix)"; continue ;;
|
||||||
scp -O -q root/usr/share/ucode/luci/runtime.uc "$HOST:/usr/share/ucode/luci/"
|
esac
|
||||||
echo " ucode/luci/runtime.uc"
|
destdir="${dest%/*}"
|
||||||
scp -O -q root/usr/share/luci/menu.d/luci-app-zt-gateway.json "$HOST:/usr/share/luci/menu.d/"
|
ssh -q "$HOST" mkdir -p "$destdir"
|
||||||
echo " menu.d/luci-app-zt-gateway.json"
|
scp -O -q "$src" "$HOST:$dest"
|
||||||
scp -O -q root/usr/share/rpcd/acl.d/luci-app-zt-gateway.json "$HOST:/usr/share/rpcd/acl.d/"
|
echo " ${src#./}"
|
||||||
echo " acl.d/luci-app-zt-gateway.json"
|
UPLOADED=$((UPLOADED + 1))
|
||||||
scp -O -q root/etc/config/zt-gateway "$HOST:/etc/config/"
|
done
|
||||||
echo " config/zt-gateway"
|
|
||||||
|
|
||||||
# Frontend view
|
|
||||||
ssh -q "$HOST" mkdir -p /www/luci-static/resources/view/zt-gateway
|
|
||||||
scp -O -q htdocs/luci-static/resources/view/zt-gateway/overview.js "$HOST:/www/luci-static/resources/view/zt-gateway/"
|
|
||||||
echo " overview.js"
|
|
||||||
|
|
||||||
echo "==> Setting permissions..."
|
echo "==> Setting permissions..."
|
||||||
ssh -q "$HOST" chmod +x /usr/sbin/zt-gateway-switch /usr/sbin/zt-gateway-setup
|
ssh -q "$HOST" find /usr/sbin -name 'zt-gateway-*' -exec chmod +x {} +
|
||||||
echo "==> Restarting rpcd and uhttpd..."
|
|
||||||
|
echo "==> Clearing LuCI cache and restarting services..."
|
||||||
|
ssh -q "$HOST" rm -rf /tmp/luci-* 2>/dev/null || true
|
||||||
ssh -q "$HOST" /etc/init.d/rpcd restart
|
ssh -q "$HOST" /etc/init.d/rpcd restart
|
||||||
ssh -q "$HOST" /etc/init.d/uhttpd restart
|
ssh -q "$HOST" /etc/init.d/uhttpd restart
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,10 @@
|
|||||||
#
|
#
|
||||||
# Environment overrides (for testing and non-default configs):
|
# Environment overrides (for testing and non-default configs):
|
||||||
# ZTG_BRIDGE bridge device (default: br-zt)
|
# ZTG_BRIDGE bridge device (default: br-zt)
|
||||||
# ZTG_BRIDGE_PORTS space-separated ports (default: ztabc0)
|
# ZTG_BRIDGE_PORTS space-separated ports (default: auto-detect zt* interface)
|
||||||
# ZTG_WIBLAN_CIDR WIBLAN subnet (default: 10.11.13.0/24)
|
# ZTG_WIBLAN_CIDR WIBLAN subnet (default: 10.11.13.0/24)
|
||||||
# ZTG_WIBLAN_GW WIBLAN gateway IP (default: 10.11.13.1)
|
# ZTG_WIBLAN_GW WIBLAN gateway IP (default: 10.11.13.1)
|
||||||
|
# ZTG_BRIDGE_NETMASK bridge subnet mask (default: 255.255.254.0 /23)
|
||||||
# ZTG_WIBLAN_LEASE_FIRST first DHCP IP (default: 10.11.13.100)
|
# ZTG_WIBLAN_LEASE_FIRST first DHCP IP (default: 10.11.13.100)
|
||||||
# ZTG_WIBLAN_LEASE_LAST last DHCP IP (default: 10.11.13.200)
|
# ZTG_WIBLAN_LEASE_LAST last DHCP IP (default: 10.11.13.200)
|
||||||
# ZTG_TABLE_MAIN main policy table (default: 100)
|
# ZTG_TABLE_MAIN main policy table (default: 100)
|
||||||
@@ -35,6 +36,10 @@
|
|||||||
# ZTG_RCLOCAL rc.local path
|
# ZTG_RCLOCAL rc.local path
|
||||||
# ZTG_DHCPCONF DHCP UCI config file (default: /etc/config/dhcp)
|
# ZTG_DHCPCONF DHCP UCI config file (default: /etc/config/dhcp)
|
||||||
# ZTG_NETWORKCONF network UCI config file (default: /etc/config/network)
|
# ZTG_NETWORKCONF network UCI config file (default: /etc/config/network)
|
||||||
|
# ZTG_WIFI_SSID WiFi AP SSID (default: WIBLAN)
|
||||||
|
# ZTG_WIFI_KEY WiFi AP WPA2 key (default: zt-r0ute-2026)
|
||||||
|
# ZTG_WIFI_RADIO WiFi radio device (default: auto-detect first radio)
|
||||||
|
# ZTG_WIFI_ENCRYPTION WiFi encryption (default: psk2)
|
||||||
# ZTG_SKIP_PERSIST skip UCI persistence (testing)
|
# ZTG_SKIP_PERSIST skip UCI persistence (testing)
|
||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
@@ -43,9 +48,20 @@ set -eu
|
|||||||
# Config
|
# Config
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
BRIDGE="${ZTG_BRIDGE:-br-zt}"
|
BRIDGE="${ZTG_BRIDGE:-br-zt}"
|
||||||
BRIDGE_PORTS="${ZTG_BRIDGE_PORTS:-ztabc0}"
|
# Auto-detect ZeroTier interface if not specified (zt + random suffix, not br-zt)
|
||||||
|
if [ -n "${ZTG_BRIDGE_PORTS:-}" ]; then
|
||||||
|
BRIDGE_PORTS="$ZTG_BRIDGE_PORTS"
|
||||||
|
else
|
||||||
|
BRIDGE_PORTS=$(ip -o link show 2>/dev/null \
|
||||||
|
| awk -F': ' '/^[0-9]+:/{gsub(/@.*/, "", $2); if ($2 ~ /^zt/ && $2 != "br-zt") print $2; exit}')
|
||||||
|
if [ -z "$BRIDGE_PORTS" ]; then
|
||||||
|
BRIDGE_PORTS="ztabc0"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
WIBLAN_CIDR="${ZTG_WIBLAN_CIDR:-10.11.13.0/24}"
|
WIBLAN_CIDR="${ZTG_WIBLAN_CIDR:-10.11.13.0/24}"
|
||||||
WIBLAN_GW="${ZTG_WIBLAN_GW:-10.11.13.1}"
|
WIBLAN_GW="${ZTG_WIBLAN_GW:-10.11.13.1}"
|
||||||
|
# Bridge netmask must be /23 to cover both ZeroTier (10.11.12.x) and WIBLAN (10.11.13.x)
|
||||||
|
BRIDGE_NETMASK="${ZTG_BRIDGE_NETMASK:-255.255.254.0}"
|
||||||
WIBLAN_LEASE_FIRST="${ZTG_WIBLAN_LEASE_FIRST:-10.11.13.100}"
|
WIBLAN_LEASE_FIRST="${ZTG_WIBLAN_LEASE_FIRST:-10.11.13.100}"
|
||||||
WIBLAN_LEASE_LAST="${ZTG_WIBLAN_LEASE_LAST:-10.11.13.200}"
|
WIBLAN_LEASE_LAST="${ZTG_WIBLAN_LEASE_LAST:-10.11.13.200}"
|
||||||
TABLE_MAIN="${ZTG_TABLE_MAIN:-100}"
|
TABLE_MAIN="${ZTG_TABLE_MAIN:-100}"
|
||||||
@@ -57,11 +73,21 @@ HOTPLUG="${ZTG_HOTPLUG:-/etc/hotplug.d/net/99-zerotier-bridge}"
|
|||||||
RCLOCAL="${ZTG_RCLOCAL:-/etc/rc.local}"
|
RCLOCAL="${ZTG_RCLOCAL:-/etc/rc.local}"
|
||||||
DHCPCONF="${ZTG_DHCPCONF:-/etc/config/dhcp}"
|
DHCPCONF="${ZTG_DHCPCONF:-/etc/config/dhcp}"
|
||||||
SKIP_PERSIST="${ZTG_SKIP_PERSIST:-0}"
|
SKIP_PERSIST="${ZTG_SKIP_PERSIST:-0}"
|
||||||
|
WiFi_SSID="${ZTG_WIFI_SSID:-WIBLAN}"
|
||||||
|
WiFi_KEY="${ZTG_WIFI_KEY:-zt-r0ute-2026}"
|
||||||
|
WiFi_ENCRYPTION="${ZTG_WIFI_ENCRYPTION:-psk2}"
|
||||||
|
# Auto-detect first WiFi radio if not specified
|
||||||
|
WiFi_RADIO="${ZTG_WIFI_RADIO:-}"
|
||||||
|
if [ -z "$WiFi_RADIO" ]; then
|
||||||
|
WiFi_RADIO=$(uci -q get wireless.@wifi-device[0].name 2>/dev/null || echo "radio0")
|
||||||
|
fi
|
||||||
|
|
||||||
# Derived: extract prefix bits from CIDR
|
# Derived: extract prefix bits from CIDR
|
||||||
WIBLAN_BITS="${WIBLAN_CIDR##*/}"
|
WIBLAN_BITS="${WIBLAN_CIDR##*/}"
|
||||||
# UCI-safe section name (replace hyphens with underscores)
|
# UCI-safe section name (replace hyphens with underscores)
|
||||||
BRIDGE_UCI=$(printf '%s' "$BRIDGE" | tr '-' '_')
|
BRIDGE_UCI=$(printf '%s' "$BRIDGE" | tr '-' '_')
|
||||||
|
# Interface name (used by DHCP and WiFi AP, must reference interface not device)
|
||||||
|
WIBLAN_IFACE="zt_wiblan"
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -78,16 +104,17 @@ ensure_uci_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# CIDR to dotted mask (e.g. 24 -> 255.255.255.0)
|
||||||
# CIDR to dotted mask (e.g. 24 -> 255.255.255.0)
|
# CIDR to dotted mask (e.g. 24 -> 255.255.255.0)
|
||||||
_cidr_to_mask() {
|
_cidr_to_mask() {
|
||||||
bits=$1
|
bits=$1
|
||||||
mask=""
|
# Build octets from the CIDR prefix
|
||||||
|
octets=""
|
||||||
while [ "$bits" -gt 0 ]; do
|
while [ "$bits" -gt 0 ]; do
|
||||||
if [ "$bits" -ge 8 ]; then
|
if [ "$bits" -ge 8 ]; then
|
||||||
oct=255
|
oct=255
|
||||||
bits=$((bits - 8))
|
bits=$((bits - 8))
|
||||||
else
|
else
|
||||||
# Build partial octet: bits leading 1s in MSB position
|
|
||||||
oct=0
|
oct=0
|
||||||
j=0
|
j=0
|
||||||
while [ $j -lt "$bits" ]; do
|
while [ $j -lt "$bits" ]; do
|
||||||
@@ -96,13 +123,17 @@ _cidr_to_mask() {
|
|||||||
done
|
done
|
||||||
bits=0
|
bits=0
|
||||||
fi
|
fi
|
||||||
if [ -n "$mask" ]; then
|
if [ -n "$octets" ]; then
|
||||||
mask="${mask}.${oct}"
|
octets="${octets}.${oct}"
|
||||||
else
|
else
|
||||||
mask="${oct}"
|
octets="${oct}"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
printf '%s' "$mask"
|
# Pad remaining octets with 0
|
||||||
|
while [ "$(printf '%s' "$octets" | tr -cd '.' | wc -c)" -lt 3 ]; do
|
||||||
|
octets="${octets}.0"
|
||||||
|
done
|
||||||
|
printf '%s' "$octets"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
@@ -211,10 +242,10 @@ cmd_setup_bridge() {
|
|||||||
# Create interface section bridging to br-zt for WIBLAN
|
# Create interface section bridging to br-zt for WIBLAN
|
||||||
if ! uci -q get "network.zt_wiblan" >/dev/null 2>&1; then
|
if ! uci -q get "network.zt_wiblan" >/dev/null 2>&1; then
|
||||||
uci -q set "network.zt_wiblan=interface"
|
uci -q set "network.zt_wiblan=interface"
|
||||||
uci -q set "network.zt_wiblan.proto='static'"
|
uci -q set "network.zt_wiblan.proto=static"
|
||||||
uci -q set "network.zt_wiblan.device='${BRIDGE}'"
|
uci -q set "network.zt_wiblan.device=${BRIDGE}"
|
||||||
uci -q set "network.zt_wiblan.ipaddr='${WIBLAN_GW}'"
|
uci -q set "network.zt_wiblan.ipaddr=${WIBLAN_GW}"
|
||||||
uci -q set "network.zt_wiblan.netmask='$(_cidr_to_mask "$WIBLAN_BITS")'"
|
uci -q set "network.zt_wiblan.netmask=${BRIDGE_NETMASK}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$SKIP_PERSIST" != "1" ]; then
|
if [ "$SKIP_PERSIST" != "1" ]; then
|
||||||
@@ -228,6 +259,30 @@ cmd_setup_bridge() {
|
|||||||
log "warning: ifup zt_wiblan failed; may need 'service network restart'"
|
log "warning: ifup zt_wiblan failed; may need 'service network restart'"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Ensure ZeroTier-assigned IP stays on the ZT interface (needed for ARP)
|
||||||
|
# When ZT interface is a bridge port, the assigned IP can be lost
|
||||||
|
zt_ip=$(zerotier-cli listnetworks 2>/dev/null \
|
||||||
|
| awk '/OK/{for(i=6;i<=NF;i++) if($i ~ /\//) {split($i,a,"/"); print a[1]; exit}}')
|
||||||
|
zt_bits=$(zerotier-cli listnetworks 2>/dev/null \
|
||||||
|
| awk '/OK/{for(i=6;i<=NF;i++) if($i ~ /\//) {split($i,a,"/"); print a[2]; exit}}')
|
||||||
|
if [ -n "$zt_ip" ] && [ -n "$zt_bits" ]; then
|
||||||
|
if ! ip -4 addr show dev "$BRIDGE_PORTS" 2>/dev/null | grep -q "$zt_ip"; then
|
||||||
|
ip addr add "${zt_ip}/${zt_bits}" dev "$BRIDGE_PORTS" 2>/dev/null || \
|
||||||
|
log "warning: could not add ZT IP ${zt_ip}/${zt_bits} to ${BRIDGE_PORTS}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# Add bridge interface to firewall LAN zone (needed for nftables fw4)
|
||||||
|
if command -v uci >/dev/null 2>&1 && [ "$SKIP_PERSIST" != "1" ]; then
|
||||||
|
lan_zone=$(uci -q get firewall.@zone[0].name 2>/dev/null)
|
||||||
|
if [ "$lan_zone" = "lan" ]; then
|
||||||
|
if ! uci -q get firewall.@zone[0].network 2>/dev/null | grep -q "zt_wiblan"; then
|
||||||
|
uci -q add_list "firewall.@zone[0].network=zt_wiblan"
|
||||||
|
uci -q commit firewall
|
||||||
|
log "added zt_wiblan to firewall LAN zone"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
log "bridge ${BRIDGE} setup complete"
|
log "bridge ${BRIDGE} setup complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,15 +296,38 @@ cmd_setup_routing() {
|
|||||||
ip route replace "$WIBLAN_GW" dev "$BRIDGE" 2>/dev/null || \
|
ip route replace "$WIBLAN_GW" dev "$BRIDGE" 2>/dev/null || \
|
||||||
log "warning: host route to ${WIBLAN_GW} failed"
|
log "warning: host route to ${WIBLAN_GW} failed"
|
||||||
|
|
||||||
# Table 100 (main policy): default via WIBLAN_GW
|
# Table 100 (main policy): default via exit gateway + direct WIBLAN subnet route
|
||||||
ip route replace default via "$WIBLAN_GW" dev "$BRIDGE" table "$TABLE_MAIN"
|
# The exit gateway is the ZeroTier peer that has internet access (not WIBLAN_GW which is local)
|
||||||
|
EXIT_GW=$(uci -q get zt-gateway.global.active_ip 2>/dev/null || \
|
||||||
|
uci -q get zt-gateway.@gateway[0].ip 2>/dev/null || \
|
||||||
|
ip route show table "$TABLE_MWAN" 2>/dev/null \
|
||||||
|
| awk '/default/{for(i=1;i<=NF;i++) if($i=="via") {print $(i+1); exit}}')
|
||||||
|
if [ -z "$EXIT_GW" ]; then
|
||||||
|
# Fallback: find ZeroTier peer IP on the same /23
|
||||||
|
EXIT_GW=$(ip route show table "$TABLE_MWAN" 2>/dev/null \
|
||||||
|
| awk '/via.*dev/{for(i=1;i<=NF;i++) if($i=="via") {print $(i+1); exit}}')
|
||||||
|
fi
|
||||||
|
if [ -n "$EXIT_GW" ]; then
|
||||||
|
log "exit gateway: ${EXIT_GW}"
|
||||||
|
ip route replace default via "$EXIT_GW" dev "$BRIDGE_PORTS" table "$TABLE_MAIN"
|
||||||
|
ip route replace "$WIBLAN_CIDR" dev "$BRIDGE" table "$TABLE_MAIN"
|
||||||
|
|
||||||
# Table 101 (drain): default via WIBLAN_GW (same default; drain overrides per-flow)
|
# Table 101 (drain): default via exit gateway
|
||||||
ip route replace default via "$WIBLAN_GW" dev "$BRIDGE" table "$TABLE_DRAIN"
|
ip route replace default via "$EXIT_GW" dev "$BRIDGE_PORTS" table "$TABLE_DRAIN"
|
||||||
|
else
|
||||||
|
log "warning: could not determine exit gateway; using WIBLAN_GW"
|
||||||
|
ip route replace default via "$WIBLAN_GW" dev "$BRIDGE" table "$TABLE_MAIN"
|
||||||
|
ip route replace "$WIBLAN_CIDR" dev "$BRIDGE" table "$TABLE_MAIN"
|
||||||
|
ip route replace default via "$WIBLAN_GW" dev "$BRIDGE" table "$TABLE_DRAIN"
|
||||||
|
fi
|
||||||
|
|
||||||
# mwan3 return-traffic table: route WIBLAN back through bridge
|
# mwan3 return-traffic table: route WIBLAN back through bridge
|
||||||
ip route replace "$WIBLAN_CIDR" dev "$BRIDGE" table "$TABLE_MWAN"
|
ip route replace "$WIBLAN_CIDR" dev "$BRIDGE" table "$TABLE_MWAN"
|
||||||
|
|
||||||
|
# ip rule: WIBLAN subnet -> main policy table
|
||||||
|
ip rule add from "$WIBLAN_CIDR" table "$TABLE_MAIN" priority 100 2>/dev/null || \
|
||||||
|
ip rule replace from "$WIBLAN_CIDR" table "$TABLE_MAIN" priority 100
|
||||||
|
|
||||||
# ip rule: fwmark 0x100 -> drain table
|
# ip rule: fwmark 0x100 -> drain table
|
||||||
ip rule add fwmark "$FWMARK" table "$TABLE_DRAIN" priority "$DRAIN_PRIORITY" 2>/dev/null || \
|
ip rule add fwmark "$FWMARK" table "$TABLE_DRAIN" priority "$DRAIN_PRIORITY" 2>/dev/null || \
|
||||||
ip rule replace fwmark "$FWMARK" table "$TABLE_DRAIN" priority "$DRAIN_PRIORITY"
|
ip rule replace fwmark "$FWMARK" table "$TABLE_DRAIN" priority "$DRAIN_PRIORITY"
|
||||||
@@ -262,20 +340,28 @@ cmd_setup_routing() {
|
|||||||
# Host route
|
# Host route
|
||||||
if ! uci -q get "network.zt_wiblan_host" >/dev/null 2>&1; then
|
if ! uci -q get "network.zt_wiblan_host" >/dev/null 2>&1; then
|
||||||
uci -q set "network.zt_wiblan_host=route"
|
uci -q set "network.zt_wiblan_host=route"
|
||||||
uci -q set "network.zt_wiblan_host.target='${WIBLAN_GW}'"
|
uci -q set "network.zt_wiblan_host.target=${WIBLAN_GW}"
|
||||||
uci -q set "network.zt_wiblan_host.interface='${BRIDGE}'"
|
uci -q set "network.zt_wiblan_host.interface=${BRIDGE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Main policy table default route
|
# Main policy table default route
|
||||||
if ! uci -q get "network.zt_wiblan_default" >/dev/null 2>&1; then
|
if ! uci -q get "network.zt_wiblan_default" >/dev/null 2>&1; then
|
||||||
uci -q set "network.zt_wiblan_default=route"
|
uci -q set "network.zt_wiblan_default=route"
|
||||||
uci -q set "network.zt_wiblan_default.target='0.0.0.0'"
|
uci -q set "network.zt_wiblan_default.target=0.0.0.0"
|
||||||
uci -q set "network.zt_wiblan_default.netmask='0.0.0.0'"
|
uci -q set "network.zt_wiblan_default.netmask=0.0.0.0"
|
||||||
uci -q set "network.zt_wiblan_default.gateway='${WIBLAN_GW}'"
|
uci -q set "network.zt_wiblan_default.gateway=${WIBLAN_GW}"
|
||||||
uci -q set "network.zt_wiblan_default.interface='${BRIDGE}'"
|
uci -q set "network.zt_wiblan_default.interface=${BRIDGE}"
|
||||||
uci -q set "network.zt_wiblan_default.table='${TABLE_MAIN}'"
|
uci -q set "network.zt_wiblan_default.table=${TABLE_MAIN}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# WIBLAN subnet route in main policy table
|
||||||
|
if ! uci -q get "network.zt_wiblan_subnet" >/dev/null 2>&1; then
|
||||||
|
uci -q set "network.zt_wiblan_subnet=route"
|
||||||
|
uci -q set "network.zt_wiblan_subnet.target=${WIBLAN_CIDR}"
|
||||||
|
uci -q set "network.zt_wiblan_subnet.netmask=$(_cidr_to_mask "$WIBLAN_BITS")"
|
||||||
|
uci -q set "network.zt_wiblan_subnet.interface=${BRIDGE}"
|
||||||
|
uci -q set "network.zt_wiblan_subnet.table=${TABLE_MAIN}"
|
||||||
|
fi
|
||||||
uci commit network
|
uci commit network
|
||||||
log "routing UCI config committed"
|
log "routing UCI config committed"
|
||||||
fi
|
fi
|
||||||
@@ -299,20 +385,17 @@ cmd_setup_dhcp() {
|
|||||||
else
|
else
|
||||||
uci -q set "dhcp.${BRIDGE_UCI}=dhcp"
|
uci -q set "dhcp.${BRIDGE_UCI}=dhcp"
|
||||||
fi
|
fi
|
||||||
|
uci -q set "dhcp.${BRIDGE_UCI}.interface=${WIBLAN_IFACE}"
|
||||||
uci -q set "dhcp.${BRIDGE_UCI}.interface=${BRIDGE}"
|
|
||||||
uci -q set "dhcp.${BRIDGE_UCI}.start=${WIBLAN_LEASE_FIRST##*.}"
|
uci -q set "dhcp.${BRIDGE_UCI}.start=${WIBLAN_LEASE_FIRST##*.}"
|
||||||
uci -q set "dhcp.${BRIDGE_UCI}.limit=$(( ${WIBLAN_LEASE_LAST##*.} - ${WIBLAN_LEASE_FIRST##*.} + 1 ))"
|
uci -q set "dhcp.${BRIDGE_UCI}.limit=$(( ${WIBLAN_LEASE_LAST##*.} - ${WIBLAN_LEASE_FIRST##*.} + 1 ))"
|
||||||
uci -q set "dhcp.${BRIDGE_UCI}.leasetime=12h"
|
uci -q set "dhcp.${BRIDGE_UCI}.leasetime=12h"
|
||||||
|
# Provide gateway and DNS to DHCP clients
|
||||||
|
uci -q set "dhcp.${BRIDGE_UCI}.dhcp_option=3,${WIBLAN_GW}"
|
||||||
|
uci -q add_list "dhcp.${BRIDGE_UCI}.dhcp_option=6,8.8.8.8,1.1.1.1"
|
||||||
|
|
||||||
# Ignore WIBLAN subnet in upstream DHCP (prevent handing out
|
# Disable DHCP on LAN to prevent conflicting leases
|
||||||
# conflicting leases on the LAN side)
|
if ! uci -q test dhcp.lan.ignore >/dev/null 2>&1; then
|
||||||
lan_iface=$(uci -q get dhcp.lan.interface 2>/dev/null || echo "lan")
|
uci -q set "dhcp.lan.ignore=1"
|
||||||
if [ -n "$lan_iface" ]; then
|
|
||||||
# Add WIBLAN to lan's ignore list if not already there
|
|
||||||
if ! uci -q get "dhcp.lan.ignore" 2>/dev/null | grep -q "$WIBLAN_CIDR"; then
|
|
||||||
uci -q add_list "dhcp.lan.dhcp_option='6,${WIBLAN_GW}'" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$SKIP_PERSIST" != "1" ]; then
|
if [ "$SKIP_PERSIST" != "1" ]; then
|
||||||
@@ -329,6 +412,50 @@ cmd_setup_dhcp() {
|
|||||||
log "DHCP setup complete"
|
log "DHCP setup complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# setup-wifi-ap
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
cmd_setup_wifi_ap() {
|
||||||
|
log "setting up WiFi AP (${WiFi_SSID}) on ${WiFi_RADIO}..."
|
||||||
|
|
||||||
|
if ! command -v uci >/dev/null 2>&1; then
|
||||||
|
die 3 "uci not found; cannot configure WiFi"
|
||||||
|
fi
|
||||||
|
ensure_uci_config wireless
|
||||||
|
|
||||||
|
# Find existing wifinet section for our SSID, or create new one
|
||||||
|
existing=""
|
||||||
|
for idx in 0 1 2 3 4 5 6 7 8 9; do
|
||||||
|
if uci -q get "wireless.wifinet${idx}.ssid" 2>/dev/null | grep -q "^${WiFi_SSID}$"; then
|
||||||
|
existing="wifinet${idx}"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$existing" ]; then
|
||||||
|
log "WiFi AP '${WiFi_SSID}' already exists (${existing}); updating"
|
||||||
|
section="$existing"
|
||||||
|
else
|
||||||
|
section=$(uci -q add wireless wifi-iface)
|
||||||
|
log "created new wireless section: ${section}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
uci -q set "wireless.${section}.device=${WiFi_RADIO}"
|
||||||
|
uci -q set "wireless.${section}.mode=ap"
|
||||||
|
uci -q set "wireless.${section}.ssid=${WiFi_SSID}"
|
||||||
|
uci -q set "wireless.${section}.encryption=${WiFi_ENCRYPTION}"
|
||||||
|
uci -q set "wireless.${section}.key=${WiFi_KEY}"
|
||||||
|
uci -q set "wireless.${section}.network=${WIBLAN_IFACE}"
|
||||||
|
uci -q set "wireless.${section}.wpa_disable_eapol_key_retries=1"
|
||||||
|
|
||||||
|
if [ "$SKIP_PERSIST" != "1" ]; then
|
||||||
|
uci commit wireless
|
||||||
|
log "wireless UCI config committed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "WiFi AP setup complete: ssid=${WiFi_SSID} radio=${WiFi_RADIO} bridge=${BRIDGE}"
|
||||||
|
}
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
# setup-hotplug
|
# setup-hotplug
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
@@ -359,6 +486,20 @@ ZTG_WIBLAN_CIDR="${ZTG_WIBLAN_CIDR:-10.11.13.0/24}"
|
|||||||
|
|
||||||
case "$ACTION" in
|
case "$ACTION" in
|
||||||
ifup)
|
ifup)
|
||||||
|
# Ensure ZeroTier-assigned IP stays on the ZT interface (needed for ARP)
|
||||||
|
zt_if=$(ip -o link show 2>/dev/null | awk -F': ' '/^[0-9]+:/{gsub(/@.*/, "", $2); if ($2 ~ /^zt/ && $2 != "'"$ZTG_BRIDGE"'") print $2; exit}')
|
||||||
|
if [ -n "$zt_if" ]; then
|
||||||
|
zt_ip=$(zerotier-cli listnetworks 2>/dev/null \
|
||||||
|
| awk '/OK/{for(i=6;i<=NF;i++) if($i ~ /\//) {split($i,a,"/"); print a[1]; exit}}')
|
||||||
|
zt_bits=$(zerotier-cli listnetworks 2>/dev/null \
|
||||||
|
| awk '/OK/{for(i=6;i<=NF;i++) if($i ~ /\//) {split($i,a,"/"); print a[2]; exit}}')
|
||||||
|
if [ -n "$zt_ip" ] && [ -n "$zt_bits" ] && \
|
||||||
|
! ip -4 addr show dev "$zt_if" 2>/dev/null | grep -q "$zt_ip"; then
|
||||||
|
ip addr add "${zt_ip}/${zt_bits}" dev "$zt_if" 2>/dev/null
|
||||||
|
logger -t zt-gw-hotplug "added ZT IP ${zt_ip}/${zt_bits} to ${zt_if}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Read active gateway from UCI
|
# Read active gateway from UCI
|
||||||
active_ip=$(uci -q get zt-gateway.global.active_ip 2>/dev/null || \
|
active_ip=$(uci -q get zt-gateway.global.active_ip 2>/dev/null || \
|
||||||
uci -q get zt-gateway.global.active_gateway 2>/dev/null)
|
uci -q get zt-gateway.global.active_gateway 2>/dev/null)
|
||||||
@@ -497,6 +638,7 @@ cmd_setup_all() {
|
|||||||
cmd_setup_bridge
|
cmd_setup_bridge
|
||||||
cmd_setup_routing
|
cmd_setup_routing
|
||||||
cmd_setup_dhcp
|
cmd_setup_dhcp
|
||||||
|
cmd_setup_wifi_ap
|
||||||
cmd_setup_hotplug
|
cmd_setup_hotplug
|
||||||
cmd_setup_persistence
|
cmd_setup_persistence
|
||||||
|
|
||||||
@@ -508,6 +650,7 @@ cmd_setup_all() {
|
|||||||
log " Gateway IP: ${WIBLAN_GW}"
|
log " Gateway IP: ${WIBLAN_GW}"
|
||||||
log " Tables: ${TABLE_MAIN} (main), ${TABLE_DRAIN} (drain)"
|
log " Tables: ${TABLE_MAIN} (main), ${TABLE_DRAIN} (drain)"
|
||||||
log " DHCP: ${WIBLAN_LEASE_FIRST} - ${WIBLAN_LEASE_LAST}"
|
log " DHCP: ${WIBLAN_LEASE_FIRST} - ${WIBLAN_LEASE_LAST}"
|
||||||
|
log " WiFi AP: ${WiFi_SSID} on ${WiFi_RADIO}"
|
||||||
log "========================================="
|
log "========================================="
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,6 +666,7 @@ Commands:
|
|||||||
setup-bridge Create the br-zt bridge interface
|
setup-bridge Create the br-zt bridge interface
|
||||||
setup-routing Configure policy routing (tables 100/101, ip rules)
|
setup-routing Configure policy routing (tables 100/101, ip rules)
|
||||||
setup-dhcp Configure DHCP for WIBLAN on br-zt
|
setup-dhcp Configure DHCP for WIBLAN on br-zt
|
||||||
|
setup-wifi-ap Create WIBLAN WiFi AP bridged to br-zt
|
||||||
setup-hotplug Create hotplug script to re-apply routes on ifup
|
setup-hotplug Create hotplug script to re-apply routes on ifup
|
||||||
setup-persistence Write rc.local + UCI network routes
|
setup-persistence Write rc.local + UCI network routes
|
||||||
setup-all Run all setup-* commands in order
|
setup-all Run all setup-* commands in order
|
||||||
@@ -546,6 +690,7 @@ main() {
|
|||||||
setup-bridge) cmd_setup_bridge "$@" ;;
|
setup-bridge) cmd_setup_bridge "$@" ;;
|
||||||
setup-routing) cmd_setup_routing "$@" ;;
|
setup-routing) cmd_setup_routing "$@" ;;
|
||||||
setup-dhcp) cmd_setup_dhcp "$@" ;;
|
setup-dhcp) cmd_setup_dhcp "$@" ;;
|
||||||
|
setup-wifi-ap) cmd_setup_wifi_ap "$@" ;;
|
||||||
setup-hotplug) cmd_setup_hotplug "$@" ;;
|
setup-hotplug) cmd_setup_hotplug "$@" ;;
|
||||||
setup-persistence) cmd_setup_persistence "$@" ;;
|
setup-persistence) cmd_setup_persistence "$@" ;;
|
||||||
setup-all) cmd_setup_all "$@" ;;
|
setup-all) cmd_setup_all "$@" ;;
|
||||||
|
|||||||
@@ -301,10 +301,10 @@ return {
|
|||||||
},
|
},
|
||||||
call: function(req) {
|
call: function(req) {
|
||||||
const command = req.args?.command;
|
const command = req.args?.command;
|
||||||
if (!command || !match(command, /^(status|setup-bridge|setup-routing|setup-dhcp|setup-hotplug|setup-persistence|setup-all)$/)) {
|
if (!command || !match(command, /^(status|setup-bridge|setup-routing|setup-dhcp|setup-wifi-ap|setup-hotplug|setup-persistence|setup-all)$/)) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Invalid command. Valid: status, setup-bridge, setup-routing, setup-dhcp, setup-hotplug, setup-persistence, setup-all'
|
message: 'Invalid command. Valid: status, setup-bridge, setup-routing, setup-dhcp, setup-wifi-ap, setup-hotplug, setup-persistence, setup-all'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user