Files
torware/torware.sh

9909 lines
437 KiB
Bash
Raw Permalink Normal View History

#!/bin/bash
#
# ╔═════════════════════════════════════════════════════════ ╗
# ║ 🧅 TORWARE v1.1 ║
# ║ ║
# ║ One-click setup for Tor Bridge/Relay nodes ║
# ║ ║
# ║ • Installs Docker (if needed) ║
# ║ • Runs Tor Bridge/Relay in Docker with live stats ║
# ║ • Snowflake WebRTC proxy support ║
# ║ • Auto-start on boot via systemd/OpenRC/SysVinit ║
# ║ • Easy management via CLI or interactive menu ║
# ║ ║
# ║ Tor Project: https://www.torproject.org/ ║
# ╚═════════════════════════════════════════════════════════ ╝
#
# Usage:
# curl -sL https://git.samnet.dev/SamNet-dev/torware/raw/branch/main/torware.sh | sudo bash
#
# Tor relay types:
# Bridge (obfs4) - Hidden entry point, helps censored users (DEFAULT)
# Middle Relay - Routes traffic within Tor network
# Exit Relay - Final hop, higher risk (advanced users only)
#
set -eo pipefail
# Ensure consistent numeric formatting across locales
export LC_NUMERIC=C
# Require bash 4.2+ (for associative arrays and declare -g)
if [ -z "$BASH_VERSION" ]; then
echo "Error: This script requires bash. Please run with: bash $0"
exit 1
fi
if [ "${BASH_VERSINFO[0]:-0}" -lt 4 ] || { [ "${BASH_VERSINFO[0]:-0}" -eq 4 ] && [ "${BASH_VERSINFO[1]:-0}" -lt 2 ]; }; then
echo "Error: This script requires bash 4.2+ (found $BASH_VERSION). Please upgrade bash."
exit 1
fi
# Global temp file tracking for cleanup
_TMP_FILES=()
_cleanup_tmp() {
for f in "${_TMP_FILES[@]}"; do
rm -rf "$f" 2>/dev/null || true
done
# Clean caches and CPU state for THIS process
rm -f "${TMPDIR:-/tmp}"/torware_cpu_state_$$ 2>/dev/null || true
rm -f "${TMPDIR:-/tmp}"/.tg_curl.* 2>/dev/null || true
rm -f "${TMPDIR:-/tmp}"/.tor_fp_cache_* 2>/dev/null || true
rm -f "${TMPDIR:-/tmp}"/.tor_cookie_cache_* 2>/dev/null || true
}
trap '_cleanup_tmp' EXIT
VERSION="1.1"
# Docker image tags — pinned to specific versions for reproducibility. Use :latest for auto-updates.
BRIDGE_IMAGE="thetorproject/obfs4-bridge:0.24"
RELAY_IMAGE="osminogin/tor-simple:0.4.8.10"
SNOWFLAKE_IMAGE="thetorproject/snowflake-proxy:latest"
INSTALL_DIR="${INSTALL_DIR:-/opt/torware}"
# Validate INSTALL_DIR is absolute
case "$INSTALL_DIR" in
/*) ;; # OK
*) echo "Error: INSTALL_DIR must be an absolute path"; exit 1 ;;
esac
BACKUP_DIR="$INSTALL_DIR/backups"
STATS_DIR="$INSTALL_DIR/relay_stats"
CONTAINERS_DIR="$INSTALL_DIR/containers"
FORCE_REINSTALL=false
# Defaults
RELAY_TYPE="bridge"
NICKNAME=""
CONTACT_INFO=""
BANDWIDTH=5 # Mbps
CONTAINER_COUNT=1
ORPORT_BASE=9001
CONTROLPORT_BASE=9051
PT_PORT_BASE=9100
DATA_CAP_GB=0
# Snowflake proxy settings
SNOWFLAKE_ENABLED="false"
SNOWFLAKE_COUNT=1
SNOWFLAKE_CONTAINER="snowflake-proxy"
SNOWFLAKE_VOLUME="snowflake-data"
SNOWFLAKE_METRICS_PORT=9999
SNOWFLAKE_CPUS="1.5"
SNOWFLAKE_MEMORY="512m"
SNOWFLAKE_CPUS_1=""
SNOWFLAKE_MEMORY_1=""
SNOWFLAKE_CPUS_2=""
SNOWFLAKE_MEMORY_2=""
# Unbounded (Lantern) proxy settings
UNBOUNDED_ENABLED="false"
UNBOUNDED_CONTAINER="unbounded-proxy"
UNBOUNDED_VOLUME="unbounded-data"
UNBOUNDED_IMAGE="torware/unbounded-widget:latest"
UNBOUNDED_CPUS="0.5"
UNBOUNDED_MEMORY="256m"
UNBOUNDED_FREDDIE="https://freddie.iantem.io"
UNBOUNDED_EGRESS="wss://unbounded.iantem.io"
UNBOUNDED_TAG=""
# MTProxy (Telegram) settings
MTPROXY_ENABLED="false"
MTPROXY_CONTAINER="mtproxy"
MTPROXY_IMAGE="nineseconds/mtg:2.1.7"
MTPROXY_PORT=8443
MTPROXY_METRICS_PORT=3129
MTPROXY_DOMAIN="cloudflare.com"
MTPROXY_SECRET=""
MTPROXY_CPUS="0.5"
MTPROXY_MEMORY="128m"
MTPROXY_CONCURRENCY=8192
MTPROXY_BLOCKLIST_COUNTRIES=""
#═══════════════════════════════════════════════════════════════════════
# Centralized Configuration
#═══════════════════════════════════════════════════════════════════════
declare -gA CONFIG=(
# Relay Configuration
[relay_type]="bridge"
[relay_nickname]=""
[relay_contact]=""
[relay_bandwidth]=5
[relay_container_count]=1
[relay_orport_base]=9001
[relay_controlport_base]=9051
[relay_ptport_base]=9100
[relay_data_cap_gb]=0
[relay_exit_policy]="reduced"
# Snowflake Proxy
[snowflake_enabled]="false"
[snowflake_count]=1
[snowflake_cpus]="1.5"
[snowflake_memory]="512m"
# Unbounded Proxy
[unbounded_enabled]="false"
[unbounded_cpus]="0.5"
[unbounded_memory]="256m"
# MTProxy (Telegram)
[mtproxy_enabled]="false"
[mtproxy_port]=8443
[mtproxy_domain]="cloudflare.com"
[mtproxy_secret]=""
[mtproxy_concurrency]=8192
# Telegram Integration
[telegram_enabled]="false"
[telegram_bot_token]=""
[telegram_chat_id]=""
[telegram_interval]=6
)
config_get() {
local key="$1"
echo "${CONFIG[$key]:-}"
}
config_set() {
local key="$1"
local val="$2"
CONFIG[$key]="$val"
}
config_sync_from_globals() {
CONFIG[relay_type]="${RELAY_TYPE:-bridge}"
CONFIG[relay_nickname]="${NICKNAME:-}"
CONFIG[relay_contact]="${CONTACT_INFO:-}"
CONFIG[relay_bandwidth]="${BANDWIDTH:-5}"
CONFIG[relay_container_count]="${CONTAINER_COUNT:-1}"
CONFIG[relay_orport_base]="${ORPORT_BASE:-9001}"
CONFIG[relay_controlport_base]="${CONTROLPORT_BASE:-9051}"
CONFIG[relay_ptport_base]="${PT_PORT_BASE:-9100}"
CONFIG[relay_data_cap_gb]="${DATA_CAP_GB:-0}"
CONFIG[snowflake_enabled]="${SNOWFLAKE_ENABLED:-false}"
CONFIG[snowflake_count]="${SNOWFLAKE_COUNT:-1}"
CONFIG[unbounded_enabled]="${UNBOUNDED_ENABLED:-false}"
CONFIG[mtproxy_enabled]="${MTPROXY_ENABLED:-false}"
CONFIG[mtproxy_port]="${MTPROXY_PORT:-8443}"
CONFIG[mtproxy_domain]="${MTPROXY_DOMAIN:-cloudflare.com}"
CONFIG[telegram_enabled]="${TELEGRAM_ENABLED:-false}"
CONFIG[telegram_bot_token]="${TELEGRAM_BOT_TOKEN:-}"
CONFIG[telegram_chat_id]="${TELEGRAM_CHAT_ID:-}"
}
config_sync_to_globals() {
RELAY_TYPE="${CONFIG[relay_type]}"
NICKNAME="${CONFIG[relay_nickname]}"
CONTACT_INFO="${CONFIG[relay_contact]}"
BANDWIDTH="${CONFIG[relay_bandwidth]}"
CONTAINER_COUNT="${CONFIG[relay_container_count]}"
ORPORT_BASE="${CONFIG[relay_orport_base]}"
CONTROLPORT_BASE="${CONFIG[relay_controlport_base]}"
PT_PORT_BASE="${CONFIG[relay_ptport_base]}"
DATA_CAP_GB="${CONFIG[relay_data_cap_gb]}"
SNOWFLAKE_ENABLED="${CONFIG[snowflake_enabled]}"
SNOWFLAKE_COUNT="${CONFIG[snowflake_count]}"
UNBOUNDED_ENABLED="${CONFIG[unbounded_enabled]}"
MTPROXY_ENABLED="${CONFIG[mtproxy_enabled]}"
MTPROXY_PORT="${CONFIG[mtproxy_port]}"
MTPROXY_DOMAIN="${CONFIG[mtproxy_domain]}"
TELEGRAM_ENABLED="${CONFIG[telegram_enabled]}"
TELEGRAM_BOT_TOKEN="${CONFIG[telegram_bot_token]}"
TELEGRAM_CHAT_ID="${CONFIG[telegram_chat_id]}"
}
# Colors — disable when stdout is not a terminal
if [ -t 1 ]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' MAGENTA='' BOLD='' DIM='' NC=''
fi
#═══════════════════════════════════════════════════════════════════════
# Utility Functions
#═══════════════════════════════════════════════════════════════════════
# Structured logging support
LOG_FORMAT="${LOG_FORMAT:-text}" # text or json
LOG_FILE="${LOG_FILE:-}" # Optional file output
log_json() {
local level="$1" msg="$2"
local ts
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
# Escape special JSON characters in message
msg="${msg//\\/\\\\}"
msg="${msg//\"/\\\"}"
msg="${msg//$'\n'/\\n}"
msg="${msg//$'\r'/\\r}"
msg="${msg//$'\t'/\\t}"
local json="{\"timestamp\":\"$ts\",\"level\":\"$level\",\"message\":\"$msg\"}"
if [ -n "$LOG_FILE" ]; then
echo "$json" >> "$LOG_FILE"
fi
if [ "$LOG_FORMAT" = "json" ]; then
echo "$json"
return 0
fi
return 1
}
print_header() {
local W=57
local bar="═════════════════════════════════════════════════════════"
echo -e "${CYAN}"
echo "${bar}"
# 🧅 emoji = 2 display cols but 1 char; " 🧅 " = 5 display cols, printf sees 4 → use W-5+1=53
printf "║ 🧅 %-52s║\n" "TORWARE v${VERSION}"
echo "${bar}"
# — is 1 display col, 1 char; normal printf works. Content: 2+53+2=57
printf "║ %-57s║\n" "Tor Bridge/Relay nodes — easy setup & management"
echo "${bar}"
echo -e "${NC}"
}
log_info() {
log_json "INFO" "$1" || echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
log_json "SUCCESS" "$1" || echo -e "${GREEN}[✓]${NC} $1"
}
log_warn() {
log_json "WARN" "$1" || echo -e "${YELLOW}[!]${NC} $1" >&2
}
log_error() {
log_json "ERROR" "$1" || echo -e "${RED}[✗]${NC} $1" >&2
}
check_root() {
if [ "$EUID" -ne 0 ]; then
log_error "This script must be run as root (use sudo)"
exit 1
fi
}
validate_port() {
local port="$1"
[[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ]
}
format_bytes() {
local bytes=$1
[[ "$bytes" =~ ^[0-9]+$ ]] || bytes=0
if [ -z "$bytes" ] || [ "$bytes" = "0" ]; then
echo "0 B"
return
fi
if [ "$bytes" -lt 1024 ] 2>/dev/null; then
echo "${bytes} B"
elif [ "$bytes" -lt 1048576 ] 2>/dev/null; then
echo "$(awk -v b="$bytes" 'BEGIN {printf "%.1f", b/1024}') KB"
elif [ "$bytes" -lt 1073741824 ] 2>/dev/null; then
echo "$(awk -v b="$bytes" 'BEGIN {printf "%.2f", b/1048576}') MB"
else
echo "$(awk -v b="$bytes" 'BEGIN {printf "%.2f", b/1073741824}') GB"
fi
}
format_number() {
local num=$1
if [ -z "$num" ] || [ "$num" = "0" ]; then
echo "0"
return
fi
if [ "$num" -ge 1000000 ] 2>/dev/null; then
echo "$(awk -v n="$num" 'BEGIN {printf "%.1f", n/1000000}')M"
elif [ "$num" -ge 1000 ] 2>/dev/null; then
echo "$(awk -v n="$num" 'BEGIN {printf "%.1f", n/1000}')K"
else
echo "$num"
fi
}
format_duration() {
local secs=$1
[[ "$secs" =~ ^-?[0-9]+$ ]] || secs=0
if [ "$secs" -lt 1 ]; then
echo "0s"
return
fi
local days=$((secs / 86400))
local hours=$(( (secs % 86400) / 3600 ))
local mins=$(( (secs % 3600) / 60 ))
local remaining=$((secs % 60))
if [ "$days" -gt 0 ]; then
echo "${days}d ${hours}h ${mins}m"
elif [ "$hours" -gt 0 ]; then
echo "${hours}h ${mins}m"
elif [ "$mins" -gt 0 ]; then
echo "${mins}m"
else
echo "${remaining}s"
fi
}
escape_md() {
local text="$1"
text="${text//\\/\\\\}"
text="${text//\*/\\*}"
text="${text//_/\\_}"
text="${text//\`/\\\`}"
text="${text//\[/\\[}"
text="${text//\]/\\]}"
echo "$text"
}
# Convert 2-letter country code to human-readable name
country_code_to_name() {
local cc="$1"
case "$cc" in
# Americas (18)
us) echo "United States" ;; ca) echo "Canada" ;; mx) echo "Mexico" ;;
br) echo "Brazil" ;; ar) echo "Argentina" ;; co) echo "Colombia" ;;
cl) echo "Chile" ;; pe) echo "Peru" ;; ve) echo "Venezuela" ;;
ec) echo "Ecuador" ;; bo) echo "Bolivia" ;; py) echo "Paraguay" ;;
uy) echo "Uruguay" ;; cu) echo "Cuba" ;; cr) echo "Costa Rica" ;;
pa) echo "Panama" ;; do) echo "Dominican Rep." ;; gt) echo "Guatemala" ;;
# Europe West (16)
gb|uk) echo "United Kingdom" ;; de) echo "Germany" ;; fr) echo "France" ;;
nl) echo "Netherlands" ;; be) echo "Belgium" ;; at) echo "Austria" ;;
ch) echo "Switzerland" ;; ie) echo "Ireland" ;; lu) echo "Luxembourg" ;;
es) echo "Spain" ;; pt) echo "Portugal" ;; it) echo "Italy" ;;
gr) echo "Greece" ;; mt) echo "Malta" ;; cy) echo "Cyprus" ;;
is) echo "Iceland" ;;
# Europe North (7)
se) echo "Sweden" ;; no) echo "Norway" ;; fi) echo "Finland" ;;
dk) echo "Denmark" ;; ee) echo "Estonia" ;; lv) echo "Latvia" ;;
lt) echo "Lithuania" ;;
# Europe East (16)
pl) echo "Poland" ;; cz) echo "Czech Rep." ;; sk) echo "Slovakia" ;;
hu) echo "Hungary" ;; ro) echo "Romania" ;; bg) echo "Bulgaria" ;;
hr) echo "Croatia" ;; rs) echo "Serbia" ;; si) echo "Slovenia" ;;
ba) echo "Bosnia" ;; mk) echo "N. Macedonia" ;; al) echo "Albania" ;;
me) echo "Montenegro" ;; ua) echo "Ukraine" ;; md) echo "Moldova" ;;
by) echo "Belarus" ;;
# Russia & Central Asia (6)
ru) echo "Russia" ;; kz) echo "Kazakhstan" ;; uz) echo "Uzbekistan" ;;
tm) echo "Turkmenistan" ;; kg) echo "Kyrgyzstan" ;; tj) echo "Tajikistan" ;;
# Middle East (10)
tr) echo "Turkey" ;; il) echo "Israel" ;; sa) echo "Saudi Arabia" ;;
ae) echo "UAE" ;; ir) echo "Iran" ;; iq) echo "Iraq" ;;
sy) echo "Syria" ;; jo) echo "Jordan" ;; lb) echo "Lebanon" ;;
qa) echo "Qatar" ;;
# Africa (12)
za) echo "South Africa" ;; ng) echo "Nigeria" ;; ke) echo "Kenya" ;;
eg) echo "Egypt" ;; ma) echo "Morocco" ;; tn) echo "Tunisia" ;;
gh) echo "Ghana" ;; et) echo "Ethiopia" ;; tz) echo "Tanzania" ;;
ug) echo "Uganda" ;; dz) echo "Algeria" ;; ly) echo "Libya" ;;
# Asia East (8)
cn) echo "China" ;; jp) echo "Japan" ;; kr) echo "South Korea" ;;
tw) echo "Taiwan" ;; hk) echo "Hong Kong" ;; mn) echo "Mongolia" ;;
kp) echo "North Korea" ;; mo) echo "Macau" ;;
# Asia South & Southeast (14)
in) echo "India" ;; pk) echo "Pakistan" ;; bd) echo "Bangladesh" ;;
np) echo "Nepal" ;; lk) echo "Sri Lanka" ;; mm) echo "Myanmar" ;;
th) echo "Thailand" ;; vn) echo "Vietnam" ;; ph) echo "Philippines" ;;
id) echo "Indonesia" ;; my) echo "Malaysia" ;; sg) echo "Singapore" ;;
kh) echo "Cambodia" ;; la) echo "Laos" ;;
# Oceania & Caucasus (6)
au) echo "Australia" ;; nz) echo "New Zealand" ;; ge) echo "Georgia" ;;
am) echo "Armenia" ;; az) echo "Azerbaijan" ;; bh) echo "Bahrain" ;;
mu) echo "Mauritius" ;; zm) echo "Zambia" ;; sd) echo "Sudan" ;;
zw) echo "Zimbabwe" ;; mz) echo "Mozambique" ;; cm) echo "Cameroon" ;;
ci) echo "Ivory Coast" ;; sn) echo "Senegal" ;; cd) echo "DR Congo" ;;
ao) echo "Angola" ;; om) echo "Oman" ;; kw) echo "Kuwait" ;;
'??') echo "Unknown" ;;
*) echo "$cc" | tr '[:lower:]' '[:upper:]' ;;
esac
}
#═══════════════════════════════════════════════════════════════════════
# OS Detection & Package Management
#═══════════════════════════════════════════════════════════════════════
detect_os() {
OS="unknown"
OS_VERSION="unknown"
OS_FAMILY="unknown"
HAS_SYSTEMD=false
PKG_MANAGER="unknown"
if [ -f /etc/os-release ] && [ -r /etc/os-release ]; then
# Read only the specific vars we need (avoid executing arbitrary content)
OS=$(sed -n 's/^ID=//p' /etc/os-release | tr -d '"' | head -1)
OS_VERSION=$(sed -n 's/^VERSION_ID=//p' /etc/os-release | tr -d '"' | head -1)
OS="${OS:-unknown}"
OS_VERSION="${OS_VERSION:-unknown}"
elif [ -f /etc/redhat-release ]; then
OS="rhel"
elif [ -f /etc/debian_version ]; then
OS="debian"
elif [ -f /etc/alpine-release ]; then
OS="alpine"
elif [ -f /etc/arch-release ]; then
OS="arch"
elif [ -f /etc/SuSE-release ] || [ -f /etc/SUSE-brand ]; then
OS="opensuse"
else
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
fi
case "$OS" in
ubuntu|debian|linuxmint|pop|elementary|zorin|kali|raspbian)
OS_FAMILY="debian"
PKG_MANAGER="apt"
;;
rhel|centos|fedora|rocky|almalinux|oracle|amazon|amzn)
OS_FAMILY="rhel"
if command -v dnf &>/dev/null; then
PKG_MANAGER="dnf"
else
PKG_MANAGER="yum"
fi
;;
arch|manjaro|endeavouros|garuda)
OS_FAMILY="arch"
PKG_MANAGER="pacman"
;;
opensuse|opensuse-leap|opensuse-tumbleweed|sles)
OS_FAMILY="suse"
PKG_MANAGER="zypper"
;;
alpine)
OS_FAMILY="alpine"
PKG_MANAGER="apk"
;;
*)
OS_FAMILY="unknown"
PKG_MANAGER="unknown"
;;
esac
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
HAS_SYSTEMD=true
fi
log_info "Detected: $OS ($OS_FAMILY family), Package manager: $PKG_MANAGER"
if command -v podman &>/dev/null && ! command -v docker &>/dev/null; then
log_warn "Podman detected. This script is optimized for Docker."
log_warn "If installation fails, consider installing 'docker-ce' manually."
fi
}
install_package() {
local package="$1"
log_info "Installing $package..."
case "$PKG_MANAGER" in
apt)
apt-get update -q || log_warn "apt-get update failed, attempting install anyway..."
if apt-get install -y -q "$package"; then
log_success "$package installed successfully"
else
log_error "Failed to install $package"
return 1
fi
;;
dnf)
if dnf install -y -q "$package"; then
log_success "$package installed successfully"
else
log_error "Failed to install $package"
return 1
fi
;;
yum)
if yum install -y -q "$package"; then
log_success "$package installed successfully"
else
log_error "Failed to install $package"
return 1
fi
;;
pacman)
if pacman -S --noconfirm --needed "$package"; then
log_success "$package installed successfully"
else
log_error "Failed to install $package"
return 1
fi
;;
zypper)
if zypper install -y -n "$package"; then
log_success "$package installed successfully"
else
log_error "Failed to install $package"
return 1
fi
;;
apk)
if apk add --no-cache "$package"; then
log_success "$package installed successfully"
else
log_error "Failed to install $package"
return 1
fi
;;
*)
log_warn "Unknown package manager. Please install $package manually."
return 1
;;
esac
}
check_dependencies() {
if [ "$OS_FAMILY" = "alpine" ]; then
if ! command -v bash &>/dev/null; then
log_info "Installing bash..."
apk add --no-cache bash 2>/dev/null
fi
fi
if ! command -v curl &>/dev/null; then
install_package curl || log_warn "Could not install curl automatically"
fi
if ! command -v awk &>/dev/null; then
case "$PKG_MANAGER" in
apt) install_package gawk || log_warn "Could not install gawk" ;;
apk) install_package gawk || log_warn "Could not install gawk" ;;
*) install_package awk || log_warn "Could not install awk" ;;
esac
fi
if ! command -v free &>/dev/null; then
case "$PKG_MANAGER" in
apt|dnf|yum) install_package procps || log_warn "Could not install procps" ;;
pacman) install_package procps-ng || log_warn "Could not install procps" ;;
zypper) install_package procps || log_warn "Could not install procps" ;;
apk) install_package procps || log_warn "Could not install procps" ;;
esac
fi
if ! command -v tput &>/dev/null; then
case "$PKG_MANAGER" in
apt) install_package ncurses-bin || log_warn "Could not install ncurses-bin" ;;
apk) install_package ncurses || log_warn "Could not install ncurses" ;;
*) install_package ncurses || log_warn "Could not install ncurses" ;;
esac
fi
# netcat for ControlPort communication
if ! command -v nc &>/dev/null && ! command -v ncat &>/dev/null; then
case "$PKG_MANAGER" in
apt) install_package netcat-openbsd || log_warn "Could not install netcat" ;;
dnf|yum) install_package nmap-ncat || log_warn "Could not install ncat" ;;
pacman) install_package openbsd-netcat || log_warn "Could not install netcat" ;;
zypper) install_package netcat-openbsd || log_warn "Could not install netcat" ;;
apk) install_package netcat-openbsd || log_warn "Could not install netcat" ;;
esac
fi
# GeoIP (for country stats on middle/exit relays — bridges use Tor's built-in CLIENTS_SEEN)
# Only needed if user runs non-bridge relay types
if ! command -v geoiplookup &>/dev/null && ! command -v mmdblookup &>/dev/null; then
log_info "GeoIP tools not found (optional — needed for middle/exit relay country stats)"
log_info "Bridges use Tor's built-in country data and don't need GeoIP."
case "$PKG_MANAGER" in
apt)
install_package geoip-bin 2>/dev/null || true
install_package geoip-database 2>/dev/null || true
;;
dnf|yum)
install_package GeoIP 2>/dev/null || true
;;
pacman) install_package geoip 2>/dev/null || true ;;
zypper) install_package GeoIP 2>/dev/null || true ;;
apk) install_package geoip 2>/dev/null || true ;;
*) true ;;
esac
fi
# qrencode is optional — only useful for bridge line QR codes
if ! command -v qrencode &>/dev/null; then
install_package qrencode 2>/dev/null || log_info "qrencode not installed (optional — for bridge line QR codes)"
fi
}
#═══════════════════════════════════════════════════════════════════════
# System Resource Detection
#═══════════════════════════════════════════════════════════════════════
get_ram_mb() {
local ram=""
if command -v free &>/dev/null; then
ram=$(free -m 2>/dev/null | awk '/^Mem:/{print $2}')
fi
if [ -z "$ram" ] || [ "$ram" = "0" ]; then
if [ -f /proc/meminfo ]; then
local kb=$(awk '/^MemTotal:/{print $2}' /proc/meminfo 2>/dev/null)
if [ -n "$kb" ]; then
ram=$((kb / 1024))
fi
fi
fi
if [ -z "$ram" ] || [ "$ram" -lt 1 ] 2>/dev/null; then
echo 1 # Fallback: 1MB (prevents division-by-zero in callers)
else
echo "$ram"
fi
}
get_cpu_cores() {
local cores=1
if command -v nproc &>/dev/null; then
cores=$(nproc)
elif [ -f /proc/cpuinfo ]; then
cores=$(grep -c ^processor /proc/cpuinfo)
fi
if [ -z "$cores" ] || [ "$cores" -lt 1 ] 2>/dev/null; then
echo 1
else
echo "$cores"
fi
}
get_public_ip() {
# Try multiple services to get public IP
local ip=""
ip=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null) ||
ip=$(curl -s --max-time 5 https://ifconfig.me 2>/dev/null) ||
ip=$(curl -s --max-time 5 https://icanhazip.com 2>/dev/null) ||
ip=""
# Validate it looks like an IP (basic check)
if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || [[ "$ip" =~ : ]]; then
echo "$ip"
else
echo ""
fi
}
get_net_speed() {
local iface
iface=$(ip route 2>/dev/null | awk '/default/{print $5; exit}')
if [ -z "$iface" ]; then
echo "0.00 0.00"
return
fi
local rx1 tx1 rx2 tx2
rx1=$(cat /sys/class/net/"$iface"/statistics/rx_bytes 2>/dev/null || echo 0)
tx1=$(cat /sys/class/net/"$iface"/statistics/tx_bytes 2>/dev/null || echo 0)
sleep 0.5
rx2=$(cat /sys/class/net/"$iface"/statistics/rx_bytes 2>/dev/null || echo 0)
tx2=$(cat /sys/class/net/"$iface"/statistics/tx_bytes 2>/dev/null || echo 0)
local rx_speed tx_speed
rx_speed=$(awk -v r1="$rx1" -v r2="$rx2" 'BEGIN {printf "%.2f", (r2 - r1) * 2 * 8 / 1000000}')
tx_speed=$(awk -v t1="$tx1" -v t2="$tx2" 'BEGIN {printf "%.2f", (t2 - t1) * 2 * 8 / 1000000}')
echo "$rx_speed $tx_speed"
}
#═══════════════════════════════════════════════════════════════════════
# Docker Installation
#═══════════════════════════════════════════════════════════════════════
install_docker() {
if command -v docker &>/dev/null; then
log_success "Docker is already installed"
return 0
fi
log_info "Installing Docker..."
if [ "$OS_FAMILY" = "rhel" ]; then
log_info "Adding Docker repo for RHEL..."
if command -v dnf &>/dev/null; then
dnf install -y -q dnf-plugins-core 2>/dev/null || true
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo 2>/dev/null || true
else
yum install -y -q yum-utils 2>/dev/null || true
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo 2>/dev/null || true
fi
fi
if [ "$OS_FAMILY" = "alpine" ]; then
if ! apk add --no-cache docker docker-cli-compose 2>/dev/null; then
log_error "Failed to install Docker on Alpine"
return 1
fi
rc-update add docker boot 2>/dev/null || true
service docker start 2>/dev/null || rc-service docker start 2>/dev/null || true
else
# Download Docker install script first, then execute (safer than piping curl|sh)
local docker_script
docker_script=$(mktemp) || { log_error "Failed to create temp file"; return 1; }
if ! curl -fsSL https://get.docker.com -o "$docker_script"; then
rm -f "$docker_script"
log_error "Failed to download Docker installation script."
log_info "Try installing docker manually: https://docs.docker.com/engine/install/"
return 1
fi
if ! sh "$docker_script"; then
rm -f "$docker_script"
log_error "Official Docker installation script failed."
log_info "Try installing docker manually: https://docs.docker.com/engine/install/"
return 1
fi
rm -f "$docker_script"
if [ "$HAS_SYSTEMD" = "true" ]; then
systemctl enable docker 2>/dev/null || true
systemctl start docker 2>/dev/null || true
else
if command -v update-rc.d &>/dev/null; then
update-rc.d docker defaults 2>/dev/null || true
elif command -v chkconfig &>/dev/null; then
chkconfig docker on 2>/dev/null || true
elif command -v rc-update &>/dev/null; then
rc-update add docker default 2>/dev/null || true
fi
service docker start 2>/dev/null || /etc/init.d/docker start 2>/dev/null || true
fi
fi
sleep 3
local retries=27
while ! docker info &>/dev/null && [ $retries -gt 0 ]; do
sleep 1
retries=$((retries - 1))
done
if docker info &>/dev/null; then
log_success "Docker installed successfully"
else
log_error "Docker installation may have failed. Please check manually."
return 1
fi
}
#═══════════════════════════════════════════════════════════════════════
# Container Naming & Configuration Helpers
#═══════════════════════════════════════════════════════════════════════
get_container_name() {
local idx=$1
if [ "$idx" -le 1 ] 2>/dev/null; then
echo "torware"
else
echo "torware-${idx}"
fi
}
get_volume_name() {
local idx=$1
echo "relay-data-${idx}"
}
get_snowflake_name() {
local idx=$1
if [ "$idx" -le 1 ] 2>/dev/null; then
echo "snowflake-proxy"
else
echo "snowflake-proxy-${idx}"
fi
}
get_snowflake_volume() {
local idx=$1
if [ "$idx" -le 1 ] 2>/dev/null; then
echo "snowflake-data"
else
echo "snowflake-data-${idx}"
fi
}
get_snowflake_metrics_port() {
local idx=$1
echo $((10000 - idx))
}
get_snowflake_cpus() {
local idx=$1
local var="SNOWFLAKE_CPUS_${idx}"
local val="${!var}"
if [ -n "$val" ]; then
echo "$val"
else
echo "${SNOWFLAKE_CPUS:-1.5}"
fi
}
get_snowflake_memory() {
local idx=$1
local var="SNOWFLAKE_MEMORY_${idx}"
local val="${!var}"
if [ -n "$val" ]; then
echo "$val"
else
echo "${SNOWFLAKE_MEMORY:-512m}"
fi
}
get_snowflake_default_cpus() {
local cores=$(nproc 2>/dev/null || echo 1)
if [ "$cores" -ge 2 ]; then
echo "1.5"
else
echo "1.0"
fi
}
get_snowflake_default_memory() {
local total_mb=$(awk '/MemTotal/ {printf "%.0f", $2/1024}' /proc/meminfo 2>/dev/null || echo 0)
if [ "$total_mb" -ge 3500 ]; then
echo "1g"
else
echo "512m"
fi
}
get_unbounded_default_cpus() {
local cores=$(nproc 2>/dev/null || echo 1)
if [ "$cores" -ge 2 ]; then
echo "0.5"
else
echo "0.25"
fi
}
get_unbounded_default_memory() {
local total_mb=$(awk '/MemTotal/ {printf "%.0f", $2/1024}' /proc/meminfo 2>/dev/null || echo 0)
if [ "$total_mb" -ge 1024 ]; then
echo "256m"
else
echo "128m"
fi
}
is_unbounded_running() {
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${UNBOUNDED_CONTAINER}$"
}
get_container_orport() {
local idx=$1
local var="ORPORT_${idx}"
local val="${!var}"
if [ -n "$val" ]; then
echo "$val"
else
echo $((ORPORT_BASE + idx - 1))
fi
}
get_container_controlport() {
local idx=$1
echo $((CONTROLPORT_BASE + idx - 1))
}
get_container_ptport() {
local idx=$1
local var="PT_PORT_${idx}"
local val="${!var}"
if [ -n "$val" ]; then
echo "$val"
else
echo $((PT_PORT_BASE + idx - 1))
fi
}
get_container_bandwidth() {
local idx=$1
local var="BANDWIDTH_${idx}"
local val="${!var}"
if [ -n "$val" ]; then
echo "$val"
else
echo "$BANDWIDTH"
fi
}
get_container_cpus() {
local idx=$1
local var="CPUS_${idx}"
echo "${!var}"
}
get_container_memory() {
local idx=$1
local var="MEMORY_${idx}"
echo "${!var}"
}
get_container_relay_type() {
local idx=$1
local var="RELAY_TYPE_${idx}"
local val="${!var}"
if [ -n "$val" ]; then
echo "$val"
else
echo "$RELAY_TYPE"
fi
}
get_container_nickname() {
local idx=$1
if [ "$idx" -le 1 ] 2>/dev/null; then
echo "$NICKNAME"
else
echo "${NICKNAME}${idx}"
fi
}
#═══════════════════════════════════════════════════════════════════════
# Torrc Generation
#═══════════════════════════════════════════════════════════════════════
generate_torrc() {
local idx=$1
local orport=$(get_container_orport $idx)
local controlport=$(get_container_controlport $idx)
local ptport=$(get_container_ptport $idx)
local bw=$(get_container_bandwidth $idx)
local nick=$(get_container_nickname $idx)
local torrc_dir="$CONTAINERS_DIR/relay-${idx}"
mkdir -p "$torrc_dir"
# Sanitize contact info for torrc (strip quotes that could break the directive)
local safe_contact
safe_contact=$(printf '%s' "$CONTACT_INFO" | tr -d '\042\134')
# Convert bandwidth from Mbps to bytes/sec for torrc
local bw_bytes=""
local bw_burst=""
if [ "$bw" != "-1" ] && [ -n "$bw" ]; then
# BandwidthRate in bytes per second (Mbps * 125000 = bytes/sec)
bw_bytes=$(awk -v b="$bw" 'BEGIN {printf "%.0f", b * 125000}')
# Burst = 2x rate
bw_burst=$(awk -v b="$bw" 'BEGIN {printf "%.0f", b * 250000}')
fi
local torrc_file="$torrc_dir/torrc"
cat > "$torrc_file" << EOF
# Torware Configuration - Generated by torware.sh v${VERSION}
# Container: $(get_container_name $idx)
# Generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC')
# Identity
Nickname ${nick}
ContactInfo "${safe_contact}"
# Network Ports
ORPort ${orport}
ControlPort 127.0.0.1:${controlport}
# Authentication
CookieAuthentication 1
# Data directory
DataDirectory /var/lib/tor
EOF
# Relay-type specific configuration
local rtype=$(get_container_relay_type $idx)
case "$rtype" in
bridge)
cat >> "$torrc_file" << EOF
# Bridge Configuration (obfs4)
BridgeRelay 1
ServerTransportPlugin obfs4 exec /usr/bin/obfs4proxy
ServerTransportListenAddr obfs4 0.0.0.0:${ptport}
ExtORPort auto
PublishServerDescriptor bridge
EOF
;;
middle)
cat >> "$torrc_file" << EOF
# Middle Relay Configuration
ExitRelay 0
ExitPolicy reject *:*
ExitPolicy reject6 *:*
PublishServerDescriptor v3
EOF
;;
exit)
cat >> "$torrc_file" << EOF
# Exit Relay Configuration
$(generate_exit_policy)
PublishServerDescriptor v3
EOF
;;
esac
# Bandwidth limits
if [ -n "$bw_bytes" ]; then
cat >> "$torrc_file" << EOF
# Bandwidth Limits
BandwidthRate ${bw_bytes}
BandwidthBurst ${bw_burst}
RelayBandwidthRate ${bw_bytes}
RelayBandwidthBurst ${bw_burst}
EOF
fi
# Data cap / accounting
if [ "${DATA_CAP_GB}" -gt 0 ] 2>/dev/null; then
cat >> "$torrc_file" << EOF
# Traffic Accounting
AccountingMax ${DATA_CAP_GB} GB
AccountingStart month 1 00:00
EOF
fi
# Logging
cat >> "$torrc_file" << EOF
# Logging
Log notice stdout
EOF
chmod 644 "$torrc_file"
}
generate_exit_policy() {
case "${EXIT_POLICY:-reduced}" in
reduced)
cat << 'EOF'
# Reduced Exit Policy (web traffic only)
ExitPolicy accept *:80
ExitPolicy accept *:443
ExitPolicy reject *:*
ExitPolicy reject6 *:*
EOF
;;
default)
cat << 'EOF'
# Default Exit Policy (common ports)
ExitPolicy accept *:20-23
ExitPolicy accept *:43
ExitPolicy accept *:53
ExitPolicy accept *:79-81
ExitPolicy accept *:88
ExitPolicy accept *:110
ExitPolicy accept *:143
ExitPolicy accept *:194
ExitPolicy accept *:220
ExitPolicy accept *:389
ExitPolicy accept *:443
ExitPolicy accept *:465
ExitPolicy accept *:531
ExitPolicy accept *:543-544
ExitPolicy accept *:554
ExitPolicy accept *:563
ExitPolicy accept *:587
ExitPolicy accept *:636
ExitPolicy accept *:706
ExitPolicy accept *:749
ExitPolicy accept *:873
ExitPolicy accept *:902-904
ExitPolicy accept *:981
ExitPolicy accept *:989-995
ExitPolicy accept *:1194
ExitPolicy accept *:1220
ExitPolicy accept *:1293
ExitPolicy accept *:1500
ExitPolicy accept *:1533
ExitPolicy accept *:1677
ExitPolicy accept *:1723
ExitPolicy accept *:1755
ExitPolicy accept *:1863
ExitPolicy accept *:2082-2083
ExitPolicy accept *:2086-2087
ExitPolicy accept *:2095-2096
ExitPolicy accept *:2102-2104
ExitPolicy accept *:3128
ExitPolicy accept *:3389
ExitPolicy accept *:3690
ExitPolicy accept *:4321
ExitPolicy accept *:4643
ExitPolicy accept *:5050
ExitPolicy accept *:5190
ExitPolicy accept *:5222-5223
ExitPolicy accept *:5228
ExitPolicy accept *:5900
ExitPolicy accept *:6660-6669
ExitPolicy accept *:6679
ExitPolicy accept *:6697
ExitPolicy accept *:8000
ExitPolicy accept *:8008
ExitPolicy accept *:8074
ExitPolicy accept *:8080
ExitPolicy accept *:8082
ExitPolicy accept *:8087-8088
ExitPolicy accept *:8232-8233
ExitPolicy accept *:8332-8333
ExitPolicy accept *:8443
ExitPolicy accept *:8888
ExitPolicy accept *:9418
ExitPolicy accept *:11371
ExitPolicy accept *:19294
ExitPolicy accept *:19638
ExitPolicy accept *:50002
ExitPolicy accept *:64738
ExitPolicy reject *:*
ExitPolicy reject6 *:*
EOF
;;
full)
cat << 'EOF'
# Full Exit Policy (all ports - HIGHEST RISK)
ExitPolicy accept *:*
ExitPolicy accept6 *:*
EOF
;;
esac
}
#═══════════════════════════════════════════════════════════════════════
# ControlPort Communication
#═══════════════════════════════════════════════════════════════════════
# Get the netcat command available on this system
get_nc_cmd() {
if command -v ncat &>/dev/null; then
echo "ncat"
elif command -v nc &>/dev/null; then
echo "nc"
else
echo ""
fi
}
# Read cookie from Docker volume and convert to hex (cached for 60s)
get_control_cookie() {
local idx=$1
local vol=$(get_volume_name $idx)
local cache_file="${TMPDIR:-/tmp}/.tor_cookie_cache_${idx}"
# Symlink protection - refuse to use if symlink
if [ -L "$cache_file" ]; then
rm -f "$cache_file" 2>/dev/null
fi
# Use cache if fresh and regular file (avoid spawning Docker container every call)
if [ -f "$cache_file" ] && [ ! -L "$cache_file" ]; then
local mtime
mtime=$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null || echo 0)
local age=$(( $(date +%s) - mtime ))
if [ "$age" -lt 60 ] && [ "$age" -ge 0 ]; then
cat "$cache_file"
return
fi
fi
local cookie
cookie=$(docker run --rm -v "${vol}:/data:ro" alpine \
sh -c 'od -A n -t x1 /data/control_auth_cookie 2>/dev/null | tr -d " \n"' 2>/dev/null)
if [ -n "$cookie" ]; then
# Write to temp file then atomically move to prevent TOCTOU race
local tmp_cache
tmp_cache=$(mktemp "${TMPDIR:-/tmp}/.tor_cookie_cache_${idx}.XXXXXX" 2>/dev/null) || return
( umask 077; echo "$cookie" > "$tmp_cache" ) && mv -f "$tmp_cache" "$cache_file" 2>/dev/null
fi
echo "$cookie"
}
# Query ControlPort with authentication
controlport_query() {
local port=$1
shift
local nc_cmd=$(get_nc_cmd)
if [ -z "$nc_cmd" ]; then
return 1
fi
# Build the query: authenticate, run commands, quit
local idx=$((port - CONTROLPORT_BASE + 1))
local cookie=$(get_control_cookie $idx)
if [ -z "$cookie" ]; then
return 1
fi
{
printf 'AUTHENTICATE %s\r\n' "$cookie"
for cmd in "$@"; do
printf "%s\r\n" "$cmd"
done
printf "QUIT\r\n"
} | timeout 5 "$nc_cmd" 127.0.0.1 "$port" 2>/dev/null | tr -d '\r'
}
# Get traffic bytes (read, written) from a container
get_tor_traffic() {
local idx=$1
local port=$(get_container_controlport $idx)
local result
result=$(controlport_query "$port" "GETINFO traffic/read" "GETINFO traffic/written" 2>/dev/null)
local read_bytes write_bytes
read_bytes=$(echo "$result" | sed -n 's/.*traffic\/read=\([0-9]*\).*/\1/p' | head -1 2>/dev/null || echo "0")
write_bytes=$(echo "$result" | sed -n 's/.*traffic\/written=\([0-9]*\).*/\1/p' | head -1 2>/dev/null || echo "0")
echo "$read_bytes $write_bytes"
}
# Get circuit count from a container
get_tor_circuits() {
local idx=$1
local port=$(get_container_controlport $idx)
local result
result=$(controlport_query "$port" "GETINFO circuit-status" 2>/dev/null)
# Count lines that start with a circuit ID (number)
local count
count=$(echo "$result" | grep -cE '^[0-9]+ (BUILT|EXTENDED|LAUNCHED)' 2>/dev/null || echo "0")
count=${count//[^0-9]/}
echo "${count:-0}"
}
# Get OR connection count
get_tor_connections() {
local idx=$1
local port=$(get_container_controlport $idx)
local result
result=$(controlport_query "$port" "GETINFO orconn-status" 2>/dev/null)
local count
count=$(echo "$result" | grep -c '\$' 2>/dev/null || echo "0")
count=${count//[^0-9]/}
echo "${count:-0}"
}
# Get accounting info (data cap usage)
get_tor_accounting() {
local idx=$1
local port=$(get_container_controlport $idx)
local result
result=$(controlport_query "$port" \
"GETINFO accounting/enabled" \
"GETINFO accounting/bytes" \
"GETINFO accounting/bytes-left" \
"GETINFO accounting/interval-end" 2>/dev/null)
echo "$result"
}
# Get relay fingerprint (cached for 300s to avoid spawning Docker container every call)
get_tor_fingerprint() {
local idx=$1
local cache_file="${TMPDIR:-/tmp}/.tor_fp_cache_${idx}"
# Symlink protection - refuse to use if symlink
if [ -L "$cache_file" ]; then
rm -f "$cache_file" 2>/dev/null
fi
if [ -f "$cache_file" ] && [ ! -L "$cache_file" ]; then
local mtime
mtime=$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null || echo 0)
local age=$(( $(date +%s) - mtime ))
if [ "$age" -lt 300 ] && [ "$age" -ge 0 ]; then
cat "$cache_file"
return
fi
fi
local vol=$(get_volume_name $idx)
local fp
fp=$(docker run --rm -v "${vol}:/data:ro" alpine \
cat /data/fingerprint 2>/dev/null | awk '{print $2}')
if [ -n "$fp" ]; then
# Write to temp file then atomically move to prevent TOCTOU race
local tmp_cache
tmp_cache=$(mktemp "${TMPDIR:-/tmp}/.tor_fp_cache_${idx}.XXXXXX" 2>/dev/null) || return
( umask 077; echo "$fp" > "$tmp_cache" ) && mv -f "$tmp_cache" "$cache_file" 2>/dev/null
fi
echo "$fp"
}
# Cached external IP (avoids repeated curl calls)
_CACHED_EXTERNAL_IP=""
_get_external_ip() {
if [ -z "$_CACHED_EXTERNAL_IP" ]; then
_CACHED_EXTERNAL_IP=$(curl -s --max-time 5 ifconfig.me 2>/dev/null || echo "")
fi
echo "$_CACHED_EXTERNAL_IP"
}
# Get bridge line (for obfs4 bridges)
get_bridge_line() {
local idx=$1
local cname=$(get_container_name $idx)
local raw
raw=$(docker exec "$cname" cat /var/lib/tor/pt_state/obfs4_bridgeline.txt 2>/dev/null | grep -vE '^#|^$' | head -1)
if [ -n "$raw" ]; then
# Replace placeholders with real IP, port, and fingerprint
local ip
ip=$(_get_external_ip)
local ptport=$(get_container_ptport "$idx")
local fp=$(get_tor_fingerprint "$idx")
if [ -n "$ip" ]; then
raw="${raw/<IP ADDRESS>/$ip}"
raw="${raw/<PORT>/$ptport}"
raw="${raw/<FINGERPRINT>/$fp}"
fi
echo "$raw"
fi
}
# Get bridge client country stats (bridges only — uses Tor's built-in CLIENTS_SEEN)
# Returns lines like: ir=50 cn=120 us=30
get_tor_clients_seen() {
local idx=$1
local port=$(get_container_controlport $idx)
local result
result=$(controlport_query "$port" "GETINFO status/clients-seen" 2>/dev/null)
# Extract the CountrySummary field: cc=num,cc=num,...
local countries
countries=$(echo "$result" | sed -n 's/.*CountrySummary=\([^ \r]*\).*/\1/p' | head -1 2>/dev/null)
echo "$countries"
}
# Resolve IP to country via Tor ControlPort
tor_geo_lookup() {
local ip=$1
# Validate IP format (IPv4 only)
if ! [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "??"
return
fi
local idx=${2:-1}
local port=$(get_container_controlport $idx)
local result
result=$(controlport_query "$port" "GETINFO ip-to-country/$ip" 2>/dev/null)
local sed_safe_ip=$(printf '%s' "$ip" | sed 's/[.]/\\./g; s/[/]/\\//g')
echo "$result" | sed -n "s/.*ip-to-country\/${sed_safe_ip}=\([^\r]*\)/\1/p" | head -1 || echo "??"
}
#═══════════════════════════════════════════════════════════════════════
# Interactive Setup Wizard
#═══════════════════════════════════════════════════════════════════════
prompt_relay_settings() {
while true; do
local ram_mb=$(get_ram_mb)
local cpu_cores=$(get_cpu_cores)
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} TORWARE CONFIGURATION ${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BOLD}Server Info:${NC}"
echo -e " CPU Cores: ${GREEN}${cpu_cores}${NC}"
if [ "$ram_mb" -ge 1000 ]; then
local ram_gb=$(awk -v m="$ram_mb" 'BEGIN {printf "%.1f", m/1024}')
echo -e " RAM: ${GREEN}${ram_gb} GB${NC}"
else
echo -e " RAM: ${GREEN}${ram_mb} MB${NC}"
fi
echo ""
# ── Suggested Setup Modes ──
echo -e " ${BOLD}Choose a Setup Mode:${NC}"
echo ""
echo -e " ${DIM}Options 1-4: You can add Snowflake, Lantern, or Telegram proxy later from the menu.${NC}"
echo ""
echo -e " ${GREEN}${BOLD} 1. Single Bridge${NC} (${BOLD}RECOMMENDED${NC})"
echo -e " 1 obfs4 bridge container — ideal for most users"
echo -e " Helps censored users connect to Tor"
echo -e " Your IP stays PRIVATE (not publicly listed)"
echo -e " Zero legal risk, zero abuse complaints"
echo ""
echo -e " ${GREEN} 2. Multi-Bridge${NC}"
echo -e " 2-5 bridge containers on the same IP"
echo -e " Each bridge gets its own identity and ORPort"
echo -e " All bridges stay private — safe to run together"
echo -e " Best for servers with spare CPU/RAM"
echo ""
echo -e " ${YELLOW} 3. Middle Relay${NC}"
echo -e " 1 middle relay container — routes Tor traffic"
echo -e " Your IP WILL be publicly listed in the Tor consensus"
echo -e " No exit traffic, so lower risk than an exit relay"
echo -e " Helps increase Tor network capacity"
echo ""
echo -e " ${DIM} 4. Custom${NC}"
echo -e " Choose relay type and container count manually"
echo -e " Includes exit relay option (advanced, legal risks)"
echo ""
echo -e " ${CYAN} 5. Snowflake Only${NC}"
echo -e " No Tor relay — just run Snowflake WebRTC proxy"
echo -e " Helps censored users connect to Tor"
echo -e " Lightweight, no IP exposure, zero config"
echo ""
echo -e " ${CYAN} 6. Unbounded Only (Lantern)${NC}"
echo -e " No Tor relay — just run Lantern Unbounded WebRTC proxy"
echo -e " Helps censored users connect via Lantern network"
echo -e " Lightweight, no IP exposure, zero config"
echo ""
echo -e " ${CYAN} 7. Telegram Proxy Only (MTProxy)${NC}"
echo -e " No Tor relay — just run MTProxy for Telegram"
echo -e " Helps censored users access Telegram"
echo -e " FakeTLS disguises traffic as HTTPS, share link/QR"
echo ""
echo -e " ${DIM} 0. Exit${NC}"
echo ""
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " Choose setup mode [0-7] (default: 1)"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " mode: " input_mode < /dev/tty || true
case "${input_mode:-1}" in
1|"")
RELAY_TYPE="bridge"
CONTAINER_COUNT=1
echo -e " Selected: ${GREEN}Single Bridge (obfs4)${NC}"
;;
2)
RELAY_TYPE="bridge"
# Recommend container count based on resources
local rec_multi=2
if [ "$cpu_cores" -ge 4 ] && [ "$ram_mb" -ge 4096 ]; then
rec_multi=3
fi
echo ""
echo -e " How many bridge containers? (2-5, default: ${rec_multi})"
echo -e " ${DIM}System: ${cpu_cores} CPU core(s), ${ram_mb}MB RAM${NC}"
read -p " bridges: " input_multi < /dev/tty || true
if [ -z "$input_multi" ]; then
CONTAINER_COUNT=$rec_multi
elif [[ "$input_multi" =~ ^[2-5]$ ]]; then
CONTAINER_COUNT=$input_multi
else
log_warn "Invalid input. Using default: ${rec_multi}"
CONTAINER_COUNT=$rec_multi
fi
echo -e " Selected: ${GREEN}${CONTAINER_COUNT}x Bridge (obfs4)${NC}"
;;
3)
RELAY_TYPE="middle"
CONTAINER_COUNT=1
echo -e " Selected: ${YELLOW}Middle Relay${NC}"
echo ""
echo -e " ${DIM}Note: Your IP will be publicly listed in the Tor consensus.${NC}"
echo -e " ${DIM}This is normal for middle relays and poses low risk.${NC}"
;;
4)
local _custom_needs_count=true
# ── Custom: Relay Type ──
echo ""
echo -e " ${BOLD}Relay Type:${NC}"
echo -e " ${GREEN}1. Bridge (obfs4)${NC} — IP stays private, helps censored users"
echo -e " ${YELLOW}2. Middle Relay${NC} — IP is public, routes Tor traffic"
echo -e " ${RED}3. Exit Relay${NC} — ADVANCED: IP is public, receives abuse complaints"
echo ""
read -p " relay type [1-3] (default: 1): " input_type < /dev/tty || true
case "${input_type:-1}" in
1|"")
RELAY_TYPE="bridge"
echo -e " Selected: ${GREEN}Bridge (obfs4)${NC}"
;;
2)
RELAY_TYPE="middle"
echo -e " Selected: ${YELLOW}Middle Relay${NC}"
;;
3)
echo ""
echo -e "${RED}╔═══════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}║ EXIT RELAY — LEGAL WARNING ║${NC}"
echo -e "${RED}╠═══════════════════════════════════════════════════════════════════╣${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ Running an exit relay means: ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ • Your IP will be publicly listed as a Tor exit ║${NC}"
echo -e "${RED}║ • Abuse complaints may be sent to your ISP ║${NC}"
echo -e "${RED}║ • Some services may block your IP ║${NC}"
echo -e "${RED}║ • You may receive DMCA notices or legal requests ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ Learn more: ║${NC}"
echo -e "${RED}║ https://community.torproject.org/relay/community-resources/ ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}╚═══════════════════════════════════════════════════════════════════╝${NC}"
echo ""
read -p " Type 'I UNDERSTAND' to continue, or press Enter for Bridge: " confirm_exit < /dev/tty || true
if [ "$confirm_exit" = "I UNDERSTAND" ]; then
RELAY_TYPE="exit"
echo -e " Selected: ${RED}Exit Relay${NC}"
echo ""
echo -e " ${BOLD}Exit Policy:${NC}"
echo -e " ${GREEN}a. Reduced${NC} (ports 80, 443 only) - ${BOLD}RECOMMENDED${NC}"
echo -e " ${YELLOW}b. Default${NC} (common ports)"
echo -e " ${RED}c. Full${NC} (all ports) - HIGHEST RISK"
echo ""
read -p " Exit policy [a/b/c] (default: a): " ep_choice < /dev/tty || true
case "${ep_choice:-a}" in
b) EXIT_POLICY="default" ;;
c) EXIT_POLICY="full" ;;
*) EXIT_POLICY="reduced" ;;
esac
else
RELAY_TYPE="bridge"
echo -e " Defaulting to: ${GREEN}Bridge (obfs4)${NC}"
fi
;;
*)
RELAY_TYPE="bridge"
echo -e " Invalid input. Defaulting to: ${GREEN}Bridge (obfs4)${NC}"
;;
esac
# Custom mode: skip to container count below (handled after this case)
;;
5)
# Snowflake-only mode: no Tor relay
RELAY_TYPE="none"
CONTAINER_COUNT=0
SNOWFLAKE_ENABLED="true"
echo -e " Selected: ${CYAN}Snowflake Only${NC} (no Tor relay)"
echo ""
echo -e " ${DIM}You can run up to 2 Snowflake instances. Each registers${NC}"
echo -e " ${DIM}independently with the broker for more client assignments.${NC}"
read -p " Number of Snowflake instances (1-2) [1]: " _sf_count < /dev/tty || true
if [ "$_sf_count" = "2" ]; then
SNOWFLAKE_COUNT=2
else
SNOWFLAKE_COUNT=1
fi
local _def_cpu=$(get_snowflake_default_cpus)
local _def_mem=$(get_snowflake_default_memory)
for _si in $(seq 1 $SNOWFLAKE_COUNT); do
echo ""
if [ "$SNOWFLAKE_COUNT" -gt 1 ]; then
echo -e " ${BOLD}Instance #${_si}:${NC}"
fi
read -p " CPU cores [${_def_cpu}]: " _sf_cpu < /dev/tty || true
read -p " RAM limit [${_def_mem}]: " _sf_mem < /dev/tty || true
[ -z "$_sf_cpu" ] && _sf_cpu="$_def_cpu"
[ -z "$_sf_mem" ] && _sf_mem="$_def_mem"
[[ "$_sf_cpu" =~ ^[0-9]+\.?[0-9]*$ ]] || _sf_cpu="$_def_cpu"
[[ "$_sf_mem" =~ ^[0-9]+[mMgG]$ ]] || _sf_mem="$_def_mem"
_sf_mem=$(echo "$_sf_mem" | tr '[:upper:]' '[:lower:]')
eval "SNOWFLAKE_CPUS_${_si}=\"$_sf_cpu\""
eval "SNOWFLAKE_MEMORY_${_si}=\"$_sf_mem\""
done
echo -e " Snowflake: ${GREEN}${SNOWFLAKE_COUNT} instance(s)${NC}"
;;
6)
# Unbounded-only mode: no Tor relay
RELAY_TYPE="none"
CONTAINER_COUNT=0
UNBOUNDED_ENABLED="true"
echo -e " Selected: ${CYAN}Unbounded Only${NC} (Lantern network, no Tor relay)"
echo ""
local _def_ub_cpu=$(get_unbounded_default_cpus)
local _def_ub_mem=$(get_unbounded_default_memory)
read -p " CPU cores [${_def_ub_cpu}]: " _ub_cpu < /dev/tty || true
read -p " RAM limit [${_def_ub_mem}]: " _ub_mem < /dev/tty || true
[ -z "$_ub_cpu" ] && _ub_cpu="$_def_ub_cpu"
[ -z "$_ub_mem" ] && _ub_mem="$_def_ub_mem"
[[ "$_ub_cpu" =~ ^[0-9]+\.?[0-9]*$ ]] || _ub_cpu="$_def_ub_cpu"
[[ "$_ub_mem" =~ ^[0-9]+[mMgG]$ ]] || _ub_mem="$_def_ub_mem"
_ub_mem=$(echo "$_ub_mem" | tr '[:upper:]' '[:lower:]')
UNBOUNDED_CPUS="$_ub_cpu"
UNBOUNDED_MEMORY="$_ub_mem"
echo -e " Unbounded: ${GREEN}Enabled${NC} (CPU: ${_ub_cpu}, RAM: ${_ub_mem})"
;;
7)
# MTProxy-only mode: no Tor relay
RELAY_TYPE="none"
CONTAINER_COUNT=0
MTPROXY_ENABLED="true"
echo -e " Selected: ${CYAN}Telegram Proxy Only${NC} (MTProxy, no Tor relay)"
echo ""
echo -e " ${DIM}FakeTLS domain (any popular HTTPS site — you don't need to own it):${NC}"
echo -e " ${DIM} Traffic will appear as normal HTTPS to this domain.${NC}"
echo -e " ${DIM} 1. cloudflare.com (recommended)${NC}"
echo -e " ${DIM} 2. google.com${NC}"
echo -e " ${DIM} 3. Custom (any HTTPS site, e.g. microsoft.com, amazon.com)${NC}"
read -p " Domain choice [1]: " _mtp_dom_choice < /dev/tty || true
case "${_mtp_dom_choice:-1}" in
2) MTPROXY_DOMAIN="google.com" ;;
3)
read -p " Enter domain (e.g. microsoft.com): " _mtp_custom_dom < /dev/tty || true
MTPROXY_DOMAIN="${_mtp_custom_dom:-cloudflare.com}"
;;
*) MTPROXY_DOMAIN="cloudflare.com" ;;
esac
echo ""
read -p " Port [8443]: " _mtp_port < /dev/tty || true
MTPROXY_PORT="${_mtp_port:-8443}"
[[ "$MTPROXY_PORT" =~ ^[0-9]+$ ]] || MTPROXY_PORT=8443
echo ""
local _def_mtp_cpu="0.5"
local _def_mtp_mem="128m"
read -p " CPU cores [${_def_mtp_cpu}]: " _mtp_cpu < /dev/tty || true
read -p " RAM limit [${_def_mtp_mem}]: " _mtp_mem < /dev/tty || true
[ -z "$_mtp_cpu" ] && _mtp_cpu="$_def_mtp_cpu"
[ -z "$_mtp_mem" ] && _mtp_mem="$_def_mtp_mem"
[[ "$_mtp_cpu" =~ ^[0-9]+\.?[0-9]*$ ]] || _mtp_cpu="$_def_mtp_cpu"
[[ "$_mtp_mem" =~ ^[0-9]+[mMgG]$ ]] || _mtp_mem="$_def_mtp_mem"
_mtp_mem=$(echo "$_mtp_mem" | tr '[:upper:]' '[:lower:]')
MTPROXY_CPUS="$_mtp_cpu"
MTPROXY_MEMORY="$_mtp_mem"
MTPROXY_SECRET="" # Will be generated on first run
echo -e " MTProxy: ${GREEN}Enabled${NC} (Port: ${MTPROXY_PORT}, Domain: ${MTPROXY_DOMAIN})"
echo ""
echo -e " ${DIM}Tip: You can configure connection limits and geo-blocking in the main menu.${NC}"
;;
0)
echo -e " ${YELLOW}Exiting setup.${NC}"
exit 0
;;
*)
RELAY_TYPE="bridge"
CONTAINER_COUNT=1
echo -e " Invalid input. Defaulting to: ${GREEN}Single Bridge (obfs4)${NC}"
;;
esac
# Skip relay config for proxy-only modes (Snowflake-only or Unbounded-only)
if [ "$RELAY_TYPE" = "none" ]; then
# Jump to summary confirmation
:
else
# ── Container Count (custom mode only — modes 1-3 already set CONTAINER_COUNT) ──
if [ "${input_mode:-1}" = "4" ] && [ "${_custom_needs_count:-false}" = "true" ]; then
CONTAINER_COUNT="" # will be set below
local rec_containers=1
if [ "$cpu_cores" -ge 4 ] && [ "$ram_mb" -ge 4096 ]; then
rec_containers=2
fi
echo ""
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " How many containers to run? (1-5)"
echo -e " Each gets a unique ORPort and identity"
echo -e " ${DIM}System: ${cpu_cores} CPU core(s), ${ram_mb}MB RAM${NC}"
echo -e " Press Enter for default: ${GREEN}${rec_containers}${NC}"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " containers: " input_containers < /dev/tty || true
if [ -z "$input_containers" ]; then
CONTAINER_COUNT=$rec_containers
elif [[ "$input_containers" =~ ^[1-5]$ ]]; then
CONTAINER_COUNT=$input_containers
else
log_warn "Invalid input. Using default: ${rec_containers}"
CONTAINER_COUNT=$rec_containers
fi
fi
# ── Per-Container Relay Type (custom mode, multiple containers) ──
if [ "${input_mode:-1}" = "4" ] && [ "$CONTAINER_COUNT" -gt 1 ]; then
echo ""
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " Use the same relay type (${GREEN}${RELAY_TYPE}${NC}) for all containers?"
echo -e " Selecting 'n' lets you assign different types per container"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " Same type for all? [Y/n] " same_type < /dev/tty || true
if [[ "$same_type" =~ ^[Nn]$ ]]; then
echo ""
for ci in $(seq 1 $CONTAINER_COUNT); do
echo -e " ${BOLD}Container $ci:${NC} [1=Bridge, 2=Middle, 3=Exit] (default: 1)"
read -p " type: " ct_choice < /dev/tty || true
case "${ct_choice:-1}" in
2)
printf -v "RELAY_TYPE_${ci}" '%s' "middle"
echo -e " -> ${YELLOW}Middle Relay${NC}"
;;
3)
printf -v "RELAY_TYPE_${ci}" '%s' "exit"
echo -e " -> ${RED}Exit Relay${NC}"
;;
*)
printf -v "RELAY_TYPE_${ci}" '%s' "bridge"
echo -e " -> ${GREEN}Bridge${NC}"
;;
esac
done
# Check for mixed bridge + relay on same host (unsafe per Tor Project guidance)
local _has_bridge=false _has_relay=false
for ci in $(seq 1 $CONTAINER_COUNT); do
local _crt=$(get_container_relay_type $ci)
[ "$_crt" = "bridge" ] && _has_bridge=true || _has_relay=true
done
if [ "$_has_bridge" = "true" ] && [ "$_has_relay" = "true" ]; then
echo ""
echo -e " ${RED}╔══════════════════════════════════════════════════════════╗${NC}"
echo -e " ${RED}║ WARNING: Bridge + Relay on same IP is NOT recommended ║${NC}"
echo -e " ${RED}╠══════════════════════════════════════════════════════════╣${NC}"
echo -e " ${RED}${NC} Bridges are UNLISTED — their IPs are kept private. ${RED}${NC}"
echo -e " ${RED}${NC} Middle relays are PUBLICLY LISTED in the Tor consensus. ${RED}${NC}"
echo -e " ${RED}${NC} ${RED}${NC}"
echo -e " ${RED}${NC} Running both on the same IP exposes your bridge's IP ${RED}${NC}"
echo -e " ${RED}${NC} through the public relay listing, defeating the purpose ${RED}${NC}"
echo -e " ${RED}${NC} of running a bridge. ${RED}${NC}"
echo -e " ${RED}${NC} ${RED}${NC}"
echo -e " ${RED}${NC} Safe combos: all bridges OR all middle relays. ${RED}${NC}"
echo -e " ${RED}╚══════════════════════════════════════════════════════════╝${NC}"
echo ""
read -p " Continue anyway? [y/N] " _mix_confirm < /dev/tty || true
if [[ ! "$_mix_confirm" =~ ^[Yy]$ ]]; then
echo -e " Resetting all containers to ${GREEN}${RELAY_TYPE}${NC}"
for ci in $(seq 1 $CONTAINER_COUNT); do
printf -v "RELAY_TYPE_${ci}" '%s' "$RELAY_TYPE"
done
fi
fi
fi
fi
echo ""
# ── Nickname ──
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " Enter a nickname for your relay (1-19 chars, alphanumeric)"
echo -e " Press Enter for default: ${GREEN}Torware${NC}"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " nickname: " input_nick < /dev/tty || true
if [ -z "$input_nick" ]; then
NICKNAME="Torware"
elif [[ "$input_nick" =~ ^[A-Za-z0-9]{1,19}$ ]]; then
NICKNAME="$input_nick"
else
log_warn "Invalid nickname (must be 1-19 alphanumeric chars). Using default."
NICKNAME="Torware"
fi
echo ""
# ── Contact Info ──
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " Enter contact email (shown to Tor Project, not public)"
echo -e " Press Enter to skip"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " email: " input_email < /dev/tty || true
CONTACT_INFO="${input_email:-nobody@example.com}"
echo ""
# ── Bandwidth ──
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " Do you want to set ${BOLD}UNLIMITED${NC} bandwidth?"
echo -e " ${YELLOW}Note: For relays, Tor recommends at least 2 Mbps.${NC}"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " Set unlimited bandwidth? [y/N] " unlimited_bw < /dev/tty || true
if [[ "$unlimited_bw" =~ ^[Yy]$ ]]; then
BANDWIDTH="-1"
echo -e " Selected: ${GREEN}Unlimited${NC}"
else
echo ""
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " Enter bandwidth in Mbps (1-100)"
echo -e " Press Enter for default: ${GREEN}5${NC} Mbps"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " bandwidth: " input_bandwidth < /dev/tty || true
if [ -z "$input_bandwidth" ]; then
BANDWIDTH=5
elif [[ "$input_bandwidth" =~ ^[0-9]+$ ]] && [ "$input_bandwidth" -ge 1 ] && [ "$input_bandwidth" -le 100 ]; then
BANDWIDTH=$input_bandwidth
elif [[ "$input_bandwidth" =~ ^[0-9]*\.[0-9]+$ ]]; then
local float_ok=$(awk -v val="$input_bandwidth" 'BEGIN { print (val >= 1 && val <= 100) ? "yes" : "no" }')
if [ "$float_ok" = "yes" ]; then
BANDWIDTH=$input_bandwidth
else
log_warn "Invalid input. Using default: 5 Mbps"
BANDWIDTH=5
fi
else
log_warn "Invalid input. Using default: 5 Mbps"
BANDWIDTH=5
fi
fi
echo ""
# ── Data Cap ──
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " Set a monthly data cap? (in GB, 0 = unlimited)"
echo -e " Tor will automatically hibernate when cap is reached."
echo -e " Press Enter for default: ${GREEN}0${NC} (unlimited)"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " data cap (GB): " input_cap < /dev/tty || true
if [ -z "$input_cap" ] || [ "$input_cap" = "0" ]; then
DATA_CAP_GB=0
elif [[ "$input_cap" =~ ^[0-9]+$ ]] && [ "$input_cap" -ge 1 ]; then
DATA_CAP_GB=$input_cap
else
log_warn "Invalid input. Using unlimited."
DATA_CAP_GB=0
fi
echo ""
# ── Snowflake Proxy ──
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " ${BOLD}Snowflake Proxy${NC} (${GREEN}RECOMMENDED${NC})"
echo -e " Runs a lightweight WebRTC proxy alongside your relay."
echo -e " Helps censored users connect to Tor (looks like a video call)."
echo -e " Uses ~10-30MB RAM, negligible CPU. No extra config needed."
echo -e " ${DIM}Learn more: https://snowflake.torproject.org/${NC}"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " Enable Snowflake proxy? [Y/n] " input_sf < /dev/tty || true
if [[ "$input_sf" =~ ^[Nn]$ ]]; then
SNOWFLAKE_ENABLED="false"
echo -e " Snowflake: ${DIM}Disabled${NC}"
else
SNOWFLAKE_ENABLED="true"
echo -e " Snowflake: ${GREEN}Enabled${NC}"
echo ""
echo -e " ${DIM}You can run up to 2 Snowflake instances. Each registers${NC}"
echo -e " ${DIM}independently with the broker for more client assignments.${NC}"
read -p " Number of Snowflake instances (1-2) [1]: " _sf_count < /dev/tty || true
if [ "$_sf_count" = "2" ]; then
SNOWFLAKE_COUNT=2
else
SNOWFLAKE_COUNT=1
fi
local _def_cpu=$(get_snowflake_default_cpus)
local _def_mem=$(get_snowflake_default_memory)
for _si in $(seq 1 $SNOWFLAKE_COUNT); do
echo ""
if [ "$SNOWFLAKE_COUNT" -gt 1 ]; then
echo -e " ${BOLD}Instance #${_si}:${NC}"
fi
read -p " CPU cores [${_def_cpu}]: " _sf_cpu < /dev/tty || true
read -p " RAM limit [${_def_mem}]: " _sf_mem < /dev/tty || true
[ -z "$_sf_cpu" ] && _sf_cpu="$_def_cpu"
[ -z "$_sf_mem" ] && _sf_mem="$_def_mem"
[[ "$_sf_cpu" =~ ^[0-9]+\.?[0-9]*$ ]] || _sf_cpu="$_def_cpu"
[[ "$_sf_mem" =~ ^[0-9]+[mMgG]$ ]] || _sf_mem="$_def_mem"
_sf_mem=$(echo "$_sf_mem" | tr '[:upper:]' '[:lower:]')
eval "SNOWFLAKE_CPUS_${_si}=\"$_sf_cpu\""
eval "SNOWFLAKE_MEMORY_${_si}=\"$_sf_mem\""
done
echo -e " Snowflake: ${GREEN}${SNOWFLAKE_COUNT} instance(s)${NC}"
fi
# ── Unbounded (Lantern) Proxy ──
echo ""
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " ${BOLD}Unbounded Proxy (Lantern)${NC}"
echo -e " Runs a lightweight WebRTC proxy alongside your relay."
echo -e " Helps censored users connect via the Lantern network."
echo -e " Very lightweight (~10MB RAM). No port forwarding needed."
echo -e " ${DIM}Learn more: https://unbounded.lantern.io/${NC}"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " Enable Unbounded proxy? [y/N] " input_ub < /dev/tty || true
if [[ "$input_ub" =~ ^[Yy]$ ]]; then
UNBOUNDED_ENABLED="true"
local _def_ub_cpu=$(get_unbounded_default_cpus)
local _def_ub_mem=$(get_unbounded_default_memory)
read -p " CPU cores [${_def_ub_cpu}]: " _ub_cpu < /dev/tty || true
read -p " RAM limit [${_def_ub_mem}]: " _ub_mem < /dev/tty || true
[ -z "$_ub_cpu" ] && _ub_cpu="$_def_ub_cpu"
[ -z "$_ub_mem" ] && _ub_mem="$_def_ub_mem"
[[ "$_ub_cpu" =~ ^[0-9]+\.?[0-9]*$ ]] || _ub_cpu="$_def_ub_cpu"
[[ "$_ub_mem" =~ ^[0-9]+[mMgG]$ ]] || _ub_mem="$_def_ub_mem"
_ub_mem=$(echo "$_ub_mem" | tr '[:upper:]' '[:lower:]')
UNBOUNDED_CPUS="$_ub_cpu"
UNBOUNDED_MEMORY="$_ub_mem"
echo -e " Unbounded: ${GREEN}Enabled${NC} (CPU: ${_ub_cpu}, RAM: ${_ub_mem})"
else
UNBOUNDED_ENABLED="false"
echo -e " Unbounded: ${DIM}Disabled${NC}"
fi
# ── MTProxy (Telegram) Prompt ──
echo ""
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " ${BOLD}📱 MTProxy (Telegram Proxy)${NC}"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " Run a proxy that helps censored users access Telegram."
echo -e " Uses FakeTLS to disguise traffic as normal HTTPS."
echo -e " Very lightweight (~50MB RAM). Share link/QR with users."
echo -e " ${DIM}Learn more: https://core.telegram.org/proxy${NC}"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " Enable MTProxy? [y/N] " input_mtp < /dev/tty || true
if [[ "$input_mtp" =~ ^[Yy]$ ]]; then
MTPROXY_ENABLED="true"
echo ""
echo -e " ${DIM}FakeTLS domain (any popular HTTPS site — you don't need to own it):${NC}"
echo -e " ${DIM} Traffic will appear as normal HTTPS to this domain.${NC}"
echo -e " ${DIM} 1. cloudflare.com (recommended)${NC}"
echo -e " ${DIM} 2. google.com${NC}"
echo -e " ${DIM} 3. Custom (any HTTPS site, e.g. microsoft.com, amazon.com)${NC}"
read -p " Domain choice [1]: " _mtp_dom_choice < /dev/tty || true
case "${_mtp_dom_choice:-1}" in
2) MTPROXY_DOMAIN="google.com" ;;
3)
read -p " Enter domain (e.g. microsoft.com): " _mtp_custom_dom < /dev/tty || true
MTPROXY_DOMAIN="${_mtp_custom_dom:-cloudflare.com}"
;;
*) MTPROXY_DOMAIN="cloudflare.com" ;;
esac
echo ""
echo -e " ${DIM}MTProxy uses host networking. Choose an available port.${NC}"
echo -e " ${DIM}Common choices: 443, 8443, 8080, 9443${NC}"
read -p " Port [8443]: " _mtp_port < /dev/tty || true
_mtp_port="${_mtp_port:-8443}"
if [[ "$_mtp_port" =~ ^[0-9]+$ ]]; then
if ss -tln 2>/dev/null | grep -q ":${_mtp_port} " || netstat -tln 2>/dev/null | grep -q ":${_mtp_port} "; then
log_warn "Port ${_mtp_port} appears to be in use. You can change it later via settings."
fi
MTPROXY_PORT="$_mtp_port"
else
MTPROXY_PORT=8443
fi
echo ""
local _def_mtp_cpu="0.5"
local _def_mtp_mem="128m"
read -p " CPU cores [${_def_mtp_cpu}]: " _mtp_cpu < /dev/tty || true
read -p " RAM limit [${_def_mtp_mem}]: " _mtp_mem < /dev/tty || true
[ -z "$_mtp_cpu" ] && _mtp_cpu="$_def_mtp_cpu"
[ -z "$_mtp_mem" ] && _mtp_mem="$_def_mtp_mem"
[[ "$_mtp_cpu" =~ ^[0-9]+\.?[0-9]*$ ]] || _mtp_cpu="$_def_mtp_cpu"
[[ "$_mtp_mem" =~ ^[0-9]+[mMgG]$ ]] || _mtp_mem="$_def_mtp_mem"
_mtp_mem=$(echo "$_mtp_mem" | tr '[:upper:]' '[:lower:]')
MTPROXY_CPUS="$_mtp_cpu"
MTPROXY_MEMORY="$_mtp_mem"
MTPROXY_SECRET="" # Will be generated on first run
echo -e " MTProxy: ${GREEN}Enabled${NC} (Port: ${MTPROXY_PORT}, Domain: ${MTPROXY_DOMAIN})"
else
MTPROXY_ENABLED="false"
echo -e " MTProxy: ${DIM}Disabled${NC}"
fi
fi # end of relay-type != none block
echo ""
# ── Summary ──
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " ${BOLD}Your Settings:${NC}"
# Show per-container types if mixed
local has_mixed=false
if [ "${CONTAINER_COUNT:-0}" -gt 1 ]; then
for _ci in $(seq 1 $CONTAINER_COUNT); do
local _rt=$(get_container_relay_type $_ci)
[ "$_rt" != "$RELAY_TYPE" ] && has_mixed=true
done
fi
if [ "$RELAY_TYPE" = "none" ]; then
echo -e " Mode: ${CYAN}Proxy Only${NC}"
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
echo -e " Snowflake: ${GREEN}${SNOWFLAKE_COUNT} instance(s)${NC}"
for _si in $(seq 1 ${SNOWFLAKE_COUNT}); do
echo -e " Instance #${_si}: CPU $(get_snowflake_cpus $_si), RAM $(get_snowflake_memory $_si)"
done
fi
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
echo -e " Unbounded: ${GREEN}Enabled${NC} (CPU: ${UNBOUNDED_CPUS:-0.5}, RAM: ${UNBOUNDED_MEMORY:-256m})"
fi
if [ "$MTPROXY_ENABLED" = "true" ]; then
echo -e " MTProxy: ${GREEN}Enabled${NC} (Port: ${MTPROXY_PORT}, Domain: ${MTPROXY_DOMAIN})"
fi
else
if [ "$has_mixed" = "true" ]; then
echo -e " Relay Types:"
for _ci in $(seq 1 $CONTAINER_COUNT); do
echo -e " Container $_ci: ${GREEN}$(get_container_relay_type $_ci)${NC}"
done
else
echo -e " Relay Type: ${GREEN}${RELAY_TYPE}${NC}"
fi
echo -e " Nickname: ${GREEN}${NICKNAME}${NC}"
echo -e " Contact: ${GREEN}${CONTACT_INFO}${NC}"
if [ "$BANDWIDTH" = "-1" ]; then
echo -e " Bandwidth: ${GREEN}Unlimited${NC}"
else
echo -e " Bandwidth: ${GREEN}${BANDWIDTH}${NC} Mbps"
fi
echo -e " Containers: ${GREEN}${CONTAINER_COUNT}${NC}"
if [ "$DATA_CAP_GB" -gt 0 ] 2>/dev/null; then
echo -e " Data Cap: ${GREEN}${DATA_CAP_GB} GB/month${NC}"
else
echo -e " Data Cap: ${GREEN}Unlimited${NC}"
fi
echo -e " ORPorts: ${GREEN}${ORPORT_BASE}-$((ORPORT_BASE + CONTAINER_COUNT - 1))${NC}"
# Show obfs4 ports only for bridge containers
local _has_bridge=false
for _ci in $(seq 1 $CONTAINER_COUNT); do
[ "$(get_container_relay_type $_ci)" = "bridge" ] && _has_bridge=true
done
if [ "$_has_bridge" = "true" ]; then
local _bridge_ports=""
for _ci in $(seq 1 $CONTAINER_COUNT); do
if [ "$(get_container_relay_type $_ci)" = "bridge" ]; then
local _pp=$(get_container_ptport $_ci)
if [ -z "$_bridge_ports" ]; then _bridge_ports="$_pp"; else _bridge_ports="${_bridge_ports}, ${_pp}"; fi
fi
done
echo -e " obfs4 Ports: ${GREEN}${_bridge_ports}${NC}"
fi
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
echo -e " Snowflake: ${GREEN}${SNOWFLAKE_COUNT} instance(s)${NC}"
fi
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
echo -e " Unbounded: ${GREEN}Enabled${NC} (CPU: ${UNBOUNDED_CPUS:-0.5}, RAM: ${UNBOUNDED_MEMORY:-256m})"
fi
if [ "$MTPROXY_ENABLED" = "true" ]; then
echo -e " MTProxy: ${GREEN}Enabled${NC} (Port: ${MTPROXY_PORT}, Domain: ${MTPROXY_DOMAIN})"
fi
fi
# Show auto-calculated resource limits (only for relay modes)
if [ "${CONTAINER_COUNT:-0}" -gt 0 ]; then
local _sys_cores=$(get_cpu_cores)
local _sys_ram=$(get_ram_mb)
local _per_cpu=$(awk -v c="$_sys_cores" -v n="$CONTAINER_COUNT" 'BEGIN {v=(c>1)?(c-1)/n:0.5; if(v<0.5)v=0.5; printf "%.1f",v}')
local _per_ram=$(awk -v r="$_sys_ram" -v n="$CONTAINER_COUNT" 'BEGIN {v=(r-512)/n; if(v<256)v=256; if(v>2048)v=2048; printf "%.0f",v}')
# Validate values (some awk implementations return "inf" on edge cases)
[[ "$_per_cpu" =~ ^[0-9]+\.?[0-9]*$ ]] || _per_cpu="0.5"
[[ "$_per_ram" =~ ^[0-9]+$ ]] || _per_ram="256"
echo -e " Resources: ${GREEN}${_per_cpu} CPU / ${_per_ram}MB RAM${NC} per relay container"
fi
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo ""
read -p " Proceed with these settings? [Y/n] " confirm < /dev/tty || true
if [[ "$confirm" =~ ^[Nn]$ ]]; then
continue
fi
break
done
}
#═══════════════════════════════════════════════════════════════════════
# Settings Management
#═══════════════════════════════════════════════════════════════════════
save_settings() {
mkdir -p "$INSTALL_DIR"
local _tmp
_tmp=$(mktemp "$INSTALL_DIR/settings.conf.XXXXXX") || { log_error "Failed to create temp file"; return 1; }
_TMP_FILES+=("$_tmp")
# Capture current globals BEFORE load_settings overwrites them
# Relay settings
local _caller_relay_type="${RELAY_TYPE:-bridge}"
local _caller_nickname="${NICKNAME:-}"
local _caller_contact_info="${CONTACT_INFO:-}"
local _caller_bandwidth="${BANDWIDTH:-5}"
local _caller_container_count="${CONTAINER_COUNT:-1}"
local _caller_orport_base="${ORPORT_BASE:-9001}"
local _caller_controlport_base="${CONTROLPORT_BASE:-9051}"
local _caller_pt_port_base="${PT_PORT_BASE:-9100}"
local _caller_exit_policy="${EXIT_POLICY:-reduced}"
local _caller_data_cap_gb="${DATA_CAP_GB:-0}"
# Per-container relay types
local _caller_relay_type_1="${RELAY_TYPE_1:-}"
local _caller_relay_type_2="${RELAY_TYPE_2:-}"
local _caller_relay_type_3="${RELAY_TYPE_3:-}"
local _caller_relay_type_4="${RELAY_TYPE_4:-}"
local _caller_relay_type_5="${RELAY_TYPE_5:-}"
local _caller_snowflake_count="${SNOWFLAKE_COUNT:-1}"
local _caller_snowflake_cpus_1="${SNOWFLAKE_CPUS_1:-}"
local _caller_snowflake_memory_1="${SNOWFLAKE_MEMORY_1:-}"
local _caller_snowflake_cpus_2="${SNOWFLAKE_CPUS_2:-}"
local _caller_snowflake_memory_2="${SNOWFLAKE_MEMORY_2:-}"
local _caller_snowflake_enabled="${SNOWFLAKE_ENABLED:-false}"
local _caller_snowflake_cpus="${SNOWFLAKE_CPUS:-1.0}"
local _caller_snowflake_memory="${SNOWFLAKE_MEMORY:-256m}"
# Capture Unbounded globals
local _caller_unbounded_enabled="${UNBOUNDED_ENABLED:-false}"
local _caller_unbounded_cpus="${UNBOUNDED_CPUS:-0.5}"
local _caller_unbounded_memory="${UNBOUNDED_MEMORY:-256m}"
local _caller_unbounded_freddie="${UNBOUNDED_FREDDIE:-https://freddie.iantem.io}"
local _caller_unbounded_egress="${UNBOUNDED_EGRESS:-wss://unbounded.iantem.io}"
local _caller_unbounded_tag="${UNBOUNDED_TAG:-}"
# Capture MTProxy globals
local _caller_mtproxy_enabled="${MTPROXY_ENABLED:-false}"
local _caller_mtproxy_port="${MTPROXY_PORT:-8443}"
local _caller_mtproxy_metrics_port="${MTPROXY_METRICS_PORT:-3129}"
local _caller_mtproxy_domain="${MTPROXY_DOMAIN:-cloudflare.com}"
local _caller_mtproxy_secret="${MTPROXY_SECRET:-}"
local _caller_mtproxy_cpus="${MTPROXY_CPUS:-0.5}"
local _caller_mtproxy_memory="${MTPROXY_MEMORY:-128m}"
local _caller_mtproxy_concurrency="${MTPROXY_CONCURRENCY:-8192}"
local _caller_mtproxy_blocklist_countries="${MTPROXY_BLOCKLIST_COUNTRIES:-}"
# Preserve existing Telegram settings on reinstall
local _caller_tg_token="${TELEGRAM_BOT_TOKEN:-}"
local _caller_tg_chat="${TELEGRAM_CHAT_ID:-}"
local _caller_tg_interval="${TELEGRAM_INTERVAL:-6}"
local _caller_tg_enabled="${TELEGRAM_ENABLED:-false}"
local _caller_tg_alerts="${TELEGRAM_ALERTS_ENABLED:-true}"
local _caller_tg_daily="${TELEGRAM_DAILY_SUMMARY:-true}"
local _caller_tg_weekly="${TELEGRAM_WEEKLY_SUMMARY:-true}"
local _caller_tg_label="${TELEGRAM_SERVER_LABEL:-}"
local _caller_tg_start_hour="${TELEGRAM_START_HOUR:-0}"
local _tg_token="" _tg_chat="" _tg_interval="6" _tg_enabled="false"
local _tg_alerts="true" _tg_daily="true" _tg_weekly="true" _tg_label="" _tg_start_hour="0"
if [ -f "$INSTALL_DIR/settings.conf" ]; then
load_settings
_tg_token="${TELEGRAM_BOT_TOKEN:-}"
_tg_chat="${TELEGRAM_CHAT_ID:-}"
_tg_interval="${TELEGRAM_INTERVAL:-6}"
_tg_enabled="${TELEGRAM_ENABLED:-false}"
_tg_alerts="${TELEGRAM_ALERTS_ENABLED:-true}"
_tg_daily="${TELEGRAM_DAILY_SUMMARY:-true}"
_tg_weekly="${TELEGRAM_WEEKLY_SUMMARY:-true}"
_tg_label="${TELEGRAM_SERVER_LABEL:-}"
_tg_start_hour="${TELEGRAM_START_HOUR:-0}"
fi
# Always use caller's current globals — they reflect the user's latest action
_tg_token="$_caller_tg_token"
_tg_chat="$_caller_tg_chat"
_tg_enabled="$_caller_tg_enabled"
_tg_interval="$_caller_tg_interval"
_tg_alerts="$_caller_tg_alerts"
_tg_daily="$_caller_tg_daily"
_tg_weekly="$_caller_tg_weekly"
_tg_label="$_caller_tg_label"
_tg_start_hour="$_caller_tg_start_hour"
# Restore relay globals after load_settings clobbered them
RELAY_TYPE="$_caller_relay_type"
NICKNAME="$_caller_nickname"
CONTACT_INFO="$_caller_contact_info"
BANDWIDTH="$_caller_bandwidth"
CONTAINER_COUNT="$_caller_container_count"
ORPORT_BASE="$_caller_orport_base"
CONTROLPORT_BASE="$_caller_controlport_base"
PT_PORT_BASE="$_caller_pt_port_base"
EXIT_POLICY="$_caller_exit_policy"
DATA_CAP_GB="$_caller_data_cap_gb"
RELAY_TYPE_1="$_caller_relay_type_1"
RELAY_TYPE_2="$_caller_relay_type_2"
RELAY_TYPE_3="$_caller_relay_type_3"
RELAY_TYPE_4="$_caller_relay_type_4"
RELAY_TYPE_5="$_caller_relay_type_5"
# Restore snowflake globals after load_settings clobbered them
SNOWFLAKE_COUNT="$_caller_snowflake_count"
SNOWFLAKE_ENABLED="$_caller_snowflake_enabled"
SNOWFLAKE_CPUS="$_caller_snowflake_cpus"
SNOWFLAKE_MEMORY="$_caller_snowflake_memory"
SNOWFLAKE_CPUS_1="$_caller_snowflake_cpus_1"
SNOWFLAKE_MEMORY_1="$_caller_snowflake_memory_1"
SNOWFLAKE_CPUS_2="$_caller_snowflake_cpus_2"
SNOWFLAKE_MEMORY_2="$_caller_snowflake_memory_2"
# Restore unbounded globals after load_settings clobbered them
UNBOUNDED_ENABLED="$_caller_unbounded_enabled"
UNBOUNDED_CPUS="$_caller_unbounded_cpus"
UNBOUNDED_MEMORY="$_caller_unbounded_memory"
UNBOUNDED_FREDDIE="$_caller_unbounded_freddie"
UNBOUNDED_EGRESS="$_caller_unbounded_egress"
UNBOUNDED_TAG="$_caller_unbounded_tag"
# Restore MTProxy globals after load_settings clobbered them
MTPROXY_ENABLED="$_caller_mtproxy_enabled"
MTPROXY_PORT="$_caller_mtproxy_port"
MTPROXY_METRICS_PORT="$_caller_mtproxy_metrics_port"
MTPROXY_DOMAIN="$_caller_mtproxy_domain"
MTPROXY_SECRET="$_caller_mtproxy_secret"
MTPROXY_CPUS="$_caller_mtproxy_cpus"
MTPROXY_MEMORY="$_caller_mtproxy_memory"
MTPROXY_CONCURRENCY="$_caller_mtproxy_concurrency"
MTPROXY_BLOCKLIST_COUNTRIES="$_caller_mtproxy_blocklist_countries"
# Restore ALL telegram globals after load_settings clobbered them
TELEGRAM_BOT_TOKEN="$_tg_token"
TELEGRAM_CHAT_ID="$_tg_chat"
TELEGRAM_ENABLED="$_tg_enabled"
TELEGRAM_INTERVAL="$_tg_interval"
TELEGRAM_ALERTS_ENABLED="$_tg_alerts"
TELEGRAM_DAILY_SUMMARY="$_tg_daily"
TELEGRAM_WEEKLY_SUMMARY="$_tg_weekly"
TELEGRAM_SERVER_LABEL="$_tg_label"
TELEGRAM_START_HOUR="$_tg_start_hour"
# Sanitize values — strip characters that could break shell sourcing
local _safe_contact
_safe_contact=$(printf '%s' "$CONTACT_INFO" | tr -d '\047\042\140\044\134')
local _safe_nickname
_safe_nickname=$(printf '%s' "$NICKNAME" | tr -cd 'A-Za-z0-9')
local _safe_tg_label
_safe_tg_label=$(printf '%s' "$_tg_label" | tr -d '\047\042\140\044\134')
local _safe_tg_token
_safe_tg_token=$(printf '%s' "$_tg_token" | tr -cd 'A-Za-z0-9:_-')
local _safe_tg_chat
_safe_tg_chat=$(printf '%s' "$_tg_chat" | tr -cd 'A-Za-z0-9_-')
cat > "$_tmp" << EOF
# Torware Settings - v${VERSION}
# Generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC')
# Relay Configuration
RELAY_TYPE='${RELAY_TYPE}'
NICKNAME='${_safe_nickname}'
CONTACT_INFO='${_safe_contact}'
BANDWIDTH='${BANDWIDTH}'
CONTAINER_COUNT='${CONTAINER_COUNT:-1}'
ORPORT_BASE='${ORPORT_BASE}'
CONTROLPORT_BASE='${CONTROLPORT_BASE}'
PT_PORT_BASE='${PT_PORT_BASE}'
EXIT_POLICY='${EXIT_POLICY:-reduced}'
DATA_CAP_GB='${DATA_CAP_GB:-0}'
# Snowflake Proxy
SNOWFLAKE_ENABLED='${SNOWFLAKE_ENABLED:-false}'
SNOWFLAKE_COUNT='${SNOWFLAKE_COUNT:-1}'
SNOWFLAKE_CPUS='${SNOWFLAKE_CPUS:-1.0}'
SNOWFLAKE_MEMORY='${SNOWFLAKE_MEMORY:-256m}'
SNOWFLAKE_CPUS_1='${SNOWFLAKE_CPUS_1}'
SNOWFLAKE_MEMORY_1='${SNOWFLAKE_MEMORY_1}'
SNOWFLAKE_CPUS_2='${SNOWFLAKE_CPUS_2}'
SNOWFLAKE_MEMORY_2='${SNOWFLAKE_MEMORY_2}'
# Unbounded (Lantern) Proxy
UNBOUNDED_ENABLED='${UNBOUNDED_ENABLED:-false}'
UNBOUNDED_CPUS='${UNBOUNDED_CPUS:-0.5}'
UNBOUNDED_MEMORY='${UNBOUNDED_MEMORY:-256m}'
UNBOUNDED_FREDDIE='${UNBOUNDED_FREDDIE:-https://freddie.iantem.io}'
UNBOUNDED_EGRESS='${UNBOUNDED_EGRESS:-wss://unbounded.iantem.io}'
UNBOUNDED_TAG='${UNBOUNDED_TAG}'
# MTProxy (Telegram Proxy)
MTPROXY_ENABLED='${MTPROXY_ENABLED:-false}'
MTPROXY_PORT='${MTPROXY_PORT:-8443}'
MTPROXY_METRICS_PORT='${MTPROXY_METRICS_PORT:-3129}'
MTPROXY_DOMAIN='${MTPROXY_DOMAIN:-cloudflare.com}'
MTPROXY_SECRET='${MTPROXY_SECRET}'
MTPROXY_CPUS='${MTPROXY_CPUS:-0.5}'
MTPROXY_MEMORY='${MTPROXY_MEMORY:-128m}'
MTPROXY_CONCURRENCY='${MTPROXY_CONCURRENCY:-8192}'
MTPROXY_BLOCKLIST_COUNTRIES='${MTPROXY_BLOCKLIST_COUNTRIES}'
# Telegram Integration
TELEGRAM_BOT_TOKEN='${_safe_tg_token}'
TELEGRAM_CHAT_ID='${_safe_tg_chat}'
TELEGRAM_INTERVAL='${_tg_interval}'
TELEGRAM_ENABLED='${_tg_enabled}'
TELEGRAM_ALERTS_ENABLED='${_tg_alerts}'
TELEGRAM_DAILY_SUMMARY='${_tg_daily}'
TELEGRAM_WEEKLY_SUMMARY='${_tg_weekly}'
TELEGRAM_SERVER_LABEL='${_safe_tg_label}'
TELEGRAM_START_HOUR='${_tg_start_hour}'
# Docker Resource Limits (per-container overrides)
DOCKER_CPUS=''
DOCKER_MEMORY=''
EOF
# Add per-container overrides (sanitize to strip quotes/shell metacharacters)
for i in $(seq 1 ${CONTAINER_COUNT:-1}); do
local rt_var="RELAY_TYPE_${i}"
local bw_var="BANDWIDTH_${i}"
local cpus_var="CPUS_${i}"
local mem_var="MEMORY_${i}"
local orport_var="ORPORT_${i}"
local ptport_var="PT_PORT_${i}"
# Relay type: only allow known values
if [ -n "${!rt_var}" ]; then
local _srt="${!rt_var}"
case "$_srt" in bridge|middle|exit) ;; *) _srt="bridge" ;; esac
echo "${rt_var}='${_srt}'" >> "$_tmp"
fi
# Numeric values: strip non-numeric/dot chars
if [ -n "${!bw_var}" ]; then echo "${bw_var}='$(printf '%s' "${!bw_var}" | tr -cd '0-9.\-')'" >> "$_tmp"; fi
if [ -n "${!cpus_var}" ]; then echo "${cpus_var}='$(printf '%s' "${!cpus_var}" | tr -cd '0-9.')'" >> "$_tmp"; fi
if [ -n "${!mem_var}" ]; then echo "${mem_var}='$(printf '%s' "${!mem_var}" | tr -cd '0-9a-zA-Z')'" >> "$_tmp"; fi
if [ -n "${!orport_var}" ]; then echo "${orport_var}='$(printf '%s' "${!orport_var}" | tr -cd '0-9')'" >> "$_tmp"; fi
if [ -n "${!ptport_var}" ]; then echo "${ptport_var}='$(printf '%s' "${!ptport_var}" | tr -cd '0-9')'" >> "$_tmp"; fi
done
if ! chmod 600 "$_tmp" 2>/dev/null; then
log_warn "Could not set permissions on settings file — check filesystem"
fi
if ! mv "$_tmp" "$INSTALL_DIR/settings.conf"; then
log_error "Failed to save settings (mv failed). Check disk space and permissions."
rm -f "$_tmp" 2>/dev/null
return 1
fi
if [ ! -f "$INSTALL_DIR/settings.conf" ]; then
log_error "Failed to save settings. File missing after write."
return 1
fi
log_success "Settings saved"
}
load_settings() {
if [ -f "$INSTALL_DIR/settings.conf" ]; then
# Migration: Update old Freddie URL to new one (Lantern moved from Heroku)
if grep -q 'bf-freddie\.herokuapp\.com' "$INSTALL_DIR/settings.conf" 2>/dev/null; then
sed -i 's|bf-freddie\.herokuapp\.com|freddie.iantem.io|g' "$INSTALL_DIR/settings.conf" 2>/dev/null
fi
# Copy to temp file to avoid TOCTOU race between validation and parse
local _tmp_settings
_tmp_settings=$(mktemp "${TMPDIR:-/tmp}/.tor_settings.XXXXXX") || return 1
cp "$INSTALL_DIR/settings.conf" "$_tmp_settings" || { rm -f "$_tmp_settings"; return 1; }
chmod 600 "$_tmp_settings" 2>/dev/null
# Whitelist validation on the copy
if grep -vE '^\s*$|^\s*#|^[A-Za-z_][A-Za-z0-9_]*='\''[^'\'']*'\''$|^[A-Za-z_][A-Za-z0-9_]*=[0-9]+$|^[A-Za-z_][A-Za-z0-9_]*=(true|false)$' "$_tmp_settings" 2>/dev/null | grep -q .; then
log_error "settings.conf contains unsafe content. Refusing to load."
rm -f "$_tmp_settings"
return 1
fi
# Parse key=value explicitly instead of sourcing (safer)
local key val line
while IFS= read -r line || [[ -n "$line" ]]; do
# Skip empty lines and comments
[[ "$line" =~ ^[[:space:]]*$ ]] && continue
[[ "$line" =~ ^[[:space:]]*# ]] && continue
# Match single-quoted string: VAR='value'
if [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=\'([^\']*)\' ]]; then
key="${BASH_REMATCH[1]}"
val="${BASH_REMATCH[2]}"
# Match numeric: VAR=123
elif [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=([0-9]+)$ ]]; then
key="${BASH_REMATCH[1]}"
val="${BASH_REMATCH[2]}"
# Match boolean: VAR=true or VAR=false
elif [[ "$line" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(true|false)$ ]]; then
key="${BASH_REMATCH[1]}"
val="${BASH_REMATCH[2]}"
else
continue
fi
# Use declare -g to set variable in global scope safely
declare -g "$key=$val"
done < "$_tmp_settings"
rm -f "$_tmp_settings"
# Sync loaded values to CONFIG array
config_sync_from_globals
fi
}
#═══════════════════════════════════════════════════════════════════════
# Docker Container Lifecycle
#═══════════════════════════════════════════════════════════════════════
get_docker_image() {
local idx=${1:-1}
local rtype=$(get_container_relay_type $idx)
case "$rtype" in
bridge) echo "$BRIDGE_IMAGE" ;;
*) echo "$RELAY_IMAGE" ;;
esac
}
run_relay_container() {
local idx=$1
local cname=$(get_container_name $idx)
local vname=$(get_volume_name $idx)
local orport=$(get_container_orport $idx)
local controlport=$(get_container_controlport $idx)
local ptport=$(get_container_ptport $idx)
local image=$(get_docker_image $idx)
# Validate ports
if ! validate_port "$orport" || ! validate_port "$controlport" || ! validate_port "$ptport"; then
log_error "Invalid port configuration for container $idx (OR:$orport CP:$controlport PT:$ptport)"
return 1
fi
# Generate torrc for this container (only needed for middle/exit — bridges use env vars)
local rtype=$(get_container_relay_type $idx)
if [ "$rtype" != "bridge" ]; then
generate_torrc $idx
fi
# Remove existing container
docker rm -f "$cname" 2>/dev/null || true
# Ensure volume exists
docker volume create "$vname" 2>/dev/null || true
# Resource limits (use arrays for safe word splitting)
local resource_args=()
local cpus=$(get_container_cpus $idx)
local mem=$(get_container_memory $idx)
# Auto-calculate defaults if not explicitly set
if [ -z "$cpus" ] && [ -z "$DOCKER_CPUS" ]; then
local _sys_cores=$(get_cpu_cores)
local _count=${CONTAINER_COUNT:-1}
# Reserve 1 core for the host, split the rest among containers (min 0.5 per container)
cpus=$(awk -v c="$_sys_cores" -v n="$_count" 'BEGIN {v=(c>1)?(c-1)/n:0.5; if(v<0.5)v=0.5; printf "%.1f",v}')
# Validate cpus is a valid number (some awk implementations return "inf" on edge cases)
if ! [[ "$cpus" =~ ^[0-9]+\.?[0-9]*$ ]]; then
cpus="0.5"
fi
elif [ -z "$cpus" ] && [ -n "$DOCKER_CPUS" ]; then
cpus="$DOCKER_CPUS"
fi
if [ -z "$mem" ] && [ -z "$DOCKER_MEMORY" ]; then
local _sys_ram=$(get_ram_mb)
local _count=${CONTAINER_COUNT:-1}
# Reserve 512MB for host, split rest among containers (min 256MB, max 2GB per container)
local _per_mb=$(awk -v r="$_sys_ram" -v n="$_count" 'BEGIN {v=(r-512)/n; if(v<256)v=256; if(v>2048)v=2048; printf "%.0f",v}')
# Validate memory is a valid number
if [[ "$_per_mb" =~ ^[0-9]+$ ]]; then
mem="${_per_mb}m"
else
mem="256m"
fi
elif [ -z "$mem" ] && [ -n "$DOCKER_MEMORY" ]; then
mem="$DOCKER_MEMORY"
fi
[ -n "$cpus" ] && resource_args+=(--cpus "$cpus")
[ -n "$mem" ] && resource_args+=(--memory "$mem")
local torrc_path="$CONTAINERS_DIR/relay-${idx}/torrc"
# Compute bandwidth in bytes/sec for env vars
local bw=$(get_container_bandwidth $idx)
local bw_bytes=""
if [ "$bw" != "-1" ] && [ -n "$bw" ]; then
bw_bytes=$(awk -v b="$bw" 'BEGIN {printf "%.0f", b * 125000}')
fi
if [ "$rtype" = "bridge" ]; then
# For the official bridge image, use environment variables
# Enable ControlPort via OBFS4V_ env vars for monitoring
local bw_env=()
if [ -n "$bw_bytes" ]; then
bw_env=(-e "OBFS4V_BandwidthRate=${bw_bytes}" -e "OBFS4V_BandwidthBurst=$((bw_bytes * 2))")
fi
# NOTE: --network host is required for Tor relays: ORPort must bind directly on the
# host interface, and ControlPort cookie auth requires shared localhost access.
if ! docker run -d \
--name "$cname" \
--restart unless-stopped \
--log-opt max-size=15m \
--log-opt max-file=3 \
-v "${vname}:/var/lib/tor" \
--network host \
-e "OR_PORT=${orport}" \
-e "PT_PORT=${ptport}" \
-e "EMAIL=${CONTACT_INFO}" \
-e "NICKNAME=$(get_container_nickname $idx)" \
-e "OBFS4_ENABLE_ADDITIONAL_VARIABLES=1" \
-e "OBFS4V_ControlPort=127.0.0.1:${controlport}" \
-e "OBFS4V_CookieAuthentication=1" \
--health-cmd "nc -z 127.0.0.1 ${controlport} || exit 1" \
--health-interval=60s \
--health-timeout=10s \
--health-retries=3 \
--health-start-period=120s \
"${bw_env[@]}" \
"${resource_args[@]}" \
"$image"; then
log_error "Failed to start $cname (bridge)"
return 1
fi
else
# For middle/exit relays, mount our custom torrc
# NOTE: --network host required for Tor ORPort binding and ControlPort cookie auth
if ! docker run -d \
--name "$cname" \
--restart unless-stopped \
--log-opt max-size=15m \
--log-opt max-file=3 \
-v "${vname}:/var/lib/tor" \
-v "${torrc_path}:/etc/tor/torrc:ro" \
--network host \
--health-cmd "nc -z 127.0.0.1 ${controlport} || exit 1" \
--health-interval=60s \
--health-timeout=10s \
--health-retries=3 \
--health-start-period=120s \
"${resource_args[@]}" \
"$image"; then
log_error "Failed to start $cname (relay)"
return 1
fi
fi
log_success "$cname started (ORPort: $orport, ControlPort: $controlport)"
}
run_all_containers() {
local count=${CONTAINER_COUNT:-1}
# Proxy-only mode: skip relay containers entirely
if [ "$count" -le 0 ] 2>/dev/null || [ "$RELAY_TYPE" = "none" ]; then
log_info "Starting Torware (proxy-only mode)..."
run_all_snowflake_containers
run_unbounded_container
run_mtproxy_container
local _any_proxy=false
is_snowflake_running && _any_proxy=true
is_unbounded_running && _any_proxy=true
is_mtproxy_running && _any_proxy=true
if [ "$_any_proxy" = "true" ]; then
log_success "Proxy containers started"
else
log_error "No proxy containers started"
exit 1
fi
return 0
fi
log_info "Starting Torware ($count container(s))..."
# Pull required images (may need both bridge and relay images for mixed types)
local needs_bridge=false needs_relay=false
for i in $(seq 1 $count); do
local rt=$(get_container_relay_type $i)
[ "$rt" = "bridge" ] && needs_bridge=true || needs_relay=true
done
if [ "$needs_bridge" = "true" ]; then
log_info "Pulling bridge image ($BRIDGE_IMAGE)..."
if ! docker pull "$BRIDGE_IMAGE"; then
log_error "Failed to pull bridge image. Check your internet connection."
exit 1
fi
fi
if [ "$needs_relay" = "true" ]; then
log_info "Pulling relay image ($RELAY_IMAGE)..."
if ! docker pull "$RELAY_IMAGE"; then
log_error "Failed to pull relay image. Check your internet connection."
exit 1
fi
fi
# Pull Alpine image used for cookie auth reading
log_info "Pulling utility image (alpine:latest)..."
docker pull alpine:latest 2>/dev/null || log_warn "Could not pre-pull alpine image (cookie auth may be slower on first use)"
for i in $(seq 1 $count); do
run_relay_container $i
done
# Start Snowflake proxy if enabled
run_all_snowflake_containers
# Start Unbounded proxy if enabled
run_unbounded_container
# Wait for relay container to be running and bootstrapping
local _retries=0
local _max_retries=12
local _relay_started=false
local _cname=$(get_container_name 1)
while [ $_retries -lt $_max_retries ]; do
sleep 5
# Check if container is running or restarting
if docker ps -a --format '{{.Names}} {{.Status}}' | grep -q "^${_cname} "; then
local _status=$(docker ps -a --format '{{.Status}}' --filter "name=^${_cname}$" | head -1)
# Check logs for bootstrap progress
local _logs=$(docker logs "$_cname" 2>&1 | tail -20)
if echo "$_logs" | grep -q "Bootstrapped 100%"; then
_relay_started=true
break
elif echo "$_logs" | grep -q "Bootstrapped"; then
local _pct=$(echo "$_logs" | sed -n 's/.*Bootstrapped \([0-9]*\).*/\1/p' | tail -1)
log_info "Relay bootstrapping... ${_pct}%"
fi
# If container is running (not crashed), keep waiting
if docker ps --format '{{.Names}}' | grep -q "^${_cname}$"; then
: # still running, keep waiting
elif echo "$_status" | grep -qi "exited"; then
log_error "Tor relay container exited unexpectedly"
docker logs "$_cname" 2>&1 | tail -10
exit 1
fi
fi
_retries=$((_retries + 1))
done
if [ "$_relay_started" = "true" ] || docker ps --format '{{.Names}}' | grep -q "^${_cname}$"; then
local bw_display="$BANDWIDTH Mbps"
[ "$BANDWIDTH" = "-1" ] && bw_display="Unlimited"
log_success "Settings: default_type=$RELAY_TYPE, bandwidth=$bw_display, containers=$count"
if [ "$SNOWFLAKE_ENABLED" = "true" ] && is_snowflake_running; then
log_success "Snowflake proxy: running (WebRTC transport)"
fi
else
log_error "Tor relay failed to start"
docker logs "$_cname" 2>&1 | tail -10
exit 1
fi
}
start_relay() {
local count=${CONTAINER_COUNT:-1}
for i in $(seq 1 $count); do
local cname=$(get_container_name $i)
if docker ps -a --format '{{.Names}}' | grep -q "^${cname}$"; then
if docker start "$cname" 2>/dev/null; then
log_success "$cname started"
else
log_error "Failed to start $cname, recreating..."
run_relay_container $i
fi
else
run_relay_container $i
fi
done
}
stop_relay() {
log_info "Stopping Tor relay containers..."
for i in $(seq 1 ${CONTAINER_COUNT:-1}); do
local cname=$(get_container_name $i)
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
docker stop --timeout 30 "$cname" 2>/dev/null || true
log_success "$cname stopped"
fi
done
}
restart_relay() {
log_info "Restarting Tor relay containers..."
local count=${CONTAINER_COUNT:-1}
for i in $(seq 1 $count); do
local cname=$(get_container_name $i)
# Regenerate torrc in case settings changed (only for non-bridge; bridges use env vars)
local _rtype=$(get_container_relay_type $i)
if [ "$_rtype" != "bridge" ]; then
generate_torrc $i
fi
# Check if container exists
if docker ps -a --format '{{.Names}}' | grep -q "^${cname}$"; then
docker rm -f "$cname" 2>/dev/null || true
fi
run_relay_container $i
done
}
#═══════════════════════════════════════════════════════════════════════
# Snowflake Proxy Container Lifecycle
#═══════════════════════════════════════════════════════════════════════
run_snowflake_container() {
local idx=${1:-1}
if [ "$SNOWFLAKE_ENABLED" != "true" ]; then
return 0
fi
local cname=$(get_snowflake_name $idx)
local vname=$(get_snowflake_volume $idx)
local mport=$(get_snowflake_metrics_port $idx)
local sf_cpus=$(get_snowflake_cpus $idx)
local sf_memory=$(get_snowflake_memory $idx)
log_info "Starting Snowflake proxy ($cname)..."
# Pull image if not already cached locally
if ! docker image inspect "$SNOWFLAKE_IMAGE" &>/dev/null; then
if ! docker pull "$SNOWFLAKE_IMAGE"; then
log_error "Failed to pull Snowflake image."
return 1
fi
fi
# Remove existing
docker rm -f "$cname" 2>/dev/null || true
# Ensure volume exists for data persistence
docker volume create "$vname" 2>/dev/null || true
if ! docker run -d \
--name "$cname" \
--restart unless-stopped \
--log-opt max-size=10m \
--log-opt max-file=3 \
--cpus "$(awk -v req="${sf_cpus}" -v cores="$(nproc 2>/dev/null || echo 1)" 'BEGIN{c=req+0; if(c>cores+0) c=cores+0; printf "%.2f",c}')" \
--memory "${sf_memory}" \
--memory-swap "${sf_memory}" \
--network host \
--health-cmd "wget -q -O /dev/null http://127.0.0.1:${mport}/ || exit 1" \
--health-interval=300s \
--health-timeout=10s \
--health-retries=5 \
--health-start-period=3600s \
-v "${vname}:/var/lib/snowflake" \
"$SNOWFLAKE_IMAGE" \
-metrics -metrics-address "127.0.0.1" -metrics-port "${mport}"; then
log_error "Failed to start Snowflake proxy ($cname)"
return 1
fi
log_success "Snowflake proxy started: $cname (metrics on port $mport)"
}
run_all_snowflake_containers() {
if [ "$SNOWFLAKE_ENABLED" != "true" ]; then
return 0
fi
# Pull image once
if ! docker pull "$SNOWFLAKE_IMAGE"; then
log_error "Failed to pull Snowflake image."
return 1
fi
for i in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
run_snowflake_container $i
done
}
stop_snowflake_container() {
for i in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
local cname=$(get_snowflake_name $i)
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
docker stop --timeout 10 "$cname" 2>/dev/null || true
log_success "$cname stopped"
fi
done
}
start_snowflake_container() {
if [ "$SNOWFLAKE_ENABLED" != "true" ]; then
return 0
fi
for i in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
local cname=$(get_snowflake_name $i)
if docker ps -a --format '{{.Names}}' | grep -q "^${cname}$"; then
if docker start "$cname" 2>/dev/null; then
log_success "$cname started"
else
log_warn "Failed to start $cname, recreating..."
run_snowflake_container $i
fi
else
run_snowflake_container $i
fi
done
}
restart_snowflake_container() {
if [ "$SNOWFLAKE_ENABLED" != "true" ]; then
return 0
fi
for i in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
local cname=$(get_snowflake_name $i)
docker rm -f "$cname" 2>/dev/null || true
run_snowflake_container $i
done
}
is_snowflake_running() {
# Returns true if at least one snowflake instance is running
for i in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
local cname=$(get_snowflake_name $i)
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
return 0
fi
done
return 1
}
get_snowflake_stats() {
# Aggregate stats across all snowflake instances
local total_connections=0 total_inbound=0 total_outbound=0
for i in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
local cname=$(get_snowflake_name $i)
local mport=$(get_snowflake_metrics_port $i)
# Get connections from Prometheus metrics
local metrics=""
metrics=$(curl -s --max-time 3 "http://127.0.0.1:${mport}/internal/metrics" 2>/dev/null)
if [ -n "$metrics" ]; then
local conns=$(echo "$metrics" | awk '
/^tor_snowflake_proxy_connections_total[{ ]/ { sum += $NF }
END { printf "%.0f", sum }
' 2>/dev/null)
total_connections=$((total_connections + ${conns:-0}))
fi
# Get cumulative traffic from docker logs
local log_data
log_data=$(docker logs "$cname" 2>&1 | grep "Traffic Relayed" 2>/dev/null)
if [ -n "$log_data" ]; then
local ib=$(echo "$log_data" | awk -F'[↓↑]' '{
split($2, a, " "); gsub(/[^0-9.]/, "", a[1]); sum += a[1]
} END { printf "%.0f", sum * 1024 }' 2>/dev/null)
local ob=$(echo "$log_data" | awk -F'[↓↑]' '{
split($3, a, " "); gsub(/[^0-9.]/, "", a[1]); sum += a[1]
} END { printf "%.0f", sum * 1024 }' 2>/dev/null)
total_inbound=$((total_inbound + ${ib:-0}))
total_outbound=$((total_outbound + ${ob:-0}))
fi
done
echo "${total_connections} ${total_inbound} ${total_outbound}"
}
get_snowflake_instance_stats() {
# Get stats for a single instance
local idx=${1:-1}
local cname=$(get_snowflake_name $idx)
local mport=$(get_snowflake_metrics_port $idx)
local connections=0 inbound=0 outbound=0
local metrics=""
metrics=$(curl -s --max-time 3 "http://127.0.0.1:${mport}/internal/metrics" 2>/dev/null)
if [ -n "$metrics" ]; then
connections=$(echo "$metrics" | awk '
/^tor_snowflake_proxy_connections_total[{ ]/ { sum += $NF }
END { printf "%.0f", sum }
' 2>/dev/null)
fi
local log_data
log_data=$(docker logs "$cname" 2>&1 | grep "Traffic Relayed" 2>/dev/null)
if [ -n "$log_data" ]; then
inbound=$(echo "$log_data" | awk -F'[↓↑]' '{
split($2, a, " "); gsub(/[^0-9.]/, "", a[1]); sum += a[1]
} END { printf "%.0f", sum * 1024 }' 2>/dev/null)
outbound=$(echo "$log_data" | awk -F'[↓↑]' '{
split($3, a, " "); gsub(/[^0-9.]/, "", a[1]); sum += a[1]
} END { printf "%.0f", sum * 1024 }' 2>/dev/null)
fi
echo "${connections:-0} ${inbound:-0} ${outbound:-0}"
}
get_snowflake_country_stats() {
# Aggregate country stats across all snowflake instances
local all_metrics=""
for i in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
local mport=$(get_snowflake_metrics_port $i)
local metrics=""
metrics=$(curl -s --max-time 3 "http://127.0.0.1:${mport}/internal/metrics" 2>/dev/null) || continue
all_metrics+="$metrics"$'\n'
done
[ -z "$all_metrics" ] && return 1
echo "$all_metrics" | awk '
/^tor_snowflake_proxy_connections_total\{/ {
val = $NF + 0
country = ""
s = $0
idx = index(s, "country=\"")
if (idx > 0) {
s = substr(s, idx + 9)
end = index(s, "\"")
if (end > 0) country = substr(s, 1, end - 1)
}
if (country != "" && val > 0) counts[country] += val
}
END {
for (c in counts) print counts[c] "|" c
}
' 2>/dev/null | sort -t'|' -k1 -nr
}
#═══════════════════════════════════════════════════════════════════════
# Unbounded (Lantern) Proxy Container Lifecycle
#═══════════════════════════════════════════════════════════════════════
build_unbounded_image() {
if docker image inspect "$UNBOUNDED_IMAGE" &>/dev/null; then
return 0
fi
log_info "Building Unbounded widget image (this may take a few minutes on first run)..."
# Pin to commit b53a6690f363 (May 2025) — matches production freddie server (v0.0.2 protocol)
# The main branch has v2.x which is rejected by production servers still running v0.x
log_info "Building unbounded widget (pinned to production-compatible commit)..."
local build_dir
build_dir=$(mktemp -d)
cat > "${build_dir}/Dockerfile" <<'DOCKERFILE'
FROM golang:1.24-alpine AS builder
RUN apk add --no-cache git
WORKDIR /src
RUN git clone https://github.com/getlantern/unbounded.git . && git checkout b53a6690f363
WORKDIR /src/cmd
RUN go build -o /go/bin/widget --ldflags="-X 'main.clientType=widget'"
FROM alpine:3.19
RUN apk add --no-cache ca-certificates procps
COPY --from=builder /go/bin/widget /usr/local/bin/widget
ENTRYPOINT ["widget"]
DOCKERFILE
if ! docker build -t "$UNBOUNDED_IMAGE" "$build_dir"; then
log_error "Failed to build Unbounded image."
rm -rf "$build_dir"
return 1
fi
rm -rf "$build_dir"
log_success "Unbounded image built: $UNBOUNDED_IMAGE"
}
run_unbounded_container() {
if [ "$UNBOUNDED_ENABLED" != "true" ]; then
return 0
fi
local cname="$UNBOUNDED_CONTAINER"
local vname="$UNBOUNDED_VOLUME"
local ub_cpus="${UNBOUNDED_CPUS:-0.5}"
local ub_memory="${UNBOUNDED_MEMORY:-256m}"
local ub_tag="${UNBOUNDED_TAG:-torware-$(hostname 2>/dev/null || echo node)}"
log_info "Starting Unbounded proxy ($cname)..."
build_unbounded_image || return 1
# Remove existing
docker rm -f "$cname" 2>/dev/null || true
# Ensure volume exists
docker volume create "$vname" 2>/dev/null || true
if ! docker run -d \
--name "$cname" \
--restart unless-stopped \
--log-opt max-size=10m \
--log-opt max-file=3 \
--cpus "$(awk -v req="${ub_cpus}" -v cores="$(nproc 2>/dev/null || echo 1)" 'BEGIN{c=req+0; if(c>cores+0) c=cores+0; printf "%.2f",c}')" \
--memory "${ub_memory}" \
--memory-swap "${ub_memory}" \
--health-cmd "pgrep widget || exit 1" \
--health-interval=300s \
--health-timeout=10s \
--health-retries=5 \
--health-start-period=60s \
-e "FREDDIE=${UNBOUNDED_FREDDIE:-https://freddie.iantem.io}" \
-e "EGRESS=${UNBOUNDED_EGRESS:-wss://unbounded.iantem.io}" \
-e "TAG=${ub_tag}" \
-v "${vname}:/var/lib/unbounded" \
"$UNBOUNDED_IMAGE"; then
log_error "Failed to start Unbounded proxy ($cname)"
return 1
fi
log_success "Unbounded proxy started: $cname"
}
stop_unbounded_container() {
local cname="$UNBOUNDED_CONTAINER"
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
docker stop --timeout 10 "$cname" 2>/dev/null || true
log_success "$cname stopped"
fi
}
start_unbounded_container() {
if [ "$UNBOUNDED_ENABLED" != "true" ]; then
return 0
fi
local cname="$UNBOUNDED_CONTAINER"
if docker ps -a --format '{{.Names}}' | grep -q "^${cname}$"; then
if docker start "$cname" 2>/dev/null; then
log_success "$cname started"
else
log_warn "Failed to start $cname, recreating..."
run_unbounded_container
fi
else
run_unbounded_container
fi
}
restart_unbounded_container() {
if [ "$UNBOUNDED_ENABLED" != "true" ]; then
return 0
fi
local cname="$UNBOUNDED_CONTAINER"
docker rm -f "$cname" 2>/dev/null || true
run_unbounded_container
}
get_unbounded_stats() {
# Returns: "live_connections all_time_connections"
local cname="$UNBOUNDED_CONTAINER"
if ! docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
echo "0 0"
return
fi
local live=0 total=0
# Parse widget log messages for connection events
local log_data
log_data=$(docker logs "$cname" 2>&1)
if [ -n "$log_data" ]; then
local opened=$(echo "$log_data" | grep -c "datachannel has opened" 2>/dev/null || echo 0)
local closed=$(echo "$log_data" | grep -c "datachannel has closed" 2>/dev/null || echo 0)
total=${opened:-0}
live=$(( ${opened:-0} - ${closed:-0} ))
[ "$live" -lt 0 ] && live=0
fi
echo "${live:-0} ${total:-0}"
}
#═══════════════════════════════════════════════════════════════════════
# MTProxy (Telegram) Management
#═══════════════════════════════════════════════════════════════════════
generate_mtproxy_secret() {
local domain="${1:-$MTPROXY_DOMAIN}"
domain="${domain:-cloudflare.com}"
# Generate secret using mtg container
docker run --rm "$MTPROXY_IMAGE" generate-secret --hex "$domain" 2>/dev/null
}
get_mtproxy_link() {
local server_ip="${1:-$(get_public_ip)}"
local port="${MTPROXY_PORT:-8443}"
local secret="$MTPROXY_SECRET"
if [ -z "$secret" ]; then
return 1
fi
echo "tg://proxy?server=${server_ip}&port=${port}&secret=${secret}"
}
get_mtproxy_link_https() {
local server_ip="${1:-$(get_public_ip)}"
local port="${MTPROXY_PORT:-8443}"
local secret="$MTPROXY_SECRET"
if [ -z "$secret" ]; then
return 1
fi
echo "https://t.me/proxy?server=${server_ip}&port=${port}&secret=${secret}"
}
show_mtproxy_qr() {
local link
link=$(get_mtproxy_link_https "$1")
if [ -z "$link" ]; then
log_error "MTProxy secret not configured"
return 1
fi
# Generate QR code using Unicode block characters
# Check if qrencode is available
if command -v qrencode &>/dev/null; then
echo ""
echo -e "${BOLD}Scan this QR code in Telegram:${NC}"
echo ""
qrencode -t ANSIUTF8 "$link"
else
# Fallback: try using Docker with qrencode image (pass link via env var for safety)
if docker run --rm -e "QR_LINK=$link" alpine:latest sh -c 'apk add --no-cache qrencode >/dev/null 2>&1 && qrencode -t ANSIUTF8 "$QR_LINK"' 2>/dev/null; then
:
else
# Final fallback: just show the link
echo ""
echo -e "${YELLOW}QR code generation not available (install qrencode for QR support)${NC}"
echo -e "${DIM}Install with: apt install qrencode${NC}"
fi
fi
echo ""
echo -e "${BOLD}Or share this link:${NC}"
echo -e "${CYAN}$link${NC}"
echo ""
}
is_mtproxy_running() {
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${MTPROXY_CONTAINER}$"
}
run_mtproxy_container() {
if [ "$MTPROXY_ENABLED" != "true" ]; then
return 0
fi
local cname="$MTPROXY_CONTAINER"
local port="${MTPROXY_PORT:-8443}"
local metrics_port="${MTPROXY_METRICS_PORT:-3129}"
local cpus="${MTPROXY_CPUS:-0.5}"
local memory="${MTPROXY_MEMORY:-128m}"
local secret="$MTPROXY_SECRET"
local concurrency="${MTPROXY_CONCURRENCY:-8192}"
local blocklist_countries="${MTPROXY_BLOCKLIST_COUNTRIES:-}"
# Pull image if not present (needed for secret generation)
if ! docker image inspect "$MTPROXY_IMAGE" &>/dev/null; then
log_info "Pulling MTProxy image..."
if ! docker pull "$MTPROXY_IMAGE"; then
log_error "Failed to pull MTProxy image"
return 1
fi
fi
# Generate secret if not set (requires image to be present)
if [ -z "$secret" ]; then
log_info "Generating MTProxy secret..."
secret=$(generate_mtproxy_secret)
if [ -z "$secret" ]; then
log_error "Failed to generate MTProxy secret"
return 1
fi
MTPROXY_SECRET="$secret"
save_settings
fi
# Create config directory
local config_dir="$INSTALL_DIR/mtproxy"
mkdir -p "$config_dir"
# Build blocklist URLs from country codes
# Uses ipdeny.com country CIDR lists (reliable, updated daily)
local blocklist_urls=""
if [ -n "$blocklist_countries" ]; then
for cc in $(echo "$blocklist_countries" | tr ',' ' ' | tr '[:upper:]' '[:lower:]'); do
# Validate country code: must be exactly 2 lowercase letters
if [[ "$cc" =~ ^[a-z]{2}$ ]]; then
blocklist_urls+=" \"https://www.ipdeny.com/ipblocks/data/aggregated/${cc}-aggregated.zone\","$'\n'
fi
done
fi
# Generate TOML config (uses actual ports since we use host networking)
cat > "$config_dir/config.toml" << EOF
# MTProxy configuration - generated by Torware
secret = "$secret"
bind-to = "0.0.0.0:${port}"
concurrency = $concurrency
[stats.prometheus]
enabled = true
bind-to = "127.0.0.1:${metrics_port}"
[defense.anti-replay]
enabled = true
max-size = "1mib"
error-rate = 0.001
EOF
# Add blocklist if countries specified
if [ -n "$blocklist_urls" ]; then
cat >> "$config_dir/config.toml" << EOF
[defense.blocklist]
enabled = true
download-concurrency = 2
update-each = "24h"
urls = [
$blocklist_urls]
EOF
log_info "Geo-blocking enabled for: $blocklist_countries"
fi
# Remove existing container
docker rm -f "$cname" 2>/dev/null || true
# Check if port is available
if ss -tln 2>/dev/null | grep -q ":${port} " || netstat -tln 2>/dev/null | grep -q ":${port} "; then
log_error "Port ${port} is already in use. Change MTProxy port in settings."
return 1
fi
log_info "Starting MTProxy container..."
if docker run -d \
--name "$cname" \
--restart unless-stopped \
--network host \
--log-opt max-size=10m \
--log-opt max-file=3 \
--cpus "$cpus" \
--memory "$memory" \
--memory-swap "$memory" \
-v "${config_dir}/config.toml:/config.toml:ro" \
"$MTPROXY_IMAGE" run /config.toml; then
log_success "MTProxy started on port $port (FakeTLS: ${MTPROXY_DOMAIN}, max connections: $concurrency)"
# Send Telegram notification with link and QR code (async, don't block startup)
telegram_notify_mtproxy_started &>/dev/null &
return 0
else
log_error "Failed to start MTProxy"
return 1
fi
}
stop_mtproxy_container() {
local cname="$MTPROXY_CONTAINER"
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
docker stop --timeout 10 "$cname" 2>/dev/null || true
log_success "MTProxy stopped"
fi
}
start_mtproxy_container() {
if [ "$MTPROXY_ENABLED" != "true" ]; then
return 0
fi
local cname="$MTPROXY_CONTAINER"
if docker ps -a --format '{{.Names}}' | grep -q "^${cname}$"; then
if docker start "$cname" 2>/dev/null; then
log_success "MTProxy started"
else
log_warn "Failed to start MTProxy, recreating..."
run_mtproxy_container
fi
else
run_mtproxy_container
fi
}
restart_mtproxy_container() {
if [ "$MTPROXY_ENABLED" != "true" ]; then
return 0
fi
local cname="$MTPROXY_CONTAINER"
docker rm -f "$cname" 2>/dev/null || true
run_mtproxy_container
}
get_mtproxy_stats() {
# Returns: "traffic_in traffic_out"
# Uses prometheus metrics (works with host networking)
if ! is_mtproxy_running; then
echo "0 0"
return
fi
local metrics_port="${MTPROXY_METRICS_PORT:-3129}"
# mtg serves prometheus metrics at "/" by default (not "/metrics")
local metrics
metrics=$(curl -s --max-time 2 "http://127.0.0.1:${metrics_port}/" 2>/dev/null)
if [ -n "$metrics" ]; then
# Parse Prometheus metrics - mtg uses prefix "mtg_" by default:
# - mtg_telegram_traffic{direction="to_client"} = bytes downloaded (to user)
# - mtg_telegram_traffic{direction="from_client"} = bytes uploaded (from user)
local traffic_in traffic_out
traffic_in=$(echo "$metrics" | awk '/^mtg_telegram_traffic\{.*direction="to_client"/ {sum+=$NF} END {printf "%.0f", sum}' 2>/dev/null)
traffic_out=$(echo "$metrics" | awk '/^mtg_telegram_traffic\{.*direction="from_client"/ {sum+=$NF} END {printf "%.0f", sum}' 2>/dev/null)
echo "${traffic_in:-0} ${traffic_out:-0}"
return
fi
# Fallback: return zeros (docker stats doesn't work with host networking)
echo "0 0"
}
#═══════════════════════════════════════════════════════════════════════
# Status Display
#═══════════════════════════════════════════════════════════════════════
show_status() {
load_settings
local count=${CONTAINER_COUNT:-1}
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} 🧅 TORWARE STATUS ${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BOLD}Default Type:${NC} ${GREEN}${RELAY_TYPE}${NC}"
echo -e " ${BOLD}Nickname:${NC} ${GREEN}${NICKNAME}${NC}"
if [ "$BANDWIDTH" = "-1" ]; then
echo -e " ${BOLD}Bandwidth:${NC} ${GREEN}Unlimited${NC}"
else
echo -e " ${BOLD}Bandwidth:${NC} ${GREEN}${BANDWIDTH} Mbps${NC}"
fi
echo ""
local total_read=0
local total_written=0
local total_circuits=0
local total_conns=0
local running=0
for i in $(seq 1 $count); do
local cname=$(get_container_name $i)
local orport=$(get_container_orport $i)
local controlport=$(get_container_controlport $i)
local status="STOPPED"
local status_color="${RED}"
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
status="RUNNING"
status_color="${GREEN}"
running=$((running + 1))
# Get uptime
local started_at
started_at=$(docker inspect --format '{{.State.StartedAt}}' "$cname" 2>/dev/null)
local uptime_str=""
if [ -n "$started_at" ]; then
local start_epoch
start_epoch=$(date -d "${started_at}" +%s 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%S" "${started_at%%.*}" +%s 2>/dev/null || echo "0")
local now_epoch=$(date +%s)
if [ "$start_epoch" -gt 0 ] 2>/dev/null; then
uptime_str=$(format_duration $((now_epoch - start_epoch)))
fi
fi
# Get traffic
local traffic
traffic=$(get_tor_traffic $i)
local rb=$(echo "$traffic" | awk '{print $1}')
local wb=$(echo "$traffic" | awk '{print $2}')
rb=${rb:-0}; wb=${wb:-0}
total_read=$((total_read + rb))
total_written=$((total_written + wb))
# Get circuits
local circuits=$(get_tor_circuits $i)
circuits=${circuits//[^0-9]/}; circuits=${circuits:-0}
total_circuits=$((total_circuits + circuits))
# Get connections
local conns=$(get_tor_connections $i)
conns=${conns//[^0-9]/}; conns=${conns:-0}
total_conns=$((total_conns + conns))
local c_rtype=$(get_container_relay_type $i)
echo -e " ${BOLD}Container ${i} [${c_rtype}]:${NC} ${status_color}${status}${NC} (up ${uptime_str:-unknown})"
echo -e " ORPort: ${orport} | ControlPort: ${controlport}"
echo -e " Traffic: ↓ $(format_bytes $rb)$(format_bytes $wb)"
echo -e " Circuits: ${circuits} | Connections: ${conns}"
# Show fingerprint
local fp=$(get_tor_fingerprint $i)
if [ -n "$fp" ]; then
echo -e " Fingerprint: ${DIM}${fp}${NC}"
fi
# Show bridge line for bridges
if [ "$c_rtype" = "bridge" ]; then
local bl=$(get_bridge_line $i)
if [ -n "$bl" ]; then
echo -e " Bridge Line: ${DIM}${bl:0:120}...${NC}"
fi
fi
else
echo -e " ${BOLD}Container ${i}:${NC} ${status_color}${status}${NC}"
fi
echo ""
done
# Snowflake status
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
echo -e " ${BOLD}Snowflake Proxy:${NC}"
if is_snowflake_running; then
local sf_stats=$(get_snowflake_stats 2>/dev/null)
local sf_conns=$(echo "$sf_stats" | awk '{print $1}')
local sf_in=$(echo "$sf_stats" | awk '{print $2}')
local sf_out=$(echo "$sf_stats" | awk '{print $3}')
echo -e " Status: ${GREEN}RUNNING${NC}"
echo -e " Connections: ${sf_conns:-0} Traffic: ↓ $(format_bytes ${sf_in:-0})$(format_bytes ${sf_out:-0})"
else
echo -e " Status: ${RED}STOPPED${NC}"
fi
echo ""
fi
# Unbounded status
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
echo -e " ${BOLD}Unbounded Proxy (Lantern):${NC}"
if is_unbounded_running; then
local ub_stats=$(get_unbounded_stats 2>/dev/null)
local ub_live=$(echo "$ub_stats" | awk '{print $1}')
local ub_total=$(echo "$ub_stats" | awk '{print $2}')
echo -e " Status: ${GREEN}RUNNING${NC}"
echo -e " Live connections: ${ub_live:-0} | All-time: ${ub_total:-0}"
else
echo -e " Status: ${RED}STOPPED${NC}"
fi
echo ""
fi
# MTProxy status
if [ "$MTPROXY_ENABLED" = "true" ]; then
echo -e " ${BOLD}MTProxy (Telegram):${NC}"
if is_mtproxy_running; then
local mtp_stats=$(get_mtproxy_stats 2>/dev/null)
local mtp_in=$(echo "$mtp_stats" | awk '{print $1}')
local mtp_out=$(echo "$mtp_stats" | awk '{print $2}')
echo -e " Status: ${GREEN}RUNNING${NC}"
echo -e " Traffic: ↓ $(format_bytes ${mtp_in:-0})$(format_bytes ${mtp_out:-0})"
echo -e " Port: ${MTPROXY_PORT} | Domain: ${MTPROXY_DOMAIN}"
else
echo -e " Status: ${RED}STOPPED${NC}"
fi
echo ""
fi
# Include snowflake in totals
local total_containers=$count
local total_running=$running
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
total_containers=$((total_containers + 1))
if is_snowflake_running; then
total_running=$((total_running + 1))
local sf_stats=$(get_snowflake_stats 2>/dev/null)
local sf_in=$(echo "$sf_stats" | awk '{print $2}')
local sf_out=$(echo "$sf_stats" | awk '{print $3}')
total_read=$((total_read + ${sf_in:-0}))
total_written=$((total_written + ${sf_out:-0}))
fi
fi
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
total_containers=$((total_containers + 1))
if is_unbounded_running; then
total_running=$((total_running + 1))
fi
fi
if [ "$MTPROXY_ENABLED" = "true" ]; then
total_containers=$((total_containers + 1))
if is_mtproxy_running; then
total_running=$((total_running + 1))
local mtp_stats=$(get_mtproxy_stats 2>/dev/null)
local mtp_in=$(echo "$mtp_stats" | awk '{print $1}')
local mtp_out=$(echo "$mtp_stats" | awk '{print $2}')
total_read=$((total_read + ${mtp_in:-0}))
total_written=$((total_written + ${mtp_out:-0}))
fi
fi
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " ${BOLD}Totals:${NC}"
echo -e " Containers: ${GREEN}${total_running}${NC}/${total_containers} running"
echo -e " Traffic: ↓ $(format_bytes $total_read)$(format_bytes $total_written)"
echo -e " Circuits: ${total_circuits}"
echo -e " Connections: ${total_conns}"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo ""
}
#═══════════════════════════════════════════════════════════════════════
# Backup & Restore
#═══════════════════════════════════════════════════════════════════════
backup_keys() {
load_settings
local count=${CONTAINER_COUNT:-1}
mkdir -p "$BACKUP_DIR"
local timestamp=$(date '+%Y%m%d_%H%M%S')
for i in $(seq 1 $count); do
local vname=$(get_volume_name $i)
local backup_file="$BACKUP_DIR/tor_keys_relay${i}_${timestamp}.tar.gz"
if docker volume inspect "$vname" &>/dev/null; then
( umask 077; docker run --rm -v "${vname}:/data:ro" alpine \
sh -c 'cd /data && tar -czf - keys fingerprint pt_state state 2>/dev/null || tar -czf - keys fingerprint state 2>/dev/null' \
> "$backup_file" 2>/dev/null )
if [ -s "$backup_file" ]; then
chmod 600 "$backup_file"
local fp=$(get_tor_fingerprint $i)
log_success "Relay $i backed up: $backup_file"
if [ -n "$fp" ]; then
echo -e " Fingerprint: ${DIM}${fp}${NC}"
fi
else
rm -f "$backup_file"
log_warn "Relay $i: No key data found to backup"
fi
else
log_warn "Relay $i: Volume $vname does not exist"
fi
done
# Rotate old backups — keep only the 10 most recent per relay
for i in $(seq 1 $count); do
local old_backups
old_backups=$(find "$BACKUP_DIR" -maxdepth 1 -name "tor_keys_relay${i}_*.tar.gz" 2>/dev/null | sort -r | tail -n +11)
if [ -n "$old_backups" ]; then
echo "$old_backups" | xargs rm -f 2>/dev/null
log_info "Rotated old backups for relay $i (keeping 10 most recent)"
fi
done
}
restore_keys() {
load_settings
if [ ! -d "$BACKUP_DIR" ]; then
log_warn "No backups directory found."
return 1
fi
local backups=()
while IFS= read -r -d '' f; do
backups+=("$f")
done < <(find "$BACKUP_DIR" -maxdepth 1 -name 'tor_keys_*.tar.gz' -print0 2>/dev/null | sort -z -r)
if [ ${#backups[@]} -eq 0 ]; then
log_warn "No backup files found."
return 1
fi
echo ""
echo -e "${CYAN} Available Backups:${NC}"
echo ""
local idx=1
for backup in "${backups[@]}"; do
local fname=$(basename "$backup")
echo -e " ${GREEN}${idx}.${NC} $fname"
idx=$((idx + 1))
done
echo ""
read -p " Select backup number (or 'q' to cancel): " choice < /dev/tty || true
if [ "$choice" = "q" ] || [ -z "$choice" ]; then
return 0
fi
if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#backups[@]} ]; then
log_error "Invalid selection."
return 1
fi
local selected="${backups[$((choice - 1))]}"
local target_relay=1
# Try to detect which relay this backup is for
local fname=$(basename "$selected")
if [[ "$fname" =~ relay([0-9]+) ]]; then
target_relay="${BASH_REMATCH[1]}"
fi
echo ""
read -p " Restore to relay container $target_relay? [Y/n] " confirm < /dev/tty || true
if [[ "$confirm" =~ ^[Nn]$ ]]; then
return 0
fi
local vname=$(get_volume_name $target_relay)
local cname=$(get_container_name $target_relay)
# Stop container
docker stop --timeout 30 "$cname" 2>/dev/null || true
# Ensure volume exists
docker volume create "$vname" 2>/dev/null || true
# Validate backup filename (prevent path traversal)
local restore_basename
restore_basename=$(basename "$selected")
if [[ ! "$restore_basename" =~ ^tor_keys_relay[0-9]+_[0-9]+_[0-9]+\.tar\.gz$ ]]; then
log_error "Invalid backup filename: $restore_basename"
return 1
fi
# Validate backup integrity
if ! tar -tzf "$selected" &>/dev/null; then
log_error "Backup file is corrupt or not a valid tar.gz: $restore_basename"
return 1
fi
# Restore from backup
if docker run --rm -v "${vname}:/data" -v "$(dirname "$selected"):/backup:ro" alpine \
sh -c "cd /data && tar -xzf '/backup/$restore_basename'" 2>/dev/null; then
log_success "Keys restored to relay $target_relay"
# Restart container
docker start "$cname" 2>/dev/null || run_relay_container $target_relay
else
log_error "Failed to restore keys"
return 1
fi
}
check_and_offer_backup_restore() {
if [ ! -d "$BACKUP_DIR" ]; then
return 0
fi
local latest_backup
latest_backup=$(find "$BACKUP_DIR" -maxdepth 1 -name 'tor_keys_*.tar.gz' -print 2>/dev/null | sort -r | head -1)
if [ -z "$latest_backup" ]; then
return 0
fi
local backup_filename=$(basename "$latest_backup")
# Validate filename to prevent path traversal
if ! [[ "$backup_filename" =~ ^tor_keys_relay[0-9]+_[0-9]+_[0-9]+\.tar\.gz$ ]]; then
log_warn "Backup filename has unexpected format: $backup_filename — skipping"
return 0
fi
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} 📁 PREVIOUS RELAY IDENTITY BACKUP FOUND${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " A backup of your relay identity keys was found:"
echo -e " ${YELLOW}File:${NC} $backup_filename"
echo ""
echo -e " Restoring will:"
echo -e " • Preserve your relay's identity on the Tor network"
echo -e " • Maintain your relay's reputation and consensus weight"
echo -e " • Keep the same fingerprint and bridge line"
echo ""
echo -e " ${YELLOW}Note:${NC} If you don't restore, a new identity will be generated."
echo ""
while true; do
read -p " Restore your previous relay identity? (y/n): " restore_choice < /dev/tty || true
if [[ "$restore_choice" =~ ^[Yy]$ ]]; then
echo ""
log_info "Restoring relay identity from backup..."
# Determine target relay
local target_relay=1
if [[ "$backup_filename" =~ relay([0-9]+) ]]; then
target_relay="${BASH_REMATCH[1]}"
fi
local vname=$(get_volume_name $target_relay)
docker volume create "$vname" 2>/dev/null || true
local restore_ok=false
if docker run --rm -v "${vname}:/data" -v "$BACKUP_DIR":/backup:ro alpine \
sh -c 'cd /data && tar -xzf "/backup/$1"' -- "$backup_filename" 2>/dev/null; then
restore_ok=true
fi
if [ "$restore_ok" = "true" ]; then
log_success "Relay identity restored successfully!"
echo ""
return 0
else
log_error "Failed to restore backup. Proceeding with fresh install."
echo ""
return 1
fi
elif [[ "$restore_choice" =~ ^[Nn]$ ]]; then
echo ""
log_info "Skipping restore. A new relay identity will be generated."
echo ""
return 1
else
echo " Please enter y or n."
fi
done
}
#═══════════════════════════════════════════════════════════════════════
# Auto-Start Services
#═══════════════════════════════════════════════════════════════════════
setup_autostart() {
log_info "Setting up auto-start on boot..."
if [ "$HAS_SYSTEMD" = "true" ]; then
cat > /etc/systemd/system/torware.service << EOF
[Unit]
Description=Torware Service
After=network.target docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
TimeoutStartSec=300
TimeoutStopSec=120
ExecStart=/usr/local/bin/torware start
ExecStop=/usr/local/bin/torware stop
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload 2>/dev/null || true
systemctl enable torware.service 2>/dev/null || true
systemctl start torware.service 2>/dev/null || true
log_success "Systemd service created, enabled, and started"
elif command -v rc-update &>/dev/null; then
cat > /etc/init.d/torware << 'EOF'
#!/sbin/openrc-run
name="torware"
description="Torware Service"
depend() {
need docker
after network
}
start() {
ebegin "Starting Torware"
/usr/local/bin/torware start
eend $?
}
stop() {
ebegin "Stopping Torware"
/usr/local/bin/torware stop
eend $?
}
status() {
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^torware'; then
einfo "Torware is running"
return 0
else
einfo "Torware is stopped"
return 3
fi
}
EOF
chmod +x /etc/init.d/torware
rc-update add torware default 2>/dev/null || true
log_success "OpenRC service created and enabled"
elif [ -d /etc/init.d ]; then
cat > /etc/init.d/torware << 'EOF'
#!/bin/sh
### BEGIN INIT INFO
# Provides: torware
# Required-Start: docker
# Required-Stop: docker
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Torware Service
### END INIT INFO
case "$1" in
start)
/usr/local/bin/torware start
;;
stop)
/usr/local/bin/torware stop
;;
restart)
/usr/local/bin/torware restart
;;
status)
docker ps | grep -q torware && echo "Running" || echo "Stopped"
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
EOF
chmod +x /etc/init.d/torware
if command -v update-rc.d &>/dev/null; then
update-rc.d torware defaults 2>/dev/null || true
elif command -v chkconfig &>/dev/null; then
chkconfig torware on 2>/dev/null || true
fi
log_success "SysVinit service created and enabled"
else
log_warn "Could not set up auto-start. Docker's restart policy will handle restarts."
fi
}
#═══════════════════════════════════════════════════════════════════════
# Doctor - Comprehensive Diagnostics
#═══════════════════════════════════════════════════════════════════════
run_doctor() {
load_settings
local issues=0
local warnings=0
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} 🩺 TORWARE DOCTOR ${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BOLD}Running comprehensive diagnostics...${NC}"
echo ""
# 1. System Requirements
echo -e " ${BOLD}[System Requirements]${NC}"
# Check disk space
echo -n " Disk space: "
local free_space=$(df -BG "$INSTALL_DIR" 2>/dev/null | awk 'NR==2 {print $4}' | tr -d 'G')
if [ "${free_space:-0}" -ge 5 ] 2>/dev/null; then
echo -e "${GREEN}OK${NC} (${free_space}GB free)"
elif [ "${free_space:-0}" -ge 2 ] 2>/dev/null; then
echo -e "${YELLOW}LOW${NC} (${free_space}GB free - recommend 5GB+)"
((warnings++))
else
echo -e "${RED}CRITICAL${NC} (${free_space:-?}GB free)"
((issues++))
fi
# Check RAM
echo -n " Available RAM: "
local free_ram=$(free -m 2>/dev/null | awk '/^Mem:/ {print $7}')
if [ "${free_ram:-0}" -ge 512 ] 2>/dev/null; then
echo -e "${GREEN}OK${NC} (${free_ram}MB available)"
elif [ "${free_ram:-0}" -ge 256 ] 2>/dev/null; then
echo -e "${YELLOW}LOW${NC} (${free_ram}MB available)"
((warnings++))
else
echo -e "${RED}CRITICAL${NC} (${free_ram:-?}MB available)"
((issues++))
fi
# Check CPU load
echo -n " CPU load: "
local load=$(awk '{print $1}' /proc/loadavg 2>/dev/null)
local cores=$(get_cpu_cores)
local load_pct=$(awk "BEGIN {printf \"%.0f\", ($load / $cores) * 100}" 2>/dev/null || echo "?")
if [ "${load_pct:-100}" -le 80 ] 2>/dev/null; then
echo -e "${GREEN}OK${NC} (${load_pct}%)"
else
echo -e "${YELLOW}HIGH${NC} (${load_pct}%)"
((warnings++))
fi
echo ""
echo -e " ${BOLD}[Docker Environment]${NC}"
# Check Docker daemon
echo -n " Docker daemon: "
if docker info &>/dev/null; then
local docker_ver=$(docker --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
echo -e "${GREEN}OK${NC} (v${docker_ver})"
else
echo -e "${RED}FAILED${NC} - Docker is not running"
((issues++))
fi
# Check Docker images
echo -n " Bridge image: "
if docker image inspect "$BRIDGE_IMAGE" &>/dev/null; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${YELLOW}NOT PULLED${NC} (will download on first run)"
((warnings++))
fi
echo -n " Relay image: "
if docker image inspect "$RELAY_IMAGE" &>/dev/null; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${YELLOW}NOT PULLED${NC}"
((warnings++))
fi
echo ""
echo -e " ${BOLD}[Network Connectivity]${NC}"
# Check outbound connectivity
echo -n " Internet access: "
if curl -s --max-time 5 https://check.torproject.org &>/dev/null; then
echo -e "${GREEN}OK${NC}"
elif curl -s --max-time 5 https://www.google.com &>/dev/null; then
echo -e "${GREEN}OK${NC} (via Google)"
else
echo -e "${RED}FAILED${NC} - No internet connectivity"
((issues++))
fi
# Check DNS resolution
echo -n " DNS resolution: "
if host torproject.org &>/dev/null || nslookup torproject.org &>/dev/null 2>&1; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}FAILED${NC} - DNS not working"
((issues++))
fi
# Check external IP
echo -n " External IP: "
local ext_ip=$(get_external_ip 2>/dev/null)
if [ -n "$ext_ip" ]; then
echo -e "${GREEN}OK${NC} (${ext_ip})"
else
echo -e "${YELLOW}UNKNOWN${NC} - Could not detect"
((warnings++))
fi
# Check if ORPort is likely reachable
echo -n " ORPort reachability: "
local orport=${ORPORT_BASE:-9001}
if command -v nc &>/dev/null && timeout 3 nc -z 127.0.0.1 "$orport" 2>/dev/null; then
echo -e "${GREEN}OK${NC} (port $orport listening)"
elif docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^torware"; then
echo -e "${GREEN}OK${NC} (container running)"
else
echo -e "${YELLOW}NOT TESTED${NC} (container not running)"
fi
echo ""
echo -e " ${BOLD}[Configuration]${NC}"
# Check settings file
echo -n " Settings file: "
if [ -f "$INSTALL_DIR/settings.conf" ]; then
# Validate format
if grep -vE '^\s*$|^\s*#|^[A-Za-z_][A-Za-z0-9_]*='\''[^'\'']*'\''$|^[A-Za-z_][A-Za-z0-9_]*=[0-9]+$|^[A-Za-z_][A-Za-z0-9_]*=(true|false)$' "$INSTALL_DIR/settings.conf" 2>/dev/null | grep -q .; then
echo -e "${RED}INVALID${NC} - Contains unsafe content"
((issues++))
else
echo -e "${GREEN}OK${NC}"
fi
else
echo -e "${YELLOW}MISSING${NC}"
((warnings++))
fi
# Check data volumes
echo -n " Data volumes: "
local vol_count=$(docker volume ls --format '{{.Name}}' 2>/dev/null | grep -c "^relay-data" || echo 0)
if [ "$vol_count" -gt 0 ]; then
echo -e "${GREEN}OK${NC} (${vol_count} volume(s))"
else
echo -e "${YELLOW}NONE${NC} (will be created on first run)"
fi
# Check relay keys backup
echo -n " Relay key backups: "
local backup_count=$(ls -1 "$BACKUP_DIR"/*.tar.gz 2>/dev/null | wc -l)
if [ "$backup_count" -gt 0 ]; then
echo -e "${GREEN}OK${NC} (${backup_count} backup(s))"
else
echo -e "${YELLOW}NONE${NC} - Consider running 'torware backup'"
((warnings++))
fi
echo ""
echo -e " ${BOLD}[Container Health]${NC}"
# Check running containers
local count=${CONTAINER_COUNT:-1}
if [ "$count" -gt 0 ] && [ "$RELAY_TYPE" != "none" ]; then
for i in $(seq 1 $count); do
local cname=$(get_container_name $i)
echo -n " ${cname}: "
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
local health=$(docker inspect --format='{{.State.Health.Status}}' "$cname" 2>/dev/null || echo "none")
case "$health" in
healthy) echo -e "${GREEN}HEALTHY${NC}" ;;
unhealthy) echo -e "${RED}UNHEALTHY${NC}"; ((issues++)) ;;
starting) echo -e "${YELLOW}STARTING${NC}" ;;
*) echo -e "${GREEN}RUNNING${NC} (no health check)" ;;
esac
elif docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
echo -e "${RED}STOPPED${NC}"
((issues++))
else
echo -e "${DIM}NOT CREATED${NC}"
fi
done
fi
# Check proxy containers
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
echo -n " snowflake-proxy: "
if is_snowflake_running; then
echo -e "${GREEN}RUNNING${NC}"
else
echo -e "${RED}STOPPED${NC}"
((issues++))
fi
fi
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
echo -n " unbounded-proxy: "
if is_unbounded_running; then
echo -e "${GREEN}RUNNING${NC}"
else
echo -e "${RED}STOPPED${NC}"
((issues++))
fi
fi
if [ "$MTPROXY_ENABLED" = "true" ]; then
echo -n " mtproxy: "
if is_mtproxy_running; then
echo -e "${GREEN}RUNNING${NC}"
else
echo -e "${RED}STOPPED${NC}"
((issues++))
fi
fi
# Summary
echo ""
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
if [ "$issues" -eq 0 ] && [ "$warnings" -eq 0 ]; then
echo -e " ${GREEN}${BOLD}✓ All checks passed!${NC}"
elif [ "$issues" -eq 0 ]; then
echo -e " ${YELLOW}${BOLD}${warnings} warning(s), no critical issues${NC}"
else
echo -e " ${RED}${BOLD}${issues} issue(s), ${warnings} warning(s)${NC}"
fi
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo ""
return $issues
}
#═══════════════════════════════════════════════════════════════════════
# Health Check
#═══════════════════════════════════════════════════════════════════════
health_check() {
load_settings
local count=${CONTAINER_COUNT:-1}
local all_ok=true
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} 🧅 TORWARE HEALTH CHECK ${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo ""
# 1. Docker daemon
echo -n " Docker daemon: "
if docker info &>/dev/null; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}FAILED${NC} - Docker is not running"
all_ok=false
fi
for i in $(seq 1 $count); do
local cname=$(get_container_name $i)
local controlport=$(get_container_controlport $i)
echo ""
echo -e " ${BOLD}--- Container $i ($cname) ---${NC}"
# 2. Container exists
echo -n " Container exists: "
if docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}NOT FOUND${NC}"
all_ok=false
continue
fi
# 3. Container running
echo -n " Container running: "
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}STOPPED${NC}"
all_ok=false
continue
fi
# 4. Restart count
local restarts
restarts=$(docker inspect --format '{{.RestartCount}}' "$cname" 2>/dev/null || echo "0")
echo -n " Restart count: "
if [ "$restarts" -eq 0 ] 2>/dev/null; then
echo -e "${GREEN}0 (healthy)${NC}"
elif [ "$restarts" -lt 5 ] 2>/dev/null; then
echo -e "${YELLOW}${restarts} (some restarts)${NC}"
else
echo -e "${RED}${restarts} (excessive)${NC}"
all_ok=false
fi
# 5. ControlPort accessible
echo -n " ControlPort: "
local nc_cmd=$(get_nc_cmd)
if [ -n "$nc_cmd" ] && timeout 3 $nc_cmd -z 127.0.0.1 "$controlport" 2>/dev/null; then
echo -e "${GREEN}OK (port $controlport)${NC}"
else
echo -e "${YELLOW}NOT ACCESSIBLE (port $controlport)${NC}"
fi
# 6. Cookie auth
echo -n " Cookie auth: "
local cookie=$(get_control_cookie $i)
if [ -n "$cookie" ] && [ "$cookie" != "" ]; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${YELLOW}NO COOKIE (relay may still be bootstrapping)${NC}"
fi
# 7. Data volume
local vname=$(get_volume_name $i)
echo -n " Data volume: "
if docker volume inspect "$vname" &>/dev/null; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}MISSING${NC}"
all_ok=false
fi
# 8. Network mode
echo -n " Network (host mode): "
local netmode
netmode=$(docker inspect --format '{{.HostConfig.NetworkMode}}' "$cname" 2>/dev/null)
if [ "$netmode" = "host" ]; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}${netmode} (should be host)${NC}"
all_ok=false
fi
# 9. Fingerprint
echo -n " Relay fingerprint: "
local fp=$(get_tor_fingerprint $i)
if [ -n "$fp" ]; then
echo -e "${GREEN}OK${NC} (${fp:0:20}...)"
else
echo -e "${YELLOW}NOT YET GENERATED${NC}"
fi
# 10. Logs check
echo -n " Recent activity: "
local recent_logs
recent_logs=$(docker logs --tail 30 "$cname" 2>&1)
if echo "$recent_logs" | grep -qi "bootstrapped 100%\|self-testing indicates"; then
echo -e "${GREEN}OK (fully bootstrapped)${NC}"
elif echo "$recent_logs" | grep -qi "bootstrapped"; then
local pct
pct=$(echo "$recent_logs" | sed -n 's/.*Bootstrapped \([0-9]*\).*/\1/p' | tail -1)
echo -e "${YELLOW}Bootstrapping (${pct}%)${NC}"
else
echo -e "${YELLOW}Checking...${NC}"
fi
done
echo ""
# 11. GeoIP (bridges use Tor's built-in CLIENTS_SEEN; relays need system GeoIP)
echo -n " GeoIP available: "
if command -v geoiplookup &>/dev/null; then
echo -e "${GREEN}OK (geoiplookup)${NC}"
elif command -v mmdblookup &>/dev/null; then
echo -e "${GREEN}OK (mmdblookup)${NC}"
else
# Check if any non-bridge containers exist
local _needs_geoip=false
for _gi in $(seq 1 $count); do
[ "$(get_container_relay_type $_gi)" != "bridge" ] && _needs_geoip=true
done
if [ "$_needs_geoip" = "true" ]; then
echo -e "${YELLOW}NOT INSTALLED (needed for relay country stats)${NC}"
else
echo -e "${GREEN}N/A (bridges use Tor's built-in country data)${NC}"
fi
fi
# 12. netcat
echo -n " netcat available: "
if command -v nc &>/dev/null || command -v ncat &>/dev/null; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}NOT INSTALLED (ControlPort queries will fail)${NC}"
all_ok=false
fi
# 13. od (used for cookie hex conversion — always available in Alpine/busybox)
echo -n " od available: "
if command -v od &>/dev/null; then
echo -e "${GREEN}OK (host)${NC}"
elif docker image inspect alpine &>/dev/null; then
echo -e "${GREEN}OK (Alpine has od via busybox)${NC}"
else
echo -e "${YELLOW}UNCHECKED (Alpine image not cached)${NC}"
fi
# Snowflake proxy health
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
for _sfi in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
local _sfn=$(get_snowflake_name $_sfi)
local _sfm=$(get_snowflake_metrics_port $_sfi)
echo ""
echo -e " ${BOLD}--- ${_sfn} ---${NC}"
echo -n " Container running: "
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${_sfn}$"; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}STOPPED${NC}"
all_ok=false
fi
echo -n " Metrics endpoint: "
if curl -s --max-time 3 "http://127.0.0.1:${_sfm}/internal/metrics" &>/dev/null; then
echo -e "${GREEN}OK (port $_sfm)${NC}"
else
echo -e "${YELLOW}NOT ACCESSIBLE${NC}"
fi
done
fi
# Unbounded proxy health
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
local _ubn="$UNBOUNDED_CONTAINER"
echo ""
echo -e " ${BOLD}--- ${_ubn} ---${NC}"
echo -n " Container running: "
if is_unbounded_running; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}STOPPED${NC}"
all_ok=false
fi
echo -n " Process alive: "
if docker exec "$_ubn" pgrep widget &>/dev/null; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}NOT RUNNING${NC}"
all_ok=false
fi
fi
# MTProxy health
if [ "$MTPROXY_ENABLED" = "true" ]; then
local _mtpn="$MTPROXY_CONTAINER"
local _mtpm="${MTPROXY_METRICS_PORT:-3129}"
echo ""
echo -e " ${BOLD}--- ${_mtpn} ---${NC}"
echo -n " Container running: "
if is_mtproxy_running; then
echo -e "${GREEN}OK${NC}"
else
echo -e "${RED}STOPPED${NC}"
all_ok=false
fi
echo -n " Metrics endpoint: "
if curl -s --max-time 3 "http://127.0.0.1:${_mtpm}/" &>/dev/null; then
echo -e "${GREEN}OK (port $_mtpm)${NC}"
else
echo -e "${YELLOW}NOT ACCESSIBLE${NC}"
fi
fi
echo ""
if [ "$all_ok" = "true" ]; then
echo -e " ${GREEN}✓ All health checks passed${NC}"
else
echo -e " ${YELLOW}⚠ Some checks failed. Review issues above.${NC}"
fi
echo ""
}
#═══════════════════════════════════════════════════════════════════════
# Container Stats Aggregation
#═══════════════════════════════════════════════════════════════════════
get_container_stats() {
# Get CPU and RAM usage across all torware containers
local names=""
for i in $(seq 1 ${CONTAINER_COUNT:-1}); do
names+=" $(get_container_name $i)"
done
if [ "$SNOWFLAKE_ENABLED" = "true" ] && is_snowflake_running; then
for _sfi in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
local _sfn=$(get_snowflake_name $_sfi)
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${_sfn}$" && names+=" $_sfn"
done
fi
if [ "$UNBOUNDED_ENABLED" = "true" ] && is_unbounded_running; then
names+=" $UNBOUNDED_CONTAINER"
fi
if [ "$MTPROXY_ENABLED" = "true" ] && is_mtproxy_running; then
names+=" $MTPROXY_CONTAINER"
fi
local all_stats=$(timeout 10 docker stats --no-stream --format "{{.CPUPerc}} {{.MemUsage}}" $names 2>/dev/null)
local _nlines=$(echo "$all_stats" | wc -l)
if [ -z "$all_stats" ]; then
echo "0% 0MiB"
elif [ "$_nlines" -le 1 ]; then
echo "$all_stats"
else
echo "$all_stats" | awk '{
cpu = $1; gsub(/%/, "", cpu); total_cpu += cpu + 0
mem = $2; gsub(/[^0-9.]/, "", mem); mem += 0
if ($2 ~ /GiB/) mem *= 1024
else if ($2 ~ /KiB/) mem /= 1024
total_mem += mem
if (mem_limit == "") mem_limit = $4
found = 1
} END {
if (!found) { print "0% 0MiB"; exit }
if (total_mem >= 1024) mem_display = sprintf("%.2fGiB", total_mem/1024)
else mem_display = sprintf("%.1fMiB", total_mem)
printf "%.2f%% %s / %s\n", total_cpu, mem_display, mem_limit
}'
fi
}
#═══════════════════════════════════════════════════════════════════════
# Live TUI Dashboard (Phase 2)
#═══════════════════════════════════════════════════════════════════════
print_dashboard_header() {
local EL="\033[K"
local bar="═══════════════════════════════════════════════════════════════════"
echo -e "${CYAN}${bar}${NC}${EL}"
# 🧅 emoji = 2 display cols but 1 char, so printf sees 4 cols for " 🧅 " but display is 5; use %-62s
printf "${CYAN}${NC} 🧅 ${BOLD}%-62s${NC}${CYAN}${NC}${EL}\n" "TORWARE v${VERSION} TORWARE LIVE STATISTICS"
echo -e "${CYAN}${bar}${NC}${EL}"
# Detect mixed relay types for dashboard header
local _dh_type="${RELAY_TYPE}"
for _dhi in $(seq 1 ${CONTAINER_COUNT:-1}); do
[ "$(get_container_relay_type $_dhi)" != "$RELAY_TYPE" ] && _dh_type="mixed" && break
done
# Inner width=67: " Relay Type: "(14) + type(10) + " Nickname: "(12) + nick(31) = 67
local _type_pad=$(printf '%-10s' "$_dh_type")
local _nick_pad=$(printf '%-31s' "$NICKNAME")
echo -e "${CYAN}${NC} Relay Type: ${GREEN}${_type_pad}${NC} Nickname: ${GREEN}${_nick_pad}${NC}${CYAN}${NC}${EL}"
local _bw_text
if [ "$BANDWIDTH" = "-1" ]; then
_bw_text="Unlimited"
else
_bw_text="${BANDWIDTH} Mbps"
fi
# " Bandwidth: "(14) + bw(53) = 67
local _bw_pad=$(printf '%-53s' "$_bw_text")
echo -e "${CYAN}${NC} Bandwidth: ${GREEN}${_bw_pad}${NC}${CYAN}${NC}${EL}"
echo -e "${CYAN}${bar}${NC}${EL}"
}
show_dashboard() {
load_settings
local stop_dashboard=0
local _dash_pid=$$
# Pre-seed CPU state file so first iteration has a valid delta
local _cpu_seed="${TMPDIR:-/tmp}/torware_cpu_state_$$"
if [ -f /proc/stat ] && [ ! -f "$_cpu_seed" ]; then
read -r _cpu user nice system idle iowait irq softirq steal guest < /proc/stat 2>/dev/null
echo "$(( user + nice + system + idle + iowait + irq + softirq + steal )) $(( user + nice + system + irq + softirq + steal ))" > "$_cpu_seed" 2>/dev/null
fi
_dashboard_cleanup() {
echo -ne "\033[?25h"
tput rmcup 2>/dev/null || true
rm -rf "/tmp/.tor_dash.${_dash_pid}."* 2>/dev/null
}
trap '_dashboard_cleanup; stop_dashboard=1' INT TERM
trap '_dashboard_cleanup' RETURN
# Alternate screen buffer
tput smcup 2>/dev/null || true
echo -ne "\033[?25l" # Hide cursor
clear
while [ $stop_dashboard -eq 0 ]; do
# Cursor home (no clear = no flicker)
if ! tput cup 0 0 2>/dev/null; then
printf "\033[H"
fi
local EL="\033[K"
local count=${CONTAINER_COUNT:-1}
print_dashboard_header
echo -e "${EL}"
# Collect stats from all containers in parallel
local _tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/.tor_dash.${_dash_pid}.XXXXXX")
# Cache docker ps once per cycle
local _running_containers
_running_containers=$(docker ps --format '{{.Names}}' 2>/dev/null)
echo "$_running_containers" > "$_tmpdir/ps_cache"
# ControlPort queries in parallel per container
# Note: subshells inherit env (including TELEGRAM_BOT_TOKEN) but only write to temp files
for i in $(seq 1 $count); do
(
local cname=$(get_container_name $i)
local port=$(get_container_controlport $i)
local running=0
if echo "$_running_containers" | grep -q "^${cname}$"; then
running=1
# Uptime
local started_at
started_at=$(docker inspect --format '{{.State.StartedAt}}' "$cname" 2>/dev/null)
local start_epoch=0
if [ -n "$started_at" ]; then
start_epoch=$(date -d "$started_at" +%s 2>/dev/null || date -jf "%Y-%m-%dT%H:%M:%S" "${started_at%%.*}" +%s 2>/dev/null || echo "0")
fi
# Traffic via ControlPort
local result
result=$(controlport_query "$port" \
"GETINFO traffic/read" \
"GETINFO traffic/written" \
"GETINFO circuit-status" \
"GETINFO orconn-status" 2>/dev/null)
local rb=$(echo "$result" | sed -n 's/.*traffic\/read=\([0-9]*\).*/\1/p' | head -1 2>/dev/null || echo "0")
local wb=$(echo "$result" | sed -n 's/.*traffic\/written=\([0-9]*\).*/\1/p' | head -1 2>/dev/null || echo "0")
local circuits=$(echo "$result" | grep -cE '^[0-9]+ (BUILT|EXTENDED|LAUNCHED)' 2>/dev/null || echo "0")
local conns=$(echo "$result" | grep -c '\$' 2>/dev/null || echo "0")
rb=${rb//[^0-9]/}; rb=${rb:-0}
wb=${wb//[^0-9]/}; wb=${wb:-0}
circuits=${circuits//[^0-9]/}; circuits=${circuits:-0}
conns=${conns//[^0-9]/}; conns=${conns:-0}
echo "$running $rb $wb $circuits $conns $start_epoch" > "$_tmpdir/c_${i}"
else
echo "0 0 0 0 0 0" > "$_tmpdir/c_${i}"
fi
) &
done
# System stats + proxy stats in parallel
( get_container_stats > "$_tmpdir/cstats" ) &
( get_net_speed > "$_tmpdir/net" ) &
if [ "$SNOWFLAKE_ENABLED" = "true" ] && echo "$_running_containers" | grep -q "^snowflake-proxy$"; then
( get_snowflake_stats > "$_tmpdir/sf_stats" ) 2>/dev/null &
( get_snowflake_country_stats > "$_tmpdir/sf_countries" ) 2>/dev/null &
fi
if [ "$UNBOUNDED_ENABLED" = "true" ] && echo "$_running_containers" | grep -q "^${UNBOUNDED_CONTAINER}$"; then
( get_unbounded_stats > "$_tmpdir/ub_stats" ) 2>/dev/null &
fi
if [ "$MTPROXY_ENABLED" = "true" ] && echo "$_running_containers" | grep -q "^${MTPROXY_CONTAINER}$"; then
( get_mtproxy_stats > "$_tmpdir/mtp_stats" ) 2>/dev/null &
fi
if [ "${DATA_CAP_GB}" -gt 0 ] 2>/dev/null; then
( controlport_query "$(get_container_controlport 1)" \
"GETINFO accounting/bytes" \
"GETINFO accounting/bytes-left" \
"GETINFO accounting/interval-end" > "$_tmpdir/acct" ) 2>/dev/null &
fi
wait
# Aggregate
local total_read=0 total_written=0 total_circuits=0 total_conns=0 running=0
local earliest_start=0
for i in $(seq 1 $count); do
if [ -f "$_tmpdir/c_${i}" ]; then
read -r c_run c_rb c_wb c_circ c_conn c_start < "$_tmpdir/c_${i}"
c_run=${c_run:-0}; c_rb=${c_rb:-0}; c_wb=${c_wb:-0}; c_circ=${c_circ:-0}; c_conn=${c_conn:-0}; c_start=${c_start:-0}
if [ "$c_run" = "1" ]; then
running=$((running + 1))
total_read=$((total_read + c_rb))
total_written=$((total_written + c_wb))
total_circuits=$((total_circuits + c_circ))
total_conns=$((total_conns + c_conn))
if [ "$earliest_start" -eq 0 ] || { [ "$c_start" -gt 0 ] && [ "$c_start" -lt "$earliest_start" ]; }; then
earliest_start=$c_start
fi
fi
fi
done
# Include snowflake in totals (from parallel-fetched cache)
local _cached_sf_stats=""
if [ -f "$_tmpdir/sf_stats" ]; then
_cached_sf_stats=$(cat "$_tmpdir/sf_stats")
local _sf_in=$(echo "$_cached_sf_stats" | awk '{print $2}'); _sf_in=${_sf_in:-0}
local _sf_out=$(echo "$_cached_sf_stats" | awk '{print $3}'); _sf_out=${_sf_out:-0}
total_read=$((total_read + _sf_in))
total_written=$((total_written + _sf_out))
fi
local uptime_str="N/A"
if [ "$earliest_start" -gt 0 ]; then
uptime_str=$(format_duration $(($(date +%s) - earliest_start)))
fi
# Resource stats
local stats=$(cat "$_tmpdir/cstats" 2>/dev/null)
local net_speed=$(cat "$_tmpdir/net" 2>/dev/null)
# Normalize App CPU
local raw_app_cpu=$(echo "$stats" | awk '{print $1}' | tr -d '%')
local num_cores=$(get_cpu_cores)
# Ensure num_cores is at least 1 to prevent division by zero
[ "${num_cores:-0}" -lt 1 ] 2>/dev/null && num_cores=1
local app_cpu_display="0%"
if [[ "$raw_app_cpu" =~ ^[0-9.]+$ ]]; then
app_cpu_display=$(awk -v cpu="$raw_app_cpu" -v cores="$num_cores" 'BEGIN {printf "%.2f%%", (cores > 0) ? cpu / cores : 0}')
if [ "$num_cores" -gt 1 ]; then
app_cpu_display="${app_cpu_display} (${raw_app_cpu}% vCPU)"
fi
fi
local app_ram=$(echo "$stats" | awk '{print $2, $3, $4}')
# System CPU/RAM inline (quick /proc read)
local sys_cpu="N/A" sys_ram_used="N/A" sys_ram_total="N/A"
local cpu_tmp="${TMPDIR:-/tmp}/torware_cpu_state_$$"
if [ -f /proc/stat ]; then
read -r _cpu user nice system idle iowait irq softirq steal guest < /proc/stat
local total_curr=$((user + nice + system + idle + iowait + irq + softirq + steal))
local work_curr=$((user + nice + system + irq + softirq + steal))
if [ -f "$cpu_tmp" ]; then
read -r total_prev work_prev < "$cpu_tmp"
local dt=$((total_curr - total_prev))
local dw=$((work_curr - work_prev))
if [ "$dt" -gt 0 ]; then
sys_cpu=$(awk -v w="$dw" -v t="$dt" 'BEGIN { printf "%.1f%%", w * 100 / t }')
fi
fi
echo "$total_curr $work_curr" > "$cpu_tmp"
fi
if command -v free &>/dev/null; then
local free_out=$(free -m 2>/dev/null)
sys_ram_used=$(echo "$free_out" | awk '/^Mem:/{if ($3>=1024) printf "%.1fGiB",$3/1024; else printf "%.0fMiB",$3}')
sys_ram_total=$(echo "$free_out" | awk '/^Mem:/{if ($2>=1024) printf "%.1fGiB",$2/1024; else printf "%.0fMiB",$2}')
fi
local rx_mbps=$(echo "$net_speed" | awk '{print $1}')
local tx_mbps=$(echo "$net_speed" | awk '{print $2}')
# ── Display ──
if [ "$running" -gt 0 ]; then
echo -e "${BOLD}Status:${NC} ${GREEN}Running${NC} (${uptime_str})${EL}"
echo -e " Containers: ${GREEN}${running}${NC}/${count} Circuits: ${GREEN}${total_circuits}${NC} Connections: ${GREEN}${total_conns}${NC}${EL}"
# Include unbounded in totals (from parallel-fetched cache)
local _cached_ub_stats=""
if [ -f "$_tmpdir/ub_stats" ]; then
_cached_ub_stats=$(cat "$_tmpdir/ub_stats")
local _ub_in=$(echo "$_cached_ub_stats" | awk '{print $2}'); _ub_in=${_ub_in:-0}
local _ub_out=$(echo "$_cached_ub_stats" | awk '{print $3}'); _ub_out=${_ub_out:-0}
total_read=$((total_read + _ub_in))
total_written=$((total_written + _ub_out))
fi
# Include MTProxy in totals (from parallel-fetched cache)
local _cached_mtp_stats=""
if [ -f "$_tmpdir/mtp_stats" ]; then
_cached_mtp_stats=$(cat "$_tmpdir/mtp_stats")
local _mtp_in=$(echo "$_cached_mtp_stats" | awk '{print $1}'); _mtp_in=${_mtp_in:-0}
local _mtp_out=$(echo "$_cached_mtp_stats" | awk '{print $2}'); _mtp_out=${_mtp_out:-0}
total_read=$((total_read + _mtp_in))
total_written=$((total_written + _mtp_out))
fi
echo -e "${EL}"
echo -e "${CYAN}═══ Traffic (total) ═══${NC}${EL}"
echo -e " Downloaded: ${CYAN}$(format_bytes $total_read)${NC}${EL}"
echo -e " Uploaded: ${CYAN}$(format_bytes $total_written)${NC}${EL}"
echo -e "${EL}"
echo -e "${CYAN}═══ Resource Usage ═══${NC}${EL}"
local _acpu=$(printf '%-24s' "$app_cpu_display")
local _aram=$(printf '%-20s' "$app_ram")
echo -e " App: CPU: ${YELLOW}${_acpu}${NC}| RAM: ${YELLOW}${_aram}${NC}${EL}"
local _scpu=$(printf '%-24s' "$sys_cpu")
local _sram=$(printf '%-20s' "$sys_ram_used / $sys_ram_total")
echo -e " System: CPU: ${YELLOW}${_scpu}${NC}| RAM: ${YELLOW}${_sram}${NC}${EL}"
local _rx=$(printf '%-10s' "$rx_mbps")
local _tx=$(printf '%-10s' "$tx_mbps")
echo -e " Total: Net: ${YELLOW}${_rx}Mbps ↑ ${_tx}Mbps${NC}${EL}"
# Data cap from Tor accounting (from parallel-fetched cache)
if [ "${DATA_CAP_GB}" -gt 0 ] 2>/dev/null; then
echo -e "${EL}"
echo -e "${CYAN}═══ DATA CAP ═══${NC}${EL}"
local acct_result
acct_result=$(cat "$_tmpdir/acct" 2>/dev/null)
local acct_bytes=$(echo "$acct_result" | sed -n 's/.*accounting\/bytes=\([^\r]*\)/\1/p' | head -1 2>/dev/null)
local acct_left=$(echo "$acct_result" | sed -n 's/.*accounting\/bytes-left=\([^\r]*\)/\1/p' | head -1 2>/dev/null)
local acct_end=$(echo "$acct_result" | sed -n 's/.*accounting\/interval-end=\([^\r]*\)/\1/p' | head -1 2>/dev/null)
if [ -n "$acct_bytes" ]; then
local acct_read=$(echo "$acct_bytes" | awk '{print $1}')
local acct_write=$(echo "$acct_bytes" | awk '{print $2}')
local acct_total=$((acct_read + acct_write))
echo -e " Used: ${YELLOW}$(format_bytes $acct_total)${NC} / ${GREEN}${DATA_CAP_GB} GB${NC}${EL}"
[ -n "$acct_end" ] && echo -e " Resets: ${DIM}${acct_end}${NC}${EL}"
fi
fi
# Country breakdown — try tracker snapshot, fallback to CLIENTS_SEEN for bridges
local snap_file="$STATS_DIR/tracker_snapshot"
local data_file="$STATS_DIR/cumulative_data"
# If no tracker data, try querying CLIENTS_SEEN directly for bridges
if [ ! -s "$snap_file" ]; then
local _any_bridge_running=false
for _bi in $(seq 1 $count); do
[ "$(get_container_relay_type $_bi)" = "bridge" ] && _any_bridge_running=true && break
done
if [ "$_any_bridge_running" = "true" ]; then
local cs_data=$(get_tor_clients_seen 1)
if [ -n "$cs_data" ]; then
# Write a temporary snap file from CLIENTS_SEEN
local _tmp_snap=""
IFS=',' read -ra _cs_entries <<< "$cs_data"
for _cse in "${_cs_entries[@]}"; do
local _cc=$(echo "$_cse" | cut -d= -f1)
local _cn=$(echo "$_cse" | cut -d= -f2)
[ -z "$_cc" ] || [ -z "$_cn" ] && continue
local _country
_country=$(country_code_to_name "$_cc")
_tmp_snap+="UP|${_country}|${_cn}|bridge\n"
done
[ -n "$_tmp_snap" ] && printf '%b' "$_tmp_snap" > "$snap_file"
fi
fi
fi
if [ -s "$snap_file" ] || [ -s "$data_file" ]; then
echo -e "${EL}"
# Left: Active Circuits by Country
local left_lines=()
if [ -s "$snap_file" ] && [ "$total_circuits" -gt 0 ]; then
local snap_data
snap_data=$(awk -F'|' '{c[$2]+=$3} END{for(co in c) if(co!="") print c[co]"|"co}' "$snap_file" 2>/dev/null | sort -t'|' -k1 -nr | head -5)
local snap_total=0
if [ -n "$snap_data" ]; then
while IFS='|' read -r cnt co; do
snap_total=$((snap_total + cnt))
done <<< "$snap_data"
fi
[ "$snap_total" -eq 0 ] && snap_total=1
if [ -n "$snap_data" ]; then
while IFS='|' read -r cnt country; do
[ -z "$country" ] && continue
local pct=$((cnt * 100 / snap_total))
[ "$pct" -gt 100 ] && pct=100
local bl=$((pct / 20)); [ "$bl" -lt 1 ] && bl=1; [ "$bl" -gt 5 ] && bl=5
local bf=""; local bp=""; for ((bi=0; bi<bl; bi++)); do bf+="█"; done; for ((bi=bl; bi<5; bi++)); do bp+=" "; done
left_lines+=("$(printf "%-11.11s %3d%% \033[32m%s%s\033[0m %5s" "$country" "$pct" "$bf" "$bp" "$(format_number $cnt)")")
done <<< "$snap_data"
fi
fi
# Right: Top 5 Upload (cumulative)
local right_lines=()
if [ -s "$data_file" ]; then
local all_upload
all_upload=$(awk -F'|' '{if($1!="" && $3+0>0) print $3"|"$1}' "$data_file" 2>/dev/null | sort -t'|' -k1 -nr)
local top5_upload=$(echo "$all_upload" | head -5)
local total_upload=0
if [ -n "$all_upload" ]; then
while IFS='|' read -r bytes co; do
bytes=$(printf '%.0f' "${bytes:-0}" 2>/dev/null) || bytes=0
total_upload=$((total_upload + bytes))
done <<< "$all_upload"
fi
[ "$total_upload" -eq 0 ] && total_upload=1
if [ -n "$top5_upload" ]; then
while IFS='|' read -r bytes country; do
[ -z "$country" ] && continue
bytes=$(printf '%.0f' "${bytes:-0}" 2>/dev/null) || bytes=0
local pct=$((bytes * 100 / total_upload))
local bl=$((pct / 20)); [ "$bl" -lt 1 ] && bl=1; [ "$bl" -gt 5 ] && bl=5
local bf=""; local bp=""; for ((bi=0; bi<bl; bi++)); do bf+="█"; done; for ((bi=bl; bi<5; bi++)); do bp+=" "; done
local fmt_bytes=$(format_bytes $bytes)
right_lines+=("$(printf "%-11.11s %3d%% \033[35m%s%s\033[0m %9s" "$country" "$pct" "$bf" "$bp" "$fmt_bytes")")
done <<< "$top5_upload"
fi
fi
# Print side by side
if [ ${#left_lines[@]} -gt 0 ] || [ ${#right_lines[@]} -gt 0 ]; then
# Use "CLIENTS" for bridges, "CIRCUITS" for relays
local _country_label="CIRCUITS BY COUNTRY"
local _any_b=false
for _li in $(seq 1 $count); do [ "$(get_container_relay_type $_li)" = "bridge" ] && _any_b=true; done
[ "$_any_b" = "true" ] && _country_label="CLIENTS BY COUNTRY"
printf " ${GREEN}${BOLD}%-30s${NC} ${YELLOW}${BOLD}%s${NC}${EL}\n" "$_country_label" "TOP 5 UPLOAD (cumulative)"
local max_rows=${#left_lines[@]}
[ ${#right_lines[@]} -gt $max_rows ] && max_rows=${#right_lines[@]}
for ((ri=0; ri<max_rows; ri++)); do
local lc="${left_lines[$ri]:-}"
local rc="${right_lines[$ri]:-}"
if [ -n "$lc" ] && [ -n "$rc" ]; then
printf " "
echo -ne "$lc"
printf " "
echo -e "$rc${EL}"
elif [ -n "$lc" ]; then
printf " "
echo -e "$lc${EL}"
elif [ -n "$rc" ]; then
printf " %-30s " ""
echo -e "$rc${EL}"
fi
done
fi
fi
echo -e "${EL}"
elif [ "$SNOWFLAKE_ENABLED" = "true" ] && [ -f "$_tmpdir/sf_stats" ]; then
# Snowflake-only mode (no relay containers)
local _sf_only_t="$_cached_sf_stats"
local _sf_only_in=$(echo "$_sf_only_t" | awk '{print $2}')
local _sf_only_out=$(echo "$_sf_only_t" | awk '{print $3}')
echo -e "${BOLD}Status:${NC} ${GREEN}Running (Snowflake only)${NC}${EL}"
echo -e "${EL}"
echo -e "${CYAN}═══ Traffic (total) ═══${NC}${EL}"
echo -e " Downloaded: ${CYAN}$(format_bytes ${_sf_only_in:-0})${NC}${EL}"
echo -e " Uploaded: ${CYAN}$(format_bytes ${_sf_only_out:-0})${NC}${EL}"
echo -e "${EL}"
else
echo -e "${BOLD}Status:${NC} ${RED}Stopped${NC}${EL}"
echo -e " All container(s) are stopped.${EL}"
echo -e "${EL}"
fi
# Snowflake stats (from parallel-fetched cache)
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
local _sf_label="Snowflake Proxy (WebRTC)"
[ "${SNOWFLAKE_COUNT:-1}" -gt 1 ] && _sf_label="Snowflake Proxy (WebRTC) x${SNOWFLAKE_COUNT}"
echo -e "${CYAN}═══ ${_sf_label} ═══${NC}${EL}"
if [ -f "$_tmpdir/sf_stats" ]; then
local sf_stats="$_cached_sf_stats"
local sf_conns=$(echo "$sf_stats" | awk '{print $1}')
local sf_in=$(echo "$sf_stats" | awk '{print $2}')
local sf_out=$(echo "$sf_stats" | awk '{print $3}')
echo -e " Status: ${GREEN}Running${NC} Connections: ${GREEN}${sf_conns:-0}${NC} Traffic: ↓ $(format_bytes ${sf_in:-0})$(format_bytes ${sf_out:-0})${EL}"
# Snowflake country breakdown (top 5)
local sf_countries=$(cat "$_tmpdir/sf_countries" 2>/dev/null | head -5)
if [ -n "$sf_countries" ]; then
local sf_total=0
while IFS='|' read -r cnt co; do
sf_total=$((sf_total + cnt))
done <<< "$sf_countries"
[ "$sf_total" -eq 0 ] && sf_total=1
while IFS='|' read -r cnt country; do
[ -z "$country" ] && continue
local pct=$((cnt * 100 / sf_total))
local _sfname=$(country_code_to_name "$country")
printf " %-14s %3d%% %5s connections${EL}\n" "$_sfname" "$pct" "$cnt"
done <<< "$sf_countries"
fi
else
echo -e " Status: ${RED}Stopped${NC}${EL}"
fi
echo -e "${EL}"
fi
# Unbounded (Lantern) stats (from parallel-fetched cache)
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
echo -e "${CYAN}═══ Unbounded Proxy (Lantern) ═══${NC}${EL}"
if [ -f "$_tmpdir/ub_stats" ]; then
local ub_stats="$_cached_ub_stats"
local ub_live=$(echo "$ub_stats" | awk '{print $1}')
local ub_total=$(echo "$ub_stats" | awk '{print $2}')
echo -e " Status: ${GREEN}Running${NC} Live: ${GREEN}${ub_live:-0}${NC} All-time: ${ub_total:-0}${EL}"
else
echo -e " Status: ${RED}Stopped${NC}${EL}"
fi
echo -e "${EL}"
fi
# MTProxy stats (from parallel-fetched cache)
if [ "$MTPROXY_ENABLED" = "true" ]; then
echo -e "${CYAN}═══ MTProxy (Telegram) ═══${NC}${EL}"
if [ -f "$_tmpdir/mtp_stats" ]; then
local mtp_stats=$(cat "$_tmpdir/mtp_stats")
local mtp_in=$(echo "$mtp_stats" | awk '{print $1}')
local mtp_out=$(echo "$mtp_stats" | awk '{print $2}')
echo -e " Status: ${GREEN}Running${NC} Traffic: ↓ $(format_bytes ${mtp_in:-0})$(format_bytes ${mtp_out:-0})${EL}"
echo -e " Port: ${MTPROXY_PORT} | FakeTLS Domain: ${MTPROXY_DOMAIN}${EL}"
else
echo -e " Status: ${RED}Stopped${NC}${EL}"
fi
echo -e "${EL}"
fi
# Per-container status (compact) — reuse parallel-fetched data
if [ "$count" -gt 1 ]; then
echo -e "${CYAN}═══ Per-Container ═══${NC}${EL}"
for i in $(seq 1 $count); do
local cname=$(get_container_name $i)
local c_status="${RED}STOP${NC}"
local c_info=""
if [ -f "$_tmpdir/c_${i}" ]; then
read -r c_run c_rb c_wb c_circ c_conn c_start < "$_tmpdir/c_${i}"
if [ "${c_run:-0}" = "1" ]; then
c_status="${GREEN} UP ${NC}"
c_info="$(format_bytes ${c_rb:-0})$(format_bytes ${c_wb:-0}) C:${c_circ:-0}"
fi
fi
printf " %-14s [${c_status}] %s${EL}\n" "$cname" "$c_info"
done
echo -e "${EL}"
fi
rm -rf "$_tmpdir"
echo -e "${BOLD}Refreshes every 5 seconds. Press any key to return to menu...${NC}${EL}"
# Clear leftover lines
if ! tput ed 2>/dev/null; then
printf "\033[J"
fi
# Wait 4s for keypress
if read -t 4 -n 1 -s < /dev/tty 2>/dev/null; then
stop_dashboard=1
fi
done
_dashboard_cleanup
trap - INT TERM RETURN
}
#═══════════════════════════════════════════════════════════════════════
# Advanced Stats (Phase 3)
#═══════════════════════════════════════════════════════════════════════
show_advanced_stats() {
load_settings
local stop_stats=0
_stats_cleanup() {
echo -ne "\033[?25h"
tput rmcup 2>/dev/null || true
}
trap '_stats_cleanup; stop_stats=1' INT TERM
trap '_stats_cleanup' RETURN
tput smcup 2>/dev/null || true
echo -ne "\033[?25l"
clear
while [ $stop_stats -eq 0 ]; do
if ! tput cup 0 0 2>/dev/null; then
printf "\033[H"
fi
local EL="\033[K"
local count=${CONTAINER_COUNT:-1}
local _bar="═══════════════════════════════════════════════════════════════════"
echo -e "${CYAN}${_bar}${NC}${EL}"
printf "${CYAN}${NC} 🧅 %-62s${CYAN}${NC}${EL}\n" "TORWARE ADVANCED STATISTICS"
echo -e "${CYAN}${_bar}${NC}${EL}"
echo -e "${EL}"
# Per-container detailed stats
echo -e "${CYAN}═══ Container Details ═══${NC}${EL}"
printf " ${BOLD}%-18s %-8s %-10s %-10s %-8s %-8s %-8s${NC}${EL}\n" "Name" "Status" "Download" "Upload" "Circuits" "Conns" "CPU"
# Cache docker ps and docker stats once
local _adv_running
_adv_running=$(docker ps --format '{{.Names}}' 2>/dev/null)
local docker_stats_out
docker_stats_out=$(timeout 10 docker stats --no-stream --format "{{.Name}} {{.CPUPerc}} {{.MemUsage}}" 2>/dev/null)
# Parallel ControlPort queries for all containers
local _adv_tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/.tor_adv.$$.XXXXXX")
for i in $(seq 1 $count); do
(
local cname=$(get_container_name $i)
local port=$(get_container_controlport $i)
if echo "$_adv_running" | grep -q "^${cname}$"; then
local result
result=$(controlport_query "$port" \
"GETINFO traffic/read" \
"GETINFO traffic/written" \
"GETINFO circuit-status" \
"GETINFO orconn-status" 2>/dev/null)
local rb=$(echo "$result" | sed -n 's/.*traffic\/read=\([0-9]*\).*/\1/p' | head -1 2>/dev/null || echo "0")
local wb=$(echo "$result" | sed -n 's/.*traffic\/written=\([0-9]*\).*/\1/p' | head -1 2>/dev/null || echo "0")
local circ=$(echo "$result" | grep -cE '^[0-9]+ (BUILT|EXTENDED|LAUNCHED)' 2>/dev/null || echo "0")
local conn=$(echo "$result" | grep -c '\$' 2>/dev/null || echo "0")
rb=${rb//[^0-9]/}; rb=${rb:-0}
wb=${wb//[^0-9]/}; wb=${wb:-0}
circ=${circ//[^0-9]/}; circ=${circ:-0}
conn=${conn//[^0-9]/}; conn=${conn:-0}
echo "1 $rb $wb $circ $conn" > "$_adv_tmpdir/c_${i}"
else
echo "0 0 0 0 0" > "$_adv_tmpdir/c_${i}"
fi
) &
done
# Snowflake + Unbounded stats in parallel too
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
for _sfi in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
( get_snowflake_instance_stats $_sfi > "$_adv_tmpdir/sf_${_sfi}" ) 2>/dev/null &
done
( get_snowflake_country_stats > "$_adv_tmpdir/sf_countries" ) 2>/dev/null &
fi
if [ "$UNBOUNDED_ENABLED" = "true" ] && echo "$_adv_running" | grep -q "^${UNBOUNDED_CONTAINER}$"; then
( get_unbounded_stats > "$_adv_tmpdir/ub_stats" ) 2>/dev/null &
fi
wait
for i in $(seq 1 $count); do
local cname=$(get_container_name $i)
local status="${RED}STOPPED${NC}"
local dl="" ul="" circ="" conn="" cpu=""
if [ -f "$_adv_tmpdir/c_${i}" ]; then
read -r _arun _arb _awb _acirc _aconn < "$_adv_tmpdir/c_${i}"
if [ "${_arun:-0}" = "1" ]; then
status="${GREEN}RUNNING${NC}"
dl=$(format_bytes ${_arb:-0})
ul=$(format_bytes ${_awb:-0})
circ=${_acirc:-0}
conn=${_aconn:-0}
cpu=$(echo "$docker_stats_out" | grep "^${cname} " | awk '{print $2}')
fi
fi
printf " %-18s %-20b %-10s %-10s %-8s %-8s %-8s${EL}\n" \
"$cname" "$status" "${dl:-}" "${ul:-}" "${circ:-}" "${conn:-}" "${cpu:-}"
done
# Snowflake rows (from cached parallel data)
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
for _sfi in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
local _sf_cname=$(get_snowflake_name $_sfi)
local sf_status="${RED}STOPPED${NC}"
local sf_dl="" sf_ul="" sf_conns_adv="" sf_cpu=""
if echo "$_adv_running" | grep -q "^${_sf_cname}$"; then
sf_status="${GREEN}RUNNING${NC}"
if [ -f "$_adv_tmpdir/sf_${_sfi}" ]; then
local sf_s=$(cat "$_adv_tmpdir/sf_${_sfi}")
sf_conns_adv=$(echo "$sf_s" | awk '{print $1}')
sf_dl=$(format_bytes $(echo "$sf_s" | awk '{print $2}'))
sf_ul=$(format_bytes $(echo "$sf_s" | awk '{print $3}'))
fi
sf_cpu=$(echo "$docker_stats_out" | grep "^${_sf_cname} " | awk '{print $2}')
fi
printf " %-18s %-20b %-10s %-10s %-8s %-8s %-8s${EL}\n" \
"$_sf_cname" "$sf_status" "${sf_dl:-}" "${sf_ul:-}" "${sf_conns_adv:-}" "" "${sf_cpu:-}"
done
fi
# Unbounded row (from cached parallel data)
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
local _ub_cname="$UNBOUNDED_CONTAINER"
local ub_status="${RED}STOPPED${NC}"
local ub_live_adv=0 ub_total_adv=0 ub_cpu=""
if echo "$_adv_running" | grep -q "^${_ub_cname}$"; then
ub_status="${GREEN}RUNNING${NC}"
if [ -f "$_adv_tmpdir/ub_stats" ]; then
local ub_s=$(cat "$_adv_tmpdir/ub_stats")
ub_live_adv=$(echo "$ub_s" | awk '{print $1}')
ub_total_adv=$(echo "$ub_s" | awk '{print $2}')
fi
ub_cpu=$(echo "$docker_stats_out" | grep "^${_ub_cname} " | awk '{print $2}')
fi
printf " %-18s %-20b %-10s %-10s %-8s %-8s %-8s${EL}\n" \
"$_ub_cname" "$ub_status" "" "" "" "${ub_live_adv:-0}" "${ub_cpu:-}"
fi
# MTProxy row
if [ "$MTPROXY_ENABLED" = "true" ]; then
local _mtp_cname="$MTPROXY_CONTAINER"
local mtp_status="${RED}STOPPED${NC}"
local mtp_dl="" mtp_ul="" mtp_cpu=""
if echo "$_adv_running" | grep -q "^${_mtp_cname}$"; then
mtp_status="${GREEN}RUNNING${NC}"
local mtp_stats=$(get_mtproxy_stats 2>/dev/null)
local mtp_in=$(echo "$mtp_stats" | awk '{print $1}')
local mtp_out=$(echo "$mtp_stats" | awk '{print $2}')
mtp_dl=$(format_bytes ${mtp_in:-0})
mtp_ul=$(format_bytes ${mtp_out:-0})
mtp_cpu=$(echo "$docker_stats_out" | grep "^${_mtp_cname} " | awk '{print $2}')
fi
printf " %-18s %-20b %-10s %-10s %-8s %-8s %-8s${EL}\n" \
"$_mtp_cname" "$mtp_status" "${mtp_dl:-}" "${mtp_ul:-}" "" "" "${mtp_cpu:-}"
fi
echo -e "${EL}"
# Snowflake detailed stats (from cached parallel data)
if [ "$SNOWFLAKE_ENABLED" = "true" ] && echo "$_adv_running" | grep -q "^snowflake-proxy$"; then
echo -e "${CYAN}═══ Snowflake Proxy Details ═══${NC}${EL}"
local _adv_sf_country
_adv_sf_country=$(cat "$_adv_tmpdir/sf_countries" 2>/dev/null)
if [ -n "$_adv_sf_country" ]; then
local _adv_sf_total=0
while IFS='|' read -r _asc _asco; do
_adv_sf_total=$((_adv_sf_total + _asc))
done <<< "$_adv_sf_country"
[ "$_adv_sf_total" -eq 0 ] && _adv_sf_total=1
echo -e " ${BOLD}Top 5 Countries by Connections${NC}${EL}"
printf " ${BOLD}%-20s %8s %6s %-20s${NC}${EL}\n" "Country" "Conns" "Pct" "Activity"
echo "$_adv_sf_country" | head -5 | while IFS='|' read -r _asc _asco; do
[ -z "$_asco" ] && continue
_asco=$(country_code_to_name "$_asco")
local _aspct=$((_asc * 100 / _adv_sf_total))
local _asbl=$((_aspct / 5)); [ "$_asbl" -lt 1 ] && _asbl=1; [ "$_asbl" -gt 20 ] && _asbl=20
local _asbf=""; for ((_asi=0; _asi<_asbl; _asi++)); do _asbf+="█"; done
printf " %-20.20s %8s %5d%% ${MAGENTA}%s${NC}${EL}\n" "$_asco" "$_asc" "$_aspct" "$_asbf"
done
fi
echo -e "${EL}"
fi
# Country charts from tracker data
local data_file="$STATS_DIR/cumulative_data"
local ips_file="$STATS_DIR/cumulative_ips"
if [ -s "$data_file" ]; then
echo -e "${CYAN}═══ Top 5 Countries by Traffic (All-Time) ═══${NC}${EL}"
# Combine upload+download per country, sort by total
local _combined_traffic
_combined_traffic=$(awk -F'|' '{if($1!="") { dl[$1]+=$2; ul[$1]+=$3 }} END { for(c in dl) printf "%d|%d|%s\n", dl[c]+ul[c], ul[c], c }' "$data_file" 2>/dev/null | sort -t'|' -k1 -nr)
local top5_traffic=$(echo "$_combined_traffic" | head -5)
local _total_traffic=0
if [ -n "$_combined_traffic" ]; then
while IFS='|' read -r tot rest; do
tot=$(printf '%.0f' "${tot:-0}" 2>/dev/null) || tot=0
_total_traffic=$((_total_traffic + tot))
done <<< "$_combined_traffic"
fi
[ "$_total_traffic" -eq 0 ] && _total_traffic=1
printf " ${BOLD}%-14s %4s %-12s %10s %10s${NC}${EL}\n" "Country" "Pct" "" "Upload" "Download"
if [ -n "$top5_traffic" ]; then
while IFS='|' read -r total_bytes up_bytes country; do
[ -z "$country" ] && continue
total_bytes=$(printf '%.0f' "${total_bytes:-0}" 2>/dev/null) || total_bytes=0
up_bytes=$(printf '%.0f' "${up_bytes:-0}" 2>/dev/null) || up_bytes=0
local dl_bytes=$((total_bytes - up_bytes))
local pct=$((total_bytes * 100 / _total_traffic))
local bl=$((pct / 5)); [ "$bl" -lt 1 ] && bl=1; [ "$bl" -gt 12 ] && bl=12
local bf=""; for ((bi=0; bi<bl; bi++)); do bf+="█"; done
printf " %-14.14s %3d%% ${MAGENTA}%-12s${NC} %10s %10s${EL}\n" "$country" "$pct" "$bf" "$(format_bytes $up_bytes)" "$(format_bytes $dl_bytes)"
done <<< "$top5_traffic"
fi
echo -e "${EL}"
fi
if [ -s "$ips_file" ]; then
local total_ips
total_ips=$(wc -l < "$ips_file" 2>/dev/null || echo "0")
local unique_countries
unique_countries=$(awk -F'|' '{print $1}' "$ips_file" 2>/dev/null | sort -u | wc -l)
echo -e " ${BOLD}Lifetime:${NC} ${GREEN}${total_ips}${NC} unique IPs from ${GREEN}${unique_countries}${NC} countries${EL}"
echo -e "${EL}"
fi
# Unbounded detailed stats
if [ "$UNBOUNDED_ENABLED" = "true" ] && echo "$_adv_running" | grep -q "^${UNBOUNDED_CONTAINER}$"; then
echo -e "${CYAN}═══ Unbounded Proxy (Lantern) ═══${NC}${EL}"
if [ -f "$_adv_tmpdir/ub_stats" ]; then
local _ub_detail=$(cat "$_adv_tmpdir/ub_stats")
local _ub_live=$(echo "$_ub_detail" | awk '{print $1}')
local _ub_alltime=$(echo "$_ub_detail" | awk '{print $2}')
echo -e " Live connections: ${GREEN}${_ub_live:-0}${NC} | All-time served: ${_ub_alltime:-0}${EL}"
fi
echo -e "${EL}"
fi
# MTProxy detailed stats
if [ "$MTPROXY_ENABLED" = "true" ] && is_mtproxy_running; then
echo -e "${CYAN}═══ MTProxy (Telegram) ═══${NC}${EL}"
local _mtp_detail=$(get_mtproxy_stats 2>/dev/null)
local _mtp_in=$(echo "$_mtp_detail" | awk '{print $1}')
local _mtp_out=$(echo "$_mtp_detail" | awk '{print $2}')
echo -e " Traffic: ↓ $(format_bytes ${_mtp_in:-0})$(format_bytes ${_mtp_out:-0})${EL}"
echo -e " Port: ${CYAN}${MTPROXY_PORT}${NC} | FakeTLS Domain: ${CYAN}${MTPROXY_DOMAIN}${NC}${EL}"
echo -e "${EL}"
fi
rm -rf "$_adv_tmpdir"
echo -e "${BOLD}Refreshes every 15 seconds. Press any key to return...${NC}${EL}"
if ! tput ed 2>/dev/null; then
printf "\033[J"
fi
if read -t 14 -n 1 -s < /dev/tty 2>/dev/null; then
stop_stats=1
fi
done
_stats_cleanup
trap - INT TERM RETURN
}
#═══════════════════════════════════════════════════════════════════════
# Live Peers by Country (Phase 3)
#═══════════════════════════════════════════════════════════════════════
show_peers() {
load_settings
local stop_peers=0
_peers_cleanup() {
echo -ne "\033[?25h"
tput rmcup 2>/dev/null || true
}
trap '_peers_cleanup; stop_peers=1' INT TERM
trap '_peers_cleanup' RETURN
tput smcup 2>/dev/null || true
echo -ne "\033[?25l"
clear
while [ $stop_peers -eq 0 ]; do
if ! tput cup 0 0 2>/dev/null; then
printf "\033[H"
fi
local EL="\033[K"
local _bar="═══════════════════════════════════════════════════════════════════"
echo -e "${CYAN}${_bar}${NC}${EL}"
printf "${CYAN}${NC} 🧅 %-62s${CYAN}${NC}${EL}\n" "LIVE CONNECTIONS BY COUNTRY"
echo -e "${CYAN}${_bar}${NC}${EL}"
echo -e "${EL}"
local count=${CONTAINER_COUNT:-1}
local snap_file="$STATS_DIR/tracker_snapshot"
# If no tracker snapshot, try direct ControlPort query as fallback
if [ ! -s "$snap_file" ]; then
local _fb_lines=""
for _pi in $(seq 1 $count); do
local _pport=$(get_container_controlport $_pi)
local _prtype=$(get_container_relay_type $_pi)
if [ "$_prtype" = "bridge" ]; then
local _cs=$(controlport_query "$_pport" "GETINFO status/clients-seen" 2>/dev/null)
local _csdata=$(echo "$_cs" | sed -n 's/.*CountrySummary=\([^ \r]*\).*/\1/p' | head -1 2>/dev/null)
if [ -n "$_csdata" ]; then
IFS=',' read -ra _cse <<< "$_csdata"
for _ce in "${_cse[@]}"; do
local _cc=$(echo "$_ce" | cut -d= -f1)
local _cn=$(echo "$_ce" | cut -d= -f2)
[ -z "$_cc" ] || [ -z "$_cn" ] && continue
_fb_lines+="UP|$(country_code_to_name "$_cc")|${_cn}|bridge\n"
done
fi
else
local _oc=$(controlport_query "$_pport" "GETINFO orconn-status" 2>/dev/null)
local _ips=$(echo "$_oc" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' 2>/dev/null | sort -u)
if [ -n "$_ips" ]; then
while read -r _ip; do
[ -z "$_ip" ] && continue
local _geo=$(tor_geo_lookup "$_ip" 2>/dev/null)
_fb_lines+="UP|${_geo:-Unknown}|1|${_ip}\n"
done <<< "$_ips"
fi
fi
done
if [ -n "$_fb_lines" ]; then
printf '%b' "$_fb_lines" > "$snap_file.tmp.$$"
mv "$snap_file.tmp.$$" "$snap_file"
fi
fi
if [ -s "$snap_file" ]; then
local snap_data
snap_data=$(awk -F'|' '{c[$2]+=$3} END{for(co in c) if(co!="") print c[co]"|"co}' "$snap_file" 2>/dev/null | sort -t'|' -k1 -nr)
if [ -n "$snap_data" ]; then
local total=0
while IFS='|' read -r cnt co; do
total=$((total + cnt))
done <<< "$snap_data"
[ "$total" -eq 0 ] && total=1
# Limit to top 5-10 countries based on terminal size
local _term_rows=$(tput lines 2>/dev/null || echo 24)
local _max_countries=$((_term_rows - 27))
[ "$_max_countries" -lt 5 ] && _max_countries=5
[ "$_max_countries" -gt 10 ] && _max_countries=10
printf " ${BOLD}%-20s %6s %8s %-30s${NC}${EL}\n" "Country" "Traffic" "Pct" "Activity"
local _country_count=0
while IFS='|' read -r cnt country; do
[ -z "$country" ] && continue
_country_count=$((_country_count + 1))
[ "$_country_count" -gt "$_max_countries" ] && break
local pct=$((cnt * 100 / total))
local bl=$((pct / 5)); [ "$bl" -lt 1 ] && bl=1; [ "$bl" -gt 20 ] && bl=20
local bf=""; for ((bi=0; bi<bl; bi++)); do bf+="█"; done
printf " %-20.20s %6s %7d%% ${GREEN}%s${NC}${EL}\n" "$country" "$(format_bytes $cnt)" "$pct" "$bf"
done <<< "$snap_data"
local _total_countries=$(echo "$snap_data" | wc -l)
if [ "$_total_countries" -gt "$_max_countries" ]; then
echo -e " ${DIM}... and $((_total_countries - _max_countries)) more countries${NC}${EL}"
fi
else
echo -e " ${DIM}No circuit or client data available yet.${NC}${EL}"
fi
else
echo -e " ${DIM}No connections yet. Bridge may still be bootstrapping.${NC}${EL}"
echo -e " ${DIM}New bridges can take hours to receive clients from Tor's distributor.${NC}${EL}"
fi
# Snowflake section (parallel fetch)
local _peers_running
_peers_running=$(docker ps --format '{{.Names}}' 2>/dev/null)
local _peers_tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/.tor_peers.$$.XXXXXX")
if [ "$SNOWFLAKE_ENABLED" = "true" ] && echo "$_peers_running" | grep -q "^snowflake-proxy$"; then
( get_snowflake_country_stats > "$_peers_tmpdir/sf_countries" ) 2>/dev/null &
( get_snowflake_stats > "$_peers_tmpdir/sf_stats" ) 2>/dev/null &
wait
echo -e "${EL}"
local _sf_label2="Snowflake Proxy (WebRTC)"
[ "${SNOWFLAKE_COUNT:-1}" -gt 1 ] && _sf_label2="Snowflake Proxy (WebRTC) x${SNOWFLAKE_COUNT}"
echo -e "${CYAN}═══ ${_sf_label2} ═══${NC}${EL}"
local _sf_country=$(cat "$_peers_tmpdir/sf_countries" 2>/dev/null)
local _sf_stats=$(cat "$_peers_tmpdir/sf_stats" 2>/dev/null)
local _sf_conns=$(echo "$_sf_stats" | awk '{print $1}')
local _sf_in=$(echo "$_sf_stats" | awk '{print $2}')
local _sf_out=$(echo "$_sf_stats" | awk '{print $3}')
echo -e " Total connections: ${GREEN}${_sf_conns:-0}${NC} Traffic: ↓ $(format_bytes ${_sf_in:-0})$(format_bytes ${_sf_out:-0})${EL}"
if [ -n "$_sf_country" ]; then
local _sf_total=0
while IFS='|' read -r _sc _sco; do
_sf_total=$((_sf_total + _sc))
done <<< "$_sf_country"
[ "$_sf_total" -eq 0 ] && _sf_total=1
echo -e "${EL}"
printf " ${BOLD}%-20s %6s %8s %-30s${NC}${EL}\n" "Country" "Conns" "Pct" "Activity"
echo "$_sf_country" | head -5 | while IFS='|' read -r _scnt _scountry; do
[ -z "$_scountry" ] && continue
_scountry=$(country_code_to_name "$_scountry")
local _spct=$((_scnt * 100 / _sf_total))
local _sbl=$((_spct / 5)); [ "$_sbl" -lt 1 ] && _sbl=1; [ "$_sbl" -gt 20 ] && _sbl=20
local _sbf=""; for ((_si=0; _si<_sbl; _si++)); do _sbf+="█"; done
printf " %-20.20s %6s %7d%% ${MAGENTA}%s${NC}${EL}\n" "$_scountry" "$_scnt" "$_spct" "$_sbf"
done
else
echo -e " ${DIM}No Snowflake clients yet. Waiting for broker to assign connections...${NC}${EL}"
fi
fi
rm -rf "$_peers_tmpdir" 2>/dev/null
echo -e "${EL}"
echo -e "${BOLD}Refreshes every 15 seconds. Press any key to return...${NC}${EL}"
if ! tput ed 2>/dev/null; then
printf "\033[J"
fi
if read -t 14 -n 1 -s < /dev/tty 2>/dev/null; then
stop_peers=1
fi
done
_peers_cleanup
trap - INT TERM RETURN
}
#═══════════════════════════════════════════════════════════════════════
# Background Tracker Service (Phase 3)
#═══════════════════════════════════════════════════════════════════════
is_tracker_active() {
if command -v systemctl &>/dev/null; then
systemctl is-active torware-tracker.service &>/dev/null
return $?
fi
pgrep -f "torware-tracker.sh" &>/dev/null
}
geo_lookup() {
local ip="$1"
local cache_file="$STATS_DIR/geoip_cache"
# Check cache
if [ -f "$cache_file" ]; then
local cached
local escaped_ip="${ip//./\\.}"
cached=$(grep "^${escaped_ip}|" "$cache_file" 2>/dev/null | head -1 | cut -d'|' -f2)
if [ -n "$cached" ]; then
echo "$cached"
return
fi
fi
# Try Tor's built-in GeoIP via ControlPort
local country=""
local port=$(get_container_controlport 1)
local result
result=$(controlport_query "$port" "GETINFO ip-to-country/$ip" 2>/dev/null)
local code
code=$(echo "$result" | sed -n "s/.*ip-to-country\/$ip=\([a-z]*\).*/\1/p" | head -1 2>/dev/null)
if [ -n "$code" ] && [ "$code" != "??" ]; then
country=$(country_code_to_name "$code")
fi
# Fallback to system GeoIP
if [ -z "$country" ]; then
if command -v geoiplookup &>/dev/null; then
country=$(geoiplookup "$ip" 2>/dev/null | awk -F: '/Country Edition/{gsub(/^ +/,"",$2); print $2}' | head -1)
country=$(echo "$country" | sed 's/,.*//')
elif command -v mmdblookup &>/dev/null; then
local db=""
[ -f /usr/share/GeoIP/GeoLite2-Country.mmdb ] && db="/usr/share/GeoIP/GeoLite2-Country.mmdb"
[ -f /var/lib/GeoIP/GeoLite2-Country.mmdb ] && db="/var/lib/GeoIP/GeoLite2-Country.mmdb"
if [ -n "$db" ]; then
country=$(mmdblookup --file "$db" --ip "$ip" country names en 2>/dev/null | grep '"' | sed 's/.*"\(.*\)".*/\1/')
fi
fi
fi
[ -z "$country" ] && country="Unknown"
# Sanitize country name (strip pipe, newlines, control chars)
country=$(printf '%s' "$country" | tr -d '|\n\r' | tr -cd '[:print:]' | head -c 50)
[ -z "$country" ] && country="Unknown"
# Cache it
if [ -n "$cache_file" ]; then
mkdir -p "$(dirname "$cache_file")"
[ ! -f "$cache_file" ] && ( umask 077; touch "$cache_file" )
echo "${ip}|${country}" >> "$cache_file"
# Trim cache if too large
local cache_lines
cache_lines=$(wc -l < "$cache_file" 2>/dev/null || echo "0")
if [ "$cache_lines" -gt 10000 ]; then
tail -5000 "$cache_file" > "${cache_file}.tmp"
mv "${cache_file}.tmp" "$cache_file"
fi
fi
echo "$country"
}
regenerate_tracker_script() {
mkdir -p "$STATS_DIR"
cat > "$INSTALL_DIR/torware-tracker.sh" << 'TRACKER_EOF'
#!/bin/bash
# Torware Tracker - Background ControlPort Monitor
# Generated by torware.sh
if [ "${BASH_VERSINFO[0]:-0}" -lt 4 ] || { [ "${BASH_VERSINFO[0]:-0}" -eq 4 ] && [ "${BASH_VERSINFO[1]:-0}" -lt 2 ]; }; then
echo "Error: torware-tracker requires bash 4.2+ (for associative arrays)" >&2
exit 1
fi
INSTALL_DIR="REPLACE_INSTALL_DIR"
STATS_DIR="$INSTALL_DIR/relay_stats"
CONTROLPORT_BASE=REPLACE_CONTROLPORT_BASE
CONTAINER_COUNT=REPLACE_CONTAINER_COUNT
mkdir -p "$STATS_DIR"
# Source settings (with whitelist validation)
if [ -f "$INSTALL_DIR/settings.conf" ]; then
if ! grep -vE '^\s*$|^\s*#|^[A-Za-z_][A-Za-z0-9_]*='\''[^'\'']*'\''$|^[A-Za-z_][A-Za-z0-9_]*=[0-9]+$|^[A-Za-z_][A-Za-z0-9_]*=(true|false)$' "$INSTALL_DIR/settings.conf" 2>/dev/null | grep -q .; then
source "$INSTALL_DIR/settings.conf"
fi
fi
get_nc_cmd() {
if command -v ncat &>/dev/null; then echo "ncat"
elif command -v nc &>/dev/null; then echo "nc"
else echo ""; fi
}
get_control_cookie() {
local idx=$1
local vol="relay-data-${idx}"
local cache_file="${TMPDIR:-/tmp}/.tor_cookie_cache_${idx}"
[ -L "$cache_file" ] && rm -f "$cache_file"
if [ -f "$cache_file" ]; then
local mtime
mtime=$(stat -c %Y "$cache_file" 2>/dev/null || stat -f %m "$cache_file" 2>/dev/null || echo 0)
local age=$(( $(date +%s) - mtime ))
if [ "$age" -lt 60 ] && [ "$age" -ge 0 ]; then cat "$cache_file"; return; fi
fi
local cookie
cookie=$(docker run --rm -v "${vol}:/data:ro" alpine \
sh -c 'od -A n -t x1 /data/control_auth_cookie 2>/dev/null | tr -d " \n"' 2>/dev/null)
if [ -n "$cookie" ]; then
( umask 077; echo "$cookie" > "$cache_file" )
fi
echo "$cookie"
}
controlport_query() {
local port=$1; shift
local nc_cmd=$(get_nc_cmd)
[ -z "$nc_cmd" ] && return 1
local idx=$((port - CONTROLPORT_BASE + 1))
local cookie=$(get_control_cookie $idx)
[ -z "$cookie" ] && return 1
{
printf 'AUTHENTICATE %s\r\n' "$cookie"
for cmd in "$@"; do printf "%s\r\n" "$cmd"; done
printf "QUIT\r\n"
} | timeout 5 "$nc_cmd" 127.0.0.1 "$port" 2>/dev/null | tr -d '\r'
}
country_code_to_name() {
local cc="$1"
case "$cc" in
# Americas (18)
us) echo "United States" ;; ca) echo "Canada" ;; mx) echo "Mexico" ;;
br) echo "Brazil" ;; ar) echo "Argentina" ;; co) echo "Colombia" ;;
cl) echo "Chile" ;; pe) echo "Peru" ;; ve) echo "Venezuela" ;;
ec) echo "Ecuador" ;; bo) echo "Bolivia" ;; py) echo "Paraguay" ;;
uy) echo "Uruguay" ;; cu) echo "Cuba" ;; cr) echo "Costa Rica" ;;
pa) echo "Panama" ;; do) echo "Dominican Rep." ;; gt) echo "Guatemala" ;;
# Europe West (16)
gb|uk) echo "United Kingdom" ;; de) echo "Germany" ;; fr) echo "France" ;;
nl) echo "Netherlands" ;; be) echo "Belgium" ;; at) echo "Austria" ;;
ch) echo "Switzerland" ;; ie) echo "Ireland" ;; lu) echo "Luxembourg" ;;
es) echo "Spain" ;; pt) echo "Portugal" ;; it) echo "Italy" ;;
gr) echo "Greece" ;; mt) echo "Malta" ;; cy) echo "Cyprus" ;;
is) echo "Iceland" ;;
# Europe North (7)
se) echo "Sweden" ;; no) echo "Norway" ;; fi) echo "Finland" ;;
dk) echo "Denmark" ;; ee) echo "Estonia" ;; lv) echo "Latvia" ;;
lt) echo "Lithuania" ;;
# Europe East (16)
pl) echo "Poland" ;; cz) echo "Czech Rep." ;; sk) echo "Slovakia" ;;
hu) echo "Hungary" ;; ro) echo "Romania" ;; bg) echo "Bulgaria" ;;
hr) echo "Croatia" ;; rs) echo "Serbia" ;; si) echo "Slovenia" ;;
ba) echo "Bosnia" ;; mk) echo "N. Macedonia" ;; al) echo "Albania" ;;
me) echo "Montenegro" ;; ua) echo "Ukraine" ;; md) echo "Moldova" ;;
by) echo "Belarus" ;;
# Russia & Central Asia (6)
ru) echo "Russia" ;; kz) echo "Kazakhstan" ;; uz) echo "Uzbekistan" ;;
tm) echo "Turkmenistan" ;; kg) echo "Kyrgyzstan" ;; tj) echo "Tajikistan" ;;
# Middle East (10)
tr) echo "Turkey" ;; il) echo "Israel" ;; sa) echo "Saudi Arabia" ;;
ae) echo "UAE" ;; ir) echo "Iran" ;; iq) echo "Iraq" ;;
sy) echo "Syria" ;; jo) echo "Jordan" ;; lb) echo "Lebanon" ;;
qa) echo "Qatar" ;;
# Africa (12)
za) echo "South Africa" ;; ng) echo "Nigeria" ;; ke) echo "Kenya" ;;
eg) echo "Egypt" ;; ma) echo "Morocco" ;; tn) echo "Tunisia" ;;
gh) echo "Ghana" ;; et) echo "Ethiopia" ;; tz) echo "Tanzania" ;;
ug) echo "Uganda" ;; dz) echo "Algeria" ;; ly) echo "Libya" ;;
# Asia East (8)
cn) echo "China" ;; jp) echo "Japan" ;; kr) echo "South Korea" ;;
tw) echo "Taiwan" ;; hk) echo "Hong Kong" ;; mn) echo "Mongolia" ;;
kp) echo "North Korea" ;; mo) echo "Macau" ;;
# Asia South & Southeast (14)
in) echo "India" ;; pk) echo "Pakistan" ;; bd) echo "Bangladesh" ;;
np) echo "Nepal" ;; lk) echo "Sri Lanka" ;; mm) echo "Myanmar" ;;
th) echo "Thailand" ;; vn) echo "Vietnam" ;; ph) echo "Philippines" ;;
id) echo "Indonesia" ;; my) echo "Malaysia" ;; sg) echo "Singapore" ;;
kh) echo "Cambodia" ;; la) echo "Laos" ;;
# Oceania & Caucasus (6)
au) echo "Australia" ;; nz) echo "New Zealand" ;; ge) echo "Georgia" ;;
am) echo "Armenia" ;; az) echo "Azerbaijan" ;; bh) echo "Bahrain" ;;
mu) echo "Mauritius" ;; zm) echo "Zambia" ;; sd) echo "Sudan" ;;
zw) echo "Zimbabwe" ;; mz) echo "Mozambique" ;; cm) echo "Cameroon" ;;
ci) echo "Ivory Coast" ;; sn) echo "Senegal" ;; cd) echo "DR Congo" ;;
ao) echo "Angola" ;; om) echo "Oman" ;; kw) echo "Kuwait" ;;
'??') echo "Unknown" ;;
*) echo "$cc" | tr '[:lower:]' '[:upper:]' ;;
esac
}
# GeoIP lookup (simple version for tracker)
geo_lookup() {
local ip="$1"
local cache_file="$STATS_DIR/geoip_cache"
if [ -f "$cache_file" ]; then
local escaped_ip=$(printf '%s' "$ip" | sed 's/[.]/\\./g')
local cached=$(grep "^${escaped_ip}|" "$cache_file" 2>/dev/null | head -1 | cut -d'|' -f2)
[ -n "$cached" ] && echo "$cached" && return
fi
local port=$((CONTROLPORT_BASE))
local result=$(controlport_query "$port" "GETINFO ip-to-country/$ip" 2>/dev/null)
local sed_safe_ip=$(printf '%s' "$ip" | sed 's/[.]/\\./g; s/[/]/\\//g')
local code=$(echo "$result" | sed -n "s/.*ip-to-country\/${sed_safe_ip}=\([a-z]*\).*/\1/p" | head -1 2>/dev/null)
local country=""
if [ -n "$code" ] && [ "$code" != "??" ]; then
country=$(country_code_to_name "$code")
fi
if [ -z "$country" ] && command -v geoiplookup &>/dev/null; then
country=$(geoiplookup "$ip" 2>/dev/null | awk -F: '/Country Edition/{gsub(/^ +/,"",$2); print $2}' | head -1 | sed 's/,.*//')
fi
[ -z "$country" ] && country="Unknown"
mkdir -p "$(dirname "$cache_file")"
[ ! -f "$cache_file" ] && ( umask 077; touch "$cache_file" )
echo "${ip}|${country}" >> "$cache_file"
cl=$(wc -l < "$cache_file" 2>/dev/null || echo "0")
[ "$cl" -gt 10000 ] && { tail -5000 "$cache_file" > "${cache_file}.tmp"; mv "${cache_file}.tmp" "$cache_file"; }
echo "$country"
}
# Track previous traffic totals per container for delta calculation
declare -A prev_read prev_written
# Determine relay type for each container
get_relay_type_for() {
local idx=$1
local var="RELAY_TYPE_${idx}"
local val="${!var}"
if [ -n "$val" ]; then echo "$val"; else echo "${RELAY_TYPE:-bridge}"; fi
}
# Main tracker loop
while true; do
# Reload settings safely
if [ -f "$INSTALL_DIR/settings.conf" ]; then
if ! grep -vE '^\s*$|^\s*#|^[A-Za-z_][A-Za-z0-9_]*='\''[^'\'']*'\''$|^[A-Za-z_][A-Za-z0-9_]*=[0-9]+$|^[A-Za-z_][A-Za-z0-9_]*=(true|false)$' "$INSTALL_DIR/settings.conf" 2>/dev/null | grep -q .; then
source "$INSTALL_DIR/settings.conf"
fi
fi
# Collect data from all containers (unset first to clear stale keys)
unset country_up country_down 2>/dev/null
declare -A country_up country_down
snap_lines=""
for i in $(seq 1 ${CONTAINER_COUNT:-1}); do
port=$((CONTROLPORT_BASE + i - 1))
cname="torware"
[ "$i" -gt 1 ] && cname="torware-${i}"
# Check if running
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$" || continue
rtype=$(get_relay_type_for $i)
if [ "$rtype" = "bridge" ]; then
# --- BRIDGE: Use Tor's native CLIENTS_SEEN for country data ---
# This is Tor's built-in GeoIP-resolved per-country client count
clients_seen=$(controlport_query "$port" "GETINFO status/clients-seen" 2>/dev/null)
country_summary=$(echo "$clients_seen" | sed -n 's/.*CountrySummary=\([^ \r]*\).*/\1/p' | head -1 2>/dev/null)
if [ -n "$country_summary" ]; then
# Parse cc=num,cc=num,...
IFS=',' read -ra cc_entries <<< "$country_summary"
for entry in "${cc_entries[@]}"; do
cc=$(echo "$entry" | cut -d= -f1)
num=$(echo "$entry" | cut -d= -f2)
[ -z "$cc" ] || [ -z "$num" ] && continue
country=$(country_code_to_name "$cc")
snap_lines+="UP|${country}|${num}|bridge\n"
done
fi
else
# --- RELAY: Use orconn-status to get peer relay IPs, then GeoIP ---
orconn=$(controlport_query "$port" "GETINFO orconn-status" 2>/dev/null)
ips=$(echo "$orconn" | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' 2>/dev/null | awk -F. '{if($1<=255 && $2<=255 && $3<=255 && $4<=255) print}' | sort -u)
# Resolve all IPs once and cache country per IP for reuse in traffic distribution
unset _ip_country 2>/dev/null
declare -A _ip_country
if [ -n "$ips" ]; then
while read -r ip; do
[ -z "$ip" ] && continue
country=$(geo_lookup "$ip")
_ip_country["$ip"]="$country"
snap_lines+="UP|${country}|1|${ip}\n"
if ! grep -qF "${country}|${ip}" "$STATS_DIR/cumulative_ips" 2>/dev/null; then
echo "${country}|${ip}" >> "$STATS_DIR/cumulative_ips"
# Cap cumulative_ips at 50000 lines
ip_lines=$(wc -l < "$STATS_DIR/cumulative_ips" 2>/dev/null || echo 0)
if [ "$ip_lines" -gt 50000 ]; then
tail -25000 "$STATS_DIR/cumulative_ips" > "$STATS_DIR/cumulative_ips.tmp"
mv "$STATS_DIR/cumulative_ips.tmp" "$STATS_DIR/cumulative_ips"
fi
fi
done <<< "$ips"
fi
fi
# Get traffic totals and compute deltas (works for all relay types)
traffic=$(controlport_query "$port" "GETINFO traffic/read" "GETINFO traffic/written" 2>/dev/null)
rb=$(echo "$traffic" | sed -n 's/.*traffic\/read=\([0-9]*\).*/\1/p' | head -1)
wb=$(echo "$traffic" | sed -n 's/.*traffic\/written=\([0-9]*\).*/\1/p' | head -1)
[ -z "$rb" ] && rb=0
[ -z "$wb" ] && wb=0
# Compute deltas from previous reading (fixes double-counting)
p_rb=${prev_read[$i]:-0}
p_wb=${prev_written[$i]:-0}
delta_rb=$((rb - p_rb))
delta_wb=$((wb - p_wb))
# Handle container restart (counter reset)
[ "$delta_rb" -lt 0 ] && delta_rb=$rb
[ "$delta_wb" -lt 0 ] && delta_wb=$wb
prev_read[$i]=$rb
prev_written[$i]=$wb
if [ "$rtype" = "bridge" ]; then
# For bridges, distribute traffic evenly across seen countries
if [ -n "$country_summary" ]; then
total_clients=0
IFS=',' read -ra cc_entries <<< "$country_summary"
for entry in "${cc_entries[@]}"; do
num=$(echo "$entry" | cut -d= -f2)
total_clients=$((total_clients + ${num:-0}))
done
[ "$total_clients" -eq 0 ] && total_clients=1
for entry in "${cc_entries[@]}"; do
cc=$(echo "$entry" | cut -d= -f1)
num=$(echo "$entry" | cut -d= -f2)
[ -z "$cc" ] || [ -z "$num" ] && continue
country=$(country_code_to_name "$cc")
frac_up=$((delta_wb * num / total_clients))
frac_down=$((delta_rb * num / total_clients))
country_up["$country"]=$(( ${country_up["$country"]:-0} + frac_up ))
country_down["$country"]=$(( ${country_down["$country"]:-0} + frac_down ))
done
fi
else
# For relays, distribute delta traffic using cached country lookups (no repeat geo_lookup)
ip_count=${#_ip_country[@]}
[ "$ip_count" -eq 0 ] && ip_count=1
per_ip_up=$((delta_wb / ip_count))
per_ip_down=$((delta_rb / ip_count))
for ip in "${!_ip_country[@]}"; do
country="${_ip_country[$ip]}"
country_up["$country"]=$(( ${country_up["$country"]:-0} + per_ip_up ))
country_down["$country"]=$(( ${country_down["$country"]:-0} + per_ip_down ))
done
unset _ip_country
fi
done
# Write tracker snapshot atomically (last 15s window)
if [ -n "$snap_lines" ]; then
printf '%b' "$snap_lines" > "$STATS_DIR/tracker_snapshot.tmp.$$"
mv "$STATS_DIR/tracker_snapshot.tmp.$$" "$STATS_DIR/tracker_snapshot"
fi
# Merge into cumulative_data (country|from_bytes|to_bytes)
if [ ${#country_up[@]} -gt 0 ] || [ ${#country_down[@]} -gt 0 ]; then
tmp_cum="$STATS_DIR/cumulative_data.tmp.$$"
unset cum_down cum_up 2>/dev/null
declare -A cum_down cum_up
if [ -f "$STATS_DIR/cumulative_data" ]; then
while IFS='|' read -r co dl ul; do
[ -z "$co" ] && continue
cum_down["$co"]=$((${cum_down["$co"]:-0} + ${dl:-0}))
cum_up["$co"]=$((${cum_up["$co"]:-0} + ${ul:-0}))
done < "$STATS_DIR/cumulative_data"
fi
for co in "${!country_down[@]}"; do
cum_down["$co"]=$((${cum_down["$co"]:-0} + ${country_down["$co"]:-0}))
done
for co in "${!country_up[@]}"; do
cum_up["$co"]=$((${cum_up["$co"]:-0} + ${country_up["$co"]:-0}))
done
> "$tmp_cum"
for co in $(echo "${!cum_down[@]} ${!cum_up[@]}" | tr ' ' '\n' | sort -u); do
echo "${co}|${cum_down["$co"]:-0}|${cum_up["$co"]:-0}" >> "$tmp_cum"
done
mv "$tmp_cum" "$STATS_DIR/cumulative_data"
unset cum_down cum_up
fi
unset country_up country_down
# Uptime tracking
running=0
for i in $(seq 1 ${CONTAINER_COUNT:-1}); do
cname="torware"
[ "$i" -gt 1 ] && cname="torware-${i}"
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$" && running=$((running + 1))
done
echo "$(date +%s)|${running}" >> "$STATS_DIR/uptime_log"
# Rotate uptime log: trim at 10080 entries (~7 days at 15s intervals), keep 7200 (~5 days)
ul_lines=$(wc -l < "$STATS_DIR/uptime_log" 2>/dev/null || echo "0")
if [ "$ul_lines" -gt 10080 ]; then
tail -7200 "$STATS_DIR/uptime_log" > "$STATS_DIR/uptime_log.tmp"
mv "$STATS_DIR/uptime_log.tmp" "$STATS_DIR/uptime_log"
fi
sleep 15
done
TRACKER_EOF
# Replace placeholders (portable sed without -i)
# Escape & and \ for sed replacement; use # as delimiter (safe for paths)
local escaped_dir=$(printf '%s\n' "$INSTALL_DIR" | sed 's/[&\\]/\\&/g')
sed "s#REPLACE_INSTALL_DIR#${escaped_dir}#g" "$INSTALL_DIR/torware-tracker.sh" > "$INSTALL_DIR/torware-tracker.sh.tmp" && mv "$INSTALL_DIR/torware-tracker.sh.tmp" "$INSTALL_DIR/torware-tracker.sh"
sed "s#REPLACE_CONTROLPORT_BASE#$CONTROLPORT_BASE#g" "$INSTALL_DIR/torware-tracker.sh" > "$INSTALL_DIR/torware-tracker.sh.tmp" && mv "$INSTALL_DIR/torware-tracker.sh.tmp" "$INSTALL_DIR/torware-tracker.sh"
sed "s#REPLACE_CONTAINER_COUNT#${CONTAINER_COUNT:-1}#g" "$INSTALL_DIR/torware-tracker.sh" > "$INSTALL_DIR/torware-tracker.sh.tmp" && mv "$INSTALL_DIR/torware-tracker.sh.tmp" "$INSTALL_DIR/torware-tracker.sh"
chmod +x "$INSTALL_DIR/torware-tracker.sh"
}
setup_tracker_service() {
regenerate_tracker_script
# Detect systemd if not already set
if [ -z "$HAS_SYSTEMD" ]; then
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
HAS_SYSTEMD=true
else
HAS_SYSTEMD=false
fi
fi
if [ "$HAS_SYSTEMD" = "true" ]; then
cat > /etc/systemd/system/torware-tracker.service << EOF
[Unit]
Description=Torware Traffic Tracker
After=network.target docker.service
Requires=docker.service
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=simple
ExecStart=/bin/bash "${INSTALL_DIR}/torware-tracker.sh"
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload 2>/dev/null || true
systemctl enable torware-tracker.service 2>/dev/null || true
systemctl start torware-tracker.service 2>/dev/null || true
log_success "Tracker service started"
else
log_warn "Tracker service requires systemd. Run manually: bash $INSTALL_DIR/torware-tracker.sh &"
fi
}
stop_tracker_service() {
if [ "$HAS_SYSTEMD" = "true" ]; then
systemctl stop torware-tracker.service 2>/dev/null || true
fi
pkill -f "torware-tracker.sh" 2>/dev/null || true
}
#═══════════════════════════════════════════════════════════════════════
# Telegram Integration (Phase 4)
#═══════════════════════════════════════════════════════════════════════
telegram_send_message() {
local msg="$1"
local token="${TELEGRAM_BOT_TOKEN}"
local chat_id="${TELEGRAM_CHAT_ID}"
{ [ -z "$token" ] || [ -z "$chat_id" ]; } && return 1
# Add server label + IP header
local label="${TELEGRAM_SERVER_LABEL:-Torware}"
local ip
ip=$(curl -s --max-time 3 https://api.ipify.org 2>/dev/null \
|| curl -s --max-time 3 https://ifconfig.me 2>/dev/null \
|| echo "")
local escaped_label=$(escape_md "$label")
local header
if [ -n "$ip" ]; then
header="[${escaped_label} | ${ip}]"
else
header="[${escaped_label}]"
fi
local full_msg="${header} ${msg}"
# Use curl config file to avoid leaking token in process list
local _curl_cfg
_curl_cfg=$(mktemp "${TMPDIR:-/tmp}/.tg_curl.XXXXXX") || return 1
chmod 600 "$_curl_cfg"
printf 'url = "https://api.telegram.org/bot%s/sendMessage"\n' "$token" > "$_curl_cfg"
local response
response=$(curl -s --max-time 10 --max-filesize 1048576 -X POST \
-K "$_curl_cfg" \
--data-urlencode "chat_id=${chat_id}" \
--data-urlencode "text=${full_msg}" \
--data-urlencode "parse_mode=Markdown" \
2>/dev/null)
local rc=$?
rm -f "$_curl_cfg"
[ $rc -ne 0 ] && return 1
echo "$response" | grep -q '"ok":true' && return 0
return 1
}
telegram_send_photo_message() {
local photo_url="$1"
local caption="${2:-}"
local token="${TELEGRAM_BOT_TOKEN}"
local chat_id="${TELEGRAM_CHAT_ID}"
{ [ -z "$token" ] || [ -z "$chat_id" ]; } && return 1
local label="${TELEGRAM_SERVER_LABEL:-Torware}"
if [ -n "$caption" ]; then
caption="[${label}] ${caption}"
fi
local _cfg
_cfg=$(mktemp "${TMPDIR:-/tmp}/.tg_curl.XXXXXX") || return 1
chmod 600 "$_cfg"
printf 'url = "https://api.telegram.org/bot%s/sendPhoto"\n' "$token" > "$_cfg"
curl -s --max-time 15 --max-filesize 10485760 -X POST \
-K "$_cfg" \
--data-urlencode "chat_id=${chat_id}" \
--data-urlencode "photo=${photo_url}" \
--data-urlencode "caption=${caption}" \
--data-urlencode "parse_mode=Markdown" \
>/dev/null 2>&1
local rc=$?
rm -f "$_cfg"
return $rc
}
telegram_notify_mtproxy_started() {
local token="${TELEGRAM_BOT_TOKEN}"
local chat_id="${TELEGRAM_CHAT_ID}"
{ [ -z "$token" ] || [ -z "$chat_id" ]; } && return 0
[ "$TELEGRAM_ENABLED" != "true" ] && return 0
[ "$MTPROXY_ENABLED" != "true" ] && return 0
local server_ip
server_ip=$(get_public_ip)
[ -z "$server_ip" ] && return 1
local port="${MTPROXY_PORT:-8443}"
local secret="$MTPROXY_SECRET"
[ -z "$secret" ] && return 1
local https_link="https://t.me/proxy?server=${server_ip}&port=${port}&secret=${secret}"
local tg_link="tg://proxy?server=${server_ip}&port=${port}&secret=${secret}"
local encoded_link
encoded_link=$(printf '%s' "$https_link" | sed 's/&/%26/g; s/?/%3F/g; s/=/%3D/g; s/:/%3A/g; s|/|%2F|g')
local qr_url="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encoded_link}"
local message="📱 *MTProxy Started*
🔗 *Proxy Link (tap to add):*
\`${tg_link}\`
🌐 *Web Link:*
${https_link}
📊 Port: ${port} | Domain: ${MTPROXY_DOMAIN}
_Share the QR code below with users who need access._"
telegram_send_message "$message"
telegram_send_photo_message "$qr_url" "📱 *MTProxy QR Code* — Scan in Telegram to connect"
}
telegram_get_chat_id() {
local token="${TELEGRAM_BOT_TOKEN}"
[ -z "$token" ] && return 1
local _curl_cfg
_curl_cfg=$(mktemp "${TMPDIR:-/tmp}/.tg_curl.XXXXXX") || return 1
chmod 600 "$_curl_cfg"
printf 'url = "https://api.telegram.org/bot%s/getUpdates"\n' "$token" > "$_curl_cfg"
local response
response=$(curl -s --max-time 10 --max-filesize 1048576 -K "$_curl_cfg" 2>/dev/null)
rm -f "$_curl_cfg"
[ -z "$response" ] && return 1
echo "$response" | grep -q '"ok":true' || return 1
local chat_id=""
if command -v python3 &>/dev/null; then
chat_id=$(python3 -c "
import json,sys
try:
d=json.loads(sys.stdin.read())
for u in reversed(d.get('result',[])):
if 'message' in u:
print(u['message']['chat']['id']); break
elif 'my_chat_member' in u:
print(u['my_chat_member']['chat']['id']); break
except: pass
" <<< "$response" 2>/dev/null)
fi
# Fallback: POSIX-compatible grep extraction
if [ -z "$chat_id" ]; then
chat_id=$(echo "$response" | grep -o '"chat"[[:space:]]*:[[:space:]]*{[[:space:]]*"id"[[:space:]]*:[[:space:]]*-*[0-9]*' | grep -o -- '-*[0-9]*$' | tail -1 2>/dev/null)
fi
if [ -n "$chat_id" ]; then
if ! echo "$chat_id" | grep -qE '^-?[0-9]+$'; then
return 1
fi
TELEGRAM_CHAT_ID="$chat_id"
return 0
fi
return 1
}
telegram_test_message() {
local interval_label="${TELEGRAM_INTERVAL:-6}"
local container_count="${CONTAINER_COUNT:-1}"
local report=$(telegram_build_report_text)
local message="✅ *Torware Monitor Connected!*
🧅 *What is Torware?*
You are running a Tor relay node helping people in censored regions access the open internet.
📬 *What this bot sends every ${interval_label}h:*
Container status, uptime, circuits, CPU/RAM, data cap & fingerprints.
⚠️ *Alerts:*
High CPU, high RAM, all containers down, or zero connections 2+ hours.
━━━━━━━━━━━━━━━━━━━━
🎮 *Available Commands:*
━━━━━━━━━━━━━━━━━━━━
/tor\_status — Full status report
/tor\_peers — Current connections
/tor\_uptime — Container uptime
/tor\_containers — Container list
/tor\_snowflake — Snowflake proxy details
/tor\_start\_N / /tor\_stop\_N / /tor\_restart\_N
/tor\_help — Show all commands
${report}"
telegram_send_message "$message"
}
telegram_build_report_text() {
load_settings
local count=${CONTAINER_COUNT:-1}
local report="📊 *Torware Status Report*"
report+=$'\n'
report+="🕐 $(date '+%Y-%m-%d %H:%M %Z')"
report+=$'\n'
# Container status & uptime
local running=0
local total_read=0 total_written=0 total_circuits=0 total_conns=0
local earliest_start=""
for i in $(seq 1 $count); do
local cname=$(get_container_name $i)
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
running=$((running + 1))
local started=$(docker inspect --format='{{.State.StartedAt}}' "$cname" 2>/dev/null | cut -d'.' -f1)
if [ -n "$started" ]; then
local se=$(date -d "$started" +%s 2>/dev/null || echo 0)
if [ -z "$earliest_start" ] || [ "$se" -lt "$earliest_start" ] 2>/dev/null; then
earliest_start=$se
fi
fi
local traffic=$(get_tor_traffic $i)
local rb=$(echo "$traffic" | awk '{print $1}'); rb=${rb:-0}
local wb=$(echo "$traffic" | awk '{print $2}'); wb=${wb:-0}
total_read=$((total_read + rb))
total_written=$((total_written + wb))
local circ=$(get_tor_circuits $i); circ=${circ//[^0-9]/}; circ=${circ:-0}
total_circuits=$((total_circuits + circ))
local conn=$(get_tor_connections $i); conn=${conn//[^0-9]/}; conn=${conn:-0}
total_conns=$((total_conns + conn))
fi
done
# Include snowflake in totals
local total_containers=$count
local total_running=$running
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
total_containers=$((total_containers + 1))
if is_snowflake_running 2>/dev/null; then
total_running=$((total_running + 1))
local sf_stats=$(get_snowflake_stats 2>/dev/null)
local sf_in=$(echo "$sf_stats" | awk '{print $2}'); sf_in=${sf_in:-0}
local sf_out=$(echo "$sf_stats" | awk '{print $3}'); sf_out=${sf_out:-0}
total_read=$((total_read + sf_in))
total_written=$((total_written + sf_out))
fi
fi
# Include unbounded in totals
if [ "${UNBOUNDED_ENABLED:-false}" = "true" ]; then
total_containers=$((total_containers + 1))
if is_unbounded_running 2>/dev/null; then
total_running=$((total_running + 1))
# Unbounded traffic not measurable (--network host), skip traffic totals
fi
fi
# Uptime from earliest container
if [ -n "$earliest_start" ] && [ "$earliest_start" -gt 0 ] 2>/dev/null; then
local now=$(date +%s)
local up=$((now - earliest_start))
local days=$((up / 86400)) hours=$(( (up % 86400) / 3600 )) mins=$(( (up % 3600) / 60 ))
if [ "$days" -gt 0 ]; then
report+="⏱ Uptime: ${days}d ${hours}h ${mins}m"
else
report+="⏱ Uptime: ${hours}h ${mins}m"
fi
report+=$'\n'
fi
report+="📦 Containers: ${total_running}/${total_containers} running"
report+=$'\n'
report+="🔗 Circuits: ${total_circuits} | Connections: ${total_conns}"
report+=$'\n'
report+="📊 Traffic: ↓ $(format_bytes $total_read)$(format_bytes $total_written)"
report+=$'\n'
# CPU / RAM (system-wide)
local stats=$(get_container_stats 2>/dev/null)
if [ -n "$stats" ]; then
local raw_cpu=$(echo "$stats" | awk '{print $1}')
local cores=$(get_cpu_cores)
local cpu=$(awk "BEGIN {printf \"%.1f%%\", ${raw_cpu%\%} / $cores}" 2>/dev/null || echo "$raw_cpu")
local sys_ram=""
if command -v free &>/dev/null; then
local free_out=$(free -m 2>/dev/null)
local ram_used=$(echo "$free_out" | awk '/^Mem:/{if ($3>=1024) printf "%.1fGiB",$3/1024; else printf "%dMiB",$3}')
local ram_total=$(echo "$free_out" | awk '/^Mem:/{if ($2>=1024) printf "%.1fGiB",$2/1024; else printf "%dMiB",$2}')
sys_ram="${ram_used} / ${ram_total}"
fi
if [ -n "$sys_ram" ]; then
report+="🖥 CPU: ${cpu} | RAM: ${sys_ram}"
else
local ram=$(echo "$stats" | awk '{print $2, $3, $4}')
report+="🖥 CPU: ${cpu} | RAM: ${ram}"
fi
report+=$'\n'
fi
# Fingerprints
for i in $(seq 1 $count); do
local cname=$(get_container_name $i)
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
local fp=$(get_tor_fingerprint $i 2>/dev/null)
if [ -n "$fp" ]; then
local rtype=$(get_container_relay_type $i)
report+="🆔 C${i} [${rtype}]: ${fp}"
report+=$'\n'
fi
fi
done
# Snowflake details
if [ "$SNOWFLAKE_ENABLED" = "true" ] && is_snowflake_running 2>/dev/null; then
local sf_stats=$(get_snowflake_stats 2>/dev/null)
local sf_conns=$(echo "$sf_stats" | awk '{print $1}'); sf_conns=${sf_conns:-0}
local sf_in=$(echo "$sf_stats" | awk '{print $2}'); sf_in=${sf_in:-0}
local sf_out=$(echo "$sf_stats" | awk '{print $3}'); sf_out=${sf_out:-0}
local sf_total=$((sf_in + sf_out))
report+="❄ Snowflake: ${sf_conns} conns | $(format_bytes $sf_total) transferred"
report+=$'\n'
fi
echo "$report"
}
telegram_build_report() {
local msg=$(telegram_build_report_text)
telegram_send_message "$msg"
}
telegram_setup_wizard() {
# Save and restore variables on Ctrl+C
local _saved_token="$TELEGRAM_BOT_TOKEN"
local _saved_chatid="$TELEGRAM_CHAT_ID"
local _saved_interval="$TELEGRAM_INTERVAL"
local _saved_enabled="$TELEGRAM_ENABLED"
local _saved_starthour="$TELEGRAM_START_HOUR"
local _saved_label="$TELEGRAM_SERVER_LABEL"
local _saved_alerts="$TELEGRAM_ALERTS_ENABLED"
local _saved_daily="$TELEGRAM_DAILY_SUMMARY"
local _saved_weekly="$TELEGRAM_WEEKLY_SUMMARY"
trap 'TELEGRAM_BOT_TOKEN="$_saved_token"; TELEGRAM_CHAT_ID="$_saved_chatid"; TELEGRAM_INTERVAL="$_saved_interval"; TELEGRAM_ENABLED="$_saved_enabled"; TELEGRAM_START_HOUR="$_saved_starthour"; TELEGRAM_SERVER_LABEL="$_saved_label"; TELEGRAM_ALERTS_ENABLED="$_saved_alerts"; TELEGRAM_DAILY_SUMMARY="$_saved_daily"; TELEGRAM_WEEKLY_SUMMARY="$_saved_weekly"; TELEGRAM_ALERTS_ENABLED="$_saved_alerts"; TELEGRAM_DAILY_SUMMARY="$_saved_daily"; TELEGRAM_WEEKLY_SUMMARY="$_saved_weekly"; trap - SIGINT; echo; return' SIGINT
clear
echo -e "${CYAN}══════════════════════════════════════════════════════════════════${NC}"
echo -e " ${BOLD}TELEGRAM NOTIFICATIONS SETUP${NC}"
echo -e "${CYAN}══════════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BOLD}Step 1: Create a Telegram Bot${NC}"
echo -e " ${CYAN}─────────────────────────────${NC}"
echo -e " 1. Open Telegram and search for ${BOLD}@BotFather${NC}"
echo -e " 2. Send ${YELLOW}/newbot${NC}"
echo -e " 3. Choose a name (e.g. \"My Tor Monitor\")"
echo -e " 4. Choose a username (e.g. \"my_tor_relay_bot\")"
echo -e " 5. BotFather will give you a token like:"
echo -e " ${YELLOW}123456789:ABCdefGHIjklMNOpqrsTUVwxyz${NC}"
echo ""
echo -e " ${BOLD}Recommended:${NC} Send these commands to @BotFather:"
echo -e " ${YELLOW}/setjoingroups${NC} → Disable (prevents adding to groups)"
echo -e " ${YELLOW}/setprivacy${NC} → Enable (limits message access)"
echo ""
echo -e " ${YELLOW}⚠ OPSEC Note:${NC} Enabling Telegram notifications creates"
echo -e " outbound connections to api.telegram.org from this server."
echo -e " This traffic may be visible to your network provider."
echo ""
read -p " Enter your bot token: " TELEGRAM_BOT_TOKEN < /dev/tty || { trap - SIGINT; TELEGRAM_BOT_TOKEN="$_saved_token"; return; }
echo ""
# Trim whitespace
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN## }"
TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN%% }"
if [ -z "$TELEGRAM_BOT_TOKEN" ]; then
echo -e " ${RED}No token entered. Setup cancelled.${NC}"
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
trap - SIGINT; return
fi
# Validate token format
if ! echo "$TELEGRAM_BOT_TOKEN" | grep -qE '^[0-9]+:[A-Za-z0-9_-]+$'; then
echo -e " ${RED}Invalid token format. Should be like: 123456789:ABCdefGHI...${NC}"
TELEGRAM_BOT_TOKEN="$_saved_token"; TELEGRAM_CHAT_ID="$_saved_chatid"; TELEGRAM_INTERVAL="$_saved_interval"; TELEGRAM_ENABLED="$_saved_enabled"; TELEGRAM_START_HOUR="$_saved_starthour"; TELEGRAM_SERVER_LABEL="$_saved_label"; TELEGRAM_ALERTS_ENABLED="$_saved_alerts"; TELEGRAM_DAILY_SUMMARY="$_saved_daily"; TELEGRAM_WEEKLY_SUMMARY="$_saved_weekly"
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
trap - SIGINT; return
fi
# Validate token is a real bot by calling getMe
echo -ne " Verifying bot token... "
local _me_cfg
_me_cfg=$(mktemp "${TMPDIR:-/tmp}/.tg_curl.XXXXXX") || true
if [ -n "$_me_cfg" ]; then
chmod 600 "$_me_cfg"
printf 'url = "https://api.telegram.org/bot%s/getMe"\n' "$TELEGRAM_BOT_TOKEN" > "$_me_cfg"
local _me_resp
_me_resp=$(curl -s --max-time 10 -K "$_me_cfg" 2>/dev/null)
rm -f "$_me_cfg"
if echo "$_me_resp" | grep -q '"ok":true'; then
echo -e "${GREEN}✓ Valid${NC}"
else
echo -e "${RED}✗ Invalid token${NC}"
echo -e " ${RED}The Telegram API rejected this token. Please check it and try again.${NC}"
TELEGRAM_BOT_TOKEN="$_saved_token"; TELEGRAM_CHAT_ID="$_saved_chatid"; TELEGRAM_INTERVAL="$_saved_interval"; TELEGRAM_ENABLED="$_saved_enabled"; TELEGRAM_START_HOUR="$_saved_starthour"; TELEGRAM_SERVER_LABEL="$_saved_label"; TELEGRAM_ALERTS_ENABLED="$_saved_alerts"; TELEGRAM_DAILY_SUMMARY="$_saved_daily"; TELEGRAM_WEEKLY_SUMMARY="$_saved_weekly"
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
trap - SIGINT; return
fi
fi
echo ""
echo -e " ${BOLD}Step 2: Get Your Chat ID${NC}"
echo -e " ${CYAN}────────────────────────${NC}"
echo -e " 1. Open your new bot in Telegram"
echo -e " 2. Send it the message: ${YELLOW}/start${NC}"
echo -e ""
echo -e " ${YELLOW}Important:${NC} You MUST send ${BOLD}/start${NC} to the bot first!"
echo -e " The bot cannot respond to you until you do this."
echo -e ""
echo -e " 3. Press Enter here when done..."
echo ""
read -p " Press Enter after sending /start to your bot... " < /dev/tty || { trap - SIGINT; TELEGRAM_BOT_TOKEN="$_saved_token"; TELEGRAM_CHAT_ID="$_saved_chatid"; TELEGRAM_INTERVAL="$_saved_interval"; TELEGRAM_ENABLED="$_saved_enabled"; TELEGRAM_START_HOUR="$_saved_starthour"; TELEGRAM_SERVER_LABEL="$_saved_label"; TELEGRAM_ALERTS_ENABLED="$_saved_alerts"; TELEGRAM_DAILY_SUMMARY="$_saved_daily"; TELEGRAM_WEEKLY_SUMMARY="$_saved_weekly"; return; }
echo -ne " Detecting chat ID... "
local attempts=0
TELEGRAM_CHAT_ID=""
while [ $attempts -lt 3 ] && [ -z "$TELEGRAM_CHAT_ID" ]; do
if telegram_get_chat_id; then
break
fi
attempts=$((attempts + 1))
sleep 2
done
if [ -z "$TELEGRAM_CHAT_ID" ]; then
echo -e "${RED}✗ Could not auto-detect chat ID${NC}"
echo ""
echo -e " ${BOLD}You can enter it manually:${NC}"
echo -e " ${CYAN}────────────────────────────${NC}"
echo -e " Option 1: Send /start to your bot, then press Enter to retry"
echo -e " Option 2: Find your chat ID via @userinfobot on Telegram"
echo -e " and enter it below"
echo ""
read -p " Enter chat ID (or press Enter to retry): " _manual_chatid < /dev/tty || true
if [ -z "$_manual_chatid" ]; then
# Retry detection
echo -ne " Retrying detection... "
attempts=0
while [ $attempts -lt 5 ] && [ -z "$TELEGRAM_CHAT_ID" ]; do
if telegram_get_chat_id; then
break
fi
attempts=$((attempts + 1))
sleep 2
done
elif echo "$_manual_chatid" | grep -qE '^-?[0-9]+$'; then
TELEGRAM_CHAT_ID="$_manual_chatid"
else
echo -e " ${RED}Invalid chat ID. Must be a number.${NC}"
fi
if [ -z "$TELEGRAM_CHAT_ID" ]; then
echo -e " ${RED}✗ Could not get chat ID. Setup cancelled.${NC}"
TELEGRAM_BOT_TOKEN="$_saved_token"; TELEGRAM_CHAT_ID="$_saved_chatid"; TELEGRAM_INTERVAL="$_saved_interval"; TELEGRAM_ENABLED="$_saved_enabled"; TELEGRAM_START_HOUR="$_saved_starthour"; TELEGRAM_SERVER_LABEL="$_saved_label"; TELEGRAM_ALERTS_ENABLED="$_saved_alerts"; TELEGRAM_DAILY_SUMMARY="$_saved_daily"; TELEGRAM_WEEKLY_SUMMARY="$_saved_weekly"
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
trap - SIGINT; return
fi
fi
echo -e "${GREEN}✓ Chat ID: ${TELEGRAM_CHAT_ID}${NC}"
echo ""
echo -e " ${BOLD}Step 3: Notification Mode${NC}"
echo -e " ${CYAN}─────────────────────────────${NC}"
echo -e " 1. 📬 Enable notifications (reports, alerts, summaries) ${DIM}(recommended)${NC}"
echo -e " 2. 🔇 Bot only (no auto-notifications, manual use only)"
echo ""
read -p " Choice [1-2] (default 1): " _notif_choice < /dev/tty || true
if [ "${_notif_choice:-1}" = "2" ]; then
# Bot only — skip interval/start hour, disable auto-notifications
TELEGRAM_ALERTS_ENABLED=false
TELEGRAM_DAILY_SUMMARY=false
TELEGRAM_WEEKLY_SUMMARY=false
TELEGRAM_INTERVAL=6
TELEGRAM_START_HOUR=0
else
# Full notifications — ask interval and start hour
TELEGRAM_ALERTS_ENABLED=true
TELEGRAM_DAILY_SUMMARY=true
TELEGRAM_WEEKLY_SUMMARY=true
echo ""
echo -e " ${BOLD}Step 4: Notification Interval${NC}"
echo -e " ${CYAN}─────────────────────────────${NC}"
echo -e " 1. Every 1 hour"
echo -e " 2. Every 3 hours"
echo -e " 3. Every 6 hours (recommended)"
echo -e " 4. Every 12 hours"
echo -e " 5. Every 24 hours"
echo ""
read -p " Choice [1-5] (default 3): " ichoice < /dev/tty || true
case "$ichoice" in
1) TELEGRAM_INTERVAL=1 ;;
2) TELEGRAM_INTERVAL=3 ;;
4) TELEGRAM_INTERVAL=12 ;;
5) TELEGRAM_INTERVAL=24 ;;
*) TELEGRAM_INTERVAL=6 ;;
esac
echo ""
echo -e " ${BOLD}Step 5: Start Hour${NC}"
echo -e " ${CYAN}─────────────────────────────${NC}"
echo -e " What hour should reports start? (0-23, e.g. 8 = 8:00 AM)"
echo -e " Reports will repeat every ${TELEGRAM_INTERVAL}h from this hour."
echo ""
read -p " Start hour [0-23] (default 0): " shchoice < /dev/tty || true
if [ -n "$shchoice" ] && [ "$shchoice" -ge 0 ] 2>/dev/null && [ "$shchoice" -le 23 ] 2>/dev/null; then
TELEGRAM_START_HOUR=$shchoice
else
TELEGRAM_START_HOUR=0
fi
fi
TELEGRAM_ENABLED=true
save_settings
echo ""
echo -ne " Sending test message... "
if telegram_test_message; then
echo -e "${GREEN}✓ Success!${NC}"
else
echo -e "${RED}✗ Failed to send. Check your token.${NC}"
TELEGRAM_BOT_TOKEN="$_saved_token"; TELEGRAM_CHAT_ID="$_saved_chatid"; TELEGRAM_INTERVAL="$_saved_interval"; TELEGRAM_ENABLED="$_saved_enabled"; TELEGRAM_START_HOUR="$_saved_starthour"; TELEGRAM_SERVER_LABEL="$_saved_label"; TELEGRAM_ALERTS_ENABLED="$_saved_alerts"; TELEGRAM_DAILY_SUMMARY="$_saved_daily"; TELEGRAM_WEEKLY_SUMMARY="$_saved_weekly"
save_settings
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
trap - SIGINT; return
fi
# Only start the notification service if at least one notification type is enabled
if [ "$TELEGRAM_ALERTS_ENABLED" = "true" ] || [ "$TELEGRAM_DAILY_SUMMARY" = "true" ] || [ "$TELEGRAM_WEEKLY_SUMMARY" = "true" ]; then
telegram_start_notify
else
telegram_disable_service
fi
trap - SIGINT
echo ""
if [ "$TELEGRAM_ALERTS_ENABLED" = "true" ]; then
echo -e " ${GREEN}${BOLD}✓ Telegram notifications enabled!${NC}"
echo -e " You'll receive reports every ${TELEGRAM_INTERVAL}h starting at ${TELEGRAM_START_HOUR}:00."
else
echo -e " ${GREEN}${BOLD}✓ Telegram bot connected!${NC}"
echo -e " Auto-notifications are off. Use the menu to send reports manually."
fi
echo ""
read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true
}
telegram_generate_notify_script() {
cat > "$INSTALL_DIR/torware-telegram.sh" << 'TGEOF'
#!/bin/bash
# Torware Telegram Notification Service
# Runs as a systemd service, sends periodic status reports
INSTALL_DIR="REPLACE_INSTALL_DIR"
# Safe settings load with whitelist validation
if [ -f "$INSTALL_DIR/settings.conf" ]; then
if ! grep -vE '^\s*$|^\s*#|^[A-Za-z_][A-Za-z0-9_]*='\''[^'\'']*'\''$|^[A-Za-z_][A-Za-z0-9_]*=[0-9]+$|^[A-Za-z_][A-Za-z0-9_]*=(true|false)$' "$INSTALL_DIR/settings.conf" 2>/dev/null | grep -q .; then
source "$INSTALL_DIR/settings.conf"
fi
fi
# Exit if not configured
[ "$TELEGRAM_ENABLED" != "true" ] && exit 0
[ -z "$TELEGRAM_BOT_TOKEN" ] && exit 0
[ -z "$TELEGRAM_CHAT_ID" ] && exit 0
# Cache server IP once at startup
_server_ip=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null \
|| curl -s --max-time 5 https://ifconfig.me 2>/dev/null \
|| echo "")
escape_md() {
local text="$1"
text="${text//\\/\\\\}"
text="${text//\*/\\*}"
text="${text//_/\\_}"
text="${text//\`/\\\`}"
text="${text//\[/\\[}"
text="${text//\]/\\]}"
echo "$text"
}
telegram_send() {
local message="$1"
local token="${TELEGRAM_BOT_TOKEN}"
local chat_id="${TELEGRAM_CHAT_ID}"
{ [ -z "$token" ] || [ -z "$chat_id" ]; } && return 1
local label="${TELEGRAM_SERVER_LABEL:-$(hostname 2>/dev/null || echo 'unknown')}"
label=$(escape_md "$label")
if [ -n "$_server_ip" ]; then
message="[${label} | ${_server_ip}] ${message}"
else
message="[${label}] ${message}"
fi
local _cfg
_cfg=$(mktemp "${TMPDIR:-/tmp}/.tg_curl.XXXXXX") || return 1
chmod 600 "$_cfg"
printf 'url = "https://api.telegram.org/bot%s/sendMessage"\n' "$token" > "$_cfg"
curl -s --max-time 10 --max-filesize 1048576 -X POST \
-K "$_cfg" \
--data-urlencode "chat_id=${chat_id}" \
--data-urlencode "text=${message}" \
--data-urlencode "parse_mode=Markdown" \
>/dev/null 2>&1
rm -f "$_cfg"
}
telegram_send_photo() {
local photo_url="$1"
local caption="${2:-}"
local token="${TELEGRAM_BOT_TOKEN}"
local chat_id="${TELEGRAM_CHAT_ID}"
{ [ -z "$token" ] || [ -z "$chat_id" ]; } && return 1
local label="${TELEGRAM_SERVER_LABEL:-$(hostname 2>/dev/null || echo 'unknown')}"
if [ -n "$caption" ]; then
caption="[${label}] ${caption}"
fi
local _cfg
_cfg=$(mktemp "${TMPDIR:-/tmp}/.tg_curl.XXXXXX") || return 1
chmod 600 "$_cfg"
printf 'url = "https://api.telegram.org/bot%s/sendPhoto"\n' "$token" > "$_cfg"
curl -s --max-time 15 --max-filesize 10485760 -X POST \
-K "$_cfg" \
--data-urlencode "chat_id=${chat_id}" \
--data-urlencode "photo=${photo_url}" \
--data-urlencode "caption=${caption}" \
--data-urlencode "parse_mode=Markdown" \
>/dev/null 2>&1
rm -f "$_cfg"
}
telegram_notify_mtproxy_started() {
# Send MTProxy link and QR code to Telegram when proxy starts
local token="${TELEGRAM_BOT_TOKEN}"
local chat_id="${TELEGRAM_CHAT_ID}"
{ [ -z "$token" ] || [ -z "$chat_id" ]; } && return 0
[ "$TELEGRAM_ENABLED" != "true" ] && return 0
[ "$MTPROXY_ENABLED" != "true" ] && return 0
local server_ip
server_ip=$(get_public_ip)
[ -z "$server_ip" ] && return 1
local port="${MTPROXY_PORT:-8443}"
local secret="$MTPROXY_SECRET"
[ -z "$secret" ] && return 1
local https_link="https://t.me/proxy?server=${server_ip}&port=${port}&secret=${secret}"
local tg_link="tg://proxy?server=${server_ip}&port=${port}&secret=${secret}"
# URL-encode the link for QR code API
local encoded_link
encoded_link=$(printf '%s' "$https_link" | sed 's/&/%26/g; s/?/%3F/g; s/=/%3D/g; s/:/%3A/g; s|/|%2F|g')
local qr_url="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encoded_link}"
# Send message with links
local message="📱 *MTProxy Started*
🔗 *Proxy Link (tap to add):*
\`${tg_link}\`
🌐 *Web Link:*
${https_link}
📊 Port: ${port} | Domain: ${MTPROXY_DOMAIN}
_Share the QR code below with users who need access._"
telegram_send "$message"
# Send QR code as photo
telegram_send_photo "$qr_url" "📱 *MTProxy QR Code* — Scan in Telegram to connect"
}
get_container_name() {
local i=$1
if [ "$i" -le 1 ]; then
echo "torware"
else
echo "torware-${i}"
fi
}
get_cpu_cores() {
local cores=1
if command -v nproc &>/dev/null; then
cores=$(nproc)
elif [ -f /proc/cpuinfo ]; then
cores=$(grep -c '^processor' /proc/cpuinfo 2>/dev/null || echo 1)
fi
[ "$cores" -lt 1 ] 2>/dev/null && cores=1
echo "$cores"
}
track_uptime() {
local running=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -c "^torware" 2>/dev/null || true)
running=${running:-0}
echo "$(date +%s)|${running}" >> "$INSTALL_DIR/relay_stats/uptime_log"
# Trim to 10080 lines (7 days of per-minute entries)
local log_file="$INSTALL_DIR/relay_stats/uptime_log"
local lines=$(wc -l < "$log_file" 2>/dev/null || echo 0)
if [ "$lines" -gt 10080 ] 2>/dev/null; then
tail -10080 "$log_file" > "${log_file}.tmp" && mv "${log_file}.tmp" "$log_file"
fi
}
calc_uptime_pct() {
local period_secs=${1:-86400}
local log_file="$INSTALL_DIR/relay_stats/uptime_log"
[ ! -s "$log_file" ] && echo "0" && return
local cutoff=$(( $(date +%s) - period_secs ))
local total=0
local up=0
while IFS='|' read -r ts count; do
[ "$ts" -lt "$cutoff" ] 2>/dev/null && continue
total=$((total + 1))
[ "$count" -gt 0 ] 2>/dev/null && up=$((up + 1))
done < "$log_file"
[ "$total" -eq 0 ] && echo "0" && return
awk "BEGIN {printf \"%.1f\", ($up/$total)*100}" 2>/dev/null || echo "0"
}
#═══════════════════════════════════════════════════════════════════════
# Bandwidth History & Graphs
#═══════════════════════════════════════════════════════════════════════
record_bandwidth_sample() {
# Record current bandwidth to hourly history file
# Format: timestamp|download_bytes|upload_bytes
local history_file="$STATS_DIR/bandwidth_history"
local now=$(date +%s)
local hour=$(date +%Y-%m-%d-%H)
# Get current total traffic
local total_down=0 total_up=0
local count=${CONTAINER_COUNT:-1}
for i in $(seq 1 $count); do
local cname=$(get_container_name $i)
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
local stats=$(docker logs --tail 1000 "$cname" 2>&1 | grep -oE 'Heartbeat:.*' | tail -1)
local down=$(echo "$stats" | grep -oE 'Read [0-9]+' | grep -oE '[0-9]+' || echo 0)
local up=$(echo "$stats" | grep -oE 'Written [0-9]+' | grep -oE '[0-9]+' || echo 0)
total_down=$((total_down + ${down:-0}))
total_up=$((total_up + ${up:-0}))
fi
done
# Add snowflake traffic
if is_snowflake_running; then
local sf_stats=$(get_snowflake_stats 2>/dev/null)
local sf_in=$(echo "$sf_stats" | awk '{print $2}')
local sf_out=$(echo "$sf_stats" | awk '{print $3}')
total_down=$((total_down + ${sf_in:-0}))
total_up=$((total_up + ${sf_out:-0}))
fi
# Add MTProxy traffic
if is_mtproxy_running; then
local mtp_stats=$(get_mtproxy_stats 2>/dev/null)
local mtp_in=$(echo "$mtp_stats" | awk '{print $1}')
local mtp_out=$(echo "$mtp_stats" | awk '{print $2}')
total_down=$((total_down + ${mtp_in:-0}))
total_up=$((total_up + ${mtp_out:-0}))
fi
# Append to history (keep last 24 hours = 24 entries max)
echo "${hour}|${total_down}|${total_up}" >> "$history_file"
# Trim to last 24 entries
if [ -f "$history_file" ]; then
tail -24 "$history_file" > "${history_file}.tmp"
mv "${history_file}.tmp" "$history_file"
fi
}
draw_ascii_graph() {
# Draw an ASCII bar graph
# Args: title, max_width, values (space-separated)
local title="$1"
local max_width=${2:-40}
shift 2
local values=("$@")
local max_val=1
# Find max value
for v in "${values[@]}"; do
[ "${v:-0}" -gt "$max_val" ] 2>/dev/null && max_val="$v"
done
echo -e " ${BOLD}${title}${NC}"
echo -e " ${DIM}$(printf '%.0s─' $(seq 1 $((max_width + 10))))${NC}"
local hour_offset=$((24 - ${#values[@]}))
local idx=0
for v in "${values[@]}"; do
local bar_len=0
if [ "$max_val" -gt 0 ] 2>/dev/null; then
bar_len=$((v * max_width / max_val))
fi
[ "$bar_len" -lt 0 ] && bar_len=0
[ "$bar_len" -gt "$max_width" ] && bar_len=$max_width
local hour_label=$((hour_offset + idx))
local bar=""
if [ "$bar_len" -gt 0 ]; then
bar=$(printf '█%.0s' $(seq 1 $bar_len))
fi
# Format value for display
local display_val
if [ "$v" -ge 1073741824 ] 2>/dev/null; then
display_val=$(awk "BEGIN {printf \"%.1fG\", $v/1073741824}")
elif [ "$v" -ge 1048576 ] 2>/dev/null; then
display_val=$(awk "BEGIN {printf \"%.1fM\", $v/1048576}")
elif [ "$v" -ge 1024 ] 2>/dev/null; then
display_val=$(awk "BEGIN {printf \"%.1fK\", $v/1024}")
else
display_val="${v}B"
fi
printf " %2dh │${GREEN}%-${max_width}s${NC}│ %s\n" "$hour_label" "$bar" "$display_val"
((idx++))
done
echo ""
}
show_bandwidth_graphs() {
local history_file="$STATS_DIR/bandwidth_history"
if [ ! -f "$history_file" ] || [ ! -s "$history_file" ]; then
echo -e " ${YELLOW}No bandwidth history available yet.${NC}"
echo -e " ${DIM}History is recorded hourly. Check back later.${NC}"
return
fi
# Read history into arrays
local -a hours=() downloads=() uploads=()
while IFS='|' read -r hour down up; do
hours+=("$hour")
downloads+=("${down:-0}")
uploads+=("${up:-0}")
done < "$history_file"
# Calculate deltas (difference between consecutive readings)
local -a down_deltas=() up_deltas=()
local prev_down=0 prev_up=0
for i in "${!downloads[@]}"; do
local d=${downloads[$i]:-0}
local u=${uploads[$i]:-0}
if [ "$i" -eq 0 ]; then
down_deltas+=(0)
up_deltas+=(0)
else
local dd=$((d - prev_down))
local ud=$((u - prev_up))
[ "$dd" -lt 0 ] && dd=0
[ "$ud" -lt 0 ] && ud=0
down_deltas+=("$dd")
up_deltas+=("$ud")
fi
prev_down=$d
prev_up=$u
done
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} 📊 BANDWIDTH GRAPHS (Last 24h) ${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo ""
draw_ascii_graph "Download Traffic (hourly)" 35 "${down_deltas[@]}"
draw_ascii_graph "Upload Traffic (hourly)" 35 "${up_deltas[@]}"
# Summary
local total_down=0 total_up=0
for d in "${down_deltas[@]}"; do total_down=$((total_down + d)); done
for u in "${up_deltas[@]}"; do total_up=$((total_up + u)); done
echo -e " ${BOLD}24h Summary:${NC}"
echo -e " Download: ${GREEN}$(format_bytes $total_down)${NC}"
echo -e " Upload: ${GREEN}$(format_bytes $total_up)${NC}"
echo -e " Total: ${GREEN}$(format_bytes $((total_down + total_up)))${NC}"
echo ""
}
rotate_cumulative_data() {
local data_file="$INSTALL_DIR/relay_stats/cumulative_data"
local marker="$INSTALL_DIR/relay_stats/.last_rotation_month"
local current_month=$(date '+%Y-%m')
local last_month=""
[ -f "$marker" ] && last_month=$(cat "$marker" 2>/dev/null)
if [ -z "$last_month" ]; then
echo "$current_month" > "$marker"
return
fi
if [ "$current_month" != "$last_month" ] && [ -s "$data_file" ]; then
cp "$data_file" "${data_file}.${last_month}"
echo "$current_month" > "$marker"
local cutoff_ts=$(( $(date +%s) - 7776000 ))
for archive in "$INSTALL_DIR/relay_stats/cumulative_data."[0-9][0-9][0-9][0-9]-[0-9][0-9]; do
[ ! -f "$archive" ] && continue
local archive_mtime=$(stat -c %Y "$archive" 2>/dev/null || stat -f %m "$archive" 2>/dev/null || echo 0)
if [ "$archive_mtime" -gt 0 ] && [ "$archive_mtime" -lt "$cutoff_ts" ] 2>/dev/null; then
rm -f "$archive"
fi
done
fi
}
check_alerts() {
[ "$TELEGRAM_ALERTS_ENABLED" != "true" ] && return
local now=$(date +%s)
local cooldown=3600
# CPU + RAM check
local torware_containers=$(docker ps --format '{{.Names}}' 2>/dev/null | grep "^torware" 2>/dev/null || true)
local stats_line=""
if [ -n "$torware_containers" ]; then
stats_line=$(timeout 10 docker stats --no-stream --format "{{.CPUPerc}} {{.MemPerc}}" $torware_containers 2>/dev/null | head -1)
fi
local raw_cpu=$(echo "$stats_line" | awk '{print $1}')
local ram_pct=$(echo "$stats_line" | awk '{print $2}')
local cores=$(get_cpu_cores)
local cpu_val=$(awk "BEGIN {printf \"%.0f\", ${raw_cpu%\%} / $cores}" 2>/dev/null || echo 0)
if [ "${cpu_val:-0}" -gt 90 ] 2>/dev/null; then
cpu_breach=$((cpu_breach + 1))
else
cpu_breach=0
fi
if [ "$cpu_breach" -ge 3 ] && [ $((now - last_alert_cpu)) -ge $cooldown ] 2>/dev/null; then
telegram_send "⚠️ *Alert: High CPU*
CPU usage at ${cpu_val}% for 3+ minutes"
last_alert_cpu=$now
cpu_breach=0
fi
local ram_val=${ram_pct%\%}
ram_val=${ram_val%%.*}
if [ "${ram_val:-0}" -gt 90 ] 2>/dev/null; then
ram_breach=$((ram_breach + 1))
else
ram_breach=0
fi
if [ "$ram_breach" -ge 3 ] && [ $((now - last_alert_ram)) -ge $cooldown ] 2>/dev/null; then
telegram_send "⚠️ *Alert: High RAM*
Memory usage at ${ram_pct} for 3+ minutes"
last_alert_ram=$now
ram_breach=0
fi
# All containers down
local running=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -c "^torware" 2>/dev/null || true)
running=${running:-0}
if [ "$running" -eq 0 ] 2>/dev/null && [ $((now - last_alert_down)) -ge $cooldown ] 2>/dev/null; then
telegram_send "🔴 *Alert: All containers down*
No Torware containers are running!"
last_alert_down=$now
fi
# Zero connections for 2+ hours
local total_circuits=0
for i in $(seq 1 ${CONTAINER_COUNT:-1}); do
local cname=$(get_container_name $i)
local circ=$(timeout 5 docker exec "$cname" sh -c 'echo "GETINFO circuit-status" | nc 127.0.0.1 9051 2>/dev/null | grep -c BUILT' 2>/dev/null || echo 0)
total_circuits=$((total_circuits + ${circ:-0}))
done
if [ "$total_circuits" -eq 0 ] 2>/dev/null; then
if [ "$zero_peers_since" -eq 0 ] 2>/dev/null; then
zero_peers_since=$now
elif [ $((now - zero_peers_since)) -ge 7200 ] && [ $((now - last_alert_peers)) -ge $cooldown ] 2>/dev/null; then
telegram_send "⚠️ *Alert: Zero connections*
No active circuits for 2+ hours"
last_alert_peers=$now
zero_peers_since=$now
fi
else
zero_peers_since=0
fi
}
record_snapshot() {
local running=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -c "^torware" 2>/dev/null || true)
running=${running:-0}
local total_circuits=0
for i in $(seq 1 ${CONTAINER_COUNT:-1}); do
local cname=$(get_container_name $i)
local circ=$(timeout 5 docker exec "$cname" sh -c 'echo "GETINFO circuit-status" | nc 127.0.0.1 9051 2>/dev/null | grep -c BUILT' 2>/dev/null || echo 0)
total_circuits=$((total_circuits + ${circ:-0}))
done
local data_file="$INSTALL_DIR/relay_stats/cumulative_data"
local total_bw=0
[ -s "$data_file" ] && total_bw=$(awk -F'|' '{s+=$2+$3} END{print s+0}' "$data_file" 2>/dev/null)
echo "$(date +%s)|${total_circuits}|${total_bw:-0}|${running}" >> "$INSTALL_DIR/relay_stats/report_snapshots"
local snap_file="$INSTALL_DIR/relay_stats/report_snapshots"
local lines=$(wc -l < "$snap_file" 2>/dev/null || echo 0)
if [ "$lines" -gt 720 ] 2>/dev/null; then
tail -720 "$snap_file" > "${snap_file}.tmp" && mv "${snap_file}.tmp" "$snap_file"
fi
}
build_summary() {
local period_label="$1"
local period_secs="$2"
local snap_file="$INSTALL_DIR/relay_stats/report_snapshots"
[ ! -s "$snap_file" ] && return
local cutoff=$(( $(date +%s) - period_secs ))
local peak_circuits=0
local sum_circuits=0
local count=0
local first_bw=0
local last_bw=0
local got_first=false
while IFS='|' read -r ts circuits bw running; do
[ "$ts" -lt "$cutoff" ] 2>/dev/null && continue
count=$((count + 1))
sum_circuits=$((sum_circuits + ${circuits:-0}))
[ "${circuits:-0}" -gt "$peak_circuits" ] 2>/dev/null && peak_circuits=${circuits:-0}
if [ "$got_first" = false ]; then
first_bw=${bw:-0}
got_first=true
fi
last_bw=${bw:-0}
done < "$snap_file"
[ "$count" -eq 0 ] && return
local avg_circuits=$((sum_circuits / count))
local period_bw=$((${last_bw:-0} - ${first_bw:-0}))
[ "$period_bw" -lt 0 ] 2>/dev/null && period_bw=0
local bw_fmt=$(awk "BEGIN {b=$period_bw; if(b>1099511627776) printf \"%.2f TB\",b/1099511627776; else if(b>1073741824) printf \"%.2f GB\",b/1073741824; else printf \"%.1f MB\",b/1048576}" 2>/dev/null)
local uptime_pct=$(calc_uptime_pct "$period_secs")
local msg="📋 *${period_label} Summary*"
msg+=$'\n'"🕐 $(date '+%Y-%m-%d %H:%M %Z')"
msg+=$'\n'$'\n'"📊 Bandwidth served: ${bw_fmt}"
msg+=$'\n'"🔗 Peak circuits: ${peak_circuits} | Avg: ${avg_circuits}"
msg+=$'\n'"⏱ Uptime: ${uptime_pct}%"
msg+=$'\n'"📈 Data points: ${count}"
# New countries detection
local countries_file="$INSTALL_DIR/relay_stats/known_countries"
local data_file="$INSTALL_DIR/relay_stats/cumulative_data"
local new_countries=""
if [ -s "$data_file" ]; then
local current_countries=$(awk -F'|' '{if($1!="") print $1}' "$data_file" 2>/dev/null | sort -u)
if [ -f "$countries_file" ]; then
new_countries=$(comm -23 <(echo "$current_countries") <(sort "$countries_file") 2>/dev/null | head -5 | tr '\n' ', ' | sed 's/,$//')
fi
echo "$current_countries" > "$countries_file"
fi
if [ -n "$new_countries" ]; then
local safe_new=$(escape_md "$new_countries")
msg+=$'\n'"🆕 New countries: ${safe_new}"
fi
telegram_send "$msg"
}
format_bytes() {
local bytes=$1
[ -z "$bytes" ] || [ "$bytes" -eq 0 ] 2>/dev/null && echo "0 B" && return
if [ "$bytes" -ge 1073741824 ] 2>/dev/null; then
awk "BEGIN {printf \"%.2f GB\", $bytes/1073741824}"
elif [ "$bytes" -ge 1048576 ] 2>/dev/null; then
awk "BEGIN {printf \"%.2f MB\", $bytes/1048576}"
elif [ "$bytes" -ge 1024 ] 2>/dev/null; then
awk "BEGIN {printf \"%.1f KB\", $bytes/1024}"
else
echo "${bytes} B"
fi
}
build_report() {
local count=${CONTAINER_COUNT:-1}
local report="📊 *Torware Status Report*"
report+=$'\n'
report+="🕐 $(date '+%Y-%m-%d %H:%M %Z')"
report+=$'\n'
local running=0 total_read=0 total_written=0 total_circuits=0 total_conns=0
local earliest_start=""
for i in $(seq 1 $count); do
local cname=$(get_container_name $i)
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${cname}$"; then
running=$((running + 1))
local started=$(docker inspect --format='{{.State.StartedAt}}' "$cname" 2>/dev/null | cut -d'.' -f1)
if [ -n "$started" ]; then
local se=$(date -d "$started" +%s 2>/dev/null || echo 0)
if [ -z "$earliest_start" ] || [ "$se" -lt "$earliest_start" ] 2>/dev/null; then
earliest_start=$se
fi
fi
# Traffic via ControlPort
local cport=$((9051 + i - 1))
local cp_out=$(timeout 5 docker exec "$cname" sh -c "printf 'AUTHENTICATE\r\nGETINFO traffic/read\r\nGETINFO traffic/written\r\nQUIT\r\n' | nc 127.0.0.1 9051" 2>/dev/null)
local rb=$(echo "$cp_out" | sed -n 's/.*traffic\/read=\([0-9]*\).*/\1/p' | head -1); rb=${rb:-0}
local wb=$(echo "$cp_out" | sed -n 's/.*traffic\/written=\([0-9]*\).*/\1/p' | head -1); wb=${wb:-0}
total_read=$((total_read + rb))
total_written=$((total_written + wb))
# Circuits
local circ_out=$(timeout 5 docker exec "$cname" sh -c "printf 'AUTHENTICATE\r\nGETINFO circuit-status\r\nQUIT\r\n' | nc 127.0.0.1 9051" 2>/dev/null)
local circ=$(echo "$circ_out" | grep -cE '^[0-9]+ (BUILT|EXTENDED|LAUNCHED)' 2>/dev/null || echo 0)
circ=${circ//[^0-9]/}; circ=${circ:-0}
total_circuits=$((total_circuits + circ))
# Connections
local conn_out=$(timeout 5 docker exec "$cname" sh -c "printf 'AUTHENTICATE\r\nGETINFO orconn-status\r\nQUIT\r\n' | nc 127.0.0.1 9051" 2>/dev/null)
local conn=$(echo "$conn_out" | grep -c '\$' 2>/dev/null || echo 0)
conn=${conn//[^0-9]/}; conn=${conn:-0}
total_conns=$((total_conns + conn))
fi
done
# Include snowflake
local total_containers=$count
local total_running=$running
local sf_conns=0 sf_in=0 sf_out=0
if [ "${SNOWFLAKE_ENABLED:-false}" = "true" ]; then
total_containers=$((total_containers + ${SNOWFLAKE_COUNT:-1}))
for _sfi in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
local _sfn=$(get_snowflake_name $_sfi)
local _sfm=$(get_snowflake_metrics_port $_sfi)
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${_sfn}$"; then
total_running=$((total_running + 1))
local sf_metrics=$(curl -s --max-time 3 "http://127.0.0.1:${_sfm}/internal/metrics" 2>/dev/null)
[ -n "$sf_metrics" ] && sf_conns=$((sf_conns + $(echo "$sf_metrics" | awk '/^tor_snowflake_proxy_connections_total[{ ]/ { sum += $NF } END { printf "%.0f", sum }' 2>/dev/null || echo 0)))
local sf_log=$(docker logs "$_sfn" 2>&1 | grep "Traffic Relayed" 2>/dev/null)
if [ -n "$sf_log" ]; then
sf_in=$((sf_in + $(echo "$sf_log" | awk -F'[↓↑]' '{ split($2, a, " "); gsub(/[^0-9.]/, "", a[1]); sum += a[1] } END { printf "%.0f", sum * 1024 }' 2>/dev/null || echo 0)))
sf_out=$((sf_out + $(echo "$sf_log" | awk -F'[↓↑]' '{ split($3, a, " "); gsub(/[^0-9.]/, "", a[1]); sum += a[1] } END { printf "%.0f", sum * 1024 }' 2>/dev/null || echo 0)))
fi
fi
done
total_read=$((total_read + sf_in))
total_written=$((total_written + sf_out))
fi
# Include Unbounded in totals
local ub_rpt_conns=0
if [ "${UNBOUNDED_ENABLED:-false}" = "true" ]; then
total_containers=$((total_containers + 1))
if is_unbounded_running; then
total_running=$((total_running + 1))
local ub_rpt_s=$(get_unbounded_stats 2>/dev/null)
ub_rpt_conns=$(echo "$ub_rpt_s" | awk '{print $2}')
fi
fi
if [ -n "$earliest_start" ] && [ "$earliest_start" -gt 0 ] 2>/dev/null; then
local now=$(date +%s) up=$(($(date +%s) - earliest_start))
local days=$((up / 86400)) hours=$(( (up % 86400) / 3600 )) mins=$(( (up % 3600) / 60 ))
if [ "$days" -gt 0 ]; then
report+="⏱ Uptime: ${days}d ${hours}h ${mins}m"
else
report+="⏱ Uptime: ${hours}h ${mins}m"
fi
report+=$'\n'
fi
report+="📦 Containers: ${total_running}/${total_containers} running"
report+=$'\n'
report+="🔗 Circuits: ${total_circuits} | Connections: ${total_conns}"
report+=$'\n'
report+="📊 Traffic: ↓ $(format_bytes $total_read)$(format_bytes $total_written)"
report+=$'\n'
# Snowflake detail line
if [ "${SNOWFLAKE_ENABLED:-false}" = "true" ] && [ "$sf_conns" -gt 0 ] 2>/dev/null; then
local sf_total=$((sf_in + sf_out))
report+="❄ Snowflake: ${sf_conns} conns | $(format_bytes $sf_total) transferred"
report+=$'\n'
fi
# Unbounded detail line
if [ "${UNBOUNDED_ENABLED:-false}" = "true" ] && [ "${ub_rpt_conns:-0}" -gt 0 ] 2>/dev/null; then
report+="🌐 Unbounded: ${ub_rpt_conns} connections served"
report+=$'\n'
fi
# Uptime %
local uptime_pct=$(calc_uptime_pct 86400)
[ "${uptime_pct}" != "0" ] && report+="📈 Availability: ${uptime_pct}% (24h)"$'\n'
# CPU / RAM
local names=""
for i in $(seq 1 ${CONTAINER_COUNT:-1}); do
names+=" $(get_container_name $i)"
done
if [ "${SNOWFLAKE_ENABLED:-false}" = "true" ]; then
for _sfi in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
local _sfn=$(get_snowflake_name $_sfi)
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${_sfn}$" && names+=" $_sfn"
done
fi
local stats=$(timeout 10 docker stats --no-stream --format "{{.CPUPerc}} {{.MemUsage}}" $names 2>/dev/null | head -1)
if [ -n "$stats" ]; then
local raw_cpu=$(echo "$stats" | awk '{print $1}')
local cores=$(get_cpu_cores)
local cpu=$(awk "BEGIN {printf \"%.1f%%\", ${raw_cpu%\%} / $cores}" 2>/dev/null || echo "$raw_cpu")
local sys_ram=""
if command -v free &>/dev/null; then
local free_out=$(free -m 2>/dev/null)
local ram_used=$(echo "$free_out" | awk '/^Mem:/{if ($3>=1024) printf "%.1fGiB",$3/1024; else printf "%dMiB",$3}')
local ram_total=$(echo "$free_out" | awk '/^Mem:/{if ($2>=1024) printf "%.1fGiB",$2/1024; else printf "%dMiB",$2}')
sys_ram="${ram_used} / ${ram_total}"
fi
if [ -n "$sys_ram" ]; then
report+="🖥 CPU: ${cpu} | RAM: ${sys_ram}"
else
local ram=$(echo "$stats" | awk '{print $2, $3, $4}')
report+="🖥 CPU: ${cpu} | RAM: ${ram}"
fi
report+=$'\n'
fi
printf '%s' "$report"
}
process_commands() {
local offset_file="$INSTALL_DIR/relay_stats/tg_offset"
local offset=0
[ -f "$offset_file" ] && offset=$(cat "$offset_file" 2>/dev/null)
offset=${offset:-0}
[ "$offset" -eq "$offset" ] 2>/dev/null || offset=0
local _cfg
_cfg=$(mktemp "${TMPDIR:-/tmp}/.tg_curl.XXXXXX") || return
chmod 600 "$_cfg"
printf 'url = "https://api.telegram.org/bot%s/getUpdates?offset=%s&timeout=0"\n' "$TELEGRAM_BOT_TOKEN" "$((offset + 1))" > "$_cfg"
local response
response=$(curl -s --max-time 10 --max-filesize 1048576 -K "$_cfg" 2>/dev/null)
rm -f "$_cfg"
[ -z "$response" ] && return
if ! command -v python3 &>/dev/null; then
return
fi
local parsed
parsed=$(python3 -c "
import json, sys
try:
data = json.loads(sys.argv[1])
if not data.get('ok'): sys.exit(0)
results = data.get('result', [])
if not results: sys.exit(0)
for r in results:
uid = r.get('update_id', 0)
msg = r.get('message', {})
chat_id = msg.get('chat', {}).get('id', 0)
text = msg.get('text', '')
if str(chat_id) == '$TELEGRAM_CHAT_ID' and text.startswith('/tor_'):
print(f'{uid}|{text}')
else:
print(f'{uid}|')
except Exception:
try:
data = json.loads(sys.argv[1])
results = data.get('result', [])
if results:
max_uid = max(r.get('update_id', 0) for r in results)
if max_uid > 0:
print(f'{max_uid}|')
except Exception:
pass
" "$response" 2>/dev/null)
[ -z "$parsed" ] && return
local max_id=$offset
while IFS='|' read -r uid cmd; do
[ -z "$uid" ] && continue
[ "$uid" -gt "$max_id" ] 2>/dev/null && max_id=$uid
case "$cmd" in
/tor_status|/tor_status@*)
local report=$(build_report)
telegram_send "$report"
;;
/tor_peers|/tor_peers@*)
local total_circuits=0
for i in $(seq 1 ${CONTAINER_COUNT:-1}); do
local cname=$(get_container_name $i)
local circ=$(timeout 5 docker exec "$cname" sh -c 'echo "GETINFO circuit-status" | nc 127.0.0.1 9051 2>/dev/null | grep -c BUILT' 2>/dev/null || echo 0)
total_circuits=$((total_circuits + ${circ:-0}))
done
telegram_send "🔗 Active circuits: ${total_circuits}"
;;
/tor_uptime|/tor_uptime@*)
local ut_msg="⏱ *Uptime Report*"
ut_msg+="\n"
for i in $(seq 1 ${CONTAINER_COUNT:-1}); do
local cname=$(get_container_name $i)
local is_running=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -c "^${cname}$" || true)
if [ "${is_running:-0}" -gt 0 ]; then
local started=$(docker inspect --format='{{.State.StartedAt}}' "$cname" 2>/dev/null)
if [ -n "$started" ]; then
local se=$(date -d "$started" +%s 2>/dev/null || echo 0)
local diff=$(( $(date +%s) - se ))
local d=$((diff / 86400)) h=$(( (diff % 86400) / 3600 )) m=$(( (diff % 3600) / 60 ))
ut_msg+="\n📦 Container ${i}: ${d}d ${h}h ${m}m"
else
ut_msg+="\n📦 Container ${i}: ⚠ unknown"
fi
else
ut_msg+="\n📦 Container ${i}: 🔴 stopped"
fi
done
local avail=$(calc_uptime_pct 86400)
ut_msg+="\n\n📈 Availability: ${avail}% (24h)"
telegram_send "$ut_msg"
;;
/tor_containers|/tor_containers@*)
local ct_msg="📦 *Container Status*"
ct_msg+="\n"
local docker_names=$(docker ps --format '{{.Names}}' 2>/dev/null)
for i in $(seq 1 ${CONTAINER_COUNT:-1}); do
local cname=$(get_container_name $i)
ct_msg+="\n"
if echo "$docker_names" | grep -q "^${cname}$"; then
local safe_cname=$(escape_md "$cname")
ct_msg+="C${i} (${safe_cname}): 🟢 Running"
local circ=$(timeout 5 docker exec "$cname" sh -c 'echo "GETINFO circuit-status" | nc 127.0.0.1 9051 2>/dev/null | grep -c BUILT' 2>/dev/null || echo 0)
ct_msg+="\n 🔗 Circuits: ${circ:-0}"
else
local safe_cname=$(escape_md "$cname")
ct_msg+="C${i} (${safe_cname}): 🔴 Stopped"
fi
ct_msg+="\n"
done
ct_msg+="\n/tor\_restart\_N /tor\_stop\_N /tor\_start\_N — manage containers"
telegram_send "$ct_msg"
;;
/tor_restart_*|/tor_stop_*|/tor_start_*)
local action="${cmd%%_*}" # /tor_restart, /tor_stop, or /tor_start
# Remove the /tor_ prefix to get restart, stop, start
action="${action#/tor_}"
local num="${cmd##*_}"
num="${num%%@*}" # strip @botname suffix
if ! [[ "$num" =~ ^[0-9]+$ ]] || [ "$num" -lt 1 ] || [ "$num" -gt "${CONTAINER_COUNT:-1}" ]; then
telegram_send "❌ Invalid container number: ${num}. Use 1-${CONTAINER_COUNT:-1}."
else
local cname=$(get_container_name "$num")
if docker "$action" "$cname" >/dev/null 2>&1; then
local emoji="✅"
[ "$action" = "stop" ] && emoji="🛑"
[ "$action" = "start" ] && emoji="🟢"
local safe_cname=$(escape_md "$cname")
telegram_send "${emoji} Container ${num} (${safe_cname}): ${action} successful"
else
local safe_cname=$(escape_md "$cname")
telegram_send "❌ Failed to ${action} container ${num} (${safe_cname})"
fi
fi
;;
/tor_snowflake|/tor_snowflake@*)
if [ "${SNOWFLAKE_ENABLED:-false}" != "true" ]; then
telegram_send "❄ Snowflake proxy is not enabled."
elif ! is_snowflake_running; then
telegram_send "❄ *Snowflake Proxy*
🔴 Status: Stopped"
else
local _sf_agg=$(get_snowflake_stats 2>/dev/null)
local sf_total_conns=$(echo "$_sf_agg" | awk '{print $1}')
local sf_in=$(echo "$_sf_agg" | awk '{print $2}')
local sf_out=$(echo "$_sf_agg" | awk '{print $3}')
sf_total_conns=${sf_total_conns:-0}; sf_in=${sf_in:-0}; sf_out=${sf_out:-0}
local sf_total=$((sf_in + sf_out))
local sf_started=$(docker inspect --format='{{.State.StartedAt}}' "$(get_snowflake_name 1)" 2>/dev/null | cut -d'.' -f1)
local sf_uptime="unknown"
if [ -n "$sf_started" ]; then
local sf_se=$(date -d "$sf_started" +%s 2>/dev/null || echo 0)
if [ "$sf_se" -gt 0 ] 2>/dev/null; then
local sf_diff=$(( $(date +%s) - sf_se ))
local sf_d=$((sf_diff / 86400)) sf_h=$(( (sf_diff % 86400) / 3600 )) sf_m=$(( (sf_diff % 3600) / 60 ))
[ "$sf_d" -gt 0 ] && sf_uptime="${sf_d}d ${sf_h}h ${sf_m}m" || sf_uptime="${sf_h}h ${sf_m}m"
fi
fi
# Top countries
local sf_countries=""
sf_countries=$(get_snowflake_country_stats 2>/dev/null | head -5)
local sf_msg="❄ *Snowflake Proxy* (${SNOWFLAKE_COUNT:-1} instance(s))
🟢 Status: Running
⏱ Uptime: ${sf_uptime}
👥 Total connections: ${sf_total_conns}
📊 Traffic: ↓ $(format_bytes $sf_in)$(format_bytes $sf_out)
💾 Total transferred: $(format_bytes $sf_total)"
if [ -n "$sf_countries" ]; then
sf_msg+=$'\n'"🗺 Top countries:"
while IFS='|' read -r cnt cc; do
[ -z "$cc" ] && continue
sf_msg+=$'\n'" ${cc}: ${cnt}"
done <<< "$sf_countries"
fi
telegram_send "$sf_msg"
fi
;;
/tor_unbounded|/tor_unbounded@*)
if [ "${UNBOUNDED_ENABLED:-false}" != "true" ]; then
telegram_send "🌐 Unbounded proxy is not enabled."
elif ! is_unbounded_running; then
telegram_send "🌐 *Unbounded Proxy (Lantern)*
🔴 Status: Stopped"
else
local _ub_agg=$(get_unbounded_stats 2>/dev/null)
local ub_live_conns=$(echo "$_ub_agg" | awk '{print $1}')
local ub_total_conns=$(echo "$_ub_agg" | awk '{print $2}')
ub_live_conns=${ub_live_conns:-0}; ub_total_conns=${ub_total_conns:-0}
local ub_started=$(docker inspect --format='{{.State.StartedAt}}' "$UNBOUNDED_CONTAINER" 2>/dev/null | cut -d'.' -f1)
local ub_uptime="unknown"
if [ -n "$ub_started" ]; then
local ub_se=$(date -d "$ub_started" +%s 2>/dev/null || echo 0)
if [ "$ub_se" -gt 0 ] 2>/dev/null; then
local ub_diff=$(( $(date +%s) - ub_se ))
local ub_d=$((ub_diff / 86400)) ub_h=$(( (ub_diff % 86400) / 3600 )) ub_m=$(( (ub_diff % 3600) / 60 ))
[ "$ub_d" -gt 0 ] && ub_uptime="${ub_d}d ${ub_h}h ${ub_m}m" || ub_uptime="${ub_h}h ${ub_m}m"
fi
fi
local ub_msg="🌐 *Unbounded Proxy (Lantern)*
🟢 Status: Running
⏱ Uptime: ${ub_uptime}
👥 Live connections: ${ub_live_conns}
📊 All-time connections: ${ub_total_conns}"
telegram_send "$ub_msg"
fi
;;
/tor_mtproxy|/tor_mtproxy@*)
if [ "${MTPROXY_ENABLED:-false}" != "true" ]; then
telegram_send "📱 MTProxy is not enabled."
elif ! is_mtproxy_running; then
telegram_send "📱 *MTProxy (Telegram)*
🔴 Status: Stopped"
else
local _mtp_agg=$(get_mtproxy_stats 2>/dev/null)
local mtp_in=$(echo "$_mtp_agg" | awk '{print $1}')
local mtp_out=$(echo "$_mtp_agg" | awk '{print $2}')
mtp_in=${mtp_in:-0}; mtp_out=${mtp_out:-0}
local mtp_started=$(docker inspect --format='{{.State.StartedAt}}' "$MTPROXY_CONTAINER" 2>/dev/null | cut -d'.' -f1)
local mtp_uptime="unknown"
if [ -n "$mtp_started" ]; then
local mtp_se=$(date -d "$mtp_started" +%s 2>/dev/null || echo 0)
if [ "$mtp_se" -gt 0 ] 2>/dev/null; then
local mtp_diff=$(( $(date +%s) - mtp_se ))
local mtp_d=$((mtp_diff / 86400)) mtp_h=$(( (mtp_diff % 86400) / 3600 )) mtp_m=$(( (mtp_diff % 3600) / 60 ))
[ "$mtp_d" -gt 0 ] && mtp_uptime="${mtp_d}d ${mtp_h}h ${mtp_m}m" || mtp_uptime="${mtp_h}h ${mtp_m}m"
fi
fi
local mtp_msg="📱 *MTProxy (Telegram)*
🟢 Status: Running
⏱ Uptime: ${mtp_uptime}
📊 Traffic: ↓ $(format_bytes $mtp_in)$(format_bytes $mtp_out)
🔗 Port: ${MTPROXY_PORT} | Domain: ${MTPROXY_DOMAIN}
_Use /tor\_mtproxy\_qr to get the proxy link and QR code._"
telegram_send "$mtp_msg"
fi
;;
/tor_mtproxy_qr|/tor_mtproxy_qr@*)
if [ "${MTPROXY_ENABLED:-false}" != "true" ]; then
telegram_send "📱 MTProxy is not enabled."
elif ! is_mtproxy_running; then
telegram_send "📱 MTProxy is not running. Start it first."
else
# Send the link and QR code
telegram_notify_mtproxy_started
fi
;;
/tor_help|/tor_help@*)
telegram_send "📖 *Available Commands*
/tor\_status — Full status report
/tor\_peers — Current circuit count
/tor\_uptime — Per-container uptime + 24h availability
/tor\_containers — Per-container status
/tor\_snowflake — Snowflake proxy details
/tor\_unbounded — Unbounded (Lantern) proxy details
/tor\_mtproxy — MTProxy (Telegram) proxy details
/tor\_mtproxy\_qr — Get MTProxy link and QR code
/tor\_restart\_N — Restart container N
/tor\_stop\_N — Stop container N
/tor\_start\_N — Start container N
/tor\_help — Show this help"
;;
esac
done <<< "$parsed"
[ "$max_id" -gt "$offset" ] 2>/dev/null && echo "$max_id" > "$offset_file"
}
# State variables
cpu_breach=0
ram_breach=0
zero_peers_since=0
last_alert_cpu=0
last_alert_ram=0
last_alert_down=0
last_alert_peers=0
last_rotation_ts=0
# Ensure data directory exists
mkdir -p "$INSTALL_DIR/relay_stats"
# Persist daily/weekly timestamps across restarts
_ts_dir="$INSTALL_DIR/relay_stats"
last_daily_ts=$(cat "$_ts_dir/.last_daily_ts" 2>/dev/null || echo 0)
[ "$last_daily_ts" -eq "$last_daily_ts" ] 2>/dev/null || last_daily_ts=0
last_weekly_ts=$(cat "$_ts_dir/.last_weekly_ts" 2>/dev/null || echo 0)
[ "$last_weekly_ts" -eq "$last_weekly_ts" ] 2>/dev/null || last_weekly_ts=0
last_report_ts=$(cat "$_ts_dir/.last_report_ts" 2>/dev/null || echo 0)
[ "$last_report_ts" -eq "$last_report_ts" ] 2>/dev/null || last_report_ts=0
while true; do
sleep 60
# Re-read settings (with validation)
if [ -f "$INSTALL_DIR/settings.conf" ]; then
if ! grep -vE '^\s*$|^\s*#|^[A-Za-z_][A-Za-z0-9_]*='\''[^'\'']*'\''$|^[A-Za-z_][A-Za-z0-9_]*=[0-9]+$|^[A-Za-z_][A-Za-z0-9_]*=(true|false)$' "$INSTALL_DIR/settings.conf" 2>/dev/null | grep -q .; then
source "$INSTALL_DIR/settings.conf"
fi
fi
# Exit if disabled
[ "$TELEGRAM_ENABLED" != "true" ] && exit 0
[ -z "$TELEGRAM_BOT_TOKEN" ] && exit 0
# Core per-minute tasks
process_commands
track_uptime
check_alerts
# Daily rotation check
now_ts=$(date +%s)
if [ $((now_ts - last_rotation_ts)) -ge 86400 ] 2>/dev/null; then
rotate_cumulative_data
last_rotation_ts=$now_ts
fi
# Daily summary
if [ "${TELEGRAM_DAILY_SUMMARY:-true}" = "true" ] && [ $((now_ts - last_daily_ts)) -ge 86400 ] 2>/dev/null; then
build_summary "Daily" 86400
last_daily_ts=$now_ts
echo "$now_ts" > "$_ts_dir/.last_daily_ts"
fi
# Weekly summary
if [ "${TELEGRAM_WEEKLY_SUMMARY:-true}" = "true" ] && [ $((now_ts - last_weekly_ts)) -ge 604800 ] 2>/dev/null; then
build_summary "Weekly" 604800
last_weekly_ts=$now_ts
echo "$now_ts" > "$_ts_dir/.last_weekly_ts"
fi
# Regular periodic report (wall-clock aligned to start hour)
# Skip if all notification types are disabled (bot-only mode)
if [ "${TELEGRAM_ALERTS_ENABLED:-true}" = "true" ] || [ "${TELEGRAM_DAILY_SUMMARY:-true}" = "true" ] || [ "${TELEGRAM_WEEKLY_SUMMARY:-true}" = "true" ]; then
interval_hours=${TELEGRAM_INTERVAL:-6}
start_hour=${TELEGRAM_START_HOUR:-0}
interval_secs=$((interval_hours * 3600))
current_hour=$(date +%-H)
hour_diff=$(( (current_hour - start_hour + 24) % 24 ))
if [ "$interval_hours" -gt 0 ] && [ $((hour_diff % interval_hours)) -eq 0 ] 2>/dev/null; then
if [ $((now_ts - last_report_ts)) -ge $((interval_secs - 120)) ] 2>/dev/null; then
report=$(build_report)
telegram_send "$report"
record_snapshot
last_report_ts=$now_ts
echo "$now_ts" > "$_ts_dir/.last_report_ts"
fi
fi
fi
done
TGEOF
local escaped_dir=$(printf '%s\n' "$INSTALL_DIR" | sed 's/[&\\]/\\&/g')
sed "s#REPLACE_INSTALL_DIR#${escaped_dir}#g" "$INSTALL_DIR/torware-telegram.sh" > "$INSTALL_DIR/torware-telegram.sh.tmp" && mv "$INSTALL_DIR/torware-telegram.sh.tmp" "$INSTALL_DIR/torware-telegram.sh"
chmod 700 "$INSTALL_DIR/torware-telegram.sh"
}
setup_telegram_service() {
telegram_generate_notify_script
if command -v systemctl &>/dev/null; then
cat > /etc/systemd/system/torware-telegram.service << EOF
[Unit]
Description=Torware Telegram Notifications
After=network.target docker.service
Requires=docker.service
[Service]
Type=simple
ExecStart=/bin/bash $INSTALL_DIR/torware-telegram.sh
Restart=on-failure
RestartSec=30
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload 2>/dev/null || true
systemctl enable torware-telegram.service 2>/dev/null || true
systemctl restart torware-telegram.service 2>/dev/null || true
fi
}
telegram_stop_notify() {
if command -v systemctl &>/dev/null && [ -f /etc/systemd/system/torware-telegram.service ]; then
systemctl stop torware-telegram.service 2>/dev/null || true
fi
# Clean up legacy PID-based loop if present
if [ -f "$INSTALL_DIR/telegram_notify.pid" ]; then
local pid=$(cat "$INSTALL_DIR/telegram_notify.pid" 2>/dev/null)
if echo "$pid" | grep -qE '^[0-9]+$' && kill -0 "$pid" 2>/dev/null; then
kill -- -"$pid" 2>/dev/null || kill "$pid" 2>/dev/null || true
fi
rm -f "$INSTALL_DIR/telegram_notify.pid"
fi
pkill -f "torware-telegram.sh" 2>/dev/null || true
}
telegram_start_notify() {
telegram_stop_notify
if [ "$TELEGRAM_ENABLED" = "true" ] && [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then
setup_telegram_service
fi
}
telegram_disable_service() {
if command -v systemctl &>/dev/null && [ -f /etc/systemd/system/torware-telegram.service ]; then
systemctl stop torware-telegram.service 2>/dev/null || true
systemctl disable torware-telegram.service 2>/dev/null || true
fi
pkill -f "torware-telegram.sh" 2>/dev/null || true
}
show_telegram_menu() {
while true; do
# Reload settings from disk to reflect any changes
load_settings
clear
print_header
if [ "$TELEGRAM_ENABLED" = "true" ] && [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then
# Already configured — show management menu
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
echo -e "${CYAN} TELEGRAM NOTIFICATIONS${NC}"
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
echo ""
local _sh="${TELEGRAM_START_HOUR:-0}"
local alerts_st="${GREEN}ON${NC}"
[ "${TELEGRAM_ALERTS_ENABLED:-true}" != "true" ] && alerts_st="${RED}OFF${NC}"
local daily_st="${GREEN}ON${NC}"
[ "${TELEGRAM_DAILY_SUMMARY:-true}" != "true" ] && daily_st="${RED}OFF${NC}"
local weekly_st="${GREEN}ON${NC}"
[ "${TELEGRAM_WEEKLY_SUMMARY:-true}" != "true" ] && weekly_st="${RED}OFF${NC}"
# Check if any notification type is active
local _any_notif=false
if [ "${TELEGRAM_ALERTS_ENABLED:-true}" = "true" ] || [ "${TELEGRAM_DAILY_SUMMARY:-true}" = "true" ] || [ "${TELEGRAM_WEEKLY_SUMMARY:-true}" = "true" ]; then
_any_notif=true
fi
if [ "$_any_notif" = "true" ]; then
echo -e " Status: ${GREEN}✓ Enabled${NC} (every ${TELEGRAM_INTERVAL}h starting at ${_sh}:00)"
else
echo -e " Status: ${GREEN}✓ Connected${NC} ${DIM}(bot only — no auto-notifications)${NC}"
fi
echo ""
echo -e " 1. 📩 Send test message"
if [ "$_any_notif" = "true" ]; then
echo -e " 2. ⏱ Change interval"
else
echo -e " 2. 📬 Enable notifications"
fi
echo -e " 3. ❌ Disconnect bot"
echo -e " 4. 🔄 Reconfigure (new bot/chat)"
echo -e " 5. 🚨 Alerts (CPU/RAM/down): ${alerts_st}"
echo -e " 6. 📋 Daily summary: ${daily_st}"
echo -e " 7. 📊 Weekly summary: ${weekly_st}"
local cur_label="${TELEGRAM_SERVER_LABEL:-$(hostname 2>/dev/null || echo 'unknown')}"
echo -e " 8. 🏷 Server label: ${CYAN}${cur_label}${NC}"
if [ "$MTPROXY_ENABLED" = "true" ]; then
echo -e " 9. 📱 Send MTProxy link & QR"
fi
echo -e " 0. ← Back"
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
echo ""
read -p " Enter choice: " tchoice < /dev/tty || return
case "$tchoice" in
1)
echo ""
echo -ne " Sending test message... "
if telegram_test_message; then
echo -e "${GREEN}✓ Sent!${NC}"
else
echo -e "${RED}✗ Failed. Check your token/chat ID.${NC}"
fi
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
;;
2)
echo ""
if [ "$_any_notif" != "true" ]; then
# Bot-only mode — enable notifications first
TELEGRAM_ALERTS_ENABLED=true
TELEGRAM_DAILY_SUMMARY=true
TELEGRAM_WEEKLY_SUMMARY=true
fi
echo -e " Select notification interval:"
echo -e " 1. Every 1 hour"
echo -e " 2. Every 3 hours"
echo -e " 3. Every 6 hours (recommended)"
echo -e " 4. Every 12 hours"
echo -e " 5. Every 24 hours"
echo ""
read -p " Choice [1-5]: " ichoice < /dev/tty || true
case "$ichoice" in
1) TELEGRAM_INTERVAL=1 ;;
2) TELEGRAM_INTERVAL=3 ;;
3) TELEGRAM_INTERVAL=6 ;;
4) TELEGRAM_INTERVAL=12 ;;
5) TELEGRAM_INTERVAL=24 ;;
*) echo -e " ${RED}Invalid choice${NC}"; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; continue ;;
esac
echo ""
echo -e " What hour should reports start? (0-23, e.g. 8 = 8:00 AM)"
echo -e " Reports will repeat every ${TELEGRAM_INTERVAL}h from this hour."
read -p " Start hour [0-23] (default ${TELEGRAM_START_HOUR:-0}): " shchoice < /dev/tty || true
if [ -n "$shchoice" ] && [ "$shchoice" -ge 0 ] 2>/dev/null && [ "$shchoice" -le 23 ] 2>/dev/null; then
TELEGRAM_START_HOUR=$shchoice
fi
save_settings
telegram_start_notify
echo -e " ${GREEN}✓ Reports every ${TELEGRAM_INTERVAL}h starting at ${TELEGRAM_START_HOUR:-0}:00${NC}"
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
;;
3)
TELEGRAM_ENABLED=false
save_settings
telegram_disable_service
echo -e " ${GREEN}✓ Telegram notifications disabled${NC}"
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
;;
4)
telegram_setup_wizard
;;
5)
if [ "${TELEGRAM_ALERTS_ENABLED:-true}" = "true" ]; then
TELEGRAM_ALERTS_ENABLED=false
echo -e " ${RED}✗ Alerts disabled${NC}"
else
TELEGRAM_ALERTS_ENABLED=true
echo -e " ${GREEN}✓ Alerts enabled${NC}"
fi
save_settings
if [ "$TELEGRAM_ALERTS_ENABLED" = "true" ] || [ "${TELEGRAM_DAILY_SUMMARY:-true}" = "true" ] || [ "${TELEGRAM_WEEKLY_SUMMARY:-true}" = "true" ]; then
telegram_start_notify
else
telegram_disable_service
fi
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
;;
6)
if [ "${TELEGRAM_DAILY_SUMMARY:-true}" = "true" ]; then
TELEGRAM_DAILY_SUMMARY=false
echo -e " ${RED}✗ Daily summary disabled${NC}"
else
TELEGRAM_DAILY_SUMMARY=true
echo -e " ${GREEN}✓ Daily summary enabled${NC}"
fi
save_settings
if [ "${TELEGRAM_ALERTS_ENABLED:-true}" = "true" ] || [ "$TELEGRAM_DAILY_SUMMARY" = "true" ] || [ "${TELEGRAM_WEEKLY_SUMMARY:-true}" = "true" ]; then
telegram_start_notify
else
telegram_disable_service
fi
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
;;
7)
if [ "${TELEGRAM_WEEKLY_SUMMARY:-true}" = "true" ]; then
TELEGRAM_WEEKLY_SUMMARY=false
echo -e " ${RED}✗ Weekly summary disabled${NC}"
else
TELEGRAM_WEEKLY_SUMMARY=true
echo -e " ${GREEN}✓ Weekly summary enabled${NC}"
fi
save_settings
if [ "${TELEGRAM_ALERTS_ENABLED:-true}" = "true" ] || [ "${TELEGRAM_DAILY_SUMMARY:-true}" = "true" ] || [ "$TELEGRAM_WEEKLY_SUMMARY" = "true" ]; then
telegram_start_notify
else
telegram_disable_service
fi
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
;;
8)
echo ""
local cur_label="${TELEGRAM_SERVER_LABEL:-$(hostname 2>/dev/null || echo 'unknown')}"
echo -e " Current label: ${CYAN}${cur_label}${NC}"
echo -e " This label appears in all Telegram messages to identify the server."
echo -e " Leave blank to use hostname ($(hostname 2>/dev/null || echo 'unknown'))"
echo ""
read -p " New label: " new_label < /dev/tty || true
TELEGRAM_SERVER_LABEL="${new_label}"
save_settings
if [ "${TELEGRAM_ALERTS_ENABLED:-true}" = "true" ] || [ "${TELEGRAM_DAILY_SUMMARY:-true}" = "true" ] || [ "${TELEGRAM_WEEKLY_SUMMARY:-true}" = "true" ]; then
telegram_start_notify
fi
local display_label="${TELEGRAM_SERVER_LABEL:-$(hostname 2>/dev/null || echo 'unknown')}"
echo -e " ${GREEN}✓ Server label set to: ${display_label}${NC}"
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
;;
9)
echo ""
if [ "$MTPROXY_ENABLED" != "true" ]; then
log_warn "MTProxy is not enabled. Enable it first from the main menu."
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
elif ! is_mtproxy_running; then
log_warn "MTProxy is not running. Start it first with 'torware start'."
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
else
echo -ne " Sending MTProxy link & QR code... "
if telegram_notify_mtproxy_started; then
echo -e "${GREEN}✓ Sent!${NC}"
else
echo -e "${RED}✗ Failed${NC}"
fi
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
fi
;;
0) return ;;
esac
elif [ -n "$TELEGRAM_BOT_TOKEN" ] && [ -n "$TELEGRAM_CHAT_ID" ]; then
# Disabled but credentials exist — offer re-enable
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
echo -e "${CYAN} TELEGRAM NOTIFICATIONS${NC}"
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
echo ""
echo -e " Status: ${RED}✗ Disabled${NC} (credentials saved)"
echo ""
echo -e " 1. ✅ Re-enable notifications (every ${TELEGRAM_INTERVAL:-6}h)"
echo -e " 2. 🔄 Reconfigure (new bot/chat)"
echo -e " 0. ← Back"
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
echo ""
read -p " Enter choice: " tchoice < /dev/tty || return
case "$tchoice" in
1)
TELEGRAM_ENABLED=true
save_settings
telegram_start_notify
echo -e " ${GREEN}✓ Telegram notifications re-enabled${NC}"
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
;;
2)
telegram_setup_wizard
;;
0) return ;;
esac
else
# Not configured — run wizard
telegram_setup_wizard
return
fi
done
}
#═══════════════════════════════════════════════════════════════════════
# Fingerprint & Bridge Line Display
#═══════════════════════════════════════════════════════════════════════
show_fingerprint() {
load_settings
local count=${CONTAINER_COUNT:-1}
echo ""
echo -e "${CYAN} Relay Fingerprints:${NC}"
echo ""
for i in $(seq 1 $count); do
local fp=$(get_tor_fingerprint $i)
local cname=$(get_container_name $i)
local c_rtype=$(get_container_relay_type $i)
if [ -n "$fp" ]; then
echo -e " ${BOLD}$cname${NC} [${c_rtype}]: $fp"
else
echo -e " ${BOLD}$cname${NC} [${c_rtype}]: ${DIM}(not yet generated)${NC}"
fi
done
echo ""
}
show_bridge_line() {
load_settings
# Check if any container is a bridge
local count=${CONTAINER_COUNT:-1}
local any_bridge=false
for i in $(seq 1 $count); do
[ "$(get_container_relay_type $i)" = "bridge" ] && any_bridge=true
done
if [ "$any_bridge" = "false" ]; then
log_warn "Bridge lines are only available for bridge relays. No bridge containers configured."
return 1
fi
echo ""
echo -e "${CYAN} Bridge Lines (share these with users who need to bypass censorship):${NC}"
echo ""
for i in $(seq 1 $count); do
# Skip non-bridge containers
[ "$(get_container_relay_type $i)" != "bridge" ] && continue
local bl=$(get_bridge_line $i)
local cname=$(get_container_name $i)
if [ -n "$bl" ]; then
echo -e " ${BOLD}$cname:${NC}"
echo -e " ${GREEN}$bl${NC}"
echo ""
# QR code if available
if command -v qrencode &>/dev/null; then
echo -e " ${DIM}QR Code:${NC}"
echo "$bl" | qrencode -t ANSIUTF8 2>/dev/null | sed 's/^/ /'
echo ""
fi
else
echo -e " ${BOLD}$cname:${NC} ${DIM}(not yet available - relay may still be bootstrapping)${NC}"
fi
done
}
#═══════════════════════════════════════════════════════════════════════
# Uninstall
#═══════════════════════════════════════════════════════════════════════
uninstall() {
echo ""
echo -e "${RED}╔═════════════════════════════════════════════════════════════╗${NC}"
echo -e "${RED}║ UNINSTALL TORWARE ║${NC}"
echo -e "${RED}╠═════════════════════════════════════════════════════════════╣${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ This will remove: ║${NC}"
echo -e "${RED}║ • All containers (Tor, Snowflake, Unbounded, MTProxy) ║${NC}"
echo -e "${RED}║ • All Docker volumes (relay data & keys) ║${NC}"
echo -e "${RED}║ • Systemd/OpenRC services ║${NC}"
echo -e "${RED}║ • Configuration files in /opt/torware ║${NC}"
echo -e "${RED}║ • Management CLI (/usr/local/bin/torware) ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}║ Docker itself will NOT be removed. ║${NC}"
echo -e "${RED}║ ║${NC}"
echo -e "${RED}╚═════════════════════════════════════════════════════════════╝${NC}"
echo ""
read -p " Type 'yes' to confirm uninstall: " confirm < /dev/tty || true
if [ "$confirm" != "yes" ]; then
log_info "Uninstall cancelled."
return 0
fi
# Offer to keep backups
local keep_backups=false
if [ -d "$BACKUP_DIR" ] && ls "$BACKUP_DIR"/tor_keys_*.tar.gz &>/dev/null; then
echo ""
read -p " Keep backup files? [Y/n] " keep_choice < /dev/tty || true
if [[ ! "$keep_choice" =~ ^[Nn]$ ]]; then
keep_backups=true
fi
fi
echo ""
log_info "Uninstalling Torware..."
# Detect systemd if not already set (cli_main skips detect_os)
if [ -z "$HAS_SYSTEMD" ]; then
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
HAS_SYSTEMD=true
else
HAS_SYSTEMD=false
fi
fi
# Stop services
if [ "$HAS_SYSTEMD" = "true" ]; then
systemctl stop torware 2>/dev/null || true
systemctl disable torware 2>/dev/null || true
rm -f /etc/systemd/system/torware.service
systemctl stop torware-tracker 2>/dev/null || true
systemctl disable torware-tracker 2>/dev/null || true
rm -f /etc/systemd/system/torware-tracker.service
systemctl stop torware-telegram 2>/dev/null || true
systemctl disable torware-telegram 2>/dev/null || true
rm -f /etc/systemd/system/torware-telegram.service
systemctl daemon-reload 2>/dev/null || true
elif command -v rc-update &>/dev/null; then
rc-service torware stop 2>/dev/null || true
rc-update del torware 2>/dev/null || true
rm -f /etc/init.d/torware
elif [ -f /etc/init.d/torware ]; then
service torware stop 2>/dev/null || true
if command -v update-rc.d &>/dev/null; then
update-rc.d torware remove 2>/dev/null || true
fi
rm -f /etc/init.d/torware
fi
# Kill any lingering service processes
pkill -f "torware-tracker.sh" 2>/dev/null || true
pkill -f "torware-telegram.sh" 2>/dev/null || true
# Stop and remove containers (always check 1-5 to catch orphaned containers)
for i in $(seq 1 5); do
local cname=$(get_container_name $i)
docker stop --timeout 30 "$cname" 2>/dev/null || true
docker rm -f "$cname" 2>/dev/null || true
done
# Remove volumes
for i in $(seq 1 5); do
local vname=$(get_volume_name $i)
docker volume rm "$vname" 2>/dev/null || true
done
# Stop and remove Snowflake (check up to 2 for orphaned instances)
for _sfi in 1 2; do
local _sfn=$(get_snowflake_name $_sfi)
local _sfv=$(get_snowflake_volume $_sfi)
docker stop --timeout 10 "$_sfn" 2>/dev/null || true
docker rm -f "$_sfn" 2>/dev/null || true
docker volume rm "$_sfv" 2>/dev/null || true
done
# Stop and remove Unbounded
docker stop --timeout 10 "$UNBOUNDED_CONTAINER" 2>/dev/null || true
docker rm -f "$UNBOUNDED_CONTAINER" 2>/dev/null || true
docker volume rm "$UNBOUNDED_VOLUME" 2>/dev/null || true
# Stop and remove MTProxy
docker stop --timeout 10 "$MTPROXY_CONTAINER" 2>/dev/null || true
docker rm -f "$MTPROXY_CONTAINER" 2>/dev/null || true
# Remove images
docker rmi "$BRIDGE_IMAGE" 2>/dev/null || true
docker rmi "$RELAY_IMAGE" 2>/dev/null || true
docker rmi "$SNOWFLAKE_IMAGE" 2>/dev/null || true
docker rmi "$UNBOUNDED_IMAGE" 2>/dev/null || true
docker rmi "$MTPROXY_IMAGE" 2>/dev/null || true
# Remove files
if [ "$keep_backups" = "true" ]; then
# Remove everything except backups
find "$INSTALL_DIR" -mindepth 1 -maxdepth 1 ! -name backups -exec rm -rf {} + 2>/dev/null || true
log_info "Backups preserved in $BACKUP_DIR"
else
rm -rf "$INSTALL_DIR"
fi
rm -f /usr/local/bin/torware
# Clean up cookie cache and generated scripts
rm -f /tmp/.tor_cookie_cache_* "${TMPDIR:-/tmp}"/.tor_cookie_cache_* 2>/dev/null || true
rm -f /usr/local/bin/torware-tracker.sh /usr/local/bin/torware-telegram.sh 2>/dev/null || true
echo ""
log_success "Torware has been uninstalled."
echo ""
}
#═══════════════════════════════════════════════════════════════════════
# CLI Logs
#═══════════════════════════════════════════════════════════════════════
show_logs() {
load_settings
local count=${CONTAINER_COUNT:-1}
echo ""
echo -e " ${BOLD}View Logs:${NC}"
echo ""
local _opt=1
for i in $(seq 1 $count); do
local _cn=$(get_container_name $i)
local _rt=$(get_container_relay_type $i)
echo -e " ${GREEN}${_opt}.${NC} ${_cn} (${_rt})"
_opt=$((_opt + 1))
done
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
for _sfi in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
echo -e " ${GREEN}${_opt}.${NC} $(get_snowflake_name $_sfi) (WebRTC proxy)"
_opt=$((_opt + 1))
done
fi
local _ub_opt=""
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
_ub_opt=$_opt
echo -e " ${GREEN}${_opt}.${NC} ${UNBOUNDED_CONTAINER} (Lantern proxy)"
_opt=$((_opt + 1))
fi
local _mtp_opt=""
if [ "$MTPROXY_ENABLED" = "true" ]; then
_mtp_opt=$_opt
echo -e " ${GREEN}${_opt}.${NC} ${MTPROXY_CONTAINER} (Telegram proxy)"
_opt=$((_opt + 1))
fi
echo -e " ${GREEN}0.${NC} ← Back"
echo ""
read -p " choice: " choice < /dev/tty || return
[ "$choice" = "0" ] && return
local cname=""
local _sf_start=$((count + 1))
local _sf_end=$((count + ${SNOWFLAKE_COUNT:-1}))
if [ -n "$_mtp_opt" ] && [ "$choice" = "$_mtp_opt" ] 2>/dev/null; then
cname="$MTPROXY_CONTAINER"
elif [ -n "$_ub_opt" ] && [ "$choice" = "$_ub_opt" ] 2>/dev/null; then
cname="$UNBOUNDED_CONTAINER"
elif [ "$SNOWFLAKE_ENABLED" = "true" ] && [ "$choice" -ge "$_sf_start" ] 2>/dev/null && [ "$choice" -le "$_sf_end" ] 2>/dev/null; then
local _sf_idx=$((choice - count))
cname=$(get_snowflake_name $_sf_idx)
elif [[ "$choice" =~ ^[1-9]$ ]] && [ "$choice" -le "$count" ]; then
cname=$(get_container_name $choice)
else
log_warn "Invalid choice"
return
fi
log_info "Streaming logs for $cname (Ctrl+C to return to menu)..."
# Trap SIGINT so it doesn't propagate to the parent menu
trap 'true' INT
docker logs -f --tail 50 "$cname" 2>&1 || true
trap - INT
echo ""
log_info "Log stream ended."
}
#═══════════════════════════════════════════════════════════════════════
# Version & Help
#═══════════════════════════════════════════════════════════════════════
show_version() {
load_settings
echo -e " Torware v${VERSION}"
for i in $(seq 1 ${CONTAINER_COUNT:-1}); do
local image=$(get_docker_image $i)
local digest
digest=$(docker inspect --format '{{index .RepoDigests 0}}' "$image" 2>/dev/null || echo "unknown")
local rtype=$(get_container_relay_type $i)
echo -e " Container $i ($rtype): $image"
echo -e " Digest: ${digest}"
done
}
show_help() {
echo ""
echo -e "${CYAN} Torware v${VERSION}${NC}"
echo ""
echo -e " ${BOLD}Usage:${NC} torware <command>"
echo ""
echo -e " ${BOLD}Commands:${NC}"
echo -e " ${GREEN}menu${NC} Open interactive management menu"
echo -e " ${GREEN}status${NC} Show relay status"
echo -e " ${GREEN}dashboard${NC} Live TUI dashboard (auto-refresh)"
echo -e " ${GREEN}stats${NC} Advanced stats with country charts"
echo -e " ${GREEN}peers${NC} Live circuits by country"
echo -e " ${GREEN}start${NC} Start all relay containers"
echo -e " ${GREEN}stop${NC} Stop all relay containers"
echo -e " ${GREEN}restart${NC} Restart all relay containers"
echo -e " ${GREEN}logs${NC} Stream container logs"
echo -e " ${GREEN}health${NC} Run health diagnostics"
echo -e " ${GREEN}fingerprint${NC} Show relay fingerprint(s)"
echo -e " ${GREEN}bridge-line${NC} Show bridge line(s) for sharing"
echo -e " ${GREEN}backup${NC} Backup relay identity keys"
echo -e " ${GREEN}restore${NC} Restore relay keys from backup"
echo -e " ${GREEN}snowflake${NC} Toggle/manage Snowflake proxy"
echo -e " ${GREEN}unbounded${NC} Toggle/manage Unbounded (Lantern) proxy"
echo -e " ${GREEN}mtproxy${NC} Toggle/manage MTProxy (Telegram) proxy"
echo -e " ${GREEN}version${NC} Show version info"
echo -e " ${GREEN}uninstall${NC} Remove Torware"
echo ""
}
#═══════════════════════════════════════════════════════════════════════
# Interactive Menu
#═══════════════════════════════════════════════════════════════════════
show_about() {
local _back=false
while [ "$_back" = "false" ]; do
clear
echo -e "${CYAN}"
echo "═══════════════════════════════════════════════════════════════"
echo " ABOUT & LEARN"
echo "═══════════════════════════════════════════════════════════════"
echo -e "${NC}"
echo -e " ${GREEN}1.${NC} 🧅 What is Tor?"
echo -e " ${GREEN}2.${NC} 🌉 Bridge Relays (obfs4)"
echo -e " ${GREEN}3.${NC} 🔁 Middle Relays"
echo -e " ${GREEN}4.${NC} 🚪 Exit Relays"
echo -e " ${GREEN}5.${NC} ❄ Snowflake Proxy"
echo -e " ${GREEN}6.${NC} 🌐 Lantern Unbounded Proxy"
echo -e " ${GREEN}7.${NC} 📱 MTProxy (Telegram Proxy)"
echo -e " ${GREEN}8.${NC} 🔒 How Tor Circuits Work"
echo -e " ${GREEN}9.${NC} 📊 Understanding Your Dashboard"
echo -e " ${GREEN}10.${NC} ⚖ Legal & Safety Considerations"
echo -e " ${GREEN}11.${NC} 📖 About Torware"
echo -e " ${GREEN}0.${NC} ← Back"
echo ""
read -p " Choose a topic [0-11]: " _topic < /dev/tty || { _back=true; continue; }
echo ""
case "$_topic" in
1)
echo -e "${CYAN}═══ What is Tor? ═══${NC}"
echo ""
echo -e " Tor (The Onion Router) is free, open-source software that enables"
echo -e " anonymous communication over the internet. It works by encrypting"
echo -e " your traffic in multiple layers (like an onion) and routing it"
echo -e " through a series of volunteer-operated servers called relays."
echo ""
echo -e " ${BOLD}How it protects users:${NC}"
echo -e " • Each relay only knows the previous and next hop, never the full path"
echo -e " • The entry relay knows your IP but not your destination"
echo -e " • The exit relay knows the destination but not your IP"
echo -e " • No single relay can link you to your activity"
echo ""
echo -e " ${BOLD}Who uses Tor:${NC}"
echo -e " • Journalists protecting sources in authoritarian countries"
echo -e " • Activists and dissidents communicating safely"
echo -e " • Whistleblowers submitting sensitive information"
echo -e " • Ordinary people who value their privacy"
echo -e " • Researchers studying censorship and surveillance"
echo ""
echo -e " ${BOLD}The Tor network consists of ~7,000 relays run by volunteers"
echo -e " worldwide. By running Torware, you are one of them.${NC}"
;;
2)
echo -e "${CYAN}═══ Bridge Relays (obfs4) ═══${NC}"
echo ""
echo -e " Bridges are special Tor relays that are ${BOLD}not listed${NC} in the"
echo -e " public Tor directory. They serve as secret entry points for"
echo -e " users in countries that block access to known Tor relays."
echo ""
echo -e " ${BOLD}How bridges work:${NC}"
echo -e " • Bridge addresses are distributed privately via BridgeDB"
echo -e " • Users request bridges via bridges.torproject.org or email"
echo -e " • The obfs4 pluggable transport disguises Tor traffic to look"
echo -e " like random data, making it hard to detect and block"
echo ""
echo -e " ${BOLD}What happens when you run a bridge:${NC}"
echo -e " • Your IP is NOT published in the public Tor consensus"
echo -e " • BridgeDB distributes your bridge line to users who need it"
echo -e " • It can take ${YELLOW}hours to days${NC} for clients to find your bridge"
echo -e " • You help users in censored regions bypass internet blocks"
echo ""
echo -e " ${BOLD}Bridge is the safest relay type to run.${NC} Your IP stays"
echo -e " unlisted and you only serve as an entry point, never an exit."
echo ""
echo ""
echo -e " ${BOLD}🏠 Running from home? Port forwarding required:${NC}"
echo -e " Your router blocks incoming connections by default. You must"
echo -e " forward these ports in your router settings:"
echo ""
echo -e " ${GREEN}ORPort (9001 TCP)${NC} → your server's local IP"
echo -e " ${GREEN}obfs4 (9002 TCP)${NC} → your server's local IP"
echo ""
echo -e " How: Log into your router (usually 192.168.1.1 or 10.0.0.1),"
echo -e " find 'Port Forwarding' and add both TCP port forwards."
echo -e " Without this, Tor cannot confirm reachability and your bridge"
echo -e " will NOT be published or receive clients."
echo ""
echo -e " ${DIM}Snowflake does NOT need port forwarding — WebRTC handles"
echo -e " NAT traversal automatically.${NC}"
echo ""
echo -e " ${BOLD}🕐 Bridge line availability:${NC}"
echo -e " After starting, your bridge line (option 'b' in the menu) will"
echo -e " show 'not yet available' until Tor completes these steps:"
echo -e " 1. Bootstrap to 100% and self-test ORPort reachability"
echo -e " 2. Publish descriptor to bridge authority"
echo -e " 3. Get included in BridgeDB for distribution"
echo -e " This process takes ${YELLOW}a few hours to 1-2 days${NC}. You can verify"
echo -e " progress with Health Check (option 8) — look for a valid"
echo -e " fingerprint and 'ORPort reachable' in your Tor logs."
echo -e " Once the bridge line appears, share it with users who need"
echo -e " to bypass censorship — or let BridgeDB distribute it."
echo ""
echo -e " ${DIM}Docker image: thetorproject/obfs4-bridge${NC}"
;;
3)
echo -e "${CYAN}═══ Middle Relays ═══${NC}"
echo ""
echo -e " Middle relays are the backbone of the Tor network. They sit"
echo -e " between the entry (guard) and exit relays in a Tor circuit."
echo ""
echo -e " ${BOLD}What middle relays do:${NC}"
echo -e " • Receive encrypted traffic from one relay and pass it to the next"
echo -e " • Cannot see the original source or final destination"
echo -e " • Cannot decrypt the traffic content"
echo -e " • Add hops to increase anonymity for users"
echo ""
echo -e " ${BOLD}Running a middle relay:${NC}"
echo -e " • Your IP IS listed in the public Tor consensus"
echo -e " • This is generally safe — you only relay encrypted traffic"
echo -e " • No abuse complaints since you're not an exit"
echo -e " • The ExitPolicy is set to 'reject *:*' (no exit traffic)"
echo ""
echo -e " ${BOLD}Guard status:${NC} After running reliably for ~2 months, your"
echo -e " relay may earn the 'Guard' flag, meaning Tor clients trust it"
echo -e " enough to use as their entry relay. This is a sign of good"
echo -e " reputation in the network."
;;
4)
echo -e "${CYAN}═══ Exit Relays ═══${NC}"
echo ""
echo -e " Exit relays are the ${BOLD}final hop${NC} in a Tor circuit. They"
echo -e " decrypt the outermost layer of encryption and forward the"
echo -e " traffic to its final destination on the regular internet."
echo ""
echo -e " ${BOLD}What exit relays do:${NC}"
echo -e " • Connect to the destination website/service on behalf of the user"
echo -e " • Can see the destination (but NOT who is connecting)"
echo -e " • Handle the 'last mile' of the anonymized connection"
echo ""
echo -e " ${RED}${BOLD}⚠ Important considerations:${NC}"
echo -e " ${RED}• Your IP appears as the source of all traffic exiting through you${NC}"
echo -e " ${RED}• You may receive abuse complaints (DMCA, hacking reports, etc.)${NC}"
echo -e " ${RED}• Some ISPs and hosting providers prohibit exit relays${NC}"
echo -e " ${RED}• Legal implications vary by jurisdiction${NC}"
echo ""
echo -e " ${BOLD}Exit policies:${NC}"
echo -e "${GREEN}Reduced${NC} — Only allows common ports (80, 443, etc.)"
echo -e "${YELLOW}Default${NC} — Tor's standard exit policy"
echo -e "${RED}Full${NC} — Allows all traffic (most complaints)"
echo ""
echo -e " ${BOLD}Only run an exit relay if you understand the legal risks${NC}"
echo -e " ${BOLD}and your hosting provider explicitly permits it.${NC}"
;;
5)
local _sf_menu_label="Snowflake Proxy (WebRTC)"
[ "${SNOWFLAKE_COUNT:-1}" -gt 1 ] && _sf_menu_label="Snowflake Proxy (WebRTC) x${SNOWFLAKE_COUNT}"
echo -e "${CYAN}═══ ${_sf_menu_label} ═══${NC}"
echo ""
echo -e " Snowflake is a pluggable transport that uses ${BOLD}WebRTC${NC} to help"
echo -e " censored users connect to the Tor network. It's different from"
echo -e " running a Tor relay."
echo ""
echo -e " ${BOLD}How Snowflake works:${NC}"
echo -e " 1. A censored user's Tor client contacts a ${CYAN}broker${NC}"
echo -e " 2. The broker matches them with an available proxy (${GREEN}you${NC})"
echo -e " 3. A WebRTC peer-to-peer connection is established"
echo -e " 4. Your proxy forwards their traffic to a Tor bridge"
echo -e " 5. The bridge connects them to the Tor network"
echo ""
echo -e " ${BOLD}You are NOT an exit point.${NC} You're a temporary relay between"
echo -e " the censored user and a Tor bridge. The traffic is encrypted"
echo -e " end-to-end — you cannot see what the user is doing."
echo ""
echo -e " ${BOLD}NAT types (from your logs):${NC}"
echo -e "${GREEN}unrestricted${NC} — Best. Direct peer connections. Most clients."
echo -e "${YELLOW}restricted${NC} — OK. Uses TURN relay for some connections."
echo -e "${RED}unknown${NC} — May have connectivity issues."
echo ""
echo -e " ${BOLD}Resource usage:${NC} Very lightweight (0.5 CPU, 128MB RAM)."
echo -e " Can run safely alongside any relay type."
;;
6)
echo -e "${CYAN}═══ Lantern Unbounded Proxy ═══${NC}"
echo ""
echo -e " Lantern is a free, open-source censorship circumvention tool"
echo -e " that helps users in restricted countries access the open internet."
echo -e " ${BOLD}Unbounded${NC} is Lantern's volunteer proxy network — similar in spirit"
echo -e " to Snowflake, but for the Lantern network instead of Tor."
echo ""
echo -e " ${BOLD}How Unbounded works:${NC}"
echo -e " 1. You run a lightweight ${CYAN}widget${NC} process (Go binary)"
echo -e " 2. The widget registers with Lantern's ${CYAN}FREDDIE${NC} signaling server"
echo -e " 3. Censored users connect to you via ${CYAN}WebRTC${NC} (peer-to-peer)"
echo -e " 4. Your proxy forwards their traffic to Lantern's ${CYAN}EGRESS${NC} server"
echo -e " 5. The egress server delivers the traffic to its destination"
echo ""
echo -e " ${BOLD}You are NOT an exit point.${NC} All traffic exits through Lantern's"
echo -e " egress servers — your IP is never exposed as the source. The"
echo -e " traffic between you and the egress server is encrypted."
echo ""
echo -e " ${BOLD}Key differences from Snowflake:${NC}"
echo -e " • Snowflake serves the ${CYAN}Tor${NC} network; Unbounded serves ${CYAN}Lantern${NC}"
echo -e " • Unbounded multiplexes many users on a single instance"
echo -e " • Uses ${GREEN}--network host${NC} for WebRTC NAT traversal (no port forwarding)"
echo -e " • Built from source (Go) — Docker image is compiled on first run"
echo ""
echo -e " ${BOLD}Resource usage:${NC} Very lightweight (0.5 CPU, 256MB RAM)."
echo -e " Can run safely alongside any relay type and Snowflake."
echo ""
echo -e " ${BOLD}Learn more:${NC} ${CYAN}https://unbounded.lantern.io${NC}"
;;
7)
echo -e "${CYAN}═══ MTProxy (Telegram Proxy) ═══${NC}"
echo ""
echo -e " MTProxy is an official proxy protocol for Telegram, designed to"
echo -e " help users in censored countries access the messaging app."
echo -e " Torware uses ${BOLD}mtg${NC}, a modern Go implementation with ${BOLD}FakeTLS${NC}."
echo ""
echo -e " ${BOLD}How MTProxy works:${NC}"
echo -e " 1. You run an MTProxy server with a unique ${CYAN}secret key${NC}"
echo -e " 2. Users connect using a ${CYAN}tg://proxy${NC} link or QR code"
echo -e " 3. Traffic is encrypted and proxied through your server"
echo -e " 4. Telegram servers receive the connection from your IP"
echo ""
echo -e " ${BOLD}FakeTLS (Traffic Obfuscation):${NC}"
echo -e " FakeTLS makes your proxy traffic look like normal HTTPS traffic"
echo -e " to a legitimate website (like cloudflare.com). When censors"
echo -e " inspect the connection, they see what appears to be standard"
echo -e " TLS handshakes to a popular CDN. This makes it much harder to"
echo -e " detect and block."
echo ""
echo -e " ${BOLD}The 'ee' secret prefix:${NC}"
echo -e " MTProxy secrets starting with ${CYAN}ee${NC} indicate FakeTLS mode."
echo -e " The domain (e.g., cloudflare.com) is encoded in the secret."
echo -e " Older 'dd' secrets are deprecated and easier to detect."
echo ""
echo -e " ${BOLD}Sharing your proxy:${NC}"
echo -e "${CYAN}tg://proxy?...${NC} — Deep link opens directly in Telegram app"
echo -e "${CYAN}https://t.me/proxy?...${NC} — Web link works in any browser"
echo -e "${CYAN}QR code${NC} — Users can scan with Telegram's camera feature"
echo ""
echo -e " ${BOLD}Security features:${NC}"
echo -e "${GREEN}Concurrency limit${NC} — Caps max simultaneous connections"
echo -e "${GREEN}Geo-blocking${NC} — Block connections from specific countries"
echo -e "${GREEN}Anti-replay${NC} — Prevents replay attacks (built-in)"
echo ""
echo -e " ${BOLD}You are NOT an exit point.${NC} Your proxy forwards traffic to"
echo -e " Telegram's servers. Users' actual messages are end-to-end"
echo -e " encrypted by Telegram — you cannot read them."
echo ""
echo -e " ${BOLD}Resource usage:${NC} Very lightweight (0.5 CPU, 128MB RAM)."
echo -e " Can run safely alongside any relay type and other proxies."
echo ""
echo -e " ${BOLD}Docker image:${NC} ${DIM}nineseconds/mtg:2${NC}"
;;
8)
echo -e "${CYAN}═══ How Tor Circuits Work ═══${NC}"
echo ""
echo -e " A Tor circuit is a path through 3 relays:"
echo ""
echo -e " ${GREEN}You${NC} → [${CYAN}Guard/Bridge${NC}] → [${YELLOW}Middle${NC}] → [${RED}Exit${NC}] → ${BOLD}Destination${NC}"
echo ""
echo -e " ${BOLD}Each layer of encryption:${NC}"
echo -e " ┌──────────────────────────────────────────────┐"
echo -e " │ Layer 3 (Guard key): │"
echo -e " │ ┌──────────────────────────────────────┐ │"
echo -e " │ │ Layer 2 (Middle key): │ │"
echo -e " │ │ ┌──────────────────────────────┐ │ │"
echo -e " │ │ │ Layer 1 (Exit key): │ │ │"
echo -e " │ │ │ ┌──────────────────────┐ │ │ │"
echo -e " │ │ │ │ Your actual data │ │ │ │"
echo -e " │ │ │ └──────────────────────┘ │ │ │"
echo -e " │ │ └──────────────────────────────┘ │ │"
echo -e " │ └──────────────────────────────────────┘ │"
echo -e " └──────────────────────────────────────────────┘"
echo ""
echo -e " • The Guard peels Layer 3, sees the Middle address"
echo -e " • The Middle peels Layer 2, sees the Exit address"
echo -e " • The Exit peels Layer 1, sees the destination"
echo -e "${BOLD}No single relay knows both source and destination${NC}"
echo ""
echo -e " Circuits are rebuilt every ~10 minutes for extra security."
;;
9)
echo -e "${CYAN}═══ Understanding Your Dashboard ═══${NC}"
echo ""
echo -e " ${BOLD}Circuits:${NC}"
echo -e " Active Tor circuits passing through your relay right now."
echo -e " Each circuit is a 3-hop encrypted path. One user can have"
echo -e " multiple circuits open (Tor rotates every ~10 minutes),"
echo -e " so circuits ≠ users. 9 circuits might be 3-5 users."
echo ""
echo -e " ${BOLD}Connections:${NC}"
echo -e " Number of OR (Onion Router) connections to other relays."
echo -e " These are connections to Tor infrastructure (directory"
echo -e " authorities, other relays), not direct user connections."
echo -e " A new bridge with 15 connections is normal — most are"
echo -e " Tor network overhead, not individual users."
echo ""
echo -e " ${BOLD}Traffic (Downloaded/Uploaded):${NC}"
echo -e " Total bytes relayed since the container started."
echo -e " Includes both user traffic and Tor protocol overhead"
echo -e " (consensus downloads, descriptor fetches, etc.)."
echo -e " Low traffic (KB range) in the first hours is normal."
echo ""
echo -e " ${BOLD}App CPU / RAM:${NC}"
echo -e " Resource usage of your Tor container(s). CPU is normalized"
echo -e " per-core (e.g. 0.33% of one core), with total vCPU usage"
echo -e " shown in parentheses (e.g. 3.95% across all cores)."
echo ""
echo -e " ${BOLD}Snowflake section:${NC}"
echo -e " Connections: total clients matched to your proxy by the"
echo -e " Snowflake broker (cumulative, not concurrent)."
echo -e " Traffic: WebRTC data relayed to Tor bridges."
echo ""
echo -e " ${BOLD}Country breakdown (CLIENTS BY COUNTRY):${NC}"
echo -e " ${YELLOW}Important: This is NOT real-time connected users.${NC}"
echo -e " For bridges, this shows unique clients seen in the ${BOLD}last 24"
echo -e " hours${NC} (from Tor's CLIENTS_SEEN). So '8 from Estonia' means"
echo -e " 8 unique clients from Estonia used your bridge in the past"
echo -e " day, not 8 people connected right now."
echo -e " For relays: countries of peer relays you're connected to."
echo -e " For Snowflake: countries of users you've proxied for."
echo ""
echo -e " ${BOLD}Data Cap:${NC}"
echo -e " If configured, shows Tor's AccountingMax usage. Tor will"
echo -e " hibernate automatically when the cap is reached and resume"
echo -e " at the start of the next accounting period."
echo ""
echo -e " ${BOLD}What's normal for a new bridge?${NC}"
echo -e " • First few minutes: 0 circuits, low KB traffic (just Tor overhead)"
echo -e " • After 1-3 hours: a few circuits, some client countries appear"
echo -e " • After 24 hours: steady circuits, growing country list"
echo -e " • After days/weeks: stable traffic as BridgeDB distributes you"
;;
10)
echo -e "${CYAN}═══ Legal & Safety Considerations ═══${NC}"
echo ""
echo -e " ${BOLD}Bridges (safest):${NC}"
echo -e " • IP not publicly listed. Minimal legal risk."
echo -e " • No abuse complaints. You're an unlisted entry point."
echo -e " • Recommended for home connections and most hosting."
echo ""
echo -e " ${BOLD}Middle relays (safe):${NC}"
echo -e " • IP is publicly listed as a Tor relay."
echo -e " • Very rarely generates complaints — you're not an exit."
echo -e " • Some organizations may flag Tor relay IPs."
echo ""
echo -e " ${BOLD}Exit relays (requires caution):${NC}"
echo -e " • Your IP is the apparent source of users' traffic."
echo -e " • You WILL receive abuse complaints."
echo -e " • Check with your ISP/hosting provider first."
echo -e " • Consider running a reduced exit policy."
echo -e " • The Tor Project provides a legal FAQ:"
echo -e " ${CYAN}https://community.torproject.org/relay/community-resources/eff-tor-legal-faq/${NC}"
echo ""
echo -e " ${BOLD}Snowflake (very safe):${NC}"
echo -e " • You're a temporary WebRTC proxy, not an exit."
echo -e " • Traffic is encrypted end-to-end."
echo -e " • Very low legal risk."
echo ""
echo -e " ${BOLD}Unbounded / Lantern (very safe):${NC}"
echo -e " • You relay encrypted traffic to Lantern's egress servers."
echo -e " • Your IP is never the exit point — Lantern handles that."
echo -e " • Very low legal risk, similar to Snowflake."
echo ""
echo -e " ${BOLD}General tips:${NC}"
echo -e " • Run on a VPS/dedicated server rather than home if possible"
echo -e " • Use a separate IP from your personal services"
echo -e " • Set a contact email so Tor directory authorities can reach you"
echo -e " • Keep your relay updated (Torware handles Docker image updates)"
;;
11)
echo -e "${CYAN}═══ About Torware ═══${NC}"
echo ""
echo -e " ${BOLD}Torware v${VERSION}${NC}"
echo -e " An all-in-one tool for running Tor relays with Docker."
echo ""
echo -e " ${BOLD}Features:${NC}"
echo -e " • Setup wizard — Bridge, Middle, or Exit relay in minutes"
echo -e " • Multi-container — Run up to 5 relays on one host"
echo -e " • Mixed types — Different relay types per container"
echo -e " • Snowflake — Run a WebRTC proxy alongside your relay"
echo -e " • Unbounded — Help Lantern network with WebRTC proxy"
echo -e " • MTProxy — Telegram proxy with FakeTLS obfuscation"
echo -e " • Live dashboard — Real-time stats from Tor's ControlPort"
echo -e " • Country tracking — See where your traffic goes"
echo -e " • Telegram alerts — Get notified about your relay"
echo -e " • Backup/restore — Preserve your relay identity keys"
echo -e " • Health checks — 15-point diagnostic system"
echo -e " • Auto-start — Systemd service for boot persistence"
echo ""
echo -e " ${BOLD}How it works:${NC}"
echo -e " Torware manages Docker containers running official Tor images."
echo -e " Each container gets a generated torrc config, unique ports,"
echo -e " and resource limits. Stats are collected via Tor's ControlPort"
echo -e " protocol (port 9051+) using cookie authentication."
echo ""
echo -e " ${BOLD}Open source:${NC}"
echo -e " ${CYAN}https://git.samnet.dev/SamNet-dev/torware${NC}"
;;
0)
_back=true
continue
;;
*)
echo -e " ${RED}Invalid choice${NC}"
;;
esac
if [ "$_back" = "false" ]; then
echo ""
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
fi
done
}
show_menu() {
load_settings
local redraw=true
while true; do
if [ "$redraw" = "true" ]; then
clear
print_header
fi
redraw=true
echo -e " ${BOLD}Main Menu:${NC}"
echo ""
echo -e " ${GREEN}1.${NC} 📊 Live Dashboard"
echo -e " ${GREEN}2.${NC} 📈 Advanced Stats"
echo -e " ${GREEN}3.${NC} 🌍 Live Peers by Country"
echo -e " ${GREEN}4.${NC} 📜 View Logs"
echo -e " ${GREEN}5.${NC} ▶ Start Relay"
echo -e " ${GREEN}6.${NC} ⏹ Stop Relay"
echo -e " ${GREEN}7.${NC} 🔄 Restart Relay"
echo -e " ${GREEN}8.${NC} 🔍 Health Check"
echo -e " ${GREEN}9.${NC} ⚙ Settings & Tools"
echo -e " ${GREEN}f.${NC} 🔑 Show Fingerprint(s)"
# Show bridge option if any container is a bridge
local _any_bridge=false
for _mi in $(seq 1 ${CONTAINER_COUNT:-1}); do
[ "$(get_container_relay_type $_mi)" = "bridge" ] && _any_bridge=true
done
if [ "$_any_bridge" = "true" ]; then
echo -e " ${GREEN}b.${NC} 🌉 Show Bridge Line(s)"
fi
# Show Snowflake status
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
local _sf_label="${GREEN}Running${NC}"
is_snowflake_running || _sf_label="${RED}Stopped${NC}"
local _sf_cnt_label=""
[ "${SNOWFLAKE_COUNT:-1}" -gt 1 ] && _sf_cnt_label=" (${SNOWFLAKE_COUNT} instances)"
echo -e " ${GREEN}s.${NC} ❄ Snowflake Proxy [${_sf_label}]${_sf_cnt_label}"
else
echo -e " ${GREEN}s.${NC} ❄ Enable Snowflake Proxy"
fi
# Show Unbounded status
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
local _ub_label="${GREEN}Running${NC}"
is_unbounded_running || _ub_label="${RED}Stopped${NC}"
echo -e " ${GREEN}u.${NC} 🌐 Unbounded Proxy (Lantern) [${_ub_label}]"
else
echo -e " ${GREEN}u.${NC} 🌐 Enable Unbounded Proxy (Lantern)"
fi
# Show MTProxy status
if [ "$MTPROXY_ENABLED" = "true" ]; then
local _mtp_label="${GREEN}Running${NC}"
is_mtproxy_running || _mtp_label="${RED}Stopped${NC}"
echo -e " ${GREEN}m.${NC} 📱 MTProxy (Telegram) [${_mtp_label}]"
else
echo -e " ${GREEN}m.${NC} 📱 Enable MTProxy (Telegram)"
fi
echo -e " ${GREEN}t.${NC} 💬 Telegram Notifications"
echo -e " ${GREEN}a.${NC} 📖 About & Learn"
echo -e " ${GREEN}0.${NC} 🚪 Exit"
echo ""
read -p " Enter choice: " choice < /dev/tty || { echo ""; break; }
case "$choice" in
1)
show_dashboard
;;
2)
show_advanced_stats
;;
3)
show_peers
;;
4)
show_logs
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
5)
start_relay
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
start_snowflake_container
fi
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
start_unbounded_container
fi
if [ "$MTPROXY_ENABLED" = "true" ]; then
start_mtproxy_container
fi
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
6)
stop_relay
if [ "$RELAY_TYPE" = "none" ]; then
stop_snowflake_container
stop_unbounded_container
stop_mtproxy_container
fi
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
7)
restart_relay
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
restart_snowflake_container
fi
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
restart_unbounded_container
fi
if [ "$MTPROXY_ENABLED" = "true" ]; then
restart_mtproxy_container
fi
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
8)
health_check
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
9)
show_settings_menu
;;
f|F)
show_fingerprint
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
b|B)
if [ "$_any_bridge" = "true" ]; then
show_bridge_line
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
else
echo -e " ${RED}Invalid choice${NC}"
redraw=false
fi
;;
s|S)
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
echo ""
echo -e " Snowflake is currently ${GREEN}enabled${NC} (${SNOWFLAKE_COUNT:-1} instance(s))."
for _si in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
local _sn=$(get_snowflake_name $_si)
local _sc=$(get_snowflake_cpus $_si)
local _sm=$(get_snowflake_memory $_si)
local _ss="${RED}Stopped${NC}"
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${_sn}$" && _ss="${GREEN}Running${NC}"
echo -e " ${_sn}: [${_ss}] CPU: ${CYAN}${_sc}${NC} RAM: ${CYAN}${_sm}${NC}"
done
echo ""
echo -e " 1. Restart all Snowflake proxies"
echo -e " 2. Stop all Snowflake proxies"
echo -e " 3. Disable Snowflake"
echo -e " 4. Change resource limits"
if [ "${SNOWFLAKE_COUNT:-1}" -lt 2 ]; then
echo -e " 5. Add another Snowflake instance"
fi
if [ "${SNOWFLAKE_COUNT:-1}" -gt 1 ]; then
echo -e " 6. Remove Snowflake instance #${SNOWFLAKE_COUNT} (keep #1 running)"
fi
echo -e " 0. Back"
read -p " choice: " sf_choice < /dev/tty || true
case "${sf_choice:-0}" in
1) restart_snowflake_container ;;
2)
stop_snowflake_container
log_warn "Snowflake stopped but still enabled. It will restart on next 'torware start'. Use option 3 to disable permanently."
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
echo ""
;;
3)
SNOWFLAKE_ENABLED="false"
stop_snowflake_container
save_settings
log_success "Snowflake disabled"
;;
4)
echo ""
echo -e " ${DIM}CPU: number of cores (e.g. 0.5, 1.0, 2.0)${NC}"
echo -e " ${DIM}RAM: amount with unit (e.g. 128m, 256m, 512m, 1g)${NC}"
echo ""
for _si in $(seq 1 ${SNOWFLAKE_COUNT:-1}); do
local _sn=$(get_snowflake_name $_si)
local _cur_cpu=$(get_snowflake_cpus $_si)
local _cur_mem=$(get_snowflake_memory $_si)
echo -e " ${BOLD}${_sn}:${NC}"
read -p " CPU cores [${_cur_cpu}]: " _sf_cpu < /dev/tty || true
read -p " RAM limit [${_cur_mem}]: " _sf_mem < /dev/tty || true
if [ -n "$_sf_cpu" ]; then
if [[ "$_sf_cpu" =~ ^[0-9]+\.?[0-9]*$ ]]; then
eval "SNOWFLAKE_CPUS_${_si}=\"$_sf_cpu\""
else
log_warn "Invalid CPU value, keeping ${_cur_cpu}"
fi
fi
if [ -n "$_sf_mem" ]; then
if [[ "$_sf_mem" =~ ^[0-9]+[mMgG]$ ]]; then
eval "SNOWFLAKE_MEMORY_${_si}=\"$(echo "$_sf_mem" | tr '[:upper:]' '[:lower:]')\""
else
log_warn "Invalid RAM value, keeping ${_cur_mem}"
fi
fi
done
save_settings
log_success "Snowflake limits updated"
echo ""
read -p " Restart Snowflake to apply? [Y/n] " _sf_rs < /dev/tty || true
if [[ ! "$_sf_rs" =~ ^[Nn]$ ]]; then
restart_snowflake_container
fi
;;
5)
if [ "${SNOWFLAKE_COUNT:-1}" -ge 2 ]; then
log_warn "Maximum of 2 Snowflake instances supported."
else
local _new_idx=2
local _def_cpu=$(get_snowflake_default_cpus)
local _def_mem=$(get_snowflake_default_memory)
echo ""
echo -e " ${BOLD}Adding Snowflake instance #${_new_idx}${NC}"
echo -e " ${DIM}Each instance registers independently with the broker${NC}"
echo -e " ${DIM}and receives its own client assignments.${NC}"
echo ""
read -p " CPU cores [${_def_cpu}]: " _sf_cpu < /dev/tty || true
read -p " RAM limit [${_def_mem}]: " _sf_mem < /dev/tty || true
[ -z "$_sf_cpu" ] && _sf_cpu="$_def_cpu"
[ -z "$_sf_mem" ] && _sf_mem="$_def_mem"
if [[ "$_sf_cpu" =~ ^[0-9]+\.?[0-9]*$ ]]; then
eval "SNOWFLAKE_CPUS_${_new_idx}=\"$_sf_cpu\""
else
log_warn "Invalid CPU, using default ${_def_cpu}"
eval "SNOWFLAKE_CPUS_${_new_idx}=\"$_def_cpu\""
fi
if [[ "$_sf_mem" =~ ^[0-9]+[mMgG]$ ]]; then
eval "SNOWFLAKE_MEMORY_${_new_idx}=\"$(echo "$_sf_mem" | tr '[:upper:]' '[:lower:]')\""
else
log_warn "Invalid RAM, using default ${_def_mem}"
eval "SNOWFLAKE_MEMORY_${_new_idx}=\"$_def_mem\""
fi
SNOWFLAKE_COUNT=$_new_idx
save_settings
run_snowflake_container $_new_idx
fi
;;
6)
if [ "${SNOWFLAKE_COUNT:-1}" -le 1 ]; then
log_warn "Cannot remove the last instance. Use option 3 to disable Snowflake."
else
local _rm_idx=${SNOWFLAKE_COUNT}
local _rm_name=$(get_snowflake_name $_rm_idx)
docker stop --timeout 10 "$_rm_name" 2>/dev/null || true
docker rm -f "$_rm_name" 2>/dev/null || true
docker volume rm "$(get_snowflake_volume $_rm_idx)" 2>/dev/null || true
eval "SNOWFLAKE_CPUS_${_rm_idx}=''"
eval "SNOWFLAKE_MEMORY_${_rm_idx}=''"
SNOWFLAKE_COUNT=$((_rm_idx - 1))
save_settings
log_success "Removed $_rm_name (now running $SNOWFLAKE_COUNT instance(s))"
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
echo ""
fi
;;
esac
else
echo ""
echo -e " Snowflake is a WebRTC proxy that helps censored users"
echo -e " connect to Tor. Runs as a lightweight separate container."
read -p " Enable Snowflake proxy? [y/N] " sf_en < /dev/tty || true
if [[ "$sf_en" =~ ^[Yy]$ ]]; then
SNOWFLAKE_ENABLED="true"
SNOWFLAKE_COUNT=1
local _def_cpu=$(get_snowflake_default_cpus)
local _def_mem=$(get_snowflake_default_memory)
echo ""
read -p " CPU cores [${_def_cpu}]: " _sf_cpu < /dev/tty || true
read -p " RAM limit [${_def_mem}]: " _sf_mem < /dev/tty || true
[ -z "$_sf_cpu" ] && _sf_cpu="$_def_cpu"
[ -z "$_sf_mem" ] && _sf_mem="$_def_mem"
[[ "$_sf_cpu" =~ ^[0-9]+\.?[0-9]*$ ]] && SNOWFLAKE_CPUS_1="$_sf_cpu" || SNOWFLAKE_CPUS_1="$_def_cpu"
[[ "$_sf_mem" =~ ^[0-9]+[mMgG]$ ]] && SNOWFLAKE_MEMORY_1="$(echo "$_sf_mem" | tr '[:upper:]' '[:lower:]')" || SNOWFLAKE_MEMORY_1="$_def_mem"
save_settings
run_snowflake_container 1
fi
fi
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
u|U)
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
echo ""
echo -e " Unbounded (Lantern) is currently ${GREEN}enabled${NC}."
local _ubn="$UNBOUNDED_CONTAINER"
local _ubs="${RED}Stopped${NC}"
is_unbounded_running && _ubs="${GREEN}Running${NC}"
echo -e " ${_ubn}: [${_ubs}] CPU: ${CYAN}${UNBOUNDED_CPUS:-0.5}${NC} RAM: ${CYAN}${UNBOUNDED_MEMORY:-256m}${NC}"
echo ""
echo -e " 1. Restart Unbounded proxy"
echo -e " 2. Stop Unbounded proxy"
echo -e " 3. Disable Unbounded"
echo -e " 4. Change resource limits"
echo -e " ${RED}5. Remove Unbounded (stop, remove container & image)${NC}"
echo -e " 0. Back"
read -p " choice: " ub_choice < /dev/tty || true
case "${ub_choice:-0}" in
1) restart_unbounded_container ;;
2)
stop_unbounded_container
log_warn "Unbounded stopped but still enabled. It will restart on next 'torware start'. Use option 3 to disable permanently."
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
echo ""
;;
3)
UNBOUNDED_ENABLED="false"
stop_unbounded_container
save_settings
log_success "Unbounded disabled"
;;
5)
echo ""
read -p " Are you sure? This will remove the container, volume, and image. [y/N] " _ub_rm < /dev/tty || true
if [[ "$_ub_rm" =~ ^[Yy]$ ]]; then
UNBOUNDED_ENABLED="false"
stop_unbounded_container
docker rm -f "$UNBOUNDED_CONTAINER" 2>/dev/null || true
docker volume rm "$UNBOUNDED_VOLUME" 2>/dev/null || true
docker rmi "$UNBOUNDED_IMAGE" 2>/dev/null || true
save_settings
log_success "Unbounded proxy fully removed"
else
log_info "Cancelled"
fi
;;
4)
echo ""
echo -e " ${DIM}CPU: number of cores (e.g. 0.25, 0.5, 1.0)${NC}"
echo -e " ${DIM}RAM: amount with unit (e.g. 128m, 256m, 512m)${NC}"
echo ""
read -p " CPU cores [${UNBOUNDED_CPUS:-0.5}]: " _ub_cpu < /dev/tty || true
read -p " RAM limit [${UNBOUNDED_MEMORY:-256m}]: " _ub_mem < /dev/tty || true
if [ -n "$_ub_cpu" ]; then
if [[ "$_ub_cpu" =~ ^[0-9]+\.?[0-9]*$ ]]; then
UNBOUNDED_CPUS="$_ub_cpu"
else
log_warn "Invalid CPU value, keeping ${UNBOUNDED_CPUS:-0.5}"
fi
fi
if [ -n "$_ub_mem" ]; then
if [[ "$_ub_mem" =~ ^[0-9]+[mMgG]$ ]]; then
UNBOUNDED_MEMORY="$(echo "$_ub_mem" | tr '[:upper:]' '[:lower:]')"
else
log_warn "Invalid RAM value, keeping ${UNBOUNDED_MEMORY:-256m}"
fi
fi
save_settings
log_success "Unbounded limits updated"
echo ""
read -p " Restart Unbounded to apply? [Y/n] " _ub_rs < /dev/tty || true
if [[ ! "$_ub_rs" =~ ^[Nn]$ ]]; then
restart_unbounded_container
fi
;;
esac
else
echo ""
echo -e " Unbounded is a WebRTC proxy that helps censored users"
echo -e " connect via the Lantern network. Runs as a lightweight container."
read -p " Enable Unbounded proxy? [y/N] " ub_en < /dev/tty || true
if [[ "$ub_en" =~ ^[Yy]$ ]]; then
UNBOUNDED_ENABLED="true"
local _def_cpu=$(get_unbounded_default_cpus)
local _def_mem=$(get_unbounded_default_memory)
echo ""
read -p " CPU cores [${_def_cpu}]: " _ub_cpu < /dev/tty || true
read -p " RAM limit [${_def_mem}]: " _ub_mem < /dev/tty || true
[ -z "$_ub_cpu" ] && _ub_cpu="$_def_cpu"
[ -z "$_ub_mem" ] && _ub_mem="$_def_mem"
[[ "$_ub_cpu" =~ ^[0-9]+\.?[0-9]*$ ]] && UNBOUNDED_CPUS="$_ub_cpu" || UNBOUNDED_CPUS="$_def_cpu"
[[ "$_ub_mem" =~ ^[0-9]+[mMgG]$ ]] && UNBOUNDED_MEMORY="$(echo "$_ub_mem" | tr '[:upper:]' '[:lower:]')" || UNBOUNDED_MEMORY="$_def_mem"
save_settings
run_unbounded_container
fi
fi
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
m|M)
show_mtproxy_menu
;;
t|T)
show_telegram_menu
;;
a|A)
show_about
;;
0|q|Q)
echo " Goodbye!"
exit 0
;;
"")
redraw=false
;;
*)
echo -e " ${RED}Invalid choice: ${NC}${YELLOW}$choice${NC}"
redraw=false
;;
esac
done
}
manage_containers() {
load_settings
local redraw=true
while true; do
if [ "$redraw" = "true" ]; then
clear
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} MANAGE CONTAINERS ${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
fi
redraw=true
echo ""
echo -e " ${BOLD}Current containers: ${GREEN}${CONTAINER_COUNT:-0}${NC}"
echo ""
for i in $(seq 1 ${CONTAINER_COUNT:-0}); do
local _rt=$(get_container_relay_type $i)
local _cn=$(get_container_name $i)
local _or=$(get_container_orport $i)
local _pt=$(get_container_ptport $i)
local _st="${RED}stopped${NC}"
docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${_cn}$" && _st="${GREEN}running${NC}"
echo -e " ${GREEN}${i}.${NC} ${_cn} — type: ${BOLD}${_rt}${NC}, ORPort: ${_or}, PTPort: ${_pt} [${_st}]"
done
echo ""
echo -e " ${GREEN}a.${NC} Add container"
if [ "${CONTAINER_COUNT:-1}" -gt 1 ]; then
echo -e " ${GREEN}r.${NC} Remove last container"
fi
echo -e " ${GREEN}c.${NC} 🔄 Change container type"
echo -e " ${GREEN}0.${NC} ← Back"
echo ""
read -p " choice: " _mc < /dev/tty || { echo ""; break; }
case "$_mc" in
a|A)
local _cur=${CONTAINER_COUNT:-0}
if [ "$_cur" -ge 5 ]; then
log_warn "Maximum 5 containers supported"
else
# If coming from proxy-only mode and no relay settings yet, prompt for them
if [ "$RELAY_TYPE" = "none" ] && { [ -z "$NICKNAME" ] || [ "$NICKNAME" = "" ]; }; then
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} TOR RELAY SETUP ${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BOLD}This is your first Tor relay. Please provide some details:${NC}"
echo ""
# Nickname
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " Enter a nickname for your relay (1-19 chars, alphanumeric)"
echo -e " Press Enter for default: ${GREEN}Torware${NC}"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " nickname: " _input_nick < /dev/tty || true
if [ -z "$_input_nick" ]; then
NICKNAME="Torware"
elif [[ "$_input_nick" =~ ^[A-Za-z0-9]{1,19}$ ]]; then
NICKNAME="$_input_nick"
else
log_warn "Invalid nickname. Using default: Torware"
NICKNAME="Torware"
fi
echo ""
# Contact email
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " Enter contact email (shown to Tor Project, not public)"
echo -e " Press Enter to skip"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " email: " _input_email < /dev/tty || true
CONTACT_INFO="${_input_email:-nobody@example.com}"
echo ""
# Bandwidth
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
echo -e " Enter bandwidth limit in Mbps (1-100, or -1 for unlimited)"
echo -e " Press Enter for default: ${GREEN}5${NC} Mbps"
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
read -p " bandwidth: " _input_bw < /dev/tty || true
if [ -z "$_input_bw" ]; then
BANDWIDTH=5
elif [ "$_input_bw" = "-1" ]; then
BANDWIDTH="-1"
elif [[ "$_input_bw" =~ ^[0-9]+$ ]] && [ "$_input_bw" -ge 1 ] && [ "$_input_bw" -le 100 ]; then
BANDWIDTH="$_input_bw"
elif [[ "$_input_bw" =~ ^[0-9]*\.[0-9]+$ ]]; then
local _float_ok=$(awk -v val="$_input_bw" 'BEGIN { print (val >= 1 && val <= 100) ? "yes" : "no" }')
if [ "$_float_ok" = "yes" ]; then
BANDWIDTH="$_input_bw"
else
log_warn "Invalid bandwidth. Using default: 5 Mbps"
BANDWIDTH=5
fi
else
log_warn "Invalid bandwidth. Using default: 5 Mbps"
BANDWIDTH=5
fi
echo ""
fi
local _new=$((_cur + 1))
echo ""
echo -e " ${BOLD}Select type for container ${_new}:${NC}"
echo ""
echo -e " ${GREEN}1.${NC} 🌉 Bridge (obfs4)"
echo -e " Secret entry point for censored users (Iran, China, etc.)"
echo -e " IP stays ${GREEN}unlisted${NC}. Typical bandwidth: ${CYAN}1-10 Mbps${NC}"
echo ""
echo -e " ${GREEN}2.${NC} 🔁 Middle Relay"
echo -e " Backbone of the Tor network. Relays encrypted traffic."
echo -e " IP is ${YELLOW}publicly listed${NC}. Typical bandwidth: ${CYAN}10-50+ Mbps${NC}"
echo ""
echo -e " ${GREEN}3.${NC} 🚪 Exit Relay"
echo -e " Final hop — traffic exits to the internet through you."
echo -e " IP appears as ${RED}traffic source${NC}. Typical bandwidth: ${CYAN}20-100+ Mbps${NC}"
echo ""
read -p " type [1-3]: " _type_choice < /dev/tty || continue
local _new_type=""
# Check if adding middle/exit alongside existing bridges
local _has_bridge=false
for _ci in $(seq 1 $_cur); do
[ "$(get_container_relay_type $_ci)" = "bridge" ] && _has_bridge=true
done
case "$_type_choice" in
1) _new_type="bridge" ;;
2)
_new_type="middle"
if [ "$_has_bridge" = "true" ]; then
echo ""
echo -e " ${YELLOW}${BOLD}⚠ Note: You are currently running a bridge.${NC}"
echo -e " ${YELLOW}Adding a middle relay will make your IP ${BOLD}publicly visible${NC}"
echo -e " ${YELLOW}in the Tor consensus. This partly defeats the purpose of${NC}"
echo -e " ${YELLOW}running a bridge (keeping your IP unlisted).${NC}"
echo ""
echo -e " ${YELLOW}If you're on a home connection, consider running multiple${NC}"
echo -e " ${YELLOW}bridges instead, or using a separate VPS for the middle relay.${NC}"
echo ""
read -p " Continue anyway? [y/N] " _mconf < /dev/tty || continue
if [[ ! "$_mconf" =~ ^[Yy]$ ]]; then
log_info "Cancelled."
continue
fi
fi
;;
3)
if [ "$_has_bridge" = "true" ]; then
echo ""
echo -e " ${YELLOW}${BOLD}⚠ You are running a bridge — adding an exit relay will${NC}"
echo -e " ${YELLOW}make your IP publicly visible AND the source of exit traffic.${NC}"
fi
echo ""
echo -e " ${RED}${BOLD}╔═══════════════════════════════════════════════════════════╗${NC}"
echo -e " ${RED}${BOLD}║ ⚠ EXIT RELAY WARNING ⚠ ║${NC}"
echo -e " ${RED}${BOLD}╠═══════════════════════════════════════════════════════════╣${NC}"
echo -e " ${RED}║ Your IP will appear as the source of ALL traffic ║${NC}"
echo -e " ${RED}║ exiting through this relay. This means: ║${NC}"
echo -e " ${RED}║ ║${NC}"
echo -e " ${RED}║ • You WILL receive abuse complaints (DMCA, hacking, etc) ║${NC}"
echo -e " ${RED}║ • Law enforcement may contact you about user traffic ║${NC}"
echo -e " ${RED}║ • Some ISPs/hosts explicitly prohibit exit relays ║${NC}"
echo -e " ${RED}║ • Your IP may be blacklisted by some services ║${NC}"
echo -e " ${RED}║ ║${NC}"
echo -e " ${RED}║ Only run an exit relay if: ║${NC}"
echo -e " ${RED}║ • Your ISP/hosting provider explicitly allows it ║${NC}"
echo -e " ${RED}║ • You understand the legal implications ║${NC}"
echo -e " ${RED}║ • You use a dedicated IP (not your personal one) ║${NC}"
echo -e " ${RED}${BOLD}╚═══════════════════════════════════════════════════════════╝${NC}"
echo ""
read -p " Type 'I UNDERSTAND' to proceed (or anything else to cancel): " _confirm < /dev/tty || continue
if [ "$_confirm" = "I UNDERSTAND" ]; then
_new_type="exit"
echo ""
echo -e " ${BOLD}Exit policy:${NC}"
echo -e " ${GREEN}1.${NC} Reduced — web only (ports 80, 443) ${DIM}(recommended)${NC}"
echo -e " ${GREEN}2.${NC} Default — Tor's standard exit policy"
echo -e " ${GREEN}3.${NC} Full — all traffic ${RED}(most abuse complaints)${NC}"
read -p " policy [1-3, default=1]: " _pol < /dev/tty || true
case "${_pol:-1}" in
2) EXIT_POLICY="default" ;;
3) EXIT_POLICY="full" ;;
*) EXIT_POLICY="reduced" ;;
esac
else
log_info "Exit relay cancelled."
continue
fi
;;
*) log_warn "Invalid choice"; continue ;;
esac
CONTAINER_COUNT=$_new
eval "RELAY_TYPE_${_new}='${_new_type}'"
# Update main RELAY_TYPE if coming from proxy-only mode
if [ "$RELAY_TYPE" = "none" ] || [ -z "$RELAY_TYPE" ]; then
RELAY_TYPE="$_new_type"
fi
save_settings
generate_torrc $_new
local _new_or=$(get_container_orport $_new)
local _new_pt=$(get_container_ptport $_new)
echo ""
log_success "Container ${_new} added (type: ${_new_type})"
echo -e " ORPort: ${_new_or}, PTPort: ${_new_pt}"
echo ""
echo -e " ${YELLOW}⚠ Ports to forward (if running from home):${NC}"
echo -e " ${GREEN}${_new_or} TCP${NC} (ORPort)"
[ "$_new_type" = "bridge" ] && echo -e " ${GREEN}${_new_pt} TCP${NC} (obfs4)"
echo ""
read -p " Start container now? [Y/n] " _start < /dev/tty || true
if [[ ! "$_start" =~ ^[Nn]$ ]]; then
local _img=$(get_docker_image $_new)
log_info "Pulling image (${_img})..."
docker pull "$_img" || { log_error "Failed to pull image"; continue; }
run_relay_container $_new
# Restart tracker to pick up new container
stop_tracker_service
setup_tracker_service
fi
fi
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
r|R)
if [ "${CONTAINER_COUNT:-1}" -le 1 ]; then
log_warn "Cannot remove the last container"
else
local _last=${CONTAINER_COUNT}
local _lname=$(get_container_name $_last)
local _ltype=$(get_container_relay_type $_last)
echo ""
echo -e " Remove container ${_last} (${_lname}, type: ${_ltype})?"
read -p " Confirm [y/N]: " _rc < /dev/tty || true
if [[ "$_rc" =~ ^[Yy]$ ]]; then
# Stop and remove the container
docker stop --timeout 30 "$_lname" 2>/dev/null || true
docker rm "$_lname" 2>/dev/null || true
# Clear per-container vars
eval "unset RELAY_TYPE_${_last}"
eval "unset BANDWIDTH_${_last}"
eval "unset ORPORT_${_last}"
eval "unset PT_PORT_${_last}"
CONTAINER_COUNT=$((_last - 1))
save_settings
# Restart tracker
stop_tracker_service
setup_tracker_service
log_success "Container ${_last} removed"
fi
fi
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
c|C)
echo ""
read -p " Which container to change? [1-${CONTAINER_COUNT:-1}]: " _ci < /dev/tty || continue
if ! [[ "$_ci" =~ ^[1-5]$ ]] || [ "$_ci" -gt "${CONTAINER_COUNT:-1}" ]; then
log_warn "Invalid container number"
continue
fi
local _old_rt=$(get_container_relay_type $_ci)
echo ""
echo -e " Container ${_ci} is currently: ${BOLD}${_old_rt}${NC}"
echo ""
echo -e " ${GREEN}1.${NC} 🌉 Bridge — IP unlisted, ${CYAN}1-10 Mbps${NC}"
echo -e " ${GREEN}2.${NC} 🔁 Middle — IP public, ${CYAN}10-50+ Mbps${NC}"
echo -e " ${GREEN}3.${NC} 🚪 Exit — IP is traffic source, ${CYAN}20-100+ Mbps${NC}"
echo ""
read -p " new type [1-3]: " _nt < /dev/tty || continue
local _change_type=""
# Check if other containers are bridges
local _other_bridge=false
for _oi in $(seq 1 ${CONTAINER_COUNT:-1}); do
[ "$_oi" = "$_ci" ] && continue
[ "$(get_container_relay_type $_oi)" = "bridge" ] && _other_bridge=true
done
case "$_nt" in
1) _change_type="bridge" ;;
2)
_change_type="middle"
if [ "$_old_rt" = "bridge" ] || [ "$_other_bridge" = "true" ]; then
echo ""
echo -e " ${YELLOW}${BOLD}⚠ Warning: This will make your IP publicly visible${NC}"
echo -e " ${YELLOW}in the Tor consensus. If you also run a bridge on this${NC}"
echo -e " ${YELLOW}IP, the bridge's unlisted status is compromised.${NC}"
echo ""
read -p " Continue? [y/N] " _mwarn < /dev/tty || continue
if [[ ! "$_mwarn" =~ ^[Yy]$ ]]; then
log_info "Cancelled."
continue
fi
fi
;;
3)
if [ "$_old_rt" = "bridge" ] || [ "$_other_bridge" = "true" ]; then
echo ""
echo -e " ${YELLOW}${BOLD}⚠ You have a bridge on this IP — an exit relay will${NC}"
echo -e " ${YELLOW}expose your IP publicly AND attract abuse complaints.${NC}"
fi
echo ""
echo -e " ${RED}${BOLD}⚠ EXIT RELAY WARNING${NC}"
echo -e " ${RED}Your IP will be the apparent source of all exiting traffic.${NC}"
echo -e " ${RED}You WILL receive abuse complaints. Your IP may be blacklisted.${NC}"
echo -e " ${RED}Only proceed if your ISP/host allows it and you understand the risks.${NC}"
echo ""
read -p " Type 'I UNDERSTAND' to proceed: " _ec < /dev/tty || continue
if [ "$_ec" = "I UNDERSTAND" ]; then
_change_type="exit"
echo ""
echo -e " ${BOLD}Exit policy:${NC}"
echo -e " ${GREEN}1.${NC} Reduced — web only (80, 443) ${DIM}(recommended)${NC}"
echo -e " ${GREEN}2.${NC} Default — Tor standard"
echo -e " ${GREEN}3.${NC} Full — all traffic ${RED}(most complaints)${NC}"
read -p " policy [1-3, default=1]: " _ep < /dev/tty || true
case "${_ep:-1}" in
2) EXIT_POLICY="default" ;;
3) EXIT_POLICY="full" ;;
*) EXIT_POLICY="reduced" ;;
esac
else
log_info "Cancelled."
continue
fi
;;
*) log_warn "Invalid choice"; continue ;;
esac
if [ "$_change_type" = "$_old_rt" ]; then
log_info "Type unchanged"
else
eval "RELAY_TYPE_${_ci}='${_change_type}'"
# Also update global RELAY_TYPE if container 1
[ "$_ci" = "1" ] && RELAY_TYPE="$_change_type"
save_settings
generate_torrc $_ci
log_success "Container ${_ci} changed to: ${_change_type}"
echo ""
local _cn=$(get_container_name $_ci)
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "^${_cn}$"; then
read -p " Restart container to apply? [Y/n] " _rs < /dev/tty || true
if [[ ! "$_rs" =~ ^[Nn]$ ]]; then
docker stop --timeout 30 "$_cn" 2>/dev/null || true
docker rm "$_cn" 2>/dev/null || true
local _img=$(get_docker_image $_ci)
log_info "Pulling image (${_img})..."
docker pull "$_img" || { log_error "Failed to pull image"; continue; }
run_relay_container $_ci
stop_tracker_service
setup_tracker_service
fi
fi
fi
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
0|q|Q)
return
;;
"")
redraw=false
;;
*)
echo -e " ${RED}Invalid choice${NC}"
redraw=false
;;
esac
done
}
show_settings_menu() {
local redraw=true
while true; do
if [ "$redraw" = "true" ]; then
clear
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} SETTINGS & TOOLS ${NC}"
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
fi
redraw=true
echo ""
echo -e " ${GREEN}1.${NC} 📋 View Status"
echo -e " ${GREEN}2.${NC} 💾 Backup Relay Keys"
echo -e " ${GREEN}3.${NC} 📥 Restore Relay Keys"
echo -e " ${GREEN}4.${NC} 🔄 Restart Tracker Service"
echo -e " ${GREEN}5.${NC} Version Info"
echo -e " ${GREEN}6.${NC} 🐳 Manage Containers"
echo -e " ${GREEN}7.${NC} 🗑 Uninstall"
echo -e " ${GREEN}0.${NC} ← Back"
echo ""
read -p " Enter choice: " choice < /dev/tty || { echo ""; break; }
case "$choice" in
1)
show_status
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
2)
backup_keys
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
3)
restore_keys
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
4)
stop_tracker_service
setup_tracker_service
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
5)
show_version
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
6)
manage_containers
;;
7)
uninstall
exit 0
;;
0|q|Q)
return
;;
"")
redraw=false
;;
*)
echo -e " ${RED}Invalid choice${NC}"
redraw=false
;;
esac
done
}
#═══════════════════════════════════════════════════════════════════════
# Management Script Generation
#═══════════════════════════════════════════════════════════════════════
create_management_script() {
log_info "Creating management script..."
local script_url="https://git.samnet.dev/SamNet-dev/torware/raw/branch/main/torware.sh"
local source_file="$0"
local dest_file="$INSTALL_DIR/torware"
# Resolve symlinks to check if source and dest are the same file
local resolved_source resolved_dest
resolved_source=$(readlink -f "$source_file" 2>/dev/null || echo "$source_file")
resolved_dest=$(readlink -f "$dest_file" 2>/dev/null || echo "$dest_file")
# If source and dest are the same file, or running from pipe/stdin, download from server
if [ "$resolved_source" = "$resolved_dest" ] || [ ! -f "$source_file" ] || [ ! -r "$source_file" ]; then
log_info "Downloading latest version..."
if ! curl -sL "$script_url" -o "$dest_file" 2>/dev/null; then
log_error "Could not download management script."
return 1
fi
else
# Copy from local file (fresh install from downloaded script)
if ! cp "$source_file" "$dest_file"; then
log_error "Failed to copy management script"
return 1
fi
fi
chmod +x "$INSTALL_DIR/torware"
ln -sf "$INSTALL_DIR/torware" /usr/local/bin/torware
# Verify symlink
if [ ! -x /usr/local/bin/torware ]; then
log_warn "Symlink creation may have failed — try running 'torware' from $INSTALL_DIR/torware directly"
fi
log_success "Management CLI installed: /usr/local/bin/torware"
}
#═══════════════════════════════════════════════════════════════════════
# Installation Summary
#═══════════════════════════════════════════════════════════════════════
print_summary() {
echo ""
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ 🧅 TORWARE INSTALLED SUCCESSFULLY! ║${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}╠═══════════════════════════════════════════════════════════════════╣${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ Your Tor relay is now running! ║${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ Commands: ║${NC}"
echo -e "${GREEN}║ torware menu - Interactive management menu ║${NC}"
echo -e "${GREEN}║ torware status - View relay status ║${NC}"
echo -e "${GREEN}║ torware health - Run health diagnostics ║${NC}"
echo -e "${GREEN}║ torware fingerprint - Show relay fingerprint ║${NC}"
local _s_any_bridge=false
for _si in $(seq 1 ${CONTAINER_COUNT:-1}); do
[ "$(get_container_relay_type $_si)" = "bridge" ] && _s_any_bridge=true
done
if [ "$_s_any_bridge" = "true" ]; then
echo -e "${GREEN}║ torware bridge-line - Show bridge line for sharing ║${NC}"
fi
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
echo -e "${GREEN}║ torware snowflake - Snowflake proxy status ║${NC}"
fi
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
echo -e "${GREEN}║ torware unbounded - Unbounded (Lantern) proxy status ║${NC}"
fi
if [ "$MTPROXY_ENABLED" = "true" ]; then
echo -e "${GREEN}║ torware mtproxy - MTProxy (Telegram) proxy status ║${NC}"
fi
echo -e "${GREEN}║ torware logs - Stream container logs ║${NC}"
echo -e "${GREEN}║ torware --help - Full command reference ║${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ Note: It may take a few minutes for your relay to bootstrap ║${NC}"
echo -e "${GREEN}║ and appear in the Tor consensus. ║${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════════╝${NC}"
echo ""
# Show firewall reminder
local count=${CONTAINER_COUNT:-1}
echo -e "${YELLOW} ⚠ If you have a firewall enabled, make sure these ports are open:${NC}"
for i in $(seq 1 $count); do
local orport=$(get_container_orport $i)
local _rt=$(get_container_relay_type $i)
if [ "$_rt" = "bridge" ]; then
local ptport=$(get_container_ptport $i)
echo -e " Relay $i (bridge): ORPort ${GREEN}${orport}/tcp${NC}, obfs4 ${GREEN}${ptport}/tcp${NC}"
else
echo -e " Relay $i ($_rt): ORPort ${GREEN}${orport}/tcp${NC}"
fi
done
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
echo -e " Snowflake: Uses ${GREEN}--network host${NC} — no port forwarding needed (WebRTC auto-traversal)"
fi
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
echo -e " Unbounded: Uses ${GREEN}--network host${NC} — no port forwarding needed (WebRTC auto-traversal)"
fi
if [ "$MTPROXY_ENABLED" = "true" ]; then
echo -e " MTProxy: Port ${GREEN}${MTPROXY_PORT}/tcp${NC}"
fi
echo ""
}
#═══════════════════════════════════════════════════════════════════════
# MTProxy Menu (Telegram Proxy Settings)
#═══════════════════════════════════════════════════════════════════════
show_mtproxy_menu() {
if [ "$MTPROXY_ENABLED" = "true" ]; then
echo ""
echo -e "${CYAN}═══ MTProxy (Telegram) ═══${NC}"
echo ""
local _mtpn="$MTPROXY_CONTAINER"
local _mtps="${RED}Stopped${NC}"
is_mtproxy_running && _mtps="${GREEN}Running${NC}"
local _mtp_stats=$(get_mtproxy_stats 2>/dev/null)
local _mtp_in=$(echo "$_mtp_stats" | awk '{print $1}')
local _mtp_out=$(echo "$_mtp_stats" | awk '{print $2}')
echo -e " Status: [${_mtps}]"
echo -e " Traffic: ↓ ${CYAN}$(format_bytes ${_mtp_in:-0})${NC}${CYAN}$(format_bytes ${_mtp_out:-0})${NC}"
echo -e " Port: ${CYAN}${MTPROXY_PORT}${NC}"
echo -e " FakeTLS: ${CYAN}${MTPROXY_DOMAIN}${NC}"
echo -e " Max Conns: ${CYAN}${MTPROXY_CONCURRENCY:-8192}${NC}"
if [ -n "$MTPROXY_BLOCKLIST_COUNTRIES" ]; then
echo -e " Geo-Block: ${YELLOW}${MTPROXY_BLOCKLIST_COUNTRIES}${NC}"
else
echo -e " Geo-Block: ${DIM}None${NC}"
fi
echo -e " CPU/RAM: ${CYAN}${MTPROXY_CPUS:-0.5}${NC} / ${CYAN}${MTPROXY_MEMORY:-128m}${NC}"
echo ""
echo -e " 1. Show proxy link & QR code"
echo -e " 2. Restart MTProxy"
echo -e " 3. Stop MTProxy"
echo -e " 4. Disable MTProxy"
echo -e " 5. Change settings (port/domain/resources)"
echo -e " 6. Regenerate secret"
echo -e " 7. Security settings (connection limit, geo-block)"
echo -e " ${RED}8. Remove MTProxy (stop, remove container & image)${NC}"
if [ "$TELEGRAM_ENABLED" = "true" ]; then
echo -e " 9. 📱 Send link via Telegram"
fi
echo -e " 0. Back"
read -p " choice: " mtp_choice < /dev/tty || true
case "${mtp_choice:-0}" in
1)
echo ""
show_mtproxy_qr
echo ""
echo -e " ${BOLD}tg:// link (for sharing):${NC}"
echo -e " ${CYAN}$(get_mtproxy_link)${NC}"
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
echo ""
;;
2) restart_mtproxy_container ;;
3)
stop_mtproxy_container
log_warn "MTProxy stopped but still enabled. It will restart on next 'torware start'. Use option 4 to disable permanently."
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
echo ""
;;
4)
MTPROXY_ENABLED="false"
stop_mtproxy_container
save_settings
log_success "MTProxy disabled"
;;
5)
echo ""
echo -e " ${BOLD}MTProxy Settings${NC}"
echo ""
local _mtp_old_port="$MTPROXY_PORT"
echo -e " ${DIM}Note: MTProxy uses host networking. Choose an available port.${NC}"
echo -e " ${DIM}Common choices: 443, 8443, 8080, 9443${NC}"
read -p " Port [${MTPROXY_PORT}]: " _mtp_port < /dev/tty || true
if [ -n "$_mtp_port" ]; then
if [[ "$_mtp_port" =~ ^[0-9]+$ ]]; then
# Check if port is available
if ss -tln 2>/dev/null | grep -q ":${_mtp_port} " || netstat -tln 2>/dev/null | grep -q ":${_mtp_port} "; then
log_warn "Port ${_mtp_port} is already in use. Please choose another port."
else
MTPROXY_PORT="$_mtp_port"
fi
else
log_warn "Invalid port"
fi
fi
echo ""
echo -e " ${DIM}FakeTLS domain (traffic disguised as HTTPS to this domain):${NC}"
echo -e " ${DIM} 1. cloudflare.com${NC}"
echo -e " ${DIM} 2. google.com${NC}"
echo -e " ${DIM} 3. Keep current (${MTPROXY_DOMAIN})${NC}"
echo -e " ${DIM} 4. Custom domain${NC}"
read -p " Domain choice [3]: " _mtp_dom < /dev/tty || true
case "${_mtp_dom:-3}" in
1) MTPROXY_DOMAIN="cloudflare.com"; MTPROXY_SECRET="" ;;
2) MTPROXY_DOMAIN="google.com"; MTPROXY_SECRET="" ;;
4)
read -p " Enter domain: " _mtp_cdom < /dev/tty || true
if [ -n "$_mtp_cdom" ]; then
MTPROXY_DOMAIN="$_mtp_cdom"
MTPROXY_SECRET=""
fi
;;
esac
echo ""
read -p " CPU cores [${MTPROXY_CPUS:-0.5}]: " _mtp_cpu < /dev/tty || true
read -p " RAM limit [${MTPROXY_MEMORY:-128m}]: " _mtp_mem < /dev/tty || true
if [ -n "$_mtp_cpu" ]; then
[[ "$_mtp_cpu" =~ ^[0-9]+\.?[0-9]*$ ]] && MTPROXY_CPUS="$_mtp_cpu" || log_warn "Invalid CPU"
fi
if [ -n "$_mtp_mem" ]; then
[[ "$_mtp_mem" =~ ^[0-9]+[mMgG]$ ]] && MTPROXY_MEMORY="$(echo "$_mtp_mem" | tr '[:upper:]' '[:lower:]')" || log_warn "Invalid RAM"
fi
save_settings
log_success "MTProxy settings updated"
# Warn if port changed - URL will be different
if [ "$MTPROXY_PORT" != "$_mtp_old_port" ]; then
echo ""
log_warn "Port changed from ${_mtp_old_port} to ${MTPROXY_PORT}"
log_warn "The proxy URL has changed! Old links will no longer work."
echo -e " ${DIM}Users will need the new link to connect.${NC}"
fi
echo ""
read -p " Restart MTProxy to apply? [Y/n] " _mtp_rs < /dev/tty || true
if [[ ! "$_mtp_rs" =~ ^[Nn]$ ]]; then
restart_mtproxy_container
# If port changed and Telegram enabled, offer to send new link
if [ "$MTPROXY_PORT" != "$_mtp_old_port" ] && [ "$TELEGRAM_ENABLED" = "true" ]; then
echo ""
read -p " Send new proxy link to Telegram? [Y/n] " _mtp_tg < /dev/tty || true
if [[ ! "$_mtp_tg" =~ ^[Nn]$ ]]; then
telegram_notify_mtproxy_started
log_success "New proxy link sent to Telegram"
fi
fi
fi
;;
6)
echo ""
read -p " Regenerate secret? Old links will stop working. [y/N] " _mtp_regen < /dev/tty || true
if [[ "$_mtp_regen" =~ ^[Yy]$ ]]; then
MTPROXY_SECRET=""
save_settings
restart_mtproxy_container
log_success "New secret generated"
# Offer to send new link via Telegram
if [ "$TELEGRAM_ENABLED" = "true" ]; then
echo ""
read -p " Send new proxy link to Telegram? [Y/n] " _mtp_tg < /dev/tty || true
if [[ ! "$_mtp_tg" =~ ^[Nn]$ ]]; then
telegram_notify_mtproxy_started
log_success "New proxy link sent to Telegram"
fi
fi
fi
;;
7)
echo ""
echo -e " ${BOLD}Security Settings${NC}"
echo ""
echo -e " ${DIM}Max concurrent connections (default: 8192):${NC}"
read -p " Max connections [${MTPROXY_CONCURRENCY:-8192}]: " _mtp_conc < /dev/tty || true
if [ -n "$_mtp_conc" ]; then
[[ "$_mtp_conc" =~ ^[0-9]+$ ]] && MTPROXY_CONCURRENCY="$_mtp_conc" || log_warn "Invalid number"
fi
echo ""
echo -e " ${BOLD}Geo-blocking (block connections from specific countries)${NC}"
echo -e " ${DIM}Block countries that don't need censorship circumvention${NC}"
echo -e " ${DIM}(reduces abuse from data centers in open-internet regions)${NC}"
echo ""
if [ -n "$MTPROXY_BLOCKLIST_COUNTRIES" ]; then
echo -e " Current blocklist: ${YELLOW}$MTPROXY_BLOCKLIST_COUNTRIES${NC}"
else
echo -e " Current blocklist: ${GREEN}None (all allowed)${NC}"
fi
echo ""
echo -e " ${DIM}Available countries to block:${NC}"
echo -e " ${GREEN}1.${NC} US - United States"
echo -e " ${GREEN}2.${NC} DE - Germany"
echo -e " ${GREEN}3.${NC} NL - Netherlands"
echo -e " ${GREEN}4.${NC} FR - France"
echo -e " ${GREEN}5.${NC} GB - United Kingdom"
echo -e " ${GREEN}6.${NC} SG - Singapore"
echo -e " ${GREEN}7.${NC} JP - Japan"
echo -e " ${GREEN}8.${NC} CA - Canada"
echo -e " ${GREEN}9.${NC} AU - Australia"
echo -e " ${GREEN}10.${NC} KR - South Korea"
echo -e " ${GREEN}11.${NC} CN - China"
echo -e " ${GREEN}12.${NC} RU - Russia"
echo ""
echo -e " ${DIM}Enter numbers separated by commas (e.g., 1,2,3) or 'clear' to disable${NC}"
read -p " Select countries to block: " _mtp_block_sel < /dev/tty || true
if [ -n "$_mtp_block_sel" ]; then
if [ "$_mtp_block_sel" = "clear" ] || [ "$_mtp_block_sel" = "none" ] || [ "$_mtp_block_sel" = "0" ]; then
MTPROXY_BLOCKLIST_COUNTRIES=""
log_info "Geo-blocking disabled"
else
# Map numbers to country codes
local _geo_codes=""
local _geo_map=("" "US" "DE" "NL" "FR" "GB" "SG" "JP" "CA" "AU" "KR" "CN" "RU")
for _num in $(echo "$_mtp_block_sel" | tr ',' ' '); do
if [[ "$_num" =~ ^[0-9]+$ ]] && [ "$_num" -ge 1 ] && [ "$_num" -le 12 ]; then
[ -n "$_geo_codes" ] && _geo_codes+=","
_geo_codes+="${_geo_map[$_num]}"
fi
done
if [ -n "$_geo_codes" ]; then
MTPROXY_BLOCKLIST_COUNTRIES="$_geo_codes"
log_info "Will block: $_geo_codes"
fi
fi
fi
save_settings
log_success "Security settings updated"
echo ""
read -p " Restart MTProxy to apply? [Y/n] " _mtp_sec_rs < /dev/tty || true
if [[ ! "$_mtp_sec_rs" =~ ^[Nn]$ ]]; then
restart_mtproxy_container
fi
;;
8)
echo ""
read -p " Are you sure? This will remove the container and image. [y/N] " _mtp_rm < /dev/tty || true
if [[ "$_mtp_rm" =~ ^[Yy]$ ]]; then
MTPROXY_ENABLED="false"
stop_mtproxy_container
docker rm -f "$MTPROXY_CONTAINER" 2>/dev/null || true
docker rmi "$MTPROXY_IMAGE" 2>/dev/null || true
rm -rf "$INSTALL_DIR/mtproxy" 2>/dev/null || true
MTPROXY_SECRET=""
save_settings
log_success "MTProxy fully removed"
else
log_info "Cancelled"
fi
;;
9)
echo ""
if [ "$TELEGRAM_ENABLED" != "true" ]; then
log_warn "Telegram bot is not enabled."
echo -e " ${DIM}Enable Telegram notifications first from the main menu.${NC}"
elif ! is_mtproxy_running; then
log_warn "MTProxy is not running. Start it first."
else
echo -ne " Sending link & QR to Telegram... "
if telegram_notify_mtproxy_started; then
echo -e "${GREEN}✓ Sent!${NC}"
else
echo -e "${RED}✗ Failed${NC}"
fi
fi
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
;;
esac
else
echo ""
echo -e " ${BOLD}📱 MTProxy (Telegram Proxy)${NC}"
echo ""
echo -e " Run a proxy that helps censored users access Telegram."
echo -e " Uses FakeTLS to disguise traffic as normal HTTPS."
echo -e " Very lightweight (~50MB RAM). Share link/QR with users."
echo ""
read -p " Enable MTProxy? [y/N] " mtp_en < /dev/tty || true
if [[ "$mtp_en" =~ ^[Yy]$ ]]; then
MTPROXY_ENABLED="true"
echo ""
echo -e " ${DIM}FakeTLS domain (traffic disguised as HTTPS to this domain):${NC}"
echo -e " ${DIM} 1. cloudflare.com (recommended)${NC}"
echo -e " ${DIM} 2. google.com${NC}"
echo -e " ${DIM} 3. Custom domain${NC}"
read -p " Domain choice [1]: " _mtp_dom < /dev/tty || true
case "${_mtp_dom:-1}" in
2) MTPROXY_DOMAIN="google.com" ;;
3)
read -p " Enter domain: " _mtp_cdom < /dev/tty || true
MTPROXY_DOMAIN="${_mtp_cdom:-cloudflare.com}"
;;
*) MTPROXY_DOMAIN="cloudflare.com" ;;
esac
echo ""
echo -e " ${DIM}MTProxy uses host networking. Choose an available port.${NC}"
echo -e " ${DIM}Common choices: 443, 8443, 8080, 9443${NC}"
read -p " Port [8443]: " _mtp_port < /dev/tty || true
_mtp_port="${_mtp_port:-8443}"
if [[ "$_mtp_port" =~ ^[0-9]+$ ]]; then
if ss -tln 2>/dev/null | grep -q ":${_mtp_port} " || netstat -tln 2>/dev/null | grep -q ":${_mtp_port} "; then
log_warn "Port ${_mtp_port} appears to be in use. Trying anyway..."
fi
MTPROXY_PORT="$_mtp_port"
else
MTPROXY_PORT=8443
fi
MTPROXY_SECRET=""
save_settings
run_mtproxy_container
fi
fi
read -n 1 -s -r -p " Press any key to continue..." < /dev/tty
}
#═══════════════════════════════════════════════════════════════════════
# CLI Entry Point
#═══════════════════════════════════════════════════════════════════════
cli_main() {
load_settings
case "${1:-}" in
start)
start_relay
[ "$SNOWFLAKE_ENABLED" = "true" ] && start_snowflake_container
[ "$UNBOUNDED_ENABLED" = "true" ] && start_unbounded_container
[ "$MTPROXY_ENABLED" = "true" ] && start_mtproxy_container
;;
stop)
stop_relay
if [ "$RELAY_TYPE" = "none" ]; then
stop_snowflake_container
stop_unbounded_container
stop_mtproxy_container
fi
;;
restart)
restart_relay
[ "$SNOWFLAKE_ENABLED" = "true" ] && restart_snowflake_container
[ "$UNBOUNDED_ENABLED" = "true" ] && restart_unbounded_container
[ "$MTPROXY_ENABLED" = "true" ] && restart_mtproxy_container
;;
status)
show_status
;;
dashboard|dash)
show_dashboard
;;
stats)
show_advanced_stats
;;
peers)
show_peers
;;
menu)
show_menu
;;
logs)
show_logs
;;
health)
health_check
;;
doctor)
run_doctor
;;
graphs|graph|bandwidth)
show_bandwidth_graphs
;;
fingerprint)
show_fingerprint
;;
bridge-line|bridgeline)
show_bridge_line
;;
backup)
backup_keys
;;
restore)
restore_keys
;;
snowflake)
if [ "$SNOWFLAKE_ENABLED" = "true" ]; then
echo -e " Snowflake proxy: ${GREEN}enabled${NC}"
if is_snowflake_running; then
echo -e " Status: ${GREEN}running${NC}"
local sf_s=$(get_snowflake_stats 2>/dev/null)
echo -e " Connections: $(echo "$sf_s" | awk '{print $1}')"
echo -e " Traffic: ↓ $(format_bytes $(echo "$sf_s" | awk '{print $2}'))$(format_bytes $(echo "$sf_s" | awk '{print $3}'))"
else
echo -e " Status: ${RED}stopped${NC}"
echo -e " Run 'torware start' to start all containers"
fi
else
echo -e " Snowflake proxy: ${DIM}disabled${NC}"
echo -e " Enable via 'torware menu' > Snowflake option"
fi
;;
unbounded)
if [ "$UNBOUNDED_ENABLED" = "true" ]; then
echo -e " Unbounded proxy (Lantern): ${GREEN}enabled${NC}"
if is_unbounded_running; then
echo -e " Status: ${GREEN}running${NC}"
local ub_s=$(get_unbounded_stats 2>/dev/null)
echo -e " Live connections: $(echo "$ub_s" | awk '{print $1}') | All-time: $(echo "$ub_s" | awk '{print $2}')"
else
echo -e " Status: ${RED}stopped${NC}"
echo -e " Run 'torware start' to start all containers"
fi
else
echo -e " Unbounded proxy (Lantern): ${DIM}disabled${NC}"
echo -e " Enable via 'torware menu' > Unbounded option"
fi
;;
mtproxy)
show_mtproxy_menu
;;
version|--version|-v)
show_version
;;
uninstall)
uninstall
;;
help|--help|-h)
show_help
;;
*)
show_help
;;
esac
}
#═══════════════════════════════════════════════════════════════════════
# Main Installer
#═══════════════════════════════════════════════════════════════════════
show_usage() {
echo "Usage: $0 [--force]"
echo ""
echo "Options:"
echo " --force Force reinstall even if already installed"
echo " --help Show this help message"
}
main() {
# Parse arguments — extract flags first, then dispatch commands
local _args=()
for _arg in "$@"; do
case "$_arg" in
--force) FORCE_REINSTALL=true ;;
--help|-h) show_usage; exit 0 ;;
*) _args+=("$_arg") ;;
esac
done
# Dispatch CLI commands if any non-flag args remain
if [ ${#_args[@]} -gt 0 ]; then
case "${_args[0]}" in
start|stop|restart|status|dashboard|dash|stats|peers|menu|logs|health|fingerprint|bridge-line|bridgeline|backup|restore|snowflake|unbounded|mtproxy|version|--version|-v|uninstall|help)
cli_main "${_args[@]}"
exit $?
;;
esac
fi
# If we get here, we're in installer mode
check_root
print_header
detect_os
echo ""
# Check if already installed
if [ -f "$INSTALL_DIR/settings.conf" ] && [ "$FORCE_REINSTALL" != "true" ]; then
echo -e "${CYAN} Torware is already installed.${NC}"
echo ""
echo " What would you like to do?"
echo ""
echo " 1. 📊 Open management menu"
echo " 2. ⬆ Update Torware (keeps containers & settings)"
echo " 3. 🔄 Reinstall (fresh install)"
echo " 4. 🗑 Uninstall"
echo " 0. 🚪 Exit"
echo ""
read -p " Enter choice: " choice < /dev/tty || { echo -e "\n ${RED}Input error.${NC}"; exit 1; }
case "$choice" in
1)
echo -e "${CYAN}Opening management menu...${NC}"
create_management_script
exec "$INSTALL_DIR/torware" menu
;;
2)
echo ""
log_info "Updating Torware script..."
create_management_script
log_success "Torware updated. Your containers and settings are unchanged."
echo ""
echo -e " ${DIM}New features available in the management menu.${NC}"
echo ""
read -n 1 -s -r -p " Press any key to open the menu..." < /dev/tty
exec "$INSTALL_DIR/torware" menu
;;
3)
echo ""
log_info "Starting fresh reinstall..."
;;
4)
uninstall
exit 0
;;
0)
echo "Exiting."
exit 0
;;
*)
echo -e "${RED}Invalid choice.${NC}"
exit 1
;;
esac
fi
# Check dependencies
log_info "Checking dependencies..."
check_dependencies
echo ""
# Interactive settings
prompt_relay_settings
echo ""
echo -e "${CYAN}Starting installation...${NC}"
echo ""
#───────────────────────────────────────────────────────────────
# Installation Steps
#───────────────────────────────────────────────────────────────
# Step 1: Install Docker
log_info "Step 1/5: Installing Docker..."
if ! install_docker; then
log_error "Docker installation failed. Cannot continue."
exit 1
fi
echo ""
# Step 2: Check for backup restore
log_info "Step 2/5: Checking for previous relay identity..."
check_and_offer_backup_restore || true
echo ""
# Step 3: Start relay containers
log_info "Step 3/5: Starting Torware..."
# Clean up any existing containers (including Snowflake)
for i in $(seq 1 5); do
local cname=$(get_container_name $i)
docker stop --timeout 30 "$cname" 2>/dev/null || true
docker rm -f "$cname" 2>/dev/null || true
done
for _sfi in 1 2; do
local _sfn=$(get_snowflake_name $_sfi)
docker stop --timeout 10 "$_sfn" 2>/dev/null || true
docker rm -f "$_sfn" 2>/dev/null || true
done
run_all_containers
echo ""
# Step 4: Save settings and auto-start
log_info "Step 4/5: Setting up auto-start & tracker..."
if ! save_settings; then
log_error "Failed to save settings."
exit 1
fi
setup_autostart
setup_tracker_service 2>/dev/null || true
echo ""
# Step 5: Create management CLI
log_info "Step 5/5: Creating management script..."
create_management_script
# Create stats directory
mkdir -p "$STATS_DIR"
print_summary
read -p "Open management menu now? [Y/n] " open_menu < /dev/tty || true
if [[ ! "$open_menu" =~ ^[Nn]$ ]]; then
"$INSTALL_DIR/torware" menu
fi
}
main "$@"