Skip to main content

mina_p2p/connection/outgoing/
mod.rs

1mod p2p_connection_outgoing_state;
2pub use p2p_connection_outgoing_state::*;
3
4mod p2p_connection_outgoing_actions;
5pub use p2p_connection_outgoing_actions::*;
6
7mod p2p_connection_outgoing_reducer;
8
9#[cfg(feature = "p2p-libp2p")]
10use std::net::SocketAddr;
11use std::{fmt, net::IpAddr, str::FromStr};
12
13use binprot_derive::{BinProtRead, BinProtWrite};
14use multiaddr::{Multiaddr, Protocol};
15use serde::{Deserialize, Serialize};
16use thiserror::Error;
17
18#[cfg(feature = "p2p-libp2p")]
19use mina_p2p_messages::v2;
20
21use crate::{
22    webrtc::{self, Host, HttpSignalingInfo},
23    PeerId,
24};
25
26#[cfg(feature = "p2p-libp2p")]
27use crate::webrtc::SignalingMethod;
28
29// TODO(binier): maybe move to `crate::webrtc` module
30#[derive(
31    BinProtWrite, BinProtRead, derive_more::From, Debug, Ord, PartialOrd, Eq, PartialEq, Clone,
32)]
33pub enum P2pConnectionOutgoingInitOpts {
34    WebRTC {
35        peer_id: PeerId,
36        signaling: webrtc::SignalingMethod,
37    },
38    LibP2P(P2pConnectionOutgoingInitLibp2pOpts),
39}
40
41impl P2pConnectionOutgoingInitOpts {
42    pub fn with_host_resolved(self) -> Option<Self> {
43        if let Self::LibP2P(libp2p_opts) = self {
44            Some(Self::LibP2P(libp2p_opts.with_host_resolved()?))
45        } else {
46            Some(self)
47        }
48    }
49}
50
51#[derive(BinProtWrite, BinProtRead, Eq, PartialEq, Ord, PartialOrd, Debug, Clone)]
52pub struct P2pConnectionOutgoingInitLibp2pOpts {
53    pub peer_id: PeerId,
54    pub host: Host,
55    pub port: u16,
56}
57
58impl P2pConnectionOutgoingInitLibp2pOpts {
59    /// If the current host is local and there is a better host among the `addrs`,
60    /// replace the current one with the better one.
61    pub fn update_host_if_needed<'a>(&mut self, mut addrs: impl Iterator<Item = &'a Multiaddr>) {
62        fn is_local(ip: impl Into<IpAddr>) -> bool {
63            match ip.into() {
64                IpAddr::V4(ip) => ip.is_loopback() || ip.is_private(),
65                IpAddr::V6(ip) => ip.is_loopback(),
66            }
67        }
68
69        // if current dial opts is not good enough
70        let update = match &self.host {
71            Host::Domain(_) => false,
72            Host::Ipv4(ip) => is_local(*ip),
73            Host::Ipv6(ip) => is_local(*ip),
74        };
75        if update {
76            // if new options is better
77            let new = addrs.find_map(|x| {
78                x.iter().find_map(|x| match x {
79                    Protocol::Dns4(hostname) | Protocol::Dns6(hostname) => {
80                        Some(Host::Domain(hostname.into_owned()))
81                    }
82                    Protocol::Ip4(ip) if !is_local(ip) => Some(Host::Ipv4(ip)),
83                    Protocol::Ip6(ip) if !is_local(ip) => Some(Host::Ipv6(ip)),
84                    _ => None,
85                })
86            });
87            if let Some(new) = new {
88                self.host = new;
89            }
90        }
91    }
92
93    pub fn with_host_resolved(mut self) -> Option<Self> {
94        self.host = self.host.resolve()?;
95        Some(self)
96    }
97}
98
99pub(crate) mod libp2p_opts {
100    use std::net::{IpAddr, SocketAddr};
101
102    use multiaddr::Multiaddr;
103
104    use crate::{webrtc::Host, PeerId};
105
106    impl super::P2pConnectionOutgoingInitLibp2pOpts {
107        fn to_peer_id_multiaddr(&self) -> (PeerId, Multiaddr) {
108            (
109                self.peer_id,
110                Multiaddr::from_iter([(&self.host).into(), multiaddr::Protocol::Tcp(self.port)]),
111            )
112        }
113        fn into_peer_id_multiaddr(self) -> (PeerId, Multiaddr) {
114            (
115                self.peer_id,
116                Multiaddr::from_iter([(&self.host).into(), multiaddr::Protocol::Tcp(self.port)]),
117            )
118        }
119
120        pub fn matches_socket_addr(&self, addr: SocketAddr) -> bool {
121            self.port == addr.port() && self.matches_socket_ip(addr)
122        }
123
124        pub fn matches_socket_ip(&self, addr: SocketAddr) -> bool {
125            match (&self.host, addr) {
126                (Host::Ipv4(ip), SocketAddr::V4(addr)) => ip == addr.ip(),
127                (Host::Ipv6(ip), SocketAddr::V6(addr)) => ip == addr.ip(),
128                _ => false,
129            }
130        }
131    }
132
133    impl From<&super::P2pConnectionOutgoingInitLibp2pOpts> for (PeerId, Multiaddr) {
134        fn from(value: &super::P2pConnectionOutgoingInitLibp2pOpts) -> Self {
135            value.to_peer_id_multiaddr()
136        }
137    }
138
139    impl From<super::P2pConnectionOutgoingInitLibp2pOpts> for (PeerId, Multiaddr) {
140        fn from(value: super::P2pConnectionOutgoingInitLibp2pOpts) -> Self {
141            value.into_peer_id_multiaddr()
142        }
143    }
144
145    impl From<(PeerId, SocketAddr)> for super::P2pConnectionOutgoingInitLibp2pOpts {
146        fn from((peer_id, addr): (PeerId, SocketAddr)) -> Self {
147            let (host, port) = match addr {
148                SocketAddr::V4(v4) => (Host::Ipv4(*v4.ip()), v4.port()),
149                SocketAddr::V6(v6) => (Host::Ipv6(*v6.ip()), v6.port()),
150            };
151            super::P2pConnectionOutgoingInitLibp2pOpts {
152                peer_id,
153                host,
154                port,
155            }
156        }
157    }
158
159    #[derive(Debug, thiserror::Error)]
160    pub enum P2pConnectionOutgoingInitLibp2pOptsTryToSocketAddrError {
161        #[error("name unresolved: {0}")]
162        Unresolved(String),
163    }
164
165    impl TryFrom<&super::P2pConnectionOutgoingInitLibp2pOpts> for SocketAddr {
166        type Error = P2pConnectionOutgoingInitLibp2pOptsTryToSocketAddrError;
167
168        fn try_from(
169            value: &super::P2pConnectionOutgoingInitLibp2pOpts,
170        ) -> Result<Self, Self::Error> {
171            match &value.host {
172                Host::Domain(name) => Err(
173                    P2pConnectionOutgoingInitLibp2pOptsTryToSocketAddrError::Unresolved(
174                        name.clone(),
175                    ),
176                ),
177                Host::Ipv4(ip) => Ok(SocketAddr::new(IpAddr::V4(*ip), value.port)),
178                Host::Ipv6(ip) => Ok(SocketAddr::new(IpAddr::V6(*ip), value.port)),
179            }
180        }
181    }
182}
183
184impl P2pConnectionOutgoingInitOpts {
185    pub fn is_libp2p(&self) -> bool {
186        matches!(self, Self::LibP2P(_))
187    }
188
189    pub fn peer_id(&self) -> &PeerId {
190        match self {
191            Self::WebRTC { peer_id, .. } => peer_id,
192            Self::LibP2P(v) => &v.peer_id,
193        }
194    }
195
196    pub fn kind(&self) -> &'static str {
197        match self {
198            Self::WebRTC { .. } => "webrtc",
199
200            Self::LibP2P(_) => "libp2p",
201        }
202    }
203
204    pub fn can_connect_directly(&self) -> bool {
205        match self {
206            Self::LibP2P(..) => true,
207            Self::WebRTC { signaling, .. } => signaling.can_connect_directly(),
208        }
209    }
210
211    pub fn webrtc_p2p_relay_peer_id(&self) -> Option<PeerId> {
212        match self {
213            Self::WebRTC { signaling, .. } => signaling.p2p_relay_peer_id(),
214            _ => None,
215        }
216    }
217
218    /// The OCaml implementation of Mina uses the `get_some_initial_peers` RPC to exchange peer information.
219    /// Try to convert this RPC response into our peer address representation.
220    /// Recognize a hack for marking the webrtc signaling server.
221    /// Prefixes "http://" or "https://" are schemas that indicates the host is webrtc signaling.
222    #[cfg(feature = "p2p-libp2p")]
223    pub fn try_from_mina_rpc(msg: v2::NetworkPeerPeerStableV1) -> Option<Self> {
224        let peer_id_str = String::try_from(&msg.peer_id.0).ok()?;
225        let peer_id = peer_id_str.parse::<libp2p_identity::PeerId>().ok()?;
226        if peer_id.as_ref().code() == 0x12 {
227            // the peer_id is not supported
228            return None;
229        }
230
231        let host = String::try_from(&msg.host).ok()?;
232
233        let opts = if host.contains(':') {
234            let mut it = host.split(':');
235            let schema = it.next()?;
236            let host = it.next()?.trim_start_matches('/');
237            let signaling = match schema {
238                "http" => SignalingMethod::Http(HttpSignalingInfo {
239                    host: host.parse().ok()?,
240                    port: msg.libp2p_port.as_u64() as u16,
241                }),
242                "https" => SignalingMethod::Https(HttpSignalingInfo {
243                    host: host.parse().ok()?,
244                    port: msg.libp2p_port.as_u64() as u16,
245                }),
246                _ => return None,
247            };
248            Self::WebRTC {
249                peer_id: peer_id.try_into().ok()?,
250                signaling,
251            }
252        } else {
253            let opts = P2pConnectionOutgoingInitLibp2pOpts {
254                peer_id: peer_id.try_into().ok()?,
255                host: host.parse().ok()?,
256                port: msg.libp2p_port.as_u64() as u16,
257            };
258            Self::LibP2P(opts)
259        };
260        Some(opts)
261    }
262
263    /// Try to convert our peer address representation into mina RPC response.
264    /// Use a hack to mark the webrtc signaling server. Add "http://" or "https://" schema to the host address.
265    /// The OCaml node will recognize this address as incorrect and ignore it.
266    #[cfg(feature = "p2p-libp2p")]
267    pub fn try_into_mina_rpc(&self) -> Option<v2::NetworkPeerPeerStableV1> {
268        match self {
269            P2pConnectionOutgoingInitOpts::LibP2P(opts) => Some(v2::NetworkPeerPeerStableV1 {
270                host: opts.host.to_string().as_bytes().into(),
271                libp2p_port: (opts.port as u64).into(),
272                peer_id: v2::NetworkPeerPeerIdStableV1(
273                    libp2p_identity::PeerId::try_from(opts.peer_id)
274                        .ok()?
275                        .to_string()
276                        .into_bytes()
277                        .into(),
278                ),
279            }),
280            P2pConnectionOutgoingInitOpts::WebRTC { peer_id, signaling } => match signaling {
281                SignalingMethod::Http(info) => Some(v2::NetworkPeerPeerStableV1 {
282                    host: format!("http://{}", info.host).as_bytes().into(),
283                    libp2p_port: (info.port as u64).into(),
284                    peer_id: v2::NetworkPeerPeerIdStableV1(
285                        (*peer_id).to_string().into_bytes().into(),
286                    ),
287                }),
288                SignalingMethod::Https(info) => Some(v2::NetworkPeerPeerStableV1 {
289                    host: format!("https://{}", info.host).as_bytes().into(),
290                    libp2p_port: (info.port as u64).into(),
291                    peer_id: v2::NetworkPeerPeerIdStableV1(
292                        (*peer_id).to_string().into_bytes().into(),
293                    ),
294                }),
295                SignalingMethod::HttpsProxy(cluster_id, info) => {
296                    Some(v2::NetworkPeerPeerStableV1 {
297                        host: format!("https://{}/clusters/{cluster_id}", info.host)
298                            .as_bytes()
299                            .into(),
300                        libp2p_port: (info.port as u64).into(),
301                        peer_id: v2::NetworkPeerPeerIdStableV1(
302                            (*peer_id).to_string().into_bytes().into(),
303                        ),
304                    })
305                }
306                SignalingMethod::Proxied(scheme, path_prefix, info) => {
307                    Some(v2::NetworkPeerPeerStableV1 {
308                        host: format!("{scheme}://{}{}", info.host, path_prefix)
309                            .as_bytes()
310                            .into(),
311                        libp2p_port: (info.port as u64).into(),
312                        peer_id: v2::NetworkPeerPeerIdStableV1(
313                            (*peer_id).to_string().into_bytes().into(),
314                        ),
315                    })
316                }
317                SignalingMethod::P2p { .. } => None,
318            },
319        }
320    }
321
322    #[cfg(feature = "p2p-libp2p")]
323    pub fn from_libp2p_socket_addr(peer_id: PeerId, addr: SocketAddr) -> Self {
324        P2pConnectionOutgoingInitOpts::LibP2P((peer_id, addr).into())
325    }
326
327    fn parse_p2p_relay_webrtc_multiaddr(
328        maddr: &multiaddr::Multiaddr,
329    ) -> Result<Self, P2pConnectionOutgoingInitOptsParseError> {
330        let mut iter = maddr.iter();
331
332        let Some(Protocol::P2p(relay_peer_id_hash)) = iter.next() else {
333            return Err(P2pConnectionOutgoingInitOptsParseError::Other(
334                "expected p2p protocol for relay".to_string(),
335            ));
336        };
337        let relay_peer_id = libp2p_identity::PeerId::from_multihash(relay_peer_id_hash.into())
338            .map_err(|_| {
339                P2pConnectionOutgoingInitOptsParseError::Other(
340                    "invalid relay peer_id multihash".to_string(),
341                )
342            })?
343            .try_into()
344            .map_err(|_| {
345                P2pConnectionOutgoingInitOptsParseError::Other(
346                    "unexpected error converting relay PeerId".to_string(),
347                )
348            })?;
349
350        // Expect /webrtc
351        if iter.next() != Some(Protocol::WebRTC) {
352            return Err(P2pConnectionOutgoingInitOptsParseError::Other(
353                "expected webrtc protocol".to_string(),
354            ));
355        };
356
357        // Expect /p2p-circuit
358        if iter.next() != Some(Protocol::P2pCircuit) {
359            return Err(P2pConnectionOutgoingInitOptsParseError::Other(
360                "expected p2p-circuit protocol".to_string(),
361            ));
362        };
363
364        // Get target peer_id
365        let peer_id = Self::parse_p2p_peer_id(iter.next(), "target")?;
366
367        Ok(Self::WebRTC {
368            peer_id,
369            signaling: webrtc::SignalingMethod::P2p { relay_peer_id },
370        })
371    }
372
373    fn parse_p2p_peer_id(
374        protocol: Option<multiaddr::Protocol>,
375        label: &'static str,
376    ) -> Result<PeerId, P2pConnectionOutgoingInitOptsParseError> {
377        match protocol {
378            Some(Protocol::P2p(peer_id_hash)) => {
379                libp2p_identity::PeerId::from_multihash(peer_id_hash.into())
380                    .map_err(|_| {
381                        P2pConnectionOutgoingInitOptsParseError::Other(format!(
382                            "invalid {label} peer_id multihash"
383                        ))
384                    })?
385                    .try_into()
386                    .map_err(|_| {
387                        P2pConnectionOutgoingInitOptsParseError::Other(format!(
388                            "unexpected error converting {label} PeerId"
389                        ))
390                    })
391            }
392            Some(other_protocol) => Err(P2pConnectionOutgoingInitOptsParseError::Other(format!(
393                "expected p2p protocol for {label} peer id, got {other_protocol:?}"
394            ))),
395            None => Err(P2pConnectionOutgoingInitOptsParseError::Other(format!(
396                "missing {label} peer id"
397            ))),
398        }
399    }
400}
401
402impl P2pConnectionOutgoingInitLibp2pOpts {
403    pub fn to_maddr(&self) -> Option<multiaddr::Multiaddr> {
404        self.clone().try_into().ok()
405    }
406}
407
408impl fmt::Display for P2pConnectionOutgoingInitOpts {
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        let maddr: Multiaddr = self.into();
411        write!(f, "{}", maddr)
412    }
413}
414
415#[derive(Error, Serialize, Deserialize, Debug, Clone)]
416pub enum P2pConnectionOutgoingInitOptsParseError {
417    #[error("not enough args for the signaling method")]
418    NotEnoughArgs,
419    #[error("peer id parse error: {0}")]
420    PeerIdParseError(String),
421    #[error("signaling method parse error: `{0}`")]
422    SignalingMethodParseError(webrtc::SignalingMethodParseError),
423    #[error("other error: {0}")]
424    Other(String),
425}
426
427impl FromStr for P2pConnectionOutgoingInitOpts {
428    type Err = P2pConnectionOutgoingInitOptsParseError;
429
430    fn from_str(s: &str) -> Result<Self, Self::Err> {
431        if s.is_empty() {
432            return Err(P2pConnectionOutgoingInitOptsParseError::NotEnoughArgs);
433        }
434
435        // Try parsing as multiaddr first (the preferred format)
436        if let Ok(maddr) = Multiaddr::from_str(s) {
437            return Self::try_from(&maddr);
438        }
439
440        // Fallback: try legacy WebRTC format (/{peer_id}/{signaling_method}/...)
441        // This format is deprecated; prefer multiaddr format instead.
442        let id_end_index = s[1..]
443            .find('/')
444            .map(|i| i + 1)
445            .filter(|i| s.len() > *i)
446            .ok_or(P2pConnectionOutgoingInitOptsParseError::NotEnoughArgs)?;
447
448        let opts = Self::WebRTC {
449            peer_id: s[1..id_end_index].parse::<PeerId>().map_err(|err| {
450                P2pConnectionOutgoingInitOptsParseError::PeerIdParseError(err.to_string())
451            })?,
452            signaling: s[id_end_index..]
453                .parse::<webrtc::SignalingMethod>()
454                .map_err(|err| {
455                    P2pConnectionOutgoingInitOptsParseError::SignalingMethodParseError(err)
456                })?,
457        };
458
459        // Emit deprecation warning with the suggested multiaddr format
460        let suggested_maddr: Multiaddr = (&opts).into();
461        tracing::warn!(
462            message = "Deprecated address format detected. Please use multiaddr format instead.",
463            legacy_format = %s,
464            suggested_format = %suggested_maddr,
465        );
466
467        Ok(opts)
468    }
469}
470
471impl Serialize for P2pConnectionOutgoingInitOpts {
472    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
473    where
474        S: serde::Serializer,
475    {
476        serializer.serialize_str(&self.to_string())
477    }
478}
479
480impl<'de> Deserialize<'de> for P2pConnectionOutgoingInitOpts {
481    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
482    where
483        D: serde::Deserializer<'de>,
484    {
485        let s: String = Deserialize::deserialize(deserializer)?;
486        s.parse().map_err(serde::de::Error::custom)
487    }
488}
489
490impl TryFrom<P2pConnectionOutgoingInitLibp2pOpts> for multiaddr::Multiaddr {
491    type Error = libp2p_identity::DecodingError;
492
493    fn try_from(value: P2pConnectionOutgoingInitLibp2pOpts) -> Result<Self, Self::Error> {
494        use multiaddr::Protocol;
495
496        Ok(Self::empty()
497            .with(match &value.host {
498                // maybe should be just `Dns`?
499                Host::Domain(v) => Protocol::Dns4(v.into()),
500                Host::Ipv4(v) => Protocol::Ip4(*v),
501                Host::Ipv6(v) => Protocol::Ip6(*v),
502            })
503            .with(Protocol::Tcp(value.port))
504            .with(Protocol::P2p(libp2p_identity::PeerId::try_from(
505                value.peer_id,
506            )?)))
507    }
508}
509
510impl TryFrom<&multiaddr::Multiaddr> for P2pConnectionOutgoingInitOpts {
511    type Error = P2pConnectionOutgoingInitOptsParseError;
512
513    /// Parses a multiaddr into connection options.
514    ///
515    /// Supports both WebRTC and LibP2P multiaddr formats:
516    ///
517    /// **WebRTC formats** (contain `/webrtc` protocol):
518    /// - HTTP(S): `/<dns|dns4|dns6|ip4|ip6>/{host}/tcp/{port}/webrtc/<http|https>/p2p/{peer_id}`
519    /// - Proxied: `/<dns|dns4|dns6|ip4|ip6>/{host}/tcp/{port}/webrtc/<http|https>/http-path/{path}/p2p/{peer_id}`
520    /// - P2P Relay: `/p2p/{relay_peer_id}/webrtc/p2p-circuit/p2p/{target_peer_id}`
521    ///
522    /// **LibP2P format**:
523    /// - `/<dns|dns4|dns6|ip4|ip6>/{host}/tcp/{port}/p2p/{peer_id}`
524    fn try_from(maddr: &multiaddr::Multiaddr) -> Result<Self, Self::Error> {
525        // Check if this is a WebRTC multiaddr
526        let is_webrtc = maddr.iter().any(|p| p == Protocol::WebRTC);
527
528        // Standard libp2p multiaddr (no /webrtc)
529        if !is_webrtc {
530            cfg_if::cfg_if! {
531                if #[cfg(target_arch = "wasm32")] {
532                    return Err(P2pConnectionOutgoingInitOptsParseError::Other(
533                        "libp2p not supported in wasm".to_owned(),
534                    ))
535                } else {
536                    return Ok(Self::LibP2P(maddr.try_into()?))
537                }
538            };
539        }
540
541        let mut iter = maddr.iter();
542
543        // Check for P2P relay format: /p2p/{relay}/webrtc/p2p-circuit/p2p/{target}
544        // and go to parse_p2p_relay_webrtc_multiaddr.
545        // Otherwise, parse one of the HTTP-based variants:
546        // /dns|dns4|dns6|ip4|ip6/{host}/tcp/{port}/webrtc/http|https/[http-proxy/{proxy_path}/]p2p/{peer_id}
547        match iter.next() {
548            Some(Protocol::P2p(_)) => Self::parse_p2p_relay_webrtc_multiaddr(maddr),
549            other_transport_protocol => {
550                // Extract /dns|dns4|dns6|ip4|ip6/{host}
551                let host = match other_transport_protocol {
552                    Some(Protocol::Ip4(v)) => Host::Ipv4(v),
553                    Some(Protocol::Ip6(v)) => Host::Ipv6(v),
554                    Some(Protocol::Dns(v) | Protocol::Dns4(v) | Protocol::Dns6(v)) => {
555                        Host::Domain(v.to_string())
556                    }
557                    _ => {
558                        return Err(P2pConnectionOutgoingInitOptsParseError::Other(
559                            "expected host (dns/dns4/dns6/ip4/ip6) in webrtc multiaddr".to_string(),
560                        ))
561                    }
562                };
563
564                // Extract /tcp/{port}
565                let Some(Protocol::Tcp(port)) = iter.next() else {
566                    return Err(P2pConnectionOutgoingInitOptsParseError::Other(
567                        "expected tcp port in webrtc multiaddr".to_string(),
568                    ));
569                };
570
571                // Skip /webrtc
572                if iter.next() != Some(Protocol::WebRTC) {
573                    return Err(P2pConnectionOutgoingInitOptsParseError::Other(
574                        "expected webrtc protocol".to_string(),
575                    ));
576                };
577
578                // Determine signaling method: http, https, or proxy with http-path
579                let signaling_info = HttpSignalingInfo { host, port };
580                let scheme = match iter.next() {
581                    Some(Protocol::Http) => webrtc::ProxyScheme::Http,
582                    Some(Protocol::Https) => webrtc::ProxyScheme::Https,
583                    _ => {
584                        return Err(P2pConnectionOutgoingInitOptsParseError::Other(
585                            "expected http or https protocol after webrtc".to_string(),
586                        ))
587                    }
588                };
589                let (signaling, peer_id) = match iter.next() {
590                    Some(Protocol::HttpPath(path)) => {
591                        let signaling = webrtc::SignalingMethod::Proxied(
592                            scheme,
593                            path.into(),
594                            signaling_info,
595                        );
596                        let peer_id = Self::parse_p2p_peer_id(iter.next(), "webrtc")?;
597                        (signaling, peer_id)
598                    }
599                    p2p @ Some(Protocol::P2p(_)) => {
600                        let signaling = match scheme {
601                            webrtc::ProxyScheme::Http => {
602                                webrtc::SignalingMethod::Http(signaling_info)
603                            }
604                            webrtc::ProxyScheme::Https => {
605                                webrtc::SignalingMethod::Https(signaling_info)
606                            }
607                        };
608                        let peer_id = Self::parse_p2p_peer_id(p2p, "webrtc")?;
609                        (signaling, peer_id)
610                    }
611                    Some(other_protocol) => {
612                        return Err(P2pConnectionOutgoingInitOptsParseError::Other(
613                            format!("expected /p2p/peer_id or /http-path/encoded_path, got {other_protocol:?}"
614                        )))
615                    },
616                    None => {
617                        return Err(P2pConnectionOutgoingInitOptsParseError::Other(
618                            "expected p2p protocol with peer_id".to_string(),
619                        ))
620                    }
621                };
622
623                Ok(Self::WebRTC { peer_id, signaling })
624            }
625        }
626    }
627}
628
629impl TryFrom<multiaddr::Multiaddr> for P2pConnectionOutgoingInitOpts {
630    type Error = P2pConnectionOutgoingInitOptsParseError;
631
632    fn try_from(value: multiaddr::Multiaddr) -> Result<Self, Self::Error> {
633        (&value).try_into()
634    }
635}
636
637impl From<&P2pConnectionOutgoingInitOpts> for Multiaddr {
638    /// Converts connection options to a multiaddr.
639    ///
640    /// **WebRTC formats** :
641    /// - HTTP(S): `/<dns|dns4|dns6|ip4|ip6>/{host}/tcp/{port}/webrtc/<http|https>/p2p/{peer_id}`
642    /// - Proxy: `/<dns|dns4|dns6|ip4|ip6>/{host}/tcp/{port}/webrtc/<http|https>/http-path/{url_encoded_prefix}/p2p/{peer_id}`
643    /// - P2P Relay: `/p2p/{relay_peer_id}/webrtc/p2p-circuit/p2p/{target_peer_id}`
644    ///
645    /// **LibP2P format**:
646    /// - `/<dns|dns4|dns6|ip4|ip6>/{host}/tcp/{port}/p2p/{peer_id}`
647    fn from(opts: &P2pConnectionOutgoingInitOpts) -> Self {
648        match opts {
649            P2pConnectionOutgoingInitOpts::WebRTC { peer_id, signaling } => {
650                use webrtc::SignalingMethod;
651
652                // expect() safety: by the time we have a P2pConnectionOutgoingInitOpts
653                // peer_id was already validated. This is a quirk of the libp2p_identity vs mina-p2p types
654                // validation logics in:
655                // 1. P2pConnectionOutgoingInitLibp2pOpts::try_from_mina_rpc()
656                // 2. impl TryFrom<&multiaddr::Multiaddr> for P2pConnectionOutgoingInitLibp2pOpts
657                // P2pConnectionOutgoingInitOpts will never come in over the wire or from a rogue peer,
658                // possibly only bad CLI flags.
659                let peer_id_proto = Protocol::P2p(
660                    libp2p_identity::PeerId::try_from(*peer_id).expect("valid peer_id"),
661                );
662
663                match signaling {
664                    SignalingMethod::Http(info) => {
665                        let host_proto = match &info.host {
666                            Host::Domain(v) => Protocol::Dns4(v.into()),
667                            Host::Ipv4(v) => Protocol::Ip4(*v),
668                            Host::Ipv6(v) => Protocol::Ip6(*v),
669                        };
670                        Multiaddr::empty()
671                            .with(host_proto)
672                            .with(Protocol::Tcp(info.port))
673                            .with(Protocol::WebRTC)
674                            .with(Protocol::Http)
675                            .with(peer_id_proto)
676                    }
677                    SignalingMethod::Https(info) => {
678                        let host_proto = match &info.host {
679                            Host::Domain(v) => Protocol::Dns4(v.into()),
680                            Host::Ipv4(v) => Protocol::Ip4(*v),
681                            Host::Ipv6(v) => Protocol::Ip6(*v),
682                        };
683                        Multiaddr::empty()
684                            .with(host_proto)
685                            .with(Protocol::Tcp(info.port))
686                            .with(Protocol::WebRTC)
687                            .with(Protocol::Https)
688                            .with(peer_id_proto)
689                    }
690                    SignalingMethod::HttpsProxy(cluster_id, info) => {
691                        // Convert legacy cluster_id to path format: /clusters/{id}
692                        let host_proto = match &info.host {
693                            Host::Domain(v) => Protocol::Dns4(v.into()),
694                            Host::Ipv4(v) => Protocol::Ip4(*v),
695                            Host::Ipv6(v) => Protocol::Ip6(*v),
696                        };
697                        let path = format!("clusters/{}", cluster_id);
698                        Multiaddr::empty()
699                            .with(host_proto)
700                            .with(Protocol::Tcp(info.port))
701                            .with(Protocol::WebRTC)
702                            .with(Protocol::Https)
703                            .with(Protocol::HttpPath(path.into()))
704                            .with(peer_id_proto)
705                    }
706                    SignalingMethod::Proxied(scheme, path, info) => {
707                        let host_proto = match &info.host {
708                            Host::Domain(v) => Protocol::Dns4(v.into()),
709                            Host::Ipv4(v) => Protocol::Ip4(*v),
710                            Host::Ipv6(v) => Protocol::Ip6(*v),
711                        };
712                        let scheme_proto = match scheme {
713                            webrtc::ProxyScheme::Http => Protocol::Http,
714                            webrtc::ProxyScheme::Https => Protocol::Https,
715                        };
716                        Multiaddr::empty()
717                            .with(host_proto)
718                            .with(Protocol::Tcp(info.port))
719                            .with(Protocol::WebRTC)
720                            .with(scheme_proto)
721                            .with(Protocol::HttpPath(path.into()))
722                            .with(peer_id_proto)
723                    }
724                    SignalingMethod::P2p { relay_peer_id } => {
725                        // same expect() safety as peer_id_proto
726                        let relay_id_proto = Protocol::P2p(
727                            libp2p_identity::PeerId::try_from(*relay_peer_id)
728                                .expect("valid relay_peer_id"),
729                        );
730                        Multiaddr::empty()
731                            .with(relay_id_proto)
732                            .with(Protocol::WebRTC)
733                            .with(Protocol::P2pCircuit)
734                            .with(peer_id_proto)
735                    }
736                }
737            }
738            // same expect() safety rationale as the others. it's all from peer_id >__>
739            P2pConnectionOutgoingInitOpts::LibP2P(v) => v.to_maddr().expect("valid libp2p opts"),
740        }
741    }
742}
743
744impl From<P2pConnectionOutgoingInitOpts> for Multiaddr {
745    fn from(opts: P2pConnectionOutgoingInitOpts) -> Self {
746        (&opts).into()
747    }
748}
749
750impl TryFrom<&multiaddr::Multiaddr> for P2pConnectionOutgoingInitLibp2pOpts {
751    type Error = P2pConnectionOutgoingInitOptsParseError;
752
753    fn try_from(maddr: &multiaddr::Multiaddr) -> Result<Self, Self::Error> {
754        use multiaddr::Protocol;
755
756        let mut iter = maddr.iter();
757        Ok(P2pConnectionOutgoingInitLibp2pOpts {
758            host: match iter.next() {
759                Some(Protocol::Ip4(v)) => Host::Ipv4(v),
760                Some(Protocol::Dns(v) | Protocol::Dns4(v) | Protocol::Dns6(v)) => {
761                    Host::Domain(v.to_string())
762                }
763                Some(other_host) => {
764                    return Err(P2pConnectionOutgoingInitOptsParseError::Other(
765                        format!("unexpected transport in multiaddr! expected /dns|dns4|dns6|ip4|ip6/<host>, got {other_host:?}!")
766                    ));
767                }
768                None => {
769                    return Err(P2pConnectionOutgoingInitOptsParseError::Other(
770                        "missing /dns|dns4|dns6|ip4|ip6/host from multiaddr".to_string(),
771                    ));
772                }
773            },
774            port: match iter.next() {
775                Some(Protocol::Tcp(port)) => port,
776                Some(other_port) => {
777                    return Err(P2pConnectionOutgoingInitOptsParseError::Other(format!(
778                        "unexpected part in multiaddr! expected /tcp/<port>, got {other_port:?}"
779                    )));
780                }
781                None => {
782                    return Err(P2pConnectionOutgoingInitOptsParseError::Other(
783                        "missing port part from multiaddr".to_string(),
784                    ));
785                }
786            },
787            peer_id: match iter.next() {
788                Some(Protocol::P2p(hash)) => libp2p_identity::PeerId::from_multihash(hash.into())
789                    .map_err(|_| {
790                        P2pConnectionOutgoingInitOptsParseError::Other(
791                            "invalid peer_id multihash".to_string(),
792                        )
793                    })?
794                    .try_into()
795                    .map_err(|_| {
796                        P2pConnectionOutgoingInitOptsParseError::Other(
797                            "unexpected error converting PeerId".to_string(),
798                        )
799                    })?,
800                Some(_) => {
801                    return Err(P2pConnectionOutgoingInitOptsParseError::Other(
802                        "unexpected part in multiaddr! expected peer_id".to_string(),
803                    ));
804                }
805                None => {
806                    return Err(P2pConnectionOutgoingInitOptsParseError::Other(
807                        "peer_id not set in multiaddr. Missing `../p2p/<peer_id>`".to_string(),
808                    ));
809                }
810            },
811        })
812    }
813}
814
815mod measurement {
816    use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
817
818    use super::P2pConnectionOutgoingInitOpts;
819
820    // `Host` may contain `String` which allocates
821    // but hostname usually small, compared to `String` container size 24 bytes
822    impl MallocSizeOf for P2pConnectionOutgoingInitOpts {
823        fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize {
824            0
825        }
826    }
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832    use std::net::Ipv4Addr;
833
834    // Use libp2p PeerId format for test multiaddrs
835    const TEST_PEER_ID_LIBP2P: &str = "12D3KooWEiGVAFC7curXWXiGZyMWnZK9h8BKr88U8D5PKV3dXciv";
836    const TEST_RELAY_PEER_ID_LIBP2P: &str = "12D3KooWAdgYL6hv18M3iDBdaK1dRygPivSfAfBNDzie6YqydVbs";
837
838    #[test]
839    fn test_parse_webrtc_http_signaling_domain() {
840        let maddr_str = format!(
841            "/dns4/signal.example.com/tcp/8080/webrtc/http/p2p/{}",
842            TEST_PEER_ID_LIBP2P
843        );
844        let opts: P2pConnectionOutgoingInitOpts = maddr_str.parse().unwrap();
845
846        match opts {
847            P2pConnectionOutgoingInitOpts::WebRTC { signaling, .. } => match signaling {
848                webrtc::SignalingMethod::Http(info) => {
849                    assert_eq!(info.host, Host::Domain("signal.example.com".to_string()));
850                    assert_eq!(info.port, 8080);
851                }
852                x => panic!("Expected Http signaling method, got {x:?}"),
853            },
854            x => panic!("Expected WebRTC variant, got {x:?}"),
855        }
856    }
857
858    #[test]
859    fn test_parse_webrtc_http_signaling_ipv4() {
860        let maddr_str = format!(
861            "/ip4/192.168.1.100/tcp/8080/webrtc/http/p2p/{}",
862            TEST_PEER_ID_LIBP2P
863        );
864        let opts: P2pConnectionOutgoingInitOpts = maddr_str.parse().unwrap();
865
866        match opts {
867            P2pConnectionOutgoingInitOpts::WebRTC { signaling, .. } => match signaling {
868                webrtc::SignalingMethod::Http(info) => {
869                    assert_eq!(info.host, Host::Ipv4(Ipv4Addr::new(192, 168, 1, 100)));
870                    assert_eq!(info.port, 8080);
871                }
872                x => panic!("Expected Http signaling method, got {x:?}"),
873            },
874            x => panic!("Expected WebRTC variant, got {x:?}"),
875        }
876    }
877
878    #[test]
879    fn test_parse_webrtc_https_signaling() {
880        let maddr_str = format!(
881            "/dns4/signal.example.com/tcp/443/webrtc/https/p2p/{}",
882            TEST_PEER_ID_LIBP2P
883        );
884        let opts: P2pConnectionOutgoingInitOpts = maddr_str.parse().unwrap();
885
886        match opts {
887            P2pConnectionOutgoingInitOpts::WebRTC { signaling, .. } => match signaling {
888                webrtc::SignalingMethod::Https(info) => {
889                    assert_eq!(info.host, Host::Domain("signal.example.com".to_string()));
890                    assert_eq!(info.port, 443);
891                }
892                x => panic!("Expected Https signaling method, got {x:?}"),
893            },
894            x => panic!("Expected WebRTC variant, got {x:?}"),
895        }
896    }
897
898    #[test]
899    fn test_parse_webrtc_https_proxy_with_http_path() {
900        let maddr_str = format!(
901            "/dns4/proxy.example.com/tcp/443/webrtc/https/http-path/cluster%2F123%2Fmina%2Fwebrtc%2Fsignal/p2p/{}",
902            TEST_PEER_ID_LIBP2P
903        );
904        let opts: P2pConnectionOutgoingInitOpts = maddr_str.parse().unwrap();
905
906        match opts {
907            P2pConnectionOutgoingInitOpts::WebRTC { signaling, .. } => match signaling {
908                webrtc::SignalingMethod::Proxied(scheme, path, info) => {
909                    assert_eq!(scheme, webrtc::ProxyScheme::Https);
910                    assert_eq!(path.as_ref(), "cluster/123/mina/webrtc/signal");
911                    assert_eq!(info.host, Host::Domain("proxy.example.com".to_string()));
912                    assert_eq!(info.port, 443);
913                }
914                x => panic!("Expected Proxied signaling method, got {x:?}"),
915            },
916            x => panic!("Expected WebRTC variant, got {x:?}"),
917        }
918    }
919
920    #[test]
921    fn test_parse_webrtc_p2p_relay() {
922        let maddr_str = format!(
923            "/p2p/{}/webrtc/p2p-circuit/p2p/{}",
924            TEST_RELAY_PEER_ID_LIBP2P, TEST_PEER_ID_LIBP2P
925        );
926        let opts: P2pConnectionOutgoingInitOpts = maddr_str.parse().unwrap();
927
928        match opts {
929            P2pConnectionOutgoingInitOpts::WebRTC { signaling, .. } => match signaling {
930                webrtc::SignalingMethod::P2p { .. } => {
931                    // Successfully parsed as P2P relay
932                }
933                x => panic!("Expected P2p signaling method, got {x:?}"),
934            },
935            x => panic!("Expected WebRTC variant, got {x:?}"),
936        }
937    }
938
939    #[test]
940    fn test_parse_libp2p_multiaddr() {
941        let maddr_str = format!(
942            "/dns4/seed-1.devnet.gcp.o1test.net/tcp/10003/p2p/{}",
943            TEST_PEER_ID_LIBP2P
944        );
945        let opts: P2pConnectionOutgoingInitOpts = maddr_str.parse().unwrap();
946
947        match opts {
948            P2pConnectionOutgoingInitOpts::LibP2P(libp2p_opts) => {
949                assert_eq!(
950                    libp2p_opts.host,
951                    Host::Domain("seed-1.devnet.gcp.o1test.net".to_string())
952                );
953                assert_eq!(libp2p_opts.port, 10003);
954            }
955            x => panic!("Expected LibP2P variant, got {x:?}"),
956        }
957    }
958
959    #[test]
960    fn test_roundtrip_webrtc_http() {
961        let maddr_str = format!(
962            "/dns4/signal.example.com/tcp/8080/webrtc/http/p2p/{}",
963            TEST_PEER_ID_LIBP2P
964        );
965
966        // First decode: parse multiaddr string
967        let opts1: P2pConnectionOutgoingInitOpts = maddr_str.parse().unwrap();
968
969        // Encode: convert to Multiaddr, then to string
970        let maddr = Multiaddr::from(&opts1);
971        let encoded_str = maddr.to_string();
972
973        // Second decode: parse the encoded string
974        let opts2 = P2pConnectionOutgoingInitOpts::from_str(&encoded_str).unwrap();
975
976        // Both decodes should match structurally
977        assert_eq!(opts1, opts2);
978    }
979
980    #[test]
981    fn test_roundtrip_webrtc_https() {
982        let maddr_str = format!(
983            "/dns4/signal.example.com/tcp/443/webrtc/https/p2p/{}",
984            TEST_PEER_ID_LIBP2P
985        );
986
987        let opts1: P2pConnectionOutgoingInitOpts = maddr_str.parse().unwrap();
988        let maddr: Multiaddr = (&opts1).into();
989        let opts2: P2pConnectionOutgoingInitOpts = (&maddr).try_into().unwrap();
990
991        assert_eq!(opts1, opts2);
992    }
993
994    #[test]
995    fn test_roundtrip_webrtc_https_proxy() {
996        let maddr_str = format!(
997            "/dns4/proxy.example.com/tcp/443/webrtc/https/http-path/cluster%2F123/p2p/{}",
998            TEST_PEER_ID_LIBP2P
999        );
1000
1001        let opts1: P2pConnectionOutgoingInitOpts = maddr_str.parse().unwrap();
1002        let maddr: Multiaddr = (&opts1).into();
1003        let opts2: P2pConnectionOutgoingInitOpts = (&maddr).try_into().unwrap();
1004
1005        assert_eq!(opts1, opts2);
1006    }
1007
1008    #[test]
1009    fn test_roundtrip_webrtc_p2p_relay() {
1010        let maddr_str = format!(
1011            "/p2p/{}/webrtc/p2p-circuit/p2p/{}",
1012            TEST_RELAY_PEER_ID_LIBP2P, TEST_PEER_ID_LIBP2P
1013        );
1014
1015        let opts1: P2pConnectionOutgoingInitOpts = maddr_str.parse().unwrap();
1016        let maddr: Multiaddr = (&opts1).into();
1017        let opts2: P2pConnectionOutgoingInitOpts = (&maddr).try_into().unwrap();
1018
1019        assert_eq!(opts1, opts2);
1020    }
1021
1022    #[test]
1023    fn test_roundtrip_libp2p() {
1024        let maddr_str = format!("/dns4/example.com/tcp/10003/p2p/{}", TEST_PEER_ID_LIBP2P);
1025
1026        let opts1: P2pConnectionOutgoingInitOpts = maddr_str.parse().unwrap();
1027        let maddr: Multiaddr = (&opts1).into();
1028        let opts2: P2pConnectionOutgoingInitOpts = (&maddr).try_into().unwrap();
1029
1030        assert_eq!(opts1, opts2);
1031    }
1032
1033    #[test]
1034    fn test_from_multiaddr_webrtc_http() {
1035        let maddr_str = format!(
1036            "/dns4/signal.example.com/tcp/8080/webrtc/http/p2p/{}",
1037            TEST_PEER_ID_LIBP2P
1038        );
1039        let maddr: Multiaddr = maddr_str.parse().unwrap();
1040        let opts: P2pConnectionOutgoingInitOpts = (&maddr).try_into().unwrap();
1041
1042        assert!(
1043            matches!(
1044                opts,
1045                P2pConnectionOutgoingInitOpts::WebRTC {
1046                    signaling: webrtc::SignalingMethod::Http(_),
1047                    ..
1048                }
1049            ),
1050            "expected WebRTC with Http signaling, got {opts:?}"
1051        );
1052    }
1053
1054    #[test]
1055    fn test_to_multiaddr_webrtc_http() {
1056        // Create peer_id from libp2p PeerId directly
1057        let libp2p_peer_id: libp2p_identity::PeerId = TEST_PEER_ID_LIBP2P.parse().unwrap();
1058        let peer_id: PeerId = libp2p_peer_id.try_into().unwrap();
1059
1060        let opts = P2pConnectionOutgoingInitOpts::WebRTC {
1061            peer_id,
1062            signaling: webrtc::SignalingMethod::Http(HttpSignalingInfo {
1063                host: Host::Domain("signal.example.com".to_string()),
1064                port: 8080,
1065            }),
1066        };
1067
1068        let maddr: Multiaddr = (&opts).into();
1069        let maddr_str = maddr.to_string();
1070
1071        assert!(maddr_str.contains("/webrtc/http/"));
1072        assert!(maddr_str.contains("/dns4/signal.example.com/"));
1073        assert!(maddr_str.contains("/tcp/8080/"));
1074
1075        // Verify roundtrip
1076        let opts2: P2pConnectionOutgoingInitOpts = (&maddr).try_into().unwrap();
1077        assert_eq!(opts, opts2);
1078    }
1079
1080    #[test]
1081    fn test_to_multiaddr_webrtc_p2p_relay() {
1082        let libp2p_peer_id: libp2p_identity::PeerId = TEST_PEER_ID_LIBP2P.parse().unwrap();
1083        let peer_id: PeerId = libp2p_peer_id.try_into().unwrap();
1084
1085        let libp2p_relay_peer_id: libp2p_identity::PeerId =
1086            TEST_RELAY_PEER_ID_LIBP2P.parse().unwrap();
1087        let relay_peer_id: PeerId = libp2p_relay_peer_id.try_into().unwrap();
1088
1089        let opts = P2pConnectionOutgoingInitOpts::WebRTC {
1090            peer_id,
1091            signaling: webrtc::SignalingMethod::P2p { relay_peer_id },
1092        };
1093
1094        let maddr: Multiaddr = (&opts).into();
1095        let maddr_str = maddr.to_string();
1096
1097        assert!(maddr_str.contains("/webrtc/p2p-circuit/"));
1098        assert!(maddr_str.starts_with("/p2p/"));
1099
1100        // Verify roundtrip
1101        let opts2: P2pConnectionOutgoingInitOpts = (&maddr).try_into().unwrap();
1102        assert_eq!(opts, opts2);
1103    }
1104
1105    #[test]
1106    fn test_legacy_format_still_works() {
1107        // Use a peer_id in the legacy format (internal base58 check encoding)
1108        // First parse from libp2p format to get a valid internal PeerId
1109        let libp2p_peer_id: libp2p_identity::PeerId = TEST_PEER_ID_LIBP2P.parse().unwrap();
1110        let peer_id: PeerId = libp2p_peer_id.try_into().unwrap();
1111        let legacy_peer_id_str = peer_id.to_string();
1112
1113        // Test that legacy format /{peer_id}/{signaling} still parses
1114        let legacy_str = format!("/{}/http/signal.example.com/8080", legacy_peer_id_str);
1115        let opts: P2pConnectionOutgoingInitOpts = legacy_str.parse().unwrap();
1116
1117        match opts {
1118            P2pConnectionOutgoingInitOpts::WebRTC { signaling, .. } => {
1119                assert!(
1120                    matches!(signaling, webrtc::SignalingMethod::Http(_)),
1121                    "expected Http signaling, got {signaling:?}"
1122                );
1123            }
1124            x => panic!("Expected WebRTC variant, got {x:?}"),
1125        }
1126    }
1127}