saorsa_core/bootstrap/
mod.rs1pub mod contact;
21pub mod manager;
22
23pub use manager::BootstrapManager;
25pub use manager::{
26 BootstrapConfig, BootstrapStats, CacheConfig, DEFAULT_CLEANUP_INTERVAL, DEFAULT_MAX_CONTACTS,
27 DEFAULT_MERGE_INTERVAL, DEFAULT_QUALITY_UPDATE_INTERVAL,
28};
29
30pub use contact::{
32 ConnectionHistory, ContactEntry, QualityCalculator, QualityMetrics, QuicConnectionType,
33 QuicContactInfo, QuicQualityMetrics,
34};
35
36pub use four_word_networking as fourwords;
38use four_word_networking::FourWordAdaptiveEncoder;
39
40use crate::error::BootstrapError;
41use crate::{P2PError, Result};
42use std::net::IpAddr;
43use std::net::Ipv6Addr;
44use std::path::PathBuf;
45
46pub const DEFAULT_CACHE_DIR: &str = ".cache/saorsa";
48
49#[derive(Debug, Clone)]
51pub struct FourWordAddress(pub String);
52
53impl FourWordAddress {
54 pub fn from_string(s: &str) -> Result<Self> {
55 let parts: Vec<&str> = s.split(['.', '-']).collect();
56 if parts.len() != 4 {
57 return Err(P2PError::Bootstrap(BootstrapError::InvalidData(
58 "Four-word address must have exactly 4 words"
59 .to_string()
60 .into(),
61 )));
62 }
63 Ok(FourWordAddress(parts.join("-")))
64 }
65
66 pub fn validate(&self, _encoder: &WordEncoder) -> bool {
67 let parts: Vec<&str> = self.0.split(['.', '-']).collect();
68 parts.len() == 4 && parts.iter().all(|part| !part.is_empty())
69 }
70}
71
72#[derive(Debug, Clone)]
73pub struct WordDictionary;
74
75#[derive(Debug, Clone)]
76pub struct WordEncoder;
77
78impl Default for WordEncoder {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84impl WordEncoder {
85 pub fn new() -> Self {
86 Self
87 }
88
89 pub fn encode_multiaddr_string(&self, multiaddr: &str) -> Result<FourWordAddress> {
90 let socket_addr: std::net::SocketAddr = multiaddr
91 .parse()
92 .map_err(|e| P2PError::Bootstrap(BootstrapError::InvalidData(format!("{e}").into())))?;
93 self.encode_socket_addr(&socket_addr)
94 }
95
96 pub fn decode_to_socket_addr(&self, words: &FourWordAddress) -> Result<std::net::SocketAddr> {
97 let encoder = FourWordAdaptiveEncoder::new().map_err(|e| {
98 P2PError::Bootstrap(BootstrapError::InvalidData(
99 format!("Encoder init failed: {e}").into(),
100 ))
101 })?;
102 let normalized = words.0.replace(' ', "-");
103 let decoded = encoder.decode(&normalized).map_err(|e| {
104 P2PError::Bootstrap(BootstrapError::InvalidData(
105 format!("Failed to decode four-word address: {e}").into(),
106 ))
107 })?;
108 decoded.parse::<std::net::SocketAddr>().map_err(|_| {
109 P2PError::Bootstrap(BootstrapError::InvalidData(
110 "Decoded address missing port".to_string().into(),
111 ))
112 })
113 }
114
115 pub fn encode_socket_addr(&self, addr: &std::net::SocketAddr) -> Result<FourWordAddress> {
116 let encoder = FourWordAdaptiveEncoder::new().map_err(|e| {
117 P2PError::Bootstrap(BootstrapError::InvalidData(
118 format!("Encoder init failed: {e}").into(),
119 ))
120 })?;
121 let encoded = encoder
122 .encode(&addr.to_string())
123 .map_err(|e| P2PError::Bootstrap(BootstrapError::InvalidData(format!("{e}").into())))?;
124 Ok(FourWordAddress(encoded.replace(' ', "-")))
125 }
126}
127
128#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
130pub struct CacheStats {
131 pub total_contacts: usize,
133 pub high_quality_contacts: usize,
135 pub verified_contacts: usize,
137 pub last_merge: chrono::DateTime<chrono::Utc>,
139 pub last_cleanup: chrono::DateTime<chrono::Utc>,
141 pub cache_hit_rate: f64,
143 pub average_quality_score: f64,
145
146 pub iroh_contacts: usize,
149 pub nat_traversal_contacts: usize,
151 pub avg_iroh_setup_time_ms: f64,
153 pub preferred_iroh_connection_type: Option<String>,
155}
156
157pub fn ip_to_ipv6(ip: &IpAddr) -> Ipv6Addr {
162 match ip {
163 IpAddr::V4(ipv4) => ipv4.to_ipv6_mapped(),
164 IpAddr::V6(ipv6) => *ipv6,
165 }
166}
167
168pub fn home_cache_dir() -> Result<PathBuf> {
170 let home = std::env::var("HOME")
171 .or_else(|_| std::env::var("USERPROFILE"))
172 .map_err(|_| {
173 P2PError::Bootstrap(BootstrapError::CacheError(
174 "Unable to determine home directory".to_string().into(),
175 ))
176 })?;
177
178 let cache_dir = PathBuf::from(home).join(DEFAULT_CACHE_DIR);
179
180 std::fs::create_dir_all(&cache_dir).map_err(|e| {
182 P2PError::Bootstrap(BootstrapError::CacheError(
183 format!("Failed to create cache directory: {e}").into(),
184 ))
185 })?;
186
187 Ok(cache_dir)
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193 use crate::rate_limit::JoinRateLimiterConfig;
194 use crate::security::IPDiversityConfig;
195 use tempfile::TempDir;
196
197 #[tokio::test]
198 async fn test_bootstrap_manager_creation() {
199 let temp_dir = TempDir::new().unwrap();
200 let config = CacheConfig {
201 cache_dir: temp_dir.path().to_path_buf(),
202 max_contacts: 1000,
203 ..CacheConfig::default()
204 };
205
206 let manager = BootstrapManager::with_full_config(
207 config,
208 JoinRateLimiterConfig::default(),
209 IPDiversityConfig::default(),
210 )
211 .await;
212 assert!(manager.is_ok());
213 }
214
215 #[tokio::test]
216 async fn test_home_cache_dir() {
217 let result = home_cache_dir();
218 assert!(result.is_ok());
219
220 let path = result.unwrap();
221 assert!(path.exists());
222 assert!(path.is_dir());
223 }
224}