proxynt 2.0.54__tar.gz → 2.0.58__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 (177) hide show
  1. {proxynt-2.0.54/proxynt.egg-info → proxynt-2.0.58}/PKG-INFO +1 -1
  2. proxynt-2.0.58/client/heart_beat_task.py +74 -0
  3. {proxynt-2.0.54 → proxynt-2.0.58}/client/tcp_forward_client.py +58 -211
  4. {proxynt-2.0.54 → proxynt-2.0.58}/client/udp_forward_client.py +52 -100
  5. {proxynt-2.0.54 → proxynt-2.0.58}/common/pool.py +5 -1
  6. {proxynt-2.0.54 → proxynt-2.0.58}/common/websocket/__init__.py +12 -8
  7. {proxynt-2.0.54 → proxynt-2.0.58}/common/websocket/_abnf.py +165 -107
  8. proxynt-2.0.58/common/websocket/_app.py +690 -0
  9. proxynt-2.0.58/common/websocket/_cookiejar.py +74 -0
  10. {proxynt-2.0.54 → proxynt-2.0.58}/common/websocket/_core.py +200 -91
  11. proxynt-2.0.58/common/websocket/_dispatcher.py +165 -0
  12. {proxynt-2.0.54 → proxynt-2.0.58}/common/websocket/_exceptions.py +20 -4
  13. proxynt-2.0.58/common/websocket/_handshake.py +236 -0
  14. proxynt-2.0.58/common/websocket/_http.py +424 -0
  15. proxynt-2.0.58/common/websocket/_logging.py +121 -0
  16. {proxynt-2.0.54 → proxynt-2.0.58}/common/websocket/_socket.py +47 -24
  17. proxynt-2.0.58/common/websocket/_ssl_compat.py +66 -0
  18. {proxynt-2.0.54 → proxynt-2.0.58}/common/websocket/_url.py +78 -49
  19. proxynt-2.0.58/common/websocket/_utils.py +460 -0
  20. {proxynt-2.0.54 → proxynt-2.0.58}/common/websocket/_wsdump.py +65 -52
  21. {proxynt-2.0.54 → proxynt-2.0.58}/common/websocket/tests/echo-server.py +5 -3
  22. proxynt-2.0.58/common/websocket/tests/test_abnf.py +244 -0
  23. proxynt-2.0.58/common/websocket/tests/test_app.py +826 -0
  24. {proxynt-2.0.54 → proxynt-2.0.58}/common/websocket/tests/test_cookiejar.py +14 -7
  25. proxynt-2.0.58/common/websocket/tests/test_dispatcher.py +386 -0
  26. proxynt-2.0.58/common/websocket/tests/test_handshake_large_response.py +159 -0
  27. proxynt-2.0.58/common/websocket/tests/test_http.py +394 -0
  28. proxynt-2.0.58/common/websocket/tests/test_large_payloads.py +274 -0
  29. proxynt-2.0.58/common/websocket/tests/test_logging_helpers.py +85 -0
  30. proxynt-2.0.58/common/websocket/tests/test_reconnect_bad_fd.py +190 -0
  31. proxynt-2.0.58/common/websocket/tests/test_socket.py +389 -0
  32. proxynt-2.0.58/common/websocket/tests/test_socket_bugs.py +161 -0
  33. proxynt-2.0.58/common/websocket/tests/test_ssl_compat.py +92 -0
  34. proxynt-2.0.58/common/websocket/tests/test_ssl_edge_cases.py +639 -0
  35. proxynt-2.0.58/common/websocket/tests/test_url.py +505 -0
  36. proxynt-2.0.58/common/websocket/tests/test_utils.py +139 -0
  37. proxynt-2.0.58/common/websocket/tests/test_websocket.py +767 -0
  38. proxynt-2.0.58/common/websocket2/compliance/autobahn-test-report-Feb-03-2021.html +5923 -0
  39. proxynt-2.0.58/common/websocket2/setup.py +76 -0
  40. proxynt-2.0.54/common/websocket/_ssl_compat.py → proxynt-2.0.58/common/websocket2/websocket/__init__.py +13 -22
  41. proxynt-2.0.58/common/websocket2/websocket/_abnf.py +482 -0
  42. proxynt-2.0.58/common/websocket2/websocket/_app.py +690 -0
  43. proxynt-2.0.58/common/websocket2/websocket/_cookiejar.py +74 -0
  44. proxynt-2.0.58/common/websocket2/websocket/_core.py +712 -0
  45. proxynt-2.0.58/common/websocket2/websocket/_dispatcher.py +165 -0
  46. proxynt-2.0.58/common/websocket2/websocket/_exceptions.py +96 -0
  47. proxynt-2.0.58/common/websocket2/websocket/_handshake.py +236 -0
  48. proxynt-2.0.58/common/websocket2/websocket/_http.py +424 -0
  49. proxynt-2.0.58/common/websocket2/websocket/_logging.py +121 -0
  50. proxynt-2.0.58/common/websocket2/websocket/_socket.py +202 -0
  51. proxynt-2.0.58/common/websocket2/websocket/_ssl_compat.py +66 -0
  52. proxynt-2.0.58/common/websocket2/websocket/_url.py +201 -0
  53. proxynt-2.0.58/common/websocket2/websocket/_utils.py +460 -0
  54. proxynt-2.0.58/common/websocket2/websocket/_wsdump.py +244 -0
  55. proxynt-2.0.58/common/websocket2/websocket/tests/echo-server.py +23 -0
  56. proxynt-2.0.58/common/websocket2/websocket/tests/test_abnf.py +244 -0
  57. proxynt-2.0.58/common/websocket2/websocket/tests/test_app.py +826 -0
  58. proxynt-2.0.58/common/websocket2/websocket/tests/test_cookiejar.py +123 -0
  59. proxynt-2.0.58/common/websocket2/websocket/tests/test_dispatcher.py +386 -0
  60. proxynt-2.0.58/common/websocket2/websocket/tests/test_handshake_large_response.py +159 -0
  61. proxynt-2.0.58/common/websocket2/websocket/tests/test_http.py +394 -0
  62. proxynt-2.0.58/common/websocket2/websocket/tests/test_large_payloads.py +274 -0
  63. proxynt-2.0.58/common/websocket2/websocket/tests/test_logging_helpers.py +85 -0
  64. proxynt-2.0.58/common/websocket2/websocket/tests/test_reconnect_bad_fd.py +190 -0
  65. proxynt-2.0.58/common/websocket2/websocket/tests/test_socket.py +389 -0
  66. proxynt-2.0.58/common/websocket2/websocket/tests/test_socket_bugs.py +161 -0
  67. proxynt-2.0.58/common/websocket2/websocket/tests/test_ssl_compat.py +92 -0
  68. proxynt-2.0.58/common/websocket2/websocket/tests/test_ssl_edge_cases.py +639 -0
  69. proxynt-2.0.58/common/websocket2/websocket/tests/test_url.py +505 -0
  70. proxynt-2.0.58/common/websocket2/websocket/tests/test_utils.py +139 -0
  71. proxynt-2.0.58/common/websocket2/websocket/tests/test_websocket.py +767 -0
  72. {proxynt-2.0.54 → proxynt-2.0.58}/constant/system_constant.py +1 -1
  73. {proxynt-2.0.54 → proxynt-2.0.58/proxynt.egg-info}/PKG-INFO +1 -1
  74. {proxynt-2.0.54 → proxynt-2.0.58}/proxynt.egg-info/SOURCES.txt +47 -16
  75. {proxynt-2.0.54 → proxynt-2.0.58}/run_client.py +165 -237
  76. proxynt-2.0.58/server/task/__init__.py +0 -0
  77. proxynt-2.0.58/server/template/__init__.py +0 -0
  78. proxynt-2.0.54/build/lib/proxynt/server/template/base.html +0 -38
  79. proxynt-2.0.54/build/lib/proxynt/server/template/css/fonts/element-icons.woff +0 -0
  80. proxynt-2.0.54/build/lib/proxynt/server/template/css/index.css +0 -1
  81. proxynt-2.0.54/build/lib/proxynt/server/template/ele_index.html +0 -624
  82. proxynt-2.0.54/build/lib/proxynt/server/template/js/axios.min.js +0 -3
  83. proxynt-2.0.54/build/lib/proxynt/server/template/js/index.js +0 -1
  84. proxynt-2.0.54/build/lib/proxynt/server/template/js/vue.min.js +0 -6
  85. proxynt-2.0.54/build/lib/proxynt/server/template/login.html +0 -54
  86. proxynt-2.0.54/client/heart_beat_task.py +0 -82
  87. proxynt-2.0.54/common/websocket/_app.py +0 -493
  88. proxynt-2.0.54/common/websocket/_cookiejar.py +0 -64
  89. proxynt-2.0.54/common/websocket/_handshake.py +0 -193
  90. proxynt-2.0.54/common/websocket/_http.py +0 -332
  91. proxynt-2.0.54/common/websocket/_logging.py +0 -61
  92. proxynt-2.0.54/common/websocket/_utils.py +0 -104
  93. proxynt-2.0.54/common/websocket/tests/test_abnf.py +0 -89
  94. proxynt-2.0.54/common/websocket/tests/test_app.py +0 -233
  95. proxynt-2.0.54/common/websocket/tests/test_http.py +0 -176
  96. proxynt-2.0.54/common/websocket/tests/test_url.py +0 -301
  97. proxynt-2.0.54/common/websocket/tests/test_websocket.py +0 -455
  98. proxynt-2.0.54/server/template/base.html +0 -38
  99. proxynt-2.0.54/server/template/css/fonts/element-icons.woff +0 -0
  100. proxynt-2.0.54/server/template/css/index.css +0 -1
  101. proxynt-2.0.54/server/template/ele_index.html +0 -624
  102. proxynt-2.0.54/server/template/js/axios.min.js +0 -3
  103. proxynt-2.0.54/server/template/js/index.js +0 -1
  104. proxynt-2.0.54/server/template/js/vue.min.js +0 -6
  105. proxynt-2.0.54/server/template/login.html +0 -54
  106. {proxynt-2.0.54 → proxynt-2.0.58}/LICENSE +0 -0
  107. {proxynt-2.0.54 → proxynt-2.0.58}/MANIFEST.in +0 -0
  108. {proxynt-2.0.54 → proxynt-2.0.58}/__init__.py +0 -0
  109. {proxynt-2.0.54 → proxynt-2.0.58}/client/__init__.py +0 -0
  110. {proxynt-2.0.54 → proxynt-2.0.58}/client/abstract_tunnel.py +0 -0
  111. {proxynt-2.0.54 → proxynt-2.0.58}/client/clear_nonce_task.py +0 -0
  112. {proxynt-2.0.54 → proxynt-2.0.58}/client/kcp_tunnel_impl.py +0 -0
  113. {proxynt-2.0.54 → proxynt-2.0.58}/client/n4_tunnel_manager.py +0 -0
  114. {proxynt-2.0.54 → proxynt-2.0.58}/client/quic_tunnel_impl.py +0 -0
  115. {proxynt-2.0.54 → proxynt-2.0.58}/client/tunnel_protocol.py +0 -0
  116. {proxynt-2.0.54 → proxynt-2.0.58}/common/__init__.py +0 -0
  117. {proxynt-2.0.54 → proxynt-2.0.58}/common/cert_utils.py +0 -0
  118. {proxynt-2.0.54 → proxynt-2.0.58}/common/crypto/__init__.py +0 -0
  119. {proxynt-2.0.54 → proxynt-2.0.58}/common/crypto/table.py +0 -0
  120. {proxynt-2.0.54 → proxynt-2.0.58}/common/encrypt_utils.py +0 -0
  121. {proxynt-2.0.54 → proxynt-2.0.58}/common/kcp.py +0 -0
  122. {proxynt-2.0.54 → proxynt-2.0.58}/common/logger_factory.py +0 -0
  123. {proxynt-2.0.54 → proxynt-2.0.58}/common/n4_protocol.py +0 -0
  124. {proxynt-2.0.54 → proxynt-2.0.58}/common/n4_punch.py +0 -0
  125. {proxynt-2.0.54 → proxynt-2.0.58}/common/nat_serialization.py +0 -0
  126. {proxynt-2.0.54 → proxynt-2.0.58}/common/nat_serialization_v1.py +0 -0
  127. {proxynt-2.0.54 → proxynt-2.0.58}/common/nat_serialization_v2.py +0 -0
  128. {proxynt-2.0.54 → proxynt-2.0.58}/common/register_append_data.py +0 -0
  129. {proxynt-2.0.54 → proxynt-2.0.58}/common/speed_limit.py +0 -0
  130. {proxynt-2.0.54 → proxynt-2.0.58}/common/websocket/tests/__init__.py +0 -0
  131. {proxynt-2.0.54/constant → proxynt-2.0.58/common/websocket2}/__init__.py +0 -0
  132. {proxynt-2.0.54/context → proxynt-2.0.58/common/websocket2/websocket/tests}/__init__.py +0 -0
  133. {proxynt-2.0.54/entity → proxynt-2.0.58/constant}/__init__.py +0 -0
  134. {proxynt-2.0.54 → proxynt-2.0.58}/constant/message_type_constnat.py +0 -0
  135. {proxynt-2.0.54/entity/message → proxynt-2.0.58/context}/__init__.py +0 -0
  136. {proxynt-2.0.54 → proxynt-2.0.58}/context/context_utils.py +0 -0
  137. {proxynt-2.0.54/exceptions → proxynt-2.0.58/entity}/__init__.py +0 -0
  138. {proxynt-2.0.54 → proxynt-2.0.58}/entity/client_config_entity.py +0 -0
  139. {proxynt-2.0.54/p2ptest → proxynt-2.0.58/entity/message}/__init__.py +0 -0
  140. {proxynt-2.0.54 → proxynt-2.0.58}/entity/message/message_entity.py +0 -0
  141. {proxynt-2.0.54 → proxynt-2.0.58}/entity/message/push_config_entity.py +0 -0
  142. {proxynt-2.0.54 → proxynt-2.0.58}/entity/message/tcp_over_websocket_message.py +0 -0
  143. {proxynt-2.0.54 → proxynt-2.0.58}/entity/server_config_entity.py +0 -0
  144. {proxynt-2.0.54/server → proxynt-2.0.58/exceptions}/__init__.py +0 -0
  145. {proxynt-2.0.54 → proxynt-2.0.58}/exceptions/duplicated_name.py +0 -0
  146. {proxynt-2.0.54 → proxynt-2.0.58}/exceptions/invalid_password.py +0 -0
  147. {proxynt-2.0.54 → proxynt-2.0.58}/exceptions/replay_error.py +0 -0
  148. {proxynt-2.0.54 → proxynt-2.0.58}/exceptions/signature_error.py +0 -0
  149. {proxynt-2.0.54/server/task → proxynt-2.0.58/p2ptest}/__init__.py +0 -0
  150. {proxynt-2.0.54 → proxynt-2.0.58}/p2ptest/client.py +0 -0
  151. {proxynt-2.0.54 → proxynt-2.0.58}/p2ptest/n4.py +0 -0
  152. {proxynt-2.0.54 → proxynt-2.0.58}/proxynt.egg-info/dependency_links.txt +0 -0
  153. {proxynt-2.0.54 → proxynt-2.0.58}/proxynt.egg-info/entry_points.txt +0 -0
  154. {proxynt-2.0.54 → proxynt-2.0.58}/proxynt.egg-info/requires.txt +0 -0
  155. {proxynt-2.0.54 → proxynt-2.0.58}/proxynt.egg-info/top_level.txt +0 -0
  156. {proxynt-2.0.54 → proxynt-2.0.58}/run_server.py +0 -0
  157. {proxynt-2.0.54/server/template → proxynt-2.0.58/server}/__init__.py +0 -0
  158. {proxynt-2.0.54 → proxynt-2.0.58}/server/admin_http_handler.py +0 -0
  159. {proxynt-2.0.54 → proxynt-2.0.58}/server/n4.py +0 -0
  160. {proxynt-2.0.54 → proxynt-2.0.58}/server/n4_signal_service.py +0 -0
  161. {proxynt-2.0.54 → proxynt-2.0.58}/server/task/check_cookie_task.py +0 -0
  162. {proxynt-2.0.54 → proxynt-2.0.58}/server/task/clear_nonce_task.py +0 -0
  163. {proxynt-2.0.54 → proxynt-2.0.58}/server/task/heart_beat_task.py +0 -0
  164. {proxynt-2.0.54 → proxynt-2.0.58}/server/tcp_forward_client.py +0 -0
  165. {proxynt-2.0.54/build/lib/proxynt/build/lib/proxynt → proxynt-2.0.58}/server/template/base.html +0 -0
  166. {proxynt-2.0.54/build/lib/proxynt/build/lib/proxynt → proxynt-2.0.58}/server/template/css/fonts/element-icons.woff +0 -0
  167. {proxynt-2.0.54/build/lib/proxynt/build/lib/proxynt → proxynt-2.0.58}/server/template/css/index.css +0 -0
  168. {proxynt-2.0.54/build/lib/proxynt/build/lib/proxynt → proxynt-2.0.58}/server/template/ele_index.html +0 -0
  169. {proxynt-2.0.54/build/lib/proxynt/build/lib/proxynt → proxynt-2.0.58}/server/template/js/axios.min.js +0 -0
  170. {proxynt-2.0.54/build/lib/proxynt/build/lib/proxynt → proxynt-2.0.58}/server/template/js/index.js +0 -0
  171. {proxynt-2.0.54/build/lib/proxynt/build/lib/proxynt → proxynt-2.0.58}/server/template/js/vue.min.js +0 -0
  172. {proxynt-2.0.54/build/lib/proxynt/build/lib/proxynt → proxynt-2.0.58}/server/template/login.html +0 -0
  173. {proxynt-2.0.54 → proxynt-2.0.58}/server/udp_forward_client.py +0 -0
  174. {proxynt-2.0.54 → proxynt-2.0.58}/server/websocket_handler.py +0 -0
  175. {proxynt-2.0.54 → proxynt-2.0.58}/setup.cfg +0 -0
  176. {proxynt-2.0.54 → proxynt-2.0.58}/setup.py +0 -0
  177. {proxynt-2.0.54 → proxynt-2.0.58}/test_exchange.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: proxynt
3
- Version: 2.0.54
3
+ Version: 2.0.58
4
4
  Summary: UNKNOWN
5
5
  Home-page: https://github.com/sazima/proxynt
6
6
  License: UNKNOWN
@@ -0,0 +1,74 @@
1
+ import logging
2
+ import time
3
+ import traceback
4
+
5
+ from common.logger_factory import LoggerFactory
6
+ from common.nat_serialization import NatSerialization
7
+ from constant.message_type_constnat import MessageTypeConstant
8
+ from constant.system_constant import SystemConstant
9
+ from context.context_utils import ContextUtils
10
+ from entity.message.message_entity import MessageEntity
11
+
12
+
13
+ class HeatBeatTask:
14
+ def __init__(self, ws_sender, sleep_break: int, protocol_version: int):
15
+ self.ws_sender = ws_sender
16
+ self.is_running = False
17
+ self.recv_heart_beat_time: float = time.time()
18
+ self.sleep_break = sleep_break
19
+ self.protocol_version = protocol_version
20
+ self._periodic_callback = None
21
+
22
+ def set_recv_heart_beat_time(self, d: float):
23
+ self.recv_heart_beat_time = d
24
+
25
+ def start(self, io=None):
26
+ """Start heartbeat PeriodicCallback on the IOLoop. No thread needed."""
27
+ from tornado.ioloop import PeriodicCallback, IOLoop
28
+ target_io = io or IOLoop.current()
29
+ # PeriodicCallback interval is in milliseconds
30
+ self._periodic_callback = PeriodicCallback(self._tick, self.sleep_break * 1000)
31
+ self._periodic_callback.start()
32
+
33
+ def stop(self):
34
+ if self._periodic_callback is not None:
35
+ self._periodic_callback.stop()
36
+ self._periodic_callback = None
37
+
38
+ def _tick(self):
39
+ """Called by PeriodicCallback on the IOLoop."""
40
+ if not self.is_running:
41
+ return
42
+ if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
43
+ LoggerFactory.get_logger().debug('run send heartbeat')
44
+ try:
45
+ self.send_heart_beat()
46
+ except Exception:
47
+ LoggerFactory.get_logger().error(traceback.format_exc())
48
+ try:
49
+ self.check_recv_heart_beat_time()
50
+ except Exception:
51
+ LoggerFactory.get_logger().error(traceback.format_exc())
52
+
53
+ def send_heart_beat(self):
54
+ if not self.is_running:
55
+ return
56
+ ping_message: MessageEntity = {
57
+ 'type_': MessageTypeConstant.PING,
58
+ 'data': None
59
+ }
60
+ self.ws_sender.send(
61
+ NatSerialization.dumps(ping_message, ContextUtils.get_password(), False, self.protocol_version)
62
+ )
63
+
64
+ def check_recv_heart_beat_time(self):
65
+ """Close connection on heartbeat timeout."""
66
+ if not self.is_running:
67
+ return
68
+ elapsed = time.time() - self.recv_heart_beat_time
69
+ if elapsed > SystemConstant.MAX_HEART_BEAT_SECONDS:
70
+ LoggerFactory.get_logger().info(
71
+ 'Heartbeat receive timeout %.1f s, closing connection' % elapsed
72
+ )
73
+ # Closing the Tornado ws triggers on_message(None) → on_close
74
+ self.ws_sender.close()
@@ -1,6 +1,5 @@
1
1
  import logging
2
2
  import os
3
- import queue
4
3
  import socket
5
4
  import threading
6
5
  import time
@@ -8,7 +7,6 @@ import traceback
8
7
  from threading import Lock
9
8
  from typing import Dict, List
10
9
 
11
- from common import websocket
12
10
  from common.logger_factory import LoggerFactory
13
11
  from common.nat_serialization import NatSerialization
14
12
  from common.pool import SelectPool
@@ -19,169 +17,62 @@ from context.context_utils import ContextUtils
19
17
  from entity.message.message_entity import MessageEntity
20
18
 
21
19
 
22
- class MessageSender:
23
- """
24
- Dual priority queue message sender
25
- - High priority queue: for heartbeat and control messages (latency < 10ms)
26
- - Normal queue: for business data
27
- """
28
-
29
- def __init__(self, ws):
30
- # Dual priority queues
31
- self.high_priority_queue = queue.Queue(maxsize=64) # High priority: control messages
32
- self.normal_queue = queue.Queue(maxsize=1024) # Normal priority: business data
33
- self.ws = ws
34
- self.running = False
35
- self.sender_thread = threading.Thread(target=self.send_messages)
36
-
37
- def send_messages(self):
38
- """Priority send loop"""
39
- while self.running or not self.normal_queue.empty() or not self.high_priority_queue.empty():
40
- try:
41
- send_bytes = None
42
-
43
- # 1. Process high priority queue first (non-blocking)
44
- try:
45
- send_bytes = self.high_priority_queue.get_nowait()
46
- except queue.Empty:
47
- pass
48
-
49
- # 2. If high priority queue is empty, get from normal queue (blocking)
50
- if send_bytes is None:
51
- try:
52
- send_bytes = self.normal_queue.get(timeout=1)
53
- except queue.Empty:
54
- continue
55
-
56
- # 3. Send message
57
- self.ws.send(send_bytes, opcode=websocket.ABNF.OPCODE_BINARY)
58
-
59
- # Mark task as done
60
- try:
61
- self.high_priority_queue.task_done()
62
- except ValueError:
63
- self.normal_queue.task_done()
64
-
65
- except Exception as e:
66
- LoggerFactory.get_logger().error(f"Failed to send message: {e}")
67
-
68
- def enqueue_message(self, message, high_priority=False):
69
- """
70
- Add message to queue
71
- :param message: Message content
72
- :param high_priority: Whether high priority (heartbeat, control messages)
73
- """
74
- if self.running:
75
- if high_priority:
76
- try:
77
- self.high_priority_queue.put_nowait(message)
78
- except queue.Full:
79
- LoggerFactory.get_logger().warning("High priority queue full")
80
- else:
81
- try:
82
- self.normal_queue.put(message, timeout=5)
83
- except queue.Full:
84
- LoggerFactory.get_logger().warning("Normal queue full")
85
- else:
86
- LoggerFactory.get_logger().warning("WebSocket is not running. Cannot enqueue message.")
87
-
88
- def start(self):
89
- self.running = True
90
- self.sender_thread.start()
91
-
92
- def stop(self):
93
- """Safe stop: send high priority first, then normal queue"""
94
- def safe_stop():
95
- # 1. Send high priority queue first
96
- while not self.high_priority_queue.empty():
97
- try:
98
- message = self.high_priority_queue.get_nowait()
99
- self.ws.send(message, opcode=websocket.ABNF.OPCODE_BINARY)
100
- self.high_priority_queue.task_done()
101
- except queue.Empty:
102
- break
103
- except Exception as e:
104
- LoggerFactory.get_logger().error(f"Failed to send high priority message during stop: {e}")
105
-
106
- # 2. Then send normal queue
107
- while not self.normal_queue.empty():
108
- try:
109
- message = self.normal_queue.get_nowait()
110
- self.ws.send(message, opcode=websocket.ABNF.OPCODE_BINARY)
111
- self.normal_queue.task_done()
112
- except queue.Empty:
113
- break
114
- except Exception as e:
115
- LoggerFactory.get_logger().error(f"Failed to send normal message during stop: {e}")
116
-
117
- self.running = False
118
- safe_stop()
119
-
120
-
121
20
  class PrivateSocketConnection:
122
21
  """Client connecting to internal network port"""
123
22
 
124
- def __init__(self, uid: bytes, s: socket.socket, name: str, ws):
23
+ def __init__(self, uid: bytes, s: socket.socket, name: str):
125
24
  self.uid: bytes = uid
126
25
  self.socket: socket.socket = s
127
26
  self.name: str = name
128
- self.sender = MessageSender(ws)
129
- self.sender.start()
130
27
 
131
28
 
132
29
  class TcpForwardClient:
133
- def __init__(self, ws: websocket, compress_support: bool, protocol_version: int):
30
+ def __init__(self, ws_sender, compress_support: bool, protocol_version: int):
134
31
  self.uid_to_socket_connection: Dict[bytes, PrivateSocketConnection] = dict()
135
32
  self.socket_to_socket_connection: Dict[socket.socket, PrivateSocketConnection] = dict()
136
33
  self.compress_support: bool = compress_support
137
34
  self.protocol_version: int = protocol_version
138
- self.ws = ws
35
+ self.ws = ws_sender # WsSender — thread-safe, send via IOLoop.add_callback
139
36
  self.lock = Lock()
140
37
  self.socket_event_loop = SelectPool()
141
38
 
142
39
  # C2C client-to-client forward state
143
- self.c2c_rules: List[dict] = [] # C2C rules list
144
- self.c2c_listeners: Dict[str, socket.socket] = {} # rule_name → listener socket
145
- self.c2c_uid_to_rule: Dict[bytes, str] = {} # UID → rule_name
40
+ self.c2c_rules: List[dict] = []
41
+ self.c2c_listeners: Dict[str, socket.socket] = {}
42
+ self.c2c_uid_to_rule: Dict[bytes, str] = {}
146
43
 
147
44
  # N4 Tunnel Manager for P2P data transfer
148
45
  self.tunnel_manager = None
149
46
 
150
47
  # Pending data buffer: uid -> list of (data, timestamp)
151
- # Used to buffer P2P data that arrives before connection is established
152
48
  self.pending_data: Dict[bytes, list] = {}
153
49
  self.pending_data_lock = Lock()
154
- self.pending_data_timeout = 10 # seconds to keep pending data
50
+ self.pending_data_timeout = 10
155
51
 
156
52
  def set_running(self, running: bool):
157
53
  self.socket_event_loop.is_running = running
158
54
 
159
55
  def update_websocket(self, ws):
160
- """Update websocket connection reference (for client reconnection)"""
161
- self.ws = ws
162
- LoggerFactory.get_logger().info('TCP forward client websocket reference updated')
56
+ """No-op: ws_sender reference is stable; connection is updated inside WsSender."""
57
+ pass
163
58
 
164
59
  def start_forward(self):
165
60
  self.socket_event_loop.run()
166
61
 
167
62
  def setup_c2c_tcp_listeners(self, c2c_rules: List[dict]):
168
- """
169
- Setup C2C TCP listeners
170
- Create local listeners for each C2C rule to accept connections from local applications
171
- """
172
- # Clean old listeners first to avoid port conflicts
63
+ # Clean old listeners first
173
64
  for rule_name, listener in list(self.c2c_listeners.items()):
174
65
  try:
175
66
  listener.close()
176
- LoggerFactory.get_logger().info(f'Cleaned old C2C TCP listener: {rule_name}')
67
+ LoggerFactory.get_logger().info('Cleaned old C2C TCP listener: %s' % rule_name)
177
68
  except Exception as e:
178
- LoggerFactory.get_logger().error(f'Failed to close old C2C TCP listener {rule_name}: {e}')
69
+ LoggerFactory.get_logger().error('Failed to close old C2C TCP listener %s: %s' % (rule_name, e))
179
70
 
180
71
  self.c2c_listeners.clear()
181
72
  self.c2c_uid_to_rule.clear()
182
73
 
183
74
  self.c2c_rules = c2c_rules
184
- LoggerFactory.get_logger().info(f'Setting up {len(c2c_rules)} C2C TCP listeners')
75
+ LoggerFactory.get_logger().info('Setting up %d C2C TCP listeners' % len(c2c_rules))
185
76
 
186
77
  for rule in c2c_rules:
187
78
  if rule['protocol'] != 'tcp':
@@ -192,16 +83,14 @@ class TcpForwardClient:
192
83
  local_port = rule['local_port']
193
84
 
194
85
  try:
195
- # Create listener socket
196
86
  listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
197
87
  listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
198
88
  listener.bind((local_ip, local_port))
199
89
  listener.listen(128)
200
90
 
201
91
  self.c2c_listeners[rule_name] = listener
202
- LoggerFactory.get_logger().info(f'C2C TCP listener created: {rule_name} on {local_ip}:{local_port}')
92
+ LoggerFactory.get_logger().info('C2C TCP listener created: %s on %s:%d' % (rule_name, local_ip, local_port))
203
93
 
204
- # Start separate thread to handle connection acceptance
205
94
  accept_thread = threading.Thread(
206
95
  target=self.handle_c2c_tcp_accept,
207
96
  args=(listener, rule),
@@ -210,50 +99,38 @@ class TcpForwardClient:
210
99
  accept_thread.start()
211
100
 
212
101
  except Exception as e:
213
- LoggerFactory.get_logger().error(f'Failed to create C2C TCP listener {rule_name}: {e}')
102
+ LoggerFactory.get_logger().error('Failed to create C2C TCP listener %s: %s' % (rule_name, e))
214
103
  LoggerFactory.get_logger().error(traceback.format_exc())
215
104
 
216
105
  def handle_c2c_tcp_accept(self, listener: socket.socket, rule: dict):
217
- """
218
- Handle C2C TCP connection acceptance
219
- Loop to accept connections from local applications, send CLIENT_TO_CLIENT_FORWARD request for each connection
220
- """
221
106
  rule_name = rule['name']
222
107
  target_client = rule['target_client']
223
108
  protocol = rule['protocol']
224
109
  speed_limit = rule.get('speed_limit', 0.0)
225
110
 
226
- # Check if using direct mode (target_ip + target_port) or service mode (target_service)
227
111
  use_direct_mode = 'target_ip' in rule and 'target_port' in rule
228
112
 
229
- LoggerFactory.get_logger().info(f'C2C TCP accept thread started: {rule_name}')
113
+ LoggerFactory.get_logger().info('C2C TCP accept thread started: %s' % rule_name)
230
114
 
231
115
  while True:
232
116
  try:
233
117
  client_socket, client_addr = listener.accept()
234
- LoggerFactory.get_logger().info(f'C2C TCP connection accepted: {rule_name} from {client_addr}')
118
+ LoggerFactory.get_logger().info('C2C TCP connection accepted: %s from %s' % (rule_name, client_addr))
235
119
 
236
- # Enable TCP_NODELAY to reduce latency
237
120
  client_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
238
121
 
239
- # Generate unique UID
240
122
  uid = os.urandom(4)
241
123
 
242
- # Store UID → rule_name mapping
243
124
  with self.lock:
244
125
  self.c2c_uid_to_rule[uid] = rule_name
245
-
246
- # Create connection object
247
- connection = PrivateSocketConnection(uid, client_socket, rule_name, self.ws)
126
+ connection = PrivateSocketConnection(uid, client_socket, rule_name)
248
127
  self.uid_to_socket_connection[uid] = connection
249
128
  self.socket_to_socket_connection[client_socket] = connection
250
129
 
251
- # Register UID to tunnel_manager for P2P routing
252
130
  if self.tunnel_manager:
253
131
  self.tunnel_manager.register_uid(uid, target_client)
254
- LoggerFactory.get_logger().info(f'Registered UID {uid.hex()} to peer {target_client}')
132
+ LoggerFactory.get_logger().info('Registered UID %s to peer %s' % (uid.hex(), target_client))
255
133
 
256
- # Send CLIENT_TO_CLIENT_FORWARD message to server
257
134
  forward_data = {
258
135
  'uid': uid,
259
136
  'target_client': target_client,
@@ -261,7 +138,6 @@ class TcpForwardClient:
261
138
  'protocol': protocol
262
139
  }
263
140
 
264
- # Add target_ip and target_port for direct mode, or target_service for service mode
265
141
  if use_direct_mode:
266
142
  forward_data['target_ip'] = rule['target_ip']
267
143
  forward_data['target_port'] = rule['target_port']
@@ -273,24 +149,20 @@ class TcpForwardClient:
273
149
  'data': forward_data
274
150
  }
275
151
  self.ws.send(
276
- NatSerialization.dumps(forward_message, ContextUtils.get_password(), self.compress_support, self.protocol_version),
277
- websocket.ABNF.OPCODE_BINARY
152
+ NatSerialization.dumps(forward_message, ContextUtils.get_password(), self.compress_support, self.protocol_version)
278
153
  )
279
- LoggerFactory.get_logger().info(f'C2C forward request sent: {rule_name} UID: {uid.hex()}')
154
+ LoggerFactory.get_logger().info('C2C forward request sent: %s UID: %s' % (rule_name, uid.hex()))
280
155
 
281
- # Register to event loop, start forwarding data
282
156
  speed_limiter = SpeedLimiter(speed_limit) if speed_limit > 0 else None
283
157
  self.socket_event_loop.register(client_socket, ResisterAppendData(self.handle_message, speed_limiter))
284
158
 
285
- except OSError as e:
286
- # Listener closed, exit loop
287
- LoggerFactory.get_logger().info(f'C2C TCP listener closed: {rule_name}')
159
+ except OSError:
160
+ LoggerFactory.get_logger().info('C2C TCP listener closed: %s' % rule_name)
288
161
  break
289
162
  except Exception as e:
290
- LoggerFactory.get_logger().error(f'C2C TCP accept connection error {rule_name}: {e}')
163
+ LoggerFactory.get_logger().error('C2C TCP accept connection error %s: %s' % (rule_name, e))
291
164
  LoggerFactory.get_logger().error(traceback.format_exc())
292
165
 
293
-
294
166
  def handle_message(self, each: socket.socket, data: ResisterAppendData):
295
167
  connection = self.socket_to_socket_connection.get(each)
296
168
  if not connection:
@@ -301,22 +173,17 @@ class TcpForwardClient:
301
173
  except OSError:
302
174
  recv = b''
303
175
 
304
- # --- 发送端限速:在发送前等待 ---
305
176
  if data.speed_limiter and recv:
306
177
  wait_time = data.speed_limiter.acquire(len(recv))
307
178
  if wait_time > 0:
308
179
  time.sleep(wait_time)
309
180
 
310
- # --- 无缝切换逻辑 ---
311
181
  if self.tunnel_manager:
312
- # 尝试走 P2P 隧道
313
182
  if self.tunnel_manager.send_data(connection.uid, recv):
314
- # 如果发送成功返回 True,逻辑结束
315
- if not recv: # 本地连接关闭,通知隧道
183
+ if not recv:
316
184
  self.close_connection(each)
317
185
  return
318
186
 
319
- # --- 回退/初始逻辑:走 WebSocket ---
320
187
  send_message: MessageEntity = {
321
188
  'type_': MessageTypeConstant.WEBSOCKET_OVER_TCP,
322
189
  'data': {
@@ -327,7 +194,7 @@ class TcpForwardClient:
327
194
  }
328
195
  }
329
196
 
330
- connection.sender.enqueue_message(
197
+ self.ws.send(
331
198
  NatSerialization.dumps(send_message, ContextUtils.get_password(), self.compress_support, self.protocol_version)
332
199
  )
333
200
 
@@ -342,28 +209,25 @@ class TcpForwardClient:
342
209
  return True
343
210
  connection = None
344
211
  with self.lock:
345
- if uid in self.uid_to_socket_connection: # Check again
212
+ if uid in self.uid_to_socket_connection:
346
213
  return True
347
214
  try:
348
215
  if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
349
- LoggerFactory.get_logger().debug(f'create socket {name}, {uid}')
216
+ LoggerFactory.get_logger().debug('create socket %s, %s' % (name, uid))
350
217
 
351
- # Validate ip_port before splitting
352
218
  if not ip_port or ':' not in ip_port:
353
- LoggerFactory.get_logger().error(f'Invalid ip_port: {repr(ip_port)}, name: {name}, uid: {uid.hex()}')
219
+ LoggerFactory.get_logger().error('Invalid ip_port: %r, name: %s, uid: %s' % (ip_port, name, uid.hex()))
354
220
  return False
355
221
 
356
222
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
357
223
  s.settimeout(5)
358
- # Enable TCP_NODELAY to reduce latency (disable Nagle algorithm)
359
224
  s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
360
- connection = PrivateSocketConnection(uid, s, name, self.ws)
225
+ connection = PrivateSocketConnection(uid, s, name)
361
226
  self.socket_to_socket_connection[s] = connection
362
227
  ip, port = ip_port.split(':')
363
228
  try:
364
229
  s.connect((ip, int(port)))
365
230
 
366
- # Optimistic send mode: send confirmation message immediately after connection success
367
231
  confirm_message: MessageEntity = {
368
232
  'type_': MessageTypeConstant.CONNECT_CONFIRMED,
369
233
  'data': {
@@ -373,14 +237,15 @@ class TcpForwardClient:
373
237
  'ip_port': ip_port
374
238
  }
375
239
  }
376
- self.ws.send(NatSerialization.dumps(confirm_message, ContextUtils.get_password(), self.compress_support, self.protocol_version), websocket.ABNF.OPCODE_BINARY)
240
+ self.ws.send(
241
+ NatSerialization.dumps(confirm_message, ContextUtils.get_password(), self.compress_support, self.protocol_version)
242
+ )
377
243
  if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
378
- LoggerFactory.get_logger().debug(f'Connection confirmation message sent uid: {uid}')
244
+ LoggerFactory.get_logger().debug('Connection confirmation message sent uid: %s' % uid)
379
245
 
380
246
  except OSError as e:
381
- LoggerFactory.get_logger().info(f'connection error, {e}')
247
+ LoggerFactory.get_logger().info('connection error, %s' % e)
382
248
 
383
- # Optimistic send mode: send failure message after connection failure
384
249
  fail_message: MessageEntity = {
385
250
  'type_': MessageTypeConstant.CONNECT_FAILED,
386
251
  'data': {
@@ -391,9 +256,11 @@ class TcpForwardClient:
391
256
  }
392
257
  }
393
258
  try:
394
- self.ws.send(NatSerialization.dumps(fail_message, ContextUtils.get_password(), self.compress_support, self.protocol_version), websocket.ABNF.OPCODE_BINARY)
259
+ self.ws.send(
260
+ NatSerialization.dumps(fail_message, ContextUtils.get_password(), self.compress_support, self.protocol_version)
261
+ )
395
262
  except Exception as send_err:
396
- LoggerFactory.get_logger().error(f'Failed to send connection failure message: {send_err}')
263
+ LoggerFactory.get_logger().error('Failed to send connection failure message: %s' % send_err)
397
264
 
398
265
  self.close_connection(s)
399
266
  self.close_remote_socket(connection)
@@ -401,12 +268,11 @@ class TcpForwardClient:
401
268
 
402
269
  self.uid_to_socket_connection[uid] = connection
403
270
  if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
404
- LoggerFactory.get_logger().debug(f'register socket {name}, {uid}')
271
+ LoggerFactory.get_logger().debug('register socket %s, %s' % (name, uid))
405
272
  self.socket_event_loop.register(s, ResisterAppendData(self.handle_message, speed_limiter))
406
273
  if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
407
- LoggerFactory.get_logger().debug(f'register socket success {name}, {uid}')
274
+ LoggerFactory.get_logger().debug('register socket success %s, %s' % (name, uid))
408
275
 
409
- # Process any pending data that arrived before connection was established
410
276
  self._flush_pending_data(uid, s)
411
277
 
412
278
  return True
@@ -417,35 +283,33 @@ class TcpForwardClient:
417
283
  return False
418
284
 
419
285
  def close_connection(self, socket_client: socket.socket):
420
- LoggerFactory.get_logger().info(f'Closing socket {socket_client}')
286
+ LoggerFactory.get_logger().info('Closing socket %s' % socket_client)
421
287
  if socket_client in self.socket_to_socket_connection:
422
288
  connection: PrivateSocketConnection = self.socket_to_socket_connection.pop(socket_client)
423
289
  self.socket_event_loop.unregister(socket_client)
424
290
  try:
425
291
  socket_client.shutdown(socket.SHUT_RDWR)
426
292
  except OSError as e:
427
- LoggerFactory.get_logger().warn(f'Shutdown OS error {e}')
293
+ LoggerFactory.get_logger().warn('Shutdown OS error %s' % e)
428
294
  socket_client.close()
429
- connection.sender.stop()
430
- LoggerFactory.get_logger().info(f'Socket closed successfully {socket_client}')
295
+ LoggerFactory.get_logger().info('Socket closed successfully %s' % socket_client)
431
296
  if connection.uid in self.uid_to_socket_connection:
432
297
  self.uid_to_socket_connection.pop(connection.uid)
433
298
  self.close_remote_socket(connection)
434
299
 
435
300
  def close(self):
436
- LoggerFactory.get_logger().info(f'Starting to close {self.c2c_listeners}')
301
+ LoggerFactory.get_logger().info('Starting to close %s' % self.c2c_listeners)
437
302
  with self.lock:
438
- # Close all C2C listeners
439
303
  for rule_name, listener in self.c2c_listeners.items():
440
304
  try:
441
305
  try:
442
306
  listener.shutdown(socket.SHUT_RDWR)
443
307
  except OSError as e:
444
- LoggerFactory.get_logger().warn(f'Shutdown OS error {e}')
308
+ LoggerFactory.get_logger().warn('Shutdown OS error %s' % e)
445
309
  listener.close()
446
- LoggerFactory.get_logger().info(f'C2C TCP listener closed: {rule_name}')
310
+ LoggerFactory.get_logger().info('C2C TCP listener closed: %s' % rule_name)
447
311
  except Exception as e:
448
- LoggerFactory.get_logger().error(f'Failed to close C2C TCP listener {rule_name}: {e}')
312
+ LoggerFactory.get_logger().error('Failed to close C2C TCP listener %s: %s' % (rule_name, e))
449
313
 
450
314
  self.c2c_listeners.clear()
451
315
  self.c2c_rules.clear()
@@ -462,21 +326,16 @@ class TcpForwardClient:
462
326
  try:
463
327
  s.shutdown(socket.SHUT_RDWR)
464
328
  except OSError as e:
465
- LoggerFactory.get_logger().warn(f'Shutdown OS error {e}')
329
+ LoggerFactory.get_logger().warn('Shutdown OS error %s' % e)
466
330
  s.close()
467
331
  except Exception:
468
332
  LoggerFactory.get_logger().error(traceback.format_exc())
469
- c.sender.stop()
470
333
  self.uid_to_socket_connection.clear()
471
334
  self.socket_to_socket_connection.clear()
472
335
  self.set_running(False)
473
336
  self.socket_event_loop.clear()
474
337
 
475
338
  def close_remote_socket(self, connection: PrivateSocketConnection):
476
- # if name is None:
477
- # connection = self.uid_to_socket_connection.get(uid)
478
- # if not connection:
479
- # return
480
339
  name = connection.name
481
340
  send_message: MessageEntity = {
482
341
  'type_': MessageTypeConstant.WEBSOCKET_OVER_TCP,
@@ -488,43 +347,33 @@ class TcpForwardClient:
488
347
  }
489
348
  }
490
349
  start_time = time.time()
491
- self.ws.send(NatSerialization.dumps(send_message, ContextUtils.get_password(), self.compress_support, self.protocol_version), websocket.ABNF.OPCODE_BINARY)
492
- LoggerFactory.get_logger().debug(f'Send to websocket cost time {time.time() - start_time}')
350
+ self.ws.send(
351
+ NatSerialization.dumps(send_message, ContextUtils.get_password(), self.compress_support, self.protocol_version)
352
+ )
353
+ LoggerFactory.get_logger().debug('Send to websocket cost time %s' % (time.time() - start_time))
493
354
 
494
355
  def send_by_uid(self, uid: bytes, msg: bytes):
495
356
  connection = self.uid_to_socket_connection.get(uid)
496
357
  if not connection:
497
- # Connection not established yet, buffer the data
498
- # This handles the race condition where P2P data arrives before
499
- # the REQUEST_TO_CONNECT message establishes the connection
500
- if msg: # Only buffer non-empty data
358
+ if msg:
501
359
  with self.pending_data_lock:
502
360
  if uid not in self.pending_data:
503
361
  self.pending_data[uid] = []
504
362
  self.pending_data[uid].append((msg, time.time()))
505
363
  LoggerFactory.get_logger().debug(
506
- f'Buffered {len(msg)} bytes for UID {uid.hex()} (connection pending)'
364
+ 'Buffered %d bytes for UID %s (connection pending)' % (len(msg), uid.hex())
507
365
  )
508
366
  return
509
367
  try:
510
- if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
511
- LoggerFactory.get_logger().debug(f'Starting to send to socket uid: {uid}, length: {len(msg)}')
512
368
  s = connection.socket
513
369
  s.sendall(msg)
514
- if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
515
- LoggerFactory.get_logger().debug(f'Finished sending to socket uid {uid}, length: {len(msg)}')
516
370
  if not msg:
517
371
  self.close_connection(s)
518
372
  except Exception:
519
373
  LoggerFactory.get_logger().error(traceback.format_exc())
520
- # After error, send empty message, server will close connection
521
374
  self.close_remote_socket(connection)
522
375
 
523
376
  def _flush_pending_data(self, uid: bytes, sock: socket.socket):
524
- """
525
- Flush any pending data that was buffered before connection was established.
526
- This solves the race condition where P2P data arrives before REQUEST_TO_CONNECT.
527
- """
528
377
  pending_items = None
529
378
  with self.pending_data_lock:
530
379
  if uid in self.pending_data:
@@ -538,20 +387,18 @@ class TcpForwardClient:
538
387
  expired_count = 0
539
388
 
540
389
  for data, timestamp in pending_items:
541
- # Skip expired data
542
390
  if now - timestamp > self.pending_data_timeout:
543
391
  expired_count += 1
544
392
  continue
545
-
546
393
  try:
547
394
  sock.sendall(data)
548
395
  total_sent += len(data)
549
396
  except Exception as e:
550
- LoggerFactory.get_logger().error(f'Failed to flush pending data for UID {uid.hex()}: {e}')
397
+ LoggerFactory.get_logger().error('Failed to flush pending data for UID %s: %s' % (uid.hex(), e))
551
398
  break
552
399
 
553
400
  if total_sent > 0 or expired_count > 0:
554
401
  LoggerFactory.get_logger().info(
555
- f'Flushed pending data for UID {uid.hex()}: '
556
- f'{total_sent} bytes sent, {expired_count} expired packets dropped'
402
+ 'Flushed pending data for UID %s: %d bytes sent, %d expired packets dropped'
403
+ % (uid.hex(), total_sent, expired_count)
557
404
  )