diff --git a/Cargo.toml b/Cargo.toml index 0be7e42..70986e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ categories = [] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] } +tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread", "process"] } clap = { version = "4.3.11", features = ["derive"] } serde = { version = "1.0.171", features = ["derive"] } ipnet = { version = "2.8.0", features = ["serde"] } @@ -23,6 +23,7 @@ fern = "0.6.2" serde_path_to_error = "0.1.13" thiserror = "1.0.43" colored = "2.0.4" +tun-tap = "0.1.3" [[bin]] name = "protomask" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5f5abb1 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +SRC=$(wildcard src/*.rs) $(wildcard src/**/*.rs) Cargo.toml + +target/debug/protomask: $(SRC) + cargo build + sudo setcap cap_net_admin=eip $@ \ No newline at end of file diff --git a/protomask.toml b/protomask.toml index d2a93a5..a8cc9c9 100644 --- a/protomask.toml +++ b/protomask.toml @@ -1,22 +1,14 @@ # Example configuration file for protomask +[Interface] +# Addresses to use for ICMP messaging +Address4 = "192.0.2.1" +Address6 = "2001:db8:1::1" +# A list of IPv4 prefixes to NAT to +Pool = ["192.0.2.0/24"] +# The IPv6 prefix to listen for traffic on +Prefix = "64:ff9b::/96" -# Options for configuring the NAT64 interface -[interface] -# A list of IPv4 prefixes to use as the NAT64 pool -# The first address of the pool will be assigned to protomask itself (for ICMP messaging) -pool = ["192.0.2.0/24"] - -# The IPv6 prefix to use for the NAT64 interface. -prefix = "64:ff9b::/96" - -# A unique IPv6 address to use for ICMP messaging -# This will also serve DNS64 -icmpv6_address = "2001:db8:1::1" - -# Rules for controlling address mapping -[rules] -# A static mapping of IPv4 and IPv6 addresses +[Rules] +# A static mapping of IPv4 and IPv6 addresses # These addresses will be exclusively reserved, and not used in the general pool -static_map = [ - { v4 = "192.0.2.2", v6 = "2001:db8:1::2" } -] +MapStatic = [{ v4 = "192.0.2.2", v6 = "2001:db8:1::2" }] diff --git a/src/config.rs b/src/config.rs index fb47300..b6b5d82 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,12 +9,19 @@ use ipnet::{Ipv4Net, Ipv6Net}; /// Interface config #[derive(Debug, serde::Deserialize)] pub struct InterfaceConfig { - /// Ipv4 pool + /// IPv4 router address + #[serde(rename="Address4")] + pub address_v4: Ipv4Addr, + /// IPv6 router address + #[serde(rename="Address6")] + pub address_v6: Ipv6Addr, + /// Ipv4 pool + #[serde(rename="Pool")] pub pool: Vec, /// IPv6 prefix + #[serde(rename="Prefix")] pub prefix: Ipv6Net, - /// IPv6 router addr - pub icmpv6_address: Ipv6Addr, + } /// A static mapping rule @@ -30,6 +37,7 @@ pub struct AddressMappingRule { #[derive(Debug, serde::Deserialize)] pub struct RulesConfig { /// Static mapping rules + #[serde(rename="MapStatic")] pub static_map: Vec, } @@ -37,8 +45,10 @@ pub struct RulesConfig { #[derive(Debug, serde::Deserialize)] pub struct Config { /// Interface config + #[serde(rename="Interface")] pub interface: InterfaceConfig, /// Rules config + #[serde(rename="Rules")] pub rules: RulesConfig, } diff --git a/src/main.rs b/src/main.rs index 190b76e..dcd5c25 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,11 @@ use clap::Parser; use config::Config; +use nat::Nat64; -mod config; mod cli; +mod config; +mod nat; +mod types; #[tokio::main] pub async fn main() { @@ -26,5 +29,21 @@ pub async fn main() { // Parse the config file let config = Config::load(args.config_file).unwrap(); + // Create the NAT64 instance + let nat64 = Nat64::new( + config.interface.address_v4, + config.interface.address_v6, + config.interface.pool, + config.interface.prefix, + config + .rules + .static_map + .iter() + .map(|rule| (rule.v4, rule.v6)) + .collect(), + ) + .await + .unwrap(); + loop{} } diff --git a/src/nat/mod.rs b/src/nat/mod.rs new file mode 100644 index 0000000..51d4ca1 --- /dev/null +++ b/src/nat/mod.rs @@ -0,0 +1,85 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +use ipnet::{Ipv4Net, Ipv6Net}; +use tokio::process::Command; +use tun_tap::{Iface, Mode}; + +/// A cleaner way to execute an `ip` command +macro_rules! iproute2 { + ($($arg:expr),*) => {{ + Command::new("ip") + $(.arg($arg))* + .status() + }} +} + +pub struct Nat64 { + interface: Iface, +} + +impl Nat64 { + /// Bring up a new NAT64 interface + /// + /// **Arguments:** + /// - `nat_v4`: An IPv4 address to assign to this NAT instance for ICMP and other purposes + /// - `nat_v6`: An IPv6 address to assign to this NAT instance for ICMP and other purposes + /// - `ipv4_pool`: A list of IPv4 prefixes to communicate from + /// - `ipv6_prefix`: The IPv6 prefix to listen on (should generally be `64:ff9b::/96`) + pub async fn new( + nat_v4: Ipv4Addr, + nat_v6: Ipv6Addr, + ipv4_pool: Vec, + ipv6_prefix: Ipv6Net, + static_mappings: Vec<(Ipv4Addr, Ipv6Addr)>, + ) -> Result { + // Bring up tun interface + let interface = Iface::new("nat64i%d", Mode::Tun)?; + + // Configure the interface + let interface_name = interface.name(); + log::info!("Configuring interface {}", interface_name); + + // Add the nat addresses + log::debug!("Assigning {} to {}", nat_v4, interface_name); + iproute2!( + "address", + "add", + format!("{}/32", nat_v4), + "dev", + interface_name + ) + .await?; + log::debug!("Assigning {} to {}", nat_v6, interface_name); + iproute2!( + "address", + "add", + format!("{}/128", nat_v6), + "dev", + interface_name + ) + .await?; + + // Bring up the interface + log::debug!("Bringing up {}", interface_name); + iproute2!("link", "set", "dev", interface_name, "up").await?; + + // Add route for IPv6 prefix + log::debug!("Adding route {} via {}", ipv6_prefix, interface_name); + iproute2!( + "route", + "add", + ipv6_prefix.to_string(), + "dev", + interface_name + ) + .await?; + + // Add every IPv4 prefix to the routing table + for prefix in ipv4_pool { + log::debug!("Adding route {} via {}", prefix, interface_name); + iproute2!("route", "add", prefix.to_string(), "dev", interface_name).await?; + } + + Ok(Self { interface }) + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..5c86265 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,33 @@ +use std::net::{Ipv4Addr, Ipv6Addr}; + +use ipnet::Ipv4Net; + +/// Represents a pair of IP addresses for a dual-stack host or mapping +#[derive(Debug, serde::Deserialize)] +pub struct AddressPair { + /// IPv4 address + pub v4: Ipv4Addr, + /// IPv6 address + pub v6: Ipv6Addr, +} + +// /// Represents a pool of IPv4 addresses +// #[derive(Debug, serde::Deserialize)] +// pub struct Ipv4Pool { +// /// All possible addresses +// pub prefixes: Vec, +// /// Addresses that cannot be dynamically assigned +// pub reservations: Vec, +// } + +// impl Ipv4Pool { +// /// Construct a new `Ipv4Pool` +// pub fn new(prefixes: Vec) -> Self { +// Self { +// prefixes, +// reservations: Vec::new(), +// } +// } + +// /// Reserve +// } \ No newline at end of file