Files
paqctl/gfk/client/quic_client.py
SamNet-dev 975acc4cf5 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)
2026-02-04 04:25:33 -06:00

381 lines
16 KiB
Python

import asyncio
import logging
import sys
import time
import multiprocessing
from aioquic.asyncio import QuicConnectionProtocol, connect
from aioquic.quic.configuration import QuicConfiguration
from aioquic.quic.events import ConnectionTerminated, StreamDataReceived, StreamReset
import parameters
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("QuicClient")
active_protocols = []
is_quic_established = False
class TunnelClientProtocol(QuicConnectionProtocol):
def __init__(self, *args, **kwargs):
global is_quic_established
is_quic_established = False
super().__init__(*args, **kwargs)
self.loop = asyncio.get_event_loop()
self.tcp_connections = {}
self.tcp_syn_wait = {}
self.udp_addr_to_stream = {}
self.udp_stream_to_addr = {}
self.udp_stream_to_transport = {}
self.udp_last_activity = {}
active_protocols.append(self)
asyncio.create_task(self.cleanup_stale_udp_connections())
asyncio.create_task(self.check_start_connectivity())
async def check_start_connectivity(self):
global is_quic_established
try:
await asyncio.sleep(7)
if is_quic_established:
logger.info(f"Quic Established!")
else:
logger.info(f"Quic FAILED to connect")
self.connection_lost("quic connectivity")
except SystemExit as e:
logger.info(f"connectivity SystemExit: {e}")
except Exception as e:
logger.info(f"connectivity err: {e}")
def connection_lost(self, exc):
super().connection_lost(exc)
self.close_all_tcp_connections()
logger.info("QUIC connection lost. exit")
for protocol in active_protocols:
protocol.close_all_tcp_connections()
protocol.close_all_udp_connections()
protocol.close()
protocol = None
if self in active_protocols:
active_protocols.remove(self)
time.sleep(1)
sys.exit()
def close_all_tcp_connections(self):
logger.info("close all tcp")
for stream_id, (reader, writer) in self.tcp_connections.items():
logger.info(f"Closing TCP connection for stream {stream_id}...")
try:
writer.close()
except Exception as e:
logger.info(f"Error closing tcp socket: {e}")
for stream_id, (reader, writer) in self.tcp_syn_wait.items():
logger.info(f"Closing TCP connection for stream {stream_id}...")
try:
writer.close()
except Exception as e:
logger.info(f"Error closing tcp socket: {e}")
self.tcp_connections.clear()
self.tcp_syn_wait.clear()
def close_all_udp_connections(self):
logger.info("close all udp")
self.udp_addr_to_stream.clear()
self.udp_stream_to_addr.clear()
self.udp_last_activity.clear()
self.udp_stream_to_transport.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)
self.transmit()
except Exception as e:
logger.info(f"Error closing stream at client: {e}")
try:
if stream_id in self.tcp_connections:
try:
writer = self.tcp_connections[stream_id][1]
writer.close()
del self.tcp_connections[stream_id]
except Exception as e:
logger.info(f"Error closing tcp estblsh at client: {e}")
if stream_id in self.tcp_syn_wait:
try:
writer = self.tcp_syn_wait[stream_id][1]
writer.close()
del self.tcp_syn_wait[stream_id]
except Exception as e:
logger.info(f"Error closing tcp syn at client: {e}")
if stream_id in self.udp_stream_to_addr:
try:
addr = self.udp_stream_to_addr.get(stream_id)
del self.udp_addr_to_stream[addr]
del self.udp_stream_to_addr[stream_id]
del self.udp_last_activity[stream_id]
del self.udp_stream_to_transport[stream_id]
except Exception as e:
logger.info(f"Error closing udp at client: {e}")
except Exception as e:
logger.info(f"Error closing socket at client: {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)
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):
logger.info(f"Task TCP to QUIC started")
try:
(reader, writer) = self.tcp_syn_wait[stream_id]
self.tcp_connections[stream_id] = (reader, writer)
del self.tcp_syn_wait[stream_id]
while True:
data = await reader.read(4096)
if not data:
break
self._quic.send_stream_data(stream_id=stream_id, data=data, end_stream=False)
self.transmit()
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 handle_tcp_connection(self, reader, writer, target_port):
stream_id = None
try:
stream_id = self._quic.get_next_available_stream_id()
self.tcp_syn_wait[stream_id] = (reader, writer)
req_data = parameters.quic_auth_code + "connect,tcp," + str(target_port) + ",!###!"
self._quic.send_stream_data(stream_id=stream_id, data=req_data.encode("utf-8"), end_stream=False)
self.transmit()
except Exception as e:
logger.info(f"Client Error handle tcp connection: {e}")
if stream_id is not None:
self.close_this_stream(stream_id)
async def forward_udp_to_quic(self, udp_protocol):
logger.info("Task UDP to Quic started")
stream_id = None
try:
while True:
data, addr = await udp_protocol.queue.get()
stream_id = self.udp_addr_to_stream.get(addr)
if stream_id is not None:
self._quic.send_stream_data(stream_id=stream_id, data=data, end_stream=False)
self.transmit()
self.udp_last_activity[stream_id] = self.loop.time()
else:
stream_id = self.new_udp_stream(addr, udp_protocol)
if stream_id is not None:
await asyncio.sleep(0.1)
self.udp_last_activity[stream_id] = self.loop.time()
self._quic.send_stream_data(stream_id=stream_id, data=data, end_stream=False)
self.transmit()
except Exception as e:
logger.info(f"Error forwarding UDP to QUIC: {e}")
finally:
logger.info(f"Task UDP to QUIC Ended")
if stream_id is not None:
self.close_this_stream(stream_id)
def new_udp_stream(self, addr, udp_protocol):
logger.info(f"new stream for UDP addr {addr} -> {udp_protocol.target_port}")
try:
stream_id = self._quic.get_next_available_stream_id()
self.udp_addr_to_stream[addr] = stream_id
self.udp_stream_to_addr[stream_id] = addr
self.udp_stream_to_transport[stream_id] = udp_protocol.transport
self.udp_last_activity[stream_id] = self.loop.time()
req_data = parameters.quic_auth_code + "connect,udp," + str(udp_protocol.target_port) + ",!###!"
self._quic.send_stream_data(stream_id=stream_id, data=req_data.encode("utf-8"), end_stream=False)
self.transmit()
return stream_id
except Exception as e:
logger.info(f"Client Error creating new udp stream: {e}")
return None
def quic_event_received(self, event):
if isinstance(event, StreamDataReceived):
try:
if event.end_stream:
logger.info(f"Stream={event.stream_id} closed by server.")
self.close_this_stream(event.stream_id)
elif event.stream_id in self.tcp_connections:
writer = self.tcp_connections[event.stream_id][1]
writer.write(event.data)
asyncio.create_task(writer.drain())
elif event.stream_id in self.udp_stream_to_addr:
addr = self.udp_stream_to_addr[event.stream_id]
transport = self.udp_stream_to_transport[event.stream_id]
transport.sendto(event.data, addr)
elif event.stream_id in self.tcp_syn_wait:
if event.data == (parameters.quic_auth_code + "i am ready,!###!").encode("utf-8"):
asyncio.create_task(self.forward_tcp_to_quic(event.stream_id))
else:
logger.warning("unknown Data arrived to client")
except Exception as e:
logger.info(f"Quic event client error: {e}")
elif isinstance(event, StreamReset):
logger.info(f"Stream {event.stream_id} reset unexpectedly.")
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_client():
global is_quic_established
configuration = QuicConfiguration(is_client=True)
configuration.verify_mode = parameters.quic_verify_cert
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
try:
logger.warning("Attempting to connect to QUIC server...")
async with connect(parameters.quic_local_ip,
parameters.vio_udp_client_port,
configuration=configuration,
create_protocol=TunnelClientProtocol,
local_port=parameters.quic_client_port) as client:
async def start_tcp_server(local_port, target_port):
logger.warning(f"client listen tcp:{local_port} -> to server tcp:{target_port}")
server = await asyncio.start_server(
lambda r, w: asyncio.create_task(handle_tcp_client(r, w, target_port)),
'0.0.0.0', local_port
)
async with server:
await server.serve_forever()
logger.info("tcp server finished")
async def handle_tcp_client(reader, writer, target_port):
while not active_protocols:
logger.info("Waiting for an active QUIC connection...")
await asyncio.sleep(1)
protocol = active_protocols[-1]
await protocol.handle_tcp_connection(reader, writer, target_port)
async def start_udp_server(local_port, target_port):
while True:
try:
logger.warning(f"client listen udp:{local_port} -> to server udp:{target_port}")
loop = asyncio.get_event_loop()
transport, udp_protocol = await loop.create_datagram_endpoint(
lambda: UdpProtocol(client, target_port),
local_addr=('0.0.0.0', local_port)
)
mytask = asyncio.create_task(handle_udp_client(udp_protocol))
while True:
await asyncio.sleep(0.05)
if udp_protocol.has_error:
mytask.cancel()
await asyncio.sleep(1)
break
logger.info(f"udp server finished")
except Exception as e:
logger.info(f"start_udp_server ERR: {e}")
async def handle_udp_client(udp_protocol):
logger.info("creating udp task ....")
while not active_protocols:
logger.info("Waiting for an active QUIC connection...")
await asyncio.sleep(1)
protocol = active_protocols[-1]
await protocol.forward_udp_to_quic(udp_protocol)
class UdpProtocol:
def __init__(self, client, target_port):
self.transport = None
self.client = client
self.target_port = target_port
self.has_error = False
self.queue = asyncio.Queue()
def connection_made(self, transport):
logger.info("NEW DGRAM listen created")
logger.info(transport.get_extra_info('socket'))
self.transport = transport
def datagram_received(self, data, addr):
self.queue.put_nowait((data, addr))
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")
is_quic_established = True
tcp_servers_list = []
for lport, tport in parameters.tcp_port_mapping.items():
tcp_servers_list.append(start_tcp_server(lport, tport))
udp_servers_list = []
for lport, tport in parameters.udp_port_mapping.items():
udp_servers_list.append(start_udp_server(lport, tport))
await asyncio.gather(
asyncio.Future(),
*tcp_servers_list,
*udp_servers_list
)
except SystemExit as e:
logger.info(f"Caught SystemExit: {e}")
except asyncio.CancelledError as e:
logger.info(f"cancelling error: {e}. Retrying...")
except ConnectionError as e:
logger.info(f"Connection error: {e}. Retrying...")
except Exception as e:
logger.info(f"Generic error: {e}. Retrying...")
def Quic_client():
asyncio.run(run_client())
if __name__ == "__main__":
while True:
process = multiprocessing.Process(target=Quic_client)
process.start()
while process.is_alive():
time.sleep(5)
logger.info("client is dead. restarting ...")
time.sleep(1)