netcfg/
dhcpv6.rs

1// Copyright 2020 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use std::collections::{HashMap, HashSet};
6
7use {
8    fidl_fuchsia_net as fnet, fidl_fuchsia_net_dhcpv6 as fnet_dhcpv6,
9    fidl_fuchsia_net_dhcpv6_ext as fnet_dhcpv6_ext, fidl_fuchsia_net_ext as fnet_ext,
10    fidl_fuchsia_net_name as fnet_name,
11};
12
13use anyhow::Context as _;
14use async_utils::hanging_get::client::HangingGetStream;
15use async_utils::stream::{StreamMap, Tagged};
16use dns_server_watcher::{DnsServers, DnsServersUpdateSource};
17use futures::future::TryFutureExt as _;
18use futures::stream::{Stream, TryStreamExt as _};
19use log::warn;
20
21use crate::{dns, errors, DnsServerWatchers, InterfaceId};
22
23// TODO(https://fxbug.dev/329099228): Switch to using DUID-LLT and persisting it to disk.
24pub(super) fn duid(mac: fnet_ext::MacAddress) -> fnet_dhcpv6::Duid {
25    fnet_dhcpv6::Duid::LinkLayerAddress(fnet_dhcpv6::LinkLayerAddress::Ethernet(mac.into()))
26}
27
28#[derive(Copy, Clone, Debug, PartialEq)]
29pub(super) struct PrefixOnInterface {
30    interface_id: InterfaceId,
31    prefix: net_types::ip::Subnet<net_types::ip::Ipv6Addr>,
32    lifetimes: Lifetimes,
33}
34
35pub(super) type Prefixes = HashMap<net_types::ip::Subnet<net_types::ip::Ipv6Addr>, Lifetimes>;
36pub(super) type InterfaceIdTaggedPrefixesStream = Tagged<InterfaceId, PrefixesStream>;
37pub(super) type PrefixesStreamMap = StreamMap<InterfaceId, InterfaceIdTaggedPrefixesStream>;
38
39#[derive(Debug)]
40pub(super) struct ClientState {
41    pub(super) sockaddr: fnet::Ipv6SocketAddress,
42    pub(super) prefixes: Prefixes,
43}
44
45impl ClientState {
46    pub(super) fn new(sockaddr: fnet::Ipv6SocketAddress) -> Self {
47        Self { sockaddr, prefixes: Default::default() }
48    }
49}
50
51#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
52pub(super) struct Lifetimes {
53    preferred_until: zx::MonotonicInstant,
54    valid_until: zx::MonotonicInstant,
55}
56
57impl Into<fnet_dhcpv6::Lifetimes> for Lifetimes {
58    fn into(self) -> fnet_dhcpv6::Lifetimes {
59        let Self { preferred_until, valid_until } = self;
60        fnet_dhcpv6::Lifetimes {
61            preferred_until: preferred_until.into_nanos(),
62            valid_until: valid_until.into_nanos(),
63        }
64    }
65}
66
67pub(super) type PrefixesStream =
68    HangingGetStream<fnet_dhcpv6::ClientProxy, Vec<fnet_dhcpv6::Prefix>>;
69
70pub(super) fn from_fidl_prefixes(
71    fidl_prefixes: &[fnet_dhcpv6::Prefix],
72) -> Result<Prefixes, anyhow::Error> {
73    let prefixes = fidl_prefixes
74        .iter()
75        .map(
76            |&fnet_dhcpv6::Prefix {
77                 prefix:
78                     fnet::Ipv6AddressWithPrefix { addr: fnet::Ipv6Address { addr }, prefix_len },
79                 lifetimes: fnet_dhcpv6::Lifetimes { valid_until, preferred_until },
80             }| {
81                let subnet = net_types::ip::Subnet::new(
82                    net_types::ip::Ipv6Addr::from_bytes(addr),
83                    prefix_len,
84                )
85                .map_err(|e| anyhow::anyhow!("subnet parsing error: {:?}", e))?;
86                if valid_until == 0 {
87                    return Err(anyhow::anyhow!(
88                        "received DHCPv6 prefix {:?} with valid-until time of 0",
89                        subnet
90                    ));
91                }
92                if preferred_until == 0 {
93                    return Err(anyhow::anyhow!(
94                        "received DHCPv6 prefix {:?} with preferred-until time of 0",
95                        subnet
96                    ));
97                }
98                Ok((
99                    subnet,
100                    Lifetimes {
101                        valid_until: zx::MonotonicInstant::from_nanos(valid_until),
102                        preferred_until: zx::MonotonicInstant::from_nanos(preferred_until),
103                    },
104                ))
105            },
106        )
107        .collect::<Result<Prefixes, _>>()?;
108    if prefixes.len() != fidl_prefixes.len() {
109        return Err(anyhow::anyhow!(
110            "DHCPv6 prefixes {:?} contains duplicate prefix",
111            fidl_prefixes
112        ));
113    }
114    Ok(prefixes)
115}
116
117/// Start a DHCPv6 client for the specified host interface.
118pub(super) fn start_client(
119    dhcpv6_client_provider: &fnet_dhcpv6::ClientProviderProxy,
120    interface_id: InterfaceId,
121    sockaddr: fnet::Ipv6SocketAddress,
122    duid: fnet_dhcpv6::Duid,
123    prefix_delegation_config: Option<fnet_dhcpv6::PrefixDelegationConfig>,
124) -> Result<
125    (impl Stream<Item = Result<Vec<fnet_name::DnsServer_>, fidl::Error>>, PrefixesStream),
126    errors::Error,
127> {
128    let stateful = prefix_delegation_config.is_some();
129    let params = fnet_dhcpv6_ext::NewClientParams {
130        interface_id: interface_id.get(),
131        address: sockaddr,
132        config: fnet_dhcpv6_ext::ClientConfig {
133            information_config: fnet_dhcpv6_ext::InformationConfig { dns_servers: true },
134            non_temporary_address_config: Default::default(),
135            prefix_delegation_config,
136        },
137        duid: stateful.then_some(duid),
138    };
139    let (client, server) = fidl::endpoints::create_proxy::<fnet_dhcpv6::ClientMarker>();
140
141    // Not all environments may have a DHCPv6 client service so we consider this a
142    // non-fatal error.
143    dhcpv6_client_provider
144        .new_client(&params.into(), server)
145        .context("error creating new DHCPv6 client")
146        .map_err(errors::Error::NonFatal)?;
147
148    let dns_servers_stream = futures::stream::try_unfold(client.clone(), move |proxy| {
149        proxy.watch_servers().map_ok(move |s| Some((s, proxy)))
150    });
151    let prefixes_stream =
152        PrefixesStream::new_eager_with_fn_ptr(client, fnet_dhcpv6::ClientProxy::watch_prefixes);
153
154    Ok((dns_servers_stream, prefixes_stream))
155}
156
157fn get_suitable_dhcpv6_prefix(
158    current_prefix: Option<PrefixOnInterface>,
159    interface_states: &HashMap<InterfaceId, crate::InterfaceState>,
160    allowed_upstream_device_classes: &HashSet<crate::DeviceClass>,
161    interface_config: AcquirePrefixInterfaceConfig,
162) -> Option<PrefixOnInterface> {
163    if let Some(PrefixOnInterface { interface_id, prefix, lifetimes: _ }) = current_prefix {
164        let crate::InterfaceState { config, .. } =
165            interface_states.get(&interface_id).unwrap_or_else(|| {
166                panic!(
167                    "interface {} cannot be found but provides current prefix = {:?}",
168                    interface_id, current_prefix,
169                )
170            });
171        match config {
172            crate::InterfaceConfigState::Host(crate::HostInterfaceState {
173                dhcpv4_client: _,
174                dhcpv6_client_state,
175                dhcpv6_pd_config: _,
176                interface_admin_auth: _,
177                interface_naming_id: _,
178            }) => {
179                let Some(ClientState { prefixes, sockaddr: _ }) = dhcpv6_client_state.as_ref()
180                else {
181                    // It's surprising that the interface doesn't have an active DHCPv6 client
182                    // but has a DHCPv6 prefix, but this can happen during interface teardown.
183                    return None;
184                };
185                if let Some(lifetimes) = prefixes.get(&prefix) {
186                    return Some(PrefixOnInterface { interface_id, prefix, lifetimes: *lifetimes });
187                }
188            }
189            crate::InterfaceConfigState::WlanAp(wlan_ap_state) => {
190                panic!(
191                    "interface {} not expected to be WLAN AP with state: {:?}",
192                    interface_id, wlan_ap_state,
193                );
194            }
195            crate::InterfaceConfigState::Blackhole(state) => {
196                panic!(
197                    "interface {} not expected to be blackhole with state: {:?}",
198                    interface_id, state,
199                );
200            }
201        }
202    }
203
204    interface_states
205        .iter()
206        .filter_map(|(interface_id, crate::InterfaceState { config, device_class, .. })| {
207            let prefixes = match config {
208                crate::InterfaceConfigState::Host(crate::HostInterfaceState {
209                    dhcpv4_client: _,
210                    dhcpv6_client_state,
211                    dhcpv6_pd_config: _,
212                    interface_admin_auth: _,
213                    interface_naming_id: _,
214                }) => {
215                    if let Some(ClientState { prefixes, sockaddr: _ }) = dhcpv6_client_state {
216                        prefixes
217                    } else {
218                        return None;
219                    }
220                }
221                crate::InterfaceConfigState::WlanAp(crate::WlanApInterfaceState {
222                    interface_naming_id: _,
223                })
224                | crate::InterfaceConfigState::Blackhole(_) => {
225                    return None;
226                }
227            };
228            match interface_config {
229                AcquirePrefixInterfaceConfig::Upstreams => {
230                    allowed_upstream_device_classes.contains(&device_class)
231                }
232                AcquirePrefixInterfaceConfig::Id(want_id) => interface_id.get() == want_id,
233            }
234            .then(|| {
235                prefixes.iter().map(|(&prefix, &lifetimes)| PrefixOnInterface {
236                    interface_id: *interface_id,
237                    prefix,
238                    lifetimes,
239                })
240            })
241        })
242        .flatten()
243        .max_by(
244            |PrefixOnInterface {
245                 interface_id: _,
246                 prefix: _,
247                 lifetimes:
248                     Lifetimes { preferred_until: preferred_until1, valid_until: valid_until1 },
249             },
250             PrefixOnInterface {
251                 interface_id: _,
252                 prefix: _,
253                 lifetimes:
254                     Lifetimes { preferred_until: preferred_until2, valid_until: valid_until2 },
255             }| {
256                // Prefer prefixes with the highest preferred lifetime then
257                // valid lifetime.
258                (preferred_until1, valid_until1).cmp(&(preferred_until2, valid_until2))
259            },
260        )
261}
262
263pub(super) fn maybe_send_watch_prefix_response(
264    interface_states: &HashMap<InterfaceId, crate::InterfaceState>,
265    allowed_upstream_device_classes: &HashSet<crate::DeviceClass>,
266    prefix_provider_handler: Option<&mut PrefixProviderHandler>,
267) -> Result<(), anyhow::Error> {
268    let PrefixProviderHandler {
269        current_prefix,
270        interface_config,
271        preferred_prefix_len: _,
272        watch_prefix_responder,
273        prefix_control_request_stream: _,
274    } = if let Some(handler) = prefix_provider_handler {
275        handler
276    } else {
277        return Ok(());
278    };
279
280    let new_prefix = get_suitable_dhcpv6_prefix(
281        *current_prefix,
282        interface_states,
283        allowed_upstream_device_classes,
284        *interface_config,
285    );
286    if new_prefix == *current_prefix {
287        return Ok(());
288    }
289
290    if let Some(responder) = watch_prefix_responder.take() {
291        responder
292            .send(&new_prefix.map_or(
293                fnet_dhcpv6::PrefixEvent::Unassigned(fnet_dhcpv6::Empty),
294                |PrefixOnInterface { interface_id: _, prefix, lifetimes }| {
295                    fnet_dhcpv6::PrefixEvent::Assigned(fnet_dhcpv6::Prefix {
296                        prefix: fnet::Ipv6AddressWithPrefix {
297                            addr: fnet::Ipv6Address { addr: prefix.network().ipv6_bytes() },
298                            prefix_len: prefix.prefix(),
299                        },
300                        lifetimes: lifetimes.into(),
301                    })
302                },
303            ))
304            .context("failed to send PrefixControl.WatchPrefix response")?;
305        *current_prefix = new_prefix;
306    }
307
308    Ok(())
309}
310
311/// Stops the DHCPv6 client running on the specified host interface.
312///
313/// Any DNS servers learned by the client will be cleared.
314pub(super) async fn stop_client(
315    lookup_admin: &fnet_name::LookupAdminProxy,
316    dns_servers: &mut DnsServers,
317    dns_server_watch_responders: &mut dns::DnsServerWatchResponders,
318    interface_id: InterfaceId,
319    watchers: &mut DnsServerWatchers<'_>,
320    prefixes_streams: &mut PrefixesStreamMap,
321) {
322    let source = DnsServersUpdateSource::Dhcpv6 { interface_id: interface_id.get() };
323
324    // Dropping all fuchsia.net.dhcpv6/Client proxies will stop the DHCPv6 client.
325    if let None = watchers.remove(&source) {
326        // It's surprising that the DNS Watcher for the interface doesn't exist
327        // when the DHCP client is trying to be stopped, but this can happen
328        // when multiple futures try to stop the client at the same time.
329        warn!(
330            "DNS Watcher for key not present; multiple futures stopped DHCPv6 \
331            client for key {:?}; interface_id={}",
332            source, interface_id
333        );
334    }
335    if let None = prefixes_streams.remove(&interface_id) {
336        // It's surprising that the Prefix Stream for the interface doesn't exist
337        // when the DHCP client is trying to be stopped, but this can happen
338        // when multiple futures try to stop the client at the same time.
339        warn!(
340            "Prefix Stream for key not present; multiple futures stopped DHCPv6 \
341            client for key {:?}; interface_id={}",
342            source, interface_id
343        );
344    }
345
346    dns::update_servers(lookup_admin, dns_servers, dns_server_watch_responders, source, vec![])
347        .await
348}
349
350#[derive(Clone, Copy, PartialEq, Eq, Hash)]
351pub(super) enum AcquirePrefixInterfaceConfig {
352    Upstreams,
353    Id(u64),
354}
355
356pub(super) struct PrefixProviderHandler {
357    pub(super) prefix_control_request_stream: fnet_dhcpv6::PrefixControlRequestStream,
358    pub(super) watch_prefix_responder: Option<fnet_dhcpv6::PrefixControlWatchPrefixResponder>,
359    pub(super) preferred_prefix_len: Option<u8>,
360    /// Interfaces configured to perform PD on.
361    pub(super) interface_config: AcquirePrefixInterfaceConfig,
362    pub(super) current_prefix: Option<PrefixOnInterface>,
363}
364
365impl PrefixProviderHandler {
366    pub(super) fn try_next_prefix_control_request(
367        &mut self,
368    ) -> futures::stream::TryNext<'_, fnet_dhcpv6::PrefixControlRequestStream> {
369        self.prefix_control_request_stream.try_next()
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin;
376
377    use net_declare::{fidl_socket_addr_v6, net_subnet_v6};
378    use test_case::test_case;
379
380    use crate::interface::{generate_identifier, InterfaceNamingIdentifier, ProvisioningAction};
381    use crate::{DeviceClass, HostInterfaceState, InterfaceConfigState, InterfaceState};
382
383    use super::*;
384
385    const ALLOWED_UPSTREAM_DEVICE_CLASS: crate::DeviceClass = crate::DeviceClass::Ethernet;
386    const DISALLOWED_UPSTREAM_DEVICE_CLASS: crate::DeviceClass = crate::DeviceClass::Virtual;
387    const LIFETIMES: Lifetimes = Lifetimes {
388        preferred_until: zx::MonotonicInstant::from_nanos(123_000_000_000),
389        valid_until: zx::MonotonicInstant::from_nanos(456_000_000_000),
390    };
391    const RENEWED_LIFETIMES: Lifetimes = Lifetimes {
392        preferred_until: zx::MonotonicInstant::from_nanos(777_000_000_000),
393        valid_until: zx::MonotonicInstant::from_nanos(888_000_000_000),
394    };
395
396    impl InterfaceState {
397        fn new_host_with_state(
398            interface_naming_id: InterfaceNamingIdentifier,
399            control: fidl_fuchsia_net_interfaces_ext::admin::Control,
400            device_class: DeviceClass,
401            dhcpv6_pd_config: Option<fnet_dhcpv6::PrefixDelegationConfig>,
402            dhcpv6_client_state: Option<ClientState>,
403            provisioning: ProvisioningAction,
404            interface_admin_auth: fnet_interfaces_admin::GrantForInterfaceAuthorization,
405        ) -> Self {
406            Self {
407                control,
408                config: InterfaceConfigState::Host(HostInterfaceState {
409                    dhcpv4_client: crate::Dhcpv4ClientState::NotRunning,
410                    dhcpv6_client_state,
411                    dhcpv6_pd_config,
412                    interface_admin_auth,
413                    interface_naming_id,
414                }),
415                device_class,
416                provisioning,
417            }
418        }
419    }
420
421    fn fake_interface_grant() -> fnet_interfaces_admin::GrantForInterfaceAuthorization {
422        fnet_interfaces_admin::GrantForInterfaceAuthorization {
423            interface_id: 0,
424            token: zx::Event::create(),
425        }
426    }
427
428    #[test_case(
429        None,
430        [
431            (
432                DISALLOWED_UPSTREAM_DEVICE_CLASS,
433                Some(HashMap::from([(net_subnet_v6!("abcd::/64"), LIFETIMES)])),
434            )
435        ].into_iter(),
436        AcquirePrefixInterfaceConfig::Upstreams,
437        None;
438        "not_upstream"
439    )]
440    #[test_case(
441        None,
442        [
443            (
444                ALLOWED_UPSTREAM_DEVICE_CLASS,
445                Some(HashMap::from([(net_subnet_v6!("abcd::/64"), LIFETIMES)])),
446            )
447        ].into_iter(),
448        AcquirePrefixInterfaceConfig::Upstreams,
449        Some(PrefixOnInterface {
450            interface_id: InterfaceId::new(1).unwrap(),
451            prefix: net_subnet_v6!("abcd::/64"),
452            lifetimes: LIFETIMES,
453        });
454        "none_to_some"
455    )]
456    #[test_case(
457        Some(PrefixOnInterface {
458            interface_id: InterfaceId::new(1).unwrap(),
459            prefix: net_subnet_v6!("abcd::/64"),
460            lifetimes: LIFETIMES,
461        }),
462        [
463            (
464                ALLOWED_UPSTREAM_DEVICE_CLASS,
465                Some(HashMap::from([(net_subnet_v6!("abcd::/64"), LIFETIMES)])),
466            )
467        ].into_iter(),
468        AcquirePrefixInterfaceConfig::Upstreams,
469        Some(PrefixOnInterface {
470            interface_id: InterfaceId::new(1).unwrap(),
471            prefix: net_subnet_v6!("abcd::/64"),
472            lifetimes: LIFETIMES,
473        });
474        "same"
475    )]
476    #[test_case(
477        Some(PrefixOnInterface {
478            interface_id: InterfaceId::new(1).unwrap(),
479            prefix: net_subnet_v6!("abcd::/64"),
480            lifetimes: LIFETIMES,
481        }),
482        [
483            (
484                ALLOWED_UPSTREAM_DEVICE_CLASS,
485                Some(HashMap::from([(net_subnet_v6!("abcd::/64"), RENEWED_LIFETIMES)])),
486            )
487        ].into_iter(),
488        AcquirePrefixInterfaceConfig::Upstreams,
489        Some(PrefixOnInterface {
490            interface_id: InterfaceId::new(1).unwrap(),
491            prefix: net_subnet_v6!("abcd::/64"),
492            lifetimes: RENEWED_LIFETIMES,
493        });
494        "lifetime_changed"
495    )]
496    #[test_case(
497        Some(PrefixOnInterface {
498            interface_id: InterfaceId::new(1).unwrap(),
499            prefix: net_subnet_v6!("abcd::/64"),
500            lifetimes: LIFETIMES,
501        }),
502        [
503            (
504                ALLOWED_UPSTREAM_DEVICE_CLASS,
505                Some(HashMap::new()),
506            ),
507            (
508                ALLOWED_UPSTREAM_DEVICE_CLASS,
509                Some(HashMap::from([(net_subnet_v6!("efff::/64"), RENEWED_LIFETIMES)])),
510            )
511        ].into_iter(),
512        AcquirePrefixInterfaceConfig::Upstreams,
513        Some(PrefixOnInterface {
514            interface_id: InterfaceId::new(2).unwrap(),
515            prefix: net_subnet_v6!("efff::/64"),
516            lifetimes: RENEWED_LIFETIMES,
517        });
518        "different_interface"
519    )]
520    #[fuchsia::test]
521    async fn get_suitable_dhcpv6_prefix(
522        current_prefix: Option<PrefixOnInterface>,
523        interface_state_iter: impl IntoIterator<Item = (crate::DeviceClass, Option<Prefixes>)>,
524        interface_config: AcquirePrefixInterfaceConfig,
525        want: Option<PrefixOnInterface>,
526    ) {
527        let interface_states = (1..)
528            .flat_map(InterfaceId::new)
529            .zip(interface_state_iter.into_iter().map(|(device_class, prefixes)| {
530                let (control, _control_server_end) =
531                    fidl_fuchsia_net_interfaces_ext::admin::Control::create_endpoints()
532                        .expect("create endpoints");
533                InterfaceState::new_host_with_state(
534                    generate_identifier(&fidl_fuchsia_net_ext::MacAddress {
535                        octets: [0x1, 0x2, 0x3, 0x4, 0x5, 0x6],
536                    }),
537                    control,
538                    device_class,
539                    None,
540                    prefixes.map(|prefixes| ClientState {
541                        sockaddr: fidl_socket_addr_v6!("[fe80::1]:546"),
542                        prefixes: prefixes,
543                    }),
544                    ProvisioningAction::Local,
545                    fake_interface_grant(),
546                )
547            }))
548            .collect();
549        let allowed_upstream_device_classes = HashSet::from([ALLOWED_UPSTREAM_DEVICE_CLASS]);
550        assert_eq!(
551            super::get_suitable_dhcpv6_prefix(
552                current_prefix,
553                &interface_states,
554                &allowed_upstream_device_classes,
555                interface_config,
556            ),
557            want
558        );
559    }
560}