use std::{
net::{Ipv4Addr, SocketAddr, SocketAddrV4},
time::SystemTime,
};
use nlink::netlink::{
Connection, Route, Wireguard,
genl::wireguard::{AllowedIp, WgDevice, WgPeer},
link::WireguardLink,
namespace,
};
const LOCAL_PRIVATE: [u8; 32] = [0x01; 32];
const PEER_PUBLIC: [u8; 32] = [0x02; 32];
#[tokio::main]
async fn main() -> nlink::Result<()> {
let args: Vec<String> = std::env::args().collect();
match args.get(1).map(|s| s.as_str()) {
Some("show") => {
run_show().await?;
}
Some("--apply") => {
run_apply().await?;
}
_ => {
print_overview();
}
}
Ok(())
}
fn print_overview() {
println!("=== WireGuard (Generic Netlink) ===\n");
println!("The `Connection::<Wireguard>` handle sits on top of the WG");
println!("GENL family. Interface creation is rtnetlink (same as any");
println!("kind=wireguard link); key and peer config flows through GENL.\n");
println!("--- What --apply does ---\n");
println!(
" 1. Create a temporary namespace.
2. Add wg0 (kind=wireguard) and bring it up (rtnetlink).
3. Configure via GENL: private key + listen port.
4. Add a peer: public key + endpoint + allowed-ip + keepalive.
5. Dump with get_device() to verify round-trip.
6. del_peer(), then delete the namespace (wg0 goes with it).
"
);
println!("--- Code ---\n");
println!(
r#" use nlink::netlink::{{Connection, Route, Wireguard, namespace}};
use nlink::netlink::genl::wireguard::AllowedIp;
use nlink::netlink::link::WireguardLink;
use std::net::{{Ipv4Addr, SocketAddrV4}};
// rtnetlink side — create the interface.
let route: Connection<Route> = namespace::connection_for("lab")?;
route.add_link(WireguardLink::new("wg0")).await?;
route.set_link_up("wg0").await?;
// GENL side — set keys + peer.
let wg: Connection<Wireguard> = namespace::connection_for_async("lab").await?;
wg.set_device("wg0", |dev| {{
dev.private_key(local_private_key)
.listen_port(51820)
}}).await?;
wg.set_peer("wg0", peer_public_key, |peer| {{
peer.endpoint(SocketAddrV4::new(Ipv4Addr::new(10, 0, 0, 1), 51820).into())
.allowed_ip(AllowedIp::v4(Ipv4Addr::new(10, 0, 0, 0), 24))
.persistent_keepalive(25)
.replace_allowed_ips()
}}).await?;
let device = wg.get_device("wg0").await?;
for peer in &device.peers {{ /* ... */ }}
// Remove a peer without tearing down the interface:
wg.del_peer("wg0", peer_public_key).await?;
"#
);
println!("--- Re-run with `--apply` (as root) ---");
println!(" Runs the lifecycle above in a temporary namespace.");
println!();
println!("--- Read-only mode ---");
println!(" `show` probes common wg* interface names on the current host.");
}
async fn run_show() -> nlink::Result<()> {
let conn = match Connection::<Wireguard>::new_async().await {
Ok(conn) => conn,
Err(e) => {
eprintln!("Failed to connect to WireGuard GENL: {e}");
eprintln!("Try: sudo modprobe wireguard");
return Ok(());
}
};
println!("=== WireGuard interfaces (probing wg0..wg2, wg-vpn, wireguard0) ===\n");
let candidates = ["wg0", "wg1", "wg2", "wg-vpn", "wireguard0"];
let mut found = false;
for name in candidates {
match conn.get_device(name).await {
Ok(device) => {
found = true;
print_device(&device);
}
Err(e) if e.is_not_found() || e.is_no_device() => {}
Err(e) => eprintln!("Error querying {name}: {e}"),
}
}
if !found {
println!("No WireGuard interfaces found.");
println!("Build one with: sudo cargo run -p nlink --example genl_wireguard -- --apply");
}
Ok(())
}
async fn run_apply() -> nlink::Result<()> {
if unsafe { libc::geteuid() } != 0 {
eprintln!("--apply requires root (CAP_NET_ADMIN). Aborting.");
std::process::exit(1);
}
println!("=== WireGuard live demo (temporary namespace) ===");
let ns_name = format!("nlink-wg-demo-{}", std::process::id());
namespace::create(&ns_name)?;
let result = run_demo(&ns_name).await;
let _ = namespace::delete(&ns_name);
result?;
println!();
println!("Done. Namespace `{ns_name}` removed.");
Ok(())
}
async fn run_demo(ns_name: &str) -> nlink::Result<()> {
let route: Connection<Route> = namespace::connection_for(ns_name)?;
route
.add_link(WireguardLink::new("wg0"))
.await
.map_err(|e| {
eprintln!("\n add_link(wg0, kind=wireguard) failed: {e}");
eprintln!(" Is the `wireguard` kernel module loaded? `sudo modprobe wireguard`.");
e
})?;
route.set_link_up("wg0").await?;
let link = route.get_link_by_name("wg0").await?.expect("just created");
println!(
" Created wg0 (ifindex {}) in namespace `{ns_name}`.",
link.ifindex()
);
let wg: Connection<Wireguard> = namespace::connection_for_async(ns_name).await?;
println!(
" Opened WireGuard GENL connection (family_id={}).",
wg.family_id()
);
println!();
println!(" set_device: private_key + listen_port=51820");
wg.set_device("wg0", |dev| {
dev.private_key(LOCAL_PRIVATE).listen_port(51820)
})
.await?;
println!(
" set_peer: pubkey={} endpoint=10.0.0.1:51820 allowed=10.0.0.0/24 keepalive=25s",
short_key(&PEER_PUBLIC)
);
wg.set_peer("wg0", PEER_PUBLIC, |peer| {
peer.endpoint(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(10, 0, 0, 1),
51820,
)))
.allowed_ip(AllowedIp::v4(Ipv4Addr::new(10, 0, 0, 0), 24))
.persistent_keepalive(25)
.replace_allowed_ips()
})
.await?;
println!();
println!(" --- get_device(wg0) ---");
let device = wg.get_device("wg0").await?;
print_device(&device);
println!(" Removing peer via del_peer()...");
wg.del_peer("wg0", PEER_PUBLIC).await?;
let device_after = wg.get_device("wg0").await?;
println!(
" After del_peer: {} peer(s) remaining on wg0.",
device_after.peers.len()
);
Ok(())
}
fn print_device(device: &WgDevice) {
let ifname = device.ifname.as_deref().unwrap_or("?");
let ifindex = device.ifindex.unwrap_or(0);
println!(" interface: {ifname} (index {ifindex})");
if let Some(key) = &device.public_key {
println!(" public key: {}", short_key(key));
}
if let Some(port) = device.listen_port {
println!(" listen port: {port}");
}
if let Some(fwmark) = device.fwmark
&& fwmark != 0
{
println!(" fwmark: 0x{fwmark:x}");
}
if device.peers.is_empty() {
println!(" peers: (none)");
} else {
println!(" peers: {}", device.peers.len());
for peer in &device.peers {
print_peer(peer);
}
}
}
fn print_peer(peer: &WgPeer) {
println!(" peer: {}", short_key(&peer.public_key));
if let Some(ep) = &peer.endpoint {
println!(" endpoint: {ep}");
}
if !peer.allowed_ips.is_empty() {
let s: Vec<String> = peer
.allowed_ips
.iter()
.map(|ip| format!("{}/{}", ip.addr, ip.cidr))
.collect();
println!(" allowed ips: {}", s.join(", "));
}
if let Some(k) = peer.persistent_keepalive
&& k > 0
{
println!(" keepalive: every {k}s");
}
if let Some(t) = peer.last_handshake
&& let Ok(d) = SystemTime::now().duration_since(t)
{
println!(" handshake: {} seconds ago", d.as_secs());
}
if peer.rx_bytes > 0 || peer.tx_bytes > 0 {
println!(
" transfer: {} rx, {} tx (bytes)",
peer.rx_bytes, peer.tx_bytes
);
}
}
fn short_key(key: &[u8; 32]) -> String {
format!(
"{:02x}{:02x}{:02x}{:02x}…{:02x}{:02x}{:02x}{:02x}",
key[0], key[1], key[2], key[3], key[28], key[29], key[30], key[31],
)
}