"""Set / clear the Windows system-wide HTTP proxy. Three layers get configured so the proxy actually applies system-wide: 1. ``HKCU\\…\\Internet Settings`` — simple values WinINet reads first 2. ``Internet Settings\\Connections\\DefaultConnectionSettings`` binary blob — overrides #1 for any process that opens the per-connection structure. On enterprise images and Windows Server boxes this stale blob is the usual reason "chain says connected but the browser still leaks direct". 3. ``netsh winhttp set proxy`` — covers .NET, Windows Update, many services and headless browser components that bypass WinINet. Conflicting overrides (``AutoConfigURL`` / ``AutoDetect`` WPAD) are wiped on engage and restored to "off" on disengage. """ from __future__ import annotations import ctypes import logging import struct import subprocess import winreg log = logging.getLogger(__name__) _KEY_PATH = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings" _CONN_KEY_PATH = ( r"Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections" ) _SETTINGS_CHANGED = 39 _REFRESH = 37 # Flags inside the DefaultConnectionSettings binary blob _FLAG_DIRECT = 0x01 _FLAG_MANUAL_PROXY = 0x02 _FLAG_AUTO_CONFIG = 0x04 _FLAG_AUTO_DETECT = 0x08 def _broadcast() -> None: """Tell running apps (browsers, etc.) that proxy settings changed.""" try: inet = ctypes.windll.wininet # type: ignore[attr-defined] inet.InternetSetOptionW(0, _SETTINGS_CHANGED, 0, 0) inet.InternetSetOptionW(0, _REFRESH, 0, 0) except Exception: pass def _bump_counter(prev: bytes | None) -> int: """Each blob write must bump byte offset 4 for WinINet to re-read it.""" if not prev or len(prev) < 8: return 1 try: return int.from_bytes(prev[4:8], "little") + 1 except Exception: return 1 def _build_blob( proxy: str, bypass: str, auto_config_url: str = "", counter: int = 1, ) -> bytes: """Build the ``DefaultConnectionSettings`` REG_BINARY value. Layout (little-endian): u32 version (0x46) u32 counter u32 flags u32 proxy_len; bytes proxy u32 bypass_len; bytes bypass u32 auto_config_len; bytes auto_config bytes[32] padding (zeros) — WinINet expects trailing slack """ flags = _FLAG_DIRECT if proxy: flags |= _FLAG_MANUAL_PROXY if auto_config_url: flags |= _FLAG_AUTO_CONFIG p = proxy.encode("utf-8") b = bypass.encode("utf-8") a = auto_config_url.encode("utf-8") return ( struct.pack(" int: try: with winreg.OpenKey( winreg.HKEY_CURRENT_USER, _CONN_KEY_PATH, 0, winreg.KEY_QUERY_VALUE ) as key: val, _ = winreg.QueryValueEx(key, "DefaultConnectionSettings") return _bump_counter(val) except OSError: return 1 def _write_conn_blob(proxy: str, bypass: str) -> None: """Write per-connection binary so apps using the Connections key respect us.""" counter = _read_blob_counter() blob = _build_blob(proxy, bypass, "", counter) try: # The "Connections" key may not exist on freshly imaged Server installs. key = winreg.CreateKeyEx( winreg.HKEY_CURRENT_USER, _CONN_KEY_PATH, 0, winreg.KEY_SET_VALUE ) with key: winreg.SetValueEx(key, "DefaultConnectionSettings", 0, winreg.REG_BINARY, blob) # SavedLegacySettings mirrors the same blob on some builds; keep them in sync. winreg.SetValueEx(key, "SavedLegacySettings", 0, winreg.REG_BINARY, blob) except OSError as e: log.debug("Connections blob write failed: %s", e) def _clear_conn_blob() -> None: counter = _read_blob_counter() blob = _build_blob("", "", "", counter) try: with winreg.OpenKey( winreg.HKEY_CURRENT_USER, _CONN_KEY_PATH, 0, winreg.KEY_SET_VALUE ) as key: winreg.SetValueEx(key, "DefaultConnectionSettings", 0, winreg.REG_BINARY, blob) winreg.SetValueEx(key, "SavedLegacySettings", 0, winreg.REG_BINARY, blob) except OSError: pass def _wipe_autoconfig(key: winreg.HKEYType) -> None: """Remove WPAD / PAC overrides that would silently bypass our manual proxy.""" for name in ("AutoConfigURL", "AutoDetect"): try: winreg.DeleteValue(key, name) except OSError: pass def _set_winhttp_proxy(host: str, port: int, bypass: str) -> None: """netsh winhttp covers .NET / services / Windows Update / some browsers.""" try: bypass_list = ( bypass.replace(";", " ") .replace("", "") .strip() ) or "" subprocess.run( ["netsh", "winhttp", "set", "proxy", f"{host}:{port}", f"bypass-list={bypass_list}"], capture_output=True, text=True, timeout=15, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), ) except Exception as e: log.debug("netsh winhttp set proxy failed: %s", e) def _reset_winhttp_proxy() -> None: try: subprocess.run( ["netsh", "winhttp", "reset", "proxy"], capture_output=True, text=True, timeout=15, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), ) except Exception: pass def set_system_proxy( host: str, port: int, bypass: str = "localhost;127.*;10.*;192.168.*;", ) -> None: proxy = f"{host}:{port}" try: with winreg.OpenKey( winreg.HKEY_CURRENT_USER, _KEY_PATH, 0, winreg.KEY_SET_VALUE ) as key: winreg.SetValueEx(key, "ProxyEnable", 0, winreg.REG_DWORD, 1) winreg.SetValueEx(key, "ProxyServer", 0, winreg.REG_SZ, proxy) winreg.SetValueEx(key, "ProxyOverride", 0, winreg.REG_SZ, bypass) _wipe_autoconfig(key) except OSError: log.exception("Failed to set system proxy (HKCU root)") return _write_conn_blob(proxy, bypass) _set_winhttp_proxy(host, port, bypass) _broadcast() log.info("System proxy set %s (bypass=%s) — root + Connections + WinHTTP", proxy, bypass) def clear_system_proxy() -> None: try: with winreg.OpenKey( winreg.HKEY_CURRENT_USER, _KEY_PATH, 0, winreg.KEY_SET_VALUE ) as key: winreg.SetValueEx(key, "ProxyEnable", 0, winreg.REG_DWORD, 0) _wipe_autoconfig(key) except OSError: log.exception("Failed to clear system proxy") _clear_conn_blob() _reset_winhttp_proxy() _broadcast() log.info("System proxy cleared — root + Connections + WinHTTP") def is_system_proxy_set() -> bool: try: with winreg.OpenKey( winreg.HKEY_CURRENT_USER, _KEY_PATH, 0, winreg.KEY_QUERY_VALUE ) as key: val, _ = winreg.QueryValueEx(key, "ProxyEnable") return val == 1 except OSError: return False