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#[cfg(test)]
288mod tests {
289    use super::*;
290    use std::net::Ipv6Addr;
291
292    #[test]
293    fn test_network_address_creation() {
294        let addr = MultiAddr::from_ipv4(Ipv4Addr::new(127, 0, 0, 1), 8080);
295        assert_eq!(addr.ip(), Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
296        assert_eq!(addr.port(), Some(8080));
297        assert!(addr.is_ipv4());
298        assert!(addr.is_loopback());
299    }
300
301    #[test]
302    fn test_network_address_from_string() {
303        let addr = "/ip4/127.0.0.1/udp/8080/quic".parse::<MultiAddr>().unwrap();
304        assert_eq!(addr.ip(), Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
305        assert_eq!(addr.port(), Some(8080));
306    }
307
308    #[test]
309    fn test_network_address_display() {
310        let addr = MultiAddr::from_ipv4(Ipv4Addr::new(192, 168, 1, 1), 9000);
311        assert_eq!(addr.to_string(), "/ip4/192.168.1.1/udp/9000/quic");
312    }
313
314    #[test]
315    fn test_private_address_detection() {
316        let private_addr = MultiAddr::from_ipv4(Ipv4Addr::new(192, 168, 1, 1), 9000);
317        assert!(private_addr.is_private());
318
319        let public_addr = MultiAddr::from_ipv4(Ipv4Addr::new(8, 8, 8, 8), 53);
320        assert!(!public_addr.is_private());
321    }
322
323    #[test]
324    fn test_ipv6_address() {
325        let addr = MultiAddr::from_ipv6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1), 8080);
326        assert!(addr.is_ipv6());
327        assert!(addr.is_loopback());
328    }
329
330    #[test]
331    fn test_multiaddr_tcp_parsing() {
332        let addr = "/ip4/192.168.1.1/tcp/9000".parse::<MultiAddr>().unwrap();
333        assert_eq!(addr.ip(), Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))));
334        assert_eq!(addr.port(), Some(9000));
335        assert!(matches!(addr.transport(), TransportAddr::Tcp(_)));
336    }
337
338    #[test]
339    fn test_multiaddr_quic_parsing() {
340        let addr = "/ip4/10.0.0.1/udp/9000/quic".parse::<MultiAddr>().unwrap();
341        assert_eq!(addr.ip(), Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))));
342        assert_eq!(addr.port(), Some(9000));
343        assert!(matches!(addr.transport(), TransportAddr::Quic(_)));
344    }
345
346    #[test]
347    fn test_multiaddr_raw_udp_parsing() {
348        let addr = "/ip4/10.0.0.1/udp/5000".parse::<MultiAddr>().unwrap();
349        assert_eq!(addr.port(), Some(5000));
350        assert!(matches!(addr.transport(), TransportAddr::Udp(_)));
351    }
352
353    #[test]
354    fn test_multiaddr_ipv6_quic_parsing() {
355        let addr = "/ip6/::1/udp/8080/quic".parse::<MultiAddr>().unwrap();
356        assert_eq!(
357            addr.ip(),
358            Some(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)))
359        );
360        assert_eq!(addr.port(), Some(8080));
361        assert!(addr.is_loopback());
362    }
363
364    #[test]
365    fn test_display_roundtrip_quic() {
366        let addr = MultiAddr::from_ipv4(Ipv4Addr::new(1, 2, 3, 4), 9000);
367        let s = addr.to_string();
368        let parsed: MultiAddr = s.parse().unwrap();
369        assert_eq!(addr, parsed);
370    }
371
372    #[test]
373    fn test_display_roundtrip_tcp() {
374        let addr = MultiAddr::tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 80));
375        let s = addr.to_string();
376        let parsed: MultiAddr = s.parse().unwrap();
377        assert_eq!(addr, parsed);
378    }
379
380    #[test]
381    fn test_bluetooth_roundtrip() {
382        let addr = MultiAddr::new(TransportAddr::Bluetooth {
383            mac: [0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF],
384            channel: 5,
385        });
386        let s = addr.to_string();
387        assert_eq!(s, "/bt/AA:BB:CC:DD:EE:FF/rfcomm/5");
388        let parsed: MultiAddr = s.parse().unwrap();
389        assert_eq!(addr, parsed);
390    }
391
392    #[test]
393    fn test_ble_roundtrip() {
394        let addr = MultiAddr::new(TransportAddr::Ble {
395            mac: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06],
396            psm: 128,
397        });
398        let s = addr.to_string();
399        assert_eq!(s, "/ble/01:02:03:04:05:06/l2cap/128");
400        let parsed: MultiAddr = s.parse().unwrap();
401        assert_eq!(addr, parsed);
402    }
403
404    #[test]
405    fn test_lora_roundtrip() {
406        let addr = MultiAddr::new(TransportAddr::LoRa {
407            dev_addr: [0xDE, 0xAD, 0xBE, 0xEF],
408            freq_hz: 868_000_000,
409        });
410        let s = addr.to_string();
411        assert_eq!(s, "/lora/deadbeef/868000000");
412        let parsed: MultiAddr = s.parse().unwrap();
413        assert_eq!(addr, parsed);
414    }
415
416    #[test]
417    fn test_lorawan_roundtrip() {
418        let addr = MultiAddr::new(TransportAddr::LoRaWan {
419            dev_eui: 0x0011_2233_4455_6677,
420        });
421        let s = addr.to_string();
422        assert_eq!(s, "/lorawan/0011223344556677");
423        let parsed: MultiAddr = s.parse().unwrap();
424        assert_eq!(addr, parsed);
425    }
426
427    #[test]
428    fn test_peer_id_suffix() {
429        let peer_id = PeerId::from_bytes([0xAA; 32]);
430        let addr = MultiAddr::from_ipv4(Ipv4Addr::new(1, 2, 3, 4), 9000).with_peer_id(peer_id);
431        let s = addr.to_string();
432        assert!(s.starts_with("/ip4/1.2.3.4/udp/9000/quic/p2p/"));
433        let parsed: MultiAddr = s.parse().unwrap();
434        assert_eq!(addr, parsed);
435        assert_eq!(parsed.peer_id(), Some(&peer_id));
436    }
437
438    #[test]
439    fn test_non_ip_transport_accessors() {
440        let addr = MultiAddr::new(TransportAddr::Bluetooth {
441            mac: [0; 6],
442            channel: 1,
443        });
444        assert_eq!(addr.socket_addr(), None);
445        assert_eq!(addr.ip(), None);
446        assert_eq!(addr.port(), None);
447        assert!(!addr.is_loopback());
448        assert!(!addr.is_private());
449        assert!(!addr.is_ipv4());
450        assert!(!addr.is_ipv6());
451    }
452
453    #[test]
454    fn test_serde_direct_roundtrip() {
455        let addr = MultiAddr::from_ipv4(Ipv4Addr::new(10, 0, 0, 1), 9000);
456        let json = serde_json::to_string(&addr).unwrap();
457        assert_eq!(json, r#""/ip4/10.0.0.1/udp/9000/quic""#);
458        let recovered: MultiAddr = serde_json::from_str(&json).unwrap();
459        assert_eq!(addr, recovered);
460    }
461
462    #[test]
463    fn test_transport_kind() {
464        assert_eq!(
465            TransportAddr::Quic(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)).kind(),
466            "quic"
467        );
468        assert_eq!(
469            TransportAddr::Tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0)).kind(),
470            "tcp"
471        );
472        assert_eq!(
473            TransportAddr::Bluetooth {
474                mac: [0; 6],
475                channel: 0
476            }
477            .kind(),
478            "bluetooth"
479        );
480    }
481
482    #[test]
483    fn test_invalid_format_rejected() {
484        // Bare "ip:port" is no longer accepted — canonical format required.
485        assert!("127.0.0.1:8080".parse::<MultiAddr>().is_err());
486        assert!("garbage".parse::<MultiAddr>().is_err());
487        assert!("/ip4/not-an-ip/tcp/80".parse::<MultiAddr>().is_err());
488        assert!("".parse::<MultiAddr>().is_err());
489    }
490
491    /// T2: Serde roundtrip for a `MultiAddr` that includes a `/p2p/<id>` suffix.
492    #[test]
493    fn test_serde_roundtrip_with_peer_id() {
494        let peer_id = PeerId::from_bytes([0xBB; 32]);
495        let addr = MultiAddr::from_ipv4(Ipv4Addr::new(10, 0, 0, 1), 9000).with_peer_id(peer_id);
496
497        let json = serde_json::to_string(&addr).unwrap();
498        assert!(
499            json.contains("/p2p/"),
500            "serialized form must contain /p2p/ suffix"
501        );
502
503        let recovered: MultiAddr = serde_json::from_str(&json).unwrap();
504        assert_eq!(addr, recovered, "serde roundtrip must be lossless");
505        assert_eq!(recovered.peer_id(), Some(&peer_id));
506    }
507
508    /// T3: `dialable_socket_addr()` returns `None` for TCP (not currently dialable).
509    #[test]
510    fn test_dialable_socket_addr_none_for_tcp() {
511        let tcp_addr = MultiAddr::tcp(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 80));
512        assert!(
513            tcp_addr.dialable_socket_addr().is_none(),
514            "TCP addresses should not be dialable (QUIC-only policy)"
515        );
516
517        // Sanity: QUIC *is* dialable.
518        let quic_addr = MultiAddr::quic(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 80));
519        assert!(quic_addr.dialable_socket_addr().is_some());
520    }
521
522    /// T4: Standalone `/p2p/<id>` without a transport prefix is rejected.
523    #[test]
524    fn test_standalone_peer_id_rejected() {
525        let peer_hex = "aa".repeat(32); // 64 hex chars
526        let input = format!("/p2p/{peer_hex}");
527        let result = input.parse::<MultiAddr>();
528        assert!(
529            result.is_err(),
530            "standalone /p2p/<id> without transport must be rejected"
531        );
532    }
533
534    /// L2: `From<TransportAddr>` enables idiomatic `.into()` conversion.
535    #[test]
536    fn test_from_transport_addr() {
537        let transport = TransportAddr::Quic(SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 9000));
538        let addr: MultiAddr = transport.clone().into();
539        assert_eq!(addr.transport(), &transport);
540        assert_eq!(addr.peer_id(), None);
541    }
542}