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