1#![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
28pub 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#[derive(Default, Debug)]
43pub struct Configuration {
44 pub address: Option<Address>,
46 pub dns_servers: Vec<fnet::Ipv4Address>,
48 pub routers: Vec<SpecifiedAddr<Ipv4Addr>>,
50}
51
52#[derive(thiserror::Error, Debug)]
54pub enum Error {
55 #[error("invalid FIDL domain object: {0:?}")]
57 ApiViolation(anyhow::Error),
58 #[error("errors while manipulating route set: {0:?}")]
60 RouteSet(fnet_routes_admin::RouteSetError),
61 #[error("fidl error: {0:?}")]
63 Fidl(fidl::Error),
64 #[error("invalid exit reason: {0:?}")]
66 WrongExitReason(fnet_dhcp::ClientExitReason),
67 #[error("missing exit reason")]
69 MissingExitReason,
70 #[error("unexpected exit; reason: {0:?}")]
72 UnexpectedExit(Option<fnet_dhcp::ClientExitReason>),
73}
74
75const DEFAULT_SUBNET: net_types::ip::Subnet<Ipv4Addr> = net_declare::net_subnet_v4!("0.0.0.0/0");
78
79pub const DEFAULT_ADDR_PREFIX: fnet::Ipv4AddressWithPrefix = fidl_ip_v4_with_prefix!("0.0.0.0/0");
82
83pub 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#[derive(Debug)]
185pub struct Address {
186 pub address: fnet::Ipv4AddressWithPrefix,
188 pub address_parameters: fnet_interfaces_admin::AddressParameters,
190 pub address_state_provider: ServerEnd<fnet_interfaces_admin::AddressStateProviderMarker>,
192}
193
194impl Address {
195 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
220pub 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
230pub trait ClientProviderExt {
232 fn new_client_ext(
234 &self,
235 interface_id: NonZeroU64,
236 new_client_params: fnet_dhcp::NewClientParams,
237 ) -> fnet_dhcp::ClientProxy;
238
239 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#[async_trait]
273pub trait ClientExt {
274 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
311pub fn merged_configuration_stream(
315 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 #[derive(Debug)]
334 enum MergedClientEvent {
335 Terminal(Result<fnet_dhcp::ClientEvent, Error>),
337 WatchConfiguration(Result<Configuration, Error>),
339 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 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 .chain(futures::stream::once(futures::future::ready(MergedClientEvent::Terminal(
358 Err(Error::MissingExitReason),
359 )))),
360 configs.map(MergedClientEvent::WatchConfiguration),
361 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 *stream_ended = true;
425 Some(Err(err))
426 }
427 }
428 }
429 }))
430 })
431 .filter_map(futures::future::ready)
432}
433
434pub mod testutil {
436 use super::*;
437 use fuchsia_async as fasync;
438 use futures::future::ready;
439
440 pub struct DhcpClientTask {
442 client: fnet_dhcp::ClientProxy,
443 task: fasync::Task<()>,
444 }
445
446 impl DhcpClientTask {
447 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 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 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 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 #[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 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 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 assert_matches!(config_stream.next().await, None);
959 } else {
960 assert_matches!(config_stream.next().now_or_never(), None);
962 shutdown_sender.send(()).expect("shutdown receiver should not have been dropped");
963
964 assert_matches!(config_stream.next().await, None);
967 }
968 };
969
970 let ((), ()) = join!(client_fut, server_fut);
971 }
972}