9909 lines
437 KiB
Bash
9909 lines
437 KiB
Bash
#!/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 "$@"
|