Files
PROXY_GOD_MAC/proxy_chain_manager/sysproxy.py
2026-05-23 22:09:43 -07:00

612 lines
21 KiB
Python

"""Set / clear the Windows system-wide HTTP proxy.
Layers 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.
3. ``HKLM\\\\Internet Settings`` mirror (Admin only) — required on Server when
``ProxySettingsPerUser=0`` is set by Group Policy / baseline image.
4. ``netsh winhttp set proxy`` — .NET, Windows Update, services and
headless browser components that bypass WinINet.
We also actively detect and warn about:
- ``ProxySettingsPerUser=0`` → HKCU is ignored; we fix it when Admin.
- Group Policy proxy keys → those override us; we surface the path so the
user can verify or remove them.
- ``AutoConfigURL`` / ``AutoDetect`` (WPAD) → wiped on engage.
"""
from __future__ import annotations
import ctypes
import logging
import sys
import struct
import subprocess
import winreg
log = logging.getLogger(__name__)
if sys.platform == "darwin":
from .macos_privileged import run_shell, run_shell_as_admin, shell_quote
_KEY_PATH = r"Software\Microsoft\Windows\CurrentVersion\Internet Settings"
_CONN_KEY_PATH = (
r"Software\Microsoft\Windows\CurrentVersion\Internet Settings\Connections"
)
_HKLM_INET_PATH = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings"
_HKLM_CONN_PATH = (
r"SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\Connections"
)
_POLICY_PATHS = (
(winreg.HKEY_LOCAL_MACHINE,
r"SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings"),
(winreg.HKEY_CURRENT_USER,
r"SOFTWARE\Policies\Microsoft\Windows\CurrentVersion\Internet Settings"),
(winreg.HKEY_LOCAL_MACHINE,
r"SOFTWARE\Policies\Microsoft\Internet Explorer\Control Panel"),
)
_SETTINGS_CHANGED = 39
_REFRESH = 37
_FLAG_DIRECT = 0x01
_FLAG_MANUAL_PROXY = 0x02
_FLAG_AUTO_CONFIG = 0x04
_FLAG_AUTO_DETECT = 0x08
def _mac_network_services() -> list[str]:
if sys.platform != "darwin":
return []
try:
r = subprocess.run(
["networksetup", "-listallnetworkservices"],
capture_output=True,
text=True,
timeout=12,
)
except Exception:
return []
services: list[str] = []
for line in (r.stdout or "").splitlines():
name = line.strip()
if not name or name.startswith("An asterisk"):
continue
services.append(name.lstrip("*").strip())
return services
def _mac_run_networksetup(args: list[str]) -> bool:
try:
r = subprocess.run(["networksetup", *args], capture_output=True, text=True, timeout=20)
if r.returncode != 0:
log.debug("networksetup failed (%s): %s", args, (r.stderr or r.stdout or "").strip())
return r.returncode == 0
except Exception as exc:
log.debug("networksetup failed (%s): %s", args, exc)
return False
def _mac_networksetup_script(commands: list[list[str]]) -> str:
lines = ["set -e"]
for args in commands:
lines.append("networksetup " + " ".join(shell_quote(a) for a in args))
return "\n".join(lines)
def _mac_set_system_proxy(host: str, port: int, bypass: str) -> None:
bypass_hosts = [
h.strip().replace("*", "")
for h in bypass.replace(";", ",").split(",")
if h.strip() and h.strip() != "<local>"
]
services = _mac_network_services()
commands: list[list[str]] = []
for service in services:
commands.extend([
["-setwebproxy", service, host, str(port)],
["-setsecurewebproxy", service, host, str(port)],
["-setwebproxystate", service, "on"],
["-setsecurewebproxystate", service, "on"],
])
if bypass_hosts:
commands.append(["-setproxybypassdomains", service, *bypass_hosts])
script = _mac_networksetup_script(commands)
code, _, err = run_shell(script)
if code != 0:
log.info("networksetup needs administrator approval; requesting macOS credentials.")
code, _, err = run_shell_as_admin(script)
if code != 0:
raise RuntimeError(f"macOS system proxy apply failed: {err.strip() or 'networksetup failed'}")
log.info("macOS system proxy set to %s:%s for %d service(s)", host, port, len(services))
def _mac_clear_system_proxy() -> None:
services = _mac_network_services()
commands = []
for service in services:
commands.extend([
["-setwebproxystate", service, "off"],
["-setsecurewebproxystate", service, "off"],
])
if not commands:
return
script = _mac_networksetup_script(commands)
code, _, err = run_shell(script)
if code != 0:
code, _, err = run_shell_as_admin(script)
if code != 0:
raise RuntimeError(f"macOS system proxy clear failed: {err.strip() or 'networksetup failed'}")
log.info("macOS system proxy cleared for %d service(s)", len(services))
def _mac_system_proxy_set() -> bool:
for service in _mac_network_services():
try:
r = subprocess.run(
["networksetup", "-getwebproxy", service],
capture_output=True,
text=True,
timeout=8,
)
if "Enabled: Yes" in (r.stdout or ""):
return True
except Exception:
continue
return False
def _is_admin() -> bool:
try:
return ctypes.windll.shell32.IsUserAnAdmin() != 0 # type: ignore[attr-defined]
except Exception:
return False
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:
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:
"""REG_BINARY layout for DefaultConnectionSettings (little-endian)."""
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(hive: int, path: str) -> int:
try:
with winreg.OpenKey(hive, 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(hive: int, path: str, proxy: str, bypass: str) -> bool:
counter = _read_blob_counter(hive, path)
blob = _build_blob(proxy, bypass, "", counter)
try:
key = winreg.CreateKeyEx(hive, path, 0, winreg.KEY_SET_VALUE)
with key:
winreg.SetValueEx(key, "DefaultConnectionSettings", 0, winreg.REG_BINARY, blob)
winreg.SetValueEx(key, "SavedLegacySettings", 0, winreg.REG_BINARY, blob)
return True
except OSError as e:
log.debug("Connections blob write failed (%s): %s", path, e)
return False
def _clear_conn_blob(hive: int, path: str) -> None:
counter = _read_blob_counter(hive, path)
blob = _build_blob("", "", "", counter)
try:
with winreg.OpenKey(hive, 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:
for name in ("AutoConfigURL", "AutoDetect"):
try:
winreg.DeleteValue(key, name)
except OSError:
pass
def _set_winhttp_proxy(host: str, port: int, bypass: str) -> None:
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
# ── 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``.
When 0, Windows ignores HKCU proxy values and reads only HKLM. Set on many
Server images and AD policy baselines — the #1 cause of "set the proxy but
the browser still goes direct" on Server.
"""
try:
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE, _HKLM_INET_PATH, 0, winreg.KEY_QUERY_VALUE
) as key:
val, _ = winreg.QueryValueEx(key, "ProxySettingsPerUser")
return int(val)
except OSError:
return None
def _fix_proxy_settings_per_user() -> bool:
"""Force ``ProxySettingsPerUser=1`` (per-user proxy honored). Admin only."""
if not _is_admin():
return False
try:
with winreg.CreateKeyEx(
winreg.HKEY_LOCAL_MACHINE, _HKLM_INET_PATH, 0, winreg.KEY_SET_VALUE
) as key:
winreg.SetValueEx(key, "ProxySettingsPerUser", 0, winreg.REG_DWORD, 1)
log.info("HKLM\\\\ProxySettingsPerUser → 1 (was %s)", _read_proxy_settings_per_user())
return True
except OSError as e:
log.warning("Could not fix ProxySettingsPerUser: %s", e)
return False
def _set_hklm_root(proxy: str, bypass: str) -> bool:
"""Mirror the per-user keys at HKLM so machine-scope WinINet reads honor us."""
if not _is_admin():
return False
try:
with winreg.CreateKeyEx(
winreg.HKEY_LOCAL_MACHINE, _HKLM_INET_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)
return True
except OSError as e:
log.warning("HKLM root proxy write failed: %s", e)
return False
def _clear_hklm_root() -> None:
if not _is_admin():
return
try:
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE, _HKLM_INET_PATH, 0, winreg.KEY_SET_VALUE
) as key:
winreg.SetValueEx(key, "ProxyEnable", 0, winreg.REG_DWORD, 0)
_wipe_autoconfig(key)
except OSError:
pass
def detect_policy_overrides() -> list[str]:
if sys.platform != "win32":
return []
"""Return human-readable paths of Group Policy proxy locks that beat us.
Anything we find here will silently override our manual proxy. We do *not*
delete policy keys (admins set those deliberately) — we just surface them.
"""
hits: list[str] = []
proxy_value_names = ("ProxyServer", "ProxyEnable", "AutoConfigURL", "ProxyOverride")
for hive, path in _POLICY_PATHS:
try:
with winreg.OpenKey(hive, path, 0, winreg.KEY_QUERY_VALUE) as key:
for n in proxy_value_names:
try:
winreg.QueryValueEx(key, n)
hive_name = "HKLM" if hive == winreg.HKEY_LOCAL_MACHINE else "HKCU"
hits.append(f"{hive_name}\\{path}\\{n}")
except OSError:
continue
except OSError:
continue
return hits
def _read_str(hive: int, path: str, name: str) -> str:
try:
with winreg.OpenKey(hive, path, 0, winreg.KEY_QUERY_VALUE) as key:
val, _ = winreg.QueryValueEx(key, name)
return str(val)
except OSError:
return ""
def _read_dword(hive: int, path: str, name: str) -> int | None:
try:
with winreg.OpenKey(hive, path, 0, winreg.KEY_QUERY_VALUE) as key:
val, _ = winreg.QueryValueEx(key, name)
return int(val)
except OSError:
return None
def _read_winhttp_proxy() -> str:
try:
r = subprocess.run(
["netsh", "winhttp", "show", "proxy"],
capture_output=True, text=True, timeout=10,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
for ln in (r.stdout or "").splitlines():
if "proxy" in ln.lower() and ":" in ln:
return ln.strip()
return (r.stdout or "").strip().splitlines()[-1] if r.stdout else ""
except Exception:
return ""
def diagnose_system_proxy() -> list[str]:
if sys.platform == "darwin":
out = []
for service in _mac_network_services()[:8]:
try:
r = subprocess.run(
["networksetup", "-getwebproxy", service],
capture_output=True,
text=True,
timeout=8,
)
first = " ".join((r.stdout or "").splitlines()[:3])
except Exception as exc:
first = str(exc)
out.append(f"macOS {service}: {first}")
return out or ["macOS network services not detected"]
"""Return a list of human-readable lines covering every place proxy can
be configured. Logged on each engage so "chain green but browser leaks"
is debuggable without RegEdit gymnastics.
"""
lines: list[str] = []
per_user = _read_proxy_settings_per_user()
per_user_msg = (
"1 (HKCU honored)" if per_user == 1
else "0 (HKCU IGNORED — Server lock!)" if per_user == 0
else "(unset → defaults to HKCU)"
)
lines.append(f"ProxySettingsPerUser = {per_user_msg}")
en_u = _read_dword(winreg.HKEY_CURRENT_USER, _KEY_PATH, "ProxyEnable")
ps_u = _read_str(winreg.HKEY_CURRENT_USER, _KEY_PATH, "ProxyServer")
lines.append(f"HKCU Enable={en_u} Server='{ps_u}'")
en_m = _read_dword(winreg.HKEY_LOCAL_MACHINE, _HKLM_INET_PATH, "ProxyEnable")
ps_m = _read_str(winreg.HKEY_LOCAL_MACHINE, _HKLM_INET_PATH, "ProxyServer")
lines.append(f"HKLM Enable={en_m} Server='{ps_m}'")
ac_u = _read_str(winreg.HKEY_CURRENT_USER, _KEY_PATH, "AutoConfigURL")
ad_u = _read_dword(winreg.HKEY_CURRENT_USER, _KEY_PATH, "AutoDetect")
if ac_u or ad_u:
lines.append(f"WPAD/PAC: AutoConfigURL='{ac_u}' AutoDetect={ad_u}")
winhttp = _read_winhttp_proxy()
if winhttp:
lines.append(f"WinHTTP: {winhttp}")
pol = detect_policy_overrides()
if pol:
lines.append(f"Policy overrides (these BEAT us): {len(pol)} entries")
for p in pol[:5]:
lines.append(f" ! {p}")
if len(pol) > 5:
lines.append(f" ! …and {len(pol) - 5} more")
else:
lines.append("Policy overrides: none")
return lines
def set_system_proxy(
host: str,
port: int,
bypass: str = "localhost;127.*;10.*;192.168.*;<local>",
) -> None:
if sys.platform == "darwin":
_mac_set_system_proxy(host, port, bypass)
return
proxy = f"{host}:{port}"
# HKCU root
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(winreg.HKEY_CURRENT_USER, _CONN_KEY_PATH, proxy, bypass)
# If ProxySettingsPerUser=0 (Server / GPO baseline), HKCU is ignored.
# When Admin, fix the flag AND mirror to HKLM so the proxy actually applies.
per_user = _read_proxy_settings_per_user()
if per_user == 0:
log.warning(
"ProxySettingsPerUser=0 detected — Windows is ignoring per-user "
"proxy. Attempting Admin fix (HKLM mirror)."
)
if _is_admin():
_fix_proxy_settings_per_user()
_set_hklm_root(proxy, bypass)
_write_conn_blob(winreg.HKEY_LOCAL_MACHINE, _HKLM_CONN_PATH, proxy, bypass)
else:
log.warning(
"Not running as Admin — cannot fix ProxySettingsPerUser. "
"Re-launch elevated for proxy to apply on this Server image."
)
# On Admin runs always mirror HKLM root too. Harmless when PerUser=1 and
# correct when PerUser=0 or absent.
if _is_admin():
_set_hklm_root(proxy, bypass)
_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 + %d browser policy",
proxy, bypass, n_browser,
)
def clear_system_proxy() -> None:
if sys.platform == "darwin":
_mac_clear_system_proxy()
return
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(winreg.HKEY_CURRENT_USER, _CONN_KEY_PATH)
if _is_admin():
_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 + browser policy")
def is_system_proxy_set() -> bool:
if sys.platform == "darwin":
return _mac_system_proxy_set()
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