lumina_node/p2p/
swarm.rs

1use libp2p::identity::Keypair;
2use libp2p::swarm::{NetworkBehaviour, Swarm};
3use std::time::Duration;
4
5use crate::p2p::{P2pError, Result};
6
7pub(crate) use self::imp::new_swarm;
8
9#[cfg(not(target_arch = "wasm32"))]
10mod imp {
11    use std::env;
12    use std::io::Cursor;
13    use std::path::Path;
14
15    use futures::future::Either;
16    use libp2p::core::muxing::StreamMuxerBox;
17    use libp2p::core::upgrade::Version;
18    use libp2p::{dns, noise, quic, swarm, tcp, websocket, yamux, PeerId, Transport};
19    use rustls_pki_types::{CertificateDer, PrivateKeyDer};
20    use tokio::fs;
21
22    use super::*;
23
24    pub(crate) async fn new_swarm<B>(keypair: Keypair, behaviour: B) -> Result<Swarm<B>>
25    where
26        B: NetworkBehaviour,
27    {
28        let tls_key = match env::var("LUMINA_TLS_KEY_FILE") {
29            Ok(path) => Some(read_tls_key(path).await?),
30            Err(_) => None,
31        };
32
33        let tls_certs = match env::var("LUMINA_TLS_CERT_FILE") {
34            Ok(path) => Some(read_tls_certs(path).await?),
35            Err(_) => None,
36        };
37
38        // We do not use system's DNS because libp2p caches system DNS
39        // servers when `Swarm` get constructed, and doesn't update them
40        // later. This can be a problem, if device roams between networks
41        // (and old DNS addresses may not be reachable from the new network).
42        //
43        // Similarly, if node is started when there's no Internet connection,
44        // it won't use the DNS servers offered when Internet connectivity
45        // is restored. Instead we per-define globally-accessible public DNS servers.
46        let dns_config = dns::ResolverConfig::cloudflare();
47
48        let noise_config =
49            noise::Config::new(&keypair).map_err(|e| P2pError::NoiseInit(e.to_string()))?;
50
51        let wss_transport = {
52            let config = if let (Some(key), Some(certs)) = (tls_key, tls_certs) {
53                let key = websocket::tls::PrivateKey::new(key.secret_der().to_vec());
54                let certs = certs
55                    .iter()
56                    .map(|cert| websocket::tls::Certificate::new(cert.to_vec()));
57
58                websocket::tls::Config::new(key, certs)
59                    .map_err(|e| P2pError::TlsInit(format!("server config: {e}")))?
60            } else {
61                websocket::tls::Config::client()
62            };
63
64            let mut wss_transport = websocket::Config::new(dns::tokio::Transport::custom(
65                tcp::tokio::Transport::new(tcp::Config::default()),
66                dns_config.clone(),
67                dns::ResolverOpts::default(),
68            ));
69
70            wss_transport.set_tls_config(config);
71
72            wss_transport
73                .upgrade(Version::V1Lazy)
74                .authenticate(noise_config.clone())
75                .multiplex(yamux::Config::default())
76        };
77
78        let tcp_transport = tcp::tokio::Transport::new(tcp::Config::default())
79            .upgrade(Version::V1Lazy)
80            .authenticate(noise_config)
81            .multiplex(yamux::Config::default());
82
83        let quic_transport = quic::tokio::Transport::new(quic::Config::new(&keypair));
84
85        // WSS must be before TCP transport and must not be wrapped in DNS transport.
86        let transport = wss_transport
87            .or_transport(dns::tokio::Transport::custom(
88                tcp_transport
89                    .or_transport(quic_transport)
90                    .map(|either, _| match either {
91                        Either::Left((peer_id, conn)) => (peer_id, StreamMuxerBox::new(conn)),
92                        Either::Right((peer_id, conn)) => (peer_id, StreamMuxerBox::new(conn)),
93                    }),
94                dns_config,
95                dns::ResolverOpts::default(),
96            ))
97            .map(|either, _| match either {
98                Either::Left((peer_id, conn)) => (peer_id, StreamMuxerBox::new(conn)),
99                Either::Right((peer_id, conn)) => (peer_id, StreamMuxerBox::new(conn)),
100            })
101            .boxed();
102
103        let local_peer_id = PeerId::from_public_key(&keypair.public());
104
105        Ok(Swarm::new(
106            transport,
107            behaviour,
108            local_peer_id,
109            swarm::Config::with_tokio_executor()
110                // TODO: Refactor code to avoid being idle. This can be done by preloading a
111                // handler. This is how they fixed Kademlia:
112                // https://github.com/libp2p/rust-libp2p/pull/4675/files
113                .with_idle_connection_timeout(Duration::from_secs(15)),
114        ))
115    }
116
117    impl From<noise::Error> for P2pError {
118        fn from(e: noise::Error) -> Self {
119            P2pError::NoiseInit(e.to_string())
120        }
121    }
122
123    async fn read_tls_key(path: impl AsRef<Path>) -> Result<PrivateKeyDer<'static>, P2pError> {
124        let path = path.as_ref();
125
126        // TODO: read key in a preallocated memory and zero it after use
127        let data = fs::read(&path)
128            .await
129            .map_err(|e| P2pError::TlsInit(format!("{}: {e}", path.display())))?;
130
131        let mut data = Cursor::new(data);
132
133        rustls_pemfile::private_key(&mut data)
134            .map_err(|e| P2pError::TlsInit(format!("{}: {e}", path.display())))?
135            .ok_or_else(|| P2pError::TlsInit(format!("{}: Key not found in file", path.display())))
136    }
137
138    async fn read_tls_certs(
139        path: impl AsRef<Path>,
140    ) -> Result<Vec<CertificateDer<'static>>, P2pError> {
141        let path = path.as_ref();
142
143        let data = fs::read(path)
144            .await
145            .map_err(|e| P2pError::TlsInit(format!("{}: {e}", path.display())))?;
146
147        let mut data = Cursor::new(data);
148        let certs = rustls_pemfile::certs(&mut data)
149            .collect::<Result<Vec<_>, std::io::Error>>()
150            .map_err(|e| P2pError::TlsInit(format!("{}: {e}", path.display())))?;
151
152        if certs.is_empty() {
153            let e = format!("{}: Certificate not found in file", path.display());
154            Err(P2pError::TlsInit(e))
155        } else {
156            Ok(certs)
157        }
158    }
159}
160
161#[cfg(target_arch = "wasm32")]
162mod imp {
163    use super::*;
164    use libp2p::core::upgrade::Version;
165    use libp2p::{noise, websocket_websys, webtransport_websys, yamux, SwarmBuilder, Transport};
166
167    pub(crate) async fn new_swarm<B>(keypair: Keypair, behaviour: B) -> Result<Swarm<B>>
168    where
169        B: NetworkBehaviour,
170    {
171        let noise_config =
172            noise::Config::new(&keypair).map_err(|e| P2pError::NoiseInit(e.to_string()))?;
173
174        Ok(SwarmBuilder::with_existing_identity(keypair)
175            .with_wasm_bindgen()
176            .with_other_transport(move |_| {
177                Ok(websocket_websys::Transport::default()
178                    .upgrade(Version::V1Lazy)
179                    .authenticate(noise_config)
180                    .multiplex(yamux::Config::default()))
181            })
182            .expect("websocket_websys::Transport is infallible")
183            .with_other_transport(|local_keypair| {
184                let config = webtransport_websys::Config::new(local_keypair);
185                webtransport_websys::Transport::new(config)
186            })
187            .expect("webtransport_websys::Transport is infallible")
188            .with_behaviour(|_| behaviour)
189            .expect("Moving behaviour doesn't fail")
190            .with_swarm_config(|config| {
191                // TODO: Refactor code to avoid being idle. This can be done by preloading a
192                // handler. This is how they fixed Kademlia:
193                // https://github.com/libp2p/rust-libp2p/pull/4675/files
194                config.with_idle_connection_timeout(Duration::from_secs(15))
195            })
196            .build())
197    }
198}