feat(lite): merge TUN mode with IPv6 routing

This commit is contained in:
RoyirinRosmontis
2026-09-19 03:41:41 +08:00
parent 3e29012d9c
commit 50364dc17a
23 changed files with 2996 additions and 237 deletions
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""RRPP 裸 UoT v2 relay。
nginx 的 proxy_connect 模块负责 TLS、Basic Auth 和 CONNECT,relay 只监听
127.0.0.1:15201,收到的首字节就是 UoT v2 request,不再解析 HTTP。
"""
from __future__ import annotations
import ipaddress
import logging
import socket
import struct
import threading
MAGIC = "sp.v2.udp-over-tcp.arpa"
LISTEN_HOST = "127.0.0.1"
LISTEN_PORT = 15201
MAX_PACKET = 65535
IDLE_TIMEOUT = 9 * 60
def read_exact(conn: socket.socket, size: int) -> bytes:
data = bytearray()
while len(data) < size:
chunk = conn.recv(size - len(data))
if not chunk:
raise ConnectionError("客户端连接已关闭")
data.extend(chunk)
return bytes(data)
def read_address(conn: socket.socket):
atyp = read_exact(conn, 1)[0]
if atyp == 0:
host = str(ipaddress.IPv4Address(read_exact(conn, 4)))
elif atyp == 1:
host = str(ipaddress.IPv6Address(read_exact(conn, 16)))
elif atyp == 2:
size = read_exact(conn, 1)[0]
host = read_exact(conn, size).decode("idna")
else:
raise ValueError(f"未知 UoT 地址类型: {atyp}")
return host, struct.unpack("!H", read_exact(conn, 2))[0]
def encode_address(host: str, port: int) -> bytes:
try:
addr = ipaddress.ip_address(host)
except ValueError:
raw = host.encode("idna")
return b"\x02" + bytes([len(raw)]) + raw + struct.pack("!H", port)
return (b"\x00" if addr.version == 4 else b"\x01") + addr.packed + struct.pack("!H", port)
def resolve_destination(host: str, port: int):
return socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_DGRAM)[0][4]
def handle(conn: socket.socket, peer):
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp.settimeout(0.2)
# 与 nginx 的 10 分钟读写超时错开,空闲连接由 relay 先回收。
conn.settimeout(IDLE_TIMEOUT)
send_lock = threading.Lock()
try:
is_connect = read_exact(conn, 1)[0]
first_target = read_address(conn)
logging.info("UoT client %s target=%s:%s connect=%s", peer, *first_target, is_connect)
def udp_reader():
while True:
try:
data, source = udp.recvfrom(MAX_PACKET)
except socket.timeout:
continue
except OSError:
return
try:
with send_lock:
if is_connect:
conn.sendall(struct.pack("!H", len(data)) + data)
else:
conn.sendall(encode_address(source[0], source[1]) +
struct.pack("!H", len(data)) + data)
except OSError:
return
threading.Thread(target=udp_reader, daemon=True).start()
if is_connect:
while True:
length = struct.unpack("!H", read_exact(conn, 2))[0]
payload = read_exact(conn, length)
udp.sendto(payload, resolve_destination(*first_target))
else:
while True:
destination = read_address(conn)
length = struct.unpack("!H", read_exact(conn, 2))[0]
payload = read_exact(conn, length)
udp.sendto(payload, resolve_destination(*destination))
except (ConnectionError, OSError, ValueError) as exc:
logging.info("UoT client %s closed: %s", peer, exc)
finally:
udp.close()
conn.close()
def main():
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listener.bind((LISTEN_HOST, LISTEN_PORT))
listener.listen(128)
logging.info("RRPP UoT relay listening on %s:%d", LISTEN_HOST, LISTEN_PORT)
while True:
conn, peer = listener.accept()
threading.Thread(target=handle, args=(conn, peer), daemon=True).start()
if __name__ == "__main__":
main()