# proxy-watch
[](https://crates.io/crates/proxy-watch)
[](https://docs.rs/proxy-watch)
[](https://github.com/gnoays/proxy-watch/actions/workflows/ci.yml)
[](https://github.com/gnoays/proxy-watch/actions/workflows/audit.yml)
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.
```rust,no_run
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.
## Which part you need
- **The settings once**, for a program that starts, reads and exits: `read()`.
- **The settings as they change**, for a long-running program: `ProxyWatcher`, a `Stream`
that 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`'s `Proxy::custom` fed by a watcher.
- **Only `http_proxy` / `no_proxy`**: `ProxyEnv::from_env()`, on every platform.
Each has a section under **Usage**.
## Install
```sh
cargo add proxy-watch
```
GNOME 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.
## Other languages
The same library, built from this repository; each package's README has its usage.
| Node.js | [`proxy-watch`](bindings/node/README.md) on npm | [](https://www.npmjs.com/package/proxy-watch) |
| Python | [`proxy-watch`](bindings/python/README.md) on PyPI | [](https://pypi.org/project/proxy-watch/) |
| C | [header and libraries](bindings/c/README.md) on the GitHub Releases page (`bindings-v*` tags) | |
## Usage
[`examples/`](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](https://docs.rs/proxy-watch).
### Read it once
```rust,no_run
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.
### Watch it
`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.
```rust,no_run
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.
### Decide how to reach a URL
`resolve()` turns a snapshot into the ordered `ProxyStep`s to try, bypass rules applied:
```rust,no_run
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.
### With `reqwest`
`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`](examples/reqwest_client.rs).
## Pitfalls
- **`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](https://docs.rs/proxy-watch).
## Feature flags
| `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 support
| 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](https://github.com/gnoays/proxy-watch/issues/new?template=unverified-surface.yml)
asks for what the host is set to and what the crate answered; a report needs both.
## Minimum supported Rust version
**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.
## Design
- **Async-runtime agnostic:** `futures_core::Stream` on dedicated OS threads (`tokio` optional).
- **`unsafe` is 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-subprocess` puts on its worker
- the table through which `linux-gnome` calls GLib
- **Credentials stay masked** in `Debug`, and `ProxyAuth` has no `Display`. Masking covers
a URL's `user:password@`; a query, a PAC URL's `?key=` included, prints as it is.
## Contributing
[CONTRIBUTING.md](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.
## License
Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or
[MIT license](LICENSE-MIT) at your option.