1
protomask/src/nat/table.rs

227 lines
7.9 KiB
Rust

use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
time::{Duration, Instant},
};
use bimap::BiHashMap;
use ipnet::{Ipv4Net, Ipv6Net};
/// Possible errors thrown in the address reservation process
#[derive(Debug, thiserror::Error)]
pub enum TableError {
#[error("Address already reserved: {0}")]
AddressAlreadyReserved(IpAddr),
#[error("IPv4 address has no IPv6 mapping: {0}")]
NoIpv6Mapping(Ipv4Addr),
#[error("Address pool depleted")]
AddressPoolDepleted,
}
/// A NAT address table
#[derive(Debug)]
pub struct Nat64Table {
/// All possible IPv4 addresses that can be used
ipv4_pool: Vec<Ipv4Net>,
/// Current reservations
reservations: BiHashMap<Ipv6Addr, Ipv4Addr>,
/// The timestamp of each reservation (used for pruning)
reservation_times: HashMap<(Ipv6Addr, Ipv4Addr), Option<Instant>>,
/// The maximum amount of time to reserve an address pair for
reservation_timeout: Duration,
}
impl Nat64Table {
/// Construct a new NAT64 table
///
/// **Arguments:**
/// - `ipv4_pool`: The pool of IPv4 addresses to use in the mapping process
/// - `reservation_timeout`: The amount of time to reserve an address pair for
pub fn new(ipv4_pool: Vec<Ipv4Net>, reservation_timeout: Duration) -> Self {
Self {
ipv4_pool,
reservations: BiHashMap::new(),
reservation_times: HashMap::new(),
reservation_timeout,
}
}
/// Make a reservation for an IP address pair for eternity
pub fn add_infinite_reservation(
&mut self,
ipv6: Ipv6Addr,
ipv4: Ipv4Addr,
) -> Result<(), TableError> {
// Check if either address is already reserved
self.prune();
if self.reservations.contains_left(&ipv6) {
return Err(TableError::AddressAlreadyReserved(ipv6.into()));
} else if self.reservations.contains_right(&ipv4) {
return Err(TableError::AddressAlreadyReserved(ipv4.into()));
}
// Add the reservation
self.reservations.insert(ipv6, ipv4);
self.reservation_times.insert((ipv6, ipv4), None);
Ok(())
}
/// Get or assign an IPv4 address for the given IPv6 address
pub fn get_or_assign_ipv4(&mut self, ipv6: Ipv6Addr) -> Result<Ipv4Addr, TableError> {
// Prune old reservations
self.prune();
// If the IPv6 address is already reserved, return the IPv4 address
if let Some(ipv4) = self.reservations.get_by_left(&ipv6) {
// Update the reservation time
self.reservation_times
.insert((ipv6, *ipv4), Some(Instant::now()));
// Return the v4 address
return Ok(*ipv4);
}
// Otherwise, try to assign a new IPv4 address
for ipv4_net in &self.ipv4_pool {
for ipv4 in ipv4_net.hosts() {
// Check if this address is available for use
if !self.reservations.contains_right(&ipv4) {
// Add the reservation
self.reservations.insert(ipv6, ipv4);
self.reservation_times
.insert((ipv6, ipv4), Some(Instant::now()));
return Ok(ipv4);
}
}
}
// If we get here, we failed to find an available address
Err(TableError::AddressPoolDepleted)
}
/// Try to find an IPv6 address for the given IPv4 address
pub fn get_reverse(&mut self, ipv4: Ipv4Addr) -> Result<Ipv6Addr, TableError> {
// Prune old reservations
self.prune();
// If the IPv4 address is already reserved, return the IPv6 address
if let Some(ipv6) = self.reservations.get_by_right(&ipv4) {
// Update the reservation time
self.reservation_times
.insert((*ipv6, ipv4), Some(Instant::now()));
// Return the v6 address
return Ok(*ipv6);
}
// Otherwise, there is no matching reservation
Err(TableError::NoIpv6Mapping(ipv4))
}
/// Check if an address is within the IPv4 pool
pub fn is_address_within_pool(&self, address: &Ipv4Addr) -> bool {
self.ipv4_pool.iter().any(|net| net.contains(address))
}
/// Calculate the translated version of any address
pub fn calculate_xlat_addr(
&mut self,
input: &IpAddr,
ipv6_nat64_prefix: &Ipv6Net,
) -> Result<IpAddr, TableError> {
// Handle the incoming address type
match input {
// Handle IPv4
IpAddr::V4(ipv4_addr) => {
// If the address is in the IPv4 pool, it is a regular IPv4 address
if self.is_address_within_pool(ipv4_addr) {
// This means we need to pass through to `get_reverse`
return Ok(IpAddr::V6(self.get_reverse(*ipv4_addr)?));
}
// Otherwise, it shall be embedded inside the ipv6 prefix
let prefix_octets = ipv6_nat64_prefix.addr().octets();
let address_octets = ipv4_addr.octets();
return Ok(IpAddr::V6(Ipv6Addr::new(
u16::from_be_bytes([prefix_octets[0], prefix_octets[1]]),
u16::from_be_bytes([prefix_octets[2], prefix_octets[3]]),
u16::from_be_bytes([prefix_octets[4], prefix_octets[5]]),
u16::from_be_bytes([prefix_octets[6], prefix_octets[7]]),
u16::from_be_bytes([prefix_octets[8], prefix_octets[9]]),
u16::from_be_bytes([prefix_octets[10], prefix_octets[11]]),
u16::from_be_bytes([address_octets[0], address_octets[1]]),
u16::from_be_bytes([address_octets[2], address_octets[3]]),
)));
}
// Handle IPv6
IpAddr::V6(ipv6_addr) => {
// If the address is in the IPv6 prefix, it is an embedded IPv4 address
if ipv6_nat64_prefix.contains(ipv6_addr) {
let address_bytes = ipv6_addr.octets();
return Ok(IpAddr::V4(Ipv4Addr::new(
address_bytes[12],
address_bytes[13],
address_bytes[14],
address_bytes[15],
)));
}
// Otherwise, it is a regular IPv6 address and we can pass through to `get_or_assign_ipv4`
return Ok(IpAddr::V4(self.get_or_assign_ipv4(*ipv6_addr)?));
}
}
}
}
impl Nat64Table {
/// Prune old reservations
pub fn prune(&mut self) {
let now = Instant::now();
// Prune from the reservation map
self.reservations.retain(|v6, v4| {
if let Some(time) = self.reservation_times.get(&(*v6, *v4)) {
if let Some(time) = time {
now - *time < self.reservation_timeout
} else {
true
}
} else {
true
}
});
// Remove all times assigned to reservations that no longer exist
self.reservation_times.retain(|(v6, v4), _| {
self.reservations.contains_left(v6) && self.reservations.contains_right(v4)
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_infinite_reservation() {
let mut table = Nat64Table::new(
vec![Ipv4Net::new(Ipv4Addr::new(192, 0, 2, 0), 24).unwrap()],
Duration::from_secs(60),
);
// Add a reservation
table
.add_infinite_reservation("2001:db8::1".parse().unwrap(), "192.0.2.1".parse().unwrap())
.unwrap();
// Check that it worked
assert_eq!(
table
.reservations
.get_by_left(&"2001:db8::1".parse().unwrap()),
Some(&"192.0.2.1".parse().unwrap())
);
}
}