Files
paqctl/paqctl.sh
SamNet-dev 95cd496f42 feat: add NOTRACK iptables rules for GFK VIO port
Add iptables raw table NOTRACK rules for the VIO port, matching
what Paqet already has. Without NOTRACK, conntrack tracks the
crafted TCP packets which can cause them to be dropped by
hypervisor bridge netfilter (e.g. Proxmox).

Added to: boot script, _apply_firewall, _remove_firewall, and
install section.

Ref #27
2026-02-06 08:25:57 -06:00

6841 lines
272 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.14"
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" ;;
armv7l|armv7|armhf) echo "arm32" ;;
*)
log_error "Unsupported architecture: $arch"
log_error "Paqet supports amd64, arm64, and arm32 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; }
# Try curl first, fallback to wget
local download_ok=false
if curl -sL --max-time 180 --retry 3 --retry-delay 5 --fail -o "$tmp_file" "$url" 2>/dev/null; then
download_ok=true
elif command -v wget &>/dev/null; then
log_info "curl failed, trying wget..."
rm -f "$tmp_file"
if wget -q --timeout=180 --tries=3 -O "$tmp_file" "$url" 2>/dev/null; then
download_ok=true
fi
fi
if [ "$download_ok" != "true" ]; then
log_error "Failed to download: $url"
log_error "Try manual download: wget '$url' and place binary in $INSTALL_DIR/bin/"
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 - handle both standard "via X dev Y" and OpenVZ "dev Y scope link" formats
# Standard: "default via 192.168.1.1 dev eth0" -> $5 = eth0
# OpenVZ: "default dev venet0 scope link" -> $3 = venet0
local _route_line
_route_line=$(ip route show default 2>/dev/null | head -1)
if [[ "$_route_line" == *" via "* ]]; then
# Standard format with gateway
DETECTED_IFACE=$(echo "$_route_line" | awk '{print $5}')
elif [[ "$_route_line" == *" dev "* ]]; then
# OpenVZ/direct format without gateway
DETECTED_IFACE=$(echo "$_route_line" | awk '{print $3}')
fi
# Validate detected interface exists
if [ -n "$DETECTED_IFACE" ] && ! ip link show "$DETECTED_IFACE" &>/dev/null; then
DETECTED_IFACE=""
fi
if [ -z "$DETECTED_IFACE" ]; then
# Skip loopback, docker, veth, bridge, and other virtual interfaces
# Note: grep -v returns exit 1 if no matches, so we add || true for pipefail
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)' || true; } | head -1)
fi
# Local IP - wrap entire pipeline to prevent pipefail exit
if [ -n "$DETECTED_IFACE" ]; then
# Note: wrap in subshell with || true to handle cases where interface is invalid or has no IP
DETECTED_IP=$( (ip -4 addr show "$DETECTED_IFACE" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | { grep -o '[0-9.]*' || true; } | head -1) || true )
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 - only present in standard "via X" format, not in OpenVZ
if [[ "$_route_line" == *" via "* ]]; then
DETECTED_GATEWAY=$(echo "$_route_line" | awk '{print $3}')
else
DETECTED_GATEWAY=""
fi
# 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
# Note: grep returns exit 1 if no matches, so we add || true for pipefail
DETECTED_GW_MAC=$(arp -n "$DETECTED_GATEWAY" 2>/dev/null | { grep -oE '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' || true; } | 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")
# Build TCP flags YAML array (default: ["PA"])
local _tcp_local_flags _tcp_remote_flags
_tcp_local_flags=$(echo "${PAQET_TCP_LOCAL_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/')
_tcp_remote_flags=$(echo "${PAQET_TCP_REMOTE_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/')
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}"
tcp:
local_flag: ${_tcp_local_flags}
remote_flag: ${_tcp_remote_flags}
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}"
tcp:
local_flag: ${_tcp_local_flags}
remote_flag: ${_tcp_remote_flags}
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}"
PAQET_TCP_LOCAL_FLAG="${PAQET_TCP_LOCAL_FLAG:-PA}"
PAQET_TCP_REMOTE_FLAG="${PAQET_TCP_REMOTE_FLAG:-PA}"
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:-}"
GFK_TCP_FLAGS="${GFK_TCP_FLAGS:-AP}"
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 venv support (varies by distro)
# - Debian/Ubuntu: needs python3-venv or python3.X-venv package
# - Fedora/RHEL/Arch/openSUSE: venv included with python3, just need pip
# - Alpine: needs py3-pip
case "$PKG_MANAGER" in
apt)
# Debian/Ubuntu needs python3-venv package (version-specific)
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
install_package "python${pyver_full}-venv" || install_package "python3-venv"
else
install_package "python3-venv"
fi
;;
dnf)
# Fedora/RHEL 8+: venv is included with python3, just ensure pip
install_package "python3-pip" || true
;;
yum)
# Older RHEL/CentOS 7
install_package "python3-pip" || true
;;
pacman)
# Arch Linux: venv included with python, pip is separate
install_package "python-pip" || true
;;
zypper)
# openSUSE: venv included with python3
install_package "python3-pip" || true
;;
apk)
# Alpine
install_package "py3-pip" || true
;;
*)
# Try generic python3-venv, ignore if fails (venv may be built-in)
install_package "python3-venv" 2>/dev/null || true
;;
esac
# 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 (is python3-venv installed?)"
return 1
}
fi
# Verify pip exists after venv creation
if [ ! -x "$VENV_DIR/bin/pip" ]; then
log_error "venv created but pip missing (install python3-venv package)"
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")
tcp_flags = "${GFK_TCP_FLAGS:-AP}"
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 + NOTRACK to bypass conntrack)
vio_port="\${GFK_VIO_PORT:-45000}"
modprobe iptable_raw 2>/dev/null || true
iptables -t raw -C PREROUTING -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
iptables -t raw -A PREROUTING -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
iptables -t raw -C OUTPUT -p tcp --sport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
iptables -t raw -A OUTPUT -p tcp --sport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
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.14"
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}" ]; }
# Network auto-detection
detect_network() {
log_info "Auto-detecting network configuration..."
# Default interface - handle both standard "via X dev Y" and OpenVZ "dev Y scope link" formats
# Standard: "default via 192.168.1.1 dev eth0" -> $5 = eth0
# OpenVZ: "default dev venet0 scope link" -> $3 = venet0
local _route_line
_route_line=$(ip route show default 2>/dev/null | head -1)
if [[ "$_route_line" == *" via "* ]]; then
# Standard format with gateway
DETECTED_IFACE=$(echo "$_route_line" | awk '{print $5}')
elif [[ "$_route_line" == *" dev "* ]]; then
# OpenVZ/direct format without gateway
DETECTED_IFACE=$(echo "$_route_line" | awk '{print $3}')
fi
# Validate detected interface exists
if [ -n "$DETECTED_IFACE" ] && ! ip link show "$DETECTED_IFACE" &>/dev/null; then
DETECTED_IFACE=""
fi
if [ -z "$DETECTED_IFACE" ]; then
# Note: grep -v returns exit 1 if no matches, so we add || true for pipefail
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)' || true; } | head -1)
fi
# Local IP - wrap entire pipeline to prevent pipefail exit
if [ -n "$DETECTED_IFACE" ]; then
# Note: wrap in subshell with || true to handle cases where interface is invalid or has no IP
DETECTED_IP=$( (ip -4 addr show "$DETECTED_IFACE" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | { grep -o '[0-9.]*' || true; } | head -1) || true )
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 - handle OpenVZ format (may not have gateway)
if [[ "$_route_line" == *" via "* ]]; then
DETECTED_GATEWAY=$(echo "$_route_line" | awk '{print $3}')
else
DETECTED_GATEWAY=""
fi
# Gateway MAC
DETECTED_GW_MAC=""
if [ -n "$DETECTED_GATEWAY" ]; then
DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}')
if [ -z "$DETECTED_GW_MAC" ]; then
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
# Note: grep returns exit 1 if no matches, so we add || true for pipefail
DETECTED_GW_MAC=$(arp -n "$DETECTED_GATEWAY" 2>/dev/null | { grep -oE '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' || true; } | 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}"
}
_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" ;;
PAQET_TCP_LOCAL_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_LOCAL_FLAG="$value" ;;
PAQET_TCP_REMOTE_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_REMOTE_FLAG="$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" ;;
GFK_TCP_FLAGS) [[ "$value" =~ ^[FSRPAUEC]+$ ]] && GFK_TCP_FLAGS="$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:-}
GFK_TCP_FLAGS=${GFK_TCP_FLAGS:-AP}
# 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}"
PAQET_TCP_LOCAL_FLAG="${PAQET_TCP_LOCAL_FLAG:-PA}"
PAQET_TCP_REMOTE_FLAG="${PAQET_TCP_REMOTE_FLAG:-PA}"
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:-}"
GFK_TCP_FLAGS="${GFK_TCP_FLAGS:-AP}"
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" ;;
armv7l|armv7|armhf) echo "arm32" ;;
*)
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; }
# Try curl first, fallback to wget
local download_ok=false
if curl -sL --max-time 180 --retry 3 --retry-delay 5 --fail -o "$tmp_file" "$url" 2>/dev/null; then
download_ok=true
elif command -v wget &>/dev/null; then
log_info "curl failed, trying wget..."
rm -f "$tmp_file"
if wget -q --timeout=180 --tries=3 -O "$tmp_file" "$url" 2>/dev/null; then
download_ok=true
fi
fi
if [ "$download_ok" != "true" ]; then
log_error "Failed to download: $url"
log_error "Try manual download: wget '$url' and place binary in $INSTALL_DIR/bin/"
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 dnf &>/dev/null; then dnf 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
# Verify Python 3.10+ (required for GFK)
local pyver pymajor pyminor
pyver=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || echo "0.0")
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 for apt, generic for others)
if command -v apt-get &>/dev/null; then
apt-get install -y "python${pyver}-venv" 2>/dev/null || apt-get install -y python3-venv 2>/dev/null
elif command -v dnf &>/dev/null; then
dnf install -y python3-pip 2>/dev/null # dnf includes venv in python3
elif command -v yum &>/dev/null; then
yum install -y python3-pip 2>/dev/null
elif command -v apk &>/dev/null; then
apk add py3-pip 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 (is python3-venv installed?)"; return 1; }
fi
# Verify pip exists after venv creation
if [ ! -x "$VENV_DIR/bin/pip" ]; then
log_error "venv created but pip missing (install python${pyver}-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")
tcp_flags = "${GFK_TCP_FLAGS:-AP}"
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: NOTRACK + DROP TCP on VIO port so OS doesn't respond, raw socket handles it
local vio_port="${GFK_VIO_PORT:-45000}"
modprobe iptable_raw 2>/dev/null || true
# NOTRACK: bypass conntrack for VIO packets (prevents hypervisor/bridge filtering)
iptables -t raw -C PREROUTING -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
iptables -t raw -A PREROUTING -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
iptables -t raw -C OUTPUT -p tcp --sport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
iptables -t raw -A OUTPUT -p tcp --sport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
# 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 -t raw -D PREROUTING -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
iptables -t raw -D OUTPUT -p tcp --sport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
iptables -t raw -D PREROUTING -p tcp --dport "$vio_port" -j NOTRACK 2>/dev/null || true
iptables -t raw -D OUTPUT -p tcp --sport "$vio_port" -j NOTRACK 2>/dev/null || true
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"
# Also check for management script updates
update_management_script
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"
echo -e "${BOLD}Outgoing TCP flags${NC} [${GFK_TCP_FLAGS:-AP}]:"
echo -e " ${DIM}Controls TCP flags on outgoing violated packets (default: AP)${NC}"
echo -e " ${DIM}Valid flags: S(SYN) A(ACK) P(PSH) R(RST) F(FIN) U(URG)${NC}"
read -p " Flags: " input < /dev/tty || true
if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+$ ]]; then
log_error "Invalid flags. Use uppercase letters only: F, S, R, P, A, U, E, C"; return 1
fi
[ -n "$input" ] && GFK_TCP_FLAGS="$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}Outgoing TCP flags${NC} [${GFK_TCP_FLAGS:-AP}]:"
echo -e " ${DIM}Controls TCP flags on outgoing violated packets (default: AP)${NC}"
echo -e " ${DIM}Valid flags: S(SYN) A(ACK) P(PSH) R(RST) F(FIN) U(URG)${NC}"
read -p " Flags: " input < /dev/tty || true
if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+$ ]]; then
log_error "Invalid flags. Use uppercase letters only: F, S, R, P, A, U, E, C"; return 1
fi
[ -n "$input" ] && GFK_TCP_FLAGS="$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}')
# Note: grep returns exit 1 if no matches, so we add || true for pipefail
local _ip=$(ip -4 addr show "$_iface" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | { grep -o '[0-9.]*' || true; } | 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
# TCP flags (for both server and client)
echo -e "${BOLD}TCP local flag${NC} [${PAQET_TCP_LOCAL_FLAG:-PA}]:"
echo -e " ${DIM}Controls TCP flags on outgoing packets (default: PA = PSH+ACK)${NC}"
echo -e " ${DIM}Valid flags: S(SYN) A(ACK) P(PSH) R(RST) F(FIN) U(URG) E(ECE) C(CWR)${NC}"
echo -e " ${DIM}Multiple values: PA,A (tries PA first, then A)${NC}"
read -p " Flag: " input < /dev/tty || true
if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]]; then
log_warn "Invalid flags. Use: FSRPAUEC (e.g., PA or PA,A). Keeping current value."
input=""
fi
[ -n "$input" ] && PAQET_TCP_LOCAL_FLAG="$input"
echo -e "${BOLD}TCP remote flag${NC} [${PAQET_TCP_REMOTE_FLAG:-PA}]:"
echo -e " ${DIM}Controls expected TCP flags on incoming packets (default: PA)${NC}"
echo -e " ${DIM}Should match the server/client counterpart's local flag${NC}"
read -p " Flag: " input < /dev/tty || true
if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]]; then
log_warn "Invalid flags. Use: FSRPAUEC (e.g., PA or PA,A). Keeping current value."
input=""
fi
[ -n "$input" ] && PAQET_TCP_REMOTE_FLAG="$input"
# 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 _tcp_local_flags _tcp_remote_flags
_y_iface=$(_escape_yaml "$INTERFACE")
_y_ip=$(_escape_yaml "$LOCAL_IP")
_y_mac=$(_escape_yaml "$GATEWAY_MAC")
_y_key=$(_escape_yaml "$ENCRYPTION_KEY")
_tcp_local_flags=$(echo "${PAQET_TCP_LOCAL_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/')
_tcp_remote_flags=$(echo "${PAQET_TCP_REMOTE_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/')
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}"
tcp:
local_flag: ${_tcp_local_flags}
remote_flag: ${_tcp_remote_flags}
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}"
tcp:
local_flag: ${_tcp_local_flags}
remote_flag: ${_tcp_remote_flags}
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:-}"
GFK_TCP_FLAGS="${GFK_TCP_FLAGS:-AP}"
PAQET_TCP_LOCAL_FLAG="${PAQET_TCP_LOCAL_FLAG:-PA}"
PAQET_TCP_REMOTE_FLAG="${PAQET_TCP_REMOTE_FLAG:-PA}"
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 TCP packets with custom flags 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 "🔄 Service restarted" ;;
/stop) /usr/local/bin/paqctl stop 2>&1; send_message "⏹ Service stopped" ;;
/start) /usr/local/bin/paqctl start 2>&1; send_message "▶️ Service started" ;;
/version) send_message "📦 Version: ${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}
# Send startup notification
send_message "🚀 *Telegram notifications started*"$'\n'"Reports every ${TELEGRAM_INTERVAL}h | Alerts: ${TELEGRAM_ALERTS_ENABLED}"
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 ""
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
# Install Python dependencies (venv + scapy + aioquic)
install_python_deps || 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 " KCP over raw TCP packets with custom TCP flags."
echo " It operates below the OS TCP/IP stack to bypass"
echo " 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 " - Android client: github.com/AliRezaBeigy/paqetNG"
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" ;;
PAQET_TCP_LOCAL_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_LOCAL_FLAG="$value" ;;
PAQET_TCP_REMOTE_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_REMOTE_FLAG="$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" ;;
GFK_TCP_FLAGS) [[ "$value" =~ ^[FSRPAUEC]+$ ]] && GFK_TCP_FLAGS="$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 || { log_error "Failed to install Python dependencies"; exit 1; }
download_gfk || { log_error "Failed to download GFK"; exit 1; }
generate_gfk_certs || { log_error "Failed to generate certificates"; exit 1; }
if [ "$ROLE" = "server" ]; then
# Install Xray to provide SOCKS5 proxy on the target port
setup_xray_for_gfk || { log_error "Failed to setup Xray"; exit 1; }
elif [ "$ROLE" = "client" ]; then
install_microsocks || { log_error "Failed to install microsocks"; exit 1; }
create_gfk_client_wrapper || { log_error "Failed to create client wrapper"; exit 1; }
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)..."
# NOTRACK: bypass conntrack for VIO packets (prevents hypervisor/bridge filtering)
modprobe iptable_raw 2>/dev/null || true
iptables -t raw -C PREROUTING -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
iptables -t raw -A PREROUTING -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
iptables -t raw -C OUTPUT -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
iptables -t raw -A OUTPUT -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
# 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