blob: 2af80ded38564c9e22eb0bceebda0749cea238d3 [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Extensions for types in the `fidl_fuchsia_net_dhcp` crate.
#![deny(missing_docs)]
use std::{collections::HashSet, num::NonZeroU64};
use anyhow::anyhow;
use async_trait::async_trait;
use fidl::endpoints::ServerEnd;
use fidl_fuchsia_net as fnet;
use fidl_fuchsia_net_dhcp as fnet_dhcp;
use fidl_fuchsia_net_ext::IntoExt as _;
use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin;
use fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext;
use fidl_fuchsia_net_routes as fnet_routes;
use fidl_fuchsia_net_routes_admin as fnet_routes_admin;
use fidl_fuchsia_net_routes_ext as fnet_routes_ext;
use futures::{pin_mut, Future, FutureExt, Stream, StreamExt as _, TryStreamExt as _};
use net_declare::fidl_ip_v4_with_prefix;
use net_types::{
ip::{Ipv4, Ipv4Addr},
SpecifiedAddr,
};
/// The default `fnet_dhcp::NewClientParams`.
pub fn default_new_client_params() -> fnet_dhcp::NewClientParams {
fnet_dhcp::NewClientParams {
configuration_to_request: Some(fnet_dhcp::ConfigurationToRequest {
routers: Some(true),
dns_servers: Some(true),
..fnet_dhcp::ConfigurationToRequest::default()
}),
request_ip_address: Some(true),
..fnet_dhcp::NewClientParams::default()
}
}
/// Configuration acquired by the DHCP client.
#[derive(Default, Debug)]
pub struct Configuration {
/// The acquired address.
pub address: Option<Address>,
/// Acquired DNS servers.
pub dns_servers: Vec<fnet::Ipv4Address>,
/// Acquired routers.
pub routers: Vec<SpecifiedAddr<Ipv4Addr>>,
}
/// Domain errors for this crate.
#[derive(thiserror::Error, Debug)]
pub enum Error {
/// A FIDL domain object was invalid.
#[error("invalid FIDL domain object: {0:?}")]
ApiViolation(anyhow::Error),
/// An error was encountered while manipulating a route set.
#[error("errors while manipulating route set: {0:?}")]
RouteSet(fnet_routes_admin::RouteSetError),
/// A FIDL error was encountered.
#[error("fidl error: {0:?}")]
Fidl(fidl::Error),
/// An invalid ClientExitReason was observed on the client's event stream.
#[error("invalid exit reason: {0:?}")]
WrongExitReason(fnet_dhcp::ClientExitReason),
/// No ClientExitReason was provided, when one was expected.
#[error("missing exit reason")]
MissingExitReason,
/// The client unexpectedly exited.
#[error("unexpected exit; reason: {0:?}")]
UnexpectedExit(Option<fnet_dhcp::ClientExitReason>),
}
/// The default subnet used as the destination while populating a
/// `fuchsia.net.stack.ForwardingEntry` while applying newly-discovered routers.
const DEFAULT_SUBNET: net_types::ip::Subnet<Ipv4Addr> = net_declare::net_subnet_v4!("0.0.0.0/0");
/// The default subnet used as the destination while populating a
/// `fuchsia.net.routes.RouteV4` while applying newly-discovered routers.
pub const DEFAULT_ADDR_PREFIX: fnet::Ipv4AddressWithPrefix = fidl_ip_v4_with_prefix!("0.0.0.0/0");
/// Applies a new set of routers to a given `fuchsia.net.stack.Stack` and
/// set of configured routers by deleting forwarding entries for
/// newly-absent routers and adding forwarding entries for newly-present
/// ones.
pub async fn apply_new_routers(
device_id: NonZeroU64,
route_set: &fnet_routes_admin::RouteSetV4Proxy,
configured_routers: &mut HashSet<SpecifiedAddr<Ipv4Addr>>,
new_routers: impl IntoIterator<Item = SpecifiedAddr<Ipv4Addr>>,
) -> Result<(), Error> {
let route = |next_hop: &SpecifiedAddr<Ipv4Addr>| fnet_routes_ext::Route::<Ipv4> {
action: fnet_routes_ext::RouteAction::Forward(fnet_routes_ext::RouteTarget {
outbound_interface: device_id.get(),
next_hop: Some(*next_hop),
}),
destination: DEFAULT_SUBNET,
properties: fnet_routes_ext::RouteProperties {
specified_properties: fnet_routes_ext::SpecifiedRouteProperties {
metric: fnet_routes::SpecifiedMetric::InheritedFromInterface(fnet_routes::Empty),
},
},
};
let new_routers = new_routers.into_iter().collect::<HashSet<_>>();
for router in configured_routers.difference(&new_routers) {
let removed: bool = route_set
.remove_route(
&route(router)
.try_into()
.map_err(|e| Error::ApiViolation(anyhow::Error::new(e)))?,
)
.await
.map_err(Error::Fidl)?
.map_err(Error::RouteSet)?;
if !removed {
tracing::warn!("attempt to remove {router} from RouteSet was no-op");
}
}
for router in new_routers.difference(&configured_routers) {
let added: bool = route_set
.add_route(
&route(router)
.try_into()
.map_err(|e| Error::ApiViolation(anyhow::Error::new(e)))?,
)
.await
.map_err(Error::Fidl)?
.map_err(Error::RouteSet)?;
if !added {
tracing::warn!("attempt to add {router} to RouteSet was no-op");
}
}
*configured_routers = new_routers;
Ok(())
}
impl TryFrom<fnet_dhcp::ClientWatchConfigurationResponse> for Configuration {
type Error = Error;
fn try_from(
fnet_dhcp::ClientWatchConfigurationResponse {
address,
dns_servers,
routers,
..
}: fnet_dhcp::ClientWatchConfigurationResponse,
) -> Result<Self, Error> {
let address = address
.map(
|fnet_dhcp::Address {
address, address_parameters, address_state_provider, ..
}| {
Ok(Address {
address: address
.ok_or(anyhow!("Ipv4AddressWithPrefix should be present"))?,
address_parameters: address_parameters
.ok_or(anyhow!("AddressParameters should be present"))?,
address_state_provider: address_state_provider
.ok_or(anyhow!("AddressStateProvider should be present"))?,
})
},
)
.transpose()
.map_err(Error::ApiViolation);
Ok(Configuration {
address: address?,
dns_servers: dns_servers.unwrap_or_default(),
routers: routers
.unwrap_or_default()
.into_iter()
.flat_map(|addr| SpecifiedAddr::new(addr.into_ext()))
.collect(),
})
}
}
/// An IPv4 address acquired by the DHCP client.
#[derive(Debug)]
pub struct Address {
/// The acquired address and discovered prefix length.
pub address: fnet::Ipv4AddressWithPrefix,
/// Parameters for the acquired address.
pub address_parameters: fnet_interfaces_admin::AddressParameters,
/// The server end for the AddressStateProvider owned by the DHCP client.
pub address_state_provider: ServerEnd<fnet_interfaces_admin::AddressStateProviderMarker>,
}
impl Address {
/// Adds this address via `fuchsia.net.interfaces.admin.Control`.
pub fn add_to(
self,
control: &fnet_interfaces_ext::admin::Control,
) -> Result<
(),
(
fnet::Ipv4AddressWithPrefix,
fnet_interfaces_ext::admin::TerminalError<
fnet_interfaces_admin::InterfaceRemovedReason,
>,
),
> {
let Self { address, address_parameters, address_state_provider } = self;
control
.add_address(&address.into_ext(), &address_parameters, address_state_provider)
.map_err(|e| (address, e))
}
}
type ConfigurationStream = async_utils::hanging_get::client::HangingGetStream<
fnet_dhcp::ClientProxy,
fnet_dhcp::ClientWatchConfigurationResponse,
>;
/// Produces a stream of acquired DHCP configuration by executing the hanging
/// get on the provided DHCP client proxy.
pub fn configuration_stream(
client: fnet_dhcp::ClientProxy,
) -> impl futures::Stream<Item = Result<Configuration, Error>> {
ConfigurationStream::new_eager_with_fn_ptr(client, fnet_dhcp::ClientProxy::watch_configuration)
.map_err(Error::Fidl)
.and_then(|config| futures::future::ready(Configuration::try_from(config)))
}
/// Extension trait on `fidl_fuchsia_net_dhcp::ClientProviderProxy`.
pub trait ClientProviderExt {
/// Construct a new DHCP client.
fn new_client_ext(
&self,
interface_id: NonZeroU64,
new_client_params: fnet_dhcp::NewClientParams,
) -> fnet_dhcp::ClientProxy;
/// Construct a new DHCP client, returning a ClientEnd instead of a Proxy.
fn new_client_end_ext(
&self,
interface_id: NonZeroU64,
new_client_params: fnet_dhcp::NewClientParams,
) -> fidl::endpoints::ClientEnd<fnet_dhcp::ClientMarker>;
}
impl ClientProviderExt for fnet_dhcp::ClientProviderProxy {
fn new_client_ext(
&self,
interface_id: NonZeroU64,
new_client_params: fnet_dhcp::NewClientParams,
) -> fnet_dhcp::ClientProxy {
let (client, server) = fidl::endpoints::create_proxy::<fnet_dhcp::ClientMarker>()
.expect("create DHCPv4 client fidl endpoints");
self.new_client(interface_id.get(), &new_client_params, server)
.expect("create new DHCPv4 client");
client
}
fn new_client_end_ext(
&self,
interface_id: NonZeroU64,
new_client_params: fnet_dhcp::NewClientParams,
) -> fidl::endpoints::ClientEnd<fnet_dhcp::ClientMarker> {
let (client, server) = fidl::endpoints::create_endpoints::<fnet_dhcp::ClientMarker>();
self.new_client(interface_id.get(), &new_client_params, server)
.expect("create new DHCPv4 client");
client
}
}
/// Extension trait on `fidl_fuchsia_net_dhcp::ClientProxy`.
#[async_trait]
pub trait ClientExt {
/// Shuts down the client, watching for the `GracefulShutdown` exit event.
///
/// Returns an error if the `GracefulShutdown` exit event is not observed.
async fn shutdown_ext(&self, event_stream: fnet_dhcp::ClientEventStream) -> Result<(), Error>;
}
#[async_trait]
impl ClientExt for fnet_dhcp::ClientProxy {
async fn shutdown_ext(&self, event_stream: fnet_dhcp::ClientEventStream) -> Result<(), Error> {
self.shutdown().map_err(Error::Fidl)?;
let stream = event_stream.map_err(Error::Fidl).try_filter_map(|event| async move {
match event {
fnet_dhcp::ClientEvent::OnExit { reason } => Ok(match reason {
fnet_dhcp::ClientExitReason::ClientAlreadyExistsOnInterface
| fnet_dhcp::ClientExitReason::WatchConfigurationAlreadyPending
| fnet_dhcp::ClientExitReason::InvalidInterface
| fnet_dhcp::ClientExitReason::InvalidParams
| fnet_dhcp::ClientExitReason::NetworkUnreachable
| fnet_dhcp::ClientExitReason::AddressRemovedByUser
| fnet_dhcp::ClientExitReason::AddressStateProviderError
| fnet_dhcp::ClientExitReason::UnableToOpenSocket => {
return Err(Error::WrongExitReason(reason))
}
fnet_dhcp::ClientExitReason::GracefulShutdown => Some(()),
}),
}
});
pin_mut!(stream);
stream.try_next().await.and_then(|option| match option {
Some(()) => Ok(()),
None => Err(Error::MissingExitReason),
})
}
}
/// Produces a stream that merges together the configuration hanging get
/// and the [`fnet_dhcp::ClientEvent::OnExit`] terminal event.
/// The client will be shut down when `shutdown_future` completes.
pub fn merged_configuration_stream(
// Takes a `[fidl::endpoints::ClientEnd]` so that we know we can take
// the event stream without panicking.
client_end: fidl::endpoints::ClientEnd<fnet_dhcp::ClientMarker>,
shutdown_future: impl Future<Output = ()> + 'static,
) -> impl Stream<Item = Result<Configuration, Error>> + 'static {
let client = client_end.into_proxy().expect("into_proxy is infallible");
let event_stream = client.take_event_stream();
let proxy_for_shutdown = client.clone();
let shutdown_future = shutdown_future.map(move |()| proxy_for_shutdown.shutdown());
let configs = configuration_stream(client);
fn prio_left(_: &mut ()) -> futures::stream::PollNext {
futures::stream::PollNext::Left
}
// Events yielded from a merged stream of client hanging gets or terminal
// events.
#[derive(Debug)]
enum MergedClientEvent {
// A terminal event yielded by the client's event stream.
Terminal(Result<fnet_dhcp::ClientEvent, Error>),
// Configuration acquired via the client's hanging get stream.
WatchConfiguration(Result<Configuration, Error>),
// A marker event indicating shutdown was requested by the caller.
ShutdownRequested,
}
futures::stream::select_with_strategy(
futures::stream::select_with_strategy(
event_stream.map_err(Error::Fidl).map(MergedClientEvent::Terminal),
// Merge in any error we observed telling the client to shut down so
// that it can be observed as a problem with the terminal event
// stream.
futures::stream::once(shutdown_future).map(|result| match result {
Ok(()) => MergedClientEvent::ShutdownRequested,
Err(shutdown_err) => MergedClientEvent::Terminal(Err(Error::Fidl(shutdown_err))),
}),
prio_left,
)
// If the terminal event stream ends without showing an event, then
// we're missing an exit reason.
.chain(futures::stream::once(futures::future::ready(MergedClientEvent::Terminal(
Err(Error::MissingExitReason),
)))),
configs.map(MergedClientEvent::WatchConfiguration),
// Prioritize yielding terminal events.
prio_left,
)
.scan((false, false), |(stream_ended, shutdown_requested), item| {
if *stream_ended {
return futures::future::ready(None);
}
futures::future::ready(Some(match item {
MergedClientEvent::ShutdownRequested => {
assert!(!*shutdown_requested);
*shutdown_requested = true;
None
}
MergedClientEvent::Terminal(terminal_result) => {
*stream_ended = true;
match terminal_result {
Ok(fnet_dhcp::ClientEvent::OnExit { reason }) => {
if *shutdown_requested {
match reason {
fnet_dhcp::ClientExitReason::GracefulShutdown => None,
fnet_dhcp::ClientExitReason::ClientAlreadyExistsOnInterface
| fnet_dhcp::ClientExitReason::WatchConfigurationAlreadyPending
| fnet_dhcp::ClientExitReason::InvalidInterface
| fnet_dhcp::ClientExitReason::InvalidParams
| fnet_dhcp::ClientExitReason::NetworkUnreachable
| fnet_dhcp::ClientExitReason::UnableToOpenSocket
| fnet_dhcp::ClientExitReason::AddressRemovedByUser
| fnet_dhcp::ClientExitReason::AddressStateProviderError => {
Some(Err(Error::WrongExitReason(reason)))
}
}
} else {
Some(Err(Error::UnexpectedExit(Some(reason))))
}
}
Err(err) => Some(Err(match err {
err @ (Error::ApiViolation(_)
| Error::RouteSet(_)
| Error::Fidl(_)
| Error::UnexpectedExit(_)) => err,
Error::WrongExitReason(reason) => {
if *shutdown_requested {
Error::WrongExitReason(reason)
} else {
Error::UnexpectedExit(Some(reason))
}
}
Error::MissingExitReason => {
if *shutdown_requested {
Error::MissingExitReason
} else {
Error::UnexpectedExit(None)
}
}
})),
}
}
MergedClientEvent::WatchConfiguration(watch_result) => {
match watch_result {
Ok(config) => Some(Ok(config)),
Err(err) => {
// Treat all errors as fatal and stop the stream.
*stream_ended = true;
Some(Err(err))
}
}
}
}))
})
.filter_map(|x| futures::future::ready(x))
}
/// Contains types used when testing the DHCP client.
pub mod testutil {
use super::*;
use fuchsia_async as fasync;
use fuchsia_zircon as zx;
use futures::future::ready;
/// Task for polling the DHCP client.
pub struct DhcpClientTask {
client: fnet_dhcp::ClientProxy,
task: fasync::Task<()>,
}
impl DhcpClientTask {
/// Creates and returns an async task that polls the DHCP client.
pub fn new(
client: fnet_dhcp::ClientProxy,
id: NonZeroU64,
route_set: fnet_routes_admin::RouteSetV4Proxy,
control: fnet_interfaces_ext::admin::Control,
) -> DhcpClientTask {
DhcpClientTask {
client: client.clone(),
task: fasync::Task::spawn(async move {
let fnet_interfaces_admin::GrantForInterfaceAuthorization {
interface_id,
token,
} = control
.get_authorization_for_interface()
.await
.expect("get interface authorization");
route_set
.authenticate_for_interface(
fnet_interfaces_admin::ProofOfInterfaceAuthorization {
interface_id,
token,
},
)
.await
.expect("authenticate should not have FIDL error")
.expect("authenticate should succeed");
let mut final_routers =
configuration_stream(client)
.scan((), |(), item| {
ready(match item {
Err(e) => match e {
// Observing `PEER_CLOSED` is expected after the
// client is shut down, so rather than returning an
// error, simply end the stream.
Error::Fidl(fidl::Error::ClientChannelClosed {
status: zx::Status::PEER_CLOSED,
protocol_name: _,
}) => None,
Error::Fidl(_)
| Error::ApiViolation(_)
| Error::RouteSet(_)
| Error::WrongExitReason(_)
| Error::UnexpectedExit(_)
| Error::MissingExitReason => Some(Err(e)),
},
Ok(item) => Some(Ok(item)),
})
})
.try_fold(
HashSet::<SpecifiedAddr<Ipv4Addr>>::new(),
|mut routers,
Configuration {
address,
dns_servers: _,
routers: new_routers,
}| {
let control = &control;
let route_set = &route_set;
async move {
if let Some(address) = address {
address
.add_to(control)
.expect("add address should succeed");
}
apply_new_routers(
id,
&route_set,
&mut routers,
new_routers,
)
.await
.expect("applying new routers should succeed");
Ok(routers)
}
},
)
.await
.expect("watch_configuration should succeed");
// DHCP client is being shut down, so we should remove all the routers.
apply_new_routers(id, &route_set, &mut final_routers, Vec::new())
.await
.expect("removing all routers should succeed");
}),
}
}
/// Shuts down the running DHCP client and waits for the poll task to complete.
pub async fn shutdown(self) -> Result<(), Error> {
let DhcpClientTask { client, task } = self;
client
.shutdown_ext(client.take_event_stream())
.await
.expect("client shutdown should succeed");
task.await;
Ok(())
}
}
}
#[cfg(test)]
mod test {
use crate::{ClientExt as _, Error, DEFAULT_ADDR_PREFIX};
use std::{collections::HashSet, num::NonZeroU64};
use assert_matches::assert_matches;
use fidl::endpoints::RequestStream;
use fidl_fuchsia_net as fnet;
use fidl_fuchsia_net_dhcp as fnet_dhcp;
use fidl_fuchsia_net_ext::IntoExt as _;
use fidl_fuchsia_net_routes as fnet_routes;
use fidl_fuchsia_net_routes_admin as fnet_routes_admin;
use fuchsia_async as fasync;
use futures::{channel::oneshot, join, pin_mut, FutureExt as _, StreamExt as _};
use net_declare::net_ip_v4;
use net_types::{
ip::{Ip, Ipv4, Ipv4Addr},
SpecifiedAddr, SpecifiedAddress as _, Witness as _,
};
use proptest::prelude::*;
use test_case::test_case;
#[derive(proptest_derive::Arbitrary, Clone, Debug)]
struct Address {
include_address: bool,
include_address_parameters: bool,
include_address_state_provider: bool,
}
// For the purposes of this test, we only care about exercising the case
// where addresses are specified or unspecified, with no need to be able
// to distinguish between specified addresses.
#[derive(proptest_derive::Arbitrary, Clone, Debug)]
enum GeneratedIpv4Addr {
Specified,
Unspecified,
}
impl From<GeneratedIpv4Addr> for Ipv4Addr {
fn from(value: GeneratedIpv4Addr) -> Self {
match value {
GeneratedIpv4Addr::Specified => net_ip_v4!("1.1.1.1"),
GeneratedIpv4Addr::Unspecified => Ipv4::UNSPECIFIED_ADDRESS,
}
}
}
#[derive(proptest_derive::Arbitrary, Clone, Debug)]
struct ClientWatchConfigurationResponse {
address: Option<Address>,
dns_servers: Option<Vec<GeneratedIpv4Addr>>,
routers: Option<Vec<GeneratedIpv4Addr>>,
}
proptest! {
#![proptest_config(ProptestConfig {
failure_persistence: Some(
Box::<proptest::test_runner::MapFailurePersistence>::default()
),
..ProptestConfig::default()
})]
#[test]
fn try_into_configuration(response: ClientWatchConfigurationResponse) {
let make_fidl = |response: &ClientWatchConfigurationResponse| {
let ClientWatchConfigurationResponse {
address,
dns_servers,
routers,
} = response.clone();
fnet_dhcp::ClientWatchConfigurationResponse {
address: address.map(
|Address {
include_address,
include_address_parameters,
include_address_state_provider
}| {
fnet_dhcp::Address {
address: include_address.then_some(
fidl_fuchsia_net::Ipv4AddressWithPrefix {
addr: net_ip_v4!("1.1.1.1").into_ext(),
prefix_len: 24,
}
),
address_parameters: include_address_parameters.then_some(
fidl_fuchsia_net_interfaces_admin::AddressParameters::default()
),
address_state_provider: include_address_state_provider.then_some({
let (_, server) = fidl::endpoints::create_endpoints();
server
}),
..Default::default()
}
}),
dns_servers: dns_servers.map(
|list| list.into_iter().map(
|addr: GeneratedIpv4Addr| net_types::ip::Ipv4Addr::from(
addr
).into_ext()
).collect()),
routers: routers.map(
|list| list.into_iter().map(
|addr: GeneratedIpv4Addr| net_types::ip::Ipv4Addr::from(
addr
).into_ext()
).collect()),
..Default::default()
}
};
let result = crate::Configuration::try_from(make_fidl(&response));
if let Some(crate::Configuration {
address: result_address,
dns_servers: result_dns_servers,
routers: result_routers,
}) = match response.address {
Some(
Address {
include_address,
include_address_parameters,
include_address_state_provider,
}
) => {
prop_assert_eq!(
!(
include_address &&
include_address_parameters &&
include_address_state_provider
),
result.is_err(),
"must reject partially-filled address object"
);
match result {
Err(_) => None,
Ok(configuration) => Some(configuration),
}
}
None => {
prop_assert!(result.is_ok(), "absent address is always accepted");
Some(result.unwrap())
}
} {
let fnet_dhcp::ClientWatchConfigurationResponse {
dns_servers: fidl_dns_servers,
routers: fidl_routers,
address: fidl_address,
..
} = make_fidl(&response);
let want_routers: Vec<net_types::ip::Ipv4Addr> = fidl_routers
.unwrap_or_default()
.into_iter()
.flat_map(
|addr| Some(addr.into_ext()).filter(net_types::ip::Ipv4Addr::is_specified)
)
.collect();
prop_assert_eq!(
result_dns_servers,
fidl_dns_servers.unwrap_or_default()
);
prop_assert_eq!(
result_routers.into_iter().map(|addr| addr.get()).collect::<Vec<_>>(),
want_routers
);
if let Some(
crate::Address {
address: result_address,
address_parameters: result_address_parameters,
address_state_provider: _
}
) = result_address {
let fnet_dhcp::Address {
address: fidl_address,
address_parameters: fidl_address_parameters,
address_state_provider: _,
..
} = fidl_address.expect("should be present");
prop_assert_eq!(Some(result_address), fidl_address);
prop_assert_eq!(Some(result_address_parameters), fidl_address_parameters);
}
}
}
}
#[fasync::run_singlethreaded(test)]
async fn apply_new_routers() {
let (route_set, route_set_stream) =
fidl::endpoints::create_proxy_and_stream::<fnet_routes_admin::RouteSetV4Marker>()
.expect("create route set proxy and stream");
const REMOVED_ROUTER: Ipv4Addr = net_ip_v4!("1.1.1.1");
const KEPT_ROUTER: Ipv4Addr = net_ip_v4!("2.2.2.2");
const ADDED_ROUTER: Ipv4Addr = net_ip_v4!("3.3.3.3");
let mut configured_routers = [REMOVED_ROUTER, KEPT_ROUTER]
.into_iter()
.map(|addr| SpecifiedAddr::new(addr).unwrap())
.collect::<HashSet<_>>();
let device_id = const_unwrap::const_unwrap_option(NonZeroU64::new(5));
let apply_fut = crate::apply_new_routers(
device_id,
&route_set,
&mut configured_routers,
vec![
SpecifiedAddr::new(KEPT_ROUTER).unwrap(),
SpecifiedAddr::new(ADDED_ROUTER).unwrap(),
],
)
.fuse();
let route_set_fut = async move {
pin_mut!(route_set_stream);
let (route, responder) = route_set_stream
.next()
.await
.expect("should not have ended")
.expect("should not have error")
.into_remove_route()
.expect("should be remove route");
assert_eq!(
route,
fnet_routes::RouteV4 {
destination: DEFAULT_ADDR_PREFIX,
action: fnet_routes::RouteActionV4::Forward(fnet_routes::RouteTargetV4 {
outbound_interface: device_id.get(),
next_hop: Some(Box::new(REMOVED_ROUTER.into_ext()))
}),
properties: fnet_routes::RoutePropertiesV4 {
specified_properties: Some(fnet_routes::SpecifiedRouteProperties {
metric: Some(fnet_routes::SpecifiedMetric::InheritedFromInterface(
fnet_routes::Empty
)),
..Default::default()
}),
..Default::default()
}
}
);
responder.send(Ok(true)).expect("responder send");
let (route, responder) = route_set_stream
.next()
.await
.expect("should not have ended")
.expect("should not have error")
.into_add_route()
.expect("should be add route");
assert_eq!(
route,
fnet_routes::RouteV4 {
destination: DEFAULT_ADDR_PREFIX,
action: fnet_routes::RouteActionV4::Forward(fnet_routes::RouteTargetV4 {
outbound_interface: device_id.get(),
next_hop: Some(Box::new(ADDED_ROUTER.into_ext()))
}),
properties: fnet_routes::RoutePropertiesV4 {
specified_properties: Some(fnet_routes::SpecifiedRouteProperties {
metric: Some(fnet_routes::SpecifiedMetric::InheritedFromInterface(
fnet_routes::Empty
)),
..Default::default()
}),
..Default::default()
}
}
);
responder.send(Ok(true)).expect("responder send");
}
.fuse();
pin_mut!(apply_fut, route_set_fut);
let (apply_result, ()) = join!(apply_fut, route_set_fut);
apply_result.expect("apply should succeed");
}
#[test_case(
None => matches Err(Error::MissingExitReason) ; "no exit reason should cause error"
)]
#[test_case(
Some(fnet_dhcp::ClientExitReason::NetworkUnreachable) => matches Err(Error::WrongExitReason(fnet_dhcp::ClientExitReason::NetworkUnreachable)) ;
"wrong exit reason should cause error"
)]
#[test_case(
Some(fnet_dhcp::ClientExitReason::GracefulShutdown) => matches Ok(()) ;
"GracefulShutdown is correct exit reason"
)]
#[fasync::run_singlethreaded(test)]
async fn shutdown_ext(exit_reason: Option<fnet_dhcp::ClientExitReason>) -> Result<(), Error> {
let (client, stream) =
fidl::endpoints::create_proxy_and_stream::<fnet_dhcp::ClientMarker>()
.expect("create DHCP client proxy and stream");
if let Some(exit_reason) = exit_reason {
stream.control_handle().send_on_exit(exit_reason).expect("send on exit");
}
let shutdown_fut = client.shutdown_ext(client.take_event_stream()).fuse();
let server_fut = async move {
pin_mut!(stream);
let _client_control_handle = stream
.next()
.await
.expect("should not have ended")
.expect("should not have FIDL error")
.into_shutdown()
.expect("should be shutdown request");
}
.fuse();
let (shutdown_result, ()) = join!(shutdown_fut, server_fut);
shutdown_result
}
#[test_case(
None ; "client does not exit until we tell it to"
)]
#[test_case(
Some(fnet_dhcp::ClientExitReason::NetworkUnreachable);
"client exits due to network unreachable"
)]
#[test_case(
Some(fnet_dhcp::ClientExitReason::GracefulShutdown);
"client exits due to GracefulShutdown of its own accord"
)]
#[fasync::run_singlethreaded(test)]
async fn merged_configuration_stream_exit(exit_reason: Option<fnet_dhcp::ClientExitReason>) {
const ADDRESS: fnet::Ipv4AddressWithPrefix =
net_declare::fidl_ip_v4_with_prefix!("192.0.2.1/32");
let (client, stream) = fidl::endpoints::create_request_stream::<fnet_dhcp::ClientMarker>()
.expect("create DHCP client client end and stream");
let server_fut = async move {
pin_mut!(stream);
let watch_config_responder = stream
.next()
.await
.expect("should not have ended")
.expect("should not have FIDL error")
.into_watch_configuration()
.expect("should be watch configuration");
let (_asp_client, asp_server) = fidl::endpoints::create_endpoints::<
fidl_fuchsia_net_interfaces_admin::AddressStateProviderMarker,
>();
watch_config_responder
.send(fnet_dhcp::ClientWatchConfigurationResponse {
address: Some(fnet_dhcp::Address {
address: Some(ADDRESS),
address_parameters: Some(
fidl_fuchsia_net_interfaces_admin::AddressParameters::default(),
),
address_state_provider: Some(asp_server),
..Default::default()
}),
..Default::default()
})
.expect("should successfully respond to hanging get");
// Should keep polling hanging get.
let _watch_config_responder = stream
.next()
.await
.expect("should not have ended")
.expect("should not have FIDL error")
.into_watch_configuration()
.expect("should be watch configuration");
if let Some(exit_reason) = exit_reason {
stream.control_handle().send_on_exit(exit_reason).expect("send on exit");
} else {
let _client_control_handle = stream
.next()
.await
.expect("should not have ended")
.expect("should not have FIDL error")
.into_shutdown()
.expect("should be shutdown request");
stream
.control_handle()
.send_on_exit(fnet_dhcp::ClientExitReason::GracefulShutdown)
.expect("send on exit");
}
}
.fuse();
let client_fut = async move {
let (shutdown_sender, shutdown_receiver) = oneshot::channel();
let config_stream = crate::merged_configuration_stream(
client,
shutdown_receiver.map(|res| res.expect("shutdown_sender should not be dropped")),
)
.fuse();
pin_mut!(config_stream);
let initial_config = config_stream.next().await.expect("should not have ended");
let address = assert_matches!(initial_config,
Ok(crate::Configuration {
address: Some(crate::Address { address, .. }),
..
}) => address
);
assert_eq!(address, ADDRESS);
if let Some(want_reason) = exit_reason {
// The DHCP client exits on its own.
let item = config_stream.next().await.expect("should not have ended");
let got_reason = assert_matches!(item,
Err(Error::UnexpectedExit(Some(reason))) => reason
);
assert_eq!(got_reason, want_reason);
// The stream should have ended.
assert_matches!(config_stream.next().await, None);
} else {
// Poll the config stream once to indicate we're still hanging-getting.
assert_matches!(config_stream.next().now_or_never(), None);
shutdown_sender.send(()).expect("shutdown receiver should not have been dropped");
// Having sent a shutdown request, we expect the client to exit
// and the stream to end with no error.
assert_matches!(config_stream.next().await, None);
}
};
let ((), ()) = join!(client_fut, server_fut);
}
}