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

Issue:
Allow optional custom STUN servers
Allow each node to choose which STUN servers to communicate with when attempting to find their NAT type. 3 must be specified. This setting should be present inside the `NodeBuilder` struct in `citadel_sdk`, and, should propagate all the way to the library `citadel_wire` where the STUN servers are used. This will require editing many function signatures, but, will be a great first issue to solve as it will walk one through the execution flow of the program.




Discussion / hints:

Fix patch:
diff --git a/.dockerignore b/.dockerignore
index f10e58306..110531da8 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -2,6 +2,6 @@
 ./target
 target
 mobile
-mobile
 .idea/
-./deny.toml
\ No newline at end of file
+./deny.toml
+./wasi-sysroot
\ No newline at end of file
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index a19396f42..7ec126c35 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -173,3 +173,9 @@ jobs:
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
           files: ${{ env.GITHUB_WORKSPACE }}/lcov.info
+  docker_nat_client_server:
+    runs-on: ubuntu-latest
+    timeout-minutes: 80
+    steps:
+      - uses: actions/checkout@v3
+      - run: NAT_TYPE_CLIENT_A="full_cone" docker-compose -f ./docker/client_server.yml up --abort-on-container-exit --exit-code-from client_a
diff --git a/citadel_proto/src/kernel/kernel_executor.rs b/citadel_proto/src/kernel/kernel_executor.rs
index f3c4bc5fc..3a6ea4982 100644
--- a/citadel_proto/src/kernel/kernel_executor.rs
+++ b/citadel_proto/src/kernel/kernel_executor.rs
@@ -5,20 +5,16 @@ use tokio::runtime::Handle;
 use tokio::task::LocalSet;
 
 use citadel_user::account_manager::AccountManager;
-use citadel_wire::hypernode_type::NodeType;
 
 use crate::error::NetworkError;
 use crate::kernel::kernel_communicator::KernelAsyncCallbackHandler;
 use crate::kernel::kernel_trait::NetKernel;
-use crate::kernel::{KernelExecutorSettings, RuntimeFuture};
-use crate::proto::misc::underlying_proto::ServerUnderlyingProtocol;
+use crate::kernel::{KernelExecutorArguments, KernelExecutorSettings, RuntimeFuture};
 use crate::proto::node::HdpServer;
 use crate::proto::node_result::NodeResult;
 use crate::proto::outbound_sender::{unbounded, UnboundedReceiver};
 use crate::proto::packet_processor::includes::Duration;
 use crate::proto::remote::NodeRemote;
-use citadel_wire::exports::tokio_rustls::rustls::ClientConfig;
-use std::sync::Arc;
 
 /// Creates a [KernelExecutor]
 pub struct KernelExecutor<K: NetKernel> {
@@ -37,15 +33,17 @@ type KernelContext = (Handle, Pin<Box<dyn RuntimeFuture>>, Option<LocalSet>);
 impl<K: NetKernel> KernelExecutor<K> {
     /// Creates a new [KernelExecutor]. Panics if the server cannot start
     /// - underlying_proto: The proto to use for client to server communications
-    pub async fn new(
-        rt: Handle,
-        hypernode_type: NodeType,
-        account_manager: AccountManager,
-        kernel: K,
-        underlying_proto: ServerUnderlyingProtocol,
-        client_config: Option<Arc<ClientConfig>>,
-        kernel_executor_settings: KernelExecutorSettings,
-    ) -> Result<Self, NetworkError> {
+    pub async fn new(args: KernelExecutorArguments<K>) -> Result<Self, NetworkError> {
+        let KernelExecutorArguments::<K> {
+            rt,
+            hypernode_type,
+            account_manager,
+            kernel,
+            underlying_proto,
+            client_config,
+            kernel_executor_settings,
+            stun_servers,
+        } = args;
         let (server_to_kernel_tx, server_to_kernel_rx) = unbounded();
         let (server_shutdown_alerter_tx, server_shutdown_alerter_rx) =
             tokio::sync::oneshot::channel();
@@ -57,6 +55,7 @@ impl<K: NetKernel> KernelExecutor<K> {
             server_shutdown_alerter_tx,
             underlying_proto,
             client_config,
+            stun_servers,
         )
         .await
         .map_err(|err| NetworkError::Generic(err.to_string()))?;
diff --git a/citadel_proto/src/kernel/mod.rs b/citadel_proto/src/kernel/mod.rs
index 84d3d8ff5..e5a1b260c 100644
--- a/citadel_proto/src/kernel/mod.rs
+++ b/citadel_proto/src/kernel/mod.rs
@@ -1,7 +1,13 @@
+use citadel_user::account_manager::AccountManager;
+use citadel_wire::exports::ClientConfig;
+use citadel_wire::hypernode_type::NodeType;
+use std::sync::Arc;
 use tokio::macros::support::Future;
+use tokio::runtime::Handle;
 
 use crate::error::NetworkError;
 use crate::macros::ContextRequirements;
+use crate::prelude::ServerUnderlyingProtocol;
 
 /// for handling easy asynchronous callbacks
 pub mod kernel_communicator;
@@ -28,3 +34,14 @@ impl KernelExecutorSettings {
         self
     }
 }
+
+pub struct KernelExecutorArguments<K> {
+    pub rt: Handle,
+    pub hypernode_type: NodeType,
+    pub account_manager: AccountManager,
+    pub kernel: K,
+    pub underlying_proto: ServerUnderlyingProtocol,
+    pub client_config: Option<Arc<ClientConfig>>,
+    pub kernel_executor_settings: KernelExecutorSettings,
+    pub stun_servers: Option<Vec<String>>,
+}
diff --git a/citadel_proto/src/proto/node.rs b/citadel_proto/src/proto/node.rs
index 99da0b958..151e0d941 100644
--- a/citadel_proto/src/proto/node.rs
+++ b/citadel_proto/src/proto/node.rs
@@ -72,6 +72,7 @@ impl HdpServer {
         shutdown: tokio::sync::oneshot::Sender<()>,
         underlying_proto: ServerUnderlyingProtocol,
         client_config: Option<Arc<ClientConfig>>,
+        stun_servers: Option<Vec<String>>,
     ) -> io::Result<(
         NodeRemote,
         Pin<Box<dyn RuntimeFuture>>,
@@ -111,9 +112,12 @@ impl HdpServer {
             account_manager.clone(),
             time_tracker,
             client_config.clone(),
+            stun_servers.clone(),
         );
 
-        let nat_type = NatType::identify().await.map_err(|err| err.std())?;
+        let nat_type = NatType::identify(stun_servers)
+            .await
+            .map_err(|err| err.std())?;
 
         let inner = HdpServerInner {
             underlying_proto,
diff --git a/citadel_proto/src/proto/packet_processor/peer/peer_cmd_packet.rs b/citadel_proto/src/proto/packet_processor/peer/peer_cmd_packet.rs
index 7f83f36d3..bf17275a0 100644
--- a/citadel_proto/src/proto/packet_processor/peer/peer_cmd_packet.rs
+++ b/citadel_proto/src/proto/packet_processor/peer/peer_cmd_packet.rs
@@ -481,11 +481,13 @@ pub async fn process_peer_cmd(
                                             .outgoing_peer_connect_attempts
                                             .remove(&peer_cid);
                                         std::mem::drop(state_container);
+                                        let stun_servers = session.stun_servers.clone();
                                         let encrypted_config_container =
                                             generate_hole_punch_crypt_container(
                                                 endpoint_hyper_ratchet,
                                                 SecurityLevel::Standard,
                                                 peer_cid,
+                                                stun_servers,
                                             );
 
                                         // we need to use the session pqc since this signal needs to get processed by the center node
@@ -652,11 +654,13 @@ pub async fn process_peer_cmd(
                                         )
                                         .await
                                         .map_err(|err| NetworkError::Generic(err.to_string()))?;
+                                        let stun_servers = session.stun_servers.clone();
                                         let encrypted_config_container =
                                             generate_hole_punch_crypt_container(
                                                 endpoint_hyper_ratchet,
                                                 SecurityLevel::Standard,
                                                 peer_cid,
+                                                stun_servers,
                                             );
                                         let diff = Duration::from_nanos(i64::abs(
                                             timestamp - *sync_time_ns,
diff --git a/citadel_proto/src/proto/packet_processor/preconnect_packet.rs b/citadel_proto/src/proto/packet_processor/preconnect_packet.rs
index b84d61437..aa6279f1e 100644
--- a/citadel_proto/src/proto/packet_processor/preconnect_packet.rs
+++ b/citadel_proto/src/proto/packet_processor/preconnect_packet.rs
@@ -1,5 +1,5 @@
 use citadel_crypt::stacked_ratchet::StackedRatchet;
-use citadel_wire::udp_traversal::linear::encrypted_config_container::EncryptedConfigContainer;
+use citadel_wire::udp_traversal::linear::encrypted_config_container::HolePunchConfigContainer;
 use citadel_wire::udp_traversal::targetted_udp_socket_addr::HolePunchedUdpSocket;
 use netbeam::sync::RelativeNodeType;
 
@@ -248,11 +248,13 @@ pub async fn process_preconnect(
                     .await
                     .map_err(|err| NetworkError::Generic(err.to_string()))?);
                 log::trace!(target: "citadel", "Initiator created");
+                let stun_servers = session.stun_servers.clone();
                 let res = conn
                     .begin_udp_hole_punch(generate_hole_punch_crypt_container(
                         new_hyper_ratchet.clone(),
                         SecurityLevel::Standard,
                         C2S_ENCRYPTION_ONLY,
+                        stun_servers,
                     ))
                     .await;
 
@@ -350,12 +352,13 @@ pub async fn process_preconnect(
                     .await
                     .map_err(|err| NetworkError::Generic(err.to_string()))?);
                 log::trace!(target: "citadel", "Receiver created");
-
+                let stun_servers = session.stun_servers.clone();
                 let res = conn
                     .begin_udp_hole_punch(generate_hole_punch_crypt_container(
                         hyper_ratchet.clone(),
                         SecurityLevel::Standard,
                         C2S_ENCRYPTION_ONLY,
+                        stun_servers,
                     ))
                     .await;
 
@@ -614,10 +617,11 @@ pub(crate) fn generate_hole_punch_crypt_container(
     hyper_ratchet: StackedRatchet,
     security_level: SecurityLevel,
     target_cid: u64,
-) -> EncryptedConfigContainer {
+    stun_servers: Option<Vec<String>>,
+) -> HolePunchConfigContainer {
     let hyper_ratchet_cloned = hyper_ratchet.clone();
 
-    EncryptedConfigContainer::new(
+    HolePunchConfigContainer::new(
         move |plaintext| {
             packet_crafter::hole_punch::generate_packet(
                 &hyper_ratchet,
@@ -633,6 +637,7 @@ pub(crate) fn generate_hole_punch_crypt_container(
                 security_level,
             )
         },
+        stun_servers,
     )
 }
 
diff --git a/citadel_proto/src/proto/peer/p2p_conn_handler.rs b/citadel_proto/src/proto/peer/p2p_conn_handler.rs
index 8542e2a80..7f66a1b3a 100644
--- a/citadel_proto/src/proto/peer/p2p_conn_handler.rs
+++ b/citadel_proto/src/proto/peer/p2p_conn_handler.rs
@@ -20,7 +20,7 @@ use crate::proto::session::HdpSession;
 use crate::proto::state_container::VirtualConnectionType;
 use citadel_user::re_exports::__private::Formatter;
 use citadel_wire::exports::tokio_rustls::rustls;
-use citadel_wire::udp_traversal::linear::encrypted_config_container::EncryptedConfigContainer;
+use citadel_wire::udp_traversal::linear::encrypted_config_container::HolePunchConfigContainer;
 use citadel_wire::udp_traversal::targetted_udp_socket_addr::TargettedSocketAddr;
 use citadel_wire::udp_traversal::udp_hole_puncher::EndpointHolePunchExt;
 use netbeam::sync::network_endpoint::NetworkEndpoint;
@@ -281,7 +281,7 @@ pub(crate) async fn attempt_simultaneous_hole_punch(
     channel_signal: NodeResult,
     sync_time: Instant,
     ref app: NetworkEndpoint,
-    encrypted_config_container: EncryptedConfigContainer,
+    encrypted_config_container: HolePunchConfigContainer,
     client_config: Arc<rustls::ClientConfig>,
 ) -> std::io::Result<()> {
     let is_initiator = app.is_initiator();
diff --git a/citadel_proto/src/proto/session.rs b/citadel_proto/src/proto/session.rs
index 65cad551a..cb03fa7bc 100644
--- a/citadel_proto/src/proto/session.rs
+++ b/citadel_proto/src/proto/session.rs
@@ -196,6 +196,7 @@ pub struct HdpSessionInner {
     pub(super) connect_mode: DualRwLock<Option<ConnectMode>>,
     pub(super) client_config: Arc<rustls::ClientConfig>,
     pub(super) hypernode_peer_layer: HyperNodePeerLayer,
+    pub(super) stun_servers: Option<Vec<String>>,
     on_drop: UnboundedSender<()>,
 }
 
@@ -240,9 +241,9 @@ pub(crate) struct SessionInitParams {
     pub init_ticket: Ticket,
     pub client_config: Arc<rustls::ClientConfig>,
     pub hypernode_peer_layer: HyperNodePeerLayer,
-
     // this is set only when a local client is attempting to start an outbound session
     pub client_only_settings: Option<ClientOnlySessionInitSettings>,
+    pub stun_servers: Option<Vec<String>>,
 }
 
 pub(crate) struct ClientOnlySessionInitSettings {
@@ -335,6 +336,7 @@ impl HdpSession {
             .as_ref()
             .map(|r| r.keep_alive_timeout_ns)
             .unwrap_or(KEEP_ALIVE_TIMEOUT_NS);
+        let stun_servers = session_init_params.stun_servers;
 
         let mut inner = HdpSessionInner {
             hypernode_peer_layer,
@@ -374,6 +376,7 @@ impl HdpSession {
             stopper_tx: stopper_tx.clone().into(),
             queue_handle: DualLateInit::default(),
             client_config,
+            stun_servers,
         };
 
         if let Some(proposed_credentials) = session_init_params
diff --git a/citadel_proto/src/proto/session_manager.rs b/citadel_proto/src/proto/session_manager.rs
index 5d428e90f..a8997311e 100644
--- a/citadel_proto/src/proto/session_manager.rs
+++ b/citadel_proto/src/proto/session_manager.rs
@@ -69,6 +69,7 @@ pub struct HdpSessionManagerInner {
     clean_shutdown_tracker_tx: UnboundedSender<()>,
     clean_shutdown_tracker: Option<UnboundedReceiver<()>>,
     client_config: Arc<rustls::ClientConfig>,
+    stun_servers: Option<Vec<String>>,
 }
 
 impl HdpSessionManager {
@@ -79,6 +80,7 @@ impl HdpSessionManager {
         account_manager: AccountManager,
         time_tracker: TimeTracker,
         client_config: Arc<rustls::ClientConfig>,
+        stun_servers: Option<Vec<String>>,
     ) -> Self {
         let incoming_cxn_count = 0;
         let (clean_shutdown_tracker_tx, clean_shutdown_tracker_rx) = unbounded();
@@ -97,6 +99,7 @@ impl HdpSessionManager {
             kernel_tx,
             time_tracker,
             client_config,
+            stun_servers,
         };
 
         Self::from(inner)
@@ -151,6 +154,7 @@ impl HdpSessionManager {
                 peer_only_connect_mode,
                 proposed_credentials,
                 peer_layer,
+                stun_servers,
             ) = {
                 let (
                     remote,
@@ -162,6 +166,7 @@ impl HdpSessionManager {
                     cnac,
                     proposed_credentials,
                     peer_layer,
+                    stun_servers,
                 ) = {
                     let (peer_addr, cnac, proposed_credentials) = {
                         match &init_mode {
@@ -209,6 +214,7 @@ impl HdpSessionManager {
                     let account_manager = this.account_manager.clone();
                     let tt = this.time_tracker;
                     let peer_layer = this.hypernode_peer_layer.clone();
+                    let stun_servers = this.stun_servers.clone();
 
                     if let Some((init_time, ..)) = this.provisional_connections.get(&peer_addr) {
                         // Localhost is already trying to connect. However, it's possible that the entry has expired,
@@ -233,6 +239,7 @@ impl HdpSessionManager {
                         cnac,
                         proposed_credentials,
                         peer_layer,
+                        stun_servers,
                     )
                 };
 
@@ -260,6 +267,7 @@ impl HdpSessionManager {
                     peer_only_connect_mode,
                     proposed_credentials,
                     peer_layer,
+                    stun_servers,
                 )
             };
 
@@ -290,6 +298,7 @@ impl HdpSessionManager {
                 client_config: default_client_config.clone(),
                 hypernode_peer_layer: peer_layer,
                 client_only_settings: Some(client_only_settings),
+                stun_servers,
             };
 
             let (stopper, new_session) = HdpSession::new(session_init_params)?;
@@ -455,6 +464,7 @@ impl HdpSessionManager {
         let remote = this.server_remote.clone().unwrap();
         let client_config = this.client_config.clone();
         let peer_layer = this.hypernode_peer_layer.clone();
+        let stun_servers = this.stun_servers.clone();
 
         if let Some((init_time, ..)) = this.provisional_connections.get(&peer_addr) {
             if init_time.elapsed() > DO_CONNECT_EXPIRE_TIME_MS {
@@ -488,6 +498,7 @@ impl HdpSessionManager {
             client_config,
             hypernode_peer_layer: peer_layer,
             client_only_settings: None,
+            stun_servers,
         };
 
         let (stopper, new_session) = HdpSession::new(session_init_params)?;
diff --git a/citadel_sdk/examples/client.rs b/citadel_sdk/examples/client.rs
new file mode 100644
index 000000000..af9feec95
--- /dev/null
+++ b/citadel_sdk/examples/client.rs
@@ -0,0 +1,37 @@
+use citadel_sdk::prelude::*;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+#[tokio::main]
+async fn main() {
+    citadel_logging::setup_log();
+    let addr = get_env("CITADEL_SERVER_ADDR");
+    let stun0 = get_env("STUN_0_ADDR");
+    let stun1 = get_env("STUN_1_ADDR");
+    let stun2 = get_env("STUN_2_ADDR");
+
+    let finished = &AtomicBool::new(false);
+    let client = citadel_sdk::prefabs::client::single_connection
+    ::SingleClientServerConnectionKernel::new_register_defaults("Dummy user", "dummyusername", "notsecurepassword", addr, |_connection, remote| async move {
+        finished.store(true, Ordering::SeqCst);
+        remote.shutdown_kernel().await?;
+        Ok(())
+    }).unwrap();
+
+    let _ = NodeBuilder::default()
+        .with_node_type(NodeType::Peer)
+        .with_stun_servers([stun0, stun1, stun2])
+        .build(client)
+        .unwrap()
+        .await
+        .unwrap();
+
+    assert!(finished.load(Ordering::SeqCst));
+}
+
+fn get_env(key: &'static str) -> String {
+    if let Ok(env) = std::env::var(key) {
+        env
+    } else {
+        panic!("Expected the env_var {key} set")
+    }
+}
diff --git a/citadel_sdk/examples/server.rs b/citadel_sdk/examples/server.rs
new file mode 100644
index 000000000..59b23ac29
--- /dev/null
+++ b/citadel_sdk/examples/server.rs
@@ -0,0 +1,31 @@
+use citadel_sdk::prefabs::server::empty::EmptyKernel;
+use citadel_sdk::prelude::*;
+use std::net::SocketAddr;
+use std::str::FromStr;
+
+#[tokio::main]
+async fn main() {
+    citadel_logging::setup_log();
+    let addr = get_env("CITADEL_SERVER_ADDR");
+    let stun0 = get_env("STUN_0_ADDR");
+    let stun1 = get_env("STUN_1_ADDR");
+    let stun2 = get_env("STUN_2_ADDR");
+    let server = EmptyKernel::default();
+    let _ = NodeBuilder::default()
+        .with_node_type(NodeType::Server(
+            SocketAddr::from_str(addr.as_str()).unwrap(),
+        ))
+        .with_stun_servers([stun0, stun1, stun2])
+        .build(server)
+        .unwrap()
+        .await
+        .unwrap();
+}
+
+fn get_env(key: &'static str) -> String {
+    if let Ok(env) = std::env::var(key) {
+        env
+    } else {
+        panic!("Expected the env_var {key} set")
+    }
+}
diff --git a/citadel_sdk/src/builder/node_builder.rs b/citadel_sdk/src/builder/node_builder.rs
index 8767709df..5687e9d72 100644
--- a/citadel_sdk/src/builder/node_builder.rs
+++ b/citadel_sdk/src/builder/node_builder.rs
@@ -1,5 +1,6 @@
 use citadel_proto::prelude::*;
 
+use citadel_proto::kernel::KernelExecutorArguments;
 use citadel_proto::re_imports::RustlsClientConfig;
 use futures::Future;
 use std::fmt::{Debug, Formatter};
@@ -21,6 +22,7 @@ pub struct NodeBuilder {
     server_misc_settings: Option<ServerMiscSettings>,
     client_tls_config: Option<RustlsClientConfig>,
     kernel_executor_settings: Option<KernelExecutorSettings>,
+    stun_servers: Option<Vec<String>>,
 }
 
 /// An awaitable future whose return value propagates any internal protocol or kernel-level errors
@@ -72,6 +74,7 @@ impl NodeBuilder {
         let server_misc_settings = self.server_misc_settings.take();
         let client_config = self.client_tls_config.take().map(Arc::new);
         let kernel_executor_settings = self.kernel_executor_settings.take().unwrap_or_default();
+        let stun_servers = self.stun_servers.take();
 
         let underlying_proto = if let Some(proto) = self.underlying_protocol.take() {
             proto
@@ -95,8 +98,8 @@ impl NodeBuilder {
                     server_misc_settings,
                 )
                 .await?;
-                log::trace!(target: "citadel", "[NodeBuilder] Creating KernelExecutor ...");
-                let kernel_executor = KernelExecutor::new(
+
+                let args = KernelExecutorArguments {
                     rt,
                     hypernode_type,
                     account_manager,
@@ -104,8 +107,11 @@ impl NodeBuilder {
                     underlying_proto,
                     client_config,
                     kernel_executor_settings,
-                )
-                .await?;
+                    stun_servers,
+                };
+
+                log::trace!(target: "citadel", "[NodeBuilder] Creating KernelExecutor ...");
+                let kernel_executor = KernelExecutor::new(args).await?;
                 log::trace!(target: "citadel", "[NodeBuilder] Executing kernel");
                 kernel_executor.execute().await
             }),
@@ -239,6 +245,12 @@ impl NodeBuilder {
         Ok(self)
     }
 
+    /// Specifies custom STUN servers. If left unspecified, will use the defaults (twilio and Google STUN servers)
+    pub fn with_stun_servers<T: Into<String>, R: Into<Vec<T>>>(&mut self, servers: R) -> &mut Self {
+        self.stun_servers = Some(servers.into().into_iter().map(|t| t.into()).collect());
+        self
+    }
+
     fn check(&self) -> anyhow::Result<()> {
         #[cfg(feature = "google-services")]
         if let Some(svc) = self.services.as_ref() {
@@ -249,6 +261,14 @@ impl NodeBuilder {
             }
         }
 
+        if let Some(stun_servers) = self.stun_servers.as_ref() {
+            if stun_servers.len() != 3 {
+                return Err(anyhow::Error::msg(
+                    "There must be exactly 3 specified STUN servers",
+                ));
+            }
+        }
+
         Ok(())
     }
 }
@@ -281,6 +301,14 @@ mod tests {
             .is_err());
     }
 
+    #[test]
+    fn bad_config2() {
+        assert!(NodeBuilder::default()
+            .with_stun_servers(["dummy1", "dummy2"])
+            .build(EmptyKernel::default())
+            .is_err());
+    }
+
     #[rstest]
     #[tokio::test]
     #[timeout(std::time::Duration::from_secs(60))]
@@ -301,6 +329,7 @@ mod tests {
             .with_node_type(node_type)
             .with_kernel_executor_settings(kernel_settings.clone())
             .with_insecure_skip_cert_verification()
+            .with_stun_servers(["dummy1", "dummy1", "dummy3"])
             .with_native_certs()
             .await
             .unwrap();
diff --git a/citadel_user/Cargo.toml b/citadel_user/Cargo.toml
index 502ba23d0..351e14755 100644
--- a/citadel_user/Cargo.toml
+++ b/citadel_user/Cargo.toml
@@ -56,7 +56,7 @@ redis-base = { package = "redis", version = "0.21.7", features = ["tokio-comp",
 mobc = { version = "0.8.1", default-features = false, optional = true, features = ["tokio"] }
 firebase-rtdb = { path = "../firebase-rtdb", version = "0.4.0", optional = true }
 jwt = { version = "0.16.0", default-features = false, features = ["openssl"], optional = true }
-openssl = { version = "0.10.45", default-features = false, features = ["vendored"], optional = true }
+openssl = { version = "0.10.46", default-features = false, features = ["vendored"], optional = true }
 uuid = { version = "1.2.2", default-features = false, features = ["v4"] }
 bincode2 = { default-features = false, version = "2.0.1" }
 chrono = { default-features = false, version = "0.4.23", features = ["clock"] }
diff --git a/citadel_wire/Cargo.toml b/citadel_wire/Cargo.toml
index 1f5226b9c..71925c783 100644
--- a/citadel_wire/Cargo.toml
+++ b/citadel_wire/Cargo.toml
@@ -54,7 +54,7 @@ socket2 = { version = "0.5.1", features = ["all"] }
 rustls-native-certs = "0.6.2"
 igd = { version = "^0.12.0", features = ["aio"] }
 quinn = { version = "0.9.3", default-features = false, features = ["rustls"] }
-openssl = { version = "0.10.45", default-features = false, features = ["vendored"] }
+openssl = { version = "0.10.46", default-features = false, features = ["vendored"] }
 stun = { default-features = false, version = "0.4.4" }
 rcgen = { default-features = false, version = "0.10.0" }
 rustls = { version = "0.20.7", features = ["quic", "dangerous_configuration"] }
diff --git a/citadel_wire/src/standard/misc.rs b/citadel_wire/src/standard/misc.rs
index a772da421..570e0553a 100644
--- a/citadel_wire/src/standard/misc.rs
+++ b/citadel_wire/src/standard/misc.rs
@@ -1,4 +1,4 @@
-use openssl::pkcs12::ParsedPkcs12;
+use openssl::pkcs12::ParsedPkcs12_2 as ParsedPkcs12;
 use openssl::pkey::{PKey, Private};
 use openssl::stack::Stack;
 use openssl::x509::X509;
@@ -20,8 +20,12 @@ pub fn pkcs12_to_components(
     password: &str,
 ) -> Result<Pkcs12Decomposed, anyhow::Error> {
     let openssl_pkcs12 = openssl::pkcs12::Pkcs12::from_der(pkcs12_der)?;
-    let ParsedPkcs12 { chain, cert, pkey } = openssl_pkcs12.parse(password)?;
-    Ok((chain, cert, pkey))
+    let ParsedPkcs12 { ca, cert, pkey } = openssl_pkcs12.parse2(password)?;
+    Ok((
+        ca,
+        cert.ok_or_else(|| anyhow::Error::msg("Certificate missing from Pkcs12"))?,
+        pkey.ok_or_else(|| anyhow::Error::msg("Private key missing from Pkcs12"))?,
+    ))
 }
 
 pub fn pkcs12_to_quinn_keys(
diff --git a/citadel_wire/src/standard/nat_identification.rs b/citadel_wire/src/standard/nat_identification.rs
index f332d1cd9..6ee923273 100644
--- a/citadel_wire/src/standard/nat_identification.rs
+++ b/citadel_wire/src/standard/nat_identification.rs
@@ -1,4 +1,5 @@
 #![cfg_attr(feature = "localhost-testing-loopback-only", allow(unreachable_code))]
+
 use crate::error::FirewallError;
 use crate::socket_helpers::is_ipv6_enabled;
 use async_ip::IpAddressInfo;
@@ -6,6 +7,7 @@ use futures::stream::FuturesUnordered;
 use futures::{Future, StreamExt};
 use itertools::Itertools;
 use serde::{Deserialize, Serialize};
+use std::borrow::Cow;
 use std::net::{IpAddr, SocketAddr};
 use std::ops::Sub;
 use std::pin::Pin;
@@ -17,7 +19,6 @@ use stun::client::ClientBuilder;
 use stun::message::{Getter, Message, BINDING_REQUEST};
 use stun::xoraddr::XorMappedAddress;
 
-// TODO: Make stun servers configurable
 const STUN_SERVERS: [&str; 3] = [
     "global.stun.twilio.com:3478",
     "stun1.l.google.com:19302",
@@ -70,12 +71,15 @@ impl NatType {
         feature = "localhost-testing",
         tracing::instrument(target = "citadel", skip_all, ret, err(Debug))
     )]
-    pub async fn identify() -> Result<Self, FirewallError> {
-        Self::identify_timeout(IDENTIFY_TIMEOUT).await
+    pub async fn identify(stun_servers: Option<Vec<String>>) -> Result<Self, FirewallError> {
+        Self::identify_timeout(IDENTIFY_TIMEOUT, stun_servers).await
     }
 
     /// Identifies the NAT which the local node is behind
-    pub async fn identify_timeout(_timeout: Duration) -> Result<Self, FirewallError> {
+    pub async fn identify_timeout(
+        _timeout: Duration,
+        stun_servers: Option<Vec<String>>,
+    ) -> Result<Self, FirewallError> {
         /*
         #[cfg(feature = "localhost-testing-loopback-only")]
         {
@@ -85,7 +89,7 @@ impl NatType {
                 return Ok(nat_type);
             }
         }*/
-        match tokio::time::timeout(_timeout, get_nat_type()).await {
+        match tokio::time::timeout(_timeout, get_nat_type(stun_servers)).await {
             Ok(res) => res
                 .map_err(|err| FirewallError::HolePunch(err.to_string()))
                 .map(|nat_type| {
@@ -249,7 +253,13 @@ where
     feature = "localhost-testing",
     tracing::instrument(target = "citadel", skip_all, ret, err(Debug))
 )]
-async fn get_nat_type() -> Result<NatType, anyhow::Error> {
+async fn get_nat_type(stun_servers: Option<Vec<String>>) -> Result<NatType, anyhow::Error> {
+    let stun_servers = if let Some(stun_servers) = &stun_servers {
+        Cow::Owned(stun_servers.iter().map(|r| r.as_str()).collect())
+    } else {
+        Cow::Borrowed(&STUN_SERVERS as &[&str])
+    };
+
     let nat_type = async move {
         let mut msg = Message::new();
         msg.build(&[
@@ -261,7 +271,7 @@ async fn get_nat_type() -> Result<NatType, anyhow::Error> {
 
         let futures_unordered = FuturesUnordered::new();
 
-        for server in STUN_SERVERS.iter() {
+        for server in stun_servers.iter() {
             let task = async move {
                 let udp_sck = UdpSocket::bind(V4_BIND_ADDR).await?;
                 let new_bind_addr = udp_sck.local_addr()?;
@@ -408,7 +418,7 @@ async fn get_nat_type() -> Result<NatType, anyhow::Error> {
                 }
             }
 
-            _ => Err(anyhow::Error::msg("Unable to get both STUN addrs")),
+            _ => Err(anyhow::Error::msg("Unable to get all three STUN addrs")),
         }
     };
 
@@ -439,7 +449,7 @@ mod tests {
     #[tokio::test]
     async fn test_identify() {
         citadel_logging::setup_log();
-        let nat_type = NatType::identify().await.unwrap();
+        let nat_type = NatType::identify(None).await.unwrap();
         let traversal_type = nat_type.traversal_type_required();
         log::trace!(target: "citadel", "NAT Type: {:?} | Reaching this node will require: {:?} NAT traversal | Hypothetical connect scenario", nat_type, traversal_type);
     }
diff --git a/citadel_wire/src/udp_traversal/linear/encrypted_config_container.rs b/citadel_wire/src/udp_traversal/linear/encrypted_config_container.rs
index 07554b891..5f86abba5 100644
--- a/citadel_wire/src/udp_traversal/linear/encrypted_config_container.rs
+++ b/citadel_wire/src/udp_traversal/linear/encrypted_config_container.rs
@@ -3,34 +3,42 @@ use std::sync::Arc;
 
 /// Stores the functions relevant to encrypting and decrypting hole-punch related packets
 #[derive(Clone)]
-pub struct EncryptedConfigContainer {
+pub struct HolePunchConfigContainer {
     // the input into the function is the local port, which is expected to be inscribed inside the payload in an identifiable way
     generate_packet: CryptFunction<BytesMut>,
     // the input into the function are the encrypted bytes
     decrypt_packet: CryptFunction<Option<BytesMut>>,
+    // custom STUN servers
+    stun_servers: Option<Vec<String>>,
 }
 
 type CryptFunction<T> = Arc<dyn for<'a> Fn(&'a [u8]) -> T + Send + Sync + 'static>;
 
-impl EncryptedConfigContainer {
+impl HolePunchConfigContainer {
     /// Wraps the provided functions into a portable abstraction
     #[cfg(not(feature = "localhost-testing"))]
     pub fn new(
         _generate_packet: impl Fn(&[u8]) -> BytesMut + Send + Sync + 'static,
         _decrypt_packet: impl Fn(&[u8]) -> Option<BytesMut> + Send + Sync + 'static,
+        stun_servers: Option<Vec<String>>,
     ) -> Self {
         Self {
             generate_packet: Arc::new(_generate_packet),
             decrypt_packet: Arc::new(_decrypt_packet),
+            stun_servers,
         }
     }
 
+    // disable encryption
     #[cfg(feature = "localhost-testing")]
     pub fn new(
         _generate_packet: impl Fn(&[u8]) -> BytesMut + Send + Sync + 'static,
         _decrypt_packet: impl Fn(&[u8]) -> Option<BytesMut> + Send + Sync + 'static,
+        stun_servers: Option<Vec<String>>,
     ) -> Self {
-        Default::default()
+        let mut this: Self = Default::default();
+        this.stun_servers = stun_servers;
+        this
     }
 
     /// Generates a packet
@@ -42,14 +50,19 @@ impl EncryptedConfigContainer {
     pub fn decrypt_packet(&self, ciphertext: &[u8]) -> Option<BytesMut> {
         (self.decrypt_packet)(ciphertext)
     }
+
+    pub fn take_stun_servers(&mut self) -> Option<Vec<String>> {
+        self.stun_servers.take()
+    }
 }
 
-impl Default for EncryptedConfigContainer {
+impl Default for HolePunchConfigContainer {
     // identity transformation
     fn default() -> Self {
         Self {
             generate_packet: Arc::new(|input| BytesMut::from(input)),
             decrypt_packet: Arc::new(|input| Some(BytesMut::from(input))),
+            stun_servers: None,
         }
     }
 }
diff --git a/citadel_wire/src/udp_traversal/linear/method3.rs b/citadel_wire/src/udp_traversal/linear/method3.rs
index fb966f111..bfb1003b4 100644
--- a/citadel_wire/src/udp_traversal/linear/method3.rs
+++ b/citadel_wire/src/udp_traversal/linear/method3.rs
@@ -8,7 +8,7 @@ use tokio::time::Duration;
 
 use crate::error::FirewallError;
 use crate::socket_helpers::ensure_ipv6;
-use crate::udp_traversal::linear::encrypted_config_container::EncryptedConfigContainer;
+use crate::udp_traversal::linear::encrypted_config_container::HolePunchConfigContainer;
 use crate::udp_traversal::targetted_udp_socket_addr::TargettedSocketAddr;
 use crate::udp_traversal::HolePunchID;
 use citadel_io::Mutex;
@@ -19,7 +19,7 @@ use std::sync::Arc;
 // values". Source: page 7 of https://thomaspbraun.com/pdfs/NAT_Traversal/NAT_Traversal.pdf
 pub struct Method3 {
     this_node_type: RelativeNodeType,
-    encrypted_config: EncryptedConfigContainer,
+    encrypted_config: HolePunchConfigContainer,
     unique_id: HolePunchID,
     // in the case the adjacent node for id=key succeeds, yet, this node fails, recovery mode can ensue
     observed_addrs_on_syn: Mutex<HashMap<HolePunchID, TargettedSocketAddr>>,
@@ -36,7 +36,7 @@ impl Method3 {
     /// Make sure to complete the pre-process stage before calling this
     pub fn new(
         this_node_type: RelativeNodeType,
-        encrypted_config: EncryptedConfigContainer,
+        encrypted_config: HolePunchConfigContainer,
         unique_id: HolePunchID,
     ) -> Self {
         Self {
@@ -212,7 +212,7 @@ impl Method3 {
     // Handles the reception of packets, as well as sending/awaiting for a verification
     async fn recv_until(
         socket: &UdpWrapper<'_>,
-        encryptor: &EncryptedConfigContainer,
+        encryptor: &HolePunchConfigContainer,
         _unique_id: &HolePunchID,
         observed_addrs_on_syn: &Mutex<HashMap<HolePunchID, TargettedSocketAddr>>,
         _millis_delta: u64,
@@ -383,7 +383,7 @@ struct SendPacketBarrageParams<'a> {
     delta_ttl: Option<u32>,
     socket: &'a UdpWrapper<'a>,
     endpoints: &'a Vec<SocketAddr>,
-    encryptor: &'a EncryptedConfigContainer,
+    encryptor: &'a HolePunchConfigContainer,
     millis_delta: u64,
     count: u32,
     unique_id: HolePunchID,
diff --git a/citadel_wire/src/udp_traversal/linear/mod.rs b/citadel_wire/src/udp_traversal/linear/mod.rs
index 0ffe5d816..c6e6d19e5 100644
--- a/citadel_wire/src/udp_traversal/linear/mod.rs
+++ b/citadel_wire/src/udp_traversal/linear/mod.rs
@@ -6,7 +6,7 @@ use igd::PortMappingProtocol;
 use tokio::time::Duration;
 
 use crate::error::FirewallError;
-use crate::udp_traversal::linear::encrypted_config_container::EncryptedConfigContainer;
+use crate::udp_traversal::linear::encrypted_config_container::HolePunchConfigContainer;
 use crate::udp_traversal::linear::method3::Method3;
 use crate::udp_traversal::targetted_udp_socket_addr::{HolePunchedUdpSocket, TargettedSocketAddr};
 use crate::udp_traversal::{HolePunchID, NatTraversalMethod};
@@ -43,7 +43,7 @@ pub struct SingleUDPHolePuncher {
 impl SingleUDPHolePuncher {
     pub fn new(
         relative_node_type: RelativeNodeType,
-        encrypted_config_container: EncryptedConfigContainer,
+        encrypted_config_container: HolePunchConfigContainer,
         local_socket: UdpSocket,
         peer_addrs_to_ping: Vec<SocketAddr>,
     ) -> Result<Self, anyhow::Error> {
diff --git a/citadel_wire/src/udp_traversal/multi/mod.rs b/citadel_wire/src/udp_traversal/multi/mod.rs
index 47b7ce679..2cf42cf4e 100644
--- a/citadel_wire/src/udp_traversal/multi/mod.rs
+++ b/citadel_wire/src/udp_traversal/multi/mod.rs
@@ -11,7 +11,7 @@ use tokio::sync::mpsc::UnboundedReceiver;
 
 use crate::error::FirewallError;
 use crate::udp_traversal::hole_punch_config::HolePunchConfig;
-use crate::udp_traversal::linear::encrypted_config_container::EncryptedConfigContainer;
+use crate::udp_traversal::linear::encrypted_config_container::HolePunchConfigContainer;
 use crate::udp_traversal::linear::SingleUDPHolePuncher;
 use crate::udp_traversal::targetted_udp_socket_addr::HolePunchedUdpSocket;
 use crate::udp_traversal::{HolePunchID, NatTraversalMethod};
@@ -43,7 +43,7 @@ impl<'a> DualStackUdpHolePuncher<'a> {
     )]
     pub fn new(
         relative_node_type: RelativeNodeType,
-        encrypted_config_container: EncryptedConfigContainer,
+        encrypted_config_container: HolePunchConfigContainer,
         mut hole_punch_config: HolePunchConfig,
         napp: &'a NetworkEndpoint,
     ) -> Result<Self, anyhow::Error> {
diff --git a/citadel_wire/src/udp_traversal/udp_hole_puncher.rs b/citadel_wire/src/udp_traversal/udp_hole_puncher.rs
index a6ca66b3e..689ed0cd9 100644
--- a/citadel_wire/src/udp_traversal/udp_hole_puncher.rs
+++ b/citadel_wire/src/udp_traversal/udp_hole_puncher.rs
@@ -1,6 +1,6 @@
 use crate::nat_identification::NatType;
 use crate::udp_traversal::hole_punch_config::HolePunchConfig;
-use crate::udp_traversal::linear::encrypted_config_container::EncryptedConfigContainer;
+use crate::udp_traversal::linear::encrypted_config_container::HolePunchConfigContainer;
 use crate::udp_traversal::multi::DualStackUdpHolePuncher;
 use crate::udp_traversal::targetted_udp_socket_addr::HolePunchedUdpSocket;
 use citadel_io::UdpSocket;
@@ -21,14 +21,14 @@ const DEFAULT_TIMEOUT: Duration = Duration::from_millis(5000);
 impl<'a> UdpHolePuncher<'a> {
     pub fn new(
         conn: &'a NetworkEndpoint,
-        encrypted_config_container: EncryptedConfigContainer,
+        encrypted_config_container: HolePunchConfigContainer,
     ) -> Self {
         Self::new_timeout(conn, encrypted_config_container, DEFAULT_TIMEOUT)
     }
 
     pub fn new_timeout(
         conn: &'a NetworkEndpoint,
-        encrypted_config_container: EncryptedConfigContainer,
+        encrypted_config_container: HolePunchConfigContainer,
         timeout: Duration,
     ) -> Self {
         Self {
@@ -59,11 +59,12 @@ impl Future for UdpHolePuncher<'_> {
 )]
 async fn driver(
     conn: &NetworkEndpoint,
-    encrypted_config_container: EncryptedConfigContainer,
+    mut encrypted_config_container: HolePunchConfigContainer,
 ) -> Result<HolePunchedUdpSocket, anyhow::Error> {
     // create stream
     let stream = &(conn.initiate_subscription().await?);
-    let local_nat_type = &(NatType::identify()
+    let stun_servers = encrypted_config_container.take_stun_servers();
+    let local_nat_type = &(NatType::identify(stun_servers)
         .await
         .map_err(|err| anyhow::Error::msg(err.to_string()))?);
 
@@ -148,14 +149,14 @@ pub fn get_optimal_bind_socket(
 pub trait EndpointHolePunchExt {
     fn begin_udp_hole_punch(
         &self,
-        encrypted_config_container: EncryptedConfigContainer,
+        encrypted_config_container: HolePunchConfigContainer,
     ) -> UdpHolePuncher;
 }
 
 impl EndpointHolePunchExt for NetworkEndpoint {
     fn begin_udp_hole_punch(
         &self,
-        encrypted_config_container: EncryptedConfigContainer,
+        encrypted_config_container: HolePunchConfigContainer,
     ) -> UdpHolePuncher {
         UdpHolePuncher::new(self, encrypted_config_container)
     }
diff --git a/docker-compose.yml b/docker-compose.yml
deleted file mode 100644
index 56a3a0fba..000000000
--- a/docker-compose.yml
+++ /dev/null
@@ -1,75 +0,0 @@
-# Use root/example as user/password credentials
-version: '3.9'
-
-services:
-
-  db_mysql:
-    image: mysql
-    platform: linux/x86_64
-    command: --default-authentication-plugin=mysql_native_password
-    restart: always
-    ports:
-      - "127.0.0.1:3306:3306"
-    environment:
-      MYSQL_ROOT_PASSWORD: password
-      MYSQL_DATABASE: test_db
-    healthcheck:
-      test: [ "CMD", "curl", "-f", "http://127.0.0.1:3306" ]
-      interval: 30s
-      timeout: 10s
-      retries: 5
-
-  db_postgresql:
-    image: postgres
-    platform: linux/x86_64
-    restart: always
-    ports:
-      - "127.0.0.1:5432:5432"
-    environment:
-      POSTGRES_PASSWORD: password
-      POSTGRES_DB: test_db
-    healthcheck:
-      test: [ "CMD", "curl", "-f", "http://127.0.0.1:5432" ]
-      interval: 30s
-      timeout: 10s
-      retries: 5
-
-  clang:
-    platform: linux/x86_64
-    build:
-      context: ./
-      dockerfile: ./docker/rust_backend/Dockerfile
-      target: clang
-    depends_on:
-      - db_mysql
-      - db_postgresql
-
-  rust:
-    platform: linux/x86_64
-    build:
-      context: ./
-      dockerfile: ./docker/rust_backend/Dockerfile
-      target: rust
-
-  sccache:
-    platform: linux/x86_64
-    build:
-      context: ./
-      dockerfile: ./docker/rust_backend/Dockerfile
-      target: sccache
-
-  integration_tests:
-    platform: linux/x86_64
-    build:
-      context: ./
-      dockerfile: ./docker/rust_backend/Dockerfile
-      target: integration_tests
-    depends_on:
-      - db_mysql
-      - db_postgresql
-      - clang
-      - rust
-
-networks:
-  testnet:
-    driver: bridge
\ No newline at end of file
diff --git a/docker/client/Dockerfile b/docker/client/Dockerfile
new file mode 100644
index 000000000..e0dc8af97
--- /dev/null
+++ b/docker/client/Dockerfile
@@ -0,0 +1,10 @@
+FROM rust:1.67 as builder
+WORKDIR /usr/src/client
+ARG NAT_TYPE
+RUN export NAT_TYPE=$NAT_TYPE
+COPY . .
+COPY ./docker/set_nat.sh ./
+RUN apt-get update && apt-get install -y openssl libclang-dev build-essential cmake iptables inetutils-ping && rm -rf /var/lib/apt/lists/*
+RUN ./set_nat.sh
+RUN cargo install --example client --path ./citadel_sdk --debug
+CMD ["client"]
\ No newline at end of file
diff --git a/docker/client_server.yml b/docker/client_server.yml
new file mode 100644
index 000000000..17dce686e
--- /dev/null
+++ b/docker/client_server.yml
@@ -0,0 +1,101 @@
+version: '3'
+services:
+  client_a:
+#    image: ubuntu:latest
+    build:
+      context: ../
+      dockerfile: ./docker/client/Dockerfile
+      args:
+        NAT_TYPE: ${NAT_TYPE_CLIENT_A:?err}
+    environment:
+      CITADEL_SERVER_ADDR: "172.29.0.10:25000"
+      STUN_0_ADDR: "172.20.0.10:3478"
+      STUN_1_ADDR: "172.21.0.10:3478"
+      STUN_2_ADDR: "172.22.0.10:3478"
+      RUST_LOG: "citadel=trace"
+    depends_on:
+      - server
+    networks:
+      - frontend
+      - backend
+      - coturn0
+      - coturn1
+      - coturn2
+    cap_add:
+      - NET_ADMIN
+    privileged: true
+#    command: sh -c "apt-get update && apt-get install -y iptables inetutils-ping; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ping -c 3 server; ping -c 3 stun0"
+  server:
+#    image: ubuntu:latest
+    build:
+      context: ../
+      dockerfile: ./docker/server/Dockerfile
+      args:
+        NAT_TYPE: "full_cone"
+    environment:
+      CITADEL_SERVER_ADDR: "172.29.0.10:25000"
+      STUN_0_ADDR: "172.20.0.10:3478"
+      STUN_1_ADDR: "172.21.0.10:3478"
+      STUN_2_ADDR: "172.22.0.10:3478"
+      RUST_LOG: "citadel=trace"
+    depends_on:
+      - stun0
+      - stun1
+      - stun2
+    networks:
+      backend:
+        ipv4_address: 172.29.0.10
+      coturn0:
+      coturn1:
+      coturn2:
+    cap_add:
+      - NET_ADMIN
+    privileged: true
+#    entrypoint: sh -c "apt-get update && apt-get install -y iptables inetutils-ping; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; iptables -A FORWARD -j ACCEPT; iptables -t nat -A PREROUTING -i eth1 -j REDIRECT; sleep infinity"
+  stun0:
+    image: coturn/coturn:4.6.1-debian
+    networks:
+      frontend:
+      backend:
+      coturn0:
+        ipv4_address: 172.20.0.10
+  stun1:
+    image: coturn/coturn:4.6.1-debian
+    networks:
+      frontend:
+      backend:
+      coturn1:
+        ipv4_address: 172.21.0.10
+  stun2:
+    image: coturn/coturn:4.6.1-debian
+    networks:
+      frontend:
+      backend:
+      coturn2:
+        ipv4_address: 172.22.0.10
+networks:
+  frontend:
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.28.0.0/16
+  backend:
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.29.0.0/16
+  coturn0:
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.20.0.0/16
+  coturn1:
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.21.0.0/16
+  coturn2:
+    driver: bridge
+    ipam:
+      config:
+        - subnet: 172.22.0.0/16
\ No newline at end of file
diff --git a/docker/rust_backend/Dockerfile b/docker/rust_backend/Dockerfile
deleted file mode 100644
index 3ce543a75..000000000
--- a/docker/rust_backend/Dockerfile
+++ /dev/null
@@ -1,27 +0,0 @@
-# Dockerfile
-
-FROM silkeh/clang:13 as clang
-
-
-FROM rust:latest as rust
-
-
-FROM jonoh/sccache-rust as sccache
-
-
-FROM rust:latest as integration_tests
-WORKDIR /app
-
-COPY --from=clang /usr/local/lib/libclang.so /app
-COPY --from=sccache /usr/local/bin/sccache /app
-ENV LIBCLANG_PATH=/app
-ENV RUSTC_WRAPPER=/app/sccache
-ENV DCMAKE_C_COMPILER_LAUNCHER=sccache
-ENV DCMAKE_CXX_COMPILER_LAUNCHER=sccache
-
-COPY . /app
-
-EXPOSE 25021/tcp
-EXPOSE 25021/udp
-
-RUN cargo test --package citadel_user
\ No newline at end of file
diff --git a/docker/server/Dockerfile b/docker/server/Dockerfile
new file mode 100644
index 000000000..d42c2531c
--- /dev/null
+++ b/docker/server/Dockerfile
@@ -0,0 +1,10 @@
+FROM rust:1.67 as builder
+WORKDIR /usr/src/server
+ARG NAT_TYPE
+RUN export NAT_TYPE=$NAT_TYPE
+COPY . .
+COPY ./docker/set_nat.sh ./
+RUN apt-get update && apt-get install -y openssl libclang-dev build-essential cmake iptables inetutils-ping && rm -rf /var/lib/apt/lists/*
+RUN ./set_nat.sh
+RUN cargo install --example server --path ./citadel_sdk --debug
+CMD ["server"]
\ No newline at end of file
diff --git a/docker/set_nat.sh b/docker/set_nat.sh
new file mode 100755
index 000000000..12ba86143
--- /dev/null
+++ b/docker/set_nat.sh
@@ -0,0 +1,96 @@
+#!/bin/sh
+
+# Get the NAT type from environment variable (default to none)
+NAT_TYPE=${NAT_TYPE}
+
+# Get the container IP address from hostname command
+CONTAINER_IP=$(ifconfig eth0 | grep "inet addr:" | cut -d : -f 2 | cut -d " " -f 1)
+
+echo "NAT TYPE: $NAT_TYPE"
+echo "Container IP: $CONTAINER_IP"
+
+# Set up simulated latency
+tc qdisc add dev eth0 root netem delay 100ms
+
+# Configure iptables rules based on NAT type
+
+case $NAT_TYPE in
+
+  none)
+    # No NAT translation (use default rules)
+    echo "No NAT translation for $CONTAINER_IP"
+  ;;
+
+  full_cone)
+    # Full-cone NAT (one-to-one mapping of IP and port)
+
+     # Enable IP forwarding
+     echo 1 > /proc/sys/net/ipv4/ip_forward
+
+     # Flush existing rules
+     iptables -F
+     iptables -t nat -F
+
+     # Add MASQUERADE rule for outgoing packets
+     iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
+
+     # Add DNAT rule for incoming packets
+     iptables -t nat -A PREROUTING -i eth0 -j DNAT --to-destination $CONTAINER_IP
+
+     echo "Full-cone NAT translation for $CONTAINER_IP"
+   ;;
+
+   symmetric)
+     # Symmetric NAT (different mapping of IP and port for each destination)
+
+     # Enable IP forwarding
+     echo 1 > /proc/sys/net/ipv4/ip_forward
+
+     # Flush existing rules
+     iptables -F
+     iptables -t nat -F
+
+     # Add MASQUERADE rule for outgoing packets with random port selection
+     iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE --random
+
+      echo "Symmetric NAT translation for $CONTAINER_IP"
+   ;;
+
+   port_restricted)
+      # Port-restricted NAT (same mapping of IP and port for each destination, but only allow incoming packets from same source port)
+
+      # Enable IP forwarding
+      echo 1 > /proc/sys/net/ipv4/ip_forward
+
+      # Flush existing rules
+      iptables -F
+      iptables -t nat -F
+
+      # Add MASQUERADE rule for outgoing packets
+      iptables -t nat -A POSTROUTING -o eth0-j MASQUERADE
+
+      # Add DNAT rule for incoming packets with matching source port
+      iptables-t nat-A PREROUTING-i eth0-m state --state ESTABLISHED,RELATED-j DNAT--to-destination $CONTAINER_IP
+
+      echo "Port-restricted NAT translation for $CONTAINER_IP"
+   ;;
+
+   address_restricted)
+       # Address-restricted NAT (same mapping of IP and port for each destination, but only allow incoming packets from same source address)
+
+       # Enable IP forwarding
+       echo 1 > /proc/sys/net/ipv4/ip_forward
+
+       # Flush existing rules
+       iptables-F
+       iptables-t nat-F
+
+       # Add MASQUERADE rule for outgoing packets
+       iptables-t nat-A POSTROUTING-o eth0-j MASQUERADE
+
+       # Add DNAT rule for incoming packets with matching source address
+       iptables-t nat-A PREROUTING-i eth0-m state --state ESTABLISHED,RELATED-j DNAT--to-destination $CONTAINER_IP
+
+       echo "Address-restricted NAT translation for $CONTAINER_IP"
+    ;;
+esac
\ No newline at end of file


Test patch: