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:
@@ -26,6 +26,7 @@ services:
|
|||||||
- ./root/usr/share/ucode/luci/runtime.uc:/usr/share/ucode/luci/runtime.uc:ro
|
- ./root/usr/share/ucode/luci/runtime.uc:/usr/share/ucode/luci/runtime.uc:ro
|
||||||
- ./root/usr/share/luci/menu.d/luci-app-zt-gateway.json:/usr/share/luci/menu.d/luci-app-zt-gateway.json:ro
|
- ./root/usr/share/luci/menu.d/luci-app-zt-gateway.json:/usr/share/luci/menu.d/luci-app-zt-gateway.json:ro
|
||||||
- ./root/usr/share/rpcd/acl.d/luci-app-zt-gateway.json:/usr/share/rpcd/acl.d/luci-app-zt-gateway.json:ro
|
- ./root/usr/share/rpcd/acl.d/luci-app-zt-gateway.json:/usr/share/rpcd/acl.d/luci-app-zt-gateway.json:ro
|
||||||
|
- ./root/usr/sbin/zt-gateway-setup:/usr/sbin/zt-gateway-setup:ro
|
||||||
# Stage host config so the entrypoint can copy it into the writable overlay.
|
# Stage host config so the entrypoint can copy it into the writable overlay.
|
||||||
- ./root/etc/config:/host-config:ro
|
- ./root/etc/config:/host-config:ro
|
||||||
- ./htdocs/luci-static/resources/view/zt-gateway/overview.js:/www/luci-static/resources/view/zt-gateway/overview.js:ro
|
- ./htdocs/luci-static/resources/view/zt-gateway/overview.js:/www/luci-static/resources/view/zt-gateway/overview.js:ro
|
||||||
|
|||||||
@@ -29,15 +29,64 @@ test.describe('drain UI', () => {
|
|||||||
// Change mode to graceful
|
// Change mode to graceful
|
||||||
await page.locator('.zt-mode-select').selectOption('graceful');
|
await page.locator('.zt-mode-select').selectOption('graceful');
|
||||||
|
|
||||||
|
// Stub status responses to report an active drain with remaining connections.
|
||||||
|
// In Docker there are no real conntrack entries, so the drain completes
|
||||||
|
// instantly and the panel never renders without this stub.
|
||||||
|
//
|
||||||
|
// LuCI sends ubus calls as JSON-RPC batches: [{method:"call", params:[..., "status", {}]}]
|
||||||
|
// and receives: [{result: [0, {gateways:[], drain:{...}}]}]
|
||||||
|
await page.route(/\/ubus/, async (route) => {
|
||||||
|
const postData = route.request().postData() || '';
|
||||||
|
if (!postData.includes('"status"')) {
|
||||||
|
return route.fallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fabricate a status response with an active drain state.
|
||||||
|
// We avoid route.fetch() which can hang — instead return a canned
|
||||||
|
// response matching the batch format LuCI expects.
|
||||||
|
const gateways = [
|
||||||
|
{ region: 'amsterdam', label: 'Amsterdam (ocirosea641)', ip: '10.99.12.3', default: true, health_check: 'ping' },
|
||||||
|
{ region: 'tirunelveli', label: 'Tirunelveli (rpi1000)', ip: '10.99.12.5', default: false, health_check: 'ping' },
|
||||||
|
{ region: 'bangalore', label: 'Bangalore (sensecap-m4)', ip: '10.99.12.4', default: false, health_check: 'ping' }
|
||||||
|
];
|
||||||
|
const body = [{
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1,
|
||||||
|
result: [0, {
|
||||||
|
active_gateway: 'amsterdam',
|
||||||
|
active_ip: '10.99.12.3',
|
||||||
|
settings: { switch_mode: 'force', drain_timeout: 600, health_interval: 60, auto_failback: false },
|
||||||
|
gateways,
|
||||||
|
drain: {
|
||||||
|
active: true,
|
||||||
|
new_ip: '10.99.12.5',
|
||||||
|
old_ip: '10.99.12.3',
|
||||||
|
remaining_connections: 42
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}];
|
||||||
|
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
// Click "Switch to selected"
|
// Click "Switch to selected"
|
||||||
await page.locator('.cbi-button-positive').click();
|
await page.locator('.cbi-button-positive').click();
|
||||||
|
|
||||||
// Wait for the drain panel to appear (may be brief if conntrack is unavailable)
|
// The next status poll (triggered by onSwitchClick's finally block)
|
||||||
|
// will return the stubbed drain state, causing the panel to render.
|
||||||
const drainPanel = page.locator('.zt-drain-panel');
|
const drainPanel = page.locator('.zt-drain-panel');
|
||||||
await expect(drainPanel).toBeVisible({ timeout: 45_000 });
|
await expect(drainPanel).toBeVisible({ timeout: 15_000 });
|
||||||
await expect(drainPanel).toContainText('Graceful drain progress');
|
await expect(drainPanel).toContainText('Graceful drain progress');
|
||||||
|
|
||||||
// Panel references old and new IP addresses
|
// Panel references old and new IP addresses
|
||||||
await expect(drainPanel).toContainText(/\d+\.\d+\.\d+\.\d+/);
|
await expect(drainPanel).toContainText(/\d+\.\d+\.\d+\.\d+/);
|
||||||
|
} finally {
|
||||||
|
await page.unroute(/\/ubus/);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ test('displays gateway table with correct rows', async ({ page }) => {
|
|||||||
await expect(activeRow).toHaveCount(1);
|
await expect(activeRow).toHaveCount(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('active gateway radio is disabled', async ({ page }) => {
|
test('active gateway radio is enabled', async ({ page }) => {
|
||||||
const activeRow = page.locator('.zt-gateway-row', { hasText: 'active' });
|
const activeRow = page.locator('.zt-gateway-row', { hasText: 'active' });
|
||||||
const radio = activeRow.locator('input[type=radio]');
|
const radio = activeRow.locator('input[type=radio]');
|
||||||
await expect(radio).toBeDisabled();
|
await expect(radio).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('non-active gateway radios are enabled', async ({ page }) => {
|
test('non-active gateway radios are enabled', async ({ page }) => {
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ function ubusCancelDrain() {
|
|||||||
params: []
|
params: []
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
function ubusSetup(command) {
|
||||||
|
return rpc.declare({
|
||||||
|
object: UBUS_NAMESPACE,
|
||||||
|
method: 'setup',
|
||||||
|
params: ['command']
|
||||||
|
})(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return view.extend({
|
return view.extend({
|
||||||
state: {
|
state: {
|
||||||
@@ -78,7 +86,6 @@ return view.extend({
|
|||||||
value: gw.region,
|
value: gw.region,
|
||||||
change: ev => this.onSelectRegion(ev.target.value),
|
change: ev => this.onSelectRegion(ev.target.value),
|
||||||
checked: isSelected || null,
|
checked: isSelected || null,
|
||||||
disabled: isActive || null
|
|
||||||
})),
|
})),
|
||||||
E('td', { class: 'zt-region-cell' }, [
|
E('td', { class: 'zt-region-cell' }, [
|
||||||
this.renderHealthDot(false, null),
|
this.renderHealthDot(false, null),
|
||||||
@@ -125,6 +132,53 @@ return view.extend({
|
|||||||
}, _('Cancel drain (force switch)'))
|
}, _('Cancel drain (force switch)'))
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
renderSetupPanel() {
|
||||||
|
const commands = [
|
||||||
|
{ 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-dhcp', label: _('Setup DHCP'), desc: _('Configure DHCP for WIBLAN subnet') },
|
||||||
|
{ 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-all', label: _('Run Full Setup'), desc: _('Configure everything at once') }
|
||||||
|
];
|
||||||
|
|
||||||
|
const btns = commands.map(c =>
|
||||||
|
E('button', {
|
||||||
|
class: 'cbi-button',
|
||||||
|
'data-cmd': c.cmd,
|
||||||
|
title: c.desc,
|
||||||
|
click: ev => this.onSetupCommand(ev, c.cmd)
|
||||||
|
}, c.label)
|
||||||
|
);
|
||||||
|
|
||||||
|
return E('div', { class: 'zt-setup-panel' }, [
|
||||||
|
E('h4', {}, _('Gateway Setup')),
|
||||||
|
E('p', {}, _('Configure this router as a ZeroTier exit gateway. Run individual steps or "Run Full Setup" to configure everything.')),
|
||||||
|
E('div', { class: 'zt-setup-buttons' }, btns)
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
async onSetupCommand(ev, command) {
|
||||||
|
const btn = ev.target;
|
||||||
|
btn.disabled = true;
|
||||||
|
const oldLabel = btn.textContent;
|
||||||
|
btn.textContent = _('Running...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await ubusSetup(command);
|
||||||
|
if (res && res.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, '%s: %s'.format(command, res.message || _('Done.'))));
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, '%s: %s'.format(command, (res && res.message) || _('Failed.'))));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('RPC error: ') + (err.message || err)));
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = oldLabel;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
renderActiveSummary(data) {
|
renderActiveSummary(data) {
|
||||||
const region = data.active_gateway || _('none');
|
const region = data.active_gateway || _('none');
|
||||||
@@ -275,7 +329,8 @@ return view.extend({
|
|||||||
}, _('Switch to selected'))
|
}, _('Switch to selected'))
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
this.renderDrainPanel(data)
|
this.renderDrainPanel(data),
|
||||||
|
this.renderSetupPanel()
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -305,7 +360,12 @@ return view.extend({
|
|||||||
'.zt-gateways tbody tr{line-height:1.8}' +
|
'.zt-gateways tbody tr{line-height:1.8}' +
|
||||||
'.zt-mode-group{display:flex;gap:0.5em;align-items:center;flex-wrap:wrap}' +
|
'.zt-mode-group{display:flex;gap:0.5em;align-items:center;flex-wrap:wrap}' +
|
||||||
'.zt-active-summary{margin:0.5em 0 1em 0}' +
|
'.zt-active-summary{margin:0.5em 0 1em 0}' +
|
||||||
'.zt-drain-panel{margin-top:1em;padding:0.75em;background:#f8f8f8;border:1px solid #ddd}';
|
'.zt-drain-panel{margin-top:1em;padding:0.75em;background:#f8f8f8;border:1px solid #ddd}' +
|
||||||
|
'.zt-setup-panel{margin-top:1.5em;padding:0.75em;background:#f0f7ff;border:1px solid #b8d4e8}' +
|
||||||
|
'.zt-setup-buttons{display:flex;gap:0.5em;flex-wrap:wrap;margin-top:0.5em}' +
|
||||||
|
'.zt-setup-buttons .cbi-button{min-width:140px}' +
|
||||||
|
'.zt-setup-panel h4{margin-top:0}' +
|
||||||
|
'.zt-setup-panel p{margin:0.5em 0;color:#555}';
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.id = 'zt-gateway-css';
|
style.id = 'zt-gateway-css';
|
||||||
style.textContent = css;
|
style.textContent = css;
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ echo "==> Uploading files..."
|
|||||||
|
|
||||||
# root/ files → strip leading root/, map to / on device
|
# root/ files → strip leading root/, map to / on device
|
||||||
scp -O -q root/usr/sbin/zt-gateway-switch "$HOST:/usr/sbin/"
|
scp -O -q root/usr/sbin/zt-gateway-switch "$HOST:/usr/sbin/"
|
||||||
|
scp -O -q root/usr/sbin/zt-gateway-setup "$HOST:/usr/sbin/"
|
||||||
|
echo " zt-gateway-setup"
|
||||||
echo " zt-gateway-switch"
|
echo " zt-gateway-switch"
|
||||||
scp -O -q root/usr/share/rpcd/ucode/zt-gateway.uc "$HOST:/usr/share/rpcd/ucode/"
|
scp -O -q root/usr/share/rpcd/ucode/zt-gateway.uc "$HOST:/usr/share/rpcd/ucode/"
|
||||||
echo " rpcd/ucode/zt-gateway.uc"
|
echo " rpcd/ucode/zt-gateway.uc"
|
||||||
@@ -109,8 +111,7 @@ scp -O -q htdocs/luci-static/resources/view/zt-gateway/overview.js "$HOST:/www/
|
|||||||
echo " overview.js"
|
echo " overview.js"
|
||||||
|
|
||||||
echo "==> Setting permissions..."
|
echo "==> Setting permissions..."
|
||||||
ssh -q "$HOST" chmod +x /usr/sbin/zt-gateway-switch
|
ssh -q "$HOST" chmod +x /usr/sbin/zt-gateway-switch /usr/sbin/zt-gateway-setup
|
||||||
|
|
||||||
echo "==> Restarting rpcd and uhttpd..."
|
echo "==> Restarting rpcd and uhttpd..."
|
||||||
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
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ config global 'global'
|
|||||||
option drain_timeout '600'
|
option drain_timeout '600'
|
||||||
option health_interval '60'
|
option health_interval '60'
|
||||||
option auto_failback '0'
|
option auto_failback '0'
|
||||||
|
option wiblan_subnet '10.11.13.0/24'
|
||||||
|
option bridge_device 'br-zt'
|
||||||
|
option bridge_ports 'ztabc0'
|
||||||
|
option wiblan_gw '10.11.13.1'
|
||||||
|
option table_main '100'
|
||||||
|
option table_drain '101'
|
||||||
|
option dhcp_lease_first '10.11.13.100'
|
||||||
|
option dhcp_lease_last '10.11.13.200'
|
||||||
|
|
||||||
config gateway
|
config gateway
|
||||||
option region 'amsterdam'
|
option region 'amsterdam'
|
||||||
|
|||||||
557
root/usr/sbin/zt-gateway-setup
Executable file
557
root/usr/sbin/zt-gateway-setup
Executable 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 "$@"
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"read": {
|
"read": {
|
||||||
"uci": ["zt-gateway", "network"],
|
"uci": ["zt-gateway", "network"],
|
||||||
"ubus": {
|
"ubus": {
|
||||||
"zt-gateway": ["status", "health", "drain_status"]
|
"zt-gateway": ["status", "health", "drain_status", "setup"]
|
||||||
},
|
},
|
||||||
"file": {
|
"file": {
|
||||||
"/etc/hotplug.d/net/99-zerotier-bridge": ["read"],
|
"/etc/hotplug.d/net/99-zerotier-bridge": ["read"],
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"write": {
|
"write": {
|
||||||
"uci": ["zt-gateway", "network"],
|
"uci": ["zt-gateway", "network"],
|
||||||
"ubus": {
|
"ubus": {
|
||||||
"zt-gateway": ["switch", "cancel_drain"]
|
"zt-gateway": ["switch", "cancel_drain", "setup"]
|
||||||
},
|
},
|
||||||
"file": {
|
"file": {
|
||||||
"/etc/hotplug.d/net/99-zerotier-bridge": ["write"],
|
"/etc/hotplug.d/net/99-zerotier-bridge": ["write"],
|
||||||
|
|||||||
@@ -181,10 +181,6 @@ return {
|
|||||||
return { success: false, message: `Unknown gateway region: ${region}` };
|
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);
|
const result = run_switch_script_capture(gw.ip, mode);
|
||||||
if (result.code !== 0) {
|
if (result.code !== 0) {
|
||||||
@@ -297,6 +293,39 @@ return {
|
|||||||
: (result.stderr || `Cancel failed with code ${result.code}`)
|
: (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
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user