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:
2026-07-22 00:07:14 +00:00
commit f15ac69925
41 changed files with 5083 additions and 0 deletions
+47
View File
@@ -0,0 +1,47 @@
#!/bin/sh
# wg-mgmt-diagnose.sh — READ-ONLY diagnosis of the WG mgmt tunnel on the bench router.
# Run ON THE ROUTER over LAN: ssh root@192.168.8.1 (pw kitPLANE1!!), then: sh wg-mgmt-diagnose.sh
# Safe: only reads state + captures packets. Mutates nothing.
# Context: docs/handoff/2026-07-01-wg-mgmt-incident-and-resume.md
set +e
HUB_WG=10.88.0.1
MGMT_SRC=10.88.0.2
HUB_PUB=167.172.237.162
echo "===== A. FULL WG state (THE missing datapoint — check persistent-keepalive + allowed-ips) ====="
wg show
echo
echo "===== routing / policy (should steer mgmt-sourced traffic into table 1001) ====="
echo "-- ip rule --"; ip rule
echo "-- table 1001 --"; ip route show table 1001
echo "-- default routes --"; ip route show | grep -E '^default'
echo "-- underlay path to hub --"; ip route get "$HUB_PUB"
echo "-- mgmt-sourced return path --"; ip route get "$HUB_WG" from "$MGMT_SRC" 2>/dev/null
echo "-- rp_filter (expect 0; NOT the cause) --"
for f in /proc/sys/net/ipv4/conf/all/rp_filter /proc/sys/net/ipv4/conf/eth0/rp_filter \
/proc/sys/net/ipv4/conf/rmnet_mhi0/rp_filter /proc/sys/net/ipv4/conf/wgclient1/rp_filter; do
[ -f "$f" ] && echo " $f = $(cat "$f")"
done
echo "-- iface MTUs --"
for i in eth0 rmnet_mhi0 wgclient1; do ip link show "$i" 2>/dev/null | sed -n 's/.*\(mtu [0-9]*\).*/ '"$i"': \1/p'; done
echo
echo "===== B. Live capture: watch fiber(eth0) WG + decrypted(wgclient1) while pinging the hub ====="
echo " [eth0] = encrypted WG on the fiber underlay ; [wg] = decrypted inner packets"
( timeout 12 tcpdump -ni eth0 udp port 51820 2>/dev/null | sed 's/^/[eth0] /' & )
( timeout 12 tcpdump -ni wgclient1 2>/dev/null | sed 's/^/[wg] /' & )
sleep 1
ping -c 8 -W 2 -I "$MGMT_SRC" "$HUB_WG"
echo "ping rc=$?"
wait 2>/dev/null
echo
echo "===== INTERPRET ====="
cat <<'EOF'
* wg show 'persistent keepalive: (none)' -> hypothesis #1 (NAT mapping expiry). Fix: keepalive.
* hub-peer 'allowed ips' lacks 10.88.0.1 -> hypothesis #2. Replies get cryptokey-dropped.
* [eth0] shows OUTBOUND + INBOUND :51820 -> hub replies DO arrive on fiber -> decrypt/inner issue.
* [eth0] shows OUTBOUND only -> router pkts not leaving OR hub not returning -> coordinate hub capture.
* [wg] shows echo-request but no echo-reply -> confirms replies not reaching inner iface.
Next: apply scripts/diag/wg-mgmt-fix.sh (keepalive first). See the handoff doc.
EOF
+58
View File
@@ -0,0 +1,58 @@
#!/bin/sh
# wg-mgmt-fix.sh — apply ONE mgmt-tunnel robustness fix at a time, then bounce wgclient1.
# Run ON THE ROUTER over LAN: ssh root@192.168.8.1 (pw kitPLANE1!!)
# sh wg-mgmt-fix.sh keepalive # set persistent_keepalive=25 (TRY FIRST)
# sh wg-mgmt-fix.sh mtu # set wgclient1 mtu=1280 (fixes PMTU black-holes / SSH data)
# sh wg-mgmt-fix.sh rpfilter # persist loose rp_filter (=2) + hotplug re-assert
# sh wg-mgmt-fix.sh all # keepalive + mtu + rpfilter
# MUTATING but reversible (each is a uci change). Bounces the tunnel ~5s; touches nothing else.
# After each: re-test ping -c3 -W2 -I 10.88.0.2 10.88.0.1 (expect replies).
# Rationale + goal (mgmt over EITHER WAN): docs/handoff/2026-07-01-wg-mgmt-incident-and-resume.md
set -e
IFACE=wgclient1
bounce() { echo ">> uci commit network; ifup $IFACE"; uci commit network; ifup "$IFACE"; sleep 5; }
do_keepalive() {
echo "== persistent_keepalive=25 on $IFACE (hold NAT mapping open on whichever WAN) =="
uci set network.$IFACE.persistent_keepalive='25'
bounce
}
do_mtu() {
echo "== mtu=1280 on $IFACE (safe under cellular + ATT-fiber/PPPoE path MTUs) =="
uci set network.$IFACE.mtu='1280'
bounce
}
do_rpfilter() {
echo "== persist loose rp_filter (=2) so multi-WAN policy-routed mgmt isn't dropped =="
grep -q 'busrouter: loose reverse-path' /etc/sysctl.conf 2>/dev/null || cat >> /etc/sysctl.conf <<'EOF'
# busrouter: loose reverse-path filtering for multi-WAN WG mgmt failover
net.ipv4.conf.all.rp_filter=2
net.ipv4.conf.default.rp_filter=2
EOF
# apply now
sysctl -w net.ipv4.conf.all.rp_filter=2 >/dev/null
for f in /proc/sys/net/ipv4/conf/*/rp_filter; do echo 2 > "$f"; done
# re-assert on WAN flaps (GL DHCP hooks may re-tighten per-iface)
mkdir -p /etc/hotplug.d/iface
cat > /etc/hotplug.d/iface/99-busrouter-rpfilter <<'EOF'
#!/bin/sh
[ "$ACTION" = ifup ] || exit 0
for f in /proc/sys/net/ipv4/conf/*/rp_filter; do echo 2 > "$f" 2>/dev/null; done
EOF
chmod +x /etc/hotplug.d/iface/99-busrouter-rpfilter
echo " rp_filter now: all=$(cat /proc/sys/net/ipv4/conf/all/rp_filter)"
}
case "$1" in
keepalive) do_keepalive ;;
mtu) do_mtu ;;
rpfilter) do_rpfilter ;;
all) do_keepalive; do_mtu; do_rpfilter ;;
*) echo "usage: $0 {keepalive|mtu|rpfilter|all}"; exit 2 ;;
esac
echo
echo ">> Now verify: ping -c3 -W2 -I 10.88.0.2 10.88.0.1 (replies = mgmt tunnel healthy)"
+298
View File
@@ -0,0 +1,298 @@
#!/bin/sh
# provision-synology.sh — Bootstrap a Synology RT2600ac Pioneer Bus Router
#
# Run once from router LAN or console on a fresh/factory Synology SRM unit.
#
# Usage:
# provision-synology.sh <DEVICE_ID> [TAILSCALE_AUTH_KEY]
#
# DEVICE_ID : bus identifier, e.g. B0042
# Becomes the WiFi SSID on both bands (plan spec).
# TAILSCALE_AUTH_KEY : Tailscale pre-auth key (tskey-...).
# Omit if Tailscale is already configured.
#
# Prerequisites:
# - SRM 1.3.1+ with SSH enabled (Control Panel → Terminal & SNMP)
# - SD card inserted (for persistent storage)
# - aiwanbal SPK at /tmp/aiwanbal-latest.spk (or set AIWANBAL_SPK env)
# - Tailscale SPK installed (or TAILSCALE_AUTH_KEY provided to configure)
# - Eyeride Eyenet reachable at 192.168.10.1 (if GPS is used)
# - Edit HUB_TAILSCALE_IP constant below before deploying
#
# Idempotent: safe to re-run after partial failure.
set -e
DEVICE_ID="${1:?Usage: provision-synology.sh <DEVICE_ID> [TAILSCALE_AUTH_KEY]}"
TAILSCALE_AUTH_KEY="${2:-}"
AIWANBAL_SPK="${AIWANBAL_SPK:-/tmp/aiwanbal-latest.spk}"
# ── Fleet-wide constants (edit before deploying) ──────────────────────────────────
HUB_TAILSCALE_IP="100.76.128.1" # hub's Tailscale IP
HUB_TELEMETRY_PORT="8080" # telemetry API port on hub
HUB_TELEMETRY_PATH="/api/telemetry" # telemetry endpoint path
WIFI_PASS="Pioneer123"
ADMIN_PASS="Pioneer321!"
TIMEZONE="UTC"
# Guard: catch forgotten placeholder before any changes are made
case "$HUB_TAILSCALE_IP" in
REPLACE_*|CHANGE_ME*|"")
echo "[provision] ERROR: edit HUB_TAILSCALE_IP in provision-synology.sh before running" >&2
exit 1
;;
esac
TELEMETRY_HUB="http://${HUB_TAILSCALE_IP}:${HUB_TELEMETRY_PORT}${HUB_TELEMETRY_PATH}"
echo "[provision] === Pioneer Bus Router Provisioning (Synology SRM) ==="
echo "[provision] DEVICE_ID : $DEVICE_ID"
echo "[provision] HUB_TAILSCALE_IP : $HUB_TAILSCALE_IP"
echo "[provision] TELEMETRY_HUB : $TELEMETRY_HUB"
echo ""
# ── 1. Device identity ───────────────────────────────────────────────────────────
echo "[provision] 1/11 Device identity"
mkdir -p /etc/busrouter
printf '%s\n' "$DEVICE_ID" > /etc/busrouter/device-id
if [ ! -f /etc/busrouter/version ]; then
printf '0.0\n' > /etc/busrouter/version
fi
echo "[provision] device-id: $(cat /etc/busrouter/device-id)"
echo "[provision] version: $(cat /etc/busrouter/version)"
# ── 2. Hostname ──────────────────────────────────────────────────────────────────
# SRM stores hostname in /etc/synoinfo.conf and /etc/hostname
echo "[provision] 2/11 Hostname → ${DEVICE_ID}"
if command -v synosetkeyvalue >/dev/null 2>&1; then
synosetkeyvalue /etc/synoinfo.conf system hostname "$DEVICE_ID" 2>/dev/null || true
fi
hostname "$DEVICE_ID" 2>/dev/null || true
printf '%s\n' "$DEVICE_ID" > /etc/hostname 2>/dev/null || true
# ── 3. Admin accounts ────────────────────────────────────────────────────────────
# Synology SRM: the web admin user is typically "admin" (created at setup).
# SSH uses root (set via SRM Control Panel → Terminal & SNMP).
# We set the root password for fleet SSH access.
echo "[provision] 3/11 Admin account (root password)"
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"
# ── 4. WiFi SSID ─────────────────────────────────────────────────────────────────
# SRM WiFi is managed through the web UI (Wi-Fi Connect app).
# We can configure it via synowifi if available, but the authoritative path
# is the SRM Network Center. Log the expected values and attempt CLI config.
echo "[provision] 4/11 WiFi SSID=${DEVICE_ID} (2.4G + 5G)"
echo "[provision] Password: ${WIFI_PASS}"
echo "[provision] NOTE: If WiFi doesn't change, configure manually in SRM:"
echo "[provision] Wi-Fi Connect → Radio 1 (2.4G) → SSID: ${DEVICE_ID}"
echo "[provision] Wi-Fi Connect → Radio 2 (5G) → SSID: ${DEVICE_ID}"
# Try synowifi if present (may not exist on all SRM versions)
if command -v synowifi >/dev/null 2>&1; then
synowifi --set-ssid "$DEVICE_ID" --set-security wpa2-personal --set-passphrase "$WIFI_PASS" 2>/dev/null || \
echo "[provision] WARNING: synowifi command failed — configure WiFi via SRM web UI"
elif command -v /usr/syno/bin/synowifi >/dev/null 2>&1; then
/usr/syno/bin/synowifi --set-ssid "$DEVICE_ID" --set-security wpa2-personal --set-passphrase "$WIFI_PASS" 2>/dev/null || \
echo "[provision] WARNING: synowifi command failed — configure WiFi via SRM web UI"
fi
# ── 5. Install aiwanbal ──────────────────────────────────────────────────────────
echo "[provision] 5/11 aiwanbal WAN balancer"
if [ -f "$AIWANBAL_SPK" ]; then
echo "[provision] Installing aiwanbal from $AIWANBAL_SPK"
synopkg install "$AIWANBAL_SPK" 2>/dev/null || \
echo "[provision] WARNING: synopkg install failed — install manually"
else
echo "[provision] WARNING: $AIWANBAL_SPK not found"
echo "[provision] Build aiwanbal SPK first, then copy to router:"
echo "[provision] cd /home/node/workspace/aiwanbal && bash build.sh"
echo "[provision] scp -O aiwanbal-*.spk kitadmin@<router>:/tmp/aiwanbal-latest.spk"
fi
# Ensure aiwanbal is running
if synopkg status aiwanbal >/dev/null 2>&1; then
synopkg start aiwanbal 2>/dev/null || true
echo "[provision] aiwanbal started"
else
echo "[provision] WARNING: aiwanbal package not found — install before continuing"
fi
# ── 6. Configure aiwanbal for busrouter (mobile environment) ─────────────────────
echo "[provision] 6/11 aiwanbal busrouter tuning"
AIWANBAL_CONF="/etc/aiwanbal/aiwanbal.conf"
if [ -f "$AIWANBAL_CONF" ]; then
# Set environment to mobile for faster scoring adaptations
if grep -q '^ENVIRONMENT=' "$AIWANBAL_CONF" 2>/dev/null; then
sed -i 's/^ENVIRONMENT=.*/ENVIRONMENT="mobile"/' "$AIWANBAL_CONF"
else
echo 'ENVIRONMENT="mobile"' >> "$AIWANBAL_CONF"
fi
# Set device name for fleet logging
if grep -q '^DEVICE_NAME=' "$AIWANBAL_CONF" 2>/dev/null; then
sed -i "s/^DEVICE_NAME=.*/DEVICE_NAME=\"$DEVICE_ID\"/" "$AIWANBAL_CONF"
else
echo "DEVICE_NAME=\"$DEVICE_ID\"" >> "$AIWANBAL_CONF"
fi
# Metered cellular: disable automatic speed tests to save data
if grep -q '^SPEEDTEST_AUTO=' "$AIWANBAL_CONF" 2>/dev/null; then
sed -i 's/^SPEEDTEST_AUTO=.*/SPEEDTEST_AUTO="0"/' "$AIWANBAL_CONF"
else
echo 'SPEEDTEST_AUTO="0"' >> "$AIWANBAL_CONF"
fi
# Log the config update
logger -t provision "aiwanbal tuned for busrouter: mobile mode, auto-speedtest disabled"
echo "[provision] ENVIRONMENT=mobile, SPEEDTEST_AUTO=0, DEVICE_NAME=${DEVICE_ID}"
else
echo "[provision] WARNING: aiwanbal.conf not found — tune manually"
fi
# ── 7. Tailscale ─────────────────────────────────────────────────────────────────
echo "[provision] 7/11 Tailscale"
if command -v tailscale >/dev/null 2>&1; then
if [ -n "$TAILSCALE_AUTH_KEY" ]; then
echo "[provision] Authenticating with pre-auth key..."
tailscale up --authkey="$TAILSCALE_AUTH_KEY" --hostname="$DEVICE_ID" 2>/dev/null || \
echo "[provision] WARNING: tailscale up failed — check auth key"
else
ts_status=$(tailscale status --json 2>/dev/null || echo '{"BackendState":"NeedsLogin"}')
ts_state=$(echo "$ts_status" | grep -o '"BackendState":"[^"]*"' | cut -d'"' -f4)
if [ "$ts_state" = "Running" ]; then
echo "[provision] Tailscale is running"
else
echo "[provision] WARNING: Tailscale not authenticated"
echo "[provision] Run: tailscale up --hostname=${DEVICE_ID}"
echo "[provision] Or re-run with: provision-synology.sh ${DEVICE_ID} <AUTH_KEY>"
fi
fi
# Show Tailscale IP for fleet records
ts_ip=$(tailscale ip -4 2>/dev/null || echo "unknown")
echo "[provision] Tailscale IP: ${ts_ip}"
else
echo "[provision] WARNING: tailscale not found — install Tailscale SPK first"
echo "[provision] Download from: https://pkgs.tailscale.com/stable/#synology"
fi
# ── 8. Telemetry script + cron ───────────────────────────────────────────────────
echo "[provision] 8/11 Telemetry agent"
# Determine where to install the telemetry script
TELEM_SRC="/usr/lib/busrouter/telemetry-synology.sh"
TELEM_DEST="${TELEM_DEST:-/usr/lib/busrouter/telemetry-synology.sh}"
# Check if this script is bundled with the repo (running from checkout) or
# needs to be copied from a pre-staged location.
if [ -f "$TELEM_SRC" ]; then
echo "[provision] Using telemetry script from ${TELEM_SRC}"
elif [ -f "/tmp/telemetry-synology.sh" ]; then
TELEM_SRC="/tmp/telemetry-synology.sh"
echo "[provision] Using telemetry script from /tmp/"
else
# The script should be copied alongside provision-synology.sh before running
echo "[provision] WARNING: telemetry-synology.sh not found"
echo "[provision] Copy it to /tmp/telemetry-synology.sh and re-run:"
echo "[provision] scp package/kit-busrouter/files/usr/lib/busrouter/telemetry-synology.sh \\"
echo "[provision] kitadmin@<router>:/tmp/"
fi
if [ -f "$TELEM_SRC" ]; then
mkdir -p "$(dirname "$TELEM_DEST")"
cp "$TELEM_SRC" "$TELEM_DEST"
chmod +x "$TELEM_DEST"
# Configure the hub endpoint in the script
sed -i "s|TELEMETRY_HUB:=http://10.88.0.1:8080/api/telemetry|TELEMETRY_HUB:=${TELEMETRY_HUB}|" "$TELEM_DEST"
# Add cron job: run telemetry every 60 seconds
# SRM uses /etc/crontab with user field (standard vixie-cron format)
CRON_JOB="* * * * * root ${TELEM_DEST} 2>/dev/null"
if [ -f /etc/crontab ]; then
if ! grep -qF "$TELEM_DEST" /etc/crontab 2>/dev/null; then
# Sleep offset to spread fleet load: use the last octet of device ID as stagger
_stagger=0
_dev_suffix=$(echo "$DEVICE_ID" | grep -o '[0-9]*$' || echo "0")
_stagger=$(( _dev_suffix % 60 ))
printf '%s\n' "* * * * * root sleep ${_stagger}; ${TELEM_DEST} 2>/dev/null" >> /etc/crontab
echo "[provision] Cron job added (${_stagger}s offset)"
else
echo "[provision] Cron job already present"
fi
else
# Fallback: use crontab -e style
(crontab -l 2>/dev/null || true; echo "* * * * * ${TELEM_DEST} 2>/dev/null") | crontab - 2>/dev/null || \
echo "[provision] WARNING: failed to add cron job — add manually"
fi
echo "[provision] Telemetry installed at ${TELEM_DEST}"
fi
# ── 9. Hub SSH jump key ──────────────────────────────────────────────────────────
echo "[provision] 9/11 Hub SSH jump key"
mkdir -p /root/.ssh
JUMP_KEY='ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMS842ZbXPXHbVd9IjyVGubq7M/yQA/cn/vHgB5R0/ZE busfleet-hub-jump'
if ! grep -qF 'busfleet-hub-jump' /root/.ssh/authorized_keys 2>/dev/null; then
printf '%s\n' "$JUMP_KEY" >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
echo "[provision] Hub jump key installed"
else
echo "[provision] Hub jump key already present"
fi
# ── 10. Eyeride GPS config (optional) ────────────────────────────────────────────
echo "[provision] 10/11 Eyeride GPS"
EYERIDE_IP="${EYERIDE_IP:-192.168.10.1}"
EYERIDE_PORT="${EYERIDE_PORT:-8080}"
echo "[provision] Eyeride target: ${EYERIDE_IP}:${EYERIDE_PORT}"
echo "[provision] Set EYERIDE_PASSWORD env before running telemetry for GPS"
echo "[provision] To configure permanently, add to aiwanbal.conf:"
echo "[provision] EYERIDE_IP=${EYERIDE_IP}"
echo "[provision] EYERIDE_PORT=${EYERIDE_PORT}"
echo "[provision] EYERIDE_PASSWORD=<your-eyenet-password>"
# ── 11. Verify + start services ──────────────────────────────────────────────────
echo "[provision] 11/11 Final checks"
# Ensure aiwanbal is running
if synopkg status aiwanbal >/dev/null 2>&1; then
synopkg start aiwanbal 2>/dev/null || true
echo "[provision] aiwanbal is running"
fi
# Run one telemetry cycle to verify
if [ -x "$TELEM_DEST" ]; then
if "$TELEM_DEST" 2>/dev/null; then
echo "[provision] Telemetry dry-run OK"
else
echo "[provision] Telemetry dry-run completed (check logs for POST result)"
fi
fi
# ── Done ──────────────────────────────────────────────────────────────────────────
echo ""
echo "[provision] ============================================"
echo "[provision] DONE: $DEVICE_ID provisioned (Synology SRM)."
echo ""
echo " Device ID : $DEVICE_ID"
echo " Hostname : $DEVICE_ID"
echo " WiFi SSID : $DEVICE_ID (2.4G + 5G) pw: Pioneer123"
echo " Admin SSH : ssh root@<router-lan-ip> pw: Pioneer321!"
if command -v tailscale >/dev/null 2>&1; then
echo " Tailscale IP : $(tailscale ip -4 2>/dev/null || echo 'check `tailscale status`')"
fi
echo " Telemetry : ${TELEMETRY_HUB} (every 60s via cron)"
echo ""
echo " Verify:"
echo " cat /etc/busrouter/device-id"
echo " synopkg status aiwanbal"
echo " tailscale status"
echo " grep telemetry /etc/crontab"
echo " tail -f /var/log/messages | grep busrouter"
echo "[provision] ============================================"
echo ""
# ── Reboot ───────────────────────────────────────────────────────────────────────
echo "[provision] Rebooting in 5 s ..."
sleep 5
reboot
+343
View File
@@ -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