use std::net::Ipv4Addr;
#[cfg(target_os = "linux")]
use super::rtnetlink;
#[cfg(target_os = "linux")]
use crate::error::Result;
pub const GUEST_IP: Ipv4Addr = Ipv4Addr::new(169, 254, 100, 2);
pub const GUEST_GATEWAY: Ipv4Addr = Ipv4Addr::new(169, 254, 100, 1);
pub const GUEST_NETMASK: Ipv4Addr = Ipv4Addr::new(255, 255, 255, 252);
const RULE_COMMENT: &str = "arcbox-nat";
const FIB_RULE_PRIORITY: u32 = 8000;
pub fn fwmark(pool_ip: Ipv4Addr) -> u32 {
u32::from(pool_ip)
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct XtRule {
pub table: &'static str,
pub chain: &'static str,
pub spec: Vec<String>,
}
pub(crate) fn translation_rules(tap: &str, pool_ip: Ipv4Addr) -> Vec<XtRule> {
let mark = format!("{:#x}", fwmark(pool_ip));
let pool = format!("{pool_ip}/32");
let comment = |spec: &mut Vec<String>| {
spec.extend(["-m", "comment", "--comment", RULE_COMMENT].map(String::from));
};
let mut rules = Vec::new();
let mut rule = |table, chain, head: &[&str], tail: &[&str]| {
let mut spec: Vec<String> = head.iter().map(|s| (*s).to_owned()).collect();
comment(&mut spec);
spec.extend(tail.iter().map(|s| (*s).to_owned()));
rules.push(XtRule { table, chain, spec });
};
rule(
"mangle",
"PREROUTING",
&["-i", tap],
&["-j", "MARK", "--set-mark", &mark],
);
rule(
"mangle",
"PREROUTING",
&["-d", &pool],
&["-j", "MARK", "--set-mark", &mark],
);
rule(
"mangle",
"OUTPUT",
&["-d", &pool],
&["-j", "MARK", "--set-mark", &mark],
);
let guest_ip = GUEST_IP.to_string();
rule(
"nat",
"PREROUTING",
&["-d", &pool],
&["-j", "DNAT", "--to-destination", &guest_ip],
);
rule(
"nat",
"OUTPUT",
&["-d", &pool],
&["-j", "DNAT", "--to-destination", &guest_ip],
);
rule(
"nat",
"INPUT",
&["-i", tap],
&["-j", "SNAT", "--to-source", &pool_ip.to_string()],
);
rule(
"nat",
"POSTROUTING",
&[
"-s",
&format!("{GUEST_IP}/32"),
"-m",
"mark",
"--mark",
&mark,
],
&["-j", "SNAT", "--to-source", &pool_ip.to_string()],
);
rules
}
#[cfg(target_os = "linux")]
pub(crate) fn install(tap: &str, pool_ip: Ipv4Addr) -> Result<()> {
write_tap_sysctl(tap, "rp_filter", "0")?;
write_tap_sysctl(tap, "src_valid_mark", "1")?;
for rule in translation_rules(tap, pool_ip) {
run_iptables(&rule, "-A", false)?;
}
let mark = fwmark(pool_ip);
rtnetlink::execute(
&rtnetlink::new_fwmark_rule(mark, mark, FIB_RULE_PRIORITY),
&[libc::EEXIST],
)?;
rtnetlink::execute(
&rtnetlink::replace_link_route(GUEST_IP, tap_ifindex(tap)?, mark),
&[],
)?;
Ok(())
}
#[cfg(target_os = "linux")]
pub(crate) fn remove(tap: &str, pool_ip: Ipv4Addr) -> Result<()> {
let mut failures = Vec::new();
for rule in translation_rules(tap, pool_ip) {
if let Err(error) = run_iptables(&rule, "-D", true) {
failures.push(error.to_string());
}
}
let mark = fwmark(pool_ip);
if let Err(error) = rtnetlink::execute(
&rtnetlink::del_fwmark_rule(mark, mark, FIB_RULE_PRIORITY),
&[libc::ENOENT],
) {
failures.push(error.to_string());
}
if failures.is_empty() {
Ok(())
} else {
Err(crate::error::VmmError::Network(format!(
"sandbox NAT teardown incomplete for {tap}: {}",
failures.join("; ")
)))
}
}
#[cfg(target_os = "linux")]
fn write_tap_sysctl(tap: &str, key: &str, value: &str) -> Result<()> {
let path = format!("/proc/sys/net/ipv4/conf/{tap}/{key}");
std::fs::write(&path, value)
.map_err(|e| crate::error::VmmError::Network(format!("write {path}: {e}")))
}
#[cfg(target_os = "linux")]
pub(super) fn tap_ifindex(tap: &str) -> Result<u32> {
let name = std::ffi::CString::new(tap)
.map_err(|_| crate::error::VmmError::Network(format!("TAP name {tap:?} contains NUL")))?;
let index = unsafe { libc::if_nametoindex(name.as_ptr()) };
if index == 0 {
return Err(crate::error::VmmError::Network(format!(
"if_nametoindex {tap}: {}",
std::io::Error::last_os_error()
)));
}
Ok(index)
}
#[cfg(target_os = "linux")]
fn run_iptables(rule: &XtRule, verb: &str, tolerate_missing: bool) -> Result<()> {
let output = std::process::Command::new("/sbin/iptables")
.args(["-w", "2", "-t", rule.table, verb, rule.chain])
.args(&rule.spec)
.output()
.map_err(|e| crate::error::VmmError::Network(format!("run iptables: {e}")))?;
if output.status.success() || (tolerate_missing && output.status.code() == Some(1)) {
return Ok(());
}
Err(crate::error::VmmError::Network(format!(
"iptables -t {} {verb} {} {}: {}",
rule.table,
rule.chain,
rule.spec.join(" "),
String::from_utf8_lossy(&output.stderr).trim()
)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constants_form_one_point_to_point_link() {
let mask = u32::from(GUEST_NETMASK);
assert_eq!(mask.count_ones(), 30);
assert_eq!(
u32::from(GUEST_IP) & mask,
u32::from(GUEST_GATEWAY) & mask,
"guest and gateway must share the invariant subnet"
);
assert!(GUEST_IP.is_link_local());
}
#[test]
fn fwmark_is_unique_per_pool_ip() {
let a = fwmark("172.20.0.2".parse().unwrap());
let b = fwmark("172.20.0.3".parse().unwrap());
assert_ne!(a, b);
assert_ne!(a, 0, "a zero mark would match unmarked packets");
}
#[test]
fn translation_rules_pin_the_nat_contract() {
let pool: Ipv4Addr = "172.20.0.2".parse().unwrap();
let rules = translation_rules("vmtap0-2", pool);
let rendered: Vec<String> = rules
.iter()
.map(|r| format!("-t {} {} {}", r.table, r.chain, r.spec.join(" ")))
.collect();
assert_eq!(
rendered,
[
"-t mangle PREROUTING -i vmtap0-2 -m comment --comment arcbox-nat -j MARK --set-mark 0xac140002",
"-t mangle PREROUTING -d 172.20.0.2/32 -m comment --comment arcbox-nat -j MARK --set-mark 0xac140002",
"-t mangle OUTPUT -d 172.20.0.2/32 -m comment --comment arcbox-nat -j MARK --set-mark 0xac140002",
"-t nat PREROUTING -d 172.20.0.2/32 -m comment --comment arcbox-nat -j DNAT --to-destination 169.254.100.2",
"-t nat OUTPUT -d 172.20.0.2/32 -m comment --comment arcbox-nat -j DNAT --to-destination 169.254.100.2",
"-t nat INPUT -i vmtap0-2 -m comment --comment arcbox-nat -j SNAT --to-source 172.20.0.2",
"-t nat POSTROUTING -s 169.254.100.2/32 -m mark --mark 0xac140002 -m comment --comment arcbox-nat -j SNAT --to-source 172.20.0.2",
]
);
}
#[test]
fn snat_selection_never_rewrites_client_sources() {
let rules = translation_rules("vmtap0-2", "172.20.0.2".parse().unwrap());
let postrouting = rules
.iter()
.find(|r| r.chain == "POSTROUTING")
.expect("POSTROUTING SNAT rule");
assert_eq!(postrouting.spec[0], "-s");
assert_eq!(postrouting.spec[1], format!("{GUEST_IP}/32"));
}
}