Expand description
Detect and watch the operating system’s proxy settings on Windows, macOS, Linux, Android and iOS.
read answers once, starting no watcher and arming no notification route:
use proxy_watch::{Scheme, read};
let config = read()?;
match config.effective.endpoint_for(Scheme::Https) {
Some(endpoint) => println!("https goes through {endpoint}"),
None => println!("https is direct"),
}ProxyWatcher follows the settings instead: a Stream of WatchEvent that
opens with a snapshot, then publishes changes coalesced over
WatchOptions::debounce (200 ms). It runs its own OS threads, so it needs no async
runtime. Where there is no store to read (any other platform, or a Linux host with no
desktop), the answer is Error::Unsupported. Treat this as falling back to
ProxyEnv::from_env().
resolve() (behind the resolve feature, so unlinked here) turns a snapshot and a URL
into the ordered endpoints to try, bypass rules applied; a host on PAC or WPAD stops it
with Error::PacNotSupported until pac and an engine (pac-quickjs,
pac-windows-native on Windows, pac-macos-native on macOS, pac-ios-native on iOS,
or pac-android-native on Android, all four at once as pac-native) are on;
pac-subprocess runs the QuickJS engine in a worker process instead. The remaining flags are linux-gnome and
linux-kde (the Linux stores, on by default), tokio for a
tokio::sync::watch::Receiver, and tracing for lifecycle logs.
The README carries a runnable example for each of the above. The sections below are the reference it leaves out: every feature with its limits, what each platform reads and what writes it, and each backend’s limits.
§Platform coverage
Windows, GNOME and KDE are exercised on real machines. Under Flatpak or snap without
dconf, and under a KDE Flatpak with it, the Linux backend falls back to the desktop
portal (which has no change notification of its own: set
WatchOptions::poll_interval to poll) or reports Error::Sandboxed.
Unverified: the macOS backend and that sandbox path have run only in CI, the iOS
backend only on a simulator, and the Android backend on an emulator and one phone; no
device has run the iOS backend. The table below says what each run covered.
Risk: macOS reads a missing or unexpected answer as “nothing is configured”
instead of reporting an error, so a host that does have a proxy is reported as having
none. iOS reads the same dictionary the same way. The sandbox path fails loudly, so
the open question is whether the detection fires at all, not what it does once it has
fired. On Android below API 26 the watcher runs on poll_interval alone, and that
timer has not re-read on any device.
Symptom: on a machine whose settings show a proxy, the stream yields
ProxyMode::Direct and never publishes an error; on an Android device below API 26
the stream keeps its first snapshot after the settings change. src/sys/mac/mod.rs,
src/sys/linux/sandbox.rs and src/sys/linux/portal.rs carry the per-backend form.
| Platform | Status |
|---|---|
| Windows | Implemented, tested in CI (RegNotifyChangeKeyValue over Internet Settings, read through WinHttpGetIEProxyConfigForCurrentUser) |
| macOS | Implemented, tested in CI headless (SCDynamicStore plus a CFRunLoop). GUI changes, network-location switching and MDM GlobalHTTPProxy are unverified on real hardware |
| Linux (GNOME) | Implemented, tested in CI (GSettings changed signal), behind linux-gnome |
| Linux (KDE) | Implemented, tested in CI (kioslaverc file watch, no desktop environment needed), behind linux-kde |
| Linux (Flatpak/Snap) | Sandbox detected from /.flatpak-info, or (as GLib’s is_snap does it) from $SNAP/meta/snap.yaml declaring anything but confinement: classic; read via the ProxyResolver portal (needs linux-gnome) or Error::Sandboxed. Watch needs WatchOptions::poll_interval (portal has no change signal). Unverified |
| Android | Read through JNI, API 23+; the crate needs the app’s JavaVM and a Context: android-activity registers them with ndk-context, which the crate reads, and so does tao from 0.36 (Tauri 2.12); a host built on an earlier tao calls android::init instead. The bypass list is read with java.net.ProxySelector’s rules, so loopback goes direct only when the list is non-empty. Watch receives the PROXY_CHANGE_ACTION broadcast through a small receiver class the crate loads from memory (API 26+); below API 26, or where in-memory code loading is refused, it needs WatchOptions::poll_interval. Partly verified. read() (direct, manual, PAC), pac-android-native and the watcher ran on an API 36 x86-64 emulator: a settings change, a switch of the default network between a Wi-Fi with its own proxy and bypass list and mobile data, and unregistering on drop. The host registered through ndk-context there, through android::init from a Tauri 2.11 app, and through what tao registers in a Tauri 2.12 app built for release. On a phone with Android 17, a Tauri app read, resolved and received changes for a Wi-Fi proxy with a bypass list, a PAC URL, and a switch to mobile data. The poll fallback has not run, nor a global, VPN or APN proxy |
| iOS | Read through CFNetworkCopySystemProxySettings() and interpreted as macOS’s dictionary is. Watch needs WatchOptions::poll_interval: iOS gives an app no proxy change notification. The app becoming active and a network path change (nw_path_monitor, iOS 12+) re-read before the interval is up. Partly verified: on an iOS simulator in CI, watchers read, take their first network path update and drop; what they read is not checked against a configured proxy, and no device has run it |
| Environment variables | ProxyEnv::from_env() on every platform: a snapshot, never a Stream. Nothing outside the process can change them, and a process changing its own is signalled nowhere. Re-read to pick that up; ProxyEnv::captured_at() says when the snapshot was taken |
§Feature flags
| Feature | Default | What it adds |
|---|---|---|
resolve | on | resolve() and ProxyStep. No extra dependency |
linux-gnome | on | GNOME half of the Linux backend (GSettings, XDG portal fallback). Opens GLib at run time; nothing to install at build time |
linux-kde | on | KDE half (kioslaverc and its file watch). Pure Rust |
tokio | off | watch_channel(); pulls in tokio with only rt and sync |
tracing | off | Log lines giving the effective mode before and after each change, and the sources read. They are written to leave out credentials, PAC bodies and echoed parse inputs |
pac-native | off | Every native resolver below; each builds only on its own OS, so one feature list serves every target. pac::NativePacResolver names this target’s, and PacResolver::with_system_native() attaches it with its defaults. Implies pac |
pac-quickjs | off | An engine for pac: QuickJS through rquickjs. Stops a script at its timeout and bounds its heap and stack; compiles C, so it needs a C compiler for the target, and is left out of the docs.rs build for that reason: run cargo doc --features pac-quickjs to see QuickJsEvaluator. Implies pac |
pac-subprocess | off | pac::SubprocessEvaluator: each evaluation runs in a proxy-watch-pac-worker process at a path you give, killed at the timeout, with name lookups answered by your process under its PacPolicy. Needs no engine itself; the worker binary is built with pac-subprocess and pac-quickjs (cargo install proxy-watch --features pac-subprocess,pac-quickjs). Windows and Linux also cap the worker’s memory. On Linux (x86-64, AArch64) the worker runs under a seccomp filter that leaves it no file, socket or process calls; elsewhere it has no sandbox and is refused unless you call allow_unsandboxed(). Implies pac |
pac-windows-native | off | Windows only. pac::WinHttpPacResolver: download and evaluation via WinHTTP, and WPAD discovery after with_wpad(true). No PacInline; PacPolicy does not apply on this path. Implies pac |
pac-macos-native | off | macOS only: pac::CfNetworkPacResolver downloads and evaluates a PAC URL through CFNetwork, and pac::CfNetworkPacEvaluator evaluates a body there as a PacEvaluator. WPAD after with_wpad(true), through the PAC URL the system settings name; PacPolicy does not apply on this path; CFNetwork drops HTTPS and SOCKS5 entries from a script’s answer, cuts an IPv6 literal proxy at its first colon (dropped here), and the script sees the target URL cut to its scheme and host. Implies pac |
pac-ios-native | off | iOS only: the same pac::CfNetworkPacResolver and pac::CfNetworkPacEvaluator as pac-macos-native, with the same limits; WPAD resolves from the CfNetworkSystemSettings scope. Not yet run on a device or simulator. Implies pac |
pac-android-native | off | Android only: pac::AndroidPacResolver asks ProxySelector.getDefault(), which the system PAC service answers, for a Pac configuration ConnectivityManager reports, and errors once the system settings name a different PAC URL. No download or WPAD of its own; PacPolicy does not apply on this path. Implies pac |
pac | off | The pac module and resolve_with_pac(). Parsing and policy only: no JavaScript engine; you supply the script body. Implies resolve |
Platform backends are not feature-selected: they are chosen by target cfg, because
a feature enabled anywhere in a dependency graph can never be turned off again.
linux-gnome and linux-kde are the exception: they pick between stores within
Linux. Turning linux-gnome off means GLib is never opened; with both off, Linux
behaves like an unsupported target, except inside a sandbox, where the portal fallback
needs linux-gnome to be compiled in at all, so the answer is Error::Sandboxed rather
than Error::Unsupported. Both are errors; a caller matching only on Unsupported
misses that one.
§Where a host’s settings live
Reproducing a report, or writing a setting to test against, means knowing which store is being read, which is not always the one the GUI writes.
| Platform | Store | Where it comes from |
|---|---|---|
| Windows | Per-user WinINet through WinHttpGetIEProxyConfigForCurrentUser, falling back to HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings (ProxyServer, ProxyOverride, AutoConfigURL) | Settings → Network & Internet → Proxy |
| Windows | The WinHTTP machine default, WinHttpGetDefaultProxyConfiguration | netsh winhttp set proxy. Reported, but never effective: the per-user store outranks it |
| Windows | HKLM\Software\Policies\Microsoft\Windows\CurrentVersion\Internet Settings | Group Policy. Also reported, also behind the per-user store |
| macOS | The SCDynamicStore proxy dictionary (HTTPProxy, ExceptionsList, ProxyAutoConfigURLString, …) | System Settings → Network → service → Details → Proxies, or sudo networksetup -setwebproxy <service> <host> <port>. Read it back with scutil --proxy |
| Linux (GNOME) | GSettings org.gnome.system.proxy and its .http / .https / .ftp / .socks children | Settings → Network → Network Proxy, or gsettings set org.gnome.system.proxy mode 'manual' |
| Linux (KDE) | [Proxy Settings] in kioslaverc, merged across the whole XDG cascade | System Settings → Network → Proxy, which writes ~/.config/kioslaverc |
| Android | ConnectivityManager.getDefaultProxy() through JNI: host and port for http and https, the exclusion list, the PAC URL | Wi-Fi → network → Proxy, a VPN’s proxy, or adb shell settings put global http_proxy <host>:<port> |
| iOS | CFNetworkCopySystemProxySettings(), the same dictionary keys as macOS | Settings → Wi-Fi → network → Configure Proxy, a VPN’s proxy, or an MDM profile |
| Any | http_proxy, https_proxy, ftp_proxy, all_proxy, no_proxy | The process environment, read by ProxyEnv::from_env(). Lowercase beats uppercase; on Windows any other casing is read after both. An http_proxy next to a non-empty REQUEST_METHOD is refused with Error::CgiHttpProxy, because a request header sets that variable |
Each of those spells its bypass list differently, and the differences decide which hosts go
direct. The parse module’s docs have the table, entry shape by entry shape.
§Pitfalls
readcan block for seconds on macOS and inside a Linux sandbox, while a system service comes up or times out. Keep it off a UI thread.- A PAC or WPAD machine makes
resolve()fail withError::PacNotSupportedrather than answering direct, so a caller that treats an error as “go direct” routes traffic around the administrator’s proxy. Handle it, or pass the snapshot to a PAC resolver with an engine enabled. - A sandboxed watcher never fires without
WatchOptions::poll_interval: the portal has no change signal, so the stream stays on its first snapshot. - A
kioslavercthat is a symlink into another directory is read but not watched: the watch is on the config directories, and an edit to the link’s target fires nothing there. SetWatchOptions::poll_intervalif a dotfile manager links it. ProxyStep::to_url()carries the password in the clear. It exists to handuser:password@to a client;endpoint()’sDisplayleaves it out, so print that one.watch_channel()(behindtokio) publishes configuration only. A failed re-read or a dead notification route leaves the last good configuration standing with nothing marking it.
§Backend limits
Each backend documents its own limits at the top of its module. Those modules are private, so the text is in the source. These are the limits to know before relying on a backend.
- Windows 8 / Windows Server 2012 is the floor, and nothing checks it. The registry watch
arms
RegNotifyChangeKeyValuewithREG_NOTIFY_THREAD_AGNOSTIC, which Microsoft documents as “only supported in Windows 8 and later”; the function itself goes back to Windows 2000, so the flag sets the floor, not the call.pac-windows-nativecallsWinHttpCreateProxyResolverandWinHttpGetProxyForUrlEx, both of which list Windows 8 and Windows Server 2012 as their minimum supported client and server. Behaviour on an older release has not been measured. - Windows reads the settings of the active connection only.
WinHttpGetIEProxyConfigForCurrentUseris documented as returning them for the current active connection (LAN, dial-up or VPN alike) and this crate never enumerates connectoids to find the others. It also reports one mode: auto-detect wins over a PAC URL, which wins over static servers. kioslavercis merged across the whole XDG cascade (/etc/xdgthrough$XDG_CONFIG_HOME), so a system value marked immutable ([$i]) is not overridden by a user file. A[$e]flag is not expanded: KDE substitutes$VAR/${VAR}from the environment of whichever session read the file. A library that only reports should not pull process environment in on a config file’s say-so. A[$e]value that still contains a$is recorded inProxyMode::rejectedand not reported as a host, bypass pattern, or PAC script. The literal text is not the value the desktop is using, and naming it as a proxy would invent an unconfigured destination. A[$e]value with no$in it expands to itself and is read as written.- A
kioslavercwatch can go silent if a watched directory is deleted. That surfaces asWatchHealth::degraded.
Re-exports§
pub use crate::watch::DEFAULT_DEBOUNCE;pub use crate::watch::ProxyWatcher;pub use crate::watch::WatchEvent;pub use crate::watch::WatchHealth;pub use crate::watch::WatchOptions;pub use crate::watch::WatchState;pub use crate::watch::read;pub use crate::watch::read_with_options;
Modules§
- pac
pac - PAC evaluation (
pacfeature, off by default): script →Vec<ProxyStep>likeresolve. - parse
- Pure parsers for OS proxy string formats.
Structs§
- Bypass
Rules - Destinations that must not go through a proxy
(
parse::no_proxy/parse::proxy_override). - Proxy
Auth - Credentials for a proxy endpoint (HTTP Basic only).
- Proxy
Config - Point-in-time system proxy snapshot.
- Proxy
Endpoint - Concrete proxy address. IPv6 brackets resolved at parse; use
authority.#[non_exhaustive]: construct vianew/parse. - Proxy
Env - Env snapshot of
*_proxy/no_proxy(not aStream;ProxyWatcherdoes not merge these in, except where an OS setting names them, which KDE’sProxyType=4does). - Proxy
Watcher - OS proxy watcher: initial snapshot, then debounced changes (equality ignores
captured_at). Own OS threads: one, and on Linux one more per live source, plus one forWatchOptions::poll_interval(elsewhere that interval shortens the single thread’s own wait instead of adding one). Drop joins them.ProxyWatcher::healthfor route liveness. Windows, macOS, Linux, Android and iOS; elsewhereError::Unsupported. Applies no*_proxyconvention of its own: only KDE’sProxyType = 4builds aProxyEnv, from the variableskioslavercnames. - Rejected
Value - One fail-soft drop with a typed reason and origin.
- Url
- Re-export of
url::UrlforProxyMode::Pac. A parsed URL record. - Watch
Health - Liveness of change-notification routes (
ProxyWatcher::health). - Watch
Options - Tuning knobs for
ProxyWatcher::with_options(#[non_exhaustive]: useWatchOptions::new/with_*). - Watch
State - One internally consistent observation of a watcher.
Enums§
- EnvPrecedence
- Where the process environment ranks against the OS settings in
ProxyConfig::with_env. - Error
- Errors from reading OS proxy configuration, and from routing a URL through what was read.
- Host
- Re-export of
url::Host(IPv6 brackets already resolved). The host name of an URL. - Host
Pattern - A single entry of a bypass list.
- Implicit
Bypass - The destinations a resolver sends direct with no list entry naming them: the implicit
bypass set, held in
BypassRules::implicit. - IpNet
- Re-export of
ipnet::IpNetforHostPattern::Cidr. An IP network address, either IPv4 or IPv6. - Password
State - Whether
ProxyAuth::passwordhas a value, and why not when it has none. - Proxy
Config Source - Where a
ProxyModecame from.ProxyConfigkeeps all of them for explanation. - Proxy
Entry - Whether a given
Schemeuses a proxy at all. - Proxy
Mode - What a single configuration source says about proxying.
- Proxy
Scheme - The wire protocol used to reach the proxy itself.
- Proxy
Step resolve - Routing decisions, behind the
resolvefeature (on by default). Routing decisions, behind theresolvefeature (on by default). Routing decisions, behind theresolvefeature (on by default). One way to reach a destination (resolve). - Rejection
Kind - Why one configuration value was ignored.
- Rejection
Source - Where a rejected value was read from.
- Scheme
- Request scheme a proxy setting applies to (
ProxyMode::Manualper_schemekey). NotProxyScheme(how to talk to the proxy). - Watch
Event - Stream item of
ProxyWatcher: a state snapshot or a failed re-read, each stamped with the watcher’sWatchStateas it stood at delivery.
Constants§
- CGI_
MARKER_ VAR - The variable whose presence with a method in it marks a CGI environment (see
Error::CgiHttpProxy). - DEFAULT_
DEBOUNCE - The default debounce window (200 ms).
- LOCAL_
TOKEN - The Windows
ProxyOverridetoken that bypasses dot-less (intranet) host names. - NO_
LOOPBACK_ TOKEN - Windows
<-loopback>: subtracts the whole implicit bypass set (BypassRules::implicit) from the entries written before it. Parsed toHostPattern::SubtractImplicit; seeBypassRules::bypass_loopback.
Traits§
- Stream
- Re-export of
futures_core::Stream. A stream of values produced asynchronously.
Functions§
- read
- Read the OS proxy configuration once, without starting a watcher.
- read_
with_ options read, with the one option a read can honour:WatchOptions::watch_group_policy. The debounce and poll-interval fields describe a watcher this call never starts, so they are ignored.- resolve
resolve - Routing decisions, behind the
resolvefeature (on by default). Routing decisions, behind theresolvefeature (on by default). Routing decisions, behind theresolvefeature (on by default). Decide how to reachurlunderconfig. - resolve_
with_ pac pacandresolve - Routing decisions that may need a PAC script, behind the
pacfeature (off by default). Likeresolve, but evaluates PAC when the mode requires it. - watch_
channel tokio - The tokio bridge, behind the
tokiofeature (off by default). Drivewatcherinto atokio::sync::watchchannel (clonewatch::Receiverfor fan-out).