Skip to main content

hashtree_cli/
p2p_common.rs

1use 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
127/// Build default WebRTC config from daemon/app config.
128pub fn default_webrtc_config(config: &Config) -> WebRTCConfig {
129    let relays = if config.server.enable_webrtc {
130        config.nostr.relays.clone()
131    } else {
132        Vec::new()
133    };
134    let local_only_relays =
135        !relays.is_empty() && relays.iter().all(|relay| relay_is_loopback(relay));
136    let stun_servers =
137        if !config.server.enable_webrtc || (config.server.enable_multicast && local_only_relays) {
138            Vec::new()
139        } else {
140            WebRTCConfig::default().stun_servers
141        };
142    let signal_urls = if config.server.peer_signal_urls.is_empty() {
143        infer_loopback_peer_signal_url(&config.server.bind_address)
144            .into_iter()
145            .collect()
146    } else {
147        config
148            .server
149            .peer_signal_urls
150            .iter()
151            .map(|url| url.trim().trim_end_matches('/').to_string())
152            .filter(|url| url.starts_with("http://"))
153            .collect()
154    };
155
156    WebRTCConfig {
157        relays,
158        signaling_enabled: config.server.enable_webrtc,
159        hash_get_enabled: config.server.mode.hash_get_enabled(),
160        signal_urls,
161        stun_servers,
162        multicast: MulticastConfig {
163            enabled: config.server.enable_multicast,
164            group: config.server.multicast_group.clone(),
165            port: config.server.multicast_port,
166            max_peers: config.server.max_multicast_peers,
167            ..Default::default()
168        },
169        wifi_aware: WifiAwareConfig {
170            enabled: config.server.enable_wifi_aware,
171            max_peers: config.server.max_wifi_aware_peers,
172            ..Default::default()
173        },
174        bluetooth: BluetoothConfig {
175            enabled: config.server.enable_bluetooth,
176            max_peers: config.server.max_bluetooth_peers,
177        },
178        ..Default::default()
179    }
180}
181
182/// Build peer classifier used by daemon/runtime startup paths.
183pub fn build_peer_classifier(
184    data_dir: PathBuf,
185    store: Arc<dyn socialgraph::SocialGraphBackend>,
186) -> PeerClassifier {
187    let contacts_file = data_dir.join("contacts.json");
188    Arc::new(move |pubkey_hex: &str| {
189        if contacts_file.exists() {
190            if let Ok(data) = std::fs::read_to_string(&contacts_file) {
191                if let Ok(contacts) = serde_json::from_str::<Vec<String>>(&data) {
192                    if contacts.contains(&pubkey_hex.to_string()) {
193                        return PeerPool::Follows;
194                    }
195                }
196            }
197        }
198        if let Ok(pk_bytes) = hex::decode(pubkey_hex) {
199            if pk_bytes.len() == 32 {
200                let pk: [u8; 32] = pk_bytes.try_into().unwrap();
201                if let Some(dist) = socialgraph::get_follow_distance(store.as_ref(), &pk) {
202                    if dist <= 2 {
203                        return PeerPool::Follows;
204                    }
205                }
206            }
207        }
208        PeerPool::Other
209    })
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn default_webrtc_config_disables_stun_for_loopback_only_multicast() {
218        let mut config = Config::default();
219        config.server.enable_multicast = true;
220        config.server.max_multicast_peers = 4;
221        config.nostr.relays = vec!["ws://127.0.0.1:8080/ws".to_string()];
222
223        let webrtc = default_webrtc_config(&config);
224        assert!(webrtc.stun_servers.is_empty());
225    }
226
227    #[test]
228    fn default_webrtc_config_keeps_stun_for_non_loopback_relays() {
229        let mut config = Config::default();
230        config.server.enable_multicast = true;
231        config.server.max_multicast_peers = 4;
232        config.nostr.relays = vec!["wss://relay.example".to_string()];
233
234        let webrtc = default_webrtc_config(&config);
235        assert!(!webrtc.stun_servers.is_empty());
236    }
237
238    #[test]
239    fn default_webrtc_config_maps_bluetooth_limits() {
240        let mut config = Config::default();
241        config.server.enable_bluetooth = true;
242        config.server.max_bluetooth_peers = 3;
243
244        let webrtc = default_webrtc_config(&config);
245        assert!(webrtc.signaling_enabled);
246        assert!(webrtc.bluetooth.enabled);
247        assert_eq!(webrtc.bluetooth.max_peers, 3);
248    }
249
250    #[test]
251    fn default_webrtc_config_maps_wifi_aware_limits() {
252        let mut config = Config::default();
253        config.server.enable_wifi_aware = true;
254        config.server.max_wifi_aware_peers = 4;
255
256        let webrtc = default_webrtc_config(&config);
257        assert!(webrtc.signaling_enabled);
258        assert!(webrtc.wifi_aware.enabled);
259        assert_eq!(webrtc.wifi_aware.max_peers, 4);
260    }
261
262    #[test]
263    fn default_webrtc_config_uses_loopback_bind_address_for_webrtc_signaling() {
264        let mut config = Config::default();
265        config.server.bind_address = "127.0.0.1:18080".to_string();
266
267        let webrtc = default_webrtc_config(&config);
268        assert_eq!(
269            webrtc.signal_urls,
270            vec!["http://127.0.0.1:18080".to_string()]
271        );
272    }
273
274    #[test]
275    fn default_webrtc_config_prefers_explicit_peer_signal_urls() {
276        let mut config = Config::default();
277        config.server.bind_address = "127.0.0.1:18080".to_string();
278        config.server.peer_signal_urls = vec![
279            "http://peer.example:18080/".to_string(),
280            "https://peer.example/".to_string(),
281        ];
282
283        let webrtc = default_webrtc_config(&config);
284        assert_eq!(
285            webrtc.signal_urls,
286            vec!["http://peer.example:18080".to_string()]
287        );
288    }
289
290    #[test]
291    fn default_webrtc_config_strips_relays_and_stun_when_webrtc_disabled() {
292        let mut config = Config::default();
293        config.server.enable_webrtc = false;
294        config.server.enable_bluetooth = true;
295        config.server.max_bluetooth_peers = 2;
296        config.server.stun_port = 3478;
297        config.nostr.relays = vec!["wss://relay.example".to_string()];
298
299        let webrtc = default_webrtc_config(&config);
300        assert!(!webrtc.signaling_enabled);
301        assert!(webrtc.relays.is_empty());
302        assert!(webrtc.stun_servers.is_empty());
303    }
304
305    #[test]
306    fn default_webrtc_config_uses_relays_when_nostr_service_disabled() {
307        let mut config = Config::default();
308        config.nostr.enabled = false;
309        config.nostr.relays = vec!["wss://relay.example".to_string()];
310
311        let webrtc = default_webrtc_config(&config);
312
313        assert_eq!(webrtc.relays, vec!["wss://relay.example".to_string()]);
314        assert!(webrtc.signaling_enabled);
315    }
316
317    #[test]
318    fn stun_server_only_starts_when_webrtc_is_enabled() {
319        let mut config = Config::default();
320        config.server.stun_port = 3478;
321
322        assert!(should_start_stun_server(&config));
323
324        config.server.enable_webrtc = false;
325        assert!(!should_start_stun_server(&config));
326    }
327
328    #[test]
329    fn peer_router_enabled_for_wifi_aware_only() {
330        let mut config = Config::default();
331        config.server.enable_webrtc = false;
332        config.server.enable_multicast = false;
333        config.server.max_multicast_peers = 0;
334        config.server.enable_bluetooth = false;
335        config.server.max_bluetooth_peers = 0;
336        config.server.enable_wifi_aware = true;
337        config.server.max_wifi_aware_peers = 2;
338
339        assert!(peer_router_enabled(&config));
340
341        config.server.max_wifi_aware_peers = 0;
342        assert!(!peer_router_enabled(&config));
343    }
344}