From 7d15bdc96e8f79c493ee69244e8ab1645676c736 Mon Sep 17 00:00:00 2001 From: Evan Pratten Date: Fri, 4 Aug 2023 20:15:06 -0400 Subject: [PATCH] Migrate to allowing config files --- Cargo.toml | 4 +- libs/easy-tun/src/tun.rs | 5 ++ src/args/mod.rs | 4 ++ src/args/protomask.rs | 92 +++++++++++++++++++++++++++ src/args/protomask_clat.rs | 81 ++++++++++++++++++++++++ src/common/mod.rs | 1 + src/common/permissions.rs | 9 +++ src/protomask-clat.rs | 65 ++++++------------- src/protomask.rs | 124 ++++++++----------------------------- 9 files changed, 240 insertions(+), 145 deletions(-) create mode 100644 src/args/mod.rs create mode 100644 src/args/protomask.rs create mode 100644 src/args/protomask_clat.rs create mode 100644 src/common/permissions.rs diff --git a/Cargo.toml b/Cargo.toml index d14347b..0dced11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,9 +75,11 @@ protomask-metrics = { path = "libs/protomask-metrics" } 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"] } +serde = { version = "^1.0", features = ["derive"] } +serde_json = "^1.0" log = "0.4.19" fern = "0.6.2" -ipnet = "2.8.0" nix = "0.26.2" thiserror = "1.0.44" diff --git a/libs/easy-tun/src/tun.rs b/libs/easy-tun/src/tun.rs index 245329d..1aef2a6 100644 --- a/libs/easy-tun/src/tun.rs +++ b/libs/easy-tun/src/tun.rs @@ -36,6 +36,8 @@ impl Tun { #[allow(clippy::cast_possible_truncation)] #[allow(clippy::cast_lossless)] pub fn new(dev: &str) -> Result { + log::debug!("Creating new TUN device with requested name:{}", dev); + // Get a file descriptor for `/dev/net/tun` log::trace!("Opening /dev/net/tun"); let fd = OpenOptions::new() @@ -82,6 +84,9 @@ impl Tun { .unwrap() .to_string(); + // Log the success + log::debug!("Created TUN device: {}", name); + // Build the TUN struct Ok(Self { fd, name }) } diff --git a/src/args/mod.rs b/src/args/mod.rs new file mode 100644 index 0000000..c83d492 --- /dev/null +++ b/src/args/mod.rs @@ -0,0 +1,4 @@ +//! This module contains the definitions for each binary's CLI arguments and config file structure for the sake of readability. + +pub mod protomask_clat; +pub mod protomask; diff --git a/src/args/protomask.rs b/src/args/protomask.rs new file mode 100644 index 0000000..92256a3 --- /dev/null +++ b/src/args/protomask.rs @@ -0,0 +1,92 @@ +use std::{ + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, + path::PathBuf, +}; + +use ipnet::{Ipv4Net, Ipv6Net}; + +use crate::common::rfc6052::parse_network_specific_prefix; + +#[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, + + /// 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, +} diff --git a/src/args/protomask_clat.rs b/src/args/protomask_clat.rs new file mode 100644 index 0000000..8fa352b --- /dev/null +++ b/src/args/protomask_clat.rs @@ -0,0 +1,81 @@ +//! Commandline arguments and config file definitions for `protomask-clat` + +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, + + /// 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, +} diff --git a/src/common/mod.rs b/src/common/mod.rs index bdc0db4..23af3f1 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -3,3 +3,4 @@ pub mod logging; pub mod packet_handler; pub mod rfc6052; +pub mod permissions; \ No newline at end of file diff --git a/src/common/permissions.rs b/src/common/permissions.rs new file mode 100644 index 0000000..54c17c8 --- /dev/null +++ b/src/common/permissions.rs @@ -0,0 +1,9 @@ +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/protomask-clat.rs b/src/protomask-clat.rs index b3a866c..8983cb7 100644 --- a/src/protomask-clat.rs +++ b/src/protomask-clat.rs @@ -3,46 +3,19 @@ //! 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::handle_packet; +use crate::{args::protomask_clat::Args, common::permissions::ensure_root}; use clap::Parser; -use common::{logging::enable_logger, rfc6052::parse_network_specific_prefix}; +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 nix::unistd::Uid; use rfc6052::{embed_ipv4_addr_unchecked, extract_ipv4_addr_unchecked}; -use std::{ - io::{Read, Write}, - net::SocketAddr, -}; - -use crate::common::packet_handler::handle_packet; +use std::io::{Read, Write}; +mod args; mod common; -#[derive(Debug, Parser)] -#[clap(author, version, about="IPv4 to IPv6 Customer-side transLATor (CLAT)", long_about = None)] -struct Args { - /// One or more customer-side IPv4 prefixes to allow through CLAT - #[clap(short = 'c', long = "customer-prefix", required = true)] - customer_pool: Vec, - - /// Enable prometheus metrics on a given address - #[clap(long = "prometheus")] - 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)] - 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 @@ -51,16 +24,14 @@ pub async fn main() { // Initialize logging enable_logger(args.verbose); + // Load config data + let config = args.data().unwrap(); + // 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); - } + ensure_root(); // 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(); @@ -78,11 +49,11 @@ pub async fn main() { .unwrap(); // Add an IPv6 route for each customer prefix - for customer_prefix in args.customer_pool { + for customer_prefix in config.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(), + embed_ipv4_addr_unchecked(customer_prefix.addr(), config.embed_prefix), + config.embed_prefix.prefix_len() + customer_prefix.prefix_len(), ) .unwrap_unchecked() }; @@ -101,7 +72,7 @@ pub async fn main() { } // If we are configured to serve prometheus metrics, start the server - if let Some(bind_addr) = args.prom_bind_addr { + 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)); } @@ -120,8 +91,8 @@ pub async fn main() { |packet, source, dest| { Ok(translate_ipv4_to_ipv6( packet, - unsafe { embed_ipv4_addr_unchecked(*source, args.embed_prefix) }, - unsafe { embed_ipv4_addr_unchecked(*dest, args.embed_prefix) }, + unsafe { embed_ipv4_addr_unchecked(*source, config.embed_prefix) }, + unsafe { embed_ipv4_addr_unchecked(*dest, config.embed_prefix) }, ) .map(Some)?) }, @@ -129,8 +100,10 @@ pub async fn main() { |packet, source, dest| { 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()) }, + unsafe { + extract_ipv4_addr_unchecked(*source, config.embed_prefix.prefix_len()) + }, + unsafe { extract_ipv4_addr_unchecked(*dest, config.embed_prefix.prefix_len()) }, ) .map(Some)?) }, diff --git a/src/protomask.rs b/src/protomask.rs index de05a15..35ec8f7 100644 --- a/src/protomask.rs +++ b/src/protomask.rs @@ -1,108 +1,33 @@ +use crate::common::{packet_handler::handle_packet, permissions::ensure_root}; use clap::Parser; -use common::{logging::enable_logger, rfc6052::parse_network_specific_prefix}; +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, Ipv4Net, Ipv6Net}; -use nix::unistd::Uid; +use ipnet::IpNet; use rfc6052::{embed_ipv4_addr_unchecked, extract_ipv4_addr_unchecked}; use std::{ cell::RefCell, - io::{BufRead, Read, Write}, - net::{Ipv4Addr, Ipv6Addr, SocketAddr}, - path::PathBuf, + io::{Read, Write}, time::Duration, }; -use crate::common::packet_handler::handle_packet; - +mod args; mod common; -#[derive(Parser)] -#[clap(author, version, about="Fast and simple NAT64", long_about = None)] -struct Args { - #[command(flatten)] - pool: PoolArgs, - - /// A CSV file containing static address mappings from IPv6 to IPv4 - #[clap(long = "static-file")] - static_file: Option, - - /// Enable prometheus metrics on a given address - #[clap(long = "prometheus")] - prom_bind_addr: Option, - - /// RFC6052 IPv6 translation prefix - #[clap(long, default_value_t = ("64:ff9b::/96").parse().unwrap(), value_parser = parse_network_specific_prefix)] - translation_prefix: Ipv6Net, - - /// 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, -} - -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 { - /// 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 { - /// Read all pool prefixes from the chosen source - pub fn prefixes(&self) -> Result, Box> { - match !self.pool_prefixes.is_empty() { - 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) - } - } - } -} - #[tokio::main] pub async fn main() { // Parse CLI args - let args = Args::parse(); + 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 - if !Uid::effective().is_root() { - log::error!("This program must be run as root"); - std::process::exit(1); - } + ensure_root(); // Bring up a TUN interface log::debug!("Creating new TUN interface"); @@ -122,16 +47,19 @@ pub async fn main() { // Add a route for the translation prefix log::debug!( "Adding route for {} to {}", - args.translation_prefix, + config.translation_prefix, tun.name() ); - rtnl::route::route_add(IpNet::V6(args.translation_prefix), &rt_handle, tun_link_idx) - .await - .unwrap(); + rtnl::route::route_add( + IpNet::V6(config.translation_prefix), + &rt_handle, + tun_link_idx, + ) + .await + .unwrap(); // Add a route for each NAT pool prefix - let pool_prefixes = args.pool.prefixes().unwrap(); - for pool_prefix in &pool_prefixes { + 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 @@ -140,18 +68,18 @@ pub async fn main() { // Set up the address table let mut addr_table = RefCell::new(CrossProtocolNetworkAddressTableWithIpv4Pool::new( - &pool_prefixes, - Duration::from_secs(args.reservation_timeout), + &config.pool_prefixes, + Duration::from_secs(config.reservation_timeout), )); - for (v6_addr, v4_addr) in args.get_static_reservations().unwrap() { + for (v4_addr, v6_addr) in &config.static_map { addr_table .get_mut() - .insert_static(v4_addr, v6_addr) + .insert_static(*v4_addr, *v6_addr) .unwrap(); } // If we are configured to serve prometheus metrics, start the server - if let Some(bind_addr) = args.prom_bind_addr { + 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)); } @@ -170,7 +98,7 @@ pub async fn main() { |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) }, + unsafe { embed_ipv4_addr_unchecked(*source, config.translation_prefix) }, new_destination, ) .map(Some)?), @@ -185,7 +113,7 @@ pub async fn main() { packet, addr_table.borrow_mut().get_or_create_ipv4(source)?, unsafe { - extract_ipv4_addr_unchecked(*dest, args.translation_prefix.prefix_len()) + extract_ipv4_addr_unchecked(*dest, config.translation_prefix.prefix_len()) }, ) .map(Some)?)