WireGuard mesh peers as backup internet uplinks via mwan3 failover.
CLI (wgctl) uplink commands:
- uplink list/add/remove/status/test - Manage peer uplinks
- uplink failover enable/disable - Toggle automatic failover
- uplink priority/offer/withdraw - Priority and mesh advertising
Uplink Library (/usr/lib/wireguard-dashboard/uplink.sh):
- Gossip protocol integration via secubox-p2p
- WireGuard interface creation with IP allocation (172.31.x.x/16)
- mwan3 failover integration
- Connectivity testing and latency measurement
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
Phase 2 pending: LuCI dashboard integration
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1637 lines
41 KiB
Bash
Executable File
1637 lines
41 KiB
Bash
Executable File
#!/bin/sh
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
# WireGuard Dashboard RPCD backend
|
|
# Copyright (C) 2024 CyberMind.fr - Gandalf
|
|
|
|
. /lib/functions.sh
|
|
. /usr/share/libubox/jshn.sh
|
|
|
|
WG_CMD="/usr/bin/wg"
|
|
|
|
# Check if wireguard-tools is available
|
|
check_wg() {
|
|
if [ ! -x "$WG_CMD" ]; then
|
|
echo '{"error": "wireguard-tools not installed"}'
|
|
exit 0
|
|
fi
|
|
}
|
|
|
|
# Get overall status
|
|
get_status() {
|
|
json_init
|
|
|
|
# Check if wg command exists
|
|
if [ ! -x "$WG_CMD" ]; then
|
|
json_add_boolean "installed" 0
|
|
json_add_string "error" "wireguard-tools not installed"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
json_add_boolean "installed" 1
|
|
|
|
# Get all interfaces
|
|
local interfaces=$($WG_CMD show interfaces 2>/dev/null)
|
|
local iface_count=0
|
|
local peer_count=0
|
|
local total_rx=0
|
|
local total_tx=0
|
|
local active_peers=0
|
|
|
|
json_add_array "interfaces"
|
|
|
|
for iface in $interfaces; do
|
|
iface_count=$((iface_count + 1))
|
|
|
|
# Get interface details
|
|
local pubkey=$($WG_CMD show $iface public-key 2>/dev/null)
|
|
local port=$($WG_CMD show $iface listen-port 2>/dev/null)
|
|
local fwmark=$($WG_CMD show $iface fwmark 2>/dev/null)
|
|
|
|
# Get interface IP from network config
|
|
local ip=$(ip -4 addr show $iface 2>/dev/null | grep -oP 'inet \K[\d.]+')
|
|
|
|
# Check if interface is up
|
|
local state="down"
|
|
if ip link show $iface 2>/dev/null | grep -q "UP"; then
|
|
state="up"
|
|
fi
|
|
|
|
json_add_object
|
|
json_add_string "name" "$iface"
|
|
json_add_string "public_key" "$pubkey"
|
|
json_add_int "listen_port" "${port:-0}"
|
|
json_add_string "ip_address" "${ip:-N/A}"
|
|
json_add_string "state" "$state"
|
|
json_add_string "fwmark" "${fwmark:-off}"
|
|
|
|
# Count peers for this interface
|
|
local iface_peers=$($WG_CMD show $iface peers 2>/dev/null | wc -l)
|
|
json_add_int "peer_count" "$iface_peers"
|
|
peer_count=$((peer_count + iface_peers))
|
|
|
|
# Get transfer totals for interface
|
|
local transfer=$($WG_CMD show $iface transfer 2>/dev/null)
|
|
local iface_rx=0
|
|
local iface_tx=0
|
|
while read peer rx tx; do
|
|
[ -n "$rx" ] && iface_rx=$((iface_rx + ${rx:-0}))
|
|
[ -n "$tx" ] && iface_tx=$((iface_tx + ${tx:-0}))
|
|
done << EOF
|
|
$transfer
|
|
EOF
|
|
|
|
json_add_int "rx_bytes" "$iface_rx"
|
|
json_add_int "tx_bytes" "$iface_tx"
|
|
json_close_object
|
|
done
|
|
|
|
json_close_array
|
|
|
|
# Calculate totals from all interfaces
|
|
for iface in $interfaces; do
|
|
local transfer=$($WG_CMD show $iface transfer 2>/dev/null)
|
|
while read peer rx tx; do
|
|
[ -n "$rx" ] && total_rx=$((total_rx + rx))
|
|
[ -n "$tx" ] && total_tx=$((total_tx + tx))
|
|
done << EOF
|
|
$transfer
|
|
EOF
|
|
|
|
# Check for recent handshakes (within last 3 minutes = 180 seconds)
|
|
local handshakes=$($WG_CMD show $iface latest-handshakes 2>/dev/null)
|
|
local now=$(date +%s)
|
|
while read peer hs; do
|
|
if [ -n "$hs" ] && [ "$hs" != "0" ]; then
|
|
local age=$((now - hs))
|
|
if [ $age -lt 180 ]; then
|
|
active_peers=$((active_peers + 1))
|
|
fi
|
|
fi
|
|
done << EOF
|
|
$handshakes
|
|
EOF
|
|
done
|
|
|
|
json_add_int "interface_count" "$iface_count"
|
|
json_add_int "total_peers" "$peer_count"
|
|
json_add_int "active_peers" "$active_peers"
|
|
json_add_int "total_rx" "$total_rx"
|
|
json_add_int "total_tx" "$total_tx"
|
|
|
|
# Uptime - get from first interface creation
|
|
local uptime=0
|
|
for iface in $interfaces; do
|
|
if [ -d "/sys/class/net/$iface" ]; then
|
|
# Approximate from interface stats
|
|
uptime=$(cat /proc/uptime | cut -d. -f1)
|
|
break
|
|
fi
|
|
done
|
|
json_add_int "uptime" "$uptime"
|
|
|
|
json_dump
|
|
}
|
|
|
|
# Get all interfaces with details
|
|
get_interfaces() {
|
|
json_init
|
|
json_add_array "interfaces"
|
|
|
|
local interfaces=$($WG_CMD show interfaces 2>/dev/null)
|
|
|
|
for iface in $interfaces; do
|
|
local pubkey=$($WG_CMD show $iface public-key 2>/dev/null)
|
|
local privkey_exists="yes"
|
|
local port=$($WG_CMD show $iface listen-port 2>/dev/null)
|
|
local fwmark=$($WG_CMD show $iface fwmark 2>/dev/null)
|
|
|
|
# Get IP addresses
|
|
local ipv4=$(ip -4 addr show $iface 2>/dev/null | grep -oP 'inet \K[\d./]+')
|
|
local ipv6=$(ip -6 addr show $iface 2>/dev/null | grep -oP 'inet6 \K[a-f0-9:/]+' | head -1)
|
|
|
|
# Interface state
|
|
local state="down"
|
|
local mtu=1420
|
|
if ip link show $iface 2>/dev/null | grep -q "UP"; then
|
|
state="up"
|
|
mtu=$(cat /sys/class/net/$iface/mtu 2>/dev/null || echo 1420)
|
|
fi
|
|
|
|
# Get RX/TX bytes from /sys
|
|
local rx_bytes=$(cat /sys/class/net/$iface/statistics/rx_bytes 2>/dev/null || echo 0)
|
|
local tx_bytes=$(cat /sys/class/net/$iface/statistics/tx_bytes 2>/dev/null || echo 0)
|
|
|
|
json_add_object
|
|
json_add_string "name" "$iface"
|
|
json_add_string "public_key" "$pubkey"
|
|
json_add_boolean "private_key_set" 1
|
|
json_add_int "listen_port" "${port:-0}"
|
|
json_add_string "fwmark" "${fwmark:-off}"
|
|
json_add_string "ipv4_address" "${ipv4:-N/A}"
|
|
json_add_string "ipv6_address" "${ipv6:-N/A}"
|
|
json_add_string "state" "$state"
|
|
json_add_int "mtu" "$mtu"
|
|
json_add_int "rx_bytes" "$rx_bytes"
|
|
json_add_int "tx_bytes" "$tx_bytes"
|
|
json_close_object
|
|
done
|
|
|
|
json_close_array
|
|
json_dump
|
|
}
|
|
|
|
# Get all peers with details
|
|
get_peers() {
|
|
json_init
|
|
json_add_array "peers"
|
|
|
|
local interfaces=$($WG_CMD show interfaces 2>/dev/null)
|
|
|
|
for iface in $interfaces; do
|
|
local peers=$($WG_CMD show $iface peers 2>/dev/null)
|
|
local endpoints=$($WG_CMD show $iface endpoints 2>/dev/null)
|
|
local allowed_ips=$($WG_CMD show $iface allowed-ips 2>/dev/null)
|
|
local handshakes=$($WG_CMD show $iface latest-handshakes 2>/dev/null)
|
|
local transfer=$($WG_CMD show $iface transfer 2>/dev/null)
|
|
local psk=$($WG_CMD show $iface preshared-keys 2>/dev/null)
|
|
local keepalive=$($WG_CMD show $iface persistent-keepalive 2>/dev/null)
|
|
|
|
local now=$(date +%s)
|
|
|
|
for peer in $peers; do
|
|
# Get endpoint
|
|
local endpoint=$(echo "$endpoints" | grep "^$peer" | awk '{print $2}')
|
|
|
|
# Get allowed IPs
|
|
local ips=$(echo "$allowed_ips" | grep "^$peer" | cut -f2-)
|
|
|
|
# Get latest handshake
|
|
local hs=$(echo "$handshakes" | grep "^$peer" | awk '{print $2}')
|
|
local hs_ago=0
|
|
local status="inactive"
|
|
if [ -n "$hs" ] && [ "$hs" != "0" ]; then
|
|
hs_ago=$((now - hs))
|
|
if [ $hs_ago -lt 180 ]; then
|
|
status="active"
|
|
elif [ $hs_ago -lt 600 ]; then
|
|
status="idle"
|
|
fi
|
|
fi
|
|
|
|
# Get transfer
|
|
local rx=0
|
|
local tx=0
|
|
local tr=$(echo "$transfer" | grep "^$peer")
|
|
if [ -n "$tr" ]; then
|
|
rx=$(echo "$tr" | awk '{print $2}')
|
|
tx=$(echo "$tr" | awk '{print $3}')
|
|
fi
|
|
|
|
# Get preshared key status
|
|
local has_psk=0
|
|
if echo "$psk" | grep -q "^$peer.*[^(none)]"; then
|
|
has_psk=1
|
|
fi
|
|
|
|
# Get keepalive
|
|
local ka=$(echo "$keepalive" | grep "^$peer" | awk '{print $2}')
|
|
|
|
# Truncate public key for display name
|
|
local short_key=$(echo "$peer" | cut -c1-8)
|
|
|
|
json_add_object
|
|
json_add_string "interface" "$iface"
|
|
json_add_string "public_key" "$peer"
|
|
json_add_string "short_key" "$short_key"
|
|
json_add_string "endpoint" "${endpoint:-(none)}"
|
|
json_add_string "allowed_ips" "${ips:-N/A}"
|
|
json_add_int "latest_handshake" "${hs:-0}"
|
|
json_add_int "handshake_ago" "$hs_ago"
|
|
json_add_string "status" "$status"
|
|
json_add_int "rx_bytes" "${rx:-0}"
|
|
json_add_int "tx_bytes" "${tx:-0}"
|
|
json_add_boolean "preshared_key" "$has_psk"
|
|
json_add_int "keepalive" "${ka:-0}"
|
|
json_close_object
|
|
done
|
|
done
|
|
|
|
json_close_array
|
|
json_dump
|
|
}
|
|
|
|
# Get traffic statistics
|
|
get_traffic() {
|
|
json_init
|
|
|
|
local interfaces=$($WG_CMD show interfaces 2>/dev/null)
|
|
local total_rx=0
|
|
local total_tx=0
|
|
|
|
json_add_array "interfaces"
|
|
|
|
for iface in $interfaces; do
|
|
local iface_rx=0
|
|
local iface_tx=0
|
|
|
|
# Get per-peer transfer
|
|
local transfer=$($WG_CMD show $iface transfer 2>/dev/null)
|
|
|
|
json_add_object
|
|
json_add_string "name" "$iface"
|
|
|
|
json_add_array "peers"
|
|
while read peer rx tx; do
|
|
if [ -n "$peer" ]; then
|
|
json_add_object
|
|
json_add_string "public_key" "$peer"
|
|
json_add_string "short_key" "$(echo $peer | cut -c1-8)"
|
|
json_add_int "rx_bytes" "${rx:-0}"
|
|
json_add_int "tx_bytes" "${tx:-0}"
|
|
json_close_object
|
|
|
|
iface_rx=$((iface_rx + ${rx:-0}))
|
|
iface_tx=$((iface_tx + ${tx:-0}))
|
|
fi
|
|
done << EOF
|
|
$transfer
|
|
EOF
|
|
json_close_array
|
|
|
|
json_add_int "total_rx" "$iface_rx"
|
|
json_add_int "total_tx" "$iface_tx"
|
|
json_close_object
|
|
|
|
total_rx=$((total_rx + iface_rx))
|
|
total_tx=$((total_tx + iface_tx))
|
|
done
|
|
|
|
json_close_array
|
|
|
|
json_add_int "total_rx" "$total_rx"
|
|
json_add_int "total_tx" "$total_tx"
|
|
|
|
json_dump
|
|
}
|
|
|
|
# Get configuration info (sanitized - no private keys)
|
|
get_config() {
|
|
json_init
|
|
json_add_array "interfaces"
|
|
|
|
local interfaces=$($WG_CMD show interfaces 2>/dev/null)
|
|
|
|
for iface in $interfaces; do
|
|
json_add_object
|
|
json_add_string "name" "$iface"
|
|
json_add_string "public_key" "$($WG_CMD show $iface public-key 2>/dev/null)"
|
|
json_add_int "listen_port" "$($WG_CMD show $iface listen-port 2>/dev/null || echo 0)"
|
|
|
|
# Get UCI config if available
|
|
local uci_proto=$(uci -q get network.$iface.proto)
|
|
local uci_addr=$(uci -q get network.$iface.addresses)
|
|
|
|
json_add_string "uci_proto" "${uci_proto:-N/A}"
|
|
json_add_string "uci_addresses" "${uci_addr:-N/A}"
|
|
|
|
# Peers config
|
|
json_add_array "peers"
|
|
local peers=$($WG_CMD show $iface peers 2>/dev/null)
|
|
local allowed=$($WG_CMD show $iface allowed-ips 2>/dev/null)
|
|
local endpoints=$($WG_CMD show $iface endpoints 2>/dev/null)
|
|
local keepalive=$($WG_CMD show $iface persistent-keepalive 2>/dev/null)
|
|
|
|
for peer in $peers; do
|
|
local ips=$(echo "$allowed" | grep "^$peer" | cut -f2-)
|
|
local ep=$(echo "$endpoints" | grep "^$peer" | awk '{print $2}')
|
|
local ka=$(echo "$keepalive" | grep "^$peer" | awk '{print $2}')
|
|
|
|
json_add_object
|
|
json_add_string "public_key" "$peer"
|
|
json_add_string "allowed_ips" "${ips:-N/A}"
|
|
json_add_string "endpoint" "${ep:-(none)}"
|
|
json_add_int "keepalive" "${ka:-0}"
|
|
json_close_object
|
|
done
|
|
json_close_array
|
|
|
|
json_close_object
|
|
done
|
|
|
|
json_close_array
|
|
json_dump
|
|
}
|
|
|
|
# Generate keys for new peer
|
|
generate_keys() {
|
|
json_init
|
|
|
|
if [ ! -x "$WG_CMD" ]; then
|
|
json_add_string "error" "wireguard-tools not installed"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Generate private key
|
|
local privkey=$(wg genkey)
|
|
# Generate public key from private key
|
|
local pubkey=$(echo "$privkey" | wg pubkey)
|
|
# Generate preshared key
|
|
local psk=$(wg genpsk)
|
|
|
|
json_add_string "private_key" "$privkey"
|
|
json_add_string "public_key" "$pubkey"
|
|
json_add_string "preshared_key" "$psk"
|
|
|
|
json_dump
|
|
}
|
|
|
|
# Create a new WireGuard interface
|
|
create_interface() {
|
|
read input
|
|
json_load "$input"
|
|
json_get_var name name
|
|
json_get_var private_key private_key
|
|
json_get_var listen_port listen_port
|
|
json_get_var addresses addresses
|
|
json_get_var mtu mtu
|
|
|
|
json_init
|
|
|
|
# Validate required fields
|
|
if [ -z "$name" ] || [ -z "$private_key" ]; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Missing required fields: name and private_key"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Check if interface already exists
|
|
if uci -q get network.$name >/dev/null 2>&1; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Interface $name already exists"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Validate private key format (base64, 44 chars ending with =)
|
|
if ! echo "$private_key" | grep -qE '^[A-Za-z0-9+/]{43}=$'; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Invalid private key format"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Default values
|
|
[ -z "$listen_port" ] && listen_port="51820"
|
|
[ -z "$mtu" ] && mtu="1420"
|
|
|
|
# Create the interface in UCI
|
|
uci set network.$name=interface
|
|
uci set network.$name.proto='wireguard'
|
|
uci set network.$name.private_key="$private_key"
|
|
uci set network.$name.listen_port="$listen_port"
|
|
uci set network.$name.mtu="$mtu"
|
|
|
|
# Set addresses (can be multiple)
|
|
if [ -n "$addresses" ]; then
|
|
uci delete network.$name.addresses 2>/dev/null
|
|
for addr in $addresses; do
|
|
uci add_list network.$name.addresses="$addr"
|
|
done
|
|
fi
|
|
|
|
# Commit changes
|
|
if uci commit network; then
|
|
# Create firewall zone for the interface
|
|
local zone_name="wg_${name}"
|
|
local zone_exists=$(uci show firewall 2>/dev/null | grep "\.name='$zone_name'" | head -1)
|
|
|
|
if [ -z "$zone_exists" ]; then
|
|
# Add new firewall zone
|
|
local zone_section=$(uci add firewall zone)
|
|
uci set firewall.$zone_section.name="$zone_name"
|
|
uci set firewall.$zone_section.input='ACCEPT'
|
|
uci set firewall.$zone_section.output='ACCEPT'
|
|
uci set firewall.$zone_section.forward='ACCEPT'
|
|
uci add_list firewall.$zone_section.network="$name"
|
|
|
|
# Add forwarding to lan
|
|
local fwd_section=$(uci add firewall forwarding)
|
|
uci set firewall.$fwd_section.src="$zone_name"
|
|
uci set firewall.$fwd_section.dest='lan'
|
|
|
|
# Add forwarding from lan
|
|
local fwd2_section=$(uci add firewall forwarding)
|
|
uci set firewall.$fwd2_section.src='lan'
|
|
uci set firewall.$fwd2_section.dest="$zone_name"
|
|
|
|
# Add rule to allow WireGuard port from wan
|
|
local rule_section=$(uci add firewall rule)
|
|
uci set firewall.$rule_section.name="Allow-WireGuard-$name"
|
|
uci set firewall.$rule_section.src='wan'
|
|
uci set firewall.$rule_section.dest_port="$listen_port"
|
|
uci set firewall.$rule_section.proto='udp'
|
|
uci set firewall.$rule_section.target='ACCEPT'
|
|
|
|
uci commit firewall
|
|
fi
|
|
|
|
# Bring up the interface
|
|
ifup "$name" 2>/dev/null &
|
|
|
|
# Reload firewall
|
|
/etc/init.d/firewall reload 2>/dev/null &
|
|
|
|
json_add_boolean "success" 1
|
|
json_add_string "message" "Interface $name created successfully"
|
|
json_add_string "interface" "$name"
|
|
else
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Failed to commit network configuration"
|
|
fi
|
|
|
|
json_dump
|
|
}
|
|
|
|
# Add a new peer to interface
|
|
add_peer() {
|
|
read input
|
|
json_load "$input"
|
|
json_get_var iface interface
|
|
json_get_var name name
|
|
json_get_var allowed_ips allowed_ips
|
|
json_get_var public_key public_key
|
|
json_get_var psk preshared_key
|
|
json_get_var endpoint endpoint
|
|
json_get_var keepalive persistent_keepalive
|
|
json_get_var client_privkey private_key
|
|
|
|
json_init
|
|
|
|
# Validate inputs
|
|
if [ -z "$iface" ] || [ -z "$public_key" ]; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Missing required fields: interface and public_key"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Validate public key format (base64, 44 chars ending with =)
|
|
if ! echo "$public_key" | grep -qE '^[A-Za-z0-9+/]{43}=$'; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Invalid public key format"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Check if peer already exists
|
|
local existing=$(uci show network 2>/dev/null | grep "public_key='$public_key'" | head -1)
|
|
if [ -n "$existing" ]; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Peer with this public key already exists"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Default values
|
|
[ -z "$allowed_ips" ] && allowed_ips="10.0.0.2/32"
|
|
[ -z "$keepalive" ] && keepalive="25"
|
|
[ -z "$name" ] && name="peer_$(echo $public_key | cut -c1-8)"
|
|
|
|
# Create UCI section for peer using hash of full public key for uniqueness
|
|
local section_name="wgpeer_$(echo "$public_key" | md5sum | cut -c1-12)"
|
|
|
|
# Add peer to UCI network config
|
|
uci -q delete network.$section_name
|
|
uci set network.$section_name=wireguard_$iface
|
|
uci set network.$section_name.public_key="$public_key"
|
|
uci set network.$section_name.description="$name"
|
|
|
|
# Set allowed IPs (can be multiple)
|
|
uci delete network.$section_name.allowed_ips 2>/dev/null
|
|
for ip in $allowed_ips; do
|
|
uci add_list network.$section_name.allowed_ips="$ip"
|
|
done
|
|
|
|
# Optional: preshared key
|
|
if [ -n "$psk" ] && [ "$psk" != "none" ]; then
|
|
uci set network.$section_name.preshared_key="$psk"
|
|
fi
|
|
|
|
# Optional: endpoint
|
|
if [ -n "$endpoint" ] && [ "$endpoint" != "none" ]; then
|
|
local ep_host=$(echo "$endpoint" | cut -d: -f1)
|
|
local ep_port=$(echo "$endpoint" | cut -d: -f2)
|
|
uci set network.$section_name.endpoint_host="$ep_host"
|
|
uci set network.$section_name.endpoint_port="${ep_port:-51820}"
|
|
fi
|
|
|
|
# Persistent keepalive
|
|
if [ -n "$keepalive" ] && [ "$keepalive" != "0" ]; then
|
|
uci set network.$section_name.persistent_keepalive="$keepalive"
|
|
fi
|
|
|
|
# Store client private key for QR/config generation
|
|
if [ -n "$client_privkey" ]; then
|
|
uci set network.$section_name._client_private_key="$client_privkey"
|
|
fi
|
|
|
|
# Route allowed IPs
|
|
uci set network.$section_name.route_allowed_ips="1"
|
|
|
|
# Commit changes
|
|
uci commit network
|
|
|
|
# Reload interface to apply
|
|
ifup "$iface" 2>/dev/null
|
|
|
|
json_add_boolean "success" 1
|
|
json_add_string "message" "Peer added successfully"
|
|
json_add_string "section" "$section_name"
|
|
json_add_string "peer_name" "$name"
|
|
|
|
json_dump
|
|
}
|
|
|
|
# Remove a peer from interface
|
|
remove_peer() {
|
|
read input
|
|
json_load "$input"
|
|
json_get_var iface interface
|
|
json_get_var public_key public_key
|
|
|
|
json_init
|
|
|
|
if [ -z "$iface" ] || [ -z "$public_key" ]; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Missing interface or public_key"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Validate public key format (base64, 44 chars ending with =)
|
|
if ! echo "$public_key" | grep -qE '^[A-Za-z0-9+/]{43}=$'; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Invalid public key format"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Find UCI section by iterating directly
|
|
local found_section=""
|
|
local sections=$(uci show network 2>/dev/null | grep "=wireguard_$iface$" | cut -d'.' -f2 | cut -d'=' -f1)
|
|
|
|
for section in $sections; do
|
|
local key=$(uci -q get network.$section.public_key)
|
|
if [ "$key" = "$public_key" ]; then
|
|
found_section="$section"
|
|
break
|
|
fi
|
|
done
|
|
|
|
if [ -n "$found_section" ]; then
|
|
uci delete network.$found_section
|
|
if uci commit network; then
|
|
ifup "$iface" 2>/dev/null
|
|
json_add_boolean "success" 1
|
|
json_add_string "message" "Peer removed successfully"
|
|
json_add_string "section" "$found_section"
|
|
else
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Failed to commit changes"
|
|
fi
|
|
else
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Peer not found in configuration"
|
|
fi
|
|
|
|
json_dump
|
|
}
|
|
|
|
# Generate client configuration file
|
|
generate_config() {
|
|
read input
|
|
json_load "$input"
|
|
json_get_var iface interface
|
|
json_get_var peer_key peer
|
|
json_get_var peer_privkey private_key
|
|
json_get_var server_endpoint endpoint
|
|
|
|
json_init
|
|
|
|
# If private key not provided, look it up from UCI
|
|
if [ -z "$peer_privkey" ]; then
|
|
local sections=$(uci show network 2>/dev/null | grep "=wireguard_$iface$" | cut -d'.' -f2 | cut -d'=' -f1)
|
|
for section in $sections; do
|
|
local key=$(uci -q get network.$section.public_key)
|
|
if [ "$key" = "$peer_key" ]; then
|
|
peer_privkey=$(uci -q get network.$section._client_private_key)
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ -z "$peer_privkey" ]; then
|
|
json_add_string "error" "Private key not available. Please provide it manually."
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Get interface details
|
|
local server_pubkey=$($WG_CMD show $iface public-key 2>/dev/null)
|
|
local server_port=$($WG_CMD show $iface listen-port 2>/dev/null)
|
|
|
|
# Get peer allowed IPs
|
|
local allowed=$($WG_CMD show $iface allowed-ips 2>/dev/null | grep "^$peer_key" | cut -f2-)
|
|
|
|
# Get preshared key if exists
|
|
local psk=$($WG_CMD show $iface preshared-keys 2>/dev/null | grep "^$peer_key" | awk '{print $2}')
|
|
|
|
# Get DNS servers from interface
|
|
local dns=$(uci -q get network.$iface.dns || echo "1.1.1.1, 1.0.0.1")
|
|
|
|
# Build configuration file content
|
|
local config="[Interface]
|
|
PrivateKey = ${peer_privkey}
|
|
Address = ${allowed}
|
|
DNS = ${dns}
|
|
|
|
[Peer]
|
|
PublicKey = ${server_pubkey}
|
|
Endpoint = ${server_endpoint}:${server_port}
|
|
AllowedIPs = 0.0.0.0/0, ::/0
|
|
PersistentKeepalive = 25"
|
|
|
|
# Add preshared key if exists
|
|
if [ -n "$psk" ] && [ "$psk" != "(none)" ]; then
|
|
config="${config}
|
|
PresharedKey = ${psk}"
|
|
fi
|
|
|
|
json_add_string "config" "$config"
|
|
json_add_string "filename" "wireguard_${iface}_client.conf"
|
|
|
|
json_dump
|
|
}
|
|
|
|
# Generate QR code PNG (base64) for peer config
|
|
generate_qr() {
|
|
read input
|
|
json_load "$input"
|
|
json_get_var iface interface
|
|
json_get_var peer_key peer
|
|
json_get_var peer_privkey private_key
|
|
json_get_var server_endpoint endpoint
|
|
|
|
json_init
|
|
|
|
if [ ! -x "/usr/bin/qrencode" ]; then
|
|
json_add_string "error" "qrencode not installed (opkg install qrencode)"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# If private key not provided, look it up from UCI
|
|
if [ -z "$peer_privkey" ]; then
|
|
local sections=$(uci show network 2>/dev/null | grep "=wireguard_$iface$" | cut -d'.' -f2 | cut -d'=' -f1)
|
|
for section in $sections; do
|
|
local key=$(uci -q get network.$section.public_key)
|
|
if [ "$key" = "$peer_key" ]; then
|
|
peer_privkey=$(uci -q get network.$section._client_private_key)
|
|
break
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ -z "$peer_privkey" ]; then
|
|
json_add_string "error" "Private key not available. Please provide it manually."
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Get interface details
|
|
local server_pubkey=$($WG_CMD show $iface public-key 2>/dev/null)
|
|
local server_port=$($WG_CMD show $iface listen-port 2>/dev/null)
|
|
|
|
# Get peer allowed IPs
|
|
local allowed=$($WG_CMD show $iface allowed-ips 2>/dev/null | grep "^$peer_key" | cut -f2-)
|
|
|
|
# Get preshared key if exists
|
|
local psk=$($WG_CMD show $iface preshared-keys 2>/dev/null | grep "^$peer_key" | awk '{print $2}')
|
|
|
|
# Get DNS servers
|
|
local dns=$(uci -q get network.$iface.dns || echo "1.1.1.1, 1.0.0.1")
|
|
|
|
# Build endpoint - only add port if not already present
|
|
local full_endpoint="$server_endpoint"
|
|
case "$server_endpoint" in
|
|
*:*) ;; # Already has port
|
|
*) full_endpoint="${server_endpoint}:${server_port}" ;;
|
|
esac
|
|
|
|
# Build configuration
|
|
local config="[Interface]
|
|
PrivateKey = ${peer_privkey}
|
|
Address = ${allowed}
|
|
DNS = ${dns}
|
|
|
|
[Peer]
|
|
PublicKey = ${server_pubkey}
|
|
Endpoint = ${full_endpoint}
|
|
AllowedIPs = 0.0.0.0/0, ::/0
|
|
PersistentKeepalive = 25"
|
|
|
|
if [ -n "$psk" ] && [ "$psk" != "(none)" ]; then
|
|
config="${config}
|
|
PresharedKey = ${psk}"
|
|
fi
|
|
|
|
# Generate QR code as SVG and convert to base64
|
|
# Build JSON manually to avoid jshn "Argument list too long" with large base64 strings
|
|
local tmpfile="/tmp/wg_qr_$$.json"
|
|
local config_escaped=$(printf '%s' "$config" | sed 's/\\/\\\\/g; s/"/\\"/g' | awk '{printf "%s\\n", $0}' | sed '$ s/\\n$//')
|
|
|
|
printf '{"qrcode":"data:image/svg+xml;base64,' > "$tmpfile"
|
|
echo "$config" | qrencode -t SVG -o - | base64 -w 0 >> "$tmpfile"
|
|
printf '","config":"%s"}\n' "$config_escaped" >> "$tmpfile"
|
|
|
|
cat "$tmpfile"
|
|
rm -f "$tmpfile"
|
|
}
|
|
|
|
# Get bandwidth history (if vnstat available)
|
|
get_bandwidth_history() {
|
|
json_init
|
|
|
|
local interfaces=$($WG_CMD show interfaces 2>/dev/null)
|
|
|
|
json_add_array "history"
|
|
|
|
for iface in $interfaces; do
|
|
if [ -x "/usr/bin/vnstat" ]; then
|
|
# Get hourly data from vnstat if available
|
|
local vnstat_data=$(vnstat -i $iface --json h 2>/dev/null)
|
|
if [ -n "$vnstat_data" ]; then
|
|
json_add_object
|
|
json_add_string "interface" "$iface"
|
|
json_add_string "source" "vnstat"
|
|
# Parse vnstat JSON here
|
|
json_close_object
|
|
fi
|
|
fi
|
|
|
|
# Fallback: get current stats from /sys
|
|
local rx=$(cat /sys/class/net/$iface/statistics/rx_bytes 2>/dev/null || echo 0)
|
|
local tx=$(cat /sys/class/net/$iface/statistics/tx_bytes 2>/dev/null || echo 0)
|
|
|
|
json_add_object
|
|
json_add_string "interface" "$iface"
|
|
json_add_string "source" "sysfs"
|
|
json_add_int "rx_bytes" "$rx"
|
|
json_add_int "tx_bytes" "$tx"
|
|
json_add_int "timestamp" "$(date +%s)"
|
|
json_close_object
|
|
done
|
|
|
|
json_close_array
|
|
json_dump
|
|
}
|
|
|
|
# Get endpoint geolocation (simplified, based on IP ranges)
|
|
get_endpoint_info() {
|
|
read input
|
|
json_load "$input"
|
|
json_get_var endpoint endpoint
|
|
|
|
json_init
|
|
|
|
# Extract IP from endpoint
|
|
local ip=$(echo "$endpoint" | cut -d: -f1)
|
|
|
|
if [ -z "$ip" ] || [ "$ip" = "(none)" ]; then
|
|
json_add_string "error" "No endpoint"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
json_add_string "ip" "$ip"
|
|
|
|
# Check if private IP
|
|
case "$ip" in
|
|
10.*|172.16.*|172.17.*|172.18.*|172.19.*|172.2[0-9].*|172.3[01].*|192.168.*)
|
|
json_add_string "type" "private"
|
|
json_add_string "location" "Local Network"
|
|
;;
|
|
127.*)
|
|
json_add_string "type" "loopback"
|
|
json_add_string "location" "Localhost"
|
|
;;
|
|
*)
|
|
json_add_string "type" "public"
|
|
json_add_string "location" "Internet"
|
|
# Could integrate with GeoIP service here
|
|
;;
|
|
esac
|
|
|
|
# Reverse DNS lookup
|
|
local hostname=$(nslookup "$ip" 2>/dev/null | grep "name = " | awk '{print $NF}' | sed 's/\.$//')
|
|
json_add_string "hostname" "${hostname:-N/A}"
|
|
|
|
json_dump
|
|
}
|
|
|
|
# Check connectivity to peer
|
|
ping_peer() {
|
|
read input
|
|
json_load "$input"
|
|
json_get_var peer_ip ip
|
|
|
|
json_init
|
|
|
|
if [ -z "$peer_ip" ]; then
|
|
json_add_string "error" "No IP provided"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Single ping with 2 second timeout
|
|
local result=$(ping -c 1 -W 2 "$peer_ip" 2>/dev/null)
|
|
local rc=$?
|
|
|
|
if [ $rc -eq 0 ]; then
|
|
local rtt=$(echo "$result" | grep "time=" | sed 's/.*time=\([0-9.]*\).*/\1/')
|
|
json_add_boolean "reachable" 1
|
|
json_add_string "rtt_ms" "$rtt"
|
|
else
|
|
json_add_boolean "reachable" 0
|
|
json_add_string "rtt_ms" "N/A"
|
|
fi
|
|
|
|
json_add_string "target" "$peer_ip"
|
|
json_dump
|
|
}
|
|
|
|
# Interface up/down control
|
|
interface_control() {
|
|
read input
|
|
json_load "$input"
|
|
json_get_var iface interface
|
|
json_get_var action action
|
|
|
|
json_init
|
|
|
|
if [ -z "$iface" ] || [ -z "$action" ]; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Missing interface or action"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Validate action
|
|
case "$action" in
|
|
up|down|restart)
|
|
;;
|
|
*)
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Invalid action. Use: up, down, restart"
|
|
json_dump
|
|
return
|
|
;;
|
|
esac
|
|
|
|
# Check if interface exists
|
|
if ! uci -q get network.$iface >/dev/null; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Interface not found: $iface"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Execute action
|
|
case "$action" in
|
|
up)
|
|
ifup "$iface" 2>/dev/null
|
|
;;
|
|
down)
|
|
ifdown "$iface" 2>/dev/null
|
|
;;
|
|
restart)
|
|
ifdown "$iface" 2>/dev/null
|
|
sleep 1
|
|
ifup "$iface" 2>/dev/null
|
|
;;
|
|
esac
|
|
|
|
local rc=$?
|
|
if [ $rc -eq 0 ]; then
|
|
json_add_boolean "success" 1
|
|
json_add_string "message" "Interface $iface $action completed"
|
|
else
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Failed to $action interface $iface"
|
|
fi
|
|
|
|
json_dump
|
|
}
|
|
|
|
# Get peer descriptions from UCI
|
|
get_peer_descriptions() {
|
|
json_init
|
|
json_add_object "descriptions"
|
|
|
|
local interfaces=$($WG_CMD show interfaces 2>/dev/null)
|
|
|
|
for iface in $interfaces; do
|
|
local sections=$(uci show network 2>/dev/null | grep "=wireguard_$iface$" | cut -d'.' -f2 | cut -d'=' -f1)
|
|
for section in $sections; do
|
|
local pubkey=$(uci -q get network.$section.public_key)
|
|
local desc=$(uci -q get network.$section.description)
|
|
if [ -n "$pubkey" ] && [ -n "$desc" ]; then
|
|
json_add_string "$pubkey" "$desc"
|
|
fi
|
|
done
|
|
done
|
|
|
|
json_close_object
|
|
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
|
|
|
|
local interfaces=$($WG_CMD show interfaces 2>/dev/null)
|
|
local now=$(date +%s)
|
|
|
|
json_add_array "rates"
|
|
|
|
for iface in $interfaces; do
|
|
local rx_bytes=$(cat /sys/class/net/$iface/statistics/rx_bytes 2>/dev/null || echo 0)
|
|
local tx_bytes=$(cat /sys/class/net/$iface/statistics/tx_bytes 2>/dev/null || echo 0)
|
|
|
|
# Get previous values from temp file
|
|
local prev_file="/tmp/wg_rate_$iface"
|
|
local prev_rx=0
|
|
local prev_tx=0
|
|
local prev_time=0
|
|
|
|
if [ -f "$prev_file" ]; then
|
|
read prev_rx prev_tx prev_time < "$prev_file"
|
|
fi
|
|
|
|
# Calculate rates
|
|
local rx_rate=0
|
|
local tx_rate=0
|
|
local time_diff=$((now - prev_time))
|
|
|
|
if [ $time_diff -gt 0 ] && [ $prev_time -gt 0 ]; then
|
|
rx_rate=$(( (rx_bytes - prev_rx) / time_diff ))
|
|
tx_rate=$(( (tx_bytes - prev_tx) / time_diff ))
|
|
# Ensure non-negative rates
|
|
[ $rx_rate -lt 0 ] && rx_rate=0
|
|
[ $tx_rate -lt 0 ] && tx_rate=0
|
|
fi
|
|
|
|
# Save current values
|
|
echo "$rx_bytes $tx_bytes $now" > "$prev_file"
|
|
|
|
json_add_object
|
|
json_add_string "interface" "$iface"
|
|
json_add_int "rx_rate" "$rx_rate"
|
|
json_add_int "tx_rate" "$tx_rate"
|
|
json_add_int "rx_total" "$rx_bytes"
|
|
json_add_int "tx_total" "$tx_bytes"
|
|
json_close_object
|
|
done
|
|
|
|
json_close_array
|
|
json_add_int "timestamp" "$now"
|
|
json_dump
|
|
}
|
|
|
|
# Emit a single endpoint section as JSON object (callback for config_foreach)
|
|
_emit_endpoint() {
|
|
local section="$1"
|
|
local name address
|
|
config_get name "$section" name ""
|
|
config_get address "$section" address ""
|
|
json_add_object
|
|
json_add_string "id" "$section"
|
|
json_add_string "name" "$name"
|
|
json_add_string "address" "$address"
|
|
json_close_object
|
|
}
|
|
|
|
# Get all saved server endpoints from UCI
|
|
get_endpoints() {
|
|
json_init
|
|
local default_ep
|
|
default_ep=$(uci -q get wireguard_dashboard.globals.default_endpoint)
|
|
json_add_string "default" "${default_ep:-}"
|
|
json_add_array "endpoints"
|
|
config_load wireguard_dashboard
|
|
config_foreach _emit_endpoint endpoint
|
|
json_close_array
|
|
json_dump
|
|
}
|
|
|
|
# Create or update a server endpoint entry
|
|
set_endpoint() {
|
|
read input
|
|
json_load "$input"
|
|
json_get_var ep_id id
|
|
json_get_var ep_name name
|
|
json_get_var ep_address address
|
|
|
|
json_init
|
|
|
|
if [ -z "$ep_id" ] || [ -z "$ep_address" ]; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Missing required fields: id and address"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Sanitize id: only allow alphanumeric, underscore, hyphen
|
|
local safe_id=$(echo "$ep_id" | sed 's/[^a-zA-Z0-9_-]/_/g')
|
|
|
|
uci set wireguard_dashboard.${safe_id}=endpoint
|
|
uci set wireguard_dashboard.${safe_id}.name="${ep_name:-$safe_id}"
|
|
uci set wireguard_dashboard.${safe_id}.address="$ep_address"
|
|
uci commit wireguard_dashboard
|
|
|
|
json_add_boolean "success" 1
|
|
json_add_string "id" "$safe_id"
|
|
json_dump
|
|
}
|
|
|
|
# Set the default endpoint
|
|
set_default_endpoint() {
|
|
read input
|
|
json_load "$input"
|
|
json_get_var ep_id id
|
|
|
|
json_init
|
|
|
|
if [ -z "$ep_id" ]; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Missing required field: id"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
uci set wireguard_dashboard.globals.default_endpoint="$ep_id"
|
|
uci commit wireguard_dashboard
|
|
|
|
json_add_boolean "success" 1
|
|
json_dump
|
|
}
|
|
|
|
# Delete a server endpoint entry
|
|
delete_endpoint() {
|
|
read input
|
|
json_load "$input"
|
|
json_get_var ep_id id
|
|
|
|
json_init
|
|
|
|
if [ -z "$ep_id" ]; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Missing required field: id"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# Check if section exists
|
|
if ! uci -q get wireguard_dashboard.${ep_id} >/dev/null 2>&1; then
|
|
json_add_boolean "success" 0
|
|
json_add_string "error" "Endpoint not found: $ep_id"
|
|
json_dump
|
|
return
|
|
fi
|
|
|
|
# If deleting the default, clear the default
|
|
local default_ep=$(uci -q get wireguard_dashboard.globals.default_endpoint)
|
|
if [ "$default_ep" = "$ep_id" ]; then
|
|
uci set wireguard_dashboard.globals.default_endpoint=""
|
|
fi
|
|
|
|
uci delete wireguard_dashboard.${ep_id}
|
|
uci commit wireguard_dashboard
|
|
|
|
json_add_boolean "success" 1
|
|
json_dump
|
|
}
|
|
|
|
# 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"},"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
|
|
status)
|
|
get_status
|
|
;;
|
|
interfaces)
|
|
get_interfaces
|
|
;;
|
|
peers)
|
|
get_peers
|
|
;;
|
|
traffic)
|
|
get_traffic
|
|
;;
|
|
config)
|
|
get_config
|
|
;;
|
|
generate_keys)
|
|
generate_keys
|
|
;;
|
|
create_interface)
|
|
create_interface
|
|
;;
|
|
add_peer)
|
|
add_peer
|
|
;;
|
|
remove_peer)
|
|
remove_peer
|
|
;;
|
|
generate_config)
|
|
generate_config
|
|
;;
|
|
generate_qr)
|
|
generate_qr
|
|
;;
|
|
bandwidth_history)
|
|
get_bandwidth_history
|
|
;;
|
|
endpoint_info)
|
|
get_endpoint_info
|
|
;;
|
|
ping_peer)
|
|
ping_peer
|
|
;;
|
|
interface_control)
|
|
interface_control
|
|
;;
|
|
peer_descriptions)
|
|
get_peer_descriptions
|
|
;;
|
|
bandwidth_rates)
|
|
get_bandwidth_rates
|
|
;;
|
|
get_endpoints)
|
|
get_endpoints
|
|
;;
|
|
set_endpoint)
|
|
set_endpoint
|
|
;;
|
|
set_default_endpoint)
|
|
set_default_endpoint
|
|
;;
|
|
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"}'
|
|
;;
|
|
esac
|
|
;;
|
|
esac
|