Skip to main content

erbium/radv/
mod.rs

1/*   Copyright 2024 Perry Lorier
2 *
3 *  Licensed under the Apache License, Version 2.0 (the "License");
4 *  you may not use this file except in compliance with the License.
5 *  You may obtain a copy of the License at
6 *
7 *      http://www.apache.org/licenses/LICENSE-2.0
8 *
9 *  Unless required by applicable law or agreed to in writing, software
10 *  distributed under the License is distributed on an "AS IS" BASIS,
11 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 *  See the License for the specific language governing permissions and
13 *  limitations under the License.
14 *
15 *  SPDX-License-Identifier: Apache-2.0
16 *
17 *  IPv6 Router Advertisement Code
18 */
19
20use erbium_net::addr::{ALL_NODES, ALL_ROUTERS};
21use std::convert::TryInto as _;
22// TODO: erbium_net is the only place that should use nix, so we should migrate the code here that
23// depends on nix to erbium_net, but in the meantime to keep everything consistent we rely on
24// erbium_net's exported version of nix.
25use erbium_net::nix;
26
27pub(crate) mod config;
28pub mod icmppkt;
29
30#[cfg(test)]
31mod test {
32    mod rfc4861;
33}
34
35// RFC4861 Section 6.2.1
36const DEFAULT_MAX_RTR_ADV_INTERVAL: std::time::Duration = std::time::Duration::from_secs(600);
37const DEFAULT_MIN_RTR_ADV_INTERVAL: std::time::Duration =
38    std::time::Duration::from_micros((DEFAULT_MAX_RTR_ADV_INTERVAL.as_micros() / 3) as u64);
39const ADV_DEFAULT_LIFETIME: std::time::Duration =
40    std::time::Duration::from_secs(3 * DEFAULT_MAX_RTR_ADV_INTERVAL.as_secs());
41
42lazy_static::lazy_static! {
43    static ref RADV_RX_PACKETS: prometheus::IntCounterVec =
44        prometheus::register_int_counter_vec!("radv_received_packets", "Number of packets received", &["interface"])
45            .unwrap();
46    static ref RADV_SOLICITATIONS: prometheus::IntCounterVec =
47        prometheus::register_int_counter_vec!("radv_solicitations",
48            "Number of router solicitations received",
49            &["interface"])
50            .unwrap();
51    static ref RADV_TX_PACKETS: prometheus::IntCounterVec =
52        prometheus::register_int_counter_vec!("radv_sent_packets", "Number of packets sent", &["interface"])
53            .unwrap();
54}
55
56pub enum Error {
57    Io(std::io::Error),
58    Message(String),
59    UnconfiguredInterface(String),
60}
61
62impl std::fmt::Display for Error {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        match self {
65            Error::Io(e) => write!(f, "I/O Error: {:?}", e),
66            Error::Message(e) => write!(f, "{}", e),
67            Error::UnconfiguredInterface(int) => write!(
68                f,
69                "No router advertisement configuration for interface {}, ignoring.",
70                int
71            ),
72        }
73    }
74}
75
76/* An uninhabitable type to be clear that this cannot happen */
77enum Void {}
78
79impl std::fmt::Debug for Void {
80    fn fmt(&self, _: &mut std::fmt::Formatter) -> std::fmt::Result {
81        unreachable!()
82    }
83}
84
85pub struct RaAdvService {
86    netinfo: erbium_net::netinfo::SharedNetInfo,
87    conf: crate::config::SharedConfig,
88    rawsock: std::sync::Arc<erbium_net::raw::Raw6Socket>,
89}
90
91#[derive(Eq, PartialEq)]
92struct ScopeSorter(std::net::Ipv6Addr);
93
94#[derive(Eq, PartialEq)]
95enum Scope {
96    Link,
97    Loopback,
98    UniqueLocalAddress,
99    Global,
100    Unspecified,
101    Multicast,
102}
103
104const fn v6_scope(ip6: std::net::Ipv6Addr) -> Scope {
105    use std::net::*;
106    if (ip6.segments()[0] & 0xfe00) == 0xfc00 {
107        Scope::UniqueLocalAddress
108    } else if u128::from_be_bytes(ip6.octets()) == u128::from_be_bytes(Ipv6Addr::LOCALHOST.octets())
109    {
110        Scope::Loopback
111    } else if u128::from_be_bytes(ip6.octets())
112        == u128::from_be_bytes(Ipv6Addr::UNSPECIFIED.octets())
113    {
114        Scope::Unspecified
115    } else if ip6.segments()[0] == 0xfe80
116        && ip6.segments()[1] == 0
117        && ip6.segments()[2] == 0
118        && ip6.segments()[3] == 0
119    {
120        /* Follows the stricter definition in RFC4291 */
121        Scope::Link
122    } else if (ip6.segments()[0] & 0xff00) == 0xff00 {
123        Scope::Multicast
124    } else {
125        Scope::Global
126    }
127}
128
129impl Ord for ScopeSorter {
130    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
131        use Scope::*;
132        /* We prefer UniqueLocalAddress > Global > Link addresses > Other */
133        let scopes = [Scope::Unspecified, Link, Global, UniqueLocalAddress];
134        let sscope = v6_scope(self.0);
135        let oscope = v6_scope(other.0);
136        let sscopepos = scopes
137            .iter()
138            .position(|x| *x == sscope)
139            .unwrap_or(usize::MIN);
140        let oscopepos = scopes
141            .iter()
142            .position(|x| *x == oscope)
143            .unwrap_or(usize::MIN);
144        let ret = sscopepos.cmp(&oscopepos);
145        if ret == std::cmp::Ordering::Equal {
146            /* If the two addresses are the same scope, just compare on addresses */
147            /* We might want to consider other criteria here */
148            self.0.cmp(&other.0)
149        } else {
150            ret
151        }
152    }
153}
154
155impl PartialOrd for ScopeSorter {
156    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
157        Some(self.cmp(other))
158    }
159}
160
161impl RaAdvService {
162    pub fn new(
163        netinfo: erbium_net::netinfo::SharedNetInfo,
164        conf: super::config::SharedConfig,
165    ) -> Result<Self, Error> {
166        let rawsock = std::sync::Arc::new(
167            erbium_net::raw::Raw6Socket::new(erbium_net::raw::IpProto::ICMP6).map_err(Error::Io)?,
168        );
169
170        rawsock
171            .set_socket_option(erbium_net::Ipv6RecvPacketInfo, &true)
172            .map_err(Error::Io)?;
173        //TODO
174        //rawsock
175        //    .set_socket_option(erbium_net::Ipv6UnicastHops, &255)
176        //    .map_err(Error::Io)?;
177        use std::os::unix::io::AsRawFd as _;
178        erbium_net::socket::set_ipv6_unicast_hoplimit(rawsock.as_raw_fd(), 255)
179            .map_err(|e| Error::Io(e.into()))?;
180        //rawsock
181        //    .set_socket_option(erbium_net::Ipv6MulticastHops, &255)
182        //    .map_err(Error::Io)?;
183        erbium_net::socket::set_ipv6_multicast_hoplimit(rawsock.as_raw_fd(), 255)
184            .map_err(|e| Error::Io(e.into()))?;
185        //rawsock
186        //    .set_socket_option(erbium_net::Ipv6RecvHopLimit, &true)
187        //    .map_err(Error::Io)?;
188        //rawsock
189        //    .set_socket_option(erbium_net::Ipv6ImcpFilter, ...)?;
190        //    .map_err(Error::Io)?;
191        rawsock
192            .set_socket_option(
193                erbium_net::Ipv6AddMembership,
194                &nix::sys::socket::Ipv6MembershipRequest::new(ALL_ROUTERS),
195            )
196            .map_err(Error::Io)?;
197
198        Ok(Self {
199            netinfo,
200            conf,
201            rawsock,
202        })
203    }
204
205    fn build_announcement_pure(
206        config: &crate::config::Config,
207        intf: &config::Interface,
208        ll: Option<[u8; 6]>, /* TODO: This only works for ethernet */
209        mtu: Option<u32>,
210        self6: std::net::Ipv6Addr,
211        lifetime: std::time::Duration,
212    ) -> icmppkt::RtrAdvertisement {
213        let mut options = icmppkt::NDOptions::default();
214        /* Add the LL address of the interface, if it exists. */
215        if let Some(lladdr) = ll {
216            options.add_option(icmppkt::NDOptionValue::SourceLLAddr(lladdr.to_vec()));
217        }
218
219        if let Some(mtu) = mtu {
220            options.add_option(icmppkt::NDOptionValue::Mtu(mtu));
221        }
222
223        for prefix in &intf.prefixes {
224            options.add_option(icmppkt::NDOptionValue::Prefix(icmppkt::AdvPrefix {
225                prefixlen: prefix.prefixlen,
226                onlink: prefix.onlink,
227                autonomous: prefix.autonomous,
228                valid: prefix.valid,
229                preferred: prefix.preferred,
230                prefix: prefix.addr,
231            }));
232        }
233
234        if let Some(v) = &intf.rdnss.unwrap_or(
235            config
236                .dns_servers
237                .iter()
238                .filter_map(|ip| match ip {
239                    std::net::IpAddr::V6(ip6) if *ip6 == std::net::Ipv6Addr::UNSPECIFIED => {
240                        Some(self6)
241                    }
242                    std::net::IpAddr::V6(ip6) => Some(*ip6),
243                    _ => None,
244                })
245                .collect(),
246        ) {
247            options.add_option(icmppkt::NDOptionValue::RecursiveDnsServers((
248                intf.rdnss_lifetime
249                    .always_unwrap_or(3 * DEFAULT_MAX_RTR_ADV_INTERVAL),
250                v.clone(),
251            )))
252        }
253
254        if let Some(v) = &intf.dnssl.unwrap_or(config.dns_search.clone()) {
255            options.add_option(icmppkt::NDOptionValue::DnsSearchList((
256                intf.dnssl_lifetime
257                    .always_unwrap_or(3 * DEFAULT_MAX_RTR_ADV_INTERVAL),
258                v.clone(),
259            )))
260        }
261
262        if let Some(pref64) = &intf.pref64 {
263            options.add_option(icmppkt::NDOptionValue::Pref64((
264                pref64.lifetime,
265                pref64.prefixlen,
266                pref64.prefix,
267            )))
268        }
269
270        if let Some(url) = intf
271            .captive_portal
272            .as_ref()
273            .or(config.captive_portal.as_ref())
274        {
275            options.add_option(icmppkt::NDOptionValue::CaptivePortal(url.into()))
276        }
277
278        icmppkt::RtrAdvertisement {
279            hop_limit: intf.hoplimit,
280            flag_managed: intf.managed,
281            flag_other: intf.other,
282            lifetime: intf.lifetime.always_unwrap_or(lifetime),
283            reachable: intf.reachable,
284            retrans: intf.retrans,
285            options,
286        }
287    }
288
289    async fn build_announcement(
290        &self,
291        ifidx: u32,
292        intf: &config::Interface,
293    ) -> icmppkt::RtrAdvertisement {
294        /* Add the LL address of the interface, if it exists. */
295        let ll = match self.netinfo.get_linkaddr_by_ifidx(ifidx).await {
296            Some(erbium_net::netinfo::LinkLayer::Ethernet(lladdr)) => Some(lladdr),
297            _ => None,
298        };
299
300        /* Find the "best" address for an interface.
301         * We prefer UniqueLocalAddress > Global > Link > Other
302         */
303        let ScopeSorter(self6) = self
304            .netinfo
305            .get_prefixes_by_ifidx(ifidx)
306            .await
307            .unwrap() // TODO: Error?
308            .iter()
309            .filter_map(|(addr, _prefixlen)| {
310                if let std::net::IpAddr::V6(ip6) = addr {
311                    Some(ScopeSorter(*ip6))
312                } else {
313                    None
314                }
315            })
316            .max()
317            .unwrap(); /* v6 interfaces always have a linklocal, so we should have found at least one address here */
318
319        /* Let them know the Mtu of the interface */
320        /* We use the value from the config, but if they don't specify one, we just read the Mtu
321         * from the interface and use that.  If they don't want erbium to specify one, then they
322         * can set the value to "null" in the config.
323         */
324        use config::ConfigValue::*;
325        let mtu = match intf.mtu {
326            NotSpecified => self.netinfo.get_mtu_by_ifidx(ifidx).await,
327            Value(v) => Some(v),
328            DontSet => None,
329        };
330
331        /* Now we decide if we should set the lifetime (ie, that this should be used as a default
332         * route)
333         */
334        let lifetime = match intf.lifetime {
335            NotSpecified => {
336                if let Some((_gw, gwif)) = self.netinfo.get_ipv6_default_route().await {
337                    if gwif != Some(ifidx) {
338                        /* TODO: Should also check that forwarding is enabled on ifidx */
339                        ADV_DEFAULT_LIFETIME
340                    } else {
341                        std::time::Duration::from_secs(0)
342                    }
343                } else {
344                    std::time::Duration::from_secs(0)
345                }
346            }
347            Value(v) => v,
348            DontSet => std::time::Duration::from_secs(0),
349        };
350
351        Self::build_announcement_pure(&*self.conf.read().await, intf, ll, mtu, self6, lifetime)
352    }
353
354    async fn build_announcement_by_ifidx(
355        &self,
356        ifidx: u32,
357    ) -> Result<icmppkt::RtrAdvertisement, Error> {
358        let ifname = self.netinfo.get_safe_name_by_ifidx(ifidx).await;
359        if let Some(intf) = self
360            .conf
361            .read()
362            .await
363            .ra
364            .interfaces
365            .iter()
366            .find(|intf| intf.name == ifname)
367        {
368            Ok(self.build_announcement(ifidx, intf).await)
369        } else if let Some(prefixes) = self.netinfo.get_prefixes_by_ifidx(ifidx).await {
370            let addresses = &self.conf.read().await.addresses;
371            let prefixes = prefixes
372                .iter()
373                .filter_map(|(addr, prefixlen)| {
374                    if let std::net::IpAddr::V6(ref ip6) = *addr {
375                        if addresses.contains(&crate::config::Prefix::new(*addr, *prefixlen)) {
376                            Some(config::Prefix {
377                                addr: *ip6,
378                                prefixlen: *prefixlen,
379                                onlink: true,
380                                autonomous: true,
381                                valid: std::time::Duration::from_secs(2592000),
382                                preferred: std::time::Duration::from_secs(604800),
383                            })
384                        } else {
385                            None
386                        }
387                    } else {
388                        None
389                    }
390                })
391                .collect::<Vec<config::Prefix>>();
392            if prefixes.is_empty() {
393                Err(Error::UnconfiguredInterface(ifname))
394            } else {
395                let intf = config::Interface {
396                    // TODO: should we fill in the interface name correctly here?
397                    prefixes,
398                    ..Default::default()
399                };
400                Ok(self.build_announcement(ifidx, &intf).await)
401            }
402        } else {
403            Err(Error::UnconfiguredInterface(ifname))
404        }
405    }
406
407    async fn send_announcement(
408        &self,
409        msg: icmppkt::RtrAdvertisement,
410        dst: erbium_net::addr::NetAddr,
411        intf: u32,
412    ) -> Result<(), Error> {
413        let smsg = icmppkt::Icmp6::RtrAdvert(msg);
414        let s = icmppkt::serialise(&smsg);
415        use erbium_net::socket;
416        let cmsg = if intf != 0 {
417            socket::ControlMessage::new().set_src6_intf(intf)
418        } else {
419            socket::ControlMessage::new()
420        };
421        if let Err(e) = self
422            .rawsock
423            .send_msg(&s, &cmsg, socket::MsgFlags::empty(), Some(&dst))
424            .await
425        {
426            log::warn!(
427                "Failed to send router advertisement for {}(if#{}) ({}): {}",
428                self.netinfo.get_safe_name_by_ifidx(intf).await,
429                intf,
430                dst,
431                e
432            );
433        } else {
434            RADV_TX_PACKETS
435                .with_label_values(&[&self.netinfo.get_safe_name_by_ifidx(intf).await])
436                .inc();
437        }
438        Ok(())
439    }
440
441    async fn handle_solicit(
442        &self,
443        rm: erbium_net::socket::RecvMsg,
444        _in_opt: &icmppkt::NDOptions,
445    ) -> Result<(), Error> {
446        if let Some(ifidx) = rm.local_intf() {
447            if let Some(dst) = rm.address.as_ref() {
448                let ifidx = ifidx.try_into().expect("Interface with ifidx");
449                let reply = self.build_announcement_by_ifidx(ifidx).await?;
450                self.send_announcement(reply, *dst, ifidx).await
451            } else {
452                Err(Error::Io(std::io::Error::other(
453                    "Missing destination address",
454                )))
455            }
456        } else {
457            Err(Error::Io(std::io::Error::other(
458                "Packet missing interface information",
459            )))
460        }
461    }
462
463    async fn send_unsolicited(&self, ifidx: u32) -> Result<(), Error> {
464        let msg = self.build_announcement_by_ifidx(ifidx).await?;
465        let dst = std::net::SocketAddr::V6(std::net::SocketAddrV6::new(
466            ALL_NODES,
467            erbium_net::raw::IpProto::ICMP6.into(), /* port */
468            0,                                      /* flowid */
469            ifidx,                                  /* scope_id */
470        ))
471        .into();
472
473        self.send_announcement(msg, dst, ifidx).await
474    }
475
476    async fn run_unsolicited(&self) -> Result<Void, Error> {
477        use rand::RngExt as _;
478        loop {
479            /* Update the time with jitter */
480            let timeout = std::time::Duration::from_secs(rand::rng().random_range(
481                DEFAULT_MIN_RTR_ADV_INTERVAL.as_secs()..DEFAULT_MAX_RTR_ADV_INTERVAL.as_secs(),
482            ));
483            tokio::time::sleep(timeout).await;
484            for idx in self.netinfo.get_ifindexes().await {
485                if let Some(ifflags) = self.netinfo.get_flags_by_ifidx(idx).await
486                    && ifflags.has_multicast()
487                {
488                    match self.send_unsolicited(idx).await {
489                        Ok(_) => (),
490                        Err(Error::UnconfiguredInterface(_)) => (), // Ignore unconfigured interfaces.
491                        e => e?,
492                    }
493                }
494            }
495        }
496    }
497
498    async fn run_solicited(&self) -> Result<Void, Error> {
499        loop {
500            let rm = match self
501                .rawsock
502                .recv_msg(65536, erbium_net::raw::MsgFlags::empty())
503                .await
504            {
505                Ok(m) => m,
506                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
507                Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
508                Err(e) => return Err(Error::Io(e)),
509            };
510            let ifname = match rm.local_intf() {
511                Some(ifidx) => self.netinfo.get_safe_name_by_ifidx(ifidx as u32).await,
512                None => "<unknown>".into(),
513            };
514            RADV_RX_PACKETS.with_label_values(&[&ifname]).inc();
515            let msg = icmppkt::parse(&rm.buffer);
516            match msg {
517                Ok(icmppkt::Icmp6::Unknown) => (),
518                Err(_) => (),
519                Ok(icmppkt::Icmp6::RtrSolicit(opt)) => {
520                    RADV_SOLICITATIONS.with_label_values(&[&ifname]).inc();
521                    if let Err(e) = self.handle_solicit(rm, &opt).await {
522                        log::warn!("Failed to handle router solicitation: {}", e);
523                    }
524                }
525                Ok(icmppkt::Icmp6::RtrAdvert(_)) => (),
526            }
527        }
528    }
529
530    pub async fn run(self: std::sync::Arc<Self>) -> Result<(), String> {
531        use futures::StreamExt as _;
532        log::info!("Starting Router Advertisement service");
533        let mut services = futures::stream::FuturesUnordered::new();
534        let sol_self = self.clone();
535        let unsol_self = self.clone();
536        let sol = async move { sol_self.run_solicited().await };
537        let unsol = async move { unsol_self.run_unsolicited().await };
538        services.push(tokio::spawn(sol));
539        services.push(tokio::spawn(unsol));
540        while !services.is_empty() {
541            let ret = match services.next().await {
542                None => "No router advertisement services found".into(),
543                Some(Ok(Ok(v))) => format!(
544                    "Router advertisement service unexpectedly exited successfully: {:?}",
545                    v
546                ),
547                Some(Ok(Err(e))) => e.to_string(), /* If the service failed */
548                Some(Err(e)) => e.to_string(),     /* If the spawn failed */
549            };
550            log::error!("Router advertisement service shutdown: {}", ret);
551        }
552        Err("Router advertisement service shutdown".into())
553    }
554}
555
556#[cfg(test)]
557use crate::config::ConfigValue;
558
559#[test]
560fn test_build_announcement() {
561    let conf = crate::config::Config::default();
562    let msg = RaAdvService::build_announcement_pure(
563        &conf,
564        &config::Interface {
565            name: "eth0".into(),
566            hoplimit: 64,
567            managed: false,
568            other: false,
569            lifetime: ConfigValue::Value(std::time::Duration::from_secs(3600)),
570            reachable: std::time::Duration::from_secs(1800),
571            retrans: std::time::Duration::from_secs(10),
572            mtu: config::ConfigValue::NotSpecified,
573            min_rtr_adv_interval: ConfigValue::Value(std::time::Duration::from_secs(200)),
574            max_rtr_adv_interval: ConfigValue::Value(std::time::Duration::from_secs(600)),
575            prefixes: vec![config::Prefix {
576                addr: "2001:db8::".parse().unwrap(),
577                prefixlen: 64,
578                onlink: true,
579                autonomous: true,
580                valid: std::time::Duration::from_secs(3600),
581                preferred: std::time::Duration::from_secs(1800),
582            }],
583            rdnss_lifetime: config::ConfigValue::Value(std::time::Duration::from_secs(3600)),
584            rdnss: config::ConfigValue::Value(vec!["2001:db8::53".parse().unwrap()]),
585            dnssl_lifetime: config::ConfigValue::Value(std::time::Duration::from_secs(3600)),
586            dnssl: config::ConfigValue::Value(vec![]),
587            captive_portal: config::ConfigValue::Value("http://example.com/".into()),
588            pref64: Some(config::Pref64 {
589                lifetime: std::time::Duration::from_secs(600),
590                prefix: "64:ff9b::".parse().unwrap(),
591                prefixlen: 96,
592            }),
593        },
594        Some([1, 2, 3, 4, 5, 6]),
595        Some(1480),
596        std::net::Ipv6Addr::UNSPECIFIED,
597        ADV_DEFAULT_LIFETIME,
598    );
599    icmppkt::serialise(&icmppkt::Icmp6::RtrAdvert(msg));
600}
601
602#[test]
603fn test_default_values() {
604    let conf = crate::config::Config {
605        dns_servers: vec![
606            "192.0.2.53".parse().unwrap(),
607            "2001:db8::53".parse().unwrap(),
608        ],
609        dns_search: vec!["example.com".into()],
610        captive_portal: Some("example.com".into()),
611        ..Default::default()
612    };
613    let msg = RaAdvService::build_announcement_pure(
614        &conf,
615        &config::Interface {
616            dnssl: config::ConfigValue::NotSpecified,
617            rdnss: config::ConfigValue::NotSpecified,
618            captive_portal: config::ConfigValue::NotSpecified,
619            ..Default::default()
620        },
621        Some([1, 2, 3, 4, 5, 6]),
622        Some(1480),
623        std::net::Ipv6Addr::UNSPECIFIED,
624        ADV_DEFAULT_LIFETIME,
625    );
626    assert_eq!(
627        msg.options
628            .find_option(icmppkt::RDNSS)
629            .iter()
630            .map(
631                |x| if let icmppkt::NDOptionValue::RecursiveDnsServers((_, servers)) = x {
632                    servers
633                } else {
634                    panic!("bad")
635                }
636            )
637            .cloned()
638            .collect::<Vec<Vec<_>>>(),
639        vec![vec!["2001:db8::53".parse::<std::net::Ipv6Addr>().unwrap()]]
640    );
641    assert_eq!(
642        msg.options
643            .find_option(icmppkt::DNSSL)
644            .iter()
645            .map(
646                |x| if let icmppkt::NDOptionValue::DnsSearchList((_, domains)) = x {
647                    domains
648                } else {
649                    panic!("bad")
650                }
651            )
652            .cloned()
653            .collect::<Vec<Vec<_>>>(),
654        vec![vec![String::from("example.com")]]
655    );
656    assert_eq!(
657        msg.options
658            .find_option(icmppkt::CAPTIVE_PORTAL)
659            .iter()
660            .map(
661                |x| if let icmppkt::NDOptionValue::CaptivePortal(domain) = x {
662                    domain
663                } else {
664                    panic!("bad")
665                }
666            )
667            .cloned()
668            .collect::<Vec<_>>(),
669        vec![String::from("example.com")]
670    );
671}
672
673#[test]
674fn test_dontset_values() {
675    let conf = crate::config::Config {
676        dns_servers: vec![
677            "192.0.2.53".parse().unwrap(),
678            "2001:db8::53".parse().unwrap(),
679        ],
680        dns_search: vec!["example.com".into()],
681        captive_portal: Some("example.com".into()),
682        ..Default::default()
683    };
684    let msg = RaAdvService::build_announcement_pure(
685        &conf,
686        &config::Interface {
687            dnssl: config::ConfigValue::DontSet,
688            rdnss: config::ConfigValue::DontSet,
689            captive_portal: config::ConfigValue::DontSet,
690            ..Default::default()
691        },
692        Some([1, 2, 3, 4, 5, 6]),
693        Some(1480),
694        std::net::Ipv6Addr::UNSPECIFIED,
695        ADV_DEFAULT_LIFETIME,
696    );
697    assert!(msg.options.find_option(icmppkt::RDNSS).is_empty());
698    assert!(msg.options.find_option(icmppkt::DNSSL).is_empty());
699    assert!(msg.options.find_option(icmppkt::CAPTIVE_PORTAL).is_empty());
700}