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