mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
feat(network-proxy): add a SOCKS5 proxy with policy enforcement (#9803)
### Summary - Adds an optional SOCKS5 listener via `rama-socks5` - SOCKS5 is disabled by default and gated by config - Reuses existing policy enforcement and blocked-request recording - Blocks SOCKS5 in limited mode to prevent method-policy bypass - Applies bind clamping to the SOCKS5 listener ### Config New/used fields under `network_proxy`: - `enable_socks5` - `socks_url` - `enable_socks5_udp` ### Scope - Changes limited to `codex-rs/network-proxy` (+ `codex-rs/Cargo.lock`) ### Testing ```bash cd codex-rs just fmt cargo test -p codex-network-proxy --offline
This commit is contained in:
1 parent
538e1059a3
commit
877b76bb9d
7 files changed
+452
-19
No files matched your search
Generated
+28
@@ -1683,6 +1683,7 @@ dependencies = [
|
|||||||
"rama-http",
|
"rama-http",
|
||||||
"rama-http-backend",
|
"rama-http-backend",
|
||||||
"rama-net",
|
"rama-net",
|
||||||
|
"rama-socks5",
|
||||||
"rama-tcp",
|
"rama-tcp",
|
||||||
"rama-tls-boring",
|
"rama-tls-boring",
|
||||||
"rama-unix",
|
"rama-unix",
|
||||||
@@ -5959,6 +5960,21 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rama-socks5"
|
||||||
|
version = "0.3.0-alpha.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5468b263516daaf258de32542c1974b7cbe962363ad913dcb669f5d46db0ef3e"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"rama-core",
|
||||||
|
"rama-net",
|
||||||
|
"rama-tcp",
|
||||||
|
"rama-udp",
|
||||||
|
"rama-utils",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rama-tcp"
|
name = "rama-tcp"
|
||||||
version = "0.3.0-alpha.4"
|
version = "0.3.0-alpha.4"
|
||||||
@@ -5997,6 +6013,18 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rama-udp"
|
||||||
|
version = "0.3.0-alpha.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36ed05e0ecac73e084e92a3a8b1fbf16fdae8958c506f0f0eada180a2d99eef4"
|
||||||
|
dependencies = [
|
||||||
|
"rama-core",
|
||||||
|
"rama-net",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rama-unix"
|
name = "rama-unix"
|
||||||
version = "0.3.0-alpha.4"
|
version = "0.3.0-alpha.4"
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ rama-core = { version = "=0.3.0-alpha.4" }
|
|||||||
rama-http = { version = "=0.3.0-alpha.4" }
|
rama-http = { version = "=0.3.0-alpha.4" }
|
||||||
rama-http-backend = { version = "=0.3.0-alpha.4", features = ["tls"] }
|
rama-http-backend = { version = "=0.3.0-alpha.4", features = ["tls"] }
|
||||||
rama-net = { version = "=0.3.0-alpha.4", features = ["http", "tls"] }
|
rama-net = { version = "=0.3.0-alpha.4", features = ["http", "tls"] }
|
||||||
|
rama-socks5 = { version = "=0.3.0-alpha.4" }
|
||||||
rama-tcp = { version = "=0.3.0-alpha.4", features = ["http"] }
|
rama-tcp = { version = "=0.3.0-alpha.4", features = ["http"] }
|
||||||
rama-tls-boring = { version = "=0.3.0-alpha.4", features = ["http"] }
|
rama-tls-boring = { version = "=0.3.0-alpha.4", features = ["http"] }
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
`codex-network-proxy` is Codex's local network policy enforcement proxy. It runs:
|
`codex-network-proxy` is Codex's local network policy enforcement proxy. It runs:
|
||||||
|
|
||||||
- an HTTP proxy (default `127.0.0.1:3128`)
|
- an HTTP proxy (default `127.0.0.1:3128`)
|
||||||
|
- an optional SOCKS5 proxy (default `127.0.0.1:8081`, disabled by default)
|
||||||
- an admin HTTP API (default `127.0.0.1:8080`)
|
- an admin HTTP API (default `127.0.0.1:8080`)
|
||||||
|
|
||||||
It enforces an allow/deny policy and a "limited" mode intended for read-only network access.
|
It enforces an allow/deny policy and a "limited" mode intended for read-only network access.
|
||||||
@@ -20,6 +21,10 @@ Example config:
|
|||||||
enabled = true
|
enabled = true
|
||||||
proxy_url = "http://127.0.0.1:3128"
|
proxy_url = "http://127.0.0.1:3128"
|
||||||
admin_url = "http://127.0.0.1:8080"
|
admin_url = "http://127.0.0.1:8080"
|
||||||
|
# Optional SOCKS5 listener (disabled by default).
|
||||||
|
enable_socks5 = false
|
||||||
|
socks_url = "http://127.0.0.1:8081"
|
||||||
|
enable_socks5_udp = false
|
||||||
# When `enabled` is false, the proxy no-ops and does not bind listeners.
|
# When `enabled` is false, the proxy no-ops and does not bind listeners.
|
||||||
# When true, respect HTTP(S)_PROXY/ALL_PROXY for upstream requests (HTTP(S) proxies only),
|
# When true, respect HTTP(S)_PROXY/ALL_PROXY for upstream requests (HTTP(S) proxies only),
|
||||||
# including CONNECT tunnels in full mode.
|
# including CONNECT tunnels in full mode.
|
||||||
@@ -28,7 +33,7 @@ allow_upstream_proxy = false
|
|||||||
# If you want to expose these listeners beyond localhost, you must opt in explicitly.
|
# If you want to expose these listeners beyond localhost, you must opt in explicitly.
|
||||||
dangerously_allow_non_loopback_proxy = false
|
dangerously_allow_non_loopback_proxy = false
|
||||||
dangerously_allow_non_loopback_admin = false
|
dangerously_allow_non_loopback_admin = false
|
||||||
mode = "limited" # or "full"
|
mode = "full" # default when unset; use "limited" for read-only mode
|
||||||
|
|
||||||
[network_proxy.policy]
|
[network_proxy.policy]
|
||||||
# Hosts must match the allowlist (unless denied).
|
# Hosts must match the allowlist (unless denied).
|
||||||
@@ -60,6 +65,12 @@ export HTTP_PROXY="http://127.0.0.1:3128"
|
|||||||
export HTTPS_PROXY="http://127.0.0.1:3128"
|
export HTTPS_PROXY="http://127.0.0.1:3128"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For SOCKS5 traffic (when `enable_socks5 = true`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ALL_PROXY="socks5h://127.0.0.1:8081"
|
||||||
|
```
|
||||||
|
|
||||||
### 4) Understand blocks / debugging
|
### 4) Understand blocks / debugging
|
||||||
|
|
||||||
When a request is blocked, the proxy responds with `403` and includes:
|
When a request is blocked, the proxy responds with `403` and includes:
|
||||||
@@ -70,8 +81,8 @@ When a request is blocked, the proxy responds with `403` and includes:
|
|||||||
- `blocked-by-method-policy`
|
- `blocked-by-method-policy`
|
||||||
- `blocked-by-policy`
|
- `blocked-by-policy`
|
||||||
|
|
||||||
In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed for plain HTTP. HTTPS `CONNECT`
|
In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed. HTTPS `CONNECT` and SOCKS5 are
|
||||||
remains a transparent tunnel, so limited-mode method enforcement does not apply to HTTPS.
|
blocked because they would bypass method enforcement.
|
||||||
|
|
||||||
## Library API
|
## Library API
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ pub struct NetworkProxySettings {
|
|||||||
#[serde(default = "default_admin_url")]
|
#[serde(default = "default_admin_url")]
|
||||||
pub admin_url: String,
|
pub admin_url: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub enable_socks5: bool,
|
||||||
|
#[serde(default = "default_socks_url")]
|
||||||
|
pub socks_url: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub enable_socks5_udp: bool,
|
||||||
|
#[serde(default)]
|
||||||
pub allow_upstream_proxy: bool,
|
pub allow_upstream_proxy: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dangerously_allow_non_loopback_proxy: bool,
|
pub dangerously_allow_non_loopback_proxy: bool,
|
||||||
@@ -40,6 +46,9 @@ impl Default for NetworkProxySettings {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
proxy_url: default_proxy_url(),
|
proxy_url: default_proxy_url(),
|
||||||
admin_url: default_admin_url(),
|
admin_url: default_admin_url(),
|
||||||
|
enable_socks5: false,
|
||||||
|
socks_url: default_socks_url(),
|
||||||
|
enable_socks5_udp: false,
|
||||||
allow_upstream_proxy: false,
|
allow_upstream_proxy: false,
|
||||||
dangerously_allow_non_loopback_proxy: false,
|
dangerously_allow_non_loopback_proxy: false,
|
||||||
dangerously_allow_non_loopback_admin: false,
|
dangerously_allow_non_loopback_admin: false,
|
||||||
@@ -90,6 +99,10 @@ fn default_admin_url() -> String {
|
|||||||
"http://127.0.0.1:8080".to_string()
|
"http://127.0.0.1:8080".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_socks_url() -> String {
|
||||||
|
"http://127.0.0.1:8081".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
/// Clamp non-loopback bind addresses to loopback unless explicitly allowed.
|
/// Clamp non-loopback bind addresses to loopback unless explicitly allowed.
|
||||||
fn clamp_non_loopback(addr: SocketAddr, allow_non_loopback: bool, name: &str) -> SocketAddr {
|
fn clamp_non_loopback(addr: SocketAddr, allow_non_loopback: bool, name: &str) -> SocketAddr {
|
||||||
if addr.ip().is_loopback() {
|
if addr.ip().is_loopback() {
|
||||||
@@ -110,21 +123,27 @@ fn clamp_non_loopback(addr: SocketAddr, allow_non_loopback: bool, name: &str) ->
|
|||||||
|
|
||||||
pub(crate) fn clamp_bind_addrs(
|
pub(crate) fn clamp_bind_addrs(
|
||||||
http_addr: SocketAddr,
|
http_addr: SocketAddr,
|
||||||
|
socks_addr: SocketAddr,
|
||||||
admin_addr: SocketAddr,
|
admin_addr: SocketAddr,
|
||||||
cfg: &NetworkProxySettings,
|
cfg: &NetworkProxySettings,
|
||||||
) -> (SocketAddr, SocketAddr) {
|
) -> (SocketAddr, SocketAddr, SocketAddr) {
|
||||||
let http_addr = clamp_non_loopback(
|
let http_addr = clamp_non_loopback(
|
||||||
http_addr,
|
http_addr,
|
||||||
cfg.dangerously_allow_non_loopback_proxy,
|
cfg.dangerously_allow_non_loopback_proxy,
|
||||||
"HTTP proxy",
|
"HTTP proxy",
|
||||||
);
|
);
|
||||||
|
let socks_addr = clamp_non_loopback(
|
||||||
|
socks_addr,
|
||||||
|
cfg.dangerously_allow_non_loopback_proxy,
|
||||||
|
"SOCKS5 proxy",
|
||||||
|
);
|
||||||
let admin_addr = clamp_non_loopback(
|
let admin_addr = clamp_non_loopback(
|
||||||
admin_addr,
|
admin_addr,
|
||||||
cfg.dangerously_allow_non_loopback_admin,
|
cfg.dangerously_allow_non_loopback_admin,
|
||||||
"admin API",
|
"admin API",
|
||||||
);
|
);
|
||||||
if cfg.policy.allow_unix_sockets.is_empty() {
|
if cfg.policy.allow_unix_sockets.is_empty() {
|
||||||
return (http_addr, admin_addr);
|
return (http_addr, socks_addr, admin_addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
// `x-unix-socket` is intentionally a local escape hatch. If the proxy (or admin API) is
|
// `x-unix-socket` is intentionally a local escape hatch. If the proxy (or admin API) is
|
||||||
@@ -136,6 +155,11 @@ pub(crate) fn clamp_bind_addrs(
|
|||||||
"unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_proxy and clamping HTTP proxy to loopback"
|
"unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_proxy and clamping HTTP proxy to loopback"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if cfg.dangerously_allow_non_loopback_proxy && !socks_addr.ip().is_loopback() {
|
||||||
|
warn!(
|
||||||
|
"unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_proxy and clamping SOCKS5 proxy to loopback"
|
||||||
|
);
|
||||||
|
}
|
||||||
if cfg.dangerously_allow_non_loopback_admin && !admin_addr.ip().is_loopback() {
|
if cfg.dangerously_allow_non_loopback_admin && !admin_addr.ip().is_loopback() {
|
||||||
warn!(
|
warn!(
|
||||||
"unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_admin and clamping admin API to loopback"
|
"unix socket proxying is enabled; ignoring dangerously_allow_non_loopback_admin and clamping admin API to loopback"
|
||||||
@@ -143,12 +167,14 @@ pub(crate) fn clamp_bind_addrs(
|
|||||||
}
|
}
|
||||||
(
|
(
|
||||||
SocketAddr::from(([127, 0, 0, 1], http_addr.port())),
|
SocketAddr::from(([127, 0, 0, 1], http_addr.port())),
|
||||||
|
SocketAddr::from(([127, 0, 0, 1], socks_addr.port())),
|
||||||
SocketAddr::from(([127, 0, 0, 1], admin_addr.port())),
|
SocketAddr::from(([127, 0, 0, 1], admin_addr.port())),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RuntimeConfig {
|
pub struct RuntimeConfig {
|
||||||
pub http_addr: SocketAddr,
|
pub http_addr: SocketAddr,
|
||||||
|
pub socks_addr: SocketAddr,
|
||||||
pub admin_addr: SocketAddr,
|
pub admin_addr: SocketAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,16 +185,24 @@ pub fn resolve_runtime(cfg: &NetworkProxyConfig) -> Result<RuntimeConfig> {
|
|||||||
cfg.network_proxy.proxy_url
|
cfg.network_proxy.proxy_url
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
let socks_addr = resolve_addr(&cfg.network_proxy.socks_url, 8081).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"invalid network_proxy.socks_url: {}",
|
||||||
|
cfg.network_proxy.socks_url
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let admin_addr = resolve_addr(&cfg.network_proxy.admin_url, 8080).with_context(|| {
|
let admin_addr = resolve_addr(&cfg.network_proxy.admin_url, 8080).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"invalid network_proxy.admin_url: {}",
|
"invalid network_proxy.admin_url: {}",
|
||||||
cfg.network_proxy.admin_url
|
cfg.network_proxy.admin_url
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let (http_addr, admin_addr) = clamp_bind_addrs(http_addr, admin_addr, &cfg.network_proxy);
|
let (http_addr, socks_addr, admin_addr) =
|
||||||
|
clamp_bind_addrs(http_addr, socks_addr, admin_addr, &cfg.network_proxy);
|
||||||
|
|
||||||
Ok(RuntimeConfig {
|
Ok(RuntimeConfig {
|
||||||
http_addr,
|
http_addr,
|
||||||
|
socks_addr,
|
||||||
admin_addr,
|
admin_addr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -403,11 +437,14 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let http_addr = "0.0.0.0:3128".parse::<SocketAddr>().unwrap();
|
let http_addr = "0.0.0.0:3128".parse::<SocketAddr>().unwrap();
|
||||||
|
let socks_addr = "0.0.0.0:8081".parse::<SocketAddr>().unwrap();
|
||||||
let admin_addr = "0.0.0.0:8080".parse::<SocketAddr>().unwrap();
|
let admin_addr = "0.0.0.0:8080".parse::<SocketAddr>().unwrap();
|
||||||
|
|
||||||
let (http_addr, admin_addr) = clamp_bind_addrs(http_addr, admin_addr, &cfg);
|
let (http_addr, socks_addr, admin_addr) =
|
||||||
|
clamp_bind_addrs(http_addr, socks_addr, admin_addr, &cfg);
|
||||||
|
|
||||||
assert_eq!(http_addr, "0.0.0.0:3128".parse::<SocketAddr>().unwrap());
|
assert_eq!(http_addr, "0.0.0.0:3128".parse::<SocketAddr>().unwrap());
|
||||||
|
assert_eq!(socks_addr, "0.0.0.0:8081".parse::<SocketAddr>().unwrap());
|
||||||
assert_eq!(admin_addr, "0.0.0.0:8080".parse::<SocketAddr>().unwrap());
|
assert_eq!(admin_addr, "0.0.0.0:8080".parse::<SocketAddr>().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,11 +460,14 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let http_addr = "0.0.0.0:3128".parse::<SocketAddr>().unwrap();
|
let http_addr = "0.0.0.0:3128".parse::<SocketAddr>().unwrap();
|
||||||
|
let socks_addr = "0.0.0.0:8081".parse::<SocketAddr>().unwrap();
|
||||||
let admin_addr = "0.0.0.0:8080".parse::<SocketAddr>().unwrap();
|
let admin_addr = "0.0.0.0:8080".parse::<SocketAddr>().unwrap();
|
||||||
|
|
||||||
let (http_addr, admin_addr) = clamp_bind_addrs(http_addr, admin_addr, &cfg);
|
let (http_addr, socks_addr, admin_addr) =
|
||||||
|
clamp_bind_addrs(http_addr, socks_addr, admin_addr, &cfg);
|
||||||
|
|
||||||
assert_eq!(http_addr, "127.0.0.1:3128".parse::<SocketAddr>().unwrap());
|
assert_eq!(http_addr, "127.0.0.1:3128".parse::<SocketAddr>().unwrap());
|
||||||
|
assert_eq!(socks_addr, "127.0.0.1:8081".parse::<SocketAddr>().unwrap());
|
||||||
assert_eq!(admin_addr, "127.0.0.1:8080".parse::<SocketAddr>().unwrap());
|
assert_eq!(admin_addr, "127.0.0.1:8080".parse::<SocketAddr>().unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ mod proxy;
|
|||||||
mod reasons;
|
mod reasons;
|
||||||
mod responses;
|
mod responses;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
|
mod socks5;
|
||||||
mod state;
|
mod state;
|
||||||
mod upstream;
|
mod upstream;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::config;
|
|||||||
use crate::http_proxy;
|
use crate::http_proxy;
|
||||||
use crate::network_policy::NetworkPolicyDecider;
|
use crate::network_policy::NetworkPolicyDecider;
|
||||||
use crate::runtime::unix_socket_permissions_supported;
|
use crate::runtime::unix_socket_permissions_supported;
|
||||||
|
use crate::socks5;
|
||||||
use crate::state::NetworkProxyState;
|
use crate::state::NetworkProxyState;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -61,8 +62,9 @@ impl NetworkProxyBuilder {
|
|||||||
let current_cfg = state.current_cfg().await?;
|
let current_cfg = state.current_cfg().await?;
|
||||||
let runtime = config::resolve_runtime(¤t_cfg)?;
|
let runtime = config::resolve_runtime(¤t_cfg)?;
|
||||||
// Reapply bind clamping for caller overrides so unix-socket proxying stays loopback-only.
|
// Reapply bind clamping for caller overrides so unix-socket proxying stays loopback-only.
|
||||||
let (http_addr, admin_addr) = config::clamp_bind_addrs(
|
let (http_addr, socks_addr, admin_addr) = config::clamp_bind_addrs(
|
||||||
self.http_addr.unwrap_or(runtime.http_addr),
|
self.http_addr.unwrap_or(runtime.http_addr),
|
||||||
|
runtime.socks_addr,
|
||||||
self.admin_addr.unwrap_or(runtime.admin_addr),
|
self.admin_addr.unwrap_or(runtime.admin_addr),
|
||||||
¤t_cfg.network_proxy,
|
¤t_cfg.network_proxy,
|
||||||
);
|
);
|
||||||
@@ -70,6 +72,7 @@ impl NetworkProxyBuilder {
|
|||||||
Ok(NetworkProxy {
|
Ok(NetworkProxy {
|
||||||
state,
|
state,
|
||||||
http_addr,
|
http_addr,
|
||||||
|
socks_addr,
|
||||||
admin_addr,
|
admin_addr,
|
||||||
policy_decider: self.policy_decider,
|
policy_decider: self.policy_decider,
|
||||||
})
|
})
|
||||||
@@ -80,6 +83,7 @@ impl NetworkProxyBuilder {
|
|||||||
pub struct NetworkProxy {
|
pub struct NetworkProxy {
|
||||||
state: Arc<NetworkProxyState>,
|
state: Arc<NetworkProxyState>,
|
||||||
http_addr: SocketAddr,
|
http_addr: SocketAddr,
|
||||||
|
socks_addr: SocketAddr,
|
||||||
admin_addr: SocketAddr,
|
admin_addr: SocketAddr,
|
||||||
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||||
}
|
}
|
||||||
@@ -105,10 +109,21 @@ impl NetworkProxy {
|
|||||||
self.http_addr,
|
self.http_addr,
|
||||||
self.policy_decider.clone(),
|
self.policy_decider.clone(),
|
||||||
));
|
));
|
||||||
|
let socks_task = if current_cfg.network_proxy.enable_socks5 {
|
||||||
|
Some(tokio::spawn(socks5::run_socks5(
|
||||||
|
self.state.clone(),
|
||||||
|
self.socks_addr,
|
||||||
|
self.policy_decider.clone(),
|
||||||
|
current_cfg.network_proxy.enable_socks5_udp,
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let admin_task = tokio::spawn(admin::run_admin_api(self.state.clone(), self.admin_addr));
|
let admin_task = tokio::spawn(admin::run_admin_api(self.state.clone(), self.admin_addr));
|
||||||
|
|
||||||
Ok(NetworkProxyHandle {
|
Ok(NetworkProxyHandle {
|
||||||
http_task: Some(http_task),
|
http_task: Some(http_task),
|
||||||
|
socks_task,
|
||||||
admin_task: Some(admin_task),
|
admin_task: Some(admin_task),
|
||||||
completed: false,
|
completed: false,
|
||||||
})
|
})
|
||||||
@@ -117,6 +132,7 @@ impl NetworkProxy {
|
|||||||
|
|
||||||
pub struct NetworkProxyHandle {
|
pub struct NetworkProxyHandle {
|
||||||
http_task: Option<JoinHandle<Result<()>>>,
|
http_task: Option<JoinHandle<Result<()>>>,
|
||||||
|
socks_task: Option<JoinHandle<Result<()>>>,
|
||||||
admin_task: Option<JoinHandle<Result<()>>>,
|
admin_task: Option<JoinHandle<Result<()>>>,
|
||||||
completed: bool,
|
completed: bool,
|
||||||
}
|
}
|
||||||
@@ -125,6 +141,7 @@ impl NetworkProxyHandle {
|
|||||||
fn noop() -> Self {
|
fn noop() -> Self {
|
||||||
Self {
|
Self {
|
||||||
http_task: Some(tokio::spawn(async { Ok(()) })),
|
http_task: Some(tokio::spawn(async { Ok(()) })),
|
||||||
|
socks_task: None,
|
||||||
admin_task: Some(tokio::spawn(async { Ok(()) })),
|
admin_task: Some(tokio::spawn(async { Ok(()) })),
|
||||||
completed: true,
|
completed: true,
|
||||||
}
|
}
|
||||||
@@ -133,33 +150,49 @@ impl NetworkProxyHandle {
|
|||||||
pub async fn wait(mut self) -> Result<()> {
|
pub async fn wait(mut self) -> Result<()> {
|
||||||
let http_task = self.http_task.take().context("missing http proxy task")?;
|
let http_task = self.http_task.take().context("missing http proxy task")?;
|
||||||
let admin_task = self.admin_task.take().context("missing admin proxy task")?;
|
let admin_task = self.admin_task.take().context("missing admin proxy task")?;
|
||||||
|
let socks_task = self.socks_task.take();
|
||||||
let http_result = http_task.await;
|
let http_result = http_task.await;
|
||||||
let admin_result = admin_task.await;
|
let admin_result = admin_task.await;
|
||||||
|
let socks_result = match socks_task {
|
||||||
|
Some(task) => Some(task.await),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
self.completed = true;
|
self.completed = true;
|
||||||
http_result??;
|
http_result??;
|
||||||
admin_result??;
|
admin_result??;
|
||||||
|
if let Some(socks_result) = socks_result {
|
||||||
|
socks_result??;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn shutdown(mut self) -> Result<()> {
|
pub async fn shutdown(mut self) -> Result<()> {
|
||||||
abort_tasks(self.http_task.take(), self.admin_task.take()).await;
|
abort_tasks(
|
||||||
|
self.http_task.take(),
|
||||||
|
self.socks_task.take(),
|
||||||
|
self.admin_task.take(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
self.completed = true;
|
self.completed = true;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn abort_task(task: Option<JoinHandle<Result<()>>>) {
|
||||||
|
if let Some(task) = task {
|
||||||
|
task.abort();
|
||||||
|
let _ = task.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn abort_tasks(
|
async fn abort_tasks(
|
||||||
http_task: Option<JoinHandle<Result<()>>>,
|
http_task: Option<JoinHandle<Result<()>>>,
|
||||||
|
socks_task: Option<JoinHandle<Result<()>>>,
|
||||||
admin_task: Option<JoinHandle<Result<()>>>,
|
admin_task: Option<JoinHandle<Result<()>>>,
|
||||||
) {
|
) {
|
||||||
if let Some(http_task) = http_task {
|
abort_task(http_task).await;
|
||||||
http_task.abort();
|
abort_task(socks_task).await;
|
||||||
let _ = http_task.await;
|
abort_task(admin_task).await;
|
||||||
}
|
|
||||||
if let Some(admin_task) = admin_task {
|
|
||||||
admin_task.abort();
|
|
||||||
let _ = admin_task.await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for NetworkProxyHandle {
|
impl Drop for NetworkProxyHandle {
|
||||||
@@ -168,9 +201,10 @@ impl Drop for NetworkProxyHandle {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let http_task = self.http_task.take();
|
let http_task = self.http_task.take();
|
||||||
|
let socks_task = self.socks_task.take();
|
||||||
let admin_task = self.admin_task.take();
|
let admin_task = self.admin_task.take();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
abort_tasks(http_task, admin_task).await;
|
abort_tasks(http_task, socks_task, admin_task).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
use crate::config::NetworkMode;
|
||||||
|
use crate::network_policy::NetworkDecision;
|
||||||
|
use crate::network_policy::NetworkPolicyDecider;
|
||||||
|
use crate::network_policy::NetworkPolicyRequest;
|
||||||
|
use crate::network_policy::NetworkProtocol;
|
||||||
|
use crate::network_policy::evaluate_host_policy;
|
||||||
|
use crate::policy::normalize_host;
|
||||||
|
use crate::reasons::REASON_METHOD_NOT_ALLOWED;
|
||||||
|
use crate::reasons::REASON_PROXY_DISABLED;
|
||||||
|
use crate::state::BlockedRequest;
|
||||||
|
use crate::state::NetworkProxyState;
|
||||||
|
use anyhow::Context as _;
|
||||||
|
use anyhow::Result;
|
||||||
|
use rama_core::Layer;
|
||||||
|
use rama_core::Service;
|
||||||
|
use rama_core::error::BoxError;
|
||||||
|
use rama_core::extensions::ExtensionsRef;
|
||||||
|
use rama_core::layer::AddInputExtensionLayer;
|
||||||
|
use rama_core::service::service_fn;
|
||||||
|
use rama_net::client::EstablishedClientConnection;
|
||||||
|
use rama_net::stream::SocketInfo;
|
||||||
|
use rama_socks5::Socks5Acceptor;
|
||||||
|
use rama_socks5::server::DefaultConnector;
|
||||||
|
use rama_socks5::server::DefaultUdpRelay;
|
||||||
|
use rama_socks5::server::udp::RelayRequest;
|
||||||
|
use rama_socks5::server::udp::RelayResponse;
|
||||||
|
use rama_tcp::TcpStream;
|
||||||
|
use rama_tcp::client::Request as TcpRequest;
|
||||||
|
use rama_tcp::client::service::TcpConnector;
|
||||||
|
use rama_tcp::server::TcpListener;
|
||||||
|
use std::io;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tracing::error;
|
||||||
|
use tracing::info;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
pub async fn run_socks5(
|
||||||
|
state: Arc<NetworkProxyState>,
|
||||||
|
addr: SocketAddr,
|
||||||
|
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||||
|
enable_socks5_udp: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let listener = TcpListener::build()
|
||||||
|
.bind(addr)
|
||||||
|
.await
|
||||||
|
// See `http_proxy.rs` for details on why we wrap `BoxError` before converting to anyhow.
|
||||||
|
.map_err(rama_core::error::OpaqueError::from)
|
||||||
|
.map_err(anyhow::Error::from)
|
||||||
|
.with_context(|| format!("bind SOCKS5 proxy: {addr}"))?;
|
||||||
|
|
||||||
|
info!("SOCKS5 proxy listening on {addr}");
|
||||||
|
|
||||||
|
match state.network_mode().await {
|
||||||
|
Ok(NetworkMode::Limited) => {
|
||||||
|
info!("SOCKS5 is blocked in limited mode; set mode=\"full\" to allow SOCKS5");
|
||||||
|
}
|
||||||
|
Ok(NetworkMode::Full) => {}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("failed to read network mode: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tcp_connector = TcpConnector::default();
|
||||||
|
let policy_tcp_connector = service_fn({
|
||||||
|
let policy_decider = policy_decider.clone();
|
||||||
|
move |req: TcpRequest| {
|
||||||
|
let tcp_connector = tcp_connector.clone();
|
||||||
|
let policy_decider = policy_decider.clone();
|
||||||
|
async move { handle_socks5_tcp(req, tcp_connector, policy_decider).await }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let socks_connector = DefaultConnector::default().with_connector(policy_tcp_connector);
|
||||||
|
let base = Socks5Acceptor::new().with_connector(socks_connector);
|
||||||
|
|
||||||
|
if enable_socks5_udp {
|
||||||
|
let udp_state = state.clone();
|
||||||
|
let udp_decider = policy_decider.clone();
|
||||||
|
let udp_relay = DefaultUdpRelay::default().with_async_inspector(service_fn({
|
||||||
|
move |request: RelayRequest| {
|
||||||
|
let udp_state = udp_state.clone();
|
||||||
|
let udp_decider = udp_decider.clone();
|
||||||
|
async move { inspect_socks5_udp(request, udp_state, udp_decider).await }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
let socks_acceptor = base.with_udp_associator(udp_relay);
|
||||||
|
listener
|
||||||
|
.serve(AddInputExtensionLayer::new(state).into_layer(socks_acceptor))
|
||||||
|
.await;
|
||||||
|
} else {
|
||||||
|
listener
|
||||||
|
.serve(AddInputExtensionLayer::new(state).into_layer(base))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_socks5_tcp(
|
||||||
|
req: TcpRequest,
|
||||||
|
tcp_connector: TcpConnector,
|
||||||
|
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||||
|
) -> Result<EstablishedClientConnection<TcpStream, TcpRequest>, BoxError> {
|
||||||
|
let app_state = req
|
||||||
|
.extensions()
|
||||||
|
.get::<Arc<NetworkProxyState>>()
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| io::Error::other("missing state"))?;
|
||||||
|
|
||||||
|
let host = normalize_host(&req.authority.host.to_string());
|
||||||
|
let port = req.authority.port;
|
||||||
|
if host.is_empty() {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid host").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = req
|
||||||
|
.extensions()
|
||||||
|
.get::<SocketInfo>()
|
||||||
|
.map(|info| info.peer_addr().to_string());
|
||||||
|
|
||||||
|
match app_state.enabled().await {
|
||||||
|
Ok(true) => {}
|
||||||
|
Ok(false) => {
|
||||||
|
let _ = app_state
|
||||||
|
.record_blocked(BlockedRequest::new(
|
||||||
|
host.clone(),
|
||||||
|
REASON_PROXY_DISABLED.to_string(),
|
||||||
|
client.clone(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"socks5".to_string(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
let client = client.as_deref().unwrap_or_default();
|
||||||
|
warn!("SOCKS blocked; proxy disabled (client={client}, host={host})");
|
||||||
|
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "proxy disabled").into());
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to read enabled state: {err}");
|
||||||
|
return Err(io::Error::other("proxy error").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match app_state.network_mode().await {
|
||||||
|
Ok(NetworkMode::Limited) => {
|
||||||
|
let _ = app_state
|
||||||
|
.record_blocked(BlockedRequest::new(
|
||||||
|
host.clone(),
|
||||||
|
REASON_METHOD_NOT_ALLOWED.to_string(),
|
||||||
|
client.clone(),
|
||||||
|
None,
|
||||||
|
Some(NetworkMode::Limited),
|
||||||
|
"socks5".to_string(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
let client = client.as_deref().unwrap_or_default();
|
||||||
|
warn!(
|
||||||
|
"SOCKS blocked by method policy (client={client}, host={host}, mode=limited, allowed_methods=GET, HEAD, OPTIONS)"
|
||||||
|
);
|
||||||
|
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "blocked").into());
|
||||||
|
}
|
||||||
|
Ok(NetworkMode::Full) => {}
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to evaluate method policy: {err}");
|
||||||
|
return Err(io::Error::other("proxy error").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = NetworkPolicyRequest::new(
|
||||||
|
NetworkProtocol::Socks5Tcp,
|
||||||
|
host.clone(),
|
||||||
|
port,
|
||||||
|
client.clone(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
match evaluate_host_policy(&app_state, policy_decider.as_ref(), &request).await {
|
||||||
|
Ok(NetworkDecision::Deny { reason }) => {
|
||||||
|
let _ = app_state
|
||||||
|
.record_blocked(BlockedRequest::new(
|
||||||
|
host.clone(),
|
||||||
|
reason.clone(),
|
||||||
|
client.clone(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"socks5".to_string(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
let client = client.as_deref().unwrap_or_default();
|
||||||
|
warn!("SOCKS blocked (client={client}, host={host}, reason={reason})");
|
||||||
|
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "blocked").into());
|
||||||
|
}
|
||||||
|
Ok(NetworkDecision::Allow) => {
|
||||||
|
let client = client.as_deref().unwrap_or_default();
|
||||||
|
info!("SOCKS allowed (client={client}, host={host}, port={port})");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to evaluate host: {err}");
|
||||||
|
return Err(io::Error::other("proxy error").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tcp_connector.serve(req).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn inspect_socks5_udp(
|
||||||
|
request: RelayRequest,
|
||||||
|
state: Arc<NetworkProxyState>,
|
||||||
|
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||||
|
) -> io::Result<RelayResponse> {
|
||||||
|
let RelayRequest {
|
||||||
|
server_address,
|
||||||
|
payload,
|
||||||
|
extensions,
|
||||||
|
..
|
||||||
|
} = request;
|
||||||
|
|
||||||
|
let host = normalize_host(&server_address.ip_addr.to_string());
|
||||||
|
let port = server_address.port;
|
||||||
|
if host.is_empty() {
|
||||||
|
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid host"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = extensions
|
||||||
|
.get::<SocketInfo>()
|
||||||
|
.map(|info| info.peer_addr().to_string());
|
||||||
|
|
||||||
|
match state.enabled().await {
|
||||||
|
Ok(true) => {}
|
||||||
|
Ok(false) => {
|
||||||
|
let _ = state
|
||||||
|
.record_blocked(BlockedRequest::new(
|
||||||
|
host.clone(),
|
||||||
|
REASON_PROXY_DISABLED.to_string(),
|
||||||
|
client.clone(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"socks5-udp".to_string(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
let client = client.as_deref().unwrap_or_default();
|
||||||
|
warn!("SOCKS UDP blocked; proxy disabled (client={client}, host={host})");
|
||||||
|
return Ok(RelayResponse {
|
||||||
|
maybe_payload: None,
|
||||||
|
extensions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to read enabled state: {err}");
|
||||||
|
return Err(io::Error::other("proxy error"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match state.network_mode().await {
|
||||||
|
Ok(NetworkMode::Limited) => {
|
||||||
|
let _ = state
|
||||||
|
.record_blocked(BlockedRequest::new(
|
||||||
|
host.clone(),
|
||||||
|
REASON_METHOD_NOT_ALLOWED.to_string(),
|
||||||
|
client.clone(),
|
||||||
|
None,
|
||||||
|
Some(NetworkMode::Limited),
|
||||||
|
"socks5-udp".to_string(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
return Ok(RelayResponse {
|
||||||
|
maybe_payload: None,
|
||||||
|
extensions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(NetworkMode::Full) => {}
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to evaluate method policy: {err}");
|
||||||
|
return Err(io::Error::other("proxy error"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = NetworkPolicyRequest::new(
|
||||||
|
NetworkProtocol::Socks5Udp,
|
||||||
|
host.clone(),
|
||||||
|
port,
|
||||||
|
client.clone(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
match evaluate_host_policy(&state, policy_decider.as_ref(), &request).await {
|
||||||
|
Ok(NetworkDecision::Deny { reason }) => {
|
||||||
|
let _ = state
|
||||||
|
.record_blocked(BlockedRequest::new(
|
||||||
|
host.clone(),
|
||||||
|
reason.clone(),
|
||||||
|
client.clone(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
"socks5-udp".to_string(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
let client = client.as_deref().unwrap_or_default();
|
||||||
|
warn!("SOCKS UDP blocked (client={client}, host={host}, reason={reason})");
|
||||||
|
Ok(RelayResponse {
|
||||||
|
maybe_payload: None,
|
||||||
|
extensions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Ok(NetworkDecision::Allow) => Ok(RelayResponse {
|
||||||
|
maybe_payload: Some(payload),
|
||||||
|
extensions,
|
||||||
|
}),
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to evaluate UDP host: {err}");
|
||||||
|
Err(io::Error::other("proxy error"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user