proxynt 2.0.61rc2__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.61rc2 → proxynt-2.1.0}/PKG-INFO +1 -1
  2. {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/tcp_forward_client.py +14 -1
  3. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/nat_serialization_v1.py +9 -0
  4. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/pool.py +41 -16
  5. {proxynt-2.0.61rc2 → proxynt-2.1.0}/constant/message_type_constnat.py +6 -1
  6. {proxynt-2.0.61rc2 → proxynt-2.1.0}/constant/system_constant.py +1 -1
  7. {proxynt-2.0.61rc2 → proxynt-2.1.0}/proxynt.egg-info/PKG-INFO +1 -1
  8. {proxynt-2.0.61rc2 → proxynt-2.1.0}/run_client.py +25 -11
  9. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/tcp_forward_client.py +172 -58
  10. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/websocket_handler.py +99 -60
  11. {proxynt-2.0.61rc2 → proxynt-2.1.0}/LICENSE +0 -0
  12. {proxynt-2.0.61rc2 → proxynt-2.1.0}/MANIFEST.in +0 -0
  13. {proxynt-2.0.61rc2 → proxynt-2.1.0}/__init__.py +0 -0
  14. {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/__init__.py +0 -0
  15. {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/abstract_tunnel.py +0 -0
  16. {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/clear_nonce_task.py +0 -0
  17. {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/heart_beat_task.py +0 -0
  18. {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/kcp_tunnel_impl.py +0 -0
  19. {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/n4_tunnel_manager.py +0 -0
  20. {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/quic_tunnel_impl.py +0 -0
  21. {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/tunnel_protocol.py +0 -0
  22. {proxynt-2.0.61rc2 → proxynt-2.1.0}/client/udp_forward_client.py +0 -0
  23. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/__init__.py +0 -0
  24. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/cert_utils.py +0 -0
  25. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/crypto/__init__.py +0 -0
  26. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/crypto/table.py +0 -0
  27. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/encrypt_utils.py +0 -0
  28. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/kcp.py +0 -0
  29. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/logger_factory.py +0 -0
  30. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/n4_protocol.py +0 -0
  31. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/n4_punch.py +0 -0
  32. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/nat_serialization.py +0 -0
  33. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/nat_serialization_v2.py +0 -0
  34. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/register_append_data.py +0 -0
  35. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/speed_limit.py +0 -0
  36. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/__init__.py +0 -0
  37. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_abnf.py +0 -0
  38. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_app.py +0 -0
  39. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_cookiejar.py +0 -0
  40. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_core.py +0 -0
  41. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_dispatcher.py +0 -0
  42. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_exceptions.py +0 -0
  43. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_handshake.py +0 -0
  44. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_http.py +0 -0
  45. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_logging.py +0 -0
  46. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_socket.py +0 -0
  47. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_ssl_compat.py +0 -0
  48. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_url.py +0 -0
  49. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_utils.py +0 -0
  50. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/_wsdump.py +0 -0
  51. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/__init__.py +0 -0
  52. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/echo-server.py +0 -0
  53. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_abnf.py +0 -0
  54. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_app.py +0 -0
  55. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_cookiejar.py +0 -0
  56. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_dispatcher.py +0 -0
  57. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_handshake_large_response.py +0 -0
  58. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_http.py +0 -0
  59. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_large_payloads.py +0 -0
  60. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_logging_helpers.py +0 -0
  61. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_reconnect_bad_fd.py +0 -0
  62. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_socket.py +0 -0
  63. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_socket_bugs.py +0 -0
  64. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_ssl_compat.py +0 -0
  65. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_ssl_edge_cases.py +0 -0
  66. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_url.py +0 -0
  67. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_utils.py +0 -0
  68. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket/tests/test_websocket.py +0 -0
  69. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/__init__.py +0 -0
  70. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/compliance/autobahn-test-report-Feb-03-2021.html +0 -0
  71. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/setup.py +0 -0
  72. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/__init__.py +0 -0
  73. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_abnf.py +0 -0
  74. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_app.py +0 -0
  75. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_cookiejar.py +0 -0
  76. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_core.py +0 -0
  77. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_dispatcher.py +0 -0
  78. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_exceptions.py +0 -0
  79. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_handshake.py +0 -0
  80. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_http.py +0 -0
  81. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_logging.py +0 -0
  82. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_socket.py +0 -0
  83. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_ssl_compat.py +0 -0
  84. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_url.py +0 -0
  85. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_utils.py +0 -0
  86. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/_wsdump.py +0 -0
  87. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/__init__.py +0 -0
  88. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/echo-server.py +0 -0
  89. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_abnf.py +0 -0
  90. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_app.py +0 -0
  91. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_cookiejar.py +0 -0
  92. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_dispatcher.py +0 -0
  93. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_handshake_large_response.py +0 -0
  94. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_http.py +0 -0
  95. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_large_payloads.py +0 -0
  96. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_logging_helpers.py +0 -0
  97. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_reconnect_bad_fd.py +0 -0
  98. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_socket.py +0 -0
  99. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_socket_bugs.py +0 -0
  100. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_ssl_compat.py +0 -0
  101. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_ssl_edge_cases.py +0 -0
  102. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_url.py +0 -0
  103. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_utils.py +0 -0
  104. {proxynt-2.0.61rc2 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_websocket.py +0 -0
  105. {proxynt-2.0.61rc2 → proxynt-2.1.0}/constant/__init__.py +0 -0
  106. {proxynt-2.0.61rc2 → proxynt-2.1.0}/context/__init__.py +0 -0
  107. {proxynt-2.0.61rc2 → proxynt-2.1.0}/context/context_utils.py +0 -0
  108. {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/__init__.py +0 -0
  109. {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/client_config_entity.py +0 -0
  110. {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/message/__init__.py +0 -0
  111. {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/message/message_entity.py +0 -0
  112. {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/message/push_config_entity.py +0 -0
  113. {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/message/tcp_over_websocket_message.py +0 -0
  114. {proxynt-2.0.61rc2 → proxynt-2.1.0}/entity/server_config_entity.py +0 -0
  115. {proxynt-2.0.61rc2 → proxynt-2.1.0}/exceptions/__init__.py +0 -0
  116. {proxynt-2.0.61rc2 → proxynt-2.1.0}/exceptions/duplicated_name.py +0 -0
  117. {proxynt-2.0.61rc2 → proxynt-2.1.0}/exceptions/invalid_password.py +0 -0
  118. {proxynt-2.0.61rc2 → proxynt-2.1.0}/exceptions/replay_error.py +0 -0
  119. {proxynt-2.0.61rc2 → proxynt-2.1.0}/exceptions/signature_error.py +0 -0
  120. {proxynt-2.0.61rc2 → proxynt-2.1.0}/p2ptest/__init__.py +0 -0
  121. {proxynt-2.0.61rc2 → proxynt-2.1.0}/p2ptest/client.py +0 -0
  122. {proxynt-2.0.61rc2 → proxynt-2.1.0}/p2ptest/n4.py +0 -0
  123. {proxynt-2.0.61rc2 → proxynt-2.1.0}/p2ptest/tls_handshake_timeout_test.py +0 -0
  124. {proxynt-2.0.61rc2 → proxynt-2.1.0}/proxynt.egg-info/SOURCES.txt +0 -0
  125. {proxynt-2.0.61rc2 → proxynt-2.1.0}/proxynt.egg-info/dependency_links.txt +0 -0
  126. {proxynt-2.0.61rc2 → proxynt-2.1.0}/proxynt.egg-info/entry_points.txt +0 -0
  127. {proxynt-2.0.61rc2 → proxynt-2.1.0}/proxynt.egg-info/requires.txt +0 -0
  128. {proxynt-2.0.61rc2 → proxynt-2.1.0}/proxynt.egg-info/top_level.txt +0 -0
  129. {proxynt-2.0.61rc2 → proxynt-2.1.0}/run_server.py +0 -0
  130. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/__init__.py +0 -0
  131. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/admin_http_handler.py +0 -0
  132. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/n4.py +0 -0
  133. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/n4_signal_service.py +0 -0
  134. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/task/__init__.py +0 -0
  135. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/task/check_cookie_task.py +0 -0
  136. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/task/clear_nonce_task.py +0 -0
  137. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/task/heart_beat_task.py +0 -0
  138. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/__init__.py +0 -0
  139. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/base.html +0 -0
  140. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/css/fonts/element-icons.woff +0 -0
  141. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/css/index.css +0 -0
  142. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/ele_index.html +0 -0
  143. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/js/axios.min.js +0 -0
  144. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/js/index.js +0 -0
  145. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/js/vue.min.js +0 -0
  146. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/template/login.html +0 -0
  147. {proxynt-2.0.61rc2 → proxynt-2.1.0}/server/udp_forward_client.py +0 -0
  148. {proxynt-2.0.61rc2 → proxynt-2.1.0}/setup.cfg +0 -0
  149. {proxynt-2.0.61rc2 → proxynt-2.1.0}/setup.py +0 -0
  150. {proxynt-2.0.61rc2 → 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.61rc2
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] = []
@@ -382,6 +383,18 @@ class TcpForwardClient:
382
383
  )
383
384
  LoggerFactory.get_logger().debug('Send to websocket cost time %s' % (time.time() - start_time))
384
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
+
385
398
  def send_by_uid(self, uid: bytes, msg: bytes):
386
399
  connection = self.uid_to_socket_connection.get(uid)
387
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
 
@@ -26,6 +30,9 @@ class SelectPool:
26
30
  self._wakeup_r.setblocking(False)
27
31
  self._wakeup_w.setblocking(False)
28
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()
29
36
 
30
37
  def _wakeup(self):
31
38
  try:
@@ -59,6 +66,8 @@ class SelectPool:
59
66
  pass
60
67
  if fileno in self.fileno_to_client:
61
68
  self.fileno_to_client.pop(fileno)
69
+ with self._pause_lock:
70
+ self._pause_reasons.pop(s, None)
62
71
  try:
63
72
  self.selector.unregister(s)
64
73
  except (KeyError, ValueError):
@@ -66,27 +75,43 @@ class SelectPool:
66
75
  except OSError:
67
76
  LoggerFactory.get_logger().error(traceback.format_exc())
68
77
 
69
- def pause_reading(self, s: socket.socket):
78
+ def pause_reading(self, s: socket.socket, reason: str = PAUSE_REASON_SPEED_LIMIT):
70
79
  """暂停监听可读事件 (modify 掩码为 0)"""
71
- try:
72
- key = self.selector.get_key(s)
73
- self.selector.modify(s, 0, key.data)
74
- except (KeyError, ValueError):
75
- 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
76
93
 
77
- def resume_reading(self, s: socket.socket):
78
- """恢复监听可读事件"""
79
- try:
80
- key = self.selector.get_key(s)
81
- self.selector.modify(s, EVENT_READ, key.data)
82
- self._wakeup()
83
- except (KeyError, ValueError):
84
- 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()
85
110
 
86
111
  def pause_and_resume_later(self, s: socket.socket, delay_time: float):
87
112
  """暂停可读监听, delay_time 秒后恢复"""
88
- self.pause_reading(s)
89
- 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()
90
115
 
91
116
  def run(self):
92
117
  while True:
@@ -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.61rc2'
18
+ VERSION = '2.1.0'
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.61rc2
3
+ Version: 2.1.0
4
4
  Summary: UNKNOWN
5
5
  Home-page: https://github.com/sazima/proxynt
6
6
  License: UNKNOWN
@@ -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,
@@ -8,14 +8,15 @@ from collections import defaultdict
8
8
  from functools import partial
9
9
  from threading import Thread, Lock
10
10
  from asyncio import Lock as AsyncioLock
11
- from typing import Dict, Tuple, List, Set
11
+ from collections import deque
12
+ from typing import Dict, Tuple, List, Set, Optional
12
13
  import os
13
14
 
14
15
  import tornado
15
16
 
16
17
  from common.logger_factory import LoggerFactory
17
18
  from common.nat_serialization import NatSerialization
18
- from common.pool import SelectPool
19
+ from common.pool import SelectPool, PAUSE_REASON_FLOW_CONTROL
19
20
  from common.register_append_data import ResisterAppendData
20
21
  from common.speed_limit import SpeedLimiter
21
22
  from constant.message_type_constnat import MessageTypeConstant
@@ -56,8 +57,8 @@ class PublicSocketConnection:
56
57
  # Optimistic send mode configuration
57
58
  self.optimistic_mode = True # Enable optimistic send by default
58
59
  self.connection_confirmed = False # Whether connection is confirmed
59
- self.connect_requested = False # Whether REQUEST_TO_CONNECT has been sent
60
60
  self.early_data_buffer = [] # Buffer early data (data received before connection confirmation)
61
+ self.early_data_lock = Lock() # socket 线程缓冲与 tornado 线程 flush 互斥, 防止数据乱序/丢失
61
62
  self.max_early_data_packets = 65536 * 4 # Maximum buffered packets
62
63
  self.bytes_from_public: int = 0
63
64
  self.bytes_to_public: int = 0
@@ -71,6 +72,70 @@ class PublicSocketConnection:
71
72
  return f'{self.uid}_{self.socket}_{self.socket_server.name}'
72
73
 
73
74
 
75
+ SEND_QUEUE_HIGH_WATER = 4 * 1024 * 1024 # 单个 uid 积压超过该值时对来源 websocket 做背压
76
+ SEND_QUEUE_LOW_WATER = 1 * 1024 * 1024
77
+ # 客户端支持流控时: 超过 FLOW_HIGH_WATER 通知客户端暂停该 uid, 低于 FLOW_LOW_WATER 恢复
78
+ # 超过 FLOW_HARD_LIMIT 仍退回 websocket 级背压, 防止对端不响应时内存失控
79
+ FLOW_HIGH_WATER = 1 * 1024 * 1024
80
+ FLOW_LOW_WATER = 256 * 1024
81
+ FLOW_HARD_LIMIT = 8 * 1024 * 1024
82
+
83
+
84
+ class UidSendQueue:
85
+ """单个 uid 的有序发送队列, 只在 tornado 线程中使用"""
86
+
87
+ def __init__(self):
88
+ self._items = deque()
89
+ self._event = asyncio.Event()
90
+ self._drain_waiters: List[asyncio.Future] = []
91
+ self.size = 0
92
+ self.closed = False
93
+ self.remote_paused = False # 是否已通知客户端暂停该 uid
94
+
95
+ def put(self, message: bytes):
96
+ if self.closed:
97
+ return
98
+ self._items.append(message)
99
+ self.size += len(message)
100
+ self._event.set()
101
+
102
+ async def get(self) -> Optional[bytes]:
103
+ """返回下一条数据, 队列关闭时返回 None"""
104
+ while not self._items:
105
+ if self.closed:
106
+ return None
107
+ self._event.clear()
108
+ await self._event.wait()
109
+ if self.closed:
110
+ return None
111
+ return self._items.popleft()
112
+
113
+ def task_done(self, length: int):
114
+ self.size -= length
115
+ if self.size <= SEND_QUEUE_LOW_WATER:
116
+ self._wake_drain_waiters()
117
+
118
+ def wait_drained(self) -> asyncio.Future:
119
+ future = asyncio.get_event_loop().create_future()
120
+ self._drain_waiters.append(future)
121
+ return future
122
+
123
+ def close(self):
124
+ if self.closed:
125
+ return
126
+ self.closed = True
127
+ self._items.clear()
128
+ self.size = 0
129
+ self._event.set()
130
+ self._wake_drain_waiters()
131
+
132
+ def _wake_drain_waiters(self):
133
+ waiters, self._drain_waiters = self._drain_waiters, []
134
+ for future in waiters:
135
+ if not future.done():
136
+ future.set_result(None)
137
+
138
+
74
139
  class TcpForwardClient:
75
140
  _instance = None
76
141
 
@@ -83,7 +148,7 @@ class TcpForwardClient:
83
148
  self.tornado_loop = tornado_loop
84
149
  self.close_lock = Lock()
85
150
  self.client_name_to_lock: Dict[str, AsyncioLock] = dict()
86
- self.uid_to_send_lock: Dict[bytes, AsyncioLock] = dict() # 防止data/close并发竞态
151
+ self.uid_to_send_queue: Dict[bytes, 'UidSendQueue'] = dict() # 每个 uid 一个有序发送队列
87
152
 
88
153
  @classmethod
89
154
  def get_instance(cls) -> 'TcpForwardClient':
@@ -138,6 +203,8 @@ class TcpForwardClient:
138
203
  recv_error = None
139
204
  try:
140
205
  recv = each.recv(data.read_size)
206
+ except (BlockingIOError, InterruptedError):
207
+ return # 非阻塞 socket 的伪就绪, 等下次可读
141
208
  except ConnectionResetError as e:
142
209
  recv = b''
143
210
  recv_error = e
@@ -153,30 +220,29 @@ class TcpForwardClient:
153
220
 
154
221
  # Optimistic send mode: buffer data if connection not confirmed
155
222
  if socket_connection.optimistic_mode and not socket_connection.connection_confirmed:
156
- if not recv:
157
- # Client closed connection before confirmation, close immediately
158
- LoggerFactory.get_logger().info(f'recv empty before confirmed, close uid: {socket_connection.uid}')
159
- try:
160
- self.close_connection(socket_connection)
161
- except (OSError, ValueError, KeyError):
162
- LoggerFactory.get_logger().error(f'Close error: {traceback.format_exc()}')
163
- return
164
- if len(socket_connection.early_data_buffer) < socket_connection.max_early_data_packets:
165
- socket_connection.early_data_buffer.append(recv)
166
- if not socket_connection.connect_requested:
167
- # 第一块数据到达后再发送 REQUEST_TO_CONNECT,确保缓冲区已有数据
168
- socket_connection.connect_requested = True
169
- Thread(target=self.request_to_connect, args=(socket_connection,)).start()
170
- if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
171
- LoggerFactory.get_logger().debug(f'Buffering early data uid: {socket_connection.uid}, len: {len(recv)}, buffer_size: {len(socket_connection.early_data_buffer)}')
172
- else:
173
- # Too much buffered data, connection may have failed, close connection
174
- LoggerFactory.get_logger().warning(f'Too much early data buffered, closing connection uid: {socket_connection.uid}')
175
- try:
176
- self.close_connection(socket_connection)
177
- except (OSError, ValueError, KeyError):
178
- LoggerFactory.get_logger().error(f'Close error: {traceback.format_exc()}')
179
- return
223
+ with socket_connection.early_data_lock:
224
+ # 加锁后再判断一次, 避免与 CONNECT_CONFIRMED 的 flush 竞争
225
+ if not socket_connection.connection_confirmed:
226
+ if not recv:
227
+ # Client closed connection before confirmation, close immediately
228
+ LoggerFactory.get_logger().info(f'recv empty before confirmed, close uid: {socket_connection.uid}')
229
+ try:
230
+ self.close_connection(socket_connection)
231
+ except (OSError, ValueError, KeyError):
232
+ LoggerFactory.get_logger().error(f'Close error: {traceback.format_exc()}')
233
+ return
234
+ if len(socket_connection.early_data_buffer) < socket_connection.max_early_data_packets:
235
+ socket_connection.early_data_buffer.append(recv)
236
+ if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
237
+ LoggerFactory.get_logger().debug(f'Buffering early data uid: {socket_connection.uid}, len: {len(recv)}, buffer_size: {len(socket_connection.early_data_buffer)}')
238
+ else:
239
+ # Too much buffered data, connection may have failed, close connection
240
+ LoggerFactory.get_logger().warning(f'Too much early data buffered, closing connection uid: {socket_connection.uid}')
241
+ try:
242
+ self.close_connection(socket_connection)
243
+ except (OSError, ValueError, KeyError):
244
+ LoggerFactory.get_logger().error(f'Close error: {traceback.format_exc()}')
245
+ return
180
246
 
181
247
  # --- 发送端限速:暂停读取,延迟恢复 ---
182
248
  if data.speed_limiter and recv:
@@ -216,6 +282,8 @@ class TcpForwardClient:
216
282
  client: socket.socket
217
283
  # 启用 TCP_NODELAY 减少延迟(禁用 Nagle 算法)
218
284
  client.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
285
+ # 必须非阻塞: loop.sock_sendall 遇到阻塞 socket 会直接阻塞 send, 一个慢连接会卡住整个事件循环
286
+ client.setblocking(False)
219
287
  except OSError:
220
288
  return
221
289
  server = self.listen_socket_to_public_server[server_socket]
@@ -225,6 +293,9 @@ class TcpForwardClient:
225
293
  client_socket_connection = PublicSocketConnection(uid, client, server)
226
294
  self.uid_to_connection[uid] = client_socket_connection
227
295
  self.socket_to_connection[client] = client_socket_connection
296
+ # accept 后立即请求客户端建立连接, 不能等公网端先发数据: MySQL/VNC/FTP/SMTP 等由服务端先发数据的协议会卡住
297
+ # 早期数据与 CONNECT_CONFIRMED 的竞争由 early_data_lock 保证
298
+ self.request_to_connect(client_socket_connection)
228
299
  append_data = ResisterAppendData(self.handle_message, register_append_data.speed_limiter)
229
300
  self.socket_event_loop.register(client, append_data)
230
301
 
@@ -244,34 +315,72 @@ class TcpForwardClient:
244
315
  partial(handler.write_message, NatSerialization.dumps(send_message, ContextUtils.get_password(), handler.compress_support, handler.protocol_version)), True
245
316
  )
246
317
 
247
- async def send_to_socket(self, uid: bytes, message: bytes):
248
- send_start_time = time.time()
249
- if uid not in self.uid_to_connection:
250
- LoggerFactory.get_logger().debug(f'{message}, {uid} not in ')
251
- return
252
- lock = self.uid_to_send_lock.setdefault(uid, AsyncioLock())
253
- async with lock:
254
- connection = self.uid_to_connection.get(uid)
255
- if not connection:
256
- return
257
- socket_client = connection.socket
258
- if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
259
- LoggerFactory.get_logger().debug(f'send to socket uid: {uid}, len: {len(message)}')
260
- try:
261
- await asyncio.wait_for(asyncio.get_event_loop().sock_sendall(socket_client, message), timeout=30)
262
- except asyncio.TimeoutError:
263
- LoggerFactory.get_logger().warn(f"Socket send timeout for {uid.hex()}, closing connection")
264
- asyncio.ensure_future(self.close_connection_async(connection, 'send timeout'))
265
- return
266
- except OSError as e:
267
- LoggerFactory.get_logger().warn(f'{uid.hex()} os error: {e}')
268
- asyncio.ensure_future(self.close_connection_async(connection, f'send os error: {e}'))
269
- return
270
- connection.bytes_to_public += len(message)
271
- if not message:
272
- asyncio.ensure_future(self.close_connection_async(connection, 'client reported inner close'))
273
- if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
274
- LoggerFactory.get_logger().debug(f'send to socket cost time {time.time() - send_start_time}')
318
+ def enqueue_send(self, uid: bytes, message: bytes) -> Optional[asyncio.Future]:
319
+ """
320
+ 按 uid 入队发送, 立即返回, 保证同一 uid 内的数据顺序, 不同 uid 之间互不阻塞
321
+ 队列积压超过高水位时返回一个 future, 调用方 await 它做背压; 否则返回 None
322
+ """
323
+ connection = self.uid_to_connection.get(uid)
324
+ if not connection:
325
+ LoggerFactory.get_logger().debug(f'{uid.hex()} not in uid_to_connection, drop {len(message)} bytes')
326
+ return None
327
+ send_queue = self.uid_to_send_queue.get(uid)
328
+ if send_queue is None:
329
+ send_queue = UidSendQueue()
330
+ self.uid_to_send_queue[uid] = send_queue
331
+ asyncio.ensure_future(self._send_loop(connection, send_queue))
332
+ send_queue.put(message)
333
+ handler = connection.socket_server.websocket_handler
334
+ if handler.flow_control_support:
335
+ if send_queue.size > FLOW_HIGH_WATER and not send_queue.remote_paused:
336
+ send_queue.remote_paused = True
337
+ handler.send_flow_control(uid, True)
338
+ high_water = FLOW_HARD_LIMIT
339
+ else:
340
+ high_water = SEND_QUEUE_HIGH_WATER
341
+ if send_queue.size > high_water:
342
+ return send_queue.wait_drained()
343
+ return None
344
+
345
+ async def _send_loop(self, connection: PublicSocketConnection, send_queue: 'UidSendQueue'):
346
+ uid = connection.uid
347
+ loop = asyncio.get_event_loop()
348
+ try:
349
+ while True:
350
+ message = await send_queue.get()
351
+ if message is None or self.uid_to_connection.get(uid) is not connection:
352
+ return
353
+ if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
354
+ LoggerFactory.get_logger().debug(f'send to socket uid: {uid.hex()}, len: {len(message)}')
355
+ try:
356
+ await asyncio.wait_for(loop.sock_sendall(connection.socket, message), timeout=30)
357
+ except asyncio.TimeoutError:
358
+ LoggerFactory.get_logger().warn(f"Socket send timeout for {uid.hex()}, closing connection")
359
+ await self.close_connection_async(connection, 'send timeout')
360
+ return
361
+ except OSError as e:
362
+ LoggerFactory.get_logger().warn(f'{uid.hex()} os error: {e}')
363
+ await self.close_connection_async(connection, f'send os error: {e}')
364
+ return
365
+ connection.bytes_to_public += len(message)
366
+ send_queue.task_done(len(message))
367
+ if send_queue.remote_paused and send_queue.size <= FLOW_LOW_WATER:
368
+ send_queue.remote_paused = False
369
+ connection.socket_server.websocket_handler.send_flow_control(uid, False)
370
+ if not message:
371
+ await self.close_connection_async(connection, 'client reported inner close')
372
+ return
373
+ finally:
374
+ send_queue.close()
375
+ if self.uid_to_send_queue.get(uid) is send_queue:
376
+ self.uid_to_send_queue.pop(uid, None)
377
+
378
+ def set_public_reading_paused(self, connection: PublicSocketConnection, pause: bool):
379
+ """客户端通知暂停/恢复读取公网连接"""
380
+ if pause:
381
+ self.socket_event_loop.pause_reading(connection.socket, PAUSE_REASON_FLOW_CONTROL)
382
+ else:
383
+ self.socket_event_loop.resume_reading(connection.socket, PAUSE_REASON_FLOW_CONTROL)
275
384
 
276
385
  async def close_connection_async(self, connection: PublicSocketConnection, reason: str = ''):
277
386
  """Asynchronously close connection to avoid blocking in event loop"""
@@ -284,7 +393,9 @@ class TcpForwardClient:
284
393
  # Remove from tracking dictionaries
285
394
  self.uid_to_connection.pop(uid, None)
286
395
  self.socket_to_connection.pop(connection.socket, None)
287
- self.uid_to_send_lock.pop(uid, None)
396
+ send_queue = self.uid_to_send_queue.pop(uid, None)
397
+ if send_queue:
398
+ send_queue.close()
288
399
  connection.socket_server.delete_client(connection)
289
400
  # Ensure unregister before closing
290
401
  try:
@@ -309,7 +420,10 @@ class TcpForwardClient:
309
420
  self.socket_event_loop.unregister(connection.socket)
310
421
  self.uid_to_connection.pop(uid, None)
311
422
  self.socket_to_connection.pop(connection.socket, None)
312
- self.uid_to_send_lock.pop(uid, None)
423
+ send_queue = self.uid_to_send_queue.pop(uid, None)
424
+ if send_queue:
425
+ # 可能在 socket 线程调用, 交给 tornado 线程关闭队列
426
+ self.tornado_loop.add_callback(send_queue.close)
313
427
  connection.socket_server.delete_client(connection)
314
428
  connection.socket.close()
315
429
  except Exception: