From 0e1e93b6b7d25b61ebba972c1bc984a2a0f8479f Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 1 Sep 2026 18:59:42 -0600 Subject: [PATCH 1/2] fix(net): Exclude frame padding from transport checksums Limit transport checksums to the payload length declared by the IP header so Ethernet padding is never treated as payload. Restores the fix from d85bdfa1e0, accidentally reverted by 4b916284ad during a rebase. Co-authored-by: Claude Opus 5 (1M context) Signed-off-by: Daniel Noland --- net/src/headers/mod.rs | 14 +++++ net/src/packet/mod.rs | 134 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 1 deletion(-) diff --git a/net/src/headers/mod.rs b/net/src/headers/mod.rs index 67e439abff..d81a84b950 100644 --- a/net/src/headers/mod.rs +++ b/net/src/headers/mod.rs @@ -890,6 +890,20 @@ impl Headers { } } + pub(crate) fn transport_payload_len(&self) -> Option { + let ip_payload_len = match self.net.as_ref()? { + Net::Ipv4(ip) => usize::from(ip.0.payload_len().ok()?), + Net::Ipv6(ip) => usize::from(ip.0.payload_length), + }; + let after_net = self + .net_ext + .iter() + .map(|ext| usize::from(ext.size().get())) + .sum::() + + usize::from(self.transport.as_ref()?.size().get()); + ip_payload_len.checked_sub(after_net) + } + /// update the checksums of the headers pub(crate) fn update_checksums(&mut self, payload: impl AsRef<[u8]>) { let is_vxlan = self.try_vxlan().is_some(); diff --git a/net/src/packet/mod.rs b/net/src/packet/mod.rs index 3cdee54bf3..35735ea1f6 100644 --- a/net/src/packet/mod.rs +++ b/net/src/packet/mod.rs @@ -327,7 +327,12 @@ impl Packet { /// Update the network and transport checksums based on the current headers. pub fn update_checksums(&mut self) -> &mut Self { - self.headers.update_checksums(&self.payload); + let payload = self.payload.as_ref(); + let payload = match self.headers.transport_payload_len() { + Some(len) if len <= payload.len() => &payload[..len], + _ => payload, + }; + self.headers.update_checksums(payload); self.meta_mut().set_checksum_refresh(false); self } @@ -888,3 +893,130 @@ mod qos_roundtrip_tests { } } } + +#[cfg(test)] +mod padding_tests { + use crate::buffer::TestBuffer; + use crate::checksum::Checksum; + use crate::headers::{TryHeaders, TryIcmp4, TryIp, TryTcp, TryUdp}; + use crate::packet::Packet; + use crate::tcp::TcpChecksumPayload; + use crate::udp::UdpChecksumPayload; + + const MIN_ETHERNET_FRAME: usize = 60; + + fn frame(protocol: u8, l4: &[u8]) -> Vec { + let mut frame = Vec::new(); + frame.extend_from_slice(&[0x02, 0, 0, 0, 0, 1]); // destination mac + frame.extend_from_slice(&[0x02, 0, 0, 0, 0, 2]); // source mac + frame.extend_from_slice(&[0x08, 0x00]); // ipv4 + frame.extend_from_slice(&[0x45, 0x00]); + #[allow(clippy::cast_possible_truncation)] // test input is small + frame.extend_from_slice(&((20 + l4.len()) as u16).to_be_bytes()); + frame.extend_from_slice(&[0x00, 0x01, 0x00, 0x00, 0x40, protocol, 0x00, 0x00]); + frame.extend_from_slice(&[192, 168, 0, 1]); // source ip + frame.extend_from_slice(&[192, 168, 0, 2]); // destination ip + frame.extend_from_slice(l4); + frame + } + + fn tcp_ack() -> Vec { + let mut tcp = Vec::new(); + tcp.extend_from_slice(&1000_u16.to_be_bytes()); // source port + tcp.extend_from_slice(&2000_u16.to_be_bytes()); // destination port + tcp.extend_from_slice(&[0, 0, 0, 1]); // sequence number + tcp.extend_from_slice(&[0, 0, 0, 2]); // acknowledgement number + tcp.extend_from_slice(&[0x50, 0x10]); // data offset 5, ACK + tcp.extend_from_slice(&1024_u16.to_be_bytes()); // window + tcp.extend_from_slice(&[0, 0]); // checksum + tcp.extend_from_slice(&[0, 0]); // urgent pointer + tcp + } + + fn udp(payload: &[u8]) -> Vec { + let mut udp = Vec::new(); + udp.extend_from_slice(&1000_u16.to_be_bytes()); // source port + udp.extend_from_slice(&2000_u16.to_be_bytes()); // destination port + #[allow(clippy::cast_possible_truncation)] // test input is small + udp.extend_from_slice(&((8 + payload.len()) as u16).to_be_bytes()); + udp.extend_from_slice(&[0, 0]); // checksum + udp.extend_from_slice(payload); + udp + } + + fn icmp4_echo_request() -> Vec { + vec![8, 0, 0, 0, 0x00, 0x2a, 0x00, 0x01] + } + + fn parse(frame: &[u8]) -> Packet { + Packet::new(TestBuffer::from_raw_data(frame)).expect("frame does not parse") + } + + fn pad(mut frame: Vec, filler: u8) -> Vec { + assert!(frame.len() < MIN_ETHERNET_FRAME, "frame needs no padding"); + frame.resize(MIN_ETHERNET_FRAME, filler); + frame + } + + #[test] + fn tcp_checksum_excludes_zeroed_ethernet_padding() { + let mut packet = parse(&pad(frame(6, &tcp_ack()), 0)); + packet.update_checksums(); + let net = packet.headers().try_ip().expect("no ip header").clone(); + packet + .headers() + .try_tcp() + .expect("no tcp header") + .validate_checksum(&TcpChecksumPayload::new(&net, &[])) + .expect("padding leaked into the tcp checksum"); + } + + #[test] + fn udp_checksum_excludes_non_zero_ethernet_padding() { + let mut packet = parse(&pad(frame(17, &udp(&[])), 0xab)); + packet.update_checksums(); + let net = packet.headers().try_ip().expect("no ip header").clone(); + packet + .headers() + .try_udp() + .expect("no udp header") + .validate_checksum(&UdpChecksumPayload::new(&net, &[])) + .expect("padding leaked into the udp checksum"); + } + + #[test] + fn icmp4_checksum_excludes_non_zero_ethernet_padding() { + let mut packet = parse(&pad(frame(1, &icmp4_echo_request()), 0xab)); + packet.update_checksums(); + packet + .headers() + .try_icmp4() + .expect("no icmp header") + .validate_checksum(&[]) + .expect("padding leaked into the icmp checksum"); + } + + #[test] + fn checksum_still_covers_a_real_payload() { + let payload: Vec = (0..32_u8).collect(); + let mut packet = parse(&frame(17, &udp(&payload))); + assert_eq!(packet.payload().as_ref(), payload.as_slice()); + packet.update_checksums(); + let net = packet.headers().try_ip().expect("no ip header").clone(); + packet + .headers() + .try_udp() + .expect("no udp header") + .validate_checksum(&UdpChecksumPayload::new(&net, &payload)) + .expect("payload dropped out of the udp checksum"); + } + + #[test] + fn truncated_payload_does_not_panic() { + let payload: Vec = (0..32_u8).collect(); + let mut frame = frame(17, &udp(&payload)); + frame.truncate(frame.len() - 8); + let mut packet = parse(&frame); + packet.update_checksums(); + } +} From bb5cd4c3039597b5fa2942973ba5ab13a8a5398c Mon Sep 17 00:00:00 2001 From: Daniel Noland Date: Tue, 1 Sep 2026 12:50:37 -0600 Subject: [PATCH 2/2] test(net): Cover frame padding through VXLAN Generate payloads and arbitrary trailing bytes for TCP, UDP, and a VXLAN round trip. Build the tunnel with `Packet::vxlan_encap` so the fixture follows production and verifies that padding never affects transport checksums. Co-authored-by: Claude Opus 5 (1M context) Signed-off-by: Daniel Noland --- net/src/packet/mod.rs | 153 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 152 insertions(+), 1 deletion(-) diff --git a/net/src/packet/mod.rs b/net/src/packet/mod.rs index 35735ea1f6..003206faa4 100644 --- a/net/src/packet/mod.rs +++ b/net/src/packet/mod.rs @@ -898,10 +898,15 @@ mod qos_roundtrip_tests { mod padding_tests { use crate::buffer::TestBuffer; use crate::checksum::Checksum; - use crate::headers::{TryHeaders, TryIcmp4, TryIp, TryTcp, TryUdp}; + use crate::eth::ethtype::EthType; + use crate::headers::{Headers, Net, TryHeaders, TryIcmp4, TryIp, TryTcp, TryUdp}; use crate::packet::Packet; + use crate::packet::test_utils::make_default_for_eth; use crate::tcp::TcpChecksumPayload; use crate::udp::UdpChecksumPayload; + use crate::udp::UdpEncap; + use crate::vxlan::{Vni, Vxlan, VxlanEncap}; + use arrayvec::ArrayVec; const MIN_ETHERNET_FRAME: usize = 60; @@ -1019,4 +1024,150 @@ mod padding_tests { let mut packet = parse(&frame); packet.update_checksums(); } + + fn tcp(payload: &[u8]) -> Vec { + let mut tcp = tcp_ack(); + tcp.extend_from_slice(payload); + tcp + } + + fn vxlan_wrap(inner: &[u8]) -> Vec { + let mut packet = + Packet::new(TestBuffer::from_raw_data(inner)).expect("inner frame does not parse"); + packet + .vxlan_encap(&vxlan_encap_params()) + .expect("vxlan encap failed"); + packet + .serialize() + .expect("vxlan frame does not serialize") + .as_ref() + .to_vec() + } + + fn vxlan_encap_params() -> VxlanEncap { + let mut ip = crate::ipv4::Ipv4::default(); + ip.set_source( + crate::ipv4::addr::UnicastIpv4Addr::new("10.0.0.1".parse().unwrap()).unwrap(), + ); + ip.set_destination("10.0.0.2".parse().unwrap()); + ip.set_ttl(64); + ip.set_next_header(crate::ip::NextHeader::UDP); + + let headers = Headers { + eth: Some(make_default_for_eth(EthType::IPV4)), + vlan: ArrayVec::default(), + net: Some(Net::Ipv4(ip)), + net_ext: ArrayVec::default(), + transport: None, + udp_encap: Some(UdpEncap::Vxlan(Vxlan::new(Vni::new_checked(42).unwrap()))), + embedded_ip: None, + }; + VxlanEncap::new(headers).unwrap_or_else(|e| unreachable!("{e:?}")) + } + + #[test] + fn vxlan_carries_padding_that_the_inner_checksum_must_ignore() { + let inner = pad(frame(6, &tcp_ack()), 0xab); + assert_eq!( + inner.len(), + MIN_ETHERNET_FRAME, + "the inner frame should have been padded" + ); + + let mut packet = parse(&vxlan_wrap(&inner)); + packet + .vxlan_decap() + .expect("not a vxlan packet") + .expect("inner frame does not parse"); + packet.update_checksums(); + + let net = packet + .headers() + .try_ip() + .expect("no inner ip header") + .clone(); + packet + .headers() + .try_tcp() + .expect("no inner tcp header") + .validate_checksum(&TcpChecksumPayload::new(&net, &[])) + .expect("inner ethernet padding leaked into the tcp checksum"); + } + + #[test] + fn trailing_octets_never_enter_a_tcp_checksum() { + bolero::check!() + .with_type::<(Vec, Vec)>() + .for_each(|(payload, trailer)| { + let payload = &payload[..payload.len().min(256)]; + let trailer = &trailer[..trailer.len().min(64)]; + let mut bytes = frame(6, &tcp(payload)); + bytes.extend_from_slice(trailer); + let Ok(mut packet) = Packet::new(TestBuffer::from_raw_data(&bytes)) else { + return; + }; + packet.update_checksums(); + let net = packet.headers().try_ip().expect("no ip header").clone(); + packet + .headers() + .try_tcp() + .expect("no tcp header") + .validate_checksum(&TcpChecksumPayload::new(&net, payload)) + .expect("trailing octets leaked into the tcp checksum"); + }); + } + + #[test] + fn trailing_octets_never_enter_a_udp_checksum() { + bolero::check!() + .with_type::<(Vec, Vec)>() + .for_each(|(payload, trailer)| { + let payload = &payload[..payload.len().min(256)]; + let trailer = &trailer[..trailer.len().min(64)]; + let mut bytes = frame(17, &udp(payload)); + bytes.extend_from_slice(trailer); + let Ok(mut packet) = Packet::new(TestBuffer::from_raw_data(&bytes)) else { + return; + }; + packet.update_checksums(); + let net = packet.headers().try_ip().expect("no ip header").clone(); + packet + .headers() + .try_udp() + .expect("no udp header") + .validate_checksum(&UdpChecksumPayload::new(&net, payload)) + .expect("trailing octets leaked into the udp checksum"); + }); + } + + #[test] + fn trailing_octets_never_enter_a_vxlan_inner_tcp_checksum() { + bolero::check!() + .with_type::<(Vec, Vec)>() + .for_each(|(payload, trailer)| { + let payload = &payload[..payload.len().min(128)]; + let trailer = &trailer[..trailer.len().min(64)]; + let mut inner = frame(6, &tcp(payload)); + inner.extend_from_slice(trailer); + let Ok(mut packet) = Packet::new(TestBuffer::from_raw_data(&vxlan_wrap(&inner))) + else { + return; + }; + let Some(Ok(_)) = packet.vxlan_decap() else { + return; + }; + packet.update_checksums(); + let net = packet + .headers() + .try_ip() + .expect("no inner ip header") + .clone(); + packet + .headers() + .try_tcp() + .expect("no inner tcp header") + .validate_checksum(&TcpChecksumPayload::new(&net, payload)) + .expect("inner trailing octets leaked into the tcp checksum"); + }); + } }