Save-as-final-hop button, browser proxy policy + QUIC kill, HTTPS tunnel probe

Fixes 'exit proxy never used in chain' and 'browsers break when chain is green'. Chain Builder gets a Save-as-final-hop button that appends the typed exit proxy to the manual chain. On engage, Chromium browsers get pinned to our proxy via HKLM policy (ProxyMode=fixed_servers, ProxyServer, ProxyBypassList) and QuicAllowed=0 so HTTP/3 doesn't bypass HTTP proxies and stall under the kill-switch. Chain start now runs check_https_tunnel and warns clearly when proxies forward HTTP but refuse CONNECT — the real cause of green-chain-yet-blank-browser.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Indiana Holmes
2026-05-16 23:30:29 -07:00
parent 094e0577fb
commit f9c548872b
5 changed files with 160 additions and 4 deletions

View File

@@ -171,6 +171,66 @@ def _reset_winhttp_proxy() -> None:
pass
# ── browser-policy proxy (Chrome / Edge) ─────────────────────────────────────
#
# Why this exists: when you set WebRTC policy on Chrome/Edge, the browser sees
# itself as "managed". Some managed-Chrome builds then *ignore* the system
# proxy unless an explicit ``ProxyServer`` policy is also set. They also keep
# trying HTTP/3 over QUIC (UDP/443), which bypasses HTTP proxies entirely —
# under the kill-switch those UDP packets get dropped and pages hang forever.
#
# Setting these policy values pins the browser to our chain (HTTP proxy) and
# forces it off QUIC. Cleared cleanly on disengage.
_BROWSER_POLICY_PATHS = (
r"SOFTWARE\Policies\Google\Chrome",
r"SOFTWARE\Policies\Microsoft\Edge",
r"SOFTWARE\Policies\Chromium",
)
def _set_browser_proxy_policy(host: str, port: int, bypass: str) -> int:
"""Pin Chromium-based browsers (Chrome, Edge, Chromium) to our local proxy
via Group Policy registry, and disable QUIC. Returns count of keys updated.
Requires Admin to write under HKLM\\SOFTWARE\\Policies. Silently skips when
not elevated — the user gets a warning in the service log instead.
"""
if not _is_admin():
return 0
proxy_value = f"http={host}:{port};https={host}:{port}"
bypass_value = bypass.replace(";", ",")
written = 0
for path in _BROWSER_POLICY_PATHS:
try:
with winreg.CreateKeyEx(
winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_SET_VALUE
) as key:
winreg.SetValueEx(key, "ProxyMode", 0, winreg.REG_SZ, "fixed_servers")
winreg.SetValueEx(key, "ProxyServer", 0, winreg.REG_SZ, proxy_value)
winreg.SetValueEx(key, "ProxyBypassList", 0, winreg.REG_SZ, bypass_value)
winreg.SetValueEx(key, "QuicAllowed", 0, winreg.REG_DWORD, 0)
written += 1
except OSError:
continue
return written
def _clear_browser_proxy_policy() -> None:
if not _is_admin():
return
for path in _BROWSER_POLICY_PATHS:
try:
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_SET_VALUE
) as key:
for name in ("ProxyMode", "ProxyServer", "ProxyBypassList", "QuicAllowed"):
try:
winreg.DeleteValue(key, name)
except OSError:
pass
except OSError:
continue
def _read_proxy_settings_per_user() -> int | None:
"""``HKLM\\\\Internet Settings\\ProxySettingsPerUser``.
@@ -382,8 +442,12 @@ def set_system_proxy(
_write_conn_blob(winreg.HKEY_LOCAL_MACHINE, _HKLM_CONN_PATH, proxy, bypass)
_set_winhttp_proxy(host, port, bypass)
n_browser = _set_browser_proxy_policy(host, port, bypass)
_broadcast()
log.info("System proxy set %s (bypass=%s) — HKCU + HKLM(adm) + Connections + WinHTTP", proxy, bypass)
log.info(
"System proxy set %s (bypass=%s) — HKCU + HKLM(adm) + Connections + WinHTTP + %d browser policy",
proxy, bypass, n_browser,
)
def clear_system_proxy() -> None:
@@ -401,8 +465,9 @@ def clear_system_proxy() -> None:
_clear_hklm_root()
_clear_conn_blob(winreg.HKEY_LOCAL_MACHINE, _HKLM_CONN_PATH)
_reset_winhttp_proxy()
_clear_browser_proxy_policy()
_broadcast()
log.info("System proxy cleared — HKCU + HKLM(adm) + Connections + WinHTTP")
log.info("System proxy cleared — HKCU + HKLM(adm) + Connections + WinHTTP + browser policy")
def is_system_proxy_set() -> bool: