proxynt 2.0.20__tar.gz → 2.0.26__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {proxynt-2.0.20/proxynt.egg-info → proxynt-2.0.26}/PKG-INFO +1 -1
- proxynt-2.0.26/client/abstract_tunnel.py +31 -0
- proxynt-2.0.26/client/data_connection_manager.py +250 -0
- proxynt-2.0.26/client/kcp_tunnel_impl.py +52 -0
- proxynt-2.0.26/client/n4_tunnel_manager.py +246 -0
- proxynt-2.0.26/client/quic_tunnel_impl.py +18 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/client/tcp_forward_client.py +114 -18
- proxynt-2.0.26/client/tunnel_protocol.py +174 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/client/udp_forward_client.py +37 -14
- proxynt-2.0.26/common/cert_utils.py +76 -0
- proxynt-2.0.26/common/kcp.py +852 -0
- proxynt-2.0.26/common/n4_protocol.py +142 -0
- proxynt-2.0.26/common/n4_punch.py +239 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/nat_serialization.py +10 -8
- proxynt-2.0.26/common/speed_limit.py +89 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/constant/message_type_constnat.py +8 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/constant/system_constant.py +1 -1
- {proxynt-2.0.20 → proxynt-2.0.26}/context/context_utils.py +12 -1
- proxynt-2.0.26/p2ptest/client.py +153 -0
- proxynt-2.0.26/p2ptest/n4.py +459 -0
- {proxynt-2.0.20 → proxynt-2.0.26/proxynt.egg-info}/PKG-INFO +1 -1
- {proxynt-2.0.20 → proxynt-2.0.26}/proxynt.egg-info/SOURCES.txt +18 -77
- proxynt-2.0.26/proxynt.egg-info/requires.txt +12 -0
- proxynt-2.0.26/run_client.py +617 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/run_server.py +28 -8
- proxynt-2.0.26/server/n4.py +459 -0
- proxynt-2.0.26/server/n4_signal_service.py +326 -0
- proxynt-2.0.26/server/session_manager.py +288 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/tcp_forward_client.py +16 -8
- proxynt-2.0.26/server/template/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/udp_forward_client.py +86 -15
- {proxynt-2.0.20 → proxynt-2.0.26}/server/websocket_handler.py +288 -159
- {proxynt-2.0.20 → proxynt-2.0.26}/setup.py +2 -2
- proxynt-2.0.26/test_exchange.py +46 -0
- proxynt-2.0.20/client/p2p_hole_punch.py +0 -308
- proxynt-2.0.20/common/speed_limit.py +0 -33
- proxynt-2.0.20/proxynt.egg-info/requires.txt +0 -12
- proxynt-2.0.20/run_client.py +0 -510
- {proxynt-2.0.20 → proxynt-2.0.26}/LICENSE +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/MANIFEST.in +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/client/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/client/clear_nonce_task.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/client/heart_beat_task.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/crypto/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/crypto/table.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/encrypt_utils.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/logger_factory.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/pool.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/register_append_data.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_abnf.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_app.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_cookiejar.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_core.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_exceptions.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_handshake.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_http.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_logging.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_socket.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_ssl_compat.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_url.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_utils.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/_wsdump.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/echo-server.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/test_abnf.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/test_app.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/test_cookiejar.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/test_http.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/test_url.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/common/websocket/tests/test_websocket.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/constant/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/context/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/entity/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/entity/client_config_entity.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/entity/message/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/entity/message/message_entity.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/entity/message/push_config_entity.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/entity/message/tcp_over_websocket_message.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/entity/server_config_entity.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/exceptions/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/exceptions/duplicated_name.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/exceptions/invalid_password.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/exceptions/replay_error.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/exceptions/signature_error.py +0 -0
- {proxynt-2.0.20/server → proxynt-2.0.26/p2ptest}/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/proxynt.egg-info/dependency_links.txt +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/proxynt.egg-info/entry_points.txt +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/proxynt.egg-info/top_level.txt +0 -0
- {proxynt-2.0.20/server/task → proxynt-2.0.26/server}/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/admin_http_handler.py +0 -0
- {proxynt-2.0.20/server/template → proxynt-2.0.26/server/task}/__init__.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/task/check_cookie_task.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/task/clear_nonce_task.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/task/heart_beat_task.py +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/template/base.html +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/template/css/fonts/element-icons.woff +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/template/css/index.css +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/template/ele_index.html +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/template/js/axios.min.js +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/template/js/index.js +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/template/js/vue.min.js +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/server/template/login.html +0 -0
- {proxynt-2.0.20 → proxynt-2.0.26}/setup.cfg +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
class AbstractTunnel(ABC):
|
|
5
|
+
def __init__(self, peer_name: str, sock, addr, on_data_received: Callable, on_established: Callable, on_closed: Callable):
|
|
6
|
+
self.peer_name = peer_name
|
|
7
|
+
self.sock = sock
|
|
8
|
+
self.addr = addr
|
|
9
|
+
self.on_data_received = on_data_received # 应用层收到数据回调
|
|
10
|
+
self.on_established = on_established # 握手成功回调
|
|
11
|
+
self.on_closed = on_closed # 隧道关闭回调
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def establish(self):
|
|
15
|
+
"""开始协议层面的握手/初始化"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def send(self, uid: bytes, data: bytes) -> bool:
|
|
20
|
+
"""发送业务数据"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def update(self):
|
|
25
|
+
"""驱动状态机(如 KCP 需要)"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def close(self):
|
|
30
|
+
"""物理关闭"""
|
|
31
|
+
pass
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
客户端数据连接管理器
|
|
3
|
+
|
|
4
|
+
管理多个 WebSocket 数据连接:
|
|
5
|
+
- 建立 N 个数据连接并加入 Session
|
|
6
|
+
- 根据 UID hash 选择数据连接发送数据
|
|
7
|
+
- 处理数据连接断开重连
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
import traceback
|
|
14
|
+
from typing import List, Optional, Callable
|
|
15
|
+
from urllib.parse import urlparse
|
|
16
|
+
|
|
17
|
+
from common import websocket
|
|
18
|
+
from common.logger_factory import LoggerFactory
|
|
19
|
+
from common.nat_serialization import NatSerialization
|
|
20
|
+
from constant.message_type_constnat import MessageTypeConstant
|
|
21
|
+
from context.context_utils import ContextUtils
|
|
22
|
+
from entity.message.message_entity import MessageEntity
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DataConnection:
|
|
26
|
+
"""单个数据连接"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, channel_index: int, url: str, session_token: str,
|
|
29
|
+
compress_support: bool, on_message_callback: Callable):
|
|
30
|
+
self.channel_index = channel_index
|
|
31
|
+
self.url = url
|
|
32
|
+
self.session_token = session_token
|
|
33
|
+
self.compress_support = compress_support
|
|
34
|
+
self.on_message_callback = on_message_callback
|
|
35
|
+
self.ws: Optional[websocket.WebSocketApp] = None
|
|
36
|
+
self.is_connected = False
|
|
37
|
+
self.is_joined = False
|
|
38
|
+
self.running = True
|
|
39
|
+
self.thread: Optional[threading.Thread] = None
|
|
40
|
+
|
|
41
|
+
def start(self):
|
|
42
|
+
"""启动数据连接"""
|
|
43
|
+
self.thread = threading.Thread(target=self._run, daemon=True)
|
|
44
|
+
self.thread.start()
|
|
45
|
+
|
|
46
|
+
def _run(self):
|
|
47
|
+
"""连接循环"""
|
|
48
|
+
while self.running:
|
|
49
|
+
try:
|
|
50
|
+
self.ws = websocket.WebSocketApp(
|
|
51
|
+
self.url,
|
|
52
|
+
on_open=self._on_open,
|
|
53
|
+
on_message=self._on_message,
|
|
54
|
+
on_close=self._on_close,
|
|
55
|
+
on_error=self._on_error
|
|
56
|
+
)
|
|
57
|
+
self.ws.run_forever()
|
|
58
|
+
except Exception as e:
|
|
59
|
+
LoggerFactory.get_logger().error(f'Data connection {self.channel_index} error: {e}')
|
|
60
|
+
|
|
61
|
+
if self.running:
|
|
62
|
+
LoggerFactory.get_logger().info(f'Data connection {self.channel_index} reconnecting in 2s...')
|
|
63
|
+
time.sleep(2)
|
|
64
|
+
|
|
65
|
+
def _on_open(self, ws):
|
|
66
|
+
"""连接建立后发送 JOIN_SESSION"""
|
|
67
|
+
LoggerFactory.get_logger().info(f'Data connection {self.channel_index} opened, sending JOIN_SESSION')
|
|
68
|
+
self.is_connected = True
|
|
69
|
+
|
|
70
|
+
# 发送 JOIN_SESSION 消息
|
|
71
|
+
join_message: MessageEntity = {
|
|
72
|
+
'type_': MessageTypeConstant.JOIN_SESSION,
|
|
73
|
+
'data': {
|
|
74
|
+
'session_token': self.session_token,
|
|
75
|
+
'channel_index': self.channel_index
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
try:
|
|
79
|
+
ws.send(
|
|
80
|
+
NatSerialization.dumps(join_message, ContextUtils.get_password(), self.compress_support),
|
|
81
|
+
websocket.ABNF.OPCODE_BINARY
|
|
82
|
+
)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
LoggerFactory.get_logger().error(f'Failed to send JOIN_SESSION: {e}')
|
|
85
|
+
|
|
86
|
+
def _on_message(self, ws, message: bytes):
|
|
87
|
+
"""处理收到的消息"""
|
|
88
|
+
try:
|
|
89
|
+
message_data: MessageEntity = NatSerialization.loads(
|
|
90
|
+
message, ContextUtils.get_password(), self.compress_support
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# 处理 JOIN_SESSION 响应
|
|
94
|
+
if message_data['type_'] == MessageTypeConstant.JOIN_SESSION:
|
|
95
|
+
data = message_data['data']
|
|
96
|
+
if data.get('success'):
|
|
97
|
+
self.is_joined = True
|
|
98
|
+
LoggerFactory.get_logger().info(
|
|
99
|
+
f'Data connection {self.channel_index} joined session successfully'
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
LoggerFactory.get_logger().error(
|
|
103
|
+
f'Data connection {self.channel_index} failed to join session'
|
|
104
|
+
)
|
|
105
|
+
ws.close()
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
# 其他消息转发给回调处理
|
|
109
|
+
if self.on_message_callback:
|
|
110
|
+
self.on_message_callback(message_data)
|
|
111
|
+
|
|
112
|
+
except Exception as e:
|
|
113
|
+
LoggerFactory.get_logger().error(f'Data connection {self.channel_index} message error: {e}')
|
|
114
|
+
LoggerFactory.get_logger().error(traceback.format_exc())
|
|
115
|
+
|
|
116
|
+
def _on_close(self, ws, code, reason):
|
|
117
|
+
"""连接关闭"""
|
|
118
|
+
LoggerFactory.get_logger().info(
|
|
119
|
+
f'Data connection {self.channel_index} closed: code={code}, reason={reason}'
|
|
120
|
+
)
|
|
121
|
+
self.is_connected = False
|
|
122
|
+
self.is_joined = False
|
|
123
|
+
|
|
124
|
+
def _on_error(self, ws, error):
|
|
125
|
+
"""连接错误"""
|
|
126
|
+
LoggerFactory.get_logger().error(f'Data connection {self.channel_index} error: {error}')
|
|
127
|
+
|
|
128
|
+
def send(self, data: bytes):
|
|
129
|
+
"""发送数据"""
|
|
130
|
+
if self.ws and self.is_joined:
|
|
131
|
+
try:
|
|
132
|
+
self.ws.send(data, websocket.ABNF.OPCODE_BINARY)
|
|
133
|
+
return True
|
|
134
|
+
except Exception as e:
|
|
135
|
+
LoggerFactory.get_logger().error(f'Data connection {self.channel_index} send error: {e}')
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def stop(self):
|
|
139
|
+
"""停止连接"""
|
|
140
|
+
self.running = False
|
|
141
|
+
self.is_connected = False
|
|
142
|
+
self.is_joined = False
|
|
143
|
+
if self.ws:
|
|
144
|
+
try:
|
|
145
|
+
self.ws.close()
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class DataConnectionManager:
|
|
151
|
+
"""数据连接管理器"""
|
|
152
|
+
|
|
153
|
+
def __init__(self, server_url: str, compress_support: bool, on_message_callback: Callable):
|
|
154
|
+
self.server_url = server_url
|
|
155
|
+
self.compress_support = compress_support
|
|
156
|
+
self.on_message_callback = on_message_callback
|
|
157
|
+
|
|
158
|
+
self.session_token: Optional[str] = None
|
|
159
|
+
self.num_channels: int = 4
|
|
160
|
+
self.data_connections: List[DataConnection] = []
|
|
161
|
+
self.is_multi_connection: bool = False
|
|
162
|
+
self.lock = threading.Lock()
|
|
163
|
+
|
|
164
|
+
def setup_data_connections(self, session_token: str, num_channels: int = 4):
|
|
165
|
+
"""
|
|
166
|
+
设置数据连接
|
|
167
|
+
|
|
168
|
+
:param session_token: 会话令牌(hex 格式)
|
|
169
|
+
:param num_channels: 数据通道数量
|
|
170
|
+
"""
|
|
171
|
+
with self.lock:
|
|
172
|
+
# 清理旧连接
|
|
173
|
+
self.stop_all()
|
|
174
|
+
|
|
175
|
+
self.session_token = session_token
|
|
176
|
+
self.num_channels = num_channels
|
|
177
|
+
self.is_multi_connection = True
|
|
178
|
+
|
|
179
|
+
LoggerFactory.get_logger().info(
|
|
180
|
+
f'Setting up {num_channels} data connections, token={session_token[:16]}...'
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# 创建数据连接
|
|
184
|
+
for i in range(num_channels):
|
|
185
|
+
conn = DataConnection(
|
|
186
|
+
channel_index=i,
|
|
187
|
+
url=self.server_url,
|
|
188
|
+
session_token=session_token,
|
|
189
|
+
compress_support=self.compress_support,
|
|
190
|
+
on_message_callback=self.on_message_callback
|
|
191
|
+
)
|
|
192
|
+
self.data_connections.append(conn)
|
|
193
|
+
conn.start()
|
|
194
|
+
|
|
195
|
+
LoggerFactory.get_logger().info(f'Started {num_channels} data connections')
|
|
196
|
+
|
|
197
|
+
def get_data_connection(self, uid: bytes) -> Optional[DataConnection]:
|
|
198
|
+
"""
|
|
199
|
+
根据 UID 获取数据连接
|
|
200
|
+
|
|
201
|
+
:param uid: 连接 UID
|
|
202
|
+
:return: 数据连接,如果没有可用的返回 None
|
|
203
|
+
"""
|
|
204
|
+
if not self.is_multi_connection or not self.data_connections:
|
|
205
|
+
return None
|
|
206
|
+
|
|
207
|
+
# 根据 UID hash 选择通道
|
|
208
|
+
channel_index = int.from_bytes(uid[:2], 'big') % len(self.data_connections)
|
|
209
|
+
conn = self.data_connections[channel_index]
|
|
210
|
+
|
|
211
|
+
# 检查连接是否可用
|
|
212
|
+
if conn.is_joined:
|
|
213
|
+
return conn
|
|
214
|
+
|
|
215
|
+
# 如果选中的通道不可用,尝试其他通道
|
|
216
|
+
for conn in self.data_connections:
|
|
217
|
+
if conn.is_joined:
|
|
218
|
+
return conn
|
|
219
|
+
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
def send_by_uid(self, uid: bytes, data: bytes) -> bool:
|
|
223
|
+
"""
|
|
224
|
+
根据 UID 发送数据
|
|
225
|
+
|
|
226
|
+
:param uid: 连接 UID
|
|
227
|
+
:param data: 要发送的数据
|
|
228
|
+
:return: 是否成功发送
|
|
229
|
+
"""
|
|
230
|
+
conn = self.get_data_connection(uid)
|
|
231
|
+
if conn:
|
|
232
|
+
return conn.send(data)
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
def is_ready(self) -> bool:
|
|
236
|
+
"""检查是否有数据连接可用"""
|
|
237
|
+
if not self.is_multi_connection:
|
|
238
|
+
return False
|
|
239
|
+
for conn in self.data_connections:
|
|
240
|
+
if conn.is_joined:
|
|
241
|
+
return True
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
def stop_all(self):
|
|
245
|
+
"""停止所有数据连接"""
|
|
246
|
+
for conn in self.data_connections:
|
|
247
|
+
conn.stop()
|
|
248
|
+
self.data_connections.clear()
|
|
249
|
+
self.is_multi_connection = False
|
|
250
|
+
self.session_token = None
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import threading
|
|
3
|
+
import socket
|
|
4
|
+
from .abstract_tunnel import AbstractTunnel
|
|
5
|
+
from common.kcp import Kcp
|
|
6
|
+
|
|
7
|
+
class KcpTunnelImpl(AbstractTunnel):
|
|
8
|
+
def __init__(self, *args, **kwargs):
|
|
9
|
+
super().__init__(*args, **kwargs)
|
|
10
|
+
self.kcp = Kcp(12345)
|
|
11
|
+
self.kcp.output = self._udp_output
|
|
12
|
+
self.kcp.setmtu(800) # 关键:强制小于 1000
|
|
13
|
+
self.kcp.nodelay(1, 10, 2, 1)
|
|
14
|
+
self.lock = threading.Lock()
|
|
15
|
+
self.running = True
|
|
16
|
+
|
|
17
|
+
def establish(self):
|
|
18
|
+
# KCP 是无状态的,打洞成功即认为建立成功
|
|
19
|
+
threading.Thread(target=self._recv_loop, daemon=True).start()
|
|
20
|
+
# 立即通知 Manager 成功
|
|
21
|
+
self.on_established(self.peer_name)
|
|
22
|
+
|
|
23
|
+
def _udp_output(self, buf):
|
|
24
|
+
try: self.sock.sendto(buf, self.addr)
|
|
25
|
+
except: pass
|
|
26
|
+
|
|
27
|
+
def _recv_loop(self):
|
|
28
|
+
while self.running:
|
|
29
|
+
try:
|
|
30
|
+
data, addr = self.sock.recvfrom(65536)
|
|
31
|
+
with self.lock:
|
|
32
|
+
self.kcp.input(data)
|
|
33
|
+
except: break
|
|
34
|
+
|
|
35
|
+
def update(self):
|
|
36
|
+
with self.lock:
|
|
37
|
+
self.kcp.update(int(time.time() * 1000))
|
|
38
|
+
while True:
|
|
39
|
+
data = self.kcp.recv()
|
|
40
|
+
if not data: break
|
|
41
|
+
if len(data) >= 4:
|
|
42
|
+
self.on_data_received(data[:4], data[4:])
|
|
43
|
+
|
|
44
|
+
def send(self, uid: bytes, data: bytes) -> bool:
|
|
45
|
+
with self.lock:
|
|
46
|
+
return self.kcp.send(uid + data) >= 0
|
|
47
|
+
|
|
48
|
+
def close(self):
|
|
49
|
+
self.running = False
|
|
50
|
+
try: self.sock.close()
|
|
51
|
+
except: pass
|
|
52
|
+
self.on_closed(self.peer_name)
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import socket
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from typing import Dict, Optional, Tuple, Type, Set
|
|
6
|
+
|
|
7
|
+
from common.logger_factory import LoggerFactory
|
|
8
|
+
from common.n4_punch import N4PunchClient, N4Error
|
|
9
|
+
from .abstract_tunnel import AbstractTunnel
|
|
10
|
+
from .kcp_tunnel_impl import KcpTunnelImpl
|
|
11
|
+
|
|
12
|
+
def get_pair_key(client_a: str, client_b: str) -> Tuple[str, str]:
|
|
13
|
+
"""获取两个客户端唯一的配对键"""
|
|
14
|
+
return (min(client_a, client_b), max(client_a, client_b))
|
|
15
|
+
|
|
16
|
+
class N4TunnelManager:
|
|
17
|
+
"""
|
|
18
|
+
P2P 隧道管理器
|
|
19
|
+
负责物理打洞的协调及抽象协议隧道的生命周期管理。
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, local_client_name: str, server_host: str, server_port: int,
|
|
22
|
+
tunnel_class: Type[AbstractTunnel] = KcpTunnelImpl):
|
|
23
|
+
|
|
24
|
+
self.local_client_name = local_client_name
|
|
25
|
+
self.server_host = server_host
|
|
26
|
+
self.server_port = server_port
|
|
27
|
+
|
|
28
|
+
# 核心:解耦后的协议实现类构造器
|
|
29
|
+
self.TunnelClass = tunnel_class
|
|
30
|
+
|
|
31
|
+
# 存储已建立的隧道对象 {peer_name: AbstractTunnel}
|
|
32
|
+
self.tunnels: Dict[str, AbstractTunnel] = {}
|
|
33
|
+
|
|
34
|
+
# 正在进行的物理打洞客户端 {pair_key: N4PunchClient}
|
|
35
|
+
self.punch_clients: Dict[Tuple[str, str], N4PunchClient] = {}
|
|
36
|
+
|
|
37
|
+
# 映射关系 {uid: peer_name}
|
|
38
|
+
self.uid_to_peer: Dict[bytes, str] = {}
|
|
39
|
+
|
|
40
|
+
# 状态控制
|
|
41
|
+
self.pending_punch_requests: Set[str] = set() # 正在申请打洞的
|
|
42
|
+
self.handshaking_peers: Set[str] = set() # 正在进行打洞或协议握手的
|
|
43
|
+
self.last_failure_time: Dict[str, float] = {} # 记录失败时间点,用于 30s 冷却
|
|
44
|
+
|
|
45
|
+
self.lock = threading.Lock()
|
|
46
|
+
self.logger = LoggerFactory.get_logger()
|
|
47
|
+
|
|
48
|
+
# 外部回调(由 run_client.py 注入)
|
|
49
|
+
self.ws_client = None
|
|
50
|
+
self.on_data_received = None # func(uid, data)
|
|
51
|
+
self.on_tunnel_closed = None # func(peer_name)
|
|
52
|
+
self.on_punch_failed = None
|
|
53
|
+
|
|
54
|
+
self.running = False
|
|
55
|
+
self.update_thread = None
|
|
56
|
+
|
|
57
|
+
def set_ws_client(self, ws):
|
|
58
|
+
self.ws_client = ws
|
|
59
|
+
|
|
60
|
+
def start(self):
|
|
61
|
+
"""启动管理器:启动全局驱动线程"""
|
|
62
|
+
with self.lock:
|
|
63
|
+
if not self.running:
|
|
64
|
+
self.running = True
|
|
65
|
+
self.update_thread = threading.Thread(target=self._global_update_loop, daemon=True)
|
|
66
|
+
self.update_thread.start()
|
|
67
|
+
self.logger.info(f"N4 Tunnel Manager Started (Protocol: {self.TunnelClass.__name__})")
|
|
68
|
+
|
|
69
|
+
def _global_update_loop(self):
|
|
70
|
+
"""每 10ms 驱动一次所有活跃协议的状态机"""
|
|
71
|
+
while self.running:
|
|
72
|
+
# 获取当前所有隧道的快照进行遍历
|
|
73
|
+
with self.lock:
|
|
74
|
+
active_tunnels = list(self.tunnels.values())
|
|
75
|
+
|
|
76
|
+
for tunnel in active_tunnels:
|
|
77
|
+
try:
|
|
78
|
+
tunnel.update()
|
|
79
|
+
except Exception as e:
|
|
80
|
+
self.logger.error(f"Error updating tunnel {tunnel.peer_name}: {e}")
|
|
81
|
+
|
|
82
|
+
time.sleep(0.01)
|
|
83
|
+
|
|
84
|
+
def send_data(self, uid: bytes, data: bytes) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
业务数据发送入口(无缝切换的核心)
|
|
87
|
+
返回 True: 数据已交由 P2P 隧道发送
|
|
88
|
+
返回 False: 隧道不可用,调用者应继续走 WebSocket
|
|
89
|
+
"""
|
|
90
|
+
peer_name = self.uid_to_peer.get(uid)
|
|
91
|
+
if not peer_name:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
with self.lock:
|
|
95
|
+
tunnel = self.tunnels.get(peer_name)
|
|
96
|
+
|
|
97
|
+
# 1. 隧道已就绪,直接发送
|
|
98
|
+
if tunnel:
|
|
99
|
+
if tunnel.send(uid, data):
|
|
100
|
+
return True
|
|
101
|
+
else:
|
|
102
|
+
self.logger.warn(f"P2P Tunnel to {peer_name} send failed, closing.")
|
|
103
|
+
self._handle_failure(peer_name)
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
# 2. 如果正在握手或打洞,静默走 WebSocket,不重复触发
|
|
107
|
+
if peer_name in self.pending_punch_requests or peer_name in self.handshaking_peers:
|
|
108
|
+
return False
|
|
109
|
+
|
|
110
|
+
# 3. 检查冷却时间:失败后 30s 内不重试打洞
|
|
111
|
+
now = time.time()
|
|
112
|
+
if now - self.last_failure_time.get(peer_name, 0) < 30:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
# 4. 既没有隧道也不在处理中,触发打洞请求
|
|
116
|
+
if self.ws_client:
|
|
117
|
+
self.logger.info(f"P2P required for {peer_name}. Initiating {self.TunnelClass.__name__} setup.")
|
|
118
|
+
with self.lock:
|
|
119
|
+
self.pending_punch_requests.add(peer_name)
|
|
120
|
+
self.ws_client._send_punch_request(peer_name)
|
|
121
|
+
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def prepare_punch(self, peer_name: str, session_id: bytes) -> Optional[socket.socket]:
|
|
125
|
+
"""收到 PUNCH_REQUEST:初始化 Socket 池并返回用于 EXCHANGE 的第一个 Socket"""
|
|
126
|
+
with self.lock:
|
|
127
|
+
# 如果旧隧道还存在,先物理关闭
|
|
128
|
+
if peer_name in self.tunnels:
|
|
129
|
+
self.tunnels[peer_name].close()
|
|
130
|
+
self.tunnels.pop(peer_name, None)
|
|
131
|
+
|
|
132
|
+
punch_client = N4PunchClient(
|
|
133
|
+
ident=session_id,
|
|
134
|
+
server_host=self.server_host,
|
|
135
|
+
server_port=self.server_port
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
exchange_sock = punch_client.send_exchange()
|
|
140
|
+
pair_key = get_pair_key(self.local_client_name, peer_name)
|
|
141
|
+
self.punch_clients[pair_key] = punch_client
|
|
142
|
+
return exchange_sock
|
|
143
|
+
except Exception as e:
|
|
144
|
+
self.logger.error(f"Failed to prepare punch for {peer_name}: {e}")
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def receive_peer_info(self, peer_name: str, peer_ip: str, peer_port: int):
|
|
148
|
+
"""收到 PEER_INFO:后台启动物理打洞并初始化具体协议实现"""
|
|
149
|
+
pair_key = get_pair_key(self.local_client_name, peer_name)
|
|
150
|
+
punch_client = self.punch_clients.get(pair_key)
|
|
151
|
+
|
|
152
|
+
if punch_client:
|
|
153
|
+
threading.Thread(
|
|
154
|
+
target=self._run_punch_and_setup,
|
|
155
|
+
args=(punch_client, peer_name, peer_ip, peer_port, pair_key),
|
|
156
|
+
daemon=True
|
|
157
|
+
).start()
|
|
158
|
+
|
|
159
|
+
def _run_punch_and_setup(self, punch_client: N4PunchClient, peer_name: str,
|
|
160
|
+
peer_ip: str, peer_port: int, pair_key: Tuple[str, str]):
|
|
161
|
+
"""后台线程:执行打洞 -> 实例化 TunnelImpl -> 建立握手"""
|
|
162
|
+
with self.lock:
|
|
163
|
+
self.handshaking_peers.add(peer_name)
|
|
164
|
+
self.pending_punch_requests.discard(peer_name)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
# 1. 物理打洞
|
|
168
|
+
winner_sock, peer_addr = punch_client.punch(peer_ip, peer_port, wait=15)
|
|
169
|
+
self.logger.info(f"Punch SUCCESS for {peer_name}. Setting up {self.TunnelClass.__name__}...")
|
|
170
|
+
|
|
171
|
+
# 2. 确定角色
|
|
172
|
+
is_server = self.local_client_name > peer_name
|
|
173
|
+
|
|
174
|
+
# 3. 实例化具体的隧道协议实现
|
|
175
|
+
tunnel = self.TunnelClass(
|
|
176
|
+
peer_name=peer_name,
|
|
177
|
+
sock=winner_sock,
|
|
178
|
+
addr=peer_addr,
|
|
179
|
+
on_data_received=self.on_data_received,
|
|
180
|
+
on_established=self._internal_on_established,
|
|
181
|
+
on_closed=self._internal_on_closed,
|
|
182
|
+
is_server=is_server
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# 4. 开始协议激活
|
|
186
|
+
tunnel.establish()
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
self.logger.error(f"P2P setup failed for {peer_name}: {e}")
|
|
190
|
+
self._handle_failure(peer_name)
|
|
191
|
+
if self.on_punch_failed:
|
|
192
|
+
self.on_punch_failed(peer_name)
|
|
193
|
+
finally:
|
|
194
|
+
with self.lock:
|
|
195
|
+
self.punch_clients.pop(pair_key, None)
|
|
196
|
+
|
|
197
|
+
def _internal_on_established(self, tunnel: AbstractTunnel):
|
|
198
|
+
"""由具体 TunnelImpl 在协议握手成功后调用"""
|
|
199
|
+
peer_name = tunnel.peer_name
|
|
200
|
+
with self.lock:
|
|
201
|
+
self.tunnels[peer_name] = tunnel
|
|
202
|
+
self.handshaking_peers.discard(peer_name)
|
|
203
|
+
self.last_failure_time.pop(peer_name, None)
|
|
204
|
+
self.logger.info(f"=== [P2P READY] {self.TunnelClass.__name__} Tunnel Established for {peer_name} ===")
|
|
205
|
+
|
|
206
|
+
def _internal_on_closed(self, peer_name: str):
|
|
207
|
+
"""由具体 TunnelImpl 在连接断开时调用"""
|
|
208
|
+
self._handle_failure(peer_name)
|
|
209
|
+
if self.on_tunnel_closed:
|
|
210
|
+
self.on_tunnel_closed(peer_name)
|
|
211
|
+
|
|
212
|
+
def _handle_failure(self, peer_name: str):
|
|
213
|
+
"""统一失败处理逻辑"""
|
|
214
|
+
with self.lock:
|
|
215
|
+
self.tunnels.pop(peer_name, None)
|
|
216
|
+
self.handshaking_peers.discard(peer_name)
|
|
217
|
+
self.pending_punch_requests.discard(peer_name)
|
|
218
|
+
self.last_failure_time[peer_name] = time.time()
|
|
219
|
+
|
|
220
|
+
def register_uid(self, uid: bytes, peer_name: str):
|
|
221
|
+
with self.lock:
|
|
222
|
+
self.uid_to_peer[uid] = peer_name
|
|
223
|
+
|
|
224
|
+
def unregister_uid(self, uid: bytes):
|
|
225
|
+
with self.lock:
|
|
226
|
+
self.uid_to_peer.pop(uid, None)
|
|
227
|
+
|
|
228
|
+
def is_tunnel_established(self, peer_name: str) -> bool:
|
|
229
|
+
return peer_name in self.tunnels
|
|
230
|
+
|
|
231
|
+
def has_tunnel(self, peer_name: str) -> bool:
|
|
232
|
+
return peer_name in self.tunnels or peer_name in self.pending_punch_requests or peer_name in self.handshaking_peers
|
|
233
|
+
|
|
234
|
+
def get_tunnel(self, peer_name: str) -> Optional[AbstractTunnel]:
|
|
235
|
+
return self.tunnels.get(peer_name)
|
|
236
|
+
|
|
237
|
+
def stop(self):
|
|
238
|
+
"""停止管理器及所有隧道"""
|
|
239
|
+
self.running = False
|
|
240
|
+
with self.lock:
|
|
241
|
+
for tunnel in list(self.tunnels.values()):
|
|
242
|
+
tunnel.close()
|
|
243
|
+
for puncher in list(self.punch_clients.values()):
|
|
244
|
+
puncher.stop()
|
|
245
|
+
self.tunnels.clear()
|
|
246
|
+
self.punch_clients.clear()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from .abstract_tunnel import AbstractTunnel
|
|
3
|
+
|
|
4
|
+
class QuicTunnelImpl(AbstractTunnel):
|
|
5
|
+
def establish(self):
|
|
6
|
+
# 这里封装之前 N4TunnelManager 里的 _start_quic_connection 逻辑
|
|
7
|
+
# 调用 self.on_established 或 self.on_closed
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
def send(self, uid: bytes, data: bytes) -> bool:
|
|
11
|
+
# 之前的 aioquic send 逻辑
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
def update(self): pass
|
|
15
|
+
|
|
16
|
+
def close(self):
|
|
17
|
+
# 之前的 close 逻辑
|
|
18
|
+
pass
|