diff --git a/src/strace/mod.rs b/src/strace/mod.rs index b4f4c12..1fc9eac 100644 --- a/src/strace/mod.rs +++ b/src/strace/mod.rs @@ -71,6 +71,14 @@ impl IntegerExpression { _ => false, // if it was a flag field, strace would have decoded it with named consts } } + + pub fn flags(&self) -> Vec { + match self { + IntegerExpression::NamedConst(v) => vec![v.clone()], + IntegerExpression::BinaryOr(vs) => vs.iter().flat_map(|v| v.flags()).collect(), + _ => vec![], + } + } } pub type SyscallRetVal = i128; // allows holding both signed and unsigned 64 bit integers diff --git a/src/summarize.rs b/src/summarize.rs index b1d593d..848e071 100644 --- a/src/summarize.rs +++ b/src/summarize.rs @@ -8,6 +8,7 @@ use std::path::{Path, PathBuf}; use lazy_static::lazy_static; use crate::strace::{BufferType, IntegerExpression, Syscall, SyscallArg}; +use crate::systemd::{SocketFamily, SocketProtocol}; /// A high level program runtime action /// This does *not* map 1-1 with a syscall, and does *not* necessarily respect chronology @@ -19,12 +20,17 @@ pub enum ProgramAction { Write(PathBuf), /// Path was created Create(PathBuf), - /// Network (socket) activity + /// Generic network (socket) activity NetworkActivity { af: String }, /// Memory mapping with write and execute bits WriteExecuteMemoryMapping, /// Set scheduler to a real time one SetRealtimeScheduler, + /// Bind socket + SocketBind { + af: SocketFamily, + proto: SocketProtocol, + }, /// Names of the syscalls made by the program Syscalls(HashSet), } @@ -205,6 +211,9 @@ where { let mut actions = Vec::new(); let mut stats: HashMap = HashMap::new(); + // keep known socket protocols for bind handling, we don't care for the socket closings + // because the fd will be reused or never bound again + let mut known_sockets_proto: HashMap = HashMap::new(); for syscall in syscalls { let syscall = syscall?; log::trace!("{syscall:?}"); @@ -374,6 +383,27 @@ where } _ => (), } + + if name == "bind" { + let fd = if let Some(SyscallArg::Integer { + value: IntegerExpression::Literal(fd), + .. + }) = syscall.args.get(0) + { + fd + } else { + anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args); + }; + if let (Some(af), Some(proto)) = ( + SocketFamily::from_syscall_arg(af), + known_sockets_proto.get(fd), + ) { + actions.push(ProgramAction::SocketBind { + af, + proto: proto.clone(), + }); + } + } } Some(SyscallInfo::SetScheduler) => { let policy = if let Some(SyscallArg::Integer { value, .. }) = syscall.args.get(1) { @@ -396,6 +426,19 @@ where anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args); }; actions.push(ProgramAction::NetworkActivity { af }); + + let proto_flags = + if let Some(SyscallArg::Integer { value, .. }) = syscall.args.get(1) { + value.flags() + } else { + anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args); + }; + for proto in proto_flags { + if let Some(known_proto) = SocketProtocol::from_syscall_arg(&proto) { + known_sockets_proto.insert(syscall.ret_val, known_proto); + break; + } + } } Some(SyscallInfo::Mmap { prot_idx }) => { let prot = if let Some(SyscallArg::Integer { value: prot, .. }) = diff --git a/src/systemd/mod.rs b/src/systemd/mod.rs index d01dc49..e6e726e 100644 --- a/src/systemd/mod.rs +++ b/src/systemd/mod.rs @@ -5,7 +5,7 @@ mod resolver; mod service; mod version; -pub use options::{build_options, OptionDescription}; +pub use options::{build_options, OptionDescription, SocketFamily, SocketProtocol}; pub use resolver::resolve; pub use service::Service; pub use version::{KernelVersion, SystemdVersion}; diff --git a/src/systemd/options.rs b/src/systemd/options.rs index 176f8fb..96713f1 100644 --- a/src/systemd/options.rs +++ b/src/systemd/options.rs @@ -7,7 +7,9 @@ use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::str::FromStr; +use itertools::Itertools; use lazy_static::lazy_static; +use strum::IntoEnumIterator; use crate::cl::HardeningMode; use crate::systemd::{KernelVersion, SystemdVersion}; @@ -25,13 +27,24 @@ impl fmt::Display for OptionDescription { } } +#[derive(Debug, Clone)] +pub enum ListMode { + WhiteList, + BlackList, +} + /// Systemd option value #[derive(Debug, Clone)] pub enum OptionValue { Boolean(bool), // In most case we only model the 'true' value, because false is no-op and the default String(String), // enum-like, or free string - DenyList(Vec), - AllowList(Vec), + List { + values: Vec, + value_if_empty: Option, + negation_prefix: bool, + repeat_option: bool, + mode: ListMode, + }, } impl FromStr for OptionValue { @@ -99,6 +112,11 @@ pub enum OptionValueEffect { DenyWriteExecuteMemoryMapping, /// Deny real time scheduling DenyRealtimeScheduler, + /// Deny a socket family and protocol socket bind + DenySocketBind { + af: SocketFamily, + proto: SocketProtocol, + }, /// Union of multiple effects Multiple(Vec), } @@ -111,6 +129,43 @@ pub enum DenySyscalls { Single(String), } +// Not a complete enumeration, only used with SocketBindDeny +#[derive(Debug, Clone, Eq, PartialEq, strum::EnumIter, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum SocketFamily { + Ipv4, + Ipv6, +} + +impl SocketFamily { + pub fn from_syscall_arg(s: &str) -> Option { + match s { + "AF_INET" => Some(Self::Ipv4), + "AF_INET6" => Some(Self::Ipv6), + _ => None, + } + } +} + +// Not a complete enumeration, only used with SocketBindDeny +#[derive(Debug, Clone, Eq, PartialEq, strum::EnumIter, strum::Display)] +#[strum(serialize_all = "snake_case")] +pub enum SocketProtocol { + Tcp, + Udp, +} + +impl SocketProtocol { + pub fn from_syscall_arg(s: &str) -> Option { + // Only makes sense for IP addresses + match s { + "SOCK_STREAM" => Some(Self::Tcp), + "SOCK_DGRAM" => Some(Self::Udp), + _ => None, + } + } +} + impl DenySyscalls { /// Get denied syscall names pub fn syscalls(&self) -> HashSet { @@ -159,30 +214,42 @@ impl FromStr for OptionWithValue { impl fmt::Display for OptionWithValue { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}=", self.name)?; match &self.value { OptionValue::Boolean(value) => { - write!(f, "{}", if *value { "true" } else { "false" }) - } - OptionValue::String(value) => { - write!(f, "{value}") + write!(f, "{}={}", self.name, if *value { "true" } else { "false" }) } - OptionValue::DenyList(values) => { - debug_assert!(!values.is_empty()); - write!( - f, - "~{}", - values - .iter() - .map(|v| v.to_string()) - .collect::>() - .join(" ") - ) - } - OptionValue::AllowList(values) => { + OptionValue::String(value) => write!(f, "{}={}", self.name, value), + OptionValue::List { + values, + value_if_empty, + negation_prefix, + repeat_option, + .. + } => { if values.is_empty() { - write!(f, "none") + write!(f, "{}=", self.name)?; + if let Some(value_if_empty) = value_if_empty { + write!(f, "{value_if_empty}") + } else { + unreachable!() + } + } else if *repeat_option { + for (i, value) in values.iter().enumerate() { + write!(f, "{}=", self.name)?; + if *negation_prefix { + write!(f, "~")?; + } + write!(f, "{value}")?; + if i < values.len() - 1 { + writeln!(f)?; + } + } + Ok(()) } else { + write!(f, "{}=", self.name)?; + if *negation_prefix { + write!(f, "~")?; + } write!( f, "{}", @@ -1083,7 +1150,13 @@ pub fn build_options( options.push(OptionDescription { name: "RestrictAddressFamilies".to_string(), possible_values: vec![OptionValueDescription { - value: OptionValue::AllowList(afs.iter().map(|s| s.to_string()).collect()), + value: OptionValue::List { + values: afs.iter().map(|s| s.to_string()).collect(), + value_if_empty: Some("none".to_string()), + negation_prefix: false, + repeat_option: false, + mode: ListMode::WhiteList, + }, desc: OptionEffect::Cumulative( afs.into_iter() .map(|af| OptionValueEffect::DenySocketFamily(af.to_string())) @@ -1111,6 +1184,35 @@ pub fn build_options( }); } + // https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#SocketBindAllow=bind-rule + // + // We don't go as far as allowing/denying individual ports, as that would easily break for example if a port is changed + // in a server configuration + let deny_binds: Vec<_> = SocketFamily::iter() + .cartesian_product(SocketProtocol::iter()) + .collect(); + options.push(OptionDescription { + name: "SocketBindDeny".to_string(), + possible_values: vec![OptionValueDescription { + value: OptionValue::List { + values: deny_binds + .iter() + .map(|(af, proto)| format!("{af}:{proto}")) + .collect(), + value_if_empty: None, + negation_prefix: false, + repeat_option: true, + mode: ListMode::BlackList, + }, + desc: OptionEffect::Cumulative( + deny_binds + .into_iter() + .map(|(af, proto)| OptionValueEffect::DenySocketBind { af, proto }) + .collect(), + ), + }], + }); + // https://www.freedesktop.org/software/systemd/man/systemd.exec.html#LockPersonality= options.push(OptionDescription { name: "LockPersonality".to_string(), @@ -1161,12 +1263,16 @@ pub fn build_options( options.push(OptionDescription { name: "SystemCallFilter".to_string(), possible_values: vec![OptionValueDescription { - value: OptionValue::DenyList( - syscall_classes + value: OptionValue::List { + values: syscall_classes .iter() .map(|c| format!("@{c}:EPERM")) .collect(), - ), + value_if_empty: None, + negation_prefix: true, + repeat_option: false, + mode: ListMode::BlackList, + }, desc: OptionEffect::Cumulative( syscall_classes .into_iter() diff --git a/src/systemd/resolver.rs b/src/systemd/resolver.rs index 61958ae..1e1bf72 100644 --- a/src/systemd/resolver.rs +++ b/src/systemd/resolver.rs @@ -2,7 +2,7 @@ use crate::summarize::ProgramAction; use crate::systemd::options::{ - OptionDescription, OptionEffect, OptionValue, OptionValueEffect, OptionWithValue, + ListMode, OptionDescription, OptionEffect, OptionValue, OptionValueEffect, OptionWithValue, }; impl OptionValueEffect { @@ -43,6 +43,17 @@ impl OptionValueEffect { OptionValueEffect::DenyRealtimeScheduler => { !matches!(action, ProgramAction::SetRealtimeScheduler) } + OptionValueEffect::DenySocketBind { + af: denied_af, + proto: denied_proto, + } => { + if let ProgramAction::SocketBind { af, proto } = action { + if (denied_af == af) && (denied_proto == proto) { + return false; + } + } + true + } OptionValueEffect::Multiple(effects) => { effects.iter().all(|e| e.compatible(action, prev_actions)) } @@ -92,34 +103,37 @@ pub fn resolve( } OptionEffect::Cumulative(effects) => { match &opt_value_desc.value { - OptionValue::DenyList(deny_list) => { + OptionValue::List { + values, + value_if_empty, + negation_prefix, + repeat_option, + mode, + } => { let mut compatible_opts = Vec::new(); - debug_assert_eq!(deny_list.len(), effects.len()); - for (optv, opte) in deny_list.iter().zip(effects) { - if actions_compatible(opte, actions) { + debug_assert_eq!(values.len(), effects.len()); + for (optv, opte) in values.iter().zip(effects) { + let compatible = actions_compatible(opte, actions); + let enable_opt = match mode { + ListMode::WhiteList => !compatible, + ListMode::BlackList => compatible, + }; + if enable_opt { compatible_opts.push(optv.to_string()); } } - if !compatible_opts.is_empty() { + if !compatible_opts.is_empty() || value_if_empty.is_some() { candidates.push(OptionWithValue { name: opt.name.clone(), - value: OptionValue::DenyList(compatible_opts), + value: OptionValue::List { + values: compatible_opts, + value_if_empty: value_if_empty.clone(), + negation_prefix: *negation_prefix, + repeat_option: *repeat_option, + mode: mode.clone(), + }, }); - break; - } - } - OptionValue::AllowList(allow_list) => { - let mut compatible_opts = Vec::new(); - debug_assert_eq!(allow_list.len(), effects.len()); - for (optv, opte) in allow_list.iter().zip(effects) { - if !actions_compatible(opte, actions) { - compatible_opts.push(optv.to_string()); - } } - candidates.push(OptionWithValue { - name: opt.name.clone(), - value: OptionValue::AllowList(compatible_opts), - }); break; } _ => unreachable!(), diff --git a/tests/cl.rs b/tests/cl.rs index 454afe5..e82aea4 100644 --- a/tests/cl.rs +++ b/tests/cl.rs @@ -43,6 +43,10 @@ fn run_true() { .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) @@ -78,6 +82,10 @@ fn run_write_dev_null() { .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) @@ -113,6 +121,10 @@ fn run_ls_dev() { .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) @@ -148,6 +160,10 @@ fn run_ls_proc() { .stdout(predicate::str::contains("ProtectProc=").not()) .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) @@ -183,6 +199,10 @@ fn run_read_kallsyms() { .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) @@ -218,6 +238,10 @@ fn run_ls_modules() { .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) @@ -246,6 +270,10 @@ fn run_dmesg() { .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) @@ -278,6 +306,10 @@ fn run_systemctl() { .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=AF_UNIX\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) @@ -316,6 +348,10 @@ fn run_ss() { .stdout(predicate::str::contains("ProtectProc=").not()) .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=AF_NETLINK AF_UNIX\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) @@ -345,6 +381,10 @@ fn run_mmap_wx() { .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) .stdout(predicate::str::contains("MemoryDenyWriteExecute=\n").not()) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) @@ -371,6 +411,10 @@ fn run_mmap_wx() { .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) @@ -403,6 +447,10 @@ fn run_sched_realtime() { .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) .stdout(predicate::str::contains("RestrictRealtime=").not()) .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) @@ -429,8 +477,45 @@ fn run_sched_realtime() { .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) .stdout(predicate::str::contains("MemoryDenyWriteExecute=true\n").count(1)) .stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @io-event:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @network-io:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @sandbox:EPERM @setuid:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); } + +#[test] +fn run_bind() { + Command::cargo_bin(env!("CARGO_PKG_NAME")) + .unwrap() + .args(["run", "--", "python3", "-c", "import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.bind((\"127.0.0.1\", 1234))"]) + .unwrap() + .assert() + .success() + .stdout(predicate::str::contains("ProtectSystem=strict\n").count(1)) + .stdout(predicate::str::contains("ProtectHome=read-only\n").count(1)) + .stdout(if env::current_exe().unwrap().starts_with("/tmp") { + predicate::str::contains("PrivateTmp=true\n").count(0) + } else { + predicate::str::contains("PrivateTmp=true\n").count(1) + }) + .stdout(predicate::str::contains("PrivateDevices=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelTunables=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelModules=true\n").count(1)) + .stdout(predicate::str::contains("ProtectKernelLogs=true\n").count(1)) + .stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1)) + .stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1)) + .stdout(predicate::str::contains("MemoryDenyWriteExecute=\n").not()) + .stdout(predicate::str::contains("RestrictAddressFamilies=AF_INET\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:tcp\n").not()) + .stdout(predicate::str::contains("SocketBindDeny=ipv4:udp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:tcp\n").count(1)) + .stdout(predicate::str::contains("SocketBindDeny=ipv6:udp\n").count(1)) + .stdout(predicate::str::contains("LockPersonality=true\n").count(1)) + .stdout(predicate::str::contains("RestrictRealtime=true\n").count(1)) + .stdout(predicate::str::contains("ProtectClock=true\n").count(1)) + .stdout(predicate::str::contains("SystemCallFilter=~@aio:EPERM @chown:EPERM @clock:EPERM @cpu-emulation:EPERM @debug:EPERM @ipc:EPERM @keyring:EPERM @memlock:EPERM @module:EPERM @mount:EPERM @obsolete:EPERM @pkey:EPERM @privileged:EPERM @process:EPERM @raw-io:EPERM @reboot:EPERM @resources:EPERM @sandbox:EPERM @setuid:EPERM @swap:EPERM @sync:EPERM @timer:EPERM\n").count(1)); +}