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:
BigBodyCobain
2026-06-15 15:37:29 -06:00
parent 8fcb01276c
commit 5ede669a12
35 changed files with 589 additions and 263 deletions
+5
View File
@@ -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
View File
@@ -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]:
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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]:
+84 -25
View File
@@ -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}
+93 -11
View File
@@ -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