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 as fnet;
15use fidl_fuchsia_net_dhcp as fnet_dhcp;
16use fidl_fuchsia_net_ext::IntoExt as _;
17use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin;
18use fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext;
19use fidl_fuchsia_net_resources as fnet_resources;
20use fidl_fuchsia_net_routes as fnet_routes;
21use fidl_fuchsia_net_routes_admin as fnet_routes_admin;
22use fidl_fuchsia_net_routes_ext as fnet_routes_ext;
23use futures::{Future, FutureExt, Stream, StreamExt as _, TryStreamExt as _, pin_mut};
24use net_declare::fidl_ip_v4_with_prefix;
25use net_types::SpecifiedAddr;
26use net_types::ip::{Ipv4, Ipv4Addr};
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: futures::future::Shared<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 .shared(),
526 }
527 }
528
529 pub fn wait_shutdown(&self) -> impl Future<Output = ()> + 'static {
531 self.task.clone()
532 }
533
534 pub async fn shutdown(self) -> Result<(), Error> {
536 let DhcpClientTask { client, task } = self;
537 client
538 .shutdown_ext(client.take_event_stream())
539 .await
540 .expect("client shutdown should succeed");
541 task.await;
542 Ok(())
543 }
544 }
545}
546
547#[cfg(test)]
548mod test {
549 use crate::{ClientExt as _, DEFAULT_ADDR_PREFIX, Error};
550
551 use std::collections::HashSet;
552 use std::num::NonZeroU64;
553
554 use assert_matches::assert_matches;
555 use fidl::endpoints::RequestStream;
556 use fidl_fuchsia_net as fnet;
557 use fidl_fuchsia_net_dhcp as fnet_dhcp;
558 use fidl_fuchsia_net_ext::IntoExt as _;
559 use fidl_fuchsia_net_routes as fnet_routes;
560 use fidl_fuchsia_net_routes_admin as fnet_routes_admin;
561 use fuchsia_async as fasync;
562 use futures::channel::oneshot;
563 use futures::{FutureExt as _, StreamExt as _, join, pin_mut};
564 use net_declare::net_ip_v4;
565 use net_types::ip::{Ip, Ipv4, Ipv4Addr};
566 use net_types::{SpecifiedAddr, SpecifiedAddress as _, Witness as _};
567 use proptest::prelude::*;
568 use test_case::test_case;
569
570 #[derive(proptest_derive::Arbitrary, Clone, Debug)]
571 struct Address {
572 include_address: bool,
573 include_address_parameters: bool,
574 include_address_state_provider: bool,
575 }
576
577 #[derive(proptest_derive::Arbitrary, Clone, Debug)]
581 enum GeneratedIpv4Addr {
582 Specified,
583 Unspecified,
584 }
585
586 impl From<GeneratedIpv4Addr> for Ipv4Addr {
587 fn from(value: GeneratedIpv4Addr) -> Self {
588 match value {
589 GeneratedIpv4Addr::Specified => net_ip_v4!("1.1.1.1"),
590 GeneratedIpv4Addr::Unspecified => Ipv4::UNSPECIFIED_ADDRESS,
591 }
592 }
593 }
594
595 #[derive(proptest_derive::Arbitrary, Clone, Debug)]
596 struct ClientWatchConfigurationResponse {
597 address: Option<Address>,
598 dns_servers: Option<Vec<GeneratedIpv4Addr>>,
599 routers: Option<Vec<GeneratedIpv4Addr>>,
600 }
601
602 proptest! {
603 #![proptest_config(ProptestConfig {
604 failure_persistence: Some(
605 Box::<proptest::test_runner::MapFailurePersistence>::default()
606 ),
607 ..ProptestConfig::default()
608 })]
609
610 #[test]
611 fn try_into_configuration(response: ClientWatchConfigurationResponse) {
612 let make_fidl = |response: &ClientWatchConfigurationResponse| {
613 let ClientWatchConfigurationResponse {
614 address,
615 dns_servers,
616 routers,
617 } = response.clone();
618
619 fnet_dhcp::ClientWatchConfigurationResponse {
620 address: address.map(
621 |Address {
622 include_address,
623 include_address_parameters,
624 include_address_state_provider
625 }| {
626 fnet_dhcp::Address {
627 address: include_address.then_some(
628 fidl_fuchsia_net::Ipv4AddressWithPrefix {
629 addr: net_ip_v4!("1.1.1.1").into_ext(),
630 prefix_len: 24,
631 }
632 ),
633 address_parameters: include_address_parameters.then_some(
634 fidl_fuchsia_net_interfaces_admin::AddressParameters::default()
635 ),
636 address_state_provider: include_address_state_provider.then_some({
637 let (_, server) = fidl::endpoints::create_endpoints();
638 server
639 }),
640 ..Default::default()
641 }
642 }),
643 dns_servers: dns_servers.map(
644 |list| list.into_iter().map(
645 |addr: GeneratedIpv4Addr| net_types::ip::Ipv4Addr::from(
646 addr
647 ).into_ext()
648 ).collect()),
649 routers: routers.map(
650 |list| list.into_iter().map(
651 |addr: GeneratedIpv4Addr| net_types::ip::Ipv4Addr::from(
652 addr
653 ).into_ext()
654 ).collect()),
655 ..Default::default()
656 }
657 };
658
659 let result = crate::Configuration::try_from(make_fidl(&response));
660
661 if let Some(crate::Configuration {
662 address: result_address,
663 dns_servers: result_dns_servers,
664 routers: result_routers,
665 }) = match response.address {
666 Some(
667 Address {
668 include_address,
669 include_address_parameters,
670 include_address_state_provider,
671 }
672 ) => {
673 prop_assert_eq!(
674 !(
675 include_address &&
676 include_address_parameters &&
677 include_address_state_provider
678 ),
679 result.is_err(),
680 "must reject partially-filled address object"
681 );
682
683 result.ok()
684 }
685 None => {
686 prop_assert!(result.is_ok(), "absent address is always accepted");
687 Some(result.unwrap())
688 }
689 } {
690 let fnet_dhcp::ClientWatchConfigurationResponse {
691 dns_servers: fidl_dns_servers,
692 routers: fidl_routers,
693 address: fidl_address,
694 ..
695 } = make_fidl(&response);
696 let want_routers: Vec<net_types::ip::Ipv4Addr> = fidl_routers
697 .unwrap_or_default()
698 .into_iter()
699 .flat_map(
700 |addr| Some(addr.into_ext()).filter(net_types::ip::Ipv4Addr::is_specified)
701 )
702 .collect();
703 prop_assert_eq!(
704 result_dns_servers,
705 fidl_dns_servers.unwrap_or_default()
706 );
707 prop_assert_eq!(
708 result_routers.into_iter().map(|addr| addr.get()).collect::<Vec<_>>(),
709 want_routers
710 );
711
712 if let Some(
713 crate::Address {
714 address: result_address,
715 address_parameters: result_address_parameters,
716 address_state_provider: _
717 }
718 ) = result_address {
719 let fnet_dhcp::Address {
720 address: fidl_address,
721 address_parameters: fidl_address_parameters,
722 ..
723 } = fidl_address.expect("should be present");
724
725 prop_assert_eq!(Some(result_address), fidl_address);
726 prop_assert_eq!(Some(result_address_parameters), fidl_address_parameters);
727 }
728 }
729 }
730 }
731
732 #[fasync::run_singlethreaded(test)]
733 async fn apply_new_routers() {
734 let (route_set, route_set_stream) =
735 fidl::endpoints::create_proxy_and_stream::<fnet_routes_admin::RouteSetV4Marker>();
736
737 const REMOVED_ROUTER: Ipv4Addr = net_ip_v4!("1.1.1.1");
738 const KEPT_ROUTER: Ipv4Addr = net_ip_v4!("2.2.2.2");
739 const ADDED_ROUTER: Ipv4Addr = net_ip_v4!("3.3.3.3");
740
741 let mut configured_routers = [REMOVED_ROUTER, KEPT_ROUTER]
742 .into_iter()
743 .map(|addr| SpecifiedAddr::new(addr).unwrap())
744 .collect::<HashSet<_>>();
745
746 let device_id = NonZeroU64::new(5).unwrap();
747
748 let apply_fut = crate::apply_new_routers(
749 device_id,
750 &route_set,
751 &mut configured_routers,
752 vec![
753 SpecifiedAddr::new(KEPT_ROUTER).unwrap(),
754 SpecifiedAddr::new(ADDED_ROUTER).unwrap(),
755 ],
756 )
757 .fuse();
758
759 let route_set_fut = async move {
760 pin_mut!(route_set_stream);
761 let (route, responder) = route_set_stream
762 .next()
763 .await
764 .expect("should not have ended")
765 .expect("should not have error")
766 .into_remove_route()
767 .expect("should be remove route");
768 assert_eq!(
769 route,
770 fnet_routes::RouteV4 {
771 destination: DEFAULT_ADDR_PREFIX,
772 action: fnet_routes::RouteActionV4::Forward(fnet_routes::RouteTargetV4 {
773 outbound_interface: device_id.get(),
774 next_hop: Some(Box::new(REMOVED_ROUTER.into_ext()))
775 }),
776 properties: fnet_routes::RoutePropertiesV4 {
777 specified_properties: Some(fnet_routes::SpecifiedRouteProperties {
778 metric: Some(fnet_routes::SpecifiedMetric::InheritedFromInterface(
779 fnet_routes::Empty
780 )),
781 ..Default::default()
782 }),
783 ..Default::default()
784 }
785 }
786 );
787 responder.send(Ok(true)).expect("responder send");
788
789 let (route, responder) = route_set_stream
790 .next()
791 .await
792 .expect("should not have ended")
793 .expect("should not have error")
794 .into_add_route()
795 .expect("should be add route");
796 assert_eq!(
797 route,
798 fnet_routes::RouteV4 {
799 destination: DEFAULT_ADDR_PREFIX,
800 action: fnet_routes::RouteActionV4::Forward(fnet_routes::RouteTargetV4 {
801 outbound_interface: device_id.get(),
802 next_hop: Some(Box::new(ADDED_ROUTER.into_ext()))
803 }),
804 properties: fnet_routes::RoutePropertiesV4 {
805 specified_properties: Some(fnet_routes::SpecifiedRouteProperties {
806 metric: Some(fnet_routes::SpecifiedMetric::InheritedFromInterface(
807 fnet_routes::Empty
808 )),
809 ..Default::default()
810 }),
811 ..Default::default()
812 }
813 }
814 );
815 responder.send(Ok(true)).expect("responder send");
816 }
817 .fuse();
818
819 pin_mut!(apply_fut, route_set_fut);
820 let (apply_result, ()) = join!(apply_fut, route_set_fut);
821 apply_result.expect("apply should succeed");
822 }
823
824 #[test_case(
825 None => matches Err(Error::MissingExitReason) ; "no exit reason should cause error"
826 )]
827 #[test_case(
828 Some(fnet_dhcp::ClientExitReason::NetworkUnreachable) => matches Err(Error::WrongExitReason(fnet_dhcp::ClientExitReason::NetworkUnreachable)) ;
829 "wrong exit reason should cause error"
830 )]
831 #[test_case(
832 Some(fnet_dhcp::ClientExitReason::GracefulShutdown) => matches Ok(()) ;
833 "GracefulShutdown is correct exit reason"
834 )]
835 #[fasync::run_singlethreaded(test)]
836 async fn shutdown_ext(exit_reason: Option<fnet_dhcp::ClientExitReason>) -> Result<(), Error> {
837 let (client, stream) =
838 fidl::endpoints::create_proxy_and_stream::<fnet_dhcp::ClientMarker>();
839
840 if let Some(exit_reason) = exit_reason {
841 stream.control_handle().send_on_exit(exit_reason).expect("send on exit");
842 }
843
844 let shutdown_fut = client.shutdown_ext(client.take_event_stream()).fuse();
845 let server_fut = async move {
846 pin_mut!(stream);
847 let _client_control_handle = stream
848 .next()
849 .await
850 .expect("should not have ended")
851 .expect("should not have FIDL error")
852 .into_shutdown()
853 .expect("should be shutdown request");
854 }
855 .fuse();
856
857 let (shutdown_result, ()) = join!(shutdown_fut, server_fut);
858 shutdown_result
859 }
860
861 #[test_case(
862 None ; "client does not exit until we tell it to"
863 )]
864 #[test_case(
865 Some(fnet_dhcp::ClientExitReason::NetworkUnreachable);
866 "client exits due to network unreachable"
867 )]
868 #[test_case(
869 Some(fnet_dhcp::ClientExitReason::GracefulShutdown);
870 "client exits due to GracefulShutdown of its own accord"
871 )]
872 #[fasync::run_singlethreaded(test)]
873 async fn merged_configuration_stream_exit(exit_reason: Option<fnet_dhcp::ClientExitReason>) {
874 const ADDRESS: fnet::Ipv4AddressWithPrefix =
875 net_declare::fidl_ip_v4_with_prefix!("192.0.2.1/32");
876
877 let (client, stream) = fidl::endpoints::create_request_stream::<fnet_dhcp::ClientMarker>();
878
879 let server_fut = async move {
880 pin_mut!(stream);
881
882 let watch_config_responder = stream
883 .next()
884 .await
885 .expect("should not have ended")
886 .expect("should not have FIDL error")
887 .into_watch_configuration()
888 .expect("should be watch configuration");
889
890 let (_asp_client, asp_server) = fidl::endpoints::create_endpoints::<
891 fidl_fuchsia_net_interfaces_admin::AddressStateProviderMarker,
892 >();
893
894 watch_config_responder
895 .send(fnet_dhcp::ClientWatchConfigurationResponse {
896 address: Some(fnet_dhcp::Address {
897 address: Some(ADDRESS),
898 address_parameters: Some(
899 fidl_fuchsia_net_interfaces_admin::AddressParameters::default(),
900 ),
901 address_state_provider: Some(asp_server),
902 ..Default::default()
903 }),
904 ..Default::default()
905 })
906 .expect("should successfully respond to hanging get");
907
908 let _watch_config_responder = stream
910 .next()
911 .await
912 .expect("should not have ended")
913 .expect("should not have FIDL error")
914 .into_watch_configuration()
915 .expect("should be watch configuration");
916
917 if let Some(exit_reason) = exit_reason {
918 stream.control_handle().send_on_exit(exit_reason).expect("send on exit");
919 } else {
920 let _client_control_handle = stream
921 .next()
922 .await
923 .expect("should not have ended")
924 .expect("should not have FIDL error")
925 .into_shutdown()
926 .expect("should be shutdown request");
927 stream
928 .control_handle()
929 .send_on_exit(fnet_dhcp::ClientExitReason::GracefulShutdown)
930 .expect("send on exit");
931 }
932 }
933 .fuse();
934
935 let client_fut = async move {
936 let (shutdown_sender, shutdown_receiver) = oneshot::channel();
937
938 let config_stream = crate::merged_configuration_stream(
939 client,
940 shutdown_receiver.map(|res| res.expect("shutdown_sender should not be dropped")),
941 )
942 .fuse();
943 pin_mut!(config_stream);
944
945 let initial_config = config_stream.next().await.expect("should not have ended");
946 let address = assert_matches!(initial_config,
947 Ok(crate::Configuration {
948 address: Some(crate::Address { address, .. }),
949 ..
950 }) => address
951 );
952 assert_eq!(address, ADDRESS);
953
954 if let Some(want_reason) = exit_reason {
955 let item = config_stream.next().await.expect("should not have ended");
957 let got_reason = assert_matches!(item,
958 Err(Error::UnexpectedExit(Some(reason))) => reason
959 );
960 assert_eq!(got_reason, want_reason);
961
962 assert_matches!(config_stream.next().await, None);
964 } else {
965 assert_matches!(config_stream.next().now_or_never(), None);
967 shutdown_sender.send(()).expect("shutdown receiver should not have been dropped");
968
969 assert_matches!(config_stream.next().await, None);
972 }
973 };
974
975 let ((), ()) = join!(client_fut, server_fut);
976 }
977}