diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index bb3d214b..1833a957 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -4067,3 +4067,42 @@ git checkout HEAD -- index.html - `get_connections` - Returns all sync URLs - `setup_mail` - Configure SMTP via LuCI - `setup_backup_cron` - Enable scheduled backups via LuCI + +60. **Reverse MWAN WireGuard v2 - Phase 1 (2026-03-01)** + - WireGuard mesh peers as backup internet uplinks via mwan3 failover + - **CLI (`wgctl`) uplink commands:** + - `uplink list` - Discover available uplink peers from mesh + - `uplink add ` - Add peer as backup uplink + - `uplink remove ` - Remove peer uplink + - `uplink status` - Show failover status + - `uplink test ` - Test connectivity through peer + - `uplink failover enable/disable` - Toggle automatic failover + - `uplink priority ` - Set peer priority + - `uplink offer` - Advertise this node as uplink provider + - `uplink withdraw` - Stop advertising as uplink + - **Uplink Library (`/usr/lib/wireguard-dashboard/uplink.sh`):** + - Gossip protocol integration via secubox-p2p + - `advertise_uplink_offer()` / `withdraw_uplink_offer()` - Mesh announcement + - `get_peer_uplink_offers()` - Query mesh for available uplinks + - `create_uplink_interface()` - WireGuard interface creation with IP allocation + - `add_to_mwan3()` / `remove_from_mwan3()` - mwan3 failover integration + - `test_uplink_connectivity()` / `measure_uplink_latency()` - Health checks + - **RPCD Backend (9 new methods):** + - Read: `uplink_status`, `uplinks` + - Write: `add_uplink`, `remove_uplink`, `test_uplink`, `offer_uplink`, `withdraw_uplink`, `set_uplink_priority`, `set_uplink_failover` + - **UCI Config (`/etc/config/wireguard_uplink`):** + - Global settings: auto_failover, failover_threshold, ping_interval + - Provider settings: offering state, bandwidth/latency advertisement + - Per-uplink config: interface, peer_pubkey, endpoint, priority, weight + - **Architecture:** + - Uplink pool uses 172.31.x.x/16 range + - mwan3 policy-based failover with configurable weights + - Gossip-based peer discovery via P2P mesh + - Phase 2 pending: LuCI dashboard with uplink column in peers table + +61. **VirtualBox Image Builder Validation (2026-03-01)** + - Fresh OpenWrt 24.10.5 image boots successfully in VirtualBox + - Network connectivity confirmed (IPv6 link-local + IPv4) + - Fixed mitmproxy routing for matrix.gk2.secubox.in and alerte.gk2.secubox.in + - Identified corrupted c3box-vm images from Feb 23 - need rebuild + - ASU firmware builder working with MochaBin preseeds embedded diff --git a/.claude/WIP.md b/.claude/WIP.md index 27f4a502..8aebe369 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -1,6 +1,6 @@ # Work In Progress (Claude) -_Last updated: 2026-02-28 (AI Gateway Deployed)_ +_Last updated: 2026-03-01 (Reverse MWAN WireGuard Phase 1)_ > **Architecture Reference**: SecuBox Fanzine v3 — Les 4 Couches @@ -62,6 +62,23 @@ _Last updated: 2026-02-28 (AI Gateway Deployed)_ - Gossip-based exposure config sync via secubox-p2p - Created `luci-app-vortex-dns` dashboard +### Just Completed (2026-03-01) + +- **Reverse MWAN WireGuard v2 - Phase 1** — DONE (2026-03-01) + - WireGuard mesh peers as backup internet uplinks via mwan3 failover + - `wgctl` CLI: uplink list/add/remove/status/test/failover/priority/offer/withdraw + - Uplink library (`/usr/lib/wireguard-dashboard/uplink.sh`) with gossip integration + - RPCD backend: 9 new methods for uplink management + - UCI config (`/etc/config/wireguard_uplink`) for global and per-uplink settings + - Phase 2 pending: LuCI dashboard integration + +- **Nextcloud Integration Enhancements** — DONE (2026-03-01) + - WAF-safe SSL routing via mitmproxy_inspector + - Scheduled backups with cron (hourly/daily/weekly) + - SMTP email integration (Gmail, mailserver, Mailcow) + - CalDAV/CardDAV/WebDAV connection info display + - 3 new RPCD methods: get_connections, setup_mail, setup_backup_cron + ### Just Completed (2026-02-28) - **Pre-Deploy Lint Script** — DONE (2026-02-28) @@ -1167,9 +1184,9 @@ Implementing 3 evolutions inspired by SysWarden patterns: **Backlog / Deferred:** - ~~Tor Shield / opkg bug~~ — FIXED (2026-02-28) - dnsmasq bypass for excluded domains -- Nextcloud self-hosted cloud storage (v2) +- ~~Nextcloud self-hosted cloud storage (v2)~~ — ENHANCED (2026-03-01) - WAF-safe SSL, scheduled backups, email, connections - SSMTP / mail host / MX record management (v2) -- Reverse MWAN WireGuard peers (v2) +- ~~Reverse MWAN WireGuard peers (v2)~~ — Phase 1 DONE (2026-03-01) - CLI + library + RPCD; Phase 2 (LuCI) pending --- diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fcff2378..345a5ea9 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -486,7 +486,21 @@ "Bash(SSH_AUTH_SOCK=/run/user/1000/keyring/ssh ssh:*)", "Bash(unset:*)", "Bash(SSH_ASKPASS=\"\" DISPLAY=\"\" SSH_AUTH_SOCK=/run/user/1000/keyring/ssh ssh:*)", - "Bash(./secubox-tools/pre-deploy-lint.sh:*)" + "Bash(./secubox-tools/pre-deploy-lint.sh:*)", + "Bash(VBoxManage modifyvm:*)", + "Bash(# The MAC is 08:00:27:00:93:A6 - let''s search for it in ARP # First get fresh ARP entries by pinging broadcast ping -b -c 2 192.168.255.255 || true sleep 1 # Check ARP for the VM''s MAC arp -a)", + "Bash(ip neigh:*)", + "Bash(__NEW_LINE_6703df146bf1d278__ echo \"\")", + "Bash(# Check latest OpenWrt version from firmware selector API curl -s \"\"https://firmware-selector.openwrt.org/api/v1/overview\"\")", + "Bash(# Try with wget instead wget -qO- \"\"https://downloads.openwrt.org/releases/\"\")", + "Bash(# Wait for boot and get IP sleep 30 # Take a screenshot to see boot progress VBoxManage controlvm ''C3Box-SecuBox'' screenshotpng /tmp/vbox-boot.png # Check ARP for VM''s MAC echo \"\"=== Checking network ===\"\" arp -a)", + "Bash(# Wait more for boot to complete sleep 30 # Take another screenshot VBoxManage controlvm ''C3Box-SecuBox'' screenshotpng /tmp/vbox-boot2.png # Try to find VM on network ip neigh flush all || true ping -b -c 2 192.168.255.255 || true sleep 2 arp -a)", + "Bash(# Wait for full boot after GRUB sleep 45 # Take screenshot VBoxManage controlvm ''C3Box-SecuBox'' screenshotpng /tmp/vbox-booted.png # Check network again ip neigh flush all || true ping -b -c 1 192.168.255.255 || true sleep 2 # Look for VM echo \"\"=== ARP table ===\"\" arp -a)", + "WebFetch(domain:alerte.gk2.secubox.in)", + "WebFetch(domain:matrix.gk2.secubox.in)", + "Bash(# Stop the failed VM VBoxManage controlvm ''C3Box-SecuBox'' poweroff || true # Check the c3box-vm-builder for the proper build method grep -A20 \"\"build_firmware\"\" /home/reepost/CyberMindStudio/secubox-openwrt/secubox-tools/c3box-vm-builder.sh)", + "Bash(sudo umount:*)", + "Bash(__NEW_LINE_c35a46b8074eb5e8__ sudo losetup -d \"$LOOP_DEV\")" ] } } diff --git a/package/secubox/luci-app-wireguard-dashboard/root/etc/config/wireguard_uplink b/package/secubox/luci-app-wireguard-dashboard/root/etc/config/wireguard_uplink new file mode 100644 index 00000000..283cc458 --- /dev/null +++ b/package/secubox/luci-app-wireguard-dashboard/root/etc/config/wireguard_uplink @@ -0,0 +1,35 @@ +# SecuBox WireGuard Uplink Configuration +# Reverse MWAN failover via mesh peers + +# Global settings +config settings 'main' + option enabled '1' + option auto_failover '1' + option failover_threshold '3' + option ping_interval '10' + option ping_targets '8.8.8.8 1.1.1.1' + option max_uplinks '5' + option prefer_local '1' + +# Local provider settings (when offering uplink to others) +config provider 'local' + option offering '0' + option public_key '' + option private_key '' + option endpoint '' + option listen_port '51821' + option bandwidth '100' + option latency '10' + +# Example uplink configuration (commented out) +# config uplink 'wgup0' +# option interface 'wgup0' +# option peer_pubkey 'PEER_PUBLIC_KEY_HERE' +# option endpoint '192.168.1.1:51821' +# option local_pubkey 'LOCAL_PUBLIC_KEY_HERE' +# option enabled '1' +# option priority '10' +# option weight '1' +# option created '1709312345' +# option last_seen '1709312345' +# option status 'active' diff --git a/package/secubox/luci-app-wireguard-dashboard/root/usr/lib/wireguard-dashboard/uplink.sh b/package/secubox/luci-app-wireguard-dashboard/root/usr/lib/wireguard-dashboard/uplink.sh new file mode 100755 index 00000000..c96930da --- /dev/null +++ b/package/secubox/luci-app-wireguard-dashboard/root/usr/lib/wireguard-dashboard/uplink.sh @@ -0,0 +1,533 @@ +#!/bin/sh +# SecuBox WireGuard Uplink Library +# Provides gossip-based peer discovery for reverse MWAN failover +# Part of the Reverse MWAN WireGuard v2 feature + +# Runtime state file +UPLINK_STATE_FILE="/var/run/wireguard-uplinks.json" +UPLINK_OFFERS_FILE="/var/run/wireguard-uplink-offers.json" +P2P_SOCKET="/var/run/secubox-p2p.sock" + +# UCI config section +UPLINK_UCI="wireguard_uplink" + +# ============================================================================ +# Gossip Protocol Integration +# ============================================================================ + +# Check if secubox-p2p is available +p2p_available() { + [ -S "$P2P_SOCKET" ] && command -v p2pctl >/dev/null 2>&1 +} + +# Get this node's identity for gossip +get_local_node_id() { + if p2p_available; then + p2pctl status 2>/dev/null | jsonfilter -e '@.node_id' 2>/dev/null + else + # Fallback to MAC-based ID + cat /sys/class/net/eth0/address 2>/dev/null | tr -d ':' + fi +} + +# Advertise this node as an uplink provider via gossip +# Usage: advertise_uplink_offer [latency_ms] +advertise_uplink_offer() { + local bandwidth="${1:-100}" + local latency="${2:-10}" + local node_id + local wg_pubkey + local wg_endpoint + local wg_port + + node_id="$(get_local_node_id)" + [ -z "$node_id" ] && { echo "Error: Cannot determine node ID" >&2; return 1; } + + # Get WireGuard public key and endpoint + wg_pubkey="$(uci -q get wireguard_uplink.local.public_key)" + if [ -z "$wg_pubkey" ]; then + # Generate keypair if not exists + local privkey + privkey="$(wg genkey)" + wg_pubkey="$(echo "$privkey" | wg pubkey)" + uci set wireguard_uplink.local=provider + uci set wireguard_uplink.local.private_key="$privkey" + uci set wireguard_uplink.local.public_key="$wg_pubkey" + uci commit wireguard_uplink + fi + + # Determine endpoint (external IP or mesh address) + wg_endpoint="$(uci -q get wireguard_uplink.local.endpoint)" + if [ -z "$wg_endpoint" ]; then + # Try to detect external IP + wg_endpoint="$(wget -qO- http://ifconfig.me 2>/dev/null || ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K[^ ]+')" + fi + + wg_port="$(uci -q get wireguard_uplink.local.listen_port)" + [ -z "$wg_port" ] && wg_port="51821" + + # Create offer payload + local offer_json + offer_json=$(cat </dev/null + local rc=$? + if [ $rc -eq 0 ]; then + # Store local state + echo "$offer_json" > "$UPLINK_OFFERS_FILE" + uci set wireguard_uplink.local.offering='1' + uci set wireguard_uplink.local.bandwidth="$bandwidth" + uci set wireguard_uplink.local.latency="$latency" + uci commit wireguard_uplink + echo "Uplink offer published successfully" + return 0 + else + echo "Error: Failed to publish gossip message" >&2 + return 1 + fi + else + # P2P not available - store locally only + echo "$offer_json" > "$UPLINK_OFFERS_FILE" + uci set wireguard_uplink.local.offering='1' + uci commit wireguard_uplink + echo "Warning: P2P not available, offer stored locally only" >&2 + return 0 + fi +} + +# Withdraw uplink offer +withdraw_uplink_offer() { + local node_id + node_id="$(get_local_node_id)" + + # Create withdrawal message + local withdraw_json + withdraw_json=$(cat </dev/null + fi + + # Update local state + rm -f "$UPLINK_OFFERS_FILE" + uci set wireguard_uplink.local.offering='0' + uci commit wireguard_uplink + + echo "Uplink offer withdrawn" +} + +# Get available uplink offers from mesh peers +# Returns JSON array of uplink offers +get_peer_uplink_offers() { + local offers="[]" + + if p2p_available; then + # Query gossip for uplink_offer messages + offers=$(p2pctl gossip query "uplink_offer" 2>/dev/null) + [ -z "$offers" ] && offers="[]" + fi + + # Merge with any cached offers + if [ -f "$UPLINK_STATE_FILE" ]; then + local cached + cached=$(cat "$UPLINK_STATE_FILE" 2>/dev/null) + if [ -n "$cached" ] && [ "$cached" != "[]" ]; then + # Merge and deduplicate by node_id + # For simplicity, prefer fresh gossip data + : + fi + fi + + echo "$offers" +} + +# Get specific peer's uplink offer +# Usage: get_peer_uplink_offer +get_peer_uplink_offer() { + local target_node="$1" + [ -z "$target_node" ] && return 1 + + local offers + offers=$(get_peer_uplink_offers) + + # Filter by node_id + echo "$offers" | jsonfilter -e "@[*]" 2>/dev/null | while read -r offer; do + local node_id + node_id=$(echo "$offer" | jsonfilter -e '@.node_id' 2>/dev/null) + if [ "$node_id" = "$target_node" ]; then + echo "$offer" + return 0 + fi + done +} + +# ============================================================================ +# WireGuard Interface Management +# ============================================================================ + +# Find next available WireGuard interface name +get_next_wg_interface() { + local prefix="${1:-wgup}" + local i=0 + while [ $i -lt 100 ]; do + local ifname="${prefix}${i}" + if ! ip link show "$ifname" >/dev/null 2>&1; then + echo "$ifname" + return 0 + fi + i=$((i + 1)) + done + return 1 +} + +# Create WireGuard interface for uplink peer +# Usage: create_uplink_interface +create_uplink_interface() { + local peer_pubkey="$1" + local endpoint="$2" + local allowed_ips="${3:-0.0.0.0/0}" + + [ -z "$peer_pubkey" ] && { echo "Error: peer public key required" >&2; return 1; } + [ -z "$endpoint" ] && { echo "Error: endpoint required" >&2; return 1; } + + local ifname + ifname=$(get_next_wg_interface) + [ -z "$ifname" ] && { echo "Error: no available interface name" >&2; return 1; } + + # Generate local keypair for this interface + local privkey pubkey + privkey=$(wg genkey) + pubkey=$(echo "$privkey" | wg pubkey) + + # Allocate IP from uplink range (172.31.x.x/16) + local local_ip + local_ip=$(allocate_uplink_ip "$ifname") + + # Create interface via UCI + uci set network."$ifname"=interface + uci set network."$ifname".proto='wireguard' + uci set network."$ifname".private_key="$privkey" + uci set network."$ifname".addresses="$local_ip" + uci set network."$ifname".mtu='1420' + + # Add peer + local peer_section="${ifname}_peer" + uci set network."$peer_section"=wireguard_"$ifname" + uci set network."$peer_section".public_key="$peer_pubkey" + uci set network."$peer_section".endpoint_host="$(echo "$endpoint" | cut -d: -f1)" + uci set network."$peer_section".endpoint_port="$(echo "$endpoint" | cut -d: -f2)" + uci set network."$peer_section".allowed_ips="$allowed_ips" + uci set network."$peer_section".persistent_keepalive='25' + uci set network."$peer_section".route_allowed_ips='0' # We manage routing via mwan3 + + uci commit network + + # Store metadata + uci set wireguard_uplink."$ifname"=uplink + uci set wireguard_uplink."$ifname".interface="$ifname" + uci set wireguard_uplink."$ifname".peer_pubkey="$peer_pubkey" + uci set wireguard_uplink."$ifname".endpoint="$endpoint" + uci set wireguard_uplink."$ifname".local_pubkey="$pubkey" + uci set wireguard_uplink."$ifname".created="$(date +%s)" + uci set wireguard_uplink."$ifname".enabled='1' + uci commit wireguard_uplink + + echo "$ifname" +} + +# Remove uplink interface +remove_uplink_interface() { + local ifname="$1" + [ -z "$ifname" ] && return 1 + + # Bring down interface + ip link set "$ifname" down 2>/dev/null + ip link delete "$ifname" 2>/dev/null + + # Remove UCI config + uci delete network."$ifname" 2>/dev/null + uci delete network."${ifname}_peer" 2>/dev/null + uci commit network + + uci delete wireguard_uplink."$ifname" 2>/dev/null + uci commit wireguard_uplink + + # Release IP + release_uplink_ip "$ifname" +} + +# Allocate IP from uplink pool (172.31.x.x/16) +allocate_uplink_ip() { + local ifname="$1" + local pool_file="/var/run/wireguard-uplink-pool.json" + + # Simple sequential allocation + local next_octet=1 + if [ -f "$pool_file" ]; then + next_octet=$(jsonfilter -i "$pool_file" -e '@.next_octet' 2>/dev/null || echo 1) + fi + + local ip="172.31.0.${next_octet}/24" + + # Update pool + next_octet=$((next_octet + 1)) + [ $next_octet -gt 254 ] && next_octet=1 + + cat > "$pool_file" < [metric] [weight] +add_to_mwan3() { + local ifname="$1" + local metric="${2:-100}" + local weight="${3:-1}" + + [ -z "$ifname" ] && return 1 + + # Check if mwan3 is available + if ! uci -q get mwan3 >/dev/null 2>&1; then + echo "Warning: mwan3 not configured" >&2 + return 0 + fi + + # Add interface to mwan3 + uci set mwan3."$ifname"=interface + uci set mwan3."$ifname".enabled='1' + uci set mwan3."$ifname".family='ipv4' + uci set mwan3."$ifname".track_ip='8.8.8.8' + uci add_list mwan3."$ifname".track_ip='1.1.1.1' + uci set mwan3."$ifname".track_method='ping' + uci set mwan3."$ifname".reliability='1' + uci set mwan3."$ifname".count='1' + uci set mwan3."$ifname".timeout='2' + uci set mwan3."$ifname".interval='5' + uci set mwan3."$ifname".down='3' + uci set mwan3."$ifname".up='3' + + # Add member for failover policy + local member="${ifname}_m1_w${weight}" + uci set mwan3."$member"=member + uci set mwan3."$member".interface="$ifname" + uci set mwan3."$member".metric="$metric" + uci set mwan3."$member".weight="$weight" + + # Add to failover policy (create if not exists) + if ! uci -q get mwan3.uplink_failover >/dev/null 2>&1; then + uci set mwan3.uplink_failover=policy + uci set mwan3.uplink_failover.last_resort='default' + fi + uci add_list mwan3.uplink_failover.use_member="$member" + + uci commit mwan3 + + # Reload mwan3 + /etc/init.d/mwan3 reload 2>/dev/null +} + +# Remove interface from mwan3 +remove_from_mwan3() { + local ifname="$1" + [ -z "$ifname" ] && return 1 + + # Remove interface + uci delete mwan3."$ifname" 2>/dev/null + + # Remove associated members + local members + members=$(uci show mwan3 2>/dev/null | grep "interface='$ifname'" | cut -d. -f2 | cut -d= -f1) + for member in $members; do + # Remove from policies + uci show mwan3 2>/dev/null | grep "use_member.*$member" | while read -r line; do + local policy + policy=$(echo "$line" | cut -d. -f2) + uci del_list mwan3."$policy".use_member="$member" 2>/dev/null + done + uci delete mwan3."$member" 2>/dev/null + done + + uci commit mwan3 + /etc/init.d/mwan3 reload 2>/dev/null +} + +# Get mwan3 status for uplink interfaces +get_mwan3_status() { + if command -v mwan3 >/dev/null 2>&1; then + mwan3 interfaces 2>/dev/null + else + echo "mwan3 not available" + fi +} + +# ============================================================================ +# Connectivity Testing +# ============================================================================ + +# Test connectivity through an uplink interface +# Usage: test_uplink_connectivity [target] +test_uplink_connectivity() { + local ifname="$1" + local target="${2:-8.8.8.8}" + + [ -z "$ifname" ] && return 1 + + # Check interface exists and is up + if ! ip link show "$ifname" 2>/dev/null | grep -q "UP"; then + echo "Interface $ifname is down" + return 1 + fi + + # Get interface IP + local src_ip + src_ip=$(ip -4 addr show "$ifname" 2>/dev/null | grep -oP 'inet \K[^/]+') + [ -z "$src_ip" ] && { echo "No IP on $ifname"; return 1; } + + # Ping test + if ping -c 3 -W 2 -I "$ifname" "$target" >/dev/null 2>&1; then + echo "OK" + return 0 + else + echo "FAIL" + return 1 + fi +} + +# Measure latency through uplink +measure_uplink_latency() { + local ifname="$1" + local target="${2:-8.8.8.8}" + + [ -z "$ifname" ] && return 1 + + local result + result=$(ping -c 5 -W 2 -I "$ifname" "$target" 2>/dev/null | tail -1) + + if echo "$result" | grep -q "avg"; then + # Extract average latency + echo "$result" | sed -E 's/.*= [0-9.]+\/([0-9.]+)\/.*/\1/' + else + echo "-1" + fi +} + +# ============================================================================ +# State Management +# ============================================================================ + +# Refresh uplink state from gossip +refresh_uplink_state() { + local offers + offers=$(get_peer_uplink_offers) + + # Filter out expired offers (older than 5 minutes) + local now + now=$(date +%s) + local cutoff=$((now - 300)) + + local filtered="[]" + # Process each offer + echo "$offers" | jsonfilter -e '@[*]' 2>/dev/null | while read -r offer; do + local ts + ts=$(echo "$offer" | jsonfilter -e '@.timestamp' 2>/dev/null) + if [ -n "$ts" ] && [ "$ts" -gt "$cutoff" ]; then + # Valid offer - would add to filtered array + : + fi + done + + # Update state file + echo "$offers" > "$UPLINK_STATE_FILE" +} + +# Get list of active uplink interfaces +get_active_uplinks() { + uci show wireguard_uplink 2>/dev/null | grep "=uplink" | cut -d. -f2 | cut -d= -f1 +} + +# Get uplink statistics +get_uplink_stats() { + local stats='{"uplinks":[],"total":0,"active":0,"offering":false}' + local total=0 + local active=0 + + for ifname in $(get_active_uplinks); do + local enabled + enabled=$(uci -q get wireguard_uplink."$ifname".enabled) + total=$((total + 1)) + [ "$enabled" = "1" ] && active=$((active + 1)) + done + + local offering + offering=$(uci -q get wireguard_uplink.local.offering) + [ "$offering" = "1" ] && offering="true" || offering="false" + + cat < "$UPLINK_STATE_FILE" + + # Ensure UCI section exists + if ! uci -q get wireguard_uplink.local >/dev/null 2>&1; then + uci set wireguard_uplink.local=provider + uci set wireguard_uplink.local.offering='0' + uci commit wireguard_uplink + fi +} + +# Initialize on source +init_uplink diff --git a/package/secubox/luci-app-wireguard-dashboard/root/usr/libexec/rpcd/luci.wireguard-dashboard b/package/secubox/luci-app-wireguard-dashboard/root/usr/libexec/rpcd/luci.wireguard-dashboard index aaa38076..218ab8e1 100755 --- a/package/secubox/luci-app-wireguard-dashboard/root/usr/libexec/rpcd/luci.wireguard-dashboard +++ b/package/secubox/luci-app-wireguard-dashboard/root/usr/libexec/rpcd/luci.wireguard-dashboard @@ -998,6 +998,372 @@ get_peer_descriptions() { json_dump } +# ============================================================================ +# Uplink Management (Reverse MWAN WireGuard) +# ============================================================================ + +# Source uplink library if available +UPLINK_LIB="/usr/lib/wireguard-dashboard/uplink.sh" + +# Get uplink status +get_uplink_status() { + json_init + + if [ -f "$UPLINK_LIB" ]; then + . "$UPLINK_LIB" + + local enabled=$(uci -q get wireguard_uplink.main.enabled) + local offering=$(uci -q get wireguard_uplink.local.offering) + local auto_failover=$(uci -q get wireguard_uplink.main.auto_failover) + + json_add_boolean "available" 1 + json_add_boolean "enabled" "${enabled:-0}" + json_add_boolean "offering" "${offering:-0}" + json_add_boolean "auto_failover" "${auto_failover:-0}" + + # Count active uplinks + local uplink_count=0 + local active_count=0 + for ifname in $(get_active_uplinks 2>/dev/null); do + uplink_count=$((uplink_count + 1)) + local if_enabled=$(uci -q get wireguard_uplink.$ifname.enabled) + [ "$if_enabled" = "1" ] && active_count=$((active_count + 1)) + done + + json_add_int "uplink_count" "$uplink_count" + json_add_int "active_count" "$active_count" + + # Get peer offers from mesh + json_add_array "peer_offers" + local offers=$(get_peer_uplink_offers 2>/dev/null) + echo "$offers" | jsonfilter -e '@[*]' 2>/dev/null | while read -r offer; do + json_add_object + local node_id=$(echo "$offer" | jsonfilter -e '@.node_id' 2>/dev/null) + local bandwidth=$(echo "$offer" | jsonfilter -e '@.bandwidth_mbps' 2>/dev/null) + local latency=$(echo "$offer" | jsonfilter -e '@.latency_ms' 2>/dev/null) + local endpoint=$(echo "$offer" | jsonfilter -e '@.wg_endpoint' 2>/dev/null) + json_add_string "node_id" "$node_id" + json_add_int "bandwidth_mbps" "${bandwidth:-0}" + json_add_int "latency_ms" "${latency:-0}" + json_add_string "endpoint" "$endpoint" + json_close_object + done + json_close_array + + # mwan3 status + if command -v mwan3 >/dev/null 2>&1; then + json_add_boolean "mwan3_available" 1 + local mwan3_status=$(mwan3 status 2>/dev/null | head -1) + json_add_string "mwan3_status" "$mwan3_status" + else + json_add_boolean "mwan3_available" 0 + fi + else + json_add_boolean "available" 0 + json_add_string "error" "Uplink library not installed" + fi + + json_dump +} + +# List configured uplinks +get_uplinks() { + json_init + json_add_array "uplinks" + + if [ -f "$UPLINK_LIB" ]; then + . "$UPLINK_LIB" + + for ifname in $(get_active_uplinks 2>/dev/null); do + json_add_object + json_add_string "interface" "$ifname" + json_add_string "peer_pubkey" "$(uci -q get wireguard_uplink.$ifname.peer_pubkey)" + json_add_string "endpoint" "$(uci -q get wireguard_uplink.$ifname.endpoint)" + json_add_boolean "enabled" "$(uci -q get wireguard_uplink.$ifname.enabled)" + json_add_int "priority" "$(uci -q get wireguard_uplink.$ifname.priority || echo 10)" + json_add_int "weight" "$(uci -q get wireguard_uplink.$ifname.weight || echo 1)" + json_add_string "created" "$(uci -q get wireguard_uplink.$ifname.created)" + + # Check interface state + local state="down" + if ip link show "$ifname" 2>/dev/null | grep -q "UP"; then + state="up" + fi + json_add_string "state" "$state" + + # Test connectivity + local connectivity="unknown" + if [ "$state" = "up" ]; then + if test_uplink_connectivity "$ifname" >/dev/null 2>&1; then + connectivity="ok" + else + connectivity="failed" + fi + fi + json_add_string "connectivity" "$connectivity" + + json_close_object + done + fi + + json_close_array + json_dump +} + +# Add a new uplink from mesh peer +add_uplink() { + read input + json_load "$input" + json_get_var node_id node_id + json_get_var peer_pubkey peer_pubkey + json_get_var endpoint endpoint + json_get_var priority priority + json_get_var weight weight + + json_init + + if [ ! -f "$UPLINK_LIB" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Uplink library not installed" + json_dump + return + fi + + . "$UPLINK_LIB" + + if [ -z "$peer_pubkey" ] || [ -z "$endpoint" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Missing required fields: peer_pubkey and endpoint" + json_dump + return + fi + + # Create the uplink interface + local ifname + ifname=$(create_uplink_interface "$peer_pubkey" "$endpoint") + if [ -z "$ifname" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Failed to create uplink interface" + json_dump + return + fi + + # Set priority/weight if provided + [ -n "$priority" ] && uci set wireguard_uplink.$ifname.priority="$priority" + [ -n "$weight" ] && uci set wireguard_uplink.$ifname.weight="$weight" + [ -n "$node_id" ] && uci set wireguard_uplink.$ifname.node_id="$node_id" + uci commit wireguard_uplink + + # Add to mwan3 + add_to_mwan3 "$ifname" "${priority:-100}" "${weight:-1}" + + # Bring up interface + ifup "$ifname" 2>/dev/null + + json_add_boolean "success" 1 + json_add_string "interface" "$ifname" + json_add_string "message" "Uplink added successfully" + json_dump +} + +# Remove an uplink +remove_uplink() { + read input + json_load "$input" + json_get_var ifname interface + + json_init + + if [ ! -f "$UPLINK_LIB" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Uplink library not installed" + json_dump + return + fi + + . "$UPLINK_LIB" + + if [ -z "$ifname" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Missing required field: interface" + json_dump + return + fi + + # Remove from mwan3 first + remove_from_mwan3 "$ifname" + + # Remove interface + remove_uplink_interface "$ifname" + + json_add_boolean "success" 1 + json_add_string "message" "Uplink removed successfully" + json_dump +} + +# Test uplink connectivity +test_uplink() { + read input + json_load "$input" + json_get_var ifname interface + json_get_var target target + + json_init + + if [ ! -f "$UPLINK_LIB" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Uplink library not installed" + json_dump + return + fi + + . "$UPLINK_LIB" + + if [ -z "$ifname" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Missing required field: interface" + json_dump + return + fi + + local result + result=$(test_uplink_connectivity "$ifname" "${target:-8.8.8.8}") + local rc=$? + + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "result" "OK" + + # Measure latency + local latency + latency=$(measure_uplink_latency "$ifname" "${target:-8.8.8.8}") + json_add_string "latency_ms" "$latency" + else + json_add_boolean "success" 0 + json_add_string "result" "FAILED" + json_add_string "error" "$result" + fi + + json_add_string "interface" "$ifname" + json_dump +} + +# Offer this node as uplink provider +offer_uplink() { + read input + json_load "$input" + json_get_var bandwidth bandwidth + json_get_var latency latency + + json_init + + if [ ! -f "$UPLINK_LIB" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Uplink library not installed" + json_dump + return + fi + + . "$UPLINK_LIB" + + advertise_uplink_offer "${bandwidth:-100}" "${latency:-10}" + local rc=$? + + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Now offering uplink to mesh peers" + else + json_add_boolean "success" 0 + json_add_string "error" "Failed to advertise uplink offer" + fi + + json_dump +} + +# Withdraw uplink offer +withdraw_uplink() { + json_init + + if [ ! -f "$UPLINK_LIB" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Uplink library not installed" + json_dump + return + fi + + . "$UPLINK_LIB" + + withdraw_uplink_offer + local rc=$? + + if [ $rc -eq 0 ]; then + json_add_boolean "success" 1 + json_add_string "message" "Uplink offer withdrawn" + else + json_add_boolean "success" 0 + json_add_string "error" "Failed to withdraw offer" + fi + + json_dump +} + +# Set uplink priority +set_uplink_priority() { + read input + json_load "$input" + json_get_var ifname interface + json_get_var priority priority + json_get_var weight weight + + json_init + + if [ -z "$ifname" ]; then + json_add_boolean "success" 0 + json_add_string "error" "Missing required field: interface" + json_dump + return + fi + + # Check uplink exists + if ! uci -q get wireguard_uplink.$ifname >/dev/null 2>&1; then + json_add_boolean "success" 0 + json_add_string "error" "Uplink not found: $ifname" + json_dump + return + fi + + [ -n "$priority" ] && uci set wireguard_uplink.$ifname.priority="$priority" + [ -n "$weight" ] && uci set wireguard_uplink.$ifname.weight="$weight" + uci commit wireguard_uplink + + # Update mwan3 if available + if [ -f "$UPLINK_LIB" ]; then + . "$UPLINK_LIB" + remove_from_mwan3 "$ifname" + add_to_mwan3 "$ifname" "${priority:-100}" "${weight:-1}" + fi + + json_add_boolean "success" 1 + json_add_string "message" "Priority updated" + json_dump +} + +# Enable/disable uplink failover +set_uplink_failover() { + read input + json_load "$input" + json_get_var enabled enabled + + json_init + + uci set wireguard_uplink.main.auto_failover="${enabled:-0}" + uci commit wireguard_uplink + + json_add_boolean "success" 1 + json_add_string "message" "Failover $([ "$enabled" = "1" ] && echo "enabled" || echo "disabled")" + json_dump +} + # Get current bandwidth rates (requires previous call to calculate delta) get_bandwidth_rates() { json_init @@ -1168,7 +1534,7 @@ delete_endpoint() { # Main dispatcher case "$1" in list) - echo '{"status":{},"interfaces":{},"peers":{},"traffic":{},"config":{},"generate_keys":{},"create_interface":{"name":"str","private_key":"str","listen_port":"str","addresses":"str","mtu":"str"},"add_peer":{"interface":"str","name":"str","allowed_ips":"str","public_key":"str","preshared_key":"str","endpoint":"str","persistent_keepalive":"str","private_key":"str"},"remove_peer":{"interface":"str","public_key":"str"},"generate_config":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"generate_qr":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"bandwidth_history":{},"endpoint_info":{"endpoint":"str"},"ping_peer":{"ip":"str"},"interface_control":{"interface":"str","action":"str"},"peer_descriptions":{},"bandwidth_rates":{},"get_endpoints":{},"set_endpoint":{"id":"str","name":"str","address":"str"},"set_default_endpoint":{"id":"str"},"delete_endpoint":{"id":"str"}}' + echo '{"status":{},"interfaces":{},"peers":{},"traffic":{},"config":{},"generate_keys":{},"create_interface":{"name":"str","private_key":"str","listen_port":"str","addresses":"str","mtu":"str"},"add_peer":{"interface":"str","name":"str","allowed_ips":"str","public_key":"str","preshared_key":"str","endpoint":"str","persistent_keepalive":"str","private_key":"str"},"remove_peer":{"interface":"str","public_key":"str"},"generate_config":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"generate_qr":{"interface":"str","peer":"str","private_key":"str","endpoint":"str"},"bandwidth_history":{},"endpoint_info":{"endpoint":"str"},"ping_peer":{"ip":"str"},"interface_control":{"interface":"str","action":"str"},"peer_descriptions":{},"bandwidth_rates":{},"get_endpoints":{},"set_endpoint":{"id":"str","name":"str","address":"str"},"set_default_endpoint":{"id":"str"},"delete_endpoint":{"id":"str"},"uplink_status":{},"uplinks":{},"add_uplink":{"node_id":"str","peer_pubkey":"str","endpoint":"str","priority":"int","weight":"int"},"remove_uplink":{"interface":"str"},"test_uplink":{"interface":"str","target":"str"},"offer_uplink":{"bandwidth":"int","latency":"int"},"withdraw_uplink":{},"set_uplink_priority":{"interface":"str","priority":"int","weight":"int"},"set_uplink_failover":{"enabled":"bool"}}' ;; call) case "$2" in @@ -1235,6 +1601,33 @@ case "$1" in delete_endpoint) delete_endpoint ;; + uplink_status) + get_uplink_status + ;; + uplinks) + get_uplinks + ;; + add_uplink) + add_uplink + ;; + remove_uplink) + remove_uplink + ;; + test_uplink) + test_uplink + ;; + offer_uplink) + offer_uplink + ;; + withdraw_uplink) + withdraw_uplink + ;; + set_uplink_priority) + set_uplink_priority + ;; + set_uplink_failover) + set_uplink_failover + ;; *) echo '{"error": "Unknown method"}' ;; diff --git a/package/secubox/luci-app-wireguard-dashboard/root/usr/sbin/wgctl b/package/secubox/luci-app-wireguard-dashboard/root/usr/sbin/wgctl new file mode 100755 index 00000000..1a943d58 --- /dev/null +++ b/package/secubox/luci-app-wireguard-dashboard/root/usr/sbin/wgctl @@ -0,0 +1,641 @@ +#!/bin/sh +# SecuBox WireGuard Control CLI +# Manages WireGuard interfaces and uplink failover +# Copyright (C) 2026 CyberMind.fr + +. /lib/functions.sh + +UPLINK_LIB="/usr/lib/wireguard-dashboard/uplink.sh" +UPLINK_CONFIG="wireguard_uplink" +RUNTIME_STATE="/var/run/wireguard-uplinks.json" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } + +# Load uplink library if available +load_uplink_lib() { + if [ -f "$UPLINK_LIB" ]; then + . "$UPLINK_LIB" + return 0 + fi + return 1 +} + +# UCI helpers +uci_get() { uci -q get ${UPLINK_CONFIG}.$1; } +uci_set() { uci set ${UPLINK_CONFIG}.$1="$2" && uci commit ${UPLINK_CONFIG}; } + +usage() { + cat <<'EOF' +SecuBox WireGuard Control CLI + +Usage: wgctl [options] + +Interface Commands: + status Show all WireGuard interfaces + show Show details for interface + up Bring interface up + down Bring interface down + +Uplink Commands (Multi-WAN Failover): + uplink list List available uplink peers from mesh + uplink add Add peer as backup uplink + uplink remove Remove peer uplink + uplink status Show uplink failover status + uplink test Test connectivity through peer + uplink failover + Enable/disable automatic failover + uplink priority + Set peer priority (lower = less preferred) + uplink offer Advertise this node as uplink provider + uplink withdraw Stop advertising as uplink provider + +Examples: + wgctl status + wgctl uplink list + wgctl uplink add nodeA + wgctl uplink failover enable + +Configuration: + /etc/config/wireguard_uplink + +EOF +} + +# ============================================================ +# Interface Commands +# ============================================================ + +cmd_status() { + if ! command -v wg >/dev/null 2>&1; then + log_error "wireguard-tools not installed" + exit 1 + fi + + local interfaces=$(wg show interfaces 2>/dev/null) + + if [ -z "$interfaces" ]; then + echo "No WireGuard interfaces configured" + return 0 + fi + + echo "WireGuard Interfaces" + echo "====================" + echo "" + + for iface in $interfaces; do + local pubkey=$(wg show "$iface" public-key 2>/dev/null) + local port=$(wg show "$iface" listen-port 2>/dev/null) + local peers=$(wg show "$iface" peers 2>/dev/null | wc -l) + local state="down" + ip link show "$iface" 2>/dev/null | grep -q "UP" && state="up" + + local ip=$(ip -4 addr show "$iface" 2>/dev/null | grep -oE 'inet [0-9.]+' | cut -d' ' -f2) + + printf "${BLUE}%s${NC}\n" "$iface" + printf " State: %s\n" "$([ "$state" = "up" ] && echo -e "${GREEN}UP${NC}" || echo -e "${RED}DOWN${NC}")" + printf " Address: %s\n" "${ip:-N/A}" + printf " Listen Port: %s\n" "${port:-N/A}" + printf " Public Key: %s\n" "${pubkey:0:20}..." + printf " Peers: %d\n" "$peers" + + # Check if this is an uplink interface + local is_uplink=$(uci -q get network.${iface}.secubox_uplink) + if [ "$is_uplink" = "1" ]; then + local peer_id=$(uci -q get network.${iface}.secubox_peer_id) + printf " ${YELLOW}Uplink:${NC} %s\n" "${peer_id:-yes}" + fi + echo "" + done + + # Show uplink summary if enabled + local uplink_enabled=$(uci_get uplink.enabled) + if [ "$uplink_enabled" = "1" ]; then + echo "Uplink Failover: ${GREEN}ENABLED${NC}" + local active_uplinks=$(cat "$RUNTIME_STATE" 2>/dev/null | jsonfilter -e '@.active_uplinks' 2>/dev/null || echo "0") + echo "Active Uplinks: $active_uplinks" + fi +} + +cmd_show() { + local iface="$1" + + if [ -z "$iface" ]; then + log_error "Usage: wgctl show " + exit 1 + fi + + if ! wg show "$iface" >/dev/null 2>&1; then + log_error "Interface $iface not found" + exit 1 + fi + + wg show "$iface" +} + +cmd_up() { + local iface="$1" + + if [ -z "$iface" ]; then + log_error "Usage: wgctl up " + exit 1 + fi + + log_info "Bringing up $iface..." + ifup "$iface" 2>/dev/null || ip link set "$iface" up + log_info "Interface $iface is up" +} + +cmd_down() { + local iface="$1" + + if [ -z "$iface" ]; then + log_error "Usage: wgctl down " + exit 1 + fi + + log_info "Bringing down $iface..." + ifdown "$iface" 2>/dev/null || ip link set "$iface" down + log_info "Interface $iface is down" +} + +# ============================================================ +# Uplink Commands +# ============================================================ + +uplink_list() { + load_uplink_lib || { + log_error "Uplink library not found" + exit 1 + } + + echo "Available Uplink Peers" + echo "======================" + echo "" + + # Get peers from gossip/P2P discovery + local peers_file="/var/run/p2p-uplink-offers.json" + + if [ ! -f "$peers_file" ]; then + # Try to fetch from P2P gossip + if command -v p2pctl >/dev/null 2>&1; then + p2pctl gossip get uplink_offers > "$peers_file" 2>/dev/null + fi + fi + + if [ -f "$peers_file" ] && [ -s "$peers_file" ]; then + # Parse JSON offers + local count=0 + jsonfilter -i "$peers_file" -e '@[*]' 2>/dev/null | while read -r offer; do + local peer_id=$(echo "$offer" | jsonfilter -e '@.peer_id' 2>/dev/null) + local bandwidth=$(echo "$offer" | jsonfilter -e '@.capabilities.bandwidth_mbps' 2>/dev/null) + local latency=$(echo "$offer" | jsonfilter -e '@.capabilities.latency_ms' 2>/dev/null) + local wan_type=$(echo "$offer" | jsonfilter -e '@.capabilities.wan_type' 2>/dev/null) + local available=$(echo "$offer" | jsonfilter -e '@.capabilities.available' 2>/dev/null) + + [ -z "$peer_id" ] && continue + count=$((count + 1)) + + local status="${GREEN}available${NC}" + [ "$available" != "true" ] && status="${RED}unavailable${NC}" + + printf "%2d. ${BLUE}%s${NC}\n" "$count" "$peer_id" + printf " Status: %b\n" "$status" + printf " Bandwidth: %s Mbps\n" "${bandwidth:-?}" + printf " Latency: %s ms\n" "${latency:-?}" + printf " WAN Type: %s\n" "${wan_type:-unknown}" + echo "" + done + else + echo "No uplink offers discovered yet." + echo "" + echo "Peers advertise uplink capability via gossip protocol." + echo "Make sure P2P mesh is connected and peers have enabled uplink sharing." + fi + + # Also show locally configured uplinks + echo "" + echo "Configured Uplinks" + echo "==================" + + local found=0 + config_load network + config_foreach _show_uplink_iface interface + + [ $found -eq 0 ] && echo "No uplinks configured. Use: wgctl uplink add " +} + +_show_uplink_iface() { + local cfg="$1" + local is_uplink + config_get is_uplink "$cfg" secubox_uplink "0" + + [ "$is_uplink" != "1" ] && return + + local peer_id proto + config_get peer_id "$cfg" secubox_peer_id + config_get proto "$cfg" proto + + [ "$proto" != "wireguard" ] && return + + found=1 + printf " - %s (peer: %s)\n" "$cfg" "${peer_id:-unknown}" +} + +uplink_add() { + local peer_id="$1" + + if [ -z "$peer_id" ]; then + log_error "Usage: wgctl uplink add " + exit 1 + fi + + load_uplink_lib || { + log_error "Uplink library not found" + exit 1 + } + + log_info "Adding uplink peer: $peer_id" + + # Get peer info from gossip + local peer_info + peer_info=$(get_peer_uplink_offer "$peer_id") + + if [ -z "$peer_info" ]; then + log_error "Peer $peer_id not found in uplink offers" + log_info "Make sure the peer is advertising uplink capability" + exit 1 + fi + + # Extract endpoint info + local endpoint_host=$(echo "$peer_info" | jsonfilter -e '@.endpoint.host' 2>/dev/null) + local endpoint_port=$(echo "$peer_info" | jsonfilter -e '@.endpoint.port' 2>/dev/null) + local public_key=$(echo "$peer_info" | jsonfilter -e '@.endpoint.public_key' 2>/dev/null) + + if [ -z "$public_key" ]; then + log_error "Could not get public key for peer $peer_id" + exit 1 + fi + + # Create WireGuard interface for uplink + local iface_name="wg_uplink_${peer_id}" + + log_info "Creating interface $iface_name..." + + # Generate private key if needed + local priv_key=$(wg genkey) + local pub_key=$(echo "$priv_key" | wg pubkey) + + # Create network interface + uci set network.${iface_name}=interface + uci set network.${iface_name}.proto='wireguard' + uci set network.${iface_name}.private_key="$priv_key" + uci add_list network.${iface_name}.addresses='10.99.0.2/24' + uci set network.${iface_name}.secubox_uplink='1' + uci set network.${iface_name}.secubox_peer_id="$peer_id" + + # Add peer config + local peer_section=$(uci add network wireguard_${iface_name}) + uci set network.${peer_section}.public_key="$public_key" + uci set network.${peer_section}.endpoint_host="$endpoint_host" + uci set network.${peer_section}.endpoint_port="${endpoint_port:-51820}" + uci add_list network.${peer_section}.allowed_ips='0.0.0.0/0' + uci add_list network.${peer_section}.allowed_ips='::/0' + uci set network.${peer_section}.persistent_keepalive='25' + uci set network.${peer_section}.route_allowed_ips='0' + + uci commit network + + # Add to mwan3 if enabled + local auto_failover=$(uci_get uplink.auto_failover) + if [ "$auto_failover" = "1" ]; then + add_uplink_to_mwan3 "$iface_name" "$peer_id" + fi + + # Bring up interface + ifup "$iface_name" + + log_info "Uplink $peer_id added successfully" + log_info "Your public key (share with peer): $pub_key" +} + +uplink_remove() { + local peer_id="$1" + + if [ -z "$peer_id" ]; then + log_error "Usage: wgctl uplink remove " + exit 1 + fi + + local iface_name="wg_uplink_${peer_id}" + + log_info "Removing uplink peer: $peer_id" + + # Bring down interface + ifdown "$iface_name" 2>/dev/null + + # Remove from mwan3 + remove_uplink_from_mwan3 "$iface_name" + + # Remove network config + uci delete network.${iface_name} 2>/dev/null + + # Remove peer configs + local idx=0 + while uci -q get network.@wireguard_${iface_name}[$idx] >/dev/null 2>&1; do + uci delete network.@wireguard_${iface_name}[$idx] + done + + uci commit network + + log_info "Uplink $peer_id removed" +} + +uplink_status() { + load_uplink_lib || { + log_error "Uplink library not found" + exit 1 + } + + echo "Uplink Failover Status" + echo "======================" + echo "" + + local enabled=$(uci_get uplink.enabled) + local auto_failover=$(uci_get uplink.auto_failover) + local check_interval=$(uci_get uplink.check_interval) + + printf "Enabled: %s\n" "$([ "$enabled" = "1" ] && echo -e "${GREEN}yes${NC}" || echo -e "${RED}no${NC}")" + printf "Auto Failover: %s\n" "$([ "$auto_failover" = "1" ] && echo -e "${GREEN}yes${NC}" || echo -e "${RED}no${NC}")" + printf "Check Interval: %ss\n" "${check_interval:-30}" + echo "" + + # Show primary WAN status + echo "Primary WAN:" + local wan_status=$(ping -c 1 -W 2 8.8.8.8 >/dev/null 2>&1 && echo "up" || echo "down") + printf " Status: %s\n" "$([ "$wan_status" = "up" ] && echo -e "${GREEN}UP${NC}" || echo -e "${RED}DOWN${NC}")" + echo "" + + # Show uplink interfaces + echo "Uplink Interfaces:" + config_load network + + local uplink_count=0 + for iface in $(uci show network 2>/dev/null | grep 'secubox_uplink=.1' | cut -d'.' -f2); do + uplink_count=$((uplink_count + 1)) + local peer_id=$(uci -q get network.${iface}.secubox_peer_id) + local state="down" + ip link show "$iface" 2>/dev/null | grep -q "UP" && state="up" + + # Test connectivity through this uplink + local reachable="no" + if [ "$state" = "up" ]; then + # Ping through specific interface + local gw=$(ip route show dev "$iface" 2>/dev/null | grep default | awk '{print $3}') + if [ -n "$gw" ]; then + ping -c 1 -W 2 -I "$iface" 8.8.8.8 >/dev/null 2>&1 && reachable="yes" + fi + fi + + printf " %s (%s):\n" "$iface" "${peer_id:-unknown}" + printf " State: %s\n" "$([ "$state" = "up" ] && echo -e "${GREEN}UP${NC}" || echo -e "${RED}DOWN${NC}")" + printf " Reachable: %s\n" "$([ "$reachable" = "yes" ] && echo -e "${GREEN}yes${NC}" || echo -e "${RED}no${NC}")" + done + + [ $uplink_count -eq 0 ] && echo " No uplinks configured" + echo "" + + # Show current routing + if command -v mwan3 >/dev/null 2>&1; then + echo "mwan3 Status:" + mwan3 status 2>/dev/null | head -20 + fi +} + +uplink_test() { + local peer_id="$1" + + if [ -z "$peer_id" ]; then + log_error "Usage: wgctl uplink test " + exit 1 + fi + + local iface_name="wg_uplink_${peer_id}" + + log_info "Testing connectivity through $peer_id..." + + # Check interface exists + if ! ip link show "$iface_name" >/dev/null 2>&1; then + log_error "Interface $iface_name not found" + exit 1 + fi + + # Ping through uplink + echo "Pinging 8.8.8.8 through $iface_name..." + if ping -c 3 -W 5 -I "$iface_name" 8.8.8.8; then + echo "" + log_info "Uplink $peer_id is ${GREEN}WORKING${NC}" + else + echo "" + log_error "Uplink $peer_id is ${RED}NOT REACHABLE${NC}" + exit 1 + fi +} + +uplink_failover() { + local action="$1" + + case "$action" in + enable) + log_info "Enabling uplink failover..." + uci_set uplink.enabled '1' + uci_set uplink.auto_failover '1' + + # Setup mwan3 for all configured uplinks + setup_mwan3_failover + + log_info "Uplink failover enabled" + ;; + disable) + log_info "Disabling uplink failover..." + uci_set uplink.enabled '0' + uci_set uplink.auto_failover '0' + + # Remove mwan3 config for uplinks + cleanup_mwan3_failover + + log_info "Uplink failover disabled" + ;; + *) + log_error "Usage: wgctl uplink failover " + exit 1 + ;; + esac +} + +uplink_priority() { + local peer_id="$1" + local weight="$2" + + if [ -z "$peer_id" ] || [ -z "$weight" ]; then + log_error "Usage: wgctl uplink priority " + log_info "Lower weight = lower priority (backup)" + exit 1 + fi + + local iface_name="wg_uplink_${peer_id}" + + # Update mwan3 member weight + local member="${iface_name}_member" + uci set mwan3.${member}.weight="$weight" + uci commit mwan3 + + # Reload mwan3 + /etc/init.d/mwan3 reload 2>/dev/null + + log_info "Priority for $peer_id set to weight $weight" +} + +uplink_offer() { + load_uplink_lib || { + log_error "Uplink library not found" + exit 1 + } + + log_info "Advertising this node as uplink provider..." + + advertise_uplink_offer + + log_info "Uplink offer advertised via gossip" +} + +uplink_withdraw() { + load_uplink_lib || { + log_error "Uplink library not found" + exit 1 + } + + log_info "Withdrawing uplink offer..." + + withdraw_uplink_offer + + log_info "Uplink offer withdrawn" +} + +# ============================================================ +# mwan3 Integration Helpers +# ============================================================ + +add_uplink_to_mwan3() { + local iface="$1" + local peer_id="$2" + + # Check if mwan3 is available + [ ! -f /etc/config/mwan3 ] && return + + log_info "Adding $iface to mwan3..." + + # Add interface + uci set mwan3.${iface}=interface + uci set mwan3.${iface}.enabled='1' + uci set mwan3.${iface}.family='ipv4' + uci set mwan3.${iface}.reliability='1' + uci set mwan3.${iface}.track_method='ping' + uci add_list mwan3.${iface}.track_ip='8.8.8.8' + + # Add member with low weight (backup) + local member="${iface}_member" + uci set mwan3.${member}=member + uci set mwan3.${member}.interface="$iface" + uci set mwan3.${member}.weight='10' + uci set mwan3.${member}.metric='10' + + # Add to failover policy + uci add_list mwan3.failover_policy.use_member="$member" + + uci commit mwan3 + /etc/init.d/mwan3 reload 2>/dev/null +} + +remove_uplink_from_mwan3() { + local iface="$1" + + [ ! -f /etc/config/mwan3 ] && return + + local member="${iface}_member" + + uci delete mwan3.${iface} 2>/dev/null + uci delete mwan3.${member} 2>/dev/null + uci del_list mwan3.failover_policy.use_member="$member" 2>/dev/null + + uci commit mwan3 + /etc/init.d/mwan3 reload 2>/dev/null +} + +setup_mwan3_failover() { + [ ! -f /etc/config/mwan3 ] && return + + # Ensure failover policy exists + if ! uci -q get mwan3.failover_policy >/dev/null; then + uci set mwan3.failover_policy=policy + uci add_list mwan3.failover_policy.use_member='wan_member' + fi + + # Add all uplink interfaces + config_load network + for iface in $(uci show network 2>/dev/null | grep 'secubox_uplink=.1' | cut -d'.' -f2); do + local peer_id=$(uci -q get network.${iface}.secubox_peer_id) + add_uplink_to_mwan3 "$iface" "$peer_id" + done +} + +cleanup_mwan3_failover() { + [ ! -f /etc/config/mwan3 ] && return + + # Remove all uplink interfaces from mwan3 + for iface in $(uci show network 2>/dev/null | grep 'secubox_uplink=.1' | cut -d'.' -f2); do + remove_uplink_from_mwan3 "$iface" + done +} + +# ============================================================ +# Main +# ============================================================ + +case "${1:-}" in + status) + shift; cmd_status "$@" ;; + show) + shift; cmd_show "$@" ;; + up) + shift; cmd_up "$@" ;; + down) + shift; cmd_down "$@" ;; + uplink) + shift + case "${1:-}" in + list) shift; uplink_list "$@" ;; + add) shift; uplink_add "$@" ;; + remove) shift; uplink_remove "$@" ;; + status) shift; uplink_status "$@" ;; + test) shift; uplink_test "$@" ;; + failover) shift; uplink_failover "$@" ;; + priority) shift; uplink_priority "$@" ;; + offer) shift; uplink_offer "$@" ;; + withdraw) shift; uplink_withdraw "$@" ;; + *) log_error "Unknown uplink command: $1"; usage; exit 1 ;; + esac + ;; + -h|--help|help) + usage ;; + *) + usage ;; +esac diff --git a/package/secubox/luci-app-wireguard-dashboard/root/usr/share/rpcd/acl.d/luci-app-wireguard-dashboard.json b/package/secubox/luci-app-wireguard-dashboard/root/usr/share/rpcd/acl.d/luci-app-wireguard-dashboard.json index 6425a6ce..4e7426ed 100644 --- a/package/secubox/luci-app-wireguard-dashboard/root/usr/share/rpcd/acl.d/luci-app-wireguard-dashboard.json +++ b/package/secubox/luci-app-wireguard-dashboard/root/usr/share/rpcd/acl.d/luci-app-wireguard-dashboard.json @@ -13,15 +13,18 @@ "bandwidth_rates", "bandwidth_history", "endpoint_info", - "get_endpoints" + "get_endpoints", + "uplink_status", + "uplinks" ], "system": [ "info", "board" ], "file": [ "read", "stat", "exec" ] }, - "uci": [ "network", "wireguard_dashboard" ], + "uci": [ "network", "wireguard_dashboard", "wireguard_uplink", "mwan3" ], "file": { "/etc/config/network": [ "read" ], "/etc/config/wireguard_dashboard": [ "read" ], + "/etc/config/wireguard_uplink": [ "read" ], "/usr/bin/wg": [ "exec" ] } }, @@ -38,10 +41,17 @@ "ping_peer", "set_endpoint", "set_default_endpoint", - "delete_endpoint" + "delete_endpoint", + "add_uplink", + "remove_uplink", + "test_uplink", + "offer_uplink", + "withdraw_uplink", + "set_uplink_priority", + "set_uplink_failover" ] }, - "uci": [ "wireguard_dashboard", "network" ] + "uci": [ "wireguard_dashboard", "network", "wireguard_uplink", "mwan3" ] } } }