Migrate to allowing config files
This commit is contained in:
parent
2486aac690
commit
7d15bdc96e
@ -75,9 +75,11 @@ protomask-metrics = { path = "libs/protomask-metrics" }
|
||||
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] }
|
||||
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
|
||||
clap = { version = "4.3.11", features = ["derive"] }
|
||||
ipnet = { version = "2.8.0", features = ["serde"] }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = "^1.0"
|
||||
log = "0.4.19"
|
||||
fern = "0.6.2"
|
||||
ipnet = "2.8.0"
|
||||
nix = "0.26.2"
|
||||
thiserror = "1.0.44"
|
||||
|
||||
|
@ -36,6 +36,8 @@ impl Tun {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
#[allow(clippy::cast_lossless)]
|
||||
pub fn new(dev: &str) -> Result<Self, std::io::Error> {
|
||||
log::debug!("Creating new TUN device with requested name:{}", dev);
|
||||
|
||||
// Get a file descriptor for `/dev/net/tun`
|
||||
log::trace!("Opening /dev/net/tun");
|
||||
let fd = OpenOptions::new()
|
||||
@ -82,6 +84,9 @@ impl Tun {
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
// Log the success
|
||||
log::debug!("Created TUN device: {}", name);
|
||||
|
||||
// Build the TUN struct
|
||||
Ok(Self { fd, name })
|
||||
}
|
||||
|
4
src/args/mod.rs
Normal file
4
src/args/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
//! This module contains the definitions for each binary's CLI arguments and config file structure for the sake of readability.
|
||||
|
||||
pub mod protomask_clat;
|
||||
pub mod protomask;
|
92
src/args/protomask.rs
Normal file
92
src/args/protomask.rs
Normal file
@ -0,0 +1,92 @@
|
||||
use std::{
|
||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use ipnet::{Ipv4Net, Ipv6Net};
|
||||
|
||||
use crate::common::rfc6052::parse_network_specific_prefix;
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
#[clap(author, version, about="Fast and simple NAT64", long_about = None)]
|
||||
pub struct Args {
|
||||
#[command(flatten)]
|
||||
config_data: Option<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,
|
||||
|
||||
/// 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,
|
||||
}
|
81
src/args/protomask_clat.rs
Normal file
81
src/args/protomask_clat.rs
Normal file
@ -0,0 +1,81 @@
|
||||
//! Commandline arguments and config file definitions for `protomask-clat`
|
||||
|
||||
use crate::common::rfc6052::parse_network_specific_prefix;
|
||||
use ipnet::{Ipv4Net, Ipv6Net};
|
||||
use std::{net::SocketAddr, path::PathBuf};
|
||||
|
||||
#[derive(Debug, clap::Parser)]
|
||||
#[clap(author, version, about="IPv4 to IPv6 Customer-side transLATor (CLAT)", long_about = None)]
|
||||
pub struct Args {
|
||||
#[command(flatten)]
|
||||
config_data: Option<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,
|
||||
|
||||
/// 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,
|
||||
}
|
@ -3,3 +3,4 @@
|
||||
pub mod logging;
|
||||
pub mod packet_handler;
|
||||
pub mod rfc6052;
|
||||
pub mod permissions;
|
9
src/common/permissions.rs
Normal file
9
src/common/permissions.rs
Normal file
@ -0,0 +1,9 @@
|
||||
use nix::unistd::Uid;
|
||||
|
||||
/// Ensures the binary is being exxecuted as root
|
||||
pub fn ensure_root() {
|
||||
if !Uid::effective().is_root() {
|
||||
log::error!("This program must be run as root");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
@ -3,46 +3,19 @@
|
||||
//! This binary is a Customer-side transLATor (CLAT) that translates all native
|
||||
//! IPv4 traffic to IPv6 traffic for transmission over an IPv6-only ISP network.
|
||||
|
||||
use crate::common::packet_handler::handle_packet;
|
||||
use crate::{args::protomask_clat::Args, common::permissions::ensure_root};
|
||||
use clap::Parser;
|
||||
use common::{logging::enable_logger, rfc6052::parse_network_specific_prefix};
|
||||
use common::logging::enable_logger;
|
||||
use easy_tun::Tun;
|
||||
use interproto::protocols::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4};
|
||||
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
|
||||
use nix::unistd::Uid;
|
||||
use rfc6052::{embed_ipv4_addr_unchecked, extract_ipv4_addr_unchecked};
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
net::SocketAddr,
|
||||
};
|
||||
|
||||
use crate::common::packet_handler::handle_packet;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
mod args;
|
||||
mod common;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(author, version, about="IPv4 to IPv6 Customer-side transLATor (CLAT)", long_about = None)]
|
||||
struct Args {
|
||||
/// One or more customer-side IPv4 prefixes to allow through CLAT
|
||||
#[clap(short = 'c', long = "customer-prefix", required = true)]
|
||||
customer_pool: Vec<Ipv4Net>,
|
||||
|
||||
/// Enable prometheus metrics on a given address
|
||||
#[clap(long = "prometheus")]
|
||||
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)]
|
||||
embed_prefix: Ipv6Net,
|
||||
|
||||
/// Explicitly set the interface name to use
|
||||
#[clap(short, long, default_value_t = ("clat%d").to_string())]
|
||||
interface: String,
|
||||
|
||||
/// Enable verbose logging
|
||||
#[clap(short, long)]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() {
|
||||
// Parse CLI args
|
||||
@ -51,16 +24,14 @@ pub async fn main() {
|
||||
// Initialize logging
|
||||
enable_logger(args.verbose);
|
||||
|
||||
// Load config data
|
||||
let config = args.data().unwrap();
|
||||
|
||||
// We must be root to continue program execution
|
||||
if !Uid::effective().is_root() {
|
||||
log::error!("This program must be run as root");
|
||||
std::process::exit(1);
|
||||
}
|
||||
ensure_root();
|
||||
|
||||
// Bring up a TUN interface
|
||||
log::debug!("Creating new TUN interface");
|
||||
let mut tun = Tun::new(&args.interface).unwrap();
|
||||
log::debug!("Created TUN interface: {}", tun.name());
|
||||
|
||||
// Get the interface index
|
||||
let rt_handle = rtnl::new_handle().unwrap();
|
||||
@ -78,11 +49,11 @@ pub async fn main() {
|
||||
.unwrap();
|
||||
|
||||
// Add an IPv6 route for each customer prefix
|
||||
for customer_prefix in args.customer_pool {
|
||||
for customer_prefix in config.customer_pool {
|
||||
let embedded_customer_prefix = unsafe {
|
||||
Ipv6Net::new(
|
||||
embed_ipv4_addr_unchecked(customer_prefix.addr(), args.embed_prefix),
|
||||
args.embed_prefix.prefix_len() + customer_prefix.prefix_len(),
|
||||
embed_ipv4_addr_unchecked(customer_prefix.addr(), config.embed_prefix),
|
||||
config.embed_prefix.prefix_len() + customer_prefix.prefix_len(),
|
||||
)
|
||||
.unwrap_unchecked()
|
||||
};
|
||||
@ -101,7 +72,7 @@ pub async fn main() {
|
||||
}
|
||||
|
||||
// If we are configured to serve prometheus metrics, start the server
|
||||
if let Some(bind_addr) = args.prom_bind_addr {
|
||||
if let Some(bind_addr) = config.prom_bind_addr {
|
||||
log::info!("Starting prometheus server on {}", bind_addr);
|
||||
tokio::spawn(protomask_metrics::http::serve_metrics(bind_addr));
|
||||
}
|
||||
@ -120,8 +91,8 @@ pub async fn main() {
|
||||
|packet, source, dest| {
|
||||
Ok(translate_ipv4_to_ipv6(
|
||||
packet,
|
||||
unsafe { embed_ipv4_addr_unchecked(*source, args.embed_prefix) },
|
||||
unsafe { embed_ipv4_addr_unchecked(*dest, args.embed_prefix) },
|
||||
unsafe { embed_ipv4_addr_unchecked(*source, config.embed_prefix) },
|
||||
unsafe { embed_ipv4_addr_unchecked(*dest, config.embed_prefix) },
|
||||
)
|
||||
.map(Some)?)
|
||||
},
|
||||
@ -129,8 +100,10 @@ pub async fn main() {
|
||||
|packet, source, dest| {
|
||||
Ok(translate_ipv6_to_ipv4(
|
||||
packet,
|
||||
unsafe { extract_ipv4_addr_unchecked(*source, args.embed_prefix.prefix_len()) },
|
||||
unsafe { extract_ipv4_addr_unchecked(*dest, args.embed_prefix.prefix_len()) },
|
||||
unsafe {
|
||||
extract_ipv4_addr_unchecked(*source, config.embed_prefix.prefix_len())
|
||||
},
|
||||
unsafe { extract_ipv4_addr_unchecked(*dest, config.embed_prefix.prefix_len()) },
|
||||
)
|
||||
.map(Some)?)
|
||||
},
|
||||
|
124
src/protomask.rs
124
src/protomask.rs
@ -1,108 +1,33 @@
|
||||
use crate::common::{packet_handler::handle_packet, permissions::ensure_root};
|
||||
use clap::Parser;
|
||||
use common::{logging::enable_logger, rfc6052::parse_network_specific_prefix};
|
||||
use common::logging::enable_logger;
|
||||
use easy_tun::Tun;
|
||||
use fast_nat::CrossProtocolNetworkAddressTableWithIpv4Pool;
|
||||
use interproto::protocols::ip::{translate_ipv4_to_ipv6, translate_ipv6_to_ipv4};
|
||||
use ipnet::{IpNet, Ipv4Net, Ipv6Net};
|
||||
use nix::unistd::Uid;
|
||||
use ipnet::IpNet;
|
||||
use rfc6052::{embed_ipv4_addr_unchecked, extract_ipv4_addr_unchecked};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
io::{BufRead, Read, Write},
|
||||
net::{Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
path::PathBuf,
|
||||
io::{Read, Write},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::common::packet_handler::handle_packet;
|
||||
|
||||
mod args;
|
||||
mod common;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, version, about="Fast and simple NAT64", long_about = None)]
|
||||
struct Args {
|
||||
#[command(flatten)]
|
||||
pool: PoolArgs,
|
||||
|
||||
/// A CSV file containing static address mappings from IPv6 to IPv4
|
||||
#[clap(long = "static-file")]
|
||||
static_file: Option<PathBuf>,
|
||||
|
||||
/// Enable prometheus metrics on a given address
|
||||
#[clap(long = "prometheus")]
|
||||
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)]
|
||||
translation_prefix: Ipv6Net,
|
||||
|
||||
/// NAT reservation timeout in seconds
|
||||
#[clap(long, default_value = "7200")]
|
||||
reservation_timeout: u64,
|
||||
|
||||
/// Explicitly set the interface name to use
|
||||
#[clap(short, long, default_value_t = ("nat%d").to_string())]
|
||||
interface: String,
|
||||
|
||||
/// Enable verbose logging
|
||||
#[clap(short, long)]
|
||||
verbose: bool,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
pub fn get_static_reservations(
|
||||
&self,
|
||||
) -> Result<Vec<(Ipv6Addr, Ipv4Addr)>, Box<dyn std::error::Error>> {
|
||||
log::warn!("Static reservations are not yet implemented");
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::Args)]
|
||||
#[group(required = true, multiple = false)]
|
||||
struct PoolArgs {
|
||||
/// IPv4 prefixes to use as NAT pool address space
|
||||
#[clap(long = "pool-add")]
|
||||
pool_prefixes: Vec<Ipv4Net>,
|
||||
|
||||
/// A file containing newline-delimited IPv4 prefixes to use as NAT pool address space
|
||||
#[clap(long = "pool-file", conflicts_with = "pool_prefixes")]
|
||||
pool_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl PoolArgs {
|
||||
/// Read all pool prefixes from the chosen source
|
||||
pub fn prefixes(&self) -> Result<Vec<Ipv4Net>, Box<dyn std::error::Error>> {
|
||||
match !self.pool_prefixes.is_empty() {
|
||||
true => Ok(self.pool_prefixes.clone()),
|
||||
false => {
|
||||
let mut prefixes = Vec::new();
|
||||
let file = std::fs::File::open(self.pool_file.as_ref().unwrap())?;
|
||||
let reader = std::io::BufReader::new(file);
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
let prefix = line.parse::<Ipv4Net>()?;
|
||||
prefixes.push(prefix);
|
||||
}
|
||||
Ok(prefixes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
pub async fn main() {
|
||||
// Parse CLI args
|
||||
let args = Args::parse();
|
||||
let args = args::protomask::Args::parse();
|
||||
|
||||
// Initialize logging
|
||||
enable_logger(args.verbose);
|
||||
|
||||
// Load config data
|
||||
let config = args.data().unwrap();
|
||||
|
||||
// We must be root to continue program execution
|
||||
if !Uid::effective().is_root() {
|
||||
log::error!("This program must be run as root");
|
||||
std::process::exit(1);
|
||||
}
|
||||
ensure_root();
|
||||
|
||||
// Bring up a TUN interface
|
||||
log::debug!("Creating new TUN interface");
|
||||
@ -122,16 +47,19 @@ pub async fn main() {
|
||||
// Add a route for the translation prefix
|
||||
log::debug!(
|
||||
"Adding route for {} to {}",
|
||||
args.translation_prefix,
|
||||
config.translation_prefix,
|
||||
tun.name()
|
||||
);
|
||||
rtnl::route::route_add(IpNet::V6(args.translation_prefix), &rt_handle, tun_link_idx)
|
||||
.await
|
||||
.unwrap();
|
||||
rtnl::route::route_add(
|
||||
IpNet::V6(config.translation_prefix),
|
||||
&rt_handle,
|
||||
tun_link_idx,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add a route for each NAT pool prefix
|
||||
let pool_prefixes = args.pool.prefixes().unwrap();
|
||||
for pool_prefix in &pool_prefixes {
|
||||
for pool_prefix in &config.pool_prefixes {
|
||||
log::debug!("Adding route for {} to {}", pool_prefix, tun.name());
|
||||
rtnl::route::route_add(IpNet::V4(*pool_prefix), &rt_handle, tun_link_idx)
|
||||
.await
|
||||
@ -140,18 +68,18 @@ pub async fn main() {
|
||||
|
||||
// Set up the address table
|
||||
let mut addr_table = RefCell::new(CrossProtocolNetworkAddressTableWithIpv4Pool::new(
|
||||
&pool_prefixes,
|
||||
Duration::from_secs(args.reservation_timeout),
|
||||
&config.pool_prefixes,
|
||||
Duration::from_secs(config.reservation_timeout),
|
||||
));
|
||||
for (v6_addr, v4_addr) in args.get_static_reservations().unwrap() {
|
||||
for (v4_addr, v6_addr) in &config.static_map {
|
||||
addr_table
|
||||
.get_mut()
|
||||
.insert_static(v4_addr, v6_addr)
|
||||
.insert_static(*v4_addr, *v6_addr)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// If we are configured to serve prometheus metrics, start the server
|
||||
if let Some(bind_addr) = args.prom_bind_addr {
|
||||
if let Some(bind_addr) = config.prom_bind_addr {
|
||||
log::info!("Starting prometheus server on {}", bind_addr);
|
||||
tokio::spawn(protomask_metrics::http::serve_metrics(bind_addr));
|
||||
}
|
||||
@ -170,7 +98,7 @@ pub async fn main() {
|
||||
|packet, source, dest| match addr_table.borrow().get_ipv6(dest) {
|
||||
Some(new_destination) => Ok(translate_ipv4_to_ipv6(
|
||||
packet,
|
||||
unsafe { embed_ipv4_addr_unchecked(*source, args.translation_prefix) },
|
||||
unsafe { embed_ipv4_addr_unchecked(*source, config.translation_prefix) },
|
||||
new_destination,
|
||||
)
|
||||
.map(Some)?),
|
||||
@ -185,7 +113,7 @@ pub async fn main() {
|
||||
packet,
|
||||
addr_table.borrow_mut().get_or_create_ipv4(source)?,
|
||||
unsafe {
|
||||
extract_ipv4_addr_unchecked(*dest, args.translation_prefix.prefix_len())
|
||||
extract_ipv4_addr_unchecked(*dest, config.translation_prefix.prefix_len())
|
||||
},
|
||||
)
|
||||
.map(Some)?)
|
||||
|
Loading…
x
Reference in New Issue
Block a user