diff --git a/Cargo.toml b/Cargo.toml index 3ca24cb..acbb5f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,61 +16,44 @@ exclude = ["/.github/", "/.vscode/"] [workspace] members = [ "libs/easy-tun", - "libs/fast-nat", + # "libs/fast-nat", "libs/interproto", "libs/rfc6052", "libs/rtnl", - "libs/protomask-metrics", ] [features] default = [] -profiler = [ - "puffin", - "puffin_http", - "easy-tun/profile-puffin", - "fast-nat/profile-puffin", - "interproto/profile-puffin", -] [[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" +path = "src/main.rs" [dependencies] # Internal dependencies easy-tun = { version = "^2.0.0", path = "libs/easy-tun" } fast-nat = { version = "^1.0.0", path = "libs/fast-nat" } -interproto = { version = "^1.0.0", path = "libs/interproto", features = [ - "metrics", -] } +interproto = { version = "^1.0.0", path = "libs/interproto" } rfc6052 = { version = "^1.0.0", path = "libs/rfc6052" } rtnl = { version = "^1.0.0", path = "libs/rtnl", features = ["tokio"] } -protomask-metrics = { version = "^0.1.0", path = "libs/protomask-metrics" } # 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"] } ipnet = { version = "2.8.0", features = ["serde"] } -puffin_http = { version = "0.13.0", optional = true } -puffin = { version = "0.16.0", optional = true } +# puffin_http = { version = "0.13.0", optional = true } +# puffin = { version = "0.16.0", optional = true } serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0" log = "0.4.19" fern = "0.6.2" -nix = "0.26.2" +# nix = "0.26.2" thiserror = "1.0.44" -cfg-if = "1.0.0" -profiling = "1.0.9" +# cfg-if = "1.0.0" +# profiling = "1.0.9" +caps = "0.5.5" +bimap = "0.6.3" [profile.release] opt-level = 3 @@ -84,21 +67,11 @@ assets = [ "/usr/local/bin/protomask", "755", ], - [ - "target/release/protomask-clat", - "/usr/local/bin/protomask-clat", - "755", - ], [ "config/protomask.json", "/etc/protomask/protomask.json", "644", ], - [ - "config/protomask-clat.json", - "/etc/protomask/protomask-clat.json", - "644", - ], [ "README.md", "/usr/share/doc/protomask/README.md", @@ -108,16 +81,11 @@ assets = [ conf-files = [] depends = [] maintainer-scripts = "./debian/" -systemd-units = [ - { unit-name = "protomask-service", enable = false }, - { unit-name = "protomask-clat-service", enable = false }, -] +systemd-units = [{ unit-name = "protomask-service", enable = false }] [package.metadata.generate-rpm] assets = [ - { source = "target/release/protomask", dest = "/usr/local/bin/protomask", mode = "755"}, - { source = "target/release/protomask-clat", dest = "/usr/local/bin/protomask-clat", mode = "755"}, - { source = "config/protomask.json", dest = "/etc/protomask/protomask.json", mode = "644"}, - { source = "config/protomask-clat.json", dest = "/etc/protomask/protomask-clat.json", mode = "644"}, - { source = "README.md", dest = "/usr/share/doc/protomask/README.md", mode = "644"}, -] \ No newline at end of file + { source = "target/release/protomask", dest = "/usr/local/bin/protomask", mode = "755" }, + { source = "config/protomask.json", dest = "/etc/protomask/protomask.json", mode = "644" }, + { source = "README.md", dest = "/usr/share/doc/protomask/README.md", mode = "644" }, +] diff --git a/debian/protomask-clat-service b/debian/protomask-clat-service deleted file mode 100644 index 8677cfc..0000000 --- a/debian/protomask-clat-service +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description = Protomask CLAT -After = network.target - -[Service] -ExecStart = /usr/local/bin/protomask-clat --config /etc/protomask/protomask-clat.json - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/libs/easy-tun/src/tun.rs b/libs/easy-tun/src/tun.rs index 68ab26f..0d2d387 100644 --- a/libs/easy-tun/src/tun.rs +++ b/libs/easy-tun/src/tun.rs @@ -85,8 +85,9 @@ impl Tun { // Check for errors if err < 0 { - log::error!("ioctl failed: {}", err); - return Err(std::io::Error::last_os_error()); + let last_error = std::io::Error::last_os_error(); + log::error!("ioctl failed: {:?}", last_error); + return Err(last_error); } } diff --git a/libs/interproto/Cargo.toml b/libs/interproto/Cargo.toml index 6f7bcaf..e6dae8d 100644 --- a/libs/interproto/Cargo.toml +++ b/libs/interproto/Cargo.toml @@ -14,11 +14,9 @@ categories = [] [features] default = [] -metrics = ["protomask-metrics"] profile-puffin = ["profiling/profile-with-puffin"] [dependencies] -protomask-metrics = { version = "^0.1.0", path = "../protomask-metrics", optional = true } log = "^0.4" pnet = "0.34.0" thiserror = "^1.0.44" diff --git a/libs/protomask-metrics/Cargo.toml b/libs/protomask-metrics/Cargo.toml deleted file mode 100644 index 4b1a816..0000000 --- a/libs/protomask-metrics/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[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] -hyper = { version = "0.14.27", features = ["server", "http1", "tcp"] } -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 deleted file mode 100644 index cf6f4e4..0000000 --- a/libs/protomask-metrics/README.md +++ /dev/null @@ -1 +0,0 @@ -**`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/http.rs b/libs/protomask-metrics/src/http.rs deleted file mode 100644 index 0124f54..0000000 --- a/libs/protomask-metrics/src/http.rs +++ /dev/null @@ -1,44 +0,0 @@ -use hyper::{ - service::{make_service_fn, service_fn}, - Body, Method, Request, Response, Server, -}; -use prometheus::{Encoder, TextEncoder}; -use std::{convert::Infallible, net::SocketAddr}; - -/// 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/libs/protomask-metrics/src/lib.rs b/libs/protomask-metrics/src/lib.rs deleted file mode 100644 index 775643d..0000000 --- a/libs/protomask-metrics/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -#![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::doc_markdown)] - -pub mod http; -pub mod metrics; - -#[macro_use] -pub mod macros; diff --git a/libs/protomask-metrics/src/macros.rs b/libs/protomask-metrics/src/macros.rs deleted file mode 100644 index 7ee6c01..0000000 --- a/libs/protomask-metrics/src/macros.rs +++ /dev/null @@ -1,9 +0,0 @@ -/// 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 deleted file mode 100644 index 381e1eb..0000000 --- a/libs/protomask-metrics/src/metrics.rs +++ /dev/null @@ -1,37 +0,0 @@ -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(); - - /// 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(); -} diff --git a/src/args/mod.rs b/src/args/mod.rs deleted file mode 100644 index 7ff1b4f..0000000 --- a/src/args/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! This module contains the definitions for each binary's CLI arguments and config file structure for the sake of readability. - -use cfg_if::cfg_if; - -pub mod protomask; -pub mod protomask_clat; - -// Used to trick the build process into including a CLI argument based on a feature flag -cfg_if! { - if #[cfg(feature = "profiler")] { - #[derive(Debug, clap::Args)] - pub struct ProfilerArgs { - /// Expose the puffin HTTP server on this endpoint - #[clap(long)] - pub puffin_endpoint: Option, - } - } else { - #[derive(Debug, clap::Args)] - pub struct ProfilerArgs; - } -} diff --git a/src/args/protomask.rs b/src/args/protomask.rs deleted file mode 100644 index 2c717d9..0000000 --- a/src/args/protomask.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::{ - net::{Ipv4Addr, Ipv6Addr, SocketAddr}, - path::PathBuf, -}; - -use ipnet::{Ipv4Net, Ipv6Net}; - -use crate::common::rfc6052::parse_network_specific_prefix; - -use super::ProfilerArgs; - -#[derive(clap::Parser)] -#[clap(author, version, about="Fast and simple NAT64", long_about = None)] -pub struct Args { - #[command(flatten)] - config_data: Option, - - /// Path to a config file to read - #[clap(short = 'c', long = "config", conflicts_with = "Config")] - config_file: Option, - - /// Explicitly set the interface name to use - #[clap(short, long, default_value_t = ("nat%d").to_string())] - pub interface: String, - - #[command(flatten)] - pub profiler_args: ProfilerArgs, - - /// Enable verbose logging - #[clap(short, long)] - pub verbose: bool, -} - -impl Args { - #[allow(dead_code)] - pub fn data(&self) -> Result> { - match self.config_file { - Some(ref path) => { - // Read the data from the config file - let file = std::fs::File::open(path).map_err(|error| match error.kind() { - std::io::ErrorKind::NotFound => { - log::error!("Config file not found: {}", path.display()); - std::process::exit(1) - } - _ => error, - })?; - let data: Config = serde_json::from_reader(file)?; - - // We need at least one pool prefix - if data.pool_prefixes.is_empty() { - log::error!("No pool prefixes specified. At least one prefix must be specified in the `pool` property of the config file"); - std::process::exit(1); - } - - Ok(data) - } - None => match &self.config_data { - Some(data) => Ok(data.clone()), - None => { - log::error!("No configuration provided. Either use --config to specify a file or set the configuration via CLI args (see --help)"); - std::process::exit(1) - } - }, - } - } -} - -/// Program configuration. Specifiable via either CLI args or a config file -#[derive(Debug, clap::Args, serde::Deserialize, Clone)] -#[group()] -pub struct Config { - /// IPv4 prefixes to use as NAT pool address space - #[clap(long = "pool-prefix")] - #[serde(rename = "pool")] - pub pool_prefixes: Vec, - - /// Static mapping between IPv4 and IPv6 addresses - #[clap(skip)] - pub static_map: Vec<(Ipv4Addr, Ipv6Addr)>, - - /// Enable prometheus metrics on a given address - #[clap(long = "prometheus")] - #[serde(rename = "prometheus_bind_addr")] - pub prom_bind_addr: Option, - - /// RFC6052 IPv6 translation prefix - #[clap(long, default_value_t = ("64:ff9b::/96").parse().unwrap(), value_parser = parse_network_specific_prefix)] - #[serde( - rename = "prefix", - serialize_with = "crate::common::rfc6052::serialize_network_specific_prefix" - )] - pub translation_prefix: Ipv6Net, - - /// NAT reservation timeout in seconds - #[clap(long, default_value = "7200")] - pub reservation_timeout: u64, - - /// Number of queues to create on the TUN device - #[clap(long, default_value = "10")] - #[serde(rename = "queues")] - pub num_queues: usize, -} - -#[derive(Debug, serde::Deserialize, Clone)] -pub struct StaticMap { - pub ipv4: Ipv4Addr, - pub ipv6: Ipv6Addr, -} - -impl From for (Ipv4Addr, Ipv6Addr) { - fn from(val: StaticMap) -> Self { - (val.ipv4, val.ipv6) - } -} diff --git a/src/args/protomask_clat.rs b/src/args/protomask_clat.rs deleted file mode 100644 index a9d7cec..0000000 --- a/src/args/protomask_clat.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! Commandline arguments and config file definitions for `protomask-clat` - -use super::ProfilerArgs; -use crate::common::rfc6052::parse_network_specific_prefix; -use ipnet::{Ipv4Net, Ipv6Net}; -use std::{net::SocketAddr, path::PathBuf}; - -#[derive(Debug, clap::Parser)] -#[clap(author, version, about="IPv4 to IPv6 Customer-side transLATor (CLAT)", long_about = None)] -pub struct Args { - #[command(flatten)] - config_data: Option, - - /// Path to a config file to read - #[clap(short = 'c', long = "config", conflicts_with = "Config")] - config_file: Option, - - /// Explicitly set the interface name to use - #[clap(short, long, default_value_t = ("clat%d").to_string())] - pub interface: String, - - #[command(flatten)] - pub profiler_args: ProfilerArgs, - - /// Enable verbose logging - #[clap(short, long)] - pub verbose: bool, -} - -impl Args { - #[allow(dead_code)] - pub fn data(&self) -> Result> { - match self.config_file { - Some(ref path) => { - // Read the data from the config file - let file = std::fs::File::open(path).map_err(|error| match error.kind() { - std::io::ErrorKind::NotFound => { - log::error!("Config file not found: {}", path.display()); - std::process::exit(1) - } - _ => error, - })?; - let data: Config = serde_json::from_reader(file)?; - - // We need at least one customer prefix - if data.customer_pool.is_empty() { - log::error!("No customer prefixes specified. At least one prefix must be specified in the `customer_pool` property of the config file"); - std::process::exit(1); - } - - Ok(data) - } - None => match &self.config_data { - Some(data) => Ok(data.clone()), - None => { - log::error!("No configuration provided. Either use --config to specify a file or set the configuration via CLI args (see --help)"); - std::process::exit(1) - } - }, - } - } -} - -/// Program configuration. Specifiable via either CLI args or a config file -#[derive(Debug, clap::Args, serde::Deserialize, Clone)] -#[group()] -pub struct Config { - /// One or more customer-side IPv4 prefixes to allow through CLAT - #[clap(long = "customer-prefix")] - #[serde(rename = "customer_pool")] - pub customer_pool: Vec, - - /// Enable prometheus metrics on a given address - #[clap(long = "prometheus")] - #[serde(rename = "prometheus_bind_addr")] - pub prom_bind_addr: Option, - - /// 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)] - #[serde( - rename = "via", - serialize_with = "crate::common::rfc6052::serialize_network_specific_prefix" - )] - pub embed_prefix: Ipv6Net, - - /// Number of queues to create on the TUN device - #[clap(long, default_value = "10")] - #[serde(rename = "queues")] - pub num_queues: usize, -} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..e9e95e3 --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,102 @@ +use std::{ + net::{Ipv4Addr, Ipv6Addr}, + str::FromStr, +}; + +use clap::{Parser, Subcommand}; +use ipnet::{Ipv4Net, Ipv6Net}; + +/// Fast & reliable user space NAT64 +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +pub struct Args { + /// Translation engine + #[command(subcommand)] + pub engine: Modes, + + /// Enable verbose logs + #[clap(short, long)] + pub verbose: bool, +} + +#[derive(Subcommand, Debug)] +pub enum Modes { + /// Run as a NAT64 translator + Nat64 { + /// Explicitly set the interface name to use + #[clap(short, long, default_value_t = ("nat%d").to_string())] + interface: String, + + /// IPv4 prefixes to use as NAT pool address space + #[clap(short, long = "pool-prefix", required = true)] + pool_prefixes: Vec, + + /// Statically map an IPv4 and IPv6 address to each other + #[clap(short, long, value_parser = parse_static_map)] + static_map: Vec<(Ipv4Addr, Ipv6Addr)>, + + /// RFC6052 IPv6 translation prefix + #[clap(short, long, default_value_t = ("64:ff9b::/96").parse().unwrap(), value_parser = parse_network_specific_prefix)] + translation_prefix: Ipv6Net, + + /// NAT lease duration in seconds + #[clap(short, long, default_value = "7200")] + lease_duration: u64, + + /// Number of queues to create on the TUN device + #[clap(short = 'q', long, default_value = "10")] + num_queues: usize, + }, + + /// Run as a Customer-side transLATor + Clat { + /// Explicitly set the interface name to use + #[clap(short, long, default_value_t = ("clat%d").to_string())] + interface: String, + + /// One or more customer-side IPv4 prefixes to allow through CLAT + #[clap(short, long = "customer-prefix", required = true)] + customer_pool: Vec, + + /// RFC6052 IPv6 prefix to encapsulate IPv4 packets within + #[clap(short, long="via", default_value_t = ("64:ff9b::/96").parse().unwrap(), value_parser = parse_network_specific_prefix)] + embed_prefix: Ipv6Net, + + /// Number of queues to create on the TUN device + #[clap(short = 'q', long, default_value = "10")] + num_queues: usize, + }, +} + +/// Parses an [RFC6052 Section 2.2](https://datatracker.ietf.org/doc/html/rfc6052#section-2.2)-compliant IPv6 prefix from a string +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 !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) +} + +/// Parses a mapping of an IPv4 address to an IPv6 address +fn parse_static_map(string: &str) -> Result<(Ipv4Addr, Ipv6Addr), String> { + // Split the string into two parts + let parts: Vec<&str> = string.split('=').collect(); + if parts.len() != 2 { + return Err("Static map must be in the form 'IPv4=IPv6'".to_string()); + } + + // Parse the IPv4 and IPv6 addresses + let v4 = Ipv4Addr::from_str(parts[0]).map_err(|err| err.to_string())?; + let v6 = Ipv6Addr::from_str(parts[1]).map_err(|err| err.to_string())?; + + // Return the parsed mapping + Ok((v4, v6)) +} \ No newline at end of file diff --git a/src/common/logging.rs b/src/common/logging.rs deleted file mode 100644 index 72ac3b7..0000000 --- a/src/common/logging.rs +++ /dev/null @@ -1,51 +0,0 @@ -use owo_colors::{OwoColorize, Stream::Stdout}; - -/// Enable the logger -#[allow(dead_code)] -pub fn enable_logger(verbose: bool) { - fern::Dispatch::new() - .format(move |out, message, record| { - out.finish(format_args!( - "{}{}: {}", - // Level messages are padded to keep the output looking somewhat sane - match record.level() { - log::Level::Error => "ERROR" - .if_supports_color(Stdout, |text| text.red()) - .if_supports_color(Stdout, |text| text.bold()) - .to_string(), - log::Level::Warn => "WARN " - .if_supports_color(Stdout, |text| text.yellow()) - .if_supports_color(Stdout, |text| text.bold()) - .to_string(), - log::Level::Info => "INFO " - .if_supports_color(Stdout, |text| text.green()) - .if_supports_color(Stdout, |text| text.bold()) - .to_string(), - log::Level::Debug => "DEBUG" - .if_supports_color(Stdout, |text| text.bright_blue()) - .if_supports_color(Stdout, |text| text.bold()) - .to_string(), - log::Level::Trace => "TRACE" - .if_supports_color(Stdout, |text| text.bright_white()) - .if_supports_color(Stdout, |text| text.bold()) - .to_string(), - }, - // Only show the outer package name if verbose logging is enabled (otherwise nothing) - match verbose { - true => format!(" [{}]", record.target().split("::").next().unwrap()), - false => String::new(), - } - .if_supports_color(Stdout, |text| text.bright_black()), - message - )) - }) - // Set the correct log level based on CLI flags - .level(match verbose { - true => log::LevelFilter::Debug, - false => log::LevelFilter::Info, - }) - // Output to STDOUT - .chain(std::io::stdout()) - .apply() - .unwrap(); -} diff --git a/src/common/mod.rs b/src/common/mod.rs deleted file mode 100644 index a9658e6..0000000 --- a/src/common/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Common code used across all protomask binaries - -pub mod logging; -pub mod packet_handler; -pub mod permissions; -pub mod profiler; -pub mod rfc6052; diff --git a/src/common/packet_handler.rs b/src/common/packet_handler.rs deleted file mode 100644 index 8659afe..0000000 --- a/src/common/packet_handler.rs +++ /dev/null @@ -1,180 +0,0 @@ -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), -} - -/// Get the layer 3 protocol of a packet -pub fn get_layer_3_proto(packet: &[u8]) -> Option { - // 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); - Some(layer_3_proto) -} - -/// Get the source and destination addresses of an IPv4 packet -pub fn get_ipv4_src_dst(packet: &[u8]) -> (Ipv4Addr, Ipv4Addr) { - 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())); - - (source_addr, destination_addr) -} - -/// Get the source and destination addresses of an IPv6 packet -pub fn get_ipv6_src_dst(packet: &[u8]) -> (Ipv6Addr, Ipv6Addr) { - 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())); - - (source_addr, destination_addr) -} - -/// Appropriately handle a translation error -pub fn handle_translation_error( - result: Result>, PacketHandlingError>, -) -> Option> { - // We may or may not have a warn-able error - match result { - // If we get data, return it - Ok(data) => data, - // If we get an error, handle it and return None - Err(error) => match error { - PacketHandlingError::InterprotoError(interproto::error::Error::PacketTooShort { - expected, - actual, - }) => { - log::warn!( - "Got packet with length {} when expecting at least {} bytes", - actual, - expected - ); - None - } - PacketHandlingError::InterprotoError( - interproto::error::Error::UnsupportedIcmpType(icmp_type), - ) => { - log::warn!("Got a packet with an unsupported ICMP type: {}", icmp_type); - None - } - 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) => { - log::warn!("IPv4 pool exhausted. Dropping packet."); - None - } - PacketHandlingError::FastNatError(fast_nat::error::Error::InvalidIpv4Address(addr)) => { - log::warn!("Invalid IPv4 address: {}", addr); - None - } - }, - } -} - -// /// Handles checking the version number of an IP packet and calling the correct handler with needed data -// pub fn handle_packet( -// packet: &[u8], -// mut ipv4_handler: Ipv4Handler, -// mut ipv6_handler: Ipv6Handler, -// ) -> Option> -// where -// 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() { -// 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) => data, -// // If we get an error, handle it and return None -// Err(error) => match error { -// PacketHandlingError::InterprotoError(interproto::error::Error::PacketTooShort { -// expected, -// actual, -// }) => { -// log::warn!( -// "Got packet with length {} when expecting at least {} bytes", -// actual, -// expected -// ); -// None -// } -// PacketHandlingError::InterprotoError( -// interproto::error::Error::UnsupportedIcmpType(icmp_type), -// ) => { -// log::warn!("Got a packet with an unsupported ICMP type: {}", icmp_type); -// None -// } -// 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) => { -// log::warn!("IPv4 pool exhausted. Dropping packet."); -// None -// } -// PacketHandlingError::FastNatError(fast_nat::error::Error::InvalidIpv4Address(addr)) => { -// log::warn!("Invalid IPv4 address: {}", addr); -// None -// } -// }, -// } -// } diff --git a/src/common/permissions.rs b/src/common/permissions.rs deleted file mode 100644 index 54c17c8..0000000 --- a/src/common/permissions.rs +++ /dev/null @@ -1,9 +0,0 @@ -use nix::unistd::Uid; - -/// Ensures the binary is being exxecuted as root -pub fn ensure_root() { - if !Uid::effective().is_root() { - log::error!("This program must be run as root"); - std::process::exit(1); - } -} diff --git a/src/common/profiler.rs b/src/common/profiler.rs deleted file mode 100644 index 8f9fefd..0000000 --- a/src/common/profiler.rs +++ /dev/null @@ -1,20 +0,0 @@ -use cfg_if::cfg_if; - -use crate::args::ProfilerArgs; - -cfg_if! { - if #[cfg(feature = "profiler")] { - pub fn start_puffin_server(args: &ProfilerArgs) -> Option { - if let Some(endpoint) = args.puffin_endpoint { - log::info!("Starting puffin server on {}", endpoint); - puffin::set_scopes_on(true); - Some(puffin_http::Server::new(&endpoint.to_string()).unwrap()) - } else { - None - } - } - } else { - #[allow(dead_code)] - pub fn start_puffin_server(_args: &ProfilerArgs){} - } -} diff --git a/src/common/rfc6052.rs b/src/common/rfc6052.rs deleted file mode 100644 index c5802b6..0000000 --- a/src/common/rfc6052.rs +++ /dev/null @@ -1,22 +0,0 @@ -//! Utilities for interacting with [RFC6052](https://datatracker.ietf.org/doc/html/rfc6052) "IPv4-Embedded IPv6 Addresses" - -use std::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 !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) -} diff --git a/src/engines/clat.rs b/src/engines/clat.rs new file mode 100644 index 0000000..0955ecd --- /dev/null +++ b/src/engines/clat.rs @@ -0,0 +1,9 @@ +use ipnet::{Ipv4Net, Ipv6Net}; + +pub async fn do_clat( + interface: String, + customer_pool: Vec, + embed_prefix: Ipv6Net, + num_queues: usize, +) { +} diff --git a/src/engines/mod.rs b/src/engines/mod.rs new file mode 100644 index 0000000..3536b78 --- /dev/null +++ b/src/engines/mod.rs @@ -0,0 +1,3 @@ +//! This module contains each of the "translation engines" that power protomask +pub mod nat64; +pub mod clat; \ No newline at end of file diff --git a/src/engines/nat64.rs b/src/engines/nat64.rs new file mode 100644 index 0000000..0894d72 --- /dev/null +++ b/src/engines/nat64.rs @@ -0,0 +1,246 @@ +use easy_tun::Tun; +use interproto::protocols::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4}; +use ipnet::{IpNet, Ipv4Net, Ipv6Net}; +use rfc6052::{embed_ipv4_addr_unchecked, extract_ipv4_addr_unchecked}; +use std::{ + io::{Read, Write}, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + sync::{Arc, Mutex}, + thread, + time::Duration, +}; + +use crate::nat::NetworkAddressTranslationTable; + +/// Run NAT64 logic +pub async fn do_nat64( + interface: String, + pool_prefixes: Vec, + static_map: Vec<(Ipv4Addr, Ipv6Addr)>, + translation_prefix: Ipv6Net, + lease_duration: u64, + num_queues: usize, +) { + // Create a TUN interface + let tun = Arc::new( + Tun::new(&interface, num_queues) + .map_err(|err| { + log::error!("Failed to create TUN interface"); + err + }) + .unwrap(), + ); + + // Get the "interface index" of our new interface + let rt_netlink_handle = rtnl::new_handle().unwrap(); + let tun_link_index = rtnl::link::get_link_index(&rt_netlink_handle, tun.name()) + .await + .unwrap() + .unwrap(); + log::debug!("TUN interface index: {}", tun_link_index); + + // Bring the interface up + log::info!("Bringing up TUN interface: {}", tun.name()); + rtnl::link::link_up(&rt_netlink_handle, tun_link_index) + .await + .unwrap(); + + // Add a route for the translation prefix + log::debug!("Adding route for {} to {}", translation_prefix, tun.name()); + rtnl::route::route_add( + IpNet::V6(translation_prefix), + &rt_netlink_handle, + tun_link_index, + ) + .await + .unwrap(); + + // Add a route for each pool prefix + for pool_prefix in &pool_prefixes { + log::debug!("Adding route for {} to {}", pool_prefix, tun.name()); + rtnl::route::route_add( + IpNet::V4(pool_prefix.clone()), + &rt_netlink_handle, + tun_link_index, + ) + .await + .unwrap(); + } + + // Set up the address table + let address_table = Arc::new(Mutex::new(NetworkAddressTranslationTable::new( + Duration::from_secs(lease_duration), + ))); + for (ipv4, ipv6) in static_map { + log::info!("Adding static mapping: {} <--> {}", ipv4, ipv6); + address_table + .lock() + .unwrap() + .add_pair(ipv4, ipv6, false) + .unwrap(); + } + + // Perform translation + log::info!("Starting {} worker threads...", num_queues); + let mut worker_threads = Vec::new(); + for queue_id in 0..num_queues { + let tun = tun.clone(); + let address_table = address_table.clone(); + let pool_prefixes = pool_prefixes.clone(); + worker_threads.push(thread::spawn(move || { + log::debug!("Starting worker thread for queue ID: {}", queue_id); + + // Allocate a buffer for the packet + // TODO: Add custom MTU support + let mut buffer = vec![0u8; 1500]; + + // Process forever + loop { + // Read a packet + let len = tun.fd(queue_id).unwrap().read(&mut buffer).unwrap(); + let packet = &buffer[..len]; + + // If we have an empty packet, skip + if len == 0 { + log::debug!("Skipping empty packet"); + continue; + } + + // Determine the layer 3 protocol + let layer_3_proto = packet[0] >> 4; + log::trace!("Handling packet with layer 3 protocol: {}", layer_3_proto); + + // Try to translate the packet + match layer_3_proto { + // Ipv4 -> IPv6 + 4 => { + // Figure out the original (untranslated) source and destination addresses + let ipv4_source_addr = + Ipv4Addr::from(u32::from_be_bytes(packet[12..16].try_into().unwrap())); + let ipv4_destination_addr = + Ipv4Addr::from(u32::from_be_bytes(packet[16..20].try_into().unwrap())); + + // Look up the appropriate IPv6 address for the destination + let ipv6_destination_addr = address_table + .lock() + .unwrap() + .get_ipv6(&ipv4_destination_addr); + + // If we have a mapping for the destination address, we can translate the packet + if let Some(ipv6_destination_addr) = ipv6_destination_addr { + // Construct a new IPv6 source addr + let ipv6_source_addr = unsafe { + embed_ipv4_addr_unchecked(ipv4_source_addr, translation_prefix) + }; + + if let Ok(translated_packet) = translate_ipv4_to_ipv6( + packet, + ipv6_source_addr, + ipv6_destination_addr, + ) { + // Update the lease + address_table + .lock() + .unwrap() + .tick_flow(&IpAddr::V4(ipv4_destination_addr)); + + // Write the translated packet to the TUN interface + tun.fd(queue_id) + .unwrap() + .write_all(&translated_packet) + .unwrap(); + } + } + } + + // IPv6 -> IPv4 + 6 => { + // Figure out the original (untranslated) source and destination addresses + let ipv6_source_addr = + Ipv6Addr::from(u128::from_be_bytes(packet[8..24].try_into().unwrap())); + let ipv6_destination_addr = + Ipv6Addr::from(u128::from_be_bytes(packet[24..40].try_into().unwrap())); + + // Check if we have a source IPv4 address already leased. If not, attempt to allocate one + let mut ipv4_source_addr = + address_table.lock().unwrap().get_ipv4(&ipv6_source_addr); + if ipv4_source_addr.is_none() { + // Find the next available IPv4 address in the pool + ipv4_source_addr = + address_table.lock().unwrap().find_free_ipv4(&pool_prefixes); + + // Try to insert a new mapping for this address + if let Some(ipv4_source_addr) = ipv4_source_addr { + address_table + .lock() + .unwrap() + .add_pair(ipv4_source_addr, ipv6_source_addr, true) + .unwrap(); + log::debug!( + "Created mapping for {} <--> {}", + ipv6_source_addr, + ipv4_source_addr + ); + } else { + // If we are here, we have run out of IPv4 addresses + log::warn!( + "IPv4 pool exhausted. {} did not get a lease", + ipv6_source_addr + ); + continue; + } + } + + // Unwrap the source address + let ipv4_source_addr = ipv4_source_addr.unwrap(); + + // Figure out the correct IPv4 destination address + let ipv4_destination_addr = unsafe { + extract_ipv4_addr_unchecked( + ipv6_destination_addr, + translation_prefix.prefix_len(), + ) + }; + + // Translate the packet + if let Ok(translated_packet) = + translate_ipv6_to_ipv4(packet, ipv4_source_addr, ipv4_destination_addr) + { + // Update the lease + address_table + .lock() + .unwrap() + .tick_flow(&IpAddr::V6(ipv6_source_addr)); + + // Write the translated packet to the TUN interface + tun.fd(queue_id) + .unwrap() + .write_all(&translated_packet) + .unwrap(); + } + } + + // Unknown protocol + _ => { + log::warn!("Unsupported layer 3 protocol: {}", layer_3_proto); + continue; + } + } + } + })); + } + + // Spawn a helper thread that periodically cleans up expired leases + let address_table = address_table.clone(); + worker_threads.push(thread::spawn(move || loop { + thread::sleep(Duration::from_secs(30)); + log::trace!("Pruning expired leases"); + address_table.lock().unwrap().prune(); + })); + + // Wait for all workers to finish + log::info!("Processing packets"); + for worker in worker_threads { + worker.join().unwrap(); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b385509 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,101 @@ +use caps::{CapSet, Capability}; +use clap::Parser; +use owo_colors::OwoColorize; + +mod cli; +mod engines; +mod nat; + +#[tokio::main] +pub async fn main() { + // Parse CLI args + let args = cli::Args::parse(); + + // Set up the logger + fern::Dispatch::new() + .format(move |out, message, record| { + out.finish(format_args!( + "{}{}: {}", + // Level messages are padded to keep the output looking somewhat sane + match record.level() { + log::Level::Error => "ERROR" + .if_supports_color(owo_colors::Stream::Stdout, |text| text.red()) + .if_supports_color(owo_colors::Stream::Stdout, |text| text.bold()) + .to_string(), + log::Level::Warn => "WARN " + .if_supports_color(owo_colors::Stream::Stdout, |text| text.yellow()) + .if_supports_color(owo_colors::Stream::Stdout, |text| text.bold()) + .to_string(), + log::Level::Info => "INFO " + .if_supports_color(owo_colors::Stream::Stdout, |text| text.green()) + .if_supports_color(owo_colors::Stream::Stdout, |text| text.bold()) + .to_string(), + log::Level::Debug => "DEBUG" + .if_supports_color(owo_colors::Stream::Stdout, |text| text.bright_blue()) + .if_supports_color(owo_colors::Stream::Stdout, |text| text.bold()) + .to_string(), + log::Level::Trace => "TRACE" + .if_supports_color(owo_colors::Stream::Stdout, |text| text.bright_white()) + .if_supports_color(owo_colors::Stream::Stdout, |text| text.bold()) + .to_string(), + }, + // Only show the outer package name if verbose logging is enabled (otherwise nothing) + match args.verbose { + true => format!(" [{}]", record.target().split("::").next().unwrap()), + false => String::new(), + } + .if_supports_color(owo_colors::Stream::Stdout, |text| text.bright_black()), + message + )) + }) + // Set the correct log level based on CLI flags + .level(match args.verbose { + true => log::LevelFilter::Debug, + false => log::LevelFilter::Info, + }) + // Output to STDOUT + .chain(std::io::stdout()) + .apply() + .unwrap(); + + // We require NET_ADMIN capabilities to run + if !caps::has_cap(None, CapSet::Permitted, Capability::CAP_NET_ADMIN).unwrap() { + log::error!("This program must be run with the NET_ADMIN capability"); + std::process::exit(1); + } + + // Start up the correct translation engine + log::info!( + "Starting translation engine: {}", + format!("{:?}", args.engine).split(' ').next().unwrap() + ); + match args.engine { + cli::Modes::Nat64 { + interface, + pool_prefixes, + static_map, + translation_prefix, + lease_duration, + num_queues, + } => { + engines::nat64::do_nat64( + interface, + pool_prefixes, + static_map, + translation_prefix, + lease_duration, + num_queues, + ) + .await + } + cli::Modes::Clat { + interface, + customer_pool, + embed_prefix, + num_queues, + } => engines::clat::do_clat(interface, customer_pool, embed_prefix, num_queues).await, + } + + // We are done at this point + log::info!("Protomask has finished running. Cleaning up and exiting."); +} diff --git a/src/nat.rs b/src/nat.rs new file mode 100644 index 0000000..8a7209b --- /dev/null +++ b/src/nat.rs @@ -0,0 +1,153 @@ +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + time::{Duration, SystemTime}, +}; + +use bimap::BiHashMap; +use ipnet::Ipv4Net; + +#[derive(Debug, thiserror::Error)] +pub enum NatTableError { + #[error("IPv4 address {0} is already in the table")] + Ipv4AlreadyInTable(Ipv4Addr), + #[error("IPv6 address {0} is already in the table")] + Ipv6AlreadyInTable(Ipv6Addr), +} + +struct IpMappingLease { + /// Total number of times this lease has been "ticked" + ticks: u128, + + /// Time that this lease will expire + expiry: Option, +} + +pub struct NetworkAddressTranslationTable { + /// Mapping between addresses + addr_pairs: BiHashMap, + + /// Lease duration + lease_duration: Duration, + + /// Information about leases + leases: HashMap<(Ipv4Addr, Ipv6Addr), IpMappingLease>, +} + +impl NetworkAddressTranslationTable { + /// Construct a new Cross-protocol network address table + pub fn new(lease_duration: Duration) -> Self { + Self { + addr_pairs: BiHashMap::new(), + lease_duration, + leases: HashMap::new(), + } + } + + /// Add a pair of addresses to the pool + pub fn add_pair( + &mut self, + ipv4: Ipv4Addr, + ipv6: Ipv6Addr, + expires: bool, + ) -> Result<(), NatTableError> { + // If either address is already in the table, throw an error + if self.addr_pairs.contains_left(&ipv4) { + return Err(NatTableError::Ipv4AlreadyInTable(ipv4)); + } + if self.addr_pairs.contains_right(&ipv6) { + return Err(NatTableError::Ipv6AlreadyInTable(ipv6)); + } + + // Insert the pair into the table + self.addr_pairs.insert(ipv4, ipv6); + + // Add a lease to the lease table + self.leases.insert( + (ipv4, ipv6), + IpMappingLease { + ticks: 0, + expiry: if expires { + Some(SystemTime::now() + self.lease_duration) + } else { + None + }, + }, + ); + + Ok(()) + } + + /// Get the corresponding IPv6 address for a given IPv4 address + pub fn get_ipv6(&self, ipv4: &Ipv4Addr) -> Option { + self.addr_pairs.get_by_left(ipv4).map(|addr| *addr) + } + + /// Get the corresponding IPv4 address for a given IPv6 address + pub fn get_ipv4(&self, ipv6: &Ipv6Addr) -> Option { + self.addr_pairs.get_by_right(ipv6).map(|addr| *addr) + } + + /// "tick" a flow. Allowing it to continue living + pub fn tick_flow(&mut self, ip: &IpAddr) { + // Figure out the whole address pair + let (ipv4, ipv6) = match ip { + IpAddr::V4(ipv4) => { + let ipv6 = self.get_ipv6(ipv4).unwrap(); + (ipv4.clone(), ipv6) + } + IpAddr::V6(ipv6) => { + let ipv4 = self.get_ipv4(ipv6).unwrap(); + (ipv4, ipv6.clone()) + } + }; + + // Get the lease + let lease = self.leases.get_mut(&(ipv4, ipv6)).unwrap(); + + // Increment the tick count + lease.ticks += 1; + + // If the lease has an expiry, update it + if let Some(_) = lease.expiry { + lease.expiry = Some(SystemTime::now() + self.lease_duration); + } + } + + /// Prune any expired leases + pub fn prune(&mut self) { + // Get the current time + let now = SystemTime::now(); + + // Filter out any leases that have expired + self.leases.retain(|_, lease| { + // If the lease has no expiry, keep it + if lease.expiry.is_none() { + return true; + } + + // If the lease has expired, remove it + lease.expiry.unwrap() > now + }); + + // Remove any address pairs that no longer have a lease + self.addr_pairs + .retain(|ipv4, ipv6| self.leases.contains_key(&(*ipv4, *ipv6))); + } + + /// Finds a free IPv4 address within a set of prefixes + pub fn find_free_ipv4(&self, prefixes: &Vec) -> Option { + // Iterate over the prefixes + for prefix in prefixes { + // Iterate over all addresses in the prefix + for addr in prefix.hosts() { + // If the address is not in the table, return it + if !self.addr_pairs.contains_left(&addr) { + return Some(addr); + } + } + } + + None + } +} diff --git a/src/protomask-6over4.rs b/src/protomask-6over4.rs deleted file mode 100644 index fcb9aef..0000000 --- a/src/protomask-6over4.rs +++ /dev/null @@ -1,5 +0,0 @@ -//! This is a placeholder for a future 6over4 implementation -#[tokio::main] -pub async fn main() { - println!("You did it! You found the incomplete binary :)"); -} diff --git a/src/protomask-clat.rs b/src/protomask-clat.rs deleted file mode 100644 index c2ca56a..0000000 --- a/src/protomask-clat.rs +++ /dev/null @@ -1,157 +0,0 @@ -//! 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 crate::common::packet_handler::{ - get_ipv4_src_dst, get_ipv6_src_dst, get_layer_3_proto, handle_translation_error, - PacketHandlingError, -}; -use crate::common::profiler::start_puffin_server; -use crate::{args::protomask_clat::Args, common::permissions::ensure_root}; -use clap::Parser; -use common::logging::enable_logger; -use easy_tun::Tun; -use interproto::protocols::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4}; -use ipnet::{IpNet, Ipv4Net, Ipv6Net}; -use rfc6052::{embed_ipv4_addr_unchecked, extract_ipv4_addr_unchecked}; -use std::io::{Read, Write}; -use std::sync::Arc; - -mod args; -mod common; - -#[tokio::main] -pub async fn main() { - // Parse CLI args - let args = Args::parse(); - - // Initialize logging - enable_logger(args.verbose); - - // Load config data - let config = args.data().unwrap(); - - // We must be root to continue program execution - ensure_root(); - - // Start profiling - #[allow(clippy::let_unit_value)] - let _server = start_puffin_server(&args.profiler_args); - - // Bring up a TUN interface - let tun = Arc::new(Tun::new(&args.interface, config.num_queues).unwrap()); - - // 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 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 config.customer_pool { - let embedded_customer_prefix = unsafe { - Ipv6Net::new( - embed_ipv4_addr_unchecked(customer_prefix.addr(), config.embed_prefix), - config.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(); - } - - // If we are configured to serve prometheus metrics, start the server - if let Some(bind_addr) = config.prom_bind_addr { - log::info!("Starting prometheus server on {}", bind_addr); - tokio::spawn(protomask_metrics::http::serve_metrics(bind_addr)); - } - - // Translate all incoming packets - log::info!("Translating packets on {}", tun.name()); - let mut worker_threads = Vec::new(); - for queue_id in 0..config.num_queues { - let tun = Arc::clone(&tun); - worker_threads.push(std::thread::spawn(move || { - log::debug!("Starting worker thread for queue {}", queue_id); - let mut buffer = vec![0u8; 1500]; - loop { - // Indicate to the profiler that we are starting a new packet - profiling::finish_frame!(); - profiling::scope!("packet"); - - // Read a packet - let len = tun.fd(queue_id).unwrap().read(&mut buffer).unwrap(); - - // Translate it based on the Layer 3 protocol number - let translation_result: Result>, PacketHandlingError> = - match get_layer_3_proto(&buffer[..len]) { - Some(4) => { - let (source, dest) = get_ipv4_src_dst(&buffer[..len]); - translate_ipv4_to_ipv6( - &buffer[..len], - unsafe { embed_ipv4_addr_unchecked(source, config.embed_prefix) }, - unsafe { embed_ipv4_addr_unchecked(dest, config.embed_prefix) }, - ) - .map(Some) - .map_err(PacketHandlingError::from) - } - Some(6) => { - let (source, dest) = get_ipv6_src_dst(&buffer[..len]); - translate_ipv6_to_ipv4( - &buffer[..len], - unsafe { - extract_ipv4_addr_unchecked( - source, - config.embed_prefix.prefix_len(), - ) - }, - unsafe { - extract_ipv4_addr_unchecked( - dest, - config.embed_prefix.prefix_len(), - ) - }, - ) - .map(Some) - .map_err(PacketHandlingError::from) - } - Some(proto) => { - log::warn!("Unknown Layer 3 protocol: {}", proto); - continue; - } - None => { - continue; - } - }; - - // Handle any errors and write - if let Some(output) = handle_translation_error(translation_result) { - tun.fd(queue_id).unwrap().write_all(&output).unwrap(); - } - } - })); - } - for worker in worker_threads { - worker.join().unwrap(); - } -} diff --git a/src/protomask.rs b/src/protomask.rs deleted file mode 100644 index 4ffe108..0000000 --- a/src/protomask.rs +++ /dev/null @@ -1,187 +0,0 @@ -use crate::common::{ - packet_handler::{ - get_ipv4_src_dst, get_ipv6_src_dst, get_layer_3_proto, handle_translation_error, - PacketHandlingError, - }, - permissions::ensure_root, - profiler::start_puffin_server, -}; -use clap::Parser; -use common::logging::enable_logger; -use easy_tun::Tun; -use fast_nat::CrossProtocolNetworkAddressTableWithIpv4Pool; -use interproto::protocols::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4}; -use ipnet::IpNet; -use rfc6052::{embed_ipv4_addr_unchecked, extract_ipv4_addr_unchecked}; -use std::{ - io::{Read, Write}, - sync::{Arc, Mutex}, - time::Duration, -}; - -mod args; -mod common; - -#[tokio::main] -pub async fn main() { - // Parse CLI args - let args = args::protomask::Args::parse(); - - // Initialize logging - enable_logger(args.verbose); - - // Load config data - let config = args.data().unwrap(); - - // We must be root to continue program execution - ensure_root(); - - // Start profiling - #[allow(clippy::let_unit_value)] - let _server = start_puffin_server(&args.profiler_args); - - // Bring up a TUN interface - log::debug!("Creating new TUN interface"); - let tun = Arc::new(Tun::new(&args.interface, config.num_queues).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 {}", - config.translation_prefix, - tun.name() - ); - rtnl::route::route_add( - IpNet::V6(config.translation_prefix), - &rt_handle, - tun_link_idx, - ) - .await - .unwrap(); - - // Add a route for each NAT pool prefix - for pool_prefix in &config.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) - .await - .unwrap(); - } - - // Set up the address table - let addr_table = Arc::new(Mutex::new( - CrossProtocolNetworkAddressTableWithIpv4Pool::new( - &config.pool_prefixes, - Duration::from_secs(config.reservation_timeout), - ), - )); - for (v4_addr, v6_addr) in &config.static_map { - addr_table - .lock() - .unwrap() - .insert_static(*v4_addr, *v6_addr) - .unwrap(); - } - - // If we are configured to serve prometheus metrics, start the server - if let Some(bind_addr) = config.prom_bind_addr { - log::info!("Starting prometheus server on {}", bind_addr); - tokio::spawn(protomask_metrics::http::serve_metrics(bind_addr)); - } - - // Translate all incoming packets - log::info!("Translating packets on {}", tun.name()); - let mut worker_threads = Vec::new(); - for queue_id in 0..config.num_queues { - let tun = Arc::clone(&tun); - let addr_table = Arc::clone(&addr_table); - worker_threads.push(std::thread::spawn(move || { - log::debug!("Starting worker thread for queue {}", queue_id); - - let mut buffer = vec![0u8; 1500]; - loop { - // Indicate to the profiler that we are starting a new packet - profiling::finish_frame!(); - profiling::scope!("packet"); - - // Read a packet - let len = tun.fd(queue_id).unwrap().read(&mut buffer).unwrap(); - - // Translate it based on the Layer 3 protocol number - let translation_result: Result>, PacketHandlingError> = - match get_layer_3_proto(&buffer[..len]) { - Some(4) => { - let (source, dest) = get_ipv4_src_dst(&buffer[..len]); - match addr_table.lock().unwrap().get_ipv6(&dest) { - Some(new_destination) => translate_ipv4_to_ipv6( - &buffer[..len], - unsafe { - embed_ipv4_addr_unchecked(source, config.translation_prefix) - }, - new_destination, - ) - .map(Some) - .map_err(PacketHandlingError::from), - None => { - protomask_metrics::metric!( - PACKET_COUNTER, - PROTOCOL_IPV4, - STATUS_DROPPED - ); - Ok(None) - } - } - } - Some(6) => { - let (source, dest) = get_ipv6_src_dst(&buffer[..len]); - match addr_table.lock().unwrap().get_or_create_ipv4(&source) { - Ok(new_source) => { - translate_ipv6_to_ipv4(&buffer[..len], new_source, unsafe { - extract_ipv4_addr_unchecked( - dest, - config.translation_prefix.prefix_len(), - ) - }) - .map(Some) - .map_err(PacketHandlingError::from) - } - Err(error) => { - log::error!("Error getting IPv4 address: {}", error); - protomask_metrics::metric!( - PACKET_COUNTER, - PROTOCOL_IPV6, - STATUS_DROPPED - ); - Ok(None) - } - } - } - Some(proto) => { - log::warn!("Unknown Layer 3 protocol: {}", proto); - continue; - } - None => { - continue; - } - }; - - // Handle any errors and write - if let Some(output) = handle_translation_error(translation_result) { - tun.fd(queue_id).unwrap().write_all(&output).unwrap(); - } - } - })); - } - for worker in worker_threads { - worker.join().unwrap(); - } -}