use std::fs::File; use std::io::{self, Read, Seek, SeekFrom}; use std::net::{Ipv4Addr, SocketAddrV4, TcpStream, UdpSocket}; use std::os::fd::{AsFd, AsRawFd, OwnedFd}; use std::path::Path; use std::time::{Duration, Instant}; use lazaret_core::codec::decode; use lazaret_core::identity::{ ConfigurationFingerprint, DerivedPolicyDigest, DomainId, GenerationId, NetworkSidecarId, PidfsProcessIdentityV1, }; use lazaret_core::network::{CanonicalDerivedNetworkPolicy, derive_policy}; use lazaret_core::records::{ DeniedDestinationObservation, DerivedNetworkPolicy, DerivedNetworkPolicyFields, DnsObservation, HostProcessOwnershipSlot, IpAddress, IpNetwork, NetworkObservation, NetworkReachabilityProbe, NetworkReadinessAttestation, NetworkReadinessAttestationFields, PermittedDestinationObservation, PolicyAction, PolicyDelivery, ProcessOwnershipCommitAck, StagedProcessOwnership, }; use lazaret_linux::filesystem::{namespace_identity, require_write_sealed}; use lazaret_linux::process::{ ChildStandardIo, ProcessOwnershipContext, RetainedProcess, TargetCommand, child_broker, }; use rustix::event::{PollFd, PollFlags, Timespec, poll}; use rustix::io::{FdFlags, fcntl_setfd, read}; use rustix::mount::{MountPropagationFlags, mount_change}; use rustix::pipe::{PipeFlags, pipe_with}; use rustix::process::{Signal, getgid, getuid}; use rustix::thread::UnshareFlags; use serde::{Deserialize, Serialize}; const MAX_POLICY_BYTES: u64 = 64 * 1024; const HELPER_READY_TIMEOUT: Duration = Duration::from_secs(30); const ROUTE_READY_TIMEOUT: Duration = Duration::from_secs(15); const SLIRP4NETNS: &str = "/usr/bin/slirp4netns"; const IP: &str = "/usr/bin/ip"; const PROBE_TIMEOUT: Duration = Duration::from_secs(3); const MAX_DNS_PACKET: usize = 4096; #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct TcpProbeTarget { pub address: IpAddress, pub port: u16, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct DnsProbeTarget { pub resolver: IpAddress, pub query_name: String, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct NetworkProbeRequest { pub domain_id: DomainId, pub generation_id: GenerationId, pub enforcement_namespace: lazaret_core::identity::NamespaceIdentity, pub denied: Vec, pub permitted: Vec, pub dns: Vec, } #[derive(Clone, Copy, Debug)] pub struct NetworkAttestationContext { pub domain_id: DomainId, pub generation_id: GenerationId, pub configuration_fingerprint: ConfigurationFingerprint, pub network_sidecar_id: NetworkSidecarId, pub runtime_supervisor: PidfsProcessIdentityV1, pub coordinator_id: lazaret_core::identity::CoordinatorId, } pub struct PreparedNetwork { helper: Option, helper_exit: Option, attestation: NetworkReadinessAttestation, } impl PreparedNetwork { #[must_use] pub const fn attestation(&self) -> &NetworkReadinessAttestation { &self.attestation } #[must_use] pub fn helper_identity(&self) -> Option { self.helper.as_ref().map(RetainedProcess::identity) } /// # Errors /// Returns an error when exact helper liveness cannot be inspected. pub fn helper_exited(&self) -> io::Result { self.helper .as_ref() .ok_or_else(|| io::Error::other("connectivity helper handle is absent"))? .has_exited() } /// # Errors /// Returns an error unless the exact helper has exited and can be reaped. pub fn reap_exited(mut self) -> io::Result<()> { self.helper_exit.take(); let helper = self .helper .take() .ok_or_else(|| io::Error::other("connectivity helper handle is absent"))?; if !helper.has_exited()? { return Err(io::Error::other("connectivity helper is still present")); } child_broker().reap(helper) } /// # Errors /// Returns an error when the exact helper cannot be stopped and reaped. pub fn stop(mut self) -> io::Result<()> { self.helper_exit.take(); let helper = self .helper .take() .ok_or_else(|| io::Error::other("connectivity helper handle is absent"))?; let terminate = if helper.has_exited()? { None } else { helper.send_signal(Signal::TERM).err() }; let reaped = child_broker().reap(helper); match (terminate, reaped) { (None, Ok(())) => Ok(()), (Some(error), Ok(())) | (None, Err(error)) => Err(error), (Some(error), Err(reap)) => Err(io::Error::new( error.kind(), format!("{error}; exact connectivity-helper reap failed: {reap}"), )), } } } /// Verifies sealed policy delivery, supervises slirp4netns, installs routes, and attests state. /// /// # Errors /// Refuses mutable or mismatched policy, helper acquisition/readiness, route, or namespace state. pub fn prepare_network( policy_file: &File, expected_digest: DerivedPolicyDigest, context: NetworkAttestationContext, stage: Stage, acknowledge: Acknowledge, ) -> io::Result where Stage: FnOnce(StagedProcessOwnership) -> io::Result<()>, Acknowledge: FnOnce(StagedProcessOwnership) -> io::Result, { let policy = read_sealed_policy(policy_file, expected_digest)?; let (helper_ready, helper_ready_signal) = pipe_with(PipeFlags::CLOEXEC)?; let (helper_exit, helper_exit_signal) = pipe_with(PipeFlags::CLOEXEC)?; fcntl_setfd(helper_ready_signal.as_fd(), FdFlags::empty())?; fcntl_setfd(helper_exit.as_fd(), FdFlags::empty())?; let target = slirp_target( context.runtime_supervisor, helper_exit.as_raw_fd(), helper_ready_signal.as_raw_fd(), ); let helper = child_broker().acquire_with_stdio_after_ack( context.runtime_supervisor, ProcessOwnershipContext { domain_id: context.domain_id, generation_id: context.generation_id, coordinator_id: context.coordinator_id, slot: HostProcessOwnershipSlot::HelperConnectivityHelper, }, &target, ChildStandardIo::silenced(), stage, acknowledge, enter_domain_namespaces, )?; drop(helper_ready_signal); drop(helper_exit); let enforcement_namespace = match namespace_identity(Path::new("/proc/self/ns/net")) { Ok(namespace) => namespace, Err(error) => return cleanup_helper(helper, helper_exit_signal, error), }; if let Err(error) = wait_for_helper_ready(&helper_ready, &helper) { return cleanup_helper(helper, helper_exit_signal, error); } let observed_routes = match install_policy_routes(&policy) { Ok(routes) => routes, Err(error) => return cleanup_helper(helper, helper_exit_signal, error), }; let attestation = NetworkReadinessAttestation::v1(NetworkReadinessAttestationFields { domain_id: context.domain_id, generation_id: context.generation_id, configuration_fingerprint: context.configuration_fingerprint, network_sidecar_id: context.network_sidecar_id, derived_policy_digest: policy.digest(), enforcement_namespace, runtime_supervisor: context.runtime_supervisor, connectivity_helper: helper.identity(), observation: NetworkObservation { policy_delivery: PolicyDelivery::SealedMemfd, helper_ready: true, observed_routes, routes_attested: true, }, }); Ok(PreparedNetwork { helper: Some(helper), helper_exit: Some(helper_exit_signal), attestation, }) } #[allow(deprecated)] fn enter_domain_namespaces() -> io::Result<()> { let uid = getuid().as_raw(); let gid = getgid().as_raw(); rustix::thread::unshare(UnshareFlags::NEWUSER)?; std::fs::write("/proc/self/setgroups", "deny")?; std::fs::write("/proc/self/uid_map", format!("0 {uid} 1\n"))?; std::fs::write("/proc/self/gid_map", format!("0 {gid} 1\n"))?; rustix::thread::unshare(UnshareFlags::NEWNS | UnshareFlags::NEWNET)?; mount_change( "/", MountPropagationFlags::PRIVATE | MountPropagationFlags::REC, )?; Ok(()) } /// Executes the generation-bound reachability probe in the current enforcement namespace. /// /// # Errors /// Refuses changed namespace identity, empty coverage, non-IPv4 targets, or malformed DNS names. pub fn probe_current_network( request: &NetworkProbeRequest, ) -> io::Result { if namespace_identity(Path::new("/proc/self/ns/net"))? != request.enforcement_namespace { return Err(io::Error::other("network probe namespace identity changed")); } if request.denied.is_empty() || request.permitted.is_empty() || request.dns.is_empty() { return Err(io::Error::other("network probe coverage is incomplete")); } let denied_destinations = request .denied .iter() .map(|target| { Ok(DeniedDestinationObservation { destination: target.address, unreachable: TcpStream::connect_timeout(&socket_address(*target)?, PROBE_TIMEOUT) .is_err(), }) }) .collect::>>()?; let permitted_destinations = request .permitted .iter() .map(|target| { Ok(PermittedDestinationObservation { destination: target.address, reachable: TcpStream::connect_timeout(&socket_address(*target)?, PROBE_TIMEOUT) .is_ok(), }) }) .collect::>>()?; let dns_observations = request .dns .iter() .enumerate() .map(|(index, target)| dns_probe(target, u16::try_from(index).unwrap_or(u16::MAX))) .collect::>>()?; Ok(NetworkReachabilityProbe { domain_id: request.domain_id, generation_id: request.generation_id, enforcement_namespace: request.enforcement_namespace, denied_destinations, permitted_destinations, dns_observations, }) } fn socket_address(target: TcpProbeTarget) -> io::Result { let IpAddress::V4(address) = target.address else { return Err(io::Error::other("v1 network probes require IPv4")); }; if target.port == 0 { return Err(io::Error::other("network probe port is invalid")); } Ok(SocketAddrV4::new(Ipv4Addr::from(address), target.port).into()) } fn dns_probe(target: &DnsProbeTarget, transaction_id: u16) -> io::Result { let IpAddress::V4(resolver) = target.resolver else { return Err(io::Error::other("v1 DNS probes require IPv4")); }; let query = dns_query(transaction_id, &target.query_name)?; let socket = UdpSocket::bind(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0))?; socket.set_read_timeout(Some(PROBE_TIMEOUT))?; socket.set_write_timeout(Some(PROBE_TIMEOUT))?; socket.send_to(&query, SocketAddrV4::new(Ipv4Addr::from(resolver), 53))?; let mut response = [0_u8; MAX_DNS_PACKET]; let (length, source) = socket.recv_from(&mut response).map_err(|error| { io::Error::new( error.kind(), format!( "DNS probe receive from {}.{}.{}.{}:53 for {} failed: {error}", resolver[0], resolver[1], resolver[2], resolver[3], target.query_name ), ) })?; if source.ip() != Ipv4Addr::from(resolver) || source.port() != 53 { return Err(io::Error::other( "DNS probe response source mismatched resolver", )); } let resolved_addresses = dns_answers(&response[..length], transaction_id)?; Ok(DnsObservation { resolver: target.resolver, query_name: target.query_name.clone(), resolved_addresses, }) } fn dns_query(transaction_id: u16, name: &str) -> io::Result> { if name.is_empty() || name.len() > 253 || !name.is_ascii() { return Err(io::Error::other("DNS probe name is invalid")); } let mut query = Vec::with_capacity(name.len() + 18); query.extend_from_slice(&transaction_id.to_be_bytes()); query.extend_from_slice(&[0x01, 0x00, 0x00, 0x01, 0, 0, 0, 0, 0, 0]); for label in name.trim_end_matches('.').split('.') { if label.is_empty() || label.len() > 63 || !label .bytes() .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') { return Err(io::Error::other("DNS probe label is invalid")); } query.push( u8::try_from(label.len()).map_err(|_| io::Error::other("DNS label is oversized"))?, ); query.extend_from_slice(label.as_bytes()); } query.extend_from_slice(&[0, 0, 1, 0, 1]); Ok(query) } fn dns_answers(response: &[u8], transaction_id: u16) -> io::Result> { if response.len() < 12 || response[..2] != transaction_id.to_be_bytes() || response[2] & 0x80 == 0 || response[3] & 0x0f != 0 || u16::from_be_bytes([response[4], response[5]]) != 1 { return Err(io::Error::other("DNS probe response header is invalid")); } let answers = u16::from_be_bytes([response[6], response[7]]); let mut offset = skip_dns_name(response, 12)?; offset = offset .checked_add(4) .filter(|offset| *offset <= response.len()) .ok_or_else(|| io::Error::other("DNS probe question is truncated"))?; let mut addresses = Vec::new(); for _ in 0..answers { offset = skip_dns_name(response, offset)?; let header_end = offset .checked_add(10) .filter(|end| *end <= response.len()) .ok_or_else(|| io::Error::other("DNS probe answer is truncated"))?; let record_type = u16::from_be_bytes([response[offset], response[offset + 1]]); let class = u16::from_be_bytes([response[offset + 2], response[offset + 3]]); let data_length = usize::from(u16::from_be_bytes([ response[offset + 8], response[offset + 9], ])); let data_end = header_end .checked_add(data_length) .filter(|end| *end <= response.len()) .ok_or_else(|| io::Error::other("DNS probe record data is truncated"))?; if record_type == 1 && class == 1 && data_length == 4 { addresses.push(IpAddress::V4( response[header_end..data_end] .try_into() .map_err(|_| io::Error::other("DNS A record is malformed"))?, )); } offset = data_end; } if addresses.is_empty() { return Err(io::Error::other("DNS probe returned no IPv4 addresses")); } Ok(addresses) } fn skip_dns_name(packet: &[u8], mut offset: usize) -> io::Result { loop { let length = *packet .get(offset) .ok_or_else(|| io::Error::other("DNS name is truncated"))?; offset += 1; if length == 0 { return Ok(offset); } if length & 0xc0 == 0xc0 { return offset .checked_add(1) .filter(|end| *end <= packet.len()) .ok_or_else(|| io::Error::other("DNS compression pointer is truncated")); } if length & 0xc0 != 0 || length > 63 { return Err(io::Error::other("DNS name label is malformed")); } offset = offset .checked_add(usize::from(length)) .filter(|end| *end <= packet.len()) .ok_or_else(|| io::Error::other("DNS name label is truncated"))?; } } fn read_sealed_policy( policy_file: &File, expected_digest: DerivedPolicyDigest, ) -> io::Result { require_write_sealed(policy_file)?; let length = policy_file.metadata()?.len(); if length == 0 || length > MAX_POLICY_BYTES { return Err(io::Error::other("sealed network policy has invalid size")); } let mut policy_file = policy_file.try_clone()?; policy_file.seek(SeekFrom::Start(0))?; let mut bytes = Vec::with_capacity( usize::try_from(length).map_err(|_| io::Error::other("network policy is oversized"))?, ); policy_file .take(MAX_POLICY_BYTES + 1) .read_to_end(&mut bytes)?; if u64::try_from(bytes.len()).ok() != Some(length) { return Err(io::Error::other( "sealed network policy read was incomplete", )); } let decoded: DerivedNetworkPolicy = decode(&bytes).map_err(|error| io::Error::other(error.to_string()))?; let canonical = derive_policy(DerivedNetworkPolicyFields { address_families: decoded.address_families, default_actions: decoded.default_actions, prefix_rules: decoded.prefix_rules, resolvers: decoded.resolvers, }) .map_err(io::Error::other)?; if canonical.digest() != expected_digest || canonical.canonical_bytes() != bytes { return Err(io::Error::other( "sealed network policy digest or canonical bytes mismatch", )); } Ok(canonical) } fn slirp_target(runtime: PidfsProcessIdentityV1, exit_fd: i32, ready_fd: i32) -> TargetCommand { TargetCommand::new(SLIRP4NETNS) .argument("--configure") .argument("--mtu=65520") .argument("--disable-host-loopback") .argument("--enable-sandbox") .argument("--enable-seccomp") .argument(format!("--exit-fd={exit_fd}")) .argument(format!("--ready-fd={ready_fd}")) .argument(format!( "--userns-path=/proc/{}/ns/user", runtime.host_pid().get() )) .argument(runtime.host_pid().get().to_string()) .argument("tap0") .with_parent_death_kill_edge() } fn wait_for_helper_ready(ready: &OwnedFd, helper: &RetainedProcess) -> io::Result<()> { let deadline = Instant::now() + HELPER_READY_TIMEOUT; loop { if helper.has_exited()? { return Err(io::Error::other( "slirp4netns exited before signaling readiness", )); } if poll_helper_ready(ready)? { return Ok(()); } if Instant::now() >= deadline { return Err(io::Error::other("slirp4netns readiness timed out")); } } } fn poll_helper_ready(ready: &OwnedFd) -> io::Result { let mut descriptors = [PollFd::new(ready, PollFlags::IN)]; poll( &mut descriptors, Some(&Timespec { tv_sec: 0, tv_nsec: 10_000_000, }), )?; let events = descriptors[0].revents(); if events.intersects(PollFlags::IN) { let mut byte = [0_u8; 1]; return match read(ready.as_fd(), &mut byte) { Ok(0) => Err(io::Error::other("slirp4netns readiness pipe closed")), Ok(_) => Ok(true), Err(rustix::io::Errno::INTR) => Ok(false), Err(error) => Err(error.into()), }; } if events.intersects(PollFlags::HUP | PollFlags::ERR) { return Err(io::Error::other("slirp4netns readiness pipe closed")); } Ok(false) } fn install_policy_routes(policy: &CanonicalDerivedNetworkPolicy) -> io::Result> { run_ip(["link", "set", "lo", "up"])?; wait_for_default_route()?; let mut denied = Vec::new(); for rule in &policy.policy().prefix_rules { let IpAddress::V4(address) = rule.network.address else { continue; }; let cidr = ipv4_cidr(address, rule.network.prefix_length); match rule.action { PolicyAction::Deny => { run_ip(["-4", "route", "replace", &cidr, "dev", "lo"])?; denied.push(rule.network); } PolicyAction::Allow => { install_tap_route(&cidr, address, rule.network.prefix_length)?; } } } for resolver in &policy.policy().resolvers { let IpAddress::V4(address) = resolver.address else { continue; }; let cidr = ipv4_cidr(address, 32); install_tap_route(&cidr, address, 32)?; } install_tap_route("10.0.2.3/32", [10, 0, 2, 3], 32)?; verify_loopback_routes(&denied)?; Ok(denied) } fn install_tap_route(cidr: &str, address: [u8; 4], prefix_length: u8) -> io::Result<()> { if ipv4_network_contains(address, prefix_length, [10, 0, 2, 2]) { run_ip(["-4", "route", "replace", cidr, "dev", "tap0"]) } else { run_ip([ "-4", "route", "replace", cidr, "via", "10.0.2.2", "dev", "tap0", ]) } } fn ipv4_network_contains(network: [u8; 4], prefix_length: u8, address: [u8; 4]) -> bool { let mask = if prefix_length == 0 { 0 } else { u32::MAX << (32 - prefix_length) }; u32::from_be_bytes(network) & mask == u32::from_be_bytes(address) & mask } fn wait_for_default_route() -> io::Result<()> { let deadline = Instant::now() + ROUTE_READY_TIMEOUT; loop { let routes = std::fs::read_to_string("/proc/self/net/route")?; if routes.lines().skip(1).any(|line| { let mut columns = line.split_whitespace(); columns.next() == Some("tap0") && columns.next() == Some("00000000") }) { return Ok(()); } if Instant::now() >= deadline { return Err(io::Error::other("slirp4netns default route timed out")); } std::thread::sleep(Duration::from_millis(5)); } } fn verify_loopback_routes(expected: &[IpNetwork]) -> io::Result<()> { let routes = std::fs::read_to_string("/proc/self/net/route")?; for network in expected { let IpAddress::V4(address) = network.address else { continue; }; let destination = u32::from_le_bytes(address); let mask = if network.prefix_length == 0 { 0 } else { u32::MAX << (32 - network.prefix_length) }; let route_table_mask = u32::from_le_bytes(mask.to_be_bytes()); let found = routes.lines().skip(1).any(|line| { let columns = line.split_whitespace().collect::>(); columns.len() >= 8 && columns[0] == "lo" && u32::from_str_radix(columns[1], 16).ok() == Some(destination) && u32::from_str_radix(columns[7], 16).ok() == Some(route_table_mask) }); if !found { return Err(io::Error::other(format!( "loopback-unicast route is absent for {}", ipv4_cidr(address, network.prefix_length) ))); } } Ok(()) } fn run_ip(arguments: [&str; N]) -> io::Result<()> { let mut target = TargetCommand::new(IP); for argument in arguments { target = target.argument(argument); } child_broker() .run_transient(&target.with_parent_death_kill_edge()) .map(|_| ()) } fn cleanup_helper(helper: RetainedProcess, exit: OwnedFd, error: io::Error) -> io::Result { drop(exit); let _ = helper.send_signal(Signal::KILL); match child_broker().reap(helper) { Ok(()) => Err(error), Err(cleanup) => Err(io::Error::new( error.kind(), format!("{error}; exact connectivity-helper cleanup failed: {cleanup}"), )), } } fn ipv4_cidr(address: [u8; 4], prefix_length: u8) -> String { format!( "{}.{}.{}.{}/{}", address[0], address[1], address[2], address[3], prefix_length ) } #[cfg(test)] mod tests { use lazaret_core::network::derive_policy; use lazaret_core::records::{ AddressFamily, AddressFamilyDisposition, AddressFamilyDispositionValue, DefaultAction, DerivedNetworkPolicyFields, PolicyAction, PrefixRule, Resolver, }; use lazaret_linux::filesystem::sealed_memfd; use super::*; #[test] fn sealed_policy_requires_exact_canonical_digest() { let policy = policy(); let file = sealed_memfd("network-test", policy.canonical_bytes()).unwrap(); assert_eq!(read_sealed_policy(&file, policy.digest()).unwrap(), policy); assert!(read_sealed_policy(&file, DerivedPolicyDigest::new([9; 32])).is_err()); } #[test] fn dns_codec_accepts_matching_ipv4_answer_and_refuses_mutation() { let transaction = 17; let query = dns_query(transaction, "example.com").unwrap(); let mut response = query.clone(); response[2] = 0x81; response[3] = 0x80; response[6..8].copy_from_slice(&1_u16.to_be_bytes()); response.extend_from_slice(&[0xc0, 0x0c, 0, 1, 0, 1, 0, 0, 0, 60, 0, 4, 93, 184, 216, 34]); assert_eq!( dns_answers(&response, transaction).unwrap(), vec![IpAddress::V4([93, 184, 216, 34])] ); assert!(dns_answers(&response, transaction + 1).is_err()); assert!(dns_query(transaction, "bad..name").is_err()); } #[test] fn probe_refuses_a_changed_enforcement_namespace_before_network_io() { let mut namespace = namespace_identity(Path::new("/proc/self/ns/net")).unwrap(); namespace.inode = lazaret_core::identity::InodeId::new(namespace.inode.get() + 1); let request = NetworkProbeRequest { domain_id: DomainId::new([1; 32]), generation_id: GenerationId::new(7), enforcement_namespace: namespace, denied: vec![TcpProbeTarget { address: IpAddress::V4([10, 0, 0, 1]), port: 1, }], permitted: vec![TcpProbeTarget { address: IpAddress::V4([1, 1, 1, 1]), port: 443, }], dns: vec![DnsProbeTarget { resolver: IpAddress::V4([8, 8, 8, 8]), query_name: "example.com".into(), }], }; assert!(probe_current_network(&request).is_err()); } #[test] fn readiness_consumes_a_pending_byte_before_observing_writer_close() { let (reader, writer) = pipe_with(PipeFlags::empty()).unwrap(); rustix::io::write(&writer, &[1]).unwrap(); drop(writer); assert!(poll_helper_ready(&reader).unwrap()); } #[test] fn proc_route_mask_uses_little_endian_network_bytes() { let mask = u32::MAX << (32 - 8); assert_eq!(u32::from_le_bytes(mask.to_be_bytes()), 0x0000_00ff); } fn policy() -> CanonicalDerivedNetworkPolicy { derive_policy(DerivedNetworkPolicyFields { address_families: vec![ AddressFamilyDisposition { family: AddressFamily::Ipv4, disposition: AddressFamilyDispositionValue::Enabled, }, AddressFamilyDisposition { family: AddressFamily::Ipv6, disposition: AddressFamilyDispositionValue::Disabled, }, ], default_actions: vec![DefaultAction { family: AddressFamily::Ipv4, action: PolicyAction::Allow, }], prefix_rules: [ ([10, 0, 0, 0], 8), ([172, 16, 0, 0], 12), ([192, 168, 0, 0], 16), ] .into_iter() .map(|(address, prefix_length)| PrefixRule { family: AddressFamily::Ipv4, network: IpNetwork { address: IpAddress::V4(address), prefix_length, }, action: PolicyAction::Deny, }) .collect(), resolvers: vec![Resolver { family: AddressFamily::Ipv4, address: IpAddress::V4([8, 8, 8, 8]), }], }) .unwrap() } }