use std::{env, net::IpAddr};
use nlink::netlink::{
Connection, Xfrm,
xfrm::{
IpsecProtocol, PolicyAction, PolicyDirection, XfrmMode, XfrmSaBuilder, XfrmSelector,
XfrmSpBuilder, XfrmUserTmpl,
},
};
const SPI_OUT: u32 = 0x0000_AABB;
const SPI_IN: u32 = 0x0000_CCDD;
const REQID: u32 = 42;
const AUTH_KEY: [u8; 32] = [0u8; 32];
const ENCR_KEY: [u8; 16] = [0u8; 16];
#[tokio::main]
async fn main() -> nlink::Result<()> {
let args: Vec<String> = env::args().collect();
match args.get(1).map(|s| s.as_str()) {
Some("show") => {
let conn = Connection::<Xfrm>::new()?;
show_state(&conn).await?;
}
Some("--apply") => {
run_apply().await?;
}
_ => {
print_overview();
}
}
Ok(())
}
fn print_overview() {
println!("=== XFRM (IPsec) lifecycle demo ===\n");
println!("Connection<Xfrm> exposes both the dump path");
println!("(get_security_associations / get_security_policies) and");
println!("the typed write path (add/update/del/flush_sa + same for");
println!("_sp) on the same socket.\n");
println!("Modes:");
println!(" show — Dump host SA + SP tables (no privileges");
println!(" beyond netlink read access)");
println!(" --apply — Install + verify + rotate + tear down");
println!(" inside a temp namespace (root required)");
println!();
println!("See `docs/recipes/xfrm-ipsec-tunnel.md` for the");
println!("two-namespace tunnel walkthrough.");
}
async fn show_state(conn: &Connection<Xfrm>) -> nlink::Result<()> {
println!("=== Security Associations ===\n");
let sas = conn.get_security_associations().await?;
if sas.is_empty() {
println!("(none — IPsec SAs are created when VPN tunnels are");
println!(" established or when --apply runs)");
} else {
for sa in &sas {
print_sa_brief(sa);
}
}
println!("\n=== Security Policies ===\n");
let policies = conn.get_security_policies().await?;
if policies.is_empty() {
println!("(none — IPsec policies steer traffic into the SA");
println!(" lookup; without them no traffic gets encrypted)");
} else {
for pol in &policies {
print_sp_brief(pol);
}
}
Ok(())
}
fn print_sa_brief(sa: &nlink::netlink::xfrm::SecurityAssociation) {
let src = sa
.src_addr
.map(|a| a.to_string())
.unwrap_or_else(|| "?".into());
let dst = sa
.dst_addr
.map(|a| a.to_string())
.unwrap_or_else(|| "?".into());
let proto = match sa.protocol {
IpsecProtocol::Esp => "ESP",
IpsecProtocol::Ah => "AH",
IpsecProtocol::Comp => "COMP",
_ => "?",
};
let mode = match sa.mode {
XfrmMode::Transport => "transport",
XfrmMode::Tunnel => "tunnel",
XfrmMode::Beet => "beet",
_ => "?",
};
println!(
" {} {src} -> {dst} spi=0x{:08x} reqid={} mode={mode}",
proto, sa.spi, sa.reqid
);
if let Some(ref enc) = sa.enc_alg {
println!(" encr: {} ({} bits)", enc.name, enc.key_len);
}
if let Some(ref auth) = sa.auth_alg {
println!(" auth: {} ({} bits)", auth.name, auth.key_len);
}
if let Some(ref aead) = sa.aead_alg {
println!(
" aead: {} ({} bits, ICV {} bits)",
aead.name, aead.key_len, aead.icv_len
);
}
}
fn print_sp_brief(pol: &nlink::netlink::xfrm::SecurityPolicy) {
let dir = match pol.direction {
PolicyDirection::In => "IN",
PolicyDirection::Out => "OUT",
PolicyDirection::Forward => "FWD",
_ => "?",
};
let action = match pol.action {
PolicyAction::Allow => "allow",
PolicyAction::Block => "block",
_ => "?",
};
println!(
" {dir} prio={} action={action} index={}",
pol.priority, pol.index
);
}
async fn run_apply() -> nlink::Result<()> {
if unsafe { libc::geteuid() } != 0 {
eprintln!("--apply requires root (CAP_NET_ADMIN). Aborting.");
std::process::exit(1);
}
println!("=== XFRM lifecycle apply ===\n");
use nlink::lab::with_namespace;
with_namespace("xfrm-apply", |ns| async move {
println!("Created namespace: {}", ns.name());
let conn: Connection<Xfrm> = ns.connection_for()?;
let local: IpAddr = "10.50.0.1".parse().unwrap();
let peer: IpAddr = "10.50.0.2".parse().unwrap();
println!(
"\n[1/6] Installing outbound + inbound SAs (ESP-tunnel + HMAC-SHA256 + AES-CBC)..."
);
let sa_out = XfrmSaBuilder::new(local, peer, SPI_OUT, IpsecProtocol::Esp)
.mode(XfrmMode::Tunnel)
.reqid(REQID)
.auth_hmac_sha256(&AUTH_KEY)
.encr_aes_cbc(&ENCR_KEY);
conn.add_sa(sa_out).await?;
let sa_in = XfrmSaBuilder::new(peer, local, SPI_IN, IpsecProtocol::Esp)
.mode(XfrmMode::Tunnel)
.reqid(REQID)
.auth_hmac_sha256(&AUTH_KEY)
.encr_aes_cbc(&ENCR_KEY);
conn.add_sa(sa_in).await?;
println!(
" added 2 SAs (spi=0x{:08x}, spi=0x{:08x})",
SPI_OUT, SPI_IN
);
println!("\n[2/6] Installing outbound + inbound SPs...");
let sel = XfrmSelector {
family: libc::AF_INET as u16,
..Default::default()
};
let sp_out = XfrmSpBuilder::new(sel, PolicyDirection::Out)
.priority(100)
.template(XfrmUserTmpl::match_any(
local,
peer,
IpsecProtocol::Esp,
XfrmMode::Tunnel,
REQID,
));
conn.add_sp(sp_out).await?;
let sp_in = XfrmSpBuilder::new(sel, PolicyDirection::In)
.priority(100)
.template(XfrmUserTmpl::match_any(
peer,
local,
IpsecProtocol::Esp,
XfrmMode::Tunnel,
REQID,
));
conn.add_sp(sp_in).await?;
println!(" added 2 SPs (Out, In)");
println!("\n[3/6] Dumping namespace XFRM tables...");
show_state(&conn).await?;
println!("\n[4/6] Rotating outbound SA in place (update_sa)...");
let rotated = XfrmSaBuilder::new(local, peer, SPI_OUT, IpsecProtocol::Esp)
.mode(XfrmMode::Tunnel)
.reqid(REQID)
.auth_hmac_sha256(&[0xAAu8; 32])
.encr_aes_cbc(&[0xBBu8; 16]);
conn.update_sa(rotated).await?;
println!(" rotated (same SPI; keys differ)");
println!("\n[5/6] Fetching rotated SA via get_sa...");
match conn
.get_sa(local, peer, SPI_OUT, IpsecProtocol::Esp)
.await?
{
Some(sa) => print_sa_brief(&sa),
None => println!(" (not found — kernel said ENOENT)"),
}
println!("\n[6/6] Tearing down (del_sa x2 + flush_sp)...");
conn.del_sa(local, peer, SPI_OUT, IpsecProtocol::Esp)
.await?;
conn.del_sa(peer, local, SPI_IN, IpsecProtocol::Esp).await?;
conn.flush_sp().await?;
println!(" done — namespace will be deleted on Drop");
Ok(())
})
.await?;
println!("\nLifecycle complete.");
Ok(())
}