use smoltcp::wire::{
EthernetAddress, EthernetFrame, EthernetProtocol, EthernetRepr, Icmpv4DstUnreachable,
Icmpv4Message, Icmpv4Packet, Icmpv6Message, Icmpv6Packet, IpProtocol, Ipv4Packet, Ipv6Packet,
};
const ETH_HDR_LEN: usize = 14;
const IPV4_HDR_LEN: usize = 20;
const IPV6_HDR_LEN: usize = 40;
const ICMP_ERROR_HDR_LEN: usize = 8;
const IPV6_MIN_MTU: usize = 1280;
pub(crate) fn construct_packet_too_big(
original_ip_packet: &[u8],
next_hop_mtu: u32,
gateway_mac: EthernetAddress,
guest_mac: EthernetAddress,
) -> Option<Vec<u8>> {
match original_ip_packet.first()? >> 4 {
4 => construct_icmpv4_fragmentation_needed(
original_ip_packet,
next_hop_mtu,
gateway_mac,
guest_mac,
),
6 => construct_icmpv6_packet_too_big(
original_ip_packet,
next_hop_mtu,
gateway_mac,
guest_mac,
),
_ => None,
}
}
pub(crate) fn ethernet_ip_payload(frame: &[u8]) -> Option<&[u8]> {
let eth = EthernetFrame::new_checked(frame).ok()?;
match eth.ethertype() {
EthernetProtocol::Ipv4 | EthernetProtocol::Ipv6 => Some(eth.payload()),
_ => None,
}
}
fn construct_icmpv4_fragmentation_needed(
original_ip_packet: &[u8],
next_hop_mtu: u32,
gateway_mac: EthernetAddress,
guest_mac: EthernetAddress,
) -> Option<Vec<u8>> {
let original = Ipv4Packet::new_checked(original_ip_packet).ok()?;
let original_header_len = original.header_len() as usize;
if original_ip_packet.len() < original_header_len || original_header_len < IPV4_HDR_LEN {
return None;
}
let original_total_len = usize::from(original.total_len());
let quoted_len = original_ip_packet
.len()
.min(original_total_len)
.min(original_header_len + 8);
if quoted_len < original_header_len {
return None;
}
let icmp_len = ICMP_ERROR_HDR_LEN + quoted_len;
let ip_total_len = IPV4_HDR_LEN + icmp_len;
let frame_len = ETH_HDR_LEN + ip_total_len;
let mut buf = vec![0u8; frame_len];
let mut eth_frame = EthernetFrame::new_unchecked(&mut buf);
EthernetRepr {
src_addr: gateway_mac,
dst_addr: guest_mac,
ethertype: EthernetProtocol::Ipv4,
}
.emit(&mut eth_frame);
{
let mut ip = Ipv4Packet::new_unchecked(&mut buf[ETH_HDR_LEN..]);
ip.set_version(4);
ip.set_header_len(IPV4_HDR_LEN as u8);
ip.set_total_len(ip_total_len as u16);
ip.clear_flags();
ip.set_hop_limit(64);
ip.set_next_header(IpProtocol::Icmp);
ip.set_src_addr(original.dst_addr());
ip.set_dst_addr(original.src_addr());
ip.fill_checksum();
}
let icmp_buf = &mut buf[ETH_HDR_LEN + IPV4_HDR_LEN..];
{
let mut icmp = Icmpv4Packet::new_unchecked(&mut *icmp_buf);
icmp.set_msg_type(Icmpv4Message::DstUnreachable);
icmp.set_msg_code(Icmpv4DstUnreachable::FragRequired.into());
icmp.set_checksum(0);
}
icmp_buf[4..6].copy_from_slice(&0u16.to_be_bytes());
icmp_buf[6..8].copy_from_slice(&ipv4_next_hop_mtu_field(next_hop_mtu).to_be_bytes());
icmp_buf[ICMP_ERROR_HDR_LEN..].copy_from_slice(&original_ip_packet[..quoted_len]);
Icmpv4Packet::new_unchecked(icmp_buf).fill_checksum();
Some(buf)
}
fn construct_icmpv6_packet_too_big(
original_ip_packet: &[u8],
next_hop_mtu: u32,
gateway_mac: EthernetAddress,
guest_mac: EthernetAddress,
) -> Option<Vec<u8>> {
if original_ip_packet.len() < IPV6_HDR_LEN {
return None;
}
let original = Ipv6Packet::new_checked(original_ip_packet).ok()?;
let max_quote_len = IPV6_MIN_MTU - IPV6_HDR_LEN - ICMP_ERROR_HDR_LEN;
let original_total_len = IPV6_HDR_LEN + usize::from(original.payload_len());
let quoted_len = original_ip_packet
.len()
.min(original_total_len)
.min(max_quote_len);
let icmp_len = ICMP_ERROR_HDR_LEN + quoted_len;
let frame_len = ETH_HDR_LEN + IPV6_HDR_LEN + icmp_len;
let mut buf = vec![0u8; frame_len];
let mut eth_frame = EthernetFrame::new_unchecked(&mut buf);
EthernetRepr {
src_addr: gateway_mac,
dst_addr: guest_mac,
ethertype: EthernetProtocol::Ipv6,
}
.emit(&mut eth_frame);
{
let mut ip = Ipv6Packet::new_unchecked(&mut buf[ETH_HDR_LEN..]);
ip.set_version(6);
ip.set_payload_len(icmp_len as u16);
ip.set_next_header(IpProtocol::Icmpv6);
ip.set_hop_limit(64);
ip.set_src_addr(original.dst_addr());
ip.set_dst_addr(original.src_addr());
}
let src_addr = original.dst_addr();
let dst_addr = original.src_addr();
let icmp_buf = &mut buf[ETH_HDR_LEN + IPV6_HDR_LEN..];
{
let mut icmp = Icmpv6Packet::new_unchecked(&mut *icmp_buf);
icmp.set_msg_type(Icmpv6Message::PktTooBig);
icmp.set_msg_code(0);
icmp.set_pkt_too_big_mtu(next_hop_mtu);
icmp.set_checksum(0);
}
icmp_buf[ICMP_ERROR_HDR_LEN..].copy_from_slice(&original_ip_packet[..quoted_len]);
Icmpv6Packet::new_unchecked(icmp_buf).fill_checksum(&src_addr, &dst_addr);
Some(buf)
}
fn ipv4_next_hop_mtu_field(next_hop_mtu: u32) -> u16 {
next_hop_mtu.min(u32::from(u16::MAX)) as u16
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::{Ipv4Addr, Ipv6Addr};
use smoltcp::wire::{
Icmpv4DstUnreachable, Icmpv4Message, Icmpv6Message, IpProtocol, Ipv4Packet, Ipv6Packet,
UdpPacket,
};
fn gateway_mac() -> EthernetAddress {
EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01])
}
fn guest_mac() -> EthernetAddress {
EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02])
}
fn build_ipv4_udp_packet(payload: &[u8]) -> Vec<u8> {
let udp_len = 8 + payload.len();
let ip_total_len = IPV4_HDR_LEN + udp_len;
let mut buf = vec![0u8; ip_total_len];
{
let mut ip = Ipv4Packet::new_unchecked(&mut buf);
ip.set_version(4);
ip.set_header_len(IPV4_HDR_LEN as u8);
ip.set_total_len(ip_total_len as u16);
ip.clear_flags();
ip.set_dont_frag(true);
ip.set_hop_limit(64);
ip.set_next_header(IpProtocol::Udp);
ip.set_src_addr(Ipv4Addr::new(100, 96, 0, 2));
ip.set_dst_addr(Ipv4Addr::new(203, 0, 113, 10));
ip.fill_checksum();
}
let mut udp = UdpPacket::new_unchecked(&mut buf[IPV4_HDR_LEN..]);
udp.set_src_port(12345);
udp.set_dst_port(443);
udp.set_len(udp_len as u16);
udp.set_checksum(0);
udp.payload_mut().copy_from_slice(payload);
buf
}
fn build_ipv6_udp_packet(payload: &[u8]) -> Vec<u8> {
let udp_len = 8 + payload.len();
let mut buf = vec![0u8; IPV6_HDR_LEN + udp_len];
let src = "fd42:6d73:62::2".parse::<Ipv6Addr>().unwrap();
let dst = "2001:db8::10".parse::<Ipv6Addr>().unwrap();
{
let mut ip = Ipv6Packet::new_unchecked(&mut buf);
ip.set_version(6);
ip.set_payload_len(udp_len as u16);
ip.set_next_header(IpProtocol::Udp);
ip.set_hop_limit(64);
ip.set_src_addr(src);
ip.set_dst_addr(dst);
}
let mut udp = UdpPacket::new_unchecked(&mut buf[IPV6_HDR_LEN..]);
udp.set_src_port(12345);
udp.set_dst_port(443);
udp.set_len(udp_len as u16);
udp.payload_mut().copy_from_slice(payload);
udp.fill_checksum(
&smoltcp::wire::IpAddress::from(src),
&smoltcp::wire::IpAddress::from(dst),
);
buf
}
#[test]
fn constructs_ipv4_fragmentation_needed_with_next_hop_mtu() {
let original = build_ipv4_udp_packet(b"hello world");
let frame = construct_packet_too_big(&original, 1280, gateway_mac(), guest_mac()).unwrap();
let eth = EthernetFrame::new_checked(&frame).unwrap();
assert_eq!(eth.ethertype(), EthernetProtocol::Ipv4);
assert_eq!(eth.src_addr(), gateway_mac());
assert_eq!(eth.dst_addr(), guest_mac());
let ip = Ipv4Packet::new_checked(eth.payload()).unwrap();
assert_eq!(ip.src_addr(), Ipv4Addr::new(203, 0, 113, 10));
assert_eq!(ip.dst_addr(), Ipv4Addr::new(100, 96, 0, 2));
assert_eq!(ip.next_header(), IpProtocol::Icmp);
let icmp = Icmpv4Packet::new_checked(ip.payload()).unwrap();
assert_eq!(icmp.msg_type(), Icmpv4Message::DstUnreachable);
assert_eq!(
icmp.msg_code(),
u8::from(Icmpv4DstUnreachable::FragRequired)
);
assert!(icmp.verify_checksum());
assert_eq!(u16::from_be_bytes([ip.payload()[6], ip.payload()[7]]), 1280);
assert_eq!(
&ip.payload()[ICMP_ERROR_HDR_LEN..],
&original[..IPV4_HDR_LEN + 8]
);
}
#[test]
fn constructs_ipv6_packet_too_big_with_mtu() {
let original = build_ipv6_udp_packet(b"hello ipv6");
let frame = construct_packet_too_big(&original, 1280, gateway_mac(), guest_mac()).unwrap();
let eth = EthernetFrame::new_checked(&frame).unwrap();
assert_eq!(eth.ethertype(), EthernetProtocol::Ipv6);
assert_eq!(eth.src_addr(), gateway_mac());
assert_eq!(eth.dst_addr(), guest_mac());
let ip = Ipv6Packet::new_checked(eth.payload()).unwrap();
assert_eq!(ip.src_addr(), "2001:db8::10".parse::<Ipv6Addr>().unwrap());
assert_eq!(
ip.dst_addr(),
"fd42:6d73:62::2".parse::<Ipv6Addr>().unwrap()
);
assert_eq!(ip.next_header(), IpProtocol::Icmpv6);
let icmp = Icmpv6Packet::new_checked(ip.payload()).unwrap();
assert_eq!(icmp.msg_type(), Icmpv6Message::PktTooBig);
assert_eq!(icmp.pkt_too_big_mtu(), 1280);
assert!(icmp.verify_checksum(&ip.src_addr(), &ip.dst_addr()));
assert_eq!(&icmp.payload()[..original.len()], original.as_slice());
}
}