diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0eef518 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "rtnetlink" + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 9dbce6f..cab62a0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,8 @@ colored = "2.0.4" tun-tap = "0.1.3" bimap = "0.6.3" pnet_packet = "0.33.0" -# etherparse = "0.13.0" +rtnetlink = "0.13.0" +futures = "0.3.28" [[bin]] name = "protomask" diff --git a/debug.sh b/debug.sh index ae42de0..0114eda 100644 --- a/debug.sh +++ b/debug.sh @@ -13,6 +13,11 @@ ip netns exec protomask ip link add test2 type dummy ip netns exec protomask ip link set test2 up ip netns exec protomask ip addr add 172.16.10.2 dev test2 +# Turn off the firewall for the test interfaces +ip netns exec protomask firewall-cmd --zone=trusted --add-interface=nat64i0 +ip netns exec protomask firewall-cmd --zone=trusted --add-interface=test1 +ip netns exec protomask firewall-cmd --zone=trusted --add-interface=test2 + # Run protomask ip netns exec protomask ./target/debug/protomask protomask.toml -v diff --git a/src/main.rs b/src/main.rs index fbd4ad6..c052753 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ use clap::Parser; use colored::Colorize; use config::Config; -use nat::Nat64; mod cli; mod config; @@ -13,7 +12,7 @@ pub async fn main() { // Parse CLI args let args = cli::Args::parse(); - // Set up logging + // Set up logging fern::Dispatch::new() .format(|out, message, record| { out.finish(format_args!( @@ -42,22 +41,20 @@ pub async fn main() { // Parse the config file let config = Config::load(args.config_file).unwrap(); - // Create the NAT64 instance - let mut nat64 = Nat64::new( - config.interface.address_v4, - config.interface.address_v6, - config.interface.pool, - config.interface.prefix, - config - .rules - .static_map - .iter() - .map(|rule| (rule.v4, rule.v6)) - .collect(), - ) - .await - .unwrap(); + // // Create the NAT64 instance + // let mut nat64 = Nat64::new( + // config.interface.pool, + // config.interface.prefix, + // config + // .rules + // .static_map + // .iter() + // .map(|rule| (rule.v4, rule.v6)) + // .collect(), + // ) + // .await + // .unwrap(); - // Handle packets - nat64.run().await.unwrap(); + // // Handle packets + // nat64.run().await.unwrap(); } diff --git a/src/nat/interface.rs b/src/nat/interface.rs new file mode 100644 index 0000000..f9a2be7 --- /dev/null +++ b/src/nat/interface.rs @@ -0,0 +1,93 @@ +use futures::stream::TryStreamExt; +use ipnet::{Ipv4Net, Ipv6Net}; +use tun_tap::{Iface, Mode}; + +#[derive(Debug, thiserror::Error)] +pub enum InterfaceError { + #[error(transparent)] + IoError(#[from] std::io::Error), + #[error(transparent)] + NetlinkError(#[from] rtnetlink::Error), +} + +/// Wrapper around a TUN interface that automatically configures itself +#[derive(Debug)] +pub struct Nat64Interface { + /// Underlying TUN interface + interface: Iface, +} + +impl Nat64Interface { + /// Create a new NAT64 interface + pub async fn new(v6_prefix: Ipv6Net, v4_pool: Vec) -> Result { + // Bring up an rtnetlink connection + let (rt_connection, rt_handle, _) = rtnetlink::new_connection()?; + tokio::spawn(rt_connection); + + // Set up the TUN interface + let interface = Iface::without_packet_info("nat64i%d", Mode::Tun)?; + + // Get access to the new interface through rtnetlink + let interface_link = rt_handle + .link() + .get() + .match_name(interface.name().to_owned()) + .execute() + .try_next() + .await? + .expect("Interface not found even though it was just created"); + + // Bring up the interface + rt_handle + .link() + .set(interface_link.header.index) + .up() + .execute() + .await?; + log::info!("Created interface: {}", interface.name()); + + // Add the v6 prefix as a route + rt_handle + .route() + .add() + .v6() + .destination_prefix(v6_prefix.addr(), v6_prefix.prefix_len()) + .execute() + .await?; + log::info!("Added route: {} via {}", v6_prefix, interface.name()); + + // Add every prefix in the v4 pool as a route + for prefix in v4_pool { + rt_handle + .route() + .add() + .v4() + .destination_prefix(prefix.addr(), prefix.prefix_len()) + .execute() + .await?; + log::info!("Added route: {} via {}", prefix, interface.name()); + } + + Ok(Self { interface }) + } + + /// Get the interface mode + pub fn mode(&self) -> Mode { + self.interface.mode() + } + + /// Get the interface name + pub fn name(&self) -> &str { + self.interface.name() + } + + /// Receive a packet from the interface + pub fn recv(&self, buf: &mut [u8]) -> Result { + self.interface.recv(buf) + } + + /// Send a packet to the interface + pub fn send(&self, buf: &[u8]) -> Result { + self.interface.send(buf) + } +} diff --git a/src/nat/mod.rs b/src/nat/mod.rs index 6e6f50b..d668a91 100644 --- a/src/nat/mod.rs +++ b/src/nat/mod.rs @@ -1,387 +1,4 @@ -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - -use bimap::BiMap; -use colored::Colorize; -use ipnet::{Ipv4Net, Ipv6Net}; -use pnet_packet::{ - icmp::IcmpPacket, - icmpv6::Icmpv6Packet, - ip::IpNextHeaderProtocols, - ipv4::{self, Ipv4Packet, MutableIpv4Packet}, - ipv6::{Ipv6Packet, MutableIpv6Packet}, - Packet, -}; -use tokio::process::Command; -use tun_tap::{Iface, Mode}; - -use crate::nat::packet::{xlat_v6_to_v4, IpPacket}; - -use self::packet::xlat_v4_to_v6; - -mod icmp; -mod packet; - -/// A cleaner way to execute a CLI command -macro_rules! command { - ($cmd:expr, $($arg:expr),*) => {{ - Command::new($cmd) - $(.arg($arg))* - .status() - }} -} - -/// Converts bytes to a hex string for debugging -fn bytes_to_hex_str(bytes: &[u8]) -> String { - bytes - .iter() - .map(|val| format!("{:02x}", val)) - .collect::>() - .join(" ") -} - -pub struct Nat64 { - /// Handle for the Tun interface - interface: Iface, - /// Instance IPv4 address - instance_v4: Ipv4Addr, - /// Instance IPv6 address - instance_v6: Ipv6Addr, - /// IPv4 pool - ipv4_pool: Vec, - /// IPv6 prefix - ipv6_prefix: Ipv6Net, - /// A mapping of currently allocated pool reservations - pool_reservations: BiMap, -} - -impl Nat64 { - /// Bring up a new NAT64 interface - /// - /// **Arguments:** - /// - `nat_v4`: An IPv4 address to assign to this NAT instance for ICMP and other purposes - /// - `nat_v6`: An IPv6 address to assign to this NAT instance for ICMP and other purposes - /// - `ipv4_pool`: A list of IPv4 prefixes to communicate from - /// - `ipv6_prefix`: The IPv6 prefix to listen on (should generally be `64:ff9b::/96`) - pub async fn new( - nat_v4: Ipv4Addr, - nat_v6: Ipv6Addr, - ipv4_pool: Vec, - ipv6_prefix: Ipv6Net, - static_mappings: Vec<(Ipv4Addr, Ipv6Addr)>, - ) -> Result { - // Bring up tun interface - let interface = Iface::without_packet_info("nat64i%d", Mode::Tun)?; - - // Configure the interface - let interface_name = interface.name(); - log::info!("Configuring interface {}", interface_name); - - #[cfg_attr(rustfmt, rustfmt_skip)] - { - // Add the nat addresses - log::debug!("Assigning {} to {}", nat_v4, interface_name); - command!("ip", "address", "add", format!("{}/32", nat_v4), "dev", interface_name).await?; - log::debug!("Assigning {} to {}", nat_v6, interface_name); - command!("ip", "address", "add", format!("{}/128", nat_v6), "dev", interface_name ).await?; - - // Bring up the interface - log::debug!("Bringing up {}", interface_name); - command!("ip", "link", "set", "dev", interface_name, "up").await?; - - // Add route for IPv6 prefix - log::debug!("Adding route {} via {}", ipv6_prefix, interface_name); - command!("ip", "route", "add", ipv6_prefix.to_string(), "dev", interface_name).await?; - - // Configure iptables - log::debug!("Configuring iptables"); - command!("iptables", "-A", "FORWARD", "-i", interface_name, "-j", "ACCEPT").await?; - command!("iptables", "-A", "FORWARD", "-o", interface_name, "-j", "ACCEPT").await?; - command!("ip6tables", "-A", "FORWARD", "-i", interface_name, "-j", "ACCEPT").await?; - command!("ip6tables", "-A", "FORWARD", "-o", interface_name, "-j", "ACCEPT").await?; - } - - // Add every IPv4 prefix to the routing table - for prefix in ipv4_pool.iter() { - log::debug!("Adding route {} via {}", prefix, interface_name); - command!( - "ip", - "route", - "add", - prefix.to_string(), - "dev", - interface_name - ) - .await?; - } - - // Build a reservation list - let mut pool_reservations = BiMap::new(); - for (v4, v6) in static_mappings { - pool_reservations.insert(v4, v6); - } - pool_reservations.insert(nat_v4, nat_v6); - - Ok(Self { - interface, - instance_v4: nat_v4, - instance_v6: nat_v6, - ipv4_pool, - ipv6_prefix, - pool_reservations, - }) - } - - /// Block and run the NAT instance. This will handle all packets - pub async fn run(&mut self) -> Result<(), std::io::Error> { - // Read the interface MTU - let mtu: u16 = - std::fs::read_to_string(format!("/sys/class/net/{}/mtu", self.interface.name())) - .expect("Failed to read interface MTU") - .strip_suffix("\n") - .unwrap() - .parse() - .unwrap(); - - // Allocate a buffer for incoming packets - let mut buffer = vec![0; mtu as usize]; - - log::info!("Translating packets"); - loop { - // Read incoming packet - let len = self.interface.recv(&mut buffer)?; - - // Process the packet - let response = self.process(&buffer[..len]).await?; - - // If there is a response, send it - if let Some(response) = response { - self.interface.send(&response)?; - } - } - } - - /// Internal function that checks if a destination address is allowed to be processed - fn is_dest_allowed(&self, dest: IpAddr) -> bool { - return dest == self.instance_v4 - || dest == self.instance_v6 - || match dest { - IpAddr::V4(addr) => self.ipv4_pool.iter().any(|prefix| prefix.contains(&addr)), - IpAddr::V6(addr) => self.ipv6_prefix.contains(&addr), - }; - } - - /// Calculate a unique IPv4 address inside the pool for a given IPv6 address - fn calculate_ipv4(&self, _addr: Ipv6Addr) -> Option { - // Search the list of possible IPv4 addresses - for prefix in self.ipv4_pool.iter() { - for addr in prefix.hosts() { - // If this address is available, use it - if !self.pool_reservations.contains_left(&addr) { - return Some(addr); - } - } - } - - None - } - - /// Embeds an IPv4 address into an IPv6 address - fn embed_v4_into_v6(&self, addr: Ipv4Addr) -> Ipv6Addr { - let mut octets = [0u8; 16]; - octets[..12].copy_from_slice(&self.ipv6_prefix.network().octets()[..12]); - octets[12..].copy_from_slice(&addr.octets()); - Ipv6Addr::from(octets) - } - - /// Extracts an IPv4 address from an IPv6 address - fn extract_v4_from_v6(&self, addr: Ipv6Addr) -> Ipv4Addr { - let mut octets = [0u8; 4]; - octets.copy_from_slice(&addr.octets()[12..]); - Ipv4Addr::from(octets) - } - - /// Gets or creates a reservation for a given address - fn get_or_create_reservation(&mut self, addr: IpAddr) -> Option { - match addr { - IpAddr::V4(addr) => { - if self.pool_reservations.contains_left(&addr) { - return Some(IpAddr::V6( - *self.pool_reservations.get_by_left(&addr).unwrap(), - )); - } else { - return None; - } - } - IpAddr::V6(addr) => { - // If the address is already reserved, return it - if self.pool_reservations.contains_right(&addr) { - return Some(IpAddr::V4( - *self.pool_reservations.get_by_right(&addr).unwrap(), - )); - } - - // Otherwise, calculate a new address - let new_addr = self.calculate_ipv4(addr)?; - self.pool_reservations.insert(new_addr, addr); - return Some(IpAddr::V4(new_addr)); - } - } - } - - /// Internal function to process an incoming packet. - /// If `Some` is returned, the result is sent back out the interface - async fn process(&mut self, packet: &[u8]) -> Result>, std::io::Error> { - // Parse the packet - let input_packet = IpPacket::new(&packet); - if let None = input_packet { - log::warn!( - "{}", - format!( - "Malformed packet received: version: {}, len: {}", - packet[0] >> 4, - packet.len() - ) - .yellow() - ); - return Ok(None); - } - let input_packet = input_packet.unwrap(); - - // Log some info about the packet - #[cfg_attr(rustfmt, rustfmt_skip)] - { - log::debug!("Processing packet with length: {}", input_packet.len().to_string().bright_cyan()); - log::debug!("> IP Header: {}", bytes_to_hex_str(input_packet.get_header()).bright_cyan()); - log::debug!("> Source: {}", input_packet.get_source().to_string().bright_cyan()); - log::debug!("> Destination: {}", input_packet.get_destination().to_string().bright_cyan()); - log::debug!("> Next Header: {}", input_packet.get_next_header().to_string().bright_cyan()); - } - - // Ignore packets that aren't destined for the NAT instance - if !self.is_dest_allowed(input_packet.get_destination()) { - log::debug!("{}", "Ignoring packet. Invalid destination".yellow()); - return Ok(None); - } - - // Drop packets with 0 TTL - if input_packet.get_ttl() == 0 { - log::debug!("{}", "Ignoring packet. TTL is 0".yellow()); - return Ok(None); - } - - // Handle packet translation - let output_packet = match input_packet { - IpPacket::V4(packet) => { - let new_source = self.embed_v4_into_v6(packet.get_source()); - let new_dest = - self.get_or_create_reservation(std::net::IpAddr::V4(packet.get_destination())); - if let Some(IpAddr::V6(new_dest)) = new_dest { - // Log the new addresses - #[cfg_attr(rustfmt, rustfmt_skip)] - { - log::debug!("> Mapped IPv6 Source: {}", new_source.to_string().bright_cyan()); - log::debug!("> Mapped IPv6 Destination: {}", new_dest.to_string().bright_cyan()); - } - - // Handle inner packet conversion for protocols that don't support both v4 and v6 - if let Some(packet) = - Ipv4Packet::owned(match packet.get_next_level_protocol() { - // ICMP must be translated to ICMPv6 - IpNextHeaderProtocols::Icmp => { - if let Some(new_payload) = icmp::icmp_to_icmpv6( - &IcmpPacket::new(packet.payload()).unwrap(), - &new_source, - &new_dest, - ) { - // Mutate the input packet - let mut packet = - MutableIpv4Packet::owned(packet.packet().to_vec()).unwrap(); - packet.set_next_level_protocol(IpNextHeaderProtocols::Icmpv6); - packet.set_payload(&new_payload.packet().to_vec()); - packet.set_checksum(ipv4::checksum(&packet.to_immutable())); - packet.packet().to_vec() - } else { - return Ok(None); - } - } - - // By default, packets can be directly fed to the next function - _ => packet.packet().to_vec(), - }) - { - // Translate the packet - let translated = xlat_v4_to_v6(&packet, new_source, new_dest, true); - - // Log the translated packet header - log::debug!( - "> Translated Header: {}", - bytes_to_hex_str(&translated[0..40]).bright_cyan() - ); - - // Return the translated packet - translated - } else { - return Ok(None); - } - } else { - return Ok(None); - } - } - IpPacket::V6(packet) => { - let new_source = - self.get_or_create_reservation(std::net::IpAddr::V6(packet.get_source())); - let new_dest = self.extract_v4_from_v6(packet.get_destination()); - if let Some(IpAddr::V4(new_source)) = new_source { - // Log the new addresses - #[cfg_attr(rustfmt, rustfmt_skip)] - { - log::debug!("> Mapped IPv4 Source: {}", new_source.to_string().bright_cyan()); - log::debug!("> Mapped IPv4 Destination: {}", new_dest.to_string().bright_cyan()); - } - - // Handle inner packet conversion for protocols that don't support both v4 and v6 - if let Some(packet) = Ipv6Packet::owned(match packet.get_next_header() { - // ICMPv6 must be translated to ICMP - IpNextHeaderProtocols::Icmpv6 => { - if let Some(new_payload) = - icmp::icmpv6_to_icmp(&Icmpv6Packet::new(packet.payload()).unwrap()) - { - // Mutate the input packet - let mut packet = - MutableIpv6Packet::owned(packet.packet().to_vec()).unwrap(); - packet.set_next_header(IpNextHeaderProtocols::Icmp); - packet.set_payload(&new_payload.packet().to_vec()); - packet.packet().to_vec() - } else { - return Ok(None); - } - } - - // By default, packets can be directly fed to the next function - _ => packet.packet().to_vec(), - }) { - // Translate the packet - let translated = xlat_v6_to_v4(&packet, new_source, new_dest, true); - - // Log the translated packet header - log::debug!( - "> Translated Header: {}", - bytes_to_hex_str(&translated[0..20]).bright_cyan() - ); - - // Return the translated packet - translated - } else { - return Ok(None); - } - } else { - return Ok(None); - } - } - }; - - // Build the response - log::debug!("{}", "Sending translated packet".bright_green()); - return Ok(Some(output_packet)); - } -} +pub mod xlat; +pub mod packet; +pub mod table; +pub mod interface; \ No newline at end of file diff --git a/src/nat/mod.rs.old b/src/nat/mod.rs.old new file mode 100644 index 0000000..c31b46a --- /dev/null +++ b/src/nat/mod.rs.old @@ -0,0 +1,376 @@ +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use bimap::BiMap; +use colored::Colorize; +use ipnet::{Ipv4Net, Ipv6Net}; +use pnet_packet::{ + icmp::IcmpPacket, + icmpv6::Icmpv6Packet, + ip::IpNextHeaderProtocols, + ipv4::{self, Ipv4Packet, MutableIpv4Packet}, + ipv6::{Ipv6Packet, MutableIpv6Packet}, + Packet, +}; +use tokio::process::Command; +use tun_tap::{Iface, Mode}; + +use crate::nat::packet::IpPacket; + +mod xlat; +mod packet; +mod table; + +/// A cleaner way to execute a CLI command +macro_rules! command { + ($cmd:expr, $($arg:expr),*) => {{ + Command::new($cmd) + $(.arg($arg))* + .status() + }} +} + +/// Converts bytes to a hex string for debugging +fn bytes_to_hex_str(bytes: &[u8]) -> String { + bytes + .iter() + .map(|val| format!("{:02x}", val)) + .collect::>() + .join(" ") +} + +pub struct Nat64 { + /// Handle for the Tun interface + interface: Iface, + /// IPv4 pool + ipv4_pool: Vec, + /// IPv6 prefix + ipv6_prefix: Ipv6Net, + /// A mapping of currently allocated pool reservations + pool_reservations: BiMap, +} + +impl Nat64 { + /// Bring up a new NAT64 interface + /// + /// **Arguments:** + /// - `ipv4_pool`: A list of IPv4 prefixes to communicate from + /// - `ipv6_prefix`: The IPv6 prefix to listen on (should generally be `64:ff9b::/96`) + pub async fn new( + ipv4_pool: Vec, + ipv6_prefix: Ipv6Net, + static_mappings: Vec<(Ipv4Addr, Ipv6Addr)>, + ) -> Result { + // Bring up tun interface + let interface = Iface::without_packet_info("nat64i%d", Mode::Tun)?; + + // Configure the interface + let interface_name = interface.name(); + log::info!("Configuring interface {}", interface_name); + + #[cfg_attr(rustfmt, rustfmt_skip)] + { + // Add the nat addresses + log::debug!("Assigning {} to {}", nat_v4, interface_name); + command!("ip", "address", "add", format!("{}/32", nat_v4), "dev", interface_name).await?; + log::debug!("Assigning {} to {}", nat_v6, interface_name); + command!("ip", "address", "add", format!("{}/128", nat_v6), "dev", interface_name ).await?; + + // Bring up the interface + log::debug!("Bringing up {}", interface_name); + command!("ip", "link", "set", "dev", interface_name, "up").await?; + + // Add route for IPv6 prefix + log::debug!("Adding route {} via {}", ipv6_prefix, interface_name); + command!("ip", "route", "add", ipv6_prefix.to_string(), "dev", interface_name).await?; + + // Configure iptables + log::debug!("Configuring iptables"); + command!("iptables", "-A", "FORWARD", "-i", interface_name, "-j", "ACCEPT").await?; + command!("iptables", "-A", "FORWARD", "-o", interface_name, "-j", "ACCEPT").await?; + command!("ip6tables", "-A", "FORWARD", "-i", interface_name, "-j", "ACCEPT").await?; + command!("ip6tables", "-A", "FORWARD", "-o", interface_name, "-j", "ACCEPT").await?; + } + + // Add every IPv4 prefix to the routing table + for prefix in ipv4_pool.iter() { + log::debug!("Adding route {} via {}", prefix, interface_name); + command!( + "ip", + "route", + "add", + prefix.to_string(), + "dev", + interface_name + ) + .await?; + } + + // Build a reservation list + let mut pool_reservations = BiMap::new(); + for (v4, v6) in static_mappings { + pool_reservations.insert(v4, v6); + } + pool_reservations.insert(nat_v4, nat_v6); + + Ok(Self { + interface, + ipv4_pool, + ipv6_prefix, + pool_reservations, + }) + } + + /// Block and run the NAT instance. This will handle all packets + pub async fn run(&mut self) -> Result<(), std::io::Error> { + // Read the interface MTU + let mtu: u16 = + std::fs::read_to_string(format!("/sys/class/net/{}/mtu", self.interface.name())) + .expect("Failed to read interface MTU") + .strip_suffix("\n") + .unwrap() + .parse() + .unwrap(); + + // Allocate a buffer for incoming packets + let mut buffer = vec![0; mtu as usize]; + + log::info!("Translating packets"); + loop { + // Read incoming packet + let len = self.interface.recv(&mut buffer)?; + + // Process the packet + let response = self.process(&buffer[..len]).await?; + + // If there is a response, send it + if let Some(response) = response { + self.interface.send(&response)?; + } + } + } + + /// Internal function that checks if a destination address is allowed to be processed + fn is_dest_allowed(&self, dest: IpAddr) -> bool { + return dest == self.instance_v4 + || dest == self.instance_v6 + || match dest { + IpAddr::V4(addr) => self.ipv4_pool.iter().any(|prefix| prefix.contains(&addr)), + IpAddr::V6(addr) => self.ipv6_prefix.contains(&addr), + }; + } + + /// Calculate a unique IPv4 address inside the pool for a given IPv6 address + fn calculate_ipv4(&self, _addr: Ipv6Addr) -> Option { + // Search the list of possible IPv4 addresses + for prefix in self.ipv4_pool.iter() { + for addr in prefix.hosts() { + // If this address is available, use it + if !self.pool_reservations.contains_left(&addr) { + return Some(addr); + } + } + } + + None + } + + /// Embeds an IPv4 address into an IPv6 address + fn embed_v4_into_v6(&self, addr: Ipv4Addr) -> Ipv6Addr { + let mut octets = [0u8; 16]; + octets[..12].copy_from_slice(&self.ipv6_prefix.network().octets()[..12]); + octets[12..].copy_from_slice(&addr.octets()); + Ipv6Addr::from(octets) + } + + /// Extracts an IPv4 address from an IPv6 address + fn extract_v4_from_v6(&self, addr: Ipv6Addr) -> Ipv4Addr { + let mut octets = [0u8; 4]; + octets.copy_from_slice(&addr.octets()[12..]); + Ipv4Addr::from(octets) + } + + /// Gets or creates a reservation for a given address + fn get_or_create_reservation(&mut self, addr: IpAddr) -> Option { + match addr { + IpAddr::V4(addr) => { + if self.pool_reservations.contains_left(&addr) { + return Some(IpAddr::V6( + *self.pool_reservations.get_by_left(&addr).unwrap(), + )); + } else { + return None; + } + } + IpAddr::V6(addr) => { + // If the address is already reserved, return it + if self.pool_reservations.contains_right(&addr) { + return Some(IpAddr::V4( + *self.pool_reservations.get_by_right(&addr).unwrap(), + )); + } + + // Otherwise, calculate a new address + let new_addr = self.calculate_ipv4(addr)?; + self.pool_reservations.insert(new_addr, addr); + return Some(IpAddr::V4(new_addr)); + } + } + } + + /// Internal function to process an incoming packet. + /// If `Some` is returned, the result is sent back out the interface + async fn process(&mut self, packet: &[u8]) -> Result>, std::io::Error> { + // Parse the packet + let input_packet = IpPacket::new(&packet); + if let None = input_packet { + log::warn!( + "{}", + format!( + "Malformed packet received: version: {}, len: {}", + packet[0] >> 4, + packet.len() + ) + .yellow() + ); + return Ok(None); + } + let input_packet = input_packet.unwrap(); + + // Log some info about the packet + #[cfg_attr(rustfmt, rustfmt_skip)] + { + log::debug!("Processing packet with length: {}", input_packet.len().to_string().bright_cyan()); + log::debug!("> IP Header: {}", bytes_to_hex_str(input_packet.get_header()).bright_cyan()); + log::debug!("> Source: {}", input_packet.get_source().to_string().bright_cyan()); + log::debug!("> Destination: {}", input_packet.get_destination().to_string().bright_cyan()); + log::debug!("> Next Header: {}", input_packet.get_next_header().to_string().bright_cyan()); + } + + // Ignore packets that aren't destined for the NAT instance + if !self.is_dest_allowed(input_packet.get_destination()) { + log::debug!("{}", "Ignoring packet. Invalid destination".yellow()); + return Ok(None); + } + + // Drop packets with 0 TTL + if input_packet.get_ttl() == 0 { + log::debug!("{}", "Ignoring packet. TTL is 0".yellow()); + return Ok(None); + } + + // Handle packet translation + let output_packet = match input_packet { + IpPacket::V4(packet) => { + let new_source = self.embed_v4_into_v6(packet.get_source()); + let new_dest = + self.get_or_create_reservation(std::net::IpAddr::V4(packet.get_destination())); + if let Some(IpAddr::V6(new_dest)) = new_dest { + // Log the new addresses + #[cfg_attr(rustfmt, rustfmt_skip)] + { + log::debug!("> Mapped IPv6 Source: {}", new_source.to_string().bright_cyan()); + log::debug!("> Mapped IPv6 Destination: {}", new_dest.to_string().bright_cyan()); + } + + // Handle inner packet conversion for protocols that don't support both v4 and v6 + if let Some(packet) = + Ipv4Packet::owned(match packet.get_next_level_protocol() { + // ICMP must be translated to ICMPv6 + IpNextHeaderProtocols::Icmp => { + if let Some(new_payload) = xlat::icmp_to_icmpv6( + &IcmpPacket::new(packet.payload()).unwrap(), + &new_source, + &new_dest, + ) { + // Mutate the input packet + let mut packet = + MutableIpv4Packet::owned(packet.packet().to_vec()).unwrap(); + packet.set_next_level_protocol(IpNextHeaderProtocols::Icmpv6); + packet.set_payload(&new_payload.packet().to_vec()); + packet.set_checksum(ipv4::checksum(&packet.to_immutable())); + packet.packet().to_vec() + } else { + return Ok(None); + } + } + + // By default, packets can be directly fed to the next function + _ => packet.packet().to_vec(), + }) + { + // Translate the packet + let translated = xlat::ipv4_to_ipv6(&packet, new_source, new_dest, true); + + // Log the translated packet header + log::debug!( + "> Translated Header: {}", + bytes_to_hex_str(&translated[0..40]).bright_cyan() + ); + + // Return the translated packet + translated + } else { + return Ok(None); + } + } else { + return Ok(None); + } + } + IpPacket::V6(packet) => { + let new_source = + self.get_or_create_reservation(std::net::IpAddr::V6(packet.get_source())); + let new_dest = self.extract_v4_from_v6(packet.get_destination()); + if let Some(IpAddr::V4(new_source)) = new_source { + // Log the new addresses + #[cfg_attr(rustfmt, rustfmt_skip)] + { + log::debug!("> Mapped IPv4 Source: {}", new_source.to_string().bright_cyan()); + log::debug!("> Mapped IPv4 Destination: {}", new_dest.to_string().bright_cyan()); + } + + // Handle inner packet conversion for protocols that don't support both v4 and v6 + if let Some(packet) = Ipv6Packet::owned(match packet.get_next_header() { + // ICMPv6 must be translated to ICMP + IpNextHeaderProtocols::Icmpv6 => { + if let Some(new_payload) = + xlat::icmpv6_to_icmp(&Icmpv6Packet::new(packet.payload()).unwrap()) + { + // Mutate the input packet + let mut packet = + MutableIpv6Packet::owned(packet.packet().to_vec()).unwrap(); + packet.set_next_header(IpNextHeaderProtocols::Icmp); + packet.set_payload(&new_payload.packet().to_vec()); + packet.packet().to_vec() + } else { + return Ok(None); + } + } + + // By default, packets can be directly fed to the next function + _ => packet.packet().to_vec(), + }) { + // Translate the packet + let translated = xlat::ipv6_to_ipv4(&packet, new_source, new_dest, true); + + // Log the translated packet header + log::debug!( + "> Translated Header: {}", + bytes_to_hex_str(&translated[0..20]).bright_cyan() + ); + + // Return the translated packet + translated + } else { + return Ok(None); + } + } else { + return Ok(None); + } + } + }; + + // Build the response + log::debug!("{}", "Sending translated packet".bright_green()); + return Ok(Some(output_packet)); + } +} diff --git a/src/nat/packet.rs b/src/nat/packet.rs index 871896d..629efe4 100644 --- a/src/nat/packet.rs +++ b/src/nat/packet.rs @@ -1,11 +1,8 @@ -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +//! A generic internet protocol packet type -use pnet_packet::{ - ip::IpNextHeaderProtocol, - ipv4::{checksum, Ipv4, Ipv4Packet, MutableIpv4Packet}, - ipv6::{Ipv6, Ipv6Packet, MutableIpv6Packet}, - Packet, -}; +use std::net::IpAddr; + +use pnet_packet::{ip::IpNextHeaderProtocol, ipv4::Ipv4Packet, ipv6::Ipv6Packet, Packet}; /// A protocol-agnostic packet type #[derive(Debug)] @@ -91,68 +88,55 @@ impl IpPacket<'_> { } } -pub fn xlat_v6_to_v4( - ipv6_packet: &Ipv6Packet, - new_source: Ipv4Addr, - new_dest: Ipv4Addr, - decr_ttl: bool, -) -> Vec { - let data = Ipv4 { - version: 4, - header_length: 20, - dscp: 0, - ecn: 0, - total_length: 20 + ipv6_packet.payload().len() as u16, - identification: 0, - flags: 0, - fragment_offset: 0, - ttl: ipv6_packet.get_hop_limit(), - next_level_protocol: ipv6_packet.get_next_header(), - checksum: 0, - source: new_source, - destination: new_dest, - options: vec![], - payload: ipv6_packet.payload().to_vec(), - }; - let mut packet = MutableIpv4Packet::owned(vec![0; 20 + ipv6_packet.payload().len()]).unwrap(); - packet.populate(&data); - packet.set_checksum(checksum(&packet.to_immutable())); +#[cfg(test)] +mod tests { + use pnet_packet::{ipv4::MutableIpv4Packet, ipv6::MutableIpv6Packet}; - // Decrement the TTL if needed - if decr_ttl { - packet.set_ttl(packet.get_ttl() - 1); + use super::*; + + #[test] + fn test_ipv4_packet() { + // Build packet to test + let mut packet = MutableIpv4Packet::owned(vec![0; 20]).unwrap(); + packet.set_version(4); + packet.set_source("192.0.2.1".parse().unwrap()); + packet.set_destination("192.0.2.2".parse().unwrap()); + + // Parse + let header = packet.packet()[..20].to_vec(); + let packet = IpPacket::new(packet.packet()).unwrap(); + assert_eq!( + packet.get_source(), + IpAddr::V4("192.0.2.1".parse().unwrap()) + ); + assert_eq!( + packet.get_destination(), + IpAddr::V4("192.0.2.2".parse().unwrap()) + ); + assert_eq!(packet.get_header(), header); } - let mut output = packet.to_immutable().packet().to_vec(); - // TODO: There is a bug here.. for now, force write header size - output[0] = 0x45; - output -} + #[test] + fn test_ipv6_packet() { + // Build packet to test + let mut packet = MutableIpv6Packet::owned(vec![0; 40]).unwrap(); + packet.set_version(6); + packet.set_source("2001:db8::c0a8:1".parse().unwrap()); + packet.set_destination("2001:db8::c0a8:2".parse().unwrap()); -pub fn xlat_v4_to_v6( - ipv4_packet: &Ipv4Packet, - new_source: Ipv6Addr, - new_dest: Ipv6Addr, - decr_ttl: bool, -) -> Vec { - let data = Ipv6 { - version: 6, - traffic_class: 0, - flow_label: 0, - payload_length: ipv4_packet.payload().len() as u16, - next_header: ipv4_packet.get_next_level_protocol(), - hop_limit: ipv4_packet.get_ttl(), - source: new_source, - destination: new_dest, - payload: ipv4_packet.payload().to_vec(), - }; - let mut packet = MutableIpv6Packet::owned(vec![0; 40 + ipv4_packet.payload().len()]).unwrap(); - packet.populate(&data); + // Parse + let header = packet.packet()[..40].to_vec(); + let packet = IpPacket::new(packet.packet()).unwrap(); - // Decrement the TTL if needed - if decr_ttl { - packet.set_hop_limit(packet.get_hop_limit() - 1); + // Test + assert_eq!( + packet.get_source(), + IpAddr::V6("2001:db8::c0a8:1".parse().unwrap()) + ); + assert_eq!( + packet.get_destination(), + IpAddr::V6("2001:db8::c0a8:2".parse().unwrap()) + ); + assert_eq!(packet.get_header(), header); } - - packet.to_immutable().packet().to_vec() } diff --git a/src/nat/table.rs b/src/nat/table.rs new file mode 100644 index 0000000..3b8373f --- /dev/null +++ b/src/nat/table.rs @@ -0,0 +1,143 @@ +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + time::{Duration, Instant}, +}; + +use bimap::BiHashMap; +use ipnet::Ipv4Net; + +/// Possible errors thrown in the address reservation process +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Address already reserved: {0}")] + AddressAlreadyReserved(IpAddr), + #[error("Address pool depleted")] + AddressPoolDepleted, +} + +/// A NAT address table +#[derive(Debug)] +pub struct Nat64Table { + /// All possible IPv4 addresses that can be used + ipv4_pool: Vec, + /// Current reservations + reservations: BiHashMap, + /// The timestamp of each reservation (used for pruning) + reservation_times: HashMap<(Ipv6Addr, Ipv4Addr), Option>, + /// The maximum amount of time to reserve an address pair for + reservation_timeout: Duration, +} + +impl Nat64Table { + /// Construct a new NAT64 table + /// + /// **Arguments:** + /// - `ipv4_pool`: The pool of IPv4 addresses to use in the mapping process + /// - `reservation_timeout`: The amount of time to reserve an address pair for + pub fn new(ipv4_pool: Vec, reservation_timeout: Duration) -> Self { + Self { + ipv4_pool, + reservations: BiHashMap::new(), + reservation_times: HashMap::new(), + reservation_timeout, + } + } + + /// Make a reservation for an IP address pair for eternity + pub fn add_infinite_reservation( + &mut self, + ipv6: Ipv6Addr, + ipv4: Ipv4Addr, + ) -> Result<(), Error> { + // Check if either address is already reserved + self.prune(); + if self.reservations.contains_left(&ipv6) { + return Err(Error::AddressAlreadyReserved(ipv6.into())); + } else if self.reservations.contains_right(&ipv4) { + return Err(Error::AddressAlreadyReserved(ipv4.into())); + } + + // Add the reservation + self.reservations.insert(ipv6, ipv4); + self.reservation_times.insert((ipv6, ipv4), None); + Ok(()) + } + + /// Get or assign an IPv4 address for the given IPv6 address + pub fn get_or_assign_ipv4(&mut self, ipv6: Ipv6Addr) -> Result { + // Prune old reservations + self.prune(); + + // If the IPv6 address is already reserved, return the IPv4 address + if let Some(ipv4) = self.reservations.get_by_left(&ipv6) { + // Update the reservation time + self.reservation_times + .insert((ipv6, *ipv4), Some(Instant::now())); + + // Return the v4 address + return Ok(*ipv4); + } + + // Otherwise, try to assign a new IPv4 address + for ipv4_net in &self.ipv4_pool { + for ipv4 in ipv4_net.hosts(){ + // Check if this address is available for use + if !self.reservations.contains_right(&ipv4) { + // Add the reservation + self.reservations.insert(ipv6, ipv4); + self.reservation_times + .insert((ipv6, ipv4), Some(Instant::now())); + return Ok(ipv4); + } + } + } + + // If we get here, we failed to find an available address + Err(Error::AddressPoolDepleted) + } + + /// Try to find an IPv6 address for the given IPv4 address + pub fn get_reverse(&mut self, ipv4: Ipv4Addr) -> Option { + // Prune old reservations + self.prune(); + + // If the IPv4 address is already reserved, return the IPv6 address + if let Some(ipv6) = self.reservations.get_by_right(&ipv4) { + // Update the reservation time + self.reservation_times + .insert((*ipv6, ipv4), Some(Instant::now())); + + // Return the v6 address + return Some(*ipv6); + } + + // Otherwise, there is no matching reservation + None + } +} + +impl Nat64Table { + /// Prune old reservations + pub fn prune(&mut self) { + let now = Instant::now(); + + // Prune from the reservation map + self.reservations.retain(|v6, v4| { + if let Some(time) = self.reservation_times.get(&(*v6, *v4)) { + if let Some(time) = time { + now - *time < self.reservation_timeout + } else { + true + } + } else { + true + } + }); + + // Remove all times assigned to reservations that no longer exist + self.reservation_times.retain(|(v6, v4), _| { + self.reservations.contains_left(v6) && self.reservations.contains_right(v4) + }); + } +} diff --git a/src/nat/icmp.rs b/src/nat/xlat/icmp.rs similarity index 99% rename from src/nat/icmp.rs rename to src/nat/xlat/icmp.rs index ab0c2b1..f348bce 100644 --- a/src/nat/icmp.rs +++ b/src/nat/xlat/icmp.rs @@ -1,4 +1,4 @@ -//! ICMP packets require their own translation system +//! Translation logic for ICMP and ICMPv6 use std::net::Ipv6Addr; diff --git a/src/nat/xlat/ip.rs b/src/nat/xlat/ip.rs new file mode 100644 index 0000000..fb762d9 --- /dev/null +++ b/src/nat/xlat/ip.rs @@ -0,0 +1,71 @@ +//! Translation logic for IPv4 and IPv6 + +use std::net::{Ipv4Addr, Ipv6Addr}; + +use pnet_packet::{ipv6::{Ipv6Packet, Ipv6, MutableIpv6Packet}, ipv4::{Ipv4, MutableIpv4Packet, self, Ipv4Packet}, Packet}; + +pub fn ipv6_to_ipv4( + ipv6_packet: &Ipv6Packet, + new_source: Ipv4Addr, + new_dest: Ipv4Addr, + decr_ttl: bool, +) -> Vec { + let data = Ipv4 { + version: 4, + header_length: 20, + dscp: 0, + ecn: 0, + total_length: 20 + ipv6_packet.payload().len() as u16, + identification: 0, + flags: 0, + fragment_offset: 0, + ttl: ipv6_packet.get_hop_limit(), + next_level_protocol: ipv6_packet.get_next_header(), + checksum: 0, + source: new_source, + destination: new_dest, + options: vec![], + payload: ipv6_packet.payload().to_vec(), + }; + let mut packet = MutableIpv4Packet::owned(vec![0; 20 + ipv6_packet.payload().len()]).unwrap(); + packet.populate(&data); + packet.set_checksum(ipv4::checksum(&packet.to_immutable())); + + // Decrement the TTL if needed + if decr_ttl { + packet.set_ttl(packet.get_ttl() - 1); + } + + let mut output = packet.to_immutable().packet().to_vec(); + // TODO: There is a bug here.. for now, force write header size + output[0] = 0x45; + output +} + +pub fn ipv4_to_ipv6( + ipv4_packet: &Ipv4Packet, + new_source: Ipv6Addr, + new_dest: Ipv6Addr, + decr_ttl: bool, +) -> Vec { + let data = Ipv6 { + version: 6, + traffic_class: 0, + flow_label: 0, + payload_length: ipv4_packet.payload().len() as u16, + next_header: ipv4_packet.get_next_level_protocol(), + hop_limit: ipv4_packet.get_ttl(), + source: new_source, + destination: new_dest, + payload: ipv4_packet.payload().to_vec(), + }; + let mut packet = MutableIpv6Packet::owned(vec![0; 40 + ipv4_packet.payload().len()]).unwrap(); + packet.populate(&data); + + // Decrement the TTL if needed + if decr_ttl { + packet.set_hop_limit(packet.get_hop_limit() - 1); + } + + packet.to_immutable().packet().to_vec() +} diff --git a/src/nat/xlat/mod.rs b/src/nat/xlat/mod.rs new file mode 100644 index 0000000..f0e107a --- /dev/null +++ b/src/nat/xlat/mod.rs @@ -0,0 +1,7 @@ +//! Packet type translation functionality + +mod icmp; +mod ip; + +pub use icmp::{icmpv6_to_icmp, icmp_to_icmpv6}; +pub use ip::{ipv4_to_ipv6, ipv6_to_ipv4}; \ No newline at end of file