Fix exit-IP checks for HTTP-only proxies, apply proxy via WinINet Connections blob and WinHTTP, improve VPN detection on legacy Server, add SOCKS5 host:port:user:pass exit parsing, and add win_compat probe for PowerShell 2.0 hosts. Co-authored-by: Cursor <cursoragent@cursor.com>
224 lines
7.1 KiB
Python
224 lines
7.1 KiB
Python
"""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("<III", 0x46, counter, flags)
|
|
+ struct.pack("<I", len(p)) + p
|
|
+ struct.pack("<I", len(b)) + b
|
|
+ struct.pack("<I", len(a)) + a
|
|
+ b"\x00" * 32
|
|
)
|
|
|
|
|
|
def _read_blob_counter() -> 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("<local>", "<local>")
|
|
.strip()
|
|
) or "<local>"
|
|
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.*;<local>",
|
|
) -> 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
|