proxynt 2.0.24__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 (104) hide show
  1. {proxynt-2.0.24 → proxynt-2.0.26}/PKG-INFO +1 -1
  2. proxynt-2.0.26/client/data_connection_manager.py +250 -0
  3. {proxynt-2.0.24 → proxynt-2.0.26}/client/tcp_forward_client.py +30 -12
  4. {proxynt-2.0.24 → proxynt-2.0.26}/client/udp_forward_client.py +37 -14
  5. proxynt-2.0.26/common/speed_limit.py +89 -0
  6. {proxynt-2.0.24 → proxynt-2.0.26}/constant/message_type_constnat.py +4 -1
  7. {proxynt-2.0.24 → proxynt-2.0.26}/constant/system_constant.py +1 -1
  8. {proxynt-2.0.24 → proxynt-2.0.26}/proxynt.egg-info/PKG-INFO +1 -1
  9. {proxynt-2.0.24 → proxynt-2.0.26}/proxynt.egg-info/SOURCES.txt +2 -0
  10. proxynt-2.0.26/proxynt.egg-info/requires.txt +12 -0
  11. {proxynt-2.0.24 → proxynt-2.0.26}/run_client.py +102 -9
  12. {proxynt-2.0.24 → proxynt-2.0.26}/run_server.py +18 -8
  13. proxynt-2.0.26/server/session_manager.py +288 -0
  14. {proxynt-2.0.24 → proxynt-2.0.26}/server/tcp_forward_client.py +16 -8
  15. {proxynt-2.0.24 → proxynt-2.0.26}/server/udp_forward_client.py +15 -11
  16. {proxynt-2.0.24 → proxynt-2.0.26}/server/websocket_handler.py +193 -33
  17. {proxynt-2.0.24 → proxynt-2.0.26}/setup.py +2 -2
  18. proxynt-2.0.24/common/speed_limit.py +0 -33
  19. proxynt-2.0.24/proxynt.egg-info/requires.txt +0 -12
  20. {proxynt-2.0.24 → proxynt-2.0.26}/LICENSE +0 -0
  21. {proxynt-2.0.24 → proxynt-2.0.26}/MANIFEST.in +0 -0
  22. {proxynt-2.0.24 → proxynt-2.0.26}/__init__.py +0 -0
  23. {proxynt-2.0.24 → proxynt-2.0.26}/client/__init__.py +0 -0
  24. {proxynt-2.0.24 → proxynt-2.0.26}/client/abstract_tunnel.py +0 -0
  25. {proxynt-2.0.24 → proxynt-2.0.26}/client/clear_nonce_task.py +0 -0
  26. {proxynt-2.0.24 → proxynt-2.0.26}/client/heart_beat_task.py +0 -0
  27. {proxynt-2.0.24 → proxynt-2.0.26}/client/kcp_tunnel_impl.py +0 -0
  28. {proxynt-2.0.24 → proxynt-2.0.26}/client/n4_tunnel_manager.py +0 -0
  29. {proxynt-2.0.24 → proxynt-2.0.26}/client/quic_tunnel_impl.py +0 -0
  30. {proxynt-2.0.24 → proxynt-2.0.26}/client/tunnel_protocol.py +0 -0
  31. {proxynt-2.0.24 → proxynt-2.0.26}/common/__init__.py +0 -0
  32. {proxynt-2.0.24 → proxynt-2.0.26}/common/cert_utils.py +0 -0
  33. {proxynt-2.0.24 → proxynt-2.0.26}/common/crypto/__init__.py +0 -0
  34. {proxynt-2.0.24 → proxynt-2.0.26}/common/crypto/table.py +0 -0
  35. {proxynt-2.0.24 → proxynt-2.0.26}/common/encrypt_utils.py +0 -0
  36. {proxynt-2.0.24 → proxynt-2.0.26}/common/kcp.py +0 -0
  37. {proxynt-2.0.24 → proxynt-2.0.26}/common/logger_factory.py +0 -0
  38. {proxynt-2.0.24 → proxynt-2.0.26}/common/n4_protocol.py +0 -0
  39. {proxynt-2.0.24 → proxynt-2.0.26}/common/n4_punch.py +0 -0
  40. {proxynt-2.0.24 → proxynt-2.0.26}/common/nat_serialization.py +0 -0
  41. {proxynt-2.0.24 → proxynt-2.0.26}/common/pool.py +0 -0
  42. {proxynt-2.0.24 → proxynt-2.0.26}/common/register_append_data.py +0 -0
  43. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/__init__.py +0 -0
  44. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/_abnf.py +0 -0
  45. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/_app.py +0 -0
  46. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/_cookiejar.py +0 -0
  47. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/_core.py +0 -0
  48. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/_exceptions.py +0 -0
  49. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/_handshake.py +0 -0
  50. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/_http.py +0 -0
  51. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/_logging.py +0 -0
  52. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/_socket.py +0 -0
  53. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/_ssl_compat.py +0 -0
  54. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/_url.py +0 -0
  55. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/_utils.py +0 -0
  56. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/_wsdump.py +0 -0
  57. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/tests/__init__.py +0 -0
  58. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/tests/echo-server.py +0 -0
  59. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/tests/test_abnf.py +0 -0
  60. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/tests/test_app.py +0 -0
  61. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/tests/test_cookiejar.py +0 -0
  62. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/tests/test_http.py +0 -0
  63. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/tests/test_url.py +0 -0
  64. {proxynt-2.0.24 → proxynt-2.0.26}/common/websocket/tests/test_websocket.py +0 -0
  65. {proxynt-2.0.24 → proxynt-2.0.26}/constant/__init__.py +0 -0
  66. {proxynt-2.0.24 → proxynt-2.0.26}/context/__init__.py +0 -0
  67. {proxynt-2.0.24 → proxynt-2.0.26}/context/context_utils.py +0 -0
  68. {proxynt-2.0.24 → proxynt-2.0.26}/entity/__init__.py +0 -0
  69. {proxynt-2.0.24 → proxynt-2.0.26}/entity/client_config_entity.py +0 -0
  70. {proxynt-2.0.24 → proxynt-2.0.26}/entity/message/__init__.py +0 -0
  71. {proxynt-2.0.24 → proxynt-2.0.26}/entity/message/message_entity.py +0 -0
  72. {proxynt-2.0.24 → proxynt-2.0.26}/entity/message/push_config_entity.py +0 -0
  73. {proxynt-2.0.24 → proxynt-2.0.26}/entity/message/tcp_over_websocket_message.py +0 -0
  74. {proxynt-2.0.24 → proxynt-2.0.26}/entity/server_config_entity.py +0 -0
  75. {proxynt-2.0.24 → proxynt-2.0.26}/exceptions/__init__.py +0 -0
  76. {proxynt-2.0.24 → proxynt-2.0.26}/exceptions/duplicated_name.py +0 -0
  77. {proxynt-2.0.24 → proxynt-2.0.26}/exceptions/invalid_password.py +0 -0
  78. {proxynt-2.0.24 → proxynt-2.0.26}/exceptions/replay_error.py +0 -0
  79. {proxynt-2.0.24 → proxynt-2.0.26}/exceptions/signature_error.py +0 -0
  80. {proxynt-2.0.24 → proxynt-2.0.26}/p2ptest/__init__.py +0 -0
  81. {proxynt-2.0.24 → proxynt-2.0.26}/p2ptest/client.py +0 -0
  82. {proxynt-2.0.24 → proxynt-2.0.26}/p2ptest/n4.py +0 -0
  83. {proxynt-2.0.24 → proxynt-2.0.26}/proxynt.egg-info/dependency_links.txt +0 -0
  84. {proxynt-2.0.24 → proxynt-2.0.26}/proxynt.egg-info/entry_points.txt +0 -0
  85. {proxynt-2.0.24 → proxynt-2.0.26}/proxynt.egg-info/top_level.txt +0 -0
  86. {proxynt-2.0.24 → proxynt-2.0.26}/server/__init__.py +0 -0
  87. {proxynt-2.0.24 → proxynt-2.0.26}/server/admin_http_handler.py +0 -0
  88. {proxynt-2.0.24 → proxynt-2.0.26}/server/n4.py +0 -0
  89. {proxynt-2.0.24 → proxynt-2.0.26}/server/n4_signal_service.py +0 -0
  90. {proxynt-2.0.24 → proxynt-2.0.26}/server/task/__init__.py +0 -0
  91. {proxynt-2.0.24 → proxynt-2.0.26}/server/task/check_cookie_task.py +0 -0
  92. {proxynt-2.0.24 → proxynt-2.0.26}/server/task/clear_nonce_task.py +0 -0
  93. {proxynt-2.0.24 → proxynt-2.0.26}/server/task/heart_beat_task.py +0 -0
  94. {proxynt-2.0.24 → proxynt-2.0.26}/server/template/__init__.py +0 -0
  95. {proxynt-2.0.24 → proxynt-2.0.26}/server/template/base.html +0 -0
  96. {proxynt-2.0.24 → proxynt-2.0.26}/server/template/css/fonts/element-icons.woff +0 -0
  97. {proxynt-2.0.24 → proxynt-2.0.26}/server/template/css/index.css +0 -0
  98. {proxynt-2.0.24 → proxynt-2.0.26}/server/template/ele_index.html +0 -0
  99. {proxynt-2.0.24 → proxynt-2.0.26}/server/template/js/axios.min.js +0 -0
  100. {proxynt-2.0.24 → proxynt-2.0.26}/server/template/js/index.js +0 -0
  101. {proxynt-2.0.24 → proxynt-2.0.26}/server/template/js/vue.min.js +0 -0
  102. {proxynt-2.0.24 → proxynt-2.0.26}/server/template/login.html +0 -0
  103. {proxynt-2.0.24 → proxynt-2.0.26}/setup.cfg +0 -0
  104. {proxynt-2.0.24 → proxynt-2.0.26}/test_exchange.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: proxynt
3
- Version: 2.0.24
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,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
@@ -146,6 +146,9 @@ class TcpForwardClient:
146
146
  # N4 Tunnel Manager for P2P data transfer
147
147
  self.tunnel_manager = None
148
148
 
149
+ # 多连接支持:DataConnectionManager(由 run_client 注入)
150
+ self.data_conn_manager = None
151
+
149
152
  # Pending data buffer: uid -> list of (data, timestamp)
150
153
  # Used to buffer P2P data that arrives before connection is established
151
154
  self.pending_data: Dict[bytes, list] = {}
@@ -295,14 +298,8 @@ class TcpForwardClient:
295
298
  if not connection:
296
299
  return
297
300
 
298
- if data.speed_limiter and data.speed_limiter.is_exceed()[0]:
299
- self.socket_event_loop.unregister_and_register_delay(each, data, 1)
300
- return
301
-
302
301
  try:
303
302
  recv = each.recv(data.read_size)
304
- if data.speed_limiter:
305
- data.speed_limiter.add(len(recv))
306
303
  except OSError:
307
304
  recv = b''
308
305
 
@@ -311,11 +308,11 @@ class TcpForwardClient:
311
308
  # 尝试走 P2P 隧道
312
309
  if self.tunnel_manager.send_data(connection.uid, recv):
313
310
  # 如果发送成功返回 True,逻辑结束
314
- if not recv: # 本地连接关闭,通知隧道
311
+ if not recv: # 本地连接关闭,通知隧道
315
312
  self.close_connection(each)
316
313
  return
317
314
 
318
- # --- 回退/初始逻辑:走 WebSocket ---
315
+ # --- 回退/初始逻辑:走 WebSocket ---
319
316
  send_message: MessageEntity = {
320
317
  'type_': MessageTypeConstant.WEBSOCKET_OVER_TCP,
321
318
  'data': {
@@ -326,9 +323,21 @@ class TcpForwardClient:
326
323
  }
327
324
  }
328
325
 
329
- connection.sender.enqueue_message(
330
- NatSerialization.dumps(send_message, ContextUtils.get_password(), self.compress_support)
331
- )
326
+ # 发送端限速:在发送前等待
327
+ if data.speed_limiter and recv:
328
+ wait_time = data.speed_limiter.acquire(len(recv))
329
+ if wait_time > 0:
330
+ time.sleep(wait_time)
331
+
332
+ # 多连接模式:优先使用数据连接
333
+ message_bytes = NatSerialization.dumps(send_message, ContextUtils.get_password(), self.compress_support)
334
+ if self.data_conn_manager and self.data_conn_manager.is_ready():
335
+ if not self.data_conn_manager.send_by_uid(connection.uid, message_bytes):
336
+ # 数据连接发送失败,回退到控制连接
337
+ connection.sender.enqueue_message(message_bytes)
338
+ else:
339
+ # 单连接模式:使用控制连接
340
+ connection.sender.enqueue_message(message_bytes)
332
341
 
333
342
  if not recv:
334
343
  try:
@@ -487,7 +496,16 @@ class TcpForwardClient:
487
496
  }
488
497
  }
489
498
  start_time = time.time()
490
- self.ws.send(NatSerialization.dumps(send_message, ContextUtils.get_password(), self.compress_support), websocket.ABNF.OPCODE_BINARY)
499
+ message_bytes = NatSerialization.dumps(send_message, ContextUtils.get_password(), self.compress_support)
500
+
501
+ # 多连接模式:优先使用数据连接
502
+ if self.data_conn_manager and self.data_conn_manager.is_ready():
503
+ if not self.data_conn_manager.send_by_uid(connection.uid, message_bytes):
504
+ # 数据连接发送失败,回退到控制连接
505
+ self.ws.send(message_bytes, websocket.ABNF.OPCODE_BINARY)
506
+ else:
507
+ # 单连接模式
508
+ self.ws.send(message_bytes, websocket.ABNF.OPCODE_BINARY)
491
509
  LoggerFactory.get_logger().debug(f'Send to websocket cost time {time.time() - start_time}')
492
510
 
493
511
  def send_by_uid(self, uid: bytes, msg: bytes):
@@ -53,6 +53,9 @@ class UdpForwardClient:
53
53
  self.c2c_listeners: Dict[str, socket.socket] = {} # rule_name → listener socket
54
54
  self.c2c_uid_to_rule: Dict[bytes, str] = {} # UID → rule_name
55
55
 
56
+ # 多连接支持:DataConnectionManager(由 run_client 注入)
57
+ self.data_conn_manager = None
58
+
56
59
  def set_running(self, running: bool):
57
60
  """Set running state"""
58
61
  self.running = running
@@ -131,6 +134,9 @@ class UdpForwardClient:
131
134
  protocol = rule['protocol']
132
135
  speed_limit = rule.get('speed_limit', 0.0)
133
136
 
137
+ # 创建限速器
138
+ speed_limiter = SpeedLimiter(speed_limit) if speed_limit > 0 else None
139
+
134
140
  # Check if using direct mode (target_ip + target_port) or service mode (target_service)
135
141
  use_direct_mode = 'target_ip' in rule and 'target_port' in rule
136
142
 
@@ -205,10 +211,22 @@ class UdpForwardClient:
205
211
  'ip_port': f"{source_addr[0]}:{source_addr[1]}"
206
212
  }
207
213
  }
208
- self.ws.send(
209
- NatSerialization.dumps(send_message, ContextUtils.get_password(), self.compress_support),
210
- websocket.ABNF.OPCODE_BINARY
211
- )
214
+
215
+ # 发送端限速:在发送前等待
216
+ if speed_limiter and data:
217
+ wait_time = speed_limiter.acquire(len(data))
218
+ if wait_time > 0:
219
+ time.sleep(wait_time)
220
+
221
+ message_bytes = NatSerialization.dumps(send_message, ContextUtils.get_password(), self.compress_support)
222
+ # 多连接模式:优先使用数据连接
223
+ if self.data_conn_manager and self.data_conn_manager.is_ready():
224
+ if not self.data_conn_manager.send_by_uid(uid, message_bytes):
225
+ # 数据连接发送失败,回退到控制连接
226
+ self.ws.send(message_bytes, websocket.ABNF.OPCODE_BINARY)
227
+ else:
228
+ # 单连接模式
229
+ self.ws.send(message_bytes, websocket.ABNF.OPCODE_BINARY)
212
230
  LoggerFactory.get_logger().debug(f'C2C UDP data forwarded: {rule_name} UID: {uid.hex()}, len: {len(data)}')
213
231
 
214
232
  except OSError as e:
@@ -246,15 +264,6 @@ class UdpForwardClient:
246
264
 
247
265
  def _handle_udp_data(self, conn: UdpSocketConnection, data: bytes, addr):
248
266
  """Handle received UDP data"""
249
- # Speed limit handling
250
- if conn.speed_limiter and conn.speed_limiter.is_exceed()[0]:
251
- if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
252
- LoggerFactory.get_logger().debug('UDP speed limit exceeded')
253
- return
254
-
255
- if conn.speed_limiter:
256
- conn.speed_limiter.add(len(data))
257
-
258
267
  # Construct UDP message and send to public server via WebSocket
259
268
  send_message: MessageEntity = {
260
269
  'type_': MessageTypeConstant.WEBSOCKET_OVER_UDP,
@@ -270,7 +279,21 @@ class UdpForwardClient:
270
279
  LoggerFactory.get_logger().debug(f'Sending UDP to WebSocket, uid: {conn.uid}, len: {len(data)}')
271
280
 
272
281
  try:
273
- self.ws.send(NatSerialization.dumps(send_message, ContextUtils.get_password(), self.compress_support), websocket.ABNF.OPCODE_BINARY)
282
+ # 发送端限速:在发送前等待
283
+ if conn.speed_limiter and data:
284
+ wait_time = conn.speed_limiter.acquire(len(data))
285
+ if wait_time > 0:
286
+ time.sleep(wait_time)
287
+
288
+ message_bytes = NatSerialization.dumps(send_message, ContextUtils.get_password(), self.compress_support)
289
+ # 多连接模式:优先使用数据连接
290
+ if self.data_conn_manager and self.data_conn_manager.is_ready():
291
+ if not self.data_conn_manager.send_by_uid(conn.uid, message_bytes):
292
+ # 数据连接发送失败,回退到控制连接
293
+ self.ws.send(message_bytes, websocket.ABNF.OPCODE_BINARY)
294
+ else:
295
+ # 单连接模式
296
+ self.ws.send(message_bytes, websocket.ABNF.OPCODE_BINARY)
274
297
  except Exception as e:
275
298
  LoggerFactory.get_logger().error(f"Failed to send UDP to WebSocket: {e}")
276
299
 
@@ -0,0 +1,89 @@
1
+ import time
2
+ import threading
3
+
4
+
5
+ class SpeedLimiter:
6
+ """
7
+ 令牌桶限速器(发送端限速)
8
+
9
+ 使用方式:
10
+ limiter = SpeedLimiter(max_speed_mb=1.0) # 1 MB/s
11
+
12
+ # 同步使用
13
+ wait_time = limiter.acquire(len(data))
14
+ if wait_time > 0:
15
+ time.sleep(wait_time)
16
+ send(data)
17
+
18
+ # 异步使用
19
+ wait_time = limiter.acquire(len(data))
20
+ if wait_time > 0:
21
+ await asyncio.sleep(wait_time)
22
+ await send(data)
23
+ """
24
+
25
+ def __init__(self, max_speed_mb=0):
26
+ """
27
+ :param max_speed_mb: 最大速度,单位 MB/s,0 表示不限速
28
+ """
29
+ self.max_speed = max_speed_mb * 1024 * 1024 # 转换为字节/秒
30
+ self.tokens = self.max_speed # 初始令牌数(允许突发)
31
+ self.last_time = time.time()
32
+ self.lock = threading.Lock()
33
+
34
+ def acquire(self, data_len):
35
+ """
36
+ 获取发送许可
37
+
38
+ :param data_len: 要发送的数据长度(字节)
39
+ :return: 需要等待的时间(秒),0 表示不需要等待
40
+ """
41
+ if self.max_speed <= 0:
42
+ return 0
43
+
44
+ with self.lock:
45
+ now = time.time()
46
+ elapsed = now - self.last_time
47
+ self.last_time = now
48
+
49
+ # 补充令牌(按时间流逝补充,最多补充到 max_speed)
50
+ self.tokens = min(self.max_speed, self.tokens + elapsed * self.max_speed)
51
+
52
+ # 消耗令牌
53
+ self.tokens -= data_len
54
+
55
+ if self.tokens >= 0:
56
+ return 0 # 有足够令牌,不需要等待
57
+
58
+ # 令牌不足,计算需要等待的时间
59
+ wait_time = -self.tokens / self.max_speed
60
+ return wait_time
61
+
62
+ def set_max_speed(self, max_speed_mb):
63
+ """
64
+ 动态调整限速
65
+
66
+ :param max_speed_mb: 新的最大速度,单位 MB/s
67
+ """
68
+ with self.lock:
69
+ self.max_speed = max_speed_mb * 1024 * 1024
70
+ # 重置令牌,避免突然加速
71
+ self.tokens = min(self.tokens, self.max_speed)
72
+
73
+ # 保留旧接口的兼容性(但不推荐使用)
74
+ def add(self, data_len):
75
+ """兼容旧接口,等同于 acquire 但不返回等待时间"""
76
+ self.acquire(data_len)
77
+
78
+ def is_exceed(self):
79
+ """兼容旧接口,返回 (是否超速, 剩余量)"""
80
+ if self.max_speed <= 0:
81
+ return False, 0
82
+ with self.lock:
83
+ now = time.time()
84
+ elapsed = now - self.last_time
85
+ # 不更新 last_time,只是检查
86
+ current_tokens = min(self.max_speed, self.tokens + elapsed * self.max_speed)
87
+ if current_tokens >= 0:
88
+ return False, current_tokens
89
+ return True, -current_tokens
@@ -28,4 +28,7 @@ class MessageTypeConstant:
28
28
  P2P_PRE_CONNECT = 'g'
29
29
  P2P_EXCHANGE = 'h' # UDP exchange to establish NAT mapping
30
30
  P2P_PEER_INFO = 'i' # Server sends peer's actual UDP port
31
- P2P_PUNCH_REQUEST = 'j' # Request to initiate P2P hole punching
31
+ P2P_PUNCH_REQUEST = 'j' # Request to initiate P2P hole punching
32
+
33
+ # 多连接支持
34
+ JOIN_SESSION = 'k' # 数据连接加入会话(携带 session_token)
@@ -15,7 +15,7 @@ class SystemConstant:
15
15
 
16
16
  COOKIE_EXPIRE_SECONDS = 3600 * 24
17
17
 
18
- VERSION = '2.0.24'
18
+ VERSION = '2.0.26'
19
19
 
20
20
  GITHUB = 'https://github.com/sazima/proxynt'
21
21
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: proxynt
3
- Version: 2.0.24
3
+ Version: 2.0.26
4
4
  Summary: UNKNOWN
5
5
  Home-page: https://github.com/sazima/proxynt
6
6
  License: UNKNOWN
@@ -8,6 +8,7 @@ setup.py
8
8
  ./client/__init__.py
9
9
  ./client/abstract_tunnel.py
10
10
  ./client/clear_nonce_task.py
11
+ ./client/data_connection_manager.py
11
12
  ./client/heart_beat_task.py
12
13
  ./client/kcp_tunnel_impl.py
13
14
  ./client/n4_tunnel_manager.py
@@ -74,6 +75,7 @@ setup.py
74
75
  ./server/admin_http_handler.py
75
76
  ./server/n4.py
76
77
  ./server/n4_signal_service.py
78
+ ./server/session_manager.py
77
79
  ./server/tcp_forward_client.py
78
80
  ./server/udp_forward_client.py
79
81
  ./server/websocket_handler.py
@@ -0,0 +1,12 @@
1
+ tornado
2
+ typing_extensions
3
+ xxhash>=3.0.0
4
+
5
+ [:sys_platform != "win32"]
6
+ uvloop>=0.14.0
7
+
8
+ [:sys_platform == "win32"]
9
+ winloop>=0.1.0
10
+
11
+ [snappy]
12
+ python-snappy