#!/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 } # 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"}}' ;; 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 ;; *) echo '{"error": "Unknown method"}' ;; esac ;; esac