| // 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(()) |
| } |