blob: 34ac51db0fade47188e73531f134ce6d1926b01b [file] [log] [blame] [edit]
// Copyright 2022 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.
// TODO(https://fxbug.dev/42156465): Replace with GN config once available in an ffx_plugin.
#![deny(unused_results)]
use anyhow::Context as _;
use fho::{AvailabilityFlag, FfxMain, FfxTool, SimpleWriter};
use tracing::error;
use {
ffx_net_test_realm_args as ntr_args, fidl_fuchsia_developer_remotecontrol as fremotecontrol,
fidl_fuchsia_io as fio, fidl_fuchsia_net as fnet, fidl_fuchsia_net_dhcpv6 as fnet_dhcpv6,
fidl_fuchsia_net_dhcpv6_ext as fnet_dhcpv6_ext, fidl_fuchsia_net_ext as fnet_ext,
fidl_fuchsia_net_test_realm as fntr, fidl_fuchsia_sys2 as fsys,
};
async fn connect_to_protocol<S: fidl::endpoints::DiscoverableProtocolMarker>(
remote_control: &fremotecontrol::RemoteControlProxy,
moniker: &str,
) -> anyhow::Result<S::Proxy> {
let (proxy, server_end) = fidl::endpoints::create_proxy::<S>()
.with_context(|| format!("failed to create proxy to {}", S::PROTOCOL_NAME))?;
remote_control
.open_capability(
moniker,
fsys::OpenDirType::ExposedDir,
S::PROTOCOL_NAME,
server_end.into_channel(),
fio::OpenFlags::empty(),
)
.await?
.map_err(|e| {
anyhow::anyhow!(
"failed to connect to {} at {}: {:?}. Did you forget to run ffx component start?",
S::PROTOCOL_NAME,
moniker,
e
)
})?;
Ok(proxy)
}
#[derive(FfxTool)]
#[check(AvailabilityFlag("net.test.realm"))]
pub struct NetTestRealmTool {
remote_control: fremotecontrol::RemoteControlProxy,
#[command]
cmd: ntr_args::Command,
}
fho::embedded_plugin!(NetTestRealmTool);
#[async_trait::async_trait(?Send)]
impl FfxMain for NetTestRealmTool {
type Writer = SimpleWriter;
async fn main(self, _writer: Self::Writer) -> fho::Result<()> {
net_test_realm(self.remote_control, self.cmd).await.map_err(Into::into)
}
}
async fn net_test_realm(
remote_control: fremotecontrol::RemoteControlProxy,
mut cmd: ntr_args::Command,
) -> anyhow::Result<()> {
// The tool was called with a selector, make the arg an moniker.
// TODO: remove once all clients of this tool are passing monikers.
if !cmd.component_moniker.starts_with("/") {
let moniker = cmd.component_moniker.replace("\\:", ":");
cmd.component_moniker = format!("/{moniker}");
}
let controller =
connect_to_protocol::<fntr::ControllerMarker>(&remote_control, &cmd.component_moniker)
.await?;
handle_command(controller, cmd.subcommand).await
}
async fn handle_command(
controller: fntr::ControllerProxy,
command: ntr_args::Subcommand,
) -> anyhow::Result<()> {
let (result, method_name) = match command {
ntr_args::Subcommand::AddInterface(ntr_args::AddInterface {
mac_address,
name,
wait_any_ip_address,
}) => (
controller.add_interface(&mac_address.into(), &name, wait_any_ip_address).await,
"add_interface",
),
ntr_args::Subcommand::JoinMulticastGroup(ntr_args::JoinMulticastGroup {
address,
interface_id,
}) => (
{
let now = std::time::Instant::now();
println!(
"starting join_multicast_group op at {now:?}: {address:?} {interface_id:?}"
);
let result = controller.join_multicast_group(&address.into(), interface_id).await;
let elapsed = std::time::Instant::now() - now;
println!(
"finishing join_multicast_group op from {now:?}: \
{address:?} {interface_id:?}: {result:?} (after {elapsed:?})"
);
result
},
"join_multicast_group",
),
ntr_args::Subcommand::LeaveMulticastGroup(ntr_args::LeaveMulticastGroup {
address,
interface_id,
}) => (
{
let now = std::time::Instant::now();
println!(
"starting leave_multicast_group op at {now:?}: {address:?} {interface_id:?}"
);
let result = controller.leave_multicast_group(&address.into(), interface_id).await;
let elapsed = std::time::Instant::now() - now;
println!(
"finishing leave_multicast_group op from {now:?}: \
{address:?} {interface_id:?}: {result:?} (after {elapsed:?})"
);
result
},
"leave_multicast_group",
),
ntr_args::Subcommand::Ping(ntr_args::Ping {
target,
payload_length,
timeout,
interface_name,
}) => (
controller
.ping(&target.into(), payload_length, interface_name.as_deref(), timeout)
.await,
"ping",
),
ntr_args::Subcommand::PollUdp(ntr_args::PollUdp {
target,
payload,
timeout,
num_retries,
}) => (
async move {
controller
.poll_udp(
&fnet_ext::SocketAddress(target).into(),
payload.as_bytes(),
timeout,
num_retries,
)
.await
.map(|ntr_result| {
ntr_result.and_then(|bytes| {
let received = std::str::from_utf8(&bytes).map_err(|e| {
error!("error parsing {:?} as utf8: {:?}", bytes, e);
fntr::Error::Internal
})?;
println!("{}", received);
Ok(())
})
})
}
.await,
"poll_udp",
),
ntr_args::Subcommand::StartHermeticNetworkRealm(ntr_args::StartHermeticNetworkRealm {
netstack,
}) => (
controller.start_hermetic_network_realm(netstack).await,
"start_hermetic_network_realm",
),
ntr_args::Subcommand::StartStub(ntr_args::StartStub { component_url }) => {
(controller.start_stub(&component_url).await, "start_stub")
}
ntr_args::Subcommand::StopHermeticNetworkRealm(ntr_args::StopHermeticNetworkRealm {}) => {
(controller.stop_hermetic_network_realm().await, "stop_hermetic_network_realm")
}
ntr_args::Subcommand::StopStub(ntr_args::StopStub {}) => {
(controller.stop_stub().await, "stop_stub")
}
ntr_args::Subcommand::Dhcpv6Client(ntr_args::Dhcpv6Client { subcommand }) => {
match subcommand {
ntr_args::Dhcpv6ClientSubcommand::Start(ntr_args::Dhcpv6ClientStart {
interface_id,
address,
request_non_temporary_address,
request_dns_servers,
prefix_delegation_config,
}) => (
controller
.start_dhcpv6_client(
&fnet_dhcpv6_ext::NewClientParams {
interface_id: interface_id,
address: fnet::Ipv6SocketAddress {
address: address.into(),
port: fnet_dhcpv6::DEFAULT_CLIENT_PORT,
zone_index: interface_id,
},
config: fnet_dhcpv6_ext::ClientConfig {
information_config: fnet_dhcpv6_ext::InformationConfig {
dns_servers: request_dns_servers,
},
non_temporary_address_config: fnet_dhcpv6_ext::AddressConfig {
address_count: if request_non_temporary_address {
1
} else {
0
},
preferred_addresses: None,
},
prefix_delegation_config: prefix_delegation_config.map(
|pd_config| match pd_config {
None => fnet_dhcpv6::PrefixDelegationConfig::Empty(
fnet_dhcpv6::Empty,
),
Some(fnet_ext::SubnetV6 {
addr: fnet_ext::Ipv6Address(addr),
prefix_len,
}) if addr.is_unspecified() => {
fnet_dhcpv6::PrefixDelegationConfig::PrefixLength(
prefix_len,
)
}
Some(subnet) => {
fnet_dhcpv6::PrefixDelegationConfig::Prefix(
subnet.into(),
)
}
},
),
},
duid: (request_non_temporary_address
|| prefix_delegation_config.is_some())
.then(|| {
fnet_dhcpv6::Duid::Uuid(uuid::Uuid::new_v4().into_bytes())
}),
}
.into(),
)
.await,
"start_dhcpv6_client",
),
ntr_args::Dhcpv6ClientSubcommand::Stop(ntr_args::Dhcpv6ClientStop {}) => {
(controller.stop_dhcpv6_client().await, "stop_dhcpv6_client")
}
}
}
ntr_args::Subcommand::DhcpClient(ntr_args::DhcpClient { subcommand }) => match subcommand {
ntr_args::DhcpClientSubcommand::Start(ntr_args::DhcpClientStart { interface_id }) => (
controller
.start_out_of_stack_dhcpv4_client(
&fntr::ControllerStartOutOfStackDhcpv4ClientRequest {
interface_id: Some(interface_id),
..Default::default()
},
)
.await,
"start_out_of_stack_dhcpv4_client",
),
ntr_args::DhcpClientSubcommand::Stop(ntr_args::DhcpClientStop { interface_id }) => (
controller
.stop_out_of_stack_dhcpv4_client(
&fntr::ControllerStopOutOfStackDhcpv4ClientRequest {
interface_id: Some(interface_id),
..Default::default()
},
)
.await,
"stop_out_of_stack_dhcpv4_client",
),
},
};
result
.context(format!("{} failed", method_name))?
.map_err(|e| anyhow::format_err!("{} error: {:?}", method_name, e))
}
#[cfg(test)]
mod test {
use super::*;
use assert_matches::assert_matches;
use fidl_fuchsia_net as fnet;
use futures::TryStreamExt as _;
use net_declare::{fidl_ip, fidl_ip_v6_with_prefix, fidl_mac, std_ip_v6};
const COMPONENT_URL: &'static str =
"fuchsia-pkg://fuchsia.com/fake-component#meta/fake-component.cm";
const INTERFACE_ID: u64 = 1;
const INTERFACE_NAME: &'static str = "eth1";
const IP_ADDRESS: fnet::IpAddress = fidl_ip!("192.168.0.1");
const MAC_ADDRESS: fnet::MacAddress = fidl_mac!("02:03:04:05:06:07");
/// Executes the `command` and routes the emitted request to the provided
/// `response_handler`.
///
/// The `response_handler` should validate the request and output the
/// desired response.
async fn net_test_realm_command_test<F: FnOnce(fntr::ControllerRequest)>(
command: ntr_args::Subcommand,
response_handler: F,
) {
let (controller, mut requests) =
fidl::endpoints::create_proxy_and_stream::<fntr::ControllerMarker>()
.expect("create_proxy_and_stream failed");
let op = handle_command(controller, command);
let op_response = async {
let request = requests
.try_next()
.await
.expect("controller request stream error")
.expect("controller request not found");
response_handler(request);
Ok(())
};
futures::future::try_join(op, op_response).await.expect("net_test_realm command failed");
}
#[fuchsia_async::run_singlethreaded(test)]
async fn add_interface() {
net_test_realm_command_test(
ntr_args::Subcommand::AddInterface(ntr_args::AddInterface {
mac_address: MAC_ADDRESS.into(),
name: INTERFACE_NAME.to_string(),
wait_any_ip_address: false,
}),
|request| {
let (mac_address, name, wait_ip_address, responder) =
request.into_add_interface().expect("expected request of type AddInterface");
assert_eq!(MAC_ADDRESS, mac_address);
assert_eq!(INTERFACE_NAME.to_string(), name);
assert_eq!(wait_ip_address, false);
responder.send(Ok(())).expect("failed to send AddInterface response");
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn join_multicast_group() {
net_test_realm_command_test(
ntr_args::Subcommand::JoinMulticastGroup(ntr_args::JoinMulticastGroup {
address: IP_ADDRESS.into(),
interface_id: INTERFACE_ID,
}),
|request| {
let (ip_address, interface_id, responder) = request
.into_join_multicast_group()
.expect("expected request of type JoinMulticastGroup");
assert_eq!(IP_ADDRESS, ip_address);
assert_eq!(INTERFACE_ID, interface_id);
responder.send(Ok(())).expect("failed to send JoinMulticastGroup response");
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn leave_multicast_group() {
net_test_realm_command_test(
ntr_args::Subcommand::LeaveMulticastGroup(ntr_args::LeaveMulticastGroup {
address: IP_ADDRESS.into(),
interface_id: INTERFACE_ID,
}),
|request| {
let (ip_address, interface_id, responder) = request
.into_leave_multicast_group()
.expect("expected request of type LeaveMulticastGroup");
assert_eq!(IP_ADDRESS, ip_address);
assert_eq!(INTERFACE_ID, interface_id);
responder.send(Ok(())).expect("failed to send LeaveMulticastGroup response");
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn ping() {
let expected_payload_length = 100;
let expected_timeout: i64 = 1000;
net_test_realm_command_test(
ntr_args::Subcommand::Ping(ntr_args::Ping {
target: IP_ADDRESS.into(),
payload_length: expected_payload_length,
timeout: expected_timeout,
interface_name: Some(INTERFACE_NAME.to_string()),
}),
|request| {
let (target, payload_length, interface_name, timeout, responder) =
request.into_ping().expect("expected request of type Ping");
assert_eq!(IP_ADDRESS, target);
assert_eq!(expected_payload_length, payload_length);
assert_eq!(Some(INTERFACE_NAME.to_string()), interface_name);
assert_eq!(expected_timeout, timeout);
responder.send(Ok(())).expect("failed to send Ping response");
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn poll_udp() {
let expected_port = 1234;
let expected_target = (std::net::Ipv4Addr::LOCALHOST, expected_port).into();
let expected_timeout = 1000;
let expected_payload = "hello";
let expected_num_retries = 10;
net_test_realm_command_test(
ntr_args::Subcommand::PollUdp(ntr_args::PollUdp {
target: expected_target,
payload: expected_payload.to_string(),
timeout: expected_timeout,
num_retries: expected_num_retries,
}),
|request| {
let (target, payload, timeout, num_retries, responder) =
request.into_poll_udp().expect("expected request of type PollUdp");
assert_eq!(
fnet_ext::SocketAddress::from(target),
fnet_ext::SocketAddress(expected_target)
);
assert_eq!(payload, expected_payload.as_bytes());
assert_eq!(timeout, expected_timeout);
assert_eq!(num_retries, expected_num_retries);
responder.send(Ok(&payload)).expect("failed to send PollUdp response");
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn start_hermetic_network_realm() {
let expected_netstack = fntr::Netstack::V2;
net_test_realm_command_test(
ntr_args::Subcommand::StartHermeticNetworkRealm(ntr_args::StartHermeticNetworkRealm {
netstack: expected_netstack,
}),
|request| {
let (netstack, responder) = request
.into_start_hermetic_network_realm()
.expect("expected request of type StartHermeticNetworkRealm");
assert_eq!(expected_netstack, netstack);
responder.send(Ok(())).expect("failed to send StartHermeticNetworkRealm response");
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn start_stub() {
net_test_realm_command_test(
ntr_args::Subcommand::StartStub(ntr_args::StartStub {
component_url: COMPONENT_URL.to_string(),
}),
|request| {
let (component_url, responder) =
request.into_start_stub().expect("expected request of type StartStub");
assert_eq!(COMPONENT_URL.to_string(), component_url);
responder.send(Ok(())).expect("failed to send StartStub response");
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn stop_hermetic_network_realm() {
net_test_realm_command_test(
ntr_args::Subcommand::StopHermeticNetworkRealm(ntr_args::StopHermeticNetworkRealm {}),
|request| {
request
.into_stop_hermetic_network_realm()
.expect("expected request of type StopHermeticNetworkRealm")
.send(Ok(()))
.expect("failed to send StopHermeticNetworkRealm response");
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn stop_stub() {
net_test_realm_command_test(
ntr_args::Subcommand::StopStub(ntr_args::StopStub {}),
|request| {
request
.into_stop_stub()
.expect("expected request of type StopStub")
.send(Ok(()))
.expect("failed to send StopStub response");
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn dhcpv6_client_start() {
const DHCPV6_CLIENT_BIND_ADDR: fnet_ext::Ipv6Address =
fnet_ext::Ipv6Address(std_ip_v6!("fe80::1"));
const PD_HINT: fnet::Ipv6AddressWithPrefix = fidl_ip_v6_with_prefix!("2001:db8::/32");
net_test_realm_command_test(
ntr_args::Subcommand::Dhcpv6Client(ntr_args::Dhcpv6Client {
subcommand: ntr_args::Dhcpv6ClientSubcommand::Start(ntr_args::Dhcpv6ClientStart {
interface_id: INTERFACE_ID,
address: DHCPV6_CLIENT_BIND_ADDR,
request_non_temporary_address: true,
request_dns_servers: true,
prefix_delegation_config: Some(Some(PD_HINT.into())),
}),
}),
|request| {
let (params, responder) = request
.into_start_dhcpv6_client()
.expect("expected request of type StartDhcpv6Client");
let params: fnet_dhcpv6_ext::NewClientParams =
params.try_into().expect("NewClientParams should pass FIDL table validation");
assert_eq!(
params,
fnet_dhcpv6_ext::NewClientParams {
interface_id: INTERFACE_ID,
address: fnet::Ipv6SocketAddress {
address: DHCPV6_CLIENT_BIND_ADDR.into(),
port: fnet_dhcpv6::DEFAULT_CLIENT_PORT,
zone_index: INTERFACE_ID,
},
config: fnet_dhcpv6_ext::ClientConfig {
information_config: fnet_dhcpv6_ext::InformationConfig {
dns_servers: true,
},
non_temporary_address_config: fnet_dhcpv6_ext::AddressConfig {
address_count: 1,
preferred_addresses: None,
},
prefix_delegation_config: Some(
fnet_dhcpv6::PrefixDelegationConfig::Prefix(PD_HINT)
),
},
// Don't check the DUID as it's a randomly generated UUID.
duid: params.duid.clone(),
},
);
responder.send(Ok(())).expect("failed to send StartDhcpv6Client response");
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn dhcpv6_client_stop() {
net_test_realm_command_test(
ntr_args::Subcommand::Dhcpv6Client(ntr_args::Dhcpv6Client {
subcommand: ntr_args::Dhcpv6ClientSubcommand::Stop(ntr_args::Dhcpv6ClientStop {}),
}),
|request| {
request
.into_stop_dhcpv6_client()
.expect("expected request of type StopDhcpv6Client")
.send(Ok(()))
.expect("failed to send StopDhcpv6Client response");
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn dhcp_client_start() {
net_test_realm_command_test(
ntr_args::Subcommand::DhcpClient(ntr_args::DhcpClient {
subcommand: ntr_args::DhcpClientSubcommand::Start(ntr_args::DhcpClientStart {
interface_id: INTERFACE_ID,
}),
}),
|request| {
let (request, responder) = request
.into_start_out_of_stack_dhcpv4_client()
.expect("expected request of type StartOutOfStackDhcpv4Client");
assert_matches!(
request,
fntr::ControllerStartOutOfStackDhcpv4ClientRequest {
interface_id: Some(interface_id),
..
} if interface_id == INTERFACE_ID
);
responder
.send(Ok(()))
.expect("failed to send StartOutOfStackDhcpv4Client response");
},
)
.await;
}
#[fuchsia_async::run_singlethreaded(test)]
async fn dhcp_client_stop() {
net_test_realm_command_test(
ntr_args::Subcommand::DhcpClient(ntr_args::DhcpClient {
subcommand: ntr_args::DhcpClientSubcommand::Stop(ntr_args::DhcpClientStop {
interface_id: INTERFACE_ID,
}),
}),
|request| {
let (request, responder) = request
.into_stop_out_of_stack_dhcpv4_client()
.expect("expected request of type StopOutOfStackDhcpv4Client");
assert_matches!(
request,
fntr::ControllerStopOutOfStackDhcpv4ClientRequest {
interface_id: Some(interface_id),
..
} if interface_id == INTERFACE_ID
);
responder.send(Ok(())).expect("failed to send StopOutOfStackDhcpv4Client response");
},
)
.await;
}
}