1const NLMSG_HDRLEN: usize = 16;
49const NLMSG_ALIGNTO: usize = 4;
50
51const RTA_HDRLEN: usize = 4;
53const RTA_ALIGNTO: usize = 4;
54
55const IFINFOMSG_LEN: usize = 16;
57
58const RTM_NEWLINK: u16 = 16;
60
61const RTM_NEWADDR: u16 = 20;
65
66const IFADDRMSG_LEN: usize = 8;
70
71const IFA_CACHEINFO: u16 = 6;
74
75const IFLA_STATS: u16 = 7;
76const IFLA_STATS64: u16 = 23;
77const IFLA_AF_SPEC: u16 = 26;
78
79const AF_INET6: u16 = 10;
80const IFLA_INET6_STATS: u16 = 3;
81const IFLA_INET6_ICMP6STATS: u16 = 6;
82const IFLA_INET6_CACHEINFO: u16 = 5;
89
90fn align_up(value: usize, align: usize) -> usize {
91 value.div_ceil(align) * align
92}
93
94pub fn sanitize_route_link_stats(buf: &mut [u8]) -> bool {
102 let mut modified = false;
103 let mut offset = 0usize;
104
105 while offset + NLMSG_HDRLEN <= buf.len() {
106 let len = u32::from_ne_bytes(match buf[offset..offset + 4].try_into() {
107 Ok(bytes) => bytes,
108 Err(_) => return modified,
109 }) as usize;
110 let msg_type = u16::from_ne_bytes(match buf[offset + 4..offset + 6].try_into() {
111 Ok(bytes) => bytes,
112 Err(_) => return modified,
113 });
114
115 if len < NLMSG_HDRLEN || offset + len > buf.len() {
118 return modified;
119 }
120
121 match msg_type {
122 RTM_NEWLINK => {
123 let body = offset + NLMSG_HDRLEN + IFINFOMSG_LEN;
124 let end = offset + len;
125 if body <= end {
126 zero_link_attrs(buf, body, end, &mut modified);
127 }
128 }
129 RTM_NEWADDR => {
130 let body = offset + NLMSG_HDRLEN + IFADDRMSG_LEN;
131 let end = offset + len;
132 if body <= end {
133 zero_addr_attrs(buf, body, end, &mut modified);
134 }
135 }
136 _ => {}
137 }
138
139 offset += align_up(len, NLMSG_ALIGNTO);
140 }
141
142 modified
143}
144
145fn zero_link_attrs(buf: &mut [u8], mut attr: usize, end: usize, modified: &mut bool) {
147 while attr + RTA_HDRLEN <= end {
148 let (alen, atype) = match attr_header(buf, attr) {
149 Some(header) => header,
150 None => return,
151 };
152 if alen < RTA_HDRLEN || attr + alen > end {
153 return;
154 }
155 let payload = attr + RTA_HDRLEN;
156 let payload_end = attr + alen;
157
158 match atype {
159 IFLA_STATS | IFLA_STATS64 => zero_range(buf, payload, payload_end, modified),
160 IFLA_AF_SPEC => zero_af_spec(buf, payload, payload_end, modified),
161 _ => {}
162 }
163
164 attr += align_up(alen, RTA_ALIGNTO);
165 }
166}
167
168fn zero_addr_attrs(buf: &mut [u8], mut attr: usize, end: usize, modified: &mut bool) {
173 while attr + RTA_HDRLEN <= end {
174 let (alen, atype) = match attr_header(buf, attr) {
175 Some(header) => header,
176 None => return,
177 };
178 if alen < RTA_HDRLEN || attr + alen > end {
179 return;
180 }
181 if atype == IFA_CACHEINFO {
182 zero_range(buf, attr + RTA_HDRLEN, attr + alen, modified);
183 }
184 attr += align_up(alen, RTA_ALIGNTO);
185 }
186}
187
188fn zero_af_spec(buf: &mut [u8], mut fam: usize, end: usize, modified: &mut bool) {
192 while fam + RTA_HDRLEN <= end {
193 let (flen, family) = match attr_header(buf, fam) {
194 Some(header) => header,
195 None => return,
196 };
197 if flen < RTA_HDRLEN || fam + flen > end {
198 return;
199 }
200
201 if family == AF_INET6 {
202 let mut inner = fam + RTA_HDRLEN;
203 let inner_end = fam + flen;
204 while inner + RTA_HDRLEN <= inner_end {
205 let (ilen, itype) = match attr_header(buf, inner) {
206 Some(header) => header,
207 None => return,
208 };
209 if ilen < RTA_HDRLEN || inner + ilen > inner_end {
210 return;
211 }
212 if matches!(
213 itype,
214 IFLA_INET6_STATS | IFLA_INET6_ICMP6STATS | IFLA_INET6_CACHEINFO
215 ) {
216 zero_range(buf, inner + RTA_HDRLEN, inner + ilen, modified);
217 }
218 inner += align_up(ilen, RTA_ALIGNTO);
219 }
220 }
221
222 fam += align_up(flen, RTA_ALIGNTO);
223 }
224}
225
226fn attr_header(buf: &[u8], at: usize) -> Option<(usize, u16)> {
227 let len = u16::from_ne_bytes(buf.get(at..at + 2)?.try_into().ok()?) as usize;
228 let kind = u16::from_ne_bytes(buf.get(at + 2..at + 4)?.try_into().ok()?);
229 Some((len, kind))
230}
231
232fn zero_range(buf: &mut [u8], from: usize, to: usize, modified: &mut bool) {
233 if from >= to || to > buf.len() {
234 return;
235 }
236 for byte in &mut buf[from..to] {
237 if *byte != 0 {
238 *byte = 0;
239 *modified = true;
240 }
241 }
242}
243
244#[cfg(test)]
245mod test {
246 use super::*;
247
248 fn rtattr(kind: u16, payload: &[u8]) -> Vec<u8> {
249 let len = RTA_HDRLEN + payload.len();
250 let mut out = Vec::new();
251 out.extend_from_slice(&(len as u16).to_ne_bytes());
252 out.extend_from_slice(&kind.to_ne_bytes());
253 out.extend_from_slice(payload);
254 out.resize(align_up(len, RTA_ALIGNTO), 0);
255 out
256 }
257
258 fn newlink(attrs: &[u8]) -> Vec<u8> {
259 let len = NLMSG_HDRLEN + IFINFOMSG_LEN + attrs.len();
260 let mut out = Vec::new();
261 out.extend_from_slice(&(len as u32).to_ne_bytes());
262 out.extend_from_slice(&RTM_NEWLINK.to_ne_bytes());
263 out.extend_from_slice(&0u16.to_ne_bytes());
264 out.extend_from_slice(&0u32.to_ne_bytes());
265 out.extend_from_slice(&0u32.to_ne_bytes());
266 out.extend_from_slice(&[0u8; IFINFOMSG_LEN]);
267 out.extend_from_slice(attrs);
268 out
269 }
270
271 #[test]
272 fn zeroes_link_stats64() {
273 let mut msg = newlink(&rtattr(IFLA_STATS64, &[7u8; 16]));
274 let payload = NLMSG_HDRLEN + IFINFOMSG_LEN + RTA_HDRLEN;
275 assert!(sanitize_route_link_stats(&mut msg));
276 assert!(
277 msg[payload..].iter().all(|b| *b == 0),
278 "IFLA_STATS64 counters were not fully zeroed"
279 );
280 }
281
282 #[test]
283 fn zeroes_legacy_link_stats() {
284 let mut msg = newlink(&rtattr(IFLA_STATS, &[9u8; 12]));
285 assert!(sanitize_route_link_stats(&mut msg));
286 assert!(!msg.contains(&9));
287 }
288
289 #[test]
293 fn zeroes_ipv6_stats_nested_two_levels_inside_af_spec() {
294 let inner = rtattr(IFLA_INET6_STATS, &[5u8; 24]);
295 let family = rtattr(AF_INET6, &inner);
296 let mut msg = newlink(&rtattr(IFLA_AF_SPEC, &family));
297 assert!(sanitize_route_link_stats(&mut msg));
298 assert!(
299 !msg.contains(&5),
300 "nested IPv6 SNMP counters were left undeterminized"
301 );
302 }
303
304 #[test]
307 fn leaves_non_counter_attributes_alone() {
308 const IFLA_IFNAME: u16 = 3;
309 let mut msg = newlink(&rtattr(IFLA_IFNAME, b"eth0\0"));
310 assert!(!sanitize_route_link_stats(&mut msg));
311 assert!(
312 msg.windows(4).any(|w| w == b"eth0"),
313 "the interface name was modified"
314 );
315 }
316
317 #[test]
320 fn reports_unmodified_when_there_is_nothing_to_zero() {
321 let mut msg = newlink(&rtattr(IFLA_STATS64, &[0u8; 16]));
322 assert!(!sanitize_route_link_stats(&mut msg));
323 }
324
325 #[test]
327 fn refuses_to_rewrite_a_truncated_message() {
328 let full = newlink(&rtattr(IFLA_STATS64, &[3u8; 16]));
329 let mut truncated = full[..full.len() - 6].to_vec();
330 let before = truncated.clone();
331 sanitize_route_link_stats(&mut truncated);
332 assert_eq!(
333 truncated, before,
334 "a truncated reply must not be partially rewritten"
335 );
336 }
337
338 #[test]
340 fn ignores_message_types_that_are_not_newlink() {
341 let mut msg = newlink(&rtattr(IFLA_STATS64, &[4u8; 16]));
342 msg[4..6].copy_from_slice(&20u16.to_ne_bytes());
343 assert!(!sanitize_route_link_stats(&mut msg));
344 assert!(msg.contains(&4));
345 }
346}