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::{DnsServerWatchers, InterfaceId, dns, errors, network};
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>> + use<>, 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_resources as fnet_resources;
384    use net_declare::{fidl_socket_addr_v6, net_subnet_v6};
385    use test_case::test_case;
386
387    use crate::interface::{InterfaceNamingIdentifier, ProvisioningType, generate_identifier};
388    use crate::{DeviceClass, HostInterfaceState, InterfaceConfigState, InterfaceState};
389
390    use super::*;
391
392    const ALLOWED_UPSTREAM_DEVICE_CLASS: crate::DeviceClass = crate::DeviceClass::Ethernet;
393    const DISALLOWED_UPSTREAM_DEVICE_CLASS: crate::DeviceClass = crate::DeviceClass::Virtual;
394    const LIFETIMES: Lifetimes = Lifetimes {
395        preferred_until: zx::MonotonicInstant::from_nanos(123_000_000_000),
396        valid_until: zx::MonotonicInstant::from_nanos(456_000_000_000),
397    };
398    const RENEWED_LIFETIMES: Lifetimes = Lifetimes {
399        preferred_until: zx::MonotonicInstant::from_nanos(777_000_000_000),
400        valid_until: zx::MonotonicInstant::from_nanos(888_000_000_000),
401    };
402
403    impl InterfaceState {
404        fn new_host_with_state(
405            interface_naming_id: InterfaceNamingIdentifier,
406            control: fidl_fuchsia_net_interfaces_ext::admin::Control,
407            device_class: DeviceClass,
408            dhcpv6_pd_config: Option<fnet_dhcpv6::PrefixDelegationConfig>,
409            dhcpv6_client_state: Option<ClientState>,
410            provisioning: ProvisioningType,
411            interface_admin_auth: fnet_resources::GrantForInterfaceAuthorization,
412        ) -> Self {
413            Self {
414                control,
415                config: InterfaceConfigState::Host(HostInterfaceState {
416                    dhcpv4_client: crate::Dhcpv4ClientState::NotRunning,
417                    dhcpv6_client_state,
418                    dhcpv6_pd_config,
419                    interface_admin_auth,
420                    interface_naming_id,
421                }),
422                device_class,
423                provisioning,
424            }
425        }
426    }
427
428    fn fake_interface_grant() -> fnet_resources::GrantForInterfaceAuthorization {
429        fnet_resources::GrantForInterfaceAuthorization {
430            interface_id: 0,
431            token: zx::Event::create(),
432        }
433    }
434
435    #[test_case(
436        None,
437        [
438            (
439                DISALLOWED_UPSTREAM_DEVICE_CLASS,
440                Some(HashMap::from([(net_subnet_v6!("abcd::/64"), LIFETIMES)])),
441            )
442        ].into_iter(),
443        AcquirePrefixInterfaceConfig::Upstreams,
444        None;
445        "not_upstream"
446    )]
447    #[test_case(
448        None,
449        [
450            (
451                ALLOWED_UPSTREAM_DEVICE_CLASS,
452                Some(HashMap::from([(net_subnet_v6!("abcd::/64"), LIFETIMES)])),
453            )
454        ].into_iter(),
455        AcquirePrefixInterfaceConfig::Upstreams,
456        Some(PrefixOnInterface {
457            interface_id: InterfaceId::new(1).unwrap(),
458            prefix: net_subnet_v6!("abcd::/64"),
459            lifetimes: LIFETIMES,
460        });
461        "none_to_some"
462    )]
463    #[test_case(
464        Some(PrefixOnInterface {
465            interface_id: InterfaceId::new(1).unwrap(),
466            prefix: net_subnet_v6!("abcd::/64"),
467            lifetimes: LIFETIMES,
468        }),
469        [
470            (
471                ALLOWED_UPSTREAM_DEVICE_CLASS,
472                Some(HashMap::from([(net_subnet_v6!("abcd::/64"), LIFETIMES)])),
473            )
474        ].into_iter(),
475        AcquirePrefixInterfaceConfig::Upstreams,
476        Some(PrefixOnInterface {
477            interface_id: InterfaceId::new(1).unwrap(),
478            prefix: net_subnet_v6!("abcd::/64"),
479            lifetimes: LIFETIMES,
480        });
481        "same"
482    )]
483    #[test_case(
484        Some(PrefixOnInterface {
485            interface_id: InterfaceId::new(1).unwrap(),
486            prefix: net_subnet_v6!("abcd::/64"),
487            lifetimes: LIFETIMES,
488        }),
489        [
490            (
491                ALLOWED_UPSTREAM_DEVICE_CLASS,
492                Some(HashMap::from([(net_subnet_v6!("abcd::/64"), RENEWED_LIFETIMES)])),
493            )
494        ].into_iter(),
495        AcquirePrefixInterfaceConfig::Upstreams,
496        Some(PrefixOnInterface {
497            interface_id: InterfaceId::new(1).unwrap(),
498            prefix: net_subnet_v6!("abcd::/64"),
499            lifetimes: RENEWED_LIFETIMES,
500        });
501        "lifetime_changed"
502    )]
503    #[test_case(
504        Some(PrefixOnInterface {
505            interface_id: InterfaceId::new(1).unwrap(),
506            prefix: net_subnet_v6!("abcd::/64"),
507            lifetimes: LIFETIMES,
508        }),
509        [
510            (
511                ALLOWED_UPSTREAM_DEVICE_CLASS,
512                Some(HashMap::new()),
513            ),
514            (
515                ALLOWED_UPSTREAM_DEVICE_CLASS,
516                Some(HashMap::from([(net_subnet_v6!("efff::/64"), RENEWED_LIFETIMES)])),
517            )
518        ].into_iter(),
519        AcquirePrefixInterfaceConfig::Upstreams,
520        Some(PrefixOnInterface {
521            interface_id: InterfaceId::new(2).unwrap(),
522            prefix: net_subnet_v6!("efff::/64"),
523            lifetimes: RENEWED_LIFETIMES,
524        });
525        "different_interface"
526    )]
527    #[fuchsia::test]
528    async fn get_suitable_dhcpv6_prefix(
529        current_prefix: Option<PrefixOnInterface>,
530        interface_state_iter: impl IntoIterator<Item = (crate::DeviceClass, Option<Prefixes>)>,
531        interface_config: AcquirePrefixInterfaceConfig,
532        want: Option<PrefixOnInterface>,
533    ) {
534        let interface_states = (1..)
535            .flat_map(InterfaceId::new)
536            .zip(interface_state_iter.into_iter().map(|(device_class, prefixes)| {
537                let (control, _control_server_end) =
538                    fidl_fuchsia_net_interfaces_ext::admin::Control::create_endpoints()
539                        .expect("create endpoints");
540                InterfaceState::new_host_with_state(
541                    generate_identifier(&fidl_fuchsia_net_ext::MacAddress {
542                        octets: [0x1, 0x2, 0x3, 0x4, 0x5, 0x6],
543                    }),
544                    control,
545                    device_class,
546                    None,
547                    prefixes.map(|prefixes| ClientState {
548                        sockaddr: fidl_socket_addr_v6!("[fe80::1]:546"),
549                        prefixes: prefixes,
550                    }),
551                    ProvisioningType::Local,
552                    fake_interface_grant(),
553                )
554            }))
555            .collect();
556        let allowed_upstream_device_classes = HashSet::from([ALLOWED_UPSTREAM_DEVICE_CLASS]);
557        assert_eq!(
558            super::get_suitable_dhcpv6_prefix(
559                current_prefix,
560                &interface_states,
561                &allowed_upstream_device_classes,
562                interface_config,
563            ),
564            want
565        );
566    }
567}