Files
codex/codex-rs/network-proxy/src/upstream_tests.rs
T
open-matt b8d5e3f12e Allow exec-server to proxy permitted private IPs upstream (#48568)
## Why

Private IP destinations always bypassed inherited upstream proxies, preventing their use for private networks reachable through an upstream VPN proxy.

## What changed

- Add `codex exec-server --proxy-private-ips-via-upstream`, also configurable with `CODEX_EXEC_SERVER_PROXY_PRIVATE_IPS_VIA_UPSTREAM=true`. The setting defaults to disabled.
- Allow permitted RFC 1918, carrier-grade NAT, and IPv6 unique-local destinations to use an applicable upstream proxy. Loopback and link-local destinations retain direct routing, and destination access policy still applies.
- Keep connections direct when no valid upstream proxy applies or `allow_upstream_proxy=false`. Errors after selecting an upstream proxy do not trigger a direct retry.
- Rename `ExecServerRuntimePaths` to `ExecServerRuntimeOptions` and carry the routing setting from executor startup into the managed network proxy.

## Testing

Add routing coverage for private address ranges, special-use addresses, and public targets with the option enabled and disabled. Verify that HTTP and CONNECT requests still enforce destination allowlists and denylists, and update the CLI help snapshot.

GitOrigin-RevId: b7c9cc7da0e0f545694a6521b74c9b36b7b92769
2026-09-26 23:17:41 +00:00

162 lines
6.4 KiB
Rust

use super::*;
use crate::config::NetworkProxyConfig;
use crate::state::network_proxy_state_for_policy;
use pretty_assertions::assert_eq;
use rama_http::StatusCode;
use rama_http::Version;
use rama_net::address::Host;
use rama_net::address::HostWithPort;
use rama_tls_rustls::dep::pki_types::CertificateDer;
use rama_tls_rustls::dep::pki_types::PrivateKeyDer;
use rama_tls_rustls::dep::pki_types::pem::PemObject;
use rama_tls_rustls::dep::rcgen::BasicConstraints;
use rama_tls_rustls::dep::rcgen::CertificateParams;
use rama_tls_rustls::dep::rcgen::DistinguishedName;
use rama_tls_rustls::dep::rcgen::DnType;
use rama_tls_rustls::dep::rcgen::ExtendedKeyUsagePurpose;
use rama_tls_rustls::dep::rcgen::IsCa;
use rama_tls_rustls::dep::rcgen::Issuer;
use rama_tls_rustls::dep::rcgen::KeyPair;
use rama_tls_rustls::dep::rcgen::KeyUsagePurpose;
use rama_tls_rustls::dep::rcgen::PKCS_ECDSA_P256_SHA256;
use rama_tls_rustls::dep::tokio_rustls::TlsAcceptor;
use std::collections::HashMap;
use std::fs;
use std::sync::Arc;
use tempfile::tempdir;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener;
fn generate_ca(common_name: &str) -> (String, KeyPair) {
let mut params = CertificateParams::default();
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
params.key_usages = vec![
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::KeyEncipherment,
];
let mut distinguished_name = DistinguishedName::new();
distinguished_name.push(DnType::CommonName, common_name);
params.distinguished_name = distinguished_name;
let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
let cert = params.self_signed(&key_pair).unwrap();
(cert.pem(), key_pair)
}
#[tokio::test]
async fn mitm_upstream_client_trusts_startup_custom_ca() {
ensure_rustls_crypto_provider();
let temp_dir = tempdir().unwrap();
let startup_ca_path = temp_dir.path().join("startup-ca.pem");
let managed_ca_path = temp_dir.path().join("managed-ca.pem");
let (startup_ca_pem, startup_ca_key) = generate_ca("startup CA");
let (managed_ca_pem, _) = generate_ca("managed MITM CA");
fs::write(&startup_ca_path, &startup_ca_pem).unwrap();
fs::write(&managed_ca_path, managed_ca_pem).unwrap();
let issuer = Issuer::from_ca_cert_pem(&startup_ca_pem, startup_ca_key).unwrap();
let mut server_params = CertificateParams::new(vec!["localhost".to_string()]).unwrap();
server_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
server_params.key_usages = vec![
KeyUsagePurpose::DigitalSignature,
KeyUsagePurpose::KeyEncipherment,
];
let server_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).unwrap();
let server_cert = server_params.signed_by(&server_key, &issuer).unwrap();
let server_cert = CertificateDer::from_pem_slice(server_cert.pem().as_bytes()).unwrap();
let server_key = PrivateKeyDer::from_pem_slice(server_key.serialize_pem().as_bytes()).unwrap();
let mut server_config =
rustls::ServerConfig::builder_with_protocol_versions(rustls::ALL_VERSIONS)
.with_no_client_auth()
.with_single_cert(vec![server_cert], server_key)
.unwrap();
server_config.alpn_protocols = vec![b"http/1.1".to_vec()];
let env = HashMap::from([(
"SSL_CERT_FILE",
startup_ca_path.to_string_lossy().into_owned(),
)]);
let roots =
crate::certs::upstream_tls_root_store_for_cert_path(&managed_ca_path, &env).unwrap();
let baseline_roots =
crate::certs::upstream_tls_root_store_for_cert_path(&managed_ca_path, &HashMap::new())
.unwrap();
assert_eq!(roots.len(), baseline_roots.len() + 1);
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let acceptor = TlsAcceptor::from(Arc::new(server_config));
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let mut stream = acceptor.accept(stream).await.unwrap();
let mut request = [0; 4096];
let bytes_read = stream.read(&mut request).await.unwrap();
assert!(bytes_read > 0);
stream
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
.await
.unwrap();
});
let mut config = NetworkProxyConfig::default();
config.set_allowed_domains(vec!["localhost".to_string()]);
let state = Arc::new(network_proxy_state_for_policy(config));
let client = UpstreamClient::direct_with_tls_root_store(state, roots);
let response = client
.serve(
Request::builder()
.version(Version::HTTP_2)
.uri(format!("https://localhost:{}/", address.port()))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
server.await.unwrap();
}
#[test]
fn private_ip_upstream_routing_is_opt_in() {
let proxy = ProxyAddress::try_from("http://127.0.0.1:43128").unwrap();
for enabled in [false, true] {
let config = ProxyConfig {
http: Some(proxy.clone()),
https: Some(proxy.clone()),
proxy_private_ips_via_upstream: enabled,
..ProxyConfig::default()
};
for (host, use_proxy) in [
("10.0.0.1", enabled),
("172.16.0.1", enabled),
("192.168.0.1", enabled),
("100.68.58.50", enabled),
("fd00::1", enabled),
("::ffff:100.68.58.50", enabled),
("localhost", false),
("127.0.0.1", false),
("::1", false),
("169.254.169.254", false),
("fe80::1", false),
("0.0.0.0", false),
("224.0.0.1", false),
("192.0.2.1", false),
("240.0.0.1", false),
("example.com", true),
("100.63.255.255", true),
("100.128.0.0", true),
] {
let target = HostWithPort::new(host.parse::<Host>().unwrap(), /*port*/ 443);
for is_secure in [false, true] {
assert_eq!(
config.proxy_for_target(&target, is_secure),
use_proxy.then(|| proxy.clone()),
"host={host}, enabled={enabled}, secure={is_secure}"
);
}
}
}
}