hashtree_cli/
p2p_common.rs1use std::path::PathBuf;
2use std::sync::Arc;
3
4use crate::config::Config;
5use crate::socialgraph;
6use crate::webrtc::{
7 BluetoothConfig, KnownPeerSnapshot, MulticastConfig, PeerClassifier, PeerPool, WebRTCConfig,
8 WebRTCState, WifiAwareConfig,
9};
10use anyhow::{Context, Result};
11use hashtree_network::PeerMetadataSnapshot;
12
13const PEER_STATE_FILE: &str = "mesh-peer-state.json";
14const PEER_STATE_VERSION: u32 = 1;
15
16#[derive(Debug, serde::Serialize, serde::Deserialize)]
17struct PersistedPeerState {
18 version: u32,
19 #[serde(default)]
20 peer_metadata: PeerMetadataSnapshot,
21 #[serde(default)]
22 known_peers: KnownPeerSnapshot,
23}
24
25impl Default for PersistedPeerState {
26 fn default() -> Self {
27 Self {
28 version: PEER_STATE_VERSION,
29 peer_metadata: PeerMetadataSnapshot::default(),
30 known_peers: KnownPeerSnapshot::default(),
31 }
32 }
33}
34
35fn relay_is_loopback(relay: &str) -> bool {
36 relay.contains("://127.0.0.1") || relay.contains("://localhost") || relay.contains("://[::1]")
37}
38
39fn bind_address_is_loopback(host: &str) -> bool {
40 matches!(host, "127.0.0.1" | "localhost" | "::1" | "[::1]")
41}
42
43pub fn infer_loopback_peer_signal_url(bind_address: &str) -> Option<String> {
44 let trimmed = bind_address.trim();
45 let (host, port) = trimmed.rsplit_once(':')?;
46 if port.is_empty() || !port.chars().all(|ch| ch.is_ascii_digit()) {
47 return None;
48 }
49 let host = host.trim_start_matches('[').trim_end_matches(']');
50 if !bind_address_is_loopback(host) {
51 return None;
52 }
53 Some(format!("http://127.0.0.1:{port}"))
54}
55
56fn peer_state_path(data_dir: &std::path::Path) -> PathBuf {
57 data_dir.join(PEER_STATE_FILE)
58}
59
60pub async fn load_peer_state(data_dir: &std::path::Path, state: &Arc<WebRTCState>) -> Result<bool> {
61 let path = peer_state_path(data_dir);
62 let content = match std::fs::read_to_string(&path) {
63 Ok(content) => content,
64 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
65 Err(err) => return Err(err).with_context(|| format!("read {}", path.display())),
66 };
67 let persisted: PersistedPeerState = serde_json::from_str(&content)
68 .with_context(|| format!("parse persisted peer state {}", path.display()))?;
69 if persisted.version != PEER_STATE_VERSION {
70 return Ok(false);
71 }
72 state
73 .import_peer_metadata_snapshot(&persisted.peer_metadata)
74 .await;
75 state
76 .import_known_peer_snapshot(&persisted.known_peers)
77 .await;
78 Ok(true)
79}
80
81pub async fn persist_peer_state(
82 data_dir: &std::path::Path,
83 state: &Arc<WebRTCState>,
84) -> Result<()> {
85 std::fs::create_dir_all(data_dir)
86 .with_context(|| format!("create data dir {}", data_dir.display()))?;
87 let path = peer_state_path(data_dir);
88 let tmp_path = path.with_extension("json.tmp");
89 let persisted = PersistedPeerState {
90 version: PEER_STATE_VERSION,
91 peer_metadata: state.peer_metadata_snapshot().await,
92 known_peers: state.known_peer_snapshot().await,
93 };
94 let content = serde_json::to_vec_pretty(&persisted).context("encode persisted peer state")?;
95 std::fs::write(&tmp_path, content).with_context(|| format!("write {}", tmp_path.display()))?;
96 std::fs::rename(&tmp_path, &path)
97 .with_context(|| format!("replace persisted peer state {}", path.display()))?;
98 Ok(())
99}
100
101pub fn spawn_peer_state_persist_task(
102 data_dir: PathBuf,
103 state: Arc<WebRTCState>,
104) -> tokio::task::JoinHandle<()> {
105 tokio::spawn(async move {
106 let mut interval = tokio::time::interval(std::time::Duration::from_secs(5));
107 loop {
108 interval.tick().await;
109 if let Err(err) = persist_peer_state(&data_dir, &state).await {
110 tracing::debug!("Failed to persist mesh peer state: {err:#}");
111 }
112 }
113 })
114}
115
116pub fn peer_router_enabled(config: &Config) -> bool {
117 config.server.enable_webrtc
118 || (config.server.enable_multicast && config.server.max_multicast_peers > 0)
119 || (config.server.enable_wifi_aware && config.server.max_wifi_aware_peers > 0)
120 || (config.server.enable_bluetooth && config.server.max_bluetooth_peers > 0)
121}
122
123pub fn should_start_stun_server(config: &Config) -> bool {
124 config.server.enable_webrtc && config.server.stun_port > 0
125}
126
127pub fn default_webrtc_config(config: &Config) -> WebRTCConfig {
129 let active_relays = config.nostr.active_relays();
130 let local_only_relays =
131 !active_relays.is_empty() && active_relays.iter().all(|relay| relay_is_loopback(relay));
132 let relays = if config.server.enable_webrtc {
133 active_relays
134 } else {
135 Vec::new()
136 };
137 let stun_servers =
138 if !config.server.enable_webrtc || (config.server.enable_multicast && local_only_relays) {
139 Vec::new()
140 } else {
141 WebRTCConfig::default().stun_servers
142 };
143 let signal_urls = if config.server.peer_signal_urls.is_empty() {
144 infer_loopback_peer_signal_url(&config.server.bind_address)
145 .into_iter()
146 .collect()
147 } else {
148 config
149 .server
150 .peer_signal_urls
151 .iter()
152 .map(|url| url.trim().trim_end_matches('/').to_string())
153 .filter(|url| url.starts_with("http://"))
154 .collect()
155 };
156
157 WebRTCConfig {
158 relays,
159 signaling_enabled: config.server.enable_webrtc,
160 hash_get_enabled: config.server.mode.hash_get_enabled(),
161 signal_urls,
162 stun_servers,
163 multicast: MulticastConfig {
164 enabled: config.server.enable_multicast,
165 group: config.server.multicast_group.clone(),
166 port: config.server.multicast_port,
167 max_peers: config.server.max_multicast_peers,
168 ..Default::default()
169 },
170 wifi_aware: WifiAwareConfig {
171 enabled: config.server.enable_wifi_aware,
172 max_peers: config.server.max_wifi_aware_peers,
173 ..Default::default()
174 },
175 bluetooth: BluetoothConfig {
176 enabled: config.server.enable_bluetooth,
177 max_peers: config.server.max_bluetooth_peers,
178 },
179 ..Default::default()
180 }
181}
182
183pub fn build_peer_classifier(
185 data_dir: PathBuf,
186 store: Arc<dyn socialgraph::SocialGraphBackend>,
187) -> PeerClassifier {
188 let contacts_file = data_dir.join("contacts.json");
189 Arc::new(move |pubkey_hex: &str| {
190 if contacts_file.exists() {
191 if let Ok(data) = std::fs::read_to_string(&contacts_file) {
192 if let Ok(contacts) = serde_json::from_str::<Vec<String>>(&data) {
193 if contacts.contains(&pubkey_hex.to_string()) {
194 return PeerPool::Follows;
195 }
196 }
197 }
198 }
199 if let Ok(pk_bytes) = hex::decode(pubkey_hex) {
200 if pk_bytes.len() == 32 {
201 let pk: [u8; 32] = pk_bytes.try_into().unwrap();
202 if let Some(dist) = socialgraph::get_follow_distance(store.as_ref(), &pk) {
203 if dist <= 2 {
204 return PeerPool::Follows;
205 }
206 }
207 }
208 }
209 PeerPool::Other
210 })
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn default_webrtc_config_disables_stun_for_loopback_only_multicast() {
219 let mut config = Config::default();
220 config.server.enable_multicast = true;
221 config.server.max_multicast_peers = 4;
222 config.nostr.relays = vec!["ws://127.0.0.1:8080/ws".to_string()];
223
224 let webrtc = default_webrtc_config(&config);
225 assert!(webrtc.stun_servers.is_empty());
226 }
227
228 #[test]
229 fn default_webrtc_config_keeps_stun_for_non_loopback_relays() {
230 let mut config = Config::default();
231 config.server.enable_multicast = true;
232 config.server.max_multicast_peers = 4;
233 config.nostr.relays = vec!["wss://relay.example".to_string()];
234
235 let webrtc = default_webrtc_config(&config);
236 assert!(!webrtc.stun_servers.is_empty());
237 }
238
239 #[test]
240 fn default_webrtc_config_maps_bluetooth_limits() {
241 let mut config = Config::default();
242 config.server.enable_bluetooth = true;
243 config.server.max_bluetooth_peers = 3;
244
245 let webrtc = default_webrtc_config(&config);
246 assert!(webrtc.signaling_enabled);
247 assert!(webrtc.bluetooth.enabled);
248 assert_eq!(webrtc.bluetooth.max_peers, 3);
249 }
250
251 #[test]
252 fn default_webrtc_config_maps_wifi_aware_limits() {
253 let mut config = Config::default();
254 config.server.enable_wifi_aware = true;
255 config.server.max_wifi_aware_peers = 4;
256
257 let webrtc = default_webrtc_config(&config);
258 assert!(webrtc.signaling_enabled);
259 assert!(webrtc.wifi_aware.enabled);
260 assert_eq!(webrtc.wifi_aware.max_peers, 4);
261 }
262
263 #[test]
264 fn default_webrtc_config_uses_loopback_bind_address_for_webrtc_signaling() {
265 let mut config = Config::default();
266 config.server.bind_address = "127.0.0.1:18080".to_string();
267
268 let webrtc = default_webrtc_config(&config);
269 assert_eq!(
270 webrtc.signal_urls,
271 vec!["http://127.0.0.1:18080".to_string()]
272 );
273 }
274
275 #[test]
276 fn default_webrtc_config_prefers_explicit_peer_signal_urls() {
277 let mut config = Config::default();
278 config.server.bind_address = "127.0.0.1:18080".to_string();
279 config.server.peer_signal_urls = vec![
280 "http://peer.example:18080/".to_string(),
281 "https://peer.example/".to_string(),
282 ];
283
284 let webrtc = default_webrtc_config(&config);
285 assert_eq!(
286 webrtc.signal_urls,
287 vec!["http://peer.example:18080".to_string()]
288 );
289 }
290
291 #[test]
292 fn default_webrtc_config_strips_relays_and_stun_when_webrtc_disabled() {
293 let mut config = Config::default();
294 config.server.enable_webrtc = false;
295 config.server.enable_bluetooth = true;
296 config.server.max_bluetooth_peers = 2;
297 config.server.stun_port = 3478;
298 config.nostr.relays = vec!["wss://relay.example".to_string()];
299
300 let webrtc = default_webrtc_config(&config);
301 assert!(!webrtc.signaling_enabled);
302 assert!(webrtc.relays.is_empty());
303 assert!(webrtc.stun_servers.is_empty());
304 }
305
306 #[test]
307 fn stun_server_only_starts_when_webrtc_is_enabled() {
308 let mut config = Config::default();
309 config.server.stun_port = 3478;
310
311 assert!(should_start_stun_server(&config));
312
313 config.server.enable_webrtc = false;
314 assert!(!should_start_stun_server(&config));
315 }
316
317 #[test]
318 fn peer_router_enabled_for_wifi_aware_only() {
319 let mut config = Config::default();
320 config.server.enable_webrtc = false;
321 config.server.enable_multicast = false;
322 config.server.max_multicast_peers = 0;
323 config.server.enable_bluetooth = false;
324 config.server.max_bluetooth_peers = 0;
325 config.server.enable_wifi_aware = true;
326 config.server.max_wifi_aware_peers = 2;
327
328 assert!(peer_router_enabled(&config));
329
330 config.server.max_wifi_aware_peers = 0;
331 assert!(!peer_router_enabled(&config));
332 }
333}