secubox-openwrt/package/secubox/secubox-app-tor/files/usr/sbin/torctl
CyberMind-FR fa1f6ddbb8 feat(tor-shield): Add server mode for split-routing with public IP preservation
Server mode routes all outbound traffic through Tor while preserving
inbound connections (HAProxy, etc) on the public IP. Fixes kill switch
blocking response packets by adding ESTABLISHED,RELATED conntrack rule,
and adds PREROUTING chain for LAN client Tor routing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-03 13:46:26 +01:00

530 lines
12 KiB
Bash

#!/bin/sh
# SecuBox Tor Shield - CLI management tool
# Copyright (C) 2025 CyberMind.fr
CONFIG="tor-shield"
TOR_CONTROL="/var/run/tor/control"
TOR_DATA="/var/lib/tor"
. /lib/functions.sh
usage() {
cat << EOF
Usage: torctl <command> [options]
Commands:
status Show Tor Shield status
enable [preset] Enable Tor Shield (presets: anonymous, selective, censored, server)
disable Disable Tor Shield
restart Restart Tor Shield
identity Get new Tor identity (new circuits)
circuits Show active circuits
exit-ip Show current exit IP address
leak-test Test for DNS/IP leaks
bridges Manage bridge configuration
hidden Manage hidden services
Options:
-h, --help Show this help
Examples:
torctl enable anonymous Enable with full anonymity preset
torctl enable server Enable server mode (public IP + Tor outbound)
torctl status Show current status
torctl identity Request new circuits
torctl exit-ip Show Tor exit IP
EOF
}
# Send command to Tor control socket
tor_control() {
if [ ! -S "$TOR_CONTROL" ]; then
echo "Error: Tor control socket not available"
return 1
fi
echo -e "$1" | nc -U "$TOR_CONTROL" 2>/dev/null
}
# Get bootstrap percentage
get_bootstrap() {
local status=$(tor_control "GETINFO status/bootstrap-phase")
echo "$status" | grep "PROGRESS=" | sed 's/.*PROGRESS=\([0-9]*\).*/\1/'
}
# Check if Tor is running
is_running() {
pgrep tor >/dev/null 2>&1
}
# Get current exit IP
get_exit_ip() {
# Try multiple services to get external IP through Tor
local socks_port=$(uci -q get tor-shield.socks.port || echo "9050")
local ip=""
# Try check.torproject.org first
ip=$(curl -s --socks5-hostname 127.0.0.1:$socks_port https://check.torproject.org/api/ip 2>/dev/null | jsonfilter -e '@.IP' 2>/dev/null)
if [ -z "$ip" ]; then
# Fallback to ipinfo.io
ip=$(curl -s --socks5-hostname 127.0.0.1:$socks_port https://ipinfo.io/ip 2>/dev/null)
fi
echo "${ip:-unknown}"
}
# Get real IP (without Tor)
get_real_ip() {
local ip=$(curl -s --max-time 5 https://ipinfo.io/ip 2>/dev/null)
echo "${ip:-unknown}"
}
# Status command
cmd_status() {
local enabled mode bootstrap exit_ip circuit_count
config_load "$CONFIG"
config_get enabled main enabled '0'
config_get mode main mode 'transparent'
echo "Tor Shield Status"
echo "================="
if [ "$enabled" != "1" ]; then
echo "Status: DISABLED"
echo ""
echo "Enable with: torctl enable"
return 0
fi
if ! is_running; then
echo "Status: ENABLED but NOT RUNNING"
echo ""
echo "Start with: /etc/init.d/tor-shield start"
return 1
fi
bootstrap=$(get_bootstrap)
bootstrap=${bootstrap:-0}
echo "Status: ACTIVE"
echo "Mode: $mode"
echo "Bootstrap: ${bootstrap}%"
if [ "$bootstrap" -ge 100 ]; then
exit_ip=$(get_exit_ip)
echo "Exit IP: $exit_ip"
# Get circuit count
circuit_count=$(tor_control "GETINFO circuit-status" | grep -c "BUILT")
echo "Circuits: ${circuit_count:-0}"
else
echo "Exit IP: (bootstrapping...)"
fi
# Show config details
local dns_over_tor kill_switch
config_get dns_over_tor main dns_over_tor '1'
config_get kill_switch main kill_switch '1'
echo ""
echo "Configuration:"
echo " DNS over Tor: $([ "$dns_over_tor" = "1" ] && echo "Yes" || echo "No")"
echo " Kill Switch: $([ "$kill_switch" = "1" ] && echo "Yes" || echo "No")"
# Bridge status
local bridges_enabled
config_get bridges_enabled bridges enabled '0'
if [ "$bridges_enabled" = "1" ]; then
local bridge_type
config_get bridge_type bridges type 'obfs4'
echo " Bridges: $bridge_type"
fi
}
# Enable command
cmd_enable() {
local preset="${1:-anonymous}"
echo "Enabling Tor Shield with preset: $preset"
# Load preset configuration
config_load "$CONFIG"
local preset_mode preset_dns preset_kill preset_bridges preset_lan_proxy
config_get preset_mode "$preset" mode 'transparent'
config_get preset_dns "$preset" dns_over_tor '1'
config_get preset_kill "$preset" kill_switch '1'
config_get preset_bridges "$preset" use_bridges '0'
config_get preset_lan_proxy "$preset" lan_proxy '0'
# Apply preset settings
uci set tor-shield.main.enabled='1'
uci set tor-shield.main.mode="$preset_mode"
uci set tor-shield.main.dns_over_tor="$preset_dns"
uci set tor-shield.main.kill_switch="$preset_kill"
uci set tor-shield.trans.lan_proxy="$preset_lan_proxy"
if [ "$preset_bridges" = "1" ]; then
uci set tor-shield.bridges.enabled='1'
fi
uci commit tor-shield
# Start service
/etc/init.d/tor-shield restart
echo "Tor Shield enabled. Waiting for bootstrap..."
# Wait for bootstrap
local count=0
while [ $count -lt 60 ]; do
if is_running; then
local progress=$(get_bootstrap)
if [ -n "$progress" ] && [ "$progress" -ge 100 ]; then
echo "Bootstrap complete!"
cmd_status
return 0
fi
echo "Bootstrap: ${progress:-0}%"
fi
sleep 2
count=$((count + 1))
done
echo "Warning: Bootstrap taking longer than expected"
cmd_status
}
# Disable command
cmd_disable() {
echo "Disabling Tor Shield..."
uci set tor-shield.main.enabled='0'
uci commit tor-shield
/etc/init.d/tor-shield stop
echo "Tor Shield disabled."
}
# Restart command
cmd_restart() {
echo "Restarting Tor Shield..."
/etc/init.d/tor-shield restart
sleep 2
cmd_status
}
# New identity command
cmd_identity() {
if ! is_running; then
echo "Error: Tor is not running"
return 1
fi
echo "Requesting new identity..."
# Send NEWNYM signal
local result=$(tor_control "SIGNAL NEWNYM")
if echo "$result" | grep -q "250 OK"; then
echo "New identity requested successfully."
echo "New circuits will be established shortly."
sleep 3
echo ""
echo "New exit IP: $(get_exit_ip)"
else
echo "Failed to request new identity"
return 1
fi
}
# Circuits command
cmd_circuits() {
if ! is_running; then
echo "Error: Tor is not running"
return 1
fi
echo "Active Circuits"
echo "==============="
local circuits=$(tor_control "GETINFO circuit-status")
echo "$circuits" | grep "BUILT" | while read line; do
# Parse circuit info
local id=$(echo "$line" | awk '{print $1}')
local status=$(echo "$line" | awk '{print $2}')
local path=$(echo "$line" | awk '{print $3}')
if [ -n "$path" ]; then
# Extract node names/fingerprints
echo "Circuit $id: $path"
fi
done
# Get circuit count
local count=$(echo "$circuits" | grep -c "BUILT")
echo ""
echo "Total built circuits: ${count:-0}"
}
# Exit IP command
cmd_exit_ip() {
if ! is_running; then
echo "Error: Tor is not running"
return 1
fi
local bootstrap=$(get_bootstrap)
if [ -z "$bootstrap" ] || [ "$bootstrap" -lt 100 ]; then
echo "Tor is still bootstrapping (${bootstrap:-0}%)"
return 1
fi
local exit_ip=$(get_exit_ip)
local real_ip=$(get_real_ip)
echo "Real IP: $real_ip"
echo "Tor Exit IP: $exit_ip"
if [ "$exit_ip" != "$real_ip" ] && [ "$exit_ip" != "unknown" ]; then
echo "Status: PROTECTED"
else
echo "Status: WARNING - IPs match or unknown"
fi
}
# Leak test command
cmd_leak_test() {
if ! is_running; then
echo "Error: Tor is not running"
return 1
fi
echo "Running leak test..."
echo ""
local socks_port=$(uci -q get tor-shield.socks.port || echo "9050")
local leaks=0
# Test 1: IP leak
echo "1. IP Leak Test"
local tor_ip=$(get_exit_ip)
local real_ip=$(get_real_ip)
if [ "$tor_ip" = "$real_ip" ] || [ "$tor_ip" = "unknown" ]; then
echo " FAIL: IP may be leaking"
leaks=$((leaks + 1))
else
echo " PASS: Tor IP ($tor_ip) differs from real IP ($real_ip)"
fi
# Test 2: DNS leak (via Tor check)
echo ""
echo "2. Tor Detection Test"
local tor_check=$(curl -s --socks5-hostname 127.0.0.1:$socks_port https://check.torproject.org/api/ip 2>/dev/null)
local is_tor=$(echo "$tor_check" | jsonfilter -e '@.IsTor' 2>/dev/null)
if [ "$is_tor" = "true" ]; then
echo " PASS: Traffic confirmed going through Tor"
else
echo " FAIL: Traffic may not be going through Tor"
leaks=$((leaks + 1))
fi
# Test 3: DNS resolution through Tor
echo ""
echo "3. DNS Over Tor Test"
local dns_over_tor=$(uci -q get tor-shield.main.dns_over_tor)
if [ "$dns_over_tor" = "1" ]; then
# Try resolving a .onion address (only works through Tor)
local dns_test=$(curl -s --socks5-hostname 127.0.0.1:$socks_port http://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/ 2>/dev/null | head -c 100)
if [ -n "$dns_test" ]; then
echo " PASS: DNS resolution working through Tor"
else
echo " WARNING: Could not verify DNS over Tor"
fi
else
echo " SKIP: DNS over Tor is disabled"
fi
echo ""
echo "===================="
if [ $leaks -eq 0 ]; then
echo "Result: ALL TESTS PASSED"
else
echo "Result: $leaks POTENTIAL LEAK(S) DETECTED"
fi
}
# Bridges command
cmd_bridges() {
local action="${1:-status}"
case "$action" in
status)
local enabled type
config_load "$CONFIG"
config_get enabled bridges enabled '0'
config_get type bridges type 'obfs4'
echo "Bridge Configuration"
echo "===================="
echo "Enabled: $([ "$enabled" = "1" ] && echo "Yes" || echo "No")"
echo "Type: $type"
echo ""
echo "Bridge lines:"
config_list_foreach bridges bridge_lines echo_bridge
;;
enable)
uci set tor-shield.bridges.enabled='1'
uci commit tor-shield
echo "Bridges enabled. Restart Tor Shield to apply."
;;
disable)
uci set tor-shield.bridges.enabled='0'
uci commit tor-shield
echo "Bridges disabled. Restart Tor Shield to apply."
;;
add)
shift
local bridge_line="$*"
if [ -n "$bridge_line" ]; then
uci add_list tor-shield.bridges.bridge_lines="$bridge_line"
uci commit tor-shield
echo "Bridge added. Restart Tor Shield to apply."
else
echo "Usage: torctl bridges add <bridge_line>"
fi
;;
*)
echo "Usage: torctl bridges [status|enable|disable|add <line>]"
;;
esac
}
echo_bridge() {
echo " $1"
}
# Hidden services command
cmd_hidden() {
local action="${1:-list}"
case "$action" in
list)
echo "Hidden Services"
echo "==============="
config_load "$CONFIG"
config_foreach list_hidden_service hidden_service
;;
add)
shift
local name="$1"
local local_port="${2:-80}"
local virtual_port="${3:-80}"
if [ -z "$name" ]; then
echo "Usage: torctl hidden add <name> [local_port] [virtual_port]"
return 1
fi
# Create new hidden service config
uci set tor-shield.hs_$name=hidden_service
uci set tor-shield.hs_$name.name="$name"
uci set tor-shield.hs_$name.enabled='1'
uci set tor-shield.hs_$name.local_port="$local_port"
uci set tor-shield.hs_$name.virtual_port="$virtual_port"
uci commit tor-shield
echo "Hidden service '$name' created."
echo "Restart Tor Shield to generate .onion address."
;;
remove)
local name="$2"
if [ -z "$name" ]; then
echo "Usage: torctl hidden remove <name>"
return 1
fi
uci delete tor-shield.hs_$name 2>/dev/null
uci commit tor-shield
echo "Hidden service '$name' removed."
;;
*)
echo "Usage: torctl hidden [list|add|remove]"
;;
esac
}
list_hidden_service() {
local cfg="$1"
local enabled name local_port virtual_port
config_get enabled "$cfg" enabled '0'
config_get name "$cfg" name "$cfg"
config_get local_port "$cfg" local_port '80'
config_get virtual_port "$cfg" virtual_port '80'
local status="disabled"
[ "$enabled" = "1" ] && status="enabled"
local hostname_file="$TOR_DATA/hidden_service_$name/hostname"
local onion_addr="(not generated)"
if [ -f "$hostname_file" ]; then
onion_addr=$(cat "$hostname_file")
fi
echo ""
echo "Service: $name ($status)"
echo " Address: $onion_addr"
echo " Port: $virtual_port -> 127.0.0.1:$local_port"
}
# Main dispatcher
case "$1" in
status)
cmd_status
;;
enable)
shift
cmd_enable "$@"
;;
disable)
cmd_disable
;;
restart)
cmd_restart
;;
identity|new-identity|newnym)
cmd_identity
;;
circuits|circuit)
cmd_circuits
;;
exit-ip|ip)
cmd_exit_ip
;;
leak-test|leaks|test)
cmd_leak_test
;;
bridges|bridge)
shift
cmd_bridges "$@"
;;
hidden|hs|onion)
shift
cmd_hidden "$@"
;;
-h|--help|help)
usage
;;
*)
usage
exit 1
;;
esac