mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
## Why MXC's native host-loopback access is bidirectional, so it cannot enforce `allow_local_binding = false`. Treating an omitted setting as `false` prevents managed networking from working with the default configuration. ## What changed - Preserve an omitted `allow_local_binding` until the executor's sandbox policy is known. Default to `true` for Windows MXC and `false` elsewhere, including remote execution. - Reject an effective `false` for MXC managed networking after applying policy restrictions, without enabling disabled networking. - Use the executor's resolved value for remote network approval decisions while preserving explicit controller restrictions. - Document that local binding permits local servers and direct host-loopback connections and skips additional private-network destination checks; proxy domain rules still apply. ## Testing Add coverage for per-executor defaults, explicit values, MXC rejection of `false`, and remote network review cleanup with the resolved policy. GitOrigin-RevId: 0fa7c1eaec68bebada2b8f7af45688315eea70a9
245 lines
8.3 KiB
Rust
245 lines
8.3 KiB
Rust
use crate::policy::is_non_public_ip;
|
|
use crate::runtime::HostBlockDecision;
|
|
use crate::state::NetworkProxyState;
|
|
#[cfg(target_os = "macos")]
|
|
use crate::system_dns::SystemDnsResolver;
|
|
use rama_core::Service;
|
|
use rama_core::error::BoxError;
|
|
use rama_core::error::ErrorExt as _;
|
|
use rama_core::error::OpaqueError;
|
|
use rama_core::extensions::ExtensionsMut;
|
|
use rama_net::address::Host;
|
|
use rama_net::address::HostWithPort;
|
|
use rama_net::address::ProxyAddress;
|
|
use rama_net::client::EstablishedClientConnection;
|
|
use rama_net::transport::TryRefIntoTransportContext;
|
|
use rama_tcp::TcpStream;
|
|
use rama_tcp::client::TcpStreamConnector;
|
|
use rama_tcp::client::service::TcpConnector;
|
|
use std::io;
|
|
use std::net::SocketAddr;
|
|
use std::sync::Arc;
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct TargetCheckedTcpConnector {
|
|
state: Arc<NetworkProxyState>,
|
|
}
|
|
|
|
impl TargetCheckedTcpConnector {
|
|
pub(crate) fn new(state: Arc<NetworkProxyState>) -> Self {
|
|
Self { state }
|
|
}
|
|
}
|
|
|
|
impl<Input> Service<Input> for TargetCheckedTcpConnector
|
|
where
|
|
Input: TryRefIntoTransportContext + Send + ExtensionsMut + 'static,
|
|
Input::Error: Into<BoxError> + Send + Sync + 'static,
|
|
{
|
|
type Output = EstablishedClientConnection<TcpStream, Input>;
|
|
type Error = BoxError;
|
|
|
|
async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
|
|
let connector = TcpConnector::new();
|
|
#[cfg(target_os = "macos")]
|
|
let connector = connector.with_dns(SystemDnsResolver);
|
|
|
|
if input.extensions().get::<ProxyAddress>().is_some() {
|
|
return connector.serve(input).await;
|
|
}
|
|
|
|
let target = input
|
|
.try_ref_into_transport_ctx()
|
|
.map_err(|err| OpaqueError::from_boxed(err.into()).context("read network target"))?
|
|
.host_with_port()
|
|
.ok_or_else(|| OpaqueError::from_display("network target is missing a port"))?;
|
|
|
|
connector
|
|
.with_connector(TargetCheckedStreamConnector {
|
|
state: self.state.clone(),
|
|
target,
|
|
})
|
|
.serve(input)
|
|
.await
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct TargetCheckedStreamConnector {
|
|
state: Arc<NetworkProxyState>,
|
|
target: HostWithPort,
|
|
}
|
|
|
|
impl TcpStreamConnector for TargetCheckedStreamConnector {
|
|
type Error = BoxError;
|
|
|
|
async fn connect(&self, addr: SocketAddr) -> Result<TcpStream, Self::Error> {
|
|
if is_non_public_ip(addr.ip()) && !self.allows_non_public_target(addr).await? {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::PermissionDenied,
|
|
"network target rejected by policy",
|
|
)
|
|
.into());
|
|
}
|
|
|
|
tokio::net::TcpStream::connect(addr)
|
|
.await
|
|
.map(TcpStream::from)
|
|
.map_err(Into::into)
|
|
}
|
|
}
|
|
|
|
impl TargetCheckedStreamConnector {
|
|
async fn allows_non_public_target(&self, addr: SocketAddr) -> Result<bool, BoxError> {
|
|
if self.state.allow_local_binding().await.map_err(|err| {
|
|
let err: BoxError = err.into();
|
|
OpaqueError::from_boxed(err)
|
|
.context("read network proxy config")
|
|
.into_boxed()
|
|
})? {
|
|
return Ok(true);
|
|
}
|
|
|
|
if !target_matches_non_public_addr(&self.target.host, addr.ip()) {
|
|
return Ok(false);
|
|
}
|
|
|
|
self.state
|
|
.host_blocked(&self.target.host.to_string(), self.target.port)
|
|
.await
|
|
.map(|decision| decision == HostBlockDecision::Allowed)
|
|
.map_err(|err| {
|
|
let err: BoxError = err.into();
|
|
OpaqueError::from_boxed(err)
|
|
.context("evaluate network proxy target")
|
|
.into_boxed()
|
|
})
|
|
}
|
|
}
|
|
|
|
pub(crate) fn is_non_public_target(host: &Host) -> bool {
|
|
match host {
|
|
Host::Address(ip) => is_non_public_ip(*ip),
|
|
Host::Name(name) => name
|
|
.as_str()
|
|
.trim_end_matches('.')
|
|
.eq_ignore_ascii_case("localhost"),
|
|
}
|
|
}
|
|
|
|
fn target_matches_non_public_addr(host: &Host, addr: std::net::IpAddr) -> bool {
|
|
match host {
|
|
Host::Address(ip) => *ip == addr,
|
|
Host::Name(name) => {
|
|
name.as_str()
|
|
.trim_end_matches('.')
|
|
.eq_ignore_ascii_case("localhost")
|
|
&& addr.is_loopback()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::config::NetworkProxyConfig;
|
|
use crate::state::network_proxy_state_for_policy;
|
|
use rama_net::address::HostWithPort;
|
|
use std::net::Ipv4Addr;
|
|
use tokio::net::TcpListener;
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn direct_connector_rejects_non_public_target_when_local_binding_disabled() {
|
|
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
|
|
.await
|
|
.expect("bind local listener");
|
|
let target = listener.local_addr().expect("local addr");
|
|
let connector = TargetCheckedTcpConnector::new(Arc::new(network_proxy_state_for_policy(
|
|
NetworkProxyConfig::default(),
|
|
)));
|
|
|
|
let request: rama_tcp::client::Request =
|
|
rama_tcp::client::Request::new(HostWithPort::from(target));
|
|
let err = Service::serve(&connector, request)
|
|
.await
|
|
.expect_err("local target should be rejected");
|
|
|
|
assert!(
|
|
format!("{err:?}").contains("network target rejected by policy"),
|
|
"unexpected error: {err:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn direct_connector_allows_non_public_target_when_local_binding_enabled() {
|
|
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
|
|
.await
|
|
.expect("bind local listener");
|
|
let target = listener.local_addr().expect("local addr");
|
|
let connector = TargetCheckedTcpConnector::new(Arc::new(network_proxy_state_for_policy(
|
|
NetworkProxyConfig {
|
|
allow_local_binding: Some(true),
|
|
..NetworkProxyConfig::default()
|
|
},
|
|
)));
|
|
|
|
let request: rama_tcp::client::Request =
|
|
rama_tcp::client::Request::new(HostWithPort::from(target));
|
|
let result = Service::serve(&connector, request).await;
|
|
|
|
assert!(result.is_ok(), "local target should be allowed: {result:?}");
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn direct_connector_allows_explicitly_allowlisted_non_public_target() {
|
|
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
|
|
.await
|
|
.expect("bind local listener");
|
|
let target = listener.local_addr().expect("local addr");
|
|
let mut config = NetworkProxyConfig::default();
|
|
config.set_allowed_domains(vec![target.ip().to_string()]);
|
|
let connector =
|
|
TargetCheckedTcpConnector::new(Arc::new(network_proxy_state_for_policy(config)));
|
|
|
|
let request: rama_tcp::client::Request =
|
|
rama_tcp::client::Request::new(HostWithPort::from(target));
|
|
let result = Service::serve(&connector, request).await;
|
|
|
|
assert!(
|
|
result.is_ok(),
|
|
"explicitly allowlisted local target should be allowed: {result:?}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
async fn direct_connector_allows_explicitly_allowlisted_localhost_target() {
|
|
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0))
|
|
.await
|
|
.expect("bind local listener");
|
|
let target = listener.local_addr().expect("local addr");
|
|
let mut config = NetworkProxyConfig::default();
|
|
config.set_allowed_domains(vec!["localhost".to_string()]);
|
|
let connector =
|
|
TargetCheckedTcpConnector::new(Arc::new(network_proxy_state_for_policy(config)));
|
|
|
|
let request: rama_tcp::client::Request =
|
|
rama_tcp::client::Request::new(HostWithPort::new(Host::LOCALHOST_NAME, target.port()));
|
|
let result = Service::serve(&connector, request).await;
|
|
|
|
assert!(
|
|
result.is_ok(),
|
|
"explicitly allowlisted localhost target should be allowed: {result:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn resolved_private_address_does_not_match_allowlisted_hostname() {
|
|
let host = Host::Name("example.com".parse().expect("valid domain"));
|
|
|
|
assert!(!target_matches_non_public_addr(
|
|
&host,
|
|
Ipv4Addr::LOCALHOST.into()
|
|
));
|
|
}
|
|
}
|