proxynt 2.0.20__tar.gz → 2.0.26__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. {proxynt-2.0.20/proxynt.egg-info → proxynt-2.0.26}/PKG-INFO +1 -1
  2. proxynt-2.0.26/client/abstract_tunnel.py +31 -0
  3. proxynt-2.0.26/client/data_connection_manager.py +250 -0
  4. proxynt-2.0.26/client/kcp_tunnel_impl.py +52 -0
  5. proxynt-2.0.26/client/n4_tunnel_manager.py +246 -0
  6. proxynt-2.0.26/client/quic_tunnel_impl.py +18 -0
  7. {proxynt-2.0.20 → proxynt-2.0.26}/client/tcp_forward_client.py +114 -18
  8. proxynt-2.0.26/client/tunnel_protocol.py +174 -0
  9. {proxynt-2.0.20 → proxynt-2.0.26}/client/udp_forward_client.py +37 -14
  10. proxynt-2.0.26/common/cert_utils.py +76 -0
  11. proxynt-2.0.26/common/kcp.py +852 -0
  12. proxynt-2.0.26/common/n4_protocol.py +142 -0
  13. proxynt-2.0.26/common/n4_punch.py +239 -0
  14. {proxynt-2.0.20 → proxynt-2.0.26}/common/nat_serialization.py +10 -8
  15. proxynt-2.0.26/common/speed_limit.py +89 -0
  16. {proxynt-2.0.20 → proxynt-2.0.26}/constant/message_type_constnat.py +8 -0
  17. {proxynt-2.0.20 → proxynt-2.0.26}/constant/system_constant.py +1 -1
  18. {proxynt-2.0.20 → proxynt-2.0.26}/context/context_utils.py +12 -1
  19. proxynt-2.0.26/p2ptest/client.py +153 -0
  20. proxynt-2.0.26/p2ptest/n4.py +459 -0
  21. {proxynt-2.0.20 → proxynt-2.0.26/proxynt.egg-info}/PKG-INFO +1 -1
  22. {proxynt-2.0.20 → proxynt-2.0.26}/proxynt.egg-info/SOURCES.txt +18 -77
  23. proxynt-2.0.26/proxynt.egg-info/requires.txt +12 -0
  24. proxynt-2.0.26/run_client.py +617 -0
  25. {proxynt-2.0.20 → proxynt-2.0.26}/run_server.py +28 -8
  26. proxynt-2.0.26/server/n4.py +459 -0
  27. proxynt-2.0.26/server/n4_signal_service.py +326 -0
  28. proxynt-2.0.26/server/session_manager.py +288 -0
  29. {proxynt-2.0.20 → proxynt-2.0.26}/server/tcp_forward_client.py +16 -8
  30. proxynt-2.0.26/server/template/__init__.py +0 -0
  31. {proxynt-2.0.20 → proxynt-2.0.26}/server/udp_forward_client.py +86 -15
  32. {proxynt-2.0.20 → proxynt-2.0.26}/server/websocket_handler.py +288 -159
  33. {proxynt-2.0.20 → proxynt-2.0.26}/setup.py +2 -2
  34. proxynt-2.0.26/test_exchange.py +46 -0
  35. proxynt-2.0.20/client/p2p_hole_punch.py +0 -308
  36. proxynt-2.0.20/common/speed_limit.py +0 -33
  37. proxynt-2.0.20/proxynt.egg-info/requires.txt +0 -12
  38. proxynt-2.0.20/run_client.py +0 -510
  39. {proxynt-2.0.20 → proxynt-2.0.26}/LICENSE +0 -0
  40. {proxynt-2.0.20 → proxynt-2.0.26}/MANIFEST.in +0 -0
  41. {proxynt-2.0.20 → proxynt-2.0.26}/__init__.py +0 -0
  42. {proxynt-2.0.20 → proxynt-2.0.26}/client/__init__.py +0 -0
  43. {proxynt-2.0.20 → proxynt-2.0.26}/client/clear_nonce_task.py +0 -0
  44. {proxynt-2.0.20 → proxynt-2.0.26}/client/heart_beat_task.py +0 -0
  45. {proxynt-2.0.20 → proxynt-2.0.26}/common/__init__.py +0 -0
  46. {proxynt-2.0.20 → proxynt-2.0.26}/common/crypto/__init__.py +0 -0
  47. {proxynt-2.0.20 → proxynt-2.0.26}/common/crypto/table.py +0 -0
  48. {proxynt-2.0.20 → proxynt-2.0.26}/common/encrypt_utils.py +0 -0
  49. {proxynt-2.0.20 → proxynt-2.0.26}/common/logger_factory.py +0 -0
  50. {proxynt-2.0.20 → proxynt-2.0.26}/common/pool.py +0 -0
  51. {proxynt-2.0.20 → proxynt-2.0.26}/common/register_append_data.py +0 -0
  52. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/__init__.py +0 -0
  53. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_abnf.py +0 -0
  54. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_app.py +0 -0
  55. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_cookiejar.py +0 -0
  56. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_core.py +0 -0
  57. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_exceptions.py +0 -0
  58. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_handshake.py +0 -0
  59. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_http.py +0 -0
  60. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_logging.py +0 -0
  61. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_socket.py +0 -0
  62. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_ssl_compat.py +0 -0
  63. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_url.py +0 -0
  64. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_utils.py +0 -0
  65. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_wsdump.py +0 -0
  66. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/__init__.py +0 -0
  67. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/echo-server.py +0 -0
  68. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/test_abnf.py +0 -0
  69. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/test_app.py +0 -0
  70. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/test_cookiejar.py +0 -0
  71. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/test_http.py +0 -0
  72. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/test_url.py +0 -0
  73. {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/test_websocket.py +0 -0
  74. {proxynt-2.0.20 → proxynt-2.0.26}/constant/__init__.py +0 -0
  75. {proxynt-2.0.20 → proxynt-2.0.26}/context/__init__.py +0 -0
  76. {proxynt-2.0.20 → proxynt-2.0.26}/entity/__init__.py +0 -0
  77. {proxynt-2.0.20 → proxynt-2.0.26}/entity/client_config_entity.py +0 -0
  78. {proxynt-2.0.20 → proxynt-2.0.26}/entity/message/__init__.py +0 -0
  79. {proxynt-2.0.20 → proxynt-2.0.26}/entity/message/message_entity.py +0 -0
  80. {proxynt-2.0.20 → proxynt-2.0.26}/entity/message/push_config_entity.py +0 -0
  81. {proxynt-2.0.20 → proxynt-2.0.26}/entity/message/tcp_over_websocket_message.py +0 -0
  82. {proxynt-2.0.20 → proxynt-2.0.26}/entity/server_config_entity.py +0 -0
  83. {proxynt-2.0.20 → proxynt-2.0.26}/exceptions/__init__.py +0 -0
  84. {proxynt-2.0.20 → proxynt-2.0.26}/exceptions/duplicated_name.py +0 -0
  85. {proxynt-2.0.20 → proxynt-2.0.26}/exceptions/invalid_password.py +0 -0
  86. {proxynt-2.0.20 → proxynt-2.0.26}/exceptions/replay_error.py +0 -0
  87. {proxynt-2.0.20 → proxynt-2.0.26}/exceptions/signature_error.py +0 -0
  88. {proxynt-2.0.20/server → proxynt-2.0.26/p2ptest}/__init__.py +0 -0
  89. {proxynt-2.0.20 → proxynt-2.0.26}/proxynt.egg-info/dependency_links.txt +0 -0
  90. {proxynt-2.0.20 → proxynt-2.0.26}/proxynt.egg-info/entry_points.txt +0 -0
  91. {proxynt-2.0.20 → proxynt-2.0.26}/proxynt.egg-info/top_level.txt +0 -0
  92. {proxynt-2.0.20/server/task → proxynt-2.0.26/server}/__init__.py +0 -0
  93. {proxynt-2.0.20 → proxynt-2.0.26}/server/admin_http_handler.py +0 -0
  94. {proxynt-2.0.20/server/template → proxynt-2.0.26/server/task}/__init__.py +0 -0
  95. {proxynt-2.0.20 → proxynt-2.0.26}/server/task/check_cookie_task.py +0 -0
  96. {proxynt-2.0.20 → proxynt-2.0.26}/server/task/clear_nonce_task.py +0 -0
  97. {proxynt-2.0.20 → proxynt-2.0.26}/server/task/heart_beat_task.py +0 -0
  98. {proxynt-2.0.20 → proxynt-2.0.26}/server/template/base.html +0 -0
  99. {proxynt-2.0.20 → proxynt-2.0.26}/server/template/css/fonts/element-icons.woff +0 -0
  100. {proxynt-2.0.20 → proxynt-2.0.26}/server/template/css/index.css +0 -0
  101. {proxynt-2.0.20 → proxynt-2.0.26}/server/template/ele_index.html +0 -0
  102. {proxynt-2.0.20 → proxynt-2.0.26}/server/template/js/axios.min.js +0 -0
  103. {proxynt-2.0.20 → proxynt-2.0.26}/server/template/js/index.js +0 -0
  104. {proxynt-2.0.20 → proxynt-2.0.26}/server/template/js/vue.min.js +0 -0
  105. {proxynt-2.0.20 → proxynt-2.0.26}/server/template/login.html +0 -0
  106. {proxynt-2.0.20 → proxynt-2.0.26}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: proxynt
3
- Version: 2.0.20
3
+ Version: 2.0.26
4
4
  Summary: UNKNOWN
5
5
  Home-page: https://github.com/sazima/proxynt
6
6
  License: UNKNOWN
@@ -0,0 +1,31 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Callable
3
+
4
+ class AbstractTunnel(ABC):
5
+ def __init__(self, peer_name: str, sock, addr, on_data_received: Callable, on_established: Callable, on_closed: Callable):
6
+ self.peer_name = peer_name
7
+ self.sock = sock
8
+ self.addr = addr
9
+ self.on_data_received = on_data_received # 应用层收到数据回调
10
+ self.on_established = on_established # 握手成功回调
11
+ self.on_closed = on_closed # 隧道关闭回调
12
+
13
+ @abstractmethod
14
+ def establish(self):
15
+ """开始协议层面的握手/初始化"""
16
+ pass
17
+
18
+ @abstractmethod
19
+ def send(self, uid: bytes, data: bytes) -> bool:
20
+ """发送业务数据"""
21
+ pass
22
+
23
+ @abstractmethod
24
+ def update(self):
25
+ """驱动状态机(如 KCP 需要)"""
26
+ pass
27
+
28
+ @abstractmethod
29
+ def close(self):
30
+ """物理关闭"""
31
+ pass
@@ -0,0 +1,250 @@
1
+ """
2
+ 客户端数据连接管理器
3
+
4
+ 管理多个 WebSocket 数据连接:
5
+ - 建立 N 个数据连接并加入 Session
6
+ - 根据 UID hash 选择数据连接发送数据
7
+ - 处理数据连接断开重连
8
+ """
9
+
10
+ import json
11
+ import threading
12
+ import time
13
+ import traceback
14
+ from typing import List, Optional, Callable
15
+ from urllib.parse import urlparse
16
+
17
+ from common import websocket
18
+ from common.logger_factory import LoggerFactory
19
+ from common.nat_serialization import NatSerialization
20
+ from constant.message_type_constnat import MessageTypeConstant
21
+ from context.context_utils import ContextUtils
22
+ from entity.message.message_entity import MessageEntity
23
+
24
+
25
+ class DataConnection:
26
+ """单个数据连接"""
27
+
28
+ def __init__(self, channel_index: int, url: str, session_token: str,
29
+ compress_support: bool, on_message_callback: Callable):
30
+ self.channel_index = channel_index
31
+ self.url = url
32
+ self.session_token = session_token
33
+ self.compress_support = compress_support
34
+ self.on_message_callback = on_message_callback
35
+ self.ws: Optional[websocket.WebSocketApp] = None
36
+ self.is_connected = False
37
+ self.is_joined = False
38
+ self.running = True
39
+ self.thread: Optional[threading.Thread] = None
40
+
41
+ def start(self):
42
+ """启动数据连接"""
43
+ self.thread = threading.Thread(target=self._run, daemon=True)
44
+ self.thread.start()
45
+
46
+ def _run(self):
47
+ """连接循环"""
48
+ while self.running:
49
+ try:
50
+ self.ws = websocket.WebSocketApp(
51
+ self.url,
52
+ on_open=self._on_open,
53
+ on_message=self._on_message,
54
+ on_close=self._on_close,
55
+ on_error=self._on_error
56
+ )
57
+ self.ws.run_forever()
58
+ except Exception as e:
59
+ LoggerFactory.get_logger().error(f'Data connection {self.channel_index} error: {e}')
60
+
61
+ if self.running:
62
+ LoggerFactory.get_logger().info(f'Data connection {self.channel_index} reconnecting in 2s...')
63
+ time.sleep(2)
64
+
65
+ def _on_open(self, ws):
66
+ """连接建立后发送 JOIN_SESSION"""
67
+ LoggerFactory.get_logger().info(f'Data connection {self.channel_index} opened, sending JOIN_SESSION')
68
+ self.is_connected = True
69
+
70
+ # 发送 JOIN_SESSION 消息
71
+ join_message: MessageEntity = {
72
+ 'type_': MessageTypeConstant.JOIN_SESSION,
73
+ 'data': {
74
+ 'session_token': self.session_token,
75
+ 'channel_index': self.channel_index
76
+ }
77
+ }
78
+ try:
79
+ ws.send(
80
+ NatSerialization.dumps(join_message, ContextUtils.get_password(), self.compress_support),
81
+ websocket.ABNF.OPCODE_BINARY
82
+ )
83
+ except Exception as e:
84
+ LoggerFactory.get_logger().error(f'Failed to send JOIN_SESSION: {e}')
85
+
86
+ def _on_message(self, ws, message: bytes):
87
+ """处理收到的消息"""
88
+ try:
89
+ message_data: MessageEntity = NatSerialization.loads(
90
+ message, ContextUtils.get_password(), self.compress_support
91
+ )
92
+
93
+ # 处理 JOIN_SESSION 响应
94
+ if message_data['type_'] == MessageTypeConstant.JOIN_SESSION:
95
+ data = message_data['data']
96
+ if data.get('success'):
97
+ self.is_joined = True
98
+ LoggerFactory.get_logger().info(
99
+ f'Data connection {self.channel_index} joined session successfully'
100
+ )
101
+ else:
102
+ LoggerFactory.get_logger().error(
103
+ f'Data connection {self.channel_index} failed to join session'
104
+ )
105
+ ws.close()
106
+ return
107
+
108
+ # 其他消息转发给回调处理
109
+ if self.on_message_callback:
110
+ self.on_message_callback(message_data)
111
+
112
+ except Exception as e:
113
+ LoggerFactory.get_logger().error(f'Data connection {self.channel_index} message error: {e}')
114
+ LoggerFactory.get_logger().error(traceback.format_exc())
115
+
116
+ def _on_close(self, ws, code, reason):
117
+ """连接关闭"""
118
+ LoggerFactory.get_logger().info(
119
+ f'Data connection {self.channel_index} closed: code={code}, reason={reason}'
120
+ )
121
+ self.is_connected = False
122
+ self.is_joined = False
123
+
124
+ def _on_error(self, ws, error):
125
+ """连接错误"""
126
+ LoggerFactory.get_logger().error(f'Data connection {self.channel_index} error: {error}')
127
+
128
+ def send(self, data: bytes):
129
+ """发送数据"""
130
+ if self.ws and self.is_joined:
131
+ try:
132
+ self.ws.send(data, websocket.ABNF.OPCODE_BINARY)
133
+ return True
134
+ except Exception as e:
135
+ LoggerFactory.get_logger().error(f'Data connection {self.channel_index} send error: {e}')
136
+ return False
137
+
138
+ def stop(self):
139
+ """停止连接"""
140
+ self.running = False
141
+ self.is_connected = False
142
+ self.is_joined = False
143
+ if self.ws:
144
+ try:
145
+ self.ws.close()
146
+ except Exception:
147
+ pass
148
+
149
+
150
+ class DataConnectionManager:
151
+ """数据连接管理器"""
152
+
153
+ def __init__(self, server_url: str, compress_support: bool, on_message_callback: Callable):
154
+ self.server_url = server_url
155
+ self.compress_support = compress_support
156
+ self.on_message_callback = on_message_callback
157
+
158
+ self.session_token: Optional[str] = None
159
+ self.num_channels: int = 4
160
+ self.data_connections: List[DataConnection] = []
161
+ self.is_multi_connection: bool = False
162
+ self.lock = threading.Lock()
163
+
164
+ def setup_data_connections(self, session_token: str, num_channels: int = 4):
165
+ """
166
+ 设置数据连接
167
+
168
+ :param session_token: 会话令牌(hex 格式)
169
+ :param num_channels: 数据通道数量
170
+ """
171
+ with self.lock:
172
+ # 清理旧连接
173
+ self.stop_all()
174
+
175
+ self.session_token = session_token
176
+ self.num_channels = num_channels
177
+ self.is_multi_connection = True
178
+
179
+ LoggerFactory.get_logger().info(
180
+ f'Setting up {num_channels} data connections, token={session_token[:16]}...'
181
+ )
182
+
183
+ # 创建数据连接
184
+ for i in range(num_channels):
185
+ conn = DataConnection(
186
+ channel_index=i,
187
+ url=self.server_url,
188
+ session_token=session_token,
189
+ compress_support=self.compress_support,
190
+ on_message_callback=self.on_message_callback
191
+ )
192
+ self.data_connections.append(conn)
193
+ conn.start()
194
+
195
+ LoggerFactory.get_logger().info(f'Started {num_channels} data connections')
196
+
197
+ def get_data_connection(self, uid: bytes) -> Optional[DataConnection]:
198
+ """
199
+ 根据 UID 获取数据连接
200
+
201
+ :param uid: 连接 UID
202
+ :return: 数据连接,如果没有可用的返回 None
203
+ """
204
+ if not self.is_multi_connection or not self.data_connections:
205
+ return None
206
+
207
+ # 根据 UID hash 选择通道
208
+ channel_index = int.from_bytes(uid[:2], 'big') % len(self.data_connections)
209
+ conn = self.data_connections[channel_index]
210
+
211
+ # 检查连接是否可用
212
+ if conn.is_joined:
213
+ return conn
214
+
215
+ # 如果选中的通道不可用,尝试其他通道
216
+ for conn in self.data_connections:
217
+ if conn.is_joined:
218
+ return conn
219
+
220
+ return None
221
+
222
+ def send_by_uid(self, uid: bytes, data: bytes) -> bool:
223
+ """
224
+ 根据 UID 发送数据
225
+
226
+ :param uid: 连接 UID
227
+ :param data: 要发送的数据
228
+ :return: 是否成功发送
229
+ """
230
+ conn = self.get_data_connection(uid)
231
+ if conn:
232
+ return conn.send(data)
233
+ return False
234
+
235
+ def is_ready(self) -> bool:
236
+ """检查是否有数据连接可用"""
237
+ if not self.is_multi_connection:
238
+ return False
239
+ for conn in self.data_connections:
240
+ if conn.is_joined:
241
+ return True
242
+ return False
243
+
244
+ def stop_all(self):
245
+ """停止所有数据连接"""
246
+ for conn in self.data_connections:
247
+ conn.stop()
248
+ self.data_connections.clear()
249
+ self.is_multi_connection = False
250
+ self.session_token = None
@@ -0,0 +1,52 @@
1
+ import time
2
+ import threading
3
+ import socket
4
+ from .abstract_tunnel import AbstractTunnel
5
+ from common.kcp import Kcp
6
+
7
+ class KcpTunnelImpl(AbstractTunnel):
8
+ def __init__(self, *args, **kwargs):
9
+ super().__init__(*args, **kwargs)
10
+ self.kcp = Kcp(12345)
11
+ self.kcp.output = self._udp_output
12
+ self.kcp.setmtu(800) # 关键:强制小于 1000
13
+ self.kcp.nodelay(1, 10, 2, 1)
14
+ self.lock = threading.Lock()
15
+ self.running = True
16
+
17
+ def establish(self):
18
+ # KCP 是无状态的,打洞成功即认为建立成功
19
+ threading.Thread(target=self._recv_loop, daemon=True).start()
20
+ # 立即通知 Manager 成功
21
+ self.on_established(self.peer_name)
22
+
23
+ def _udp_output(self, buf):
24
+ try: self.sock.sendto(buf, self.addr)
25
+ except: pass
26
+
27
+ def _recv_loop(self):
28
+ while self.running:
29
+ try:
30
+ data, addr = self.sock.recvfrom(65536)
31
+ with self.lock:
32
+ self.kcp.input(data)
33
+ except: break
34
+
35
+ def update(self):
36
+ with self.lock:
37
+ self.kcp.update(int(time.time() * 1000))
38
+ while True:
39
+ data = self.kcp.recv()
40
+ if not data: break
41
+ if len(data) >= 4:
42
+ self.on_data_received(data[:4], data[4:])
43
+
44
+ def send(self, uid: bytes, data: bytes) -> bool:
45
+ with self.lock:
46
+ return self.kcp.send(uid + data) >= 0
47
+
48
+ def close(self):
49
+ self.running = False
50
+ try: self.sock.close()
51
+ except: pass
52
+ self.on_closed(self.peer_name)
@@ -0,0 +1,246 @@
1
+ import logging
2
+ import socket
3
+ import threading
4
+ import time
5
+ from typing import Dict, Optional, Tuple, Type, Set
6
+
7
+ from common.logger_factory import LoggerFactory
8
+ from common.n4_punch import N4PunchClient, N4Error
9
+ from .abstract_tunnel import AbstractTunnel
10
+ from .kcp_tunnel_impl import KcpTunnelImpl
11
+
12
+ def get_pair_key(client_a: str, client_b: str) -> Tuple[str, str]:
13
+ """获取两个客户端唯一的配对键"""
14
+ return (min(client_a, client_b), max(client_a, client_b))
15
+
16
+ class N4TunnelManager:
17
+ """
18
+ P2P 隧道管理器
19
+ 负责物理打洞的协调及抽象协议隧道的生命周期管理。
20
+ """
21
+ def __init__(self, local_client_name: str, server_host: str, server_port: int,
22
+ tunnel_class: Type[AbstractTunnel] = KcpTunnelImpl):
23
+
24
+ self.local_client_name = local_client_name
25
+ self.server_host = server_host
26
+ self.server_port = server_port
27
+
28
+ # 核心:解耦后的协议实现类构造器
29
+ self.TunnelClass = tunnel_class
30
+
31
+ # 存储已建立的隧道对象 {peer_name: AbstractTunnel}
32
+ self.tunnels: Dict[str, AbstractTunnel] = {}
33
+
34
+ # 正在进行的物理打洞客户端 {pair_key: N4PunchClient}
35
+ self.punch_clients: Dict[Tuple[str, str], N4PunchClient] = {}
36
+
37
+ # 映射关系 {uid: peer_name}
38
+ self.uid_to_peer: Dict[bytes, str] = {}
39
+
40
+ # 状态控制
41
+ self.pending_punch_requests: Set[str] = set() # 正在申请打洞的
42
+ self.handshaking_peers: Set[str] = set() # 正在进行打洞或协议握手的
43
+ self.last_failure_time: Dict[str, float] = {} # 记录失败时间点,用于 30s 冷却
44
+
45
+ self.lock = threading.Lock()
46
+ self.logger = LoggerFactory.get_logger()
47
+
48
+ # 外部回调(由 run_client.py 注入)
49
+ self.ws_client = None
50
+ self.on_data_received = None # func(uid, data)
51
+ self.on_tunnel_closed = None # func(peer_name)
52
+ self.on_punch_failed = None
53
+
54
+ self.running = False
55
+ self.update_thread = None
56
+
57
+ def set_ws_client(self, ws):
58
+ self.ws_client = ws
59
+
60
+ def start(self):
61
+ """启动管理器:启动全局驱动线程"""
62
+ with self.lock:
63
+ if not self.running:
64
+ self.running = True
65
+ self.update_thread = threading.Thread(target=self._global_update_loop, daemon=True)
66
+ self.update_thread.start()
67
+ self.logger.info(f"N4 Tunnel Manager Started (Protocol: {self.TunnelClass.__name__})")
68
+
69
+ def _global_update_loop(self):
70
+ """每 10ms 驱动一次所有活跃协议的状态机"""
71
+ while self.running:
72
+ # 获取当前所有隧道的快照进行遍历
73
+ with self.lock:
74
+ active_tunnels = list(self.tunnels.values())
75
+
76
+ for tunnel in active_tunnels:
77
+ try:
78
+ tunnel.update()
79
+ except Exception as e:
80
+ self.logger.error(f"Error updating tunnel {tunnel.peer_name}: {e}")
81
+
82
+ time.sleep(0.01)
83
+
84
+ def send_data(self, uid: bytes, data: bytes) -> bool:
85
+ """
86
+ 业务数据发送入口(无缝切换的核心)
87
+ 返回 True: 数据已交由 P2P 隧道发送
88
+ 返回 False: 隧道不可用,调用者应继续走 WebSocket
89
+ """
90
+ peer_name = self.uid_to_peer.get(uid)
91
+ if not peer_name:
92
+ return False
93
+
94
+ with self.lock:
95
+ tunnel = self.tunnels.get(peer_name)
96
+
97
+ # 1. 隧道已就绪,直接发送
98
+ if tunnel:
99
+ if tunnel.send(uid, data):
100
+ return True
101
+ else:
102
+ self.logger.warn(f"P2P Tunnel to {peer_name} send failed, closing.")
103
+ self._handle_failure(peer_name)
104
+ return False
105
+
106
+ # 2. 如果正在握手或打洞,静默走 WebSocket,不重复触发
107
+ if peer_name in self.pending_punch_requests or peer_name in self.handshaking_peers:
108
+ return False
109
+
110
+ # 3. 检查冷却时间:失败后 30s 内不重试打洞
111
+ now = time.time()
112
+ if now - self.last_failure_time.get(peer_name, 0) < 30:
113
+ return False
114
+
115
+ # 4. 既没有隧道也不在处理中,触发打洞请求
116
+ if self.ws_client:
117
+ self.logger.info(f"P2P required for {peer_name}. Initiating {self.TunnelClass.__name__} setup.")
118
+ with self.lock:
119
+ self.pending_punch_requests.add(peer_name)
120
+ self.ws_client._send_punch_request(peer_name)
121
+
122
+ return False
123
+
124
+ def prepare_punch(self, peer_name: str, session_id: bytes) -> Optional[socket.socket]:
125
+ """收到 PUNCH_REQUEST:初始化 Socket 池并返回用于 EXCHANGE 的第一个 Socket"""
126
+ with self.lock:
127
+ # 如果旧隧道还存在,先物理关闭
128
+ if peer_name in self.tunnels:
129
+ self.tunnels[peer_name].close()
130
+ self.tunnels.pop(peer_name, None)
131
+
132
+ punch_client = N4PunchClient(
133
+ ident=session_id,
134
+ server_host=self.server_host,
135
+ server_port=self.server_port
136
+ )
137
+
138
+ try:
139
+ exchange_sock = punch_client.send_exchange()
140
+ pair_key = get_pair_key(self.local_client_name, peer_name)
141
+ self.punch_clients[pair_key] = punch_client
142
+ return exchange_sock
143
+ except Exception as e:
144
+ self.logger.error(f"Failed to prepare punch for {peer_name}: {e}")
145
+ return None
146
+
147
+ def receive_peer_info(self, peer_name: str, peer_ip: str, peer_port: int):
148
+ """收到 PEER_INFO:后台启动物理打洞并初始化具体协议实现"""
149
+ pair_key = get_pair_key(self.local_client_name, peer_name)
150
+ punch_client = self.punch_clients.get(pair_key)
151
+
152
+ if punch_client:
153
+ threading.Thread(
154
+ target=self._run_punch_and_setup,
155
+ args=(punch_client, peer_name, peer_ip, peer_port, pair_key),
156
+ daemon=True
157
+ ).start()
158
+
159
+ def _run_punch_and_setup(self, punch_client: N4PunchClient, peer_name: str,
160
+ peer_ip: str, peer_port: int, pair_key: Tuple[str, str]):
161
+ """后台线程:执行打洞 -> 实例化 TunnelImpl -> 建立握手"""
162
+ with self.lock:
163
+ self.handshaking_peers.add(peer_name)
164
+ self.pending_punch_requests.discard(peer_name)
165
+
166
+ try:
167
+ # 1. 物理打洞
168
+ winner_sock, peer_addr = punch_client.punch(peer_ip, peer_port, wait=15)
169
+ self.logger.info(f"Punch SUCCESS for {peer_name}. Setting up {self.TunnelClass.__name__}...")
170
+
171
+ # 2. 确定角色
172
+ is_server = self.local_client_name > peer_name
173
+
174
+ # 3. 实例化具体的隧道协议实现
175
+ tunnel = self.TunnelClass(
176
+ peer_name=peer_name,
177
+ sock=winner_sock,
178
+ addr=peer_addr,
179
+ on_data_received=self.on_data_received,
180
+ on_established=self._internal_on_established,
181
+ on_closed=self._internal_on_closed,
182
+ is_server=is_server
183
+ )
184
+
185
+ # 4. 开始协议激活
186
+ tunnel.establish()
187
+
188
+ except Exception as e:
189
+ self.logger.error(f"P2P setup failed for {peer_name}: {e}")
190
+ self._handle_failure(peer_name)
191
+ if self.on_punch_failed:
192
+ self.on_punch_failed(peer_name)
193
+ finally:
194
+ with self.lock:
195
+ self.punch_clients.pop(pair_key, None)
196
+
197
+ def _internal_on_established(self, tunnel: AbstractTunnel):
198
+ """由具体 TunnelImpl 在协议握手成功后调用"""
199
+ peer_name = tunnel.peer_name
200
+ with self.lock:
201
+ self.tunnels[peer_name] = tunnel
202
+ self.handshaking_peers.discard(peer_name)
203
+ self.last_failure_time.pop(peer_name, None)
204
+ self.logger.info(f"=== [P2P READY] {self.TunnelClass.__name__} Tunnel Established for {peer_name} ===")
205
+
206
+ def _internal_on_closed(self, peer_name: str):
207
+ """由具体 TunnelImpl 在连接断开时调用"""
208
+ self._handle_failure(peer_name)
209
+ if self.on_tunnel_closed:
210
+ self.on_tunnel_closed(peer_name)
211
+
212
+ def _handle_failure(self, peer_name: str):
213
+ """统一失败处理逻辑"""
214
+ with self.lock:
215
+ self.tunnels.pop(peer_name, None)
216
+ self.handshaking_peers.discard(peer_name)
217
+ self.pending_punch_requests.discard(peer_name)
218
+ self.last_failure_time[peer_name] = time.time()
219
+
220
+ def register_uid(self, uid: bytes, peer_name: str):
221
+ with self.lock:
222
+ self.uid_to_peer[uid] = peer_name
223
+
224
+ def unregister_uid(self, uid: bytes):
225
+ with self.lock:
226
+ self.uid_to_peer.pop(uid, None)
227
+
228
+ def is_tunnel_established(self, peer_name: str) -> bool:
229
+ return peer_name in self.tunnels
230
+
231
+ def has_tunnel(self, peer_name: str) -> bool:
232
+ return peer_name in self.tunnels or peer_name in self.pending_punch_requests or peer_name in self.handshaking_peers
233
+
234
+ def get_tunnel(self, peer_name: str) -> Optional[AbstractTunnel]:
235
+ return self.tunnels.get(peer_name)
236
+
237
+ def stop(self):
238
+ """停止管理器及所有隧道"""
239
+ self.running = False
240
+ with self.lock:
241
+ for tunnel in list(self.tunnels.values()):
242
+ tunnel.close()
243
+ for puncher in list(self.punch_clients.values()):
244
+ puncher.stop()
245
+ self.tunnels.clear()
246
+ self.punch_clients.clear()
@@ -0,0 +1,18 @@
1
+ import asyncio
2
+ from .abstract_tunnel import AbstractTunnel
3
+
4
+ class QuicTunnelImpl(AbstractTunnel):
5
+ def establish(self):
6
+ # 这里封装之前 N4TunnelManager 里的 _start_quic_connection 逻辑
7
+ # 调用 self.on_established 或 self.on_closed
8
+ pass
9
+
10
+ def send(self, uid: bytes, data: bytes) -> bool:
11
+ # 之前的 aioquic send 逻辑
12
+ pass
13
+
14
+ def update(self): pass
15
+
16
+ def close(self):
17
+ # 之前的 close 逻辑
18
+ pass