big WIP
This commit is contained in:
parent
66552aeccc
commit
19c8499861
62
Cargo.toml
62
Cargo.toml
@ -16,61 +16,44 @@ exclude = ["/.github/", "/.vscode/"]
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"libs/easy-tun",
|
"libs/easy-tun",
|
||||||
"libs/fast-nat",
|
# "libs/fast-nat",
|
||||||
"libs/interproto",
|
"libs/interproto",
|
||||||
"libs/rfc6052",
|
"libs/rfc6052",
|
||||||
"libs/rtnl",
|
"libs/rtnl",
|
||||||
"libs/protomask-metrics",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
profiler = [
|
|
||||||
"puffin",
|
|
||||||
"puffin_http",
|
|
||||||
"easy-tun/profile-puffin",
|
|
||||||
"fast-nat/profile-puffin",
|
|
||||||
"interproto/profile-puffin",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "protomask"
|
name = "protomask"
|
||||||
path = "src/protomask.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "protomask-clat"
|
|
||||||
path = "src/protomask-clat.rs"
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "protomask-6over4"
|
|
||||||
path = "src/protomask-6over4.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
easy-tun = { version = "^2.0.0", path = "libs/easy-tun" }
|
easy-tun = { version = "^2.0.0", path = "libs/easy-tun" }
|
||||||
fast-nat = { version = "^1.0.0", path = "libs/fast-nat" }
|
fast-nat = { version = "^1.0.0", path = "libs/fast-nat" }
|
||||||
interproto = { version = "^1.0.0", path = "libs/interproto", features = [
|
interproto = { version = "^1.0.0", path = "libs/interproto" }
|
||||||
"metrics",
|
|
||||||
] }
|
|
||||||
rfc6052 = { version = "^1.0.0", path = "libs/rfc6052" }
|
rfc6052 = { version = "^1.0.0", path = "libs/rfc6052" }
|
||||||
rtnl = { version = "^1.0.0", path = "libs/rtnl", features = ["tokio"] }
|
rtnl = { version = "^1.0.0", path = "libs/rtnl", features = ["tokio"] }
|
||||||
protomask-metrics = { version = "^0.1.0", path = "libs/protomask-metrics" }
|
|
||||||
|
|
||||||
# External Dependencies
|
# External Dependencies
|
||||||
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] }
|
||||||
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
||||||
clap = { version = "4.3.11", features = ["derive"] }
|
clap = { version = "4.3.11", features = ["derive"] }
|
||||||
ipnet = { version = "2.8.0", features = ["serde"] }
|
ipnet = { version = "2.8.0", features = ["serde"] }
|
||||||
puffin_http = { version = "0.13.0", optional = true }
|
# puffin_http = { version = "0.13.0", optional = true }
|
||||||
puffin = { version = "0.16.0", optional = true }
|
# puffin = { version = "0.16.0", optional = true }
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
serde_json = "^1.0"
|
serde_json = "^1.0"
|
||||||
log = "0.4.19"
|
log = "0.4.19"
|
||||||
fern = "0.6.2"
|
fern = "0.6.2"
|
||||||
nix = "0.26.2"
|
# nix = "0.26.2"
|
||||||
thiserror = "1.0.44"
|
thiserror = "1.0.44"
|
||||||
cfg-if = "1.0.0"
|
# cfg-if = "1.0.0"
|
||||||
profiling = "1.0.9"
|
# profiling = "1.0.9"
|
||||||
|
caps = "0.5.5"
|
||||||
|
bimap = "0.6.3"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
@ -84,21 +67,11 @@ assets = [
|
|||||||
"/usr/local/bin/protomask",
|
"/usr/local/bin/protomask",
|
||||||
"755",
|
"755",
|
||||||
],
|
],
|
||||||
[
|
|
||||||
"target/release/protomask-clat",
|
|
||||||
"/usr/local/bin/protomask-clat",
|
|
||||||
"755",
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"config/protomask.json",
|
"config/protomask.json",
|
||||||
"/etc/protomask/protomask.json",
|
"/etc/protomask/protomask.json",
|
||||||
"644",
|
"644",
|
||||||
],
|
],
|
||||||
[
|
|
||||||
"config/protomask-clat.json",
|
|
||||||
"/etc/protomask/protomask-clat.json",
|
|
||||||
"644",
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"README.md",
|
"README.md",
|
||||||
"/usr/share/doc/protomask/README.md",
|
"/usr/share/doc/protomask/README.md",
|
||||||
@ -108,16 +81,11 @@ assets = [
|
|||||||
conf-files = []
|
conf-files = []
|
||||||
depends = []
|
depends = []
|
||||||
maintainer-scripts = "./debian/"
|
maintainer-scripts = "./debian/"
|
||||||
systemd-units = [
|
systemd-units = [{ unit-name = "protomask-service", enable = false }]
|
||||||
{ unit-name = "protomask-service", enable = false },
|
|
||||||
{ unit-name = "protomask-clat-service", enable = false },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata.generate-rpm]
|
[package.metadata.generate-rpm]
|
||||||
assets = [
|
assets = [
|
||||||
{ source = "target/release/protomask", dest = "/usr/local/bin/protomask", mode = "755"},
|
{ 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.json", dest = "/etc/protomask/protomask.json", mode = "644"},
|
{ source = "README.md", dest = "/usr/share/doc/protomask/README.md", 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"},
|
|
||||||
]
|
|
||||||
|
9
debian/protomask-clat-service
vendored
9
debian/protomask-clat-service
vendored
@ -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
|
|
@ -85,8 +85,9 @@ impl Tun {
|
|||||||
|
|
||||||
// Check for errors
|
// Check for errors
|
||||||
if err < 0 {
|
if err < 0 {
|
||||||
log::error!("ioctl failed: {}", err);
|
let last_error = std::io::Error::last_os_error();
|
||||||
return Err(std::io::Error::last_os_error());
|
log::error!("ioctl failed: {:?}", last_error);
|
||||||
|
return Err(last_error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,11 +14,9 @@ categories = []
|
|||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
metrics = ["protomask-metrics"]
|
|
||||||
profile-puffin = ["profiling/profile-with-puffin"]
|
profile-puffin = ["profiling/profile-with-puffin"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
protomask-metrics = { version = "^0.1.0", path = "../protomask-metrics", optional = true }
|
|
||||||
log = "^0.4"
|
log = "^0.4"
|
||||||
pnet = "0.34.0"
|
pnet = "0.34.0"
|
||||||
thiserror = "^1.0.44"
|
thiserror = "^1.0.44"
|
||||||
|
@ -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"
|
|
@ -1 +0,0 @@
|
|||||||
**`protomask-metrics` is exclusively for use in `protomask` and is not intended to be used on its own.**
|
|
@ -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}");
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
@ -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),+])
|
|
||||||
};
|
|
||||||
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
102
src/cli.rs
Normal 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))
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
@ -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;
|
|
@ -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
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// }
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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){}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
9
src/engines/clat.rs
Normal 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
3
src/engines/mod.rs
Normal 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
246
src/engines/nat64.rs
Normal 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
101
src/main.rs
Normal 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
153
src/nat.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
@ -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 :)");
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
187
src/protomask.rs
187
src/protomask.rs
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user