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, network, 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    netpol_networks_service: &mut network::NetpolNetworksService,
319    interface_id: InterfaceId,
320    watchers: &mut DnsServerWatchers<'_>,
321    prefixes_streams: &mut PrefixesStreamMap,
322) {
323    let source = DnsServersUpdateSource::Dhcpv6 { interface_id: interface_id.get() };
324
325    // Dropping all fuchsia.net.dhcpv6/Client proxies will stop the DHCPv6 client.
326    if let None = watchers.remove(&source) {
327        // It's surprising that the DNS Watcher for the interface doesn't exist
328        // when the DHCP client is trying to be stopped, but this can happen
329        // when multiple futures try to stop the client at the same time.
330        warn!(
331            "DNS Watcher for key not present; multiple futures stopped DHCPv6 \
332            client for key {:?}; interface_id={}",
333            source, interface_id
334        );
335    }
336    if let None = prefixes_streams.remove(&interface_id) {
337        // It's surprising that the Prefix Stream for the interface doesn't exist
338        // when the DHCP client is trying to be stopped, but this can happen
339        // when multiple futures try to stop the client at the same time.
340        warn!(
341            "Prefix Stream for key not present; multiple futures stopped DHCPv6 \
342            client for key {:?}; interface_id={}",
343            source, interface_id
344        );
345    }
346
347    dns::update_servers(
348        lookup_admin,
349        dns_servers,
350        dns_server_watch_responders,
351        netpol_networks_service,
352        source,
353        vec![],
354    )
355    .await
356}
357
358#[derive(Clone, Copy, PartialEq, Eq, Hash)]
359pub(super) enum AcquirePrefixInterfaceConfig {
360    Upstreams,
361    Id(u64),
362}
363
364pub(super) struct PrefixProviderHandler {
365    pub(super) prefix_control_request_stream: fnet_dhcpv6::PrefixControlRequestStream,
366    pub(super) watch_prefix_responder: Option<fnet_dhcpv6::PrefixControlWatchPrefixResponder>,
367    pub(super) preferred_prefix_len: Option<u8>,
368    /// Interfaces configured to perform PD on.
369    pub(super) interface_config: AcquirePrefixInterfaceConfig,
370    pub(super) current_prefix: Option<PrefixOnInterface>,
371}
372
373impl PrefixProviderHandler {
374    pub(super) fn try_next_prefix_control_request(
375        &mut self,
376    ) -> futures::stream::TryNext<'_, fnet_dhcpv6::PrefixControlRequestStream> {
377        self.prefix_control_request_stream.try_next()
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin;
384
385    use net_declare::{fidl_socket_addr_v6, net_subnet_v6};
386    use test_case::test_case;
387
388    use crate::interface::{generate_identifier, InterfaceNamingIdentifier, ProvisioningAction};
389    use crate::{DeviceClass, HostInterfaceState, InterfaceConfigState, InterfaceState};
390
391    use super::*;
392
393    const ALLOWED_UPSTREAM_DEVICE_CLASS: crate::DeviceClass = crate::DeviceClass::Ethernet;
394    const DISALLOWED_UPSTREAM_DEVICE_CLASS: crate::DeviceClass = crate::DeviceClass::Virtual;
395    const LIFETIMES: Lifetimes = Lifetimes {
396        preferred_until: zx::MonotonicInstant::from_nanos(123_000_000_000),
397        valid_until: zx::MonotonicInstant::from_nanos(456_000_000_000),
398    };
399    const RENEWED_LIFETIMES: Lifetimes = Lifetimes {
400        preferred_until: zx::MonotonicInstant::from_nanos(777_000_000_000),
401        valid_until: zx::MonotonicInstant::from_nanos(888_000_000_000),
402    };
403
404    impl InterfaceState {
405        fn new_host_with_state(
406            interface_naming_id: InterfaceNamingIdentifier,
407            control: fidl_fuchsia_net_interfaces_ext::admin::Control,
408            device_class: DeviceClass,
409            dhcpv6_pd_config: Option<fnet_dhcpv6::PrefixDelegationConfig>,
410            dhcpv6_client_state: Option<ClientState>,
411            provisioning: ProvisioningAction,
412            interface_admin_auth: fnet_interfaces_admin::GrantForInterfaceAuthorization,
413        ) -> Self {
414            Self {
415                control,
416                config: InterfaceConfigState::Host(HostInterfaceState {
417                    dhcpv4_client: crate::Dhcpv4ClientState::NotRunning,
418                    dhcpv6_client_state,
419                    dhcpv6_pd_config,
420                    interface_admin_auth,
421                    interface_naming_id,
422                }),
423                device_class,
424                provisioning,
425            }
426        }
427    }
428
429    fn fake_interface_grant() -> fnet_interfaces_admin::GrantForInterfaceAuthorization {
430        fnet_interfaces_admin::GrantForInterfaceAuthorization {
431            interface_id: 0,
432            token: zx::Event::create(),
433        }
434    }
435
436    #[test_case(
437        None,
438        [
439            (
440                DISALLOWED_UPSTREAM_DEVICE_CLASS,
441                Some(HashMap::from([(net_subnet_v6!("abcd::/64"), LIFETIMES)])),
442            )
443        ].into_iter(),
444        AcquirePrefixInterfaceConfig::Upstreams,
445        None;
446        "not_upstream"
447    )]
448    #[test_case(
449        None,
450        [
451            (
452                ALLOWED_UPSTREAM_DEVICE_CLASS,
453                Some(HashMap::from([(net_subnet_v6!("abcd::/64"), LIFETIMES)])),
454            )
455        ].into_iter(),
456        AcquirePrefixInterfaceConfig::Upstreams,
457        Some(PrefixOnInterface {
458            interface_id: InterfaceId::new(1).unwrap(),
459            prefix: net_subnet_v6!("abcd::/64"),
460            lifetimes: LIFETIMES,
461        });
462        "none_to_some"
463    )]
464    #[test_case(
465        Some(PrefixOnInterface {
466            interface_id: InterfaceId::new(1).unwrap(),
467            prefix: net_subnet_v6!("abcd::/64"),
468            lifetimes: LIFETIMES,
469        }),
470        [
471            (
472                ALLOWED_UPSTREAM_DEVICE_CLASS,
473                Some(HashMap::from([(net_subnet_v6!("abcd::/64"), LIFETIMES)])),
474            )
475        ].into_iter(),
476        AcquirePrefixInterfaceConfig::Upstreams,
477        Some(PrefixOnInterface {
478            interface_id: InterfaceId::new(1).unwrap(),
479            prefix: net_subnet_v6!("abcd::/64"),
480            lifetimes: LIFETIMES,
481        });
482        "same"
483    )]
484    #[test_case(
485        Some(PrefixOnInterface {
486            interface_id: InterfaceId::new(1).unwrap(),
487            prefix: net_subnet_v6!("abcd::/64"),
488            lifetimes: LIFETIMES,
489        }),
490        [
491            (
492                ALLOWED_UPSTREAM_DEVICE_CLASS,
493                Some(HashMap::from([(net_subnet_v6!("abcd::/64"), RENEWED_LIFETIMES)])),
494            )
495        ].into_iter(),
496        AcquirePrefixInterfaceConfig::Upstreams,
497        Some(PrefixOnInterface {
498            interface_id: InterfaceId::new(1).unwrap(),
499            prefix: net_subnet_v6!("abcd::/64"),
500            lifetimes: RENEWED_LIFETIMES,
501        });
502        "lifetime_changed"
503    )]
504    #[test_case(
505        Some(PrefixOnInterface {
506            interface_id: InterfaceId::new(1).unwrap(),
507            prefix: net_subnet_v6!("abcd::/64"),
508            lifetimes: LIFETIMES,
509        }),
510        [
511            (
512                ALLOWED_UPSTREAM_DEVICE_CLASS,
513                Some(HashMap::new()),
514            ),
515            (
516                ALLOWED_UPSTREAM_DEVICE_CLASS,
517                Some(HashMap::from([(net_subnet_v6!("efff::/64"), RENEWED_LIFETIMES)])),
518            )
519        ].into_iter(),
520        AcquirePrefixInterfaceConfig::Upstreams,
521        Some(PrefixOnInterface {
522            interface_id: InterfaceId::new(2).unwrap(),
523            prefix: net_subnet_v6!("efff::/64"),
524            lifetimes: RENEWED_LIFETIMES,
525        });
526        "different_interface"
527    )]
528    #[fuchsia::test]
529    async fn get_suitable_dhcpv6_prefix(
530        current_prefix: Option<PrefixOnInterface>,
531        interface_state_iter: impl IntoIterator<Item = (crate::DeviceClass, Option<Prefixes>)>,
532        interface_config: AcquirePrefixInterfaceConfig,
533        want: Option<PrefixOnInterface>,
534    ) {
535        let interface_states = (1..)
536            .flat_map(InterfaceId::new)
537            .zip(interface_state_iter.into_iter().map(|(device_class, prefixes)| {
538                let (control, _control_server_end) =
539                    fidl_fuchsia_net_interfaces_ext::admin::Control::create_endpoints()
540                        .expect("create endpoints");
541                InterfaceState::new_host_with_state(
542                    generate_identifier(&fidl_fuchsia_net_ext::MacAddress {
543                        octets: [0x1, 0x2, 0x3, 0x4, 0x5, 0x6],
544                    }),
545                    control,
546                    device_class,
547                    None,
548                    prefixes.map(|prefixes| ClientState {
549                        sockaddr: fidl_socket_addr_v6!("[fe80::1]:546"),
550                        prefixes: prefixes,
551                    }),
552                    ProvisioningAction::Local,
553                    fake_interface_grant(),
554                )
555            }))
556            .collect();
557        let allowed_upstream_device_classes = HashSet::from([ALLOWED_UPSTREAM_DEVICE_CLASS]);
558        assert_eq!(
559            super::get_suitable_dhcpv6_prefix(
560                current_prefix,
561                &interface_states,
562                &allowed_upstream_device_classes,
563                interface_config,
564            ),
565            want
566        );
567    }
568}