Read the operating system's proxy configuration on Windows, macOS, Linux, Android and
iOS, and get a Stream item every time it changes. On a host set to PAC, a pac-*
feature gives a URL's route from the OS's own PAC engine or a bundled one.
use proxy_watch::{read, resolve, Url};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = read()?;
println!("proxy: {:?}", config.effective);
let steps = resolve(&config, &Url::parse("https://example.com/")?)?;
println!("route: {steps:?}");
Ok(())
}A one-shot read at process start goes stale: code that read once and cached the answer
never sees a PAC URL pushed by policy, or a proxy switched out from under a long-running
program. This crate delivers that change as a Stream item.
- The settings once, for a program that starts, reads and exits:
read(). - The settings as they change, for a long-running program:
ProxyWatcher, aStreamthat opens with a snapshot. - The route for one URL, bypass rules applied:
resolve(), and a PAC engine from the feature table below when the host uses PAC or WPAD. - An HTTP client that follows the OS:
reqwest'sProxy::customfed by a watcher. - Only
http_proxy/no_proxy:ProxyEnv::from_env(), on every platform.
Each has a section under Usage.
cargo add proxy-watchGNOME support, on by default, opens GLib at run time: a build needs no C library or headers, and a machine without GLib reads the other stores.
The same library, built from this repository; each package's README has its usage.
| Language | Package | |
|---|---|---|
| Node.js | proxy-watch on npm |
|
| Python | proxy-watch on PyPI |
|
| C | header and libraries on the GitHub Releases page (bindings-v* tags) |
examples/ has a runnable file per section (current, watch, resolve,
reqwest_client), plus env, resolve_os_and_env and pac
(cargo run --example <name>; pac needs --features pac-quickjs). The API reference, every feature's limits and each
platform's details are on docs.rs.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = proxy_watch::read()?;
println!("effective: {:?}", config.effective);
Ok(())
}read() reports the OS settings only; layer http_proxy / no_proxy on top with
.with_env(&ProxyEnv::from_env()?, EnvPrecedence::BeforeSystem), or AfterSystem. On a
Linux host with no desktop store (most servers, containers and CI hosts), read() answers
Error::Unsupported: use ProxyEnv alone there.
ProxyWatcher runs on OS threads of its own, so it needs no async runtime. This example
drives it with futures-executor; add futures-util and futures-executor yourself, or
.await next() from the runtime you have.
use futures_util::StreamExt;
use proxy_watch::{ProxyWatcher, WatchEvent};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut watcher = ProxyWatcher::new()?;
while let Some(event) = futures_executor::block_on(watcher.next()) {
match event {
WatchEvent::Snapshot { state, .. } => {
let live = state.health.is_fully_live();
println!("-> {:?} (live: {live})", state.config.effective);
}
WatchEvent::Error { error, .. } => eprintln!("-> error: {error}"),
_ => {}
}
}
Ok(())
}The first item is the current configuration; each later one is a change, coalesced over
200 ms. With the tokio feature, watch_channel() publishes to a
tokio::sync::watch::Receiver instead.
resolve() turns a snapshot into the ordered ProxySteps to try, bypass rules applied:
use proxy_watch::{read, resolve, Url};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = read()?;
for step in resolve(&config, &Url::parse("https://example.com/")?)? {
match step.endpoint() {
Some(endpoint) => println!("via {endpoint}"),
None => println!("DIRECT"),
}
}
Ok(())
}On a machine configured with a PAC script or WPAD it fails with
Error::PacNotSupported instead of answering direct. pac::PacResolver answers there,
with the OS's engine (pac-native) or a bundled one (pac-quickjs, or pac-subprocess
to keep the script out of your process); examples/pac.rs shows one.
reqwest's Proxy::custom asks on every new connection, so a configuration kept fresh by
a watcher makes one Client follow the OS:
examples/reqwest_client.rs.
read()can block for seconds on macOS and inside a Linux sandbox. Keep it off a UI thread.- Do not treat a
resolve()error as "go direct": on a PAC or WPAD machine that routes traffic around the administrator's proxy. - A sandboxed watcher never fires without
WatchOptions::poll_interval.
The rest, with each platform's limits, is on docs.rs.
| Feature | Default | What it adds |
|---|---|---|
resolve |
on | resolve(): the proxy, or direct, for one URL |
linux-gnome |
on | GNOME settings; opens GLib at run time |
linux-kde |
on | KDE settings (kioslaverc) |
tokio |
off | watch_channel(), a tokio::sync::watch::Receiver |
tracing |
off | Logs naming which source changed; written to keep credentials out of them |
pac-native |
off | PAC through the OS's own resolver on Windows, macOS, iOS and Android |
pac-quickjs |
off | A bundled QuickJS engine for a PAC body you supply; needs a C compiler |
pac-subprocess |
off | The same engine in a worker process of its own; the worker is built with pac-quickjs too |
pac-windows-native |
off | The Windows part of pac-native (WinHTTP) |
pac-macos-native |
off | The macOS part of pac-native (CFNetwork) |
pac-ios-native |
off | The iOS part of pac-native (CFNetwork) |
pac-android-native |
off | The Android part of pac-native (ProxySelector) |
pac |
off | PAC parsing and policy with no engine; every pac-* feature turns it on |
| Platform | Status |
|---|---|
| Windows | Tested in CI |
| macOS | Tested in CI without a GUI. GUI changes, network-location switches and MDM GlobalHTTPProxy are unverified on real hardware |
| Linux (GNOME, KDE) | Tested in CI against dconf and a kioslaverc file, not a running desktop; behind linux-gnome and linux-kde |
| Linux (Flatpak/Snap) | Read through the desktop portal (needs linux-gnome); watching needs WatchOptions::poll_interval. Unverified |
| Android | API 23+; watching needs API 26+, or else WatchOptions::poll_interval. android-activity and Tauri 2.12+ hand the crate the app's JavaVM and Context; any other host calls proxy_watch::android::init before the first read, which otherwise fails, or aborts under panic = "abort". Partly verified on an emulator and a phone (Android 17) |
| iOS | Watching needs WatchOptions::poll_interval: iOS gives an app no proxy change notification. Partly verified on a simulator |
| Environment variables | ProxyEnv::from_env() on every platform; a snapshot, never a Stream |
Everywhere else the constructors compile and return Error::Unsupported at runtime.
Where the table says unverified, expect this failure mode: no proxy reported while the host has one, and no error. If you see it, the form asks for what the host is set to and what the crate answered; a report needs both.
1.88, with every feature. CI checks it on Linux x86-64
(cargo check --all-targets --all-features); the other targets' dependencies are not
checked against it.
- Async-runtime agnostic:
futures_core::Streamon dedicated OS threads (tokiooptional). unsafeis confined to the code that calls the OS directly. The OS-independent core has none. It is in:- the Windows and macOS backends, the iOS backend and the Core Foundation conversion it shares with macOS
- the Android backend's JNI entry
- the WinHTTP and CFNetwork PAC engines
- the limits
pac-subprocessputs on its worker - the table through which
linux-gnomecalls GLib
- Credentials stay masked in
Debug, andProxyAuthhas noDisplay. Masking covers a URL'suser:password@; a query, a PAC URL's?key=included, prints as it is.
CONTRIBUTING.md has the checks CI applies and how to run them, what
#[ignore] means here (it marks the tests that rewrite your machine's real proxy settings)
and what the prose gates in xtask/ require of a comment.
Licensed under either of Apache License, Version 2.0 or MIT license at your option.