Files
paqctl/paqctl.sh
SamNet-dev ee5dd009fa fix: correct GFK SOCKS5 port display and add missing client config wizard
When a panel is present, SOCKS5 is appended as a second port mapping
(e.g., 14000:443,14001:10443) but the UI always showed the first
mapping's port as SOCKS5. Now uses GFK_SOCKS_VIO_PORT when set.

Also fixes: install_additional_backend() for GFK clients was missing
a config wizard (server IP, auth code, port mappings were never
prompted), and _install_gfk_components() was missing the client
wrapper creation.

Closes #38
2026-02-08 16:17:28 -06:00

7537 lines
317 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.15"
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
# Firewall rules: use firewalld if active, otherwise iptables
if _is_firewalld_active; then
log_info "firewalld detected — will use firewall-cmd for rules"
elif ! 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
# openssl is required for GFK certificate generation
if ! command -v openssl &>/dev/null; then
install_package openssl || log_warn "Could not install openssl"
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
# Fedora/RHEL: ensure libpcap.so.1 symlink exists (package may only install versioned .so)
if [ "$PKG_MANAGER" = "dnf" ] || [ "$PKG_MANAGER" = "yum" ]; then
if ! ldconfig -p 2>/dev/null | grep -q 'libpcap\.so\.1 '; then
local _pcap_lib
_pcap_lib=$(find /usr/lib64 /usr/lib /lib64 /lib -name 'libpcap.so.*' -type f 2>/dev/null | head -1)
if [ -n "$_pcap_lib" ]; then
local _libdir
_libdir=$(dirname "$_pcap_lib")
if [ ! -e "${_libdir}/libpcap.so.1" ]; then
log_info "Creating libpcap.so.1 symlink for Fedora/RHEL compatibility"
ln -sf "$_pcap_lib" "${_libdir}/libpcap.so.1"
fi
ldconfig 2>/dev/null || true
fi
fi
fi
}
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 + SOCKS5)"
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}"
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:-}"
GFK_SOCKS_PORT="${GFK_SOCKS_PORT:-}"
GFK_SOCKS_VIO_PORT="${GFK_SOCKS_VIO_PORT:-}"
XRAY_PANEL_DETECTED="${XRAY_PANEL_DETECTED:-false}"
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"
}
#═══════════════════════════════════════════════════════════════════════
# Firewall Management
#═══════════════════════════════════════════════════════════════════════
_is_firewalld_active() {
command -v firewall-cmd &>/dev/null && firewall-cmd --state 2>/dev/null | grep -q running
}
apply_iptables_rules() {
local port="$1"
if [ -z "$port" ]; then
log_error "No port specified for iptables rules"
return 1
fi
log_info "Applying firewall rules for port $port..."
# firewalld path (Fedora/RHEL)
if _is_firewalld_active; then
firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
{ log_error "Failed to add PREROUTING NOTRACK rule via firewalld"; return 1; }
firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
{ log_error "Failed to add OUTPUT NOTRACK rule via firewalld"; return 1; }
firewall-cmd --direct --query-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
{ log_error "Failed to add RST DROP rule via firewalld"; return 1; }
log_success "IPv4 firewalld rules applied"
# IPv6
firewall-cmd --direct --add-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --add-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --add-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
persist_iptables_rules
return 0
fi
# iptables path (Debian/Ubuntu/Arch/etc.)
modprobe iptable_raw 2>/dev/null || true
modprobe iptable_mangle 2>/dev/null || true
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
local TAG="paqctl"
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
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
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
log_info "Removing firewall rules for port $port..."
# firewalld path
if _is_firewalld_active; then
firewall-cmd --direct --remove-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv4 filter INPUT 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv4 filter OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
# IPv6
firewall-cmd --direct --remove-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv6 filter INPUT 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv6 filter OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
log_success "firewalld rules removed"
return 0
fi
# iptables path
local TAG="paqctl"
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
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
log_success "iptables rules removed"
}
persist_iptables_rules() {
if _is_firewalld_active; then
firewall-cmd --runtime-to-permanent 2>/dev/null || true
return 0
fi
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 ok=true
if _is_firewalld_active; then
firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || ok=false
firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || ok=false
firewall-cmd --direct --query-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || ok=false
else
local TAG="paqctl"
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
fi
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 && return 0
[ -x /usr/local/bin/xray ] && return 0
[ -x /usr/local/x-ui/bin/xray-linux-amd64 ] && return 0
return 1
}
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)"
}
# Add a SOCKS5 inbound to an existing xray config (panel) without touching other inbounds
_add_xray_gfk_socks() {
local port="$1"
python3 -c "
import json, sys
port = int(sys.argv[1])
config_path = sys.argv[2]
try:
with open(config_path, 'r') as f:
cfg = json.load(f)
except:
cfg = {'inbounds': [], 'outbounds': [{'tag': 'direct', 'protocol': 'freedom', 'settings': {}}]}
cfg.setdefault('inbounds', [])
cfg['inbounds'] = [i for i in cfg['inbounds'] if i.get('tag') != 'gfk-socks']
cfg['inbounds'].append({
'tag': 'gfk-socks',
'port': port,
'listen': '127.0.0.1',
'protocol': 'socks',
'settings': {'auth': 'noauth', 'udp': True},
'sniffing': {'enabled': True, 'destOverride': ['http', 'tls']}
})
if not any(o.get('protocol') == 'freedom' for o in cfg.get('outbounds', [])):
cfg.setdefault('outbounds', []).append({'tag': 'direct', 'protocol': 'freedom', 'settings': {}})
with open(config_path, 'w') as f:
json.dump(cfg, f, indent=2)
" "$port" "$XRAY_CONFIG_FILE" 2>/dev/null
if [ $? -ne 0 ]; then
log_error "Failed to add SOCKS5 inbound to existing Xray config"
return 1
fi
log_success "Added GFK SOCKS5 inbound on 127.0.0.1:$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
local _xray_bin=""
[ -x /usr/local/bin/xray ] && _xray_bin="/usr/local/bin/xray"
[ -z "$_xray_bin" ] && [ -x /usr/local/x-ui/bin/xray-linux-amd64 ] && _xray_bin="/usr/local/x-ui/bin/xray-linux-amd64"
if [ -n "$_xray_bin" ]; then
pkill -x xray 2>/dev/null || true
sleep 1
nohup "$_xray_bin" run -c "$XRAY_CONFIG_FILE" > /var/log/xray.log 2>&1 &
sleep 2
if pgrep -f "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() {
local target_port
target_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f2 | cut -d, -f1)
if pgrep -x xray &>/dev/null || pgrep -x xray-linux-amd64 &>/dev/null; then
XRAY_PANEL_DETECTED=true
log_info "Existing Xray detected — adding SOCKS5 alongside panel..."
# Clean up any leftover standalone GFK xray from prior installs
pkill -f "xray run -c.*gfk-socks.json" 2>/dev/null || true
rm -f "${XRAY_CONFIG_DIR}/gfk-socks.json" 2>/dev/null
# Check all existing target ports from mappings
local mapping pairs
IFS=',' read -ra pairs <<< "${GFK_PORT_MAPPINGS:-14000:443}"
for mapping in "${pairs[@]}"; do
local vio_port="${mapping%%:*}"
local tp="${mapping##*:}"
if ss -tln 2>/dev/null | grep -q ":${tp} "; then
log_success "Port $tp is listening — GFK will forward VIO port $vio_port to this port"
else
log_warn "Port $tp is NOT listening — make sure your panel inbound is on port $tp"
fi
done
# Find free port for SOCKS5 (starting at 10443)
local socks_port=10443
while ss -tln 2>/dev/null | grep -q ":${socks_port} "; do
socks_port=$((socks_port + 1))
if [ "$socks_port" -gt 65000 ]; then
log_warn "Could not find free port for SOCKS5 — panel-only mode"
echo ""
local first_vio
first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1)
log_warn "For panel-to-panel: configure Iran panel outbound to 127.0.0.1:${first_vio}"
return 0
fi
done
# Add SOCKS5 inbound to existing xray config
_add_xray_gfk_socks "$socks_port" || {
log_warn "Could not add SOCKS5 to panel config — panel-only mode"
return 0
}
# Restart xray to load new config
systemctl restart xray 2>/dev/null || pkill -SIGHUP xray 2>/dev/null || true
sleep 2
# Find next VIO port (highest existing + 1) and append SOCKS5 mapping
local max_vio=0
for mapping in "${pairs[@]}"; do
local v="${mapping%%:*}"
[ "$v" -gt "$max_vio" ] && max_vio="$v"
done
local socks_vio=$((max_vio + 1))
GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS},${socks_vio}:${socks_port}"
GFK_SOCKS_PORT="$socks_port"
GFK_SOCKS_VIO_PORT="$socks_vio"
log_success "SOCKS5 proxy added on port $socks_port (VIO port $socks_vio)"
echo ""
log_info "Port mappings updated: ${GFK_PORT_MAPPINGS}"
log_warn "Use these SAME mappings on the client side"
echo ""
local first_vio
first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1)
log_warn "For panel-to-panel: configure Iran panel outbound to 127.0.0.1:${first_vio}"
log_warn "For direct SOCKS5: use 127.0.0.1:${socks_vio} as your proxy on client"
return 0
fi
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
if ! command -v openssl &>/dev/null; then
log_info "Installing openssl..."
install_package openssl || { log_error "Failed to install openssl"; return 1; }
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"
mkdir -p "$INSTALL_DIR/bin"
cat > "$wrapper" << 'WRAPEOF'
#!/bin/bash
set -e
GFK_DIR="REPLACE_ME_GFK_DIR"
INSTALL_DIR="REPLACE_ME_INSTALL_DIR"
cd "$GFK_DIR"
"$INSTALL_DIR/venv/bin/python" mainclient.py &
PID1=$!
trap "kill $PID1 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" "$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"
# Detect firewall backend
_use_firewalld=false
if command -v firewall-cmd &>/dev/null && firewall-cmd --state 2>/dev/null | grep -q running; then
_use_firewalld=true
fi
# Apply firewall rules (server + client)
if [ "\$ROLE" = "server" ]; then
port="\${LISTEN_PORT:-8443}"
vio_port="\${GFK_VIO_PORT:-45000}"
TAG="paqctl"
if [ "\$_use_firewalld" = true ]; then
# Paqet rules via firewalld
firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "\$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "\$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --add-rule ipv4 mangle OUTPUT 0 -p tcp --sport "\$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
# GFK rules via firewalld
firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "\$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "\$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "\$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
# IPv6 GFK
firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "\$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --runtime-to-permanent 2>/dev/null || true
else
# Paqet rules via iptables
modprobe iptable_raw 2>/dev/null || true
modprobe iptable_mangle 2>/dev/null || true
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
# GFK rules via iptables
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
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
else
# GFK client firewall rules
vio_client_port="\${GFK_VIO_CLIENT_PORT:-40000}"
TAG="paqctl"
if [ "\$_use_firewalld" = true ]; then
firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "\$vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "\$vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "\$vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "\$vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --runtime-to-permanent 2>/dev/null || true
else
modprobe iptable_raw 2>/dev/null || true
iptables -t raw -C PREROUTING -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
iptables -t raw -A PREROUTING -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
iptables -t raw -C OUTPUT -p tcp --sport "\$vio_client_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
iptables -t raw -A OUTPUT -p tcp --sport "\$vio_client_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
iptables -C INPUT -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
iptables -A INPUT -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null
iptables -C OUTPUT -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
iptables -A OUTPUT -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null
if command -v ip6tables &>/dev/null; then
ip6tables -C INPUT -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
ip6tables -A INPUT -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || true
ip6tables -C OUTPUT -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
ip6tables -A OUTPUT -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || true
fi
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 ] || [ -x /usr/local/x-ui/bin/xray-linux-amd64 ]; 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 &
sleep 2
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.15"
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" ;;
GFK_SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_SOCKS_PORT="$value" ;;
GFK_SOCKS_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_SOCKS_VIO_PORT="$value" ;;
XRAY_PANEL_DETECTED) XRAY_PANEL_DETECTED="$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:-}
GFK_SOCKS_PORT=${GFK_SOCKS_PORT:-}
GFK_SOCKS_VIO_PORT=${GFK_SOCKS_VIO_PORT:-}
XRAY_PANEL_DETECTED=${XRAY_PANEL_DETECTED:-false}
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:-}"
GFK_SOCKS_PORT="${GFK_SOCKS_PORT:-}"
GFK_SOCKS_VIO_PORT="${GFK_SOCKS_VIO_PORT:-}"
XRAY_PANEL_DETECTED="${XRAY_PANEL_DETECTED:-false}"
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
if ! command -v openssl &>/dev/null; then
log_info "Installing openssl..."
if command -v apt-get &>/dev/null; then apt-get install -y openssl 2>/dev/null
elif command -v dnf &>/dev/null; then dnf install -y openssl 2>/dev/null
elif command -v yum &>/dev/null; then yum install -y openssl 2>/dev/null
elif command -v apk &>/dev/null; then apk add openssl 2>/dev/null
elif command -v pacman &>/dev/null; then pacman -S --noconfirm openssl 2>/dev/null
fi
command -v openssl &>/dev/null || { log_error "Failed to install openssl"; return 1; }
fi
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"
mkdir -p "$INSTALL_DIR/bin"
cat > "$wrapper" << 'WEOF'
#!/bin/bash
set -e
GFK_DIR="REPLACE_GFK"
INSTALL_DIR="REPLACE_INST"
cd "$GFK_DIR"
"$INSTALL_DIR/venv/bin/python" mainclient.py &
PID1=$!
trap "kill $PID1 2>/dev/null; wait" EXIT INT TERM
wait
WEOF
sed "s#REPLACE_GFK#${GFK_DIR}#g; s#REPLACE_INST#${INSTALL_DIR}#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 ] || [ -x /usr/local/x-ui/bin/xray-linux-amd64 ]; 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 &
sleep 2
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
}
#═══════════════════════════════════════════════════════════════════════
# Firewall (internal commands)
#═══════════════════════════════════════════════════════════════════════
_is_firewalld_active() {
command -v firewall-cmd &>/dev/null && firewall-cmd --state 2>/dev/null | grep -q running
}
_apply_firewall() {
if ! _is_firewalld_active && ! command -v iptables &>/dev/null; then
echo -e "${YELLOW}[!]${NC} No firewall backend found (iptables or firewalld)." >&2
return 1
fi
if [ "$BACKEND" = "gfw-knocker" ]; then
local vio_port
if [ "$ROLE" = "server" ]; then
vio_port="${GFK_VIO_PORT:-45000}"
else
vio_port="${GFK_VIO_CLIENT_PORT:-40000}"
fi
if _is_firewalld_active; then
firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --query-rule ipv4 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
echo -e "${YELLOW}[!]${NC} Failed to add VIO port DROP rule via firewalld" >&2
firewall-cmd --direct --query-rule ipv4 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule via firewalld" >&2
firewall-cmd --direct --query-rule ipv6 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --query-rule ipv6 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
else
local TAG="paqctl"
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 || 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
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
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
fi
return 0
fi
[ "$ROLE" != "server" ] && return 0
local port="${LISTEN_PORT:-8443}"
if _is_firewalld_active; then
firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
echo -e "${YELLOW}[!]${NC} Failed to add PREROUTING NOTRACK rule via firewalld" >&2
firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
echo -e "${YELLOW}[!]${NC} Failed to add OUTPUT NOTRACK rule via firewalld" >&2
firewall-cmd --direct --query-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule via firewalld" >&2
firewall-cmd --direct --query-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
firewall-cmd --direct --add-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --query-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
firewall-cmd --direct --add-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --query-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
else
local TAG="paqctl"
modprobe iptable_raw 2>/dev/null || true
modprobe iptable_mangle 2>/dev/null || true
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
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
fi
}
_remove_firewall() {
if ! _is_firewalld_active && ! command -v iptables &>/dev/null; then
return 0
fi
if [ "$BACKEND" = "gfw-knocker" ]; then
local vio_port
if [ "$ROLE" = "server" ]; then
vio_port="${GFK_VIO_PORT:-45000}"
else
vio_port="${GFK_VIO_CLIENT_PORT:-40000}"
fi
if _is_firewalld_active; then
firewall-cmd --direct --remove-rule ipv4 raw PREROUTING 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv4 raw OUTPUT 0 -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv4 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv4 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv6 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv6 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
else
local TAG="paqctl"
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
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
fi
return 0
fi
[ "$ROLE" != "server" ] && return 0
local port="${LISTEN_PORT:-8443}"
if _is_firewalld_active; then
firewall-cmd --direct --remove-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --remove-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
else
local TAG="paqctl"
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
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
fi
}
# Remove ALL paqctl-tagged firewall rules (for complete uninstall)
_remove_all_paqctl_firewall_rules() {
# firewalld: remove paqctl-tagged direct rules
if _is_firewalld_active; then
local _rules
_rules=$(firewall-cmd --direct --get-all-rules 2>/dev/null) || true
if [ -n "$_rules" ]; then
echo "$_rules" | grep "paqctl" | while IFS= read -r _rule; do
firewall-cmd --direct --remove-rule $_rule 2>/dev/null || true
firewall-cmd --permanent --direct --remove-rule $_rule 2>/dev/null || true
done
fi
return 0
fi
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 _is_firewalld_active; then
firewall-cmd --runtime-to-permanent 2>/dev/null || true
return 0
fi
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}"
if [ "$ROLE" = "server" ]; then
if [ "${XRAY_PANEL_DETECTED:-false}" = "true" ] && [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
local _md=""
IFS=',' read -ra _pairs <<< "${GFK_PORT_MAPPINGS}"
for _p in "${_pairs[@]}"; do
if [ "${_p%%:*}" = "${GFK_SOCKS_VIO_PORT}" ]; then
_md="${_md:+${_md}, }${_p} (SOCKS5)"
else
_md="${_md:+${_md}, }${_p} (panel)"
fi
done
echo -e " Mappings: ${_md}"
echo -e " SOCKS5: ${GREEN}127.0.0.1:${GFK_SOCKS_PORT}${NC} (server-side)"
echo -e " Client use: ${GREEN}127.0.0.1:${GFK_SOCKS_VIO_PORT}${NC} (set as proxy on client)"
elif [ "${XRAY_PANEL_DETECTED:-false}" = "true" ]; then
echo -e " Mappings: ${GFK_PORT_MAPPINGS}"
echo -e " SOCKS5: ${YELLOW}not configured${NC}"
else
echo -e " Mappings: ${GFK_PORT_MAPPINGS}"
local _srv_port _cli_port
_srv_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f2)
_cli_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
echo -e " SOCKS5: ${GREEN}127.0.0.1:${_srv_port}${NC} (server-side)"
echo -e " Client use: ${GREEN}127.0.0.1:${_cli_port}${NC} (set as proxy on client)"
fi
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 " Mappings: ${GFK_PORT_MAPPINGS}"
local _fv
if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
_fv="$GFK_SOCKS_VIO_PORT"
else
_fv=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
fi
echo -e " Proxy: ${GREEN}SOCKS5 127.0.0.1:${_fv}${NC} (set as browser proxy)"
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. SOCKS5 port (client)
if [ "$ROLE" = "client" ]; then
local _socks_vio
if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
_socks_vio="$GFK_SOCKS_VIO_PORT"
else
_socks_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
fi
if is_running && ss -tlnp 2>/dev/null | grep -q ":${_socks_vio} "; then
echo -e " ${GREEN}✓${NC} SOCKS5 port ${_socks_vio} is listening"
elif is_running; then
echo -e " ${RED}✗${NC} SOCKS5 port ${_socks_vio} 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"
# Regenerate client wrapper (removes legacy microsocks startup)
if [ "$ROLE" = "client" ]; then
create_gfk_client_wrapper
pkill -f "${INSTALL_DIR}/bin/microsocks" 2>/dev/null || true
fi
# 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" ] && [ "$BACKEND" != "gfw-knocker" ]; then
echo ""
log_info "Firewall rules only apply in server mode or GFK client 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 _fw_backend="iptables"
_is_firewalld_active && _fw_backend="firewalld"
echo -e " ${DIM}Backend: ${_fw_backend}${NC}"
echo ""
if [ "$BACKEND" = "gfw-knocker" ]; then
local vio_port
if [ "$ROLE" = "server" ]; then
vio_port="${GFK_VIO_PORT:-45000}"
else
vio_port="${GFK_VIO_CLIENT_PORT:-40000}"
fi
echo -e " ${BOLD}Required rules for VIO port ${vio_port}:${NC}"
echo ""
if [ "$_fw_backend" = "firewalld" ]; then
firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} PREROUTING NOTRACK (dport $vio_port)" \
|| echo -e " ${RED}✗${NC} PREROUTING NOTRACK (dport $vio_port) ${DIM}MISSING${NC}"
firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} OUTPUT NOTRACK (sport $vio_port)" \
|| echo -e " ${RED}✗${NC} OUTPUT NOTRACK (sport $vio_port) ${DIM}MISSING${NC}"
firewall-cmd --direct --query-rule ipv4 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} INPUT DROP (dport $vio_port)" \
|| echo -e " ${RED}✗${NC} INPUT DROP (dport $vio_port) ${DIM}MISSING${NC}"
firewall-cmd --direct --query-rule ipv4 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} RST DROP (sport $vio_port)" \
|| echo -e " ${RED}✗${NC} RST DROP (sport $vio_port) ${DIM}MISSING${NC}"
else
iptables -t raw -C PREROUTING -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} PREROUTING NOTRACK (dport $vio_port)" \
|| echo -e " ${RED}✗${NC} PREROUTING NOTRACK (dport $vio_port) ${DIM}MISSING${NC}"
iptables -t raw -C OUTPUT -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} OUTPUT NOTRACK (sport $vio_port)" \
|| echo -e " ${RED}✗${NC} OUTPUT NOTRACK (sport $vio_port) ${DIM}MISSING${NC}"
iptables -C INPUT -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} INPUT DROP (dport $vio_port)" \
|| echo -e " ${RED}✗${NC} INPUT DROP (dport $vio_port) ${DIM}MISSING${NC}"
iptables -C OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} RST DROP (sport $vio_port)" \
|| echo -e " ${RED}✗${NC} RST DROP (sport $vio_port) ${DIM}MISSING${NC}"
fi
else
local port="${LISTEN_PORT:-8443}"
echo -e " ${BOLD}Required rules for port ${port}:${NC}"
echo ""
if [ "$_fw_backend" = "firewalld" ]; then
firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} PREROUTING NOTRACK (dport $port)" \
|| echo -e " ${RED}✗${NC} PREROUTING NOTRACK (dport $port) ${DIM}MISSING${NC}"
firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} OUTPUT NOTRACK (sport $port)" \
|| echo -e " ${RED}✗${NC} OUTPUT NOTRACK (sport $port) ${DIM}MISSING${NC}"
firewall-cmd --direct --query-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} RST DROP (sport $port)" \
|| echo -e " ${RED}✗${NC} RST DROP (sport $port) ${DIM}MISSING${NC}"
else
iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} PREROUTING NOTRACK (dport $port)" \
|| echo -e " ${RED}✗${NC} PREROUTING NOTRACK (dport $port) ${DIM}MISSING${NC}"
iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} OUTPUT NOTRACK (sport $port)" \
|| echo -e " ${RED}✗${NC} OUTPUT NOTRACK (sport $port) ${DIM}MISSING${NC}"
iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "paqctl" --tcp-flags RST RST -j DROP 2>/dev/null \
&& echo -e " ${GREEN}✓${NC} RST DROP (sport $port)" \
|| echo -e " ${RED}✗${NC} RST DROP (sport $port) ${DIM}MISSING${NC}"
fi
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"
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:-}"
GFK_SOCKS_PORT="${GFK_SOCKS_PORT:-}"
GFK_SOCKS_VIO_PORT="${GFK_SOCKS_VIO_PORT:-}"
XRAY_PANEL_DETECTED="${XRAY_PANEL_DETECTED:-false}"
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|\
XRAY_PANEL_DETECTED|\
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|\
GFK_SOCKS_PORT|GFK_SOCKS_VIO_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
# Collect GFK configuration for client role
if [ "$ROLE" = "client" ]; then
echo ""
echo -e "${BOLD}GFK Client Configuration${NC}"
echo -e "${DIM}(these must match your server settings)${NC}"
echo ""
echo -e "${BOLD}Server IP${NC} (server's public IP):"
read -p " IP: " input < /dev/tty || true
if [ -z "$input" ] || ! _validate_ip "$input"; then
log_error "Valid server IP is required."
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
return 1
fi
GFK_SERVER_IP="$input"
echo -e "${BOLD}Server's VIO TCP port${NC} [45000] (must match server):"
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
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
echo -e "${BOLD}Server's QUIC port${NC} [25000] (must match server):"
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
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
echo -e "${BOLD}QUIC auth code${NC} (from server setup):"
read -p " Auth code: " input < /dev/tty || true
if [ -z "$input" ]; then
log_error "Auth code is required."
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
return 1
fi
GFK_AUTH_CODE="$input"
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}"
echo ""
fi
# 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 && return 0
[ -x /usr/local/bin/xray ] && return 0
[ -x /usr/local/x-ui/bin/xray-linux-amd64 ] && return 0
return 1
}
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)"
}
_add_xray_gfk_socks() {
local port="$1"
python3 -c "
import json, sys
port = int(sys.argv[1])
config_path = sys.argv[2]
try:
with open(config_path, 'r') as f:
cfg = json.load(f)
except:
cfg = {'inbounds': [], 'outbounds': [{'tag': 'direct', 'protocol': 'freedom', 'settings': {}}]}
cfg.setdefault('inbounds', [])
cfg['inbounds'] = [i for i in cfg['inbounds'] if i.get('tag') != 'gfk-socks']
cfg['inbounds'].append({
'tag': 'gfk-socks', 'port': port, 'listen': '127.0.0.1', 'protocol': 'socks',
'settings': {'auth': 'noauth', 'udp': True},
'sniffing': {'enabled': True, 'destOverride': ['http', 'tls']}
})
if not any(o.get('protocol') == 'freedom' for o in cfg.get('outbounds', [])):
cfg.setdefault('outbounds', []).append({'tag': 'direct', 'protocol': 'freedom', 'settings': {}})
with open(config_path, 'w') as f:
json.dump(cfg, f, indent=2)
" "$port" "$XRAY_CONFIG_FILE" 2>/dev/null
if [ $? -ne 0 ]; then
log_error "Failed to add SOCKS5 inbound to existing Xray config"
return 1
fi
log_success "Added GFK SOCKS5 inbound on 127.0.0.1:$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
local _xray_bin=""
[ -x /usr/local/bin/xray ] && _xray_bin="/usr/local/bin/xray"
[ -z "$_xray_bin" ] && [ -x /usr/local/x-ui/bin/xray-linux-amd64 ] && _xray_bin="/usr/local/x-ui/bin/xray-linux-amd64"
if [ -n "$_xray_bin" ]; then
pkill -x xray 2>/dev/null || true
sleep 1
nohup "$_xray_bin" run -c "$XRAY_CONFIG_FILE" > /var/log/xray.log 2>&1 &
sleep 2
if pgrep -f "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)
if pgrep -x xray &>/dev/null || pgrep -x xray-linux-amd64 &>/dev/null; then
XRAY_PANEL_DETECTED=true
log_info "Existing Xray detected — adding SOCKS5 alongside panel..."
# Clean up any leftover standalone GFK xray from prior installs
pkill -f "xray run -c.*gfk-socks.json" 2>/dev/null || true
rm -f "${XRAY_CONFIG_DIR}/gfk-socks.json" 2>/dev/null
# Check all existing target ports from mappings
local mapping pairs
IFS=',' read -ra pairs <<< "${GFK_PORT_MAPPINGS:-14000:443}"
for mapping in "${pairs[@]}"; do
local vio_port="${mapping%%:*}"
local tp="${mapping##*:}"
if ss -tln 2>/dev/null | grep -q ":${tp} "; then
log_success "Port $tp is listening — GFK will forward VIO port $vio_port to this port"
else
log_warn "Port $tp is NOT listening — make sure your panel inbound is on port $tp"
fi
done
# Find free port for SOCKS5 (starting at 10443)
local socks_port=10443
while ss -tln 2>/dev/null | grep -q ":${socks_port} "; do
socks_port=$((socks_port + 1))
if [ "$socks_port" -gt 65000 ]; then
log_warn "Could not find free port for SOCKS5 — panel-only mode"
echo ""
local first_vio
first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1)
log_warn "For panel-to-panel: configure Iran panel outbound to 127.0.0.1:${first_vio}"
return 0
fi
done
# Add SOCKS5 inbound to existing xray config
_add_xray_gfk_socks "$socks_port" || {
log_warn "Could not add SOCKS5 to panel config — panel-only mode"
return 0
}
# Restart xray to load new config
systemctl restart xray 2>/dev/null || pkill -SIGHUP xray 2>/dev/null || true
sleep 2
# Find next VIO port (highest existing + 1) and append SOCKS5 mapping
local max_vio=0
for mapping in "${pairs[@]}"; do
local v="${mapping%%:*}"
[ "$v" -gt "$max_vio" ] && max_vio="$v"
done
local socks_vio=$((max_vio + 1))
GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS},${socks_vio}:${socks_port}"
GFK_SOCKS_PORT="$socks_port"
GFK_SOCKS_VIO_PORT="$socks_vio"
log_success "SOCKS5 proxy added on port $socks_port (VIO port $socks_vio)"
echo ""
log_info "Port mappings updated: ${GFK_PORT_MAPPINGS}"
log_warn "Use these SAME mappings on the client side"
echo ""
local first_vio
first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1)
log_warn "For panel-to-panel: configure Iran panel outbound to 127.0.0.1:${first_vio}"
log_warn "For direct SOCKS5: use 127.0.0.1:${socks_vio} as your proxy on client"
return 0
fi
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
# Setup Xray (server only — adds SOCKS5 alongside panel if detected)
if [ "$ROLE" = "server" ]; then
setup_xray_for_gfk || return 1
elif [ "$ROLE" = "client" ]; then
create_gfk_client_wrapper
fi
# Generate parameters.py config
generate_gfk_config || return 1
save_settings
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"
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
# Stop standalone GFK xray and clean up config
pkill -f "xray run -c.*gfk-socks.json" 2>/dev/null || true
rm -f /usr/local/etc/xray/gfk-socks.json 2>/dev/null
# Remove gfk-socks inbound from panel's xray config if present
if [ -f "$XRAY_CONFIG_FILE" ] && command -v python3 &>/dev/null; then
python3 -c "
import json, sys
try:
with open(sys.argv[1], 'r') as f:
cfg = json.load(f)
orig_len = len(cfg.get('inbounds', []))
cfg['inbounds'] = [i for i in cfg.get('inbounds', []) if i.get('tag') != 'gfk-socks']
if len(cfg['inbounds']) < orig_len:
with open(sys.argv[1], 'w') as f:
json.dump(cfg, f, indent=2)
except: pass
" "$XRAY_CONFIG_FILE" 2>/dev/null
systemctl restart xray 2>/dev/null || true
fi
# 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} AGPL-3.0 - Copyright (C) 2026 SamNet Technologies, LLC"
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} AGPL-3.0 - Copyright (C) 2026 SamNet Technologies, LLC"
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}"
local _gfk_proxy_port
if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
_gfk_proxy_port="$GFK_SOCKS_VIO_PORT"
else
_gfk_proxy_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
fi
echo -e " ${DIM}Client proxy: 127.0.0.1:${_gfk_proxy_port} (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
local _paqet_info=""
if [ "$ROLE" = "server" ]; then
_paqet_info="Port: ${LISTEN_PORT:-8443}"
else
_paqet_info="Server: ${REMOTE_SERVER:-N/A}"
fi
if is_paqet_running; then
echo -e " Paqet: ${GREEN}● Running${NC} | ${_paqet_info} | SOCKS5: 127.0.0.1:${SOCKS_PORT:-1080}"
else
echo -e " Paqet: ${RED}○ Stopped${NC} | ${_paqet_info}"
fi
else
echo -e " Paqet: ${DIM}not installed${NC}"
fi
# GFK status
if [ "$gfk_installed" = true ]; then
if is_gfk_running; then
local _gfk_sv
if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
_gfk_sv="$GFK_SOCKS_VIO_PORT"
else
_gfk_sv=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
fi
echo -e " GFK: ${GREEN}● Running${NC} | VIO: ${GFK_VIO_PORT:-45000} | SOCKS5: 127.0.0.1:${_gfk_sv}"
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" ;;
GFK_SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_SOCKS_PORT="$value" ;;
GFK_SOCKS_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_SOCKS_VIO_PORT="$value" ;;
XRAY_PANEL_DETECTED) XRAY_PANEL_DETECTED="$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 SOCKS5 proxy (adds alongside panel if detected)
setup_xray_for_gfk || { log_error "Failed to setup Xray"; exit 1; }
# Regenerate config if mappings changed (panel detected → SOCKS5 added)
if [ "${XRAY_PANEL_DETECTED:-false}" = "true" ]; then
generate_gfk_config || { log_error "Failed to regenerate GFK config"; exit 1; }
fi
elif [ "$ROLE" = "client" ]; then
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
local _vio_port="${GFK_VIO_PORT:-45000}"
log_info "Blocking VIO TCP port $_vio_port (raw socket handles it)..."
if _is_firewalld_active; then
firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --query-rule ipv4 filter INPUT 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
log_warn "Failed to add VIO INPUT DROP rule via firewalld"
firewall-cmd --direct --query-rule ipv4 filter OUTPUT 0 -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -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 via firewalld"
firewall-cmd --direct --query-rule ipv6 filter INPUT 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --query-rule ipv6 filter OUTPUT 0 -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
persist_iptables_rules
elif command -v iptables &>/dev/null; then
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
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"
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"
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
else
log_warn "iptables not found - firewall rules cannot be applied"
fi
else
local _vio_client_port="${GFK_VIO_CLIENT_PORT:-40000}"
log_info "Applying NOTRACK + DROP rules for VIO client port $_vio_client_port..."
if _is_firewalld_active; then
firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
firewall-cmd --direct --query-rule ipv4 filter INPUT 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
log_warn "Failed to add VIO client INPUT DROP rule via firewalld"
firewall-cmd --direct --query-rule ipv4 filter OUTPUT 0 -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
log_warn "Failed to add VIO client RST DROP rule via firewalld"
firewall-cmd --direct --query-rule ipv6 filter INPUT 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
firewall-cmd --direct --query-rule ipv6 filter OUTPUT 0 -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
persist_iptables_rules
elif command -v iptables &>/dev/null; then
modprobe iptable_raw 2>/dev/null || true
iptables -t raw -C PREROUTING -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
iptables -t raw -A PREROUTING -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
iptables -t raw -C OUTPUT -p tcp --sport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
iptables -t raw -A OUTPUT -p tcp --sport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
iptables -C INPUT -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
iptables -A INPUT -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
log_warn "Failed to add VIO client INPUT DROP rule"
iptables -C OUTPUT -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
iptables -A OUTPUT -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
log_warn "Failed to add VIO client RST DROP rule"
if command -v ip6tables &>/dev/null; then
ip6tables -C INPUT -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
ip6tables -A INPUT -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
ip6tables -C OUTPUT -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
ip6tables -A OUTPUT -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
fi
else
log_warn "iptables not found - firewall rules cannot be applied"
fi
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}"
if [ "${XRAY_PANEL_DETECTED:-false}" = "true" ]; then
echo -e " Xray: ${BOLD}Existing panel detected (forwarding to port ${_xray_port})${NC}"
if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
echo -e " SOCKS5: ${BOLD}127.0.0.1:${GFK_SOCKS_PORT} (auto-added, VIO port ${GFK_SOCKS_VIO_PORT})${NC}"
echo ""
echo -e " ${GREEN}✓ GFK forwards to panel + SOCKS5 proxy added${NC}"
else
echo ""
echo -e " ${GREEN}✓ GFK forwards to panel${NC}"
fi
local _first_vio
_first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1)
echo -e " ${YELLOW}! Panel users: configure Iran outbound → 127.0.0.1:${_first_vio}${NC}"
if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
echo -e " ${YELLOW}! Direct SOCKS5: use 127.0.0.1:${GFK_SOCKS_VIO_PORT} on client${NC}"
fi
else
echo -e " Xray: ${BOLD}127.0.0.1:${_xray_port} (SOCKS5)${NC}"
echo ""
echo -e " ${GREEN}✓ Xray SOCKS5 proxy installed and running${NC}"
fi
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}"
if [ "${XRAY_PANEL_DETECTED:-false}" = "true" ] && [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
echo -e "${YELLOW}${NC}"
echo -e "${YELLOW}${NC} ${GREEN}Proxy port: 127.0.0.1:${GFK_SOCKS_VIO_PORT} (SOCKS5 — use this on client)${NC}"
local _panel_vio
_panel_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
echo -e "${YELLOW}${NC} Panel port: 127.0.0.1:${_panel_vio} (vmess/vless — for panel-to-panel)"
elif [ "${XRAY_PANEL_DETECTED:-false}" = "true" ]; then
local _panel_vio
_panel_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
echo -e "${YELLOW}${NC}"
echo -e "${YELLOW}${NC} Panel port: 127.0.0.1:${_panel_vio} (vmess/vless — for panel-to-panel)"
else
local _proxy_vio
_proxy_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
echo -e "${YELLOW}${NC}"
echo -e "${YELLOW}${NC} ${GREEN}Proxy port: 127.0.0.1:${_proxy_vio} (SOCKS5 — use this on client)${NC}"
fi
echo -e "${YELLOW}╚═══════════════════════════════════════════════════════════════╝${NC}"
else
local _socks_vio
if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then
_socks_vio="$GFK_SOCKS_VIO_PORT"
else
_socks_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
fi
echo -e " Server: ${BOLD}${GFK_SERVER_IP}${NC}"
echo -e " SOCKS5: ${BOLD}127.0.0.1:${_socks_vio}${NC}"
echo ""
echo -e " ${YELLOW}Test your proxy:${NC}"
echo -e " ${BOLD} curl --proxy socks5h://127.0.0.1:${_socks_vio} 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