paqctl v1.0.0 - Unified proxy manager for bypassing firewalls

Features:
- Dual backend support: paqet (KCP) and GFW-knocker (violated TCP + QUIC)
- Both backends can run simultaneously when both are installed
- Automatic config.yaml generation for paqet backend
- Windows client support with PowerShell script
- Telegram monitoring integration
- Systemd service management

Backends:
- paqet: Single Go binary with built-in SOCKS5 (port 1080)
- GFW-knocker: Python-based with violated TCP tunneling (port 14000)
This commit is contained in:
SamNet-dev
2026-02-04 00:05:04 -06:00
commit 975acc4cf5
13 changed files with 10042 additions and 0 deletions

36
gfk/server/mainserver.py Normal file
View File

@@ -0,0 +1,36 @@
import subprocess
import os
import time
import sys
import signal
scripts = ['quic_server.py', 'vio_server.py']
def run_script(script_name):
# Use sys.executable to run with the same Python interpreter (venv)
os.system(f"pkill -f {script_name}")
time.sleep(0.5)
p = subprocess.Popen([sys.executable, script_name])
return p
processes = []
def signal_handler(sig, frame):
print('You pressed Ctrl+C!')
for p in processes:
print("terminated:",p)
p.terminate()
sys.exit(0)
if __name__ == "__main__":
p1 = run_script(scripts[0])
time.sleep(1)
p2 = run_script(scripts[1])
processes.extend([p1, p2]) # Modify global list, don't shadow it
signal.signal(signal.SIGINT, signal_handler)
p1.wait()
p2.wait()
print("All subprocesses have completed.")

310
gfk/server/quic_server.py Normal file
View File

@@ -0,0 +1,310 @@
import asyncio
import logging
import signal
import sys
from aioquic.asyncio import QuicConnectionProtocol, serve
from aioquic.quic.configuration import QuicConfiguration
from aioquic.quic.events import ConnectionTerminated, StreamDataReceived, StreamReset
import parameters
# Setup basic logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("QuicServer")
# Global list to track active protocol instances
active_protocols = []
class TunnelServerProtocol(QuicConnectionProtocol):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.loop = asyncio.get_event_loop()
self.tcp_connections = {} # Map TCP connections to QUIC streams
self.udp_connections = {} # Map UDP connections to QUIC streams
self.udp_last_activity = {} # Track last activity time for UDP connections
active_protocols.append(self) # Add this protocol instance to the list
try:
asyncio.create_task(self.cleanup_stale_udp_connections())
except Exception as e:
logger.info(f"Error in cleanup_stale_udp task: {e}")
def connection_lost(self, exc):
logger.info("Quic channel lost")
if self in active_protocols:
active_protocols.remove(self)
super().connection_lost(exc)
self.close_all_tcp_connections()
self.close_all_udp_connections()
def close_all_tcp_connections(self):
logger.info("Closing all TCP connections from server...")
for stream_id, (reader, writer) in self.tcp_connections.items():
logger.info(f"Closing TCP connection for stream {stream_id}...")
writer.close()
self.tcp_connections.clear()
def close_all_udp_connections(self):
logger.info("Closing all UDP connections from server...")
for stream_id, (transport, _) in self.udp_connections.items():
logger.info(f"Closing UDP connection for stream {stream_id}...")
transport.close()
self.udp_connections.clear()
self.udp_last_activity.clear()
def close_this_stream(self, stream_id):
try:
logger.info(f"FIN to stream={stream_id} sent")
self._quic.send_stream_data(stream_id, b"", end_stream=True) # Send FIN flag
self.transmit() # Send the FIN flag over the network
except Exception as e:
logger.info(f"Error closing stream at server: {e}")
try:
if stream_id in self.tcp_connections:
writer = self.tcp_connections[stream_id][1]
writer.close()
del self.tcp_connections[stream_id]
if stream_id in self.udp_connections:
transport, _ = self.udp_connections[stream_id]
transport.close()
del self.udp_connections[stream_id]
del self.udp_last_activity[stream_id]
except Exception as e:
logger.info(f"Error closing socket at server: {e}")
async def cleanup_stale_udp_connections(self):
logger.info("UDP cleanup task running!")
check_time = min(parameters.udp_timeout,60)
while True:
await asyncio.sleep(check_time) # Run cleanup every 60 seconds
current_time = self.loop.time()
stale_streams = [
stream_id for stream_id, last_time in self.udp_last_activity.items()
if current_time - last_time > parameters.udp_timeout
]
for stream_id in stale_streams:
logger.info(f"idle UDP stream={stream_id} timeout reached")
self.close_this_stream(stream_id)
async def forward_tcp_to_quic(self, stream_id, reader):
logger.info(f"Task TCP to QUIC started")
try:
while True:
data = await reader.read(4096) # Read data from TCP socket
if not data:
break
# logger.info(f"Forwarding data from TCP to QUIC on stream {stream_id}")
self._quic.send_stream_data(stream_id=stream_id, data=data, end_stream=False)
self.transmit() # Flush
except Exception as e:
logger.info(f"Error forwarding TCP to QUIC: {e}")
finally:
logger.info(f"Task TCP to QUIC Ended")
self.close_this_stream(stream_id)
async def connect_tcp(self, stream_id, target_port):
logger.info(f"Connecting to TCP:{target_port}...")
try:
reader, writer = await asyncio.open_connection(parameters.xray_server_ip_address, target_port)
logger.info(f"TCP connection established for stream {stream_id} to port {target_port}")
# Start forwarding data from TCP to QUIC
asyncio.create_task(self.forward_tcp_to_quic(stream_id, reader))
resp_data = parameters.quic_auth_code + "i am ready,!###!"
self._quic.send_stream_data(stream_id=stream_id, data=resp_data.encode("utf-8"), end_stream=False)
self.transmit() # Flush
self.tcp_connections[stream_id] = (reader, writer)
except Exception as e:
logger.info(f"Failed to establish TCP:{target_port} connection: {e}")
self.close_this_stream(stream_id)
async def forward_udp_to_quic(self, stream_id, protocol):
logger.info(f"Task UDP to QUIC started")
try:
while True:
data, _ = await protocol.queue.get() # Wait for data from UDP
if(data == None):
break
# logger.info(f"Forwarding data from UDP to QUIC on stream {stream_id}")
self._quic.send_stream_data(stream_id, data)
self.transmit() # Flush
self.udp_last_activity[stream_id] = self.loop.time()
except Exception as e:
logger.info(f"Error forwarding UDP to QUIC: {e}")
finally:
logger.info(f"Task UDP to QUIC Ended")
self.close_this_stream(stream_id)
async def connect_udp(self, stream_id, target_port):
class UdpProtocol:
def __init__(self):
self.transport = None
self.queue = asyncio.Queue()
self.stream_id = stream_id
def connection_made(self, transport):
self.transport = transport
def datagram_received(self, data, addr):
logger.info(f"put this to queue data={data} addr={addr}")
self.queue.put_nowait((data, addr))
def error_received(self, exc):
logger.info(f"UDP error received: {exc}")
self.queue.put_nowait((None, None)) # to cancel task
if(self.transport):
self.transport.close()
logger.info("UDP transport closed")
def connection_lost(self, exc):
logger.info("UDP connection lost.")
self.queue.put_nowait((None, None)) # to cancel task
if(self.transport):
self.transport.close()
logger.info("UDP transport closed")
try:
# Create a UDP socket
logger.info(f"Connecting to UDP:{target_port}...")
loop = asyncio.get_event_loop()
transport, protocol = await loop.create_datagram_endpoint(
UdpProtocol,
remote_addr=(parameters.xray_server_ip_address, target_port)
)
self.udp_connections[stream_id] = (transport, protocol)
self.udp_last_activity[stream_id] = self.loop.time() # Track last activity time
logger.info(f"UDP connection established for stream {stream_id} to port {target_port}")
asyncio.create_task(self.forward_udp_to_quic(stream_id, protocol))
except Exception as e:
logger.info(f"Failed to establish UDP connection: {e}")
def quic_event_received(self, event):
# print("EVENT",event)
if isinstance(event, StreamDataReceived):
try:
# logger.info(f"Server received from QUIC on stream {event.stream_id}")
# logger.info(f"Server TCP IDs -> {self.tcp_connections.keys()}")
# logger.info(f"Server UDP IDs -> {self.udp_connections.keys()}")
if event.end_stream:
logger.info(f"Stream={event.stream_id} closed by client.")
self.close_this_stream(event.stream_id)
# Forward data to the corresponding TCP connection
elif event.stream_id in self.tcp_connections:
writer = self.tcp_connections[event.stream_id][1]
writer.write(event.data) # Send data over TCP
try:
asyncio.create_task(writer.drain())
except ConnectionResetError as e42:
logger.info(f"ERR in writer drain task : {e42}")
except Exception as e43:
logger.info(f"ERR in writer drain task : {e43}")
# Forward data to the corresponding UDP connection
elif event.stream_id in self.udp_connections:
transport, _ = self.udp_connections[event.stream_id]
transport.sendto(event.data) # Send data over UDP
self.udp_last_activity[event.stream_id] = self.loop.time() # Update last activity time
else:
socket_type = None
socket_port = 0
# Assume req is like => auth+"connect,udp,443,!###!"
new_req = event.data.split(b",!###!", 1)
req_header = ""
try:
req_header = new_req[0].decode("utf-8")
except Exception as e47:
logger.info(f"ERR in req decoding : {e47}")
req_header=""
logger.info("New req comes -> " + req_header)
if req_header.startswith(parameters.quic_auth_code + "connect,"):
j = len(parameters.quic_auth_code) + 8
if req_header[j:j + 3] == "tcp":
socket_type = "tcp"
elif req_header[j:j + 3] == "udp":
socket_type = "udp"
try:
socket_port = int(req_header[j + 4:])
except ValueError:
logger.info("Invalid port.")
if socket_port > 0:
if socket_type == "tcp":
# New stream detected, create a TCP connection
asyncio.create_task(self.connect_tcp(event.stream_id, socket_port))
elif socket_type == "udp":
# New stream detected, create a UDP connection
asyncio.create_task(self.connect_udp(event.stream_id, socket_port))
else:
logger.info("Invalid Req: socket type unknown.")
else:
logger.info("Invalid Req: socket port unknown.")
else:
logger.info("Invalid Req header")
except Exception as e:
logger.info(f"Quic event server error: {e}")
elif isinstance(event, StreamReset):
# Handle stream reset (client closed the stream)
logger.info(f"Stream {event.stream_id} reset by client.")
self.close_this_stream(event.stream_id)
elif isinstance(event, ConnectionTerminated):
logger.info(f"Connection lost: {event.reason_phrase}")
self.connection_lost(event.reason_phrase)
async def run_server():
configuration = QuicConfiguration(is_client=False)
configuration.load_cert_chain(parameters.quic_cert_filepath[0], parameters.quic_cert_filepath[1])
configuration.max_data = parameters.quic_max_data
configuration.max_stream_data = parameters.quic_max_stream_data
configuration.idle_timeout = parameters.quic_idle_timeout
configuration.max_datagram_size = parameters.quic_mtu
# Start QUIC server
await serve("0.0.0.0", parameters.quic_server_port, configuration=configuration, create_protocol=TunnelServerProtocol)
logger.warning(f"Server listening for QUIC on port {parameters.quic_server_port}")
# Keep the server running
await asyncio.Future() # Run forever
def handle_shutdown(signum, frame):
logger.info("Shutting down server gracefully...")
for protocol in active_protocols:
protocol.close_all_tcp_connections()
protocol.close_all_udp_connections()
protocol.close()
logger.info("Server shutdown complete.")
sys.exit(0)
if __name__ == "__main__":
signal.signal(signal.SIGTERM, handle_shutdown)
signal.signal(signal.SIGINT, handle_shutdown)
asyncio.run(run_server())

230
gfk/server/vio_server.py Normal file
View File

@@ -0,0 +1,230 @@
from scapy.all import AsyncSniffer,IP,TCP,Raw,conf
import asyncio
import random
import parameters
import logging
import time
# Setup basic logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("VioServer")
vps_ip = parameters.vps_ip
vio_tcp_server_port = parameters.vio_tcp_server_port
vio_udp_server_port = parameters.vio_udp_server_port
quic_local_ip = parameters.quic_local_ip
quic_server_port = parameters.quic_server_port
global client_ip # obtained during sniffing
global client_port # obtained during sniffing
client_ip = "1.1.1.1"
client_port = 443
tcp_options=[
('MSS', 1280), # Maximum Segment Size
('WScale', 8), # Window Scale
('SAckOK', ''), # Selective ACK Permitted
]
async def async_sniff_realtime(qu1):
logger.info("sniffer started")
try:
def process_packet(packet):
# logger.info(f"sniffed before if at {time.time()}")
# Check flags using 'in' to handle different flag orderings (AP vs PA)
flags = str(packet[TCP].flags) if packet.haslayer(TCP) else ''
if packet.haslayer(TCP) and packet[TCP].dport == vio_tcp_server_port and 'A' in flags and 'P' in flags:
data1 = packet[TCP].load
client_ip = packet[IP].src
client_port = packet[TCP].sport
qu1.put_nowait( (data1,client_ip,client_port) )
# logger.info(f"sniffed on tcp : {client_ip} {client_port} at {time.time()}")
async def start_sniffer():
sniffer = AsyncSniffer(prn=process_packet,
filter=f"tcp and dst host {vps_ip} and dst port {vio_tcp_server_port}",
store=False)
sniffer.start()
return sniffer
sniffer = await start_sniffer()
return sniffer
except Exception as e:
logger.info(f"sniff Generic error: {e}....")
async def forward_vio_to_quic(qu1, transport):
global client_ip
global client_port
logger.info(f"Task vio to Quic started")
addr = (quic_local_ip,quic_server_port)
# addr = ("192.168.1.140",quic_server_port)
try:
while True:
# update client_ip, client_port from the queue
data,client_ip,client_port = await qu1.get()
# logger.info(f"data qu1 fetched {data} at {time.time()}")
if(data == None):
break
transport.sendto(data , addr)
# logger.info(f"data sent to udp {data} -> {addr} at {time.time()}")
# qu1.task_done()
except Exception as e:
logger.info(f"Error forwarding vio to Quic: {e}")
finally:
logger.info(f"Task vio to Quic Ended.")
basepkt = IP() / TCP(sport=vio_tcp_server_port, seq=1, flags="AP", ack=0, options=tcp_options) / Raw(load=b"")
skt = conf.L3socket()
def send_to_violated_TCP(binary_data,client_ip,client_port):
# logger.info(f"client ip = {client_ip}")
new_pkt = basepkt.copy()
new_pkt[IP].dst = client_ip
new_pkt[TCP].dport = client_port
# new_pkt[TCP].seq = random.randint(1024,1048576)
# new_pkt[TCP].ack = random.randint(1024,1048576)
new_pkt[TCP].load = binary_data
skt.send(new_pkt)
async def forward_quic_to_vio(protocol):
logger.info(f"Task QUIC to vio started")
global client_ip
global client_port
try:
while True:
data = await protocol.queue.get() # Wait for data from UDP
if(data == None):
break
send_to_violated_TCP(data,client_ip,client_port)
# logger.info(f"data send to tcp {data} at {time.time()}")
except Exception as e:
logger.info(f"Error forwarding QUIC to vio: {e}")
finally:
logger.info(f"Task QUIC to vio Ended.")
async def start_udp_server(qu1):
while True:
try:
logger.warning(f"violated tcp:{vio_tcp_server_port} -> quic {quic_local_ip}:{quic_server_port} -> ")
loop = asyncio.get_event_loop()
transport, udp_protocol = await loop.create_datagram_endpoint(
lambda: UdpProtocol(),
local_addr=("0.0.0.0", vio_udp_server_port),
remote_addr=(quic_local_ip, quic_server_port)
# remote_addr=("192.168.1.140", quic_server_port)
)
task1 = asyncio.create_task(forward_quic_to_vio(udp_protocol))
task2 = asyncio.create_task(forward_vio_to_quic(qu1,transport))
while True:
await asyncio.sleep(0.02) # this make async loop to switch better between process
if(udp_protocol.has_error):
task1.cancel()
task2.cancel()
await asyncio.sleep(1)
logger.info(f"all task cancelled")
break
except Exception as e:
logger.info(f"vioServer ERR: {e}")
finally:
transport.close()
await asyncio.sleep(0.5)
transport.abort()
logger.info("aborting transport ...")
await asyncio.sleep(1.5)
logger.info("vio inner finished")
class UdpProtocol:
def __init__(self):
self.transport = None
self.has_error = False
self.queue = asyncio.Queue()
def connection_made(self, transport):
logger.info("NEW DGRAM socket created")
logger.info(transport.get_extra_info('socket'))
self.transport = transport
def pause_writing(self):
pass # UDP doesn't need flow control, but we had to implement
def resume_writing(self):
pass # UDP doesn't need flow control, but we had to implement
def datagram_received(self, data, addr):
self.queue.put_nowait(data)
# logger.info(f"data received from udp {data} at {time.time()}")
def error_received(self, exc):
logger.info(f"UDP error received: {exc}")
self.has_error = True
if(self.transport):
self.transport.close()
logger.info("UDP transport closed")
def connection_lost(self, exc):
logger.info(f"UDP lost. {exc}")
self.has_error = True
if(self.transport):
self.transport.close()
logger.info("UDP transport closed")
async def run_vio_server():
sniffer = None
try:
qu1 = asyncio.Queue()
sniffer = await async_sniff_realtime(qu1)
await asyncio.gather(
start_udp_server(qu1),
return_exceptions=True
)
logger.info("end ?")
except SystemExit as e:
logger.info(f"Caught SystemExit: {e}")
except asyncio.CancelledError as e:
logger.info(f"cancelling error: {e}")
except ConnectionError as e:
logger.info(f"Connection error: {e}")
except Exception as e:
logger.info(f"Generic error: {e}")
finally:
if sniffer is not None:
sniffer.stop()
logger.info("stop sniffer")
if __name__ == "__main__":
asyncio.run(run_vio_server())