const NLMSG_HDRLEN: usize = 16;
const NLMSG_ALIGNTO: usize = 4;
const RTA_HDRLEN: usize = 4;
const RTA_ALIGNTO: usize = 4;
const IFINFOMSG_LEN: usize = 16;
const RTM_NEWLINK: u16 = 16;
const RTM_NEWADDR: u16 = 20;
const IFADDRMSG_LEN: usize = 8;
const IFA_CACHEINFO: u16 = 6;
const IFLA_STATS: u16 = 7;
const IFLA_STATS64: u16 = 23;
const IFLA_AF_SPEC: u16 = 26;
const AF_INET6: u16 = 10;
const IFLA_INET6_STATS: u16 = 3;
const IFLA_INET6_ICMP6STATS: u16 = 6;
const IFLA_INET6_CACHEINFO: u16 = 5;
fn align_up(value: usize, align: usize) -> usize {
value.div_ceil(align) * align
}
pub fn sanitize_route_link_stats(buf: &mut [u8]) -> bool {
let mut modified = false;
let mut offset = 0usize;
while offset + NLMSG_HDRLEN <= buf.len() {
let len = u32::from_ne_bytes(match buf[offset..offset + 4].try_into() {
Ok(bytes) => bytes,
Err(_) => return modified,
}) as usize;
let msg_type = u16::from_ne_bytes(match buf[offset + 4..offset + 6].try_into() {
Ok(bytes) => bytes,
Err(_) => return modified,
});
if len < NLMSG_HDRLEN || offset + len > buf.len() {
return modified;
}
match msg_type {
RTM_NEWLINK => {
let body = offset + NLMSG_HDRLEN + IFINFOMSG_LEN;
let end = offset + len;
if body <= end {
zero_link_attrs(buf, body, end, &mut modified);
}
}
RTM_NEWADDR => {
let body = offset + NLMSG_HDRLEN + IFADDRMSG_LEN;
let end = offset + len;
if body <= end {
zero_addr_attrs(buf, body, end, &mut modified);
}
}
_ => {}
}
offset += align_up(len, NLMSG_ALIGNTO);
}
modified
}
fn zero_link_attrs(buf: &mut [u8], mut attr: usize, end: usize, modified: &mut bool) {
while attr + RTA_HDRLEN <= end {
let (alen, atype) = match attr_header(buf, attr) {
Some(header) => header,
None => return,
};
if alen < RTA_HDRLEN || attr + alen > end {
return;
}
let payload = attr + RTA_HDRLEN;
let payload_end = attr + alen;
match atype {
IFLA_STATS | IFLA_STATS64 => zero_range(buf, payload, payload_end, modified),
IFLA_AF_SPEC => zero_af_spec(buf, payload, payload_end, modified),
_ => {}
}
attr += align_up(alen, RTA_ALIGNTO);
}
}
fn zero_addr_attrs(buf: &mut [u8], mut attr: usize, end: usize, modified: &mut bool) {
while attr + RTA_HDRLEN <= end {
let (alen, atype) = match attr_header(buf, attr) {
Some(header) => header,
None => return,
};
if alen < RTA_HDRLEN || attr + alen > end {
return;
}
if atype == IFA_CACHEINFO {
zero_range(buf, attr + RTA_HDRLEN, attr + alen, modified);
}
attr += align_up(alen, RTA_ALIGNTO);
}
}
fn zero_af_spec(buf: &mut [u8], mut fam: usize, end: usize, modified: &mut bool) {
while fam + RTA_HDRLEN <= end {
let (flen, family) = match attr_header(buf, fam) {
Some(header) => header,
None => return,
};
if flen < RTA_HDRLEN || fam + flen > end {
return;
}
if family == AF_INET6 {
let mut inner = fam + RTA_HDRLEN;
let inner_end = fam + flen;
while inner + RTA_HDRLEN <= inner_end {
let (ilen, itype) = match attr_header(buf, inner) {
Some(header) => header,
None => return,
};
if ilen < RTA_HDRLEN || inner + ilen > inner_end {
return;
}
if matches!(
itype,
IFLA_INET6_STATS | IFLA_INET6_ICMP6STATS | IFLA_INET6_CACHEINFO
) {
zero_range(buf, inner + RTA_HDRLEN, inner + ilen, modified);
}
inner += align_up(ilen, RTA_ALIGNTO);
}
}
fam += align_up(flen, RTA_ALIGNTO);
}
}
fn attr_header(buf: &[u8], at: usize) -> Option<(usize, u16)> {
let len = u16::from_ne_bytes(buf.get(at..at + 2)?.try_into().ok()?) as usize;
let kind = u16::from_ne_bytes(buf.get(at + 2..at + 4)?.try_into().ok()?);
Some((len, kind))
}
fn zero_range(buf: &mut [u8], from: usize, to: usize, modified: &mut bool) {
if from >= to || to > buf.len() {
return;
}
for byte in &mut buf[from..to] {
if *byte != 0 {
*byte = 0;
*modified = true;
}
}
}
#[cfg(test)]
mod test {
use super::*;
fn rtattr(kind: u16, payload: &[u8]) -> Vec<u8> {
let len = RTA_HDRLEN + payload.len();
let mut out = Vec::new();
out.extend_from_slice(&(len as u16).to_ne_bytes());
out.extend_from_slice(&kind.to_ne_bytes());
out.extend_from_slice(payload);
out.resize(align_up(len, RTA_ALIGNTO), 0);
out
}
fn newlink(attrs: &[u8]) -> Vec<u8> {
let len = NLMSG_HDRLEN + IFINFOMSG_LEN + attrs.len();
let mut out = Vec::new();
out.extend_from_slice(&(len as u32).to_ne_bytes());
out.extend_from_slice(&RTM_NEWLINK.to_ne_bytes());
out.extend_from_slice(&0u16.to_ne_bytes());
out.extend_from_slice(&0u32.to_ne_bytes());
out.extend_from_slice(&0u32.to_ne_bytes());
out.extend_from_slice(&[0u8; IFINFOMSG_LEN]);
out.extend_from_slice(attrs);
out
}
#[test]
fn zeroes_link_stats64() {
let mut msg = newlink(&rtattr(IFLA_STATS64, &[7u8; 16]));
let payload = NLMSG_HDRLEN + IFINFOMSG_LEN + RTA_HDRLEN;
assert!(sanitize_route_link_stats(&mut msg));
assert!(
msg[payload..].iter().all(|b| *b == 0),
"IFLA_STATS64 counters were not fully zeroed"
);
}
#[test]
fn zeroes_legacy_link_stats() {
let mut msg = newlink(&rtattr(IFLA_STATS, &[9u8; 12]));
assert!(sanitize_route_link_stats(&mut msg));
assert!(!msg.contains(&9));
}
#[test]
fn zeroes_ipv6_stats_nested_two_levels_inside_af_spec() {
let inner = rtattr(IFLA_INET6_STATS, &[5u8; 24]);
let family = rtattr(AF_INET6, &inner);
let mut msg = newlink(&rtattr(IFLA_AF_SPEC, &family));
assert!(sanitize_route_link_stats(&mut msg));
assert!(
!msg.contains(&5),
"nested IPv6 SNMP counters were left undeterminized"
);
}
#[test]
fn leaves_non_counter_attributes_alone() {
const IFLA_IFNAME: u16 = 3;
let mut msg = newlink(&rtattr(IFLA_IFNAME, b"eth0\0"));
assert!(!sanitize_route_link_stats(&mut msg));
assert!(
msg.windows(4).any(|w| w == b"eth0"),
"the interface name was modified"
);
}
#[test]
fn reports_unmodified_when_there_is_nothing_to_zero() {
let mut msg = newlink(&rtattr(IFLA_STATS64, &[0u8; 16]));
assert!(!sanitize_route_link_stats(&mut msg));
}
#[test]
fn refuses_to_rewrite_a_truncated_message() {
let full = newlink(&rtattr(IFLA_STATS64, &[3u8; 16]));
let mut truncated = full[..full.len() - 6].to_vec();
let before = truncated.clone();
sanitize_route_link_stats(&mut truncated);
assert_eq!(
truncated, before,
"a truncated reply must not be partially rewritten"
);
}
#[test]
fn ignores_message_types_that_are_not_newlink() {
let mut msg = newlink(&rtattr(IFLA_STATS64, &[4u8; 16]));
msg[4..6].copy_from_slice(&20u16.to_ne_bytes());
assert!(!sanitize_route_link_stats(&mut msg));
assert!(msg.contains(&4));
}
}