1#[cfg(target_os = "linux")]
22mod gateway;
23mod node;
24mod peer;
25mod transport;
26
27use crate::upper::config::{DnsConfig, TunConfig};
28use crate::{Identity, IdentityError};
29use serde::{Deserialize, Serialize};
30use std::path::{Path, PathBuf};
31use thiserror::Error;
32
33#[cfg(target_os = "linux")]
34pub use gateway::{ConntrackConfig, GatewayConfig, GatewayDnsConfig, PortForward, Proto};
35pub use node::{
36 BloomConfig, BuffersConfig, CacheConfig, ControlConfig, DiscoveryConfig, LimitsConfig,
37 NodeConfig, NostrDiscoveryConfig, NostrDiscoveryPolicy, RateLimitConfig, RekeyConfig,
38 RetryConfig, RoutingConfig, RoutingMode, SessionConfig, SessionMmpConfig, TreeConfig,
39};
40pub use peer::{ConnectPolicy, PeerAddress, PeerConfig};
41#[cfg(feature = "sim-transport")]
42pub use transport::SimTransportConfig;
43pub use transport::{
44 BleConfig, DirectoryServiceConfig, EthernetConfig, TcpConfig, TorConfig, TransportInstances,
45 TransportsConfig, UdpConfig, WebRtcConfig,
46};
47
48const CONFIG_FILENAME: &str = "fips.yaml";
50
51const KEY_FILENAME: &str = "fips.key";
53
54const PUB_FILENAME: &str = "fips.pub";
56
57fn is_loopback_addr_str(addr: &str) -> bool {
64 if let Some(rest) = addr.strip_prefix('[')
66 && let Some(end) = rest.find(']')
67 {
68 let host = &rest[..end];
69 return host == "::1";
70 }
71 let host = match addr.rsplit_once(':') {
73 Some((h, _)) => h,
74 None => addr,
75 };
76 host == "localhost" || host == "::1" || host == "0:0:0:0:0:0:0:1" || host.starts_with("127.")
77}
78
79pub fn key_file_path(config_path: &Path) -> PathBuf {
81 config_path
82 .parent()
83 .unwrap_or(Path::new("."))
84 .join(KEY_FILENAME)
85}
86
87pub fn pub_file_path(config_path: &Path) -> PathBuf {
89 config_path
90 .parent()
91 .unwrap_or(Path::new("."))
92 .join(PUB_FILENAME)
93}
94
95#[cfg(unix)]
103pub(crate) fn resolve_default_socket(filename: &str) -> String {
104 if Path::new("/run/fips").is_dir() {
105 return format!("/run/fips/{filename}");
106 }
107
108 if let Ok(xdg) = std::env::var("XDG_RUNTIME_DIR")
109 && Path::new(&xdg).is_dir()
110 {
111 return format!("{xdg}/fips/{filename}");
112 }
113
114 format!("/tmp/fips-{filename}")
115}
116
117pub fn default_control_path() -> PathBuf {
123 #[cfg(unix)]
124 {
125 PathBuf::from(resolve_default_socket("control.sock"))
126 }
127 #[cfg(windows)]
128 {
129 PathBuf::from("21210")
130 }
131}
132
133pub fn default_gateway_path() -> PathBuf {
138 #[cfg(unix)]
139 {
140 PathBuf::from(resolve_default_socket("gateway.sock"))
141 }
142 #[cfg(windows)]
143 {
144 PathBuf::from("21211")
145 }
146}
147
148pub fn read_key_file(path: &Path) -> Result<String, ConfigError> {
150 let contents = std::fs::read_to_string(path).map_err(|e| ConfigError::ReadFile {
151 path: path.to_path_buf(),
152 source: e,
153 })?;
154 let nsec = contents.trim().to_string();
155 if nsec.is_empty() {
156 return Err(ConfigError::EmptyKeyFile {
157 path: path.to_path_buf(),
158 });
159 }
160 Ok(nsec)
161}
162
163pub fn write_key_file(path: &Path, nsec: &str) -> Result<(), ConfigError> {
168 use std::io::Write;
169
170 let mut opts = std::fs::OpenOptions::new();
171 opts.write(true).create(true).truncate(true);
172
173 #[cfg(unix)]
174 {
175 use std::os::unix::fs::OpenOptionsExt;
176 opts.mode(0o600);
177 }
178
179 let mut file = opts.open(path).map_err(|e| ConfigError::WriteKeyFile {
180 path: path.to_path_buf(),
181 source: e,
182 })?;
183
184 #[cfg(unix)]
185 {
186 use std::os::unix::fs::PermissionsExt;
187 file.set_permissions(std::fs::Permissions::from_mode(0o600))
188 .map_err(|e| ConfigError::WriteKeyFile {
189 path: path.to_path_buf(),
190 source: e,
191 })?;
192 }
193
194 file.write_all(nsec.as_bytes())
195 .map_err(|e| ConfigError::WriteKeyFile {
196 path: path.to_path_buf(),
197 source: e,
198 })?;
199 file.write_all(b"\n")
200 .map_err(|e| ConfigError::WriteKeyFile {
201 path: path.to_path_buf(),
202 source: e,
203 })?;
204 Ok(())
205}
206
207pub fn write_pub_file(path: &Path, npub: &str) -> Result<(), ConfigError> {
212 use std::io::Write;
213
214 let mut opts = std::fs::OpenOptions::new();
215 opts.write(true).create(true).truncate(true);
216
217 #[cfg(unix)]
218 {
219 use std::os::unix::fs::OpenOptionsExt;
220 opts.mode(0o644);
221 }
222
223 let mut file = opts.open(path).map_err(|e| ConfigError::WriteKeyFile {
224 path: path.to_path_buf(),
225 source: e,
226 })?;
227
228 file.write_all(npub.as_bytes())
229 .map_err(|e| ConfigError::WriteKeyFile {
230 path: path.to_path_buf(),
231 source: e,
232 })?;
233 file.write_all(b"\n")
234 .map_err(|e| ConfigError::WriteKeyFile {
235 path: path.to_path_buf(),
236 source: e,
237 })?;
238 Ok(())
239}
240
241pub fn resolve_identity(
258 config: &Config,
259 loaded_paths: &[PathBuf],
260) -> Result<ResolvedIdentity, ConfigError> {
261 use crate::encode_nsec;
262
263 if let Some(nsec) = &config.node.identity.nsec {
265 return Ok(ResolvedIdentity {
266 nsec: nsec.clone(),
267 source: IdentitySource::Config,
268 });
269 }
270
271 let config_ref = if let Some(path) = loaded_paths.last() {
273 path.clone()
274 } else {
275 Config::search_paths()
276 .first()
277 .cloned()
278 .unwrap_or_else(|| PathBuf::from("./fips.yaml"))
279 };
280 let key_path = key_file_path(&config_ref);
281 let pub_path = pub_file_path(&config_ref);
282
283 if config.node.identity.persistent {
284 if key_path.exists() {
286 let nsec = read_key_file(&key_path)?;
287 let identity = Identity::from_secret_str(&nsec)?;
288 let _ = write_pub_file(&pub_path, &identity.npub());
289 return Ok(ResolvedIdentity {
290 nsec,
291 source: IdentitySource::KeyFile(key_path),
292 });
293 }
294
295 let identity = Identity::generate();
297 let nsec = encode_nsec(&identity.keypair().secret_key());
298 let npub = identity.npub();
299
300 if let Some(parent) = key_path.parent() {
301 let _ = std::fs::create_dir_all(parent);
302 }
303
304 match write_key_file(&key_path, &nsec) {
305 Ok(()) => {
306 let _ = write_pub_file(&pub_path, &npub);
307 Ok(ResolvedIdentity {
308 nsec,
309 source: IdentitySource::Generated(key_path),
310 })
311 }
312 Err(_) => Ok(ResolvedIdentity {
313 nsec,
314 source: IdentitySource::Ephemeral,
315 }),
316 }
317 } else {
318 let identity = Identity::generate();
321 let nsec = encode_nsec(&identity.keypair().secret_key());
322 let npub = identity.npub();
323
324 if let Some(parent) = key_path.parent() {
325 let _ = std::fs::create_dir_all(parent);
326 }
327
328 let _ = write_key_file(&key_path, &nsec);
329 let _ = write_pub_file(&pub_path, &npub);
330
331 Ok(ResolvedIdentity {
332 nsec,
333 source: IdentitySource::Ephemeral,
334 })
335 }
336}
337
338pub struct ResolvedIdentity {
340 pub nsec: String,
342 pub source: IdentitySource,
344}
345
346pub enum IdentitySource {
348 Config,
350 KeyFile(PathBuf),
352 Generated(PathBuf),
354 Ephemeral,
356}
357
358#[derive(Debug, Error)]
360pub enum ConfigError {
361 #[error("failed to read config file {path}: {source}")]
362 ReadFile {
363 path: PathBuf,
364 source: std::io::Error,
365 },
366
367 #[error("failed to parse config file {path}: {source}")]
368 ParseYaml {
369 path: PathBuf,
370 source: serde_yaml::Error,
371 },
372
373 #[error("key file is empty: {path}")]
374 EmptyKeyFile { path: PathBuf },
375
376 #[error("failed to write key file {path}: {source}")]
377 WriteKeyFile {
378 path: PathBuf,
379 source: std::io::Error,
380 },
381
382 #[error("identity error: {0}")]
383 Identity(#[from] IdentityError),
384
385 #[error("invalid configuration: {0}")]
386 Validation(String),
387}
388
389#[derive(Debug, Clone, Default, Serialize, Deserialize)]
391pub struct IdentityConfig {
392 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub nsec: Option<String>,
396
397 #[serde(default)]
401 pub persistent: bool,
402}
403
404#[derive(Debug, Clone, Default, Serialize, Deserialize)]
406pub struct Config {
407 #[serde(default)]
409 pub node: NodeConfig,
410
411 #[serde(default)]
413 pub tun: TunConfig,
414
415 #[serde(default)]
417 pub dns: DnsConfig,
418
419 #[serde(default, skip_serializing_if = "TransportsConfig::is_empty")]
421 pub transports: TransportsConfig,
422
423 #[serde(default, skip_serializing_if = "Vec::is_empty")]
425 pub peers: Vec<PeerConfig>,
426
427 #[cfg(target_os = "linux")]
429 #[serde(default, skip_serializing_if = "Option::is_none")]
430 pub gateway: Option<GatewayConfig>,
431}
432
433impl Config {
434 pub fn new() -> Self {
436 Self::default()
437 }
438
439 pub fn load() -> Result<(Self, Vec<PathBuf>), ConfigError> {
449 let search_paths = Self::search_paths();
450 Self::load_from_paths(&search_paths)
451 }
452
453 pub fn load_from_paths(paths: &[PathBuf]) -> Result<(Self, Vec<PathBuf>), ConfigError> {
457 let mut config = Config::default();
458 let mut loaded_paths = Vec::new();
459
460 for path in paths {
461 if path.exists() {
462 let file_config = Self::load_file(path)?;
463 config.merge(file_config);
464 loaded_paths.push(path.clone());
465 }
466 }
467
468 Ok((config, loaded_paths))
469 }
470
471 pub fn load_file(path: &Path) -> Result<Self, ConfigError> {
473 let contents = std::fs::read_to_string(path).map_err(|e| ConfigError::ReadFile {
474 path: path.to_path_buf(),
475 source: e,
476 })?;
477
478 serde_yaml::from_str(&contents).map_err(|e| ConfigError::ParseYaml {
479 path: path.to_path_buf(),
480 source: e,
481 })
482 }
483
484 pub fn search_paths() -> Vec<PathBuf> {
486 let mut paths = Vec::new();
487
488 paths.push(PathBuf::from("/etc/fips").join(CONFIG_FILENAME));
490
491 if let Some(config_dir) = dirs::config_dir() {
493 paths.push(config_dir.join("fips").join(CONFIG_FILENAME));
494 }
495
496 if let Some(home_dir) = dirs::home_dir() {
498 paths.push(home_dir.join(".fips.yaml"));
499 }
500
501 paths.push(PathBuf::from(".").join(CONFIG_FILENAME));
503
504 paths
505 }
506
507 pub fn merge(&mut self, other: Config) {
511 if other.node.identity.nsec.is_some() {
513 self.node.identity.nsec = other.node.identity.nsec;
514 }
515 if other.node.identity.persistent {
516 self.node.identity.persistent = true;
517 }
518 if other.node.leaf_only {
520 self.node.leaf_only = true;
521 }
522 if other.tun.enabled {
524 self.tun.enabled = true;
525 }
526 if other.tun.name.is_some() {
527 self.tun.name = other.tun.name;
528 }
529 if other.tun.mtu.is_some() {
530 self.tun.mtu = other.tun.mtu;
531 }
532 self.dns.enabled = other.dns.enabled;
534 if other.dns.bind_addr.is_some() {
535 self.dns.bind_addr = other.dns.bind_addr;
536 }
537 if other.dns.port.is_some() {
538 self.dns.port = other.dns.port;
539 }
540 if other.dns.ttl.is_some() {
541 self.dns.ttl = other.dns.ttl;
542 }
543 self.transports.merge(other.transports);
545 if !other.peers.is_empty() {
547 self.peers = other.peers;
548 }
549 #[cfg(target_os = "linux")]
551 if other.gateway.is_some() {
552 self.gateway = other.gateway;
553 }
554 }
555
556 pub fn create_identity(&self) -> Result<Identity, ConfigError> {
561 match &self.node.identity.nsec {
562 Some(nsec) => Ok(Identity::from_secret_str(nsec)?),
563 None => Ok(Identity::generate()),
564 }
565 }
566
567 pub fn has_identity(&self) -> bool {
569 self.node.identity.nsec.is_some()
570 }
571
572 pub fn is_leaf_only(&self) -> bool {
574 self.node.leaf_only
575 }
576
577 pub fn peers(&self) -> &[PeerConfig] {
579 &self.peers
580 }
581
582 pub fn auto_connect_peers(&self) -> impl Iterator<Item = &PeerConfig> {
584 self.peers.iter().filter(|p| p.is_auto_connect())
585 }
586
587 pub fn validate(&self) -> Result<(), ConfigError> {
589 let nostr = &self.node.discovery.nostr;
590
591 let any_transport_advertises_on_nostr = self
592 .transports
593 .udp
594 .iter()
595 .any(|(_, cfg)| cfg.advertise_on_nostr())
596 || self
597 .transports
598 .tcp
599 .iter()
600 .any(|(_, cfg)| cfg.advertise_on_nostr())
601 || self
602 .transports
603 .tor
604 .iter()
605 .any(|(_, cfg)| cfg.advertise_on_nostr())
606 || self
607 .transports
608 .webrtc
609 .iter()
610 .any(|(_, cfg)| cfg.advertise_on_nostr());
611
612 if any_transport_advertises_on_nostr && !nostr.enabled {
613 return Err(ConfigError::Validation(
614 "at least one transport has `advertise_on_nostr = true`, but `node.discovery.nostr.enabled` is false".to_string(),
615 ));
616 }
617
618 for (i, peer) in self.peers.iter().enumerate() {
619 if peer.addresses.is_empty() && !nostr.enabled {
620 return Err(ConfigError::Validation(format!(
621 "peers[{i}] ({}): must specify at least one address, or enable `node.discovery.nostr` to resolve endpoints from Nostr adverts",
622 peer.npub
623 )));
624 }
625 }
626
627 let has_nat_udp_advert = self
628 .transports
629 .udp
630 .iter()
631 .any(|(_, cfg)| cfg.advertise_on_nostr() && !cfg.is_public());
632
633 if nostr.enabled && has_nat_udp_advert {
634 if nostr.dm_relays.is_empty() {
635 return Err(ConfigError::Validation(
636 "NAT UDP advert publishing requires `node.discovery.nostr.dm_relays` to be non-empty".to_string(),
637 ));
638 }
639 if nostr.stun_servers.is_empty() {
640 return Err(ConfigError::Validation(
641 "NAT UDP advert publishing requires `node.discovery.nostr.stun_servers` to be non-empty".to_string(),
642 ));
643 }
644 }
645
646 let has_webrtc_advert_without_relays = self.transports.webrtc.iter().any(|(_, cfg)| {
647 cfg.advertise_on_nostr() && cfg.signal_relays(&nostr.dm_relays).is_empty()
648 });
649
650 if nostr.enabled && has_webrtc_advert_without_relays {
651 return Err(ConfigError::Validation(
652 "WebRTC advert publishing requires `node.discovery.nostr.dm_relays` or `transports.webrtc.signal_relays` to be non-empty".to_string(),
653 ));
654 }
655
656 for (name, cfg) in self.transports.udp.iter() {
663 if cfg.outbound_only() {
664 continue;
665 }
666 if is_loopback_addr_str(cfg.bind_addr()) {
667 let any_external_peer = self.peers.iter().any(|peer| {
668 peer.addresses
669 .iter()
670 .any(|a| a.transport == "udp" && !is_loopback_addr_str(&a.addr))
671 });
672 if any_external_peer {
673 let label = name.unwrap_or("(unnamed)");
674 return Err(ConfigError::Validation(format!(
675 "transports.udp[{label}].bind_addr is loopback ({}) but at least one peer has a non-loopback UDP address; \
676 fips cannot reach external peers from a loopback-bound socket. \
677 Use bind_addr: \"0.0.0.0:2121\" (with kernel-firewall hardening if exposure is a concern), or set outbound_only: true.",
678 cfg.bind_addr()
679 )));
680 }
681 }
682 }
683
684 Ok(())
685 }
686
687 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
689 serde_yaml::to_string(self)
690 }
691}
692
693#[cfg(test)]
694mod tests {
695 use super::*;
696 use std::collections::HashMap;
697 use std::fs;
698 use tempfile::TempDir;
699
700 #[cfg(unix)]
701 static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
702
703 #[test]
704 fn test_empty_config() {
705 let config = Config::new();
706 assert!(config.node.identity.nsec.is_none());
707 assert!(!config.has_identity());
708 }
709
710 #[test]
711 fn test_parse_yaml_with_nsec() {
712 let yaml = r#"
713node:
714 identity:
715 nsec: nsec1qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxfnm5g9
716"#;
717 let config: Config = serde_yaml::from_str(yaml).unwrap();
718 assert!(config.node.identity.nsec.is_some());
719 assert!(config.has_identity());
720 }
721
722 #[test]
723 fn test_parse_yaml_with_hex() {
724 let yaml = r#"
725node:
726 identity:
727 nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
728"#;
729 let config: Config = serde_yaml::from_str(yaml).unwrap();
730 assert!(config.node.identity.nsec.is_some());
731
732 let identity = config.create_identity().unwrap();
733 assert!(!identity.npub().is_empty());
734 }
735
736 #[test]
737 fn test_parse_yaml_empty() {
738 let yaml = "";
739 let config: Config = serde_yaml::from_str(yaml).unwrap();
740 assert!(config.node.identity.nsec.is_none());
741 }
742
743 #[test]
744 fn test_parse_yaml_partial() {
745 let yaml = r#"
746node:
747 identity: {}
748"#;
749 let config: Config = serde_yaml::from_str(yaml).unwrap();
750 assert!(config.node.identity.nsec.is_none());
751 }
752
753 #[test]
754 fn test_merge_configs() {
755 let mut base = Config::new();
756 base.node.identity.nsec = Some("base_nsec".to_string());
757
758 let mut override_config = Config::new();
759 override_config.node.identity.nsec = Some("override_nsec".to_string());
760
761 base.merge(override_config);
762 assert_eq!(base.node.identity.nsec, Some("override_nsec".to_string()));
763 }
764
765 #[test]
766 fn test_merge_preserves_base_when_override_empty() {
767 let mut base = Config::new();
768 base.node.identity.nsec = Some("base_nsec".to_string());
769
770 let override_config = Config::new();
771
772 base.merge(override_config);
773 assert_eq!(base.node.identity.nsec, Some("base_nsec".to_string()));
774 }
775
776 #[test]
777 fn test_create_identity_from_nsec() {
778 let mut config = Config::new();
779 config.node.identity.nsec =
780 Some("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string());
781
782 let identity = config.create_identity().unwrap();
783 assert!(!identity.npub().is_empty());
784 }
785
786 #[test]
787 fn test_create_identity_generates_new() {
788 let config = Config::new();
789 let identity = config.create_identity().unwrap();
790 assert!(!identity.npub().is_empty());
791 }
792
793 #[test]
794 fn test_load_from_file() {
795 let temp_dir = TempDir::new().unwrap();
796 let config_path = temp_dir.path().join("fips.yaml");
797
798 let yaml = r#"
799node:
800 identity:
801 nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
802"#;
803 fs::write(&config_path, yaml).unwrap();
804
805 let config = Config::load_file(&config_path).unwrap();
806 assert!(config.node.identity.nsec.is_some());
807 }
808
809 #[test]
810 fn test_load_from_paths_merges() {
811 let temp_dir = TempDir::new().unwrap();
812
813 let low_priority = temp_dir.path().join("low.yaml");
815 let high_priority = temp_dir.path().join("high.yaml");
816
817 fs::write(
818 &low_priority,
819 r#"
820node:
821 identity:
822 nsec: "low_priority_nsec"
823"#,
824 )
825 .unwrap();
826
827 fs::write(
828 &high_priority,
829 r#"
830node:
831 identity:
832 nsec: "high_priority_nsec"
833"#,
834 )
835 .unwrap();
836
837 let paths = vec![low_priority.clone(), high_priority.clone()];
838 let (config, loaded) = Config::load_from_paths(&paths).unwrap();
839
840 assert_eq!(loaded.len(), 2);
841 assert_eq!(
842 config.node.identity.nsec,
843 Some("high_priority_nsec".to_string())
844 );
845 }
846
847 #[test]
848 fn test_load_skips_missing_files() {
849 let temp_dir = TempDir::new().unwrap();
850 let existing = temp_dir.path().join("exists.yaml");
851 let missing = temp_dir.path().join("missing.yaml");
852
853 fs::write(
854 &existing,
855 r#"
856node:
857 identity:
858 nsec: "existing_nsec"
859"#,
860 )
861 .unwrap();
862
863 let paths = vec![missing, existing.clone()];
864 let (config, loaded) = Config::load_from_paths(&paths).unwrap();
865
866 assert_eq!(loaded.len(), 1);
867 assert_eq!(loaded[0], existing);
868 assert_eq!(config.node.identity.nsec, Some("existing_nsec".to_string()));
869 }
870
871 #[test]
872 fn test_search_paths_includes_expected() {
873 let paths = Config::search_paths();
874
875 assert!(paths.iter().any(|p| p.ends_with("fips.yaml")));
877
878 #[cfg(unix)]
880 assert!(
881 paths
882 .iter()
883 .any(|p| p.starts_with("/etc/fips") && p.ends_with("fips.yaml"))
884 );
885 }
886
887 #[test]
888 fn test_to_yaml() {
889 let mut config = Config::new();
890 config.node.identity.nsec = Some("test_nsec".to_string());
891
892 let yaml = config.to_yaml().unwrap();
893 assert!(yaml.contains("node:"));
894 assert!(yaml.contains("identity:"));
895 assert!(yaml.contains("nsec:"));
896 assert!(yaml.contains("test_nsec"));
897 }
898
899 #[test]
900 fn test_key_file_write_read_roundtrip() {
901 let temp_dir = TempDir::new().unwrap();
902 let key_path = temp_dir.path().join("fips.key");
903
904 let identity = crate::Identity::generate();
905 let nsec = crate::encode_nsec(&identity.keypair().secret_key());
906
907 write_key_file(&key_path, &nsec).unwrap();
908
909 let loaded_nsec = read_key_file(&key_path).unwrap();
910 assert_eq!(loaded_nsec, nsec);
911
912 let loaded_identity = crate::Identity::from_secret_str(&loaded_nsec).unwrap();
914 assert_eq!(loaded_identity.npub(), identity.npub());
915 }
916
917 #[cfg(unix)]
918 #[test]
919 fn test_key_file_permissions() {
920 use std::os::unix::fs::MetadataExt;
921
922 let temp_dir = TempDir::new().unwrap();
923 let key_path = temp_dir.path().join("fips.key");
924
925 write_key_file(&key_path, "nsec1test").unwrap();
926
927 let metadata = fs::metadata(&key_path).unwrap();
928 assert_eq!(metadata.mode() & 0o777, 0o600);
929 }
930
931 #[cfg(unix)]
932 #[test]
933 fn test_key_file_permissions_are_tightened_on_overwrite() {
934 use std::os::unix::fs::{MetadataExt, PermissionsExt};
935
936 let temp_dir = TempDir::new().unwrap();
937 let key_path = temp_dir.path().join("fips.key");
938 fs::write(&key_path, "old\n").unwrap();
939 fs::set_permissions(&key_path, fs::Permissions::from_mode(0o644)).unwrap();
940
941 write_key_file(&key_path, "nsec1test").unwrap();
942
943 let metadata = fs::metadata(&key_path).unwrap();
944 assert_eq!(metadata.mode() & 0o777, 0o600);
945 assert_eq!(read_key_file(&key_path).unwrap(), "nsec1test");
946 }
947
948 #[cfg(unix)]
949 #[test]
950 fn test_pub_file_permissions() {
951 use std::os::unix::fs::MetadataExt;
952
953 let temp_dir = TempDir::new().unwrap();
954 let pub_path = temp_dir.path().join("fips.pub");
955
956 write_pub_file(&pub_path, "npub1test").unwrap();
957
958 let metadata = fs::metadata(&pub_path).unwrap();
959 assert_eq!(metadata.mode() & 0o777, 0o644);
960 }
961
962 #[test]
963 fn test_key_file_empty_error() {
964 let temp_dir = TempDir::new().unwrap();
965 let key_path = temp_dir.path().join("fips.key");
966
967 fs::write(&key_path, "").unwrap();
968
969 let result = read_key_file(&key_path);
970 assert!(result.is_err());
971 assert!(result.unwrap_err().to_string().contains("empty"));
972 }
973
974 #[test]
975 fn test_key_file_whitespace_trimmed() {
976 let temp_dir = TempDir::new().unwrap();
977 let key_path = temp_dir.path().join("fips.key");
978
979 fs::write(&key_path, " nsec1test \n").unwrap();
980
981 let nsec = read_key_file(&key_path).unwrap();
982 assert_eq!(nsec, "nsec1test");
983 }
984
985 #[test]
986 fn test_key_file_path_derivation() {
987 let config_path = PathBuf::from("/etc/fips/fips.yaml");
988 assert_eq!(
989 key_file_path(&config_path),
990 PathBuf::from("/etc/fips/fips.key")
991 );
992 assert_eq!(
993 pub_file_path(&config_path),
994 PathBuf::from("/etc/fips/fips.pub")
995 );
996 }
997
998 #[cfg(windows)]
999 #[test]
1000 fn test_key_file_write_read_roundtrip_windows() {
1001 let temp_dir = TempDir::new().unwrap();
1002 let key_path = temp_dir.path().join("fips.key");
1003
1004 let identity = crate::Identity::generate();
1005 let nsec = crate::encode_nsec(&identity.keypair().secret_key());
1006
1007 write_key_file(&key_path, &nsec).unwrap();
1008
1009 let loaded_nsec = read_key_file(&key_path).unwrap();
1011 assert_eq!(loaded_nsec, nsec);
1012
1013 let loaded_identity = crate::Identity::from_secret_str(&loaded_nsec).unwrap();
1015 assert_eq!(loaded_identity.npub(), identity.npub());
1016 }
1017
1018 #[test]
1019 fn test_resolve_identity_from_config() {
1020 let mut config = Config::new();
1021 config.node.identity.nsec =
1022 Some("0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string());
1023
1024 let resolved = resolve_identity(&config, &[]).unwrap();
1025 assert!(matches!(resolved.source, IdentitySource::Config));
1026 }
1027
1028 #[test]
1029 fn test_resolve_identity_ephemeral_by_default() {
1030 let temp_dir = TempDir::new().unwrap();
1031 let config_path = temp_dir.path().join("fips.yaml");
1032
1033 fs::write(&config_path, "node:\n identity: {}\n").unwrap();
1034
1035 let config = Config::load_file(&config_path).unwrap();
1036 assert!(!config.node.identity.persistent);
1037
1038 let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
1039 assert!(matches!(resolved.source, IdentitySource::Ephemeral));
1040
1041 let key_path = temp_dir.path().join("fips.key");
1043 let pub_path = temp_dir.path().join("fips.pub");
1044 assert!(key_path.exists());
1045 assert!(pub_path.exists());
1046 }
1047
1048 #[test]
1049 fn test_resolve_identity_ephemeral_changes_each_call() {
1050 let temp_dir = TempDir::new().unwrap();
1051 let config_path = temp_dir.path().join("fips.yaml");
1052
1053 fs::write(&config_path, "node:\n identity: {}\n").unwrap();
1054
1055 let config = Config::load_file(&config_path).unwrap();
1056 let first = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
1057 let second = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
1058
1059 assert_ne!(first.nsec, second.nsec);
1061 }
1062
1063 #[test]
1064 fn test_resolve_identity_persistent_from_key_file() {
1065 let temp_dir = TempDir::new().unwrap();
1066 let config_path = temp_dir.path().join("fips.yaml");
1067 let key_path = temp_dir.path().join("fips.key");
1068
1069 fs::write(&config_path, "node:\n identity:\n persistent: true\n").unwrap();
1070
1071 let identity = crate::Identity::generate();
1073 let nsec = crate::encode_nsec(&identity.keypair().secret_key());
1074 write_key_file(&key_path, &nsec).unwrap();
1075
1076 let config = Config::load_file(&config_path).unwrap();
1077 assert!(config.node.identity.persistent);
1078
1079 let resolved = resolve_identity(&config, &[config_path]).unwrap();
1080 assert!(matches!(resolved.source, IdentitySource::KeyFile(_)));
1081 assert_eq!(resolved.nsec, nsec);
1082 }
1083
1084 #[test]
1085 fn test_resolve_identity_persistent_generates_and_persists() {
1086 let temp_dir = TempDir::new().unwrap();
1087 let config_path = temp_dir.path().join("fips.yaml");
1088
1089 fs::write(&config_path, "node:\n identity:\n persistent: true\n").unwrap();
1090
1091 let config = Config::load_file(&config_path).unwrap();
1092 let resolved = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
1093
1094 assert!(matches!(resolved.source, IdentitySource::Generated(_)));
1095
1096 let key_path = temp_dir.path().join("fips.key");
1098 let pub_path = temp_dir.path().join("fips.pub");
1099 assert!(key_path.exists());
1100 assert!(pub_path.exists());
1101
1102 let resolved2 = resolve_identity(&config, std::slice::from_ref(&config_path)).unwrap();
1104 assert!(matches!(resolved2.source, IdentitySource::KeyFile(_)));
1105 assert_eq!(resolved.nsec, resolved2.nsec);
1106 }
1107
1108 #[test]
1109 fn test_to_yaml_empty_nsec_omitted() {
1110 let config = Config::new();
1111 let yaml = config.to_yaml().unwrap();
1112
1113 assert!(!yaml.contains("nsec:"));
1115 }
1116
1117 #[test]
1118 fn test_parse_transport_single_instance() {
1119 let yaml = r#"
1120transports:
1121 udp:
1122 bind_addr: "0.0.0.0:2121"
1123 mtu: 1400
1124"#;
1125 let config: Config = serde_yaml::from_str(yaml).unwrap();
1126
1127 assert_eq!(config.transports.udp.len(), 1);
1128 let instances: Vec<_> = config.transports.udp.iter().collect();
1129 assert_eq!(instances.len(), 1);
1130 assert_eq!(instances[0].0, None); assert_eq!(instances[0].1.bind_addr(), "0.0.0.0:2121");
1132 assert_eq!(instances[0].1.mtu(), 1400);
1133 }
1134
1135 #[test]
1136 fn test_parse_transport_named_instances() {
1137 let yaml = r#"
1138transports:
1139 udp:
1140 main:
1141 bind_addr: "0.0.0.0:2121"
1142 backup:
1143 bind_addr: "192.168.1.100:2122"
1144 mtu: 1280
1145"#;
1146 let config: Config = serde_yaml::from_str(yaml).unwrap();
1147
1148 assert_eq!(config.transports.udp.len(), 2);
1149
1150 let instances: std::collections::HashMap<_, _> = config.transports.udp.iter().collect();
1151
1152 assert!(instances.contains_key(&Some("main")));
1154 assert!(instances.contains_key(&Some("backup")));
1155 assert_eq!(instances[&Some("main")].bind_addr(), "0.0.0.0:2121");
1156 assert_eq!(instances[&Some("backup")].bind_addr(), "192.168.1.100:2122");
1157 assert_eq!(instances[&Some("backup")].mtu(), 1280);
1158 }
1159
1160 #[test]
1161 fn test_parse_transport_empty() {
1162 let yaml = r#"
1163transports: {}
1164"#;
1165 let config: Config = serde_yaml::from_str(yaml).unwrap();
1166 assert!(config.transports.udp.is_empty());
1167 assert!(config.transports.is_empty());
1168 }
1169
1170 #[test]
1171 fn test_transport_instances_iter() {
1172 let single = TransportInstances::Single(UdpConfig {
1174 bind_addr: Some("0.0.0.0:2121".to_string()),
1175 mtu: None,
1176 ..Default::default()
1177 });
1178 let items: Vec<_> = single.iter().collect();
1179 assert_eq!(items.len(), 1);
1180 assert_eq!(items[0].0, None);
1181
1182 let mut map = HashMap::new();
1184 map.insert("a".to_string(), UdpConfig::default());
1185 map.insert("b".to_string(), UdpConfig::default());
1186 let named = TransportInstances::Named(map);
1187 let items: Vec<_> = named.iter().collect();
1188 assert_eq!(items.len(), 2);
1189 assert!(items.iter().all(|(name, _)| name.is_some()));
1191 }
1192
1193 #[test]
1194 fn test_parse_peer_config() {
1195 let yaml = r#"
1196peers:
1197 - npub: "npub1abc123"
1198 alias: "gateway"
1199 addresses:
1200 - transport: udp
1201 addr: "192.168.1.1:2121"
1202 priority: 1
1203 - transport: tor
1204 addr: "xyz.onion:2121"
1205 priority: 2
1206 connect_policy: auto_connect
1207"#;
1208 let config: Config = serde_yaml::from_str(yaml).unwrap();
1209
1210 assert_eq!(config.peers.len(), 1);
1211 let peer = &config.peers[0];
1212 assert_eq!(peer.npub, "npub1abc123");
1213 assert_eq!(peer.alias, Some("gateway".to_string()));
1214 assert_eq!(peer.addresses.len(), 2);
1215 assert!(peer.is_auto_connect());
1216
1217 let sorted = peer.addresses_by_priority();
1219 assert_eq!(sorted[0].transport, "udp");
1220 assert_eq!(sorted[0].priority, 1);
1221 assert_eq!(sorted[1].transport, "tor");
1222 assert_eq!(sorted[1].priority, 2);
1223 }
1224
1225 #[test]
1226 fn test_parse_peer_minimal() {
1227 let yaml = r#"
1228peers:
1229 - npub: "npub1xyz"
1230 addresses:
1231 - transport: udp
1232 addr: "10.0.0.1:2121"
1233"#;
1234 let config: Config = serde_yaml::from_str(yaml).unwrap();
1235
1236 assert_eq!(config.peers.len(), 1);
1237 let peer = &config.peers[0];
1238 assert_eq!(peer.npub, "npub1xyz");
1239 assert!(peer.alias.is_none());
1240 assert!(peer.is_auto_connect());
1242 assert_eq!(peer.addresses[0].priority, 100);
1244 }
1245
1246 #[test]
1247 fn test_parse_multiple_peers() {
1248 let yaml = r#"
1249peers:
1250 - npub: "npub1peer1"
1251 addresses:
1252 - transport: udp
1253 addr: "10.0.0.1:2121"
1254 - npub: "npub1peer2"
1255 addresses:
1256 - transport: udp
1257 addr: "10.0.0.2:2121"
1258 connect_policy: on_demand
1259"#;
1260 let config: Config = serde_yaml::from_str(yaml).unwrap();
1261
1262 assert_eq!(config.peers.len(), 2);
1263 assert_eq!(config.auto_connect_peers().count(), 1);
1264 }
1265
1266 #[test]
1267 fn test_peer_config_builder() {
1268 let peer = PeerConfig::new("npub1test", "udp", "192.168.1.1:2121")
1269 .with_alias("test-peer")
1270 .with_address(PeerAddress::with_priority("tor", "xyz.onion:2121", 50));
1271
1272 assert_eq!(peer.npub, "npub1test");
1273 assert_eq!(peer.alias, Some("test-peer".to_string()));
1274 assert_eq!(peer.addresses.len(), 2);
1275 assert!(peer.is_auto_connect());
1276 }
1277
1278 #[test]
1279 fn test_parse_nostr_discovery_config() {
1280 let yaml = r#"
1281node:
1282 discovery:
1283 nostr:
1284 enabled: true
1285 advertise: false
1286 policy: configured_only
1287 open_discovery_max_pending: 12
1288 app: "fips.nat.test.v1"
1289 signal_ttl_secs: 45
1290 advert_relays:
1291 - "wss://relay-a.example"
1292 dm_relays:
1293 - "wss://relay-b.example"
1294 stun_servers:
1295 - "stun:stun.example.org:3478"
1296peers:
1297 - npub: "npub1peer"
1298 addresses:
1299 - transport: udp
1300 addr: "nat"
1301"#;
1302 let config: Config = serde_yaml::from_str(yaml).unwrap();
1303 assert!(config.node.discovery.nostr.enabled);
1304 assert!(!config.node.discovery.nostr.advertise);
1305 assert_eq!(config.node.discovery.nostr.app, "fips.nat.test.v1");
1306 assert_eq!(config.node.discovery.nostr.signal_ttl_secs, 45);
1307 assert_eq!(
1308 config.node.discovery.nostr.policy,
1309 NostrDiscoveryPolicy::ConfiguredOnly
1310 );
1311 assert_eq!(config.node.discovery.nostr.open_discovery_max_pending, 12);
1312 assert_eq!(
1313 config.node.discovery.nostr.advert_relays,
1314 vec!["wss://relay-a.example".to_string()]
1315 );
1316 assert_eq!(
1317 config.node.discovery.nostr.dm_relays,
1318 vec!["wss://relay-b.example".to_string()]
1319 );
1320 assert_eq!(
1321 config.node.discovery.nostr.stun_servers,
1322 vec!["stun:stun.example.org:3478".to_string()]
1323 );
1324 assert_eq!(
1325 config.peers[0].addresses[0].addr, "nat",
1326 "udp:nat address should parse without special-casing in YAML"
1327 );
1328 }
1329
1330 #[test]
1331 fn test_validate_transport_advert_requires_nostr_enabled() {
1332 let mut config = Config::default();
1333 config.transports.udp = TransportInstances::Single(UdpConfig {
1334 advertise_on_nostr: Some(true),
1335 ..Default::default()
1336 });
1337 config.node.discovery.nostr.enabled = false;
1338
1339 let err = config.validate().expect_err("validation should fail");
1340 assert!(err.to_string().contains("advertise_on_nostr"));
1341
1342 config.transports.udp = TransportInstances::default();
1343 config.transports.webrtc = TransportInstances::Single(WebRtcConfig {
1344 advertise_on_nostr: Some(true),
1345 ..Default::default()
1346 });
1347
1348 let err = config.validate().expect_err("validation should fail");
1349 assert!(err.to_string().contains("advertise_on_nostr"));
1350 }
1351
1352 #[test]
1353 fn test_validate_empty_peer_addresses_require_nostr_enabled() {
1354 let mut config = Config {
1355 peers: vec![PeerConfig {
1356 npub: "npub1peer".to_string(),
1357 ..Default::default()
1358 }],
1359 ..Default::default()
1360 };
1361 config.node.discovery.nostr.enabled = false;
1362
1363 let err = config.validate().expect_err("validation should fail");
1364 assert!(err.to_string().contains("node.discovery.nostr"));
1365 }
1366
1367 #[test]
1368 fn test_validate_peer_addresses_optional_with_nostr_enabled() {
1369 let mut config = Config {
1371 peers: vec![PeerConfig {
1372 npub: "npub1peer".to_string(),
1373 ..Default::default()
1374 }],
1375 ..Default::default()
1376 };
1377 let err = config.validate().expect_err("validation should fail");
1378 assert!(err.to_string().contains("at least one address"));
1379
1380 config.node.discovery.nostr.enabled = true;
1382 config
1383 .validate()
1384 .expect("Nostr discovery should allow empty addresses");
1385 }
1386
1387 #[test]
1388 fn test_validate_nat_udp_advert_requires_relays_and_stun() {
1389 let mut config = Config::default();
1390 config.node.discovery.nostr.enabled = true;
1391 config.node.discovery.nostr.dm_relays.clear();
1392 config.transports.udp = TransportInstances::Single(UdpConfig {
1393 advertise_on_nostr: Some(true),
1394 public: Some(false),
1395 ..Default::default()
1396 });
1397
1398 let err = config.validate().expect_err("validation should fail");
1399 assert!(err.to_string().contains("dm_relays"));
1400
1401 config.node.discovery.nostr.dm_relays = vec!["wss://relay.example".to_string()];
1402 config.node.discovery.nostr.stun_servers.clear();
1403 let err = config.validate().expect_err("validation should fail");
1404 assert!(err.to_string().contains("stun_servers"));
1405 }
1406
1407 #[test]
1408 fn test_validate_webrtc_advert_requires_relays() {
1409 let mut config = Config::default();
1410 config.node.discovery.nostr.enabled = true;
1411 config.node.discovery.nostr.dm_relays.clear();
1412 config.transports.webrtc = TransportInstances::Single(WebRtcConfig {
1413 advertise_on_nostr: Some(true),
1414 ..Default::default()
1415 });
1416
1417 let err = config.validate().expect_err("validation should fail");
1418 assert!(err.to_string().contains("dm_relays"));
1419
1420 if let TransportInstances::Single(cfg) = &mut config.transports.webrtc {
1421 cfg.signal_relays = Some(vec!["wss://relay.example".to_string()]);
1422 }
1423 config
1424 .validate()
1425 .expect("WebRTC transport-specific relays should satisfy validation");
1426 }
1427
1428 #[test]
1429 fn test_is_loopback_addr_str() {
1430 assert!(is_loopback_addr_str("127.0.0.1:2121"));
1431 assert!(is_loopback_addr_str("127.0.0.5:9999"));
1432 assert!(is_loopback_addr_str("[::1]:2121"));
1433 assert!(is_loopback_addr_str("::1:2121"));
1434 assert!(is_loopback_addr_str("localhost:80"));
1435 assert!(!is_loopback_addr_str("0.0.0.0:2121"));
1436 assert!(!is_loopback_addr_str("192.168.1.1:2121"));
1437 assert!(!is_loopback_addr_str("[fd00::1]:2121"));
1438 assert!(!is_loopback_addr_str("core-vm.tail65015.ts.net:2121"));
1439 assert!(!is_loopback_addr_str("example.com:443"));
1440 }
1441
1442 #[cfg(unix)]
1443 #[test]
1444 fn test_resolve_default_socket_call_sites_agree() {
1445 let _guard = ENV_MUTEX.lock().unwrap();
1446
1447 let control_client = default_control_path().to_string_lossy().into_owned();
1448 let gateway_client = default_gateway_path().to_string_lossy().into_owned();
1449 let control_daemon = ControlConfig::default().socket_path;
1450
1451 assert_eq!(control_daemon, control_client);
1452
1453 let control_dir = Path::new(&control_client)
1454 .parent()
1455 .map(|p| p.to_string_lossy().into_owned())
1456 .unwrap_or_default();
1457 let gateway_dir = Path::new(&gateway_client)
1458 .parent()
1459 .map(|p| p.to_string_lossy().into_owned())
1460 .unwrap_or_default();
1461 assert_eq!(control_dir, gateway_dir);
1462 }
1463
1464 #[cfg(unix)]
1465 #[test]
1466 fn test_resolve_default_socket_xdg_when_no_run_fips() {
1467 let _guard = ENV_MUTEX.lock().unwrap();
1468
1469 let temp_dir = TempDir::new().unwrap();
1470 let prev_xdg = std::env::var("XDG_RUNTIME_DIR").ok();
1471
1472 unsafe {
1475 std::env::set_var("XDG_RUNTIME_DIR", temp_dir.path());
1476 }
1477
1478 let path = resolve_default_socket("control.sock");
1479
1480 unsafe {
1482 match prev_xdg {
1483 Some(value) => std::env::set_var("XDG_RUNTIME_DIR", value),
1484 None => std::env::remove_var("XDG_RUNTIME_DIR"),
1485 }
1486 }
1487
1488 assert!(
1489 path.starts_with("/run/fips/")
1490 || path.starts_with(&format!("{}/fips/", temp_dir.path().display())),
1491 "expected /run/fips or XDG path, got: {path}"
1492 );
1493 }
1494
1495 #[cfg(unix)]
1496 #[test]
1497 fn test_resolve_default_socket_tmp_when_xdg_invalid() {
1498 let _guard = ENV_MUTEX.lock().unwrap();
1499
1500 let prev_xdg = std::env::var("XDG_RUNTIME_DIR").ok();
1501 let bogus = "/nonexistent-xdg-runtime-dir-for-fips-test-zzz";
1502
1503 unsafe {
1505 std::env::set_var("XDG_RUNTIME_DIR", bogus);
1506 }
1507
1508 let path = resolve_default_socket("gateway.sock");
1509
1510 unsafe {
1512 match prev_xdg {
1513 Some(value) => std::env::set_var("XDG_RUNTIME_DIR", value),
1514 None => std::env::remove_var("XDG_RUNTIME_DIR"),
1515 }
1516 }
1517
1518 assert!(
1519 path.starts_with("/run/fips/") || path == "/tmp/fips-gateway.sock",
1520 "expected /run/fips or /tmp fallback, got: {path}"
1521 );
1522 assert!(
1523 !path.starts_with(bogus),
1524 "stale XDG_RUNTIME_DIR leaked into resolver: {path}"
1525 );
1526 }
1527
1528 #[test]
1529 fn test_validate_loopback_bind_with_external_peer_rejected() {
1530 use crate::config::PeerAddress;
1531 let mut config = Config::default();
1532 config.transports.udp = TransportInstances::Single(UdpConfig {
1533 bind_addr: Some("127.0.0.1:2121".to_string()),
1534 ..Default::default()
1535 });
1536 config.peers = vec![PeerConfig {
1537 npub: "npub1peer".to_string(),
1538 addresses: vec![PeerAddress::new("udp", "core-vm.tail65015.ts.net:2121")],
1539 ..Default::default()
1540 }];
1541
1542 let err = config.validate().expect_err("validation should fail");
1543 let msg = err.to_string();
1544 assert!(msg.contains("loopback"), "got: {msg}");
1545 assert!(msg.contains("non-loopback"), "got: {msg}");
1546 }
1547
1548 #[test]
1549 fn test_validate_loopback_bind_with_loopback_peer_ok() {
1550 use crate::config::PeerAddress;
1551 let mut config = Config::default();
1552 config.transports.udp = TransportInstances::Single(UdpConfig {
1553 bind_addr: Some("127.0.0.1:2121".to_string()),
1554 ..Default::default()
1555 });
1556 config.peers = vec![PeerConfig {
1557 npub: "npub1peer".to_string(),
1558 addresses: vec![PeerAddress::new("udp", "127.0.0.2:2121")],
1559 ..Default::default()
1560 }];
1561
1562 config
1563 .validate()
1564 .expect("loopback peer with loopback bind should validate");
1565 }
1566
1567 #[test]
1568 fn test_validate_outbound_only_exempt_from_loopback_check() {
1569 use crate::config::PeerAddress;
1570 let mut config = Config::default();
1571 config.transports.udp = TransportInstances::Single(UdpConfig {
1574 bind_addr: Some("127.0.0.1:2121".to_string()),
1575 outbound_only: Some(true),
1576 ..Default::default()
1577 });
1578 config.peers = vec![PeerConfig {
1579 npub: "npub1peer".to_string(),
1580 addresses: vec![PeerAddress::new("udp", "core-vm.tail65015.ts.net:2121")],
1581 ..Default::default()
1582 }];
1583
1584 config
1585 .validate()
1586 .expect("outbound_only should be exempt from the loopback check");
1587 }
1588
1589 #[test]
1590 fn test_outbound_only_forces_ephemeral_bind() {
1591 let cfg = UdpConfig {
1592 bind_addr: Some("127.0.0.1:2121".to_string()),
1593 outbound_only: Some(true),
1594 ..Default::default()
1595 };
1596 assert_eq!(cfg.bind_addr(), "0.0.0.0:0");
1597 assert!(cfg.outbound_only());
1598 }
1599
1600 #[test]
1601 fn test_outbound_only_forces_advertise_off() {
1602 let cfg = UdpConfig {
1603 advertise_on_nostr: Some(true),
1604 outbound_only: Some(true),
1605 ..Default::default()
1606 };
1607 assert!(!cfg.advertise_on_nostr());
1608 }
1609
1610 #[test]
1611 fn test_udp_accept_connections_default_true() {
1612 let cfg = UdpConfig::default();
1613 assert!(cfg.accept_connections());
1614 }
1615}