Skip to content

Commit

Permalink
feat: support RestrictAddressFamilies systemd option
Browse files Browse the repository at this point in the history
  • Loading branch information
desbma-s1n committed Sep 27, 2023
1 parent bc90525 commit 10d0dad
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 23 deletions.
13 changes: 13 additions & 0 deletions src/summarize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ pub enum ProgramAction {
Write(PathBuf),
/// Path was created
Create(PathBuf),
/// Network (socket) activity
NetworkActivity { af: String },
/// Names of the syscalls made by the program
Syscalls(HashSet<String>),
}
Expand Down Expand Up @@ -304,6 +306,17 @@ where
}
_ => (),
}
} else if name == "socket" {
let af = if let Some(SyscallArg::Integer {
value: IntegerExpression::NamedConst(af),
..
}) = syscall.args.get(0)
{
af.to_string()
} else {
anyhow::bail!("Unexpected args for {}: {:?}", name, syscall.args);
};
actions.push(ProgramAction::NetworkActivity { af });
}
}

Expand Down
79 changes: 79 additions & 0 deletions src/systemd/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ 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<String>),
AllowList(Vec<String>),
}

impl FromStr for OptionValue {
Expand Down Expand Up @@ -91,6 +92,8 @@ pub enum OptionValueEffect {
/// See https://github.com/systemd/systemd/blob/v254/src/shared/seccomp-util.c#L306
/// for the content of each class
DenySyscall { class: String },
/// Deny a socket family
DenySocketFamily(String),
/// Union of multiple effects
Multiple(Vec<OptionValueEffect>),
}
Expand Down Expand Up @@ -127,6 +130,7 @@ impl fmt::Display for OptionWithValue {
write!(f, "{value}")
}
OptionValue::DenyList(values) => {
debug_assert!(!values.is_empty());
write!(
f,
"~{}",
Expand All @@ -137,6 +141,21 @@ impl fmt::Display for OptionWithValue {
.join(" ")
)
}
OptionValue::AllowList(values) => {
if values.is_empty() {
write!(f, "none")
} else {
write!(
f,
"{}",
values
.iter()
.map(|v| v.to_string())
.collect::<Vec<_>>()
.join(" ")
)
}
}
}
}
}
Expand Down Expand Up @@ -965,6 +984,66 @@ pub fn build_options(
});
}

// https://www.freedesktop.org/software/systemd/man/systemd.exec.html#RestrictAddressFamilies=
// https://man7.org/linux/man-pages/man7/address_families.7.html
let afs = [
"AF_ALG",
"AF_APPLETALK",
"AF_ATMPVC",
"AF_ATMSVC",
"AF_AX",
"AF_BLUETOOTH",
"AF_BRIDGE",
"AF_CAIF",
"AF_CAN",
"AF_DECnet",
"AF_ECONET",
"AF_IB",
"AF_IEEE",
"AF_INET",
"AF_IPX",
"AF_IRDA",
"AF_ISDN",
"AF_IUCV",
"AF_KCM",
"AF_KEY",
"AF_LLC",
"AF_LOCAL",
"AF_MPLS",
"AF_NETBEUI",
"AF_NETLINK",
"AF_NETROM",
"AF_PACKET",
"AF_PHONET",
"AF_PPPOX",
"AF_QIPCRTR",
"AF_RDS",
"AF_ROSE",
"AF_RXRPC",
"AF_SECURITY",
"AF_SMC",
"AF_TIPC",
"AF_UNIX",
"AF_VSOCK",
"AF_WANPIPE",
"AF_X",
"AF_XDP",
];
options.push(OptionDescription {
name: "RestrictAddressFamilies".to_string(),
possible_values: vec![OptionValueDescription {
value: OptionValue::AllowList(afs.iter().map(|s| s.to_string()).collect()),
desc: OptionEffect::Cumulative(
afs.into_iter()
.map(|af| OptionValueEffect::DenySocketFamily(af.to_string()))
.collect(),
),
}],
});

// https://www.freedesktop.org/software/systemd/man/systemd.exec.html#PrivateNetwork=
// TODO

// https://www.freedesktop.org/software/systemd/man/systemd.exec.html#SystemCallFilter=
let mut syscall_classes: Vec<_> = SYSCALL_CLASSES.keys().cloned().collect();
syscall_classes.sort();
Expand Down
56 changes: 39 additions & 17 deletions src/systemd/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ impl OptionValueEffect {
true
}
}
OptionValueEffect::DenySocketFamily(denied_af) => {
if let ProgramAction::NetworkActivity { af } = action {
af != denied_af
} else {
true
}
}
OptionValueEffect::Multiple(effects) => {
effects.iter().all(|e| e.compatible(action, prev_actions))
}
Expand Down Expand Up @@ -72,24 +79,39 @@ pub fn resolve(
}
}
OptionEffect::Cumulative(effects) => {
let opt_values = if let OptionValue::DenyList(v) = &opt_value_desc.value {
v
} else {
unreachable!()
};
debug_assert_eq!(opt_values.len(), effects.len());
let mut compatible_opts = Vec::new();
for (optv, opte) in opt_values.iter().zip(effects) {
if actions_compatible(opte, actions) {
compatible_opts.push(optv.clone());
match &opt_value_desc.value {
OptionValue::DenyList(deny_list) => {
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) {
compatible_opts.push(optv.to_string());
}
}
if !compatible_opts.is_empty() {
candidates.push(OptionWithValue {
name: opt.name.clone(),
value: OptionValue::DenyList(compatible_opts),
});
break;
}
}
}
if !opt_values.is_empty() {
candidates.push(OptionWithValue {
name: opt.name.clone(),
value: OptionValue::DenyList(compatible_opts),
});
}
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!(),
};
}
}
}
Expand Down
40 changes: 34 additions & 6 deletions tests/cl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fn run_true() {
.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("RestrictAddressFamilies=none\n").count(1))
.stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @signal @swap @sync @timer\n").count(1));
}

Expand All @@ -44,6 +45,7 @@ fn run_write_dev_null() {
.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("RestrictAddressFamilies=none\n").count(1))
.stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @swap @sync @timer\n").count(1));
}

Expand All @@ -57,12 +59,13 @@ fn run_ls_dev() {
.success()
.stdout(predicate::str::contains("ProtectSystem=strict\n").count(1))
.stdout(predicate::str::contains("PrivateTmp=true\n").count(1))
.stdout(predicate::str::contains("PrivateDevices=true\n").not())
.stdout(predicate::str::contains("PrivateDevices=").not())
.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("RestrictAddressFamilies=none\n").count(1))
.stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @signal @swap @sync @timer\n").count(1));
}

Expand All @@ -81,7 +84,8 @@ fn run_ls_proc() {
.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").not())
.stdout(predicate::str::contains("ProtectProc=").not())
.stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1))
.stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @signal @swap @sync @timer\n").count(1));
}

Expand All @@ -96,11 +100,12 @@ fn run_read_kallsyms() {
.stdout(predicate::str::contains("ProtectSystem=strict\n").count(1))
.stdout(predicate::str::contains("PrivateTmp=true\n").count(1))
.stdout(predicate::str::contains("PrivateDevices=true\n").count(1))
.stdout(predicate::str::contains("ProtectKernelTunables=true\n").not())
.stdout(predicate::str::contains("ProtectKernelTunables=").not())
.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("RestrictAddressFamilies=none\n").count(1))
.stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @signal @swap @sync @timer\n").count(1));
}

Expand All @@ -116,10 +121,11 @@ fn run_ls_modules() {
.stdout(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").not())
.stdout(predicate::str::contains("ProtectKernelModules=").not())
.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("RestrictAddressFamilies=none\n").count(1))
.stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @signal @swap @sync @timer\n").count(1));
}

Expand All @@ -134,12 +140,13 @@ fn run_dmesg() {
.success()
.stdout(predicate::str::contains("ProtectSystem=strict\n").count(1))
.stdout(predicate::str::contains("PrivateTmp=true\n").count(1))
.stdout(predicate::str::contains("PrivateDevices=true\n").not())
.stdout(predicate::str::contains("PrivateDevices=").not())
.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").not())
.stdout(predicate::str::contains("ProtectKernelLogs=").not())
.stdout(predicate::str::contains("ProtectControlGroups=true\n").count(1))
.stdout(predicate::str::contains("ProtectProc=ptraceable\n").count(1))
.stdout(predicate::str::contains("RestrictAddressFamilies=none\n").count(1))
.stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @network-io @obsolete @pkey @privileged @process @raw-io @reboot @resources @sandbox @setuid @signal @swap @sync @timer\n").count(1));
}

Expand All @@ -160,8 +167,29 @@ fn run_systemctl() {
.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("RestrictAddressFamilies=AF_UNIX\n").count(1))
.stdout(predicates::boolean::OrPredicate::new(
predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @ipc @keyring @memlock @module @mount @obsolete @pkey @privileged @raw-io @reboot @resources @sandbox @setuid @swap @sync @timer\n").count(1),
predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @obsolete @pkey @privileged @raw-io @reboot @resources @sandbox @setuid @swap @sync @timer\n").count(1),
));
}

#[test]
fn run_ss() {
Command::cargo_bin(env!("CARGO_PKG_NAME"))
.unwrap()
.args(["run", "--", "ss", "-ltpn"])
.unwrap()
.assert()
.success()
.stdout(predicate::str::contains("ProtectSystem=strict\n").count(1))
.stdout(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=").not())
.stdout(predicate::str::contains("RestrictAddressFamilies=AF_NETLINK\n").count(1))
.stdout(predicate::str::contains("SystemCallFilter=~@aio @chown @clock @cpu-emulation @debug @io-event @ipc @keyring @memlock @module @mount @obsolete @pkey @privileged @raw-io @reboot @resources @sandbox @setuid @signal @swap @sync @timer\n").count(1));
}

0 comments on commit 10d0dad

Please sign in to comment.