mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-09 20:26:03 +02:00
Ship ShadowBroker v0.9.83 with live Infonet gate messaging and DM protocols.
Gate hashchain replication, Tor/SOCKS transport hardening, terminal session teardown, v0.9.83 UI/changelog, and release digest pins for seamless updater verification.
This commit is contained in:
@@ -51,5 +51,10 @@
|
||||
"ShadowBroker_v0.9.82.zip": "202ab043465741dcc06de57c19ec8314904332f8e818b891d7174655719d084c",
|
||||
"ShadowBroker_0.9.82_x64-setup.exe": "0eb9f2bda02ab691b39687641abc97e6bfb507b42f48de21970ad7dfb4ea15fc",
|
||||
"ShadowBroker_0.9.82_x64_en-US.msi": "ced08f930171c0c08009a958cc30b0171a09f982230fc217c6808c2ed7ab2e30"
|
||||
},
|
||||
"v0.9.83": {
|
||||
"ShadowBroker_v0.9.83.zip": "53f56631731ad3cdc7be68df09bedd6570ed91ecda6fa57c39651098e15666c7",
|
||||
"ShadowBroker_0.9.83_x64-setup.exe": "d62170af4b9df0b190832b7bb3ad6bfe8a7ac01472f2c7b39cf2a1b61edc7492",
|
||||
"ShadowBroker_0.9.83_x64_en-US.msi": "b664cc0003a29f7ce88b04c2b425643dbe7ed897342fc6e9a2378bc1910c6850"
|
||||
}
|
||||
}
|
||||
|
||||
+49
-8
@@ -1247,6 +1247,26 @@ def _local_infonet_peer_url() -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def _clear_stale_arti_sync_backoff() -> None:
|
||||
"""Drop cached Arti warmup errors once SOCKS transport is actually ready."""
|
||||
from dataclasses import replace
|
||||
|
||||
with _NODE_RUNTIME_LOCK:
|
||||
current = get_sync_state()
|
||||
error_lower = str(current.last_error or "").lower()
|
||||
if "arti" not in error_lower and "onion sync requires" not in error_lower:
|
||||
return
|
||||
set_sync_state(
|
||||
replace(
|
||||
current,
|
||||
last_error="",
|
||||
consecutive_failures=0,
|
||||
next_sync_due_at=int(time.time()),
|
||||
last_outcome="idle" if current.last_outcome == "error" else current.last_outcome,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _ensure_infonet_private_transport_ready(reason: str = "") -> bool:
|
||||
"""Warm the local onion transport before private Infonet sync.
|
||||
|
||||
@@ -1275,15 +1295,36 @@ def _ensure_infonet_private_transport_ready(reason: str = "") -> bool:
|
||||
|
||||
label = f" ({reason})" if reason else ""
|
||||
logger.info("Infonet private transport warmup starting%s", label)
|
||||
tor_result = tor_service.start(target_port=8000)
|
||||
if tor_result.get("ok"):
|
||||
from services.wormhole_supervisor import invalidate_arti_ready_cache
|
||||
|
||||
for attempt in range(3):
|
||||
tor_result = tor_service.start(target_port=8000)
|
||||
if not tor_result.get("ok"):
|
||||
logger.warning(
|
||||
"Infonet private transport warmup incomplete%s: %s",
|
||||
label,
|
||||
tor_result,
|
||||
)
|
||||
continue
|
||||
_write_env_value("MESH_ARTI_ENABLED", "true")
|
||||
get_settings.cache_clear()
|
||||
if _check_arti_ready():
|
||||
logger.info("Infonet private transport ready%s", label)
|
||||
threading.Thread(target=_swarm_bootstrap_after_transport_ready, daemon=True).start()
|
||||
return True
|
||||
logger.warning("Infonet private transport warmup incomplete%s: %s", label, tor_result)
|
||||
invalidate_arti_ready_cache()
|
||||
deadline = time.monotonic() + 30.0
|
||||
while time.monotonic() < deadline:
|
||||
if _check_arti_ready(force=True):
|
||||
logger.info("Infonet private transport ready%s", label)
|
||||
_clear_stale_arti_sync_backoff()
|
||||
threading.Thread(target=_swarm_bootstrap_after_transport_ready, daemon=True).start()
|
||||
_kick_public_sync_background(f"transport_ready{label}")
|
||||
return True
|
||||
time.sleep(1.0)
|
||||
logger.warning(
|
||||
"Infonet private transport SOCKS not ready after Tor start (attempt %d/3)%s",
|
||||
attempt + 1,
|
||||
label,
|
||||
)
|
||||
tor_service.stop()
|
||||
logger.warning("Infonet private transport warmup incomplete%s", label)
|
||||
return False
|
||||
except Exception as exc:
|
||||
logger.warning("Infonet private transport warmup failed: %s", exc)
|
||||
@@ -11704,7 +11745,7 @@ async def api_wormhole_dm_contact_sever(request: Request, peer_id: str):
|
||||
return {"ok": False, "detail": str(exc)}
|
||||
|
||||
|
||||
_WORMHOLE_PUBLIC_FIELDS = {"installed", "configured", "running", "ready"}
|
||||
_WORMHOLE_PUBLIC_FIELDS = {"installed", "configured", "running", "ready", "arti_ready"}
|
||||
|
||||
|
||||
def _redact_wormhole_status(state: dict[str, Any], authenticated: bool) -> dict[str, Any]:
|
||||
|
||||
@@ -7,7 +7,7 @@ py-modules = []
|
||||
|
||||
[project]
|
||||
name = "backend"
|
||||
version = "0.9.82"
|
||||
version = "0.9.83"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"apscheduler==3.10.3",
|
||||
|
||||
@@ -1348,7 +1348,7 @@ async def api_wormhole_dm_contact_sever(request: Request, peer_id: str):
|
||||
return {"ok": False, "detail": str(exc)}
|
||||
|
||||
|
||||
_WORMHOLE_PUBLIC_FIELDS = {"installed", "configured", "running", "ready"}
|
||||
_WORMHOLE_PUBLIC_FIELDS = {"installed", "configured", "running", "ready", "arti_ready"}
|
||||
|
||||
|
||||
def _redact_wormhole_status(state: dict[str, Any], authenticated: bool) -> dict[str, Any]:
|
||||
|
||||
@@ -33,6 +33,52 @@ TOR_INSTALL_DIR = TOR_DIR / "tor_bin"
|
||||
_STARTUP_TIMEOUT_S = 90
|
||||
_POLL_INTERVAL_S = 1.0
|
||||
|
||||
|
||||
def _arti_socks_port() -> int:
|
||||
from services.config import get_settings
|
||||
|
||||
return int(get_settings().MESH_ARTI_SOCKS_PORT or 9050)
|
||||
|
||||
|
||||
def _torrc_socks_line(socks_port: int) -> str:
|
||||
return f"SocksPort {socks_port}\n"
|
||||
|
||||
|
||||
def _torrc_has_socks_port(socks_port: int) -> bool:
|
||||
if not TORRC_PATH.exists():
|
||||
return False
|
||||
return _torrc_socks_line(socks_port) in TORRC_PATH.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _local_socks_listening(socks_port: int) -> bool:
|
||||
return _local_socks_handshake_ready(socks_port, timeout=0.75)
|
||||
|
||||
|
||||
def _local_socks_handshake_ready(socks_port: int, *, timeout: float = 5.0) -> bool:
|
||||
import socket
|
||||
|
||||
try:
|
||||
with socket.create_connection(("127.0.0.1", socks_port), timeout=timeout) as sock:
|
||||
sock.settimeout(timeout)
|
||||
sock.sendall(b"\x05\x01\x00")
|
||||
return sock.recv(2) == b"\x05\x00"
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _write_torrc(*, target_port: int, socks_port: int) -> None:
|
||||
TOR_DIR.mkdir(parents=True, exist_ok=True)
|
||||
hidden_service_dir = TOR_DIR / "hidden_service"
|
||||
hidden_service_dir.mkdir(parents=True, exist_ok=True)
|
||||
torrc_content = (
|
||||
f"DataDirectory {TOR_DATA_DIR.as_posix()}\n"
|
||||
f"HiddenServiceDir {hidden_service_dir.as_posix()}\n"
|
||||
f"HiddenServicePort {target_port} 127.0.0.1:{target_port}\n"
|
||||
f"{_torrc_socks_line(socks_port)}"
|
||||
"Log notice stderr\n"
|
||||
)
|
||||
TORRC_PATH.write_text(torrc_content, encoding="utf-8")
|
||||
|
||||
# Windows x86_64 Tor Expert Bundle URLs. Keep a fallback so first-run
|
||||
# onboarding does not break when Tor rotates point releases.
|
||||
_TOR_EXPERT_BUNDLE_URLS = [
|
||||
@@ -357,12 +403,28 @@ class TorHiddenService:
|
||||
def start(self, target_port: int = 8000) -> dict:
|
||||
"""Start Tor hidden service pointing to target_port on localhost."""
|
||||
with self._lock:
|
||||
socks_port = _arti_socks_port()
|
||||
if self._running and self._process and self._process.poll() is None:
|
||||
return {
|
||||
"ok": True,
|
||||
"onion_address": self._onion_address,
|
||||
"detail": "already running",
|
||||
}
|
||||
if _torrc_has_socks_port(socks_port) and _local_socks_handshake_ready(socks_port, timeout=1.5):
|
||||
return {
|
||||
"ok": True,
|
||||
"onion_address": self._onion_address,
|
||||
"detail": "already running",
|
||||
}
|
||||
logger.info(
|
||||
"Tor is running without a ready SOCKS proxy on port %s — restarting",
|
||||
socks_port,
|
||||
)
|
||||
try:
|
||||
self._process.terminate()
|
||||
self._process.wait(timeout=10)
|
||||
except Exception:
|
||||
try:
|
||||
self._process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
self._process = None
|
||||
self._running = False
|
||||
|
||||
self._error = ""
|
||||
tor_bin = _find_tor_binary()
|
||||
@@ -388,20 +450,9 @@ class TorHiddenService:
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
from services.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
socks_port_line = ""
|
||||
if not bool(getattr(settings, "MESH_ARTI_ENABLED", False)):
|
||||
socks_port_line = "SocksPort 9050\n"
|
||||
torrc_content = (
|
||||
f"DataDirectory {TOR_DATA_DIR.as_posix()}\n"
|
||||
f"HiddenServiceDir {hidden_service_dir.as_posix()}\n"
|
||||
f"HiddenServicePort {target_port} 127.0.0.1:{target_port}\n"
|
||||
f"{socks_port_line}"
|
||||
"Log notice stderr\n"
|
||||
)
|
||||
TORRC_PATH.write_text(torrc_content, encoding="utf-8")
|
||||
# Mesh "Arti" transport uses Tor's local SOCKS proxy for .onion peers.
|
||||
# Always publish SocksPort — MESH_ARTI_ENABLED only gates callers, not Tor.
|
||||
_write_torrc(target_port=target_port, socks_port=socks_port)
|
||||
|
||||
try:
|
||||
self._process = subprocess.Popen(
|
||||
@@ -434,15 +485,23 @@ class TorHiddenService:
|
||||
hostname = HOSTNAME_PATH.read_text().strip()
|
||||
if hostname.endswith(".onion"):
|
||||
self._onion_address = f"http://{hostname}:8000"
|
||||
logger.info("Tor hidden service ready: %s", self._onion_address)
|
||||
return {
|
||||
"ok": True,
|
||||
"onion_address": self._onion_address,
|
||||
}
|
||||
if _local_socks_handshake_ready(socks_port, timeout=3.0):
|
||||
logger.info(
|
||||
"Tor hidden service ready: %s (SOCKS %s)",
|
||||
self._onion_address,
|
||||
socks_port,
|
||||
)
|
||||
return {
|
||||
"ok": True,
|
||||
"onion_address": self._onion_address,
|
||||
}
|
||||
|
||||
time.sleep(_POLL_INTERVAL_S)
|
||||
|
||||
self._error = f"Tor did not generate hostname within {_STARTUP_TIMEOUT_S}s"
|
||||
self._error = (
|
||||
f"Tor did not publish a ready hidden service and SOCKS proxy "
|
||||
f"on port {socks_port} within {_STARTUP_TIMEOUT_S}s"
|
||||
)
|
||||
self.stop()
|
||||
return {"ok": False, "detail": self._error}
|
||||
|
||||
|
||||
@@ -27,6 +27,13 @@ _STATE_CACHE_TS = 0.0
|
||||
_STATE_CACHE_TTL_S = 2.0
|
||||
_ARTI_PROOF_CACHE: dict[str, Any] = {"port": 0, "ok": False, "ts": 0.0}
|
||||
_ARTI_PROOF_CACHE_TTL_S = 30.0
|
||||
_ARTI_STATUS_CACHE: dict[str, Any] = {"port": 0, "ready": False, "ts": 0.0}
|
||||
_ARTI_STATUS_FAIL_TTL_S = 4.0
|
||||
_ARTI_PROBE_LOCK = threading.Lock()
|
||||
_ARTI_SOCKS_FAILURES = 0
|
||||
_ARTI_LAST_TOR_RECOVERY_TS = 0.0
|
||||
_ARTI_TOR_RECOVERY_COOLDOWN_S = 45.0
|
||||
_ARTI_SOCKS_CONNECT_TIMEOUT_S = 5.0
|
||||
_PRIVATE_CLEARNET_FALLBACK_WINDOW_S = 300.0
|
||||
|
||||
BACKEND_DIR = Path(__file__).resolve().parent.parent
|
||||
@@ -70,16 +77,43 @@ _WORMHOLE_ENV_EXPLICIT = {
|
||||
"PRIVACY_CORE_MIN_VERSION",
|
||||
}
|
||||
|
||||
def _check_arti_ready() -> bool:
|
||||
from services.config import get_settings
|
||||
def invalidate_arti_ready_cache() -> None:
|
||||
_ARTI_PROOF_CACHE.update({"port": 0, "ok": False, "ts": 0.0})
|
||||
_ARTI_STATUS_CACHE.update({"port": 0, "ready": False, "ts": 0.0})
|
||||
|
||||
settings = get_settings()
|
||||
if not bool(settings.MESH_ARTI_ENABLED):
|
||||
return False
|
||||
socks_port = int(settings.MESH_ARTI_SOCKS_PORT or 9050)
|
||||
|
||||
def _maybe_recover_tor_socks_transport(socks_port: int) -> None:
|
||||
global _ARTI_SOCKS_FAILURES, _ARTI_LAST_TOR_RECOVERY_TS
|
||||
|
||||
_ARTI_SOCKS_FAILURES += 1
|
||||
if _ARTI_SOCKS_FAILURES < 3:
|
||||
return
|
||||
now = time.time()
|
||||
if (now - _ARTI_LAST_TOR_RECOVERY_TS) < _ARTI_TOR_RECOVERY_COOLDOWN_S:
|
||||
return
|
||||
_ARTI_LAST_TOR_RECOVERY_TS = now
|
||||
_ARTI_SOCKS_FAILURES = 0
|
||||
try:
|
||||
with socket.create_connection((WORMHOLE_HOST, socks_port), timeout=2.0) as sock:
|
||||
# SOCKS5 greeting: version 5, 1 auth method, no-auth.
|
||||
from services.tor_hidden_service import tor_service
|
||||
|
||||
logger.warning(
|
||||
"Tor SOCKS on port %s is wedged — recycling Tor hidden service",
|
||||
socks_port,
|
||||
)
|
||||
tor_service.stop()
|
||||
tor_service.start(target_port=8000)
|
||||
invalidate_arti_ready_cache()
|
||||
except Exception as exc:
|
||||
logger.warning("Tor SOCKS recovery failed: %s", exc)
|
||||
|
||||
|
||||
def _probe_arti_socks_ready(socks_port: int) -> bool:
|
||||
try:
|
||||
with socket.create_connection(
|
||||
(WORMHOLE_HOST, socks_port),
|
||||
timeout=_ARTI_SOCKS_CONNECT_TIMEOUT_S,
|
||||
) as sock:
|
||||
sock.settimeout(_ARTI_SOCKS_CONNECT_TIMEOUT_S)
|
||||
sock.sendall(b"\x05\x01\x00")
|
||||
response = sock.recv(2)
|
||||
if response != b"\x05\x00":
|
||||
@@ -88,6 +122,53 @@ def _check_arti_ready() -> bool:
|
||||
except Exception as exc:
|
||||
logger.warning("Arti SOCKS check failed on port %s: %s", socks_port, exc)
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _check_arti_ready(*, force: bool = False) -> bool:
|
||||
from services.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
if not bool(settings.MESH_ARTI_ENABLED):
|
||||
return False
|
||||
socks_port = int(settings.MESH_ARTI_SOCKS_PORT or 9050)
|
||||
now = time.time()
|
||||
if not force:
|
||||
if (
|
||||
int(_ARTI_STATUS_CACHE.get("port", 0) or 0) == socks_port
|
||||
and (now - float(_ARTI_STATUS_CACHE.get("ts", 0.0) or 0.0)) < _ARTI_STATUS_FAIL_TTL_S
|
||||
):
|
||||
return bool(_ARTI_STATUS_CACHE.get("ready"))
|
||||
if (
|
||||
int(_ARTI_PROOF_CACHE.get("port", 0) or 0) == socks_port
|
||||
and bool(_ARTI_PROOF_CACHE.get("ok"))
|
||||
and (now - float(_ARTI_PROOF_CACHE.get("ts", 0.0) or 0.0)) < _ARTI_PROOF_CACHE_TTL_S
|
||||
):
|
||||
return True
|
||||
|
||||
with _ARTI_PROBE_LOCK:
|
||||
now = time.time()
|
||||
if not force:
|
||||
if (
|
||||
int(_ARTI_STATUS_CACHE.get("port", 0) or 0) == socks_port
|
||||
and (now - float(_ARTI_STATUS_CACHE.get("ts", 0.0) or 0.0)) < _ARTI_STATUS_FAIL_TTL_S
|
||||
):
|
||||
return bool(_ARTI_STATUS_CACHE.get("ready"))
|
||||
if (
|
||||
int(_ARTI_PROOF_CACHE.get("port", 0) or 0) == socks_port
|
||||
and bool(_ARTI_PROOF_CACHE.get("ok"))
|
||||
and (now - float(_ARTI_PROOF_CACHE.get("ts", 0.0) or 0.0)) < _ARTI_PROOF_CACHE_TTL_S
|
||||
):
|
||||
return True
|
||||
|
||||
if not _probe_arti_socks_ready(socks_port):
|
||||
_ARTI_STATUS_CACHE.update({"port": socks_port, "ready": False, "ts": now})
|
||||
_maybe_recover_tor_socks_transport(socks_port)
|
||||
return False
|
||||
|
||||
global _ARTI_SOCKS_FAILURES
|
||||
_ARTI_SOCKS_FAILURES = 0
|
||||
_ARTI_STATUS_CACHE.update({"port": socks_port, "ready": True, "ts": now})
|
||||
|
||||
now = time.time()
|
||||
if (
|
||||
@@ -110,12 +191,13 @@ def _check_arti_ready() -> bool:
|
||||
is_tor = bool(payload.get("IsTor")) or bool(payload.get("is_tor"))
|
||||
if not (response.ok and is_tor):
|
||||
logger.warning(
|
||||
"Arti Tor proof failed (status=%s is_tor=%s) — SOCKS is up, using Arti anyway",
|
||||
"Arti Tor proof failed (status=%s is_tor=%s)",
|
||||
getattr(response, "status_code", "unknown"),
|
||||
payload.get("IsTor", payload.get("is_tor")),
|
||||
)
|
||||
_ARTI_PROOF_CACHE.update({"port": socks_port, "ok": True, "ts": now})
|
||||
return True
|
||||
_ARTI_PROOF_CACHE.update({"port": socks_port, "ok": False, "ts": now})
|
||||
_ARTI_STATUS_CACHE.update({"port": socks_port, "ready": False, "ts": now})
|
||||
return False
|
||||
_ARTI_PROOF_CACHE.update({"port": socks_port, "ok": True, "ts": now})
|
||||
return True
|
||||
except Exception as exc:
|
||||
|
||||
@@ -51,6 +51,9 @@ class _FakeSocket:
|
||||
def recv(self, _n: int) -> bytes:
|
||||
return self._handshake_response
|
||||
|
||||
def settimeout(self, _timeout: float) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, *, ok: bool, payload: dict[str, Any], status_code: int = 200) -> None:
|
||||
@@ -76,8 +79,10 @@ def _stub_settings(monkeypatch, *, enabled: bool = True, port: int = 9050) -> No
|
||||
monkeypatch.setattr(
|
||||
"services.config.get_settings", _get_settings, raising=False
|
||||
)
|
||||
# Reset proof cache so each test starts clean.
|
||||
# Reset proof/status cache so each test starts clean.
|
||||
wormhole_supervisor._ARTI_PROOF_CACHE.update({"port": 0, "ok": False, "ts": 0.0})
|
||||
wormhole_supervisor._ARTI_STATUS_CACHE.update({"port": 0, "ready": False, "ts": 0.0})
|
||||
wormhole_supervisor._ARTI_SOCKS_FAILURES = 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Tor hidden service must always publish the mesh SOCKS port."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from services import tor_hidden_service as tor_svc
|
||||
|
||||
|
||||
def test_write_torrc_always_includes_socks_port(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(tor_svc, "TOR_DIR", tmp_path)
|
||||
monkeypatch.setattr(tor_svc, "TORRC_PATH", tmp_path / "torrc")
|
||||
monkeypatch.setattr(tor_svc, "TOR_DATA_DIR", tmp_path / "data")
|
||||
|
||||
tor_svc._write_torrc(target_port=8000, socks_port=19050)
|
||||
|
||||
content = tor_svc.TORRC_PATH.read_text(encoding="utf-8")
|
||||
assert "SocksPort 19050" in content
|
||||
assert "HiddenServicePort 8000 127.0.0.1:8000" in content
|
||||
|
||||
|
||||
def test_torrc_has_socks_port_detects_missing_line(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(tor_svc, "TORRC_PATH", tmp_path / "torrc")
|
||||
tor_svc.TORRC_PATH.write_text("HiddenServicePort 8000 127.0.0.1:8000\n", encoding="utf-8")
|
||||
|
||||
assert tor_svc._torrc_has_socks_port(9050) is False
|
||||
|
||||
tor_svc.TORRC_PATH.write_text("SocksPort 9050\n", encoding="utf-8")
|
||||
assert tor_svc._torrc_has_socks_port(9050) is True
|
||||
|
||||
|
||||
def test_local_socks_handshake_ready_accepts_valid_response(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
class FakeSock:
|
||||
def __init__(self) -> None:
|
||||
self._sent = b""
|
||||
|
||||
def settimeout(self, timeout: float) -> None:
|
||||
return None
|
||||
|
||||
def sendall(self, payload: bytes) -> None:
|
||||
self._sent = payload
|
||||
|
||||
def recv(self, size: int) -> bytes:
|
||||
assert self._sent == b"\x05\x01\x00"
|
||||
return b"\x05\x00"
|
||||
|
||||
def __enter__(self) -> "FakeSock":
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: object) -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(
|
||||
socket,
|
||||
"create_connection",
|
||||
lambda *_args, **_kwargs: FakeSock(),
|
||||
)
|
||||
assert tor_svc._local_socks_handshake_ready(9050) is True
|
||||
Reference in New Issue
Block a user