From 43c8f2388f3f541c711cb9cc786d677bd8299605 Mon Sep 17 00:00:00 2001 From: Nicolas Kagami Date: Thu, 6 Aug 2026 10:46:25 -0300 Subject: [PATCH] unify the per-family NAT mapping code Replace the duplicated v4/v6 mapping storage and logic with a single NatEntry type and a NatMap generic over NatAddress; the public API keeps its per-family signatures as thin wrappers. --- dpd/src/api_server.rs | 42 +-- dpd/src/main.rs | 6 +- dpd/src/nat.rs | 849 ++++++++++++++++++------------------------ dpd/src/rpw/mod.rs | 12 +- dpd/src/table/nat.rs | 24 ++ 5 files changed, 408 insertions(+), 525 deletions(-) diff --git a/dpd/src/api_server.rs b/dpd/src/api_server.rs index e65d6ddb..f8393f93 100644 --- a/dpd/src/api_server.rs +++ b/dpd/src/api_server.rs @@ -111,7 +111,7 @@ use crate::switch_port::FixedSideDevice; use crate::switch_port::LedState; use crate::transceivers::PowerState; use crate::types::DpdError; -use crate::{Switch, arp, loopback, nat, ports, route}; +use crate::{Switch, arp, loopback, ports, route}; use common::attached_subnet::AttachedSubnetEntry; use common::nat::{Ipv4Nat, Ipv6Nat}; use common::network::{InstanceTarget, MacAddr, NatTarget}; @@ -1422,8 +1422,7 @@ impl DpdApi for DpdApiImpl { WhichPage::Next(Ipv6Token { ip }) => Some(*ip), }; - let entries = nat::get_ipv6_addrs_range( - switch, + let entries = switch.nat.get_addrs_range( last_addr, usize::try_from(max).expect("invalid usize"), ); @@ -1449,8 +1448,7 @@ impl DpdApi for DpdApiImpl { WhichPage::Next(NatToken { port }) => Some(*port), }; - let entries = nat::get_ipv6_mappings_range( - switch, + let entries = switch.nat.get_mappings_range( params.ipv6, port, usize::try_from(max).expect("invalid usize"), @@ -1469,8 +1467,7 @@ impl DpdApi for DpdApiImpl { ) -> Result, HttpError> { let switch: &Switch = rqctx.context(); let params = path.into_inner(); - match nat::get_ipv6_mapping(switch, params.ipv6, params.low, params.low) - { + match switch.nat.get_mapping(params.ipv6, params.low, params.low) { Ok(tgt) => Ok(HttpResponseOk(tgt)), Err(e) => Err(e.into()), } @@ -1483,7 +1480,7 @@ impl DpdApi for DpdApiImpl { ) -> Result { let switch: &Switch = rqctx.context(); let params = path.into_inner(); - match nat::set_ipv6_mapping( + match switch.nat.add_mapping( switch, params.ipv6, params.low, @@ -1501,7 +1498,9 @@ impl DpdApi for DpdApiImpl { ) -> Result { let switch: &Switch = rqctx.context(); let params = path.into_inner(); - nat::clear_ipv6_mapping(switch, params.ipv6, params.low, params.low) + switch + .nat + .remove_mapping(switch, params.ipv6, params.low, params.low) .map(|_| HttpResponseDeleted()) .map_err(HttpError::from) } @@ -1511,7 +1510,7 @@ impl DpdApi for DpdApiImpl { ) -> Result { let switch: &Switch = rqctx.context(); - match nat::reset_ipv6(switch) { + match switch.nat.reset::(switch) { Ok(_) => Ok(HttpResponseUpdatedNoContent()), Err(e) => Err(e.into()), } @@ -1530,8 +1529,7 @@ impl DpdApi for DpdApiImpl { WhichPage::Next(Ipv4Token { ip }) => Some(*ip), }; - let entries = nat::get_ipv4_addrs_range( - switch, + let entries = switch.nat.get_addrs_range( last_addr, usize::try_from(max).expect("invalid usize"), ); @@ -1558,8 +1556,7 @@ impl DpdApi for DpdApiImpl { WhichPage::Next(NatToken { port }) => Some(*port), }; - let entries = nat::get_ipv4_mappings_range( - switch, + let entries = switch.nat.get_mappings_range( params.ipv4, port, usize::try_from(max).expect("invalid usize"), @@ -1578,8 +1575,7 @@ impl DpdApi for DpdApiImpl { ) -> Result, HttpError> { let switch: &Switch = rqctx.context(); let params = path.into_inner(); - match nat::get_ipv4_mapping(switch, params.ipv4, params.low, params.low) - { + match switch.nat.get_mapping(params.ipv4, params.low, params.low) { Ok(tgt) => Ok(HttpResponseOk(tgt)), Err(e) => Err(e.into()), } @@ -1592,7 +1588,7 @@ impl DpdApi for DpdApiImpl { ) -> Result { let switch: &Switch = rqctx.context(); let params = path.into_inner(); - match nat::set_ipv4_mapping( + match switch.nat.add_mapping( switch, params.ipv4, params.low, @@ -1610,7 +1606,9 @@ impl DpdApi for DpdApiImpl { ) -> Result { let switch: &Switch = rqctx.context(); let params = path.into_inner(); - nat::clear_ipv4_mapping(switch, params.ipv4, params.low, params.low) + switch + .nat + .remove_mapping(switch, params.ipv4, params.low, params.low) .map(|_| HttpResponseDeleted()) .map_err(HttpError::from) } @@ -1620,7 +1618,7 @@ impl DpdApi for DpdApiImpl { ) -> Result { let switch: &Switch = rqctx.context(); - match nat::reset_ipv4(switch) { + match switch.nat.reset::(switch) { Ok(_) => Ok(HttpResponseUpdatedNoContent()), Err(e) => Err(e.into()), } @@ -1744,11 +1742,11 @@ impl DpdApi for DpdApiImpl { error!(switch.log, "failed to clear all link state: {:?}", e); err = Some(e); } - if let Err(e) = nat::reset_ipv4(switch) { + if let Err(e) = switch.nat.reset::(switch) { error!(switch.log, "failed to reset ipv4 nat table: {:?}", e); err = Some(e); } - if let Err(e) = nat::reset_ipv6(switch) { + if let Err(e) = switch.nat.reset::(switch) { error!(switch.log, "failed to reset ipv6 nat table: {:?}", e); err = Some(e); } @@ -1913,7 +1911,7 @@ impl DpdApi for DpdApiImpl { ) -> Result, HttpError> { let switch = rqctx.context(); - Ok(HttpResponseOk(nat::get_nat_generation(switch))) + Ok(HttpResponseOk(switch.nat.generation())) } async fn nat_trigger_update( diff --git a/dpd/src/main.rs b/dpd/src/main.rs index e978abbd..4c3a8b1b 100644 --- a/dpd/src/main.rs +++ b/dpd/src/main.rs @@ -193,7 +193,7 @@ pub struct Switch { pub links: Mutex, pub routes: TokioMutex, pub arp: Mutex, - pub nat: Mutex, + pub nat: nat::Nat, pub attached_subnet: Mutex, pub loopback: Mutex, pub identifiers: Mutex>, @@ -298,6 +298,8 @@ impl Switch { let ws_log = log.new(slog::o!("unit" => "workflow_server")); let workflow_server = rpw::WorkflowServer::new(ws_log); + let nat = nat::Nat::new(&log); + Ok(Switch { start_time, config: Mutex::new(config), @@ -308,7 +310,7 @@ impl Switch { counters, routes: TokioMutex::new(route_data), arp: Mutex::new(arp::init()), - nat: Mutex::new(nat::init()), + nat, attached_subnet: Mutex::new(attached_subnet::init()), loopback: Mutex::new(loopback::init()), switch_ports, diff --git a/dpd/src/nat.rs b/dpd/src/nat.rs index 7d654f10..f5b4c5a1 100644 --- a/dpd/src/nat.rs +++ b/dpd/src/nat.rs @@ -4,17 +4,17 @@ // // Copyright 2026 Oxide Computer Company -use slog::{debug, error, trace}; +use slog::{Logger, debug, error, trace}; use std::collections::BTreeMap; use std::fmt; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::ops::Bound; +use std::sync::{Mutex, MutexGuard}; use crate::Switch; use crate::table; -use crate::table::nat::{add_entry, delete_entry}; +use crate::table::nat::{NatAddress, add_entry, delete_entry}; use crate::types::{DpdError, DpdResult}; -use common::nat::{Ipv4Nat, Ipv6Nat}; use common::network::NatTarget; /// An inclusive range of ports, guaranteed by construction to have @@ -62,41 +62,376 @@ impl fmt::Display for PortRange { } } -#[derive(PartialEq)] -pub(crate) struct Ipv6NatEntry { +#[derive(Clone, PartialEq)] +pub(crate) struct NatEntry { pub l4_ports: PortRange, pub tgt: NatTarget, } -impl fmt::Display for Ipv6NatEntry { +impl fmt::Display for NatEntry { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{} -> {}", self.l4_ports, self.tgt) } } -#[derive(Clone, PartialEq)] -pub(crate) struct Ipv4NatEntry { - pub l4_ports: PortRange, - pub tgt: NatTarget, +pub(crate) struct NatMap { + mappings: BTreeMap>, } -impl fmt::Display for Ipv4NatEntry { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} -> {}", self.l4_ports, self.tgt) +impl NatMap { + fn new() -> Self { + NatMap { mappings: BTreeMap::new() } + } + + fn get_addrs_range(&self, last_addr: Option, max: usize) -> Vec { + let max = max.min(64); + + let range = match last_addr { + Some(a) => (Bound::Excluded(a), Bound::Unbounded), + None => (Bound::Unbounded, Bound::Unbounded), + }; + + self.mappings.range(range).take(max).map(|(ip, _)| *ip).collect() + } + + /// Paginates through the mappings for one address, using `last_port` as + /// the starting offset + fn get_mappings_range( + &self, + external: A, + last_port: Option, + max: usize, + ) -> Vec { + let max = max.min(64); + + let port = match last_port { + None => 0, + Some(l) => l + 1, + }; + + self.mappings + .get(&external) + .map(|entries| { + entries + .iter() + .filter(|e| e.l4_ports.low >= port) + .take(max) + .map(|e| external.reservation(e.l4_ports, e.tgt)) + .collect() + }) + .unwrap_or_default() + } + + /// Find the first `NatTarget` where its `NatEntry` overlaps with the + /// provided port range + fn get_mapping( + &self, + nat_ip: A, + low: u16, + high: u16, + ) -> DpdResult { + let range = PortRange::new(low, high)?; + if let Some(v) = self.mappings.get(&nat_ip) + && let Some(idx) = + find_first_mapping(v.iter().map(|e| e.l4_ports), range) + { + return Ok(v[idx].tgt); + } + Err(DpdError::Missing("no mapping".into())) + } + + fn add_mapping( + &mut self, + switch: &Switch, + nat_ip: A, + low: u16, + high: u16, + tgt: NatTarget, + ) -> DpdResult<()> { + let l4_ports = PortRange::new(low, high)?; + let new_entry = NatEntry { l4_ports, tgt }; + let full = format!("{nat_ip}/{new_entry}"); + trace!(switch.log, "adding nat entry {}", full); + + let entries = self.mappings.entry(nat_ip).or_default(); + if entries.contains(&new_entry) { + // entry already exists + return Ok(()); + } + let Some(idx) = + find_space(entries.iter().map(|e| e.l4_ports), l4_ports) + else { + error!(switch.log, "unable to add {}: conflicting mapping", full); + return Err(DpdError::Exists("conflicting mapping".into())); + }; + + match add_entry(switch, nat_ip, l4_ports, tgt) { + Err(e) => { + error!(switch.log, "failed to add {}: {:?}", full, e); + Err(e) + } + _ => { + debug!(switch.log, "added nat entry {}", full); + entries.insert(idx, new_entry); + Ok(()) + } + } + } + + /// Find the first `NatEntry` that overlaps with the provided port range, + /// then remove it. + fn remove_mapping( + &mut self, + switch: &Switch, + nat_ip: A, + low: u16, + high: u16, + ) -> DpdResult<()> { + let range = PortRange::new(low, high)?; + trace!(switch.log, "clearing nat entry covering {}/{}", nat_ip, range); + + if let Some(entries) = self.mappings.get_mut(&nat_ip) + && let Some(idx) = + find_first_mapping(entries.iter().map(|e| e.l4_ports), range) + { + let ent = entries.remove(idx); + if entries.is_empty() { + self.mappings.remove(&nat_ip); + } + let full = format!("{nat_ip}/{ent}"); + return match delete_entry(switch, nat_ip, ent.l4_ports) { + Err(e) => { + error!(switch.log, "failed to clear {}: {:?}", full, e); + Err(e) + } + _ => { + debug!(switch.log, "cleared nat entry {}", full); + Ok(()) + } + }; + } + + Ok(()) + } + + /// Deletes any `NatEntry` that overlaps with the provided port range + fn remove_overlapping_mappings( + &mut self, + switch: &Switch, + nat_ip: A, + l4_ports: PortRange, + ) -> DpdResult<()> { + trace!( + switch.log, + "clearing all nat entries overlapping with {}/{}", nat_ip, l4_ports + ); + + if let Some(entries) = self.mappings.get_mut(&nat_ip) { + let mut mappings_to_delete = + find_mappings(entries.iter().map(|e| e.l4_ports), l4_ports); + // delete starting with the last index first, or you'll end up shifting the + // collection underneath you + mappings_to_delete.reverse(); + for idx in mappings_to_delete { + let ent = entries.remove(idx); + let full = format!("{nat_ip}/{ent}"); + match delete_entry(switch, nat_ip, ent.l4_ports) { + Err(e) => { + error!(switch.log, "failed to clear {}: {:?}", full, e); + return Err(e); + } + _ => { + debug!(switch.log, "cleared nat entry {}", full); + } + }; + } + if entries.is_empty() { + self.mappings.remove(&nat_ip); + } + } + + Ok(()) + } + + fn reset(&mut self, switch: &Switch) -> DpdResult<()> { + self.mappings.clear(); + table::nat::reset::(switch) + } +} + +pub struct Nat(Mutex); + +impl Nat { + pub(crate) fn new(log: &Logger) -> Self { + Nat(Mutex::new(NatData { + ipv6: NatMap::new(), + ipv4: NatMap::new(), + ipv4_generation: 0, + log: log.clone(), + })) + } + + fn lock(&self) -> MutexGuard<'_, NatData> { + self.0.lock().unwrap() + } + + /// Lock the NAT state and run `f` against the table for family `A`. + fn with_family( + &self, + f: impl FnOnce(&mut NatMap) -> R, + ) -> R { + f(self.lock().family()) + } + + pub(crate) fn get_addrs_range( + &self, + last_addr: Option, + max: usize, + ) -> Vec { + self.with_family(|t| t.get_addrs_range(last_addr, max)) + } + + pub(crate) fn get_mappings_range( + &self, + external: A, + last_port: Option, + max: usize, + ) -> Vec { + self.with_family(|t| t.get_mappings_range(external, last_port, max)) + } + + pub(crate) fn get_mapping( + &self, + nat_ip: A, + low: u16, + high: u16, + ) -> DpdResult { + self.with_family(|t| t.get_mapping(nat_ip, low, high)) + } + + pub(crate) fn add_mapping( + &self, + switch: &Switch, + nat_ip: A, + low: u16, + high: u16, + tgt: NatTarget, + ) -> DpdResult<()> { + self.with_family(|t| t.add_mapping(switch, nat_ip, low, high, tgt)) + } + + pub(crate) fn remove_mapping( + &self, + switch: &Switch, + nat_ip: A, + low: u16, + high: u16, + ) -> DpdResult<()> { + self.with_family(|t| t.remove_mapping(switch, nat_ip, low, high)) + } + + pub(crate) fn remove_overlapping_mappings( + &self, + switch: &Switch, + nat_ip: A, + l4_ports: PortRange, + ) -> DpdResult<()> { + self.with_family(|t| { + t.remove_overlapping_mappings(switch, nat_ip, l4_ports) + }) + } + + pub(crate) fn reset(&self, switch: &Switch) -> DpdResult<()> { + self.with_family(|t: &mut NatMap| t.reset(switch)) + } + + pub(crate) fn set_mapping( + &self, + switch: &Switch, + nat_ip: IpAddr, + low: u16, + high: u16, + tgt: NatTarget, + ) -> DpdResult<()> { + match nat_ip { + IpAddr::V4(ip) => self.add_mapping(switch, ip, low, high, tgt), + IpAddr::V6(ip) => self.add_mapping(switch, ip, low, high, tgt), + } + } + + pub(crate) fn clear_mapping( + &self, + switch: &Switch, + nat_ip: IpAddr, + low: u16, + high: u16, + ) -> DpdResult<()> { + match nat_ip { + IpAddr::V4(ip) => self.remove_mapping(switch, ip, low, high), + IpAddr::V6(ip) => self.remove_mapping(switch, ip, low, high), + } + } + + pub(crate) fn clear_overlapping_mappings( + &self, + switch: &Switch, + nat_ip: IpAddr, + low: u16, + high: u16, + ) -> DpdResult<()> { + let l4_ports = PortRange::new(low, high)?; + match nat_ip { + IpAddr::V4(ip) => { + self.remove_overlapping_mappings(switch, ip, l4_ports) + } + IpAddr::V6(ip) => { + self.remove_overlapping_mappings(switch, ip, l4_ports) + } + } + } + + pub(crate) fn generation(&self) -> i64 { + let data = self.lock(); + debug!(data.log, "fetching nat generation"); + data.ipv4_generation + } + + pub(crate) fn set_generation(&self, generation: i64) { + let mut data = self.lock(); + debug!(data.log, "setting nat generation"); + data.ipv4_generation = generation; } } + pub struct NatData { - ipv6_mappings: BTreeMap>, - ipv4_mappings: BTreeMap>, + ipv6: NatMap, + ipv4: NatMap, ipv4_generation: i64, + log: Logger, +} + +impl NatData { + /// Select the NAT table for address family `A`. + fn family(&mut self) -> &mut NatMap { + A::family(self) + } +} + +/// Ties an address family to its NAT table inside `NatData`. +pub(crate) trait NatFamily: NatAddress { + fn family(data: &mut NatData) -> &mut NatMap; } -fn ipv6_entry(ipv6: Ipv6Addr, e: &Ipv6NatEntry) -> String { - format!("{ipv6}/{e}") +impl NatFamily for Ipv4Addr { + fn family(data: &mut NatData) -> &mut NatMap { + &mut data.ipv4 + } } -fn ipv4_entry(ipv4: Ipv4Addr, e: &Ipv4NatEntry) -> String { - format!("{ipv4}/{e}") +impl NatFamily for Ipv6Addr { + fn family(data: &mut NatData) -> &mut NatMap { + &mut data.ipv6 + } } /// find index of first mapping that overlaps with supplied port range @@ -177,479 +512,3 @@ fn test_mapping() { assert_eq!(space(3, 5), None); assert_eq!(space(3, 8), None); } - -pub fn get_ipv6_addrs_range( - switch: &Switch, - last_addr: Option, - mut max: usize, -) -> Vec { - max = std::cmp::min(max, 64); - let nat = switch.nat.lock().unwrap(); - - let range = match last_addr { - Some(a) => (Bound::Excluded(a), Bound::Unbounded), - None => (Bound::Unbounded, Bound::Unbounded), - }; - - nat.ipv6_mappings.range(range).take(max).map(|(ip, _)| *ip).collect() -} - -/// Paginates through `Ipv6Nat` using `last_port` as the starting offset -pub fn get_ipv6_mappings_range( - switch: &Switch, - external: Ipv6Addr, - last_port: Option, - mut max: usize, -) -> Vec { - max = std::cmp::min(max, 64); - let nat = switch.nat.lock().unwrap(); - let mappings = match nat.ipv6_mappings.get(&external) { - Some(m) => m, - None => return Vec::new(), - }; - - let port = match last_port { - None => 0, - Some(l) => l + 1, - }; - - let mut entries = Vec::new(); - - for m in mappings { - if m.l4_ports.low >= port { - entries.push(Ipv6Nat { - external, - low: m.l4_ports.low, - high: m.l4_ports.high, - target: m.tgt, - }); - if entries.len() >= max { - break; - } - } - } - entries -} - -/// Find the first `NatTarget` where its `Ipv6NatEntry` matches the provided -/// `Ipv6Addr` and overlaps with the provided port range -pub fn get_ipv6_mapping( - switch: &Switch, - nat_ip: Ipv6Addr, - low: u16, - high: u16, -) -> DpdResult { - let range = PortRange::new(low, high)?; - let nat = switch.nat.lock().unwrap(); - if let Some(v) = nat.ipv6_mappings.get(&nat_ip) - && let Some(idx) = - find_first_mapping(v.iter().map(|e| e.l4_ports), range) - { - return Ok(v[idx].tgt); - } - Err(DpdError::Missing("no mapping".into())) -} - -pub fn set_ipv6_mapping( - switch: &Switch, - nat_ip: Ipv6Addr, - low: u16, - high: u16, - tgt: NatTarget, -) -> DpdResult<()> { - let l4_ports = PortRange::new(low, high)?; - let new_entry = Ipv6NatEntry { l4_ports, tgt }; - let full = ipv6_entry(nat_ip, &new_entry); - trace!(switch.log, "adding nat entry {}", full); - - let mut nat = switch.nat.lock().unwrap(); - let (entries, idx) = match nat.ipv6_mappings.get_mut(&nat_ip) { - Some(e) => { - if e.contains(&new_entry) { - // entry already exists - return Ok(()); - } - match find_space(e.iter().map(|x| x.l4_ports), l4_ports) { - Some(i) => (e, i), - None => { - trace!( - switch.log, - "unable to add nat entry {}: conflicting mapping", full - ); - return Err(DpdError::Exists("conflicting mapping".into())); - } - } - } - None => { - nat.ipv6_mappings.insert(nat_ip, Vec::new()); - (nat.ipv6_mappings.get_mut(&nat_ip).unwrap(), 0) - } - }; - - match add_entry(switch, nat_ip, l4_ports, tgt) { - Err(e) => { - error!(switch.log, "failed to add {}: {:?}", full, e); - Err(e) - } - _ => { - debug!(switch.log, "added nat entry {}", full); - entries.insert(idx, new_entry); - Ok(()) - } - } -} - -/// Find the first `NatTarget` where its `Ipv6NatEntry` matches the provided -/// `Ipv6Addr` and overlaps with the provided port range, then remove it. -pub fn clear_ipv6_mapping( - switch: &Switch, - nat_ip: Ipv6Addr, - low: u16, - high: u16, -) -> DpdResult<()> { - let range = PortRange::new(low, high)?; - let mut nat = switch.nat.lock().unwrap(); - trace!(switch.log, "clearing nat entry {}/{}-{}", nat_ip, low, high); - - if let Some(mappings) = nat.ipv6_mappings.get_mut(&nat_ip) - && let Some(idx) = - find_first_mapping(mappings.iter().map(|e| e.l4_ports), range) - { - let ent = mappings.remove(idx); - if mappings.is_empty() { - nat.ipv6_mappings.remove(&nat_ip); - } - let full = ipv6_entry(nat_ip, &ent); - return match delete_entry(switch, nat_ip, ent.l4_ports) { - Err(e) => { - error!(switch.log, "failed to clear {}: {:?}", full, e); - Err(e) - } - _ => { - debug!(switch.log, "cleared nat entry {}", full); - Ok(()) - } - }; - } - - Ok(()) -} - -pub fn get_ipv4_addrs_range( - switch: &Switch, - last_addr: Option, - mut max: usize, -) -> Vec { - max = std::cmp::min(max, 64); - let nat = switch.nat.lock().unwrap(); - - let range = match last_addr { - Some(a) => (Bound::Excluded(a), Bound::Unbounded), - None => (Bound::Unbounded, Bound::Unbounded), - }; - - nat.ipv4_mappings.range(range).take(max).map(|(ip, _)| *ip).collect() -} - -/// Paginates through `Ipv4Nat` using `last_port` as the starting offset -pub fn get_ipv4_mappings_range( - switch: &Switch, - external: Ipv4Addr, - last_port: Option, - mut max: usize, -) -> Vec { - max = std::cmp::min(max, 64); - let nat = switch.nat.lock().unwrap(); - let mappings = match nat.ipv4_mappings.get(&external) { - Some(m) => m, - None => return Vec::new(), - }; - - let port = match last_port { - None => 0, - Some(l) => l + 1, - }; - - let mut entries = Vec::new(); - - for m in mappings { - if m.l4_ports.low >= port { - entries.push(Ipv4Nat { - external, - low: m.l4_ports.low, - high: m.l4_ports.high, - target: m.tgt, - }); - if entries.len() >= max { - break; - } - } - } - entries -} - -/// Find the first `NatTarget` where its `Ipv4NatEntry` matches the provided -/// `Ipv4Addr` and overlaps with the provided port range -pub fn get_ipv4_mapping( - switch: &Switch, - nat_ip: Ipv4Addr, - low: u16, - high: u16, -) -> DpdResult { - let range = PortRange::new(low, high)?; - let nat = switch.nat.lock().unwrap(); - if let Some(v) = nat.ipv4_mappings.get(&nat_ip) - && let Some(idx) = - find_first_mapping(v.iter().map(|e| e.l4_ports), range) - { - return Ok(v[idx].tgt); - } - Err(DpdError::Missing("no mapping".into())) -} - -pub fn set_mapping( - switch: &Switch, - nat_ip: IpAddr, - low: u16, - high: u16, - tgt: NatTarget, -) -> DpdResult<()> { - match nat_ip { - IpAddr::V4(nat_ip) => set_ipv4_mapping(switch, nat_ip, low, high, tgt), - IpAddr::V6(nat_ip) => set_ipv6_mapping(switch, nat_ip, low, high, tgt), - } -} - -pub fn set_ipv4_mapping( - switch: &Switch, - nat_ip: Ipv4Addr, - low: u16, - high: u16, - tgt: NatTarget, -) -> DpdResult<()> { - let l4_ports = PortRange::new(low, high)?; - let new_entry = Ipv4NatEntry { l4_ports, tgt }; - let full = ipv4_entry(nat_ip, &new_entry); - trace!(switch.log, "adding nat entry {}", full); - - let mut nat = switch.nat.lock().unwrap(); - let (entries, idx) = match nat.ipv4_mappings.get_mut(&nat_ip) { - Some(e) => { - if e.contains(&new_entry) { - // entry already exists - return Ok(()); - } - match find_space(e.iter().map(|x| x.l4_ports), l4_ports) { - Some(i) => (e, i), - None => { - error!( - switch.log, - "unable to add {}: conflicting mapping", full - ); - return Err(DpdError::Exists("conflicting mapping".into())); - } - } - } - None => { - nat.ipv4_mappings.insert(nat_ip, Vec::new()); - (nat.ipv4_mappings.get_mut(&nat_ip).unwrap(), 0) - } - }; - - match add_entry(switch, nat_ip, l4_ports, tgt) { - Err(e) => { - error!(switch.log, "failed to add nat entry {}: {:?}", full, e); - Err(e) - } - _ => { - debug!(switch.log, "added nat entry {}", full); - entries.insert(idx, new_entry); - Ok(()) - } - } -} - -pub fn clear_mapping( - switch: &Switch, - nat_ip: IpAddr, - low: u16, - high: u16, -) -> DpdResult<()> { - match nat_ip { - IpAddr::V4(nat_ip) => clear_ipv4_mapping(switch, nat_ip, low, high), - IpAddr::V6(nat_ip) => clear_ipv6_mapping(switch, nat_ip, low, high), - } -} - -/// Find the first `NatTarget` where its `Ipv4NatEntry` matches the provided -/// `Ipv4Addr` and overlaps with the provided port range, then remove it. -pub fn clear_ipv4_mapping( - switch: &Switch, - nat_ip: Ipv4Addr, - low: u16, - high: u16, -) -> DpdResult<()> { - let range = PortRange::new(low, high)?; - let mut nat = switch.nat.lock().unwrap(); - trace!( - switch.log, - "clearing nat entry covering {}/{}-{}", nat_ip, low, high - ); - - if let Some(mappings) = nat.ipv4_mappings.get_mut(&nat_ip) - && let Some(idx) = - find_first_mapping(mappings.iter().map(|e| e.l4_ports), range) - { - let ent = mappings.remove(idx); - if mappings.is_empty() { - nat.ipv4_mappings.remove(&nat_ip); - } - let full = ipv4_entry(nat_ip, &ent); - return match delete_entry(switch, nat_ip, ent.l4_ports) { - Err(e) => { - error!(switch.log, "failed to clear {}: {:?}", full, e); - Err(e) - } - _ => { - debug!(switch.log, "cleared nat entry {}", full); - Ok(()) - } - }; - } - - Ok(()) -} - -pub fn clear_overlapping_mappings( - switch: &Switch, - nat_ip: IpAddr, - low: u16, - high: u16, -) -> DpdResult<()> { - match nat_ip { - IpAddr::V4(nat_ip) => { - clear_overlapping_mappings_v4(switch, nat_ip, low, high) - } - IpAddr::V6(nat_ip) => { - clear_overlapping_mappings_v6(switch, nat_ip, low, high) - } - } -} - -/// Deletes any `Ipv4NatEntry` where each entry matches the provided -/// `Ipv4Addr` and overlaps with the provided port range -pub fn clear_overlapping_mappings_v4( - switch: &Switch, - nat_ip: Ipv4Addr, - low: u16, - high: u16, -) -> DpdResult<()> { - let range = PortRange::new(low, high)?; - let mut nat = switch.nat.lock().unwrap(); - trace!( - switch.log, - "clearing all nat entries overlapping with {}/{}-{}", nat_ip, low, high - ); - - if let Some(mappings) = nat.ipv4_mappings.get_mut(&nat_ip) { - let mut mappings_to_delete = - find_mappings(mappings.iter().map(|e| e.l4_ports), range); - // delete starting with the last index first, or you'll end up shifting the - // collection underneath you - mappings_to_delete.reverse(); - for idx in mappings_to_delete { - let ent = mappings.remove(idx); - let full = ipv4_entry(nat_ip, &ent); - match delete_entry(switch, nat_ip, ent.l4_ports) { - Err(e) => { - error!(switch.log, "failed to clear {}: {:?}", full, e); - return Err(e); - } - _ => { - debug!(switch.log, "cleared nat entry {}", full); - } - }; - } - if mappings.is_empty() { - nat.ipv4_mappings.remove(&nat_ip); - } - } - - Ok(()) -} - -pub fn clear_overlapping_mappings_v6( - switch: &Switch, - nat_ip: Ipv6Addr, - low: u16, - high: u16, -) -> DpdResult<()> { - let range = PortRange::new(low, high)?; - let mut nat = switch.nat.lock().unwrap(); - trace!( - switch.log, - "clearing all nat entries overlapping with {}/{}-{}", nat_ip, low, high - ); - - if let Some(mappings) = nat.ipv6_mappings.get_mut(&nat_ip) { - let mut mappings_to_delete = - find_mappings(mappings.iter().map(|e| e.l4_ports), range); - // delete starting with the last index first, or you'll end up shifting the - // collection underneath you - mappings_to_delete.reverse(); - for idx in mappings_to_delete { - let ent = mappings.remove(idx); - let full = ipv6_entry(nat_ip, &ent); - match delete_entry(switch, nat_ip, ent.l4_ports) { - Err(e) => { - error!(switch.log, "failed to clear {}: {:?}", full, e); - return Err(e); - } - _ => { - debug!(switch.log, "cleared nat entry {}", full); - } - }; - } - if mappings.is_empty() { - nat.ipv6_mappings.remove(&nat_ip); - } - } - - Ok(()) -} - -pub fn reset_ipv6(switch: &Switch) -> DpdResult<()> { - let mut nat = switch.nat.lock().unwrap(); - - nat.ipv6_mappings.clear(); - table::nat::reset::(switch) -} - -pub fn reset_ipv4(switch: &Switch) -> DpdResult<()> { - let mut nat = switch.nat.lock().unwrap(); - - nat.ipv4_mappings.clear(); - table::nat::reset::(switch) -} - -pub fn set_nat_generation(switch: &Switch, generation: i64) { - let mut nat = switch.nat.lock().unwrap(); - - debug!(switch.log, "setting nat generation"); - nat.ipv4_generation = generation; -} - -pub fn get_nat_generation(switch: &Switch) -> i64 { - let nat = switch.nat.lock().unwrap(); - - debug!(switch.log, "fetching nat generation"); - nat.ipv4_generation -} - -pub fn init() -> NatData { - NatData { - ipv6_mappings: BTreeMap::new(), - ipv4_mappings: BTreeMap::new(), - ipv4_generation: 0, - } -} diff --git a/dpd/src/rpw/mod.rs b/dpd/src/rpw/mod.rs index 5b3ef261..62698c5c 100644 --- a/dpd/src/rpw/mod.rs +++ b/dpd/src/rpw/mod.rs @@ -19,7 +19,7 @@ use tokio::{ time::{Duration, Instant, sleep}, }; -use crate::{Switch, nat, types::DpdError::Exists}; +use crate::{Switch, types::DpdError::Exists}; use nexus_client::Client as NexusClient; use nexus_client::types::NatEntryView; @@ -101,7 +101,7 @@ pub async fn nat_workflow( wait(timer.clone()).await; debug!(log, "starting nat reconciliation"); - let generation = nat::get_nat_generation(&switch); + let generation = switch.nat.generation(); debug!(log, "we are currently at nat generation: {}", generation); let mut updates = @@ -176,7 +176,7 @@ fn apply_updates( }; if entry.deleted { - if let Err(e) = nat::clear_mapping( + if let Err(e) = switch.nat.clear_mapping( switch, nat_ip, entry.first_port, @@ -186,7 +186,7 @@ fn apply_updates( continue; }; } else { - while let Err(e) = nat::set_mapping( + while let Err(e) = switch.nat.set_mapping( switch, nat_ip, entry.first_port, @@ -195,7 +195,7 @@ fn apply_updates( ) { let final_error = match e { Exists(_) => { - match nat::clear_overlapping_mappings( + match switch.nat.clear_overlapping_mappings( switch, nat_ip, entry.first_port, @@ -216,7 +216,7 @@ fn apply_updates( } // update gen if nat entry update was successful generation = entry.r#gen; - nat::set_nat_generation(switch, generation); + switch.nat.set_generation(generation); } generation } diff --git a/dpd/src/table/nat.rs b/dpd/src/table/nat.rs index b09b3210..51debfdc 100644 --- a/dpd/src/table/nat.rs +++ b/dpd/src/table/nat.rs @@ -18,6 +18,7 @@ use aal_macros::*; use crate::Switch; use crate::nat::PortRange; use crate::table::*; +use common::nat::{Ipv4Nat, Ipv6Nat}; use common::network::{MacAddr, NatTarget}; pub(crate) trait NatAddress: Copy + Ord + fmt::Display { @@ -26,9 +27,12 @@ pub(crate) trait NatAddress: Copy + Ord + fmt::Display { type MatchKey: MatchParse + Hash + fmt::Display; type Action: ActionParse; + type Reservation; fn match_key(self, ports: PortRange) -> Self::MatchKey; fn action(tgt: NatTarget) -> Self::Action; + fn reservation(self, ports: PortRange, tgt: NatTarget) + -> Self::Reservation; } pub(crate) fn add_entry( @@ -79,6 +83,7 @@ impl NatAddress for Ipv4Addr { type MatchKey = Ipv4MatchKey; type Action = Ipv4Action; + type Reservation = Ipv4Nat; fn match_key(self, ports: PortRange) -> Ipv4MatchKey { Ipv4MatchKey::new(self, ports.low(), ports.high()) @@ -91,6 +96,15 @@ impl NatAddress for Ipv4Addr { vni: tgt.vni.as_u32(), } } + + fn reservation(self, ports: PortRange, tgt: NatTarget) -> Ipv4Nat { + Ipv4Nat { + external: self, + low: ports.low(), + high: ports.high(), + target: tgt, + } + } } impl NatAddress for Ipv6Addr { @@ -99,6 +113,7 @@ impl NatAddress for Ipv6Addr { type MatchKey = Ipv6MatchKey; type Action = Ipv6Action; + type Reservation = Ipv6Nat; fn match_key(self, ports: PortRange) -> Ipv6MatchKey { Ipv6MatchKey::new(self, ports.low(), ports.high()) @@ -111,6 +126,15 @@ impl NatAddress for Ipv6Addr { vni: tgt.vni.as_u32(), } } + + fn reservation(self, ports: PortRange, tgt: NatTarget) -> Ipv6Nat { + Ipv6Nat { + external: self, + low: ports.low(), + high: ports.high(), + target: tgt, + } + } } #[derive(MatchParse, Hash)]