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