307 lines
12 KiB
Python
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()
|