From f4dc096d83bae557bb68c1f6c87ced24c8fcfac2 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 12:22:00 -0400 Subject: [PATCH 01/78] Implement `easy-tun` crate --- Cargo.toml | 109 +++++++++++------------ libs/easy-tun/Cargo.toml | 22 +++++ libs/easy-tun/README.md | 3 + libs/easy-tun/examples/print_traffic.rs | 17 ++++ libs/easy-tun/src/lib.rs | 7 ++ libs/easy-tun/src/tun.rs | 110 ++++++++++++++++++++++++ 6 files changed, 215 insertions(+), 53 deletions(-) create mode 100644 libs/easy-tun/Cargo.toml create mode 100644 libs/easy-tun/README.md create mode 100644 libs/easy-tun/examples/print_traffic.rs create mode 100644 libs/easy-tun/src/lib.rs create mode 100644 libs/easy-tun/src/tun.rs diff --git a/Cargo.toml b/Cargo.toml index 37458a9..4033a35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,56 +1,59 @@ -[package] -name = "protomask" -version = "0.2.0" -authors = ["Evan Pratten "] -edition = "2021" -description = "A user space NAT64 implementation" -readme = "README.md" -homepage = "https://github.com/ewpratten/protomask" -documentation = "https://docs.rs/protomask" -repository = "https://github.com/ewpratten/protomask" -license = "GPL-3.0" -keywords = [] -categories = [] +# [package] +# name = "protomask" +# version = "0.2.0" +# authors = ["Evan Pratten "] +# edition = "2021" +# description = "A user space NAT64 implementation" +# readme = "README.md" +# homepage = "https://github.com/ewpratten/protomask" +# documentation = "https://docs.rs/protomask" +# repository = "https://github.com/ewpratten/protomask" +# license = "GPL-3.0" +# keywords = [] +# categories = [] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] -protomask-tun = { path = "protomask-tun", version = "0.1.0" } -tokio = { version = "1.29.1", features = [ - "macros", - "rt-multi-thread", - # "process", - "sync" -] } -clap = { version = "4.3.11", features = ["derive"] } -serde = { version = "1.0.171", features = ["derive"] } -ipnet = { version = "2.8.0", features = ["serde"] } -hyper = { version = "0.14.27", features = ["server", "http1", "tcp"] } -owo-colors = { version = "3.5.0", features = ["supports-colors"] } -toml = "0.7.6" -log = "0.4.19" -fern = "0.6.2" -serde_path_to_error = "0.1.13" -thiserror = "1.0.43" -tun-tap = "0.1.3" -bimap = "0.6.3" -pnet_packet = "0.34.0" -rtnetlink = "0.13.0" -futures = "0.3.28" -prometheus = "0.13.3" -lazy_static = "1.4.0" +# # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +# [dependencies] +# protomask-tun = { path = "protomask-tun", version = "0.1.0" } +# tokio = { version = "1.29.1", features = [ +# "macros", +# "rt-multi-thread", +# # "process", +# "sync" +# ] } +# clap = { version = "4.3.11", features = ["derive"] } +# serde = { version = "1.0.171", features = ["derive"] } +# ipnet = { version = "2.8.0", features = ["serde"] } +# hyper = { version = "0.14.27", features = ["server", "http1", "tcp"] } +# owo-colors = { version = "3.5.0", features = ["supports-colors"] } +# toml = "0.7.6" +# log = "0.4.19" +# fern = "0.6.2" +# serde_path_to_error = "0.1.13" +# thiserror = "1.0.43" +# tun-tap = "0.1.3" +# bimap = "0.6.3" +# pnet_packet = "0.34.0" +# rtnetlink = "0.13.0" +# futures = "0.3.28" +# prometheus = "0.13.3" +# lazy_static = "1.4.0" -[[bin]] -name = "protomask" -path = "src/cli/main.rs" +# [[bin]] +# name = "protomask" +# path = "src/cli/main.rs" -[package.metadata.deb] -section = "network" -assets = [ - ["target/release/protomask", "/usr/local/bin/protomask", "755"], - ["./protomask.toml", "/etc/protomask.toml", "644"], - ["README.md", "usr/share/doc/protomask/README.md", "644"] -] -conf-files = ["/etc/protomask.toml"] -depends = [] -maintainer-scripts = "./debian/" -systemd-units = { enable = false } \ No newline at end of file +# [package.metadata.deb] +# section = "network" +# assets = [ +# ["target/release/protomask", "/usr/local/bin/protomask", "755"], +# ["./protomask.toml", "/etc/protomask.toml", "644"], +# ["README.md", "usr/share/doc/protomask/README.md", "644"] +# ] +# conf-files = ["/etc/protomask.toml"] +# depends = [] +# maintainer-scripts = "./debian/" +# systemd-units = { enable = false } + +[workspace] +members=["libs/easy-tun"] \ No newline at end of file diff --git a/libs/easy-tun/Cargo.toml b/libs/easy-tun/Cargo.toml new file mode 100644 index 0000000..e638933 --- /dev/null +++ b/libs/easy-tun/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "easy-tun" +version = "0.1.0" +authors = ["Evan Pratten "] +edition = "2021" +description = "A pure-rust TUN interface library" +readme = "README.md" +homepage = "https://github.com/ewpratten/protomask/tree/master/libs/easy-tun" +documentation = "https://docs.rs/easy-tun" +repository = "https://github.com/ewpratten/protomask" +license = "GPL-3.0" +keywords = [] +categories = [] + +[dependencies] +log = "^0.4" +libc = "^0.2" +ioctl-gen = "^0.1.1" +rtnetlink = "^0.13.0" + +[dev-dependencies] +env_logger = "0.10.0" diff --git a/libs/easy-tun/README.md b/libs/easy-tun/README.md new file mode 100644 index 0000000..7961b7b --- /dev/null +++ b/libs/easy-tun/README.md @@ -0,0 +1,3 @@ +# easy-tun + +`easy-tun` is a pure-Rust library that can bring up and manage a TUN interface by directly interacting with the [Universal TUN/TAP Driver](https://docs.kernel.org/networking/tuntap.html). diff --git a/libs/easy-tun/examples/print_traffic.rs b/libs/easy-tun/examples/print_traffic.rs new file mode 100644 index 0000000..ac44ebb --- /dev/null +++ b/libs/easy-tun/examples/print_traffic.rs @@ -0,0 +1,17 @@ +use easy_tun::Tun; +use std::io::Read; + +fn main() { + // Enable logs + env_logger::init(); + + // Bring up a TUN interface + let mut tun = Tun::new("tun%d").unwrap(); + + // Loop and read from the interface + let mut buffer = [0u8; 1500]; + loop { + let length = tun.read(&mut buffer).unwrap(); + println!("{:?}", &buffer[..length]); + } +} diff --git a/libs/easy-tun/src/lib.rs b/libs/easy-tun/src/lib.rs new file mode 100644 index 0000000..2a78190 --- /dev/null +++ b/libs/easy-tun/src/lib.rs @@ -0,0 +1,7 @@ +#![deny(clippy::pedantic)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::missing_panics_doc)] + +mod tun; +pub use tun::Tun; \ No newline at end of file diff --git a/libs/easy-tun/src/tun.rs b/libs/easy-tun/src/tun.rs new file mode 100644 index 0000000..7e7d3ab --- /dev/null +++ b/libs/easy-tun/src/tun.rs @@ -0,0 +1,110 @@ +use std::{ + fs::{File, OpenOptions}, + io::{Read, Write}, + mem::size_of, + os::fd::{AsRawFd, IntoRawFd, RawFd}, +}; + +use ioctl_gen::{ioc, iow}; +use libc::{__c_anonymous_ifr_ifru, ifreq, ioctl, IFF_NO_PI, IFF_TUN, IF_NAMESIZE}; + +/// A TUN device +pub struct Tun { + /// Internal file descriptor for the TUN device + fd: File, + /// Device name + name: String, +} + +impl Tun { + /// Creates a new Tun device with the given name. + /// + /// The `name` argument must be less than the system's `IFNAMSIZ` constant, + /// and may contain a `%d` format specifier to allow for multiple devices with the same name. + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_lossless)] + pub fn new(dev: &str) -> Result { + // Get a file descriptor for `/dev/net/tun` + log::trace!("Opening /dev/net/tun"); + let fd = OpenOptions::new() + .read(true) + .write(true) + .open("/dev/net/tun")?; + + // Copy the device name into a C string with padding + // NOTE: No zero padding is needed because we pre-init the array to all 0s + let mut dev_cstr = [0i8; IF_NAMESIZE]; + let dev_bytes: Vec = dev.chars().map(|c| c as i8).collect(); + let dev_len = dev_bytes.len().min(IF_NAMESIZE); + log::trace!("Device name length after truncation: {}", dev_len); + dev_cstr[..dev_len].copy_from_slice(&dev_bytes[..dev_len]); + + // Build an `ifreq` struct to send to the kernel + let mut ifr = ifreq { + ifr_name: dev_cstr, + ifr_ifru: __c_anonymous_ifr_ifru { + ifru_flags: (IFF_TUN | IFF_NO_PI) as i16, + }, + }; + + // Make an ioctl call to create the TUN device + log::trace!("Calling ioctl to create TUN device"); + let err = unsafe { + ioctl( + fd.as_raw_fd(), + iow!('T', 202, size_of::()) as u64, + &mut ifr, + ) + }; + log::trace!("ioctl returned: {}", err); + + // Check for errors + if err < 0 { + log::error!("ioctl failed: {}", err); + return Err(std::io::Error::last_os_error()); + } + + // Get the name of the device + let name = unsafe { std::ffi::CStr::from_ptr(ifr.ifr_name.as_ptr()) } + .to_str() + .unwrap() + .to_string(); + + // Build the TUN struct + Ok(Self { fd, name }) + } + + /// Get the name of the TUN device + #[must_use] + pub fn name(&self) -> &str { + &self.name + } +} + +impl AsRawFd for Tun { + fn as_raw_fd(&self) -> RawFd { + self.fd.as_raw_fd() + } +} + +impl IntoRawFd for Tun { + fn into_raw_fd(self) -> RawFd { + self.fd.into_raw_fd() + } +} + +impl Read for Tun { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.fd.read(buf) + } +} + +impl Write for Tun { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.fd.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.fd.flush() + } +} From 198b7bcd92ca59c53104d0de8e10816baa264652 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 13:14:51 -0400 Subject: [PATCH 02/78] Implement fast NAT --- Cargo.toml | 2 +- libs/easy-tun/src/lib.rs | 1 + libs/fast-nat/Cargo.toml | 17 ++++++ libs/fast-nat/README.md | 0 libs/fast-nat/src/bimap.rs | 111 +++++++++++++++++++++++++++++++++++ libs/fast-nat/src/cpnat.rs | 92 +++++++++++++++++++++++++++++ libs/fast-nat/src/lib.rs | 9 +++ libs/fast-nat/src/nat.rs | 87 +++++++++++++++++++++++++++ libs/fast-nat/src/timeout.rs | 13 ++++ 9 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 libs/fast-nat/Cargo.toml create mode 100644 libs/fast-nat/README.md create mode 100644 libs/fast-nat/src/bimap.rs create mode 100644 libs/fast-nat/src/cpnat.rs create mode 100644 libs/fast-nat/src/lib.rs create mode 100644 libs/fast-nat/src/nat.rs create mode 100644 libs/fast-nat/src/timeout.rs diff --git a/Cargo.toml b/Cargo.toml index 4033a35..5904fc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,4 +56,4 @@ # systemd-units = { enable = false } [workspace] -members=["libs/easy-tun"] \ No newline at end of file +members = ["libs/easy-tun", "libs/fast-nat"] diff --git a/libs/easy-tun/src/lib.rs b/libs/easy-tun/src/lib.rs index 2a78190..87bad45 100644 --- a/libs/easy-tun/src/lib.rs +++ b/libs/easy-tun/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] #![deny(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] #![allow(clippy::missing_errors_doc)] diff --git a/libs/fast-nat/Cargo.toml b/libs/fast-nat/Cargo.toml new file mode 100644 index 0000000..00f4927 --- /dev/null +++ b/libs/fast-nat/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "fast-nat" +version = "0.1.0" +authors = ["Evan Pratten "] +edition = "2021" +description = "An OSI layer 3 Network Address Table built for speed" +readme = "README.md" +homepage = "https://github.com/ewpratten/protomask/tree/master/libs/fast-nat" +documentation = "https://docs.rs/fast-nat" +repository = "https://github.com/ewpratten/protomask" +license = "GPL-3.0" +keywords = [] +categories = [] + +[dependencies] +log = "^0.4" +rustc-hash = "1.1.0" diff --git a/libs/fast-nat/README.md b/libs/fast-nat/README.md new file mode 100644 index 0000000..e69de29 diff --git a/libs/fast-nat/src/bimap.rs b/libs/fast-nat/src/bimap.rs new file mode 100644 index 0000000..367f5c7 --- /dev/null +++ b/libs/fast-nat/src/bimap.rs @@ -0,0 +1,111 @@ +use std::hash::Hash; + +use rustc_hash::FxHashMap; + +/// A bi-directional hash map +#[derive(Debug, Clone)] +pub struct BiHashMap { + /// Mapping from a left value to a right value + left_to_right: FxHashMap, + /// Mapping from a right value to a left value + right_to_left: FxHashMap, +} + +impl BiHashMap +where + Left: Eq + Hash + Clone, + Right: Eq + Hash + Clone, +{ + /// Construct a new empty `BiHashMap` + pub fn new() -> Self { + Self::default() + } + + /// Insert a new mapping into the `BiHashMap` + pub fn insert(&mut self, left: Left, right: Right) { + self.left_to_right.insert(left.clone(), right.clone()); + self.right_to_left.insert(right, left); + } + + /// Get the right value for a given left value + pub fn get_right(&self, left: &Left) -> Option<&Right> { + self.left_to_right.get(left) + } + + /// Get the left value for a given right value + pub fn get_left(&self, right: &Right) -> Option<&Left> { + self.right_to_left.get(right) + } + + /// Remove a mapping from the `BiHashMap` + pub fn remove(&mut self, left: &Left, right: &Right) { + self.left_to_right.remove(left); + self.right_to_left.remove(right); + } + + /// Remove a mapping from the `BiHashMap` by left value + #[allow(dead_code)] + pub fn remove_left(&mut self, left: &Left) { + if let Some(right) = self.left_to_right.remove(left) { + self.right_to_left.remove(&right); + } + } + + /// Remove a mapping from the `BiHashMap` by right value + #[allow(dead_code)] + pub fn remove_right(&mut self, right: &Right) { + if let Some(left) = self.right_to_left.remove(right) { + self.left_to_right.remove(&left); + } + } +} + +impl Default for BiHashMap { + fn default() -> Self { + Self { + left_to_right: FxHashMap::default(), + right_to_left: FxHashMap::default(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_insert() { + let mut bimap = BiHashMap::new(); + bimap.insert(1, "one"); + bimap.insert(2, "two"); + bimap.insert(3, "three"); + + assert_eq!(bimap.get_right(&1), Some(&"one")); + assert_eq!(bimap.get_right(&2), Some(&"two")); + assert_eq!(bimap.get_right(&3), Some(&"three")); + + assert_eq!(bimap.get_left(&"one"), Some(&1)); + assert_eq!(bimap.get_left(&"two"), Some(&2)); + assert_eq!(bimap.get_left(&"three"), Some(&3)); + } + + #[test] + fn test_remove() { + let mut bimap = BiHashMap::new(); + bimap.insert(1, "one"); + bimap.insert(2, "two"); + bimap.insert(3, "three"); + + bimap.remove(&1, &"one"); + assert_eq!(bimap.get_right(&1), None); + assert_eq!(bimap.get_left(&"one"), None); + + bimap.remove_left(&2); + assert_eq!(bimap.get_right(&2), None); + assert_eq!(bimap.get_left(&"two"), None); + + bimap.remove_right(&"three"); + assert_eq!(bimap.get_right(&3), None); + assert_eq!(bimap.get_left(&"three"), None); + } +} \ No newline at end of file diff --git a/libs/fast-nat/src/cpnat.rs b/libs/fast-nat/src/cpnat.rs new file mode 100644 index 0000000..7d67f4b --- /dev/null +++ b/libs/fast-nat/src/cpnat.rs @@ -0,0 +1,92 @@ +use std::time::Duration; + +use rustc_hash::FxHashMap; + +use crate::{bimap::BiHashMap, timeout::MaybeTimeout}; + +/// A table of network address mappings across IPv4 and IPv6 +#[derive(Debug)] +pub struct CrossProtocolNetworkAddressTable { + /// Internal address map + addr_map: BiHashMap, + /// Secondary map used to keep track of timeouts + timeouts: FxHashMap<(u32, u128), MaybeTimeout>, +} + +impl CrossProtocolNetworkAddressTable { + /// Construct a new empty `CrossProtocolNetworkAddressTable` + pub fn new() -> Self { + Self { + addr_map: BiHashMap::new(), + timeouts: FxHashMap::default(), + } + } + + /// Prune all old mappings + pub fn prune(&mut self) { + log::trace!("Pruning old network address mappings"); + + // Compare all mappings against a common timestamp + let now = std::time::Instant::now(); + + // Remove all old mappings from both the bimap and the timeouts map + self.timeouts.retain(|(left, right), timeout| { + match timeout { + // Retain all indefinite mappings + MaybeTimeout::Never => true, + // Only retain mappings that haven't timed out yet + MaybeTimeout::After { duration, start } => { + let should_retain = now.duration_since(*start) < *duration; + if !should_retain { + log::trace!( + "Mapping {:?} -> {:?} has timed out and will be removed", + left, + right + ); + self.addr_map.remove(left, right); + } + should_retain + } + } + }); + } + + /// Insert a new indefinite mapping + pub fn insert_indefinite, T6: Into>(&mut self, ipv4: T4, ipv6: T6) { + self.prune(); + let (ipv4, ipv6) = (ipv4.into(), ipv6.into()); + self.addr_map.insert(ipv4, ipv6); + self.timeouts.insert((ipv4, ipv6), MaybeTimeout::Never); + } + + /// Insert a new mapping with a finite time-to-live + pub fn insert, T6: Into>( + &mut self, + ipv4: T4, + ipv6: T6, + duration: Duration, + ) { + self.prune(); + let (ipv4, ipv6) = (ipv4.into(), ipv6.into()); + self.addr_map.insert(ipv4, ipv6); + self.timeouts.insert( + (ipv4, ipv6), + MaybeTimeout::After { + duration, + start: std::time::Instant::now(), + }, + ); + } + + /// Get the IPv6 address for a given IPv4 address + #[must_use] + pub fn get_ipv6>(&self, ipv4: T) -> Option { + self.addr_map.get_right(&ipv4.into()).copied() + } + + /// Get the IPv4 address for a given IPv6 address + #[must_use] + pub fn get_ipv4>(&self, ipv6: T) -> Option { + self.addr_map.get_left(&ipv6.into()).copied() + } +} diff --git a/libs/fast-nat/src/lib.rs b/libs/fast-nat/src/lib.rs new file mode 100644 index 0000000..e0bc1f4 --- /dev/null +++ b/libs/fast-nat/src/lib.rs @@ -0,0 +1,9 @@ +#![doc = include_str!("../README.md")] + +mod bimap; +mod cpnat; +mod nat; +mod timeout; + +pub use cpnat::CrossProtocolNetworkAddressTable; +pub use nat::NetworkAddressTable; diff --git a/libs/fast-nat/src/nat.rs b/libs/fast-nat/src/nat.rs new file mode 100644 index 0000000..d6bc175 --- /dev/null +++ b/libs/fast-nat/src/nat.rs @@ -0,0 +1,87 @@ +use std::time::Duration; + +use rustc_hash::FxHashMap; + +use crate::{bimap::BiHashMap, timeout::MaybeTimeout}; + +/// A table of network address mappings +#[derive(Debug)] +pub struct NetworkAddressTable { + /// Internal address map + addr_map: BiHashMap, + /// Secondary map used to keep track of timeouts + timeouts: FxHashMap<(u32, u32), MaybeTimeout>, +} + +impl NetworkAddressTable { + /// Construct a new empty `NetworkAddressTable` + pub fn new() -> Self { + Self { + addr_map: BiHashMap::new(), + timeouts: FxHashMap::default(), + } + } + + /// Prune all old mappings + pub fn prune(&mut self) { + log::trace!("Pruning old network address mappings"); + + // Compare all mappings against a common timestamp + let now = std::time::Instant::now(); + + // Remove all old mappings from both the bimap and the timeouts map + self.timeouts.retain(|(left, right), timeout| { + match timeout { + // Retain all indefinite mappings + MaybeTimeout::Never => true, + // Only retain mappings that haven't timed out yet + MaybeTimeout::After { duration, start } => { + let should_retain = now.duration_since(*start) < *duration; + if !should_retain { + log::trace!( + "Mapping {:?} -> {:?} has timed out and will be removed", + left, + right + ); + self.addr_map.remove(left, right); + } + should_retain + } + } + }); + } + + /// Insert a new indefinite mapping + pub fn insert_indefinite>(&mut self, left: T, right: T) { + self.prune(); + let (left, right) = (left.into(), right.into()); + self.addr_map.insert(left, right); + self.timeouts.insert((left, right), MaybeTimeout::Never); + } + + /// Insert a new mapping with a finite time-to-live + pub fn insert>(&mut self, left: T, right: T, duration: Duration) { + self.prune(); + let (left, right) = (left.into(), right.into()); + self.addr_map.insert(left, right); + self.timeouts.insert( + (left, right), + MaybeTimeout::After { + duration, + start: std::time::Instant::now(), + }, + ); + } + + /// Get the right value for a given left value + #[must_use] + pub fn get_right>(&self, left: T) -> Option { + self.addr_map.get_right(&left.into()).copied() + } + + /// Get the left value for a given right value + #[must_use] + pub fn get_left>(&self, right: T) -> Option { + self.addr_map.get_left(&right.into()).copied() + } +} diff --git a/libs/fast-nat/src/timeout.rs b/libs/fast-nat/src/timeout.rs new file mode 100644 index 0000000..9691f6f --- /dev/null +++ b/libs/fast-nat/src/timeout.rs @@ -0,0 +1,13 @@ +use std::time::Duration; + +/// Describes a possible timeout for a mapping +#[derive(Debug, Clone, Copy)] +pub enum MaybeTimeout { + /// Indicates that a mapping should never time out + Never, + /// Indicates that a mapping should time out after a given duration + After { + duration: Duration, + start: std::time::Instant, + }, +} \ No newline at end of file From fca597a194fb0ba9ae060089220c76d21aea6b8e Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 14:30:11 -0400 Subject: [PATCH 03/78] Remove protomask-tun in favor of easy-tun --- protomask-tun/Cargo.toml | 23 ---- protomask-tun/README.md | 3 - protomask-tun/src/error.rs | 10 -- protomask-tun/src/lib.rs | 5 - protomask-tun/src/tun.rs | 226 ------------------------------------- 5 files changed, 267 deletions(-) delete mode 100644 protomask-tun/Cargo.toml delete mode 100644 protomask-tun/README.md delete mode 100644 protomask-tun/src/error.rs delete mode 100644 protomask-tun/src/lib.rs delete mode 100644 protomask-tun/src/tun.rs diff --git a/protomask-tun/Cargo.toml b/protomask-tun/Cargo.toml deleted file mode 100644 index a2a24aa..0000000 --- a/protomask-tun/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "protomask-tun" -version = "0.1.0" -authors = ["Evan Pratten "] -edition = "2021" -description = "An async interface to Linux TUN devices" -readme = "README.md" -homepage = "https://github.com/ewpratten/protomask/protomask-tun" -documentation = "https://docs.rs/protomask-tun" -repository = "https://github.com/ewpratten/protomask" -license = "GPL-3.0" -keywords = [] -categories = [] - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[dependencies] -tokio = { version = "1.29.1", features = ["sync", "rt"] } -log = "0.4.19" -thiserror = "1.0.43" -tun-tap = "0.1.3" -rtnetlink = "0.13.0" -futures = "0.3.28" -ipnet = "^2.8.0" diff --git a/protomask-tun/README.md b/protomask-tun/README.md deleted file mode 100644 index 2f77949..0000000 --- a/protomask-tun/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# protomask-tun - -An async interface to Linux TUN devices. Support library for `protomask`. diff --git a/protomask-tun/src/error.rs b/protomask-tun/src/error.rs deleted file mode 100644 index ed62a8e..0000000 --- a/protomask-tun/src/error.rs +++ /dev/null @@ -1,10 +0,0 @@ -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error(transparent)] - IoError(#[from] std::io::Error), - - #[error(transparent)] - NetlinkError(#[from] rtnetlink::Error), -} - -pub type Result = std::result::Result; \ No newline at end of file diff --git a/protomask-tun/src/lib.rs b/protomask-tun/src/lib.rs deleted file mode 100644 index b75b6df..0000000 --- a/protomask-tun/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod error; -mod tun; - -pub use error::{Error, Result}; -pub use tun::TunDevice; diff --git a/protomask-tun/src/tun.rs b/protomask-tun/src/tun.rs deleted file mode 100644 index 79511b5..0000000 --- a/protomask-tun/src/tun.rs +++ /dev/null @@ -1,226 +0,0 @@ -use std::{ - io::{Read, Write}, - net::IpAddr, - os::fd::{AsRawFd, FromRawFd}, -}; - -use futures::TryStreamExt; -use ipnet::IpNet; -use tokio::{ - sync::{broadcast, mpsc}, - task, -}; -use tun_tap::Mode; - -use crate::Result; - -#[derive(Debug)] -pub struct TunDevice { - device: tun_tap::Iface, - rt_handle: rtnetlink::Handle, - link_index: u32, - mtu: usize, -} - -impl TunDevice { - /// Create and bring up a new TUN device - /// - /// ## Name format - /// - /// The name field can be any string. If `%d` is present in the string, - /// it will be replaced with a unique number. - pub async fn new(name: &str) -> Result { - // Bring up an rtnetlink connection - let (rt_connection, rt_handle, _) = rtnetlink::new_connection().map_err(|err| { - log::error!("Failed to open rtnetlink connection"); - log::error!("{}", err); - err - })?; - tokio::spawn(rt_connection); - - // Create the TUN device - let tun_device = tun_tap::Iface::without_packet_info(name, Mode::Tun)?; - log::debug!("Created new TUN device: {}", tun_device.name()); - - // Get access to the link through rtnetlink - // NOTE: I don't think there is any way this can fail, so `except` is probably OK - let tun_link = rt_handle - .link() - .get() - .match_name(tun_device.name().to_owned()) - .execute() - .try_next() - .await? - .expect("Failed to access newly created TUN device"); - - // Bring the link up - rt_handle - .link() - .set(tun_link.header.index) - .up() - .execute() - .await - .map_err(|err| { - log::error!("Failed to bring up link"); - log::error!("{}", err); - err - })?; - log::debug!("Brought {} up", tun_device.name()); - - // Read the link MTU - let mtu: usize = - std::fs::read_to_string(format!("/sys/class/net/{}/mtu", tun_device.name())) - .expect("Failed to read link MTU") - .strip_suffix("\n") - .unwrap() - .parse() - .unwrap(); - - Ok(Self { - device: tun_device, - rt_handle, - link_index: tun_link.header.index, - mtu, - }) - } - - /// Add an IP address to this device - pub async fn add_address(&mut self, ip_address: IpAddr, prefix_len: u8) -> Result<()> { - self.rt_handle - .address() - .add(self.link_index, ip_address, prefix_len) - .execute() - .await - .map_err(|err| { - log::error!("Failed to add address {} to link", ip_address); - log::error!("{}", err); - err - })?; - - Ok(()) - } - - /// Remove an IP address from this device - pub async fn remove_address(&mut self, ip_address: IpAddr, prefix_len: u8) -> Result<()> { - // Find the address message that matches the given address - if let Some(address_message) = self - .rt_handle - .address() - .get() - .set_link_index_filter(self.link_index) - .set_address_filter(ip_address) - .set_prefix_length_filter(prefix_len) - .execute() - .try_next() - .await - .map_err(|err| { - log::error!("Failed to find address {} on link", ip_address); - log::error!("{}", err); - err - })? - { - // Delete the address - self.rt_handle - .address() - .del(address_message) - .execute() - .await - .map_err(|err| { - log::error!("Failed to remove address {} from link", ip_address); - log::error!("{}", err); - err - })?; - } - - Ok(()) - } - - /// Add a route to this device - pub async fn add_route(&mut self, destination: IpNet) -> Result<()> { - match destination { - IpNet::V4(destination) => { - self.rt_handle - .route() - .add() - .v4() - .output_interface(self.link_index) - .destination_prefix(destination.addr(), destination.prefix_len()) - .execute() - .await - .map_err(|err| { - log::error!("Failed to add route {} to link", destination); - log::error!("{}", err); - err - })?; - } - IpNet::V6(destination) => { - self.rt_handle - .route() - .add() - .v6() - .output_interface(self.link_index) - .destination_prefix(destination.addr(), destination.prefix_len()) - .execute() - .await - .map_err(|err| { - log::error!("Failed to add route {} to link", destination); - log::error!("{}", err); - err - })?; - } - } - - Ok(()) - } - - /// Spawns worker threads, and returns a tx/rx pair for the caller to interact with them - pub async fn spawn_worker(&self) -> (mpsc::Sender>, broadcast::Receiver>) { - // Create a channel for packets to be sent to the caller - let (tx_to_caller, rx_from_worker) = broadcast::channel(65535); - - // Create a channel for packets being received from the caller - let (tx_to_worker, mut rx_from_caller) = mpsc::channel(65535); - - // Clone some values for use in worker threads - let mtu = self.mtu; - let device_fd = self.device.as_raw_fd(); - - // Create a task that broadcasts all incoming packets - let _rx_task = task::spawn_blocking(move || { - // Build a buffer to read packets into - let mut buffer = vec![0u8; mtu]; - - // Create a file to access the TUN device - let mut device = unsafe { std::fs::File::from_raw_fd(device_fd) }; - - loop { - // Read a packet from the TUN device - let packet_len = device.read(&mut buffer[..]).unwrap(); - let packet = buffer[..packet_len].to_vec(); - - // Broadcast the packet to all listeners - tx_to_caller.send(packet).unwrap(); - } - }); - - // Create a task that sends all outgoing packets - let _tx_task = task::spawn(async move { - // Create a file to access the TUN device - let mut device = unsafe { std::fs::File::from_raw_fd(device_fd) }; - - loop { - // Wait for a packet to be sent - let packet: Vec = rx_from_caller.recv().await.unwrap(); - - // Write the packet to the TUN device - device.write_all(&packet[..]).unwrap(); - } - }); - - // Create a task that sends all outgoing packets - let _tx_task = task::spawn_blocking(|| {}); - - // Return an rx/tx pair for the caller to interact with the workers - (tx_to_worker, rx_from_worker) - } -} From ebbe72fc19d0d8b14d9fd79c6ce6e1b019016767 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 14:54:30 -0400 Subject: [PATCH 04/78] Fix clippy errors and create new package --- Cargo.toml | 2 +- libs/fast-nat/README.md | 5 +++++ libs/fast-nat/src/cpnat.rs | 15 +++++++++++---- libs/fast-nat/src/lib.rs | 4 ++++ libs/fast-nat/src/nat.rs | 15 +++++++++++---- libs/interproto/Cargo.toml | 17 +++++++++++++++++ libs/interproto/README.md | 0 libs/interproto/src/lib.rs | 5 +++++ 8 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 libs/interproto/Cargo.toml create mode 100644 libs/interproto/README.md create mode 100644 libs/interproto/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 5904fc1..348c03e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,4 +56,4 @@ # systemd-units = { enable = false } [workspace] -members = ["libs/easy-tun", "libs/fast-nat"] +members = ["libs/easy-tun", "libs/fast-nat", "libs/interproto"] diff --git a/libs/fast-nat/README.md b/libs/fast-nat/README.md index e69de29..2144c63 100644 --- a/libs/fast-nat/README.md +++ b/libs/fast-nat/README.md @@ -0,0 +1,5 @@ +# Fast Network Address Table + +`fast-nat` is an OSI layer 3 Network Address Table built for speed. + +While this library can be used on its own just fine, it was designed for use in `protomask`. diff --git a/libs/fast-nat/src/cpnat.rs b/libs/fast-nat/src/cpnat.rs index 7d67f4b..e467013 100644 --- a/libs/fast-nat/src/cpnat.rs +++ b/libs/fast-nat/src/cpnat.rs @@ -15,11 +15,9 @@ pub struct CrossProtocolNetworkAddressTable { impl CrossProtocolNetworkAddressTable { /// Construct a new empty `CrossProtocolNetworkAddressTable` + #[must_use] pub fn new() -> Self { - Self { - addr_map: BiHashMap::new(), - timeouts: FxHashMap::default(), - } + Self::default() } /// Prune all old mappings @@ -90,3 +88,12 @@ impl CrossProtocolNetworkAddressTable { self.addr_map.get_left(&ipv6.into()).copied() } } + +impl Default for CrossProtocolNetworkAddressTable { + fn default() -> Self { + Self { + addr_map: BiHashMap::new(), + timeouts: FxHashMap::default(), + } + } +} diff --git a/libs/fast-nat/src/lib.rs b/libs/fast-nat/src/lib.rs index e0bc1f4..5a0e3fd 100644 --- a/libs/fast-nat/src/lib.rs +++ b/libs/fast-nat/src/lib.rs @@ -1,4 +1,8 @@ #![doc = include_str!("../README.md")] +#![deny(clippy::pedantic)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::missing_panics_doc)] mod bimap; mod cpnat; diff --git a/libs/fast-nat/src/nat.rs b/libs/fast-nat/src/nat.rs index d6bc175..e29263a 100644 --- a/libs/fast-nat/src/nat.rs +++ b/libs/fast-nat/src/nat.rs @@ -15,11 +15,9 @@ pub struct NetworkAddressTable { impl NetworkAddressTable { /// Construct a new empty `NetworkAddressTable` + #[must_use] pub fn new() -> Self { - Self { - addr_map: BiHashMap::new(), - timeouts: FxHashMap::default(), - } + Self::default() } /// Prune all old mappings @@ -85,3 +83,12 @@ impl NetworkAddressTable { self.addr_map.get_left(&right.into()).copied() } } + +impl Default for NetworkAddressTable { + fn default() -> Self { + Self { + addr_map: BiHashMap::new(), + timeouts: FxHashMap::default(), + } + } +} \ No newline at end of file diff --git a/libs/interproto/Cargo.toml b/libs/interproto/Cargo.toml new file mode 100644 index 0000000..323d828 --- /dev/null +++ b/libs/interproto/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "interproto" +version = "0.1.0" +authors = ["Evan Pratten "] +edition = "2021" +description = "Utilities for translating packets between IPv4 and IPv6" +readme = "README.md" +homepage = "https://github.com/ewpratten/protomask/tree/master/libs/interproto" +documentation = "https://docs.rs/interproto" +repository = "https://github.com/ewpratten/protomask" +license = "GPL-3.0" +keywords = [] +categories = [] + +[dependencies] +log = "^0.4" +pnet = "^0.34.0" \ No newline at end of file diff --git a/libs/interproto/README.md b/libs/interproto/README.md new file mode 100644 index 0000000..e69de29 diff --git a/libs/interproto/src/lib.rs b/libs/interproto/src/lib.rs new file mode 100644 index 0000000..44f5417 --- /dev/null +++ b/libs/interproto/src/lib.rs @@ -0,0 +1,5 @@ +#![doc = include_str!("../README.md")] +#![deny(clippy::pedantic)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::missing_panics_doc)] \ No newline at end of file From 37f9e91a9559f1bb376b7bcb3416d1dd74a7d2e0 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 16:28:15 -0400 Subject: [PATCH 05/78] move all translation code to interproto --- libs/interproto/Cargo.toml | 3 +- libs/interproto/src/error.rs | 13 ++ libs/interproto/src/lib.rs | 5 +- libs/interproto/src/protocols/icmp/mod.rs | 131 +++++++++++++++++ .../src/protocols/icmp/type_code.rs | 101 +++++++++++++ libs/interproto/src/protocols/ip.rs | 137 ++++++++++++++++++ libs/interproto/src/protocols/mod.rs | 4 + libs/interproto/src/protocols/tcp.rs | 110 ++++++++++++++ libs/interproto/src/protocols/udp.rs | 110 ++++++++++++++ 9 files changed, 612 insertions(+), 2 deletions(-) create mode 100644 libs/interproto/src/error.rs create mode 100644 libs/interproto/src/protocols/icmp/mod.rs create mode 100644 libs/interproto/src/protocols/icmp/type_code.rs create mode 100644 libs/interproto/src/protocols/ip.rs create mode 100644 libs/interproto/src/protocols/mod.rs create mode 100644 libs/interproto/src/protocols/tcp.rs create mode 100644 libs/interproto/src/protocols/udp.rs diff --git a/libs/interproto/Cargo.toml b/libs/interproto/Cargo.toml index 323d828..3e1405c 100644 --- a/libs/interproto/Cargo.toml +++ b/libs/interproto/Cargo.toml @@ -14,4 +14,5 @@ categories = [] [dependencies] log = "^0.4" -pnet = "^0.34.0" \ No newline at end of file +pnet = "0.34.0" +thiserror = "^1.0.44" \ No newline at end of file diff --git a/libs/interproto/src/error.rs b/libs/interproto/src/error.rs new file mode 100644 index 0000000..9add768 --- /dev/null +++ b/libs/interproto/src/error.rs @@ -0,0 +1,13 @@ +/// All possible errors thrown by `interproto` functions +#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)] +pub enum Error { + #[error("Packet too short. Expected at least {expected} bytes, got {actual}")] + PacketTooShort { expected: usize, actual: usize }, + #[error("Unsupported ICMP type: {0}")] + UnsupportedIcmpType(u8), + #[error("Unsupported ICMPv6 type: {0}")] + UnsupportedIcmpv6Type(u8), +} + +/// Result type for `interproto` +pub type Result = std::result::Result; diff --git a/libs/interproto/src/lib.rs b/libs/interproto/src/lib.rs index 44f5417..1486821 100644 --- a/libs/interproto/src/lib.rs +++ b/libs/interproto/src/lib.rs @@ -2,4 +2,7 @@ #![deny(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] #![allow(clippy::missing_errors_doc)] -#![allow(clippy::missing_panics_doc)] \ No newline at end of file +#![allow(clippy::missing_panics_doc)] + +pub mod protocols; +pub mod error; \ No newline at end of file diff --git a/libs/interproto/src/protocols/icmp/mod.rs b/libs/interproto/src/protocols/icmp/mod.rs new file mode 100644 index 0000000..8aa9a79 --- /dev/null +++ b/libs/interproto/src/protocols/icmp/mod.rs @@ -0,0 +1,131 @@ +use crate::{ + error::{Error, Result}, + protocols::ip::translate_ipv4_to_ipv6, +}; +use pnet::packet::{ + icmp::{self, IcmpPacket, IcmpTypes, MutableIcmpPacket}, + icmpv6::{self, Icmpv6Packet, Icmpv6Types, MutableIcmpv6Packet}, + Packet, +}; +use std::net::{Ipv4Addr, Ipv6Addr}; + +use super::ip::translate_ipv6_to_ipv4; + +mod type_code; + +/// Translate an ICMP packet to ICMPv6. This will make a best guess at the ICMPv6 type and code since there is no 1:1 mapping. +#[allow(clippy::deprecated_cfg_attr)] +pub fn translate_icmp_to_icmpv6( + icmp_packet: &[u8], + new_source: Ipv6Addr, + new_destination: Ipv6Addr, +) -> Result> { + // Access the ICMP packet data in a safe way + let icmp_packet = IcmpPacket::new(icmp_packet).ok_or(Error::PacketTooShort { + expected: IcmpPacket::minimum_packet_size(), + actual: icmp_packet.len(), + })?; + + // Translate the ICMP type and code to their ICMPv6 equivalents + let (icmpv6_type, icmpv6_code) = type_code::translate_type_and_code_4_to_6( + icmp_packet.get_icmp_type(), + icmp_packet.get_icmp_code(), + )?; + + // Some ICMP types require special payload edits + let payload = match icmpv6_type { + Icmpv6Types::TimeExceeded => { + // Time exceeded messages contain the original IPv4 header and part of the payload. (with 4 bytes of forward padding) + // We need to translate the IPv4 header and the payload, but keep the padding + let mut output = vec![0u8; icmp_packet.payload().len()]; + output.copy_from_slice(&icmp_packet.payload()[..4]); + output.extend_from_slice(&translate_ipv4_to_ipv6( + &icmp_packet.payload()[4..], + new_source, + new_destination, + )?); + output + } + _ => icmp_packet.payload().to_vec(), + }; + + // Build a buffer to store the new ICMPv6 packet + let mut output_buffer = vec![0u8; IcmpPacket::minimum_packet_size() + payload.len()]; + + // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. + let mut icmpv6_packet = + unsafe { MutableIcmpv6Packet::new(&mut output_buffer).unwrap_unchecked() }; + + // Set the header fields + icmpv6_packet.set_icmpv6_type(icmpv6_type); + icmpv6_packet.set_icmpv6_code(icmpv6_code); + icmpv6_packet.set_checksum(0); + + // Copy the payload + icmpv6_packet.set_payload(&payload); + + // Calculate the checksum + icmpv6_packet.set_checksum(icmpv6::checksum( + &icmpv6_packet.to_immutable(), + &new_source, + &new_destination, + )); + + // Return the translated packet + Ok(output_buffer) +} + +/// Translate an ICMPv6 packet to ICMP. This will make a best guess at the ICMP type and code since there is no 1:1 mapping. +pub fn translate_icmpv6_to_icmp( + icmpv6_packet: &[u8], + new_source: Ipv4Addr, + new_destination: Ipv4Addr, +) -> Result> { + // Access the ICMPv6 packet data in a safe way + let icmpv6_packet = Icmpv6Packet::new(icmpv6_packet).ok_or(Error::PacketTooShort { + expected: Icmpv6Packet::minimum_packet_size(), + actual: icmpv6_packet.len(), + })?; + + // Translate the ICMPv6 type and code to their ICMP equivalents + let (icmp_type, icmp_code) = type_code::translate_type_and_code_6_to_4( + icmpv6_packet.get_icmpv6_type(), + icmpv6_packet.get_icmpv6_code(), + )?; + + // Some ICMP types require special payload edits + let payload = match icmp_type { + IcmpTypes::TimeExceeded => { + // Time exceeded messages contain the original IPv6 header and part of the payload. (with 4 bytes of forward padding) + // We need to translate the IPv6 header and the payload, but keep the padding + let mut output = vec![0u8; icmpv6_packet.payload().len()]; + output.copy_from_slice(&icmpv6_packet.payload()[..4]); + output.extend_from_slice(&translate_ipv6_to_ipv4( + &icmpv6_packet.payload()[4..], + new_source, + new_destination, + )?); + output + } + _ => icmpv6_packet.payload().to_vec(), + }; + + // Build a buffer to store the new ICMP packet + let mut output_buffer = vec![0u8; Icmpv6Packet::minimum_packet_size() + payload.len()]; + + // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. + let mut icmp_packet = unsafe { MutableIcmpPacket::new(&mut output_buffer).unwrap_unchecked() }; + + // Set the header fields + icmp_packet.set_icmp_type(icmp_type); + icmp_packet.set_icmp_code(icmp_code); + + // Copy the payload + icmp_packet.set_payload(&payload); + + // Calculate the checksum + icmp_packet.set_checksum(icmp::checksum(&icmp_packet.to_immutable())); + + // Return the translated packet + Ok(output_buffer) +} diff --git a/libs/interproto/src/protocols/icmp/type_code.rs b/libs/interproto/src/protocols/icmp/type_code.rs new file mode 100644 index 0000000..e566a84 --- /dev/null +++ b/libs/interproto/src/protocols/icmp/type_code.rs @@ -0,0 +1,101 @@ +//! Look-up-tables for translating between ICMP (type,code) tuples and ICMPv6 (type,code) tuples. + +use pnet::packet::{ + icmp::{destination_unreachable, IcmpCode, IcmpType, IcmpTypes}, + icmpv6::{Icmpv6Code, Icmpv6Type, Icmpv6Types}, +}; + +use crate::error::{Error, Result}; + +/// Best effort translation from an ICMP type and code to an ICMPv6 type and code +#[allow(clippy::deprecated_cfg_attr)] +pub fn translate_type_and_code_4_to_6( + icmp_type: IcmpType, + icmp_code: IcmpCode, +) -> Result<(Icmpv6Type, Icmpv6Code)> { + match (icmp_type, icmp_code) { + // Echo Request + (IcmpTypes::EchoRequest, _) => Ok((Icmpv6Types::EchoRequest, Icmpv6Code(0))), + + // Echo Reply + (IcmpTypes::EchoReply, _) => Ok((Icmpv6Types::EchoReply, Icmpv6Code(0))), + + // Packet Too Big + ( + IcmpTypes::DestinationUnreachable, + destination_unreachable::IcmpCodes::FragmentationRequiredAndDFFlagSet, + ) => Ok((Icmpv6Types::PacketTooBig, Icmpv6Code(0))), + + // Destination Unreachable + (IcmpTypes::DestinationUnreachable, icmp_code) => Ok(( + Icmpv6Types::DestinationUnreachable, + #[cfg_attr(rustfmt, rustfmt_skip)] + #[allow(clippy::match_same_arms)] + Icmpv6Code(match icmp_code { + destination_unreachable::IcmpCodes::DestinationHostUnreachable => 3, + destination_unreachable::IcmpCodes::DestinationProtocolUnreachable => 4, + destination_unreachable::IcmpCodes::DestinationPortUnreachable => 4, + destination_unreachable::IcmpCodes::SourceRouteFailed => 5, + destination_unreachable::IcmpCodes::SourceHostIsolated => 2, + destination_unreachable::IcmpCodes::NetworkAdministrativelyProhibited => 1, + destination_unreachable::IcmpCodes::HostAdministrativelyProhibited => 1, + destination_unreachable::IcmpCodes::CommunicationAdministrativelyProhibited => 1, + + // Default to No Route to Destination + _ => 0, + }), + )), + + // Time Exceeded + (IcmpTypes::TimeExceeded, icmp_code) => { + Ok((Icmpv6Types::TimeExceeded, Icmpv6Code(icmp_code.0))) + } + + // Default unsupported + (icmp_type, _) => Err(Error::UnsupportedIcmpType(icmp_type.0)), + } +} + +/// Best effort translation from an ICMPv6 type and code to an ICMP type and code +#[allow(clippy::deprecated_cfg_attr)] +pub fn translate_type_and_code_6_to_4( + icmp_type: Icmpv6Type, + icmp_code: Icmpv6Code, +) -> Result<(IcmpType, IcmpCode)> { + match (icmp_type, icmp_code) { + // Echo Request + (Icmpv6Types::EchoRequest, _) => Ok((IcmpTypes::EchoRequest, IcmpCode(0))), + + // Echo Reply + (Icmpv6Types::EchoReply, _) => Ok((IcmpTypes::EchoReply, IcmpCode(0))), + + // Packet Too Big + (Icmpv6Types::PacketTooBig, _) => Ok(( + IcmpTypes::DestinationUnreachable, + destination_unreachable::IcmpCodes::FragmentationRequiredAndDFFlagSet, + )), + + // Destination Unreachable + (Icmpv6Types::DestinationUnreachable, icmp_code) => Ok(( + IcmpTypes::DestinationUnreachable, + #[cfg_attr(rustfmt, rustfmt_skip)] + #[allow(clippy::match_same_arms)] + match icmp_code.0 { + 1 => destination_unreachable::IcmpCodes::CommunicationAdministrativelyProhibited, + 2 => destination_unreachable::IcmpCodes::SourceHostIsolated, + 3 => destination_unreachable::IcmpCodes::DestinationHostUnreachable, + 4 => destination_unreachable::IcmpCodes::DestinationPortUnreachable, + 5 => destination_unreachable::IcmpCodes::SourceRouteFailed, + _ => destination_unreachable::IcmpCodes::DestinationNetworkUnreachable, + }, + )), + + // Time Exceeded + (Icmpv6Types::TimeExceeded, icmp_code) => { + Ok((IcmpTypes::TimeExceeded, IcmpCode(icmp_code.0))) + } + + // Default unsupported + (icmp_type, _) => Err(Error::UnsupportedIcmpv6Type(icmp_type.0)), + } +} diff --git a/libs/interproto/src/protocols/ip.rs b/libs/interproto/src/protocols/ip.rs new file mode 100644 index 0000000..54d934c --- /dev/null +++ b/libs/interproto/src/protocols/ip.rs @@ -0,0 +1,137 @@ +//! Translation functions that can convert packets between IPv4 and IPv6. + +use super::{ + icmp::{translate_icmp_to_icmpv6, translate_icmpv6_to_icmp}, + tcp::{recalculate_tcp_checksum_ipv4, recalculate_tcp_checksum_ipv6}, udp::{recalculate_udp_checksum_ipv6, recalculate_udp_checksum_ipv4}, +}; +use crate::error::{Error, Result}; +use pnet::packet::{ + ip::IpNextHeaderProtocols, + ipv4::{self, Ipv4Packet, MutableIpv4Packet}, + ipv6::{Ipv6Packet, MutableIpv6Packet}, + Packet, +}; +use std::net::{Ipv4Addr, Ipv6Addr}; + +/// Translates an IPv4 packet into an IPv6 packet. The packet payload will be translated recursively as needed. +pub fn translate_ipv4_to_ipv6( + ipv4_packet: &[u8], + new_source: Ipv6Addr, + new_destination: Ipv6Addr, +) -> Result> { + // Access the IPv4 packet data in a safe way + let ipv4_packet = Ipv4Packet::new(ipv4_packet).ok_or(Error::PacketTooShort { + expected: Ipv4Packet::minimum_packet_size(), + actual: ipv4_packet.len(), + })?; + + // Perform recursive translation to determine the new payload + let new_payload = match ipv4_packet.get_next_level_protocol() { + // Pass ICMP packets to the icmp-to-icmpv6 translator + IpNextHeaderProtocols::Icmp => { + translate_icmp_to_icmpv6(ipv4_packet.payload(), new_source, new_destination)? + } + + // Pass TCP packets to the tcp translator + IpNextHeaderProtocols::Tcp => { + recalculate_tcp_checksum_ipv6(ipv4_packet.payload(), new_source, new_destination)? + } + + // Pass UDP packets to the udp translator + IpNextHeaderProtocols::Udp => { + recalculate_udp_checksum_ipv6(ipv4_packet.payload(), new_source, new_destination)? + } + + // If the next level protocol is not something we know how to translate, + // just assume the payload can be passed through as-is + protocol => { + log::warn!("Unsupported next level protocol: {:?}", protocol); + ipv4_packet.payload().to_vec() + } + }; + + // Build a buffer to store the new IPv6 packet + let mut output_buffer = vec![0u8; Ipv6Packet::minimum_packet_size() + new_payload.len()]; + + // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. + let mut ipv6_packet = unsafe { MutableIpv6Packet::new(&mut output_buffer).unwrap_unchecked() }; + + // Set the header fields + ipv6_packet.set_version(6); + ipv6_packet.set_next_header(match ipv4_packet.get_next_level_protocol() { + IpNextHeaderProtocols::Icmp => IpNextHeaderProtocols::Icmpv6, + proto => proto, + }); + ipv6_packet.set_hop_limit(ipv4_packet.get_ttl()); + ipv6_packet.set_source(new_source); + ipv6_packet.set_destination(new_destination); + + // Copy the payload to the buffer + ipv6_packet.set_payload(&new_payload); + + // Return the buffer + Ok(output_buffer) +} + +/// Translates an IPv6 packet into an IPv4 packet. The packet payload will be translated recursively as needed. +pub fn translate_ipv6_to_ipv4( + ipv6_packet: &[u8], + new_source: Ipv4Addr, + new_destination: Ipv4Addr, +) -> Result> { + // Access the IPv6 packet data in a safe way + let ipv6_packet = Ipv6Packet::new(ipv6_packet).ok_or(Error::PacketTooShort { + expected: Ipv6Packet::minimum_packet_size(), + actual: ipv6_packet.len(), + })?; + + // Perform recursive translation to determine the new payload + let new_payload = match ipv6_packet.get_next_header() { + // Pass ICMP packets to the icmpv6-to-icmp translator + IpNextHeaderProtocols::Icmpv6 => { + translate_icmpv6_to_icmp(ipv6_packet.payload(), new_source, new_destination)? + } + + // Pass TCP packets to the tcp translator + IpNextHeaderProtocols::Tcp => { + recalculate_tcp_checksum_ipv4(ipv6_packet.payload(), new_source, new_destination)? + } + + // Pass UDP packets to the udp translator + IpNextHeaderProtocols::Udp => { + recalculate_udp_checksum_ipv4(ipv6_packet.payload(), new_source, new_destination)? + } + + // If the next header is not something we know how to translate, + // just assume the payload can be passed through as-is + protocol => { + log::warn!("Unsupported next header: {:?}", protocol); + ipv6_packet.payload().to_vec() + } + }; + + // Build a buffer to store the new IPv4 packet + let mut output_buffer = vec![0u8; Ipv4Packet::minimum_packet_size() + new_payload.len()]; + + // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. + let mut ipv4_packet = unsafe { MutableIpv4Packet::new(&mut output_buffer).unwrap_unchecked() }; + + // Set the header fields + ipv4_packet.set_version(4); + ipv4_packet.set_ttl(ipv6_packet.get_hop_limit()); + ipv4_packet.set_next_level_protocol(match ipv6_packet.get_next_header() { + IpNextHeaderProtocols::Icmpv6 => IpNextHeaderProtocols::Icmp, + proto => proto, + }); + ipv4_packet.set_source(new_source); + ipv4_packet.set_destination(new_destination); + + // Copy the payload to the buffer + ipv4_packet.set_payload(&new_payload); + + // Calculate the checksum + ipv4_packet.set_checksum(ipv4::checksum(&ipv4_packet.to_immutable())); + + // Return the buffer + Ok(output_buffer) +} diff --git a/libs/interproto/src/protocols/mod.rs b/libs/interproto/src/protocols/mod.rs new file mode 100644 index 0000000..5dff8bd --- /dev/null +++ b/libs/interproto/src/protocols/mod.rs @@ -0,0 +1,4 @@ +pub mod ip; +pub mod tcp; +pub mod udp; +pub mod icmp; \ No newline at end of file diff --git a/libs/interproto/src/protocols/tcp.rs b/libs/interproto/src/protocols/tcp.rs new file mode 100644 index 0000000..4885b67 --- /dev/null +++ b/libs/interproto/src/protocols/tcp.rs @@ -0,0 +1,110 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +use pnet::packet::tcp::{self, MutableTcpPacket, TcpPacket}; + +use crate::error::{Error, Result}; + +/// Re-calculates a TCP packet's checksum with a new IPv6 pseudo-header. +pub fn recalculate_tcp_checksum_ipv6( + tcp_packet: &[u8], + new_source: Ipv6Addr, + new_destination: Ipv6Addr, +) -> Result> { + // Clone the packet so we can modify it + let mut tcp_packet_buffer = tcp_packet.to_vec(); + + // Get safe mutable access to the packet + let mut tcp_packet = + MutableTcpPacket::new(&mut tcp_packet_buffer).ok_or(Error::PacketTooShort { + expected: TcpPacket::minimum_packet_size(), + actual: tcp_packet.len(), + })?; + + // Edit the packet's checksum + tcp_packet.set_checksum(0); + tcp_packet.set_checksum(tcp::ipv6_checksum( + &tcp_packet.to_immutable(), + &new_source, + &new_destination, + )); + + // Return the translated packet + Ok(tcp_packet_buffer) +} + +/// Re-calculates a TCP packet's checksum with a new IPv4 pseudo-header. +pub fn recalculate_tcp_checksum_ipv4( + tcp_packet: &[u8], + new_source: Ipv4Addr, + new_destination: Ipv4Addr, +) -> Result> { + // Clone the packet so we can modify it + let mut tcp_packet_buffer = tcp_packet.to_vec(); + + // Get safe mutable access to the packet + let mut tcp_packet = + MutableTcpPacket::new(&mut tcp_packet_buffer).ok_or(Error::PacketTooShort { + expected: TcpPacket::minimum_packet_size(), + actual: tcp_packet.len(), + })?; + + // Edit the packet's checksum + tcp_packet.set_checksum(0); + tcp_packet.set_checksum(tcp::ipv4_checksum( + &tcp_packet.to_immutable(), + &new_source, + &new_destination, + )); + + // Return the translated packet + Ok(tcp_packet_buffer) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_checksum_recalculate_ipv6() { + // Create an input packet + let mut input_buffer = vec![0u8; TcpPacket::minimum_packet_size() + 13]; + let mut input_packet = MutableTcpPacket::new(&mut input_buffer).unwrap(); + input_packet.set_source(1234); + input_packet.set_destination(5678); + input_packet.set_payload(&"Hello, world!".as_bytes().to_vec()); + + // Recalculate the checksum + let recalculated_buffer = recalculate_tcp_checksum_ipv6( + &input_buffer, + "2001:db8::1".parse().unwrap(), + "2001:db8::2".parse().unwrap(), + ) + .unwrap(); + + // Verify the checksum + let recalculated_packet = TcpPacket::new(&recalculated_buffer).unwrap(); + assert_eq!(recalculated_packet.get_checksum(), 0x4817); + } + + #[test] + fn test_checksum_recalculate_ipv4() { + // Create an input packet + let mut input_buffer = vec![0u8; TcpPacket::minimum_packet_size() + 13]; + let mut input_packet = MutableTcpPacket::new(&mut input_buffer).unwrap(); + input_packet.set_source(1234); + input_packet.set_destination(5678); + input_packet.set_payload(&"Hello, world!".as_bytes().to_vec()); + + // Recalculate the checksum + let recalculated_buffer = recalculate_tcp_checksum_ipv4( + &input_buffer, + "192.0.2.1".parse().unwrap(), + "192.0.2.2".parse().unwrap(), + ) + .unwrap(); + + // Verify the checksum + let recalculated_packet = TcpPacket::new(&recalculated_buffer).unwrap(); + assert_eq!(recalculated_packet.get_checksum(), 0x1f88); + } +} diff --git a/libs/interproto/src/protocols/udp.rs b/libs/interproto/src/protocols/udp.rs new file mode 100644 index 0000000..cef9a23 --- /dev/null +++ b/libs/interproto/src/protocols/udp.rs @@ -0,0 +1,110 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +use pnet::packet::udp::{self, MutableUdpPacket, UdpPacket}; + +use crate::error::{Error, Result}; + +/// Re-calculates a UDP packet's checksum with a new IPv6 pseudo-header. +pub fn recalculate_udp_checksum_ipv6( + udp_packet: &[u8], + new_source: Ipv6Addr, + new_destination: Ipv6Addr, +) -> Result> { + // Clone the packet so we can modify it + let mut udp_packet_buffer = udp_packet.to_vec(); + + // Get safe mutable access to the packet + let mut udp_packet = + MutableUdpPacket::new(&mut udp_packet_buffer).ok_or(Error::PacketTooShort { + expected: UdpPacket::minimum_packet_size(), + actual: udp_packet.len(), + })?; + + // Edit the packet's checksum + udp_packet.set_checksum(0); + udp_packet.set_checksum(udp::ipv6_checksum( + &udp_packet.to_immutable(), + &new_source, + &new_destination, + )); + + // Return the translated packet + Ok(udp_packet_buffer) +} + +/// Re-calculates a UDP packet's checksum with a new IPv4 pseudo-header. +pub fn recalculate_udp_checksum_ipv4( + udp_packet: &[u8], + new_source: Ipv4Addr, + new_destination: Ipv4Addr, +) -> Result> { + // Clone the packet so we can modify it + let mut udp_packet_buffer = udp_packet.to_vec(); + + // Get safe mutable access to the packet + let mut udp_packet = + MutableUdpPacket::new(&mut udp_packet_buffer).ok_or(Error::PacketTooShort { + expected: UdpPacket::minimum_packet_size(), + actual: udp_packet.len(), + })?; + + // Edit the packet's checksum + udp_packet.set_checksum(0); + udp_packet.set_checksum(udp::ipv4_checksum( + &udp_packet.to_immutable(), + &new_source, + &new_destination, + )); + + // Return the translated packet + Ok(udp_packet_buffer) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_recalculate_udp_checksum_ipv6() { + let mut input_buffer = vec![0u8; UdpPacket::minimum_packet_size() + 13]; + let mut udp_packet = MutableUdpPacket::new(&mut input_buffer).unwrap(); + udp_packet.set_source(1234); + udp_packet.set_destination(5678); + udp_packet.set_length(13); + udp_packet.set_payload(&"Hello, world!".as_bytes().to_vec()); + + // Recalculate the checksum + let recalculated_buffer = recalculate_udp_checksum_ipv6( + &input_buffer, + "2001:db8::1".parse().unwrap(), + "2001:db8::2".parse().unwrap(), + ) + .unwrap(); + + // Check that the checksum is correct + let recalculated_packet = UdpPacket::new(&recalculated_buffer).unwrap(); + assert_eq!(recalculated_packet.get_checksum(), 0x480b); + } + + #[test] + fn test_recalculate_udp_checksum_ipv4() { + let mut input_buffer = vec![0u8; UdpPacket::minimum_packet_size() + 13]; + let mut udp_packet = MutableUdpPacket::new(&mut input_buffer).unwrap(); + udp_packet.set_source(1234); + udp_packet.set_destination(5678); + udp_packet.set_length(13); + udp_packet.set_payload(&"Hello, world!".as_bytes().to_vec()); + + // Recalculate the checksum + let recalculated_buffer = recalculate_udp_checksum_ipv4( + &input_buffer, + "192.0.2.1".parse().unwrap(), + "192.0.2.2".parse().unwrap(), + ) + .unwrap(); + + // Check that the checksum is correct + let recalculated_packet = UdpPacket::new(&recalculated_buffer).unwrap(); + assert_eq!(recalculated_packet.get_checksum(), 0x1f7c); + } +} From 6968fdfa2a284be068db3b5ebe18041ffde14d56 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 16:28:50 -0400 Subject: [PATCH 06/78] Delete old packet logic --- src/nat/error.rs | 15 -- src/nat/mod.rs | 177 --------------------- src/nat/table.rs | 210 ------------------------- src/nat/utils.rs | 57 ------- src/packet/error.rs | 14 -- src/packet/mod.rs | 7 - src/packet/protocols/icmp.rs | 106 ------------- src/packet/protocols/icmpv6.rs | 154 ------------------- src/packet/protocols/ipv4.rs | 133 ---------------- src/packet/protocols/ipv6.rs | 95 ------------ src/packet/protocols/mod.rs | 7 - src/packet/protocols/raw.rs | 18 --- src/packet/protocols/tcp.rs | 246 ------------------------------ src/packet/protocols/udp.rs | 215 -------------------------- src/packet/xlat/icmp/mod.rs | 113 -------------- src/packet/xlat/icmp/type_code.rs | 103 ------------- src/packet/xlat/ip.rs | 129 ---------------- src/packet/xlat/mod.rs | 6 - src/packet/xlat/tcp.rs | 123 --------------- src/packet/xlat/udp.rs | 97 ------------ 20 files changed, 2025 deletions(-) delete mode 100644 src/nat/error.rs delete mode 100644 src/nat/mod.rs delete mode 100644 src/nat/table.rs delete mode 100644 src/nat/utils.rs delete mode 100644 src/packet/error.rs delete mode 100644 src/packet/mod.rs delete mode 100644 src/packet/protocols/icmp.rs delete mode 100644 src/packet/protocols/icmpv6.rs delete mode 100644 src/packet/protocols/ipv4.rs delete mode 100644 src/packet/protocols/ipv6.rs delete mode 100644 src/packet/protocols/mod.rs delete mode 100644 src/packet/protocols/raw.rs delete mode 100644 src/packet/protocols/tcp.rs delete mode 100644 src/packet/protocols/udp.rs delete mode 100644 src/packet/xlat/icmp/mod.rs delete mode 100644 src/packet/xlat/icmp/type_code.rs delete mode 100644 src/packet/xlat/ip.rs delete mode 100644 src/packet/xlat/mod.rs delete mode 100644 src/packet/xlat/tcp.rs delete mode 100644 src/packet/xlat/udp.rs diff --git a/src/nat/error.rs b/src/nat/error.rs deleted file mode 100644 index d90751a..0000000 --- a/src/nat/error.rs +++ /dev/null @@ -1,15 +0,0 @@ -#[derive(Debug, thiserror::Error)] -pub enum Nat64Error { - #[error(transparent)] - Table(#[from] super::table::TableError), - #[error(transparent)] - Tun(#[from] protomask_tun::Error), - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - PacketHandling(#[from] crate::packet::error::PacketError), - #[error(transparent)] - PacketReceive(#[from] tokio::sync::broadcast::error::RecvError), - #[error(transparent)] - PacketSend(#[from] tokio::sync::mpsc::error::SendError>), -} diff --git a/src/nat/mod.rs b/src/nat/mod.rs deleted file mode 100644 index 930b363..0000000 --- a/src/nat/mod.rs +++ /dev/null @@ -1,177 +0,0 @@ -use crate::{ - metrics::PACKET_COUNTER, - packet::{ - protocols::{ipv4::Ipv4Packet, ipv6::Ipv6Packet}, - xlat::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4}, - }, -}; - -use self::{ - error::Nat64Error, - table::Nat64Table, - utils::{embed_address, extract_address, unwrap_log}, -}; -use ipnet::{Ipv4Net, Ipv6Net}; -use protomask_tun::TunDevice; -use std::{ - net::{IpAddr, Ipv4Addr, Ipv6Addr}, - time::Duration, -}; -use tokio::sync::broadcast; - -mod error; -mod table; -mod utils; - -pub struct Nat64 { - table: Nat64Table, - interface: TunDevice, - ipv6_nat_prefix: Ipv6Net, -} - -impl Nat64 { - /// Construct a new NAT64 instance - pub async fn new( - ipv6_nat_prefix: Ipv6Net, - ipv4_pool: Vec, - static_reservations: Vec<(Ipv6Addr, Ipv4Addr)>, - reservation_duration: Duration, - ) -> Result { - // Bring up the interface - let mut interface = TunDevice::new("nat64i%d").await?; - - // Add the NAT64 prefix as a route - interface.add_route(ipv6_nat_prefix.into()).await?; - - // Add the IPv4 pool prefixes as routes - for ipv4_prefix in &ipv4_pool { - interface.add_route((*ipv4_prefix).into()).await?; - } - - // Build the table and insert any static reservations - let mut table = Nat64Table::new(ipv4_pool, reservation_duration); - for (v6, v4) in static_reservations { - table.add_infinite_reservation(v6, v4)?; - } - - Ok(Self { - table, - interface, - ipv6_nat_prefix, - }) - } - - /// Block and process all packets - pub async fn run(&mut self) -> Result<(), Nat64Error> { - // Get an rx/tx pair for the interface - let (tx, mut rx) = self.interface.spawn_worker().await; - - // Process packets in a loop - loop { - // Try to read a packet - match rx.recv().await { - Ok(packet) => { - // Clone the TX so the worker can respond with data - let tx = tx.clone(); - - // Separate logic is needed for handling IPv4 vs IPv6 packets, so a check must be done here - match packet[0] >> 4 { - 4 => { - // Parse the packet - let packet: Ipv4Packet> = packet.try_into()?; - - // Drop packets that aren't destined for a destination the table knows about - if !self.table.contains(&IpAddr::V4(packet.destination_address)) { - PACKET_COUNTER.with_label_values(&["ipv4", "dropped"]).inc(); - continue; - } - - // Get the new source and dest addresses - let new_source = - embed_address(packet.source_address, self.ipv6_nat_prefix); - let new_destination = - self.table.get_reverse(packet.destination_address)?; - - // Mark the packet as accepted - PACKET_COUNTER - .with_label_values(&["ipv4", "accepted"]) - .inc(); - - // Spawn a task to process the packet - tokio::spawn(async move { - if let Some(output) = unwrap_log(translate_ipv4_to_ipv6( - packet, - new_source, - new_destination, - )) { - tx.send(output.into()).await.unwrap(); - PACKET_COUNTER.with_label_values(&["ipv6", "sent"]).inc(); - } - }); - } - 6 => { - // Parse the packet - let packet: Ipv6Packet> = packet.try_into()?; - - // Drop packets "coming from" the NAT64 prefix - if self.ipv6_nat_prefix.contains(&packet.source_address) { - log::warn!( - "Dropping packet \"from\" NAT64 prefix: {} -> {}", - packet.source_address, - packet.destination_address - ); - PACKET_COUNTER.with_label_values(&["ipv6", "dropped"]).inc(); - continue; - } - - // Get the new source and dest addresses - let new_source = - self.table.get_or_assign_ipv4(packet.source_address)?; - let new_destination = extract_address(packet.destination_address); - - // Drop packets destined for private IPv4 addresses - if new_destination.is_private() { - log::warn!( - "Dropping packet destined for private IPv4 address: {} -> {} ({})", - packet.source_address, - packet.destination_address, - new_destination - ); - PACKET_COUNTER.with_label_values(&["ipv6", "dropped"]).inc(); - continue; - } - - // Mark the packet as accepted - PACKET_COUNTER - .with_label_values(&["ipv6", "accepted"]) - .inc(); - - // Spawn a task to process the packet - tokio::spawn(async move { - if let Some(output) = unwrap_log(translate_ipv6_to_ipv4( - &packet, - new_source, - new_destination, - )) { - tx.send(output.into()).await.unwrap(); - PACKET_COUNTER.with_label_values(&["ipv4", "sent"]).inc(); - } - }); - } - n => { - log::warn!("Unknown IP version: {}", n); - } - } - Ok(()) - } - Err(error) => match error { - broadcast::error::RecvError::Lagged(count) => { - log::warn!("Translator running behind! Dropping {} packets", count); - Ok(()) - } - error @ broadcast::error::RecvError::Closed => Err(error), - }, - }?; - } - } -} diff --git a/src/nat/table.rs b/src/nat/table.rs deleted file mode 100644 index e7e3693..0000000 --- a/src/nat/table.rs +++ /dev/null @@ -1,210 +0,0 @@ -use std::{ - collections::HashMap, - net::{IpAddr, Ipv4Addr, Ipv6Addr}, - time::{Duration, Instant}, -}; - -use bimap::BiHashMap; -use ipnet::Ipv4Net; - -use crate::metrics::{IPV4_POOL_RESERVED, IPV4_POOL_SIZE}; - -/// Possible errors thrown in the address reservation process -#[derive(Debug, thiserror::Error)] -pub enum TableError { - #[error("Address already reserved: {0}")] - AddressAlreadyReserved(IpAddr), - #[error("IPv4 address has no IPv6 mapping: {0}")] - NoIpv6Mapping(Ipv4Addr), - #[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 { - // Track the total pool size - let total_size: usize = ipv4_pool.iter().map(|net| net.hosts().count()).sum(); - IPV4_POOL_SIZE.set(total_size as i64); - - 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<(), TableError> { - // Check if either address is already reserved - self.prune(); - self.track_utilization(); - if self.reservations.contains_left(&ipv6) { - return Err(TableError::AddressAlreadyReserved(ipv6.into())); - } else if self.reservations.contains_right(&ipv4) { - return Err(TableError::AddressAlreadyReserved(ipv4.into())); - } - - // Add the reservation - self.reservations.insert(ipv6, ipv4); - self.reservation_times.insert((ipv6, ipv4), None); - log::info!("Added infinite reservation: {} -> {}", ipv6, ipv4); - Ok(()) - } - - /// Check if a given address exists in the table - pub fn contains(&self, address: &IpAddr) -> bool { - match address { - IpAddr::V4(ipv4) => self.reservations.contains_right(ipv4), - IpAddr::V6(ipv6) => self.reservations.contains_left(ipv6), - } - } - - /// 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(); - self.track_utilization(); - - // 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())); - log::info!("Assigned new reservation: {} -> {}", ipv6, ipv4); - return Ok(ipv4); - } - } - } - - // If we get here, we failed to find an available address - Err(TableError::AddressPoolDepleted) - } - - /// Try to find an IPv6 address for the given IPv4 address - pub fn get_reverse(&mut self, ipv4: Ipv4Addr) -> Result { - // Prune old reservations - self.prune(); - self.track_utilization(); - - // 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 Ok(*ipv6); - } - - // Otherwise, there is no matching reservation - Err(TableError::NoIpv6Mapping(ipv4)) - } -} - -impl Nat64Table { - /// Prune old reservations - fn prune(&mut self) { - let now = Instant::now(); - - // Prune from the reservation map - self.reservations.retain(|v6, v4| { - if let Some(Some(time)) = self.reservation_times.get(&(*v6, *v4)) { - let keep = now - *time < self.reservation_timeout; - if !keep { - log::info!("Pruned reservation: {} -> {}", v6, v4); - } - keep - } 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) - }); - } - - fn track_utilization(&self) { - // Count static and dynamic in a single pass - let (total_dynamic_reservations, total_static_reservations) = self - .reservation_times - .iter() - .map(|((_v6, _v4), time)| match time { - Some(_) => (1, 0), - None => (0, 1), - }) - .fold((0, 0), |(a1, a2), (b1, b2)| (a1 + b1, a2 + b2)); - - // Track the values - IPV4_POOL_RESERVED - .with_label_values(&["dynamic"]) - .set(i64::from(total_dynamic_reservations)); - IPV4_POOL_RESERVED - .with_label_values(&["static"]) - .set(i64::from(total_static_reservations)); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_add_infinite_reservation() { - let mut table = Nat64Table::new( - vec![Ipv4Net::new(Ipv4Addr::new(192, 0, 2, 0), 24).unwrap()], - Duration::from_secs(60), - ); - - // Add a reservation - table - .add_infinite_reservation("2001:db8::1".parse().unwrap(), "192.0.2.1".parse().unwrap()) - .unwrap(); - - // Check that it worked - assert_eq!( - table - .reservations - .get_by_left(&"2001:db8::1".parse().unwrap()), - Some(&"192.0.2.1".parse().unwrap()) - ); - } -} diff --git a/src/nat/utils.rs b/src/nat/utils.rs deleted file mode 100644 index 44d1f18..0000000 --- a/src/nat/utils.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::net::{Ipv4Addr, Ipv6Addr}; - -use ipnet::Ipv6Net; - -use crate::packet::error::PacketError; - -/// Embed an IPv4 address in an IPv6 prefix -pub fn embed_address(ipv4_address: Ipv4Addr, ipv6_prefix: Ipv6Net) -> Ipv6Addr { - let v4_octets = ipv4_address.octets(); - let v6_octets = ipv6_prefix.addr().octets(); - Ipv6Addr::new( - u16::from_be_bytes([v6_octets[0], v6_octets[1]]), - u16::from_be_bytes([v6_octets[2], v6_octets[3]]), - u16::from_be_bytes([v6_octets[4], v6_octets[5]]), - u16::from_be_bytes([v6_octets[6], v6_octets[7]]), - u16::from_be_bytes([v6_octets[8], v6_octets[9]]), - u16::from_be_bytes([v6_octets[10], v6_octets[11]]), - u16::from_be_bytes([v4_octets[0], v4_octets[1]]), - u16::from_be_bytes([v4_octets[2], v4_octets[3]]), - ) -} - -/// Extract an IPv4 address from an IPv6 address -pub fn extract_address(ipv6_address: Ipv6Addr) -> Ipv4Addr { - let octets = ipv6_address.octets(); - Ipv4Addr::new(octets[12], octets[13], octets[14], octets[15]) -} - -/// Logs errors instead of crashing out of them -pub fn unwrap_log(result: Result) -> Option { - match result { - Ok(value) => Some(value), - Err(err) => match err { - PacketError::MismatchedAddressFamily(addr_a, addr_b) => { - log::error!( - "Mismatched address family between {} and {}", - addr_a, - addr_b - ); - None - } - PacketError::TooShort(len, data) => { - log::warn!("Received packet that's too short to parse. Length {}", len); - log::debug!("Short packet: {:?}", data); - None - } - PacketError::UnsupportedIcmpType(icmp_type) => { - log::warn!("Unsupported ICMP type {}", icmp_type); - None - } - PacketError::UnsupportedIcmpv6Type(icmp_type) => { - log::warn!("Unsupported ICMPv6 type {}", icmp_type); - None - } - }, - } -} diff --git a/src/packet/error.rs b/src/packet/error.rs deleted file mode 100644 index ec2ffe9..0000000 --- a/src/packet/error.rs +++ /dev/null @@ -1,14 +0,0 @@ -use std::net::IpAddr; - -#[derive(Debug, thiserror::Error)] -pub enum PacketError { - #[error("Mismatched source and destination address family: source={0:?}, destination={1:?}")] - MismatchedAddressFamily(IpAddr, IpAddr), - #[error("Packet too short: {0}")] - TooShort(usize, Vec), - #[error("Unsupported ICMP type: {0}")] - UnsupportedIcmpType(u8), - #[error("Unsupported ICMPv6 type: {0}")] - UnsupportedIcmpv6Type(u8), -} - diff --git a/src/packet/mod.rs b/src/packet/mod.rs deleted file mode 100644 index a40cbd5..0000000 --- a/src/packet/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Custom packet modification utilities -//! -//! pnet isn't quite what we need for this project, so the contents of this module wrap it to make it easier to use. - -pub mod error; -pub mod protocols; -pub mod xlat; diff --git a/src/packet/protocols/icmp.rs b/src/packet/protocols/icmp.rs deleted file mode 100644 index 53e4a2a..0000000 --- a/src/packet/protocols/icmp.rs +++ /dev/null @@ -1,106 +0,0 @@ -use pnet_packet::{ - icmp::{IcmpCode, IcmpType}, - Packet, -}; - -use crate::packet::error::PacketError; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct IcmpPacket { - pub icmp_type: IcmpType, - pub icmp_code: IcmpCode, - pub payload: T, -} - -impl IcmpPacket { - /// Construct a new `ICMP` packet - pub fn new(icmp_type: IcmpType, icmp_code: IcmpCode, payload: T) -> Self { - Self { - icmp_type, - icmp_code, - payload, - } - } -} - -impl TryFrom> for IcmpPacket -where - T: TryFrom, Error = PacketError>, -{ - type Error = PacketError; - - fn try_from(bytes: Vec) -> Result { - // Parse the packet - let packet = pnet_packet::icmp::IcmpPacket::new(&bytes) - .ok_or(PacketError::TooShort(bytes.len(), bytes.clone()))?; - - // Return the packet - Ok(Self { - icmp_type: packet.get_icmp_type(), - icmp_code: packet.get_icmp_code(), - payload: packet.payload().to_vec().try_into()?, - }) - } -} - -impl From> for Vec -where - T: Into>, -{ - fn from(packet: IcmpPacket) -> Self { - // Convert the payload into raw bytes - let payload: Vec = packet.payload.into(); - - // Allocate a mutable packet to write into - let total_length = - pnet_packet::icmp::MutableIcmpPacket::minimum_packet_size() + payload.len(); - let mut output = - pnet_packet::icmp::MutableIcmpPacket::owned(vec![0u8; total_length]).unwrap(); - - // Write the type and code - output.set_icmp_type(packet.icmp_type); - output.set_icmp_code(packet.icmp_code); - - // Write the payload - output.set_payload(&payload); - - // Calculate the checksum - output.set_checksum(0); - output.set_checksum(pnet_packet::icmp::checksum(&output.to_immutable())); - - // Return the raw bytes - output.packet().to_vec() - } -} - -#[cfg(test)] -mod tests { - use pnet_packet::icmp::IcmpTypes; - - use super::*; - - // Test packet construction - #[test] - #[rustfmt::skip] - fn test_packet_construction() { - // Make a new packet - let packet = IcmpPacket::new( - IcmpTypes::EchoRequest, - IcmpCode(0), - "Hello, world!".as_bytes().to_vec(), - ); - - // Convert to raw bytes - let packet_bytes: Vec = packet.into(); - - // Check the contents - assert!(packet_bytes.len() >= 4 + 13); - assert_eq!(packet_bytes[0], IcmpTypes::EchoRequest.0); - assert_eq!(packet_bytes[1], 0); - assert_eq!(u16::from_be_bytes([packet_bytes[2], packet_bytes[3]]), 0xb6b3); - assert_eq!( - &packet_bytes[4..], - "Hello, world!".as_bytes().to_vec().as_slice() - ); - } -} diff --git a/src/packet/protocols/icmpv6.rs b/src/packet/protocols/icmpv6.rs deleted file mode 100644 index d320e93..0000000 --- a/src/packet/protocols/icmpv6.rs +++ /dev/null @@ -1,154 +0,0 @@ -use std::net::Ipv6Addr; - -use pnet_packet::{ - icmpv6::{Icmpv6Code, Icmpv6Type}, - Packet, -}; - -use crate::packet::error::PacketError; - -use super::raw::RawBytes; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Icmpv6Packet { - pub source_address: Ipv6Addr, - pub destination_address: Ipv6Addr, - pub icmp_type: Icmpv6Type, - pub icmp_code: Icmpv6Code, - pub payload: T, -} - -impl Icmpv6Packet { - /// Construct a new `ICMPv6` packet - pub fn new( - source_address: Ipv6Addr, - destination_address: Ipv6Addr, - icmp_type: Icmpv6Type, - icmp_code: Icmpv6Code, - payload: T, - ) -> Self { - Self { - source_address, - destination_address, - icmp_type, - icmp_code, - payload, - } - } -} - -impl Icmpv6Packet -where - T: From>, -{ - /// Construct a new `ICMPv6` packet from raw bytes - #[allow(dead_code)] - pub fn new_from_bytes( - bytes: &[u8], - source_address: Ipv6Addr, - destination_address: Ipv6Addr, - ) -> Result { - // Parse the packet - let packet = pnet_packet::icmpv6::Icmpv6Packet::new(bytes) - .ok_or(PacketError::TooShort(bytes.len(), bytes.to_vec()))?; - - // Return the packet - Ok(Self { - source_address, - destination_address, - icmp_type: packet.get_icmpv6_type(), - icmp_code: packet.get_icmpv6_code(), - payload: packet.payload().to_vec().into(), - }) - } -} - -impl Icmpv6Packet { - /// Construct a new `ICMPv6` packet with a raw payload from raw bytes - pub fn new_from_bytes_raw_payload( - bytes: &[u8], - source_address: Ipv6Addr, - destination_address: Ipv6Addr, - ) -> Result { - // Parse the packet - let packet = pnet_packet::icmpv6::Icmpv6Packet::new(bytes) - .ok_or(PacketError::TooShort(bytes.len(), bytes.to_vec()))?; - - // Return the packet - Ok(Self { - source_address, - destination_address, - icmp_type: packet.get_icmpv6_type(), - icmp_code: packet.get_icmpv6_code(), - payload: RawBytes(packet.payload().to_vec()), - }) - } -} - -impl From> for Vec -where - T: Into>, -{ - fn from(packet: Icmpv6Packet) -> Self { - // Convert the payload into raw bytes - let payload: Vec = packet.payload.into(); - - // Allocate a mutable packet to write into - let total_length = - pnet_packet::icmpv6::MutableIcmpv6Packet::minimum_packet_size() + payload.len(); - let mut output = - pnet_packet::icmpv6::MutableIcmpv6Packet::owned(vec![0u8; total_length]).unwrap(); - - // Write the type and code - output.set_icmpv6_type(packet.icmp_type); - output.set_icmpv6_code(packet.icmp_code); - - // Write the payload - output.set_payload(&payload); - - // Calculate the checksum - output.set_checksum(0); - output.set_checksum(pnet_packet::icmpv6::checksum( - &output.to_immutable(), - &packet.source_address, - &packet.destination_address, - )); - - // Return the raw bytes - output.packet().to_vec() - } -} - -#[cfg(test)] -mod tests { - use pnet_packet::icmpv6::Icmpv6Types; - - use super::*; - - // Test packet construction - #[test] - #[rustfmt::skip] - fn test_packet_construction() { - // Make a new packet - let packet = Icmpv6Packet::new( - "2001:db8:1::1".parse().unwrap(), - "2001:db8:1::2".parse().unwrap(), - Icmpv6Types::EchoRequest, - Icmpv6Code(0), - "Hello, world!".as_bytes().to_vec(), - ); - - // Convert to raw bytes - let packet_bytes: Vec = packet.into(); - - // Check the contents - assert!(packet_bytes.len() >= 4 + 13); - assert_eq!(packet_bytes[0], Icmpv6Types::EchoRequest.0); - assert_eq!(packet_bytes[1], 0); - assert_eq!(u16::from_be_bytes([packet_bytes[2], packet_bytes[3]]), 0xe2f0); - assert_eq!( - &packet_bytes[4..], - "Hello, world!".as_bytes().to_vec().as_slice() - ); - } -} diff --git a/src/packet/protocols/ipv4.rs b/src/packet/protocols/ipv4.rs deleted file mode 100644 index 6b17f6b..0000000 --- a/src/packet/protocols/ipv4.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::net::Ipv4Addr; - -use pnet_packet::{ - ip::IpNextHeaderProtocol, - ipv4::{Ipv4Option, Ipv4OptionPacket}, - Packet, -}; - -use crate::packet::error::PacketError; - -#[derive(Debug, Clone)] -pub struct Ipv4Packet { - pub dscp: u8, - pub ecn: u8, - pub identification: u16, - pub flags: u8, - pub fragment_offset: u16, - pub ttl: u8, - pub protocol: IpNextHeaderProtocol, - pub source_address: Ipv4Addr, - pub destination_address: Ipv4Addr, - pub options: Vec, - pub payload: T, -} - -impl Ipv4Packet { - /// Construct a new IPv4 packet - #[allow(clippy::too_many_arguments)] - pub fn new( - dscp: u8, - ecn: u8, - identification: u16, - flags: u8, - fragment_offset: u16, - ttl: u8, - protocol: IpNextHeaderProtocol, - source_address: Ipv4Addr, - destination_address: Ipv4Addr, - options: Vec, - payload: T, - ) -> Self { - Self { - dscp, - ecn, - identification, - flags, - fragment_offset, - ttl, - protocol, - source_address, - destination_address, - options, - payload, - } - } - - #[allow(clippy::cast_possible_truncation)] - fn options_length_words(&self) -> u8 { - self.options - .iter() - .map(|option| Ipv4OptionPacket::packet_size(option) as u8) - .sum::() - / 4 - } -} - -impl TryFrom> for Ipv4Packet -where - T: From>, -{ - type Error = PacketError; - - fn try_from(bytes: Vec) -> Result { - // Parse the packet - let packet = pnet_packet::ipv4::Ipv4Packet::new(&bytes) - .ok_or(PacketError::TooShort(bytes.len(), bytes.clone()))?; - - // Return the packet - Ok(Self { - dscp: packet.get_dscp(), - ecn: packet.get_ecn(), - identification: packet.get_identification(), - flags: packet.get_flags(), - fragment_offset: packet.get_fragment_offset(), - ttl: packet.get_ttl(), - protocol: packet.get_next_level_protocol(), - source_address: packet.get_source(), - destination_address: packet.get_destination(), - options: packet.get_options(), - payload: packet.payload().to_vec().into(), - }) - } -} - -impl From> for Vec -where - T: Into> + Clone, -{ - fn from(packet: Ipv4Packet) -> Self { - // Convert the payload into raw bytes - let payload: Vec = packet.payload.clone().into(); - - // Build the packet - let total_length = 20 + (packet.options_length_words() as usize * 4) + payload.len(); - let mut output = - pnet_packet::ipv4::MutableIpv4Packet::owned(vec![0u8; total_length]).unwrap(); - - // Set the fields - output.set_version(4); - output.set_header_length(5 + packet.options_length_words()); - output.set_dscp(packet.dscp); - output.set_ecn(packet.ecn); - output.set_total_length(total_length.try_into().unwrap()); - output.set_identification(packet.identification); - output.set_flags(packet.flags); - output.set_fragment_offset(packet.fragment_offset); - output.set_ttl(packet.ttl); - output.set_next_level_protocol(packet.protocol); - output.set_source(packet.source_address); - output.set_destination(packet.destination_address); - output.set_options(&packet.options); - - // Set the payload - output.set_payload(&payload); - - // Calculate the checksum - output.set_checksum(0); - output.set_checksum(pnet_packet::ipv4::checksum(&output.to_immutable())); - - // Return the packet - output.to_immutable().packet().to_vec() - } -} diff --git a/src/packet/protocols/ipv6.rs b/src/packet/protocols/ipv6.rs deleted file mode 100644 index bd89377..0000000 --- a/src/packet/protocols/ipv6.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::net::Ipv6Addr; - -use pnet_packet::{ip::IpNextHeaderProtocol, Packet}; - -use crate::packet::error::PacketError; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Ipv6Packet { - pub traffic_class: u8, - pub flow_label: u32, - pub next_header: IpNextHeaderProtocol, - pub hop_limit: u8, - pub source_address: Ipv6Addr, - pub destination_address: Ipv6Addr, - pub payload: T, -} - -impl Ipv6Packet { - /// Construct a new IPv6 packet - pub fn new( - traffic_class: u8, - flow_label: u32, - next_header: IpNextHeaderProtocol, - hop_limit: u8, - source_address: Ipv6Addr, - destination_address: Ipv6Addr, - payload: T, - ) -> Self { - Self { - traffic_class, - flow_label, - next_header, - hop_limit, - source_address, - destination_address, - payload, - } - } -} - -impl TryFrom> for Ipv6Packet -where - T: From>, -{ - type Error = PacketError; - - fn try_from(bytes: Vec) -> Result { - // Parse the packet - let packet = pnet_packet::ipv6::Ipv6Packet::new(&bytes) - .ok_or(PacketError::TooShort(bytes.len(), bytes.clone()))?; - - // Return the packet - Ok(Self { - traffic_class: packet.get_traffic_class(), - flow_label: packet.get_flow_label(), - next_header: packet.get_next_header(), - hop_limit: packet.get_hop_limit(), - source_address: packet.get_source(), - destination_address: packet.get_destination(), - payload: packet.payload().to_vec().into(), - }) - } -} - -impl From> for Vec -where - T: Into>, -{ - fn from(packet: Ipv6Packet) -> Self { - // Convert the payload into raw bytes - let payload: Vec = packet.payload.into(); - - // Allocate a mutable packet to write into - let total_length = - pnet_packet::ipv6::MutableIpv6Packet::minimum_packet_size() + payload.len(); - let mut output = - pnet_packet::ipv6::MutableIpv6Packet::owned(vec![0u8; total_length]).unwrap(); - - // Write the header - output.set_version(6); - output.set_traffic_class(packet.traffic_class); - output.set_flow_label(packet.flow_label); - output.set_payload_length(u16::try_from(payload.len()).unwrap()); - output.set_next_header(packet.next_header); - output.set_hop_limit(packet.hop_limit); - output.set_source(packet.source_address); - output.set_destination(packet.destination_address); - - // Write the payload - output.set_payload(&payload); - - // Return the packet - output.to_immutable().packet().to_vec() - } -} diff --git a/src/packet/protocols/mod.rs b/src/packet/protocols/mod.rs deleted file mode 100644 index ae58976..0000000 --- a/src/packet/protocols/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod icmp; -pub mod icmpv6; -pub mod ipv4; -pub mod ipv6; -pub mod raw; -pub mod tcp; -pub mod udp; diff --git a/src/packet/protocols/raw.rs b/src/packet/protocols/raw.rs deleted file mode 100644 index 91e10e7..0000000 --- a/src/packet/protocols/raw.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::packet::error::PacketError; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct RawBytes(pub Vec); - -impl TryFrom> for RawBytes { - type Error = PacketError; - - fn try_from(bytes: Vec) -> Result { - Ok(Self(bytes)) - } -} - -impl From for Vec { - fn from(val: RawBytes) -> Self { - val.0 - } -} \ No newline at end of file diff --git a/src/packet/protocols/tcp.rs b/src/packet/protocols/tcp.rs deleted file mode 100644 index d1623e5..0000000 --- a/src/packet/protocols/tcp.rs +++ /dev/null @@ -1,246 +0,0 @@ -use std::net::{IpAddr, SocketAddr}; - -use pnet_packet::{ - tcp::{TcpOption, TcpOptionPacket}, - Packet, -}; - -use super::raw::RawBytes; -use crate::packet::error::PacketError; - -/// A TCP packet -#[derive(Debug, Clone)] -pub struct TcpPacket { - source: SocketAddr, - destination: SocketAddr, - pub sequence: u32, - pub ack_number: u32, - pub flags: u8, - pub window_size: u16, - pub urgent_pointer: u16, - pub options: Vec, - pub payload: T, -} - -impl TcpPacket { - /// Construct a new TCP packet - #[allow(clippy::too_many_arguments)] - pub fn new( - source: SocketAddr, - destination: SocketAddr, - sequence: u32, - ack_number: u32, - flags: u8, - window_size: u16, - urgent_pointer: u16, - options: Vec, - payload: T, - ) -> Result { - // Ensure the source and destination addresses are the same type - if source.is_ipv4() != destination.is_ipv4() { - return Err(PacketError::MismatchedAddressFamily( - source.ip(), - destination.ip(), - )); - } - - // Build the packet - Ok(Self { - source, - destination, - sequence, - ack_number, - flags, - window_size, - urgent_pointer, - options, - payload, - }) - } - - // Set a new source - #[allow(dead_code)] - pub fn set_source(&mut self, source: SocketAddr) -> Result<(), PacketError> { - // Ensure the source and destination addresses are the same type - if source.is_ipv4() != self.destination.is_ipv4() { - return Err(PacketError::MismatchedAddressFamily( - source.ip(), - self.destination.ip(), - )); - } - - // Set the source - self.source = source; - - Ok(()) - } - - // Set a new destination - #[allow(dead_code)] - pub fn set_destination(&mut self, destination: SocketAddr) -> Result<(), PacketError> { - // Ensure the source and destination addresses are the same type - if self.source.is_ipv4() != destination.is_ipv4() { - return Err(PacketError::MismatchedAddressFamily( - self.source.ip(), - destination.ip(), - )); - } - - // Set the destination - self.destination = destination; - - Ok(()) - } - - /// Get the source - pub fn source(&self) -> SocketAddr { - self.source - } - - /// Get the destination - pub fn destination(&self) -> SocketAddr { - self.destination - } - - /// Get the length of the options in words - #[allow(clippy::cast_possible_truncation)] - fn options_length(&self) -> u8 { - self.options - .iter() - .map(|option| TcpOptionPacket::packet_size(option) as u8) - .sum::() - } -} - -impl TcpPacket -where - T: From>, -{ - /// Construct a new TCP packet from bytes - #[allow(dead_code)] - pub fn new_from_bytes( - bytes: &[u8], - source_address: IpAddr, - destination_address: IpAddr, - ) -> Result { - // Ensure the source and destination addresses are the same type - if source_address.is_ipv4() != destination_address.is_ipv4() { - return Err(PacketError::MismatchedAddressFamily( - source_address, - destination_address, - )); - } - - // Parse the packet - let parsed = pnet_packet::tcp::TcpPacket::new(bytes) - .ok_or_else(|| PacketError::TooShort(bytes.len(), bytes.to_vec()))?; - - // Build the struct - Ok(Self { - source: SocketAddr::new(source_address, parsed.get_source()), - destination: SocketAddr::new(destination_address, parsed.get_destination()), - sequence: parsed.get_sequence(), - ack_number: parsed.get_acknowledgement(), - flags: parsed.get_flags(), - window_size: parsed.get_window(), - urgent_pointer: parsed.get_urgent_ptr(), - options: parsed.get_options().clone(), - payload: parsed.payload().to_vec().into(), - }) - } -} - -impl TcpPacket { - /// Construct a new TCP packet with a raw payload from bytes - pub fn new_from_bytes_raw_payload( - bytes: &[u8], - source_address: IpAddr, - destination_address: IpAddr, - ) -> Result { - // Ensure the source and destination addresses are the same type - if source_address.is_ipv4() != destination_address.is_ipv4() { - return Err(PacketError::MismatchedAddressFamily( - source_address, - destination_address, - )); - } - - // Parse the packet - let parsed = pnet_packet::tcp::TcpPacket::new(bytes) - .ok_or_else(|| PacketError::TooShort(bytes.len(), bytes.to_vec()))?; - - // Build the struct - Ok(Self { - source: SocketAddr::new(source_address, parsed.get_source()), - destination: SocketAddr::new(destination_address, parsed.get_destination()), - sequence: parsed.get_sequence(), - ack_number: parsed.get_acknowledgement(), - flags: parsed.get_flags(), - window_size: parsed.get_window(), - urgent_pointer: parsed.get_urgent_ptr(), - options: parsed.get_options().clone(), - payload: RawBytes(parsed.payload().to_vec()), - }) - } -} - -impl From> for Vec -where - T: Into>, -{ - fn from(packet: TcpPacket) -> Self { - // Get the options length in words - let options_length = packet.options_length(); - - // Convert the payload into raw bytes - let payload: Vec = packet.payload.into(); - - // Allocate a mutable packet to write into - let total_length = pnet_packet::tcp::MutableTcpPacket::minimum_packet_size() - + options_length as usize - + payload.len(); - let mut output = - pnet_packet::tcp::MutableTcpPacket::owned(vec![0u8; total_length]).unwrap(); - - // Write the source and dest ports - output.set_source(packet.source.port()); - output.set_destination(packet.destination.port()); - - // Write the sequence and ack numbers - output.set_sequence(packet.sequence); - output.set_acknowledgement(packet.ack_number); - - // Write the offset - output.set_data_offset(5 + (options_length / 4)); - - // Write the options - output.set_options(&packet.options); - - // Write the flags - output.set_flags(packet.flags); - - // Write the window size - output.set_window(packet.window_size); - - // Write the urgent pointer - output.set_urgent_ptr(packet.urgent_pointer); - - // Write the payload - output.set_payload(&payload); - - // Calculate the checksum - output.set_checksum(0); - output.set_checksum(match (packet.source.ip(), packet.destination.ip()) { - (IpAddr::V4(source_ip), IpAddr::V4(destination_ip)) => { - pnet_packet::tcp::ipv4_checksum(&output.to_immutable(), &source_ip, &destination_ip) - } - (IpAddr::V6(source_ip), IpAddr::V6(destination_ip)) => { - pnet_packet::tcp::ipv6_checksum(&output.to_immutable(), &source_ip, &destination_ip) - } - _ => unreachable!(), - }); - - // Return the raw bytes - output.packet().to_vec() - } -} diff --git a/src/packet/protocols/udp.rs b/src/packet/protocols/udp.rs deleted file mode 100644 index 98b73dd..0000000 --- a/src/packet/protocols/udp.rs +++ /dev/null @@ -1,215 +0,0 @@ -use std::net::{IpAddr, SocketAddr}; - -use pnet_packet::Packet; - -use super::raw::RawBytes; -use crate::packet::error::PacketError; - -/// A UDP packet -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct UdpPacket { - source: SocketAddr, - destination: SocketAddr, - pub payload: T, -} - -impl UdpPacket { - /// Construct a new UDP packet - pub fn new( - source: SocketAddr, - destination: SocketAddr, - payload: T, - ) -> Result { - // Ensure the source and destination addresses are the same type - if source.is_ipv4() != destination.is_ipv4() { - return Err(PacketError::MismatchedAddressFamily( - source.ip(), - destination.ip(), - )); - } - - // Build the packet - Ok(Self { - source, - destination, - payload, - }) - } - - // Set a new source - #[allow(dead_code)] - pub fn set_source(&mut self, source: SocketAddr) -> Result<(), PacketError> { - // Ensure the source and destination addresses are the same type - if source.is_ipv4() != self.destination.is_ipv4() { - return Err(PacketError::MismatchedAddressFamily( - source.ip(), - self.destination.ip(), - )); - } - - // Set the source - self.source = source; - - Ok(()) - } - - // Set a new destination - #[allow(dead_code)] - pub fn set_destination(&mut self, destination: SocketAddr) -> Result<(), PacketError> { - // Ensure the source and destination addresses are the same type - if self.source.is_ipv4() != destination.is_ipv4() { - return Err(PacketError::MismatchedAddressFamily( - self.source.ip(), - destination.ip(), - )); - } - - // Set the destination - self.destination = destination; - - Ok(()) - } - - /// Get the source - pub fn source(&self) -> SocketAddr { - self.source - } - - /// Get the destination - pub fn destination(&self) -> SocketAddr { - self.destination - } -} - -impl UdpPacket -where - T: From>, -{ - /// Construct a new UDP packet from bytes - #[allow(dead_code)] - pub fn new_from_bytes( - bytes: &[u8], - source_address: IpAddr, - destination_address: IpAddr, - ) -> Result { - // Ensure the source and destination addresses are the same type - if source_address.is_ipv4() != destination_address.is_ipv4() { - return Err(PacketError::MismatchedAddressFamily( - source_address, - destination_address, - )); - } - - // Parse the packet - let parsed = pnet_packet::udp::UdpPacket::new(bytes) - .ok_or_else(|| PacketError::TooShort(bytes.len(), bytes.to_vec()))?; - - // Build the struct - Ok(Self { - source: SocketAddr::new(source_address, parsed.get_source()), - destination: SocketAddr::new(destination_address, parsed.get_destination()), - payload: parsed.payload().to_vec().into(), - }) - } -} - -impl UdpPacket { - /// Construct a new UDP packet with a raw payload from bytes - pub fn new_from_bytes_raw_payload( - bytes: &[u8], - source_address: IpAddr, - destination_address: IpAddr, - ) -> Result { - // Ensure the source and destination addresses are the same type - if source_address.is_ipv4() != destination_address.is_ipv4() { - return Err(PacketError::MismatchedAddressFamily( - source_address, - destination_address, - )); - } - - // Parse the packet - let parsed = pnet_packet::udp::UdpPacket::new(bytes) - .ok_or_else(|| PacketError::TooShort(bytes.len(), bytes.to_vec()))?; - - // Build the struct - Ok(Self { - source: SocketAddr::new(source_address, parsed.get_source()), - destination: SocketAddr::new(destination_address, parsed.get_destination()), - payload: RawBytes(parsed.payload().to_vec()), - }) - } -} - -impl From> for Vec -where - T: Into>, -{ - fn from(packet: UdpPacket) -> Self { - // Convert the payload into raw bytes - let payload: Vec = packet.payload.into(); - - // Allocate a mutable packet to write into - let total_length = - pnet_packet::udp::MutableUdpPacket::minimum_packet_size() + payload.len(); - let mut output = - pnet_packet::udp::MutableUdpPacket::owned(vec![0u8; total_length]).unwrap(); - - // Write the source and dest ports - output.set_source(packet.source.port()); - output.set_destination(packet.destination.port()); - - // Write the length - output.set_length(u16::try_from(total_length).unwrap()); - - // Write the payload - output.set_payload(&payload); - - // Calculate the checksum - output.set_checksum(0); - output.set_checksum(match (packet.source.ip(), packet.destination.ip()) { - (IpAddr::V4(source_ip), IpAddr::V4(destination_ip)) => { - pnet_packet::udp::ipv4_checksum(&output.to_immutable(), &source_ip, &destination_ip) - } - (IpAddr::V6(source_ip), IpAddr::V6(destination_ip)) => { - pnet_packet::udp::ipv6_checksum(&output.to_immutable(), &source_ip, &destination_ip) - } - _ => unreachable!(), - }); - - // Return the raw bytes - output.packet().to_vec() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // Test packet construction - #[test] - #[rustfmt::skip] - fn test_packet_construction() { - // Make a new packet - let packet = UdpPacket::new( - "192.0.2.1:1234".parse().unwrap(), - "192.0.2.2:5678".parse().unwrap(), - "Hello, world!".as_bytes().to_vec(), - ) - .unwrap(); - - // Convert to raw bytes - let packet_bytes: Vec = packet.into(); - - // Check the contents - assert!(packet_bytes.len() >= 8 + 13); - assert_eq!(u16::from_be_bytes([packet_bytes[0], packet_bytes[1]]), 1234); - assert_eq!(u16::from_be_bytes([packet_bytes[2], packet_bytes[3]]), 5678); - assert_eq!(u16::from_be_bytes([packet_bytes[4], packet_bytes[5]]), 8 + 13); - assert_eq!(u16::from_be_bytes([packet_bytes[6], packet_bytes[7]]), 0x1f74); - assert_eq!( - &packet_bytes[8..], - "Hello, world!".as_bytes().to_vec().as_slice() - ); - } -} diff --git a/src/packet/xlat/icmp/mod.rs b/src/packet/xlat/icmp/mod.rs deleted file mode 100644 index f0479b0..0000000 --- a/src/packet/xlat/icmp/mod.rs +++ /dev/null @@ -1,113 +0,0 @@ -#![allow(clippy::doc_markdown)] - -use std::net::{Ipv4Addr, Ipv6Addr}; - -use pnet_packet::{icmp::IcmpTypes, icmpv6::Icmpv6Types}; - -use crate::{ - metrics::ICMP_COUNTER, - packet::{ - error::PacketError, - protocols::{icmp::IcmpPacket, icmpv6::Icmpv6Packet, raw::RawBytes}, - }, -}; - -use super::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4}; - -mod type_code; - -/// Translates an ICMP packet to an ICMPv6 packet -pub fn translate_icmp_to_icmpv6( - input: IcmpPacket, - new_source: Ipv6Addr, - new_destination: Ipv6Addr, -) -> Result, PacketError> { - ICMP_COUNTER - .with_label_values(&[ - "icmp", - &input.icmp_type.0.to_string(), - &input.icmp_code.0.to_string(), - ]) - .inc(); - - // Translate the type and code - let (icmpv6_type, icmpv6_code) = - type_code::translate_type_and_code_4_to_6(input.icmp_type, input.icmp_code)?; - - // Some ICMP types require special payload edits - let payload = match icmpv6_type { - Icmpv6Types::TimeExceeded => { - // In this case, the current payload looks like: 4bytes + Ipv4(Data) - // This needs to be translated to: 4bytes + Ipv6(Data) - let inner_payload = input.payload.0[4..].to_vec(); - - // Translate - let inner_payload = - translate_ipv4_to_ipv6(inner_payload.try_into()?, new_source, new_destination)?; - let inner_payload: Vec = inner_payload.into(); - - // Build the new payload - RawBytes({ - let mut buffer = Vec::with_capacity(4 + inner_payload.len()); - buffer.extend_from_slice(&input.payload.0[..4]); - buffer.extend_from_slice(&inner_payload); - buffer - }) - } - _ => input.payload, - }; - - // Build output packet - Ok(Icmpv6Packet::new( - new_source, - new_destination, - icmpv6_type, - icmpv6_code, - payload, - )) -} - -/// Translates an ICMPv6 packet to an ICMP packet -pub fn translate_icmpv6_to_icmp( - input: Icmpv6Packet, - new_source: Ipv4Addr, - new_destination: Ipv4Addr, -) -> Result, PacketError> { - ICMP_COUNTER - .with_label_values(&[ - "icmpv6", - &input.icmp_type.0.to_string(), - &input.icmp_code.0.to_string(), - ]) - .inc(); - - // Translate the type and code - let (icmp_type, icmp_code) = - type_code::translate_type_and_code_6_to_4(input.icmp_type, input.icmp_code)?; - - // Some ICMP types require special payload edits - let payload = match icmp_type { - IcmpTypes::TimeExceeded => { - // In this case, the current payload looks like: 4bytes + Ipv6(Data) - // This needs to be translated to: 4bytes + Ipv4(Data) - let inner_payload = input.payload.0[4..].to_vec(); - - // Translate - let inner_payload = - translate_ipv6_to_ipv4(&inner_payload.try_into()?, new_source, new_destination)?; - let inner_payload: Vec = inner_payload.into(); - - // Build the new payload - RawBytes({ - let mut buffer = Vec::with_capacity(4 + inner_payload.len()); - buffer.extend_from_slice(&input.payload.0[..4]); - buffer.extend_from_slice(&inner_payload); - buffer - }) - } - _ => input.payload, - }; - - // Build output packet - Ok(IcmpPacket::new(icmp_type, icmp_code, payload)) -} diff --git a/src/packet/xlat/icmp/type_code.rs b/src/packet/xlat/icmp/type_code.rs deleted file mode 100644 index 8aead6c..0000000 --- a/src/packet/xlat/icmp/type_code.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Functions to map between ICMP and ICMPv6 types/codes - -#![allow(clippy::doc_markdown)] - -use pnet_packet::{ - icmp::{destination_unreachable, IcmpCode, IcmpType, IcmpTypes}, - icmpv6::{Icmpv6Code, Icmpv6Type, Icmpv6Types}, -}; - -use crate::packet::error::PacketError; - -/// Best effort translation from an ICMP type and code to an ICMPv6 type and code -#[allow(clippy::deprecated_cfg_attr)] -pub fn translate_type_and_code_4_to_6( - icmp_type: IcmpType, - icmp_code: IcmpCode, -) -> Result<(Icmpv6Type, Icmpv6Code), PacketError> { - match (icmp_type, icmp_code) { - // Echo Request - (IcmpTypes::EchoRequest, _) => Ok((Icmpv6Types::EchoRequest, Icmpv6Code(0))), - - // Echo Reply - (IcmpTypes::EchoReply, _) => Ok((Icmpv6Types::EchoReply, Icmpv6Code(0))), - - // Packet Too Big - ( - IcmpTypes::DestinationUnreachable, - destination_unreachable::IcmpCodes::FragmentationRequiredAndDFFlagSet, - ) => Ok((Icmpv6Types::PacketTooBig, Icmpv6Code(0))), - - // Destination Unreachable - (IcmpTypes::DestinationUnreachable, icmp_code) => Ok(( - Icmpv6Types::DestinationUnreachable, - #[cfg_attr(rustfmt, rustfmt_skip)] - #[allow(clippy::match_same_arms)] - Icmpv6Code(match icmp_code { - destination_unreachable::IcmpCodes::DestinationHostUnreachable => 3, - destination_unreachable::IcmpCodes::DestinationProtocolUnreachable => 4, - destination_unreachable::IcmpCodes::DestinationPortUnreachable => 4, - destination_unreachable::IcmpCodes::SourceRouteFailed => 5, - destination_unreachable::IcmpCodes::SourceHostIsolated => 2, - destination_unreachable::IcmpCodes::NetworkAdministrativelyProhibited => 1, - destination_unreachable::IcmpCodes::HostAdministrativelyProhibited => 1, - destination_unreachable::IcmpCodes::CommunicationAdministrativelyProhibited => 1, - - // Default to No Route to Destination - _ => 0, - }), - )), - - // Time Exceeded - (IcmpTypes::TimeExceeded, icmp_code) => { - Ok((Icmpv6Types::TimeExceeded, Icmpv6Code(icmp_code.0))) - } - - // Default unsupported - (icmp_type, _) => Err(PacketError::UnsupportedIcmpType(icmp_type.0)), - } -} - -/// Best effort translation from an ICMPv6 type and code to an ICMP type and code -#[allow(clippy::deprecated_cfg_attr)] -pub fn translate_type_and_code_6_to_4( - icmp_type: Icmpv6Type, - icmp_code: Icmpv6Code, -) -> Result<(IcmpType, IcmpCode), PacketError> { - match (icmp_type, icmp_code) { - // Echo Request - (Icmpv6Types::EchoRequest, _) => Ok((IcmpTypes::EchoRequest, IcmpCode(0))), - - // Echo Reply - (Icmpv6Types::EchoReply, _) => Ok((IcmpTypes::EchoReply, IcmpCode(0))), - - // Packet Too Big - (Icmpv6Types::PacketTooBig, _) => Ok(( - IcmpTypes::DestinationUnreachable, - destination_unreachable::IcmpCodes::FragmentationRequiredAndDFFlagSet, - )), - - // Destination Unreachable - (Icmpv6Types::DestinationUnreachable, icmp_code) => Ok(( - IcmpTypes::DestinationUnreachable, - #[cfg_attr(rustfmt, rustfmt_skip)] - #[allow(clippy::match_same_arms)] - match icmp_code.0 { - 1 => destination_unreachable::IcmpCodes::CommunicationAdministrativelyProhibited, - 2 => destination_unreachable::IcmpCodes::SourceHostIsolated, - 3 => destination_unreachable::IcmpCodes::DestinationHostUnreachable, - 4 => destination_unreachable::IcmpCodes::DestinationPortUnreachable, - 5 => destination_unreachable::IcmpCodes::SourceRouteFailed, - _ => destination_unreachable::IcmpCodes::DestinationNetworkUnreachable, - }, - )), - - // Time Exceeded - (Icmpv6Types::TimeExceeded, icmp_code) => { - Ok((IcmpTypes::TimeExceeded, IcmpCode(icmp_code.0))) - } - - // Default unsupported - (icmp_type, _) => Err(PacketError::UnsupportedIcmpv6Type(icmp_type.0)), - } -} diff --git a/src/packet/xlat/ip.rs b/src/packet/xlat/ip.rs deleted file mode 100644 index ae9804f..0000000 --- a/src/packet/xlat/ip.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; - -use pnet_packet::ip::IpNextHeaderProtocols; - -use crate::{ - packet::protocols::{icmp::IcmpPacket, tcp::TcpPacket, udp::UdpPacket}, - packet::{ - error::PacketError, - protocols::{icmpv6::Icmpv6Packet, ipv4::Ipv4Packet, ipv6::Ipv6Packet, raw::RawBytes}, - }, -}; - -use super::{ - icmp::{translate_icmp_to_icmpv6, translate_icmpv6_to_icmp}, - tcp::{translate_tcp4_to_tcp6, translate_tcp6_to_tcp4}, - udp::{translate_udp4_to_udp6, translate_udp6_to_udp4}, -}; - -/// Translates an IPv4 packet to an IPv6 packet -pub fn translate_ipv4_to_ipv6( - input: Ipv4Packet>, - new_source: Ipv6Addr, - new_destination: Ipv6Addr, -) -> Result>, PacketError> { - // Perform recursive translation to determine the new payload - let new_payload = match input.protocol { - IpNextHeaderProtocols::Icmp => { - let icmp_input: IcmpPacket = input.payload.try_into()?; - translate_icmp_to_icmpv6(icmp_input, new_source, new_destination)?.into() - } - IpNextHeaderProtocols::Udp => { - let udp_input: UdpPacket = UdpPacket::new_from_bytes_raw_payload( - &input.payload, - IpAddr::V4(input.source_address), - IpAddr::V4(input.destination_address), - )?; - translate_udp4_to_udp6(udp_input, new_source, new_destination)?.into() - } - IpNextHeaderProtocols::Tcp => { - let tcp_input: TcpPacket = TcpPacket::new_from_bytes_raw_payload( - &input.payload, - IpAddr::V4(input.source_address), - IpAddr::V4(input.destination_address), - )?; - translate_tcp4_to_tcp6(tcp_input, new_source, new_destination)?.into() - } - _ => { - log::warn!("Unsupported next level protocol: {}", input.protocol); - input.payload - } - }; - - // Build the output IPv6 packet - let output = Ipv6Packet::new( - 0, - 0, - match input.protocol { - IpNextHeaderProtocols::Icmp => IpNextHeaderProtocols::Icmpv6, - proto => proto, - }, - input.ttl, - new_source, - new_destination, - new_payload, - ); - - // Return the output - Ok(output) -} - -/// Translates an IPv6 packet to an IPv4 packet -pub fn translate_ipv6_to_ipv4( - input: &Ipv6Packet>, - new_source: Ipv4Addr, - new_destination: Ipv4Addr, -) -> Result>, PacketError> { - // Perform recursive translation to determine the new payload - let new_payload = match input.next_header { - IpNextHeaderProtocols::Icmpv6 => { - let icmpv6_input: Icmpv6Packet = Icmpv6Packet::new_from_bytes_raw_payload( - &input.payload, - input.source_address, - input.destination_address, - )?; - Some(translate_icmpv6_to_icmp(icmpv6_input, new_source, new_destination)?.into()) - } - IpNextHeaderProtocols::Udp => { - let udp_input: UdpPacket = UdpPacket::new_from_bytes_raw_payload( - &input.payload, - IpAddr::V6(input.source_address), - IpAddr::V6(input.destination_address), - )?; - Some(translate_udp6_to_udp4(udp_input, new_source, new_destination)?.into()) - } - IpNextHeaderProtocols::Tcp => { - let tcp_input: TcpPacket = TcpPacket::new_from_bytes_raw_payload( - &input.payload, - IpAddr::V6(input.source_address), - IpAddr::V6(input.destination_address), - )?; - Some(translate_tcp6_to_tcp4(tcp_input, new_source, new_destination)?.into()) - } - _ => { - log::warn!("Unsupported next level protocol: {}", input.next_header); - None - } - }; - - // Build the output IPv4 packet - let output = Ipv4Packet::new( - 0, - 0, - 0, - 0, - 0, - input.hop_limit, - match input.next_header { - IpNextHeaderProtocols::Icmpv6 => IpNextHeaderProtocols::Icmp, - proto => proto, - }, - new_source, - new_destination, - vec![], - new_payload.unwrap_or_default(), - ); - - // Return the output - Ok(output) -} diff --git a/src/packet/xlat/mod.rs b/src/packet/xlat/mod.rs deleted file mode 100644 index 0c842c1..0000000 --- a/src/packet/xlat/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Protocol translation logic - -pub mod icmp; -pub mod ip; -pub mod tcp; -pub mod udp; diff --git a/src/packet/xlat/tcp.rs b/src/packet/xlat/tcp.rs deleted file mode 100644 index 2b456c3..0000000 --- a/src/packet/xlat/tcp.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - -use crate::packet::{ - error::PacketError, - protocols::{raw::RawBytes, tcp::TcpPacket}, -}; - -/// Translates an IPv4 TCP packet to an IPv6 TCP packet -pub fn translate_tcp4_to_tcp6( - input: TcpPacket, - new_source_addr: Ipv6Addr, - new_destination_addr: Ipv6Addr, -) -> Result, PacketError> { - // Build the packet - TcpPacket::new( - SocketAddr::new(IpAddr::V6(new_source_addr), input.source().port()), - SocketAddr::new(IpAddr::V6(new_destination_addr), input.destination().port()), - input.sequence, - input.ack_number, - input.flags, - input.window_size, - input.urgent_pointer, - input.options, - input.payload, - ) -} - -/// Translates an IPv6 TCP packet to an IPv4 TCP packet -pub fn translate_tcp6_to_tcp4( - input: TcpPacket, - new_source_addr: Ipv4Addr, - new_destination_addr: Ipv4Addr, -) -> Result, PacketError> { - // Build the packet - TcpPacket::new( - SocketAddr::new(IpAddr::V4(new_source_addr), input.source().port()), - SocketAddr::new(IpAddr::V4(new_destination_addr), input.destination().port()), - input.sequence, - input.ack_number, - input.flags, - input.window_size, - input.urgent_pointer, - input.options, - input.payload, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_translate_tcp4_to_tcp6() { - let input = TcpPacket::new( - "192.0.2.1:1234".parse().unwrap(), - "192.0.2.2:5678".parse().unwrap(), - 123456, - 654321, - 0, - 4096, - 0, - Vec::new(), - RawBytes("Hello, world!".as_bytes().to_vec()), - ) - .unwrap(); - - let result = translate_tcp4_to_tcp6( - input, - "2001:db8::1".parse().unwrap(), - "2001:db8::2".parse().unwrap(), - ) - .unwrap(); - - assert_eq!(result.source(), "[2001:db8::1]:1234".parse().unwrap()); - assert_eq!(result.destination(), "[2001:db8::2]:5678".parse().unwrap()); - assert_eq!(result.sequence, 123456); - assert_eq!(result.ack_number, 654321); - assert_eq!(result.flags, 0); - assert_eq!(result.window_size, 4096); - assert_eq!(result.urgent_pointer, 0); - assert_eq!(result.options.len(), 0); - assert_eq!( - result.payload, - RawBytes("Hello, world!".as_bytes().to_vec()) - ); - } - - #[test] - fn test_translate_tcp6_to_tcp4() { - let input = TcpPacket::new( - "[2001:db8::1]:1234".parse().unwrap(), - "[2001:db8::2]:5678".parse().unwrap(), - 123456, - 654321, - 0, - 4096, - 0, - Vec::new(), - RawBytes("Hello, world!".as_bytes().to_vec()), - ) - .unwrap(); - - let result = translate_tcp6_to_tcp4( - input, - "192.0.2.1".parse().unwrap(), - "192.0.2.2".parse().unwrap(), - ) - .unwrap(); - - assert_eq!(result.source(), "192.0.2.1:1234".parse().unwrap()); - assert_eq!(result.destination(), "192.0.2.2:5678".parse().unwrap()); - assert_eq!(result.sequence, 123456); - assert_eq!(result.ack_number, 654321); - assert_eq!(result.flags, 0); - assert_eq!(result.window_size, 4096); - assert_eq!(result.urgent_pointer, 0); - assert_eq!(result.options.len(), 0); - assert_eq!( - result.payload, - RawBytes("Hello, world!".as_bytes().to_vec()) - ); - } -} diff --git a/src/packet/xlat/udp.rs b/src/packet/xlat/udp.rs deleted file mode 100644 index df2a74b..0000000 --- a/src/packet/xlat/udp.rs +++ /dev/null @@ -1,97 +0,0 @@ -use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; - -use crate::packet::{ - error::PacketError, - protocols::{raw::RawBytes, udp::UdpPacket}, -}; - -/// Translates an IPv4 UDP packet to an IPv6 UDP packet -pub fn translate_udp4_to_udp6( - input: UdpPacket, - new_source_addr: Ipv6Addr, - new_destination_addr: Ipv6Addr, -) -> Result, PacketError> { - // Build the packet - UdpPacket::new( - SocketAddr::new(IpAddr::V6(new_source_addr), input.source().port()), - SocketAddr::new(IpAddr::V6(new_destination_addr), input.destination().port()), - input.payload, - ) -} - -/// Translates an IPv6 UDP packet to an IPv4 UDP packet -pub fn translate_udp6_to_udp4( - input: UdpPacket, - new_source_addr: Ipv4Addr, - new_destination_addr: Ipv4Addr, -) -> Result, PacketError> { - // Build the packet - UdpPacket::new( - SocketAddr::new(IpAddr::V4(new_source_addr), input.source().port()), - SocketAddr::new(IpAddr::V4(new_destination_addr), input.destination().port()), - input.payload, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::packet::protocols::udp::UdpPacket; - - #[test] - fn test_translate_udp4_to_udp6() { - // Create an IPv4 UDP packet - let ipv4_packet = UdpPacket::new( - "192.0.2.1:1234".parse().unwrap(), - "192.0.2.2:5678".parse().unwrap(), - RawBytes("Hello, world!".as_bytes().to_vec()), - ) - .unwrap(); - - // Translate the packet to IPv6 - let ipv6_packet = translate_udp4_to_udp6( - ipv4_packet, - "2001:db8::1".parse().unwrap(), - "2001:db8::2".parse().unwrap(), - ) - .unwrap(); - - // Ensure the translation is correct - assert_eq!(ipv6_packet.source(), "[2001:db8::1]:1234".parse().unwrap()); - assert_eq!( - ipv6_packet.destination(), - "[2001:db8::2]:5678".parse().unwrap() - ); - assert_eq!( - ipv6_packet.payload, - RawBytes("Hello, world!".as_bytes().to_vec()) - ); - } - - #[test] - fn test_translate_udp6_to_udp4() { - // Create an IPv6 UDP packet - let ipv6_packet = UdpPacket::new( - "[2001:db8::1]:1234".parse().unwrap(), - "[2001:db8::2]:5678".parse().unwrap(), - RawBytes("Hello, world!".as_bytes().to_vec()), - ) - .unwrap(); - - // Translate the packet to IPv4 - let ipv4_packet = translate_udp6_to_udp4( - ipv6_packet, - "192.0.2.1".parse().unwrap(), - "192.0.2.2".parse().unwrap(), - ) - .unwrap(); - - // Ensure the translation is correct - assert_eq!(ipv4_packet.source(), "192.0.2.1:1234".parse().unwrap()); - assert_eq!(ipv4_packet.destination(), "192.0.2.2:5678".parse().unwrap()); - assert_eq!( - ipv4_packet.payload, - RawBytes("Hello, world!".as_bytes().to_vec()) - ); - } -} From 87547f59b625e89781a0c4d435acaaa2cb8658ce Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 16:53:41 -0400 Subject: [PATCH 07/78] Starting to build binary entrypoints --- Cargo.toml | 94 +++++++++++++++++++++++----------- src/cli/cli.rs | 16 ------ src/cli/config.rs | 94 ---------------------------------- src/cli/main.rs | 52 ------------------- src/{cli => common}/logging.rs | 0 src/common/mod.rs | 3 ++ src/lib.rs | 12 ----- src/metrics/http.rs | 45 ---------------- src/metrics/metrics.rs | 34 ------------ src/metrics/mod.rs | 6 --- src/protomask-6over4.rs | 5 ++ src/protomask-clat.rs | 51 ++++++++++++++++++ src/protomask.rs | 5 ++ 13 files changed, 129 insertions(+), 288 deletions(-) delete mode 100644 src/cli/cli.rs delete mode 100644 src/cli/config.rs delete mode 100644 src/cli/main.rs rename src/{cli => common}/logging.rs (100%) create mode 100644 src/common/mod.rs delete mode 100644 src/lib.rs delete mode 100644 src/metrics/http.rs delete mode 100644 src/metrics/metrics.rs delete mode 100644 src/metrics/mod.rs create mode 100644 src/protomask-6over4.rs create mode 100644 src/protomask-clat.rs create mode 100644 src/protomask.rs diff --git a/Cargo.toml b/Cargo.toml index 348c03e..7f59c58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,17 @@ -# [package] -# name = "protomask" -# version = "0.2.0" -# authors = ["Evan Pratten "] -# edition = "2021" -# description = "A user space NAT64 implementation" -# readme = "README.md" -# homepage = "https://github.com/ewpratten/protomask" -# documentation = "https://docs.rs/protomask" -# repository = "https://github.com/ewpratten/protomask" -# license = "GPL-3.0" -# keywords = [] -# categories = [] +[package] +name = "protomask" +version = "0.2.0" +authors = ["Evan Pratten "] +edition = "2021" +description = "A user space NAT64 implementation" +readme = "README.md" +homepage = "https://github.com/ewpratten/protomask" +documentation = "https://docs.rs/protomask" +repository = "https://github.com/ewpratten/protomask" +license = "GPL-3.0" +keywords = [] +categories = [] +exclude = ["/.github/", "/.vscode/"] # # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # [dependencies] @@ -39,21 +40,56 @@ # prometheus = "0.13.3" # lazy_static = "1.4.0" -# [[bin]] -# name = "protomask" -# path = "src/cli/main.rs" - -# [package.metadata.deb] -# section = "network" -# assets = [ -# ["target/release/protomask", "/usr/local/bin/protomask", "755"], -# ["./protomask.toml", "/etc/protomask.toml", "644"], -# ["README.md", "usr/share/doc/protomask/README.md", "644"] -# ] -# conf-files = ["/etc/protomask.toml"] -# depends = [] -# maintainer-scripts = "./debian/" -# systemd-units = { enable = false } - [workspace] members = ["libs/easy-tun", "libs/fast-nat", "libs/interproto"] + +[[bin]] +name = "protomask" +path = "src/protomask.rs" + +[[bin]] +name = "protomask-clat" +path = "src/protomask-clat.rs" + +[[bin]] +name = "protomask-6over4" +path = "src/protomask-6over4.rs" + +[dependencies] +# Internal dependencies +easy-tun = { path = "libs/easy-tun" } +fast-nat = { path = "libs/fast-nat" } +interproto = { path = "libs/interproto" } + +# External Dependencies +tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } +owo-colors = { version = "3.5.0", features = ["supports-colors"] } +clap = { version = "4.3.11", features = ["derive"] } +log = "0.4.19" +fern = "0.6.2" +ipnet = "2.8.0" +nix = "0.26.2" + +[package.metadata.deb] +section = "network" +assets = [ + [ + "target/release/protomask", + "/usr/local/bin/protomask", + "755", + ], + [ + "./protomask.toml", + "/etc/protomask.toml", + "644", + ], + [ + "README.md", + "usr/share/doc/protomask/README.md", + "644", + ], +] +conf-files = ["/etc/protomask.toml"] +depends = [] +maintainer-scripts = "./debian/" +systemd-units = { enable = false } diff --git a/src/cli/cli.rs b/src/cli/cli.rs deleted file mode 100644 index 8ccd700..0000000 --- a/src/cli/cli.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Command line argument definitions - -use std::path::PathBuf; - -use clap::Parser; - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -pub struct Args { - /// Path to the config file - pub config_file: PathBuf, - - /// Enable verbose logging - #[clap(short, long)] - pub verbose: bool, -} diff --git a/src/cli/config.rs b/src/cli/config.rs deleted file mode 100644 index 50e46f6..0000000 --- a/src/cli/config.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! Serde definitions for the config file - -use std::{ - net::{Ipv4Addr, Ipv6Addr, SocketAddr}, - path::Path, - time::Duration, -}; - -use ipnet::{Ipv4Net, Ipv6Net}; - -/// A static mapping rule -#[derive(Debug, serde::Deserialize)] -pub struct AddressMappingRule { - /// IPv4 address - pub v4: Ipv4Addr, - /// IPv6 address - pub v6: Ipv6Addr, -} - -/// Used to generate the default reservation duration -fn default_reservation_duration() -> u64 { - 7200 -} - -/// Rules config -#[derive(Debug, serde::Deserialize)] -pub struct PoolConfig { - /// Pool prefixes - #[serde(rename = "Prefixes")] - pub prefixes: Vec, - /// Static mapping rules - #[serde(rename = "Static", default = "Vec::new")] - pub static_map: Vec, - /// How long to hold a dynamic mapping for - #[serde(rename = "MaxIdleDuration", default = "default_reservation_duration")] - reservation_duration: u64, -} - -impl PoolConfig { - /// Get the reservation duration - pub fn reservation_duration(&self) -> Duration { - Duration::from_secs(self.reservation_duration) - } -} - -/// Representation of the `protomask.toml` config file -#[derive(Debug, serde::Deserialize)] -pub struct Config { - /// The NAT64 prefix - #[serde(rename = "Nat64Prefix")] - pub nat64_prefix: Ipv6Net, - /// Address to bind to for prometheus support - #[serde(rename = "Prometheus")] - pub prom_bind_addr: Option, - /// Pool configuration - #[serde(rename = "Pool")] - pub pool: PoolConfig, -} - -impl Config { - /// Load the config from a file - pub fn load>(path: P) -> Result { - // Load the file - let file_contents = std::fs::read_to_string(path)?; - - // Build the deserializer - let deserializer = toml::Deserializer::new(&file_contents); - - // Parse - match serde_path_to_error::deserialize(deserializer) { - Ok(config) => Ok(config), - // If there is a parsing error, display a reasonable error message - Err(e) => { - eprintln!( - "Failed to parse config file due to:\n {}\n at {}", - e.inner().message(), - e.path() - ); - std::process::exit(1); - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Test that fails if the example file is not valid - #[test] - fn ensure_example_is_valid() { - let _ = Config::load("protomask.toml").unwrap(); - } -} diff --git a/src/cli/main.rs b/src/cli/main.rs deleted file mode 100644 index f2f2355..0000000 --- a/src/cli/main.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! This is the entrypoint for `protomask` from the command line. - -use clap::Parser; -use config::Config; -use logging::enable_logger; -use protomask::nat::Nat64; - -mod cli; -mod config; -mod logging; - -#[tokio::main] -pub async fn main() { - // Parse CLI args - let args = cli::Args::parse(); - - // Set up logging - enable_logger(args.verbose); - - // Parse the config file - let config = Config::load(args.config_file).unwrap(); - - // Currently, only a /96 is supported - if config.nat64_prefix.prefix_len() != 96 { - log::error!("Only a /96 prefix is supported for the NAT64 prefix"); - std::process::exit(1); - } - - // Create the NAT64 instance - let mut nat64 = Nat64::new( - config.nat64_prefix, - config.pool.prefixes.clone(), - config - .pool - .static_map - .iter() - .map(|rule| (rule.v6, rule.v4)) - .collect(), - config.pool.reservation_duration(), - ) - .await - .unwrap(); - - // Handle metrics requests - if let Some(bind_addr) = config.prom_bind_addr { - log::info!("Enabling metrics server on {}", bind_addr); - tokio::spawn(protomask::metrics::serve_metrics(bind_addr)); - } - - // Handle packets - nat64.run().await.unwrap(); -} diff --git a/src/cli/logging.rs b/src/common/logging.rs similarity index 100% rename from src/cli/logging.rs rename to src/common/logging.rs diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..e568b1f --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,3 @@ +//! Common code used across all protomask binaries + +pub mod logging; diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 14f189f..0000000 --- a/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! # Protomask library -//! -//! *Note: There is a fair chance you are looking for `src/cli/main.rs` instead of this file.* - -#![deny(clippy::pedantic)] -#![allow(clippy::module_name_repetitions)] -#![allow(clippy::missing_errors_doc)] -#![allow(clippy::missing_panics_doc)] - -pub mod metrics; -pub mod nat; -mod packet; diff --git a/src/metrics/http.rs b/src/metrics/http.rs deleted file mode 100644 index 5f88e1c..0000000 --- a/src/metrics/http.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::{convert::Infallible, net::SocketAddr}; - -use hyper::{ - service::{make_service_fn, service_fn}, - Body, Method, Request, Response, Server, -}; -use prometheus::{Encoder, TextEncoder}; - -/// Handle an HTTP request -#[allow(clippy::unused_async)] -async fn handle_request(request: Request) -> Result, Infallible> { - // If the request is targeting the metrics endpoint - if request.method() == Method::GET && request.uri().path() == "/metrics" { - // Gather metrics - let metric_families = prometheus::gather(); - let body = { - let mut buffer = Vec::new(); - let encoder = TextEncoder::new(); - encoder.encode(&metric_families, &mut buffer).unwrap(); - String::from_utf8(buffer).unwrap() - }; - - // Return the response - return Ok(Response::new(Body::from(body))); - } - - // Otherwise, just return a 404 - Ok(Response::builder() - .status(404) - .body(Body::from("Not found")) - .unwrap()) -} - -/// Bring up an HTTP server that listens for metrics requests -pub async fn serve_metrics(bind_addr: SocketAddr) { - // Set up the server - let make_service = - make_service_fn(|_| async { Ok::<_, Infallible>(service_fn(handle_request)) }); - let server = Server::bind(&bind_addr).serve(make_service); - - // Run the server - if let Err(e) = server.await { - eprintln!("Metrics server error: {e}"); - } -} diff --git a/src/metrics/metrics.rs b/src/metrics/metrics.rs deleted file mode 100644 index ba32849..0000000 --- a/src/metrics/metrics.rs +++ /dev/null @@ -1,34 +0,0 @@ -use lazy_static::lazy_static; -use prometheus::{ - register_int_counter_vec, register_int_gauge, register_int_gauge_vec, IntCounterVec, IntGauge, - IntGaugeVec, -}; - -lazy_static! { - /// Counter for the number of packets processes - pub static ref PACKET_COUNTER: IntCounterVec = register_int_counter_vec!( - "packets", - "Number of packets processed", - &["protocol", "status"] - ).unwrap(); - - /// Counter for ICMP packet types - pub static ref ICMP_COUNTER: IntCounterVec = register_int_counter_vec!( - "icmp", - "Number of ICMP packets processed", - &["protocol", "type", "code"] - ).unwrap(); - - /// Gauge for the number of addresses in the IPv4 pool - pub static ref IPV4_POOL_SIZE: IntGauge = register_int_gauge!( - "ipv4_pool_size", - "Number of IPv4 addresses in the pool" - ).unwrap(); - - /// Gauge for the number of addresses currently reserved in the IPv4 pool - pub static ref IPV4_POOL_RESERVED: IntGaugeVec = register_int_gauge_vec!( - "ipv4_pool_reserved", - "Number of IPv4 addresses currently reserved", - &["static"] - ).unwrap(); -} diff --git a/src/metrics/mod.rs b/src/metrics/mod.rs deleted file mode 100644 index e79cd4c..0000000 --- a/src/metrics/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod http; -#[allow(clippy::module_inception)] -mod metrics; - -pub use http::serve_metrics; -pub(crate) use metrics::*; diff --git a/src/protomask-6over4.rs b/src/protomask-6over4.rs new file mode 100644 index 0000000..9d38cbe --- /dev/null +++ b/src/protomask-6over4.rs @@ -0,0 +1,5 @@ + +#[tokio::main] +pub async fn main(){ + +} \ No newline at end of file diff --git a/src/protomask-clat.rs b/src/protomask-clat.rs new file mode 100644 index 0000000..150b650 --- /dev/null +++ b/src/protomask-clat.rs @@ -0,0 +1,51 @@ +//! Entrypoint for the `protomask-clat` binary. +//! +//! This binary is a Customer-side transLATor (CLAT) that translates all native +//! IPv4 traffic to IPv6 traffic for transmission over an IPv6-only ISP network. + +use clap::Parser; +use common::logging::enable_logger; +use easy_tun::Tun; +use ipnet::Ipv6Net; +use nix::unistd::Uid; + +mod common; + +#[derive(Debug, Parser)] +#[clap(author, version, about="IPv4 to IPv6 Customer-side transLATor (CLAT)", long_about = None)] +struct Args { + /// IPv6 prefix to embed IPv4 addresses in + #[clap(long="via", default_value_t = ("64:ff9b::/96").parse().unwrap())] + embed_prefix: Ipv6Net, + + /// Explicitly set the interface name to use + #[clap(short, long, default_value_t = ("clat%d").to_string())] + interface: String, + + /// Enable verbose logging + #[clap(short, long)] + verbose: bool, +} + +#[tokio::main] +pub async fn main() { + // Parse CLI args + let args = Args::parse(); + + // Initialize logging + enable_logger(args.verbose); + + // We must be root to continue program execution + if !Uid::effective().is_root() { + log::error!("This program must be run as root"); + std::process::exit(1); + } + + // Bring up a TUN interface + let mut tun = Tun::new(&args.interface).unwrap(); + + log::info!("Translating packets on {}", tun.name()); + loop { + + } +} diff --git a/src/protomask.rs b/src/protomask.rs new file mode 100644 index 0000000..9d38cbe --- /dev/null +++ b/src/protomask.rs @@ -0,0 +1,5 @@ + +#[tokio::main] +pub async fn main(){ + +} \ No newline at end of file From 7e6abe67bbe22d31cac09ca682a5be884243f14d Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 18:51:20 -0400 Subject: [PATCH 08/78] implement rfc6052 address embedding --- src/common/mod.rs | 1 + src/common/rfc6052.rs | 116 ++++++++++++++++++++++++++++++++++++++++++ src/protomask-clat.rs | 48 ++++++++++++++++- 3 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 src/common/rfc6052.rs diff --git a/src/common/mod.rs b/src/common/mod.rs index e568b1f..279de81 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,3 +1,4 @@ //! Common code used across all protomask binaries pub mod logging; +pub mod rfc6052; \ No newline at end of file diff --git a/src/common/rfc6052.rs b/src/common/rfc6052.rs new file mode 100644 index 0000000..66550b3 --- /dev/null +++ b/src/common/rfc6052.rs @@ -0,0 +1,116 @@ +//! Utilities for interacting with [RFC6052](https://datatracker.ietf.org/doc/html/rfc6052) "IPv4-Embedded IPv6 Addresses" + +use std::{ + cmp::{max, min}, + net::{Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; + +use ipnet::Ipv6Net; + +/// Parses an [RFC6052 Section 2.2](https://datatracker.ietf.org/doc/html/rfc6052#section-2.2)-compliant IPv6 prefix from a string +pub fn parse_network_specific_prefix(string: &str) -> Result { + // First, parse to an IPv6Net struct + let net = Ipv6Net::from_str(string).map_err(|err| err.to_string())?; + + // Ensure the prefix length is one of the allowed lengths according to RFC6052 Section 2.2 + if ![32, 40, 48, 56, 64, 96].contains(&net.prefix_len()) { + return Err("Prefix length must be one of 32, 40, 48, 56, 64, or 96".to_owned()); + } + + // Return the parsed network struct + Ok(net) +} + +/// Embeds an IPv4 address into an IPv6 prefix +pub fn embed_to_ipv6(ipv4_addr: Ipv4Addr, ipv6_prefix: Ipv6Net) -> Ipv6Addr { + // Convert to integer types + let ipv4_addr = u32::from(ipv4_addr); + let prefix_len = ipv6_prefix.prefix_len() as i16; + let ipv6_prefix = u128::from(ipv6_prefix.addr()); + + // According to the RFC, the IPv4 address must be split on the boundary of bits 64..71. + // To accomplish this, we split the IPv4 address into two parts so we can separately mask + // and shift them into place on each side of the boundary + Ipv6Addr::from( + ipv6_prefix + | (((ipv4_addr as u128 & (0xffff_ffffu128 << (32 + min(0, prefix_len - 64)))) as u128) + << (128 - prefix_len - 32)) + | (((ipv4_addr as u128) << max(0, 128 - prefix_len - 32 - 8)) & 0x00ff_ffff_ffff_ffff), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_embed_len_32() { + assert_eq!( + embed_to_ipv6( + "192.0.2.1".parse().unwrap(), + "64:ff9b::/32".parse().unwrap() + ), + "64:ff9b:c000:0201::".parse::().unwrap() + ); + } + + #[test] + fn test_embed_len_40() { + assert_eq!( + embed_to_ipv6( + "192.0.2.1".parse().unwrap(), + "64:ff9b::/40".parse().unwrap(), + ), + "64:ff9b:00c0:0002:0001::".parse::().unwrap() + ); + } + + #[test] + fn test_embed_len_48() { + assert_eq!( + embed_to_ipv6( + "192.0.2.1".parse().unwrap(), + "64:ff9b::/48".parse().unwrap(), + ), + "64:ff9b:0000:c000:0002:0100::".parse::().unwrap() + ); + } + + #[test] + fn test_embed_len_56() { + assert_eq!( + embed_to_ipv6( + "192.0.2.1".parse().unwrap(), + "64:ff9b::/56".parse().unwrap(), + ), + "64:ff9b:0000:00c0:0000:0201::".parse::().unwrap() + ); + } + + #[test] + fn test_embed_len_64() { + assert_eq!( + embed_to_ipv6( + "192.0.2.1".parse().unwrap(), + "64:ff9b::/64".parse().unwrap(), + ), + "64:ff9b:0000:0000:00c0:0002:0100::" + .parse::() + .unwrap() + ); + } + + #[test] + fn test_embed_len_96() { + assert_eq!( + embed_to_ipv6( + "192.0.2.1".parse().unwrap(), + "64:ff9b::/96".parse().unwrap(), + ), + "64:ff9b:0000:0000:0000:0000:c000:0201" + .parse::() + .unwrap() + ); + } +} diff --git a/src/protomask-clat.rs b/src/protomask-clat.rs index 150b650..f586e50 100644 --- a/src/protomask-clat.rs +++ b/src/protomask-clat.rs @@ -4,10 +4,15 @@ //! IPv4 traffic to IPv6 traffic for transmission over an IPv6-only ISP network. use clap::Parser; -use common::logging::enable_logger; +use common::{logging::enable_logger, rfc6052::parse_network_specific_prefix}; use easy_tun::Tun; +use interproto::protocols::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4}; use ipnet::Ipv6Net; use nix::unistd::Uid; +use std::{ + io::{Read, Write}, + net::Ipv4Addr, +}; mod common; @@ -15,7 +20,7 @@ mod common; #[clap(author, version, about="IPv4 to IPv6 Customer-side transLATor (CLAT)", long_about = None)] struct Args { /// IPv6 prefix to embed IPv4 addresses in - #[clap(long="via", default_value_t = ("64:ff9b::/96").parse().unwrap())] + #[clap(long="via", default_value_t = ("64:ff9b::/96").parse().unwrap(), value_parser = parse_network_specific_prefix)] embed_prefix: Ipv6Net, /// Explicitly set the interface name to use @@ -44,8 +49,47 @@ pub async fn main() { // Bring up a TUN interface let mut tun = Tun::new(&args.interface).unwrap(); + // Translate all incoming packets log::info!("Translating packets on {}", tun.name()); + let mut buffer = vec![0u8; 1500]; loop { + // Read a packet + let len = tun.read(&mut buffer).unwrap(); + // Translate it based on the Layer 3 protocol number + let layer_3_proto = buffer[0] >> 4; + log::trace!("New packet with layer 3 protocol: {}", layer_3_proto); + let output = match layer_3_proto { + // IPv4 + 4 => { + // Get the IPv4 source and destination addresses + let ipv4_source = + u32::from_be_bytes([buffer[12], buffer[13], buffer[14], buffer[15]]); + let ipv4_destination = + u32::from_be_bytes([buffer[16], buffer[17], buffer[18], buffer[19]]); + + // Create a new IPv6 source and destination address by embedding the IPv4 addresses into the clat prefix + let new_source = u128::from(args.embed_prefix.addr()) | (ipv4_source as u128); + let new_destination = + u128::from(args.embed_prefix.addr()) | (ipv4_destination as u128); + + translate_ipv4_to_ipv6(&buffer[..len], new_source.into(), new_destination.into()) + } + + // IPv6 + 6 => translate_ipv6_to_ipv4( + &buffer[..len], + // NOTE: The new source and destination addresses are just the last + // 4 octets of the IPv6 source and destination addresses + Ipv4Addr::new(buffer[20], buffer[21], buffer[22], buffer[23]), + Ipv4Addr::new(buffer[36], buffer[37], buffer[38], buffer[39]), + ), + // Unknown + proto => { + log::warn!("Unknown Layer 3 protocol: {}", proto); + continue; + } + } + .unwrap(); } } From 2d097e64eb83c2fc76ea197b9982fa27d6775381 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 19:45:06 -0400 Subject: [PATCH 09/78] Implement rfc6052 embed and extract fns --- Cargo.toml | 3 +- libs/rfc6052/Cargo.toml | 17 +++++ libs/rfc6052/README.md | 3 + libs/rfc6052/src/embed.rs | 126 ++++++++++++++++++++++++++++++++++++ libs/rfc6052/src/error.rs | 7 ++ libs/rfc6052/src/extract.rs | 96 +++++++++++++++++++++++++++ libs/rfc6052/src/lib.rs | 14 ++++ src/common/rfc6052.rs | 106 ++---------------------------- 8 files changed, 271 insertions(+), 101 deletions(-) create mode 100644 libs/rfc6052/Cargo.toml create mode 100644 libs/rfc6052/README.md create mode 100644 libs/rfc6052/src/embed.rs create mode 100644 libs/rfc6052/src/error.rs create mode 100644 libs/rfc6052/src/extract.rs create mode 100644 libs/rfc6052/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 7f59c58..64d863a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ exclude = ["/.github/", "/.vscode/"] # lazy_static = "1.4.0" [workspace] -members = ["libs/easy-tun", "libs/fast-nat", "libs/interproto"] +members = ["libs/easy-tun", "libs/fast-nat", "libs/interproto", "libs/rfc6052"] [[bin]] name = "protomask" @@ -60,6 +60,7 @@ path = "src/protomask-6over4.rs" easy-tun = { path = "libs/easy-tun" } fast-nat = { path = "libs/fast-nat" } interproto = { path = "libs/interproto" } +rfc6052 = { path = "libs/rfc6052" } # External Dependencies tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } diff --git a/libs/rfc6052/Cargo.toml b/libs/rfc6052/Cargo.toml new file mode 100644 index 0000000..7bbb9bb --- /dev/null +++ b/libs/rfc6052/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rfc6052" +version = "1.0.0" +authors = ["Evan Pratten "] +edition = "2021" +description = "Rust functions for interacting with RFC6052 IPv4-Embedded IPv6 Addresses" +readme = "README.md" +homepage = "https://github.com/ewpratten/protomask/tree/master/libs/rfc6052" +documentation = "https://docs.rs/rfc6052" +repository = "https://github.com/ewpratten/protomask" +license = "GPL-3.0" +keywords = [] +categories = [] + +[dependencies] +thiserror = "^1.0.44" +ipnet = "^2.8.0" \ No newline at end of file diff --git a/libs/rfc6052/README.md b/libs/rfc6052/README.md new file mode 100644 index 0000000..0b5d420 --- /dev/null +++ b/libs/rfc6052/README.md @@ -0,0 +1,3 @@ +# RFC6052 for Rust + +This library provides functions for interacting with [RFC6052](https://datatracker.ietf.org/doc/html/rfc6052) IPv4-Embedded IPv6 Addresses. diff --git a/libs/rfc6052/src/embed.rs b/libs/rfc6052/src/embed.rs new file mode 100644 index 0000000..483a733 --- /dev/null +++ b/libs/rfc6052/src/embed.rs @@ -0,0 +1,126 @@ +use ipnet::Ipv6Net; +use std::cmp::{max, min}; +use std::net::{Ipv4Addr, Ipv6Addr}; + +use crate::error::Error; +use crate::ALLOWED_PREFIX_LENS; + +/// Embeds an IPv4 address into an IPv6 prefix following the method defined in [RFC6052 Section 2.2](https://datatracker.ietf.org/doc/html/rfc6052#section-2.2) +pub fn embed_ipv4_addr(ipv4_addr: Ipv4Addr, ipv6_prefix: Ipv6Net) -> Result { + // Fail if the prefix length is invalid + if !ALLOWED_PREFIX_LENS.contains(&ipv6_prefix.prefix_len()) { + return Err(Error::InvalidPrefixLength(ipv6_prefix.prefix_len())); + } + + // Fall through to the unchecked version of this function + Ok(unsafe { embed_ipv4_addr_unchecked(ipv4_addr, ipv6_prefix) }) +} + +/// Embeds an IPv4 address into an IPv6 prefix following the method defined in [RFC6052 Section 2.2](https://datatracker.ietf.org/doc/html/rfc6052#section-2.2) +/// +/// **Warning:** This function does not check that the prefix length is valid according to the RFC. Use `embed_ipv4_addr` instead. +pub unsafe fn embed_ipv4_addr_unchecked( + ipv4_addr: Ipv4Addr, + ipv6_prefix: Ipv6Net, +) -> Ipv6Addr { + // Convert to integer types + let ipv4_addr = u32::from(ipv4_addr); + let prefix_len = ipv6_prefix.prefix_len() as i16; + let ipv6_prefix = u128::from(ipv6_prefix.addr()); + + // According to the RFC, the IPv4 address must be split on the boundary of bits 64..71. + // To accomplish this, we split the IPv4 address into two parts so we can separately mask + // and shift them into place on each side of the boundary + Ipv6Addr::from( + ipv6_prefix + | (((ipv4_addr as u128 & (0xffff_ffffu128 << (32 + min(0, prefix_len - 64)))) as u128) + << (128 - prefix_len - 32)) + | (((ipv4_addr as u128) << max(0, 128 - prefix_len - 32 - 8)) & 0x00ff_ffff_ffff_ffff), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_embed_len_32() { + unsafe { + assert_eq!( + embed_ipv4_addr_unchecked( + "192.0.2.1".parse().unwrap(), + "64:ff9b::/32".parse().unwrap() + ), + "64:ff9b:c000:0201::".parse::().unwrap() + ); + } + } + + #[test] + fn test_embed_len_40() { + unsafe { + assert_eq!( + embed_ipv4_addr_unchecked( + "192.0.2.1".parse().unwrap(), + "64:ff9b::/40".parse().unwrap(), + ), + "64:ff9b:00c0:0002:0001::".parse::().unwrap() + ); + } + } + #[test] + fn test_embed_len_48() { + unsafe { + assert_eq!( + embed_ipv4_addr_unchecked( + "192.0.2.1".parse().unwrap(), + "64:ff9b::/48".parse().unwrap(), + ), + "64:ff9b:0000:c000:0002:0100::".parse::().unwrap() + ); + } + } + + #[test] + fn test_embed_len_56() { + unsafe { + assert_eq!( + embed_ipv4_addr_unchecked( + "192.0.2.1".parse().unwrap(), + "64:ff9b::/56".parse().unwrap(), + ), + "64:ff9b:0000:00c0:0000:0201::".parse::().unwrap() + ); + } + } + + #[test] + fn test_embed_len_64() { + unsafe { + assert_eq!( + embed_ipv4_addr_unchecked( + "192.0.2.1".parse().unwrap(), + "64:ff9b::/64".parse().unwrap(), + ), + "64:ff9b:0000:0000:00c0:0002:0100::" + .parse::() + .unwrap() + ); + } + } + + #[test] + fn test_embed_len_96() { + unsafe { + assert_eq!( + embed_ipv4_addr_unchecked( + "192.0.2.1".parse().unwrap(), + "64:ff9b::/96".parse().unwrap(), + ), + "64:ff9b:0000:0000:0000:0000:c000:0201" + .parse::() + .unwrap() + ); + } + } +} diff --git a/libs/rfc6052/src/error.rs b/libs/rfc6052/src/error.rs new file mode 100644 index 0000000..c616737 --- /dev/null +++ b/libs/rfc6052/src/error.rs @@ -0,0 +1,7 @@ +//! Error types for this library + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Invalid IPv6 prefix length: {0}. Must be one of 32, 40, 48, 56, 64, or 96")] + InvalidPrefixLength(u8), +} \ No newline at end of file diff --git a/libs/rfc6052/src/extract.rs b/libs/rfc6052/src/extract.rs new file mode 100644 index 0000000..12dedcf --- /dev/null +++ b/libs/rfc6052/src/extract.rs @@ -0,0 +1,96 @@ +use crate::{error::Error, ALLOWED_PREFIX_LENS}; +use std::cmp::max; +use std::net::{Ipv4Addr, Ipv6Addr}; + +// /// Extracts an IPv4 address from an IPv6 prefix following the method defined in [RFC6052 Section 2.2](https://datatracker.ietf.org/doc/html/rfc6052#section-2.2) +// pub fn extract_ipv4_addr(ipv6_addr: Ipv6Addr, prefix_length: u8) -> Result { +// // Fail if the prefix length is invalid +// if !ALLOWED_PREFIX_LENS.contains(&prefix_length) { +// return Err(Error::InvalidPrefixLength(prefix_length)); +// } + +// // Fall through to the unchecked version of this function +// Ok(unsafe { extract_ipv4_addr_unchecked(ipv6_addr, prefix_length) }) +// } + +/// Extracts an IPv4 address from an IPv6 prefix following the method defined in [RFC6052 Section 2.2](https://datatracker.ietf.org/doc/html/rfc6052#section-2.2) +/// +/// **Warning:** This function does not check that the prefix length is valid according to the RFC. Use `extract_ipv4_addr` instead. +pub unsafe fn extract_ipv4_addr_unchecked(ipv6_addr: Ipv6Addr, prefix_length: u8) -> Ipv4Addr { + // Convert the IPv6 address to a number for easier manipulation + let ipv6_addr = u128::from(ipv6_addr); + let host_part = ipv6_addr & ((1 << (128 - prefix_length)) - 1); + + // Extract the IPv4 address from the IPv6 address + Ipv4Addr::from( + // format!("{:02x}", + (((host_part & 0xffff_ffff_ffff_ffff_0000_0000_0000_0000) + | (host_part & 0x00ff_ffff_ffff_ffff) << 8) + >> max(8, 128 - prefix_length - 32)) as u32, // ) + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_len_32() { + unsafe { + assert_eq!( + extract_ipv4_addr_unchecked("64:ff9b:c000:0201::".parse().unwrap(), 32), + "192.0.2.1".parse::().unwrap(), + ) + } + } + + #[test] + fn test_extract_len_40() { + unsafe { + assert_eq!( + extract_ipv4_addr_unchecked("64:ff9b:00c0:0002:0001::".parse().unwrap(), 40), + "192.0.2.1".parse::().unwrap(), + ) + } + } + + #[test] + fn test_extract_len_48() { + unsafe { + assert_eq!( + extract_ipv4_addr_unchecked("64:ff9b:0000:c000:0002:0100::".parse().unwrap(), 48), + "192.0.2.1".parse::().unwrap(), + ) + } + } + + #[test] + fn test_extract_len_56() { + unsafe { + assert_eq!( + extract_ipv4_addr_unchecked("64:ff9b:0000:00c0:0000:0201::".parse().unwrap(), 56), + "192.0.2.1".parse::().unwrap(), + ) + } + } + + #[test] + fn test_extract_len_64() { + unsafe { + assert_eq!( + extract_ipv4_addr_unchecked("64:ff9b:0000:0000:00c0:0002:0100::".parse().unwrap(), 64), + "192.0.2.1".parse::().unwrap(), + ) + } + } + + #[test] + fn test_extract_len_96() { + unsafe { + assert_eq!( + extract_ipv4_addr_unchecked("64:ff9b:0000:0000:0000:0000:c000:0201".parse().unwrap(), 96), + "192.0.2.1".parse::().unwrap(), + ) + } + } +} diff --git a/libs/rfc6052/src/lib.rs b/libs/rfc6052/src/lib.rs new file mode 100644 index 0000000..d81d97d --- /dev/null +++ b/libs/rfc6052/src/lib.rs @@ -0,0 +1,14 @@ +#![doc = include_str!("../README.md")] +#![deny(clippy::pedantic)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::missing_panics_doc)] + +pub mod error; + +mod embed; +mod extract; +pub use embed::embed_ipv4_addr; + +/// All allowed IPv6 prefix lengths according to [RFC6052 Section 2.2](https://datatracker.ietf.org/doc/html/rfc6052#section-2.2) +pub const ALLOWED_PREFIX_LENS: [u8; 6] = [32, 40, 48, 56, 64, 96]; diff --git a/src/common/rfc6052.rs b/src/common/rfc6052.rs index 66550b3..c5802b6 100644 --- a/src/common/rfc6052.rs +++ b/src/common/rfc6052.rs @@ -1,10 +1,6 @@ //! Utilities for interacting with [RFC6052](https://datatracker.ietf.org/doc/html/rfc6052) "IPv4-Embedded IPv6 Addresses" -use std::{ - cmp::{max, min}, - net::{Ipv4Addr, Ipv6Addr}, - str::FromStr, -}; +use std::str::FromStr; use ipnet::Ipv6Net; @@ -14,103 +10,13 @@ pub fn parse_network_specific_prefix(string: &str) -> Result { let net = Ipv6Net::from_str(string).map_err(|err| err.to_string())?; // Ensure the prefix length is one of the allowed lengths according to RFC6052 Section 2.2 - if ![32, 40, 48, 56, 64, 96].contains(&net.prefix_len()) { - return Err("Prefix length must be one of 32, 40, 48, 56, 64, or 96".to_owned()); + if !rfc6052::ALLOWED_PREFIX_LENS.contains(&net.prefix_len()) { + return Err(format!( + "Prefix length must be one of {:?}", + rfc6052::ALLOWED_PREFIX_LENS + )); } // Return the parsed network struct Ok(net) } - -/// Embeds an IPv4 address into an IPv6 prefix -pub fn embed_to_ipv6(ipv4_addr: Ipv4Addr, ipv6_prefix: Ipv6Net) -> Ipv6Addr { - // Convert to integer types - let ipv4_addr = u32::from(ipv4_addr); - let prefix_len = ipv6_prefix.prefix_len() as i16; - let ipv6_prefix = u128::from(ipv6_prefix.addr()); - - // According to the RFC, the IPv4 address must be split on the boundary of bits 64..71. - // To accomplish this, we split the IPv4 address into two parts so we can separately mask - // and shift them into place on each side of the boundary - Ipv6Addr::from( - ipv6_prefix - | (((ipv4_addr as u128 & (0xffff_ffffu128 << (32 + min(0, prefix_len - 64)))) as u128) - << (128 - prefix_len - 32)) - | (((ipv4_addr as u128) << max(0, 128 - prefix_len - 32 - 8)) & 0x00ff_ffff_ffff_ffff), - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_embed_len_32() { - assert_eq!( - embed_to_ipv6( - "192.0.2.1".parse().unwrap(), - "64:ff9b::/32".parse().unwrap() - ), - "64:ff9b:c000:0201::".parse::().unwrap() - ); - } - - #[test] - fn test_embed_len_40() { - assert_eq!( - embed_to_ipv6( - "192.0.2.1".parse().unwrap(), - "64:ff9b::/40".parse().unwrap(), - ), - "64:ff9b:00c0:0002:0001::".parse::().unwrap() - ); - } - - #[test] - fn test_embed_len_48() { - assert_eq!( - embed_to_ipv6( - "192.0.2.1".parse().unwrap(), - "64:ff9b::/48".parse().unwrap(), - ), - "64:ff9b:0000:c000:0002:0100::".parse::().unwrap() - ); - } - - #[test] - fn test_embed_len_56() { - assert_eq!( - embed_to_ipv6( - "192.0.2.1".parse().unwrap(), - "64:ff9b::/56".parse().unwrap(), - ), - "64:ff9b:0000:00c0:0000:0201::".parse::().unwrap() - ); - } - - #[test] - fn test_embed_len_64() { - assert_eq!( - embed_to_ipv6( - "192.0.2.1".parse().unwrap(), - "64:ff9b::/64".parse().unwrap(), - ), - "64:ff9b:0000:0000:00c0:0002:0100::" - .parse::() - .unwrap() - ); - } - - #[test] - fn test_embed_len_96() { - assert_eq!( - embed_to_ipv6( - "192.0.2.1".parse().unwrap(), - "64:ff9b::/96".parse().unwrap(), - ), - "64:ff9b:0000:0000:0000:0000:c000:0201" - .parse::() - .unwrap() - ); - } -} From dc5d524e3dd6a586d29018b3b85afe96c4adac5f Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 19:45:40 -0400 Subject: [PATCH 10/78] Expose new fns --- libs/rfc6052/src/extract.rs | 31 ++++++++++++++++++------------- libs/rfc6052/src/lib.rs | 3 ++- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/libs/rfc6052/src/extract.rs b/libs/rfc6052/src/extract.rs index 12dedcf..3931820 100644 --- a/libs/rfc6052/src/extract.rs +++ b/libs/rfc6052/src/extract.rs @@ -2,16 +2,16 @@ use crate::{error::Error, ALLOWED_PREFIX_LENS}; use std::cmp::max; use std::net::{Ipv4Addr, Ipv6Addr}; -// /// Extracts an IPv4 address from an IPv6 prefix following the method defined in [RFC6052 Section 2.2](https://datatracker.ietf.org/doc/html/rfc6052#section-2.2) -// pub fn extract_ipv4_addr(ipv6_addr: Ipv6Addr, prefix_length: u8) -> Result { -// // Fail if the prefix length is invalid -// if !ALLOWED_PREFIX_LENS.contains(&prefix_length) { -// return Err(Error::InvalidPrefixLength(prefix_length)); -// } +/// Extracts an IPv4 address from an IPv6 prefix following the method defined in [RFC6052 Section 2.2](https://datatracker.ietf.org/doc/html/rfc6052#section-2.2) +pub fn extract_ipv4_addr(ipv6_addr: Ipv6Addr, prefix_length: u8) -> Result { + // Fail if the prefix length is invalid + if !ALLOWED_PREFIX_LENS.contains(&prefix_length) { + return Err(Error::InvalidPrefixLength(prefix_length)); + } -// // Fall through to the unchecked version of this function -// Ok(unsafe { extract_ipv4_addr_unchecked(ipv6_addr, prefix_length) }) -// } + // Fall through to the unchecked version of this function + Ok(unsafe { extract_ipv4_addr_unchecked(ipv6_addr, prefix_length) }) +} /// Extracts an IPv4 address from an IPv6 prefix following the method defined in [RFC6052 Section 2.2](https://datatracker.ietf.org/doc/html/rfc6052#section-2.2) /// @@ -23,10 +23,9 @@ pub unsafe fn extract_ipv4_addr_unchecked(ipv6_addr: Ipv6Addr, prefix_length: u8 // Extract the IPv4 address from the IPv6 address Ipv4Addr::from( - // format!("{:02x}", (((host_part & 0xffff_ffff_ffff_ffff_0000_0000_0000_0000) | (host_part & 0x00ff_ffff_ffff_ffff) << 8) - >> max(8, 128 - prefix_length - 32)) as u32, // ) + >> max(8, 128 - prefix_length - 32)) as u32, ) } @@ -78,7 +77,10 @@ mod tests { fn test_extract_len_64() { unsafe { assert_eq!( - extract_ipv4_addr_unchecked("64:ff9b:0000:0000:00c0:0002:0100::".parse().unwrap(), 64), + extract_ipv4_addr_unchecked( + "64:ff9b:0000:0000:00c0:0002:0100::".parse().unwrap(), + 64 + ), "192.0.2.1".parse::().unwrap(), ) } @@ -88,7 +90,10 @@ mod tests { fn test_extract_len_96() { unsafe { assert_eq!( - extract_ipv4_addr_unchecked("64:ff9b:0000:0000:0000:0000:c000:0201".parse().unwrap(), 96), + extract_ipv4_addr_unchecked( + "64:ff9b:0000:0000:0000:0000:c000:0201".parse().unwrap(), + 96 + ), "192.0.2.1".parse::().unwrap(), ) } diff --git a/libs/rfc6052/src/lib.rs b/libs/rfc6052/src/lib.rs index d81d97d..f6203b9 100644 --- a/libs/rfc6052/src/lib.rs +++ b/libs/rfc6052/src/lib.rs @@ -8,7 +8,8 @@ pub mod error; mod embed; mod extract; -pub use embed::embed_ipv4_addr; +pub use embed::{embed_ipv4_addr, embed_ipv4_addr_unchecked}; +pub use extract::{extract_ipv4_addr, extract_ipv4_addr_unchecked}; /// All allowed IPv6 prefix lengths according to [RFC6052 Section 2.2](https://datatracker.ietf.org/doc/html/rfc6052#section-2.2) pub const ALLOWED_PREFIX_LENS: [u8; 6] = [32, 40, 48, 56, 64, 96]; From f1c1afed961e8f5232d05b93ee27aa9158dbfc47 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 19:50:30 -0400 Subject: [PATCH 11/78] appease clippy --- libs/interproto/src/lib.rs | 1 + libs/rfc6052/src/embed.rs | 10 +++++----- libs/rfc6052/src/extract.rs | 3 +++ libs/rfc6052/src/lib.rs | 1 + 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/libs/interproto/src/lib.rs b/libs/interproto/src/lib.rs index 1486821..f5e4aa0 100644 --- a/libs/interproto/src/lib.rs +++ b/libs/interproto/src/lib.rs @@ -3,6 +3,7 @@ #![allow(clippy::module_name_repetitions)] #![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_panics_doc)] +#![allow(clippy::doc_markdown)] pub mod protocols; pub mod error; \ No newline at end of file diff --git a/libs/rfc6052/src/embed.rs b/libs/rfc6052/src/embed.rs index 483a733..598825a 100644 --- a/libs/rfc6052/src/embed.rs +++ b/libs/rfc6052/src/embed.rs @@ -19,10 +19,10 @@ pub fn embed_ipv4_addr(ipv4_addr: Ipv4Addr, ipv6_prefix: Ipv6Net) -> Result Ipv6Addr { +#[must_use] +#[allow(clippy::cast_lossless)] +#[allow(clippy::cast_possible_truncation)] +pub unsafe fn embed_ipv4_addr_unchecked(ipv4_addr: Ipv4Addr, ipv6_prefix: Ipv6Net) -> Ipv6Addr { // Convert to integer types let ipv4_addr = u32::from(ipv4_addr); let prefix_len = ipv6_prefix.prefix_len() as i16; @@ -33,7 +33,7 @@ pub unsafe fn embed_ipv4_addr_unchecked( // and shift them into place on each side of the boundary Ipv6Addr::from( ipv6_prefix - | (((ipv4_addr as u128 & (0xffff_ffffu128 << (32 + min(0, prefix_len - 64)))) as u128) + | ((ipv4_addr as u128 & (0xffff_ffffu128 << (32 + min(0, prefix_len - 64)))) << (128 - prefix_len - 32)) | (((ipv4_addr as u128) << max(0, 128 - prefix_len - 32 - 8)) & 0x00ff_ffff_ffff_ffff), ) diff --git a/libs/rfc6052/src/extract.rs b/libs/rfc6052/src/extract.rs index 3931820..3b86722 100644 --- a/libs/rfc6052/src/extract.rs +++ b/libs/rfc6052/src/extract.rs @@ -16,6 +16,9 @@ pub fn extract_ipv4_addr(ipv6_addr: Ipv6Addr, prefix_length: u8) -> Result Ipv4Addr { // Convert the IPv6 address to a number for easier manipulation let ipv6_addr = u128::from(ipv6_addr); diff --git a/libs/rfc6052/src/lib.rs b/libs/rfc6052/src/lib.rs index f6203b9..6477eb3 100644 --- a/libs/rfc6052/src/lib.rs +++ b/libs/rfc6052/src/lib.rs @@ -3,6 +3,7 @@ #![allow(clippy::module_name_repetitions)] #![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_panics_doc)] +#![allow(clippy::missing_safety_doc)] pub mod error; From eb1da675de5f7f2acf5c90f4cc94a094d3b53d1c Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 19:59:23 -0400 Subject: [PATCH 12/78] Implement the rest of clat packet translation --- src/protomask-clat.rs | 51 +++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/src/protomask-clat.rs b/src/protomask-clat.rs index f586e50..c86744a 100644 --- a/src/protomask-clat.rs +++ b/src/protomask-clat.rs @@ -9,9 +9,10 @@ use easy_tun::Tun; use interproto::protocols::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4}; use ipnet::Ipv6Net; use nix::unistd::Uid; +use rfc6052::{embed_ipv4_addr_unchecked, extract_ipv4_addr_unchecked}; use std::{ io::{Read, Write}, - net::Ipv4Addr, + net::{Ipv4Addr, Ipv6Addr}, }; mod common; @@ -61,28 +62,37 @@ pub async fn main() { log::trace!("New packet with layer 3 protocol: {}", layer_3_proto); let output = match layer_3_proto { // IPv4 - 4 => { - // Get the IPv4 source and destination addresses - let ipv4_source = - u32::from_be_bytes([buffer[12], buffer[13], buffer[14], buffer[15]]); - let ipv4_destination = - u32::from_be_bytes([buffer[16], buffer[17], buffer[18], buffer[19]]); - - // Create a new IPv6 source and destination address by embedding the IPv4 addresses into the clat prefix - let new_source = u128::from(args.embed_prefix.addr()) | (ipv4_source as u128); - let new_destination = - u128::from(args.embed_prefix.addr()) | (ipv4_destination as u128); - - translate_ipv4_to_ipv6(&buffer[..len], new_source.into(), new_destination.into()) - } + 4 => translate_ipv4_to_ipv6( + &buffer[..len], + unsafe { + embed_ipv4_addr_unchecked( + Ipv4Addr::from(u32::from_be_bytes(buffer[12..16].try_into().unwrap())), + args.embed_prefix, + ) + }, + unsafe { + embed_ipv4_addr_unchecked( + Ipv4Addr::from(u32::from_be_bytes(buffer[16..20].try_into().unwrap())), + args.embed_prefix, + ) + }, + ), // IPv6 6 => translate_ipv6_to_ipv4( &buffer[..len], - // NOTE: The new source and destination addresses are just the last - // 4 octets of the IPv6 source and destination addresses - Ipv4Addr::new(buffer[20], buffer[21], buffer[22], buffer[23]), - Ipv4Addr::new(buffer[36], buffer[37], buffer[38], buffer[39]), + unsafe { + extract_ipv4_addr_unchecked( + Ipv6Addr::from(u128::from_be_bytes(buffer[8..24].try_into().unwrap())), + args.embed_prefix.prefix_len(), + ) + }, + unsafe { + extract_ipv4_addr_unchecked( + Ipv6Addr::from(u128::from_be_bytes(buffer[24..40].try_into().unwrap())), + args.embed_prefix.prefix_len(), + ) + }, ), // Unknown proto => { @@ -91,5 +101,8 @@ pub async fn main() { } } .unwrap(); + + // Write the translated packet back to the TUN interface + tun.write(&output).unwrap(); } } From 7d0a9f08dd27d6305888b24ef48aacb57ac7e006 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 20:04:57 -0400 Subject: [PATCH 13/78] Update keywords --- libs/rfc6052/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/rfc6052/Cargo.toml b/libs/rfc6052/Cargo.toml index 7bbb9bb..45f68d7 100644 --- a/libs/rfc6052/Cargo.toml +++ b/libs/rfc6052/Cargo.toml @@ -9,9 +9,9 @@ homepage = "https://github.com/ewpratten/protomask/tree/master/libs/rfc6052" documentation = "https://docs.rs/rfc6052" repository = "https://github.com/ewpratten/protomask" license = "GPL-3.0" -keywords = [] -categories = [] +keywords = ["rfc6052", "ipv4-embedded-ipv6", "address-translation", "ipv4", "ipv6"] +categories = ["algorithms", "encoding", "network-programming"] [dependencies] thiserror = "^1.0.44" -ipnet = "^2.8.0" \ No newline at end of file +ipnet = "^2.8.0" From 032dcb7fa9b8b9ffd8b06ac2c476fb0792477f05 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 20:05:00 -0400 Subject: [PATCH 14/78] Create rtnl --- libs/rtnl/Cargo.toml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 libs/rtnl/Cargo.toml diff --git a/libs/rtnl/Cargo.toml b/libs/rtnl/Cargo.toml new file mode 100644 index 0000000..7bbb9bb --- /dev/null +++ b/libs/rtnl/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rfc6052" +version = "1.0.0" +authors = ["Evan Pratten "] +edition = "2021" +description = "Rust functions for interacting with RFC6052 IPv4-Embedded IPv6 Addresses" +readme = "README.md" +homepage = "https://github.com/ewpratten/protomask/tree/master/libs/rfc6052" +documentation = "https://docs.rs/rfc6052" +repository = "https://github.com/ewpratten/protomask" +license = "GPL-3.0" +keywords = [] +categories = [] + +[dependencies] +thiserror = "^1.0.44" +ipnet = "^2.8.0" \ No newline at end of file From 833d4308fb4a9c2eeff3f48941a778e18c9fcf4f Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Wed, 2 Aug 2023 22:44:30 -0400 Subject: [PATCH 15/78] wip clat --- Cargo.toml | 9 +++- libs/interproto/src/protocols/ip.rs | 3 ++ libs/rtnl/Cargo.toml | 18 +++++--- libs/rtnl/README.md | 3 ++ libs/rtnl/src/ip.rs | 71 +++++++++++++++++++++++++++++ libs/rtnl/src/lib.rs | 22 +++++++++ libs/rtnl/src/link.rs | 31 +++++++++++++ libs/rtnl/src/route.rs | 41 +++++++++++++++++ src/protomask-clat.rs | 48 ++++++++++++++++--- 9 files changed, 231 insertions(+), 15 deletions(-) create mode 100644 libs/rtnl/README.md create mode 100644 libs/rtnl/src/ip.rs create mode 100644 libs/rtnl/src/lib.rs create mode 100644 libs/rtnl/src/link.rs create mode 100644 libs/rtnl/src/route.rs diff --git a/Cargo.toml b/Cargo.toml index 64d863a..c83dea3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,13 @@ exclude = ["/.github/", "/.vscode/"] # lazy_static = "1.4.0" [workspace] -members = ["libs/easy-tun", "libs/fast-nat", "libs/interproto", "libs/rfc6052"] +members = [ + "libs/easy-tun", + "libs/fast-nat", + "libs/interproto", + "libs/rfc6052", + "libs/rtnl", +] [[bin]] name = "protomask" @@ -61,6 +67,7 @@ easy-tun = { path = "libs/easy-tun" } fast-nat = { path = "libs/fast-nat" } interproto = { path = "libs/interproto" } rfc6052 = { path = "libs/rfc6052" } +rtnl = { path = "libs/rtnl", features = ["tokio"] } # External Dependencies tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } diff --git a/libs/interproto/src/protocols/ip.rs b/libs/interproto/src/protocols/ip.rs index 54d934c..b7c84e6 100644 --- a/libs/interproto/src/protocols/ip.rs +++ b/libs/interproto/src/protocols/ip.rs @@ -65,6 +65,7 @@ pub fn translate_ipv4_to_ipv6( ipv6_packet.set_hop_limit(ipv4_packet.get_ttl()); ipv6_packet.set_source(new_source); ipv6_packet.set_destination(new_destination); + ipv6_packet.set_payload_length(new_payload.len().try_into().unwrap()); // Copy the payload to the buffer ipv6_packet.set_payload(&new_payload); @@ -118,6 +119,7 @@ pub fn translate_ipv6_to_ipv4( // Set the header fields ipv4_packet.set_version(4); + ipv4_packet.set_header_length(5); ipv4_packet.set_ttl(ipv6_packet.get_hop_limit()); ipv4_packet.set_next_level_protocol(match ipv6_packet.get_next_header() { IpNextHeaderProtocols::Icmpv6 => IpNextHeaderProtocols::Icmp, @@ -125,6 +127,7 @@ pub fn translate_ipv6_to_ipv4( }); ipv4_packet.set_source(new_source); ipv4_packet.set_destination(new_destination); + ipv4_packet.set_total_length((Ipv4Packet::minimum_packet_size() + new_payload.len()).try_into().unwrap()); // Copy the payload to the buffer ipv4_packet.set_payload(&new_payload); diff --git a/libs/rtnl/Cargo.toml b/libs/rtnl/Cargo.toml index 7bbb9bb..7ab90d4 100644 --- a/libs/rtnl/Cargo.toml +++ b/libs/rtnl/Cargo.toml @@ -1,17 +1,21 @@ [package] -name = "rfc6052" -version = "1.0.0" +name = "rtnl" +version = "0.1.0" authors = ["Evan Pratten "] edition = "2021" -description = "Rust functions for interacting with RFC6052 IPv4-Embedded IPv6 Addresses" +description = "Slightly sane wrapper around rtnetlink" readme = "README.md" -homepage = "https://github.com/ewpratten/protomask/tree/master/libs/rfc6052" -documentation = "https://docs.rs/rfc6052" +homepage = "https://github.com/ewpratten/protomask/tree/master/libs/rtnl" +documentation = "https://docs.rs/rtnl" repository = "https://github.com/ewpratten/protomask" license = "GPL-3.0" keywords = [] categories = [] + [dependencies] -thiserror = "^1.0.44" -ipnet = "^2.8.0" \ No newline at end of file +tokio = { version = "1.29.1", optional = true, features = ["rt-multi-thread"] } +log = "0.4.19" +rtnetlink = "0.13.1" +futures = "0.3.28" +ipnet = "^2.8.0" diff --git a/libs/rtnl/README.md b/libs/rtnl/README.md new file mode 100644 index 0000000..b27c96d --- /dev/null +++ b/libs/rtnl/README.md @@ -0,0 +1,3 @@ +# RTNL + +A slightly sane wrapper around [`rtnetlink`](https://crates.io/crates/rtnetlink) diff --git a/libs/rtnl/src/ip.rs b/libs/rtnl/src/ip.rs new file mode 100644 index 0000000..ffae235 --- /dev/null +++ b/libs/rtnl/src/ip.rs @@ -0,0 +1,71 @@ +//! Utilities for manipulating the addresses assigned to links + +use std::net::IpAddr; + +use futures::TryStreamExt; +use rtnetlink::Handle; + +/// Add an IP address to a link +pub async fn addr_add( + ip_addr: IpAddr, + prefix_len: u8, + rt_handle: &Handle, + link_index: u32, +) -> Result<(), rtnetlink::Error> { + log::trace!("Adding address {} to link {}", ip_addr, link_index); + rt_handle + .address() + .add(link_index, ip_addr, prefix_len) + .execute() + .await + .map_err(|err| { + log::error!("Failed to add address {} to link {}", ip_addr, link_index); + log::error!("{}", err); + err + }) +} + +/// Remove an IP address from a link +pub async fn addr_del( + ip_addr: IpAddr, + prefix_len: u8, + rt_handle: &Handle, + link_index: u32, +) -> Result<(), rtnetlink::Error> { + log::trace!("Removing address {} from link {}", ip_addr, link_index); + + // Find the address message that matches the given address + if let Some(address_message) = rt_handle + .address() + .get() + .set_link_index_filter(link_index) + .set_address_filter(ip_addr) + .set_prefix_length_filter(prefix_len) + .execute() + .try_next() + .await + .map_err(|err| { + log::error!("Failed to find address {} on link {}", ip_addr, link_index); + log::error!("{}", err); + err + })? + { + // Delete the address + rt_handle + .address() + .del(address_message) + .execute() + .await + .map_err(|err| { + log::error!( + "Failed to remove address {} from link {}", + ip_addr, + link_index + ); + log::error!("{}", err); + err + })?; + } + + Ok(()) +} diff --git a/libs/rtnl/src/lib.rs b/libs/rtnl/src/lib.rs new file mode 100644 index 0000000..dbd29da --- /dev/null +++ b/libs/rtnl/src/lib.rs @@ -0,0 +1,22 @@ +#![doc = include_str!("../README.md")] +#![deny(clippy::pedantic)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::missing_panics_doc)] +#![allow(clippy::missing_safety_doc)] + +pub mod link; +pub mod ip; +pub mod route; + +/// Get a handle on a new rtnetlink connection +#[cfg(feature="tokio")] +pub fn new_handle() -> Result { + let (rt_connection, rt_handle, _) = rtnetlink::new_connection().map_err(|err| { + log::error!("Failed to open rtnetlink connection"); + log::error!("{}", err); + err + })?; + tokio::spawn(rt_connection); + Ok(rt_handle) +} diff --git a/libs/rtnl/src/link.rs b/libs/rtnl/src/link.rs new file mode 100644 index 0000000..932f982 --- /dev/null +++ b/libs/rtnl/src/link.rs @@ -0,0 +1,31 @@ +//! Utilities for operating on a link/interface/device + +use futures::TryStreamExt; +use rtnetlink::Handle; + +/// Bring up a link by its link index +pub async fn link_up(rt_handle: &Handle, link_index: u32) -> Result<(), rtnetlink::Error> { + log::trace!("Bringing up link {}", link_index); + rt_handle.link().set(link_index).up().execute().await +} + +/// Bring down a link by its link index +pub async fn link_down(rt_handle: &Handle, link_index: u32) -> Result<(), rtnetlink::Error> { + log::trace!("Bringing down link {}", link_index); + rt_handle.link().set(link_index).down().execute().await +} + +/// Get the link index of a link by its name +pub async fn get_link_index( + rt_handle: &Handle, + link_name: &str, +) -> Result, rtnetlink::Error> { + Ok(rt_handle + .link() + .get() + .match_name(link_name.to_owned()) + .execute() + .try_next() + .await? + .map(|message| message.header.index)) +} diff --git a/libs/rtnl/src/route.rs b/libs/rtnl/src/route.rs new file mode 100644 index 0000000..225ef7e --- /dev/null +++ b/libs/rtnl/src/route.rs @@ -0,0 +1,41 @@ +//! Utilities for interacting with the routing table + +use ipnet::IpNet; +use rtnetlink::Handle; + +/// Add a route to a link +pub async fn route_add( + destination: IpNet, + rt_handle: &Handle, + link_index: u32, +) -> Result<(), rtnetlink::Error> { + log::trace!("Adding route {} to link {}", destination, link_index); + match destination { + IpNet::V4(destination) => rt_handle + .route() + .add() + .v4() + .output_interface(link_index) + .destination_prefix(destination.addr(), destination.prefix_len()) + .execute() + .await + .map_err(|err| { + log::error!("Failed to add route {} to link", destination); + log::error!("{}", err); + err + }), + IpNet::V6(destination) => rt_handle + .route() + .add() + .v6() + .output_interface(link_index) + .destination_prefix(destination.addr(), destination.prefix_len()) + .execute() + .await + .map_err(|err| { + log::error!("Failed to add route {} to link", destination); + log::error!("{}", err); + err + }), + } +} diff --git a/src/protomask-clat.rs b/src/protomask-clat.rs index c86744a..71d0067 100644 --- a/src/protomask-clat.rs +++ b/src/protomask-clat.rs @@ -7,7 +7,7 @@ use clap::Parser; use common::{logging::enable_logger, rfc6052::parse_network_specific_prefix}; use easy_tun::Tun; use interproto::protocols::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4}; -use ipnet::Ipv6Net; +use ipnet::{IpNet, Ipv4Net, Ipv6Net}; use nix::unistd::Uid; use rfc6052::{embed_ipv4_addr_unchecked, extract_ipv4_addr_unchecked}; use std::{ @@ -48,7 +48,26 @@ pub async fn main() { } // Bring up a TUN interface + log::debug!("Creating new TUN interface"); let mut tun = Tun::new(&args.interface).unwrap(); + log::debug!("Created TUN interface: {}", tun.name()); + + // Configure the new interface + // - Bring up + // - Add IPv6 prefix as a route + // - Point IPv4 default route to the new interface + let rt_handle = rtnl::new_handle().unwrap(); + let tun_link_idx = rtnl::link::get_link_index(&rt_handle, tun.name()) + .await + .unwrap() + .unwrap(); + rtnl::link::link_up(&rt_handle, tun_link_idx).await.unwrap(); + rtnl::route::route_add(IpNet::V6(args.embed_prefix), &rt_handle, tun_link_idx) + .await + .unwrap(); + rtnl::route::route_add(IpNet::V4(Ipv4Net::default()), &rt_handle, tun_link_idx) + .await + .unwrap(); // Translate all incoming packets log::info!("Translating packets on {}", tun.name()); @@ -60,7 +79,7 @@ pub async fn main() { // Translate it based on the Layer 3 protocol number let layer_3_proto = buffer[0] >> 4; log::trace!("New packet with layer 3 protocol: {}", layer_3_proto); - let output = match layer_3_proto { + match match layer_3_proto { // IPv4 4 => translate_ipv4_to_ipv6( &buffer[..len], @@ -99,10 +118,25 @@ pub async fn main() { log::warn!("Unknown Layer 3 protocol: {}", proto); continue; } - } - .unwrap(); - - // Write the translated packet back to the TUN interface - tun.write(&output).unwrap(); + } { + Ok(data) => { + // Write the translated packet back to the TUN interface + tun.write(&data).unwrap(); + } + Err(error) => match error { + interproto::error::Error::PacketTooShort { expected, actual } => log::warn!( + "Got packet with length {} when expecting at least {} bytes", + actual, + expected + ), + interproto::error::Error::UnsupportedIcmpType(icmp_type) => { + log::warn!("Got a packet with an unsupported ICMP type: {}", icmp_type) + } + interproto::error::Error::UnsupportedIcmpv6Type(icmpv6_type) => log::warn!( + "Got a packet with an unsupported ICMPv6 type: {}", + icmpv6_type + ), + }, + }; } } From 74ad55bde54c44832bfc1d846fcca495d6929459 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 11:27:20 -0400 Subject: [PATCH 16/78] CLAT is functional --- libs/interproto/src/protocols/icmp/mod.rs | 4 +- src/common/mod.rs | 3 +- src/common/packet_handler.rs | 79 ++++++++++++++ src/protomask-clat.rs | 122 ++++++++++------------ 4 files changed, 138 insertions(+), 70 deletions(-) create mode 100644 src/common/packet_handler.rs diff --git a/libs/interproto/src/protocols/icmp/mod.rs b/libs/interproto/src/protocols/icmp/mod.rs index 8aa9a79..a529d6c 100644 --- a/libs/interproto/src/protocols/icmp/mod.rs +++ b/libs/interproto/src/protocols/icmp/mod.rs @@ -37,7 +37,7 @@ pub fn translate_icmp_to_icmpv6( Icmpv6Types::TimeExceeded => { // Time exceeded messages contain the original IPv4 header and part of the payload. (with 4 bytes of forward padding) // We need to translate the IPv4 header and the payload, but keep the padding - let mut output = vec![0u8; icmp_packet.payload().len()]; + let mut output = vec![0u8; 4]; output.copy_from_slice(&icmp_packet.payload()[..4]); output.extend_from_slice(&translate_ipv4_to_ipv6( &icmp_packet.payload()[4..], @@ -98,7 +98,7 @@ pub fn translate_icmpv6_to_icmp( IcmpTypes::TimeExceeded => { // Time exceeded messages contain the original IPv6 header and part of the payload. (with 4 bytes of forward padding) // We need to translate the IPv6 header and the payload, but keep the padding - let mut output = vec![0u8; icmpv6_packet.payload().len()]; + let mut output = vec![0u8; 4]; output.copy_from_slice(&icmpv6_packet.payload()[..4]); output.extend_from_slice(&translate_ipv6_to_ipv4( &icmpv6_packet.payload()[4..], diff --git a/src/common/mod.rs b/src/common/mod.rs index 279de81..33fdbba 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,4 +1,5 @@ //! Common code used across all protomask binaries pub mod logging; -pub mod rfc6052; \ No newline at end of file +pub mod rfc6052; +pub mod packet_handler; \ No newline at end of file diff --git a/src/common/packet_handler.rs b/src/common/packet_handler.rs new file mode 100644 index 0000000..351cf35 --- /dev/null +++ b/src/common/packet_handler.rs @@ -0,0 +1,79 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +pub fn handle_packet( + packet: &[u8], + ipv4_handler: Ipv4Handler, + ipv6_handler: Ipv6Handler, +) -> Option> +where + Ipv4Handler: Fn(&[u8], &Ipv4Addr, &Ipv4Addr) -> Result, interproto::error::Error>, + Ipv6Handler: Fn(&[u8], &Ipv6Addr, &Ipv6Addr) -> Result, interproto::error::Error>, +{ + // If the packet is empty, return nothing + if packet.is_empty() { + return None; + } + + // Switch on the layer 3 protocol number to call the correct handler + let layer_3_proto = packet[0] >> 4; + log::trace!("New packet with layer 3 protocol: {}", layer_3_proto); + let handler_response = match layer_3_proto { + // IPv4 + 4 => { + // Extract the source and destination addresses + let source_addr = + Ipv4Addr::from(u32::from_be_bytes(packet[12..16].try_into().unwrap())); + let destination_addr = + Ipv4Addr::from(u32::from_be_bytes(packet[16..20].try_into().unwrap())); + + // Call the handler + ipv4_handler(packet, &source_addr, &destination_addr) + } + + // IPv6 + 6 => { + // Extract the source and destination addresses + let source_addr = + Ipv6Addr::from(u128::from_be_bytes(packet[8..24].try_into().unwrap())); + let destination_addr = + Ipv6Addr::from(u128::from_be_bytes(packet[24..40].try_into().unwrap())); + + // Call the handler + ipv6_handler(packet, &source_addr, &destination_addr) + } + + // Unknown protocol numbers can't be handled + proto => { + log::warn!("Unknown Layer 3 protocol: {}", proto); + return None; + } + }; + + // The response from the handler may or may not be a warn-able error + match handler_response { + // If we get data, return it + Ok(data) => Some(data), + // If we get an error, handle it and return None + Err(error) => match error { + interproto::error::Error::PacketTooShort { expected, actual } => { + log::warn!( + "Got packet with length {} when expecting at least {} bytes", + actual, + expected + ); + None + } + interproto::error::Error::UnsupportedIcmpType(icmp_type) => { + log::warn!("Got a packet with an unsupported ICMP type: {}", icmp_type); + None + } + interproto::error::Error::UnsupportedIcmpv6Type(icmpv6_type) => { + log::warn!( + "Got a packet with an unsupported ICMPv6 type: {}", + icmpv6_type + ); + None + } + }, + } +} diff --git a/src/protomask-clat.rs b/src/protomask-clat.rs index 71d0067..c16e6df 100644 --- a/src/protomask-clat.rs +++ b/src/protomask-clat.rs @@ -15,6 +15,8 @@ use std::{ net::{Ipv4Addr, Ipv6Addr}, }; +use crate::common::packet_handler::handle_packet; + mod common; #[derive(Debug, Parser)] @@ -24,6 +26,10 @@ struct Args { #[clap(long="via", default_value_t = ("64:ff9b::/96").parse().unwrap(), value_parser = parse_network_specific_prefix)] embed_prefix: Ipv6Net, + /// One or more customer-side IPv4 prefixes to allow through CLAT + #[clap(short = 'c', long = "customer-prefix", required = true)] + customer_pool: Vec, + /// Explicitly set the interface name to use #[clap(short, long, default_value_t = ("clat%d").to_string())] interface: String, @@ -52,23 +58,44 @@ pub async fn main() { let mut tun = Tun::new(&args.interface).unwrap(); log::debug!("Created TUN interface: {}", tun.name()); - // Configure the new interface - // - Bring up - // - Add IPv6 prefix as a route - // - Point IPv4 default route to the new interface + // Get the interface index let rt_handle = rtnl::new_handle().unwrap(); let tun_link_idx = rtnl::link::get_link_index(&rt_handle, tun.name()) .await .unwrap() .unwrap(); + + // Bring the interface up rtnl::link::link_up(&rt_handle, tun_link_idx).await.unwrap(); - rtnl::route::route_add(IpNet::V6(args.embed_prefix), &rt_handle, tun_link_idx) - .await - .unwrap(); + + // Add an IPv4 default route towards the interface rtnl::route::route_add(IpNet::V4(Ipv4Net::default()), &rt_handle, tun_link_idx) .await .unwrap(); + // Add an IPv6 route for each customer prefix + for customer_prefix in args.customer_pool { + let embedded_customer_prefix = unsafe { + Ipv6Net::new( + embed_ipv4_addr_unchecked(customer_prefix.addr(), args.embed_prefix), + args.embed_prefix.prefix_len() + customer_prefix.prefix_len(), + ) + .unwrap_unchecked() + }; + log::debug!( + "Adding route for {} to {}", + embedded_customer_prefix, + tun.name() + ); + rtnl::route::route_add( + IpNet::V6(embedded_customer_prefix), + &rt_handle, + tun_link_idx, + ) + .await + .unwrap(); + } + // Translate all incoming packets log::info!("Translating packets on {}", tun.name()); let mut buffer = vec![0u8; 1500]; @@ -77,66 +104,27 @@ pub async fn main() { let len = tun.read(&mut buffer).unwrap(); // Translate it based on the Layer 3 protocol number - let layer_3_proto = buffer[0] >> 4; - log::trace!("New packet with layer 3 protocol: {}", layer_3_proto); - match match layer_3_proto { - // IPv4 - 4 => translate_ipv4_to_ipv6( - &buffer[..len], - unsafe { - embed_ipv4_addr_unchecked( - Ipv4Addr::from(u32::from_be_bytes(buffer[12..16].try_into().unwrap())), - args.embed_prefix, - ) - }, - unsafe { - embed_ipv4_addr_unchecked( - Ipv4Addr::from(u32::from_be_bytes(buffer[16..20].try_into().unwrap())), - args.embed_prefix, - ) - }, - ), - - // IPv6 - 6 => translate_ipv6_to_ipv4( - &buffer[..len], - unsafe { - extract_ipv4_addr_unchecked( - Ipv6Addr::from(u128::from_be_bytes(buffer[8..24].try_into().unwrap())), - args.embed_prefix.prefix_len(), - ) - }, - unsafe { - extract_ipv4_addr_unchecked( - Ipv6Addr::from(u128::from_be_bytes(buffer[24..40].try_into().unwrap())), - args.embed_prefix.prefix_len(), - ) - }, - ), - // Unknown - proto => { - log::warn!("Unknown Layer 3 protocol: {}", proto); - continue; - } - } { - Ok(data) => { - // Write the translated packet back to the TUN interface - tun.write(&data).unwrap(); - } - Err(error) => match error { - interproto::error::Error::PacketTooShort { expected, actual } => log::warn!( - "Got packet with length {} when expecting at least {} bytes", - actual, - expected - ), - interproto::error::Error::UnsupportedIcmpType(icmp_type) => { - log::warn!("Got a packet with an unsupported ICMP type: {}", icmp_type) - } - interproto::error::Error::UnsupportedIcmpv6Type(icmpv6_type) => log::warn!( - "Got a packet with an unsupported ICMPv6 type: {}", - icmpv6_type - ), + if let Some(output) = handle_packet( + &buffer[..len], + // IPv4 -> IPv6 + |packet, source, dest| { + translate_ipv4_to_ipv6( + packet, + unsafe { embed_ipv4_addr_unchecked(*source, args.embed_prefix) }, + unsafe { embed_ipv4_addr_unchecked(*dest, args.embed_prefix) }, + ) }, - }; + // IPv6 -> IPv4 + |packet, source, dest| { + translate_ipv6_to_ipv4( + packet, + unsafe { extract_ipv4_addr_unchecked(*source, args.embed_prefix.prefix_len()) }, + unsafe { extract_ipv4_addr_unchecked(*dest, args.embed_prefix.prefix_len()) }, + ) + }, + ) { + // Write the packet if we get one back from the handler functions + tun.write(&output).unwrap(); + } } } From 3c01686b56fabee7a9717be954e3336ead87040f Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 11:28:04 -0400 Subject: [PATCH 17/78] small bugfix --- src/protomask-clat.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/protomask-clat.rs b/src/protomask-clat.rs index c16e6df..b024273 100644 --- a/src/protomask-clat.rs +++ b/src/protomask-clat.rs @@ -10,10 +10,7 @@ use interproto::protocols::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4}; use ipnet::{IpNet, Ipv4Net, Ipv6Net}; use nix::unistd::Uid; use rfc6052::{embed_ipv4_addr_unchecked, extract_ipv4_addr_unchecked}; -use std::{ - io::{Read, Write}, - net::{Ipv4Addr, Ipv6Addr}, -}; +use std::io::{Read, Write}; use crate::common::packet_handler::handle_packet; @@ -124,7 +121,7 @@ pub async fn main() { }, ) { // Write the packet if we get one back from the handler functions - tun.write(&output).unwrap(); + tun.write_all(&output).unwrap(); } } } From af023c0d230c9029457d7c0389cfde289dc9b510 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 11:50:51 -0400 Subject: [PATCH 18/78] Set up mdbook and tweak CI --- .github/workflows/audit.yml | 5 +++-- .github/workflows/build.yml | 4 ++-- .github/workflows/clippy.yml | 23 ----------------------- .github/workflows/lint.yml | 36 ++++++++++++++++++++++++++++++++++++ book.toml | 16 ++++++++++++++++ docs/book/SUMMARY.md | 3 +++ docs/book/chapter_1.md | 1 + 7 files changed, 61 insertions(+), 27 deletions(-) delete mode 100644 .github/workflows/clippy.yml create mode 100644 .github/workflows/lint.yml create mode 100644 book.toml create mode 100644 docs/book/SUMMARY.md create mode 100644 docs/book/chapter_1.md diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 9c08f67..0411450 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -1,4 +1,4 @@ -name: Security audit +name: Audit on: push: @@ -17,7 +17,8 @@ permissions: contents: read jobs: - security_audit: + rust_audit: + name: Rust Audit runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 70b2efb..8eb93b3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: jobs: build_and_test: - name: Rust project + name: Run Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -29,4 +29,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --release \ No newline at end of file + args: --all --release \ No newline at end of file diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml deleted file mode 100644 index 4cb8fdb..0000000 --- a/.github/workflows/clippy.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Clippy check - -on: - push: - paths: - - '.github/workflows/clippy.yml' - - '**/Cargo.toml' - - '**/*.rs' - -permissions: - checks: write - contents: read - -jobs: - clippy_check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - run: rustup component add clippy - - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..f1519f2 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,36 @@ +name: Lint + +on: + push: + paths: + - '.github/workflows/lint.yml' + - '**/Cargo.toml' + - '**/*.rs' + +permissions: + checks: write + contents: read + +jobs: + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Install clippy + run: rustup component add clippy + - name: Run clippy + uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features + + rustfmt: + name: rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Install rustfmt + run: rustup component add rustfmt + - name: Check formatting + run: cargo fmt --all --check diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..9c648e5 --- /dev/null +++ b/book.toml @@ -0,0 +1,16 @@ +[book] +authors = ["Evan Pratten"] +language = "en" +multilingual = false +src = "docs/book" +title = "Protomask Documentation" + +[rust] +edition = "2021" + +[build] +build-dir = "target/book" +create-missing = true + +[output.html] +mathjax-support = true \ No newline at end of file diff --git a/docs/book/SUMMARY.md b/docs/book/SUMMARY.md new file mode 100644 index 0000000..7390c82 --- /dev/null +++ b/docs/book/SUMMARY.md @@ -0,0 +1,3 @@ +# Summary + +- [Chapter 1](./chapter_1.md) diff --git a/docs/book/chapter_1.md b/docs/book/chapter_1.md new file mode 100644 index 0000000..b743fda --- /dev/null +++ b/docs/book/chapter_1.md @@ -0,0 +1 @@ +# Chapter 1 From 40a9f7efda80422d6ec545cd97d5fa61320bc31a Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 11:52:55 -0400 Subject: [PATCH 19/78] Cut out old README content --- README.md | 51 +++------------------------------------------------ 1 file changed, 3 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index dd27900..9875b37 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,11 @@ # protomask -[![Crates.io](https://img.shields.io/crates/v/protomask)](https://crates.io/crates/protomask) -[![Docs.rs](https://docs.rs/protomask/badge.svg)](https://docs.rs/protomask) +[![Crates.io](https://img.shields.io/crates/v/protomask)](https://crates.io/crates/protomask) [![Build](https://github.com/Ewpratten/protomask/actions/workflows/build.yml/badge.svg)](https://github.com/Ewpratten/protomask/actions/workflows/build.yml) +[![Audit](https://github.com/ewpratten/protomask/actions/workflows/audit.yml/badge.svg)](https://github.com/ewpratten/protomask/actions/workflows/audit.yml) **A user space [NAT64](https://en.wikipedia.org/wiki/NAT64) implementation.** -Protomask started as a challenge to create a NAT64 implementation in a weekend. The goal of protomask is to *keep things simple*. - -There aren't many knobs to tweak, so stateful NAT or source address filtering will require protomask to be paired with a utility like `iptables`. - -## How it works - -Protomask operates by listening on an IPv6 `/96` prefix for incoming traffic. - -When a new IPv6 host sends traffic through protomask, it is dynamically assigned an IPv4 address from a pool of addresses on a first-come-first-serve basis. - -From then on, all subsequent packets coming from that same IPv6 host will be NATed through the assigned IPv4 address until the reservation period expires. Likewise, a similar process occurs for return traffic. - -For hosts that necessitate a consistent IPv4 address, it is possible to configure a static mapping in the configuration file. This ensures it always communicates using the same IPv4 address no matter how long it is offline for. This is useful for single-stack hosts that need IPv4 DNS entries. - -## Configuration - -Protomask uses a [TOML](https://toml.io) configuration file. Here is a functional example: - -```toml -# The NAT64 prefix to route to protomask -Nat64Prefix = "64:ff9b::/96" -# Setting this will enable prometheus metrics -Prometheus = "[::1]:8080" # Optional, defaults to disabled - -[Pool] -# All prefixes in the pool -Prefixes = ["192.0.2.0/24"] -# The maximum duration an ipv4 address from the pool will be reserved for after becoming idle -MaxIdleDuration = 7200 # Optional, seconds. Defaults to 7200 (2 hours) -# Permanent address mappings -Static = [{ v4 = "192.0.2.2", v6 = "2001:db8:1::2" }] -``` +*This section is WIP* ## Installation @@ -61,17 +30,3 @@ systemctl start protomask ```bash cargo install protomask ``` - -## Usage - -```text -Usage: protomask [OPTIONS] - -Arguments: - Path to the config file - -Options: - -v, --verbose Enable verbose logging - -h, --help Print help - -V, --version Print version -``` From 0d627d430b7a8324a1469ae31741831b2d27114e Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 12:18:09 -0400 Subject: [PATCH 20/78] Use matrix for builds --- .github/workflows/build.yml | 34 +++++++++++++++++++++++++--------- src/protomask-6over4.rs | 8 ++++---- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8eb93b3..3d13a42 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,30 +3,46 @@ name: Build on: pull_request: push: - paths: - - '.github/workflows/build.yml' - - '**/Cargo.toml' - - '**/Cargo.lock' - - '**/src/*' + paths: + - ".github/workflows/build.yml" + - "**/Cargo.toml" + - "**/Cargo.lock" + - "**/src/*" jobs: build_and_test: - name: Run Tests + name: Run Tests + + # Use a build matrix to run this job targeting all supported platforms runs-on: ubuntu-latest + strategy: + matrix: + # All supported targets + target: + - x86_64-unknown-linux-musl + - aarch64-unknown-linux-musl + # All supported Rust channels + rust_channel: + - stable + + # Common build steps steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: ${{ matrix.rust_channel }} + override: true - name: Compile uses: actions-rs/cargo@v1 with: + use-cross: true command: build - args: --release + args: --release --target ${{ matrix.target }} - name: Run Tests uses: actions-rs/cargo@v1 with: + use-cross: true command: test - args: --all --release \ No newline at end of file + args: --all --release --target ${{ matrix.target }} diff --git a/src/protomask-6over4.rs b/src/protomask-6over4.rs index 9d38cbe..fcb9aef 100644 --- a/src/protomask-6over4.rs +++ b/src/protomask-6over4.rs @@ -1,5 +1,5 @@ - +//! This is a placeholder for a future 6over4 implementation #[tokio::main] -pub async fn main(){ - -} \ No newline at end of file +pub async fn main() { + println!("You did it! You found the incomplete binary :)"); +} From 079416b3eb0fc6bd7c8879f580023dfd7db108c1 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 12:21:59 -0400 Subject: [PATCH 21/78] Apply rustfmt fixes --- libs/easy-tun/src/lib.rs | 2 +- libs/fast-nat/src/bimap.rs | 2 +- libs/fast-nat/src/nat.rs | 2 +- libs/fast-nat/src/timeout.rs | 2 +- libs/interproto/src/lib.rs | 2 +- libs/interproto/src/protocols/ip.rs | 9 +++++++-- libs/interproto/src/protocols/mod.rs | 2 +- libs/rfc6052/src/error.rs | 2 +- libs/rtnl/src/lib.rs | 4 ++-- src/common/mod.rs | 2 +- src/protomask.rs | 5 +---- 11 files changed, 18 insertions(+), 16 deletions(-) diff --git a/libs/easy-tun/src/lib.rs b/libs/easy-tun/src/lib.rs index 87bad45..23d3614 100644 --- a/libs/easy-tun/src/lib.rs +++ b/libs/easy-tun/src/lib.rs @@ -5,4 +5,4 @@ #![allow(clippy::missing_panics_doc)] mod tun; -pub use tun::Tun; \ No newline at end of file +pub use tun::Tun; diff --git a/libs/fast-nat/src/bimap.rs b/libs/fast-nat/src/bimap.rs index 367f5c7..4f1688e 100644 --- a/libs/fast-nat/src/bimap.rs +++ b/libs/fast-nat/src/bimap.rs @@ -108,4 +108,4 @@ mod tests { assert_eq!(bimap.get_right(&3), None); assert_eq!(bimap.get_left(&"three"), None); } -} \ No newline at end of file +} diff --git a/libs/fast-nat/src/nat.rs b/libs/fast-nat/src/nat.rs index e29263a..1fc6d3b 100644 --- a/libs/fast-nat/src/nat.rs +++ b/libs/fast-nat/src/nat.rs @@ -91,4 +91,4 @@ impl Default for NetworkAddressTable { timeouts: FxHashMap::default(), } } -} \ No newline at end of file +} diff --git a/libs/fast-nat/src/timeout.rs b/libs/fast-nat/src/timeout.rs index 9691f6f..cad3989 100644 --- a/libs/fast-nat/src/timeout.rs +++ b/libs/fast-nat/src/timeout.rs @@ -10,4 +10,4 @@ pub enum MaybeTimeout { duration: Duration, start: std::time::Instant, }, -} \ No newline at end of file +} diff --git a/libs/interproto/src/lib.rs b/libs/interproto/src/lib.rs index f5e4aa0..c7894fb 100644 --- a/libs/interproto/src/lib.rs +++ b/libs/interproto/src/lib.rs @@ -5,5 +5,5 @@ #![allow(clippy::missing_panics_doc)] #![allow(clippy::doc_markdown)] +pub mod error; pub mod protocols; -pub mod error; \ No newline at end of file diff --git a/libs/interproto/src/protocols/ip.rs b/libs/interproto/src/protocols/ip.rs index b7c84e6..2ad9b16 100644 --- a/libs/interproto/src/protocols/ip.rs +++ b/libs/interproto/src/protocols/ip.rs @@ -2,7 +2,8 @@ use super::{ icmp::{translate_icmp_to_icmpv6, translate_icmpv6_to_icmp}, - tcp::{recalculate_tcp_checksum_ipv4, recalculate_tcp_checksum_ipv6}, udp::{recalculate_udp_checksum_ipv6, recalculate_udp_checksum_ipv4}, + tcp::{recalculate_tcp_checksum_ipv4, recalculate_tcp_checksum_ipv6}, + udp::{recalculate_udp_checksum_ipv4, recalculate_udp_checksum_ipv6}, }; use crate::error::{Error, Result}; use pnet::packet::{ @@ -127,7 +128,11 @@ pub fn translate_ipv6_to_ipv4( }); ipv4_packet.set_source(new_source); ipv4_packet.set_destination(new_destination); - ipv4_packet.set_total_length((Ipv4Packet::minimum_packet_size() + new_payload.len()).try_into().unwrap()); + ipv4_packet.set_total_length( + (Ipv4Packet::minimum_packet_size() + new_payload.len()) + .try_into() + .unwrap(), + ); // Copy the payload to the buffer ipv4_packet.set_payload(&new_payload); diff --git a/libs/interproto/src/protocols/mod.rs b/libs/interproto/src/protocols/mod.rs index 5dff8bd..50baf68 100644 --- a/libs/interproto/src/protocols/mod.rs +++ b/libs/interproto/src/protocols/mod.rs @@ -1,4 +1,4 @@ +pub mod icmp; pub mod ip; pub mod tcp; pub mod udp; -pub mod icmp; \ No newline at end of file diff --git a/libs/rfc6052/src/error.rs b/libs/rfc6052/src/error.rs index c616737..85fb837 100644 --- a/libs/rfc6052/src/error.rs +++ b/libs/rfc6052/src/error.rs @@ -4,4 +4,4 @@ pub enum Error { #[error("Invalid IPv6 prefix length: {0}. Must be one of 32, 40, 48, 56, 64, or 96")] InvalidPrefixLength(u8), -} \ No newline at end of file +} diff --git a/libs/rtnl/src/lib.rs b/libs/rtnl/src/lib.rs index dbd29da..6d8647c 100644 --- a/libs/rtnl/src/lib.rs +++ b/libs/rtnl/src/lib.rs @@ -5,12 +5,12 @@ #![allow(clippy::missing_panics_doc)] #![allow(clippy::missing_safety_doc)] -pub mod link; pub mod ip; +pub mod link; pub mod route; /// Get a handle on a new rtnetlink connection -#[cfg(feature="tokio")] +#[cfg(feature = "tokio")] pub fn new_handle() -> Result { let (rt_connection, rt_handle, _) = rtnetlink::new_connection().map_err(|err| { log::error!("Failed to open rtnetlink connection"); diff --git a/src/common/mod.rs b/src/common/mod.rs index 33fdbba..bdc0db4 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,5 +1,5 @@ //! Common code used across all protomask binaries pub mod logging; +pub mod packet_handler; pub mod rfc6052; -pub mod packet_handler; \ No newline at end of file diff --git a/src/protomask.rs b/src/protomask.rs index 9d38cbe..b8c879c 100644 --- a/src/protomask.rs +++ b/src/protomask.rs @@ -1,5 +1,2 @@ - #[tokio::main] -pub async fn main(){ - -} \ No newline at end of file +pub async fn main() {} From 62e1bdb1c0197fd08c62bd9a2bede9cafd223872 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 12:36:10 -0400 Subject: [PATCH 22/78] Fix issues with arch-specific types --- libs/easy-tun/src/tun.rs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/libs/easy-tun/src/tun.rs b/libs/easy-tun/src/tun.rs index 7e7d3ab..245329d 100644 --- a/libs/easy-tun/src/tun.rs +++ b/libs/easy-tun/src/tun.rs @@ -8,6 +8,18 @@ use std::{ use ioctl_gen::{ioc, iow}; use libc::{__c_anonymous_ifr_ifru, ifreq, ioctl, IFF_NO_PI, IFF_TUN, IF_NAMESIZE}; +/// Architecture / target environment specific definitions +mod arch { + #[cfg(all(target_os = "linux", target_env = "gnu"))] + pub type IoctlRequestType = libc::c_ulong; + #[cfg(all(target_os = "linux", target_env = "musl"))] + pub type IoctlRequestType = libc::c_int; + #[cfg(not(any(target_env = "gnu", target_env = "musl")))] + compile_error!( + "Unsupported target environment. Only gnu and musl targets are currently supported." + ); +} + /// A TUN device pub struct Tun { /// Internal file descriptor for the TUN device @@ -33,8 +45,8 @@ impl Tun { // Copy the device name into a C string with padding // NOTE: No zero padding is needed because we pre-init the array to all 0s - let mut dev_cstr = [0i8; IF_NAMESIZE]; - let dev_bytes: Vec = dev.chars().map(|c| c as i8).collect(); + let mut dev_cstr: [libc::c_char; IF_NAMESIZE] = [0; IF_NAMESIZE]; + let dev_bytes: Vec = dev.chars().map(|c| c as libc::c_char).collect(); let dev_len = dev_bytes.len().min(IF_NAMESIZE); log::trace!("Device name length after truncation: {}", dev_len); dev_cstr[..dev_len].copy_from_slice(&dev_bytes[..dev_len]); @@ -52,7 +64,7 @@ impl Tun { let err = unsafe { ioctl( fd.as_raw_fd(), - iow!('T', 202, size_of::()) as u64, + iow!('T', 202, size_of::()) as arch::IoctlRequestType, &mut ifr, ) }; From 5d8919e2b5213e3802d9916deedb902755b2056a Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 12:48:58 -0400 Subject: [PATCH 23/78] Automatically build debs --- .github/workflows/build.yml | 21 ++++++++++++++++++++- Makefile | 17 ----------------- 2 files changed, 20 insertions(+), 18 deletions(-) delete mode 100644 Makefile diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d13a42..ed36fe8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ on: jobs: build_and_test: - name: Run Tests + name: Build & Test # Use a build matrix to run this job targeting all supported platforms runs-on: ubuntu-latest @@ -33,6 +33,12 @@ jobs: toolchain: ${{ matrix.rust_channel }} override: true + - name: Install cargo-deb + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-deb + - name: Compile uses: actions-rs/cargo@v1 with: @@ -46,3 +52,16 @@ jobs: use-cross: true command: test args: --all --release --target ${{ matrix.target }} + + - name: Package DEB + uses: actions-rs/cargo@v1 + with: + use-cross: false + command: deb + args: --target ${{ matrix.target }} --no-build + + - name: Upload DEB + uses: actions/upload-artifact@v3 + with: + name: debian-packages + path: target/${{ matrix.target }}/debian/*.deb diff --git a/Makefile b/Makefile deleted file mode 100644 index f2b9397..0000000 --- a/Makefile +++ /dev/null @@ -1,17 +0,0 @@ -# All sources used to build the protomask binary -SRC = Cargo.toml $(shell find src/ -type f -name '*.rs') $(shell find protomask-tun/src/ -type f -name '*.rs') - -# Used to auto-version things -CRATE_VERSION = $(shell sed -n -r "s/^version = \"([0-9\.]+)\"/\1/p" Cargo.toml) - -target/x86_64-unknown-linux-musl/release/protomask: $(SRC) - cross build --target x86_64-unknown-linux-musl --release - -target/aarch64-unknown-linux-musl/release/protomask: $(SRC) - cross build --target aarch64-unknown-linux-musl --release - -target/x86_64-unknown-linux-musl/debian/protomask_${CRATE_VERSION}_amd64.deb: target/x86_64-unknown-linux-musl/release/protomask - cargo deb --target x86_64-unknown-linux-musl --no-build - -target/aarch64-unknown-linux-musl/debian/protomask_${CRATE_VERSION}_arm64.deb: target/aarch64-unknown-linux-musl/release/protomask - cargo deb --target aarch64-unknown-linux-musl --no-build From f69625c9b006200ff35cad0334773f2ffadfa629 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 13:12:58 -0400 Subject: [PATCH 24/78] Implement nat64 cli args --- src/protomask-clat.rs | 2 +- src/protomask.rs | 68 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/protomask-clat.rs b/src/protomask-clat.rs index b024273..cab3299 100644 --- a/src/protomask-clat.rs +++ b/src/protomask-clat.rs @@ -19,7 +19,7 @@ mod common; #[derive(Debug, Parser)] #[clap(author, version, about="IPv4 to IPv6 Customer-side transLATor (CLAT)", long_about = None)] struct Args { - /// IPv6 prefix to embed IPv4 addresses in + /// RFC6052 IPv6 prefix to encapsulate IPv4 packets within #[clap(long="via", default_value_t = ("64:ff9b::/96").parse().unwrap(), value_parser = parse_network_specific_prefix)] embed_prefix: Ipv6Net, diff --git a/src/protomask.rs b/src/protomask.rs index b8c879c..cb42757 100644 --- a/src/protomask.rs +++ b/src/protomask.rs @@ -1,2 +1,68 @@ +use std::path::PathBuf; + +use clap::Parser; +use common::{logging::enable_logger, rfc6052::parse_network_specific_prefix}; +use ipnet::{Ipv4Net, Ipv6Net}; +use nix::unistd::Uid; + +mod common; + +#[derive(Parser)] +#[clap(author, version, about="Fast and simple NAT64", long_about = None)] +struct Args { + /// RFC6052 IPv6 translation prefix + #[clap(long, default_value_t = ("64:ff9b::/96").parse().unwrap(), value_parser = parse_network_specific_prefix)] + translation_prefix: Ipv6Net, + + #[command(flatten)] + pool: PoolArgs, + + /// A CSV file containing static address mappings from IPv6 to IPv4 + #[clap(long = "static-file")] + static_file: Option, + + /// NAT reservation timeout in seconds + #[clap(long, default_value = "7200")] + reservation_timeout: u64, + + /// Explicitly set the interface name to use + #[clap(short, long, default_value_t = ("nat%d").to_string())] + interface: String, + + /// Enable verbose logging + #[clap(short, long)] + verbose: bool, +} + +#[derive(clap::Args)] +#[group(required = true, multiple = false)] +struct PoolArgs { + /// IPv4 prefixes to use as NAT pool address space + #[clap(long = "pool-add")] + pool_prefixes: Vec, + + /// A file containing newline-delimited IPv4 prefixes to use as NAT pool address space + #[clap(long = "pool-file", conflicts_with = "pool_prefixes")] + pool_file: Option, +} + +impl PoolArgs { + pub fn prefixes(&self) -> Result, std::io::Error> { + todo!() + } +} + #[tokio::main] -pub async fn main() {} +pub async fn main() { + // Parse CLI args + let args = Args::parse(); + + // Initialize logging + enable_logger(args.verbose); + + // We must be root to continue program execution + if !Uid::effective().is_root() { + log::error!("This program must be run as root"); + std::process::exit(1); + } +} From f2b2be54c822e0f9f84cd698e4d0a5513b6ec9cf Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 16:58:44 -0400 Subject: [PATCH 25/78] Implement metrics in interproto --- Cargo.toml | 4 +- libs/interproto/Cargo.toml | 7 +- libs/interproto/src/protocols/icmp/mod.rs | 199 +++++++++++-------- libs/interproto/src/protocols/ip.rs | 222 +++++++++++++--------- libs/interproto/src/protocols/tcp.rs | 98 ++++++---- libs/interproto/src/protocols/udp.rs | 98 ++++++---- libs/protomask-metrics/Cargo.toml | 18 ++ libs/protomask-metrics/README.md | 1 + libs/protomask-metrics/src/lib.rs | 10 + libs/protomask-metrics/src/macros.rs | 10 + libs/protomask-metrics/src/metrics.rs | 30 +++ src/protomask.rs | 113 ++++++++++- 12 files changed, 556 insertions(+), 254 deletions(-) create mode 100644 libs/protomask-metrics/Cargo.toml create mode 100644 libs/protomask-metrics/README.md create mode 100644 libs/protomask-metrics/src/lib.rs create mode 100644 libs/protomask-metrics/src/macros.rs create mode 100644 libs/protomask-metrics/src/metrics.rs diff --git a/Cargo.toml b/Cargo.toml index c83dea3..819b552 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ members = [ "libs/interproto", "libs/rfc6052", "libs/rtnl", + "libs/protomask-metrics", ] [[bin]] @@ -65,9 +66,10 @@ path = "src/protomask-6over4.rs" # Internal dependencies easy-tun = { path = "libs/easy-tun" } fast-nat = { path = "libs/fast-nat" } -interproto = { path = "libs/interproto" } +interproto = { path = "libs/interproto", features = ["metrics"] } rfc6052 = { path = "libs/rfc6052" } rtnl = { path = "libs/rtnl", features = ["tokio"] } +protomask-metrics = { path = "libs/protomask-metrics" } # External Dependencies tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } diff --git a/libs/interproto/Cargo.toml b/libs/interproto/Cargo.toml index 3e1405c..357e1b0 100644 --- a/libs/interproto/Cargo.toml +++ b/libs/interproto/Cargo.toml @@ -12,7 +12,12 @@ license = "GPL-3.0" keywords = [] categories = [] +[features] +default = [] +metrics = ["protomask-metrics"] + [dependencies] +protomask-metrics = { path = "../protomask-metrics", optional = true } log = "^0.4" pnet = "0.34.0" -thiserror = "^1.0.44" \ No newline at end of file +thiserror = "^1.0.44" diff --git a/libs/interproto/src/protocols/icmp/mod.rs b/libs/interproto/src/protocols/icmp/mod.rs index a529d6c..748bef2 100644 --- a/libs/interproto/src/protocols/icmp/mod.rs +++ b/libs/interproto/src/protocols/icmp/mod.rs @@ -20,59 +20,74 @@ pub fn translate_icmp_to_icmpv6( new_source: Ipv6Addr, new_destination: Ipv6Addr, ) -> Result> { - // Access the ICMP packet data in a safe way - let icmp_packet = IcmpPacket::new(icmp_packet).ok_or(Error::PacketTooShort { - expected: IcmpPacket::minimum_packet_size(), - actual: icmp_packet.len(), - })?; + // This scope is used to collect packet drop metrics + { + // Access the ICMP packet data in a safe way + let icmp_packet = IcmpPacket::new(icmp_packet).ok_or(Error::PacketTooShort { + expected: IcmpPacket::minimum_packet_size(), + actual: icmp_packet.len(), + })?; - // Translate the ICMP type and code to their ICMPv6 equivalents - let (icmpv6_type, icmpv6_code) = type_code::translate_type_and_code_4_to_6( - icmp_packet.get_icmp_type(), - icmp_packet.get_icmp_code(), - )?; + // Translate the ICMP type and code to their ICMPv6 equivalents + let (icmpv6_type, icmpv6_code) = type_code::translate_type_and_code_4_to_6( + icmp_packet.get_icmp_type(), + icmp_packet.get_icmp_code(), + )?; - // Some ICMP types require special payload edits - let payload = match icmpv6_type { - Icmpv6Types::TimeExceeded => { - // Time exceeded messages contain the original IPv4 header and part of the payload. (with 4 bytes of forward padding) - // We need to translate the IPv4 header and the payload, but keep the padding - let mut output = vec![0u8; 4]; - output.copy_from_slice(&icmp_packet.payload()[..4]); - output.extend_from_slice(&translate_ipv4_to_ipv6( - &icmp_packet.payload()[4..], - new_source, - new_destination, - )?); - output - } - _ => icmp_packet.payload().to_vec(), - }; + // Some ICMP types require special payload edits + let payload = match icmpv6_type { + Icmpv6Types::TimeExceeded => { + // Time exceeded messages contain the original IPv4 header and part of the payload. (with 4 bytes of forward padding) + // We need to translate the IPv4 header and the payload, but keep the padding + let mut output = vec![0u8; 4]; + output.copy_from_slice(&icmp_packet.payload()[..4]); + output.extend_from_slice(&translate_ipv4_to_ipv6( + &icmp_packet.payload()[4..], + new_source, + new_destination, + )?); + output + } + _ => icmp_packet.payload().to_vec(), + }; - // Build a buffer to store the new ICMPv6 packet - let mut output_buffer = vec![0u8; IcmpPacket::minimum_packet_size() + payload.len()]; + // Build a buffer to store the new ICMPv6 packet + let mut output_buffer = vec![0u8; IcmpPacket::minimum_packet_size() + payload.len()]; - // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. - let mut icmpv6_packet = - unsafe { MutableIcmpv6Packet::new(&mut output_buffer).unwrap_unchecked() }; + // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. + let mut icmpv6_packet = + unsafe { MutableIcmpv6Packet::new(&mut output_buffer).unwrap_unchecked() }; - // Set the header fields - icmpv6_packet.set_icmpv6_type(icmpv6_type); - icmpv6_packet.set_icmpv6_code(icmpv6_code); - icmpv6_packet.set_checksum(0); + // Set the header fields + icmpv6_packet.set_icmpv6_type(icmpv6_type); + icmpv6_packet.set_icmpv6_code(icmpv6_code); + icmpv6_packet.set_checksum(0); - // Copy the payload - icmpv6_packet.set_payload(&payload); + // Copy the payload + icmpv6_packet.set_payload(&payload); - // Calculate the checksum - icmpv6_packet.set_checksum(icmpv6::checksum( - &icmpv6_packet.to_immutable(), - &new_source, - &new_destination, - )); + // Calculate the checksum + icmpv6_packet.set_checksum(icmpv6::checksum( + &icmpv6_packet.to_immutable(), + &new_source, + &new_destination, + )); - // Return the translated packet - Ok(output_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_ICMP, STATUS_TRANSLATED).inc(); + + // Return the translated packet + Ok(output_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_ICMP, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } /// Translate an ICMPv6 packet to ICMP. This will make a best guess at the ICMP type and code since there is no 1:1 mapping. @@ -81,51 +96,67 @@ pub fn translate_icmpv6_to_icmp( new_source: Ipv4Addr, new_destination: Ipv4Addr, ) -> Result> { - // Access the ICMPv6 packet data in a safe way - let icmpv6_packet = Icmpv6Packet::new(icmpv6_packet).ok_or(Error::PacketTooShort { - expected: Icmpv6Packet::minimum_packet_size(), - actual: icmpv6_packet.len(), - })?; + // This scope is used to collect packet drop metrics + { + // Access the ICMPv6 packet data in a safe way + let icmpv6_packet = Icmpv6Packet::new(icmpv6_packet).ok_or(Error::PacketTooShort { + expected: Icmpv6Packet::minimum_packet_size(), + actual: icmpv6_packet.len(), + })?; - // Translate the ICMPv6 type and code to their ICMP equivalents - let (icmp_type, icmp_code) = type_code::translate_type_and_code_6_to_4( - icmpv6_packet.get_icmpv6_type(), - icmpv6_packet.get_icmpv6_code(), - )?; + // Translate the ICMPv6 type and code to their ICMP equivalents + let (icmp_type, icmp_code) = type_code::translate_type_and_code_6_to_4( + icmpv6_packet.get_icmpv6_type(), + icmpv6_packet.get_icmpv6_code(), + )?; - // Some ICMP types require special payload edits - let payload = match icmp_type { - IcmpTypes::TimeExceeded => { - // Time exceeded messages contain the original IPv6 header and part of the payload. (with 4 bytes of forward padding) - // We need to translate the IPv6 header and the payload, but keep the padding - let mut output = vec![0u8; 4]; - output.copy_from_slice(&icmpv6_packet.payload()[..4]); - output.extend_from_slice(&translate_ipv6_to_ipv4( - &icmpv6_packet.payload()[4..], - new_source, - new_destination, - )?); - output - } - _ => icmpv6_packet.payload().to_vec(), - }; + // Some ICMP types require special payload edits + let payload = match icmp_type { + IcmpTypes::TimeExceeded => { + // Time exceeded messages contain the original IPv6 header and part of the payload. (with 4 bytes of forward padding) + // We need to translate the IPv6 header and the payload, but keep the padding + let mut output = vec![0u8; 4]; + output.copy_from_slice(&icmpv6_packet.payload()[..4]); + output.extend_from_slice(&translate_ipv6_to_ipv4( + &icmpv6_packet.payload()[4..], + new_source, + new_destination, + )?); + output + } + _ => icmpv6_packet.payload().to_vec(), + }; - // Build a buffer to store the new ICMP packet - let mut output_buffer = vec![0u8; Icmpv6Packet::minimum_packet_size() + payload.len()]; + // Build a buffer to store the new ICMP packet + let mut output_buffer = vec![0u8; Icmpv6Packet::minimum_packet_size() + payload.len()]; - // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. - let mut icmp_packet = unsafe { MutableIcmpPacket::new(&mut output_buffer).unwrap_unchecked() }; + // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. + let mut icmp_packet = + unsafe { MutableIcmpPacket::new(&mut output_buffer).unwrap_unchecked() }; - // Set the header fields - icmp_packet.set_icmp_type(icmp_type); - icmp_packet.set_icmp_code(icmp_code); + // Set the header fields + icmp_packet.set_icmp_type(icmp_type); + icmp_packet.set_icmp_code(icmp_code); - // Copy the payload - icmp_packet.set_payload(&payload); + // Copy the payload + icmp_packet.set_payload(&payload); - // Calculate the checksum - icmp_packet.set_checksum(icmp::checksum(&icmp_packet.to_immutable())); + // Calculate the checksum + icmp_packet.set_checksum(icmp::checksum(&icmp_packet.to_immutable())); - // Return the translated packet - Ok(output_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_ICMPV6, STATUS_TRANSLATED).inc(); + + // Return the translated packet + Ok(output_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_ICMPV6, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } diff --git a/libs/interproto/src/protocols/ip.rs b/libs/interproto/src/protocols/ip.rs index 2ad9b16..daec44e 100644 --- a/libs/interproto/src/protocols/ip.rs +++ b/libs/interproto/src/protocols/ip.rs @@ -20,59 +20,75 @@ pub fn translate_ipv4_to_ipv6( new_source: Ipv6Addr, new_destination: Ipv6Addr, ) -> Result> { - // Access the IPv4 packet data in a safe way - let ipv4_packet = Ipv4Packet::new(ipv4_packet).ok_or(Error::PacketTooShort { - expected: Ipv4Packet::minimum_packet_size(), - actual: ipv4_packet.len(), - })?; + // This scope is used to collect packet drop metrics + { + // Access the IPv4 packet data in a safe way + let ipv4_packet = Ipv4Packet::new(ipv4_packet).ok_or(Error::PacketTooShort { + expected: Ipv4Packet::minimum_packet_size(), + actual: ipv4_packet.len(), + })?; - // Perform recursive translation to determine the new payload - let new_payload = match ipv4_packet.get_next_level_protocol() { - // Pass ICMP packets to the icmp-to-icmpv6 translator - IpNextHeaderProtocols::Icmp => { - translate_icmp_to_icmpv6(ipv4_packet.payload(), new_source, new_destination)? - } + // Perform recursive translation to determine the new payload + let new_payload = match ipv4_packet.get_next_level_protocol() { + // Pass ICMP packets to the icmp-to-icmpv6 translator + IpNextHeaderProtocols::Icmp => { + translate_icmp_to_icmpv6(ipv4_packet.payload(), new_source, new_destination)? + } - // Pass TCP packets to the tcp translator - IpNextHeaderProtocols::Tcp => { - recalculate_tcp_checksum_ipv6(ipv4_packet.payload(), new_source, new_destination)? - } + // Pass TCP packets to the tcp translator + IpNextHeaderProtocols::Tcp => { + recalculate_tcp_checksum_ipv6(ipv4_packet.payload(), new_source, new_destination)? + } - // Pass UDP packets to the udp translator - IpNextHeaderProtocols::Udp => { - recalculate_udp_checksum_ipv6(ipv4_packet.payload(), new_source, new_destination)? - } + // Pass UDP packets to the udp translator + IpNextHeaderProtocols::Udp => { + recalculate_udp_checksum_ipv6(ipv4_packet.payload(), new_source, new_destination)? + } - // If the next level protocol is not something we know how to translate, - // just assume the payload can be passed through as-is - protocol => { - log::warn!("Unsupported next level protocol: {:?}", protocol); - ipv4_packet.payload().to_vec() - } - }; + // If the next level protocol is not something we know how to translate, + // just assume the payload can be passed through as-is + protocol => { + log::warn!("Unsupported next level protocol: {:?}", protocol); + ipv4_packet.payload().to_vec() + } + }; - // Build a buffer to store the new IPv6 packet - let mut output_buffer = vec![0u8; Ipv6Packet::minimum_packet_size() + new_payload.len()]; + // Build a buffer to store the new IPv6 packet + let mut output_buffer = vec![0u8; Ipv6Packet::minimum_packet_size() + new_payload.len()]; - // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. - let mut ipv6_packet = unsafe { MutableIpv6Packet::new(&mut output_buffer).unwrap_unchecked() }; + // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. + let mut ipv6_packet = + unsafe { MutableIpv6Packet::new(&mut output_buffer).unwrap_unchecked() }; - // Set the header fields - ipv6_packet.set_version(6); - ipv6_packet.set_next_header(match ipv4_packet.get_next_level_protocol() { - IpNextHeaderProtocols::Icmp => IpNextHeaderProtocols::Icmpv6, - proto => proto, - }); - ipv6_packet.set_hop_limit(ipv4_packet.get_ttl()); - ipv6_packet.set_source(new_source); - ipv6_packet.set_destination(new_destination); - ipv6_packet.set_payload_length(new_payload.len().try_into().unwrap()); + // Set the header fields + ipv6_packet.set_version(6); + ipv6_packet.set_next_header(match ipv4_packet.get_next_level_protocol() { + IpNextHeaderProtocols::Icmp => IpNextHeaderProtocols::Icmpv6, + proto => proto, + }); + ipv6_packet.set_hop_limit(ipv4_packet.get_ttl()); + ipv6_packet.set_source(new_source); + ipv6_packet.set_destination(new_destination); + ipv6_packet.set_payload_length(new_payload.len().try_into().unwrap()); - // Copy the payload to the buffer - ipv6_packet.set_payload(&new_payload); + // Copy the payload to the buffer + ipv6_packet.set_payload(&new_payload); - // Return the buffer - Ok(output_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_IPV4, STATUS_TRANSLATED).inc(); + + // Return the buffer + Ok(output_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_IPV4, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } /// Translates an IPv6 packet into an IPv4 packet. The packet payload will be translated recursively as needed. @@ -81,65 +97,81 @@ pub fn translate_ipv6_to_ipv4( new_source: Ipv4Addr, new_destination: Ipv4Addr, ) -> Result> { - // Access the IPv6 packet data in a safe way - let ipv6_packet = Ipv6Packet::new(ipv6_packet).ok_or(Error::PacketTooShort { - expected: Ipv6Packet::minimum_packet_size(), - actual: ipv6_packet.len(), - })?; + // This scope is used to collect packet drop metrics + { + // Access the IPv6 packet data in a safe way + let ipv6_packet = Ipv6Packet::new(ipv6_packet).ok_or(Error::PacketTooShort { + expected: Ipv6Packet::minimum_packet_size(), + actual: ipv6_packet.len(), + })?; - // Perform recursive translation to determine the new payload - let new_payload = match ipv6_packet.get_next_header() { - // Pass ICMP packets to the icmpv6-to-icmp translator - IpNextHeaderProtocols::Icmpv6 => { - translate_icmpv6_to_icmp(ipv6_packet.payload(), new_source, new_destination)? - } + // Perform recursive translation to determine the new payload + let new_payload = match ipv6_packet.get_next_header() { + // Pass ICMP packets to the icmpv6-to-icmp translator + IpNextHeaderProtocols::Icmpv6 => { + translate_icmpv6_to_icmp(ipv6_packet.payload(), new_source, new_destination)? + } - // Pass TCP packets to the tcp translator - IpNextHeaderProtocols::Tcp => { - recalculate_tcp_checksum_ipv4(ipv6_packet.payload(), new_source, new_destination)? - } + // Pass TCP packets to the tcp translator + IpNextHeaderProtocols::Tcp => { + recalculate_tcp_checksum_ipv4(ipv6_packet.payload(), new_source, new_destination)? + } - // Pass UDP packets to the udp translator - IpNextHeaderProtocols::Udp => { - recalculate_udp_checksum_ipv4(ipv6_packet.payload(), new_source, new_destination)? - } + // Pass UDP packets to the udp translator + IpNextHeaderProtocols::Udp => { + recalculate_udp_checksum_ipv4(ipv6_packet.payload(), new_source, new_destination)? + } - // If the next header is not something we know how to translate, - // just assume the payload can be passed through as-is - protocol => { - log::warn!("Unsupported next header: {:?}", protocol); - ipv6_packet.payload().to_vec() - } - }; + // If the next header is not something we know how to translate, + // just assume the payload can be passed through as-is + protocol => { + log::warn!("Unsupported next header: {:?}", protocol); + ipv6_packet.payload().to_vec() + } + }; - // Build a buffer to store the new IPv4 packet - let mut output_buffer = vec![0u8; Ipv4Packet::minimum_packet_size() + new_payload.len()]; + // Build a buffer to store the new IPv4 packet + let mut output_buffer = vec![0u8; Ipv4Packet::minimum_packet_size() + new_payload.len()]; - // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. - let mut ipv4_packet = unsafe { MutableIpv4Packet::new(&mut output_buffer).unwrap_unchecked() }; + // NOTE: There is no way this can fail since we are creating the buffer with explicitly enough space. + let mut ipv4_packet = + unsafe { MutableIpv4Packet::new(&mut output_buffer).unwrap_unchecked() }; - // Set the header fields - ipv4_packet.set_version(4); - ipv4_packet.set_header_length(5); - ipv4_packet.set_ttl(ipv6_packet.get_hop_limit()); - ipv4_packet.set_next_level_protocol(match ipv6_packet.get_next_header() { - IpNextHeaderProtocols::Icmpv6 => IpNextHeaderProtocols::Icmp, - proto => proto, - }); - ipv4_packet.set_source(new_source); - ipv4_packet.set_destination(new_destination); - ipv4_packet.set_total_length( - (Ipv4Packet::minimum_packet_size() + new_payload.len()) - .try_into() - .unwrap(), - ); + // Set the header fields + ipv4_packet.set_version(4); + ipv4_packet.set_header_length(5); + ipv4_packet.set_ttl(ipv6_packet.get_hop_limit()); + ipv4_packet.set_next_level_protocol(match ipv6_packet.get_next_header() { + IpNextHeaderProtocols::Icmpv6 => IpNextHeaderProtocols::Icmp, + proto => proto, + }); + ipv4_packet.set_source(new_source); + ipv4_packet.set_destination(new_destination); + ipv4_packet.set_total_length( + (Ipv4Packet::minimum_packet_size() + new_payload.len()) + .try_into() + .unwrap(), + ); - // Copy the payload to the buffer - ipv4_packet.set_payload(&new_payload); + // Copy the payload to the buffer + ipv4_packet.set_payload(&new_payload); - // Calculate the checksum - ipv4_packet.set_checksum(ipv4::checksum(&ipv4_packet.to_immutable())); + // Calculate the checksum + ipv4_packet.set_checksum(ipv4::checksum(&ipv4_packet.to_immutable())); - // Return the buffer - Ok(output_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_IPV6, STATUS_TRANSLATED).inc(); + + // Return the buffer + Ok(output_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_IPV6, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } diff --git a/libs/interproto/src/protocols/tcp.rs b/libs/interproto/src/protocols/tcp.rs index 4885b67..e33bc23 100644 --- a/libs/interproto/src/protocols/tcp.rs +++ b/libs/interproto/src/protocols/tcp.rs @@ -10,26 +10,41 @@ pub fn recalculate_tcp_checksum_ipv6( new_source: Ipv6Addr, new_destination: Ipv6Addr, ) -> Result> { - // Clone the packet so we can modify it - let mut tcp_packet_buffer = tcp_packet.to_vec(); + // This scope is used to collect packet drop metrics + { + // Clone the packet so we can modify it + let mut tcp_packet_buffer = tcp_packet.to_vec(); - // Get safe mutable access to the packet - let mut tcp_packet = - MutableTcpPacket::new(&mut tcp_packet_buffer).ok_or(Error::PacketTooShort { - expected: TcpPacket::minimum_packet_size(), - actual: tcp_packet.len(), - })?; + // Get safe mutable access to the packet + let mut tcp_packet = + MutableTcpPacket::new(&mut tcp_packet_buffer).ok_or(Error::PacketTooShort { + expected: TcpPacket::minimum_packet_size(), + actual: tcp_packet.len(), + })?; - // Edit the packet's checksum - tcp_packet.set_checksum(0); - tcp_packet.set_checksum(tcp::ipv6_checksum( - &tcp_packet.to_immutable(), - &new_source, - &new_destination, - )); + // Edit the packet's checksum + tcp_packet.set_checksum(0); + tcp_packet.set_checksum(tcp::ipv6_checksum( + &tcp_packet.to_immutable(), + &new_source, + &new_destination, + )); - // Return the translated packet - Ok(tcp_packet_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_TCP, STATUS_TRANSLATED).inc(); + + // Return the translated packet + Ok(tcp_packet_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_TCP, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } /// Re-calculates a TCP packet's checksum with a new IPv4 pseudo-header. @@ -38,26 +53,41 @@ pub fn recalculate_tcp_checksum_ipv4( new_source: Ipv4Addr, new_destination: Ipv4Addr, ) -> Result> { - // Clone the packet so we can modify it - let mut tcp_packet_buffer = tcp_packet.to_vec(); + // This scope is used to collect packet drop metrics + { + // Clone the packet so we can modify it + let mut tcp_packet_buffer = tcp_packet.to_vec(); - // Get safe mutable access to the packet - let mut tcp_packet = - MutableTcpPacket::new(&mut tcp_packet_buffer).ok_or(Error::PacketTooShort { - expected: TcpPacket::minimum_packet_size(), - actual: tcp_packet.len(), - })?; + // Get safe mutable access to the packet + let mut tcp_packet = + MutableTcpPacket::new(&mut tcp_packet_buffer).ok_or(Error::PacketTooShort { + expected: TcpPacket::minimum_packet_size(), + actual: tcp_packet.len(), + })?; - // Edit the packet's checksum - tcp_packet.set_checksum(0); - tcp_packet.set_checksum(tcp::ipv4_checksum( - &tcp_packet.to_immutable(), - &new_source, - &new_destination, - )); + // Edit the packet's checksum + tcp_packet.set_checksum(0); + tcp_packet.set_checksum(tcp::ipv4_checksum( + &tcp_packet.to_immutable(), + &new_source, + &new_destination, + )); - // Return the translated packet - Ok(tcp_packet_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_TCP, STATUS_TRANSLATED).inc(); + + // Return the translated packet + Ok(tcp_packet_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_TCP, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } #[cfg(test)] diff --git a/libs/interproto/src/protocols/udp.rs b/libs/interproto/src/protocols/udp.rs index cef9a23..099ef7c 100644 --- a/libs/interproto/src/protocols/udp.rs +++ b/libs/interproto/src/protocols/udp.rs @@ -10,26 +10,41 @@ pub fn recalculate_udp_checksum_ipv6( new_source: Ipv6Addr, new_destination: Ipv6Addr, ) -> Result> { - // Clone the packet so we can modify it - let mut udp_packet_buffer = udp_packet.to_vec(); + // This scope is used to collect packet drop metrics + { + // Clone the packet so we can modify it + let mut udp_packet_buffer = udp_packet.to_vec(); - // Get safe mutable access to the packet - let mut udp_packet = - MutableUdpPacket::new(&mut udp_packet_buffer).ok_or(Error::PacketTooShort { - expected: UdpPacket::minimum_packet_size(), - actual: udp_packet.len(), - })?; + // Get safe mutable access to the packet + let mut udp_packet = + MutableUdpPacket::new(&mut udp_packet_buffer).ok_or(Error::PacketTooShort { + expected: UdpPacket::minimum_packet_size(), + actual: udp_packet.len(), + })?; - // Edit the packet's checksum - udp_packet.set_checksum(0); - udp_packet.set_checksum(udp::ipv6_checksum( - &udp_packet.to_immutable(), - &new_source, - &new_destination, - )); + // Edit the packet's checksum + udp_packet.set_checksum(0); + udp_packet.set_checksum(udp::ipv6_checksum( + &udp_packet.to_immutable(), + &new_source, + &new_destination, + )); - // Return the translated packet - Ok(udp_packet_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_UDP, STATUS_TRANSLATED).inc(); + + // Return the translated packet + Ok(udp_packet_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_UDP, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } /// Re-calculates a UDP packet's checksum with a new IPv4 pseudo-header. @@ -38,26 +53,41 @@ pub fn recalculate_udp_checksum_ipv4( new_source: Ipv4Addr, new_destination: Ipv4Addr, ) -> Result> { - // Clone the packet so we can modify it - let mut udp_packet_buffer = udp_packet.to_vec(); + // This scope is used to collect packet drop metrics + { + // Clone the packet so we can modify it + let mut udp_packet_buffer = udp_packet.to_vec(); - // Get safe mutable access to the packet - let mut udp_packet = - MutableUdpPacket::new(&mut udp_packet_buffer).ok_or(Error::PacketTooShort { - expected: UdpPacket::minimum_packet_size(), - actual: udp_packet.len(), - })?; + // Get safe mutable access to the packet + let mut udp_packet = + MutableUdpPacket::new(&mut udp_packet_buffer).ok_or(Error::PacketTooShort { + expected: UdpPacket::minimum_packet_size(), + actual: udp_packet.len(), + })?; - // Edit the packet's checksum - udp_packet.set_checksum(0); - udp_packet.set_checksum(udp::ipv4_checksum( - &udp_packet.to_immutable(), - &new_source, - &new_destination, - )); + // Edit the packet's checksum + udp_packet.set_checksum(0); + udp_packet.set_checksum(udp::ipv4_checksum( + &udp_packet.to_immutable(), + &new_source, + &new_destination, + )); - // Return the translated packet - Ok(udp_packet_buffer) + // Track the translated packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_UDP, STATUS_TRANSLATED).inc(); + + // Return the translated packet + Ok(udp_packet_buffer) + } + .map_err(|error| { + // Track the dropped packet + #[cfg(feature = "metrics")] + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_UDP, STATUS_DROPPED).inc(); + + // Pass the error through + error + }) } #[cfg(test)] diff --git a/libs/protomask-metrics/Cargo.toml b/libs/protomask-metrics/Cargo.toml new file mode 100644 index 0000000..7a27df5 --- /dev/null +++ b/libs/protomask-metrics/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "protomask-metrics" +version = "0.1.0" +authors = ["Evan Pratten "] +edition = "2021" +description = "Internal metrics library used by protomask" +readme = "README.md" +homepage = "https://github.com/ewpratten/protomask/tree/master/libs/protomask-metrics" +documentation = "https://docs.rs/protomask-metrics" +repository = "https://github.com/ewpratten/protomask" +license = "GPL-3.0" +keywords = [] +categories = [] + +[dependencies] +log = "^0.4" +prometheus = "0.13.3" +lazy_static = "1.4.0" \ No newline at end of file diff --git a/libs/protomask-metrics/README.md b/libs/protomask-metrics/README.md new file mode 100644 index 0000000..cf6f4e4 --- /dev/null +++ b/libs/protomask-metrics/README.md @@ -0,0 +1 @@ +**`protomask-metrics` is exclusively for use in `protomask` and is not intended to be used on its own.** diff --git a/libs/protomask-metrics/src/lib.rs b/libs/protomask-metrics/src/lib.rs new file mode 100644 index 0000000..08ca4d4 --- /dev/null +++ b/libs/protomask-metrics/src/lib.rs @@ -0,0 +1,10 @@ +#![doc = include_str!("../README.md")] +#![deny(clippy::pedantic)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::missing_panics_doc)] + +pub mod metrics; + +#[macro_use] +pub mod macros; \ No newline at end of file diff --git a/libs/protomask-metrics/src/macros.rs b/libs/protomask-metrics/src/macros.rs new file mode 100644 index 0000000..4e98602 --- /dev/null +++ b/libs/protomask-metrics/src/macros.rs @@ -0,0 +1,10 @@ + +/// A short-hand way to access one of the metrics in `protomask_metrics::metrics` +#[macro_export] +macro_rules! metric { + // Accept and name and multiple labels + ($metric_name: ident, $($label_name: ident),+) => { + protomask_metrics::metrics::$metric_name.with_label_values(&[$(protomask_metrics::metrics::label_values::$label_name),+]) + }; + +} diff --git a/libs/protomask-metrics/src/metrics.rs b/libs/protomask-metrics/src/metrics.rs new file mode 100644 index 0000000..e7fd8c9 --- /dev/null +++ b/libs/protomask-metrics/src/metrics.rs @@ -0,0 +1,30 @@ +use lazy_static::lazy_static; + +pub mod label_values { + /// IPv4 protocol + pub const PROTOCOL_IPV4: &str = "ipv4"; + /// IPv6 protocol + pub const PROTOCOL_IPV6: &str = "ipv6"; + /// ICMP protocol + pub const PROTOCOL_ICMP: &str = "icmp"; + /// ICMPv6 protocol + pub const PROTOCOL_ICMPV6: &str = "icmpv6"; + /// TCP protocol + pub const PROTOCOL_TCP: &str = "tcp"; + /// UDP protocol + pub const PROTOCOL_UDP: &str = "udp"; + + /// Dropped status + pub const STATUS_DROPPED: &str = "dropped"; + /// Translated status + pub const STATUS_TRANSLATED: &str = "translated"; +} + +lazy_static! { + /// Counter for the number of packets processed + pub static ref PACKET_COUNTER: prometheus::IntCounterVec = prometheus::register_int_counter_vec!( + "protomask_packets", + "Number of packets processed", + &["protocol", "status"] + ).unwrap(); +} diff --git a/src/protomask.rs b/src/protomask.rs index cb42757..8d34c30 100644 --- a/src/protomask.rs +++ b/src/protomask.rs @@ -1,9 +1,15 @@ -use std::path::PathBuf; - use clap::Parser; use common::{logging::enable_logger, rfc6052::parse_network_specific_prefix}; -use ipnet::{Ipv4Net, Ipv6Net}; +use easy_tun::Tun; +use fast_nat::CrossProtocolNetworkAddressTable; +use interproto::protocols::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4}; +use ipnet::{IpNet, Ipv4Net, Ipv6Net}; use nix::unistd::Uid; +use std::{ + io::{BufRead, Read, Write}, + net::{Ipv4Addr, Ipv6Addr}, + path::PathBuf, +}; mod common; @@ -34,6 +40,15 @@ struct Args { verbose: bool, } +impl Args { + pub fn get_static_reservations( + &self, + ) -> Result, Box> { + log::warn!("Static reservations are not yet implemented"); + Ok(Vec::new()) + } +} + #[derive(clap::Args)] #[group(required = true, multiple = false)] struct PoolArgs { @@ -47,8 +62,22 @@ struct PoolArgs { } impl PoolArgs { - pub fn prefixes(&self) -> Result, std::io::Error> { - todo!() + /// Read all pool prefixes from the chosen source + pub fn prefixes(&self) -> Result, Box> { + match self.pool_prefixes.len() > 0 { + true => Ok(self.pool_prefixes.clone()), + false => { + let mut prefixes = Vec::new(); + let file = std::fs::File::open(self.pool_file.as_ref().unwrap())?; + let reader = std::io::BufReader::new(file); + for line in reader.lines() { + let line = line?; + let prefix = line.parse::()?; + prefixes.push(prefix); + } + Ok(prefixes) + } + } } } @@ -65,4 +94,78 @@ pub async fn main() { log::error!("This program must be run as root"); std::process::exit(1); } + + // Bring up a TUN interface + log::debug!("Creating new TUN interface"); + let mut tun = Tun::new(&args.interface).unwrap(); + log::debug!("Created TUN interface: {}", tun.name()); + + // Get the interface index + let rt_handle = rtnl::new_handle().unwrap(); + let tun_link_idx = rtnl::link::get_link_index(&rt_handle, tun.name()) + .await + .unwrap() + .unwrap(); + + // Bring the interface up + rtnl::link::link_up(&rt_handle, tun_link_idx).await.unwrap(); + + // Add a route for the translation prefix + log::debug!( + "Adding route for {} to {}", + args.translation_prefix, + tun.name() + ); + rtnl::route::route_add(IpNet::V6(args.translation_prefix), &rt_handle, tun_link_idx) + .await + .unwrap(); + + // Add a route for each NAT pool prefix + for pool_prefix in args.pool.prefixes().unwrap() { + log::debug!("Adding route for {} to {}", pool_prefix, tun.name()); + rtnl::route::route_add(IpNet::V4(pool_prefix), &rt_handle, tun_link_idx) + .await + .unwrap(); + } + + // Set up the address table + let mut addr_table = CrossProtocolNetworkAddressTable::default(); + for (v6_addr, v4_addr) in args.get_static_reservations().unwrap() { + addr_table.insert_indefinite(v4_addr, v6_addr); + } + + // Translate all incoming packets + log::info!("Translating packets on {}", tun.name()); + let mut buffer = vec![0u8; 1500]; + // loop { + // // Read a packet + // let len = tun.read(&mut buffer).unwrap(); + + // // Translate it based on the Layer 3 protocol number + // if let Some(output) = handle_packet( + // &buffer[..len], + // // IPv4 -> IPv6 + // |packet, source, dest| { + // // translate_ipv4_to_ipv6( + // // packet, + // // unsafe { embed_ipv4_addr_unchecked(*source, args.embed_prefix) }, + // // unsafe { embed_ipv4_addr_unchecked(*dest, args.embed_prefix) }, + // // ) + // todo!() + // }, + // // IPv6 -> IPv4 + // |packet, source, dest| { + + // // translate_ipv6_to_ipv4( + // // packet, + // // unsafe { extract_ipv4_addr_unchecked(*source, args.embed_prefix.prefix_len()) }, + // // unsafe { extract_ipv4_addr_unchecked(*dest, args.embed_prefix.prefix_len()) }, + // // ) + // todo!() + // }, + // ) { + // // Write the packet if we get one back from the handler functions + // tun.write_all(&output).unwrap(); + // } + // } } From 8330515f6990fb9393776000333498e292cbdbef Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 18:13:48 -0400 Subject: [PATCH 26/78] Add ICMP tracker --- libs/interproto/src/protocols/icmp/mod.rs | 24 +++++++++++++++++++---- libs/protomask-metrics/src/metrics.rs | 7 +++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/libs/interproto/src/protocols/icmp/mod.rs b/libs/interproto/src/protocols/icmp/mod.rs index 748bef2..aafc31c 100644 --- a/libs/interproto/src/protocols/icmp/mod.rs +++ b/libs/interproto/src/protocols/icmp/mod.rs @@ -28,6 +28,16 @@ pub fn translate_icmp_to_icmpv6( actual: icmp_packet.len(), })?; + // Track the incoming packet's type and code + #[cfg(feature = "metrics")] + protomask_metrics::metrics::ICMP_COUNTER + .with_label_values(&[ + protomask_metrics::metrics::label_values::PROTOCOL_ICMP, + &icmp_packet.get_icmp_type().0.to_string(), + &icmp_packet.get_icmp_code().0.to_string(), + ]) + .inc(); + // Translate the ICMP type and code to their ICMPv6 equivalents let (icmpv6_type, icmpv6_code) = type_code::translate_type_and_code_4_to_6( icmp_packet.get_icmp_type(), @@ -104,6 +114,16 @@ pub fn translate_icmpv6_to_icmp( actual: icmpv6_packet.len(), })?; + // Track the incoming packet's type and code + #[cfg(feature = "metrics")] + protomask_metrics::metrics::ICMP_COUNTER + .with_label_values(&[ + protomask_metrics::metrics::label_values::PROTOCOL_ICMPV6, + &icmpv6_packet.get_icmpv6_type().0.to_string(), + &icmpv6_packet.get_icmpv6_code().0.to_string(), + ]) + .inc(); + // Translate the ICMPv6 type and code to their ICMP equivalents let (icmp_type, icmp_code) = type_code::translate_type_and_code_6_to_4( icmpv6_packet.get_icmpv6_type(), @@ -144,10 +164,6 @@ pub fn translate_icmpv6_to_icmp( // Calculate the checksum icmp_packet.set_checksum(icmp::checksum(&icmp_packet.to_immutable())); - // Track the translated packet - #[cfg(feature = "metrics")] - protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_ICMPV6, STATUS_TRANSLATED).inc(); - // Return the translated packet Ok(output_buffer) } diff --git a/libs/protomask-metrics/src/metrics.rs b/libs/protomask-metrics/src/metrics.rs index e7fd8c9..381e1eb 100644 --- a/libs/protomask-metrics/src/metrics.rs +++ b/libs/protomask-metrics/src/metrics.rs @@ -27,4 +27,11 @@ lazy_static! { "Number of packets processed", &["protocol", "status"] ).unwrap(); + + /// Counter for the number of different types of ICMP packets received + pub static ref ICMP_COUNTER: prometheus::IntCounterVec = prometheus::register_int_counter_vec!( + "protomask_icmp_packets_recv", + "Number of ICMP packets received", + &["protocol", "icmp_type", "icmp_code"] + ).unwrap(); } From 37faf7d7a1802925fab58952d3ec376c3f28f6d9 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 20:03:07 -0400 Subject: [PATCH 27/78] Finish first pass of protomask bin --- Cargo.toml | 1 + libs/fast-nat/Cargo.toml | 1 + libs/fast-nat/src/bimap.rs | 5 ++ libs/fast-nat/src/cpnat.rs | 98 +++++++++++++++++++++++++++- libs/fast-nat/src/error.rs | 7 ++ libs/fast-nat/src/lib.rs | 3 +- libs/protomask-metrics/src/lib.rs | 2 +- libs/protomask-metrics/src/macros.rs | 1 - src/common/packet_handler.rs | 40 +++++++++--- src/protomask-clat.rs | 6 +- src/protomask.rs | 93 ++++++++++++++++---------- 11 files changed, 208 insertions(+), 49 deletions(-) create mode 100644 libs/fast-nat/src/error.rs diff --git a/Cargo.toml b/Cargo.toml index 819b552..cbb247d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,6 +79,7 @@ log = "0.4.19" fern = "0.6.2" ipnet = "2.8.0" nix = "0.26.2" +thiserror = "1.0.44" [package.metadata.deb] section = "network" diff --git a/libs/fast-nat/Cargo.toml b/libs/fast-nat/Cargo.toml index 00f4927..e6d1993 100644 --- a/libs/fast-nat/Cargo.toml +++ b/libs/fast-nat/Cargo.toml @@ -15,3 +15,4 @@ categories = [] [dependencies] log = "^0.4" rustc-hash = "1.1.0" +thiserror = "^1.0.44" \ No newline at end of file diff --git a/libs/fast-nat/src/bimap.rs b/libs/fast-nat/src/bimap.rs index 4f1688e..86dfed0 100644 --- a/libs/fast-nat/src/bimap.rs +++ b/libs/fast-nat/src/bimap.rs @@ -58,6 +58,11 @@ where self.left_to_right.remove(&left); } } + + /// Get the total number of mappings in the `BiHashMap` + pub fn len(&self) -> usize { + self.left_to_right.len() + } } impl Default for BiHashMap { diff --git a/libs/fast-nat/src/cpnat.rs b/libs/fast-nat/src/cpnat.rs index e467013..5121994 100644 --- a/libs/fast-nat/src/cpnat.rs +++ b/libs/fast-nat/src/cpnat.rs @@ -2,7 +2,7 @@ use std::time::Duration; use rustc_hash::FxHashMap; -use crate::{bimap::BiHashMap, timeout::MaybeTimeout}; +use crate::{bimap::BiHashMap, error::Error, timeout::MaybeTimeout}; /// A table of network address mappings across IPv4 and IPv6 #[derive(Debug)] @@ -87,6 +87,12 @@ impl CrossProtocolNetworkAddressTable { pub fn get_ipv4>(&self, ipv6: T) -> Option { self.addr_map.get_left(&ipv6.into()).copied() } + + /// Get the number of mappings in the table + #[must_use] + pub fn len(&self) -> usize { + self.addr_map.len() + } } impl Default for CrossProtocolNetworkAddressTable { @@ -97,3 +103,93 @@ impl Default for CrossProtocolNetworkAddressTable { } } } + +#[derive(Debug)] +pub struct CrossProtocolNetworkAddressTableWithIpv4Pool { + /// Internal table + table: CrossProtocolNetworkAddressTable, + /// Internal pool of IPv4 prefixes to assign new mappings from + pool: Vec<(u32, u32)>, + /// The timeout to use for new entries + timeout: Duration, + /// The pre-calculated maximum number of mappings that can be created + max_mappings: usize, +} + +impl CrossProtocolNetworkAddressTableWithIpv4Pool { + /// Construct a new Cross-protocol network address table with a given IPv4 pool + pub fn new + Clone>(pool: Vec<(T, T)>, timeout: Duration) -> Self { + Self { + table: CrossProtocolNetworkAddressTable::default(), + pool: pool + .iter() + .map(|(a, b)| (a.clone().into(), b.clone().into())) + .collect(), + timeout, + max_mappings: pool + .iter() + .map(|(_, netmask)| (*netmask).clone().into() as usize) + .map(|netmask| !netmask) + .sum(), + } + } + + /// Check if the pool contains an address + #[must_use] + pub fn contains>(&self, addr: T) -> bool { + let addr = addr.into(); + self.pool + .iter() + .any(|(network_addr, netmask)| (addr & netmask) == *network_addr) + } + + /// Insert a new static mapping + pub fn insert_static, T6: Into>( + &mut self, + ipv4: T4, + ipv6: T6, + ) -> Result<(), Error> { + let (ipv4, ipv6) = (ipv4.into(), ipv6.into()); + if !self.contains(ipv4) { + return Err(Error::InvalidIpv4Address(ipv4)); + } + self.table.insert_indefinite(ipv4, ipv6); + Ok(()) + } + + /// Gets the IPv4 address for a given IPv6 address or inserts a new mapping if one does not exist (if possible) + pub fn get_or_create_ipv4>(&mut self, ipv6: T) -> Result { + let ipv6 = ipv6.into(); + + // Return the known mapping if it exists + if let Some(ipv4) = self.table.get_ipv4(ipv6) { + return Ok(ipv4); + } + + // Otherwise, we first need to make sure there is actually room for a new mapping + if self.table.len() >= self.max_mappings { + return Err(Error::Ipv4PoolExhausted(self.max_mappings)); + } + + // Find the next available IPv4 address in the pool + let new_address = self + .pool + .iter() + .map(|(network_address, netmask)| (*network_address, *network_address | !netmask)) + .find(|(network_address, _)| self.table.get_ipv6(*network_address).is_none()) + .map(|(_, new_address)| new_address) + .ok_or(Error::Ipv4PoolExhausted(self.max_mappings))?; + + // Insert the new mapping + self.table.insert(new_address, ipv6, self.timeout); + + // Return the new address + Ok(new_address) + } + + /// Gets the IPv6 address for a given IPv4 address if it exists + #[must_use] + pub fn get_ipv6>(&self, ipv4: T) -> Option { + self.table.get_ipv6(ipv4) + } +} diff --git a/libs/fast-nat/src/error.rs b/libs/fast-nat/src/error.rs new file mode 100644 index 0000000..0b2e276 --- /dev/null +++ b/libs/fast-nat/src/error.rs @@ -0,0 +1,7 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Ipv4 address does not belong to the NAT pool: {0:02x}")] + InvalidIpv4Address(u32), + #[error("IPv4 pool exhausted. All {0} spots filled")] + Ipv4PoolExhausted(usize), +} diff --git a/libs/fast-nat/src/lib.rs b/libs/fast-nat/src/lib.rs index 5a0e3fd..f6319dc 100644 --- a/libs/fast-nat/src/lib.rs +++ b/libs/fast-nat/src/lib.rs @@ -6,8 +6,9 @@ mod bimap; mod cpnat; +pub mod error; mod nat; mod timeout; -pub use cpnat::CrossProtocolNetworkAddressTable; +pub use cpnat::{CrossProtocolNetworkAddressTable, CrossProtocolNetworkAddressTableWithIpv4Pool}; pub use nat::NetworkAddressTable; diff --git a/libs/protomask-metrics/src/lib.rs b/libs/protomask-metrics/src/lib.rs index 08ca4d4..8347e92 100644 --- a/libs/protomask-metrics/src/lib.rs +++ b/libs/protomask-metrics/src/lib.rs @@ -7,4 +7,4 @@ pub mod metrics; #[macro_use] -pub mod macros; \ No newline at end of file +pub mod macros; diff --git a/libs/protomask-metrics/src/macros.rs b/libs/protomask-metrics/src/macros.rs index 4e98602..7ee6c01 100644 --- a/libs/protomask-metrics/src/macros.rs +++ b/libs/protomask-metrics/src/macros.rs @@ -1,4 +1,3 @@ - /// A short-hand way to access one of the metrics in `protomask_metrics::metrics` #[macro_export] macro_rules! metric { diff --git a/src/common/packet_handler.rs b/src/common/packet_handler.rs index 351cf35..eeb2dfc 100644 --- a/src/common/packet_handler.rs +++ b/src/common/packet_handler.rs @@ -1,13 +1,22 @@ use std::net::{Ipv4Addr, Ipv6Addr}; +#[derive(Debug, thiserror::Error)] +pub enum PacketHandlingError { + #[error(transparent)] + InterprotoError(#[from] interproto::error::Error), + #[error(transparent)] + FastNatError(#[from] fast_nat::error::Error), +} + +/// Handles checking the version number of an IP packet and calling the correct handler with needed data pub fn handle_packet( packet: &[u8], - ipv4_handler: Ipv4Handler, - ipv6_handler: Ipv6Handler, + mut ipv4_handler: Ipv4Handler, + mut ipv6_handler: Ipv6Handler, ) -> Option> where - Ipv4Handler: Fn(&[u8], &Ipv4Addr, &Ipv4Addr) -> Result, interproto::error::Error>, - Ipv6Handler: Fn(&[u8], &Ipv6Addr, &Ipv6Addr) -> Result, interproto::error::Error>, + Ipv4Handler: FnMut(&[u8], &Ipv4Addr, &Ipv4Addr) -> Result>, PacketHandlingError>, + Ipv6Handler: FnMut(&[u8], &Ipv6Addr, &Ipv6Addr) -> Result>, PacketHandlingError>, { // If the packet is empty, return nothing if packet.is_empty() { @@ -52,10 +61,13 @@ where // The response from the handler may or may not be a warn-able error match handler_response { // If we get data, return it - Ok(data) => Some(data), + Ok(data) => data, // If we get an error, handle it and return None Err(error) => match error { - interproto::error::Error::PacketTooShort { expected, actual } => { + PacketHandlingError::InterprotoError(interproto::error::Error::PacketTooShort { + expected, + actual, + }) => { log::warn!( "Got packet with length {} when expecting at least {} bytes", actual, @@ -63,17 +75,29 @@ where ); None } - interproto::error::Error::UnsupportedIcmpType(icmp_type) => { + PacketHandlingError::InterprotoError( + interproto::error::Error::UnsupportedIcmpType(icmp_type), + ) => { log::warn!("Got a packet with an unsupported ICMP type: {}", icmp_type); None } - interproto::error::Error::UnsupportedIcmpv6Type(icmpv6_type) => { + PacketHandlingError::InterprotoError( + interproto::error::Error::UnsupportedIcmpv6Type(icmpv6_type), + ) => { log::warn!( "Got a packet with an unsupported ICMPv6 type: {}", icmpv6_type ); None } + PacketHandlingError::FastNatError(fast_nat::error::Error::Ipv4PoolExhausted(size)) => { + log::warn!("IPv4 pool exhausted with {} mappings", size); + None + } + PacketHandlingError::FastNatError(fast_nat::error::Error::InvalidIpv4Address(addr)) => { + log::warn!("Invalid IPv4 address: {}", addr); + None + } }, } } diff --git a/src/protomask-clat.rs b/src/protomask-clat.rs index cab3299..4f8c117 100644 --- a/src/protomask-clat.rs +++ b/src/protomask-clat.rs @@ -105,19 +105,21 @@ pub async fn main() { &buffer[..len], // IPv4 -> IPv6 |packet, source, dest| { - translate_ipv4_to_ipv6( + Ok(translate_ipv4_to_ipv6( packet, unsafe { embed_ipv4_addr_unchecked(*source, args.embed_prefix) }, unsafe { embed_ipv4_addr_unchecked(*dest, args.embed_prefix) }, ) + .map(|output| Some(output))?) }, // IPv6 -> IPv4 |packet, source, dest| { - translate_ipv6_to_ipv4( + Ok(translate_ipv6_to_ipv4( packet, unsafe { extract_ipv4_addr_unchecked(*source, args.embed_prefix.prefix_len()) }, unsafe { extract_ipv4_addr_unchecked(*dest, args.embed_prefix.prefix_len()) }, ) + .map(|output| Some(output))?) }, ) { // Write the packet if we get one back from the handler functions diff --git a/src/protomask.rs b/src/protomask.rs index 8d34c30..f0e6a86 100644 --- a/src/protomask.rs +++ b/src/protomask.rs @@ -1,16 +1,21 @@ use clap::Parser; use common::{logging::enable_logger, rfc6052::parse_network_specific_prefix}; use easy_tun::Tun; -use fast_nat::CrossProtocolNetworkAddressTable; +use fast_nat::CrossProtocolNetworkAddressTableWithIpv4Pool; use interproto::protocols::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4}; use ipnet::{IpNet, Ipv4Net, Ipv6Net}; use nix::unistd::Uid; +use rfc6052::{embed_ipv4_addr_unchecked, extract_ipv4_addr_unchecked}; use std::{ + cell::RefCell, io::{BufRead, Read, Write}, net::{Ipv4Addr, Ipv6Addr}, path::PathBuf, + time::Duration, }; +use crate::common::packet_handler::handle_packet; + mod common; #[derive(Parser)] @@ -121,51 +126,69 @@ pub async fn main() { .unwrap(); // Add a route for each NAT pool prefix - for pool_prefix in args.pool.prefixes().unwrap() { + let pool_prefixes = args.pool.prefixes().unwrap(); + for pool_prefix in &pool_prefixes { log::debug!("Adding route for {} to {}", pool_prefix, tun.name()); - rtnl::route::route_add(IpNet::V4(pool_prefix), &rt_handle, tun_link_idx) + rtnl::route::route_add(IpNet::V4(*pool_prefix), &rt_handle, tun_link_idx) .await .unwrap(); } // Set up the address table - let mut addr_table = CrossProtocolNetworkAddressTable::default(); + let mut addr_table = RefCell::new(CrossProtocolNetworkAddressTableWithIpv4Pool::new( + pool_prefixes + .iter() + .map(|prefix| (u32::from(prefix.addr()), prefix.prefix_len() as u32)) + .collect(), + Duration::from_secs(args.reservation_timeout), + )); for (v6_addr, v4_addr) in args.get_static_reservations().unwrap() { - addr_table.insert_indefinite(v4_addr, v6_addr); + addr_table + .get_mut() + .insert_static(v4_addr, v6_addr) + .unwrap(); } // Translate all incoming packets log::info!("Translating packets on {}", tun.name()); let mut buffer = vec![0u8; 1500]; - // loop { - // // Read a packet - // let len = tun.read(&mut buffer).unwrap(); + loop { + // Read a packet + let len = tun.read(&mut buffer).unwrap(); - // // Translate it based on the Layer 3 protocol number - // if let Some(output) = handle_packet( - // &buffer[..len], - // // IPv4 -> IPv6 - // |packet, source, dest| { - // // translate_ipv4_to_ipv6( - // // packet, - // // unsafe { embed_ipv4_addr_unchecked(*source, args.embed_prefix) }, - // // unsafe { embed_ipv4_addr_unchecked(*dest, args.embed_prefix) }, - // // ) - // todo!() - // }, - // // IPv6 -> IPv4 - // |packet, source, dest| { - - // // translate_ipv6_to_ipv4( - // // packet, - // // unsafe { extract_ipv4_addr_unchecked(*source, args.embed_prefix.prefix_len()) }, - // // unsafe { extract_ipv4_addr_unchecked(*dest, args.embed_prefix.prefix_len()) }, - // // ) - // todo!() - // }, - // ) { - // // Write the packet if we get one back from the handler functions - // tun.write_all(&output).unwrap(); - // } - // } + // Translate it based on the Layer 3 protocol number + if let Some(output) = handle_packet( + &buffer[..len], + // IPv4 -> IPv6 + |packet, source, dest| match addr_table.borrow().get_ipv6(*dest) { + Some(new_destination) => Ok(translate_ipv4_to_ipv6( + packet, + unsafe { embed_ipv4_addr_unchecked(*source, args.translation_prefix) }, + new_destination.into(), + ) + .map(|output| Some(output))?), + None => { + protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_IPV4, STATUS_DROPPED); + Ok(None) + } + }, + // IPv6 -> IPv4 + |packet, source, dest| { + Ok(translate_ipv6_to_ipv4( + packet, + addr_table + .borrow_mut() + .get_or_create_ipv4(source.clone())? + .into(), + unsafe { + extract_ipv4_addr_unchecked(*dest, args.translation_prefix.prefix_len()) + }, + ) + .map(|output| Some(output))?) + }, + ) { + // Write the packet if we get one back from the handler functions + tun.write_all(&output).unwrap(); + } + } } From 234aa922d0e7e363e0e1f55ab777aa3680e2fdd6 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 20:07:13 -0400 Subject: [PATCH 28/78] Appease clippy --- libs/fast-nat/src/bimap.rs | 5 +++++ libs/fast-nat/src/cpnat.rs | 9 ++++++++- libs/protomask-metrics/src/lib.rs | 1 + src/protomask-clat.rs | 4 ++-- src/protomask.rs | 14 ++++++-------- 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/libs/fast-nat/src/bimap.rs b/libs/fast-nat/src/bimap.rs index 86dfed0..e2f5f11 100644 --- a/libs/fast-nat/src/bimap.rs +++ b/libs/fast-nat/src/bimap.rs @@ -63,6 +63,11 @@ where pub fn len(&self) -> usize { self.left_to_right.len() } + + /// Check if the `BiHashMap` is empty + pub fn is_empty(&self) -> bool { + self.left_to_right.is_empty() + } } impl Default for BiHashMap { diff --git a/libs/fast-nat/src/cpnat.rs b/libs/fast-nat/src/cpnat.rs index 5121994..52b65b4 100644 --- a/libs/fast-nat/src/cpnat.rs +++ b/libs/fast-nat/src/cpnat.rs @@ -93,6 +93,12 @@ impl CrossProtocolNetworkAddressTable { pub fn len(&self) -> usize { self.addr_map.len() } + + /// Check if the table is empty + #[must_use] + pub fn is_empty(&self) -> bool { + self.addr_map.is_empty() + } } impl Default for CrossProtocolNetworkAddressTable { @@ -118,7 +124,8 @@ pub struct CrossProtocolNetworkAddressTableWithIpv4Pool { impl CrossProtocolNetworkAddressTableWithIpv4Pool { /// Construct a new Cross-protocol network address table with a given IPv4 pool - pub fn new + Clone>(pool: Vec<(T, T)>, timeout: Duration) -> Self { + #[must_use] + pub fn new + Clone>(pool: &[(T, T)], timeout: Duration) -> Self { Self { table: CrossProtocolNetworkAddressTable::default(), pool: pool diff --git a/libs/protomask-metrics/src/lib.rs b/libs/protomask-metrics/src/lib.rs index 8347e92..e79c974 100644 --- a/libs/protomask-metrics/src/lib.rs +++ b/libs/protomask-metrics/src/lib.rs @@ -3,6 +3,7 @@ #![allow(clippy::module_name_repetitions)] #![allow(clippy::missing_errors_doc)] #![allow(clippy::missing_panics_doc)] +#![allow(clippy::doc_markdown)] pub mod metrics; diff --git a/src/protomask-clat.rs b/src/protomask-clat.rs index 4f8c117..9e3c3dd 100644 --- a/src/protomask-clat.rs +++ b/src/protomask-clat.rs @@ -110,7 +110,7 @@ pub async fn main() { unsafe { embed_ipv4_addr_unchecked(*source, args.embed_prefix) }, unsafe { embed_ipv4_addr_unchecked(*dest, args.embed_prefix) }, ) - .map(|output| Some(output))?) + .map(Some)?) }, // IPv6 -> IPv4 |packet, source, dest| { @@ -119,7 +119,7 @@ pub async fn main() { unsafe { extract_ipv4_addr_unchecked(*source, args.embed_prefix.prefix_len()) }, unsafe { extract_ipv4_addr_unchecked(*dest, args.embed_prefix.prefix_len()) }, ) - .map(|output| Some(output))?) + .map(Some)?) }, ) { // Write the packet if we get one back from the handler functions diff --git a/src/protomask.rs b/src/protomask.rs index f0e6a86..308e7a1 100644 --- a/src/protomask.rs +++ b/src/protomask.rs @@ -69,7 +69,7 @@ struct PoolArgs { impl PoolArgs { /// Read all pool prefixes from the chosen source pub fn prefixes(&self) -> Result, Box> { - match self.pool_prefixes.len() > 0 { + match !self.pool_prefixes.is_empty() { true => Ok(self.pool_prefixes.clone()), false => { let mut prefixes = Vec::new(); @@ -139,7 +139,8 @@ pub async fn main() { pool_prefixes .iter() .map(|prefix| (u32::from(prefix.addr()), prefix.prefix_len() as u32)) - .collect(), + .collect::>() + .as_slice(), Duration::from_secs(args.reservation_timeout), )); for (v6_addr, v4_addr) in args.get_static_reservations().unwrap() { @@ -166,7 +167,7 @@ pub async fn main() { unsafe { embed_ipv4_addr_unchecked(*source, args.translation_prefix) }, new_destination.into(), ) - .map(|output| Some(output))?), + .map(Some)?), None => { protomask_metrics::metric!(PACKET_COUNTER, PROTOCOL_IPV4, STATUS_DROPPED); Ok(None) @@ -176,15 +177,12 @@ pub async fn main() { |packet, source, dest| { Ok(translate_ipv6_to_ipv4( packet, - addr_table - .borrow_mut() - .get_or_create_ipv4(source.clone())? - .into(), + addr_table.borrow_mut().get_or_create_ipv4(*source)?.into(), unsafe { extract_ipv4_addr_unchecked(*dest, args.translation_prefix.prefix_len()) }, ) - .map(|output| Some(output))?) + .map(Some)?) }, ) { // Write the packet if we get one back from the handler functions From 0b24c119976072a0b189518dc8e722ae0c2f81e7 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 20:08:23 -0400 Subject: [PATCH 29/78] Remove old config file --- protomask.toml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 protomask.toml diff --git a/protomask.toml b/protomask.toml deleted file mode 100644 index 2092731..0000000 --- a/protomask.toml +++ /dev/null @@ -1,12 +0,0 @@ -# The NAT64 prefix to route to protomask -Nat64Prefix = "64:ff9b::/96" -# Setting this will enable prometheus metrics -Prometheus = "[::1]:8080" # Optional, defaults to disabled - -[Pool] -# All prefixes in the pool -Prefixes = ["192.0.2.0/24"] -# The maximum duration an ipv4 address from the pool will be reserved for after becoming idle -MaxIdleDuration = 7200 # Optional, seconds. Defaults to 7200 (2 hours) -# Permanent address mappings -Static = [{ v4 = "192.0.2.2", v6 = "2001:db8:1::2" }] From a0b4360557164f4c82e9a78f957de491faf66adc Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 20:16:33 -0400 Subject: [PATCH 30/78] Attempt to fix strip issue --- .github/workflows/build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed36fe8..a1fa0b3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,6 +33,11 @@ jobs: toolchain: ${{ matrix.rust_channel }} override: true + # Builds targeting aarch64 require `aarch64-linux-gnu-strip` + - name: Install aarch64-linux-gnu-strip + if: matrix.target == 'aarch64-unknown-linux-musl' + run: sudo apt-get install -y binutils-aarch64-linux-gnu + - name: Install cargo-deb uses: actions-rs/cargo@v1 with: From 213a49db14273898c29eb79c087a273f16d4f243 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 20:22:18 -0400 Subject: [PATCH 31/78] Add more info to workflow runs --- .github/workflows/build.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a1fa0b3..5520f83 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,7 +36,7 @@ jobs: # Builds targeting aarch64 require `aarch64-linux-gnu-strip` - name: Install aarch64-linux-gnu-strip if: matrix.target == 'aarch64-unknown-linux-musl' - run: sudo apt-get install -y binutils-aarch64-linux-gnu + run: sudo apt-get update && sudo apt-get install -y binutils-aarch64-linux-gnu - name: Install cargo-deb uses: actions-rs/cargo@v1 @@ -70,3 +70,22 @@ jobs: with: name: debian-packages path: target/${{ matrix.target }}/debian/*.deb + + - name: Determine binary sizes + id: get-bin-size-info + run: | + body="$(du -h target/${{ matrix.target }}/release/protomask{,-clat,-6over4} | sort -hr)" + delimiter="$(openssl rand -hex 8)" + echo "body<<$delimiter" >> $GITHUB_OUTPUT + echo "$body" >> $GITHUB_OUTPUT + echo "$delimiter" >> $GITHUB_OUTPUT + + - name: Add binary size info to commit + uses: peter-evans/commit-comment@v2 + with: + body: | + ## Binary sizes for `${{ matrix.target }}` + + ``` + ${{ steps.get-bin-size-info.outputs.body }} + ``` From 025ba7e8d1871ecba9ae5b10eace3ed03b05cd0a Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 20:30:13 -0400 Subject: [PATCH 32/78] Fix deb build issues --- .cargo/config.toml | 2 ++ Cargo.toml | 13 +++++++++---- debian/service | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..b23105b --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[registries.crates-io] +protocol = "sparse" \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index cbb247d..d14347b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,9 +90,14 @@ assets = [ "755", ], [ - "./protomask.toml", - "/etc/protomask.toml", - "644", + "target/release/protomask-clat", + "/usr/local/bin/protomask-clat", + "755", + ], + [ + "target/release/protomask-6over4", + "/usr/local/bin/protomask-6over4", + "755", ], [ "README.md", @@ -100,7 +105,7 @@ assets = [ "644", ], ] -conf-files = ["/etc/protomask.toml"] +conf-files = [] depends = [] maintainer-scripts = "./debian/" systemd-units = { enable = false } diff --git a/debian/service b/debian/service index 877a10a..2b5db60 100644 --- a/debian/service +++ b/debian/service @@ -3,7 +3,7 @@ Description = Protomask After = network.target [Service] -ExecStart = /usr/local/bin/protomask /etc/protomask.toml +ExecStart = /usr/local/bin/protomask --pool-file /etc/protomask/pool.txt [Install] WantedBy=multi-user.target \ No newline at end of file From 93b110b3954bf87303bafdcdb06ba6623135543e Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 20:49:28 -0400 Subject: [PATCH 33/78] Add information about the build channel --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5520f83..5e54a4e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,6 +85,7 @@ jobs: with: body: | ## Binary sizes for `${{ matrix.target }}` + **Channel:** `${{ matrix.rust_channel }}` ``` ${{ steps.get-bin-size-info.outputs.body }} From 74bd4605809be1843e2c3c33397bb863d35c4fe8 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 22:59:03 -0400 Subject: [PATCH 34/78] Basic website content --- .vscode/tasks.json | 18 ++++++++++++- docs/website/.gitignore | 1 + docs/website/config.toml | 8 ++++++ docs/website/content/_index.md | 33 +++++++++++++++++++++++ docs/website/sass/main.scss | 7 +++++ docs/website/templates/base.html | 45 +++++++++++++++++++++++++++++++ docs/website/templates/index.html | 19 +++++++++++++ 7 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 docs/website/.gitignore create mode 100644 docs/website/config.toml create mode 100644 docs/website/content/_index.md create mode 100644 docs/website/sass/main.scss create mode 100644 docs/website/templates/base.html create mode 100644 docs/website/templates/index.html diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 2270824..26b8f05 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -4,6 +4,9 @@ { "type": "cargo", "command": "build", + "args": [ + "--workspace" + ], "problemMatcher": [ "$rustc", "$rust-panic" @@ -14,12 +17,25 @@ { "type": "cargo", "command": "test", + "args": [ + "--workspace" + ], "problemMatcher": [ "$rustc", "$rust-panic" ], "group": "test", "label": "rust: cargo test" - } + }, + { + "type": "shell", + "command": "zola", + "args": [ + "-r", + "docs/website", + "serve" + ], + "label": "zola: serve website" + } ] } \ No newline at end of file diff --git a/docs/website/.gitignore b/docs/website/.gitignore new file mode 100644 index 0000000..85a1daf --- /dev/null +++ b/docs/website/.gitignore @@ -0,0 +1 @@ +/public \ No newline at end of file diff --git a/docs/website/config.toml b/docs/website/config.toml new file mode 100644 index 0000000..e92be9a --- /dev/null +++ b/docs/website/config.toml @@ -0,0 +1,8 @@ +base_url = "https://protomask.ewpratten.com" +compile_sass = true +build_search_index = true + +[markdown] +highlight_code = true + +[extra] diff --git a/docs/website/content/_index.md b/docs/website/content/_index.md new file mode 100644 index 0000000..d7d76b9 --- /dev/null +++ b/docs/website/content/_index.md @@ -0,0 +1,33 @@ +--- +title: Protomask +--- + +## What is protomask? + +Protomask is a user space [NAT64](https://en.wikipedia.org/wiki/NAT64) implementation geared towards networks that need fast and reliable inter-protocol packet translation. Behind the scenes, protomask uses the [Universal TUN/TAP Device Driver](https://docs.kernel.org/networking/tuntap.html) to translate incoming packets from IPv4 to IPv6 and vice-versa. + +## The protomask tool suite + +To accomplish the various translation needs of an IPv6-only or dual-stack ISP, the protomask tool suite includes a few tools: + +- **`protomask`**: The main NAT64 daemon + - Translates IPv6 packets using *RFC6052 IPv4-Embedded IPv6 Addressing* to native IPv4 traffic + - Can handle high volumes of traffic from multiple clients simultaneously +- **`protomask-clat`**: A Customer-side transLATor (CLAT) implementation + - Intended to be deployed at the customer edge to pass IPv4 traffic over an IPv6-only ISP's network + +Every tool in the protomask suite is easy to deploy and configure, plus supports optionally exposing Prometheus metrics for remote monitoring. + +## The protomask library suite + +The development of protomask necessitated the creation of a few specialized software libraries. Since the technology developed for protomask is useful outside of the scope of this project, these libraries are also available for general use: + +- **`easy-tun`**: A minimal TUN interface library +- **`fast-nat`**: A library designed for highly efficient mapping and lookup of IP address pairs +- **`interproto`**: The heart of protomask, a library for translating many types of packets between layer 3 protocols +- **`rfc6052`**: A Rust implementation of RFC6052 +- **`rtnl`**: A high-level wrapper around `rtnetlink` + +## Want to learn more? + +Check out the [documentation](/book) for a deeper dive into protomask, or head over to the [GitHub repository](https://github.com/ewpratten/protomask) to see the source code. diff --git a/docs/website/sass/main.scss b/docs/website/sass/main.scss new file mode 100644 index 0000000..717d4cc --- /dev/null +++ b/docs/website/sass/main.scss @@ -0,0 +1,7 @@ +.container { + max-width: 1000px !important; + + h2 { + font-weight: bold; + } +} diff --git a/docs/website/templates/base.html b/docs/website/templates/base.html new file mode 100644 index 0000000..798ffb3 --- /dev/null +++ b/docs/website/templates/base.html @@ -0,0 +1,45 @@ + + + + + + + {% block title %}{% endblock title %} + + + + + + + + + + + + {% block content %}{% endblock content %} + +
+
+

Protomask is a project by Evan Pratten

+ + + + + + \ No newline at end of file diff --git a/docs/website/templates/index.html b/docs/website/templates/index.html new file mode 100644 index 0000000..2b92d6d --- /dev/null +++ b/docs/website/templates/index.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %}{{section.title}}{% endblock title %} + +{% block content %} +
+
+

Protomask

+ The ultimate tool suite for transitioning to IPv6 +
+
+ +
+ +
+ {{section.content|safe}} +
+ +{% endblock content %} \ No newline at end of file From 0eab85aebbe6ca0c81384d1c5ad848d16b2bcd3f Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:18:12 -0400 Subject: [PATCH 35/78] Working on the book --- book.toml | 4 +++- docs/book/SUMMARY.md | 9 ++++++++- docs/book/binaries/protomask-clat.md | 1 + docs/book/binaries/protomask.md | 1 + docs/book/chapter_1.md | 1 - docs/book/introduction.md | 11 +++++++++++ docs/book/libraries.md | 11 +++++++++++ 7 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 docs/book/binaries/protomask-clat.md create mode 100644 docs/book/binaries/protomask.md delete mode 100644 docs/book/chapter_1.md create mode 100644 docs/book/introduction.md create mode 100644 docs/book/libraries.md diff --git a/book.toml b/book.toml index 9c648e5..7e3db90 100644 --- a/book.toml +++ b/book.toml @@ -13,4 +13,6 @@ build-dir = "target/book" create-missing = true [output.html] -mathjax-support = true \ No newline at end of file +mathjax-support = true +git-repository-url = "https://github.com/ewpratten/protomask" +site-url = "/book/" \ No newline at end of file diff --git a/docs/book/SUMMARY.md b/docs/book/SUMMARY.md index 7390c82..80f7df5 100644 --- a/docs/book/SUMMARY.md +++ b/docs/book/SUMMARY.md @@ -1,3 +1,10 @@ # Summary -- [Chapter 1](./chapter_1.md) +- [Introduction](./introduction.md) +- [Libraries](./libraries.md) + +# User Guides + +- [Using `protomask`](./binaries/protomask.md) +- [Using `protomask-clat`](./binaries/protomask-clat.md) + diff --git a/docs/book/binaries/protomask-clat.md b/docs/book/binaries/protomask-clat.md new file mode 100644 index 0000000..85a0123 --- /dev/null +++ b/docs/book/binaries/protomask-clat.md @@ -0,0 +1 @@ +# `protomask-clat` User Guide diff --git a/docs/book/binaries/protomask.md b/docs/book/binaries/protomask.md new file mode 100644 index 0000000..34749b4 --- /dev/null +++ b/docs/book/binaries/protomask.md @@ -0,0 +1 @@ +# `protomask` User Guide diff --git a/docs/book/chapter_1.md b/docs/book/chapter_1.md deleted file mode 100644 index b743fda..0000000 --- a/docs/book/chapter_1.md +++ /dev/null @@ -1 +0,0 @@ -# Chapter 1 diff --git a/docs/book/introduction.md b/docs/book/introduction.md new file mode 100644 index 0000000..c6820ff --- /dev/null +++ b/docs/book/introduction.md @@ -0,0 +1,11 @@ +# Introduction + +Protomask is a user space [NAT64](https://en.wikipedia.org/wiki/NAT64) implementation geared towards networks that need fast and reliable inter-protocol packet translation. Behind the scenes, protomask uses the [Universal TUN/TAP Device Driver](https://docs.kernel.org/networking/tuntap.html) to translate incoming packets from IPv4 to IPv6 and vice-versa. + +For an overview of the project, see the [website](https://protomask.ewpratten.com) or [GitHub repository](https://github.com/ewpratten/protomask). + +## Table of Contents + +- [Protomask library suite documentation](./libraries.html) +- [Using `protomask`](./binaries/protomask.html) +- [Using `protomask-clat`](./binaries/protomask-clat.html) diff --git a/docs/book/libraries.md b/docs/book/libraries.md new file mode 100644 index 0000000..fa697ca --- /dev/null +++ b/docs/book/libraries.md @@ -0,0 +1,11 @@ +# Libraries + +The protomask library suite is a collection of libraries that were originally developed for use in the protomask tool suite, but are also quite useful outside of the scope of this project. + +These libraries are thoroughly documented in their respective docs.rs crate pages: + +- [`easy-tun`: A minimal TUN interface library](https://docs.rs/easy-tun) +- [`fast-nat`: A library designed for highly efficient mapping and lookup of IP address pairs](https://docs.rs/fast-nat) +- [`interproto`: A library for translating packets between protocols](https://docs.rs/interproto) +- [`rfc6052`: A Rust implementation of RFC6052](https://docs.rs/rfc6052) +- [`rtnl`: A high-level wrapper around `rtnetlink`](https://docs.rs/rtnl) \ No newline at end of file From 1632f8f111dafc50cebd3964cbac22886e4a7daf Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:27:01 -0400 Subject: [PATCH 36/78] Add docs CI --- .github/workflows/docs.yml | 31 +++++++++++++++++++++++++++++++ .vscode/settings.json | 8 +++++++- CODEOWNERS | 1 + 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docs.yml create mode 100644 CODEOWNERS diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..e9b3395 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,31 @@ +name: Documentation + +on: + pull_request: + push: + paths: + - ".github/workflows/docs.yml" + - "docs/**" + - "book.toml" + +jobs: + build: + name: Build Docs + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Setup mdBook + uses: peaceiris/actions-mdbook@v1 + with: + mdbook-version: "latest" + + - name: Build Book + run: mdbook build + + - name: Build Website + uses: shalzz/zola-deploy-action@v0.17.2 + env: + BUILD_DIR: ./docs/website + BUILD_ONLY: true diff --git a/.vscode/settings.json b/.vscode/settings.json index 17b3803..95c1b16 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,5 +5,11 @@ "pnet", "Protomask", "rtnetlink" - ] + ], + "[yaml]": { + "editor.insertSpaces": true, + "editor.tabSize": 2, + "editor.autoIndent": "advanced", + "diffEditor.ignoreTrimWhitespace": false + } } \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..546a104 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @ewpratten \ No newline at end of file From 52d06a1b89de136b66f27b3e6d06c3f1e1332385 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:28:48 -0400 Subject: [PATCH 37/78] Combine book and website --- .github/workflows/docs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e9b3395..89433c7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -29,3 +29,6 @@ jobs: env: BUILD_DIR: ./docs/website BUILD_ONLY: true + + - name: Combine Book and Website + run: mkdir -p /tmp/combined && cp -rv docs/website/* /tmp/combined && cp -rv book/ /tmp/combined/book From e856e355ea0b076e106c80ad8c5acc678e1342ab Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:33:35 -0400 Subject: [PATCH 38/78] test deploy --- .github/workflows/docs.yml | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 89433c7..72e27e5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,15 +4,19 @@ on: pull_request: push: paths: - - ".github/workflows/docs.yml" - - "docs/**" - - "book.toml" + - ".github/workflows/docs.yml" + - "docs/**" + - "book.toml" jobs: build: name: Build Docs runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: - uses: actions/checkout@v3 @@ -31,4 +35,14 @@ jobs: BUILD_ONLY: true - name: Combine Book and Website - run: mkdir -p /tmp/combined && cp -rv docs/website/* /tmp/combined && cp -rv book/ /tmp/combined/book + run: mkdir -p /tmp/combined && cp -rv docs/website/* /tmp/combined && cp -rv target/book/ /tmp/combined/book + + - name: Upload artifact + uses: actions/upload-pages-artifact@v2 + with: + path: /tmp/combined + + - name: Deploy + uses: actions/deploy-pages@v2 + id: deployment + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/ewpratten/reorg2' }} From bb3e2c2a708ae75f5d125fbd1e1aa7e4a75b092c Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:35:10 -0400 Subject: [PATCH 39/78] Add CNAME and perms --- .github/workflows/docs.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 72e27e5..08c52d9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,6 +13,10 @@ jobs: name: Build Docs runs-on: ubuntu-latest + permissions: + pages: write + id-token: write + environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} @@ -37,6 +41,9 @@ jobs: - name: Combine Book and Website run: mkdir -p /tmp/combined && cp -rv docs/website/* /tmp/combined && cp -rv target/book/ /tmp/combined/book + - name: Write CNAME file + run: echo "protomask.ewpratten.com" > /tmp/combined/CNAME + - name: Upload artifact uses: actions/upload-pages-artifact@v2 with: From 5d1cff98b96d32a0422f9d8b8d79c96a6be13e04 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:45:26 -0400 Subject: [PATCH 40/78] wip readme --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9875b37..39be8fd 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,21 @@ # protomask -[![Crates.io](https://img.shields.io/crates/v/protomask)](https://crates.io/crates/protomask) -[![Build](https://github.com/Ewpratten/protomask/actions/workflows/build.yml/badge.svg)](https://github.com/Ewpratten/protomask/actions/workflows/build.yml) +[![GitHub release](https://img.shields.io/github/v/release/ewpratten/protomask)](https://github.com/ewpratten/protomask/releases/latest) +[![Build](https://github.com/Ewpratten/protomask/actions/workflows/build.yml/badge.svg)](https://github.com/ewpratten/protomask/actions/workflows/build.yml) [![Audit](https://github.com/ewpratten/protomask/actions/workflows/audit.yml/badge.svg)](https://github.com/ewpratten/protomask/actions/workflows/audit.yml) **A user space [NAT64](https://en.wikipedia.org/wiki/NAT64) implementation.** -*This section is WIP* +This repository contains: + +- `protomask`: The main NAT64 daemon +- `protomask-clat`: A Customer-side transLATor (CLAT) implementation +- `easy-tun`: A minimal TUN interface library +- `fast-nat`: A library designed for highly efficient mapping and lookup of IP address pairs +- `interproto`: A library for translating packets between protocols +- `rfc6052`: A Rust implementation of RFC6052 +- `rtnl`: A high-level wrapper around `rtnetlink` + +For user-oriented documentation, see the [protomask website](https://protomask.ewpratten.com). ## Installation From c4862a224e91ccacbc1dac34bdea7a2779b73dda Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:47:57 -0400 Subject: [PATCH 41/78] Add badges --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 39be8fd..5fc57fc 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,19 @@ This repository contains: - `protomask`: The main NAT64 daemon + - [![Crates.io](https://img.shields.io/crates/v/protomask)](https://crates.io/crates/protomask) - `protomask-clat`: A Customer-side transLATor (CLAT) implementation + - [![Crates.io](https://img.shields.io/crates/v/protomask)](https://crates.io/crates/protomask) - `easy-tun`: A minimal TUN interface library + - [![Crates.io](https://img.shields.io/crates/v/easy-tun)](https://crates.io/crates/easy-tun) [![Docs.rs](https://docs.rs/easy-tun/badge.svg)](https://docs.rs/easy-tun) - `fast-nat`: A library designed for highly efficient mapping and lookup of IP address pairs + - [![Crates.io](https://img.shields.io/crates/v/fast-nat)](https://crates.io/crates/fast-nat) [![Docs.rs](https://docs.rs/fast-nat/badge.svg)](https://docs.rs/fast-nat) - `interproto`: A library for translating packets between protocols + - [![Crates.io](https://img.shields.io/crates/v/interproto)](https://crates.io/crates/interproto) [![Docs.rs](https://docs.rs/interproto/badge.svg)](https://docs.rs/interproto) - `rfc6052`: A Rust implementation of RFC6052 + - [![Crates.io](https://img.shields.io/crates/v/rfc6052)](https://crates.io/crates/rfc6052) [![Docs.rs](https://docs.rs/rfc6052/badge.svg)](https://docs.rs/rfc6052) - `rtnl`: A high-level wrapper around `rtnetlink` + - [![Crates.io](https://img.shields.io/crates/v/rtnl)](https://crates.io/crates/rtnl) [![Docs.rs](https://docs.rs/rtnl/badge.svg)](https://docs.rs/rtnl) For user-oriented documentation, see the [protomask website](https://protomask.ewpratten.com). From 62132213a9321477dae95d8780990f334b4ebb90 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:50:05 -0400 Subject: [PATCH 42/78] change format --- README.md | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 5fc57fc..1886204 100644 --- a/README.md +++ b/README.md @@ -7,20 +7,13 @@ This repository contains: -- `protomask`: The main NAT64 daemon - - [![Crates.io](https://img.shields.io/crates/v/protomask)](https://crates.io/crates/protomask) -- `protomask-clat`: A Customer-side transLATor (CLAT) implementation - - [![Crates.io](https://img.shields.io/crates/v/protomask)](https://crates.io/crates/protomask) -- `easy-tun`: A minimal TUN interface library - - [![Crates.io](https://img.shields.io/crates/v/easy-tun)](https://crates.io/crates/easy-tun) [![Docs.rs](https://docs.rs/easy-tun/badge.svg)](https://docs.rs/easy-tun) -- `fast-nat`: A library designed for highly efficient mapping and lookup of IP address pairs - - [![Crates.io](https://img.shields.io/crates/v/fast-nat)](https://crates.io/crates/fast-nat) [![Docs.rs](https://docs.rs/fast-nat/badge.svg)](https://docs.rs/fast-nat) -- `interproto`: A library for translating packets between protocols - - [![Crates.io](https://img.shields.io/crates/v/interproto)](https://crates.io/crates/interproto) [![Docs.rs](https://docs.rs/interproto/badge.svg)](https://docs.rs/interproto) -- `rfc6052`: A Rust implementation of RFC6052 - - [![Crates.io](https://img.shields.io/crates/v/rfc6052)](https://crates.io/crates/rfc6052) [![Docs.rs](https://docs.rs/rfc6052/badge.svg)](https://docs.rs/rfc6052) -- `rtnl`: A high-level wrapper around `rtnetlink` - - [![Crates.io](https://img.shields.io/crates/v/rtnl)](https://crates.io/crates/rtnl) [![Docs.rs](https://docs.rs/rtnl/badge.svg)](https://docs.rs/rtnl) +- [`protomask`](./src/protomask.rs): [![Crates.io](https://img.shields.io/crates/v/protomask)](https://crates.io/crates/protomask) +- [`protomask-clat`](./src/protomask-clat.rs): [![Crates.io](https://img.shields.io/crates/v/protomask)](https://crates.io/crates/protomask) +- [`easy-tun`](./libs/easy-tun/): [![Crates.io](https://img.shields.io/crates/v/easy-tun)](https://crates.io/crates/easy-tun) [![Docs.rs](https://docs.rs/easy-tun/badge.svg)](https://docs.rs/easy-tun) +- [`fast-nat`](./libs/fast-nat/): [![Crates.io](https://img.shields.io/crates/v/fast-nat)](https://crates.io/crates/fast-nat) [![Docs.rs](https://docs.rs/fast-nat/badge.svg)](https://docs.rs/fast-nat) +- [`interproto`](./libs/interproto/): [![Crates.io](https://img.shields.io/crates/v/interproto)](https://crates.io/crates/interproto) [![Docs.rs](https://docs.rs/interproto/badge.svg)](https://docs.rs/interproto) +- [`rfc6052`](./libs/rfc6052/): [![Crates.io](https://img.shields.io/crates/v/rfc6052)](https://crates.io/crates/rfc6052) [![Docs.rs](https://docs.rs/rfc6052/badge.svg)](https://docs.rs/rfc6052) +- [`rtnl`](./libs/rtnl/): [![Crates.io](https://img.shields.io/crates/v/rtnl)](https://crates.io/crates/rtnl) [![Docs.rs](https://docs.rs/rtnl/badge.svg)](https://docs.rs/rtnl) For user-oriented documentation, see the [protomask website](https://protomask.ewpratten.com). From 920e1bf3d33e45e4204dcc930989d9bee62d1d56 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:51:59 -0400 Subject: [PATCH 43/78] table fmt --- README.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1886204..97e5d1c 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,15 @@ This repository contains: -- [`protomask`](./src/protomask.rs): [![Crates.io](https://img.shields.io/crates/v/protomask)](https://crates.io/crates/protomask) -- [`protomask-clat`](./src/protomask-clat.rs): [![Crates.io](https://img.shields.io/crates/v/protomask)](https://crates.io/crates/protomask) -- [`easy-tun`](./libs/easy-tun/): [![Crates.io](https://img.shields.io/crates/v/easy-tun)](https://crates.io/crates/easy-tun) [![Docs.rs](https://docs.rs/easy-tun/badge.svg)](https://docs.rs/easy-tun) -- [`fast-nat`](./libs/fast-nat/): [![Crates.io](https://img.shields.io/crates/v/fast-nat)](https://crates.io/crates/fast-nat) [![Docs.rs](https://docs.rs/fast-nat/badge.svg)](https://docs.rs/fast-nat) -- [`interproto`](./libs/interproto/): [![Crates.io](https://img.shields.io/crates/v/interproto)](https://crates.io/crates/interproto) [![Docs.rs](https://docs.rs/interproto/badge.svg)](https://docs.rs/interproto) -- [`rfc6052`](./libs/rfc6052/): [![Crates.io](https://img.shields.io/crates/v/rfc6052)](https://crates.io/crates/rfc6052) [![Docs.rs](https://docs.rs/rfc6052/badge.svg)](https://docs.rs/rfc6052) -- [`rtnl`](./libs/rtnl/): [![Crates.io](https://img.shields.io/crates/v/rtnl)](https://crates.io/crates/rtnl) [![Docs.rs](https://docs.rs/rtnl/badge.svg)](https://docs.rs/rtnl) +| Crate | Info | +| -- | -- | +| [`protomask`](./src/protomask.rs) | [![Crates.io](https://img.shields.io/crates/v/protomask)](https://crates.io/crates/protomask) | +| [`protomask-clat`](./src/protomask-clat.rs) | [![Crates.io](https://img.shields.io/crates/v/protomask)](https://crates.io/crates/protomask) | +| [`easy-tun`](./libs/easy-tun/) | [![Crates.io](https://img.shields.io/crates/v/easy-tun)](https://crates.io/crates/easy-tun) [![Docs.rs](https://docs.rs/easy-tun/badge.svg)](https://docs.rs/easy-tun) | +| [`fast-nat`](./libs/fast-nat/) | [![Crates.io](https://img.shields.io/crates/v/fast-nat)](https://crates.io/crates/fast-nat) [![Docs.rs](https://docs.rs/fast-nat/badge.svg)](https://docs.rs/fast-nat) | +| [`interproto`](./libs/interproto/) | [![Crates.io](https://img.shields.io/crates/v/interproto)](https://crates.io/crates/interproto) [![Docs.rs](https://docs.rs/interproto/badge.svg)](https://docs.rs/interproto) | +| [`rfc6052`](./libs/rfc6052/) | [![Crates.io](https://img.shields.io/crates/v/rfc6052)](https://crates.io/crates/rfc6052) [![Docs.rs](https://docs.rs/rfc6052/badge.svg)](https://docs.rs/rfc6052) | +| [`rtnl`](./libs/rtnl/) | [![Crates.io](https://img.shields.io/crates/v/rtnl)](https://crates.io/crates/rtnl) [![Docs.rs](https://docs.rs/rtnl/badge.svg)](https://docs.rs/rtnl) | For user-oriented documentation, see the [protomask website](https://protomask.ewpratten.com). From 3522ccedda14266103b0243f3ea5da6eafcce327 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:54:12 -0400 Subject: [PATCH 44/78] Add badges to project readmes --- libs/easy-tun/README.md | 2 ++ libs/fast-nat/README.md | 2 ++ libs/interproto/README.md | 3 +++ libs/rfc6052/README.md | 2 ++ libs/rtnl/README.md | 2 ++ 5 files changed, 11 insertions(+) diff --git a/libs/easy-tun/README.md b/libs/easy-tun/README.md index 7961b7b..5606b85 100644 --- a/libs/easy-tun/README.md +++ b/libs/easy-tun/README.md @@ -1,3 +1,5 @@ # easy-tun +[![Crates.io](https://img.shields.io/crates/v/easy-tun)](https://crates.io/crates/easy-tun) +[![Docs.rs](https://docs.rs/easy-tun/badge.svg)](https://docs.rs/easy-tun) `easy-tun` is a pure-Rust library that can bring up and manage a TUN interface by directly interacting with the [Universal TUN/TAP Driver](https://docs.kernel.org/networking/tuntap.html). diff --git a/libs/fast-nat/README.md b/libs/fast-nat/README.md index 2144c63..b1562e2 100644 --- a/libs/fast-nat/README.md +++ b/libs/fast-nat/README.md @@ -1,4 +1,6 @@ # Fast Network Address Table +[![Crates.io](https://img.shields.io/crates/v/fast-nat)](https://crates.io/crates/fast-nat) +[![Docs.rs](https://docs.rs/fast-nat/badge.svg)](https://docs.rs/fast-nat) `fast-nat` is an OSI layer 3 Network Address Table built for speed. diff --git a/libs/interproto/README.md b/libs/interproto/README.md index e69de29..1eb1bee 100644 --- a/libs/interproto/README.md +++ b/libs/interproto/README.md @@ -0,0 +1,3 @@ +# Interproto: The internet protocol translation library +[![Crates.io](https://img.shields.io/crates/v/interproto)](https://crates.io/crates/interproto) +[![Docs.rs](https://docs.rs/interproto/badge.svg)](https://docs.rs/interproto) \ No newline at end of file diff --git a/libs/rfc6052/README.md b/libs/rfc6052/README.md index 0b5d420..c6fc56f 100644 --- a/libs/rfc6052/README.md +++ b/libs/rfc6052/README.md @@ -1,3 +1,5 @@ # RFC6052 for Rust +[![Crates.io](https://img.shields.io/crates/v/rfc6052)](https://crates.io/crates/rfc6052) +[![Docs.rs](https://docs.rs/rfc6052/badge.svg)](https://docs.rs/rfc6052) This library provides functions for interacting with [RFC6052](https://datatracker.ietf.org/doc/html/rfc6052) IPv4-Embedded IPv6 Addresses. diff --git a/libs/rtnl/README.md b/libs/rtnl/README.md index b27c96d..11d1623 100644 --- a/libs/rtnl/README.md +++ b/libs/rtnl/README.md @@ -1,3 +1,5 @@ # RTNL +[![Crates.io](https://img.shields.io/crates/v/rtnl)](https://crates.io/crates/rtnl) +[![Docs.rs](https://docs.rs/rtnl/badge.svg)](https://docs.rs/rtnl) A slightly sane wrapper around [`rtnetlink`](https://crates.io/crates/rtnetlink) From a9ca9f2b89d72d18f58d1befe840ff47f9b9146d Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:56:15 -0400 Subject: [PATCH 45/78] Fix incorrect site file structure --- .github/workflows/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 08c52d9..c251eb7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -39,7 +39,7 @@ jobs: BUILD_ONLY: true - name: Combine Book and Website - run: mkdir -p /tmp/combined && cp -rv docs/website/* /tmp/combined && cp -rv target/book/ /tmp/combined/book + run: mkdir -p /tmp/combined && cp -rv docs/website/public/* /tmp/combined && cp -rv target/book/ /tmp/combined/book - name: Write CNAME file run: echo "protomask.ewpratten.com" > /tmp/combined/CNAME From eddcc2eaf3293c7a9d885680e9f2110abd9a1dc5 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:57:59 -0400 Subject: [PATCH 46/78] Change the tagline --- docs/website/templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/website/templates/index.html b/docs/website/templates/index.html index 2b92d6d..d6598f0 100644 --- a/docs/website/templates/index.html +++ b/docs/website/templates/index.html @@ -6,7 +6,7 @@

Protomask

- The ultimate tool suite for transitioning to IPv6 + Fast & reliable userspace NAT64
From c0c8b5791f850d39cd661abc027c24e73f053d5c Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:58:35 -0400 Subject: [PATCH 47/78] fix spacing --- docs/website/templates/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/website/templates/index.html b/docs/website/templates/index.html index d6598f0..2bfb836 100644 --- a/docs/website/templates/index.html +++ b/docs/website/templates/index.html @@ -6,7 +6,7 @@

Protomask

- Fast & reliable userspace NAT64 + Fast & reliable user space NAT64
From f205e63428d65b0dd0cec6b06ba260d1327f26bb Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Thu, 3 Aug 2023 23:59:19 -0400 Subject: [PATCH 48/78] Update tagline in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 97e5d1c..e904d86 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build](https://github.com/Ewpratten/protomask/actions/workflows/build.yml/badge.svg)](https://github.com/ewpratten/protomask/actions/workflows/build.yml) [![Audit](https://github.com/ewpratten/protomask/actions/workflows/audit.yml/badge.svg)](https://github.com/ewpratten/protomask/actions/workflows/audit.yml) -**A user space [NAT64](https://en.wikipedia.org/wiki/NAT64) implementation.** +**Fast & reliable user space [NAT64](https://en.wikipedia.org/wiki/NAT64).** This repository contains: From 71f60e55c5053b1faf0484dd088614d481e897b7 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Fri, 4 Aug 2023 00:00:15 -0400 Subject: [PATCH 49/78] reorg readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e904d86..a247d3c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ **Fast & reliable user space [NAT64](https://en.wikipedia.org/wiki/NAT64).** -This repository contains: +For user-oriented documentation, see the [protomask website](https://protomask.ewpratten.com). + +## Table of Contents | Crate | Info | | -- | -- | @@ -17,7 +19,7 @@ This repository contains: | [`rfc6052`](./libs/rfc6052/) | [![Crates.io](https://img.shields.io/crates/v/rfc6052)](https://crates.io/crates/rfc6052) [![Docs.rs](https://docs.rs/rfc6052/badge.svg)](https://docs.rs/rfc6052) | | [`rtnl`](./libs/rtnl/) | [![Crates.io](https://img.shields.io/crates/v/rtnl)](https://crates.io/crates/rtnl) [![Docs.rs](https://docs.rs/rtnl/badge.svg)](https://docs.rs/rtnl) | -For user-oriented documentation, see the [protomask website](https://protomask.ewpratten.com). + ## Installation From 718dfbed242af1a65656679413cd1c8b6d857b3f Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Fri, 4 Aug 2023 00:02:51 -0400 Subject: [PATCH 50/78] test --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index a247d3c..83c5984 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ -# protomask +# Protomask: Fast & reliable user space NAT64 [![GitHub release](https://img.shields.io/github/v/release/ewpratten/protomask)](https://github.com/ewpratten/protomask/releases/latest) [![Build](https://github.com/Ewpratten/protomask/actions/workflows/build.yml/badge.svg)](https://github.com/ewpratten/protomask/actions/workflows/build.yml) [![Audit](https://github.com/ewpratten/protomask/actions/workflows/audit.yml/badge.svg)](https://github.com/ewpratten/protomask/actions/workflows/audit.yml) -**Fast & reliable user space [NAT64](https://en.wikipedia.org/wiki/NAT64).** For user-oriented documentation, see the [protomask website](https://protomask.ewpratten.com). From a28e720b1abff98ea00514055214e135fde74675 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Fri, 4 Aug 2023 00:09:36 -0400 Subject: [PATCH 51/78] Fix some web scaling bugs --- docs/website/sass/main.scss | 14 ++++++++++++++ docs/website/templates/base.html | 8 ++++---- docs/website/templates/index.html | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/website/sass/main.scss b/docs/website/sass/main.scss index 717d4cc..61d6f9a 100644 --- a/docs/website/sass/main.scss +++ b/docs/website/sass/main.scss @@ -5,3 +5,17 @@ font-weight: bold; } } + +@media screen and (max-width: 405px) { + .navbar-brand{ + display:none; + } +} + +#hero-title { + font-size:5em; + + @media screen and (max-width: 420px) { + font-size: 3em; + } +} \ No newline at end of file diff --git a/docs/website/templates/base.html b/docs/website/templates/base.html index 798ffb3..19fffed 100644 --- a/docs/website/templates/base.html +++ b/docs/website/templates/base.html @@ -17,12 +17,12 @@ -