Skip to main content

bgpkit_parser/models/network/
nexthop.rs

1use crate::models::BgpModelsError;
2use std::fmt::{Debug, Display, Formatter};
3use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
4use std::str::FromStr;
5
6/// Route Distinguisher for VPN next-hops - RFC 4364, Section 4.1
7/// An 8-byte value used to distinguish VPN routes with potentially overlapping address spaces
8#[derive(PartialEq, Copy, Clone, Eq, Hash)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
11pub struct RouteDistinguisher(pub [u8; 8]);
12
13impl Debug for RouteDistinguisher {
14    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
15        write!(
16            f,
17            "RD({:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x})",
18            self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7]
19        )
20    }
21}
22
23impl Display for RouteDistinguisher {
24    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
25        write!(
26            f,
27            "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
28            self.0[0], self.0[1], self.0[2], self.0[3], self.0[4], self.0[5], self.0[6], self.0[7]
29        )
30    }
31}
32
33/// enum that represents the type of the next hop address.
34///
35/// [NextHopAddress] is used when parsing for next hops in [Nlri](crate::models::Nlri).
36/// RFC 8950 extends this to support VPN next-hops with Route Distinguishers.
37#[derive(PartialEq, Copy, Clone, Eq, Hash)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
40pub enum NextHopAddress {
41    Ipv4(Ipv4Addr),
42    Ipv6(Ipv6Addr),
43    Ipv6LinkLocal(Ipv6Addr, Ipv6Addr),
44    /// VPN-IPv6 next hop - RFC 8950, Section 4
45    /// Contains Route Distinguisher (8 bytes) + IPv6 address (16 bytes) = 24 bytes total
46    VpnIpv6(RouteDistinguisher, Ipv6Addr),
47    /// VPN-IPv6 next hop with link-local - RFC 8950, Section 4  
48    /// Contains RD (8 bytes) + IPv6 (16 bytes) + RD (8 bytes) + IPv6 link-local (16 bytes) = 48 bytes total
49    VpnIpv6LinkLocal(RouteDistinguisher, Ipv6Addr, RouteDistinguisher, Ipv6Addr),
50}
51
52impl NextHopAddress {
53    /// Returns true if the next hop is a link local address
54    pub const fn is_link_local(&self) -> bool {
55        match self {
56            NextHopAddress::Ipv4(x) => x.is_link_local(),
57            NextHopAddress::Ipv6(x) => x.is_unicast_link_local(),
58            NextHopAddress::Ipv6LinkLocal(_, _) => true,
59            NextHopAddress::VpnIpv6(_, x) => x.is_unicast_link_local(),
60            NextHopAddress::VpnIpv6LinkLocal(_, _, _, _) => true,
61        }
62    }
63
64    /// Returns the address that this next hop points to — the first address
65    /// of a pair, in wire order. See [`global_addr`](Self::global_addr) to
66    /// resolve by scope instead.
67    pub const fn addr(&self) -> IpAddr {
68        match self {
69            NextHopAddress::Ipv4(x) => IpAddr::V4(*x),
70            NextHopAddress::Ipv6(x) => IpAddr::V6(*x),
71            NextHopAddress::Ipv6LinkLocal(x, _) => IpAddr::V6(*x),
72            NextHopAddress::VpnIpv6(_, x) => IpAddr::V6(*x),
73            NextHopAddress::VpnIpv6LinkLocal(_, x, _, _) => IpAddr::V6(*x),
74        }
75    }
76
77    /// The two addresses of an RFC 2545 pair, in wire order.
78    const fn pair(&self) -> Option<(Ipv6Addr, Ipv6Addr)> {
79        match self {
80            NextHopAddress::Ipv6LinkLocal(x, y) => Some((*x, *y)),
81            NextHopAddress::VpnIpv6LinkLocal(_, x, _, y) => Some((*x, *y)),
82            _ => None,
83        }
84    }
85
86    /// Returns the global-scope address of the next hop. Pairs are resolved
87    /// by scope, not position — a reversed RFC 2545 pair still yields the
88    /// global address; same-scope pairs and single addresses fall back to
89    /// [`addr`](Self::addr).
90    pub const fn global_addr(&self) -> IpAddr {
91        match self.pair() {
92            Some((first, second)) => {
93                if first.is_unicast_link_local() && !second.is_unicast_link_local() {
94                    IpAddr::V6(second)
95                } else {
96                    IpAddr::V6(first)
97                }
98            }
99            None => self.addr(),
100        }
101    }
102
103    /// Returns the link-local half of a pair (RFC 2545): the address
104    /// [`global_addr`](Self::global_addr) does not return, when link-local.
105    /// Single addresses yield `None`, even link-local ones.
106    pub const fn link_local_addr(&self) -> Option<Ipv6Addr> {
107        match self.pair() {
108            // mirrors global_addr(), which keeps `first` of a both-link-local pair
109            Some((_, second)) if second.is_unicast_link_local() => Some(second),
110            Some((first, _)) if first.is_unicast_link_local() => Some(first),
111            _ => None,
112        }
113    }
114}
115
116/// Parses a single IPv4/IPv6 address, or a comma-joined IPv6 pair as RIS Live
117/// renders an RFC 2545 next hop (`"2001:db8::1,fe80::1"`), stored positionally
118/// as [`NextHopAddress::Ipv6LinkLocal`]. Whitespace around the separator is
119/// tolerated.
120impl FromStr for NextHopAddress {
121    type Err = BgpModelsError;
122
123    fn from_str(s: &str) -> Result<Self, Self::Err> {
124        fn invalid(addr: &str, e: std::net::AddrParseError) -> BgpModelsError {
125            BgpModelsError::NextHopParsingError(format!("invalid address {addr:?}: {e}"))
126        }
127        fn parse_v6(addr: &str) -> Result<Ipv6Addr, BgpModelsError> {
128            let addr = addr.trim();
129            Ipv6Addr::from_str(addr).map_err(|e| invalid(addr, e))
130        }
131
132        match s.split_once(',') {
133            None => {
134                let addr = s.trim();
135                IpAddr::from_str(addr)
136                    .map(NextHopAddress::from)
137                    .map_err(|e| invalid(addr, e))
138            }
139            Some((_, second)) if second.contains(',') => Err(BgpModelsError::NextHopParsingError(
140                format!("more than two addresses: {s:?}"),
141            )),
142            Some((first, second)) => Ok(NextHopAddress::Ipv6LinkLocal(
143                parse_v6(first)?,
144                parse_v6(second)?,
145            )),
146        }
147    }
148}
149
150// Attempt to reduce the size of the debug output
151impl Debug for NextHopAddress {
152    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
153        match self {
154            NextHopAddress::Ipv4(x) => write!(f, "{x}"),
155            NextHopAddress::Ipv6(x) => write!(f, "{x}"),
156            NextHopAddress::Ipv6LinkLocal(x, y) => write!(f, "Ipv6LinkLocal({x}, {y})"),
157            NextHopAddress::VpnIpv6(rd, x) => write!(f, "VpnIpv6({rd}, {x})"),
158            NextHopAddress::VpnIpv6LinkLocal(rd1, x, rd2, y) => {
159                write!(f, "VpnIpv6LinkLocal({rd1}, {x}, {rd2}, {y})")
160            }
161        }
162    }
163}
164
165/// Renders the address; pairs render comma-joined in wire order, round-tripping
166/// with [`FromStr`] (non-VPN forms). Route Distinguishers are not rendered.
167impl Display for NextHopAddress {
168    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
169        match self.pair() {
170            Some((first, second)) => write!(f, "{first},{second}"),
171            None => write!(f, "{}", self.addr()),
172        }
173    }
174}
175
176impl From<IpAddr> for NextHopAddress {
177    fn from(value: IpAddr) -> Self {
178        match value {
179            IpAddr::V4(x) => NextHopAddress::Ipv4(x),
180            IpAddr::V6(x) => NextHopAddress::Ipv6(x),
181        }
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
189
190    #[test]
191    fn test_next_hop_address_is_link_local() {
192        let ipv4_addr = Ipv4Addr::new(169, 254, 0, 1);
193        let ipv6_addr = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0);
194        let ipv6_link_local_addrs = (
195            Ipv6Addr::new(0xfe80, 0, 0, 1, 0, 0, 0, 1),
196            Ipv6Addr::new(0xfe80, 0, 0, 2, 0, 0, 0, 1),
197        );
198
199        let next_hop_ipv4 = NextHopAddress::Ipv4(ipv4_addr);
200        let next_hop_ipv6 = NextHopAddress::Ipv6(ipv6_addr);
201        let next_hop_ipv6_link_local =
202            NextHopAddress::Ipv6LinkLocal(ipv6_link_local_addrs.0, ipv6_link_local_addrs.1);
203
204        assert!(next_hop_ipv4.is_link_local());
205        assert!(next_hop_ipv6.is_link_local());
206        assert!(next_hop_ipv6_link_local.is_link_local());
207    }
208
209    #[test]
210    fn test_next_hop_address_addr() {
211        let ipv4_addr = Ipv4Addr::new(192, 0, 2, 1);
212        let ipv6_addr = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1);
213        let ipv6_link_local_addrs = (
214            Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0),
215            Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0),
216        );
217
218        let next_hop_ipv4 = NextHopAddress::Ipv4(ipv4_addr);
219        let next_hop_ipv6 = NextHopAddress::Ipv6(ipv6_addr);
220        let next_hop_ipv6_link_local =
221            NextHopAddress::Ipv6LinkLocal(ipv6_link_local_addrs.0, ipv6_link_local_addrs.1);
222
223        assert_eq!(next_hop_ipv4.addr(), IpAddr::V4(ipv4_addr));
224        assert_eq!(next_hop_ipv6.addr(), IpAddr::V6(ipv6_addr));
225        assert_eq!(
226            next_hop_ipv6_link_local.addr(),
227            IpAddr::V6(ipv6_link_local_addrs.0)
228        );
229    }
230
231    #[test]
232    fn test_next_hop_address_from() {
233        let ipv4_addr = IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1));
234        let ipv6_addr = IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1));
235
236        let next_hop_ipv4 = NextHopAddress::from(ipv4_addr);
237        let next_hop_ipv6 = NextHopAddress::from(ipv6_addr);
238
239        assert_eq!(next_hop_ipv4.addr(), ipv4_addr);
240        assert_eq!(next_hop_ipv6.addr(), ipv6_addr);
241    }
242
243    #[test]
244    fn test_debug_for_next_hop_address() {
245        let ipv4_addr = Ipv4Addr::new(192, 0, 2, 1);
246        let ipv6_addr = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1);
247        let ipv6_link_local_addrs = (
248            Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0),
249            Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0),
250        );
251
252        let next_hop_ipv4 = NextHopAddress::Ipv4(ipv4_addr);
253        let next_hop_ipv6 = NextHopAddress::Ipv6(ipv6_addr);
254        let next_hop_ipv6_link_local =
255            NextHopAddress::Ipv6LinkLocal(ipv6_link_local_addrs.0, ipv6_link_local_addrs.1);
256
257        assert_eq!(format!("{next_hop_ipv4:?}"), "192.0.2.1");
258        assert_eq!(format!("{next_hop_ipv6:?}"), "2001:db8::1");
259        assert_eq!(
260            format!("{next_hop_ipv6_link_local:?}"),
261            "Ipv6LinkLocal(fe80::, fe80::)"
262        );
263    }
264
265    #[test]
266    fn test_display_for_next_hop_address() {
267        let ipv4_addr = Ipv4Addr::new(192, 0, 2, 1);
268        let ipv6_addr = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1);
269        let ipv6_link_local_addrs = (
270            Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0),
271            Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 0),
272        );
273
274        let next_hop_ipv4 = NextHopAddress::Ipv4(ipv4_addr);
275        let next_hop_ipv6 = NextHopAddress::Ipv6(ipv6_addr);
276        let next_hop_ipv6_link_local =
277            NextHopAddress::Ipv6LinkLocal(ipv6_link_local_addrs.0, ipv6_link_local_addrs.1);
278
279        assert_eq!(format!("{next_hop_ipv4}"), "192.0.2.1");
280        assert_eq!(format!("{next_hop_ipv6}"), "2001:db8::1");
281        assert_eq!(format!("{next_hop_ipv6_link_local}"), "fe80::,fe80::");
282    }
283
284    #[test]
285    fn test_next_hop_address_from_str() {
286        // single addresses
287        assert_eq!(
288            "192.0.2.1".parse::<NextHopAddress>().unwrap(),
289            NextHopAddress::Ipv4(Ipv4Addr::new(192, 0, 2, 1))
290        );
291        assert_eq!(
292            "2001:db8::1".parse::<NextHopAddress>().unwrap(),
293            NextHopAddress::Ipv6("2001:db8::1".parse().unwrap())
294        );
295        // a lone link-local address is a plain single next hop
296        assert_eq!(
297            "fe80::1".parse::<NextHopAddress>().unwrap(),
298            NextHopAddress::Ipv6("fe80::1".parse().unwrap())
299        );
300
301        // a comma-joined pair is stored positionally, in wire order
302        let pair = NextHopAddress::Ipv6LinkLocal(
303            "2001:db8::1".parse().unwrap(),
304            "fe80::1".parse().unwrap(),
305        );
306        assert_eq!(
307            "2001:db8::1,fe80::1".parse::<NextHopAddress>().unwrap(),
308            pair
309        );
310        assert_eq!(
311            "fe80::1,2001:db8::1".parse::<NextHopAddress>().unwrap(),
312            NextHopAddress::Ipv6LinkLocal(
313                "fe80::1".parse().unwrap(),
314                "2001:db8::1".parse().unwrap()
315            )
316        );
317        // whitespace around the separator is tolerated
318        assert_eq!(
319            "2001:db8::1, fe80::1".parse::<NextHopAddress>().unwrap(),
320            pair
321        );
322
323        // errors: not one address or two IPv6 addresses
324        assert!("".parse::<NextHopAddress>().is_err());
325        assert!("not-an-address".parse::<NextHopAddress>().is_err());
326        assert!("fe80::1%eth0".parse::<NextHopAddress>().is_err());
327        assert!("2001:db8::1,fe80::1,fe80::2"
328            .parse::<NextHopAddress>()
329            .is_err());
330        assert!("2001:db8::1,".parse::<NextHopAddress>().is_err());
331        assert!("192.0.2.1,fe80::1".parse::<NextHopAddress>().is_err());
332    }
333
334    #[test]
335    fn test_next_hop_address_display_from_str_round_trip() {
336        for repr in [
337            "192.0.2.1",
338            "2001:db8::1",
339            "2001:db8::1,fe80::1",
340            "fe80::1,2001:db8::1",
341        ] {
342            assert_eq!(repr.parse::<NextHopAddress>().unwrap().to_string(), repr);
343        }
344    }
345
346    #[test]
347    fn test_next_hop_address_global_addr() {
348        let global: Ipv6Addr = "2001:db8::1".parse().unwrap();
349        let link_local: Ipv6Addr = "fe80::1".parse().unwrap();
350        let rd = RouteDistinguisher([0; 8]);
351
352        // single addresses are returned as-is, including a lone link-local one
353        assert_eq!(
354            NextHopAddress::Ipv6(link_local).global_addr(),
355            IpAddr::V6(link_local)
356        );
357
358        // a pair is resolved by scope, regardless of order
359        assert_eq!(
360            NextHopAddress::Ipv6LinkLocal(global, link_local).global_addr(),
361            IpAddr::V6(global)
362        );
363        assert_eq!(
364            NextHopAddress::Ipv6LinkLocal(link_local, global).global_addr(),
365            IpAddr::V6(global)
366        );
367        assert_eq!(
368            NextHopAddress::VpnIpv6LinkLocal(rd, link_local, rd, global).global_addr(),
369            IpAddr::V6(global)
370        );
371
372        // a same-scope pair falls back to the first address
373        let second_global: Ipv6Addr = "2001:db8::2".parse().unwrap();
374        let second_link_local: Ipv6Addr = "fe80::2".parse().unwrap();
375        assert_eq!(
376            NextHopAddress::Ipv6LinkLocal(global, second_global).global_addr(),
377            IpAddr::V6(global)
378        );
379        assert_eq!(
380            NextHopAddress::Ipv6LinkLocal(link_local, second_link_local).global_addr(),
381            IpAddr::V6(link_local)
382        );
383    }
384
385    #[test]
386    fn test_next_hop_address_link_local_addr() {
387        let global: Ipv6Addr = "2001:db8::1".parse().unwrap();
388        let link_local: Ipv6Addr = "fe80::1".parse().unwrap();
389
390        // single addresses have no link-local companion
391        assert_eq!(NextHopAddress::Ipv6(global).link_local_addr(), None);
392        assert_eq!(NextHopAddress::Ipv6(link_local).link_local_addr(), None);
393
394        // the link-local half of a pair, regardless of order
395        assert_eq!(
396            NextHopAddress::Ipv6LinkLocal(global, link_local).link_local_addr(),
397            Some(link_local)
398        );
399        assert_eq!(
400            NextHopAddress::Ipv6LinkLocal(link_local, global).link_local_addr(),
401            Some(link_local)
402        );
403
404        // same-scope pairs: the half that global_addr() does not return
405        let second_link_local: Ipv6Addr = "fe80::2".parse().unwrap();
406        assert_eq!(
407            NextHopAddress::Ipv6LinkLocal(link_local, second_link_local).link_local_addr(),
408            Some(second_link_local)
409        );
410        assert_eq!(
411            NextHopAddress::Ipv6LinkLocal(global, "2001:db8::2".parse().unwrap()).link_local_addr(),
412            None
413        );
414    }
415
416    #[test]
417    fn test_route_distinguisher() {
418        let rd = RouteDistinguisher([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]);
419
420        // Test Debug format
421        assert_eq!(format!("{rd:?}"), "RD(00:01:02:03:04:05:06:07)");
422
423        // Test Display format
424        assert_eq!(format!("{rd}"), "00:01:02:03:04:05:06:07");
425    }
426
427    #[test]
428    fn test_vpn_next_hop_address() {
429        let rd = RouteDistinguisher([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]);
430        let ipv6_addr = Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1);
431        let ipv6_link_local = Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1);
432        let rd2 = RouteDistinguisher([0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17]);
433
434        // Test VpnIpv6
435        let vpn_next_hop = NextHopAddress::VpnIpv6(rd, ipv6_addr);
436        assert_eq!(vpn_next_hop.addr(), IpAddr::V6(ipv6_addr));
437        assert!(!vpn_next_hop.is_link_local());
438        assert_eq!(format!("{vpn_next_hop}"), "2001:db8::1");
439        assert_eq!(
440            format!("{vpn_next_hop:?}"),
441            "VpnIpv6(00:01:02:03:04:05:06:07, 2001:db8::1)"
442        );
443
444        // Test VpnIpv6LinkLocal
445        let vpn_ll_next_hop = NextHopAddress::VpnIpv6LinkLocal(rd, ipv6_addr, rd2, ipv6_link_local);
446        assert_eq!(vpn_ll_next_hop.addr(), IpAddr::V6(ipv6_addr));
447        assert!(vpn_ll_next_hop.is_link_local()); // Should return true for VpnIpv6LinkLocal
448        assert_eq!(format!("{vpn_ll_next_hop}"), "2001:db8::1,fe80::1");
449        assert_eq!(format!("{vpn_ll_next_hop:?}"), "VpnIpv6LinkLocal(00:01:02:03:04:05:06:07, 2001:db8::1, 10:11:12:13:14:15:16:17, fe80::1)");
450
451        // Test VpnIpv6 with link-local IP (not VpnIpv6LinkLocal variant)
452        let vpn_ll_ip = NextHopAddress::VpnIpv6(rd, ipv6_link_local);
453        assert!(vpn_ll_ip.is_link_local()); // Should detect link-local from IPv6 address
454    }
455}