mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 18:18:55 +00:00
chore(gateway): remove NAT64/46 module (#9626)
This has been disabled for several releases now and is not causing any problems in production. We can therefore safely remove it. It is about time we do this because our tests are actually still testing the variant without the feature flag and therefore deviate from what we do in production. We therefore have to convert the tests as well. Doing so uncovered a minor problem in our ICMP error parsing code: We attempted to parse the payload of an ICMP error as a fully-valid layer 4 header (e.g. TCP header or UDP header). However, per the RFC a node only needs to embed the first 8 bytes of the original packet in an ICMPv4 error. That is not enough to parse a valid TCP header as those are at least 20 bytes. I don't expect this to be a huge problem in production right now though. We only use this code to parse ICMP errors arriving on the Gateway and I _think_ most devices actually include more than 8 bytes. This only surfaced because we are very strict with only embedding exactly 8 bytes when we generate an ICMP error. Additionally, we change our ICMP errors to be sent from the resource IP rather than the Gateway's TUN device. Given that we perform NAT on these IPs anyway, I think this can still be argued to be RFC conform. The _proxy_ IP which we are trying to contact can be reached but it cannot be routed further. Therefore the destination is unreachable, yet the source of this error is the proxy IP itself. I think this is actually more correct than sending the packets from the Gateway's TUN device because the TUN device itself is not a routing hop per-se: its IP won't ever show up in the routing path.
This commit is contained in:
2
.github/workflows/_rust.yml
vendored
2
.github/workflows/_rust.yml
vendored
@@ -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
|
||||
|
||||
@@ -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<Icmpv4Type> {
|
||||
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<Icmpv6Type> {
|
||||
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<Vec<u8>> {
|
||||
pub fn translate_destination(self, dst: IpAddr, src_proto: Protocol) -> Result<Vec<u8>> {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Vec<u8>>,
|
||||
start: usize,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl ConvertibleIpv4Packet {
|
||||
pub fn new(ip: IpPacketBuf, len: usize) -> Result<ConvertibleIpv4Packet> {
|
||||
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<ConvertibleIpv6Packet> {
|
||||
// `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<Vec<u8>>,
|
||||
start: usize,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl ConvertibleIpv6Packet {
|
||||
pub fn new(ip: IpPacketBuf, len: usize) -> Result<ConvertibleIpv6Packet> {
|
||||
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<ConvertibleIpv4Packet> {
|
||||
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<Self> {
|
||||
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<IpPacket> {
|
||||
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<IpPacket> {
|
||||
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<IpPacket> {
|
||||
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<IpPacket> {
|
||||
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<IpPacket> {
|
||||
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<IpPacket> {
|
||||
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<Layer4Protocol> {
|
||||
// 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")?
|
||||
|
||||
@@ -156,16 +156,23 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icmp_dst_unreachable(
|
||||
ipv4_src: Ipv4Addr,
|
||||
ipv6_src: Ipv6Addr,
|
||||
original_packet: &IpPacket,
|
||||
) -> Result<IpPacket> {
|
||||
pub fn icmp_dst_unreachable(original_packet: &IpPacket) -> Result<IpPacket> {
|
||||
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<u8>,
|
||||
) {
|
||||
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<u8>,
|
||||
) {
|
||||
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<Value = Vec<u8>> {
|
||||
collection::vec(any::<u8>(), 0..=max_size)
|
||||
}
|
||||
|
||||
@@ -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<usize> {
|
||||
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<etherparse::Icmpv6Header> {
|
||||
// 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<Icmpv6Type> {
|
||||
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),
|
||||
})
|
||||
}
|
||||
@@ -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<etherparse::Icmpv4Header> {
|
||||
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<etherparse::icmpv4::DestUnreachableHeader> {
|
||||
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;
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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<Value = IpPacket> {
|
||||
(
|
||||
any::<Ipv4Addr>(),
|
||||
any::<Ipv4Addr>(),
|
||||
any::<u16>(),
|
||||
any::<u16>(),
|
||||
any::<Vec<u8>>(),
|
||||
)
|
||||
.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<IpPacket>| r.unwrap())
|
||||
}
|
||||
|
||||
fn tcp_packet_v6() -> impl Strategy<Value = IpPacket> {
|
||||
(
|
||||
any::<Ipv6Addr>(),
|
||||
any::<Ipv6Addr>(),
|
||||
any::<u16>(),
|
||||
any::<u16>(),
|
||||
any::<Vec<u8>>(),
|
||||
)
|
||||
.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<IpPacket>| r.unwrap())
|
||||
}
|
||||
|
||||
fn udp_packet_v4() -> impl Strategy<Value = IpPacket> {
|
||||
(
|
||||
any::<Ipv4Addr>(),
|
||||
any::<Ipv4Addr>(),
|
||||
any::<u16>(),
|
||||
any::<u16>(),
|
||||
any::<Vec<u8>>(),
|
||||
)
|
||||
.prop_map(|(src, dst, sport, dport, payload)| {
|
||||
build!(
|
||||
PacketBuilder::ipv4(src.octets(), dst.octets(), 64).udp(sport, dport),
|
||||
payload
|
||||
)
|
||||
})
|
||||
.prop_map(|r: anyhow::Result<IpPacket>| r.unwrap())
|
||||
}
|
||||
|
||||
fn udp_packet_v6() -> impl Strategy<Value = IpPacket> {
|
||||
(
|
||||
any::<Ipv6Addr>(),
|
||||
any::<Ipv6Addr>(),
|
||||
any::<u16>(),
|
||||
any::<u16>(),
|
||||
any::<Vec<u8>>(),
|
||||
)
|
||||
.prop_map(|(src, dst, sport, dport, payload)| {
|
||||
build!(
|
||||
PacketBuilder::ipv6(src.octets(), dst.octets(), 64).udp(sport, dport),
|
||||
payload
|
||||
)
|
||||
})
|
||||
.prop_map(|r: anyhow::Result<IpPacket>| r.unwrap())
|
||||
}
|
||||
|
||||
fn icmp_request_packet_v4() -> impl Strategy<Value = IpPacket> {
|
||||
(
|
||||
any::<Ipv4Addr>(),
|
||||
any::<Ipv4Addr>(),
|
||||
any::<u16>(),
|
||||
any::<u16>(),
|
||||
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<IpPacket>| r.unwrap())
|
||||
}
|
||||
|
||||
fn icmp_reply_packet_v4() -> impl Strategy<Value = IpPacket> {
|
||||
(
|
||||
any::<Ipv4Addr>(),
|
||||
any::<Ipv4Addr>(),
|
||||
any::<u16>(),
|
||||
any::<u16>(),
|
||||
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<IpPacket>| r.unwrap())
|
||||
}
|
||||
|
||||
fn icmp_request_packet_v6() -> impl Strategy<Value = IpPacket> {
|
||||
(
|
||||
any::<Ipv6Addr>(),
|
||||
any::<Ipv6Addr>(),
|
||||
any::<u16>(),
|
||||
any::<u16>(),
|
||||
)
|
||||
.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<IpPacket>| r.unwrap())
|
||||
}
|
||||
|
||||
fn icmp_reply_packet_v6() -> impl Strategy<Value = IpPacket> {
|
||||
(
|
||||
any::<Ipv6Addr>(),
|
||||
any::<Ipv6Addr>(),
|
||||
any::<u16>(),
|
||||
any::<u16>(),
|
||||
)
|
||||
.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<IpPacket>| r.unwrap())
|
||||
}
|
||||
|
||||
fn ipv4_options() -> impl Strategy<Value = Ipv4Options> {
|
||||
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<Value = IpPacket> {
|
||||
prop_oneof![
|
||||
tcp_packet_v4(),
|
||||
udp_packet_v4(),
|
||||
icmp_request_packet_v4(),
|
||||
icmp_reply_packet_v4(),
|
||||
]
|
||||
}
|
||||
|
||||
fn packet_v6() -> impl Strategy<Value = IpPacket> {
|
||||
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::<Ipv4Addr>())] new_src: Ipv4Addr,
|
||||
#[strategy(any::<Ipv4Addr>())] 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::<Ipv6Addr>())] new_src: Ipv6Addr,
|
||||
#[strategy(any::<Ipv6Addr>())] 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);
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<IpPacket> {
|
||||
// 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
|
||||
|
||||
@@ -154,19 +154,6 @@ fn assert_packets_properties<T, U>(
|
||||
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<T, U>(
|
||||
// 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<T, U>(
|
||||
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<T, U>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -409,8 +409,8 @@ fn echo_reply(mut req: IpPacket) -> Option<IpPacket> {
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user