The start-both.sh script was sourcing paqctl.conf which doesn't exist. Changed to settings.conf where LISTEN_PORT and GFK_VIO_PORT are saved. This caused iptables rules to always use default ports (8443/45000) even when user configured different ports, resulting in port mismatch.
6577 lines
258 KiB
Bash
6577 lines
258 KiB
Bash
#!/bin/bash
|
|
#
|
|
# ╔═══════════════════════════════════════════════════════════════════╗
|
|
# ║ PAQCTL - Paqet Manager v1.0.0 ║
|
|
# ║ ║
|
|
# ║ One-click setup for Paqet raw-socket proxy ║
|
|
# ║ ║
|
|
# ║ * Installs paqet binary + libpcap ║
|
|
# ║ * Auto-detects network config ║
|
|
# ║ * Configures server or client mode ║
|
|
# ║ * Manages iptables rules ║
|
|
# ║ * Auto-start on boot via systemd/OpenRC/SysVinit ║
|
|
# ║ * Easy management via CLI or interactive menu ║
|
|
# ║ ║
|
|
# ║ Paqet: https://github.com/SamNet-dev/paqctl ║
|
|
# ╚═══════════════════════════════════════════════════════════════════╝
|
|
#
|
|
# Usage:
|
|
# curl -sL https://raw.githubusercontent.com/SamNet-dev/paqctl/main/paqctl.sh | sudo bash
|
|
#
|
|
# Or: wget paqctl.sh && sudo bash paqctl.sh
|
|
#
|
|
|
|
set -eo pipefail
|
|
|
|
# Require bash
|
|
if [ -z "$BASH_VERSION" ]; then
|
|
echo "Error: This script requires bash. Please run with: bash $0"
|
|
exit 1
|
|
fi
|
|
|
|
VERSION="1.0.0"
|
|
|
|
# Pinned versions for stability (update these after testing new releases)
|
|
PAQET_VERSION_PINNED="v1.0.0-alpha.13"
|
|
XRAY_VERSION_PINNED="v26.2.4"
|
|
GFK_VERSION_PINNED="v1.0.0"
|
|
|
|
PAQET_REPO="hanselime/paqet"
|
|
PAQET_API_URL="https://api.github.com/repos/${PAQET_REPO}/releases/latest"
|
|
INSTALL_DIR="${INSTALL_DIR:-/opt/paqctl}"
|
|
BACKUP_DIR="$INSTALL_DIR/backups"
|
|
GFK_REPO="SamNet-dev/paqctl"
|
|
GFK_BRANCH="main"
|
|
GFK_RAW_URL="https://raw.githubusercontent.com/${GFK_REPO}/${GFK_BRANCH}/gfk"
|
|
GFK_DIR="$INSTALL_DIR/gfk"
|
|
MICROSOCKS_REPO="rofl0r/microsocks"
|
|
BACKEND="${BACKEND:-paqet}"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
MAGENTA='\033[0;35m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
NC='\033[0m'
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Utility Functions
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
print_header() {
|
|
echo -e "${CYAN}"
|
|
echo "╔════════════════════════════════════════════════════════════════╗"
|
|
echo "║ PAQCTL - Paqet Manager v${VERSION} ║"
|
|
echo "║ Raw-socket encrypted proxy - bypass firewalls ║"
|
|
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"
|
|
|
|
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
|
|
|
|
case "$OS" in
|
|
ubuntu|debian|linuxmint|pop|elementary|zorin|kali|raspbian)
|
|
OS_FAMILY="debian"
|
|
PKG_MANAGER="apt"
|
|
;;
|
|
rhel|centos|fedora|rocky|almalinux|oracle|amazon|amzn)
|
|
OS_FAMILY="rhel"
|
|
if command -v dnf &>/dev/null; then
|
|
PKG_MANAGER="dnf"
|
|
else
|
|
PKG_MANAGER="yum"
|
|
fi
|
|
;;
|
|
arch|manjaro|endeavouros|garuda)
|
|
OS_FAMILY="arch"
|
|
PKG_MANAGER="pacman"
|
|
;;
|
|
opensuse|opensuse-leap|opensuse-tumbleweed|sles)
|
|
OS_FAMILY="suse"
|
|
PKG_MANAGER="zypper"
|
|
;;
|
|
alpine)
|
|
OS_FAMILY="alpine"
|
|
PKG_MANAGER="apk"
|
|
;;
|
|
*)
|
|
OS_FAMILY="unknown"
|
|
PKG_MANAGER="unknown"
|
|
;;
|
|
esac
|
|
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
HAS_SYSTEMD=true
|
|
fi
|
|
|
|
log_info "Detected: $OS ($OS_FAMILY family), Package manager: $PKG_MANAGER"
|
|
}
|
|
|
|
install_package() {
|
|
local package="$1"
|
|
log_info "Installing $package..."
|
|
|
|
case "$PKG_MANAGER" in
|
|
apt)
|
|
apt-get update -q 2>/dev/null || log_warn "apt-get update failed, attempting install anyway..."
|
|
if apt-get install -y -q "$package"; then
|
|
log_success "$package installed successfully"
|
|
else
|
|
log_error "Failed to install $package"
|
|
return 1
|
|
fi
|
|
;;
|
|
dnf)
|
|
if dnf install -y -q "$package"; then
|
|
log_success "$package installed successfully"
|
|
else
|
|
log_error "Failed to install $package"
|
|
return 1
|
|
fi
|
|
;;
|
|
yum)
|
|
if yum install -y -q "$package"; then
|
|
log_success "$package installed successfully"
|
|
else
|
|
log_error "Failed to install $package"
|
|
return 1
|
|
fi
|
|
;;
|
|
pacman)
|
|
if pacman -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() {
|
|
if [ "$OS_FAMILY" = "alpine" ]; then
|
|
if ! command -v bash &>/dev/null; then
|
|
apk add --no-cache bash 2>/dev/null
|
|
fi
|
|
fi
|
|
|
|
if ! command -v curl &>/dev/null; then
|
|
install_package curl || log_warn "Could not install curl automatically"
|
|
fi
|
|
|
|
if ! command -v tar &>/dev/null; then
|
|
install_package tar || log_warn "Could not install tar automatically"
|
|
fi
|
|
|
|
if ! command -v ip &>/dev/null; then
|
|
case "$PKG_MANAGER" in
|
|
apt) install_package iproute2 || log_warn "Could not install iproute2" ;;
|
|
dnf|yum) install_package iproute || log_warn "Could not install iproute" ;;
|
|
pacman) install_package iproute2 || log_warn "Could not install iproute2" ;;
|
|
zypper) install_package iproute2 || log_warn "Could not install iproute2" ;;
|
|
apk) install_package iproute2 || log_warn "Could not install iproute2" ;;
|
|
esac
|
|
fi
|
|
|
|
if ! command -v tput &>/dev/null; then
|
|
case "$PKG_MANAGER" in
|
|
apt) install_package ncurses-bin || log_warn "Could not install ncurses-bin" ;;
|
|
apk) install_package ncurses || log_warn "Could not install ncurses" ;;
|
|
*) install_package ncurses || log_warn "Could not install ncurses" ;;
|
|
esac
|
|
fi
|
|
|
|
# iptables is required for firewall rules (server mode)
|
|
if ! command -v iptables &>/dev/null; then
|
|
log_info "Installing iptables..."
|
|
case "$PKG_MANAGER" in
|
|
apt) install_package iptables || log_warn "Could not install iptables - firewall rules may not work" ;;
|
|
dnf|yum) install_package iptables || log_warn "Could not install iptables" ;;
|
|
pacman) install_package iptables || log_warn "Could not install iptables" ;;
|
|
zypper) install_package iptables || log_warn "Could not install iptables" ;;
|
|
apk) install_package iptables || log_warn "Could not install iptables" ;;
|
|
*) log_warn "Please install iptables manually for firewall rules to work" ;;
|
|
esac
|
|
fi
|
|
|
|
# libpcap is required by paqet
|
|
install_libpcap
|
|
}
|
|
|
|
install_libpcap() {
|
|
log_info "Checking for libpcap..."
|
|
|
|
# Check if already available
|
|
if ldconfig -p 2>/dev/null | grep -q libpcap; then
|
|
log_success "libpcap already installed"
|
|
return 0
|
|
fi
|
|
|
|
case "$PKG_MANAGER" in
|
|
apt) install_package libpcap-dev ;;
|
|
dnf|yum) install_package libpcap-devel ;;
|
|
pacman) install_package libpcap ;;
|
|
zypper) install_package libpcap-devel ;;
|
|
apk) install_package libpcap-dev ;;
|
|
*) log_warn "Please install libpcap manually for your distribution"; return 1 ;;
|
|
esac
|
|
}
|
|
|
|
detect_arch() {
|
|
local arch
|
|
arch=$(uname -m)
|
|
case "$arch" in
|
|
x86_64|amd64) echo "amd64" ;;
|
|
aarch64|arm64) echo "arm64" ;;
|
|
*)
|
|
log_error "Unsupported architecture: $arch"
|
|
log_error "Paqet supports amd64 and arm64 only"
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Input Validation Functions
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
_validate_port() { [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]; }
|
|
|
|
_validate_ip() {
|
|
[[ "$1" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1
|
|
local IFS='.'; set -- $1
|
|
[ "$1" -le 255 ] && [ "$2" -le 255 ] && [ "$3" -le 255 ] && [ "$4" -le 255 ]
|
|
}
|
|
|
|
_validate_mac() { [[ "$1" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; }
|
|
|
|
_validate_iface() { [[ "$1" =~ ^[a-zA-Z0-9._-]+$ ]] && [ ${#1} -le 64 ]; }
|
|
|
|
_validate_version_tag() {
|
|
[[ "$1" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]]
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Binary Download & Install
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
# Retry helper with exponential backoff for API requests
|
|
_curl_with_retry() {
|
|
local url="$1"
|
|
local max_attempts="${2:-3}"
|
|
local attempt=1
|
|
local delay=2
|
|
local response=""
|
|
while [ $attempt -le $max_attempts ]; do
|
|
response=$(curl -s --max-time 15 "$url" 2>/dev/null)
|
|
if [ -n "$response" ]; then
|
|
# Check for rate limit response
|
|
if echo "$response" | grep -q '"message".*rate limit'; then
|
|
log_warn "GitHub API rate limited, waiting ${delay}s (attempt $attempt/$max_attempts)"
|
|
sleep $delay
|
|
delay=$((delay * 2))
|
|
attempt=$((attempt + 1))
|
|
continue
|
|
fi
|
|
echo "$response"
|
|
return 0
|
|
fi
|
|
[ $attempt -lt $max_attempts ] && sleep $delay
|
|
delay=$((delay * 2))
|
|
attempt=$((attempt + 1))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
get_latest_version() {
|
|
local response
|
|
response=$(_curl_with_retry "$PAQET_API_URL" 3)
|
|
if [ -z "$response" ]; then
|
|
log_error "Failed to query GitHub API after retries"
|
|
return 1
|
|
fi
|
|
local tag
|
|
tag=$(echo "$response" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"')
|
|
if [ -z "$tag" ]; then
|
|
log_error "Could not determine latest paqet version"
|
|
return 1
|
|
fi
|
|
if ! _validate_version_tag "$tag"; then
|
|
log_error "Invalid version tag format: $tag"
|
|
return 1
|
|
fi
|
|
echo "$tag"
|
|
}
|
|
|
|
download_paqet() {
|
|
local version="$1"
|
|
local arch
|
|
arch=$(detect_arch)
|
|
local os_name="linux"
|
|
local ext="tar.gz"
|
|
local filename="paqet-${os_name}-${arch}-${version}.${ext}"
|
|
local url="https://github.com/${PAQET_REPO}/releases/download/${version}/${filename}"
|
|
|
|
log_info "Downloading paqet ${version} for ${os_name}/${arch}..."
|
|
|
|
if ! mkdir -p "$INSTALL_DIR/bin"; then
|
|
log_error "Failed to create directory $INSTALL_DIR/bin"
|
|
return 1
|
|
fi
|
|
local tmp_file
|
|
tmp_file=$(mktemp "/tmp/paqet-download-XXXXXXXX.${ext}") || { log_error "Failed to create temp file"; return 1; }
|
|
|
|
if ! curl -sL --max-time 120 --fail -o "$tmp_file" "$url"; then
|
|
log_error "Failed to download: $url"
|
|
rm -f "$tmp_file"
|
|
return 1
|
|
fi
|
|
|
|
# Validate download
|
|
local fsize
|
|
fsize=$(stat -c%s "$tmp_file" 2>/dev/null || stat -f%z "$tmp_file" 2>/dev/null || wc -c < "$tmp_file" 2>/dev/null || echo 0)
|
|
if [ "$fsize" -lt 1000 ]; then
|
|
log_error "Downloaded file is too small ($fsize bytes). Download may have failed."
|
|
rm -f "$tmp_file"
|
|
return 1
|
|
fi
|
|
|
|
# Extract
|
|
log_info "Extracting..."
|
|
local tmp_extract
|
|
tmp_extract=$(mktemp -d "/tmp/paqet-extract-XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
|
|
if ! tar -xzf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then
|
|
log_error "Failed to extract archive"
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
return 1
|
|
fi
|
|
|
|
# Find the binary in extracted files
|
|
local binary_name="paqet_${os_name}_${arch}"
|
|
local found_binary=""
|
|
found_binary=$(find "$tmp_extract" -name "$binary_name" -type f 2>/dev/null | head -1)
|
|
if [ -z "$found_binary" ]; then
|
|
# Try alternate name patterns
|
|
found_binary=$(find "$tmp_extract" -name "paqet*" -type f -executable 2>/dev/null | head -1)
|
|
fi
|
|
if [ -z "$found_binary" ]; then
|
|
found_binary=$(find "$tmp_extract" -name "paqet*" -type f 2>/dev/null | head -1)
|
|
fi
|
|
|
|
if [ -z "$found_binary" ]; then
|
|
log_error "Could not find paqet binary in archive"
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
return 1
|
|
fi
|
|
|
|
# Stop paqet if running to avoid "Text file busy" error
|
|
if pgrep -f "$INSTALL_DIR/bin/paqet" &>/dev/null; then
|
|
log_info "Stopping paqet to update binary..."
|
|
pkill -f "$INSTALL_DIR/bin/paqet" 2>/dev/null || true
|
|
sleep 1
|
|
fi
|
|
|
|
if ! cp "$found_binary" "$INSTALL_DIR/bin/paqet"; then
|
|
log_error "Failed to copy paqet binary to $INSTALL_DIR/bin/"
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
return 1
|
|
fi
|
|
if ! chmod +x "$INSTALL_DIR/bin/paqet"; then
|
|
log_error "Failed to make paqet binary executable"
|
|
return 1
|
|
fi
|
|
|
|
# Copy example configs if they exist
|
|
find "$tmp_extract" -name "*.yaml.example" -exec cp {} "$INSTALL_DIR/" \; 2>/dev/null || true
|
|
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
|
|
# Verify binary runs
|
|
if "$INSTALL_DIR/bin/paqet" version &>/dev/null; then
|
|
log_success "paqet ${version} installed successfully"
|
|
else
|
|
log_warn "paqet binary installed but version check failed (may need libpcap)"
|
|
fi
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Network Auto-Detection
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
detect_network() {
|
|
log_info "Auto-detecting network configuration..."
|
|
|
|
# Default interface
|
|
DETECTED_IFACE=$(ip route show default 2>/dev/null | awk '{print $5; exit}')
|
|
if [ -z "$DETECTED_IFACE" ]; then
|
|
# Skip loopback, docker, veth, bridge, and other virtual interfaces
|
|
DETECTED_IFACE=$(ip -o link show 2>/dev/null | awk -F': ' '{gsub(/ /,"",$2); print $2}' | grep -vE '^(lo|docker[0-9]|br-|veth|virbr|tun|tap|wg)' | head -1)
|
|
fi
|
|
|
|
# Local IP
|
|
if [ -n "$DETECTED_IFACE" ]; then
|
|
DETECTED_IP=$(ip -4 addr show "$DETECTED_IFACE" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | grep -o '[0-9.]*' | head -1)
|
|
fi
|
|
if [ -z "$DETECTED_IP" ]; then
|
|
DETECTED_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
[ -z "$DETECTED_IP" ] && DETECTED_IP=$(ip -4 addr show scope global 2>/dev/null | awk '/inet /{gsub(/\/.*/, "", $2); print $2; exit}')
|
|
fi
|
|
|
|
# Gateway IP
|
|
DETECTED_GATEWAY=$(ip route show default 2>/dev/null | awk '{print $3; exit}')
|
|
|
|
# Gateway MAC
|
|
DETECTED_GW_MAC=""
|
|
if [ -n "$DETECTED_GATEWAY" ]; then
|
|
# Try ip neigh first (most reliable on Linux)
|
|
DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}')
|
|
if [ -z "$DETECTED_GW_MAC" ]; then
|
|
# Trigger ARP resolution
|
|
ping -c 1 -W 2 "$DETECTED_GATEWAY" &>/dev/null || true
|
|
sleep 1
|
|
DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}')
|
|
fi
|
|
if [ -z "$DETECTED_GW_MAC" ] && command -v arp &>/dev/null; then
|
|
# Fallback: parse arp output looking for MAC pattern
|
|
DETECTED_GW_MAC=$(arp -n "$DETECTED_GATEWAY" 2>/dev/null | grep -oE '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' | head -1)
|
|
fi
|
|
fi
|
|
|
|
log_info "Interface: ${DETECTED_IFACE:-unknown}"
|
|
log_info "Local IP: ${DETECTED_IP:-unknown}"
|
|
log_info "Gateway: ${DETECTED_GATEWAY:-unknown}"
|
|
log_info "GW MAC: ${DETECTED_GW_MAC:-unknown}"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Configuration Wizard
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
run_config_wizard() {
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} PAQCTL CONFIGURATION WIZARD${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
# Backend selection
|
|
echo -e "${BOLD}Select backend:${NC}"
|
|
echo " 1. paqet (Go/KCP, built-in SOCKS5, single binary)"
|
|
echo " 2. gfw-knocker (Python/QUIC, port forwarding + microsocks)"
|
|
echo ""
|
|
local backend_choice
|
|
read -p " Enter choice [1/2]: " backend_choice < /dev/tty || true
|
|
case "$backend_choice" in
|
|
2) BACKEND="gfw-knocker" ;;
|
|
*) BACKEND="paqet" ;;
|
|
esac
|
|
echo ""
|
|
log_info "Selected backend: $BACKEND"
|
|
echo ""
|
|
|
|
# Role selection
|
|
echo -e "${BOLD}Select role:${NC}"
|
|
echo " 1. Server (accept connections from clients)"
|
|
echo " 2. Client (connect to a server, provides SOCKS5 proxy)"
|
|
echo ""
|
|
local role_choice
|
|
read -p " Enter choice [1/2]: " role_choice < /dev/tty || true
|
|
case "$role_choice" in
|
|
1) ROLE="server" ;;
|
|
2) ROLE="client" ;;
|
|
*)
|
|
log_warn "Invalid choice. Defaulting to server."
|
|
ROLE="server"
|
|
;;
|
|
esac
|
|
echo ""
|
|
log_info "Selected role: $ROLE"
|
|
|
|
if [ "$BACKEND" = "paqet" ]; then
|
|
_wizard_paqet
|
|
else
|
|
_wizard_gfk
|
|
fi
|
|
|
|
# Save settings
|
|
save_settings
|
|
}
|
|
|
|
_wizard_paqet() {
|
|
# Auto-detect network
|
|
detect_network
|
|
echo ""
|
|
|
|
# Confirm/override interface
|
|
echo -e "${BOLD}Network interface${NC} [${DETECTED_IFACE:-eth0}]:"
|
|
read -p " Interface: " input < /dev/tty || true
|
|
IFACE="${input:-$DETECTED_IFACE}"
|
|
IFACE="${IFACE:-eth0}"
|
|
if ! _validate_iface "$IFACE"; then
|
|
log_warn "Invalid interface name. Using eth0."
|
|
IFACE="eth0"
|
|
fi
|
|
|
|
# Confirm/override local IP
|
|
echo -e "${BOLD}Local IP${NC} [${DETECTED_IP:-auto}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
LOCAL_IP="${input:-$DETECTED_IP}"
|
|
if [ -n "$LOCAL_IP" ] && ! _validate_ip "$LOCAL_IP"; then
|
|
log_warn "Invalid IP format. Using detected IP."
|
|
LOCAL_IP="$DETECTED_IP"
|
|
fi
|
|
|
|
# Confirm/override gateway MAC
|
|
echo -e "${BOLD}Gateway MAC address${NC} [${DETECTED_GW_MAC:-auto}]:"
|
|
read -p " MAC: " input < /dev/tty || true
|
|
GW_MAC="${input:-$DETECTED_GW_MAC}"
|
|
|
|
if [ -z "$GW_MAC" ] || ! _validate_mac "$GW_MAC"; then
|
|
if [ -n "$GW_MAC" ]; then
|
|
log_warn "Invalid MAC format detected."
|
|
else
|
|
log_error "Could not detect gateway MAC address."
|
|
fi
|
|
log_error "Please enter it manually (format: aa:bb:cc:dd:ee:ff)"
|
|
read -p " Gateway MAC: " GW_MAC < /dev/tty || true
|
|
if [ -z "$GW_MAC" ] || ! _validate_mac "$GW_MAC"; then
|
|
log_error "Valid gateway MAC is required for paqet to function."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
echo ""
|
|
echo -e "${BOLD}Listen port${NC} [8443]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
LISTEN_PORT="${input:-8443}"
|
|
if ! [[ "$LISTEN_PORT" =~ ^[0-9]+$ ]] || [ "$LISTEN_PORT" -lt 1 ] || [ "$LISTEN_PORT" -gt 65535 ]; then
|
|
log_warn "Invalid port. Using default 8443."
|
|
LISTEN_PORT=8443
|
|
fi
|
|
|
|
echo ""
|
|
log_info "Generating encryption key..."
|
|
ENCRYPTION_KEY=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true)
|
|
if [ -z "$ENCRYPTION_KEY" ]; then
|
|
log_warn "Could not auto-generate key. Using openssl fallback..."
|
|
ENCRYPTION_KEY=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32 || true)
|
|
fi
|
|
if [ -z "$ENCRYPTION_KEY" ] || [ "${#ENCRYPTION_KEY}" -lt 16 ]; then
|
|
log_error "Failed to generate a valid encryption key"
|
|
return 1
|
|
fi
|
|
echo ""
|
|
echo -e "${GREEN}${BOLD} Encryption Key: ${ENCRYPTION_KEY}${NC}"
|
|
echo ""
|
|
echo -e "${YELLOW} IMPORTANT: Save this key! Clients need it to connect.${NC}"
|
|
echo ""
|
|
SOCKS_PORT=""
|
|
else
|
|
echo ""
|
|
echo -e "${BOLD}Remote server address${NC} (IP:PORT):"
|
|
read -p " Server: " REMOTE_SERVER < /dev/tty || true
|
|
if [ -z "$REMOTE_SERVER" ]; then
|
|
log_error "Remote server address is required."
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${BOLD}Encryption key${NC} (from server setup):"
|
|
read -p " Key: " ENCRYPTION_KEY < /dev/tty || true
|
|
if [ -z "$ENCRYPTION_KEY" ]; then
|
|
log_error "Encryption key is required."
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${BOLD}SOCKS5 listen port${NC} [1080]:"
|
|
read -p " SOCKS port: " input < /dev/tty || true
|
|
SOCKS_PORT="${input:-1080}"
|
|
if ! [[ "$SOCKS_PORT" =~ ^[0-9]+$ ]] || [ "$SOCKS_PORT" -lt 1 ] || [ "$SOCKS_PORT" -gt 65535 ]; then
|
|
log_warn "Invalid port. Using default 1080."
|
|
SOCKS_PORT=1080
|
|
fi
|
|
LISTEN_PORT=""
|
|
fi
|
|
|
|
# Generate YAML config
|
|
generate_config
|
|
}
|
|
|
|
_wizard_gfk() {
|
|
if [ "$ROLE" = "server" ]; then
|
|
# Server IP (this machine's public IP)
|
|
detect_network
|
|
echo ""
|
|
echo -e "${BOLD}This server's public IP${NC} [${DETECTED_IP:-}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
GFK_SERVER_IP="${input:-$DETECTED_IP}"
|
|
if [ -z "$GFK_SERVER_IP" ] || ! _validate_ip "$GFK_SERVER_IP"; then
|
|
log_error "Valid server IP is required."
|
|
exit 1
|
|
fi
|
|
|
|
# VIO TCP port (must be closed to OS, raw socket handles it)
|
|
echo ""
|
|
echo -e "${BOLD}VIO TCP port${NC} [45000] (raw socket port, must be blocked by firewall):"
|
|
read -p " Port: " input < /dev/tty || true
|
|
GFK_VIO_PORT="${input:-45000}"
|
|
if ! _validate_port "$GFK_VIO_PORT"; then
|
|
log_warn "Invalid port. Using default 45000."
|
|
GFK_VIO_PORT=45000
|
|
fi
|
|
|
|
# QUIC port
|
|
echo ""
|
|
echo -e "${BOLD}QUIC tunnel port${NC} [25000]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
GFK_QUIC_PORT="${input:-25000}"
|
|
if ! _validate_port "$GFK_QUIC_PORT"; then
|
|
log_warn "Invalid port. Using default 25000."
|
|
GFK_QUIC_PORT=25000
|
|
fi
|
|
|
|
# Auth code
|
|
echo ""
|
|
local auto_auth
|
|
auto_auth=$(openssl rand -base64 16 2>/dev/null | tr -d '=+/' | head -c 16)
|
|
echo -e "${BOLD}QUIC auth code${NC} [auto-generated]:"
|
|
read -p " Auth code: " input < /dev/tty || true
|
|
GFK_AUTH_CODE="${input:-$auto_auth}"
|
|
echo ""
|
|
echo -e "${GREEN}${BOLD} Auth Code: ${GFK_AUTH_CODE}${NC}"
|
|
echo ""
|
|
echo -e "${YELLOW} IMPORTANT: Save this auth code! Clients need it to connect.${NC}"
|
|
echo ""
|
|
|
|
# Port mappings
|
|
echo -e "${BOLD}TCP port mappings${NC} (local:remote, comma-separated) [14000:443]:"
|
|
echo -e " ${DIM}Example: 14000:443,15000:2096,16000:10809${NC}"
|
|
read -p " Mappings: " input < /dev/tty || true
|
|
GFK_PORT_MAPPINGS="${input:-14000:443}"
|
|
MICROSOCKS_PORT=""
|
|
|
|
else
|
|
# Client: server IP
|
|
echo ""
|
|
echo -e "${BOLD}Remote server IP${NC} (server's public IP):"
|
|
read -p " Server IP: " GFK_SERVER_IP < /dev/tty || true
|
|
if [ -z "$GFK_SERVER_IP" ] || ! _validate_ip "$GFK_SERVER_IP"; then
|
|
log_error "Valid server IP is required."
|
|
exit 1
|
|
fi
|
|
|
|
# Server's VIO TCP port (what port the server is listening on)
|
|
echo ""
|
|
echo -e "${BOLD}Server's VIO TCP port${NC} [45000] (must match server config):"
|
|
read -p " Port: " input < /dev/tty || true
|
|
GFK_VIO_PORT="${input:-45000}"
|
|
if ! _validate_port "$GFK_VIO_PORT"; then
|
|
log_warn "Invalid port. Using default 45000."
|
|
GFK_VIO_PORT=45000
|
|
fi
|
|
|
|
# Local VIO client port (client's local binding)
|
|
echo ""
|
|
echo -e "${BOLD}Local VIO client port${NC} [40000]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
GFK_VIO_CLIENT_PORT="${input:-40000}"
|
|
if ! _validate_port "$GFK_VIO_CLIENT_PORT"; then
|
|
log_warn "Invalid port. Using default 40000."
|
|
GFK_VIO_CLIENT_PORT=40000
|
|
fi
|
|
|
|
# Server's QUIC port
|
|
echo ""
|
|
echo -e "${BOLD}Server's QUIC port${NC} [25000] (must match server config):"
|
|
read -p " Port: " input < /dev/tty || true
|
|
GFK_QUIC_PORT="${input:-25000}"
|
|
if ! _validate_port "$GFK_QUIC_PORT"; then
|
|
log_warn "Invalid port. Using default 25000."
|
|
GFK_QUIC_PORT=25000
|
|
fi
|
|
|
|
# Local QUIC client port
|
|
echo ""
|
|
echo -e "${BOLD}Local QUIC client port${NC} [20000]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
GFK_QUIC_CLIENT_PORT="${input:-20000}"
|
|
if ! _validate_port "$GFK_QUIC_CLIENT_PORT"; then
|
|
log_warn "Invalid port. Using default 20000."
|
|
GFK_QUIC_CLIENT_PORT=20000
|
|
fi
|
|
|
|
# Auth code (from server)
|
|
echo ""
|
|
echo -e "${BOLD}QUIC auth code${NC} (from server setup):"
|
|
read -p " Auth code: " GFK_AUTH_CODE < /dev/tty || true
|
|
if [ -z "$GFK_AUTH_CODE" ]; then
|
|
log_error "Auth code is required."
|
|
exit 1
|
|
fi
|
|
|
|
# Port mappings (must match server)
|
|
echo ""
|
|
echo -e "${BOLD}TCP port mappings${NC} (must match server) [14000:443]:"
|
|
read -p " Mappings: " input < /dev/tty || true
|
|
GFK_PORT_MAPPINGS="${input:-14000:443}"
|
|
|
|
# SOCKS5 port via microsocks
|
|
echo ""
|
|
echo -e "${BOLD}SOCKS5 listen port${NC} (via microsocks) [1080]:"
|
|
read -p " SOCKS port: " input < /dev/tty || true
|
|
MICROSOCKS_PORT="${input:-1080}"
|
|
if ! _validate_port "$MICROSOCKS_PORT"; then
|
|
log_warn "Invalid port. Using default 1080."
|
|
MICROSOCKS_PORT=1080
|
|
fi
|
|
SOCKS_PORT="$MICROSOCKS_PORT"
|
|
fi
|
|
|
|
# Generate GFK config
|
|
generate_gfk_config
|
|
}
|
|
|
|
generate_config() {
|
|
log_info "Generating paqet configuration..."
|
|
|
|
# Validate required fields
|
|
if [ -z "$IFACE" ] || [ -z "$LOCAL_IP" ] || [ -z "$GW_MAC" ] || [ -z "$ENCRYPTION_KEY" ]; then
|
|
log_error "Missing required configuration fields (interface, ip, gateway_mac, or secret)"
|
|
return 1
|
|
fi
|
|
if [ "$ROLE" = "server" ]; then
|
|
if [ -z "$LISTEN_PORT" ]; then log_error "Missing listen port"; return 1; fi
|
|
else
|
|
if [ -z "$REMOTE_SERVER" ] || [ -z "$SOCKS_PORT" ]; then
|
|
log_error "Missing server address or SOCKS port"
|
|
return 1
|
|
fi
|
|
local _rs_ip="${REMOTE_SERVER%:*}" _rs_port="${REMOTE_SERVER##*:}"
|
|
if ! _validate_ip "$_rs_ip" || ! _validate_port "$_rs_port"; then
|
|
log_error "Server address must be valid IP:PORT (e.g. 1.2.3.4:8443)"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Escape YAML special characters to prevent injection
|
|
_escape_yaml() {
|
|
local s="$1"
|
|
# If value contains special chars, quote it
|
|
if [[ "$s" =~ [:\#\[\]\{\}\"\'\|\>\<\&\*\!\%\@\`] ]] || [[ "$s" =~ ^[[:space:]] ]] || [[ "$s" =~ [[:space:]]$ ]]; then
|
|
s="${s//\\/\\\\}" # Escape backslashes
|
|
s="${s//\"/\\\"}" # Escape double quotes
|
|
printf '"%s"' "$s"
|
|
else
|
|
printf '%s' "$s"
|
|
fi
|
|
}
|
|
|
|
# Ensure install directory exists
|
|
mkdir -p "$INSTALL_DIR" || { log_error "Failed to create $INSTALL_DIR"; return 1; }
|
|
|
|
local tmp_conf
|
|
tmp_conf=$(mktemp "$INSTALL_DIR/config.yaml.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
|
|
# Set permissions on temp file before writing (fixes race condition)
|
|
chmod 600 "$tmp_conf" 2>/dev/null
|
|
|
|
(
|
|
umask 077
|
|
local _y_iface _y_ip _y_mac _y_key _y_server _y_port
|
|
_y_iface=$(_escape_yaml "$IFACE")
|
|
_y_ip=$(_escape_yaml "$LOCAL_IP")
|
|
_y_mac=$(_escape_yaml "$GW_MAC")
|
|
_y_key=$(_escape_yaml "$ENCRYPTION_KEY")
|
|
if [ "$ROLE" = "server" ]; then
|
|
cat > "$tmp_conf" << EOF
|
|
role: "server"
|
|
|
|
log:
|
|
level: "info"
|
|
|
|
listen:
|
|
addr: ":${LISTEN_PORT}"
|
|
|
|
network:
|
|
interface: "${_y_iface}"
|
|
ipv4:
|
|
addr: "${_y_ip}:${LISTEN_PORT}"
|
|
router_mac: "${_y_mac}"
|
|
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_y_key}"
|
|
EOF
|
|
else
|
|
local _rs_ip="${REMOTE_SERVER%:*}" _rs_port="${REMOTE_SERVER##*:}"
|
|
_y_server=$(_escape_yaml "$REMOTE_SERVER")
|
|
cat > "$tmp_conf" << EOF
|
|
role: "client"
|
|
|
|
log:
|
|
level: "info"
|
|
|
|
socks5:
|
|
- listen: "127.0.0.1:${SOCKS_PORT}"
|
|
|
|
network:
|
|
interface: "${_y_iface}"
|
|
ipv4:
|
|
addr: "${_y_ip}:0"
|
|
router_mac: "${_y_mac}"
|
|
|
|
server:
|
|
addr: "${_y_server}"
|
|
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_y_key}"
|
|
EOF
|
|
fi
|
|
)
|
|
if ! mv "$tmp_conf" "$INSTALL_DIR/config.yaml"; then
|
|
log_error "Failed to save configuration file"
|
|
rm -f "$tmp_conf"
|
|
return 1
|
|
fi
|
|
# Ensure final permissions (mv preserves source permissions on most systems)
|
|
chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null
|
|
log_success "Configuration saved to $INSTALL_DIR/config.yaml"
|
|
}
|
|
|
|
save_settings() {
|
|
# Preserve existing Telegram settings if present
|
|
local _tg_token="" _tg_chat="" _tg_interval=6 _tg_enabled=false
|
|
local _tg_alerts=true _tg_daily=true _tg_weekly=true _tg_label="" _tg_start_hour=0
|
|
if [ -f "$INSTALL_DIR/settings.conf" ]; then
|
|
# Safe settings loading without eval
|
|
while IFS='=' read -r key value; do
|
|
[[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue
|
|
# Remove surrounding quotes and sanitize value
|
|
value="${value#\"}"; value="${value%\"}"
|
|
# Validate value doesn't contain dangerous characters
|
|
if [[ "$value" =~ [\`\$\(] ]]; then
|
|
continue # Skip potentially dangerous values
|
|
fi
|
|
case "$key" in
|
|
TELEGRAM_BOT_TOKEN) _tg_token="$value" ;;
|
|
TELEGRAM_CHAT_ID) _tg_chat="$value" ;;
|
|
TELEGRAM_INTERVAL) [[ "$value" =~ ^[0-9]+$ ]] && _tg_interval="$value" ;;
|
|
TELEGRAM_ENABLED) _tg_enabled="$value" ;;
|
|
TELEGRAM_ALERTS_ENABLED) _tg_alerts="$value" ;;
|
|
TELEGRAM_DAILY_SUMMARY) _tg_daily="$value" ;;
|
|
TELEGRAM_WEEKLY_SUMMARY) _tg_weekly="$value" ;;
|
|
TELEGRAM_SERVER_LABEL) _tg_label="$value" ;;
|
|
TELEGRAM_START_HOUR) [[ "$value" =~ ^[0-9]+$ ]] && _tg_start_hour="$value" ;;
|
|
esac
|
|
done < <(grep '^[A-Z_][A-Z_0-9]*=' "$INSTALL_DIR/settings.conf")
|
|
fi
|
|
|
|
# Sanitize sensitive values - remove shell metacharacters and control chars
|
|
_sanitize_value() {
|
|
printf '%s' "$1" | tr -d '"$`\\'\''(){}[]<>|;&!\n\r\t'
|
|
}
|
|
local _safe_key; _safe_key=$(_sanitize_value "${ENCRYPTION_KEY:-}")
|
|
local _safe_auth; _safe_auth=$(_sanitize_value "${GFK_AUTH_CODE:-}")
|
|
local _tmp
|
|
_tmp=$(mktemp "$INSTALL_DIR/settings.conf.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
|
|
(
|
|
umask 077
|
|
cat > "$_tmp" << EOF
|
|
BACKEND="${BACKEND:-paqet}"
|
|
ROLE="${ROLE}"
|
|
PAQET_VERSION="${PAQET_VERSION:-unknown}"
|
|
PAQCTL_VERSION="${VERSION}"
|
|
LISTEN_PORT="${LISTEN_PORT:-}"
|
|
SOCKS_PORT="${SOCKS_PORT:-}"
|
|
INTERFACE="${IFACE:-}"
|
|
LOCAL_IP="${LOCAL_IP:-}"
|
|
GATEWAY_MAC="${GW_MAC:-}"
|
|
ENCRYPTION_KEY="${_safe_key}"
|
|
REMOTE_SERVER="${REMOTE_SERVER:-}"
|
|
GFK_VIO_PORT="${GFK_VIO_PORT:-}"
|
|
GFK_VIO_CLIENT_PORT="${GFK_VIO_CLIENT_PORT:-}"
|
|
GFK_QUIC_PORT="${GFK_QUIC_PORT:-}"
|
|
GFK_QUIC_CLIENT_PORT="${GFK_QUIC_CLIENT_PORT:-}"
|
|
GFK_AUTH_CODE="${_safe_auth}"
|
|
GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS:-}"
|
|
MICROSOCKS_PORT="${MICROSOCKS_PORT:-}"
|
|
GFK_SERVER_IP="${GFK_SERVER_IP:-}"
|
|
TELEGRAM_BOT_TOKEN="${_tg_token}"
|
|
TELEGRAM_CHAT_ID="${_tg_chat}"
|
|
TELEGRAM_INTERVAL=${_tg_interval}
|
|
TELEGRAM_ENABLED=${_tg_enabled}
|
|
TELEGRAM_ALERTS_ENABLED=${_tg_alerts}
|
|
TELEGRAM_DAILY_SUMMARY=${_tg_daily}
|
|
TELEGRAM_WEEKLY_SUMMARY=${_tg_weekly}
|
|
TELEGRAM_SERVER_LABEL="${_tg_label}"
|
|
TELEGRAM_START_HOUR=${_tg_start_hour}
|
|
EOF
|
|
)
|
|
if ! mv "$_tmp" "$INSTALL_DIR/settings.conf"; then
|
|
log_error "Failed to save settings"
|
|
rm -f "$_tmp"
|
|
return 1
|
|
fi
|
|
chmod 600 "$INSTALL_DIR/settings.conf" 2>/dev/null
|
|
log_success "Settings saved"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# iptables Management
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
apply_iptables_rules() {
|
|
local port="$1"
|
|
if [ -z "$port" ]; then
|
|
log_error "No port specified for iptables rules"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Applying iptables rules for port $port..."
|
|
|
|
# Load required kernel modules
|
|
modprobe iptable_raw 2>/dev/null || true
|
|
modprobe iptable_mangle 2>/dev/null || true
|
|
|
|
# Warn about active firewall managers
|
|
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
|
|
log_warn "ufw is active — ensure port $port/tcp is allowed: sudo ufw allow $port/tcp"
|
|
fi
|
|
if command -v firewall-cmd &>/dev/null && firewall-cmd --state 2>/dev/null | grep -q running; then
|
|
log_warn "firewalld is active — ensure port $port is open"
|
|
fi
|
|
|
|
# Tag for identifying paqctl rules
|
|
local TAG="paqctl"
|
|
|
|
# Check if all rules already exist (IPv4) - check both tagged and untagged
|
|
if iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null && \
|
|
iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null && \
|
|
iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null; then
|
|
log_info "iptables rules already in place"
|
|
else
|
|
# Apply missing IPv4 rules individually (tagged with "paqctl" comment)
|
|
iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
iptables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || {
|
|
log_error "Failed to add PREROUTING NOTRACK rule"
|
|
return 1
|
|
}
|
|
iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
iptables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || {
|
|
log_error "Failed to add OUTPUT NOTRACK rule"
|
|
return 1
|
|
}
|
|
iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
|
|
iptables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || {
|
|
log_error "Failed to add RST DROP rule"
|
|
return 1
|
|
}
|
|
log_success "IPv4 iptables rules applied"
|
|
fi
|
|
|
|
# Apply IPv6 rules if ip6tables is available (tagged with "paqctl" comment)
|
|
if command -v ip6tables &>/dev/null; then
|
|
local _ipv6_ok=true
|
|
ip6tables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
ip6tables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || _ipv6_ok=false
|
|
ip6tables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
ip6tables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || _ipv6_ok=false
|
|
ip6tables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
|
|
ip6tables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || _ipv6_ok=false
|
|
if [ "$_ipv6_ok" = true ]; then
|
|
log_success "IPv6 iptables rules applied"
|
|
else
|
|
log_warn "Some IPv6 iptables rules failed (IPv6 may not be available)"
|
|
fi
|
|
fi
|
|
|
|
# Persist rules
|
|
persist_iptables_rules
|
|
}
|
|
|
|
remove_iptables_rules() {
|
|
local port="$1"
|
|
if [ -z "$port" ]; then return 0; fi
|
|
|
|
local TAG="paqctl"
|
|
log_info "Removing iptables rules for port $port..."
|
|
# Remove tagged rules
|
|
iptables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
iptables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
iptables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
# Also remove untagged rules for backwards compatibility
|
|
iptables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true
|
|
iptables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true
|
|
iptables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
# Remove IPv6 rules
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
fi
|
|
log_success "iptables rules removed"
|
|
}
|
|
|
|
persist_iptables_rules() {
|
|
if command -v netfilter-persistent &>/dev/null; then
|
|
netfilter-persistent save 2>/dev/null || true
|
|
elif command -v iptables-save &>/dev/null; then
|
|
if [ -d /etc/iptables ]; then
|
|
iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
|
|
command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true
|
|
elif [ -f /etc/debian_version ] && [ ! -d /etc/iptables ]; then
|
|
mkdir -p /etc/iptables
|
|
iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
|
|
command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true
|
|
elif [ -d /etc/sysconfig ]; then
|
|
iptables-save > /etc/sysconfig/iptables 2>/dev/null || true
|
|
command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/sysconfig/ip6tables 2>/dev/null || true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
check_iptables_rules() {
|
|
local port="$1"
|
|
if [ -z "$port" ]; then return 1; fi
|
|
|
|
local TAG="paqctl"
|
|
local ok=true
|
|
iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || ok=false
|
|
iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || ok=false
|
|
iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || ok=false
|
|
|
|
if [ "$ok" = true ]; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# GFW-knocker Backend Functions
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
install_python_deps() {
|
|
log_info "Installing Python dependencies for GFW-knocker..."
|
|
if ! command -v python3 &>/dev/null; then
|
|
install_package python3
|
|
fi
|
|
# Ensure python3 version >= 3.10
|
|
local pyver
|
|
pyver=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || echo "0.0")
|
|
local pymajor pyminor
|
|
pymajor=$(echo "$pyver" | cut -d. -f1)
|
|
pyminor=$(echo "$pyver" | cut -d. -f2)
|
|
if [ "$pymajor" -lt 3 ] || { [ "$pymajor" -eq 3 ] && [ "$pyminor" -lt 10 ]; }; then
|
|
log_error "Python 3.10+ required, found $pyver"
|
|
return 1
|
|
fi
|
|
|
|
# Install python3-venv (version-specific package for Debian/Ubuntu)
|
|
# The generic python3-venv may not work, need python3.X-venv
|
|
local venv_pkg="python3-venv"
|
|
if [ "$PKG_MANAGER" = "apt" ]; then
|
|
# Get specific version like python3.12-venv
|
|
local pyver_full
|
|
pyver_full=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
|
|
if [ -n "$pyver_full" ]; then
|
|
venv_pkg="python${pyver_full}-venv"
|
|
fi
|
|
fi
|
|
install_package "$venv_pkg"
|
|
|
|
# Create virtual environment
|
|
local VENV_DIR="$INSTALL_DIR/venv"
|
|
# Check if venv exists AND is complete (has pip)
|
|
if [ ! -x "$VENV_DIR/bin/pip" ]; then
|
|
# Remove broken/incomplete venv if exists
|
|
[ -d "$VENV_DIR" ] && rm -rf "$VENV_DIR"
|
|
log_info "Creating Python virtual environment..."
|
|
python3 -m venv "$VENV_DIR" || {
|
|
log_error "Failed to create virtual environment"
|
|
return 1
|
|
}
|
|
fi
|
|
|
|
# Install packages in venv
|
|
log_info "Installing scapy and aioquic in venv..."
|
|
"$VENV_DIR/bin/pip" install --quiet --upgrade pip 2>/dev/null || true
|
|
"$VENV_DIR/bin/pip" install --quiet scapy aioquic 2>/dev/null || {
|
|
# Try with --break-system-packages as fallback (shouldn't be needed in venv)
|
|
"$VENV_DIR/bin/pip" install scapy aioquic || {
|
|
log_error "Failed to install Python packages (scapy, aioquic)"
|
|
return 1
|
|
}
|
|
}
|
|
|
|
# Verify
|
|
if "$VENV_DIR/bin/python" -c "import scapy; import aioquic" 2>/dev/null; then
|
|
log_success "Python dependencies installed (scapy, aioquic)"
|
|
else
|
|
log_error "Python package verification failed"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
install_microsocks() {
|
|
log_info "Installing microsocks for SOCKS5 proxy..."
|
|
if [ -x "$INSTALL_DIR/bin/microsocks" ]; then
|
|
log_success "microsocks already installed"
|
|
return 0
|
|
fi
|
|
# Build dependencies
|
|
command -v gcc &>/dev/null || install_package gcc
|
|
command -v make &>/dev/null || install_package make
|
|
local tmp_dir
|
|
tmp_dir=$(mktemp -d)
|
|
if ! curl -sL "https://github.com/${MICROSOCKS_REPO}/archive/refs/heads/master.tar.gz" -o "$tmp_dir/microsocks.tar.gz"; then
|
|
log_error "Failed to download microsocks"
|
|
rm -rf "$tmp_dir"
|
|
return 1
|
|
fi
|
|
tar -xzf "$tmp_dir/microsocks.tar.gz" -C "$tmp_dir" 2>/dev/null || {
|
|
log_error "Failed to extract microsocks"
|
|
rm -rf "$tmp_dir"
|
|
return 1
|
|
}
|
|
local src_dir
|
|
src_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "microsocks*" | head -1)
|
|
if [ -z "$src_dir" ]; then
|
|
log_error "microsocks source directory not found"
|
|
rm -rf "$tmp_dir"
|
|
return 1
|
|
fi
|
|
if ! make -C "$src_dir" -j"$(nproc 2>/dev/null || echo 1)" 2>/dev/null; then
|
|
log_error "Failed to compile microsocks"
|
|
rm -rf "$tmp_dir"
|
|
return 1
|
|
fi
|
|
mkdir -p "$INSTALL_DIR/bin"
|
|
cp "$src_dir/microsocks" "$INSTALL_DIR/bin/microsocks"
|
|
chmod 755 "$INSTALL_DIR/bin/microsocks"
|
|
rm -rf "$tmp_dir"
|
|
log_success "microsocks installed"
|
|
}
|
|
|
|
#───────────────────────────────────────────────────────────────────────
|
|
# Xray Installation (for GFK server - provides SOCKS5 on port 443)
|
|
#───────────────────────────────────────────────────────────────────────
|
|
|
|
XRAY_CONFIG_DIR="/usr/local/etc/xray"
|
|
XRAY_CONFIG_FILE="$XRAY_CONFIG_DIR/config.json"
|
|
|
|
check_xray_installed() {
|
|
command -v xray &>/dev/null || [ -x /usr/local/bin/xray ]
|
|
}
|
|
|
|
install_xray() {
|
|
if check_xray_installed; then
|
|
log_info "Xray is already installed"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Installing Xray ${XRAY_VERSION_PINNED}..."
|
|
|
|
# Use official Xray install script with pinned version for stability
|
|
local tmp_script
|
|
tmp_script=$(mktemp)
|
|
if ! curl -sL https://github.com/XTLS/Xray-install/raw/main/install-release.sh -o "$tmp_script"; then
|
|
log_error "Failed to download Xray installer"
|
|
rm -f "$tmp_script"
|
|
return 1
|
|
fi
|
|
|
|
# Install specific version (not latest) for stability
|
|
if ! bash "$tmp_script" install --version "$XRAY_VERSION_PINNED" 2>/dev/null; then
|
|
log_error "Failed to install Xray"
|
|
rm -f "$tmp_script"
|
|
return 1
|
|
fi
|
|
rm -f "$tmp_script"
|
|
|
|
log_success "Xray ${XRAY_VERSION_PINNED} installed"
|
|
}
|
|
|
|
configure_xray_socks() {
|
|
local listen_port="${1:-443}"
|
|
|
|
log_info "Configuring Xray SOCKS5 proxy on port $listen_port..."
|
|
|
|
mkdir -p "$XRAY_CONFIG_DIR"
|
|
|
|
# Create simple SOCKS5 inbound config
|
|
cat > "$XRAY_CONFIG_FILE" << EOF
|
|
{
|
|
"log": {
|
|
"loglevel": "warning"
|
|
},
|
|
"inbounds": [
|
|
{
|
|
"tag": "socks-in",
|
|
"port": ${listen_port},
|
|
"listen": "127.0.0.1",
|
|
"protocol": "socks",
|
|
"settings": {
|
|
"auth": "noauth",
|
|
"udp": true
|
|
},
|
|
"sniffing": {
|
|
"enabled": true,
|
|
"destOverride": ["http", "tls"]
|
|
}
|
|
}
|
|
],
|
|
"outbounds": [
|
|
{
|
|
"tag": "direct",
|
|
"protocol": "freedom",
|
|
"settings": {}
|
|
}
|
|
]
|
|
}
|
|
EOF
|
|
chmod 644 "$XRAY_CONFIG_FILE" # Xray service runs as 'nobody', needs read access
|
|
log_success "Xray configured (SOCKS5 on 127.0.0.1:$listen_port)"
|
|
}
|
|
|
|
start_xray() {
|
|
log_info "Starting Xray service..."
|
|
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
# Stop first, reload daemon, then start - with retry
|
|
systemctl stop xray 2>/dev/null || true
|
|
sleep 1
|
|
systemctl daemon-reload 2>/dev/null || true
|
|
systemctl enable xray 2>/dev/null || true
|
|
|
|
# Try up to 3 times
|
|
local attempt
|
|
for attempt in 1 2 3; do
|
|
systemctl start xray 2>/dev/null
|
|
sleep 2
|
|
if systemctl is-active --quiet xray; then
|
|
log_success "Xray started"
|
|
return 0
|
|
fi
|
|
[ "$attempt" -lt 3 ] && sleep 1
|
|
done
|
|
log_error "Failed to start Xray after 3 attempts"
|
|
return 1
|
|
else
|
|
# Direct start for non-systemd
|
|
if [ -x /usr/local/bin/xray ]; then
|
|
pkill -x xray 2>/dev/null || true
|
|
sleep 1
|
|
nohup /usr/local/bin/xray run -c "$XRAY_CONFIG_FILE" > /var/log/xray.log 2>&1 &
|
|
sleep 2
|
|
if pgrep -x xray &>/dev/null; then
|
|
log_success "Xray started"
|
|
return 0
|
|
fi
|
|
fi
|
|
log_error "Failed to start Xray"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
stop_xray() {
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
systemctl stop xray 2>/dev/null || true
|
|
else
|
|
pkill -x xray 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
setup_xray_for_gfk() {
|
|
# Get the first target port from GFK_PORT_MAPPINGS (e.g., "14000:443" -> 443)
|
|
local target_port
|
|
target_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f2 | cut -d, -f1)
|
|
|
|
install_xray || return 1
|
|
configure_xray_socks "$target_port" || return 1
|
|
start_xray || return 1
|
|
}
|
|
|
|
download_gfk() {
|
|
log_info "Downloading GFW-knocker scripts..."
|
|
if ! mkdir -p "$GFK_DIR"; then
|
|
log_error "Failed to create $GFK_DIR"
|
|
return 1
|
|
fi
|
|
# Note: parameters.py is generated by generate_gfk_config(), don't download it
|
|
# Download server scripts from gfk/server/
|
|
local server_files="mainserver.py quic_server.py vio_server.py"
|
|
local f
|
|
for f in $server_files; do
|
|
if ! curl -sL "$GFK_RAW_URL/server/$f" -o "$GFK_DIR/$f"; then
|
|
log_error "Failed to download $f"
|
|
return 1
|
|
fi
|
|
done
|
|
# Download client scripts from gfk/client/
|
|
local client_files="mainclient.py quic_client.py vio_client.py"
|
|
for f in $client_files; do
|
|
if ! curl -sL "$GFK_RAW_URL/client/$f" -o "$GFK_DIR/$f"; then
|
|
log_error "Failed to download $f"
|
|
return 1
|
|
fi
|
|
done
|
|
chmod 600 "$GFK_DIR"/*.py
|
|
# Patch mainserver.py to use venv python for subprocesses
|
|
if [ -f "$GFK_DIR/mainserver.py" ]; then
|
|
sed -i "s|'python3'|'$INSTALL_DIR/venv/bin/python'|g" "$GFK_DIR/mainserver.py"
|
|
fi
|
|
log_success "GFW-knocker scripts downloaded to $GFK_DIR"
|
|
}
|
|
|
|
generate_gfk_certs() {
|
|
if [ -f "$GFK_DIR/cert.pem" ] && [ -f "$GFK_DIR/key.pem" ]; then
|
|
log_info "GFW-knocker certificates already exist"
|
|
return 0
|
|
fi
|
|
log_info "Generating QUIC TLS certificates..."
|
|
if ! openssl req -x509 -newkey rsa:2048 -keyout "$GFK_DIR/key.pem" \
|
|
-out "$GFK_DIR/cert.pem" -days 3650 -nodes -subj "/CN=gfk" 2>/dev/null; then
|
|
log_error "Failed to generate certificates"
|
|
return 1
|
|
fi
|
|
chmod 600 "$GFK_DIR/key.pem" "$GFK_DIR/cert.pem"
|
|
log_success "QUIC certificates generated"
|
|
}
|
|
|
|
generate_gfk_config() {
|
|
log_info "Generating GFW-knocker configuration..."
|
|
# Ensure GFK directory exists
|
|
mkdir -p "$GFK_DIR" || { log_error "Failed to create $GFK_DIR"; return 1; }
|
|
local _tmp
|
|
_tmp=$(mktemp "$GFK_DIR/parameters.py.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
|
|
|
|
# Determine port values based on role - validate they are numeric
|
|
local vio_tcp_server_port="${GFK_VIO_PORT:-45000}"
|
|
local vio_tcp_client_port="${GFK_VIO_CLIENT_PORT:-40000}"
|
|
local vio_udp_server_port="${GFK_VIO_UDP_SERVER:-35000}"
|
|
local vio_udp_client_port="${GFK_VIO_UDP_CLIENT:-30000}"
|
|
local quic_server_port="${GFK_QUIC_PORT:-25000}"
|
|
local quic_client_port="${GFK_QUIC_CLIENT_PORT:-20000}"
|
|
|
|
# Validate all ports are numeric
|
|
for _p in "$vio_tcp_server_port" "$vio_tcp_client_port" "$vio_udp_server_port" \
|
|
"$vio_udp_client_port" "$quic_server_port" "$quic_client_port"; do
|
|
if ! [[ "$_p" =~ ^[0-9]+$ ]]; then
|
|
log_error "Invalid port number: $_p"
|
|
rm -f "$_tmp"
|
|
return 1
|
|
fi
|
|
done
|
|
|
|
# Escape Python string - prevents code injection
|
|
_escape_py_string() {
|
|
local s="$1"
|
|
s="${s//\\/\\\\}" # Escape backslashes first
|
|
s="${s//\"/\\\"}" # Escape double quotes
|
|
s="${s//\'/\\\'}" # Escape single quotes
|
|
s="${s//$'\n'/\\n}" # Escape newlines
|
|
s="${s//$'\r'/\\r}" # Escape carriage returns
|
|
printf '%s' "$s"
|
|
}
|
|
|
|
# Validate and escape server IP
|
|
local safe_server_ip
|
|
safe_server_ip=$(_escape_py_string "${GFK_SERVER_IP:-}")
|
|
if ! _validate_ip "${GFK_SERVER_IP:-}"; then
|
|
log_error "Invalid server IP: ${GFK_SERVER_IP:-}"
|
|
rm -f "$_tmp"
|
|
return 1
|
|
fi
|
|
|
|
# Validate and escape auth code
|
|
local safe_auth_code
|
|
safe_auth_code=$(_escape_py_string "${GFK_AUTH_CODE:-}")
|
|
|
|
# Build port mapping dict string with validation
|
|
local tcp_mapping="${GFK_PORT_MAPPINGS:-14000:443}"
|
|
local mapping_str="{"
|
|
local first=true
|
|
local pair
|
|
for pair in $(echo "$tcp_mapping" | tr ',' ' '); do
|
|
local lport rport
|
|
lport=$(echo "$pair" | cut -d: -f1)
|
|
rport=$(echo "$pair" | cut -d: -f2)
|
|
# Validate both ports are numeric
|
|
if ! [[ "$lport" =~ ^[0-9]+$ ]] || ! [[ "$rport" =~ ^[0-9]+$ ]]; then
|
|
log_error "Invalid port mapping: $pair (must be numeric:numeric)"
|
|
rm -f "$_tmp"
|
|
return 1
|
|
fi
|
|
if [ "$first" = true ]; then
|
|
mapping_str="${mapping_str}${lport}: ${rport}"
|
|
first=false
|
|
else
|
|
mapping_str="${mapping_str}, ${lport}: ${rport}"
|
|
fi
|
|
done
|
|
mapping_str="${mapping_str}}"
|
|
|
|
# Escape GFK_DIR for Python string
|
|
local safe_gfk_dir
|
|
safe_gfk_dir=$(_escape_py_string "${GFK_DIR}")
|
|
|
|
(
|
|
umask 077
|
|
cat > "$_tmp" << PYEOF
|
|
# GFW-knocker parameters - auto-generated by paqctl
|
|
# Do not edit manually
|
|
|
|
vps_ip = "${safe_server_ip}"
|
|
xray_server_ip_address = "127.0.0.1"
|
|
|
|
tcp_port_mapping = ${mapping_str}
|
|
udp_port_mapping = {}
|
|
|
|
vio_tcp_server_port = ${vio_tcp_server_port}
|
|
vio_tcp_client_port = ${vio_tcp_client_port}
|
|
vio_udp_server_port = ${vio_udp_server_port}
|
|
vio_udp_client_port = ${vio_udp_client_port}
|
|
|
|
quic_server_port = ${quic_server_port}
|
|
quic_client_port = ${quic_client_port}
|
|
quic_local_ip = "127.0.0.1"
|
|
|
|
quic_idle_timeout = 86400
|
|
udp_timeout = 300
|
|
quic_mtu = 1420
|
|
quic_verify_cert = False
|
|
quic_max_data = 1073741824
|
|
quic_max_stream_data = 1073741824
|
|
|
|
quic_auth_code = "${safe_auth_code}"
|
|
|
|
quic_cert_filepath = ("${safe_gfk_dir}/cert.pem", "${safe_gfk_dir}/key.pem")
|
|
PYEOF
|
|
)
|
|
if ! mv "$_tmp" "$GFK_DIR/parameters.py"; then
|
|
log_error "Failed to save GFW-knocker configuration"
|
|
rm -f "$_tmp"
|
|
return 1
|
|
fi
|
|
chmod 600 "$GFK_DIR/parameters.py"
|
|
log_success "GFW-knocker configuration saved"
|
|
}
|
|
|
|
create_gfk_client_wrapper() {
|
|
log_info "Creating GFW-knocker client wrapper..."
|
|
local wrapper="$INSTALL_DIR/bin/gfk-client.sh"
|
|
local msport="${MICROSOCKS_PORT:-1080}"
|
|
mkdir -p "$INSTALL_DIR/bin"
|
|
cat > "$wrapper" << 'WRAPEOF'
|
|
#!/bin/bash
|
|
set -e
|
|
GFK_DIR="REPLACE_ME_GFK_DIR"
|
|
INSTALL_DIR="REPLACE_ME_INSTALL_DIR"
|
|
MICROSOCKS_PORT="REPLACE_ME_MSPORT"
|
|
|
|
cd "$GFK_DIR"
|
|
"$INSTALL_DIR/venv/bin/python" mainclient.py &
|
|
PID1=$!
|
|
"$INSTALL_DIR/bin/microsocks" -i 127.0.0.1 -p "$MICROSOCKS_PORT" &
|
|
PID2=$!
|
|
trap "kill $PID1 $PID2 2>/dev/null; wait" EXIT INT TERM
|
|
wait
|
|
WRAPEOF
|
|
sed "s#REPLACE_ME_GFK_DIR#${GFK_DIR}#g; s#REPLACE_ME_INSTALL_DIR#${INSTALL_DIR}#g; s#REPLACE_ME_MSPORT#${msport}#g" "$wrapper" > "$wrapper.sed" && mv "$wrapper.sed" "$wrapper"
|
|
chmod 755 "$wrapper"
|
|
log_success "Client wrapper created at $wrapper"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Service Management
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
setup_service() {
|
|
log_info "Setting up auto-start on boot..."
|
|
|
|
# Check which backends are installed
|
|
local paqet_installed=false gfk_installed=false
|
|
[ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
|
|
if [ "$ROLE" = "server" ]; then
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
|
|
else
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
|
|
fi
|
|
|
|
# If both backends are installed, create a combined service
|
|
local _both_installed=false
|
|
[ "$paqet_installed" = true ] && [ "$gfk_installed" = true ] && _both_installed=true
|
|
|
|
# Compute ExecStart based on backend
|
|
local _exec_start _working_dir _svc_desc _svc_type="simple"
|
|
if [ "$_both_installed" = true ]; then
|
|
_svc_desc="Paqet Combined Proxy Service (Paqet + GFK)"
|
|
_working_dir="${INSTALL_DIR}"
|
|
_svc_type="forking"
|
|
# Create a wrapper script that starts both backends
|
|
cat > "${INSTALL_DIR}/bin/start-both.sh" << BOTH_SCRIPT
|
|
#!/bin/bash
|
|
INSTALL_DIR="/opt/paqctl"
|
|
GFK_DIR="\${INSTALL_DIR}/gfk"
|
|
ROLE="${ROLE}"
|
|
|
|
# Source config for ports
|
|
[ -f "\${INSTALL_DIR}/settings.conf" ] && . "\${INSTALL_DIR}/settings.conf"
|
|
|
|
# Apply firewall rules (server only)
|
|
if [ "\$ROLE" = "server" ]; then
|
|
# Apply paqet firewall rules (NOTRACK for KCP)
|
|
modprobe iptable_raw 2>/dev/null || true
|
|
modprobe iptable_mangle 2>/dev/null || true
|
|
port="\${LISTEN_PORT:-8443}"
|
|
TAG="paqctl"
|
|
iptables -t raw -C PREROUTING -p tcp --dport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
|
|
iptables -t raw -A PREROUTING -p tcp --dport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
|
|
iptables -t raw -C OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
|
|
iptables -t raw -A OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
|
|
iptables -t mangle -C OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \\
|
|
iptables -t mangle -A OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" --tcp-flags RST RST -j DROP 2>/dev/null
|
|
|
|
# Apply GFK firewall rules (DROP on VIO port)
|
|
vio_port="\${GFK_VIO_PORT:-45000}"
|
|
iptables -C INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
|
|
iptables -A INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null
|
|
iptables -C OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
|
|
iptables -A OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null
|
|
# IPv6 rules for GFK
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -C INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
|
|
ip6tables -A INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || true
|
|
ip6tables -C OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
|
|
ip6tables -A OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || true
|
|
fi
|
|
fi
|
|
|
|
# Start paqet backend
|
|
(umask 077; touch /var/log/paqet-backend.log)
|
|
nohup "\${INSTALL_DIR}/bin/paqet" run -c "\${INSTALL_DIR}/config.yaml" > /var/log/paqet-backend.log 2>&1 &
|
|
echo \$! > /run/paqet-backend.pid
|
|
|
|
# Start GFK backend
|
|
(umask 077; touch /var/log/gfk-backend.log)
|
|
if [ "\$ROLE" = "server" ]; then
|
|
# Start Xray if available
|
|
if command -v xray &>/dev/null || [ -x /usr/local/bin/xray ]; then
|
|
if ! pgrep -f "xray run" &>/dev/null; then
|
|
systemctl start xray 2>/dev/null || xray run -c /usr/local/etc/xray/config.json &>/dev/null &
|
|
fi
|
|
fi
|
|
cd "\$GFK_DIR"
|
|
nohup "\${INSTALL_DIR}/venv/bin/python" "\${GFK_DIR}/mainserver.py" > /var/log/gfk-backend.log 2>&1 &
|
|
else
|
|
if [ -x "\${INSTALL_DIR}/bin/gfk-client.sh" ]; then
|
|
nohup "\${INSTALL_DIR}/bin/gfk-client.sh" > /var/log/gfk-backend.log 2>&1 &
|
|
else
|
|
cd "\$GFK_DIR"
|
|
nohup "\${INSTALL_DIR}/venv/bin/python" "\${GFK_DIR}/mainclient.py" > /var/log/gfk-backend.log 2>&1 &
|
|
fi
|
|
fi
|
|
echo \$! > /run/gfk-backend.pid
|
|
|
|
sleep 1
|
|
exit 0
|
|
BOTH_SCRIPT
|
|
chmod +x "${INSTALL_DIR}/bin/start-both.sh"
|
|
_exec_start="${INSTALL_DIR}/bin/start-both.sh"
|
|
elif [ "$BACKEND" = "gfw-knocker" ]; then
|
|
_svc_desc="GFW-knocker Proxy Service"
|
|
_working_dir="${GFK_DIR}"
|
|
if [ "$ROLE" = "server" ]; then
|
|
_exec_start="${INSTALL_DIR}/venv/bin/python ${GFK_DIR}/mainserver.py"
|
|
else
|
|
_exec_start="${INSTALL_DIR}/bin/gfk-client.sh"
|
|
fi
|
|
else
|
|
_svc_desc="Paqet Proxy Service"
|
|
_working_dir="${INSTALL_DIR}"
|
|
_exec_start="${INSTALL_DIR}/bin/paqet run -c ${INSTALL_DIR}/config.yaml"
|
|
fi
|
|
|
|
if [ "$HAS_SYSTEMD" = "true" ]; then
|
|
if [ "$_both_installed" = true ]; then
|
|
# Combined service for both backends
|
|
cat > /etc/systemd/system/paqctl.service << EOF
|
|
[Unit]
|
|
Description=${_svc_desc}
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=${_svc_type}
|
|
WorkingDirectory=${_working_dir}
|
|
ExecStart=${_exec_start}
|
|
ExecStop=/usr/local/bin/paqctl stop
|
|
ExecStopPost=/usr/local/bin/paqctl _remove-firewall
|
|
RemainAfterExit=yes
|
|
KillMode=mixed
|
|
KillSignal=SIGTERM
|
|
TimeoutStopSec=30
|
|
LimitNOFILE=65535
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
SyslogIdentifier=paqctl
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
else
|
|
# Single backend service
|
|
cat > /etc/systemd/system/paqctl.service << EOF
|
|
[Unit]
|
|
Description=${_svc_desc}
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=${_svc_type}
|
|
WorkingDirectory=${_working_dir}
|
|
ExecStartPre=/usr/local/bin/paqctl _apply-firewall
|
|
ExecStart=${_exec_start}
|
|
ExecStopPost=/usr/local/bin/paqctl _remove-firewall
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
KillMode=mixed
|
|
KillSignal=SIGTERM
|
|
TimeoutStopSec=30
|
|
LimitNOFILE=65535
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
SyslogIdentifier=paqctl
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
fi
|
|
|
|
systemctl daemon-reload 2>/dev/null || true
|
|
systemctl enable paqctl.service 2>/dev/null || true
|
|
log_success "Systemd service created and enabled"
|
|
|
|
elif command -v rc-update &>/dev/null; then
|
|
local _openrc_run
|
|
_openrc_run=$(command -v openrc-run 2>/dev/null || echo "/sbin/openrc-run")
|
|
cat > /etc/init.d/paqctl << EOF
|
|
#!${_openrc_run}
|
|
|
|
name="paqctl"
|
|
description="${_svc_desc}"
|
|
command="$(echo "${_exec_start}" | awk '{print $1}')"
|
|
command_args="$(echo "${_exec_start}" | cut -d' ' -f2-)"
|
|
if [ "\$command_args" = "\$command" ]; then command_args=""; fi
|
|
command_background=true
|
|
pidfile="/run/\${RC_SVCNAME}.pid"
|
|
|
|
depend() {
|
|
need net
|
|
after firewall
|
|
}
|
|
|
|
start_pre() {
|
|
/usr/local/bin/paqctl _apply-firewall
|
|
}
|
|
|
|
stop_post() {
|
|
/usr/local/bin/paqctl _remove-firewall
|
|
}
|
|
EOF
|
|
if ! chmod +x /etc/init.d/paqctl; then
|
|
log_error "Failed to make init script executable"
|
|
return 1
|
|
fi
|
|
rc-update add paqctl default 2>/dev/null || true
|
|
log_success "OpenRC service created and enabled"
|
|
|
|
elif [ -d /etc/init.d ]; then
|
|
cat > /etc/init.d/paqctl << SYSV
|
|
#!/bin/bash
|
|
### BEGIN INIT INFO
|
|
# Provides: paqctl
|
|
# Required-Start: \$remote_fs \$network \$syslog
|
|
# Required-Stop: \$remote_fs \$network \$syslog
|
|
# Default-Start: 2 3 4 5
|
|
# Default-Stop: 0 1 6
|
|
# Short-Description: ${_svc_desc}
|
|
### END INIT INFO
|
|
|
|
case "\$1" in
|
|
start)
|
|
/usr/local/bin/paqctl _apply-firewall
|
|
${_exec_start} &
|
|
_pid=\$!
|
|
sleep 1
|
|
if kill -0 "\$_pid" 2>/dev/null; then
|
|
echo \$_pid > /run/paqctl.pid
|
|
else
|
|
echo "Failed to start paqet"
|
|
/usr/local/bin/paqctl _remove-firewall
|
|
exit 1
|
|
fi
|
|
;;
|
|
stop)
|
|
if [ -f /run/paqctl.pid ]; then
|
|
_pid=\$(cat /run/paqctl.pid)
|
|
kill "\$_pid" 2>/dev/null
|
|
_count=0
|
|
while kill -0 "\$_pid" 2>/dev/null && [ \$_count -lt 10 ]; do
|
|
sleep 1
|
|
_count=\$((_count + 1))
|
|
done
|
|
kill -0 "\$_pid" 2>/dev/null && kill -9 "\$_pid" 2>/dev/null
|
|
rm -f /run/paqctl.pid
|
|
fi
|
|
/usr/local/bin/paqctl _remove-firewall
|
|
;;
|
|
restart)
|
|
\$0 stop
|
|
sleep 1
|
|
\$0 start
|
|
;;
|
|
status)
|
|
[ -f /run/paqctl.pid ] && kill -0 "\$(cat /run/paqctl.pid)" 2>/dev/null && echo "Running" || echo "Stopped"
|
|
;;
|
|
*)
|
|
echo "Usage: \$0 {start|stop|restart|status}"
|
|
exit 1
|
|
;;
|
|
esac
|
|
SYSV
|
|
if ! chmod +x /etc/init.d/paqctl; then
|
|
log_error "Failed to make init script executable"
|
|
return 1
|
|
fi
|
|
if command -v update-rc.d &>/dev/null; then
|
|
update-rc.d paqctl defaults 2>/dev/null || true
|
|
elif command -v chkconfig &>/dev/null; then
|
|
chkconfig paqctl on 2>/dev/null || true
|
|
fi
|
|
log_success "SysVinit service created and enabled"
|
|
|
|
else
|
|
log_warn "Could not set up auto-start. You can start paqet manually with: sudo paqctl start"
|
|
fi
|
|
}
|
|
|
|
setup_logrotate() {
|
|
# Only set up if logrotate is available
|
|
command -v logrotate &>/dev/null || return 0
|
|
|
|
log_info "Setting up log rotation..."
|
|
|
|
cat > /etc/logrotate.d/paqctl << 'LOGROTATE'
|
|
/var/log/paqctl.log
|
|
/var/log/paqet-backend.log
|
|
/var/log/gfk-backend.log
|
|
/var/log/xray.log
|
|
{
|
|
daily
|
|
rotate 7
|
|
compress
|
|
delaycompress
|
|
missingok
|
|
notifempty
|
|
create 0640 root root
|
|
sharedscripts
|
|
postrotate
|
|
# Signal processes to reopen logs if needed
|
|
systemctl reload paqctl.service 2>/dev/null || true
|
|
endscript
|
|
}
|
|
LOGROTATE
|
|
|
|
log_success "Log rotation configured (7 days, compressed)"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Management Script (Embedded)
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
create_management_script() {
|
|
local tmp_script
|
|
tmp_script=$(mktemp "$INSTALL_DIR/paqctl.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
|
|
cat > "$tmp_script" << 'MANAGEMENT'
|
|
#!/bin/bash
|
|
#
|
|
# paqctl - Paqet Manager
|
|
# https://github.com/SamNet-dev/paqctl
|
|
#
|
|
|
|
VERSION="1.0.0"
|
|
|
|
# Pinned versions for stability (update these after testing new releases)
|
|
PAQET_VERSION_PINNED="v1.0.0-alpha.13"
|
|
XRAY_VERSION_PINNED="v26.2.4"
|
|
GFK_VERSION_PINNED="v1.0.0"
|
|
|
|
INSTALL_DIR="REPLACE_ME_INSTALL_DIR"
|
|
BACKUP_DIR="$INSTALL_DIR/backups"
|
|
PAQET_REPO="hanselime/paqet"
|
|
PAQET_API_URL="https://api.github.com/repos/${PAQET_REPO}/releases/latest"
|
|
GFK_REPO="SamNet-dev/paqctl"
|
|
GFK_BRANCH="main"
|
|
GFK_RAW_URL="https://raw.githubusercontent.com/${GFK_REPO}/${GFK_BRANCH}/gfk"
|
|
GFK_DIR="$INSTALL_DIR/gfk"
|
|
MICROSOCKS_REPO="rofl0r/microsocks"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
MAGENTA='\033[0;35m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
NC='\033[0m'
|
|
|
|
# Input validation helpers
|
|
_validate_port() { [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]; }
|
|
_validate_ip() {
|
|
[[ "$1" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1
|
|
local IFS='.'; set -- $1
|
|
[ "$1" -le 255 ] && [ "$2" -le 255 ] && [ "$3" -le 255 ] && [ "$4" -le 255 ]
|
|
}
|
|
_validate_mac() { [[ "$1" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; }
|
|
_validate_iface() { [[ "$1" =~ ^[a-zA-Z0-9._-]+$ ]] && [ ${#1} -le 64 ]; }
|
|
# Safe string length check - prevents DoS via extremely long inputs
|
|
_check_length() { [ ${#1} -le "${2:-256}" ]; }
|
|
_load_settings() {
|
|
[ -f "$INSTALL_DIR/settings.conf" ] || return 0
|
|
# Safe settings loading without eval - uses case statement
|
|
while IFS='=' read -r key value; do
|
|
[[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue
|
|
value="${value#\"}"; value="${value%\"}"
|
|
# Skip values with dangerous shell characters
|
|
[[ "$value" =~ [\`\$\(] ]] && continue
|
|
case "$key" in
|
|
BACKEND) BACKEND="$value" ;;
|
|
ROLE) ROLE="$value" ;;
|
|
PAQET_VERSION) PAQET_VERSION="$value" ;;
|
|
PAQCTL_VERSION) PAQCTL_VERSION="$value" ;;
|
|
LISTEN_PORT) [[ "$value" =~ ^[0-9]*$ ]] && LISTEN_PORT="$value" ;;
|
|
SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && SOCKS_PORT="$value" ;;
|
|
INTERFACE) INTERFACE="$value" ;;
|
|
LOCAL_IP) LOCAL_IP="$value" ;;
|
|
GATEWAY_MAC) GATEWAY_MAC="$value" ;;
|
|
ENCRYPTION_KEY) ENCRYPTION_KEY="$value" ;;
|
|
REMOTE_SERVER) REMOTE_SERVER="$value" ;;
|
|
GFK_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_PORT="$value" ;;
|
|
GFK_VIO_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_CLIENT_PORT="$value" ;;
|
|
GFK_QUIC_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_PORT="$value" ;;
|
|
GFK_QUIC_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_CLIENT_PORT="$value" ;;
|
|
GFK_AUTH_CODE) GFK_AUTH_CODE="$value" ;;
|
|
GFK_PORT_MAPPINGS) GFK_PORT_MAPPINGS="$value" ;;
|
|
MICROSOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && MICROSOCKS_PORT="$value" ;;
|
|
GFK_SERVER_IP) GFK_SERVER_IP="$value" ;;
|
|
TELEGRAM_BOT_TOKEN) TELEGRAM_BOT_TOKEN="$value" ;;
|
|
TELEGRAM_CHAT_ID) TELEGRAM_CHAT_ID="$value" ;;
|
|
TELEGRAM_INTERVAL) [[ "$value" =~ ^[0-9]+$ ]] && TELEGRAM_INTERVAL="$value" ;;
|
|
TELEGRAM_ENABLED) TELEGRAM_ENABLED="$value" ;;
|
|
TELEGRAM_ALERTS_ENABLED) TELEGRAM_ALERTS_ENABLED="$value" ;;
|
|
TELEGRAM_DAILY_SUMMARY) TELEGRAM_DAILY_SUMMARY="$value" ;;
|
|
TELEGRAM_WEEKLY_SUMMARY) TELEGRAM_WEEKLY_SUMMARY="$value" ;;
|
|
TELEGRAM_SERVER_LABEL) TELEGRAM_SERVER_LABEL="$value" ;;
|
|
TELEGRAM_START_HOUR) [[ "$value" =~ ^[0-9]+$ ]] && TELEGRAM_START_HOUR="$value" ;;
|
|
esac
|
|
done < <(grep '^[A-Z_][A-Z_0-9]*=' "$INSTALL_DIR/settings.conf")
|
|
}
|
|
|
|
# Load settings
|
|
_load_settings
|
|
ROLE=${ROLE:-server}
|
|
PAQET_VERSION=${PAQET_VERSION:-unknown}
|
|
LISTEN_PORT=${LISTEN_PORT:-8443}
|
|
SOCKS_PORT=${SOCKS_PORT:-1080}
|
|
INTERFACE=${INTERFACE:-eth0}
|
|
LOCAL_IP=${LOCAL_IP:-}
|
|
GATEWAY_MAC=${GATEWAY_MAC:-}
|
|
ENCRYPTION_KEY=${ENCRYPTION_KEY:-}
|
|
REMOTE_SERVER=${REMOTE_SERVER:-}
|
|
TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
|
|
TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
|
|
TELEGRAM_INTERVAL=${TELEGRAM_INTERVAL:-6}
|
|
TELEGRAM_ENABLED=${TELEGRAM_ENABLED:-false}
|
|
TELEGRAM_ALERTS_ENABLED=${TELEGRAM_ALERTS_ENABLED:-true}
|
|
TELEGRAM_DAILY_SUMMARY=${TELEGRAM_DAILY_SUMMARY:-true}
|
|
TELEGRAM_WEEKLY_SUMMARY=${TELEGRAM_WEEKLY_SUMMARY:-true}
|
|
TELEGRAM_SERVER_LABEL=${TELEGRAM_SERVER_LABEL:-}
|
|
TELEGRAM_START_HOUR=${TELEGRAM_START_HOUR:-0}
|
|
BACKEND=${BACKEND:-paqet}
|
|
GFK_VIO_PORT=${GFK_VIO_PORT:-}
|
|
GFK_QUIC_PORT=${GFK_QUIC_PORT:-}
|
|
GFK_AUTH_CODE=${GFK_AUTH_CODE:-}
|
|
GFK_PORT_MAPPINGS=${GFK_PORT_MAPPINGS:-}
|
|
MICROSOCKS_PORT=${MICROSOCKS_PORT:-}
|
|
GFK_SERVER_IP=${GFK_SERVER_IP:-}
|
|
|
|
# Ensure root
|
|
if [ "$EUID" -ne 0 ]; then
|
|
echo -e "${RED}Error: This command must be run as root (use sudo paqctl)${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
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"; }
|
|
|
|
# Retry helper with exponential backoff for API requests
|
|
_curl_with_retry() {
|
|
local url="$1"
|
|
local max_attempts="${2:-3}"
|
|
local attempt=1
|
|
local delay=2
|
|
local response=""
|
|
while [ $attempt -le $max_attempts ]; do
|
|
response=$(curl -s --max-time 15 "$url" 2>/dev/null)
|
|
if [ -n "$response" ]; then
|
|
if echo "$response" | grep -q '"message".*rate limit'; then
|
|
log_warn "API rate limited, waiting ${delay}s..."
|
|
sleep $delay
|
|
delay=$((delay * 2))
|
|
attempt=$((attempt + 1))
|
|
continue
|
|
fi
|
|
echo "$response"
|
|
return 0
|
|
fi
|
|
[ $attempt -lt $max_attempts ] && sleep $delay
|
|
delay=$((delay * 2))
|
|
attempt=$((attempt + 1))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
_validate_version_tag() {
|
|
# Strict validation: only allow vX.Y.Z or X.Y.Z format with optional -suffix
|
|
[[ "$1" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]]
|
|
}
|
|
|
|
# Safe sed: escape replacement string to prevent metachar injection
|
|
_sed_escape() { printf '%s\n' "$1" | sed 's/[&/\]/\\&/g'; }
|
|
_safe_update_setting() {
|
|
local key="$1" value="$2" file="$3"
|
|
local escaped_value
|
|
escaped_value=$(_sed_escape "$value")
|
|
sed "s/^${key}=.*/${key}=\"${escaped_value}\"/" "$file" > "$file.tmp" 2>/dev/null && mv "$file.tmp" "$file" || true
|
|
}
|
|
|
|
print_header() {
|
|
echo -e "${CYAN}"
|
|
echo "╔════════════════════════════════════════════════════════════════╗"
|
|
echo "║ PAQCTL - Paqet Manager v${VERSION} ║"
|
|
echo "║ Raw-socket encrypted proxy - bypass firewalls ║"
|
|
echo "╚════════════════════════════════════════════════════════════════╝"
|
|
echo -e "${NC}"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Settings Save (management script)
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
save_settings() {
|
|
local _tg_token="${TELEGRAM_BOT_TOKEN:-}"
|
|
local _tg_chat="${TELEGRAM_CHAT_ID:-}"
|
|
local _tg_interval="${TELEGRAM_INTERVAL:-6}"
|
|
local _tg_enabled="${TELEGRAM_ENABLED:-false}"
|
|
local _tg_alerts="${TELEGRAM_ALERTS_ENABLED:-true}"
|
|
local _tg_daily="${TELEGRAM_DAILY_SUMMARY:-true}"
|
|
local _tg_weekly="${TELEGRAM_WEEKLY_SUMMARY:-true}"
|
|
local _tg_label="${TELEGRAM_SERVER_LABEL:-}"
|
|
local _tg_start_hour="${TELEGRAM_START_HOUR:-0}"
|
|
# Sanitize sensitive values - remove shell metacharacters and control chars
|
|
_sanitize_value() {
|
|
printf '%s' "$1" | tr -d '"$`\\'\''(){}[]<>|;&!\n\r\t'
|
|
}
|
|
local _safe_key; _safe_key=$(_sanitize_value "${ENCRYPTION_KEY:-}")
|
|
local _safe_auth; _safe_auth=$(_sanitize_value "${GFK_AUTH_CODE:-}")
|
|
local _tmp
|
|
_tmp=$(mktemp "$INSTALL_DIR/settings.conf.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
|
|
(umask 077; cat > "$_tmp" << SEOF
|
|
BACKEND="${BACKEND:-paqet}"
|
|
ROLE="${ROLE}"
|
|
PAQET_VERSION="${PAQET_VERSION:-unknown}"
|
|
PAQCTL_VERSION="${VERSION}"
|
|
LISTEN_PORT="${LISTEN_PORT:-}"
|
|
SOCKS_PORT="${SOCKS_PORT:-}"
|
|
INTERFACE="${INTERFACE:-}"
|
|
LOCAL_IP="${LOCAL_IP:-}"
|
|
GATEWAY_MAC="${GATEWAY_MAC:-}"
|
|
ENCRYPTION_KEY="${_safe_key}"
|
|
REMOTE_SERVER="${REMOTE_SERVER:-}"
|
|
GFK_VIO_PORT="${GFK_VIO_PORT:-}"
|
|
GFK_VIO_CLIENT_PORT="${GFK_VIO_CLIENT_PORT:-}"
|
|
GFK_QUIC_PORT="${GFK_QUIC_PORT:-}"
|
|
GFK_QUIC_CLIENT_PORT="${GFK_QUIC_CLIENT_PORT:-}"
|
|
GFK_AUTH_CODE="${_safe_auth}"
|
|
GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS:-}"
|
|
MICROSOCKS_PORT="${MICROSOCKS_PORT:-}"
|
|
GFK_SERVER_IP="${GFK_SERVER_IP:-}"
|
|
TELEGRAM_BOT_TOKEN="${_tg_token}"
|
|
TELEGRAM_CHAT_ID="${_tg_chat}"
|
|
TELEGRAM_INTERVAL=${_tg_interval}
|
|
TELEGRAM_ENABLED=${_tg_enabled}
|
|
TELEGRAM_ALERTS_ENABLED=${_tg_alerts}
|
|
TELEGRAM_DAILY_SUMMARY=${_tg_daily}
|
|
TELEGRAM_WEEKLY_SUMMARY=${_tg_weekly}
|
|
TELEGRAM_SERVER_LABEL="${_tg_label}"
|
|
TELEGRAM_START_HOUR=${_tg_start_hour}
|
|
SEOF
|
|
)
|
|
if ! mv "$_tmp" "$INSTALL_DIR/settings.conf"; then
|
|
log_error "Failed to save settings"
|
|
rm -f "$_tmp"
|
|
return 1
|
|
fi
|
|
chmod 600 "$INSTALL_DIR/settings.conf" 2>/dev/null
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Architecture Detection & Paqet Download (management script)
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
detect_arch() {
|
|
local arch
|
|
arch=$(uname -m)
|
|
case "$arch" in
|
|
x86_64|amd64) echo "amd64" ;;
|
|
aarch64|arm64) echo "arm64" ;;
|
|
*)
|
|
log_error "Unsupported architecture: $arch"
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
download_paqet() {
|
|
local version="$1"
|
|
local arch
|
|
arch=$(detect_arch) || return 1
|
|
local os_name="linux"
|
|
local ext="tar.gz"
|
|
local filename="paqet-${os_name}-${arch}-${version}.${ext}"
|
|
local url="https://github.com/${PAQET_REPO}/releases/download/${version}/${filename}"
|
|
|
|
log_info "Downloading paqet ${version} for ${os_name}/${arch}..."
|
|
|
|
mkdir -p "$INSTALL_DIR/bin" || { log_error "Failed to create directory"; return 1; }
|
|
local tmp_file
|
|
tmp_file=$(mktemp "/tmp/paqet-download-XXXXXXXX.${ext}") || { log_error "Failed to create temp file"; return 1; }
|
|
|
|
if ! curl -sL --max-time 120 --fail -o "$tmp_file" "$url"; then
|
|
log_error "Failed to download: $url"
|
|
rm -f "$tmp_file"
|
|
return 1
|
|
fi
|
|
|
|
# Validate download
|
|
local fsize
|
|
fsize=$(stat -c%s "$tmp_file" 2>/dev/null || stat -f%z "$tmp_file" 2>/dev/null || wc -c < "$tmp_file" 2>/dev/null || echo 0)
|
|
if [ "$fsize" -lt 1000 ]; then
|
|
log_error "Downloaded file is too small ($fsize bytes)"
|
|
rm -f "$tmp_file"
|
|
return 1
|
|
fi
|
|
|
|
# Extract
|
|
log_info "Extracting..."
|
|
local tmp_extract
|
|
tmp_extract=$(mktemp -d "/tmp/paqet-extract-XXXXXXXX") || { log_error "Failed to create temp dir"; return 1; }
|
|
if ! tar -xzf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then
|
|
log_error "Failed to extract archive"
|
|
rm -f "$tmp_file"; rm -rf "$tmp_extract"
|
|
return 1
|
|
fi
|
|
|
|
# Find the binary
|
|
local binary_name="paqet_${os_name}_${arch}"
|
|
local found_binary=""
|
|
found_binary=$(find "$tmp_extract" -name "$binary_name" -type f 2>/dev/null | head -1)
|
|
[ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f -executable 2>/dev/null | head -1)
|
|
[ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f 2>/dev/null | head -1)
|
|
|
|
if [ -z "$found_binary" ]; then
|
|
log_error "Could not find paqet binary in archive"
|
|
rm -f "$tmp_file"; rm -rf "$tmp_extract"
|
|
return 1
|
|
fi
|
|
|
|
# Stop paqet if running to avoid "Text file busy" error
|
|
pkill -f "$INSTALL_DIR/bin/paqet" 2>/dev/null || true
|
|
sleep 1
|
|
|
|
if ! cp "$found_binary" "$INSTALL_DIR/bin/paqet"; then
|
|
log_error "Failed to copy paqet binary"
|
|
rm -f "$tmp_file"; rm -rf "$tmp_extract"
|
|
return 1
|
|
fi
|
|
chmod +x "$INSTALL_DIR/bin/paqet" || { log_error "Failed to make paqet executable"; return 1; }
|
|
|
|
rm -f "$tmp_file"; rm -rf "$tmp_extract"
|
|
|
|
if "$INSTALL_DIR/bin/paqet" version &>/dev/null; then
|
|
log_success "paqet ${version} installed successfully"
|
|
else
|
|
log_warn "paqet installed but version check failed (may need libpcap)"
|
|
fi
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# GFK Helper Functions (management script)
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
install_python_deps() {
|
|
log_info "Installing Python dependencies..."
|
|
if ! command -v python3 &>/dev/null; then
|
|
if command -v apt-get &>/dev/null; then apt-get install -y python3 2>/dev/null
|
|
elif command -v yum &>/dev/null; then yum install -y python3 2>/dev/null
|
|
elif command -v apk &>/dev/null; then apk add python3 2>/dev/null
|
|
fi
|
|
fi
|
|
# Install python3-venv (version-specific for apt)
|
|
if command -v apt-get &>/dev/null; then
|
|
local pyver; pyver=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
|
|
[ -n "$pyver" ] && apt-get install -y "python${pyver}-venv" 2>/dev/null || apt-get install -y python3-venv 2>/dev/null
|
|
fi
|
|
# Use venv (recreate if broken/incomplete)
|
|
local VENV_DIR="$INSTALL_DIR/venv"
|
|
if [ ! -x "$VENV_DIR/bin/pip" ]; then
|
|
[ -d "$VENV_DIR" ] && rm -rf "$VENV_DIR"
|
|
python3 -m venv "$VENV_DIR" || { log_error "Failed to create venv"; return 1; }
|
|
fi
|
|
"$VENV_DIR/bin/pip" install --quiet --upgrade pip 2>/dev/null || true
|
|
"$VENV_DIR/bin/pip" install --quiet scapy aioquic 2>/dev/null || { log_error "Failed to install Python packages"; return 1; }
|
|
"$VENV_DIR/bin/python" -c "import scapy; import aioquic" 2>/dev/null || { log_error "Python deps verification failed"; return 1; }
|
|
log_success "Python dependencies OK"
|
|
}
|
|
|
|
install_microsocks() {
|
|
log_info "Installing microsocks..."
|
|
[ -x "$INSTALL_DIR/bin/microsocks" ] && { log_success "microsocks already installed"; return 0; }
|
|
command -v gcc &>/dev/null || {
|
|
if command -v apt-get &>/dev/null; then apt-get install -y gcc make 2>/dev/null
|
|
elif command -v yum &>/dev/null; then yum install -y gcc make 2>/dev/null
|
|
elif command -v apk &>/dev/null; then apk add gcc make musl-dev 2>/dev/null
|
|
fi
|
|
}
|
|
local tmp_dir; tmp_dir=$(mktemp -d)
|
|
curl -sL "https://github.com/${MICROSOCKS_REPO}/archive/refs/heads/master.tar.gz" -o "$tmp_dir/ms.tar.gz" || { rm -rf "$tmp_dir"; return 1; }
|
|
tar -xzf "$tmp_dir/ms.tar.gz" -C "$tmp_dir" 2>/dev/null || { rm -rf "$tmp_dir"; return 1; }
|
|
local src; src=$(find "$tmp_dir" -maxdepth 1 -type d -name "microsocks*" | head -1)
|
|
[ -z "$src" ] && { rm -rf "$tmp_dir"; return 1; }
|
|
make -C "$src" -j"$(nproc 2>/dev/null || echo 1)" 2>/dev/null || { rm -rf "$tmp_dir"; return 1; }
|
|
mkdir -p "$INSTALL_DIR/bin"
|
|
cp "$src/microsocks" "$INSTALL_DIR/bin/microsocks"
|
|
chmod 755 "$INSTALL_DIR/bin/microsocks"
|
|
rm -rf "$tmp_dir"
|
|
log_success "microsocks installed"
|
|
}
|
|
|
|
download_gfk() {
|
|
log_info "Downloading GFW-knocker scripts..."
|
|
mkdir -p "$GFK_DIR" || return 1
|
|
# Note: parameters.py is generated by generate_gfk_config(), don't download it
|
|
local f
|
|
# Download server scripts from gfk/server/
|
|
for f in mainserver.py quic_server.py vio_server.py; do
|
|
curl -sL "$GFK_RAW_URL/server/$f" -o "$GFK_DIR/$f" || { log_error "Failed to download $f"; return 1; }
|
|
done
|
|
# Download client scripts from gfk/client/
|
|
for f in mainclient.py quic_client.py vio_client.py; do
|
|
curl -sL "$GFK_RAW_URL/client/$f" -o "$GFK_DIR/$f" || { log_error "Failed to download $f"; return 1; }
|
|
done
|
|
chmod 600 "$GFK_DIR"/*.py
|
|
# Patch mainserver.py to use venv python for subprocesses
|
|
[ -f "$GFK_DIR/mainserver.py" ] && sed -i "s|'python3'|'$INSTALL_DIR/venv/bin/python'|g" "$GFK_DIR/mainserver.py"
|
|
log_success "GFW-knocker scripts downloaded"
|
|
}
|
|
|
|
generate_gfk_certs() {
|
|
[ -f "$GFK_DIR/cert.pem" ] && [ -f "$GFK_DIR/key.pem" ] && return 0
|
|
log_info "Generating QUIC certificates..."
|
|
openssl req -x509 -newkey rsa:2048 -keyout "$GFK_DIR/key.pem" \
|
|
-out "$GFK_DIR/cert.pem" -days 3650 -nodes -subj "/CN=gfk" 2>/dev/null || return 1
|
|
chmod 600 "$GFK_DIR/key.pem" "$GFK_DIR/cert.pem"
|
|
log_success "QUIC certificates generated"
|
|
}
|
|
|
|
generate_gfk_config() {
|
|
log_info "Generating GFW-knocker config..."
|
|
local _tmp; _tmp=$(mktemp "$GFK_DIR/parameters.py.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
|
|
local vio_tcp_server_port="${GFK_VIO_PORT:-45000}"
|
|
local vio_tcp_client_port="${GFK_VIO_CLIENT_PORT:-40000}"
|
|
local vio_udp_server_port="${GFK_VIO_UDP_SERVER:-35000}"
|
|
local vio_udp_client_port="${GFK_VIO_UDP_CLIENT:-30000}"
|
|
local quic_server_port="${GFK_QUIC_PORT:-25000}"
|
|
local quic_client_port="${GFK_QUIC_CLIENT_PORT:-20000}"
|
|
# Validate all ports are numeric
|
|
local _p; for _p in "$vio_tcp_server_port" "$vio_tcp_client_port" "$vio_udp_server_port" \
|
|
"$vio_udp_client_port" "$quic_server_port" "$quic_client_port"; do
|
|
[[ "$_p" =~ ^[0-9]+$ ]] || { log_error "Invalid port: $_p"; rm -f "$_tmp"; return 1; }
|
|
done
|
|
# Escape Python strings to prevent code injection
|
|
_esc_py() { local s="$1"; s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; s="${s//\'/\\\'}"; printf '%s' "$s"; }
|
|
local safe_ip; safe_ip=$(_esc_py "${GFK_SERVER_IP:-}")
|
|
local safe_auth; safe_auth=$(_esc_py "${GFK_AUTH_CODE:-}")
|
|
local safe_dir; safe_dir=$(_esc_py "${GFK_DIR}")
|
|
# Validate and build port mapping
|
|
local tcp_mapping="${GFK_PORT_MAPPINGS:-14000:443}"
|
|
local mapping_str="{" first=true pair lport rport
|
|
for pair in $(echo "$tcp_mapping" | tr ',' ' '); do
|
|
lport=$(echo "$pair" | cut -d: -f1); rport=$(echo "$pair" | cut -d: -f2)
|
|
[[ "$lport" =~ ^[0-9]+$ ]] && [[ "$rport" =~ ^[0-9]+$ ]] || { log_error "Invalid mapping: $pair"; rm -f "$_tmp"; return 1; }
|
|
[ "$first" = true ] && { mapping_str="${mapping_str}${lport}: ${rport}"; first=false; } || mapping_str="${mapping_str}, ${lport}: ${rport}"
|
|
done
|
|
mapping_str="${mapping_str}}"
|
|
(umask 077; cat > "$_tmp" << PYEOF
|
|
vps_ip = "${safe_ip}"
|
|
xray_server_ip_address = "127.0.0.1"
|
|
tcp_port_mapping = ${mapping_str}
|
|
udp_port_mapping = {}
|
|
vio_tcp_server_port = ${vio_tcp_server_port}
|
|
vio_tcp_client_port = ${vio_tcp_client_port}
|
|
vio_udp_server_port = ${vio_udp_server_port}
|
|
vio_udp_client_port = ${vio_udp_client_port}
|
|
quic_server_port = ${quic_server_port}
|
|
quic_client_port = ${quic_client_port}
|
|
quic_local_ip = "127.0.0.1"
|
|
quic_idle_timeout = 86400
|
|
udp_timeout = 300
|
|
quic_mtu = 1420
|
|
quic_verify_cert = False
|
|
quic_max_data = 1073741824
|
|
quic_max_stream_data = 1073741824
|
|
quic_auth_code = "${safe_auth}"
|
|
quic_cert_filepath = ("${safe_dir}/cert.pem", "${safe_dir}/key.pem")
|
|
PYEOF
|
|
)
|
|
mv "$_tmp" "$GFK_DIR/parameters.py" || { rm -f "$_tmp"; return 1; }
|
|
chmod 600 "$GFK_DIR/parameters.py"
|
|
log_success "GFW-knocker config saved"
|
|
}
|
|
|
|
create_gfk_client_wrapper() {
|
|
local wrapper="$INSTALL_DIR/bin/gfk-client.sh"
|
|
local msport="${MICROSOCKS_PORT:-1080}"
|
|
mkdir -p "$INSTALL_DIR/bin"
|
|
cat > "$wrapper" << 'WEOF'
|
|
#!/bin/bash
|
|
set -e
|
|
GFK_DIR="REPLACE_GFK"
|
|
INSTALL_DIR="REPLACE_INST"
|
|
MICROSOCKS_PORT="REPLACE_MSP"
|
|
cd "$GFK_DIR"
|
|
"$INSTALL_DIR/venv/bin/python" mainclient.py &
|
|
PID1=$!
|
|
"$INSTALL_DIR/bin/microsocks" -i 127.0.0.1 -p "$MICROSOCKS_PORT" &
|
|
PID2=$!
|
|
trap "kill $PID1 $PID2 2>/dev/null; wait" EXIT INT TERM
|
|
wait
|
|
WEOF
|
|
sed "s#REPLACE_GFK#${GFK_DIR}#g; s#REPLACE_INST#${INSTALL_DIR}#g; s#REPLACE_MSP#${msport}#g" "$wrapper" > "$wrapper.sed" && mv "$wrapper.sed" "$wrapper"
|
|
chmod 755 "$wrapper"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Service Control
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
is_running() {
|
|
# Check which backends are installed
|
|
local paqet_installed=false gfk_installed=false
|
|
[ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
|
|
if [ "$ROLE" = "server" ]; then
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
|
|
else
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
|
|
fi
|
|
|
|
# If both backends installed, return true if EITHER is running
|
|
if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then
|
|
is_paqet_running && return 0
|
|
is_gfk_running && return 0
|
|
return 1
|
|
fi
|
|
|
|
# Single backend mode - original logic
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
systemctl is-active paqctl.service &>/dev/null && return 0
|
|
elif [ -f /run/paqctl.pid ]; then
|
|
local _pid
|
|
_pid=$(cat /run/paqctl.pid 2>/dev/null)
|
|
# Validate PID is numeric and process exists
|
|
[[ "$_pid" =~ ^[0-9]+$ ]] && kill -0 "$_pid" 2>/dev/null && return 0
|
|
fi
|
|
# Also check for the process directly with more specific patterns
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
# Use full path matching to avoid false positives
|
|
pgrep -f "${GFK_DIR}/mainserver.py" &>/dev/null && return 0
|
|
pgrep -f "${GFK_DIR}/mainclient.py" &>/dev/null && return 0
|
|
pgrep -f "${INSTALL_DIR}/bin/gfk-client.sh" &>/dev/null && return 0
|
|
else
|
|
# Match specific config file path
|
|
pgrep -f "${INSTALL_DIR}/bin/paqet run -c ${INSTALL_DIR}/config.yaml" &>/dev/null && return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Check if paqet backend specifically is running
|
|
is_paqet_running() {
|
|
pgrep -f "${INSTALL_DIR}/bin/paqet run -c ${INSTALL_DIR}/config.yaml" &>/dev/null && return 0
|
|
return 1
|
|
}
|
|
|
|
# Check if GFK backend specifically is running
|
|
is_gfk_running() {
|
|
if [ "$ROLE" = "server" ]; then
|
|
pgrep -f "${GFK_DIR}/mainserver.py" &>/dev/null && return 0
|
|
else
|
|
pgrep -f "${GFK_DIR}/mainclient.py" &>/dev/null && return 0
|
|
pgrep -f "${INSTALL_DIR}/bin/gfk-client.sh" &>/dev/null && return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Start paqet backend only
|
|
start_paqet_backend() {
|
|
if is_paqet_running; then
|
|
log_warn "paqet is already running"
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -f "$INSTALL_DIR/bin/paqet" ]; then
|
|
log_error "paqet binary not installed. Use 'Install additional backend' first."
|
|
return 1
|
|
fi
|
|
|
|
# Generate config.yaml if missing - prompt for values
|
|
if [ ! -f "$INSTALL_DIR/config.yaml" ]; then
|
|
echo ""
|
|
echo -e "${YELLOW}config.yaml not found. Let's configure paqet:${NC}"
|
|
echo ""
|
|
|
|
detect_network
|
|
local _det_iface="$DETECTED_IFACE"
|
|
local _det_ip="$DETECTED_IP"
|
|
local _det_mac="$DETECTED_GW_MAC"
|
|
|
|
echo -e "${BOLD}Network Interface${NC} [${_det_iface:-eth0}]:"
|
|
read -p " Interface: " input < /dev/tty || true
|
|
local _iface="${input:-${_det_iface:-eth0}}"
|
|
|
|
echo -e "${BOLD}Local IP${NC} [${_det_ip:-}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
local _local_ip="${input:-$_det_ip}"
|
|
|
|
echo -e "${BOLD}Gateway MAC${NC} [${_det_mac:-}]:"
|
|
read -p " MAC: " input < /dev/tty || true
|
|
local _gw_mac="${input:-$_det_mac}"
|
|
|
|
local _key
|
|
_key=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true)
|
|
if [ -z "$_key" ]; then
|
|
_key=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32 || true)
|
|
fi
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
echo -e "${BOLD}Listen Port${NC} [8443]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
local _port="${input:-8443}"
|
|
|
|
echo ""
|
|
echo -e "${GREEN}${BOLD} Generated Key: ${_key}${NC}"
|
|
echo -e "${BOLD}Encryption Key${NC} (Enter to use generated):"
|
|
read -p " Key: " input < /dev/tty || true
|
|
[ -n "$input" ] && _key="$input"
|
|
|
|
LISTEN_PORT="$_port"
|
|
ENCRYPTION_KEY="$_key"
|
|
|
|
cat > "$INSTALL_DIR/config.yaml" << EOFCFG
|
|
role: "server"
|
|
log:
|
|
level: "info"
|
|
listen:
|
|
addr: ":${_port}"
|
|
network:
|
|
interface: "${_iface}"
|
|
ipv4:
|
|
addr: "${_local_ip}:${_port}"
|
|
router_mac: "${_gw_mac}"
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_key}"
|
|
EOFCFG
|
|
else
|
|
echo -e "${BOLD}Remote Server${NC} (IP:PORT):"
|
|
read -p " Server: " input < /dev/tty || true
|
|
local _server="${input:-${REMOTE_SERVER:-}}"
|
|
|
|
echo -e "${BOLD}Encryption Key${NC} (from server):"
|
|
read -p " Key: " input < /dev/tty || true
|
|
[ -n "$input" ] && _key="$input"
|
|
|
|
echo -e "${BOLD}SOCKS5 Port${NC} [1080]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
local _socks="${input:-1080}"
|
|
|
|
REMOTE_SERVER="$_server"
|
|
SOCKS_PORT="$_socks"
|
|
ENCRYPTION_KEY="$_key"
|
|
|
|
cat > "$INSTALL_DIR/config.yaml" << EOFCFG
|
|
role: "client"
|
|
log:
|
|
level: "info"
|
|
socks5:
|
|
- listen: "127.0.0.1:${_socks}"
|
|
network:
|
|
interface: "${_iface}"
|
|
ipv4:
|
|
addr: "${_local_ip}:0"
|
|
router_mac: "${_gw_mac}"
|
|
server:
|
|
addr: "${_server}"
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_key}"
|
|
EOFCFG
|
|
fi
|
|
|
|
if [ ! -f "$INSTALL_DIR/config.yaml" ]; then
|
|
log_error "Failed to write config.yaml"
|
|
return 1
|
|
fi
|
|
chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null
|
|
INTERFACE="$_iface"
|
|
LOCAL_IP="$_local_ip"
|
|
GATEWAY_MAC="$_gw_mac"
|
|
save_settings 2>/dev/null || true
|
|
log_success "Configuration saved"
|
|
echo ""
|
|
fi
|
|
|
|
log_info "Starting paqet backend..."
|
|
|
|
# Apply paqet firewall rules
|
|
local _saved_backend="$BACKEND"
|
|
BACKEND="paqet"
|
|
_apply_firewall
|
|
BACKEND="$_saved_backend"
|
|
|
|
(umask 077; touch /var/log/paqet-backend.log)
|
|
nohup "$INSTALL_DIR/bin/paqet" run -c "$INSTALL_DIR/config.yaml" > /var/log/paqet-backend.log 2>&1 &
|
|
echo $! > /run/paqet-backend.pid
|
|
|
|
sleep 2
|
|
if is_paqet_running; then
|
|
log_success "paqet backend started"
|
|
else
|
|
log_error "paqet failed to start. Check: tail /var/log/paqet-backend.log"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Stop paqet backend only
|
|
stop_paqet_backend() {
|
|
if ! is_paqet_running; then
|
|
log_warn "paqet is not running"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Stopping paqet backend..."
|
|
|
|
if [ -f /run/paqet-backend.pid ]; then
|
|
local _pid
|
|
_pid=$(cat /run/paqet-backend.pid 2>/dev/null)
|
|
if [ -n "$_pid" ] && [[ "$_pid" =~ ^[0-9]+$ ]]; then
|
|
kill "$_pid" 2>/dev/null
|
|
sleep 1
|
|
kill -0 "$_pid" 2>/dev/null && kill -9 "$_pid" 2>/dev/null
|
|
fi
|
|
rm -f /run/paqet-backend.pid
|
|
fi
|
|
|
|
pkill -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true
|
|
|
|
# Remove paqet firewall rules
|
|
local _saved_backend="$BACKEND"
|
|
BACKEND="paqet"
|
|
_remove_firewall
|
|
BACKEND="$_saved_backend"
|
|
|
|
sleep 1
|
|
if ! is_paqet_running; then
|
|
log_success "paqet backend stopped"
|
|
else
|
|
pkill -9 -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true
|
|
log_success "paqet backend stopped (forced)"
|
|
fi
|
|
}
|
|
|
|
# Start GFK backend only
|
|
start_gfk_backend() {
|
|
if is_gfk_running; then
|
|
log_warn "gfw-knocker is already running"
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -d "$GFK_DIR" ] || [ ! -f "$GFK_DIR/quic_server.py" ]; then
|
|
log_error "gfw-knocker not installed. Use 'Install additional backend' first."
|
|
return 1
|
|
fi
|
|
|
|
log_info "Starting gfw-knocker backend..."
|
|
|
|
# Apply GFK firewall rules
|
|
local _saved_backend="$BACKEND"
|
|
BACKEND="gfw-knocker"
|
|
_apply_firewall
|
|
BACKEND="$_saved_backend"
|
|
|
|
(umask 077; touch /var/log/gfk-backend.log)
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
# Start Xray if not running
|
|
if command -v xray &>/dev/null || [ -x /usr/local/bin/xray ]; then
|
|
if ! pgrep -f "xray run" &>/dev/null; then
|
|
systemctl start xray 2>/dev/null || xray run -c /usr/local/etc/xray/config.json &>/dev/null &
|
|
fi
|
|
fi
|
|
# Run from GFK_DIR so relative script paths work
|
|
pushd "$GFK_DIR" >/dev/null
|
|
nohup "$INSTALL_DIR/venv/bin/python" "$GFK_DIR/mainserver.py" > /var/log/gfk-backend.log 2>&1 &
|
|
popd >/dev/null
|
|
else
|
|
if [ -x "$INSTALL_DIR/bin/gfk-client.sh" ]; then
|
|
nohup "$INSTALL_DIR/bin/gfk-client.sh" > /var/log/gfk-backend.log 2>&1 &
|
|
else
|
|
# Run from GFK_DIR so relative script paths work
|
|
pushd "$GFK_DIR" >/dev/null
|
|
nohup "$INSTALL_DIR/venv/bin/python" "$GFK_DIR/mainclient.py" > /var/log/gfk-backend.log 2>&1 &
|
|
popd >/dev/null
|
|
fi
|
|
fi
|
|
echo $! > /run/gfk-backend.pid
|
|
|
|
sleep 2
|
|
if is_gfk_running; then
|
|
log_success "gfw-knocker backend started"
|
|
else
|
|
log_error "gfw-knocker failed to start. Check: tail /var/log/gfk-backend.log"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Stop GFK backend only
|
|
stop_gfk_backend() {
|
|
if ! is_gfk_running; then
|
|
log_warn "gfw-knocker is not running"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Stopping gfw-knocker backend..."
|
|
|
|
if [ -f /run/gfk-backend.pid ]; then
|
|
local _pid
|
|
_pid=$(cat /run/gfk-backend.pid 2>/dev/null)
|
|
if [ -n "$_pid" ] && [[ "$_pid" =~ ^[0-9]+$ ]]; then
|
|
kill "$_pid" 2>/dev/null
|
|
sleep 1
|
|
kill -0 "$_pid" 2>/dev/null && kill -9 "$_pid" 2>/dev/null
|
|
fi
|
|
rm -f /run/gfk-backend.pid
|
|
fi
|
|
|
|
pkill -f "${GFK_DIR}/mainserver.py" 2>/dev/null || true
|
|
pkill -f "${GFK_DIR}/mainclient.py" 2>/dev/null || true
|
|
pkill -f "${INSTALL_DIR}/bin/gfk-client.sh" 2>/dev/null || true
|
|
pkill -f "${INSTALL_DIR}/bin/microsocks" 2>/dev/null || true
|
|
|
|
# Remove GFK firewall rules
|
|
local _saved_backend="$BACKEND"
|
|
BACKEND="gfw-knocker"
|
|
_remove_firewall
|
|
BACKEND="$_saved_backend"
|
|
|
|
sleep 1
|
|
if ! is_gfk_running; then
|
|
log_success "gfw-knocker backend stopped"
|
|
else
|
|
pkill -9 -f "${GFK_DIR}/mainserver.py" 2>/dev/null || true
|
|
pkill -9 -f "${GFK_DIR}/mainclient.py" 2>/dev/null || true
|
|
pkill -9 -f "${INSTALL_DIR}/bin/gfk-client.sh" 2>/dev/null || true
|
|
log_success "gfw-knocker backend stopped (forced)"
|
|
fi
|
|
}
|
|
|
|
start_paqet() {
|
|
# Check which backends are installed
|
|
local paqet_installed=false gfk_installed=false
|
|
[ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
|
|
if [ "$ROLE" = "server" ]; then
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
|
|
else
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
|
|
fi
|
|
|
|
# If both backends installed, start both
|
|
if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then
|
|
local started_something=false
|
|
if ! is_paqet_running; then
|
|
start_paqet_backend && started_something=true
|
|
else
|
|
log_warn "paqet is already running"
|
|
fi
|
|
if ! is_gfk_running; then
|
|
start_gfk_backend && started_something=true
|
|
else
|
|
log_warn "gfw-knocker is already running"
|
|
fi
|
|
[ "$started_something" = true ] && return 0
|
|
return 0
|
|
fi
|
|
|
|
# Single backend mode - original logic
|
|
if is_running; then
|
|
log_warn "${BACKEND} is already running"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Starting paqet..."
|
|
local _direct_start=false
|
|
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
systemctl start paqctl.service 2>/dev/null
|
|
elif command -v rc-service &>/dev/null; then
|
|
rc-service paqctl start 2>/dev/null
|
|
elif [ -x /etc/init.d/paqctl ]; then
|
|
/etc/init.d/paqctl start 2>/dev/null
|
|
else
|
|
# Direct start - track for cleanup on failure
|
|
_direct_start=true
|
|
_apply_firewall
|
|
(umask 077; touch /var/log/paqctl.log)
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
if [ "$ROLE" = "client" ] && [ -x "$INSTALL_DIR/bin/gfk-client.sh" ]; then
|
|
nohup "$INSTALL_DIR/bin/gfk-client.sh" > /var/log/paqctl.log 2>&1 &
|
|
else
|
|
nohup "$INSTALL_DIR/venv/bin/python" "$GFK_DIR/mainserver.py" > /var/log/paqctl.log 2>&1 &
|
|
fi
|
|
else
|
|
nohup "$INSTALL_DIR/bin/paqet" run -c "$INSTALL_DIR/config.yaml" > /var/log/paqctl.log 2>&1 &
|
|
fi
|
|
echo $! > /run/paqctl.pid
|
|
fi
|
|
|
|
sleep 2
|
|
if is_running; then
|
|
log_success "${BACKEND} started successfully"
|
|
else
|
|
log_error "${BACKEND} failed to start. Check logs: sudo paqctl logs"
|
|
# Clean up firewall rules on failure (only for direct start)
|
|
if [ "$_direct_start" = true ]; then
|
|
_remove_firewall
|
|
fi
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
stop_paqet() {
|
|
# Check which backends are installed
|
|
local paqet_installed=false gfk_installed=false
|
|
[ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
|
|
if [ "$ROLE" = "server" ]; then
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
|
|
else
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
|
|
fi
|
|
|
|
# If both backends installed, stop both
|
|
if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then
|
|
local stopped_something=false
|
|
if is_paqet_running; then
|
|
stop_paqet_backend && stopped_something=true
|
|
fi
|
|
if is_gfk_running; then
|
|
stop_gfk_backend && stopped_something=true
|
|
fi
|
|
if [ "$stopped_something" = false ]; then
|
|
log_warn "No backends are running"
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
# Single backend mode - original logic
|
|
if ! is_running; then
|
|
log_warn "paqet is not running"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Stopping paqet..."
|
|
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
systemctl stop paqctl.service 2>/dev/null
|
|
elif command -v rc-service &>/dev/null; then
|
|
rc-service paqctl stop 2>/dev/null
|
|
elif [ -x /etc/init.d/paqctl ]; then
|
|
/etc/init.d/paqctl stop 2>/dev/null
|
|
else
|
|
if [ -f /run/paqctl.pid ]; then
|
|
local _pid
|
|
_pid=$(cat /run/paqctl.pid 2>/dev/null)
|
|
if [ -n "$_pid" ]; then
|
|
kill "$_pid" 2>/dev/null
|
|
local _count=0
|
|
while kill -0 "$_pid" 2>/dev/null && [ $_count -lt 10 ]; do
|
|
sleep 1
|
|
_count=$((_count + 1))
|
|
done
|
|
kill -0 "$_pid" 2>/dev/null && kill -9 "$_pid" 2>/dev/null
|
|
fi
|
|
rm -f /run/paqctl.pid
|
|
fi
|
|
# Use specific paths to avoid killing unrelated processes
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
pkill -f "${GFK_DIR}/mainserver.py" 2>/dev/null || true
|
|
pkill -f "${GFK_DIR}/mainclient.py" 2>/dev/null || true
|
|
pkill -f "${INSTALL_DIR}/bin/gfk-client.sh" 2>/dev/null || true
|
|
pkill -f "${INSTALL_DIR}/bin/microsocks" 2>/dev/null || true
|
|
else
|
|
pkill -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true
|
|
fi
|
|
_remove_firewall
|
|
fi
|
|
|
|
sleep 1
|
|
if ! is_running; then
|
|
log_success "${BACKEND} stopped"
|
|
else
|
|
log_warn "${BACKEND} may still be running, force killing..."
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
pkill -9 -f "${GFK_DIR}/mainserver.py" 2>/dev/null || true
|
|
pkill -9 -f "${GFK_DIR}/mainclient.py" 2>/dev/null || true
|
|
pkill -9 -f "${INSTALL_DIR}/bin/gfk-client.sh" 2>/dev/null || true
|
|
pkill -9 -f "${INSTALL_DIR}/bin/microsocks" 2>/dev/null || true
|
|
else
|
|
pkill -9 -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true
|
|
fi
|
|
sleep 1
|
|
log_success "${BACKEND} stopped"
|
|
fi
|
|
}
|
|
|
|
restart_paqet() {
|
|
stop_paqet
|
|
sleep 1
|
|
start_paqet
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# iptables (internal commands)
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
_apply_firewall() {
|
|
[ "$ROLE" != "server" ] && return 0
|
|
if ! command -v iptables &>/dev/null; then
|
|
echo -e "${YELLOW}[!]${NC} iptables not found. Firewall rules cannot be applied." >&2
|
|
return 1
|
|
fi
|
|
|
|
# Tag for identifying our rules
|
|
local TAG="paqctl"
|
|
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
# GFK: DROP TCP on VIO port so OS doesn't respond, raw socket handles it
|
|
local vio_port="${GFK_VIO_PORT:-45000}"
|
|
# Drop incoming TCP on VIO port (scapy sniffer will handle it)
|
|
iptables -C INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || \
|
|
iptables -A INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || \
|
|
echo -e "${YELLOW}[!]${NC} Failed to add VIO port DROP rule" >&2
|
|
# Drop outgoing RST packets on VIO port (prevents kernel from interfering with violated TCP)
|
|
iptables -C OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || \
|
|
iptables -A OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || \
|
|
echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule" >&2
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -C INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || \
|
|
ip6tables -A INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || true
|
|
ip6tables -C OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || \
|
|
ip6tables -A OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || true
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
modprobe iptable_raw 2>/dev/null || true
|
|
modprobe iptable_mangle 2>/dev/null || true
|
|
local port="${LISTEN_PORT:-8443}"
|
|
# IPv4 - all rules tagged with "paqctl" comment
|
|
iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
iptables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
echo -e "${YELLOW}[!]${NC} Failed to add PREROUTING NOTRACK rule" >&2
|
|
iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
iptables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
echo -e "${YELLOW}[!]${NC} Failed to add OUTPUT NOTRACK rule" >&2
|
|
iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
|
|
iptables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
|
|
echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule" >&2
|
|
# IPv6
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
ip6tables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
ip6tables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
|
|
ip6tables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
_remove_firewall() {
|
|
[ "$ROLE" != "server" ] && return 0
|
|
command -v iptables &>/dev/null || return 0
|
|
|
|
local TAG="paqctl"
|
|
|
|
# Always respect BACKEND variable - remove only that backend's firewall rules
|
|
# This allows stop_paqet_backend and stop_gfk_backend to remove their own rules independently
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
local vio_port="${GFK_VIO_PORT:-45000}"
|
|
iptables -D INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || true
|
|
iptables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || true
|
|
# Also try without comment for backwards compatibility
|
|
iptables -D INPUT -p tcp --dport "$vio_port" -j DROP 2>/dev/null || true
|
|
iptables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -D INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || true
|
|
ip6tables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || true
|
|
ip6tables -D INPUT -p tcp --dport "$vio_port" -j DROP 2>/dev/null || true
|
|
ip6tables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
local port="${LISTEN_PORT:-8443}"
|
|
# Remove tagged rules
|
|
iptables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
iptables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
iptables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
# Also try without comment for backwards compatibility with old rules
|
|
iptables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true
|
|
iptables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true
|
|
iptables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
# Remove ALL paqctl-tagged firewall rules (for complete uninstall)
|
|
_remove_all_paqctl_firewall_rules() {
|
|
command -v iptables &>/dev/null || return 0
|
|
local TAG="paqctl"
|
|
|
|
# Remove all rules with "paqctl" comment from all tables
|
|
# Loop to remove multiple rules if port was changed
|
|
local i
|
|
for i in {1..10}; do
|
|
iptables -t raw -S 2>/dev/null | grep -q "paqctl" || break
|
|
iptables -t raw -S 2>/dev/null | grep "paqctl" | while read -r rule; do
|
|
# Convert -A to -D for deletion
|
|
local del_rule="${rule/-A /-D }"
|
|
eval "iptables -t raw $del_rule" 2>/dev/null || true
|
|
done
|
|
done
|
|
|
|
for i in {1..10}; do
|
|
iptables -t mangle -S 2>/dev/null | grep -q "paqctl" || break
|
|
iptables -t mangle -S 2>/dev/null | grep "paqctl" | while read -r rule; do
|
|
local del_rule="${rule/-A /-D }"
|
|
eval "iptables -t mangle $del_rule" 2>/dev/null || true
|
|
done
|
|
done
|
|
|
|
for i in {1..10}; do
|
|
iptables -S 2>/dev/null | grep -q "paqctl" || break
|
|
iptables -S 2>/dev/null | grep "paqctl" | while read -r rule; do
|
|
local del_rule="${rule/-A /-D }"
|
|
eval "iptables $del_rule" 2>/dev/null || true
|
|
done
|
|
done
|
|
|
|
# Same for IPv6
|
|
if command -v ip6tables &>/dev/null; then
|
|
for i in {1..10}; do
|
|
ip6tables -t raw -S 2>/dev/null | grep -q "paqctl" || break
|
|
ip6tables -t raw -S 2>/dev/null | grep "paqctl" | while read -r rule; do
|
|
local del_rule="${rule/-A /-D }"
|
|
eval "ip6tables -t raw $del_rule" 2>/dev/null || true
|
|
done
|
|
done
|
|
|
|
for i in {1..10}; do
|
|
ip6tables -t mangle -S 2>/dev/null | grep -q "paqctl" || break
|
|
ip6tables -t mangle -S 2>/dev/null | grep "paqctl" | while read -r rule; do
|
|
local del_rule="${rule/-A /-D }"
|
|
eval "ip6tables -t mangle $del_rule" 2>/dev/null || true
|
|
done
|
|
done
|
|
|
|
for i in {1..10}; do
|
|
ip6tables -S 2>/dev/null | grep -q "paqctl" || break
|
|
ip6tables -S 2>/dev/null | grep "paqctl" | while read -r rule; do
|
|
local del_rule="${rule/-A /-D }"
|
|
eval "ip6tables $del_rule" 2>/dev/null || true
|
|
done
|
|
done
|
|
fi
|
|
}
|
|
|
|
_persist_firewall() {
|
|
if command -v netfilter-persistent &>/dev/null; then
|
|
netfilter-persistent save 2>/dev/null || true
|
|
elif command -v iptables-save &>/dev/null; then
|
|
if [ -d /etc/iptables ]; then
|
|
iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
|
|
command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true
|
|
elif [ -f /etc/debian_version ] && [ ! -d /etc/iptables ]; then
|
|
mkdir -p /etc/iptables
|
|
iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
|
|
command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true
|
|
elif [ -d /etc/sysconfig ]; then
|
|
iptables-save > /etc/sysconfig/iptables 2>/dev/null || true
|
|
command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/sysconfig/ip6tables 2>/dev/null || true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Status & Info
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
show_status() {
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} PAQCTL STATUS (${BACKEND})${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
# Running status
|
|
if is_running; then
|
|
echo -e " Status: ${GREEN}● Running${NC}"
|
|
# Uptime
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
local started
|
|
started=$(systemctl show paqctl.service --property=ActiveEnterTimestamp 2>/dev/null | cut -d= -f2)
|
|
if [ -n "$started" ]; then
|
|
local started_ts
|
|
started_ts=$(date -d "$started" +%s 2>/dev/null || echo 0)
|
|
if [ "$started_ts" -gt 0 ] 2>/dev/null; then
|
|
local now=$(date +%s)
|
|
local up=$((now - started_ts))
|
|
local days=$((up / 86400))
|
|
local hours=$(( (up % 86400) / 3600 ))
|
|
local mins=$(( (up % 3600) / 60 ))
|
|
if [ "$days" -gt 0 ]; then
|
|
echo -e " Uptime: ${days}d ${hours}h ${mins}m"
|
|
else
|
|
echo -e " Uptime: ${hours}h ${mins}m"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
# PID
|
|
local pid
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
pid=$(pgrep -f "mainserver.py|mainclient.py" 2>/dev/null | head -1)
|
|
else
|
|
pid=$(pgrep -f "paqet run -c" 2>/dev/null | head -1)
|
|
fi
|
|
[ -n "$pid" ] && echo -e " PID: $pid"
|
|
|
|
# CPU/RAM of process
|
|
if [ -n "$pid" ]; then
|
|
local cpu_mem
|
|
cpu_mem=$(ps -p "$pid" -o %cpu=,%mem= 2>/dev/null | head -1)
|
|
if [ -n "$cpu_mem" ]; then
|
|
local cpu=$(echo "$cpu_mem" | awk '{print $1}')
|
|
local mem=$(echo "$cpu_mem" | awk '{print $2}')
|
|
echo -e " CPU: ${cpu}%"
|
|
echo -e " Memory: ${mem}%"
|
|
fi
|
|
fi
|
|
else
|
|
echo -e " Status: ${RED}● Stopped${NC}"
|
|
fi
|
|
|
|
echo ""
|
|
echo -e " ${DIM}── Configuration ──${NC}"
|
|
echo -e " Backend: ${BOLD}${BACKEND}${NC}"
|
|
echo -e " Role: ${BOLD}${ROLE}${NC}"
|
|
echo -e " Version: ${PAQET_VERSION}"
|
|
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
echo -e " Server IP: ${GFK_SERVER_IP}"
|
|
echo -e " VIO port: ${GFK_VIO_PORT}"
|
|
echo -e " QUIC port: ${GFK_QUIC_PORT}"
|
|
echo -e " Mappings: ${GFK_PORT_MAPPINGS}"
|
|
if [ "$ROLE" = "server" ]; then
|
|
echo -e " Auth code: ${GFK_AUTH_CODE:0:8}..."
|
|
local _vio_port="${GFK_VIO_PORT:-45000}"
|
|
local _input_ok=false _rst_ok=false
|
|
if iptables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
iptables -C INPUT -p tcp --dport "$_vio_port" -j DROP 2>/dev/null; then
|
|
_input_ok=true
|
|
fi
|
|
if iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -j DROP 2>/dev/null; then
|
|
_rst_ok=true
|
|
fi
|
|
if [ "$_input_ok" = true ] && [ "$_rst_ok" = true ]; then
|
|
echo -e " Firewall: ${GREEN}VIO port blocked${NC}"
|
|
elif [ "$_input_ok" = true ]; then
|
|
echo -e " Firewall: ${YELLOW}Partial (RST DROP missing)${NC}"
|
|
else
|
|
echo -e " Firewall: ${RED}VIO port NOT blocked${NC}"
|
|
fi
|
|
else
|
|
echo -e " SOCKS5: 127.0.0.1:${MICROSOCKS_PORT:-1080}"
|
|
fi
|
|
else
|
|
echo -e " Interface: ${INTERFACE}"
|
|
echo -e " Local IP: ${LOCAL_IP}"
|
|
if [ "$ROLE" = "server" ]; then
|
|
echo -e " Port: ${LISTEN_PORT}"
|
|
echo -e " Key: ${ENCRYPTION_KEY:0:8}..."
|
|
if iptables -t raw -C PREROUTING -p tcp --dport "$LISTEN_PORT" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then
|
|
echo -e " Firewall: ${GREEN}Rules active${NC}"
|
|
else
|
|
echo -e " Firewall: ${RED}Rules missing${NC}"
|
|
fi
|
|
else
|
|
echo -e " Server: ${REMOTE_SERVER}"
|
|
echo -e " SOCKS port: ${SOCKS_PORT}"
|
|
echo -e " Key: ${ENCRYPTION_KEY:0:8}..."
|
|
fi
|
|
fi
|
|
|
|
# Telegram
|
|
if [ "$TELEGRAM_ENABLED" = "true" ]; then
|
|
echo -e " Telegram: ${GREEN}Enabled${NC}"
|
|
else
|
|
echo -e " Telegram: ${DIM}Disabled${NC}"
|
|
fi
|
|
|
|
echo ""
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Logs
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
show_logs() {
|
|
echo ""
|
|
log_info "Showing paqet logs (Ctrl+C to return to menu)..."
|
|
echo ""
|
|
|
|
# Trap Ctrl+C to return to menu instead of exiting
|
|
trap 'echo ""; log_info "Returning to menu..."; return 0' INT
|
|
|
|
if command -v journalctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
journalctl -u paqctl.service -f --no-pager -n 50
|
|
elif [ -f /var/log/paqctl.log ]; then
|
|
tail -f -n 50 /var/log/paqctl.log
|
|
else
|
|
log_warn "No logs found. Is paqet running?"
|
|
fi
|
|
|
|
# Restore default trap
|
|
trap - INT
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Health Check
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
health_check() {
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} HEALTH CHECK${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
local issues=0
|
|
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
# 1. Python scripts exist
|
|
if [ -f "$GFK_DIR/mainserver.py" ] && [ -f "$GFK_DIR/mainclient.py" ]; then
|
|
echo -e " ${GREEN}✓${NC} GFW-knocker scripts found"
|
|
else
|
|
echo -e " ${RED}✗${NC} GFW-knocker scripts missing from $GFK_DIR"
|
|
issues=$((issues + 1))
|
|
fi
|
|
|
|
# 2. Python + deps (check venv)
|
|
if [ -x "$INSTALL_DIR/venv/bin/python" ] && "$INSTALL_DIR/venv/bin/python" -c "import scapy; import aioquic" 2>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} Python dependencies OK (scapy, aioquic)"
|
|
else
|
|
echo -e " ${RED}✗${NC} Python dependencies missing (venv not setup)"
|
|
issues=$((issues + 1))
|
|
fi
|
|
|
|
# 3. Config
|
|
if [ -f "$GFK_DIR/parameters.py" ]; then
|
|
echo -e " ${GREEN}✓${NC} GFK configuration found"
|
|
else
|
|
echo -e " ${RED}✗${NC} GFK configuration missing"
|
|
issues=$((issues + 1))
|
|
fi
|
|
|
|
# 4. Certificates
|
|
if [ -f "$GFK_DIR/cert.pem" ] && [ -f "$GFK_DIR/key.pem" ]; then
|
|
echo -e " ${GREEN}✓${NC} QUIC certificates found"
|
|
else
|
|
echo -e " ${RED}✗${NC} QUIC certificates missing"
|
|
issues=$((issues + 1))
|
|
fi
|
|
|
|
# 5. Service running
|
|
if is_running; then
|
|
echo -e " ${GREEN}✓${NC} GFW-knocker is running"
|
|
else
|
|
echo -e " ${RED}✗${NC} GFW-knocker is not running"
|
|
issues=$((issues + 1))
|
|
fi
|
|
|
|
# 6. Firewall (server)
|
|
if [ "$ROLE" = "server" ]; then
|
|
# Check both tagged and untagged rules (tagged added by _apply_firewall, untagged by install wizard)
|
|
local _vio_port="${GFK_VIO_PORT:-45000}"
|
|
if iptables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
iptables -C INPUT -p tcp --dport "$_vio_port" -j DROP 2>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} VIO port ${_vio_port} INPUT blocked"
|
|
else
|
|
echo -e " ${RED}✗${NC} VIO port ${_vio_port} INPUT NOT blocked"
|
|
issues=$((issues + 1))
|
|
fi
|
|
# Check RST DROP rule (prevents kernel from sending RST packets)
|
|
if iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -j DROP 2>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} VIO port ${_vio_port} RST DROP in place"
|
|
else
|
|
echo -e " ${RED}✗${NC} VIO port ${_vio_port} RST DROP missing"
|
|
issues=$((issues + 1))
|
|
fi
|
|
fi
|
|
|
|
# 7. microsocks (client)
|
|
if [ "$ROLE" = "client" ]; then
|
|
if [ -x "$INSTALL_DIR/bin/microsocks" ]; then
|
|
echo -e " ${GREEN}✓${NC} microsocks binary found"
|
|
else
|
|
echo -e " ${RED}✗${NC} microsocks binary missing"
|
|
issues=$((issues + 1))
|
|
fi
|
|
if is_running && ss -tlnp 2>/dev/null | grep -q ":${MICROSOCKS_PORT:-1080}"; then
|
|
echo -e " ${GREEN}✓${NC} SOCKS5 port ${MICROSOCKS_PORT:-1080} is listening"
|
|
elif is_running; then
|
|
echo -e " ${RED}✗${NC} SOCKS5 port ${MICROSOCKS_PORT:-1080} not listening"
|
|
issues=$((issues + 1))
|
|
fi
|
|
fi
|
|
else
|
|
# 1. Binary exists
|
|
if [ -x "$INSTALL_DIR/bin/paqet" ]; then
|
|
echo -e " ${GREEN}✓${NC} paqet binary found"
|
|
else
|
|
echo -e " ${RED}✗${NC} paqet binary not found at $INSTALL_DIR/bin/paqet"
|
|
issues=$((issues + 1))
|
|
fi
|
|
|
|
# 2. Config exists
|
|
if [ -f "$INSTALL_DIR/config.yaml" ]; then
|
|
echo -e " ${GREEN}✓${NC} Configuration file found"
|
|
else
|
|
echo -e " ${RED}✗${NC} Configuration file missing"
|
|
issues=$((issues + 1))
|
|
fi
|
|
|
|
# 3. Service running
|
|
if is_running; then
|
|
echo -e " ${GREEN}✓${NC} paqet is running"
|
|
else
|
|
echo -e " ${RED}✗${NC} paqet is not running"
|
|
issues=$((issues + 1))
|
|
fi
|
|
|
|
# 4. libpcap
|
|
if ldconfig -p 2>/dev/null | grep -q libpcap; then
|
|
echo -e " ${GREEN}✓${NC} libpcap is available"
|
|
else
|
|
echo -e " ${YELLOW}!${NC} libpcap not found in ldconfig (may still work)"
|
|
fi
|
|
|
|
# 5. iptables (server only)
|
|
if [ "$ROLE" = "server" ]; then
|
|
if iptables -t raw -C PREROUTING -p tcp --dport "$LISTEN_PORT" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} iptables NOTRACK rules in place (port $LISTEN_PORT)"
|
|
else
|
|
echo -e " ${RED}✗${NC} iptables NOTRACK rules missing for port $LISTEN_PORT"
|
|
issues=$((issues + 1))
|
|
fi
|
|
|
|
if iptables -t mangle -C OUTPUT -p tcp --sport "$LISTEN_PORT" -m comment --comment "paqctl" --tcp-flags RST RST -j DROP 2>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} iptables RST DROP rule in place"
|
|
else
|
|
echo -e " ${RED}✗${NC} iptables RST DROP rule missing"
|
|
issues=$((issues + 1))
|
|
fi
|
|
fi
|
|
|
|
# 6. Port listening (server) or connectivity (client)
|
|
if [ "$ROLE" = "server" ] && is_running; then
|
|
if ss -tlnp 2>/dev/null | grep -q ":${LISTEN_PORT}"; then
|
|
echo -e " ${GREEN}✓${NC} Port $LISTEN_PORT is listening"
|
|
else
|
|
echo -e " ${YELLOW}!${NC} Port $LISTEN_PORT not shown in ss (paqet uses raw sockets)"
|
|
fi
|
|
fi
|
|
|
|
if [ "$ROLE" = "client" ] && is_running; then
|
|
if ss -tlnp 2>/dev/null | grep -q ":${SOCKS_PORT}"; then
|
|
echo -e " ${GREEN}✓${NC} SOCKS5 port $SOCKS_PORT is listening"
|
|
else
|
|
echo -e " ${RED}✗${NC} SOCKS5 port $SOCKS_PORT is not listening"
|
|
issues=$((issues + 1))
|
|
fi
|
|
fi
|
|
|
|
# 7. Paqet ping test
|
|
if is_running && [ -x "$INSTALL_DIR/bin/paqet" ]; then
|
|
echo -e " ${DIM}Running paqet ping test...${NC}"
|
|
local ping_result
|
|
ping_result=$(timeout 10 "$INSTALL_DIR/bin/paqet" ping -c "$INSTALL_DIR/config.yaml" 2>&1 || true)
|
|
if echo "$ping_result" | grep -qi "success\|pong\|ok\|alive\|rtt"; then
|
|
echo -e " ${GREEN}✓${NC} Paqet ping: OK"
|
|
elif [ -n "$ping_result" ]; then
|
|
echo -e " ${YELLOW}!${NC} Paqet ping: $(echo "$ping_result" | head -1)"
|
|
else
|
|
echo -e " ${YELLOW}!${NC} Paqet ping: no response (may not be supported)"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# 8. Network connectivity
|
|
if curl -s --max-time 5 https://api.github.com &>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} Internet connectivity: OK"
|
|
else
|
|
echo -e " ${YELLOW}!${NC} Cannot reach GitHub API (may be firewall/network)"
|
|
fi
|
|
|
|
# 9. Systemd service
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
if systemctl is-enabled paqctl.service &>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} Auto-start on boot: enabled"
|
|
else
|
|
echo -e " ${YELLOW}!${NC} Auto-start on boot: disabled"
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
if [ "$issues" -eq 0 ]; then
|
|
echo -e " ${GREEN}${BOLD}All checks passed!${NC}"
|
|
else
|
|
echo -e " ${RED}${BOLD}$issues issue(s) found${NC}"
|
|
fi
|
|
echo ""
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Update
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
update_gfk() {
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} UPDATE GFW-KNOCKER${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
log_info "Downloading latest GFW-knocker scripts..."
|
|
local tmp_dir
|
|
tmp_dir=$(mktemp -d)
|
|
local server_files="mainserver.py quic_server.py vio_server.py"
|
|
local client_files="mainclient.py quic_client.py vio_client.py"
|
|
local f changed=false
|
|
# Download server scripts
|
|
for f in $server_files; do
|
|
if ! curl -sL "$GFK_RAW_URL/server/$f" -o "$tmp_dir/$f"; then
|
|
log_error "Failed to download $f"
|
|
rm -rf "$tmp_dir"
|
|
return 1
|
|
fi
|
|
if ! diff -q "$tmp_dir/$f" "$GFK_DIR/$f" &>/dev/null; then
|
|
changed=true
|
|
fi
|
|
done
|
|
# Download client scripts
|
|
for f in $client_files; do
|
|
if ! curl -sL "$GFK_RAW_URL/client/$f" -o "$tmp_dir/$f"; then
|
|
log_error "Failed to download $f"
|
|
rm -rf "$tmp_dir"
|
|
return 1
|
|
fi
|
|
if ! diff -q "$tmp_dir/$f" "$GFK_DIR/$f" &>/dev/null; then
|
|
changed=true
|
|
fi
|
|
done
|
|
|
|
if [ "$changed" = true ]; then
|
|
local was_running=false
|
|
is_running && was_running=true
|
|
[ "$was_running" = true ] && stop_paqet
|
|
|
|
# Backup old scripts
|
|
mkdir -p "$BACKUP_DIR"
|
|
local all_files="$server_files $client_files"
|
|
for f in $all_files; do
|
|
[ -f "$GFK_DIR/$f" ] && cp "$GFK_DIR/$f" "$BACKUP_DIR/${f}.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
|
|
done
|
|
|
|
for f in $all_files; do
|
|
cp "$tmp_dir/$f" "$GFK_DIR/$f"
|
|
done
|
|
chmod 600 "$GFK_DIR"/*.py
|
|
# Patch mainserver.py to use venv python for subprocesses
|
|
[ -f "$GFK_DIR/mainserver.py" ] && sed -i "s|'python3'|'$INSTALL_DIR/venv/bin/python'|g" "$GFK_DIR/mainserver.py"
|
|
log_success "GFW-knocker scripts updated"
|
|
|
|
# Also upgrade Python deps in venv
|
|
"$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade scapy aioquic 2>/dev/null || true
|
|
|
|
[ "$was_running" = true ] && start_paqet
|
|
else
|
|
log_success "GFW-knocker scripts are already up to date"
|
|
"$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade scapy aioquic 2>/dev/null || true
|
|
fi
|
|
rm -rf "$tmp_dir"
|
|
echo ""
|
|
}
|
|
|
|
update_paqet() {
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
update_gfk
|
|
return
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} UPDATE PAQET${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
log_info "Querying GitHub for latest release..."
|
|
|
|
# Get latest version from GitHub with retry
|
|
local response
|
|
response=$(_curl_with_retry "$PAQET_API_URL" 3)
|
|
if [ -z "$response" ]; then
|
|
log_error "Failed to query GitHub API after retries. Check your internet connection."
|
|
return 1
|
|
fi
|
|
|
|
local latest_tag
|
|
latest_tag=$(echo "$response" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"')
|
|
if [ -z "$latest_tag" ] || ! _validate_version_tag "$latest_tag"; then
|
|
log_error "Could not determine valid version from GitHub"
|
|
return 1
|
|
fi
|
|
|
|
# Extract release date
|
|
local release_date
|
|
release_date=$(echo "$response" | grep -o '"published_at"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"' | cut -dT -f1)
|
|
|
|
# Extract release notes (body field)
|
|
local release_notes=""
|
|
if command -v python3 &>/dev/null; then
|
|
release_notes=$(python3 -c "
|
|
import json,sys
|
|
try:
|
|
d=json.loads(sys.stdin.read())
|
|
body=d.get('body','')
|
|
if body:
|
|
# Truncate to first 500 chars, strip markdown
|
|
body=body[:500].replace('**','').replace('##','').replace('# ','')
|
|
print(body)
|
|
except: pass
|
|
" <<< "$response" 2>/dev/null)
|
|
fi
|
|
|
|
local current="${PAQET_VERSION:-unknown}"
|
|
local bin_ver
|
|
bin_ver=$("$INSTALL_DIR/bin/paqet" version 2>/dev/null || echo "unknown")
|
|
|
|
echo ""
|
|
echo -e " ${DIM}── Version Info ──${NC}"
|
|
echo -e " Installed version: ${BOLD}${current}${NC}"
|
|
echo -e " Binary reports: ${BOLD}${bin_ver}${NC}"
|
|
echo -e " Latest release: ${BOLD}${latest_tag}${NC}"
|
|
[ -n "$release_date" ] && echo -e " Release date: ${release_date}"
|
|
|
|
if [ "$current" = "$latest_tag" ]; then
|
|
echo ""
|
|
log_success "You are already on the latest version!"
|
|
echo ""
|
|
echo -e " ${DIM}Options:${NC}"
|
|
echo " 1. Force reinstall current version"
|
|
echo " 2. Rollback to previous version"
|
|
echo " 3. Update management script only"
|
|
echo " b. Back"
|
|
echo ""
|
|
read -p " Choice: " up_choice < /dev/tty || true
|
|
case "$up_choice" in
|
|
1)
|
|
read -p " Force reinstall ${current}? [y/N]: " _fc < /dev/tty || true
|
|
[[ "$_fc" =~ ^[Yy]$ ]] || { log_info "Cancelled"; return 0; }
|
|
;;
|
|
2) rollback_paqet; return ;;
|
|
3) update_management_script; return ;;
|
|
[bB]) return 0 ;;
|
|
*) return 0 ;;
|
|
esac
|
|
fi
|
|
|
|
# Show release notes if available
|
|
if [ -n "$release_notes" ]; then
|
|
echo ""
|
|
echo -e " ${DIM}── Release Notes ──${NC}"
|
|
echo "$release_notes" | while IFS= read -r line; do
|
|
echo -e " ${DIM}${line}${NC}"
|
|
done
|
|
echo ""
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${YELLOW}╔════════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${YELLOW}║ ${BOLD}⚠ WARNING: Updating may cause compatibility issues!${NC}${YELLOW} ║${NC}"
|
|
echo -e "${YELLOW}╠════════════════════════════════════════════════════════════════╣${NC}"
|
|
echo -e "${YELLOW}║${NC} paqctl was tested with: ${BOLD}${PAQET_VERSION_PINNED}${NC}"
|
|
echo -e "${YELLOW}║${NC} Newer versions may have breaking changes or bugs."
|
|
echo -e "${YELLOW}║${NC} You can rollback with: ${BOLD}sudo paqctl rollback${NC}"
|
|
echo -e "${YELLOW}╚════════════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
read -p " Update to ${latest_tag}? [y/N]: " confirm < /dev/tty || true
|
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
|
log_info "Update cancelled"
|
|
return 0
|
|
fi
|
|
|
|
# Download new binary
|
|
_download_and_install_binary "$latest_tag" || return 1
|
|
|
|
# Check for management script update
|
|
update_management_script
|
|
}
|
|
|
|
_download_and_install_binary() {
|
|
local target_tag="$1"
|
|
local arch
|
|
arch=$(uname -m)
|
|
case "$arch" in
|
|
x86_64|amd64) arch="amd64" ;;
|
|
aarch64|arm64) arch="arm64" ;;
|
|
*) log_error "Unsupported architecture: $arch"; return 1 ;;
|
|
esac
|
|
|
|
local filename="paqet-linux-${arch}-${target_tag}.tar.gz"
|
|
local url="https://github.com/${PAQET_REPO}/releases/download/${target_tag}/${filename}"
|
|
local tmp_file
|
|
tmp_file=$(mktemp "/tmp/paqet-update-XXXXXXXX.tar.gz")
|
|
|
|
log_info "Downloading ${filename}..."
|
|
if ! curl -sL --max-time 120 --fail -o "$tmp_file" "$url"; then
|
|
log_error "Download failed: $url"
|
|
rm -f "$tmp_file"
|
|
return 1
|
|
fi
|
|
|
|
# Validate
|
|
local fsize
|
|
fsize=$(stat -c%s "$tmp_file" 2>/dev/null || stat -f%z "$tmp_file" 2>/dev/null || wc -c < "$tmp_file" 2>/dev/null || echo 0)
|
|
if [ "$fsize" -lt 1000 ]; then
|
|
log_error "Downloaded file too small ($fsize bytes). Aborting."
|
|
rm -f "$tmp_file"
|
|
return 1
|
|
fi
|
|
|
|
# Extract
|
|
local tmp_extract
|
|
tmp_extract=$(mktemp -d "/tmp/paqet-update-extract-XXXXXXXX")
|
|
if ! tar -xzf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then
|
|
log_error "Failed to extract archive"
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
return 1
|
|
fi
|
|
|
|
local binary_name="paqet_linux_${arch}"
|
|
local found_binary
|
|
found_binary=$(find "$tmp_extract" -name "$binary_name" -type f 2>/dev/null | head -1)
|
|
[ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f -executable 2>/dev/null | head -1)
|
|
[ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f 2>/dev/null | head -1)
|
|
|
|
if [ -z "$found_binary" ]; then
|
|
log_error "Could not find paqet binary in archive"
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
return 1
|
|
fi
|
|
|
|
# Stop service, replace, start
|
|
local was_running=false
|
|
if is_running; then
|
|
was_running=true
|
|
stop_paqet
|
|
fi
|
|
|
|
# Backup old binary with version tag for rollback
|
|
if ! mkdir -p "$BACKUP_DIR"; then
|
|
log_warn "Failed to create backup directory"
|
|
fi
|
|
local old_ver="${PAQET_VERSION:-unknown}"
|
|
cp "$INSTALL_DIR/bin/paqet" "$BACKUP_DIR/paqet.${old_ver}.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
|
|
|
|
if ! cp "$found_binary" "$INSTALL_DIR/bin/paqet"; then
|
|
log_error "Failed to copy new binary"
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
# Restore from backup
|
|
local latest_backup
|
|
latest_backup=$(ls -t "$BACKUP_DIR"/paqet.* 2>/dev/null | head -1)
|
|
[ -n "$latest_backup" ] && cp "$latest_backup" "$INSTALL_DIR/bin/paqet" && chmod +x "$INSTALL_DIR/bin/paqet"
|
|
[ "$was_running" = true ] && start_paqet
|
|
return 1
|
|
fi
|
|
chmod +x "$INSTALL_DIR/bin/paqet"
|
|
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
|
|
# Verify the new binary works
|
|
if ! "$INSTALL_DIR/bin/paqet" version &>/dev/null; then
|
|
log_warn "New binary failed verification. Restoring backup..."
|
|
local latest_backup
|
|
latest_backup=$(ls -t "$BACKUP_DIR"/paqet.* 2>/dev/null | head -1)
|
|
if [ -n "$latest_backup" ]; then
|
|
cp "$latest_backup" "$INSTALL_DIR/bin/paqet"
|
|
chmod +x "$INSTALL_DIR/bin/paqet"
|
|
log_error "Update failed — previous version restored"
|
|
return 1
|
|
fi
|
|
log_error "Update failed and no backup available"
|
|
return 1
|
|
fi
|
|
|
|
# Update version in settings
|
|
PAQET_VERSION="$target_tag"
|
|
_safe_update_setting "PAQET_VERSION" "$target_tag" "$INSTALL_DIR/settings.conf"
|
|
|
|
log_success "paqet updated to ${target_tag}"
|
|
|
|
if [ "$was_running" = true ]; then
|
|
start_paqet
|
|
fi
|
|
}
|
|
|
|
rollback_paqet() {
|
|
echo ""
|
|
if [ ! -d "$BACKUP_DIR" ]; then
|
|
log_warn "No backups found"
|
|
return 1
|
|
fi
|
|
|
|
local backups=()
|
|
local i=1
|
|
echo -e " ${BOLD}Available binary backups:${NC}"
|
|
echo ""
|
|
for f in "$BACKUP_DIR"/paqet.*; do
|
|
[ -f "$f" ] || continue
|
|
backups+=("$f")
|
|
local bname=$(basename "$f")
|
|
local bsize=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null || wc -c < "$f" 2>/dev/null || echo "?")
|
|
echo " $i. $bname (${bsize} bytes)"
|
|
i=$((i + 1))
|
|
done
|
|
|
|
if [ ${#backups[@]} -eq 0 ]; then
|
|
log_warn "No binary backups found in $BACKUP_DIR"
|
|
return 1
|
|
fi
|
|
|
|
echo ""
|
|
echo " 0. Cancel"
|
|
echo ""
|
|
read -p " Select backup to restore [0-${#backups[@]}]: " choice < /dev/tty || true
|
|
if [ "$choice" = "0" ]; then
|
|
log_info "Cancelled"
|
|
return 0
|
|
fi
|
|
if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#backups[@]} ]; then
|
|
log_error "Invalid choice"
|
|
return 1
|
|
fi
|
|
|
|
local selected="${backups[$((choice-1))]}"
|
|
log_info "Rolling back to: $(basename "$selected")"
|
|
|
|
local was_running=false
|
|
is_running && was_running=true
|
|
[ "$was_running" = true ] && stop_paqet
|
|
|
|
if ! cp "$selected" "$INSTALL_DIR/bin/paqet"; then
|
|
log_error "Failed to restore backup"
|
|
[ "$was_running" = true ] && start_paqet
|
|
return 1
|
|
fi
|
|
chmod +x "$INSTALL_DIR/bin/paqet"
|
|
|
|
# Verify restored binary
|
|
if ! "$INSTALL_DIR/bin/paqet" version &>/dev/null; then
|
|
log_warn "Restored binary failed verification (may need libpcap)"
|
|
fi
|
|
|
|
# Try to extract version from the filename (format: paqet.vX.Y.Z.TIMESTAMP)
|
|
local restored_ver=""
|
|
local _bname
|
|
_bname=$(basename "$selected")
|
|
# Extract version: remove 'paqet.' prefix and '.YYYYMMDDHHMMSS' timestamp suffix
|
|
restored_ver=$(echo "$_bname" | sed 's/^paqet\.//' | sed 's/\.[0-9]\{14\}$//')
|
|
# Validate extracted version looks reasonable
|
|
if [ -n "$restored_ver" ] && [ "$restored_ver" != "backup" ] && [ "$restored_ver" != "$_bname" ]; then
|
|
if _validate_version_tag "$restored_ver"; then
|
|
PAQET_VERSION="$restored_ver"
|
|
_safe_update_setting "PAQET_VERSION" "$restored_ver" "$INSTALL_DIR/settings.conf"
|
|
log_info "Restored version: $restored_ver"
|
|
else
|
|
log_warn "Could not determine version from backup filename, keeping current version setting"
|
|
fi
|
|
else
|
|
log_warn "Could not extract version from backup filename"
|
|
fi
|
|
|
|
log_success "Rolled back successfully"
|
|
|
|
[ "$was_running" = true ] && start_paqet
|
|
}
|
|
|
|
update_management_script() {
|
|
local update_url="https://raw.githubusercontent.com/SamNet-dev/paqctl/main/paqctl.sh"
|
|
local tmp_script
|
|
tmp_script=$(mktemp "/tmp/paqctl-update-XXXXXXXX.sh")
|
|
|
|
log_info "Checking for management script updates..."
|
|
if ! curl -sL --max-time 30 --max-filesize 2097152 -o "$tmp_script" "$update_url" 2>/dev/null; then
|
|
log_warn "Could not check for script updates"
|
|
rm -f "$tmp_script"
|
|
return 0
|
|
fi
|
|
|
|
# Validate: must contain our markers, be a bash script, and pass syntax check
|
|
if ! head -n 1 "$tmp_script" 2>/dev/null | grep -q "^#!.*bash"; then
|
|
log_warn "Downloaded file is not a bash script, skipping"
|
|
rm -f "$tmp_script"
|
|
return 0
|
|
fi
|
|
if grep -q "PAQET_REPO=" "$tmp_script" && \
|
|
grep -q "create_management_script" "$tmp_script" && \
|
|
grep -q "PAQCTL_VERSION=" "$tmp_script" && \
|
|
bash -n "$tmp_script" 2>/dev/null; then
|
|
local _update_output
|
|
if _update_output=$(bash "$tmp_script" --update-components 2>&1); then
|
|
log_success "Management script updated"
|
|
else
|
|
log_warn "Management script update execution failed: ${_update_output:-unknown error}"
|
|
fi
|
|
else
|
|
log_warn "Downloaded script failed validation, skipping"
|
|
fi
|
|
rm -f "$tmp_script"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Secret Key Generation
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
generate_secret() {
|
|
echo ""
|
|
local key
|
|
key=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true)
|
|
if [ -z "$key" ]; then
|
|
key=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32)
|
|
fi
|
|
echo -e " ${GREEN}${BOLD}New encryption key: ${key}${NC}"
|
|
echo ""
|
|
echo -e " ${DIM}Share this key securely with client users.${NC}"
|
|
echo ""
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Firewall Display
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
show_firewall() {
|
|
if [ "$ROLE" != "server" ]; then
|
|
echo ""
|
|
log_info "Firewall rules only apply in server mode"
|
|
echo ""
|
|
return
|
|
fi
|
|
|
|
local redraw=true
|
|
while true; do
|
|
if [ "$redraw" = true ]; then
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} FIREWALL RULES${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
local port="${LISTEN_PORT:-8443}"
|
|
|
|
echo -e " ${BOLD}Required rules for port ${port}:${NC}"
|
|
echo ""
|
|
|
|
if iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} PREROUTING NOTRACK (dport $port)"
|
|
else
|
|
echo -e " ${RED}✗${NC} PREROUTING NOTRACK (dport $port) ${DIM}MISSING${NC}"
|
|
fi
|
|
|
|
if iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} OUTPUT NOTRACK (sport $port)"
|
|
else
|
|
echo -e " ${RED}✗${NC} OUTPUT NOTRACK (sport $port) ${DIM}MISSING${NC}"
|
|
fi
|
|
|
|
if iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "paqctl" --tcp-flags RST RST -j DROP 2>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} RST DROP (sport $port)"
|
|
else
|
|
echo -e " ${RED}✗${NC} RST DROP (sport $port) ${DIM}MISSING${NC}"
|
|
fi
|
|
|
|
echo ""
|
|
echo -e " ${BOLD}Actions:${NC}"
|
|
echo " 1. Apply missing rules"
|
|
echo " 2. Remove all rules"
|
|
echo " b. Back"
|
|
echo ""
|
|
redraw=false
|
|
fi
|
|
|
|
read -p " Choice: " fw_choice < /dev/tty || break
|
|
case "$fw_choice" in
|
|
1)
|
|
_apply_firewall
|
|
_persist_firewall
|
|
log_success "Firewall rules applied and persisted"
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
redraw=true
|
|
;;
|
|
2)
|
|
_remove_firewall
|
|
_persist_firewall
|
|
log_success "Firewall rules removed"
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
redraw=true
|
|
;;
|
|
b|B) return ;;
|
|
"") ;;
|
|
*) echo -e " ${RED}Invalid choice${NC}" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Configuration
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
_change_config_gfk() {
|
|
local was_running="$1"
|
|
echo ""
|
|
echo -e "${BOLD}Select role:${NC}"
|
|
echo " 1. Server"
|
|
echo " 2. Client"
|
|
echo ""
|
|
local role_choice
|
|
read -p " Enter choice [1/2]: " role_choice < /dev/tty || true
|
|
case "$role_choice" in
|
|
1) ROLE="server" ;;
|
|
2) ROLE="client" ;;
|
|
*) log_warn "Invalid. Keeping current role: $ROLE" ;;
|
|
esac
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
echo -e "${BOLD}Server public IP${NC} [${GFK_SERVER_IP}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_ip "$input"; then
|
|
log_error "Invalid IP address"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_SERVER_IP="$input"
|
|
|
|
echo -e "${BOLD}VIO TCP port${NC} [${GFK_VIO_PORT:-45000}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_VIO_PORT="$input"
|
|
|
|
echo -e "${BOLD}QUIC port${NC} [${GFK_QUIC_PORT:-25000}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_QUIC_PORT="$input"
|
|
|
|
echo -e "${BOLD}Auth code${NC} [keep current]:"
|
|
read -p " Code: " input < /dev/tty || true
|
|
[ -n "$input" ] && GFK_AUTH_CODE="$input"
|
|
|
|
echo -e "${BOLD}Port mappings${NC} [${GFK_PORT_MAPPINGS:-14000:443}]:"
|
|
read -p " Mappings: " input < /dev/tty || true
|
|
[ -n "$input" ] && GFK_PORT_MAPPINGS="$input"
|
|
else
|
|
echo -e "${BOLD}Server IP${NC} [${GFK_SERVER_IP}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_ip "$input"; then
|
|
log_error "Invalid IP address"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_SERVER_IP="$input"
|
|
|
|
echo -e "${BOLD}Server's VIO TCP port${NC} [${GFK_VIO_PORT:-45000}] (must match server):"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_VIO_PORT="$input"
|
|
|
|
echo -e "${BOLD}Local VIO client port${NC} [${GFK_VIO_CLIENT_PORT:-40000}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_VIO_CLIENT_PORT="$input"
|
|
|
|
echo -e "${BOLD}Server's QUIC port${NC} [${GFK_QUIC_PORT:-25000}] (must match server):"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_QUIC_PORT="$input"
|
|
|
|
echo -e "${BOLD}Local QUIC client port${NC} [${GFK_QUIC_CLIENT_PORT:-20000}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_QUIC_CLIENT_PORT="$input"
|
|
|
|
echo -e "${BOLD}Auth code${NC}:"
|
|
read -p " Code: " input < /dev/tty || true
|
|
[ -n "$input" ] && GFK_AUTH_CODE="$input"
|
|
|
|
echo -e "${BOLD}Port mappings${NC} [${GFK_PORT_MAPPINGS:-14000:443}]:"
|
|
read -p " Mappings: " input < /dev/tty || true
|
|
[ -n "$input" ] && GFK_PORT_MAPPINGS="$input"
|
|
|
|
echo -e "${BOLD}SOCKS5 port${NC} [${MICROSOCKS_PORT:-1080}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && MICROSOCKS_PORT="$input"
|
|
SOCKS_PORT="${MICROSOCKS_PORT:-1080}"
|
|
fi
|
|
|
|
# Regenerate parameters.py
|
|
generate_gfk_config || { [ "$was_running" = true ] && start_paqet; return 1; }
|
|
|
|
# Regenerate wrapper if client
|
|
if [ "$ROLE" = "client" ]; then
|
|
create_gfk_client_wrapper
|
|
fi
|
|
|
|
# Save settings
|
|
local IFACE="" GW_MAC=""
|
|
save_settings
|
|
|
|
# Re-apply firewall
|
|
_apply_firewall
|
|
|
|
# Restart
|
|
[ "$was_running" = true ] && start_paqet
|
|
log_success "GFW-knocker configuration updated"
|
|
}
|
|
|
|
change_config() {
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} CHANGE CONFIGURATION${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
local _warn_text="config"
|
|
[ "$BACKEND" = "gfw-knocker" ] && _warn_text="parameters.py"
|
|
echo -e " ${YELLOW}Warning: This will regenerate ${_warn_text} and restart ${BACKEND}.${NC}"
|
|
echo ""
|
|
read -p " Continue? [y/N]: " confirm < /dev/tty || true
|
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
|
return 0
|
|
fi
|
|
|
|
local was_running=false
|
|
is_running && was_running=true
|
|
[ "$was_running" = true ] && stop_paqet
|
|
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
_remove_firewall
|
|
_change_config_gfk "$was_running"
|
|
return
|
|
fi
|
|
|
|
# Remove old firewall rules (save old port before user changes it)
|
|
local _saved_port="$LISTEN_PORT"
|
|
if [ "$ROLE" = "server" ] && [ -n "$_saved_port" ]; then
|
|
_remove_firewall
|
|
fi
|
|
|
|
# Re-run wizard (inline version)
|
|
echo ""
|
|
echo -e "${BOLD}Select role:${NC}"
|
|
echo " 1. Server"
|
|
echo " 2. Client"
|
|
echo ""
|
|
local role_choice
|
|
read -p " Enter choice [1/2]: " role_choice < /dev/tty || true
|
|
case "$role_choice" in
|
|
1) ROLE="server" ;;
|
|
2) ROLE="client" ;;
|
|
*)
|
|
log_warn "Invalid choice. Defaulting to server."
|
|
ROLE="server"
|
|
;;
|
|
esac
|
|
|
|
# Detect network
|
|
local _iface=$(ip route show default 2>/dev/null | awk '{print $5; exit}')
|
|
local _ip=$(ip -4 addr show "$_iface" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | grep -o '[0-9.]*' | head -1)
|
|
local _gw=$(ip route show default 2>/dev/null | awk '{print $3; exit}')
|
|
local _gw_mac=""
|
|
[ -n "$_gw" ] && _gw_mac=$(ip neigh show "$_gw" 2>/dev/null | awk '/lladdr/{print $5; exit}')
|
|
|
|
echo ""
|
|
echo -e "${BOLD}Interface${NC} [${_iface:-$INTERFACE}]:"
|
|
read -p " Interface: " input < /dev/tty || true
|
|
INTERFACE="${input:-${_iface:-$INTERFACE}}"
|
|
|
|
echo -e "${BOLD}Local IP${NC} [${_ip:-$LOCAL_IP}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
LOCAL_IP="${input:-${_ip:-$LOCAL_IP}}"
|
|
|
|
echo -e "${BOLD}Gateway MAC${NC} [${_gw_mac:-$GATEWAY_MAC}]:"
|
|
read -p " MAC: " input < /dev/tty || true
|
|
GATEWAY_MAC="${input:-${_gw_mac:-$GATEWAY_MAC}}"
|
|
if [ -n "$GATEWAY_MAC" ] && ! _validate_mac "$GATEWAY_MAC"; then
|
|
log_warn "Invalid MAC address format (expected: aa:bb:cc:dd:ee:ff)"
|
|
read -p " Enter valid MAC address: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_mac "$input"; then
|
|
log_warn "Invalid MAC format, keeping current value"
|
|
input=""
|
|
fi
|
|
[ -n "$input" ] && GATEWAY_MAC="$input"
|
|
fi
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
echo -e "${BOLD}Port${NC} [${LISTEN_PORT:-8443}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
LISTEN_PORT="${input:-${LISTEN_PORT:-8443}}"
|
|
if ! _validate_port "$LISTEN_PORT"; then
|
|
log_warn "Invalid port. Using default 8443."
|
|
LISTEN_PORT=8443
|
|
fi
|
|
|
|
echo -e "${BOLD}Encryption key${NC} [keep current]:"
|
|
read -p " Key (enter to keep): " input < /dev/tty || true
|
|
[ -n "$input" ] && ENCRYPTION_KEY="$input"
|
|
REMOTE_SERVER=""
|
|
SOCKS_PORT=""
|
|
else
|
|
echo -e "${BOLD}Remote server${NC} (IP:PORT):"
|
|
read -p " Server: " input < /dev/tty || true
|
|
REMOTE_SERVER="${input:-$REMOTE_SERVER}"
|
|
|
|
echo -e "${BOLD}Encryption key${NC}:"
|
|
read -p " Key: " input < /dev/tty || true
|
|
[ -n "$input" ] && ENCRYPTION_KEY="$input"
|
|
|
|
echo -e "${BOLD}SOCKS5 port${NC} [${SOCKS_PORT:-1080}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
SOCKS_PORT="${input:-${SOCKS_PORT:-1080}}"
|
|
LISTEN_PORT=""
|
|
fi
|
|
|
|
# Save
|
|
local IFACE="$INTERFACE"
|
|
local GW_MAC="$GATEWAY_MAC"
|
|
# Regenerate YAML
|
|
local tmp_conf
|
|
tmp_conf=$(mktemp "$INSTALL_DIR/config.yaml.XXXXXXXX")
|
|
# Validate required fields
|
|
if [ -z "$INTERFACE" ] || [ -z "$LOCAL_IP" ] || [ -z "$GATEWAY_MAC" ] || [ -z "$ENCRYPTION_KEY" ]; then
|
|
log_error "Missing required configuration fields"
|
|
rm -f "$tmp_conf"
|
|
[ "$was_running" = true ] && start_paqet
|
|
return 1
|
|
fi
|
|
|
|
# Escape YAML special characters to prevent injection
|
|
_escape_yaml() {
|
|
local s="$1"
|
|
if [[ "$s" =~ [:\#\[\]\{\}\"\'\|\>\<\&\*\!\%\@\`] ]] || [[ "$s" =~ ^[[:space:]] ]] || [[ "$s" =~ [[:space:]]$ ]]; then
|
|
s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; printf '"%s"' "$s"
|
|
else
|
|
printf '%s' "$s"
|
|
fi
|
|
}
|
|
# Set permissions before writing
|
|
chmod 600 "$tmp_conf" 2>/dev/null
|
|
(
|
|
umask 077
|
|
local _y_iface _y_ip _y_mac _y_key _y_server
|
|
_y_iface=$(_escape_yaml "$INTERFACE")
|
|
_y_ip=$(_escape_yaml "$LOCAL_IP")
|
|
_y_mac=$(_escape_yaml "$GATEWAY_MAC")
|
|
_y_key=$(_escape_yaml "$ENCRYPTION_KEY")
|
|
if [ "$ROLE" = "server" ]; then
|
|
cat > "$tmp_conf" << EOF
|
|
role: "server"
|
|
|
|
log:
|
|
level: "info"
|
|
|
|
listen:
|
|
addr: ":${LISTEN_PORT}"
|
|
|
|
network:
|
|
interface: "${_y_iface}"
|
|
ipv4:
|
|
addr: "${_y_ip}:${LISTEN_PORT}"
|
|
router_mac: "${_y_mac}"
|
|
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_y_key}"
|
|
EOF
|
|
else
|
|
_y_server=$(_escape_yaml "$REMOTE_SERVER")
|
|
cat > "$tmp_conf" << EOF
|
|
role: "client"
|
|
|
|
log:
|
|
level: "info"
|
|
|
|
socks5:
|
|
- listen: "127.0.0.1:${SOCKS_PORT}"
|
|
|
|
network:
|
|
interface: "${_y_iface}"
|
|
ipv4:
|
|
addr: "${_y_ip}:0"
|
|
router_mac: "${_y_mac}"
|
|
|
|
server:
|
|
addr: "${_y_server}"
|
|
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_y_key}"
|
|
EOF
|
|
fi
|
|
)
|
|
if ! mv "$tmp_conf" "$INSTALL_DIR/config.yaml"; then
|
|
log_error "Failed to save configuration"
|
|
rm -f "$tmp_conf"
|
|
[ "$was_running" = true ] && start_paqet
|
|
return 1
|
|
fi
|
|
chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null
|
|
|
|
# Save settings
|
|
local _tmp
|
|
_tmp=$(mktemp "$INSTALL_DIR/settings.conf.XXXXXXXX")
|
|
# Read current telegram settings
|
|
local _tg_token="${TELEGRAM_BOT_TOKEN:-}"
|
|
local _tg_chat="${TELEGRAM_CHAT_ID:-}"
|
|
local _tg_interval="${TELEGRAM_INTERVAL:-6}"
|
|
local _tg_enabled="${TELEGRAM_ENABLED:-false}"
|
|
local _tg_alerts="${TELEGRAM_ALERTS_ENABLED:-true}"
|
|
local _tg_daily="${TELEGRAM_DAILY_SUMMARY:-true}"
|
|
local _tg_weekly="${TELEGRAM_WEEKLY_SUMMARY:-true}"
|
|
local _tg_label="${TELEGRAM_SERVER_LABEL:-}"
|
|
local _tg_start_hour="${TELEGRAM_START_HOUR:-0}"
|
|
(
|
|
umask 077
|
|
cat > "$_tmp" << EOF
|
|
BACKEND="${BACKEND:-paqet}"
|
|
ROLE="${ROLE}"
|
|
PAQET_VERSION="${PAQET_VERSION}"
|
|
PAQCTL_VERSION="${VERSION}"
|
|
LISTEN_PORT="${LISTEN_PORT:-}"
|
|
SOCKS_PORT="${SOCKS_PORT:-}"
|
|
INTERFACE="${INTERFACE}"
|
|
LOCAL_IP="${LOCAL_IP}"
|
|
GATEWAY_MAC="${GATEWAY_MAC}"
|
|
ENCRYPTION_KEY="${ENCRYPTION_KEY}"
|
|
REMOTE_SERVER="${REMOTE_SERVER:-}"
|
|
GFK_VIO_PORT="${GFK_VIO_PORT:-}"
|
|
GFK_QUIC_PORT="${GFK_QUIC_PORT:-}"
|
|
GFK_AUTH_CODE="${GFK_AUTH_CODE:-}"
|
|
GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS:-}"
|
|
MICROSOCKS_PORT="${MICROSOCKS_PORT:-}"
|
|
GFK_SERVER_IP="${GFK_SERVER_IP:-}"
|
|
TELEGRAM_BOT_TOKEN="${_tg_token}"
|
|
TELEGRAM_CHAT_ID="${_tg_chat}"
|
|
TELEGRAM_INTERVAL=${_tg_interval}
|
|
TELEGRAM_ENABLED=${_tg_enabled}
|
|
TELEGRAM_ALERTS_ENABLED=${_tg_alerts}
|
|
TELEGRAM_DAILY_SUMMARY=${_tg_daily}
|
|
TELEGRAM_WEEKLY_SUMMARY=${_tg_weekly}
|
|
TELEGRAM_SERVER_LABEL="${_tg_label}"
|
|
TELEGRAM_START_HOUR=${_tg_start_hour}
|
|
EOF
|
|
)
|
|
if ! mv "$_tmp" "$INSTALL_DIR/settings.conf"; then
|
|
log_error "Failed to save settings"
|
|
rm -f "$_tmp"
|
|
fi
|
|
chmod 600 "$INSTALL_DIR/settings.conf" 2>/dev/null
|
|
|
|
log_success "Configuration updated"
|
|
|
|
if [ "$was_running" = true ]; then
|
|
start_paqet
|
|
fi
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Backup & Restore
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
backup_config() {
|
|
(umask 077; mkdir -p "$BACKUP_DIR")
|
|
chmod 700 "$BACKUP_DIR" 2>/dev/null
|
|
local ts=$(date +%Y%m%d%H%M%S)
|
|
local backup_file="$BACKUP_DIR/paqctl-backup-${ts}.tar.gz"
|
|
|
|
if ! (umask 077; tar -czf "$backup_file" \
|
|
-C "$INSTALL_DIR" \
|
|
config.yaml settings.conf 2>/dev/null); then
|
|
log_error "Failed to create backup archive"
|
|
rm -f "$backup_file"
|
|
return 1
|
|
fi
|
|
echo ""
|
|
log_success "Backup saved to: $backup_file"
|
|
echo ""
|
|
}
|
|
|
|
restore_config() {
|
|
echo ""
|
|
if [ ! -d "$BACKUP_DIR" ] || [ -z "$(ls -A "$BACKUP_DIR"/*.tar.gz 2>/dev/null)" ]; then
|
|
log_warn "No backups found in $BACKUP_DIR"
|
|
return 1
|
|
fi
|
|
|
|
echo -e "${BOLD}Available backups:${NC}"
|
|
echo ""
|
|
local i=1
|
|
local backups=()
|
|
for f in "$BACKUP_DIR"/*.tar.gz; do
|
|
backups+=("$f")
|
|
echo " $i. $(basename "$f")"
|
|
i=$((i + 1))
|
|
done
|
|
echo ""
|
|
echo " 0. Cancel"
|
|
echo ""
|
|
read -p " Select backup [0-${#backups[@]}]: " choice < /dev/tty || true
|
|
if [ "$choice" = "0" ]; then
|
|
log_info "Cancelled"
|
|
return 0
|
|
fi
|
|
if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#backups[@]} ]; then
|
|
log_error "Invalid choice"
|
|
return 1
|
|
fi
|
|
|
|
local selected="${backups[$((choice-1))]}"
|
|
log_info "Restoring from: $(basename "$selected")"
|
|
|
|
local was_running=false
|
|
is_running && was_running=true
|
|
[ "$was_running" = true ] && stop_paqet
|
|
|
|
if ! (umask 077; tar -xzf "$selected" -C "$INSTALL_DIR" 2>/dev/null); then
|
|
log_error "Failed to extract backup archive"
|
|
[ "$was_running" = true ] && start_paqet
|
|
return 1
|
|
fi
|
|
chmod 600 "$INSTALL_DIR/config.yaml" "$INSTALL_DIR/settings.conf" 2>/dev/null
|
|
chown root:root "$INSTALL_DIR/config.yaml" "$INSTALL_DIR/settings.conf" 2>/dev/null
|
|
|
|
# Reload settings
|
|
_load_settings
|
|
|
|
log_success "Configuration restored"
|
|
|
|
[ "$was_running" = true ] && start_paqet
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Telegram Integration
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
# Secure Telegram API curl - writes token to temp file to avoid /proc exposure
|
|
_telegram_api_curl() {
|
|
local endpoint="$1"
|
|
shift
|
|
local _tg_tmp
|
|
_tg_tmp=$(mktemp "${INSTALL_DIR}/.tg_curl.XXXXXXXX") || return 1
|
|
chmod 600 "$_tg_tmp" 2>/dev/null
|
|
printf 'url = "https://api.telegram.org/bot%s/%s"\n' "$TELEGRAM_BOT_TOKEN" "$endpoint" > "$_tg_tmp"
|
|
local _result
|
|
_result=$(curl -s --max-time 10 --max-filesize 1048576 -K "$_tg_tmp" "$@" 2>/dev/null)
|
|
local _exit=$?
|
|
rm -f "$_tg_tmp"
|
|
[ $_exit -eq 0 ] && echo "$_result"
|
|
return $_exit
|
|
}
|
|
|
|
escape_telegram_markdown() {
|
|
local text="$1"
|
|
text="${text//\\/\\\\}"
|
|
text="${text//\*/\\*}"
|
|
text="${text//_/\\_}"
|
|
text="${text//\`/\\\`}"
|
|
text="${text//\[/\\[}"
|
|
text="${text//\]/\\]}"
|
|
echo "$text"
|
|
}
|
|
|
|
telegram_send_message() {
|
|
local message="$1"
|
|
{ [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; } && return 1
|
|
local label="${TELEGRAM_SERVER_LABEL:-$(hostname 2>/dev/null || echo 'unknown')}"
|
|
label=$(escape_telegram_markdown "$label")
|
|
local _ip=$(curl -s --max-time 3 https://api.ipify.org 2>/dev/null || echo "")
|
|
if [ -n "$_ip" ]; then
|
|
message="[${label} | ${_ip}] ${message}"
|
|
else
|
|
message="[${label}] ${message}"
|
|
fi
|
|
local response
|
|
response=$(_telegram_api_curl "sendMessage" \
|
|
-X POST \
|
|
--data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \
|
|
--data-urlencode "text=$message" \
|
|
--data-urlencode "parse_mode=Markdown")
|
|
[ $? -ne 0 ] && return 1
|
|
echo "$response" | grep -q '"ok":true' && return 0
|
|
return 1
|
|
}
|
|
|
|
telegram_get_chat_id() {
|
|
local response
|
|
response=$(_telegram_api_curl "getUpdates")
|
|
[ -z "$response" ] && return 1
|
|
echo "$response" | grep -q '"ok":true' || return 1
|
|
local chat_id=""
|
|
if command -v python3 &>/dev/null; then
|
|
chat_id=$(python3 -c "
|
|
import json,sys
|
|
try:
|
|
d=json.loads(sys.stdin.read())
|
|
msgs=d.get('result',[])
|
|
if msgs:
|
|
print(msgs[-1]['message']['chat']['id'])
|
|
except: pass
|
|
" <<< "$response" 2>/dev/null)
|
|
fi
|
|
if [ -z "$chat_id" ]; then
|
|
chat_id=$(echo "$response" | grep -o '"chat"[[:space:]]*:[[:space:]]*{[[:space:]]*"id"[[:space:]]*:[[:space:]]*-\?[0-9]\+' | grep -o -- '-\?[0-9]\+$' | tail -1 2>/dev/null)
|
|
fi
|
|
if [ -n "$chat_id" ] && echo "$chat_id" | grep -qE '^-?[0-9]+$'; then
|
|
TELEGRAM_CHAT_ID="$chat_id"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
telegram_build_report() {
|
|
local report="📊 *Paqet Status Report*"
|
|
report+=$'\n'
|
|
report+="🕐 $(date '+%Y-%m-%d %H:%M %Z')"
|
|
report+=$'\n\n'
|
|
|
|
if is_running; then
|
|
report+="✅ Status: Running"
|
|
else
|
|
report+="❌ Status: Stopped"
|
|
fi
|
|
report+=$'\n'
|
|
report+="📡 Role: ${ROLE}"
|
|
report+=$'\n'
|
|
report+="📦 Version: ${PAQET_VERSION}"
|
|
report+=$'\n'
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
report+="🔌 Port: ${LISTEN_PORT}"
|
|
report+=$'\n'
|
|
if iptables -t raw -C PREROUTING -p tcp --dport "$LISTEN_PORT" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then
|
|
report+="🛡 Firewall: Rules active"
|
|
else
|
|
report+="⚠️ Firewall: Rules missing"
|
|
fi
|
|
else
|
|
report+="🔗 Server: ${REMOTE_SERVER}"
|
|
report+=$'\n'
|
|
report+="🧦 SOCKS: port ${SOCKS_PORT}"
|
|
fi
|
|
report+=$'\n'
|
|
|
|
# Uptime
|
|
if is_running && command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
local started
|
|
started=$(systemctl show paqctl.service --property=ActiveEnterTimestamp 2>/dev/null | cut -d= -f2)
|
|
if [ -n "$started" ]; then
|
|
local started_ts
|
|
started_ts=$(date -d "$started" +%s 2>/dev/null || echo 0)
|
|
if [ "$started_ts" -gt 0 ] 2>/dev/null; then
|
|
local now=$(date +%s)
|
|
local up=$((now - started_ts))
|
|
local days=$((up / 86400))
|
|
local hours=$(( (up % 86400) / 3600 ))
|
|
local mins=$(( (up % 3600) / 60 ))
|
|
if [ "$days" -gt 0 ]; then
|
|
report+="⏱ Uptime: ${days}d ${hours}h ${mins}m"
|
|
else
|
|
report+="⏱ Uptime: ${hours}h ${mins}m"
|
|
fi
|
|
report+=$'\n'
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# CPU/RAM
|
|
local pid
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
pid=$(pgrep -f "mainserver.py|mainclient.py" 2>/dev/null | head -1)
|
|
else
|
|
pid=$(pgrep -f "paqet run -c" 2>/dev/null | head -1)
|
|
fi
|
|
if [ -n "$pid" ]; then
|
|
local cpu_mem
|
|
cpu_mem=$(ps -p "$pid" -o %cpu=,%mem= 2>/dev/null | head -1)
|
|
if [ -n "$cpu_mem" ]; then
|
|
local cpu=$(echo "$cpu_mem" | awk '{print $1}')
|
|
local mem=$(echo "$cpu_mem" | awk '{print $2}')
|
|
report+="💻 CPU: ${cpu}% | RAM: ${mem}%"
|
|
report+=$'\n'
|
|
fi
|
|
fi
|
|
|
|
echo "$report"
|
|
}
|
|
|
|
telegram_test_message() {
|
|
local interval_label="${TELEGRAM_INTERVAL:-6}"
|
|
local report=$(telegram_build_report)
|
|
local backend_name="${BACKEND:-paqet}"
|
|
|
|
# Backend-specific description
|
|
local tech_desc=""
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
tech_desc="🔗 *What is GFW-Knocker?*
|
|
An advanced anti-censorship tool using 'violated TCP' packets + QUIC tunneling.
|
|
Designed for heavy DPI environments like the Great Firewall.
|
|
• Raw socket layer bypasses kernel TCP stack
|
|
• QUIC tunnel provides encrypted transport
|
|
• Requires Xray on server for SOCKS5 proxy"
|
|
else
|
|
tech_desc="🔗 *What is Paqet?*
|
|
A raw-socket encrypted proxy using KCP protocol.
|
|
Simple all-in-one solution with built-in SOCKS5 proxy.
|
|
• KCP over raw sockets bypasses DPI
|
|
• Built-in SOCKS5 proxy (no extra software needed)
|
|
• Easy setup with just IP, port, and key"
|
|
fi
|
|
|
|
local message="✅ *paqctl Connected!*
|
|
|
|
📦 *About paqctl*
|
|
A unified management tool for bypass proxies.
|
|
Supports two backends for different network conditions:
|
|
• *paqet* — Simple KCP-based proxy (recommended)
|
|
• *gfw-knocker* — Advanced violated-TCP + QUIC tunnel
|
|
|
|
━━━━━━━━━━━━━━━━━━━━
|
|
${tech_desc}
|
|
|
|
📬 *What this bot sends you every ${interval_label}h:*
|
|
• Service status & uptime
|
|
• CPU & RAM usage
|
|
• Configuration summary
|
|
• Firewall rule status
|
|
|
|
⚠️ *Alerts:*
|
|
If the service goes down or is restarted, you will receive an immediate alert.
|
|
|
|
━━━━━━━━━━━━━━━━━━━━
|
|
🎮 *Available Commands:*
|
|
━━━━━━━━━━━━━━━━━━━━
|
|
/status — Full status report
|
|
/health — Run health check
|
|
/restart — Restart ${backend_name}
|
|
/stop — Stop ${backend_name}
|
|
/start — Start ${backend_name}
|
|
/version — Show version info
|
|
|
|
━━━━━━━━━━━━━━━━━━━━
|
|
📊 *Your first report:*
|
|
━━━━━━━━━━━━━━━━━━━━
|
|
|
|
${report}"
|
|
telegram_send_message "$message"
|
|
}
|
|
|
|
telegram_generate_notify_script() {
|
|
local script_path="$INSTALL_DIR/paqctl-telegram.sh"
|
|
local _tmp
|
|
_tmp=$(mktemp "${script_path}.XXXXXXXX")
|
|
cat > "$_tmp" << 'TGSCRIPT'
|
|
#!/bin/bash
|
|
# paqctl Telegram notification daemon
|
|
|
|
INSTALL_DIR="REPLACE_ME_INSTALL_DIR"
|
|
|
|
# Safe settings loader - parses key=value with validation
|
|
_load_settings() {
|
|
[ -f "$INSTALL_DIR/settings.conf" ] || return 0
|
|
while IFS='=' read -r key value; do
|
|
[[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue
|
|
value="${value#\"}"; value="${value%\"}"
|
|
# Skip values with dangerous shell characters
|
|
[[ "$value" =~ [\`\$\(] ]] && continue
|
|
case "$key" in
|
|
BACKEND|ROLE|PAQET_VERSION|PAQCTL_VERSION|INTERFACE|LOCAL_IP|GATEWAY_MAC|\
|
|
ENCRYPTION_KEY|REMOTE_SERVER|GFK_AUTH_CODE|GFK_PORT_MAPPINGS|GFK_SERVER_IP|\
|
|
TELEGRAM_BOT_TOKEN|TELEGRAM_CHAT_ID|TELEGRAM_SERVER_LABEL|\
|
|
TELEGRAM_ENABLED|TELEGRAM_ALERTS_ENABLED|TELEGRAM_DAILY_SUMMARY|TELEGRAM_WEEKLY_SUMMARY)
|
|
export "$key=$value" ;;
|
|
LISTEN_PORT|SOCKS_PORT|GFK_VIO_PORT|GFK_VIO_CLIENT_PORT|GFK_QUIC_PORT|GFK_QUIC_CLIENT_PORT|MICROSOCKS_PORT|\
|
|
TELEGRAM_INTERVAL|TELEGRAM_START_HOUR)
|
|
[[ "$value" =~ ^[0-9]*$ ]] && export "$key=$value" ;;
|
|
esac
|
|
done < <(grep '^[A-Z_][A-Z_0-9]*=' "$INSTALL_DIR/settings.conf")
|
|
}
|
|
_load_settings
|
|
|
|
TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
|
|
TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-}
|
|
TELEGRAM_INTERVAL=${TELEGRAM_INTERVAL:-6}
|
|
TELEGRAM_ALERTS_ENABLED=${TELEGRAM_ALERTS_ENABLED:-true}
|
|
TELEGRAM_DAILY_SUMMARY=${TELEGRAM_DAILY_SUMMARY:-true}
|
|
TELEGRAM_WEEKLY_SUMMARY=${TELEGRAM_WEEKLY_SUMMARY:-true}
|
|
TELEGRAM_START_HOUR=${TELEGRAM_START_HOUR:-0}
|
|
|
|
{ [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; } && exit 1
|
|
|
|
escape_telegram_markdown() {
|
|
local text="$1"
|
|
text="${text//\\/\\\\}"
|
|
text="${text//\*/\\*}"
|
|
text="${text//_/\\_}"
|
|
text="${text//\`/\\\`}"
|
|
text="${text//\[/\\[}"
|
|
text="${text//\]/\\]}"
|
|
echo "$text"
|
|
}
|
|
|
|
# Secure Telegram API curl - writes token to temp file to avoid /proc exposure
|
|
_tg_api_curl() {
|
|
local endpoint="$1"
|
|
shift
|
|
local _tg_tmp
|
|
_tg_tmp=$(mktemp "${INSTALL_DIR}/.tg_curl.XXXXXXXX") || return 1
|
|
chmod 600 "$_tg_tmp" 2>/dev/null
|
|
printf 'url = "https://api.telegram.org/bot%s/%s"\n' "$TELEGRAM_BOT_TOKEN" "$endpoint" > "$_tg_tmp"
|
|
local _result
|
|
_result=$(curl -s --max-time 10 --max-filesize 1048576 -K "$_tg_tmp" "$@" 2>/dev/null)
|
|
local _exit=$?
|
|
rm -f "$_tg_tmp"
|
|
[ $_exit -eq 0 ] && echo "$_result"
|
|
return $_exit
|
|
}
|
|
|
|
send_message() {
|
|
local message="$1"
|
|
{ [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; } && return 1
|
|
local label="${TELEGRAM_SERVER_LABEL:-$(hostname 2>/dev/null || echo 'unknown')}"
|
|
label=$(escape_telegram_markdown "$label")
|
|
local _ip=$(curl -s --max-time 3 https://api.ipify.org 2>/dev/null || echo "")
|
|
[ -n "$_ip" ] && message="[${label} | ${_ip}] ${message}" || message="[${label}] ${message}"
|
|
local response
|
|
response=$(_tg_api_curl "sendMessage" \
|
|
-X POST \
|
|
--data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \
|
|
--data-urlencode "text=$message" \
|
|
--data-urlencode "parse_mode=Markdown")
|
|
[ $? -ne 0 ] && return 1
|
|
echo "$response" | grep -q '"ok":true' && return 0
|
|
return 1
|
|
}
|
|
|
|
is_running() {
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
pgrep -f "mainserver.py|mainclient.py|gfk-client.sh" &>/dev/null
|
|
else
|
|
pgrep -f "paqet run -c" &>/dev/null
|
|
fi
|
|
}
|
|
|
|
get_main_pid() {
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
pgrep -f "mainserver.py" 2>/dev/null | head -1
|
|
else
|
|
pgrep -f "paqet run -c" 2>/dev/null | head -1
|
|
fi
|
|
}
|
|
|
|
build_report() {
|
|
local report="📊 *${BACKEND} Status Report*"$'\n'
|
|
report+="🕐 $(date '+%Y-%m-%d %H:%M %Z')"$'\n\n'
|
|
if is_running; then
|
|
report+="✅ Status: Running"
|
|
else
|
|
report+="❌ Status: Stopped"
|
|
fi
|
|
report+=$'\n'"📡 Role: ${ROLE:-unknown}"$'\n'
|
|
report+="📦 Version: ${PAQET_VERSION:-unknown}"$'\n'
|
|
local pid=$(get_main_pid)
|
|
if [ -n "$pid" ]; then
|
|
local cpu_mem=$(ps -p "$pid" -o %cpu=,%mem= 2>/dev/null | head -1)
|
|
if [ -n "$cpu_mem" ]; then
|
|
local cpu=$(echo "$cpu_mem" | awk '{print $1}')
|
|
local mem=$(echo "$cpu_mem" | awk '{print $2}')
|
|
report+="💻 CPU: ${cpu}% | RAM: ${mem}%"$'\n'
|
|
fi
|
|
fi
|
|
echo "$report"
|
|
}
|
|
|
|
LAST_COMMAND_TIME=0
|
|
COMMAND_COOLDOWN=5
|
|
|
|
check_commands() {
|
|
local response
|
|
response=$(_tg_api_curl "getUpdates" \
|
|
-X POST \
|
|
--data-urlencode "offset=${LAST_UPDATE_ID:-0}" \
|
|
--data-urlencode "limit=10")
|
|
[ -z "$response" ] && return
|
|
echo "$response" | grep -q '"ok":true' || return
|
|
|
|
if command -v python3 &>/dev/null; then
|
|
local cmds
|
|
local _safe_chat_id
|
|
_safe_chat_id=$(printf '%s' "$TELEGRAM_CHAT_ID" | tr -cd '0-9-')
|
|
[ -z "$_safe_chat_id" ] && return
|
|
cmds=$(python3 -c "
|
|
import json,sys
|
|
try:
|
|
d=json.loads(sys.stdin.read())
|
|
chat_id=sys.argv[1]
|
|
if not chat_id: sys.exit(0)
|
|
for r in d.get('result',[]):
|
|
uid=r['update_id']
|
|
txt=r.get('message',{}).get('text','').replace('|','')
|
|
cid=str(r.get('message',{}).get('chat',{}).get('id',''))
|
|
if cid==chat_id and txt.startswith('/'):
|
|
print(f'{uid}|{txt}')
|
|
except: pass
|
|
" "$_safe_chat_id" <<< "$response" 2>/dev/null)
|
|
|
|
while IFS='|' read -r uid cmd; do
|
|
[ -z "$uid" ] && continue
|
|
# Validate uid is numeric
|
|
[[ "$uid" =~ ^[0-9]+$ ]] || continue
|
|
LAST_UPDATE_ID=$((uid + 1))
|
|
cmd="${cmd%% *}" # strip arguments, match command only
|
|
|
|
# Rate limiting
|
|
local _now
|
|
_now=$(date +%s)
|
|
if [ $((_now - LAST_COMMAND_TIME)) -lt $COMMAND_COOLDOWN ]; then
|
|
continue
|
|
fi
|
|
LAST_COMMAND_TIME=$_now
|
|
|
|
case "$cmd" in
|
|
/status) send_message "$(build_report)" ;;
|
|
/health) send_message "$(/usr/local/bin/paqctl health 2>&1 | head -30)" ;;
|
|
/restart) /usr/local/bin/paqctl restart 2>&1; send_message "🔄 paqet restarted" ;;
|
|
/stop) /usr/local/bin/paqctl stop 2>&1; send_message "⏹ paqet stopped" ;;
|
|
/start) /usr/local/bin/paqctl start 2>&1; send_message "▶️ paqet started" ;;
|
|
/version) send_message "📦 paqet: ${PAQET_VERSION:-unknown} | paqctl: ${PAQCTL_VERSION:-unknown}" ;;
|
|
esac
|
|
done <<< "$cmds"
|
|
fi
|
|
}
|
|
|
|
# Alert state
|
|
LAST_STATE="unknown"
|
|
LAST_REPORT=0
|
|
LAST_DAILY=0
|
|
LAST_WEEKLY=0
|
|
LAST_UPDATE_ID=0
|
|
|
|
# Initialize update offset
|
|
init_response=$(_tg_api_curl "getUpdates" \
|
|
-X POST \
|
|
--data-urlencode "offset=-1")
|
|
if command -v python3 &>/dev/null; then
|
|
LAST_UPDATE_ID=$(python3 -c "
|
|
import json,sys
|
|
try:
|
|
d=json.loads(sys.stdin.read())
|
|
r=d.get('result',[])
|
|
if r: print(r[-1]['update_id']+1)
|
|
else: print(0)
|
|
except: print(0)
|
|
" <<< "$init_response" 2>/dev/null)
|
|
fi
|
|
LAST_UPDATE_ID=${LAST_UPDATE_ID:-0}
|
|
|
|
while true; do
|
|
# Reload settings periodically (safe parser, no code execution)
|
|
_load_settings
|
|
|
|
# Check commands from Telegram
|
|
check_commands
|
|
|
|
# Service state alerts
|
|
current_state="stopped"
|
|
is_running && current_state="running"
|
|
|
|
if [ "$TELEGRAM_ALERTS_ENABLED" = "true" ]; then
|
|
if [ "$LAST_STATE" = "running" ] && [ "$current_state" = "stopped" ]; then
|
|
send_message "🚨 *ALERT:* ${BACKEND} service has stopped!"
|
|
elif [ "$LAST_STATE" = "stopped" ] && [ "$current_state" = "running" ]; then
|
|
send_message "✅ ${BACKEND} service is back up"
|
|
fi
|
|
|
|
# High CPU alert
|
|
_pid=$(get_main_pid)
|
|
if [ -n "$_pid" ]; then
|
|
_cpu=$(ps -p "$_pid" -o %cpu= 2>/dev/null | awk '{printf "%.0f", $1}')
|
|
if [ "${_cpu:-0}" -gt 80 ] 2>/dev/null; then
|
|
send_message "⚠️ High CPU usage: ${_cpu}%"
|
|
fi
|
|
fi
|
|
fi
|
|
LAST_STATE="$current_state"
|
|
|
|
# Periodic reports
|
|
_now=$(date +%s)
|
|
_interval_secs=$(( ${TELEGRAM_INTERVAL:-6} * 3600 ))
|
|
if [ $((_now - LAST_REPORT)) -ge "$_interval_secs" ]; then
|
|
send_message "$(build_report)"
|
|
LAST_REPORT=$_now
|
|
fi
|
|
|
|
# Daily summary
|
|
_hour=$(date +%H)
|
|
_day_of_week=$(date +%u)
|
|
if [ "$TELEGRAM_DAILY_SUMMARY" = "true" ] && [ "$_hour" = "$(printf '%02d' ${TELEGRAM_START_HOUR:-0})" ]; then
|
|
if [ $((_now - LAST_DAILY)) -ge 86400 ]; then
|
|
send_message "📅 *Daily Summary*"$'\n'"$(build_report)"
|
|
LAST_DAILY=$_now
|
|
fi
|
|
fi
|
|
|
|
# Weekly summary (Monday)
|
|
if [ "$TELEGRAM_WEEKLY_SUMMARY" = "true" ] && [ "$_day_of_week" = "1" ] && [ "$_hour" = "$(printf '%02d' ${TELEGRAM_START_HOUR:-0})" ]; then
|
|
if [ $((_now - LAST_WEEKLY)) -ge 604800 ]; then
|
|
send_message "📆 *Weekly Summary*"$'\n'"$(build_report)"
|
|
LAST_WEEKLY=$_now
|
|
fi
|
|
fi
|
|
|
|
sleep 30
|
|
done
|
|
TGSCRIPT
|
|
|
|
sed "s#REPLACE_ME_INSTALL_DIR#$INSTALL_DIR#g" "$_tmp" > "$_tmp.sed" && mv "$_tmp.sed" "$_tmp"
|
|
if ! chmod +x "$_tmp"; then
|
|
log_error "Failed to make Telegram script executable"
|
|
rm -f "$_tmp"
|
|
return 1
|
|
fi
|
|
if ! mv "$_tmp" "$script_path"; then
|
|
log_error "Failed to install Telegram script"
|
|
rm -f "$_tmp"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
setup_telegram_service() {
|
|
telegram_generate_notify_script
|
|
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
cat > /etc/systemd/system/paqctl-telegram.service << EOF
|
|
[Unit]
|
|
Description=paqctl Telegram Notification Service
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
ExecStart=$(command -v bash) ${INSTALL_DIR}/paqctl-telegram.sh
|
|
Restart=on-failure
|
|
RestartSec=10
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
systemctl daemon-reload 2>/dev/null || true
|
|
systemctl enable paqctl-telegram.service 2>/dev/null || true
|
|
systemctl start paqctl-telegram.service 2>/dev/null || true
|
|
log_success "Telegram service started"
|
|
else
|
|
log_warn "Systemd not available. Run the Telegram daemon manually:"
|
|
log_info " nohup bash $INSTALL_DIR/paqctl-telegram.sh &"
|
|
fi
|
|
}
|
|
|
|
stop_telegram_service() {
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
systemctl stop paqctl-telegram.service 2>/dev/null || true
|
|
systemctl disable paqctl-telegram.service 2>/dev/null || true
|
|
fi
|
|
pkill -f "paqctl-telegram.sh" 2>/dev/null || true
|
|
log_success "Telegram service stopped"
|
|
}
|
|
|
|
show_telegram_menu() {
|
|
local redraw=true
|
|
while true; do
|
|
if [ "$redraw" = true ]; then
|
|
clear
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} TELEGRAM NOTIFICATIONS${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
if [ "$TELEGRAM_ENABLED" = "true" ]; then
|
|
echo -e " Status: ${GREEN}Enabled${NC}"
|
|
if command -v systemctl &>/dev/null && systemctl is-active paqctl-telegram.service &>/dev/null; then
|
|
echo -e " Service: ${GREEN}Running${NC}"
|
|
else
|
|
echo -e " Service: ${RED}Stopped${NC}"
|
|
fi
|
|
else
|
|
echo -e " Status: ${DIM}Disabled${NC}"
|
|
fi
|
|
|
|
echo ""
|
|
echo " 1. Setup / Change bot"
|
|
echo " 2. Test notification"
|
|
echo " 3. Enable & start service"
|
|
echo " 4. Disable & stop service"
|
|
echo " 5. Set check interval (currently: ${TELEGRAM_INTERVAL}h)"
|
|
echo " 6. Set server label (currently: ${TELEGRAM_SERVER_LABEL:-hostname})"
|
|
echo " 7. Toggle alerts (currently: ${TELEGRAM_ALERTS_ENABLED})"
|
|
echo " b. Back"
|
|
echo ""
|
|
redraw=false
|
|
fi
|
|
|
|
read -p " Choice: " tg_choice < /dev/tty || break
|
|
case "$tg_choice" in
|
|
1)
|
|
echo ""
|
|
echo -e "${BOLD}Telegram Bot Setup${NC}"
|
|
echo ""
|
|
echo " 1. Open Telegram and message @BotFather"
|
|
echo " 2. Send /newbot and follow the steps"
|
|
echo " 3. Copy the bot token"
|
|
echo ""
|
|
read -p " Enter bot token: " input < /dev/tty || true
|
|
if [ -n "$input" ]; then
|
|
TELEGRAM_BOT_TOKEN="$input"
|
|
echo ""
|
|
echo " Now send any message to your bot in Telegram..."
|
|
echo ""
|
|
for _i in $(seq 15 -1 1); do
|
|
printf "\r Waiting: %2ds " "$_i"
|
|
sleep 1
|
|
done
|
|
printf "\r \r"
|
|
if telegram_get_chat_id; then
|
|
log_success "Chat ID detected: $TELEGRAM_CHAT_ID"
|
|
# Save
|
|
_safe_update_setting "TELEGRAM_BOT_TOKEN" "$TELEGRAM_BOT_TOKEN" "$INSTALL_DIR/settings.conf"
|
|
_safe_update_setting "TELEGRAM_CHAT_ID" "$TELEGRAM_CHAT_ID" "$INSTALL_DIR/settings.conf"
|
|
else
|
|
log_error "Could not detect chat ID. Make sure you sent a message to the bot."
|
|
echo ""
|
|
read -p " Enter chat ID manually (or press Enter to cancel): " input < /dev/tty || true
|
|
if [ -n "$input" ]; then
|
|
TELEGRAM_CHAT_ID="$input"
|
|
_safe_update_setting "TELEGRAM_BOT_TOKEN" "$TELEGRAM_BOT_TOKEN" "$INSTALL_DIR/settings.conf"
|
|
_safe_update_setting "TELEGRAM_CHAT_ID" "$TELEGRAM_CHAT_ID" "$INSTALL_DIR/settings.conf"
|
|
fi
|
|
fi
|
|
fi
|
|
redraw=true
|
|
;;
|
|
2)
|
|
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
|
|
log_error "Bot not configured. Run setup first."
|
|
else
|
|
if telegram_test_message; then
|
|
log_success "Test message sent!"
|
|
else
|
|
log_error "Failed to send. Check token and chat ID."
|
|
fi
|
|
fi
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
redraw=true
|
|
;;
|
|
3)
|
|
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
|
|
log_error "Bot not configured. Run setup first."
|
|
else
|
|
TELEGRAM_ENABLED=true
|
|
_safe_update_setting "TELEGRAM_ENABLED" "true" "$INSTALL_DIR/settings.conf"
|
|
setup_telegram_service
|
|
fi
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
redraw=true
|
|
;;
|
|
4)
|
|
TELEGRAM_ENABLED=false
|
|
_safe_update_setting "TELEGRAM_ENABLED" "false" "$INSTALL_DIR/settings.conf"
|
|
stop_telegram_service
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
redraw=true
|
|
;;
|
|
5)
|
|
echo ""
|
|
read -p " Check interval in hours [1-24]: " input < /dev/tty || true
|
|
if [[ "$input" =~ ^[0-9]+$ ]] && [ "$input" -ge 1 ] && [ "$input" -le 24 ]; then
|
|
TELEGRAM_INTERVAL="$input"
|
|
_safe_update_setting "TELEGRAM_INTERVAL" "$input" "$INSTALL_DIR/settings.conf"
|
|
log_success "Interval set to ${input}h"
|
|
# Restart service if running
|
|
if command -v systemctl &>/dev/null && systemctl is-active paqctl-telegram.service &>/dev/null; then
|
|
telegram_generate_notify_script
|
|
systemctl restart paqctl-telegram.service 2>/dev/null || true
|
|
fi
|
|
else
|
|
log_warn "Invalid value"
|
|
fi
|
|
redraw=true
|
|
;;
|
|
6)
|
|
echo ""
|
|
read -p " Server label: " input < /dev/tty || true
|
|
if [ -n "$input" ]; then
|
|
TELEGRAM_SERVER_LABEL="$input"
|
|
_safe_update_setting "TELEGRAM_SERVER_LABEL" "$input" "$INSTALL_DIR/settings.conf"
|
|
log_success "Label set to: $input"
|
|
fi
|
|
redraw=true
|
|
;;
|
|
7)
|
|
if [ "$TELEGRAM_ALERTS_ENABLED" = "true" ]; then
|
|
TELEGRAM_ALERTS_ENABLED=false
|
|
else
|
|
TELEGRAM_ALERTS_ENABLED=true
|
|
fi
|
|
_safe_update_setting "TELEGRAM_ALERTS_ENABLED" "$TELEGRAM_ALERTS_ENABLED" "$INSTALL_DIR/settings.conf"
|
|
log_info "Alerts: $TELEGRAM_ALERTS_ENABLED"
|
|
redraw=true
|
|
;;
|
|
b|B) return ;;
|
|
"") ;;
|
|
*) echo -e " ${RED}Invalid choice${NC}" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Switch Backend
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
switch_backend() {
|
|
local current_backend="${BACKEND:-paqet}"
|
|
local new_backend
|
|
if [ "$current_backend" = "paqet" ]; then
|
|
new_backend="gfw-knocker"
|
|
else
|
|
new_backend="paqet"
|
|
fi
|
|
|
|
# Check if the other backend is installed
|
|
local other_installed=false
|
|
if [ "$new_backend" = "gfw-knocker" ]; then
|
|
if [ "$ROLE" = "server" ]; then
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && other_installed=true
|
|
else
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && other_installed=true
|
|
fi
|
|
else
|
|
[ -f "$INSTALL_DIR/bin/paqet" ] && other_installed=true
|
|
fi
|
|
|
|
if [ "$other_installed" = false ]; then
|
|
echo ""
|
|
echo -e "${YELLOW}${new_backend} is not installed.${NC}"
|
|
echo ""
|
|
echo " Use 'Install additional backend' option to install it first."
|
|
echo ""
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${BOLD}Switch active backend from ${current_backend} to ${new_backend}?${NC}"
|
|
echo ""
|
|
echo " This will:"
|
|
echo " - Stop ${current_backend}"
|
|
echo " - Start ${new_backend}"
|
|
echo " - Both backends remain installed"
|
|
echo ""
|
|
read -p " Proceed? [y/N]: " confirm < /dev/tty || true
|
|
[[ "$confirm" =~ ^[Yy]$ ]] || { log_info "Cancelled"; return 0; }
|
|
|
|
# Stop current
|
|
stop_paqet
|
|
_remove_firewall
|
|
|
|
# Switch to new backend
|
|
BACKEND="$new_backend"
|
|
save_settings
|
|
|
|
# Setup firewall and start new backend
|
|
_apply_firewall
|
|
start_paqet
|
|
|
|
log_success "Switched to ${new_backend}"
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
}
|
|
|
|
install_additional_backend() {
|
|
local current_backend="${BACKEND:-paqet}"
|
|
local new_backend
|
|
if [ "$current_backend" = "paqet" ]; then
|
|
new_backend="gfw-knocker"
|
|
else
|
|
new_backend="paqet"
|
|
fi
|
|
|
|
# Check if already installed
|
|
local already_installed=false
|
|
if [ "$new_backend" = "gfw-knocker" ]; then
|
|
if [ "$ROLE" = "server" ]; then
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && already_installed=true
|
|
else
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && already_installed=true
|
|
fi
|
|
else
|
|
[ -f "$INSTALL_DIR/bin/paqet" ] && already_installed=true
|
|
fi
|
|
|
|
if [ "$already_installed" = true ]; then
|
|
echo ""
|
|
echo -e "${GREEN}${new_backend} is already installed.${NC}"
|
|
echo ""
|
|
echo " Use 'Switch backend' to change the active backend."
|
|
echo ""
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
return 0
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${BOLD}Install ${new_backend} alongside ${current_backend}?${NC}"
|
|
echo ""
|
|
echo " This will:"
|
|
echo " - Keep ${current_backend} running"
|
|
echo " - Install ${new_backend} as an additional option"
|
|
echo " - You can switch between them anytime"
|
|
echo ""
|
|
read -p " Proceed? [y/N]: " confirm < /dev/tty || true
|
|
[[ "$confirm" =~ ^[Yy]$ ]] || { log_info "Cancelled"; return 0; }
|
|
|
|
echo ""
|
|
log_info "Installing ${new_backend}..."
|
|
|
|
if [ "$new_backend" = "gfw-knocker" ]; then
|
|
# Install GFK without changing current backend
|
|
if ! _install_gfk_components; then
|
|
log_error "Failed to install ${new_backend}"
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
return 1
|
|
fi
|
|
else
|
|
# Install paqet without changing current backend
|
|
if ! _install_paqet_components; then
|
|
log_error "Failed to install ${new_backend}"
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
log_success "${new_backend} installed successfully!"
|
|
echo ""
|
|
echo " Use 'Switch backend' to activate it."
|
|
echo ""
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
}
|
|
|
|
_install_paqet_components() {
|
|
log_info "Downloading paqet binary..."
|
|
if ! download_paqet "$PAQET_VERSION_PINNED"; then
|
|
log_error "Failed to download paqet"
|
|
return 1
|
|
fi
|
|
log_success "paqet binary installed"
|
|
|
|
# Generate config.yaml if it doesn't exist
|
|
if [ ! -f "$INSTALL_DIR/config.yaml" ]; then
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} PAQET CONFIGURATION${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
# Detect network settings
|
|
detect_network
|
|
local _det_iface="$DETECTED_IFACE"
|
|
local _det_ip="$DETECTED_IP"
|
|
local _det_mac="$DETECTED_GW_MAC"
|
|
|
|
# Prompt for interface
|
|
echo -e "${BOLD}Network Interface${NC} [${_det_iface:-eth0}]:"
|
|
read -p " Interface: " input < /dev/tty || true
|
|
local _iface="${input:-${_det_iface:-eth0}}"
|
|
|
|
# Prompt for local IP
|
|
echo -e "${BOLD}Local IP${NC} [${_det_ip:-}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
local _local_ip="${input:-$_det_ip}"
|
|
|
|
# Prompt for gateway MAC
|
|
echo -e "${BOLD}Gateway MAC${NC} [${_det_mac:-}]:"
|
|
read -p " MAC: " input < /dev/tty || true
|
|
local _gw_mac="${input:-$_det_mac}"
|
|
|
|
# Validate MAC if provided
|
|
if [ -n "$_gw_mac" ] && ! [[ "$_gw_mac" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; then
|
|
log_warn "Invalid MAC format. Expected: aa:bb:cc:dd:ee:ff"
|
|
read -p " Enter valid MAC: " input < /dev/tty || true
|
|
[ -n "$input" ] && _gw_mac="$input"
|
|
fi
|
|
|
|
# Generate encryption key
|
|
local _key
|
|
_key=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true)
|
|
if [ -z "$_key" ]; then
|
|
_key=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32 || true)
|
|
fi
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
# Prompt for port
|
|
echo -e "${BOLD}Listen Port${NC} [8443]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
local _port="${input:-8443}"
|
|
|
|
# Show generated key
|
|
echo ""
|
|
echo -e "${GREEN}${BOLD} Generated Encryption Key: ${_key}${NC}"
|
|
echo -e "${YELLOW} IMPORTANT: Save this key! Clients need it to connect.${NC}"
|
|
echo ""
|
|
echo -e "${BOLD}Encryption Key${NC} (press Enter to use generated key):"
|
|
read -p " Key: " input < /dev/tty || true
|
|
[ -n "$input" ] && _key="$input"
|
|
|
|
LISTEN_PORT="$_port"
|
|
ENCRYPTION_KEY="$_key"
|
|
else
|
|
# Client prompts
|
|
echo -e "${BOLD}Remote Server${NC} (IP:PORT):"
|
|
read -p " Server: " input < /dev/tty || true
|
|
local _server="${input:-${REMOTE_SERVER:-}}"
|
|
if [ -z "$_server" ]; then
|
|
log_warn "No server specified. You must edit config.yaml later."
|
|
_server="SERVER_IP:8443"
|
|
fi
|
|
|
|
echo -e "${BOLD}Encryption Key${NC} (from server):"
|
|
read -p " Key: " input < /dev/tty || true
|
|
[ -n "$input" ] && _key="$input"
|
|
|
|
echo -e "${BOLD}SOCKS5 Port${NC} [1080]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
local _socks="${input:-1080}"
|
|
|
|
REMOTE_SERVER="$_server"
|
|
SOCKS_PORT="$_socks"
|
|
ENCRYPTION_KEY="$_key"
|
|
fi
|
|
|
|
# Validate required fields
|
|
if [ -z "$_iface" ] || [ -z "$_local_ip" ] || [ -z "$_gw_mac" ]; then
|
|
log_error "Missing required fields (interface, IP, or MAC)"
|
|
return 1
|
|
fi
|
|
if [ -z "$_key" ] || [ "${#_key}" -lt 16 ]; then
|
|
log_error "Invalid encryption key"
|
|
return 1
|
|
fi
|
|
|
|
# Helper to escape YAML values
|
|
_escape_yaml_val() {
|
|
local s="$1"
|
|
if [[ "$s" =~ [:\#\[\]\{\}\"\'\|\>\<\&\*\!\%\@\`] ]] || [[ "$s" =~ ^[[:space:]] ]] || [[ "$s" =~ [[:space:]]$ ]]; then
|
|
s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; printf '"%s"' "$s"
|
|
else
|
|
printf '%s' "$s"
|
|
fi
|
|
}
|
|
|
|
local _y_iface _y_ip _y_mac _y_key
|
|
_y_iface=$(_escape_yaml_val "$_iface")
|
|
_y_ip=$(_escape_yaml_val "$_local_ip")
|
|
_y_mac=$(_escape_yaml_val "$_gw_mac")
|
|
_y_key=$(_escape_yaml_val "$_key")
|
|
|
|
local tmp_conf
|
|
tmp_conf=$(mktemp "$INSTALL_DIR/config.yaml.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
|
|
chmod 600 "$tmp_conf" 2>/dev/null
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
cat > "$tmp_conf" << EOF
|
|
role: "server"
|
|
|
|
log:
|
|
level: "info"
|
|
|
|
listen:
|
|
addr: ":${_port}"
|
|
|
|
network:
|
|
interface: "${_y_iface}"
|
|
ipv4:
|
|
addr: "${_y_ip}:${_port}"
|
|
router_mac: "${_y_mac}"
|
|
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_y_key}"
|
|
EOF
|
|
else
|
|
cat > "$tmp_conf" << EOF
|
|
role: "client"
|
|
|
|
log:
|
|
level: "info"
|
|
|
|
socks5:
|
|
- listen: "127.0.0.1:${_socks}"
|
|
|
|
network:
|
|
interface: "${_y_iface}"
|
|
ipv4:
|
|
addr: "${_y_ip}:0"
|
|
router_mac: "${_y_mac}"
|
|
|
|
server:
|
|
addr: "${_server}"
|
|
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_y_key}"
|
|
EOF
|
|
fi
|
|
|
|
if ! mv "$tmp_conf" "$INSTALL_DIR/config.yaml"; then
|
|
log_error "Failed to save config.yaml"
|
|
rm -f "$tmp_conf"
|
|
return 1
|
|
fi
|
|
chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null
|
|
|
|
# Update global vars for settings
|
|
INTERFACE="$_iface"
|
|
LOCAL_IP="$_local_ip"
|
|
GATEWAY_MAC="$_gw_mac"
|
|
|
|
log_success "Configuration saved to $INSTALL_DIR/config.yaml"
|
|
|
|
# Save to settings.conf for persistence
|
|
save_settings 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
check_xray_installed() {
|
|
command -v xray &>/dev/null || [ -x /usr/local/bin/xray ]
|
|
}
|
|
|
|
XRAY_CONFIG_DIR="/usr/local/etc/xray"
|
|
XRAY_CONFIG_FILE="$XRAY_CONFIG_DIR/config.json"
|
|
|
|
install_xray() {
|
|
if check_xray_installed; then
|
|
log_info "Xray is already installed"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Installing Xray ${XRAY_VERSION_PINNED}..."
|
|
|
|
local tmp_script
|
|
tmp_script=$(mktemp)
|
|
if ! curl -sL https://github.com/XTLS/Xray-install/raw/main/install-release.sh -o "$tmp_script"; then
|
|
log_error "Failed to download Xray installer"
|
|
rm -f "$tmp_script"
|
|
return 1
|
|
fi
|
|
|
|
if ! bash "$tmp_script" install --version "$XRAY_VERSION_PINNED" 2>/dev/null; then
|
|
log_error "Failed to install Xray"
|
|
rm -f "$tmp_script"
|
|
return 1
|
|
fi
|
|
rm -f "$tmp_script"
|
|
|
|
log_success "Xray ${XRAY_VERSION_PINNED} installed"
|
|
}
|
|
|
|
configure_xray_socks() {
|
|
local listen_port="${1:-443}"
|
|
log_info "Configuring Xray SOCKS5 proxy on port $listen_port..."
|
|
mkdir -p "$XRAY_CONFIG_DIR"
|
|
cat > "$XRAY_CONFIG_FILE" << EOF
|
|
{
|
|
"log": { "loglevel": "warning" },
|
|
"inbounds": [{
|
|
"tag": "socks-in",
|
|
"port": ${listen_port},
|
|
"listen": "127.0.0.1",
|
|
"protocol": "socks",
|
|
"settings": { "auth": "noauth", "udp": true },
|
|
"sniffing": { "enabled": true, "destOverride": ["http", "tls"] }
|
|
}],
|
|
"outbounds": [{ "tag": "direct", "protocol": "freedom", "settings": {} }]
|
|
}
|
|
EOF
|
|
chmod 644 "$XRAY_CONFIG_FILE"
|
|
log_success "Xray configured (SOCKS5 on 127.0.0.1:$listen_port)"
|
|
}
|
|
|
|
start_xray() {
|
|
log_info "Starting Xray service..."
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
systemctl stop xray 2>/dev/null || true
|
|
sleep 1
|
|
systemctl daemon-reload 2>/dev/null || true
|
|
systemctl enable xray 2>/dev/null || true
|
|
local attempt
|
|
for attempt in 1 2 3; do
|
|
systemctl start xray 2>/dev/null
|
|
sleep 2
|
|
if systemctl is-active --quiet xray; then
|
|
log_success "Xray started"
|
|
return 0
|
|
fi
|
|
[ "$attempt" -lt 3 ] && sleep 1
|
|
done
|
|
log_error "Failed to start Xray after 3 attempts"
|
|
return 1
|
|
else
|
|
if [ -x /usr/local/bin/xray ]; then
|
|
pkill -x xray 2>/dev/null || true
|
|
sleep 1
|
|
nohup /usr/local/bin/xray run -c "$XRAY_CONFIG_FILE" > /var/log/xray.log 2>&1 &
|
|
sleep 2
|
|
if pgrep -x xray &>/dev/null; then
|
|
log_success "Xray started"
|
|
return 0
|
|
fi
|
|
fi
|
|
log_error "Failed to start Xray"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
setup_xray_for_gfk() {
|
|
local target_port
|
|
target_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f2 | cut -d, -f1)
|
|
install_xray || return 1
|
|
configure_xray_socks "$target_port" || return 1
|
|
start_xray || return 1
|
|
}
|
|
|
|
_install_gfk_components() {
|
|
log_info "Installing GFK components..."
|
|
|
|
# Auto-detect server IP if not set (critical for server-side sniffer filter)
|
|
if [ -z "${GFK_SERVER_IP:-}" ] && [ "$ROLE" = "server" ]; then
|
|
GFK_SERVER_IP="${LOCAL_IP:-}"
|
|
[ -z "$GFK_SERVER_IP" ] && GFK_SERVER_IP=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}')
|
|
[ -z "$GFK_SERVER_IP" ] && GFK_SERVER_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
if [ -n "$GFK_SERVER_IP" ]; then
|
|
log_info "Auto-detected server IP: ${GFK_SERVER_IP}"
|
|
else
|
|
log_error "Could not detect server IP. Set GFK_SERVER_IP manually."
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Auto-generate auth code if not set
|
|
if [ -z "${GFK_AUTH_CODE:-}" ] || [ "$GFK_AUTH_CODE" = "not set" ]; then
|
|
GFK_AUTH_CODE=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 16 2>/dev/null || openssl rand -hex 8)
|
|
log_info "Generated GFK auth code: ${GFK_AUTH_CODE}"
|
|
fi
|
|
|
|
# Save settings with server IP and auth code
|
|
save_settings
|
|
|
|
# Create venv if needed
|
|
if [ ! -d "$INSTALL_DIR/venv" ]; then
|
|
python3 -m venv "$INSTALL_DIR/venv" || {
|
|
log_error "Failed to create Python venv"
|
|
return 1
|
|
}
|
|
fi
|
|
|
|
# Install Python packages
|
|
"$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade pip 2>/dev/null || true
|
|
"$INSTALL_DIR/venv/bin/pip" install --quiet scapy aioquic || {
|
|
log_error "Failed to install Python packages (scapy, aioquic)"
|
|
return 1
|
|
}
|
|
|
|
# Download GFK scripts (server and client)
|
|
download_gfk || return 1
|
|
|
|
# Generate TLS certificates for QUIC
|
|
generate_gfk_certs || return 1
|
|
|
|
# Generate parameters.py config
|
|
generate_gfk_config || return 1
|
|
|
|
# Setup Xray (install, configure, start)
|
|
setup_xray_for_gfk || return 1
|
|
|
|
log_success "GFK components installed"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Uninstall
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
uninstall_paqctl() {
|
|
echo ""
|
|
echo -e "${RED}${BOLD} UNINSTALL PAQCTL${NC}"
|
|
echo ""
|
|
echo -e " This will remove:"
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
echo " - GFW-knocker scripts and config"
|
|
echo " - microsocks binary"
|
|
else
|
|
echo " - paqet binary"
|
|
fi
|
|
echo " - All configuration files"
|
|
echo " - Systemd services"
|
|
echo " - Firewall rules"
|
|
echo " - Telegram service"
|
|
echo ""
|
|
read -p " Are you sure? [y/N]: " confirm < /dev/tty || true
|
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
|
log_info "Cancelled"
|
|
return 0
|
|
fi
|
|
|
|
# Stop services
|
|
stop_paqet
|
|
stop_telegram_service
|
|
|
|
# Remove ALL paqctl firewall rules (tagged with "paqctl" comment)
|
|
log_info "Removing firewall rules..."
|
|
_remove_all_paqctl_firewall_rules
|
|
# Also try the port-specific removal for backwards compatibility
|
|
_remove_firewall
|
|
|
|
# Remove systemd services
|
|
if command -v systemctl &>/dev/null; then
|
|
systemctl stop paqctl.service 2>/dev/null || true
|
|
systemctl disable paqctl.service 2>/dev/null || true
|
|
systemctl stop paqctl-telegram.service 2>/dev/null || true
|
|
systemctl disable paqctl-telegram.service 2>/dev/null || true
|
|
rm -f /etc/systemd/system/paqctl.service
|
|
rm -f /etc/systemd/system/paqctl-telegram.service
|
|
systemctl daemon-reload 2>/dev/null || true
|
|
fi
|
|
|
|
# Remove OpenRC/SysVinit
|
|
rm -f /etc/init.d/paqctl 2>/dev/null
|
|
|
|
# Remove symlink
|
|
rm -f /usr/local/bin/paqctl
|
|
|
|
# Remove install directory
|
|
rm -rf "${INSTALL_DIR:?}"
|
|
|
|
echo ""
|
|
log_success "paqctl has been completely uninstalled"
|
|
echo ""
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Help
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
show_help() {
|
|
echo ""
|
|
echo -e "${BOLD}paqctl${NC} - Paqet Manager v${VERSION}"
|
|
echo ""
|
|
echo -e "${BOLD}Usage:${NC} sudo paqctl <command>"
|
|
echo ""
|
|
echo -e "${BOLD}Commands:${NC}"
|
|
echo " menu Interactive menu (default)"
|
|
echo " status Show service status and configuration"
|
|
echo ""
|
|
echo -e "${BOLD}Backend Control (individual):${NC}"
|
|
echo " start-paqet Start paqet backend only"
|
|
echo " stop-paqet Stop paqet backend only"
|
|
echo " start-gfk Start GFK backend only"
|
|
echo " stop-gfk Stop GFK backend only"
|
|
echo " start-all Start both backends"
|
|
echo " stop-all Stop both backends"
|
|
echo ""
|
|
echo -e "${BOLD}Legacy (uses active backend):${NC}"
|
|
echo " start Start active backend"
|
|
echo " stop Stop active backend"
|
|
echo " restart Restart active backend"
|
|
echo ""
|
|
echo -e "${BOLD}Other:${NC}"
|
|
echo " logs View logs (live)"
|
|
echo " health Run health check diagnostics"
|
|
echo " update Check for and install updates"
|
|
echo " config Change configuration"
|
|
echo " secret Generate a new encryption key"
|
|
echo " firewall Manage iptables rules"
|
|
echo " backup Backup configuration"
|
|
echo " restore Restore from backup"
|
|
echo " telegram Telegram notification settings"
|
|
echo " rollback Roll back to a previous paqet version"
|
|
echo " ping Test connectivity (paqet ping)"
|
|
echo " dump Capture packets for diagnostics (paqet dump)"
|
|
echo " uninstall Remove paqctl completely"
|
|
echo " version Show version info"
|
|
echo " help Show this help"
|
|
echo ""
|
|
echo -e "${BOLD}Paqet:${NC} https://github.com/SamNet-dev/paqctl"
|
|
echo ""
|
|
}
|
|
|
|
show_version() {
|
|
echo ""
|
|
echo -e " paqctl version: ${BOLD}${VERSION}${NC}"
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
echo -e " backend: ${BOLD}gfw-knocker${NC}"
|
|
local py_ver; py_ver=$(python3 --version 2>/dev/null || echo "unknown")
|
|
echo -e " python: ${BOLD}${py_ver}${NC}"
|
|
else
|
|
echo -e " paqet version: ${BOLD}${PAQET_VERSION}${NC}"
|
|
local bin_ver
|
|
bin_ver=$("$INSTALL_DIR/bin/paqet" version 2>/dev/null || echo "unknown")
|
|
echo -e " paqet binary: ${BOLD}${bin_ver}${NC}"
|
|
if echo "$PAQET_VERSION" | grep -qi "alpha\|beta\|rc"; then
|
|
echo ""
|
|
echo -e " ${YELLOW}Note: paqet is in alpha phase — expect breaking changes between versions.${NC}"
|
|
fi
|
|
fi
|
|
echo ""
|
|
echo -e " ${DIM}paqctl by SamNet-dev: https://github.com/SamNet-dev/paqctl${NC}"
|
|
echo ""
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Paqet Diagnostic Tools (ping / dump)
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
run_ping() {
|
|
echo ""
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
log_warn "ping diagnostic is only available for paqet backend"
|
|
return 0
|
|
fi
|
|
if [ ! -x "$INSTALL_DIR/bin/paqet" ]; then
|
|
log_error "paqet binary not found"
|
|
return 1
|
|
fi
|
|
if [ ! -f "$INSTALL_DIR/config.yaml" ]; then
|
|
log_error "config.yaml not found. Run: sudo paqctl config"
|
|
return 1
|
|
fi
|
|
log_info "Running paqet ping (Ctrl+C to stop)..."
|
|
echo ""
|
|
"$INSTALL_DIR/bin/paqet" ping -c "$INSTALL_DIR/config.yaml" 2>&1 || true
|
|
echo ""
|
|
}
|
|
|
|
run_dump() {
|
|
echo ""
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
log_warn "dump diagnostic is only available for paqet backend"
|
|
return 0
|
|
fi
|
|
if [ ! -x "$INSTALL_DIR/bin/paqet" ]; then
|
|
log_error "paqet binary not found"
|
|
return 1
|
|
fi
|
|
if [ ! -f "$INSTALL_DIR/config.yaml" ]; then
|
|
log_error "config.yaml not found. Run: sudo paqctl config"
|
|
return 1
|
|
fi
|
|
log_info "Running paqet dump — packet capture diagnostic (Ctrl+C to stop)..."
|
|
echo -e "${DIM} This shows raw packets being sent and received by paqet.${NC}"
|
|
echo ""
|
|
"$INSTALL_DIR/bin/paqet" dump -c "$INSTALL_DIR/config.yaml" 2>&1 || true
|
|
echo ""
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Settings Menu
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
show_settings_menu() {
|
|
local redraw=true
|
|
while true; do
|
|
if [ "$redraw" = true ]; then
|
|
clear
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} SETTINGS & TOOLS${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
echo " 1. Change configuration"
|
|
echo " 2. Manage firewall rules"
|
|
echo " 3. Generate encryption key"
|
|
echo " 4. Backup configuration"
|
|
echo " 5. Restore from backup"
|
|
echo " 6. Health check"
|
|
echo " 7. Telegram notifications"
|
|
echo " 8. Version info"
|
|
echo " 9. Rollback to previous version"
|
|
echo " p. Ping test (connectivity)"
|
|
echo " d. Packet dump (diagnostics)"
|
|
echo " a. Install additional backend"
|
|
echo " s. Switch backend (current: ${BACKEND})"
|
|
echo " u. Uninstall"
|
|
echo ""
|
|
echo " b. Back to main menu"
|
|
echo ""
|
|
redraw=false
|
|
fi
|
|
|
|
read -p " Choice: " s_choice < /dev/tty || break
|
|
case "$s_choice" in
|
|
1) change_config; redraw=true ;;
|
|
2) show_firewall; redraw=true ;;
|
|
3) generate_secret; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;;
|
|
4) backup_config; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;;
|
|
5) restore_config; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;;
|
|
6) health_check; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;;
|
|
7) show_telegram_menu; redraw=true ;;
|
|
8) show_version; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;;
|
|
9) rollback_paqet; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;;
|
|
p|P) run_ping; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;;
|
|
d|D) run_dump; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;;
|
|
a|A) install_additional_backend; redraw=true ;;
|
|
s|S) switch_backend; redraw=true ;;
|
|
u|U) uninstall_paqctl; exit 0 ;;
|
|
b|B) return ;;
|
|
"") ;;
|
|
*) echo -e " ${RED}Invalid choice${NC}" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Info Menu
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
show_info_menu() {
|
|
local redraw=true
|
|
while true; do
|
|
if [ "$redraw" = true ]; then
|
|
clear
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} INFO & HELP${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
echo " 1. How Paqet Works"
|
|
echo " 2. Server vs Client Mode"
|
|
echo " 3. Firewall Rules Explained"
|
|
echo " 4. Troubleshooting"
|
|
echo " 5. About"
|
|
echo ""
|
|
echo " b. Back"
|
|
echo ""
|
|
redraw=false
|
|
fi
|
|
|
|
read -p " Choice: " i_choice < /dev/tty || break
|
|
case "$i_choice" in
|
|
1)
|
|
clear
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} HOW PAQET WORKS${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
echo -e " ${BOLD}Overview:${NC}"
|
|
echo " Paqet is a bidirectional packet-level proxy written in Go."
|
|
echo " Unlike traditional proxies (Shadowsocks, V2Ray, etc.) that"
|
|
echo " operate at the application or transport layer, paqet works"
|
|
echo " at the raw socket level — below the OS network stack."
|
|
echo ""
|
|
echo -e " ${BOLD}How it works step by step:${NC}"
|
|
echo ""
|
|
echo " 1. PACKET CRAFTING"
|
|
echo " Paqet uses gopacket + libpcap to craft TCP packets"
|
|
echo " directly, bypassing the kernel's TCP/IP stack entirely."
|
|
echo " This means the OS doesn't even know there's a connection."
|
|
echo ""
|
|
echo " 2. KCP ENCRYPTED TRANSPORT"
|
|
echo " All traffic between client and server is encrypted using"
|
|
echo " the KCP protocol with AES symmetric key encryption."
|
|
echo " KCP provides reliable, ordered delivery over raw packets"
|
|
echo " with built-in error correction and retransmission."
|
|
echo ""
|
|
echo " 3. CONNECTION MULTIPLEXING"
|
|
echo " Multiple connections are multiplexed over a single KCP"
|
|
echo " session using smux, reducing overhead and improving"
|
|
echo " performance for concurrent requests."
|
|
echo ""
|
|
echo " 4. FIREWALL BYPASS"
|
|
echo " Because it operates below the OS network stack, paqet"
|
|
echo " bypasses traditional firewalls (ufw, firewalld) and"
|
|
echo " kernel-level connection tracking (conntrack). The OS"
|
|
echo " firewall never sees the traffic as a 'connection'."
|
|
echo ""
|
|
echo " 5. SOCKS5 PROXY (Client)"
|
|
echo " On the client side, paqet exposes a standard SOCKS5"
|
|
echo " proxy that any application can use. Traffic enters"
|
|
echo " the SOCKS5 port, gets encrypted and sent via raw"
|
|
echo " packets to the server, which forwards it to the"
|
|
echo " destination on the open internet."
|
|
echo ""
|
|
echo -e " ${DIM}Technical stack: Go, gopacket, libpcap, KCP, smux, AES${NC}"
|
|
echo -e " ${DIM}Project: https://github.com/SamNet-dev/paqctl${NC}"
|
|
echo ""
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
redraw=true
|
|
;;
|
|
2)
|
|
clear
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} SERVER VS CLIENT MODE${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
echo -e " ${GREEN}${BOLD}SERVER MODE${NC}"
|
|
echo -e " ${DIM}─────────────────────────────────────────────${NC}"
|
|
echo " The server is the exit node. It receives encrypted raw"
|
|
echo " packets from clients, decrypts them, and forwards the"
|
|
echo " traffic to the open internet. Responses are encrypted"
|
|
echo " and sent back to the client."
|
|
echo ""
|
|
echo " Requirements:"
|
|
echo " - A server with a public IP address"
|
|
echo " - Root access (raw sockets need it)"
|
|
echo " - libpcap installed"
|
|
echo " - iptables NOTRACK + RST DROP rules (auto-managed)"
|
|
echo " - An open port (paqctl manages firewall rules, but you"
|
|
echo " may need to allow the port in your cloud provider's"
|
|
echo " security group / network firewall)"
|
|
echo ""
|
|
echo " After setup, share with your clients:"
|
|
echo " - Server IP and port (e.g. 1.2.3.4:8443)"
|
|
echo " - Encryption key (generated during setup)"
|
|
echo ""
|
|
echo -e " ${CYAN}${BOLD}CLIENT MODE${NC}"
|
|
echo -e " ${DIM}─────────────────────────────────────────────${NC}"
|
|
echo " The client connects to a paqet server and provides a"
|
|
echo " local SOCKS5 proxy. Applications on your machine connect"
|
|
echo " to the SOCKS5 port, and traffic is tunneled through"
|
|
echo " paqet's encrypted raw-socket connection to the server."
|
|
echo ""
|
|
echo " Requirements:"
|
|
echo " - Server IP:PORT and encryption key from the server admin"
|
|
echo " - Root access (raw sockets need it)"
|
|
echo " - libpcap installed"
|
|
echo ""
|
|
echo " Usage after setup:"
|
|
echo " Browser: Set SOCKS5 proxy to 127.0.0.1:1080"
|
|
echo " curl: curl --proxy socks5h://127.0.0.1:1080 URL"
|
|
echo " System: Configure system proxy to SOCKS5 127.0.0.1:1080"
|
|
echo ""
|
|
echo -e " ${BOLD}Data flow:${NC}"
|
|
echo " App -> SOCKS5(:1080) -> paqet client -> raw packets"
|
|
echo " -> internet -> paqet server -> destination website"
|
|
echo ""
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
redraw=true
|
|
;;
|
|
3)
|
|
clear
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} FIREWALL RULES EXPLAINED${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
echo " Paqet requires specific iptables rules on the SERVER."
|
|
echo " These rules are needed because paqet crafts raw TCP"
|
|
echo " packets, and without them the kernel interferes."
|
|
echo ""
|
|
echo -e " ${BOLD}Rule 1: PREROUTING NOTRACK${NC}"
|
|
echo " iptables -t raw -A PREROUTING -p tcp --dport PORT -j NOTRACK"
|
|
echo ""
|
|
echo " WHY: Tells the kernel's connection tracker (conntrack) to"
|
|
echo " ignore incoming packets on the paqet port. Without this,"
|
|
echo " conntrack tries to match packets to connections it doesn't"
|
|
echo " know about and may drop them."
|
|
echo ""
|
|
echo -e " ${BOLD}Rule 2: OUTPUT NOTRACK${NC}"
|
|
echo " iptables -t raw -A OUTPUT -p tcp --sport PORT -j NOTRACK"
|
|
echo ""
|
|
echo " WHY: Same as above but for outgoing packets. Prevents"
|
|
echo " conntrack from tracking paqet's outbound raw packets."
|
|
echo ""
|
|
echo -e " ${BOLD}Rule 3: RST DROP${NC}"
|
|
echo " iptables -t mangle -A OUTPUT -p tcp --sport PORT"
|
|
echo " --tcp-flags RST RST -j DROP"
|
|
echo ""
|
|
echo " WHY: When the kernel sees incoming TCP SYN packets on a"
|
|
echo " port with no listening socket, it sends TCP RST (reset)"
|
|
echo " back. This would kill paqet connections. This rule drops"
|
|
echo " those RST packets so paqet can handle them instead."
|
|
echo ""
|
|
echo -e " ${DIM}These rules are auto-managed by paqctl:${NC}"
|
|
echo -e " ${DIM} - Applied on service start (ExecStartPre)${NC}"
|
|
echo -e " ${DIM} - Removed on service stop (ExecStopPost)${NC}"
|
|
echo -e " ${DIM} - Persisted across reboots when possible${NC}"
|
|
echo ""
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
redraw=true
|
|
;;
|
|
4)
|
|
clear
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} TROUBLESHOOTING${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
echo -e " ${BOLD}Service won't start:${NC}"
|
|
echo " 1. Check logs: sudo paqctl logs"
|
|
echo " 2. Run health check: sudo paqctl health"
|
|
echo " 3. Verify libpcap: ldconfig -p | grep libpcap"
|
|
echo " 4. Check config: cat /opt/paqctl/config.yaml"
|
|
echo " 5. Test binary: sudo /opt/paqctl/bin/paqet version"
|
|
echo ""
|
|
echo -e " ${BOLD}Client can't connect to server:${NC}"
|
|
echo " 1. Verify server IP and port are correct"
|
|
echo " 2. Check encryption key matches exactly"
|
|
echo " 3. Ensure server iptables rules are active:"
|
|
echo " sudo paqctl firewall (on server)"
|
|
echo " 4. Check cloud security group allows the port"
|
|
echo " 5. Test raw connectivity:"
|
|
echo " sudo /opt/paqctl/bin/paqet ping -c /opt/paqctl/config.yaml"
|
|
echo " 6. Run packet dump to see what's happening:"
|
|
echo " sudo /opt/paqctl/bin/paqet dump -c /opt/paqctl/config.yaml"
|
|
echo ""
|
|
echo -e " ${BOLD}SOCKS5 not working (client side):${NC}"
|
|
echo " 1. Verify client is running: sudo paqctl status"
|
|
echo " 2. Test the proxy directly:"
|
|
echo " curl -v --proxy socks5h://127.0.0.1:1080 https://httpbin.org/ip"
|
|
echo " 3. Check SOCKS port is listening:"
|
|
echo " ss -tlnp | grep 1080"
|
|
echo " 4. Check if paqet output shows errors:"
|
|
echo " sudo paqctl logs"
|
|
echo ""
|
|
echo -e " ${BOLD}High CPU / Memory:${NC}"
|
|
echo " 1. Check process stats: sudo paqctl status"
|
|
echo " 2. Restart the service: sudo paqctl restart"
|
|
echo " 3. Check for latest version: sudo paqctl update"
|
|
echo ""
|
|
echo -e " ${BOLD}After system reboot:${NC}"
|
|
echo " 1. paqctl auto-starts via systemd (check: systemctl status paqctl)"
|
|
echo " 2. iptables rules are re-applied by ExecStartPre"
|
|
echo " 3. If rules are missing: sudo paqctl firewall -> Apply"
|
|
echo ""
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
redraw=true
|
|
;;
|
|
5)
|
|
clear
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} ABOUT${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
echo -e " ${BOLD}paqctl v${VERSION}${NC} - Paqet Management Tool"
|
|
echo ""
|
|
echo -e " ${CYAN}── Paqet ──${NC}"
|
|
echo ""
|
|
echo -e " ${BOLD}Creator:${NC} hanselime"
|
|
echo -e " ${BOLD}Repository:${NC} https://github.com/SamNet-dev/paqctl"
|
|
echo -e " ${BOLD}License:${NC} MIT"
|
|
echo -e " ${BOLD}Language:${NC} Go"
|
|
echo -e " ${BOLD}Contact:${NC} Signal @hanselime.11"
|
|
echo ""
|
|
echo " Paqet is a bidirectional packet-level proxy that uses"
|
|
echo " raw sockets (gopacket + libpcap) with KCP encrypted"
|
|
echo " transport. It operates below the OS TCP/IP stack to"
|
|
echo " bypass firewalls and deep packet inspection."
|
|
echo ""
|
|
echo " Features:"
|
|
echo " - Raw TCP packet crafting via gopacket"
|
|
echo " - KCP + AES symmetric encryption"
|
|
echo " - SOCKS5 proxy for dynamic connections"
|
|
echo " - Connection multiplexing via smux"
|
|
echo " - Cross-platform (Linux, macOS, Windows)"
|
|
echo ""
|
|
echo -e " ${CYAN}── paqctl Management Tool ──${NC}"
|
|
echo ""
|
|
echo -e " ${BOLD}Built by:${NC} SamNet-dev"
|
|
echo -e " ${BOLD}Repository:${NC} https://github.com/SamNet-dev/paqctl"
|
|
echo -e " ${BOLD}License:${NC} MIT"
|
|
echo ""
|
|
echo " paqctl provides one-click installation, configuration,"
|
|
echo " service management, auto-updates, health monitoring,"
|
|
echo " and Telegram notifications for paqet."
|
|
echo ""
|
|
echo -e " ${DIM}Original paqet by hanselime, improved by SamNet.${NC}"
|
|
echo ""
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
redraw=true
|
|
;;
|
|
b|B) return ;;
|
|
"") ;;
|
|
*) echo -e " ${RED}Invalid choice${NC}" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Connection Info Display
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
show_connection_info() {
|
|
clear
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} CLIENT CONNECTION INFO${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
_load_settings
|
|
|
|
local local_ip
|
|
local_ip=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}' || hostname -I 2>/dev/null | awk '{print $1}' || echo "unknown")
|
|
|
|
local paqet_installed=false
|
|
local gfk_installed=false
|
|
[ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
|
|
if [ "$ROLE" = "server" ]; then
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
|
|
else
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
|
|
fi
|
|
|
|
if [ "$paqet_installed" = true ]; then
|
|
echo -e " ${GREEN}${BOLD}━━━ PAQET ━━━${NC}"
|
|
echo ""
|
|
local paqet_port="${LISTEN_PORT:-8443}"
|
|
local paqet_key="${ENCRYPTION_KEY:-not set}"
|
|
# Try to get key from config if not in settings
|
|
if [ "$paqet_key" = "not set" ] && [ -f "$INSTALL_DIR/config.yaml" ]; then
|
|
paqet_key=$(grep -E "^key:" "$INSTALL_DIR/config.yaml" 2>/dev/null | awk '{print $2}' | tr -d '"' || echo "not set")
|
|
fi
|
|
echo -e " ${YELLOW}╔═══════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e " ${YELLOW}║${NC} Server: ${BOLD}${local_ip}:${paqet_port}${NC}"
|
|
echo -e " ${YELLOW}║${NC} Key: ${BOLD}${paqet_key}${NC}"
|
|
echo -e " ${YELLOW}╚═══════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
echo -e " ${DIM}Client proxy: 127.0.0.1:1080 (SOCKS5)${NC}"
|
|
echo ""
|
|
fi
|
|
|
|
if [ "$gfk_installed" = true ]; then
|
|
echo -e " ${MAGENTA}${BOLD}━━━ GFW-KNOCKER ━━━${NC}"
|
|
echo ""
|
|
local gfk_ip="${GFK_SERVER_IP:-$local_ip}"
|
|
local gfk_auth="${GFK_AUTH_CODE:-not set}"
|
|
local gfk_mappings="${GFK_PORT_MAPPINGS:-14000:443}"
|
|
echo -e " ${YELLOW}╔═══════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e " ${YELLOW}║${NC} Server IP: ${BOLD}${gfk_ip}${NC}"
|
|
echo -e " ${YELLOW}║${NC} Auth Code: ${BOLD}${gfk_auth}${NC}"
|
|
echo -e " ${YELLOW}║${NC} Mappings: ${BOLD}${gfk_mappings}${NC}"
|
|
echo -e " ${YELLOW}╚═══════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
echo -e " ${DIM}VIO port: ${GFK_VIO_PORT:-45000} | QUIC port: ${GFK_QUIC_PORT:-25000}${NC}"
|
|
echo -e " ${DIM}Client proxy: 127.0.0.1:14000 (SOCKS5)${NC}"
|
|
echo ""
|
|
fi
|
|
|
|
if [ "$paqet_installed" = false ] && [ "$gfk_installed" = false ]; then
|
|
echo -e " ${YELLOW}No backends installed yet.${NC}"
|
|
echo ""
|
|
echo " Run 'sudo paqctl menu' and select 'Settings & Tools'"
|
|
echo " to install a backend."
|
|
echo ""
|
|
fi
|
|
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Interactive Menu
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
show_menu() {
|
|
# Auto-fix systemd service if in failed state
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
local svc_state=$(systemctl is-active paqctl.service 2>/dev/null)
|
|
if [ "$svc_state" = "failed" ]; then
|
|
systemctl reset-failed paqctl.service 2>/dev/null || true
|
|
fi
|
|
fi
|
|
|
|
# Reload settings
|
|
_load_settings
|
|
|
|
local paqet_installed=false
|
|
local gfk_installed=false
|
|
local redraw=true
|
|
|
|
while true; do
|
|
if [ "$redraw" = true ]; then
|
|
# Re-check what's installed each redraw
|
|
paqet_installed=false
|
|
gfk_installed=false
|
|
[ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
|
|
if [ "$ROLE" = "server" ]; then
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
|
|
else
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
|
|
fi
|
|
|
|
clear
|
|
print_header
|
|
|
|
# Status line showing both backends
|
|
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
|
|
echo -e " ${BOLD}BACKEND STATUS${NC} (Role: ${ROLE})"
|
|
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
|
|
|
|
# Paqet status
|
|
if [ "$paqet_installed" = true ]; then
|
|
if is_paqet_running; then
|
|
echo -e " Paqet: ${GREEN}● Running${NC} | Port: ${LISTEN_PORT:-8443} | SOCKS5: 127.0.0.1:${SOCKS_PORT:-1080}"
|
|
else
|
|
echo -e " Paqet: ${RED}○ Stopped${NC} | Port: ${LISTEN_PORT:-8443}"
|
|
fi
|
|
else
|
|
echo -e " Paqet: ${DIM}not installed${NC}"
|
|
fi
|
|
|
|
# GFK status
|
|
if [ "$gfk_installed" = true ]; then
|
|
if is_gfk_running; then
|
|
echo -e " GFK: ${GREEN}● Running${NC} | VIO: ${GFK_VIO_PORT:-45000} | SOCKS5: 127.0.0.1:14000"
|
|
else
|
|
echo -e " GFK: ${RED}○ Stopped${NC} | VIO: ${GFK_VIO_PORT:-45000}"
|
|
fi
|
|
else
|
|
echo -e " GFK: ${DIM}not installed${NC}"
|
|
fi
|
|
|
|
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
|
|
|
|
echo ""
|
|
echo -e " ${CYAN}MAIN MENU${NC}"
|
|
echo ""
|
|
echo " 1. View status"
|
|
echo " 2. View logs"
|
|
echo " 3. Health check"
|
|
echo ""
|
|
|
|
# Paqet controls
|
|
if [ "$paqet_installed" = true ]; then
|
|
if is_paqet_running; then
|
|
echo -e " p. ${RED}Stop${NC} Paqet"
|
|
else
|
|
echo -e " p. ${GREEN}Start${NC} Paqet"
|
|
fi
|
|
fi
|
|
|
|
# GFK controls
|
|
if [ "$gfk_installed" = true ]; then
|
|
if is_gfk_running; then
|
|
echo -e " g. ${RED}Stop${NC} GFK"
|
|
else
|
|
echo -e " g. ${GREEN}Start${NC} GFK"
|
|
fi
|
|
fi
|
|
|
|
# Start/Stop all
|
|
if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then
|
|
echo ""
|
|
if is_paqet_running && is_gfk_running; then
|
|
echo -e " a. ${RED}Stop ALL${NC} backends"
|
|
elif ! is_paqet_running && ! is_gfk_running; then
|
|
echo -e " a. ${GREEN}Start ALL${NC} backends"
|
|
else
|
|
echo " a. Toggle ALL backends"
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo " 8. Settings & Tools"
|
|
echo -e " ${YELLOW}c. Connection Info${NC}"
|
|
echo " i. Info & Help"
|
|
echo -e " ${RED}u. Uninstall${NC}"
|
|
echo " 0. Exit"
|
|
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
|
|
echo ""
|
|
redraw=false
|
|
fi
|
|
|
|
echo -n " Select option: "
|
|
if ! read choice < /dev/tty 2>/dev/null; then
|
|
log_error "Cannot read input. If piped, run: sudo paqctl menu"
|
|
exit 1
|
|
fi
|
|
|
|
case "$choice" in
|
|
1) show_status; read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true; redraw=true ;;
|
|
2) show_logs; redraw=true ;;
|
|
3) health_check; read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true; redraw=true ;;
|
|
p|P)
|
|
if [ "$paqet_installed" = true ]; then
|
|
if is_paqet_running; then
|
|
stop_paqet_backend
|
|
else
|
|
start_paqet_backend
|
|
fi
|
|
read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true
|
|
else
|
|
echo -e " ${YELLOW}Paqet not installed${NC}"
|
|
fi
|
|
redraw=true
|
|
;;
|
|
g|G)
|
|
if [ "$gfk_installed" = true ]; then
|
|
if is_gfk_running; then
|
|
stop_gfk_backend
|
|
else
|
|
start_gfk_backend
|
|
fi
|
|
read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true
|
|
else
|
|
echo -e " ${YELLOW}GFK not installed${NC}"
|
|
fi
|
|
redraw=true
|
|
;;
|
|
a|A)
|
|
if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then
|
|
if is_paqet_running && is_gfk_running; then
|
|
# Stop all
|
|
stop_paqet_backend
|
|
stop_gfk_backend
|
|
elif ! is_paqet_running && ! is_gfk_running; then
|
|
# Start all
|
|
start_paqet_backend
|
|
start_gfk_backend
|
|
else
|
|
# Mixed state - ask user
|
|
echo ""
|
|
echo " 1. Start all backends"
|
|
echo " 2. Stop all backends"
|
|
echo -n " Choice: "
|
|
read subchoice < /dev/tty || true
|
|
case "$subchoice" in
|
|
1)
|
|
[ "$paqet_installed" = true ] && ! is_paqet_running && start_paqet_backend
|
|
[ "$gfk_installed" = true ] && ! is_gfk_running && start_gfk_backend
|
|
;;
|
|
2)
|
|
is_paqet_running && stop_paqet_backend
|
|
is_gfk_running && stop_gfk_backend
|
|
;;
|
|
esac
|
|
fi
|
|
read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true
|
|
fi
|
|
redraw=true
|
|
;;
|
|
8) show_settings_menu; redraw=true ;;
|
|
c|C) show_connection_info; redraw=true ;;
|
|
i|I) show_info_menu; redraw=true ;;
|
|
u|U) uninstall_paqctl; exit 0 ;;
|
|
0) echo " Exiting."; exit 0 ;;
|
|
"") ;;
|
|
*) echo -e " ${RED}Invalid choice: ${NC}${YELLOW}$choice${NC}" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# CLI Command Router
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
case "${1:-menu}" in
|
|
status) show_status ;;
|
|
start) start_paqet ;;
|
|
stop) stop_paqet ;;
|
|
restart) restart_paqet ;;
|
|
start-paqet) start_paqet_backend ;;
|
|
stop-paqet) stop_paqet_backend ;;
|
|
start-gfk) start_gfk_backend ;;
|
|
stop-gfk) stop_gfk_backend ;;
|
|
start-all) start_paqet_backend; start_gfk_backend ;;
|
|
stop-all) stop_paqet_backend; stop_gfk_backend ;;
|
|
logs) show_logs ;;
|
|
health) health_check ;;
|
|
update) update_paqet ;;
|
|
config) change_config ;;
|
|
secret) generate_secret ;;
|
|
firewall) show_firewall ;;
|
|
rollback) rollback_paqet ;;
|
|
ping) run_ping ;;
|
|
dump) run_dump ;;
|
|
backup) backup_config ;;
|
|
restore) restore_config ;;
|
|
telegram) show_telegram_menu ;;
|
|
uninstall) uninstall_paqctl ;;
|
|
version) show_version ;;
|
|
help|--help|-h) show_help ;;
|
|
menu) show_menu ;;
|
|
_apply-firewall) _apply_firewall ;;
|
|
_remove-firewall) _remove_firewall ;;
|
|
*)
|
|
echo -e "${RED}Unknown command: $1${NC}"
|
|
echo "Run 'sudo paqctl help' for usage."
|
|
exit 1
|
|
;;
|
|
esac
|
|
MANAGEMENT
|
|
|
|
# Replace placeholder
|
|
sed "s#REPLACE_ME_INSTALL_DIR#$INSTALL_DIR#g" "$tmp_script" > "$tmp_script.sed" && mv "$tmp_script.sed" "$tmp_script"
|
|
|
|
if ! chmod +x "$tmp_script"; then
|
|
log_error "Failed to make management script executable"
|
|
rm -f "$tmp_script"
|
|
return 1
|
|
fi
|
|
if ! mv -f "$tmp_script" "$INSTALL_DIR/paqctl"; then
|
|
log_error "Failed to install management script"
|
|
rm -f "$tmp_script"
|
|
return 1
|
|
fi
|
|
|
|
# Create symlink
|
|
rm -f /usr/local/bin/paqctl 2>/dev/null
|
|
if ! ln -sf "$INSTALL_DIR/paqctl" /usr/local/bin/paqctl; then
|
|
log_warn "Failed to create symlink /usr/local/bin/paqctl"
|
|
fi
|
|
|
|
log_success "Management script installed → /usr/local/bin/paqctl"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Main Installation Flow
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
_load_settings() {
|
|
[ -f "$INSTALL_DIR/settings.conf" ] || return 0
|
|
# Safe settings loading without eval
|
|
while IFS='=' read -r key value; do
|
|
[[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue
|
|
value="${value#\"}"; value="${value%\"}"
|
|
# Skip values with dangerous shell characters
|
|
[[ "$value" =~ [\`\$\(] ]] && continue
|
|
case "$key" in
|
|
BACKEND) BACKEND="$value" ;;
|
|
ROLE) ROLE="$value" ;;
|
|
PAQET_VERSION) PAQET_VERSION="$value" ;;
|
|
PAQCTL_VERSION) PAQCTL_VERSION="$value" ;;
|
|
LISTEN_PORT) [[ "$value" =~ ^[0-9]*$ ]] && LISTEN_PORT="$value" ;;
|
|
SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && SOCKS_PORT="$value" ;;
|
|
INTERFACE) INTERFACE="$value" ;;
|
|
LOCAL_IP) LOCAL_IP="$value" ;;
|
|
GATEWAY_MAC) GATEWAY_MAC="$value" ;;
|
|
ENCRYPTION_KEY) ENCRYPTION_KEY="$value" ;;
|
|
REMOTE_SERVER) REMOTE_SERVER="$value" ;;
|
|
GFK_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_PORT="$value" ;;
|
|
GFK_VIO_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_CLIENT_PORT="$value" ;;
|
|
GFK_QUIC_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_PORT="$value" ;;
|
|
GFK_QUIC_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_CLIENT_PORT="$value" ;;
|
|
GFK_AUTH_CODE) GFK_AUTH_CODE="$value" ;;
|
|
GFK_PORT_MAPPINGS) GFK_PORT_MAPPINGS="$value" ;;
|
|
MICROSOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && MICROSOCKS_PORT="$value" ;;
|
|
GFK_SERVER_IP) GFK_SERVER_IP="$value" ;;
|
|
TELEGRAM_BOT_TOKEN) TELEGRAM_BOT_TOKEN="$value" ;;
|
|
TELEGRAM_CHAT_ID) TELEGRAM_CHAT_ID="$value" ;;
|
|
TELEGRAM_INTERVAL) [[ "$value" =~ ^[0-9]+$ ]] && TELEGRAM_INTERVAL="$value" ;;
|
|
TELEGRAM_ENABLED) TELEGRAM_ENABLED="$value" ;;
|
|
TELEGRAM_ALERTS_ENABLED) TELEGRAM_ALERTS_ENABLED="$value" ;;
|
|
TELEGRAM_DAILY_SUMMARY) TELEGRAM_DAILY_SUMMARY="$value" ;;
|
|
TELEGRAM_WEEKLY_SUMMARY) TELEGRAM_WEEKLY_SUMMARY="$value" ;;
|
|
TELEGRAM_SERVER_LABEL) TELEGRAM_SERVER_LABEL="$value" ;;
|
|
TELEGRAM_START_HOUR) [[ "$value" =~ ^[0-9]+$ ]] && TELEGRAM_START_HOUR="$value" ;;
|
|
esac
|
|
done < <(grep '^[A-Z_][A-Z_0-9]*=' "$INSTALL_DIR/settings.conf")
|
|
}
|
|
|
|
# Handle --update-components flag (called during self-update)
|
|
if [ "${1:-}" = "--update-components" ]; then
|
|
INSTALL_DIR="${INSTALL_DIR:-/opt/paqctl}"
|
|
_load_settings
|
|
create_management_script
|
|
exit 0
|
|
fi
|
|
|
|
main() {
|
|
check_root
|
|
print_header
|
|
|
|
# Check if already installed
|
|
if [ -f "$INSTALL_DIR/settings.conf" ] && { [ -x "$INSTALL_DIR/bin/paqet" ] || [ -f "$GFK_DIR/mainserver.py" ]; }; then
|
|
_load_settings
|
|
log_info "paqctl is already installed (backend: ${BACKEND:-paqet})."
|
|
echo ""
|
|
echo " 1. Reinstall / Reconfigure"
|
|
echo " 2. Open menu (same as: sudo paqctl menu)"
|
|
echo " 3. Exit"
|
|
echo ""
|
|
read -p " Choice [1-3]: " choice < /dev/tty || true
|
|
case "$choice" in
|
|
1) log_info "Reinstalling..." ;;
|
|
2) exec /usr/local/bin/paqctl menu ;;
|
|
*) exit 0 ;;
|
|
esac
|
|
fi
|
|
|
|
# Step 1: Detect OS
|
|
log_info "Step 1/7: Detecting operating system..."
|
|
detect_os
|
|
echo ""
|
|
|
|
# Step 2: Install dependencies
|
|
log_info "Step 2/7: Installing dependencies..."
|
|
check_dependencies
|
|
echo ""
|
|
|
|
# Step 3: Configuration wizard (determines backend + role + config)
|
|
log_info "Step 3/7: Configuration..."
|
|
run_config_wizard
|
|
echo ""
|
|
|
|
# Step 4: Backend-specific dependencies and download
|
|
log_info "Step 4/7: Setting up ${BACKEND} backend..."
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
install_python_deps
|
|
download_gfk
|
|
generate_gfk_certs
|
|
if [ "$ROLE" = "server" ]; then
|
|
# Install Xray to provide SOCKS5 proxy on the target port
|
|
setup_xray_for_gfk
|
|
elif [ "$ROLE" = "client" ]; then
|
|
install_microsocks
|
|
create_gfk_client_wrapper
|
|
fi
|
|
PAQET_VERSION="$GFK_VERSION_PINNED"
|
|
log_info "Using GFK ${PAQET_VERSION} (pinned for stability)"
|
|
else
|
|
# Use pinned version for stability (update command can get latest)
|
|
PAQET_VERSION="$PAQET_VERSION_PINNED"
|
|
log_info "Installing paqet ${PAQET_VERSION} (pinned for stability)"
|
|
download_paqet "$PAQET_VERSION"
|
|
fi
|
|
echo ""
|
|
|
|
# Step 5: Apply firewall rules
|
|
log_info "Step 5/7: Firewall setup..."
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
if [ "$ROLE" = "server" ]; then
|
|
if ! command -v iptables &>/dev/null; then
|
|
log_warn "iptables not found - firewall rules cannot be applied"
|
|
else
|
|
local _vio_port="${GFK_VIO_PORT:-45000}"
|
|
log_info "Blocking VIO TCP port $_vio_port (raw socket handles it)..."
|
|
# Use same tagging as _apply_firewall for consistency
|
|
# Drop incoming TCP on VIO port (scapy sniffer handles it)
|
|
iptables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
iptables -A INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
log_warn "Failed to add VIO INPUT DROP rule"
|
|
# Drop outgoing RST packets on VIO port (prevents kernel from interfering)
|
|
iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
iptables -A OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
log_warn "Failed to add VIO RST DROP rule"
|
|
# IPv6 rules
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
ip6tables -A INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
|
|
ip6tables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
ip6tables -A OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
|
|
fi
|
|
fi
|
|
else
|
|
log_info "Client mode - no firewall rules needed"
|
|
fi
|
|
elif [ "$ROLE" = "server" ]; then
|
|
apply_iptables_rules "$LISTEN_PORT"
|
|
else
|
|
log_info "Client mode - no firewall rules needed"
|
|
fi
|
|
echo ""
|
|
|
|
# Step 6: Create service + management script
|
|
log_info "Step 6/7: Setting up service..."
|
|
if ! mkdir -p "$INSTALL_DIR/bin" "$BACKUP_DIR"; then
|
|
log_error "Failed to create installation directories"
|
|
exit 1
|
|
fi
|
|
create_management_script
|
|
setup_service
|
|
setup_logrotate
|
|
# Save settings to persist version and config
|
|
save_settings
|
|
echo ""
|
|
|
|
# Step 7: Start the service
|
|
log_info "Step 7/7: Starting ${BACKEND}..."
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
systemctl start paqctl.service 2>/dev/null
|
|
fi
|
|
|
|
sleep 2
|
|
|
|
# Final summary
|
|
echo ""
|
|
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${GREEN}${BOLD} INSTALLATION COMPLETE!${NC}"
|
|
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
echo -e " Backend: ${BOLD}${BACKEND}${NC}"
|
|
echo -e " Role: ${BOLD}${ROLE}${NC}"
|
|
echo -e " Version: ${BOLD}${PAQET_VERSION}${NC}"
|
|
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
if [ "$ROLE" = "server" ]; then
|
|
local _xray_port
|
|
_xray_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f2 | cut -d, -f1)
|
|
echo -e " VIO port: ${BOLD}${GFK_VIO_PORT}${NC}"
|
|
echo -e " QUIC port: ${BOLD}${GFK_QUIC_PORT}${NC}"
|
|
echo -e " Xray: ${BOLD}127.0.0.1:${_xray_port} (SOCKS5)${NC}"
|
|
echo ""
|
|
echo -e " ${GREEN}✓ Xray SOCKS5 proxy installed and running${NC}"
|
|
echo ""
|
|
echo -e "${YELLOW}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${YELLOW}║ ${BOLD}CLIENT CONNECTION INFO - SAVE THIS!${NC}${YELLOW} ║${NC}"
|
|
echo -e "${YELLOW}╠═══════════════════════════════════════════════════════════════╣${NC}"
|
|
echo -e "${YELLOW}║${NC} Server IP: ${BOLD}${GFK_SERVER_IP}${NC}"
|
|
echo -e "${YELLOW}║${NC} Auth Code: ${BOLD}${GFK_AUTH_CODE}${NC}"
|
|
echo -e "${YELLOW}║${NC} Mappings: ${BOLD}${GFK_PORT_MAPPINGS}${NC}"
|
|
echo -e "${YELLOW}╚═══════════════════════════════════════════════════════════════╝${NC}"
|
|
else
|
|
echo -e " Server: ${BOLD}${GFK_SERVER_IP}${NC}"
|
|
echo -e " SOCKS5: ${BOLD}127.0.0.1:${MICROSOCKS_PORT}${NC}"
|
|
echo ""
|
|
echo -e " ${YELLOW}Test your proxy:${NC}"
|
|
echo -e " ${BOLD} curl --proxy socks5h://127.0.0.1:${MICROSOCKS_PORT} https://httpbin.org/ip${NC}"
|
|
fi
|
|
elif [ "$ROLE" = "server" ]; then
|
|
echo -e " Port: ${BOLD}${LISTEN_PORT}${NC}"
|
|
echo ""
|
|
echo -e "${YELLOW}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${YELLOW}║ ${BOLD}CLIENT CONNECTION INFO - SAVE THIS!${NC}${YELLOW} ║${NC}"
|
|
echo -e "${YELLOW}╠═══════════════════════════════════════════════════════════════╣${NC}"
|
|
echo -e "${YELLOW}║${NC} Server: ${BOLD}${LOCAL_IP}:${LISTEN_PORT}${NC}"
|
|
echo -e "${YELLOW}║${NC} Key: ${BOLD}${ENCRYPTION_KEY}${NC}"
|
|
echo -e "${YELLOW}╚═══════════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
echo -e " ${CYAN}Key also saved in: ${INSTALL_DIR}/config.yaml${NC}"
|
|
else
|
|
echo -e " Server: ${BOLD}${REMOTE_SERVER}${NC}"
|
|
echo -e " SOCKS5: ${BOLD}127.0.0.1:${SOCKS_PORT}${NC}"
|
|
echo ""
|
|
echo -e " ${YELLOW}Test your proxy:${NC}"
|
|
echo -e " ${BOLD} curl --proxy socks5h://127.0.0.1:${SOCKS_PORT} https://httpbin.org/ip${NC}"
|
|
fi
|
|
|
|
echo ""
|
|
echo -e " ${CYAN}Management commands:${NC}"
|
|
echo " sudo paqctl menu Interactive menu"
|
|
echo " sudo paqctl status Check status"
|
|
echo " sudo paqctl health Health check"
|
|
echo " sudo paqctl logs View logs"
|
|
echo " sudo paqctl update Update paqet"
|
|
echo " sudo paqctl help All commands"
|
|
echo ""
|
|
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
echo -e " ${BOLD}${YELLOW}⚠ IMPORTANT: Save the connection info above before continuing!${NC}"
|
|
echo ""
|
|
echo -e " ${CYAN}Press Y to open the management menu, or any other key to exit...${NC}"
|
|
read -n 1 -r choice < /dev/tty || true
|
|
echo ""
|
|
if [[ "$choice" =~ ^[Yy]$ ]]; then
|
|
exec /usr/local/bin/paqctl menu
|
|
else
|
|
echo -e " ${GREEN}Run 'sudo paqctl menu' when ready.${NC}"
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
# Handle command line arguments
|
|
case "${1:-}" in
|
|
menu)
|
|
check_root
|
|
if [ -f "$INSTALL_DIR/settings.conf" ]; then
|
|
_load_settings
|
|
show_menu
|
|
else
|
|
echo -e "${RED}paqctl is not installed. Run the installer first.${NC}"
|
|
exit 1
|
|
fi
|
|
;;
|
|
*)
|
|
main "$@"
|
|
;;
|
|
esac
|