proxynt 2.0.61rc1__tar.gz → 2.1.0__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 (150) hide show
  1. {proxynt-2.0.61rc1 → proxynt-2.1.0}/PKG-INFO +1 -1
  2. {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/tcp_forward_client.py +29 -1
  3. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/nat_serialization_v1.py +9 -0
  4. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/pool.py +63 -15
  5. {proxynt-2.0.61rc1 → proxynt-2.1.0}/constant/message_type_constnat.py +6 -1
  6. {proxynt-2.0.61rc1 → proxynt-2.1.0}/constant/system_constant.py +1 -1
  7. proxynt-2.1.0/p2ptest/tls_handshake_timeout_test.py +66 -0
  8. {proxynt-2.0.61rc1 → proxynt-2.1.0}/proxynt.egg-info/PKG-INFO +1 -1
  9. {proxynt-2.0.61rc1 → proxynt-2.1.0}/proxynt.egg-info/SOURCES.txt +1 -0
  10. {proxynt-2.0.61rc1 → proxynt-2.1.0}/run_client.py +25 -11
  11. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/tcp_forward_client.py +172 -58
  12. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/websocket_handler.py +99 -60
  13. {proxynt-2.0.61rc1 → proxynt-2.1.0}/LICENSE +0 -0
  14. {proxynt-2.0.61rc1 → proxynt-2.1.0}/MANIFEST.in +0 -0
  15. {proxynt-2.0.61rc1 → proxynt-2.1.0}/__init__.py +0 -0
  16. {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/__init__.py +0 -0
  17. {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/abstract_tunnel.py +0 -0
  18. {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/clear_nonce_task.py +0 -0
  19. {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/heart_beat_task.py +0 -0
  20. {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/kcp_tunnel_impl.py +0 -0
  21. {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/n4_tunnel_manager.py +0 -0
  22. {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/quic_tunnel_impl.py +0 -0
  23. {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/tunnel_protocol.py +0 -0
  24. {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/udp_forward_client.py +0 -0
  25. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/__init__.py +0 -0
  26. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/cert_utils.py +0 -0
  27. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/crypto/__init__.py +0 -0
  28. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/crypto/table.py +0 -0
  29. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/encrypt_utils.py +0 -0
  30. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/kcp.py +0 -0
  31. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/logger_factory.py +0 -0
  32. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/n4_protocol.py +0 -0
  33. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/n4_punch.py +0 -0
  34. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/nat_serialization.py +0 -0
  35. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/nat_serialization_v2.py +0 -0
  36. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/register_append_data.py +0 -0
  37. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/speed_limit.py +0 -0
  38. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/__init__.py +0 -0
  39. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_abnf.py +0 -0
  40. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_app.py +0 -0
  41. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_cookiejar.py +0 -0
  42. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_core.py +0 -0
  43. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_dispatcher.py +0 -0
  44. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_exceptions.py +0 -0
  45. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_handshake.py +0 -0
  46. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_http.py +0 -0
  47. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_logging.py +0 -0
  48. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_socket.py +0 -0
  49. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_ssl_compat.py +0 -0
  50. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_url.py +0 -0
  51. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_utils.py +0 -0
  52. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_wsdump.py +0 -0
  53. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/__init__.py +0 -0
  54. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/echo-server.py +0 -0
  55. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_abnf.py +0 -0
  56. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_app.py +0 -0
  57. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_cookiejar.py +0 -0
  58. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_dispatcher.py +0 -0
  59. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_handshake_large_response.py +0 -0
  60. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_http.py +0 -0
  61. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_large_payloads.py +0 -0
  62. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_logging_helpers.py +0 -0
  63. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_reconnect_bad_fd.py +0 -0
  64. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_socket.py +0 -0
  65. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_socket_bugs.py +0 -0
  66. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_ssl_compat.py +0 -0
  67. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_ssl_edge_cases.py +0 -0
  68. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_url.py +0 -0
  69. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_utils.py +0 -0
  70. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_websocket.py +0 -0
  71. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/__init__.py +0 -0
  72. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/compliance/autobahn-test-report-Feb-03-2021.html +0 -0
  73. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/setup.py +0 -0
  74. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/__init__.py +0 -0
  75. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_abnf.py +0 -0
  76. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_app.py +0 -0
  77. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_cookiejar.py +0 -0
  78. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_core.py +0 -0
  79. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_dispatcher.py +0 -0
  80. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_exceptions.py +0 -0
  81. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_handshake.py +0 -0
  82. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_http.py +0 -0
  83. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_logging.py +0 -0
  84. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_socket.py +0 -0
  85. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_ssl_compat.py +0 -0
  86. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_url.py +0 -0
  87. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_utils.py +0 -0
  88. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_wsdump.py +0 -0
  89. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/__init__.py +0 -0
  90. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/echo-server.py +0 -0
  91. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_abnf.py +0 -0
  92. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_app.py +0 -0
  93. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_cookiejar.py +0 -0
  94. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_dispatcher.py +0 -0
  95. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_handshake_large_response.py +0 -0
  96. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_http.py +0 -0
  97. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_large_payloads.py +0 -0
  98. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_logging_helpers.py +0 -0
  99. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_reconnect_bad_fd.py +0 -0
  100. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_socket.py +0 -0
  101. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_socket_bugs.py +0 -0
  102. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_ssl_compat.py +0 -0
  103. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_ssl_edge_cases.py +0 -0
  104. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_url.py +0 -0
  105. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_utils.py +0 -0
  106. {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_websocket.py +0 -0
  107. {proxynt-2.0.61rc1 → proxynt-2.1.0}/constant/__init__.py +0 -0
  108. {proxynt-2.0.61rc1 → proxynt-2.1.0}/context/__init__.py +0 -0
  109. {proxynt-2.0.61rc1 → proxynt-2.1.0}/context/context_utils.py +0 -0
  110. {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/__init__.py +0 -0
  111. {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/client_config_entity.py +0 -0
  112. {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/message/__init__.py +0 -0
  113. {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/message/message_entity.py +0 -0
  114. {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/message/push_config_entity.py +0 -0
  115. {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/message/tcp_over_websocket_message.py +0 -0
  116. {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/server_config_entity.py +0 -0
  117. {proxynt-2.0.61rc1 → proxynt-2.1.0}/exceptions/__init__.py +0 -0
  118. {proxynt-2.0.61rc1 → proxynt-2.1.0}/exceptions/duplicated_name.py +0 -0
  119. {proxynt-2.0.61rc1 → proxynt-2.1.0}/exceptions/invalid_password.py +0 -0
  120. {proxynt-2.0.61rc1 → proxynt-2.1.0}/exceptions/replay_error.py +0 -0
  121. {proxynt-2.0.61rc1 → proxynt-2.1.0}/exceptions/signature_error.py +0 -0
  122. {proxynt-2.0.61rc1 → proxynt-2.1.0}/p2ptest/__init__.py +0 -0
  123. {proxynt-2.0.61rc1 → proxynt-2.1.0}/p2ptest/client.py +0 -0
  124. {proxynt-2.0.61rc1 → proxynt-2.1.0}/p2ptest/n4.py +0 -0
  125. {proxynt-2.0.61rc1 → proxynt-2.1.0}/proxynt.egg-info/dependency_links.txt +0 -0
  126. {proxynt-2.0.61rc1 → proxynt-2.1.0}/proxynt.egg-info/entry_points.txt +0 -0
  127. {proxynt-2.0.61rc1 → proxynt-2.1.0}/proxynt.egg-info/requires.txt +0 -0
  128. {proxynt-2.0.61rc1 → proxynt-2.1.0}/proxynt.egg-info/top_level.txt +0 -0
  129. {proxynt-2.0.61rc1 → proxynt-2.1.0}/run_server.py +0 -0
  130. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/__init__.py +0 -0
  131. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/admin_http_handler.py +0 -0
  132. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/n4.py +0 -0
  133. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/n4_signal_service.py +0 -0
  134. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/task/__init__.py +0 -0
  135. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/task/check_cookie_task.py +0 -0
  136. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/task/clear_nonce_task.py +0 -0
  137. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/task/heart_beat_task.py +0 -0
  138. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/__init__.py +0 -0
  139. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/base.html +0 -0
  140. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/css/fonts/element-icons.woff +0 -0
  141. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/css/index.css +0 -0
  142. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/ele_index.html +0 -0
  143. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/js/axios.min.js +0 -0
  144. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/js/index.js +0 -0
  145. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/js/vue.min.js +0 -0
  146. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/login.html +0 -0
  147. {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/udp_forward_client.py +0 -0
  148. {proxynt-2.0.61rc1 → proxynt-2.1.0}/setup.cfg +0 -0
  149. {proxynt-2.0.61rc1 → proxynt-2.1.0}/setup.py +0 -0
  150. {proxynt-2.0.61rc1 → proxynt-2.1.0}/test_exchange.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: proxynt
3
- Version: 2.0.61rc1
3
+ Version: 2.1.0
4
4
  Summary: UNKNOWN
5
5
  Home-page: https://github.com/sazima/proxynt
6
6
  License: UNKNOWN
@@ -9,7 +9,7 @@ from typing import Dict, List
9
9
 
10
10
  from common.logger_factory import LoggerFactory
11
11
  from common.nat_serialization import NatSerialization
12
- from common.pool import SelectPool
12
+ from common.pool import SelectPool, PAUSE_REASON_FLOW_CONTROL
13
13
  from common.register_append_data import ResisterAppendData
14
14
  from common.speed_limit import SpeedLimiter
15
15
  from constant.message_type_constnat import MessageTypeConstant
@@ -38,6 +38,7 @@ class TcpForwardClient:
38
38
  self.ws = ws_sender # WsSender — thread-safe, send via IOLoop.add_callback
39
39
  self.lock = Lock()
40
40
  self.socket_event_loop = SelectPool()
41
+ self.flow_control_enabled = False # 服务端是否支持按连接流控, 由 PUSH_CONFIG 协商
41
42
 
42
43
  # C2C client-to-client forward state
43
44
  self.c2c_rules: List[dict] = []
@@ -52,6 +53,10 @@ class TcpForwardClient:
52
53
  self.pending_data_lock = Lock()
53
54
  self.pending_data_timeout = 10
54
55
 
56
+ # 已关闭的 uid -> 关闭时间;服务端的关闭通知晚于本地关闭时,避免为旧 uid 重建后端连接
57
+ self.recently_closed: Dict[bytes, float] = {}
58
+ self.recently_closed_ttl = 60
59
+
55
60
  def set_running(self, running: bool):
56
61
  self.socket_event_loop.is_running = running
57
62
 
@@ -223,6 +228,9 @@ class TcpForwardClient:
223
228
  with self.lock:
224
229
  if uid in self.uid_to_socket_connection:
225
230
  return True
231
+ if uid in self.recently_closed:
232
+ LoggerFactory.get_logger().info('ignore data for closed uid: %s' % uid.hex())
233
+ return False
226
234
  try:
227
235
  if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
228
236
  LoggerFactory.get_logger().debug('create socket %s, %s' % (name, uid))
@@ -301,6 +309,7 @@ class TcpForwardClient:
301
309
  connection: PrivateSocketConnection = self.socket_to_socket_connection.pop(socket_client)
302
310
  LoggerFactory.get_logger().info('close uid: %s, sent=%d recv=%d, alive=%.3fs' % (
303
311
  connection.uid.hex(), connection.bytes_sent, connection.bytes_recv, time.time() - connection.create_time))
312
+ self._mark_closed(connection.uid)
304
313
  self.socket_event_loop.unregister(socket_client)
305
314
  try:
306
315
  socket_client.shutdown(socket.SHUT_RDWR)
@@ -312,6 +321,13 @@ class TcpForwardClient:
312
321
  self.uid_to_socket_connection.pop(connection.uid)
313
322
  self.close_remote_socket(connection)
314
323
 
324
+ def _mark_closed(self, uid: bytes):
325
+ now = time.time()
326
+ self.recently_closed[uid] = now
327
+ if len(self.recently_closed) > 1000:
328
+ for k in [k for k, t in self.recently_closed.items() if now - t > self.recently_closed_ttl]:
329
+ self.recently_closed.pop(k, None)
330
+
315
331
  def close(self):
316
332
  LoggerFactory.get_logger().info('Starting to close %s' % self.c2c_listeners)
317
333
  with self.lock:
@@ -367,6 +383,18 @@ class TcpForwardClient:
367
383
  )
368
384
  LoggerFactory.get_logger().debug('Send to websocket cost time %s' % (time.time() - start_time))
369
385
 
386
+ def set_local_reading_paused(self, uid: bytes, pause: bool):
387
+ """服务端通知该 uid 积压过多, 暂停/恢复读取本地连接"""
388
+ connection = self.uid_to_socket_connection.get(uid)
389
+ if not connection:
390
+ return
391
+ if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
392
+ LoggerFactory.get_logger().debug('flow control %s uid: %s' % ('pause' if pause else 'resume', uid.hex()))
393
+ if pause:
394
+ self.socket_event_loop.pause_reading(connection.socket, PAUSE_REASON_FLOW_CONTROL)
395
+ else:
396
+ self.socket_event_loop.resume_reading(connection.socket, PAUSE_REASON_FLOW_CONTROL)
397
+
370
398
  def send_by_uid(self, uid: bytes, msg: bytes):
371
399
  connection = self.uid_to_socket_connection.get(uid)
372
400
  if not connection:
@@ -74,6 +74,8 @@ class NatSerializationV1:
74
74
  magic, mode_flag, len(target_client), len(target_service), len(source_rule_name), len(protocol),
75
75
  uid, target_client, target_service, source_rule_name, protocol)
76
76
 
77
+ elif type_ in (MessageTypeConstant.FLOW_PAUSE, MessageTypeConstant.FLOW_RESUME):
78
+ body = struct.pack(f'{UID_LEN}s', data['data']['uid'])
77
79
  elif type_ == MessageTypeConstant.PUSH_CONFIG:
78
80
  body = json.dumps(data).encode()
79
81
  elif type_ == MessageTypeConstant.PING:
@@ -190,6 +192,13 @@ class NatSerializationV1:
190
192
  }
191
193
  return return_data
192
194
 
195
+ elif type_.decode() in (MessageTypeConstant.FLOW_PAUSE, MessageTypeConstant.FLOW_RESUME):
196
+ return_data: MessageEntity = {
197
+ 'type_': type_.decode(),
198
+ 'data': {'uid': body[:UID_LEN]}
199
+ }
200
+ return return_data
201
+
193
202
  elif type_ == MessageTypeConstant.PUSH_CONFIG.encode():
194
203
  return_data: MessageEntity = json.loads(body.decode())
195
204
  return return_data
@@ -4,6 +4,7 @@ import threading
4
4
  import time
5
5
  import traceback
6
6
  from selectors import DefaultSelector, EVENT_READ
7
+ from typing import Dict, Set
7
8
 
8
9
  from common.logger_factory import LoggerFactory
9
10
  from common.register_append_data import ResisterAppendData
@@ -14,6 +15,9 @@ from constant.system_constant import SystemConstant
14
15
  参考 shadowsocks eventloop 模式
15
16
  """
16
17
 
18
+ PAUSE_REASON_SPEED_LIMIT = 'speed_limit'
19
+ PAUSE_REASON_FLOW_CONTROL = 'flow_control'
20
+
17
21
 
18
22
  class SelectPool:
19
23
 
@@ -21,6 +25,27 @@ class SelectPool:
21
25
  self.is_running = True
22
26
  self.fileno_to_client = dict()
23
27
  self.selector = DefaultSelector()
28
+ # 其他线程 register 时唤醒阻塞中的 select,否则 Windows 上新 socket 要等本轮 select 超时才被监听
29
+ self._wakeup_r, self._wakeup_w = socket.socketpair()
30
+ self._wakeup_r.setblocking(False)
31
+ self._wakeup_w.setblocking(False)
32
+ self.selector.register(self._wakeup_r, EVENT_READ, None)
33
+ # 每个 socket 的暂停原因 (如 speed_limit / flow_control), 全部解除才恢复读取
34
+ self._pause_reasons: Dict[socket.socket, Set[str]] = dict()
35
+ self._pause_lock = threading.Lock()
36
+
37
+ def _wakeup(self):
38
+ try:
39
+ self._wakeup_w.send(b'\x00')
40
+ except OSError:
41
+ pass
42
+
43
+ def _drain_wakeup(self):
44
+ try:
45
+ while self._wakeup_r.recv(4096):
46
+ pass
47
+ except OSError:
48
+ pass
24
49
 
25
50
  def stop(self):
26
51
  self.is_running = False
@@ -31,6 +56,7 @@ class SelectPool:
31
56
  def register(self, s: socket.socket, data: ResisterAppendData):
32
57
  self.fileno_to_client[s.fileno()] = s
33
58
  self.selector.register(s, EVENT_READ, data)
59
+ self._wakeup()
34
60
 
35
61
  def unregister(self, s: socket.socket):
36
62
  fileno = -1
@@ -40,6 +66,8 @@ class SelectPool:
40
66
  pass
41
67
  if fileno in self.fileno_to_client:
42
68
  self.fileno_to_client.pop(fileno)
69
+ with self._pause_lock:
70
+ self._pause_reasons.pop(s, None)
43
71
  try:
44
72
  self.selector.unregister(s)
45
73
  except (KeyError, ValueError):
@@ -47,26 +75,43 @@ class SelectPool:
47
75
  except OSError:
48
76
  LoggerFactory.get_logger().error(traceback.format_exc())
49
77
 
50
- def pause_reading(self, s: socket.socket):
78
+ def pause_reading(self, s: socket.socket, reason: str = PAUSE_REASON_SPEED_LIMIT):
51
79
  """暂停监听可读事件 (modify 掩码为 0)"""
52
- try:
53
- key = self.selector.get_key(s)
54
- self.selector.modify(s, 0, key.data)
55
- except (KeyError, ValueError):
56
- pass
80
+ with self._pause_lock:
81
+ try:
82
+ key = self.selector.get_key(s)
83
+ except (KeyError, ValueError):
84
+ return
85
+ reasons = self._pause_reasons.setdefault(s, set())
86
+ first_pause = not reasons
87
+ reasons.add(reason)
88
+ if first_pause:
89
+ try:
90
+ self.selector.modify(s, 0, key.data)
91
+ except (KeyError, ValueError):
92
+ pass
57
93
 
58
- def resume_reading(self, s: socket.socket):
59
- """恢复监听可读事件"""
60
- try:
61
- key = self.selector.get_key(s)
62
- self.selector.modify(s, EVENT_READ, key.data)
63
- except (KeyError, ValueError):
64
- pass
94
+ def resume_reading(self, s: socket.socket, reason: str = PAUSE_REASON_SPEED_LIMIT):
95
+ """解除一个暂停原因, 没有其他原因时恢复监听可读事件"""
96
+ with self._pause_lock:
97
+ reasons = self._pause_reasons.get(s)
98
+ if not reasons or reason not in reasons:
99
+ return
100
+ reasons.discard(reason)
101
+ if reasons:
102
+ return
103
+ self._pause_reasons.pop(s, None)
104
+ try:
105
+ key = self.selector.get_key(s)
106
+ self.selector.modify(s, EVENT_READ, key.data)
107
+ except (KeyError, ValueError):
108
+ return
109
+ self._wakeup()
65
110
 
66
111
  def pause_and_resume_later(self, s: socket.socket, delay_time: float):
67
112
  """暂停可读监听, delay_time 秒后恢复"""
68
- self.pause_reading(s)
69
- threading.Timer(delay_time, self.resume_reading, args=(s,)).start()
113
+ self.pause_reading(s, PAUSE_REASON_SPEED_LIMIT)
114
+ threading.Timer(delay_time, self.resume_reading, args=(s, PAUSE_REASON_SPEED_LIMIT)).start()
70
115
 
71
116
  def run(self):
72
117
  while True:
@@ -80,6 +125,9 @@ class SelectPool:
80
125
  time.sleep(0.5)
81
126
  continue
82
127
  for key, mask in ready:
128
+ if key.data is None:
129
+ self._drain_wakeup()
130
+ continue
83
131
  fileno = key.fd
84
132
  client = self.fileno_to_client.get(fileno)
85
133
  if client is None:
@@ -28,4 +28,9 @@ 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
+ # 按连接流控: 接收方某个 uid 积压过多时通知发送方暂停/恢复读取该 uid 的数据源
34
+ # 仅在双方通过 PUSH_CONFIG 协商支持后发送 (旧版本反序列化未知类型会报错)
35
+ FLOW_PAUSE = 'k'
36
+ FLOW_RESUME = 'l'
@@ -15,7 +15,7 @@ class SystemConstant:
15
15
 
16
16
  COOKIE_EXPIRE_SECONDS = 3600 * 24
17
17
 
18
- VERSION = '2.0.61rc1'
18
+ VERSION = '2.1.0'
19
19
 
20
20
  GITHUB = 'https://github.com/sazima/proxynt'
21
21
 
@@ -0,0 +1,66 @@
1
+ """
2
+ 测试后端 TLS 握手超时:发送 ClientHello,收完服务端握手数据后故意不回 Finished,
3
+ 测量后端多久主动断开。
4
+
5
+ 用法: python tls_handshake_timeout_test.py 10.253.68.143 31015 [最长等待秒数]
6
+ """
7
+ import socket
8
+ import ssl
9
+ import sys
10
+ import time
11
+
12
+
13
+ def main():
14
+ host = sys.argv[1]
15
+ port = int(sys.argv[2])
16
+ max_wait = float(sys.argv[3]) if len(sys.argv) > 3 else 30
17
+
18
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
19
+ ctx.check_hostname = False
20
+ ctx.verify_mode = ssl.CERT_NONE
21
+
22
+ incoming = ssl.MemoryBIO()
23
+ outgoing = ssl.MemoryBIO()
24
+ tls = ctx.wrap_bio(incoming, outgoing)
25
+
26
+ try:
27
+ tls.do_handshake()
28
+ except ssl.SSLWantReadError:
29
+ pass
30
+ client_hello = outgoing.read()
31
+
32
+ s = socket.create_connection((host, port), timeout=5)
33
+ s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
34
+ t0 = time.time()
35
+ s.sendall(client_hello)
36
+ print(f'[{0:.3f}s] connected, sent ClientHello {len(client_hello)} bytes')
37
+
38
+ s.settimeout(0.2)
39
+ total = 0
40
+ last_data = None
41
+ while True:
42
+ elapsed = time.time() - t0
43
+ if elapsed > max_wait:
44
+ print(f'[{elapsed:.3f}s] backend still open after {max_wait}s, giving up (no short handshake timeout)')
45
+ break
46
+ try:
47
+ data = s.recv(65536)
48
+ except socket.timeout:
49
+ continue
50
+ except OSError as e:
51
+ print(f'[{elapsed:.3f}s] recv error: {type(e).__name__} {e} (total recv {total} bytes, '
52
+ f'last data at {last_data:.3f}s)')
53
+ break
54
+ if not data:
55
+ print(f'[{elapsed:.3f}s] backend closed (EOF) (total recv {total} bytes, '
56
+ f'last data at {last_data:.3f}s)')
57
+ break
58
+ total += len(data)
59
+ last_data = elapsed
60
+ print(f'[{elapsed:.3f}s] recv {len(data)} bytes (total {total}), not replying...')
61
+
62
+ s.close()
63
+
64
+
65
+ if __name__ == '__main__':
66
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: proxynt
3
- Version: 2.0.61rc1
3
+ Version: 2.1.0
4
4
  Summary: UNKNOWN
5
5
  Home-page: https://github.com/sazima/proxynt
6
6
  License: UNKNOWN
@@ -119,6 +119,7 @@ setup.py
119
119
  ./p2ptest/__init__.py
120
120
  ./p2ptest/client.py
121
121
  ./p2ptest/n4.py
122
+ ./p2ptest/tls_handshake_timeout_test.py
122
123
  ./server/__init__.py
123
124
  ./server/admin_http_handler.py
124
125
  ./server/n4.py
@@ -79,6 +79,7 @@ class WsSender:
79
79
  """Thread-safe WebSocket sender wrapping Tornado WebSocketClientConnection."""
80
80
 
81
81
  MAX_PENDING_WRITES = 32
82
+ WRITE_BACKPRESSURE_TIMEOUT = 10
82
83
 
83
84
  def __init__(self):
84
85
  self._ws = None
@@ -104,24 +105,30 @@ class WsSender:
104
105
  io.add_callback(ws.write_message, data, binary=True)
105
106
  return
106
107
 
107
- acquired = self._write_sem.acquire(timeout=10)
108
- if not acquired or self._ws is None:
108
+ write_sem = self._write_sem
109
+ acquired = write_sem.acquire(timeout=self.WRITE_BACKPRESSURE_TIMEOUT)
110
+ if self._ws is None:
111
+ if acquired:
112
+ write_sem.release()
109
113
  return
110
- io.add_callback(self._do_write, data)
114
+ if not acquired:
115
+ # 不能丢弃: TCP 流中间少一块数据会导致对端协议错乱(如 sshd Bad packet length)
116
+ LoggerFactory.get_logger().warning(f'websocket write backlog over {self.WRITE_BACKPRESSURE_TIMEOUT}s, send without backpressure')
117
+ io.add_callback(self._do_write, data, write_sem if acquired else None)
111
118
 
112
- def _do_write(self, data):
119
+ def _do_write(self, data, write_sem):
113
120
  ws = self._ws
114
121
  if ws is None:
115
- self._write_sem.release()
122
+ if write_sem:
123
+ write_sem.release()
116
124
  return
117
125
  try:
118
126
  future = ws.write_message(data, binary=True)
119
- ioloop.IOLoop.current().add_future(future, lambda f: self._on_write_done())
127
+ if write_sem:
128
+ ioloop.IOLoop.current().add_future(future, lambda f: write_sem.release())
120
129
  except Exception:
121
- self._write_sem.release()
122
-
123
- def _on_write_done(self):
124
- self._write_sem.release()
130
+ if write_sem:
131
+ write_sem.release()
125
132
 
126
133
  def close(self):
127
134
  ws = self._ws
@@ -295,6 +302,7 @@ class WebsocketClient:
295
302
  push_config: PushConfigEntity = message_data['data']
296
303
  self.public_ip = push_config.get('public_ip')
297
304
  self.public_port = push_config.get('public_port')
305
+ self.forward_client.flow_control_enabled = bool(push_config.get('server_flow_control', False))
298
306
 
299
307
  for d in push_config['config_list']:
300
308
  if d.get('speed_limit'):
@@ -306,6 +314,11 @@ class WebsocketClient:
306
314
  self.forward_client.setup_c2c_tcp_listeners(c2c_rules)
307
315
  self.udp_forward_client.setup_c2c_udp_listeners(c2c_rules)
308
316
 
317
+ elif msg_type == MessageTypeConstant.FLOW_PAUSE:
318
+ self.forward_client.set_local_reading_paused(message_data['data']['uid'], True)
319
+ elif msg_type == MessageTypeConstant.FLOW_RESUME:
320
+ self.forward_client.set_local_reading_paused(message_data['data']['uid'], False)
321
+
309
322
  # P2P punch disabled
310
323
  elif msg_type == MessageTypeConstant.P2P_PUNCH_REQUEST:
311
324
  LoggerFactory.get_logger().info('P2P punch disabled, ignoring request')
@@ -337,7 +350,8 @@ class WebsocketClient:
337
350
  'config_list': push_client_data,
338
351
  "client_name": client_name,
339
352
  'version': SystemConstant.VERSION,
340
- 'p2p_supported': False
353
+ 'p2p_supported': False,
354
+ 'flow_control': True
341
355
  }
342
356
  message: MessageEntity = {
343
357
  'type_': MessageTypeConstant.PUSH_CONFIG,