1use std::collections::{HashMap, HashSet};
7use std::io;
8use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
9use std::os::unix::io::{AsRawFd, RawFd};
10use std::sync::Arc;
11
12use serde::{Deserialize, Serialize};
13
14use crate::error::SandboxError;
15use crate::seccomp::ctx::SupervisorCtx;
16use crate::seccomp::notif::{read_child_mem, write_child_mem, NotifAction};
17use crate::sys::structs::{SeccompNotif, AF_INET, AF_INET6, ECONNREFUSED};
18
19const MAX_SEND_BUF: usize = 64 << 20;
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
26pub struct IpCidr {
27 pub addr: IpAddr,
28 pub prefix_len: u8,
29}
30
31impl IpCidr {
32 pub fn parse(s: &str) -> Result<Self, SandboxError> {
36 let (addr_str, prefix) = match s.split_once('/') {
37 Some((a, p)) => {
38 let len: u8 = p.parse().map_err(|_| {
39 SandboxError::Invalid(format!("invalid prefix length in `{}`", s))
40 })?;
41 (a, Some(len))
42 }
43 None => (s, None),
44 };
45 let addr: IpAddr = addr_str.parse().map_err(|_| {
46 SandboxError::Invalid(format!("`{}` is not a valid IP address", s))
47 })?;
48 let max = match addr {
49 IpAddr::V4(_) => 32u8,
50 IpAddr::V6(_) => 128u8,
51 };
52 let prefix_len = prefix.unwrap_or(max);
53 if prefix_len > max {
54 return Err(SandboxError::Invalid(format!(
55 "prefix /{} too large for {} in `{}`",
56 prefix_len,
57 if max == 32 { "IPv4" } else { "IPv6" },
58 s
59 )));
60 }
61 Ok(IpCidr { addr, prefix_len })
62 }
63
64 pub fn is_single_host(&self) -> bool {
67 match self.addr {
68 IpAddr::V4(_) => self.prefix_len == 32,
69 IpAddr::V6(_) => self.prefix_len == 128,
70 }
71 }
72
73 pub fn contains(&self, ip: IpAddr) -> bool {
76 match (self.addr, ip) {
77 (IpAddr::V4(net), IpAddr::V4(ip)) => {
78 if self.prefix_len == 0 {
79 return true;
80 }
81 let mask = u32::MAX << (32 - self.prefix_len);
82 (u32::from(net) & mask) == (u32::from(ip) & mask)
83 }
84 (IpAddr::V6(net), IpAddr::V6(ip)) => {
85 if self.prefix_len == 0 {
86 return true;
87 }
88 let mask = u128::MAX << (128 - self.prefix_len);
89 (u128::from(net) & mask) == (u128::from(ip) & mask)
90 }
91 _ => false,
92 }
93 }
94}
95
96impl std::fmt::Display for IpCidr {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 if self.is_single_host() {
101 write!(f, "{}", self.addr)
102 } else {
103 write!(f, "{}/{}", self.addr, self.prefix_len)
104 }
105 }
106}
107
108#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
114pub enum NetTarget {
115 AnyIp,
117 Cidr(IpCidr),
119 Host(String),
121}
122
123#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
128pub struct NetRule {
129 #[serde(default = "default_protocol_tcp")]
131 pub protocol: Protocol,
132 pub target: NetTarget,
134 pub ports: Vec<u16>,
137 #[serde(default)]
139 pub all_ports: bool,
140}
141
142pub type NetAllow = NetRule;
145pub type NetDeny = NetRule;
146
147fn default_protocol_tcp() -> Protocol {
148 Protocol::Tcp
149}
150
151impl NetRule {
152 pub fn parse_allow(spec: &str) -> Result<NetRule, SandboxError> {
163 Self::parse_spec(spec, "--net-allow", true)
164 }
165
166 pub fn parse_deny(spec: &str) -> Result<NetDeny, SandboxError> {
171 Self::parse_spec(spec, "--net-deny", false)
172 }
173
174 fn parse_spec(spec: &str, label: &str, allow_hosts: bool) -> Result<NetRule, SandboxError> {
178 let (protocol, rest) = match spec.split_once("://") {
179 Some((scheme, body)) => {
180 let proto = Protocol::parse(scheme).ok_or_else(|| {
181 SandboxError::Invalid(format!(
182 "{}: unknown scheme `{}://` in `{}` (expected tcp, udp, icmp)",
183 label, scheme, spec
184 ))
185 })?;
186 (proto, body)
187 }
188 None => (Protocol::Tcp, spec),
189 };
190
191 if protocol == Protocol::Icmp {
193 if rest.is_empty() {
194 return Err(SandboxError::Invalid(format!(
195 "{}: icmp rule needs a host/IP or `*`, got `{}`",
196 label, spec
197 )));
198 }
199 if rest != "*" && IpCidr::parse(rest).is_err() && rest.contains(':') {
202 return Err(SandboxError::Invalid(format!(
203 "{}: icmp rule takes no port, got `{}`",
204 label, spec
205 )));
206 }
207 return Ok(NetRule {
208 protocol,
209 target: parse_target(rest, label, allow_hosts)?,
210 ports: Vec::new(),
211 all_ports: true,
212 });
213 }
214
215 if let Some(stripped) = rest.strip_prefix('[') {
217 let (inside, port_part) = stripped.rsplit_once("]:").ok_or_else(|| {
218 SandboxError::Invalid(format!("{}: malformed bracketed address in `{}`", label, spec))
219 })?;
220 let (ports, all_ports) = parse_ports(port_part, label, spec)?;
221 return Ok(NetRule {
222 protocol,
223 target: NetTarget::Cidr(IpCidr::parse(inside)?),
224 ports,
225 all_ports,
226 });
227 }
228
229 if rest.is_empty() {
232 return Err(SandboxError::Invalid(format!(
233 "{}: empty rule in `{}` (use `*` for any host)",
234 label, spec
235 )));
236 }
237
238 if let Ok(cidr) = IpCidr::parse(rest) {
242 return Ok(NetRule {
243 protocol,
244 target: NetTarget::Cidr(cidr),
245 ports: Vec::new(),
246 all_ports: true,
247 });
248 }
249
250 let (host_part, port_part) = match rest.rsplit_once(':') {
254 Some((h, p)) => (h, Some(p)),
255 None => (rest, None),
256 };
257 let target = parse_target(host_part, label, allow_hosts)?;
258 let (ports, all_ports) = match port_part {
259 Some(p) => parse_ports(p, label, spec)?,
260 None => (Vec::new(), true),
261 };
262 Ok(NetRule {
263 protocol,
264 target,
265 ports,
266 all_ports,
267 })
268 }
269}
270
271fn parse_target(s: &str, label: &str, allow_hosts: bool) -> Result<NetTarget, SandboxError> {
274 match s {
275 "" | "*" => Ok(NetTarget::AnyIp),
276 _ if s.contains('/') => Ok(NetTarget::Cidr(
279 IpCidr::parse(s).map_err(|e| SandboxError::Invalid(format!("{}: {}", label, e)))?,
280 )),
281 _ => {
282 if let Ok(cidr) = IpCidr::parse(s) {
283 Ok(NetTarget::Cidr(cidr))
284 } else if allow_hosts {
285 Ok(NetTarget::Host(s.to_string()))
286 } else {
287 Err(SandboxError::Invalid(format!(
288 "{}: `{}` is not an IP or CIDR (hostnames are not allowed; \
289 use --http-deny for domains)",
290 label, s
291 )))
292 }
293 }
294 }
295}
296
297fn parse_ports(s: &str, label: &str, full: &str) -> Result<(Vec<u16>, bool), SandboxError> {
300 let mut ports = Vec::new();
301 let mut saw_wildcard = false;
302 for p in s.split(',') {
303 let p = p.trim();
304 if p == "*" {
305 saw_wildcard = true;
306 continue;
307 }
308 let n: u16 = p.parse().map_err(|_| {
309 SandboxError::Invalid(format!("{}: invalid port `{}` in `{}`", label, p, full))
310 })?;
311 if n == 0 {
312 return Err(SandboxError::Invalid(format!(
313 "{}: port 0 is not valid in `{}`",
314 label, full
315 )));
316 }
317 ports.push(n);
318 }
319 if saw_wildcard && !ports.is_empty() {
320 return Err(SandboxError::Invalid(format!(
321 "{}: cannot mix `*` with concrete ports in `{}`",
322 label, full
323 )));
324 }
325 if !saw_wildcard && ports.is_empty() {
326 return Err(SandboxError::Invalid(format!(
327 "{}: at least one port required in `{}`",
328 label, full
329 )));
330 }
331 Ok((ports, saw_wildcard))
332}
333
334#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
349#[serde(rename_all = "lowercase")]
350pub enum Protocol {
351 Tcp,
352 Udp,
353 Icmp,
354}
355
356impl Protocol {
357 fn parse(s: &str) -> Option<Self> {
358 match s {
359 "tcp" => Some(Protocol::Tcp),
360 "udp" => Some(Protocol::Udp),
361 "icmp" => Some(Protocol::Icmp),
362 _ => None,
363 }
364 }
365}
366
367fn parse_ip_from_sockaddr(bytes: &[u8]) -> Option<IpAddr> {
374 if bytes.len() < 2 {
375 return None;
376 }
377 let family = u16::from_ne_bytes([bytes[0], bytes[1]]) as u32;
378 match family {
379 f if f == AF_INET => {
380 if bytes.len() < 8 {
381 return None;
382 }
383 Some(IpAddr::V4(Ipv4Addr::new(
384 bytes[4], bytes[5], bytes[6], bytes[7],
385 )))
386 }
387 f if f == AF_INET6 => {
388 if bytes.len() < 24 {
389 return None;
390 }
391 let mut addr_bytes = [0u8; 16];
392 addr_bytes.copy_from_slice(&bytes[8..24]);
393 Some(IpAddr::V6(Ipv6Addr::from(addr_bytes)))
394 }
395 _ => None,
396 }
397}
398
399fn parse_port_from_sockaddr(bytes: &[u8]) -> Option<u16> {
406 if bytes.len() < 4 {
407 return None;
408 }
409 let family = u16::from_ne_bytes([bytes[0], bytes[1]]) as u32;
410 match family {
411 f if f == AF_INET || f == AF_INET6 => {
412 Some(u16::from_be_bytes([bytes[2], bytes[3]]))
413 }
414 _ => None,
415 }
416}
417
418fn set_port_in_sockaddr(bytes: &mut [u8], port: u16) {
419 if bytes.len() >= 4 {
420 let port_bytes = port.to_be_bytes();
421 bytes[2] = port_bytes[0];
422 bytes[3] = port_bytes[1];
423 }
424}
425
426pub(crate) fn query_socket_protocol(fd: RawFd) -> Option<Protocol> {
437 let mut proto: libc::c_int = 0;
438 let mut len: libc::socklen_t = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
439 let rc = unsafe {
440 libc::getsockopt(
441 fd,
442 libc::SOL_SOCKET,
443 libc::SO_PROTOCOL,
444 &mut proto as *mut _ as *mut libc::c_void,
445 &mut len,
446 )
447 };
448 if rc != 0 {
449 return None;
450 }
451 match proto {
452 libc::IPPROTO_TCP => Some(Protocol::Tcp),
453 libc::IPPROTO_UDP => Some(Protocol::Udp),
454 libc::IPPROTO_ICMP | libc::IPPROTO_ICMPV6 => Some(Protocol::Icmp),
458 _ => None,
459 }
460}
461
462async fn connect_on_behalf(
474 notif: &SeccompNotif,
475 ctx: &Arc<SupervisorCtx>,
476 notif_fd: RawFd,
477) -> NotifAction {
478 let args = ¬if.data.args;
479 let sockfd = args[0] as i32;
480 let addr_ptr = args[1];
481 let addr_len = args[2] as u32;
482
483 let addr_bytes =
485 match read_child_mem(notif_fd, notif.id, notif.pid, addr_ptr, addr_len as usize) {
486 Ok(b) => b,
487 Err(_) => return NotifAction::Errno(libc::EIO),
488 };
489
490 if let Some(ip) = parse_ip_from_sockaddr(&addr_bytes) {
498 let dest_port = parse_port_from_sockaddr(&addr_bytes);
499 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
500 Ok(fd) => fd,
501 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
502 };
503 let protocol = match query_socket_protocol(dup_fd.as_raw_fd()) {
504 Some(p) => p,
505 None => return NotifAction::Errno(ECONNREFUSED),
506 };
507 let ns = ctx.network.lock().await;
508 let live_policy = {
509 let pfs = ctx.policy_fn.lock().await;
510 pfs.live_policy.clone()
511 };
512 let effective = ns.effective_network_policy(notif.pid, protocol, live_policy.as_ref());
513 match (effective, dest_port) {
514 (crate::seccomp::notif::NetworkPolicy::Unrestricted, _) => {
515 }
519 (policy, Some(p)) => {
520 if !policy.allows(ip, p) {
524 return NotifAction::Errno(ECONNREFUSED);
525 }
526 }
527 (_, None) => {
528 return NotifAction::Errno(ECONNREFUSED);
530 }
531 }
532 let http_acl_addr = ns.http_acl_addr;
534 let http_acl_intercept = dest_port.map_or(false, |p| ns.http_acl_ports.contains(&p));
535 let http_acl_orig_dest = ns.http_acl_orig_dest.clone();
536 let remapped_loopback_port = if ctx.policy.port_remap && ip.is_loopback() {
537 dest_port.and_then(|p| ns.port_map.get_real(p))
538 } else {
539 None
540 };
541
542 drop(ns);
543
544 let mut redirected = false;
546 let is_ipv6 = parse_ip_from_sockaddr(&addr_bytes)
547 .map_or(false, |ip| ip.is_ipv6());
548 let (mut connect_addr, connect_len) = if let Some(proxy_addr) = http_acl_addr {
549 if http_acl_intercept {
550 redirected = true;
551 if is_ipv6 {
552 let mut sa6: libc::sockaddr_in6 = unsafe { std::mem::zeroed() };
555 sa6.sin6_family = libc::AF_INET6 as u16;
556 sa6.sin6_port = proxy_addr.port().to_be();
557 let mapped = std::net::Ipv6Addr::from(
559 match proxy_addr {
560 std::net::SocketAddr::V4(v4) => v4.ip().to_ipv6_mapped(),
561 std::net::SocketAddr::V6(v6) => *v6.ip(),
562 }
563 );
564 sa6.sin6_addr.s6_addr = mapped.octets();
565 let bytes = unsafe {
566 std::slice::from_raw_parts(
567 &sa6 as *const _ as *const u8,
568 std::mem::size_of::<libc::sockaddr_in6>(),
569 )
570 }
571 .to_vec();
572 (bytes, std::mem::size_of::<libc::sockaddr_in6>() as u32)
573 } else {
574 let mut sa: libc::sockaddr_in = unsafe { std::mem::zeroed() };
576 sa.sin_family = libc::AF_INET as u16;
577 sa.sin_port = proxy_addr.port().to_be();
578 match proxy_addr {
579 std::net::SocketAddr::V4(v4) => {
580 sa.sin_addr.s_addr = u32::from_ne_bytes(v4.ip().octets());
581 }
582 std::net::SocketAddr::V6(_) => {
583 return NotifAction::Errno(libc::EAFNOSUPPORT);
585 }
586 }
587 let bytes = unsafe {
588 std::slice::from_raw_parts(
589 &sa as *const _ as *const u8,
590 std::mem::size_of::<libc::sockaddr_in>(),
591 )
592 }
593 .to_vec();
594 (bytes, std::mem::size_of::<libc::sockaddr_in>() as u32)
595 }
596 } else {
597 (addr_bytes.clone(), addr_len)
598 }
599 } else {
600 (addr_bytes.clone(), addr_len)
601 };
602 if !redirected {
603 if let Some(real_port) = remapped_loopback_port {
604 set_port_in_sockaddr(&mut connect_addr, real_port);
607 }
608 }
609
610 if redirected {
619 if let Some(ref orig_dest_map) = http_acl_orig_dest {
620 if let Some(orig_ip) = parse_ip_from_sockaddr(&addr_bytes) {
621 if is_ipv6 {
624 let mut bind_sa6: libc::sockaddr_in6 = unsafe { std::mem::zeroed() };
625 bind_sa6.sin6_family = libc::AF_INET6 as u16;
626 unsafe {
628 libc::bind(
629 dup_fd.as_raw_fd(),
630 &bind_sa6 as *const _ as *const libc::sockaddr,
631 std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
632 );
633 }
634 let mut local_sa6: libc::sockaddr_in6 = unsafe { std::mem::zeroed() };
635 let mut local_len: libc::socklen_t =
636 std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t;
637 let gs_ret = unsafe {
638 libc::getsockname(
639 dup_fd.as_raw_fd(),
640 &mut local_sa6 as *mut _ as *mut libc::sockaddr,
641 &mut local_len,
642 )
643 };
644 if gs_ret == 0 {
645 let local_port = u16::from_be(local_sa6.sin6_port);
646 let local_ip = Ipv6Addr::from(local_sa6.sin6_addr.s6_addr);
647 let local_addr = std::net::SocketAddr::V6(
648 std::net::SocketAddrV6::new(local_ip, local_port, 0, 0),
649 );
650 if let Ok(mut map) = orig_dest_map.write() {
651 map.insert(local_addr, orig_ip);
652 }
653 }
654 } else {
655 let mut bind_sa: libc::sockaddr_in = unsafe { std::mem::zeroed() };
656 bind_sa.sin_family = libc::AF_INET as u16;
657 unsafe {
659 libc::bind(
660 dup_fd.as_raw_fd(),
661 &bind_sa as *const _ as *const libc::sockaddr,
662 std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
663 );
664 }
665 let mut local_sa: libc::sockaddr_in = unsafe { std::mem::zeroed() };
666 let mut local_len: libc::socklen_t =
667 std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t;
668 let gs_ret = unsafe {
669 libc::getsockname(
670 dup_fd.as_raw_fd(),
671 &mut local_sa as *mut _ as *mut libc::sockaddr,
672 &mut local_len,
673 )
674 };
675 if gs_ret == 0 {
676 let local_port = u16::from_be(local_sa.sin_port);
677 let local_ip = Ipv4Addr::from(u32::from_be(local_sa.sin_addr.s_addr));
678 let local_addr = std::net::SocketAddr::V4(
679 std::net::SocketAddrV4::new(local_ip, local_port),
680 );
681 if let Ok(mut map) = orig_dest_map.write() {
682 map.insert(local_addr, orig_ip);
683 }
684 }
685 }
686 }
687 }
688 }
689
690 let ret = unsafe {
692 libc::connect(
693 dup_fd.as_raw_fd(),
694 connect_addr.as_ptr() as *const libc::sockaddr,
695 connect_len as libc::socklen_t,
696 )
697 };
698
699 if ret == 0 {
704 NotifAction::ReturnValue(0)
705 } else {
706 let errno = unsafe { *libc::__errno_location() };
707 NotifAction::Errno(errno)
708 }
709 } else {
711 NotifAction::Continue
713 }
714}
715
716async fn sendto_on_behalf(
732 notif: &SeccompNotif,
733 ctx: &Arc<SupervisorCtx>,
734 notif_fd: RawFd,
735) -> NotifAction {
736 let args = ¬if.data.args;
737 let sockfd = args[0] as i32;
738 let buf_ptr = args[1];
739 let buf_len = args[2] as usize;
740 if buf_len > MAX_SEND_BUF {
741 return NotifAction::Errno(libc::EMSGSIZE);
742 }
743 let flags = args[3] as i32;
744 let addr_ptr = args[4];
745 let addr_len = args[5] as u32;
746
747 if addr_ptr == 0 {
748 return NotifAction::Continue; }
750
751 let addr_bytes =
753 match read_child_mem(notif_fd, notif.id, notif.pid, addr_ptr, addr_len as usize) {
754 Ok(b) => b,
755 Err(_) => return NotifAction::Errno(libc::EIO),
756 };
757
758 if let Some(ip) = parse_ip_from_sockaddr(&addr_bytes) {
762 let dest_port = parse_port_from_sockaddr(&addr_bytes);
763 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
764 Ok(fd) => fd,
765 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
766 };
767 let protocol = match query_socket_protocol(dup_fd.as_raw_fd()) {
768 Some(p) => p,
769 None => return NotifAction::Errno(ECONNREFUSED),
770 };
771 let ns = ctx.network.lock().await;
772 let live_policy = {
773 let pfs = ctx.policy_fn.lock().await;
774 pfs.live_policy.clone()
775 };
776 let effective = ns.effective_network_policy(notif.pid, protocol, live_policy.as_ref());
777 if !matches!(effective, crate::seccomp::notif::NetworkPolicy::Unrestricted) {
778 match dest_port {
779 Some(p) if !effective.allows(ip, p) => {
780 return NotifAction::Errno(ECONNREFUSED);
781 }
782 None => return NotifAction::Errno(ECONNREFUSED),
783 Some(_) => {}
784 }
785 }
786 drop(ns);
787
788 let data = match read_child_mem(notif_fd, notif.id, notif.pid, buf_ptr, buf_len) {
790 Ok(b) => b,
791 Err(_) => return NotifAction::Errno(libc::EIO),
792 };
793
794 let ret = unsafe {
798 libc::sendto(
799 dup_fd.as_raw_fd(),
800 data.as_ptr() as *const libc::c_void,
801 data.len(),
802 flags,
803 addr_bytes.as_ptr() as *const libc::sockaddr,
804 addr_len as libc::socklen_t,
805 )
806 };
807
808 if ret >= 0 {
810 NotifAction::ReturnValue(ret as i64)
811 } else {
812 let errno = unsafe { *libc::__errno_location() };
813 NotifAction::Errno(errno)
814 }
815 } else {
816 NotifAction::Continue
818 }
819}
820
821async fn sendmsg_on_behalf(
832 notif: &SeccompNotif,
833 ctx: &Arc<SupervisorCtx>,
834 notif_fd: RawFd,
835) -> NotifAction {
836 let args = ¬if.data.args;
837 let sockfd = args[0] as i32;
838 let msghdr_ptr = args[1];
839 let flags = args[2] as i32;
840
841 match prescan_msghdr(notif, notif_fd, msghdr_ptr) {
846 PrescanResult::ContinueWholeCall => return NotifAction::Continue,
847 PrescanResult::Errno(e) => return NotifAction::Errno(e),
848 PrescanResult::OnBehalf => {}
849 }
850
851 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
852 Ok(fd) => fd,
853 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
854 };
855 let protocol = match query_socket_protocol(dup_fd.as_raw_fd()) {
856 Some(p) => p,
857 None => return NotifAction::Errno(ECONNREFUSED),
858 };
859
860 match send_msghdr_on_behalf(notif, ctx, notif_fd, &dup_fd, protocol, msghdr_ptr, flags).await {
861 Ok(n) => NotifAction::ReturnValue(n as i64),
862 Err(errno) => NotifAction::Errno(errno),
863 }
864}
865
866#[derive(Clone, Copy)]
871enum PrescanResult {
872 OnBehalf,
875 ContinueWholeCall,
881 Errno(i32),
884}
885
886fn prescan_msghdr(
891 notif: &SeccompNotif,
892 notif_fd: RawFd,
893 msghdr_ptr: u64,
894) -> PrescanResult {
895 let msghdr_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msghdr_ptr, 56) {
896 Ok(b) if b.len() >= 56 => b,
897 _ => return PrescanResult::Errno(libc::EFAULT),
898 };
899 let msg_name_ptr = u64::from_ne_bytes(msghdr_bytes[0..8].try_into().unwrap());
900 if msg_name_ptr == 0 {
901 return PrescanResult::ContinueWholeCall;
902 }
903 let msg_namelen = u32::from_ne_bytes(msghdr_bytes[8..12].try_into().unwrap());
904 let addr_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msg_name_ptr, msg_namelen as usize) {
905 Ok(b) => b,
906 Err(_) => return PrescanResult::Errno(libc::EIO),
907 };
908 if parse_ip_from_sockaddr(&addr_bytes).is_none() {
909 return PrescanResult::ContinueWholeCall;
910 }
911 PrescanResult::OnBehalf
912}
913
914async fn send_msghdr_on_behalf(
927 notif: &SeccompNotif,
928 ctx: &Arc<SupervisorCtx>,
929 notif_fd: RawFd,
930 dup_fd: &std::os::unix::io::OwnedFd,
931 protocol: Protocol,
932 msghdr_ptr: u64,
933 flags: i32,
934) -> Result<isize, i32> {
935 let msghdr_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msghdr_ptr, 56) {
936 Ok(b) if b.len() >= 56 => b,
937 _ => return Err(libc::EFAULT),
938 };
939 let msg_name_ptr = u64::from_ne_bytes(msghdr_bytes[0..8].try_into().unwrap());
940 let msg_namelen = u32::from_ne_bytes(msghdr_bytes[8..12].try_into().unwrap());
941 let msg_iov_ptr = u64::from_ne_bytes(msghdr_bytes[16..24].try_into().unwrap());
942 let msg_iovlen = u64::from_ne_bytes(msghdr_bytes[24..32].try_into().unwrap());
943 let msg_control_ptr = u64::from_ne_bytes(msghdr_bytes[32..40].try_into().unwrap());
944 let msg_controllen = u64::from_ne_bytes(msghdr_bytes[40..48].try_into().unwrap());
945
946 let addr_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msg_name_ptr, msg_namelen as usize) {
947 Ok(b) => b,
948 Err(_) => return Err(libc::EIO),
949 };
950 let ip = match parse_ip_from_sockaddr(&addr_bytes) {
951 Some(ip) => ip,
952 None => return Err(libc::EAFNOSUPPORT),
956 };
957 let dest_port = parse_port_from_sockaddr(&addr_bytes);
958
959 let ns = ctx.network.lock().await;
960 let live_policy = {
961 let pfs = ctx.policy_fn.lock().await;
962 pfs.live_policy.clone()
963 };
964 let effective = ns.effective_network_policy(notif.pid, protocol, live_policy.as_ref());
965 if !matches!(effective, crate::seccomp::notif::NetworkPolicy::Unrestricted) {
966 match dest_port {
967 Some(p) if !effective.allows(ip, p) => return Err(ECONNREFUSED),
968 None => return Err(ECONNREFUSED),
969 Some(_) => {}
970 }
971 }
972 drop(ns);
973
974 let iovlen = (msg_iovlen as usize).min(1024);
975 let iov_size = iovlen * 16;
976 let iov_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msg_iov_ptr, iov_size) {
977 Ok(b) => b,
978 Err(_) => return Err(libc::EIO),
979 };
980 let mut data_bufs: Vec<Vec<u8>> = Vec::with_capacity(iovlen);
981 let mut local_iovs: Vec<libc::iovec> = Vec::with_capacity(iovlen);
982 for i in 0..iovlen {
983 let off = i * 16;
984 if off + 16 > iov_bytes.len() { break; }
985 let iov_base = u64::from_ne_bytes(iov_bytes[off..off + 8].try_into().unwrap());
986 let iov_len = u64::from_ne_bytes(iov_bytes[off + 8..off + 16].try_into().unwrap()) as usize;
987 if iov_len > MAX_SEND_BUF {
988 return Err(libc::EMSGSIZE);
989 }
990 if iov_base == 0 || iov_len == 0 {
991 data_bufs.push(Vec::new());
992 continue;
993 }
994 let buf = match read_child_mem(notif_fd, notif.id, notif.pid, iov_base, iov_len) {
995 Ok(b) => b,
996 Err(_) => return Err(libc::EIO),
997 };
998 data_bufs.push(buf);
999 }
1000 for buf in &data_bufs {
1001 local_iovs.push(libc::iovec {
1002 iov_base: buf.as_ptr() as *mut libc::c_void,
1003 iov_len: buf.len(),
1004 });
1005 }
1006
1007 let control_buf = if msg_control_ptr != 0 && msg_controllen > 0 {
1008 let len = (msg_controllen as usize).min(4096);
1009 read_child_mem(notif_fd, notif.id, notif.pid, msg_control_ptr, len).ok()
1010 } else {
1011 None
1012 };
1013
1014 let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
1015 msg.msg_name = addr_bytes.as_ptr() as *mut libc::c_void;
1016 msg.msg_namelen = addr_bytes.len() as u32;
1017 msg.msg_iov = local_iovs.as_mut_ptr();
1018 msg.msg_iovlen = local_iovs.len();
1019 if let Some(ref ctrl) = control_buf {
1020 msg.msg_control = ctrl.as_ptr() as *mut libc::c_void;
1021 msg.msg_controllen = ctrl.len();
1022 }
1023
1024 let ret = unsafe { libc::sendmsg(dup_fd.as_raw_fd(), &msg, flags) };
1025 if ret >= 0 {
1026 Ok(ret)
1027 } else {
1028 Err(unsafe { *libc::__errno_location() })
1029 }
1030}
1031
1032const MMSGHDR_SIZE: usize = 64;
1040const MSG_LEN_OFFSET: usize = 56;
1041const MAX_MMSGHDR_ENTRIES: usize = 256;
1046
1047async fn sendmmsg_on_behalf(
1060 notif: &SeccompNotif,
1061 ctx: &Arc<SupervisorCtx>,
1062 notif_fd: RawFd,
1063) -> NotifAction {
1064 let args = ¬if.data.args;
1065 let sockfd = args[0] as i32;
1066 let msgvec_ptr = args[1];
1067 let vlen = (args[2] as u32 as usize).min(MAX_MMSGHDR_ENTRIES);
1068 let flags = args[3] as i32;
1069
1070 if vlen == 0 {
1071 return NotifAction::ReturnValue(0);
1072 }
1073
1074 for i in 0..vlen {
1080 let entry_ptr = msgvec_ptr + (i * MMSGHDR_SIZE) as u64;
1081 match prescan_msghdr(notif, notif_fd, entry_ptr) {
1082 PrescanResult::OnBehalf => continue,
1083 PrescanResult::ContinueWholeCall => return NotifAction::Continue,
1084 PrescanResult::Errno(e) => return NotifAction::Errno(e),
1085 }
1086 }
1087
1088 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
1089 Ok(fd) => fd,
1090 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
1091 };
1092 let protocol = match query_socket_protocol(dup_fd.as_raw_fd()) {
1093 Some(p) => p,
1094 None => return NotifAction::Errno(ECONNREFUSED),
1095 };
1096
1097 let mut sent: usize = 0;
1098 let mut first_errno: Option<i32> = None;
1099
1100 for i in 0..vlen {
1101 let entry_ptr = msgvec_ptr + (i * MMSGHDR_SIZE) as u64;
1102 match send_msghdr_on_behalf(notif, ctx, notif_fd, &dup_fd, protocol, entry_ptr, flags).await {
1103 Ok(n) => {
1104 let bytes = (n as u32).to_ne_bytes();
1105 let _ = write_child_mem(
1106 notif_fd, notif.id, notif.pid,
1107 entry_ptr + MSG_LEN_OFFSET as u64,
1108 &bytes,
1109 );
1110 sent += 1;
1111 }
1112 Err(errno) => {
1113 first_errno = Some(errno);
1114 break;
1115 }
1116 }
1117 }
1118
1119 if sent > 0 {
1120 NotifAction::ReturnValue(sent as i64)
1121 } else {
1122 NotifAction::Errno(first_errno.unwrap_or(ECONNREFUSED))
1126 }
1127}
1128
1129pub(crate) async fn handle_net(
1157 notif: &SeccompNotif,
1158 ctx: &Arc<SupervisorCtx>,
1159 notif_fd: RawFd,
1160) -> NotifAction {
1161 let nr = notif.data.nr as i64;
1162
1163 if nr == libc::SYS_connect {
1164 connect_on_behalf(notif, ctx, notif_fd).await
1165 } else if nr == libc::SYS_sendto {
1166 sendto_on_behalf(notif, ctx, notif_fd).await
1167 } else if nr == libc::SYS_sendmsg {
1168 sendmsg_on_behalf(notif, ctx, notif_fd).await
1169 } else if nr == libc::SYS_sendmmsg {
1170 sendmmsg_on_behalf(notif, ctx, notif_fd).await
1171 } else {
1172 NotifAction::Continue
1173 }
1174}
1175
1176pub struct ResolvedNetAllow {
1182 pub per_ip: HashMap<IpAddr, HashSet<u16>>,
1186 pub per_ip_all_ports: HashSet<IpAddr>,
1191 pub cidrs: Vec<(IpCidr, crate::seccomp::notif::PortAllow)>,
1195 pub any_ip_ports: HashSet<u16>,
1197 pub any_ip_all_ports: bool,
1201}
1202
1203pub struct ResolvedNetAllowSet {
1209 pub tcp: ResolvedNetAllow,
1210 pub udp: ResolvedNetAllow,
1211 pub icmp: ResolvedNetAllow,
1212 pub concrete_host_entries: String,
1218}
1219
1220pub async fn resolve_net_allow(
1229 rules: &[NetAllow],
1230) -> io::Result<ResolvedNetAllowSet> {
1231 use crate::seccomp::notif::PortAllow;
1232 let per_proto = |target: Protocol| async move {
1233 let mut per_ip: HashMap<IpAddr, HashSet<u16>> = HashMap::new();
1234 let mut per_ip_all_ports: HashSet<IpAddr> = HashSet::new();
1235 let mut cidrs: Vec<(IpCidr, PortAllow)> = Vec::new();
1236 let mut any_ip_ports: HashSet<u16> = HashSet::new();
1237 let mut any_ip_all_ports = false;
1238 let mut local_etc_hosts = String::new();
1239
1240 for rule in rules.iter().filter(|r| r.protocol == target) {
1241 match &rule.target {
1242 NetTarget::AnyIp => {
1243 if rule.all_ports || target == Protocol::Icmp {
1244 any_ip_all_ports = true;
1247 } else {
1248 for &p in &rule.ports {
1249 any_ip_ports.insert(p);
1250 }
1251 }
1252 }
1253 NetTarget::Cidr(c) => {
1254 let pa = if rule.all_ports || target == Protocol::Icmp {
1257 PortAllow::Any
1258 } else {
1259 PortAllow::Specific(rule.ports.iter().copied().collect())
1260 };
1261 cidrs.push((*c, pa));
1262 }
1263 NetTarget::Host(host) => {
1264 let addr = format!("{}:0", host);
1265 let resolved = tokio::net::lookup_host(addr.as_str()).await.map_err(|e| {
1266 io::Error::new(
1267 e.kind(),
1268 format!("failed to resolve host '{}': {}", host, e),
1269 )
1270 })?;
1271 for socket_addr in resolved {
1272 let ip = socket_addr.ip();
1273 if rule.all_ports || target == Protocol::Icmp {
1274 per_ip_all_ports.insert(ip);
1275 per_ip.entry(ip).or_default();
1276 } else {
1277 let entry = per_ip.entry(ip).or_default();
1278 for &p in &rule.ports {
1279 entry.insert(p);
1280 }
1281 }
1282 local_etc_hosts.push_str(&format!("{} {}\n", ip, host));
1283 }
1284 }
1285 }
1286 }
1287
1288 Ok::<_, io::Error>((
1289 ResolvedNetAllow {
1290 per_ip,
1291 per_ip_all_ports,
1292 cidrs,
1293 any_ip_ports,
1294 any_ip_all_ports,
1295 },
1296 local_etc_hosts,
1297 ))
1298 };
1299
1300 let (tcp, tcp_eh) = per_proto(Protocol::Tcp).await?;
1301 let (udp, udp_eh) = per_proto(Protocol::Udp).await?;
1302 let (icmp, icmp_eh) = per_proto(Protocol::Icmp).await?;
1303
1304 let mut concrete_host_entries = String::new();
1305 for chunk in [tcp_eh, udp_eh, icmp_eh] {
1306 concrete_host_entries.push_str(&chunk);
1307 }
1308
1309 Ok(ResolvedNetAllowSet {
1310 tcp,
1311 udp,
1312 icmp,
1313 concrete_host_entries,
1314 })
1315}
1316
1317pub struct ResolvedNetDenySet {
1319 pub tcp: crate::seccomp::notif::NetworkPolicy,
1320 pub udp: crate::seccomp::notif::NetworkPolicy,
1321 pub icmp: crate::seccomp::notif::NetworkPolicy,
1322}
1323
1324pub fn resolve_net_deny(rules: &[NetDeny]) -> ResolvedNetDenySet {
1327 use crate::seccomp::notif::{NetworkPolicy, PortAllow};
1328
1329 let per_proto = |target: Protocol| -> NetworkPolicy {
1330 let mut cidrs: Vec<(IpCidr, PortAllow)> = Vec::new();
1331 let mut any_ip_ports: HashSet<u16> = HashSet::new();
1332 let mut deny_all = false;
1333 let mut saw_rule = false;
1334
1335 for rule in rules.iter().filter(|r| r.protocol == target) {
1336 saw_rule = true;
1337 match &rule.target {
1338 NetTarget::AnyIp => {
1339 if rule.all_ports || target == Protocol::Icmp {
1340 deny_all = true;
1341 } else {
1342 for &p in &rule.ports {
1343 any_ip_ports.insert(p);
1344 }
1345 }
1346 }
1347 NetTarget::Cidr(c) => {
1348 let pa = if rule.all_ports || target == Protocol::Icmp {
1349 PortAllow::Any
1350 } else {
1351 PortAllow::Specific(rule.ports.iter().copied().collect())
1352 };
1353 cidrs.push((*c, pa));
1354 }
1355 NetTarget::Host(_) => unreachable!("net-deny rejects hostnames"),
1358 }
1359 }
1360
1361 if !saw_rule {
1362 NetworkPolicy::Unrestricted
1363 } else {
1364 NetworkPolicy::DenyList {
1365 cidrs,
1366 any_ip_ports,
1367 deny_all,
1368 }
1369 }
1370 };
1371
1372 ResolvedNetDenySet {
1373 tcp: per_proto(Protocol::Tcp),
1374 udp: per_proto(Protocol::Udp),
1375 icmp: per_proto(Protocol::Icmp),
1376 }
1377}
1378
1379pub fn compose_virtual_etc_hosts(
1396 chroot_root: Option<&std::path::Path>,
1397 concrete_host_entries: &str,
1398) -> String {
1399 let mut out = String::new();
1400 let mut has_v4_localhost = false;
1401 let mut has_v6_localhost = false;
1402
1403 if let Some(root) = chroot_root {
1404 if let Ok(image) = std::fs::read_to_string(root.join("etc").join("hosts")) {
1405 for line in image.lines() {
1406 let stripped = line.split('#').next().unwrap_or("");
1409 let mut parts = stripped.split_whitespace();
1410 let Some(ip) = parts.next() else { continue };
1411 for name in parts {
1412 if name == "localhost" {
1413 if ip == "127.0.0.1" {
1414 has_v4_localhost = true;
1415 } else if ip == "::1" {
1416 has_v6_localhost = true;
1417 }
1418 }
1419 }
1420 }
1421 out.push_str(&image);
1422 if !out.is_empty() && !out.ends_with('\n') {
1423 out.push('\n');
1424 }
1425 }
1426 }
1427
1428 if !has_v4_localhost {
1429 out.push_str("127.0.0.1 localhost\n");
1430 }
1431 if !has_v6_localhost {
1432 out.push_str("::1 localhost\n");
1433 }
1434 out.push_str(concrete_host_entries);
1435 out
1436}
1437
1438#[cfg(test)]
1443mod tests {
1444 use super::*;
1445
1446 #[test]
1449 fn netallow_parse_concrete_host_port() {
1450 let r = NetRule::parse_allow("example.com:443").unwrap();
1451 assert!(matches!(&r.target, NetTarget::Host(h) if h == "example.com"));
1452 assert_eq!(r.ports, vec![443]);
1453 assert!(!r.all_ports);
1454 }
1455
1456 #[test]
1457 fn netallow_parse_any_host_port() {
1458 let r = NetRule::parse_allow(":8080").unwrap();
1459 assert_eq!(r.target, NetTarget::AnyIp);
1460 assert_eq!(r.ports, vec![8080]);
1461 assert!(!r.all_ports);
1462
1463 let r = NetRule::parse_allow("*:8080").unwrap();
1464 assert_eq!(r.target, NetTarget::AnyIp);
1465 assert_eq!(r.ports, vec![8080]);
1466 assert!(!r.all_ports);
1467 }
1468
1469 #[test]
1470 fn netallow_parse_multiple_ports() {
1471 let r = NetRule::parse_allow("github.com:22,80,443").unwrap();
1472 assert!(matches!(&r.target, NetTarget::Host(h) if h == "github.com"));
1473 assert_eq!(r.ports, vec![22, 80, 443]);
1474 assert!(!r.all_ports);
1475 }
1476
1477 #[test]
1478 fn netallow_parse_wildcard_any_host_any_port_colon() {
1479 let r = NetRule::parse_allow(":*").unwrap();
1480 assert_eq!(r.target, NetTarget::AnyIp);
1481 assert!(r.ports.is_empty());
1482 assert!(r.all_ports);
1483 }
1484
1485 #[test]
1486 fn netallow_parse_wildcard_any_host_any_port_star() {
1487 let r = NetRule::parse_allow("*:*").unwrap();
1488 assert_eq!(r.target, NetTarget::AnyIp);
1489 assert!(r.ports.is_empty());
1490 assert!(r.all_ports);
1491 }
1492
1493 #[test]
1494 fn netallow_parse_wildcard_concrete_host_any_port() {
1495 let r = NetRule::parse_allow("example.com:*").unwrap();
1496 assert!(matches!(&r.target, NetTarget::Host(h) if h == "example.com"));
1497 assert!(r.ports.is_empty());
1498 assert!(r.all_ports);
1499 }
1500
1501 #[test]
1502 fn netallow_parse_rejects_mixed_wildcard_and_concrete() {
1503 let err = NetRule::parse_allow("example.com:80,*").unwrap_err();
1507 assert!(format!("{}", err).contains("cannot mix"));
1508 let err = NetRule::parse_allow("example.com:*,80").unwrap_err();
1509 assert!(format!("{}", err).contains("cannot mix"));
1510 }
1511
1512 #[test]
1513 fn netallow_parse_rejects_port_zero() {
1514 let err = NetRule::parse_allow("example.com:0").unwrap_err();
1515 assert!(format!("{}", err).contains("port 0"));
1516 }
1517
1518 #[test]
1519 fn netallow_parse_rejects_empty_port() {
1520 let err = NetRule::parse_allow("example.com:").unwrap_err();
1521 assert!(format!("{}", err).contains("invalid port"));
1522 }
1523
1524 #[test]
1525 fn netallow_bare_host_is_all_ports() {
1526 let r = NetRule::parse_allow("example.com").unwrap();
1529 assert!(matches!(&r.target, NetTarget::Host(h) if h == "example.com"));
1530 assert!(r.all_ports);
1531 assert!(r.ports.is_empty());
1532 }
1533
1534 #[test]
1535 fn netallow_bare_star_is_any_host_all_ports() {
1536 let r = NetRule::parse_allow("*").unwrap();
1537 assert_eq!(r.target, NetTarget::AnyIp);
1538 assert!(r.all_ports);
1539 assert!(r.ports.is_empty());
1540 }
1541
1542 #[test]
1543 fn netallow_empty_spec_rejected() {
1544 assert!(NetRule::parse_allow("").is_err());
1545 assert!(NetRule::parse_allow("tcp://").is_err());
1546 }
1547
1548 #[test]
1549 fn netallow_cidr_target_with_port() {
1550 let r = NetRule::parse_allow("10.0.0.0/8:80").unwrap();
1553 assert!(matches!(&r.target, NetTarget::Cidr(c) if !c.is_single_host()));
1554 assert_eq!(r.ports, vec![80]);
1555 assert!(!r.all_ports);
1556 }
1557
1558 #[test]
1559 fn netallow_ipv6_literal_and_bracket() {
1560 let lo: std::net::IpAddr = "::1".parse().unwrap();
1561 let r = NetRule::parse_allow("::1").unwrap();
1563 assert!(matches!(&r.target, NetTarget::Cidr(c) if c.addr == lo && c.is_single_host()));
1564 assert!(r.all_ports);
1565 let r = NetRule::parse_allow("[::1]:443").unwrap();
1567 assert!(matches!(&r.target, NetTarget::Cidr(c) if c.addr == lo && c.is_single_host()));
1568 assert_eq!(r.ports, vec![443]);
1569 let r = NetRule::parse_allow("fc00::/7").unwrap();
1571 assert!(matches!(&r.target, NetTarget::Cidr(c) if !c.is_single_host()));
1572 assert!(r.all_ports);
1573 }
1574
1575 #[tokio::test]
1576 async fn test_resolve_net_allow_cidr_no_dns() {
1577 let rules = vec![
1580 NetAllow { protocol: Protocol::Tcp, target: NetTarget::Cidr(IpCidr::parse("10.0.0.0/8").unwrap()), ports: vec![80], all_ports: false },
1581 NetAllow { protocol: Protocol::Tcp, target: NetTarget::Cidr(IpCidr::parse("1.2.3.4").unwrap()), ports: vec![], all_ports: true },
1582 ];
1583 let resolved = resolve_net_allow(&rules).await.unwrap();
1584 assert_eq!(resolved.tcp.cidrs.len(), 2);
1585 assert!(resolved.tcp.per_ip.is_empty());
1586 assert!(resolved.concrete_host_entries.is_empty());
1587 }
1588
1589 #[test]
1590 fn netallow_parse_repeated_wildcard_is_idempotent() {
1591 let r = NetRule::parse_allow(":*,*").unwrap();
1594 assert!(r.all_ports);
1595 assert!(r.ports.is_empty());
1596 }
1597
1598 #[test]
1601 fn netallow_bare_form_defaults_to_tcp() {
1602 let r = NetRule::parse_allow("example.com:443").unwrap();
1603 assert_eq!(r.protocol, Protocol::Tcp);
1604 }
1605
1606 #[test]
1607 fn netallow_explicit_tcp_scheme() {
1608 let r = NetRule::parse_allow("tcp://example.com:443").unwrap();
1609 assert_eq!(r.protocol, Protocol::Tcp);
1610 assert!(matches!(&r.target, NetTarget::Host(h) if h == "example.com"));
1611 assert_eq!(r.ports, vec![443]);
1612 }
1613
1614 #[test]
1615 fn netallow_udp_scheme_with_host_port() {
1616 let r = NetRule::parse_allow("udp://1.1.1.1:53").unwrap();
1617 assert_eq!(r.protocol, Protocol::Udp);
1618 let one: std::net::IpAddr = "1.1.1.1".parse().unwrap();
1620 assert!(matches!(&r.target, NetTarget::Cidr(c) if c.addr == one && c.is_single_host()));
1621 assert_eq!(r.ports, vec![53]);
1622 }
1623
1624 #[test]
1625 fn netallow_udp_wildcard_any_anywhere() {
1626 let r = NetRule::parse_allow("udp://*:*").unwrap();
1628 assert_eq!(r.protocol, Protocol::Udp);
1629 assert_eq!(r.target, NetTarget::AnyIp);
1630 assert!(r.all_ports);
1631 }
1632
1633 #[test]
1634 fn netallow_icmp_scheme_with_host() {
1635 let r = NetRule::parse_allow("icmp://github.com").unwrap();
1636 assert_eq!(r.protocol, Protocol::Icmp);
1637 assert!(matches!(&r.target, NetTarget::Host(h) if h == "github.com"));
1638 assert!(r.ports.is_empty());
1639 assert!(r.all_ports);
1641 }
1642
1643 #[test]
1644 fn netallow_icmp_wildcard() {
1645 let r = NetRule::parse_allow("icmp://*").unwrap();
1648 assert_eq!(r.protocol, Protocol::Icmp);
1649 assert_eq!(r.target, NetTarget::AnyIp);
1650 }
1651
1652 #[test]
1653 fn netallow_icmp_rejects_port() {
1654 let err = NetRule::parse_allow("icmp://github.com:80").unwrap_err();
1658 assert!(format!("{}", err).contains("icmp rule takes no port"));
1659 }
1660
1661 #[test]
1662 fn netallow_icmp_rejects_empty_body() {
1663 let err = NetRule::parse_allow("icmp://").unwrap_err();
1664 assert!(format!("{}", err).contains("needs a host/IP or `*`"));
1665 }
1666
1667 #[test]
1668 fn netallow_unknown_scheme_rejected() {
1669 for spec in ["sctp://host:1234", "icmp-raw://*"] {
1672 let err = NetRule::parse_allow(spec).unwrap_err();
1673 assert!(format!("{}", err).contains("unknown scheme"), "spec: {}", spec);
1674 }
1675 }
1676
1677 #[tokio::test]
1678 async fn test_resolve_net_allow_empty() {
1679 let resolved = resolve_net_allow(&[]).await.unwrap();
1680 assert!(resolved.tcp.per_ip.is_empty());
1681 assert!(resolved.tcp.any_ip_ports.is_empty());
1682 assert!(resolved.udp.per_ip.is_empty());
1683 assert!(resolved.icmp.per_ip.is_empty());
1684 assert!(resolved.concrete_host_entries.is_empty());
1686 }
1687
1688 #[tokio::test]
1689 async fn test_resolve_net_allow_concrete_host() {
1690 let rules = vec![NetAllow {
1691 protocol: Protocol::Tcp,
1692 target: NetTarget::Host("localhost".to_string()),
1693 ports: vec![80, 443],
1694 all_ports: false,
1695 }];
1696 let resolved = resolve_net_allow(&rules).await.unwrap();
1697 assert!(!resolved.tcp.per_ip.is_empty());
1700 for ports in resolved.tcp.per_ip.values() {
1701 assert!(ports.contains(&80));
1702 assert!(ports.contains(&443));
1703 }
1704 assert!(resolved.udp.per_ip.is_empty());
1705 assert!(resolved.icmp.per_ip.is_empty());
1706 assert!(resolved.concrete_host_entries.contains("127.0.0.1 localhost"));
1708 }
1709
1710 #[tokio::test]
1711 async fn test_resolve_net_allow_any_ip() {
1712 let rules = vec![NetAllow {
1713 protocol: Protocol::Tcp,
1714 target: NetTarget::AnyIp,
1715 ports: vec![8080],
1716 all_ports: false,
1717 }];
1718 let resolved = resolve_net_allow(&rules).await.unwrap();
1719 assert!(resolved.tcp.per_ip.is_empty());
1720 assert!(resolved.tcp.any_ip_ports.contains(&8080));
1721 assert!(!resolved.tcp.any_ip_all_ports);
1722 assert!(resolved.concrete_host_entries.is_empty());
1724 }
1725
1726 #[tokio::test]
1727 async fn test_resolve_net_allow_any_ip_all_ports() {
1728 let rules = vec![NetAllow {
1730 protocol: Protocol::Tcp,
1731 target: NetTarget::AnyIp,
1732 ports: vec![],
1733 all_ports: true,
1734 }];
1735 let resolved = resolve_net_allow(&rules).await.unwrap();
1736 assert!(resolved.tcp.any_ip_all_ports);
1737 assert!(resolved.tcp.per_ip.is_empty());
1738 assert!(resolved.tcp.per_ip_all_ports.is_empty());
1739 assert!(resolved.tcp.any_ip_ports.is_empty());
1740 assert!(!resolved.udp.any_ip_all_ports);
1742 assert!(!resolved.icmp.any_ip_all_ports);
1743 }
1744
1745 #[tokio::test]
1746 async fn test_resolve_net_allow_concrete_host_all_ports() {
1747 let rules = vec![NetAllow {
1749 protocol: Protocol::Tcp,
1750 target: NetTarget::Host("localhost".to_string()),
1751 ports: vec![],
1752 all_ports: true,
1753 }];
1754 let resolved = resolve_net_allow(&rules).await.unwrap();
1755 assert!(!resolved.tcp.any_ip_all_ports);
1756 assert!(
1757 !resolved.tcp.per_ip_all_ports.is_empty(),
1758 "localhost should resolve to at least one IP marked as any-port"
1759 );
1760 for ip in resolved.tcp.per_ip_all_ports.iter() {
1761 assert!(resolved.tcp.per_ip.contains_key(ip));
1762 }
1763 assert!(resolved.concrete_host_entries.contains("localhost"));
1764 }
1765
1766 #[tokio::test]
1767 async fn test_resolve_net_allow_mixed_wildcard_and_concrete() {
1768 let rules = vec![
1773 NetAllow {
1774 protocol: Protocol::Tcp,
1775 target: NetTarget::AnyIp,
1776 ports: vec![],
1777 all_ports: true,
1778 },
1779 NetAllow {
1780 protocol: Protocol::Tcp,
1781 target: NetTarget::Host("localhost".to_string()),
1782 ports: vec![22],
1783 all_ports: false,
1784 },
1785 ];
1786 let resolved = resolve_net_allow(&rules).await.unwrap();
1787 assert!(resolved.tcp.any_ip_all_ports);
1788 assert!(!resolved.tcp.per_ip.is_empty());
1789 }
1790
1791 #[tokio::test]
1796 async fn test_resolve_per_protocol_isolation() {
1797 let rules = vec![
1800 NetAllow {
1801 protocol: Protocol::Tcp,
1802 target: NetTarget::Host("localhost".to_string()),
1803 ports: vec![443],
1804 all_ports: false,
1805 },
1806 NetAllow {
1807 protocol: Protocol::Udp,
1808 target: NetTarget::AnyIp,
1809 ports: vec![53],
1810 all_ports: false,
1811 },
1812 ];
1813 let resolved = resolve_net_allow(&rules).await.unwrap();
1814 assert!(
1815 !resolved.tcp.per_ip.is_empty(),
1816 "TCP rule should populate tcp set"
1817 );
1818 assert!(
1819 resolved.udp.any_ip_ports.contains(&53),
1820 "UDP rule should populate udp set"
1821 );
1822 for ports in resolved.tcp.per_ip.values() {
1825 assert!(!ports.contains(&53), "UDP port leaked into TCP set");
1826 }
1827 assert!(!resolved.udp.any_ip_ports.contains(&443), "TCP port leaked into UDP set");
1828 }
1829
1830 #[tokio::test]
1831 async fn test_resolve_icmp_no_ports() {
1832 let rules = vec![NetAllow {
1835 protocol: Protocol::Icmp,
1836 target: NetTarget::Host("localhost".to_string()),
1837 ports: vec![],
1838 all_ports: false,
1839 }];
1840 let resolved = resolve_net_allow(&rules).await.unwrap();
1841 assert!(
1842 !resolved.icmp.per_ip.is_empty(),
1843 "icmp host should populate per_ip"
1844 );
1845 assert!(
1846 !resolved.icmp.per_ip_all_ports.is_empty(),
1847 "icmp host should mark per_ip_all_ports (no port check)"
1848 );
1849 assert!(resolved.icmp.any_ip_ports.is_empty());
1850 assert!(resolved.tcp.per_ip.is_empty());
1852 assert!(resolved.udp.per_ip.is_empty());
1853 }
1854
1855 #[tokio::test]
1856 async fn test_resolve_icmp_wildcard() {
1857 let rules = vec![NetAllow {
1859 protocol: Protocol::Icmp,
1860 target: NetTarget::AnyIp,
1861 ports: vec![],
1862 all_ports: false,
1863 }];
1864 let resolved = resolve_net_allow(&rules).await.unwrap();
1865 assert!(resolved.icmp.any_ip_all_ports);
1866 assert!(!resolved.tcp.any_ip_all_ports);
1867 }
1868
1869 use std::io::Write;
1874
1875 fn temp_rootfs_with_hosts(name: &str, hosts_content: Option<&str>) -> std::path::PathBuf {
1876 let dir = std::env::temp_dir().join(format!(
1877 "sandlock-test-compose-hosts-{}-{}",
1878 name, std::process::id()
1879 ));
1880 let _ = std::fs::create_dir_all(dir.join("etc"));
1881 if let Some(content) = hosts_content {
1882 let mut f = std::fs::File::create(dir.join("etc").join("hosts")).unwrap();
1883 f.write_all(content.as_bytes()).unwrap();
1884 }
1885 dir
1886 }
1887
1888 #[test]
1889 fn compose_no_chroot_emits_loopback_base() {
1890 let out = compose_virtual_etc_hosts(None, "");
1893 assert_eq!(out, "127.0.0.1 localhost\n::1 localhost\n");
1894 }
1895
1896 #[test]
1897 fn compose_no_chroot_appends_concrete_entries() {
1898 let out = compose_virtual_etc_hosts(None, "10.0.0.1 api\n");
1899 assert_eq!(out, "127.0.0.1 localhost\n::1 localhost\n10.0.0.1 api\n");
1900 }
1901
1902 #[test]
1903 fn compose_chroot_seeds_from_image_and_injects_missing_loopback() {
1904 let rootfs = temp_rootfs_with_hosts(
1908 "no-localhost",
1909 Some("10.0.0.5 myimage.local\n"),
1910 );
1911 let out = compose_virtual_etc_hosts(Some(&rootfs), "");
1912 assert!(out.contains("10.0.0.5 myimage.local"), "image entry missing: {out}");
1913 assert!(out.contains("127.0.0.1 localhost"), "v4 loopback missing: {out}");
1914 assert!(out.contains("::1 localhost"), "v6 loopback missing: {out}");
1915 let _ = std::fs::remove_dir_all(&rootfs);
1916 }
1917
1918 #[test]
1919 fn compose_chroot_does_not_duplicate_existing_loopback() {
1920 let rootfs = temp_rootfs_with_hosts(
1922 "both-localhost",
1923 Some("127.0.0.1 localhost\n::1 localhost\n10.0.0.5 myimage.local\n"),
1924 );
1925 let out = compose_virtual_etc_hosts(Some(&rootfs), "");
1926 assert_eq!(out.matches("127.0.0.1 localhost").count(), 1, "v4 dup'd: {out}");
1927 assert_eq!(out.matches("::1 localhost").count(), 1, "v6 dup'd: {out}");
1928 assert!(out.contains("10.0.0.5 myimage.local"));
1929 let _ = std::fs::remove_dir_all(&rootfs);
1930 }
1931
1932 #[test]
1933 fn compose_chroot_injects_only_missing_family() {
1934 let rootfs = temp_rootfs_with_hosts(
1936 "only-v4-localhost",
1937 Some("127.0.0.1 localhost myimage\n"),
1938 );
1939 let out = compose_virtual_etc_hosts(Some(&rootfs), "");
1940 assert_eq!(out.matches("127.0.0.1 localhost").count(), 1);
1941 assert!(out.contains("::1 localhost"), "v6 loopback should be injected: {out}");
1942 let _ = std::fs::remove_dir_all(&rootfs);
1943 }
1944
1945 #[test]
1946 fn compose_chroot_missing_file_falls_back_to_loopback() {
1947 let rootfs = temp_rootfs_with_hosts("no-file", None);
1950 let out = compose_virtual_etc_hosts(Some(&rootfs), "10.0.0.1 api\n");
1951 assert_eq!(out, "127.0.0.1 localhost\n::1 localhost\n10.0.0.1 api\n");
1952 let _ = std::fs::remove_dir_all(&rootfs);
1953 }
1954
1955 #[test]
1956 fn compose_chroot_strips_inline_comments_when_detecting_loopback() {
1957 let rootfs = temp_rootfs_with_hosts(
1961 "with-comments",
1962 Some("127.0.0.1 # localhost is a comment here\n"),
1963 );
1964 let out = compose_virtual_etc_hosts(Some(&rootfs), "");
1965 assert!(
1967 out.lines().any(|l| l.trim() == "127.0.0.1 localhost"),
1968 "v4 loopback should still be injected: {out}"
1969 );
1970 let _ = std::fs::remove_dir_all(&rootfs);
1971 }
1972
1973 #[test]
1976 fn ipcidr_parse_bare_ipv4_is_host_route() {
1977 let c = IpCidr::parse("1.2.3.4").unwrap();
1978 assert_eq!(c.prefix_len, 32);
1979 assert!(c.contains("1.2.3.4".parse().unwrap()));
1980 assert!(!c.contains("1.2.3.5".parse().unwrap()));
1981 }
1982
1983 #[test]
1984 fn ipcidr_parse_ipv4_range_contains() {
1985 let c = IpCidr::parse("10.0.0.0/8").unwrap();
1986 assert!(c.contains("10.3.7.9".parse().unwrap()));
1987 assert!(!c.contains("11.0.0.1".parse().unwrap()));
1988 }
1989
1990 #[test]
1991 fn ipcidr_parse_ipv6_range_contains() {
1992 let c = IpCidr::parse("fc00::/7").unwrap();
1993 assert!(c.contains("fd00::1".parse().unwrap()));
1994 assert!(!c.contains("2001:db8::1".parse().unwrap()));
1995 }
1996
1997 #[test]
1998 fn ipcidr_zero_prefix_matches_all_same_family() {
1999 let c = IpCidr::parse("0.0.0.0/0").unwrap();
2000 assert!(c.contains("8.8.8.8".parse().unwrap()));
2001 assert!(!c.contains("::1".parse().unwrap())); }
2003
2004 #[test]
2005 fn ipcidr_rejects_hostname() {
2006 assert!(IpCidr::parse("example.com").is_err());
2007 }
2008
2009 #[test]
2010 fn ipcidr_rejects_oversized_prefix() {
2011 assert!(IpCidr::parse("10.0.0.0/33").is_err());
2012 assert!(IpCidr::parse("fc00::/129").is_err());
2013 }
2014
2015 #[test]
2018 fn netdeny_bare_cidr_is_all_ports_tcp() {
2019 let rule = NetRule::parse_deny("10.0.0.0/8").unwrap();
2020 assert_eq!(rule.protocol, Protocol::Tcp);
2021 assert!(matches!(rule.target, NetTarget::Cidr(_)));
2022 assert!(rule.all_ports);
2023 }
2024
2025 #[test]
2026 fn netdeny_bare_ip_is_host_route_all_ports() {
2027 let rule = NetRule::parse_deny("169.254.169.254").unwrap();
2028 match &rule.target {
2029 NetTarget::Cidr(c) => assert_eq!(c.prefix_len, 32),
2030 _ => panic!("expected cidr"),
2031 }
2032 assert!(rule.all_ports);
2033 }
2034
2035 #[test]
2036 fn netdeny_cidr_with_port() {
2037 let rule = NetRule::parse_deny("10.0.0.0/8:443").unwrap();
2038 assert_eq!(rule.ports, vec![443]);
2039 assert!(!rule.all_ports);
2040 }
2041
2042 #[test]
2043 fn netdeny_any_ip_port() {
2044 let rule = NetRule::parse_deny(":25").unwrap();
2045 assert!(matches!(rule.target, NetTarget::AnyIp));
2046 assert_eq!(rule.ports, vec![25]);
2047 }
2048
2049 #[test]
2050 fn netdeny_udp_scheme() {
2051 let rule = NetRule::parse_deny("udp://192.168.0.0/16:53").unwrap();
2052 assert_eq!(rule.protocol, Protocol::Udp);
2053 assert_eq!(rule.ports, vec![53]);
2054 }
2055
2056 #[test]
2057 fn netdeny_ipv6_bracket_port() {
2058 let rule = NetRule::parse_deny("[::1]:443").unwrap();
2059 assert_eq!(rule.ports, vec![443]);
2060 match &rule.target {
2061 NetTarget::Cidr(c) => assert_eq!(c.prefix_len, 128),
2062 _ => panic!("expected cidr"),
2063 }
2064 }
2065
2066 #[test]
2067 fn netdeny_rejects_hostname() {
2068 assert!(NetRule::parse_deny("evil.com:443").is_err());
2069 assert!(NetRule::parse_deny("evil.com").is_err());
2070 }
2071
2072 #[test]
2073 fn netdeny_bare_ipv6_address_all_ports() {
2074 let rule = NetRule::parse_deny("::1").unwrap();
2075 assert!(rule.all_ports);
2076 match &rule.target {
2077 NetTarget::Cidr(c) => assert_eq!(c.prefix_len, 128),
2078 _ => panic!("expected cidr"),
2079 }
2080 }
2081
2082 #[test]
2083 fn netdeny_bare_ipv6_cidr_all_ports() {
2084 let rule = NetRule::parse_deny("fc00::/7").unwrap();
2085 assert!(rule.all_ports);
2086 let ula: std::net::IpAddr = "fd00::1".parse().unwrap();
2087 assert!(matches!(&rule.target, NetTarget::Cidr(c) if c.contains(ula)));
2088 }
2089
2090 #[test]
2091 fn netdeny_empty_icmp_body_is_rejected() {
2092 assert!(NetRule::parse_deny("icmp://").is_err());
2093 }
2094
2095 #[test]
2096 fn netdeny_bare_star_is_any_ip_all_ports() {
2097 let rule = NetRule::parse_deny("*").unwrap();
2100 assert_eq!(rule.protocol, Protocol::Tcp);
2101 assert!(matches!(rule.target, NetTarget::AnyIp));
2102 assert!(rule.all_ports);
2103 assert!(rule.ports.is_empty());
2104 }
2105
2106 #[test]
2107 fn netdeny_udp_bare_star_all_ports() {
2108 let rule = NetRule::parse_deny("udp://*").unwrap();
2109 assert_eq!(rule.protocol, Protocol::Udp);
2110 assert!(matches!(rule.target, NetTarget::AnyIp));
2111 assert!(rule.all_ports);
2112 }
2113
2114 #[test]
2115 fn netdeny_empty_spec_rejected() {
2116 assert!(NetRule::parse_deny("").is_err());
2118 assert!(NetRule::parse_deny("udp://").is_err());
2119 }
2120
2121 #[test]
2124 fn resolve_net_deny_groups_per_protocol() {
2125 let rule = NetRule::parse_deny("10.0.0.0/8").unwrap();
2126 let set = resolve_net_deny(std::slice::from_ref(&rule));
2127 assert!(!set.tcp.allows("10.0.0.1".parse().unwrap(), 443));
2129 assert!(set.udp.allows("10.0.0.1".parse().unwrap(), 443));
2130 }
2131
2132 #[test]
2133 fn resolve_net_deny_any_ip_port() {
2134 let rule = NetRule::parse_deny(":25").unwrap();
2135 let set = resolve_net_deny(std::slice::from_ref(&rule));
2136 assert!(!set.tcp.allows("8.8.8.8".parse().unwrap(), 25));
2137 assert!(set.tcp.allows("8.8.8.8".parse().unwrap(), 80));
2138 }
2139}