The AmneziaWG sidecar failed at startup with "Physical interface for Amnezia UDP transport not found". Three compounding causes: - physical_network() spawned PowerShell and filtered on HardwareInterface, which wrongly excludes Hyper-V / WSL / Docker vEthernet uplinks, so every AWG key failed identically on such hosts; a transient miss aborted the whole startup with a hard OSError. - A successful tunnel changes the outbound IP (e.g. 10.9.0.49), which the network monitor misread as a real network change and reconnected on, re-running the resolve while the tunnel held the default route — a self-inflicted flap. Replace the PowerShell probe with a pure-WinAPI resolver: extend win_netinfo (GetAdaptersAddresses gateways, DNS, OperStatus, IfType, Ipv4Metric) and pick the up adapter that owns an IPv4 gateway and a routable address, lowest Ipv4Metric. Requiring a gateway naturally excludes the WireGuard/AWG TUN (it has none), so the relay never binds back onto its own tunnel; dropping HardwareInterface fixes vEthernet uplinks. The owned Go core still hard-requires a non-zero interface index (IP_UNICAST_IF, no default-bind fallback), so a transient miss is retried instead of aborting. Ignore network-change events whose address is the active tunnel's own, breaking the reconnect loop. Extract the shared sidecar seam — the loopback SOCKS relay must accept a TCP connection before the sing-box front dials it — into engines/sidecar.wait_for_loopback_relay(). Hysteria delegates to it (no behavior change); Amnezia adopts it as a confirming gate after relay_ready, so both cores share one readiness contract. The divergent domain models (config handoff, handshake detection, failure taxonomy, recovery) intentionally stay per-engine. Verified on Linux: 890 unit tests pass (offscreen). Tunnel-comes-up and Hysteria startup still need on-device confirmation on Windows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HAjvZYzPW2yTtToKJXGdbS
39 lines
1.3 KiB
Python
39 lines
1.3 KiB
Python
"""Loopback relay readiness — the shared seam between sing-box and each core."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import socket
|
|
import time
|
|
from typing import Callable
|
|
|
|
from ...constants import PROXY_HOST
|
|
from ...platform.windows.subprocess_utils import sleep_with_events
|
|
|
|
|
|
def wait_for_loopback_relay(
|
|
port: int,
|
|
*,
|
|
host: str = PROXY_HOST,
|
|
timeout: float = 10.0,
|
|
should_continue: Callable[[], bool] | None = None,
|
|
connect_timeout: float = 0.15,
|
|
step: float = 0.05,
|
|
) -> bool:
|
|
"""Wait until a core's local SOCKS relay accepts a loopback TCP connection.
|
|
|
|
Returns ``True`` as soon as a connection succeeds, ``False`` if the deadline
|
|
passes or ``should_continue`` reports the attempt should be abandoned (e.g.
|
|
the owned process died or the transition was superseded). No payload is
|
|
sent — this only proves the listener is bound and accepting, which is the
|
|
precondition the sing-box front relies on before dialing the relay.
|
|
"""
|
|
deadline = time.monotonic() + max(0.0, timeout)
|
|
while time.monotonic() < deadline:
|
|
if should_continue is not None and not should_continue():
|
|
return False
|
|
try:
|
|
with socket.create_connection((host, int(port)), timeout=connect_timeout):
|
|
return True
|
|
except OSError:
|
|
sleep_with_events(step)
|
|
return False
|