#!/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 : 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 }" 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