301 lines
10 KiB
Rust
301 lines
10 KiB
Rust
//! Integration tests for network operations in sandbox
|
|
use crate::sandbox::common::*;
|
|
use crate::skip_if_unsupported;
|
|
use claudia_lib::sandbox::executor::SandboxExecutor;
|
|
use gaol::profile::{Profile, Operation, AddressPattern};
|
|
use serial_test::serial;
|
|
use std::net::TcpListener;
|
|
use tempfile::TempDir;
|
|
|
|
/// Get an available port for testing
|
|
fn get_available_port() -> u16 {
|
|
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to 0");
|
|
let port = listener.local_addr().expect("Failed to get local addr").port();
|
|
drop(listener); // Release the port
|
|
port
|
|
}
|
|
|
|
/// Test allowed network operations
|
|
#[test]
|
|
#[serial]
|
|
fn test_allowed_network_all() {
|
|
skip_if_unsupported!();
|
|
|
|
let platform = PlatformConfig::current();
|
|
if !platform.supports_network_all {
|
|
eprintln!("Skipping test: network all not supported on this platform");
|
|
return;
|
|
}
|
|
|
|
// Create test project
|
|
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
|
|
|
// Create profile allowing all network access
|
|
let operations = vec![
|
|
Operation::NetworkOutbound(AddressPattern::All),
|
|
];
|
|
|
|
let profile = match Profile::new(operations) {
|
|
Ok(p) => p,
|
|
Err(_) => {
|
|
eprintln!("Failed to create profile - operation not supported");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Create test binary that connects to localhost
|
|
let port = get_available_port();
|
|
let test_code = test_code::network_connect(&format!("127.0.0.1:{}", port));
|
|
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
|
let binary_path = create_test_binary("test_network", &test_code, binary_dir.path())
|
|
.expect("Failed to create test binary");
|
|
|
|
// Start a listener on the port
|
|
let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
|
|
.expect("Failed to bind listener");
|
|
|
|
// Execute in sandbox
|
|
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
|
match executor.execute_sandboxed_spawn(
|
|
&binary_path.to_string_lossy(),
|
|
&[],
|
|
&test_fs.project_path,
|
|
) {
|
|
Ok(mut child) => {
|
|
// Accept connection in a thread
|
|
std::thread::spawn(move || {
|
|
let _ = listener.accept();
|
|
});
|
|
|
|
let status = child.wait().expect("Failed to wait for child");
|
|
assert!(status.success(), "Network connection should succeed when allowed");
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Test forbidden network operations
|
|
#[test]
|
|
#[serial]
|
|
fn test_forbidden_network() {
|
|
skip_if_unsupported!();
|
|
|
|
// Create test project
|
|
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
|
|
|
// Create profile without network permissions
|
|
let operations = vec![
|
|
Operation::FileReadAll(gaol::profile::PathPattern::Subpath(test_fs.project_path.clone())),
|
|
];
|
|
|
|
let profile = match Profile::new(operations) {
|
|
Ok(p) => p,
|
|
Err(_) => {
|
|
eprintln!("Failed to create profile - operation not supported");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Create test binary that tries to connect
|
|
let test_code = test_code::network_connect("google.com:80");
|
|
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
|
let binary_path = create_test_binary("test_no_network", &test_code, binary_dir.path())
|
|
.expect("Failed to create test binary");
|
|
|
|
// Execute in sandbox
|
|
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
|
match executor.execute_sandboxed_spawn(
|
|
&binary_path.to_string_lossy(),
|
|
&[],
|
|
&test_fs.project_path,
|
|
) {
|
|
Ok(mut child) => {
|
|
let status = child.wait().expect("Failed to wait for child");
|
|
// Network restrictions might not work on all platforms
|
|
if status.success() {
|
|
eprintln!("WARNING: Network connection was not blocked (platform limitation)");
|
|
if std::env::consts::OS == "linux" {
|
|
panic!("Network should be blocked on Linux when not allowed");
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Test TCP port-specific network rules (macOS only)
|
|
#[test]
|
|
#[serial]
|
|
#[cfg(target_os = "macos")]
|
|
fn test_network_tcp_port_specific() {
|
|
let platform = PlatformConfig::current();
|
|
if !platform.supports_network_tcp {
|
|
eprintln!("Skipping test: TCP port filtering not supported");
|
|
return;
|
|
}
|
|
|
|
// Create test project
|
|
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
|
|
|
// Get two ports - one allowed, one forbidden
|
|
let allowed_port = get_available_port();
|
|
let forbidden_port = get_available_port();
|
|
|
|
// Create profile allowing only specific port
|
|
let operations = vec![
|
|
Operation::NetworkOutbound(AddressPattern::Tcp(allowed_port)),
|
|
];
|
|
|
|
let profile = match Profile::new(operations) {
|
|
Ok(p) => p,
|
|
Err(_) => {
|
|
eprintln!("Failed to create profile - operation not supported");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Test 1: Allowed port
|
|
{
|
|
let test_code = test_code::network_connect(&format!("127.0.0.1:{}", allowed_port));
|
|
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
|
let binary_path = create_test_binary("test_allowed_port", &test_code, binary_dir.path())
|
|
.expect("Failed to create test binary");
|
|
|
|
let listener = TcpListener::bind(format!("127.0.0.1:{}", allowed_port))
|
|
.expect("Failed to bind listener");
|
|
|
|
let executor = SandboxExecutor::new(profile.clone(), test_fs.project_path.clone());
|
|
match executor.execute_sandboxed_spawn(
|
|
&binary_path.to_string_lossy(),
|
|
&[],
|
|
&test_fs.project_path,
|
|
) {
|
|
Ok(mut child) => {
|
|
std::thread::spawn(move || {
|
|
let _ = listener.accept();
|
|
});
|
|
|
|
let status = child.wait().expect("Failed to wait for child");
|
|
assert!(status.success(), "Connection to allowed port should succeed");
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test 2: Forbidden port
|
|
{
|
|
let test_code = test_code::network_connect(&format!("127.0.0.1:{}", forbidden_port));
|
|
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
|
let binary_path = create_test_binary("test_forbidden_port", &test_code, binary_dir.path())
|
|
.expect("Failed to create test binary");
|
|
|
|
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
|
match executor.execute_sandboxed_spawn(
|
|
&binary_path.to_string_lossy(),
|
|
&[],
|
|
&test_fs.project_path,
|
|
) {
|
|
Ok(mut child) => {
|
|
let status = child.wait().expect("Failed to wait for child");
|
|
assert!(!status.success(), "Connection to forbidden port should fail");
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Test local socket connections (Unix domain sockets)
|
|
#[test]
|
|
#[serial]
|
|
#[cfg(unix)]
|
|
fn test_local_socket_connections() {
|
|
skip_if_unsupported!();
|
|
|
|
let platform = PlatformConfig::current();
|
|
|
|
// Create test project
|
|
let test_fs = TestFileSystem::new().expect("Failed to create test filesystem");
|
|
let socket_path = test_fs.project_path.join("test.sock");
|
|
|
|
// Create appropriate profile based on platform
|
|
let operations = if platform.supports_network_local {
|
|
vec![
|
|
Operation::NetworkOutbound(AddressPattern::LocalSocket(socket_path.clone())),
|
|
]
|
|
} else if platform.supports_network_all {
|
|
// Fallback to allowing all network
|
|
vec![
|
|
Operation::NetworkOutbound(AddressPattern::All),
|
|
]
|
|
} else {
|
|
eprintln!("Skipping test: no network support on this platform");
|
|
return;
|
|
};
|
|
|
|
let profile = match Profile::new(operations) {
|
|
Ok(p) => p,
|
|
Err(_) => {
|
|
eprintln!("Failed to create profile - operation not supported");
|
|
return;
|
|
}
|
|
};
|
|
|
|
// Create test binary that connects to local socket
|
|
let test_code = format!(
|
|
r#"
|
|
use std::os::unix::net::UnixStream;
|
|
|
|
fn main() {{
|
|
match UnixStream::connect("{}") {{
|
|
Ok(_) => {{
|
|
println!("SUCCESS: Connected to local socket");
|
|
}}
|
|
Err(e) => {{
|
|
eprintln!("FAILURE: {{}}", e);
|
|
std::process::exit(1);
|
|
}}
|
|
}}
|
|
}}
|
|
"#,
|
|
socket_path.to_string_lossy()
|
|
);
|
|
|
|
let binary_dir = TempDir::new().expect("Failed to create temp dir");
|
|
let binary_path = create_test_binary("test_local_socket", &test_code, binary_dir.path())
|
|
.expect("Failed to create test binary");
|
|
|
|
// Create Unix socket listener
|
|
use std::os::unix::net::UnixListener;
|
|
let listener = UnixListener::bind(&socket_path).expect("Failed to bind Unix socket");
|
|
|
|
// Execute in sandbox
|
|
let executor = SandboxExecutor::new(profile, test_fs.project_path.clone());
|
|
match executor.execute_sandboxed_spawn(
|
|
&binary_path.to_string_lossy(),
|
|
&[],
|
|
&test_fs.project_path,
|
|
) {
|
|
Ok(mut child) => {
|
|
std::thread::spawn(move || {
|
|
let _ = listener.accept();
|
|
});
|
|
|
|
let status = child.wait().expect("Failed to wait for child");
|
|
assert!(status.success(), "Local socket connection should succeed when allowed");
|
|
}
|
|
Err(e) => {
|
|
eprintln!("Sandbox execution failed: {} (may be expected in CI)", e);
|
|
}
|
|
}
|
|
|
|
// Clean up socket file
|
|
let _ = std::fs::remove_file(&socket_path);
|
|
}
|