block-proxy 0.1.15 → 0.1.16
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.
- package/.claude/settings.local.json +88 -34
- package/.claude/skills/icon_generate/skill.md +45 -0
- package/CLAUDE.md +3 -3
- package/README.md +28 -187
- package/client/app.py +29 -9
- package/client/build.sh +6 -0
- package/client/config.py +1 -0
- package/client/config_window.py +36 -26
- package/client/icons/app.icns +0 -0
- package/client/icons/christmas-sock_light.png +0 -0
- package/client/icons/christmas-sock_light_bar.png +0 -0
- package/client/icons/christmas-sock_light_bar_off.png +0 -0
- package/client/icons/socks_on_G.png +0 -0
- package/client/icons/socks_on_G_bar.png +0 -0
- package/client/icons/socks_on_M.png +0 -0
- package/client/icons/socks_on_M_bar.png +0 -0
- package/client/proxy_core.py +305 -21
- package/client/tests/test_proxy_core_udp.py +466 -0
- package/package.json +1 -1
- package/socks5/server.js +74 -167
- package/wiki.md +287 -0
- package/client/icons/backup/app_example.png +0 -0
- package/client/icons/backup/christmas-sock_dark.png +0 -0
- package/client/icons/backup/christmas-sock_light.png +0 -0
- package/client/icons/backup/socks_on_G.png +0 -0
- package/client/icons/backup/socks_on_M.png +0 -0
- package/client/icons/christmas-sock_dark.png +0 -0
package/client/config_window.py
CHANGED
|
@@ -39,12 +39,13 @@ def show_config_window(config_path):
|
|
|
39
39
|
config = json.load(f)
|
|
40
40
|
|
|
41
41
|
def save_and_close():
|
|
42
|
+
config["server"]["protocol"] = protocol_var.get()
|
|
42
43
|
config["server"]["address"] = entries["address"].get()
|
|
43
44
|
config["server"]["port"] = int(entries["port"].get())
|
|
44
45
|
config["server"]["username"] = entries["username"].get()
|
|
45
46
|
config["server"]["password"] = entries["password"].get()
|
|
46
47
|
config["server"]["tls"] = tls_var.get()
|
|
47
|
-
config["server"]["allowInsecure"] = insecure_var.get()
|
|
48
|
+
config["server"]["allowInsecure"] = insecure_var.get()
|
|
48
49
|
config["local"]["socks_port"] = int(entries["socks_port"].get())
|
|
49
50
|
config["local"]["http_port"] = int(entries["http_port"].get())
|
|
50
51
|
config["local"]["udp"] = udp_var.get()
|
|
@@ -55,12 +56,12 @@ def show_config_window(config_path):
|
|
|
55
56
|
json.dump(config, f, indent=2)
|
|
56
57
|
root.destroy()
|
|
57
58
|
|
|
58
|
-
pos = _center_on_mouse_screen(400,
|
|
59
|
+
pos = _center_on_mouse_screen(400, 490)
|
|
59
60
|
|
|
60
61
|
root = tk.Tk()
|
|
61
|
-
root.title("
|
|
62
|
+
root.title("节点配置")
|
|
62
63
|
root.resizable(False, False)
|
|
63
|
-
w, h = 400,
|
|
64
|
+
w, h = 400, 490
|
|
64
65
|
if pos:
|
|
65
66
|
x, y = pos
|
|
66
67
|
else:
|
|
@@ -76,6 +77,16 @@ def show_config_window(config_path):
|
|
|
76
77
|
frame.grid_columnconfigure(1, weight=1)
|
|
77
78
|
|
|
78
79
|
entries = {}
|
|
80
|
+
row = 0
|
|
81
|
+
|
|
82
|
+
ttk.Label(frame, text="协议:").grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
|
|
83
|
+
protocol_var = tk.StringVar(value=config["server"].get("protocol", "socks5"))
|
|
84
|
+
protocol_combo = ttk.Combobox(
|
|
85
|
+
frame, textvariable=protocol_var, values=["socks5", "http"], state="readonly", width=10
|
|
86
|
+
)
|
|
87
|
+
protocol_combo.grid(row=row, column=1, sticky="w", pady=4)
|
|
88
|
+
row += 1
|
|
89
|
+
|
|
79
90
|
fields = [
|
|
80
91
|
("address", "地址:", config["server"]["address"]),
|
|
81
92
|
("port", "端口:", str(config["server"]["port"])),
|
|
@@ -85,44 +96,43 @@ def show_config_window(config_path):
|
|
|
85
96
|
("http_port", "本地HTTP端口:", str(config["local"]["http_port"])),
|
|
86
97
|
]
|
|
87
98
|
|
|
88
|
-
for
|
|
89
|
-
ttk.Label(frame, text=label).grid(row=
|
|
99
|
+
for key, label, default in fields:
|
|
100
|
+
ttk.Label(frame, text=label).grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
|
|
90
101
|
entry = ttk.Entry(frame)
|
|
91
102
|
entry.insert(0, default)
|
|
92
|
-
entry.grid(row=
|
|
103
|
+
entry.grid(row=row, column=1, sticky="ew", pady=4)
|
|
93
104
|
entries[key] = entry
|
|
94
|
-
|
|
95
|
-
row = len(fields)
|
|
105
|
+
row += 1
|
|
96
106
|
|
|
97
107
|
tls_var = tk.BooleanVar(value=config["server"]["tls"])
|
|
98
108
|
ttk.Label(frame, text="启用 TLS:").grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
|
|
99
|
-
ttk.
|
|
100
|
-
|
|
101
|
-
)
|
|
109
|
+
tls_frame = ttk.Frame(frame)
|
|
110
|
+
tls_frame.grid(row=row, column=1, sticky="w", pady=4)
|
|
111
|
+
ttk.Checkbutton(tls_frame, variable=tls_var).pack(side="left")
|
|
112
|
+
ttk.Label(tls_frame, text="(需节点服务器支持)", foreground="gray").pack(side="left")
|
|
102
113
|
row += 1
|
|
103
114
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
111
|
-
insecure_combo.grid(row=row, column=1, sticky="w", pady=4)
|
|
115
|
+
insecure_var = tk.BooleanVar(value=config["server"]["allowInsecure"])
|
|
116
|
+
ttk.Label(frame, text="允许不安全连接:").grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
|
|
117
|
+
insecure_frame = ttk.Frame(frame)
|
|
118
|
+
insecure_frame.grid(row=row, column=1, sticky="w", pady=4)
|
|
119
|
+
ttk.Checkbutton(insecure_frame, variable=insecure_var).pack(side="left")
|
|
120
|
+
ttk.Label(insecure_frame, text="(跳过证书验证)", foreground="gray").pack(side="left")
|
|
112
121
|
row += 1
|
|
113
122
|
|
|
114
123
|
udp_var = tk.BooleanVar(value=config["local"]["udp"])
|
|
115
124
|
ttk.Label(frame, text="启用 UDP:").grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
|
|
116
|
-
ttk.
|
|
117
|
-
|
|
118
|
-
)
|
|
125
|
+
udp_frame = ttk.Frame(frame)
|
|
126
|
+
udp_frame.grid(row=row, column=1, sticky="w", pady=4)
|
|
127
|
+
ttk.Checkbutton(udp_frame, variable=udp_var).pack(side="left")
|
|
119
128
|
row += 1
|
|
120
129
|
|
|
121
130
|
proxy_private_var = tk.BooleanVar(value=config["local"].get("proxy_private", False))
|
|
122
131
|
ttk.Label(frame, text="代理私有地址段:").grid(row=row, column=0, sticky="w", pady=4, padx=(0, 8))
|
|
123
|
-
ttk.
|
|
124
|
-
|
|
125
|
-
)
|
|
132
|
+
private_frame = ttk.Frame(frame)
|
|
133
|
+
private_frame.grid(row=row, column=1, sticky="w", pady=4)
|
|
134
|
+
ttk.Checkbutton(private_frame, variable=proxy_private_var).pack(side="left")
|
|
135
|
+
ttk.Label(private_frame, text="(192.168.x / 172.16.x / 10.x)", foreground="gray").pack(side="left")
|
|
126
136
|
row += 1
|
|
127
137
|
|
|
128
138
|
ttk.Separator(frame, orient="horizontal").grid(
|
package/client/icons/app.icns
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/client/proxy_core.py
CHANGED
|
@@ -29,6 +29,21 @@ def is_private_ip(host):
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
RELAY_IDLE_TIMEOUT = 300
|
|
32
|
+
UDP_IDLE_TIMEOUT = 120
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def write_udp_frame(writer, data):
|
|
36
|
+
frame = struct.pack("!H", len(data)) + data
|
|
37
|
+
writer.write(frame)
|
|
38
|
+
await writer.drain()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def read_udp_frame(reader):
|
|
42
|
+
length_data = await reader.readexactly(2)
|
|
43
|
+
length = struct.unpack("!H", length_data)[0]
|
|
44
|
+
if length == 0 or length > 65535:
|
|
45
|
+
raise Exception("invalid UDP frame length")
|
|
46
|
+
return await reader.readexactly(length)
|
|
32
47
|
|
|
33
48
|
|
|
34
49
|
async def relay(reader, writer):
|
|
@@ -57,6 +72,7 @@ async def relay(reader, writer):
|
|
|
57
72
|
|
|
58
73
|
CONNECT_TIMEOUT = 10
|
|
59
74
|
HANDSHAKE_TIMEOUT = 10
|
|
75
|
+
LOCAL_HANDSHAKE_TIMEOUT = 30
|
|
60
76
|
|
|
61
77
|
|
|
62
78
|
async def connect_upstream_socks5(server_config, dest_addr, dest_port, ssl_ctx=None):
|
|
@@ -129,7 +145,138 @@ async def connect_upstream_socks5(server_config, dest_addr, dest_port, ssl_ctx=N
|
|
|
129
145
|
elif reply[3] == 0x04:
|
|
130
146
|
await reader.readexactly(16 + 2)
|
|
131
147
|
|
|
132
|
-
|
|
148
|
+
try:
|
|
149
|
+
await asyncio.wait_for(_handshake(), timeout=HANDSHAKE_TIMEOUT)
|
|
150
|
+
except Exception:
|
|
151
|
+
writer.close()
|
|
152
|
+
try:
|
|
153
|
+
await writer.wait_closed()
|
|
154
|
+
except OSError:
|
|
155
|
+
pass
|
|
156
|
+
raise
|
|
157
|
+
|
|
158
|
+
return reader, writer
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def connect_upstream_http(server_config, dest_addr, dest_port, ssl_ctx=None):
|
|
162
|
+
host = server_config["address"]
|
|
163
|
+
port = server_config["port"]
|
|
164
|
+
username = server_config["username"]
|
|
165
|
+
password = server_config["password"]
|
|
166
|
+
use_tls = server_config["tls"]
|
|
167
|
+
|
|
168
|
+
reader, writer = await asyncio.wait_for(
|
|
169
|
+
asyncio.open_connection(
|
|
170
|
+
host, port, ssl=ssl_ctx if use_tls else None,
|
|
171
|
+
server_hostname=host if use_tls else None,
|
|
172
|
+
),
|
|
173
|
+
timeout=CONNECT_TIMEOUT,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
async def _handshake():
|
|
177
|
+
target = f"{dest_addr}:{dest_port}"
|
|
178
|
+
lines = [f"CONNECT {target} HTTP/1.1", f"Host: {target}"]
|
|
179
|
+
if username and password:
|
|
180
|
+
import base64
|
|
181
|
+
cred = base64.b64encode(f"{username}:{password}".encode()).decode()
|
|
182
|
+
lines.append(f"Proxy-Authorization: Basic {cred}")
|
|
183
|
+
lines.append("")
|
|
184
|
+
lines.append("")
|
|
185
|
+
writer.write("\r\n".join(lines).encode())
|
|
186
|
+
await writer.drain()
|
|
187
|
+
|
|
188
|
+
status_line = await reader.readline()
|
|
189
|
+
if not status_line:
|
|
190
|
+
raise Exception("HTTP proxy closed connection")
|
|
191
|
+
parts = status_line.decode().split(" ", 2)
|
|
192
|
+
if len(parts) < 2 or not parts[1].startswith("2"):
|
|
193
|
+
raise Exception(f"HTTP proxy CONNECT failed: {status_line.decode().strip()}")
|
|
194
|
+
while True:
|
|
195
|
+
line = await reader.readline()
|
|
196
|
+
if line in (b"\r\n", b"\n", b""):
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
await asyncio.wait_for(_handshake(), timeout=HANDSHAKE_TIMEOUT)
|
|
201
|
+
except Exception:
|
|
202
|
+
writer.close()
|
|
203
|
+
try:
|
|
204
|
+
await writer.wait_closed()
|
|
205
|
+
except OSError:
|
|
206
|
+
pass
|
|
207
|
+
raise
|
|
208
|
+
|
|
209
|
+
return reader, writer
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
async def connect_upstream_udp_associate(server_config, ssl_ctx=None):
|
|
213
|
+
host = server_config["address"]
|
|
214
|
+
port = server_config["port"]
|
|
215
|
+
username = server_config["username"]
|
|
216
|
+
password = server_config["password"]
|
|
217
|
+
use_tls = server_config["tls"]
|
|
218
|
+
|
|
219
|
+
reader, writer = await asyncio.wait_for(
|
|
220
|
+
asyncio.open_connection(
|
|
221
|
+
host, port, ssl=ssl_ctx if use_tls else None,
|
|
222
|
+
server_hostname=host if use_tls else None,
|
|
223
|
+
),
|
|
224
|
+
timeout=CONNECT_TIMEOUT,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
async def _handshake():
|
|
228
|
+
if username and password:
|
|
229
|
+
writer.write(b"\x05\x01\x02")
|
|
230
|
+
else:
|
|
231
|
+
writer.write(b"\x05\x01\x00")
|
|
232
|
+
await writer.drain()
|
|
233
|
+
|
|
234
|
+
resp = await reader.readexactly(2)
|
|
235
|
+
if resp[0] != 0x05:
|
|
236
|
+
raise Exception("SOCKS5 version mismatch")
|
|
237
|
+
|
|
238
|
+
if resp[1] == 0x02:
|
|
239
|
+
uname = username.encode("utf-8")
|
|
240
|
+
passwd = password.encode("utf-8")
|
|
241
|
+
writer.write(
|
|
242
|
+
b"\x01"
|
|
243
|
+
+ struct.pack("B", len(uname))
|
|
244
|
+
+ uname
|
|
245
|
+
+ struct.pack("B", len(passwd))
|
|
246
|
+
+ passwd
|
|
247
|
+
)
|
|
248
|
+
await writer.drain()
|
|
249
|
+
auth_resp = await reader.readexactly(2)
|
|
250
|
+
if auth_resp[1] != 0x00:
|
|
251
|
+
raise Exception("SOCKS5 auth failed")
|
|
252
|
+
elif resp[1] == 0xFF:
|
|
253
|
+
raise Exception("SOCKS5 no acceptable auth method")
|
|
254
|
+
|
|
255
|
+
# CMD=0x03 UDP ASSOCIATE, DST.ADDR=0.0.0.0:0
|
|
256
|
+
writer.write(b"\x05\x03\x00\x01" + b"\x00" * 4 + b"\x00\x00")
|
|
257
|
+
await writer.drain()
|
|
258
|
+
|
|
259
|
+
reply = await reader.readexactly(4)
|
|
260
|
+
if reply[1] != 0x00:
|
|
261
|
+
raise Exception(f"SOCKS5 UDP ASSOCIATE failed: {reply[1]:#x}")
|
|
262
|
+
|
|
263
|
+
if reply[3] == 0x01:
|
|
264
|
+
await reader.readexactly(4 + 2)
|
|
265
|
+
elif reply[3] == 0x03:
|
|
266
|
+
length = (await reader.readexactly(1))[0]
|
|
267
|
+
await reader.readexactly(length + 2)
|
|
268
|
+
elif reply[3] == 0x04:
|
|
269
|
+
await reader.readexactly(16 + 2)
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
await asyncio.wait_for(_handshake(), timeout=HANDSHAKE_TIMEOUT)
|
|
273
|
+
except Exception:
|
|
274
|
+
writer.close()
|
|
275
|
+
try:
|
|
276
|
+
await writer.wait_closed()
|
|
277
|
+
except OSError:
|
|
278
|
+
pass
|
|
279
|
+
raise
|
|
133
280
|
|
|
134
281
|
return reader, writer
|
|
135
282
|
|
|
@@ -141,6 +288,24 @@ async def connect_direct(dest_addr, dest_port):
|
|
|
141
288
|
MAX_CONCURRENT = 256
|
|
142
289
|
|
|
143
290
|
|
|
291
|
+
class _UdpRelayProtocol(asyncio.DatagramProtocol):
|
|
292
|
+
def __init__(self, tcp_writer, loop):
|
|
293
|
+
self._tcp_writer = tcp_writer
|
|
294
|
+
self._loop = loop
|
|
295
|
+
self.client_addr = None
|
|
296
|
+
self.transport = None
|
|
297
|
+
|
|
298
|
+
def connection_made(self, transport):
|
|
299
|
+
self.transport = transport
|
|
300
|
+
|
|
301
|
+
def datagram_received(self, data, addr):
|
|
302
|
+
self.client_addr = addr
|
|
303
|
+
if self._tcp_writer.is_closing():
|
|
304
|
+
return
|
|
305
|
+
frame = struct.pack("!H", len(data)) + data
|
|
306
|
+
self._tcp_writer.write(frame)
|
|
307
|
+
|
|
308
|
+
|
|
144
309
|
class ProxyCore:
|
|
145
310
|
def __init__(self):
|
|
146
311
|
self._loop = None
|
|
@@ -150,6 +315,7 @@ class ProxyCore:
|
|
|
150
315
|
self._running = False
|
|
151
316
|
self._server_config = None
|
|
152
317
|
self._proxy_private = False
|
|
318
|
+
self._udp_enabled = True
|
|
153
319
|
self._socks_port = 1080
|
|
154
320
|
self._http_port = 1087
|
|
155
321
|
self._ssl_ctx = None
|
|
@@ -175,6 +341,7 @@ class ProxyCore:
|
|
|
175
341
|
self._socks_port = local["socks_port"]
|
|
176
342
|
self._http_port = local["http_port"]
|
|
177
343
|
self._proxy_private = local.get("proxy_private", False)
|
|
344
|
+
self._udp_enabled = local.get("udp", True)
|
|
178
345
|
self._build_ssl_context()
|
|
179
346
|
|
|
180
347
|
self._loop = asyncio.new_event_loop()
|
|
@@ -202,15 +369,25 @@ class ProxyCore:
|
|
|
202
369
|
self._loop.stop()
|
|
203
370
|
self._loop.call_soon_threadsafe(_shutdown)
|
|
204
371
|
if self._thread:
|
|
205
|
-
self._thread.join(timeout=
|
|
206
|
-
|
|
207
|
-
|
|
372
|
+
self._thread.join(timeout=5)
|
|
373
|
+
if not self._thread.is_alive() and self._loop:
|
|
374
|
+
self._loop.close()
|
|
375
|
+
elif self._loop:
|
|
376
|
+
logger.warning("proxy thread did not exit in time, skipping loop.close()")
|
|
208
377
|
self._loop = None
|
|
209
378
|
self._thread = None
|
|
210
379
|
|
|
211
380
|
def is_running(self):
|
|
212
381
|
return self._running and self._thread is not None and self._thread.is_alive()
|
|
213
382
|
|
|
383
|
+
@property
|
|
384
|
+
def socks_port(self):
|
|
385
|
+
return self._socks_port
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def http_port(self):
|
|
389
|
+
return self._http_port
|
|
390
|
+
|
|
214
391
|
async def _measure_latency(self):
|
|
215
392
|
import time
|
|
216
393
|
start = time.monotonic()
|
|
@@ -280,12 +457,26 @@ class ProxyCore:
|
|
|
280
457
|
|
|
281
458
|
async def _start_servers(self):
|
|
282
459
|
self._semaphore = asyncio.Semaphore(MAX_CONCURRENT)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
460
|
+
max_attempts = 100
|
|
461
|
+
for attempt in range(max_attempts):
|
|
462
|
+
try:
|
|
463
|
+
self._socks_server = await asyncio.start_server(
|
|
464
|
+
self._handle_socks, "127.0.0.1", self._socks_port
|
|
465
|
+
)
|
|
466
|
+
self._http_server = await asyncio.start_server(
|
|
467
|
+
self._handle_http, "127.0.0.1", self._http_port
|
|
468
|
+
)
|
|
469
|
+
return
|
|
470
|
+
except OSError as e:
|
|
471
|
+
if e.errno == 48 and attempt < max_attempts - 1:
|
|
472
|
+
if self._socks_server:
|
|
473
|
+
self._socks_server.close()
|
|
474
|
+
await self._socks_server.wait_closed()
|
|
475
|
+
self._socks_server = None
|
|
476
|
+
self._socks_port += 1
|
|
477
|
+
self._http_port += 1
|
|
478
|
+
else:
|
|
479
|
+
raise
|
|
289
480
|
|
|
290
481
|
async def _stop_servers(self):
|
|
291
482
|
if self._socks_server:
|
|
@@ -303,6 +494,11 @@ class ProxyCore:
|
|
|
303
494
|
async def _connect_target(self, dest_addr, dest_port):
|
|
304
495
|
if self._should_direct(dest_addr):
|
|
305
496
|
return await connect_direct(dest_addr, dest_port)
|
|
497
|
+
protocol = self._server_config.get("protocol", "socks5")
|
|
498
|
+
if protocol == "http":
|
|
499
|
+
return await connect_upstream_http(
|
|
500
|
+
self._server_config, dest_addr, dest_port, ssl_ctx=self._ssl_ctx
|
|
501
|
+
)
|
|
306
502
|
return await connect_upstream_socks5(
|
|
307
503
|
self._server_config, dest_addr, dest_port, ssl_ctx=self._ssl_ctx
|
|
308
504
|
)
|
|
@@ -313,19 +509,23 @@ class ProxyCore:
|
|
|
313
509
|
|
|
314
510
|
async def _do_handle_socks(self, client_reader, client_writer):
|
|
315
511
|
try:
|
|
316
|
-
header = await client_reader.readexactly(2)
|
|
512
|
+
header = await asyncio.wait_for(client_reader.readexactly(2), timeout=LOCAL_HANDSHAKE_TIMEOUT)
|
|
317
513
|
ver, nmethods = header
|
|
318
514
|
if ver != 0x05:
|
|
319
515
|
client_writer.close()
|
|
320
516
|
return
|
|
321
|
-
await client_reader.readexactly(nmethods)
|
|
517
|
+
await asyncio.wait_for(client_reader.readexactly(nmethods), timeout=LOCAL_HANDSHAKE_TIMEOUT)
|
|
322
518
|
|
|
323
519
|
client_writer.write(b"\x05\x00")
|
|
324
520
|
await client_writer.drain()
|
|
325
521
|
|
|
326
|
-
req = await client_reader.readexactly(4)
|
|
522
|
+
req = await asyncio.wait_for(client_reader.readexactly(4), timeout=LOCAL_HANDSHAKE_TIMEOUT)
|
|
327
523
|
ver, cmd, _, atyp = req
|
|
328
524
|
|
|
525
|
+
if cmd == 0x03:
|
|
526
|
+
await self._handle_udp_associate(client_reader, client_writer, atyp)
|
|
527
|
+
return
|
|
528
|
+
|
|
329
529
|
if cmd != 0x01:
|
|
330
530
|
client_writer.write(
|
|
331
531
|
b"\x05\x07\x00\x01" + b"\x00" * 4 + b"\x00\x00"
|
|
@@ -335,13 +535,13 @@ class ProxyCore:
|
|
|
335
535
|
return
|
|
336
536
|
|
|
337
537
|
if atyp == 0x01:
|
|
338
|
-
raw = await client_reader.readexactly(4)
|
|
538
|
+
raw = await asyncio.wait_for(client_reader.readexactly(4), timeout=LOCAL_HANDSHAKE_TIMEOUT)
|
|
339
539
|
dest_addr = str(ipaddress.IPv4Address(raw))
|
|
340
540
|
elif atyp == 0x03:
|
|
341
|
-
length = (await client_reader.readexactly(1))[0]
|
|
342
|
-
dest_addr = (await client_reader.readexactly(length)).decode("utf-8")
|
|
541
|
+
length = (await asyncio.wait_for(client_reader.readexactly(1), timeout=LOCAL_HANDSHAKE_TIMEOUT))[0]
|
|
542
|
+
dest_addr = (await asyncio.wait_for(client_reader.readexactly(length), timeout=LOCAL_HANDSHAKE_TIMEOUT)).decode("utf-8")
|
|
343
543
|
elif atyp == 0x04:
|
|
344
|
-
raw = await client_reader.readexactly(16)
|
|
544
|
+
raw = await asyncio.wait_for(client_reader.readexactly(16), timeout=LOCAL_HANDSHAKE_TIMEOUT)
|
|
345
545
|
dest_addr = str(ipaddress.IPv6Address(raw))
|
|
346
546
|
else:
|
|
347
547
|
client_writer.write(
|
|
@@ -351,7 +551,7 @@ class ProxyCore:
|
|
|
351
551
|
client_writer.close()
|
|
352
552
|
return
|
|
353
553
|
|
|
354
|
-
port_data = await client_reader.readexactly(2)
|
|
554
|
+
port_data = await asyncio.wait_for(client_reader.readexactly(2), timeout=LOCAL_HANDSHAKE_TIMEOUT)
|
|
355
555
|
dest_port = struct.unpack("!H", port_data)[0]
|
|
356
556
|
|
|
357
557
|
remote_reader, remote_writer = await self._connect_target(
|
|
@@ -376,13 +576,97 @@ class ProxyCore:
|
|
|
376
576
|
except OSError:
|
|
377
577
|
pass
|
|
378
578
|
|
|
579
|
+
async def _handle_udp_associate(self, client_reader, client_writer, atyp):
|
|
580
|
+
# 消费掉请求中剩余的地址和端口字段
|
|
581
|
+
try:
|
|
582
|
+
if atyp == 0x01:
|
|
583
|
+
await asyncio.wait_for(client_reader.readexactly(4 + 2), timeout=LOCAL_HANDSHAKE_TIMEOUT)
|
|
584
|
+
elif atyp == 0x03:
|
|
585
|
+
length = (await asyncio.wait_for(client_reader.readexactly(1), timeout=LOCAL_HANDSHAKE_TIMEOUT))[0]
|
|
586
|
+
await asyncio.wait_for(client_reader.readexactly(length + 2), timeout=LOCAL_HANDSHAKE_TIMEOUT)
|
|
587
|
+
elif atyp == 0x04:
|
|
588
|
+
await asyncio.wait_for(client_reader.readexactly(16 + 2), timeout=LOCAL_HANDSHAKE_TIMEOUT)
|
|
589
|
+
except Exception:
|
|
590
|
+
client_writer.close()
|
|
591
|
+
return
|
|
592
|
+
|
|
593
|
+
protocol = self._server_config.get("protocol", "socks5")
|
|
594
|
+
udp_enabled = getattr(self, "_udp_enabled", True)
|
|
595
|
+
if protocol != "socks5" or not udp_enabled:
|
|
596
|
+
client_writer.write(b"\x05\x07\x00\x01" + b"\x00" * 4 + b"\x00\x00")
|
|
597
|
+
await client_writer.drain()
|
|
598
|
+
client_writer.close()
|
|
599
|
+
return
|
|
600
|
+
|
|
601
|
+
try:
|
|
602
|
+
remote_reader, remote_writer = await connect_upstream_udp_associate(
|
|
603
|
+
self._server_config, ssl_ctx=self._ssl_ctx
|
|
604
|
+
)
|
|
605
|
+
except Exception:
|
|
606
|
+
client_writer.write(b"\x05\x05\x00\x01" + b"\x00" * 4 + b"\x00\x00")
|
|
607
|
+
await client_writer.drain()
|
|
608
|
+
client_writer.close()
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
loop = asyncio.get_event_loop()
|
|
612
|
+
transport, udp_relay = await loop.create_datagram_endpoint(
|
|
613
|
+
lambda: _UdpRelayProtocol(remote_writer, loop),
|
|
614
|
+
local_addr=("127.0.0.1", 0),
|
|
615
|
+
)
|
|
616
|
+
relay_addr = transport.get_extra_info("sockname")
|
|
617
|
+
relay_port = relay_addr[1]
|
|
618
|
+
|
|
619
|
+
# 回复客户端 UDP relay 地址
|
|
620
|
+
reply = b"\x05\x00\x00\x01\x7f\x00\x00\x01" + struct.pack("!H", relay_port)
|
|
621
|
+
client_writer.write(reply)
|
|
622
|
+
await client_writer.drain()
|
|
623
|
+
|
|
624
|
+
async def _tcp_to_udp():
|
|
625
|
+
try:
|
|
626
|
+
while True:
|
|
627
|
+
frame_data = await asyncio.wait_for(
|
|
628
|
+
read_udp_frame(remote_reader), timeout=UDP_IDLE_TIMEOUT
|
|
629
|
+
)
|
|
630
|
+
if udp_relay.client_addr:
|
|
631
|
+
transport.sendto(frame_data, udp_relay.client_addr)
|
|
632
|
+
except (asyncio.TimeoutError, asyncio.IncompleteReadError,
|
|
633
|
+
ConnectionResetError, BrokenPipeError, OSError):
|
|
634
|
+
pass
|
|
635
|
+
|
|
636
|
+
async def _wait_control_close():
|
|
637
|
+
try:
|
|
638
|
+
await client_reader.read(1)
|
|
639
|
+
except (ConnectionResetError, BrokenPipeError, OSError):
|
|
640
|
+
pass
|
|
641
|
+
|
|
642
|
+
tasks = [
|
|
643
|
+
asyncio.ensure_future(_tcp_to_udp()),
|
|
644
|
+
asyncio.ensure_future(_wait_control_close()),
|
|
645
|
+
]
|
|
646
|
+
try:
|
|
647
|
+
await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
|
|
648
|
+
for t in tasks:
|
|
649
|
+
t.cancel()
|
|
650
|
+
finally:
|
|
651
|
+
transport.close()
|
|
652
|
+
remote_writer.close()
|
|
653
|
+
try:
|
|
654
|
+
await remote_writer.wait_closed()
|
|
655
|
+
except OSError:
|
|
656
|
+
pass
|
|
657
|
+
try:
|
|
658
|
+
client_writer.close()
|
|
659
|
+
await client_writer.wait_closed()
|
|
660
|
+
except OSError:
|
|
661
|
+
pass
|
|
662
|
+
|
|
379
663
|
async def _handle_http(self, client_reader, client_writer):
|
|
380
664
|
async with self._semaphore:
|
|
381
665
|
await self._do_handle_http(client_reader, client_writer)
|
|
382
666
|
|
|
383
667
|
async def _do_handle_http(self, client_reader, client_writer):
|
|
384
668
|
try:
|
|
385
|
-
raw_line = await client_reader.readline()
|
|
669
|
+
raw_line = await asyncio.wait_for(client_reader.readline(), timeout=LOCAL_HANDSHAKE_TIMEOUT)
|
|
386
670
|
if not raw_line:
|
|
387
671
|
client_writer.close()
|
|
388
672
|
return
|
|
@@ -404,7 +688,7 @@ class ProxyCore:
|
|
|
404
688
|
port = 443
|
|
405
689
|
|
|
406
690
|
while True:
|
|
407
|
-
header_line = await client_reader.readline()
|
|
691
|
+
header_line = await asyncio.wait_for(client_reader.readline(), timeout=LOCAL_HANDSHAKE_TIMEOUT)
|
|
408
692
|
if header_line in (b"\r\n", b"\n", b""):
|
|
409
693
|
break
|
|
410
694
|
|
|
@@ -442,7 +726,7 @@ class ProxyCore:
|
|
|
442
726
|
|
|
443
727
|
headers = []
|
|
444
728
|
while True:
|
|
445
|
-
header_line = await client_reader.readline()
|
|
729
|
+
header_line = await asyncio.wait_for(client_reader.readline(), timeout=LOCAL_HANDSHAKE_TIMEOUT)
|
|
446
730
|
if header_line in (b"\r\n", b"\n", b""):
|
|
447
731
|
break
|
|
448
732
|
headers.append(header_line)
|