Repository: Avarok-Cybersecurity/Citadel-Protocol (language: rust)

Issue:
Create more holistic integration testing in virtualized networks
As of now, our integration testing consists of booting up nodes within the same program and connecting to the corresponding localhost ports.

This approach does not mock real scenarios where there is sporadic network latency. One possible approach is to create a docker-compose file that sets up a virtual network (1 central node, n possible clients/peers) with simulated latency (e.g., 100ms). This approach will be good for basic integration testing, but, will not be good for testing NAT traversal. Thus, on top of this docker-compose setup, we will need a set up for testing NAT traversal.

Testing NAT-traversal will be more challenging. The current unit tests inside `citadel_wire` simulate the expected internal/external mappings for different NAT types, so in theory, when UDP hole punching begins, these predicted external endpoints should be reached. However, like before, we are making this assumption without bringing the network conditions into context. Any ideas?


Discussion / hints:
Potential lead: https://github.com/enobufs/nat-traversal-test-using-docker
#154 has helped us get closer to this goal, but, it is not yet complete. The current issue is that the STUN servers return the IP address of the client with respect to the network the STUN server is on. One solution is to put the STUN server on the same network as the client, and, to ensure that the client performs IP masquerading locally on its own machine via iptables (if this would work).

EDIT: complete
Other TODOs: Add a singular job where there is a chaotic testing environment: https://medium.com/hackernoon/network-emulation-for-docker-containers-f4d36b656cc3
Where we are at currently: we have docker containers (which have insofar been very useful) that simulate different NAT behaviours (full cone, address-restricted, port-restricted, and symmetric NATs). All four currently have a full cone NAT for the server, and each possible selection for the client.

We also need to extend this behaviour to P2P connections. This could be up to 16 possible P2P tests (4*4).

Once that is complete, we then need to work on addr translation testing. These addr translation tests will prove whether or not the built-in NAT traversal works when the mapping between internal and external socket addresses are asymmetric.
As of #164, we are left with only addr translation testing and improvements. Then, this will be closed.

Fix patch:
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 42cfe677c..73fb5a8d8 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -78,6 +78,8 @@ jobs:
       - uses: taiki-e/install-action@nextest
       - run: cargo nextest run --package netbeam
       - run: cargo nextest run --package citadel_wire --features=localhost-testing
+        env:
+          RUST_LOG: citadel=trace
 
   citadel_sdk:
     strategy:
@@ -208,6 +210,6 @@ jobs:
       NAT_TYPE_PEER_B: ${{ matrix.nat_type_peer_b }}
     steps:
       - uses: actions/checkout@v3
-      - if: ${{ matrix.nat_type_peer_a != 'symmetric' && matrix.nat_type_peer_b != 'symmetric' }}
+      - if: ${{ ! (matrix.nat_type_peer_a == 'symmetric' && matrix.nat_type_peer_b == 'symmetric') }}
         run: docker-compose -f ./docker/peer_to_peer.yml up --abort-on-container-exit --exit-code-from peer_a
         shell: bash
diff --git a/citadel_proto/src/proto/peer/peer_crypt.rs b/citadel_proto/src/proto/peer/peer_crypt.rs
index e24f6afef..e34b8e831 100644
--- a/citadel_proto/src/proto/peer/peer_crypt.rs
+++ b/citadel_proto/src/proto/peer/peer_crypt.rs
@@ -26,8 +26,6 @@ pub struct PeerNatInfo {
 }
 
 impl PeerNatInfo {
-    /// Since symmetric NATs will change IP addressed between differing endpoints, we can't connect thereto. We can however connect when the IP is the same. We will assume the ip is the same as the one visible from the central server,
-    /// additionally allowing for connections where the server + clients are on the LAN
     pub fn generate_proper_listener_connect_addr(
         &self,
         local_nat_type: &NatType,
diff --git a/citadel_wire/src/standard/nat_identification.rs b/citadel_wire/src/standard/nat_identification.rs
index 654ebeb18..4d5d939e2 100644
--- a/citadel_wire/src/standard/nat_identification.rs
+++ b/citadel_wire/src/standard/nat_identification.rs
@@ -14,6 +14,7 @@ use std::pin::Pin;
 use std::sync::Arc;
 use std::time::Duration;
 
+use crate::udp_traversal::hole_punch_config::AddrBand;
 use citadel_io::UdpSocket;
 use stun::client::ClientBuilder;
 use stun::message::{Getter, Message, BINDING_REQUEST};
@@ -27,43 +28,56 @@ const STUN_SERVERS: [&str; 3] = [
 
 const V4_BIND_ADDR: &str = "0.0.0.0:0";
 const IDENTIFY_TIMEOUT: Duration = Duration::from_millis(4500);
-pub(crate) const MAX_PORT_DELTA_FOR_PREDICTION: usize = 30;
-pub(crate) const MAX_LAST_OCTET_DELTA_FOR_PREDICTION: usize = 2;
 
 #[derive(Debug, Clone, Serialize, Deserialize, Default)]
-pub enum NatType {
-    /// ip_int:port_in == ip_ext:port_ext
-    EIM(SocketAddr, Option<IpAddressInfo>, bool),
-    /// Does not matter "where" the node connects to: the ip translates, but, the port stays the same
-    PortPreserved(IpAddr, Option<IpAddressInfo>, bool),
-    /// Predictable Endpoint dependent Mapping NAT. Contains the detected delta.
-    EDM(SocketAddr, Option<IpAddressInfo>, i32, bool),
-    /// Unpredictable Endpoint dependent Mapping NAT. Contains the detected IPs.
-    EDMRandomIp(Vec<IpAddr>, Option<IpAddressInfo>, bool),
-    /// IP translates, yet, port is preserved between internal:external
-    EDMRandomIPPortPreserved(Vec<IpAddr>, Option<IpAddressInfo>, bool),
-    /// Possibly unpredictable Endpoint dependent Mapping NAT. Contains the detected ports.
-    EDMRandomPort(SocketAddr, Option<IpAddressInfo>, Vec<u16>, bool),
-    /// Unknown or could not be determined
+pub enum IpTranslation {
+    /// The IP is the same
+    Identity,
+    /// The IP is translated, but, is invariant between outbound connections
+    Constant { external: IpAddr },
+    /// The IP is translated, and, is variant between outbound connections such that the NAT assigns
+    /// an IP that is completely independent of the internal IP
+    DeltaIndependentOffset {
+        average_delta: i32,
+        last_allocated_external_ip: IpAddr,
+    },
+    /// The IP is unpredictable
     #[default]
-    Unknown,
+    Unpredictable,
 }
 
-#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
-pub enum TraversalTypeRequired {
-    /// Use the linear hole punch subroutines in this crate
-    Linear,
-    /// Use the linear hole punch subroutines in this crate with adjusted port params
-    Delta(i32),
-    /// direct p2p not possible
-    TURN,
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub enum PortTranslation {
+    /// The port remains the same between internal:external mappings
+    Identity,
+    /// The port is translated by a constant delta between internal:external mappings
+    DeltaConstantOffset { delta: i32 },
+    /// The port is translated invariantly between internal:external mappings such that the NAT assigns
+    /// a port that is completely independent of the internal port
+    DeltaIndependentOffset {
+        average_delta: i32,
+        last_allocated_external_port: u16,
+    },
+    /// The port is unpredictable
+    #[default]
+    Unpredictable,
 }
 
-// we only need to check the NAT type once per node
-lazy_static::lazy_static! {
-    pub static ref LOCALHOST_TESTING_NAT_TYPE: citadel_io::Mutex<Option<NatType>> = citadel_io::Mutex::new(None);
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct NatType {
+    pub ip_translation: IpTranslation,
+    pub port_translation: PortTranslation,
+    pub ip_info: Option<IpAddressInfo>,
+    pub is_ipv6_enabled: bool,
 }
 
+pub struct SocketPair {
+    pub internal: SocketAddr,
+    pub external: SocketAddr,
+}
+
+pub const MAX_OCTET_DELTA: i32 = 5;
+
 impl NatType {
     /// Identifies the NAT which the local node is behind. Timeout at the default (5s)
     /// `local_bind_addr`: Only relevant for localhost testing
@@ -80,15 +94,6 @@ impl NatType {
         _timeout: Duration,
         stun_servers: Option<Vec<String>>,
     ) -> Result<Self, FirewallError> {
-        /*
-        #[cfg(feature = "localhost-testing-loopback-only")]
-        {
-            let lock = LOCALHOST_TESTING_NAT_TYPE.lock();
-            if let Some(nat_type) = lock.clone() {
-                log::warn!(target: "citadel", "Will not detect NAT type since local already has it: {:?}", nat_type);
-                return Ok(nat_type);
-            }
-        }*/
         match tokio::time::timeout(_timeout, get_nat_type(stun_servers)).await {
             Ok(res) => res
                 .map_err(|err| FirewallError::HolePunch(err.to_string()))
@@ -101,11 +106,12 @@ impl NatType {
                 log::warn!(target: "citadel", "Timeout on NAT identification occurred");
                 if cfg!(feature = "localhost-testing") {
                     log::warn!(target: "citadel", "Will use default NatType for localhost-testing");
-                    Ok(NatType::PortPreserved(
-                        IpAddr::from([127, 0, 0, 1]),
-                        None,
-                        false,
-                    ))
+                    Ok(NatType {
+                        ip_translation: IpTranslation::Identity,
+                        port_translation: PortTranslation::Identity,
+                        ip_info: Some(IpAddressInfo::localhost()),
+                        is_ipv6_enabled: false,
+                    })
                 } else {
                     Err(FirewallError::HolePunch(
                         "NAT identification elapsed".to_string(),
@@ -115,6 +121,272 @@ impl NatType {
         }
     }
 
+    pub fn new(addr0: SocketPair, addr1: SocketPair, addr2: SocketPair) -> Self {
+        let ip_translation = Self::get_ip_translation(&addr0, &addr1, &addr2);
+        let port_translation = Self::get_port_translation(&addr0, &addr1, &addr2);
+        Self {
+            ip_translation,
+            port_translation,
+            ip_info: None,
+            is_ipv6_enabled: is_ipv6_enabled(),
+        }
+    }
+
+    // given a local socket address, predicts the external addresses. Returns None if unpredictable
+    pub(crate) fn predict(&self, internal_addr: &SocketAddr) -> Option<Vec<AddrBand>> {
+        let mut bands = Vec::new();
+        let internal_port = internal_addr.port();
+
+        // These anticipated ports will be used for all ip translations
+        let anticipated_ports = match self.port_translation {
+            PortTranslation::Identity => vec![internal_port],
+            PortTranslation::DeltaConstantOffset { delta } => {
+                vec![Self::wrapping_port_add(internal_port, delta)]
+            }
+            PortTranslation::DeltaIndependentOffset {
+                average_delta,
+                last_allocated_external_port,
+            } => {
+                let mut ports = Vec::new();
+                for i in average_delta..=(3 * average_delta) {
+                    ports.push(Self::wrapping_port_add(last_allocated_external_port, i));
+                }
+                ports
+            }
+            PortTranslation::Unpredictable => return None,
+        };
+
+        // add the base IPs
+        match self.ip_translation {
+            IpTranslation::Identity => {
+                let band = AddrBand {
+                    necessary_ip: internal_addr.ip(),
+                    anticipated_ports: anticipated_ports.clone(),
+                };
+                bands.push(band);
+            }
+            IpTranslation::Constant { external } => {
+                let band = AddrBand {
+                    necessary_ip: external,
+                    anticipated_ports: anticipated_ports.clone(),
+                };
+                bands.push(band);
+            }
+            IpTranslation::DeltaIndependentOffset {
+                average_delta,
+                last_allocated_external_ip,
+            } => {
+                for i in average_delta..=(3 * average_delta) {
+                    let next_ip = match last_allocated_external_ip {
+                        IpAddr::V4(ip) => {
+                            let mut octets = ip.octets();
+                            octets[3] = octets[3].wrapping_add(i as u8);
+                            IpAddr::from(octets)
+                        }
+                        IpAddr::V6(ip) => {
+                            let mut octets = ip.octets();
+                            octets[15] = octets[15].wrapping_add(i as u8);
+                            IpAddr::from(octets)
+                        }
+                    };
+
+                    let band = AddrBand {
+                        necessary_ip: next_ip,
+                        anticipated_ports: anticipated_ports.clone(),
+                    };
+
+                    bands.push(band);
+                }
+            }
+            IpTranslation::Unpredictable => return None,
+        }
+
+        // finally, add the ipv6 band (if we have it)
+        if let Some(ip_info) = &self.ip_info {
+            if let Some(ext_ip) = &ip_info.external_ipv6 {
+                let band = AddrBand {
+                    necessary_ip: *ext_ip,
+                    anticipated_ports: anticipated_ports.clone(),
+                };
+                bands.push(band);
+            }
+
+            // now, add the internal IP (potentially)
+            if bands.iter().all(|r| r.necessary_ip != ip_info.internal_ip) {
+                let band = AddrBand {
+                    necessary_ip: ip_info.internal_ip,
+                    anticipated_ports,
+                };
+                bands.push(band);
+            }
+        }
+
+        // final case: nodes are on localhost
+        // now, add the internal IP (potentially)
+        if bands.iter().all(|r| {
+            r.necessary_ip != internal_addr.ip() && !r.anticipated_ports.contains(&internal_port)
+        }) {
+            let band = AddrBand {
+                necessary_ip: internal_addr.ip(),
+                anticipated_ports: vec![internal_port],
+            };
+            bands.push(band);
+        }
+
+        Some(bands)
+    }
+
+    /// Given 3 socket addresses, determine the port translation
+    fn get_ip_translation(
+        addr0: &SocketPair,
+        addr1: &SocketPair,
+        addr2: &SocketPair,
+    ) -> IpTranslation {
+        let ip0_internal = addr0.internal.ip();
+        let ip0_external = addr0.external.ip();
+        let ip1_internal = addr1.internal.ip();
+        let ip1_external = addr1.external.ip();
+        let ip2_internal = addr2.internal.ip();
+        let ip2_external = addr2.external.ip();
+
+        if ip0_internal == ip0_external
+            && ip1_internal == ip1_external
+            && ip2_internal == ip2_external
+        {
+            return IpTranslation::Identity;
+        }
+
+        if ip0_external == ip1_external && ip1_external == ip2_external {
+            return IpTranslation::Constant {
+                external: ip0_external,
+            };
+        }
+
+        let last_octet_external0 = Self::last_octet(&ip0_external);
+        let last_octet_external1 = Self::last_octet(&ip1_external);
+        let last_octet_external2 = Self::last_octet(&ip2_external);
+
+        // Third case: NAT translates each ip's last octet independently
+        // e.g., NAT may begin allocating at *.100, and incrementing upwards
+        // e.g., [192.168.2.1] -> [192.168.2.100]
+        // e.g., [192.168.2.2] -> [192.168.2.101]
+        // e.g., [192.168.2.10] -> [192.168.2.102]
+        // compare the external ip's last octet to find a pattern
+        let last_external_octets = &[
+            last_octet_external0,
+            last_octet_external1,
+            last_octet_external2,
+        ];
+        let average_delta = average_delta(last_external_octets);
+        // if the delta is within an acceptable range, say, 5, then we can assume that the NAT is
+        // potentially predictable
+        if average_delta <= MAX_OCTET_DELTA as usize {
+            let highest = *last_external_octets.iter().max().unwrap();
+
+            let last_allocated_external_ip = if last_external_octets[0] == highest {
+                ip0_external
+            } else if last_external_octets[1] == highest {
+                ip1_external
+            } else {
+                ip2_external
+            };
+
+            return IpTranslation::DeltaIndependentOffset {
+                average_delta: average_delta as _,
+                last_allocated_external_ip,
+            };
+        }
+
+        IpTranslation::Unpredictable
+    }
+
+    fn get_port_translation(
+        addr0: &SocketPair,
+        addr1: &SocketPair,
+        addr2: &SocketPair,
+    ) -> PortTranslation {
+        let port0_internal = addr0.internal.port();
+        let port0_external = addr0.external.port();
+        let port1_internal = addr1.internal.port();
+        let port1_external = addr1.external.port();
+        let port2_internal = addr2.internal.port();
+        let port2_external = addr2.external.port();
+
+        if port0_internal == port0_external
+            && port1_internal == port1_external
+            && port2_internal == port2_external
+        {
+            return PortTranslation::Identity;
+        }
+
+        let port0_delta = port0_external as i32 - port0_internal as i32;
+        let port1_delta = port1_external as i32 - port1_internal as i32;
+        let port2_delta = port2_external as i32 - port2_internal as i32;
+
+        if port0_delta == port1_delta && port1_delta == port2_delta {
+            return PortTranslation::DeltaConstantOffset { delta: port0_delta };
+        }
+
+        // last case: NAT translates each port's last octet independently
+        // e.g., NAT may begin allocating at 25000, and incrementing upwards
+        // e.g., 1098 > 25000
+        // e.g., 10000 > 25001
+        // e.g., 15000 > 25002
+        let port_external_octets = &[port0_external, port1_external, port2_external];
+
+        let average_delta = average_delta(port_external_octets);
+        // if the delta is within an acceptable range, say, 5, then we can assume that the NAT predictably allocates ports
+        if average_delta <= MAX_OCTET_DELTA as usize {
+            let highest = *port_external_octets.iter().max().unwrap();
+
+            let last_allocated_external_port = if port_external_octets[0] == highest {
+                port0_external
+            } else if port_external_octets[1] == highest {
+                port1_external
+            } else {
+                port2_external
+            };
+
+            return PortTranslation::DeltaIndependentOffset {
+                average_delta: average_delta as _,
+                last_allocated_external_port,
+            };
+        }
+
+        PortTranslation::Unpredictable
+    }
+
+    fn last_octet(ip: &IpAddr) -> u8 {
+        match ip {
+            IpAddr::V4(ip) => ip.octets()[3],
+            IpAddr::V6(ip) => ip.octets()[15],
+        }
+    }
+
+    fn wrapping_port_add(port: u16, delta: i32) -> u16 {
+        let mut new_port = port.wrapping_add(delta as u16);
+        if new_port < 1024 {
+            new_port += 1024
+        }
+
+        new_port
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
+pub enum TraversalTypeRequired {
+    /// Use the linear hole punch subroutines in this crate
+    Direct,
+    /// direct p2p not possible
+    TURN,
+}
+
+// we only need to check the NAT type once per node
+lazy_static::lazy_static! {
+    pub static ref LOCALHOST_TESTING_NAT_TYPE: citadel_io::Mutex<Option<NatType>> = citadel_io::Mutex::new(None);
+}
+
+impl NatType {
     /// Returns the NAT traversal type required to access self and other, respectively
     pub fn traversal_type_required_with(
         &self,
@@ -126,51 +398,12 @@ impl NatType {
     }
 
     pub fn traversal_type_required(&self) -> TraversalTypeRequired {
-        match self {
-            NatType::EIM(..) => TraversalTypeRequired::Linear,
-            NatType::PortPreserved(..) => TraversalTypeRequired::Linear,
-            NatType::EDM(_, _, n, _) => TraversalTypeRequired::Delta(*n),
-            NatType::EDMRandomPort(_, _, _ports, _) => {
-                let average_delta = self.get_average_delta_for_rand_port().unwrap();
-                if average_delta > MAX_PORT_DELTA_FOR_PREDICTION as _ {
-                    TraversalTypeRequired::TURN
-                } else {
-                    TraversalTypeRequired::Delta(average_delta as _)
-                }
-            }
-
-            NatType::EDMRandomIPPortPreserved(addrs, _, _) => {
-                let mut octets: Vec<[u8; 4]> = vec![];
-                for addr in addrs {
-                    match addr {
-                        IpAddr::V4(v4) => octets.push(v4.octets()),
-
-                        IpAddr::V6(_) => {
-                            // in this case, we assume the v6 addr is stable, and,
-                            // return as traversable (this should be unreachable in prod code)
-                            return TraversalTypeRequired::Linear;
-                        }
-                    }
-                }
-
-                let delta = average_delta(&octets.iter().map(|r| r[3]).collect::<Vec<u8>>());
-                if delta < MAX_LAST_OCTET_DELTA_FOR_PREDICTION {
-                    // make sure the first three octets of every ip are equivalent
-                    if octets
-                        .iter()
-                        .map(|r| Vec::from(&r[0..3] as &[u8]))
-                        .all_equal()
-                    {
-                        TraversalTypeRequired::Delta(delta as _)
-                    } else {
-                        TraversalTypeRequired::TURN
-                    }
-                } else {
-                    // IP is too
-                    TraversalTypeRequired::TURN
-                }
-            }
-            NatType::EDMRandomIp(..) | NatType::Unknown => TraversalTypeRequired::TURN,
+        if matches!(self.ip_translation, IpTranslation::Unpredictable)
+            || matches!(self.port_translation, PortTranslation::Unpredictable)
+        {
+            TraversalTypeRequired::TURN
+        } else {
+            TraversalTypeRequired::Direct
         }
     }
 
@@ -187,52 +420,11 @@ impl NatType {
     }
 
     pub fn ip_addr_info(&self) -> Option<&IpAddressInfo> {
-        match self {
-            NatType::EIM(_, ip, _)
-            | NatType::EDM(_, ip, ..)
-            | NatType::PortPreserved(_, ip, _)
-            | NatType::EDMRandomIp(_, ip, _)
-            | NatType::EDMRandomIPPortPreserved(_, ip, _)
-            | NatType::EDMRandomPort(_, ip, _, _) => ip.as_ref(),
-            NatType::Unknown => None,
-        }
-    }
-
-    pub fn get_average_delta_for_rand_port(&self) -> Option<u16> {
-        match self {
-            Self::EDMRandomPort(_, _, ports, _) => {
-                let average = average_delta(ports);
-                Some(average as _)
-            }
-
-            _ => None,
-        }
+        self.ip_info.as_ref()
     }
 
     pub fn is_ipv6_compatible(&self) -> bool {
-        match self {
-            NatType::EIM(_, _, v6)
-            | NatType::PortPreserved(_, _, v6)
-            | NatType::EDM(_, _, _, v6)
-            | NatType::EDMRandomIp(_, _, v6)
-            | NatType::EDMRandomIPPortPreserved(_, _, v6)
-            | NatType::EDMRandomPort(_, _, _, v6) => *v6,
-            NatType::Unknown => false,
-        }
-    }
-
-    fn store_ip_info(&mut self, info: Option<IpAddressInfo>) {
-        if let Some(info) = info {
-            match self {
-                NatType::PortPreserved(_, ip, _)
-                | NatType::EIM(_, ip, _)
-                | NatType::EDM(_, ip, ..)
-                | NatType::EDMRandomIp(_, ip, _)
-                | NatType::EDMRandomIPPortPreserved(_, ip, _)
-                | NatType::EDMRandomPort(_, ip, _, _) => *ip = Some(info),
-                NatType::Unknown => {}
-            }
-        }
+        self.is_ipv6_enabled
     }
 }
 
@@ -318,7 +510,6 @@ async fn get_nat_type(stun_servers: Option<Vec<String>>) -> Result<NatType, anyh
         let third_natted_addr = results
             .pop()
             .ok_or_else(|| anyhow::Error::msg("Third result not present"))??;
-        let is_ipv6_allowed = is_ipv6_enabled();
 
         // now, we determine what the nat does when mapping internal socket addrs to external socket addrs
         match (first_natted_addr, second_natted_addr, third_natted_addr) {
@@ -327,97 +518,23 @@ async fn get_nat_type(stun_servers: Option<Vec<String>>) -> Result<NatType, anyh
                 Some((addr2_ext, addr2_int)),
                 Some((addr3_ext, addr3_int)),
             ) => {
-                let port_preserved = addr_ext.port() == addr_int.port()
-                    && addr2_ext.port() == addr2_int.port()
-                    && addr3_ext.port() == addr3_int.port();
-                // if there is zero changes in the mapping, then we have EIM
-                if addr_ext == addr_int && addr2_ext == addr2_int && addr3_ext == addr3_int {
-                    // It doesn't matter where we connect; we always get the same socket addr
-                    return Ok(NatType::EIM(addr_ext, None, is_ipv6_allowed));
-                }
-
-                // if ANY addrs are v6, then NAT traversal to this node should be easy
-                if let Some(v6_addr) = [addr_ext, addr2_ext, addr3_ext]
-                    .iter()
-                    .find(|r| r.is_ipv6())
-                {
-                    return Ok(NatType::EIM(*v6_addr, None, is_ipv6_allowed));
-                }
-
-                // if the external IPs translated during the process, this may not be good for addr prediction
-                if (addr_ext.ip() != addr2_ext.ip()) || (addr2_ext.ip() != addr3_ext.ip()) {
-                    // Check to see if the first three octets are all the same
-                    match (addr_ext, addr2_ext, addr3_ext) {
-                        (SocketAddr::V4(v4_0), SocketAddr::V4(v4_1), SocketAddr::V4(v4_2)) => {
-                            if v4_0.ip().octets()[..3] == v4_1.ip().octets()[..3]
-                                && v4_1.ip().octets()[..3] == v4_2.ip().octets()[..3]
-                            {
-                                // first three octets are equivalent. Check to see if port preservation is true
-                                if port_preserved {
-                                    // check to make sure the average delta is
-                                    if average_delta(&vec![
-                                        v4_0.ip().octets()[3],
-                                        v4_0.ip().octets()[3],
-                                        v4_0.ip().octets()[3],
-                                    ]) < MAX_LAST_OCTET_DELTA_FOR_PREDICTION
-                                    {
-                                        return Ok(NatType::EDMRandomIPPortPreserved(
-                                            vec![addr_ext.ip(), addr2_ext.ip(), addr3_ext.ip()],
-                                            None,
-                                            is_ipv6_allowed,
-                                        ));
-                                    }
-                                }
-                            }
-                        }
-
-                        _ => {
-                            unreachable!("Ipv6 addrs block should have returned already")
-                        }
-                    }
-                    // this is the worst nat type since ip's are unpredictable. Just use TURN if other is random IP,
-                    // unless, the other has a predictable addr
-                    return Ok(NatType::EDMRandomIp(
-                        vec![addr_ext.ip(), addr2_ext.ip(), addr3_ext.ip()],
-                        None,
-                        is_ipv6_allowed,
-                    ));
-                }
-
-                // check to see if ext_port == int_port
-                if port_preserved {
-                    // in this case, the IP changes, however, the port stays the same. The NAT maps as such:
-                    // ip0:port -> ip1:port
-                    return Ok(NatType::PortPreserved(addr_ext.ip(), None, is_ipv6_allowed));
-                }
-
-                let deltas = &mut [addr_ext.port(), addr2_ext.port(), addr3_ext.port()];
-                deltas.sort_unstable();
-
-                let delta0 = i32::abs(deltas[0] as i32 - deltas[1] as i32);
-                let delta1 = i32::abs(deltas[1] as i32 - deltas[2] as i32);
-                log::trace!(target: "citadel", "[external] Delta0: {} | Delta1: {}", delta0, delta1);
-
-                let highest_latest_port = deltas[2];
-                let highest_last_addr = SocketAddr::new(addr_ext.ip(), highest_latest_port);
-
-                if delta0 == delta1 {
-                    // This means the ports are predictable. Use TCP simultaneous connect on expected ports based on delta. It is expected this data be sent to the peer. The peer will then connect to the socket ip:(LOCAL_BIND_PORT+delta)
-                    Ok(NatType::EDM(
-                        highest_last_addr,
-                        None,
-                        delta0,
-                        is_ipv6_allowed,
-                    ))
-                } else {
-                    // the IP's are equal, but, the ports are not predictable; use TURN
-                    Ok(NatType::EDMRandomPort(
-                        highest_last_addr,
-                        None,
-                        vec![addr_ext.port(), addr2_ext.port(), addr3_ext.port()],
-                        is_ipv6_allowed,
-                    ))
-                }
+                let pair0 = SocketPair {
+                    internal: addr_int,
+                    external: addr_ext,
+                };
+                let pair1 = SocketPair {
+                    internal: addr2_int,
+                    external: addr2_ext,
+                };
+                let pair2 = SocketPair {
+                    internal: addr3_int,
+                    external: addr3_ext,
+                };
+
+                let net_type = NatType::new(pair0, pair1, pair2);
+                log::info!(target: "citadel", "NAT type: {:?}", net_type);
+
+                Ok(net_type)
             }
 
             _ => Err(anyhow::Error::msg("Unable to get all three STUN addrs")),
@@ -452,13 +569,16 @@ async fn get_nat_type(stun_servers: Option<Vec<String>>) -> Result<NatType, anyh
     log::trace!(target: "citadel", "NAT Type: {nat_type:?} | IpInfo: {ip_info:?}");
     let ip_info = ip_info.map_err(|err| anyhow::Error::msg(err.to_string()))?;
 
-    nat_type.store_ip_info(ip_info);
+    nat_type.ip_info = ip_info;
     Ok(nat_type)
 }
 
 #[cfg(test)]
 mod tests {
-    use crate::nat_identification::NatType;
+    use crate::nat_identification::{
+        IpTranslation, NatType, PortTranslation, SocketPair, MAX_OCTET_DELTA,
+    };
+    use crate::udp_traversal::hole_punch_config::AddrBand;
     use std::net::{IpAddr, SocketAddr};
     use std::str::FromStr;
 
@@ -471,125 +591,316 @@ mod tests {
     }
 
     #[test]
-    fn test_average_delta_computation() {
-        assert_average_delta_inner(vec![70, 10, 50, 30], 20);
-        assert_average_delta_inner(vec![10, 30, 50, 70], 20);
-        assert_average_delta_inner(vec![10, 30, 50, 70, 90, 110], 20);
-        assert_average_delta_inner(vec![1, 2, 3, 4, 5, 6], 1);
-        assert_average_delta_inner(vec![2, 4, 6, 8, 10, 12], 2);
+    fn test_addr_translation_identification_identity_to_identity() {
+        let addr0 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:10000").unwrap(),
+            external: SocketAddr::from_str("192.168.2.1:10000").unwrap(),
+        };
+
+        let addr1 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:20000").unwrap(),
+            external: SocketAddr::from_str("192.168.2.1:20000").unwrap(),
+        };
+
+        let addr2 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:30000").unwrap(),
+            external: SocketAddr::from_str("192.168.2.1:30000").unwrap(),
+        };
+
+        let nat_type = NatType::new(addr0, addr1, addr2);
+        assert!(matches!(nat_type.ip_translation, IpTranslation::Identity));
+        assert!(matches!(
+            nat_type.port_translation,
+            PortTranslation::Identity
+        ));
+
+        let internal_addr = SocketAddr::from_str("192.168.2.1:30000").unwrap();
+        let predicted_addrs = nat_type.predict(&internal_addr).unwrap();
+        assert_eq!(
+            predicted_addrs,
+            vec![AddrBand {
+                necessary_ip: internal_addr.ip(),
+                anticipated_ports: vec![30000]
+            }]
+        );
     }
 
-    fn assert_average_delta_inner(ports: Vec<u16>, expected: u16) {
-        let dummy_nat_type = NatType::EDMRandomPort(
-            SocketAddr::from_str("127.0.0.1:1234").unwrap(),
-            None,
-            ports,
-            false,
-        );
+    #[test]
+    fn test_addr_translation_identification_identity_to_delta_constant_offset() {
+        let addr0 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:10000").unwrap(),
+            external: SocketAddr::from_str("192.168.2.1:10010").unwrap(),
+        };
+
+        let addr1 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:20000").unwrap(),
+            external: SocketAddr::from_str("192.168.2.1:20010").unwrap(),
+        };
+
+        let addr2 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:30000").unwrap(),
+            external: SocketAddr::from_str("192.168.2.1:30010").unwrap(),
+        };
+
+        let nat_type = NatType::new(addr0, addr1, addr2);
+        assert!(matches!(nat_type.ip_translation, IpTranslation::Identity));
+        assert!(matches!(
+            nat_type.port_translation,
+            PortTranslation::DeltaConstantOffset { delta: 10 }
+        ));
+        let internal_addr = SocketAddr::from_str("192.168.2.1:30000").unwrap();
+        let predicted_addrs = nat_type.predict(&internal_addr).unwrap();
         assert_eq!(
-            expected,
-            dummy_nat_type.get_average_delta_for_rand_port().unwrap()
-        )
+            predicted_addrs,
+            vec![AddrBand {
+                necessary_ip: internal_addr.ip(),
+                anticipated_ports: vec![30010]
+            }]
+        );
     }
 
     #[test]
-    fn test_nat_traversal_compat() {
-        let dummy_addr = SocketAddr::from_str("127.0.0.1:1234").unwrap();
-        let dummy_ip0_ok = IpAddr::from_str("123.100.200.100").unwrap();
-        let dummy_ip1_ok = IpAddr::from_str("123.100.200.101").unwrap();
-        let dummy_ip2_ok = IpAddr::from_str("123.100.200.102").unwrap();
-        let dummy_ip3_err = IpAddr::from_str("100.100.200.103").unwrap();
-
-        let eim = &NatType::EIM(dummy_addr, None, true);
-        let edm = &NatType::EDM(dummy_addr, None, 1, true);
-        let port_preserved = &NatType::PortPreserved(dummy_addr.ip(), None, true);
-        let random_port_compat =
-            &NatType::EDMRandomPort(dummy_addr, None, vec![10, 20, 30, 40], true);
-        let random_port_bad = &NatType::EDMRandomPort(dummy_addr, None, vec![40, 80, 120], true);
-        let random_ip = &NatType::EDMRandomIp(vec![dummy_addr.ip()], None, true);
-        let random_ip_port_preserved_ok = &NatType::EDMRandomIPPortPreserved(
-            vec![dummy_ip0_ok, dummy_ip1_ok, dummy_ip2_ok],
-            None,
-            true,
-        );
-        let random_ip_port_preserved_err = &NatType::EDMRandomIPPortPreserved(
-            vec![dummy_ip0_ok, dummy_ip1_ok, dummy_ip3_err],
-            None,
-            true,
-        );
+    // below the limit
+    fn test_addr_translation_identification_identity_to_delta_independent_offset0() {
+        let addr0 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:10000").unwrap(),
+            external: SocketAddr::from_str("192.168.2.1:50000").unwrap(),
+        };
+
+        let addr1 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:20000").unwrap(),
+            external: SocketAddr::from_str("192.168.2.1:50001").unwrap(),
+        };
+
+        let addr2 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:30000").unwrap(),
+            external: SocketAddr::from_str("192.168.2.1:50002").unwrap(),
+        };
+
+        let nat_type = NatType::new(addr0, addr1, addr2);
+        let _average_delta = 1;
+        assert!(matches!(nat_type.ip_translation, IpTranslation::Identity));
+        assert!(matches!(
+            nat_type.port_translation,
+            PortTranslation::DeltaIndependentOffset {
+                average_delta: _average_delta,
+                last_allocated_external_port: 50002
+            }
+        ));
 
-        // Start with EIM
-        inner_nat_traversal_compat(eim, eim, true);
-        inner_nat_traversal_compat(eim, edm, true);
-        inner_nat_traversal_compat(eim, port_preserved, true);
-        inner_nat_traversal_compat(eim, random_port_compat, true);
-
-        inner_nat_traversal_compat(eim, random_port_bad, true);
-        inner_nat_traversal_compat(eim, random_ip, true);
-
-        inner_nat_traversal_compat(eim, random_ip_port_preserved_ok, true);
-        inner_nat_traversal_compat(eim, random_ip_port_preserved_err, true);
-
-        // EDM
-        inner_nat_traversal_compat(edm, edm, true);
-        inner_nat_traversal_compat(edm, port_preserved, true);
-        inner_nat_traversal_compat(edm, random_port_compat, true);
-
-        inner_nat_traversal_compat(edm, random_port_bad, true);
-        inner_nat_traversal_compat(edm, random_ip, true);
-        inner_nat_traversal_compat(edm, random_ip_port_preserved_ok, true);
-        inner_nat_traversal_compat(edm, random_ip_port_preserved_err, true);
-
-        // PortPreserved
-        inner_nat_traversal_compat(port_preserved, port_preserved, true);
-        inner_nat_traversal_compat(port_preserved, random_port_compat, true);
-
-        inner_nat_traversal_compat(port_preserved, random_port_bad, true);
-        inner_nat_traversal_compat(port_preserved, random_ip, true);
-        inner_nat_traversal_compat(port_preserved, random_ip_port_preserved_ok, true);
-        inner_nat_traversal_compat(port_preserved, random_ip_port_preserved_err, true);
-
-        // Random port (compat)
-        inner_nat_traversal_compat(random_port_compat, random_port_compat, true);
-        inner_nat_traversal_compat(random_port_compat, random_port_bad, true);
-        inner_nat_traversal_compat(random_port_compat, random_ip, true);
-        inner_nat_traversal_compat(random_port_compat, random_ip_port_preserved_ok, true);
-        inner_nat_traversal_compat(random_port_compat, random_ip_port_preserved_err, true);
-
-        // Random port (bad)
-        inner_nat_traversal_compat(random_port_bad, random_port_bad, false);
-        inner_nat_traversal_compat(random_port_bad, random_ip, false);
-        inner_nat_traversal_compat(random_port_bad, random_ip_port_preserved_ok, true);
-        inner_nat_traversal_compat(random_port_bad, random_ip_port_preserved_err, false);
-
-        // Random ip
-        inner_nat_traversal_compat(random_ip, random_ip, false);
-        inner_nat_traversal_compat(random_ip, random_ip_port_preserved_ok, true);
-        inner_nat_traversal_compat(random_ip, random_ip_port_preserved_err, false);
-
-        // random ip, port preserved, delta ok
-        inner_nat_traversal_compat(
-            random_ip_port_preserved_ok,
-            random_ip_port_preserved_ok,
-            true,
+        let internal_addr = SocketAddr::from_str("192.168.2.1:30000").unwrap();
+        let predicted_addrs = nat_type.predict(&internal_addr).unwrap();
+        assert_eq!(
+            predicted_addrs,
+            vec![AddrBand {
+                necessary_ip: internal_addr.ip(),
+                anticipated_ports: vec![50003, 50004, 50005]
+            }]
         );
-        inner_nat_traversal_compat(
-            random_ip_port_preserved_ok,
-            random_ip_port_preserved_err,
-            true,
+    }
+
+    #[test]
+    // at the limit
+    fn test_addr_translation_identification_identity_to_delta_independent_offset1() {
+        let mappings = [
+            50000,
+            50000 + MAX_OCTET_DELTA as u16,
+            50000 + 2 * MAX_OCTET_DELTA as u16,
+        ];
+
+        let addr0 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:10000").unwrap(),
+            external: SocketAddr::from_str(&format!("192.168.2.1:{}", mappings[0])).unwrap(),
+        };
+
+        let addr1 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:20000").unwrap(),
+            external: SocketAddr::from_str(&format!("192.168.2.1:{}", mappings[1])).unwrap(),
+        };
+
+        let addr2 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:30000").unwrap(),
+            external: SocketAddr::from_str(&format!("192.168.2.1:{}", mappings[2])).unwrap(),
+        };
+
+        let nat_type = NatType::new(addr0, addr1, addr2);
+        let _last_allocated_external_port = mappings[2];
+        assert!(matches!(nat_type.ip_translation, IpTranslation::Identity));
+        assert!(matches!(
+            nat_type.port_translation,
+            PortTranslation::DeltaIndependentOffset {
+                average_delta: MAX_OCTET_DELTA,
+                last_allocated_external_port: _last_allocated_external_port
+            }
+        ));
+
+        let internal_addr = SocketAddr::from_str("192.168.2.1:30000").unwrap();
+        let predicted_addrs = nat_type.predict(&internal_addr).unwrap();
+        assert_eq!(
+            predicted_addrs,
+            vec![AddrBand {
+                necessary_ip: internal_addr.ip(),
+                anticipated_ports: (50015u16..=50025u16).into_iter().collect()
+            }]
         );
+    }
+
+    #[test]
+    // above the limit
+    fn test_addr_translation_identification_identity_to_delta_independent_offset2() {
+        let mappings = [
+            50000,
+            50000 + 1u16 + MAX_OCTET_DELTA as u16,
+            50000 + 2u16 + 2 * MAX_OCTET_DELTA as u16,
+        ];
+
+        let addr0 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:10000").unwrap(),
+            external: SocketAddr::from_str(&format!("192.168.2.1:{}", mappings[0])).unwrap(),
+        };
+
+        let addr1 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:20000").unwrap(),
+            external: SocketAddr::from_str(&format!("192.168.2.1:{}", mappings[1])).unwrap(),
+        };
+
+        let addr2 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:30000").unwrap(),
+            external: SocketAddr::from_str(&format!("192.168.2.1:{}", mappings[2])).unwrap(),
+        };
+
+        let nat_type = NatType::new(addr0, addr1, addr2);
+        let _last_allocated_external_port = mappings[2];
+        assert!(matches!(nat_type.ip_translation, IpTranslation::Identity));
+        assert!(matches!(
+            nat_type.port_translation,
+            PortTranslation::Unpredictable
+        ));
+
+        let internal_addr = SocketAddr::from_str("192.168.2.1:30000").unwrap();
+        let predicted_addrs = nat_type.predict(&internal_addr);
+        assert!(predicted_addrs.is_none());
+    }
 
-        // random ip, port preserved, delta bad
-        inner_nat_traversal_compat(
-            random_ip_port_preserved_err,
-            random_ip_port_preserved_err,
-            false,
+    #[test]
+    fn test_addr_translation_identification_identity_to_unpredictable() {
+        let addr0 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:10000").unwrap(),
+            external: SocketAddr::from_str("192.168.2.1:10001").unwrap(),
+        };
+
+        let addr1 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:20000").unwrap(),
+            external: SocketAddr::from_str("192.168.2.1:10500").unwrap(),
+        };
+
+        let addr2 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:30000").unwrap(),
+            external: SocketAddr::from_str("192.168.2.1:16000").unwrap(),
+        };
+
+        let nat_type = NatType::new(addr0, addr1, addr2);
+        assert!(matches!(nat_type.ip_translation, IpTranslation::Identity));
+        assert!(matches!(
+            nat_type.port_translation,
+            PortTranslation::Unpredictable
+        ));
+
+        let internal_addr = SocketAddr::from_str("192.168.2.1:10000").unwrap();
+        let predicted_addrs = nat_type.predict(&internal_addr);
+        assert!(predicted_addrs.is_none());
+    }
+
+    #[test]
+    fn test_addr_translation_identification_constant_to_identity() {
+        let addr0 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:10000").unwrap(),
+            external: SocketAddr::from_str("10.1.10.1:10000").unwrap(),
+        };
+
+        let addr1 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:20000").unwrap(),
+            external: SocketAddr::from_str("10.1.10.1:20000").unwrap(),
+        };
+
+        let addr2 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:30000").unwrap(),
+            external: SocketAddr::from_str("10.1.10.1:30000").unwrap(),
+        };
+
+        let nat_type = NatType::new(addr0, addr1, addr2);
+        let _external = IpAddr::from_str("10.1.10.1").unwrap();
+        assert!(matches!(
+            nat_type.ip_translation,
+            IpTranslation::Constant {
+                external: _external
+            }
+        ));
+        assert!(matches!(
+            nat_type.port_translation,
+            PortTranslation::Identity
+        ));
+
+        let internal_addr = SocketAddr::from_str("192.168.2.1:30000").unwrap();
+        let predicted_addrs = nat_type.predict(&internal_addr).unwrap();
+        assert_eq!(
+            predicted_addrs,
+            vec![AddrBand {
+                necessary_ip: IpAddr::from_str("10.1.10.1").unwrap(),
+                anticipated_ports: vec![30000]
+            }]
         );
     }
 
-    fn inner_nat_traversal_compat(ty1: &NatType, ty2: &NatType, stun_compat: bool) {
-        // also prove comparisons are commutative
-        assert_eq!(stun_compat, ty1.stun_compatible(ty2));
-        assert_eq!(stun_compat, ty2.stun_compatible(ty1));
+    #[test]
+    fn test_addr_translation_identification_delta_offset_to_identity() {
+        let addr0 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:10000").unwrap(),
+            external: SocketAddr::from_str("10.1.10.100:10000").unwrap(),
+        };
+
+        let addr1 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:20000").unwrap(),
+            external: SocketAddr::from_str("10.1.10.101:20000").unwrap(),
+        };
+
+        let addr2 = SocketPair {
+            internal: SocketAddr::from_str("192.168.2.1:30000").unwrap(),
+            external: SocketAddr::from_str("10.1.10.102:30000").unwrap(),
+        };
+
+        let nat_type = NatType::new(addr0, addr1, addr2);
+        let _last_allocated_external_ip = IpAddr::from_str("10.1.10.102").unwrap();
+        assert!(matches!(
+            nat_type.ip_translation,
+            IpTranslation::DeltaIndependentOffset {
+                average_delta: 1,
+                last_allocated_external_ip: _last_allocated_external_ip
+            }
+        ));
+        assert!(matches!(
+            nat_type.port_translation,
+            PortTranslation::Identity
+        ));
+
+        let internal_addr = SocketAddr::from_str("192.168.2.1:30000").unwrap();
+        let predicted_addrs = nat_type.predict(&internal_addr).unwrap();
+        assert_eq!(
+            predicted_addrs,
+            vec![
+                AddrBand {
+                    necessary_ip: IpAddr::from_str("10.1.10.103").unwrap(),
+                    anticipated_ports: vec![30000]
+                },
+                AddrBand {
+                    necessary_ip: IpAddr::from_str("10.1.10.104").unwrap(),
+                    anticipated_ports: vec![30000],
+                },
+                AddrBand {
+                    necessary_ip: IpAddr::from_str("10.1.10.105").unwrap(),
+                    anticipated_ports: vec![30000],
+                },
+            ]
+        );
     }
 }
diff --git a/citadel_wire/src/udp_traversal/hole_punch_config.rs b/citadel_wire/src/udp_traversal/hole_punch_config.rs
index 7463f0f61..4481aab17 100644
--- a/citadel_wire/src/udp_traversal/hole_punch_config.rs
+++ b/citadel_wire/src/udp_traversal/hole_punch_config.rs
@@ -1,315 +1,40 @@
 #![cfg_attr(feature = "localhost-testing-loopback-only", allow(unreachable_code))]
+
 use crate::nat_identification::NatType;
-use async_ip::IpAddressInfo;
 use citadel_io::UdpSocket;
+use std::collections::HashSet;
 use std::net::{IpAddr, SocketAddr};
 
 #[derive(Debug)]
 pub struct HolePunchConfig {
-    /// The IP address that must be connected to based on NAT traversal
+    /// The IP addresses that must be connected to based on NAT traversal
     pub bands: Vec<AddrBand>,
     // sockets bound to ports specially prepared for NAT traversal
     pub(crate) locally_bound_sockets: Option<Vec<UdpSocket>>,
 }
 
-const SPREAD: u16 = 6;
-
-impl HolePunchConfig {
-    /// The expectation is that both `local_nat_info` and `peer_nat_info`
-    /// were both *recently* obtained, since the port information may no longer
-    /// be relevant in a busy NAT
-    ///
-    /// Will return None if hole punching is determined not to be possible (TURN required)
-    ///
-    /// `initial_local_socket` is necessary since the peer must also know the declared internal port.
-    /// Once the initial local socket is created, it is expect that its bind port is sent to the peer,
-    /// where that value will be passed to this function as `peer_declared_internal_port1
-    /// SO_REUSE does not need to be on for `initial_local_socket`
-    pub fn new(
-        local_nat_info: &NatType,
-        peer_nat_info: &NatType,
-        first_local_socket: UdpSocket,
-        peer_declared_internal_port: u16,
-    ) -> Result<Self, anyhow::Error> {
-        // if this is not localhost testing, always check
-        // we do not check when doing localhost-testing since we don't want
-        // a runner behind an unpredictable NAT to error out
-        #[cfg(not(feature = "localhost-testing"))]
-        {
-            if !local_nat_info.stun_compatible(peer_nat_info) {
-                return Err(anyhow::Error::msg(
-                    "This cannot be called if STUN is not compatible",
-                ));
-            }
-        }
-
-        // below is only needed if the peer is behind a random port NAT
-        let peer_average_delta_opt = peer_nat_info.get_average_delta_for_rand_port();
-
-        // determine how we will connect to the peer
-        match peer_nat_info {
-            NatType::EIM(direct_addr, other_addrs, ..) => {
-                let mut bands = Vec::new();
-                // Below assumes the addr port is exactly whatever the server saw it as.
-                // NOTE: since a QUIC connection may be established, using up the UDP port,
-                // it is necessary that *before* the peer sends their info to this node, the port
-                // is accurately reflected in this direct addr
-
-                Self::load_alternate_bands(&mut bands, other_addrs.clone(), direct_addr.ip(), peer_declared_internal_port);
-
-                // add the default external addr
-                bands.push(AddrBand {
-                    necessary_ip: direct_addr.ip(),
-                    anticipated_ports: vec![peer_declared_internal_port]
-                });
-
-                let locally_bound_sockets = Self::generate_local_sockets(local_nat_info, first_local_socket)?;
-
-                Ok(Self {
-                    bands,
-                    locally_bound_sockets: Some(locally_bound_sockets)
-                })
-            }
-
-            NatType::PortPreserved(direct_addr, other_addrs, ..) => {
-                let mut bands = Vec::new();
-                // since there is no port translation, we assume the port below
-                Self::load_alternate_bands(&mut bands, other_addrs.clone(), *direct_addr,peer_declared_internal_port);
-
-                bands.push(AddrBand {
-                    necessary_ip: *direct_addr,
-                    anticipated_ports: vec![peer_declared_internal_port]
-                });
-
-                let locally_bound_sockets = Self::generate_local_sockets(local_nat_info, first_local_socket)?;
-
-                Ok(Self {
-                    bands,
-                    locally_bound_sockets: Some(locally_bound_sockets)
-                })
-            }
-
-            NatType::EDM(last_external_addr, other_addrs, delta, _) => {
-                let mut bands = Vec::new();
-                Self::generate_predict_ports_config(*delta as _, *last_external_addr, &mut bands, other_addrs.clone(), peer_declared_internal_port, local_nat_info);
-                let locally_bound_sockets = Self::generate_local_sockets(local_nat_info, first_local_socket)?;
-
-                Ok(Self {
-                    bands,
-                    locally_bound_sockets: Some(locally_bound_sockets)
-                })
-            }
-
-            NatType::EDMRandomPort(last_external_addr, other_addrs, ..) => {
-                let mut bands = Vec::new();
-                let delta = peer_average_delta_opt.ok_or_else(||anyhow::Error::msg("Expected acceptable average delta"))?;
-                Self::generate_predict_ports_config(delta, *last_external_addr, &mut bands, other_addrs.clone(), peer_declared_internal_port, local_nat_info);
-                let locally_bound_sockets = Self::generate_local_sockets(local_nat_info, first_local_socket)?;
-
-                Ok(Self {
-                    bands,
-                    locally_bound_sockets: Some(locally_bound_sockets)
-                })
-            }
-
-            NatType::EDMRandomIp(_, _addr, _is_v6_allowed) => {
-                // Thanks to the preceding logic, if we get here, we know the local node has a predictable address
-                // evidently, the peer does not, however, this does not matter.
-                // The packets we send likely will not make contact with the peer. However, once the peer
-                // contacts us, we can then send a packet back to them. The peer will send packets to us at
-                // the addr implicated by the first local socket. Thus, we keep the current socket, and,
-                // create an empty send band
-
-                // NOTE: the above assertion about the local node having a predictable addr
-                // may not be true in localhost-testing mode. In the case we are in localhost-testing
-                // mode, AND, both 'nodes' are behind an unpredictable NAT, simply connect to the internal
-                // addr
-                if cfg!(feature = "localhost-testing") {
-                    log::trace!(target: "citadel", "Simulating peer has port preserved config");
-                    // pretend the peer NAT has a PortPreserved config
-                    let direct_addr = _addr.clone().ok_or_else(||anyhow::Error::msg("unable to simulate PortPreserved config"))?;
-                    let simulated_peer_nat = NatType::PortPreserved(direct_addr.internal_ip, Some(direct_addr), *_is_v6_allowed);
-                    Self::new(local_nat_info, &simulated_peer_nat, first_local_socket, peer_declared_internal_port)
-                } else {
-                    Ok(Self {
-                        bands: vec![],
-                        locally_bound_sockets: Some(vec![first_local_socket])
-                    })
-                }
-            }
-
-            NatType::EDMRandomIPPortPreserved(addrs, other_addrs, _) => {
-                // the peer has semi-predictable addrs, each with a spread of 1
-                // port is preserved, therefore, internal=external
-                let first_addr = addrs[0];
-                match first_addr {
-                    IpAddr::V4(v4) => {
-                        let mut octets = v4.octets();
-                        let mut bands = vec![];
-
-                        // use the first last external addr, mostly arbitrary
-                        Self::load_alternate_bands(&mut bands, other_addrs.clone(), addrs[0], peer_declared_internal_port);
-
-                        for _ in 0..4 {
-                            // increment the last octet by 1
-                            octets[3] = octets[3].wrapping_add(1);
-                            bands.push(AddrBand { necessary_ip: IpAddr::from(octets), anticipated_ports: vec![peer_declared_internal_port] })
-                        }
-
-                        Ok(Self {
-                            bands,
-                            locally_bound_sockets: Some(vec![first_local_socket])
-                        })
-                    },
-
-                    _ => {
-                        Err(anyhow::Error::msg("Should not be an ipv6 addr in this block ..."))
-                    }
-                }
-            }
-
-            _ => {
-                Err(anyhow::Error::msg("This function should not be called since one or more of the peers cannot be reached via STUN-like traversal"))
-            }
-        }
-    }
-
-    // `first_local_socket` is needed since it contains information vital for the adjacent node to connect,
-    // especially if behind the same LAN. For maximum likelihood of NAT traversal, it is recommended that if
-    // ipv6_is_enabled, the first_local_socket is also v6
-    fn generate_local_sockets(
-        _local_nat_info: &NatType,
-        first_local_socket: UdpSocket,
-    ) -> Result<Vec<UdpSocket>, anyhow::Error> {
-        let ret = vec![first_local_socket];
-        Ok(ret)
-    }
-
-    #[allow(dead_code)]
-    fn generate_bind_for_delta_config(
-        ret: &mut Vec<UdpSocket>,
-        delta: u16,
-        local_bind_addr: SocketAddr,
-    ) -> Result<(), anyhow::Error> {
-        // our internal port does not matter. The peer will predict the ports
-        // we bind to. We will open SPREAD * delta ports locally since the peer will expect
-        // that many ports to be open
-        let delta = Self::check_delta(delta);
-        let ports_to_bind_to = std::cmp::max(SPREAD * delta, 1);
-        for _ in 0..ports_to_bind_to {
-            ret.push(crate::socket_helpers::get_udp_socket(SocketAddr::new(
-                local_bind_addr.ip(),
-                0,
-            ))?);
-        }
-
-        Ok(())
-    }
-
-    fn check_delta(delta: u16) -> u16 {
-        // limit overflows when multiplying
-        std::cmp::min(delta, 30)
-    }
-
-    fn generate_predict_ports_config(
-        delta: u16,
-        last_external_addr: SocketAddr,
-        bands: &mut Vec<AddrBand>,
-        other_addrs: Option<IpAddressInfo>,
-        peer_declared_internal_port: u16,
-        local_nat_type: &NatType,
-    ) {
-        // We need to generate a band of possible connect addrs. If delta = 5, then,
-        // we create a band of delta * 6 possible connect addrs each with 1 port spacing
-        // apart. The maximum delta is defined as 30, which means this may try pinging up to
-        // 6 * 30 = 180 possible addresses
-        // const SPREAD: u16 = 6;
-        // if delta is zero (which would be odd), assume max of 1
-        let delta = Self::check_delta(delta);
-        let mut ports_to_target_count = std::cmp::max(SPREAD * delta, 1);
-
-        if matches!(local_nat_type, NatType::PortPreserved(..)) {
-            // since local's node is behind a predictable NAT, we don't have to worry about trying to
-            // target the peer's NAT. Just add 1 target. This will help prevent flooding
-            ports_to_target_count = 1;
-        }
-
-        // Note: with this type, it does NOT matter "where" the peer is bound to. To *predict* the port,
-        // we take the port of the last_external_addr, then, begin incrementing at one above it to
-        // ports_to_target_count
-        let beginning_port = last_external_addr.port().wrapping_add(1);
-        let ending_port = beginning_port.wrapping_add(ports_to_target_count);
-        let ports = (beginning_port..ending_port).collect::<Vec<u16>>();
-
-        Self::load_alternate_bands(
-            bands,
-            other_addrs,
-            last_external_addr.ip(),
-            peer_declared_internal_port,
-        );
-
-        // add the default external band
-        // note: this ASSUMES last_external_addr is ipv4 (which it should be, since the NAT identification
-        // subroutine uses STUNv4
-        bands.push(AddrBand {
-            necessary_ip: last_external_addr.ip(),
-            anticipated_ports: ports,
-        });
-    }
-
-    /// Loads alternate bands, assuming port preservation mostly accounting for internal LANS
-    fn load_alternate_bands(
-        bands: &mut Vec<AddrBand>,
-        other_addrs: Option<IpAddressInfo>,
-        last_external_addr: IpAddr,
-        peer_declared_internal_port: u16,
-    ) {
-        // add the default alternate band
-        if let Some(other_addrs) = other_addrs {
-            let anticipated_ports = vec![peer_declared_internal_port];
-
-            if other_addrs.internal_ip != last_external_addr {
-                bands.push(AddrBand {
-                    necessary_ip: other_addrs.internal_ip,
-                    // note: Here, we don't use the external ports above. We use the internal one
-                    // declared by the peer (note: this means the peer must deterministically generate this
-                    // prior to sending its information here. This means it must first bind to a new UDP socket
-                    // before sending over its information)
-                    anticipated_ports: anticipated_ports.clone(),
-                });
-            }
-
-            if let Some(external_v6) = other_addrs.external_ipv6 {
-                if external_v6 != last_external_addr {
-                    bands.push(AddrBand {
-                        necessary_ip: external_v6,
-                        // since this is an external v6 addr, we assume no need for port mapping
-                        // (v6 addresses have no need for predictive NAT traversal). We can then assume a 1:1 port
-                        // mapping from the peer declared internal port to its external port
-                        anticipated_ports,
-                    });
-                }
-            }
-        }
-    }
-}
-
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
 pub struct AddrBand {
-    necessary_ip: IpAddr,
-    anticipated_ports: Vec<u16>,
+    pub(crate) necessary_ip: IpAddr,
+    pub(crate) anticipated_ports: Vec<u16>,
 }
 
 impl IntoIterator for HolePunchConfig {
     type Item = SocketAddr;
-    type IntoIter = std::vec::IntoIter<SocketAddr>;
+    type IntoIter = std::collections::hash_set::IntoIter<SocketAddr>;
 
     fn into_iter(mut self) -> Self::IntoIter {
-        let mut ret = vec![];
+        // Use a HashSet to enforce uniqueness
+        let mut ret = HashSet::new();
 
-        for band in self.bands.iter_mut() {
+        for mut band in self.bands.drain(..) {
             for next in band.by_ref() {
-                ret.push(next);
+                if next.ip() == IpAddr::from([0, 0, 0, 0]) {
+                    // we never want to send to 0.0.0.0 addrs, only loopbacks
+                    ret.insert(SocketAddr::new(IpAddr::from([127, 0, 0, 1]), next.port()));
+                } else {
+                    ret.insert(next);
+                }
             }
         }
 
@@ -327,129 +52,47 @@ impl Iterator for AddrBand {
     }
 }
 
-#[cfg(test)]
-mod tests {
-    use crate::nat_identification::NatType;
-    use crate::udp_traversal::hole_punch_config::HolePunchConfig;
-    use crate::udp_traversal::udp_hole_puncher::get_optimal_bind_socket;
-    use async_ip::IpAddressInfo;
-    use std::net::{IpAddr, SocketAddr};
-    use std::str::FromStr;
-
-    #[tokio::test]
-    async fn test_hole_punch_config() {
-        let dummy_addr = SocketAddr::from_str("127.0.0.1:1234").unwrap();
-        let dummy_ip0_ok = IpAddr::from_str("123.100.200.100").unwrap();
-        let dummy_ip1_ok = IpAddr::from_str("123.100.200.101").unwrap();
-        let dummy_ip2_ok = IpAddr::from_str("123.100.200.102").unwrap();
-        let dummy_ip3_err = IpAddr::from_str("100.100.200.103").unwrap();
-
-        let ip_addr_info_dummy = Some(IpAddressInfo {
-            internal_ip: IpAddr::from([0, 0, 0, 0]),
-            external_ipv6: None,
-        });
-
-        let eim = &NatType::EIM(dummy_addr, ip_addr_info_dummy.clone(), true);
-        let edm = &NatType::EDM(dummy_addr, ip_addr_info_dummy.clone(), 1, true);
-        let port_preserved =
-            &NatType::PortPreserved(dummy_addr.ip(), ip_addr_info_dummy.clone(), true);
-        let random_port_compat = &NatType::EDMRandomPort(
-            dummy_addr,
-            ip_addr_info_dummy.clone(),
-            vec![10, 20, 30, 40],
-            true,
-        );
-        let random_port_bad = &NatType::EDMRandomPort(
-            dummy_addr,
-            ip_addr_info_dummy.clone(),
-            vec![40, 80, 120],
-            true,
-        );
-        let random_ip =
-            &NatType::EDMRandomIp(vec![dummy_addr.ip()], ip_addr_info_dummy.clone(), true);
-        let random_ip_port_preserved_ok = &NatType::EDMRandomIPPortPreserved(
-            vec![dummy_ip0_ok, dummy_ip1_ok, dummy_ip2_ok],
-            ip_addr_info_dummy.clone(),
-            true,
-        );
-        let random_ip_port_preserved_err = &NatType::EDMRandomIPPortPreserved(
-            vec![dummy_ip0_ok, dummy_ip1_ok, dummy_ip3_err],
-            ip_addr_info_dummy,
-            true,
-        );
-
-        // Start with EIM
-        inner_test(eim, eim);
-        inner_test(eim, edm);
-        inner_test(eim, port_preserved);
-        inner_test(eim, random_port_compat);
-
-        inner_test(eim, random_port_bad);
-        inner_test(eim, random_ip);
-
-        inner_test(eim, random_ip_port_preserved_ok);
-        inner_test(eim, random_ip_port_preserved_err);
-
-        // EDM
-        inner_test(edm, edm);
-        inner_test(edm, port_preserved);
-        inner_test(edm, random_port_compat);
-
-        inner_test(edm, random_port_bad);
-        inner_test(edm, random_ip);
-        inner_test(edm, random_ip_port_preserved_ok);
-        inner_test(edm, random_ip_port_preserved_err);
-
-        // PortPreserved
-        inner_test(port_preserved, port_preserved);
-        inner_test(port_preserved, random_port_compat);
-
-        inner_test(port_preserved, random_port_bad);
-        inner_test(port_preserved, random_ip);
-        inner_test(port_preserved, random_ip_port_preserved_ok);
-        inner_test(port_preserved, random_ip_port_preserved_err);
-
-        // Random port (compat)
-        inner_test(random_port_compat, random_port_compat);
-        inner_test(random_port_compat, random_port_bad);
-        inner_test(random_port_compat, random_ip);
-        inner_test(random_port_compat, random_ip_port_preserved_ok);
-        inner_test(random_port_compat, random_ip_port_preserved_err);
-
-        // Random port (bad)
-        inner_test(random_port_bad, random_ip_port_preserved_ok);
+impl HolePunchConfig {
+    pub fn new(
+        peer_nat: &NatType,
+        peer_internal_addr: &SocketAddr,
+        local_socket: UdpSocket,
+    ) -> Self {
+        let mut this = if let Some(bands) = peer_nat.predict(peer_internal_addr) {
+            Self {
+                bands,
+                locally_bound_sockets: Some(vec![local_socket]),
+            }
+        } else if cfg!(feature = "localhost-testing") {
+            log::info!(target: "citadel", "Will revert to localhost testing mode (not recommended for production use (peer addr: {:?}))", peer_internal_addr);
+            Self {
+                bands: get_localhost_bands(peer_internal_addr),
+                locally_bound_sockets: Some(vec![local_socket]),
+            }
+        } else {
+            // the peer nat is untraversable. However, they may still be able to connect to this node.
+            // As such, we will only listen:
+            Self {
+                bands: vec![AddrBand {
+                    necessary_ip: peer_internal_addr.ip(),
+                    anticipated_ports: vec![peer_internal_addr.port()],
+                }],
+                locally_bound_sockets: Some(vec![local_socket]),
+            }
+        };
 
-        // Random ip
-        inner_test(random_ip, random_ip_port_preserved_ok);
+        // Sometimes, even on localhost testing, both NATs are predictable, therefore the second branch above
+        // does not execute. This means that it entirely misses out on the localhost adjacent node.
+        // Therefore, we need to add it here:
+        this.bands.extend(get_localhost_bands(peer_internal_addr));
 
-        // random ip, port preserved, delta ok
-        inner_test(random_ip_port_preserved_ok, random_ip_port_preserved_ok);
-        inner_test(random_ip_port_preserved_ok, random_ip_port_preserved_err);
+        this
     }
+}
 
-    fn inner_test(local_nat_type: &NatType, peer_nat_type: &NatType) {
-        assert!(local_nat_type.stun_compatible(peer_nat_type));
-        let initial_socket_local = get_optimal_bind_socket(local_nat_type, peer_nat_type).unwrap();
-        let internal_bind_port_local = initial_socket_local.local_addr().unwrap().port();
-
-        let initial_socket_remote = get_optimal_bind_socket(peer_nat_type, local_nat_type).unwrap();
-        let internal_bind_port_remote = initial_socket_remote.local_addr().unwrap().port();
-        // TODO: assertions
-        // local
-        HolePunchConfig::new(
-            local_nat_type,
-            peer_nat_type,
-            initial_socket_local,
-            internal_bind_port_remote,
-        )
-        .unwrap();
-        // remote
-        HolePunchConfig::new(
-            peer_nat_type,
-            local_nat_type,
-            initial_socket_remote,
-            internal_bind_port_local,
-        )
-        .unwrap();
-    }
+fn get_localhost_bands(peer_internal_addr: &SocketAddr) -> Vec<AddrBand> {
+    vec![AddrBand {
+        necessary_ip: IpAddr::from([127, 0, 0, 1]),
+        anticipated_ports: vec![peer_internal_addr.port()],
+    }]
 }
diff --git a/citadel_wire/src/udp_traversal/linear/method3.rs b/citadel_wire/src/udp_traversal/linear/method3.rs
index bfb1003b4..6797e4e64 100644
--- a/citadel_wire/src/udp_traversal/linear/method3.rs
+++ b/citadel_wire/src/udp_traversal/linear/method3.rs
@@ -106,8 +106,8 @@ impl Method3 {
 
         let receiver_task = async move {
             // we are only interested in the first receiver to receive a value
-            if let Ok(res) = tokio::time::timeout(
-                Duration::from_millis(2000),
+            let timeout = tokio::time::timeout(
+                Duration::from_millis(3000),
                 Self::recv_until(
                     socket_wrapper,
                     encryptor,
@@ -117,15 +117,13 @@ impl Method3 {
                     this_node_type,
                     packet_send_params,
                 ),
-            )
-            .await
-            .map_err(|err| FirewallError::HolePunch(err.to_string()))?
-            {
-                Ok(res)
-            } else {
-                Err(FirewallError::HolePunch(
-                    "No UDP penetration detected".to_string(),
-                ))
+            );
+
+            match timeout.await {
+                Ok(res) => res,
+                Err(_) => Err(FirewallError::HolePunch(
+                    "Timeout while waiting for UDP penetration".to_string(),
+                )),
             }
         };
 
@@ -199,9 +197,16 @@ impl Method3 {
 
                 let packet = encryptor.generate_packet(&packet_plaintext);
                 log::trace!(target: "citadel", "Sending TTL={} to {} || {:?}", ttl, endpoint, &packet[..] as &[u8]);
-                if !socket.send(&packet, *endpoint, Some(ttl)).await? {
-                    log::trace!(target: "citadel", "Early-terminating SYN barrage");
-                    return Ok(());
+                match socket.send(&packet, *endpoint, Some(ttl)).await {
+                    Ok(can_continue) => {
+                        if !can_continue {
+                            log::trace!(target: "citadel", "Early-terminating SYN barrage");
+                            return Ok(());
+                        }
+                    }
+                    Err(err) => {
+                        log::warn!(target: "citadel", "Error sending packet from {:?}: {:?}", socket.socket.local_addr()?, err);
+                    }
                 }
             }
         }
@@ -223,101 +228,115 @@ impl Method3 {
         log::trace!(target: "citadel", "[Hole-punch] Listening on {:?}", socket.socket.local_addr().unwrap());
 
         let mut has_received_syn = false;
-        //let mut recv_from_required = None;
-        while let Ok((len, peer_external_addr)) = socket.recv_from(buf).await {
-            log::trace!(target: "citadel", "[UDP Hole-punch] RECV packet from {:?} | {:?}", &peer_external_addr, &buf[..len]);
-            let packet = match encryptor.decrypt_packet(&buf[..len]) {
-                Some(plaintext) => plaintext,
-                _ => {
-                    log::warn!(target: "citadel", "BAD Hole-punch packet: decryption failed!");
-                    continue;
-                }
-            };
-
-            match bincode2::deserialize(&packet)
-                .map_err(|err| FirewallError::HolePunch(err.to_string()))
-            {
-                Ok(NatPacket::Syn(peer_unique_id, ttl, adjacent_node_type, their_send_addr)) => {
-                    if adjacent_node_type == this_node_type {
-                        log::warn!(target: "citadel", "RECV loopback packet; will discard");
-                        continue;
+        loop {
+            match socket.recv_from(buf).await {
+                Ok((len, peer_external_addr)) => {
+                    log::trace!(target: "citadel", "[UDP Hole-punch] RECV packet from {:?} | {:?}", &peer_external_addr, &buf[..len]);
+                    let packet = match encryptor.decrypt_packet(&buf[..len]) {
+                        Some(plaintext) => plaintext,
+                        _ => {
+                            log::warn!(target: "citadel", "BAD Hole-punch packet: decryption failed!");
+                            continue;
+                        }
+                    };
+
+                    match bincode2::deserialize(&packet)
+                        .map_err(|err| FirewallError::HolePunch(err.to_string()))
+                    {
+                        Ok(NatPacket::Syn(
+                            peer_unique_id,
+                            ttl,
+                            adjacent_node_type,
+                            their_send_addr,
+                        )) => {
+                            if adjacent_node_type == this_node_type {
+                                log::warn!(target: "citadel", "RECV loopback packet; will discard");
+                                continue;
+                            }
+
+                            if has_received_syn {
+                                continue;
+                            }
+
+                            log::trace!(target: "citadel", "RECV SYN from {:?}", peer_unique_id);
+                            let hole_punched_addr = TargettedSocketAddr::new(
+                                peer_external_addr,
+                                peer_external_addr,
+                                peer_unique_id,
+                            );
+
+                            observed_addrs_on_syn
+                                .lock()
+                                .insert(peer_unique_id, hole_punched_addr);
+                            log::trace!(target: "citadel", "Received TTL={} packet from {:?}. Awaiting mutual recognition... ", ttl, peer_external_addr);
+
+                            has_received_syn = true;
+
+                            let send_addrs = send_packet_params
+                                .endpoints
+                                .iter()
+                                .copied()
+                                .chain(std::iter::once(peer_external_addr))
+                                .collect::<Vec<SocketAddr>>();
+
+                            let mut send_params = send_packet_params.clone();
+                            send_params.endpoints = &send_addrs;
+
+                            Self::send_packet_barrage(&send_params, Some(their_send_addr))
+                                .await
+                                .map_err(|err| FirewallError::HolePunch(err.to_string()))?;
+                        }
+
+                        // the reception of a SynAck proves the existence of a hole punched since there is bidirectional communication through the NAT
+                        Ok(NatPacket::SynAck(
+                            adjacent_unique_id,
+                            adjacent_node_type,
+                            address_we_sent_to,
+                        )) => {
+                            log::trace!(target: "citadel", "RECV SYN_ACK");
+                            if adjacent_node_type == this_node_type {
+                                log::warn!(target: "citadel", "RECV self-referential packet; will discard");
+                                continue;
+                            }
+
+                            // NOTE: it is entirely possible that we receive a SynAck before even getting a Syn.
+                            // Since we send SYNs to the other node, and, it's possible that we don't receive a SYN by the time
+                            // the other node ACKs our sent out SYN, we should not terminate.
+                            let expected_addr = address_we_sent_to;
+
+                            if peer_external_addr != expected_addr {
+                                log::warn!(target: "citadel", "[will allow] RECV SYN_ACK that comes from the wrong addr. RECV: {:?}, Expected: {:?}", peer_external_addr, expected_addr);
+                                //continue;
+                            }
+
+                            // this means there was a successful ping-pong.
+                            // initial should be address_we_went_to, natted would be peer_external_addr
+                            let hole_punched_addr = TargettedSocketAddr::new(
+                                address_we_sent_to,
+                                peer_external_addr,
+                                adjacent_unique_id,
+                            );
+                            log::trace!(target: "citadel", "***UDP Hole-punch to {:?} success!***", &hole_punched_addr);
+                            socket.stop_outgoing_traffic().await;
+
+                            return Ok(hole_punched_addr);
+                        }
+
+                        Err(err) => {
+                            log::warn!(target: "citadel", "Unable to deserialize packet {:?} from {:?}: {:?}", &packet[..], peer_external_addr, err);
+                        }
                     }
-
-                    if has_received_syn {
-                        continue;
-                    }
-
-                    log::trace!(target: "citadel", "RECV SYN from {:?}", peer_unique_id);
-                    let hole_punched_addr = TargettedSocketAddr::new(
-                        peer_external_addr,
-                        peer_external_addr,
-                        peer_unique_id,
-                    );
-
-                    observed_addrs_on_syn
-                        .lock()
-                        .insert(peer_unique_id, hole_punched_addr);
-                    log::trace!(target: "citadel", "Received TTL={} packet from {:?}. Awaiting mutual recognition... ", ttl, peer_external_addr);
-
-                    has_received_syn = true;
-
-                    let send_addrs = send_packet_params
-                        .endpoints
-                        .iter()
-                        .copied()
-                        .chain(std::iter::once(peer_external_addr))
-                        .collect::<Vec<SocketAddr>>();
-
-                    let mut send_params = send_packet_params.clone();
-                    send_params.endpoints = &send_addrs;
-
-                    Self::send_packet_barrage(&send_params, Some(their_send_addr))
-                        .await
-                        .map_err(|err| FirewallError::HolePunch(err.to_string()))?;
-                }
-
-                // the reception of a SynAck proves the existence of a hole punched since there is bidirectional communication through the NAT
-                Ok(NatPacket::SynAck(
-                    adjacent_unique_id,
-                    adjacent_node_type,
-                    address_we_sent_to,
-                )) => {
-                    log::trace!(target: "citadel", "RECV SYN_ACK");
-                    if adjacent_node_type == this_node_type {
-                        log::warn!(target: "citadel", "RECV self-referential packet; will discard");
-                        continue;
-                    }
-
-                    // NOTE: it is entirely possible that we receive a SynAck before even getting a Syn.
-                    // Since we send SYNs to the other node, and, it's possible that we don't receive a SYN by the time
-                    // the other node ACKs our sent out SYN, we should not terminate.
-                    let expected_addr = address_we_sent_to;
-
-                    if peer_external_addr != expected_addr {
-                        log::warn!(target: "citadel", "[will allow] RECV SYN_ACK that comes from the wrong addr. RECV: {:?}, Expected: {:?}", peer_external_addr, expected_addr);
-                        //continue;
-                    }
-
-                    // this means there was a successful ping-pong.
-                    // initial should be address_we_went_to, natted would be peer_external_addr
-                    let hole_punched_addr = TargettedSocketAddr::new(
-                        address_we_sent_to,
-                        peer_external_addr,
-                        adjacent_unique_id,
-                    );
-                    log::trace!(target: "citadel", "***UDP Hole-punch to {:?} success!***", &hole_punched_addr);
-                    socket.stop_outgoing_traffic().await;
-
-                    return Ok(hole_punched_addr);
                 }
 
                 Err(err) => {
-                    log::warn!(target: "citadel", "Unable to deserialize packet {:?} from {:?}: {:?}", &packet[..], peer_external_addr, err);
+                    log::error!(
+                        target: "citadel",
+                        "Error receiving packet from {:?}: {err:?}",
+                        socket.socket.local_addr()?
+                    )
                 }
             }
         }
-
-        Err(FirewallError::HolePunch("Socket recv error".to_string()))
     }
 }
 
diff --git a/citadel_wire/src/udp_traversal/mod.rs b/citadel_wire/src/udp_traversal/mod.rs
index f11e93d8c..cb439a581 100644
--- a/citadel_wire/src/udp_traversal/mod.rs
+++ b/citadel_wire/src/udp_traversal/mod.rs
@@ -9,7 +9,7 @@ pub mod targetted_udp_socket_addr;
 
 pub mod udp_hole_puncher;
 
-mod hole_punch_config;
+pub mod hole_punch_config;
 pub mod multi;
 
 #[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize)]
diff --git a/citadel_wire/src/udp_traversal/multi/mod.rs b/citadel_wire/src/udp_traversal/multi/mod.rs
index 5b9f2e79d..f69c44207 100644
--- a/citadel_wire/src/udp_traversal/multi/mod.rs
+++ b/citadel_wire/src/udp_traversal/multi/mod.rs
@@ -105,9 +105,11 @@ async fn drive(
         None
     };
 
+    log::trace!(target: "citadel", "Initiating subscription ...");
     // initiate a dedicated channel for sending packets for coordination
     let conn = &(app.initiate_subscription().await?);
 
+    log::trace!(target: "citadel", "Initiating NetMutex ...");
     // setup a mutex for handling contentions
     let net_mutex = &(app.mutex::<Option<()>>(value).await?);
 
@@ -270,21 +272,19 @@ async fn drive(
                         if net_lock.as_ref().is_none() {
                             log::trace!(target: "citadel", "*** Local won! Will command other side to use ({:?}, {:?})", peer_unique_id, local_id);
                             *net_lock = Some(());
-                            socket.cleanse()?;
-                            submit_final_candidate(socket)?;
+                            let _ = socket.cleanse();
                             // Hold the mutex to prevent the other side from accessing the data. It will need to end via the other means
                             send(DualStackCandidate::MutexSet(peer_unique_id, local_id), conn)
                                 .await?;
+                            submit_final_candidate(socket)?;
                             log::trace!(target: "citadel", "*** [winner] Awaiting the signal ...");
-                            std::mem::drop(net_lock);
                             // the winner will drop once the adjacent node sends a WinnerCanEnd signal
                             winner_can_end_rx.await?;
                             log::trace!(target: "citadel", "*** [winner] received the signal");
+                            std::mem::drop(net_lock);
                             return signal_done();
                         } else {
-                            unreachable!(
-                                "Should not happen since the winner holds the mutex until complete"
-                            );
+                            log::error!(target: "citadel", "This should not happen");
                         }
                     } else {
                         log::trace!(target: "citadel", "While looping, detected that the socket was taken")
@@ -317,7 +317,7 @@ async fn drive(
                 log::trace!(target: "citadel", "*** received MutexSet. Will unconditionally end ...");
                 assert!(loser_value_set.lock().replace((local, remote)).is_none());
                 let hole_punched_socket = assert_rebuild_ready(local, remote).await?;
-                hole_punched_socket.cleanse()?;
+                let _ = hole_punched_socket.cleanse();
                 submit_final_candidate(hole_punched_socket)?;
                 // return here. The winner must exit last
                 send(DualStackCandidate::WinnerCanEnd, conn).await?;
@@ -337,15 +337,24 @@ async fn drive(
     let sender_reader_combo = futures::future::try_join(futures_resolver, reader);
 
     tokio::select! {
-        res0 = sender_reader_combo => res0.map(|_| ())?,
-        res1 = done_rx => res1?,
-        res2 = futures_executor => res2?
+        res0 = sender_reader_combo => {
+            log::trace!(target: "citadel", "[DualStack] Sender/Reader combo finished {res0:?}");
+            res0.map(|_| ())?
+        },
+        res1 = done_rx => {
+            log::trace!(target: "citadel", "[DualStack] Done signal received {res1:?}");
+            res1?
+        },
+        res2 = futures_executor => {
+            log::trace!(target: "citadel", "[DualStack] Futures executor finished {res2:?}");
+            res2?
+        }
     };
 
     log::trace!(target: "citadel", "*** ENDING DualStack ***");
 
     let sock = final_candidate_rx.await?;
-    sock.cleanse()?;
+    let _ = sock.cleanse();
 
     Ok(sock)
 }
diff --git a/citadel_wire/src/udp_traversal/udp_hole_puncher.rs b/citadel_wire/src/udp_traversal/udp_hole_puncher.rs
index 2f12cf62f..684ce8615 100644
--- a/citadel_wire/src/udp_traversal/udp_hole_puncher.rs
+++ b/citadel_wire/src/udp_traversal/udp_hole_puncher.rs
@@ -71,22 +71,22 @@ async fn driver(
     stream.send_serialized(local_nat_type).await?;
     let peer_nat_type = &(stream.recv_serialized::<NatType>().await?);
 
-    log::trace!(target: "citadel", "[driver] Local NAT type: {:?} | Peer NAT type: {:?}", local_nat_type, peer_nat_type);
     let local_initial_socket = get_optimal_bind_socket(local_nat_type, peer_nat_type)?;
-    let internal_bind_port = local_initial_socket.local_addr()?.port();
+    let internal_bind_addr = local_initial_socket.local_addr()?;
 
     // exchange internal bind port, also synchronizing the beginning of the hole punch process
     // while doing so
-    let peer_internal_bind_port = conn.sync_exchange_payload(internal_bind_port).await?;
-
+    let peer_internal_bind_addr = conn.sync_exchange_payload(internal_bind_addr).await?;
+    log::trace!(target: "citadel", "\n~~~~~~~~~~~~\n [driver] Local NAT type: {:?}\n Peer NAT type: {:?}", local_nat_type, peer_nat_type);
+    log::trace!(target: "citadel", "[driver] Local internal bind addr: {internal_bind_addr:?}\nPeer internal bind addr: {peer_internal_bind_addr:?}");
+    log::trace!(target: "citadel", "\n~~~~~~~~~~~~\n");
     // the next functions takes everything insofar obtained into account without causing collisions with any existing
     // connections (e.g., no conflicts with the primary stream existing in conn)
     let hole_punch_config = HolePunchConfig::new(
-        local_nat_type,
         peer_nat_type,
+        &peer_internal_bind_addr,
         local_initial_socket,
-        peer_internal_bind_port,
-    )?;
+    );
 
     let conn = conn.clone();
     log::trace!(target: "citadel", "[driver] Synchronized; will now execute dualstack hole-puncher ... config: {:?}", hole_punch_config);
@@ -143,8 +143,14 @@ pub fn get_optimal_bind_socket(
         // bind to IN_ADDR6_ANY. Allows both conns from loopback and public internet
         crate::socket_helpers::get_udp_socket("[::]:0")
     } else {
-        // bind to IN_ADDR4_ANY. Allows both conns from loopback and public internet
-        crate::socket_helpers::get_udp_socket("0.0.0.0:0")
+        #[cfg(not(feature = "localhost-testing"))]
+        {
+            crate::socket_helpers::get_udp_socket("0.0.0.0:0")
+        }
+        #[cfg(feature = "localhost-testing")]
+        {
+            crate::socket_helpers::get_udp_socket("127.0.0.1:0")
+        }
     }
 }
 
@@ -196,28 +202,35 @@ mod tests {
         let client = citadel_io::spawn(client);
         let (res0, res1) = tokio::join!(server, client);
         log::trace!(target: "citadel", "JOIN complete! {:?} | {:?}", res0, res1);
-        let (res0, res1) = (res0.unwrap(), res1.unwrap());
-
-        let dummy_bytes = b"Hello, world!";
-
-        log::trace!(target: "citadel", "A");
-        res0.socket
-            .send_to(dummy_bytes as &[u8], res0.addr.send_address)
-            .await
-            .unwrap();
-        log::trace!(target: "citadel", "B");
-        let buf = &mut [0u8; 4096];
-        let (len, _addr) = res1.socket.recv_from(buf).await.unwrap();
-        //assert_eq!(res1.addr.receive_address, addr);
-        log::trace!(target: "citadel", "C");
-        assert_ne!(len, 0);
-        res1.socket
-            .send_to(dummy_bytes, res1.addr.send_address)
-            .await
-            .unwrap();
-        let (len, _addr) = res0.socket.recv_from(buf).await.unwrap();
-        assert_ne!(len, 0);
-        //assert_eq!(res0.addr.receive_address, addr);
-        log::trace!(target: "citadel", "D");
+        let (_res0, _res1) = (res0.unwrap(), res1.unwrap());
+
+        // due to the error "An existing connection was forcibly closed by the remote host",
+        // we must gate this test on localhost-testing + NOT windows
+        #[cfg(not(target_os = "windows"))]
+        {
+            let dummy_bytes = b"Hello, world!";
+
+            log::trace!(target: "citadel", "A");
+            _res0
+                .socket
+                .send_to(dummy_bytes as &[u8], _res0.addr.send_address)
+                .await
+                .unwrap();
+            log::trace!(target: "citadel", "B");
+            let buf = &mut [0u8; 4096];
+            let (len, _addr) = _res1.socket.recv_from(buf).await.unwrap();
+            //assert_eq!(res1.addr.receive_address, addr);
+            log::trace!(target: "citadel", "C");
+            assert_ne!(len, 0);
+            _res1
+                .socket
+                .send_to(dummy_bytes, _res1.addr.send_address)
+                .await
+                .unwrap();
+            let (len, _addr) = _res0.socket.recv_from(buf).await.unwrap();
+            assert_ne!(len, 0);
+            //assert_eq!(res0.addr.receive_address, addr);
+            log::trace!(target: "citadel", "D");
+        }
     }
 }
diff --git a/docker/client_server.yml b/docker/client_server.yml
index d418ce3cf..851ce11d6 100644
--- a/docker/client_server.yml
+++ b/docker/client_server.yml
@@ -34,6 +34,7 @@ services:
       STUN_2_ADDR: "172.28.0.12:3478"
       RUST_LOG: "citadel=trace"
       NAT_TYPE: "full_cone"
+      ADJACENT_NAT_TYPE: ${NAT_TYPE_CLIENT_A:?err}
     depends_on:
       - stun0
       - stun1


Test patch:
diff --git a/citadel_sdk/src/test_common.rs b/citadel_sdk/src/test_common.rs
index b266344f1..6428ef8dd 100644
--- a/citadel_sdk/src/test_common.rs
+++ b/citadel_sdk/src/test_common.rs
@@ -8,6 +8,7 @@ use crate::prelude::*;
 use futures::Future;
 use std::net::SocketAddr;
 use std::str::FromStr;
+use std::time::Duration;
 use tokio::net::TcpListener;
 
 #[allow(dead_code)]
@@ -157,6 +158,8 @@ pub async fn udp_mode_assertions(
             citadel_logging::info!(target: "citadel", "Inside UDP mode assertions AB2.5 ...");
             tx.unbounded_send(b"Hello, world!" as &[u8]).unwrap();
             assert_eq!(rx.next().await.unwrap().as_ref(), b"Hello, world!");
+            // wait to give time for the other side to receive the message
+            tokio::time::sleep(Duration::from_millis(500)).await;
             //wait_for_peers().await;
             std::mem::forget((tx, rx)); // do not run destructor to not trigger premature
         }