|
|
@@ -18,6 +18,10 @@ SOCKS_VERSION = 5
|
|
|
UDP_WARMUP_BROADCAST_PACKETS = 6
|
|
|
UDP_SHADOW_PROBE_INTERVAL_SEC = 0.25
|
|
|
UDP_FAST_FAILOVER_MISSES = 3
|
|
|
+UDP_FLOW_IDLE_CLEANUP_SEC = 30.0
|
|
|
+UDP_PACKET_CLIENT_MAP_LIMIT = 4096
|
|
|
+UDP_DIRECT_PENDING_LIMIT = 128
|
|
|
+UDP_SOCKET_BUFFER_BYTES = 1 << 20
|
|
|
|
|
|
|
|
|
async def read_exact(reader: asyncio.StreamReader, size: int) -> bytes:
|
|
|
@@ -112,6 +116,7 @@ class UdpFlowState:
|
|
|
last_probe_at: float = 0.0
|
|
|
winner_miss_streak: int = 0
|
|
|
target_family: int = 0
|
|
|
+ last_cleanup_at: float = 0.0
|
|
|
|
|
|
def touch(self, now: float) -> None:
|
|
|
self.last_activity = now
|
|
|
@@ -124,10 +129,13 @@ class UdpAssociateServer(asyncio.DatagramProtocol):
|
|
|
self.client_addr = None
|
|
|
self.associate_peer = None
|
|
|
self.packet_counter = itertools.count(1)
|
|
|
+ self.last_packet_id = 0
|
|
|
self.client_flows: dict[tuple[tuple[str, int], str, int], UdpFlowState] = {}
|
|
|
self.flow_counter = itertools.count(1)
|
|
|
self.last_summary_at = 0.0
|
|
|
self.win_counts: Dict[str, int] = {}
|
|
|
+ self._last_client_port_log_at = 0.0
|
|
|
+ self._last_flow_cleanup_at = 0.0
|
|
|
|
|
|
def connection_made(self, transport) -> None:
|
|
|
self.transport = transport
|
|
|
@@ -149,7 +157,10 @@ class UdpAssociateServer(asyncio.DatagramProtocol):
|
|
|
print(f"[edge] udp client bound addr={addr[0]}:{addr[1]}")
|
|
|
elif addr != self.client_addr:
|
|
|
if addr[0] == self.client_addr[0]:
|
|
|
- print(f"[edge] udp client port update host={addr[0]} old_port={self.client_addr[1]} new_port={addr[1]}")
|
|
|
+ now = asyncio.get_running_loop().time()
|
|
|
+ if now - self._last_client_port_log_at >= 30:
|
|
|
+ self._last_client_port_log_at = now
|
|
|
+ print(f"[edge] udp client port update host={addr[0]} old_port={self.client_addr[1]} new_port={addr[1]}")
|
|
|
self.client_addr = addr
|
|
|
else:
|
|
|
print(f"[edge] udp client rebound old={self.client_addr[0]}:{self.client_addr[1]} new={addr[0]}:{addr[1]}")
|
|
|
@@ -166,8 +177,11 @@ class UdpAssociateServer(asyncio.DatagramProtocol):
|
|
|
flow.client_addr = (addr[0], addr[1])
|
|
|
flow.packets_sent += 1
|
|
|
packet_id = next(self.packet_counter)
|
|
|
+ self.last_packet_id = packet_id
|
|
|
flow.packet_client_addrs[packet_id] = (addr[0], addr[1])
|
|
|
+ self._cleanup_packet_state(flow, now)
|
|
|
asyncio.create_task(self.edge.forward_udp(flow, payload, packet_id, (addr[0], addr[1]), self))
|
|
|
+ self._cleanup_inactive_flows(now)
|
|
|
self._log_udp_summary()
|
|
|
|
|
|
def _reset_client_state(self, addr) -> None:
|
|
|
@@ -225,6 +239,38 @@ class UdpAssociateServer(asyncio.DatagramProtocol):
|
|
|
if flow.winner_name == source_name and target_addr is not None:
|
|
|
self.transport.sendto(packet, target_addr)
|
|
|
|
|
|
+ def _cleanup_packet_state(self, flow: UdpFlowState, now: float) -> None:
|
|
|
+ if flow.last_cleanup_at and now - flow.last_cleanup_at < 1.0:
|
|
|
+ return
|
|
|
+ flow.last_cleanup_at = now
|
|
|
+ expired_packet_ids = [
|
|
|
+ packet_id
|
|
|
+ for packet_id in flow.packet_client_addrs
|
|
|
+ if packet_id <= (self.last_packet_id - UDP_PACKET_CLIENT_MAP_LIMIT)
|
|
|
+ ]
|
|
|
+ for packet_id in expired_packet_ids:
|
|
|
+ flow.packet_client_addrs.pop(packet_id, None)
|
|
|
+ for path_name, pending in list(flow.direct_pending_clients.items()):
|
|
|
+ while len(pending) > UDP_DIRECT_PENDING_LIMIT:
|
|
|
+ pending.popleft()
|
|
|
+ if not pending:
|
|
|
+ flow.direct_pending_clients.pop(path_name, None)
|
|
|
+
|
|
|
+ def _cleanup_inactive_flows(self, now: float) -> None:
|
|
|
+ if self._last_flow_cleanup_at and now - self._last_flow_cleanup_at < 5.0:
|
|
|
+ return
|
|
|
+ self._last_flow_cleanup_at = now
|
|
|
+ expired_keys = [
|
|
|
+ key
|
|
|
+ for key, flow in self.client_flows.items()
|
|
|
+ if now - flow.last_activity >= UDP_FLOW_IDLE_CLEANUP_SEC
|
|
|
+ ]
|
|
|
+ for key in expired_keys:
|
|
|
+ flow = self.client_flows.pop(key, None)
|
|
|
+ if flow is None:
|
|
|
+ continue
|
|
|
+ self.edge.release_udp_flow(flow)
|
|
|
+
|
|
|
def set_flow_candidates(self, flow: UdpFlowState, candidate_names: tuple[str, ...]) -> None:
|
|
|
if not flow.candidate_names:
|
|
|
flow.candidate_names = candidate_names
|
|
|
@@ -307,6 +353,31 @@ class UdpEdge:
|
|
|
self.udp_flow_sessions: dict[tuple[int, int], UdpFlowState] = {}
|
|
|
self.udp_server: UdpAssociateServer | None = None
|
|
|
|
|
|
+ def _udp_direct_copies(self) -> int:
|
|
|
+ if self.config.udp_direct_copies is not None:
|
|
|
+ return max(1, self.config.udp_direct_copies)
|
|
|
+ return max(1, self.config.udp_redundancy + 1)
|
|
|
+
|
|
|
+ def _udp_relay_copies(self) -> int:
|
|
|
+ if self.config.udp_relay_copies is not None:
|
|
|
+ return max(1, self.config.udp_relay_copies)
|
|
|
+ return max(1, self.config.udp_redundancy + 1)
|
|
|
+
|
|
|
+ def release_udp_flow(self, flow: UdpFlowState) -> None:
|
|
|
+ for stream_id in list(flow.link_streams.values()):
|
|
|
+ self.udp_flow_sessions.pop((flow.flow_id, stream_id), None)
|
|
|
+ flow.link_streams.clear()
|
|
|
+ flow.initialized_links.clear()
|
|
|
+ flow.packet_client_addrs.clear()
|
|
|
+ for task in list(flow.direct_tasks.values()):
|
|
|
+ task.cancel()
|
|
|
+ flow.direct_tasks.clear()
|
|
|
+ for sock in list(flow.direct_sockets.values()):
|
|
|
+ with contextlib.suppress(Exception):
|
|
|
+ sock.close()
|
|
|
+ flow.direct_sockets.clear()
|
|
|
+ flow.direct_pending_clients.clear()
|
|
|
+
|
|
|
async def start(self) -> None:
|
|
|
await self.scheduler.start()
|
|
|
await self._connect_relays()
|
|
|
@@ -383,6 +454,10 @@ class UdpEdge:
|
|
|
family = socket.AF_INET6 if ":" in flow.target_host else socket.AF_INET
|
|
|
sock = socket.socket(family, socket.SOCK_DGRAM)
|
|
|
sock.setblocking(False)
|
|
|
+ with contextlib.suppress(OSError):
|
|
|
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, UDP_SOCKET_BUFFER_BYTES)
|
|
|
+ with contextlib.suppress(OSError):
|
|
|
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, UDP_SOCKET_BUFFER_BYTES)
|
|
|
await asyncio.get_running_loop().sock_connect(sock, (flow.target_host, flow.target_port))
|
|
|
flow.direct_sockets[name] = sock
|
|
|
flow.direct_tasks[name] = asyncio.create_task(self._pump_udp_direct(flow, name, sock, udp_server))
|
|
|
@@ -445,10 +520,11 @@ class UdpEdge:
|
|
|
active_direct_names = [direct_names[0]]
|
|
|
elif links:
|
|
|
active_links = links[:1]
|
|
|
- copies = max(1, self.config.udp_redundancy + 1)
|
|
|
+ direct_copies = self._udp_direct_copies()
|
|
|
+ relay_copies = self._udp_relay_copies()
|
|
|
sent_any = False
|
|
|
- for attempt in range(copies):
|
|
|
- for path_name in active_direct_names:
|
|
|
+ for attempt in range(max(direct_copies, relay_copies)):
|
|
|
+ for path_name in active_direct_names if attempt < direct_copies else ():
|
|
|
sock = flow.direct_sockets.get(path_name)
|
|
|
if sock is None:
|
|
|
continue
|
|
|
@@ -472,7 +548,7 @@ class UdpEdge:
|
|
|
if path_name not in flow.relay_error_seen:
|
|
|
flow.relay_error_seen.add(path_name)
|
|
|
print(f"[edge] udp relay error flow={flow.flow_id} relay={path_name} error={exc!r}")
|
|
|
- for link in active_links:
|
|
|
+ for link in active_links if attempt < relay_copies else ():
|
|
|
stream_id = flow.link_streams.get(link.node.name)
|
|
|
if stream_id is None:
|
|
|
stream_id = next(self.udp_stream_ids)
|
|
|
@@ -492,7 +568,7 @@ class UdpEdge:
|
|
|
if link.node.name not in flow.relay_error_seen:
|
|
|
flow.relay_error_seen.add(link.node.name)
|
|
|
print(f"[edge] udp relay error flow={flow.flow_id} relay={link.node.name} error={exc!r}")
|
|
|
- if attempt + 1 < copies and self.config.udp_copy_interval_ms > 0:
|
|
|
+ if attempt + 1 < max(direct_copies, relay_copies) and self.config.udp_copy_interval_ms > 0:
|
|
|
await asyncio.sleep(self.config.udp_copy_interval_ms / 1000)
|
|
|
if not sent_any:
|
|
|
udp_server.note_unsent(flow, packet_id)
|