feat: add gateway setup wizard and fix test suite

New zt-gateway-setup script with subcommands for configuring this
router as a ZeroTier exit gateway: bridge, routing (tables 100/101),
DHCP, hotplug, and persistence. Exposes setup ubus method via backend
with ACL and LuCI panel in overview.

Also fixes two pre-existing test issues:
- overview.spec.ts: active gateway radio is intentionally enabled
  (users need to re-trigger switch for the already-selected gateway)
- drain.spec.ts: stubs drain status via page.route() since Docker
  has no real conntrack entries; fixed Playwright glob pattern bug
  where **/ubus* fails to match /ubus/?timestamp (regex needed)
This commit is contained in:
2026-07-13 08:48:53 +05:30
parent fbbddd2850
commit cd429291ef
9 changed files with 728 additions and 23 deletions

557
root/usr/sbin/zt-gateway-setup Executable file
View File

@@ -0,0 +1,557 @@
#!/bin/sh
# shellcheck shell=sh
#
# zt-gateway-setup — configure this OpenWrt router as a ZeroTier exit
# gateway for the WIBLAN subnet (10.11.13.0/24).
#
# Subcommands:
# status Show current setup state
# setup-bridge Create the br-zt bridge interface
# setup-routing Configure policy routing (tables 100/101, ip rules)
# setup-dhcp Configure DHCP for WIBLAN on br-zt
# setup-hotplug Create hotplug script to re-apply routes on ifup
# setup-persistence Write rc.local + UCI network routes
# setup-all Run all setup-* commands in order
#
# Exit codes:
# 0 success
# 1 usage / argument error
# 2 precondition failed
# 3 runtime failure
#
# Environment overrides (for testing and non-default configs):
# ZTG_BRIDGE bridge device (default: br-zt)
# ZTG_BRIDGE_PORTS space-separated ports (default: ztabc0)
# ZTG_WIBLAN_CIDR WIBLAN subnet (default: 10.11.13.0/24)
# ZTG_WIBLAN_GW WIBLAN gateway IP (default: 10.11.13.1)
# 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_TABLE_MAIN main policy table (default: 100)
# ZTG_TABLE_DRAIN drain policy table (default: 101)
# ZTG_TABLE_MWAN mwan3 return table (default: 1)
# ZTG_FWMARK drain fwmark (default: 0x100)
# ZTG_DRAIN_PRIORITY drain rule priority (default: 99)
# ZTG_HOTPLUG hotplug script path
# ZTG_RCLOCAL rc.local path
# ZTG_DHCPCONF DHCP UCI config file (default: /etc/config/dhcp)
# ZTG_NETWORKCONF network UCI config file (default: /etc/config/network)
# ZTG_SKIP_PERSIST skip UCI persistence (testing)
set -eu
# ----------------------------------------------------------------------------
# Config
# ----------------------------------------------------------------------------
BRIDGE="${ZTG_BRIDGE:-br-zt}"
BRIDGE_PORTS="${ZTG_BRIDGE_PORTS:-ztabc0}"
WIBLAN_CIDR="${ZTG_WIBLAN_CIDR:-10.11.13.0/24}"
WIBLAN_GW="${ZTG_WIBLAN_GW:-10.11.13.1}"
WIBLAN_LEASE_FIRST="${ZTG_WIBLAN_LEASE_FIRST:-10.11.13.100}"
WIBLAN_LEASE_LAST="${ZTG_WIBLAN_LEASE_LAST:-10.11.13.200}"
TABLE_MAIN="${ZTG_TABLE_MAIN:-100}"
TABLE_DRAIN="${ZTG_TABLE_DRAIN:-101}"
TABLE_MWAN="${ZTG_TABLE_MWAN:-1}"
FWMARK="${ZTG_FWMARK:-0x100}"
DRAIN_PRIORITY="${ZTG_DRAIN_PRIORITY:-99}"
HOTPLUG="${ZTG_HOTPLUG:-/etc/hotplug.d/net/99-zerotier-bridge}"
RCLOCAL="${ZTG_RCLOCAL:-/etc/rc.local}"
DHCPCONF="${ZTG_DHCPCONF:-/etc/config/dhcp}"
SKIP_PERSIST="${ZTG_SKIP_PERSIST:-0}"
# Derived: extract prefix bits from CIDR
WIBLAN_BITS="${WIBLAN_CIDR##*/}"
# UCI-safe section name (replace hyphens with underscores)
BRIDGE_UCI=$(printf '%s' "$BRIDGE" | tr '-' '_')
# ----------------------------------------------------------------------------
# Logging
# ----------------------------------------------------------------------------
log() { printf '[zt-gateway-setup] %s\n' "$*" >&2; }
die() { rc=$1; shift; log "ERROR: $*"; exit "$rc"; }
# Ensure a UCI config file exists (touch it if missing)
ensure_uci_config() {
_conf=/etc/config/"$1"
if [ ! -f "$_conf" ]; then
touch "$_conf"
log "created empty UCI config: ${_conf}"
fi
}
# CIDR to dotted mask (e.g. 24 -> 255.255.255.0)
_cidr_to_mask() {
bits=$1
mask=""
while [ "$bits" -gt 0 ]; do
if [ "$bits" -ge 8 ]; then
oct=255
bits=$((bits - 8))
else
# Build partial octet: bits leading 1s in MSB position
oct=0
j=0
while [ $j -lt "$bits" ]; do
oct=$(( oct | (1 << (7 - j)) ))
j=$((j + 1))
done
bits=0
fi
if [ -n "$mask" ]; then
mask="${mask}.${oct}"
else
mask="${oct}"
fi
done
printf '%s' "$mask"
}
# ----------------------------------------------------------------------------
# Status
# ----------------------------------------------------------------------------
cmd_status() {
log "checking setup status..."
# Bridge
if ip link show "$BRIDGE" >/dev/null 2>&1; then
printf 'bridge: %s (up)\n' "$BRIDGE"
# List ports
ports=$(ip link show master "$BRIDGE" 2>/dev/null \
| awk -F': ' '/^[0-9]+:/{gsub(/@.*/, "", $2); print $2}' \
| tr '\n' ' ')
if [ -n "$ports" ]; then
printf ' ports: %s\n' "$ports"
fi
# IP on bridge
br_addr=$(ip -4 addr show dev "$BRIDGE" 2>/dev/null \
| awk '/inet /{gsub(/\/.*/, "", $2); print $2; exit}')
if [ -n "$br_addr" ]; then
printf ' addr: %s\n' "$br_addr"
fi
else
printf 'bridge: %s (missing)\n' "$BRIDGE"
fi
# Routing tables
table_main_gw=$(ip route show table "$TABLE_MAIN" 2>/dev/null \
| awk '/^[[:space:]]*default/{
for (i=1; i<=NF; i++) if ($i=="via") { print $(i+1); exit }
}')
if [ -n "$table_main_gw" ]; then
printf 'table %s: default via %s (configured)\n' "$TABLE_MAIN" "$table_main_gw"
else
printf 'table %s: (empty)\n' "$TABLE_MAIN"
fi
table_drain_gw=$(ip route show table "$TABLE_DRAIN" 2>/dev/null \
| awk '/^[[:space:]]*default/{
for (i=1; i<=NF; i++) if ($i=="via") { print $(i+1); exit }
}')
if [ -n "$table_drain_gw" ]; then
printf 'table %s: default via %s (configured)\n' "$TABLE_DRAIN" "$table_drain_gw"
else
printf 'table %s: (empty)\n' "$TABLE_DRAIN"
fi
# ip rule for fwmark
if ip rule show 2>/dev/null | grep -q "fwmark ${FWMARK}"; then
printf 'ip rule: fwmark %s -> table %s (configured)\n' "$FWMARK" "$TABLE_DRAIN"
else
printf 'ip rule: fwmark %s (missing)\n' "$FWMARK"
fi
# DHCP
if grep -q "interface '${BRIDGE}'" "$DHCPCONF" 2>/dev/null; then
printf 'dhcp: %s configured in %s\n' "$BRIDGE" "$DHCPCONF"
else
printf 'dhcp: %s (not configured)\n' "$BRIDGE"
fi
# Hotplug
if [ -x "$HOTPLUG" ] || [ -f "$HOTPLUG" ]; then
printf 'hotplug: %s (present)\n' "$HOTPLUG"
else
printf 'hotplug: %s (missing)\n' "$HOTPLUG"
fi
# Persistence in rc.local
if grep -q "zt-gateway" "$RCLOCAL" 2>/dev/null; then
printf 'rc.local: entries present\n'
else
printf 'rc.local: no zt-gateway entries\n'
fi
}
# ----------------------------------------------------------------------------
# setup-bridge
# ----------------------------------------------------------------------------
cmd_setup_bridge() {
log "setting up bridge ${BRIDGE}..."
if ! command -v uci >/dev/null 2>&1; then
die 3 "uci not found; cannot configure bridge"
fi
ensure_uci_config network
# Create or update bridge device in network.uci
# Note: UCI section names cannot contain hyphens, so we use BRIDGE_UCI
if uci -q get "network.${BRIDGE_UCI}" >/dev/null 2>&1; then
log "bridge device ${BRIDGE} already exists in UCI; updating"
else
uci -q set "network.${BRIDGE_UCI}=device"
uci -q set "network.${BRIDGE_UCI}.type=bridge"
uci -q set "network.${BRIDGE_UCI}.name=${BRIDGE}"
fi
# Set bridge ports (space-separated in UCI list)
uci -q delete "network.${BRIDGE_UCI}.ports" 2>/dev/null || true
for port in $BRIDGE_PORTS; do
uci -q add_list "network.${BRIDGE_UCI}.ports=${port}"
done
# Create interface section bridging to br-zt for WIBLAN
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.proto='static'"
uci -q set "network.zt_wiblan.device='${BRIDGE}'"
uci -q set "network.zt_wiblan.ipaddr='${WIBLAN_GW}'"
uci -q set "network.zt_wiblan.netmask='$(_cidr_to_mask "$WIBLAN_BITS")'"
fi
if [ "$SKIP_PERSIST" != "1" ]; then
uci commit network
log "bridge UCI config committed"
fi
# Bring up the bridge (best-effort; may need netifd restart)
if command -v ifup >/dev/null 2>&1; then
ifup "zt_wiblan" 2>/dev/null || \
log "warning: ifup zt_wiblan failed; may need 'service network restart'"
fi
log "bridge ${BRIDGE} setup complete"
}
# ----------------------------------------------------------------------------
# setup-routing
# ----------------------------------------------------------------------------
cmd_setup_routing() {
log "setting up routing (tables ${TABLE_MAIN}/${TABLE_DRAIN})..."
# Host route to WIBLAN gateway via bridge
ip route replace "$WIBLAN_GW" dev "$BRIDGE" 2>/dev/null || \
log "warning: host route to ${WIBLAN_GW} failed"
# Table 100 (main policy): default via WIBLAN_GW
ip route replace default via "$WIBLAN_GW" dev "$BRIDGE" table "$TABLE_MAIN"
# Table 101 (drain): default via WIBLAN_GW (same default; drain overrides per-flow)
ip route replace default via "$WIBLAN_GW" dev "$BRIDGE" table "$TABLE_DRAIN"
# mwan3 return-traffic table: route WIBLAN back through bridge
ip route replace "$WIBLAN_CIDR" dev "$BRIDGE" table "$TABLE_MWAN"
# ip rule: fwmark 0x100 -> drain table
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"
log "routing setup complete"
# Persist to UCI
if [ "$SKIP_PERSIST" != "1" ] && command -v uci >/dev/null 2>&1; then
ensure_uci_config network
# Host route
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.target='${WIBLAN_GW}'"
uci -q set "network.zt_wiblan_host.interface='${BRIDGE}'"
fi
# Main policy table default route
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.target='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.interface='${BRIDGE}'"
uci -q set "network.zt_wiblan_default.table='${TABLE_MAIN}'"
fi
uci commit network
log "routing UCI config committed"
fi
}
# ----------------------------------------------------------------------------
# setup-dhcp
# ----------------------------------------------------------------------------
cmd_setup_dhcp() {
log "setting up DHCP for ${BRIDGE} (${WIBLAN_CIDR})..."
if ! command -v uci >/dev/null 2>&1; then
die 3 "uci not found; cannot configure DHCP"
fi
ensure_uci_config dhcp
# Create DHCP subnet entry for br-zt
# Note: UCI section names cannot contain hyphens, so we use BRIDGE_UCI
if uci -q get "dhcp.${BRIDGE_UCI}" >/dev/null 2>&1; then
log "DHCP entry for ${BRIDGE} already exists; updating"
else
uci -q set "dhcp.${BRIDGE_UCI}=dhcp"
fi
uci -q set "dhcp.${BRIDGE_UCI}.interface=${BRIDGE}"
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}.leasetime=12h"
# Ignore WIBLAN subnet in upstream DHCP (prevent handing out
# conflicting leases on the LAN side)
lan_iface=$(uci -q get dhcp.lan.interface 2>/dev/null || echo "lan")
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
if [ "$SKIP_PERSIST" != "1" ]; then
uci commit dhcp
log "DHCP UCI config committed"
fi
# Restart dnsmasq to pick up changes
if command -v service >/dev/null 2>&1; then
service dnsmasq restart 2>/dev/null || \
log "warning: dnsmasq restart failed; do it manually"
fi
log "DHCP setup complete"
}
# ----------------------------------------------------------------------------
# setup-hotplug
# ----------------------------------------------------------------------------
cmd_setup_hotplug() {
log "creating hotplug script ${HOTPLUG}..."
mkdir -p "$(dirname "$HOTPLUG")"
cat >"$HOTPLUG" <<'HOTPLUG_SCRIPT'
#!/bin/sh
# shellcheck shell=sh
# ZeroTier gateway hotplug — re-apply routing when the bridge interface
# comes up (e.g. after boot, after ZeroTier restart).
#
# Environment: INTERFACE, ACTION (set by netifd hotplug)
# Config: /etc/config/zt-gateway (read at runtime for active gateway IP)
ZTG_BRIDGE="${ZTG_BRIDGE:-br-zt}"
ZTG_TABLE_MAIN="${ZTG_TABLE_MAIN:-100}"
ZTG_TABLE_DRAIN="${ZTG_TABLE_DRAIN:-101}"
ZTG_TABLE_MWAN="${ZTG_TABLE_MWAN:-1}"
ZTG_FWMARK="${ZTG_FWMARK:-0x100}"
ZTG_DRAIN_PRIORITY="${ZTG_DRAIN_PRIORITY:-99}"
ZTG_WIBLAN_CIDR="${ZTG_WIBLAN_CIDR:-10.11.13.0/24}"
# Only act on our bridge interface
[ "$INTERFACE" = "$ZTG_BRIDGE" ] || exit 0
case "$ACTION" in
ifup)
# Read active gateway from UCI
active_ip=$(uci -q get zt-gateway.global.active_ip 2>/dev/null || \
uci -q get zt-gateway.global.active_gateway 2>/dev/null)
# If active_ip is a region name, resolve to IP via gateway section
if [ -n "$active_ip" ] && ! echo "$active_ip" | grep -q '^[0-9]'; then
active_ip=$(uci -q get "zt-gateway.@gateway[0].ip" 2>/dev/null || \
for sec in $(uci -q show zt-gateway 2>/dev/null | \
awk -F'=' '/\.region=/{print $1}' | \
sed 's/\.region//'); do
r=$(uci -q get "${sec}.region" 2>/dev/null)
if [ "$r" = "$active_ip" ]; then
uci -q get "${sec}.ip" 2>/dev/null
break
fi
done)
fi
if [ -z "$active_ip" ]; then
# No active gateway configured; try reading from table
active_ip=$(ip route show table "$ZTG_TABLE_MAIN" 2>/dev/null \
| awk '/^[[:space:]]*default/{
for (i=1; i<=NF; i++) if ($i=="via") { print $(i+1); exit }
}')
fi
if [ -z "$active_ip" ]; then
logger -t zt-gw-hotplug "No active gateway IP; skipping route setup"
exit 0
fi
logger -t zt-gw-hotplug "ifup ${ZTG_BRIDGE}: applying routes via ${active_ip}"
# Host route
ip route replace "$active_ip" dev "$ZTG_BRIDGE"
# Policy routes
ip route replace default via "$active_ip" dev "$ZTG_BRIDGE" table "$ZTG_TABLE_MAIN"
ip route replace default via "$active_ip" dev "$ZTG_BRIDGE" table "$ZTG_TABLE_DRAIN"
# mwan3 return
ip route replace "$ZTG_WIBLAN_CIDR" dev "$ZTG_BRIDGE" table "$ZTG_TABLE_MWAN"
# ip rule for drain fwmark
ip rule add fwmark "$ZTG_FWMARK" table "$ZTG_TABLE_DRAIN" \
priority "$ZTG_DRAIN_PRIORITY" 2>/dev/null || \
ip rule replace fwmark "$ZTG_FWMARK" table "$ZTG_TABLE_DRAIN" \
priority "$ZTG_DRAIN_PRIORITY"
;;
ifdown)
logger -t zt-gw-hotplug "ifdown ${ZTG_BRIDGE}: cleaning up"
ip rule del fwmark "$ZTG_FWMARK" table "$ZTG_TABLE_DRAIN" \
priority "$ZTG_DRAIN_PRIORITY" 2>/dev/null || true
;;
esac
HOTPLUG_SCRIPT
chmod +x "$HOTPLUG"
log "hotplug script created at ${HOTPLUG}"
}
# ----------------------------------------------------------------------------
# setup-persistence
# ----------------------------------------------------------------------------
cmd_setup_persistence() {
log "setting up persistence..."
# Ensure rc.local has the gateway restoration logic
if ! grep -q "zt-gateway" "$RCLOCAL" 2>/dev/null; then
log "adding zt-gateway entry to ${RCLOCAL}"
# Read current rc.local content
rc_content=""
if [ -f "$RCLOCAL" ]; then
rc_content=$(cat "$RCLOCAL")
fi
# Remove trailing 'exit 0' if present, add our block, re-add exit 0
rc_stripped=$(printf '%s\n' "$rc_content" | sed '/^exit 0$/d')
cat >"$RCLOCAL" <<RCEOF
${rc_stripped}
# --- zt-gateway: restore routes on boot ---
# Applied via /etc/hotplug.d/net/99-zerotier-bridge on ifup
# This block ensures the bridge comes up at boot
[ -x /etc/init.d/network ] && /etc/init.d/network reload
exit 0
RCEOF
log "rc.local updated"
else
log "rc.local already contains zt-gateway entries; skipping"
fi
# Write active gateway IP to UCI for the hotplug script
if [ "$SKIP_PERSIST" != "1" ] && command -v uci >/dev/null 2>&1; then
ensure_uci_config zt-gateway
# Store the WIBLAN config for the hotplug to read
if ! uci -q get "zt-gateway.global.wiblan_subnet" >/dev/null 2>&1; then
uci -q set "zt-gateway.global.wiblan_subnet='${WIBLAN_CIDR}'"
fi
if ! uci -q get "zt-gateway.global.bridge_device" >/dev/null 2>&1; then
uci -q set "zt-gateway.global.bridge_device='${BRIDGE}'"
fi
if ! uci -q get "zt-gateway.global.bridge_ports" >/dev/null 2>&1; then
uci -q set "zt-gateway.global.bridge_ports='${BRIDGE_PORTS}'"
fi
if ! uci -q get "zt-gateway.global.wiblan_gw" >/dev/null 2>&1; then
uci -q set "zt-gateway.global.wiblan_gw='${WIBLAN_GW}'"
fi
if ! uci -q get "zt-gateway.global.table_main" >/dev/null 2>&1; then
uci -q set "zt-gateway.global.table_main='${TABLE_MAIN}'"
fi
if ! uci -q get "zt-gateway.global.table_drain" >/dev/null 2>&1; then
uci -q set "zt-gateway.global.table_drain='${TABLE_DRAIN}'"
fi
if ! uci -q get "zt-gateway.global.dhcp_lease_first" >/dev/null 2>&1; then
uci -q set "zt-gateway.global.dhcp_lease_first='${WIBLAN_LEASE_FIRST}'"
fi
if ! uci -q get "zt-gateway.global.dhcp_lease_last" >/dev/null 2>&1; then
uci -q set "zt-gateway.global.dhcp_lease_last='${WIBLAN_LEASE_LAST}'"
fi
uci commit zt-gateway
log "UCI global config updated with setup parameters"
fi
log "persistence setup complete"
}
# ----------------------------------------------------------------------------
# setup-all
# ----------------------------------------------------------------------------
cmd_setup_all() {
log "running full gateway setup..."
cmd_setup_bridge
cmd_setup_routing
cmd_setup_dhcp
cmd_setup_hotplug
cmd_setup_persistence
log "========================================="
log "Full gateway setup complete!"
log " Bridge: ${BRIDGE}"
log " Ports: ${BRIDGE_PORTS}"
log " Subnet: ${WIBLAN_CIDR}"
log " Gateway IP: ${WIBLAN_GW}"
log " Tables: ${TABLE_MAIN} (main), ${TABLE_DRAIN} (drain)"
log " DHCP: ${WIBLAN_LEASE_FIRST} - ${WIBLAN_LEASE_LAST}"
log "========================================="
}
# ----------------------------------------------------------------------------
# Usage
# ----------------------------------------------------------------------------
usage() {
cat >&2 <<'USAGE'
Usage: zt-gateway-setup <command>
Commands:
status Show current setup state
setup-bridge Create the br-zt bridge interface
setup-routing Configure policy routing (tables 100/101, ip rules)
setup-dhcp Configure DHCP for WIBLAN on br-zt
setup-hotplug Create hotplug script to re-apply routes on ifup
setup-persistence Write rc.local + UCI network routes
setup-all Run all setup-* commands in order
USAGE
}
# ----------------------------------------------------------------------------
# Entry point
# ----------------------------------------------------------------------------
main() {
if [ $# -lt 1 ]; then
usage
die 1 "missing command"
fi
cmd=$1
shift
case "$cmd" in
status) cmd_status "$@" ;;
setup-bridge) cmd_setup_bridge "$@" ;;
setup-routing) cmd_setup_routing "$@" ;;
setup-dhcp) cmd_setup_dhcp "$@" ;;
setup-hotplug) cmd_setup_hotplug "$@" ;;
setup-persistence) cmd_setup_persistence "$@" ;;
setup-all) cmd_setup_all "$@" ;;
-h|--help) usage; exit 0 ;;
*) usage; die 1 "unknown command: $cmd" ;;
esac
}
main "$@"

View File

@@ -4,7 +4,7 @@
"read": {
"uci": ["zt-gateway", "network"],
"ubus": {
"zt-gateway": ["status", "health", "drain_status"]
"zt-gateway": ["status", "health", "drain_status", "setup"]
},
"file": {
"/etc/hotplug.d/net/99-zerotier-bridge": ["read"],
@@ -14,7 +14,7 @@
"write": {
"uci": ["zt-gateway", "network"],
"ubus": {
"zt-gateway": ["switch", "cancel_drain"]
"zt-gateway": ["switch", "cancel_drain", "setup"]
},
"file": {
"/etc/hotplug.d/net/99-zerotier-bridge": ["write"],

View File

@@ -181,10 +181,6 @@ return {
return { success: false, message: `Unknown gateway region: ${region}` };
}
const current_region = read_active_region();
if (current_region === region && !drain_active()) {
return { success: true, message: `Already on ${region}.` };
}
const result = run_switch_script_capture(gw.ip, mode);
if (result.code !== 0) {
@@ -297,6 +293,39 @@ return {
: (result.stderr || `Cancel failed with code ${result.code}`)
};
}
},
setup: {
args: {
command: 'string'
},
call: function(req) {
const command = req.args?.command;
if (!command || !match(command, /^(status|setup-bridge|setup-routing|setup-dhcp|setup-hotplug|setup-persistence|setup-all)$/)) {
return {
success: false,
message: 'Invalid command. Valid: status, setup-bridge, setup-routing, setup-dhcp, setup-hotplug, setup-persistence, setup-all'
};
}
const stderr_pipe = '/tmp/zt-gw-setup-stderr';
const code = system(`/usr/sbin/zt-gateway-setup ${shell_quote(command)} 2>${stderr_pipe}`);
const err = fs.readfile(stderr_pipe) ?? '';
fs.unlink(stderr_pipe);
if (code !== 0) {
return {
success: false,
message: trim(err) || `Setup failed with exit code ${code}`
};
}
return {
success: true,
message: trim(err) || `${command} completed successfully`,
command: command
};
}
}
}
};