mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 19:18:25 +00:00
Test the Ethernet receive loop's own data-frame parse
The data frame carries a two-byte length field so the receiver can trim Ethernet minimum-frame padding, which would otherwise reach AEAD as ciphertext. The unit test for that trim rebuilt the parse inline instead of calling the receive loop, so a regression in the loop's slice could not fail it: changing the loop to slice buf[3..len] left every Ethernet test green. The parse is now one function, data_payload, which the receive loop calls for every data frame and the tests drive directly; the loop keeps no other slice of the buffer for data frames. The padding test now runs against it, and new tests cover an unpadded frame, a zero-length padded frame, a frame shorter than the header and a length field larger than the received bytes. Dropping the length trim now fails the padding and zero-length tests, and dropping the bounds check fails the over-long length test. Behaviour is unchanged: the loop forwards the same bytes and drops the same frames with the same trace messages. The trim is covered by a unit test with a synthetic padded frame because no traffic reaches it on the veth links the chaos harness uses. Measured on a live ethernet-mesh run: an empty ICMPv6 echo is a 226-byte frame and a 1200-byte echo a 1342-byte frame; the smallest data frame seen was a 54-byte control frame, and short frames (54-byte data, 48-byte beacons) arrived unpadded; none of 250 received data frames carried bytes beyond its length field.
This commit is contained in:
+105
-31
@@ -384,6 +384,42 @@ impl Transport for EthernetTransport {
|
|||||||
// Receive Loop
|
// Receive Loop
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
/// Why a received data frame was dropped by [`data_payload`].
|
||||||
|
///
|
||||||
|
/// Private and only logged at trace, so not a `thiserror` type.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
enum DataFrameError {
|
||||||
|
/// Fewer bytes than the type byte and the length field.
|
||||||
|
TooShort { len: usize },
|
||||||
|
/// The length field names more payload than the frame carries.
|
||||||
|
LengthExceeds {
|
||||||
|
payload_len: usize,
|
||||||
|
available: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract the payload of a received data frame.
|
||||||
|
///
|
||||||
|
/// `frame` is the received bytes including the type byte, which the caller
|
||||||
|
/// has already matched as `FRAME_TYPE_DATA`. The layout is
|
||||||
|
/// `[type:1][length:2 LE][payload:N][padding]`: the length field is what
|
||||||
|
/// trims Ethernet minimum-frame padding, which would otherwise be handed to
|
||||||
|
/// AEAD verification as ciphertext.
|
||||||
|
fn data_payload(frame: &[u8]) -> Result<&[u8], DataFrameError> {
|
||||||
|
if frame.len() < 3 {
|
||||||
|
return Err(DataFrameError::TooShort { len: frame.len() });
|
||||||
|
}
|
||||||
|
let payload_len = u16::from_le_bytes([frame[1], frame[2]]) as usize;
|
||||||
|
let available = frame.len() - 3;
|
||||||
|
if payload_len > available {
|
||||||
|
return Err(DataFrameError::LengthExceeds {
|
||||||
|
payload_len,
|
||||||
|
available,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(&frame[3..3 + payload_len])
|
||||||
|
}
|
||||||
|
|
||||||
/// Ethernet receive loop — runs as a spawned task.
|
/// Ethernet receive loop — runs as a spawned task.
|
||||||
async fn ethernet_receive_loop(
|
async fn ethernet_receive_loop(
|
||||||
socket: Arc<AsyncPacketSocket>,
|
socket: Arc<AsyncPacketSocket>,
|
||||||
@@ -411,29 +447,31 @@ async fn ethernet_receive_loop(
|
|||||||
let frame_type = buf[0];
|
let frame_type = buf[0];
|
||||||
match frame_type {
|
match frame_type {
|
||||||
FRAME_TYPE_DATA => {
|
FRAME_TYPE_DATA => {
|
||||||
// Data frame: [type:1][length:2 LE][payload:N]
|
let data = match data_payload(&buf[..len]) {
|
||||||
// Use the length field to trim Ethernet minimum-frame padding.
|
Ok(payload) => payload.to_vec(),
|
||||||
if len < 3 {
|
Err(DataFrameError::TooShort { len }) => {
|
||||||
trace!("Data frame too short ({len} bytes), ignoring");
|
trace!("Data frame too short ({len} bytes), ignoring");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let payload_len = u16::from_le_bytes([buf[1], buf[2]]) as usize;
|
Err(DataFrameError::LengthExceeds {
|
||||||
if payload_len > len - 3 {
|
payload_len,
|
||||||
trace!(
|
available,
|
||||||
"Data frame length field ({payload_len}) exceeds \
|
}) => {
|
||||||
available bytes ({}), ignoring",
|
trace!(
|
||||||
len - 3
|
"Data frame length field ({payload_len}) exceeds \
|
||||||
);
|
available bytes ({available}), ignoring"
|
||||||
continue;
|
);
|
||||||
}
|
continue;
|
||||||
let data = buf[3..3 + payload_len].to_vec();
|
}
|
||||||
|
};
|
||||||
|
let bytes = data.len();
|
||||||
let addr = TransportAddr::from_bytes(&src_mac);
|
let addr = TransportAddr::from_bytes(&src_mac);
|
||||||
let packet = ReceivedPacket::new(transport_id, addr, data);
|
let packet = ReceivedPacket::new(transport_id, addr, data);
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
transport_id = %transport_id,
|
transport_id = %transport_id,
|
||||||
remote_mac = %format_mac(&src_mac),
|
remote_mac = %format_mac(&src_mac),
|
||||||
bytes = payload_len,
|
bytes,
|
||||||
"Ethernet data frame received"
|
"Ethernet data frame received"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -717,26 +755,62 @@ mod tests {
|
|||||||
assert_eq!(&frame[3..], &[1, 2, 3, 4]); // payload
|
assert_eq!(&frame[3..], &[1, 2, 3, 4]); // payload
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build a data frame as the sender does: type, LE length, payload.
|
||||||
|
fn data_frame(payload: &[u8]) -> Vec<u8> {
|
||||||
|
let mut frame = Vec::with_capacity(3 + payload.len());
|
||||||
|
frame.push(FRAME_TYPE_DATA);
|
||||||
|
frame.extend_from_slice(&(payload.len() as u16).to_le_bytes());
|
||||||
|
frame.extend_from_slice(payload);
|
||||||
|
frame
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_data_frame_padding_trimmed() {
|
fn test_data_frame_padding_trimmed() {
|
||||||
// Simulate Ethernet minimum-frame padding: a 4-byte payload produces
|
// Simulate Ethernet minimum-frame padding: a 4-byte payload produces
|
||||||
// a 7-byte frame (type + len + payload), padded to 46 bytes by NIC.
|
// a 7-byte frame (type + len + payload), padded to 46 bytes by NIC.
|
||||||
let payload = vec![0xAA, 0xBB, 0xCC, 0xDD];
|
// Drives the receive loop's own parse, not a copy of it.
|
||||||
let payload_len = payload.len() as u16;
|
let mut frame = data_frame(&[0xAA, 0xBB, 0xCC, 0xDD]);
|
||||||
|
|
||||||
// Build frame as sender would
|
|
||||||
let mut frame = Vec::with_capacity(3 + payload.len());
|
|
||||||
frame.push(FRAME_TYPE_DATA);
|
|
||||||
frame.extend_from_slice(&payload_len.to_le_bytes());
|
|
||||||
frame.extend_from_slice(&payload);
|
|
||||||
|
|
||||||
// Simulate NIC padding to 46 bytes
|
|
||||||
frame.resize(46, 0x00);
|
frame.resize(46, 0x00);
|
||||||
|
|
||||||
// Receiver extracts using length field
|
assert_eq!(data_payload(&frame), Ok(&[0xAA, 0xBB, 0xCC, 0xDD][..]));
|
||||||
let recv_len = u16::from_le_bytes([frame[1], frame[2]]) as usize;
|
}
|
||||||
let extracted = &frame[3..3 + recv_len];
|
|
||||||
assert_eq!(extracted, &[0xAA, 0xBB, 0xCC, 0xDD]);
|
#[test]
|
||||||
|
fn test_data_payload_unpadded_frame_returns_whole_payload() {
|
||||||
|
let payload: Vec<u8> = (0..200).map(|i| i as u8).collect();
|
||||||
|
let frame = data_frame(&payload);
|
||||||
|
|
||||||
|
assert_eq!(data_payload(&frame), Ok(&payload[..]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_data_payload_zero_length_padded_frame_returns_empty() {
|
||||||
|
let mut frame = data_frame(&[]);
|
||||||
|
frame.resize(46, 0x00);
|
||||||
|
|
||||||
|
assert_eq!(data_payload(&frame), Ok(&[][..]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_data_payload_frame_shorter_than_header_is_rejected() {
|
||||||
|
assert_eq!(
|
||||||
|
data_payload(&[FRAME_TYPE_DATA, 0x04]),
|
||||||
|
Err(DataFrameError::TooShort { len: 2 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_data_payload_length_field_beyond_received_bytes_is_rejected() {
|
||||||
|
let mut frame = data_frame(&[0xAA, 0xBB, 0xCC, 0xDD]);
|
||||||
|
frame.truncate(6);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
data_payload(&frame),
|
||||||
|
Err(DataFrameError::LengthExceeds {
|
||||||
|
payload_len: 4,
|
||||||
|
available: 3
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user