Skip to main content

citadel_sdk/builder/
node_builder.rs

1//! Node Builder API for Citadel Protocol
2//!
3//! This module provides a builder pattern interface for constructing and configuring Citadel network nodes.
4//! The builder supports creating both peer and server nodes with customizable settings for security,
5//! networking, and backend storage.
6//!
7//! # Features
8//! - Flexible node type configuration (Peer/Server)
9//! - Multiple backend storage options (In-memory, Filesystem, SQL with enterprise feature)
10//! - Customizable security settings (TLS, certificates)
11//! - Google services integration (optional)
12//! - STUN servers for NAT traversal: required for every node, with no default list
13//! - Server authentication via pre-shared keys
14//!
15//! # Example
16//! ```rust
17//! use citadel_sdk::prelude::*;
18//! use std::net::SocketAddr;
19//! use std::str::FromStr;
20//!
21//! // Create a basic server node
22//! let builder = DefaultNodeBuilder::default()
23//!     .with_node_type(NodeType::Server(SocketAddr::from_str("0.0.0.0:25021").unwrap()))
24//!     .with_stun_servers(["stun1.example.com:3478", "stun2.example.com:3478", "stun3.example.com:3478"])
25//!     .with_backend(BackendType::InMemory);
26//! ```
27//!
28//! # Important Notes
29//! - Every node requires at least three distinct STUN servers ([`NodeBuilder::with_stun_servers`])
30//! - Server nodes require a valid bind address
31//! - Default backend is filesystem-based when the "filesystem" feature is enabled
32//! - TLS is enabled by default with self-signed certificates
33//! - When using Google services, both services JSON and database config must be set
34//!
35//! # Related Components
36//! - [`NetKernel`]: Core networking kernel that processes node operations
37//! - [`KernelExecutor`]: Executor for running the network kernel
38//! - [`BackendType`]: Storage backend configurations
39
40use citadel_proto::prelude::*;
41
42use citadel_io::ServerMode;
43use citadel_proto::kernel::KernelExecutorArguments;
44use citadel_proto::macros::{ContextRequirements, LocalContextRequirements};
45use citadel_types::crypto::{HeaderObfuscatorSettings, PreSharedKey};
46use futures::Future;
47use std::fmt::{Debug, Formatter};
48use std::marker::PhantomData;
49use std::pin::Pin;
50use std::task::{Context, Poll};
51
52/// Used to construct a running client/peer or server instance
53pub struct NodeBuilder<R: Ratchet = StackedRatchet, T: PlatformOps = DefaultTransport> {
54    hypernode_type: Option<NodeType>,
55    underlying_protocol: Option<ServerMode<T>>,
56    backend_type: Option<BackendType>,
57    server_argon_settings: Option<ArgonDefaultServerSettings>,
58    #[cfg(feature = "google-services")]
59    services: Option<ServicesConfig>,
60    server_misc_settings: Option<ServerMiscSettings>,
61    client_tls_config: Option<T::ClientConfig>,
62    kernel_executor_settings: Option<KernelExecutorSettings>,
63    stun_servers: Option<Vec<String>>,
64    turn_servers: Option<Vec<TurnServerConfig>>,
65    local_only_server_settings: Option<ServerOnlySessionInitSettings>,
66    path_policy: Option<PathPolicy>,
67    websocket_listener: Option<citadel_io::WebSocketListen>,
68    injected_listener: Option<T::Listener>,
69    #[cfg(target_family = "wasm")]
70    serverless_config: Option<ServerlessConfig>,
71    _ratchet: PhantomData<R>,
72    _transport: PhantomData<T>,
73}
74
75/// Default node builder type using platform-appropriate transport
76pub type DefaultNodeBuilder = NodeBuilder<StackedRatchet, DefaultTransport>;
77/// Lightweight node builder using `MonoRatchet` for reduced cryptographic overhead
78pub type LightweightNodeBuilder = NodeBuilder<MonoRatchet, DefaultTransport>;
79
80impl<R: Ratchet, T: PlatformOps> Default for NodeBuilder<R, T> {
81    fn default() -> Self {
82        Self {
83            hypernode_type: None,
84            underlying_protocol: None,
85            backend_type: None,
86            server_argon_settings: None,
87            #[cfg(feature = "google-services")]
88            services: None,
89            server_misc_settings: None,
90            client_tls_config: None,
91            kernel_executor_settings: None,
92            stun_servers: None,
93            turn_servers: None,
94            local_only_server_settings: None,
95            path_policy: None,
96            websocket_listener: None,
97            injected_listener: None,
98            #[cfg(target_family = "wasm")]
99            serverless_config: None,
100            _ratchet: Default::default(),
101            _transport: Default::default(),
102        }
103    }
104}
105
106/// An awaitable future whose return value propagates any internal protocol or kernel-level errors
107pub struct NodeFuture<'a, K> {
108    inner: Pin<Box<dyn FutureContextRequirements<'a, Result<K, NetworkError>>>>,
109    _pd: PhantomData<fn() -> K>,
110}
111
112#[cfg(feature = "multi-threaded")]
113trait FutureContextRequirements<'a, Output>:
114    Future<Output = Output> + Send + LocalContextRequirements<'a>
115{
116}
117#[cfg(feature = "multi-threaded")]
118impl<'a, T: Future<Output = Output> + Send + LocalContextRequirements<'a>, Output>
119    FutureContextRequirements<'a, Output> for T
120{
121}
122
123#[cfg(not(feature = "multi-threaded"))]
124trait FutureContextRequirements<'a, Output>:
125    Future<Output = Output> + LocalContextRequirements<'a>
126{
127}
128#[cfg(not(feature = "multi-threaded"))]
129impl<'a, T: Future<Output = Output> + LocalContextRequirements<'a>, Output>
130    crate::builder::node_builder::FutureContextRequirements<'a, Output> for T
131{
132}
133
134impl<K> Debug for NodeFuture<'_, K> {
135    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
136        write!(f, "NodeFuture")
137    }
138}
139
140impl<K> Future for NodeFuture<'_, K> {
141    type Output = Result<K, NetworkError>;
142
143    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
144        self.inner.as_mut().poll(cx)
145    }
146}
147
148impl<R: Ratchet + ContextRequirements, T: PlatformOps> NodeBuilder<R, T> {
149    /// Returns a future that represents the both the protocol and kernel execution
150    pub fn build<'a, 'b: 'a, K: NetKernel<R> + 'b>(
151        &'a mut self,
152        kernel: K,
153    ) -> anyhow::Result<NodeFuture<'b, K>> {
154        self.check()?;
155        let hypernode_type = self.hypernode_type.take().unwrap_or_default();
156        let backend_type = self.backend_type.take().unwrap_or_default();
157        let server_argon_settings = self.server_argon_settings.take();
158        #[cfg(feature = "google-services")]
159        let server_services_cfg = self.services.take();
160        #[cfg(not(feature = "google-services"))]
161        let server_services_cfg = None;
162        let server_misc_settings = self.server_misc_settings.take();
163        let client_config = self.client_tls_config.take();
164        let kernel_executor_settings = self.kernel_executor_settings.take().unwrap_or_default();
165        let stun_servers = StunServers::required(self.stun_servers.take())?;
166        let turn_servers = self.turn_servers.take();
167        let underlying_proto = self.underlying_protocol.take();
168        let server_only_session_init_settings = server_settings(
169            self.local_only_server_settings.take(),
170            self.path_policy.take(),
171        );
172        let websocket_listener = self.websocket_listener.take();
173        let injected_listener = self.injected_listener.take();
174        #[cfg(target_family = "wasm")]
175        let serverless_config = self.serverless_config.take();
176
177        Ok(NodeFuture {
178            _pd: Default::default(),
179            inner: Box::pin(async move {
180                let underlying_proto = match underlying_proto {
181                    Some(proto) => proto,
182                    None => T::default_server_config().await.map_err(|err| {
183                        citadel_io::error!(
184                            citadel_io::ErrorCode::NodeDefaultServerConfigFailed,
185                            err.to_string()
186                        )
187                    })?,
188                };
189
190                T::config_warnings(&underlying_proto);
191
192                // Serverless mode: perform signaling and override node
193                // type, listener, and client config based on assigned role.
194                #[cfg(target_family = "wasm")]
195                let (pre_built_listener, client_config, hypernode_type) = if let Some(sl_config) =
196                    serverless_config
197                {
198                    let conn = establish_serverless_connection(
199                        sl_config.signaling.as_ref(),
200                        &sl_config.room_token,
201                        &sl_config.ice_servers,
202                        sl_config.poll_interval_ms,
203                        sl_config.timeout_ms,
204                    )
205                    .await
206                    .map_err(|e: std::io::Error| NetworkError::generic(e.to_string()))?;
207
208                    T::setup_serverless_transport(conn.stream, conn.is_server_role, client_config)
209                } else {
210                    (injected_listener, client_config, hypernode_type)
211                };
212
213                #[cfg(not(target_family = "wasm"))]
214                let pre_built_listener = injected_listener;
215
216                log::trace!(target: "citadel", "[NodeBuilder] Checking Tokio runtime ...");
217                let rt = citadel_io::try_current_runtime().map_err(NetworkError::generic)?;
218                log::trace!(target: "citadel", "[NodeBuilder] Creating account manager ...");
219                let account_manager = AccountManager::new(
220                    backend_type,
221                    server_argon_settings,
222                    server_services_cfg,
223                    server_misc_settings,
224                )
225                .await?;
226
227                let args: KernelExecutorArguments<_, _, T> = KernelExecutorArguments {
228                    rt,
229                    hypernode_type,
230                    account_manager,
231                    kernel,
232                    underlying_proto,
233                    client_config,
234                    kernel_executor_settings,
235                    stun_servers,
236                    turn_servers,
237                    server_only_session_init_settings,
238                    websocket_listener,
239                    pre_built_listener,
240                };
241
242                log::trace!(target: "citadel", "[NodeBuilder] Creating KernelExecutor ...");
243                let kernel_executor = KernelExecutor::<_, R>::new(args).await?;
244                log::trace!(target: "citadel", "[NodeBuilder] Executing kernel");
245                kernel_executor.execute().await
246            }),
247        })
248    }
249
250    /// Defines the node type. By default, Peer is used. If a server is desired, a bind address is expected
251    /// ```
252    /// use citadel_sdk::prelude::DefaultNodeBuilder;
253    /// use citadel_proto::prelude::NodeType;
254    ///
255    /// DefaultNodeBuilder::default().with_node_type(NodeType::server("0.0.0.0:25021").unwrap());
256    /// ```
257    pub fn with_node_type(&mut self, node_type: NodeType) -> &mut Self {
258        self.hypernode_type = Some(node_type);
259        self
260    }
261
262    /// Sets the backend used to synchronize client account information. By default, uses the filesystem.
263    /// When the enterprise feature is set, a SQL database (MySQL, PostgreSQL, SQLite) is available. Using a single SQL cluster can be used in combination with
264    /// a cluster of load-balancing running ['NetKernel']'s on different IPs to construct scaled applications
265    pub fn with_backend(&mut self, backend_type: BackendType) -> &mut Self {
266        self.backend_type = Some(backend_type);
267        self
268    }
269
270    /// Sets the desired settings for the [`KernelExecutor`]
271    pub fn with_kernel_executor_settings(
272        &mut self,
273        kernel_executor_settings: KernelExecutorSettings,
274    ) -> &mut Self {
275        self.kernel_executor_settings = Some(kernel_executor_settings);
276        self
277    }
278
279    /// Attaches custom Argon settings for password hashing at the server
280    pub fn with_server_argon_settings(
281        &mut self,
282        settings: ArgonDefaultServerSettings,
283    ) -> &mut Self {
284        self.server_argon_settings = Some(settings);
285        self
286    }
287
288    /// Attaches a google services json path, allowing the use of Google Auth and other dependent services like Realtime Database for android/IOS messaging. Required when using [`Self::with_google_realtime_database_config`]
289    #[cfg(feature = "google-services")]
290    pub fn with_google_services_json_path<V: Into<String>>(&mut self, path: V) -> &mut Self {
291        let cfg = self.get_or_create_services();
292        cfg.google_services_json_path = Some(path.into());
293        self
294    }
295
296    /// Attaches miscellaneous server settings (e.g., transient mode, credential requirements)
297    pub fn with_server_misc_settings(&mut self, misc_settings: ServerMiscSettings) -> &mut Self {
298        self.server_misc_settings = Some(misc_settings);
299        self
300    }
301
302    /// Creates a Google Realtime Database configuration given the project URL and API Key. Requires the use of [`Self::with_google_services_json_path`] to allow minting of JsonWebTokens
303    /// at the central server
304    #[cfg(feature = "google-services")]
305    pub fn with_google_realtime_database_config<V: Into<String>, W: Into<String>>(
306        &mut self,
307        url: V,
308        api_key: W,
309    ) -> &mut Self {
310        let cfg = self.get_or_create_services();
311        cfg.google_rtdb = Some(RtdbConfig {
312            url: url.into(),
313            api_key: api_key.into(),
314        });
315        self
316    }
317
318    /// Sets the underlying protocol / server configuration for the transport.
319    /// Default: TLS transport w/ self-signed cert (for NativeIO)
320    pub fn with_underlying_protocol(&mut self, proto: ServerMode<T>) -> &mut Self {
321        self.underlying_protocol = Some(proto);
322        self
323    }
324
325    /// Sets the client-side transport configuration.
326    pub fn with_client_config(&mut self, config: T::ClientConfig) -> &mut Self {
327        self.client_tls_config = Some(config);
328        self
329    }
330
331    #[cfg(feature = "google-services")]
332    fn get_or_create_services(&mut self) -> &mut ServicesConfig {
333        if self.services.is_some() {
334            self.services.as_mut().unwrap()
335        } else {
336            let cfg = ServicesConfig::default();
337            self.services = Some(cfg);
338            self.services.as_mut().unwrap()
339        }
340    }
341
342    /// The STUN servers this node learns its NAT mapping from: required, for every node type.
343    /// There is no default list; [`Self::build`] refuses a builder without one, and refuses a
344    /// list of fewer than [`MIN_STUN_SERVERS`] distinct `host:port` endpoints (see
345    /// [`StunServers::new`]).
346    pub fn with_stun_servers<V: Into<String>, S: Into<Vec<V>>>(&mut self, servers: S) -> &mut Self {
347        self.stun_servers = Some(servers.into().into_iter().map(|t| t.into()).collect());
348        self
349    }
350
351    /// Specifies TURN servers for relay-based NAT traversal.
352    ///
353    /// TURN servers relay traffic when direct peer-to-peer connections fail
354    /// due to restrictive NAT configurations. On WASM targets, these are
355    /// passed as ICE servers to the WebRTC peer connection.
356    pub fn with_turn_servers<S: Into<Vec<TurnServerConfig>>>(&mut self, servers: S) -> &mut Self {
357        self.turn_servers = Some(servers.into());
358        self
359    }
360
361    /// Adds an optional WebSocket listener to a server node.
362    ///
363    /// When set, the server will accept both its primary transport (TCP/TLS/QUIC)
364    /// AND WebSocket connections on the specified address. This enables browser
365    /// clients (via WASM) to connect to the same server.
366    ///
367    /// Has no effect on client/peer nodes.
368    pub fn with_websocket_listener(&mut self, addr: std::net::SocketAddr) -> &mut Self {
369        self.websocket_listener = Some(citadel_io::WebSocketListen::Addr(addr));
370        self
371    }
372
373    /// Like [`Self::with_websocket_listener`], but accepts on a socket the caller already bound.
374    /// The port is held from the caller's bind onward, so nothing else can take it before the
375    /// node starts.
376    pub fn with_bound_websocket_listener(&mut self, listener: std::net::TcpListener) -> &mut Self {
377        self.websocket_listener = Some(citadel_io::WebSocketListen::Bound(listener));
378        self
379    }
380
381    /// Serves a server node from a listener the host feeds, instead of binding one. The node
382    /// accepts whatever connections the listener yields and never opens a socket of its own; the
383    /// bind address given to [`NodeType::Server`] is only recorded. On WASM this is how a server
384    /// runs where the host accepts sockets itself — see `WasmListener::injected`.
385    ///
386    /// Only meaningful for [`NodeType::Server`]; `build` refuses it on a peer.
387    pub fn with_injected_listener(&mut self, listener: T::Listener) -> &mut Self {
388        self.injected_listener = Some(listener);
389        self
390    }
391
392    /// Enables serverless browser-to-browser mode. Both peers run identical code;
393    /// one is automatically assigned the server role and the other the client role
394    /// via deterministic signaling through the provided [`ServerlessConfig`].
395    ///
396    /// When set, `hypernode_type`, `underlying_protocol`, and `client_config` are
397    /// determined automatically after signaling completes.
398    #[cfg(target_family = "wasm")]
399    pub fn with_no_central_server(&mut self, config: ServerlessConfig) -> &mut Self {
400        self.serverless_config = Some(config);
401        self
402    }
403
404    /// Sets the pre-shared key for the server. Only a server should set this value
405    /// If no value is set, any client can connect to the server. If a pre-shared key
406    /// is specified, the client must have the matching pre-shared key in order to
407    /// register and connect with the server.
408    pub fn with_server_password<V: Into<PreSharedKey>>(&mut self, password: V) -> &mut Self {
409        let mut server_only_settings = self.local_only_server_settings.clone().unwrap_or_default();
410        server_only_settings.declared_pre_shared_key = Some(password.into());
411        self.local_only_server_settings = Some(server_only_settings);
412        self
413    }
414
415    /// Sets the header obfuscator settings for the server
416    pub fn with_server_declared_header_obfuscation<V: Into<HeaderObfuscatorSettings>>(
417        &mut self,
418        header_obfuscator_settings: V,
419    ) -> &mut Self {
420        let mut server_only_settings = self.local_only_server_settings.clone().unwrap_or_default();
421        server_only_settings.declared_header_obfuscation_setting =
422            header_obfuscator_settings.into();
423        self.local_only_server_settings = Some(server_only_settings);
424        self
425    }
426
427    fn check(&self) -> anyhow::Result<()> {
428        check_path_policy(self.hypernode_type.as_ref(), self.path_policy.as_ref())?;
429        if self.injected_listener.is_some()
430            && !matches!(self.hypernode_type, Some(NodeType::Server(_)))
431        {
432            return Err(anyhow::Error::msg(
433                "An injected listener requires NodeType::Server",
434            ));
435        }
436
437        #[cfg(feature = "google-services")]
438        if let Some(svc) = self.services.as_ref() {
439            if svc.google_rtdb.is_some() && svc.google_services_json_path.is_none() {
440                return Err(anyhow::Error::msg(
441                    "Google realtime database is enabled, yet, a services path is not provided",
442                ));
443            }
444        }
445
446        Ok(())
447    }
448}
449
450/// NativeIO-specific builder methods for TLS certificate configuration
451#[cfg(not(target_family = "wasm"))]
452impl<R: Ratchet + ContextRequirements> NodeBuilder<R, NativeIO> {
453    /// Wraps a rustls client config, preserving any previously-set `require_cert_verification`
454    /// policy so that the ordering of `with_*_certs` and `with_require_cert_verification` does not
455    /// matter.
456    fn set_client_rustls_config(
457        &mut self,
458        config: std::sync::Arc<citadel_proto::re_imports::RustlsClientConfig>,
459    ) {
460        let require_cert_verification = self
461            .client_tls_config
462            .as_ref()
463            .map(|cfg| cfg.require_cert_verification)
464            .unwrap_or(false);
465        self.client_tls_config = Some(citadel_proto::re_imports::NativeClientConfig {
466            config,
467            require_cert_verification,
468        });
469    }
470
471    /// Loads the accepted cert chain stored by the local operating system
472    /// If a custom set of certs is required, run [`Self::with_custom_certs`]
473    /// This is the default if no client TLS config is specified
474    pub async fn with_native_certs(&mut self) -> anyhow::Result<&mut Self> {
475        let certs = citadel_proto::re_imports::load_native_certs_async().await?;
476        let cfg = citadel_proto::re_imports::cert_vec_to_secure_client_config(&certs)?;
477        self.set_client_rustls_config(std::sync::Arc::new(cfg));
478        Ok(self)
479    }
480
481    /// The client will skip unconditionally server certificate verification
482    /// This is not recommended
483    pub fn with_insecure_skip_cert_verification(&mut self) -> &mut Self {
484        // Explicit opt-out: this clears any require-verification policy by design.
485        self.client_tls_config = Some(citadel_proto::re_imports::NativeClientConfig::new(
486            std::sync::Arc::new(citadel_proto::re_imports::insecure::rustls_client_config()),
487        ));
488        self
489    }
490
491    /// Requires server certificate verification, refusing to fall back to the accept-any-cert path
492    /// even if the peer advertises a self-signed server (i.e. an attacker cannot downgrade the
493    /// transport by flipping the `is_self_signed`/`domain` signal). If no client TLS config has been
494    /// set yet, the OS-native cert roots are loaded first. Order-independent with respect to the
495    /// `with_*_certs` methods.
496    pub async fn with_require_cert_verification(&mut self) -> anyhow::Result<&mut Self> {
497        if self.client_tls_config.is_none() {
498            let _ = self.with_native_certs().await?;
499        }
500        if let Some(cfg) = self.client_tls_config.as_mut() {
501            cfg.require_cert_verification = true;
502        }
503        Ok(self)
504    }
505
506    /// Loads a custom list of certs into the acceptable certificate list. Connections that present server certificates
507    /// that are outside of this list during the handshake process are refused
508    pub fn with_custom_certs<V: AsRef<[u8]>>(
509        &mut self,
510        custom_certs: &[V],
511    ) -> anyhow::Result<&mut Self> {
512        let cfg = citadel_proto::re_imports::create_rustls_client_config(custom_certs)?;
513        self.set_client_rustls_config(std::sync::Arc::new(cfg));
514        Ok(self)
515    }
516
517    /// The file should be a PEM formatted certificate
518    #[cfg(feature = "std")]
519    pub async fn with_pem_file<P: AsRef<std::path::Path>>(
520        &mut self,
521        path: P,
522    ) -> anyhow::Result<&mut Self> {
523        use citadel_wire::exports::{Certificate, PemObject};
524        let mut der = std::io::Cursor::new(citadel_io::tokio::fs::read(path).await?);
525        let certs: Vec<Certificate<'static>> =
526            Certificate::pem_reader_iter(&mut der).collect::<Result<Vec<_>, _>>()?;
527        let cfg = citadel_proto::re_imports::create_rustls_client_config(&certs)?;
528        self.set_client_rustls_config(std::sync::Arc::new(cfg));
529        Ok(self)
530    }
531}
532
533#[cfg(all(test, not(target_family = "wasm")))]
534mod tests {
535    use crate::builder::node_builder::DefaultNodeBuilder;
536    use crate::prefabs::server::empty::EmptyKernel;
537    use crate::prelude::{BackendType, NodeType};
538    use citadel_io::tokio;
539    use citadel_proto::prelude::{
540        KernelExecutorSettings, NativeIO, NativeP2PConfig, NativeSecureConfig, ServerMode,
541    };
542    use rstest::rstest;
543    use std::str::FromStr;
544
545    #[test]
546    #[cfg(feature = "google-services")]
547    fn okay_config() {
548        let _ = DefaultNodeBuilder::default()
549            .with_google_realtime_database_config("123", "456")
550            .with_google_services_json_path("abc")
551            .build(EmptyKernel::default())
552            .unwrap();
553    }
554
555    #[test]
556    #[cfg(feature = "google-services")]
557    fn bad_config() {
558        assert!(DefaultNodeBuilder::default()
559            .with_google_realtime_database_config("123", "456")
560            .build(EmptyKernel::default())
561            .is_err());
562    }
563
564    #[rstest]
565    #[tokio::test]
566    #[timeout(std::time::Duration::from_secs(60))]
567    #[allow(clippy::let_underscore_must_use)]
568    async fn test_options(
569        #[values(ServerMode::P2P(NativeP2PConfig::self_signed()), ServerMode::OrderedReliableSecure(NativeSecureConfig::self_signed().unwrap())
570        )]
571        underlying_protocol: ServerMode<NativeIO>,
572        #[values(NodeType::Peer, NodeType::Server(std::net::SocketAddr::from_str("127.0.0.1:9999").unwrap()
573        ))]
574        node_type: NodeType,
575        #[values(KernelExecutorSettings::default(), KernelExecutorSettings::default().with_max_concurrency(2)
576        )]
577        kernel_settings: KernelExecutorSettings,
578        #[values(BackendType::InMemory, BackendType::new("file:/hello_world/path/").unwrap())]
579        backend_type: BackendType,
580    ) {
581        let mut builder = DefaultNodeBuilder::default();
582        let _ = builder
583            .with_underlying_protocol(underlying_protocol.clone())
584            .with_backend(backend_type.clone())
585            .with_node_type(node_type)
586            .with_kernel_executor_settings(kernel_settings.clone())
587            .with_insecure_skip_cert_verification()
588            .with_stun_servers(crate::test_common::LOCALHOST_STUN_SERVERS)
589            .with_native_certs()
590            .await
591            .unwrap();
592
593        if node_type.is_server() {
594            let _ = builder.with_path_policy(crate::test_common::localhost_path_policy());
595        }
596        assert!(builder.underlying_protocol.is_some());
597        assert_eq!(backend_type, builder.backend_type.clone().unwrap());
598        assert_eq!(node_type, builder.hypernode_type.unwrap());
599        assert_eq!(
600            kernel_settings,
601            builder.kernel_executor_settings.clone().unwrap()
602        );
603
604        drop(builder.build(EmptyKernel::default()).unwrap());
605    }
606}
607
608#[cfg(all(test, not(target_family = "wasm")))]
609#[path = "node_builder_stun_tests.rs"]
610mod stun_tests;
611
612#[path = "node_builder_path_policy.rs"]
613mod path_policy;
614use path_policy::{check_path_policy, server_settings};