feat: kit-busrouter v2.0 — complete Synology + GL fleet router platform
Includes: - package/ GL-XE3000 kit-busrouter (opkg) - scripts/provision.sh (GL) and provision-synology.sh (Synology) - syno-balance/ — new WAN balancer replacing aiwanbal (SmartWAN adapter) - kit-connect/ — unified connectivity SPK (Tailscale + reverse SSH) - docs/deployment/synology-rt2600ac-checklist.md — 62-point checklist - docs/provisioning/device-identity.md — fleet identity spec - docs/pilot/checklist.md — field pilot validation - x4078_20260721.dss — reference config backup Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Executable
+343
@@ -0,0 +1,343 @@
|
||||
#!/bin/sh
|
||||
# provision.sh — Bootstrap a GL-XE3000 Pioneer Bus Router
|
||||
#
|
||||
# Run once from router LAN (192.168.8.1) or console on a fresh/factory unit.
|
||||
#
|
||||
# Usage:
|
||||
# provision.sh <DEVICE_ID> <WG_PRIVKEY> <WG_TUNNEL_IP>
|
||||
#
|
||||
# DEVICE_ID : bus identifier, e.g. B0042
|
||||
# Becomes the WiFi SSID on both bands (plan spec "X0000" means
|
||||
# SSID = the device ID, not a literal string "X0000").
|
||||
# WG_PRIVKEY : base64 WireGuard private key for this unit (wg genkey)
|
||||
# WG_TUNNEL_IP : assigned tunnel IP, e.g. 10.88.0.42 (no DHCP; hub admin assigns)
|
||||
#
|
||||
# Prerequisites:
|
||||
# - Cellular SIM installed and rmnet_mhi0 online (for opkg)
|
||||
# - kit-busrouter .ipk at /tmp/kit-busrouter.ipk (or set BUSROUTER_IPK env)
|
||||
# - WG keypair generated; public key registered at hub before running this script
|
||||
# - Edit HUB_PUBKEY constant below before deploying
|
||||
#
|
||||
# Idempotent: safe to re-run after partial failure.
|
||||
|
||||
set -e
|
||||
|
||||
DEVICE_ID="${1:?Usage: provision.sh <DEVICE_ID> <WG_PRIVKEY> <WG_TUNNEL_IP>}"
|
||||
WG_PRIVKEY="${2:?Missing WG_PRIVKEY}"
|
||||
WG_TUNNEL_IP="${3:?Missing WG_TUNNEL_IP}"
|
||||
|
||||
BUSROUTER_IPK="${BUSROUTER_IPK:-/tmp/kit-busrouter.ipk}"
|
||||
|
||||
# ── Fleet-wide constants (edit before deploying) ──────────────────────────────
|
||||
HUB_PUBKEY="4QAauZ9gS/wZ/u0Wf66OcJFGo4erng3V5mwpYpARlmc="
|
||||
HUB_HOST="167.172.237.162"
|
||||
HUB_PORT="51820"
|
||||
WG_ALLOWED="10.88.0.0/24"
|
||||
WIFI_PASS="Pioneer123"
|
||||
ADMIN_PASS="Pioneer321!"
|
||||
TIMEZONE="UTC"
|
||||
TIMEZONE_NAME="UTC"
|
||||
|
||||
# Guard: catch forgotten placeholder before any changes are made
|
||||
case "$HUB_PUBKEY" in REPLACE_*)
|
||||
echo "[provision] ERROR: edit HUB_PUBKEY in provision.sh before running" >&2
|
||||
exit 1
|
||||
esac
|
||||
|
||||
echo "[provision] === Pioneer Bus Router Provisioning ==="
|
||||
echo "[provision] DEVICE_ID : $DEVICE_ID"
|
||||
echo "[provision] WG_TUNNEL_IP: $WG_TUNNEL_IP"
|
||||
|
||||
# ── 1. Identity files ─────────────────────────────────────────────────────────
|
||||
echo "[provision] 1/12 Device identity"
|
||||
mkdir -p /etc/busrouter
|
||||
printf '%s\n' "$DEVICE_ID" > /etc/busrouter/device-id
|
||||
[ -f /etc/busrouter/version ] || printf '0.0\n' > /etc/busrouter/version
|
||||
|
||||
# ── 2. Hostname + timezone ────────────────────────────────────────────────────
|
||||
echo "[provision] 2/12 Hostname + timezone"
|
||||
uci set system.@system[0].hostname="$DEVICE_ID"
|
||||
uci set system.@system[0].timezone="$TIMEZONE"
|
||||
uci set system.@system[0].zonename="$TIMEZONE_NAME"
|
||||
uci commit system
|
||||
|
||||
# ── 3. Admin accounts ─────────────────────────────────────────────────────────
|
||||
# GL-XE3000 SSH/WebUI authenticates as root. pioadmin is a uid=0 alias so that
|
||||
# "ssh pioadmin@..." works alongside "ssh root@..." with the same password.
|
||||
echo "[provision] 3/12 Admin accounts (root + pioadmin uid=0 alias)"
|
||||
printf '%s\n%s\n' "$ADMIN_PASS" "$ADMIN_PASS" | passwd root 2>/dev/null || \
|
||||
echo "root:${ADMIN_PASS}" | chpasswd 2>/dev/null || \
|
||||
echo "[provision] WARNING: failed to set root password"
|
||||
|
||||
if ! grep -q '^pioadmin:' /etc/passwd 2>/dev/null; then
|
||||
echo "pioadmin:x:0:0:Fleet Admin:/root:/bin/ash" >> /etc/passwd
|
||||
fi
|
||||
# BusyBox shadow needs 9 colon-separated fields
|
||||
if ! grep -q '^pioadmin:' /etc/shadow 2>/dev/null; then
|
||||
echo "pioadmin:!:0:0:99999:7:::" >> /etc/shadow
|
||||
fi
|
||||
printf '%s\n%s\n' "$ADMIN_PASS" "$ADMIN_PASS" | passwd pioadmin 2>/dev/null || \
|
||||
echo "pioadmin:${ADMIN_PASS}" | chpasswd 2>/dev/null || \
|
||||
echo "[provision] WARNING: failed to set pioadmin password"
|
||||
|
||||
# ── 4. WiFi SSID (2.4 GHz + 5 GHz) ──────────────────────────────────────────
|
||||
echo "[provision] 4/12 SSID=${DEVICE_ID} / Pioneer123"
|
||||
ifaces=$(uci show wireless 2>/dev/null | grep '=wifi-iface' | cut -d. -f2 | cut -d= -f1)
|
||||
if [ -z "$ifaces" ]; then
|
||||
echo "[provision] WARNING: no wifi-iface sections found — check 'uci show wireless'"
|
||||
else
|
||||
for iface in $ifaces; do
|
||||
uci set "wireless.${iface}.ssid=${DEVICE_ID}"
|
||||
uci set "wireless.${iface}.key=${WIFI_PASS}"
|
||||
uci set "wireless.${iface}.encryption=psk2"
|
||||
done
|
||||
uci commit wireless
|
||||
wifi reload 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# ── 5. Stand down GL's kmwan ─────────────────────────────────────────────────
|
||||
echo "[provision] 5/13 Disabling GL kmwan"
|
||||
/etc/init.d/kmwan disable 2>/dev/null || true
|
||||
/etc/init.d/kmwan stop 2>/dev/null || true
|
||||
|
||||
# ── 5b. Disable GL route_policy VPN kill switch ───────────────────────────────
|
||||
# GL firmware treats the WireGuard client as a "Primary Tunnel" and enables a
|
||||
# kill switch that marks ALL LAN traffic for WG routing. Since our WG only
|
||||
# carries management traffic (AllowedIPs=10.88.0.0/24), this kills internet
|
||||
# for LAN clients. Disable the rule so GL does not interfere with mwan3.
|
||||
echo "[provision] 5b/13 Disabling GL route_policy kill switch"
|
||||
# The anonymous @rule[0] is the "Primary Tunnel" rule (wgclient1, killswitch=1)
|
||||
uci set route_policy.@rule[0].killswitch='0' 2>/dev/null || true
|
||||
uci set route_policy.@rule[0].enabled='0' 2>/dev/null || true
|
||||
uci commit route_policy 2>/dev/null || true
|
||||
|
||||
# ── 6. Install packages ───────────────────────────────────────────────────────
|
||||
echo "[provision] 6/12 opkg update + mwan3"
|
||||
opkg update
|
||||
opkg install mwan3
|
||||
if [ -f "$BUSROUTER_IPK" ]; then
|
||||
echo "[provision] Installing kit-busrouter"
|
||||
opkg install --force-reinstall "$BUSROUTER_IPK"
|
||||
else
|
||||
echo "[provision] WARNING: $BUSROUTER_IPK not found — install kit-busrouter manually"
|
||||
fi
|
||||
|
||||
# ── 7. wwan interface: enable DHCP default route for repeater ─────────────────
|
||||
# GL sets classlessroute=0 by default, which prevents the DHCP-obtained gateway
|
||||
# from being installed in the main routing table. Without a default via apcli0,
|
||||
# the repeater cannot forward internet traffic even when connected.
|
||||
echo "[provision] 7/13 wwan classlessroute fix (enable repeater gateway)"
|
||||
uci set network.wwan.classlessroute=1
|
||||
uci commit network
|
||||
|
||||
# ── 8. mwan3 base config ──────────────────────────────────────────────────────
|
||||
# Priority: wwan (repeater, metric 1) > modem_0001 (5G cellular, metric 2) > wan (Starlink, metric 3)
|
||||
# last_resort='default' — if all mwan3 members are offline, fall back to whatever
|
||||
# the main routing table has rather than returning unreachable to all clients.
|
||||
echo "[provision] 8/13 mwan3 (priority: wwan(1) > modem_0001(2) > wan(3))"
|
||||
uci batch << 'MWAN3_EOF'
|
||||
delete mwan3
|
||||
set mwan3.globals=globals
|
||||
set mwan3.globals.mmx_mask='0x3F00'
|
||||
set mwan3.globals.logging='0'
|
||||
set mwan3.wwan=interface
|
||||
set mwan3.wwan.enabled='1'
|
||||
set mwan3.wwan.initial_state='online'
|
||||
set mwan3.wwan.family='ipv4'
|
||||
set mwan3.wwan.track_ip='1.1.1.1'
|
||||
add_list mwan3.wwan.track_ip='8.8.8.8'
|
||||
set mwan3.wwan.track_method='ping'
|
||||
set mwan3.wwan.reliability='1'
|
||||
set mwan3.wwan.count='1'
|
||||
set mwan3.wwan.down='3'
|
||||
set mwan3.wwan.up='3'
|
||||
set mwan3.wwan.interval='5'
|
||||
set mwan3.modem_0001=interface
|
||||
set mwan3.modem_0001.enabled='1'
|
||||
set mwan3.modem_0001.initial_state='online'
|
||||
set mwan3.modem_0001.family='ipv4'
|
||||
set mwan3.modem_0001.track_ip='1.1.1.1'
|
||||
add_list mwan3.modem_0001.track_ip='8.8.8.8'
|
||||
set mwan3.modem_0001.track_method='ping'
|
||||
set mwan3.modem_0001.reliability='1'
|
||||
set mwan3.modem_0001.count='1'
|
||||
set mwan3.modem_0001.down='3'
|
||||
set mwan3.modem_0001.up='3'
|
||||
set mwan3.modem_0001.interval='5'
|
||||
set mwan3.wan=interface
|
||||
set mwan3.wan.enabled='1'
|
||||
set mwan3.wan.initial_state='online'
|
||||
set mwan3.wan.family='ipv4'
|
||||
set mwan3.wan.track_ip='1.1.1.1'
|
||||
add_list mwan3.wan.track_ip='8.8.8.8'
|
||||
set mwan3.wan.track_method='ping'
|
||||
set mwan3.wan.reliability='1'
|
||||
set mwan3.wan.count='1'
|
||||
set mwan3.wan.down='3'
|
||||
set mwan3.wan.up='3'
|
||||
set mwan3.wan.interval='5'
|
||||
set mwan3.wwan_mbr=member
|
||||
set mwan3.wwan_mbr.interface='wwan'
|
||||
set mwan3.wwan_mbr.metric='1'
|
||||
set mwan3.wwan_mbr.weight='100'
|
||||
set mwan3.cell_mbr=member
|
||||
set mwan3.cell_mbr.interface='modem_0001'
|
||||
set mwan3.cell_mbr.metric='2'
|
||||
set mwan3.cell_mbr.weight='100'
|
||||
set mwan3.wan_mbr=member
|
||||
set mwan3.wan_mbr.interface='wan'
|
||||
set mwan3.wan_mbr.metric='3'
|
||||
set mwan3.wan_mbr.weight='100'
|
||||
set mwan3.balanced=policy
|
||||
set mwan3.balanced.last_resort='default'
|
||||
add_list mwan3.balanced.use_member='wwan_mbr'
|
||||
add_list mwan3.balanced.use_member='cell_mbr'
|
||||
add_list mwan3.balanced.use_member='wan_mbr'
|
||||
set mwan3.default_rule=rule
|
||||
set mwan3.default_rule.dest='0.0.0.0/0'
|
||||
set mwan3.default_rule.proto='all'
|
||||
set mwan3.default_rule.use_policy='balanced'
|
||||
MWAN3_EOF
|
||||
uci commit mwan3
|
||||
/etc/init.d/mwan3 enable
|
||||
/etc/init.d/mwan3 restart 2>/dev/null || true
|
||||
|
||||
# ── 8. WireGuard mgmt tunnel ──────────────────────────────────────────────────
|
||||
# GL-XE3000 uses proto 'wgclient' (GL proprietary). The interface wgclient1 exists
|
||||
# in the factory image and references a peer config section (typically peer_2001).
|
||||
# Private key goes on the INTERFACE; public key + endpoint go on the PEER section.
|
||||
echo "[provision] 9/13 WireGuard (wgclient1)"
|
||||
|
||||
# Ensure interface section exists with correct type
|
||||
uci get network.wgclient1 >/dev/null 2>&1 || uci set network.wgclient1=wgclient
|
||||
uci set network.wgclient1.ip4table='1001'
|
||||
uci set network.wgclient1.mtu='1280'
|
||||
# Private key belongs on the interface (GL wgclient proto reads it here)
|
||||
uci set network.wgclient1.private_key="${WG_PRIVKEY}"
|
||||
uci set network.wgclient1.ip_address="${WG_TUNNEL_IP}/32"
|
||||
|
||||
# Update the peer section (hub endpoint + pubkey)
|
||||
peer_sec=""
|
||||
if uci get network.peer_2001 >/dev/null 2>&1; then
|
||||
peer_sec="peer_2001"
|
||||
else
|
||||
peer_sec=$(uci show network 2>/dev/null | grep '=wgclient_peer' | head -n1 \
|
||||
| cut -d. -f2 | cut -d= -f1)
|
||||
fi
|
||||
if [ -n "$peer_sec" ]; then
|
||||
uci set "network.${peer_sec}.public_key=${HUB_PUBKEY}"
|
||||
uci set "network.${peer_sec}.endpoint_host=${HUB_HOST}"
|
||||
uci set "network.${peer_sec}.endpoint_port=${HUB_PORT}"
|
||||
uci set "network.${peer_sec}.allowed_ips=${WG_ALLOWED}"
|
||||
uci set "network.${peer_sec}.persistent_keepalive=25"
|
||||
else
|
||||
echo "[provision] WARNING: no WG peer section found — configure hub peer manually"
|
||||
fi
|
||||
uci commit network
|
||||
|
||||
# ── 9b. Inbound mgmt SSH over the tunnel ──────────────────────────────────────
|
||||
# Bring up the path the hub/lab uses to reach this unit. Without BOTH of these
|
||||
# the tunnel comes up but SSH silently hits the wgclient1 zone DROP.
|
||||
# See docs/mgmt-tunnel-ssh.md for the full explanation.
|
||||
echo "[provision] 9b/13 Inbound mgmt SSH (firewall rule + hub jump key)"
|
||||
|
||||
# The WG client interface lives in its own firewall zone (defaults input=DROP).
|
||||
# Find the zone bound to wgclient1; fall back to the interface name.
|
||||
WG_ZONE=$(uci show firewall 2>/dev/null | awk -F"[.=]" '
|
||||
/\.name=/ {z=$2; name=$0}
|
||||
/\.network=/ && /wgclient1/ {print z; exit}')
|
||||
[ -n "$WG_ZONE" ] || WG_ZONE="wgclient1"
|
||||
|
||||
if ! uci show firewall 2>/dev/null | grep -q "name='Allow-WG-mgmt-SSH'"; then
|
||||
uci add firewall rule >/dev/null
|
||||
uci set firewall.@rule[-1].name='Allow-WG-mgmt-SSH'
|
||||
uci set firewall.@rule[-1].src="$WG_ZONE"
|
||||
uci set firewall.@rule[-1].proto='tcp'
|
||||
uci set firewall.@rule[-1].dest_port='22'
|
||||
uci set firewall.@rule[-1].family='ipv4'
|
||||
uci set firewall.@rule[-1].src_ip="$WG_ALLOWED"
|
||||
uci set firewall.@rule[-1].target='ACCEPT'
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall restart 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Install the busfleet-hub jump key so the hub can SSH inbound over the tunnel.
|
||||
mkdir -p /etc/dropbear
|
||||
JUMP_KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMS842ZbXPXHbVd9IjyVGubq7M/yQA/cn/vHgB5R0/ZE busfleet-hub-jump'
|
||||
if ! grep -qF 'busfleet-hub-jump' /etc/dropbear/authorized_keys 2>/dev/null; then
|
||||
printf '%s\n' "$JUMP_KEY" >> /etc/dropbear/authorized_keys
|
||||
fi
|
||||
chmod 600 /etc/dropbear/authorized_keys
|
||||
|
||||
# ── 9. ip rule: WG mgmt tunnel survives mwan3 ─────────────────────────────────
|
||||
# See device-identity.md §4 for root-cause explanation.
|
||||
echo "[provision] 10/13 WG mgmt ip rule (priority 99 -> table 1001)"
|
||||
ip rule del to "$WG_ALLOWED" priority 99 lookup 1001 2>/dev/null || true
|
||||
ip rule add to "$WG_ALLOWED" priority 99 lookup 1001 2>/dev/null || true
|
||||
|
||||
# Persist via UCI (uci show uses key='value' format, so this grep is correct)
|
||||
if ! uci show network 2>/dev/null | grep -q "\.dest='${WG_ALLOWED}'"; then
|
||||
uci add network rule
|
||||
uci set network.@rule[-1].lookup='1001'
|
||||
uci set network.@rule[-1].dest="${WG_ALLOWED}"
|
||||
uci set network.@rule[-1].priority='99'
|
||||
uci commit network
|
||||
fi
|
||||
|
||||
# ── 10. rp_filter=2 for wgclient1 ────────────────────────────────────────────
|
||||
echo "[provision] 11/13 rp_filter + hotplug"
|
||||
# GL-XE3000 may use /etc/sysctl.d/ drop-ins; write there for reliability
|
||||
mkdir -p /etc/sysctl.d
|
||||
printf 'net.ipv4.conf.wgclient1.rp_filter=2\n' > /etc/sysctl.d/10-busrouter.conf
|
||||
sysctl -w net.ipv4.conf.wgclient1.rp_filter=2 2>/dev/null || true
|
||||
# Also add to /etc/sysctl.conf if it exists and doesn't already have our setting
|
||||
if [ -f /etc/sysctl.conf ]; then
|
||||
grep -q 'wgclient1.rp_filter' /etc/sysctl.conf || \
|
||||
printf 'net.ipv4.conf.wgclient1.rp_filter=2\n' >> /etc/sysctl.conf
|
||||
fi
|
||||
|
||||
# Inline hotplug: re-assert rp_filter after any WAN state change
|
||||
cat > /etc/hotplug.d/iface/99-busrouter-rpfilter << 'HOTPLUG_EOF'
|
||||
#!/bin/sh
|
||||
[ "$ACTION" = "ifup" ] || exit 0
|
||||
sysctl -w net.ipv4.conf.wgclient1.rp_filter=2 2>/dev/null || true
|
||||
HOTPLUG_EOF
|
||||
chmod +x /etc/hotplug.d/iface/99-busrouter-rpfilter
|
||||
|
||||
# ── 11. GNSS enable on boot ───────────────────────────────────────────────────
|
||||
echo "[provision] 12/13 GNSS boot enable"
|
||||
if ! grep -q 'AT+QGPS=1' /etc/rc.local 2>/dev/null; then
|
||||
printf '# Enable Quectel GNSS engine on boot (OFF by default)\n' >> /etc/rc.local
|
||||
printf 'sleep 10; gl_modem -B 1-1.2 AT AT+QGPS=1 2>/dev/null\n' >> /etc/rc.local
|
||||
fi
|
||||
|
||||
# ── 12b. Apply GL route_policy change (restart gl-tertf daemon) ──────────────
|
||||
# gl-tertf reads /etc/config/route_policy and installs iptables tunnel marks.
|
||||
# Restart it now so the kill switch disable takes effect before the reboot.
|
||||
/etc/init.d/gl-tertf restart 2>/dev/null || true
|
||||
|
||||
# ── 13. Enable + start busrouter daemon ──────────────────────────────────────
|
||||
echo "[provision] 13/13 busrouter daemon"
|
||||
if [ -f /etc/init.d/busrouter ]; then
|
||||
/etc/init.d/busrouter enable
|
||||
/etc/init.d/busrouter start 2>/dev/null || true
|
||||
else
|
||||
echo "[provision] WARNING: busrouter init not found — install kit-busrouter first"
|
||||
fi
|
||||
|
||||
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[provision] ============================================"
|
||||
echo "[provision] DONE: $DEVICE_ID provisioned."
|
||||
echo " WiFi SSID : $DEVICE_ID (2.4G + 5G) pw: Pioneer123"
|
||||
echo " Admin SSH : ssh root@192.168.8.1 pw: Pioneer321!"
|
||||
echo " ssh pioadmin@192.168.8.1 pw: Pioneer321!"
|
||||
echo " WG tunnel : $WG_TUNNEL_IP"
|
||||
echo " Verify : mwan3 status"
|
||||
echo " cat /etc/busrouter/device-id"
|
||||
echo " ps | grep daemon.sh"
|
||||
echo "[provision] ============================================"
|
||||
echo ""
|
||||
echo "[provision] Rebooting in 5 s ..."
|
||||
sleep 5
|
||||
reboot
|
||||
Reference in New Issue
Block a user