mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
feat(linux-sandbox): route DNS through managed proxy
Co-authored-by: Codex noreply@openai.com
This commit is contained in:
co-authored by
Codex noreply@openai.com
parent
6b69ac60bd
commit
2307518a48
Generated
+1
@@ -3309,6 +3309,7 @@ dependencies = [
|
||||
"landlock",
|
||||
"libc",
|
||||
"pretty_assertions",
|
||||
"rustix 1.1.4",
|
||||
"seccompiler",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
@@ -388,6 +388,7 @@ rustls = { version = "0.23", default-features = false, features = [
|
||||
] }
|
||||
rustls-native-certs = "0.8.3"
|
||||
rustls-pki-types = "1.14.0"
|
||||
rustix = { version = "1.1.4", features = ["thread"] }
|
||||
schemars = "0.8.22"
|
||||
seccompiler = "0.5.0"
|
||||
semver = "1.0"
|
||||
|
||||
@@ -31,6 +31,7 @@ seccompiler = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
rustix = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dev-dependencies]
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
use codex_network_proxy::DNS_PROXY_ENV_KEY;
|
||||
use codex_network_proxy::DNS_PROXY_SESSION_PREFACE;
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::TcpListener;
|
||||
use std::net::TcpStream;
|
||||
use std::net::UdpSocket;
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
const SANDBOX_DNS_LISTEN_ADDR: (Ipv4Addr, u16) = (Ipv4Addr::LOCALHOST, 53);
|
||||
const DNS_IO_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
const MAX_DNS_MESSAGE_BYTES: usize = usize::from(u16::MAX);
|
||||
|
||||
pub(crate) struct BoundDnsStub {
|
||||
udp_socket: UdpSocket,
|
||||
tcp_listener: TcpListener,
|
||||
}
|
||||
|
||||
pub(crate) fn bind_netns_dns_stub() -> io::Result<BoundDnsStub> {
|
||||
let udp_socket = UdpSocket::bind(SANDBOX_DNS_LISTEN_ADDR)?;
|
||||
let tcp_listener = TcpListener::bind(SANDBOX_DNS_LISTEN_ADDR)?;
|
||||
Ok(BoundDnsStub {
|
||||
udp_socket,
|
||||
tcp_listener,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn spawn_netns_dns_stub(bound: BoundDnsStub) -> io::Result<()> {
|
||||
let endpoint = take_dns_proxy_endpoint_from_env()?.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"DNS bridge expected a proxy endpoint",
|
||||
)
|
||||
})?;
|
||||
let pid = unsafe { libc::fork() };
|
||||
if pid < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
if pid == 0 {
|
||||
let status = i32::from(
|
||||
crate::proxy_routing::harden_bridge_process()
|
||||
.and_then(|_| run_netns_dns_stub(bound, endpoint))
|
||||
.is_err(),
|
||||
);
|
||||
unsafe { libc::_exit(status) };
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn take_dns_proxy_endpoint_from_env() -> io::Result<Option<SocketAddr>> {
|
||||
let Some(value) = std::env::var_os(DNS_PROXY_ENV_KEY) else {
|
||||
return Ok(None);
|
||||
};
|
||||
// SAFETY: the helper process is single-threaded at this point, before execing
|
||||
// the user command.
|
||||
unsafe { std::env::remove_var(DNS_PROXY_ENV_KEY) };
|
||||
let value = value
|
||||
.into_string()
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "DNS proxy URL is not UTF-8"))?;
|
||||
parse_dns_proxy_endpoint(value.as_str()).map(Some)
|
||||
}
|
||||
|
||||
fn parse_dns_proxy_endpoint(value: &str) -> io::Result<SocketAddr> {
|
||||
let parsed = Url::parse(value).map_err(|err| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("invalid DNS proxy URL: {err}"),
|
||||
)
|
||||
})?;
|
||||
if parsed.scheme() != "tcp" {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"DNS proxy URL must use tcp scheme",
|
||||
));
|
||||
}
|
||||
let Some(host) = parsed.host_str() else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"DNS proxy URL is missing host",
|
||||
));
|
||||
};
|
||||
if host != "127.0.0.1" {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"DNS proxy URL must point at 127.0.0.1",
|
||||
));
|
||||
}
|
||||
let Some(port) = parsed.port() else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"DNS proxy URL is missing port",
|
||||
));
|
||||
};
|
||||
Ok(SocketAddr::from((Ipv4Addr::LOCALHOST, port)))
|
||||
}
|
||||
|
||||
fn run_netns_dns_stub(bound: BoundDnsStub, endpoint: SocketAddr) -> io::Result<()> {
|
||||
let udp_endpoint = endpoint;
|
||||
std::thread::Builder::new().spawn(move || netns_udp_loop(bound.udp_socket, udp_endpoint))?;
|
||||
netns_tcp_loop(bound.tcp_listener, endpoint)
|
||||
}
|
||||
|
||||
fn netns_udp_loop(socket: UdpSocket, endpoint: SocketAddr) -> io::Result<()> {
|
||||
let mut query = vec![0; MAX_DNS_MESSAGE_BYTES];
|
||||
loop {
|
||||
let (len, peer) = socket.recv_from(&mut query)?;
|
||||
let response = resolve_through_proxy(endpoint, &query[..len])?;
|
||||
socket.send_to(&response, peer)?;
|
||||
}
|
||||
}
|
||||
|
||||
fn netns_tcp_loop(listener: TcpListener, endpoint: SocketAddr) -> io::Result<()> {
|
||||
loop {
|
||||
let (mut client, _) = listener.accept()?;
|
||||
client.set_read_timeout(Some(DNS_IO_TIMEOUT))?;
|
||||
client.set_write_timeout(Some(DNS_IO_TIMEOUT))?;
|
||||
std::thread::spawn(move || {
|
||||
let _ = serve_tcp_client(&mut client, endpoint);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn serve_tcp_client(client: &mut TcpStream, endpoint: SocketAddr) -> io::Result<()> {
|
||||
while let Ok(query) = read_frame(client) {
|
||||
let response = resolve_through_proxy(endpoint, &query)?;
|
||||
write_frame(client, &response)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_through_proxy(endpoint: SocketAddr, query: &[u8]) -> io::Result<Vec<u8>> {
|
||||
let mut stream = TcpStream::connect(endpoint)?;
|
||||
stream.set_read_timeout(Some(DNS_IO_TIMEOUT))?;
|
||||
stream.set_write_timeout(Some(DNS_IO_TIMEOUT))?;
|
||||
stream.write_all(DNS_PROXY_SESSION_PREFACE)?;
|
||||
let mut ack = [0_u8; DNS_PROXY_SESSION_PREFACE.len()];
|
||||
stream.read_exact(&mut ack)?;
|
||||
if &ack != DNS_PROXY_SESSION_PREFACE {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"DNS proxy rejected session preface",
|
||||
));
|
||||
}
|
||||
write_frame(&mut stream, query)?;
|
||||
read_frame(&mut stream)
|
||||
}
|
||||
|
||||
fn write_frame(writer: &mut impl Write, payload: &[u8]) -> io::Result<()> {
|
||||
let len = u16::try_from(payload.len()).map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"DNS message exceeds maximum wire length",
|
||||
)
|
||||
})?;
|
||||
writer.write_all(&len.to_be_bytes())?;
|
||||
writer.write_all(payload)
|
||||
}
|
||||
|
||||
fn read_frame(reader: &mut impl Read) -> io::Result<Vec<u8>> {
|
||||
let mut len = [0; 2];
|
||||
reader.read_exact(&mut len)?;
|
||||
let mut payload = vec![0; usize::from(u16::from_be_bytes(len))];
|
||||
reader.read_exact(&mut payload)?;
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "dns_routing_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,45 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::net::TcpListener;
|
||||
use std::sync::mpsc;
|
||||
|
||||
#[test]
|
||||
fn parses_loopback_dns_proxy_endpoint() {
|
||||
assert_eq!(
|
||||
parse_dns_proxy_endpoint("tcp://127.0.0.1:43128").expect("endpoint"),
|
||||
SocketAddr::from((Ipv4Addr::LOCALHOST, 43128))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_loopback_dns_proxy_endpoint() {
|
||||
let err =
|
||||
parse_dns_proxy_endpoint("tcp://192.0.2.10:43128").expect_err("non-loopback endpoint");
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forwards_dns_query_to_private_proxy_session() {
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind DNS proxy fixture");
|
||||
let endpoint = listener.local_addr().expect("fixture local addr");
|
||||
let (received_tx, received_rx) = mpsc::channel();
|
||||
let expected_response = b"dns-response".to_vec();
|
||||
let response = expected_response.clone();
|
||||
std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept DNS proxy session");
|
||||
let mut preface = [0_u8; DNS_PROXY_SESSION_PREFACE.len()];
|
||||
stream.read_exact(&mut preface).expect("read preface");
|
||||
assert_eq!(&preface, DNS_PROXY_SESSION_PREFACE);
|
||||
stream
|
||||
.write_all(DNS_PROXY_SESSION_PREFACE)
|
||||
.expect("write preface ack");
|
||||
let query = read_frame(&mut stream).expect("read query");
|
||||
received_tx.send(query).expect("send query");
|
||||
write_frame(&mut stream, &response).expect("write response");
|
||||
});
|
||||
|
||||
let response = resolve_through_proxy(endpoint, b"dns-query").expect("proxy response");
|
||||
|
||||
assert_eq!(received_rx.recv().expect("query"), b"dns-query");
|
||||
assert_eq!(response, expected_response);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
use crate::bwrap::BwrapArgs;
|
||||
use codex_protocol::permissions::ReadDenyMatcher;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
use rustix::thread::CapabilitySet;
|
||||
use rustix::thread::CapabilitySets;
|
||||
use rustix::thread::capabilities;
|
||||
use rustix::thread::capability_is_in_ambient_set;
|
||||
use rustix::thread::capability_is_in_bounding_set;
|
||||
use rustix::thread::clear_ambient_capability_set;
|
||||
use rustix::thread::remove_capability_from_bounding_set;
|
||||
use rustix::thread::set_capabilities;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::Seek;
|
||||
use std::io::SeekFrom;
|
||||
use std::io::Write;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::fd::FromRawFd;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const RESOLV_CONF_PATH: &str = "/etc/resolv.conf";
|
||||
const LOADER_ENV_KEYS: &[&str] = &["LD_AUDIT", "LD_LIBRARY_PATH", "LD_PRELOAD"];
|
||||
|
||||
pub(crate) type LoaderEnvironment = Vec<(String, String)>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ResolvConfMount {
|
||||
pub(crate) file: File,
|
||||
pub(crate) path: PathBuf,
|
||||
}
|
||||
|
||||
pub(crate) fn create_resolv_conf_file() -> io::Result<File> {
|
||||
let fd = unsafe { libc::memfd_create(c"codex-resolv-conf".as_ptr(), libc::MFD_CLOEXEC) };
|
||||
if fd < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
let mut file = unsafe { File::from_raw_fd(fd) };
|
||||
file.write_all(b"nameserver 127.0.0.1\n")?;
|
||||
file.seek(SeekFrom::Start(0))?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
pub(crate) fn resolv_conf_mount_path(
|
||||
policy: &FileSystemSandboxPolicy,
|
||||
cwd: &Path,
|
||||
) -> io::Result<PathBuf> {
|
||||
let logical_path = Path::new(RESOLV_CONF_PATH);
|
||||
let target_path = logical_path.canonicalize()?;
|
||||
ensure_resolver_paths_allowed(policy, cwd, logical_path, &target_path)?;
|
||||
Ok(target_path)
|
||||
}
|
||||
|
||||
fn ensure_resolver_paths_allowed(
|
||||
policy: &FileSystemSandboxPolicy,
|
||||
cwd: &Path,
|
||||
logical_path: &Path,
|
||||
target_path: &Path,
|
||||
) -> io::Result<()> {
|
||||
let deny_matcher = ReadDenyMatcher::new(policy, cwd);
|
||||
if [logical_path, target_path].into_iter().any(|path| {
|
||||
!policy.can_read_path_with_cwd(path, cwd)
|
||||
|| deny_matcher
|
||||
.as_ref()
|
||||
.is_some_and(|deny| deny.is_read_denied(path))
|
||||
}) {
|
||||
return Err(io::Error::from(io::ErrorKind::PermissionDenied));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn capture_loader_environment() -> LoaderEnvironment {
|
||||
LOADER_ENV_KEYS
|
||||
.iter()
|
||||
.filter_map(|&key| {
|
||||
std::env::var_os(key).map(|value| {
|
||||
(
|
||||
key.to_string(),
|
||||
value
|
||||
.into_string()
|
||||
.unwrap_or_else(|_| panic!("{key} must contain valid UTF-8")),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn restore_loader_environment(environment: LoaderEnvironment) {
|
||||
for (key, value) in environment {
|
||||
// SAFETY: the setup process is single-threaded and has dropped all capabilities.
|
||||
unsafe { std::env::set_var(key, value) };
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn append_bwrap_args(bwrap_args: &mut BwrapArgs, mount: ResolvConfMount) {
|
||||
let separator = bwrap_args
|
||||
.args
|
||||
.iter()
|
||||
.position(|arg| arg == "--")
|
||||
.unwrap_or_else(|| panic!("bubblewrap argv is missing command separator '--'"));
|
||||
let Some(parent) = mount.path.parent() else {
|
||||
panic!("resolver configuration path has no parent");
|
||||
};
|
||||
let fd = mount.file.as_raw_fd().to_string();
|
||||
bwrap_args.args.splice(
|
||||
separator..separator,
|
||||
LOADER_ENV_KEYS
|
||||
.iter()
|
||||
.flat_map(|key| ["--unsetenv".to_string(), (*key).to_string()])
|
||||
.chain([
|
||||
"--cap-drop".to_string(),
|
||||
"ALL".to_string(),
|
||||
"--cap-add".to_string(),
|
||||
"CAP_NET_BIND_SERVICE".to_string(),
|
||||
"--cap-add".to_string(),
|
||||
"CAP_SETPCAP".to_string(),
|
||||
"--dir".to_string(),
|
||||
parent.to_string_lossy().into_owned(),
|
||||
"--perms".to_string(),
|
||||
"444".to_string(),
|
||||
"--ro-bind-data".to_string(),
|
||||
fd,
|
||||
mount.path.to_string_lossy().into_owned(),
|
||||
]),
|
||||
);
|
||||
bwrap_args.preserved_files.push(mount.file);
|
||||
}
|
||||
|
||||
pub(crate) fn drop_and_verify_capabilities() -> io::Result<()> {
|
||||
let ignore_unknown = |result: rustix::io::Result<()>| match result {
|
||||
Ok(()) | Err(rustix::io::Errno::INVAL) => Ok(()),
|
||||
Err(err) => Err(io::Error::from_raw_os_error(err.raw_os_error())),
|
||||
};
|
||||
ignore_unknown(clear_ambient_capability_set())?;
|
||||
for bit in 0..u64::BITS {
|
||||
let capability = CapabilitySet::from_bits_retain(1_u64 << bit);
|
||||
match capability_is_in_ambient_set(capability) {
|
||||
Ok(false) | Err(rustix::io::Errno::INVAL) => {}
|
||||
Ok(true) => return Err(io::Error::other("capability remained in ambient set")),
|
||||
Err(err) => return Err(io::Error::from_raw_os_error(err.raw_os_error())),
|
||||
}
|
||||
ignore_unknown(remove_capability_from_bounding_set(capability))?;
|
||||
match capability_is_in_bounding_set(capability) {
|
||||
Ok(false) | Err(rustix::io::Errno::INVAL) => {}
|
||||
Ok(true) => return Err(io::Error::other("capability remained in bounding set")),
|
||||
Err(err) => return Err(io::Error::from_raw_os_error(err.raw_os_error())),
|
||||
}
|
||||
}
|
||||
let empty = CapabilitySets {
|
||||
effective: CapabilitySet::empty(),
|
||||
permitted: CapabilitySet::empty(),
|
||||
inheritable: CapabilitySet::empty(),
|
||||
};
|
||||
set_capabilities(None, empty).map_err(io::Error::from)?;
|
||||
if capabilities(None).map_err(io::Error::from)? != empty {
|
||||
return Err(io::Error::other("capabilities remained after DNS setup"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "dns_setup_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,76 @@
|
||||
use super::*;
|
||||
use crate::bwrap::BwrapArgs;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn unsets_loader_environment_before_inner_command() {
|
||||
let mut bwrap_args = BwrapArgs {
|
||||
args: vec![
|
||||
"bwrap".to_string(),
|
||||
"--".to_string(),
|
||||
"/bin/true".to_string(),
|
||||
],
|
||||
preserved_files: Vec::new(),
|
||||
synthetic_mount_targets: Vec::new(),
|
||||
protected_create_targets: Vec::new(),
|
||||
};
|
||||
append_bwrap_args(
|
||||
&mut bwrap_args,
|
||||
ResolvConfMount {
|
||||
file: tempfile::tempfile().expect("temporary resolver configuration"),
|
||||
path: PathBuf::from("/etc/resolv.conf"),
|
||||
},
|
||||
);
|
||||
|
||||
let separator = bwrap_args
|
||||
.args
|
||||
.iter()
|
||||
.position(|arg| arg == "--")
|
||||
.expect("command separator");
|
||||
let setup_args = &bwrap_args.args[..separator];
|
||||
for key in ["LD_AUDIT", "LD_LIBRARY_PATH", "LD_PRELOAD"] {
|
||||
assert!(
|
||||
setup_args
|
||||
.windows(2)
|
||||
.any(|args| args == ["--unsetenv", key]),
|
||||
"{key} must be unset before the privileged DNS setup command"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_read_denied_resolver_paths() {
|
||||
let logical_path = PathBuf::from("/etc/resolv.conf");
|
||||
let target_path = PathBuf::from("/run/systemd/resolve/stub-resolv.conf");
|
||||
for denied_path in [&logical_path, &target_path] {
|
||||
let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Read,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: AbsolutePathBuf::from_absolute_path(denied_path.clone())
|
||||
.expect("absolute resolver configuration"),
|
||||
},
|
||||
access: FileSystemAccessMode::Deny,
|
||||
},
|
||||
]);
|
||||
|
||||
let err = ensure_resolver_paths_allowed(
|
||||
&file_system_sandbox_policy,
|
||||
Path::new("/"),
|
||||
&logical_path,
|
||||
&target_path,
|
||||
)
|
||||
.expect_err("read-denied resolver configuration must be rejected");
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::PermissionDenied);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use std::ffi::CString;
|
||||
use std::fs::File;
|
||||
use std::os::raw::c_char;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
use std::sync::OnceLock;
|
||||
@@ -25,16 +26,19 @@ enum BubblewrapLauncher {
|
||||
struct SystemBwrapLauncher {
|
||||
program: AbsolutePathBuf,
|
||||
supports_argv0: bool,
|
||||
supports_cap_add: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct SystemBwrapCapabilities {
|
||||
supports_argv0: bool,
|
||||
supports_cap_add: bool,
|
||||
supports_perms: bool,
|
||||
is_setuid: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn exec_bwrap(argv: Vec<String>, preserved_files: Vec<File>) -> ! {
|
||||
match preferred_bwrap_launcher() {
|
||||
match preferred_bwrap_launcher_for_argv(&argv) {
|
||||
BubblewrapLauncher::System(launcher) => {
|
||||
exec_system_bwrap(&launcher.program, argv, preserved_files)
|
||||
}
|
||||
@@ -48,6 +52,13 @@ pub(crate) fn exec_bwrap(argv: Vec<String>, preserved_files: Vec<File>) -> ! {
|
||||
}
|
||||
}
|
||||
|
||||
fn preferred_bwrap_launcher_for_argv(argv: &[String]) -> BubblewrapLauncher {
|
||||
if argv.iter().any(|arg| arg == "--cap-add") {
|
||||
return preferred_bwrap_launcher_supporting_cap_add();
|
||||
}
|
||||
preferred_bwrap_launcher()
|
||||
}
|
||||
|
||||
fn preferred_bwrap_launcher() -> BubblewrapLauncher {
|
||||
static LAUNCHER: OnceLock<BubblewrapLauncher> = OnceLock::new();
|
||||
LAUNCHER
|
||||
@@ -66,6 +77,21 @@ fn preferred_bwrap_launcher() -> BubblewrapLauncher {
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn preferred_bwrap_launcher_supporting_cap_add() -> BubblewrapLauncher {
|
||||
match preferred_bwrap_launcher() {
|
||||
BubblewrapLauncher::System(launcher) if launcher.supports_cap_add => {
|
||||
BubblewrapLauncher::System(launcher)
|
||||
}
|
||||
BubblewrapLauncher::System(_) | BubblewrapLauncher::Unavailable => {
|
||||
match bundled_bwrap::launcher() {
|
||||
Some(launcher) => BubblewrapLauncher::Bundled(launcher),
|
||||
None => BubblewrapLauncher::Unavailable,
|
||||
}
|
||||
}
|
||||
bundled @ BubblewrapLauncher::Bundled(_) => bundled,
|
||||
}
|
||||
}
|
||||
|
||||
fn system_bwrap_launcher_for_path(system_bwrap_path: &Path) -> Option<SystemBwrapLauncher> {
|
||||
system_bwrap_launcher_for_path_with_probe(system_bwrap_path, system_bwrap_capabilities)
|
||||
}
|
||||
@@ -80,7 +106,9 @@ fn system_bwrap_launcher_for_path_with_probe(
|
||||
|
||||
let Some(SystemBwrapCapabilities {
|
||||
supports_argv0,
|
||||
supports_cap_add,
|
||||
supports_perms: true,
|
||||
is_setuid,
|
||||
}) = system_bwrap_capabilities(system_bwrap_path)
|
||||
else {
|
||||
return None;
|
||||
@@ -95,6 +123,7 @@ fn system_bwrap_launcher_for_path_with_probe(
|
||||
Some(SystemBwrapLauncher {
|
||||
program: system_bwrap_path,
|
||||
supports_argv0,
|
||||
supports_cap_add: supports_cap_add && !is_setuid,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -105,6 +134,14 @@ pub(crate) fn preferred_bwrap_supports_argv0() -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn preferred_bwrap_supports_cap_add() -> bool {
|
||||
match preferred_bwrap_launcher_supporting_cap_add() {
|
||||
BubblewrapLauncher::System(launcher) => launcher.supports_cap_add,
|
||||
BubblewrapLauncher::Bundled(_) => true,
|
||||
BubblewrapLauncher::Unavailable => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn system_bwrap_capabilities(system_bwrap_path: &Path) -> Option<SystemBwrapCapabilities> {
|
||||
// bubblewrap added `--argv0` in v0.9.0:
|
||||
// https://github.com/containers/bubblewrap/releases/tag/v0.9.0
|
||||
@@ -117,9 +154,12 @@ fn system_bwrap_capabilities(system_bwrap_path: &Path) -> Option<SystemBwrapCapa
|
||||
};
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let metadata = system_bwrap_path.metadata().ok()?;
|
||||
Some(SystemBwrapCapabilities {
|
||||
supports_argv0: stdout.contains("--argv0") || stderr.contains("--argv0"),
|
||||
supports_cap_add: stdout.contains("--cap-add") || stderr.contains("--cap-add"),
|
||||
supports_perms: stdout.contains("--perms") || stderr.contains("--perms"),
|
||||
is_setuid: metadata.permissions().mode() & libc::S_ISUID != 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -167,12 +207,15 @@ mod tests {
|
||||
system_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| {
|
||||
Some(SystemBwrapCapabilities {
|
||||
supports_argv0: true,
|
||||
supports_cap_add: false,
|
||||
supports_perms: true,
|
||||
is_setuid: false,
|
||||
})
|
||||
}),
|
||||
Some(SystemBwrapLauncher {
|
||||
program: expected,
|
||||
supports_argv0: true,
|
||||
supports_cap_add: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -186,12 +229,15 @@ mod tests {
|
||||
system_bwrap_launcher_for_path_with_probe(fake_bwrap_path, |_| {
|
||||
Some(SystemBwrapCapabilities {
|
||||
supports_argv0: false,
|
||||
supports_cap_add: false,
|
||||
supports_perms: true,
|
||||
is_setuid: false,
|
||||
})
|
||||
}),
|
||||
Some(SystemBwrapLauncher {
|
||||
program: AbsolutePathBuf::from_absolute_path(fake_bwrap_path).expect("absolute"),
|
||||
supports_argv0: false,
|
||||
supports_cap_add: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -204,13 +250,34 @@ mod tests {
|
||||
system_bwrap_launcher_for_path_with_probe(fake_bwrap.path(), |_| {
|
||||
Some(SystemBwrapCapabilities {
|
||||
supports_argv0: false,
|
||||
supports_cap_add: false,
|
||||
supports_perms: false,
|
||||
is_setuid: false,
|
||||
})
|
||||
}),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disables_system_cap_add_when_system_bwrap_is_setuid() {
|
||||
let fake_bwrap = NamedTempFile::new().expect("temp file");
|
||||
|
||||
assert_eq!(
|
||||
system_bwrap_launcher_for_path_with_probe(fake_bwrap.path(), |_| {
|
||||
Some(SystemBwrapCapabilities {
|
||||
supports_argv0: true,
|
||||
supports_cap_add: true,
|
||||
supports_perms: true,
|
||||
is_setuid: true,
|
||||
})
|
||||
})
|
||||
.expect("system launcher")
|
||||
.supports_cap_add,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_system_bwrap_when_system_bwrap_is_missing() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -10,6 +10,10 @@ mod bundled_bwrap;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod bwrap;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod dns_routing;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod dns_setup;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod exec_util;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod landlock;
|
||||
|
||||
@@ -20,11 +20,23 @@ use std::time::Duration;
|
||||
use crate::bwrap::BwrapNetworkMode;
|
||||
use crate::bwrap::BwrapOptions;
|
||||
use crate::bwrap::create_bwrap_command_args;
|
||||
use crate::dns_routing::bind_netns_dns_stub;
|
||||
use crate::dns_routing::spawn_netns_dns_stub;
|
||||
use crate::dns_setup::LoaderEnvironment;
|
||||
use crate::dns_setup::ResolvConfMount;
|
||||
use crate::dns_setup::append_bwrap_args as append_dns_bwrap_args;
|
||||
use crate::dns_setup::capture_loader_environment;
|
||||
use crate::dns_setup::create_resolv_conf_file;
|
||||
use crate::dns_setup::drop_and_verify_capabilities;
|
||||
use crate::dns_setup::resolv_conf_mount_path;
|
||||
use crate::dns_setup::restore_loader_environment;
|
||||
use crate::landlock::apply_permission_profile_to_current_thread;
|
||||
use crate::launcher::exec_bwrap;
|
||||
use crate::launcher::preferred_bwrap_supports_argv0;
|
||||
use crate::launcher::preferred_bwrap_supports_cap_add;
|
||||
use crate::proxy_routing::activate_proxy_routes_in_netns;
|
||||
use crate::proxy_routing::prepare_host_proxy_route_spec;
|
||||
use codex_network_proxy::DNS_PROXY_ENV_KEY;
|
||||
use codex_protocol::error::Result as CodexResult;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::FileSystemSandboxPolicy;
|
||||
@@ -126,6 +138,10 @@ pub struct LandlockCommand {
|
||||
#[arg(long = "proxy-route-spec", hide = true)]
|
||||
pub proxy_route_spec: Option<String>,
|
||||
|
||||
/// Internal loader environment restored after temporary DNS setup capabilities are dropped.
|
||||
#[arg(long = "loader-environment", hide = true)]
|
||||
pub loader_environment: Option<String>,
|
||||
|
||||
/// When set, skip mounting a fresh `/proc` even though PID isolation is
|
||||
/// still enabled. This is primarily intended for restrictive container
|
||||
/// environments that deny `--proc /proc`.
|
||||
@@ -153,6 +169,7 @@ pub fn run_main() -> ! {
|
||||
apply_seccomp_then_exec,
|
||||
allow_network_for_proxy,
|
||||
proxy_route_spec,
|
||||
loader_environment,
|
||||
no_proc,
|
||||
command,
|
||||
} = LandlockCommand::parse();
|
||||
@@ -177,12 +194,35 @@ pub fn run_main() -> ! {
|
||||
// established the filesystem view.
|
||||
if apply_seccomp_then_exec {
|
||||
if allow_network_for_proxy {
|
||||
let bound_dns = if std::env::var_os(DNS_PROXY_ENV_KEY).is_some() {
|
||||
Some(
|
||||
bind_netns_dns_stub()
|
||||
.unwrap_or_else(|err| panic!("error binding Linux DNS bridge: {err}")),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let loader_environment = parse_loader_environment(loader_environment.as_deref())
|
||||
.unwrap_or_else(|err| panic!("invalid loader environment: {err}"));
|
||||
if bound_dns.is_some() {
|
||||
drop_and_verify_capabilities().unwrap_or_else(|err| {
|
||||
panic!("error dropping Linux DNS setup capabilities: {err}")
|
||||
});
|
||||
if let Some(loader_environment) = loader_environment {
|
||||
restore_loader_environment(loader_environment);
|
||||
}
|
||||
}
|
||||
let spec = proxy_route_spec
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| panic!("managed proxy mode requires --proxy-route-spec"));
|
||||
if let Err(err) = activate_proxy_routes_in_netns(spec) {
|
||||
panic!("error activating Linux proxy routing bridge: {err}");
|
||||
}
|
||||
if let Some(bound_dns) = bound_dns
|
||||
&& let Err(err) = spawn_netns_dns_stub(bound_dns)
|
||||
{
|
||||
panic!("error activating Linux DNS bridge: {err}");
|
||||
}
|
||||
}
|
||||
let proxy_routing_active = allow_network_for_proxy;
|
||||
if let Err(e) = apply_permission_profile_to_current_thread(
|
||||
@@ -214,6 +254,11 @@ pub fn run_main() -> ! {
|
||||
// Outer stage: bubblewrap first, then re-enter this binary in the
|
||||
// sandboxed environment to apply seccomp. This path never falls back
|
||||
// to legacy Landlock on failure.
|
||||
let dns_setup = prepare_dns_setup(
|
||||
allow_network_for_proxy,
|
||||
&file_system_sandbox_policy,
|
||||
&sandbox_policy_cwd,
|
||||
);
|
||||
let proxy_route_spec =
|
||||
if allow_network_for_proxy {
|
||||
Some(prepare_host_proxy_route_spec().unwrap_or_else(|err| {
|
||||
@@ -228,6 +273,9 @@ pub fn run_main() -> ! {
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy,
|
||||
proxy_route_spec,
|
||||
loader_environment: dns_setup
|
||||
.as_ref()
|
||||
.map(|setup| serialize_loader_environment(&setup.loader_environment)),
|
||||
command,
|
||||
});
|
||||
run_bwrap_with_proc_fallback(
|
||||
@@ -238,6 +286,7 @@ pub fn run_main() -> ! {
|
||||
inner,
|
||||
!no_proc,
|
||||
allow_network_for_proxy,
|
||||
dns_setup.map(|setup| setup.resolv_conf),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -254,6 +303,11 @@ pub fn run_main() -> ! {
|
||||
exec_or_panic(command);
|
||||
}
|
||||
|
||||
struct DnsSetup {
|
||||
resolv_conf: ResolvConfMount,
|
||||
loader_environment: LoaderEnvironment,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct EffectivePermissions {
|
||||
permission_profile: PermissionProfile,
|
||||
@@ -278,6 +332,45 @@ fn parse_permission_profile(value: &str) -> std::result::Result<PermissionProfil
|
||||
serde_json::from_str(value).map_err(|err| format!("invalid permission profile JSON: {err}"))
|
||||
}
|
||||
|
||||
fn parse_loader_environment(
|
||||
value: Option<&str>,
|
||||
) -> std::result::Result<Option<LoaderEnvironment>, String> {
|
||||
value
|
||||
.map(serde_json::from_str)
|
||||
.transpose()
|
||||
.map_err(|err| format!("invalid loader environment JSON: {err}"))
|
||||
}
|
||||
|
||||
fn serialize_loader_environment(environment: &LoaderEnvironment) -> String {
|
||||
serde_json::to_string(environment)
|
||||
.unwrap_or_else(|err| panic!("failed to serialize loader environment: {err}"))
|
||||
}
|
||||
|
||||
fn prepare_dns_setup(
|
||||
allow_network_for_proxy: bool,
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
sandbox_policy_cwd: &Path,
|
||||
) -> Option<DnsSetup> {
|
||||
if !allow_network_for_proxy || std::env::var_os(DNS_PROXY_ENV_KEY).is_none() {
|
||||
return None;
|
||||
}
|
||||
if !preferred_bwrap_supports_cap_add() {
|
||||
// SAFETY: the sandbox helper is single-threaded here, before it forks bridge workers or
|
||||
// executes the user command.
|
||||
unsafe { std::env::remove_var(DNS_PROXY_ENV_KEY) };
|
||||
return None;
|
||||
}
|
||||
Some(DnsSetup {
|
||||
resolv_conf: ResolvConfMount {
|
||||
file: create_resolv_conf_file()
|
||||
.unwrap_or_else(|err| panic!("failed to create resolver configuration: {err}")),
|
||||
path: resolv_conf_mount_path(file_system_sandbox_policy, sandbox_policy_cwd)
|
||||
.unwrap_or_else(|err| panic!("failed to prepare resolver configuration: {err}")),
|
||||
},
|
||||
loader_environment: capture_loader_environment(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_permission_profile(
|
||||
permission_profile: Option<PermissionProfile>,
|
||||
) -> Result<EffectivePermissions, ResolvePermissionProfileError> {
|
||||
@@ -322,6 +415,7 @@ fn run_bwrap_with_proc_fallback(
|
||||
inner: Vec<String>,
|
||||
mount_proc: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
dns_resolv_conf: Option<ResolvConfMount>,
|
||||
) -> ! {
|
||||
let network_mode = bwrap_network_mode(network_sandbox_policy, allow_network_for_proxy);
|
||||
let mut mount_proc = mount_proc;
|
||||
@@ -354,6 +448,9 @@ fn run_bwrap_with_proc_fallback(
|
||||
options,
|
||||
)
|
||||
.unwrap_or_else(|err| exit_with_bwrap_build_error(err));
|
||||
if let Some(dns_resolv_conf) = dns_resolv_conf {
|
||||
append_dns_bwrap_args(&mut bwrap_args, dns_resolv_conf);
|
||||
}
|
||||
apply_inner_command_argv0(&mut bwrap_args.args);
|
||||
run_or_exec_bwrap(bwrap_args);
|
||||
}
|
||||
@@ -1394,6 +1491,7 @@ struct InnerSeccompCommandArgs<'a> {
|
||||
permission_profile: &'a PermissionProfile,
|
||||
allow_network_for_proxy: bool,
|
||||
proxy_route_spec: Option<String>,
|
||||
loader_environment: Option<String>,
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -1405,6 +1503,7 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>
|
||||
permission_profile,
|
||||
allow_network_for_proxy,
|
||||
proxy_route_spec,
|
||||
loader_environment,
|
||||
command,
|
||||
} = args;
|
||||
let current_exe = match std::env::current_exe() {
|
||||
@@ -1436,6 +1535,10 @@ fn build_inner_seccomp_command(args: InnerSeccompCommandArgs<'_>) -> Vec<String>
|
||||
.unwrap_or_else(|| panic!("managed proxy mode requires a proxy route spec"));
|
||||
inner.push("--proxy-route-spec".to_string());
|
||||
inner.push(proxy_route_spec);
|
||||
if let Some(loader_environment) = loader_environment {
|
||||
inner.push("--loader-environment".to_string());
|
||||
inner.push(loader_environment);
|
||||
}
|
||||
}
|
||||
inner.push("--".to_string());
|
||||
inner.extend(command);
|
||||
|
||||
@@ -468,6 +468,7 @@ fn managed_proxy_inner_command_includes_route_spec() {
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy: true,
|
||||
proxy_route_spec: Some("{\"routes\":[]}".to_string()),
|
||||
loader_environment: None,
|
||||
command: vec!["/bin/true".to_string()],
|
||||
});
|
||||
|
||||
@@ -484,6 +485,7 @@ fn inner_command_includes_permission_profile_flag() {
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy: false,
|
||||
proxy_route_spec: None,
|
||||
loader_environment: None,
|
||||
command: vec!["/bin/true".to_string()],
|
||||
});
|
||||
|
||||
@@ -503,6 +505,7 @@ fn non_managed_inner_command_omits_route_spec() {
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy: false,
|
||||
proxy_route_spec: None,
|
||||
loader_environment: None,
|
||||
command: vec!["/bin/true".to_string()],
|
||||
});
|
||||
|
||||
@@ -519,6 +522,7 @@ fn managed_proxy_inner_command_requires_route_spec() {
|
||||
permission_profile: &permission_profile,
|
||||
allow_network_for_proxy: true,
|
||||
proxy_route_spec: None,
|
||||
loader_environment: None,
|
||||
command: vec!["/bin/true".to_string()],
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use codex_network_proxy::DNS_PROXY_ENV_KEY;
|
||||
use codex_network_proxy::PROXY_ATTRIBUTION_TOKEN_ENV_KEY;
|
||||
use codex_network_proxy::write_attribution_frame;
|
||||
use serde::Deserialize;
|
||||
@@ -41,6 +42,7 @@ const PROXY_ENV_KEYS: &[&str] = &[
|
||||
"PIP_PROXY",
|
||||
"DOCKER_HTTP_PROXY",
|
||||
"DOCKER_HTTPS_PROXY",
|
||||
DNS_PROXY_ENV_KEY,
|
||||
];
|
||||
|
||||
const PROXY_SOCKET_DIR_PREFIX: &str = "codex-linux-sandbox-proxy-";
|
||||
@@ -672,7 +674,7 @@ fn set_parent_death_signal() -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
fn harden_bridge_process() -> io::Result<()> {
|
||||
pub(crate) fn harden_bridge_process() -> io::Result<()> {
|
||||
set_parent_death_signal()?;
|
||||
codex_process_hardening::disable_process_dumping()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use codex_core::exec_env::create_env;
|
||||
use codex_network_proxy::DNS_PROXY_ENV_KEY;
|
||||
use codex_network_proxy::DNS_PROXY_SESSION_PREFACE;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -40,6 +42,7 @@ const PROXY_ENV_KEYS: &[&str] = &[
|
||||
"PIP_PROXY",
|
||||
"DOCKER_HTTP_PROXY",
|
||||
"DOCKER_HTTPS_PROXY",
|
||||
DNS_PROXY_ENV_KEY,
|
||||
];
|
||||
|
||||
fn create_env_from_core_vars() -> HashMap<String, String> {
|
||||
@@ -301,3 +304,142 @@ async fn managed_proxy_mode_denies_af_unix_socket_but_allows_socketpair() {
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn managed_proxy_mode_routes_native_dns_through_bridge() {
|
||||
if let Some(skip_reason) = managed_proxy_skip_reason().await {
|
||||
eprintln!("skipping managed proxy test: {skip_reason}");
|
||||
return;
|
||||
}
|
||||
|
||||
if !command_available("python3").await {
|
||||
eprintln!("skipping managed proxy DNS test: python3 is unavailable");
|
||||
return;
|
||||
}
|
||||
|
||||
let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).expect("bind DNS proxy listener");
|
||||
let proxy_port = listener.local_addr().expect("DNS proxy local addr").port();
|
||||
let (query_tx, query_rx) = std::sync::mpsc::channel();
|
||||
std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("accept DNS proxy connection");
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(3)))
|
||||
.expect("set DNS proxy read timeout");
|
||||
let mut preface = [0_u8; DNS_PROXY_SESSION_PREFACE.len()];
|
||||
stream.read_exact(&mut preface).expect("read DNS preface");
|
||||
assert_eq!(&preface, DNS_PROXY_SESSION_PREFACE);
|
||||
stream
|
||||
.write_all(DNS_PROXY_SESSION_PREFACE)
|
||||
.expect("write DNS preface ack");
|
||||
let query = read_frame(&mut stream).expect("read DNS query");
|
||||
let response = dns_a_response(&query, [203, 0, 113, 9]);
|
||||
query_tx.send(query).expect("send DNS query");
|
||||
write_frame(&mut stream, &response).expect("write DNS response");
|
||||
});
|
||||
|
||||
let mut env = create_env_from_core_vars();
|
||||
strip_proxy_env(&mut env);
|
||||
env.insert(
|
||||
DNS_PROXY_ENV_KEY.to_string(),
|
||||
format!("tcp://127.0.0.1:{proxy_port}"),
|
||||
);
|
||||
|
||||
let output = run_linux_sandbox_direct(
|
||||
&[
|
||||
"python3",
|
||||
"-c",
|
||||
"import os,socket,sys\nif 'CODEX_NETWORK_PROXY_DNS' in os.environ:\n sys.exit(3)\ninfo = socket.getaddrinfo('fixture.test', 80, socket.AF_INET, socket.SOCK_STREAM)\nprint(info[0][4][0])\n",
|
||||
],
|
||||
&PermissionProfile::Disabled,
|
||||
/*allow_network_for_proxy*/ true,
|
||||
env,
|
||||
NETWORK_TIMEOUT_MS,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
output.status.success(),
|
||||
true,
|
||||
"expected native DNS to resolve through bridge; status={:?}; stdout={}; stderr={}",
|
||||
output.status.code(),
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert_eq!(
|
||||
String::from_utf8_lossy(&output.stdout).trim(),
|
||||
"203.0.113.9"
|
||||
);
|
||||
let query = query_rx
|
||||
.recv_timeout(Duration::from_secs(3))
|
||||
.expect("expected DNS query through proxy bridge");
|
||||
assert!(
|
||||
dns_query_name(&query).is_some_and(|name| name == "fixture.test"),
|
||||
"expected fixture.test DNS query, got wire bytes: {query:?}"
|
||||
);
|
||||
}
|
||||
|
||||
fn dns_query_name(query: &[u8]) -> Option<String> {
|
||||
let mut offset = 12usize;
|
||||
let mut labels = Vec::new();
|
||||
loop {
|
||||
let len = *query.get(offset)? as usize;
|
||||
offset += 1;
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
let label = query.get(offset..offset.checked_add(len)?)?;
|
||||
labels.push(std::str::from_utf8(label).ok()?);
|
||||
offset += len;
|
||||
}
|
||||
Some(labels.join("."))
|
||||
}
|
||||
|
||||
fn dns_a_response(query: &[u8], address: [u8; 4]) -> Vec<u8> {
|
||||
let question_end = dns_question_end(query).expect("DNS question end");
|
||||
let mut response = Vec::new();
|
||||
response.extend_from_slice(&query[..2]);
|
||||
response.extend_from_slice(&[0x81, 0x80]);
|
||||
response.extend_from_slice(&[0x00, 0x01]);
|
||||
response.extend_from_slice(&[0x00, 0x01]);
|
||||
response.extend_from_slice(&[0x00, 0x00]);
|
||||
response.extend_from_slice(&[0x00, 0x00]);
|
||||
response.extend_from_slice(&query[12..question_end]);
|
||||
response.extend_from_slice(&[0xc0, 0x0c]);
|
||||
response.extend_from_slice(&[0x00, 0x01]);
|
||||
response.extend_from_slice(&[0x00, 0x01]);
|
||||
response.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
|
||||
response.extend_from_slice(&[0x00, 0x04]);
|
||||
response.extend_from_slice(&address);
|
||||
response
|
||||
}
|
||||
|
||||
fn dns_question_end(query: &[u8]) -> Option<usize> {
|
||||
let mut offset = 12usize;
|
||||
loop {
|
||||
let len = *query.get(offset)? as usize;
|
||||
offset += 1;
|
||||
if len == 0 {
|
||||
return offset.checked_add(4).filter(|end| *end <= query.len());
|
||||
}
|
||||
offset = offset.checked_add(len)?;
|
||||
}
|
||||
}
|
||||
|
||||
fn write_frame(writer: &mut impl Write, payload: &[u8]) -> std::io::Result<()> {
|
||||
let len = u16::try_from(payload.len()).map_err(|_| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"DNS message exceeds maximum wire length",
|
||||
)
|
||||
})?;
|
||||
writer.write_all(&len.to_be_bytes())?;
|
||||
writer.write_all(payload)
|
||||
}
|
||||
|
||||
fn read_frame(reader: &mut impl Read) -> std::io::Result<Vec<u8>> {
|
||||
let mut len = [0; 2];
|
||||
reader.read_exact(&mut len)?;
|
||||
let mut payload = vec![0; usize::from(u16::from_be_bytes(len))];
|
||||
reader.read_exact(&mut payload)?;
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user