From 8cc28499e9f439daf96e09f3041c7d1eaf27b011 Mon Sep 17 00:00:00 2001 From: Gabi Date: Fri, 14 Jun 2024 18:33:07 -0300 Subject: [PATCH] chore(connlib): implement IP translation according to RFC6145 (#5364) As part of #4994, we need to translate IP packets between IPv4 and IPv6. This PR introduces the `ConvertiblePacket` abstraction that implements this. --- rust/Cargo.lock | 3 + rust/connlib/snownet/src/node.rs | 21 +- rust/connlib/tunnel/src/device_channel.rs | 8 +- .../tunnel/src/device_channel/tun_windows.rs | 5 +- rust/connlib/tunnel/src/dns.rs | 7 +- rust/connlib/tunnel/src/lib.rs | 17 +- rust/ip-packet/Cargo.toml | 8 + .../proptest-regressions/proptests.txt | 11 + rust/ip-packet/src/lib.rs | 926 +++++++++++++++++- rust/ip-packet/src/make.rs | 166 ++-- rust/ip-packet/src/proptests.rs | 233 +++++ 11 files changed, 1293 insertions(+), 112 deletions(-) create mode 100644 rust/ip-packet/proptest-regressions/proptests.txt create mode 100644 rust/ip-packet/src/proptests.rs diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 06fe8f771..f0b1c4509 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -3170,6 +3170,9 @@ version = "0.1.0" dependencies = [ "hickory-proto", "pnet_packet", + "proptest", + "test-strategy", + "thiserror", ] [[package]] diff --git a/rust/connlib/snownet/src/node.rs b/rust/connlib/snownet/src/node.rs index 1ef3dd532..6279c1ec3 100644 --- a/rust/connlib/snownet/src/node.rs +++ b/rust/connlib/snownet/src/node.rs @@ -9,9 +9,9 @@ use boringtun::noise::{Tunn, TunnResult}; use boringtun::x25519::PublicKey; use boringtun::{noise::rate_limiter::RateLimiter, x25519::StaticSecret}; use core::fmt; -use ip_packet::ipv4::MutableIpv4Packet; -use ip_packet::ipv6::MutableIpv6Packet; -use ip_packet::{IpPacket, MutableIpPacket, Packet as _}; +use ip_packet::{ + ConvertibleIpv4Packet, ConvertibleIpv6Packet, IpPacket, MutableIpPacket, Packet as _, +}; use rand::random; use secrecy::{ExposeSecret, Secret}; use std::borrow::Cow; @@ -1643,7 +1643,7 @@ where transmits: &mut VecDeque>, now: Instant, ) -> ControlFlow, MutableIpPacket<'b>> { - match self.tunnel.decapsulate(None, packet, buffer) { + match self.tunnel.decapsulate(None, packet, &mut buffer[20..]) { TunnResult::Done => ControlFlow::Break(Ok(())), TunnResult::Err(e) => ControlFlow::Break(Err(Error::Decapsulate(e))), @@ -1652,15 +1652,20 @@ where // In our API, we parse the packets directly as an IpPacket. // Thus, the caller can query whatever data they'd like, not just the source IP so we don't return it in addition. TunnResult::WriteToTunnelV4(packet, ip) => { - let ipv4_packet = - MutableIpv4Packet::new(packet).expect("boringtun verifies validity"); + let packet_len = packet.len(); + let ipv4_packet = ConvertibleIpv4Packet::new(&mut buffer[..(packet_len + 20)]) + .expect("boringtun verifies validity"); debug_assert_eq!(ipv4_packet.get_source(), ip); ControlFlow::Continue(ipv4_packet.into()) } TunnResult::WriteToTunnelV6(packet, ip) => { - let ipv6_packet = - MutableIpv6Packet::new(packet).expect("boringtun verifies validity"); + // For ipv4 we need to use buffer to create the ip packet because we need the extra 20 bytes at the beginning + // for ipv6 we just need this to convince the borrow-checker that `packet`'s lifetime isn't `'b`, otherwise it's taken + // as `'b` for all branches. + let packet_len = packet.len(); + let ipv6_packet = ConvertibleIpv6Packet::new(&mut buffer[20..(packet_len + 20)]) + .expect("boringtun verifies validity"); debug_assert_eq!(ipv6_packet.get_source(), ip); ControlFlow::Continue(ipv6_packet.into()) diff --git a/rust/connlib/tunnel/src/device_channel.rs b/rust/connlib/tunnel/src/device_channel.rs index e613e7aeb..8742565c6 100644 --- a/rust/connlib/tunnel/src/device_channel.rs +++ b/rust/connlib/tunnel/src/device_channel.rs @@ -133,7 +133,7 @@ impl Device { return Poll::Pending; }; - let n = std::task::ready!(tun.poll_read(buf, cx))?; + let n = std::task::ready!(tun.poll_read(&mut buf[20..], cx))?; if n == 0 { return Poll::Ready(Err(io::Error::new( @@ -142,7 +142,7 @@ impl Device { ))); } - let packet = MutableIpPacket::new(&mut buf[..n]).ok_or_else(|| { + let packet = MutableIpPacket::new(&mut buf[..(n + 20)]).ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, "received bytes are not an IP packet", @@ -167,7 +167,7 @@ impl Device { return Poll::Pending; }; - let n = std::task::ready!(tun.poll_read(buf, cx))?; + let n = std::task::ready!(tun.poll_read(&mut buf[20..], cx))?; if n == 0 { return Poll::Ready(Err(io::Error::new( @@ -176,7 +176,7 @@ impl Device { ))); } - let packet = MutableIpPacket::new(&mut buf[..n]).ok_or_else(|| { + let packet = MutableIpPacket::new(&mut buf[..(n + 20)]).ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidInput, "received bytes are not an IP packet", diff --git a/rust/connlib/tunnel/src/device_channel/tun_windows.rs b/rust/connlib/tunnel/src/device_channel/tun_windows.rs index 268e3decc..1444abf0b 100644 --- a/rust/connlib/tunnel/src/device_channel/tun_windows.rs +++ b/rust/connlib/tunnel/src/device_channel/tun_windows.rs @@ -1,6 +1,7 @@ +use crate::MTU; use connlib_shared::{ windows::{CREATE_NO_WINDOW, TUNNEL_NAME}, - Callbacks, Result, DEFAULT_MTU, + Callbacks, Result, }; use ip_network::IpNetwork; use std::{ @@ -83,7 +84,7 @@ impl Tun { .stdout(Stdio::null()) .status()?; - set_iface_config(adapter.get_luid(), DEFAULT_MTU)?; + set_iface_config(adapter.get_luid(), MTU as u32)?; let session = Arc::new(adapter.start_session(wintun::MAX_RING_CAPACITY)?); let (packet_tx, packet_rx) = mpsc::channel(5); diff --git a/rust/connlib/tunnel/src/dns.rs b/rust/connlib/tunnel/src/dns.rs index 310f7d5aa..f96d8f020 100644 --- a/rust/connlib/tunnel/src/dns.rs +++ b/rust/connlib/tunnel/src/dns.rs @@ -220,8 +220,11 @@ fn build_response(original_pkt: IpPacket<'_>, mut dns_answer: Vec) -> IpPack let response_len = dns_answer.len(); let original_dgm = original_pkt.unwrap_as_udp(); let hdr_len = original_pkt.packet_size() - original_dgm.payload().len(); - let mut res_buf = Vec::with_capacity(hdr_len + response_len); + let mut res_buf = Vec::with_capacity(hdr_len + response_len + 20); + // TODO: this is some weirdness due to how MutableIpPacket is implemented + // we need an extra 20 bytes padding. + res_buf.extend_from_slice(&[0; 20]); res_buf.extend_from_slice(&original_pkt.packet()[..hdr_len]); res_buf.append(&mut dns_answer); @@ -245,6 +248,8 @@ fn build_response(original_pkt: IpPacket<'_>, mut dns_answer: Vec) -> IpPack pkt.unwrap_as_udp().set_checksum(udp_checksum); pkt.set_ipv4_checksum(); + // TODO: more of this weirdness + res_buf.drain(0..20); IpPacket::owned(res_buf).unwrap() } diff --git a/rust/connlib/tunnel/src/lib.rs b/rust/connlib/tunnel/src/lib.rs index d6c90bb20..228a10048 100644 --- a/rust/connlib/tunnel/src/lib.rs +++ b/rust/connlib/tunnel/src/lib.rs @@ -38,7 +38,6 @@ mod utils; mod tests; const MAX_UDP_SIZE: usize = (1 << 16) - 1; - const MTU: usize = 1280; const REALM: &str = "firezone"; @@ -61,15 +60,13 @@ pub struct Tunnel { /// Handles all side-effects. io: Io, - // TODO: could we make these buffers smaller? Since all the valid packets will be at most - // MTU + Wireguard Header + optionally Data Channel + UDP header + IPV4/IPV6 header (1280 + 32 + 4 + 8 + 40 = 1364) - // or STUN control messages which afaik are smaller than that ip4_read_buf: Box<[u8; MAX_UDP_SIZE]>, ip6_read_buf: Box<[u8; MAX_UDP_SIZE]>, // We need an extra 16 bytes on top of the MTU for write_buf since boringtun copies the extra AEAD tag before decrypting it - write_buf: Box<[u8; MTU + 16]>, - device_read_buf: Box<[u8; MTU]>, + write_buf: Box<[u8; MTU + 16 + 20]>, + // We have 20 extra bytes to be able to convert between ipv4 and ipv6 + device_read_buf: Box<[u8; MTU + 20]>, } impl ClientTunnel @@ -85,10 +82,10 @@ where io: Io::new(sockets)?, callbacks, role_state: ClientState::new(private_key), - write_buf: Box::new([0u8; MTU + 16]), + write_buf: Box::new([0u8; MTU + 16 + 20]), ip4_read_buf: Box::new([0u8; MAX_UDP_SIZE]), ip6_read_buf: Box::new([0u8; MAX_UDP_SIZE]), - device_read_buf: Box::new([0u8; MTU]), + device_read_buf: Box::new([0u8; MTU + 20]), }) } @@ -187,10 +184,10 @@ where io: Io::new(sockets)?, callbacks, role_state: GatewayState::new(private_key), - write_buf: Box::new([0u8; MTU + 16]), + write_buf: Box::new([0u8; MTU + 20 + 16]), ip4_read_buf: Box::new([0u8; MAX_UDP_SIZE]), ip6_read_buf: Box::new([0u8; MAX_UDP_SIZE]), - device_read_buf: Box::new([0u8; MTU]), + device_read_buf: Box::new([0u8; MTU + 20]), }) } diff --git a/rust/ip-packet/Cargo.toml b/rust/ip-packet/Cargo.toml index d21adffcb..7d6ec7bb9 100644 --- a/rust/ip-packet/Cargo.toml +++ b/rust/ip-packet/Cargo.toml @@ -7,9 +7,17 @@ publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +proptest = ["dep:proptest"] + [dependencies] pnet_packet = { version = "0.34" } hickory-proto = { workspace = true } +proptest = { version = "1.4.0", optional = true } +thiserror = "1" + +[dev-dependencies] +test-strategy = "0.3.1" [lints] workspace = true diff --git a/rust/ip-packet/proptest-regressions/proptests.txt b/rust/ip-packet/proptest-regressions/proptests.txt new file mode 100644 index 000000000..6a7745e3b --- /dev/null +++ b/rust/ip-packet/proptest-regressions/proptests.txt @@ -0,0 +1,11 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 28493c42d9a80886807dbafa94ceb96ca9ac11e25c5f5e907a507468039b19e4 # shrinks to input = _CanTranslateDstPacketArgs { packet: Ipv6(ConvertibleIpv6Packet { buf: Owned([96, 0, 0, 0, 0, 8, 17, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 255, 222]) }), src_v4: 0.0.0.0, src_v6: ::ffff:127.0.0.1, dst: ::ffff:0.0.0.0 } +cc d0df47592e3f20988340dd52e6e2d5b1da3398085aa70423cbd302567d2a1162 # shrinks to input = _CanTranslateDstPacketArgs { packet: Ipv4(ConvertibleIpv4Packet { buf: Owned([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 40, 0, 0, 0, 0, 64, 6, 122, 209, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 0, 0, 128, 175, 101, 0, 0]) }), src_v4: 0.0.0.0, src_v6: ::ffff:0.0.0.0, dst: ::ffff:0.0.0.0 } +cc 63b7eaae54d351ff5dbda1673d09efc202850e2b356a5f28df8aa59bdb075581 # shrinks to input = _CanTranslateSrcPacketArgs { packet: Ipv4(ConvertibleIpv4Packet { buf: Owned([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 28, 0, 0, 0, 0, 64, 17, 122, 210, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 255, 222]) }), dst_v4: 0.0.0.0, dst_v6: c:5aa3:3b04:ca41:bbf6:d619:2c8b:9da7, src: ::ffff:254.113.152.73 } +cc 4bbd1ef685070370226af2c039fba677cc79d9a1ca41f8573805de2d6a5cd27c # shrinks to input = _CanTranslateDstPacketArgs { packet: Ipv4(ConvertibleIpv4Packet { buf: Owned([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 60, 0, 0, 192, 0, 64, 1, 186, 193, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 247, 255, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) }), src_v4: 0.0.0.0, src_v6: ::8:674, dst: ::ffff:127.0.0.1 } +cc 308072a334e0b7555485fe3181f8a817dd31d30dec1823eb6992f6265a56f078 # shrinks to input = _CanTranslateSrcPacketArgs { packet: Ipv4(ConvertibleIpv4Packet { buf: Owned([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 69, 0, 0, 60, 0, 0, 192, 0, 64, 1, 186, 193, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 247, 255, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) }), dst_v4: 0.0.0.0, dst_v6: ::ffff:0.0.0.0, src: 0.0.0.0 } diff --git a/rust/ip-packet/src/lib.rs b/rust/ip-packet/src/lib.rs index b34c7b081..9601fed6c 100644 --- a/rust/ip-packet/src/lib.rs +++ b/rust/ip-packet/src/lib.rs @@ -2,15 +2,25 @@ pub mod make; pub use pnet_packet::*; +#[cfg(all(test, feature = "proptest"))] +mod proptests; + use pnet_packet::{ - icmpv6::MutableIcmpv6Packet, + icmp::{ + echo_reply::MutableEchoReplyPacket, echo_request::MutableEchoRequestPacket, IcmpTypes, + MutableIcmpPacket, + }, + icmpv6::{Icmpv6Type, Icmpv6Types, MutableIcmpv6Packet}, ip::{IpNextHeaderProtocol, IpNextHeaderProtocols}, - ipv4::{Ipv4Packet, MutableIpv4Packet}, + ipv4::{Ipv4Flags, Ipv4Packet, MutableIpv4Packet}, ipv6::{Ipv6Packet, MutableIpv6Packet}, tcp::{MutableTcpPacket, TcpPacket}, udp::{MutableUdpPacket, UdpPacket}, }; -use std::net::IpAddr; +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + ops::{Deref, DerefMut}, +}; macro_rules! for_both { ($this:ident, |$name:ident| $body:expr) => { @@ -31,6 +41,40 @@ macro_rules! swap_src_dst { }; } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Protocol { + Tcp(u16), + Udp(u16), + Icmp(u16), +} + +impl Protocol { + pub fn same_type(&self, other: &Protocol) -> bool { + matches!( + (self, other), + (Protocol::Tcp(_), Protocol::Tcp(_)) + | (Protocol::Udp(_), Protocol::Udp(_)) + | (Protocol::Icmp(_), Protocol::Icmp(_)) + ) + } + + pub fn value(&self) -> u16 { + match self { + Protocol::Tcp(v) => *v, + Protocol::Udp(v) => *v, + Protocol::Icmp(v) => *v, + } + } + + pub fn with_value(self, value: u16) -> Protocol { + match self { + Protocol::Tcp(_) => Protocol::Tcp(value), + Protocol::Udp(_) => Protocol::Udp(value), + Protocol::Icmp(_) => Protocol::Icmp(value), + } + } +} + #[derive(Debug, PartialEq)] pub enum IpPacket<'a> { Ipv4(Ipv4Packet<'a>), @@ -43,6 +87,22 @@ pub enum IcmpPacket<'a> { Ipv6(icmpv6::Icmpv6Packet<'a>), } +impl<'a> IcmpPacket<'a> { + pub fn identifier(&self) -> Option { + let request_id = self.as_echo_request().map(|r| r.identifier()); + let reply_id = self.as_echo_reply().map(|r| r.identifier()); + + request_id.or(reply_id) + } + + pub fn sequence(&self) -> Option { + let request_id = self.as_echo_request().map(|r| r.sequence()); + let reply_id = self.as_echo_reply().map(|r| r.sequence()); + + request_id.or(reply_id) + } +} + #[derive(Debug, PartialEq)] pub enum IcmpEchoRequest<'a> { Ipv4(icmp::echo_request::EchoRequestPacket<'a>), @@ -57,23 +117,653 @@ pub enum IcmpEchoReply<'a> { #[derive(Debug, PartialEq)] pub enum MutableIpPacket<'a> { - Ipv4(MutableIpv4Packet<'a>), - Ipv6(MutableIpv6Packet<'a>), + Ipv4(ConvertibleIpv4Packet<'a>), + Ipv6(ConvertibleIpv6Packet<'a>), +} + +#[derive(Debug, PartialEq)] +enum MaybeOwned<'a> { + RefMut(&'a mut [u8]), + Owned(Vec), +} + +impl<'a> MaybeOwned<'a> { + fn remove_from_head(self, bytes: usize) -> MaybeOwned<'a> { + match self { + MaybeOwned::RefMut(ref_mut) => MaybeOwned::RefMut(&mut ref_mut[bytes..]), + MaybeOwned::Owned(mut owned) => { + owned.drain(0..bytes); + MaybeOwned::Owned(owned) + } + } + } +} + +impl<'a> Deref for MaybeOwned<'a> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + match self { + MaybeOwned::RefMut(ref_mut) => ref_mut, + MaybeOwned::Owned(owned) => owned, + } + } +} + +impl<'a> DerefMut for MaybeOwned<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + MaybeOwned::RefMut(ref_mut) => ref_mut, + MaybeOwned::Owned(owned) => owned, + } + } +} + +#[derive(Debug, PartialEq)] +pub struct ConvertibleIpv4Packet<'a> { + buf: MaybeOwned<'a>, +} + +impl<'a> ConvertibleIpv4Packet<'a> { + pub fn new(buf: &'a mut [u8]) -> Option> { + MutableIpv4Packet::new(&mut buf[20..])?; + Some(Self { + buf: MaybeOwned::RefMut(buf), + }) + } + + fn owned(mut buf: Vec) -> Option> { + MutableIpv4Packet::new(&mut buf[20..])?; + Some(Self { + buf: MaybeOwned::Owned(buf), + }) + } + + fn as_ipv4(&mut self) -> MutableIpv4Packet { + MutableIpv4Packet::new(&mut self.buf[20..]) + .expect("when constructed we checked that this is some") + } + + pub fn to_immutable(&self) -> Ipv4Packet { + Ipv4Packet::new(&self.buf[20..]).expect("when constructed we checked that this is some") + } + + pub fn get_source(&self) -> Ipv4Addr { + self.to_immutable().get_source() + } + + fn get_destination(&self) -> Ipv4Addr { + self.to_immutable().get_destination() + } + + fn set_source(&mut self, source: Ipv4Addr) { + self.as_ipv4().set_source(source); + } + + fn set_destination(&mut self, destination: Ipv4Addr) { + self.as_ipv4().set_destination(destination); + } + + fn set_checksum(&mut self, checksum: u16) { + self.as_ipv4().set_checksum(checksum); + } + + pub fn set_total_length(&mut self, total_length: u16) { + self.as_ipv4().set_total_length(total_length); + } + + pub fn set_header_length(&mut self, header_length: u8) { + self.as_ipv4().set_header_length(header_length); + } + + fn consume_to_immutable(self) -> Ipv4Packet<'a> { + match self.buf { + MaybeOwned::RefMut(buf) => { + Ipv4Packet::new(&buf[20..]).expect("when constructed we checked that this is some") + } + MaybeOwned::Owned(mut owned) => { + owned.drain(..20); + Ipv4Packet::owned(owned).expect("when constructed we checked that this is some") + } + } + } + + fn consume_to_ipv6( + mut self, + src: Ipv6Addr, + dst: Ipv6Addr, + ) -> Option> { + // First we store the old values before modifying the old packet + let dscp = self.as_ipv4().get_dscp(); + let total_length = self.as_ipv4().get_total_length(); + let header_length = self.header_length(); + let ttl = self.as_ipv4().get_ttl(); + let next_level_protocol = self.as_ipv4().get_next_level_protocol(); + + let mut buf = self.buf.remove_from_head(header_length - 20); + buf[0..40].fill(0); + let mut pkt = ConvertibleIpv6Packet { buf }; + + // 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. + + // Version: 6 + pkt.as_ipv6().set_version(6); + + // 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 + pkt.as_ipv6().set_traffic_class(dscp); + + // Flow Label: 0 (all zero bits) + pkt.as_ipv6().set_flow_label(0); + + // Payload Length: Total length value from the IPv4 header, minus the + // size of the IPv4 header and IPv4 options, if present. + pkt.as_ipv6() + .set_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. + let mut pkt = if next_level_protocol == IpNextHeaderProtocols::Icmp { + let mut pkt = pkt.update_icmpv4_header_to_icmpv6()?; + pkt.as_ipv6().set_next_header(IpNextHeaderProtocols::Icmpv6); + pkt + } else { + pkt.as_ipv6().set_next_header(next_level_protocol); + pkt + }; + + // 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 + pkt.as_ipv6().set_hop_limit(ttl); + + // 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 + pkt.set_source(src); + + // 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]. + pkt.set_destination(dst); + + Some(pkt) + } + + fn update_icmpv6_header_to_icmpv4(mut self) -> Option> { + let Some(mut icmp) = MutableIcmpv6Packet::new(self.payload_mut()) else { + return Some(self); + }; + // ICMPv6 informational messages: + + match icmp.get_icmpv6_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. + Icmpv6Types::EchoRequest => { + icmp.set_icmpv6_type(Icmpv6Type(8)); + } + Icmpv6Types::EchoReply => { + icmp.set_icmpv6_type(Icmpv6Type(0)); + } + // 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. + // (TODO:)ICMPv6 error messages: + _ => return None, + } + + Some(self) + } + + fn header_length(&self) -> usize { + self.to_immutable().packet_size() - self.to_immutable().payload().len() + } +} + +impl<'a> Packet for ConvertibleIpv4Packet<'a> { + fn packet(&self) -> &[u8] { + &self.buf[20..] + } + + fn payload(&self) -> &[u8] { + &self.buf[(self.header_length() + 20)..] + } +} + +impl<'a> MutablePacket for ConvertibleIpv4Packet<'a> { + fn packet_mut(&mut self) -> &mut [u8] { + &mut self.buf[20..] + } + + fn payload_mut(&mut self) -> &mut [u8] { + let header_len = self.header_length(); + &mut self.buf[(header_len + 20)..] + } +} + +#[derive(Debug, PartialEq)] +pub struct ConvertibleIpv6Packet<'a> { + buf: MaybeOwned<'a>, +} + +impl<'a> ConvertibleIpv6Packet<'a> { + pub fn new(buf: &'a mut [u8]) -> Option> { + MutableIpv6Packet::new(buf)?; + Some(Self { + buf: MaybeOwned::RefMut(buf), + }) + } + + fn owned(mut buf: Vec) -> Option> { + MutableIpv6Packet::new(&mut buf)?; + Some(Self { + buf: MaybeOwned::Owned(buf), + }) + } + + fn as_ipv6(&mut self) -> MutableIpv6Packet { + MutableIpv6Packet::new(&mut self.buf) + .expect("when constructed we checked that this is some") + } + + fn to_immutable(&self) -> Ipv6Packet { + Ipv6Packet::new(&self.buf).expect("when constructed we checked that this is some") + } + + pub fn get_source(&self) -> Ipv6Addr { + self.to_immutable().get_source() + } + + fn get_destination(&self) -> Ipv6Addr { + self.to_immutable().get_destination() + } + + fn set_source(&mut self, source: Ipv6Addr) { + self.as_ipv6().set_source(source); + } + + fn set_destination(&mut self, destination: Ipv6Addr) { + self.as_ipv6().set_destination(destination); + } + + pub fn set_payload_length(&mut self, payload_length: u16) { + self.as_ipv6().set_payload_length(payload_length); + } + + fn consume_to_immutable(self) -> Ipv6Packet<'a> { + match self.buf { + MaybeOwned::RefMut(buf) => { + Ipv6Packet::new(buf).expect("when constructed we checked that this is some") + } + MaybeOwned::Owned(owned) => { + Ipv6Packet::owned(owned).expect("when constructed we checked that this is some") + } + } + } + + fn consume_to_ipv4( + mut self, + src: Ipv4Addr, + dst: Ipv4Addr, + ) -> Option> { + let traffic_class = self.as_ipv6().get_traffic_class(); + let payload_length = self.as_ipv6().get_payload_length(); + let hop_limit = self.as_ipv6().get_hop_limit(); + let next_header = self.as_ipv6().get_next_header(); + + self.buf[..40].fill(0); + let mut pkt = ConvertibleIpv4Packet { buf: self.buf }; + + // 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. + + // Version: 4 + pkt.as_ipv4().set_version(4); + + // Internet Header Length: 5 (no IPv4 options) + pkt.as_ipv4().set_header_length(5); + + // 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. + pkt.as_ipv4().set_dscp(traffic_class); + + // Total Length: Payload length value from the IPv6 header, plus the + // size of the IPv4 header. + pkt.as_ipv4().set_total_length(payload_length + 20); + + // 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. + pkt.as_ipv4().set_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. + pkt.as_ipv4() + .set_flags(Ipv4Flags::DontFragment | !Ipv4Flags::MoreFragments); + + // Fragment Offset: All zeros. + pkt.as_ipv4().set_fragment_offset(0); + + // 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 + pkt.as_ipv4().set_ttl(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. + + let mut pkt = match next_header { + IpNextHeaderProtocols::Ipv6Frag => { + // TODO: + return None; + } + IpNextHeaderProtocols::Icmpv6 => { + let mut pkt = pkt.update_icmpv6_header_to_icmpv4()?; + pkt.as_ipv4() + .set_next_level_protocol(IpNextHeaderProtocols::Icmp); + pkt + } + IpNextHeaderProtocols::Hopopt + | IpNextHeaderProtocols::Ipv6Route + | IpNextHeaderProtocols::Ipv6Opts => { + return None; + } + proto => { + pkt.as_ipv4().set_next_level_protocol(proto); + pkt + } + }; + + // Header Checksum: Computed once the IPv4 header has been created. + + // 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. + pkt.as_ipv4().set_source(src); + + // 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. + pkt.as_ipv4().set_destination(dst); + + // 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. + + Some(pkt) + } + + fn update_icmpv4_header_to_icmpv6(mut self) -> Option> { + let Some(mut icmp) = MutableIcmpPacket::new(self.payload_mut()) else { + return Some(self); + }; + + // 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: + + match icmp.get_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. + IcmpTypes::EchoRequest => { + icmp.set_icmp_type(icmp::IcmpType(128)); + } + IcmpTypes::EchoReply => { + icmp.set_icmp_type(icmp::IcmpType(129)); + } + // 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. + IcmpTypes::TimeExceeded => { + icmp.set_icmp_type(icmp::IcmpType(3)); + } + // (TODO) 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. + + // Information Request/Reply (Type 15 and Type 16): Obsoleted in + // ICMPv6. Silently drop. + // Timestamp and Timestamp Reply (Type 13 and Type 14): Obsoleted in + // ICMPv6. Silently drop. + // Address Mask Request/Reply (Type 17 and Type 18): Obsoleted in + // ICMPv6. Silently drop. + // ICMP Router Advertisement (Type 9): Single-hop message. Silently + // drop. + // ICMP Router Solicitation (Type 10): Single-hop message. Silently + // drop. + // 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. + // Redirect (Type 5): Single-hop message. Silently drop. + // Alternative Host Address (Type 6): Silently drop. + // Source Quench (Type 4): Obsoleted in ICMPv6. Silently drop. + _ => { + return None; + } + } + + Some(self) + } +} + +impl<'a> Packet for ConvertibleIpv6Packet<'a> { + fn packet(&self) -> &[u8] { + &self.buf + } + + fn payload(&self) -> &[u8] { + let header_len = + self.to_immutable().packet_size() - self.to_immutable().get_payload_length() as usize; + &self.buf[header_len..] + } +} + +impl<'a> MutablePacket for ConvertibleIpv6Packet<'a> { + fn packet_mut(&mut self) -> &mut [u8] { + &mut self.buf + } + + fn payload_mut(&mut self) -> &mut [u8] { + let header_len = + self.to_immutable().packet_size() - self.to_immutable().get_payload_length() as usize; + &mut self.buf[header_len..] + } +} + +pub fn ipv4_embedded(ip: Ipv4Addr) -> Ipv6Addr { + Ipv6Addr::new( + 0x64, + 0xff9b, + 0x00, + 0x00, + 0x00, + 0x00, + u16::from_be_bytes([ip.octets()[0], ip.octets()[1]]), + u16::from_be_bytes([ip.octets()[2], ip.octets()[3]]), + ) +} + +pub fn ipv6_translated(ip: Ipv6Addr) -> Option { + if ip.segments()[0] != 0x64 + || ip.segments()[1] != 0xff9b + || ip.segments()[2] != 0 + || ip.segments()[3] != 0 + || ip.segments()[4] != 0 + || ip.segments()[5] != 0 + { + return None; + } + + Some(Ipv4Addr::new( + ip.octets()[12], + ip.octets()[13], + ip.octets()[14], + ip.octets()[15], + )) } impl<'a> MutableIpPacket<'a> { + // TODO: this API is a bit akward, since you have to pass the extra prepended 20 bytes pub fn new(buf: &'a mut [u8]) -> Option { - match buf[0] >> 4 { - 4 => Some(MutableIpPacket::Ipv4(MutableIpv4Packet::new(buf)?)), - 6 => Some(MutableIpPacket::Ipv6(MutableIpv6Packet::new(buf)?)), + match buf[20] >> 4 { + 4 => Some(MutableIpPacket::Ipv4(ConvertibleIpv4Packet::new(buf)?)), + 6 => Some(MutableIpPacket::Ipv6(ConvertibleIpv6Packet::new( + &mut buf[20..], + )?)), _ => None, } } - pub fn owned(data: Vec) -> Option> { - let packet = match data[0] >> 4 { - 4 => MutableIpv4Packet::owned(data)?.into(), - 6 => MutableIpv6Packet::owned(data)?.into(), + pub fn owned(mut data: Vec) -> Option> { + let packet = match data[20] >> 4 { + 4 => ConvertibleIpv4Packet::owned(data)?.into(), + 6 => { + data.drain(0..20); + ConvertibleIpv6Packet::owned(data)?.into() + } _ => return None, }; @@ -82,10 +772,10 @@ impl<'a> MutableIpPacket<'a> { pub fn to_owned(&self) -> MutableIpPacket<'static> { match self { - MutableIpPacket::Ipv4(i) => MutableIpv4Packet::owned(i.packet().to_vec()) + MutableIpPacket::Ipv4(i) => ConvertibleIpv4Packet::owned(i.buf.to_vec()) .expect("owned packet should still be valid") .into(), - MutableIpPacket::Ipv6(i) => MutableIpv6Packet::owned(i.packet().to_vec()) + MutableIpPacket::Ipv6(i) => ConvertibleIpv6Packet::owned(i.buf.to_vec()) .expect("owned packet should still be valid") .into(), } @@ -95,6 +785,24 @@ impl<'a> MutableIpPacket<'a> { for_both!(self, |i| i.to_immutable().into()) } + fn consume_to_ipv4(self, src: Ipv4Addr, dst: Ipv4Addr) -> Option> { + match self { + MutableIpPacket::Ipv4(pkt) => Some(MutableIpPacket::Ipv4(pkt)), + MutableIpPacket::Ipv6(pkt) => { + Some(MutableIpPacket::Ipv4(pkt.consume_to_ipv4(src, dst)?)) + } + } + } + + fn consume_to_ipv6(self, src: Ipv6Addr, dst: Ipv6Addr) -> Option> { + match self { + MutableIpPacket::Ipv4(pkt) => { + Some(MutableIpPacket::Ipv6(pkt.consume_to_ipv6(src, dst)?)) + } + MutableIpPacket::Ipv6(pkt) => Some(MutableIpPacket::Ipv6(pkt)), + } + } + pub fn source(&self) -> IpAddr { for_both!(self, |i| i.get_source().into()) } @@ -103,10 +811,73 @@ impl<'a> MutableIpPacket<'a> { for_both!(self, |i| i.get_destination().into()) } + pub fn set_source_protocol(&mut self, v: u16) { + if let Some(mut p) = self.as_tcp() { + p.set_source(v); + } + + if let Some(mut p) = self.as_udp() { + p.set_source(v); + } + + self.set_icmp_identifier(v); + } + + pub fn set_destination_protocol(&mut self, v: u16) { + if let Some(mut p) = self.as_tcp() { + p.set_destination(v); + } + + if let Some(mut p) = self.as_udp() { + p.set_destination(v); + } + + self.set_icmp_identifier(v); + } + + fn set_icmp_identifier(&mut self, v: u16) { + if let Some(mut p) = self.as_icmp() { + if p.get_icmp_type() == IcmpTypes::EchoReply { + let Some(mut echo_reply) = MutableEchoReplyPacket::new(p.packet_mut()) else { + return; + }; + echo_reply.set_identifier(v) + } + + if p.get_icmp_type() == IcmpTypes::EchoRequest { + let Some(mut echo_request) = MutableEchoRequestPacket::new(p.packet_mut()) else { + return; + }; + echo_request.set_identifier(v); + } + } + + if let Some(mut p) = self.as_icmpv6() { + if p.get_icmpv6_type() == Icmpv6Types::EchoReply { + let Some(mut echo_reply) = + icmpv6::echo_reply::MutableEchoReplyPacket::new(p.packet_mut()) + else { + return; + }; + echo_reply.set_identifier(v) + } + + if p.get_icmpv6_type() == Icmpv6Types::EchoRequest { + let Some(mut echo_request) = + icmpv6::echo_request::MutableEchoRequestPacket::new(p.packet_mut()) + else { + return; + }; + echo_request.set_identifier(v); + } + } + } + #[inline] pub fn update_checksum(&mut self) { - // Note: neither ipv6 nor icmp have a checksum. + // Note: ipv6 doesn't have a checksum. self.set_icmpv6_checksum(); + self.set_icmpv4_checksum(); self.set_udp_checksum(); self.set_tcp_checksum(); // Note: Ipv4 checksum should be set after the others, @@ -188,6 +959,20 @@ impl<'a> MutableIpPacket<'a> { } } + fn set_icmpv4_checksum(&mut self) { + if let Some(mut pkt) = self.as_icmp() { + let checksum = icmp::checksum(&pkt.to_immutable()); + pkt.set_checksum(checksum); + } + } + + fn as_icmp(&mut self) -> Option { + self.to_immutable() + .is_icmp() + .then(|| MutableIcmpPacket::new(self.payload_mut())) + .flatten() + } + fn as_icmpv6(&mut self) -> Option { self.to_immutable() .is_icmpv6() @@ -220,6 +1005,38 @@ impl<'a> MutableIpPacket<'a> { } } + pub fn translate_destination( + mut self, + src_v4: Ipv4Addr, + src_v6: Ipv6Addr, + dst: IpAddr, + ) -> Option> { + match (&self, dst) { + (&MutableIpPacket::Ipv4(_), IpAddr::V6(dst)) => self.consume_to_ipv6(src_v6, dst), + (&MutableIpPacket::Ipv6(_), IpAddr::V4(dst)) => self.consume_to_ipv4(src_v4, dst), + _ => { + self.set_dst(dst); + Some(self) + } + } + } + + pub fn translate_source( + mut self, + dst_v4: Ipv4Addr, + dst_v6: Ipv6Addr, + src: IpAddr, + ) -> Option> { + match (&self, src) { + (&MutableIpPacket::Ipv4(_), IpAddr::V6(src)) => self.consume_to_ipv6(src, dst_v6), + (&MutableIpPacket::Ipv6(_), IpAddr::V4(src)) => self.consume_to_ipv4(src, dst_v4), + _ => { + self.set_src(src); + Some(self) + } + } + } + #[inline] pub fn set_dst(&mut self, dst: IpAddr) { match (self, dst) { @@ -259,6 +1076,46 @@ impl<'a> IpPacket<'a> { } } + pub fn source_protocol(&self) -> Result { + if let Some(p) = self.as_tcp() { + return Ok(Protocol::Tcp(p.get_source())); + } + + if let Some(p) = self.as_udp() { + return Ok(Protocol::Udp(p.get_source())); + } + + if let Some(p) = self.as_icmp() { + let id = p + .identifier() + .ok_or(UnsupportedProtocol(self.next_header()))?; + + return Ok(Protocol::Icmp(id)); + } + + Err(UnsupportedProtocol(self.next_header())) + } + + pub fn destination_protocol(&self) -> Result { + if let Some(p) = self.as_tcp() { + return Ok(Protocol::Tcp(p.get_destination())); + } + + if let Some(p) = self.as_udp() { + return Ok(Protocol::Udp(p.get_destination())); + } + + if let Some(p) = self.as_icmp() { + let id = p + .identifier() + .ok_or(UnsupportedProtocol(self.next_header()))?; + + return Ok(Protocol::Icmp(id)); + } + + Err(UnsupportedProtocol(self.next_header())) + } + pub fn source(&self) -> IpAddr { for_both!(self, |i| i.get_source().into()) } @@ -289,10 +1146,6 @@ impl<'a> IpPacket<'a> { Some(packet) } - pub fn is_icmpv6(&self) -> bool { - self.next_header() == IpNextHeaderProtocols::Icmpv6 - } - pub fn next_header(&self) -> IpNextHeaderProtocol { match self { Self::Ipv4(p) => p.get_next_level_protocol(), @@ -308,6 +1161,14 @@ impl<'a> IpPacket<'a> { self.next_header() == IpNextHeaderProtocols::Tcp } + fn is_icmp(&self) -> bool { + self.next_header() == IpNextHeaderProtocols::Icmp + } + + fn is_icmpv6(&self) -> bool { + self.next_header() == IpNextHeaderProtocols::Icmpv6 + } + pub fn as_udp(&self) -> Option { self.is_udp() .then(|| UdpPacket::new(self.payload())) @@ -403,6 +1264,21 @@ impl<'a> IcmpPacket<'a> { IcmpPacket::Ipv4(_) | IcmpPacket::Ipv6(_) => None, } } + + pub fn is_echo_reply(&self) -> bool { + self.as_echo_reply().is_some() + } + + pub fn is_echo_request(&self) -> bool { + self.as_echo_request().is_some() + } + + pub fn checksum(&self) -> u16 { + match self { + IcmpPacket::Ipv4(p) => p.get_checksum(), + IcmpPacket::Ipv6(p) => p.get_checksum(), + } + } } impl<'a> IcmpEchoRequest<'a> { @@ -446,14 +1322,14 @@ impl<'a> From> for IpPacket<'a> { } } -impl<'a> From> for MutableIpPacket<'a> { - fn from(value: MutableIpv4Packet<'a>) -> Self { +impl<'a> From> for MutableIpPacket<'a> { + fn from(value: ConvertibleIpv4Packet<'a>) -> Self { Self::Ipv4(value) } } -impl<'a> From> for MutableIpPacket<'a> { - fn from(value: MutableIpv6Packet<'a>) -> Self { +impl<'a> From> for MutableIpPacket<'a> { + fn from(value: ConvertibleIpv6Packet<'a>) -> Self { Self::Ipv6(value) } } @@ -496,3 +1372,7 @@ impl<'a> PacketSize for IpPacket<'a> { } } } + +#[derive(Debug, thiserror::Error)] +#[error("Unsupported IP protocol: {0}")] +pub struct UnsupportedProtocol(IpNextHeaderProtocol); diff --git a/rust/ip-packet/src/make.rs b/rust/ip-packet/src/make.rs index e8e9ae478..64fbe24a2 100644 --- a/rust/ip-packet/src/make.rs +++ b/rust/ip-packet/src/make.rs @@ -7,7 +7,7 @@ use hickory_proto::{ }; use pnet_packet::{ ip::IpNextHeaderProtocol, - ipv4::MutableIpv4Packet, + ipv4::{Ipv4Flags, MutableIpv4Packet}, ipv6::MutableIpv6Packet, tcp::{self, MutableTcpPacket}, udp::{self, MutableUdpPacket}, @@ -23,6 +23,15 @@ pub fn icmp_request_packet( icmp_packet(src, dst.into(), seq, identifier, IcmpKind::Request) } +pub fn icmp_reply_packet( + src: IpAddr, + dst: impl Into, + seq: u16, + identifier: u16, +) -> MutableIpPacket<'static> { + icmp_packet(src, dst.into(), seq, identifier, IcmpKind::Response) +} + pub fn icmp_response_packet(packet: IpPacket<'static>) -> MutableIpPacket<'static> { let icmp = packet .as_icmp() @@ -38,12 +47,66 @@ pub fn icmp_response_packet(packet: IpPacket<'static>) -> MutableIpPacket<'stati ) } -enum IcmpKind { +#[cfg_attr(test, derive(Debug, test_strategy::Arbitrary))] +pub(crate) enum IcmpKind { Request, Response, } -fn icmp_packet( +pub(crate) fn icmp4_packet_with_options( + src: Ipv4Addr, + dst: Ipv4Addr, + seq: u16, + identifier: u16, + kind: IcmpKind, + ip_header_length: u8, +) -> MutableIpPacket<'static> { + use crate::{ + icmp::{ + echo_request::{IcmpCodes, MutableEchoRequestPacket}, + IcmpTypes, MutableIcmpPacket, + }, + ip::IpNextHeaderProtocols, + MutablePacket as _, + }; + + let ip_header_bytes = ip_header_length * 4; + let mut buf = vec![0u8; 60 + ip_header_bytes as usize]; + + ipv4_header( + src, + dst, + IpNextHeaderProtocols::Icmp, + ip_header_length, + &mut buf[20..], + ); + + let mut icmp_packet = + MutableIcmpPacket::new(&mut buf[(20 + ip_header_bytes as usize)..]).unwrap(); + + match kind { + IcmpKind::Request => { + icmp_packet.set_icmp_type(IcmpTypes::EchoRequest); + icmp_packet.set_icmp_code(IcmpCodes::NoCode); + } + IcmpKind::Response => { + icmp_packet.set_icmp_type(IcmpTypes::EchoReply); + icmp_packet.set_icmp_code(IcmpCodes::NoCode); + } + } + + icmp_packet.set_checksum(0); + + let mut echo_request_packet = MutableEchoRequestPacket::new(icmp_packet.packet_mut()).unwrap(); + echo_request_packet.set_sequence_number(seq); + echo_request_packet.set_identifier(identifier); + + let mut result = MutableIpPacket::owned(buf).unwrap(); + result.update_checksum(); + result +} + +pub(crate) fn icmp_packet( src: IpAddr, dst: IpAddr, seq: u16, @@ -52,44 +115,7 @@ fn icmp_packet( ) -> MutableIpPacket<'static> { match (src, dst) { (IpAddr::V4(src), IpAddr::V4(dst)) => { - use crate::{ - icmp::{ - echo_request::{IcmpCodes, MutableEchoRequestPacket}, - IcmpTypes, MutableIcmpPacket, - }, - ip::IpNextHeaderProtocols, - MutablePacket as _, Packet as _, - }; - - let mut buf = vec![0u8; 60]; - - ipv4_header(src, dst, IpNextHeaderProtocols::Icmp, &mut buf[..]); - - let mut icmp_packet = MutableIcmpPacket::new(&mut buf[20..]).unwrap(); - - match kind { - IcmpKind::Request => { - icmp_packet.set_icmp_type(IcmpTypes::EchoRequest); - icmp_packet.set_icmp_code(IcmpCodes::NoCode); - } - IcmpKind::Response => { - icmp_packet.set_icmp_type(IcmpTypes::EchoReply); - icmp_packet.set_icmp_code(IcmpCodes::NoCode); - } - } - - icmp_packet.set_checksum(0); - - let mut echo_request_packet = - MutableEchoRequestPacket::new(icmp_packet.packet_mut()).unwrap(); - echo_request_packet.set_sequence_number(seq); - echo_request_packet.set_identifier(identifier); - echo_request_packet.set_checksum(crate::util::checksum( - echo_request_packet.to_immutable().packet(), - 2, - )); - - MutableIpPacket::owned(buf).unwrap() + icmp4_packet_with_options(src, dst, seq, identifier, kind, 5) } (IpAddr::V6(src), IpAddr::V6(dst)) => { use crate::{ @@ -101,11 +127,11 @@ fn icmp_packet( MutablePacket as _, }; - let mut buf = vec![0u8; 128]; + let mut buf = vec![0u8; 128 + 20]; - ipv6_header(src, dst, IpNextHeaderProtocols::Icmpv6, &mut buf); + ipv6_header(src, dst, IpNextHeaderProtocols::Icmpv6, &mut buf[20..]); - let mut icmp_packet = MutableIcmpv6Packet::new(&mut buf[40..]).unwrap(); + let mut icmp_packet = MutableIcmpv6Packet::new(&mut buf[60..]).unwrap(); match kind { IcmpKind::Request => { @@ -124,12 +150,9 @@ fn icmp_packet( echo_request_packet.set_sequence_number(seq); echo_request_packet.set_checksum(0); - let checksum = crate::icmpv6::checksum(&icmp_packet.to_immutable(), &src, &dst); - MutableEchoRequestPacket::new(icmp_packet.packet_mut()) - .unwrap() - .set_checksum(checksum); - - MutableIpPacket::owned(buf).unwrap() + let mut result = MutableIpPacket::owned(buf).unwrap(); + result.update_checksum(); + result } (IpAddr::V6(_), IpAddr::V4(_)) | (IpAddr::V4(_), IpAddr::V6(_)) => { panic!("IPs must be of the same version") @@ -148,22 +171,23 @@ pub fn tcp_packet( (IpAddr::V4(src), IpAddr::V4(dst)) => { use crate::ip::IpNextHeaderProtocols; - let len = 20 + 20 + payload.len(); + let len = 20 + 20 + payload.len() + 20; + let mut buf = vec![0u8; len]; - ipv4_header(src, dst, IpNextHeaderProtocols::Tcp, &mut buf); + ipv4_header(src, dst, IpNextHeaderProtocols::Tcp, 5, &mut buf[20..]); - tcp_header(saddr, daddr, sport, dport, &payload, &mut buf[20..]); + tcp_header(saddr, daddr, sport, dport, &payload, &mut buf[40..]); MutableIpPacket::owned(buf).unwrap() } (IpAddr::V6(src), IpAddr::V6(dst)) => { use crate::ip::IpNextHeaderProtocols; - let mut buf = vec![0u8; 40 + 20 + payload.len()]; + let mut buf = vec![0u8; 40 + 20 + payload.len() + 20]; - ipv6_header(src, dst, IpNextHeaderProtocols::Tcp, &mut buf); + ipv6_header(src, dst, IpNextHeaderProtocols::Tcp, &mut buf[20..]); - tcp_header(saddr, daddr, sport, dport, &payload, &mut buf[40..]); + tcp_header(saddr, daddr, sport, dport, &payload, &mut buf[60..]); MutableIpPacket::owned(buf).unwrap() } (IpAddr::V6(_), IpAddr::V4(_)) | (IpAddr::V4(_), IpAddr::V6(_)) => { @@ -183,22 +207,22 @@ pub fn udp_packet( (IpAddr::V4(src), IpAddr::V4(dst)) => { use crate::ip::IpNextHeaderProtocols; - let len = 20 + 8 + payload.len(); + let len = 20 + 8 + payload.len() + 20; let mut buf = vec![0u8; len]; - ipv4_header(src, dst, IpNextHeaderProtocols::Udp, &mut buf); + ipv4_header(src, dst, IpNextHeaderProtocols::Udp, 5, &mut buf[20..]); - udp_header(saddr, daddr, sport, dport, &payload, &mut buf[20..]); + udp_header(saddr, daddr, sport, dport, &payload, &mut buf[40..]); MutableIpPacket::owned(buf).unwrap() } (IpAddr::V6(src), IpAddr::V6(dst)) => { use crate::ip::IpNextHeaderProtocols; - let mut buf = vec![0u8; 40 + 8 + payload.len()]; + let mut buf = vec![0u8; 40 + 8 + payload.len() + 20]; - ipv6_header(src, dst, IpNextHeaderProtocols::Udp, &mut buf); + ipv6_header(src, dst, IpNextHeaderProtocols::Udp, &mut buf[20..]); - udp_header(saddr, daddr, sport, dport, &payload, &mut buf[40..]); + udp_header(saddr, daddr, sport, dport, &payload, &mut buf[60..]); MutableIpPacket::owned(buf).unwrap() } (IpAddr::V6(_), IpAddr::V4(_)) | (IpAddr::V4(_), IpAddr::V6(_)) => { @@ -297,11 +321,25 @@ pub fn dns_err_response(packet: IpPacket<'static>, code: ResponseCode) -> Mutabl ) } -fn ipv4_header(src: Ipv4Addr, dst: Ipv4Addr, proto: IpNextHeaderProtocol, buf: &mut [u8]) { +fn ipv4_header( + src: Ipv4Addr, + dst: Ipv4Addr, + proto: IpNextHeaderProtocol, + // We allow setting the ip header length as a way to emulate ip options without having to set ip options + ip_header_length: u8, + buf: &mut [u8], +) { + assert!(ip_header_length >= 5); + assert!(ip_header_length <= 16); let len = buf.len(); let mut ipv4_packet = MutableIpv4Packet::new(buf).unwrap(); ipv4_packet.set_version(4); - ipv4_packet.set_header_length(5); + + // TODO: packet conversion always set the flags like this. + // we still need to support fragmented packets for translated packet properly + ipv4_packet.set_flags(Ipv4Flags::DontFragment | !Ipv4Flags::MoreFragments); + + ipv4_packet.set_header_length(ip_header_length); ipv4_packet.set_total_length(len as u16); ipv4_packet.set_ttl(64); ipv4_packet.set_next_level_protocol(proto); diff --git a/rust/ip-packet/src/proptests.rs b/rust/ip-packet/src/proptests.rs new file mode 100644 index 000000000..5bc943d94 --- /dev/null +++ b/rust/ip-packet/src/proptests.rs @@ -0,0 +1,233 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use pnet_packet::Packet; +use proptest::arbitrary::any; +use proptest::prop_oneof; +use proptest::strategy::Strategy; + +use crate::make::{icmp4_packet_with_options, icmp_packet, tcp_packet, udp_packet, IcmpKind}; +use crate::MutableIpPacket; + +fn tcp_packet_v4() -> impl Strategy> { + ( + any::(), + any::(), + any::(), + any::(), + any::>(), + ) + .prop_map(|(src, dst, sport, dport, payload)| { + tcp_packet(src.into(), dst.into(), sport, dport, payload) + }) +} + +fn tcp_packet_v6() -> impl Strategy> { + ( + any::(), + any::(), + any::(), + any::(), + any::>(), + ) + .prop_map(|(src, dst, sport, dport, payload)| { + tcp_packet(src.into(), dst.into(), sport, dport, payload) + }) +} + +fn udp_packet_v4() -> impl Strategy> { + ( + any::(), + any::(), + any::(), + any::(), + any::>(), + ) + .prop_map(|(src, dst, sport, dport, payload)| { + udp_packet(src.into(), dst.into(), sport, dport, payload) + }) +} + +fn udp_packet_v6() -> impl Strategy> { + ( + any::(), + any::(), + any::(), + any::(), + any::>(), + ) + .prop_map(|(src, dst, sport, dport, payload)| { + udp_packet(src.into(), dst.into(), sport, dport, payload) + }) +} + +fn icmp_packet_v4() -> impl Strategy> { + ( + any::(), + any::(), + any::(), + any::(), + any::(), + ) + .prop_map(|(src, dst, id, seq, kind)| icmp_packet(src.into(), dst.into(), id, seq, kind)) +} + +fn icmp_packet_v4_header_options() -> impl Strategy> { + ( + any::(), + any::(), + any::(), + any::(), + any::(), + (5u8..15), + ) + .prop_map(|(src, dst, id, seq, kind, header_length)| { + icmp4_packet_with_options(src, dst, id, seq, kind, header_length) + }) +} + +fn icmp_packet_v6() -> impl Strategy> { + ( + any::(), + any::(), + any::(), + any::(), + any::(), + ) + .prop_map(|(src, dst, id, seq, kind)| icmp_packet(src.into(), dst.into(), id, seq, kind)) +} + +fn packet() -> impl Strategy> { + prop_oneof![ + tcp_packet_v4(), + tcp_packet_v6(), + udp_packet_v4(), + udp_packet_v6(), + icmp_packet_v4(), + icmp_packet_v6(), + ] +} + +#[test_strategy::proptest()] +fn can_translate_dst_packet_back_and_forth( + #[strategy(packet())] packet: MutableIpPacket<'static>, + #[strategy(any::())] src_v4: Ipv4Addr, + #[strategy(any::())] src_v6: Ipv6Addr, + #[strategy(any::())] dst: IpAddr, +) { + let original_source = packet.source(); + let original_destination = packet.destination(); + let original_packet = packet.packet().to_vec(); + + let original_source_v4 = if let IpAddr::V4(v4) = original_source { + v4 + } else { + Ipv4Addr::UNSPECIFIED + }; + let original_source_v6 = if let IpAddr::V6(v6) = original_source { + v6 + } else { + Ipv6Addr::UNSPECIFIED + }; + + let packet = packet.translate_destination(src_v4, src_v6, dst).unwrap(); + + assert!(packet.source() == IpAddr::from(src_v4) || packet.source() == IpAddr::from(src_v6) || packet.source() == original_source, "either the translated packet was set to one of the sources or it wasn't translated and it kept the old source"); + assert_eq!(packet.destination(), dst); + + let mut packet = packet + .translate_destination(original_source_v4, original_source_v6, original_destination) + .unwrap(); + packet.update_checksum(); + + assert_eq!(packet.packet(), original_packet); +} + +#[test_strategy::proptest()] +fn can_translate_src_packet_back_and_forth( + #[strategy(packet())] packet: MutableIpPacket<'static>, + #[strategy(any::())] dst_v4: Ipv4Addr, + #[strategy(any::())] dst_v6: Ipv6Addr, + #[strategy(any::())] src: IpAddr, +) { + let original_source = packet.source(); + let original_destination = packet.destination(); + let original_packet = packet.packet().to_vec(); + + let original_destination_v4 = if let IpAddr::V4(v4) = original_destination { + v4 + } else { + Ipv4Addr::UNSPECIFIED + }; + let original_destination_v6 = if let IpAddr::V6(v6) = original_destination { + v6 + } else { + Ipv6Addr::UNSPECIFIED + }; + + let packet = packet.translate_source(dst_v4, dst_v6, src).unwrap(); + + assert!(packet.destination() == IpAddr::from(dst_v4) || packet.destination() == IpAddr::from(dst_v6) || packet.destination() == original_destination, "either the translated packet was set to one of the destinations or it wasn't translated and it kept the old destination"); + assert_eq!(packet.source(), src); + + let mut packet = packet + .translate_source( + original_destination_v4, + original_destination_v6, + original_source, + ) + .unwrap(); + packet.update_checksum(); + + assert_eq!(packet.packet(), original_packet); +} + +#[test_strategy::proptest()] +fn can_translate_dst_packet_with_options( + #[strategy(icmp_packet_v4_header_options())] packet: MutableIpPacket<'static>, + #[strategy(any::())] src_v4: Ipv4Addr, + #[strategy(any::())] src_v6: Ipv6Addr, + #[strategy(any::())] dst: IpAddr, +) { + let source_protocol = packet.to_immutable().source_protocol().unwrap(); + let destination_protocol = packet.to_immutable().destination_protocol().unwrap(); + let source = packet.source(); + let sequence = packet.to_immutable().as_icmp().and_then(|i| i.sequence()); + let identifier = packet.to_immutable().as_icmp().and_then(|i| i.identifier()); + + let packet = packet.translate_destination(src_v4, src_v6, dst).unwrap(); + let packet = packet.to_immutable().to_owned(); + let icmp = packet.as_icmp().unwrap(); + + assert!(packet.source() == IpAddr::from(src_v4) || packet.source() == IpAddr::from(src_v6) || packet.source() == source, "either the translated packet was set to one of the sources or it wasn't translated and it kept the old source"); + assert_eq!(packet.destination(), dst); + assert_eq!(source_protocol, packet.source_protocol().unwrap()); + assert_eq!(destination_protocol, packet.destination_protocol().unwrap()); + + assert_eq!(sequence, icmp.sequence()); + assert_eq!(identifier, icmp.identifier()); +} +#[test_strategy::proptest()] +fn can_translate_src_packet_with_options( + #[strategy(icmp_packet_v4_header_options())] packet: MutableIpPacket<'static>, + #[strategy(any::())] dst_v4: Ipv4Addr, + #[strategy(any::())] dst_v6: Ipv6Addr, + #[strategy(any::())] src: IpAddr, +) { + let source_protocol = packet.to_immutable().source_protocol().unwrap(); + let destination_protocol = packet.to_immutable().destination_protocol().unwrap(); + let destination = packet.destination(); + let sequence = packet.to_immutable().as_icmp().and_then(|i| i.sequence()); + let identifier = packet.to_immutable().as_icmp().and_then(|i| i.identifier()); + + let packet = packet.translate_source(dst_v4, dst_v6, src).unwrap(); + let packet = packet.to_immutable().to_owned(); + let icmp = packet.as_icmp().unwrap(); + + assert!(packet.destination() == IpAddr::from(dst_v4) || packet.destination() == IpAddr::from(dst_v6) || packet.destination() == destination, "either the translated packet was set to one of the destinations or it wasn't translated and it kept the old destination"); + assert_eq!(packet.source(), src); + assert_eq!(source_protocol, packet.source_protocol().unwrap()); + assert_eq!(destination_protocol, packet.destination_protocol().unwrap()); + + assert_eq!(sequence, icmp.sequence()); + assert_eq!(identifier, icmp.identifier()); +}