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