//! Real components can accept TCP connections. Host port registration and host-only config deny access. use super::hook_guest; use sausage::{ exec::{Command, Exec, Options}, process::{Runtime, net::NetPolicy}, world::World, }; use std::{sync::Arc, time::Duration}; #[test] fn host_policy() { let temp = tempfile::tempdir().unwrap(); std::fs::write( temp.path().join("network.toml"), "deny = ['127.0.0.1:9001', '[::1]:9002']", ) .unwrap(); let policy = NetPolicy::open(temp.path()).unwrap(); policy.deny_host_port(9000); for address in [ "127.0.0.1:9000", "0.0.0.0:9000", "[::1]:9000", "[::ffff:127.0.0.1]:9000", "127.0.0.1:9001", "[::1]:9002", ] { assert!(!policy.allows(address.parse().unwrap())); } assert!(policy.allows("0.0.0.0:8080".parse().unwrap())); for interface in if_addrs::get_if_addrs().unwrap() { assert!(!policy.allows(std::net::SocketAddr::new(interface.ip(), 9000))); } assert!(!policy.allows("[::ffff:127.0.0.1]:9001".parse().unwrap())); } async fn run(runtime: &Arc, mode: &str, address: String) -> String { let mut output = Vec::new(); let code = runtime .exec( Command::Argv(vec!["/bin/guest.wasm".into(), mode.into(), address]), Options { cwd: "/".into(), timeout: Some(Duration::from_secs(30)), ..Default::default() }, &mut |_, bytes| output.extend_from_slice(bytes), ) .await .unwrap(); assert_eq!(code, 0); String::from_utf8(output).unwrap() } #[tokio::test] async fn guest_tcp_and_host_denial() { let guest = hook_guest::component(); let temp = tempfile::tempdir().unwrap(); let world = World::open(temp.path()).unwrap(); std::fs::create_dir_all(temp.path().join("world/bin")).unwrap(); std::fs::copy(guest, temp.path().join("world/bin/guest.wasm")).unwrap(); let configured = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); std::fs::write( world.host_dir.join("network.toml"), format!("deny = ['{}']", configured.local_addr().unwrap()), ) .unwrap(); let runtime = Runtime::new(world).unwrap(); assert!( run( &runtime, "connect", configured.local_addr().unwrap().to_string() ) .await .contains("denied") ); let native = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let denied = native.local_addr().unwrap(); runtime.network.deny_host_port(denied.port()); assert!( run(&runtime, "connect", denied.to_string()) .await .contains("denied") ); let ephemeral = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); let address = ephemeral.local_addr().unwrap(); drop(ephemeral); let server_runtime = runtime.clone(); let (tx, rx) = tokio::sync::oneshot::channel(); let server = tokio::spawn(async move { let mut tx = Some(tx); let mut bytes = Vec::new(); server_runtime .exec( Command::Argv(vec![ "/bin/guest.wasm".into(), "listen".into(), format!("0.0.0.0:{}", address.port()), ]), Options { cwd: "/".into(), timeout: Some(Duration::from_secs(30)), ..Default::default() }, &mut |_, chunk| { bytes.extend_from_slice(chunk); if bytes.windows(5).any(|w| w == b"ready") && let Some(tx) = tx.take() { let _ = tx.send(()); } }, ) .await .unwrap() }); tokio::time::timeout(Duration::from_secs(30), rx) .await .unwrap() .unwrap(); assert!( run(&runtime, "connect", format!("localhost:{}", address.port())) .await .contains("ping") ); assert_eq!(server.await.unwrap(), 0); }