block-proxy 0.1.14 → 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.
@@ -0,0 +1,466 @@
1
+ import asyncio
2
+ import struct
3
+ import os
4
+ import sys
5
+ import pytest
6
+
7
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
8
+ from proxy_core import (
9
+ write_udp_frame, read_udp_frame, ProxyCore, _UdpRelayProtocol,
10
+ connect_upstream_udp_associate,
11
+ )
12
+
13
+
14
+ class TestUdpFrameEncoding:
15
+ @staticmethod
16
+ async def _make_pipe():
17
+ """Create a TCP loopback connection pair for testing."""
18
+ server_ready = asyncio.Event()
19
+ conns = []
20
+
21
+ async def on_connect(reader, writer):
22
+ conns.append((reader, writer))
23
+ server_ready.set()
24
+
25
+ server = await asyncio.start_server(on_connect, "127.0.0.1", 0)
26
+ port = server.sockets[0].getsockname()[1]
27
+ c_reader, c_writer = await asyncio.open_connection("127.0.0.1", port)
28
+ await asyncio.wait_for(server_ready.wait(), timeout=2)
29
+ s_reader, s_writer = conns[0]
30
+ server.close()
31
+ return c_reader, c_writer, s_reader, s_writer
32
+
33
+ def test_frame_round_trip(self):
34
+ async def _run():
35
+ c_reader, c_writer, s_reader, s_writer = await self._make_pipe()
36
+
37
+ payload = b"\x00\x00\x00\x01\x7f\x00\x00\x01\x00\x50test data"
38
+ await write_udp_frame(s_writer, payload)
39
+ result = await asyncio.wait_for(read_udp_frame(c_reader), timeout=2)
40
+ assert result == payload
41
+
42
+ c_writer.close()
43
+ s_writer.close()
44
+
45
+ asyncio.run(_run())
46
+
47
+ def test_multiple_frames(self):
48
+ async def _run():
49
+ c_reader, c_writer, s_reader, s_writer = await self._make_pipe()
50
+
51
+ payloads = [b"frame1_data", b"frame2_data_longer", b"f3"]
52
+ for p in payloads:
53
+ await write_udp_frame(s_writer, p)
54
+
55
+ for p in payloads:
56
+ result = await asyncio.wait_for(read_udp_frame(c_reader), timeout=2)
57
+ assert result == p
58
+
59
+ c_writer.close()
60
+ s_writer.close()
61
+
62
+ asyncio.run(_run())
63
+
64
+ def test_empty_frame_raises(self):
65
+ async def _run():
66
+ c_reader, c_writer, s_reader, s_writer = await self._make_pipe()
67
+
68
+ s_writer.write(struct.pack("!H", 0))
69
+ await s_writer.drain()
70
+
71
+ with pytest.raises(Exception, match="invalid UDP frame length"):
72
+ await asyncio.wait_for(read_udp_frame(c_reader), timeout=2)
73
+
74
+ c_writer.close()
75
+ s_writer.close()
76
+
77
+ asyncio.run(_run())
78
+
79
+
80
+ class TestUdpRelayProtocol:
81
+ def test_datagram_received_writes_frame(self):
82
+ async def _run():
83
+ server_ready = asyncio.Event()
84
+ conns = []
85
+
86
+ async def on_connect(reader, writer):
87
+ conns.append((reader, writer))
88
+ server_ready.set()
89
+
90
+ server = await asyncio.start_server(on_connect, "127.0.0.1", 0)
91
+ port = server.sockets[0].getsockname()[1]
92
+ c_reader, c_writer = await asyncio.open_connection("127.0.0.1", port)
93
+ await asyncio.wait_for(server_ready.wait(), timeout=2)
94
+ s_reader, s_writer = conns[0]
95
+ server.close()
96
+
97
+ loop = asyncio.get_event_loop()
98
+ proto = _UdpRelayProtocol(s_writer, loop)
99
+ proto.connection_made(None)
100
+
101
+ data = b"\x00\x00\x00\x01\x08\x08\x08\x08\x00\x35dns_query"
102
+ proto.datagram_received(data, ("127.0.0.1", 54321))
103
+
104
+ assert proto.client_addr == ("127.0.0.1", 54321)
105
+
106
+ # Need to flush since datagram_received doesn't await drain
107
+ await asyncio.sleep(0.05)
108
+ frame = await asyncio.wait_for(read_udp_frame(c_reader), timeout=2)
109
+ assert frame == data
110
+
111
+ c_writer.close()
112
+ s_writer.close()
113
+
114
+ asyncio.run(_run())
115
+
116
+
117
+ class TestMockSocks5UdpServer:
118
+ """End-to-end test with a mock SOCKS5 server that supports UDP over TCP framing."""
119
+
120
+ @staticmethod
121
+ async def _mock_socks5_server(reader, writer):
122
+ # Auth negotiation
123
+ header = await reader.readexactly(2)
124
+ nmethods = header[1]
125
+ await reader.readexactly(nmethods)
126
+ writer.write(b"\x05\x00") # No auth
127
+ await writer.drain()
128
+
129
+ # Request
130
+ req = await reader.readexactly(4)
131
+ cmd = req[1]
132
+ atyp = req[3]
133
+ if atyp == 0x01:
134
+ await reader.readexactly(4 + 2)
135
+ elif atyp == 0x03:
136
+ length = (await reader.readexactly(1))[0]
137
+ await reader.readexactly(length + 2)
138
+ elif atyp == 0x04:
139
+ await reader.readexactly(16 + 2)
140
+
141
+ if cmd != 0x03:
142
+ writer.write(b"\x05\x07\x00\x01" + b"\x00" * 4 + b"\x00\x00")
143
+ await writer.drain()
144
+ writer.close()
145
+ return
146
+
147
+ # UDP ASSOCIATE success
148
+ writer.write(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00")
149
+ await writer.drain()
150
+
151
+ # Echo server: read frames and echo back with modified payload
152
+ try:
153
+ while True:
154
+ length_data = await reader.readexactly(2)
155
+ length = struct.unpack("!H", length_data)[0]
156
+ payload = await reader.readexactly(length)
157
+
158
+ # Parse SOCKS5 UDP header to find data offset, then echo with "REPLY:" prefix
159
+ if len(payload) < 10:
160
+ continue
161
+ atyp = payload[3]
162
+ if atyp == 0x01:
163
+ header_len = 10
164
+ elif atyp == 0x03:
165
+ header_len = 5 + payload[4] + 2
166
+ elif atyp == 0x04:
167
+ header_len = 22
168
+ else:
169
+ continue
170
+
171
+ udp_header = payload[:header_len]
172
+ udp_data = payload[header_len:]
173
+ response_data = b"REPLY:" + udp_data
174
+ response_payload = udp_header + response_data
175
+
176
+ frame = struct.pack("!H", len(response_payload)) + response_payload
177
+ writer.write(frame)
178
+ await writer.drain()
179
+ except (asyncio.IncompleteReadError, ConnectionResetError, BrokenPipeError):
180
+ pass
181
+ finally:
182
+ writer.close()
183
+
184
+ def test_udp_associate_end_to_end(self):
185
+ """Test with ProxyCore in its own thread connecting to mock server."""
186
+ import socket as sock_mod
187
+ import threading
188
+
189
+ # Start mock server in a background thread with its own loop
190
+ mock_loop = asyncio.new_event_loop()
191
+ server_ready = threading.Event()
192
+ server_port_holder = [0]
193
+ stop_event = asyncio.Event()
194
+
195
+ def run_mock_server():
196
+ asyncio.set_event_loop(mock_loop)
197
+ async def _start():
198
+ server = await asyncio.start_server(
199
+ self._mock_socks5_server, "127.0.0.1", 0
200
+ )
201
+ server_port_holder[0] = server.sockets[0].getsockname()[1]
202
+ server_ready.set()
203
+ await stop_event.wait()
204
+ server.close()
205
+ await server.wait_closed()
206
+ try:
207
+ mock_loop.run_until_complete(_start())
208
+ except Exception:
209
+ pass
210
+ finally:
211
+ mock_loop.close()
212
+
213
+ t = threading.Thread(target=run_mock_server, daemon=True)
214
+ t.start()
215
+ server_ready.wait(timeout=5)
216
+ server_port = server_port_holder[0]
217
+
218
+ config = {
219
+ "server": {
220
+ "protocol": "socks5",
221
+ "address": "127.0.0.1",
222
+ "port": server_port,
223
+ "username": "",
224
+ "password": "",
225
+ "tls": False,
226
+ "allowInsecure": True,
227
+ },
228
+ "local": {
229
+ "socks_port": 0,
230
+ "http_port": 0,
231
+ "udp": True,
232
+ },
233
+ }
234
+
235
+ s = sock_mod.socket()
236
+ s.bind(("127.0.0.1", 0))
237
+ config["local"]["socks_port"] = s.getsockname()[1]
238
+ s.close()
239
+ s = sock_mod.socket()
240
+ s.bind(("127.0.0.1", 0))
241
+ config["local"]["http_port"] = s.getsockname()[1]
242
+ s.close()
243
+
244
+ proxy = ProxyCore()
245
+ proxy.start(config)
246
+ actual_socks_port = proxy.socks_port
247
+
248
+ import time
249
+ time.sleep(0.2)
250
+
251
+ try:
252
+ # Connect to proxy's local SOCKS5 port
253
+ ctrl_sock = sock_mod.socket(sock_mod.AF_INET, sock_mod.SOCK_STREAM)
254
+ ctrl_sock.settimeout(5)
255
+ ctrl_sock.connect(("127.0.0.1", actual_socks_port))
256
+
257
+ # SOCKS5 handshake
258
+ ctrl_sock.sendall(b"\x05\x01\x00")
259
+ resp = ctrl_sock.recv(2)
260
+ assert resp == b"\x05\x00"
261
+
262
+ # UDP ASSOCIATE
263
+ ctrl_sock.sendall(b"\x05\x03\x00\x01\x00\x00\x00\x00\x00\x00")
264
+ reply = ctrl_sock.recv(10)
265
+ assert len(reply) == 10
266
+ assert reply[1] == 0x00
267
+ relay_port = struct.unpack("!H", reply[8:10])[0]
268
+ assert relay_port > 0
269
+
270
+ # Send UDP to relay
271
+ udp_sock = sock_mod.socket(sock_mod.AF_INET, sock_mod.SOCK_DGRAM)
272
+ udp_sock.settimeout(5)
273
+
274
+ udp_header = b"\x00\x00\x00\x01\x08\x08\x08\x08\x00\x35"
275
+ udp_data = b"hello_udp"
276
+ udp_sock.sendto(udp_header + udp_data, ("127.0.0.1", relay_port))
277
+
278
+ # Receive response
279
+ response, _ = udp_sock.recvfrom(65535)
280
+ assert response[10:] == b"REPLY:hello_udp"
281
+
282
+ udp_sock.close()
283
+ ctrl_sock.close()
284
+ finally:
285
+ proxy.stop()
286
+ mock_loop.call_soon_threadsafe(stop_event.set)
287
+
288
+ def test_udp_associate_with_auth(self):
289
+ async def _mock_auth_server(reader, writer):
290
+ header = await reader.readexactly(2)
291
+ nmethods = header[1]
292
+ await reader.readexactly(nmethods)
293
+ writer.write(b"\x05\x02") # Username/password auth
294
+ await writer.drain()
295
+
296
+ # Read auth: [ver(1), ulen(1), username(ulen), plen(1), password(plen)]
297
+ ver = await reader.readexactly(1)
298
+ ulen_b = await reader.readexactly(1)
299
+ ulen = ulen_b[0]
300
+ username = (await reader.readexactly(ulen)).decode()
301
+ plen_b = await reader.readexactly(1)
302
+ plen = plen_b[0]
303
+ password = (await reader.readexactly(plen)).decode()
304
+
305
+ if username == "testuser" and password == "testpass":
306
+ writer.write(b"\x01\x00")
307
+ else:
308
+ writer.write(b"\x01\xff")
309
+ writer.close()
310
+ return
311
+ await writer.drain()
312
+
313
+ # Request
314
+ req = await reader.readexactly(4)
315
+ if req[3] == 0x01:
316
+ await reader.readexactly(6)
317
+
318
+ writer.write(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00")
319
+ await writer.drain()
320
+
321
+ # Keep connection alive briefly
322
+ try:
323
+ await asyncio.wait_for(reader.read(1), timeout=2)
324
+ except (asyncio.TimeoutError, ConnectionResetError):
325
+ pass
326
+ writer.close()
327
+
328
+ async def _run():
329
+ server = await asyncio.start_server(
330
+ _mock_auth_server, "127.0.0.1", 0
331
+ )
332
+ server_port = server.sockets[0].getsockname()[1]
333
+
334
+ config = {
335
+ "address": "127.0.0.1",
336
+ "port": server_port,
337
+ "username": "testuser",
338
+ "password": "testpass",
339
+ "tls": False,
340
+ "allowInsecure": True,
341
+ }
342
+
343
+ reader, writer = await connect_upstream_udp_associate(config)
344
+ assert reader is not None
345
+ assert writer is not None
346
+ writer.close()
347
+ await writer.wait_closed()
348
+ server.close()
349
+ await server.wait_closed()
350
+
351
+ asyncio.run(_run())
352
+
353
+
354
+ class TestUdpDisabled:
355
+ def test_http_protocol_rejects_udp(self):
356
+ async def _run():
357
+ import socket as sock_mod
358
+
359
+ config = {
360
+ "server": {
361
+ "protocol": "http",
362
+ "address": "127.0.0.1",
363
+ "port": 9999,
364
+ "username": "",
365
+ "password": "",
366
+ "tls": False,
367
+ "allowInsecure": True,
368
+ },
369
+ "local": {
370
+ "socks_port": 0,
371
+ "http_port": 0,
372
+ "udp": True,
373
+ },
374
+ }
375
+
376
+ s = sock_mod.socket()
377
+ s.bind(("127.0.0.1", 0))
378
+ socks_port = s.getsockname()[1]
379
+ s.close()
380
+ s = sock_mod.socket()
381
+ s.bind(("127.0.0.1", 0))
382
+ http_port = s.getsockname()[1]
383
+ s.close()
384
+
385
+ config["local"]["socks_port"] = socks_port
386
+ config["local"]["http_port"] = http_port
387
+
388
+ proxy = ProxyCore()
389
+ proxy.start(config)
390
+ actual_socks_port = proxy.socks_port
391
+
392
+ await asyncio.sleep(0.1)
393
+
394
+ reader, writer = await asyncio.open_connection("127.0.0.1", actual_socks_port)
395
+ writer.write(b"\x05\x01\x00")
396
+ await writer.drain()
397
+ await reader.readexactly(2)
398
+
399
+ # UDP ASSOCIATE
400
+ writer.write(b"\x05\x03\x00\x01\x00\x00\x00\x00\x00\x00")
401
+ await writer.drain()
402
+
403
+ reply = await asyncio.wait_for(reader.readexactly(10), timeout=5)
404
+ assert reply[1] == 0x07 # Command not supported
405
+
406
+ writer.close()
407
+ await writer.wait_closed()
408
+ proxy.stop()
409
+
410
+ asyncio.run(_run())
411
+
412
+ def test_udp_false_config_rejects_udp(self):
413
+ async def _run():
414
+ import socket as sock_mod
415
+
416
+ config = {
417
+ "server": {
418
+ "protocol": "socks5",
419
+ "address": "127.0.0.1",
420
+ "port": 9999,
421
+ "username": "",
422
+ "password": "",
423
+ "tls": False,
424
+ "allowInsecure": True,
425
+ },
426
+ "local": {
427
+ "socks_port": 0,
428
+ "http_port": 0,
429
+ "udp": False,
430
+ },
431
+ }
432
+
433
+ s = sock_mod.socket()
434
+ s.bind(("127.0.0.1", 0))
435
+ socks_port = s.getsockname()[1]
436
+ s.close()
437
+ s = sock_mod.socket()
438
+ s.bind(("127.0.0.1", 0))
439
+ http_port = s.getsockname()[1]
440
+ s.close()
441
+
442
+ config["local"]["socks_port"] = socks_port
443
+ config["local"]["http_port"] = http_port
444
+
445
+ proxy = ProxyCore()
446
+ proxy.start(config)
447
+ actual_socks_port = proxy.socks_port
448
+
449
+ await asyncio.sleep(0.1)
450
+
451
+ reader, writer = await asyncio.open_connection("127.0.0.1", actual_socks_port)
452
+ writer.write(b"\x05\x01\x00")
453
+ await writer.drain()
454
+ await reader.readexactly(2)
455
+
456
+ writer.write(b"\x05\x03\x00\x01\x00\x00\x00\x00\x00\x00")
457
+ await writer.drain()
458
+
459
+ reply = await asyncio.wait_for(reader.readexactly(10), timeout=5)
460
+ assert reply[1] == 0x07 # Command not supported
461
+
462
+ writer.close()
463
+ await writer.wait_closed()
464
+ proxy.stop()
465
+
466
+ asyncio.run(_run())
package/config.json CHANGED
@@ -3,5 +3,7 @@
3
3
  "proxy_port": 8001,
4
4
  "socks5_port": 8002,
5
5
  "auth_username": "admin",
6
- "auth_password": "admin"
6
+ "auth_password": "admin",
7
+ "enable_express": "1",
8
+ "enable_socks5": "1"
7
9
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "block-proxy",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Small-scale network mitm proxy filter",
5
5
  "bin": {
6
6
  "block-proxy": "bin/start.js"