- Add GFK_SOCKS_PORT, GFK_SOCKS_VIO_PORT, XRAY_PANEL_DETECTED to save_settings() and _load_settings() across all script locations - Status menu labels mappings as (panel)/(SOCKS5) when panel detected - Status shows server-side and client-side proxy ports clearly - Install summary CLIENT CONNECTION INFO box now shows which port is SOCKS5 proxy vs panel, so users know exactly what to configure
7156 lines
286 KiB
Bash
7156 lines
286 KiB
Bash
#!/bin/bash
|
|
#
|
|
# ╔═══════════════════════════════════════════════════════════════════╗
|
|
# ║ PAQCTL - Paqet Manager v1.0.0 ║
|
|
# ║ ║
|
|
# ║ One-click setup for Paqet raw-socket proxy ║
|
|
# ║ ║
|
|
# ║ * Installs paqet binary + libpcap ║
|
|
# ║ * Auto-detects network config ║
|
|
# ║ * Configures server or client mode ║
|
|
# ║ * Manages iptables rules ║
|
|
# ║ * Auto-start on boot via systemd/OpenRC/SysVinit ║
|
|
# ║ * Easy management via CLI or interactive menu ║
|
|
# ║ ║
|
|
# ║ Paqet: https://github.com/SamNet-dev/paqctl ║
|
|
# ╚═══════════════════════════════════════════════════════════════════╝
|
|
#
|
|
# Usage:
|
|
# curl -sL https://raw.githubusercontent.com/SamNet-dev/paqctl/main/paqctl.sh | sudo bash
|
|
#
|
|
# Or: wget paqctl.sh && sudo bash paqctl.sh
|
|
#
|
|
|
|
set -eo pipefail
|
|
|
|
# Require bash
|
|
if [ -z "$BASH_VERSION" ]; then
|
|
echo "Error: This script requires bash. Please run with: bash $0"
|
|
exit 1
|
|
fi
|
|
|
|
VERSION="1.0.0"
|
|
|
|
# Pinned versions for stability (update these after testing new releases)
|
|
PAQET_VERSION_PINNED="v1.0.0-alpha.14"
|
|
XRAY_VERSION_PINNED="v26.2.4"
|
|
GFK_VERSION_PINNED="v1.0.0"
|
|
|
|
PAQET_REPO="hanselime/paqet"
|
|
PAQET_API_URL="https://api.github.com/repos/${PAQET_REPO}/releases/latest"
|
|
INSTALL_DIR="${INSTALL_DIR:-/opt/paqctl}"
|
|
BACKUP_DIR="$INSTALL_DIR/backups"
|
|
GFK_REPO="SamNet-dev/paqctl"
|
|
GFK_BRANCH="main"
|
|
GFK_RAW_URL="https://raw.githubusercontent.com/${GFK_REPO}/${GFK_BRANCH}/gfk"
|
|
GFK_DIR="$INSTALL_DIR/gfk"
|
|
MICROSOCKS_REPO="rofl0r/microsocks"
|
|
BACKEND="${BACKEND:-paqet}"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
MAGENTA='\033[0;35m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
NC='\033[0m'
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Utility Functions
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
print_header() {
|
|
echo -e "${CYAN}"
|
|
echo "╔════════════════════════════════════════════════════════════════╗"
|
|
echo "║ PAQCTL - Paqet Manager v${VERSION} ║"
|
|
echo "║ Raw-socket encrypted proxy - bypass firewalls ║"
|
|
echo "╚════════════════════════════════════════════════════════════════╝"
|
|
echo -e "${NC}"
|
|
}
|
|
|
|
log_info() {
|
|
echo -e "${BLUE}[INFO]${NC} $1"
|
|
}
|
|
|
|
log_success() {
|
|
echo -e "${GREEN}[✓]${NC} $1"
|
|
}
|
|
|
|
log_warn() {
|
|
echo -e "${YELLOW}[!]${NC} $1"
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${RED}[✗]${NC} $1"
|
|
}
|
|
|
|
check_root() {
|
|
if [ "$EUID" -ne 0 ]; then
|
|
log_error "This script must be run as root (use sudo)"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
detect_os() {
|
|
OS="unknown"
|
|
OS_VERSION="unknown"
|
|
OS_FAMILY="unknown"
|
|
HAS_SYSTEMD=false
|
|
PKG_MANAGER="unknown"
|
|
|
|
if [ -f /etc/os-release ]; then
|
|
. /etc/os-release
|
|
OS="$ID"
|
|
OS_VERSION="${VERSION_ID:-unknown}"
|
|
elif [ -f /etc/redhat-release ]; then
|
|
OS="rhel"
|
|
elif [ -f /etc/debian_version ]; then
|
|
OS="debian"
|
|
elif [ -f /etc/alpine-release ]; then
|
|
OS="alpine"
|
|
elif [ -f /etc/arch-release ]; then
|
|
OS="arch"
|
|
elif [ -f /etc/SuSE-release ] || [ -f /etc/SUSE-brand ]; then
|
|
OS="opensuse"
|
|
else
|
|
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
|
fi
|
|
|
|
case "$OS" in
|
|
ubuntu|debian|linuxmint|pop|elementary|zorin|kali|raspbian)
|
|
OS_FAMILY="debian"
|
|
PKG_MANAGER="apt"
|
|
;;
|
|
rhel|centos|fedora|rocky|almalinux|oracle|amazon|amzn)
|
|
OS_FAMILY="rhel"
|
|
if command -v dnf &>/dev/null; then
|
|
PKG_MANAGER="dnf"
|
|
else
|
|
PKG_MANAGER="yum"
|
|
fi
|
|
;;
|
|
arch|manjaro|endeavouros|garuda)
|
|
OS_FAMILY="arch"
|
|
PKG_MANAGER="pacman"
|
|
;;
|
|
opensuse|opensuse-leap|opensuse-tumbleweed|sles)
|
|
OS_FAMILY="suse"
|
|
PKG_MANAGER="zypper"
|
|
;;
|
|
alpine)
|
|
OS_FAMILY="alpine"
|
|
PKG_MANAGER="apk"
|
|
;;
|
|
*)
|
|
OS_FAMILY="unknown"
|
|
PKG_MANAGER="unknown"
|
|
;;
|
|
esac
|
|
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
HAS_SYSTEMD=true
|
|
fi
|
|
|
|
log_info "Detected: $OS ($OS_FAMILY family), Package manager: $PKG_MANAGER"
|
|
}
|
|
|
|
install_package() {
|
|
local package="$1"
|
|
log_info "Installing $package..."
|
|
|
|
case "$PKG_MANAGER" in
|
|
apt)
|
|
apt-get update -q 2>/dev/null || log_warn "apt-get update failed, attempting install anyway..."
|
|
if apt-get install -y -q "$package"; then
|
|
log_success "$package installed successfully"
|
|
else
|
|
log_error "Failed to install $package"
|
|
return 1
|
|
fi
|
|
;;
|
|
dnf)
|
|
if dnf install -y -q "$package"; then
|
|
log_success "$package installed successfully"
|
|
else
|
|
log_error "Failed to install $package"
|
|
return 1
|
|
fi
|
|
;;
|
|
yum)
|
|
if yum install -y -q "$package"; then
|
|
log_success "$package installed successfully"
|
|
else
|
|
log_error "Failed to install $package"
|
|
return 1
|
|
fi
|
|
;;
|
|
pacman)
|
|
if pacman -Sy --noconfirm "$package"; then
|
|
log_success "$package installed successfully"
|
|
else
|
|
log_error "Failed to install $package"
|
|
return 1
|
|
fi
|
|
;;
|
|
zypper)
|
|
if zypper install -y -n "$package"; then
|
|
log_success "$package installed successfully"
|
|
else
|
|
log_error "Failed to install $package"
|
|
return 1
|
|
fi
|
|
;;
|
|
apk)
|
|
if apk add --no-cache "$package"; then
|
|
log_success "$package installed successfully"
|
|
else
|
|
log_error "Failed to install $package"
|
|
return 1
|
|
fi
|
|
;;
|
|
*)
|
|
log_warn "Unknown package manager. Please install $package manually."
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
check_dependencies() {
|
|
if [ "$OS_FAMILY" = "alpine" ]; then
|
|
if ! command -v bash &>/dev/null; then
|
|
apk add --no-cache bash 2>/dev/null
|
|
fi
|
|
fi
|
|
|
|
if ! command -v curl &>/dev/null; then
|
|
install_package curl || log_warn "Could not install curl automatically"
|
|
fi
|
|
|
|
if ! command -v tar &>/dev/null; then
|
|
install_package tar || log_warn "Could not install tar automatically"
|
|
fi
|
|
|
|
if ! command -v ip &>/dev/null; then
|
|
case "$PKG_MANAGER" in
|
|
apt) install_package iproute2 || log_warn "Could not install iproute2" ;;
|
|
dnf|yum) install_package iproute || log_warn "Could not install iproute" ;;
|
|
pacman) install_package iproute2 || log_warn "Could not install iproute2" ;;
|
|
zypper) install_package iproute2 || log_warn "Could not install iproute2" ;;
|
|
apk) install_package iproute2 || log_warn "Could not install iproute2" ;;
|
|
esac
|
|
fi
|
|
|
|
if ! command -v tput &>/dev/null; then
|
|
case "$PKG_MANAGER" in
|
|
apt) install_package ncurses-bin || log_warn "Could not install ncurses-bin" ;;
|
|
apk) install_package ncurses || log_warn "Could not install ncurses" ;;
|
|
*) install_package ncurses || log_warn "Could not install ncurses" ;;
|
|
esac
|
|
fi
|
|
|
|
# iptables is required for firewall rules (server mode)
|
|
if ! command -v iptables &>/dev/null; then
|
|
log_info "Installing iptables..."
|
|
case "$PKG_MANAGER" in
|
|
apt) install_package iptables || log_warn "Could not install iptables - firewall rules may not work" ;;
|
|
dnf|yum) install_package iptables || log_warn "Could not install iptables" ;;
|
|
pacman) install_package iptables || log_warn "Could not install iptables" ;;
|
|
zypper) install_package iptables || log_warn "Could not install iptables" ;;
|
|
apk) install_package iptables || log_warn "Could not install iptables" ;;
|
|
*) log_warn "Please install iptables manually for firewall rules to work" ;;
|
|
esac
|
|
fi
|
|
|
|
# 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
|
|
}
|
|
|
|
detect_arch() {
|
|
local arch
|
|
arch=$(uname -m)
|
|
case "$arch" in
|
|
x86_64|amd64) echo "amd64" ;;
|
|
aarch64|arm64) echo "arm64" ;;
|
|
armv7l|armv7|armhf) echo "arm32" ;;
|
|
*)
|
|
log_error "Unsupported architecture: $arch"
|
|
log_error "Paqet supports amd64, arm64, and arm32 only"
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Input Validation Functions
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
_validate_port() { [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]; }
|
|
|
|
_validate_ip() {
|
|
[[ "$1" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1
|
|
local IFS='.'; set -- $1
|
|
[ "$1" -le 255 ] && [ "$2" -le 255 ] && [ "$3" -le 255 ] && [ "$4" -le 255 ]
|
|
}
|
|
|
|
_validate_mac() { [[ "$1" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; }
|
|
|
|
_validate_iface() { [[ "$1" =~ ^[a-zA-Z0-9._-]+$ ]] && [ ${#1} -le 64 ]; }
|
|
|
|
_validate_version_tag() {
|
|
[[ "$1" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]]
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Binary Download & Install
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
# Retry helper with exponential backoff for API requests
|
|
_curl_with_retry() {
|
|
local url="$1"
|
|
local max_attempts="${2:-3}"
|
|
local attempt=1
|
|
local delay=2
|
|
local response=""
|
|
while [ $attempt -le $max_attempts ]; do
|
|
response=$(curl -s --max-time 15 "$url" 2>/dev/null)
|
|
if [ -n "$response" ]; then
|
|
# Check for rate limit response
|
|
if echo "$response" | grep -q '"message".*rate limit'; then
|
|
log_warn "GitHub API rate limited, waiting ${delay}s (attempt $attempt/$max_attempts)"
|
|
sleep $delay
|
|
delay=$((delay * 2))
|
|
attempt=$((attempt + 1))
|
|
continue
|
|
fi
|
|
echo "$response"
|
|
return 0
|
|
fi
|
|
[ $attempt -lt $max_attempts ] && sleep $delay
|
|
delay=$((delay * 2))
|
|
attempt=$((attempt + 1))
|
|
done
|
|
return 1
|
|
}
|
|
|
|
get_latest_version() {
|
|
local response
|
|
response=$(_curl_with_retry "$PAQET_API_URL" 3)
|
|
if [ -z "$response" ]; then
|
|
log_error "Failed to query GitHub API after retries"
|
|
return 1
|
|
fi
|
|
local tag
|
|
tag=$(echo "$response" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"')
|
|
if [ -z "$tag" ]; then
|
|
log_error "Could not determine latest paqet version"
|
|
return 1
|
|
fi
|
|
if ! _validate_version_tag "$tag"; then
|
|
log_error "Invalid version tag format: $tag"
|
|
return 1
|
|
fi
|
|
echo "$tag"
|
|
}
|
|
|
|
download_paqet() {
|
|
local version="$1"
|
|
local arch
|
|
arch=$(detect_arch)
|
|
local os_name="linux"
|
|
local ext="tar.gz"
|
|
local filename="paqet-${os_name}-${arch}-${version}.${ext}"
|
|
local url="https://github.com/${PAQET_REPO}/releases/download/${version}/${filename}"
|
|
|
|
log_info "Downloading paqet ${version} for ${os_name}/${arch}..."
|
|
|
|
if ! mkdir -p "$INSTALL_DIR/bin"; then
|
|
log_error "Failed to create directory $INSTALL_DIR/bin"
|
|
return 1
|
|
fi
|
|
local tmp_file
|
|
tmp_file=$(mktemp "/tmp/paqet-download-XXXXXXXX.${ext}") || { log_error "Failed to create temp file"; return 1; }
|
|
|
|
# Try curl first, fallback to wget
|
|
local download_ok=false
|
|
if curl -sL --max-time 180 --retry 3 --retry-delay 5 --fail -o "$tmp_file" "$url" 2>/dev/null; then
|
|
download_ok=true
|
|
elif command -v wget &>/dev/null; then
|
|
log_info "curl failed, trying wget..."
|
|
rm -f "$tmp_file"
|
|
if wget -q --timeout=180 --tries=3 -O "$tmp_file" "$url" 2>/dev/null; then
|
|
download_ok=true
|
|
fi
|
|
fi
|
|
|
|
if [ "$download_ok" != "true" ]; then
|
|
log_error "Failed to download: $url"
|
|
log_error "Try manual download: wget '$url' and place binary in $INSTALL_DIR/bin/"
|
|
rm -f "$tmp_file"
|
|
return 1
|
|
fi
|
|
|
|
# Validate download
|
|
local fsize
|
|
fsize=$(stat -c%s "$tmp_file" 2>/dev/null || stat -f%z "$tmp_file" 2>/dev/null || wc -c < "$tmp_file" 2>/dev/null || echo 0)
|
|
if [ "$fsize" -lt 1000 ]; then
|
|
log_error "Downloaded file is too small ($fsize bytes). Download may have failed."
|
|
rm -f "$tmp_file"
|
|
return 1
|
|
fi
|
|
|
|
# Extract
|
|
log_info "Extracting..."
|
|
local tmp_extract
|
|
tmp_extract=$(mktemp -d "/tmp/paqet-extract-XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
|
|
if ! tar -xzf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then
|
|
log_error "Failed to extract archive"
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
return 1
|
|
fi
|
|
|
|
# Find the binary in extracted files
|
|
local binary_name="paqet_${os_name}_${arch}"
|
|
local found_binary=""
|
|
found_binary=$(find "$tmp_extract" -name "$binary_name" -type f 2>/dev/null | head -1)
|
|
if [ -z "$found_binary" ]; then
|
|
# Try alternate name patterns
|
|
found_binary=$(find "$tmp_extract" -name "paqet*" -type f -executable 2>/dev/null | head -1)
|
|
fi
|
|
if [ -z "$found_binary" ]; then
|
|
found_binary=$(find "$tmp_extract" -name "paqet*" -type f 2>/dev/null | head -1)
|
|
fi
|
|
|
|
if [ -z "$found_binary" ]; then
|
|
log_error "Could not find paqet binary in archive"
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
return 1
|
|
fi
|
|
|
|
# Stop paqet if running to avoid "Text file busy" error
|
|
if pgrep -f "$INSTALL_DIR/bin/paqet" &>/dev/null; then
|
|
log_info "Stopping paqet to update binary..."
|
|
pkill -f "$INSTALL_DIR/bin/paqet" 2>/dev/null || true
|
|
sleep 1
|
|
fi
|
|
|
|
if ! cp "$found_binary" "$INSTALL_DIR/bin/paqet"; then
|
|
log_error "Failed to copy paqet binary to $INSTALL_DIR/bin/"
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
return 1
|
|
fi
|
|
if ! chmod +x "$INSTALL_DIR/bin/paqet"; then
|
|
log_error "Failed to make paqet binary executable"
|
|
return 1
|
|
fi
|
|
|
|
# Copy example configs if they exist
|
|
find "$tmp_extract" -name "*.yaml.example" -exec cp {} "$INSTALL_DIR/" \; 2>/dev/null || true
|
|
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
|
|
# Verify binary runs
|
|
if "$INSTALL_DIR/bin/paqet" version &>/dev/null; then
|
|
log_success "paqet ${version} installed successfully"
|
|
else
|
|
log_warn "paqet binary installed but version check failed (may need libpcap)"
|
|
fi
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Network Auto-Detection
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
detect_network() {
|
|
log_info "Auto-detecting network configuration..."
|
|
|
|
# Default interface - handle both standard "via X dev Y" and OpenVZ "dev Y scope link" formats
|
|
# Standard: "default via 192.168.1.1 dev eth0" -> $5 = eth0
|
|
# OpenVZ: "default dev venet0 scope link" -> $3 = venet0
|
|
local _route_line
|
|
_route_line=$(ip route show default 2>/dev/null | head -1)
|
|
if [[ "$_route_line" == *" via "* ]]; then
|
|
# Standard format with gateway
|
|
DETECTED_IFACE=$(echo "$_route_line" | awk '{print $5}')
|
|
elif [[ "$_route_line" == *" dev "* ]]; then
|
|
# OpenVZ/direct format without gateway
|
|
DETECTED_IFACE=$(echo "$_route_line" | awk '{print $3}')
|
|
fi
|
|
|
|
# Validate detected interface exists
|
|
if [ -n "$DETECTED_IFACE" ] && ! ip link show "$DETECTED_IFACE" &>/dev/null; then
|
|
DETECTED_IFACE=""
|
|
fi
|
|
|
|
if [ -z "$DETECTED_IFACE" ]; then
|
|
# Skip loopback, docker, veth, bridge, and other virtual interfaces
|
|
# Note: grep -v returns exit 1 if no matches, so we add || true for pipefail
|
|
DETECTED_IFACE=$(ip -o link show 2>/dev/null | awk -F': ' '{gsub(/ /,"",$2); print $2}' | { grep -vE '^(lo|docker[0-9]|br-|veth|virbr|tun|tap|wg)' || true; } | head -1)
|
|
fi
|
|
|
|
# Local IP - wrap entire pipeline to prevent pipefail exit
|
|
if [ -n "$DETECTED_IFACE" ]; then
|
|
# Note: wrap in subshell with || true to handle cases where interface is invalid or has no IP
|
|
DETECTED_IP=$( (ip -4 addr show "$DETECTED_IFACE" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | { grep -o '[0-9.]*' || true; } | head -1) || true )
|
|
fi
|
|
if [ -z "$DETECTED_IP" ]; then
|
|
DETECTED_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
[ -z "$DETECTED_IP" ] && DETECTED_IP=$(ip -4 addr show scope global 2>/dev/null | awk '/inet /{gsub(/\/.*/, "", $2); print $2; exit}')
|
|
fi
|
|
|
|
# Gateway IP - only present in standard "via X" format, not in OpenVZ
|
|
if [[ "$_route_line" == *" via "* ]]; then
|
|
DETECTED_GATEWAY=$(echo "$_route_line" | awk '{print $3}')
|
|
else
|
|
DETECTED_GATEWAY=""
|
|
fi
|
|
|
|
# Gateway MAC
|
|
DETECTED_GW_MAC=""
|
|
if [ -n "$DETECTED_GATEWAY" ]; then
|
|
# Try ip neigh first (most reliable on Linux)
|
|
DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}')
|
|
if [ -z "$DETECTED_GW_MAC" ]; then
|
|
# Trigger ARP resolution
|
|
ping -c 1 -W 2 "$DETECTED_GATEWAY" &>/dev/null || true
|
|
sleep 1
|
|
DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}')
|
|
fi
|
|
if [ -z "$DETECTED_GW_MAC" ] && command -v arp &>/dev/null; then
|
|
# Fallback: parse arp output looking for MAC pattern
|
|
# Note: grep returns exit 1 if no matches, so we add || true for pipefail
|
|
DETECTED_GW_MAC=$(arp -n "$DETECTED_GATEWAY" 2>/dev/null | { grep -oE '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' || true; } | head -1)
|
|
fi
|
|
fi
|
|
|
|
log_info "Interface: ${DETECTED_IFACE:-unknown}"
|
|
log_info "Local IP: ${DETECTED_IP:-unknown}"
|
|
log_info "Gateway: ${DETECTED_GATEWAY:-unknown}"
|
|
log_info "GW MAC: ${DETECTED_GW_MAC:-unknown}"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Configuration Wizard
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
run_config_wizard() {
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} PAQCTL CONFIGURATION WIZARD${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
# Backend selection
|
|
echo -e "${BOLD}Select backend:${NC}"
|
|
echo " 1. paqet (Go/KCP, built-in SOCKS5, single binary)"
|
|
echo " 2. gfw-knocker (Python/QUIC, port forwarding + microsocks)"
|
|
echo ""
|
|
local backend_choice
|
|
read -p " Enter choice [1/2]: " backend_choice < /dev/tty || true
|
|
case "$backend_choice" in
|
|
2) BACKEND="gfw-knocker" ;;
|
|
*) BACKEND="paqet" ;;
|
|
esac
|
|
echo ""
|
|
log_info "Selected backend: $BACKEND"
|
|
echo ""
|
|
|
|
# Role selection
|
|
echo -e "${BOLD}Select role:${NC}"
|
|
echo " 1. Server (accept connections from clients)"
|
|
echo " 2. Client (connect to a server, provides SOCKS5 proxy)"
|
|
echo ""
|
|
local role_choice
|
|
read -p " Enter choice [1/2]: " role_choice < /dev/tty || true
|
|
case "$role_choice" in
|
|
1) ROLE="server" ;;
|
|
2) ROLE="client" ;;
|
|
*)
|
|
log_warn "Invalid choice. Defaulting to server."
|
|
ROLE="server"
|
|
;;
|
|
esac
|
|
echo ""
|
|
log_info "Selected role: $ROLE"
|
|
|
|
if [ "$BACKEND" = "paqet" ]; then
|
|
_wizard_paqet
|
|
else
|
|
_wizard_gfk
|
|
fi
|
|
|
|
# Save settings
|
|
save_settings
|
|
}
|
|
|
|
_wizard_paqet() {
|
|
# Auto-detect network
|
|
detect_network
|
|
echo ""
|
|
|
|
# Confirm/override interface
|
|
echo -e "${BOLD}Network interface${NC} [${DETECTED_IFACE:-eth0}]:"
|
|
read -p " Interface: " input < /dev/tty || true
|
|
IFACE="${input:-$DETECTED_IFACE}"
|
|
IFACE="${IFACE:-eth0}"
|
|
if ! _validate_iface "$IFACE"; then
|
|
log_warn "Invalid interface name. Using eth0."
|
|
IFACE="eth0"
|
|
fi
|
|
|
|
# Confirm/override local IP
|
|
echo -e "${BOLD}Local IP${NC} [${DETECTED_IP:-auto}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
LOCAL_IP="${input:-$DETECTED_IP}"
|
|
if [ -n "$LOCAL_IP" ] && ! _validate_ip "$LOCAL_IP"; then
|
|
log_warn "Invalid IP format. Using detected IP."
|
|
LOCAL_IP="$DETECTED_IP"
|
|
fi
|
|
|
|
# Confirm/override gateway MAC
|
|
echo -e "${BOLD}Gateway MAC address${NC} [${DETECTED_GW_MAC:-auto}]:"
|
|
read -p " MAC: " input < /dev/tty || true
|
|
GW_MAC="${input:-$DETECTED_GW_MAC}"
|
|
|
|
if [ -z "$GW_MAC" ] || ! _validate_mac "$GW_MAC"; then
|
|
if [ -n "$GW_MAC" ]; then
|
|
log_warn "Invalid MAC format detected."
|
|
else
|
|
log_error "Could not detect gateway MAC address."
|
|
fi
|
|
log_error "Please enter it manually (format: aa:bb:cc:dd:ee:ff)"
|
|
read -p " Gateway MAC: " GW_MAC < /dev/tty || true
|
|
if [ -z "$GW_MAC" ] || ! _validate_mac "$GW_MAC"; then
|
|
log_error "Valid gateway MAC is required for paqet to function."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
echo ""
|
|
echo -e "${BOLD}Listen port${NC} [8443]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
LISTEN_PORT="${input:-8443}"
|
|
if ! [[ "$LISTEN_PORT" =~ ^[0-9]+$ ]] || [ "$LISTEN_PORT" -lt 1 ] || [ "$LISTEN_PORT" -gt 65535 ]; then
|
|
log_warn "Invalid port. Using default 8443."
|
|
LISTEN_PORT=8443
|
|
fi
|
|
|
|
echo ""
|
|
log_info "Generating encryption key..."
|
|
ENCRYPTION_KEY=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true)
|
|
if [ -z "$ENCRYPTION_KEY" ]; then
|
|
log_warn "Could not auto-generate key. Using openssl fallback..."
|
|
ENCRYPTION_KEY=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32 || true)
|
|
fi
|
|
if [ -z "$ENCRYPTION_KEY" ] || [ "${#ENCRYPTION_KEY}" -lt 16 ]; then
|
|
log_error "Failed to generate a valid encryption key"
|
|
return 1
|
|
fi
|
|
echo ""
|
|
echo -e "${GREEN}${BOLD} Encryption Key: ${ENCRYPTION_KEY}${NC}"
|
|
echo ""
|
|
echo -e "${YELLOW} IMPORTANT: Save this key! Clients need it to connect.${NC}"
|
|
echo ""
|
|
SOCKS_PORT=""
|
|
else
|
|
echo ""
|
|
echo -e "${BOLD}Remote server address${NC} (IP:PORT):"
|
|
read -p " Server: " REMOTE_SERVER < /dev/tty || true
|
|
if [ -z "$REMOTE_SERVER" ]; then
|
|
log_error "Remote server address is required."
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${BOLD}Encryption key${NC} (from server setup):"
|
|
read -p " Key: " ENCRYPTION_KEY < /dev/tty || true
|
|
if [ -z "$ENCRYPTION_KEY" ]; then
|
|
log_error "Encryption key is required."
|
|
exit 1
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${BOLD}SOCKS5 listen port${NC} [1080]:"
|
|
read -p " SOCKS port: " input < /dev/tty || true
|
|
SOCKS_PORT="${input:-1080}"
|
|
if ! [[ "$SOCKS_PORT" =~ ^[0-9]+$ ]] || [ "$SOCKS_PORT" -lt 1 ] || [ "$SOCKS_PORT" -gt 65535 ]; then
|
|
log_warn "Invalid port. Using default 1080."
|
|
SOCKS_PORT=1080
|
|
fi
|
|
LISTEN_PORT=""
|
|
fi
|
|
|
|
# Generate YAML config
|
|
generate_config
|
|
}
|
|
|
|
_wizard_gfk() {
|
|
if [ "$ROLE" = "server" ]; then
|
|
# Server IP (this machine's public IP)
|
|
detect_network
|
|
echo ""
|
|
echo -e "${BOLD}This server's public IP${NC} [${DETECTED_IP:-}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
GFK_SERVER_IP="${input:-$DETECTED_IP}"
|
|
if [ -z "$GFK_SERVER_IP" ] || ! _validate_ip "$GFK_SERVER_IP"; then
|
|
log_error "Valid server IP is required."
|
|
exit 1
|
|
fi
|
|
|
|
# VIO TCP port (must be closed to OS, raw socket handles it)
|
|
echo ""
|
|
echo -e "${BOLD}VIO TCP port${NC} [45000] (raw socket port, must be blocked by firewall):"
|
|
read -p " Port: " input < /dev/tty || true
|
|
GFK_VIO_PORT="${input:-45000}"
|
|
if ! _validate_port "$GFK_VIO_PORT"; then
|
|
log_warn "Invalid port. Using default 45000."
|
|
GFK_VIO_PORT=45000
|
|
fi
|
|
|
|
# QUIC port
|
|
echo ""
|
|
echo -e "${BOLD}QUIC tunnel port${NC} [25000]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
GFK_QUIC_PORT="${input:-25000}"
|
|
if ! _validate_port "$GFK_QUIC_PORT"; then
|
|
log_warn "Invalid port. Using default 25000."
|
|
GFK_QUIC_PORT=25000
|
|
fi
|
|
|
|
# Auth code
|
|
echo ""
|
|
local auto_auth
|
|
auto_auth=$(openssl rand -base64 16 2>/dev/null | tr -d '=+/' | head -c 16)
|
|
echo -e "${BOLD}QUIC auth code${NC} [auto-generated]:"
|
|
read -p " Auth code: " input < /dev/tty || true
|
|
GFK_AUTH_CODE="${input:-$auto_auth}"
|
|
echo ""
|
|
echo -e "${GREEN}${BOLD} Auth Code: ${GFK_AUTH_CODE}${NC}"
|
|
echo ""
|
|
echo -e "${YELLOW} IMPORTANT: Save this auth code! Clients need it to connect.${NC}"
|
|
echo ""
|
|
|
|
# Port mappings
|
|
echo -e "${BOLD}TCP port mappings${NC} (local:remote, comma-separated) [14000:443]:"
|
|
echo -e " ${DIM}Example: 14000:443,15000:2096,16000:10809${NC}"
|
|
read -p " Mappings: " input < /dev/tty || true
|
|
GFK_PORT_MAPPINGS="${input:-14000:443}"
|
|
MICROSOCKS_PORT=""
|
|
|
|
else
|
|
# Client: server IP
|
|
echo ""
|
|
echo -e "${BOLD}Remote server IP${NC} (server's public IP):"
|
|
read -p " Server IP: " GFK_SERVER_IP < /dev/tty || true
|
|
if [ -z "$GFK_SERVER_IP" ] || ! _validate_ip "$GFK_SERVER_IP"; then
|
|
log_error "Valid server IP is required."
|
|
exit 1
|
|
fi
|
|
|
|
# Server's VIO TCP port (what port the server is listening on)
|
|
echo ""
|
|
echo -e "${BOLD}Server's VIO TCP port${NC} [45000] (must match server config):"
|
|
read -p " Port: " input < /dev/tty || true
|
|
GFK_VIO_PORT="${input:-45000}"
|
|
if ! _validate_port "$GFK_VIO_PORT"; then
|
|
log_warn "Invalid port. Using default 45000."
|
|
GFK_VIO_PORT=45000
|
|
fi
|
|
|
|
# Local VIO client port (client's local binding)
|
|
echo ""
|
|
echo -e "${BOLD}Local VIO client port${NC} [40000]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
GFK_VIO_CLIENT_PORT="${input:-40000}"
|
|
if ! _validate_port "$GFK_VIO_CLIENT_PORT"; then
|
|
log_warn "Invalid port. Using default 40000."
|
|
GFK_VIO_CLIENT_PORT=40000
|
|
fi
|
|
|
|
# Server's QUIC port
|
|
echo ""
|
|
echo -e "${BOLD}Server's QUIC port${NC} [25000] (must match server config):"
|
|
read -p " Port: " input < /dev/tty || true
|
|
GFK_QUIC_PORT="${input:-25000}"
|
|
if ! _validate_port "$GFK_QUIC_PORT"; then
|
|
log_warn "Invalid port. Using default 25000."
|
|
GFK_QUIC_PORT=25000
|
|
fi
|
|
|
|
# Local QUIC client port
|
|
echo ""
|
|
echo -e "${BOLD}Local QUIC client port${NC} [20000]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
GFK_QUIC_CLIENT_PORT="${input:-20000}"
|
|
if ! _validate_port "$GFK_QUIC_CLIENT_PORT"; then
|
|
log_warn "Invalid port. Using default 20000."
|
|
GFK_QUIC_CLIENT_PORT=20000
|
|
fi
|
|
|
|
# Auth code (from server)
|
|
echo ""
|
|
echo -e "${BOLD}QUIC auth code${NC} (from server setup):"
|
|
read -p " Auth code: " GFK_AUTH_CODE < /dev/tty || true
|
|
if [ -z "$GFK_AUTH_CODE" ]; then
|
|
log_error "Auth code is required."
|
|
exit 1
|
|
fi
|
|
|
|
# Port mappings (must match server)
|
|
echo ""
|
|
echo -e "${BOLD}TCP port mappings${NC} (must match server) [14000:443]:"
|
|
read -p " Mappings: " input < /dev/tty || true
|
|
GFK_PORT_MAPPINGS="${input:-14000:443}"
|
|
|
|
# SOCKS5 port via microsocks
|
|
echo ""
|
|
echo -e "${BOLD}SOCKS5 listen port${NC} (via microsocks) [1080]:"
|
|
read -p " SOCKS port: " input < /dev/tty || true
|
|
MICROSOCKS_PORT="${input:-1080}"
|
|
if ! _validate_port "$MICROSOCKS_PORT"; then
|
|
log_warn "Invalid port. Using default 1080."
|
|
MICROSOCKS_PORT=1080
|
|
fi
|
|
SOCKS_PORT="$MICROSOCKS_PORT"
|
|
fi
|
|
|
|
# Generate GFK config
|
|
generate_gfk_config
|
|
}
|
|
|
|
generate_config() {
|
|
log_info "Generating paqet configuration..."
|
|
|
|
# Validate required fields
|
|
if [ -z "$IFACE" ] || [ -z "$LOCAL_IP" ] || [ -z "$GW_MAC" ] || [ -z "$ENCRYPTION_KEY" ]; then
|
|
log_error "Missing required configuration fields (interface, ip, gateway_mac, or secret)"
|
|
return 1
|
|
fi
|
|
if [ "$ROLE" = "server" ]; then
|
|
if [ -z "$LISTEN_PORT" ]; then log_error "Missing listen port"; return 1; fi
|
|
else
|
|
if [ -z "$REMOTE_SERVER" ] || [ -z "$SOCKS_PORT" ]; then
|
|
log_error "Missing server address or SOCKS port"
|
|
return 1
|
|
fi
|
|
local _rs_ip="${REMOTE_SERVER%:*}" _rs_port="${REMOTE_SERVER##*:}"
|
|
if ! _validate_ip "$_rs_ip" || ! _validate_port "$_rs_port"; then
|
|
log_error "Server address must be valid IP:PORT (e.g. 1.2.3.4:8443)"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
# Escape YAML special characters to prevent injection
|
|
_escape_yaml() {
|
|
local s="$1"
|
|
# If value contains special chars, quote it
|
|
if [[ "$s" =~ [:\#\[\]\{\}\"\'\|\>\<\&\*\!\%\@\`] ]] || [[ "$s" =~ ^[[:space:]] ]] || [[ "$s" =~ [[:space:]]$ ]]; then
|
|
s="${s//\\/\\\\}" # Escape backslashes
|
|
s="${s//\"/\\\"}" # Escape double quotes
|
|
printf '"%s"' "$s"
|
|
else
|
|
printf '%s' "$s"
|
|
fi
|
|
}
|
|
|
|
# Ensure install directory exists
|
|
mkdir -p "$INSTALL_DIR" || { log_error "Failed to create $INSTALL_DIR"; return 1; }
|
|
|
|
local tmp_conf
|
|
tmp_conf=$(mktemp "$INSTALL_DIR/config.yaml.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
|
|
# Set permissions on temp file before writing (fixes race condition)
|
|
chmod 600 "$tmp_conf" 2>/dev/null
|
|
|
|
(
|
|
umask 077
|
|
local _y_iface _y_ip _y_mac _y_key _y_server _y_port
|
|
_y_iface=$(_escape_yaml "$IFACE")
|
|
_y_ip=$(_escape_yaml "$LOCAL_IP")
|
|
_y_mac=$(_escape_yaml "$GW_MAC")
|
|
_y_key=$(_escape_yaml "$ENCRYPTION_KEY")
|
|
# Build TCP flags YAML array (default: ["PA"])
|
|
local _tcp_local_flags _tcp_remote_flags
|
|
_tcp_local_flags=$(echo "${PAQET_TCP_LOCAL_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/')
|
|
_tcp_remote_flags=$(echo "${PAQET_TCP_REMOTE_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/')
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
cat > "$tmp_conf" << EOF
|
|
role: "server"
|
|
|
|
log:
|
|
level: "info"
|
|
|
|
listen:
|
|
addr: ":${LISTEN_PORT}"
|
|
|
|
network:
|
|
interface: "${_y_iface}"
|
|
ipv4:
|
|
addr: "${_y_ip}:${LISTEN_PORT}"
|
|
router_mac: "${_y_mac}"
|
|
tcp:
|
|
local_flag: ${_tcp_local_flags}
|
|
remote_flag: ${_tcp_remote_flags}
|
|
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_y_key}"
|
|
EOF
|
|
else
|
|
local _rs_ip="${REMOTE_SERVER%:*}" _rs_port="${REMOTE_SERVER##*:}"
|
|
_y_server=$(_escape_yaml "$REMOTE_SERVER")
|
|
cat > "$tmp_conf" << EOF
|
|
role: "client"
|
|
|
|
log:
|
|
level: "info"
|
|
|
|
socks5:
|
|
- listen: "127.0.0.1:${SOCKS_PORT}"
|
|
|
|
network:
|
|
interface: "${_y_iface}"
|
|
ipv4:
|
|
addr: "${_y_ip}:0"
|
|
router_mac: "${_y_mac}"
|
|
tcp:
|
|
local_flag: ${_tcp_local_flags}
|
|
remote_flag: ${_tcp_remote_flags}
|
|
|
|
server:
|
|
addr: "${_y_server}"
|
|
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_y_key}"
|
|
EOF
|
|
fi
|
|
)
|
|
if ! mv "$tmp_conf" "$INSTALL_DIR/config.yaml"; then
|
|
log_error "Failed to save configuration file"
|
|
rm -f "$tmp_conf"
|
|
return 1
|
|
fi
|
|
# Ensure final permissions (mv preserves source permissions on most systems)
|
|
chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null
|
|
log_success "Configuration saved to $INSTALL_DIR/config.yaml"
|
|
}
|
|
|
|
save_settings() {
|
|
# Preserve existing Telegram settings if present
|
|
local _tg_token="" _tg_chat="" _tg_interval=6 _tg_enabled=false
|
|
local _tg_alerts=true _tg_daily=true _tg_weekly=true _tg_label="" _tg_start_hour=0
|
|
if [ -f "$INSTALL_DIR/settings.conf" ]; then
|
|
# Safe settings loading without eval
|
|
while IFS='=' read -r key value; do
|
|
[[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue
|
|
# Remove surrounding quotes and sanitize value
|
|
value="${value#\"}"; value="${value%\"}"
|
|
# Validate value doesn't contain dangerous characters
|
|
if [[ "$value" =~ [\`\$\(] ]]; then
|
|
continue # Skip potentially dangerous values
|
|
fi
|
|
case "$key" in
|
|
TELEGRAM_BOT_TOKEN) _tg_token="$value" ;;
|
|
TELEGRAM_CHAT_ID) _tg_chat="$value" ;;
|
|
TELEGRAM_INTERVAL) [[ "$value" =~ ^[0-9]+$ ]] && _tg_interval="$value" ;;
|
|
TELEGRAM_ENABLED) _tg_enabled="$value" ;;
|
|
TELEGRAM_ALERTS_ENABLED) _tg_alerts="$value" ;;
|
|
TELEGRAM_DAILY_SUMMARY) _tg_daily="$value" ;;
|
|
TELEGRAM_WEEKLY_SUMMARY) _tg_weekly="$value" ;;
|
|
TELEGRAM_SERVER_LABEL) _tg_label="$value" ;;
|
|
TELEGRAM_START_HOUR) [[ "$value" =~ ^[0-9]+$ ]] && _tg_start_hour="$value" ;;
|
|
esac
|
|
done < <(grep '^[A-Z_][A-Z_0-9]*=' "$INSTALL_DIR/settings.conf")
|
|
fi
|
|
|
|
# Sanitize sensitive values - remove shell metacharacters and control chars
|
|
_sanitize_value() {
|
|
printf '%s' "$1" | tr -d '"$`\\'\''(){}[]<>|;&!\n\r\t'
|
|
}
|
|
local _safe_key; _safe_key=$(_sanitize_value "${ENCRYPTION_KEY:-}")
|
|
local _safe_auth; _safe_auth=$(_sanitize_value "${GFK_AUTH_CODE:-}")
|
|
local _tmp
|
|
_tmp=$(mktemp "$INSTALL_DIR/settings.conf.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
|
|
(
|
|
umask 077
|
|
cat > "$_tmp" << EOF
|
|
BACKEND="${BACKEND:-paqet}"
|
|
ROLE="${ROLE}"
|
|
PAQET_VERSION="${PAQET_VERSION:-unknown}"
|
|
PAQCTL_VERSION="${VERSION}"
|
|
LISTEN_PORT="${LISTEN_PORT:-}"
|
|
SOCKS_PORT="${SOCKS_PORT:-}"
|
|
INTERFACE="${IFACE:-}"
|
|
LOCAL_IP="${LOCAL_IP:-}"
|
|
GATEWAY_MAC="${GW_MAC:-}"
|
|
ENCRYPTION_KEY="${_safe_key}"
|
|
PAQET_TCP_LOCAL_FLAG="${PAQET_TCP_LOCAL_FLAG:-PA}"
|
|
PAQET_TCP_REMOTE_FLAG="${PAQET_TCP_REMOTE_FLAG:-PA}"
|
|
REMOTE_SERVER="${REMOTE_SERVER:-}"
|
|
GFK_VIO_PORT="${GFK_VIO_PORT:-}"
|
|
GFK_VIO_CLIENT_PORT="${GFK_VIO_CLIENT_PORT:-}"
|
|
GFK_QUIC_PORT="${GFK_QUIC_PORT:-}"
|
|
GFK_QUIC_CLIENT_PORT="${GFK_QUIC_CLIENT_PORT:-}"
|
|
GFK_AUTH_CODE="${_safe_auth}"
|
|
GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS:-}"
|
|
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"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# iptables Management
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
apply_iptables_rules() {
|
|
local port="$1"
|
|
if [ -z "$port" ]; then
|
|
log_error "No port specified for iptables rules"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Applying iptables rules for port $port..."
|
|
|
|
# Load required kernel modules
|
|
modprobe iptable_raw 2>/dev/null || true
|
|
modprobe iptable_mangle 2>/dev/null || true
|
|
|
|
# Warn about active firewall managers
|
|
if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
|
|
log_warn "ufw is active — ensure port $port/tcp is allowed: sudo ufw allow $port/tcp"
|
|
fi
|
|
if command -v firewall-cmd &>/dev/null && firewall-cmd --state 2>/dev/null | grep -q running; then
|
|
log_warn "firewalld is active — ensure port $port is open"
|
|
fi
|
|
|
|
# Tag for identifying paqctl rules
|
|
local TAG="paqctl"
|
|
|
|
# Check if all rules already exist (IPv4) - check both tagged and untagged
|
|
if iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null && \
|
|
iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null && \
|
|
iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null; then
|
|
log_info "iptables rules already in place"
|
|
else
|
|
# Apply missing IPv4 rules individually (tagged with "paqctl" comment)
|
|
iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
iptables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || {
|
|
log_error "Failed to add PREROUTING NOTRACK rule"
|
|
return 1
|
|
}
|
|
iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
iptables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || {
|
|
log_error "Failed to add OUTPUT NOTRACK rule"
|
|
return 1
|
|
}
|
|
iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
|
|
iptables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || {
|
|
log_error "Failed to add RST DROP rule"
|
|
return 1
|
|
}
|
|
log_success "IPv4 iptables rules applied"
|
|
fi
|
|
|
|
# Apply IPv6 rules if ip6tables is available (tagged with "paqctl" comment)
|
|
if command -v ip6tables &>/dev/null; then
|
|
local _ipv6_ok=true
|
|
ip6tables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
ip6tables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || _ipv6_ok=false
|
|
ip6tables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
ip6tables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || _ipv6_ok=false
|
|
ip6tables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
|
|
ip6tables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || _ipv6_ok=false
|
|
if [ "$_ipv6_ok" = true ]; then
|
|
log_success "IPv6 iptables rules applied"
|
|
else
|
|
log_warn "Some IPv6 iptables rules failed (IPv6 may not be available)"
|
|
fi
|
|
fi
|
|
|
|
# Persist rules
|
|
persist_iptables_rules
|
|
}
|
|
|
|
remove_iptables_rules() {
|
|
local port="$1"
|
|
if [ -z "$port" ]; then return 0; fi
|
|
|
|
local TAG="paqctl"
|
|
log_info "Removing iptables rules for port $port..."
|
|
# Remove tagged rules
|
|
iptables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
iptables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
iptables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
# Also remove untagged rules for backwards compatibility
|
|
iptables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true
|
|
iptables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true
|
|
iptables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
# Remove IPv6 rules
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
fi
|
|
log_success "iptables rules removed"
|
|
}
|
|
|
|
persist_iptables_rules() {
|
|
if command -v netfilter-persistent &>/dev/null; then
|
|
netfilter-persistent save 2>/dev/null || true
|
|
elif command -v iptables-save &>/dev/null; then
|
|
if [ -d /etc/iptables ]; then
|
|
iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
|
|
command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true
|
|
elif [ -f /etc/debian_version ] && [ ! -d /etc/iptables ]; then
|
|
mkdir -p /etc/iptables
|
|
iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
|
|
command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true
|
|
elif [ -d /etc/sysconfig ]; then
|
|
iptables-save > /etc/sysconfig/iptables 2>/dev/null || true
|
|
command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/sysconfig/ip6tables 2>/dev/null || true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
check_iptables_rules() {
|
|
local port="$1"
|
|
if [ -z "$port" ]; then return 1; fi
|
|
|
|
local TAG="paqctl"
|
|
local ok=true
|
|
iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || ok=false
|
|
iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || ok=false
|
|
iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || ok=false
|
|
|
|
if [ "$ok" = true ]; then
|
|
return 0
|
|
else
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# GFW-knocker Backend Functions
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
install_python_deps() {
|
|
log_info "Installing Python dependencies for GFW-knocker..."
|
|
if ! command -v python3 &>/dev/null; then
|
|
install_package python3
|
|
fi
|
|
# Ensure python3 version >= 3.10
|
|
local pyver
|
|
pyver=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || echo "0.0")
|
|
local pymajor pyminor
|
|
pymajor=$(echo "$pyver" | cut -d. -f1)
|
|
pyminor=$(echo "$pyver" | cut -d. -f2)
|
|
if [ "$pymajor" -lt 3 ] || { [ "$pymajor" -eq 3 ] && [ "$pyminor" -lt 10 ]; }; then
|
|
log_error "Python 3.10+ required, found $pyver"
|
|
return 1
|
|
fi
|
|
|
|
# Install venv support (varies by distro)
|
|
# - Debian/Ubuntu: needs python3-venv or python3.X-venv package
|
|
# - Fedora/RHEL/Arch/openSUSE: venv included with python3, just need pip
|
|
# - Alpine: needs py3-pip
|
|
case "$PKG_MANAGER" in
|
|
apt)
|
|
# Debian/Ubuntu needs python3-venv package (version-specific)
|
|
local pyver_full
|
|
pyver_full=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null)
|
|
if [ -n "$pyver_full" ]; then
|
|
install_package "python${pyver_full}-venv" || install_package "python3-venv"
|
|
else
|
|
install_package "python3-venv"
|
|
fi
|
|
;;
|
|
dnf)
|
|
# Fedora/RHEL 8+: venv is included with python3, just ensure pip
|
|
install_package "python3-pip" || true
|
|
;;
|
|
yum)
|
|
# Older RHEL/CentOS 7
|
|
install_package "python3-pip" || true
|
|
;;
|
|
pacman)
|
|
# Arch Linux: venv included with python, pip is separate
|
|
install_package "python-pip" || true
|
|
;;
|
|
zypper)
|
|
# openSUSE: venv included with python3
|
|
install_package "python3-pip" || true
|
|
;;
|
|
apk)
|
|
# Alpine
|
|
install_package "py3-pip" || true
|
|
;;
|
|
*)
|
|
# Try generic python3-venv, ignore if fails (venv may be built-in)
|
|
install_package "python3-venv" 2>/dev/null || true
|
|
;;
|
|
esac
|
|
|
|
# Create virtual environment
|
|
local VENV_DIR="$INSTALL_DIR/venv"
|
|
# Check if venv exists AND is complete (has pip)
|
|
if [ ! -x "$VENV_DIR/bin/pip" ]; then
|
|
# Remove broken/incomplete venv if exists
|
|
[ -d "$VENV_DIR" ] && rm -rf "$VENV_DIR"
|
|
log_info "Creating Python virtual environment..."
|
|
python3 -m venv "$VENV_DIR" || {
|
|
log_error "Failed to create virtual environment (is python3-venv installed?)"
|
|
return 1
|
|
}
|
|
fi
|
|
|
|
# Verify pip exists after venv creation
|
|
if [ ! -x "$VENV_DIR/bin/pip" ]; then
|
|
log_error "venv created but pip missing (install python3-venv package)"
|
|
return 1
|
|
fi
|
|
|
|
# Install packages in venv
|
|
log_info "Installing scapy and aioquic in venv..."
|
|
"$VENV_DIR/bin/pip" install --quiet --upgrade pip 2>/dev/null || true
|
|
"$VENV_DIR/bin/pip" install --quiet scapy aioquic 2>/dev/null || {
|
|
# Try with --break-system-packages as fallback (shouldn't be needed in venv)
|
|
"$VENV_DIR/bin/pip" install scapy aioquic || {
|
|
log_error "Failed to install Python packages (scapy, aioquic)"
|
|
return 1
|
|
}
|
|
}
|
|
|
|
# Verify
|
|
if "$VENV_DIR/bin/python" -c "import scapy; import aioquic" 2>/dev/null; then
|
|
log_success "Python dependencies installed (scapy, aioquic)"
|
|
else
|
|
log_error "Python package verification failed"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
install_microsocks() {
|
|
log_info "Installing microsocks for SOCKS5 proxy..."
|
|
if [ -x "$INSTALL_DIR/bin/microsocks" ]; then
|
|
log_success "microsocks already installed"
|
|
return 0
|
|
fi
|
|
# Build dependencies
|
|
command -v gcc &>/dev/null || install_package gcc
|
|
command -v make &>/dev/null || install_package make
|
|
local tmp_dir
|
|
tmp_dir=$(mktemp -d)
|
|
if ! curl -sL "https://github.com/${MICROSOCKS_REPO}/archive/refs/heads/master.tar.gz" -o "$tmp_dir/microsocks.tar.gz"; then
|
|
log_error "Failed to download microsocks"
|
|
rm -rf "$tmp_dir"
|
|
return 1
|
|
fi
|
|
tar -xzf "$tmp_dir/microsocks.tar.gz" -C "$tmp_dir" 2>/dev/null || {
|
|
log_error "Failed to extract microsocks"
|
|
rm -rf "$tmp_dir"
|
|
return 1
|
|
}
|
|
local src_dir
|
|
src_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "microsocks*" | head -1)
|
|
if [ -z "$src_dir" ]; then
|
|
log_error "microsocks source directory not found"
|
|
rm -rf "$tmp_dir"
|
|
return 1
|
|
fi
|
|
if ! make -C "$src_dir" -j"$(nproc 2>/dev/null || echo 1)" 2>/dev/null; then
|
|
log_error "Failed to compile microsocks"
|
|
rm -rf "$tmp_dir"
|
|
return 1
|
|
fi
|
|
mkdir -p "$INSTALL_DIR/bin"
|
|
cp "$src_dir/microsocks" "$INSTALL_DIR/bin/microsocks"
|
|
chmod 755 "$INSTALL_DIR/bin/microsocks"
|
|
rm -rf "$tmp_dir"
|
|
log_success "microsocks installed"
|
|
}
|
|
|
|
#───────────────────────────────────────────────────────────────────────
|
|
# Xray Installation (for GFK server - provides SOCKS5 on port 443)
|
|
#───────────────────────────────────────────────────────────────────────
|
|
|
|
XRAY_CONFIG_DIR="/usr/local/etc/xray"
|
|
XRAY_CONFIG_FILE="$XRAY_CONFIG_DIR/config.json"
|
|
|
|
check_xray_installed() {
|
|
command -v xray &>/dev/null || [ -x /usr/local/bin/xray ]
|
|
}
|
|
|
|
install_xray() {
|
|
if check_xray_installed; then
|
|
log_info "Xray is already installed"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Installing Xray ${XRAY_VERSION_PINNED}..."
|
|
|
|
# Use official Xray install script with pinned version for stability
|
|
local tmp_script
|
|
tmp_script=$(mktemp)
|
|
if ! curl -sL https://github.com/XTLS/Xray-install/raw/main/install-release.sh -o "$tmp_script"; then
|
|
log_error "Failed to download Xray installer"
|
|
rm -f "$tmp_script"
|
|
return 1
|
|
fi
|
|
|
|
# Install specific version (not latest) for stability
|
|
if ! bash "$tmp_script" install --version "$XRAY_VERSION_PINNED" 2>/dev/null; then
|
|
log_error "Failed to install Xray"
|
|
rm -f "$tmp_script"
|
|
return 1
|
|
fi
|
|
rm -f "$tmp_script"
|
|
|
|
log_success "Xray ${XRAY_VERSION_PINNED} installed"
|
|
}
|
|
|
|
configure_xray_socks() {
|
|
local listen_port="${1:-443}"
|
|
|
|
log_info "Configuring Xray SOCKS5 proxy on port $listen_port..."
|
|
|
|
mkdir -p "$XRAY_CONFIG_DIR"
|
|
|
|
# Create simple SOCKS5 inbound config
|
|
cat > "$XRAY_CONFIG_FILE" << EOF
|
|
{
|
|
"log": {
|
|
"loglevel": "warning"
|
|
},
|
|
"inbounds": [
|
|
{
|
|
"tag": "socks-in",
|
|
"port": ${listen_port},
|
|
"listen": "127.0.0.1",
|
|
"protocol": "socks",
|
|
"settings": {
|
|
"auth": "noauth",
|
|
"udp": true
|
|
},
|
|
"sniffing": {
|
|
"enabled": true,
|
|
"destOverride": ["http", "tls"]
|
|
}
|
|
}
|
|
],
|
|
"outbounds": [
|
|
{
|
|
"tag": "direct",
|
|
"protocol": "freedom",
|
|
"settings": {}
|
|
}
|
|
]
|
|
}
|
|
EOF
|
|
chmod 644 "$XRAY_CONFIG_FILE" # Xray service runs as 'nobody', needs read access
|
|
log_success "Xray configured (SOCKS5 on 127.0.0.1:$listen_port)"
|
|
}
|
|
|
|
# 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
|
|
if [ -x /usr/local/bin/xray ]; then
|
|
pkill -x xray 2>/dev/null || true
|
|
sleep 1
|
|
nohup /usr/local/bin/xray run -c "$XRAY_CONFIG_FILE" > /var/log/xray.log 2>&1 &
|
|
sleep 2
|
|
if pgrep -x xray &>/dev/null; then
|
|
log_success "Xray started"
|
|
return 0
|
|
fi
|
|
fi
|
|
log_error "Failed to start Xray"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
stop_xray() {
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
systemctl stop xray 2>/dev/null || true
|
|
else
|
|
pkill -x xray 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
setup_xray_for_gfk() {
|
|
local target_port
|
|
target_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f2 | cut -d, -f1)
|
|
|
|
if pgrep -x xray &>/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"
|
|
local msport="${MICROSOCKS_PORT:-1080}"
|
|
mkdir -p "$INSTALL_DIR/bin"
|
|
cat > "$wrapper" << 'WRAPEOF'
|
|
#!/bin/bash
|
|
set -e
|
|
GFK_DIR="REPLACE_ME_GFK_DIR"
|
|
INSTALL_DIR="REPLACE_ME_INSTALL_DIR"
|
|
MICROSOCKS_PORT="REPLACE_ME_MSPORT"
|
|
|
|
cd "$GFK_DIR"
|
|
"$INSTALL_DIR/venv/bin/python" mainclient.py &
|
|
PID1=$!
|
|
"$INSTALL_DIR/bin/microsocks" -i 127.0.0.1 -p "$MICROSOCKS_PORT" &
|
|
PID2=$!
|
|
trap "kill $PID1 $PID2 2>/dev/null; wait" EXIT INT TERM
|
|
wait
|
|
WRAPEOF
|
|
sed "s#REPLACE_ME_GFK_DIR#${GFK_DIR}#g; s#REPLACE_ME_INSTALL_DIR#${INSTALL_DIR}#g; s#REPLACE_ME_MSPORT#${msport}#g" "$wrapper" > "$wrapper.sed" && mv "$wrapper.sed" "$wrapper"
|
|
chmod 755 "$wrapper"
|
|
log_success "Client wrapper created at $wrapper"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Service Management
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
setup_service() {
|
|
log_info "Setting up auto-start on boot..."
|
|
|
|
# Check which backends are installed
|
|
local paqet_installed=false gfk_installed=false
|
|
[ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
|
|
if [ "$ROLE" = "server" ]; then
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
|
|
else
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
|
|
fi
|
|
|
|
# If both backends are installed, create a combined service
|
|
local _both_installed=false
|
|
[ "$paqet_installed" = true ] && [ "$gfk_installed" = true ] && _both_installed=true
|
|
|
|
# Compute ExecStart based on backend
|
|
local _exec_start _working_dir _svc_desc _svc_type="simple"
|
|
if [ "$_both_installed" = true ]; then
|
|
_svc_desc="Paqet Combined Proxy Service (Paqet + GFK)"
|
|
_working_dir="${INSTALL_DIR}"
|
|
_svc_type="forking"
|
|
# Create a wrapper script that starts both backends
|
|
cat > "${INSTALL_DIR}/bin/start-both.sh" << BOTH_SCRIPT
|
|
#!/bin/bash
|
|
INSTALL_DIR="/opt/paqctl"
|
|
GFK_DIR="\${INSTALL_DIR}/gfk"
|
|
ROLE="${ROLE}"
|
|
|
|
# Source config for ports
|
|
[ -f "\${INSTALL_DIR}/settings.conf" ] && . "\${INSTALL_DIR}/settings.conf"
|
|
|
|
# Apply firewall rules (server only)
|
|
if [ "\$ROLE" = "server" ]; then
|
|
# Apply paqet firewall rules (NOTRACK for KCP)
|
|
modprobe iptable_raw 2>/dev/null || true
|
|
modprobe iptable_mangle 2>/dev/null || true
|
|
port="\${LISTEN_PORT:-8443}"
|
|
TAG="paqctl"
|
|
iptables -t raw -C PREROUTING -p tcp --dport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
|
|
iptables -t raw -A PREROUTING -p tcp --dport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
|
|
iptables -t raw -C OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
|
|
iptables -t raw -A OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
|
|
iptables -t mangle -C OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \\
|
|
iptables -t mangle -A OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" --tcp-flags RST RST -j DROP 2>/dev/null
|
|
|
|
# Apply GFK firewall rules (DROP on VIO port + NOTRACK to bypass conntrack)
|
|
vio_port="\${GFK_VIO_PORT:-45000}"
|
|
modprobe iptable_raw 2>/dev/null || true
|
|
iptables -t raw -C PREROUTING -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
|
|
iptables -t raw -A PREROUTING -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
|
|
iptables -t raw -C OUTPUT -p tcp --sport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\
|
|
iptables -t raw -A OUTPUT -p tcp --sport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null
|
|
iptables -C INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
|
|
iptables -A INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null
|
|
iptables -C OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
|
|
iptables -A OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null
|
|
# IPv6 rules for GFK
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -C INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
|
|
ip6tables -A INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || true
|
|
ip6tables -C OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\
|
|
ip6tables -A OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || true
|
|
fi
|
|
fi
|
|
|
|
# Start paqet backend
|
|
(umask 077; touch /var/log/paqet-backend.log)
|
|
nohup "\${INSTALL_DIR}/bin/paqet" run -c "\${INSTALL_DIR}/config.yaml" > /var/log/paqet-backend.log 2>&1 &
|
|
echo \$! > /run/paqet-backend.pid
|
|
|
|
# Start GFK backend
|
|
(umask 077; touch /var/log/gfk-backend.log)
|
|
if [ "\$ROLE" = "server" ]; then
|
|
# Start Xray if available
|
|
if command -v xray &>/dev/null || [ -x /usr/local/bin/xray ]; then
|
|
if ! pgrep -f "xray run" &>/dev/null; then
|
|
systemctl start xray 2>/dev/null || xray run -c /usr/local/etc/xray/config.json &>/dev/null &
|
|
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.14"
|
|
XRAY_VERSION_PINNED="v26.2.4"
|
|
GFK_VERSION_PINNED="v1.0.0"
|
|
|
|
INSTALL_DIR="REPLACE_ME_INSTALL_DIR"
|
|
BACKUP_DIR="$INSTALL_DIR/backups"
|
|
PAQET_REPO="hanselime/paqet"
|
|
PAQET_API_URL="https://api.github.com/repos/${PAQET_REPO}/releases/latest"
|
|
GFK_REPO="SamNet-dev/paqctl"
|
|
GFK_BRANCH="main"
|
|
GFK_RAW_URL="https://raw.githubusercontent.com/${GFK_REPO}/${GFK_BRANCH}/gfk"
|
|
GFK_DIR="$INSTALL_DIR/gfk"
|
|
MICROSOCKS_REPO="rofl0r/microsocks"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
MAGENTA='\033[0;35m'
|
|
BOLD='\033[1m'
|
|
DIM='\033[2m'
|
|
NC='\033[0m'
|
|
|
|
# Input validation helpers
|
|
_validate_port() { [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]; }
|
|
_validate_ip() {
|
|
[[ "$1" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1
|
|
local IFS='.'; set -- $1
|
|
[ "$1" -le 255 ] && [ "$2" -le 255 ] && [ "$3" -le 255 ] && [ "$4" -le 255 ]
|
|
}
|
|
_validate_mac() { [[ "$1" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; }
|
|
_validate_iface() { [[ "$1" =~ ^[a-zA-Z0-9._-]+$ ]] && [ ${#1} -le 64 ]; }
|
|
# Safe string length check - prevents DoS via extremely long inputs
|
|
_check_length() { [ ${#1} -le "${2:-256}" ]; }
|
|
|
|
# Network auto-detection
|
|
detect_network() {
|
|
log_info "Auto-detecting network configuration..."
|
|
|
|
# Default interface - handle both standard "via X dev Y" and OpenVZ "dev Y scope link" formats
|
|
# Standard: "default via 192.168.1.1 dev eth0" -> $5 = eth0
|
|
# OpenVZ: "default dev venet0 scope link" -> $3 = venet0
|
|
local _route_line
|
|
_route_line=$(ip route show default 2>/dev/null | head -1)
|
|
if [[ "$_route_line" == *" via "* ]]; then
|
|
# Standard format with gateway
|
|
DETECTED_IFACE=$(echo "$_route_line" | awk '{print $5}')
|
|
elif [[ "$_route_line" == *" dev "* ]]; then
|
|
# OpenVZ/direct format without gateway
|
|
DETECTED_IFACE=$(echo "$_route_line" | awk '{print $3}')
|
|
fi
|
|
|
|
# Validate detected interface exists
|
|
if [ -n "$DETECTED_IFACE" ] && ! ip link show "$DETECTED_IFACE" &>/dev/null; then
|
|
DETECTED_IFACE=""
|
|
fi
|
|
|
|
if [ -z "$DETECTED_IFACE" ]; then
|
|
# Note: grep -v returns exit 1 if no matches, so we add || true for pipefail
|
|
DETECTED_IFACE=$(ip -o link show 2>/dev/null | awk -F': ' '{gsub(/ /,"",$2); print $2}' | { grep -vE '^(lo|docker[0-9]|br-|veth|virbr|tun|tap|wg)' || true; } | head -1)
|
|
fi
|
|
|
|
# Local IP - wrap entire pipeline to prevent pipefail exit
|
|
if [ -n "$DETECTED_IFACE" ]; then
|
|
# Note: wrap in subshell with || true to handle cases where interface is invalid or has no IP
|
|
DETECTED_IP=$( (ip -4 addr show "$DETECTED_IFACE" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | { grep -o '[0-9.]*' || true; } | head -1) || true )
|
|
fi
|
|
if [ -z "$DETECTED_IP" ]; then
|
|
DETECTED_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
[ -z "$DETECTED_IP" ] && DETECTED_IP=$(ip -4 addr show scope global 2>/dev/null | awk '/inet /{gsub(/\/.*/, "", $2); print $2; exit}')
|
|
fi
|
|
|
|
# Gateway IP - handle OpenVZ format (may not have gateway)
|
|
if [[ "$_route_line" == *" via "* ]]; then
|
|
DETECTED_GATEWAY=$(echo "$_route_line" | awk '{print $3}')
|
|
else
|
|
DETECTED_GATEWAY=""
|
|
fi
|
|
|
|
# Gateway MAC
|
|
DETECTED_GW_MAC=""
|
|
if [ -n "$DETECTED_GATEWAY" ]; then
|
|
DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}')
|
|
if [ -z "$DETECTED_GW_MAC" ]; then
|
|
ping -c 1 -W 2 "$DETECTED_GATEWAY" &>/dev/null || true
|
|
sleep 1
|
|
DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}')
|
|
fi
|
|
if [ -z "$DETECTED_GW_MAC" ] && command -v arp &>/dev/null; then
|
|
# Note: grep returns exit 1 if no matches, so we add || true for pipefail
|
|
DETECTED_GW_MAC=$(arp -n "$DETECTED_GATEWAY" 2>/dev/null | { grep -oE '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' || true; } | head -1)
|
|
fi
|
|
fi
|
|
|
|
log_info "Interface: ${DETECTED_IFACE:-unknown}"
|
|
log_info "Local IP: ${DETECTED_IP:-unknown}"
|
|
log_info "Gateway: ${DETECTED_GATEWAY:-unknown}"
|
|
log_info "GW MAC: ${DETECTED_GW_MAC:-unknown}"
|
|
}
|
|
|
|
_load_settings() {
|
|
[ -f "$INSTALL_DIR/settings.conf" ] || return 0
|
|
# Safe settings loading without eval - uses case statement
|
|
while IFS='=' read -r key value; do
|
|
[[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue
|
|
value="${value#\"}"; value="${value%\"}"
|
|
# Skip values with dangerous shell characters
|
|
[[ "$value" =~ [\`\$\(] ]] && continue
|
|
case "$key" in
|
|
BACKEND) BACKEND="$value" ;;
|
|
ROLE) ROLE="$value" ;;
|
|
PAQET_VERSION) PAQET_VERSION="$value" ;;
|
|
PAQCTL_VERSION) PAQCTL_VERSION="$value" ;;
|
|
LISTEN_PORT) [[ "$value" =~ ^[0-9]*$ ]] && LISTEN_PORT="$value" ;;
|
|
SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && SOCKS_PORT="$value" ;;
|
|
INTERFACE) INTERFACE="$value" ;;
|
|
LOCAL_IP) LOCAL_IP="$value" ;;
|
|
GATEWAY_MAC) GATEWAY_MAC="$value" ;;
|
|
ENCRYPTION_KEY) ENCRYPTION_KEY="$value" ;;
|
|
PAQET_TCP_LOCAL_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_LOCAL_FLAG="$value" ;;
|
|
PAQET_TCP_REMOTE_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_REMOTE_FLAG="$value" ;;
|
|
REMOTE_SERVER) REMOTE_SERVER="$value" ;;
|
|
GFK_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_PORT="$value" ;;
|
|
GFK_VIO_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_CLIENT_PORT="$value" ;;
|
|
GFK_QUIC_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_PORT="$value" ;;
|
|
GFK_QUIC_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_CLIENT_PORT="$value" ;;
|
|
GFK_AUTH_CODE) GFK_AUTH_CODE="$value" ;;
|
|
GFK_PORT_MAPPINGS) GFK_PORT_MAPPINGS="$value" ;;
|
|
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"
|
|
local msport="${MICROSOCKS_PORT:-1080}"
|
|
mkdir -p "$INSTALL_DIR/bin"
|
|
cat > "$wrapper" << 'WEOF'
|
|
#!/bin/bash
|
|
set -e
|
|
GFK_DIR="REPLACE_GFK"
|
|
INSTALL_DIR="REPLACE_INST"
|
|
MICROSOCKS_PORT="REPLACE_MSP"
|
|
cd "$GFK_DIR"
|
|
"$INSTALL_DIR/venv/bin/python" mainclient.py &
|
|
PID1=$!
|
|
"$INSTALL_DIR/bin/microsocks" -i 127.0.0.1 -p "$MICROSOCKS_PORT" &
|
|
PID2=$!
|
|
trap "kill $PID1 $PID2 2>/dev/null; wait" EXIT INT TERM
|
|
wait
|
|
WEOF
|
|
sed "s#REPLACE_GFK#${GFK_DIR}#g; s#REPLACE_INST#${INSTALL_DIR}#g; s#REPLACE_MSP#${msport}#g" "$wrapper" > "$wrapper.sed" && mv "$wrapper.sed" "$wrapper"
|
|
chmod 755 "$wrapper"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Service Control
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
is_running() {
|
|
# Check which backends are installed
|
|
local paqet_installed=false gfk_installed=false
|
|
[ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
|
|
if [ "$ROLE" = "server" ]; then
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
|
|
else
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
|
|
fi
|
|
|
|
# If both backends installed, return true if EITHER is running
|
|
if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then
|
|
is_paqet_running && return 0
|
|
is_gfk_running && return 0
|
|
return 1
|
|
fi
|
|
|
|
# Single backend mode - original logic
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
systemctl is-active paqctl.service &>/dev/null && return 0
|
|
elif [ -f /run/paqctl.pid ]; then
|
|
local _pid
|
|
_pid=$(cat /run/paqctl.pid 2>/dev/null)
|
|
# Validate PID is numeric and process exists
|
|
[[ "$_pid" =~ ^[0-9]+$ ]] && kill -0 "$_pid" 2>/dev/null && return 0
|
|
fi
|
|
# Also check for the process directly with more specific patterns
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
# Use full path matching to avoid false positives
|
|
pgrep -f "${GFK_DIR}/mainserver.py" &>/dev/null && return 0
|
|
pgrep -f "${GFK_DIR}/mainclient.py" &>/dev/null && return 0
|
|
pgrep -f "${INSTALL_DIR}/bin/gfk-client.sh" &>/dev/null && return 0
|
|
else
|
|
# Match specific config file path
|
|
pgrep -f "${INSTALL_DIR}/bin/paqet run -c ${INSTALL_DIR}/config.yaml" &>/dev/null && return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Check if paqet backend specifically is running
|
|
is_paqet_running() {
|
|
pgrep -f "${INSTALL_DIR}/bin/paqet run -c ${INSTALL_DIR}/config.yaml" &>/dev/null && return 0
|
|
return 1
|
|
}
|
|
|
|
# Check if GFK backend specifically is running
|
|
is_gfk_running() {
|
|
if [ "$ROLE" = "server" ]; then
|
|
pgrep -f "${GFK_DIR}/mainserver.py" &>/dev/null && return 0
|
|
else
|
|
pgrep -f "${GFK_DIR}/mainclient.py" &>/dev/null && return 0
|
|
pgrep -f "${INSTALL_DIR}/bin/gfk-client.sh" &>/dev/null && return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Start paqet backend only
|
|
start_paqet_backend() {
|
|
if is_paqet_running; then
|
|
log_warn "paqet is already running"
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -f "$INSTALL_DIR/bin/paqet" ]; then
|
|
log_error "paqet binary not installed. Use 'Install additional backend' first."
|
|
return 1
|
|
fi
|
|
|
|
# Generate config.yaml if missing - prompt for values
|
|
if [ ! -f "$INSTALL_DIR/config.yaml" ]; then
|
|
echo ""
|
|
echo -e "${YELLOW}config.yaml not found. Let's configure paqet:${NC}"
|
|
echo ""
|
|
|
|
detect_network
|
|
local _det_iface="$DETECTED_IFACE"
|
|
local _det_ip="$DETECTED_IP"
|
|
local _det_mac="$DETECTED_GW_MAC"
|
|
|
|
echo -e "${BOLD}Network Interface${NC} [${_det_iface:-eth0}]:"
|
|
read -p " Interface: " input < /dev/tty || true
|
|
local _iface="${input:-${_det_iface:-eth0}}"
|
|
|
|
echo -e "${BOLD}Local IP${NC} [${_det_ip:-}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
local _local_ip="${input:-$_det_ip}"
|
|
|
|
echo -e "${BOLD}Gateway MAC${NC} [${_det_mac:-}]:"
|
|
read -p " MAC: " input < /dev/tty || true
|
|
local _gw_mac="${input:-$_det_mac}"
|
|
|
|
local _key
|
|
_key=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true)
|
|
if [ -z "$_key" ]; then
|
|
_key=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32 || true)
|
|
fi
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
echo -e "${BOLD}Listen Port${NC} [8443]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
local _port="${input:-8443}"
|
|
|
|
echo ""
|
|
echo -e "${GREEN}${BOLD} Generated Key: ${_key}${NC}"
|
|
echo -e "${BOLD}Encryption Key${NC} (Enter to use generated):"
|
|
read -p " Key: " input < /dev/tty || true
|
|
[ -n "$input" ] && _key="$input"
|
|
|
|
LISTEN_PORT="$_port"
|
|
ENCRYPTION_KEY="$_key"
|
|
|
|
cat > "$INSTALL_DIR/config.yaml" << EOFCFG
|
|
role: "server"
|
|
log:
|
|
level: "info"
|
|
listen:
|
|
addr: ":${_port}"
|
|
network:
|
|
interface: "${_iface}"
|
|
ipv4:
|
|
addr: "${_local_ip}:${_port}"
|
|
router_mac: "${_gw_mac}"
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_key}"
|
|
EOFCFG
|
|
else
|
|
echo -e "${BOLD}Remote Server${NC} (IP:PORT):"
|
|
read -p " Server: " input < /dev/tty || true
|
|
local _server="${input:-${REMOTE_SERVER:-}}"
|
|
|
|
echo -e "${BOLD}Encryption Key${NC} (from server):"
|
|
read -p " Key: " input < /dev/tty || true
|
|
[ -n "$input" ] && _key="$input"
|
|
|
|
echo -e "${BOLD}SOCKS5 Port${NC} [1080]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
local _socks="${input:-1080}"
|
|
|
|
REMOTE_SERVER="$_server"
|
|
SOCKS_PORT="$_socks"
|
|
ENCRYPTION_KEY="$_key"
|
|
|
|
cat > "$INSTALL_DIR/config.yaml" << EOFCFG
|
|
role: "client"
|
|
log:
|
|
level: "info"
|
|
socks5:
|
|
- listen: "127.0.0.1:${_socks}"
|
|
network:
|
|
interface: "${_iface}"
|
|
ipv4:
|
|
addr: "${_local_ip}:0"
|
|
router_mac: "${_gw_mac}"
|
|
server:
|
|
addr: "${_server}"
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_key}"
|
|
EOFCFG
|
|
fi
|
|
|
|
if [ ! -f "$INSTALL_DIR/config.yaml" ]; then
|
|
log_error "Failed to write config.yaml"
|
|
return 1
|
|
fi
|
|
chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null
|
|
INTERFACE="$_iface"
|
|
LOCAL_IP="$_local_ip"
|
|
GATEWAY_MAC="$_gw_mac"
|
|
save_settings 2>/dev/null || true
|
|
log_success "Configuration saved"
|
|
echo ""
|
|
fi
|
|
|
|
log_info "Starting paqet backend..."
|
|
|
|
# Apply paqet firewall rules
|
|
local _saved_backend="$BACKEND"
|
|
BACKEND="paqet"
|
|
_apply_firewall
|
|
BACKEND="$_saved_backend"
|
|
|
|
(umask 077; touch /var/log/paqet-backend.log)
|
|
nohup "$INSTALL_DIR/bin/paqet" run -c "$INSTALL_DIR/config.yaml" > /var/log/paqet-backend.log 2>&1 &
|
|
echo $! > /run/paqet-backend.pid
|
|
|
|
sleep 2
|
|
if is_paqet_running; then
|
|
log_success "paqet backend started"
|
|
else
|
|
log_error "paqet failed to start. Check: tail /var/log/paqet-backend.log"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Stop paqet backend only
|
|
stop_paqet_backend() {
|
|
if ! is_paqet_running; then
|
|
log_warn "paqet is not running"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Stopping paqet backend..."
|
|
|
|
if [ -f /run/paqet-backend.pid ]; then
|
|
local _pid
|
|
_pid=$(cat /run/paqet-backend.pid 2>/dev/null)
|
|
if [ -n "$_pid" ] && [[ "$_pid" =~ ^[0-9]+$ ]]; then
|
|
kill "$_pid" 2>/dev/null
|
|
sleep 1
|
|
kill -0 "$_pid" 2>/dev/null && kill -9 "$_pid" 2>/dev/null
|
|
fi
|
|
rm -f /run/paqet-backend.pid
|
|
fi
|
|
|
|
pkill -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true
|
|
|
|
# Remove paqet firewall rules
|
|
local _saved_backend="$BACKEND"
|
|
BACKEND="paqet"
|
|
_remove_firewall
|
|
BACKEND="$_saved_backend"
|
|
|
|
sleep 1
|
|
if ! is_paqet_running; then
|
|
log_success "paqet backend stopped"
|
|
else
|
|
pkill -9 -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true
|
|
log_success "paqet backend stopped (forced)"
|
|
fi
|
|
}
|
|
|
|
# Start GFK backend only
|
|
start_gfk_backend() {
|
|
if is_gfk_running; then
|
|
log_warn "gfw-knocker is already running"
|
|
return 0
|
|
fi
|
|
|
|
if [ ! -d "$GFK_DIR" ] || [ ! -f "$GFK_DIR/quic_server.py" ]; then
|
|
log_error "gfw-knocker not installed. Use 'Install additional backend' first."
|
|
return 1
|
|
fi
|
|
|
|
log_info "Starting gfw-knocker backend..."
|
|
|
|
# Apply GFK firewall rules
|
|
local _saved_backend="$BACKEND"
|
|
BACKEND="gfw-knocker"
|
|
_apply_firewall
|
|
BACKEND="$_saved_backend"
|
|
|
|
(umask 077; touch /var/log/gfk-backend.log)
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
# Start Xray if not running
|
|
if command -v xray &>/dev/null || [ -x /usr/local/bin/xray ]; then
|
|
if ! pgrep -f "xray run" &>/dev/null; then
|
|
systemctl start xray 2>/dev/null || xray run -c /usr/local/etc/xray/config.json &>/dev/null &
|
|
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
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# iptables (internal commands)
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
_apply_firewall() {
|
|
[ "$ROLE" != "server" ] && return 0
|
|
if ! command -v iptables &>/dev/null; then
|
|
echo -e "${YELLOW}[!]${NC} iptables not found. Firewall rules cannot be applied." >&2
|
|
return 1
|
|
fi
|
|
|
|
# Tag for identifying our rules
|
|
local TAG="paqctl"
|
|
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
# GFK: NOTRACK + DROP TCP on VIO port so OS doesn't respond, raw socket handles it
|
|
local vio_port="${GFK_VIO_PORT:-45000}"
|
|
modprobe iptable_raw 2>/dev/null || true
|
|
# NOTRACK: bypass conntrack for VIO packets (prevents hypervisor/bridge filtering)
|
|
iptables -t raw -C PREROUTING -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
iptables -t raw -A PREROUTING -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
iptables -t raw -C OUTPUT -p tcp --sport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
iptables -t raw -A OUTPUT -p tcp --sport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
# Drop incoming TCP on VIO port (scapy sniffer will handle it)
|
|
iptables -C INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || \
|
|
iptables -A INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || \
|
|
echo -e "${YELLOW}[!]${NC} Failed to add VIO port DROP rule" >&2
|
|
# Drop outgoing RST packets on VIO port (prevents kernel from interfering with violated TCP)
|
|
iptables -C OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || \
|
|
iptables -A OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || \
|
|
echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule" >&2
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -C INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || \
|
|
ip6tables -A INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || true
|
|
ip6tables -C OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || \
|
|
ip6tables -A OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || true
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
modprobe iptable_raw 2>/dev/null || true
|
|
modprobe iptable_mangle 2>/dev/null || true
|
|
local port="${LISTEN_PORT:-8443}"
|
|
# IPv4 - all rules tagged with "paqctl" comment
|
|
iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
iptables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
echo -e "${YELLOW}[!]${NC} Failed to add PREROUTING NOTRACK rule" >&2
|
|
iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
iptables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
echo -e "${YELLOW}[!]${NC} Failed to add OUTPUT NOTRACK rule" >&2
|
|
iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
|
|
iptables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
|
|
echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule" >&2
|
|
# IPv6
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
ip6tables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \
|
|
ip6tables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \
|
|
ip6tables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
_remove_firewall() {
|
|
[ "$ROLE" != "server" ] && return 0
|
|
command -v iptables &>/dev/null || return 0
|
|
|
|
local TAG="paqctl"
|
|
|
|
# Always respect BACKEND variable - remove only that backend's firewall rules
|
|
# This allows stop_paqet_backend and stop_gfk_backend to remove their own rules independently
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
local vio_port="${GFK_VIO_PORT:-45000}"
|
|
iptables -t raw -D PREROUTING -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
iptables -t raw -D OUTPUT -p tcp --sport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
iptables -t raw -D PREROUTING -p tcp --dport "$vio_port" -j NOTRACK 2>/dev/null || true
|
|
iptables -t raw -D OUTPUT -p tcp --sport "$vio_port" -j NOTRACK 2>/dev/null || true
|
|
iptables -D INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || true
|
|
iptables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || true
|
|
# Also try without comment for backwards compatibility
|
|
iptables -D INPUT -p tcp --dport "$vio_port" -j DROP 2>/dev/null || true
|
|
iptables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -D INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || true
|
|
ip6tables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || true
|
|
ip6tables -D INPUT -p tcp --dport "$vio_port" -j DROP 2>/dev/null || true
|
|
ip6tables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
local port="${LISTEN_PORT:-8443}"
|
|
# Remove tagged rules
|
|
iptables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
iptables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
iptables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
# Also try without comment for backwards compatibility with old rules
|
|
iptables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true
|
|
iptables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true
|
|
iptables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true
|
|
ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
# Remove ALL paqctl-tagged firewall rules (for complete uninstall)
|
|
_remove_all_paqctl_firewall_rules() {
|
|
command -v iptables &>/dev/null || return 0
|
|
local TAG="paqctl"
|
|
|
|
# Remove all rules with "paqctl" comment from all tables
|
|
# Loop to remove multiple rules if port was changed
|
|
local i
|
|
for i in {1..10}; do
|
|
iptables -t raw -S 2>/dev/null | grep -q "paqctl" || break
|
|
iptables -t raw -S 2>/dev/null | grep "paqctl" | while read -r rule; do
|
|
# Convert -A to -D for deletion
|
|
local del_rule="${rule/-A /-D }"
|
|
eval "iptables -t raw $del_rule" 2>/dev/null || true
|
|
done
|
|
done
|
|
|
|
for i in {1..10}; do
|
|
iptables -t mangle -S 2>/dev/null | grep -q "paqctl" || break
|
|
iptables -t mangle -S 2>/dev/null | grep "paqctl" | while read -r rule; do
|
|
local del_rule="${rule/-A /-D }"
|
|
eval "iptables -t mangle $del_rule" 2>/dev/null || true
|
|
done
|
|
done
|
|
|
|
for i in {1..10}; do
|
|
iptables -S 2>/dev/null | grep -q "paqctl" || break
|
|
iptables -S 2>/dev/null | grep "paqctl" | while read -r rule; do
|
|
local del_rule="${rule/-A /-D }"
|
|
eval "iptables $del_rule" 2>/dev/null || true
|
|
done
|
|
done
|
|
|
|
# Same for IPv6
|
|
if command -v ip6tables &>/dev/null; then
|
|
for i in {1..10}; do
|
|
ip6tables -t raw -S 2>/dev/null | grep -q "paqctl" || break
|
|
ip6tables -t raw -S 2>/dev/null | grep "paqctl" | while read -r rule; do
|
|
local del_rule="${rule/-A /-D }"
|
|
eval "ip6tables -t raw $del_rule" 2>/dev/null || true
|
|
done
|
|
done
|
|
|
|
for i in {1..10}; do
|
|
ip6tables -t mangle -S 2>/dev/null | grep -q "paqctl" || break
|
|
ip6tables -t mangle -S 2>/dev/null | grep "paqctl" | while read -r rule; do
|
|
local del_rule="${rule/-A /-D }"
|
|
eval "ip6tables -t mangle $del_rule" 2>/dev/null || true
|
|
done
|
|
done
|
|
|
|
for i in {1..10}; do
|
|
ip6tables -S 2>/dev/null | grep -q "paqctl" || break
|
|
ip6tables -S 2>/dev/null | grep "paqctl" | while read -r rule; do
|
|
local del_rule="${rule/-A /-D }"
|
|
eval "ip6tables $del_rule" 2>/dev/null || true
|
|
done
|
|
done
|
|
fi
|
|
}
|
|
|
|
_persist_firewall() {
|
|
if command -v netfilter-persistent &>/dev/null; then
|
|
netfilter-persistent save 2>/dev/null || true
|
|
elif command -v iptables-save &>/dev/null; then
|
|
if [ -d /etc/iptables ]; then
|
|
iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
|
|
command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true
|
|
elif [ -f /etc/debian_version ] && [ ! -d /etc/iptables ]; then
|
|
mkdir -p /etc/iptables
|
|
iptables-save > /etc/iptables/rules.v4 2>/dev/null || true
|
|
command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true
|
|
elif [ -d /etc/sysconfig ]; then
|
|
iptables-save > /etc/sysconfig/iptables 2>/dev/null || true
|
|
command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/sysconfig/ip6tables 2>/dev/null || true
|
|
fi
|
|
fi
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Status & Info
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
show_status() {
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} PAQCTL STATUS (${BACKEND})${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
# Running status
|
|
if is_running; then
|
|
echo -e " Status: ${GREEN}● Running${NC}"
|
|
# Uptime
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
local started
|
|
started=$(systemctl show paqctl.service --property=ActiveEnterTimestamp 2>/dev/null | cut -d= -f2)
|
|
if [ -n "$started" ]; then
|
|
local started_ts
|
|
started_ts=$(date -d "$started" +%s 2>/dev/null || echo 0)
|
|
if [ "$started_ts" -gt 0 ] 2>/dev/null; then
|
|
local now=$(date +%s)
|
|
local up=$((now - started_ts))
|
|
local days=$((up / 86400))
|
|
local hours=$(( (up % 86400) / 3600 ))
|
|
local mins=$(( (up % 3600) / 60 ))
|
|
if [ "$days" -gt 0 ]; then
|
|
echo -e " Uptime: ${days}d ${hours}h ${mins}m"
|
|
else
|
|
echo -e " Uptime: ${hours}h ${mins}m"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
# PID
|
|
local pid
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
pid=$(pgrep -f "mainserver.py|mainclient.py" 2>/dev/null | head -1)
|
|
else
|
|
pid=$(pgrep -f "paqet run -c" 2>/dev/null | head -1)
|
|
fi
|
|
[ -n "$pid" ] && echo -e " PID: $pid"
|
|
|
|
# CPU/RAM of process
|
|
if [ -n "$pid" ]; then
|
|
local cpu_mem
|
|
cpu_mem=$(ps -p "$pid" -o %cpu=,%mem= 2>/dev/null | head -1)
|
|
if [ -n "$cpu_mem" ]; then
|
|
local cpu=$(echo "$cpu_mem" | awk '{print $1}')
|
|
local mem=$(echo "$cpu_mem" | awk '{print $2}')
|
|
echo -e " CPU: ${cpu}%"
|
|
echo -e " Memory: ${mem}%"
|
|
fi
|
|
fi
|
|
else
|
|
echo -e " Status: ${RED}● Stopped${NC}"
|
|
fi
|
|
|
|
echo ""
|
|
echo -e " ${DIM}── Configuration ──${NC}"
|
|
echo -e " Backend: ${BOLD}${BACKEND}${NC}"
|
|
echo -e " Role: ${BOLD}${ROLE}${NC}"
|
|
echo -e " Version: ${PAQET_VERSION}"
|
|
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
echo -e " Server IP: ${GFK_SERVER_IP}"
|
|
echo -e " VIO port: ${GFK_VIO_PORT}"
|
|
echo -e " QUIC port: ${GFK_QUIC_PORT}"
|
|
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)"
|
|
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
|
|
_fv=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1)
|
|
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. microsocks (client)
|
|
if [ "$ROLE" = "client" ]; then
|
|
if [ -x "$INSTALL_DIR/bin/microsocks" ]; then
|
|
echo -e " ${GREEN}✓${NC} microsocks binary found"
|
|
else
|
|
echo -e " ${RED}✗${NC} microsocks binary missing"
|
|
issues=$((issues + 1))
|
|
fi
|
|
if is_running && ss -tlnp 2>/dev/null | grep -q ":${MICROSOCKS_PORT:-1080}"; then
|
|
echo -e " ${GREEN}✓${NC} SOCKS5 port ${MICROSOCKS_PORT:-1080} is listening"
|
|
elif is_running; then
|
|
echo -e " ${RED}✗${NC} SOCKS5 port ${MICROSOCKS_PORT:-1080} not listening"
|
|
issues=$((issues + 1))
|
|
fi
|
|
fi
|
|
else
|
|
# 1. Binary exists
|
|
if [ -x "$INSTALL_DIR/bin/paqet" ]; then
|
|
echo -e " ${GREEN}✓${NC} paqet binary found"
|
|
else
|
|
echo -e " ${RED}✗${NC} paqet binary not found at $INSTALL_DIR/bin/paqet"
|
|
issues=$((issues + 1))
|
|
fi
|
|
|
|
# 2. Config exists
|
|
if [ -f "$INSTALL_DIR/config.yaml" ]; then
|
|
echo -e " ${GREEN}✓${NC} Configuration file found"
|
|
else
|
|
echo -e " ${RED}✗${NC} Configuration file missing"
|
|
issues=$((issues + 1))
|
|
fi
|
|
|
|
# 3. Service running
|
|
if is_running; then
|
|
echo -e " ${GREEN}✓${NC} paqet is running"
|
|
else
|
|
echo -e " ${RED}✗${NC} paqet is not running"
|
|
issues=$((issues + 1))
|
|
fi
|
|
|
|
# 4. libpcap
|
|
if ldconfig -p 2>/dev/null | grep -q libpcap; then
|
|
echo -e " ${GREEN}✓${NC} libpcap is available"
|
|
else
|
|
echo -e " ${YELLOW}!${NC} libpcap not found in ldconfig (may still work)"
|
|
fi
|
|
|
|
# 5. iptables (server only)
|
|
if [ "$ROLE" = "server" ]; then
|
|
if iptables -t raw -C PREROUTING -p tcp --dport "$LISTEN_PORT" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} iptables NOTRACK rules in place (port $LISTEN_PORT)"
|
|
else
|
|
echo -e " ${RED}✗${NC} iptables NOTRACK rules missing for port $LISTEN_PORT"
|
|
issues=$((issues + 1))
|
|
fi
|
|
|
|
if iptables -t mangle -C OUTPUT -p tcp --sport "$LISTEN_PORT" -m comment --comment "paqctl" --tcp-flags RST RST -j DROP 2>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} iptables RST DROP rule in place"
|
|
else
|
|
echo -e " ${RED}✗${NC} iptables RST DROP rule missing"
|
|
issues=$((issues + 1))
|
|
fi
|
|
fi
|
|
|
|
# 6. Port listening (server) or connectivity (client)
|
|
if [ "$ROLE" = "server" ] && is_running; then
|
|
if ss -tlnp 2>/dev/null | grep -q ":${LISTEN_PORT}"; then
|
|
echo -e " ${GREEN}✓${NC} Port $LISTEN_PORT is listening"
|
|
else
|
|
echo -e " ${YELLOW}!${NC} Port $LISTEN_PORT not shown in ss (paqet uses raw sockets)"
|
|
fi
|
|
fi
|
|
|
|
if [ "$ROLE" = "client" ] && is_running; then
|
|
if ss -tlnp 2>/dev/null | grep -q ":${SOCKS_PORT}"; then
|
|
echo -e " ${GREEN}✓${NC} SOCKS5 port $SOCKS_PORT is listening"
|
|
else
|
|
echo -e " ${RED}✗${NC} SOCKS5 port $SOCKS_PORT is not listening"
|
|
issues=$((issues + 1))
|
|
fi
|
|
fi
|
|
|
|
# 7. Paqet ping test
|
|
if is_running && [ -x "$INSTALL_DIR/bin/paqet" ]; then
|
|
echo -e " ${DIM}Running paqet ping test...${NC}"
|
|
local ping_result
|
|
ping_result=$(timeout 10 "$INSTALL_DIR/bin/paqet" ping -c "$INSTALL_DIR/config.yaml" 2>&1 || true)
|
|
if echo "$ping_result" | grep -qi "success\|pong\|ok\|alive\|rtt"; then
|
|
echo -e " ${GREEN}✓${NC} Paqet ping: OK"
|
|
elif [ -n "$ping_result" ]; then
|
|
echo -e " ${YELLOW}!${NC} Paqet ping: $(echo "$ping_result" | head -1)"
|
|
else
|
|
echo -e " ${YELLOW}!${NC} Paqet ping: no response (may not be supported)"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# 8. Network connectivity
|
|
if curl -s --max-time 5 https://api.github.com &>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} Internet connectivity: OK"
|
|
else
|
|
echo -e " ${YELLOW}!${NC} Cannot reach GitHub API (may be firewall/network)"
|
|
fi
|
|
|
|
# 9. Systemd service
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
if systemctl is-enabled paqctl.service &>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} Auto-start on boot: enabled"
|
|
else
|
|
echo -e " ${YELLOW}!${NC} Auto-start on boot: disabled"
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
if [ "$issues" -eq 0 ]; then
|
|
echo -e " ${GREEN}${BOLD}All checks passed!${NC}"
|
|
else
|
|
echo -e " ${RED}${BOLD}$issues issue(s) found${NC}"
|
|
fi
|
|
echo ""
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Update
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
update_gfk() {
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} UPDATE GFW-KNOCKER${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
log_info "Downloading latest GFW-knocker scripts..."
|
|
local tmp_dir
|
|
tmp_dir=$(mktemp -d)
|
|
local server_files="mainserver.py quic_server.py vio_server.py"
|
|
local client_files="mainclient.py quic_client.py vio_client.py"
|
|
local f changed=false
|
|
# Download server scripts
|
|
for f in $server_files; do
|
|
if ! curl -sL "$GFK_RAW_URL/server/$f" -o "$tmp_dir/$f"; then
|
|
log_error "Failed to download $f"
|
|
rm -rf "$tmp_dir"
|
|
return 1
|
|
fi
|
|
if ! diff -q "$tmp_dir/$f" "$GFK_DIR/$f" &>/dev/null; then
|
|
changed=true
|
|
fi
|
|
done
|
|
# Download client scripts
|
|
for f in $client_files; do
|
|
if ! curl -sL "$GFK_RAW_URL/client/$f" -o "$tmp_dir/$f"; then
|
|
log_error "Failed to download $f"
|
|
rm -rf "$tmp_dir"
|
|
return 1
|
|
fi
|
|
if ! diff -q "$tmp_dir/$f" "$GFK_DIR/$f" &>/dev/null; then
|
|
changed=true
|
|
fi
|
|
done
|
|
|
|
if [ "$changed" = true ]; then
|
|
local was_running=false
|
|
is_running && was_running=true
|
|
[ "$was_running" = true ] && stop_paqet
|
|
|
|
# Backup old scripts
|
|
mkdir -p "$BACKUP_DIR"
|
|
local all_files="$server_files $client_files"
|
|
for f in $all_files; do
|
|
[ -f "$GFK_DIR/$f" ] && cp "$GFK_DIR/$f" "$BACKUP_DIR/${f}.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
|
|
done
|
|
|
|
for f in $all_files; do
|
|
cp "$tmp_dir/$f" "$GFK_DIR/$f"
|
|
done
|
|
chmod 600 "$GFK_DIR"/*.py
|
|
# Patch mainserver.py to use venv python for subprocesses
|
|
[ -f "$GFK_DIR/mainserver.py" ] && sed -i "s|'python3'|'$INSTALL_DIR/venv/bin/python'|g" "$GFK_DIR/mainserver.py"
|
|
log_success "GFW-knocker scripts updated"
|
|
|
|
# Also upgrade Python deps in venv
|
|
"$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade scapy aioquic 2>/dev/null || true
|
|
|
|
[ "$was_running" = true ] && start_paqet
|
|
else
|
|
log_success "GFW-knocker scripts are already up to date"
|
|
"$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade scapy aioquic 2>/dev/null || true
|
|
fi
|
|
rm -rf "$tmp_dir"
|
|
|
|
# Also check for management script updates
|
|
update_management_script
|
|
echo ""
|
|
}
|
|
|
|
update_paqet() {
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
update_gfk
|
|
return
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} UPDATE PAQET${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
log_info "Querying GitHub for latest release..."
|
|
|
|
# Get latest version from GitHub with retry
|
|
local response
|
|
response=$(_curl_with_retry "$PAQET_API_URL" 3)
|
|
if [ -z "$response" ]; then
|
|
log_error "Failed to query GitHub API after retries. Check your internet connection."
|
|
return 1
|
|
fi
|
|
|
|
local latest_tag
|
|
latest_tag=$(echo "$response" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"')
|
|
if [ -z "$latest_tag" ] || ! _validate_version_tag "$latest_tag"; then
|
|
log_error "Could not determine valid version from GitHub"
|
|
return 1
|
|
fi
|
|
|
|
# Extract release date
|
|
local release_date
|
|
release_date=$(echo "$response" | grep -o '"published_at"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"' | cut -dT -f1)
|
|
|
|
# Extract release notes (body field)
|
|
local release_notes=""
|
|
if command -v python3 &>/dev/null; then
|
|
release_notes=$(python3 -c "
|
|
import json,sys
|
|
try:
|
|
d=json.loads(sys.stdin.read())
|
|
body=d.get('body','')
|
|
if body:
|
|
# Truncate to first 500 chars, strip markdown
|
|
body=body[:500].replace('**','').replace('##','').replace('# ','')
|
|
print(body)
|
|
except: pass
|
|
" <<< "$response" 2>/dev/null)
|
|
fi
|
|
|
|
local current="${PAQET_VERSION:-unknown}"
|
|
local bin_ver
|
|
bin_ver=$("$INSTALL_DIR/bin/paqet" version 2>/dev/null || echo "unknown")
|
|
|
|
echo ""
|
|
echo -e " ${DIM}── Version Info ──${NC}"
|
|
echo -e " Installed version: ${BOLD}${current}${NC}"
|
|
echo -e " Binary reports: ${BOLD}${bin_ver}${NC}"
|
|
echo -e " Latest release: ${BOLD}${latest_tag}${NC}"
|
|
[ -n "$release_date" ] && echo -e " Release date: ${release_date}"
|
|
|
|
if [ "$current" = "$latest_tag" ]; then
|
|
echo ""
|
|
log_success "You are already on the latest version!"
|
|
echo ""
|
|
echo -e " ${DIM}Options:${NC}"
|
|
echo " 1. Force reinstall current version"
|
|
echo " 2. Rollback to previous version"
|
|
echo " 3. Update management script only"
|
|
echo " b. Back"
|
|
echo ""
|
|
read -p " Choice: " up_choice < /dev/tty || true
|
|
case "$up_choice" in
|
|
1)
|
|
read -p " Force reinstall ${current}? [y/N]: " _fc < /dev/tty || true
|
|
[[ "$_fc" =~ ^[Yy]$ ]] || { log_info "Cancelled"; return 0; }
|
|
;;
|
|
2) rollback_paqet; return ;;
|
|
3) update_management_script; return ;;
|
|
[bB]) return 0 ;;
|
|
*) return 0 ;;
|
|
esac
|
|
fi
|
|
|
|
# Show release notes if available
|
|
if [ -n "$release_notes" ]; then
|
|
echo ""
|
|
echo -e " ${DIM}── Release Notes ──${NC}"
|
|
echo "$release_notes" | while IFS= read -r line; do
|
|
echo -e " ${DIM}${line}${NC}"
|
|
done
|
|
echo ""
|
|
fi
|
|
|
|
echo ""
|
|
echo -e "${YELLOW}╔════════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${YELLOW}║ ${BOLD}⚠ WARNING: Updating may cause compatibility issues!${NC}${YELLOW} ║${NC}"
|
|
echo -e "${YELLOW}╠════════════════════════════════════════════════════════════════╣${NC}"
|
|
echo -e "${YELLOW}║${NC} paqctl was tested with: ${BOLD}${PAQET_VERSION_PINNED}${NC}"
|
|
echo -e "${YELLOW}║${NC} Newer versions may have breaking changes or bugs."
|
|
echo -e "${YELLOW}║${NC} You can rollback with: ${BOLD}sudo paqctl rollback${NC}"
|
|
echo -e "${YELLOW}╚════════════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
read -p " Update to ${latest_tag}? [y/N]: " confirm < /dev/tty || true
|
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
|
log_info "Update cancelled"
|
|
return 0
|
|
fi
|
|
|
|
# Download new binary
|
|
_download_and_install_binary "$latest_tag" || return 1
|
|
|
|
# Check for management script update
|
|
update_management_script
|
|
}
|
|
|
|
_download_and_install_binary() {
|
|
local target_tag="$1"
|
|
local arch
|
|
arch=$(uname -m)
|
|
case "$arch" in
|
|
x86_64|amd64) arch="amd64" ;;
|
|
aarch64|arm64) arch="arm64" ;;
|
|
*) log_error "Unsupported architecture: $arch"; return 1 ;;
|
|
esac
|
|
|
|
local filename="paqet-linux-${arch}-${target_tag}.tar.gz"
|
|
local url="https://github.com/${PAQET_REPO}/releases/download/${target_tag}/${filename}"
|
|
local tmp_file
|
|
tmp_file=$(mktemp "/tmp/paqet-update-XXXXXXXX.tar.gz")
|
|
|
|
log_info "Downloading ${filename}..."
|
|
if ! curl -sL --max-time 120 --fail -o "$tmp_file" "$url"; then
|
|
log_error "Download failed: $url"
|
|
rm -f "$tmp_file"
|
|
return 1
|
|
fi
|
|
|
|
# Validate
|
|
local fsize
|
|
fsize=$(stat -c%s "$tmp_file" 2>/dev/null || stat -f%z "$tmp_file" 2>/dev/null || wc -c < "$tmp_file" 2>/dev/null || echo 0)
|
|
if [ "$fsize" -lt 1000 ]; then
|
|
log_error "Downloaded file too small ($fsize bytes). Aborting."
|
|
rm -f "$tmp_file"
|
|
return 1
|
|
fi
|
|
|
|
# Extract
|
|
local tmp_extract
|
|
tmp_extract=$(mktemp -d "/tmp/paqet-update-extract-XXXXXXXX")
|
|
if ! tar -xzf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then
|
|
log_error "Failed to extract archive"
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
return 1
|
|
fi
|
|
|
|
local binary_name="paqet_linux_${arch}"
|
|
local found_binary
|
|
found_binary=$(find "$tmp_extract" -name "$binary_name" -type f 2>/dev/null | head -1)
|
|
[ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f -executable 2>/dev/null | head -1)
|
|
[ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f 2>/dev/null | head -1)
|
|
|
|
if [ -z "$found_binary" ]; then
|
|
log_error "Could not find paqet binary in archive"
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
return 1
|
|
fi
|
|
|
|
# Stop service, replace, start
|
|
local was_running=false
|
|
if is_running; then
|
|
was_running=true
|
|
stop_paqet
|
|
fi
|
|
|
|
# Backup old binary with version tag for rollback
|
|
if ! mkdir -p "$BACKUP_DIR"; then
|
|
log_warn "Failed to create backup directory"
|
|
fi
|
|
local old_ver="${PAQET_VERSION:-unknown}"
|
|
cp "$INSTALL_DIR/bin/paqet" "$BACKUP_DIR/paqet.${old_ver}.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true
|
|
|
|
if ! cp "$found_binary" "$INSTALL_DIR/bin/paqet"; then
|
|
log_error "Failed to copy new binary"
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
# Restore from backup
|
|
local latest_backup
|
|
latest_backup=$(ls -t "$BACKUP_DIR"/paqet.* 2>/dev/null | head -1)
|
|
[ -n "$latest_backup" ] && cp "$latest_backup" "$INSTALL_DIR/bin/paqet" && chmod +x "$INSTALL_DIR/bin/paqet"
|
|
[ "$was_running" = true ] && start_paqet
|
|
return 1
|
|
fi
|
|
chmod +x "$INSTALL_DIR/bin/paqet"
|
|
|
|
rm -f "$tmp_file"
|
|
rm -rf "$tmp_extract"
|
|
|
|
# Verify the new binary works
|
|
if ! "$INSTALL_DIR/bin/paqet" version &>/dev/null; then
|
|
log_warn "New binary failed verification. Restoring backup..."
|
|
local latest_backup
|
|
latest_backup=$(ls -t "$BACKUP_DIR"/paqet.* 2>/dev/null | head -1)
|
|
if [ -n "$latest_backup" ]; then
|
|
cp "$latest_backup" "$INSTALL_DIR/bin/paqet"
|
|
chmod +x "$INSTALL_DIR/bin/paqet"
|
|
log_error "Update failed — previous version restored"
|
|
return 1
|
|
fi
|
|
log_error "Update failed and no backup available"
|
|
return 1
|
|
fi
|
|
|
|
# Update version in settings
|
|
PAQET_VERSION="$target_tag"
|
|
_safe_update_setting "PAQET_VERSION" "$target_tag" "$INSTALL_DIR/settings.conf"
|
|
|
|
log_success "paqet updated to ${target_tag}"
|
|
|
|
if [ "$was_running" = true ]; then
|
|
start_paqet
|
|
fi
|
|
}
|
|
|
|
rollback_paqet() {
|
|
echo ""
|
|
if [ ! -d "$BACKUP_DIR" ]; then
|
|
log_warn "No backups found"
|
|
return 1
|
|
fi
|
|
|
|
local backups=()
|
|
local i=1
|
|
echo -e " ${BOLD}Available binary backups:${NC}"
|
|
echo ""
|
|
for f in "$BACKUP_DIR"/paqet.*; do
|
|
[ -f "$f" ] || continue
|
|
backups+=("$f")
|
|
local bname=$(basename "$f")
|
|
local bsize=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null || wc -c < "$f" 2>/dev/null || echo "?")
|
|
echo " $i. $bname (${bsize} bytes)"
|
|
i=$((i + 1))
|
|
done
|
|
|
|
if [ ${#backups[@]} -eq 0 ]; then
|
|
log_warn "No binary backups found in $BACKUP_DIR"
|
|
return 1
|
|
fi
|
|
|
|
echo ""
|
|
echo " 0. Cancel"
|
|
echo ""
|
|
read -p " Select backup to restore [0-${#backups[@]}]: " choice < /dev/tty || true
|
|
if [ "$choice" = "0" ]; then
|
|
log_info "Cancelled"
|
|
return 0
|
|
fi
|
|
if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#backups[@]} ]; then
|
|
log_error "Invalid choice"
|
|
return 1
|
|
fi
|
|
|
|
local selected="${backups[$((choice-1))]}"
|
|
log_info "Rolling back to: $(basename "$selected")"
|
|
|
|
local was_running=false
|
|
is_running && was_running=true
|
|
[ "$was_running" = true ] && stop_paqet
|
|
|
|
if ! cp "$selected" "$INSTALL_DIR/bin/paqet"; then
|
|
log_error "Failed to restore backup"
|
|
[ "$was_running" = true ] && start_paqet
|
|
return 1
|
|
fi
|
|
chmod +x "$INSTALL_DIR/bin/paqet"
|
|
|
|
# Verify restored binary
|
|
if ! "$INSTALL_DIR/bin/paqet" version &>/dev/null; then
|
|
log_warn "Restored binary failed verification (may need libpcap)"
|
|
fi
|
|
|
|
# Try to extract version from the filename (format: paqet.vX.Y.Z.TIMESTAMP)
|
|
local restored_ver=""
|
|
local _bname
|
|
_bname=$(basename "$selected")
|
|
# Extract version: remove 'paqet.' prefix and '.YYYYMMDDHHMMSS' timestamp suffix
|
|
restored_ver=$(echo "$_bname" | sed 's/^paqet\.//' | sed 's/\.[0-9]\{14\}$//')
|
|
# Validate extracted version looks reasonable
|
|
if [ -n "$restored_ver" ] && [ "$restored_ver" != "backup" ] && [ "$restored_ver" != "$_bname" ]; then
|
|
if _validate_version_tag "$restored_ver"; then
|
|
PAQET_VERSION="$restored_ver"
|
|
_safe_update_setting "PAQET_VERSION" "$restored_ver" "$INSTALL_DIR/settings.conf"
|
|
log_info "Restored version: $restored_ver"
|
|
else
|
|
log_warn "Could not determine version from backup filename, keeping current version setting"
|
|
fi
|
|
else
|
|
log_warn "Could not extract version from backup filename"
|
|
fi
|
|
|
|
log_success "Rolled back successfully"
|
|
|
|
[ "$was_running" = true ] && start_paqet
|
|
}
|
|
|
|
update_management_script() {
|
|
local update_url="https://raw.githubusercontent.com/SamNet-dev/paqctl/main/paqctl.sh"
|
|
local tmp_script
|
|
tmp_script=$(mktemp "/tmp/paqctl-update-XXXXXXXX.sh")
|
|
|
|
log_info "Checking for management script updates..."
|
|
if ! curl -sL --max-time 30 --max-filesize 2097152 -o "$tmp_script" "$update_url" 2>/dev/null; then
|
|
log_warn "Could not check for script updates"
|
|
rm -f "$tmp_script"
|
|
return 0
|
|
fi
|
|
|
|
# Validate: must contain our markers, be a bash script, and pass syntax check
|
|
if ! head -n 1 "$tmp_script" 2>/dev/null | grep -q "^#!.*bash"; then
|
|
log_warn "Downloaded file is not a bash script, skipping"
|
|
rm -f "$tmp_script"
|
|
return 0
|
|
fi
|
|
if grep -q "PAQET_REPO=" "$tmp_script" && \
|
|
grep -q "create_management_script" "$tmp_script" && \
|
|
grep -q "PAQCTL_VERSION=" "$tmp_script" && \
|
|
bash -n "$tmp_script" 2>/dev/null; then
|
|
local _update_output
|
|
if _update_output=$(bash "$tmp_script" --update-components 2>&1); then
|
|
log_success "Management script updated"
|
|
else
|
|
log_warn "Management script update execution failed: ${_update_output:-unknown error}"
|
|
fi
|
|
else
|
|
log_warn "Downloaded script failed validation, skipping"
|
|
fi
|
|
rm -f "$tmp_script"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Secret Key Generation
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
generate_secret() {
|
|
echo ""
|
|
local key
|
|
key=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true)
|
|
if [ -z "$key" ]; then
|
|
key=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32)
|
|
fi
|
|
echo -e " ${GREEN}${BOLD}New encryption key: ${key}${NC}"
|
|
echo ""
|
|
echo -e " ${DIM}Share this key securely with client users.${NC}"
|
|
echo ""
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Firewall Display
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
show_firewall() {
|
|
if [ "$ROLE" != "server" ]; then
|
|
echo ""
|
|
log_info "Firewall rules only apply in server mode"
|
|
echo ""
|
|
return
|
|
fi
|
|
|
|
local redraw=true
|
|
while true; do
|
|
if [ "$redraw" = true ]; then
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} FIREWALL RULES${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
local port="${LISTEN_PORT:-8443}"
|
|
|
|
echo -e " ${BOLD}Required rules for port ${port}:${NC}"
|
|
echo ""
|
|
|
|
if iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} PREROUTING NOTRACK (dport $port)"
|
|
else
|
|
echo -e " ${RED}✗${NC} PREROUTING NOTRACK (dport $port) ${DIM}MISSING${NC}"
|
|
fi
|
|
|
|
if iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} OUTPUT NOTRACK (sport $port)"
|
|
else
|
|
echo -e " ${RED}✗${NC} OUTPUT NOTRACK (sport $port) ${DIM}MISSING${NC}"
|
|
fi
|
|
|
|
if iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "paqctl" --tcp-flags RST RST -j DROP 2>/dev/null; then
|
|
echo -e " ${GREEN}✓${NC} RST DROP (sport $port)"
|
|
else
|
|
echo -e " ${RED}✗${NC} RST DROP (sport $port) ${DIM}MISSING${NC}"
|
|
fi
|
|
|
|
echo ""
|
|
echo -e " ${BOLD}Actions:${NC}"
|
|
echo " 1. Apply missing rules"
|
|
echo " 2. Remove all rules"
|
|
echo " b. Back"
|
|
echo ""
|
|
redraw=false
|
|
fi
|
|
|
|
read -p " Choice: " fw_choice < /dev/tty || break
|
|
case "$fw_choice" in
|
|
1)
|
|
_apply_firewall
|
|
_persist_firewall
|
|
log_success "Firewall rules applied and persisted"
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
redraw=true
|
|
;;
|
|
2)
|
|
_remove_firewall
|
|
_persist_firewall
|
|
log_success "Firewall rules removed"
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
redraw=true
|
|
;;
|
|
b|B) return ;;
|
|
"") ;;
|
|
*) echo -e " ${RED}Invalid choice${NC}" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Configuration
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
_change_config_gfk() {
|
|
local was_running="$1"
|
|
echo ""
|
|
echo -e "${BOLD}Select role:${NC}"
|
|
echo " 1. Server"
|
|
echo " 2. Client"
|
|
echo ""
|
|
local role_choice
|
|
read -p " Enter choice [1/2]: " role_choice < /dev/tty || true
|
|
case "$role_choice" in
|
|
1) ROLE="server" ;;
|
|
2) ROLE="client" ;;
|
|
*) log_warn "Invalid. Keeping current role: $ROLE" ;;
|
|
esac
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
echo -e "${BOLD}Server public IP${NC} [${GFK_SERVER_IP}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_ip "$input"; then
|
|
log_error "Invalid IP address"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_SERVER_IP="$input"
|
|
|
|
echo -e "${BOLD}VIO TCP port${NC} [${GFK_VIO_PORT:-45000}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_VIO_PORT="$input"
|
|
|
|
echo -e "${BOLD}QUIC port${NC} [${GFK_QUIC_PORT:-25000}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_QUIC_PORT="$input"
|
|
|
|
echo -e "${BOLD}Auth code${NC} [keep current]:"
|
|
read -p " Code: " input < /dev/tty || true
|
|
[ -n "$input" ] && GFK_AUTH_CODE="$input"
|
|
|
|
echo -e "${BOLD}Port mappings${NC} [${GFK_PORT_MAPPINGS:-14000:443}]:"
|
|
read -p " Mappings: " input < /dev/tty || true
|
|
[ -n "$input" ] && GFK_PORT_MAPPINGS="$input"
|
|
|
|
echo -e "${BOLD}Outgoing TCP flags${NC} [${GFK_TCP_FLAGS:-AP}]:"
|
|
echo -e " ${DIM}Controls TCP flags on outgoing violated packets (default: AP)${NC}"
|
|
echo -e " ${DIM}Valid flags: S(SYN) A(ACK) P(PSH) R(RST) F(FIN) U(URG)${NC}"
|
|
read -p " Flags: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+$ ]]; then
|
|
log_error "Invalid flags. Use uppercase letters only: F, S, R, P, A, U, E, C"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_TCP_FLAGS="$input"
|
|
else
|
|
echo -e "${BOLD}Server IP${NC} [${GFK_SERVER_IP}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_ip "$input"; then
|
|
log_error "Invalid IP address"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_SERVER_IP="$input"
|
|
|
|
echo -e "${BOLD}Server's VIO TCP port${NC} [${GFK_VIO_PORT:-45000}] (must match server):"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_VIO_PORT="$input"
|
|
|
|
echo -e "${BOLD}Local VIO client port${NC} [${GFK_VIO_CLIENT_PORT:-40000}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_VIO_CLIENT_PORT="$input"
|
|
|
|
echo -e "${BOLD}Server's QUIC port${NC} [${GFK_QUIC_PORT:-25000}] (must match server):"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_QUIC_PORT="$input"
|
|
|
|
echo -e "${BOLD}Local QUIC client port${NC} [${GFK_QUIC_CLIENT_PORT:-20000}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_QUIC_CLIENT_PORT="$input"
|
|
|
|
echo -e "${BOLD}Auth code${NC}:"
|
|
read -p " Code: " input < /dev/tty || true
|
|
[ -n "$input" ] && GFK_AUTH_CODE="$input"
|
|
|
|
echo -e "${BOLD}Port mappings${NC} [${GFK_PORT_MAPPINGS:-14000:443}]:"
|
|
read -p " Mappings: " input < /dev/tty || true
|
|
[ -n "$input" ] && GFK_PORT_MAPPINGS="$input"
|
|
|
|
echo -e "${BOLD}Outgoing TCP flags${NC} [${GFK_TCP_FLAGS:-AP}]:"
|
|
echo -e " ${DIM}Controls TCP flags on outgoing violated packets (default: AP)${NC}"
|
|
echo -e " ${DIM}Valid flags: S(SYN) A(ACK) P(PSH) R(RST) F(FIN) U(URG)${NC}"
|
|
read -p " Flags: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+$ ]]; then
|
|
log_error "Invalid flags. Use uppercase letters only: F, S, R, P, A, U, E, C"; return 1
|
|
fi
|
|
[ -n "$input" ] && GFK_TCP_FLAGS="$input"
|
|
|
|
echo -e "${BOLD}SOCKS5 port${NC} [${MICROSOCKS_PORT:-1080}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_port "$input"; then
|
|
log_error "Invalid port number"; return 1
|
|
fi
|
|
[ -n "$input" ] && MICROSOCKS_PORT="$input"
|
|
SOCKS_PORT="${MICROSOCKS_PORT:-1080}"
|
|
fi
|
|
|
|
# Regenerate parameters.py
|
|
generate_gfk_config || { [ "$was_running" = true ] && start_paqet; return 1; }
|
|
|
|
# Regenerate wrapper if client
|
|
if [ "$ROLE" = "client" ]; then
|
|
create_gfk_client_wrapper
|
|
fi
|
|
|
|
# Save settings
|
|
local IFACE="" GW_MAC=""
|
|
save_settings
|
|
|
|
# Re-apply firewall
|
|
_apply_firewall
|
|
|
|
# Restart
|
|
[ "$was_running" = true ] && start_paqet
|
|
log_success "GFW-knocker configuration updated"
|
|
}
|
|
|
|
change_config() {
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} CHANGE CONFIGURATION${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
local _warn_text="config"
|
|
[ "$BACKEND" = "gfw-knocker" ] && _warn_text="parameters.py"
|
|
echo -e " ${YELLOW}Warning: This will regenerate ${_warn_text} and restart ${BACKEND}.${NC}"
|
|
echo ""
|
|
read -p " Continue? [y/N]: " confirm < /dev/tty || true
|
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
|
return 0
|
|
fi
|
|
|
|
local was_running=false
|
|
is_running && was_running=true
|
|
[ "$was_running" = true ] && stop_paqet
|
|
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
_remove_firewall
|
|
_change_config_gfk "$was_running"
|
|
return
|
|
fi
|
|
|
|
# Remove old firewall rules (save old port before user changes it)
|
|
local _saved_port="$LISTEN_PORT"
|
|
if [ "$ROLE" = "server" ] && [ -n "$_saved_port" ]; then
|
|
_remove_firewall
|
|
fi
|
|
|
|
# Re-run wizard (inline version)
|
|
echo ""
|
|
echo -e "${BOLD}Select role:${NC}"
|
|
echo " 1. Server"
|
|
echo " 2. Client"
|
|
echo ""
|
|
local role_choice
|
|
read -p " Enter choice [1/2]: " role_choice < /dev/tty || true
|
|
case "$role_choice" in
|
|
1) ROLE="server" ;;
|
|
2) ROLE="client" ;;
|
|
*)
|
|
log_warn "Invalid choice. Defaulting to server."
|
|
ROLE="server"
|
|
;;
|
|
esac
|
|
|
|
# Detect network
|
|
local _iface=$(ip route show default 2>/dev/null | awk '{print $5; exit}')
|
|
# Note: grep returns exit 1 if no matches, so we add || true for pipefail
|
|
local _ip=$(ip -4 addr show "$_iface" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | { grep -o '[0-9.]*' || true; } | head -1)
|
|
local _gw=$(ip route show default 2>/dev/null | awk '{print $3; exit}')
|
|
local _gw_mac=""
|
|
[ -n "$_gw" ] && _gw_mac=$(ip neigh show "$_gw" 2>/dev/null | awk '/lladdr/{print $5; exit}')
|
|
|
|
echo ""
|
|
echo -e "${BOLD}Interface${NC} [${_iface:-$INTERFACE}]:"
|
|
read -p " Interface: " input < /dev/tty || true
|
|
INTERFACE="${input:-${_iface:-$INTERFACE}}"
|
|
|
|
echo -e "${BOLD}Local IP${NC} [${_ip:-$LOCAL_IP}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
LOCAL_IP="${input:-${_ip:-$LOCAL_IP}}"
|
|
|
|
echo -e "${BOLD}Gateway MAC${NC} [${_gw_mac:-$GATEWAY_MAC}]:"
|
|
read -p " MAC: " input < /dev/tty || true
|
|
GATEWAY_MAC="${input:-${_gw_mac:-$GATEWAY_MAC}}"
|
|
if [ -n "$GATEWAY_MAC" ] && ! _validate_mac "$GATEWAY_MAC"; then
|
|
log_warn "Invalid MAC address format (expected: aa:bb:cc:dd:ee:ff)"
|
|
read -p " Enter valid MAC address: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! _validate_mac "$input"; then
|
|
log_warn "Invalid MAC format, keeping current value"
|
|
input=""
|
|
fi
|
|
[ -n "$input" ] && GATEWAY_MAC="$input"
|
|
fi
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
echo -e "${BOLD}Port${NC} [${LISTEN_PORT:-8443}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
LISTEN_PORT="${input:-${LISTEN_PORT:-8443}}"
|
|
if ! _validate_port "$LISTEN_PORT"; then
|
|
log_warn "Invalid port. Using default 8443."
|
|
LISTEN_PORT=8443
|
|
fi
|
|
|
|
echo -e "${BOLD}Encryption key${NC} [keep current]:"
|
|
read -p " Key (enter to keep): " input < /dev/tty || true
|
|
[ -n "$input" ] && ENCRYPTION_KEY="$input"
|
|
REMOTE_SERVER=""
|
|
SOCKS_PORT=""
|
|
else
|
|
echo -e "${BOLD}Remote server${NC} (IP:PORT):"
|
|
read -p " Server: " input < /dev/tty || true
|
|
REMOTE_SERVER="${input:-$REMOTE_SERVER}"
|
|
|
|
echo -e "${BOLD}Encryption key${NC}:"
|
|
read -p " Key: " input < /dev/tty || true
|
|
[ -n "$input" ] && ENCRYPTION_KEY="$input"
|
|
|
|
echo -e "${BOLD}SOCKS5 port${NC} [${SOCKS_PORT:-1080}]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
SOCKS_PORT="${input:-${SOCKS_PORT:-1080}}"
|
|
LISTEN_PORT=""
|
|
fi
|
|
|
|
# TCP flags (for both server and client)
|
|
echo -e "${BOLD}TCP local flag${NC} [${PAQET_TCP_LOCAL_FLAG:-PA}]:"
|
|
echo -e " ${DIM}Controls TCP flags on outgoing packets (default: PA = PSH+ACK)${NC}"
|
|
echo -e " ${DIM}Valid flags: S(SYN) A(ACK) P(PSH) R(RST) F(FIN) U(URG) E(ECE) C(CWR)${NC}"
|
|
echo -e " ${DIM}Multiple values: PA,A (tries PA first, then A)${NC}"
|
|
read -p " Flag: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]]; then
|
|
log_warn "Invalid flags. Use: FSRPAUEC (e.g., PA or PA,A). Keeping current value."
|
|
input=""
|
|
fi
|
|
[ -n "$input" ] && PAQET_TCP_LOCAL_FLAG="$input"
|
|
|
|
echo -e "${BOLD}TCP remote flag${NC} [${PAQET_TCP_REMOTE_FLAG:-PA}]:"
|
|
echo -e " ${DIM}Controls expected TCP flags on incoming packets (default: PA)${NC}"
|
|
echo -e " ${DIM}Should match the server/client counterpart's local flag${NC}"
|
|
read -p " Flag: " input < /dev/tty || true
|
|
if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]]; then
|
|
log_warn "Invalid flags. Use: FSRPAUEC (e.g., PA or PA,A). Keeping current value."
|
|
input=""
|
|
fi
|
|
[ -n "$input" ] && PAQET_TCP_REMOTE_FLAG="$input"
|
|
|
|
# Save
|
|
local IFACE="$INTERFACE"
|
|
local GW_MAC="$GATEWAY_MAC"
|
|
# Regenerate YAML
|
|
local tmp_conf
|
|
tmp_conf=$(mktemp "$INSTALL_DIR/config.yaml.XXXXXXXX")
|
|
# Validate required fields
|
|
if [ -z "$INTERFACE" ] || [ -z "$LOCAL_IP" ] || [ -z "$GATEWAY_MAC" ] || [ -z "$ENCRYPTION_KEY" ]; then
|
|
log_error "Missing required configuration fields"
|
|
rm -f "$tmp_conf"
|
|
[ "$was_running" = true ] && start_paqet
|
|
return 1
|
|
fi
|
|
|
|
# Escape YAML special characters to prevent injection
|
|
_escape_yaml() {
|
|
local s="$1"
|
|
if [[ "$s" =~ [:\#\[\]\{\}\"\'\|\>\<\&\*\!\%\@\`] ]] || [[ "$s" =~ ^[[:space:]] ]] || [[ "$s" =~ [[:space:]]$ ]]; then
|
|
s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; printf '"%s"' "$s"
|
|
else
|
|
printf '%s' "$s"
|
|
fi
|
|
}
|
|
# Set permissions before writing
|
|
chmod 600 "$tmp_conf" 2>/dev/null
|
|
(
|
|
umask 077
|
|
local _y_iface _y_ip _y_mac _y_key _y_server _tcp_local_flags _tcp_remote_flags
|
|
_y_iface=$(_escape_yaml "$INTERFACE")
|
|
_y_ip=$(_escape_yaml "$LOCAL_IP")
|
|
_y_mac=$(_escape_yaml "$GATEWAY_MAC")
|
|
_y_key=$(_escape_yaml "$ENCRYPTION_KEY")
|
|
_tcp_local_flags=$(echo "${PAQET_TCP_LOCAL_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/')
|
|
_tcp_remote_flags=$(echo "${PAQET_TCP_REMOTE_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/')
|
|
if [ "$ROLE" = "server" ]; then
|
|
cat > "$tmp_conf" << EOF
|
|
role: "server"
|
|
|
|
log:
|
|
level: "info"
|
|
|
|
listen:
|
|
addr: ":${LISTEN_PORT}"
|
|
|
|
network:
|
|
interface: "${_y_iface}"
|
|
ipv4:
|
|
addr: "${_y_ip}:${LISTEN_PORT}"
|
|
router_mac: "${_y_mac}"
|
|
tcp:
|
|
local_flag: ${_tcp_local_flags}
|
|
remote_flag: ${_tcp_remote_flags}
|
|
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_y_key}"
|
|
EOF
|
|
else
|
|
_y_server=$(_escape_yaml "$REMOTE_SERVER")
|
|
cat > "$tmp_conf" << EOF
|
|
role: "client"
|
|
|
|
log:
|
|
level: "info"
|
|
|
|
socks5:
|
|
- listen: "127.0.0.1:${SOCKS_PORT}"
|
|
|
|
network:
|
|
interface: "${_y_iface}"
|
|
ipv4:
|
|
addr: "${_y_ip}:0"
|
|
router_mac: "${_y_mac}"
|
|
tcp:
|
|
local_flag: ${_tcp_local_flags}
|
|
remote_flag: ${_tcp_remote_flags}
|
|
|
|
server:
|
|
addr: "${_y_server}"
|
|
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_y_key}"
|
|
EOF
|
|
fi
|
|
)
|
|
if ! mv "$tmp_conf" "$INSTALL_DIR/config.yaml"; then
|
|
log_error "Failed to save configuration"
|
|
rm -f "$tmp_conf"
|
|
[ "$was_running" = true ] && start_paqet
|
|
return 1
|
|
fi
|
|
chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null
|
|
|
|
# Save settings
|
|
local _tmp
|
|
_tmp=$(mktemp "$INSTALL_DIR/settings.conf.XXXXXXXX")
|
|
# Read current telegram settings
|
|
local _tg_token="${TELEGRAM_BOT_TOKEN:-}"
|
|
local _tg_chat="${TELEGRAM_CHAT_ID:-}"
|
|
local _tg_interval="${TELEGRAM_INTERVAL:-6}"
|
|
local _tg_enabled="${TELEGRAM_ENABLED:-false}"
|
|
local _tg_alerts="${TELEGRAM_ALERTS_ENABLED:-true}"
|
|
local _tg_daily="${TELEGRAM_DAILY_SUMMARY:-true}"
|
|
local _tg_weekly="${TELEGRAM_WEEKLY_SUMMARY:-true}"
|
|
local _tg_label="${TELEGRAM_SERVER_LABEL:-}"
|
|
local _tg_start_hour="${TELEGRAM_START_HOUR:-0}"
|
|
(
|
|
umask 077
|
|
cat > "$_tmp" << EOF
|
|
BACKEND="${BACKEND:-paqet}"
|
|
ROLE="${ROLE}"
|
|
PAQET_VERSION="${PAQET_VERSION}"
|
|
PAQCTL_VERSION="${VERSION}"
|
|
LISTEN_PORT="${LISTEN_PORT:-}"
|
|
SOCKS_PORT="${SOCKS_PORT:-}"
|
|
INTERFACE="${INTERFACE}"
|
|
LOCAL_IP="${LOCAL_IP}"
|
|
GATEWAY_MAC="${GATEWAY_MAC}"
|
|
ENCRYPTION_KEY="${ENCRYPTION_KEY}"
|
|
REMOTE_SERVER="${REMOTE_SERVER:-}"
|
|
GFK_VIO_PORT="${GFK_VIO_PORT:-}"
|
|
GFK_QUIC_PORT="${GFK_QUIC_PORT:-}"
|
|
GFK_AUTH_CODE="${GFK_AUTH_CODE:-}"
|
|
GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS:-}"
|
|
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
|
|
# Install GFK without changing current backend
|
|
if ! _install_gfk_components; then
|
|
log_error "Failed to install ${new_backend}"
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
return 1
|
|
fi
|
|
else
|
|
# Install paqet without changing current backend
|
|
if ! _install_paqet_components; then
|
|
log_error "Failed to install ${new_backend}"
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
log_success "${new_backend} installed successfully!"
|
|
echo ""
|
|
echo " Use 'Switch backend' to activate it."
|
|
echo ""
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
}
|
|
|
|
_install_paqet_components() {
|
|
log_info "Downloading paqet binary..."
|
|
if ! download_paqet "$PAQET_VERSION_PINNED"; then
|
|
log_error "Failed to download paqet"
|
|
return 1
|
|
fi
|
|
log_success "paqet binary installed"
|
|
|
|
# Generate config.yaml if it doesn't exist
|
|
if [ ! -f "$INSTALL_DIR/config.yaml" ]; then
|
|
echo ""
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} PAQET CONFIGURATION${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
# Detect network settings
|
|
detect_network
|
|
local _det_iface="$DETECTED_IFACE"
|
|
local _det_ip="$DETECTED_IP"
|
|
local _det_mac="$DETECTED_GW_MAC"
|
|
|
|
# Prompt for interface
|
|
echo -e "${BOLD}Network Interface${NC} [${_det_iface:-eth0}]:"
|
|
read -p " Interface: " input < /dev/tty || true
|
|
local _iface="${input:-${_det_iface:-eth0}}"
|
|
|
|
# Prompt for local IP
|
|
echo -e "${BOLD}Local IP${NC} [${_det_ip:-}]:"
|
|
read -p " IP: " input < /dev/tty || true
|
|
local _local_ip="${input:-$_det_ip}"
|
|
|
|
# Prompt for gateway MAC
|
|
echo -e "${BOLD}Gateway MAC${NC} [${_det_mac:-}]:"
|
|
read -p " MAC: " input < /dev/tty || true
|
|
local _gw_mac="${input:-$_det_mac}"
|
|
|
|
# Validate MAC if provided
|
|
if [ -n "$_gw_mac" ] && ! [[ "$_gw_mac" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; then
|
|
log_warn "Invalid MAC format. Expected: aa:bb:cc:dd:ee:ff"
|
|
read -p " Enter valid MAC: " input < /dev/tty || true
|
|
[ -n "$input" ] && _gw_mac="$input"
|
|
fi
|
|
|
|
# Generate encryption key
|
|
local _key
|
|
_key=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true)
|
|
if [ -z "$_key" ]; then
|
|
_key=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32 || true)
|
|
fi
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
# Prompt for port
|
|
echo -e "${BOLD}Listen Port${NC} [8443]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
local _port="${input:-8443}"
|
|
|
|
# Show generated key
|
|
echo ""
|
|
echo -e "${GREEN}${BOLD} Generated Encryption Key: ${_key}${NC}"
|
|
echo -e "${YELLOW} IMPORTANT: Save this key! Clients need it to connect.${NC}"
|
|
echo ""
|
|
echo -e "${BOLD}Encryption Key${NC} (press Enter to use generated key):"
|
|
read -p " Key: " input < /dev/tty || true
|
|
[ -n "$input" ] && _key="$input"
|
|
|
|
LISTEN_PORT="$_port"
|
|
ENCRYPTION_KEY="$_key"
|
|
else
|
|
# Client prompts
|
|
echo -e "${BOLD}Remote Server${NC} (IP:PORT):"
|
|
read -p " Server: " input < /dev/tty || true
|
|
local _server="${input:-${REMOTE_SERVER:-}}"
|
|
if [ -z "$_server" ]; then
|
|
log_warn "No server specified. You must edit config.yaml later."
|
|
_server="SERVER_IP:8443"
|
|
fi
|
|
|
|
echo -e "${BOLD}Encryption Key${NC} (from server):"
|
|
read -p " Key: " input < /dev/tty || true
|
|
[ -n "$input" ] && _key="$input"
|
|
|
|
echo -e "${BOLD}SOCKS5 Port${NC} [1080]:"
|
|
read -p " Port: " input < /dev/tty || true
|
|
local _socks="${input:-1080}"
|
|
|
|
REMOTE_SERVER="$_server"
|
|
SOCKS_PORT="$_socks"
|
|
ENCRYPTION_KEY="$_key"
|
|
fi
|
|
|
|
# Validate required fields
|
|
if [ -z "$_iface" ] || [ -z "$_local_ip" ] || [ -z "$_gw_mac" ]; then
|
|
log_error "Missing required fields (interface, IP, or MAC)"
|
|
return 1
|
|
fi
|
|
if [ -z "$_key" ] || [ "${#_key}" -lt 16 ]; then
|
|
log_error "Invalid encryption key"
|
|
return 1
|
|
fi
|
|
|
|
# Helper to escape YAML values
|
|
_escape_yaml_val() {
|
|
local s="$1"
|
|
if [[ "$s" =~ [:\#\[\]\{\}\"\'\|\>\<\&\*\!\%\@\`] ]] || [[ "$s" =~ ^[[:space:]] ]] || [[ "$s" =~ [[:space:]]$ ]]; then
|
|
s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; printf '"%s"' "$s"
|
|
else
|
|
printf '%s' "$s"
|
|
fi
|
|
}
|
|
|
|
local _y_iface _y_ip _y_mac _y_key
|
|
_y_iface=$(_escape_yaml_val "$_iface")
|
|
_y_ip=$(_escape_yaml_val "$_local_ip")
|
|
_y_mac=$(_escape_yaml_val "$_gw_mac")
|
|
_y_key=$(_escape_yaml_val "$_key")
|
|
|
|
local tmp_conf
|
|
tmp_conf=$(mktemp "$INSTALL_DIR/config.yaml.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; }
|
|
chmod 600 "$tmp_conf" 2>/dev/null
|
|
|
|
if [ "$ROLE" = "server" ]; then
|
|
cat > "$tmp_conf" << EOF
|
|
role: "server"
|
|
|
|
log:
|
|
level: "info"
|
|
|
|
listen:
|
|
addr: ":${_port}"
|
|
|
|
network:
|
|
interface: "${_y_iface}"
|
|
ipv4:
|
|
addr: "${_y_ip}:${_port}"
|
|
router_mac: "${_y_mac}"
|
|
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_y_key}"
|
|
EOF
|
|
else
|
|
cat > "$tmp_conf" << EOF
|
|
role: "client"
|
|
|
|
log:
|
|
level: "info"
|
|
|
|
socks5:
|
|
- listen: "127.0.0.1:${_socks}"
|
|
|
|
network:
|
|
interface: "${_y_iface}"
|
|
ipv4:
|
|
addr: "${_y_ip}:0"
|
|
router_mac: "${_y_mac}"
|
|
|
|
server:
|
|
addr: "${_server}"
|
|
|
|
transport:
|
|
protocol: "kcp"
|
|
kcp:
|
|
mode: "fast"
|
|
key: "${_y_key}"
|
|
EOF
|
|
fi
|
|
|
|
if ! mv "$tmp_conf" "$INSTALL_DIR/config.yaml"; then
|
|
log_error "Failed to save config.yaml"
|
|
rm -f "$tmp_conf"
|
|
return 1
|
|
fi
|
|
chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null
|
|
|
|
# Update global vars for settings
|
|
INTERFACE="$_iface"
|
|
LOCAL_IP="$_local_ip"
|
|
GATEWAY_MAC="$_gw_mac"
|
|
|
|
log_success "Configuration saved to $INSTALL_DIR/config.yaml"
|
|
|
|
# Save to settings.conf for persistence
|
|
save_settings 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
check_xray_installed() {
|
|
command -v xray &>/dev/null || [ -x /usr/local/bin/xray ]
|
|
}
|
|
|
|
XRAY_CONFIG_DIR="/usr/local/etc/xray"
|
|
XRAY_CONFIG_FILE="$XRAY_CONFIG_DIR/config.json"
|
|
|
|
install_xray() {
|
|
if check_xray_installed; then
|
|
log_info "Xray is already installed"
|
|
return 0
|
|
fi
|
|
|
|
log_info "Installing Xray ${XRAY_VERSION_PINNED}..."
|
|
|
|
local tmp_script
|
|
tmp_script=$(mktemp)
|
|
if ! curl -sL https://github.com/XTLS/Xray-install/raw/main/install-release.sh -o "$tmp_script"; then
|
|
log_error "Failed to download Xray installer"
|
|
rm -f "$tmp_script"
|
|
return 1
|
|
fi
|
|
|
|
if ! bash "$tmp_script" install --version "$XRAY_VERSION_PINNED" 2>/dev/null; then
|
|
log_error "Failed to install Xray"
|
|
rm -f "$tmp_script"
|
|
return 1
|
|
fi
|
|
rm -f "$tmp_script"
|
|
|
|
log_success "Xray ${XRAY_VERSION_PINNED} installed"
|
|
}
|
|
|
|
configure_xray_socks() {
|
|
local listen_port="${1:-443}"
|
|
log_info "Configuring Xray SOCKS5 proxy on port $listen_port..."
|
|
mkdir -p "$XRAY_CONFIG_DIR"
|
|
cat > "$XRAY_CONFIG_FILE" << EOF
|
|
{
|
|
"log": { "loglevel": "warning" },
|
|
"inbounds": [{
|
|
"tag": "socks-in",
|
|
"port": ${listen_port},
|
|
"listen": "127.0.0.1",
|
|
"protocol": "socks",
|
|
"settings": { "auth": "noauth", "udp": true },
|
|
"sniffing": { "enabled": true, "destOverride": ["http", "tls"] }
|
|
}],
|
|
"outbounds": [{ "tag": "direct", "protocol": "freedom", "settings": {} }]
|
|
}
|
|
EOF
|
|
chmod 644 "$XRAY_CONFIG_FILE"
|
|
log_success "Xray configured (SOCKS5 on 127.0.0.1:$listen_port)"
|
|
}
|
|
|
|
_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
|
|
if [ -x /usr/local/bin/xray ]; then
|
|
pkill -x xray 2>/dev/null || true
|
|
sleep 1
|
|
nohup /usr/local/bin/xray run -c "$XRAY_CONFIG_FILE" > /var/log/xray.log 2>&1 &
|
|
sleep 2
|
|
if pgrep -x xray &>/dev/null; then
|
|
log_success "Xray started"
|
|
return 0
|
|
fi
|
|
fi
|
|
log_error "Failed to start Xray"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
setup_xray_for_gfk() {
|
|
local target_port
|
|
target_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f2 | cut -d, -f1)
|
|
|
|
if pgrep -x xray &>/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
|
|
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"
|
|
echo " - microsocks binary"
|
|
else
|
|
echo " - paqet binary"
|
|
fi
|
|
echo " - All configuration files"
|
|
echo " - Systemd services"
|
|
echo " - Firewall rules"
|
|
echo " - Telegram service"
|
|
echo ""
|
|
read -p " Are you sure? [y/N]: " confirm < /dev/tty || true
|
|
if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
|
|
log_info "Cancelled"
|
|
return 0
|
|
fi
|
|
|
|
# Stop services
|
|
stop_paqet
|
|
stop_telegram_service
|
|
|
|
# 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} MIT"
|
|
echo -e " ${BOLD}Language:${NC} Go"
|
|
echo -e " ${BOLD}Contact:${NC} Signal @hanselime.11"
|
|
echo ""
|
|
echo " Paqet is a bidirectional packet-level proxy that uses"
|
|
echo " KCP over raw TCP packets with custom TCP flags."
|
|
echo " It operates below the OS TCP/IP stack to bypass"
|
|
echo " firewalls and deep packet inspection."
|
|
echo ""
|
|
echo " Features:"
|
|
echo " - Raw TCP packet crafting via gopacket"
|
|
echo " - KCP + AES symmetric encryption"
|
|
echo " - SOCKS5 proxy for dynamic connections"
|
|
echo " - Connection multiplexing via smux"
|
|
echo " - Cross-platform (Linux, macOS, Windows)"
|
|
echo " - Android client: github.com/AliRezaBeigy/paqetNG"
|
|
echo ""
|
|
echo -e " ${CYAN}── paqctl Management Tool ──${NC}"
|
|
echo ""
|
|
echo -e " ${BOLD}Built by:${NC} SamNet-dev"
|
|
echo -e " ${BOLD}Repository:${NC} https://github.com/SamNet-dev/paqctl"
|
|
echo -e " ${BOLD}License:${NC} MIT"
|
|
echo ""
|
|
echo " paqctl provides one-click installation, configuration,"
|
|
echo " service management, auto-updates, health monitoring,"
|
|
echo " and Telegram notifications for paqet."
|
|
echo ""
|
|
echo -e " ${DIM}Original paqet by hanselime, improved by SamNet.${NC}"
|
|
echo ""
|
|
read -n 1 -s -r -p " Press any key..." < /dev/tty || true
|
|
redraw=true
|
|
;;
|
|
b|B) return ;;
|
|
"") ;;
|
|
*) echo -e " ${RED}Invalid choice${NC}" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Connection Info Display
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
show_connection_info() {
|
|
clear
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${BOLD} CLIENT CONNECTION INFO${NC}"
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
|
|
_load_settings
|
|
|
|
local local_ip
|
|
local_ip=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}' || hostname -I 2>/dev/null | awk '{print $1}' || echo "unknown")
|
|
|
|
local paqet_installed=false
|
|
local gfk_installed=false
|
|
[ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
|
|
if [ "$ROLE" = "server" ]; then
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
|
|
else
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
|
|
fi
|
|
|
|
if [ "$paqet_installed" = true ]; then
|
|
echo -e " ${GREEN}${BOLD}━━━ PAQET ━━━${NC}"
|
|
echo ""
|
|
local paqet_port="${LISTEN_PORT:-8443}"
|
|
local paqet_key="${ENCRYPTION_KEY:-not set}"
|
|
# Try to get key from config if not in settings
|
|
if [ "$paqet_key" = "not set" ] && [ -f "$INSTALL_DIR/config.yaml" ]; then
|
|
paqet_key=$(grep -E "^key:" "$INSTALL_DIR/config.yaml" 2>/dev/null | awk '{print $2}' | tr -d '"' || echo "not set")
|
|
fi
|
|
echo -e " ${YELLOW}╔═══════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e " ${YELLOW}║${NC} Server: ${BOLD}${local_ip}:${paqet_port}${NC}"
|
|
echo -e " ${YELLOW}║${NC} Key: ${BOLD}${paqet_key}${NC}"
|
|
echo -e " ${YELLOW}╚═══════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
echo -e " ${DIM}Client proxy: 127.0.0.1:1080 (SOCKS5)${NC}"
|
|
echo ""
|
|
fi
|
|
|
|
if [ "$gfk_installed" = true ]; then
|
|
echo -e " ${MAGENTA}${BOLD}━━━ GFW-KNOCKER ━━━${NC}"
|
|
echo ""
|
|
local gfk_ip="${GFK_SERVER_IP:-$local_ip}"
|
|
local gfk_auth="${GFK_AUTH_CODE:-not set}"
|
|
local gfk_mappings="${GFK_PORT_MAPPINGS:-14000:443}"
|
|
echo -e " ${YELLOW}╔═══════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e " ${YELLOW}║${NC} Server IP: ${BOLD}${gfk_ip}${NC}"
|
|
echo -e " ${YELLOW}║${NC} Auth Code: ${BOLD}${gfk_auth}${NC}"
|
|
echo -e " ${YELLOW}║${NC} Mappings: ${BOLD}${gfk_mappings}${NC}"
|
|
echo -e " ${YELLOW}╚═══════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
echo -e " ${DIM}VIO port: ${GFK_VIO_PORT:-45000} | QUIC port: ${GFK_QUIC_PORT:-25000}${NC}"
|
|
echo -e " ${DIM}Client proxy: 127.0.0.1:14000 (SOCKS5)${NC}"
|
|
echo ""
|
|
fi
|
|
|
|
if [ "$paqet_installed" = false ] && [ "$gfk_installed" = false ]; then
|
|
echo -e " ${YELLOW}No backends installed yet.${NC}"
|
|
echo ""
|
|
echo " Run 'sudo paqctl menu' and select 'Settings & Tools'"
|
|
echo " to install a backend."
|
|
echo ""
|
|
fi
|
|
|
|
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Interactive Menu
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
show_menu() {
|
|
# Auto-fix systemd service if in failed state
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
local svc_state=$(systemctl is-active paqctl.service 2>/dev/null)
|
|
if [ "$svc_state" = "failed" ]; then
|
|
systemctl reset-failed paqctl.service 2>/dev/null || true
|
|
fi
|
|
fi
|
|
|
|
# Reload settings
|
|
_load_settings
|
|
|
|
local paqet_installed=false
|
|
local gfk_installed=false
|
|
local redraw=true
|
|
|
|
while true; do
|
|
if [ "$redraw" = true ]; then
|
|
# Re-check what's installed each redraw
|
|
paqet_installed=false
|
|
gfk_installed=false
|
|
[ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true
|
|
if [ "$ROLE" = "server" ]; then
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true
|
|
else
|
|
[ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true
|
|
fi
|
|
|
|
clear
|
|
print_header
|
|
|
|
# Status line showing both backends
|
|
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
|
|
echo -e " ${BOLD}BACKEND STATUS${NC} (Role: ${ROLE})"
|
|
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
|
|
|
|
# Paqet status
|
|
if [ "$paqet_installed" = true ]; then
|
|
if is_paqet_running; then
|
|
echo -e " Paqet: ${GREEN}● Running${NC} | Port: ${LISTEN_PORT:-8443} | SOCKS5: 127.0.0.1:${SOCKS_PORT:-1080}"
|
|
else
|
|
echo -e " Paqet: ${RED}○ Stopped${NC} | Port: ${LISTEN_PORT:-8443}"
|
|
fi
|
|
else
|
|
echo -e " Paqet: ${DIM}not installed${NC}"
|
|
fi
|
|
|
|
# GFK status
|
|
if [ "$gfk_installed" = true ]; then
|
|
if is_gfk_running; then
|
|
echo -e " GFK: ${GREEN}● Running${NC} | VIO: ${GFK_VIO_PORT:-45000} | SOCKS5: 127.0.0.1:14000"
|
|
else
|
|
echo -e " GFK: ${RED}○ Stopped${NC} | VIO: ${GFK_VIO_PORT:-45000}"
|
|
fi
|
|
else
|
|
echo -e " GFK: ${DIM}not installed${NC}"
|
|
fi
|
|
|
|
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
|
|
|
|
echo ""
|
|
echo -e " ${CYAN}MAIN MENU${NC}"
|
|
echo ""
|
|
echo " 1. View status"
|
|
echo " 2. View logs"
|
|
echo " 3. Health check"
|
|
echo ""
|
|
|
|
# Paqet controls
|
|
if [ "$paqet_installed" = true ]; then
|
|
if is_paqet_running; then
|
|
echo -e " p. ${RED}Stop${NC} Paqet"
|
|
else
|
|
echo -e " p. ${GREEN}Start${NC} Paqet"
|
|
fi
|
|
fi
|
|
|
|
# GFK controls
|
|
if [ "$gfk_installed" = true ]; then
|
|
if is_gfk_running; then
|
|
echo -e " g. ${RED}Stop${NC} GFK"
|
|
else
|
|
echo -e " g. ${GREEN}Start${NC} GFK"
|
|
fi
|
|
fi
|
|
|
|
# Start/Stop all
|
|
if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then
|
|
echo ""
|
|
if is_paqet_running && is_gfk_running; then
|
|
echo -e " a. ${RED}Stop ALL${NC} backends"
|
|
elif ! is_paqet_running && ! is_gfk_running; then
|
|
echo -e " a. ${GREEN}Start ALL${NC} backends"
|
|
else
|
|
echo " a. Toggle ALL backends"
|
|
fi
|
|
fi
|
|
|
|
echo ""
|
|
echo " 8. Settings & Tools"
|
|
echo -e " ${YELLOW}c. Connection Info${NC}"
|
|
echo " i. Info & Help"
|
|
echo -e " ${RED}u. Uninstall${NC}"
|
|
echo " 0. Exit"
|
|
echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}"
|
|
echo ""
|
|
redraw=false
|
|
fi
|
|
|
|
echo -n " Select option: "
|
|
if ! read choice < /dev/tty 2>/dev/null; then
|
|
log_error "Cannot read input. If piped, run: sudo paqctl menu"
|
|
exit 1
|
|
fi
|
|
|
|
case "$choice" in
|
|
1) show_status; read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true; redraw=true ;;
|
|
2) show_logs; redraw=true ;;
|
|
3) health_check; read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true; redraw=true ;;
|
|
p|P)
|
|
if [ "$paqet_installed" = true ]; then
|
|
if is_paqet_running; then
|
|
stop_paqet_backend
|
|
else
|
|
start_paqet_backend
|
|
fi
|
|
read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true
|
|
else
|
|
echo -e " ${YELLOW}Paqet not installed${NC}"
|
|
fi
|
|
redraw=true
|
|
;;
|
|
g|G)
|
|
if [ "$gfk_installed" = true ]; then
|
|
if is_gfk_running; then
|
|
stop_gfk_backend
|
|
else
|
|
start_gfk_backend
|
|
fi
|
|
read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true
|
|
else
|
|
echo -e " ${YELLOW}GFK not installed${NC}"
|
|
fi
|
|
redraw=true
|
|
;;
|
|
a|A)
|
|
if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then
|
|
if is_paqet_running && is_gfk_running; then
|
|
# Stop all
|
|
stop_paqet_backend
|
|
stop_gfk_backend
|
|
elif ! is_paqet_running && ! is_gfk_running; then
|
|
# Start all
|
|
start_paqet_backend
|
|
start_gfk_backend
|
|
else
|
|
# Mixed state - ask user
|
|
echo ""
|
|
echo " 1. Start all backends"
|
|
echo " 2. Stop all backends"
|
|
echo -n " Choice: "
|
|
read subchoice < /dev/tty || true
|
|
case "$subchoice" in
|
|
1)
|
|
[ "$paqet_installed" = true ] && ! is_paqet_running && start_paqet_backend
|
|
[ "$gfk_installed" = true ] && ! is_gfk_running && start_gfk_backend
|
|
;;
|
|
2)
|
|
is_paqet_running && stop_paqet_backend
|
|
is_gfk_running && stop_gfk_backend
|
|
;;
|
|
esac
|
|
fi
|
|
read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true
|
|
fi
|
|
redraw=true
|
|
;;
|
|
8) show_settings_menu; redraw=true ;;
|
|
c|C) show_connection_info; redraw=true ;;
|
|
i|I) show_info_menu; redraw=true ;;
|
|
u|U) uninstall_paqctl; exit 0 ;;
|
|
0) echo " Exiting."; exit 0 ;;
|
|
"") ;;
|
|
*) echo -e " ${RED}Invalid choice: ${NC}${YELLOW}$choice${NC}" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# CLI Command Router
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
case "${1:-menu}" in
|
|
status) show_status ;;
|
|
start) start_paqet ;;
|
|
stop) stop_paqet ;;
|
|
restart) restart_paqet ;;
|
|
start-paqet) start_paqet_backend ;;
|
|
stop-paqet) stop_paqet_backend ;;
|
|
start-gfk) start_gfk_backend ;;
|
|
stop-gfk) stop_gfk_backend ;;
|
|
start-all) start_paqet_backend; start_gfk_backend ;;
|
|
stop-all) stop_paqet_backend; stop_gfk_backend ;;
|
|
logs) show_logs ;;
|
|
health) health_check ;;
|
|
update) update_paqet ;;
|
|
config) change_config ;;
|
|
secret) generate_secret ;;
|
|
firewall) show_firewall ;;
|
|
rollback) rollback_paqet ;;
|
|
ping) run_ping ;;
|
|
dump) run_dump ;;
|
|
backup) backup_config ;;
|
|
restore) restore_config ;;
|
|
telegram) show_telegram_menu ;;
|
|
uninstall) uninstall_paqctl ;;
|
|
version) show_version ;;
|
|
help|--help|-h) show_help ;;
|
|
menu) show_menu ;;
|
|
_apply-firewall) _apply_firewall ;;
|
|
_remove-firewall) _remove_firewall ;;
|
|
*)
|
|
echo -e "${RED}Unknown command: $1${NC}"
|
|
echo "Run 'sudo paqctl help' for usage."
|
|
exit 1
|
|
;;
|
|
esac
|
|
MANAGEMENT
|
|
|
|
# Replace placeholder
|
|
sed "s#REPLACE_ME_INSTALL_DIR#$INSTALL_DIR#g" "$tmp_script" > "$tmp_script.sed" && mv "$tmp_script.sed" "$tmp_script"
|
|
|
|
if ! chmod +x "$tmp_script"; then
|
|
log_error "Failed to make management script executable"
|
|
rm -f "$tmp_script"
|
|
return 1
|
|
fi
|
|
if ! mv -f "$tmp_script" "$INSTALL_DIR/paqctl"; then
|
|
log_error "Failed to install management script"
|
|
rm -f "$tmp_script"
|
|
return 1
|
|
fi
|
|
|
|
# Create symlink
|
|
rm -f /usr/local/bin/paqctl 2>/dev/null
|
|
if ! ln -sf "$INSTALL_DIR/paqctl" /usr/local/bin/paqctl; then
|
|
log_warn "Failed to create symlink /usr/local/bin/paqctl"
|
|
fi
|
|
|
|
log_success "Management script installed → /usr/local/bin/paqctl"
|
|
}
|
|
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
# Main Installation Flow
|
|
#═══════════════════════════════════════════════════════════════════════
|
|
|
|
_load_settings() {
|
|
[ -f "$INSTALL_DIR/settings.conf" ] || return 0
|
|
# Safe settings loading without eval
|
|
while IFS='=' read -r key value; do
|
|
[[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue
|
|
value="${value#\"}"; value="${value%\"}"
|
|
# Skip values with dangerous shell characters
|
|
[[ "$value" =~ [\`\$\(] ]] && continue
|
|
case "$key" in
|
|
BACKEND) BACKEND="$value" ;;
|
|
ROLE) ROLE="$value" ;;
|
|
PAQET_VERSION) PAQET_VERSION="$value" ;;
|
|
PAQCTL_VERSION) PAQCTL_VERSION="$value" ;;
|
|
LISTEN_PORT) [[ "$value" =~ ^[0-9]*$ ]] && LISTEN_PORT="$value" ;;
|
|
SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && SOCKS_PORT="$value" ;;
|
|
INTERFACE) INTERFACE="$value" ;;
|
|
LOCAL_IP) LOCAL_IP="$value" ;;
|
|
GATEWAY_MAC) GATEWAY_MAC="$value" ;;
|
|
ENCRYPTION_KEY) ENCRYPTION_KEY="$value" ;;
|
|
PAQET_TCP_LOCAL_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_LOCAL_FLAG="$value" ;;
|
|
PAQET_TCP_REMOTE_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_REMOTE_FLAG="$value" ;;
|
|
REMOTE_SERVER) REMOTE_SERVER="$value" ;;
|
|
GFK_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_PORT="$value" ;;
|
|
GFK_VIO_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_CLIENT_PORT="$value" ;;
|
|
GFK_QUIC_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_PORT="$value" ;;
|
|
GFK_QUIC_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_CLIENT_PORT="$value" ;;
|
|
GFK_AUTH_CODE) GFK_AUTH_CODE="$value" ;;
|
|
GFK_PORT_MAPPINGS) GFK_PORT_MAPPINGS="$value" ;;
|
|
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
|
|
install_microsocks || { log_error "Failed to install microsocks"; exit 1; }
|
|
create_gfk_client_wrapper || { log_error "Failed to create client wrapper"; exit 1; }
|
|
fi
|
|
PAQET_VERSION="$GFK_VERSION_PINNED"
|
|
log_info "Using GFK ${PAQET_VERSION} (pinned for stability)"
|
|
else
|
|
# Use pinned version for stability (update command can get latest)
|
|
PAQET_VERSION="$PAQET_VERSION_PINNED"
|
|
log_info "Installing paqet ${PAQET_VERSION} (pinned for stability)"
|
|
download_paqet "$PAQET_VERSION"
|
|
fi
|
|
echo ""
|
|
|
|
# Step 5: Apply firewall rules
|
|
log_info "Step 5/7: Firewall setup..."
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
if [ "$ROLE" = "server" ]; then
|
|
if ! command -v iptables &>/dev/null; then
|
|
log_warn "iptables not found - firewall rules cannot be applied"
|
|
else
|
|
local _vio_port="${GFK_VIO_PORT:-45000}"
|
|
log_info "Blocking VIO TCP port $_vio_port (raw socket handles it)..."
|
|
# NOTRACK: bypass conntrack for VIO packets (prevents hypervisor/bridge filtering)
|
|
modprobe iptable_raw 2>/dev/null || true
|
|
iptables -t raw -C PREROUTING -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
|
|
iptables -t raw -A PREROUTING -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
|
|
iptables -t raw -C OUTPUT -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \
|
|
iptables -t raw -A OUTPUT -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true
|
|
# Drop incoming TCP on VIO port (scapy sniffer handles it)
|
|
iptables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
iptables -A INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
log_warn "Failed to add VIO INPUT DROP rule"
|
|
# Drop outgoing RST packets on VIO port (prevents kernel from interfering)
|
|
iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
iptables -A OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
log_warn "Failed to add VIO RST DROP rule"
|
|
# IPv6 rules
|
|
if command -v ip6tables &>/dev/null; then
|
|
ip6tables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
ip6tables -A INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true
|
|
ip6tables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \
|
|
ip6tables -A OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true
|
|
fi
|
|
fi
|
|
else
|
|
log_info "Client mode - no firewall rules needed"
|
|
fi
|
|
elif [ "$ROLE" = "server" ]; then
|
|
apply_iptables_rules "$LISTEN_PORT"
|
|
else
|
|
log_info "Client mode - no firewall rules needed"
|
|
fi
|
|
echo ""
|
|
|
|
# Step 6: Create service + management script
|
|
log_info "Step 6/7: Setting up service..."
|
|
if ! mkdir -p "$INSTALL_DIR/bin" "$BACKUP_DIR"; then
|
|
log_error "Failed to create installation directories"
|
|
exit 1
|
|
fi
|
|
create_management_script
|
|
setup_service
|
|
setup_logrotate
|
|
# Save settings to persist version and config
|
|
save_settings
|
|
echo ""
|
|
|
|
# Step 7: Start the service
|
|
log_info "Step 7/7: Starting ${BACKEND}..."
|
|
if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then
|
|
systemctl start paqctl.service 2>/dev/null
|
|
fi
|
|
|
|
sleep 2
|
|
|
|
# Final summary
|
|
echo ""
|
|
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo -e "${GREEN}${BOLD} INSTALLATION COMPLETE!${NC}"
|
|
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
echo -e " Backend: ${BOLD}${BACKEND}${NC}"
|
|
echo -e " Role: ${BOLD}${ROLE}${NC}"
|
|
echo -e " Version: ${BOLD}${PAQET_VERSION}${NC}"
|
|
|
|
if [ "$BACKEND" = "gfw-knocker" ]; then
|
|
if [ "$ROLE" = "server" ]; then
|
|
local _xray_port
|
|
_xray_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f2 | cut -d, -f1)
|
|
echo -e " VIO port: ${BOLD}${GFK_VIO_PORT}${NC}"
|
|
echo -e " QUIC port: ${BOLD}${GFK_QUIC_PORT}${NC}"
|
|
if [ "${XRAY_PANEL_DETECTED:-false}" = "true" ]; then
|
|
echo -e " Xray: ${BOLD}Existing panel detected (forwarding to port ${_xray_port})${NC}"
|
|
echo -e " SOCKS5: ${BOLD}127.0.0.1:${GFK_SOCKS_PORT:-N/A} (auto-added, VIO port ${GFK_SOCKS_VIO_PORT:-N/A})${NC}"
|
|
echo ""
|
|
echo -e " ${GREEN}✓ GFK forwards to panel + SOCKS5 proxy added${NC}"
|
|
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}"
|
|
echo -e " ${YELLOW}! Direct SOCKS5: use 127.0.0.1:${GFK_SOCKS_VIO_PORT:-} on client${NC}"
|
|
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)"
|
|
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
|
|
echo -e " Server: ${BOLD}${GFK_SERVER_IP}${NC}"
|
|
echo -e " SOCKS5: ${BOLD}127.0.0.1:${MICROSOCKS_PORT}${NC}"
|
|
echo ""
|
|
echo -e " ${YELLOW}Test your proxy:${NC}"
|
|
echo -e " ${BOLD} curl --proxy socks5h://127.0.0.1:${MICROSOCKS_PORT} https://httpbin.org/ip${NC}"
|
|
fi
|
|
elif [ "$ROLE" = "server" ]; then
|
|
echo -e " Port: ${BOLD}${LISTEN_PORT}${NC}"
|
|
echo ""
|
|
echo -e "${YELLOW}╔═══════════════════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${YELLOW}║ ${BOLD}CLIENT CONNECTION INFO - SAVE THIS!${NC}${YELLOW} ║${NC}"
|
|
echo -e "${YELLOW}╠═══════════════════════════════════════════════════════════════╣${NC}"
|
|
echo -e "${YELLOW}║${NC} Server: ${BOLD}${LOCAL_IP}:${LISTEN_PORT}${NC}"
|
|
echo -e "${YELLOW}║${NC} Key: ${BOLD}${ENCRYPTION_KEY}${NC}"
|
|
echo -e "${YELLOW}╚═══════════════════════════════════════════════════════════════╝${NC}"
|
|
echo ""
|
|
echo -e " ${CYAN}Key also saved in: ${INSTALL_DIR}/config.yaml${NC}"
|
|
else
|
|
echo -e " Server: ${BOLD}${REMOTE_SERVER}${NC}"
|
|
echo -e " SOCKS5: ${BOLD}127.0.0.1:${SOCKS_PORT}${NC}"
|
|
echo ""
|
|
echo -e " ${YELLOW}Test your proxy:${NC}"
|
|
echo -e " ${BOLD} curl --proxy socks5h://127.0.0.1:${SOCKS_PORT} https://httpbin.org/ip${NC}"
|
|
fi
|
|
|
|
echo ""
|
|
echo -e " ${CYAN}Management commands:${NC}"
|
|
echo " sudo paqctl menu Interactive menu"
|
|
echo " sudo paqctl status Check status"
|
|
echo " sudo paqctl health Health check"
|
|
echo " sudo paqctl logs View logs"
|
|
echo " sudo paqctl update Update paqet"
|
|
echo " sudo paqctl help All commands"
|
|
echo ""
|
|
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
|
|
echo ""
|
|
echo -e " ${BOLD}${YELLOW}⚠ IMPORTANT: Save the connection info above before continuing!${NC}"
|
|
echo ""
|
|
echo -e " ${CYAN}Press Y to open the management menu, or any other key to exit...${NC}"
|
|
read -n 1 -r choice < /dev/tty || true
|
|
echo ""
|
|
if [[ "$choice" =~ ^[Yy]$ ]]; then
|
|
exec /usr/local/bin/paqctl menu
|
|
else
|
|
echo -e " ${GREEN}Run 'sudo paqctl menu' when ready.${NC}"
|
|
echo ""
|
|
fi
|
|
}
|
|
|
|
# Handle command line arguments
|
|
case "${1:-}" in
|
|
menu)
|
|
check_root
|
|
if [ -f "$INSTALL_DIR/settings.conf" ]; then
|
|
_load_settings
|
|
show_menu
|
|
else
|
|
echo -e "${RED}paqctl is not installed. Run the installer first.${NC}"
|
|
exit 1
|
|
fi
|
|
;;
|
|
*)
|
|
main "$@"
|
|
;;
|
|
esac
|