From 383569de92efb6e93b4f95370c688ece7369909f Mon Sep 17 00:00:00 2001 From: Parahub AI Date: Thu, 5 Feb 2026 20:21:06 +0000 Subject: [PATCH] feat: Split firmware into Bee (L2 transport) and Bumblebee (L3 gateway) roles Bee (wr3000, ar300m16): minimal batman-adv mesh relay with gw_mode=client, no yggdrasil/GRE6/VPN/SQM/DoH, Parahub_Free bridged to private network. Bumblebee (axt1800, mt3000, mt6000, ax6s, ax53u): full stack with gw_mode=server, yggdrasil overlay, GRE6 tunnel, guest isolation, SQM, DoH. Build creates /etc/parahub/role marker; heartbeat reports firmware_role and mesh_ip; Bee uses public URL, Bumblebee tries yggdrasil with fallback. Co-Authored-By: Claude Opus 4.6 --- files/etc/uci-defaults/99-parahub-mesh | 243 ++++++++++++++++++++----- files/usr/bin/parahub-heartbeat | 46 ++++- scripts/build.sh | 86 +++++---- 3 files changed, 286 insertions(+), 89 deletions(-) diff --git a/files/etc/uci-defaults/99-parahub-mesh b/files/etc/uci-defaults/99-parahub-mesh index f0ed257..fab5d99 100755 --- a/files/etc/uci-defaults/99-parahub-mesh +++ b/files/etc/uci-defaults/99-parahub-mesh @@ -2,9 +2,16 @@ # Parahub Mesh Node — Zero-Touch First Boot Configuration # This script runs once on first boot via uci-defaults mechanism. # It configures: batman-adv mesh, dual-band WiFi (private+public), firewall zones, SQM shaping. +# +# Roles: +# bumblebee (L3 Gateway) — full stack: yggdrasil, GRE6, VPN, guest isolation, SQM, DoH +# bee (L2 Transport) — minimal: batman-adv mesh relay, heartbeat set -e +# Read firmware role (written by build.sh) +ROLE=$(cat /etc/parahub/role 2>/dev/null || echo "bumblebee") + # ============================================================================ # 1. IDENTITY GENERATION # ============================================================================ @@ -55,6 +62,7 @@ mkdir -p /etc/parahub cat > /etc/parahub/keys </dev/null || true uci -q delete dhcp.guest 2>/dev/null || true -uci batch <<-DHCP_EOF +if [ "$ROLE" = "bee" ]; then + # Bee: private DHCP only (no guest pool) + uci batch <<-DHCP_EOF +set dhcp.private=dhcp +set dhcp.private.interface='private' +set dhcp.private.start='100' +set dhcp.private.limit='150' +set dhcp.private.leasetime='12h' +DHCP_EOF +else + # Bumblebee: private + guest DHCP + uci batch <<-DHCP_EOF # --- Private DHCP --- set dhcp.private=dhcp set dhcp.private.interface='private' @@ -388,16 +520,16 @@ set dhcp.guest.start='100' set dhcp.guest.limit='50' set dhcp.guest.leasetime='1h' DHCP_EOF +fi + uci commit dhcp # ============================================================================ -# 8. SQM TRAFFIC SHAPING (guest 512 kbps limit) +# 8. SQM TRAFFIC SHAPING (Bumblebee only — guest 512 kbps limit) # ============================================================================ -# Find the guest interface device name (will be set after network restart) -# SQM watches the interface name from the network config - -uci batch <<-SQM_EOF +if [ "$ROLE" != "bee" ]; then + uci batch <<-SQM_EOF set sqm.guest=queue set sqm.guest.enabled='1' set sqm.guest.interface='br-guest' @@ -408,15 +540,15 @@ set sqm.guest.script='piece_of_cake.qos' set sqm.guest.linklayer='ethernet' set sqm.guest.overhead='44' SQM_EOF -uci commit sqm + uci commit sqm +fi # ============================================================================ -# 9. DNS-OVER-HTTPS (guest DNS privacy) +# 9. DNS-OVER-HTTPS (Bumblebee only — guest DNS privacy) # ============================================================================ -# https-dns-proxy: local DoH resolver for guest DNS queries -# Guest DNS is hijacked via firewall redirect to this resolver -uci batch <<-DOH_EOF +if [ "$ROLE" != "bee" ]; then + uci batch <<-DOH_EOF set https-dns-proxy.main=main set https-dns-proxy.main.doh_paramters='-4' set https-dns-proxy.main.listen_addr='0.0.0.0' @@ -427,16 +559,17 @@ set https-dns-proxy.cloudflare.resolver_url='https://1.1.1.1/dns-query' set https-dns-proxy.cloudflare.listen_addr='127.0.0.1' set https-dns-proxy.cloudflare.listen_port='5053' DOH_EOF -uci commit https-dns-proxy + uci commit https-dns-proxy -# Configure dnsmasq to use DoH for upstream -uci -q delete dhcp.@dnsmasq[0].server 2>/dev/null || true -uci add_list dhcp.@dnsmasq[0].server='127.0.0.1#5053' -uci set dhcp.@dnsmasq[0].noresolv='1' -uci commit dhcp + # Configure dnsmasq to use DoH for upstream + uci -q delete dhcp.@dnsmasq[0].server 2>/dev/null || true + uci add_list dhcp.@dnsmasq[0].server='127.0.0.1#5053' + uci set dhcp.@dnsmasq[0].noresolv='1' + uci commit dhcp -# Enable https-dns-proxy service -/etc/init.d/https-dns-proxy enable 2>/dev/null || true + # Enable https-dns-proxy service + /etc/init.d/https-dns-proxy enable 2>/dev/null || true +fi # ============================================================================ # 10. SYSTEM SETTINGS @@ -453,33 +586,37 @@ SYS_EOF uci commit system # ============================================================================ -# 11. YGGDRASIL OVERLAY NETWORK +# 11. YGGDRASIL OVERLAY NETWORK (Bumblebee only) # ============================================================================ -# Generate unique keys for this node -yggdrasil -genconf | sed 's/IfName: .*/IfName: ygg0/' > /etc/yggdrasil.conf +if [ "$ROLE" != "bee" ]; then + # Generate unique keys for this node + yggdrasil -genconf | sed 's/IfName: .*/IfName: ygg0/' > /etc/yggdrasil.conf -# Add VPS gateway as static Yggdrasil peer (for GRE6 tunnel) -sed -i 's|Peers: \[\]|Peers: ["tls://91.98.123.238:443"]|' /etc/yggdrasil.conf + # Add VPS gateway as static Yggdrasil peer (for GRE6 tunnel) + sed -i 's|Peers: \[\]|Peers: ["tls://91.98.123.238:443"]|' /etc/yggdrasil.conf -# UCI network interface for yggdrasil TUN -uci batch <<-YGG_EOF + # UCI network interface for yggdrasil TUN + uci batch <<-YGG_EOF set network.yggdrasil=interface set network.yggdrasil.device='ygg0' set network.yggdrasil.proto='none' YGG_EOF -uci commit network + uci commit network -# Add yggdrasil to LAN zone (mesh nodes trust each other) -uci add_list firewall.@zone[0].network='yggdrasil' -uci commit firewall + # Add yggdrasil to LAN zone (mesh nodes trust each other) + uci add_list firewall.@zone[0].network='yggdrasil' + uci commit firewall -# Enable yggdrasil service -/etc/init.d/yggdrasil enable + # Enable yggdrasil service + /etc/init.d/yggdrasil enable -# Save yggdrasil address to node keys file -YGG_ADDR=$(yggdrasil -address -useconffile /etc/yggdrasil.conf 2>/dev/null || echo "unknown") -echo "YGGDRASIL_ADDRESS=${YGG_ADDR}" >> /etc/parahub/keys + # Save yggdrasil address to node keys file + YGG_ADDR=$(yggdrasil -address -useconffile /etc/yggdrasil.conf 2>/dev/null || echo "unknown") + echo "YGGDRASIL_ADDRESS=${YGG_ADDR}" >> /etc/parahub/keys +else + YGG_ADDR="none" +fi # ============================================================================ # 12. HEARTBEAT (phone-home to Parahub cloud) @@ -496,13 +633,21 @@ echo "*/5 * * * * /usr/bin/parahub-heartbeat" >> /etc/crontabs/root # ============================================================================ # Log completion -logger -t parahub-mesh "First boot configuration complete" +logger -t parahub-mesh "First boot configuration complete (role: ${ROLE})" logger -t parahub-mesh "Hostname: ${HOSTNAME}" +logger -t parahub-mesh "Role: ${ROLE}" logger -t parahub-mesh "Private: ${PRIVATE_SSID} @ ${PRIV_IP}/24" -logger -t parahub-mesh "Guest: ${PUBLIC_SSID} @ ${GUEST_IP}/24" -logger -t parahub-mesh "Mesh ID: ${MESH_ID}" -logger -t parahub-mesh "Yggdrasil: ${YGG_ADDR}" -logger -t parahub-mesh "GRE tunnel: 172.16.0.2 → VPS gateway (Mullvad Portugal)" -logger -t parahub-mesh "Kill switch: guest→vpn_tunnel only (no wan)" + +if [ "$ROLE" = "bee" ]; then + logger -t parahub-mesh "Parahub_Free: bridged to private (no guest isolation)" + logger -t parahub-mesh "bat0 gw_mode: client (uses Bumblebee as gateway)" +else + logger -t parahub-mesh "Guest: ${PUBLIC_SSID} @ ${GUEST_IP}/24" + logger -t parahub-mesh "Mesh ID: ${MESH_ID}" + logger -t parahub-mesh "Yggdrasil: ${YGG_ADDR}" + logger -t parahub-mesh "GRE tunnel: 172.16.0.2 → VPS gateway (Mullvad Portugal)" + logger -t parahub-mesh "Kill switch: guest→vpn_tunnel only (no wan)" + logger -t parahub-mesh "bat0 gw_mode: server (gateway for Bee nodes)" +fi exit 0 diff --git a/files/usr/bin/parahub-heartbeat b/files/usr/bin/parahub-heartbeat index f06dfd8..b61c2c2 100755 --- a/files/usr/bin/parahub-heartbeat +++ b/files/usr/bin/parahub-heartbeat @@ -1,10 +1,11 @@ #!/bin/sh -# Phone-home heartbeat to Parahub cloud via Yggdrasil -PARAHUB_API="http://[200:abb9:5810:37d3:8a4c:98a6:b82b:969a]/api/v1/iot/mesh/heartbeat" +# Phone-home heartbeat to Parahub cloud +PARAHUB_API_YGG="http://[200:abb9:5810:37d3:8a4c:98a6:b82b:969a]/api/v1/iot/mesh/heartbeat" +PARAHUB_API_PUBLIC="https://parahub.io/api/v1/iot/mesh/heartbeat" HEARTBEAT_KEY="IqPosrcpTQKSbLHxOG3iLTl_K2MsDxvd" -# Check Yggdrasil connectivity first -ping6 -c 1 -W 3 200:abb9:5810:37d3:8a4c:98a6:b82b:969a >/dev/null 2>&1 || exit 0 +# Read firmware role +ROLE=$(cat /etc/parahub/role 2>/dev/null || echo "unknown") # Read identity from /etc/parahub/keys . /etc/parahub/keys 2>/dev/null @@ -14,18 +15,45 @@ YGG_ADDR="${YGGDRASIL_ADDRESS:-unknown}" SSID="${PRIVATE_SSID:-unknown}" UPTIME="$(cut -d. -f1 /proc/uptime)" +# Get mesh IP (br-private address) +MESH_IP=$(ip -4 addr show br-private 2>/dev/null | grep -o 'inet [0-9.]*' | cut -d' ' -f2) +MESH_IP="${MESH_IP:-unknown}" + # Detect hardware from board_name HW=$(cat /tmp/sysinfo/board_name 2>/dev/null) case "$HW" in glinet,gl-axt1800) HW="axt1800" ;; glinet,gl-mt3000) HW="mt3000" ;; glinet,gl-mt6000) HW="mt6000" ;; + xiaomi,redmi-router-ax6s) HW="ax6s" ;; asus,rt-ax53u) HW="ax53u" ;; + glinet,gl-ar300m16) HW="ar300m16" ;; + cudy,wr3000-v1) HW="wr3000" ;; *) HW="${HW:-unknown}" ;; esac -curl -s -m 10 -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer ${HEARTBEAT_KEY}" \ - -d "{\"mac\":\"${MAC}\",\"hostname\":\"${HOSTNAME}\",\"yggdrasil_address\":\"${YGG_ADDR}\",\"firmware_version\":\"25.12.0-rc4\",\"hardware_profile\":\"${HW}\",\"uptime\":${UPTIME},\"private_ssid\":\"${SSID}\"}" \ - "${PARAHUB_API}" >/dev/null 2>&1 +PAYLOAD="{\"mac\":\"${MAC}\",\"hostname\":\"${HOSTNAME}\",\"yggdrasil_address\":\"${YGG_ADDR}\",\"firmware_version\":\"25.12.0-rc4\",\"hardware_profile\":\"${HW}\",\"uptime\":${UPTIME},\"private_ssid\":\"${SSID}\",\"firmware_role\":\"${ROLE}\",\"mesh_ip\":\"${MESH_IP}\"}" + +if [ "$ROLE" = "bee" ]; then + # Bee: no yggdrasil, use public URL only + curl -s -m 10 -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${HEARTBEAT_KEY}" \ + -d "$PAYLOAD" \ + "${PARAHUB_API_PUBLIC}" >/dev/null 2>&1 +else + # Bumblebee: try yggdrasil first, fallback to public + if ping6 -c 1 -W 3 200:abb9:5810:37d3:8a4c:98a6:b82b:969a >/dev/null 2>&1; then + curl -s -m 10 -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${HEARTBEAT_KEY}" \ + -d "$PAYLOAD" \ + "${PARAHUB_API_YGG}" >/dev/null 2>&1 + else + curl -s -m 10 -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${HEARTBEAT_KEY}" \ + -d "$PAYLOAD" \ + "${PARAHUB_API_PUBLIC}" >/dev/null 2>&1 + fi +fi diff --git a/scripts/build.sh b/scripts/build.sh index 7d182d6..5121159 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -22,26 +22,37 @@ device_config() { axt1800) OPENWRT_TARGET="qualcommax/ipq60xx" PROFILE="glinet_gl-axt1800" + FIRMWARE_ROLE="bumblebee" ;; mt3000) OPENWRT_TARGET="mediatek/filogic" PROFILE="glinet_gl-mt3000" + FIRMWARE_ROLE="bumblebee" ;; mt6000) OPENWRT_TARGET="mediatek/filogic" PROFILE="glinet_gl-mt6000" + FIRMWARE_ROLE="bumblebee" ;; ax6s) OPENWRT_TARGET="mediatek/filogic" PROFILE="xiaomi_redmi-router-ax6s" + FIRMWARE_ROLE="bumblebee" ;; ax53u) OPENWRT_TARGET="ramips/mt7621" PROFILE="asus_rt-ax53u" + FIRMWARE_ROLE="bumblebee" ;; ar300m16) OPENWRT_TARGET="ath79/generic" PROFILE="glinet_gl-ar300m16" + FIRMWARE_ROLE="bee" + ;; + wr3000) + OPENWRT_TARGET="mediatek/filogic" + PROFILE="cudy_wr3000-v1" + FIRMWARE_ROLE="bee" ;; *) return 1 @@ -53,42 +64,32 @@ device_config() { # Packages # ============================================================================ -PACKAGES_CORE=( - # batman-adv mesh +# Bee (L2 Transport): minimal mesh relay — no overlay, no guest isolation +PACKAGES_BEE=( kmod-batman-adv batctl-full - - # 802.11s mesh support (replace basic wpad, includes OWE) wpad-mesh-mbedtls -wpad-basic-mbedtls + luci + curl +) - # Yggdrasil overlay network +# Bumblebee (L3 Gateway): full stack — overlay, VPN, guest isolation, diagnostics +PACKAGES_BUMBLEBEE=( + kmod-batman-adv + batctl-full + wpad-mesh-mbedtls + -wpad-basic-mbedtls yggdrasil - - # GRE6 tunnel (guest traffic → VPS gateway) kmod-gre6 - - # WireGuard (optional local Mullvad via parahub-mullvad script) kmod-wireguard wireguard-tools luci-proto-wireguard - - # DNS-over-HTTPS for guest privacy https-dns-proxy - - # SQM traffic shaping sqm-scripts kmod-sched-cake - - # Management luci luci-app-sqm -) - -PACKAGES_FULL=( - "${PACKAGES_CORE[@]}" - - # Diagnostics tcpdump iperf3 iwinfo @@ -102,13 +103,18 @@ PACKAGES_FULL=( usage() { echo "Usage: $0 " echo "" - echo "Devices:" - echo " axt1800 GL.iNet GL-AXT1800 (Slate AX) qualcommax/ipq60xx" - echo " mt3000 GL.iNet GL-MT3000 (Beryl AX) mediatek/filogic" - echo " mt6000 GL.iNet GL-MT6000 (Flint 2) mediatek/filogic" - echo " ax6s Xiaomi Redmi AX6S mediatek/filogic" - echo " ax53u Asus RT-AX53U ramips/mt7621" - echo " ar300m16 GL.iNet GL-AR300M16-EXT (16MB) ath79/generic" + echo "Devices: Role" + echo " axt1800 GL.iNet GL-AXT1800 (Slate AX) qualcommax/ipq60xx Bumblebee" + echo " mt3000 GL.iNet GL-MT3000 (Beryl AX) mediatek/filogic Bumblebee" + echo " mt6000 GL.iNet GL-MT6000 (Flint 2) mediatek/filogic Bumblebee" + echo " ax6s Xiaomi Redmi AX6S mediatek/filogic Bumblebee" + echo " ax53u Asus RT-AX53U ramips/mt7621 Bumblebee" + echo " ar300m16 GL.iNet GL-AR300M16-EXT (16MB) ath79/generic Bee" + echo " wr3000 Cudy AX3000 (WR3000) mediatek/filogic Bee" + echo "" + echo "Roles:" + echo " Bumblebee L3 Gateway — full stack (yggdrasil, VPN, guest isolation, SQM, DoH)" + echo " Bee L2 Transport — minimal mesh relay (batman-adv, luci, heartbeat)" echo "" echo "OpenWrt version: ${OPENWRT_VERSION} (override with OPENWRT_VERSION env var)" echo "" @@ -146,18 +152,35 @@ download_builder() { build_firmware() { local dir dir="$(builder_dir)" - local packages="${PACKAGES_FULL[*]} ${PACKAGES_EXTRA:-}" + + # Select package list by role + local packages + if [ "$FIRMWARE_ROLE" = "bee" ]; then + packages="${PACKAGES_BEE[*]} ${PACKAGES_EXTRA:-}" + else + packages="${PACKAGES_BUMBLEBEE[*]} ${PACKAGES_EXTRA:-}" + fi + + # Create temp FILES dir with role marker + local tmpfiles + tmpfiles=$(mktemp -d) + cp -a "${PROJECT_DIR}/files/"* "$tmpfiles/" + mkdir -p "$tmpfiles/etc/parahub" + echo "$FIRMWARE_ROLE" > "$tmpfiles/etc/parahub/role" echo "Building firmware for profile: ${PROFILE}" + echo "Role: ${FIRMWARE_ROLE}" echo "Packages: ${packages}" - echo "Custom files: ${PROJECT_DIR}/files" + echo "Custom files: ${tmpfiles}" make -C "$dir" image \ PROFILE="$PROFILE" \ PACKAGES="$packages" \ - FILES="${PROJECT_DIR}/files" \ + FILES="$tmpfiles" \ BIN_DIR="${PROJECT_DIR}/output" + rm -rf "$tmpfiles" + echo "" echo "Build complete! Firmware images:" ls -lh "${PROJECT_DIR}/output/"*.bin 2>/dev/null || true @@ -183,6 +206,7 @@ fi echo "=== Parahub Mesh Firmware Builder ===" echo "Device: ${INPUT}" +echo "Role: ${FIRMWARE_ROLE}" echo "OpenWrt: ${OPENWRT_VERSION}" echo "Target: ${OPENWRT_TARGET}" echo "Profile: ${PROFILE}"