secubox-openwrt/package/secubox/secubox-p2p/root/usr/sbin/secubox-p2p
CyberMind-FR 747d1ffbaa perf(secubox-p2p): Optimize shared services for faster LuCI response
- Skip IPv6 addresses and use active_address when available
- Filter out local node from shared services query
- Increase curl max-time to 10s for slow CGI responses
- Skip null/empty peer addresses
- Reduces response time from 48s to ~5s

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-13 12:20:44 +01:00

909 lines
26 KiB
Bash

#!/bin/sh
# SecuBox P2P Hub Manager
# Handles peer discovery, mesh networking, and service federation
# Supports WAN IP, LAN IP, and WireGuard tunnel redundancy
VERSION="0.6.0"
CONFIG_FILE="/etc/config/secubox-p2p"
PEERS_FILE="/tmp/secubox-p2p-peers.json"
SERVICES_FILE="/tmp/secubox-p2p-services.json"
STATE_DIR="/var/run/secubox-p2p"
MDNS_PID_FILE="$STATE_DIR/mdns.pid"
API_PORT=7331
# Initialize
init() {
mkdir -p "$STATE_DIR"
[ -f "$PEERS_FILE" ] || echo '{"peers":[]}' > "$PEERS_FILE"
[ -f "$SERVICES_FILE" ] || echo '{"services":[]}' > "$SERVICES_FILE"
# Initialize node info
_init_node_info
}
# Get LAN IP address
get_lan_ip() {
local lan_ip
lan_ip=$(ip -4 addr show br-lan 2>/dev/null | grep -oE 'inet [0-9.]+' | awk '{print $2}' | head -1)
[ -z "$lan_ip" ] && lan_ip=$(uci -q get network.lan.ipaddr)
echo "${lan_ip:-127.0.0.1}"
}
# Get WAN IP address (real external IP)
get_wan_ip() {
local wan_ip wan_iface
# Get WAN interface
wan_iface=$(uci -q get network.wan.device || uci -q get network.wan.ifname || echo "eth0")
# Try to get IP from WAN interface
wan_ip=$(ip -4 addr show "$wan_iface" 2>/dev/null | grep -oE 'inet [0-9.]+' | awk '{print $2}' | head -1)
# If no direct WAN IP, try to get public IP via external service
if [ -z "$wan_ip" ] || echo "$wan_ip" | grep -qE '^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.)'; then
# Behind NAT - try to get public IP
wan_ip=$(curl -s --connect-timeout 3 https://api.ipify.org 2>/dev/null)
[ -z "$wan_ip" ] && wan_ip=$(curl -s --connect-timeout 3 https://ifconfig.me 2>/dev/null)
fi
echo "${wan_ip:-}"
}
# Get WireGuard tunnel IP(s)
get_wg_ips() {
local wg_ips=""
# Check all WireGuard interfaces
for wg_iface in $(ip link show type wireguard 2>/dev/null | grep -oE 'wg[0-9]+' | head -5); do
local wg_ip
wg_ip=$(ip -4 addr show "$wg_iface" 2>/dev/null | grep -oE 'inet [0-9.]+' | awk '{print $2}')
if [ -n "$wg_ip" ]; then
[ -n "$wg_ips" ] && wg_ips="$wg_ips,"
wg_ips="${wg_ips}$wg_ip"
fi
done
echo "$wg_ips"
}
# Get all node addresses as JSON array
get_node_addresses() {
local lan_ip wan_ip wg_ips addresses
lan_ip=$(get_lan_ip)
wan_ip=$(get_wan_ip)
wg_ips=$(get_wg_ips)
# Build addresses JSON array
addresses="[{\"type\":\"lan\",\"address\":\"$lan_ip\",\"port\":$API_PORT}"
if [ -n "$wan_ip" ]; then
addresses="$addresses,{\"type\":\"wan\",\"address\":\"$wan_ip\",\"port\":$API_PORT}"
fi
# Add WireGuard IPs
if [ -n "$wg_ips" ]; then
for wg_ip in $(echo "$wg_ips" | tr ',' ' '); do
addresses="$addresses,{\"type\":\"wireguard\",\"address\":\"$wg_ip\",\"port\":$API_PORT}"
done
fi
addresses="$addresses]"
echo "$addresses"
}
# Initialize node identity
_init_node_info() {
local node_name
node_name=$(get_config main node_name "")
# Generate node name from hostname if not set
if [ -z "$node_name" ]; then
node_name=$(cat /proc/sys/kernel/hostname 2>/dev/null || echo "secubox")
set_config main node_name "$node_name"
fi
# Generate node ID if not exists
local node_id_file="$STATE_DIR/node.id"
if [ ! -f "$node_id_file" ]; then
# Generate unique node ID from MAC address
local mac
mac=$(ip link show br-lan 2>/dev/null | grep ether | awk '{print $2}' | tr -d ':')
[ -z "$mac" ] && mac=$(cat /proc/sys/kernel/random/uuid | tr -d '-' | head -c 12)
echo "sb-${mac}" > "$node_id_file"
fi
}
# Get config value
get_config() {
local section="$1"
local option="$2"
local default="$3"
uci -q get "secubox-p2p.${section}.${option}" || echo "$default"
}
# Set config value
set_config() {
local section="$1"
local option="$2"
local value="$3"
uci set "secubox-p2p.${section}.${option}=$value"
uci commit secubox-p2p
}
# Discover peers via mDNS
discover_mdns() {
local timeout="${1:-5}"
local peers="[]"
# Check if avahi-browse is available
if command -v avahi-browse >/dev/null 2>&1; then
# Discover _secubox._tcp services
local discovered=$(avahi-browse -t -r _secubox._tcp 2>/dev/null | grep -E "^=|hostname|address" || true)
if [ -n "$discovered" ]; then
# Parse discovered services into JSON
echo "$discovered" | awk '
/^=/ { name=$4 }
/hostname/ { host=$3 }
/address/ {
addr=$3
if (name && addr) {
printf "{\"id\":\"%s\",\"name\":\"%s\",\"address\":\"%s\",\"status\":\"online\"},", name, name, addr
}
}
' | sed 's/,$//' | awk '{print "["$0"]"}'
else
echo "[]"
fi
else
# Fallback: scan local network
local gateway=$(ip route | grep default | awk '{print $3}')
local subnet=$(echo "$gateway" | sed 's/\.[0-9]*$/./')
# Quick ping scan
for i in $(seq 1 254); do
ping -c1 -W1 "${subnet}${i}" >/dev/null 2>&1 &
done
wait
# Check for SecuBox peers via HTTP
arp -n | grep -v incomplete | awk '{print $1}' | while read ip; do
if curl -s --connect-timeout 1 "http://${ip}/cgi-bin/luci/admin/secubox" >/dev/null 2>&1; then
echo "{\"id\":\"peer-${ip}\",\"name\":\"SecuBox@${ip}\",\"address\":\"${ip}\",\"status\":\"online\"}"
fi
done | jq -s '.' 2>/dev/null || echo "[]"
fi
}
# Get peers list
get_peers() {
# Ensure state dir and node info exist
mkdir -p "$STATE_DIR"
_init_node_info
# If no peers file or empty, ensure local node is registered
if [ ! -f "$PEERS_FILE" ] || grep -q '"peers":\[\]' "$PEERS_FILE" 2>/dev/null; then
register_self
fi
if [ -f "$PEERS_FILE" ]; then
cat "$PEERS_FILE"
else
# Still no file? Create with local node
local node_name node_id lan_ip
node_name=$(get_config main node_name "secubox")
node_id=$(cat "$STATE_DIR/node.id" 2>/dev/null || echo "unknown")
lan_ip=$(ip -4 addr show br-lan 2>/dev/null | grep -oE 'inet [0-9.]+' | awk '{print $2}' | head -1)
[ -z "$lan_ip" ] && lan_ip=$(uci -q get network.lan.ipaddr || echo "127.0.0.1")
echo "{\"peers\":[{\"id\":\"$node_id\",\"name\":\"$node_name (local)\",\"address\":\"$lan_ip\",\"status\":\"online\",\"is_local\":true}]}"
fi
}
# Add peer with optional WAN and WireGuard addresses
add_peer() {
local address="$1"
local name="${2:-Peer}"
local wan_address="${3:-}"
local wg_address="${4:-}"
local id="peer-$(echo "$address" | md5sum | cut -c1-8)"
local peers=$(get_peers)
# Build addresses array
local addresses="[{\"type\":\"lan\",\"address\":\"$address\",\"port\":$API_PORT}"
[ -n "$wan_address" ] && addresses="$addresses,{\"type\":\"wan\",\"address\":\"$wan_address\",\"port\":$API_PORT}"
[ -n "$wg_address" ] && addresses="$addresses,{\"type\":\"wireguard\",\"address\":\"$wg_address\",\"port\":$API_PORT}"
addresses="$addresses]"
local new_peer="{\"id\":\"$id\",\"name\":\"$name\",\"address\":\"$address\",\"wan_address\":\"${wan_address:-}\",\"wg_addresses\":\"${wg_address:-}\",\"addresses\":$addresses,\"status\":\"unknown\",\"added\":\"$(date -Iseconds)\"}"
echo "$peers" | jq ".peers += [$new_peer]" > "$PEERS_FILE"
echo "{\"success\":true,\"peer_id\":\"$id\"}"
}
# Remove peer
remove_peer() {
local peer_id="$1"
local peers=$(get_peers)
echo "$peers" | jq ".peers = [.peers[] | select(.id != \"$peer_id\")]" > "$PEERS_FILE"
echo "{\"success\":true}"
}
# Get settings
get_settings() {
cat <<EOF
{
"enabled": $(get_config main enabled 1),
"node_name": "$(get_config main node_name "")",
"discovery_enabled": $(get_config main discovery_enabled 1),
"sharing_enabled": $(get_config main sharing_enabled 1),
"auto_sync": $(get_config main auto_sync 1),
"sync_interval": $(get_config main sync_interval 60),
"dns_federation": {
"enabled": $(get_config dns enabled 0),
"primary_dns": "$(get_config dns primary_dns "127.0.0.1:53")",
"base_domain": "$(get_config dns base_domain "sb.local")"
},
"wireguard": {
"enabled": $(get_config wireguard enabled 0),
"listen_port": $(get_config wireguard listen_port 51820),
"network_cidr": "$(get_config wireguard network_cidr "10.100.0.0/24")"
},
"haproxy": {
"enabled": $(get_config haproxy enabled 0),
"strategy": "$(get_config haproxy strategy "round-robin")"
},
"registry": {
"base_url": "$(get_config registry base_url "sb.local")",
"cache_enabled": $(get_config registry cache_enabled 1)
}
}
EOF
}
# Set settings
set_settings() {
local json="$1"
# Parse and apply settings
local enabled=$(echo "$json" | jsonfilter -e '@.enabled')
local sharing=$(echo "$json" | jsonfilter -e '@.sharing_enabled')
local discovery=$(echo "$json" | jsonfilter -e '@.discovery_enabled')
[ -n "$enabled" ] && set_config main enabled "$enabled"
[ -n "$sharing" ] && set_config main sharing_enabled "$sharing"
[ -n "$discovery" ] && set_config main discovery_enabled "$discovery"
echo "{\"success\":true}"
}
# Get local services - scan init.d and detect running status
get_services() {
local services=""
local count=0
# Service port mapping
get_service_port() {
case "$1" in
dnsmasq) echo "53" ;;
uhttpd) echo "80" ;;
nginx) echo "80" ;;
haproxy) echo "80,443" ;;
crowdsec) echo "8080" ;;
crowdsec-firewall-bouncer) echo "" ;;
dropbear) echo "22" ;;
sshd) echo "22" ;;
firewall*) echo "" ;;
wireguard|wg*) echo "51820" ;;
gitea) echo "3000" ;;
localai) echo "8080" ;;
ollama) echo "11434" ;;
nextcloud) echo "8080" ;;
lxc*) echo "" ;;
rpcd) echo "" ;;
*) echo "" ;;
esac
}
# Scan init.d services
for init_script in /etc/init.d/*; do
[ -x "$init_script" ] || continue
local svc_name=$(basename "$init_script")
# Skip system services
case "$svc_name" in
boot|done|rcS|rc.local|umount|sysfixtime|sysntpd|gpio_switch) continue ;;
esac
# Check if service is running
local status="stopped"
local pid=""
# Method 1: Check via init script status
if "$init_script" status >/dev/null 2>&1; then
if "$init_script" status 2>&1 | grep -qiE "running|active"; then
status="running"
fi
fi
# Method 2: Check via pgrep (fallback)
if [ "$status" = "stopped" ]; then
if pgrep -f "$svc_name" >/dev/null 2>&1; then
status="running"
pid=$(pgrep -f "$svc_name" | head -1)
fi
fi
# Method 3: Check if enabled in UCI/procd
local enabled="0"
if "$init_script" enabled 2>/dev/null; then
enabled="1"
fi
local port=$(get_service_port "$svc_name")
# Build JSON entry
if [ $count -gt 0 ]; then
services="$services,"
fi
services="$services{\"name\":\"$svc_name\",\"status\":\"$status\",\"enabled\":$enabled,\"port\":\"$port\",\"pid\":\"$pid\"}"
count=$((count + 1))
done
echo "{\"services\":[$services],\"total\":$count}"
}
# Get shared services (from peers)
get_shared_services() {
local all_services="[]"
# Use active_address if available, skip IPv6 and local nodes
local peers=$(get_peers | jq -r '.peers[] | select(.status=="online") | select(.is_local==false or .is_local==null) | .active_address // .address' | grep -v '^\[' | sed 's/ .*//')
for peer_addr in $peers; do
[ -z "$peer_addr" ] && continue
[ "$peer_addr" = "null" ] && continue
# Fetch services with reasonable timeout
local response=$(curl -s --connect-timeout 2 --max-time 10 "http://${peer_addr}/cgi-bin/p2p-services" 2>/dev/null)
if [ -n "$response" ]; then
local peer_services=$(echo "$response" | jq -c '.services // []' 2>/dev/null)
if [ -n "$peer_services" ] && [ "$peer_services" != "null" ] && [ "$peer_services" != "[]" ]; then
# Add peer info to each service
peer_services=$(echo "$peer_services" | jq --arg addr "$peer_addr" '[.[] | . + {peer: $addr}]')
all_services=$(echo "$all_services" "$peer_services" | jq -s '.[0] + .[1]' 2>/dev/null)
fi
fi
done
echo "{\"shared_services\":$all_services}"
}
# Sync with peers
sync_catalog() {
local peers=$(get_peers | jq -r '.peers[].address')
local synced=0
for peer_addr in $peers; do
if curl -s --connect-timeout 2 "http://${peer_addr}/cgi-bin/luci" >/dev/null 2>&1; then
synced=$((synced + 1))
fi
done
echo "{\"success\":true,\"synced_peers\":$synced}"
}
# Broadcast command to all peers
broadcast_command() {
local cmd="$1"
local peers=$(get_peers | jq -r '.peers[] | select(.status=="online") | .address')
local success=0
local failed=0
for peer_addr in $peers; do
if curl -s --connect-timeout 5 -X POST "http://${peer_addr}:8080/p2p/command" -d "{\"command\":\"$cmd\"}" >/dev/null 2>&1; then
success=$((success + 1))
else
failed=$((failed + 1))
fi
done
echo "{\"success\":true,\"broadcast_success\":$success,\"broadcast_failed\":$failed}"
}
# Publish mDNS service announcement
publish_mdns() {
local node_name
node_name=$(get_config main node_name "secubox")
# Kill any existing avahi-publish process
[ -f "$MDNS_PID_FILE" ] && kill $(cat "$MDNS_PID_FILE") 2>/dev/null
rm -f "$MDNS_PID_FILE"
# Check if avahi-publish is available
if command -v avahi-publish >/dev/null 2>&1; then
# Get LAN IP for service registration
local lan_ip
lan_ip=$(ip -4 addr show br-lan 2>/dev/null | grep -oE 'inet [0-9.]+' | awk '{print $2}' | head -1)
[ -z "$lan_ip" ] && lan_ip=$(uci -q get network.lan.ipaddr)
# Publish _secubox._tcp service
avahi-publish -s "$node_name" "_secubox._tcp" "$API_PORT" \
"version=$VERSION" \
"type=mesh-node" \
"ip=$lan_ip" &
echo $! > "$MDNS_PID_FILE"
logger -t secubox-p2p "mDNS service published: $node_name._secubox._tcp on port $API_PORT"
return 0
else
logger -t secubox-p2p "WARNING: avahi-publish not available, mDNS disabled"
return 1
fi
}
# Stop mDNS service announcement
stop_mdns() {
if [ -f "$MDNS_PID_FILE" ]; then
kill $(cat "$MDNS_PID_FILE") 2>/dev/null
rm -f "$MDNS_PID_FILE"
logger -t secubox-p2p "mDNS service stopped"
fi
}
# ===========================================
# DNS Federation - Dynamic mesh DNS entries
# ===========================================
DNS_HOSTS_FILE="/tmp/hosts/secubox-mesh"
DNS_DOMAIN_BASE="mesh.local"
# Initialize DNS federation
dns_init() {
local dns_enabled=$(get_config dns enabled 0)
[ "$dns_enabled" = "1" ] || return 0
DNS_DOMAIN_BASE=$(get_config dns base_domain "mesh.local")
# Ensure hosts directory exists
mkdir -p /tmp/hosts
# Create or update mesh hosts file
touch "$DNS_HOSTS_FILE"
# Ensure dnsmasq reads from /tmp/hosts
# Check if addn-hosts is configured
if ! uci -q get dhcp.@dnsmasq[0].addnhosts | grep -q "/tmp/hosts"; then
uci add_list dhcp.@dnsmasq[0].addnhosts="/tmp/hosts"
uci commit dhcp
logger -t secubox-p2p "Added /tmp/hosts to dnsmasq additional hosts"
fi
}
# Update DNS entries for all mesh peers
dns_update_peers() {
local dns_enabled=$(get_config dns enabled 0)
[ "$dns_enabled" = "1" ] || return 0
dns_init
local base_domain=$(get_config dns base_domain "mesh.local")
local hosts_content=""
local count=0
# Get all peers (including local)
local peers_json=$(cat "$PEERS_FILE" 2>/dev/null || echo '{"peers":[]}')
# Parse peers and build hosts entries
# Format: IP hostname hostname.mesh.local
if command -v jq >/dev/null 2>&1; then
hosts_content=$(echo "$peers_json" | jq -r '.peers[] | select(.status == "online" or .is_local == true) |
# Build hostname entries
(
(.address // empty) as $lan |
(.wan_address // empty) as $wan |
(.wg_addresses // empty) as $wg |
(.name // "peer") | gsub(" \\(local\\)"; "") | gsub("[^a-zA-Z0-9-]"; "-") | ascii_downcase as $hostname |
# LAN entry (primary for local network)
(if $lan != "" and $lan != null then
"\($lan)\t\($hostname)\t\($hostname).'$base_domain'"
else empty end),
# WireGuard entry (for mesh access)
(if $wg != "" and $wg != null then
($wg | split(",")[0]) as $wg_ip |
if $wg_ip != "" then
"\($wg_ip)\t\($hostname)-wg\t\($hostname)-wg.'$base_domain'"
else empty end
else empty end)
)
' 2>/dev/null)
else
# Fallback without jq - use jsonfilter
local peer_count=$(echo "$peers_json" | jsonfilter -e '@.peers[*]' 2>/dev/null | wc -l)
local i=0
while [ $i -lt $peer_count ]; do
local addr=$(echo "$peers_json" | jsonfilter -e "@.peers[$i].address" 2>/dev/null)
local name=$(echo "$peers_json" | jsonfilter -e "@.peers[$i].name" 2>/dev/null)
local status=$(echo "$peers_json" | jsonfilter -e "@.peers[$i].status" 2>/dev/null)
local is_local=$(echo "$peers_json" | jsonfilter -e "@.peers[$i].is_local" 2>/dev/null)
local wg_addr=$(echo "$peers_json" | jsonfilter -e "@.peers[$i].wg_addresses" 2>/dev/null | cut -d',' -f1)
# Only add online peers or local node
if [ "$status" = "online" ] || [ "$is_local" = "true" ]; then
# Clean hostname (remove special chars)
local hostname=$(echo "$name" | sed -e 's/ (local)//' -e 's/[^a-zA-Z0-9-]/-/g' | tr 'A-Z' 'a-z')
# Add LAN entry
if [ -n "$addr" ]; then
hosts_content="$hosts_content$addr $hostname $hostname.$base_domain
"
count=$((count + 1))
fi
# Add WireGuard entry
if [ -n "$wg_addr" ]; then
hosts_content="$hosts_content$wg_addr ${hostname}-wg ${hostname}-wg.$base_domain
"
count=$((count + 1))
fi
fi
i=$((i + 1))
done
fi
# Write hosts file
echo "# SecuBox Mesh DNS Federation - Auto-generated" > "$DNS_HOSTS_FILE"
echo "# Domain: $base_domain" >> "$DNS_HOSTS_FILE"
echo "# Updated: $(date -Iseconds 2>/dev/null || date)" >> "$DNS_HOSTS_FILE"
echo "" >> "$DNS_HOSTS_FILE"
echo "$hosts_content" >> "$DNS_HOSTS_FILE"
# Count actual entries (non-comment, non-empty lines)
count=$(grep -c "^[0-9]" "$DNS_HOSTS_FILE" 2>/dev/null || echo "0")
# Signal dnsmasq to reload hosts
killall -HUP dnsmasq 2>/dev/null || /etc/init.d/dnsmasq reload 2>/dev/null
logger -t secubox-p2p "DNS federation updated: $count mesh host entries in $base_domain"
echo "{\"success\":true,\"entries\":$count,\"domain\":\"$base_domain\"}"
}
# Add a single peer to DNS
dns_add_peer() {
local hostname="$1"
local ip="$2"
local dns_enabled=$(get_config dns enabled 0)
[ "$dns_enabled" = "1" ] || return 0
local base_domain=$(get_config dns base_domain "mesh.local")
# Clean hostname
hostname=$(echo "$hostname" | sed -e 's/[^a-zA-Z0-9-]/-/g' | tr 'A-Z' 'a-z')
# Append to hosts file (avoid duplicates)
if ! grep -q " $hostname " "$DNS_HOSTS_FILE" 2>/dev/null; then
echo "$ip $hostname $hostname.$base_domain" >> "$DNS_HOSTS_FILE"
killall -HUP dnsmasq 2>/dev/null
logger -t secubox-p2p "DNS: Added $hostname.$base_domain -> $ip"
fi
}
# Remove a peer from DNS
dns_remove_peer() {
local hostname="$1"
local dns_enabled=$(get_config dns enabled 0)
[ "$dns_enabled" = "1" ] || return 0
# Clean hostname
hostname=$(echo "$hostname" | sed -e 's/[^a-zA-Z0-9-]/-/g' | tr 'A-Z' 'a-z')
# Remove from hosts file
sed -i "/ $hostname /d" "$DNS_HOSTS_FILE" 2>/dev/null
killall -HUP dnsmasq 2>/dev/null
logger -t secubox-p2p "DNS: Removed $hostname"
}
# Get DNS federation status
dns_status() {
local dns_enabled=$(get_config dns enabled 0)
local base_domain=$(get_config dns base_domain "mesh.local")
local entry_count=0
if [ -f "$DNS_HOSTS_FILE" ]; then
entry_count=$(grep -c "^[0-9]" "$DNS_HOSTS_FILE" 2>/dev/null || echo "0")
fi
cat << EOF
{
"enabled": $dns_enabled,
"domain": "$base_domain",
"hosts_file": "$DNS_HOSTS_FILE",
"entries": $entry_count,
"dnsmasq_running": $(pgrep dnsmasq >/dev/null && echo "true" || echo "false")
}
EOF
}
# Enable DNS federation
dns_enable() {
set_config dns enabled 1
local base_domain="${1:-mesh.local}"
set_config dns base_domain "$base_domain"
dns_init
dns_update_peers
logger -t secubox-p2p "DNS federation enabled with domain: $base_domain"
echo "{\"success\":true,\"domain\":\"$base_domain\"}"
}
# Disable DNS federation
dns_disable() {
set_config dns enabled 0
# Clear hosts file
echo "# SecuBox Mesh DNS Federation - Disabled" > "$DNS_HOSTS_FILE"
killall -HUP dnsmasq 2>/dev/null
logger -t secubox-p2p "DNS federation disabled"
echo "{\"success\":true}"
}
# Get node status JSON (for REST API)
# Now includes WAN IP and WireGuard tunnel addresses
get_node_status() {
local node_name node_id lan_ip wan_ip wg_ips addresses uptime
node_name=$(get_config main node_name "secubox")
node_id=$(cat "$STATE_DIR/node.id" 2>/dev/null || echo "unknown")
lan_ip=$(get_lan_ip)
wan_ip=$(get_wan_ip)
wg_ips=$(get_wg_ips)
addresses=$(get_node_addresses)
uptime=$(cat /proc/uptime | cut -d' ' -f1)
cat <<EOF
{
"node_id": "$node_id",
"node_name": "$node_name",
"version": "$VERSION",
"address": "$lan_ip",
"wan_address": "${wan_ip:-null}",
"wg_addresses": "${wg_ips:-}",
"addresses": $addresses,
"api_port": $API_PORT,
"uptime": $uptime,
"discovery_enabled": $(get_config main discovery_enabled 1),
"sharing_enabled": $(get_config main sharing_enabled 1),
"peer_count": $(get_peers | jsonfilter -e '@.peers[*]' 2>/dev/null | wc -l)
}
EOF
}
# Register self in peer list (ensure node is visible in its own mesh view)
# Now includes WAN IP and WireGuard tunnel addresses for redundancy
register_self() {
local node_name node_id lan_ip wan_ip wg_ips addresses
node_name=$(get_config main node_name "secubox")
node_id=$(cat "$STATE_DIR/node.id" 2>/dev/null || echo "unknown")
# Get all available addresses
lan_ip=$(get_lan_ip)
wan_ip=$(get_wan_ip)
wg_ips=$(get_wg_ips)
addresses=$(get_node_addresses)
# Build peer entry with all addresses
# Primary address is LAN for local network, but WAN and WG are included for external/redundancy
local self_peer="{\"id\":\"$node_id\",\"name\":\"$node_name (local)\",\"address\":\"$lan_ip\",\"wan_address\":\"${wan_ip:-}\",\"wg_addresses\":\"${wg_ips:-}\",\"addresses\":$addresses,\"status\":\"online\",\"is_local\":true,\"added\":\"$(date -Iseconds)\"}"
# Check if self is already registered using grep (jsonfilter syntax workaround)
local current=$(cat "$PEERS_FILE" 2>/dev/null || echo '{"peers":[]}')
local exists=""
if echo "$current" | grep -q "\"$node_id\""; then
exists="yes"
fi
if [ -z "$exists" ]; then
# Check if peers array is empty (handle whitespace variations)
if echo "$current" | grep -qE '"peers"[[:space:]]*:[[:space:]]*\[[[:space:]]*\]'; then
# Empty array - just set self as first peer
echo "{\"peers\":[$self_peer]}" > "$PEERS_FILE"
else
# Array has entries - prepend self using awk for reliable JSON manipulation
echo "$current" | awk -v self="$self_peer" '{
gsub(/"peers"[[:space:]]*:[[:space:]]*\[/, "\"peers\": [" self ", ")
print
}' > "$PEERS_FILE.tmp"
if [ -s "$PEERS_FILE.tmp" ]; then
mv "$PEERS_FILE.tmp" "$PEERS_FILE"
else
# Fallback: fresh peers file
echo "{\"peers\":[$self_peer]}" > "$PEERS_FILE"
fi
fi
logger -t secubox-p2p "Registered local node: $node_name ($node_id) LAN=$lan_ip WAN=${wan_ip:-none} WG=${wg_ips:-none}"
fi
}
# Daemon mode
daemon_loop() {
init
# Publish mDNS service
publish_mdns
# Register self in peer list
register_self
# Initialize DNS federation
dns_init
# Setup signal handlers
trap 'stop_mdns; exit 0' INT TERM
while true; do
# Auto-discovery if enabled
if [ "$(get_config main discovery_enabled 1)" = "1" ]; then
local discovered=$(discover_mdns 3)
if [ "$discovered" != "[]" ] && [ -n "$discovered" ]; then
# Update peers file with discovered peers
local current=$(get_peers)
# Parse discovered peers (handle jq or jsonfilter)
if command -v jq >/dev/null 2>&1; then
for peer in $(echo "$discovered" | jq -c '.[]' 2>/dev/null); do
local peer_id=$(echo "$peer" | jq -r '.id')
local exists=$(echo "$current" | jq ".peers[] | select(.id==\"$peer_id\")")
if [ -z "$exists" ]; then
current=$(echo "$current" | jq ".peers += [$peer]")
fi
done
echo "$current" > "$PEERS_FILE"
fi
fi
fi
# Update peer status (skip local node)
# Try all available addresses: WireGuard first (secure tunnel), then WAN, then LAN
local peers=$(get_peers)
if command -v jq >/dev/null 2>&1; then
local updated_peers=$(echo "$peers" | jq -c '.peers[] | select(.is_local != true)' 2>/dev/null)
for peer in $updated_peers; do
local addr=$(echo "$peer" | jq -r '.address')
local wan_addr=$(echo "$peer" | jq -r '.wan_address // empty')
local wg_addrs=$(echo "$peer" | jq -r '.wg_addresses // empty')
local id=$(echo "$peer" | jq -r '.id')
local reachable=""
local best_addr=""
# Try WireGuard addresses first (secure redundancy)
if [ -n "$wg_addrs" ]; then
for wg_addr in $(echo "$wg_addrs" | tr ',' ' '); do
if ping -c1 -W1 "$wg_addr" >/dev/null 2>&1; then
reachable="yes"
best_addr="$wg_addr (wg)"
break
fi
done
fi
# Try WAN address if WG failed
if [ -z "$reachable" ] && [ -n "$wan_addr" ] && [ "$wan_addr" != "null" ]; then
if ping -c1 -W1 "$wan_addr" >/dev/null 2>&1; then
reachable="yes"
best_addr="$wan_addr (wan)"
fi
fi
# Try LAN address as fallback
if [ -z "$reachable" ]; then
if ping -c1 -W1 "$addr" >/dev/null 2>&1; then
reachable="yes"
best_addr="$addr (lan)"
fi
fi
if [ -n "$reachable" ]; then
peers=$(echo "$peers" | jq "(.peers[] | select(.id==\"$id\")).status = \"online\"")
peers=$(echo "$peers" | jq "(.peers[] | select(.id==\"$id\")).active_address = \"$best_addr\"")
else
peers=$(echo "$peers" | jq "(.peers[] | select(.id==\"$id\")).status = \"offline\"")
peers=$(echo "$peers" | jq "(.peers[] | select(.id==\"$id\")).active_address = null")
fi
done
echo "$peers" > "$PEERS_FILE"
fi
# Update DNS federation with current peer list
dns_update_peers >/dev/null 2>&1
# Sleep interval
local interval=$(get_config main sync_interval 60)
sleep "$interval"
done
}
# Main
case "$1" in
daemon)
daemon_loop
;;
discover)
discover_mdns "${2:-5}"
;;
peers)
get_peers
;;
add-peer)
add_peer "$2" "$3"
;;
remove-peer)
remove_peer "$2"
;;
settings)
get_settings
;;
set-settings)
set_settings "$2"
;;
services)
get_services
;;
shared-services)
get_shared_services
;;
sync)
sync_catalog
;;
broadcast)
broadcast_command "$2"
;;
version)
echo "$VERSION"
;;
status)
get_node_status
;;
publish-mdns)
publish_mdns
;;
stop-mdns)
stop_mdns
;;
register-self)
init
register_self
;;
# DNS Federation commands
dns-status)
dns_status
;;
dns-enable)
dns_enable "$2"
;;
dns-disable)
dns_disable
;;
dns-update)
init
dns_update_peers
;;
dns-add)
dns_add_peer "$2" "$3"
;;
dns-remove)
dns_remove_peer "$2"
;;
*)
echo "Usage: $0 {daemon|discover|peers|add-peer|remove-peer|settings|set-settings|services|shared-services|sync|broadcast|version|status|publish-mdns|stop-mdns|register-self|dns-status|dns-enable|dns-disable|dns-update|dns-add|dns-remove}"
exit 1
;;
esac