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, Hash, Serialize, Deserialize)]
38#[serde(rename_all = "lowercase")]
39pub enum Protocol {
40 Tcp,
41 Udp,
42 Icmp,
43}
44
45impl Protocol {
46 fn parse(s: &str) -> Option<Self> {
47 match s {
48 "tcp" => Some(Protocol::Tcp),
49 "udp" => Some(Protocol::Udp),
50 "icmp" => Some(Protocol::Icmp),
51 _ => None,
52 }
53 }
54}
55
56#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
66pub struct NetAllow {
67 #[serde(default = "default_protocol_tcp")]
69 pub protocol: Protocol,
70 pub host: Option<String>,
72 pub ports: Vec<u16>,
75 #[serde(default)]
79 pub all_ports: bool,
80}
81
82fn default_protocol_tcp() -> Protocol {
83 Protocol::Tcp
84}
85
86impl NetAllow {
87 pub fn parse(s: &str) -> Result<Self, SandboxError> {
99 let (protocol, rest) = match s.split_once("://") {
102 Some((scheme, body)) => {
103 let proto = Protocol::parse(scheme).ok_or_else(|| {
104 SandboxError::Invalid(format!(
105 "--net-allow: unknown scheme `{}://` in `{}` (expected tcp, udp, icmp)",
106 scheme, s
107 ))
108 })?;
109 (proto, body)
110 }
111 None => (Protocol::Tcp, s),
112 };
113
114 if protocol == Protocol::Icmp {
115 return Self::parse_icmp(rest, s);
116 }
117
118 let (host_part, port_part) = rest.rsplit_once(':').ok_or_else(|| {
119 SandboxError::Invalid(format!(
120 "--net-allow: expected `host:port` or `:port`, got `{}`",
121 s
122 ))
123 })?;
124 let host = match host_part {
125 "" | "*" => None,
126 h => Some(h.to_string()),
127 };
128
129 let mut ports = Vec::new();
133 let mut saw_wildcard = false;
134 for p in port_part.split(',') {
135 let p = p.trim();
136 if p == "*" {
137 saw_wildcard = true;
138 continue;
139 }
140 let n: u16 = p.parse().map_err(|_| {
141 SandboxError::Invalid(format!("--net-allow: invalid port `{}` in `{}`", p, s))
142 })?;
143 if n == 0 {
144 return Err(SandboxError::Invalid(format!(
145 "--net-allow: port 0 is not valid in `{}`",
146 s
147 )));
148 }
149 ports.push(n);
150 }
151 if saw_wildcard && !ports.is_empty() {
152 return Err(SandboxError::Invalid(format!(
153 "--net-allow: cannot mix `*` with concrete ports in `{}`",
154 s
155 )));
156 }
157 if !saw_wildcard && ports.is_empty() {
158 return Err(SandboxError::Invalid(format!(
159 "--net-allow: at least one port required in `{}`",
160 s
161 )));
162 }
163 Ok(NetAllow {
164 protocol,
165 host,
166 ports,
167 all_ports: saw_wildcard,
168 })
169 }
170
171 fn parse_icmp(body: &str, full: &str) -> Result<Self, SandboxError> {
174 if body.contains(':') {
175 return Err(SandboxError::Invalid(format!(
176 "--net-allow: icmp rules take no port, got `{}`",
177 full
178 )));
179 }
180 if body.is_empty() {
181 return Err(SandboxError::Invalid(format!(
182 "--net-allow: icmp rule needs a host or `*`, got `{}`",
183 full
184 )));
185 }
186 let host = match body {
187 "*" => None,
188 h => Some(h.to_string()),
189 };
190 Ok(NetAllow {
191 protocol: Protocol::Icmp,
192 host,
193 ports: Vec::new(),
194 all_ports: false,
195 })
196 }
197}
198
199fn parse_ip_from_sockaddr(bytes: &[u8]) -> Option<IpAddr> {
206 if bytes.len() < 2 {
207 return None;
208 }
209 let family = u16::from_ne_bytes([bytes[0], bytes[1]]) as u32;
210 match family {
211 f if f == AF_INET => {
212 if bytes.len() < 8 {
213 return None;
214 }
215 Some(IpAddr::V4(Ipv4Addr::new(
216 bytes[4], bytes[5], bytes[6], bytes[7],
217 )))
218 }
219 f if f == AF_INET6 => {
220 if bytes.len() < 24 {
221 return None;
222 }
223 let mut addr_bytes = [0u8; 16];
224 addr_bytes.copy_from_slice(&bytes[8..24]);
225 Some(IpAddr::V6(Ipv6Addr::from(addr_bytes)))
226 }
227 _ => None,
228 }
229}
230
231fn parse_port_from_sockaddr(bytes: &[u8]) -> Option<u16> {
238 if bytes.len() < 4 {
239 return None;
240 }
241 let family = u16::from_ne_bytes([bytes[0], bytes[1]]) as u32;
242 match family {
243 f if f == AF_INET || f == AF_INET6 => {
244 Some(u16::from_be_bytes([bytes[2], bytes[3]]))
245 }
246 _ => None,
247 }
248}
249
250fn set_port_in_sockaddr(bytes: &mut [u8], port: u16) {
251 if bytes.len() >= 4 {
252 let port_bytes = port.to_be_bytes();
253 bytes[2] = port_bytes[0];
254 bytes[3] = port_bytes[1];
255 }
256}
257
258fn query_socket_protocol(fd: RawFd) -> Option<Protocol> {
269 let mut proto: libc::c_int = 0;
270 let mut len: libc::socklen_t = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
271 let rc = unsafe {
272 libc::getsockopt(
273 fd,
274 libc::SOL_SOCKET,
275 libc::SO_PROTOCOL,
276 &mut proto as *mut _ as *mut libc::c_void,
277 &mut len,
278 )
279 };
280 if rc != 0 {
281 return None;
282 }
283 match proto {
284 libc::IPPROTO_TCP => Some(Protocol::Tcp),
285 libc::IPPROTO_UDP => Some(Protocol::Udp),
286 libc::IPPROTO_ICMP | libc::IPPROTO_ICMPV6 => Some(Protocol::Icmp),
290 _ => None,
291 }
292}
293
294async fn connect_on_behalf(
306 notif: &SeccompNotif,
307 ctx: &Arc<SupervisorCtx>,
308 notif_fd: RawFd,
309) -> NotifAction {
310 let args = ¬if.data.args;
311 let sockfd = args[0] as i32;
312 let addr_ptr = args[1];
313 let addr_len = args[2] as u32;
314
315 let addr_bytes =
317 match read_child_mem(notif_fd, notif.id, notif.pid, addr_ptr, addr_len as usize) {
318 Ok(b) => b,
319 Err(_) => return NotifAction::Errno(libc::EIO),
320 };
321
322 if let Some(ip) = parse_ip_from_sockaddr(&addr_bytes) {
330 let dest_port = parse_port_from_sockaddr(&addr_bytes);
331 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
332 Ok(fd) => fd,
333 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
334 };
335 let protocol = match query_socket_protocol(dup_fd.as_raw_fd()) {
336 Some(p) => p,
337 None => return NotifAction::Errno(ECONNREFUSED),
338 };
339 let ns = ctx.network.lock().await;
340 let live_policy = {
341 let pfs = ctx.policy_fn.lock().await;
342 pfs.live_policy.clone()
343 };
344 let effective = ns.effective_network_policy(notif.pid, protocol, live_policy.as_ref());
345 match (effective, dest_port) {
346 (crate::seccomp::notif::NetworkPolicy::Unrestricted, _) => {
347 }
351 (policy, Some(p)) => {
352 if !policy.allows(ip, p) {
356 return NotifAction::Errno(ECONNREFUSED);
357 }
358 }
359 (_, None) => {
360 return NotifAction::Errno(ECONNREFUSED);
362 }
363 }
364 let http_acl_addr = ns.http_acl_addr;
366 let http_acl_intercept = dest_port.map_or(false, |p| ns.http_acl_ports.contains(&p));
367 let http_acl_orig_dest = ns.http_acl_orig_dest.clone();
368 let remapped_loopback_port = if ctx.policy.port_remap && ip.is_loopback() {
369 dest_port.and_then(|p| ns.port_map.get_real(p))
370 } else {
371 None
372 };
373
374 drop(ns);
375
376 let mut redirected = false;
378 let is_ipv6 = parse_ip_from_sockaddr(&addr_bytes)
379 .map_or(false, |ip| ip.is_ipv6());
380 let (mut connect_addr, connect_len) = if let Some(proxy_addr) = http_acl_addr {
381 if http_acl_intercept {
382 redirected = true;
383 if is_ipv6 {
384 let mut sa6: libc::sockaddr_in6 = unsafe { std::mem::zeroed() };
387 sa6.sin6_family = libc::AF_INET6 as u16;
388 sa6.sin6_port = proxy_addr.port().to_be();
389 let mapped = std::net::Ipv6Addr::from(
391 match proxy_addr {
392 std::net::SocketAddr::V4(v4) => v4.ip().to_ipv6_mapped(),
393 std::net::SocketAddr::V6(v6) => *v6.ip(),
394 }
395 );
396 sa6.sin6_addr.s6_addr = mapped.octets();
397 let bytes = unsafe {
398 std::slice::from_raw_parts(
399 &sa6 as *const _ as *const u8,
400 std::mem::size_of::<libc::sockaddr_in6>(),
401 )
402 }
403 .to_vec();
404 (bytes, std::mem::size_of::<libc::sockaddr_in6>() as u32)
405 } else {
406 let mut sa: libc::sockaddr_in = unsafe { std::mem::zeroed() };
408 sa.sin_family = libc::AF_INET as u16;
409 sa.sin_port = proxy_addr.port().to_be();
410 match proxy_addr {
411 std::net::SocketAddr::V4(v4) => {
412 sa.sin_addr.s_addr = u32::from_ne_bytes(v4.ip().octets());
413 }
414 std::net::SocketAddr::V6(_) => {
415 return NotifAction::Errno(libc::EAFNOSUPPORT);
417 }
418 }
419 let bytes = unsafe {
420 std::slice::from_raw_parts(
421 &sa as *const _ as *const u8,
422 std::mem::size_of::<libc::sockaddr_in>(),
423 )
424 }
425 .to_vec();
426 (bytes, std::mem::size_of::<libc::sockaddr_in>() as u32)
427 }
428 } else {
429 (addr_bytes.clone(), addr_len)
430 }
431 } else {
432 (addr_bytes.clone(), addr_len)
433 };
434 if !redirected {
435 if let Some(real_port) = remapped_loopback_port {
436 set_port_in_sockaddr(&mut connect_addr, real_port);
439 }
440 }
441
442 if redirected {
451 if let Some(ref orig_dest_map) = http_acl_orig_dest {
452 if let Some(orig_ip) = parse_ip_from_sockaddr(&addr_bytes) {
453 if is_ipv6 {
456 let mut bind_sa6: libc::sockaddr_in6 = unsafe { std::mem::zeroed() };
457 bind_sa6.sin6_family = libc::AF_INET6 as u16;
458 unsafe {
460 libc::bind(
461 dup_fd.as_raw_fd(),
462 &bind_sa6 as *const _ as *const libc::sockaddr,
463 std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
464 );
465 }
466 let mut local_sa6: libc::sockaddr_in6 = unsafe { std::mem::zeroed() };
467 let mut local_len: libc::socklen_t =
468 std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t;
469 let gs_ret = unsafe {
470 libc::getsockname(
471 dup_fd.as_raw_fd(),
472 &mut local_sa6 as *mut _ as *mut libc::sockaddr,
473 &mut local_len,
474 )
475 };
476 if gs_ret == 0 {
477 let local_port = u16::from_be(local_sa6.sin6_port);
478 let local_ip = Ipv6Addr::from(local_sa6.sin6_addr.s6_addr);
479 let local_addr = std::net::SocketAddr::V6(
480 std::net::SocketAddrV6::new(local_ip, local_port, 0, 0),
481 );
482 if let Ok(mut map) = orig_dest_map.write() {
483 map.insert(local_addr, orig_ip);
484 }
485 }
486 } else {
487 let mut bind_sa: libc::sockaddr_in = unsafe { std::mem::zeroed() };
488 bind_sa.sin_family = libc::AF_INET as u16;
489 unsafe {
491 libc::bind(
492 dup_fd.as_raw_fd(),
493 &bind_sa as *const _ as *const libc::sockaddr,
494 std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
495 );
496 }
497 let mut local_sa: libc::sockaddr_in = unsafe { std::mem::zeroed() };
498 let mut local_len: libc::socklen_t =
499 std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t;
500 let gs_ret = unsafe {
501 libc::getsockname(
502 dup_fd.as_raw_fd(),
503 &mut local_sa as *mut _ as *mut libc::sockaddr,
504 &mut local_len,
505 )
506 };
507 if gs_ret == 0 {
508 let local_port = u16::from_be(local_sa.sin_port);
509 let local_ip = Ipv4Addr::from(u32::from_be(local_sa.sin_addr.s_addr));
510 let local_addr = std::net::SocketAddr::V4(
511 std::net::SocketAddrV4::new(local_ip, local_port),
512 );
513 if let Ok(mut map) = orig_dest_map.write() {
514 map.insert(local_addr, orig_ip);
515 }
516 }
517 }
518 }
519 }
520 }
521
522 let ret = unsafe {
524 libc::connect(
525 dup_fd.as_raw_fd(),
526 connect_addr.as_ptr() as *const libc::sockaddr,
527 connect_len as libc::socklen_t,
528 )
529 };
530
531 if ret == 0 {
536 NotifAction::ReturnValue(0)
537 } else {
538 let errno = unsafe { *libc::__errno_location() };
539 NotifAction::Errno(errno)
540 }
541 } else {
543 NotifAction::Continue
545 }
546}
547
548async fn sendto_on_behalf(
564 notif: &SeccompNotif,
565 ctx: &Arc<SupervisorCtx>,
566 notif_fd: RawFd,
567) -> NotifAction {
568 let args = ¬if.data.args;
569 let sockfd = args[0] as i32;
570 let buf_ptr = args[1];
571 let buf_len = args[2] as usize;
572 if buf_len > MAX_SEND_BUF {
573 return NotifAction::Errno(libc::EMSGSIZE);
574 }
575 let flags = args[3] as i32;
576 let addr_ptr = args[4];
577 let addr_len = args[5] as u32;
578
579 if addr_ptr == 0 {
580 return NotifAction::Continue; }
582
583 let addr_bytes =
585 match read_child_mem(notif_fd, notif.id, notif.pid, addr_ptr, addr_len as usize) {
586 Ok(b) => b,
587 Err(_) => return NotifAction::Errno(libc::EIO),
588 };
589
590 if let Some(ip) = parse_ip_from_sockaddr(&addr_bytes) {
594 let dest_port = parse_port_from_sockaddr(&addr_bytes);
595 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
596 Ok(fd) => fd,
597 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
598 };
599 let protocol = match query_socket_protocol(dup_fd.as_raw_fd()) {
600 Some(p) => p,
601 None => return NotifAction::Errno(ECONNREFUSED),
602 };
603 let ns = ctx.network.lock().await;
604 let live_policy = {
605 let pfs = ctx.policy_fn.lock().await;
606 pfs.live_policy.clone()
607 };
608 let effective = ns.effective_network_policy(notif.pid, protocol, live_policy.as_ref());
609 if !matches!(effective, crate::seccomp::notif::NetworkPolicy::Unrestricted) {
610 match dest_port {
611 Some(p) if !effective.allows(ip, p) => {
612 return NotifAction::Errno(ECONNREFUSED);
613 }
614 None => return NotifAction::Errno(ECONNREFUSED),
615 Some(_) => {}
616 }
617 }
618 drop(ns);
619
620 let data = match read_child_mem(notif_fd, notif.id, notif.pid, buf_ptr, buf_len) {
622 Ok(b) => b,
623 Err(_) => return NotifAction::Errno(libc::EIO),
624 };
625
626 let ret = unsafe {
630 libc::sendto(
631 dup_fd.as_raw_fd(),
632 data.as_ptr() as *const libc::c_void,
633 data.len(),
634 flags,
635 addr_bytes.as_ptr() as *const libc::sockaddr,
636 addr_len as libc::socklen_t,
637 )
638 };
639
640 if ret >= 0 {
642 NotifAction::ReturnValue(ret as i64)
643 } else {
644 let errno = unsafe { *libc::__errno_location() };
645 NotifAction::Errno(errno)
646 }
647 } else {
648 NotifAction::Continue
650 }
651}
652
653async fn sendmsg_on_behalf(
664 notif: &SeccompNotif,
665 ctx: &Arc<SupervisorCtx>,
666 notif_fd: RawFd,
667) -> NotifAction {
668 let args = ¬if.data.args;
669 let sockfd = args[0] as i32;
670 let msghdr_ptr = args[1];
671 let flags = args[2] as i32;
672
673 match prescan_msghdr(notif, notif_fd, msghdr_ptr) {
678 PrescanResult::ContinueWholeCall => return NotifAction::Continue,
679 PrescanResult::Errno(e) => return NotifAction::Errno(e),
680 PrescanResult::OnBehalf => {}
681 }
682
683 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
684 Ok(fd) => fd,
685 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
686 };
687 let protocol = match query_socket_protocol(dup_fd.as_raw_fd()) {
688 Some(p) => p,
689 None => return NotifAction::Errno(ECONNREFUSED),
690 };
691
692 match send_msghdr_on_behalf(notif, ctx, notif_fd, &dup_fd, protocol, msghdr_ptr, flags).await {
693 Ok(n) => NotifAction::ReturnValue(n as i64),
694 Err(errno) => NotifAction::Errno(errno),
695 }
696}
697
698#[derive(Clone, Copy)]
703enum PrescanResult {
704 OnBehalf,
707 ContinueWholeCall,
713 Errno(i32),
716}
717
718fn prescan_msghdr(
723 notif: &SeccompNotif,
724 notif_fd: RawFd,
725 msghdr_ptr: u64,
726) -> PrescanResult {
727 let msghdr_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msghdr_ptr, 56) {
728 Ok(b) if b.len() >= 56 => b,
729 _ => return PrescanResult::Errno(libc::EFAULT),
730 };
731 let msg_name_ptr = u64::from_ne_bytes(msghdr_bytes[0..8].try_into().unwrap());
732 if msg_name_ptr == 0 {
733 return PrescanResult::ContinueWholeCall;
734 }
735 let msg_namelen = u32::from_ne_bytes(msghdr_bytes[8..12].try_into().unwrap());
736 let addr_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msg_name_ptr, msg_namelen as usize) {
737 Ok(b) => b,
738 Err(_) => return PrescanResult::Errno(libc::EIO),
739 };
740 if parse_ip_from_sockaddr(&addr_bytes).is_none() {
741 return PrescanResult::ContinueWholeCall;
742 }
743 PrescanResult::OnBehalf
744}
745
746async fn send_msghdr_on_behalf(
759 notif: &SeccompNotif,
760 ctx: &Arc<SupervisorCtx>,
761 notif_fd: RawFd,
762 dup_fd: &std::os::unix::io::OwnedFd,
763 protocol: Protocol,
764 msghdr_ptr: u64,
765 flags: i32,
766) -> Result<isize, i32> {
767 let msghdr_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msghdr_ptr, 56) {
768 Ok(b) if b.len() >= 56 => b,
769 _ => return Err(libc::EFAULT),
770 };
771 let msg_name_ptr = u64::from_ne_bytes(msghdr_bytes[0..8].try_into().unwrap());
772 let msg_namelen = u32::from_ne_bytes(msghdr_bytes[8..12].try_into().unwrap());
773 let msg_iov_ptr = u64::from_ne_bytes(msghdr_bytes[16..24].try_into().unwrap());
774 let msg_iovlen = u64::from_ne_bytes(msghdr_bytes[24..32].try_into().unwrap());
775 let msg_control_ptr = u64::from_ne_bytes(msghdr_bytes[32..40].try_into().unwrap());
776 let msg_controllen = u64::from_ne_bytes(msghdr_bytes[40..48].try_into().unwrap());
777
778 let addr_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msg_name_ptr, msg_namelen as usize) {
779 Ok(b) => b,
780 Err(_) => return Err(libc::EIO),
781 };
782 let ip = match parse_ip_from_sockaddr(&addr_bytes) {
783 Some(ip) => ip,
784 None => return Err(libc::EAFNOSUPPORT),
788 };
789 let dest_port = parse_port_from_sockaddr(&addr_bytes);
790
791 let ns = ctx.network.lock().await;
792 let live_policy = {
793 let pfs = ctx.policy_fn.lock().await;
794 pfs.live_policy.clone()
795 };
796 let effective = ns.effective_network_policy(notif.pid, protocol, live_policy.as_ref());
797 if !matches!(effective, crate::seccomp::notif::NetworkPolicy::Unrestricted) {
798 match dest_port {
799 Some(p) if !effective.allows(ip, p) => return Err(ECONNREFUSED),
800 None => return Err(ECONNREFUSED),
801 Some(_) => {}
802 }
803 }
804 drop(ns);
805
806 let iovlen = (msg_iovlen as usize).min(1024);
807 let iov_size = iovlen * 16;
808 let iov_bytes = match read_child_mem(notif_fd, notif.id, notif.pid, msg_iov_ptr, iov_size) {
809 Ok(b) => b,
810 Err(_) => return Err(libc::EIO),
811 };
812 let mut data_bufs: Vec<Vec<u8>> = Vec::with_capacity(iovlen);
813 let mut local_iovs: Vec<libc::iovec> = Vec::with_capacity(iovlen);
814 for i in 0..iovlen {
815 let off = i * 16;
816 if off + 16 > iov_bytes.len() { break; }
817 let iov_base = u64::from_ne_bytes(iov_bytes[off..off + 8].try_into().unwrap());
818 let iov_len = u64::from_ne_bytes(iov_bytes[off + 8..off + 16].try_into().unwrap()) as usize;
819 if iov_len > MAX_SEND_BUF {
820 return Err(libc::EMSGSIZE);
821 }
822 if iov_base == 0 || iov_len == 0 {
823 data_bufs.push(Vec::new());
824 continue;
825 }
826 let buf = match read_child_mem(notif_fd, notif.id, notif.pid, iov_base, iov_len) {
827 Ok(b) => b,
828 Err(_) => return Err(libc::EIO),
829 };
830 data_bufs.push(buf);
831 }
832 for buf in &data_bufs {
833 local_iovs.push(libc::iovec {
834 iov_base: buf.as_ptr() as *mut libc::c_void,
835 iov_len: buf.len(),
836 });
837 }
838
839 let control_buf = if msg_control_ptr != 0 && msg_controllen > 0 {
840 let len = (msg_controllen as usize).min(4096);
841 read_child_mem(notif_fd, notif.id, notif.pid, msg_control_ptr, len).ok()
842 } else {
843 None
844 };
845
846 let mut msg: libc::msghdr = unsafe { std::mem::zeroed() };
847 msg.msg_name = addr_bytes.as_ptr() as *mut libc::c_void;
848 msg.msg_namelen = addr_bytes.len() as u32;
849 msg.msg_iov = local_iovs.as_mut_ptr();
850 msg.msg_iovlen = local_iovs.len();
851 if let Some(ref ctrl) = control_buf {
852 msg.msg_control = ctrl.as_ptr() as *mut libc::c_void;
853 msg.msg_controllen = ctrl.len();
854 }
855
856 let ret = unsafe { libc::sendmsg(dup_fd.as_raw_fd(), &msg, flags) };
857 if ret >= 0 {
858 Ok(ret)
859 } else {
860 Err(unsafe { *libc::__errno_location() })
861 }
862}
863
864const MMSGHDR_SIZE: usize = 64;
872const MSG_LEN_OFFSET: usize = 56;
873const MAX_MMSGHDR_ENTRIES: usize = 256;
878
879async fn sendmmsg_on_behalf(
892 notif: &SeccompNotif,
893 ctx: &Arc<SupervisorCtx>,
894 notif_fd: RawFd,
895) -> NotifAction {
896 let args = ¬if.data.args;
897 let sockfd = args[0] as i32;
898 let msgvec_ptr = args[1];
899 let vlen = (args[2] as u32 as usize).min(MAX_MMSGHDR_ENTRIES);
900 let flags = args[3] as i32;
901
902 if vlen == 0 {
903 return NotifAction::ReturnValue(0);
904 }
905
906 for i in 0..vlen {
912 let entry_ptr = msgvec_ptr + (i * MMSGHDR_SIZE) as u64;
913 match prescan_msghdr(notif, notif_fd, entry_ptr) {
914 PrescanResult::OnBehalf => continue,
915 PrescanResult::ContinueWholeCall => return NotifAction::Continue,
916 PrescanResult::Errno(e) => return NotifAction::Errno(e),
917 }
918 }
919
920 let dup_fd = match crate::seccomp::notif::dup_fd_from_pid(notif.pid, sockfd) {
921 Ok(fd) => fd,
922 Err(e) => return NotifAction::Errno(e.raw_os_error().unwrap_or(libc::EBADF)),
923 };
924 let protocol = match query_socket_protocol(dup_fd.as_raw_fd()) {
925 Some(p) => p,
926 None => return NotifAction::Errno(ECONNREFUSED),
927 };
928
929 let mut sent: usize = 0;
930 let mut first_errno: Option<i32> = None;
931
932 for i in 0..vlen {
933 let entry_ptr = msgvec_ptr + (i * MMSGHDR_SIZE) as u64;
934 match send_msghdr_on_behalf(notif, ctx, notif_fd, &dup_fd, protocol, entry_ptr, flags).await {
935 Ok(n) => {
936 let bytes = (n as u32).to_ne_bytes();
937 let _ = write_child_mem(
938 notif_fd, notif.id, notif.pid,
939 entry_ptr + MSG_LEN_OFFSET as u64,
940 &bytes,
941 );
942 sent += 1;
943 }
944 Err(errno) => {
945 first_errno = Some(errno);
946 break;
947 }
948 }
949 }
950
951 if sent > 0 {
952 NotifAction::ReturnValue(sent as i64)
953 } else {
954 NotifAction::Errno(first_errno.unwrap_or(ECONNREFUSED))
958 }
959}
960
961pub(crate) async fn handle_net(
989 notif: &SeccompNotif,
990 ctx: &Arc<SupervisorCtx>,
991 notif_fd: RawFd,
992) -> NotifAction {
993 let nr = notif.data.nr as i64;
994
995 if nr == libc::SYS_connect {
996 connect_on_behalf(notif, ctx, notif_fd).await
997 } else if nr == libc::SYS_sendto {
998 sendto_on_behalf(notif, ctx, notif_fd).await
999 } else if nr == libc::SYS_sendmsg {
1000 sendmsg_on_behalf(notif, ctx, notif_fd).await
1001 } else if nr == libc::SYS_sendmmsg {
1002 sendmmsg_on_behalf(notif, ctx, notif_fd).await
1003 } else {
1004 NotifAction::Continue
1005 }
1006}
1007
1008pub struct ResolvedNetAllow {
1014 pub per_ip: HashMap<IpAddr, HashSet<u16>>,
1018 pub per_ip_all_ports: HashSet<IpAddr>,
1023 pub any_ip_ports: HashSet<u16>,
1025 pub any_ip_all_ports: bool,
1029}
1030
1031pub struct ResolvedNetAllowSet {
1037 pub tcp: ResolvedNetAllow,
1038 pub udp: ResolvedNetAllow,
1039 pub icmp: ResolvedNetAllow,
1040 pub etc_hosts: Option<String>,
1043}
1044
1045pub async fn resolve_net_allow(
1054 rules: &[NetAllow],
1055) -> io::Result<ResolvedNetAllowSet> {
1056 let mut etc_hosts = String::from("127.0.0.1 localhost\n::1 localhost\n");
1059 let mut has_concrete_host = false;
1060
1061 let per_proto = |target: Protocol| async move {
1062 let mut per_ip: HashMap<IpAddr, HashSet<u16>> = HashMap::new();
1063 let mut per_ip_all_ports: HashSet<IpAddr> = HashSet::new();
1064 let mut any_ip_ports: HashSet<u16> = HashSet::new();
1065 let mut any_ip_all_ports = false;
1066 let mut local_etc_hosts = String::new();
1067 let mut local_has_concrete = false;
1068
1069 for rule in rules.iter().filter(|r| r.protocol == target) {
1070 match &rule.host {
1071 None => {
1072 if rule.all_ports || target == Protocol::Icmp {
1073 any_ip_all_ports = true;
1076 } else {
1077 for &p in &rule.ports {
1078 any_ip_ports.insert(p);
1079 }
1080 }
1081 }
1082 Some(host) => {
1083 local_has_concrete = true;
1084 let addr = format!("{}:0", host);
1085 let resolved = tokio::net::lookup_host(addr.as_str()).await.map_err(|e| {
1086 io::Error::new(
1087 e.kind(),
1088 format!("failed to resolve host '{}': {}", host, e),
1089 )
1090 })?;
1091 for socket_addr in resolved {
1092 let ip = socket_addr.ip();
1093 if rule.all_ports || target == Protocol::Icmp {
1094 per_ip_all_ports.insert(ip);
1095 per_ip.entry(ip).or_default();
1096 } else {
1097 let entry = per_ip.entry(ip).or_default();
1098 for &p in &rule.ports {
1099 entry.insert(p);
1100 }
1101 }
1102 local_etc_hosts.push_str(&format!("{} {}\n", ip, host));
1103 }
1104 }
1105 }
1106 }
1107
1108 Ok::<_, io::Error>((
1109 ResolvedNetAllow {
1110 per_ip,
1111 per_ip_all_ports,
1112 any_ip_ports,
1113 any_ip_all_ports,
1114 },
1115 local_etc_hosts,
1116 local_has_concrete,
1117 ))
1118 };
1119
1120 let (tcp, tcp_eh, tcp_concrete) = per_proto(Protocol::Tcp).await?;
1121 let (udp, udp_eh, udp_concrete) = per_proto(Protocol::Udp).await?;
1122 let (icmp, icmp_eh, icmp_concrete) = per_proto(Protocol::Icmp).await?;
1123
1124 for chunk in [tcp_eh, udp_eh, icmp_eh] {
1125 etc_hosts.push_str(&chunk);
1126 }
1127 has_concrete_host |= tcp_concrete || udp_concrete || icmp_concrete;
1128
1129 Ok(ResolvedNetAllowSet {
1130 tcp,
1131 udp,
1132 icmp,
1133 etc_hosts: if has_concrete_host { Some(etc_hosts) } else { None },
1134 })
1135}
1136
1137#[cfg(test)]
1142mod tests {
1143 use super::*;
1144
1145 #[test]
1148 fn netallow_parse_concrete_host_port() {
1149 let r = NetAllow::parse("example.com:443").unwrap();
1150 assert_eq!(r.host.as_deref(), Some("example.com"));
1151 assert_eq!(r.ports, vec![443]);
1152 assert!(!r.all_ports);
1153 }
1154
1155 #[test]
1156 fn netallow_parse_any_host_port() {
1157 let r = NetAllow::parse(":8080").unwrap();
1158 assert_eq!(r.host, None);
1159 assert_eq!(r.ports, vec![8080]);
1160 assert!(!r.all_ports);
1161
1162 let r = NetAllow::parse("*:8080").unwrap();
1163 assert_eq!(r.host, None);
1164 assert_eq!(r.ports, vec![8080]);
1165 assert!(!r.all_ports);
1166 }
1167
1168 #[test]
1169 fn netallow_parse_multiple_ports() {
1170 let r = NetAllow::parse("github.com:22,80,443").unwrap();
1171 assert_eq!(r.host.as_deref(), Some("github.com"));
1172 assert_eq!(r.ports, vec![22, 80, 443]);
1173 assert!(!r.all_ports);
1174 }
1175
1176 #[test]
1177 fn netallow_parse_wildcard_any_host_any_port_colon() {
1178 let r = NetAllow::parse(":*").unwrap();
1179 assert_eq!(r.host, None);
1180 assert!(r.ports.is_empty());
1181 assert!(r.all_ports);
1182 }
1183
1184 #[test]
1185 fn netallow_parse_wildcard_any_host_any_port_star() {
1186 let r = NetAllow::parse("*:*").unwrap();
1187 assert_eq!(r.host, None);
1188 assert!(r.ports.is_empty());
1189 assert!(r.all_ports);
1190 }
1191
1192 #[test]
1193 fn netallow_parse_wildcard_concrete_host_any_port() {
1194 let r = NetAllow::parse("example.com:*").unwrap();
1195 assert_eq!(r.host.as_deref(), Some("example.com"));
1196 assert!(r.ports.is_empty());
1197 assert!(r.all_ports);
1198 }
1199
1200 #[test]
1201 fn netallow_parse_rejects_mixed_wildcard_and_concrete() {
1202 let err = NetAllow::parse("example.com:80,*").unwrap_err();
1206 assert!(format!("{}", err).contains("cannot mix"));
1207 let err = NetAllow::parse("example.com:*,80").unwrap_err();
1208 assert!(format!("{}", err).contains("cannot mix"));
1209 }
1210
1211 #[test]
1212 fn netallow_parse_rejects_port_zero() {
1213 let err = NetAllow::parse("example.com:0").unwrap_err();
1214 assert!(format!("{}", err).contains("port 0"));
1215 }
1216
1217 #[test]
1218 fn netallow_parse_rejects_empty_port() {
1219 let err = NetAllow::parse("example.com:").unwrap_err();
1220 assert!(format!("{}", err).contains("invalid port"));
1221 }
1222
1223 #[test]
1224 fn netallow_parse_rejects_no_colon() {
1225 let err = NetAllow::parse("example.com").unwrap_err();
1226 assert!(format!("{}", err).contains("expected"));
1227 }
1228
1229 #[test]
1230 fn netallow_parse_repeated_wildcard_is_idempotent() {
1231 let r = NetAllow::parse(":*,*").unwrap();
1234 assert!(r.all_ports);
1235 assert!(r.ports.is_empty());
1236 }
1237
1238 #[test]
1241 fn netallow_bare_form_defaults_to_tcp() {
1242 let r = NetAllow::parse("example.com:443").unwrap();
1243 assert_eq!(r.protocol, Protocol::Tcp);
1244 }
1245
1246 #[test]
1247 fn netallow_explicit_tcp_scheme() {
1248 let r = NetAllow::parse("tcp://example.com:443").unwrap();
1249 assert_eq!(r.protocol, Protocol::Tcp);
1250 assert_eq!(r.host.as_deref(), Some("example.com"));
1251 assert_eq!(r.ports, vec![443]);
1252 }
1253
1254 #[test]
1255 fn netallow_udp_scheme_with_host_port() {
1256 let r = NetAllow::parse("udp://1.1.1.1:53").unwrap();
1257 assert_eq!(r.protocol, Protocol::Udp);
1258 assert_eq!(r.host.as_deref(), Some("1.1.1.1"));
1259 assert_eq!(r.ports, vec![53]);
1260 }
1261
1262 #[test]
1263 fn netallow_udp_wildcard_any_anywhere() {
1264 let r = NetAllow::parse("udp://*:*").unwrap();
1266 assert_eq!(r.protocol, Protocol::Udp);
1267 assert_eq!(r.host, None);
1268 assert!(r.all_ports);
1269 }
1270
1271 #[test]
1272 fn netallow_icmp_scheme_with_host() {
1273 let r = NetAllow::parse("icmp://github.com").unwrap();
1274 assert_eq!(r.protocol, Protocol::Icmp);
1275 assert_eq!(r.host.as_deref(), Some("github.com"));
1276 assert!(r.ports.is_empty());
1277 assert!(!r.all_ports);
1278 }
1279
1280 #[test]
1281 fn netallow_icmp_wildcard() {
1282 let r = NetAllow::parse("icmp://*").unwrap();
1285 assert_eq!(r.protocol, Protocol::Icmp);
1286 assert_eq!(r.host, None);
1287 }
1288
1289 #[test]
1290 fn netallow_icmp_rejects_port() {
1291 let err = NetAllow::parse("icmp://github.com:80").unwrap_err();
1295 assert!(format!("{}", err).contains("icmp rules take no port"));
1296 }
1297
1298 #[test]
1299 fn netallow_icmp_rejects_empty_body() {
1300 let err = NetAllow::parse("icmp://").unwrap_err();
1301 assert!(format!("{}", err).contains("needs a host or `*`"));
1302 }
1303
1304 #[test]
1305 fn netallow_unknown_scheme_rejected() {
1306 for spec in ["sctp://host:1234", "icmp-raw://*"] {
1309 let err = NetAllow::parse(spec).unwrap_err();
1310 assert!(format!("{}", err).contains("unknown scheme"), "spec: {}", spec);
1311 }
1312 }
1313
1314 #[tokio::test]
1315 async fn test_resolve_net_allow_empty() {
1316 let resolved = resolve_net_allow(&[]).await.unwrap();
1317 assert!(resolved.tcp.per_ip.is_empty());
1318 assert!(resolved.tcp.any_ip_ports.is_empty());
1319 assert!(resolved.udp.per_ip.is_empty());
1320 assert!(resolved.icmp.per_ip.is_empty());
1321 assert!(resolved.etc_hosts.is_none());
1322 }
1323
1324 #[tokio::test]
1325 async fn test_resolve_net_allow_concrete_host() {
1326 let rules = vec![NetAllow {
1327 protocol: Protocol::Tcp,
1328 host: Some("localhost".to_string()),
1329 ports: vec![80, 443],
1330 all_ports: false,
1331 }];
1332 let resolved = resolve_net_allow(&rules).await.unwrap();
1333 assert!(!resolved.tcp.per_ip.is_empty());
1336 for ports in resolved.tcp.per_ip.values() {
1337 assert!(ports.contains(&80));
1338 assert!(ports.contains(&443));
1339 }
1340 assert!(resolved.udp.per_ip.is_empty());
1341 assert!(resolved.icmp.per_ip.is_empty());
1342 assert!(resolved.etc_hosts.as_deref().unwrap_or("").contains("localhost"));
1343 }
1344
1345 #[tokio::test]
1346 async fn test_resolve_net_allow_any_ip() {
1347 let rules = vec![NetAllow {
1348 protocol: Protocol::Tcp,
1349 host: None,
1350 ports: vec![8080],
1351 all_ports: false,
1352 }];
1353 let resolved = resolve_net_allow(&rules).await.unwrap();
1354 assert!(resolved.tcp.per_ip.is_empty());
1355 assert!(resolved.tcp.any_ip_ports.contains(&8080));
1356 assert!(!resolved.tcp.any_ip_all_ports);
1357 assert!(resolved.etc_hosts.is_none());
1358 }
1359
1360 #[tokio::test]
1361 async fn test_resolve_net_allow_any_ip_all_ports() {
1362 let rules = vec![NetAllow {
1364 protocol: Protocol::Tcp,
1365 host: None,
1366 ports: vec![],
1367 all_ports: true,
1368 }];
1369 let resolved = resolve_net_allow(&rules).await.unwrap();
1370 assert!(resolved.tcp.any_ip_all_ports);
1371 assert!(resolved.tcp.per_ip.is_empty());
1372 assert!(resolved.tcp.per_ip_all_ports.is_empty());
1373 assert!(resolved.tcp.any_ip_ports.is_empty());
1374 assert!(!resolved.udp.any_ip_all_ports);
1376 assert!(!resolved.icmp.any_ip_all_ports);
1377 }
1378
1379 #[tokio::test]
1380 async fn test_resolve_net_allow_concrete_host_all_ports() {
1381 let rules = vec![NetAllow {
1383 protocol: Protocol::Tcp,
1384 host: Some("localhost".to_string()),
1385 ports: vec![],
1386 all_ports: true,
1387 }];
1388 let resolved = resolve_net_allow(&rules).await.unwrap();
1389 assert!(!resolved.tcp.any_ip_all_ports);
1390 assert!(
1391 !resolved.tcp.per_ip_all_ports.is_empty(),
1392 "localhost should resolve to at least one IP marked as any-port"
1393 );
1394 for ip in resolved.tcp.per_ip_all_ports.iter() {
1395 assert!(resolved.tcp.per_ip.contains_key(ip));
1396 }
1397 assert!(resolved.etc_hosts.is_some());
1398 }
1399
1400 #[tokio::test]
1401 async fn test_resolve_net_allow_mixed_wildcard_and_concrete() {
1402 let rules = vec![
1407 NetAllow {
1408 protocol: Protocol::Tcp,
1409 host: None,
1410 ports: vec![],
1411 all_ports: true,
1412 },
1413 NetAllow {
1414 protocol: Protocol::Tcp,
1415 host: Some("localhost".to_string()),
1416 ports: vec![22],
1417 all_ports: false,
1418 },
1419 ];
1420 let resolved = resolve_net_allow(&rules).await.unwrap();
1421 assert!(resolved.tcp.any_ip_all_ports);
1422 assert!(!resolved.tcp.per_ip.is_empty());
1423 }
1424
1425 #[tokio::test]
1430 async fn test_resolve_per_protocol_isolation() {
1431 let rules = vec![
1434 NetAllow {
1435 protocol: Protocol::Tcp,
1436 host: Some("localhost".to_string()),
1437 ports: vec![443],
1438 all_ports: false,
1439 },
1440 NetAllow {
1441 protocol: Protocol::Udp,
1442 host: None,
1443 ports: vec![53],
1444 all_ports: false,
1445 },
1446 ];
1447 let resolved = resolve_net_allow(&rules).await.unwrap();
1448 assert!(
1449 !resolved.tcp.per_ip.is_empty(),
1450 "TCP rule should populate tcp set"
1451 );
1452 assert!(
1453 resolved.udp.any_ip_ports.contains(&53),
1454 "UDP rule should populate udp set"
1455 );
1456 for ports in resolved.tcp.per_ip.values() {
1459 assert!(!ports.contains(&53), "UDP port leaked into TCP set");
1460 }
1461 assert!(!resolved.udp.any_ip_ports.contains(&443), "TCP port leaked into UDP set");
1462 }
1463
1464 #[tokio::test]
1465 async fn test_resolve_icmp_no_ports() {
1466 let rules = vec![NetAllow {
1469 protocol: Protocol::Icmp,
1470 host: Some("localhost".to_string()),
1471 ports: vec![],
1472 all_ports: false,
1473 }];
1474 let resolved = resolve_net_allow(&rules).await.unwrap();
1475 assert!(
1476 !resolved.icmp.per_ip.is_empty(),
1477 "icmp host should populate per_ip"
1478 );
1479 assert!(
1480 !resolved.icmp.per_ip_all_ports.is_empty(),
1481 "icmp host should mark per_ip_all_ports (no port check)"
1482 );
1483 assert!(resolved.icmp.any_ip_ports.is_empty());
1484 assert!(resolved.tcp.per_ip.is_empty());
1486 assert!(resolved.udp.per_ip.is_empty());
1487 }
1488
1489 #[tokio::test]
1490 async fn test_resolve_icmp_wildcard() {
1491 let rules = vec![NetAllow {
1493 protocol: Protocol::Icmp,
1494 host: None,
1495 ports: vec![],
1496 all_ports: false,
1497 }];
1498 let resolved = resolve_net_allow(&rules).await.unwrap();
1499 assert!(resolved.icmp.any_ip_all_ports);
1500 assert!(!resolved.tcp.any_ip_all_ports);
1501 }
1502}