//! ICMP error message generation — mirrors Linux 7.1's `icmp_send`. //! //! Reference files: //! - `net/ipv4/icmp.c:802` — `__icmp_send()` — builds ICMPv4 error messages //! - `net/ipv6/icmp.c:icmpv6_send()` — ICMPv6 error messages //! - `include/uapi/linux/icmp.h` — ICMP type/code constants //! //! ICMP error messages contain: ICMP header + original IP header + 8 bytes //! of original transport payload (RFC 792 §3.1, RFC 4443 §2.4). use smoltcp::wire::{ Icmpv4Packet, Icmpv4Repr, Icmpv6Packet, Icmpv6Repr, IpAddress, Ipv4Address, Ipv4Packet, Ipv4Repr, Ipv6Address, Ipv6Packet, Ipv6Repr, }; /// Builds an ICMPv4 Destination Unreachable error message (Type 3). /// Code 3 = Port Unreachable (mirrors `ICMP_PORT_UNREACH` in icmp.c). pub fn build_icmpv4_port_unreachable( original_packet: &[u8], ) -> Option> { let ipv4 = Ipv4Packet::new_checked(original_packet).ok()?; let src = ipv4.src_addr(); let dst = ipv4.dst_addr(); let ip_header_len = ipv4.header_len() as usize; let total_len = original_packet.len().min(ip_header_len + 8); let mut payload = alloc::vec![0u8; 4 + total_len]; payload[0] = 0; payload[1] = 0; payload[2] = 0; payload[3] = 0; payload[4..4 + total_len].copy_from_slice(&original_packet[..total_len]); let icmp_repr = Icmpv4Repr::DstUnreachable { reason: smoltcp::wire::Icmpv4DstUnreachable::PortUnreachable, header: Ipv4Repr { src_addr: dst, dst_addr: src, next_header: smoltcp::wire::IpProtocol::Icmp, hop_limit: 64, payload_len: 4 + total_len, }, data: &payload, }; let mut buf = alloc::vec![0u8; icmp_repr.buffer_len()]; let mut pkt = Icmpv4Packet::new_unchecked(&mut buf); icmp_repr.emit(&mut pkt, &smoltcp::phy::ChecksumCapabilities::ignored()); Some(buf) } /// Builds an ICMPv4 Time Exceeded error message (Type 11, Code 0). /// Mirrors `ICMP_TIME_EXCEEDED` / `ICMP_EXC_TTL` in `icmp.c:1168`. pub fn build_icmpv4_time_exceeded( original_packet: &[u8], ) -> Option> { let ipv4 = Ipv4Packet::new_checked(original_packet).ok()?; let src = ipv4.src_addr(); let dst = ipv4.dst_addr(); let ip_header_len = ipv4.header_len() as usize; let total_len = original_packet.len().min(ip_header_len + 8); let mut payload = alloc::vec![0u8; 4 + total_len]; payload[4..4 + total_len].copy_from_slice(&original_packet[..total_len]); let icmp_repr = Icmpv4Repr::TimeExceeded { reason: smoltcp::wire::Icmpv4TimeExceeded::TtlExpired, header: Ipv4Repr { src_addr: dst, dst_addr: src, next_header: smoltcp::wire::IpProtocol::Icmp, hop_limit: 64, payload_len: 4 + total_len, }, data: &payload, }; let mut buf = alloc::vec![0u8; icmp_repr.buffer_len()]; let mut pkt = Icmpv4Packet::new_unchecked(&mut buf); icmp_repr.emit(&mut pkt, &smoltcp::phy::ChecksumCapabilities::ignored()); Some(buf) } /// Builds an ICMPv6 Destination Unreachable error (Type 1, Code 4). pub fn build_icmpv6_port_unreachable( original_packet: &[u8], ) -> Option> { let ipv6 = Ipv6Packet::new_checked(original_packet).ok()?; let src = ipv6.src_addr(); let _dst = ipv6.dst_addr(); let total_len = original_packet.len().min(48); let mut data = alloc::vec![0u8; 4 + total_len]; data[4..4 + total_len].copy_from_slice(&original_packet[..total_len]); let icmp_repr = Icmpv6Repr::DstUnreachable { reason: smoltcp::wire::Icmpv6DstUnreachable::PortUnreachable, header: Ipv6Repr { src_addr: _dst, dst_addr: src, next_header: smoltcp::wire::IpProtocol::Icmpv6, hop_limit: 64, payload_len: 4 + total_len, }, data: &data, }; let mut buf = alloc::vec![0u8; icmp_repr.buffer_len()]; let mut pkt = Icmpv6Packet::new_unchecked(&mut buf); icmp_repr.emit(&_dst, &src, &mut pkt, &smoltcp::phy::ChecksumCapabilities::ignored()); Some(buf) } extern crate alloc; #[cfg(test)] mod tests { use super::*; fn make_standard_packet(src: [u8; 4], dst: [u8; 4]) -> Vec { // 20-byte standard IPv4 header + 8-byte UDP header. let mut p = vec![0u8; 28]; p[0] = 0x45; // version 4, IHL 5 p[2] = 0; p[3] = 28; p[8] = 64; p[9] = 17; // UDP p[10] = 0; p[11] = 0; p[12..16].copy_from_slice(&src); p[16..20].copy_from_slice(&dst); p } #[test] fn icmpv4_short_packet_returns_none() { let p = vec![0u8; 10]; assert!(build_icmpv4_port_unreachable(&p).is_none()); } #[test] fn icmpv4_preserves_destination_address() { // Regression: the previous code computed IHL as // (ipv4.version() & 0x0f) * 4 = 4*4 = 16 (WRONG). // smoltcp's version() returns 4 (the version number), not the // combined version/IHL byte. The correct IHL for a standard // 20-byte header is 5. With the bug, the ICMP error would // include only 16+8=24 bytes of original, truncating the // destination IP. With the fix, all 20+8=28 bytes included. let original = make_standard_packet([10, 0, 0, 1], [192, 168, 1, 1]); let icmp = build_icmpv4_port_unreachable(&original).expect("should build"); // ICMP DstUnreachable layout: // bytes 0-3: type(1) + code(1) + checksum(2) // bytes 4-7: unused(4) // bytes 8-31: ICMP-wrapped IP header (src=orig_dst, dst=orig_src) // bytes 32+: original IP packet (and L4 data) // The original IP's dst IP first byte is at offset 32+16=48. assert_eq!(icmp[48], 192, "dst[0] must be 192, was {}", icmp[48]); assert_eq!(icmp[49], 168); assert_eq!(icmp[50], 1); assert_eq!(icmp[51], 1); } #[test] fn icmpv4_with_ip_options_includes_extended_header() { // IHL=6 means 24-byte header (20 + 4 bytes options). // The bug would compute IHL=4 (16 bytes) and truncate. let mut p = vec![0u8; 32]; p[0] = 0x46; // version 4, IHL 6 p[2] = 0; p[3] = 32; p[8] = 64; p[9] = 17; p[10] = 0; p[11] = 0; p[12] = 10; p[13] = 0; p[14] = 0; p[15] = 1; // src p[16] = 192; p[17] = 168; p[18] = 1; p[19] = 1; // dst let icmp = build_icmpv4_port_unreachable(&p).expect("should build"); // Source IP of the original packet is at offset 32+12=44. // (See comment above for the full ICMP layout.) assert_eq!(icmp[44], 10, "src[0] must be 10 with IHL=6"); assert_eq!(icmp[45], 0); assert_eq!(icmp[46], 0); assert_eq!(icmp[47], 1); } }