Skip to content

About

Watch the OS proxy configuration and get every change as a Stream (Windows / macOS / Linux)

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

proxy-watch

crates.io docs.rs CI Audit

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.

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

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.

Language Package
Node.js proxy-watch on npm npm
Python proxy-watch on PyPI PyPI
C header and libraries on the GitHub Releases page (bindings-v* tags)

Usage

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.

Read it once

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.

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 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.

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.

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.

Feature flags

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 support

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.

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 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 or MIT license at your option.

About

Watch the OS proxy configuration and get every change as a Stream (Windows / macOS / Linux)

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages