Skip to main content

saorsa_core/
address.rs

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