1
This commit is contained in:
Evan Pratten 2024-05-20 11:28:44 -04:00
parent 66552aeccc
commit 19c8499861
28 changed files with 632 additions and 1045 deletions

View File

@ -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"},
]
{ 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" },
]

View File

@ -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

View File

@ -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);
}
}

View File

@ -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"

View File

@ -1,19 +0,0 @@
[package]
name = "protomask-metrics"
version = "0.1.0"
authors = ["Evan Pratten <ewpratten@gmail.com>"]
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"

View File

@ -1 +0,0 @@
**`protomask-metrics` is exclusively for use in `protomask` and is not intended to be used on its own.**

View File

@ -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<Body>) -> Result<Response<Body>, 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}");
}
}

View File

@ -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;

View File

@ -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),+])
};
}

View File

@ -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();
}

View File

@ -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<std::net::SocketAddr>,
}
} else {
#[derive(Debug, clap::Args)]
pub struct ProfilerArgs;
}
}

View File

@ -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<Config>,
/// Path to a config file to read
#[clap(short = 'c', long = "config", conflicts_with = "Config")]
config_file: Option<PathBuf>,
/// 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<Config, Box<dyn std::error::Error>> {
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<Ipv4Net>,
/// 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<SocketAddr>,
/// 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<StaticMap> for (Ipv4Addr, Ipv6Addr) {
fn from(val: StaticMap) -> Self {
(val.ipv4, val.ipv6)
}
}

View File

@ -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<Config>,
/// Path to a config file to read
#[clap(short = 'c', long = "config", conflicts_with = "Config")]
config_file: Option<PathBuf>,
/// 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<Config, Box<dyn std::error::Error>> {
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<Ipv4Net>,
/// Enable prometheus metrics on a given address
#[clap(long = "prometheus")]
#[serde(rename = "prometheus_bind_addr")]
pub prom_bind_addr: Option<SocketAddr>,
/// 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,
}

102
src/cli.rs Normal file
View File

@ -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<Ipv4Net>,
/// 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<Ipv4Net>,
/// 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<Ipv6Net, String> {
// 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))
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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<u8> {
// 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<Option<Vec<u8>>, PacketHandlingError>,
) -> Option<Vec<u8>> {
// 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<Ipv4Handler, Ipv6Handler>(
// packet: &[u8],
// mut ipv4_handler: Ipv4Handler,
// mut ipv6_handler: Ipv6Handler,
// ) -> Option<Vec<u8>>
// where
// Ipv4Handler: FnMut(&[u8], &Ipv4Addr, &Ipv4Addr) -> Result<Option<Vec<u8>>, PacketHandlingError>,
// Ipv6Handler: FnMut(&[u8], &Ipv6Addr, &Ipv6Addr) -> Result<Option<Vec<u8>>, 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
// }
// },
// }
// }

View File

@ -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);
}
}

View File

@ -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<puffin_http::Server> {
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){}
}
}

View File

@ -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<Ipv6Net, String> {
// 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)
}

9
src/engines/clat.rs Normal file
View File

@ -0,0 +1,9 @@
use ipnet::{Ipv4Net, Ipv6Net};
pub async fn do_clat(
interface: String,
customer_pool: Vec<Ipv4Net>,
embed_prefix: Ipv6Net,
num_queues: usize,
) {
}

3
src/engines/mod.rs Normal file
View File

@ -0,0 +1,3 @@
//! This module contains each of the "translation engines" that power protomask
pub mod nat64;
pub mod clat;

246
src/engines/nat64.rs Normal file
View File

@ -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<Ipv4Net>,
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();
}
}

101
src/main.rs Normal file
View File

@ -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.");
}

153
src/nat.rs Normal file
View File

@ -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<SystemTime>,
}
pub struct NetworkAddressTranslationTable {
/// Mapping between addresses
addr_pairs: BiHashMap<Ipv4Addr, Ipv6Addr>,
/// 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<Ipv6Addr> {
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<Ipv4Addr> {
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<Ipv4Net>) -> Option<Ipv4Addr> {
// 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
}
}

View File

@ -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 :)");
}

View File

@ -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<Option<Vec<u8>>, 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();
}
}

View File

@ -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<Option<Vec<u8>>, 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();
}
}