use std::net::IpAddr;
use std::process::Command;
use super::FirewallError;
const CHAIN: &str = "ZLAYER-OVERLAY";
const PARENTS: &[&str] = &["INPUT", "FORWARD"];
const IN_CHAIN: &str = "ZLAYER-OVERLAY-IN";
const NAT_CHAIN: &str = "ZLAYER-OVERLAY-NAT";
const ISO_CHAIN: &str = "ZLAYER-OVERLAY-ISO";
const OVERLAY_IFACE_WILDCARD: &str = "zl-+";
fn tool_for_cidr(cidr: &str) -> &'static str {
if cidr.contains(':') {
"ip6tables"
} else {
"iptables"
}
}
fn run(tool: &str, args: &[&str]) -> Result<bool, FirewallError> {
let output = Command::new(tool)
.args(args)
.output()
.map_err(|e| FirewallError::AddRule {
name: format!("{tool} {}", args.join(" ")),
reason: e.to_string(),
})?;
Ok(output.status.success())
}
pub fn ensure_overlay_subnet_rules(overlay_cidr: &str) -> Result<(), FirewallError> {
let tool = tool_for_cidr(overlay_cidr);
let _ = run(tool, &["-N", CHAIN])?;
let _ = run(tool, &["-F", CHAIN])?;
if !run(
tool,
&[
"-A",
CHAIN,
"-m",
"conntrack",
"--ctstate",
"ESTABLISHED,RELATED",
"-j",
"RETURN",
],
)? {
return Err(FirewallError::AddRule {
name: format!("{CHAIN} established,related"),
reason: format!("{tool} -A returned non-zero"),
});
}
for direction in ["-s", "-d"] {
if !run(
tool,
&["-A", CHAIN, direction, overlay_cidr, "-j", "ACCEPT"],
)? {
return Err(FirewallError::AddRule {
name: format!("{CHAIN} {direction} {overlay_cidr}"),
reason: format!("{tool} -A returned non-zero"),
});
}
}
for parent in PARENTS {
let present = run(tool, &["-C", parent, "-j", CHAIN]).unwrap_or(false);
if !present && !run(tool, &["-A", parent, "-j", CHAIN])? {
return Err(FirewallError::AddRule {
name: format!("{parent} -> {CHAIN}"),
reason: format!("{tool} -A {parent} returned non-zero"),
});
}
}
Ok(())
}
pub fn remove_overlay_subnet_rules() {
for tool in ["iptables", "ip6tables"] {
for parent in PARENTS {
let _ = run(tool, &["-D", parent, "-j", CHAIN]);
}
let _ = run(tool, &["-F", CHAIN]);
let _ = run(tool, &["-X", CHAIN]);
}
}
fn masquerade_rule_args(op: &str, overlay_cidr: &str) -> Vec<String> {
vec![
"-t".to_string(),
"nat".to_string(),
op.to_string(),
NAT_CHAIN.to_string(),
"-s".to_string(),
overlay_cidr.to_string(),
"!".to_string(),
"-o".to_string(),
OVERLAY_IFACE_WILDCARD.to_string(),
"-j".to_string(),
"MASQUERADE".to_string(),
]
}
pub fn ensure_overlay_masquerade(overlay_cidr: &str) -> Result<(), FirewallError> {
let tool = tool_for_cidr(overlay_cidr);
let _ = run(tool, &["-t", "nat", "-N", NAT_CHAIN])?;
let _ = run(tool, &["-t", "nat", "-F", NAT_CHAIN])?;
let add: Vec<String> = masquerade_rule_args("-A", overlay_cidr);
let add_ref: Vec<&str> = add.iter().map(String::as_str).collect();
if !run(tool, &add_ref)? {
return Err(FirewallError::AddRule {
name: format!("{NAT_CHAIN} masquerade {overlay_cidr}"),
reason: format!("{tool} -t nat -A returned non-zero"),
});
}
let present = run(tool, &["-t", "nat", "-C", "POSTROUTING", "-j", NAT_CHAIN]).unwrap_or(false);
if !present && !run(tool, &["-t", "nat", "-A", "POSTROUTING", "-j", NAT_CHAIN])? {
return Err(FirewallError::AddRule {
name: format!("POSTROUTING -> {NAT_CHAIN}"),
reason: format!("{tool} -t nat -A POSTROUTING returned non-zero"),
});
}
Ok(())
}
pub fn remove_overlay_masquerade() {
for tool in ["iptables", "ip6tables"] {
let _ = run(tool, &["-t", "nat", "-D", "POSTROUTING", "-j", NAT_CHAIN]);
let _ = run(tool, &["-t", "nat", "-F", NAT_CHAIN]);
let _ = run(tool, &["-t", "nat", "-X", NAT_CHAIN]);
}
}
fn iso_pair_rule_args(op: &str, a_ip: IpAddr, b_ip: IpAddr) -> Vec<String> {
vec![
op.to_string(),
ISO_CHAIN.to_string(),
"-s".to_string(),
a_ip.to_string(),
"-d".to_string(),
b_ip.to_string(),
"-j".to_string(),
"RETURN".to_string(),
]
}
fn iso_node_rule_args(op: &str, member_ip: IpAddr, node_ip: IpAddr) -> Vec<String> {
let host_mask = if node_ip.is_ipv6() { "/128" } else { "/32" };
vec![
op.to_string(),
ISO_CHAIN.to_string(),
"-s".to_string(),
member_ip.to_string(),
"-d".to_string(),
format!("{node_ip}{host_mask}"),
"-j".to_string(),
"RETURN".to_string(),
]
}
fn iso_drop_rule_args(op: &str, member_ip: IpAddr, overlay_cidr: &str) -> Vec<String> {
vec![
op.to_string(),
ISO_CHAIN.to_string(),
"-s".to_string(),
member_ip.to_string(),
"-d".to_string(),
overlay_cidr.to_string(),
"-j".to_string(),
"DROP".to_string(),
]
}
fn ensure_iso_rule(tool: &str, rule: &[String], name: &str) -> Result<(), FirewallError> {
let mut probe = rule.to_vec();
probe[0] = "-C".to_string();
let probe_ref: Vec<&str> = probe.iter().map(String::as_str).collect();
if run(tool, &probe_ref).unwrap_or(false) {
return Ok(());
}
let rule_ref: Vec<&str> = rule.iter().map(String::as_str).collect();
if !run(tool, &rule_ref)? {
return Err(FirewallError::AddRule {
name: name.to_string(),
reason: format!("{tool} {} returned non-zero", rule[0]),
});
}
Ok(())
}
pub fn ensure_member_isolation(
network: &str,
member_ip: IpAddr,
peers: &[IpAddr],
node_ip: IpAddr,
overlay_cidr: &str,
) -> Result<(), FirewallError> {
let _ = network;
let tool = "iptables";
let _ = run(tool, &["-N", ISO_CHAIN])?;
let present = run(tool, &["-C", "FORWARD", "-j", ISO_CHAIN]).unwrap_or(false);
if !present && !run(tool, &["-A", "FORWARD", "-j", ISO_CHAIN])? {
return Err(FirewallError::AddRule {
name: format!("FORWARD -> {ISO_CHAIN}"),
reason: format!("{tool} -A FORWARD returned non-zero"),
});
}
let est = vec![
"-I".to_string(),
ISO_CHAIN.to_string(),
"-m".to_string(),
"conntrack".to_string(),
"--ctstate".to_string(),
"ESTABLISHED,RELATED".to_string(),
"-j".to_string(),
"RETURN".to_string(),
];
let est = insert_pos(est);
ensure_iso_rule(tool, &est, &format!("{ISO_CHAIN} established,related"))?;
for peer in peers {
let fwd = iso_pair_rule_args("-I", member_ip, *peer);
let fwd = insert_pos(fwd);
ensure_iso_rule(
tool,
&fwd,
&format!("{ISO_CHAIN} allow {member_ip} -> {peer}"),
)?;
let rev = iso_pair_rule_args("-I", *peer, member_ip);
let rev = insert_pos(rev);
ensure_iso_rule(
tool,
&rev,
&format!("{ISO_CHAIN} allow {peer} -> {member_ip}"),
)?;
}
let node = iso_node_rule_args("-I", member_ip, node_ip);
let node = insert_pos(node);
ensure_iso_rule(
tool,
&node,
&format!("{ISO_CHAIN} allow {member_ip} -> {node_ip}"),
)?;
let drop = iso_drop_rule_args("-A", member_ip, overlay_cidr);
ensure_iso_rule(
tool,
&drop,
&format!("{ISO_CHAIN} drop {member_ip} -> {overlay_cidr}"),
)?;
Ok(())
}
fn insert_pos(mut rule: Vec<String>) -> Vec<String> {
rule.insert(2, "1".to_string());
rule
}
pub fn remove_member_isolation(
network: &str,
member_ip: IpAddr,
peers: &[IpAddr],
node_ip: IpAddr,
overlay_cidr: &str,
) {
let _ = network;
let tool = "iptables";
for peer in peers {
let fwd = iso_pair_rule_args("-D", member_ip, *peer);
let fwd_ref: Vec<&str> = fwd.iter().map(String::as_str).collect();
let _ = run(tool, &fwd_ref);
let rev = iso_pair_rule_args("-D", *peer, member_ip);
let rev_ref: Vec<&str> = rev.iter().map(String::as_str).collect();
let _ = run(tool, &rev_ref);
}
let node = iso_node_rule_args("-D", member_ip, node_ip);
let node_ref: Vec<&str> = node.iter().map(String::as_str).collect();
let _ = run(tool, &node_ref);
let drop = iso_drop_rule_args("-D", member_ip, overlay_cidr);
let drop_ref: Vec<&str> = drop.iter().map(String::as_str).collect();
let _ = run(tool, &drop_ref);
}
pub fn remove_overlay_isolation() {
for tool in ["iptables", "ip6tables"] {
let _ = run(tool, &["-D", "FORWARD", "-j", ISO_CHAIN]);
let _ = run(tool, &["-F", ISO_CHAIN]);
let _ = run(tool, &["-X", ISO_CHAIN]);
}
}
fn port_rule_args(op: &str, port: u16, proto: &str) -> Vec<String> {
vec![
op.to_string(),
IN_CHAIN.to_string(),
"-p".to_string(),
proto.to_string(),
"--dport".to_string(),
port.to_string(),
"-j".to_string(),
"ACCEPT".to_string(),
]
}
fn ensure_in_chain(tool: &str) -> Result<(), FirewallError> {
let _ = run(tool, &["-N", IN_CHAIN])?;
let est_present = run(
tool,
&[
"-C",
IN_CHAIN,
"-m",
"conntrack",
"--ctstate",
"ESTABLISHED,RELATED",
"-j",
"RETURN",
],
)
.unwrap_or(false);
if !est_present
&& !run(
tool,
&[
"-I",
IN_CHAIN,
"1",
"-m",
"conntrack",
"--ctstate",
"ESTABLISHED,RELATED",
"-j",
"RETURN",
],
)?
{
return Err(FirewallError::AddRule {
name: format!("{IN_CHAIN} established,related"),
reason: format!("{tool} -I {IN_CHAIN} returned non-zero"),
});
}
let present = run(tool, &["-C", "INPUT", "-j", IN_CHAIN]).unwrap_or(false);
if !present && !run(tool, &["-A", "INPUT", "-j", IN_CHAIN])? {
return Err(FirewallError::AddRule {
name: format!("INPUT -> {IN_CHAIN}"),
reason: format!("{tool} -A INPUT returned non-zero"),
});
}
Ok(())
}
fn ensure_port_rule(tool: &str, proto: &str, port: u16) -> Result<(), FirewallError> {
let probe: Vec<String> = port_rule_args("-C", port, proto);
let probe_ref: Vec<&str> = probe.iter().map(String::as_str).collect();
if run(tool, &probe_ref).unwrap_or(false) {
return Ok(());
}
let append: Vec<String> = port_rule_args("-A", port, proto);
let append_ref: Vec<&str> = append.iter().map(String::as_str).collect();
if !run(tool, &append_ref)? {
return Err(FirewallError::AddRule {
name: format!("{IN_CHAIN} -p {proto} --dport {port}"),
reason: format!("{tool} -A returned non-zero"),
});
}
Ok(())
}
pub fn ensure_overlay_rules(
wg_port: u16,
api_port: u16,
raft_port: u16,
) -> Result<(), FirewallError> {
let rules: [(&str, u16); 5] = [
("udp", wg_port),
("tcp", api_port),
("tcp", raft_port),
("udp", 53),
("tcp", 53),
];
ensure_in_chain("iptables")?;
for (proto, port) in rules {
ensure_port_rule("iptables", proto, port)?;
}
if ensure_in_chain("ip6tables").is_ok() {
for (proto, port) in rules {
let _ = ensure_port_rule("ip6tables", proto, port);
}
}
Ok(())
}
pub fn ensure_published_port(port: u16, udp: bool) -> Result<(), FirewallError> {
let proto = if udp { "udp" } else { "tcp" };
ensure_in_chain("iptables")?;
ensure_port_rule("iptables", proto, port)?;
if ensure_in_chain("ip6tables").is_ok() {
let _ = ensure_port_rule("ip6tables", proto, port);
}
Ok(())
}
pub fn remove_published_port(port: u16, udp: bool) {
let proto = if udp { "udp" } else { "tcp" };
let args: Vec<String> = port_rule_args("-D", port, proto);
let args_ref: Vec<&str> = args.iter().map(String::as_str).collect();
for tool in ["iptables", "ip6tables"] {
let _ = run(tool, &args_ref);
}
}
pub fn remove_overlay_rules() {
for tool in ["iptables", "ip6tables"] {
let _ = run(tool, &["-D", "INPUT", "-j", IN_CHAIN]);
let _ = run(tool, &["-F", IN_CHAIN]);
let _ = run(tool, &["-X", IN_CHAIN]);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn port_rule_args_builds_check_probe() {
assert_eq!(
port_rule_args("-C", 3669, "tcp"),
vec![
"-C",
"ZLAYER-OVERLAY-IN",
"-p",
"tcp",
"--dport",
"3669",
"-j",
"ACCEPT",
]
);
}
#[test]
fn port_rule_args_builds_append_and_delete() {
assert_eq!(
port_rule_args("-A", 51820, "udp"),
vec![
"-A",
"ZLAYER-OVERLAY-IN",
"-p",
"udp",
"--dport",
"51820",
"-j",
"ACCEPT",
]
);
assert_eq!(
port_rule_args("-D", 51820, "udp"),
vec![
"-D",
"ZLAYER-OVERLAY-IN",
"-p",
"udp",
"--dport",
"51820",
"-j",
"ACCEPT",
]
);
}
#[test]
fn dns_builds_both_udp_and_tcp_on_port_53() {
assert_eq!(
port_rule_args("-C", 53, "udp"),
vec![
"-C",
"ZLAYER-OVERLAY-IN",
"-p",
"udp",
"--dport",
"53",
"-j",
"ACCEPT",
]
);
assert_eq!(
port_rule_args("-C", 53, "tcp"),
vec![
"-C",
"ZLAYER-OVERLAY-IN",
"-p",
"tcp",
"--dport",
"53",
"-j",
"ACCEPT",
]
);
}
#[test]
fn masquerade_rule_args_builds_append_in_nat_table() {
assert_eq!(
masquerade_rule_args("-A", "10.200.0.0/16"),
vec![
"-t",
"nat",
"-A",
"ZLAYER-OVERLAY-NAT",
"-s",
"10.200.0.0/16",
"!",
"-o",
"zl-+",
"-j",
"MASQUERADE",
]
);
}
#[test]
fn remove_overlay_isolation_is_idempotent_no_chain() {
remove_overlay_isolation();
remove_overlay_isolation();
}
#[test]
fn masquerade_rule_args_probe_and_delete_differ_only_by_op() {
let probe = masquerade_rule_args("-C", "10.200.0.0/16");
let delete = masquerade_rule_args("-D", "10.200.0.0/16");
assert_eq!(probe[2], "-C");
assert_eq!(delete[2], "-D");
assert_eq!(&probe[3..], &delete[3..]);
assert_eq!(probe[6], "!");
assert_eq!(probe[7], "-o");
assert_eq!(probe[8], "zl-+");
}
#[test]
#[ignore = "Requires root (mutates the nat table via iptables)"]
fn masquerade_against_kernel_nat_table_installs_and_removes() {
const TEST_CHAIN: &str = "ZLTEST-NAT-EGRESS";
const CIDR: &str = "10.211.0.0/16";
let Ok(true) = run("iptables", &["-t", "nat", "-L", "-n"]) else {
eprintln!("skipping: iptables nat table not accessible (need root)");
return;
};
let probe_rule = [
"-t",
"nat",
"-C",
TEST_CHAIN,
"-s",
CIDR,
"!",
"-o",
"zl-+",
"-j",
"MASQUERADE",
];
let probe_jump = ["-t", "nat", "-C", "POSTROUTING", "-j", TEST_CHAIN];
let _ = run("iptables", &["-t", "nat", "-N", TEST_CHAIN]);
let _ = run("iptables", &["-t", "nat", "-F", TEST_CHAIN]);
let add_rule = run(
"iptables",
&[
"-t",
"nat",
"-A",
TEST_CHAIN,
"-s",
CIDR,
"!",
"-o",
"zl-+",
"-j",
"MASQUERADE",
],
);
let add_jump = run(
"iptables",
&["-t", "nat", "-A", "POSTROUTING", "-j", TEST_CHAIN],
);
let rule_present = run("iptables", &probe_rule).unwrap_or(false);
let jump_present = run("iptables", &probe_jump).unwrap_or(false);
let _ = run(
"iptables",
&["-t", "nat", "-D", "POSTROUTING", "-j", TEST_CHAIN],
);
let _ = run("iptables", &["-t", "nat", "-F", TEST_CHAIN]);
let _ = run("iptables", &["-t", "nat", "-X", TEST_CHAIN]);
let rule_gone = !run("iptables", &probe_rule).unwrap_or(false);
let jump_gone = !run("iptables", &probe_jump).unwrap_or(false);
assert!(add_rule.unwrap_or(false), "masquerade -A should succeed");
assert!(
add_jump.unwrap_or(false),
"POSTROUTING jump -A should succeed"
);
assert!(
rule_present,
"masquerade rule should be present after install"
);
assert!(
jump_present,
"POSTROUTING jump should be present after install"
);
assert!(rule_gone, "masquerade rule should be gone after teardown");
assert!(jump_gone, "POSTROUTING jump should be gone after teardown");
}
fn ip(s: &str) -> IpAddr {
s.parse().expect("valid IP literal")
}
#[test]
fn iso_pair_rule_args_builds_bidirectional_return() {
assert_eq!(
iso_pair_rule_args("-I", ip("10.200.1.5"), ip("10.200.2.7")),
vec![
"-I",
"ZLAYER-OVERLAY-ISO",
"-s",
"10.200.1.5",
"-d",
"10.200.2.7",
"-j",
"RETURN",
]
);
assert_eq!(
iso_pair_rule_args("-I", ip("10.200.2.7"), ip("10.200.1.5")),
vec![
"-I",
"ZLAYER-OVERLAY-ISO",
"-s",
"10.200.2.7",
"-d",
"10.200.1.5",
"-j",
"RETURN",
]
);
}
#[test]
fn iso_node_rule_args_appends_host_mask() {
assert_eq!(
iso_node_rule_args("-I", ip("10.200.1.5"), ip("10.200.0.1")),
vec![
"-I",
"ZLAYER-OVERLAY-ISO",
"-s",
"10.200.1.5",
"-d",
"10.200.0.1/32",
"-j",
"RETURN",
]
);
assert_eq!(
iso_node_rule_args("-I", ip("fd00::5"), ip("fd00::1")),
vec![
"-I",
"ZLAYER-OVERLAY-ISO",
"-s",
"fd00::5",
"-d",
"fd00::1/128",
"-j",
"RETURN",
]
);
}
#[test]
fn iso_drop_rule_args_builds_overlay_catch_all() {
assert_eq!(
iso_drop_rule_args("-A", ip("10.200.1.5"), "10.200.0.0/16"),
vec![
"-A",
"ZLAYER-OVERLAY-ISO",
"-s",
"10.200.1.5",
"-d",
"10.200.0.0/16",
"-j",
"DROP",
]
);
}
#[test]
fn iso_rule_args_ops_differ_only_by_leading_flag() {
let m = ip("10.200.1.5");
let p = ip("10.200.2.7");
let n = ip("10.200.0.1");
let check = iso_pair_rule_args("-C", m, p);
let install = iso_pair_rule_args("-A", m, p);
let delete = iso_pair_rule_args("-D", m, p);
assert_eq!(check[0], "-C");
assert_eq!(install[0], "-A");
assert_eq!(delete[0], "-D");
assert_eq!(&check[1..], &install[1..]);
assert_eq!(&check[1..], &delete[1..]);
let nc = iso_node_rule_args("-C", m, n);
let nd = iso_node_rule_args("-D", m, n);
assert_eq!(nc[0], "-C");
assert_eq!(nd[0], "-D");
assert_eq!(&nc[1..], &nd[1..]);
let dc = iso_drop_rule_args("-C", m, "10.200.0.0/16");
let dd = iso_drop_rule_args("-D", m, "10.200.0.0/16");
assert_eq!(dc[0], "-C");
assert_eq!(dd[0], "-D");
assert_eq!(&dc[1..], &dd[1..]);
}
#[test]
fn raft_port_rule_uses_tcp() {
assert_eq!(
port_rule_args("-A", 7000, "tcp"),
vec![
"-A",
"ZLAYER-OVERLAY-IN",
"-p",
"tcp",
"--dport",
"7000",
"-j",
"ACCEPT",
]
);
}
}