test(sandbox): 验证原生 TCP 与 UDP 回环隔离
This commit is contained in:
@@ -304,6 +304,20 @@ mod tests {
|
|||||||
// Only tests use cmd.exe, with fixed commands and controlled temporary paths.
|
// Only tests use cmd.exe, with fixed commands and controlled temporary paths.
|
||||||
// A production extension launcher must use a verified entry, never a shell.
|
// A production extension launcher must use a verified entry, never a shell.
|
||||||
fn checked_process(profile: &Profile, command: Option<&str>) -> Option<u32> {
|
fn checked_process(profile: &Profile, command: Option<&str>) -> Option<u32> {
|
||||||
|
let executable = std::path::PathBuf::from(std::env::var_os("SystemRoot").unwrap())
|
||||||
|
.join("System32/cmd.exe");
|
||||||
|
checked_executable(
|
||||||
|
profile,
|
||||||
|
&executable,
|
||||||
|
command.map(|c| format!("cmd.exe /d /c {c}")),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn checked_executable(
|
||||||
|
profile: &Profile,
|
||||||
|
executable: &std::path::Path,
|
||||||
|
command: Option<String>,
|
||||||
|
) -> Option<u32> {
|
||||||
let mut attributes = Attributes::new();
|
let mut attributes = Attributes::new();
|
||||||
let caps = SECURITY_CAPABILITIES {
|
let caps = SECURITY_CAPABILITIES {
|
||||||
AppContainerSid: profile.sid(),
|
AppContainerSid: profile.sid(),
|
||||||
@@ -328,8 +342,6 @@ mod tests {
|
|||||||
let mut startup = STARTUPINFOEXW::default();
|
let mut startup = STARTUPINFOEXW::default();
|
||||||
startup.StartupInfo.cb = size_of::<STARTUPINFOEXW>() as u32;
|
startup.StartupInfo.cb = size_of::<STARTUPINFOEXW>() as u32;
|
||||||
startup.lpAttributeList = attributes.buffer.as_mut_ptr().cast();
|
startup.lpAttributeList = attributes.buffer.as_mut_ptr().cast();
|
||||||
let executable = std::path::PathBuf::from(std::env::var_os("SystemRoot").unwrap())
|
|
||||||
.join("System32/cmd.exe");
|
|
||||||
let executable: Vec<u16> = executable
|
let executable: Vec<u16> = executable
|
||||||
.as_os_str()
|
.as_os_str()
|
||||||
.encode_wide()
|
.encode_wide()
|
||||||
@@ -346,12 +358,8 @@ mod tests {
|
|||||||
.encode_utf16()
|
.encode_utf16()
|
||||||
.collect();
|
.collect();
|
||||||
let mut command_line: Vec<u16> = command
|
let mut command_line: Vec<u16> = command
|
||||||
.map(|command| {
|
.as_ref()
|
||||||
format!("cmd.exe /d /c {command}")
|
.map(|command| command.encode_utf16().chain(Some(0)).collect())
|
||||||
.encode_utf16()
|
|
||||||
.chain(Some(0))
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let mut info = PROCESS_INFORMATION::default();
|
let mut info = PROCESS_INFORMATION::default();
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
@@ -581,4 +589,105 @@ mod tests {
|
|||||||
drop(handle);
|
drop(handle);
|
||||||
profile.remove().unwrap();
|
profile.remove().unwrap();
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn real_container_cannot_reach_ipv4_or_ipv6_loopback_listeners() {
|
||||||
|
use std::{
|
||||||
|
net::{TcpListener, UdpSocket},
|
||||||
|
os::windows::fs::OpenOptionsExt,
|
||||||
|
};
|
||||||
|
use windows_sys::Win32::Storage::FileSystem::*;
|
||||||
|
let profile = Profile::create().unwrap();
|
||||||
|
let package = tempfile::tempdir().unwrap();
|
||||||
|
let executable = package.path().join("network-probe.exe");
|
||||||
|
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("tests/fixtures/sandbox_network_probe.rs");
|
||||||
|
let compile = std::process::Command::new("rustc")
|
||||||
|
.arg("--edition=2021")
|
||||||
|
.arg(&fixture)
|
||||||
|
.arg("-o")
|
||||||
|
.arg(&executable)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
compile.status.success(),
|
||||||
|
"{}",
|
||||||
|
String::from_utf8_lossy(&compile.stderr)
|
||||||
|
);
|
||||||
|
let open = |path: &std::path::Path| {
|
||||||
|
std::fs::OpenOptions::new()
|
||||||
|
.access_mode(READ_CONTROL | WRITE_DAC)
|
||||||
|
.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
|
||||||
|
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
|
||||||
|
.open(path)
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
let root = open(package.path());
|
||||||
|
let entry = open(&executable);
|
||||||
|
profile.grant_package_read_execute(&root).unwrap();
|
||||||
|
profile.grant_package_read_execute(&entry).unwrap();
|
||||||
|
for ip in ["127.0.0.1:0", "[::1]:0"] {
|
||||||
|
let tcp = TcpListener::bind(ip).unwrap();
|
||||||
|
let udp = UdpSocket::bind(ip).unwrap();
|
||||||
|
for (mode, address) in [
|
||||||
|
("tcp", tcp.local_addr().unwrap()),
|
||||||
|
("udp", udp.local_addr().unwrap()),
|
||||||
|
] {
|
||||||
|
let address = address.to_string();
|
||||||
|
// The exact executable and target work outside containment.
|
||||||
|
assert!(std::process::Command::new(&executable)
|
||||||
|
.args([mode, &address])
|
||||||
|
.status()
|
||||||
|
.unwrap()
|
||||||
|
.success());
|
||||||
|
if mode == "tcp" {
|
||||||
|
tcp.set_nonblocking(true).unwrap();
|
||||||
|
tcp.accept().unwrap();
|
||||||
|
} else {
|
||||||
|
udp.set_read_timeout(Some(std::time::Duration::from_secs(2)))
|
||||||
|
.unwrap();
|
||||||
|
let mut bytes = [0u8; 8];
|
||||||
|
assert_eq!(udp.recv(&mut bytes).unwrap(), 5);
|
||||||
|
assert_eq!(&bytes[..5], b"probe");
|
||||||
|
udp.set_nonblocking(true).unwrap();
|
||||||
|
}
|
||||||
|
let command = format!("\"{}\" {mode} {address}", executable.display());
|
||||||
|
// Loopback isolation can silently drop packets. TCP must
|
||||||
|
// explicitly report denial or timeout; UDP send may succeed,
|
||||||
|
// but no datagram may reach the controlled listener below.
|
||||||
|
let exit = checked_executable(&profile, &executable, Some(command));
|
||||||
|
eprintln!("container network probe {mode} {address}: {exit:?}");
|
||||||
|
if mode == "tcp" {
|
||||||
|
assert!(matches!(exit, Some(77 | 80)), "{mode} {address}: {exit:?}");
|
||||||
|
} else {
|
||||||
|
assert!(matches!(exit, Some(0 | 77)), "{mode} {address}: {exit:?}");
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||||
|
}
|
||||||
|
if mode == "tcp" {
|
||||||
|
assert_eq!(
|
||||||
|
tcp.accept().unwrap_err().kind(),
|
||||||
|
std::io::ErrorKind::WouldBlock
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
assert_eq!(
|
||||||
|
udp.recv(&mut [0u8; 8]).unwrap_err().kind(),
|
||||||
|
std::io::ErrorKind::WouldBlock
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(std::process::Command::new(&executable)
|
||||||
|
.args([mode, &address])
|
||||||
|
.status()
|
||||||
|
.unwrap()
|
||||||
|
.success());
|
||||||
|
if mode == "tcp" {
|
||||||
|
tcp.accept().unwrap();
|
||||||
|
} else {
|
||||||
|
udp.set_nonblocking(false).unwrap();
|
||||||
|
assert_eq!(udp.recv(&mut [0u8; 8]).unwrap(), 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(entry);
|
||||||
|
drop(root);
|
||||||
|
profile.remove().unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
//! Standalone native test probe; never shipped or used to launch extensions.
|
||||||
|
use std::net::{SocketAddr, TcpStream, UdpSocket};
|
||||||
|
use std::time::Duration;
|
||||||
|
fn main() {
|
||||||
|
let args: Vec<_> = std::env::args().collect();
|
||||||
|
if args.len() != 3 {
|
||||||
|
std::process::exit(79);
|
||||||
|
}
|
||||||
|
let address: SocketAddr = args[2].parse().unwrap();
|
||||||
|
let result = match args[1].as_str() {
|
||||||
|
"tcp" => TcpStream::connect_timeout(&address, Duration::from_secs(2)).map(|_| ()),
|
||||||
|
"udp" => UdpSocket::bind(if address.is_ipv4() {
|
||||||
|
"0.0.0.0:0"
|
||||||
|
} else {
|
||||||
|
"[::]:0"
|
||||||
|
})
|
||||||
|
.and_then(|socket| socket.send_to(b"probe", address))
|
||||||
|
.map(|_| ()),
|
||||||
|
_ => std::process::exit(79),
|
||||||
|
};
|
||||||
|
std::process::exit(match result {
|
||||||
|
Ok(()) => 0,
|
||||||
|
Err(error) if error.raw_os_error() == Some(10013) => 77,
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::TimedOut => 80,
|
||||||
|
Err(_) => 81,
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user