From 53af5ac2e9f44df3fbf8678f517b2340cc98d94a Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Thu, 5 Feb 2026 04:18:33 +0100 Subject: [PATCH] feat(exposure): Add emancipate/revoke commands for multi-channel exposure - `emancipate [--tor] [--dns] [--mesh] [--all]` Unified multi-channel exposure: Tor + DNS/SSL + Mesh in single command - `revoke [--tor] [--dns] [--mesh] [--all]` Removes exposure from selected channels - Enhanced `status` command shows emancipated services with active channels - UCI tracking for emancipation state (port, domain, channel flags) - Integrates with dnsctl, haproxyctl, and secubox-p2p Co-Authored-By: Claude Opus 4.5 --- .claude/HISTORY.md | 11 + .claude/TODO.md | 14 +- .claude/WIP.md | 9 + .../files/usr/sbin/secubox-exposure | 344 +++++++++++++++++- 4 files changed, 364 insertions(+), 14 deletions(-) diff --git a/.claude/HISTORY.md b/.claude/HISTORY.md index f8334b32..1eeea768 100644 --- a/.claude/HISTORY.md +++ b/.claude/HISTORY.md @@ -174,3 +174,14 @@ _Last updated: 2026-02-05_ - Network rates now display in bits (Kbps/Mbps/Gbps) instead of bytes. - Uses SI units (1000 base) for industry-standard notation. - Dash placeholder ("— ↓ · — ↑") before first data point. + +22. **Punk Exposure Emancipate CLI (2026-02-05)** + - `secubox-exposure emancipate [--tor] [--dns] [--mesh] [--all]` + - Unified multi-channel exposure: Tor + DNS/SSL + Mesh in single command. + - Creates DNS A record via `dnsctl`, HAProxy vhost, requests certificate. + - Publishes to mesh via `secubox-p2p publish`. + - Stores emancipation state in UCI for status tracking. + - `secubox-exposure revoke [--tor] [--dns] [--mesh] [--all]` + - Inverse of emancipate: removes exposure from selected channels. + - Cleans up DNS records, HAProxy vhosts, certificates, mesh publishing. + - Enhanced `status` command shows emancipated services with active channels. diff --git a/.claude/TODO.md b/.claude/TODO.md index 11e37101..9db587fc 100644 --- a/.claude/TODO.md +++ b/.claude/TODO.md @@ -83,12 +83,14 @@ _Last updated: 2026-02-05_ - Budget cap: configurable monthly cloud spend limit via LiteLLM. - ANSSI CSPN: Data Classifier + Mistral EU + offline mode = triple sovereignty proof. -13. **Punk Exposure Multi-Domain DNS** - - Multi-domain DNS with P2P exposure and Tor endpoints. - - Classical HTTPS endpoint (DNS provider API: OVH, Gandi, Cloudflare). - - Administrable DNS provider API integration via `dnsctl`. - - Mapped to local services, mesh-federated, locally tweakable. - - Follows Peek / Poke / Emancipate model (see `PUNK-EXPOSURE.md`). +13. ~~**Punk Exposure Multi-Domain DNS**~~ — In Progress (2026-02-05) + - ~~Multi-domain DNS with P2P exposure and Tor endpoints.~~ + - ~~Classical HTTPS endpoint (DNS provider API: OVH, Gandi, Cloudflare).~~ + - ~~Administrable DNS provider API integration via `dnsctl`.~~ + - ~~Mapped to local services, mesh-federated, locally tweakable.~~ + - ~~Follows Peek / Poke / Emancipate model (see `PUNK-EXPOSURE.md`).~~ + - Phase 1 DONE: `emancipate` and `revoke` CLI commands added to secubox-exposure. + - Phase 2-4: Dashboard enhancements pending. 14. **Jellyfin Post-Install** - Complete startup wizard (media library configuration). diff --git a/.claude/WIP.md b/.claude/WIP.md index 671f4a79..37065fb6 100644 --- a/.claude/WIP.md +++ b/.claude/WIP.md @@ -91,6 +91,15 @@ - `formatBits()` helper for network rates (Kbps/Mbps/Gbps). - Cyberpunk theme support for empty state. +- **Punk Exposure Emancipate CLI** + Status: DONE (2026-02-05) + Notes: Phase 1 of multi-channel exposure system. + - `secubox-exposure emancipate [--tor] [--dns] [--mesh] [--all]` + - `secubox-exposure revoke [--tor] [--dns] [--mesh] [--all]` + - UCI tracking for emancipated services with channel status. + - Status command shows emancipated services. + - TODO: Fix mesh integration (secubox-p2p uses different commands). + ## Next Up 1. Rebuild bonus feed with all 2026-02-04/05 changes (IPK files need rebuild). diff --git a/package/secubox/secubox-app-exposure/files/usr/sbin/secubox-exposure b/package/secubox/secubox-app-exposure/files/usr/sbin/secubox-exposure index a3fbb64b..1c5b47b0 100755 --- a/package/secubox/secubox-app-exposure/files/usr/sbin/secubox-exposure +++ b/package/secubox/secubox-app-exposure/files/usr/sbin/secubox-exposure @@ -598,6 +598,286 @@ cmd_ssl_remove() { log_ok "SSL backend removed" } +# ============================================================================ +# EMANCIPATE - Unified Multi-Channel Exposure +# ============================================================================ + +cmd_emancipate() { + local service="$1" + local port="$2" + local domain="$3" + shift 3 2>/dev/null || true + + if [ -z "$service" ] || [ -z "$port" ]; then + log_err "Usage: secubox-exposure emancipate [--tor] [--dns] [--mesh] [--all]" + return 1 + fi + + local enable_tor=0 enable_dns=0 enable_mesh=0 + + # Parse flags + while [ $# -gt 0 ]; do + case "$1" in + --tor) enable_tor=1 ;; + --dns) enable_dns=1 ;; + --mesh) enable_mesh=1 ;; + --all) enable_tor=1; enable_dns=1; enable_mesh=1 ;; + esac + shift + done + + # Default to --all if no flags specified + if [ "$enable_tor" = "0" ] && [ "$enable_dns" = "0" ] && [ "$enable_mesh" = "0" ]; then + enable_tor=1 + enable_dns=1 + enable_mesh=1 + fi + + load_config + + # Validate: check if port is listening + local listen_info=$(netstat -tlnp 2>/dev/null | grep ":$port ") + if [ -z "$listen_info" ]; then + log_warn "Port $port is not currently listening (service may start later)" + else + # Check if localhost-only + local listen_addr=$(echo "$listen_info" | awk '{print $4}' | head -1) + case "$listen_addr" in + 127.0.0.1:*|::1:*) + log_err "Cannot expose localhost-only service (listening on $listen_addr)" + log_err "Service must bind to 0.0.0.0 or a specific LAN IP" + return 1 + ;; + esac + fi + + echo "" + log_info "Emancipating ${CYAN}$service${NC} on port $port" + echo "" + + local success=0 + local failed=0 + + # Channel 1: Tor Hidden Service + if [ "$enable_tor" = "1" ]; then + log_info "Channel 1: Tor Hidden Service..." + if cmd_tor_add "$service" "$port" 80 2>/dev/null; then + local onion=$(cat "$TOR_HIDDEN_DIR/$service/hostname" 2>/dev/null) + if [ -n "$onion" ]; then + echo -e " ${GREEN}✓${NC} Tor: http://$onion" + success=$((success + 1)) + else + echo -e " ${YELLOW}⏳${NC} Tor: Generating onion address..." + success=$((success + 1)) + fi + else + echo -e " ${RED}✗${NC} Tor: Failed to create hidden service" + failed=$((failed + 1)) + fi + fi + + # Channel 2: DNS/SSL via HAProxy + if [ "$enable_dns" = "1" ]; then + if [ -z "$domain" ]; then + echo -e " ${YELLOW}⚠${NC} DNS: Skipped (no domain specified)" + else + log_info "Channel 2: DNS/SSL via HAProxy..." + + # Check for dnsctl + if [ -x "/usr/sbin/dnsctl" ]; then + # Get public IP + local public_ip=$(curl -s --connect-timeout 5 ifconfig.me 2>/dev/null || \ + curl -s --connect-timeout 5 icanhazip.com 2>/dev/null || \ + echo "") + + if [ -n "$public_ip" ]; then + # Extract subdomain from FQDN (e.g., gitea.example.com -> gitea) + local subdomain="${domain%%.*}" + + # Create DNS A record + if /usr/sbin/dnsctl add A "$subdomain" "$public_ip" 2>/dev/null; then + log_ok "DNS A record created: $subdomain -> $public_ip" + else + log_warn "DNS A record creation failed (may already exist)" + fi + else + log_warn "Could not determine public IP for DNS record" + fi + else + log_warn "dnsctl not found - skipping DNS record creation" + fi + + # Create HAProxy vhost + backend + if cmd_ssl_add "$service" "$domain" "$port" 2>/dev/null; then + echo -e " ${GREEN}✓${NC} DNS: https://$domain" + success=$((success + 1)) + + # Request certificate if haproxyctl supports it + if [ -x "/usr/sbin/haproxyctl" ]; then + log_info "Requesting SSL certificate for $domain..." + /usr/sbin/haproxyctl cert add "$domain" 2>/dev/null || \ + log_warn "Certificate request may have failed (check haproxyctl logs)" + fi + else + echo -e " ${RED}✗${NC} DNS: Failed to create HAProxy backend" + failed=$((failed + 1)) + fi + fi + fi + + # Channel 3: Mesh P2P + if [ "$enable_mesh" = "1" ]; then + log_info "Channel 3: Mesh P2P..." + if [ -x "/usr/sbin/secubox-p2p" ]; then + if /usr/sbin/secubox-p2p publish "$service" "$port" "$service" 2>/dev/null; then + echo -e " ${GREEN}✓${NC} Mesh: http://${service}.mesh.local" + success=$((success + 1)) + + # Update mesh DNS + /usr/sbin/secubox-p2p dns-update 2>/dev/null || true + else + echo -e " ${RED}✗${NC} Mesh: Failed to publish service" + failed=$((failed + 1)) + fi + else + echo -e " ${YELLOW}⚠${NC} Mesh: secubox-p2p not installed" + fi + fi + + # Store emancipation state in UCI + uci set "${CONFIG_NAME}.${service}=service" + uci set "${CONFIG_NAME}.${service}.port=$port" + [ -n "$domain" ] && uci set "${CONFIG_NAME}.${service}.domain=$domain" + uci set "${CONFIG_NAME}.${service}.tor=$enable_tor" + uci set "${CONFIG_NAME}.${service}.dns=$enable_dns" + uci set "${CONFIG_NAME}.${service}.mesh=$enable_mesh" + uci set "${CONFIG_NAME}.${service}.emancipated=1" + uci commit "$CONFIG_NAME" + + echo "" + if [ "$failed" -eq 0 ]; then + log_ok "Emancipation complete: $success channel(s) active" + else + log_warn "Emancipation partial: $success succeeded, $failed failed" + fi + echo "" +} + +cmd_revoke() { + local service="$1" + shift 2>/dev/null || true + + if [ -z "$service" ]; then + log_err "Usage: secubox-exposure revoke [--tor] [--dns] [--mesh] [--all]" + return 1 + fi + + local enable_tor=0 enable_dns=0 enable_mesh=0 + + # Parse flags + while [ $# -gt 0 ]; do + case "$1" in + --tor) enable_tor=1 ;; + --dns) enable_dns=1 ;; + --mesh) enable_mesh=1 ;; + --all) enable_tor=1; enable_dns=1; enable_mesh=1 ;; + esac + shift + done + + # Default to --all if no flags specified + if [ "$enable_tor" = "0" ] && [ "$enable_dns" = "0" ] && [ "$enable_mesh" = "0" ]; then + enable_tor=1 + enable_dns=1 + enable_mesh=1 + fi + + load_config + + echo "" + log_info "Revoking exposure for ${CYAN}$service${NC}" + echo "" + + local success=0 + + # Channel 1: Remove Tor Hidden Service + if [ "$enable_tor" = "1" ]; then + if [ -d "$TOR_HIDDEN_DIR/$service" ]; then + log_info "Removing Tor hidden service..." + cmd_tor_remove "$service" 2>/dev/null + echo -e " ${GREEN}✓${NC} Tor: Hidden service removed" + success=$((success + 1)) + else + echo -e " ${YELLOW}−${NC} Tor: No hidden service found" + fi + fi + + # Channel 2: Remove DNS/SSL + if [ "$enable_dns" = "1" ]; then + local domain=$(uci -q get "${CONFIG_NAME}.${service}.domain") + + if [ -n "$domain" ]; then + log_info "Removing DNS/SSL..." + + # Remove DNS record + if [ -x "/usr/sbin/dnsctl" ]; then + local subdomain="${domain%%.*}" + /usr/sbin/dnsctl rm A "$subdomain" 2>/dev/null || true + fi + + # Remove HAProxy vhost/backend + cmd_ssl_remove "$service" 2>/dev/null + echo -e " ${GREEN}✓${NC} DNS: HAProxy vhost removed" + success=$((success + 1)) + + # Remove certificate + if [ -x "/usr/sbin/haproxyctl" ]; then + /usr/sbin/haproxyctl cert remove "$domain" 2>/dev/null || true + fi + else + echo -e " ${YELLOW}−${NC} DNS: No domain configured" + fi + fi + + # Channel 3: Remove from Mesh + if [ "$enable_mesh" = "1" ]; then + if [ -x "/usr/sbin/secubox-p2p" ]; then + log_info "Removing from mesh..." + /usr/sbin/secubox-p2p unpublish "$service" 2>/dev/null || true + /usr/sbin/secubox-p2p dns-update 2>/dev/null || true + echo -e " ${GREEN}✓${NC} Mesh: Service unpublished" + success=$((success + 1)) + else + echo -e " ${YELLOW}−${NC} Mesh: secubox-p2p not installed" + fi + fi + + # Update UCI + if [ "$enable_tor" = "1" ] && [ "$enable_dns" = "1" ] && [ "$enable_mesh" = "1" ]; then + # Full revoke - delete the entire section + uci delete "${CONFIG_NAME}.${service}" 2>/dev/null + else + # Partial revoke - just update flags + [ "$enable_tor" = "1" ] && uci set "${CONFIG_NAME}.${service}.tor=0" + [ "$enable_dns" = "1" ] && uci set "${CONFIG_NAME}.${service}.dns=0" + [ "$enable_mesh" = "1" ] && uci set "${CONFIG_NAME}.${service}.mesh=0" + + # Check if all channels are now disabled + local tor_enabled=$(uci -q get "${CONFIG_NAME}.${service}.tor" || echo "0") + local dns_enabled=$(uci -q get "${CONFIG_NAME}.${service}.dns" || echo "0") + local mesh_enabled=$(uci -q get "${CONFIG_NAME}.${service}.mesh" || echo "0") + + if [ "$tor_enabled" = "0" ] && [ "$dns_enabled" = "0" ] && [ "$mesh_enabled" = "0" ]; then + uci set "${CONFIG_NAME}.${service}.emancipated=0" + fi + fi + uci commit "$CONFIG_NAME" + + echo "" + log_ok "Revocation complete: $success channel(s) removed" + echo "" +} + # ============================================================================ # STATUS & HELP # ============================================================================ @@ -648,6 +928,31 @@ cmd_status() { done [ "$ssl_backends" = "0" ] && echo " (none configured)" echo "" + + # Emancipated services + echo -e "${BLUE}Emancipated Services:${NC}" + local emancipated=0 + for svc in $(uci show "$CONFIG_NAME" 2>/dev/null | grep "=service$" | cut -d'.' -f2 | cut -d'=' -f1); do + local is_emancipated=$(uci -q get "${CONFIG_NAME}.${svc}.emancipated") + [ "$is_emancipated" != "1" ] && continue + + local port=$(uci -q get "${CONFIG_NAME}.${svc}.port") + local domain=$(uci -q get "${CONFIG_NAME}.${svc}.domain") + local tor=$(uci -q get "${CONFIG_NAME}.${svc}.tor") + local dns=$(uci -q get "${CONFIG_NAME}.${svc}.dns") + local mesh=$(uci -q get "${CONFIG_NAME}.${svc}.mesh") + + local channels="" + [ "$tor" = "1" ] && channels="${channels}Tor " + [ "$dns" = "1" ] && channels="${channels}DNS " + [ "$mesh" = "1" ] && channels="${channels}Mesh" + + echo " - ${svc} (:${port}) [${channels}]" + [ -n "$domain" ] && echo " Domain: ${domain}" + emancipated=$((emancipated + 1)) + done + [ "$emancipated" = "0" ] && echo " (none)" + echo "" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" } @@ -663,6 +968,13 @@ COMMANDS: fix-port [port] Change service port (auto-assigns if no port given) status Show exposure status summary + emancipate [flags] + Expose service through multiple channels + Flags: --tor, --dns, --mesh, --all (default) + + revoke [flags] Remove service exposure + Flags: --tor, --dns, --mesh, --all (default) + tor add [port] Create Tor hidden service tor list List hidden services tor remove Remove hidden service @@ -673,17 +985,25 @@ COMMANDS: ssl remove Remove SSL backend EXAMPLES: + # Full emancipation (Tor + DNS + Mesh) + secubox-exposure emancipate gitea 3000 gitea.example.com --all + + # Tor-only exposure + secubox-exposure emancipate streamlit 8501 "" --tor + + # DNS/SSL only + secubox-exposure emancipate ollama 11434 ai.example.com --dns + + # Revoke all channels + secubox-exposure revoke gitea --all + + # Revoke only Tor + secubox-exposure revoke gitea --tor + + # Legacy commands secubox-exposure scan - secubox-exposure conflicts - secubox-exposure fix-port domoticz 8180 - secubox-exposure tor add gitea - secubox-exposure tor add streamlit 8501 80 - secubox-exposure tor list - secubox-exposure ssl add gitea git.example.com - secubox-exposure ssl add streamlit app.example.com 8501 - secubox-exposure ssl list EOF } @@ -706,6 +1026,14 @@ case "$1" in status) cmd_status ;; + emancipate) + shift + cmd_emancipate "$@" + ;; + revoke) + shift + cmd_revoke "$@" + ;; tor) case "$2" in add) cmd_tor_add "$3" "$4" "$5" ;;