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::{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
27pub 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#[derive(Default, Debug)]
42pub struct Configuration {
43 pub address: Option<Address>,
45 pub dns_servers: Vec<fnet::Ipv4Address>,
47 pub routers: Vec<SpecifiedAddr<Ipv4Addr>>,
49}
50
51#[derive(thiserror::Error, Debug)]
53pub enum Error {
54 #[error("invalid FIDL domain object: {0:?}")]
56 ApiViolation(anyhow::Error),
57 #[error("errors while manipulating route set: {0:?}")]
59 RouteSet(fnet_routes_admin::RouteSetError),
60 #[error("fidl error: {0:?}")]
62 Fidl(fidl::Error),
63 #[error("invalid exit reason: {0:?}")]
65 WrongExitReason(fnet_dhcp::ClientExitReason),
66 #[error("missing exit reason")]
68 MissingExitReason,
69 #[error("unexpected exit; reason: {0:?}")]
71 UnexpectedExit(Option<fnet_dhcp::ClientExitReason>),
72}
73
74const DEFAULT_SUBNET: net_types::ip::Subnet<Ipv4Addr> = net_declare::net_subnet_v4!("0.0.0.0/0");
77
78pub const DEFAULT_ADDR_PREFIX: fnet::Ipv4AddressWithPrefix = fidl_ip_v4_with_prefix!("0.0.0.0/0");
81
82pub 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#[derive(Debug)]
184pub struct Address {
185 pub address: fnet::Ipv4AddressWithPrefix,
187 pub address_parameters: fnet_interfaces_admin::AddressParameters,
189 pub address_state_provider: ServerEnd<fnet_interfaces_admin::AddressStateProviderMarker>,
191}
192
193impl Address {
194 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
219pub 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
229pub trait ClientProviderExt {
231 fn new_client_ext(
233 &self,
234 interface_id: NonZeroU64,
235 new_client_params: fnet_dhcp::NewClientParams,
236 ) -> fnet_dhcp::ClientProxy;
237
238 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#[async_trait]
272pub trait ClientExt {
273 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
310pub fn merged_configuration_stream(
314 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 #[derive(Debug)]
333 enum MergedClientEvent {
334 Terminal(Result<fnet_dhcp::ClientEvent, Error>),
336 WatchConfiguration(Result<Configuration, Error>),
338 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 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 .chain(futures::stream::once(futures::future::ready(MergedClientEvent::Terminal(
357 Err(Error::MissingExitReason),
358 )))),
359 configs.map(MergedClientEvent::WatchConfiguration),
360 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 *stream_ended = true;
424 Some(Err(err))
425 }
426 }
427 }
428 }))
429 })
430 .filter_map(futures::future::ready)
431}
432
433pub mod testutil {
435 use super::*;
436 use fuchsia_async as fasync;
437 use futures::future::ready;
438
439 pub struct DhcpClientTask {
441 client: fnet_dhcp::ClientProxy,
442 task: fasync::Task<()>,
443 }
444
445 impl DhcpClientTask {
446 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 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 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 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 #[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 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 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 assert_matches!(config_stream.next().await, None);
962 } else {
963 assert_matches!(config_stream.next().now_or_never(), None);
965 shutdown_sender.send(()).expect("shutdown receiver should not have been dropped");
966
967 assert_matches!(config_stream.next().await, None);
970 }
971 };
972
973 let ((), ()) = join!(client_fut, server_fut);
974 }
975}