blob: f23ec01c2c4d8128c93e8583b70bf9ec639567a9 [file] [log] [blame]
// Copyright 2020 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.
#![cfg(test)]
use anyhow::Context as _;
use dhcp::protocol::IntoFidlExt as _;
use fuchsia_async::TimeoutExt as _;
use futures::future::TryFutureExt as _;
use futures::stream::{self, StreamExt as _, TryStreamExt as _};
use net_declare::{fidl_ip_v4, fidl_mac};
use netstack_testing_common::environments::{KnownServices, Netstack2, TestSandboxExt as _};
use netstack_testing_common::Result;
use netstack_testing_macros::variants_test;
use std::convert::TryFrom as _;
// Encapsulates a minimal configuration needed to test a DHCP client/server combination.
struct DhcpTestConfig<'a> {
// Server IP address.
server_addr: fidl_fuchsia_net::Ipv4Address,
// Address pool for the DHCP server.
managed_addrs: dhcp::configuration::ManagedAddresses,
// Device name generated by netemul.
// TODO(https://fxbug.dev/73252): De-magic these strings by setting the interface
// name from these test through the Netstack API.
netemul_device_name: &'a str,
}
impl DhcpTestConfig<'_> {
fn expected_acquired(&self) -> fidl_fuchsia_net::Subnet {
let Self {
server_addr: _,
managed_addrs:
dhcp::configuration::ManagedAddresses { mask, pool_range_start, pool_range_stop: _ },
netemul_device_name: _,
} = self;
fidl_fuchsia_net::Subnet {
addr: fidl_fuchsia_net::IpAddress::Ipv4(pool_range_start.into_fidl()),
prefix_len: mask.ones(),
}
}
fn server_subnet(&self) -> fidl_fuchsia_net::Subnet {
let Self {
server_addr,
managed_addrs:
dhcp::configuration::ManagedAddresses { mask, pool_range_start: _, pool_range_stop: _ },
netemul_device_name: _,
} = self;
fidl_fuchsia_net::Subnet {
addr: fidl_fuchsia_net::IpAddress::Ipv4(*server_addr),
prefix_len: mask.ones(),
}
}
fn dhcp_parameters(&self) -> Vec<fidl_fuchsia_net_dhcp::Parameter> {
let Self { server_addr, netemul_device_name, managed_addrs } = self;
vec![
fidl_fuchsia_net_dhcp::Parameter::IpAddrs(vec![*server_addr]),
fidl_fuchsia_net_dhcp::Parameter::AddressPool(managed_addrs.into_fidl()),
fidl_fuchsia_net_dhcp::Parameter::BoundDeviceNames(vec![
netemul_device_name.to_string()
]),
]
}
}
fn default_test_config() -> Result<DhcpTestConfig<'static>> {
let mask = dhcp::configuration::SubnetMask::try_from(25)?;
Ok(DhcpTestConfig {
server_addr: fidl_ip_v4!("192.168.0.1"),
managed_addrs: dhcp::configuration::ManagedAddresses {
mask: mask,
pool_range_start: std::net::Ipv4Addr::new(192, 168, 0, 2),
pool_range_stop: std::net::Ipv4Addr::new(192, 168, 0, 5),
},
netemul_device_name: "eth2",
})
}
fn alt_test_config() -> Result<DhcpTestConfig<'static>> {
let mask = dhcp::configuration::SubnetMask::try_from(24)?;
Ok(DhcpTestConfig {
server_addr: fidl_ip_v4!("192.168.1.1"),
managed_addrs: dhcp::configuration::ManagedAddresses {
mask: mask,
pool_range_start: std::net::Ipv4Addr::new(192, 168, 1, 2),
pool_range_stop: std::net::Ipv4Addr::new(192, 168, 1, 5),
},
netemul_device_name: "eth3",
})
}
const DEFAULT_NETWORK_NAME: &'static str = "net";
/// Endpoints in DHCP tests are either
/// 1. attached to the server stack, which will have DHCP servers serving on them.
/// 2. or attached to the client stack, which will have DHCP clients started on
/// them to request addresses.
enum DhcpTestEnv {
Client(fidl_fuchsia_net::Subnet),
Server,
}
struct DhcpTestEndpoint<'a> {
name: &'a str,
env: DhcpTestEnv,
/// static_addrs holds the static addresses configured on the endpoint
/// before any server or client is started.
static_addrs: Vec<fidl_fuchsia_net::Subnet>,
}
/// A network can have multiple endpoints. Each endpoint can be attached to a
/// different stack.
struct DhcpTestNetwork<'a> {
name: &'a str,
eps: &'a mut [DhcpTestEndpoint<'a>],
}
async fn set_server_parameters(
dhcp_server: &fidl_fuchsia_net_dhcp::Server_Proxy,
parameters: &mut [fidl_fuchsia_net_dhcp::Parameter],
) -> Result {
stream::iter(parameters.iter_mut())
.map(Ok)
.try_for_each_concurrent(None, |parameter| async move {
dhcp_server
.set_parameter(parameter)
.await
.context("failed to call dhcp/Server.SetParameter")?
.map_err(fuchsia_zircon::Status::from_raw)
.with_context(|| {
format!("dhcp/Server.SetParameter({:?}) returned error", parameter)
})
})
.await
.context("failed to set server parameters")
}
async fn client_acquires_addr(
client_env: &netemul::TestEnvironment<'_>,
interfaces: &[netemul::TestInterface<'_>],
expected_acquired: fidl_fuchsia_net::Subnet,
cycles: usize,
client_renews: bool,
) -> Result {
let client_interface_state = client_env
.connect_to_service::<fidl_fuchsia_net_interfaces::StateMarker>()
.context("failed to connect to client fuchsia.net.interfaces/State")?;
let (watcher, watcher_server) =
::fidl::endpoints::create_proxy::<fidl_fuchsia_net_interfaces::WatcherMarker>()
.context("failed to create interface watcher proxy")?;
let () = client_interface_state
.get_watcher(fidl_fuchsia_net_interfaces::WatcherOptions::EMPTY, watcher_server)
.context("failed to initialize interface watcher")?;
for interface in interfaces.iter() {
let mut properties =
fidl_fuchsia_net_interfaces_ext::InterfaceState::Unknown(interface.id());
for () in std::iter::repeat(()).take(cycles) {
// Enable the interface and assert that binding fails before the address is acquired.
let () = interface.stop_dhcp().await.context("failed to stop DHCP")?;
let () = interface.set_link_up(true).await.context("failed to bring link up")?;
matches::assert_matches!(
bind(&client_env, expected_acquired).await,
Err(e @ anyhow::Error {..})
if e.downcast_ref::<std::io::Error>()
.ok_or(anyhow::anyhow!("bind() did not return std::io::Error"))?
.raw_os_error() == Some(libc::EADDRNOTAVAIL)
);
let () = interface.start_dhcp().await.context("failed to start DHCP")?;
let () = assert_interface_assigned_addr(
client_env,
expected_acquired,
&watcher,
&mut properties,
)
.await?;
// If test covers renewal behavior, check that a subsequent interface changed event
// occurs where the client retains its address, i.e. that it successfully renewed its
// lease. It will take lease_length/2 duration for the client to renew its address
// and trigger the subsequent interface changed event.
if client_renews {
let () = assert_interface_assigned_addr(
client_env,
expected_acquired,
&watcher,
&mut properties,
)
.await?;
}
// Set interface online signal to down and wait for address to be removed.
let () = interface.set_link_up(false).await.context("failed to bring link down")?;
let () = fidl_fuchsia_net_interfaces_ext::wait_interface_with_id(
fidl_fuchsia_net_interfaces_ext::event_stream(watcher.clone()),
&mut properties,
|fidl_fuchsia_net_interfaces_ext::Properties {
id: _,
addresses,
online: _,
device_class: _,
has_default_ipv4_route: _,
has_default_ipv6_route: _,
name: _,
}| {
if addresses.iter().any(|&fidl_fuchsia_net_interfaces_ext::Address { addr }| {
addr == expected_acquired
}) {
None
} else {
Some(())
}
},
)
.await
.context("failed to wait for address removal")?;
}
}
Ok(())
}
async fn assert_interface_assigned_addr(
client_env: &netemul::TestEnvironment<'_>,
expected_acquired: fidl_fuchsia_net::Subnet,
watcher: &fidl_fuchsia_net_interfaces::WatcherProxy,
mut properties: &mut fidl_fuchsia_net_interfaces_ext::InterfaceState,
) -> Result {
let addr = fidl_fuchsia_net_interfaces_ext::wait_interface_with_id(
fidl_fuchsia_net_interfaces_ext::event_stream(watcher.clone()),
&mut properties,
|fidl_fuchsia_net_interfaces_ext::Properties {
id: _,
addresses,
online: _,
device_class: _,
has_default_ipv4_route: _,
has_default_ipv6_route: _,
name: _,
}| {
addresses.iter().find_map(
|&fidl_fuchsia_net_interfaces_ext::Address { addr: subnet }| {
let fidl_fuchsia_net::Subnet { addr, prefix_len: _ } = subnet;
match addr {
fidl_fuchsia_net::IpAddress::Ipv4(_) => Some(subnet),
fidl_fuchsia_net::IpAddress::Ipv6(_) => None,
}
},
)
},
)
.map_err(anyhow::Error::from)
.on_timeout(
// Netstack's DHCP client retries every 3 seconds. At the time of writing, dhcpd
// loses the race here and only starts after the first request from the DHCP
// client, which results in a 3 second toll. This test typically takes ~4.5
// seconds; we apply a large multiple to be safe.
fuchsia_async::Time::after(fuchsia_zircon::Duration::from_seconds(60)),
|| Err(anyhow::anyhow!("timed out")),
)
.await
.context("failed to observe DHCP acquisition on client ep")?;
assert_eq!(addr, expected_acquired);
// Address acquired; bind is expected to succeed.
let _: std::net::UdpSocket =
bind(&client_env, expected_acquired).await.context("binding to UDP socket failed")?;
Ok(())
}
fn bind<'a>(
client_env: &'a netemul::TestEnvironment<'_>,
fidl_fuchsia_net::Subnet { addr, prefix_len: _ }: fidl_fuchsia_net::Subnet,
) -> impl futures::Future<Output = Result<std::net::UdpSocket>> + 'a {
use netemul::EnvironmentUdpSocket as _;
let fidl_fuchsia_net_ext::IpAddress(ip_address) = addr.into();
std::net::UdpSocket::bind_in_env(client_env, std::net::SocketAddr::new(ip_address, 0))
}
/// test_dhcp starts 2 netstacks, client and server, and attaches endpoints to
/// them in potentially multiple networks based on the input network
/// configuration.
///
/// DHCP servers are started on the server side, configured from the dhcp_parameters argument.
/// Notice based on the configuration, it is possible that multiple servers are started and bound
/// to different endpoints.
///
/// DHCP clients are started on each client endpoint, attempt to acquire
/// addresses through DHCP and compare them to expected address.
///
/// The DHCP client's renewal path is tested with the `client_renews` flag. Since a client only
/// renews after lease_length/2 seconds has passed, `dhcp_parameters` should include a short lease
/// length when `client_renews` is set.
async fn test_dhcp<E: netemul::Endpoint>(
name: &str,
network_configs: &mut [DhcpTestNetwork<'_>],
dhcp_parameters: &mut [&mut [fidl_fuchsia_net_dhcp::Parameter]],
cycles: usize,
client_renews: bool,
) -> Result {
let sandbox = netemul::TestSandbox::new().context("failed to create sandbox")?;
let sandbox_ref = &sandbox;
let server_environments = stream::iter(dhcp_parameters)
.enumerate()
.then(|(id, parameters)| async move {
let server_environment = sandbox_ref
.create_netstack_environment_with::<Netstack2, _, _>(
format!("{}_server_{}", name, id),
&[KnownServices::SecureStash],
)
.context("failed to create server environment")?;
let launcher =
server_environment.get_launcher().context("failed to create launcher")?;
let dhcpd = fuchsia_component::client::launch(
&launcher,
KnownServices::DhcpServer.get_url().to_string(),
None,
)
.context("failed to start dhcpd")?;
let dhcp_server = dhcpd
.connect_to_service::<fidl_fuchsia_net_dhcp::Server_Marker>()
.context("failed to connect to DHCP server")?;
let () = set_server_parameters(&dhcp_server, parameters).await?;
Result::Ok((server_environment, dhcpd))
})
.try_collect::<Vec<_>>()
.await?;
let client_environment = sandbox
.create_netstack_environment::<Netstack2, _>(format!("{}_client", name))
.context("failed to create client environment")?;
let client_env_ref = &client_environment;
let server_environments_ref = &server_environments;
let networks = stream::iter(network_configs)
.then(|DhcpTestNetwork { name, eps }| async move {
let network =
sandbox_ref.create_network(*name).await.context("failed to create network")?;
// `network` is returned at the end of the scope so it is not
// dropped.
let network_ref = &network;
let eps = stream::iter(eps.into_iter())
.then(|DhcpTestEndpoint { name, env, static_addrs }| async move {
let test_environments = match env {
DhcpTestEnv::Client(subnet) => {
let _: &fidl_fuchsia_net::Subnet = subnet;
stream::iter(std::iter::once(client_env_ref)).left_stream()
}
DhcpTestEnv::Server => {
stream::iter(server_environments_ref.iter().map(|(env, _dhcpd)| env))
.right_stream()
}
};
let name = &*name;
let interfaces = test_environments
.enumerate()
.zip(futures::stream::repeat(static_addrs.clone()))
.then(|((id, test_environment), static_addrs)| async move {
let name = format!("{}-{}", name, id);
let iface = test_environment
.join_network::<E, _>(
network_ref,
name,
&netemul::InterfaceConfig::None,
)
.await
.context("failed to create endpoint")?;
for a in static_addrs.into_iter() {
let () = iface
.add_ip_addr(a)
.await
.with_context(|| format!("failed to add address {:?}", a))?;
}
Ok::<_, anyhow::Error>(iface)
})
.try_collect::<Vec<_>>()
.await
.context("failed to create interface")?;
Result::Ok((env, interfaces))
})
.try_collect::<Vec<_>>()
.await?;
Result::Ok((network, eps))
})
.try_collect::<Vec<_>>()
.await?;
let () = stream::iter(server_environments.iter())
.map(Ok)
.try_for_each_concurrent(None, |(_env, dhcpd)| async move {
let dhcp_server = dhcpd
.connect_to_service::<fidl_fuchsia_net_dhcp::Server_Marker>()
.context("failed to connect to DHCP server")?;
dhcp_server
.start_serving()
.await
.context("failed to call dhcp/Server.StartServing")?
.map_err(fuchsia_zircon::Status::from_raw)
.context("dhcp/Server.StartServing returned error")
})
.await?;
stream::iter(
// Iterate over references to prevent filter from dropping endpoints.
networks
.iter()
.flat_map(|(_, eps)| eps)
.filter_map(|(env, interfaces)| match env {
// We only care about whether client endpoints can acquire
// addresses through DHCP client or not.
DhcpTestEnv::Client(expected_acquired) => Some((expected_acquired, interfaces)),
DhcpTestEnv::Server => None,
})
.map(Result::Ok),
)
.try_for_each(|(expected_acquired, interfaces)| async move {
client_acquires_addr(client_env_ref, interfaces, *expected_acquired, cycles, client_renews)
.await
})
.await
}
#[variants_test]
async fn acquire_dhcp_with_dhcpd_bound_device<E: netemul::Endpoint>(name: &str) -> Result {
let config = default_test_config().context("failed to create test config")?;
test_dhcp::<E>(
name,
&mut [DhcpTestNetwork {
name: DEFAULT_NETWORK_NAME,
eps: &mut [
DhcpTestEndpoint {
name: "server-ep",
env: DhcpTestEnv::Server,
static_addrs: vec![config.server_subnet()],
},
DhcpTestEndpoint {
name: "client-ep",
env: DhcpTestEnv::Client(config.expected_acquired()),
static_addrs: Vec::new(),
},
],
}],
&mut [&mut config.dhcp_parameters()],
1,
false,
)
.await
}
#[variants_test]
async fn acquire_dhcp_then_renew_with_dhcpd_bound_device<E: netemul::Endpoint>(
name: &str,
) -> Result {
// TODO(https://fxbug.dev/74365): Reenable flaky test once underlying race is fixed.
// #[ignore] doesn't work with #[variants_test] so we have to employ the following hack.
if true {
return Ok(());
}
let config = default_test_config().context("failed to create test config")?;
let mut dhcp_parameters = config.dhcp_parameters();
let () = dhcp_parameters.push(fidl_fuchsia_net_dhcp::Parameter::Lease(
fidl_fuchsia_net_dhcp::LeaseLength {
default: Some(4),
max: Some(4),
..fidl_fuchsia_net_dhcp::LeaseLength::EMPTY
},
));
test_dhcp::<E>(
name,
&mut [DhcpTestNetwork {
name: DEFAULT_NETWORK_NAME,
eps: &mut [
DhcpTestEndpoint {
name: "server-ep",
env: DhcpTestEnv::Server,
static_addrs: vec![config.server_subnet()],
},
DhcpTestEndpoint {
name: "client-ep",
env: DhcpTestEnv::Client(config.expected_acquired()),
static_addrs: Vec::new(),
},
],
}],
&mut [&mut dhcp_parameters],
1,
true,
)
.await
}
#[variants_test]
async fn acquire_dhcp_with_dhcpd_bound_device_dup_addr<E: netemul::Endpoint>(name: &str) -> Result {
let config = default_test_config().context("failed to create test config")?;
let expected_acquired = config.expected_acquired();
let expected_addr = match expected_acquired.addr {
fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address { addr: mut octets }) => {
// We expect to assign the address numericaly succeeding the default client address
// since the default client address will be assigned to a neighbor of the client so
// the client should decline the offer and restart DHCP.
*octets.iter_mut().last().expect("IPv4 addresses have a non-zero number of octets") +=
1;
fidl_fuchsia_net::Subnet {
addr: fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address {
addr: octets,
}),
..expected_acquired
}
}
fidl_fuchsia_net::IpAddress::Ipv6(a) => {
return Err(anyhow::anyhow!("expected IPv4 address; got IPv6 address = {:?}", a));
}
};
// Tests that if the client detects an address is already assigned to a neighbor,
// the client will decline the request and restart DHCP. In this test, the neighbor
// with the address assigned is the DHCP server.
test_dhcp::<E>(
name,
&mut [DhcpTestNetwork {
name: DEFAULT_NETWORK_NAME,
eps: &mut [
// Use two separate endpoints for the server so that its unicast responses to
// the expected address acquired actually reach the network. This is not a hack around
// Fuchsia behavior: Linux behaves the same way. On Linux, given an endpoint that
// is BINDTODEVICE, unicasting to an IP address bound to another endpoint on the
// same host WILL reach the network, rather than going through loopback. However,
// if the first endpoint is NOT BINDTODEVICE, the outgoing message will be sent via
// loopback.
DhcpTestEndpoint {
name: "server-ep",
env: DhcpTestEnv::Server,
static_addrs: vec![config.server_subnet()],
},
DhcpTestEndpoint {
name: "server-ep2",
env: DhcpTestEnv::Server,
static_addrs: vec![expected_acquired],
},
DhcpTestEndpoint {
name: "client-ep",
env: DhcpTestEnv::Client(expected_addr),
static_addrs: Vec::new(),
},
],
}],
&mut [&mut config.dhcp_parameters()],
1,
false,
)
.await
}
#[variants_test]
async fn acquire_dhcp_with_multiple_network<E: netemul::Endpoint>(name: &str) -> Result {
let default_config = default_test_config().context("failed to create default test config")?;
let alt_config = alt_test_config().context("failed to create alt test config")?;
test_dhcp::<E>(
name,
&mut [
DhcpTestNetwork {
name: "net1",
eps: &mut [
DhcpTestEndpoint {
name: "server-ep1",
env: DhcpTestEnv::Server,
static_addrs: vec![default_config.server_subnet()],
},
DhcpTestEndpoint {
name: "client-ep1",
env: DhcpTestEnv::Client(default_config.expected_acquired()),
static_addrs: Vec::new(),
},
],
},
DhcpTestNetwork {
name: "net2",
eps: &mut [
DhcpTestEndpoint {
name: "server-ep2",
env: DhcpTestEnv::Server,
static_addrs: vec![alt_config.server_subnet()],
},
DhcpTestEndpoint {
name: "client-ep2",
env: DhcpTestEnv::Client(alt_config.expected_acquired()),
static_addrs: Vec::new(),
},
],
},
],
&mut [&mut default_config.dhcp_parameters(), &mut alt_config.dhcp_parameters()],
1,
false,
)
.await
}
#[derive(Copy, Clone)]
enum PersistenceMode {
Persistent,
Ephemeral,
}
impl std::fmt::Display for PersistenceMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PersistenceMode::Persistent => write!(f, "persistent"),
PersistenceMode::Ephemeral => write!(f, "ephemeral"),
}
}
}
impl PersistenceMode {
fn dhcpd_args(&self) -> Option<Vec<String>> {
match self {
Self::Persistent => Some(vec![String::from("--persistent")]),
Self::Ephemeral => None,
}
}
fn dhcpd_params_after_restart(
&self,
) -> Result<Vec<(fidl_fuchsia_net_dhcp::ParameterName, fidl_fuchsia_net_dhcp::Parameter)>> {
Ok(match self {
Self::Persistent => {
let params = test_dhcpd_params().context("failed to create test dhcpd params")?;
params.into_iter().map(|p| (param_name(&p), p)).collect()
}
Self::Ephemeral => vec![
fidl_fuchsia_net_dhcp::Parameter::IpAddrs(vec![]),
fidl_fuchsia_net_dhcp::Parameter::AddressPool(fidl_fuchsia_net_dhcp::AddressPool {
prefix_length: Some(0),
range_start: Some(fidl_fuchsia_net::Ipv4Address { addr: [0, 0, 0, 0] }),
range_stop: Some(fidl_fuchsia_net::Ipv4Address { addr: [0, 0, 0, 0] }),
..fidl_fuchsia_net_dhcp::AddressPool::EMPTY
}),
fidl_fuchsia_net_dhcp::Parameter::Lease(fidl_fuchsia_net_dhcp::LeaseLength {
default: Some(86400),
max: Some(86400),
..fidl_fuchsia_net_dhcp::LeaseLength::EMPTY
}),
fidl_fuchsia_net_dhcp::Parameter::PermittedMacs(vec![]),
fidl_fuchsia_net_dhcp::Parameter::StaticallyAssignedAddrs(vec![]),
fidl_fuchsia_net_dhcp::Parameter::ArpProbe(false),
fidl_fuchsia_net_dhcp::Parameter::BoundDeviceNames(vec![]),
]
.into_iter()
.map(|p| (param_name(&p), p))
.collect(),
})
}
}
// This collection of parameters is defined as a function because we need to allocate a Vec which
// cannot be done statically, i.e. as a constant.
fn test_dhcpd_params() -> Result<Vec<fidl_fuchsia_net_dhcp::Parameter>> {
let config = default_test_config().context("failed to create test config")?;
Ok(vec![
fidl_fuchsia_net_dhcp::Parameter::IpAddrs(vec![config.server_addr]),
fidl_fuchsia_net_dhcp::Parameter::AddressPool(config.managed_addrs.into_fidl()),
fidl_fuchsia_net_dhcp::Parameter::Lease(fidl_fuchsia_net_dhcp::LeaseLength {
default: Some(60),
max: Some(60),
..fidl_fuchsia_net_dhcp::LeaseLength::EMPTY
}),
fidl_fuchsia_net_dhcp::Parameter::PermittedMacs(vec![fidl_mac!("aa:bb:cc:dd:ee:ff")]),
fidl_fuchsia_net_dhcp::Parameter::StaticallyAssignedAddrs(vec![
fidl_fuchsia_net_dhcp::StaticAssignment {
host: Some(fidl_mac!("aa:bb:cc:dd:ee:ff")),
assigned_addr: Some(fidl_ip_v4!("192.168.0.2")),
..fidl_fuchsia_net_dhcp::StaticAssignment::EMPTY
},
]),
fidl_fuchsia_net_dhcp::Parameter::ArpProbe(true),
fidl_fuchsia_net_dhcp::Parameter::BoundDeviceNames(vec!["eth2".to_string()]),
])
}
fn param_name(param: &fidl_fuchsia_net_dhcp::Parameter) -> fidl_fuchsia_net_dhcp::ParameterName {
match param {
fidl_fuchsia_net_dhcp::Parameter::IpAddrs(_) => {
fidl_fuchsia_net_dhcp::ParameterName::IpAddrs
}
fidl_fuchsia_net_dhcp::Parameter::AddressPool(_) => {
fidl_fuchsia_net_dhcp::ParameterName::AddressPool
}
fidl_fuchsia_net_dhcp::Parameter::Lease(_) => {
fidl_fuchsia_net_dhcp::ParameterName::LeaseLength
}
fidl_fuchsia_net_dhcp::Parameter::PermittedMacs(_) => {
fidl_fuchsia_net_dhcp::ParameterName::PermittedMacs
}
fidl_fuchsia_net_dhcp::Parameter::StaticallyAssignedAddrs(_) => {
fidl_fuchsia_net_dhcp::ParameterName::StaticallyAssignedAddrs
}
fidl_fuchsia_net_dhcp::Parameter::ArpProbe(_) => {
fidl_fuchsia_net_dhcp::ParameterName::ArpProbe
}
fidl_fuchsia_net_dhcp::Parameter::BoundDeviceNames(_) => {
fidl_fuchsia_net_dhcp::ParameterName::BoundDeviceNames
}
fidl_fuchsia_net_dhcp::ParameterUnknown!() => {
panic!("attempted to retrieve name of Parameter::Unknown");
}
}
}
// This test guards against regression for the issue found in https://fxbug.dev/62989. The test
// attempts to create an inconsistent state on the dhcp server by allowing the server to complete a
// transaction with a client, thereby creating a record of a lease. The server is then restarted;
// if the linked issue has not been fixed, then the server will inadvertently erase its
// configuration parameters from persistent storage, which will lead to an inconsistent server
// state on the next restart. Finally, the server is restarted one more time, and then its
// clear_leases() function is triggered, which will cause a panic if the server is in an
// inconsistent state.
#[variants_test]
async fn acquire_persistent_dhcp_server_after_restart<E: netemul::Endpoint>(name: &str) -> Result {
let mode = PersistenceMode::Persistent;
Ok(acquire_dhcp_server_after_restart::<E>(&format!("{}_{}", name, mode), mode).await?)
}
// An ephemeral dhcp server cannot become inconsistent with its persistent state because it has
// none. However, without persistent state, an ephemeral dhcp server cannot run without explicit
// configuration. This test verifies that an ephemeral dhcp server will return an error if run
// after restarting.
#[variants_test]
async fn acquire_ephemeral_dhcp_server_after_restart<E: netemul::Endpoint>(name: &str) -> Result {
let mode = PersistenceMode::Ephemeral;
Ok(acquire_dhcp_server_after_restart::<E>(&format!("{}_{}", name, mode), mode).await?)
}
fn setup_component_proxy(
mode: PersistenceMode,
server_env: &netemul::TestEnvironment<'_>,
) -> Result<(fuchsia_component::client::App, fidl_fuchsia_net_dhcp::Server_Proxy)> {
let dhcpd = fuchsia_component::client::launch(
&server_env.get_launcher().context("failed to create launcher")?,
KnownServices::DhcpServer.get_url().to_string(),
mode.dhcpd_args(),
)
.context("failed to start dhcpd")?;
let dhcp_server = dhcpd
.connect_to_service::<fidl_fuchsia_net_dhcp::Server_Marker>()
.context("failed to connect to DHCP server")?;
Ok((dhcpd, dhcp_server))
}
async fn cleanup_component(dhcpd: &mut fuchsia_component::client::App) -> Result {
let () = dhcpd.kill().context("failed to kill dhcpd component")?;
assert_eq!(
dhcpd.wait().await.context("failed to await dhcpd component exit")?.code(),
fuchsia_zircon::sys::ZX_TASK_RETCODE_SYSCALL_KILL
);
Ok(())
}
async fn acquire_dhcp_server_after_restart<E: netemul::Endpoint>(
name: &str,
mode: PersistenceMode,
) -> Result {
let sandbox = netemul::TestSandbox::new().context("failed to create sandbox")?;
let server_env = sandbox
.create_netstack_environment_with::<Netstack2, _, _>(
format!("{}_server", name),
&[KnownServices::SecureStash],
)
.context("failed to create server environment")?;
let client_env = sandbox
.create_netstack_environment::<Netstack2, _>(format!("{}_client", name))
.context("failed to create client environment")?;
let network = sandbox.create_network(name).await.context("failed to create network")?;
let config = default_test_config().context("failed to create test config")?;
let _server_ep = server_env
.join_network::<E, _>(
&network,
"server-ep",
&netemul::InterfaceConfig::StaticIp(config.server_subnet()),
)
.await
.context("failed to create server network endpoint")?;
let client_ep = client_env
.join_network::<E, _>(&network, "client-ep", &netemul::InterfaceConfig::None)
.await
.context("failed to create client network endpoint")?;
// Complete initial DHCP transaction in order to store a lease record in the server's
// persistent storage.
{
let (mut dhcpd, dhcp_server) = setup_component_proxy(mode, &server_env)?;
let () = set_server_parameters(&dhcp_server, &mut config.dhcp_parameters()).await?;
let () = dhcp_server
.start_serving()
.await
.context("failed to call dhcp/Server.StartServing")?
.map_err(fuchsia_zircon::Status::from_raw)
.context("dhcp/Server.StartServing returned error")?;
let () =
client_acquires_addr(&client_env, &[client_ep], config.expected_acquired(), 1, false)
.await
.context("client failed to acquire address")?;
let () =
dhcp_server.stop_serving().await.context("failed to call dhcp/Server.StopServing")?;
let () = cleanup_component(&mut dhcpd).await?;
}
// Restart the server in an attempt to force the server's persistent storage into an
// inconsistent state whereby the addresses leased to clients do not agree with the contents of
// the server's address pool. If the server is in ephemeral mode, it will fail at the call to
// start_serving() since it will not have retained its parameters.
{
let (mut dhcpd, dhcp_server) = setup_component_proxy(mode, &server_env)?;
let () = match mode {
PersistenceMode::Persistent => {
let () = dhcp_server
.start_serving()
.await
.context("failed to call dhcp/Server.StartServing")?
.map_err(fuchsia_zircon::Status::from_raw)
.context("dhcp/Server.StartServing returned error")?;
dhcp_server
.stop_serving()
.await
.context("failed to call dhcp/Server.StopServing")?
}
PersistenceMode::Ephemeral => {
matches::assert_matches!(
dhcp_server
.start_serving()
.await
.context("failed to call dhcp/Server.StartServing")?
.map_err(fuchsia_zircon::Status::from_raw),
Err(fuchsia_zircon::Status::INVALID_ARGS)
);
}
};
let () = cleanup_component(&mut dhcpd).await?;
}
// Restart the server again in order to load the inconsistent state into the server's runtime
// representation. Call clear_leases() to trigger a panic resulting from inconsistent state,
// provided that the issue motivating this test is unfixed/regressed. If the server is in
// ephemeral mode, it will fail at the call to start_serving() since it will not have retained
// its parameters.
{
let (mut dhcpd, dhcp_server) = setup_component_proxy(mode, &server_env)?;
let () = match mode {
PersistenceMode::Persistent => {
let () = dhcp_server
.start_serving()
.await
.context("failed to call dhcp/Server.StartServing")?
.map_err(fuchsia_zircon::Status::from_raw)
.context("dhcp/Server.StartServing returned error")?;
let () = dhcp_server
.stop_serving()
.await
.context("failed to call dhcp/Server.StopServing")?;
dhcp_server
.clear_leases()
.await
.context("failed to call dhcp/Server.ClearLeases")?
.map_err(fuchsia_zircon::Status::from_raw)
.context("dhcp/Server.ClearLeases returned error")?;
}
PersistenceMode::Ephemeral => {
matches::assert_matches!(
dhcp_server
.start_serving()
.await
.context("failed to call dhcp/Server.StartServing")?
.map_err(fuchsia_zircon::Status::from_raw),
Err(fuchsia_zircon::Status::INVALID_ARGS)
);
}
};
let () = cleanup_component(&mut dhcpd).await?;
}
Ok(())
}
#[variants_test]
async fn test_dhcp_server_persistence_mode_persistent<E: netemul::Endpoint>(name: &str) -> Result {
let mode = PersistenceMode::Persistent;
Ok(test_dhcp_server_persistence_mode::<E>(&format!("{}_{}", name, mode), mode).await?)
}
#[variants_test]
async fn test_dhcp_server_persistence_mode_ephemeral<E: netemul::Endpoint>(name: &str) -> Result {
let mode = PersistenceMode::Ephemeral;
Ok(test_dhcp_server_persistence_mode::<E>(&format!("{}_{}", name, mode), mode).await?)
}
async fn test_dhcp_server_persistence_mode<E: netemul::Endpoint>(
name: &str,
mode: PersistenceMode,
) -> Result {
let sandbox = netemul::TestSandbox::new().context("failed to create sandbox")?;
let server_env = sandbox
.create_netstack_environment_with::<Netstack2, _, _>(
format!("{}_server", name),
&[KnownServices::SecureStash],
)
.context("failed to create server environment")?;
let network = sandbox.create_network(name).await.context("failed to create network")?;
let config = default_test_config().context("failed to create test config")?;
let _server_ep = server_env
.join_network::<E, _>(
&network,
"server-ep",
&netemul::InterfaceConfig::StaticIp(config.server_subnet()),
)
.await
.context("failed to create server network endpoint")?;
// Configure the server with parameters and then restart it.
{
let mut params = test_dhcpd_params().context("failed to create test dhcpd params")?;
let (mut dhcpd, dhcp_server) = setup_component_proxy(mode, &server_env)?;
let () = set_server_parameters(&dhcp_server, &mut params).await?;
let () = cleanup_component(&mut dhcpd).await?;
}
// Assert that configured parameters after the restart correspond to the persistence mode of the
// server.
{
let (mut dhcpd, dhcp_server) = setup_component_proxy(mode, &server_env)?;
let dhcp_server = &dhcp_server;
let mut params = mode
.dhcpd_params_after_restart()
.context("failed to create dhcpd params after restart.")?;
let () = stream::iter(params.iter_mut())
.map(Ok)
.try_for_each_concurrent(None, |(name, parameter)| async move {
Result::Ok(assert_eq!(
dhcp_server
.get_parameter(*name)
.await
.with_context(|| {
format!("failed to call dhcp/Server.GetParameter({:?})", name)
})?
.map_err(fuchsia_zircon::Status::from_raw)
.with_context(|| {
format!("dhcp/Server.GetParameter({:?}) returned error", name)
})
.unwrap(),
*parameter
))
})
.await
.context("failed to get server parameters")?;
let () = cleanup_component(&mut dhcpd).await?;
}
Ok(())
}