fidl_fuchsia_net_dhcp_ext/
lib.rs

1// Copyright 2023 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
5//! Extensions for types in the `fidl_fuchsia_net_dhcp` crate.
6#![deny(missing_docs)]
7
8use std::collections::HashSet;
9use std::num::NonZeroU64;
10
11use anyhow::anyhow;
12use async_trait::async_trait;
13use fidl::endpoints::ServerEnd;
14use fidl_fuchsia_net_ext::IntoExt as _;
15use futures::{Future, FutureExt, Stream, StreamExt as _, TryStreamExt as _, pin_mut};
16use net_declare::fidl_ip_v4_with_prefix;
17use net_types::SpecifiedAddr;
18use net_types::ip::{Ipv4, Ipv4Addr};
19use {
20    fidl_fuchsia_net as fnet, fidl_fuchsia_net_dhcp as fnet_dhcp,
21    fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin,
22    fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext,
23    fidl_fuchsia_net_resources as fnet_resources, fidl_fuchsia_net_routes as fnet_routes,
24    fidl_fuchsia_net_routes_admin as fnet_routes_admin,
25    fidl_fuchsia_net_routes_ext as fnet_routes_ext,
26};
27
28/// The default `fnet_dhcp::NewClientParams`.
29pub fn default_new_client_params() -> fnet_dhcp::NewClientParams {
30    fnet_dhcp::NewClientParams {
31        configuration_to_request: Some(fnet_dhcp::ConfigurationToRequest {
32            routers: Some(true),
33            dns_servers: Some(true),
34            ..fnet_dhcp::ConfigurationToRequest::default()
35        }),
36        request_ip_address: Some(true),
37        ..fnet_dhcp::NewClientParams::default()
38    }
39}
40
41/// Configuration acquired by the DHCP client.
42#[derive(Default, Debug)]
43pub struct Configuration {
44    /// The acquired address.
45    pub address: Option<Address>,
46    /// Acquired DNS servers.
47    pub dns_servers: Vec<fnet::Ipv4Address>,
48    /// Acquired routers.
49    pub routers: Vec<SpecifiedAddr<Ipv4Addr>>,
50}
51
52/// Domain errors for this crate.
53#[derive(thiserror::Error, Debug)]
54pub enum Error {
55    /// A FIDL domain object was invalid.
56    #[error("invalid FIDL domain object: {0:?}")]
57    ApiViolation(anyhow::Error),
58    /// An error was encountered while manipulating a route set.
59    #[error("errors while manipulating route set: {0:?}")]
60    RouteSet(fnet_routes_admin::RouteSetError),
61    /// A FIDL error was encountered.
62    #[error("fidl error: {0:?}")]
63    Fidl(fidl::Error),
64    /// An invalid ClientExitReason was observed on the client's event stream.
65    #[error("invalid exit reason: {0:?}")]
66    WrongExitReason(fnet_dhcp::ClientExitReason),
67    /// No ClientExitReason was provided, when one was expected.
68    #[error("missing exit reason")]
69    MissingExitReason,
70    /// The client unexpectedly exited.
71    #[error("unexpected exit; reason: {0:?}")]
72    UnexpectedExit(Option<fnet_dhcp::ClientExitReason>),
73}
74
75/// The default subnet used as the destination while populating a
76/// [`fnet_routes_ext::Route`] while applying newly-discovered routers.
77const DEFAULT_SUBNET: net_types::ip::Subnet<Ipv4Addr> = net_declare::net_subnet_v4!("0.0.0.0/0");
78
79/// The default subnet used as the destination while populating a
80/// `fuchsia.net.routes.RouteV4` while applying newly-discovered routers.
81pub const DEFAULT_ADDR_PREFIX: fnet::Ipv4AddressWithPrefix = fidl_ip_v4_with_prefix!("0.0.0.0/0");
82
83/// Applies a new set of routers
84///
85/// Applies a new set of routers to a given `fuchsia.net.stack.Stack` and
86/// set of configured routers by deleting forwarding entries for
87/// newly-absent routers and adding forwarding entries for newly-present
88/// ones.
89pub async fn apply_new_routers(
90    device_id: NonZeroU64,
91    route_set: &fnet_routes_admin::RouteSetV4Proxy,
92    configured_routers: &mut HashSet<SpecifiedAddr<Ipv4Addr>>,
93    new_routers: impl IntoIterator<Item = SpecifiedAddr<Ipv4Addr>>,
94) -> Result<(), Error> {
95    let route = |next_hop: &SpecifiedAddr<Ipv4Addr>| fnet_routes_ext::Route::<Ipv4> {
96        action: fnet_routes_ext::RouteAction::Forward(fnet_routes_ext::RouteTarget {
97            outbound_interface: device_id.get(),
98            next_hop: Some(*next_hop),
99        }),
100        destination: DEFAULT_SUBNET,
101        properties: fnet_routes_ext::RouteProperties {
102            specified_properties: fnet_routes_ext::SpecifiedRouteProperties {
103                metric: fnet_routes::SpecifiedMetric::InheritedFromInterface(fnet_routes::Empty),
104            },
105        },
106    };
107
108    let new_routers = new_routers.into_iter().collect::<HashSet<_>>();
109
110    for router in configured_routers.difference(&new_routers) {
111        let removed: bool = route_set
112            .remove_route(
113                &route(router)
114                    .try_into()
115                    .map_err(|e| Error::ApiViolation(anyhow::Error::new(e)))?,
116            )
117            .await
118            .map_err(Error::Fidl)?
119            .map_err(Error::RouteSet)?;
120        if !removed {
121            log::warn!("attempt to remove {router} from RouteSet was no-op");
122        }
123    }
124
125    for router in new_routers.difference(configured_routers) {
126        let added: bool = route_set
127            .add_route(
128                &route(router)
129                    .try_into()
130                    .map_err(|e| Error::ApiViolation(anyhow::Error::new(e)))?,
131            )
132            .await
133            .map_err(Error::Fidl)?
134            .map_err(Error::RouteSet)?;
135        if !added {
136            log::warn!("attempt to add {router} to RouteSet was no-op");
137        }
138    }
139
140    *configured_routers = new_routers;
141    Ok(())
142}
143
144impl TryFrom<fnet_dhcp::ClientWatchConfigurationResponse> for Configuration {
145    type Error = Error;
146    fn try_from(
147        fnet_dhcp::ClientWatchConfigurationResponse {
148            address,
149            dns_servers,
150            routers,
151            ..
152        }: fnet_dhcp::ClientWatchConfigurationResponse,
153    ) -> Result<Self, Error> {
154        let address = address
155            .map(
156                |fnet_dhcp::Address {
157                     address, address_parameters, address_state_provider, ..
158                 }| {
159                    Ok(Address {
160                        address: address
161                            .ok_or_else(|| anyhow!("Ipv4AddressWithPrefix should be present"))?,
162                        address_parameters: address_parameters
163                            .ok_or_else(|| anyhow!("AddressParameters should be present"))?,
164                        address_state_provider: address_state_provider
165                            .ok_or_else(|| anyhow!("AddressStateProvider should be present"))?,
166                    })
167                },
168            )
169            .transpose()
170            .map_err(Error::ApiViolation);
171        Ok(Configuration {
172            address: address?,
173            dns_servers: dns_servers.unwrap_or_default(),
174            routers: routers
175                .unwrap_or_default()
176                .into_iter()
177                .flat_map(|addr| SpecifiedAddr::new(addr.into_ext()))
178                .collect(),
179        })
180    }
181}
182
183/// An IPv4 address acquired by the DHCP client.
184#[derive(Debug)]
185pub struct Address {
186    /// The acquired address and discovered prefix length.
187    pub address: fnet::Ipv4AddressWithPrefix,
188    /// Parameters for the acquired address.
189    pub address_parameters: fnet_interfaces_admin::AddressParameters,
190    /// The server end for the AddressStateProvider owned by the DHCP client.
191    pub address_state_provider: ServerEnd<fnet_interfaces_admin::AddressStateProviderMarker>,
192}
193
194impl Address {
195    /// Adds this address via `fuchsia.net.interfaces.admin.Control`.
196    pub fn add_to(
197        self,
198        control: &fnet_interfaces_ext::admin::Control,
199    ) -> Result<
200        (),
201        (
202            fnet::Ipv4AddressWithPrefix,
203            fnet_interfaces_ext::admin::TerminalError<
204                fnet_interfaces_admin::InterfaceRemovedReason,
205            >,
206        ),
207    > {
208        let Self { address, address_parameters, address_state_provider } = self;
209        control
210            .add_address(&address.into_ext(), &address_parameters, address_state_provider)
211            .map_err(|e| (address, e))
212    }
213}
214
215type ConfigurationStream = async_utils::hanging_get::client::HangingGetStream<
216    fnet_dhcp::ClientProxy,
217    fnet_dhcp::ClientWatchConfigurationResponse,
218>;
219
220/// Produces a stream of acquired DHCP configuration by executing the hanging
221/// get on the provided DHCP client proxy.
222pub fn configuration_stream(
223    client: fnet_dhcp::ClientProxy,
224) -> impl futures::Stream<Item = Result<Configuration, Error>> {
225    ConfigurationStream::new_eager_with_fn_ptr(client, fnet_dhcp::ClientProxy::watch_configuration)
226        .map_err(Error::Fidl)
227        .and_then(|config| futures::future::ready(Configuration::try_from(config)))
228}
229
230/// Extension trait on `fidl_fuchsia_net_dhcp::ClientProviderProxy`.
231pub trait ClientProviderExt {
232    /// Construct a new DHCP client.
233    fn new_client_ext(
234        &self,
235        interface_id: NonZeroU64,
236        new_client_params: fnet_dhcp::NewClientParams,
237    ) -> fnet_dhcp::ClientProxy;
238
239    /// Construct a new DHCP client, returning a ClientEnd instead of a Proxy.
240    fn new_client_end_ext(
241        &self,
242        interface_id: NonZeroU64,
243        new_client_params: fnet_dhcp::NewClientParams,
244    ) -> fidl::endpoints::ClientEnd<fnet_dhcp::ClientMarker>;
245}
246
247impl ClientProviderExt for fnet_dhcp::ClientProviderProxy {
248    fn new_client_ext(
249        &self,
250        interface_id: NonZeroU64,
251        new_client_params: fnet_dhcp::NewClientParams,
252    ) -> fnet_dhcp::ClientProxy {
253        let (client, server) = fidl::endpoints::create_proxy::<fnet_dhcp::ClientMarker>();
254        self.new_client(interface_id.get(), &new_client_params, server)
255            .expect("create new DHCPv4 client");
256        client
257    }
258
259    fn new_client_end_ext(
260        &self,
261        interface_id: NonZeroU64,
262        new_client_params: fnet_dhcp::NewClientParams,
263    ) -> fidl::endpoints::ClientEnd<fnet_dhcp::ClientMarker> {
264        let (client, server) = fidl::endpoints::create_endpoints::<fnet_dhcp::ClientMarker>();
265        self.new_client(interface_id.get(), &new_client_params, server)
266            .expect("create new DHCPv4 client");
267        client
268    }
269}
270
271/// Extension trait on `fidl_fuchsia_net_dhcp::ClientProxy`.
272#[async_trait]
273pub trait ClientExt {
274    /// Shuts down the client, watching for the `GracefulShutdown` exit event.
275    ///
276    /// Returns an error if the `GracefulShutdown` exit event is not observed.
277    async fn shutdown_ext(&self, event_stream: fnet_dhcp::ClientEventStream) -> Result<(), Error>;
278}
279
280#[async_trait]
281impl ClientExt for fnet_dhcp::ClientProxy {
282    async fn shutdown_ext(&self, event_stream: fnet_dhcp::ClientEventStream) -> Result<(), Error> {
283        self.shutdown().map_err(Error::Fidl)?;
284
285        let stream = event_stream.map_err(Error::Fidl).try_filter_map(|event| async move {
286            match event {
287                fnet_dhcp::ClientEvent::OnExit { reason } => Ok(match reason {
288                    fnet_dhcp::ClientExitReason::ClientAlreadyExistsOnInterface
289                    | fnet_dhcp::ClientExitReason::WatchConfigurationAlreadyPending
290                    | fnet_dhcp::ClientExitReason::InvalidInterface
291                    | fnet_dhcp::ClientExitReason::InvalidParams
292                    | fnet_dhcp::ClientExitReason::NetworkUnreachable
293                    | fnet_dhcp::ClientExitReason::AddressRemovedByUser
294                    | fnet_dhcp::ClientExitReason::AddressStateProviderError
295                    | fnet_dhcp::ClientExitReason::UnableToOpenSocket => {
296                        return Err(Error::WrongExitReason(reason));
297                    }
298                    fnet_dhcp::ClientExitReason::GracefulShutdown => Some(()),
299                }),
300            }
301        });
302
303        pin_mut!(stream);
304        stream.try_next().await.and_then(|option| match option {
305            Some(()) => Ok(()),
306            None => Err(Error::MissingExitReason),
307        })
308    }
309}
310
311/// Produces a stream that merges together the configuration hanging get
312/// and the [`fnet_dhcp::ClientEvent::OnExit`] terminal event.
313/// The client will be shut down when `shutdown_future` completes.
314pub fn merged_configuration_stream(
315    // Takes a `[fidl::endpoints::ClientEnd]` so that we know we can take
316    // the event stream without panicking.
317    client_end: fidl::endpoints::ClientEnd<fnet_dhcp::ClientMarker>,
318    shutdown_future: impl Future<Output = ()> + 'static,
319) -> impl Stream<Item = Result<Configuration, Error>> + 'static {
320    let client = client_end.into_proxy();
321    let event_stream = client.take_event_stream();
322
323    let proxy_for_shutdown = client.clone();
324    let shutdown_future = shutdown_future.map(move |()| proxy_for_shutdown.shutdown());
325    let configs = configuration_stream(client);
326
327    fn prio_left(_: &mut ()) -> futures::stream::PollNext {
328        futures::stream::PollNext::Left
329    }
330
331    // Events yielded from a merged stream of client hanging gets or terminal
332    // events.
333    #[derive(Debug)]
334    enum MergedClientEvent {
335        // A terminal event yielded by the client's event stream.
336        Terminal(Result<fnet_dhcp::ClientEvent, Error>),
337        // Configuration acquired via the client's hanging get stream.
338        WatchConfiguration(Result<Configuration, Error>),
339        // A marker event indicating shutdown was requested by the caller.
340        ShutdownRequested,
341    }
342
343    futures::stream::select_with_strategy(
344        futures::stream::select_with_strategy(
345            event_stream.map_err(Error::Fidl).map(MergedClientEvent::Terminal),
346            // Merge in any error we observed telling the client to shut down so
347            // that it can be observed as a problem with the terminal event
348            // stream.
349            futures::stream::once(shutdown_future).map(|result| match result {
350                Ok(()) => MergedClientEvent::ShutdownRequested,
351                Err(shutdown_err) => MergedClientEvent::Terminal(Err(Error::Fidl(shutdown_err))),
352            }),
353            prio_left,
354        )
355        // If the terminal event stream ends without showing an event, then
356        // we're missing an exit reason.
357        .chain(futures::stream::once(futures::future::ready(MergedClientEvent::Terminal(
358            Err(Error::MissingExitReason),
359        )))),
360        configs.map(MergedClientEvent::WatchConfiguration),
361        // Prioritize yielding terminal events.
362        prio_left,
363    )
364    .scan((false, false), |(stream_ended, shutdown_requested), item| {
365        if *stream_ended {
366            return futures::future::ready(None);
367        }
368
369        futures::future::ready(Some(match item {
370            MergedClientEvent::ShutdownRequested => {
371                assert!(!*shutdown_requested);
372                *shutdown_requested = true;
373                None
374            }
375            MergedClientEvent::Terminal(terminal_result) => {
376                *stream_ended = true;
377                match terminal_result {
378                    Ok(fnet_dhcp::ClientEvent::OnExit { reason }) => {
379                        if *shutdown_requested {
380                            match reason {
381                                fnet_dhcp::ClientExitReason::GracefulShutdown => None,
382                                fnet_dhcp::ClientExitReason::ClientAlreadyExistsOnInterface
383                                | fnet_dhcp::ClientExitReason::WatchConfigurationAlreadyPending
384                                | fnet_dhcp::ClientExitReason::InvalidInterface
385                                | fnet_dhcp::ClientExitReason::InvalidParams
386                                | fnet_dhcp::ClientExitReason::NetworkUnreachable
387                                | fnet_dhcp::ClientExitReason::UnableToOpenSocket
388                                | fnet_dhcp::ClientExitReason::AddressRemovedByUser
389                                | fnet_dhcp::ClientExitReason::AddressStateProviderError => {
390                                    Some(Err(Error::WrongExitReason(reason)))
391                                }
392                            }
393                        } else {
394                            Some(Err(Error::UnexpectedExit(Some(reason))))
395                        }
396                    }
397                    Err(err) => Some(Err(match err {
398                        err @ (Error::ApiViolation(_)
399                        | Error::RouteSet(_)
400                        | Error::Fidl(_)
401                        | Error::UnexpectedExit(_)) => err,
402                        Error::WrongExitReason(reason) => {
403                            if *shutdown_requested {
404                                Error::WrongExitReason(reason)
405                            } else {
406                                Error::UnexpectedExit(Some(reason))
407                            }
408                        }
409                        Error::MissingExitReason => {
410                            if *shutdown_requested {
411                                Error::MissingExitReason
412                            } else {
413                                Error::UnexpectedExit(None)
414                            }
415                        }
416                    })),
417                }
418            }
419            MergedClientEvent::WatchConfiguration(watch_result) => {
420                match watch_result {
421                    Ok(config) => Some(Ok(config)),
422                    Err(err) => {
423                        // Treat all errors as fatal and stop the stream.
424                        *stream_ended = true;
425                        Some(Err(err))
426                    }
427                }
428            }
429        }))
430    })
431    .filter_map(futures::future::ready)
432}
433
434/// Contains types used when testing the DHCP client.
435pub mod testutil {
436    use super::*;
437    use fuchsia_async as fasync;
438    use futures::future::ready;
439
440    /// Task for polling the DHCP client.
441    pub struct DhcpClientTask {
442        client: fnet_dhcp::ClientProxy,
443        task: fasync::Task<()>,
444    }
445
446    impl DhcpClientTask {
447        /// Creates and returns an async task that polls the DHCP client.
448        pub fn new(
449            client: fnet_dhcp::ClientProxy,
450            id: NonZeroU64,
451            route_set: fnet_routes_admin::RouteSetV4Proxy,
452            control: fnet_interfaces_ext::admin::Control,
453        ) -> DhcpClientTask {
454            DhcpClientTask {
455                client: client.clone(),
456                task: fasync::Task::spawn(async move {
457                    let fnet_resources::GrantForInterfaceAuthorization { interface_id, token } =
458                        control
459                            .get_authorization_for_interface()
460                            .await
461                            .expect("get interface authorization");
462                    route_set
463                        .authenticate_for_interface(fnet_resources::ProofOfInterfaceAuthorization {
464                            interface_id,
465                            token,
466                        })
467                        .await
468                        .expect("authenticate should not have FIDL error")
469                        .expect("authenticate should succeed");
470
471                    let mut final_routers =
472                        configuration_stream(client)
473                            .scan((), |(), item| {
474                                ready(match item {
475                                    Err(e) => match e {
476                                        // Observing `PEER_CLOSED` is expected after the
477                                        // client is shut down, so rather than returning an
478                                        // error, simply end the stream.
479                                        Error::Fidl(fidl::Error::ClientChannelClosed {
480                                            status: zx::Status::PEER_CLOSED,
481                                            ..
482                                        }) => None,
483                                        Error::Fidl(_)
484                                        | Error::ApiViolation(_)
485                                        | Error::RouteSet(_)
486                                        | Error::WrongExitReason(_)
487                                        | Error::UnexpectedExit(_)
488                                        | Error::MissingExitReason => Some(Err(e)),
489                                    },
490                                    Ok(item) => Some(Ok(item)),
491                                })
492                            })
493                            .try_fold(
494                                HashSet::<SpecifiedAddr<Ipv4Addr>>::new(),
495                                |mut routers,
496                                 Configuration {
497                                     address,
498                                     dns_servers: _,
499                                     routers: new_routers,
500                                 }| {
501                                    let control = &control;
502                                    let route_set = &route_set;
503                                    async move {
504                                        if let Some(address) = address {
505                                            address
506                                                .add_to(control)
507                                                .expect("add address should succeed");
508                                        }
509
510                                        apply_new_routers(id, route_set, &mut routers, new_routers)
511                                            .await
512                                            .expect("applying new routers should succeed");
513                                        Ok(routers)
514                                    }
515                                },
516                            )
517                            .await
518                            .expect("watch_configuration should succeed");
519
520                    // DHCP client is being shut down, so we should remove all the routers.
521                    apply_new_routers(id, &route_set, &mut final_routers, Vec::new())
522                        .await
523                        .expect("removing all routers should succeed");
524                }),
525            }
526        }
527
528        /// Shuts down the running DHCP client and waits for the poll task to complete.
529        pub async fn shutdown(self) -> Result<(), Error> {
530            let DhcpClientTask { client, task } = self;
531            client
532                .shutdown_ext(client.take_event_stream())
533                .await
534                .expect("client shutdown should succeed");
535            task.await;
536            Ok(())
537        }
538    }
539}
540
541#[cfg(test)]
542mod test {
543    use crate::{ClientExt as _, DEFAULT_ADDR_PREFIX, Error};
544
545    use std::collections::HashSet;
546    use std::num::NonZeroU64;
547
548    use assert_matches::assert_matches;
549    use fidl::endpoints::RequestStream;
550    use fidl_fuchsia_net_ext::IntoExt as _;
551    use futures::channel::oneshot;
552    use futures::{FutureExt as _, StreamExt as _, join, pin_mut};
553    use net_declare::net_ip_v4;
554    use net_types::ip::{Ip, Ipv4, Ipv4Addr};
555    use net_types::{SpecifiedAddr, SpecifiedAddress as _, Witness as _};
556    use proptest::prelude::*;
557    use test_case::test_case;
558    use {
559        fidl_fuchsia_net as fnet, fidl_fuchsia_net_dhcp as fnet_dhcp,
560        fidl_fuchsia_net_routes as fnet_routes, fidl_fuchsia_net_routes_admin as fnet_routes_admin,
561        fuchsia_async as fasync,
562    };
563
564    #[derive(proptest_derive::Arbitrary, Clone, Debug)]
565    struct Address {
566        include_address: bool,
567        include_address_parameters: bool,
568        include_address_state_provider: bool,
569    }
570
571    // For the purposes of this test, we only care about exercising the case
572    // where addresses are specified or unspecified, with no need to be able
573    // to distinguish between specified addresses.
574    #[derive(proptest_derive::Arbitrary, Clone, Debug)]
575    enum GeneratedIpv4Addr {
576        Specified,
577        Unspecified,
578    }
579
580    impl From<GeneratedIpv4Addr> for Ipv4Addr {
581        fn from(value: GeneratedIpv4Addr) -> Self {
582            match value {
583                GeneratedIpv4Addr::Specified => net_ip_v4!("1.1.1.1"),
584                GeneratedIpv4Addr::Unspecified => Ipv4::UNSPECIFIED_ADDRESS,
585            }
586        }
587    }
588
589    #[derive(proptest_derive::Arbitrary, Clone, Debug)]
590    struct ClientWatchConfigurationResponse {
591        address: Option<Address>,
592        dns_servers: Option<Vec<GeneratedIpv4Addr>>,
593        routers: Option<Vec<GeneratedIpv4Addr>>,
594    }
595
596    proptest! {
597        #![proptest_config(ProptestConfig {
598            failure_persistence: Some(
599                Box::<proptest::test_runner::MapFailurePersistence>::default()
600            ),
601            ..ProptestConfig::default()
602        })]
603
604        #[test]
605        fn try_into_configuration(response: ClientWatchConfigurationResponse) {
606            let make_fidl = |response: &ClientWatchConfigurationResponse| {
607                let ClientWatchConfigurationResponse {
608                    address,
609                    dns_servers,
610                    routers,
611                } = response.clone();
612
613                fnet_dhcp::ClientWatchConfigurationResponse {
614                    address: address.map(
615                        |Address {
616                            include_address,
617                            include_address_parameters,
618                            include_address_state_provider
619                        }| {
620                        fnet_dhcp::Address {
621                            address: include_address.then_some(
622                                fidl_fuchsia_net::Ipv4AddressWithPrefix {
623                                    addr: net_ip_v4!("1.1.1.1").into_ext(),
624                                    prefix_len: 24,
625                                }
626                            ),
627                            address_parameters: include_address_parameters.then_some(
628                                fidl_fuchsia_net_interfaces_admin::AddressParameters::default()
629                            ),
630                            address_state_provider: include_address_state_provider.then_some({
631                                let (_, server) = fidl::endpoints::create_endpoints();
632                                server
633                            }),
634                            ..Default::default()
635                        }
636                    }),
637                    dns_servers: dns_servers.map(
638                        |list| list.into_iter().map(
639                            |addr: GeneratedIpv4Addr| net_types::ip::Ipv4Addr::from(
640                                addr
641                            ).into_ext()
642                        ).collect()),
643                    routers: routers.map(
644                        |list| list.into_iter().map(
645                            |addr: GeneratedIpv4Addr| net_types::ip::Ipv4Addr::from(
646                                addr
647                            ).into_ext()
648                        ).collect()),
649                    ..Default::default()
650                }
651            };
652
653            let result = crate::Configuration::try_from(make_fidl(&response));
654
655            if let Some(crate::Configuration {
656                address: result_address,
657                dns_servers: result_dns_servers,
658                routers: result_routers,
659            }) = match response.address {
660                Some(
661                    Address {
662                        include_address,
663                        include_address_parameters,
664                        include_address_state_provider,
665                    }
666                ) => {
667                    prop_assert_eq!(
668                        !(
669                            include_address &&
670                            include_address_parameters &&
671                            include_address_state_provider
672                        ),
673                        result.is_err(),
674                        "must reject partially-filled address object"
675                    );
676
677                    result.ok()
678                }
679                None => {
680                    prop_assert!(result.is_ok(), "absent address is always accepted");
681                    Some(result.unwrap())
682                }
683            } {
684                let fnet_dhcp::ClientWatchConfigurationResponse {
685                    dns_servers: fidl_dns_servers,
686                    routers: fidl_routers,
687                    address: fidl_address,
688                    ..
689                } = make_fidl(&response);
690                let want_routers: Vec<net_types::ip::Ipv4Addr> = fidl_routers
691                    .unwrap_or_default()
692                    .into_iter()
693                    .flat_map(
694                        |addr| Some(addr.into_ext()).filter(net_types::ip::Ipv4Addr::is_specified)
695                    )
696                    .collect();
697                prop_assert_eq!(
698                    result_dns_servers,
699                    fidl_dns_servers.unwrap_or_default()
700                );
701                prop_assert_eq!(
702                    result_routers.into_iter().map(|addr| addr.get()).collect::<Vec<_>>(),
703                    want_routers
704                );
705
706                if let Some(
707                    crate::Address {
708                        address: result_address,
709                        address_parameters: result_address_parameters,
710                        address_state_provider: _
711                    }
712                ) = result_address {
713                    let fnet_dhcp::Address {
714                        address: fidl_address,
715                        address_parameters: fidl_address_parameters,
716                        address_state_provider: _,
717                        ..
718                    } = fidl_address.expect("should be present");
719
720                    prop_assert_eq!(Some(result_address), fidl_address);
721                    prop_assert_eq!(Some(result_address_parameters), fidl_address_parameters);
722                }
723            }
724        }
725    }
726
727    #[fasync::run_singlethreaded(test)]
728    async fn apply_new_routers() {
729        let (route_set, route_set_stream) =
730            fidl::endpoints::create_proxy_and_stream::<fnet_routes_admin::RouteSetV4Marker>();
731
732        const REMOVED_ROUTER: Ipv4Addr = net_ip_v4!("1.1.1.1");
733        const KEPT_ROUTER: Ipv4Addr = net_ip_v4!("2.2.2.2");
734        const ADDED_ROUTER: Ipv4Addr = net_ip_v4!("3.3.3.3");
735
736        let mut configured_routers = [REMOVED_ROUTER, KEPT_ROUTER]
737            .into_iter()
738            .map(|addr| SpecifiedAddr::new(addr).unwrap())
739            .collect::<HashSet<_>>();
740
741        let device_id = NonZeroU64::new(5).unwrap();
742
743        let apply_fut = crate::apply_new_routers(
744            device_id,
745            &route_set,
746            &mut configured_routers,
747            vec![
748                SpecifiedAddr::new(KEPT_ROUTER).unwrap(),
749                SpecifiedAddr::new(ADDED_ROUTER).unwrap(),
750            ],
751        )
752        .fuse();
753
754        let route_set_fut = async move {
755            pin_mut!(route_set_stream);
756            let (route, responder) = route_set_stream
757                .next()
758                .await
759                .expect("should not have ended")
760                .expect("should not have error")
761                .into_remove_route()
762                .expect("should be remove route");
763            assert_eq!(
764                route,
765                fnet_routes::RouteV4 {
766                    destination: DEFAULT_ADDR_PREFIX,
767                    action: fnet_routes::RouteActionV4::Forward(fnet_routes::RouteTargetV4 {
768                        outbound_interface: device_id.get(),
769                        next_hop: Some(Box::new(REMOVED_ROUTER.into_ext()))
770                    }),
771                    properties: fnet_routes::RoutePropertiesV4 {
772                        specified_properties: Some(fnet_routes::SpecifiedRouteProperties {
773                            metric: Some(fnet_routes::SpecifiedMetric::InheritedFromInterface(
774                                fnet_routes::Empty
775                            )),
776                            ..Default::default()
777                        }),
778                        ..Default::default()
779                    }
780                }
781            );
782            responder.send(Ok(true)).expect("responder send");
783
784            let (route, responder) = route_set_stream
785                .next()
786                .await
787                .expect("should not have ended")
788                .expect("should not have error")
789                .into_add_route()
790                .expect("should be add route");
791            assert_eq!(
792                route,
793                fnet_routes::RouteV4 {
794                    destination: DEFAULT_ADDR_PREFIX,
795                    action: fnet_routes::RouteActionV4::Forward(fnet_routes::RouteTargetV4 {
796                        outbound_interface: device_id.get(),
797                        next_hop: Some(Box::new(ADDED_ROUTER.into_ext()))
798                    }),
799                    properties: fnet_routes::RoutePropertiesV4 {
800                        specified_properties: Some(fnet_routes::SpecifiedRouteProperties {
801                            metric: Some(fnet_routes::SpecifiedMetric::InheritedFromInterface(
802                                fnet_routes::Empty
803                            )),
804                            ..Default::default()
805                        }),
806                        ..Default::default()
807                    }
808                }
809            );
810            responder.send(Ok(true)).expect("responder send");
811        }
812        .fuse();
813
814        pin_mut!(apply_fut, route_set_fut);
815        let (apply_result, ()) = join!(apply_fut, route_set_fut);
816        apply_result.expect("apply should succeed");
817    }
818
819    #[test_case(
820        None => matches Err(Error::MissingExitReason) ; "no exit reason should cause error"
821    )]
822    #[test_case(
823        Some(fnet_dhcp::ClientExitReason::NetworkUnreachable) => matches Err(Error::WrongExitReason(fnet_dhcp::ClientExitReason::NetworkUnreachable)) ;
824        "wrong exit reason should cause error"
825    )]
826    #[test_case(
827        Some(fnet_dhcp::ClientExitReason::GracefulShutdown) => matches Ok(()) ;
828        "GracefulShutdown is correct exit reason"
829    )]
830    #[fasync::run_singlethreaded(test)]
831    async fn shutdown_ext(exit_reason: Option<fnet_dhcp::ClientExitReason>) -> Result<(), Error> {
832        let (client, stream) =
833            fidl::endpoints::create_proxy_and_stream::<fnet_dhcp::ClientMarker>();
834
835        if let Some(exit_reason) = exit_reason {
836            stream.control_handle().send_on_exit(exit_reason).expect("send on exit");
837        }
838
839        let shutdown_fut = client.shutdown_ext(client.take_event_stream()).fuse();
840        let server_fut = async move {
841            pin_mut!(stream);
842            let _client_control_handle = stream
843                .next()
844                .await
845                .expect("should not have ended")
846                .expect("should not have FIDL error")
847                .into_shutdown()
848                .expect("should be shutdown request");
849        }
850        .fuse();
851
852        let (shutdown_result, ()) = join!(shutdown_fut, server_fut);
853        shutdown_result
854    }
855
856    #[test_case(
857        None ; "client does not exit until we tell it to"
858    )]
859    #[test_case(
860        Some(fnet_dhcp::ClientExitReason::NetworkUnreachable);
861        "client exits due to network unreachable"
862    )]
863    #[test_case(
864        Some(fnet_dhcp::ClientExitReason::GracefulShutdown);
865        "client exits due to GracefulShutdown of its own accord"
866    )]
867    #[fasync::run_singlethreaded(test)]
868    async fn merged_configuration_stream_exit(exit_reason: Option<fnet_dhcp::ClientExitReason>) {
869        const ADDRESS: fnet::Ipv4AddressWithPrefix =
870            net_declare::fidl_ip_v4_with_prefix!("192.0.2.1/32");
871
872        let (client, stream) = fidl::endpoints::create_request_stream::<fnet_dhcp::ClientMarker>();
873
874        let server_fut = async move {
875            pin_mut!(stream);
876
877            let watch_config_responder = stream
878                .next()
879                .await
880                .expect("should not have ended")
881                .expect("should not have FIDL error")
882                .into_watch_configuration()
883                .expect("should be watch configuration");
884
885            let (_asp_client, asp_server) = fidl::endpoints::create_endpoints::<
886                fidl_fuchsia_net_interfaces_admin::AddressStateProviderMarker,
887            >();
888
889            watch_config_responder
890                .send(fnet_dhcp::ClientWatchConfigurationResponse {
891                    address: Some(fnet_dhcp::Address {
892                        address: Some(ADDRESS),
893                        address_parameters: Some(
894                            fidl_fuchsia_net_interfaces_admin::AddressParameters::default(),
895                        ),
896                        address_state_provider: Some(asp_server),
897                        ..Default::default()
898                    }),
899                    ..Default::default()
900                })
901                .expect("should successfully respond to hanging get");
902
903            // Should keep polling hanging get.
904            let _watch_config_responder = stream
905                .next()
906                .await
907                .expect("should not have ended")
908                .expect("should not have FIDL error")
909                .into_watch_configuration()
910                .expect("should be watch configuration");
911
912            if let Some(exit_reason) = exit_reason {
913                stream.control_handle().send_on_exit(exit_reason).expect("send on exit");
914            } else {
915                let _client_control_handle = stream
916                    .next()
917                    .await
918                    .expect("should not have ended")
919                    .expect("should not have FIDL error")
920                    .into_shutdown()
921                    .expect("should be shutdown request");
922                stream
923                    .control_handle()
924                    .send_on_exit(fnet_dhcp::ClientExitReason::GracefulShutdown)
925                    .expect("send on exit");
926            }
927        }
928        .fuse();
929
930        let client_fut = async move {
931            let (shutdown_sender, shutdown_receiver) = oneshot::channel();
932
933            let config_stream = crate::merged_configuration_stream(
934                client,
935                shutdown_receiver.map(|res| res.expect("shutdown_sender should not be dropped")),
936            )
937            .fuse();
938            pin_mut!(config_stream);
939
940            let initial_config = config_stream.next().await.expect("should not have ended");
941            let address = assert_matches!(initial_config,
942                Ok(crate::Configuration {
943                    address: Some(crate::Address { address, .. }),
944                    ..
945                }) => address
946            );
947            assert_eq!(address, ADDRESS);
948
949            if let Some(want_reason) = exit_reason {
950                // The DHCP client exits on its own.
951                let item = config_stream.next().await.expect("should not have ended");
952                let got_reason = assert_matches!(item,
953                    Err(Error::UnexpectedExit(Some(reason))) => reason
954                );
955                assert_eq!(got_reason, want_reason);
956
957                // The stream should have ended.
958                assert_matches!(config_stream.next().await, None);
959            } else {
960                // Poll the config stream once to indicate we're still hanging-getting.
961                assert_matches!(config_stream.next().now_or_never(), None);
962                shutdown_sender.send(()).expect("shutdown receiver should not have been dropped");
963
964                // Having sent a shutdown request, we expect the client to exit
965                // and the stream to end with no error.
966                assert_matches!(config_stream.next().await, None);
967            }
968        };
969
970        let ((), ()) = join!(client_fut, server_fut);
971    }
972}