mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
network-proxy: focus PR1 on core http + policy
This commit is contained in:
151
codex-rs/Cargo.lock
generated
151
codex-rs/Cargo.lock
generated
@@ -396,45 +396,6 @@ dependencies = [
|
||||
"term",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60"
|
||||
dependencies = [
|
||||
"asn1-rs-derive",
|
||||
"asn1-rs-impl",
|
||||
"displaydoc",
|
||||
"nom 7.1.3",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-derive"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "asn1-rs-impl"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assert-json-diff"
|
||||
version = "2.0.2"
|
||||
@@ -1718,12 +1679,10 @@ dependencies = [
|
||||
"rama-http",
|
||||
"rama-http-backend",
|
||||
"rama-net",
|
||||
"rama-socks5",
|
||||
"rama-tcp",
|
||||
"rama-tls-boring",
|
||||
"rama-unix",
|
||||
"rama-utils",
|
||||
"rcgen",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"time",
|
||||
@@ -2652,20 +2611,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der-parser"
|
||||
version = "10.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"displaydoc",
|
||||
"nom 7.1.3",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
"rusticata-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.4"
|
||||
@@ -5128,15 +5073,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oid-registry"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
@@ -5419,16 +5355,6 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "3.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pem-rfc7468"
|
||||
version = "0.7.0"
|
||||
@@ -6139,21 +6065,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rama-tcp"
|
||||
version = "0.3.0-alpha.4"
|
||||
@@ -6192,18 +6103,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rama-unix"
|
||||
version = "0.3.0-alpha.4"
|
||||
@@ -6351,20 +6250,6 @@ dependencies = [
|
||||
"ratatui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rcgen"
|
||||
version = "0.14.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ec0a99f2de91c3cddc84b37e7db80e4d96b743e05607f647eb236fc0455907f"
|
||||
dependencies = [
|
||||
"pem",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"time",
|
||||
"x509-parser",
|
||||
"yasna",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.15"
|
||||
@@ -6595,15 +6480,6 @@ dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusticata-macros"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
|
||||
dependencies = [
|
||||
"nom 7.1.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.44"
|
||||
@@ -9652,24 +9528,6 @@ version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
|
||||
|
||||
[[package]]
|
||||
name = "x509-parser"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb3e137310115a65136898d2079f003ce33331a6c4b0d51f1531d1be082b6425"
|
||||
dependencies = [
|
||||
"asn1-rs",
|
||||
"data-encoding",
|
||||
"der-parser",
|
||||
"lazy_static",
|
||||
"nom 7.1.3",
|
||||
"oid-registry",
|
||||
"ring",
|
||||
"rusticata-macros",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xdg-home"
|
||||
version = "1.3.0"
|
||||
@@ -9686,15 +9544,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yasna"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
|
||||
dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.0"
|
||||
|
||||
@@ -22,7 +22,6 @@ clap = { workspace = true, features = ["derive"] }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
rcgen-rama = { package = "rcgen", version = "0.14", default-features = false, features = ["pem", "x509-parser", "ring"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
time = { workspace = true }
|
||||
@@ -33,7 +32,6 @@ rama-core = { 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-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-tls-boring = { version = "=0.3.0-alpha.4", features = ["http"] }
|
||||
rama-utils = { version = "=0.3.0-alpha.4" }
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
`codex-network-proxy` is Codex's local network policy enforcement proxy. It runs:
|
||||
|
||||
- an HTTP proxy (default `127.0.0.1:3128`)
|
||||
- a SOCKS5 proxy (default `127.0.0.1:8081`)
|
||||
- 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.
|
||||
@@ -44,42 +43,15 @@ allow_local_binding = false
|
||||
|
||||
# macOS-only: allows proxying to a unix socket when request includes `x-unix-socket: /path`.
|
||||
allow_unix_sockets = ["/tmp/example.sock"]
|
||||
|
||||
[network_proxy.mitm]
|
||||
# Enables CONNECT MITM for limited-mode HTTPS. If disabled, CONNECT is blocked in limited mode.
|
||||
enabled = true
|
||||
|
||||
# When true, logs request/response body sizes (up to max_body_bytes).
|
||||
inspect = false
|
||||
max_body_bytes = 4096
|
||||
|
||||
# These are resolved relative to $CODEX_HOME when relative.
|
||||
ca_cert_path = "network_proxy/mitm/ca.pem"
|
||||
ca_key_path = "network_proxy/mitm/ca.key"
|
||||
```
|
||||
|
||||
### 2) Initialize MITM directories (optional)
|
||||
|
||||
This ensures the MITM directory exists (and is a good smoke test that the binary runs):
|
||||
|
||||
```bash
|
||||
cargo run -p codex-network-proxy -- init
|
||||
```
|
||||
|
||||
### 3) Run the proxy
|
||||
### 2) Run the proxy
|
||||
|
||||
```bash
|
||||
cargo run -p codex-network-proxy --
|
||||
```
|
||||
|
||||
Optional flags:
|
||||
|
||||
```bash
|
||||
# Enable SOCKS5 UDP associate support (off by default).
|
||||
cargo run -p codex-network-proxy -- --enable-socks5-udp
|
||||
```
|
||||
|
||||
### 4) Point a client at it
|
||||
### 3) Point a client at it
|
||||
|
||||
For HTTP(S) traffic:
|
||||
|
||||
@@ -88,13 +60,7 @@ export HTTP_PROXY="http://127.0.0.1:3128"
|
||||
export HTTPS_PROXY="http://127.0.0.1:3128"
|
||||
```
|
||||
|
||||
For SOCKS5 traffic:
|
||||
|
||||
```bash
|
||||
export ALL_PROXY="socks5://127.0.0.1:8081"
|
||||
```
|
||||
|
||||
### 5) Understand blocks / debugging
|
||||
### 4) Understand blocks / debugging
|
||||
|
||||
When a request is blocked, the proxy responds with `403` and includes:
|
||||
|
||||
@@ -102,13 +68,10 @@ When a request is blocked, the proxy responds with `403` and includes:
|
||||
- `blocked-by-allowlist`
|
||||
- `blocked-by-denylist`
|
||||
- `blocked-by-method-policy`
|
||||
- `blocked-by-mitm-required`
|
||||
- `blocked-by-policy`
|
||||
|
||||
In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed. In addition, HTTPS `CONNECT`
|
||||
requires MITM to be enabled to allow read-only HTTPS; otherwise the proxy blocks CONNECT with
|
||||
reason `mitm_required`. In "full" mode, CONNECT is always a transparent tunnel even if MITM
|
||||
is enabled.
|
||||
In "limited" mode, only `GET`, `HEAD`, and `OPTIONS` are allowed for plain HTTP. HTTPS `CONNECT`
|
||||
remains a transparent tunnel, so limited-mode method enforcement does not apply to HTTPS.
|
||||
|
||||
## Library API
|
||||
|
||||
@@ -119,7 +82,6 @@ use codex_network_proxy::{NetworkProxy, NetworkDecision, NetworkPolicyRequest};
|
||||
|
||||
let proxy = NetworkProxy::builder()
|
||||
.http_addr("127.0.0.1:8080".parse()?)
|
||||
.socks_addr("127.0.0.1:1080".parse()?)
|
||||
.admin_addr("127.0.0.1:9000".parse()?)
|
||||
.policy_decider(|request: NetworkPolicyRequest| async move {
|
||||
// Example: auto-allow when exec policy already approved a command prefix.
|
||||
@@ -175,7 +137,7 @@ curl -sS -X POST http://127.0.0.1:8080/reload
|
||||
|
||||
- Unix socket proxying via the `x-unix-socket` header is **macOS-only**; other platforms will
|
||||
reject unix socket requests.
|
||||
- MITM TLS termination uses BoringSSL via Rama's `rama-tls-boring`; building the proxy requires a
|
||||
- HTTPS tunneling uses BoringSSL via Rama's `rama-tls-boring`; building the proxy requires a
|
||||
native toolchain and CMake on macOS/Linux/Windows.
|
||||
|
||||
## Security notes (important)
|
||||
@@ -191,7 +153,7 @@ what it can reasonably guarantee.
|
||||
allowlisted (best-effort DNS lookup).
|
||||
- Limited mode enforcement:
|
||||
- only `GET`, `HEAD`, and `OPTIONS` are allowed
|
||||
- HTTPS `CONNECT` requires MITM to be enabled, otherwise CONNECT is blocked (to avoid “tunnel hides method” bypass).
|
||||
- HTTPS `CONNECT` remains a tunnel; limited-mode method enforcement does not apply to HTTPS
|
||||
- Listener safety defaults:
|
||||
- the admin API is unauthenticated; non-loopback binds are clamped unless explicitly enabled via
|
||||
`dangerously_allow_non_loopback_admin`
|
||||
@@ -200,10 +162,6 @@ what it can reasonably guarantee.
|
||||
- when unix socket proxying is enabled, both listeners are forced to loopback to avoid turning the
|
||||
proxy into a remote bridge into local daemons.
|
||||
- `enabled` is enforced at runtime; when false the proxy no-ops and does not bind listeners.
|
||||
- MITM CA key handling:
|
||||
- the CA key file is created with restrictive permissions (`0600`) and written atomically using
|
||||
create-new + fsync + rename, to avoid partial writes or transiently-permissive modes.
|
||||
|
||||
Limitations:
|
||||
|
||||
- DNS rebinding is hard to fully prevent without pinning the resolved IP(s) all the way down to the
|
||||
|
||||
@@ -2,7 +2,6 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::net::IpAddr;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
@@ -29,8 +28,6 @@ pub struct NetworkProxyConfig {
|
||||
pub mode: NetworkMode,
|
||||
#[serde(default)]
|
||||
pub policy: NetworkPolicy,
|
||||
#[serde(default)]
|
||||
pub mitm: MitmConfig,
|
||||
}
|
||||
|
||||
impl Default for NetworkProxyConfig {
|
||||
@@ -44,7 +41,6 @@ impl Default for NetworkProxyConfig {
|
||||
dangerously_allow_non_loopback_admin: false,
|
||||
mode: NetworkMode::default(),
|
||||
policy: NetworkPolicy::default(),
|
||||
mitm: MitmConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,32 +65,6 @@ pub enum NetworkMode {
|
||||
Full,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MitmConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub inspect: bool,
|
||||
#[serde(default = "default_mitm_max_body_bytes")]
|
||||
pub max_body_bytes: usize,
|
||||
#[serde(default = "default_ca_cert_path")]
|
||||
pub ca_cert_path: PathBuf,
|
||||
#[serde(default = "default_ca_key_path")]
|
||||
pub ca_key_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for MitmConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
inspect: false,
|
||||
max_body_bytes: default_mitm_max_body_bytes(),
|
||||
ca_cert_path: default_ca_cert_path(),
|
||||
ca_key_path: default_ca_key_path(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_proxy_url() -> String {
|
||||
"http://127.0.0.1:3128".to_string()
|
||||
}
|
||||
@@ -103,18 +73,6 @@ fn default_admin_url() -> String {
|
||||
"http://127.0.0.1:8080".to_string()
|
||||
}
|
||||
|
||||
fn default_ca_cert_path() -> PathBuf {
|
||||
PathBuf::from("network_proxy/mitm/ca.pem")
|
||||
}
|
||||
|
||||
fn default_ca_key_path() -> PathBuf {
|
||||
PathBuf::from("network_proxy/mitm/ca.key")
|
||||
}
|
||||
|
||||
fn default_mitm_max_body_bytes() -> usize {
|
||||
4096
|
||||
}
|
||||
|
||||
fn clamp_non_loopback(addr: SocketAddr, allow_non_loopback: bool, name: &str) -> SocketAddr {
|
||||
if addr.ip().is_loopback() {
|
||||
return addr;
|
||||
@@ -173,7 +131,6 @@ pub(crate) fn clamp_bind_addrs(
|
||||
|
||||
pub struct RuntimeConfig {
|
||||
pub http_addr: SocketAddr,
|
||||
pub socks_addr: SocketAddr,
|
||||
pub admin_addr: SocketAddr,
|
||||
}
|
||||
|
||||
@@ -181,11 +138,9 @@ pub fn resolve_runtime(cfg: &Config) -> RuntimeConfig {
|
||||
let http_addr = resolve_addr(&cfg.network_proxy.proxy_url, 3128);
|
||||
let admin_addr = resolve_addr(&cfg.network_proxy.admin_url, 8080);
|
||||
let (http_addr, admin_addr) = clamp_bind_addrs(http_addr, admin_addr, &cfg.network_proxy);
|
||||
let socks_addr = SocketAddr::from(([127, 0, 0, 1], 8081));
|
||||
|
||||
RuntimeConfig {
|
||||
http_addr,
|
||||
socks_addr,
|
||||
admin_addr,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::config::NetworkMode;
|
||||
use crate::mitm;
|
||||
use crate::network_policy::NetworkDecision;
|
||||
use crate::network_policy::NetworkPolicyDecider;
|
||||
use crate::network_policy::NetworkPolicyRequest;
|
||||
@@ -196,39 +195,8 @@ async fn http_connect_accept(
|
||||
}
|
||||
};
|
||||
|
||||
let mitm_state = match app_state.mitm_state().await {
|
||||
Ok(state) => state,
|
||||
Err(err) => {
|
||||
error!("failed to load MITM state: {err}");
|
||||
return Err(text_response(StatusCode::INTERNAL_SERVER_ERROR, "error"));
|
||||
}
|
||||
};
|
||||
|
||||
if mode == NetworkMode::Limited && mitm_state.is_none() {
|
||||
// Limited mode is designed to be read-only. Without MITM, a CONNECT tunnel would hide the
|
||||
// inner HTTP method/headers from the proxy, effectively bypassing method policy.
|
||||
let _ = app_state
|
||||
.record_blocked(BlockedRequest::new(
|
||||
host.clone(),
|
||||
"mitm_required".to_string(),
|
||||
client.clone(),
|
||||
Some("CONNECT".to_string()),
|
||||
Some(NetworkMode::Limited),
|
||||
"http-connect".to_string(),
|
||||
))
|
||||
.await;
|
||||
let client = client.as_deref().unwrap_or_default();
|
||||
warn!(
|
||||
"CONNECT blocked; MITM required for read-only HTTPS in limited mode (client={client}, host={host}, mode=limited, allowed_methods=GET, HEAD, OPTIONS)"
|
||||
);
|
||||
return Err(blocked_text("mitm_required"));
|
||||
}
|
||||
|
||||
req.extensions_mut().insert(ProxyTarget(authority));
|
||||
req.extensions_mut().insert(mode);
|
||||
if let Some(mitm_state) = mitm_state {
|
||||
req.extensions_mut().insert(mitm_state);
|
||||
}
|
||||
|
||||
Ok((
|
||||
Response::builder()
|
||||
@@ -240,12 +208,6 @@ async fn http_connect_accept(
|
||||
}
|
||||
|
||||
async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> {
|
||||
let mode = upgraded
|
||||
.extensions()
|
||||
.get::<NetworkMode>()
|
||||
.copied()
|
||||
.unwrap_or(NetworkMode::Full);
|
||||
|
||||
let Some(target) = upgraded
|
||||
.extensions()
|
||||
.get::<ProxyTarget>()
|
||||
@@ -254,21 +216,7 @@ async fn http_connect_proxy(upgraded: Upgraded) -> Result<(), Infallible> {
|
||||
warn!("CONNECT missing proxy target");
|
||||
return Ok(());
|
||||
};
|
||||
let host = normalize_host(&target.host.to_string());
|
||||
|
||||
if mode == NetworkMode::Limited
|
||||
&& upgraded
|
||||
.extensions()
|
||||
.get::<Arc<mitm::MitmState>>()
|
||||
.is_some()
|
||||
{
|
||||
let port = target.port;
|
||||
info!("CONNECT MITM enabled (host={host}, port={port}, mode={mode:?})");
|
||||
if let Err(err) = mitm::mitm_tunnel(upgraded).await {
|
||||
warn!("MITM tunnel error: {err}");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let _host = normalize_host(&target.host.to_string());
|
||||
|
||||
let allow_upstream_proxy = match upgraded.extensions().get::<Arc<AppState>>().cloned() {
|
||||
Some(state) => match state.allow_upstream_proxy().await {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use codex_core::config::find_codex_home;
|
||||
use std::fs;
|
||||
|
||||
pub fn run_init() -> Result<()> {
|
||||
let codex_home = find_codex_home().context("failed to resolve CODEX_HOME")?;
|
||||
let root = codex_home.join("network_proxy");
|
||||
let mitm_dir = root.join("mitm");
|
||||
|
||||
fs::create_dir_all(&root).with_context(|| format!("failed to create {}", root.display()))?;
|
||||
fs::create_dir_all(&mitm_dir)
|
||||
.with_context(|| format!("failed to create {}", mitm_dir.display()))?;
|
||||
|
||||
println!("ensured {}", mitm_dir.display());
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
mod admin;
|
||||
mod config;
|
||||
mod http_proxy;
|
||||
mod init;
|
||||
mod mitm;
|
||||
mod network_policy;
|
||||
mod policy;
|
||||
mod proxy;
|
||||
mod responses;
|
||||
mod runtime;
|
||||
mod socks5;
|
||||
mod state;
|
||||
mod upstream;
|
||||
|
||||
@@ -18,18 +15,11 @@ pub use network_policy::NetworkPolicyDecider;
|
||||
pub use network_policy::NetworkPolicyRequest;
|
||||
pub use network_policy::NetworkProtocol;
|
||||
pub use proxy::Args;
|
||||
pub use proxy::Command;
|
||||
pub use proxy::NetworkProxy;
|
||||
pub use proxy::NetworkProxyBuilder;
|
||||
pub use proxy::NetworkProxyHandle;
|
||||
pub use proxy::run_init;
|
||||
|
||||
pub async fn run_main(args: Args) -> Result<()> {
|
||||
if let Some(Command::Init) = args.command {
|
||||
run_init()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let proxy = NetworkProxy::from_cli_args(args).await?;
|
||||
proxy.run().await?.wait().await
|
||||
}
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use codex_network_proxy::Args;
|
||||
use codex_network_proxy::Command;
|
||||
use codex_network_proxy::NetworkProxy;
|
||||
use codex_network_proxy::run_init;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let args = Args::parse();
|
||||
if let Some(Command::Init) = args.command {
|
||||
run_init()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let proxy = NetworkProxy::from_cli_args(args).await?;
|
||||
proxy.run().await?.wait().await
|
||||
}
|
||||
|
||||
@@ -1,622 +0,0 @@
|
||||
use crate::config::MitmConfig;
|
||||
use crate::config::NetworkMode;
|
||||
use crate::policy::method_allowed;
|
||||
use crate::policy::normalize_host;
|
||||
use crate::responses::blocked_text_response;
|
||||
use crate::state::AppState;
|
||||
use crate::state::BlockedRequest;
|
||||
use crate::upstream::UpstreamClient;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use rama_core::Layer;
|
||||
use rama_core::Service;
|
||||
use rama_core::bytes::Bytes;
|
||||
use rama_core::error::BoxError;
|
||||
use rama_core::extensions::ExtensionsRef;
|
||||
use rama_core::futures::stream::Stream;
|
||||
use rama_core::rt::Executor;
|
||||
use rama_core::service::service_fn;
|
||||
use rama_http::Body;
|
||||
use rama_http::BodyDataStream;
|
||||
use rama_http::HeaderValue;
|
||||
use rama_http::Request;
|
||||
use rama_http::Response;
|
||||
use rama_http::StatusCode;
|
||||
use rama_http::Uri;
|
||||
use rama_http::header::HOST;
|
||||
use rama_http::layer::remove_header::RemoveRequestHeaderLayer;
|
||||
use rama_http::layer::remove_header::RemoveResponseHeaderLayer;
|
||||
use rama_http_backend::server::HttpServer;
|
||||
use rama_http_backend::server::layer::upgrade::Upgraded;
|
||||
use rama_net::proxy::ProxyTarget;
|
||||
use rama_net::stream::SocketInfo;
|
||||
use rama_net::tls::ApplicationProtocol;
|
||||
use rama_net::tls::DataEncoding;
|
||||
use rama_net::tls::server::ServerAuth;
|
||||
use rama_net::tls::server::ServerAuthData;
|
||||
use rama_net::tls::server::ServerConfig;
|
||||
use rama_tls_boring::server::TlsAcceptorData;
|
||||
use rama_tls_boring::server::TlsAcceptorLayer;
|
||||
use rama_utils::str::NonEmptyStr;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::net::IpAddr;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::task::Context as TaskContext;
|
||||
use std::task::Poll;
|
||||
use std::time::SystemTime;
|
||||
use std::time::UNIX_EPOCH;
|
||||
use tracing::info;
|
||||
use tracing::warn;
|
||||
|
||||
use rcgen_rama::BasicConstraints;
|
||||
use rcgen_rama::CertificateParams;
|
||||
use rcgen_rama::DistinguishedName;
|
||||
use rcgen_rama::DnType;
|
||||
use rcgen_rama::ExtendedKeyUsagePurpose;
|
||||
use rcgen_rama::IsCa;
|
||||
use rcgen_rama::Issuer;
|
||||
use rcgen_rama::KeyPair;
|
||||
use rcgen_rama::KeyUsagePurpose;
|
||||
use rcgen_rama::SanType;
|
||||
|
||||
pub struct MitmState {
|
||||
issuer: Issuer<'static, KeyPair>,
|
||||
upstream: UpstreamClient,
|
||||
inspect: bool,
|
||||
max_body_bytes: usize,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MitmState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// Avoid dumping internal state (CA material, connectors, etc.) to logs.
|
||||
f.debug_struct("MitmState")
|
||||
.field("inspect", &self.inspect)
|
||||
.field("max_body_bytes", &self.max_body_bytes)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl MitmState {
|
||||
pub fn new(cfg: &MitmConfig, allow_upstream_proxy: bool) -> Result<Self> {
|
||||
// MITM exists to make limited-mode HTTPS enforceable: once CONNECT is established, plain
|
||||
// proxying would lose visibility into the inner HTTP request. We generate/load a local CA
|
||||
// and issue per-host leaf certs so we can terminate TLS and apply policy.
|
||||
let (ca_cert_pem, ca_key_pem) = load_or_create_ca(cfg)?;
|
||||
let ca_key = KeyPair::from_pem(&ca_key_pem).context("failed to parse CA key")?;
|
||||
let issuer: Issuer<'static, KeyPair> =
|
||||
Issuer::from_ca_cert_pem(&ca_cert_pem, ca_key).context("failed to parse CA cert")?;
|
||||
|
||||
let upstream = if allow_upstream_proxy {
|
||||
UpstreamClient::from_env_proxy()
|
||||
} else {
|
||||
UpstreamClient::direct()
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
issuer,
|
||||
upstream,
|
||||
inspect: cfg.inspect,
|
||||
max_body_bytes: cfg.max_body_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
fn tls_acceptor_data_for_host(&self, host: &str) -> Result<TlsAcceptorData> {
|
||||
let (cert_pem, key_pem) = issue_host_certificate_pem(host, &self.issuer)?;
|
||||
let cert_chain = DataEncoding::Pem(
|
||||
NonEmptyStr::try_from(cert_pem.as_str()).context("failed to encode host cert PEM")?,
|
||||
);
|
||||
let private_key = DataEncoding::Pem(
|
||||
NonEmptyStr::try_from(key_pem.as_str()).context("failed to encode host key PEM")?,
|
||||
);
|
||||
let auth = ServerAuthData {
|
||||
private_key,
|
||||
cert_chain,
|
||||
ocsp: None,
|
||||
};
|
||||
|
||||
let mut server_config = ServerConfig::new(ServerAuth::Single(auth));
|
||||
server_config.application_layer_protocol_negotiation = Some(vec![
|
||||
ApplicationProtocol::HTTP_2,
|
||||
ApplicationProtocol::HTTP_11,
|
||||
]);
|
||||
|
||||
TlsAcceptorData::try_from(server_config).context("failed to build boring acceptor config")
|
||||
}
|
||||
|
||||
pub fn inspect_enabled(&self) -> bool {
|
||||
self.inspect
|
||||
}
|
||||
|
||||
pub fn max_body_bytes(&self) -> usize {
|
||||
self.max_body_bytes
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mitm_tunnel(upgraded: Upgraded) -> Result<()> {
|
||||
let state = upgraded
|
||||
.extensions()
|
||||
.get::<Arc<MitmState>>()
|
||||
.cloned()
|
||||
.context("missing MITM state")?;
|
||||
let target = upgraded
|
||||
.extensions()
|
||||
.get::<ProxyTarget>()
|
||||
.context("missing proxy target")?
|
||||
.0
|
||||
.clone();
|
||||
let host = normalize_host(&target.host.to_string());
|
||||
let acceptor_data = state.tls_acceptor_data_for_host(&host)?;
|
||||
|
||||
let executor = upgraded
|
||||
.extensions()
|
||||
.get::<Executor>()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let http_service = HttpServer::auto(executor).service(
|
||||
(
|
||||
RemoveResponseHeaderLayer::hop_by_hop(),
|
||||
RemoveRequestHeaderLayer::hop_by_hop(),
|
||||
)
|
||||
.into_layer(service_fn(handle_mitm_request)),
|
||||
);
|
||||
|
||||
let https_service = TlsAcceptorLayer::new(acceptor_data)
|
||||
.with_store_client_hello(true)
|
||||
.into_layer(http_service);
|
||||
|
||||
https_service
|
||||
.serve(upgraded)
|
||||
.await
|
||||
.map_err(|err| anyhow!("MITM serve error: {err}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_mitm_request(req: Request) -> Result<Response, std::convert::Infallible> {
|
||||
let response = match forward_request(req).await {
|
||||
Ok(resp) => resp,
|
||||
Err(err) => {
|
||||
warn!("MITM upstream request failed: {err}");
|
||||
text_response(StatusCode::BAD_GATEWAY, "mitm upstream error")
|
||||
}
|
||||
};
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn forward_request(req: Request) -> Result<Response> {
|
||||
let target = req
|
||||
.extensions()
|
||||
.get::<ProxyTarget>()
|
||||
.context("missing proxy target")?
|
||||
.0
|
||||
.clone();
|
||||
|
||||
let target_host = normalize_host(&target.host.to_string());
|
||||
let target_port = target.port;
|
||||
let mode = req
|
||||
.extensions()
|
||||
.get::<NetworkMode>()
|
||||
.copied()
|
||||
.unwrap_or(NetworkMode::Full);
|
||||
let mitm = req
|
||||
.extensions()
|
||||
.get::<Arc<MitmState>>()
|
||||
.cloned()
|
||||
.context("missing MITM state")?;
|
||||
let app_state = req
|
||||
.extensions()
|
||||
.get::<Arc<AppState>>()
|
||||
.cloned()
|
||||
.context("missing app state")?;
|
||||
|
||||
if req.method().as_str() == "CONNECT" {
|
||||
return Ok(text_response(
|
||||
StatusCode::METHOD_NOT_ALLOWED,
|
||||
"CONNECT not supported inside MITM",
|
||||
));
|
||||
}
|
||||
|
||||
let method = req.method().as_str().to_string();
|
||||
let path = path_and_query(req.uri());
|
||||
let client = req
|
||||
.extensions()
|
||||
.get::<SocketInfo>()
|
||||
.map(|info| info.peer_addr().to_string());
|
||||
|
||||
if let Some(request_host) = extract_request_host(&req) {
|
||||
let normalized = normalize_host(&request_host);
|
||||
if !normalized.is_empty() && normalized != target_host {
|
||||
warn!("MITM host mismatch (target={target_host}, request_host={normalized})");
|
||||
return Ok(text_response(StatusCode::BAD_REQUEST, "host mismatch"));
|
||||
}
|
||||
}
|
||||
|
||||
if !method_allowed(mode, method.as_str()) {
|
||||
let _ = app_state
|
||||
.record_blocked(BlockedRequest::new(
|
||||
target_host.clone(),
|
||||
"method_not_allowed".to_string(),
|
||||
client.clone(),
|
||||
Some(method.clone()),
|
||||
Some(NetworkMode::Limited),
|
||||
"https".to_string(),
|
||||
))
|
||||
.await;
|
||||
warn!(
|
||||
"MITM blocked by method policy (host={target_host}, method={method}, path={path}, mode={mode:?}, allowed_methods=GET, HEAD, OPTIONS)"
|
||||
);
|
||||
return Ok(blocked_text("method_not_allowed"));
|
||||
}
|
||||
|
||||
let (mut parts, body) = req.into_parts();
|
||||
let authority = authority_header_value(&target_host, target_port);
|
||||
parts.uri = build_https_uri(&authority, &path)?;
|
||||
parts
|
||||
.headers
|
||||
.insert(HOST, HeaderValue::from_str(&authority)?);
|
||||
|
||||
let inspect = mitm.inspect_enabled();
|
||||
let max_body_bytes = mitm.max_body_bytes();
|
||||
let body = if inspect {
|
||||
inspect_body(
|
||||
body,
|
||||
max_body_bytes,
|
||||
RequestLogContext {
|
||||
host: authority.clone(),
|
||||
method: method.clone(),
|
||||
path: path.clone(),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
body
|
||||
};
|
||||
|
||||
let upstream_req = Request::from_parts(parts, body);
|
||||
let upstream_resp = mitm.upstream.serve(upstream_req).await?;
|
||||
respond_with_inspection(
|
||||
upstream_resp,
|
||||
inspect,
|
||||
max_body_bytes,
|
||||
&method,
|
||||
&path,
|
||||
&authority,
|
||||
)
|
||||
}
|
||||
|
||||
fn respond_with_inspection(
|
||||
resp: Response,
|
||||
inspect: bool,
|
||||
max_body_bytes: usize,
|
||||
method: &str,
|
||||
path: &str,
|
||||
authority: &str,
|
||||
) -> Result<Response> {
|
||||
if !inspect {
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
let (parts, body) = resp.into_parts();
|
||||
let body = inspect_body(
|
||||
body,
|
||||
max_body_bytes,
|
||||
ResponseLogContext {
|
||||
host: authority.to_string(),
|
||||
method: method.to_string(),
|
||||
path: path.to_string(),
|
||||
status: parts.status,
|
||||
},
|
||||
);
|
||||
Ok(Response::from_parts(parts, body))
|
||||
}
|
||||
|
||||
fn inspect_body<T: BodyLoggable + Send + 'static>(
|
||||
body: Body,
|
||||
max_body_bytes: usize,
|
||||
ctx: T,
|
||||
) -> Body {
|
||||
Body::from_stream(InspectStream {
|
||||
inner: Box::pin(body.into_data_stream()),
|
||||
ctx: Some(Box::new(ctx)),
|
||||
len: 0,
|
||||
max_body_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
struct InspectStream<T> {
|
||||
inner: Pin<Box<BodyDataStream>>,
|
||||
ctx: Option<Box<T>>,
|
||||
len: usize,
|
||||
max_body_bytes: usize,
|
||||
}
|
||||
|
||||
impl<T: BodyLoggable> Stream for InspectStream<T> {
|
||||
type Item = Result<Bytes, BoxError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
match this.inner.as_mut().poll_next(cx) {
|
||||
Poll::Ready(Some(Ok(bytes))) => {
|
||||
this.len = this.len.saturating_add(bytes.len());
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err))),
|
||||
Poll::Ready(None) => {
|
||||
if let Some(ctx) = this.ctx.take() {
|
||||
ctx.log(this.len, this.len > this.max_body_bytes);
|
||||
}
|
||||
Poll::Ready(None)
|
||||
}
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RequestLogContext {
|
||||
host: String,
|
||||
method: String,
|
||||
path: String,
|
||||
}
|
||||
|
||||
struct ResponseLogContext {
|
||||
host: String,
|
||||
method: String,
|
||||
path: String,
|
||||
status: StatusCode,
|
||||
}
|
||||
|
||||
trait BodyLoggable {
|
||||
fn log(self, len: usize, truncated: bool);
|
||||
}
|
||||
|
||||
impl BodyLoggable for RequestLogContext {
|
||||
fn log(self, len: usize, truncated: bool) {
|
||||
let host = self.host;
|
||||
let method = self.method;
|
||||
let path = self.path;
|
||||
info!(
|
||||
"MITM inspected request body (host={host}, method={method}, path={path}, body_len={len}, truncated={truncated})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl BodyLoggable for ResponseLogContext {
|
||||
fn log(self, len: usize, truncated: bool) {
|
||||
let host = self.host;
|
||||
let method = self.method;
|
||||
let path = self.path;
|
||||
let status = self.status;
|
||||
info!(
|
||||
"MITM inspected response body (host={host}, method={method}, path={path}, status={status}, body_len={len}, truncated={truncated})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_request_host(req: &Request) -> Option<String> {
|
||||
req.headers()
|
||||
.get(HOST)
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(ToString::to_string)
|
||||
.or_else(|| req.uri().authority().map(|a| a.as_str().to_string()))
|
||||
}
|
||||
|
||||
fn authority_header_value(host: &str, port: u16) -> String {
|
||||
// Host header / URI authority formatting.
|
||||
if host.contains(':') {
|
||||
if port == 443 {
|
||||
format!("[{host}]")
|
||||
} else {
|
||||
format!("[{host}]:{port}")
|
||||
}
|
||||
} else if port == 443 {
|
||||
host.to_string()
|
||||
} else {
|
||||
format!("{host}:{port}")
|
||||
}
|
||||
}
|
||||
|
||||
fn build_https_uri(authority: &str, path: &str) -> Result<Uri> {
|
||||
let target = format!("https://{authority}{path}");
|
||||
Ok(target.parse()?)
|
||||
}
|
||||
|
||||
fn path_and_query(uri: &Uri) -> String {
|
||||
uri.path_and_query()
|
||||
.map(rama_http::uri::PathAndQuery::as_str)
|
||||
.unwrap_or("/")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn issue_host_certificate_pem(
|
||||
host: &str,
|
||||
issuer: &Issuer<'_, KeyPair>,
|
||||
) -> Result<(String, String)> {
|
||||
let mut params = if let Ok(ip) = host.parse::<IpAddr>() {
|
||||
let mut params = CertificateParams::new(Vec::new())
|
||||
.map_err(|err| anyhow!("failed to create cert params: {err}"))?;
|
||||
params.subject_alt_names.push(SanType::IpAddress(ip));
|
||||
params
|
||||
} else {
|
||||
CertificateParams::new(vec![host.to_string()])
|
||||
.map_err(|err| anyhow!("failed to create cert params: {err}"))?
|
||||
};
|
||||
|
||||
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
|
||||
params.key_usages = vec![
|
||||
KeyUsagePurpose::DigitalSignature,
|
||||
KeyUsagePurpose::KeyEncipherment,
|
||||
];
|
||||
|
||||
let key_pair = KeyPair::generate_for(&rcgen_rama::PKCS_ECDSA_P256_SHA256)
|
||||
.map_err(|err| anyhow!("failed to generate host key pair: {err}"))?;
|
||||
let cert = params
|
||||
.signed_by(&key_pair, issuer)
|
||||
.map_err(|err| anyhow!("failed to sign host cert: {err}"))?;
|
||||
|
||||
Ok((cert.pem(), key_pair.serialize_pem()))
|
||||
}
|
||||
|
||||
fn load_or_create_ca(cfg: &MitmConfig) -> Result<(String, String)> {
|
||||
let cert_path = &cfg.ca_cert_path;
|
||||
let key_path = &cfg.ca_key_path;
|
||||
|
||||
if cert_path.exists() || key_path.exists() {
|
||||
if !cert_path.exists() || !key_path.exists() {
|
||||
return Err(anyhow!("both ca_cert_path and ca_key_path must exist"));
|
||||
}
|
||||
let cert_pem = fs::read_to_string(cert_path)
|
||||
.with_context(|| format!("failed to read CA cert {}", cert_path.display()))?;
|
||||
let key_pem = fs::read_to_string(key_path)
|
||||
.with_context(|| format!("failed to read CA key {}", key_path.display()))?;
|
||||
return Ok((cert_pem, key_pem));
|
||||
}
|
||||
|
||||
if let Some(parent) = cert_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create {}", parent.display()))?;
|
||||
}
|
||||
if let Some(parent) = key_path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("failed to create {}", parent.display()))?;
|
||||
}
|
||||
|
||||
let (cert_pem, key_pem) = generate_ca()?;
|
||||
// The CA key is a high-value secret. Create it atomically with restrictive permissions.
|
||||
// The cert can be world-readable, but we still write it atomically to avoid partial writes.
|
||||
//
|
||||
// We intentionally use create-new semantics: if a key already exists, we should not overwrite
|
||||
// it silently (that would invalidate previously-trusted cert chains).
|
||||
write_atomic_create_new(key_path, key_pem.as_bytes(), 0o600)
|
||||
.with_context(|| format!("failed to persist CA key {}", key_path.display()))?;
|
||||
if let Err(err) = write_atomic_create_new(cert_path, cert_pem.as_bytes(), 0o644)
|
||||
.with_context(|| format!("failed to persist CA cert {}", cert_path.display()))
|
||||
{
|
||||
// Avoid leaving a partially-created CA around (cert missing) if the second write fails.
|
||||
let _ = fs::remove_file(key_path);
|
||||
return Err(err);
|
||||
}
|
||||
let cert_path = cert_path.display();
|
||||
let key_path = key_path.display();
|
||||
info!("generated MITM CA (cert_path={cert_path}, key_path={key_path})");
|
||||
Ok((cert_pem, key_pem))
|
||||
}
|
||||
|
||||
fn generate_ca() -> Result<(String, String)> {
|
||||
let mut params = CertificateParams::default();
|
||||
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
|
||||
params.key_usages = vec![
|
||||
KeyUsagePurpose::KeyCertSign,
|
||||
KeyUsagePurpose::DigitalSignature,
|
||||
KeyUsagePurpose::KeyEncipherment,
|
||||
];
|
||||
let mut dn = DistinguishedName::new();
|
||||
dn.push(DnType::CommonName, "network_proxy MITM CA");
|
||||
params.distinguished_name = dn;
|
||||
|
||||
let key_pair = KeyPair::generate_for(&rcgen_rama::PKCS_ECDSA_P256_SHA256)
|
||||
.map_err(|err| anyhow!("failed to generate CA key pair: {err}"))?;
|
||||
let cert = params
|
||||
.self_signed(&key_pair)
|
||||
.map_err(|err| anyhow!("failed to generate CA cert: {err}"))?;
|
||||
Ok((cert.pem(), key_pair.serialize_pem()))
|
||||
}
|
||||
|
||||
fn write_atomic_create_new(path: &std::path::Path, contents: &[u8], mode: u32) -> Result<()> {
|
||||
let parent = path
|
||||
.parent()
|
||||
.ok_or_else(|| anyhow!("missing parent directory"))?;
|
||||
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos();
|
||||
let pid = std::process::id();
|
||||
let file_name = path.file_name().unwrap_or_default().to_string_lossy();
|
||||
let tmp_path = parent.join(format!(".{file_name}.tmp.{pid}.{nanos}"));
|
||||
|
||||
let mut file = open_create_new_with_mode(&tmp_path, mode)?;
|
||||
file.write_all(contents)
|
||||
.with_context(|| format!("failed to write {}", tmp_path.display()))?;
|
||||
file.sync_all()
|
||||
.with_context(|| format!("failed to fsync {}", tmp_path.display()))?;
|
||||
drop(file);
|
||||
|
||||
// Create the final file using "create-new" semantics (no overwrite). `rename` on Unix can
|
||||
// overwrite existing files, so prefer a hard-link, which fails if the destination exists.
|
||||
match fs::hard_link(&tmp_path, path) {
|
||||
Ok(()) => {
|
||||
fs::remove_file(&tmp_path)
|
||||
.with_context(|| format!("failed to remove {}", tmp_path.display()))?;
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
let _ = fs::remove_file(&tmp_path);
|
||||
return Err(anyhow!(
|
||||
"refusing to overwrite existing file {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
Err(_) => {
|
||||
// Best-effort fallback for environments where hard links are not supported.
|
||||
// This is still subject to a TOCTOU race, but the typical case is a private per-user
|
||||
// config directory, where other users cannot create files anyway.
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(&tmp_path);
|
||||
return Err(anyhow!(
|
||||
"refusing to overwrite existing file {}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
fs::rename(&tmp_path, path).with_context(|| {
|
||||
format!(
|
||||
"failed to rename {} -> {}",
|
||||
tmp_path.display(),
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
// Best-effort durability: ensure the directory entry is persisted too.
|
||||
let dir = File::open(parent).with_context(|| format!("failed to open {}", parent.display()))?;
|
||||
dir.sync_all()
|
||||
.with_context(|| format!("failed to fsync {}", parent.display()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn open_create_new_with_mode(path: &std::path::Path, mode: u32) -> Result<File> {
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.mode(mode)
|
||||
.open(path)
|
||||
.with_context(|| format!("failed to create {}", path.display()))
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn open_create_new_with_mode(path: &std::path::Path, _mode: u32) -> Result<File> {
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(path)
|
||||
.with_context(|| format!("failed to create {}", path.display()))
|
||||
}
|
||||
|
||||
fn blocked_text(reason: &str) -> Response {
|
||||
blocked_text_response(reason)
|
||||
}
|
||||
|
||||
fn text_response(status: StatusCode, body: &str) -> Response {
|
||||
Response::builder()
|
||||
.status(status)
|
||||
.header("content-type", "text/plain")
|
||||
.body(Body::from(body.to_string()))
|
||||
.unwrap_or_else(|_| Response::new(Body::from(body.to_string())))
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
use crate::admin;
|
||||
use crate::config;
|
||||
use crate::http_proxy;
|
||||
use crate::init;
|
||||
use crate::network_policy::NetworkPolicyDecider;
|
||||
use crate::socks5;
|
||||
use crate::state::AppState;
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use clap::Subcommand;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::task::JoinHandle;
|
||||
@@ -15,28 +12,14 @@ use tracing::warn;
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
#[command(name = "codex-network-proxy", about = "Codex network sandbox proxy")]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
/// Enable SOCKS5 UDP associate support (default: disabled).
|
||||
#[arg(long, default_value_t = false)]
|
||||
pub enable_socks5_udp: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
pub enum Command {
|
||||
/// Initialize the Codex network proxy directories (e.g. MITM cert paths).
|
||||
Init,
|
||||
}
|
||||
pub struct Args {}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct NetworkProxyBuilder {
|
||||
state: Option<Arc<AppState>>,
|
||||
http_addr: Option<SocketAddr>,
|
||||
socks_addr: Option<SocketAddr>,
|
||||
admin_addr: Option<SocketAddr>,
|
||||
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||
enable_socks5_udp: bool,
|
||||
}
|
||||
|
||||
impl NetworkProxyBuilder {
|
||||
@@ -52,12 +35,6 @@ impl NetworkProxyBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn socks_addr(mut self, addr: SocketAddr) -> Self {
|
||||
self.socks_addr = Some(addr);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn admin_addr(mut self, addr: SocketAddr) -> Self {
|
||||
self.admin_addr = Some(addr);
|
||||
@@ -79,12 +56,6 @@ impl NetworkProxyBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn enable_socks5_udp(mut self, enabled: bool) -> Self {
|
||||
self.enable_socks5_udp = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn build(self) -> Result<NetworkProxy> {
|
||||
let state = match self.state {
|
||||
Some(state) => state,
|
||||
@@ -102,10 +73,8 @@ impl NetworkProxyBuilder {
|
||||
Ok(NetworkProxy {
|
||||
state,
|
||||
http_addr,
|
||||
socks_addr: self.socks_addr.unwrap_or(runtime.socks_addr),
|
||||
admin_addr,
|
||||
policy_decider: self.policy_decider,
|
||||
enable_socks5_udp: self.enable_socks5_udp,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -114,10 +83,8 @@ impl NetworkProxyBuilder {
|
||||
pub struct NetworkProxy {
|
||||
state: Arc<AppState>,
|
||||
http_addr: SocketAddr,
|
||||
socks_addr: SocketAddr,
|
||||
admin_addr: SocketAddr,
|
||||
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||
enable_socks5_udp: bool,
|
||||
}
|
||||
|
||||
impl NetworkProxy {
|
||||
@@ -126,9 +93,8 @@ impl NetworkProxy {
|
||||
NetworkProxyBuilder::default()
|
||||
}
|
||||
|
||||
pub async fn from_cli_args(args: Args) -> Result<Self> {
|
||||
let mut builder = Self::builder();
|
||||
builder = builder.enable_socks5_udp(args.enable_socks5_udp);
|
||||
pub async fn from_cli_args(_args: Args) -> Result<Self> {
|
||||
let builder = Self::builder();
|
||||
builder.build().await
|
||||
}
|
||||
|
||||
@@ -148,17 +114,10 @@ impl NetworkProxy {
|
||||
self.http_addr,
|
||||
self.policy_decider.clone(),
|
||||
));
|
||||
let socks_task = tokio::spawn(socks5::run_socks5(
|
||||
self.state.clone(),
|
||||
self.socks_addr,
|
||||
self.policy_decider.clone(),
|
||||
self.enable_socks5_udp,
|
||||
));
|
||||
let admin_task = tokio::spawn(admin::run_admin_api(self.state.clone(), self.admin_addr));
|
||||
|
||||
Ok(NetworkProxyHandle {
|
||||
http_task,
|
||||
socks_task,
|
||||
admin_task,
|
||||
})
|
||||
}
|
||||
@@ -166,7 +125,6 @@ impl NetworkProxy {
|
||||
|
||||
pub struct NetworkProxyHandle {
|
||||
http_task: JoinHandle<Result<()>>,
|
||||
socks_task: JoinHandle<Result<()>>,
|
||||
admin_task: JoinHandle<Result<()>>,
|
||||
}
|
||||
|
||||
@@ -174,29 +132,21 @@ impl NetworkProxyHandle {
|
||||
fn noop() -> Self {
|
||||
Self {
|
||||
http_task: tokio::spawn(async { Ok(()) }),
|
||||
socks_task: tokio::spawn(async { Ok(()) }),
|
||||
admin_task: tokio::spawn(async { Ok(()) }),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wait(self) -> Result<()> {
|
||||
self.http_task.await??;
|
||||
self.socks_task.await??;
|
||||
self.admin_task.await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown(self) -> Result<()> {
|
||||
self.http_task.abort();
|
||||
self.socks_task.abort();
|
||||
self.admin_task.abort();
|
||||
let _ = self.http_task.await;
|
||||
let _ = self.socks_task.await;
|
||||
let _ = self.admin_task.await;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_init() -> Result<()> {
|
||||
init::run_init()
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ pub fn blocked_header_value(reason: &str) -> &'static str {
|
||||
"not_allowed" | "not_allowed_local" => "blocked-by-allowlist",
|
||||
"denied" => "blocked-by-denylist",
|
||||
"method_not_allowed" => "blocked-by-method-policy",
|
||||
"mitm_required" => "blocked-by-mitm-required",
|
||||
_ => "blocked-by-policy",
|
||||
}
|
||||
}
|
||||
@@ -39,7 +38,6 @@ pub fn blocked_message(reason: &str) -> &'static str {
|
||||
"not_allowed_local" => "Codex blocked this request: local/private addresses not allowed.",
|
||||
"denied" => "Codex blocked this request: domain denied by policy.",
|
||||
"method_not_allowed" => "Codex blocked this request: method not allowed in limited mode.",
|
||||
"mitm_required" => "Codex blocked this request: MITM required for limited HTTPS.",
|
||||
_ => "Codex blocked this request by network policy.",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::config::Config;
|
||||
use crate::config::NetworkMode;
|
||||
use crate::mitm::MitmState;
|
||||
use crate::policy::is_loopback_host;
|
||||
use crate::policy::is_non_public_ip;
|
||||
use crate::policy::method_allowed;
|
||||
@@ -68,7 +67,6 @@ pub(crate) struct ConfigState {
|
||||
pub(crate) mtime: Option<SystemTime>,
|
||||
pub(crate) allow_set: GlobSet,
|
||||
pub(crate) deny_set: GlobSet,
|
||||
pub(crate) mitm: Option<Arc<MitmState>>,
|
||||
pub(crate) constraints: NetworkProxyConstraints,
|
||||
pub(crate) cfg_path: PathBuf,
|
||||
pub(crate) blocked: VecDeque<BlockedRequest>,
|
||||
@@ -283,12 +281,6 @@ impl AppState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn mitm_state(&self) -> Result<Option<Arc<MitmState>>> {
|
||||
self.reload_if_needed().await?;
|
||||
let guard = self.state.read().await;
|
||||
Ok(guard.mitm.clone())
|
||||
}
|
||||
|
||||
async fn reload_if_needed(&self) -> Result<()> {
|
||||
let needs_reload = {
|
||||
let guard = self.state.read().await;
|
||||
@@ -415,7 +407,6 @@ pub(crate) fn app_state_for_policy(policy: crate::config::NetworkPolicy) -> AppS
|
||||
mtime: None,
|
||||
allow_set,
|
||||
deny_set,
|
||||
mitm: None,
|
||||
constraints: NetworkProxyConstraints::default(),
|
||||
cfg_path: PathBuf::from("/nonexistent/config.toml"),
|
||||
blocked: VecDeque::new(),
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
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::state::AppState;
|
||||
use crate::state::BlockedRequest;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::Result;
|
||||
use rama_core::Layer;
|
||||
use rama_core::Service;
|
||||
use rama_core::extensions::ExtensionsRef;
|
||||
use rama_core::layer::AddInputExtensionLayer;
|
||||
use rama_core::service::service_fn;
|
||||
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::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<AppState>,
|
||||
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 {
|
||||
let app_state = req
|
||||
.extensions()
|
||||
.get::<Arc<AppState>>()
|
||||
.cloned()
|
||||
.ok_or_else(|| io::Error::other("missing state"))?;
|
||||
|
||||
let host = normalize_host(&req.authority.host.to_string());
|
||||
let port = req.authority.port;
|
||||
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(),
|
||||
"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(),
|
||||
"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
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
let RelayRequest {
|
||||
server_address,
|
||||
payload,
|
||||
extensions,
|
||||
..
|
||||
} = request;
|
||||
|
||||
let host = normalize_host(&server_address.ip_addr.to_string());
|
||||
let port = server_address.port;
|
||||
let client = extensions
|
||||
.get::<SocketInfo>()
|
||||
.map(|info| info.peer_addr().to_string());
|
||||
match udp_state.enabled().await {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
let _ = udp_state
|
||||
.record_blocked(BlockedRequest::new(
|
||||
host.clone(),
|
||||
"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 udp_state.network_mode().await {
|
||||
Ok(NetworkMode::Limited) => {
|
||||
let _ = udp_state
|
||||
.record_blocked(BlockedRequest::new(
|
||||
host.clone(),
|
||||
"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(&udp_state, udp_decider.as_ref(), &request).await {
|
||||
Ok(NetworkDecision::Deny { reason }) => {
|
||||
let _ = udp_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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
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(())
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
use crate::config::Config;
|
||||
use crate::config::MitmConfig;
|
||||
use crate::config::NetworkMode;
|
||||
use crate::mitm::MitmState;
|
||||
use crate::policy::DomainPattern;
|
||||
use crate::policy::compile_globset;
|
||||
use crate::runtime::ConfigState;
|
||||
@@ -15,8 +13,6 @@ use codex_core::config::ConstraintError;
|
||||
use codex_core::config_loader::RequirementSource;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use crate::runtime::AppState;
|
||||
pub use crate::runtime::BlockedRequest;
|
||||
@@ -36,7 +32,7 @@ pub(crate) async fn build_config_state() -> Result<ConfigState> {
|
||||
// Deserialize from the merged effective config, rather than parsing config.toml ourselves.
|
||||
// This avoids a second parser/merger implementation (and the drift that comes with it).
|
||||
let merged_toml = codex_cfg.config_layer_stack.effective_config();
|
||||
let mut config: Config = merged_toml
|
||||
let config: Config = merged_toml
|
||||
.try_into()
|
||||
.context("failed to deserialize network proxy config")?;
|
||||
|
||||
@@ -44,52 +40,20 @@ pub(crate) async fn build_config_state() -> Result<ConfigState> {
|
||||
// trusted/managed layers (e.g., MDM). Enforce this before building runtime state.
|
||||
let constraints = enforce_trusted_constraints(&codex_cfg.config_layer_stack, &config)?;
|
||||
|
||||
// Permit relative MITM paths for ergonomics; resolve them relative to CODEX_HOME so the
|
||||
// proxy can be configured from multiple config locations without changing cert paths.
|
||||
resolve_mitm_paths(&mut config, &codex_cfg.codex_home);
|
||||
let mtime = cfg_path.metadata().and_then(|m| m.modified()).ok();
|
||||
let deny_set = compile_globset(&config.network_proxy.policy.denied_domains)?;
|
||||
let allow_set = compile_globset(&config.network_proxy.policy.allowed_domains)?;
|
||||
let mitm = if config.network_proxy.mitm.enabled {
|
||||
build_mitm_state(
|
||||
&config.network_proxy.mitm,
|
||||
config.network_proxy.allow_upstream_proxy,
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(ConfigState {
|
||||
config,
|
||||
mtime,
|
||||
allow_set,
|
||||
deny_set,
|
||||
mitm,
|
||||
constraints,
|
||||
cfg_path,
|
||||
blocked: std::collections::VecDeque::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_mitm_paths(config: &mut Config, codex_home: &Path) {
|
||||
let base = codex_home;
|
||||
if config.network_proxy.mitm.ca_cert_path.is_relative() {
|
||||
config.network_proxy.mitm.ca_cert_path = base.join(&config.network_proxy.mitm.ca_cert_path);
|
||||
}
|
||||
if config.network_proxy.mitm.ca_key_path.is_relative() {
|
||||
config.network_proxy.mitm.ca_key_path = base.join(&config.network_proxy.mitm.ca_key_path);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_mitm_state(
|
||||
config: &MitmConfig,
|
||||
allow_upstream_proxy: bool,
|
||||
) -> Result<Option<Arc<MitmState>>> {
|
||||
Ok(Some(Arc::new(MitmState::new(
|
||||
config,
|
||||
allow_upstream_proxy,
|
||||
)?)))
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct PartialConfig {
|
||||
#[serde(default)]
|
||||
|
||||
Reference in New Issue
Block a user