Skip to main content

ferripfs_network/
swarm.rs

1// Ported from: kubo/core/node/libp2p/transport.go
2// Kubo version: v0.39.0
3//
4// Original work: Copyright (c) Protocol Labs, Inc.
5// Port: Copyright (c) 2026 ferripfs contributors
6// SPDX-License-Identifier: MIT OR Apache-2.0
7
8//! Swarm builder with transport configuration.
9
10use std::time::Duration;
11
12use libp2p::{
13    identity::Keypair, noise, yamux, Multiaddr, PeerId, Swarm,
14    SwarmBuilder as Libp2pSwarmBuilder,
15};
16
17use crate::{behavior::FerripfsBehavior, NetworkError, NetworkResult};
18
19/// Configuration for building a swarm.
20#[derive(Debug, Clone)]
21pub struct SwarmConfig {
22    /// Enable TCP transport
23    pub tcp_enabled: bool,
24    /// Enable QUIC transport
25    pub quic_enabled: bool,
26    /// Enable WebSocket transport
27    pub websocket_enabled: bool,
28    /// Enable mDNS discovery
29    pub mdns_enabled: bool,
30    /// Enable relay client
31    pub relay_enabled: bool,
32    /// Connection idle timeout
33    pub idle_timeout: Duration,
34    /// Maximum established connections per peer
35    pub max_connections_per_peer: u32,
36    /// Connection manager low water mark
37    pub conn_mgr_low: u32,
38    /// Connection manager high water mark
39    pub conn_mgr_high: u32,
40}
41
42impl Default for SwarmConfig {
43    fn default() -> Self {
44        Self {
45            tcp_enabled: true,
46            quic_enabled: true,
47            websocket_enabled: true,
48            mdns_enabled: true,
49            relay_enabled: true,
50            idle_timeout: Duration::from_secs(30),
51            max_connections_per_peer: 8,
52            conn_mgr_low: 32,
53            conn_mgr_high: 96,
54        }
55    }
56}
57
58impl SwarmConfig {
59    /// Create config from ferripfs config.
60    pub fn from_config(config: &ferripfs_config::Config) -> Self {
61        let mut sc = Self::default();
62
63        // Parse transport settings - use with_default to get bool value
64        if let Some(ref flag) = config.swarm.transports.network.tcp {
65            sc.tcp_enabled = flag.with_default(true);
66        }
67        if let Some(ref flag) = config.swarm.transports.network.quic {
68            sc.quic_enabled = flag.with_default(true);
69        }
70        if let Some(ref flag) = config.swarm.transports.network.websocket {
71            sc.websocket_enabled = flag.with_default(true);
72        }
73        if let Some(ref flag) = config.swarm.relay_client.enabled {
74            sc.relay_enabled = flag.with_default(true);
75        }
76
77        // Parse connection manager settings
78        if let Some(ref low) = config.swarm.conn_mgr.low_water {
79            if let Some(v) = low.value() {
80                sc.conn_mgr_low = v as u32;
81            }
82        }
83        if let Some(ref high) = config.swarm.conn_mgr.high_water {
84            if let Some(v) = high.value() {
85                sc.conn_mgr_high = v as u32;
86            }
87        }
88
89        sc
90    }
91}
92
93/// Builder for creating a libp2p swarm.
94pub struct SwarmBuilder {
95    keypair: Keypair,
96    config: SwarmConfig,
97    listen_addrs: Vec<Multiaddr>,
98    bootstrap_peers: Vec<(PeerId, Multiaddr)>,
99}
100
101impl SwarmBuilder {
102    /// Create a new swarm builder.
103    pub fn new(keypair: Keypair) -> Self {
104        Self {
105            keypair,
106            config: SwarmConfig::default(),
107            listen_addrs: Vec::new(),
108            bootstrap_peers: Vec::new(),
109        }
110    }
111
112    /// Set swarm configuration.
113    pub fn with_config(mut self, config: SwarmConfig) -> Self {
114        self.config = config;
115        self
116    }
117
118    /// Add listen addresses.
119    pub fn with_listen_addrs(mut self, addrs: Vec<Multiaddr>) -> Self {
120        self.listen_addrs = addrs;
121        self
122    }
123
124    /// Add bootstrap peers.
125    pub fn with_bootstrap_peers(mut self, peers: Vec<(PeerId, Multiaddr)>) -> Self {
126        self.bootstrap_peers = peers;
127        self
128    }
129
130    /// Build the swarm.
131    pub fn build(self) -> NetworkResult<Swarm<FerripfsBehavior>> {
132        // Build the swarm using the builder pattern
133        let swarm = Libp2pSwarmBuilder::with_existing_identity(self.keypair)
134            .with_tokio()
135            .with_tcp(
136                libp2p::tcp::Config::default(),
137                noise::Config::new,
138                yamux::Config::default,
139            )
140            .map_err(|e| NetworkError::Transport(e.to_string()))?
141            .with_quic()
142            .with_dns()
143            .map_err(|e| NetworkError::Transport(e.to_string()))?
144            .with_relay_client(noise::Config::new, yamux::Config::default)
145            .map_err(|e| NetworkError::Transport(e.to_string()))?
146            .with_behaviour(|keypair, relay_client| {
147                FerripfsBehavior::new(
148                    PeerId::from(keypair.public()),
149                    keypair.public(),
150                    relay_client,
151                )
152                .expect("Failed to create network behavior")
153            })
154            .expect("Infallible error occurred")
155            .with_swarm_config(|c| c.with_idle_connection_timeout(self.config.idle_timeout))
156            .build();
157
158        Ok(swarm)
159    }
160}
161
162/// Parse a multiaddr string into a Multiaddr.
163pub fn parse_multiaddr(s: &str) -> NetworkResult<Multiaddr> {
164    s.parse()
165        .map_err(|e: multiaddr::Error| NetworkError::InvalidMultiaddr(e.to_string()))
166}
167
168/// Parse a peer ID string into a PeerId.
169pub fn parse_peer_id(s: &str) -> NetworkResult<PeerId> {
170    s.parse()
171        .map_err(|e: libp2p::identity::ParseError| NetworkError::InvalidPeerId(e.to_string()))
172}
173
174/// Extract peer ID from a multiaddr if present.
175pub fn extract_peer_id(addr: &Multiaddr) -> Option<PeerId> {
176    addr.iter().find_map(|p| {
177        if let multiaddr::Protocol::P2p(peer_id) = p {
178            Some(peer_id)
179        } else {
180            None
181        }
182    })
183}
184
185/// Parse bootstrap peer multiaddr into (PeerId, Multiaddr) pair.
186pub fn parse_bootstrap_peer(addr_str: &str) -> NetworkResult<(PeerId, Multiaddr)> {
187    let addr = parse_multiaddr(addr_str)?;
188    let peer_id = extract_peer_id(&addr).ok_or_else(|| {
189        NetworkError::InvalidMultiaddr("Missing peer ID in bootstrap address".into())
190    })?;
191
192    // Remove the /p2p component to get the transport address
193    let transport_addr: Multiaddr = addr
194        .iter()
195        .filter(|p| !matches!(p, multiaddr::Protocol::P2p(_)))
196        .collect();
197
198    Ok((peer_id, transport_addr))
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_parse_multiaddr() {
207        let addr = parse_multiaddr("/ip4/127.0.0.1/tcp/4001").unwrap();
208        assert!(addr.to_string().contains("127.0.0.1"));
209    }
210
211    #[test]
212    fn test_parse_bootstrap_peer() {
213        let addr =
214            "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ";
215        let (peer_id, transport) = parse_bootstrap_peer(addr).unwrap();
216        assert_eq!(
217            peer_id.to_string(),
218            "QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ"
219        );
220        assert!(transport.to_string().contains("104.131.131.82"));
221    }
222}