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.
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/PKG-INFO +1 -1
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/tcp_forward_client.py +29 -1
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/nat_serialization_v1.py +9 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/pool.py +63 -15
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/constant/message_type_constnat.py +6 -1
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/constant/system_constant.py +1 -1
- proxynt-2.1.0/p2ptest/tls_handshake_timeout_test.py +66 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/proxynt.egg-info/PKG-INFO +1 -1
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/proxynt.egg-info/SOURCES.txt +1 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/run_client.py +25 -11
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/tcp_forward_client.py +172 -58
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/websocket_handler.py +99 -60
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/LICENSE +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/MANIFEST.in +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/abstract_tunnel.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/clear_nonce_task.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/heart_beat_task.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/kcp_tunnel_impl.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/n4_tunnel_manager.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/quic_tunnel_impl.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/tunnel_protocol.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/client/udp_forward_client.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/cert_utils.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/crypto/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/crypto/table.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/encrypt_utils.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/kcp.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/logger_factory.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/n4_protocol.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/n4_punch.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/nat_serialization.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/nat_serialization_v2.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/register_append_data.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/speed_limit.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_abnf.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_app.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_cookiejar.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_core.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_dispatcher.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_exceptions.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_handshake.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_http.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_logging.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_socket.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_ssl_compat.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_url.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_utils.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/_wsdump.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/echo-server.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_abnf.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_app.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_cookiejar.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_dispatcher.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_handshake_large_response.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_http.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_large_payloads.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_logging_helpers.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_reconnect_bad_fd.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_socket.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_socket_bugs.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_ssl_compat.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_ssl_edge_cases.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_url.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_utils.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket/tests/test_websocket.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/compliance/autobahn-test-report-Feb-03-2021.html +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/setup.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_abnf.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_app.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_cookiejar.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_core.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_dispatcher.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_exceptions.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_handshake.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_http.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_logging.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_socket.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_ssl_compat.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_url.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_utils.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/_wsdump.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/echo-server.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_abnf.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_app.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_cookiejar.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_dispatcher.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_handshake_large_response.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_http.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_large_payloads.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_logging_helpers.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_reconnect_bad_fd.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_socket.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_socket_bugs.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_ssl_compat.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_ssl_edge_cases.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_url.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_utils.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/common/websocket2/websocket/tests/test_websocket.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/constant/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/context/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/context/context_utils.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/client_config_entity.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/message/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/message/message_entity.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/message/push_config_entity.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/message/tcp_over_websocket_message.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/entity/server_config_entity.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/exceptions/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/exceptions/duplicated_name.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/exceptions/invalid_password.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/exceptions/replay_error.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/exceptions/signature_error.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/p2ptest/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/p2ptest/client.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/p2ptest/n4.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/proxynt.egg-info/dependency_links.txt +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/proxynt.egg-info/entry_points.txt +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/proxynt.egg-info/requires.txt +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/proxynt.egg-info/top_level.txt +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/run_server.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/admin_http_handler.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/n4.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/n4_signal_service.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/task/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/task/check_cookie_task.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/task/clear_nonce_task.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/task/heart_beat_task.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/__init__.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/base.html +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/css/fonts/element-icons.woff +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/css/index.css +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/ele_index.html +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/js/axios.min.js +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/js/index.js +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/js/vue.min.js +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/template/login.html +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/server/udp_forward_client.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/setup.cfg +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/setup.py +0 -0
- {proxynt-2.0.61rc1 → proxynt-2.1.0}/test_exchange.py +0 -0
|
@@ -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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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'
|
|
@@ -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()
|
|
@@ -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
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
+
if write_sem:
|
|
123
|
+
write_sem.release()
|
|
116
124
|
return
|
|
117
125
|
try:
|
|
118
126
|
future = ws.write_message(data, binary=True)
|
|
119
|
-
|
|
127
|
+
if write_sem:
|
|
128
|
+
ioloop.IOLoop.current().add_future(future, lambda f: write_sem.release())
|
|
120
129
|
except Exception:
|
|
121
|
-
|
|
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,
|