mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
## 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
162 lines
6.4 KiB
Rust
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}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|