Files
blackroad/roadnet/roadnet-node-setup.sh
Alexa Amundson 78fbe80f2a Initial monorepo — everything BlackRoad in one place
bin/       230 CLI tools (ask-*, br-*, agent-*, roadid, carpool)
scripts/   99 automation scripts
fleet/     Node configs and deployment
workers/   Cloudflare Worker sources (roadpay, road-search, squad webhooks)
roadc/     RoadC programming language
roadnet/   Mesh network (5 APs, WireGuard)
operator/  Memory system scripts
config/    System configs
dotfiles/  Shell configs
docs/      Documentation

BlackRoad OS — Pave Tomorrow.

RoadChain-SHA2048: d1a24f55318d338b
RoadChain-Identity: alexa@sovereign
RoadChain-Full: d1a24f55318d338b24b60bad7be39286379c76ae5470817482100cb0ddbbcb97e147d07ac7243da0a9f0363e4e5c833d612b9c0df3a3cd20802465420278ef74875a5b77f55af6fe42a931b8b635b3d0d0b6bde9abf33dc42eea52bc03c951406d8cbe49f1a3d29b26a94dade05e9477f34a7d4d4c6ec4005c3c2ac54e73a68440c512c8e83fd9b1fe234750b898ef8f4032c23db173961fe225e67a0432b5293a9714f76c5c57ed5fdf35b9fb40fd73c03ebf88b7253c6a0575f5afb6a6b49b3bda310602fb1ef676859962dad2aebbb2875814b30eee0a8ba195e482d4cbc91d8819e7f38f6db53e8063401649c77bb994371473cabfb917fb53e8cbe73d60
2026-03-14 17:08:41 -05:00

309 lines
11 KiB
Bash
Executable File

#!/bin/bash
# RoadNet Node Setup — runs on each Pi via SSH
# Turns a Raspberry Pi into a RoadNet access point
# Usage: ./roadnet-node-setup.sh --node-id 1 --channel 1 --password <pass> [--bluetooth]
set -e
PINK='\033[38;5;205m'
GREEN='\033[38;5;82m'
AMBER='\033[38;5;214m'
RED='\033[38;5;196m'
RESET='\033[0m'
NODE_ID=""
CHANNEL=""
PASSWORD=""
ENABLE_BT=false
PIHOLE_DNS="192.168.4.49"
SSID="RoadNet"
while [[ $# -gt 0 ]]; do
case $1 in
--node-id) NODE_ID="$2"; shift 2 ;;
--channel) CHANNEL="$2"; shift 2 ;;
--password) PASSWORD="$2"; shift 2 ;;
--bluetooth) ENABLE_BT=true; shift ;;
*) shift ;;
esac
done
if [[ -z "$NODE_ID" || -z "$CHANNEL" || -z "$PASSWORD" ]]; then
echo -e "${RED}Usage: $0 --node-id <1-4> --channel <1|6|11> --password <pass> [--bluetooth]${RESET}"
exit 1
fi
SUBNET="10.10.${NODE_ID}"
AP_IP="${SUBNET}.1"
DHCP_START="${SUBNET}.100"
DHCP_END="${SUBNET}.200"
echo -e "${PINK}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e "${PINK} ROADNET NODE SETUP — Node ${NODE_ID} ($(hostname))${RESET}"
echo -e "${PINK}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
# Safety: verify wlan0 is connected (we won't touch it — using virtual AP uap0)
echo -e "${AMBER}[1/8] Checking connectivity...${RESET}"
if ip link show wlan0 2>/dev/null | grep -q "UP"; then
WLAN_IP=$(ip addr show wlan0 2>/dev/null | grep 'inet ' | awk '{print $2}')
echo -e "${GREEN} ✓ wlan0 is UP (${WLAN_IP}) — will NOT be touched${RESET}"
echo -e "${GREEN} ✓ Using virtual AP (uap0) alongside wlan0${RESET}"
else
echo -e "${RED} ✗ wlan0 not found! Aborting.${RESET}"
exit 1
fi
# Backup existing configs
echo -e "${AMBER}[2/8] Backing up existing configs...${RESET}"
BACKUP_DIR="/etc/roadnet-backup/$(date +%Y%m%d-%H%M%S)"
sudo mkdir -p "$BACKUP_DIR"
for f in /etc/hostapd/hostapd.conf /etc/dnsmasq.conf /etc/dhcpcd.conf /etc/network/interfaces; do
[[ -f "$f" ]] && sudo cp "$f" "$BACKUP_DIR/" 2>/dev/null || true
done
echo -e "${GREEN} ✓ Backed up to ${BACKUP_DIR}${RESET}"
# Install packages
echo -e "${AMBER}[3/8] Installing hostapd + dnsmasq (if needed)...${RESET}"
NEED_INSTALL=false
which hostapd >/dev/null 2>&1 || NEED_INSTALL=true
which dnsmasq >/dev/null 2>&1 || NEED_INSTALL=true
if $NEED_INSTALL; then
sudo apt-get update -qq 2>/dev/null || true
sudo apt-get install -y -qq hostapd dnsmasq 2>/dev/null || true
fi
echo -e "${GREEN} ✓ hostapd + dnsmasq ready${RESET}"
# Stop conflicting services (but not existing Pi-hole dnsmasq)
echo -e "${AMBER}[4/8] Configuring services...${RESET}"
sudo systemctl stop hostapd 2>/dev/null || true
sudo systemctl unmask hostapd 2>/dev/null || true
# Unblock WiFi
sudo rfkill unblock wifi 2>/dev/null || true
# Determine mode: if eth0 has carrier, use wlan0 directly; otherwise create virtual AP (uap0)
echo -e "${AMBER}[5/8] Configuring access point...${RESET}"
# ALWAYS use virtual AP (uap0) — never steal wlan0 from home WiFi
# This keeps SSH alive and home internet working while running the AP
WIFI_IF="uap0"
echo -e "${AMBER} Creating virtual AP (uap0) — wlan0 stays connected to home WiFi${RESET}"
# Remove old uap0 if exists
sudo iw dev uap0 del 2>/dev/null || true
sleep 1
# Add virtual interface for AP
sudo iw dev wlan0 interface add uap0 type __ap
sudo ip link set uap0 up
# Set different MAC to avoid conflicts with wlan0
ORIG_MAC=$(cat /sys/class/net/wlan0/address 2>/dev/null)
if [[ -n "$ORIG_MAC" ]]; then
NEW_MAC=$(echo "$ORIG_MAC" | awk -F: '{OFS=":"; $6=sprintf("%02x",(("0x"$6)+1)%256); print}')
sudo ip link set uap0 address "$NEW_MAC" 2>/dev/null || true
fi
echo -e "${GREEN} ✓ Virtual AP interface uap0 created${RESET}"
sudo ip addr add "${AP_IP}/24" dev "$WIFI_IF" 2>/dev/null || true
echo -e "${GREEN} AP interface: ${WIFI_IF}${RESET}"
# Write hostapd config
sudo tee /etc/hostapd/hostapd-roadnet.conf >/dev/null <<HOSTAPD
interface=${WIFI_IF}
driver=nl80211
ssid=${SSID}
hw_mode=g
channel=${CHANNEL}
wmm_enabled=0
macaddr_acl=0
auth_algs=1
wpa=2
wpa_passphrase=${PASSWORD}
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP
country_code=US
ieee80211n=1
ieee80211d=1
HOSTAPD
echo -e "${GREEN} ✓ hostapd configured: SSID=${SSID} CH=${CHANNEL}${RESET}"
# Write dnsmasq drop-in for RoadNet (doesn't replace existing)
sudo tee /etc/dnsmasq.d/roadnet.conf >/dev/null <<DNSMASQ
# RoadNet DHCP for WiFi clients
interface=${WIFI_IF}
listen-address=${AP_IP}
bind-dynamic
dhcp-range=${DHCP_START},${DHCP_END},255.255.255.0,24h
dhcp-option=option:router,${AP_IP}
dhcp-option=option:dns-server,${AP_IP}
server=${PIHOLE_DNS}
DNSMASQ
echo -e "${GREEN} ✓ dnsmasq configured: DHCP ${DHCP_START}-${DHCP_END}${RESET}"
# NAT / iptables
echo -e "${AMBER}[6/8] Setting up NAT routing...${RESET}"
sudo sysctl -w net.ipv4.ip_forward=1 >/dev/null
if ! grep -q "net.ipv4.ip_forward=1" /etc/sysctl.conf 2>/dev/null; then
echo "net.ipv4.ip_forward=1" | sudo tee -a /etc/sysctl.conf >/dev/null
fi
# Detect outbound interface — use wlan0 since that's our internet path
# (uap0 is the AP, wlan0 stays connected to home WiFi)
OUT_IF="wlan0"
# Add NAT rules (idempotent)
sudo iptables -t nat -C POSTROUTING -s "${SUBNET}.0/24" -o "$OUT_IF" -j MASQUERADE 2>/dev/null || \
sudo iptables -t nat -A POSTROUTING -s "${SUBNET}.0/24" -o "$OUT_IF" -j MASQUERADE
# Also masquerade through WireGuard if available
if ip link show wg0 >/dev/null 2>&1; then
sudo iptables -t nat -C POSTROUTING -s "${SUBNET}.0/24" -o wg0 -j MASQUERADE 2>/dev/null || \
sudo iptables -t nat -A POSTROUTING -s "${SUBNET}.0/24" -o wg0 -j MASQUERADE
fi
# Allow forwarding
sudo iptables -C FORWARD -i "$WIFI_IF" -o "$OUT_IF" -j ACCEPT 2>/dev/null || \
sudo iptables -A FORWARD -i "$WIFI_IF" -o "$OUT_IF" -j ACCEPT
sudo iptables -C FORWARD -i "$OUT_IF" -o "$WIFI_IF" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
sudo iptables -A FORWARD -i "$OUT_IF" -o "$WIFI_IF" -m state --state RELATED,ESTABLISHED -j ACCEPT
echo -e "${GREEN} ✓ NAT routing enabled (${WIFI_IF}${OUT_IF})${RESET}"
# Install failover service
echo -e "${AMBER}[7/8] Installing failover monitor...${RESET}"
sudo tee /usr/local/bin/roadnet-failover.sh >/dev/null <<'FAILOVER'
#!/bin/bash
# RoadNet Failover Monitor
# Checks connectivity and switches exit routes
SUBNET_PREFIX="10.10"
LOG="/var/log/roadnet-failover.log"
CURRENT_EXIT=""
OUT_IF="eth0"
ip link show eth0 >/dev/null 2>&1 || OUT_IF="end0"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG"; }
check_exit() {
# Try primary (eth0 via home router)
if ping -c 1 -W 2 -I "$OUT_IF" 1.1.1.1 >/dev/null 2>&1; then
echo "eth0"
return
fi
# Try WireGuard
if ip link show wg0 >/dev/null 2>&1; then
if ping -c 1 -W 2 -I wg0 1.1.1.1 >/dev/null 2>&1; then
echo "wg0"
return
fi
fi
# Try Bluetooth PAN
if ip link show bnep0 >/dev/null 2>&1; then
echo "bnep0"
return
fi
echo "none"
}
while true; do
NEW_EXIT=$(check_exit)
if [[ "$NEW_EXIT" != "$CURRENT_EXIT" ]]; then
log "EXIT CHANGE: ${CURRENT_EXIT:-none} → ${NEW_EXIT}"
CURRENT_EXIT="$NEW_EXIT"
if [[ "$NEW_EXIT" == "wg0" ]]; then
# Switch RoadNet traffic to WireGuard
ip route replace default via 10.8.0.1 dev wg0 table 100 2>/dev/null
log "Routing through WireGuard (10.8.0.1)"
elif [[ "$NEW_EXIT" == "eth0" || "$NEW_EXIT" == "end0" ]]; then
GW=$(ip route show default dev "$OUT_IF" 2>/dev/null | awk '{print $3}' | head -1)
[[ -z "$GW" ]] && GW="192.168.4.1"
ip route replace default via "$GW" dev "$OUT_IF" table 100 2>/dev/null
log "Routing through ${OUT_IF} (${GW})"
fi
fi
sleep 10
done
FAILOVER
sudo chmod +x /usr/local/bin/roadnet-failover.sh
sudo tee /etc/systemd/system/roadnet-failover.service >/dev/null <<'UNIT'
[Unit]
Description=RoadNet Failover Monitor
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/roadnet-failover.sh
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
UNIT
sudo systemctl daemon-reload
sudo systemctl enable roadnet-failover.service
echo -e "${GREEN} ✓ Failover monitor installed${RESET}"
# Bluetooth PAN (Aria only)
if $ENABLE_BT; then
echo -e "${AMBER}[7b] Setting up Bluetooth PAN...${RESET}"
sudo tee /usr/local/bin/roadnet-bluetooth.sh >/dev/null <<'BTPAN'
#!/bin/bash
# RoadNet Bluetooth PAN server
bluetoothctl discoverable on 2>/dev/null
bluetoothctl pairable on 2>/dev/null
# Create NAP bridge
sudo brctl addbr br-roadnet 2>/dev/null || true
sudo ip addr add 10.10.99.1/24 dev br-roadnet 2>/dev/null || true
sudo ip link set br-roadnet up
# Enable NAT for BT clients
sudo iptables -t nat -A POSTROUTING -s 10.10.99.0/24 -j MASQUERADE 2>/dev/null
# Start BT NAP
sudo bt-network -s nap br-roadnet 2>/dev/null &
echo "Bluetooth PAN active on br-roadnet (10.10.99.0/24)"
BTPAN
sudo chmod +x /usr/local/bin/roadnet-bluetooth.sh
echo -e "${GREEN} ✓ Bluetooth PAN configured${RESET}"
fi
# Start everything
echo -e "${AMBER}[8/8] Starting RoadNet...${RESET}"
# Restart dnsmasq to pick up new config
sudo systemctl restart dnsmasq 2>/dev/null || sudo dnsmasq --conf-file=/etc/dnsmasq.d/roadnet.conf &
# Start hostapd
sudo hostapd -B /etc/hostapd/hostapd-roadnet.conf 2>/dev/null
HOSTAPD_PID=$?
# Start failover
sudo systemctl start roadnet-failover.service
# Start BT if enabled
if $ENABLE_BT; then
sudo /usr/local/bin/roadnet-bluetooth.sh &
fi
# Policy routing: RoadNet traffic uses table 100
sudo ip rule add from "${SUBNET}.0/24" lookup 100 2>/dev/null || true
GW=$(ip route show default 2>/dev/null | awk '{print $3}' | head -1)
[[ -z "$GW" ]] && GW="192.168.4.1"
sudo ip route add default via "$GW" table 100 2>/dev/null || true
echo ""
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
echo -e "${GREEN} ROADNET NODE ${NODE_ID} — ONLINE${RESET}"
echo -e "${GREEN} SSID: ${SSID} | Channel: ${CHANNEL}${RESET}"
echo -e "${GREEN} AP IP: ${AP_IP} | DHCP: ${DHCP_START}-${DHCP_END}${RESET}"
echo -e "${GREEN} DNS: ${PIHOLE_DNS} (Pi-hole)${RESET}"
echo -e "${GREEN} NAT: ${WIFI_IF}${OUT_IF}${RESET}"
$ENABLE_BT && echo -e "${GREEN} Bluetooth PAN: 10.10.99.0/24${RESET}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"