#[cfg(target_os = "linux")]
use std::process::Command;
#[derive(Clone, Debug)]
pub struct PartitionConfig {
pub group_a: Vec<String>,
pub group_b: Vec<String>,
}
#[derive(Debug)]
pub enum PartitionError {
Unsupported,
NotRoot,
Spawn(std::io::Error),
NonZeroExit {
stderr: String,
code: Option<i32>,
},
EmptyGroup,
}
impl std::fmt::Display for PartitionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unsupported => write!(f, "partition only on Linux"),
Self::NotRoot => write!(f, "iptables benoetigt root"),
Self::Spawn(e) => write!(f, "konnte iptables nicht starten: {e}"),
Self::NonZeroExit { stderr, code } => {
write!(f, "iptables fehlgeschlagen (exit {code:?}): {stderr}")
}
Self::EmptyGroup => write!(f, "beide Gruppen muessen >=1 IP enthalten"),
}
}
}
impl std::error::Error for PartitionError {}
#[cfg(target_os = "linux")]
const COMMENT_TAG: &str = "zerodds-chaos-partition";
#[cfg(target_os = "linux")]
fn require_root() -> Result<(), PartitionError> {
let euid = unsafe { libc_stub::geteuid() };
if euid != 0 {
Err(PartitionError::NotRoot)
} else {
Ok(())
}
}
#[cfg(target_os = "linux")]
pub fn apply(cfg: &PartitionConfig) -> Result<(), PartitionError> {
require_root()?;
if cfg.group_a.is_empty() || cfg.group_b.is_empty() {
return Err(PartitionError::EmptyGroup);
}
let _ = clear();
for a in &cfg.group_a {
for b in &cfg.group_b {
run_iptables(&[
"-A",
"FORWARD",
"-s",
a,
"-d",
b,
"-m",
"comment",
"--comment",
COMMENT_TAG,
"-j",
"DROP",
])?;
run_iptables(&[
"-A",
"FORWARD",
"-s",
b,
"-d",
a,
"-m",
"comment",
"--comment",
COMMENT_TAG,
"-j",
"DROP",
])?;
run_iptables(&[
"-A",
"INPUT",
"-s",
a,
"-d",
b,
"-m",
"comment",
"--comment",
COMMENT_TAG,
"-j",
"DROP",
])?;
run_iptables(&[
"-A",
"INPUT",
"-s",
b,
"-d",
a,
"-m",
"comment",
"--comment",
COMMENT_TAG,
"-j",
"DROP",
])?;
}
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn apply(_cfg: &PartitionConfig) -> Result<(), PartitionError> {
Err(PartitionError::Unsupported)
}
#[cfg(target_os = "linux")]
pub fn clear() -> Result<(), PartitionError> {
require_root()?;
for chain in ["FORWARD", "INPUT", "OUTPUT"] {
let out = Command::new("iptables")
.args(["-S", chain])
.output()
.map_err(PartitionError::Spawn)?;
if !out.status.success() {
continue;
}
let lines = String::from_utf8_lossy(&out.stdout);
for line in lines.lines() {
if line.contains(COMMENT_TAG) && line.starts_with("-A ") {
let del_line = line.replacen("-A ", "-D ", 1);
let args: Vec<&str> = del_line.split_whitespace().collect();
let _ = run_iptables(&args);
}
}
}
Ok(())
}
#[cfg(not(target_os = "linux"))]
pub fn clear() -> Result<(), PartitionError> {
Err(PartitionError::Unsupported)
}
#[cfg(target_os = "linux")]
fn run_iptables(args: &[&str]) -> Result<(), PartitionError> {
let out = Command::new("iptables")
.args(args)
.output()
.map_err(PartitionError::Spawn)?;
if !out.status.success() {
return Err(PartitionError::NonZeroExit {
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
code: out.status.code(),
});
}
Ok(())
}
#[cfg(target_os = "linux")]
mod libc_stub {
unsafe extern "C" {
pub fn geteuid() -> u32;
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)] mod tests {
use super::*;
#[test]
fn partition_default_works() {
let cfg = PartitionConfig {
group_a: vec!["10.0.0.1".into()],
group_b: vec!["10.0.0.2".into()],
};
assert_eq!(cfg.group_a.len(), 1);
assert_eq!(cfg.group_b.len(), 1);
}
#[cfg(not(target_os = "linux"))]
#[test]
fn non_linux_returns_unsupported() {
let cfg = PartitionConfig {
group_a: vec!["10.0.0.1".into()],
group_b: vec!["10.0.0.2".into()],
};
assert!(matches!(apply(&cfg), Err(PartitionError::Unsupported)));
assert!(matches!(clear(), Err(PartitionError::Unsupported)));
}
#[cfg(target_os = "linux")]
#[test]
fn empty_group_rejected() {
let euid = unsafe { libc_stub::geteuid() };
if euid != 0 {
return;
}
let cfg = PartitionConfig {
group_a: vec![],
group_b: vec!["10.0.0.2".into()],
};
let r = apply(&cfg);
assert!(matches!(r, Err(PartitionError::EmptyGroup)));
}
}