1use std::collections::{HashMap, HashSet};
7use std::io;
8use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
9use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd, 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 if !ctx.policy.ip_connect_supervised(ip.is_loopback()) {
507 return NotifAction::Continue;
508 }
509 let dest_port = parse_port_from_sockaddr(&addr_bytes);
510 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
511 Ok(fd) => fd,
512 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
513 };
514 let protocol = match query_socket_protocol(dup_fd.as_raw_fd()) {
515 Some(p) => p,
516 None => return NotifAction::Errno(ECONNREFUSED),
517 };
518 let ns = ctx.network.lock().await;
519 let live_policy = {
520 let pfs = ctx.policy_fn.lock().await;
521 pfs.live_policy.clone()
522 };
523 let effective = ns.effective_network_policy(notif.pid, protocol, live_policy.as_ref());
524 match (effective, dest_port) {
525 (crate::seccomp::notif::NetworkPolicy::Unrestricted, _) => {
526 }
530 (policy, Some(p)) => {
531 if !policy.allows(ip, p) {
535 return NotifAction::Errno(ECONNREFUSED);
536 }
537 }
538 (_, None) => {
539 return NotifAction::Errno(ECONNREFUSED);
541 }
542 }
543 let http_acl_addr = ns.http_acl_addr;
545 let http_acl_intercept = dest_port.map_or(false, |p| ns.http_acl_ports.contains(&p));
546 let http_acl_orig_dest = ns.http_acl_orig_dest.clone();
547 let remapped_loopback_port = if ctx.policy.port_remap && ip.is_loopback() {
548 dest_port.and_then(|p| ns.port_map.get_real(p))
549 } else {
550 None
551 };
552
553 drop(ns);
554
555 let mut redirected = false;
557 let is_ipv6 = parse_ip_from_sockaddr(&addr_bytes)
558 .map_or(false, |ip| ip.is_ipv6());
559 let (mut connect_addr, connect_len) = if let Some(proxy_addr) = http_acl_addr {
560 if http_acl_intercept {
561 redirected = true;
562 if is_ipv6 {
563 let mut sa6: libc::sockaddr_in6 = unsafe { std::mem::zeroed() };
566 sa6.sin6_family = libc::AF_INET6 as u16;
567 sa6.sin6_port = proxy_addr.port().to_be();
568 let mapped = std::net::Ipv6Addr::from(
570 match proxy_addr {
571 std::net::SocketAddr::V4(v4) => v4.ip().to_ipv6_mapped(),
572 std::net::SocketAddr::V6(v6) => *v6.ip(),
573 }
574 );
575 sa6.sin6_addr.s6_addr = mapped.octets();
576 let bytes = unsafe {
577 std::slice::from_raw_parts(
578 &sa6 as *const _ as *const u8,
579 std::mem::size_of::<libc::sockaddr_in6>(),
580 )
581 }
582 .to_vec();
583 (bytes, std::mem::size_of::<libc::sockaddr_in6>() as u32)
584 } else {
585 let mut sa: libc::sockaddr_in = unsafe { std::mem::zeroed() };
587 sa.sin_family = libc::AF_INET as u16;
588 sa.sin_port = proxy_addr.port().to_be();
589 match proxy_addr {
590 std::net::SocketAddr::V4(v4) => {
591 sa.sin_addr.s_addr = u32::from_ne_bytes(v4.ip().octets());
592 }
593 std::net::SocketAddr::V6(_) => {
594 return NotifAction::Errno(libc::EAFNOSUPPORT);
596 }
597 }
598 let bytes = unsafe {
599 std::slice::from_raw_parts(
600 &sa as *const _ as *const u8,
601 std::mem::size_of::<libc::sockaddr_in>(),
602 )
603 }
604 .to_vec();
605 (bytes, std::mem::size_of::<libc::sockaddr_in>() as u32)
606 }
607 } else {
608 (addr_bytes.clone(), addr_len)
609 }
610 } else {
611 (addr_bytes.clone(), addr_len)
612 };
613 if !redirected {
614 if let Some(real_port) = remapped_loopback_port {
615 set_port_in_sockaddr(&mut connect_addr, real_port);
618 }
619 }
620
621 if redirected {
630 if let Some(ref orig_dest_map) = http_acl_orig_dest {
631 if let Some(orig_ip) = parse_ip_from_sockaddr(&addr_bytes) {
632 if is_ipv6 {
635 let mut bind_sa6: libc::sockaddr_in6 = unsafe { std::mem::zeroed() };
636 bind_sa6.sin6_family = libc::AF_INET6 as u16;
637 unsafe {
639 libc::bind(
640 dup_fd.as_raw_fd(),
641 &bind_sa6 as *const _ as *const libc::sockaddr,
642 std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
643 );
644 }
645 let mut local_sa6: libc::sockaddr_in6 = unsafe { std::mem::zeroed() };
646 let mut local_len: libc::socklen_t =
647 std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t;
648 let gs_ret = unsafe {
649 libc::getsockname(
650 dup_fd.as_raw_fd(),
651 &mut local_sa6 as *mut _ as *mut libc::sockaddr,
652 &mut local_len,
653 )
654 };
655 if gs_ret == 0 {
656 let local_port = u16::from_be(local_sa6.sin6_port);
657 let local_ip = Ipv6Addr::from(local_sa6.sin6_addr.s6_addr);
658 let local_addr = std::net::SocketAddr::V6(
659 std::net::SocketAddrV6::new(local_ip, local_port, 0, 0),
660 );
661 if let Ok(mut map) = orig_dest_map.write() {
662 map.insert(local_addr, orig_ip);
663 }
664 }
665 } else {
666 let mut bind_sa: libc::sockaddr_in = unsafe { std::mem::zeroed() };
667 bind_sa.sin_family = libc::AF_INET as u16;
668 unsafe {
670 libc::bind(
671 dup_fd.as_raw_fd(),
672 &bind_sa as *const _ as *const libc::sockaddr,
673 std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
674 );
675 }
676 let mut local_sa: libc::sockaddr_in = unsafe { std::mem::zeroed() };
677 let mut local_len: libc::socklen_t =
678 std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t;
679 let gs_ret = unsafe {
680 libc::getsockname(
681 dup_fd.as_raw_fd(),
682 &mut local_sa as *mut _ as *mut libc::sockaddr,
683 &mut local_len,
684 )
685 };
686 if gs_ret == 0 {
687 let local_port = u16::from_be(local_sa.sin_port);
688 let local_ip = Ipv4Addr::from(u32::from_be(local_sa.sin_addr.s_addr));
689 let local_addr = std::net::SocketAddr::V4(
690 std::net::SocketAddrV4::new(local_ip, local_port),
691 );
692 if let Ok(mut map) = orig_dest_map.write() {
693 map.insert(local_addr, orig_ip);
694 }
695 }
696 }
697 }
698 }
699 }
700
701 let ret = unsafe {
703 libc::connect(
704 dup_fd.as_raw_fd(),
705 connect_addr.as_ptr() as *const libc::sockaddr,
706 connect_len as libc::socklen_t,
707 )
708 };
709
710 if ret == 0 {
715 NotifAction::ReturnValue(0)
716 } else {
717 let errno = unsafe { *libc::__errno_location() };
718 NotifAction::Errno(errno)
719 }
720 } else {
722 match named_unix_socket_path(&addr_bytes) {
732 Some(path) if ctx.policy.has_unix_fs_gate => {
733 if ctx.policy.chroot_root.is_some() {
734 if path_under_any(&path, &ctx.policy.chroot_writable) {
739 NotifAction::Continue
740 } else {
741 NotifAction::Errno(libc::EACCES)
742 }
743 } else {
744 connect_named_unix_on_behalf(
748 notif.pid,
749 sockfd,
750 &path,
751 &ctx.policy.chroot_writable,
752 )
753 }
754 }
755 _ => NotifAction::Continue,
757 }
758 }
759}
760
761fn resolve_named_unix_target(
768 child_pid: u32,
769 sun_path: &std::path::Path,
770 writable: &[std::path::PathBuf],
771) -> Result<OwnedFd, NotifAction> {
772 let proc_path = format!("/proc/{}/root{}", child_pid, sun_path.display());
776 let c_proc = std::ffi::CString::new(proc_path)
777 .map_err(|_| NotifAction::Errno(libc::EACCES))?;
778 let pinned_raw = unsafe { libc::open(c_proc.as_ptr(), libc::O_PATH | libc::O_CLOEXEC) };
779 if pinned_raw < 0 {
780 return Err(NotifAction::Errno(ECONNREFUSED));
782 }
783 let pinned = unsafe { OwnedFd::from_raw_fd(pinned_raw) };
784
785 let real = std::fs::read_link(format!("/proc/self/fd/{}", pinned.as_raw_fd()))
787 .map_err(|_| NotifAction::Errno(libc::EACCES))?;
788 if real_path_under_any(&real, writable) {
789 Ok(pinned)
790 } else {
791 Err(NotifAction::Errno(libc::EACCES))
792 }
793}
794
795fn proc_self_fd_sockaddr(fd: RawFd) -> Option<(libc::sockaddr_un, libc::socklen_t)> {
800 let path = format!("/proc/self/fd/{}", fd);
801 let bytes = path.as_bytes();
802 let mut sun: libc::sockaddr_un = unsafe { std::mem::zeroed() };
803 if bytes.len() >= sun.sun_path.len() {
804 return None;
805 }
806 sun.sun_family = libc::AF_UNIX as libc::sa_family_t;
807 for (i, &b) in bytes.iter().enumerate() {
808 sun.sun_path[i] = b as libc::c_char;
809 }
810 let len = (std::mem::size_of::<libc::sa_family_t>() + bytes.len() + 1) as libc::socklen_t;
811 Some((sun, len))
812}
813
814fn connect_named_unix_on_behalf(
818 child_pid: u32,
819 sockfd: i32,
820 sun_path: &std::path::Path,
821 writable: &[std::path::PathBuf],
822) -> NotifAction {
823 let pinned = match resolve_named_unix_target(child_pid, sun_path, writable) {
824 Ok(fd) => fd,
825 Err(action) => return action,
826 };
827 let (sun, len) = match proc_self_fd_sockaddr(pinned.as_raw_fd()) {
828 Some(s) => s,
829 None => return NotifAction::Errno(libc::ENAMETOOLONG),
830 };
831 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(child_pid, sockfd) {
832 Ok(fd) => fd,
833 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
834 };
835 let ret = unsafe {
836 libc::connect(
837 dup_fd.as_raw_fd(),
838 &sun as *const libc::sockaddr_un as *const libc::sockaddr,
839 len,
840 )
841 };
842 if ret == 0 {
843 NotifAction::ReturnValue(0)
844 } else {
845 NotifAction::Errno(unsafe { *libc::__errno_location() })
846 }
847}
848
849fn sendto_named_unix_on_behalf(
853 notif: &SeccompNotif,
854 notif_fd: RawFd,
855 sockfd: i32,
856 buf_ptr: u64,
857 buf_len: usize,
858 flags: i32,
859 sun_path: &std::path::Path,
860 writable: &[std::path::PathBuf],
861) -> NotifAction {
862 let pinned = match resolve_named_unix_target(notif.pid, sun_path, writable) {
863 Ok(fd) => fd,
864 Err(action) => return action,
865 };
866 let (sun, len) = match proc_self_fd_sockaddr(pinned.as_raw_fd()) {
867 Some(s) => s,
868 None => return NotifAction::Errno(libc::ENAMETOOLONG),
869 };
870 let data = match read_child_mem(notif_fd, notif.id, notif.pid, buf_ptr, buf_len) {
871 Ok(b) => b,
872 Err(_) => return NotifAction::Errno(libc::EIO),
873 };
874 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
875 Ok(fd) => fd,
876 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
877 };
878 let ret = unsafe {
879 libc::sendto(
880 dup_fd.as_raw_fd(),
881 data.as_ptr() as *const libc::c_void,
882 data.len(),
883 flags,
884 &sun as *const libc::sockaddr_un as *const libc::sockaddr,
885 len,
886 )
887 };
888 if ret >= 0 {
889 NotifAction::ReturnValue(ret as i64)
890 } else {
891 NotifAction::Errno(unsafe { *libc::__errno_location() })
892 }
893}
894
895fn real_path_under_any(real: &std::path::Path, prefixes: &[std::path::PathBuf]) -> bool {
898 prefixes.iter().any(|p| {
899 let canon = std::fs::canonicalize(p);
900 real.starts_with(canon.as_deref().unwrap_or(p))
901 })
902}
903
904fn unix_sendmsg_gate(
909 notif: &SeccompNotif,
910 ctx: &Arc<SupervisorCtx>,
911 notif_fd: RawFd,
912 sockfd: i32,
913 msghdr_ptr: u64,
914 flags: i32,
915) -> Option<NotifAction> {
916 let msghdr_bytes = read_child_mem(notif_fd, notif.id, notif.pid, msghdr_ptr, 56).ok()?;
917 if msghdr_bytes.len() < 56 {
918 return None;
919 }
920 let msg_name_ptr = u64::from_ne_bytes(msghdr_bytes[0..8].try_into().unwrap());
921 if msg_name_ptr == 0 {
922 return None; }
924 let msg_namelen = u32::from_ne_bytes(msghdr_bytes[8..12].try_into().unwrap());
925 let addr_bytes =
926 read_child_mem(notif_fd, notif.id, notif.pid, msg_name_ptr, msg_namelen as usize).ok()?;
927 let path = named_unix_socket_path(&addr_bytes)?;
929
930 if ctx.policy.chroot_root.is_some() {
931 return Some(if path_under_any(&path, &ctx.policy.chroot_writable) {
932 NotifAction::Continue
933 } else {
934 NotifAction::Errno(libc::EACCES)
935 });
936 }
937 Some(sendmsg_named_unix_on_behalf(
938 notif,
939 notif_fd,
940 sockfd,
941 msghdr_ptr,
942 flags,
943 &path,
944 &ctx.policy.chroot_writable,
945 ))
946}
947
948fn sendmsg_named_unix_on_behalf(
952 notif: &SeccompNotif,
953 notif_fd: RawFd,
954 sockfd: i32,
955 msghdr_ptr: u64,
956 flags: i32,
957 sun_path: &std::path::Path,
958 writable: &[std::path::PathBuf],
959) -> NotifAction {
960 match send_named_unix_msghdr(notif, notif_fd, sockfd, msghdr_ptr, flags, sun_path, writable) {
961 Ok(n) => NotifAction::ReturnValue(n as i64),
962 Err(errno) => NotifAction::Errno(errno),
963 }
964}
965
966fn send_named_unix_msghdr(
971 notif: &SeccompNotif,
972 notif_fd: RawFd,
973 sockfd: i32,
974 msghdr_ptr: u64,
975 flags: i32,
976 sun_path: &std::path::Path,
977 writable: &[std::path::PathBuf],
978) -> Result<isize, i32> {
979 let pinned = match resolve_named_unix_target(notif.pid, sun_path, writable) {
980 Ok(fd) => fd,
981 Err(NotifAction::Errno(e)) => return Err(e),
982 Err(_) => return Err(libc::EACCES),
983 };
984 let (sun, sun_len) = proc_self_fd_sockaddr(pinned.as_raw_fd()).ok_or(libc::ENAMETOOLONG)?;
985
986 let msghdr_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msghdr_ptr, 56) {
987 Ok(b) if b.len() >= 56 => b,
988 _ => return Err(libc::EFAULT),
989 };
990 let msg_iov_ptr = u64::from_ne_bytes(msghdr_bytes[16..24].try_into().unwrap());
991 let msg_iovlen = u64::from_ne_bytes(msghdr_bytes[24..32].try_into().unwrap());
992 let msg_control_ptr = u64::from_ne_bytes(msghdr_bytes[32..40].try_into().unwrap());
993 let msg_controllen = u64::from_ne_bytes(msghdr_bytes[40..48].try_into().unwrap());
994
995 let iovlen = (msg_iovlen as usize).min(1024);
996 let iov_bytes = read_child_mem(notif_fd, notif.id, notif.pid, msg_iov_ptr, iovlen * 16)
997 .map_err(|_| libc::EIO)?;
998 let mut data_bufs: Vec<Vec<u8>> = Vec::with_capacity(iovlen);
999 for i in 0..iovlen {
1000 let off = i * 16;
1001 if off + 16 > iov_bytes.len() {
1002 break;
1003 }
1004 let base = u64::from_ne_bytes(iov_bytes[off..off + 8].try_into().unwrap());
1005 let len = u64::from_ne_bytes(iov_bytes[off + 8..off + 16].try_into().unwrap()) as usize;
1006 if len > MAX_SEND_BUF {
1007 return Err(libc::EMSGSIZE);
1008 }
1009 if base == 0 || len == 0 {
1010 data_bufs.push(Vec::new());
1011 continue;
1012 }
1013 data_bufs.push(read_child_mem(notif_fd, notif.id, notif.pid, base, len).map_err(|_| libc::EIO)?);
1014 }
1015 let mut local_iovs: Vec<libc::iovec> = data_bufs
1016 .iter()
1017 .map(|b| libc::iovec {
1018 iov_base: b.as_ptr() as *mut libc::c_void,
1019 iov_len: b.len(),
1020 })
1021 .collect();
1022
1023 let control_buf = if msg_control_ptr != 0 && msg_controllen > 0 {
1024 let len = (msg_controllen as usize).min(4096);
1025 read_child_mem(notif_fd, notif.id, notif.pid, msg_control_ptr, len).ok()
1026 } else {
1027 None
1028 };
1029
1030 let dup_fd = crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd)
1031 .map_err(|e| e.raw_os_error().unwrap_or(libc::EBADF))?;
1032
1033 let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
1034 msg.msg_name = &sun as *const libc::sockaddr_un as *mut libc::c_void;
1035 msg.msg_namelen = sun_len;
1036 msg.msg_iov = local_iovs.as_mut_ptr();
1037 msg.msg_iovlen = local_iovs.len();
1038 if let Some(ref ctrl) = control_buf {
1039 msg.msg_control = ctrl.as_ptr() as *mut libc::c_void;
1040 msg.msg_controllen = ctrl.len();
1041 }
1042 let ret = unsafe { libc::sendmsg(dup_fd.as_raw_fd(), &msg, flags) };
1043 if ret >= 0 {
1044 Ok(ret)
1045 } else {
1046 Err(unsafe { *libc::__errno_location() })
1047 }
1048}
1049
1050fn mmsg_entry_named_unix_path(
1054 notif: &SeccompNotif,
1055 notif_fd: RawFd,
1056 entry_ptr: u64,
1057) -> Option<std::path::PathBuf> {
1058 let hdr = read_child_mem(notif_fd, notif.id, notif.pid, entry_ptr, 12).ok()?;
1059 if hdr.len() < 12 {
1060 return None;
1061 }
1062 let msg_name_ptr = u64::from_ne_bytes(hdr[0..8].try_into().unwrap());
1063 if msg_name_ptr == 0 {
1064 return None;
1065 }
1066 let msg_namelen = u32::from_ne_bytes(hdr[8..12].try_into().unwrap());
1067 let addr_bytes =
1068 read_child_mem(notif_fd, notif.id, notif.pid, msg_name_ptr, msg_namelen as usize).ok()?;
1069 named_unix_socket_path(&addr_bytes)
1070}
1071
1072fn sendmmsg_named_unix_on_behalf(
1079 notif: &SeccompNotif,
1080 notif_fd: RawFd,
1081 sockfd: i32,
1082 msgvec_ptr: u64,
1083 vlen: usize,
1084 flags: i32,
1085 writable: &[std::path::PathBuf],
1086) -> NotifAction {
1087 let mut sent: usize = 0;
1088 let mut first_errno: Option<i32> = None;
1089 for i in 0..vlen {
1090 let entry_ptr = msgvec_ptr + (i * MMSGHDR_SIZE) as u64;
1091 let path = match mmsg_entry_named_unix_path(notif, notif_fd, entry_ptr) {
1092 Some(p) => p,
1093 None => break,
1096 };
1097 match send_named_unix_msghdr(notif, notif_fd, sockfd, entry_ptr, flags, &path, writable) {
1098 Ok(n) => {
1099 let bytes = (n as u32).to_ne_bytes();
1100 let _ = write_child_mem(
1101 notif_fd,
1102 notif.id,
1103 notif.pid,
1104 entry_ptr + MSG_LEN_OFFSET as u64,
1105 &bytes,
1106 );
1107 sent += 1;
1108 }
1109 Err(errno) => {
1110 first_errno = Some(errno);
1111 break;
1112 }
1113 }
1114 }
1115 if sent > 0 {
1116 NotifAction::ReturnValue(sent as i64)
1117 } else {
1118 NotifAction::Errno(first_errno.unwrap_or(libc::EACCES))
1119 }
1120}
1121
1122fn named_unix_socket_path(addr_bytes: &[u8]) -> Option<std::path::PathBuf> {
1127 if addr_bytes.len() < 3 {
1130 return None;
1131 }
1132 let family = u16::from_ne_bytes([addr_bytes[0], addr_bytes[1]]);
1133 if family != libc::AF_UNIX as u16 {
1134 return None;
1135 }
1136 let sun_path = &addr_bytes[2..];
1137 if sun_path[0] == 0 {
1138 return None; }
1140 let end = sun_path.iter().position(|&b| b == 0).unwrap_or(sun_path.len());
1141 let raw = &sun_path[..end];
1142 if raw.is_empty() {
1143 return None;
1144 }
1145 std::str::from_utf8(raw).ok().map(std::path::PathBuf::from)
1146}
1147
1148fn path_under_any(path: &std::path::Path, prefixes: &[std::path::PathBuf]) -> bool {
1152 let norm = crate::chroot::resolve::confine(&path.to_string_lossy());
1153 prefixes.iter().any(|p| norm.starts_with(p))
1154}
1155
1156async fn sendto_on_behalf(
1172 notif: &SeccompNotif,
1173 ctx: &Arc<SupervisorCtx>,
1174 notif_fd: RawFd,
1175) -> NotifAction {
1176 let args = ¬if.data.args;
1177 let sockfd = args[0] as i32;
1178 let buf_ptr = args[1];
1179 let buf_len = args[2] as usize;
1180 if buf_len > MAX_SEND_BUF {
1181 return NotifAction::Errno(libc::EMSGSIZE);
1182 }
1183 let flags = args[3] as i32;
1184 let addr_ptr = args[4];
1185 let addr_len = args[5] as u32;
1186
1187 if addr_ptr == 0 {
1188 return NotifAction::Continue; }
1190
1191 let addr_bytes =
1193 match read_child_mem(notif_fd, notif.id, notif.pid, addr_ptr, addr_len as usize) {
1194 Ok(b) => b,
1195 Err(_) => return NotifAction::Errno(libc::EIO),
1196 };
1197
1198 if let Some(ip) = parse_ip_from_sockaddr(&addr_bytes) {
1202 let dest_port = parse_port_from_sockaddr(&addr_bytes);
1203 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
1204 Ok(fd) => fd,
1205 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
1206 };
1207 let protocol = match query_socket_protocol(dup_fd.as_raw_fd()) {
1208 Some(p) => p,
1209 None => return NotifAction::Errno(ECONNREFUSED),
1210 };
1211 let ns = ctx.network.lock().await;
1212 let live_policy = {
1213 let pfs = ctx.policy_fn.lock().await;
1214 pfs.live_policy.clone()
1215 };
1216 let effective = ns.effective_network_policy(notif.pid, protocol, live_policy.as_ref());
1217 if !matches!(effective, crate::seccomp::notif::NetworkPolicy::Unrestricted) {
1218 match dest_port {
1219 Some(p) if !effective.allows(ip, p) => {
1220 return NotifAction::Errno(ECONNREFUSED);
1221 }
1222 None => return NotifAction::Errno(ECONNREFUSED),
1223 Some(_) => {}
1224 }
1225 }
1226 drop(ns);
1227
1228 let data = match read_child_mem(notif_fd, notif.id, notif.pid, buf_ptr, buf_len) {
1230 Ok(b) => b,
1231 Err(_) => return NotifAction::Errno(libc::EIO),
1232 };
1233
1234 let ret = unsafe {
1238 libc::sendto(
1239 dup_fd.as_raw_fd(),
1240 data.as_ptr() as *const libc::c_void,
1241 data.len(),
1242 flags,
1243 addr_bytes.as_ptr() as *const libc::sockaddr,
1244 addr_len as libc::socklen_t,
1245 )
1246 };
1247
1248 if ret >= 0 {
1250 NotifAction::ReturnValue(ret as i64)
1251 } else {
1252 let errno = unsafe { *libc::__errno_location() };
1253 NotifAction::Errno(errno)
1254 }
1255 } else {
1256 match named_unix_socket_path(&addr_bytes) {
1260 Some(path) if ctx.policy.has_unix_fs_gate => {
1261 if ctx.policy.chroot_root.is_some() {
1262 if path_under_any(&path, &ctx.policy.chroot_writable) {
1263 NotifAction::Continue
1264 } else {
1265 NotifAction::Errno(libc::EACCES)
1266 }
1267 } else {
1268 sendto_named_unix_on_behalf(
1269 notif,
1270 notif_fd,
1271 sockfd,
1272 buf_ptr,
1273 buf_len,
1274 flags,
1275 &path,
1276 &ctx.policy.chroot_writable,
1277 )
1278 }
1279 }
1280 _ => NotifAction::Continue,
1281 }
1282 }
1283}
1284
1285async fn sendmsg_on_behalf(
1296 notif: &SeccompNotif,
1297 ctx: &Arc<SupervisorCtx>,
1298 notif_fd: RawFd,
1299) -> NotifAction {
1300 let args = ¬if.data.args;
1301 let sockfd = args[0] as i32;
1302 let msghdr_ptr = args[1];
1303 let flags = args[2] as i32;
1304
1305 if ctx.policy.has_unix_fs_gate {
1309 if let Some(action) = unix_sendmsg_gate(notif, ctx, notif_fd, sockfd, msghdr_ptr, flags) {
1310 return action;
1311 }
1312 }
1313
1314 let dest_policy = ctx.policy.has_net_destination_policy;
1320 if !dest_policy {
1321 match prescan_msghdr(notif, notif_fd, msghdr_ptr) {
1325 PrescanResult::ContinueWholeCall => return NotifAction::Continue,
1326 PrescanResult::Errno(e) => return NotifAction::Errno(e),
1327 PrescanResult::OnBehalf => {}
1328 }
1329 }
1330
1331 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
1332 Ok(fd) => fd,
1333 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
1334 };
1335 let protocol = match query_socket_protocol(dup_fd.as_raw_fd()) {
1336 Some(p) => p,
1337 None => return NotifAction::Errno(ECONNREFUSED),
1338 };
1339
1340 match send_msghdr_on_behalf(notif, ctx, notif_fd, &dup_fd, protocol, msghdr_ptr, flags).await {
1341 Ok(n) => NotifAction::ReturnValue(n as i64),
1342 Err(errno) => NotifAction::Errno(errno),
1343 }
1344}
1345
1346#[derive(Clone, Copy)]
1351enum PrescanResult {
1352 OnBehalf,
1355 ContinueWholeCall,
1361 Errno(i32),
1364}
1365
1366fn prescan_msghdr(
1371 notif: &SeccompNotif,
1372 notif_fd: RawFd,
1373 msghdr_ptr: u64,
1374) -> PrescanResult {
1375 let msghdr_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msghdr_ptr, 56) {
1376 Ok(b) if b.len() >= 56 => b,
1377 _ => return PrescanResult::Errno(libc::EFAULT),
1378 };
1379 let msg_name_ptr = u64::from_ne_bytes(msghdr_bytes[0..8].try_into().unwrap());
1380 if msg_name_ptr == 0 {
1381 return PrescanResult::ContinueWholeCall;
1382 }
1383 let msg_namelen = u32::from_ne_bytes(msghdr_bytes[8..12].try_into().unwrap());
1384 let addr_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msg_name_ptr, msg_namelen as usize) {
1385 Ok(b) => b,
1386 Err(_) => return PrescanResult::Errno(libc::EIO),
1387 };
1388 if parse_ip_from_sockaddr(&addr_bytes).is_none() {
1389 return PrescanResult::ContinueWholeCall;
1390 }
1391 PrescanResult::OnBehalf
1392}
1393
1394async fn send_msghdr_on_behalf(
1407 notif: &SeccompNotif,
1408 ctx: &Arc<SupervisorCtx>,
1409 notif_fd: RawFd,
1410 dup_fd: &std::os::unix::io::OwnedFd,
1411 protocol: Protocol,
1412 msghdr_ptr: u64,
1413 flags: i32,
1414) -> Result<isize, i32> {
1415 let msghdr_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msghdr_ptr, 56) {
1416 Ok(b) if b.len() >= 56 => b,
1417 _ => return Err(libc::EFAULT),
1418 };
1419 let msg_name_ptr = u64::from_ne_bytes(msghdr_bytes[0..8].try_into().unwrap());
1420 let msg_namelen = u32::from_ne_bytes(msghdr_bytes[8..12].try_into().unwrap());
1421 let msg_iov_ptr = u64::from_ne_bytes(msghdr_bytes[16..24].try_into().unwrap());
1422 let msg_iovlen = u64::from_ne_bytes(msghdr_bytes[24..32].try_into().unwrap());
1423 let msg_control_ptr = u64::from_ne_bytes(msghdr_bytes[32..40].try_into().unwrap());
1424 let msg_controllen = u64::from_ne_bytes(msghdr_bytes[40..48].try_into().unwrap());
1425
1426 let connected = msg_name_ptr == 0 || msg_namelen == 0;
1434 let addr_bytes = if connected {
1435 Vec::new()
1436 } else {
1437 match read_child_mem(notif_fd, notif.id, notif.pid, msg_name_ptr, msg_namelen as usize) {
1438 Ok(b) => b,
1439 Err(_) => return Err(libc::EIO),
1440 }
1441 };
1442 if !connected {
1443 let ip = match parse_ip_from_sockaddr(&addr_bytes) {
1444 Some(ip) => ip,
1445 None => return Err(libc::EAFNOSUPPORT),
1448 };
1449 let dest_port = parse_port_from_sockaddr(&addr_bytes);
1450
1451 let ns = ctx.network.lock().await;
1452 let live_policy = {
1453 let pfs = ctx.policy_fn.lock().await;
1454 pfs.live_policy.clone()
1455 };
1456 let effective = ns.effective_network_policy(notif.pid, protocol, live_policy.as_ref());
1457 if !matches!(effective, crate::seccomp::notif::NetworkPolicy::Unrestricted) {
1458 match dest_port {
1459 Some(p) if !effective.allows(ip, p) => return Err(ECONNREFUSED),
1460 None => return Err(ECONNREFUSED),
1461 Some(_) => {}
1462 }
1463 }
1464 drop(ns);
1465 }
1466
1467 let iovlen = (msg_iovlen as usize).min(1024);
1468 let iov_size = iovlen * 16;
1469 let iov_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msg_iov_ptr, iov_size) {
1470 Ok(b) => b,
1471 Err(_) => return Err(libc::EIO),
1472 };
1473 let mut data_bufs: Vec<Vec<u8>> = Vec::with_capacity(iovlen);
1474 let mut local_iovs: Vec<libc::iovec> = Vec::with_capacity(iovlen);
1475 for i in 0..iovlen {
1476 let off = i * 16;
1477 if off + 16 > iov_bytes.len() { break; }
1478 let iov_base = u64::from_ne_bytes(iov_bytes[off..off + 8].try_into().unwrap());
1479 let iov_len = u64::from_ne_bytes(iov_bytes[off + 8..off + 16].try_into().unwrap()) as usize;
1480 if iov_len > MAX_SEND_BUF {
1481 return Err(libc::EMSGSIZE);
1482 }
1483 if iov_base == 0 || iov_len == 0 {
1484 data_bufs.push(Vec::new());
1485 continue;
1486 }
1487 let buf = match read_child_mem(notif_fd, notif.id, notif.pid, iov_base, iov_len) {
1488 Ok(b) => b,
1489 Err(_) => return Err(libc::EIO),
1490 };
1491 data_bufs.push(buf);
1492 }
1493 for buf in &data_bufs {
1494 local_iovs.push(libc::iovec {
1495 iov_base: buf.as_ptr() as *mut libc::c_void,
1496 iov_len: buf.len(),
1497 });
1498 }
1499
1500 let control_buf = if msg_control_ptr != 0 && msg_controllen > 0 {
1501 let len = (msg_controllen as usize).min(4096);
1502 read_child_mem(notif_fd, notif.id, notif.pid, msg_control_ptr, len).ok()
1503 } else {
1504 None
1505 };
1506
1507 let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
1508 if !connected {
1509 msg.msg_name = addr_bytes.as_ptr() as *mut libc::c_void;
1510 msg.msg_namelen = addr_bytes.len() as u32;
1511 }
1512 msg.msg_iov = local_iovs.as_mut_ptr();
1513 msg.msg_iovlen = local_iovs.len();
1514 if let Some(ref ctrl) = control_buf {
1515 msg.msg_control = ctrl.as_ptr() as *mut libc::c_void;
1516 msg.msg_controllen = ctrl.len();
1517 }
1518
1519 let ret = unsafe { libc::sendmsg(dup_fd.as_raw_fd(), &msg, flags) };
1520 if ret >= 0 {
1521 Ok(ret)
1522 } else {
1523 Err(unsafe { *libc::__errno_location() })
1524 }
1525}
1526
1527const MMSGHDR_SIZE: usize = 64;
1535const MSG_LEN_OFFSET: usize = 56;
1536const MAX_MMSGHDR_ENTRIES: usize = 256;
1541
1542async fn sendmmsg_on_behalf(
1555 notif: &SeccompNotif,
1556 ctx: &Arc<SupervisorCtx>,
1557 notif_fd: RawFd,
1558) -> NotifAction {
1559 let args = ¬if.data.args;
1560 let sockfd = args[0] as i32;
1561 let msgvec_ptr = args[1];
1562 let vlen = (args[2] as u32 as usize).min(MAX_MMSGHDR_ENTRIES);
1563 let flags = args[3] as i32;
1564
1565 if vlen == 0 {
1566 return NotifAction::ReturnValue(0);
1567 }
1568
1569 if ctx.policy.has_unix_fs_gate {
1574 let mut named_unix = false;
1575 for i in 0..vlen {
1576 let entry_ptr = msgvec_ptr + (i * MMSGHDR_SIZE) as u64;
1577 if mmsg_entry_named_unix_path(notif, notif_fd, entry_ptr).is_some() {
1578 named_unix = true;
1579 break;
1580 }
1581 }
1582 if named_unix {
1583 if ctx.policy.chroot_root.is_some() {
1584 for i in 0..vlen {
1587 let entry_ptr = msgvec_ptr + (i * MMSGHDR_SIZE) as u64;
1588 if let Some(path) = mmsg_entry_named_unix_path(notif, notif_fd, entry_ptr) {
1589 if !path_under_any(&path, &ctx.policy.chroot_writable) {
1590 return NotifAction::Errno(libc::EACCES);
1591 }
1592 }
1593 }
1594 } else {
1596 return sendmmsg_named_unix_on_behalf(
1597 notif,
1598 notif_fd,
1599 sockfd,
1600 msgvec_ptr,
1601 vlen,
1602 flags,
1603 &ctx.policy.chroot_writable,
1604 );
1605 }
1606 }
1607 }
1608
1609 if ctx.policy.has_net_destination_policy {
1616 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
1617 Ok(fd) => fd,
1618 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
1619 };
1620 let protocol = match query_socket_protocol(dup_fd.as_raw_fd()) {
1621 Some(p) => p,
1622 None => return NotifAction::Errno(ECONNREFUSED),
1623 };
1624 let mut sent: usize = 0;
1625 let mut first_errno: Option<i32> = None;
1626 for i in 0..vlen {
1627 let entry_ptr = msgvec_ptr + (i * MMSGHDR_SIZE) as u64;
1628 match send_msghdr_on_behalf(notif, ctx, notif_fd, &dup_fd, protocol, entry_ptr, flags)
1629 .await
1630 {
1631 Ok(n) => {
1632 let bytes = (n as u32).to_ne_bytes();
1633 let _ = write_child_mem(
1634 notif_fd,
1635 notif.id,
1636 notif.pid,
1637 entry_ptr + MSG_LEN_OFFSET as u64,
1638 &bytes,
1639 );
1640 sent += 1;
1641 }
1642 Err(errno) => {
1643 first_errno = Some(errno);
1644 break;
1645 }
1646 }
1647 }
1648 return if sent > 0 {
1649 NotifAction::ReturnValue(sent as i64)
1650 } else {
1651 NotifAction::Errno(first_errno.unwrap_or(ECONNREFUSED))
1652 };
1653 }
1654
1655 for i in 0..vlen {
1661 let entry_ptr = msgvec_ptr + (i * MMSGHDR_SIZE) as u64;
1662 match prescan_msghdr(notif, notif_fd, entry_ptr) {
1663 PrescanResult::OnBehalf => continue,
1664 PrescanResult::ContinueWholeCall => return NotifAction::Continue,
1665 PrescanResult::Errno(e) => return NotifAction::Errno(e),
1666 }
1667 }
1668
1669 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
1670 Ok(fd) => fd,
1671 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
1672 };
1673 let protocol = match query_socket_protocol(dup_fd.as_raw_fd()) {
1674 Some(p) => p,
1675 None => return NotifAction::Errno(ECONNREFUSED),
1676 };
1677
1678 let mut sent: usize = 0;
1679 let mut first_errno: Option<i32> = None;
1680
1681 for i in 0..vlen {
1682 let entry_ptr = msgvec_ptr + (i * MMSGHDR_SIZE) as u64;
1683 match send_msghdr_on_behalf(notif, ctx, notif_fd, &dup_fd, protocol, entry_ptr, flags).await {
1684 Ok(n) => {
1685 let bytes = (n as u32).to_ne_bytes();
1686 let _ = write_child_mem(
1687 notif_fd, notif.id, notif.pid,
1688 entry_ptr + MSG_LEN_OFFSET as u64,
1689 &bytes,
1690 );
1691 sent += 1;
1692 }
1693 Err(errno) => {
1694 first_errno = Some(errno);
1695 break;
1696 }
1697 }
1698 }
1699
1700 if sent > 0 {
1701 NotifAction::ReturnValue(sent as i64)
1702 } else {
1703 NotifAction::Errno(first_errno.unwrap_or(ECONNREFUSED))
1707 }
1708}
1709
1710pub(crate) async fn handle_net(
1738 notif: &SeccompNotif,
1739 ctx: &Arc<SupervisorCtx>,
1740 notif_fd: RawFd,
1741) -> NotifAction {
1742 let nr = notif.data.nr as i64;
1743
1744 if nr == libc::SYS_connect {
1745 connect_on_behalf(notif, ctx, notif_fd).await
1746 } else if nr == libc::SYS_sendto {
1747 sendto_on_behalf(notif, ctx, notif_fd).await
1748 } else if nr == libc::SYS_sendmsg {
1749 sendmsg_on_behalf(notif, ctx, notif_fd).await
1750 } else if nr == libc::SYS_sendmmsg {
1751 sendmmsg_on_behalf(notif, ctx, notif_fd).await
1752 } else {
1753 NotifAction::Continue
1754 }
1755}
1756
1757pub struct ResolvedNetAllow {
1763 pub per_ip: HashMap<IpAddr, HashSet<u16>>,
1767 pub per_ip_all_ports: HashSet<IpAddr>,
1772 pub cidrs: Vec<(IpCidr, crate::seccomp::notif::PortAllow)>,
1776 pub any_ip_ports: HashSet<u16>,
1778 pub any_ip_all_ports: bool,
1782}
1783
1784pub struct ResolvedNetAllowSet {
1790 pub tcp: ResolvedNetAllow,
1791 pub udp: ResolvedNetAllow,
1792 pub icmp: ResolvedNetAllow,
1793 pub concrete_host_entries: String,
1799}
1800
1801pub async fn resolve_net_allow(
1810 rules: &[NetAllow],
1811) -> io::Result<ResolvedNetAllowSet> {
1812 use crate::seccomp::notif::PortAllow;
1813 let per_proto = |target: Protocol| async move {
1814 let mut per_ip: HashMap<IpAddr, HashSet<u16>> = HashMap::new();
1815 let mut per_ip_all_ports: HashSet<IpAddr> = HashSet::new();
1816 let mut cidrs: Vec<(IpCidr, PortAllow)> = Vec::new();
1817 let mut any_ip_ports: HashSet<u16> = HashSet::new();
1818 let mut any_ip_all_ports = false;
1819 let mut local_etc_hosts = String::new();
1820
1821 for rule in rules.iter().filter(|r| r.protocol == target) {
1822 match &rule.target {
1823 NetTarget::AnyIp => {
1824 if rule.all_ports || target == Protocol::Icmp {
1825 any_ip_all_ports = true;
1828 } else {
1829 for &p in &rule.ports {
1830 any_ip_ports.insert(p);
1831 }
1832 }
1833 }
1834 NetTarget::Cidr(c) => {
1835 let pa = if rule.all_ports || target == Protocol::Icmp {
1838 PortAllow::Any
1839 } else {
1840 PortAllow::Specific(rule.ports.iter().copied().collect())
1841 };
1842 cidrs.push((*c, pa));
1843 }
1844 NetTarget::Host(host) => {
1845 let addr = format!("{}:0", host);
1846 let resolved = tokio::net::lookup_host(addr.as_str()).await.map_err(|e| {
1847 io::Error::new(
1848 e.kind(),
1849 format!("failed to resolve host '{}': {}", host, e),
1850 )
1851 })?;
1852 for socket_addr in resolved {
1853 let ip = socket_addr.ip();
1854 if rule.all_ports || target == Protocol::Icmp {
1855 per_ip_all_ports.insert(ip);
1856 per_ip.entry(ip).or_default();
1857 } else {
1858 let entry = per_ip.entry(ip).or_default();
1859 for &p in &rule.ports {
1860 entry.insert(p);
1861 }
1862 }
1863 local_etc_hosts.push_str(&format!("{} {}\n", ip, host));
1864 }
1865 }
1866 }
1867 }
1868
1869 Ok::<_, io::Error>((
1870 ResolvedNetAllow {
1871 per_ip,
1872 per_ip_all_ports,
1873 cidrs,
1874 any_ip_ports,
1875 any_ip_all_ports,
1876 },
1877 local_etc_hosts,
1878 ))
1879 };
1880
1881 let (tcp, tcp_eh) = per_proto(Protocol::Tcp).await?;
1882 let (udp, udp_eh) = per_proto(Protocol::Udp).await?;
1883 let (icmp, icmp_eh) = per_proto(Protocol::Icmp).await?;
1884
1885 let mut concrete_host_entries = String::new();
1886 for chunk in [tcp_eh, udp_eh, icmp_eh] {
1887 concrete_host_entries.push_str(&chunk);
1888 }
1889
1890 Ok(ResolvedNetAllowSet {
1891 tcp,
1892 udp,
1893 icmp,
1894 concrete_host_entries,
1895 })
1896}
1897
1898pub struct ResolvedNetDenySet {
1900 pub tcp: crate::seccomp::notif::NetworkPolicy,
1901 pub udp: crate::seccomp::notif::NetworkPolicy,
1902 pub icmp: crate::seccomp::notif::NetworkPolicy,
1903}
1904
1905pub fn resolve_net_deny(rules: &[NetDeny]) -> ResolvedNetDenySet {
1908 use crate::seccomp::notif::{NetworkPolicy, PortAllow};
1909
1910 let per_proto = |target: Protocol| -> NetworkPolicy {
1911 let mut cidrs: Vec<(IpCidr, PortAllow)> = Vec::new();
1912 let mut any_ip_ports: HashSet<u16> = HashSet::new();
1913 let mut deny_all = false;
1914 let mut saw_rule = false;
1915
1916 for rule in rules.iter().filter(|r| r.protocol == target) {
1917 saw_rule = true;
1918 match &rule.target {
1919 NetTarget::AnyIp => {
1920 if rule.all_ports || target == Protocol::Icmp {
1921 deny_all = true;
1922 } else {
1923 for &p in &rule.ports {
1924 any_ip_ports.insert(p);
1925 }
1926 }
1927 }
1928 NetTarget::Cidr(c) => {
1929 let pa = if rule.all_ports || target == Protocol::Icmp {
1930 PortAllow::Any
1931 } else {
1932 PortAllow::Specific(rule.ports.iter().copied().collect())
1933 };
1934 cidrs.push((*c, pa));
1935 }
1936 NetTarget::Host(_) => unreachable!("net-deny rejects hostnames"),
1939 }
1940 }
1941
1942 if !saw_rule {
1943 NetworkPolicy::Unrestricted
1944 } else {
1945 NetworkPolicy::DenyList {
1946 cidrs,
1947 any_ip_ports,
1948 deny_all,
1949 }
1950 }
1951 };
1952
1953 ResolvedNetDenySet {
1954 tcp: per_proto(Protocol::Tcp),
1955 udp: per_proto(Protocol::Udp),
1956 icmp: per_proto(Protocol::Icmp),
1957 }
1958}
1959
1960pub fn compose_virtual_etc_hosts(
1977 chroot_root: Option<&std::path::Path>,
1978 concrete_host_entries: &str,
1979) -> String {
1980 let mut out = String::new();
1981 let mut has_v4_localhost = false;
1982 let mut has_v6_localhost = false;
1983
1984 if let Some(root) = chroot_root {
1985 if let Ok(image) = std::fs::read_to_string(root.join("etc").join("hosts")) {
1986 for line in image.lines() {
1987 let stripped = line.split('#').next().unwrap_or("");
1990 let mut parts = stripped.split_whitespace();
1991 let Some(ip) = parts.next() else { continue };
1992 for name in parts {
1993 if name == "localhost" {
1994 if ip == "127.0.0.1" {
1995 has_v4_localhost = true;
1996 } else if ip == "::1" {
1997 has_v6_localhost = true;
1998 }
1999 }
2000 }
2001 }
2002 out.push_str(&image);
2003 if !out.is_empty() && !out.ends_with('\n') {
2004 out.push('\n');
2005 }
2006 }
2007 }
2008
2009 if !has_v4_localhost {
2010 out.push_str("127.0.0.1 localhost\n");
2011 }
2012 if !has_v6_localhost {
2013 out.push_str("::1 localhost\n");
2014 }
2015 out.push_str(concrete_host_entries);
2016 out
2017}
2018
2019#[cfg(test)]
2024mod tests {
2025 use super::*;
2026
2027 #[test]
2030 fn netallow_parse_concrete_host_port() {
2031 let r = NetRule::parse_allow("example.com:443").unwrap();
2032 assert!(matches!(&r.target, NetTarget::Host(h) if h == "example.com"));
2033 assert_eq!(r.ports, vec![443]);
2034 assert!(!r.all_ports);
2035 }
2036
2037 #[test]
2038 fn netallow_parse_any_host_port() {
2039 let r = NetRule::parse_allow(":8080").unwrap();
2040 assert_eq!(r.target, NetTarget::AnyIp);
2041 assert_eq!(r.ports, vec![8080]);
2042 assert!(!r.all_ports);
2043
2044 let r = NetRule::parse_allow("*:8080").unwrap();
2045 assert_eq!(r.target, NetTarget::AnyIp);
2046 assert_eq!(r.ports, vec![8080]);
2047 assert!(!r.all_ports);
2048 }
2049
2050 #[test]
2051 fn netallow_parse_multiple_ports() {
2052 let r = NetRule::parse_allow("github.com:22,80,443").unwrap();
2053 assert!(matches!(&r.target, NetTarget::Host(h) if h == "github.com"));
2054 assert_eq!(r.ports, vec![22, 80, 443]);
2055 assert!(!r.all_ports);
2056 }
2057
2058 #[test]
2059 fn netallow_parse_wildcard_any_host_any_port_colon() {
2060 let r = NetRule::parse_allow(":*").unwrap();
2061 assert_eq!(r.target, NetTarget::AnyIp);
2062 assert!(r.ports.is_empty());
2063 assert!(r.all_ports);
2064 }
2065
2066 #[test]
2067 fn netallow_parse_wildcard_any_host_any_port_star() {
2068 let r = NetRule::parse_allow("*:*").unwrap();
2069 assert_eq!(r.target, NetTarget::AnyIp);
2070 assert!(r.ports.is_empty());
2071 assert!(r.all_ports);
2072 }
2073
2074 #[test]
2075 fn netallow_parse_wildcard_concrete_host_any_port() {
2076 let r = NetRule::parse_allow("example.com:*").unwrap();
2077 assert!(matches!(&r.target, NetTarget::Host(h) if h == "example.com"));
2078 assert!(r.ports.is_empty());
2079 assert!(r.all_ports);
2080 }
2081
2082 #[test]
2083 fn netallow_parse_rejects_mixed_wildcard_and_concrete() {
2084 let err = NetRule::parse_allow("example.com:80,*").unwrap_err();
2088 assert!(format!("{}", err).contains("cannot mix"));
2089 let err = NetRule::parse_allow("example.com:*,80").unwrap_err();
2090 assert!(format!("{}", err).contains("cannot mix"));
2091 }
2092
2093 #[test]
2094 fn netallow_parse_rejects_port_zero() {
2095 let err = NetRule::parse_allow("example.com:0").unwrap_err();
2096 assert!(format!("{}", err).contains("port 0"));
2097 }
2098
2099 #[test]
2100 fn netallow_parse_rejects_empty_port() {
2101 let err = NetRule::parse_allow("example.com:").unwrap_err();
2102 assert!(format!("{}", err).contains("invalid port"));
2103 }
2104
2105 #[test]
2106 fn netallow_bare_host_is_all_ports() {
2107 let r = NetRule::parse_allow("example.com").unwrap();
2110 assert!(matches!(&r.target, NetTarget::Host(h) if h == "example.com"));
2111 assert!(r.all_ports);
2112 assert!(r.ports.is_empty());
2113 }
2114
2115 #[test]
2116 fn netallow_bare_star_is_any_host_all_ports() {
2117 let r = NetRule::parse_allow("*").unwrap();
2118 assert_eq!(r.target, NetTarget::AnyIp);
2119 assert!(r.all_ports);
2120 assert!(r.ports.is_empty());
2121 }
2122
2123 #[test]
2124 fn netallow_empty_spec_rejected() {
2125 assert!(NetRule::parse_allow("").is_err());
2126 assert!(NetRule::parse_allow("tcp://").is_err());
2127 }
2128
2129 #[test]
2130 fn netallow_cidr_target_with_port() {
2131 let r = NetRule::parse_allow("10.0.0.0/8:80").unwrap();
2134 assert!(matches!(&r.target, NetTarget::Cidr(c) if !c.is_single_host()));
2135 assert_eq!(r.ports, vec![80]);
2136 assert!(!r.all_ports);
2137 }
2138
2139 #[test]
2140 fn netallow_ipv6_literal_and_bracket() {
2141 let lo: std::net::IpAddr = "::1".parse().unwrap();
2142 let r = NetRule::parse_allow("::1").unwrap();
2144 assert!(matches!(&r.target, NetTarget::Cidr(c) if c.addr == lo && c.is_single_host()));
2145 assert!(r.all_ports);
2146 let r = NetRule::parse_allow("[::1]:443").unwrap();
2148 assert!(matches!(&r.target, NetTarget::Cidr(c) if c.addr == lo && c.is_single_host()));
2149 assert_eq!(r.ports, vec![443]);
2150 let r = NetRule::parse_allow("fc00::/7").unwrap();
2152 assert!(matches!(&r.target, NetTarget::Cidr(c) if !c.is_single_host()));
2153 assert!(r.all_ports);
2154 }
2155
2156 #[tokio::test]
2157 async fn test_resolve_net_allow_cidr_no_dns() {
2158 let rules = vec![
2161 NetAllow { protocol: Protocol::Tcp, target: NetTarget::Cidr(IpCidr::parse("10.0.0.0/8").unwrap()), ports: vec![80], all_ports: false },
2162 NetAllow { protocol: Protocol::Tcp, target: NetTarget::Cidr(IpCidr::parse("1.2.3.4").unwrap()), ports: vec![], all_ports: true },
2163 ];
2164 let resolved = resolve_net_allow(&rules).await.unwrap();
2165 assert_eq!(resolved.tcp.cidrs.len(), 2);
2166 assert!(resolved.tcp.per_ip.is_empty());
2167 assert!(resolved.concrete_host_entries.is_empty());
2168 }
2169
2170 #[test]
2171 fn netallow_parse_repeated_wildcard_is_idempotent() {
2172 let r = NetRule::parse_allow(":*,*").unwrap();
2175 assert!(r.all_ports);
2176 assert!(r.ports.is_empty());
2177 }
2178
2179 #[test]
2182 fn netallow_bare_form_defaults_to_tcp() {
2183 let r = NetRule::parse_allow("example.com:443").unwrap();
2184 assert_eq!(r.protocol, Protocol::Tcp);
2185 }
2186
2187 #[test]
2188 fn netallow_explicit_tcp_scheme() {
2189 let r = NetRule::parse_allow("tcp://example.com:443").unwrap();
2190 assert_eq!(r.protocol, Protocol::Tcp);
2191 assert!(matches!(&r.target, NetTarget::Host(h) if h == "example.com"));
2192 assert_eq!(r.ports, vec![443]);
2193 }
2194
2195 #[test]
2196 fn netallow_udp_scheme_with_host_port() {
2197 let r = NetRule::parse_allow("udp://1.1.1.1:53").unwrap();
2198 assert_eq!(r.protocol, Protocol::Udp);
2199 let one: std::net::IpAddr = "1.1.1.1".parse().unwrap();
2201 assert!(matches!(&r.target, NetTarget::Cidr(c) if c.addr == one && c.is_single_host()));
2202 assert_eq!(r.ports, vec![53]);
2203 }
2204
2205 #[test]
2206 fn netallow_udp_wildcard_any_anywhere() {
2207 let r = NetRule::parse_allow("udp://*:*").unwrap();
2209 assert_eq!(r.protocol, Protocol::Udp);
2210 assert_eq!(r.target, NetTarget::AnyIp);
2211 assert!(r.all_ports);
2212 }
2213
2214 #[test]
2215 fn netallow_icmp_scheme_with_host() {
2216 let r = NetRule::parse_allow("icmp://github.com").unwrap();
2217 assert_eq!(r.protocol, Protocol::Icmp);
2218 assert!(matches!(&r.target, NetTarget::Host(h) if h == "github.com"));
2219 assert!(r.ports.is_empty());
2220 assert!(r.all_ports);
2222 }
2223
2224 #[test]
2225 fn netallow_icmp_wildcard() {
2226 let r = NetRule::parse_allow("icmp://*").unwrap();
2229 assert_eq!(r.protocol, Protocol::Icmp);
2230 assert_eq!(r.target, NetTarget::AnyIp);
2231 }
2232
2233 #[test]
2234 fn netallow_icmp_rejects_port() {
2235 let err = NetRule::parse_allow("icmp://github.com:80").unwrap_err();
2239 assert!(format!("{}", err).contains("icmp rule takes no port"));
2240 }
2241
2242 #[test]
2243 fn netallow_icmp_rejects_empty_body() {
2244 let err = NetRule::parse_allow("icmp://").unwrap_err();
2245 assert!(format!("{}", err).contains("needs a host/IP or `*`"));
2246 }
2247
2248 #[test]
2249 fn netallow_unknown_scheme_rejected() {
2250 for spec in ["sctp://host:1234", "icmp-raw://*"] {
2253 let err = NetRule::parse_allow(spec).unwrap_err();
2254 assert!(format!("{}", err).contains("unknown scheme"), "spec: {}", spec);
2255 }
2256 }
2257
2258 #[tokio::test]
2259 async fn test_resolve_net_allow_empty() {
2260 let resolved = resolve_net_allow(&[]).await.unwrap();
2261 assert!(resolved.tcp.per_ip.is_empty());
2262 assert!(resolved.tcp.any_ip_ports.is_empty());
2263 assert!(resolved.udp.per_ip.is_empty());
2264 assert!(resolved.icmp.per_ip.is_empty());
2265 assert!(resolved.concrete_host_entries.is_empty());
2267 }
2268
2269 #[tokio::test]
2270 async fn test_resolve_net_allow_concrete_host() {
2271 let rules = vec![NetAllow {
2272 protocol: Protocol::Tcp,
2273 target: NetTarget::Host("localhost".to_string()),
2274 ports: vec![80, 443],
2275 all_ports: false,
2276 }];
2277 let resolved = resolve_net_allow(&rules).await.unwrap();
2278 assert!(!resolved.tcp.per_ip.is_empty());
2281 for ports in resolved.tcp.per_ip.values() {
2282 assert!(ports.contains(&80));
2283 assert!(ports.contains(&443));
2284 }
2285 assert!(resolved.udp.per_ip.is_empty());
2286 assert!(resolved.icmp.per_ip.is_empty());
2287 assert!(resolved.concrete_host_entries.contains("127.0.0.1 localhost"));
2289 }
2290
2291 #[tokio::test]
2292 async fn test_resolve_net_allow_any_ip() {
2293 let rules = vec![NetAllow {
2294 protocol: Protocol::Tcp,
2295 target: NetTarget::AnyIp,
2296 ports: vec![8080],
2297 all_ports: false,
2298 }];
2299 let resolved = resolve_net_allow(&rules).await.unwrap();
2300 assert!(resolved.tcp.per_ip.is_empty());
2301 assert!(resolved.tcp.any_ip_ports.contains(&8080));
2302 assert!(!resolved.tcp.any_ip_all_ports);
2303 assert!(resolved.concrete_host_entries.is_empty());
2305 }
2306
2307 #[tokio::test]
2308 async fn test_resolve_net_allow_any_ip_all_ports() {
2309 let rules = vec![NetAllow {
2311 protocol: Protocol::Tcp,
2312 target: NetTarget::AnyIp,
2313 ports: vec![],
2314 all_ports: true,
2315 }];
2316 let resolved = resolve_net_allow(&rules).await.unwrap();
2317 assert!(resolved.tcp.any_ip_all_ports);
2318 assert!(resolved.tcp.per_ip.is_empty());
2319 assert!(resolved.tcp.per_ip_all_ports.is_empty());
2320 assert!(resolved.tcp.any_ip_ports.is_empty());
2321 assert!(!resolved.udp.any_ip_all_ports);
2323 assert!(!resolved.icmp.any_ip_all_ports);
2324 }
2325
2326 #[tokio::test]
2327 async fn test_resolve_net_allow_concrete_host_all_ports() {
2328 let rules = vec![NetAllow {
2330 protocol: Protocol::Tcp,
2331 target: NetTarget::Host("localhost".to_string()),
2332 ports: vec![],
2333 all_ports: true,
2334 }];
2335 let resolved = resolve_net_allow(&rules).await.unwrap();
2336 assert!(!resolved.tcp.any_ip_all_ports);
2337 assert!(
2338 !resolved.tcp.per_ip_all_ports.is_empty(),
2339 "localhost should resolve to at least one IP marked as any-port"
2340 );
2341 for ip in resolved.tcp.per_ip_all_ports.iter() {
2342 assert!(resolved.tcp.per_ip.contains_key(ip));
2343 }
2344 assert!(resolved.concrete_host_entries.contains("localhost"));
2345 }
2346
2347 #[tokio::test]
2348 async fn test_resolve_net_allow_mixed_wildcard_and_concrete() {
2349 let rules = vec![
2354 NetAllow {
2355 protocol: Protocol::Tcp,
2356 target: NetTarget::AnyIp,
2357 ports: vec![],
2358 all_ports: true,
2359 },
2360 NetAllow {
2361 protocol: Protocol::Tcp,
2362 target: NetTarget::Host("localhost".to_string()),
2363 ports: vec![22],
2364 all_ports: false,
2365 },
2366 ];
2367 let resolved = resolve_net_allow(&rules).await.unwrap();
2368 assert!(resolved.tcp.any_ip_all_ports);
2369 assert!(!resolved.tcp.per_ip.is_empty());
2370 }
2371
2372 #[tokio::test]
2377 async fn test_resolve_per_protocol_isolation() {
2378 let rules = vec![
2381 NetAllow {
2382 protocol: Protocol::Tcp,
2383 target: NetTarget::Host("localhost".to_string()),
2384 ports: vec![443],
2385 all_ports: false,
2386 },
2387 NetAllow {
2388 protocol: Protocol::Udp,
2389 target: NetTarget::AnyIp,
2390 ports: vec![53],
2391 all_ports: false,
2392 },
2393 ];
2394 let resolved = resolve_net_allow(&rules).await.unwrap();
2395 assert!(
2396 !resolved.tcp.per_ip.is_empty(),
2397 "TCP rule should populate tcp set"
2398 );
2399 assert!(
2400 resolved.udp.any_ip_ports.contains(&53),
2401 "UDP rule should populate udp set"
2402 );
2403 for ports in resolved.tcp.per_ip.values() {
2406 assert!(!ports.contains(&53), "UDP port leaked into TCP set");
2407 }
2408 assert!(!resolved.udp.any_ip_ports.contains(&443), "TCP port leaked into UDP set");
2409 }
2410
2411 #[tokio::test]
2412 async fn test_resolve_icmp_no_ports() {
2413 let rules = vec![NetAllow {
2416 protocol: Protocol::Icmp,
2417 target: NetTarget::Host("localhost".to_string()),
2418 ports: vec![],
2419 all_ports: false,
2420 }];
2421 let resolved = resolve_net_allow(&rules).await.unwrap();
2422 assert!(
2423 !resolved.icmp.per_ip.is_empty(),
2424 "icmp host should populate per_ip"
2425 );
2426 assert!(
2427 !resolved.icmp.per_ip_all_ports.is_empty(),
2428 "icmp host should mark per_ip_all_ports (no port check)"
2429 );
2430 assert!(resolved.icmp.any_ip_ports.is_empty());
2431 assert!(resolved.tcp.per_ip.is_empty());
2433 assert!(resolved.udp.per_ip.is_empty());
2434 }
2435
2436 #[tokio::test]
2437 async fn test_resolve_icmp_wildcard() {
2438 let rules = vec![NetAllow {
2440 protocol: Protocol::Icmp,
2441 target: NetTarget::AnyIp,
2442 ports: vec![],
2443 all_ports: false,
2444 }];
2445 let resolved = resolve_net_allow(&rules).await.unwrap();
2446 assert!(resolved.icmp.any_ip_all_ports);
2447 assert!(!resolved.tcp.any_ip_all_ports);
2448 }
2449
2450 use std::io::Write;
2455
2456 fn temp_rootfs_with_hosts(name: &str, hosts_content: Option<&str>) -> std::path::PathBuf {
2457 let dir = std::env::temp_dir().join(format!(
2458 "sandlock-test-compose-hosts-{}-{}",
2459 name, std::process::id()
2460 ));
2461 let _ = std::fs::create_dir_all(dir.join("etc"));
2462 if let Some(content) = hosts_content {
2463 let mut f = std::fs::File::create(dir.join("etc").join("hosts")).unwrap();
2464 f.write_all(content.as_bytes()).unwrap();
2465 }
2466 dir
2467 }
2468
2469 #[test]
2470 fn compose_no_chroot_emits_loopback_base() {
2471 let out = compose_virtual_etc_hosts(None, "");
2474 assert_eq!(out, "127.0.0.1 localhost\n::1 localhost\n");
2475 }
2476
2477 #[test]
2478 fn compose_no_chroot_appends_concrete_entries() {
2479 let out = compose_virtual_etc_hosts(None, "10.0.0.1 api\n");
2480 assert_eq!(out, "127.0.0.1 localhost\n::1 localhost\n10.0.0.1 api\n");
2481 }
2482
2483 #[test]
2484 fn compose_chroot_seeds_from_image_and_injects_missing_loopback() {
2485 let rootfs = temp_rootfs_with_hosts(
2489 "no-localhost",
2490 Some("10.0.0.5 myimage.local\n"),
2491 );
2492 let out = compose_virtual_etc_hosts(Some(&rootfs), "");
2493 assert!(out.contains("10.0.0.5 myimage.local"), "image entry missing: {out}");
2494 assert!(out.contains("127.0.0.1 localhost"), "v4 loopback missing: {out}");
2495 assert!(out.contains("::1 localhost"), "v6 loopback missing: {out}");
2496 let _ = std::fs::remove_dir_all(&rootfs);
2497 }
2498
2499 #[test]
2500 fn compose_chroot_does_not_duplicate_existing_loopback() {
2501 let rootfs = temp_rootfs_with_hosts(
2503 "both-localhost",
2504 Some("127.0.0.1 localhost\n::1 localhost\n10.0.0.5 myimage.local\n"),
2505 );
2506 let out = compose_virtual_etc_hosts(Some(&rootfs), "");
2507 assert_eq!(out.matches("127.0.0.1 localhost").count(), 1, "v4 dup'd: {out}");
2508 assert_eq!(out.matches("::1 localhost").count(), 1, "v6 dup'd: {out}");
2509 assert!(out.contains("10.0.0.5 myimage.local"));
2510 let _ = std::fs::remove_dir_all(&rootfs);
2511 }
2512
2513 #[test]
2514 fn compose_chroot_injects_only_missing_family() {
2515 let rootfs = temp_rootfs_with_hosts(
2517 "only-v4-localhost",
2518 Some("127.0.0.1 localhost myimage\n"),
2519 );
2520 let out = compose_virtual_etc_hosts(Some(&rootfs), "");
2521 assert_eq!(out.matches("127.0.0.1 localhost").count(), 1);
2522 assert!(out.contains("::1 localhost"), "v6 loopback should be injected: {out}");
2523 let _ = std::fs::remove_dir_all(&rootfs);
2524 }
2525
2526 #[test]
2527 fn compose_chroot_missing_file_falls_back_to_loopback() {
2528 let rootfs = temp_rootfs_with_hosts("no-file", None);
2531 let out = compose_virtual_etc_hosts(Some(&rootfs), "10.0.0.1 api\n");
2532 assert_eq!(out, "127.0.0.1 localhost\n::1 localhost\n10.0.0.1 api\n");
2533 let _ = std::fs::remove_dir_all(&rootfs);
2534 }
2535
2536 #[test]
2537 fn compose_chroot_strips_inline_comments_when_detecting_loopback() {
2538 let rootfs = temp_rootfs_with_hosts(
2542 "with-comments",
2543 Some("127.0.0.1 # localhost is a comment here\n"),
2544 );
2545 let out = compose_virtual_etc_hosts(Some(&rootfs), "");
2546 assert!(
2548 out.lines().any(|l| l.trim() == "127.0.0.1 localhost"),
2549 "v4 loopback should still be injected: {out}"
2550 );
2551 let _ = std::fs::remove_dir_all(&rootfs);
2552 }
2553
2554 #[test]
2557 fn ipcidr_parse_bare_ipv4_is_host_route() {
2558 let c = IpCidr::parse("1.2.3.4").unwrap();
2559 assert_eq!(c.prefix_len, 32);
2560 assert!(c.contains("1.2.3.4".parse().unwrap()));
2561 assert!(!c.contains("1.2.3.5".parse().unwrap()));
2562 }
2563
2564 #[test]
2565 fn ipcidr_parse_ipv4_range_contains() {
2566 let c = IpCidr::parse("10.0.0.0/8").unwrap();
2567 assert!(c.contains("10.3.7.9".parse().unwrap()));
2568 assert!(!c.contains("11.0.0.1".parse().unwrap()));
2569 }
2570
2571 #[test]
2572 fn ipcidr_parse_ipv6_range_contains() {
2573 let c = IpCidr::parse("fc00::/7").unwrap();
2574 assert!(c.contains("fd00::1".parse().unwrap()));
2575 assert!(!c.contains("2001:db8::1".parse().unwrap()));
2576 }
2577
2578 #[test]
2579 fn ipcidr_zero_prefix_matches_all_same_family() {
2580 let c = IpCidr::parse("0.0.0.0/0").unwrap();
2581 assert!(c.contains("8.8.8.8".parse().unwrap()));
2582 assert!(!c.contains("::1".parse().unwrap())); }
2584
2585 #[test]
2586 fn ipcidr_rejects_hostname() {
2587 assert!(IpCidr::parse("example.com").is_err());
2588 }
2589
2590 #[test]
2591 fn ipcidr_rejects_oversized_prefix() {
2592 assert!(IpCidr::parse("10.0.0.0/33").is_err());
2593 assert!(IpCidr::parse("fc00::/129").is_err());
2594 }
2595
2596 #[test]
2599 fn netdeny_bare_cidr_is_all_ports_tcp() {
2600 let rule = NetRule::parse_deny("10.0.0.0/8").unwrap();
2601 assert_eq!(rule.protocol, Protocol::Tcp);
2602 assert!(matches!(rule.target, NetTarget::Cidr(_)));
2603 assert!(rule.all_ports);
2604 }
2605
2606 #[test]
2607 fn netdeny_bare_ip_is_host_route_all_ports() {
2608 let rule = NetRule::parse_deny("169.254.169.254").unwrap();
2609 match &rule.target {
2610 NetTarget::Cidr(c) => assert_eq!(c.prefix_len, 32),
2611 _ => panic!("expected cidr"),
2612 }
2613 assert!(rule.all_ports);
2614 }
2615
2616 #[test]
2617 fn netdeny_cidr_with_port() {
2618 let rule = NetRule::parse_deny("10.0.0.0/8:443").unwrap();
2619 assert_eq!(rule.ports, vec![443]);
2620 assert!(!rule.all_ports);
2621 }
2622
2623 #[test]
2624 fn netdeny_any_ip_port() {
2625 let rule = NetRule::parse_deny(":25").unwrap();
2626 assert!(matches!(rule.target, NetTarget::AnyIp));
2627 assert_eq!(rule.ports, vec![25]);
2628 }
2629
2630 #[test]
2631 fn netdeny_udp_scheme() {
2632 let rule = NetRule::parse_deny("udp://192.168.0.0/16:53").unwrap();
2633 assert_eq!(rule.protocol, Protocol::Udp);
2634 assert_eq!(rule.ports, vec![53]);
2635 }
2636
2637 #[test]
2638 fn netdeny_ipv6_bracket_port() {
2639 let rule = NetRule::parse_deny("[::1]:443").unwrap();
2640 assert_eq!(rule.ports, vec![443]);
2641 match &rule.target {
2642 NetTarget::Cidr(c) => assert_eq!(c.prefix_len, 128),
2643 _ => panic!("expected cidr"),
2644 }
2645 }
2646
2647 #[test]
2648 fn netdeny_rejects_hostname() {
2649 assert!(NetRule::parse_deny("evil.com:443").is_err());
2650 assert!(NetRule::parse_deny("evil.com").is_err());
2651 }
2652
2653 #[test]
2654 fn netdeny_bare_ipv6_address_all_ports() {
2655 let rule = NetRule::parse_deny("::1").unwrap();
2656 assert!(rule.all_ports);
2657 match &rule.target {
2658 NetTarget::Cidr(c) => assert_eq!(c.prefix_len, 128),
2659 _ => panic!("expected cidr"),
2660 }
2661 }
2662
2663 #[test]
2664 fn netdeny_bare_ipv6_cidr_all_ports() {
2665 let rule = NetRule::parse_deny("fc00::/7").unwrap();
2666 assert!(rule.all_ports);
2667 let ula: std::net::IpAddr = "fd00::1".parse().unwrap();
2668 assert!(matches!(&rule.target, NetTarget::Cidr(c) if c.contains(ula)));
2669 }
2670
2671 #[test]
2672 fn netdeny_empty_icmp_body_is_rejected() {
2673 assert!(NetRule::parse_deny("icmp://").is_err());
2674 }
2675
2676 #[test]
2677 fn netdeny_bare_star_is_any_ip_all_ports() {
2678 let rule = NetRule::parse_deny("*").unwrap();
2681 assert_eq!(rule.protocol, Protocol::Tcp);
2682 assert!(matches!(rule.target, NetTarget::AnyIp));
2683 assert!(rule.all_ports);
2684 assert!(rule.ports.is_empty());
2685 }
2686
2687 #[test]
2688 fn netdeny_udp_bare_star_all_ports() {
2689 let rule = NetRule::parse_deny("udp://*").unwrap();
2690 assert_eq!(rule.protocol, Protocol::Udp);
2691 assert!(matches!(rule.target, NetTarget::AnyIp));
2692 assert!(rule.all_ports);
2693 }
2694
2695 #[test]
2696 fn netdeny_empty_spec_rejected() {
2697 assert!(NetRule::parse_deny("").is_err());
2699 assert!(NetRule::parse_deny("udp://").is_err());
2700 }
2701
2702 #[test]
2705 fn resolve_net_deny_groups_per_protocol() {
2706 let rule = NetRule::parse_deny("10.0.0.0/8").unwrap();
2707 let set = resolve_net_deny(std::slice::from_ref(&rule));
2708 assert!(!set.tcp.allows("10.0.0.1".parse().unwrap(), 443));
2710 assert!(set.udp.allows("10.0.0.1".parse().unwrap(), 443));
2711 }
2712
2713 #[test]
2714 fn resolve_net_deny_any_ip_port() {
2715 let rule = NetRule::parse_deny(":25").unwrap();
2716 let set = resolve_net_deny(std::slice::from_ref(&rule));
2717 assert!(!set.tcp.allows("8.8.8.8".parse().unwrap(), 25));
2718 assert!(set.tcp.allows("8.8.8.8".parse().unwrap(), 80));
2719 }
2720}