block-proxy 0.1.11 → 0.1.13

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 (62) hide show
  1. package/.agents/skills/commit/skill.md +40 -0
  2. package/.claude/settings.local.json +29 -1
  3. package/.claude/skills/build-client/skill.md +24 -0
  4. package/.claude/skills/commit/skill.md +34 -26
  5. package/.claude/skills/release-client/skill.md +68 -0
  6. package/CLAUDE.md +109 -47
  7. package/Dockerfile +1 -1
  8. package/README.md +69 -60
  9. package/build/asset-manifest.json +6 -6
  10. package/build/index.html +1 -1
  11. package/build/static/css/main.3f317ce6.css +2 -0
  12. package/build/static/css/main.3f317ce6.css.map +1 -0
  13. package/build/static/js/{main.2247fb80.js → main.68f66be0.js} +3 -3
  14. package/build/static/js/main.68f66be0.js.map +1 -0
  15. package/client/app.py +312 -0
  16. package/client/build.sh +84 -0
  17. package/client/config.py +49 -0
  18. package/client/config_window.py +155 -0
  19. package/client/icons/app.icns +0 -0
  20. package/client/icons/app_example.png +0 -0
  21. package/client/icons/app_icon.png +0 -0
  22. package/client/icons/backup/app_example.png +0 -0
  23. package/client/icons/backup/christmas-sock_dark.png +0 -0
  24. package/client/icons/backup/christmas-sock_light.png +0 -0
  25. package/client/icons/backup/socks_on_G.png +0 -0
  26. package/client/icons/backup/socks_on_M.png +0 -0
  27. package/client/icons/christmas-sock_dark.png +0 -0
  28. package/client/icons/christmas-sock_light.png +0 -0
  29. package/client/icons/christmas-sock_light_bar.png +0 -0
  30. package/client/icons/socks_on_G.png +0 -0
  31. package/client/icons/socks_on_G_bar.png +0 -0
  32. package/client/icons/socks_on_M.png +0 -0
  33. package/client/icons/socks_on_M_bar.png +0 -0
  34. package/client/main.py +28 -0
  35. package/client/proxy_core.py +475 -0
  36. package/client/requirements.txt +3 -0
  37. package/client/scripts/download_xray.sh +30 -0
  38. package/client/setup.py +30 -0
  39. package/client/system_proxy.py +94 -0
  40. package/client/tests/__init__.py +0 -0
  41. package/client/tests/test_config.py +72 -0
  42. package/client/tests/test_system_proxy.py +69 -0
  43. package/client/watch-icons.js +31 -0
  44. package/config.json +82 -5
  45. package/docs/superpowers/plans/2026-05-27-blockproxyclient.md +1274 -0
  46. package/docs/superpowers/specs/2026-05-27-blockproxyclient-design.md +264 -0
  47. package/package.json +11 -5
  48. package/proxy/proxy.js +70 -18
  49. package/server/express.js +17 -1
  50. package/skills-lock.json +11 -0
  51. package/socks5/server.js +2 -2
  52. package/src/App.css +596 -276
  53. package/src/App.js +25 -22
  54. package/src/index.css +3 -4
  55. package/test/lib/mock-server.js +133 -0
  56. package/test/proxy-tests.js +708 -0
  57. package/test/run.js +330 -0
  58. package/build/static/css/main.8bfa3d5f.css +0 -2
  59. package/build/static/css/main.8bfa3d5f.css.map +0 -1
  60. package/build/static/js/main.2247fb80.js.map +0 -1
  61. package/hack-of-anyproxy/lib/requestHandler.js +0 -1060
  62. /package/build/static/js/{main.2247fb80.js.LICENSE.txt → main.68f66be0.js.LICENSE.txt} +0 -0
@@ -0,0 +1,475 @@
1
+ import asyncio
2
+ import functools
3
+ import ipaddress
4
+ import logging
5
+ import ssl
6
+ import struct
7
+ import threading
8
+
9
+ logger = logging.getLogger("proxy_core")
10
+
11
+ PRIVATE_NETWORKS = [
12
+ ipaddress.ip_network("127.0.0.0/8"),
13
+ ipaddress.ip_network("10.0.0.0/8"),
14
+ ipaddress.ip_network("172.16.0.0/12"),
15
+ ipaddress.ip_network("192.168.0.0/16"),
16
+ ipaddress.ip_network("::1/128"),
17
+ ipaddress.ip_network("fc00::/7"),
18
+ ipaddress.ip_network("fe80::/10"),
19
+ ]
20
+
21
+
22
+ @functools.lru_cache(maxsize=256)
23
+ def is_private_ip(host):
24
+ try:
25
+ addr = ipaddress.ip_address(host)
26
+ return any(addr in net for net in PRIVATE_NETWORKS)
27
+ except ValueError:
28
+ return False
29
+
30
+
31
+ RELAY_IDLE_TIMEOUT = 300
32
+
33
+
34
+ async def relay(reader, writer):
35
+ try:
36
+ while True:
37
+ data = await asyncio.wait_for(reader.read(65536), timeout=RELAY_IDLE_TIMEOUT)
38
+ if not data:
39
+ break
40
+ writer.write(data)
41
+ if writer.transport.get_write_buffer_size() > 65536:
42
+ await writer.drain()
43
+ except asyncio.TimeoutError:
44
+ pass
45
+ except (ConnectionResetError, BrokenPipeError, OSError):
46
+ pass
47
+ finally:
48
+ try:
49
+ if writer.can_write_eof():
50
+ writer.write_eof()
51
+ else:
52
+ writer.close()
53
+ await writer.wait_closed()
54
+ except OSError:
55
+ pass
56
+
57
+
58
+ CONNECT_TIMEOUT = 10
59
+ HANDSHAKE_TIMEOUT = 10
60
+
61
+
62
+ async def connect_upstream_socks5(server_config, dest_addr, dest_port, ssl_ctx=None):
63
+ host = server_config["address"]
64
+ port = server_config["port"]
65
+ username = server_config["username"]
66
+ password = server_config["password"]
67
+ use_tls = server_config["tls"]
68
+
69
+ reader, writer = await asyncio.wait_for(
70
+ asyncio.open_connection(
71
+ host, port, ssl=ssl_ctx if use_tls else None,
72
+ server_hostname=host if use_tls else None,
73
+ ),
74
+ timeout=CONNECT_TIMEOUT,
75
+ )
76
+
77
+ async def _handshake():
78
+ if username and password:
79
+ writer.write(b"\x05\x01\x02")
80
+ else:
81
+ writer.write(b"\x05\x01\x00")
82
+ await writer.drain()
83
+
84
+ resp = await reader.readexactly(2)
85
+ if resp[0] != 0x05:
86
+ raise Exception("SOCKS5 version mismatch")
87
+
88
+ if resp[1] == 0x02:
89
+ uname = username.encode("utf-8")
90
+ passwd = password.encode("utf-8")
91
+ writer.write(
92
+ b"\x01"
93
+ + struct.pack("B", len(uname))
94
+ + uname
95
+ + struct.pack("B", len(passwd))
96
+ + passwd
97
+ )
98
+ await writer.drain()
99
+ auth_resp = await reader.readexactly(2)
100
+ if auth_resp[1] != 0x00:
101
+ raise Exception("SOCKS5 auth failed")
102
+ elif resp[1] == 0xFF:
103
+ raise Exception("SOCKS5 no acceptable auth method")
104
+
105
+ try:
106
+ addr = ipaddress.ip_address(dest_addr)
107
+ if isinstance(addr, ipaddress.IPv4Address):
108
+ addr_data = b"\x01" + addr.packed
109
+ else:
110
+ addr_data = b"\x04" + addr.packed
111
+ except ValueError:
112
+ encoded = dest_addr.encode("utf-8")
113
+ addr_data = b"\x03" + struct.pack("B", len(encoded)) + encoded
114
+
115
+ writer.write(
116
+ b"\x05\x01\x00" + addr_data + struct.pack("!H", dest_port)
117
+ )
118
+ await writer.drain()
119
+
120
+ reply = await reader.readexactly(4)
121
+ if reply[1] != 0x00:
122
+ raise Exception(f"SOCKS5 CONNECT failed: {reply[1]:#x}")
123
+
124
+ if reply[3] == 0x01:
125
+ await reader.readexactly(4 + 2)
126
+ elif reply[3] == 0x03:
127
+ length = (await reader.readexactly(1))[0]
128
+ await reader.readexactly(length + 2)
129
+ elif reply[3] == 0x04:
130
+ await reader.readexactly(16 + 2)
131
+
132
+ await asyncio.wait_for(_handshake(), timeout=HANDSHAKE_TIMEOUT)
133
+
134
+ return reader, writer
135
+
136
+
137
+ async def connect_direct(dest_addr, dest_port):
138
+ return await asyncio.open_connection(dest_addr, dest_port)
139
+
140
+
141
+ MAX_CONCURRENT = 256
142
+
143
+
144
+ class ProxyCore:
145
+ def __init__(self):
146
+ self._loop = None
147
+ self._thread = None
148
+ self._socks_server = None
149
+ self._http_server = None
150
+ self._running = False
151
+ self._server_config = None
152
+ self._proxy_private = False
153
+ self._socks_port = 1080
154
+ self._http_port = 1087
155
+ self._ssl_ctx = None
156
+ self._semaphore = None
157
+
158
+ def _build_ssl_context(self):
159
+ server = self._server_config
160
+ if not server["tls"]:
161
+ self._ssl_ctx = None
162
+ return
163
+ ctx = ssl.create_default_context()
164
+ if server["allowInsecure"]:
165
+ ctx.check_hostname = False
166
+ ctx.verify_mode = ssl.CERT_NONE
167
+ self._ssl_ctx = ctx
168
+
169
+ def start(self, user_config):
170
+ if self._running:
171
+ self.stop()
172
+
173
+ self._server_config = user_config["server"]
174
+ local = user_config["local"]
175
+ self._socks_port = local["socks_port"]
176
+ self._http_port = local["http_port"]
177
+ self._proxy_private = local.get("proxy_private", False)
178
+ self._build_ssl_context()
179
+
180
+ self._loop = asyncio.new_event_loop()
181
+ started = threading.Event()
182
+ self._start_error = None
183
+ self._thread = threading.Thread(
184
+ target=self._run_loop, args=(started,), daemon=True
185
+ )
186
+ self._thread.start()
187
+ started.wait(timeout=5)
188
+ if self._start_error:
189
+ raise self._start_error
190
+ self._running = True
191
+
192
+ def stop(self):
193
+ if not self._running:
194
+ return
195
+ self._running = False
196
+ if self._loop and self._loop.is_running():
197
+ def _shutdown():
198
+ if self._socks_server:
199
+ self._socks_server.close()
200
+ if self._http_server:
201
+ self._http_server.close()
202
+ self._loop.stop()
203
+ self._loop.call_soon_threadsafe(_shutdown)
204
+ if self._thread:
205
+ self._thread.join(timeout=2)
206
+ if self._loop:
207
+ self._loop.close()
208
+ self._loop = None
209
+ self._thread = None
210
+
211
+ def is_running(self):
212
+ return self._running and self._thread is not None and self._thread.is_alive()
213
+
214
+ async def _measure_latency(self):
215
+ import time
216
+ start = time.monotonic()
217
+ try:
218
+ reader, writer = await asyncio.wait_for(
219
+ asyncio.open_connection(
220
+ self._server_config["address"],
221
+ self._server_config["port"],
222
+ ssl=self._ssl_ctx if self._server_config["tls"] else None,
223
+ server_hostname=self._server_config["address"] if self._server_config["tls"] else None,
224
+ ),
225
+ timeout=5,
226
+ )
227
+ username = self._server_config.get("username", "")
228
+ password = self._server_config.get("password", "")
229
+ if username and password:
230
+ writer.write(b"\x05\x01\x02")
231
+ else:
232
+ writer.write(b"\x05\x01\x00")
233
+ await writer.drain()
234
+ resp = await asyncio.wait_for(reader.readexactly(2), timeout=5)
235
+ if resp[0] != 0x05:
236
+ writer.close()
237
+ return None
238
+ if resp[1] == 0x02 and username and password:
239
+ uname = username.encode("utf-8")
240
+ passwd = password.encode("utf-8")
241
+ writer.write(
242
+ b"\x01"
243
+ + struct.pack("B", len(uname)) + uname
244
+ + struct.pack("B", len(passwd)) + passwd
245
+ )
246
+ await writer.drain()
247
+ auth_resp = await asyncio.wait_for(reader.readexactly(2), timeout=5)
248
+ if auth_resp[1] != 0x00:
249
+ writer.close()
250
+ return None
251
+ elapsed = time.monotonic() - start
252
+ writer.close()
253
+ try:
254
+ await writer.wait_closed()
255
+ except OSError:
256
+ pass
257
+ return int(elapsed * 1000)
258
+ except Exception:
259
+ return None
260
+
261
+ def measure_latency(self):
262
+ if not self._running or not self._loop or not self._loop.is_running():
263
+ return None
264
+ future = asyncio.run_coroutine_threadsafe(self._measure_latency(), self._loop)
265
+ try:
266
+ return future.result(timeout=6)
267
+ except Exception:
268
+ return None
269
+
270
+ def _run_loop(self, started_event):
271
+ asyncio.set_event_loop(self._loop)
272
+ try:
273
+ self._loop.run_until_complete(self._start_servers())
274
+ except OSError as e:
275
+ self._start_error = e
276
+ started_event.set()
277
+ return
278
+ started_event.set()
279
+ self._loop.run_forever()
280
+
281
+ async def _start_servers(self):
282
+ self._semaphore = asyncio.Semaphore(MAX_CONCURRENT)
283
+ self._socks_server = await asyncio.start_server(
284
+ self._handle_socks, "127.0.0.1", self._socks_port
285
+ )
286
+ self._http_server = await asyncio.start_server(
287
+ self._handle_http, "127.0.0.1", self._http_port
288
+ )
289
+
290
+ async def _stop_servers(self):
291
+ if self._socks_server:
292
+ self._socks_server.close()
293
+ await self._socks_server.wait_closed()
294
+ if self._http_server:
295
+ self._http_server.close()
296
+ await self._http_server.wait_closed()
297
+
298
+ def _should_direct(self, host):
299
+ if self._proxy_private:
300
+ return False
301
+ return is_private_ip(host)
302
+
303
+ async def _connect_target(self, dest_addr, dest_port):
304
+ if self._should_direct(dest_addr):
305
+ return await connect_direct(dest_addr, dest_port)
306
+ return await connect_upstream_socks5(
307
+ self._server_config, dest_addr, dest_port, ssl_ctx=self._ssl_ctx
308
+ )
309
+
310
+ async def _handle_socks(self, client_reader, client_writer):
311
+ async with self._semaphore:
312
+ await self._do_handle_socks(client_reader, client_writer)
313
+
314
+ async def _do_handle_socks(self, client_reader, client_writer):
315
+ try:
316
+ header = await client_reader.readexactly(2)
317
+ ver, nmethods = header
318
+ if ver != 0x05:
319
+ client_writer.close()
320
+ return
321
+ await client_reader.readexactly(nmethods)
322
+
323
+ client_writer.write(b"\x05\x00")
324
+ await client_writer.drain()
325
+
326
+ req = await client_reader.readexactly(4)
327
+ ver, cmd, _, atyp = req
328
+
329
+ if cmd != 0x01:
330
+ client_writer.write(
331
+ b"\x05\x07\x00\x01" + b"\x00" * 4 + b"\x00\x00"
332
+ )
333
+ await client_writer.drain()
334
+ client_writer.close()
335
+ return
336
+
337
+ if atyp == 0x01:
338
+ raw = await client_reader.readexactly(4)
339
+ dest_addr = str(ipaddress.IPv4Address(raw))
340
+ elif atyp == 0x03:
341
+ length = (await client_reader.readexactly(1))[0]
342
+ dest_addr = (await client_reader.readexactly(length)).decode("utf-8")
343
+ elif atyp == 0x04:
344
+ raw = await client_reader.readexactly(16)
345
+ dest_addr = str(ipaddress.IPv6Address(raw))
346
+ else:
347
+ client_writer.write(
348
+ b"\x05\x08\x00\x01" + b"\x00" * 4 + b"\x00\x00"
349
+ )
350
+ await client_writer.drain()
351
+ client_writer.close()
352
+ return
353
+
354
+ port_data = await client_reader.readexactly(2)
355
+ dest_port = struct.unpack("!H", port_data)[0]
356
+
357
+ remote_reader, remote_writer = await self._connect_target(
358
+ dest_addr, dest_port
359
+ )
360
+
361
+ client_writer.write(
362
+ b"\x05\x00\x00\x01" + b"\x00" * 4 + b"\x00\x00"
363
+ )
364
+ await client_writer.drain()
365
+
366
+ await asyncio.gather(
367
+ relay(client_reader, remote_writer),
368
+ relay(remote_reader, client_writer),
369
+ )
370
+ except Exception:
371
+ pass
372
+ finally:
373
+ try:
374
+ client_writer.close()
375
+ await client_writer.wait_closed()
376
+ except OSError:
377
+ pass
378
+
379
+ async def _handle_http(self, client_reader, client_writer):
380
+ async with self._semaphore:
381
+ await self._do_handle_http(client_reader, client_writer)
382
+
383
+ async def _do_handle_http(self, client_reader, client_writer):
384
+ try:
385
+ raw_line = await client_reader.readline()
386
+ if not raw_line:
387
+ client_writer.close()
388
+ return
389
+ line = raw_line.decode("utf-8", errors="replace").strip()
390
+ parts = line.split()
391
+ if len(parts) < 3:
392
+ client_writer.close()
393
+ return
394
+
395
+ method = parts[0].upper()
396
+
397
+ if method == "CONNECT":
398
+ target = parts[1]
399
+ if ":" in target:
400
+ host, port_str = target.rsplit(":", 1)
401
+ port = int(port_str)
402
+ else:
403
+ host = target
404
+ port = 443
405
+
406
+ while True:
407
+ header_line = await client_reader.readline()
408
+ if header_line in (b"\r\n", b"\n", b""):
409
+ break
410
+
411
+ remote_reader, remote_writer = await self._connect_target(
412
+ host, port
413
+ )
414
+
415
+ client_writer.write(
416
+ b"HTTP/1.1 200 Connection Established\r\n\r\n"
417
+ )
418
+ await client_writer.drain()
419
+
420
+ await asyncio.gather(
421
+ relay(client_reader, remote_writer),
422
+ relay(remote_reader, client_writer),
423
+ )
424
+ else:
425
+ url = parts[1]
426
+ if url.startswith("http://"):
427
+ url_body = url[7:]
428
+ slash_idx = url_body.find("/")
429
+ if slash_idx == -1:
430
+ host_part = url_body
431
+ path = "/"
432
+ else:
433
+ host_part = url_body[:slash_idx]
434
+ path = url_body[slash_idx:]
435
+
436
+ if ":" in host_part:
437
+ host, port_str = host_part.rsplit(":", 1)
438
+ port = int(port_str)
439
+ else:
440
+ host = host_part
441
+ port = 80
442
+
443
+ headers = []
444
+ while True:
445
+ header_line = await client_reader.readline()
446
+ if header_line in (b"\r\n", b"\n", b""):
447
+ break
448
+ headers.append(header_line)
449
+
450
+ remote_reader, remote_writer = await self._connect_target(
451
+ host, port
452
+ )
453
+
454
+ request_line = f"{method} {path} {parts[2]}\r\n".encode()
455
+ remote_writer.write(request_line)
456
+ for h in headers:
457
+ remote_writer.write(h)
458
+ remote_writer.write(b"\r\n")
459
+ await remote_writer.drain()
460
+
461
+ await asyncio.gather(
462
+ relay(remote_reader, client_writer),
463
+ relay(client_reader, remote_writer),
464
+ )
465
+ else:
466
+ client_writer.close()
467
+ return
468
+ except Exception:
469
+ pass
470
+ finally:
471
+ try:
472
+ client_writer.close()
473
+ await client_writer.wait_closed()
474
+ except OSError:
475
+ pass
@@ -0,0 +1,3 @@
1
+ rumps>=0.4.0
2
+ py2app>=0.28
3
+ pytest>=7.0
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ XRAY_VERSION="v25.5.16"
5
+ PLATFORM="macos"
6
+ ARCH=$(uname -m)
7
+
8
+ if [ "$ARCH" = "arm64" ]; then
9
+ FILENAME="Xray-macos-arm64-v8a.zip"
10
+ elif [ "$ARCH" = "x86_64" ]; then
11
+ FILENAME="Xray-macos-64.zip"
12
+ else
13
+ echo "Unsupported architecture: $ARCH"
14
+ exit 1
15
+ fi
16
+
17
+ URL="https://github.com/XTLS/Xray-core/releases/download/${XRAY_VERSION}/${FILENAME}"
18
+ DEST_DIR="$(cd "$(dirname "$0")/../resources" && pwd)"
19
+
20
+ echo "Downloading xray-core ${XRAY_VERSION} for ${ARCH}..."
21
+ curl -L -o /tmp/xray.zip "$URL"
22
+
23
+ echo "Extracting..."
24
+ unzip -o /tmp/xray.zip xray -d /tmp
25
+ mv /tmp/xray "$DEST_DIR/core"
26
+ chmod +x "$DEST_DIR/core"
27
+ rm /tmp/xray.zip
28
+
29
+ echo "Done: $DEST_DIR/core"
30
+ "$DEST_DIR/core" version
@@ -0,0 +1,30 @@
1
+ from setuptools import setup
2
+
3
+ APP = ["main.py"]
4
+ DATA_FILES = [
5
+ ("icons", [
6
+ "icons/socks_on_G.png",
7
+ "icons/socks_on_M.png",
8
+ "icons/christmas-sock_light.png",
9
+ ]),
10
+ ]
11
+ OPTIONS = {
12
+ "argv_emulation": False,
13
+ "iconfile": "icons/app_example.png",
14
+ "plist": {
15
+ "LSUIElement": True,
16
+ "CFBundleName": "SocksClient",
17
+ "CFBundleIdentifier": "com.jaylli.socksclient",
18
+ "CFBundleVersion": "1.0.0",
19
+ "CFBundleShortVersionString": "1.0.0",
20
+ },
21
+ "packages": ["rumps"],
22
+ }
23
+
24
+ setup(
25
+ name="SocksClient",
26
+ app=APP,
27
+ data_files=DATA_FILES,
28
+ options={"py2app": OPTIONS},
29
+ setup_requires=["py2app"],
30
+ )
@@ -0,0 +1,94 @@
1
+ import logging
2
+ import subprocess
3
+ import atexit
4
+ import signal
5
+ from concurrent.futures import ThreadPoolExecutor
6
+
7
+ logger = logging.getLogger("system_proxy")
8
+
9
+
10
+ def _run_networksetup(args):
11
+ result = subprocess.run(args, capture_output=True, text=True)
12
+ if result.returncode != 0:
13
+ logger.warning("networksetup failed: %s -> %s", " ".join(args), result.stderr.strip())
14
+ return result.returncode == 0
15
+
16
+
17
+ class SystemProxy:
18
+ def __init__(self):
19
+ self._enabled = False
20
+ self._interfaces = []
21
+ atexit.register(self._cleanup)
22
+ signal.signal(signal.SIGTERM, self._signal_handler)
23
+
24
+ def _signal_handler(self, signum, frame):
25
+ self._cleanup()
26
+ raise SystemExit(0)
27
+
28
+ def _cleanup(self):
29
+ if self._enabled:
30
+ try:
31
+ self.disable()
32
+ except Exception:
33
+ pass
34
+
35
+ def _get_active_interfaces(self):
36
+ result = subprocess.run(
37
+ ["networksetup", "-listallnetworkservices"],
38
+ capture_output=True,
39
+ text=True,
40
+ )
41
+ interfaces = []
42
+ for line in result.stdout.strip().split("\n"):
43
+ if line.startswith("*") or line.startswith("An asterisk"):
44
+ continue
45
+ if line.strip():
46
+ interfaces.append(line.strip())
47
+ return interfaces
48
+
49
+ def enable(self, socks_port, http_port):
50
+ self._interfaces = self._get_active_interfaces()
51
+ tasks = []
52
+ for iface in self._interfaces:
53
+ tasks.extend([
54
+ ["networksetup", "-setsocksfirewallproxy", iface, "127.0.0.1", str(socks_port)],
55
+ ["networksetup", "-setsocksfirewallproxystate", iface, "on"],
56
+ ["networksetup", "-setwebproxy", iface, "127.0.0.1", str(http_port)],
57
+ ["networksetup", "-setwebproxystate", iface, "on"],
58
+ ["networksetup", "-setsecurewebproxy", iface, "127.0.0.1", str(http_port)],
59
+ ["networksetup", "-setsecurewebproxystate", iface, "on"],
60
+ ])
61
+ with ThreadPoolExecutor(max_workers=8) as pool:
62
+ list(pool.map(_run_networksetup, tasks))
63
+ self._enabled = True
64
+ self._verify(socks_port, http_port)
65
+
66
+ def _verify(self, socks_port, http_port):
67
+ checks = [
68
+ ("-getsocksfirewallproxy", "-setsocksfirewallproxystate"),
69
+ ("-getwebproxy", "-setwebproxystate"),
70
+ ("-getsecurewebproxy", "-setsecurewebproxystate"),
71
+ ]
72
+ for iface in self._interfaces:
73
+ for get_cmd, set_cmd in checks:
74
+ result = subprocess.run(
75
+ ["networksetup", get_cmd, iface],
76
+ capture_output=True, text=True,
77
+ )
78
+ if "Enabled: No" in result.stdout:
79
+ logger.warning("proxy not enabled after setting: %s %s, retrying", get_cmd, iface)
80
+ _run_networksetup(["networksetup", set_cmd, iface, "on"])
81
+
82
+ def disable(self):
83
+ interfaces = self._interfaces or self._get_active_interfaces()
84
+ tasks = []
85
+ for iface in interfaces:
86
+ tasks.extend([
87
+ ["networksetup", "-setsocksfirewallproxystate", iface, "off"],
88
+ ["networksetup", "-setwebproxystate", iface, "off"],
89
+ ["networksetup", "-setsecurewebproxystate", iface, "off"],
90
+ ])
91
+ with ThreadPoolExecutor(max_workers=8) as pool:
92
+ list(pool.map(_run_networksetup, tasks))
93
+ self._enabled = False
94
+ self._interfaces = []
File without changes