Files
proxynet/quad4_proxynet/client.py
2025-12-26 15:09:05 -06:00

307 lines
12 KiB
Python

import socket
import threading
import RNS
import RNS.vendor.umsgpack as umsgpack
import time
from .common import APP_NAME, load_or_create_identity
class ProxynetClient:
def __init__(self, local_port=1080, identity_name="proxynet_client", preferred_servers=None):
self.local_port = local_port
self.identity = load_or_create_identity(identity_name)
if isinstance(preferred_servers, str):
self.preferred_servers = [s.strip() for s in preferred_servers.split(",") if s.strip()]
else:
self.preferred_servers = preferred_servers or []
self.discovered_servers = set()
self.lock = threading.Lock()
self.udp_associations = {}
self.aspect_filter = RNS.Destination.expand_name(None, APP_NAME, "rproxy")
# Register for announcements
RNS.Transport.register_announce_handler(self)
def start(self):
server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_sock.bind(('127.0.0.1', self.local_port))
server_sock.listen(100)
RNS.log(f"SOCKS5 proxy listening on 127.0.0.1:{self.local_port}", RNS.LOG_NOTICE)
# Start a UDP listener for SOCKS5 UDP ASSOCIATE
self.udp_relay_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.udp_relay_sock.bind(('127.0.0.1', 0))
self.udp_relay_port = self.udp_relay_sock.getsockname()[1]
threading.Thread(target=self.udp_relay_loop, daemon=True).start()
while True:
client_sock, addr = server_sock.accept()
threading.Thread(target=self.handle_socks_connection, args=(client_sock,), daemon=True).start()
def udp_relay_loop(self):
while True:
try:
data, addr = self.udp_relay_sock.recvfrom(65535)
# SOCKS5 UDP Request Header
if data[2] != 0: # Fragmented packets not supported
continue
header_pos = 4
atyp = data[3]
if atyp == 1: # IPv4
host = socket.inet_ntoa(data[header_pos:header_pos+4])
header_pos += 4
elif atyp == 3: # Domain
length = data[header_pos]
host = data[header_pos+1:header_pos+1+length].decode()
header_pos += 1 + length
elif atyp == 4: # IPv6
host = socket.inet_ntop(socket.AF_INET6, data[header_pos:header_pos+16])
header_pos += 16
else:
continue
port = int.from_bytes(data[header_pos:header_pos+2], 'big')
payload = data[header_pos+2:]
# Store association for return packets
with self.lock:
self.udp_associations[(host, port)] = addr
dest_hash = self.select_server()
if dest_hash:
link = self.establish_link(dest_hash)
if link:
msg = umsgpack.packb({
'type': 'udp_packet',
'host': host,
'port': port,
'payload': payload
})
RNS.Packet(link, msg).send()
except Exception as e:
RNS.log(f"UDP relay error: {e}", RNS.LOG_DEBUG)
def received_announce(self, destination_hash, announced_identity, app_data):
with self.lock:
if destination_hash not in self.discovered_servers:
self.discovered_servers.add(destination_hash)
RNS.log(f"Discovered proxynet server: {RNS.prettyhexrep(destination_hash)}", RNS.LOG_NOTICE)
def handle_socks_connection(self, client_sock):
try:
# SOCKS5 Handshake
version = client_sock.recv(1)
if version != b'\x05':
client_sock.close()
return
nmethods = ord(client_sock.recv(1))
methods = client_sock.recv(nmethods)
if b'\x00' not in methods:
client_sock.sendall(b'\x05\xff')
client_sock.close()
return
client_sock.sendall(b'\x05\x00')
# Request
header = client_sock.recv(4)
if header[0] != 5:
client_sock.close()
return
cmd = header[1] # 1: CONNECT, 3: UDP ASSOCIATE
atyp = header[3]
if cmd == 1: # CONNECT (TCP)
if atyp == 1: # IPv4
host = socket.inet_ntoa(client_sock.recv(4))
elif atyp == 3: # Domain name
domain_len = ord(client_sock.recv(1))
host = client_sock.recv(domain_len).decode()
elif atyp == 4: # IPv6
host = socket.inet6_ntop(socket.AF_INET6, client_sock.recv(16))
else:
client_sock.close()
return
port = int.from_bytes(client_sock.recv(2), 'big')
RNS.log(f"SOCKS5 TCP request for {host}:{port}", RNS.LOG_NOTICE)
self.handle_tcp_forward(client_sock, host, port)
elif cmd == 3: # UDP ASSOCIATE
# The app tells us its address (usually ignored)
# We tell the app our UDP relay address
RNS.log(f"SOCKS5 UDP ASSOCIATE request", RNS.LOG_NOTICE)
# Skip the address provided by the app
if atyp == 1: client_sock.recv(4)
elif atyp == 3: client_sock.recv(ord(client_sock.recv(1)))
elif atyp == 4: client_sock.recv(16)
client_sock.recv(2) # Skip port
# Send response with our UDP relay port
# BND.ADDR (127.0.0.1) and BND.PORT
reply = b'\x05\x00\x00\x01' + socket.inet_aton('127.0.0.1') + self.udp_relay_port.to_bytes(2, 'big')
client_sock.sendall(reply)
# We must keep the TCP connection open. When it closes, the UDP association ends.
while True:
if not client_sock.recv(1024):
break
client_sock.close()
else:
client_sock.sendall(b'\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00') # Command not supported
client_sock.close()
except Exception as e:
RNS.log(f"Error handling connection: {e}", RNS.LOG_ERROR)
try:
client_sock.close()
except:
pass
def handle_tcp_forward(self, client_sock, host, port):
# Select server
dest_hash = self.select_server()
if not dest_hash:
RNS.log("No servers available", RNS.LOG_ERROR)
client_sock.sendall(b'\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00')
client_sock.close()
return
# Establish Link
link = self.establish_link(dest_hash)
if not link:
client_sock.sendall(b'\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00')
client_sock.close()
return
response_received = threading.Event()
success = [False]
def packet_received(message, packet):
try:
data = umsgpack.unpackb(message)
if data.get('type') == 'response':
success[0] = data.get('success')
response_received.set()
elif data.get('type') == 'data':
client_sock.sendall(data.get('payload'))
elif data.get('type') == 'udp_response':
# Wrap the response in SOCKS5 UDP header and send to the app
host = data.get('host')
port = data.get('port')
payload = data.get('payload')
with self.lock:
app_addr = self.udp_associations.get((host, port))
if app_addr:
# SOCKS5 UDP Header: RSV(0,0) FRAG(0) ATYP(1) BND.ADDR(4) BND.PORT(2)
header = b'\x00\x00\x00\x01' + socket.inet_aton(host) + port.to_bytes(2, 'big')
self.udp_relay_sock.sendto(header + payload, app_addr)
except Exception as e:
RNS.log(f"Client packet error: {e}", RNS.LOG_ERROR)
link.set_packet_callback(packet_received)
connect_msg = umsgpack.packb({
'type': 'connect',
'host': host,
'port': port
})
RNS.Packet(link, connect_msg).send()
if not response_received.wait(timeout=30) or not success[0]:
client_sock.sendall(b'\x05\x01\x00\x01\x00\x00\x00\x00\x00\x00')
client_sock.close()
link.teardown()
return
client_sock.sendall(b'\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00')
# Relay data
try:
while True:
data = client_sock.recv(16384)
if not data:
break
msg = umsgpack.packb({'type': 'data', 'payload': data})
RNS.Packet(link, msg).send()
except Exception as e:
RNS.log(f"Relay error: {e}", RNS.LOG_DEBUG)
finally:
link.teardown()
client_sock.close()
def select_server(self):
# 1. Check preferred servers
for server_hex in self.preferred_servers:
try:
dest_hash = bytes.fromhex(server_hex)
if RNS.Transport.has_path(dest_hash):
return dest_hash
except:
continue
# 2. Check discovered servers
with self.lock:
for dest_hash in self.discovered_servers:
if RNS.Transport.has_path(dest_hash):
return dest_hash
# 3. Try to request path to preferred servers
if self.preferred_servers:
server_hex = self.preferred_servers[0]
try:
dest_hash = bytes.fromhex(server_hex)
RNS.Transport.request_path(dest_hash)
start = time.time()
while not RNS.Transport.has_path(dest_hash) and time.time() - start < 5:
time.sleep(0.5)
if RNS.Transport.has_path(dest_hash):
return dest_hash
except:
pass
return None
def establish_link(self, dest_hash):
remote_identity = RNS.Identity.recall(dest_hash)
if not remote_identity:
RNS.log(f"Could not recall identity for {RNS.prettyhexrep(dest_hash)}", RNS.LOG_ERROR)
return None
destination = RNS.Destination(
remote_identity,
RNS.Destination.OUT,
RNS.Destination.SINGLE,
APP_NAME,
"rproxy"
)
link = RNS.Link(destination)
start = time.time()
while link.status != RNS.Link.ACTIVE:
time.sleep(0.1)
if time.time() - start > 15:
RNS.log("Link establishment timed out", RNS.LOG_ERROR)
return None
if link.status == RNS.Link.CLOSED:
RNS.log("Link establishment failed", RNS.LOG_ERROR)
return None
return link
def run_client(port=1080, identity_name="proxynet_client", preferred_servers=None):
client = ProxynetClient(port, identity_name, preferred_servers)
client.start()