Skip to main content

saorsa_core/
address.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is licensed under the MIT license <LICENSE-MIT or
4// https://opensource.org/licenses/MIT> or the Apache License, Version 2.0
5// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, at your
6// option. This file may not be copied, modified, or distributed except
7// according to those terms.
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under these licenses is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
13//! # Address Types
14//!
15//! Composable, self-describing multi-transport address type for the Saorsa P2P
16//! network. Wraps [`saorsa_transport::TransportAddr`] with an optional
17//! [`PeerId`] suffix.
18//!
19//! ## Canonical string format
20//!
21//! ```text
22//! /ip4/<ipv4>/udp/<port>/quic[/p2p/<peer-id>]
23//! /ip6/<ipv6>/udp/<port>/quic[/p2p/<peer-id>]
24//! /ip4/<ipv4>/udp/<port>/webrtc-direct/certhash/<multihash>/p2p/<peer-id>
25//! /ip6/<ipv6>/udp/<port>/webrtc-direct/certhash/<multihash>/p2p/<peer-id>
26//! /ip4/<ipv4>/tcp/<port>[/p2p/<peer-id>]
27//! /ip6/<ipv6>/tcp/<port>[/p2p/<peer-id>]
28//! /ip4/<ipv4>/udp/<port>[/p2p/<peer-id>]
29//! /bt/<AA:BB:CC:DD:EE:FF>/rfcomm/<channel>[/p2p/<peer-id>]
30//! /ble/<AA:BB:CC:DD:EE:FF>/l2cap/<psm>[/p2p/<peer-id>]
31//! /lora/<hex-dev-addr>/<freq-hz>[/p2p/<peer-id>]
32//! /lorawan/<hex-dev-eui>[/p2p/<peer-id>]
33//! ```
34
35use std::fmt::{self, Display};
36use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
37use std::str::FromStr;
38
39use anyhow::{Result, anyhow};
40use serde::{Deserialize, Serialize};
41
42pub use saorsa_transport::transport::{TransportAddr, WebRtcCertificateHash, WebRtcDirectAddr};
43
44use crate::identity::peer_id::PeerId;
45
46/// Return true for IP addresses that should only be advertised for local
47/// reachability.
48#[must_use]
49pub(crate) fn is_lan_ip(ip: IpAddr) -> bool {
50    match ip {
51        IpAddr::V4(ip) => is_lan_ipv4(ip),
52        IpAddr::V6(ip) => {
53            if let Some(ip) = ip.to_ipv4_mapped() {
54                return is_lan_ipv4(ip);
55            }
56            is_lan_ipv6(ip)
57        }
58    }
59}
60
61fn is_lan_ipv4(ip: Ipv4Addr) -> bool {
62    ip.is_loopback()
63        || ip.is_private()
64        || ip.is_link_local()
65        || (ip.octets()[0] == 100 && (ip.octets()[1] & 0b1100_0000) == 64)
66}
67
68fn is_lan_ipv6(ip: Ipv6Addr) -> bool {
69    let octets = ip.octets();
70    ip.is_loopback()
71        || (octets[0] & 0xfe) == 0xfc
72        || (octets[0] == 0xfe && (octets[1] & 0xc0) == 0x80)
73}
74
75/// Composable, self-describing network address with an optional [`PeerId`]
76/// suffix.
77///
78/// Wraps a [`TransportAddr`] (which describes *how* to reach a network
79/// endpoint) with an optional peer identity (which describes *who* is behind
80/// it).
81#[derive(Debug, Clone, PartialEq, Eq, Hash)]
82pub struct MultiAddr {
83    transport: TransportAddr,
84    peer_id: Option<PeerId>,
85}
86
87impl From<TransportAddr> for MultiAddr {
88    fn from(transport: TransportAddr) -> Self {
89        Self::new(transport)
90    }
91}
92
93impl MultiAddr {
94    /// Create a `MultiAddr` from a [`TransportAddr`].
95    #[must_use]
96    pub fn new(transport: TransportAddr) -> Self {
97        Self {
98            transport,
99            peer_id: None,
100        }
101    }
102
103    /// Shorthand for `TransportAddr::Quic`.
104    #[must_use]
105    pub fn quic(addr: SocketAddr) -> Self {
106        Self::new(TransportAddr::Quic(addr))
107    }
108
109    /// Shorthand for `TransportAddr::Tcp`.
110    #[must_use]
111    pub fn tcp(addr: SocketAddr) -> Self {
112        Self::new(TransportAddr::Tcp(addr))
113    }
114
115    /// Create a browser-compatible WebRTC Direct multiaddress.
116    #[must_use]
117    pub fn webrtc_direct(addr: WebRtcDirectAddr) -> Self {
118        Self::new(TransportAddr::WebRtcDirect(addr))
119    }
120
121    /// Builder: attach a [`PeerId`] to this address.
122    #[must_use]
123    pub fn with_peer_id(mut self, peer_id: PeerId) -> Self {
124        self.peer_id = Some(peer_id);
125        self
126    }
127
128    /// Create a QUIC `MultiAddr` from an IP address and port.
129    #[must_use]
130    pub fn from_ip_port(ip: IpAddr, port: u16) -> Self {
131        Self::quic(SocketAddr::new(ip, port))
132    }
133
134    /// Create a QUIC `MultiAddr` from an IPv4 address and port.
135    #[must_use]
136    pub fn from_ipv4(ip: Ipv4Addr, port: u16) -> Self {
137        Self::from_ip_port(IpAddr::V4(ip), port)
138    }
139
140    /// Create a QUIC `MultiAddr` from an IPv6 address and port.
141    #[must_use]
142    pub fn from_ipv6(ip: Ipv6Addr, port: u16) -> Self {
143        Self::from_ip_port(IpAddr::V6(ip), port)
144    }
145
146    // -----------------------------------------------------------------------
147    // Accessors
148    // -----------------------------------------------------------------------
149
150    /// The underlying transport address.
151    #[must_use]
152    pub fn transport(&self) -> &TransportAddr {
153        &self.transport
154    }
155
156    /// Optional peer identity suffix.
157    #[must_use]
158    pub fn peer_id(&self) -> Option<&PeerId> {
159        self.peer_id.as_ref()
160    }
161
162    /// Return the WebRTC Direct endpoint when this address uses WebRTC Direct.
163    #[must_use]
164    pub fn webrtc_direct_addr(&self) -> Option<&WebRtcDirectAddr> {
165        match &self.transport {
166            TransportAddr::WebRtcDirect(address) => Some(address),
167            _ => None,
168        }
169    }
170
171    /// Whether this address describes a browser-compatible WebRTC Direct endpoint.
172    #[must_use]
173    pub fn is_webrtc_direct(&self) -> bool {
174        self.webrtc_direct_addr().is_some()
175    }
176
177    /// `true` when this address uses the QUIC transport — the only transport
178    /// currently supported for dialing. When additional transports are added,
179    /// update this method (and [`Self::dialable_socket_addr`]) accordingly.
180    #[must_use]
181    pub fn is_quic(&self) -> bool {
182        matches!(self.transport, TransportAddr::Quic(_))
183    }
184
185    /// Returns the [`SocketAddr`] **only** for transports we can currently
186    /// dial (QUIC). Returns `None` for all other transports, including
187    /// IP-based ones like TCP that we do not yet support.
188    ///
189    /// Use [`Self::socket_addr`] when you need the raw socket address
190    /// regardless of transport (e.g. IP diversity checks, geo lookups).
191    #[must_use]
192    pub fn dialable_socket_addr(&self) -> Option<SocketAddr> {
193        match self.transport {
194            TransportAddr::Quic(sa) => Some(sa),
195            _ => None,
196        }
197    }
198
199    /// Returns the socket address for IP-based transports (`Quic`, `Tcp`,
200    /// `Udp`), `None` for non-IP transports.
201    #[must_use]
202    pub fn socket_addr(&self) -> Option<SocketAddr> {
203        match &self.transport {
204            TransportAddr::WebRtcDirect(address) => Some(address.socket_addr()),
205            _ => self.transport.as_socket_addr(),
206        }
207    }
208
209    /// Whether this address can be retained in an advertised peer record.
210    /// Rejects wildcard IPs and port zero; loopback remains valid for local devnets.
211    #[must_use]
212    pub fn is_storable(&self) -> bool {
213        self.socket_addr()
214            .is_none_or(|sa| !sa.ip().is_unspecified() && sa.port() != 0)
215    }
216
217    /// Returns the IP address for IP-based transports, `None` otherwise.
218    #[must_use]
219    pub fn ip(&self) -> Option<IpAddr> {
220        self.socket_addr().map(|a| a.ip())
221    }
222
223    /// Returns the port for IP-based transports, `None` otherwise.
224    #[must_use]
225    pub fn port(&self) -> Option<u16> {
226        match &self.transport {
227            TransportAddr::WebRtcDirect(address) => Some(address.port()),
228            _ => self.socket_addr().map(|a| a.port()),
229        }
230    }
231
232    /// `true` for IP-based transports with IPv4 addressing.
233    pub fn is_ipv4(&self) -> bool {
234        self.socket_addr().is_some_and(|a| a.is_ipv4())
235    }
236
237    /// `true` for IP-based transports with IPv6 addressing.
238    pub fn is_ipv6(&self) -> bool {
239        self.socket_addr().is_some_and(|a| a.is_ipv6())
240    }
241
242    /// `true` if this is an IP-based loopback address, `false` otherwise.
243    pub fn is_loopback(&self) -> bool {
244        self.ip().is_some_and(|ip| ip.is_loopback())
245    }
246
247    /// `true` if this is an IP-based local-scope address, `false`
248    /// otherwise.
249    pub fn is_private(&self) -> bool {
250        self.ip().is_some_and(is_lan_ip)
251    }
252}
253
254// ---------------------------------------------------------------------------
255// Display — delegates transport part to TransportAddr, appends /p2p/ suffix
256// ---------------------------------------------------------------------------
257
258impl Display for MultiAddr {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        write!(f, "{}", self.transport)?;
261        if let Some(pid) = &self.peer_id {
262            write!(f, "/p2p/{}", pid.to_hex())?;
263        }
264        Ok(())
265    }
266}
267
268// ---------------------------------------------------------------------------
269// FromStr — strips /p2p/ suffix, delegates transport parsing to TransportAddr
270// ---------------------------------------------------------------------------
271
272impl FromStr for MultiAddr {
273    type Err = anyhow::Error;
274
275    fn from_str(s: &str) -> Result<Self> {
276        if s.is_empty() {
277            return Err(anyhow!("Invalid address format: empty string"));
278        }
279
280        // Look for /p2p/ suffix (find last occurrence to be safe).
281        if let Some(p2p_idx) = s.rfind("/p2p/") {
282            let transport_part = &s[..p2p_idx];
283            let peer_hex = &s[p2p_idx + 5..]; // skip "/p2p/"
284
285            // Reject standalone /p2p/<id> with no transport.
286            if transport_part.is_empty() {
287                return Err(anyhow!(
288                    "Peer-only addresses (/p2p/<id>) are not yet supported as standalone MultiAddr"
289                ));
290            }
291
292            // Reject trailing garbage after peer ID.
293            if peer_hex.contains('/') {
294                return Err(anyhow!(
295                    "Unexpected trailing components after peer ID in: {}",
296                    s
297                ));
298            }
299
300            let transport = transport_part
301                .parse::<TransportAddr>()
302                .map_err(|e| anyhow!("Invalid transport address: {}", e))?;
303            let peer_id = PeerId::from_hex(peer_hex)
304                .map_err(|e| anyhow!("Invalid peer ID in address: {}", e))?;
305
306            Ok(MultiAddr {
307                transport,
308                peer_id: Some(peer_id),
309            })
310        } else {
311            // No /p2p/ suffix — pure transport address.
312            let transport = s
313                .parse::<TransportAddr>()
314                .map_err(|e| anyhow!("Invalid address: {}", e))?;
315
316            Ok(MultiAddr {
317                transport,
318                peer_id: None,
319            })
320        }
321    }
322}
323
324// ---------------------------------------------------------------------------
325// Serde — serialize as canonical string
326// ---------------------------------------------------------------------------
327
328impl Serialize for MultiAddr {
329    fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
330        s.serialize_str(&self.to_string())
331    }
332}
333
334impl<'de> Deserialize<'de> for MultiAddr {
335    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
336        let s = String::deserialize(d)?;
337        s.parse::<MultiAddr>().map_err(serde::de::Error::custom)
338    }
339}
340
341// ---------------------------------------------------------------------------
342// AddressBook
343// ---------------------------------------------------------------------------
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use std::net::Ipv6Addr;
349
350    #[test]
351    fn test_network_address_creation() {
352        let addr = MultiAddr::from_ipv4(Ipv4Addr::new(127, 0, 0, 1), 8080);
353        assert_eq!(addr.ip(), Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
354        assert_eq!(addr.port(), Some(8080));
355        assert!(addr.is_ipv4());
356        assert!(addr.is_loopback());
357    }
358
359    #[test]
360    fn test_network_address_from_string() {
361        let addr = "/ip4/127.0.0.1/udp/8080/quic".parse::<MultiAddr>().unwrap();
362        assert_eq!(addr.ip(), Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
363        assert_eq!(addr.port(), Some(8080));
364    }
365
366    #[test]
367    fn test_network_address_display() {
368        let addr = MultiAddr::from_ipv4(Ipv4Addr::new(192, 168, 1, 1), 9000);
369        assert_eq!(addr.to_string(), "/ip4/192.168.1.1/udp/9000/quic");
370    }
371
372    #[test]
373    fn test_private_address_detection() {
374        let private_addr = MultiAddr::from_ipv4(Ipv4Addr::new(192, 168, 1, 1), 9000);
375        assert!(private_addr.is_private());
376
377        let public_addr = MultiAddr::from_ipv4(Ipv4Addr::new(8, 8, 8, 8), 53);
378        assert!(!public_addr.is_private());
379    }
380
381    #[test]
382    fn test_ipv4_mapped_lan_address_detection() {
383        let mapped_private: IpAddr = "::ffff:192.168.1.10".parse().unwrap();
384        let mapped_loopback: IpAddr = "::ffff:127.0.0.1".parse().unwrap();
385        let mapped_link_local: IpAddr = "::ffff:169.254.1.10".parse().unwrap();
386        let mapped_cgnat: IpAddr = "::ffff:100.64.0.1".parse().unwrap();
387        let mapped_public: IpAddr = "::ffff:8.8.8.8".parse().unwrap();
388
389        assert!(is_lan_ip(mapped_private));
390        assert!(is_lan_ip(mapped_loopback));
391        assert!(is_lan_ip(mapped_link_local));
392        assert!(is_lan_ip(mapped_cgnat));
393        assert!(!is_lan_ip(mapped_public));
394    }
395
396    #[test]
397    fn test_ipv4_mapped_private_multiaddr_detection() {
398        let addr: MultiAddr = "/ip6/::ffff:192.168.1.10/udp/9000/quic".parse().unwrap();
399
400        assert!(addr.is_private());
401    }
402
403    #[test]
404    fn test_ipv6_address() {
405        let addr = MultiAddr::from_ipv6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1), 8080);
406        assert!(addr.is_ipv6());
407        assert!(addr.is_loopback());
408    }
409
410    #[test]
411    fn test_multiaddr_tcp_parsing() {
412        let addr = "/ip4/192.168.1.1/tcp/9000".parse::<MultiAddr>().unwrap();
413        assert_eq!(addr.ip(), Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
414        assert_eq!(addr.port(), Some(9000));
415        assert!(matches!(addr.transport(), TransportAddr::Tcp(_)));
416    }
417
418    #[test]
419    fn test_multiaddr_quic_parsing() {
420        let addr = "/ip4/10.0.0.1/udp/9000/quic".parse::<MultiAddr>().unwrap();
421        assert_eq!(addr.ip(), Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
422        assert_eq!(addr.port(), Some(9000));
423        assert!(matches!(addr.transport(), TransportAddr::Quic(_)));
424    }
425
426    #[test]
427    fn test_multiaddr_webrtc_direct_roundtrip_with_peer_id() {
428        let peer_id = PeerId::from_bytes([0x55; 32]);
429        let certificate_hash = WebRtcCertificateHash::new([0x11; 32]);
430        let endpoint =
431            WebRtcDirectAddr::new("203.0.113.7:443".parse().unwrap(), certificate_hash).unwrap();
432        let address = MultiAddr::webrtc_direct(endpoint).with_peer_id(peer_id);
433
434        let encoded = address.to_string();
435        assert_eq!(encoded.matches("/certhash/").count(), 1);
436        assert!(encoded.ends_with(&format!("/p2p/{}", peer_id.to_hex())));
437        let parsed = encoded.parse::<MultiAddr>().unwrap();
438        assert_eq!(parsed, address);
439        assert_eq!(parsed.peer_id(), Some(&peer_id));
440        assert!(parsed.is_webrtc_direct());
441        assert!(!parsed.is_quic());
442        assert_eq!(parsed.port(), Some(443));
443        assert_eq!(
444            parsed.webrtc_direct_addr().unwrap().certificate_hash(),
445            certificate_hash
446        );
447    }
448
449    #[test]
450    fn test_webrtc_direct_multiaddr_postcard_roundtrip() {
451        let peer_id = PeerId::from_bytes([0x66; 32]);
452        let endpoint = WebRtcDirectAddr::new(
453            "127.0.0.1:443".parse().unwrap(),
454            WebRtcCertificateHash::new([0x33; 32]),
455        )
456        .unwrap();
457        let address = MultiAddr::webrtc_direct(endpoint).with_peer_id(peer_id);
458
459        let bytes = postcard::to_stdvec(&address).unwrap();
460        let decoded: MultiAddr = postcard::from_bytes(&bytes).unwrap();
461        assert_eq!(decoded, address);
462    }
463
464    #[test]
465    fn test_multiaddr_raw_udp_parsing() {
466        let addr = "/ip4/10.0.0.1/udp/5000".parse::<MultiAddr>().unwrap();
467        assert_eq!(addr.port(), Some(5000));
468        assert!(matches!(addr.transport(), TransportAddr::Udp(_)));
469    }
470
471    #[test]
472    fn test_multiaddr_ipv6_quic_parsing() {
473        let addr = "/ip6/::1/udp/8080/quic".parse::<MultiAddr>().unwrap();
474        assert_eq!(
475            addr.ip(),
476            Some(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)))
477        );
478        assert_eq!(addr.port(), Some(8080));
479        assert!(addr.is_loopback());
480    }
481
482    #[test]
483    fn test_display_roundtrip_quic() {
484        let addr = MultiAddr::from_ipv4(Ipv4Addr::new(1, 2, 3, 4), 9000);
485        let s = addr.to_string();
486        let parsed: MultiAddr = s.parse().unwrap();
487        assert_eq!(addr, parsed);
488    }
489
490    #[test]
491    fn test_display_roundtrip_tcp() {
492        let addr = MultiAddr::tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 80));
493        let s = addr.to_string();
494        let parsed: MultiAddr = s.parse().unwrap();
495        assert_eq!(addr, parsed);
496    }
497
498    #[test]
499    fn test_bluetooth_roundtrip() {
500        let addr = MultiAddr::new(TransportAddr::Bluetooth {
501            mac: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF],
502            channel: 5,
503        });
504        let s = addr.to_string();
505        assert_eq!(s, "/bt/AA:BB:CC:DD:EE:FF/rfcomm/5");
506        let parsed: MultiAddr = s.parse().unwrap();
507        assert_eq!(addr, parsed);
508    }
509
510    #[test]
511    fn test_ble_roundtrip() {
512        let addr = MultiAddr::new(TransportAddr::Ble {
513            mac: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06],
514            psm: 128,
515        });
516        let s = addr.to_string();
517        assert_eq!(s, "/ble/01:02:03:04:05:06/l2cap/128");
518        let parsed: MultiAddr = s.parse().unwrap();
519        assert_eq!(addr, parsed);
520    }
521
522    #[test]
523    fn test_lora_roundtrip() {
524        let addr = MultiAddr::new(TransportAddr::LoRa {
525            dev_addr: [0xDE, 0xAD, 0xBE, 0xEF],
526            freq_hz: 868_000_000,
527        });
528        let s = addr.to_string();
529        assert_eq!(s, "/lora/deadbeef/868000000");
530        let parsed: MultiAddr = s.parse().unwrap();
531        assert_eq!(addr, parsed);
532    }
533
534    #[test]
535    fn test_lorawan_roundtrip() {
536        let addr = MultiAddr::new(TransportAddr::LoRaWan {
537            dev_eui: 0x0011_2233_4455_6677,
538        });
539        let s = addr.to_string();
540        assert_eq!(s, "/lorawan/0011223344556677");
541        let parsed: MultiAddr = s.parse().unwrap();
542        assert_eq!(addr, parsed);
543    }
544
545    #[test]
546    fn test_peer_id_suffix() {
547        let peer_id = PeerId::from_bytes([0xAA; 32]);
548        let addr = MultiAddr::from_ipv4(Ipv4Addr::new(1, 2, 3, 4), 9000).with_peer_id(peer_id);
549        let s = addr.to_string();
550        assert!(s.starts_with("/ip4/1.2.3.4/udp/9000/quic/p2p/"));
551        let parsed: MultiAddr = s.parse().unwrap();
552        assert_eq!(addr, parsed);
553        assert_eq!(parsed.peer_id(), Some(&peer_id));
554    }
555
556    #[test]
557    fn test_non_ip_transport_accessors() {
558        let addr = MultiAddr::new(TransportAddr::Bluetooth {
559            mac: [0; 6],
560            channel: 1,
561        });
562        assert_eq!(addr.socket_addr(), None);
563        assert_eq!(addr.ip(), None);
564        assert_eq!(addr.port(), None);
565        assert!(!addr.is_loopback());
566        assert!(!addr.is_private());
567        assert!(!addr.is_ipv4());
568        assert!(!addr.is_ipv6());
569    }
570
571    #[test]
572    fn test_serde_direct_roundtrip() {
573        let addr = MultiAddr::from_ipv4(Ipv4Addr::new(10, 0, 0, 1), 9000);
574        let json = serde_json::to_string(&addr).unwrap();
575        assert_eq!(json, r#""/ip4/10.0.0.1/udp/9000/quic""#);
576        let recovered: MultiAddr = serde_json::from_str(&json).unwrap();
577        assert_eq!(addr, recovered);
578    }
579
580    #[test]
581    fn test_transport_kind() {
582        assert_eq!(
583            TransportAddr::Quic(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)).kind(),
584            "quic"
585        );
586        assert_eq!(
587            TransportAddr::Tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)).kind(),
588            "tcp"
589        );
590        assert_eq!(
591            TransportAddr::Bluetooth {
592                mac: [0; 6],
593                channel: 0
594            }
595            .kind(),
596            "bluetooth"
597        );
598    }
599
600    #[test]
601    fn test_invalid_format_rejected() {
602        // Bare "ip:port" is no longer accepted — canonical format required.
603        assert!("127.0.0.1:8080".parse::<MultiAddr>().is_err());
604        assert!("garbage".parse::<MultiAddr>().is_err());
605        assert!("/ip4/not-an-ip/tcp/80".parse::<MultiAddr>().is_err());
606        assert!("".parse::<MultiAddr>().is_err());
607    }
608
609    /// T2: Serde roundtrip for a `MultiAddr` that includes a `/p2p/<id>` suffix.
610    #[test]
611    fn test_serde_roundtrip_with_peer_id() {
612        let peer_id = PeerId::from_bytes([0xBB; 32]);
613        let addr = MultiAddr::from_ipv4(Ipv4Addr::new(10, 0, 0, 1), 9000).with_peer_id(peer_id);
614
615        let json = serde_json::to_string(&addr).unwrap();
616        assert!(
617            json.contains("/p2p/"),
618            "serialized form must contain /p2p/ suffix"
619        );
620
621        let recovered: MultiAddr = serde_json::from_str(&json).unwrap();
622        assert_eq!(addr, recovered, "serde roundtrip must be lossless");
623        assert_eq!(recovered.peer_id(), Some(&peer_id));
624    }
625
626    /// T3: `dialable_socket_addr()` returns `None` for TCP (not currently dialable).
627    #[test]
628    fn test_dialable_socket_addr_none_for_tcp() {
629        let tcp_addr = MultiAddr::tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 80));
630        assert!(
631            tcp_addr.dialable_socket_addr().is_none(),
632            "TCP addresses should not be dialable (QUIC-only policy)"
633        );
634
635        // Sanity: QUIC *is* dialable.
636        let quic_addr = MultiAddr::quic(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 80));
637        assert!(quic_addr.dialable_socket_addr().is_some());
638    }
639
640    /// T4: Standalone `/p2p/<id>` without a transport prefix is rejected.
641    #[test]
642    fn test_standalone_peer_id_rejected() {
643        let peer_hex = "aa".repeat(32); // 64 hex chars
644        let input = format!("/p2p/{peer_hex}");
645        let result = input.parse::<MultiAddr>();
646        assert!(
647            result.is_err(),
648            "standalone /p2p/<id> without transport must be rejected"
649        );
650    }
651
652    /// L2: `From<TransportAddr>` enables idiomatic `.into()` conversion.
653    #[test]
654    fn test_from_transport_addr() {
655        let transport = TransportAddr::Quic(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9000));
656        let addr: MultiAddr = transport.clone().into();
657        assert_eq!(addr.transport(), &transport);
658        assert_eq!(addr.peer_id(), None);
659    }
660}