Skip to content

System proxy detection

proxylib.os

Platform-agnostic system proxy detection.

Dispatches to a per-OS backend (:mod:.nt on Windows, :mod:.darwin on macOS, :mod:.posix elsewhere) and, via :func:auto_proxy, falls back to WPAD discovery (:mod:proxylib.pac.wpad) when the OS/env has no explicit proxy configured.

Both :func:system_proxy and :func:auto_proxy take a provider:

  • "python" (default): proxylib's own detection + PAC-via-dukpy path -- works everywhere, including sandboxes/testing.
  • "system": outsources resolution to the OS's native proxy engine (WinHttpProxyMap on Windows, CFNetworkProxyMap on macOS, LibProxyMap on POSIX if its shared library is loadable -- falls back to "python" there if not). These already do their own WPAD/PAC, so :func:auto_proxy doesn't run its own WPAD fallback on top of them.

__all__ = ('system_proxy', 'auto_proxy') module-attribute

auto_proxy(provider='python', **urlopen_kwargs)

Resolve the effective ProxyMap for this machine.

Order: OS-native settings / env vars (:func:system_proxy) → if that yields a PAC URL, load it → if it yields no usable config at all (the "python" provider's own signal for "OS has nothing configured"), try WPAD discovery → otherwise DIRECT.

Source code in src/proxylib/os/__init__.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def auto_proxy(provider: str = "python", **urlopen_kwargs) -> ProxyMap:
    """Resolve the effective ``ProxyMap`` for this machine.

    Order: OS-native settings / env vars (:func:`system_proxy`) → if that
    yields a PAC URL, load it → if it yields no usable config at all (the
    ``"python"`` provider's own signal for "OS has nothing configured"),
    try WPAD discovery → otherwise DIRECT.
    """
    proxy = system_proxy(provider)
    if isinstance(proxy, str):
        return load_pac(proxy, **urlopen_kwargs)
    if isinstance(proxy, SimpleProxyMap) and proxy[""] == (None,):
        from ..pac.wpad import discover

        discovered = discover(**urlopen_kwargs)
        if discovered is not None:
            return discovered
    return proxy

system_proxy(provider='python')

Read this machine's proxy configuration. See the module docstring for what provider means.

Source code in src/proxylib/os/__init__.py
56
57
58
59
60
61
62
63
def system_proxy(provider: str = "python") -> "ProxyMap|str":
    """Read this machine's proxy configuration. See the module docstring
    for what ``provider`` means."""
    if provider == "system":
        native = _native_system_provider()
        if native is not None:
            return native
    return _python_system_proxy()

proxylib.os.nt

Windows system proxy detection via the WinHTTP "IE proxy config" API.

Uses WinHttpGetIEProxyConfigForCurrentUser (stdlib ctypes, no extra dependency) rather than hand-parsing the registry's undocumented Connections\DefaultConnectionSettings/SavedLegacySettings binary blob. That blob is the underlying data source (its flags DWORD at offset 8 has bit 0x08 set when "Automatically detect settings" is on — verified empirically), but its trailing layout has known version differences across Windows releases; the WinHTTP API is Microsoft's supported way to read the same data without guessing byte offsets. This mirrors what Chromium does.

__all__ = ('system_proxy', 'WinHttpProxyMap') module-attribute

WinHttpProxyMap()

Bases: ProxyMap

A ProxyMap backed by WinHTTP's own autoproxy engine (WinHttpGetProxyForUrl).

Unlike :func:system_proxy's Python-side WPAD/PAC path, this outsources WPAD/DHCP-252 discovery, NTLM/Kerberos SSO for fetching the PAC script, and PAC JS execution itself to WinHTTP -- no dukpy needed on Windows. Falls back to the static manual proxy (from WinHttpGetIEProxyConfigForCurrentUser) when autoproxy isn't configured, or when WinHttpGetProxyForUrl fails; DIRECT if nothing at all is configured.

Reuses one WinHttpOpen session handle for its lifetime (release it with :meth:close, or let garbage collection do it) -- opening a session per lookup would be wasteful, and WinHTTP's own autoproxy result caching is scoped to the session.

Source code in src/proxylib/os/nt.py
245
246
247
248
def __init__(self) -> None:
    self._hsession = _winhttp.WinHttpOpen(
        "proxylib", _WINHTTP_ACCESS_TYPE_NO_PROXY, None, None, 0
    )

__slots__ = ('_hsession',) class-attribute instance-attribute

__del__()

Source code in src/proxylib/os/nt.py
255
256
def __del__(self) -> None:
    self.close()

__getitem__(url)

Source code in src/proxylib/os/nt.py
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
def __getitem__(self, url: str) -> "Iterable[Optional[Proxy]]":
    if not self._hsession:
        raise KeyError(url)
    ie = _read_ie_proxy_config()
    auto_detect = bool(ie and ie.auto_detect)
    auto_config_url = ie.auto_config_url if ie else None

    if auto_detect or auto_config_url:
        proxy, _bypass, ok, last_error = _query_proxy_for_url(
            self._hsession, url, auto_detect, auto_config_url, auto_logon=False
        )
        # Documented WinHTTP pattern: only retry with
        # fAutoLogonIfChallenged=True on an actual NTLM/Kerberos
        # challenge -- always-True disables WinHTTP's own autoproxy
        # result caching.
        if not ok and last_error == _ERROR_WINHTTP_LOGIN_FAILURE:
            proxy, _bypass, ok, last_error = _query_proxy_for_url(
                self._hsession, url, auto_detect, auto_config_url, auto_logon=True
            )
        if ok:
            if not proxy:
                return [None]
            resolved = _resolve_for_scheme(url, proxy)
            return [resolved] if resolved else [None]
        # WinHttpGetProxyForUrl failed (e.g. ERROR_WINHTTP_AUTODETECTION_FAILED)
        # -- fall through to the static manual-proxy fallback below.

    manual = _manual_proxy_map(ie) if ie else None
    if manual is not None:
        return manual[url]
    return [None]

close()

Source code in src/proxylib/os/nt.py
250
251
252
253
def close(self) -> None:
    if self._hsession:
        _winhttp.WinHttpCloseHandle(self._hsession)
        self._hsession = None

system_proxy()

Read proxy settings the way Windows/IE/Edge do.

Mirrors Windows' own precedence: if "Automatically detect settings" is on, WPAD is tried first; then the configured PAC script (if any); then a manual proxy (if any); otherwise DIRECT.

Source code in src/proxylib/os/nt.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def system_proxy() -> "ProxyMap|str":
    """Read proxy settings the way Windows/IE/Edge do.

    Mirrors Windows' own precedence: if "Automatically detect settings" is
    on, WPAD is tried first; then the configured PAC script (if any); then a
    manual proxy (if any); otherwise DIRECT.
    """
    settings = _read_ie_proxy_config()
    if settings is None:
        return SimpleProxyMap()

    if settings.auto_detect:
        discovered = _wpad_discover()
        if discovered is not None:
            return discovered

    if settings.auto_config_url:
        return settings.auto_config_url

    return _manual_proxy_map(settings) or SimpleProxyMap()

proxylib.os.darwin

macOS system proxy detection via scutil --proxy (no extra dependency), plus :class:CFNetworkProxyMap, a ctypes binding to the native CFNetwork proxy-resolution API.

__all__ = ('system_proxy', 'CFNetworkProxyMap') module-attribute

CFNetworkProxyMap(deadline=5.0)

Bases: ProxyMap

Resolves each request URL via macOS's native CFNetwork proxy engine.

Unlike :func:system_proxy (which reads static settings once via scutil --proxy and relies on EnvProxyConfig for NO_PROXY-style matching), CFNetworkCopyProxiesForURL applies the system's real per-URL bypass-exception matching itself. When the system is configured with a PAC URL, it's executed natively (CFNetworkExecuteProxyAutoConfigurationURL) -- no dukpy needed -- pumped against a run loop in small slices up to deadline seconds (default 5.0) so a dead PAC server or hung DNS lookup can't block forever; on timeout, raises KeyError (a resolution failure per the ProxyMap contract -- same as LibProxyMap -- so a fallback/chain can proceed) rather than hanging or silently returning DIRECT.

Validated by a real (unmocked) smoke test on macOS CI, but only for the common "direct/manual proxy" path -- the PAC-execution/run-loop-deadline branch is not yet exercised by that CI job, so treat it more cautiously. See root AGENTS.md for details.

Source code in src/proxylib/os/darwin.py
414
415
def __init__(self, deadline: float = 5.0) -> None:
    self.deadline = deadline

__slots__ = ('deadline',) class-attribute instance-attribute

deadline = deadline instance-attribute

__getitem__(url)

Source code in src/proxylib/os/darwin.py
417
418
419
420
421
def __getitem__(self, url: str) -> "Iterable[Optional[Proxy]]":
    entries = _resolve_proxies_for_url(url, self.deadline)
    if entries is None:
        raise KeyError(url)
    return [Proxy.from_str(entry) if entry else None for entry in entries]

system_proxy()

Read proxy settings from macOS's System Configuration via scutil --proxy.

Mirrors macOS's own Network preferences precedence: if "Auto Proxy Discovery" (WPAD) is on, it's tried first; then Automatic Proxy Configuration (a PAC URL) if enabled; then a manual HTTP/HTTPS proxy; otherwise DIRECT.

Source code in src/proxylib/os/darwin.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def system_proxy() -> "ProxyMap|str":
    """Read proxy settings from macOS's System Configuration via ``scutil --proxy``.

    Mirrors macOS's own Network preferences precedence: if "Auto Proxy
    Discovery" (WPAD) is on, it's tried first; then Automatic Proxy
    Configuration (a PAC URL) if enabled; then a manual HTTP/HTTPS proxy;
    otherwise DIRECT.
    """
    settings = _read_scutil_proxy()

    if settings.get("ProxyAutoDiscoveryEnable") == "1":
        discovered = _wpad_discover()
        if discovered is not None:
            return discovered

    if settings.get("ProxyAutoConfigEnable") == "1":
        pac_url = settings.get("ProxyAutoConfigURLString")
        if pac_url:
            return pac_url

    http_enabled = settings.get("HTTPEnable") == "1"
    https_enabled = settings.get("HTTPSEnable") == "1"
    http_proxy = (
        f"http://{settings['HTTPProxy']}:{settings.get('HTTPPort', 80)}"
        if http_enabled and settings.get("HTTPProxy")
        else None
    )
    https_proxy = (
        f"http://{settings['HTTPSProxy']}:{settings.get('HTTPSPort', 443)}"
        if https_enabled and settings.get("HTTPSProxy")
        else None
    )
    if http_proxy or https_proxy:
        overrides = settings.get("ExceptionsList", [])
        return EnvProxyConfig(
            http_proxy or https_proxy, https_proxy or http_proxy, overrides
        )

    return SimpleProxyMap()

proxylib.os.posix

Linux/other POSIX system proxy detection.

There is no single authoritative proxy config store on Linux; the portable convention is environment variables. As a best-effort improvement, :func:system_proxy also tries (in order): libproxy (:mod:.libproxy, if its proxy CLI is installed -- generally the most authoritative single source since it's itself a full cross-desktop resolution engine), GNOME (:mod:.gnome), MATE (:mod:.mate), KDE (:mod:.kde), and NetworkManager (:mod:.networkmanager) -- each only if its respective binary/file is present, each degrading to "not configured" (not an error) otherwise. If more than one of these has stale/leftover config on the same machine (e.g. switched desktop environments), the first match in that order wins -- there is no desktop-session detection (XDG_CURRENT_DESKTOP) to disambiguate. Other desktop environments (Xfce, Cinnamon, ...) aren't covered directly (though libproxy, if installed, may cover them) — set HTTP_PROXY/ HTTPS_PROXY/NO_PROXY or PROXY_PAC explicitly on those.

__all__ = ('system_proxy',) module-attribute

system_proxy()

PROXY_PAC/HTTP_PROXY-family env vars, with best-effort desktop checks.

Source code in src/proxylib/os/posix/__init__.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def system_proxy() -> "ProxyMap|str":
    """``PROXY_PAC``/``HTTP_PROXY``-family env vars, with best-effort desktop checks."""
    pac = os.environ.get("PROXY_PAC")
    if pac:
        return pac

    for backend in _DESKTOP_BACKENDS:
        result = backend.detect()
        if result is not None:
            return result

    has_env_proxy = any(
        os.environ.get(name)
        for name in ("HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy")
    )
    if has_env_proxy:
        return EnvProxyConfig.from_env()

    return SimpleProxyMap()

proxylib.os.posix.gnome

GNOME desktop proxy detection via gsettings (org.gnome.system.proxy).

__all__ = ('detect',) module-attribute

detect()

Source code in src/proxylib/os/posix/gnome.py
11
12
def detect() -> "ProxyMap|str|None":
    return read_desktop_proxy("org.gnome.system.proxy")

proxylib.os.posix.mate

MATE desktop proxy detection via gsettings (org.mate.system.proxy).

MATE's schema is a near-verbatim fork of GNOME's (same key names), inherited from their shared GNOME 2 ancestry.

__all__ = ('detect',) module-attribute

detect()

Source code in src/proxylib/os/posix/mate.py
15
16
def detect() -> "ProxyMap|str|None":
    return read_desktop_proxy("org.mate.system.proxy")

proxylib.os.posix.kde

KDE desktop proxy detection via kioslaverc.

__all__ = ('detect',) module-attribute

detect()

Source code in src/proxylib/os/posix/kde.py
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def detect() -> "ProxyMap|str|None":
    section = _read_proxy_settings()
    if section is None:
        return None
    proxy_type = section.get("proxytype", _KDE_PROXY_NONE)

    if proxy_type == _KDE_PROXY_WPAD:
        return _wpad_discover()
    if proxy_type == _KDE_PROXY_PAC_SCRIPT:
        pac_url = section.get("proxy config script", "")
        return pac_url or None
    if proxy_type != _KDE_PROXY_MANUAL:
        # none (0), environment variables (4, handled by system_proxy()'s
        # own env var fallback), or an unrecognized value.
        return None

    http_proxy = _host_port(section, "httpproxy")
    https_proxy = _host_port(section, "httpsproxy") or http_proxy
    if not (http_proxy or https_proxy):
        return None
    overrides = [h.strip() for h in section.get("noproxyfor", "").split(",") if h.strip()]
    return EnvProxyConfig(http_proxy or https_proxy, https_proxy, overrides)

proxylib.os.posix.networkmanager

NetworkManager proxy detection.

NetworkManager's own per-connection proxy setting (see the settings-proxy docs <https://networkmanager.dev/docs/api/latest/settings-proxy.html>_, exposed over D-Bus on org.freedesktop.NetworkManager.Settings.Connection) only supports method = "none" or "auto" -- there's no manual host:port option at this layer, unlike the desktop-level backends.

Read via nmcli (NetworkManager's own CLI) when it's on PATH; if it isn't (a minimal install with just the daemon + D-Bus, no CLI tools), fall back to talking to the same D-Bus interface directly via dbus-send. Neither needs a non-stdlib dependency (e.g. dbus-python/pydbus).

__all__ = ('detect',) module-attribute

detect()

Source code in src/proxylib/os/posix/networkmanager.py
176
177
178
def detect() -> "ProxyMap|str|None":
    settings = _proxy_settings_via_nmcli() or _proxy_settings_via_dbus_send()
    return _resolve(settings) if settings else None

proxylib.os.posix.libproxy

libproxy integration, via ctypes bindings to its C API directly (no subprocess, no non-stdlib dependency).

libproxy <https://libproxy.github.io/libproxy/>_ is itself a cross-desktop proxy-resolution engine -- it already knows how to read GNOME/KDE/env vars/ PAC/WPAD/etc, generally with more fidelity than proxylib's own best-effort per-desktop probes (:mod:.gnome, :mod:.kde, ...). When its shared library is loadable, :mod:..posix prefers it over those probes.

Unlike the other backends, this isn't a "read the config once at startup" detector -- resolution is per-URL, so :class:LibProxyMap is its own :class:~proxylib.proxy.ProxyMap implementation that calls into libproxy fresh for every lookup: a new pxProxyFactory per call (matching libproxy's own API contract -- see px_proxy_factory_free -- and sidestepping any question of whether a shared factory is safe to reuse across threads).

__all__ = ('LibProxyMap', 'detect') module-attribute

LibProxyMap(lib=None)

Bases: ProxyMap

Resolves each request URL via libproxy's C API.

Construct with no arguments to use the auto-detected shared library; __getitem__ raises KeyError (like any other resolution failure) if libproxy isn't available, rather than erroring at construction time.

Source code in src/proxylib/os/posix/libproxy.py
74
75
def __init__(self, lib: "Optional[ctypes.CDLL]" = None) -> None:
    self._lib = lib if lib is not None else _get_libproxy()

__slots__ = ('_lib',) class-attribute instance-attribute

__getitem__(uri)

Source code in src/proxylib/os/posix/libproxy.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def __getitem__(self, uri: str) -> "Iterable[Optional[Proxy]]":
    lib = self._lib
    if lib is None:
        raise KeyError(uri)

    factory = lib.px_proxy_factory_new()
    if not factory:
        raise KeyError(uri)
    try:
        proxies_array = lib.px_proxy_factory_get_proxies(factory, uri.encode("utf-8"))
        if not proxies_array:
            return [None]
        try:
            results: "list[Optional[Proxy]]" = []
            i = 0
            while proxies_array[i] is not None:
                entry = proxies_array[i].decode("utf-8")
                results.append(
                    None if entry.startswith("direct://") else Proxy.from_str(entry)
                )
                i += 1
            return results or [None]
        finally:
            lib.px_proxy_factory_free_proxies(proxies_array)
    finally:
        lib.px_proxy_factory_free(factory)

detect()

Source code in src/proxylib/os/posix/libproxy.py
105
106
107
def detect() -> "Optional[LibProxyMap]":
    lib = _get_libproxy()
    return LibProxyMap(lib) if lib is not None else None