use std::net::Ipv4Addr;
const NLMSG_ERROR: u16 = 2;
const RTM_NEWROUTE: u16 = 24;
const RTM_NEWRULE: u16 = 32;
const RTM_DELRULE: u16 = 33;
const NLM_F_REQUEST: u16 = 0x1;
const NLM_F_ACK: u16 = 0x4;
const NLM_F_REPLACE: u16 = 0x100;
const NLM_F_EXCL: u16 = 0x200;
const NLM_F_CREATE: u16 = 0x400;
const AF_INET: u8 = 2;
const RT_TABLE_UNSPEC: u8 = 0;
const RT_TABLE_MAIN: u8 = 254;
const RTPROT_BOOT: u8 = 3;
const RT_SCOPE_UNIVERSE: u8 = 0;
const RT_SCOPE_LINK: u8 = 253;
const RTN_UNICAST: u8 = 1;
const FR_ACT_TO_TBL: u8 = 1;
const RTNH_F_ONLINK: u32 = 4;
const RTA_DST: u16 = 1;
const RTA_GATEWAY: u16 = 5;
const RTA_OIF: u16 = 4;
const RTA_TABLE: u16 = 15;
const FRA_PRIORITY: u16 = 6;
const FRA_FWMARK: u16 = 10;
const FRA_TABLE: u16 = 15;
const NLMSG_HDR_LEN: usize = 16;
struct MessageBuilder {
buf: Vec<u8>,
}
impl MessageBuilder {
fn new(msg_type: u16, flags: u16, header: [u8; 12]) -> Self {
let mut buf = Vec::with_capacity(64);
buf.extend_from_slice(&0u32.to_ne_bytes()); buf.extend_from_slice(&msg_type.to_ne_bytes());
buf.extend_from_slice(&(NLM_F_REQUEST | NLM_F_ACK | flags).to_ne_bytes());
buf.extend_from_slice(&1u32.to_ne_bytes()); buf.extend_from_slice(&0u32.to_ne_bytes()); buf.extend_from_slice(&header);
Self { buf }
}
fn attr(&mut self, attr_type: u16, payload: &[u8]) {
let len = 4 + payload.len();
self.buf
.extend_from_slice(&u16::try_from(len).expect("attr fits u16").to_ne_bytes());
self.buf.extend_from_slice(&attr_type.to_ne_bytes());
self.buf.extend_from_slice(payload);
self.buf.resize(self.buf.len().next_multiple_of(4), 0);
}
fn attr_u32(&mut self, attr_type: u16, value: u32) {
self.attr(attr_type, &value.to_ne_bytes());
}
fn finish(mut self) -> Vec<u8> {
let len = u32::try_from(self.buf.len()).expect("message fits u32");
self.buf[..4].copy_from_slice(&len.to_ne_bytes());
self.buf
}
}
fn fib_rule_header(table: u8, action: u8) -> [u8; 12] {
let mut header = [0u8; 12];
header[0] = AF_INET;
header[4] = table;
header[7] = action;
header
}
fn fwmark_rule(msg_type: u16, flags: u16, mark: u32, table: u32, priority: u32) -> Vec<u8> {
let mut msg = MessageBuilder::new(
msg_type,
flags,
fib_rule_header(RT_TABLE_UNSPEC, FR_ACT_TO_TBL),
);
msg.attr_u32(FRA_PRIORITY, priority);
msg.attr_u32(FRA_FWMARK, mark);
msg.attr_u32(FRA_TABLE, table);
msg.finish()
}
pub(super) fn new_fwmark_rule(mark: u32, table: u32, priority: u32) -> Vec<u8> {
fwmark_rule(
RTM_NEWRULE,
NLM_F_CREATE | NLM_F_EXCL,
mark,
table,
priority,
)
}
pub(super) fn del_fwmark_rule(mark: u32, table: u32, priority: u32) -> Vec<u8> {
fwmark_rule(RTM_DELRULE, 0, mark, table, priority)
}
pub(super) fn replace_link_route(dst: Ipv4Addr, oif: u32, table: u32) -> Vec<u8> {
let mut header = [0u8; 12];
header[0] = AF_INET;
header[1] = 32; header[4] = RT_TABLE_UNSPEC;
header[5] = RTPROT_BOOT;
header[6] = RT_SCOPE_LINK;
header[7] = RTN_UNICAST;
let mut msg = MessageBuilder::new(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_REPLACE, header);
msg.attr_u32(RTA_TABLE, table);
msg.attr(RTA_DST, &dst.octets());
msg.attr_u32(RTA_OIF, oif);
msg.finish()
}
pub(super) fn replace_gateway_route(dst: Ipv4Addr, gateway: Ipv4Addr, oif: u32) -> Vec<u8> {
let mut header = [0u8; 12];
header[0] = AF_INET;
header[1] = 32; header[4] = RT_TABLE_MAIN;
header[5] = RTPROT_BOOT;
header[6] = RT_SCOPE_UNIVERSE;
header[7] = RTN_UNICAST;
header[8..12].copy_from_slice(&RTNH_F_ONLINK.to_ne_bytes());
let mut msg = MessageBuilder::new(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_REPLACE, header);
msg.attr(RTA_DST, &dst.octets());
msg.attr(RTA_GATEWAY, &gateway.octets());
msg.attr_u32(RTA_OIF, oif);
msg.finish()
}
#[cfg(target_os = "linux")]
pub(super) fn execute(message: &[u8], tolerated_errnos: &[i32]) -> crate::error::Result<()> {
use crate::error::VmmError;
let fd = unsafe {
libc::socket(
libc::AF_NETLINK,
libc::SOCK_RAW | libc::SOCK_CLOEXEC,
libc::NETLINK_ROUTE,
)
};
if fd < 0 {
return Err(VmmError::Network(format!(
"netlink socket: {}",
std::io::Error::last_os_error()
)));
}
let fd = unsafe { <std::os::fd::OwnedFd as std::os::fd::FromRawFd>::from_raw_fd(fd) };
let raw = std::os::fd::AsRawFd::as_raw_fd(&fd);
let sent = unsafe { libc::send(raw, message.as_ptr().cast(), message.len(), 0) };
if sent != message.len().cast_signed() {
return Err(VmmError::Network(format!(
"netlink send: {}",
std::io::Error::last_os_error()
)));
}
let mut reply = [0u8; 4096];
let received = unsafe { libc::recv(raw, reply.as_mut_ptr().cast(), reply.len(), 0) };
if received < 0 {
return Err(VmmError::Network(format!(
"netlink recv: {}",
std::io::Error::last_os_error()
)));
}
match ack_errno(&reply[..received as usize]) {
Ok(0) => Ok(()),
Ok(errno) if tolerated_errnos.contains(&errno) => Ok(()),
Ok(errno) => Err(VmmError::Network(format!(
"netlink request failed: {}",
std::io::Error::from_raw_os_error(errno)
))),
Err(reason) => Err(VmmError::Network(format!("netlink reply: {reason}"))),
}
}
fn ack_errno(reply: &[u8]) -> std::result::Result<i32, String> {
if reply.len() < NLMSG_HDR_LEN + 4 {
return Err(format!("truncated reply ({} bytes)", reply.len()));
}
let msg_type = u16::from_ne_bytes(reply[4..6].try_into().expect("length checked"));
if msg_type != NLMSG_ERROR {
return Err(format!("unexpected reply type {msg_type}"));
}
let negative_errno = i32::from_ne_bytes(
reply[NLMSG_HDR_LEN..NLMSG_HDR_LEN + 4]
.try_into()
.expect("length checked"),
);
Ok(-negative_errno)
}
#[cfg(test)]
mod tests {
use super::*;
fn attrs(message: &[u8]) -> Vec<(u16, Vec<u8>)> {
let total = u32::from_ne_bytes(message[..4].try_into().unwrap()) as usize;
assert_eq!(total, message.len(), "nlmsghdr len must cover the message");
let mut offset = NLMSG_HDR_LEN + 12;
let mut parsed = vec![];
while offset < total {
let len = u16::from_ne_bytes(message[offset..offset + 2].try_into().unwrap()) as usize;
let ty = u16::from_ne_bytes(message[offset + 2..offset + 4].try_into().unwrap());
parsed.push((ty, message[offset + 4..offset + len].to_vec()));
offset += len.next_multiple_of(4);
}
parsed
}
fn msg_type_and_flags(message: &[u8]) -> (u16, u16) {
(
u16::from_ne_bytes(message[4..6].try_into().unwrap()),
u16::from_ne_bytes(message[6..8].try_into().unwrap()),
)
}
#[test]
fn fwmark_rule_carries_mark_table_and_priority() {
let msg = new_fwmark_rule(0xAC14_0002, 0xAC14_0002, 8000);
let (ty, flags) = msg_type_and_flags(&msg);
assert_eq!(ty, RTM_NEWRULE);
assert_eq!(
flags,
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_EXCL,
"adds must be EXCL so retries surface EEXIST instead of stacking rules"
);
assert_eq!(msg[NLMSG_HDR_LEN], AF_INET);
assert_eq!(msg[NLMSG_HDR_LEN + 7], FR_ACT_TO_TBL);
assert_eq!(
attrs(&msg),
vec![
(FRA_PRIORITY, 8000u32.to_ne_bytes().to_vec()),
(FRA_FWMARK, 0xAC14_0002u32.to_ne_bytes().to_vec()),
(FRA_TABLE, 0xAC14_0002u32.to_ne_bytes().to_vec()),
]
);
}
#[test]
fn rule_delete_mirrors_the_add() {
let add = new_fwmark_rule(7, 7, 8000);
let del = del_fwmark_rule(7, 7, 8000);
assert_eq!(msg_type_and_flags(&del).0, RTM_DELRULE);
assert_eq!(attrs(&add), attrs(&del), "delete must match the added rule");
}
#[test]
fn link_route_targets_the_device_in_the_sandbox_table() {
let msg = replace_link_route(Ipv4Addr::new(169, 254, 100, 2), 42, 0xAC14_0002);
let (ty, flags) = msg_type_and_flags(&msg);
assert_eq!(ty, RTM_NEWROUTE);
assert_eq!(
flags,
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_REPLACE
);
assert_eq!(msg[NLMSG_HDR_LEN], AF_INET);
assert_eq!(msg[NLMSG_HDR_LEN + 1], 32);
assert_eq!(msg[NLMSG_HDR_LEN + 6], RT_SCOPE_LINK);
assert_eq!(msg[NLMSG_HDR_LEN + 7], RTN_UNICAST);
assert_eq!(
attrs(&msg),
vec![
(RTA_TABLE, 0xAC14_0002u32.to_ne_bytes().to_vec()),
(RTA_DST, vec![169, 254, 100, 2]),
(RTA_OIF, 42u32.to_ne_bytes().to_vec()),
]
);
}
#[test]
fn gateway_route_is_an_onlink_main_table_replace() {
let msg = replace_gateway_route(
Ipv4Addr::new(172, 20, 0, 2),
Ipv4Addr::new(169, 254, 100, 2),
42,
);
let (ty, flags) = msg_type_and_flags(&msg);
assert_eq!(ty, RTM_NEWROUTE);
assert_eq!(
flags,
NLM_F_REQUEST | NLM_F_ACK | NLM_F_CREATE | NLM_F_REPLACE,
"must replace the kernel's peer route for the same /32"
);
assert_eq!(msg[NLMSG_HDR_LEN], AF_INET);
assert_eq!(msg[NLMSG_HDR_LEN + 1], 32);
assert_eq!(msg[NLMSG_HDR_LEN + 4], RT_TABLE_MAIN);
assert_eq!(msg[NLMSG_HDR_LEN + 6], RT_SCOPE_UNIVERSE);
assert_eq!(msg[NLMSG_HDR_LEN + 7], RTN_UNICAST);
assert_eq!(
msg[NLMSG_HDR_LEN + 8..NLMSG_HDR_LEN + 12],
RTNH_F_ONLINK.to_ne_bytes()
);
assert_eq!(
attrs(&msg),
vec![
(RTA_DST, vec![172, 20, 0, 2]),
(RTA_GATEWAY, vec![169, 254, 100, 2]),
(RTA_OIF, 42u32.to_ne_bytes().to_vec()),
]
);
}
#[test]
fn ack_errno_decodes_success_and_failure() {
let mut ok = vec![0u8; 20];
ok[..4].copy_from_slice(&20u32.to_ne_bytes());
ok[4..6].copy_from_slice(&NLMSG_ERROR.to_ne_bytes());
assert_eq!(ack_errno(&ok), Ok(0));
let mut eexist = ok.clone();
eexist[NLMSG_HDR_LEN..NLMSG_HDR_LEN + 4].copy_from_slice(&(-17i32).to_ne_bytes());
assert_eq!(ack_errno(&eexist), Ok(17));
assert!(ack_errno(&[0u8; 4]).is_err());
}
}