Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions crates/proxy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "robius-proxy"
version.workspace = true
edition.workspace = true
authors = [
"Tyrese Luo <tyreseluo@outlook.com>",
"Project Robius Maintainers",
]
homepage.workspace = true
categories.workspace = true
license.workspace = true
readme = "README.md"
repository.workspace = true

[dependencies]
cfg-if.workspace = true
log = { workspace = true, optional = true }

[target.'cfg(target_os = "macos")'.dependencies]
objc2-app-kit = { workspace = true, features = ["NSWorkspace"] }
objc2-foundation = { workspace = true, features = ["NSString", "NSURL"] }
51 changes: 51 additions & 0 deletions crates/proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# `robius-proxy`

This crate provides easy Rust interfaces to get and apply proxy across multiple platforms, including:

- Modes: direct, manual (HTTP/HTTPS/SOCKS), PAC URL, WPAD.
- Platforms:
1. **macOS** : via `networksetup`/`scutil`; others return
2. Others(Planned): `Error::Unsupported` until added.

## Usage

```rust
use robius_proxy::{BypassList, ProxyEndpoint, ProxyManager, ProxyMode, ProxySettings, ProxyState};

let manager = ProxyManager::new()?;
let current = manager.current()?;

println!("Current mode: {:?}", current.mode);
println!("Proxy enabled: {}", current.is_enabled());

let state = ProxyState::manual(ProxySettings {
http: Some(ProxyEndpoint::new("proxy.local", 8080)),
https: None,
socks: None,
bypass: BypassList::new(vec!["localhost".into(), "127.0.0.1".into()]),
});

manager.apply(state)?;
```

## Examples

- Show current configuration: `cargo run -p robius-proxy --example show_current`
- Set manual proxies (macOS): `cargo run -p robius-proxy --example set_manual`

Examples invoke `networksetup` under the hood, so they may prompt for system
permissions depending on your macOS configuration.

## Platform support

- macOS: implemented via `networksetup` and `scutil`.
- Linux (planned): reserve hooks for NetworkManager, GNOME proxy settings, and
system-wide environment exports.
- Windows (planned): reserve hooks for WinHTTP/WinINET and per-connection
proxy configuration.
- Android (planned): reserve hooks for global HTTP proxy settings available to
device owner apps.
- iOS (planned): reserve hooks for CFNetwork proxy dictionaries.
- OpenHarmony (planned): reserve hooks for system proxy APIs when exposed.

Contributions adding new platforms or improving parsing/validation are welcome.
65 changes: 65 additions & 0 deletions crates/proxy/examples/show_current.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use robius_proxy::{ProxyManager, ProxyMode, ProxyState};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let manager = ProxyManager::new()?;
let state = manager.current()?;
let is_enabled = state.is_enabled();

println!("Proxy enabled => {}", is_enabled);

if is_enabled {
println!("========= Current proxy configuration =========");
print_state(&state);
}

Ok(())
}

fn print_state(state: &ProxyState) {
match &state.mode {
ProxyMode::Direct => println!(" mode: Direct (no proxy)"),
ProxyMode::Manual(settings) => {
println!(" mode: Manual");
if let Some(http) = &settings.http {
println!(" http: {}:{}", http.host, http.port);
}
if let Some(https) = &settings.https {
println!(" https: {}:{}", https.host, https.port);
}
if let Some(socks) = &settings.socks {
println!(" socks: {}:{}", socks.host, socks.port);
}
if settings.bypass.entries.is_empty() {
println!(" bypass: (none)");
} else {
println!(" bypass:");
for entry in &settings.bypass.entries {
println!(" - {entry}");
}
}
}
ProxyMode::AutoConfigUrl { url, bypass } => {
println!(" mode: PAC url");
println!(" url: {url}");
if bypass.entries.is_empty() {
println!(" bypass: (none)");
} else {
println!(" bypass:");
for entry in &bypass.entries {
println!(" - {entry}");
}
}
}
ProxyMode::AutoDiscovery { bypass } => {
println!(" mode: Auto-discovery (WPAD)");
if bypass.entries.is_empty() {
println!(" bypass: (none)");
} else {
println!(" bypass:");
for entry in &bypass.entries {
println!(" - {entry}");
}
}
}
}
}
44 changes: 44 additions & 0 deletions crates/proxy/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use std::{fmt, io};

/// Result alias for the proxy crate.
pub type Result<T> = std::result::Result<T, Error>;

/// Errors that can arise while interacting with system proxy settings.
#[derive(Debug)]
pub enum Error {
/// The operation is not supported on this platform.
Unsupported,
/// Input data was invalid for the target platform.
InvalidInput(&'static str),
/// A command failed to execute successfully.
CommandFailed {
command: String,
stderr: String,
},
/// Parsing system proxy data failed.
Parse(String),
/// I/O failure while invoking platform facilities.
Io(io::Error),
}

impl From<io::Error> for Error {
fn from(value: io::Error) -> Self {
Self::Io(value)
}
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unsupported => write!(f, "operation not supported on this platform"),
Self::InvalidInput(reason) => write!(f, "invalid proxy input: {reason}"),
Self::CommandFailed { command, stderr } => {
write!(f, "command `{command}` failed: {stderr}")
}
Self::Parse(reason) => write!(f, "failed to parse proxy data: {reason}"),
Self::Io(e) => write!(f, "io error: {e}"),
}
}
}

impl std::error::Error for Error {}
168 changes: 168 additions & 0 deletions crates/proxy/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
//! Cross-platform access to system proxy settings.
//!
//! The API is intentionally small and capability-oriented: you describe the
//! desired [`ProxyState`] and the crate applies it using the native facilities
//! of the underlying platform. Only macOS is implemented right now; other
//! platforms fall back to [`Error::Unsupported`].
//!
//! The abstractions are designed to be forward-compatible with additional
//! platforms such as Linux, Windows, Android, iOS, and OpenHarmony.

mod error;
mod sys;

pub use crate::error::{Error, Result};

/// Uniform representation of a proxy endpoint.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProxyEndpoint {
/// Hostname or IP of the proxy.
pub host: String,
/// TCP port on which the proxy listens.
pub port: u16,
/// Optional credentials when the proxy requires authentication.
pub credentials: Option<Credentials>,
}

impl ProxyEndpoint {
pub fn new(host: impl Into<String>, port: u16) -> Self {
Self {
host: host.into(),
port,
credentials: None,
}
}

pub fn with_credentials(
host: impl Into<String>,
port: u16,
username: impl Into<String>,
password: Option<String>,
) -> Self {
Self {
host: host.into(),
port,
credentials: Some(Credentials {
username: username.into(),
password,
}),
}
}
}

/// Credentials for proxies that require authentication.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Credentials {
pub username: String,
pub password: Option<String>,
}

/// List of hosts, domains, or CIDRs that should bypass the proxy.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct BypassList {
pub entries: Vec<String>,
}

impl BypassList {
pub fn new(entries: impl Into<Vec<String>>) -> Self {
Self {
entries: entries.into(),
}
}
}

/// Proxy configuration for manual modes.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ProxySettings {
pub http: Option<ProxyEndpoint>,
pub https: Option<ProxyEndpoint>,
pub socks: Option<ProxyEndpoint>,
pub bypass: BypassList,
}

/// Modes supported by the abstraction layer.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ProxyMode {
/// No proxy usage.
Direct,
/// User-specified endpoints.
Manual(ProxySettings),
/// Proxy auto-configuration (PAC) script URL.
AutoConfigUrl {
url: String,
bypass: BypassList,
},
/// WPAD / automatic discovery.
AutoDiscovery {
bypass: BypassList,
},
}

/// High-level proxy state exposed to callers.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ProxyState {
pub mode: ProxyMode,
}

impl ProxyState {
pub fn direct() -> Self {
Self {
mode: ProxyMode::Direct,
}
}

pub fn manual(settings: ProxySettings) -> Self {
Self {
mode: ProxyMode::Manual(settings),
}
}

pub fn pac(url: impl Into<String>, bypass: BypassList) -> Self {
Self {
mode: ProxyMode::AutoConfigUrl {
url: url.into(),
bypass,
},
}
}

pub fn auto_discovery(bypass: BypassList) -> Self {
Self {
mode: ProxyMode::AutoDiscovery { bypass },
}
}

/// Returns true when any proxy mode is enabled (manual/PAC/WPAD).
pub fn is_enabled(&self) -> bool {
!matches!(self.mode, ProxyMode::Direct)
}
}

/// Entry point for interacting with system proxy settings.
pub struct ProxyManager {
inner: sys::Manager,
}

impl ProxyManager {
/// Constructs a manager bound to the current platform.
pub fn new() -> Result<Self> {
Ok(Self {
inner: sys::Manager::new()?,
})
}

/// Reads the current proxy state from the system.
pub fn current(&self) -> Result<ProxyState> {
self.inner.current()
}

/// Applies the given proxy state to the system.
pub fn apply(&self, state: ProxyState) -> Result<()> {
self.inner.apply(state)
}

/// Convenience helper to disable all proxies.
pub fn disable(&self) -> Result<()> {
self.apply(ProxyState::direct())
}
}
9 changes: 9 additions & 0 deletions crates/proxy/src/sys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
cfg_if::cfg_if! {
if #[cfg(target_os = "macos")] {
mod macos;
pub(crate) use macos::Manager;
} else {
mod unsupported;
pub(crate) use unsupported::Manager;
}
}
Loading