2696 lines
112 KiB
Bash
2696 lines
112 KiB
Bash
#!/bin/bash
|
||
#
|
||
# ╔═══════════════════════════════════════════════════════════════════╗
|
||
# ║ 🚀 PSIPHON CONDUIT MANAGER v1.0.2 ║
|
||
# ║ ║
|
||
# ║ One-click setup for Psiphon Conduit ║
|
||
# ║ ║
|
||
# ║ • Installs Docker (if needed) ║
|
||
# ║ • Runs Conduit in Docker with live stats ║
|
||
# ║ • Auto-start on boot via systemd/OpenRC/SysVinit ║
|
||
# ║ • Easy management via CLI or interactive menu ║
|
||
# ║ ║
|
||
# ║ GitHub: https://github.com/Psiphon-Inc/conduit ║
|
||
# ╚═══════════════════════════════════════════════════════════════════╝
|
||
# core engine: https://github.com/Psiphon-Labs/psiphon-tunnel-core
|
||
# Usage:
|
||
# curl -sL https://raw.githubusercontent.com/SamNet-dev/conduit-manager/main/conduit.sh | sudo bash
|
||
#
|
||
# Reference: https://github.com/ssmirr/conduit/releases/tag/d8522a8
|
||
# Conduit CLI options:
|
||
# -m, --max-clients int maximum number of proxy clients (1-1000) (default 200)
|
||
# -b, --bandwidth float bandwidth limit per peer in Mbps (1-40, or -1 for unlimited) (default 5)
|
||
# -v, --verbose increase verbosity (-v for verbose, -vv for debug)
|
||
#
|
||
|
||
set -e
|
||
|
||
# Ensure we're running in bash (not sh/dash)
|
||
if [ -z "$BASH_VERSION" ]; then
|
||
echo "Error: This script requires bash. Please run with: bash $0"
|
||
exit 1
|
||
fi
|
||
|
||
VERSION="1.0.2"
|
||
CONDUIT_IMAGE="ghcr.io/ssmirr/conduit/conduit:d8522a8"
|
||
INSTALL_DIR="${INSTALL_DIR:-/opt/conduit}"
|
||
BACKUP_DIR="$INSTALL_DIR/backups"
|
||
FORCE_REINSTALL=false
|
||
|
||
# Colors
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
CYAN='\033[0;36m'
|
||
BOLD='\033[1m'
|
||
NC='\033[0m'
|
||
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
# Utility Functions
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
|
||
print_header() {
|
||
echo -e "${CYAN}"
|
||
echo "╔═══════════════════════════════════════════════════════════════════╗"
|
||
echo "║ 🚀 PSIPHON CONDUIT MANAGER v${VERSION} ║"
|
||
echo "╠═══════════════════════════════════════════════════════════════════╣"
|
||
echo "║ Help users access the open internet during shutdowns ║"
|
||
echo "╚═══════════════════════════════════════════════════════════════════╝"
|
||
echo -e "${NC}"
|
||
}
|
||
|
||
log_info() {
|
||
echo -e "${BLUE}[INFO]${NC} $1"
|
||
}
|
||
|
||
log_success() {
|
||
echo -e "${GREEN}[✓]${NC} $1"
|
||
}
|
||
|
||
log_warn() {
|
||
echo -e "${YELLOW}[!]${NC} $1"
|
||
}
|
||
|
||
log_error() {
|
||
echo -e "${RED}[✗]${NC} $1"
|
||
}
|
||
|
||
check_root() {
|
||
if [ "$EUID" -ne 0 ]; then
|
||
log_error "This script must be run as root (use sudo)"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
detect_os() {
|
||
OS="unknown"
|
||
OS_VERSION="unknown"
|
||
OS_FAMILY="unknown"
|
||
HAS_SYSTEMD=false
|
||
PKG_MANAGER="unknown"
|
||
|
||
# Detect OS from /etc/os-release
|
||
if [ -f /etc/os-release ]; then
|
||
. /etc/os-release
|
||
OS="$ID"
|
||
OS_VERSION="${VERSION_ID:-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
|
||
|
||
# Determine OS family and package manager
|
||
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
|
||
|
||
# Check for systemd
|
||
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)
|
||
# Make update failure non-fatal but log it
|
||
apt-get update -q || log_warn "apt-get update failed, attempting to install regardless..."
|
||
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 -Sy --noconfirm "$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() {
|
||
# Check for bash
|
||
if [ "$OS_FAMILY" = "alpine" ]; then
|
||
if ! command -v bash &>/dev/null; then
|
||
log_info "Installing bash (required for this script)..."
|
||
apk add --no-cache bash 2>/dev/null
|
||
fi
|
||
fi
|
||
|
||
# Check for curl
|
||
if ! command -v curl &>/dev/null; then
|
||
install_package curl || log_warn "Could not install curl automatically"
|
||
fi
|
||
|
||
# Check for basic tools
|
||
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
|
||
|
||
# Check for free command
|
||
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
|
||
|
||
# Check for tput (ncurses)
|
||
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
|
||
|
||
# Check for tcpdump
|
||
if ! command -v tcpdump &>/dev/null; then
|
||
install_package tcpdump || log_warn "Could not install tcpdump automatically"
|
||
fi
|
||
|
||
# Check for GeoIP tools
|
||
if ! command -v geoiplookup &>/dev/null; then
|
||
case "$PKG_MANAGER" in
|
||
apt)
|
||
# geoip-bin and geoip-database for newer systems
|
||
install_package geoip-bin || log_warn "Could not install geoip-bin"
|
||
install_package geoip-database || log_warn "Could not install geoip-database"
|
||
;;
|
||
dnf|yum)
|
||
# On RHEL/CentOS
|
||
if ! rpm -q epel-release &>/dev/null; then
|
||
log_info "Enabling EPEL repository for GeoIP..."
|
||
$PKG_MANAGER install -y epel-release &>/dev/null || true
|
||
fi
|
||
install_package GeoIP || log_warn "Could not install GeoIP."
|
||
;;
|
||
pacman) install_package geoip || log_warn "Could not install geoip." ;;
|
||
zypper) install_package GeoIP || log_warn "Could not install GeoIP." ;;
|
||
apk) install_package geoip || log_warn "Could not install geoip." ;;
|
||
*) log_warn "Could not install geoiplookup automatically" ;;
|
||
esac
|
||
fi
|
||
}
|
||
|
||
get_ram_mb() {
|
||
# Get RAM in MB
|
||
local ram=""
|
||
|
||
# Try free command first
|
||
if command -v free &>/dev/null; then
|
||
ram=$(free -m 2>/dev/null | awk '/^Mem:/{print $2}')
|
||
fi
|
||
|
||
# Fallback: parse /proc/meminfo
|
||
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
|
||
|
||
# Ensure minimum of 1
|
||
if [ -z "$ram" ] || [ "$ram" -lt 1 ] 2>/dev/null; then
|
||
echo 1
|
||
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
|
||
|
||
# Safety check
|
||
if [ -z "$cores" ] || [ "$cores" -lt 1 ] 2>/dev/null; then
|
||
echo 1
|
||
else
|
||
echo "$cores"
|
||
fi
|
||
}
|
||
|
||
calculate_recommended_clients() {
|
||
local cores=$(get_cpu_cores)
|
||
# Logic: 100 clients per CPU core, max 1000
|
||
local recommended=$((cores * 100))
|
||
if [ "$recommended" -gt 1000 ]; then
|
||
echo 1000
|
||
else
|
||
echo "$recommended"
|
||
fi
|
||
}
|
||
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
# Interactive Setup
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
|
||
prompt_settings() {
|
||
local ram_mb=$(get_ram_mb)
|
||
local cpu_cores=$(get_cpu_cores)
|
||
local recommended=$(calculate_recommended_clients)
|
||
|
||
echo ""
|
||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${CYAN} CONDUIT 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 "BEGIN {printf \"%.1f\", $ram_mb/1024}")
|
||
echo -e " RAM: ${GREEN}${ram_gb} GB${NC}"
|
||
else
|
||
echo -e " RAM: ${GREEN}${ram_mb} MB${NC}"
|
||
fi
|
||
echo -e " Recommended max-clients: ${GREEN}${recommended}${NC}"
|
||
echo ""
|
||
echo -e " ${BOLD}Conduit Options:${NC}"
|
||
echo -e " ${YELLOW}--max-clients${NC} Maximum proxy clients (1-1000)"
|
||
echo -e " ${YELLOW}--bandwidth${NC} Bandwidth per peer in Mbps (1-40, or -1 for unlimited)"
|
||
echo ""
|
||
|
||
# Max clients prompt
|
||
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
|
||
echo -e " Enter max-clients (1-1000)"
|
||
echo -e " Press Enter for recommended: ${GREEN}${recommended}${NC}"
|
||
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
|
||
read -p " max-clients: " input_clients < /dev/tty || true
|
||
|
||
if [ -z "$input_clients" ]; then
|
||
MAX_CLIENTS=$recommended
|
||
elif [[ "$input_clients" =~ ^[0-9]+$ ]] && [ "$input_clients" -ge 1 ] && [ "$input_clients" -le 1000 ]; then
|
||
MAX_CLIENTS=$input_clients
|
||
else
|
||
log_warn "Invalid input. Using recommended: $recommended"
|
||
MAX_CLIENTS=$recommended
|
||
fi
|
||
|
||
echo ""
|
||
|
||
# Bandwidth prompt
|
||
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
|
||
echo -e " Do you want to set ${BOLD}UNLIMITED${NC} bandwidth? (Recommended for servers)"
|
||
echo -e " ${YELLOW}Note: High bandwidth usage may attract attention.${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 (-1)${NC}"
|
||
else
|
||
echo ""
|
||
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
|
||
echo -e " Enter bandwidth per peer in Mbps (1-40)"
|
||
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 40 ]; 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 <= 40) ? "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 ""
|
||
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
|
||
echo -e " ${BOLD}Your Settings:${NC}"
|
||
echo -e " Max Clients: ${GREEN}${MAX_CLIENTS}${NC}"
|
||
if [ "$BANDWIDTH" == "-1" ]; then
|
||
echo -e " Bandwidth: ${GREEN}Unlimited${NC}"
|
||
else
|
||
echo -e " Bandwidth: ${GREEN}${BANDWIDTH}${NC} Mbps"
|
||
fi
|
||
echo -e "${CYAN}───────────────────────────────────────────────────────────────${NC}"
|
||
echo ""
|
||
|
||
read -p " Proceed with these settings? [Y/n] " confirm < /dev/tty || true
|
||
if [[ "$confirm" =~ ^[Nn] ]]; then
|
||
prompt_settings
|
||
fi
|
||
}
|
||
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
# Installation Functions
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
|
||
install_docker() {
|
||
if command -v docker &>/dev/null; then
|
||
log_success "Docker is already installed"
|
||
return 0
|
||
fi
|
||
|
||
log_info "Installing Docker..."
|
||
|
||
# Check OS family for specific requirements
|
||
if [ "$OS_FAMILY" = "rhel" ]; then
|
||
log_info "Installing RHEL-specific Docker dependencies..."
|
||
$PKG_MANAGER 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
|
||
fi
|
||
|
||
# Alpine
|
||
if [ "$OS_FAMILY" = "alpine" ]; then
|
||
apk add --no-cache docker docker-cli-compose 2>/dev/null
|
||
rc-update add docker boot 2>/dev/null || true
|
||
service docker start 2>/dev/null || rc-service docker start 2>/dev/null || true
|
||
else
|
||
# Use official Docker install
|
||
if ! curl -fsSL https://get.docker.com | sh; then
|
||
log_error "Official Docker installation script failed."
|
||
log_info "Try installing docker manually: https://docs.docker.com/engine/install/"
|
||
return 1
|
||
fi
|
||
|
||
# Enable and start Docker
|
||
if [ "$HAS_SYSTEMD" = "true" ]; then
|
||
systemctl enable docker 2>/dev/null || true
|
||
systemctl start docker 2>/dev/null || true
|
||
else
|
||
# Fallback for non-systemd (SysVinit, OpenRC, etc.)
|
||
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
|
||
|
||
# Wait for Docker to be ready
|
||
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
|
||
}
|
||
|
||
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
# check_and_offer_backup_restore() - Check for existing backup keys
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
# Backup location: /opt/conduit/backups/
|
||
# Key file format: conduit_key_YYYYMMDD_HHMMSS.json
|
||
#
|
||
# Returns:
|
||
# 0 - Backup was restored (or none existed)
|
||
# 1 - User declined restore (fresh install)
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
check_and_offer_backup_restore() {
|
||
|
||
if [ ! -d "$BACKUP_DIR" ]; then
|
||
return 0
|
||
fi
|
||
|
||
# Find the most recent backup file
|
||
local latest_backup=$(ls -t "$BACKUP_DIR"/conduit_key_*.json 2>/dev/null | head -1)
|
||
|
||
if [ -z "$latest_backup" ]; then
|
||
return 0
|
||
fi
|
||
|
||
# Extract timestamp from filename for display
|
||
local backup_filename=$(basename "$latest_backup")
|
||
local backup_date=$(echo "$backup_filename" | sed -E 's/conduit_key_([0-9]{8})_([0-9]{6})\.json/\1/')
|
||
local backup_time=$(echo "$backup_filename" | sed -E 's/conduit_key_([0-9]{8})_([0-9]{6})\.json/\2/')
|
||
|
||
# Format date for display (YYYYMMDD -> YYYY-MM-DD)
|
||
local formatted_date="${backup_date:0:4}-${backup_date:4:2}-${backup_date:6:2}"
|
||
local formatted_time="${backup_time:0:2}:${backup_time:2:2}:${backup_time:4:2}"
|
||
|
||
# Prompt user about restoring the backup
|
||
echo ""
|
||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${CYAN} 📁 PREVIOUS NODE IDENTITY BACKUP FOUND${NC}"
|
||
echo -e "${CYAN}═══════════════════════════════════════════════════════════════════${NC}"
|
||
echo ""
|
||
echo -e " A backup of your node identity key was found:"
|
||
echo -e " ${YELLOW}File:${NC} $backup_filename"
|
||
echo -e " ${YELLOW}Date:${NC} $formatted_date $formatted_time"
|
||
echo ""
|
||
echo -e " Restoring this key will:"
|
||
echo -e " • Preserve your node's identity on the Psiphon network"
|
||
echo -e " • Maintain any accumulated reputation"
|
||
echo -e " • Allow peers to reconnect to your known node ID"
|
||
echo ""
|
||
echo -e " ${YELLOW}Note:${NC} If you don't restore, a new identity will be generated."
|
||
echo ""
|
||
|
||
read -p " Do you want to restore your previous node identity? (y/n): " restore_choice < /dev/tty || true
|
||
|
||
if [ "$restore_choice" = "y" ] || [ "$restore_choice" = "Y" ]; then
|
||
echo ""
|
||
log_info "Restoring node identity from backup..."
|
||
|
||
# Ensure the Docker volume exists
|
||
docker volume create conduit-data 2>/dev/null || true
|
||
docker run --rm -v conduit-data:/home/conduit/data -v "$BACKUP_DIR":/backup alpine \
|
||
sh -c "cp /backup/$backup_filename /home/conduit/data/conduit_key.json && chown -R 1000:1000 /home/conduit/data"
|
||
|
||
if [ $? -eq 0 ]; then
|
||
log_success "Node identity restored successfully!"
|
||
echo ""
|
||
return 0
|
||
else
|
||
log_error "Failed to restore backup. Proceeding with fresh install."
|
||
echo ""
|
||
return 1
|
||
fi
|
||
else
|
||
echo ""
|
||
log_info "Skipping restore. A new node identity will be generated."
|
||
echo ""
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# run_conduit() - Pull image, verify digest, and start container
|
||
run_conduit() {
|
||
log_info "Starting Conduit container..."
|
||
|
||
# Check for existing conduit containers (any image containing conduit)
|
||
local existing=$(docker ps -a --filter "ancestor=ghcr.io/ssmirr/conduit/conduit" --format "{{.Names}}")
|
||
if [ -n "$existing" ] && [ "$existing" != "conduit" ]; then
|
||
log_warn "Detected other Conduit containers: $existing"
|
||
log_warn "Running multiple instances may cause port conflicts."
|
||
fi
|
||
|
||
# Stop and remove any existing container
|
||
docker rm -f conduit 2>/dev/null || true
|
||
|
||
# Pull the official Conduit image from GitHub Container Registry
|
||
log_info "Pulling Conduit image ($CONDUIT_IMAGE)..."
|
||
if ! docker pull $CONDUIT_IMAGE; then
|
||
log_error "Failed to pull Conduit image. Check your internet connection."
|
||
exit 1
|
||
fi
|
||
|
||
|
||
# Ensure volume exists and has correct permissions for the conduit user (uid 1000)
|
||
docker volume create conduit-data 2>/dev/null || true
|
||
docker run --rm -v conduit-data:/home/conduit/data alpine \
|
||
sh -c "chown -R 1000:1000 /home/conduit/data" 2>/dev/null || true
|
||
|
||
# Start the Conduit container
|
||
docker run -d \
|
||
--name conduit \
|
||
--restart unless-stopped \
|
||
-v conduit-data:/home/conduit/data \
|
||
--network host \
|
||
$CONDUIT_IMAGE \
|
||
start --max-clients "$MAX_CLIENTS" --bandwidth "$BANDWIDTH" --stats-file
|
||
|
||
# Wait for container to initialize
|
||
sleep 3
|
||
|
||
# Verify container is running
|
||
if docker ps | grep -q conduit; then
|
||
log_success "Conduit container is running"
|
||
if [ "$BANDWIDTH" == "-1" ]; then
|
||
log_success "Settings: max-clients=$MAX_CLIENTS, bandwidth=Unlimited"
|
||
else
|
||
log_success "Settings: max-clients=$MAX_CLIENTS, bandwidth=${BANDWIDTH}Mbps"
|
||
fi
|
||
else
|
||
log_error "Conduit failed to start"
|
||
docker logs conduit 2>&1 | tail -10
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
save_settings() {
|
||
mkdir -p "$INSTALL_DIR"
|
||
|
||
# Save settings
|
||
cat > "$INSTALL_DIR/settings.conf" << EOF
|
||
MAX_CLIENTS=$MAX_CLIENTS
|
||
BANDWIDTH=$BANDWIDTH
|
||
EOF
|
||
|
||
if [ ! -f "$INSTALL_DIR/settings.conf" ]; then
|
||
log_error "Failed to save settings. Check disk space and permissions."
|
||
return 1
|
||
fi
|
||
|
||
log_success "Settings saved"
|
||
}
|
||
|
||
setup_autostart() {
|
||
log_info "Setting up auto-start on boot..."
|
||
|
||
if [ "$HAS_SYSTEMD" = "true" ]; then
|
||
# Systemd-based systems
|
||
local docker_path=$(command -v docker)
|
||
cat > /etc/systemd/system/conduit.service << EOF
|
||
[Unit]
|
||
Description=Psiphon Conduit Service
|
||
After=network.target docker.service
|
||
Requires=docker.service
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
RemainAfterExit=yes
|
||
ExecStart=$docker_path start conduit
|
||
ExecStop=$docker_path stop conduit
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
systemctl daemon-reload
|
||
systemctl enable conduit.service 2>/dev/null || true
|
||
systemctl start conduit.service 2>/dev/null || true
|
||
log_success "Systemd service created, enabled, and started"
|
||
|
||
elif command -v rc-update &>/dev/null; then
|
||
# OpenRC (Alpine, Gentoo, etc.)
|
||
cat > /etc/init.d/conduit << 'EOF'
|
||
#!/sbin/openrc-run
|
||
|
||
name="conduit"
|
||
description="Psiphon Conduit Service"
|
||
depend() {
|
||
need docker
|
||
after network
|
||
}
|
||
start() {
|
||
ebegin "Starting Conduit"
|
||
docker start conduit
|
||
eend $?
|
||
}
|
||
stop() {
|
||
ebegin "Stopping Conduit"
|
||
docker stop conduit
|
||
eend $?
|
||
}
|
||
EOF
|
||
chmod +x /etc/init.d/conduit
|
||
rc-update add conduit default 2>/dev/null || true
|
||
log_success "OpenRC service created and enabled"
|
||
|
||
elif [ -d /etc/init.d ]; then
|
||
# SysVinit fallback
|
||
cat > /etc/init.d/conduit << 'EOF'
|
||
#!/bin/sh
|
||
### BEGIN INIT INFO
|
||
# Provides: conduit
|
||
# Required-Start: $docker
|
||
# Required-Stop: $docker
|
||
# Default-Start: 2 3 4 5
|
||
# Default-Stop: 0 1 6
|
||
# Short-Description: Psiphon Conduit Service
|
||
### END INIT INFO
|
||
|
||
case "$1" in
|
||
start)
|
||
docker start conduit
|
||
;;
|
||
stop)
|
||
docker stop conduit
|
||
;;
|
||
restart)
|
||
docker restart conduit
|
||
;;
|
||
status)
|
||
docker ps | grep -q conduit && echo "Running" || echo "Stopped"
|
||
;;
|
||
*)
|
||
echo "Usage: $0 {start|stop|restart|status}"
|
||
exit 1
|
||
;;
|
||
esac
|
||
EOF
|
||
chmod +x /etc/init.d/conduit
|
||
if command -v update-rc.d &>/dev/null; then
|
||
update-rc.d conduit defaults 2>/dev/null || true
|
||
elif command -v chkconfig &>/dev/null; then
|
||
chkconfig conduit 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."
|
||
log_info "Container is set to restart unless-stopped, which works on reboot if Docker starts."
|
||
fi
|
||
}
|
||
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
# Management Script
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
|
||
create_management_script() {
|
||
# Generate the management script.
|
||
cat > "$INSTALL_DIR/conduit" << 'MANAGEMENT'
|
||
#!/bin/bash
|
||
#
|
||
# Psiphon Conduit Manager
|
||
# Reference: https://github.com/ssmirr/conduit/releases/tag/d8522a8
|
||
#
|
||
|
||
VERSION="1.0.2"
|
||
INSTALL_DIR="REPLACE_ME_INSTALL_DIR"
|
||
BACKUP_DIR="$INSTALL_DIR/backups"
|
||
CONDUIT_IMAGE="ghcr.io/ssmirr/conduit/conduit:d8522a8"
|
||
|
||
# Colors
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
CYAN='\033[0;36m'
|
||
BOLD='\033[1m'
|
||
NC='\033[0m'
|
||
|
||
# Load settings
|
||
[ -f "$INSTALL_DIR/settings.conf" ] && source "$INSTALL_DIR/settings.conf"
|
||
MAX_CLIENTS=${MAX_CLIENTS:-200}
|
||
BANDWIDTH=${BANDWIDTH:-5}
|
||
|
||
# Ensure we're running as root
|
||
if [ "$EUID" -ne 0 ]; then
|
||
echo -e "${RED}Error: This command must be run as root (use sudo conduit)${NC}"
|
||
exit 1
|
||
fi
|
||
|
||
# Check if Docker is available
|
||
check_docker() {
|
||
if ! command -v docker &>/dev/null; then
|
||
echo -e "${RED}Error: Docker is not installed!${NC}"
|
||
echo ""
|
||
echo "Docker is required to run Conduit. Please reinstall:"
|
||
echo " curl -fsSL https://get.docker.com | sudo sh"
|
||
echo ""
|
||
echo "Or re-run the Conduit installer:"
|
||
echo " sudo bash conduit.sh"
|
||
exit 1
|
||
fi
|
||
|
||
if ! docker info &>/dev/null; then
|
||
echo -e "${RED}Error: Docker daemon is not running!${NC}"
|
||
echo ""
|
||
echo "Start Docker with:"
|
||
echo " sudo systemctl start docker # For systemd"
|
||
echo " sudo /etc/init.d/docker start # For SysVinit"
|
||
echo " sudo rc-service docker start # For OpenRC"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Run Docker check
|
||
check_docker
|
||
|
||
# Check for awk (needed for stats parsing)
|
||
if ! command -v awk &>/dev/null; then
|
||
echo -e "${YELLOW}Warning: awk not found. Some stats may not display correctly.${NC}"
|
||
fi
|
||
|
||
# Helper: Fix volume permissions for conduit user (uid 1000)
|
||
fix_volume_permissions() {
|
||
docker run --rm -v conduit-data:/home/conduit/data alpine \
|
||
sh -c "chown -R 1000:1000 /home/conduit/data" 2>/dev/null || true
|
||
}
|
||
|
||
# Helper: Start/recreate conduit container with current settings
|
||
run_conduit_container() {
|
||
docker run -d \
|
||
--name conduit \
|
||
--restart unless-stopped \
|
||
-v conduit-data:/home/conduit/data \
|
||
--network host \
|
||
$CONDUIT_IMAGE \
|
||
start --max-clients "$MAX_CLIENTS" --bandwidth "$BANDWIDTH" --stats-file
|
||
}
|
||
|
||
print_header() {
|
||
echo -e "${CYAN}"
|
||
echo "╔═══════════════════════════════════════════════════════════════════╗"
|
||
printf "║ 🚀 PSIPHON CONDUIT MANAGER v%-5s ║\n" "${VERSION}"
|
||
echo "╚═══════════════════════════════════════════════════════════════════╝"
|
||
echo -e "${NC}"
|
||
}
|
||
|
||
print_live_stats_header() {
|
||
local EL="\033[K"
|
||
echo -e "${CYAN}╔═══════════════════════════════════════════════════════════════════╗${EL}"
|
||
echo -e "║ CONDUIT LIVE STATISTICS ║${EL}"
|
||
echo -e "╠═══════════════════════════════════════════════════════════════════╣${EL}"
|
||
printf "║ Max Clients: ${GREEN}%-52s${CYAN}║${EL}\n" "${MAX_CLIENTS}"
|
||
if [ "$BANDWIDTH" == "-1" ]; then
|
||
printf "║ Bandwidth: ${GREEN}%-52s${CYAN}║${EL}\n" "Unlimited"
|
||
else
|
||
printf "║ Bandwidth: ${GREEN}%-52s${CYAN}║${EL}\n" "${BANDWIDTH} Mbps"
|
||
fi
|
||
echo -e "║ ║${EL}"
|
||
echo -e "╚═══════════════════════════════════════════════════════════════════╝${EL}"
|
||
echo -e "${NC}\033[K"
|
||
}
|
||
|
||
|
||
|
||
get_node_id() {
|
||
if docker volume inspect conduit-data >/dev/null 2>&1; then
|
||
local mountpoint=$(docker volume inspect conduit-data --format '{{ .Mountpoint }}')
|
||
if [ -f "$mountpoint/conduit_key.json" ]; then
|
||
# Extract privateKeyBase64, decode, take last 32 bytes, encode base64
|
||
# Logic provided by user
|
||
cat "$mountpoint/conduit_key.json" | grep "privateKeyBase64" | awk -F'"' '{print $4}' | base64 -d 2>/dev/null | tail -c 32 | base64 | tr -d '=\n'
|
||
fi
|
||
fi
|
||
}
|
||
|
||
show_dashboard() {
|
||
local stop_dashboard=0
|
||
# Setup trap to catch signals gracefully
|
||
trap 'stop_dashboard=1' SIGINT SIGTERM
|
||
|
||
# Use alternate screen buffer if available for smoother experience
|
||
tput smcup 2>/dev/null || true
|
||
echo -ne "\033[?25l" # Hide cursor
|
||
# Initial clear
|
||
clear
|
||
|
||
while [ $stop_dashboard -eq 0 ]; do
|
||
# Move cursor to top-left (0,0)
|
||
# We NO LONGER clear the screen here to avoid the "full black" flash
|
||
if ! tput cup 0 0 2>/dev/null; then
|
||
printf "\033[H"
|
||
fi
|
||
|
||
print_live_stats_header
|
||
|
||
show_status "live"
|
||
|
||
# Show Node ID in its own section
|
||
local node_id=$(get_node_id)
|
||
if [ -n "$node_id" ]; then
|
||
echo -e "${CYAN}═══ CONDUIT ID ═══${NC}\033[K"
|
||
echo -e " ${CYAN}${node_id}${NC}\033[K"
|
||
echo -e "\033[K"
|
||
fi
|
||
|
||
echo -e "${BOLD}Refreshes every 5 seconds. Press any key to return to menu...${NC}\033[K"
|
||
|
||
# Clear any leftover lines below the dashboard content (Erase to End of Display)
|
||
# This only cleans up if the dashboard gets shorter
|
||
if ! tput ed 2>/dev/null; then
|
||
printf "\033[J"
|
||
fi
|
||
|
||
# Wait 4 seconds for keypress (compensating for processing time)
|
||
# Redirect from /dev/tty ensures it works when the script is piped
|
||
if read -t 4 -n 1 -s <> /dev/tty 2>/dev/null; then
|
||
stop_dashboard=1
|
||
fi
|
||
done
|
||
|
||
echo -ne "\033[?25h" # Show cursor
|
||
# Restore main screen buffer
|
||
tput rmcup 2>/dev/null || true
|
||
trap - SIGINT SIGTERM # Reset traps
|
||
}
|
||
|
||
get_container_stats() {
|
||
# Get CPU and RAM usage for conduit container
|
||
# Returns: "CPU_PERCENT RAM_USAGE"
|
||
local stats=$(docker stats --no-stream --format "{{.CPUPerc}} {{.MemUsage}}" conduit 2>/dev/null)
|
||
if [ -z "$stats" ]; then
|
||
echo "0% 0MiB"
|
||
else
|
||
# Extract just the raw numbers/units, simpler format
|
||
echo "$stats"
|
||
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_system_stats() {
|
||
# Get System CPU (Live Delta) and RAM
|
||
# Returns: "CPU_PERCENT RAM_USED RAM_TOTAL RAM_PCT"
|
||
|
||
# 1. System CPU (Stateful Average)
|
||
local sys_cpu="0%"
|
||
local cpu_tmp="/tmp/conduit_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 total_delta=$((total_curr - total_prev))
|
||
local work_delta=$((work_curr - work_prev))
|
||
|
||
if [ "$total_delta" -gt 0 ]; then
|
||
local cpu_usage=$(awk -v w="$work_delta" -v t="$total_delta" 'BEGIN { printf "%.1f", w * 100 / t }' 2>/dev/null || echo 0)
|
||
sys_cpu="${cpu_usage}%"
|
||
fi
|
||
else
|
||
sys_cpu="Calc..." # First run calibration
|
||
fi
|
||
|
||
# Save current state for next run
|
||
echo "$total_curr $work_curr" > "$cpu_tmp"
|
||
else
|
||
sys_cpu="N/A"
|
||
fi
|
||
|
||
# 2. System RAM (Used, Total, Percentage)
|
||
local sys_ram_used="N/A"
|
||
local sys_ram_total="N/A"
|
||
local sys_ram_pct="N/A"
|
||
|
||
if command -v free &>/dev/null; then
|
||
# Output: used total percentage
|
||
local ram_data=$(free -m 2>/dev/null | awk '/^Mem:/{printf "%s %s %.2f%%", $3, $2, ($3/$2)*100}')
|
||
local ram_human=$(free -h 2>/dev/null | awk '/^Mem:/{print $3 " " $2}')
|
||
|
||
sys_ram_used=$(echo "$ram_human" | awk '{print $1}')
|
||
sys_ram_total=$(echo "$ram_human" | awk '{print $2}')
|
||
sys_ram_pct=$(echo "$ram_data" | awk '{print $3}')
|
||
fi
|
||
|
||
echo "$sys_cpu $sys_ram_used $sys_ram_total $sys_ram_pct"
|
||
}
|
||
|
||
show_live_stats() {
|
||
# Check if container is running first
|
||
if ! docker ps 2>/dev/null | grep -q "[[:space:]]conduit$"; then
|
||
print_header
|
||
echo -e "${RED}Conduit is not running!${NC}"
|
||
echo "Start it first with option 6 or 'conduit start'"
|
||
read -n 1 -s -r -p "Press any key to continue..." < /dev/tty 2>/dev/null || true
|
||
return 1
|
||
fi
|
||
|
||
echo -e "${CYAN}Streaming live statistics... Press Ctrl+C to return to menu${NC}"
|
||
echo -e "${YELLOW}(showing live logs filtered for [STATS])${NC}"
|
||
echo ""
|
||
|
||
# Trap Ctrl+C to allow handled exit from the log stream
|
||
trap 'echo -e "\n${CYAN}Returning to menu...${NC}"; return' SIGINT
|
||
|
||
# Stream logs and filter for [STATS]
|
||
# We check if grep supports --line-buffered for smoother output, fallback to standard grep
|
||
if grep --help 2>&1 | grep -q -- --line-buffered; then
|
||
docker logs -f --tail 20 conduit 2>&1 | grep --line-buffered "\[STATS\]"
|
||
else
|
||
docker logs -f --tail 20 conduit 2>&1 | grep "\[STATS\]"
|
||
fi
|
||
|
||
# Reset trap
|
||
trap - SIGINT
|
||
}
|
||
|
||
# format_bytes() - Convert bytes to human-readable format (B, KB, MB, GB)
|
||
format_bytes() {
|
||
local bytes=$1
|
||
|
||
# Handle empty or zero input
|
||
if [ -z "$bytes" ] || [ "$bytes" -eq 0 ] 2>/dev/null; then
|
||
echo "0 B"
|
||
return
|
||
fi
|
||
|
||
# Convert based on size thresholds (using binary units)
|
||
# 1 GB = 1073741824 bytes (1024^3)
|
||
# 1 MB = 1048576 bytes (1024^2)
|
||
# 1 KB = 1024 bytes
|
||
if [ "$bytes" -ge 1073741824 ]; then
|
||
awk "BEGIN {printf \"%.2f GB\", $bytes/1073741824}"
|
||
elif [ "$bytes" -ge 1048576 ]; then
|
||
awk "BEGIN {printf \"%.2f MB\", $bytes/1048576}"
|
||
elif [ "$bytes" -ge 1024 ]; then
|
||
awk "BEGIN {printf \"%.2f KB\", $bytes/1024}"
|
||
else
|
||
echo "$bytes B"
|
||
fi
|
||
}
|
||
|
||
# show_peers() - Live peer traffic by country using tcpdump + GeoIP
|
||
show_peers() {
|
||
# Flag to control the main loop - set to 1 on user interrupt
|
||
local stop_peers=0
|
||
trap 'stop_peers=1' SIGINT SIGTERM
|
||
|
||
# Verify required dependencies are installed
|
||
if ! command -v tcpdump &>/dev/null || ! command -v geoiplookup &>/dev/null; then
|
||
echo -e "${RED}Error: tcpdump or geoiplookup not found!${NC}"
|
||
echo "Please re-run the main installer to fix dependencies."
|
||
read -n 1 -s -r -p "Press any key to return..." < /dev/tty || true
|
||
return 1
|
||
fi
|
||
|
||
# Network interface detection
|
||
# Use "any" to capture on all interfaces
|
||
local iface="any"
|
||
|
||
# Detect local IP address to determine traffic direction
|
||
# Method 1: Query the route to a public IP (most reliable)
|
||
# Method 2: Fallback to hostname -I
|
||
local local_ip=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7}')
|
||
[ -z "$local_ip" ] && local_ip=$(hostname -I | awk '{print $1}')
|
||
|
||
# Clean temporary working files (per-cycle data only)
|
||
rm -f /tmp/conduit_peers_current /tmp/conduit_peers_raw
|
||
rm -f /tmp/conduit_traffic_from /tmp/conduit_traffic_to
|
||
touch /tmp/conduit_traffic_from /tmp/conduit_traffic_to
|
||
|
||
# Persistent data directory - survives across option 9 sessions
|
||
local persist_dir="/opt/conduit/traffic_stats"
|
||
mkdir -p "$persist_dir"
|
||
|
||
# Get container start time to detect restarts
|
||
local container_start=$(docker inspect --format='{{.State.StartedAt}}' conduit 2>/dev/null | cut -d'.' -f1)
|
||
local stored_start=""
|
||
[ -f "$persist_dir/container_start" ] && stored_start=$(cat "$persist_dir/container_start")
|
||
|
||
# If container was restarted, reset all cumulative data
|
||
if [ "$container_start" != "$stored_start" ]; then
|
||
echo "$container_start" > "$persist_dir/container_start"
|
||
rm -f "$persist_dir/cumulative_data" "$persist_dir/cumulative_ips" "$persist_dir/session_start"
|
||
fi
|
||
|
||
# Cumulative data files persist until Conduit restarts
|
||
# Format: Country|TotalFrom|TotalTo (bytes received from / sent to)
|
||
[ ! -f "$persist_dir/cumulative_data" ] && touch "$persist_dir/cumulative_data"
|
||
# Format: Country|IP (one line per unique IP seen)
|
||
[ ! -f "$persist_dir/cumulative_ips" ] && touch "$persist_dir/cumulative_ips"
|
||
|
||
# Session start time - when we first started tracking (persists until Conduit restart)
|
||
if [ ! -f "$persist_dir/session_start" ]; then
|
||
date +%s > "$persist_dir/session_start"
|
||
fi
|
||
local session_start=$(cat "$persist_dir/session_start")
|
||
|
||
# Enter alternate screen buffer (preserves terminal history)
|
||
tput smcup 2>/dev/null || true
|
||
# Hide cursor for cleaner display
|
||
echo -ne "\033[?25l"
|
||
|
||
#═══════════════════════════════════════════════════════════════════
|
||
# Main display loop - runs until user presses a key
|
||
#═══════════════════════════════════════════════════════════════════
|
||
while [ $stop_peers -eq 0 ]; do
|
||
# Clear screen completely and move to top-left
|
||
clear
|
||
printf "\033[H"
|
||
|
||
#───────────────────────────────────────────────────────────────
|
||
# Header Section - Compact title bar with live status indicator
|
||
# Shows: Title, session duration, and [LIVE - last 15s] indicator
|
||
#───────────────────────────────────────────────────────────────
|
||
# Calculate how long this view session has been running
|
||
local now=$(date +%s)
|
||
local duration=$((now - session_start))
|
||
local dur_min=$((duration / 60))
|
||
local dur_sec=$((duration % 60))
|
||
local duration_str=$(printf "%02d:%02d" $dur_min $dur_sec)
|
||
|
||
echo -e "${CYAN}╔═══════════════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "║ LIVE PEER TRAFFIC BY COUNTRY ║"
|
||
echo -e "${CYAN}╠═══════════════════════════════════════════════════════════════════╣${NC}"
|
||
if [ -f /tmp/conduit_peers_current ]; then
|
||
# Data is available - show last update time
|
||
local update_time=$(date '+%H:%M:%S')
|
||
echo -e "║ Last Update: ${update_time} ${GREEN}[LIVE]${NC} ║"
|
||
else
|
||
# Waiting for first data capture
|
||
echo -e "║ Status: ${YELLOW}Initializing...${NC} ║"
|
||
fi
|
||
echo -e "${CYAN}╚═══════════════════════════════════════════════════════════════════╝${NC}"
|
||
echo -e ""
|
||
|
||
#───────────────────────────────────────────────────────────────
|
||
# Data Tables - Display TOP 10 countries by traffic volume
|
||
#
|
||
# "TRAFFIC FROM" = Data received from that country (incoming)
|
||
# These are peers connecting TO your Conduit node
|
||
# "TRAFFIC TO" = Data sent to that country (outgoing)
|
||
# This is data your node sends back to peers
|
||
#
|
||
# Columns explained:
|
||
# Total = Cumulative bytes since this view started
|
||
# Speed = Current transfer rate (from last 15-second window)
|
||
# IPs = Unique IP addresses (Total seen / Currently active)
|
||
#
|
||
# Colors: GREEN = incoming traffic, YELLOW = outgoing traffic
|
||
# #FreeIran = RED (solidarity highlight)
|
||
#───────────────────────────────────────────────────────────────
|
||
if [ -s /tmp/conduit_traffic_from ]; then
|
||
# Section 1: Top 10 countries by incoming traffic (data FROM them)
|
||
# This shows which countries have peers connecting to your node
|
||
echo -e "${GREEN}${BOLD} 📥 TOP 10 TRAFFIC FROM (peers connecting to you)${NC}"
|
||
echo -e " ─────────────────────────────────────────────────────────────────────────"
|
||
printf " ${BOLD}%-26s${NC} ${GREEN}${BOLD}%10s %12s${NC} %-12s\n" "Country" "Total" "Speed" "IPs (all/now)"
|
||
echo -e " ─────────────────────────────────────────────────────────────────────────"
|
||
# Read top 10 entries from incoming-traffic-sorted file
|
||
head -10 /tmp/conduit_traffic_from | while read -r line; do
|
||
# Parse pipe-delimited fields: Country|TotalFrom|TotalTo|SpeedFrom|SpeedTo|TotalIPs|ActiveIPs
|
||
local country=$(echo "$line" | cut -d'|' -f1)
|
||
local from_bytes=$(echo "$line" | cut -d'|' -f2)
|
||
local from_speed=$(echo "$line" | cut -d'|' -f4)
|
||
local total_ips=$(echo "$line" | cut -d'|' -f6)
|
||
local active_ips=$(echo "$line" | cut -d'|' -f7)
|
||
# Format bytes to human-readable (KB/MB/GB)
|
||
local from_fmt=$(format_bytes "$from_bytes")
|
||
local from_spd_fmt=$(format_bytes "$from_speed")/s
|
||
# Format IP counts - handle empty values
|
||
[ -z "$total_ips" ] && total_ips="0"
|
||
[ -z "$active_ips" ] && active_ips="0"
|
||
local ip_display="${total_ips}/${active_ips}"
|
||
# Print row: CYAN country, GREEN values (Total/Speed right-aligned, IPs left-aligned)
|
||
printf " ${CYAN}%-26s${NC} ${GREEN}${BOLD}%10s %12s${NC} %-12s\n" "$country" "$from_fmt" "$from_spd_fmt" "$ip_display"
|
||
done
|
||
echo ""
|
||
|
||
# Section 2: Top 10 countries by outgoing traffic (data TO them)
|
||
# This shows which countries you're sending the most data to
|
||
echo -e "${YELLOW}${BOLD} 📤 TOP 10 TRAFFIC TO (data sent to peers)${NC}"
|
||
echo -e " ─────────────────────────────────────────────────────────────────────────"
|
||
printf " ${BOLD}%-26s${NC} ${YELLOW}${BOLD}%10s %12s${NC} %-12s\n" "Country" "Total" "Speed" "IPs (all/now)"
|
||
echo -e " ─────────────────────────────────────────────────────────────────────────"
|
||
# Read top 10 entries from outgoing-traffic-sorted file
|
||
head -10 /tmp/conduit_traffic_to | while read -r line; do
|
||
# Parse pipe-delimited fields: Country|TotalFrom|TotalTo|SpeedFrom|SpeedTo|TotalIPs|ActiveIPs
|
||
local country=$(echo "$line" | cut -d'|' -f1)
|
||
local to_bytes=$(echo "$line" | cut -d'|' -f3)
|
||
local to_speed=$(echo "$line" | cut -d'|' -f5)
|
||
local total_ips=$(echo "$line" | cut -d'|' -f6)
|
||
local active_ips=$(echo "$line" | cut -d'|' -f7)
|
||
# Format bytes to human-readable (KB/MB/GB)
|
||
local to_fmt=$(format_bytes "$to_bytes")
|
||
local to_spd_fmt=$(format_bytes "$to_speed")/s
|
||
# Format IP counts - handle empty values
|
||
[ -z "$total_ips" ] && total_ips="0"
|
||
[ -z "$active_ips" ] && active_ips="0"
|
||
local ip_display="${total_ips}/${active_ips}"
|
||
# Print row: CYAN country, YELLOW values (Total/Speed right-aligned, IPs left-aligned)
|
||
printf " ${CYAN}%-26s${NC} ${YELLOW}${BOLD}%10s %12s${NC} %-12s\n" "$country" "$to_fmt" "$to_spd_fmt" "$ip_display"
|
||
done
|
||
else
|
||
# No data yet - show waiting message with padding
|
||
echo -e " ${YELLOW}Waiting for first snapshot... (High traffic helps speed this up)${NC}"
|
||
for i in {1..20}; do echo ""; done
|
||
fi
|
||
|
||
echo -e ""
|
||
echo -e "${CYAN}════════════════════════════════════════════════════════════════════════════${NC}"
|
||
|
||
#═══════════════════════════════════════════════════════════════════
|
||
# Background Traffic Capture
|
||
#═══════════════════════════════════════════════════════════════════
|
||
# Uses tcpdump to capture live network packets for 15 seconds
|
||
# tcpdump flags:
|
||
# -n : Don't resolve hostnames (faster)
|
||
# -i : Interface to capture on ("any" = all interfaces)
|
||
# -q : Quiet output (less verbose)
|
||
#
|
||
# The captured output is piped to awk which:
|
||
# 1. Extracts source and destination IP addresses
|
||
# 2. Extracts packet length from each line
|
||
# 3. Filters out private/local IP ranges (RFC 1918)
|
||
# 4. Determines traffic direction (from vs to)
|
||
# 5. Aggregates bytes per IP address
|
||
# 6. Outputs: IP|bytes_from_remote|bytes_to_remote
|
||
#
|
||
# Traffic direction naming (from your server's perspective):
|
||
# "from" = bytes received FROM remote IP (remote -> local)
|
||
# "to" = bytes sent TO remote IP (local -> remote)
|
||
#═══════════════════════════════════════════════════════════════════
|
||
# Wrap pipeline in subshell so $! captures the whole pipeline PID, not just awk
|
||
# This ensures the progress indicator runs for the full 15-second capture
|
||
(
|
||
timeout 15 tcpdump -ni $iface -q '(tcp or udp)' 2>/dev/null | \
|
||
awk -v local_ip="$local_ip" '
|
||
# Portable awk script - works with mawk, gawk, and busybox awk
|
||
/IP/ {
|
||
# Parse tcpdump output to extract IPs and packet length
|
||
# Example format: "IP 192.168.1.1.443 > 8.8.8.8.12345: TCP, length 1460"
|
||
# Or: "IP 10.0.0.1.22 > 203.0.113.5.54321: UDP, length 64"
|
||
|
||
src = ""
|
||
dst = ""
|
||
len = 0
|
||
|
||
# Find the field containing "IP" and extract source/dest
|
||
for (i = 1; i <= NF; i++) {
|
||
if ($i == "IP") {
|
||
# Next field is source IP.port
|
||
src_field = $(i+1)
|
||
# Field after ">" is dest IP.port
|
||
for (j = i+2; j <= NF; j++) {
|
||
if ($(j-1) == ">") {
|
||
dst_field = $j
|
||
# Remove trailing colon if present
|
||
gsub(/:$/, "", dst_field)
|
||
break
|
||
}
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
# Extract IP from IP.port format (remove last .port segment)
|
||
# Example: 192.168.1.1.443 -> 192.168.1.1
|
||
if (src_field != "") {
|
||
n = split(src_field, parts, ".")
|
||
if (n >= 4) {
|
||
src = parts[1] "." parts[2] "." parts[3] "." parts[4]
|
||
}
|
||
}
|
||
if (dst_field != "") {
|
||
n = split(dst_field, parts, ".")
|
||
if (n >= 4) {
|
||
dst = parts[1] "." parts[2] "." parts[3] "." parts[4]
|
||
}
|
||
}
|
||
|
||
# Extract packet length - look for "length N" pattern
|
||
for (i = 1; i <= NF; i++) {
|
||
if ($i == "length") {
|
||
len = $(i+1) + 0
|
||
break
|
||
}
|
||
}
|
||
# Fallback: use last numeric field if no "length" found
|
||
if (len == 0) {
|
||
for (i = NF; i > 0; i--) {
|
||
if ($i ~ /^[0-9]+$/) {
|
||
len = $i + 0
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
# Skip if we could not parse IPs
|
||
if (src == "" && dst == "") next
|
||
|
||
# Filter out private/reserved IP ranges (RFC 1918 + others)
|
||
# 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8,
|
||
# 0.0.0.0/8, 169.254.0.0/16 (link-local)
|
||
if (src ~ /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|0\.|169\.254\.)/) src = ""
|
||
if (dst ~ /^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|0\.|169\.254\.)/) dst = ""
|
||
|
||
# Determine traffic direction based on local IP
|
||
# "traffic_from" = bytes coming FROM remote (incoming to your server)
|
||
# "traffic_to" = bytes going TO remote (outgoing from your server)
|
||
if (src == local_ip && dst != "" && dst != local_ip) {
|
||
# Outgoing: packet going FROM local TO remote
|
||
traffic_to[dst] += len
|
||
ips[dst] = 1
|
||
} else if (dst == local_ip && src != "" && src != local_ip) {
|
||
# Incoming: packet coming FROM remote TO local
|
||
traffic_from[src] += len
|
||
ips[src] = 1
|
||
} else if (src != "" && src != local_ip) {
|
||
# Fallback: non-local source = incoming traffic
|
||
traffic_from[src] += len
|
||
ips[src] = 1
|
||
} else if (dst != "" && dst != local_ip) {
|
||
# Fallback: non-local destination = outgoing traffic
|
||
traffic_to[dst] += len
|
||
ips[dst] = 1
|
||
}
|
||
}
|
||
END {
|
||
# Output aggregated data: IP|bytes_from|bytes_to
|
||
for (ip in ips) {
|
||
from_bytes = traffic_from[ip] + 0 # Default to 0 if undefined
|
||
to_bytes = traffic_to[ip] + 0
|
||
print ip "|" from_bytes "|" to_bytes
|
||
}
|
||
}' > /tmp/conduit_peers_raw
|
||
) 2>/dev/null &
|
||
|
||
# Store subshell PID for cleanup if user exits early
|
||
local tcpdump_pid=$!
|
||
|
||
#───────────────────────────────────────────────────────────────
|
||
# Progress Indicator Loop - runs for exactly 15 seconds
|
||
# Shows animated dots while tcpdump captures data
|
||
# Checks for user keypress every second to allow early exit
|
||
#───────────────────────────────────────────────────────────────
|
||
local count=0
|
||
while [ $count -lt 15 ]; do
|
||
if read -t 1 -n 1 -s <> /dev/tty 2>/dev/null; then
|
||
stop_peers=1
|
||
kill $tcpdump_pid 2>/dev/null
|
||
break
|
||
fi
|
||
count=$((count + 1))
|
||
echo -ne "\r [${YELLOW}"
|
||
for ((i=0; i<count; i++)); do echo -n "•"; done
|
||
for ((i=count; i<15; i++)); do echo -n " "; done
|
||
echo -ne "${NC}] Capturing next update... (Any key to exit) \033[K"
|
||
done
|
||
|
||
# Wait for tcpdump to finish (should already be done after 15s)
|
||
wait $tcpdump_pid 2>/dev/null
|
||
|
||
# Exit loop if user requested stop
|
||
if [ $stop_peers -eq 1 ]; then break; fi
|
||
|
||
#═══════════════════════════════════════════════════════════════════
|
||
# GeoIP Resolution and Country Aggregation (Cumulative)
|
||
#═══════════════════════════════════════════════════════════════════
|
||
# Process the raw IP data:
|
||
# 1. Read each IP with its from/to bytes from this cycle
|
||
# 2. Resolve IP to country using geoiplookup
|
||
# 3. Add to cumulative totals (persisted in temp file)
|
||
# 4. Track unique IPs per country (cumulative and active)
|
||
# 5. Calculate bandwidth speed (bytes per second from 15s window)
|
||
# 6. Create sorted output files for display
|
||
#
|
||
# Traffic direction naming:
|
||
# "from" = bytes received FROM remote IP (incoming to your server)
|
||
# "to" = bytes sent TO remote IP (outgoing from your server)
|
||
#═══════════════════════════════════════════════════════════════════
|
||
if [ -s /tmp/conduit_peers_raw ]; then
|
||
# Associative arrays for this capture cycle - MUST unset first!
|
||
# In bash, 'declare -A' does NOT clear existing arrays, causing accumulation bug
|
||
unset cycle_from cycle_to cycle_ips ip_to_country
|
||
declare -A cycle_from # Bytes received FROM each country this cycle
|
||
declare -A cycle_to # Bytes sent TO each country this cycle
|
||
declare -A cycle_ips # IPs seen this cycle per country (for active count)
|
||
declare -A ip_to_country # Map IP -> country for deduplication
|
||
|
||
# Process each IP from the raw capture data
|
||
# Raw format: IP|bytes_from|bytes_to
|
||
while IFS='|' read -r ip from_bytes to_bytes; do
|
||
[ -z "$ip" ] && continue
|
||
|
||
# Resolve IP to country using GeoIP database
|
||
local country_info=$(geoiplookup "$ip" 2>/dev/null | awk -F: '/Country Edition/{print $2}' | sed 's/^ //')
|
||
[ -z "$country_info" ] && country_info="Unknown"
|
||
|
||
# Normalize certain country names for display
|
||
country_info=$(echo "$country_info" | sed 's/Iran, Islamic Republic of/Iran - #FreeIran/' | sed 's/Moldova, Republic of/Moldova/')
|
||
|
||
# Store IP to country mapping for later
|
||
ip_to_country["$ip"]="$country_info"
|
||
|
||
# Aggregate this cycle's traffic by country
|
||
cycle_from["$country_info"]=$((${cycle_from["$country_info"]:-0} + from_bytes))
|
||
cycle_to["$country_info"]=$((${cycle_to["$country_info"]:-0} + to_bytes))
|
||
|
||
# Track active IPs this cycle (append IP to country's IP list)
|
||
cycle_ips["$country_info"]="${cycle_ips["$country_info"]} $ip"
|
||
done < /tmp/conduit_peers_raw
|
||
|
||
# Load existing cumulative traffic data from persistent storage
|
||
unset cumul_from cumul_to
|
||
declare -A cumul_from
|
||
declare -A cumul_to
|
||
if [ -s "$persist_dir/cumulative_data" ]; then
|
||
while IFS='|' read -r country cfrom cto; do
|
||
[ -z "$country" ] && continue
|
||
cumul_from["$country"]=$cfrom
|
||
cumul_to["$country"]=$cto
|
||
done < "$persist_dir/cumulative_data"
|
||
fi
|
||
|
||
# Add this cycle's traffic to cumulative totals
|
||
for country in "${!cycle_from[@]}"; do
|
||
cumul_from["$country"]=$((${cumul_from["$country"]:-0} + ${cycle_from["$country"]}))
|
||
cumul_to["$country"]=$((${cumul_to["$country"]:-0} + ${cycle_to["$country"]}))
|
||
done
|
||
|
||
# Save updated cumulative traffic data to persistent storage
|
||
> "$persist_dir/cumulative_data"
|
||
for country in "${!cumul_from[@]}"; do
|
||
echo "${country}|${cumul_from[$country]}|${cumul_to[$country]}" >> "$persist_dir/cumulative_data"
|
||
done
|
||
|
||
# Update cumulative IP tracking (add new IPs seen this cycle)
|
||
for ip in "${!ip_to_country[@]}"; do
|
||
local country="${ip_to_country[$ip]}"
|
||
# Check if this IP|Country combo already exists
|
||
if ! grep -q "^${country}|${ip}$" "$persist_dir/cumulative_ips" 2>/dev/null; then
|
||
echo "${country}|${ip}" >> "$persist_dir/cumulative_ips"
|
||
fi
|
||
done
|
||
|
||
# Count total unique IPs per country (cumulative)
|
||
unset total_ips_count
|
||
declare -A total_ips_count
|
||
if [ -s "$persist_dir/cumulative_ips" ]; then
|
||
while IFS='|' read -r country ip; do
|
||
[ -z "$country" ] && continue
|
||
total_ips_count["$country"]=$((${total_ips_count["$country"]:-0} + 1))
|
||
done < "$persist_dir/cumulative_ips"
|
||
fi
|
||
|
||
# Count active IPs this cycle per country
|
||
unset active_ips_count
|
||
declare -A active_ips_count
|
||
for country in "${!cycle_ips[@]}"; do
|
||
# Count unique IPs in this cycle's IP list for this country
|
||
local unique_count=$(echo "${cycle_ips[$country]}" | tr ' ' '\n' | sort -u | grep -c '.')
|
||
active_ips_count["$country"]=$unique_count
|
||
done
|
||
|
||
# Generate sorted output with all metrics
|
||
# Format: Country|TotalFrom|TotalTo|SpeedFrom|SpeedTo|TotalIPs|ActiveIPs
|
||
> /tmp/conduit_traffic_from
|
||
> /tmp/conduit_traffic_to
|
||
for country in "${!cumul_from[@]}"; do
|
||
local total_from=${cumul_from[$country]}
|
||
local total_to=${cumul_to[$country]}
|
||
local cycle_from_val=${cycle_from["$country"]:-0}
|
||
local cycle_to_val=${cycle_to["$country"]:-0}
|
||
# Calculate speed (bytes per second) from 15-second capture
|
||
local speed_from=$((cycle_from_val / 15))
|
||
local speed_to=$((cycle_to_val / 15))
|
||
# Get IP counts
|
||
local total_ips=${total_ips_count["$country"]:-0}
|
||
local active_ips=${active_ips_count["$country"]:-0}
|
||
echo "${country}|${total_from}|${total_to}|${speed_from}|${speed_to}|${total_ips}|${active_ips}" >> /tmp/conduit_traffic_from
|
||
done
|
||
|
||
# Sort by total incoming traffic (field 2) descending
|
||
sort -t'|' -k2 -nr -o /tmp/conduit_traffic_from /tmp/conduit_traffic_from
|
||
|
||
# Copy and sort by total outgoing traffic (field 3) descending
|
||
cp /tmp/conduit_traffic_from /tmp/conduit_traffic_to
|
||
sort -t'|' -k3 -nr -o /tmp/conduit_traffic_to /tmp/conduit_traffic_to
|
||
|
||
# Touch marker file to indicate data is ready for display
|
||
touch /tmp/conduit_peers_current
|
||
fi
|
||
|
||
echo -ne "\r ${GREEN}✓ Update complete! Refreshing...${NC} \033[K"
|
||
sleep 1
|
||
done
|
||
# End of main display loop
|
||
|
||
#═══════════════════════════════════════════════════════════════════
|
||
# Cleanup - restore terminal state and remove temp files
|
||
# Note: Persistent data in /opt/conduit/traffic_stats/ is NOT removed
|
||
# It persists until Conduit container restarts
|
||
#═══════════════════════════════════════════════════════════════════
|
||
echo -ne "\033[?25h" # Show cursor
|
||
tput rmcup 2>/dev/null || true # Exit alternate screen buffer
|
||
# Remove only temporary working files (not persistent cumulative data)
|
||
rm -f /tmp/conduit_peers_current /tmp/conduit_peers_raw
|
||
rm -f /tmp/conduit_traffic_from /tmp/conduit_traffic_to
|
||
trap - SIGINT SIGTERM # Remove signal handlers
|
||
}
|
||
|
||
get_net_speed() {
|
||
# Calculate System Network Speed (Active 0.5s Sample)
|
||
# Returns: "RX_MBPS TX_MBPS"
|
||
local iface=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $5}')
|
||
[ -z "$iface" ] && iface=$(ip route list default 2>/dev/null | awk '{print $5}')
|
||
|
||
if [ -n "$iface" ] && [ -f "/sys/class/net/$iface/statistics/rx_bytes" ]; then
|
||
local rx1=$(cat /sys/class/net/$iface/statistics/rx_bytes)
|
||
local tx1=$(cat /sys/class/net/$iface/statistics/tx_bytes)
|
||
|
||
sleep 0.5
|
||
|
||
local rx2=$(cat /sys/class/net/$iface/statistics/rx_bytes)
|
||
local tx2=$(cat /sys/class/net/$iface/statistics/tx_bytes)
|
||
|
||
# Calculate Delta (Bytes)
|
||
local rx_delta=$((rx2 - rx1))
|
||
local tx_delta=$((tx2 - tx1))
|
||
|
||
# Convert to Mbps: (bytes * 8 bits) / (0.5 sec * 1,000,000)
|
||
# Formula simplified: bytes * 16 / 1000000
|
||
|
||
local rx_mbps=$(awk -v b="$rx_delta" 'BEGIN { printf "%.2f", (b * 16) / 1000000 }')
|
||
local tx_mbps=$(awk -v b="$tx_delta" 'BEGIN { printf "%.2f", (b * 16) / 1000000 }')
|
||
|
||
echo "$rx_mbps $tx_mbps"
|
||
else
|
||
echo "0.00 0.00"
|
||
fi
|
||
}
|
||
|
||
show_status() {
|
||
local mode="${1:-normal}" # 'live' mode adds line clearing
|
||
local EL=""
|
||
if [ "$mode" == "live" ]; then
|
||
EL="\033[K" # Erase Line escape code
|
||
fi
|
||
|
||
echo ""
|
||
|
||
|
||
if docker ps 2>/dev/null | grep -q "[[:space:]]conduit$"; then
|
||
# Fetch stats once
|
||
local logs=$(docker logs --tail 1000 conduit 2>&1 | grep "STATS" | tail -1)
|
||
|
||
# Get Resource Stats
|
||
local stats=$(get_container_stats)
|
||
|
||
# Normalize App CPU (Docker % / Cores)
|
||
local raw_app_cpu=$(echo "$stats" | awk '{print $1}' | tr -d '%')
|
||
local num_cores=$(get_cpu_cores)
|
||
local app_cpu="0%"
|
||
local app_cpu_display=""
|
||
|
||
if [[ "$raw_app_cpu" =~ ^[0-9.]+$ ]]; then
|
||
# Use awk for floating point math
|
||
app_cpu=$(awk -v cpu="$raw_app_cpu" -v cores="$num_cores" 'BEGIN {printf "%.2f%%", cpu / cores}')
|
||
if [ "$num_cores" -gt 1 ]; then
|
||
app_cpu_display="${app_cpu} (${raw_app_cpu}% vCPU)"
|
||
else
|
||
app_cpu_display="${app_cpu}"
|
||
fi
|
||
else
|
||
app_cpu="${raw_app_cpu}%"
|
||
app_cpu_display="${app_cpu}"
|
||
fi
|
||
|
||
# Keep full "Used / Limit" string for App RAM
|
||
local app_ram=$(echo "$stats" | awk '{print $2, $3, $4}')
|
||
|
||
local sys_stats=$(get_system_stats)
|
||
local sys_cpu=$(echo "$sys_stats" | awk '{print $1}')
|
||
local sys_ram_used=$(echo "$sys_stats" | awk '{print $2}')
|
||
local sys_ram_total=$(echo "$sys_stats" | awk '{print $3}')
|
||
local sys_ram_pct=$(echo "$sys_stats" | awk '{print $4}')
|
||
|
||
local sys_ram_pct=$(echo "$sys_stats" | awk '{print $4}')
|
||
|
||
# New Metric: Network Speed (System Wide)
|
||
local net_speed=$(get_net_speed)
|
||
local rx_mbps=$(echo "$net_speed" | awk '{print $1}')
|
||
local tx_mbps=$(echo "$net_speed" | awk '{print $2}')
|
||
local net_display="↓ ${rx_mbps} Mbps ↑ ${tx_mbps} Mbps"
|
||
|
||
if [ -n "$logs" ]; then
|
||
local connecting=$(echo "$logs" | sed -n 's/.*Connecting:[[:space:]]*\([0-9]*\).*/\1/p')
|
||
local connected=$(echo "$logs" | sed -n 's/.*Connected:[[:space:]]*\([0-9]*\).*/\1/p')
|
||
local upload=$(echo "$logs" | sed -n 's/.*Up:[[:space:]]*\([^|]*\).*/\1/p' | xargs)
|
||
local download=$(echo "$logs" | sed -n 's/.*Down:[[:space:]]*\([^|]*\).*/\1/p' | xargs)
|
||
local uptime=$(echo "$logs" | sed -n 's/.*Uptime:[[:space:]]*\(.*\)/\1/p' | xargs)
|
||
|
||
# Default to 0 if missing/empty
|
||
connecting=${connecting:-0}
|
||
connected=${connected:-0}
|
||
|
||
echo -e "🚀 PSIPHON CONDUIT MANAGER v${VERSION}${EL}"
|
||
echo -e "${NC}${EL}"
|
||
|
||
if [ -n "$uptime" ]; then
|
||
echo -e "${BOLD}Status:${NC} ${GREEN}Running${NC} (${uptime}) | ${BOLD}Clients:${NC} ${GREEN}${connected}${NC} connected, ${YELLOW}${connecting}${NC} connecting${EL}"
|
||
else
|
||
echo -e "${BOLD}Status:${NC} ${GREEN}Running${NC} | ${BOLD}Clients:${NC} ${GREEN}${connected}${NC} connected, ${YELLOW}${connecting}${NC} connecting${EL}"
|
||
fi
|
||
|
||
echo -e "${EL}"
|
||
echo -e "${CYAN}═══ Traffic ═══${NC}${EL}"
|
||
[ -n "$upload" ] && echo -e " Upload: ${CYAN}${upload}${NC}${EL}"
|
||
[ -n "$download" ] && echo -e " Download: ${CYAN}${download}${NC}${EL}"
|
||
|
||
echo -e "${EL}"
|
||
echo -e "${CYAN}═══ Resource Usage ═══${NC}${EL}"
|
||
printf " %-8s CPU: ${YELLOW}%-20s${NC} | RAM: ${YELLOW}%-20s${NC}${EL}\n" "App:" "$app_cpu_display" "$app_ram"
|
||
printf " %-8s CPU: ${YELLOW}%-20s${NC} | RAM: ${YELLOW}%-20s${NC}${EL}\n" "System:" "$sys_cpu" "$sys_ram_used / $sys_ram_total"
|
||
printf " %-8s Net: ${YELLOW}%-43s${NC}${EL}\n" "Total:" "$net_display"
|
||
|
||
else
|
||
echo -e "🚀 PSIPHON CONDUIT MANAGER v${VERSION}${EL}"
|
||
echo -e "${NC}${EL}"
|
||
echo -e "${BOLD}Status:${NC} ${GREEN}Running${NC}${EL}"
|
||
echo -e "${EL}"
|
||
echo -e "${CYAN}═══ Resource Usage ═══${NC}${EL}"
|
||
printf " %-8s CPU: ${YELLOW}%-20s${NC} | RAM: ${YELLOW}%-20s${NC}${EL}\n" "App:" "$app_cpu_display" "$app_ram"
|
||
printf " %-8s CPU: ${YELLOW}%-20s${NC} | RAM: ${YELLOW}%-20s${NC}${EL}\n" "System:" "$sys_cpu" "$sys_ram_used / $sys_ram_total"
|
||
printf " %-8s Net: ${YELLOW}%-43s${NC}${EL}\n" "Total:" "$net_display"
|
||
echo -e "${EL}"
|
||
echo -e " Stats: ${YELLOW}Waiting for first stats...${NC}${EL}"
|
||
fi
|
||
|
||
else
|
||
echo -e "🚀 PSIPHON CONDUIT MANAGER v${VERSION}${EL}"
|
||
echo -e "${NC}${EL}"
|
||
echo -e "${BOLD}Status:${NC} ${RED}Stopped${NC}${EL}"
|
||
fi
|
||
|
||
|
||
|
||
echo ""
|
||
echo -e "${CYAN}═══ SETTINGS ═══${NC}${EL}"
|
||
echo -e " Max Clients: ${MAX_CLIENTS}${EL}"
|
||
if [ "$BANDWIDTH" == "-1" ]; then
|
||
echo -e " Bandwidth: Unlimited${EL}"
|
||
else
|
||
echo -e " Bandwidth: ${BANDWIDTH} Mbps${EL}"
|
||
fi
|
||
|
||
|
||
echo ""
|
||
echo -e "${CYAN}═══ AUTO-START SERVICE ═══${NC}"
|
||
# Check for systemd
|
||
if command -v systemctl &>/dev/null && systemctl is-enabled conduit.service 2>/dev/null | grep -q "enabled"; then
|
||
echo -e " Auto-start: ${GREEN}Enabled (systemd)${NC}"
|
||
local svc_status=$(systemctl is-active conduit.service 2>/dev/null)
|
||
echo -e " Service: ${svc_status:-unknown}"
|
||
# Check for OpenRC
|
||
elif command -v rc-status &>/dev/null && rc-status -a 2>/dev/null | grep -q "conduit"; then
|
||
echo -e " Auto-start: ${GREEN}Enabled (OpenRC)${NC}"
|
||
# Check for SysVinit
|
||
elif [ -f /etc/init.d/conduit ]; then
|
||
echo -e " Auto-start: ${GREEN}Enabled (SysVinit)${NC}"
|
||
else
|
||
echo -e " Auto-start: ${YELLOW}Not configured${NC}"
|
||
echo -e " Note: Docker restart policy handles restarts"
|
||
fi
|
||
echo ""
|
||
}
|
||
|
||
start_conduit() {
|
||
echo "Starting Conduit..."
|
||
|
||
# Check if container exists (running or stopped)
|
||
if docker ps -a 2>/dev/null | grep -q "[[:space:]]conduit$"; then
|
||
# Check if container is already running
|
||
if docker ps 2>/dev/null | grep -q "[[:space:]]conduit$"; then
|
||
echo -e "${GREEN}✓ Conduit is already running${NC}"
|
||
return 0
|
||
fi
|
||
|
||
# Container exists but stopped - recreate it to ensure -v flag is included
|
||
echo "Recreating container with stats enabled..."
|
||
docker rm conduit 2>/dev/null || true
|
||
fi
|
||
|
||
# Create new container
|
||
echo "Creating Conduit container..."
|
||
docker volume create conduit-data 2>/dev/null || true
|
||
|
||
fix_volume_permissions
|
||
run_conduit_container
|
||
|
||
if [ $? -eq 0 ]; then
|
||
echo -e "${GREEN}✓ Conduit started with stats enabled${NC}"
|
||
else
|
||
echo -e "${RED}✗ Failed to start Conduit${NC}"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
stop_conduit() {
|
||
echo "Stopping Conduit..."
|
||
if docker ps 2>/dev/null | grep -q "[[:space:]]conduit$"; then
|
||
docker stop conduit 2>/dev/null
|
||
echo -e "${YELLOW}✓ Conduit stopped${NC}"
|
||
else
|
||
echo -e "${YELLOW}Conduit is not running${NC}"
|
||
fi
|
||
}
|
||
|
||
restart_conduit() {
|
||
echo "Restarting Conduit..."
|
||
if docker ps -a 2>/dev/null | grep -q "[[:space:]]conduit$"; then
|
||
# Stop and remove the existing container
|
||
docker stop conduit 2>/dev/null || true
|
||
docker rm conduit 2>/dev/null || true
|
||
|
||
fix_volume_permissions
|
||
run_conduit_container
|
||
|
||
if [ $? -eq 0 ]; then
|
||
echo -e "${GREEN}✓ Conduit restarted with stats enabled${NC}"
|
||
else
|
||
echo -e "${RED}✗ Failed to restart Conduit${NC}"
|
||
return 1
|
||
fi
|
||
else
|
||
echo -e "${RED}Conduit container not found. Use 'conduit start' to create it.${NC}"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
change_settings() {
|
||
echo ""
|
||
echo -e "${CYAN}Current Settings:${NC}"
|
||
echo -e " Max Clients: ${MAX_CLIENTS}"
|
||
if [ "$BANDWIDTH" == "-1" ]; then
|
||
echo -e " Bandwidth: Unlimited"
|
||
else
|
||
echo -e " Bandwidth: ${BANDWIDTH} Mbps"
|
||
fi
|
||
echo ""
|
||
|
||
read -p "New max-clients (1-1000) [${MAX_CLIENTS}]: " new_clients < /dev/tty || true
|
||
|
||
|
||
# Bandwidth prompt logic for settings menu
|
||
echo ""
|
||
if [ "$BANDWIDTH" == "-1" ]; then
|
||
echo "Current bandwidth: Unlimited"
|
||
else
|
||
echo "Current bandwidth: ${BANDWIDTH} Mbps"
|
||
fi
|
||
read -p "Set unlimited bandwidth (-1)? [y/N]: " set_unlimited < /dev/tty || true
|
||
|
||
if [[ "$set_unlimited" =~ ^[Yy] ]]; then
|
||
new_bandwidth="-1"
|
||
else
|
||
read -p "New bandwidth in Mbps (1-40) [${BANDWIDTH}]: " input_bw < /dev/tty || true
|
||
if [ -n "$input_bw" ]; then
|
||
new_bandwidth="$input_bw"
|
||
fi
|
||
fi
|
||
|
||
# Validate max-clients
|
||
if [ -n "$new_clients" ]; then
|
||
if [[ "$new_clients" =~ ^[0-9]+$ ]] && [ "$new_clients" -ge 1 ] && [ "$new_clients" -le 1000 ]; then
|
||
MAX_CLIENTS=$new_clients
|
||
else
|
||
echo -e "${YELLOW}Invalid max-clients. Keeping current: ${MAX_CLIENTS}${NC}"
|
||
fi
|
||
fi
|
||
|
||
# Validate bandwidth
|
||
if [ -n "$new_bandwidth" ]; then
|
||
if [ "$new_bandwidth" = "-1" ]; then
|
||
BANDWIDTH="-1"
|
||
elif [[ "$new_bandwidth" =~ ^[0-9]+$ ]] && [ "$new_bandwidth" -ge 1 ] && [ "$new_bandwidth" -le 40 ]; then
|
||
BANDWIDTH=$new_bandwidth
|
||
elif [[ "$new_bandwidth" =~ ^[0-9]*\.[0-9]+$ ]]; then
|
||
local float_ok=$(awk -v val="$new_bandwidth" 'BEGIN { print (val >= 1 && val <= 40) ? "yes" : "no" }')
|
||
if [ "$float_ok" = "yes" ]; then
|
||
BANDWIDTH=$new_bandwidth
|
||
else
|
||
echo -e "${YELLOW}Invalid bandwidth. Keeping current: ${BANDWIDTH}${NC}"
|
||
fi
|
||
else
|
||
echo -e "${YELLOW}Invalid bandwidth. Keeping current: ${BANDWIDTH}${NC}"
|
||
fi
|
||
fi
|
||
|
||
# Save settings
|
||
cat > "$INSTALL_DIR/settings.conf" << EOF
|
||
MAX_CLIENTS=$MAX_CLIENTS
|
||
BANDWIDTH=$BANDWIDTH
|
||
EOF
|
||
|
||
echo ""
|
||
echo "Updating and recreating Conduit container with new settings..."
|
||
docker rm -f conduit 2>/dev/null || true
|
||
sleep 2 # Wait for container cleanup to complete
|
||
echo "Pulling latest image..."
|
||
docker pull $CONDUIT_IMAGE 2>/dev/null || echo -e "${YELLOW}Could not pull latest image, using cached version${NC}"
|
||
fix_volume_permissions
|
||
run_conduit_container
|
||
|
||
if [ $? -eq 0 ]; then
|
||
echo -e "${GREEN}✓ Settings updated and Conduit restarted${NC}"
|
||
echo -e " Max Clients: ${MAX_CLIENTS}"
|
||
if [ "$BANDWIDTH" == "-1" ]; then
|
||
echo -e " Bandwidth: Unlimited"
|
||
else
|
||
echo -e " Bandwidth: ${BANDWIDTH} Mbps"
|
||
fi
|
||
else
|
||
echo -e "${RED}✗ Failed to restart Conduit${NC}"
|
||
fi
|
||
}
|
||
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
# show_logs() - Display color-coded Docker logs
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
# Colors log entries based on their type:
|
||
# [OK] - Green (successful operations)
|
||
# [INFO] - Cyan (informational messages)
|
||
# [STATS] - Blue (statistics)
|
||
# [WARN] - Yellow (warnings)
|
||
# [ERROR] - Red (errors)
|
||
# [DEBUG] - Gray (debug messages)
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
show_logs() {
|
||
if ! docker ps -a 2>/dev/null | grep -q conduit; then
|
||
echo -e "${RED}Conduit container not found.${NC}"
|
||
return 1
|
||
fi
|
||
|
||
echo -e "${CYAN}Streaming all logs (filtered, no [STATS])... Press Ctrl+C to stop${NC}"
|
||
echo ""
|
||
|
||
# Stream ALL docker logs, filtering out [STATS] lines for cleaner output
|
||
docker logs -f conduit 2>&1 | grep -v "\[STATS\]"
|
||
}
|
||
|
||
uninstall_all() {
|
||
echo ""
|
||
echo -e "${RED}╔═══════════════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${RED}║ ⚠️ UNINSTALL CONDUIT ║${NC}"
|
||
echo -e "${RED}╚═══════════════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
echo "This will completely remove:"
|
||
echo " • Conduit Docker container"
|
||
echo " • Conduit Docker image"
|
||
echo " • Conduit data volume (all stored data)"
|
||
echo " • Auto-start service (systemd/OpenRC/SysVinit)"
|
||
echo " • Configuration files"
|
||
echo " • Management CLI"
|
||
echo ""
|
||
echo -e "${RED}WARNING: This action cannot be undone!${NC}"
|
||
echo ""
|
||
read -p "Are you sure you want to uninstall? (type 'yes' to confirm): " confirm < /dev/tty || true
|
||
|
||
if [ "$confirm" != "yes" ]; then
|
||
echo "Uninstall cancelled."
|
||
return 0
|
||
fi
|
||
|
||
# Check for backup keys
|
||
local keep_backups=false
|
||
if [ -d "$BACKUP_DIR" ] && [ "$(ls -A $BACKUP_DIR 2>/dev/null)" ]; then
|
||
echo ""
|
||
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════════${NC}"
|
||
echo -e "${YELLOW} 📁 Backup keys found in: ${BACKUP_DIR}${NC}"
|
||
echo -e "${YELLOW}═══════════════════════════════════════════════════════════════════${NC}"
|
||
echo ""
|
||
echo "You have backed up node identity keys. These allow you to restore"
|
||
echo "your node identity if you reinstall Conduit later."
|
||
echo ""
|
||
read -p "Do you want to KEEP your backup keys? (y/n): " keep_confirm < /dev/tty || true
|
||
|
||
if [ "$keep_confirm" = "y" ] || [ "$keep_confirm" = "Y" ]; then
|
||
keep_backups=true
|
||
echo -e "${GREEN}✓ Backup keys will be preserved.${NC}"
|
||
else
|
||
echo -e "${YELLOW}⚠ Backup keys will be deleted.${NC}"
|
||
fi
|
||
echo ""
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${BLUE}[INFO]${NC} Stopping Conduit container..."
|
||
docker stop conduit 2>/dev/null || true
|
||
|
||
echo -e "${BLUE}[INFO]${NC} Removing Conduit container..."
|
||
docker rm -f conduit 2>/dev/null || true
|
||
|
||
echo -e "${BLUE}[INFO]${NC} Removing Conduit Docker image..."
|
||
docker rmi "$CONDUIT_IMAGE" 2>/dev/null || true
|
||
|
||
echo -e "${BLUE}[INFO]${NC} Removing Conduit data volume..."
|
||
docker volume rm conduit-data 2>/dev/null || true
|
||
|
||
echo -e "${BLUE}[INFO]${NC} Removing auto-start service..."
|
||
# Systemd
|
||
systemctl stop conduit.service 2>/dev/null || true
|
||
systemctl disable conduit.service 2>/dev/null || true
|
||
rm -f /etc/systemd/system/conduit.service
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
# OpenRC / SysVinit
|
||
rc-service conduit stop 2>/dev/null || true
|
||
rc-update del conduit 2>/dev/null || true
|
||
service conduit stop 2>/dev/null || true
|
||
update-rc.d conduit remove 2>/dev/null || true
|
||
chkconfig conduit off 2>/dev/null || true
|
||
rm -f /etc/init.d/conduit
|
||
|
||
echo -e "${BLUE}[INFO]${NC} Removing configuration files..."
|
||
if [ "$keep_backups" = true ]; then
|
||
# Keep backup directory, remove everything else in /opt/conduit
|
||
echo -e "${BLUE}[INFO]${NC} Preserving backup keys in ${BACKUP_DIR}..."
|
||
# Remove files in /opt/conduit but keep backups subdirectory
|
||
rm -f /opt/conduit/config.env 2>/dev/null || true
|
||
rm -f /opt/conduit/conduit 2>/dev/null || true
|
||
find /opt/conduit -maxdepth 1 -type f -delete 2>/dev/null || true
|
||
else
|
||
# Remove everything including backups
|
||
rm -rf /opt/conduit
|
||
fi
|
||
rm -f /usr/local/bin/conduit
|
||
|
||
echo ""
|
||
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ UNINSTALL COMPLETE! ║${NC}"
|
||
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
echo "Conduit and all related components have been removed."
|
||
if [ "$keep_backups" = true ]; then
|
||
echo ""
|
||
echo -e "${CYAN}📁 Your backup keys are preserved in: ${BACKUP_DIR}${NC}"
|
||
echo " You can use these to restore your node identity after reinstalling."
|
||
fi
|
||
echo ""
|
||
echo "Note: Docker itself was NOT removed."
|
||
echo ""
|
||
}
|
||
|
||
show_menu() {
|
||
local redraw=true
|
||
while true; do
|
||
if [ "$redraw" = true ]; then
|
||
clear
|
||
print_header
|
||
|
||
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
|
||
echo -e "${CYAN} MANAGEMENT OPTIONS${NC}"
|
||
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
|
||
echo -e " 1. 📈 View status dashboard"
|
||
echo -e " 2. 📊 Live connection stats"
|
||
echo -e " 3. 📋 View logs (filtered)"
|
||
echo -e " 4. ⚙️ Change settings (max-clients, bandwidth)"
|
||
echo ""
|
||
echo -e " 5. 🔄 Update Conduit"
|
||
echo -e " 6. ▶️ Start Conduit"
|
||
echo -e " 7. ⏹️ Stop Conduit"
|
||
echo -e " 8. 🔁 Restart Conduit"
|
||
echo ""
|
||
echo -e " 9. 🌍 View live peers by country (Live Map)"
|
||
echo ""
|
||
echo -e " h. 🩺 Health check"
|
||
echo -e " b. 💾 Backup node key"
|
||
echo -e " r. 📥 Restore node key"
|
||
echo ""
|
||
echo -e " u. 🗑️ Uninstall (remove everything)"
|
||
echo -e " v. ℹ️ Version info"
|
||
echo -e " 0. 🚪 Exit"
|
||
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
|
||
echo ""
|
||
redraw=false
|
||
fi
|
||
|
||
read -p " Enter choice: " choice < /dev/tty || { echo "Input error. Exiting."; exit 1; }
|
||
|
||
case $choice in
|
||
1)
|
||
show_dashboard
|
||
redraw=true
|
||
;;
|
||
2)
|
||
show_live_stats
|
||
redraw=true
|
||
;;
|
||
3)
|
||
show_logs
|
||
redraw=true
|
||
;;
|
||
4)
|
||
change_settings
|
||
redraw=true
|
||
;;
|
||
5)
|
||
update_conduit
|
||
read -n 1 -s -r -p "Press any key to return..." < /dev/tty || true
|
||
redraw=true
|
||
;;
|
||
6)
|
||
start_conduit
|
||
read -n 1 -s -r -p "Press any key to return..." < /dev/tty || true
|
||
redraw=true
|
||
;;
|
||
7)
|
||
stop_conduit
|
||
read -n 1 -s -r -p "Press any key to return..." < /dev/tty || true
|
||
redraw=true
|
||
;;
|
||
8)
|
||
restart_conduit
|
||
read -n 1 -s -r -p "Press any key to return..." < /dev/tty || true
|
||
redraw=true
|
||
;;
|
||
9)
|
||
show_peers
|
||
redraw=true
|
||
;;
|
||
h|H)
|
||
health_check
|
||
read -n 1 -s -r -p "Press any key to return..." < /dev/tty || true
|
||
redraw=true
|
||
;;
|
||
b|B)
|
||
backup_key
|
||
read -n 1 -s -r -p "Press any key to return..." < /dev/tty || true
|
||
redraw=true
|
||
;;
|
||
r|R)
|
||
restore_key
|
||
read -n 1 -s -r -p "Press any key to return..." < /dev/tty || true
|
||
redraw=true
|
||
;;
|
||
u)
|
||
uninstall_all
|
||
exit 0
|
||
;;
|
||
v|V)
|
||
show_version
|
||
read -n 1 -s -r -p "Press any key to return..." < /dev/tty || true
|
||
redraw=true
|
||
;;
|
||
0)
|
||
echo "Exiting."
|
||
exit 0
|
||
;;
|
||
"")
|
||
# Ignore empty Enter key
|
||
;;
|
||
*)
|
||
echo -e "${RED}Invalid choice: ${NC}${YELLOW}$choice${NC}"
|
||
echo -e "${CYAN}Choose an option from 0-9, h, b, r, u, or v.${NC}"
|
||
;;
|
||
esac
|
||
done
|
||
}
|
||
|
||
# Command line interface
|
||
show_help() {
|
||
echo "Usage: conduit [command]"
|
||
echo ""
|
||
echo "Commands:"
|
||
echo " status Show current status with resource usage"
|
||
echo " stats View live statistics"
|
||
echo " logs View raw Docker logs"
|
||
echo " health Run health check on Conduit container"
|
||
echo " start Start Conduit container"
|
||
echo " stop Stop Conduit container"
|
||
echo " restart Restart Conduit container"
|
||
echo " update Update to latest Conduit image"
|
||
echo " settings Change max-clients/bandwidth"
|
||
echo " backup Backup Conduit node identity key"
|
||
echo " restore Restore Conduit node identity from backup"
|
||
echo " uninstall Remove everything (container, data, service)"
|
||
echo " menu Open interactive menu (default)"
|
||
echo " version Show version information"
|
||
echo " help Show this help"
|
||
}
|
||
|
||
show_version() {
|
||
echo "Conduit Manager v${VERSION}"
|
||
echo "Image: ${CONDUIT_IMAGE}"
|
||
|
||
# Show actual running image digest if available
|
||
if docker ps 2>/dev/null | grep -q "[[:space:]]conduit$"; then
|
||
local actual=$(docker inspect --format='{{index .RepoDigests 0}}' "$CONDUIT_IMAGE" 2>/dev/null | grep -o 'sha256:[a-f0-9]*')
|
||
if [ -n "$actual" ]; then
|
||
echo "Running Digest: ${actual}"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
health_check() {
|
||
echo -e "${CYAN}═══ CONDUIT HEALTH CHECK ═══${NC}"
|
||
echo ""
|
||
|
||
local all_ok=true
|
||
|
||
# 1. Check if Docker is running
|
||
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
|
||
|
||
# 2. Check if container exists
|
||
echo -n "Container exists: "
|
||
if docker ps -a 2>/dev/null | grep -q "[[:space:]]conduit$"; then
|
||
echo -e "${GREEN}OK${NC}"
|
||
else
|
||
echo -e "${RED}FAILED${NC} - Container not found"
|
||
all_ok=false
|
||
fi
|
||
|
||
# 3. Check if container is running
|
||
echo -n "Container running: "
|
||
if docker ps 2>/dev/null | grep -q "[[:space:]]conduit$"; then
|
||
echo -e "${GREEN}OK${NC}"
|
||
else
|
||
echo -e "${RED}FAILED${NC} - Container is stopped"
|
||
all_ok=false
|
||
fi
|
||
|
||
# 4. Check container health/restart count
|
||
echo -n "Restart count: "
|
||
local restarts=$(docker inspect --format='{{.RestartCount}}' conduit 2>/dev/null)
|
||
if [ -n "$restarts" ]; then
|
||
if [ "$restarts" -eq 0 ]; then
|
||
echo -e "${GREEN}${restarts}${NC} (healthy)"
|
||
elif [ "$restarts" -lt 5 ]; then
|
||
echo -e "${YELLOW}${restarts}${NC} (some restarts)"
|
||
else
|
||
echo -e "${RED}${restarts}${NC} (excessive restarts)"
|
||
all_ok=false
|
||
fi
|
||
else
|
||
echo -e "${YELLOW}N/A${NC}"
|
||
fi
|
||
|
||
# 5. Check if Conduit has connected to network
|
||
echo -n "Network connection: "
|
||
local connected=$(docker logs --tail 100 conduit 2>&1 | grep -c "Connected to Psiphon" || true)
|
||
if [ "$connected" -gt 0 ]; then
|
||
echo -e "${GREEN}OK${NC} (Connected to Psiphon network)"
|
||
else
|
||
local info_lines=$(docker logs --tail 100 conduit 2>&1 | grep -c "\[INFO\]" || true)
|
||
if [ "$info_lines" -gt 0 ]; then
|
||
echo -e "${YELLOW}CONNECTING${NC} - Establishing connection..."
|
||
else
|
||
echo -e "${YELLOW}WAITING${NC} - Starting up..."
|
||
fi
|
||
fi
|
||
|
||
# 5b. Check if STATS output is enabled (requires -v flag)
|
||
echo -n "Stats output: "
|
||
local stats_count=$(docker logs --tail 100 conduit 2>&1 | grep -c "\[STATS\]" || true)
|
||
if [ "$stats_count" -gt 0 ]; then
|
||
echo -e "${GREEN}OK${NC} (${stats_count} entries)"
|
||
else
|
||
echo -e "${YELLOW}NONE${NC} - Run 'conduit restart' to enable"
|
||
fi
|
||
|
||
# 6. Check data volume
|
||
echo -n "Data volume: "
|
||
if docker volume inspect conduit-data &>/dev/null; then
|
||
echo -e "${GREEN}OK${NC}"
|
||
else
|
||
echo -e "${RED}FAILED${NC} - Volume not found"
|
||
all_ok=false
|
||
fi
|
||
|
||
# 7. Check node key exists
|
||
echo -n "Node identity key: "
|
||
local mountpoint=$(docker volume inspect conduit-data --format '{{ .Mountpoint }}' 2>/dev/null)
|
||
if [ -n "$mountpoint" ] && [ -f "$mountpoint/conduit_key.json" ]; then
|
||
echo -e "${GREEN}OK${NC}"
|
||
else
|
||
echo -e "${YELLOW}PENDING${NC} - Will be created on first run"
|
||
fi
|
||
|
||
# 8. Check network connectivity (port binding)
|
||
echo -n "Network (host mode): "
|
||
local network_mode=$(docker inspect --format='{{.HostConfig.NetworkMode}}' conduit 2>/dev/null)
|
||
if [ "$network_mode" = "host" ]; then
|
||
echo -e "${GREEN}OK${NC}"
|
||
else
|
||
echo -e "${YELLOW}WARN${NC} - Not using host network mode"
|
||
fi
|
||
|
||
echo ""
|
||
if [ "$all_ok" = true ]; then
|
||
echo -e "${GREEN}✓ All health checks passed${NC}"
|
||
return 0
|
||
else
|
||
echo -e "${RED}✗ Some health checks failed${NC}"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
backup_key() {
|
||
echo -e "${CYAN}═══ BACKUP CONDUIT NODE KEY ═══${NC}"
|
||
echo ""
|
||
|
||
local mountpoint=$(docker volume inspect conduit-data --format '{{ .Mountpoint }}' 2>/dev/null)
|
||
|
||
if [ -z "$mountpoint" ]; then
|
||
echo -e "${RED}Error: Could not find conduit-data volume${NC}"
|
||
return 1
|
||
fi
|
||
|
||
if [ ! -f "$mountpoint/conduit_key.json" ]; then
|
||
echo -e "${RED}Error: No node key found. Has Conduit been started at least once?${NC}"
|
||
return 1
|
||
fi
|
||
|
||
# Create backup directory
|
||
mkdir -p "$INSTALL_DIR/backups"
|
||
|
||
# Create timestamped backup
|
||
local timestamp=$(date '+%Y%m%d_%H%M%S')
|
||
local backup_file="$INSTALL_DIR/backups/conduit_key_${timestamp}.json"
|
||
|
||
cp "$mountpoint/conduit_key.json" "$backup_file"
|
||
chmod 600 "$backup_file"
|
||
|
||
# Get node ID for display
|
||
local node_id=$(cat "$mountpoint/conduit_key.json" | grep "privateKeyBase64" | awk -F'"' '{print $4}' | base64 -d 2>/dev/null | tail -c 32 | base64 | tr -d '=\n')
|
||
|
||
echo -e "${GREEN}✓ Backup created successfully${NC}"
|
||
echo ""
|
||
echo " Backup file: ${CYAN}${backup_file}${NC}"
|
||
echo " Node ID: ${CYAN}${node_id}${NC}"
|
||
echo ""
|
||
echo -e "${YELLOW}Important:${NC} Store this backup securely. It contains your node's"
|
||
echo "private key which identifies your node on the Psiphon network."
|
||
echo ""
|
||
|
||
# List all backups
|
||
echo "All backups:"
|
||
ls -la "$INSTALL_DIR/backups/"*.json 2>/dev/null | awk '{print " " $9 " (" $5 " bytes)"}'
|
||
}
|
||
|
||
restore_key() {
|
||
echo -e "${CYAN}═══ RESTORE CONDUIT NODE KEY ═══${NC}"
|
||
echo ""
|
||
|
||
local backup_dir="$INSTALL_DIR/backups"
|
||
|
||
# Check if backup directory exists and has files
|
||
if [ ! -d "$backup_dir" ] || [ -z "$(ls -A $backup_dir/*.json 2>/dev/null)" ]; then
|
||
echo -e "${YELLOW}No backups found in ${backup_dir}${NC}"
|
||
echo ""
|
||
echo "To restore from a custom path, provide the file path:"
|
||
read -p " Backup file path (or press Enter to cancel): " custom_path < /dev/tty || true
|
||
|
||
if [ -z "$custom_path" ]; then
|
||
echo "Restore cancelled."
|
||
return 0
|
||
fi
|
||
|
||
if [ ! -f "$custom_path" ]; then
|
||
echo -e "${RED}Error: File not found: ${custom_path}${NC}"
|
||
return 1
|
||
fi
|
||
|
||
backup_file="$custom_path"
|
||
else
|
||
# List available backups
|
||
echo "Available backups:"
|
||
local i=1
|
||
local backups=()
|
||
for f in "$backup_dir"/*.json; do
|
||
backups+=("$f")
|
||
local node_id=$(cat "$f" | grep "privateKeyBase64" | awk -F'"' '{print $4}' | base64 -d 2>/dev/null | tail -c 32 | base64 | tr -d '=\n' 2>/dev/null)
|
||
echo " ${i}. $(basename "$f") - Node: ${node_id:-unknown}"
|
||
i=$((i + 1))
|
||
done
|
||
echo ""
|
||
|
||
read -p " Select backup number (or 0 to cancel): " selection < /dev/tty || true
|
||
|
||
if [ "$selection" = "0" ] || [ -z "$selection" ]; then
|
||
echo "Restore cancelled."
|
||
return 0
|
||
fi
|
||
|
||
if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt ${#backups[@]} ]; then
|
||
echo -e "${RED}Invalid selection${NC}"
|
||
return 1
|
||
fi
|
||
|
||
backup_file="${backups[$((selection - 1))]}"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${YELLOW}Warning:${NC} This will replace the current node key."
|
||
echo "The container will be stopped and restarted."
|
||
echo ""
|
||
read -p "Proceed with restore? [y/N] " confirm < /dev/tty || true
|
||
|
||
if [[ ! "$confirm" =~ ^[Yy] ]]; then
|
||
echo "Restore cancelled."
|
||
return 0
|
||
fi
|
||
|
||
# Stop container
|
||
echo ""
|
||
echo "Stopping Conduit..."
|
||
docker stop conduit 2>/dev/null || true
|
||
|
||
# Get volume mountpoint
|
||
local mountpoint=$(docker volume inspect conduit-data --format '{{ .Mountpoint }}' 2>/dev/null)
|
||
|
||
if [ -z "$mountpoint" ]; then
|
||
echo -e "${RED}Error: Could not find conduit-data volume${NC}"
|
||
return 1
|
||
fi
|
||
|
||
# Backup current key if exists
|
||
if [ -f "$mountpoint/conduit_key.json" ]; then
|
||
local timestamp=$(date '+%Y%m%d_%H%M%S')
|
||
mkdir -p "$backup_dir"
|
||
cp "$mountpoint/conduit_key.json" "$backup_dir/conduit_key_pre_restore_${timestamp}.json"
|
||
echo " Current key backed up to: conduit_key_pre_restore_${timestamp}.json"
|
||
fi
|
||
|
||
# Restore the key
|
||
cp "$backup_file" "$mountpoint/conduit_key.json"
|
||
chmod 600 "$mountpoint/conduit_key.json"
|
||
|
||
# Restart container
|
||
echo "Starting Conduit..."
|
||
docker start conduit 2>/dev/null
|
||
|
||
local node_id=$(cat "$mountpoint/conduit_key.json" | grep "privateKeyBase64" | awk -F'"' '{print $4}' | base64 -d 2>/dev/null | tail -c 32 | base64 | tr -d '=\n')
|
||
|
||
echo ""
|
||
echo -e "${GREEN}✓ Node key restored successfully${NC}"
|
||
echo " Node ID: ${CYAN}${node_id}${NC}"
|
||
}
|
||
|
||
update_conduit() {
|
||
echo -e "${CYAN}═══ UPDATE CONDUIT ═══${NC}"
|
||
echo ""
|
||
|
||
echo "Current image: ${CONDUIT_IMAGE}"
|
||
echo ""
|
||
|
||
# Check for updates by pulling
|
||
echo "Checking for updates..."
|
||
if ! docker pull $CONDUIT_IMAGE 2>/dev/null; then
|
||
echo -e "${RED}Failed to check for updates. Check your internet connection.${NC}"
|
||
return 1
|
||
fi
|
||
|
||
|
||
echo ""
|
||
echo "Recreating container with updated image..."
|
||
|
||
# Save if container was running
|
||
local was_running=false
|
||
if docker ps 2>/dev/null | grep -q "[[:space:]]conduit$"; then
|
||
was_running=true
|
||
fi
|
||
|
||
# Remove old container
|
||
docker rm -f conduit 2>/dev/null || true
|
||
|
||
fix_volume_permissions
|
||
run_conduit_container
|
||
|
||
if [ $? -eq 0 ]; then
|
||
echo -e "${GREEN}✓ Conduit updated and restarted${NC}"
|
||
else
|
||
echo -e "${RED}✗ Failed to start updated container${NC}"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
case "${1:-menu}" in
|
||
status) show_status ;;
|
||
stats) show_live_stats ;;
|
||
logs) show_logs ;;
|
||
health) health_check ;;
|
||
start) start_conduit ;;
|
||
stop) stop_conduit ;;
|
||
restart) restart_conduit ;;
|
||
update) update_conduit ;;
|
||
peers) show_peers ;;
|
||
settings) change_settings ;;
|
||
backup) backup_key ;;
|
||
restore) restore_key ;;
|
||
uninstall) uninstall_all ;;
|
||
version|-v|--version) show_version ;;
|
||
help|-h|--help) show_help ;;
|
||
menu|*) show_menu ;;
|
||
esac
|
||
MANAGEMENT
|
||
|
||
# Patch the INSTALL_DIR in the generated script
|
||
sed -i "s#REPLACE_ME_INSTALL_DIR#$INSTALL_DIR#g" "$INSTALL_DIR/conduit"
|
||
|
||
chmod +x "$INSTALL_DIR/conduit"
|
||
# Force create symlink
|
||
rm -f /usr/local/bin/conduit 2>/dev/null || true
|
||
ln -s "$INSTALL_DIR/conduit" /usr/local/bin/conduit
|
||
|
||
log_success "Management script installed: conduit"
|
||
}
|
||
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
# Summary
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
|
||
print_summary() {
|
||
local init_type="Enabled"
|
||
if [ "$HAS_SYSTEMD" = "true" ]; then
|
||
init_type="Enabled (systemd)"
|
||
elif command -v rc-update &>/dev/null; then
|
||
init_type="Enabled (OpenRC)"
|
||
elif [ -d /etc/init.d ]; then
|
||
init_type="Enabled (SysVinit)"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ INSTALLATION COMPLETE! ║${NC}"
|
||
echo -e "${GREEN}╠═══════════════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║${NC} Conduit is running and ready to help users! ${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} 📊 Settings: ${GREEN}║${NC}"
|
||
printf "${GREEN}║${NC} Max Clients: ${CYAN}%-4s${NC} ${GREEN}║${NC}\n" "${MAX_CLIENTS}"
|
||
if [ "$BANDWIDTH" == "-1" ]; then
|
||
echo -e "${GREEN}║${NC} Bandwidth: ${CYAN}Unlimited${NC} ${GREEN}║${NC}"
|
||
else
|
||
printf "${GREEN}║${NC} Bandwidth: ${CYAN}%-4s${NC} Mbps ${GREEN}║${NC}\n" "${BANDWIDTH}"
|
||
fi
|
||
printf "${GREEN}║${NC} Auto-start: ${CYAN}%-20s${NC} ${GREEN}║${NC}\n" "${init_type}"
|
||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||
echo -e "${GREEN}╠═══════════════════════════════════════════════════════════════════╣${NC}"
|
||
echo -e "${GREEN}║${NC} COMMANDS: ${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} ${CYAN}conduit${NC} # Open management menu ${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} ${CYAN}conduit stats${NC} # View live statistics + CPU/RAM ${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} ${CYAN}conduit status${NC} # Quick status with resource usage ${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} ${CYAN}conduit logs${NC} # View raw logs ${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} ${CYAN}conduit settings${NC} # Change max-clients/bandwidth ${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} ${CYAN}conduit uninstall${NC} # Remove everything ${GREEN}║${NC}"
|
||
echo -e "${GREEN}║${NC} ${GREEN}║${NC}"
|
||
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
echo -e " ${YELLOW}View live stats now:${NC} conduit stats"
|
||
echo ""
|
||
}
|
||
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
# Uninstall Function
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
|
||
uninstall() {
|
||
echo ""
|
||
echo -e "${CYAN}╔═══════════════════════════════════════════════════════════════════╗${NC}"
|
||
echo "║ ⚠️ UNINSTALL CONDUIT ║"
|
||
echo "╚═══════════════════════════════════════════════════════════════════╝"
|
||
echo ""
|
||
echo "This will completely remove:"
|
||
echo " • Conduit Docker container"
|
||
echo " • Conduit Docker image"
|
||
echo " • Conduit data volume (all stored data)"
|
||
echo " • Auto-start service (systemd/OpenRC/SysVinit)"
|
||
echo " • Configuration files"
|
||
echo " • Management CLI"
|
||
echo ""
|
||
echo -e "${RED}WARNING: This action cannot be undone!${NC}"
|
||
echo ""
|
||
read -p "Are you sure you want to uninstall? (type 'yes' to confirm): " confirm < /dev/tty || true
|
||
|
||
if [ "$confirm" != "yes" ]; then
|
||
echo "Uninstall cancelled."
|
||
exit 0
|
||
fi
|
||
|
||
echo ""
|
||
log_info "Stopping Conduit container..."
|
||
docker stop conduit 2>/dev/null || true
|
||
|
||
log_info "Removing Conduit container..."
|
||
docker rm -f conduit 2>/dev/null || true
|
||
|
||
log_info "Removing Conduit Docker image..."
|
||
docker rmi "$CONDUIT_IMAGE" 2>/dev/null || true
|
||
|
||
log_info "Removing Conduit data volume..."
|
||
docker volume rm conduit-data 2>/dev/null || true
|
||
|
||
log_info "Removing auto-start service..."
|
||
# Systemd
|
||
systemctl stop conduit.service 2>/dev/null || true
|
||
systemctl disable conduit.service 2>/dev/null || true
|
||
rm -f /etc/systemd/system/conduit.service
|
||
systemctl daemon-reload 2>/dev/null || true
|
||
# OpenRC / SysVinit
|
||
rc-service conduit stop 2>/dev/null || true
|
||
rc-update del conduit 2>/dev/null || true
|
||
service conduit stop 2>/dev/null || true
|
||
update-rc.d conduit remove 2>/dev/null || true
|
||
chkconfig conduit off 2>/dev/null || true
|
||
rm -f /etc/init.d/conduit
|
||
|
||
log_info "Removing configuration files..."
|
||
rm -rf "$INSTALL_DIR"
|
||
rm -f /usr/local/bin/conduit
|
||
|
||
echo ""
|
||
echo -e "${GREEN}╔═══════════════════════════════════════════════════════════════════╗${NC}"
|
||
echo -e "${GREEN}║ ✅ UNINSTALL COMPLETE! ║${NC}"
|
||
echo -e "${GREEN}╚═══════════════════════════════════════════════════════════════════╝${NC}"
|
||
echo ""
|
||
echo "Conduit and all related components have been removed."
|
||
echo ""
|
||
echo "Note: Docker itself was NOT removed."
|
||
echo ""
|
||
}
|
||
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
# Main
|
||
#═══════════════════════════════════════════════════════════════════════
|
||
|
||
show_usage() {
|
||
echo "Psiphon Conduit Manager v${VERSION}"
|
||
echo ""
|
||
echo "Usage: $0 [OPTIONS]"
|
||
echo ""
|
||
echo "Options:"
|
||
echo " (no args) Install or open management menu if already installed"
|
||
echo " --reinstall Force fresh reinstall"
|
||
echo " --uninstall Completely remove Conduit and all components"
|
||
echo " --help, -h Show this help message"
|
||
echo ""
|
||
echo "Examples:"
|
||
echo " sudo bash $0 # Install or open menu"
|
||
echo " sudo bash $0 --reinstall # Fresh install"
|
||
echo " sudo bash $0 --uninstall # Remove everything"
|
||
echo ""
|
||
echo "After install, use: conduit"
|
||
}
|
||
|
||
main() {
|
||
# Handle command line arguments
|
||
case "${1:-}" in
|
||
--uninstall|-u)
|
||
check_root
|
||
uninstall
|
||
exit 0
|
||
;;
|
||
--help|-h)
|
||
show_usage
|
||
exit 0
|
||
;;
|
||
--reinstall)
|
||
# Force reinstall
|
||
FORCE_REINSTALL=true
|
||
;;
|
||
esac
|
||
|
||
print_header
|
||
check_root
|
||
detect_os
|
||
|
||
# Ensure all tools (including new ones like tcpdump) are present
|
||
check_dependencies
|
||
|
||
# Check if already installed
|
||
if [ -f "$INSTALL_DIR/conduit" ] && [ "$FORCE_REINSTALL" != "true" ]; then
|
||
echo -e "${GREEN}Conduit is already installed!${NC}"
|
||
echo ""
|
||
echo "What would you like to do?"
|
||
echo ""
|
||
echo " 1. 📊 Open management menu"
|
||
echo " 2. 🔄 Reinstall (fresh install)"
|
||
echo " 3. 🗑️ Uninstall"
|
||
echo " 0. 🚪 Exit"
|
||
echo ""
|
||
read -p " Enter choice: " choice < /dev/tty || true
|
||
|
||
case $choice in
|
||
1)
|
||
echo -e "${CYAN}Updating management script and opening menu...${NC}"
|
||
create_management_script
|
||
exec "$INSTALL_DIR/conduit" menu
|
||
;;
|
||
2)
|
||
echo ""
|
||
log_info "Starting fresh reinstall..."
|
||
;;
|
||
3)
|
||
uninstall
|
||
exit 0
|
||
;;
|
||
0)
|
||
echo "Exiting."
|
||
exit 0
|
||
;;
|
||
*)
|
||
echo -e "${RED}Invalid choice: ${NC}${YELLOW}$choice${NC}"
|
||
echo -e "${CYAN}Returning to installer...${NC}"
|
||
sleep 1
|
||
main "$@"
|
||
;;
|
||
esac
|
||
fi
|
||
|
||
# Interactive settings prompt (max-clients, bandwidth)
|
||
prompt_settings
|
||
|
||
echo ""
|
||
echo -e "${CYAN}Starting installation...${NC}"
|
||
echo ""
|
||
|
||
#───────────────────────────────────────────────────────────────
|
||
# Installation Steps (5 steps if backup exists, otherwise 4)
|
||
#───────────────────────────────────────────────────────────────
|
||
|
||
# Step 1: Install Docker (if not already installed)
|
||
log_info "Step 1/5: Installing Docker..."
|
||
install_docker
|
||
|
||
echo ""
|
||
|
||
# Step 2: Check for and optionally restore backup keys
|
||
# This preserves node identity if user had a previous installation
|
||
log_info "Step 2/5: Checking for previous node identity..."
|
||
check_and_offer_backup_restore
|
||
|
||
echo ""
|
||
|
||
# Step 3: Start Conduit container
|
||
log_info "Step 3/5: Starting Conduit..."
|
||
run_conduit
|
||
|
||
echo ""
|
||
|
||
# Step 4: Save settings and configure auto-start service
|
||
log_info "Step 4/5: Setting up auto-start..."
|
||
save_settings
|
||
setup_autostart
|
||
|
||
echo ""
|
||
|
||
# Step 5: Create the 'conduit' CLI management script
|
||
log_info "Step 5/5: Creating management script..."
|
||
create_management_script
|
||
|
||
print_summary
|
||
|
||
read -p "Open management menu now? [Y/n] " open_menu < /dev/tty || true
|
||
if [[ ! "$open_menu" =~ ^[Nn] ]]; then
|
||
"$INSTALL_DIR/conduit" menu
|
||
fi
|
||
}
|
||
#
|
||
# REACHED END OF SCRIPT - VERSION 1.0.2
|
||
# ###############################################################################
|
||
main "$@"
|