From cd684930479c8ace4636bdbe03d3200e0eb6a07f Mon Sep 17 00:00:00 2001 From: Parahub AI Date: Thu, 5 Feb 2026 11:24:38 +0000 Subject: [PATCH] feat: uci-defaults zero-touch mesh node auto-configuration First-boot script that configures a Parahub mesh node with zero user interaction: batman-adv BATMAN_V mesh, dual-band WiFi (private SAE + public open), firewall zones with guest isolation, SQM 128kbps shaping, MAC-derived subnets for collision avoidance, and key generation. Co-Authored-By: Claude Opus 4.5 --- files/etc/uci-defaults/99-parahub-mesh | 386 +++++++++++++++++++++++++ scripts/build.sh | 138 +++++++++ 2 files changed, 524 insertions(+) create mode 100755 files/etc/uci-defaults/99-parahub-mesh create mode 100755 scripts/build.sh diff --git a/files/etc/uci-defaults/99-parahub-mesh b/files/etc/uci-defaults/99-parahub-mesh new file mode 100755 index 0000000..75f47f9 --- /dev/null +++ b/files/etc/uci-defaults/99-parahub-mesh @@ -0,0 +1,386 @@ +#!/bin/sh +# 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. + +set -e + +# ============================================================================ +# 1. IDENTITY GENERATION +# ============================================================================ + +# Get br-lan MAC (fallback to eth0 if br-lan doesn't exist yet) +BASE_MAC=$(cat /sys/class/net/br-lan/address 2>/dev/null || cat /sys/class/net/eth0/address 2>/dev/null || echo "00:00:00:00:00:00") + +# Last 4 hex digits of MAC for unique suffix +NODE_SUFFIX=$(echo "$BASE_MAC" | awk -F: '{print toupper($5$6)}') + +HOSTNAME="Parahub-${NODE_SUFFIX}" +PRIVATE_SSID="Parahub_${NODE_SUFFIX}" +MESH_ID="parahub-mesh" +PUBLIC_SSID="Parahub_Free" + +# ============================================================================ +# 2. SUBNET GENERATION (collision avoidance from MAC octets) +# ============================================================================ + +# Private: octets 4,5 → 10.P1.P2.0/24 +PRIV_O1=$(echo "$BASE_MAC" | awk -F: '{printf "%d", "0x"$4}') +PRIV_O2=$(echo "$BASE_MAC" | awk -F: '{printf "%d", "0x"$5}') + +# Guest: octets 2,3 → 10.G1.G2.0/24 (guaranteed different from private since different MAC positions) +GUEST_O1=$(echo "$BASE_MAC" | awk -F: '{printf "%d", "0x"$2}') +GUEST_O2=$(echo "$BASE_MAC" | awk -F: '{printf "%d", "0x"$3}') + +# Avoid 0 and 255 in second octet (reserved) +[ "$PRIV_O1" -eq 0 ] && PRIV_O1=1 +[ "$PRIV_O1" -eq 255 ] && PRIV_O1=254 +[ "$GUEST_O1" -eq 0 ] && GUEST_O1=2 +[ "$GUEST_O1" -eq 255 ] && GUEST_O1=253 + +PRIV_SUBNET="10.${PRIV_O1}.${PRIV_O2}.0" +PRIV_IP="10.${PRIV_O1}.${PRIV_O2}.1" +GUEST_SUBNET="10.${GUEST_O1}.${GUEST_O2}.0" +GUEST_IP="10.${GUEST_O1}.${GUEST_O2}.1" + +# ============================================================================ +# 3. KEY GENERATION +# ============================================================================ + +PRIVATE_KEY=$(head -c 64 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 12) +MESH_KEY=$(head -c 64 /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 16) + +mkdir -p /etc/parahub +cat > /etc/parahub/keys </dev/null || echo "") + htmode=$(uci -q get "wireless.${radio}.htmode" 2>/dev/null || echo "") + + if [ "$band" = "2g" ]; then + RADIO_2G="$radio" + elif [ "$band" = "5g" ]; then + RADIO_5G="$radio" + else + # Fallback: detect from htmode or channel + channel=$(uci -q get "wireless.${radio}.channel" 2>/dev/null || echo "0") + if [ "$channel" -le 14 ] 2>/dev/null; then + RADIO_2G="$radio" + else + RADIO_5G="$radio" + fi + fi +done + +# Fallback: if only one radio detected, use it for both +if [ -z "$RADIO_2G" ] && [ -n "$RADIO_5G" ]; then + RADIO_2G="$RADIO_5G" +fi +if [ -z "$RADIO_5G" ] && [ -n "$RADIO_2G" ]; then + RADIO_5G="$RADIO_2G" +fi + +# Remove all existing wifi-iface sections +while uci -q delete wireless.@wifi-iface[0]; do :; done + +# Enable radios (remove disabled flag) +for radio in $RADIOS; do + uci -q delete "wireless.${radio}.disabled" 2>/dev/null || true +done + +# --- 2.4GHz radio: mesh backhaul + Parahub_Free AP --- +if [ -n "$RADIO_2G" ]; then + # Mesh interface on 2.4GHz + uci batch <<-WIFI_2G_MESH +set wireless.mesh_2g=wifi-iface +set wireless.mesh_2g.device='${RADIO_2G}' +set wireless.mesh_2g.mode='mesh' +set wireless.mesh_2g.mesh_id='${MESH_ID}' +set wireless.mesh_2g.mesh_fwding='0' +set wireless.mesh_2g.encryption='sae' +set wireless.mesh_2g.key='${MESH_KEY}' +set wireless.mesh_2g.network='bat0_hardif_mesh0' +WIFI_2G_MESH + + # Public AP on 2.4GHz (better range) + uci batch <<-WIFI_2G_PUB +set wireless.public_2g=wifi-iface +set wireless.public_2g.device='${RADIO_2G}' +set wireless.public_2g.mode='ap' +set wireless.public_2g.ssid='${PUBLIC_SSID}' +set wireless.public_2g.encryption='none' +set wireless.public_2g.isolate='1' +set wireless.public_2g.network='guest' +WIFI_2G_PUB +fi + +# --- 5GHz radio: mesh backhaul + Private AP --- +if [ -n "$RADIO_5G" ]; then + # Mesh interface on 5GHz + uci batch <<-WIFI_5G_MESH +set wireless.mesh_5g=wifi-iface +set wireless.mesh_5g.device='${RADIO_5G}' +set wireless.mesh_5g.mode='mesh' +set wireless.mesh_5g.mesh_id='${MESH_ID}' +set wireless.mesh_5g.mesh_fwding='0' +set wireless.mesh_5g.encryption='sae' +set wireless.mesh_5g.key='${MESH_KEY}' +set wireless.mesh_5g.network='bat0_hardif_mesh1' +WIFI_5G_MESH + + # Private AP on 5GHz (better throughput) + uci batch <<-WIFI_5G_PRIV +set wireless.private_5g=wifi-iface +set wireless.private_5g.device='${RADIO_5G}' +set wireless.private_5g.mode='ap' +set wireless.private_5g.ssid='${PRIVATE_SSID}' +set wireless.private_5g.encryption='sae' +set wireless.private_5g.key='${PRIVATE_KEY}' +set wireless.private_5g.network='private' +WIFI_5G_PRIV +fi + +uci commit wireless + +# ============================================================================ +# 6. FIREWALL CONFIGURATION +# ============================================================================ + +# Reset firewall to clean state +while uci -q delete firewall.@zone[0]; do :; done +while uci -q delete firewall.@forwarding[0]; do :; done +while uci -q delete firewall.@rule[0]; do :; done + +uci batch <<-FW_EOF +# --- Zone: lan (private network) --- +add firewall zone +set firewall.@zone[-1].name='lan' +set firewall.@zone[-1].input='ACCEPT' +set firewall.@zone[-1].output='ACCEPT' +set firewall.@zone[-1].forward='ACCEPT' +add_list firewall.@zone[-1].network='private' + +# --- Zone: guest --- +add firewall zone +set firewall.@zone[-1].name='guest' +set firewall.@zone[-1].input='REJECT' +set firewall.@zone[-1].output='ACCEPT' +set firewall.@zone[-1].forward='REJECT' +add_list firewall.@zone[-1].network='guest' + +# --- Zone: wan --- +add firewall zone +set firewall.@zone[-1].name='wan' +set firewall.@zone[-1].input='REJECT' +set firewall.@zone[-1].output='ACCEPT' +set firewall.@zone[-1].forward='REJECT' +set firewall.@zone[-1].masq='1' +set firewall.@zone[-1].mtu_fix='1' +add_list firewall.@zone[-1].network='wan' + +# --- Forwarding: lan → wan (internet for owner) --- +add firewall forwarding +set firewall.@forwarding[-1].src='lan' +set firewall.@forwarding[-1].dest='wan' + +# --- Forwarding: guest → wan (internet for guests) --- +add firewall forwarding +set firewall.@forwarding[-1].src='guest' +set firewall.@forwarding[-1].dest='wan' + +# --- Rule: guest DHCP (allow guests to get IP) --- +add firewall rule +set firewall.@rule[-1].name='Guest DHCP' +set firewall.@rule[-1].src='guest' +set firewall.@rule[-1].proto='udp' +set firewall.@rule[-1].dest_port='67' +set firewall.@rule[-1].target='ACCEPT' + +# --- Rule: guest DNS (allow guests to resolve) --- +add firewall rule +set firewall.@rule[-1].name='Guest DNS' +set firewall.@rule[-1].src='guest' +set firewall.@rule[-1].proto='tcpudp' +set firewall.@rule[-1].dest_port='53' +set firewall.@rule[-1].target='ACCEPT' + +# --- Rule: block guest → lan (isolation) --- +add firewall rule +set firewall.@rule[-1].name='Block guest to LAN' +set firewall.@rule[-1].src='guest' +set firewall.@rule[-1].dest='lan' +set firewall.@rule[-1].proto='all' +set firewall.@rule[-1].target='REJECT' + +# --- Standard WAN input rules --- +add firewall rule +set firewall.@rule[-1].name='Allow-DHCP-Renew' +set firewall.@rule[-1].src='wan' +set firewall.@rule[-1].proto='udp' +set firewall.@rule[-1].dest_port='68' +set firewall.@rule[-1].target='ACCEPT' +set firewall.@rule[-1].family='ipv4' + +add firewall rule +set firewall.@rule[-1].name='Allow-Ping' +set firewall.@rule[-1].src='wan' +set firewall.@rule[-1].proto='icmp' +set firewall.@rule[-1].icmp_type='echo-request' +set firewall.@rule[-1].target='ACCEPT' +FW_EOF +uci commit firewall + +# ============================================================================ +# 7. DHCP CONFIGURATION +# ============================================================================ + +# Remove existing DHCP pools that may conflict +uci -q delete dhcp.lan 2>/dev/null || true +uci -q delete dhcp.guest 2>/dev/null || true + +uci batch <<-DHCP_EOF +# --- Private DHCP --- +set dhcp.private=dhcp +set dhcp.private.interface='private' +set dhcp.private.start='100' +set dhcp.private.limit='150' +set dhcp.private.leasetime='12h' + +# --- Guest DHCP --- +set dhcp.guest=dhcp +set dhcp.guest.interface='guest' +set dhcp.guest.start='100' +set dhcp.guest.limit='50' +set dhcp.guest.leasetime='1h' +DHCP_EOF +uci commit dhcp + +# ============================================================================ +# 8. SQM TRAFFIC SHAPING (guest 128 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 +set sqm.guest=queue +set sqm.guest.enabled='1' +set sqm.guest.interface='br-guest' +set sqm.guest.download='128' +set sqm.guest.upload='128' +set sqm.guest.qdisc='cake' +set sqm.guest.script='piece_of_cake.qos' +set sqm.guest.linklayer='ethernet' +set sqm.guest.overhead='44' +SQM_EOF +uci commit sqm + +# ============================================================================ +# 9. SYSTEM SETTINGS +# ============================================================================ + +uci batch <<-SYS_EOF +set system.@system[0].hostname='${HOSTNAME}' +set system.@system[0].timezone='UTC0' +set system.@system[0].zonename='UTC' +set system.@system[0].log_proto='udp' +set system.@system[0].conloglevel='7' +set system.@system[0].cronloglevel='9' +SYS_EOF +uci commit system + +# ============================================================================ +# 10. FINAL +# ============================================================================ + +# Log completion +logger -t parahub-mesh "First boot configuration complete" +logger -t parahub-mesh "Hostname: ${HOSTNAME}" +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}" + +exit 0 diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..b273b9f --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,138 @@ +#!/bin/bash +# Parahub Mesh Firmware Builder +# Uses OpenWrt Image Builder to create custom firmware with mesh packages. +# +# Usage: ./scripts/build.sh +# Example: ./scripts/build.sh glinet_gl-axt1800 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +# ============================================================================ +# Configuration +# ============================================================================ + +OPENWRT_VERSION="${OPENWRT_VERSION:-23.05.5}" +OPENWRT_TARGET="${OPENWRT_TARGET:-mediatek/filogic}" +BUILDER_URL="https://downloads.openwrt.org/releases/${OPENWRT_VERSION}/targets/${OPENWRT_TARGET}/openwrt-imagebuilder-${OPENWRT_VERSION}-${OPENWRT_TARGET//\//-}.Linux-x86_64.tar.xz" +BUILDER_DIR="${PROJECT_DIR}/imagebuilder" + +# Profile mapping (friendly name → Image Builder profile) +declare -A PROFILES=( + ["axt1800"]="glinet_gl-axt1800" + ["mt3000"]="glinet_gl-mt3000" + ["ax6s"]="xiaomi_redmi-router-ax6s" +) + +# ============================================================================ +# Packages +# ============================================================================ + +# Core mesh packages (always included) +PACKAGES_CORE=( + # batman-adv mesh + kmod-batman-adv + batctl-full + + # 802.11s mesh support + wpad-mesh-mbedtls + -wpad-basic-mbedtls + + # SQM traffic shaping + sqm-scripts + kmod-sched-cake + + # Utilities + luci + luci-app-sqm +) + +# Full package set +PACKAGES_FULL=( + "${PACKAGES_CORE[@]}" + + # Diagnostics + tcpdump + iperf3 + iwinfo + curl +) + +# ============================================================================ +# Functions +# ============================================================================ + +usage() { + echo "Usage: $0 " + echo "" + echo "Profiles:" + for key in "${!PROFILES[@]}"; do + echo " ${key} → ${PROFILES[$key]}" + done + echo "" + echo "Environment variables:" + echo " OPENWRT_VERSION OpenWrt release (default: ${OPENWRT_VERSION})" + echo " OPENWRT_TARGET Target platform (default: ${OPENWRT_TARGET})" + echo " PACKAGES_EXTRA Additional packages (space-separated)" + exit 1 +} + +download_builder() { + if [ -d "$BUILDER_DIR" ]; then + echo "Image Builder already downloaded, skipping..." + return + fi + + echo "Downloading OpenWrt Image Builder ${OPENWRT_VERSION}..." + mkdir -p "$BUILDER_DIR" + wget -q --show-progress -O- "$BUILDER_URL" | tar -xJ --strip-components=1 -C "$BUILDER_DIR" +} + +build_firmware() { + local profile="$1" + local packages="${PACKAGES_FULL[*]} ${PACKAGES_EXTRA:-}" + + echo "Building firmware for profile: ${profile}" + echo "Packages: ${packages}" + echo "Custom files: ${PROJECT_DIR}/files" + + make -C "$BUILDER_DIR" image \ + PROFILE="$profile" \ + PACKAGES="$packages" \ + FILES="${PROJECT_DIR}/files" \ + BIN_DIR="${PROJECT_DIR}/output" + + echo "" + echo "Build complete! Firmware images:" + ls -lh "${PROJECT_DIR}/output/"*.bin 2>/dev/null || echo "(no .bin files found)" + ls -lh "${PROJECT_DIR}/output/"*.img* 2>/dev/null || echo "(no .img files found)" +} + +# ============================================================================ +# Main +# ============================================================================ + +if [ $# -lt 1 ]; then + usage +fi + +INPUT_PROFILE="$1" + +# Resolve profile name +if [ -n "${PROFILES[$INPUT_PROFILE]+x}" ]; then + PROFILE="${PROFILES[$INPUT_PROFILE]}" +else + # Assume it's a raw Image Builder profile name + PROFILE="$INPUT_PROFILE" +fi + +echo "=== Parahub Mesh Firmware Builder ===" +echo "OpenWrt: ${OPENWRT_VERSION}" +echo "Target: ${OPENWRT_TARGET}" +echo "Profile: ${PROFILE}" +echo "" + +download_builder +build_firmware "$PROFILE"