Files
codex/codex-rs/network-proxy/src/connect_policy.rs
T
olliem-oai 94350a9101 Add managed network review domains before allow rules (#56154)
## Why

Managed network policies need to require approval for selected hosts even when an exact or wildcard allow rule matches them.

## What changed

- Add `experimental_network.review_domains` to managed requirements and route matching hosts through the existing network approval flow. Explicit denials retain precedence, and approved connections still undergo DNS and local-address checks.
- Preserve review patterns across environment policy attachments and remote proxy configuration. Reject execution on an exec-server that lacks the `networkProxyReviewDomains` capability when review patterns are present.
- Expose `reviewDomains` through the app-server requirements API and generated schemas, and show `review_domains` in debug configuration output.
- Update approval reasons to say that network access requires approval rather than assuming the host is absent from the allowlist.

## Testing

Extend coverage for review precedence over exact and wildcard allows, explicit denials, DNS checks after local and remote approvals, approval hooks, policy merging, remote configuration round trips, and requirements API serialization.

GitOrigin-RevId: 4a861061a0ed80b3537d3b72f2caecb0db2a4f0b
2026-10-22 11:52:34 +00:00

261 lines
9.0 KiB
Rust

use crate::policy::is_non_public_ip;
use crate::runtime::HostAuthorization;
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;
/// Connects traffic whose host policy has already allowed the request.
/// Resolved non-public targets still undergo explicit-deny and local-address checks.
#[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_with_local_binding(
&self.target.host.to_string(),
self.target.port,
/*allow_local_binding*/ None,
HostAuthorization::Approved,
)
.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()]);
config.review_domains = vec![target.ip().to_string()];
let state = Arc::new(network_proxy_state_for_policy(config));
let connector = TargetCheckedTcpConnector::new(Arc::clone(&state));
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:?}"
);
state
.add_denied_domain(&target.ip().to_string())
.await
.unwrap();
let request = rama_tcp::client::Request::new(HostWithPort::from(target));
assert!(Service::serve(&connector, request).await.is_err());
}
#[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()]);
config.review_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()
));
}
}