diff --git a/.github/workflows/_rust.yml b/.github/workflows/_rust.yml index 1313d87fe..f75ecf9d8 100644 --- a/.github/workflows/_rust.yml +++ b/.github/workflows/_rust.yml @@ -105,8 +105,6 @@ jobs: rg --count --no-ignore "Packet for DNS resource" $TESTCASES_DIR rg --count --no-ignore "Packet for CIDR resource" $TESTCASES_DIR rg --count --no-ignore "Packet for Internet resource" $TESTCASES_DIR - rg --count --no-ignore "Performed IP-NAT46" $TESTCASES_DIR - rg --count --no-ignore "Performed IP-NAT64" $TESTCASES_DIR rg --count --no-ignore "Truncating DNS response" $TESTCASES_DIR rg --count --no-ignore "Destination is unreachable" $TESTCASES_DIR rg --count --no-ignore "Forwarding query for DNS resource to corresponding site" $TESTCASES_DIR diff --git a/rust/connlib/ip-packet/src/icmp_dest_unreachable.rs b/rust/connlib/ip-packet/src/icmp_dest_unreachable.rs index 04932759a..ce574277c 100644 --- a/rust/connlib/ip-packet/src/icmp_dest_unreachable.rs +++ b/rust/connlib/ip-packet/src/icmp_dest_unreachable.rs @@ -1,9 +1,9 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use anyhow::{Context as _, Result}; +use anyhow::{Context as _, Result, bail}; use etherparse::{Icmpv4Type, Icmpv6Type, LaxIpv4Slice, LaxIpv6Slice, icmpv4, icmpv6}; -use crate::{Layer4Protocol, Protocol, nat46, nat64}; +use crate::{Layer4Protocol, Protocol}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum DestUnreachable { @@ -21,9 +21,12 @@ impl DestUnreachable { pub fn into_icmp_v4_type(self) -> Result { let header = match self { DestUnreachable::V4 { header, .. } => header, - DestUnreachable::V6Unreachable(code) => nat64::translate_dest_unreachable(code) - .with_context(|| format!("Cannot translate {code:?} to ICMPv4"))?, - DestUnreachable::V6PacketTooBig { mtu } => nat64::translate_packet_too_big(mtu), + DestUnreachable::V6Unreachable(_) => { + bail!("Cannot translate IPv6 unreachable to ICMPv4") + } + DestUnreachable::V6PacketTooBig { .. } => { + bail!("Cannot translate IPv6 packet too big to ICMPv4") + } }; Ok(Icmpv4Type::DestinationUnreachable(header)) @@ -31,15 +34,8 @@ impl DestUnreachable { pub fn into_icmp_v6_type(self) -> Result { match self { - DestUnreachable::V4 { - header, - total_length, - } => { - let icmpv6_type = - nat46::translate_icmp_unreachable(header.clone(), total_length) - .with_context(|| format!("Cannot translate {header:?} to ICMPv6"))?; - - Ok(icmpv6_type) + DestUnreachable::V4 { .. } => { + bail!("Cannot translate IPv4 unreachable to ICMPv6") } DestUnreachable::V6Unreachable(code) => Ok(Icmpv6Type::DestinationUnreachable(code)), DestUnreachable::V6PacketTooBig { mtu } => Ok(Icmpv6Type::PacketTooBig { mtu }), @@ -81,15 +77,7 @@ impl FailedPacket { } /// Translates the failed packet to point to the new `destination` address and originating from the given `src_proto`. - /// - /// The new `dst` address may be a different IP version than the original packet in which case NAT64/NAT46 will be applied. - pub fn translate_destination( - mut self, - dst: IpAddr, - src_proto: Protocol, - src_v4: Ipv4Addr, - src_v6: Ipv6Addr, - ) -> Result> { + pub fn translate_destination(self, dst: IpAddr, src_proto: Protocol) -> Result> { match (self.failed_dst, dst) { (IpAddr::V4(_), IpAddr::V4(dst)) => { translate_original_ipv4_packet(self.raw, dst, src_proto) @@ -97,22 +85,8 @@ impl FailedPacket { (IpAddr::V6(_), IpAddr::V6(dst)) => { translate_original_ipv6_packet(self.raw, dst, src_proto) } - (IpAddr::V6(_), IpAddr::V4(dst)) => { - nat64::translate_in_place(&mut self.raw, src_v4, dst) - .context("Failed to translate unrouteable IPv6 packet to IPv4")?; - let packet = self.raw.split_off(20); - - translate_original_ipv4_packet(packet, dst, src_proto) - } - (IpAddr::V4(_), IpAddr::V6(dst)) => { - let mut packet = [vec![0u8; 20], self.raw].concat(); - - let offset = nat46::translate_in_place(&mut packet, src_v6, dst) - .context("Failed to translate unrouteable IPv4 packet to IPv6")?; - let packet = packet.split_off(offset); - - translate_original_ipv6_packet(packet, dst, src_proto) - } + (IpAddr::V6(_), IpAddr::V4(_)) => bail!("Cannot translate from IPv6 to IPv4"), + (IpAddr::V4(_), IpAddr::V6(_)) => bail!("Cannot translate from IPv4 to IPv6"), } } } diff --git a/rust/connlib/ip-packet/src/lib.rs b/rust/connlib/ip-packet/src/lib.rs index 27d3222bf..9c976aead 100644 --- a/rust/connlib/ip-packet/src/lib.rs +++ b/rust/connlib/ip-packet/src/lib.rs @@ -6,8 +6,6 @@ mod fz_p2p_control; mod fz_p2p_control_slice; mod icmp_dest_unreachable; -mod nat46; -mod nat64; #[cfg(feature = "proptest")] #[allow(clippy::unwrap_used)] pub mod proptest; @@ -18,9 +16,6 @@ pub use fz_p2p_control::EventType as FzP2pEventType; pub use fz_p2p_control_slice::FzP2pControlSlice; pub use icmp_dest_unreachable::{DestUnreachable, FailedPacket}; -#[cfg(all(test, feature = "proptest"))] -mod proptests; - use anyhow::{Context as _, Result, bail}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::sync::LazyLock; @@ -49,19 +44,12 @@ pub const MAX_UDP_PAYLOAD: u16 = MAX_IP_PAYLOAD - etherparse::UdpHeader::LEN as /// /// - The TUN device MTU is constrained to 1280 ([`MAX_IP_SIZE`]). /// - WireGuard adds an overhoad of 32 bytes ([`WG_OVERHEAD`]). -/// - In case NAT46 comes into effect, the size may increase by 20 ([`NAT46_OVERHEAD`]). /// - In case the connection is relayed, a 4 byte overhead is added ([`DATA_CHANNEL_OVERHEAD`]). /// -/// There is only a single scenario within which all of these apply at once: -/// A client receiving a relayed IPv6 packet from a Gateway from an IPv4-only DNS resource where the sender (i.e. the resource) maxed out the MTU (1280). -/// In that case, the Gateway needs to translate the packet to IPv6, thus increasing the header size by 20 bytes. /// WireGuard adds its fixed 32-byte overhead and the relayed connections adds its 4 byte overhead. -pub const MAX_FZ_PAYLOAD: usize = - MAX_IP_SIZE + WG_OVERHEAD + NAT46_OVERHEAD + DATA_CHANNEL_OVERHEAD; +pub const MAX_FZ_PAYLOAD: usize = MAX_IP_SIZE + WG_OVERHEAD + DATA_CHANNEL_OVERHEAD; /// Wireguard has a 32-byte overhead (4b message type + 4b receiver idx + 8b packet counter + 16b AEAD tag) pub const WG_OVERHEAD: usize = 32; -/// In order to do NAT46 without copying, we need 20 extra byte in the buffer (IPv6 packets are 20 byte bigger than IPv4). -pub(crate) const NAT46_OVERHEAD: usize = 20; /// TURN's data channels have a 4 byte overhead. pub const DATA_CHANNEL_OVERHEAD: usize = 4; @@ -137,7 +125,7 @@ impl IpPacketBuf { } pub fn buf(&mut self) -> &mut [u8] { - &mut self.inner[NAT46_OVERHEAD..] // We read packets at an offset so we can convert without copying. + &mut self.inner } } @@ -211,17 +199,12 @@ impl std::fmt::Debug for IpPacket { #[derive(Debug, PartialEq, Clone)] pub struct ConvertibleIpv4Packet { buf: Buffer>, - start: usize, len: usize, } impl ConvertibleIpv4Packet { pub fn new(ip: IpPacketBuf, len: usize) -> Result { - let this = Self { - buf: ip.inner, - start: NAT46_OVERHEAD, - len, - }; + let this = Self { buf: ip.inner, len }; Ipv4Slice::from_slice(this.packet()).context("Invalid IPv4 packet")?; Ok(this) @@ -243,65 +226,28 @@ impl ConvertibleIpv4Packet { self.header().destination_addr() } - fn consume_to_ipv6(mut self, src: Ipv6Addr, dst: Ipv6Addr) -> Result { - // `translate_in_place` expects the packet to sit at a 20-byte offset. - // `self.start` tells us where the packet actually starts, thus we need to pass `self.start - 20` to the function. - let start_minus_padding = self - .start - .checked_sub(NAT46_OVERHEAD) - .context("Invalid `start`of IP packet in buffer")?; - - let offset = nat46::translate_in_place( - &mut self.buf[start_minus_padding..(self.start + self.len)], - src, - dst, - ) - .context("NAT46 failed")?; - - // We need to handle 2 cases here: - // `offset` > NAT46_OVERHEAD - // `offset` < NAT46_OVERHEAD - // By casting to an `isize` we can simply compute the _difference_ of the packet length. - // `offset` points into the buffer we passed to `translate_in_place`. - // We passed 20 (NAT46_OVERHEAD) bytes more to that function. - // Thus, 20 - offset tells us the difference in length of the new packet. - let len_diff = (NAT46_OVERHEAD as isize) - (offset as isize); - let len = (self.len as isize) + len_diff; - - Ok(ConvertibleIpv6Packet { - buf: self.buf, - start: start_minus_padding + offset, - len: len as usize, - }) - } - fn header_length(&self) -> usize { (self.header().ihl() * 4) as usize } pub fn packet(&self) -> &[u8] { - &self.buf[self.start..(self.start + self.len)] + &self.buf[..self.len] } fn packet_mut(&mut self) -> &mut [u8] { - &mut self.buf[self.start..(self.start + self.len)] + &mut self.buf[..self.len] } } #[derive(Debug, PartialEq, Clone)] pub struct ConvertibleIpv6Packet { buf: Buffer>, - start: usize, len: usize, } impl ConvertibleIpv6Packet { pub fn new(ip: IpPacketBuf, len: usize) -> Result { - let this = Self { - buf: ip.inner, - start: NAT46_OVERHEAD, - len, - }; + let this = Self { buf: ip.inner, len }; Ipv6Slice::from_slice(this.packet()).context("Invalid IPv6 packet")?; @@ -325,22 +271,12 @@ impl ConvertibleIpv6Packet { self.header().destination_addr() } - fn consume_to_ipv4(mut self, src: Ipv4Addr, dst: Ipv4Addr) -> Result { - nat64::translate_in_place(self.packet_mut(), src, dst).context("NAT64 failed")?; - - Ok(ConvertibleIpv4Packet { - buf: self.buf, - start: self.start + 20, - len: self.len - 20, - }) - } - pub fn packet(&self) -> &[u8] { - &self.buf[self.start..(self.start + self.len)] + &self.buf[..self.len] } fn packet_mut(&mut self) -> &mut [u8] { - &mut self.buf[self.start..(self.start + self.len)] + &mut self.buf[..self.len] } } @@ -380,27 +316,13 @@ impl IpPacket { pub fn new(buf: IpPacketBuf, len: usize) -> Result { anyhow::ensure!(len <= MAX_IP_SIZE, "Packet too large (len: {len})"); - Ok(match buf.inner[NAT46_OVERHEAD] >> 4 { + Ok(match buf.inner[0] >> 4 { 4 => IpPacket::Ipv4(ConvertibleIpv4Packet::new(buf, len)?), 6 => IpPacket::Ipv6(ConvertibleIpv6Packet::new(buf, len)?), v => bail!("Invalid IP version: {v}"), }) } - pub(crate) fn consume_to_ipv4(self, src: Ipv4Addr, dst: Ipv4Addr) -> Result { - match self { - IpPacket::Ipv4(pkt) => Ok(IpPacket::Ipv4(pkt)), - IpPacket::Ipv6(pkt) => Ok(IpPacket::Ipv4(pkt.consume_to_ipv4(src, dst)?)), - } - } - - pub(crate) fn consume_to_ipv6(self, src: Ipv6Addr, dst: Ipv6Addr) -> Result { - match self { - IpPacket::Ipv4(pkt) => Ok(IpPacket::Ipv6(pkt.consume_to_ipv6(src, dst)?)), - IpPacket::Ipv6(pkt) => Ok(IpPacket::Ipv6(pkt)), - } - } - pub fn source(&self) -> IpAddr { for_both!(self, |i| i.get_source().into()) } @@ -809,48 +731,22 @@ impl IpPacket { Some(header) } - pub fn translate_destination( - mut self, - src_v4: Ipv4Addr, - src_v6: Ipv6Addr, - src_proto: Protocol, - dst: IpAddr, - ) -> Result { - let mut packet = match (&self, dst) { - (&IpPacket::Ipv4(_), IpAddr::V6(dst)) => self.consume_to_ipv6(src_v6, dst)?, - (&IpPacket::Ipv6(_), IpAddr::V4(dst)) => self.consume_to_ipv4(src_v4, dst)?, - _ => { - self.set_dst(dst); - self - } - }; - packet.set_source_protocol(src_proto.value()); + pub fn translate_destination(mut self, src_proto: Protocol, dst: IpAddr) -> Result { + self.set_dst(dst)?; + self.set_source_protocol(src_proto.value()); - Ok(packet) + Ok(self) } - pub fn translate_source( - mut self, - dst_v4: Ipv4Addr, - dst_v6: Ipv6Addr, - dst_proto: Protocol, - src: IpAddr, - ) -> Result { - let mut packet = match (&self, src) { - (&IpPacket::Ipv4(_), IpAddr::V6(src)) => self.consume_to_ipv6(src, dst_v6)?, - (&IpPacket::Ipv6(_), IpAddr::V4(src)) => self.consume_to_ipv4(src, dst_v4)?, - _ => { - self.set_src(src); - self - } - }; - packet.set_destination_protocol(dst_proto.value()); + pub fn translate_source(mut self, dst_proto: Protocol, src: IpAddr) -> Result { + self.set_src(src)?; + self.set_destination_protocol(dst_proto.value()); - Ok(packet) + Ok(self) } #[inline] - pub fn set_dst(&mut self, dst: IpAddr) { + pub fn set_dst(&mut self, dst: IpAddr) -> Result<()> { match (self, dst) { (Self::Ipv4(p), IpAddr::V4(d)) => { p.header_mut().set_destination(d.octets()); @@ -859,16 +755,18 @@ impl IpPacket { p.header_mut().set_destination(d.octets()); } (Self::Ipv4(_), IpAddr::V6(_)) => { - debug_assert!(false, "Cannot set an IPv6 address on an IPv4 packet") + bail!("Cannot set an IPv6 address on an IPv4 packet") } (Self::Ipv6(_), IpAddr::V4(_)) => { - debug_assert!(false, "Cannot set an IPv4 address on an IPv6 packet") + bail!("Cannot set an IPv4 address on an IPv6 packet") } } + + Ok(()) } #[inline] - pub fn set_src(&mut self, src: IpAddr) { + pub fn set_src(&mut self, src: IpAddr) -> Result<()> { match (self, src) { (Self::Ipv4(p), IpAddr::V4(s)) => { p.header_mut().set_source(s.octets()); @@ -877,12 +775,14 @@ impl IpPacket { p.header_mut().set_source(s.octets()); } (Self::Ipv4(_), IpAddr::V6(_)) => { - debug_assert!(false, "Cannot set an IPv6 address on an IPv4 packet") + bail!("Cannot set an IPv6 address on an IPv4 packet") } (Self::Ipv6(_), IpAddr::V4(_)) => { - debug_assert!(false, "Cannot set an IPv4 address on an IPv6 packet") + bail!("Cannot set an IPv4 address on an IPv6 packet") } } + + Ok(()) } /// Updates the ECN flags of this packet with the ECN value from the transport layer. @@ -1036,25 +936,23 @@ impl IpPacket { } fn extract_l4_proto(payload: &[u8], protocol: IpNumber) -> Result { + // ICMP messages SHOULD always contain at least 8 bytes of the original L4 payload. + let (src_port, remaining) = payload + .split_first_chunk::<2>() + .context("Payload is not long enough for src port")?; + let (dst_port, _) = remaining + .split_first_chunk::<2>() + .context("Payload is not long enough for dst port")?; + let proto = match protocol { - IpNumber::UDP => { - let udp = - UdpHeaderSlice::from_slice(payload).context("Failed to parse payload as UDP")?; - - Layer4Protocol::Udp { - src: udp.source_port(), - dst: udp.destination_port(), - } - } - IpNumber::TCP => { - let tcp = - TcpHeaderSlice::from_slice(payload).context("Failed to parse payload as TCP")?; - - Layer4Protocol::Tcp { - src: tcp.source_port(), - dst: tcp.destination_port(), - } - } + IpNumber::UDP => Layer4Protocol::Udp { + src: u16::from_be_bytes(*src_port), + dst: u16::from_be_bytes(*dst_port), + }, + IpNumber::TCP => Layer4Protocol::Tcp { + src: u16::from_be_bytes(*src_port), + dst: u16::from_be_bytes(*dst_port), + }, IpNumber::ICMP => { let icmp_type = Icmpv4Slice::from_slice(payload) .context("Failed to parse payload as ICMPv4")? diff --git a/rust/connlib/ip-packet/src/make.rs b/rust/connlib/ip-packet/src/make.rs index 54af34ccf..4be25cfdd 100644 --- a/rust/connlib/ip-packet/src/make.rs +++ b/rust/connlib/ip-packet/src/make.rs @@ -156,16 +156,23 @@ where } } -pub fn icmp_dst_unreachable( - ipv4_src: Ipv4Addr, - ipv6_src: Ipv6Addr, - original_packet: &IpPacket, -) -> Result { +pub fn icmp_dst_unreachable(original_packet: &IpPacket) -> Result { let src = original_packet.source(); + let dst = original_packet.destination(); - let icmp_error = match src { - IpAddr::V4(src) => icmpv4_network_unreachable(ipv4_src, src, original_packet)?, - IpAddr::V6(src) => icmpv6_address_unreachable(ipv6_src, src, original_packet)?, + let icmp_error = match (src, dst) { + (IpAddr::V4(src), IpAddr::V4(dst)) => { + icmpv4_network_unreachable(dst, src, original_packet)? + } + (IpAddr::V6(src), IpAddr::V6(dst)) => { + icmpv6_address_unreachable(dst, src, original_packet)? + } + (IpAddr::V4(_), IpAddr::V6(_)) => { + bail!("Invalid IP packet: Inconsistent IP address versions") + } + (IpAddr::V6(_), IpAddr::V4(_)) => { + bail!("Invalid IP packet: Inconsistent IP address versions") + } }; Ok(icmp_error) @@ -235,14 +242,22 @@ mod tests { fn ipv4_icmp_unreachable( #[strategy(payload(MAX_IP_SIZE - Ipv4Header::MIN_LEN - UdpHeader::LEN))] payload: Vec, ) { - let unreachable_packet = - udp_packet(Ipv4Addr::LOCALHOST, Ipv4Addr::LOCALHOST, 0, 0, payload).unwrap(); + let unreachable_packet = udp_packet( + Ipv4Addr::new(10, 0, 0, 1), + Ipv4Addr::LOCALHOST, + 0, + 0, + payload, + ) + .unwrap(); - let icmp_error = - icmp_dst_unreachable(ERROR_SRC_IPV4, ERROR_SRC_IPV6, &unreachable_packet).unwrap(); + let icmp_error = icmp_dst_unreachable(&unreachable_packet).unwrap(); - assert_eq!(icmp_error.destination(), IpAddr::V4(Ipv4Addr::LOCALHOST)); - assert_eq!(icmp_error.source(), IpAddr::V4(ERROR_SRC_IPV4)); + assert_eq!( + icmp_error.destination(), + IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)) + ); + assert_eq!(icmp_error.source(), IpAddr::V4(Ipv4Addr::LOCALHOST)); assert!(matches!( icmp_error.icmp_unreachable_destination(), Ok(Some(_)) @@ -253,23 +268,28 @@ mod tests { fn ipv6_icmp_unreachable_max_payload( #[strategy(payload(MAX_IP_SIZE - Ipv6Header::LEN - UdpHeader::LEN))] payload: Vec, ) { - let unreachable_packet = - udp_packet(Ipv6Addr::LOCALHOST, Ipv6Addr::LOCALHOST, 0, 0, payload).unwrap(); + let unreachable_packet = udp_packet( + Ipv6Addr::new(1, 0, 0, 0, 0, 0, 0, 1), + Ipv6Addr::LOCALHOST, + 0, + 0, + payload, + ) + .unwrap(); - let icmp_error = - icmp_dst_unreachable(ERROR_SRC_IPV4, ERROR_SRC_IPV6, &unreachable_packet).unwrap(); + let icmp_error = icmp_dst_unreachable(&unreachable_packet).unwrap(); - assert_eq!(icmp_error.destination(), IpAddr::V6(Ipv6Addr::LOCALHOST)); - assert_eq!(icmp_error.source(), IpAddr::V6(ERROR_SRC_IPV6)); + assert_eq!( + icmp_error.destination(), + IpAddr::V6(Ipv6Addr::new(1, 0, 0, 0, 0, 0, 0, 1)) + ); + assert_eq!(icmp_error.source(), IpAddr::V6(Ipv6Addr::LOCALHOST)); assert!(matches!( icmp_error.icmp_unreachable_destination(), Ok(Some(_)) )); } - const ERROR_SRC_IPV4: Ipv4Addr = Ipv4Addr::new(1, 1, 1, 1); - const ERROR_SRC_IPV6: Ipv6Addr = Ipv6Addr::new(1, 1, 1, 1, 1, 1, 1, 1); - fn payload(max_size: usize) -> impl Strategy> { collection::vec(any::(), 0..=max_size) } diff --git a/rust/connlib/ip-packet/src/nat46.rs b/rust/connlib/ip-packet/src/nat46.rs deleted file mode 100644 index 8c46acbbe..000000000 --- a/rust/connlib/ip-packet/src/nat46.rs +++ /dev/null @@ -1,296 +0,0 @@ -use anyhow::Result; -use etherparse::{ - Icmpv4Header, Icmpv4Type, Icmpv6Header, Icmpv6Type, IpNumber, Ipv6FlowLabel, Ipv6Header, - icmpv4, - icmpv6::{self, ParameterProblemHeader}, -}; -use std::{io::Cursor, net::Ipv6Addr}; - -use crate::{ImpossibleTranslation, NAT46_OVERHEAD}; - -/// Performs IPv4 -> IPv6 NAT on the packet in `buf` to the given src & dst IP. -/// -/// An IPv6 IP-header may be up to 20 bytes bigger than its corresponding IPv4 counterpart. -/// Thus, the IPv4 packet is expected to sit at an offset of [`NAT46_OVERHEAD`] bytes in `buf`. -/// -/// # Returns -/// -/// - Ok(offset): The offset within `buf` at which the new IPv6 packet starts. -pub fn translate_in_place(buf: &mut [u8], src: Ipv6Addr, dst: Ipv6Addr) -> Result { - let ipv4_packet = &buf[NAT46_OVERHEAD..]; - - let (headers, payload) = etherparse::IpHeaders::from_ipv4_slice(ipv4_packet)?; - let (ipv4_header, _extensions) = headers.ipv4().expect("We successfully parsed as IPv4"); - - let total_length = ipv4_header.total_len; - let header_length = ipv4_header.header_len(); - let start_of_ip_payload = 20 + header_length; - - // TODO: - /* - If the DF flag is not set and the IPv4 packet will result in an IPv6 - packet larger than 1280 bytes, the packet SHOULD be fragmented so the - resulting IPv6 packet (with Fragment Header added to each fragment) - will be less than or equal to 1280 bytes. For example, if the packet - - is fragmented prior to the translation, the IPv4 packets should be - fragmented so that their length, excluding the IPv4 header, is at - most 1232 bytes (1280 minus 40 for the IPv6 header and 8 for the - Fragment Header). The translator MAY provide a configuration - function for the network administrator to adjust the threshold of the - minimum IPv6 MTU to a value greater than 1280-byte if the real value - of the minimum IPv6 MTU in the network is known to the administrator. - The resulting fragments are then translated independently using the - logic described below. - - If the DF bit is set and the MTU of the next-hop interface is less - than the total length value of the IPv4 packet plus 20, the - translator MUST send an ICMPv4 "Fragmentation Needed" error message - to the IPv4 source address. - - If the DF bit is set and the packet is not a fragment (i.e., the More - Fragments (MF) flag is not set and the Fragment Offset is equal to - zero), then the translator SHOULD NOT add a Fragment Header to the - resulting packet. - */ - // Note the RFC has notes on how to set fragmentation fields. - - let ipv6_header = Ipv6Header { - // Traffic Class: By default, copied from the IP Type Of Service (TOS) - // octet. According to [RFC2474], the semantics of the bits are - // identical in IPv4 and IPv6. However, in some IPv4 environments - // these fields might be used with the old semantics of "Type Of - // Service and Precedence". An implementation of a translator SHOULD - // support an administratively configurable option to ignore the IPv4 - // TOS and always set the IPv6 traffic class (TC) to zero. In - // addition, if the translator is at an administrative boundary, the - // filtering and update considerations of [RFC2475] may be - // applicable. - // Note: DSCP is the new name for TOS - traffic_class: ipv4_header.dscp.value(), - - // Flow Label: 0 (all zero bits) - flow_label: Ipv6FlowLabel::ZERO, - - // Payload Length: Total length value from the IPv4 header, minus the - // size of the IPv4 header and IPv4 options, if present. - payload_length: total_length - (header_length as u16), - - // Next Header: For ICMPv4 (1), it is changed to ICMPv6 (58); - // otherwise, the protocol field MUST be copied from the IPv4 header. - next_header: match ipv4_header.protocol { - IpNumber::ICMP => IpNumber::IPV6_ICMP, - other => other, - }, - - // Hop Limit: The hop limit is derived from the TTL value in the IPv4 - // header. Since the translator is a router, as part of forwarding - // the packet it needs to decrement either the IPv4 TTL (before the - // translation) or the IPv6 Hop Limit (after the translation). As - // part of decrementing the TTL or Hop Limit, the translator (as any - // router) MUST check for zero and send the ICMPv4 "TTL Exceeded" or - // ICMPv6 "Hop Limit Exceeded" error. - // TODO: do we really need to act as a router? - // reducing the ttl and having to send back a message makes things much harder - hop_limit: ipv4_header.time_to_live, - - // Source Address: The IPv4-converted address derived from the IPv4 - // source address per [RFC6052], Section 2.3. - // Note: Rust implements RFC4291 with to_ipv6_mapped but buf RFC6145 - // recommends the above. The advantage of using RFC6052 are explained in - // section 4.2 of that RFC - - // If the translator gets an illegal source address (e.g., 0.0.0.0, - // 127.0.0.1, etc.), the translator SHOULD silently drop the packet - // (as discussed in Section 5.3.7 of [RFC1812]). - // TODO: drop illegal source address? I don't think we need to do it - source: src.octets(), - - // Destination Address: In the stateless mode, which is to say that if - // the IPv4 destination address is within a range of configured IPv4 - // stateless translation prefix, the IPv6 destination address is the - // IPv4-translatable address derived from the IPv4 destination - // address per [RFC6052], Section 2.3. A workflow example of - // stateless translation is shown in Appendix A of this document. - - // In the stateful mode (which is to say that if the IPv4 destination - // address is not within the range of any configured IPv4 stateless - // translation prefix), the IPv6 destination address and - // corresponding transport-layer destination port are derived from - // the Binding Information Bases (BIBs) reflecting current session - // state in the translator as described in [RFC6146]. - destination: dst.octets(), - }; - - tracing::trace!(from = ?ipv4_header, to = ?ipv6_header, "Performed IP-NAT46"); - - if ipv4_header.protocol == IpNumber::ICMP { - let (icmpv4_header, _icmp_payload) = Icmpv4Header::from_slice(payload.payload)?; - let icmpv4_header_length = icmpv4_header.header_len(); - - // Optimisation to only clone when we are actually logging. - let icmpv4_header_dbg = tracing::event_enabled!(tracing::Level::TRACE) - .then(|| tracing::field::debug(icmpv4_header.clone())); - - let icmpv6_header = - translate_icmpv4_header(total_length, icmpv4_header).ok_or(ImpossibleTranslation)?; - let icmpv6_header_length = icmpv6_header.header_len(); - - tracing::trace!(from = icmpv4_header_dbg, to = ?icmpv6_header, "Performed ICMP-NAT46"); - - // We assume that the sizeof the ICMP header does not change and the payload will be in the correct spot. - debug_assert_eq!( - icmpv4_header_length, icmpv6_header_length, - "Length of ICMPv6 header should be equal to length of ICMPv4 header" - ); - - let (_ip_header, ip_payload) = buf.split_at_mut(start_of_ip_payload); - - icmpv6_header.write(&mut Cursor::new(ip_payload))?; - }; - - let start_of_ipv6_header = start_of_ip_payload - Ipv6Header::LEN; - - let (_, ipv6_header_buf) = buf.split_at_mut(start_of_ipv6_header); - ipv6_header.write(&mut Cursor::new(ipv6_header_buf))?; - - Ok(start_of_ipv6_header) -} - -fn translate_icmpv4_header( - total_length: u16, - icmpv4_header: etherparse::Icmpv4Header, -) -> Option { - // Note: we only really need to support reply/request because we need - // the identification to do nat anyways as source port. - // So the rest of the implementation is not fully made. - // Specially some consideration has to be made for ICMP error payload - // so we will do it only if needed at a later time - - // ICMPv4 query messages: - let icmpv6_type = match icmpv4_header.icmp_type { - // Echo and Echo Reply (Type 8 and Type 0): Adjust the Type values - // to 128 and 129, respectively, and adjust the ICMP checksum both - // to take the type change into account and to include the ICMPv6 - // pseudo-header. - Icmpv4Type::EchoRequest(header) => Icmpv6Type::EchoRequest(header), - Icmpv4Type::EchoReply(header) => Icmpv6Type::EchoReply(header), - - // Time Exceeded (Type 11): Set the Type to 3, and adjust the - // ICMP checksum both to take the type change into account and - // to include the ICMPv6 pseudo-header. The Code is unchanged. - Icmpv4Type::TimeExceeded(i) => { - Icmpv6Type::TimeExceeded(icmpv6::TimeExceededCode::from_u8(i.code_u8())?) - } - - // Destination Unreachable (Type 3): Translate the Code as - // described below, set the Type to 1, and adjust the ICMP - // checksum both to take the type/code change into account and - // to include the ICMPv6 pseudo-header. - Icmpv4Type::DestinationUnreachable(i) => translate_icmp_unreachable(i, total_length)?, - Icmpv4Type::Redirect(_) => return None, - Icmpv4Type::ParameterProblem(_) => return None, - - // Timestamp and Timestamp Reply (Type 13 and Type 14): Obsoleted in - // ICMPv6. Silently drop. - Icmpv4Type::TimestampRequest(_) | Icmpv4Type::TimestampReply(_) => return None, - - // Unknown ICMPv4 types: Silently drop. - // IGMP messages: While the Multicast Listener Discovery (MLD) - // messages [RFC2710] [RFC3590] [RFC3810] are the logical IPv6 - // counterparts for the IPv4 IGMP messages, all the "normal" IGMP - // messages are single-hop messages and SHOULD be silently dropped - // by the translator. Other IGMP messages might be used by - // multicast routing protocols and, since it would be a - // configuration error to try to have router adjacencies across - // IP/ICMP translators, those packets SHOULD also be silently - // dropped. - Icmpv4Type::Unknown { .. } => return None, - }; - - Some(Icmpv6Header::new(icmpv6_type)) -} - -pub fn translate_icmp_unreachable( - header: icmpv4::DestUnreachableHeader, - total_length: u16, -) -> Option { - use icmpv4::DestUnreachableHeader::*; - use icmpv6::DestUnreachableCode::*; - - Some(match header { - // Code 0, 1 (Net Unreachable, Host Unreachable): Set the Code - // to 0 (No route to destination). - Network | Host => Icmpv6Type::DestinationUnreachable(NoRoute), - - // Code 2 (Protocol Unreachable): Translate to an ICMPv6 - // Parameter Problem (Type 4, Code 1) and make the Pointer - // point to the IPv6 Next Header field. - Protocol => Icmpv6Type::ParameterProblem(ParameterProblemHeader { - code: icmpv6::ParameterProblemCode::UnrecognizedNextHeader, - pointer: 6, // The "Next Header" field is always at a fixed offset. - }), - // Code 3 (Port Unreachable): Set the Code to 4 (Port - // unreachable). - icmpv4::DestUnreachableHeader::Port => { - Icmpv6Type::DestinationUnreachable(icmpv6::DestUnreachableCode::Port) - } - // Code 4 (Fragmentation Needed and DF was Set): Translate to - // an ICMPv6 Packet Too Big message (Type 2) with Code set - // to 0. The MTU field MUST be adjusted for the difference - // between the IPv4 and IPv6 header sizes, i.e., - // minimum(advertised MTU+20, MTU_of_IPv6_nexthop, - // (MTU_of_IPv4_nexthop)+20). Note that if the IPv4 router - // set the MTU field to zero, i.e., the router does not - // implement [RFC1191], then the translator MUST use the - // plateau values specified in [RFC1191] to determine a - // likely path MTU and include that path MTU in the ICMPv6 - // packet. (Use the greatest plateau value that is less - // than the returned Total Length field.) - - // See also the requirements in Section 6. - FragmentationNeeded { next_hop_mtu: 0 } => { - const PLATEAU_VALUES: [u16; 10] = - [68, 296, 508, 1006, 1492, 2002, 4352, 8166, 32000, 65535]; - - let mtu = PLATEAU_VALUES - .into_iter() - .filter(|mtu| *mtu < total_length) - .max()?; - - Icmpv6Type::PacketTooBig { mtu: mtu as u32 } - } - FragmentationNeeded { next_hop_mtu } => Icmpv6Type::PacketTooBig { - mtu: next_hop_mtu as u32 + 20, - }, - - // Code 5 (Source Route Failed): Set the Code to 0 (No route - // to destination). Note that this error is unlikely since - // source routes are not translated. - SourceRouteFailed => Icmpv6Type::DestinationUnreachable(NoRoute), - // Code 6, 7, 8: Set the Code to 0 (No route to destination). - NetworkUnknown | HostUnknown | Isolated => Icmpv6Type::DestinationUnreachable(NoRoute), - - // Code 9, 10 (Communication with Destination Host - // Administratively Prohibited): Set the Code to 1 - // (Communication with destination administratively - // prohibited). - NetworkProhibited | HostProhibited => Icmpv6Type::DestinationUnreachable(Prohibited), - - // Code 11, 12: Set the Code to 0 (No route to destination). - TosNetwork | TosHost => Icmpv6Type::DestinationUnreachable(NoRoute), - - // Code 13 (Communication Administratively Prohibited): Set - // the Code to 1 (Communication with destination - // administratively prohibited). - FilterProhibited => Icmpv6Type::DestinationUnreachable(Prohibited), - - // Code 14 (Host Precedence Violation): Silently drop. - HostPrecedenceViolation => return None, - - // Code 15 (Precedence cutoff in effect): Set the Code to 1 - // (Communication with destination administratively - // prohibited). - PrecedenceCutoff => Icmpv6Type::DestinationUnreachable(Prohibited), - }) -} diff --git a/rust/connlib/ip-packet/src/nat64.rs b/rust/connlib/ip-packet/src/nat64.rs deleted file mode 100644 index 25d3f1df2..000000000 --- a/rust/connlib/ip-packet/src/nat64.rs +++ /dev/null @@ -1,334 +0,0 @@ -use anyhow::Result; -use etherparse::{ - Icmpv6Header, IpFragOffset, IpNumber, Ipv4Dscp, Ipv4Ecn, Ipv4Header, Ipv4Options, Ipv6Header, -}; -use std::{io::Cursor, net::Ipv4Addr}; - -use crate::ImpossibleTranslation; - -/// Performs IPv6 -> IPv4 NAT on the packet in `buf` to the given src & dst IP. -/// -/// IPv6 headers have a fixed size of 40 bytes. -/// IPv4 options are lost as part of NAT64, meaning the translated packet will always be 20 bytes shorter. -/// Thus, the IPv4 packet will always sit at an offset of 20 bytes in `buf` after the translation. -pub fn translate_in_place(buf: &mut [u8], src: Ipv4Addr, dst: Ipv4Addr) -> Result<()> { - let (headers, payload) = etherparse::IpHeaders::from_ipv6_slice(buf)?; - let (ipv6_header, _extensions) = headers.ipv6().expect("We successfully parsed as IPv6"); - - // TODO: - // If there is no IPv6 Fragment Header, the IPv4 header fields are set - // as follows: - // Note the RFC has notes on how to set fragmentation fields. - - let mut ipv4_header = Ipv4Header { - // Internet Header Length: 5 (no IPv4 options) - options: Ipv4Options::default(), - - // Type of Service (TOS) Octet: By default, copied from the IPv6 - // Traffic Class (all 8 bits). According to [RFC2474], the semantics - // of the bits are identical in IPv4 and IPv6. However, in some IPv4 - // environments, these bits might be used with the old semantics of - // "Type Of Service and Precedence". An implementation of a - // translator SHOULD provide the ability to ignore the IPv6 traffic - // class and always set the IPv4 TOS Octet to a specified value. In - // addition, if the translator is at an administrative boundary, the - // filtering and update considerations of [RFC2475] may be - // applicable. - dscp: Ipv4Dscp::try_new(ipv6_header.traffic_class).unwrap_or(Ipv4Dscp::ZERO), - - // Total Length: Payload length value from the IPv6 header, plus the - // size of the IPv4 header. - total_len: ipv6_header.payload_length + Ipv4Header::MIN_LEN_U16, - - // Identification: All zero. In order to avoid black holes caused by - // ICMPv4 filtering or non-[RFC2460]-compatible IPv6 hosts (a - // workaround is discussed in Section 6), the translator MAY provide - // a function to generate the identification value if the packet size - // is greater than 88 bytes and less than or equal to 1280 bytes. - // The translator SHOULD provide a method for operators to enable or - // disable this function. - identification: 0, - - // Flags: The More Fragments flag is set to zero. The Don't Fragment - // (DF) flag is set to one. In order to avoid black holes caused by - // ICMPv4 filtering or non-[RFC2460]-compatible IPv6 hosts (a - // workaround is discussed in Section 6), the translator MAY provide - // a function as follows. If the packet size is greater than 88 - // bytes and less than or equal to 1280 bytes, it sets the DF flag to - // zero; otherwise, it sets the DF flag to one. The translator - // SHOULD provide a method for operators to enable or disable this - // function. - more_fragments: false, - dont_fragment: true, - - // Fragment Offset: All zeros. - fragment_offset: IpFragOffset::ZERO, - - ecn: Ipv4Ecn::default(), - - // Time to Live: Time to Live is derived from Hop Limit value in IPv6 - // header. Since the translator is a router, as part of forwarding - // the packet it needs to decrement either the IPv6 Hop Limit (before - // the translation) or the IPv4 TTL (after the translation). As part - // of decrementing the TTL or Hop Limit the translator (as any - // router) MUST check for zero and send the ICMPv4 "TTL Exceeded" or - // ICMPv6 "Hop Limit Exceeded" error. - // Same note as for the other translation - time_to_live: ipv6_header.hop_limit, - - // Protocol: The IPv6-Frag (44) header is handled as discussed in - // Section 5.1.1. ICMPv6 (58) is changed to ICMPv4 (1), and the - // payload is translated as discussed in Section 5.2. The IPv6 - // headers HOPOPT (0), IPv6-Route (43), and IPv6-Opts (60) are - // skipped over during processing as they have no meaning in IPv4. - // For the first 'next header' that does not match one of the cases - // above, its Next Header value (which contains the transport - // protocol number) is copied to the protocol field in the IPv4 - // header. This means that all transport protocols are translated. - // Note: Some translated protocols will fail at the receiver for - // various reasons: some are known to fail when translated (e.g., - // IPsec Authentication Header (51)), and others will fail - // checksum validation if the address translation is not checksum - // neutral [RFC6052] and the translator does not update the - // transport protocol's checksum (because the translator doesn't - // support recalculating the checksum for that transport protocol; - // see Section 5.5). - - // Note: this seems to suggest there can be more than 1 next level protocol? - // maybe I'm misreading this. - // FIXME: We should take into account the `Ipv6Extensions` from above. - protocol: match ipv6_header.next_header { - IpNumber::IPV6_FRAGMENTATION_HEADER // TODO: Implement fragmentation? - | IpNumber::IPV6_HEADER_HOP_BY_HOP - | IpNumber::IPV6_ROUTE_HEADER - | IpNumber::IPV6_DESTINATION_OPTIONS => { - anyhow::bail!("Unable to translate IPv6 next header protocol: {:?}", ipv6_header.next_header.protocol_str()); - }, - IpNumber::IPV6_ICMP => IpNumber::ICMP, - other => other - }, - - // Header Checksum: Computed once the IPv4 header has been created. - header_checksum: 0, - - // Source Address: In the stateless mode (which is to say that if the - // IPv6 source address is within the range of a configured IPv6 - // translation prefix), the IPv4 source address is derived from the - // IPv6 source address per [RFC6052], Section 2.3. Note that the - // original IPv6 source address is an IPv4-translatable address. A - // workflow example of stateless translation is shown in Appendix A - // of this document. If the translator only supports stateless mode - // and if the IPv6 source address is not within the range of - // configured IPv6 prefix(es), the translator SHOULD drop the packet - // and respond with an ICMPv6 "Destination Unreachable, Source - // address failed ingress/egress policy" (Type 1, Code 5). - - // In the stateful mode, which is to say that if the IPv6 source - // address is not within the range of any configured IPv6 stateless - // translation prefix, the IPv4 source address and transport-layer - // source port corresponding to the IPv4-related IPv6 source address - // and source port are derived from the Binding Information Bases - // (BIBs) as described in [RFC6146]. - - // In stateless and stateful modes, if the translator gets an illegal - // source address (e.g., ::1, etc.), the translator SHOULD silently - // drop the packet. - source: src.octets(), - - // Destination Address: The IPv4 destination address is derived from - // the IPv6 destination address of the datagram being translated per - // [RFC6052], Section 2.3. Note that the original IPv6 destination - // address is an IPv4-converted address. - destination: dst.octets(), - }; - - tracing::trace!(from = ?ipv6_header, to = ?ipv4_header, "Performed IP-NAT64"); - - if ipv6_header.next_header == IpNumber::IPV6_ICMP { - let (icmpv6_header, _icmp_payload) = Icmpv6Header::from_slice(payload.payload)?; - let icmpv6_header_length = icmpv6_header.header_len(); - - // Optimisation to only clone when we are actually logging. - let icmpv6_header_dbg = tracing::event_enabled!(tracing::Level::TRACE) - .then(|| tracing::field::debug(icmpv6_header.clone())); - - let icmpv4_header = translate_icmpv6_header(icmpv6_header).ok_or(ImpossibleTranslation)?; - let icmpv4_header_length = icmpv4_header.header_len(); - - tracing::trace!(from = icmpv6_header_dbg, to = ?icmpv4_header, "Performed ICMP-NAT64"); - - // We assume that the sizeof the ICMP header does not change and the payload will be in the correct spot. - debug_assert_eq!( - icmpv4_header_length, icmpv6_header_length, - "Length of ICMPv4 header should be equal to length of ICMPv6 header" - ); - - let (_ip_header, ip_payload) = buf.split_at_mut(Ipv6Header::LEN); - - icmpv4_header.write(&mut Cursor::new(ip_payload))?; - } - - // TODO?: If a Routing header with a non-zero Segments Left field is present, - // then the packet MUST NOT be translated, and an ICMPv6 "parameter - // problem/erroneous header field encountered" (Type 4, Code 0) error - // message, with the Pointer field indicating the first byte of the - // Segments Left field, SHOULD be returned to the sender. - - ipv4_header.header_checksum = ipv4_header.calc_header_checksum(); - - debug_assert_eq!( - ipv4_header.header_len(), - Ipv4Header::MIN_LEN, - "Translated IPv4 header should be minimum length" - ); - - buf[..40].fill(0); - let ipv4_header_buf = &mut buf[20..]; - ipv4_header.write(&mut Cursor::new(ipv4_header_buf))?; - - Ok(()) -} - -fn translate_icmpv6_header( - icmpv6_header: etherparse::Icmpv6Header, -) -> Option { - use etherparse::{Icmpv4Header, Icmpv4Type, Icmpv6Type, icmpv4}; - - // Note: we only really need to support reply/request because we need - // the identification to do nat anyways as source port. - // So the rest of the implementation is not fully made. - // Specially some consideration has to be made for ICMP error payload - // so we will do it only if needed at a later time - - // ICMPv6 informational messages: - - let icmpv4_type = match icmpv6_header.icmp_type { - // Echo Request and Echo Reply (Type 128 and 129): Adjust the Type - // values to 8 and 0, respectively, and adjust the ICMP checksum - // both to take the type change into account and to exclude the - // ICMPv6 pseudo-header. - Icmpv6Type::EchoRequest(header) => Icmpv4Type::EchoRequest(header), - Icmpv6Type::EchoReply(header) => Icmpv4Type::EchoReply(header), - - // Destination Unreachable (Type 1) Set the Type to 3, and adjust - // the ICMP checksum both to take the type/code change into - // account and to exclude the ICMPv6 pseudo-header. - // - // Translate the Code as follows: - Icmpv6Type::DestinationUnreachable(i) => { - Icmpv4Type::DestinationUnreachable(translate_dest_unreachable(i)?) - } - Icmpv6Type::PacketTooBig { mtu } => { - Icmpv4Type::DestinationUnreachable(translate_packet_too_big(mtu)) - } - // Time Exceeded (Type 3): Set the Type to 11, and adjust the ICMPv4 - // checksum both to take the type change into account and to - // exclude the ICMPv6 pseudo-header. The Code is unchanged. - Icmpv6Type::TimeExceeded(code) => { - Icmpv4Type::TimeExceeded(icmpv4::TimeExceededCode::from_u8(code.code_u8())?) - } - // Translate the Code as follows: - Icmpv6Type::ParameterProblem(i) => { - use etherparse::icmpv6::ParameterProblemCode::*; - - match i.code { - // Code 0 (Erroneous header field encountered): Set to Type 12, - // Code 0, and update the pointer as defined in Figure 6. (If - // the Original IPv6 Pointer Value is not listed or the - // Translated IPv4 Pointer Value is listed as "n/a", silently - // drop the packet.) - ErroneousHeaderField => { - return None; // FIXME: Need to update the pointer - } - // Code 1 (Unrecognized Next Header type encountered): Translate - // this to an ICMPv4 protocol unreachable (Type 3, Code 2). - UnrecognizedNextHeader => { - Icmpv4Type::DestinationUnreachable(icmpv4::DestUnreachableHeader::Protocol) - } - - // Code 2 (Unrecognized IPv6 option encountered): Silently drop. - UnrecognizedIpv6Option => { - return None; - } - // Unknown error messages: Silently drop. - Ipv6FirstFragmentIncompleteHeaderChain - | SrUpperLayerHeaderError - | UnrecognizedNextHeaderByIntermediateNode - | ExtensionHeaderTooBig - | ExtensionHeaderChainTooLong - | TooManyExtensionHeaders - | TooManyOptionsInExtensionHeader - | OptionTooBig => { - return None; - } - } - } - - // MLD Multicast Listener Query/Report/Done (Type 130, 131, 132): - // Single-hop message. Silently drop. - - // Neighbor Discover messages (Type 133 through 137): Single-hop - // message. Silently drop. - - // Unknown informational messages: Silently drop. - Icmpv6Type::Unknown { .. } => return None, - }; - - Some(Icmpv4Header::new(icmpv4_type)) -} - -pub fn translate_packet_too_big(mtu: u32) -> etherparse::icmpv4::DestUnreachableHeader { - // Packet Too Big (Type 2): Translate to an ICMPv4 Destination - // Unreachable (Type 3) with Code 4, and adjust the ICMPv4 - // checksum both to take the type change into account and to - // exclude the ICMPv6 pseudo-header. The MTU field MUST be - // adjusted for the difference between the IPv4 and IPv6 header - // sizes, taking into account whether or not the packet in error - // includes a Fragment Header, i.e., minimum(advertised MTU-20, - // MTU_of_IPv4_nexthop, (MTU_of_IPv6_nexthop)-20). - // - // See also the requirements in Section 6. - - let mtu = u16::try_from(mtu).unwrap_or(u16::MAX); // Unlikely but necessary fallback. - - etherparse::icmpv4::DestUnreachableHeader::FragmentationNeeded { - next_hop_mtu: mtu - 20, // We don't know the next-hop MTUs here so we just subtract 20 bytes. - } -} - -pub fn translate_dest_unreachable( - code: etherparse::icmpv6::DestUnreachableCode, -) -> Option { - use etherparse::icmpv4::{self, DestUnreachableHeader::*}; - use etherparse::icmpv6::{self, DestUnreachableCode::*}; - - Some(match code { - // Code 0 (No route to destination): Set the Code to 1 (Host - // unreachable). - NoRoute => Host, - - // Code 1 (Communication with destination administratively - // prohibited): Set the Code to 10 (Communication with - // destination host administratively prohibited). - Prohibited => HostProhibited, - - // Code 2 (Beyond scope of source address): Set the Code to 1 - // (Host unreachable). Note that this error is very unlikely - // since an IPv4-translatable source address is typically - // considered to have global scope. - BeyondScope => Host, - - // Code 3 (Address unreachable): Set the Code to 1 (Host - // unreachable). - Address => Host, - - // Code 4 (Port unreachable): Set the Code to 3 (Port - // unreachable). - icmpv6::DestUnreachableCode::Port => icmpv4::DestUnreachableHeader::Port, - - // Other Code values: Silently drop. - SourceAddressFailedPolicy | RejectRoute => { - return None; - } - }) -} diff --git a/rust/connlib/ip-packet/src/proptests.rs b/rust/connlib/ip-packet/src/proptests.rs deleted file mode 100644 index f587b68f7..000000000 --- a/rust/connlib/ip-packet/src/proptests.rs +++ /dev/null @@ -1,248 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - -use proptest::arbitrary::any; -use proptest::prop_oneof; -use proptest::strategy::Strategy; - -use crate::{IpPacket, build}; -use etherparse::{Ipv4Extensions, Ipv4Header, Ipv4Options, PacketBuilder}; -use proptest::prelude::Just; - -const EMPTY_PAYLOAD: &[u8] = &[]; - -fn tcp_packet_v4() -> impl Strategy { - ( - any::(), - any::(), - any::(), - any::(), - any::>(), - ) - .prop_map(|(src, dst, sport, dport, payload)| { - build!( - PacketBuilder::ipv4(src.octets(), dst.octets(), 64).tcp(sport, dport, 0, 128), - payload - ) - }) - .prop_map(|r: anyhow::Result| r.unwrap()) -} - -fn tcp_packet_v6() -> impl Strategy { - ( - any::(), - any::(), - any::(), - any::(), - any::>(), - ) - .prop_map(|(src, dst, sport, dport, payload)| { - build!( - PacketBuilder::ipv6(src.octets(), dst.octets(), 64).tcp(sport, dport, 0, 128), - payload - ) - }) - .prop_map(|r: anyhow::Result| r.unwrap()) -} - -fn udp_packet_v4() -> impl Strategy { - ( - any::(), - any::(), - any::(), - any::(), - any::>(), - ) - .prop_map(|(src, dst, sport, dport, payload)| { - build!( - PacketBuilder::ipv4(src.octets(), dst.octets(), 64).udp(sport, dport), - payload - ) - }) - .prop_map(|r: anyhow::Result| r.unwrap()) -} - -fn udp_packet_v6() -> impl Strategy { - ( - any::(), - any::(), - any::(), - any::(), - any::>(), - ) - .prop_map(|(src, dst, sport, dport, payload)| { - build!( - PacketBuilder::ipv6(src.octets(), dst.octets(), 64).udp(sport, dport), - payload - ) - }) - .prop_map(|r: anyhow::Result| r.unwrap()) -} - -fn icmp_request_packet_v4() -> impl Strategy { - ( - any::(), - any::(), - any::(), - any::(), - ipv4_options(), - ) - .prop_map(|(src, dst, id, seq, options)| { - let packet = PacketBuilder::ip(etherparse::IpHeaders::Ipv4( - Ipv4Header { - source: src.octets(), - destination: dst.octets(), - options, - ..Default::default() - }, - Ipv4Extensions::default(), - )) - .icmpv4_echo_request(id, seq); - - build!(packet, EMPTY_PAYLOAD) - }) - .prop_map(|r: anyhow::Result| r.unwrap()) -} - -fn icmp_reply_packet_v4() -> impl Strategy { - ( - any::(), - any::(), - any::(), - any::(), - ipv4_options(), - ) - .prop_map(|(src, dst, id, seq, options)| { - let packet = PacketBuilder::ip(etherparse::IpHeaders::Ipv4( - Ipv4Header { - source: src.octets(), - destination: dst.octets(), - options, - ..Default::default() - }, - Ipv4Extensions::default(), - )) - .icmpv4_echo_reply(id, seq); - - build!(packet, EMPTY_PAYLOAD) - }) - .prop_map(|r: anyhow::Result| r.unwrap()) -} - -fn icmp_request_packet_v6() -> impl Strategy { - ( - any::(), - any::(), - any::(), - any::(), - ) - .prop_map(|(src, dst, id, seq)| { - build!( - PacketBuilder::ipv6(src.octets(), dst.octets(), 64).icmpv6_echo_request(id, seq), - EMPTY_PAYLOAD - ) - }) - .prop_map(|r: anyhow::Result| r.unwrap()) -} - -fn icmp_reply_packet_v6() -> impl Strategy { - ( - any::(), - any::(), - any::(), - any::(), - ) - .prop_map(|(src, dst, id, seq)| { - build!( - PacketBuilder::ipv6(src.octets(), dst.octets(), 64).icmpv6_echo_reply(id, seq), - EMPTY_PAYLOAD - ) - }) - .prop_map(|r: anyhow::Result| r.unwrap()) -} - -fn ipv4_options() -> impl Strategy { - prop_oneof![ - Just(Ipv4Options::from([0u8; 0])), - Just(Ipv4Options::from([0u8; 4])), - Just(Ipv4Options::from([0u8; 8])), - Just(Ipv4Options::from([0u8; 12])), - Just(Ipv4Options::from([0u8; 16])), - Just(Ipv4Options::from([0u8; 20])), - Just(Ipv4Options::from([0u8; 24])), - Just(Ipv4Options::from([0u8; 28])), - Just(Ipv4Options::from([0u8; 32])), - Just(Ipv4Options::from([0u8; 36])), - Just(Ipv4Options::from([0u8; 40])), - ] -} - -fn packet_v4() -> impl Strategy { - prop_oneof![ - tcp_packet_v4(), - udp_packet_v4(), - icmp_request_packet_v4(), - icmp_reply_packet_v4(), - ] -} - -fn packet_v6() -> impl Strategy { - prop_oneof![ - tcp_packet_v6(), - udp_packet_v6(), - icmp_request_packet_v6(), - icmp_reply_packet_v6(), - ] -} - -#[test_strategy::proptest()] -fn nat_6446( - #[strategy(packet_v6())] packet_v6: IpPacket, - #[strategy(any::())] new_src: Ipv4Addr, - #[strategy(any::())] new_dst: Ipv4Addr, -) { - let header = packet_v6.ipv6_header().unwrap(); - let payload = packet_v6.payload().to_vec(); - - let packet_v4 = packet_v6.consume_to_ipv4(new_src, new_dst).unwrap(); - - assert_eq!(packet_v4.source(), IpAddr::V4(new_src)); - assert_eq!(packet_v4.destination(), new_dst); - - let mut new_packet_v6 = packet_v4 - .consume_to_ipv6(header.source_addr(), header.destination_addr()) - .unwrap(); - new_packet_v6.update_checksum(); - - assert_eq!(new_packet_v6.ipv6_header().unwrap(), header); - assert_eq!(new_packet_v6.payload(), payload); -} - -#[test_strategy::proptest()] -fn nat_4664( - #[strategy(packet_v4())] packet_v4: IpPacket, - #[strategy(any::())] new_src: Ipv6Addr, - #[strategy(any::())] new_dst: Ipv6Addr, -) { - let header = packet_v4.ipv4_header().unwrap(); - let payload = packet_v4.payload().to_vec(); - - let packet_v6 = packet_v4.consume_to_ipv6(new_src, new_dst).unwrap(); - - assert_eq!(packet_v6.source(), IpAddr::V6(new_src)); - assert_eq!(packet_v6.destination(), new_dst); - - let mut new_packet_v4 = packet_v6 - .consume_to_ipv4(header.source.into(), header.destination.into()) - .unwrap(); - new_packet_v4.update_checksum(); - - let mut header_without_options = Ipv4Header { - options: Ipv4Options::default(), // IPv4 options are lost in translation. - total_len: header.total_len - header.options.len_u8() as u16, - ..header - }; - header_without_options.header_checksum = header_without_options.calc_header_checksum(); - - assert_eq!(new_packet_v4.ipv4_header().unwrap(), header_without_options); - assert_eq!(new_packet_v4.payload(), payload); -} diff --git a/rust/connlib/tunnel/src/client.rs b/rust/connlib/tunnel/src/client.rs index 81c680240..fb9f56dcb 100644 --- a/rust/connlib/tunnel/src/client.rs +++ b/rust/connlib/tunnel/src/client.rs @@ -1229,7 +1229,9 @@ impl ClientState { connlib_dns_server, now + IDS_EXPIRE, ); - packet.set_dst(upstream.ip()); + if let Err(e) = packet.set_dst(upstream.ip()) { + tracing::warn!("Failed to set destination IP for UDP DNS query: {e:#}"); + } // TODO: Remove this once we disallow non-standard DNS ports: https://github.com/firezone/firezone/issues/8330 packet .as_udp_mut() @@ -1927,7 +1929,10 @@ fn maybe_mangle_dns_response_from_upstream_dns_server( tracing::trace!(server = %src_ip, query_id = %message.id(), domain = %message.domain(), "Received UDP DNS response via tunnel"); - packet.set_src(original_dst.ip()); + if let Err(e) = packet.set_src(original_dst.ip()) { + tracing::warn!("Failed to set source IP for UDP DNS query: {e:#}"); + } + packet .as_udp_mut() .expect("we parsed it as a UDP packet earlier") diff --git a/rust/connlib/tunnel/src/peer.rs b/rust/connlib/tunnel/src/peer.rs index d13f82080..bc20132f3 100644 --- a/rust/connlib/tunnel/src/peer.rs +++ b/rust/connlib/tunnel/src/peer.rs @@ -295,25 +295,14 @@ impl ClientOnGateway { let Some(state) = self.permanent_translations.get_mut(&packet.destination()) else { return Ok(TranslateOutboundResult::DestinationUnreachable( - ip_packet::make::icmp_dst_unreachable( - self.gateway_tun.v4, - self.gateway_tun.v6, - &packet, - )?, + ip_packet::make::icmp_dst_unreachable(&packet)?, )); }; - #[expect(clippy::collapsible_if, reason = "We want the feature flag separate.")] - if firezone_telemetry::feature_flags::icmp_unreachable_instead_of_nat64() { - if state.resolved_ip.is_ipv4() != dst.is_ipv4() { - return Ok(TranslateOutboundResult::DestinationUnreachable( - ip_packet::make::icmp_dst_unreachable( - self.gateway_tun.v4, - self.gateway_tun.v6, - &packet, - )?, - )); - } + if state.resolved_ip.is_ipv4() != dst.is_ipv4() { + return Ok(TranslateOutboundResult::DestinationUnreachable( + ip_packet::make::icmp_dst_unreachable(&packet)?, + )); } let (source_protocol, real_ip) = @@ -321,12 +310,7 @@ impl ClientOnGateway { .translate_outgoing(&packet, state.resolved_ip, now)?; let mut packet = packet - .translate_destination( - self.client_tun.v4, - self.client_tun.v6, - source_protocol, - real_ip, - ) + .translate_destination(source_protocol, real_ip) .context("Failed to translate packet to new destination")?; packet.update_checksum(); @@ -429,7 +413,7 @@ impl ClientOnGateway { }; let mut packet = packet - .translate_source(self.client_tun.v4, self.client_tun.v6, proto, ip) + .translate_source(proto, ip) .context("Failed to translate packet to new source")?; packet.update_checksum(); diff --git a/rust/connlib/tunnel/src/peer/nat_table.rs b/rust/connlib/tunnel/src/peer/nat_table.rs index c735a2f8f..b9055476a 100644 --- a/rust/connlib/tunnel/src/peer/nat_table.rs +++ b/rust/connlib/tunnel/src/peer/nat_table.rs @@ -159,16 +159,11 @@ pub struct DestinationUnreachablePrototype { impl DestinationUnreachablePrototype { /// Turns this prototype into an actual ICMP error IP packet, targeting the given IPv4/IPv6 address, depending on the original Resource address. - /// - /// Due to our NAT64/64 implementation, the ICMP error that we receive on the Gateway may not be what we want to forward to the client. - /// For example, in case we translate a TCP-SYN from IPv4 to IPv6 but the IPv6 address is unreachable, we need to: - /// - Translate the failed packet embedded in the ICMP error back to an IPv4 packet. - /// - Send an ICMPv4 error instead of an ICMPv6 error. pub fn into_packet(self, dst_v4: Ipv4Addr, dst_v6: Ipv6Addr) -> Result { // First, translate the failed packet as if it would have directly originated from the client (without our NAT applied). let original_packet = self .failed_packet - .translate_destination(self.inside_dst, self.inside_proto, dst_v4, dst_v6) + .translate_destination(self.inside_dst, self.inside_proto) .context("Failed to translate unroutable packet within ICMP error")?; // Second, generate an ICMP error that originates from the originally addressed Resource. @@ -241,7 +236,7 @@ mod tests { // Pretend we are getting a response. let mut response = packet.clone(); response.set_destination_protocol(new_source_protocol.value()); - response.set_src(new_dst_ip); + response.set_src(new_dst_ip).unwrap(); // Update time. table.handle_timeout(sent_at + response_delay); @@ -298,7 +293,7 @@ mod tests { // Pretend we are getting a response. for ((p, _), (new_src_p, new_d)) in packets.iter_mut().zip(new_src_p_and_dst) { p.set_destination_protocol(new_src_p.value()); - p.set_src(new_d); + p.set_src(new_d).unwrap(); } // Translate in diff --git a/rust/connlib/tunnel/src/tests/assertions.rs b/rust/connlib/tunnel/src/tests/assertions.rs index 8cbd2a4fe..a1b9dfce0 100644 --- a/rust/connlib/tunnel/src/tests/assertions.rs +++ b/rust/connlib/tunnel/src/tests/assertions.rs @@ -154,19 +154,6 @@ fn assert_packets_properties( tracing::error!(target: "assertions", ?unexpected_replies, ?expected_handshakes, ?received_replies, "❌ Unexpected {packet_protocol} replies on client"); } - for (gid, expected_handshakes) in expected_handshakes.iter() { - let received_requests = received_requests.get(gid).unwrap(); - - let num_expected_handshakes = expected_handshakes.len(); - let num_actual_handshakes = received_requests.len(); - - if num_expected_handshakes != num_actual_handshakes { - tracing::error!(target: "assertions", %num_expected_handshakes, %num_actual_handshakes, %gid, "❌ Unexpected {packet_protocol} requests"); - } else { - tracing::info!(target: "assertions", %num_expected_handshakes, %gid, "✅ Performed the expected {packet_protocol} handshakes"); - } - } - let mut mapping = HashMap::new(); // Assert properties of the individual handshakes per gateway. @@ -174,6 +161,9 @@ fn assert_packets_properties( // Thus, we rely on a custom u64 payload attached to all packets to uniquely identify every individual packet. for (gateway, expected_handshakes) in expected_handshakes { let received_requests = received_requests.get(gateway).unwrap(); + + let mut num_expected_handshakes = expected_handshakes.len(); + for (payload, (resource_dst, t, u)) in expected_handshakes { let _guard = make_span(*t, *u).entered(); @@ -188,6 +178,16 @@ fn assert_packets_properties( assert_correct_src_and_dst_ips(client_sent_request, client_received_reply); let Some(gateway_received_request) = received_requests.get(payload) else { + if client_received_reply + .icmp_unreachable_destination() + .ok() + .is_some_and(|icmp| icmp.is_some()) + { + // If the received reply is an ICMP unreachable error, it is ok to have a missing request. + num_expected_handshakes -= 1; + continue; + } + tracing::error!(target: "assertions", "❌ Missing {packet_protocol} request on gateway"); continue; }; @@ -220,6 +220,14 @@ fn assert_packets_properties( } } } + + let num_actual_handshakes = received_requests.len(); + + if num_expected_handshakes != num_actual_handshakes { + tracing::error!(target: "assertions", %num_expected_handshakes, %num_actual_handshakes, %gateway, "❌ Unexpected {packet_protocol} requests"); + } else { + tracing::info!(target: "assertions", %num_expected_handshakes, %gateway, "✅ Performed the expected {packet_protocol} handshakes"); + } } } diff --git a/rust/connlib/tunnel/src/tests/sim_client.rs b/rust/connlib/tunnel/src/tests/sim_client.rs index f9394e46e..18694cf15 100644 --- a/rust/connlib/tunnel/src/tests/sim_client.rs +++ b/rust/connlib/tunnel/src/tests/sim_client.rs @@ -231,23 +231,29 @@ impl SimClient { /// Process an IP packet received on the client. pub(crate) fn on_received_packet(&mut self, packet: IpPacket) { - if let Some((failed_packet, _)) = packet.icmp_unreachable_destination().unwrap() { - match failed_packet.layer4_protocol() { - Layer4Protocol::Udp { src, dst } => { - self.received_udp_replies - .insert((SPort(dst), DPort(src)), packet); + match packet.icmp_unreachable_destination() { + Ok(Some((failed_packet, _))) => { + match failed_packet.layer4_protocol() { + Layer4Protocol::Udp { src, dst } => { + self.received_udp_replies + .insert((SPort(dst), DPort(src)), packet); + } + Layer4Protocol::Tcp { src, dst } => { + self.received_tcp_replies + .insert((SPort(dst), DPort(src)), packet); + } + Layer4Protocol::Icmp { seq, id } => { + self.received_icmp_replies + .insert((Seq(seq), Identifier(id)), packet); + } } - Layer4Protocol::Tcp { src, dst } => { - self.received_tcp_replies - .insert((SPort(dst), DPort(src)), packet); - } - Layer4Protocol::Icmp { seq, id } => { - self.received_icmp_replies - .insert((Seq(seq), Identifier(id)), packet); - } - } - return; + return; + } + Ok(None) => {} + Err(e) => { + tracing::error!("Failed to extract ICMP unreachable destination: {e:#}") + } } if let Some(udp) = packet.as_udp() { diff --git a/rust/connlib/tunnel/src/tests/sim_gateway.rs b/rust/connlib/tunnel/src/tests/sim_gateway.rs index 933b7607c..0409b5a2b 100644 --- a/rust/connlib/tunnel/src/tests/sim_gateway.rs +++ b/rust/connlib/tunnel/src/tests/sim_gateway.rs @@ -409,8 +409,8 @@ fn echo_reply(mut req: IpPacket) -> Option { let original_src = req.source(); let original_dst = req.destination(); - req.set_dst(original_src); - req.set_src(original_dst); + req.set_dst(original_src).unwrap(); + req.set_src(original_dst).unwrap(); Some(req) }