blob: 25bd8ba7071ebfba4b3d0ad150ac6a496507cb1a [file] [log] [blame]
// Copyright 2021 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 std::borrow::Cow;
use std::collections::HashMap;
use fidl::endpoints::Proxy as _;
use fidl_fuchsia_hardware_network as fhardware_network;
use fidl_fuchsia_net as fnet;
use fidl_fuchsia_net_interfaces as fnet_interfaces;
use fidl_fuchsia_net_interfaces_ext::{self as fnet_interfaces_ext};
use fidl_fuchsia_net_virtualization as fnet_virtualization;
use futures::StreamExt as _;
use net_declare::fidl_subnet;
use netstack_testing_common::{
interfaces, ping,
realms::{
KnownServiceProvider, ManagementAgent, ManagerConfig, NetCfgVersion, Netstack,
TestSandboxExt as _,
},
};
use netstack_testing_macros::netstack_test;
use packet::ParseBuffer as _;
use packet_formats::ethernet::EthernetFrameLengthCheck;
use test_case::test_case;
use tracing::info;
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
enum Network {
A,
B,
}
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq)]
enum Interface {
A,
B,
}
impl Interface {
fn id(&self) -> u8 {
match self {
Self::A => 1,
Self::B => 2,
}
}
}
impl std::fmt::Display for Interface {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
Self::A => write!(f, "A"),
Self::B => write!(f, "B"),
}
}
}
enum Step {
AddUpstream,
RemoveUpstream,
EnableUpstream,
DisableUpstream,
AddNetwork(Network),
AddInterface(Network, Interface),
RemoveNetwork(Network),
RemoveInterface(Network, Interface),
}
impl Step {
// Returns whether the test step may result in the bridge being torn down
// and reconstructed.
fn may_reconstruct_bridge(&self) -> bool {
match self {
Self::AddUpstream
| Self::AddInterface(_, _)
| Self::RemoveNetwork(_)
| Self::RemoveInterface(_, _) => true,
Self::EnableUpstream
| Self::DisableUpstream
| Self::RemoveUpstream
| Self::AddNetwork(_) => false,
}
}
}
struct Guest<'a> {
interface_proxy: fnet_virtualization::InterfaceProxy,
realm: netemul::TestRealm<'a>,
guest_if: netemul::TestInterface<'a>,
_net: netemul::TestNetwork<'a>,
_ep: netemul::TestEndpoint<'a>,
}
impl<'a> std::fmt::Debug for Guest<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
let Self { realm, guest_if, _net, _ep, interface_proxy: _ } = self;
f.debug_struct("Guest")
.field("realm", realm)
.field("guest_if", guest_if)
.field("_net", _net)
.field("_ep", _ep)
.finish_non_exhaustive()
}
}
impl<'a> Guest<'a> {
async fn new<S: Into<Cow<'a, str>>, N: Netstack>(
sandbox: &'a netemul::TestSandbox,
network_proxy: &fnet_virtualization::NetworkProxy,
realm_name: S,
interface: Interface,
ipv4_addr: fnet::Subnet,
) -> Guest<'a> {
let realm = sandbox
.create_netstack_realm::<N, _>(realm_name)
.expect("failed to create guest netstack realm");
let net = sandbox
.create_network(format!("net{}", interface))
.await
.expect("failed to create network between guest and host");
let guest_if = realm
.join_network(&net, format!("guest{}", interface))
.await
.expect("failed to join network as guest");
guest_if.add_address_and_subnet_route(ipv4_addr).await.expect("configure address");
let ep = net
.create_endpoint(format!("host{}", interface))
.await
.expect("failed to create endpoint on host realm connected to guest");
ep.set_link_up(true).await.expect("failed to enable endpoint");
let (device, port_id) = ep.get_netdevice().await.expect("failed to get netdevice");
let device =
device.into_proxy().expect("fuchsia.hardware.network/Device into_proxy failed");
let (port, server_end) =
fidl::endpoints::create_endpoints::<fhardware_network::PortMarker>();
device.get_port(&port_id, server_end).expect("get_port");
let (interface_proxy, server_end) =
fidl::endpoints::create_proxy::<fnet_virtualization::InterfaceMarker>()
.expect("failed to create fuchsia.net.virtualization/Interface proxy");
network_proxy.add_port(port, server_end).expect("add_port");
Self { interface_proxy, realm, _net: net, _ep: ep, guest_if }
}
}
struct NetworkClient<'a> {
network_proxy: fnet_virtualization::NetworkProxy,
interface_map: HashMap<Interface, Guest<'a>>,
}
impl<'a> std::fmt::Debug for NetworkClient<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
let Self { interface_map, network_proxy: _ } = self;
f.debug_struct("NetworkClient")
.field("interface_map", interface_map)
.finish_non_exhaustive()
}
}
impl<'a> NetworkClient<'a> {
fn new(network_proxy: fnet_virtualization::NetworkProxy) -> Self {
Self { network_proxy, interface_map: HashMap::new() }
}
}
fn create_bridged_network(
virtualization_control: &fnet_virtualization::ControlProxy,
) -> fnet_virtualization::NetworkProxy {
let (network_proxy, server_end) =
fidl::endpoints::create_proxy::<fnet_virtualization::NetworkMarker>()
.expect("failed to create fuchsia.net.virtualization/Network proxy");
virtualization_control
.create_network(
&fnet_virtualization::Config::Bridged(fnet_virtualization::Bridged::default()),
server_end,
)
.expect("create network");
network_proxy
}
// Tests netcfg's implementation of fuchsia.net.virtualization.
//
// At the beginning of each test case, a netstack named `gateway` and a
// netstack named `host` are created, connected to each other. `host` runs
// netcfg-workstation, whose implementation of the FIDL is under test.
//
// Test steps include:
// - adding and removing an interface eligible for providing upstream
// connectivity (note that adding multiple upstream-providing interfaces
// is currently unsupported),
// - adding and removing networks, which are identified by variants of the
// `Network` enum so that they can be referred to when adding/removing
// interfaces to/from them, and
// - adding and removing interfaces, which are identified by variants of the
// `Interface` enum so that they can be removed.
//
// Each netdevice client passed to `fuchsia.net.virtualization/Network.AddDevice`
// is backed by a netemul endpoint connected to a netstack running inside a
// realm. Such realms are named `guest`s as they are effectively the guest VM
// network stacks in real usage. After each test step, ensure that all
// `guest`s and the `gateway` can communicate with each other if there is a
// candidate for upstream present.
#[netstack_test]
#[test_case(
"basic",
&[
Step::AddUpstream,
Step::AddNetwork(Network::A),
Step::AddInterface(Network::A, Interface::A),
];
"basic")]
#[test_case(
"remove_network",
&[
Step::AddUpstream,
Step::AddNetwork(Network::A),
Step::AddInterface(Network::A, Interface::A),
Step::AddNetwork(Network::B),
Step::AddInterface(Network::B, Interface::B),
Step::RemoveNetwork(Network::A),
];
"remove_network")]
#[test_case(
"remove_interface",
&[
Step::AddUpstream,
Step::AddNetwork(Network::A),
Step::AddInterface(Network::A, Interface::A),
Step::AddInterface(Network::A, Interface::B),
Step::RemoveInterface(Network::A, Interface::A),
];
"remove_interface")]
#[test_case(
"add_upstream",
&[
Step::AddNetwork(Network::A),
Step::AddInterface(Network::A, Interface::A),
Step::AddUpstream,
];
"add_upstream")]
#[test_case(
"remove_upstream",
&[
Step::AddUpstream,
Step::AddNetwork(Network::A),
Step::AddInterface(Network::A, Interface::A),
Step::RemoveUpstream,
Step::AddUpstream,
];
"remove_upstream")]
#[test_case(
"disable_upstream",
&[
Step::AddUpstream,
Step::AddNetwork(Network::A),
Step::AddInterface(Network::A, Interface::A),
Step::DisableUpstream,
Step::EnableUpstream,
];
"disable_upstream")]
async fn virtualization<N: Netstack>(name: &str, sub_name: &str, steps: &[Step]) {
diagnostics_log::initialize(diagnostics_log::PublishOptions::default()).expect("init logging");
let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox");
let gateway_realm = sandbox
.create_netstack_realm::<N, _>(format!("{}_{}_gateway", name, sub_name))
.expect("failed to create gateway netstack realm");
let net_host_gateway = sandbox
.create_network("net_host_gateway")
.await
.expect("failed to create network between host and gateway");
let gateway_if = gateway_realm
.join_network(&net_host_gateway, "ep_gateway")
.await
.expect("failed to join network in gateway realm");
gateway_if
.add_address_and_subnet_route(fidl_subnet!("192.168.255.1/16"))
.await
.expect("configure address");
let host_realm = sandbox
.create_netstack_realm_with::<N, _, _>(
format!("{}_{}_host", name, sub_name),
&[
KnownServiceProvider::Manager {
agent: ManagementAgent::NetCfg(NetCfgVersion::Advanced),
config: ManagerConfig::Empty,
use_dhcp_server: false,
use_out_of_stack_dhcp_client: false,
},
KnownServiceProvider::DnsResolver,
KnownServiceProvider::FakeClock,
],
)
.expect("failed to create host netstack realm");
let host_interfaces_state = host_realm
.connect_to_protocol::<fnet_interfaces::StateMarker>()
.expect("connect to protocol");
let virtualization_control = host_realm
.connect_to_protocol::<fnet_virtualization::ControlMarker>()
.expect("failed to connect to fuchsia.net.virtualization/Control in host realm");
let mut networks = HashMap::new();
let mut upstream_if = None;
let mut bridge = None;
for step in steps {
match *step {
Step::AddUpstream => {
info!("adding upstream");
match upstream_if.replace((
host_realm
.join_network(&net_host_gateway, "ep_host")
.await
.expect("failed to join network in host realm"),
true,
)) {
Some(upstream) => panic!(
"upstream interface already present when adding upstream: {:?}",
upstream
),
None => {}
}
}
Step::RemoveUpstream => {
info!("removing upstream");
let _: (netemul::TestInterface<'_>, bool) =
upstream_if.take().expect("upstream to be removed doesn't exist");
}
Step::DisableUpstream => {
let (interface, _): (_, bool) =
upstream_if.take().expect("upstream to disable not present");
let did_disable =
interface.control().disable().await.expect("send disable").expect("disable");
assert!(did_disable);
upstream_if = Some((interface, false));
}
Step::EnableUpstream => {
let (interface, _): (_, bool) =
upstream_if.take().expect("upstream to enable not present");
let did_enable =
interface.control().enable().await.expect("send enable").expect("enable");
assert!(did_enable);
upstream_if = Some((interface, true));
}
Step::AddNetwork(network) => {
info!("adding network {:?}", network);
match networks.entry(network) {
std::collections::hash_map::Entry::Occupied(occupied) => {
panic!("test step to add network but it already exists: {:?}", occupied);
}
std::collections::hash_map::Entry::Vacant(vacant) => {
let _: &mut NetworkClient<'_> = vacant.insert(NetworkClient::new(
create_bridged_network(&virtualization_control),
));
}
}
}
Step::AddInterface(network, interface) => {
info!("adding interface {:?} to network {:?}", interface, network);
let NetworkClient { network_proxy, interface_map } =
networks.get_mut(&network).unwrap_or_else(|| {
panic!("network {:?} to add interface to doesn't exist", network)
});
match interface_map.entry(interface) {
std::collections::hash_map::Entry::Occupied(occupied) => {
panic!(
"test step to add interface to network {:?} but it already exists: {:?}",
network, occupied
);
}
std::collections::hash_map::Entry::Vacant(vacant) => {
// Create a new netstack and a new network between it and the host.
let _: &mut Guest<'_> = vacant.insert(
Guest::new::<_, N>(
&sandbox,
&network_proxy,
format!("{}_{}_guest{}", name, sub_name, interface),
interface,
fnet::Subnet {
addr: fnet::IpAddress::Ipv4(fnet::Ipv4Address {
addr: [192, 168, interface.id(), 1],
}),
prefix_len: 16,
},
)
.await,
);
}
}
}
Step::RemoveNetwork(network) => {
info!("removing network {:?}", network);
let NetworkClient { network_proxy, interface_map } =
networks.remove(&network).unwrap_or_else(|| {
panic!("network {:?} to be removed does not exist", network)
});
std::mem::drop(network_proxy);
for (_, Guest { interface_proxy, realm: _, guest_if: _, _ep, _net }) in
interface_map.into_iter()
{
match interface_proxy.on_closed().await {
Ok(signals) => {
if !signals.contains(fidl::handle::Signals::CHANNEL_PEER_CLOSED) {
panic!("signal did not contain PEER_CLOSED: {:?}", signals);
}
}
Err(status) => {
panic!("Interface proxy on_closed error: {}", status);
}
}
}
}
Step::RemoveInterface(network, interface) => {
info!("removing interface {:?} from network {:?}", interface, network);
let NetworkClient { network_proxy: _, interface_map } =
networks.get_mut(&network).unwrap_or_else(|| {
panic!("network {:?} to remove interface from doesn't exist", network)
});
let _: Guest<'_> = interface_map.remove(&interface).unwrap_or_else(|| {
panic!("interface {} to be removed does not exist", interface)
});
}
}
if let Some::<(netemul::TestInterface<'_>, _)>((_, upstream_online)) = upstream_if {
if networks
.values()
.any(|NetworkClient { interface_map, network_proxy: _ }| !interface_map.is_empty())
{
if step.may_reconstruct_bridge() {
let mut interfaces_map = HashMap::<
u64,
fidl_fuchsia_net_interfaces_ext::PropertiesAndState<()>,
>::new();
let bridge_id = fnet_interfaces_ext::wait_interface(
fnet_interfaces_ext::event_stream_from_state(
&host_interfaces_state,
fnet_interfaces_ext::IncludedAddresses::OnlyAssigned,
)
.expect("initialize interface event stream"),
&mut interfaces_map,
|interfaces_map| {
interfaces_map.values().find_map(
|&fidl_fuchsia_net_interfaces_ext::PropertiesAndState {
properties:
fnet_interfaces_ext::Properties {
id,
device_class,
addresses: _,
name: _,
online: _,
has_default_ipv4_route: _,
has_default_ipv6_route: _,
},
state: _,
}| {
match device_class {
fnet_interfaces::DeviceClass::Device(
fhardware_network::DeviceClass::Bridge,
) if Some(id.get())
!= bridge.as_ref().map(ping::Node::id) =>
{
Some(id)
}
_ => None,
}
},
)
},
)
.await
.expect("failed to wait for bridge interface");
let v6_ll = interfaces::wait_for_v6_ll(&host_interfaces_state, bridge_id.get())
.await
.expect("failed to wait for IPv6 link-local address on bridge");
bridge = Some(ping::Node::new(
&host_realm,
bridge_id.get(),
Vec::new(),
vec![v6_ll],
));
}
let nodes = futures::stream::iter(
networks
.values()
.map(|NetworkClient { interface_map, network_proxy: _ }| {
interface_map.values()
})
.flatten(),
)
.then(|Guest { realm, guest_if, _ep, interface_proxy: _, _net }| async move {
ping::Node::new_with_v4_and_v6_link_local(&realm, &guest_if)
.await
.expect("failed to construct guest node")
})
.chain(if upstream_online {
futures::stream::once(async {
ping::Node::new_with_v4_and_v6_link_local(&gateway_realm, &gateway_if)
.await
.expect("failed to construct gateway node")
})
.left_stream()
} else {
futures::stream::empty().right_stream()
})
.collect::<Vec<_>>()
.await;
// Verify that the bridge is working
bridge
.as_ref()
.expect("bridge must be present")
.ping_pairwise(nodes.as_slice())
.await
.expect("failed to ping hosts");
}
}
}
}
#[netstack_test]
async fn dhcpv4_client_started<N: Netstack>(name: &str) {
let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox");
let host_realm = sandbox
.create_netstack_realm_with::<N, _, _>(
format!("{}_host", name),
&[
KnownServiceProvider::Manager {
agent: ManagementAgent::NetCfg(NetCfgVersion::Advanced),
config: ManagerConfig::Empty,
use_dhcp_server: false,
use_out_of_stack_dhcp_client: false,
},
KnownServiceProvider::DnsResolver,
KnownServiceProvider::FakeClock,
],
)
.expect("failed to create host netstack realm");
let net = sandbox.create_network("net").await.expect("failed to create network");
let fake_ep = net.create_fake_endpoint().expect("failed to create fake endpoint on net");
let _upstream_if = host_realm
.join_network(&net, "ep_host")
.await
.expect("failed to join network in host realm");
// Create a virtualized network and attach a device.
let virtualization_control = host_realm
.connect_to_protocol::<fnet_virtualization::ControlMarker>()
.expect("failed to connect to fuchsia.net.virtualization/Control in host realm");
let network_proxy = create_bridged_network(&virtualization_control);
let _guest = Guest::new::<_, N>(
&sandbox,
&network_proxy,
"guest",
Interface::A,
fidl_subnet!("192.168.1.1/16"),
)
.await;
// Expect a DHCPv4 packet.
fake_ep
.frame_stream()
.filter_map(|r| {
let (buf, dropped) = r.expect("fake_ep frame stream error");
assert_eq!(dropped, 0);
futures::future::ready(
match packet_formats::testutil::parse_ip_packet_in_ethernet_frame::<
net_types::ip::Ipv4,
>(&buf, EthernetFrameLengthCheck::Check)
{
Ok((mut body, _src_mac, _dst_mac, src_ip, dst_ip, _proto, _ttl)) => {
match (&mut body).parse_with::<_, packet_formats::udp::UdpPacket<_>>(
packet_formats::udp::UdpParseArgs::new(src_ip, dst_ip),
) {
Ok(udp) => {
(udp.dst_port() == dhcpv4::protocol::SERVER_PORT).then(|| ())
}
Err(packet_formats::error::ParseError::NotExpected) => None,
Err(e) => panic!("failed to parse UDP packet: {}", e),
}
}
Err(packet_formats::error::IpParseError::Parse {
error: packet_formats::error::ParseError::NotExpected,
}) => None,
Err(e) => {
panic!("failed to parse IPv4 packet: {}", e);
}
},
)
})
.next()
.await
.expect("fake endpoint frame stream ended unexpectedly");
}