secubox-openwrt/package/secubox/secubox-app-yggdrasil-discovery/files/usr/sbin/yggctl
CyberMind-FR 4a0ab9530f feat(mesh): Yggdrasil extended peer discovery + bugfixes
## New Features
- secubox-app-yggdrasil-discovery: Mesh peer discovery via gossip protocol
  - yggctl CLI: status, self, peers, announce, discover, bootstrap
  - Auto-peering with trust verification (master-link fingerprint)
  - Daemon for periodic announcements

## Bug Fixes
- tor-shield: Fix opkg downloads failing when Tor active
  - DNS over Tor disabled by default
  - Auto-exclude public DNS servers from iptables rules
  - Excluded domains bypass list (openwrt.org, pool.ntp.org, etc.)

- haproxy: Fix portal 503 "End of Internet" error
  - Corrected malformed vhost backend configuration
  - Regenerated HAProxy config from UCI

- luci-app-nextcloud: Fix users list showing empty
  - RPC expect clause was extracting array, render expected object

## Updated
- Bonus feed: All IPKs rebuilt
- Documentation: HISTORY.md, WIP.md, TODO.md updated

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-28 17:32:41 +01:00

492 lines
15 KiB
Bash

#!/bin/sh
# yggctl - Yggdrasil Extended Peer Discovery CLI
# Part of SecuBox MirrorNet
VERSION="1.0.0"
DISCOVERY_LIB="/usr/lib/yggdrasil-discovery"
PEERS_CACHE="/var/lib/yggdrasil-discovery/peers.json"
ANNOUNCED_FILE="/var/lib/yggdrasil-discovery/announced.json"
. /lib/functions.sh
# Ensure directories exist
mkdir -p /var/lib/yggdrasil-discovery
# Load library modules
[ -f "$DISCOVERY_LIB/core.sh" ] && . "$DISCOVERY_LIB/core.sh"
usage() {
cat <<EOF
yggctl v$VERSION - Yggdrasil Extended Peer Discovery
Usage: yggctl <command> [options]
Peer Discovery:
status Show discovery status and stats
announce Announce this node to mesh peers
discover Discover SecuBox peers on mesh
list List known Yggdrasil peers
Peer Management:
add <pubkey> <ip> Add peer manually
remove <pubkey> Remove peer
auto-connect Auto-connect to trusted discovered peers
trust <pubkey> Mark peer as trusted
untrust <pubkey> Remove peer trust
Yggdrasil Info:
self Show this node's Yggdrasil info
peers Show current Yggdrasil peers
routes Show Yggdrasil routing table
Bootstrap:
bootstrap list List bootstrap peers
bootstrap add <uri> Add bootstrap peer
bootstrap remove Remove bootstrap peer
bootstrap connect Connect to bootstrap peers
Settings:
config show Show current configuration
config set <k> <v> Set configuration value
enable Enable auto-discovery
disable Disable auto-discovery
EOF
exit 0
}
# Get Yggdrasil self info
cmd_self() {
echo "=== Yggdrasil Node Info ==="
# Get IPv6 address
local ipv6
ipv6=$(ip -6 addr show tun0 2>/dev/null | grep -oP '2[0-9a-f:]+(?=/)')
if [ -z "$ipv6" ]; then
echo "Status: NOT RUNNING"
echo "Yggdrasil interface (tun0) not found"
return 1
fi
echo "Status: RUNNING"
echo "IPv6: $ipv6"
# Get public key via yggdrasilctl if available
if command -v yggdrasilctl >/dev/null 2>&1; then
local pubkey
pubkey=$(yggdrasilctl -json getSelf 2>/dev/null | jsonfilter -e '@.key' 2>/dev/null)
[ -n "$pubkey" ] && echo "Public Key: $pubkey"
local coords
coords=$(yggdrasilctl -json getSelf 2>/dev/null | jsonfilter -e '@.coords' 2>/dev/null)
[ -n "$coords" ] && echo "Coords: $coords"
fi
# Get hostname
echo "Hostname: $(cat /proc/sys/kernel/hostname)"
# Check if announced
if [ -f "$ANNOUNCED_FILE" ]; then
local last_announce
last_announce=$(jsonfilter -i "$ANNOUNCED_FILE" -e '@.timestamp' 2>/dev/null)
[ -n "$last_announce" ] && echo "Last Announced: $(date -d @$last_announce 2>/dev/null || date -r $last_announce 2>/dev/null || echo $last_announce)"
fi
}
# Show current Yggdrasil peers
cmd_peers() {
echo "=== Yggdrasil Peers ==="
if ! command -v yggdrasilctl >/dev/null 2>&1; then
echo "yggdrasilctl not found"
return 1
fi
yggdrasilctl -json getPeers 2>/dev/null | jsonfilter -e '@.peers[*]' 2>/dev/null | while read -r peer; do
local addr key uptime
addr=$(echo "$peer" | jsonfilter -e '@.remote' 2>/dev/null)
key=$(echo "$peer" | jsonfilter -e '@.key' 2>/dev/null)
uptime=$(echo "$peer" | jsonfilter -e '@.uptime' 2>/dev/null)
[ -n "$addr" ] && echo " $addr"
[ -n "$key" ] && echo " Key: ${key:0:16}..."
[ -n "$uptime" ] && echo " Uptime: ${uptime}s"
echo ""
done
}
# Announce this node to mesh
cmd_announce() {
echo "Announcing node to mesh..."
local ipv6
ipv6=$(ip -6 addr show tun0 2>/dev/null | grep -oP '2[0-9a-f:]+(?=/)')
if [ -z "$ipv6" ]; then
echo "Error: Yggdrasil not running (no tun0 interface)"
return 1
fi
# Get public key
local pubkey=""
if command -v yggdrasilctl >/dev/null 2>&1; then
pubkey=$(yggdrasilctl -json getSelf 2>/dev/null | jsonfilter -e '@.key' 2>/dev/null)
fi
# Get master-link trust info
local ml_fingerprint=""
local ml_role=""
if [ -f /etc/config/master-link ]; then
ml_fingerprint=$(uci -q get master-link.main.fingerprint)
ml_role=$(uci -q get master-link.main.role)
fi
# Get ZKP fingerprint if available
local zkp_fingerprint=""
if [ -f /etc/config/master-link ]; then
zkp_fingerprint=$(uci -q get master-link.main.zkp_fingerprint)
fi
local hostname
hostname=$(cat /proc/sys/kernel/hostname)
local timestamp
timestamp=$(date +%s)
# Create announcement data
local announce_data
announce_data=$(cat <<EOF
{
"ipv6": "$ipv6",
"pubkey": "$pubkey",
"hostname": "$hostname",
"ml_fingerprint": "$ml_fingerprint",
"ml_role": "$ml_role",
"zkp_fingerprint": "$zkp_fingerprint",
"timestamp": $timestamp,
"version": "$VERSION",
"services": []
}
EOF
)
# Save announcement locally
echo "$announce_data" > "$ANNOUNCED_FILE"
# Broadcast via gossip
if [ -f /usr/lib/mirrornet/gossip.sh ]; then
. /usr/lib/mirrornet/gossip.sh
local msg_id
msg_id=$(gossip_broadcast "yggdrasil_peer" "$announce_data" "$PRIORITY_NORMAL")
echo "Announced via gossip (msg_id: $msg_id)"
else
echo "Warning: gossip.sh not found, announcement not broadcast"
fi
echo "Node announced successfully"
echo " IPv6: $ipv6"
echo " Hostname: $hostname"
[ -n "$pubkey" ] && echo " PubKey: ${pubkey:0:16}..."
[ -n "$ml_fingerprint" ] && echo " ML Fingerprint: $ml_fingerprint"
}
# Discover peers
cmd_discover() {
echo "=== Discovering SecuBox Yggdrasil Peers ==="
if [ ! -f "$PEERS_CACHE" ]; then
echo "No peers discovered yet."
echo "Run 'yggctl announce' to announce yourself first."
return 0
fi
local count=0
jsonfilter -i "$PEERS_CACHE" -e '@[*]' 2>/dev/null | while read -r peer; do
local ipv6 hostname ml_fp timestamp trusted
ipv6=$(echo "$peer" | jsonfilter -e '@.ipv6' 2>/dev/null)
hostname=$(echo "$peer" | jsonfilter -e '@.hostname' 2>/dev/null)
ml_fp=$(echo "$peer" | jsonfilter -e '@.ml_fingerprint' 2>/dev/null)
timestamp=$(echo "$peer" | jsonfilter -e '@.timestamp' 2>/dev/null)
# Check if trusted via master-link
trusted="NO"
if [ -n "$ml_fp" ] && [ -f /usr/lib/secubox/master-link.sh ]; then
. /usr/lib/secubox/master-link.sh
if ml_is_peer_approved "$ml_fp" 2>/dev/null; then
trusted="YES"
fi
fi
count=$((count + 1))
echo "[$count] $hostname"
echo " IPv6: $ipv6"
[ -n "$ml_fp" ] && echo " ML Fingerprint: $ml_fp"
echo " Trusted: $trusted"
echo " Last Seen: $(date -d @$timestamp 2>/dev/null || echo $timestamp)"
echo ""
done
[ "$count" -eq 0 ] && echo "No peers discovered."
}
# List known peers
cmd_list() {
cmd_discover
}
# Auto-connect to trusted peers
cmd_auto_connect() {
echo "Auto-connecting to trusted Yggdrasil peers..."
local require_trust
require_trust=$(uci -q get yggdrasil-discovery.main.require_trust)
require_trust="${require_trust:-1}"
local min_trust
min_trust=$(uci -q get yggdrasil-discovery.main.min_trust_score)
min_trust="${min_trust:-50}"
if [ ! -f "$PEERS_CACHE" ]; then
echo "No peers to connect to. Run 'yggctl discover' first."
return 0
fi
local connected=0
jsonfilter -i "$PEERS_CACHE" -e '@[*]' 2>/dev/null | while read -r peer; do
local ipv6 pubkey ml_fp hostname
ipv6=$(echo "$peer" | jsonfilter -e '@.ipv6' 2>/dev/null)
pubkey=$(echo "$peer" | jsonfilter -e '@.pubkey' 2>/dev/null)
ml_fp=$(echo "$peer" | jsonfilter -e '@.ml_fingerprint' 2>/dev/null)
hostname=$(echo "$peer" | jsonfilter -e '@.hostname' 2>/dev/null)
[ -z "$ipv6" ] && continue
# Check trust if required
if [ "$require_trust" = "1" ]; then
local trusted=0
# Check master-link approval
if [ -n "$ml_fp" ] && [ -f /usr/lib/secubox/master-link.sh ]; then
. /usr/lib/secubox/master-link.sh
ml_is_peer_approved "$ml_fp" 2>/dev/null && trusted=1
fi
# Check reputation score
if [ "$trusted" = "0" ] && [ -f /usr/lib/mirrornet/reputation.sh ]; then
. /usr/lib/mirrornet/reputation.sh
local score
score=$(reputation_get_score "$ml_fp" 2>/dev/null || echo 0)
[ "$score" -ge "$min_trust" ] && trusted=1
fi
if [ "$trusted" = "0" ]; then
echo "Skipping untrusted peer: $hostname ($ipv6)"
continue
fi
fi
# Add as Yggdrasil peer
echo "Connecting to: $hostname ($ipv6)"
if command -v yggdrasilctl >/dev/null 2>&1; then
# Add peer via yggdrasilctl
yggdrasilctl addPeer "tcp://[$ipv6]:0" 2>/dev/null && {
echo " Connected!"
connected=$((connected + 1))
} || {
echo " Failed to connect"
}
fi
done
echo ""
echo "Connected to $connected peers"
}
# Show discovery status
cmd_status() {
echo "=== Yggdrasil Discovery Status ==="
# Check if enabled
local enabled
enabled=$(uci -q get yggdrasil-discovery.main.enabled)
echo "Discovery: $([ "$enabled" = "1" ] && echo "ENABLED" || echo "DISABLED")"
# Yggdrasil status
local ipv6
ipv6=$(ip -6 addr show tun0 2>/dev/null | grep -oP '2[0-9a-f:]+(?=/)')
echo "Yggdrasil: $([ -n "$ipv6" ] && echo "RUNNING ($ipv6)" || echo "STOPPED")"
# Peer counts
local ygg_peers=0
if command -v yggdrasilctl >/dev/null 2>&1; then
ygg_peers=$(yggdrasilctl -json getPeers 2>/dev/null | jsonfilter -e '@.peers[*]' 2>/dev/null | wc -l)
fi
echo "Yggdrasil Peers: $ygg_peers"
# Discovered peers
local discovered=0
[ -f "$PEERS_CACHE" ] && discovered=$(jsonfilter -i "$PEERS_CACHE" -e '@[*]' 2>/dev/null | wc -l)
echo "Discovered SecuBox Nodes: $discovered"
# Settings
echo ""
echo "Settings:"
echo " Auto Announce: $(uci -q get yggdrasil-discovery.main.auto_announce || echo 1)"
echo " Auto Peer: $(uci -q get yggdrasil-discovery.main.auto_peer || echo 1)"
echo " Require Trust: $(uci -q get yggdrasil-discovery.main.require_trust || echo 1)"
echo " Min Trust Score: $(uci -q get yggdrasil-discovery.main.min_trust_score || echo 50)"
# Last announcement
if [ -f "$ANNOUNCED_FILE" ]; then
local last_ts
last_ts=$(jsonfilter -i "$ANNOUNCED_FILE" -e '@.timestamp' 2>/dev/null)
[ -n "$last_ts" ] && echo " Last Announced: $(date -d @$last_ts 2>/dev/null || echo $last_ts)"
fi
}
# Bootstrap commands
cmd_bootstrap() {
local subcmd="${1:-list}"
shift 2>/dev/null
case "$subcmd" in
list)
echo "=== Bootstrap Peers ==="
uci -q get yggdrasil-discovery.bootstrap.peer | tr ' ' '\n' | while read -r peer; do
[ -n "$peer" ] && echo " $peer"
done
;;
add)
local uri="$1"
[ -z "$uri" ] && { echo "Usage: yggctl bootstrap add <tcp://host:port>"; return 1; }
uci add_list yggdrasil-discovery.bootstrap.peer="$uri"
uci commit yggdrasil-discovery
echo "Added bootstrap peer: $uri"
;;
remove)
local uri="$1"
[ -z "$uri" ] && { echo "Usage: yggctl bootstrap remove <tcp://host:port>"; return 1; }
uci del_list yggdrasil-discovery.bootstrap.peer="$uri"
uci commit yggdrasil-discovery
echo "Removed bootstrap peer: $uri"
;;
connect)
echo "Connecting to bootstrap peers..."
uci -q get yggdrasil-discovery.bootstrap.peer | tr ' ' '\n' | while read -r peer; do
[ -z "$peer" ] && continue
echo " Connecting to $peer..."
yggdrasilctl addPeer "$peer" 2>/dev/null && echo " OK" || echo " FAILED"
done
;;
*)
echo "Usage: yggctl bootstrap {list|add|remove|connect}"
;;
esac
}
# Enable/disable
cmd_enable() {
uci set yggdrasil-discovery.main.enabled='1'
uci commit yggdrasil-discovery
echo "Yggdrasil discovery enabled"
}
cmd_disable() {
uci set yggdrasil-discovery.main.enabled='0'
uci commit yggdrasil-discovery
echo "Yggdrasil discovery disabled"
}
# Config commands
cmd_config() {
local subcmd="${1:-show}"
shift 2>/dev/null
case "$subcmd" in
show)
echo "=== Discovery Configuration ==="
uci show yggdrasil-discovery
;;
set)
local key="$1"
local value="$2"
[ -z "$key" ] || [ -z "$value" ] && { echo "Usage: yggctl config set <key> <value>"; return 1; }
uci set "yggdrasil-discovery.main.$key=$value"
uci commit yggdrasil-discovery
echo "Set $key = $value"
;;
*)
echo "Usage: yggctl config {show|set}"
;;
esac
}
# Add peer manually
cmd_add() {
local pubkey="$1"
local ipv6="$2"
[ -z "$pubkey" ] || [ -z "$ipv6" ] && {
echo "Usage: yggctl add <pubkey> <ipv6>"
return 1
}
echo "Adding peer: $ipv6"
# Add to Yggdrasil
if command -v yggdrasilctl >/dev/null 2>&1; then
yggdrasilctl addPeer "tcp://[$ipv6]:0" 2>/dev/null && {
echo "Added to Yggdrasil"
} || {
echo "Warning: Could not add to Yggdrasil"
}
fi
# Add to peers cache
local timestamp
timestamp=$(date +%s)
local peer_json="{\"pubkey\":\"$pubkey\",\"ipv6\":\"$ipv6\",\"timestamp\":$timestamp,\"manual\":true}"
if [ ! -f "$PEERS_CACHE" ] || [ "$(cat "$PEERS_CACHE")" = "[]" ]; then
echo "[$peer_json]" > "$PEERS_CACHE"
else
sed "s/]$/,$peer_json]/" "$PEERS_CACHE" > "$PEERS_CACHE.tmp"
mv "$PEERS_CACHE.tmp" "$PEERS_CACHE"
fi
echo "Peer added"
}
# Main command dispatch
case "${1:-}" in
self) cmd_self ;;
peers) cmd_peers ;;
announce) cmd_announce ;;
discover) cmd_discover ;;
list) cmd_list ;;
auto-connect) cmd_auto_connect ;;
status) cmd_status ;;
bootstrap) shift; cmd_bootstrap "$@" ;;
add) shift; cmd_add "$@" ;;
enable) cmd_enable ;;
disable) cmd_disable ;;
config) shift; cmd_config "$@" ;;
routes)
if command -v yggdrasilctl >/dev/null 2>&1; then
yggdrasilctl getDHT
else
echo "yggdrasilctl not found"
fi
;;
-h|--help|help|"")
usage
;;
*)
echo "Unknown command: $1"
usage
;;
esac