blob: d86621fa36e5cebe1527769b35b3f3ad100e0cbb [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 std::collections::HashMap;
use std::fmt::Debug;
use std::hash::Hash;
use std::pin::pin;
use anyhow::Context as _;
use assert_matches::assert_matches;
use either::Either;
use fidl::endpoints::Proxy;
use fidl_fuchsia_net_ext::IntoExt;
use fidl_fuchsia_net_routes as fnet_routes;
use fidl_fuchsia_net_routes_ext as fnet_routes_ext;
use fuchsia_async::TimeoutExt;
use fuchsia_zircon_status as zx_status;
use futures::{FutureExt, StreamExt};
use net_declare::{fidl_ip, fidl_ip_v4, fidl_mac, fidl_subnet, net_subnet_v4, net_subnet_v6};
use net_types::ip::{
GenericOverIp, Ip, IpAddress, IpInvariant, IpVersion, Ipv4, Ipv4Addr, Ipv6, Ipv6Addr,
};
use netemul::{InStack, InterfaceConfig};
use netstack_testing_common::{
interfaces,
realms::{Netstack, NetstackVersion, TestRealmExt as _, TestSandboxExt as _},
};
use netstack_testing_macros::netstack_test;
async fn resolve(
routes: &fidl_fuchsia_net_routes::StateProxy,
remote: fidl_fuchsia_net::IpAddress,
) -> fidl_fuchsia_net_routes::Resolved {
routes
.resolve(&remote)
.await
.expect("routes/State.Resolve FIDL error")
.map_err(fuchsia_zircon::Status::from_raw)
.context("routes/State.Resolve error")
.expect("failed to resolve remote")
}
#[netstack_test]
async fn resolve_loopback_route<N: Netstack>(name: &str) {
let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox");
let realm = sandbox.create_netstack_realm::<N, _>(name).expect("failed to create realm");
let routes = realm
.connect_to_protocol::<fidl_fuchsia_net_routes::StateMarker>()
.expect("failed to connect to routes/State");
let routes = &routes;
let test = |remote: fidl_fuchsia_net::IpAddress, source: fidl_fuchsia_net::IpAddress| async move {
assert_eq!(
resolve(routes, remote).await,
fidl_fuchsia_net_routes::Resolved::Direct(fidl_fuchsia_net_routes::Destination {
address: Some(remote),
mac: None,
interface_id: Some(1),
source_address: Some(source),
..Default::default()
}),
);
};
test(fidl_ip!("127.0.0.1"), fidl_ip!("127.0.0.1")).await;
test(fidl_ip!("::1"), fidl_ip!("::1")).await;
}
#[netstack_test]
async fn resolve_route<N: Netstack>(name: &str) {
const GATEWAY_IP_V4: fidl_fuchsia_net::Subnet = fidl_subnet!("192.168.0.1/24");
const GATEWAY_IP_V6: fidl_fuchsia_net::Subnet = fidl_subnet!("3080::1/64");
const GATEWAY_MAC: fidl_fuchsia_net::MacAddress = fidl_mac!("02:01:02:03:04:05");
const HOST_IP_V4: fidl_fuchsia_net::Subnet = fidl_subnet!("192.168.0.2/24");
const HOST_IP_V6: fidl_fuchsia_net::Subnet = fidl_subnet!("3080::2/64");
let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox");
let net = sandbox.create_network("net").await.expect("failed to create network");
// Configure a host.
let host = sandbox
.create_netstack_realm::<N, _>(format!("{}_host", name))
.expect("failed to create client realm");
let host_stack = host
.connect_to_protocol::<fidl_fuchsia_net_stack::StackMarker>()
.expect("failed to connect to netstack");
let host_ep = host.join_network(&net, "host").await.expect("host failed to join network");
host_ep.add_address_and_subnet_route(HOST_IP_V4).await.expect("configure address");
let _host_address_state_provider = interfaces::add_subnet_address_and_route_wait_assigned(
&host_ep,
HOST_IP_V6,
fidl_fuchsia_net_interfaces_admin::AddressParameters::default(),
)
.await
.expect("add subnet address and route");
// Configure a gateway.
let gateway = sandbox
.create_netstack_realm::<N, _>(format!("{}_gateway", name))
.expect("failed to create server realm");
let gateway_ep = gateway
.join_network_with(
&net,
"gateway",
netemul::new_endpoint_config(netemul::DEFAULT_MTU, Some(GATEWAY_MAC)),
Default::default(),
)
.await
.expect("gateway failed to join network");
gateway_ep.add_address_and_subnet_route(GATEWAY_IP_V4).await.expect("configure address");
let _gateway_address_state_provider = interfaces::add_subnet_address_and_route_wait_assigned(
&gateway_ep,
GATEWAY_IP_V6,
fidl_fuchsia_net_interfaces_admin::AddressParameters::default(),
)
.await
.expect("add subnet address and route");
let routes = host
.connect_to_protocol::<fidl_fuchsia_net_routes::StateMarker>()
.expect("failed to connect to routes/State");
let routes = &routes;
let resolve_fails = move |remote: fidl_fuchsia_net::IpAddress| async move {
assert_eq!(
routes
.resolve(&remote)
.await
.expect("resolve FIDL error")
.map_err(fuchsia_zircon::Status::from_raw),
Err(fuchsia_zircon::Status::ADDRESS_UNREACHABLE)
)
};
let interface_id = host_ep.id();
let host_stack = &host_stack;
let do_test = |gateway: fidl_fuchsia_net::IpAddress,
unreachable_peer: fidl_fuchsia_net::IpAddress,
unspecified: fidl_fuchsia_net::IpAddress,
public_ip: fidl_fuchsia_net::IpAddress,
source_address: fidl_fuchsia_net::IpAddress| async move {
let gateway_node = fidl_fuchsia_net_routes::Destination {
address: Some(gateway),
mac: Some(GATEWAY_MAC),
interface_id: Some(interface_id),
source_address: Some(source_address),
..Default::default()
};
// Start asking for a route for something that is directly accessible on the
// network.
let resolved = resolve(routes, gateway).await;
assert_eq!(resolved, fidl_fuchsia_net_routes::Resolved::Direct(gateway_node.clone()));
// Fails if MAC unreachable.
resolve_fails(unreachable_peer).await;
// Fails if route unreachable.
resolve_fails(public_ip).await;
// Install a default route and try to resolve through the gateway.
let () = host_stack
.add_forwarding_entry(&fidl_fuchsia_net_stack::ForwardingEntry {
subnet: fidl_fuchsia_net::Subnet { addr: unspecified, prefix_len: 0 },
device_id: interface_id,
next_hop: Some(Box::new(gateway)),
metric: 100,
})
.await
.expect("call add_route")
.expect("add route");
// Resolve a public IP again and check that we get the gateway response.
let resolved = resolve(routes, public_ip).await;
assert_eq!(resolved, fidl_fuchsia_net_routes::Resolved::Gateway(gateway_node.clone()));
// And that the unspecified address resolves to the gateway node as well.
let resolved = resolve(routes, unspecified).await;
assert_eq!(resolved, fidl_fuchsia_net_routes::Resolved::Gateway(gateway_node));
};
do_test(
GATEWAY_IP_V4.addr,
fidl_ip!("192.168.0.3"),
fidl_ip!("0.0.0.0"),
fidl_ip!("8.8.8.8"),
HOST_IP_V4.addr,
)
.await;
do_test(
GATEWAY_IP_V6.addr,
fidl_ip!("3080::3"),
fidl_ip!("::"),
fidl_ip!("2001:4860:4860::8888"),
HOST_IP_V6.addr,
)
.await;
}
#[netstack_test]
async fn resolve_default_route_while_dhcp_is_running<N: Netstack>(name: &str) {
let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox");
let net = sandbox.create_network("net").await.expect("failed to create network");
// Configure a host.
let realm = sandbox.create_netstack_realm::<N, _>(name).expect("failed to create client realm");
let stack = realm
.connect_to_protocol::<fidl_fuchsia_net_stack::StackMarker>()
.expect("failed to connect to netstack");
let ep = realm.join_network(&net, "host").await.expect("host failed to join network");
ep.start_dhcp::<InStack>().await.expect("failed to start DHCP");
let routes = realm
.connect_to_protocol::<fidl_fuchsia_net_routes::StateMarker>()
.expect("failed to connect to routes/State");
let resolved = routes
.resolve(&fidl_ip!("0.0.0.0"))
.await
.expect("routes/State.Resolve FIDL error")
.map_err(fuchsia_zircon::Status::from_raw);
assert_eq!(resolved, Err(fuchsia_zircon::Status::ADDRESS_UNREACHABLE));
const EP_ADDR: fidl_fuchsia_net::Ipv4Address = fidl_ip_v4!("192.168.0.3");
const PREFIX_LEN: u8 = 24;
const GATEWAY_ADDR: fidl_fuchsia_net::IpAddress = fidl_ip!("192.168.0.1");
const GATEWAY_MAC: fidl_fuchsia_net::MacAddress = fidl_mac!("02:01:02:03:04:05");
const UNSPECIFIED_IP: fidl_fuchsia_net::IpAddress = fidl_ip!("0.0.0.0");
// Configure stack statically with an address and a default route while DHCP is still running.
let _host_address_state_provider = interfaces::add_address_wait_assigned(
ep.control(),
fidl_fuchsia_net::Subnet {
addr: fidl_fuchsia_net::IpAddress::Ipv4(EP_ADDR),
prefix_len: PREFIX_LEN,
},
fidl_fuchsia_net_interfaces_admin::AddressParameters::default(),
)
.await
.expect("add address");
let neigh = realm
.connect_to_protocol::<fidl_fuchsia_net_neighbor::ControllerMarker>()
.expect("failed to connect to neighbor API");
neigh
.add_entry(ep.id(), &GATEWAY_ADDR, &GATEWAY_MAC)
.await
.expect("add_entry FIDL error")
.map_err(fuchsia_zircon::Status::from_raw)
.expect("add_entry error");
// Install a default route and try to resolve through the gateway.
let () = stack
.add_forwarding_entry(&fidl_fuchsia_net_stack::ForwardingEntry {
subnet: fidl_fuchsia_net::Subnet { addr: UNSPECIFIED_IP, prefix_len: 0 },
device_id: ep.id(),
next_hop: Some(Box::new(GATEWAY_ADDR)),
metric: 100,
})
.await
.expect("call add_route")
.expect("add route");
let resolved = routes
.resolve(&UNSPECIFIED_IP)
.await
.expect("routes/State.Resolve FIDL error")
.map_err(fuchsia_zircon::Status::from_raw);
assert_eq!(
resolved,
Ok(fidl_fuchsia_net_routes::Resolved::Gateway(fidl_fuchsia_net_routes::Destination {
address: Some(GATEWAY_ADDR),
mac: Some(GATEWAY_MAC),
interface_id: Some(ep.id()),
source_address: Some(fidl_fuchsia_net::IpAddress::Ipv4(EP_ADDR)),
..Default::default()
}))
);
}
#[netstack_test]
// Resolve returns the preferred source address used when communicating with the
// destination. Expect Resolve to fail on interfaces without any assigned
// addresses, even if the destination is reachable and routable.
async fn resolve_fails_with_no_src_address<N: Netstack, I: net_types::ip::Ip>(name: &str) {
let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox");
let realm = sandbox.create_netstack_realm::<N, _>(name).expect("failed to create realm");
let device = sandbox.create_endpoint(name).await.expect("create endpoint");
let interface = realm
.install_endpoint(device, InterfaceConfig::default())
.await
.expect("install interface");
let (local, remote, subnet) = match I::VERSION {
IpVersion::V4 => {
(fidl_ip!("192.0.2.1"), fidl_ip!("192.0.2.2"), fidl_subnet!("192.0.2.0/24"))
}
IpVersion::V6 => {
(fidl_ip!("2001:0db8::1"), fidl_ip!("2001:0db8::2"), fidl_subnet!("2001:0db8::/32"))
}
};
const REMOTE_MAC: fidl_fuchsia_net::MacAddress = fidl_mac!("02:01:02:03:04:05");
// Install a route to the remote.
let stack = realm
.connect_to_protocol::<fidl_fuchsia_net_stack::StackMarker>()
.expect("failed to connect to netstack");
let () = stack
.add_forwarding_entry(&fidl_fuchsia_net_stack::ForwardingEntry {
subnet,
device_id: interface.id(),
next_hop: None,
metric: 100,
})
.await
.expect("call add_route")
.expect("add route");
// Configure the remote as a neighbor.
let neigh = realm
.connect_to_protocol::<fidl_fuchsia_net_neighbor::ControllerMarker>()
.expect("failed to connect to neighbor API");
neigh
.add_entry(interface.id(), &remote, &REMOTE_MAC)
.await
.expect("add_entry FIDL error")
.map_err(fuchsia_zircon::Status::from_raw)
.expect("add_entry error");
let routes = realm
.connect_to_protocol::<fidl_fuchsia_net_routes::StateMarker>()
.expect("failed to connect to routes/State");
// Remove the autogenerated SLAAC addresses, so that the interface truly has
// no address.
assert_eq!(
interface
.remove_ipv6_linklocal_addresses()
.await
.expect("removing IPv6 linklocal addresses should succeed")
.len(),
1
);
// Verify that resolving the route fails.
assert_eq!(
routes
.resolve(&remote)
.await
.expect("resolve FIDL error")
.map_err(fuchsia_zircon::Status::from_raw),
Err(fuchsia_zircon::Status::ADDRESS_UNREACHABLE)
);
// Install an address on the device.
interface
.add_address(fidl_fuchsia_net::Subnet { addr: local, prefix_len: I::Addr::BYTES * 8 })
.await
.expect("failed to add address");
// Verify that resolving the route succeeds.
assert_eq!(
routes
.resolve(&remote)
.await
.expect("resolve FIDL error")
.map_err(fuchsia_zircon::Status::from_raw)
.expect("resolve failed"),
fidl_fuchsia_net_routes::Resolved::Direct(fidl_fuchsia_net_routes::Destination {
address: Some(remote),
mac: Some(REMOTE_MAC),
interface_id: Some(interface.id()),
source_address: Some(local),
..Default::default()
})
);
}
fn new_installed_route<I: fnet_routes_ext::FidlRouteIpExt>(
subnet: net_types::ip::Subnet<I::Addr>,
interface: u64,
metric: u32,
metric_is_inherited: bool,
) -> fnet_routes_ext::InstalledRoute<I> {
let specified_metric = if metric_is_inherited {
fnet_routes::SpecifiedMetric::InheritedFromInterface(fnet_routes::Empty)
} else {
fnet_routes::SpecifiedMetric::ExplicitMetric(metric)
};
fnet_routes_ext::InstalledRoute {
route: fnet_routes_ext::Route {
destination: subnet,
action: fnet_routes_ext::RouteAction::Forward(fnet_routes_ext::RouteTarget {
outbound_interface: interface,
next_hop: None,
}),
properties: fnet_routes_ext::RouteProperties {
specified_properties: fnet_routes_ext::SpecifiedRouteProperties {
metric: specified_metric,
},
},
},
effective_properties: fnet_routes_ext::EffectiveRouteProperties { metric: metric },
}
}
// Asserts that two vectors contain the same entries, order independent.
fn assert_eq_unordered<T: Debug + Eq + Hash + PartialEq>(a: Vec<T>, b: Vec<T>) {
// Converts a `Vec<T>` into a `HashMap` where the key is `T` and the value
// is the count of occurrences of `T` in the vec.
fn into_counted_set<T: Eq + Hash>(set: Vec<T>) -> HashMap<T, usize> {
set.into_iter().fold(HashMap::new(), |mut map, entry| {
*map.entry(entry).or_default() += 1;
map
})
}
assert_eq!(into_counted_set(a), into_counted_set(b));
}
// Default metric values used by the netstack when creating implicit routes.
// See `src/connectivity/network/netstack/netstack.go`.
const DEFAULT_INTERFACE_METRIC: u32 = 100;
const DEFAULT_LOW_PRIORITY_METRIC: u32 = 99999;
// The initial IPv4 routes that are installed on the loopback interface.
fn initial_loopback_routes_v4<N: Netstack>(
loopback_id: u64,
) -> impl Iterator<Item = fnet_routes_ext::InstalledRoute<Ipv4>> {
[
new_installed_route(
net_subnet_v4!("127.0.0.0/8"),
loopback_id,
DEFAULT_INTERFACE_METRIC,
true,
),
new_installed_route(
net_subnet_v4!("255.255.255.255/32"),
loopback_id,
DEFAULT_LOW_PRIORITY_METRIC,
false,
),
]
.into_iter()
// TODO(https://fxbug.dev/42074061) Unify the loopback routes between
// Netstack2 and Netstack3
.chain(match N::VERSION {
NetstackVersion::Netstack3 | NetstackVersion::ProdNetstack3 => {
Either::Left(std::iter::once(new_installed_route(
net_subnet_v4!("224.0.0.0/4"),
loopback_id,
DEFAULT_INTERFACE_METRIC,
true,
)))
}
NetstackVersion::Netstack2 { tracing: _, fast_udp: _ } | NetstackVersion::ProdNetstack2 => {
Either::Right(std::iter::empty())
}
})
}
// The initial IPv6 routes that are installed on the loopback interface.
fn initial_loopback_routes_v6<N: Netstack>(
loopback_id: u64,
) -> impl Iterator<Item = fnet_routes_ext::InstalledRoute<Ipv6>> {
[new_installed_route(net_subnet_v6!("::1/128"), loopback_id, DEFAULT_INTERFACE_METRIC, true)]
.into_iter()
// TODO(https://fxbug.dev/42074061) Unify the loopback routes between
// Netstack2 and Netstack3
.chain(match N::VERSION {
NetstackVersion::Netstack3 | NetstackVersion::ProdNetstack3 => {
Either::Left(std::iter::once(new_installed_route(
net_subnet_v6!("ff00::/8"),
loopback_id,
DEFAULT_INTERFACE_METRIC,
true,
)))
}
NetstackVersion::Netstack2 { tracing: _, fast_udp: _ }
| NetstackVersion::ProdNetstack2 => Either::Right(std::iter::empty()),
})
}
// The initial IPv4 routes that are installed on an ethernet interface.
fn initial_ethernet_routes_v4(
ethernet_id: u64,
) -> impl Iterator<Item = fnet_routes_ext::InstalledRoute<Ipv4>> {
[
new_installed_route(
net_subnet_v4!("255.255.255.255/32"),
ethernet_id,
DEFAULT_LOW_PRIORITY_METRIC,
false,
),
new_installed_route(
net_subnet_v4!("224.0.0.0/4"),
ethernet_id,
DEFAULT_INTERFACE_METRIC,
true,
),
]
.into_iter()
}
// The initial IPv6 routes that are installed on the ethernet interface.
fn initial_ethernet_routes_v6(
ethernet_id: u64,
) -> impl Iterator<Item = fnet_routes_ext::InstalledRoute<Ipv6>> {
[
new_installed_route(
net_subnet_v6!("fe80::/64"),
ethernet_id,
DEFAULT_INTERFACE_METRIC,
true,
),
new_installed_route(
net_subnet_v6!("ff00::/8"),
ethernet_id,
DEFAULT_INTERFACE_METRIC,
true,
),
]
.into_iter()
}
// Verifies the startup behavior of the watcher protocols; including the
// expected preinstalled routes.
#[netstack_test]
async fn watcher_existing<N: Netstack, I: net_types::ip::Ip + fnet_routes_ext::FidlRouteIpExt>(
name: &str,
) {
let sandbox = netemul::TestSandbox::new().expect("create sandbox");
let realm = sandbox.create_netstack_realm::<N, _>(name).expect("create realm");
let loopback_id = realm
.loopback_properties()
.await
.expect("failed to get loopback properties")
.expect("loopback properties unexpectedly None")
.id;
let device = sandbox.create_endpoint(name).await.expect("create endpoint");
// TODO(https://fxbug.dev/42074358) Netstack2 only installs certain routes
// after the interface is enabled. Using `install_endpoint` installs the
// interface, enables it, and waits for it to come online.
let interface = realm
.install_endpoint(device, InterfaceConfig::default())
.await
.expect("install interface");
let interface_id = interface.id();
// The routes we expected to be installed in the netstack by default.
#[derive(GenericOverIp)]
#[generic_over_ip(I, Ip)]
struct RoutesHolder<I: fnet_routes_ext::FidlRouteIpExt>(
Vec<fnet_routes_ext::InstalledRoute<I>>,
);
let RoutesHolder(expected_routes) = I::map_ip(
IpInvariant((loopback_id, interface_id)),
|IpInvariant((loopback_id, interface_id))| {
RoutesHolder(
initial_loopback_routes_v4::<N>(loopback_id.get())
.chain(initial_ethernet_routes_v4(interface_id))
.collect::<Vec<_>>(),
)
},
|IpInvariant((loopback_id, interface_id))| {
RoutesHolder(
initial_loopback_routes_v6::<N>(loopback_id.get())
.chain(initial_ethernet_routes_v6(interface_id))
.collect::<Vec<_>>(),
)
},
);
let state_proxy =
realm.connect_to_protocol::<I::StateMarker>().expect("failed to connect to routes/State");
let event_stream = fnet_routes_ext::event_stream_from_state::<I>(&state_proxy)
.expect("failed to connect to routes watcher");
let mut event_stream = pin!(event_stream);
// Collect the routes installed in the Netstack.
let mut routes = Vec::new();
while let Some(event) = event_stream
.next()
.on_timeout(
fuchsia_async::Time::after(netstack_testing_common::ASYNC_EVENT_NEGATIVE_CHECK_TIMEOUT),
|| None,
)
.await
{
match event.expect("unexpected error in event stream") {
// Treat 'Added' and 'Existing' events the same, since we have no
// mechanism to synchronize and ensure the Netstack has finished
// initialization before connecting the routes watcher.
fnet_routes_ext::Event::Existing(route) | fnet_routes_ext::Event::Added(route) => {
routes.push(route)
}
fnet_routes_ext::Event::Idle => continue,
fnet_routes_ext::Event::Removed(route) => {
panic!("unexpectedly observed route removal: {:?}", route)
}
fnet_routes_ext::Event::Unknown => panic!("unexpectedly observed unknown event"),
}
}
// Assert that the existing routes contain exactly the expected routes.
assert_eq_unordered(routes, expected_routes);
}
// Declare subnet routes for tests to add/delete. These are known to not collide
// with routes implicitly installed by the Netstack.
const TEST_SUBNET_V4: net_types::ip::Subnet<Ipv4Addr> = net_subnet_v4!("192.168.0.0/24");
const TEST_SUBNET_V6: net_types::ip::Subnet<Ipv6Addr> = net_subnet_v6!("fd::/64");
// Verifies that a client-installed route is observed as `existing` if added
// before the watcher client connects.
#[netstack_test]
async fn watcher_add_before_watch<
N: Netstack,
I: net_types::ip::Ip + fnet_routes_ext::FidlRouteIpExt,
>(
name: &str,
) {
let sandbox = netemul::TestSandbox::new().expect("create sandbox");
let realm = sandbox.create_netstack_realm::<N, _>(name).expect("create realm");
let device = sandbox.create_endpoint(name).await.expect("create endpoint");
let interface = device.into_interface_in_realm(&realm).await.expect("add endpoint to Netstack");
let subnet: net_types::ip::Subnet<I::Addr> =
I::map_ip((), |()| TEST_SUBNET_V4, |()| TEST_SUBNET_V6);
// Add a test route.
interface.add_subnet_route(subnet.into_ext()).await.expect("failed to add route");
// Connect to the watcher protocol.
let state_proxy =
realm.connect_to_protocol::<I::StateMarker>().expect("failed to connect to routes/State");
let event_stream = fnet_routes_ext::event_stream_from_state::<I>(&state_proxy)
.expect("failed to connect to routes watcher");
// Verify that the previously added route is observed as `existing`.
let event_stream = pin!(event_stream);
let existing = fnet_routes_ext::collect_routes_until_idle::<I, Vec<_>>(event_stream)
.await
.expect("failed to collect existing routes");
let expected_route =
new_installed_route(subnet, interface.id(), DEFAULT_INTERFACE_METRIC, true);
assert!(
existing.contains(&expected_route),
"route: {:?}, existing: {:?}",
expected_route,
existing
)
}
// Verifies the watcher protocols correctly report `added` and `removed` events.
#[netstack_test]
async fn watcher_add_remove<N: Netstack, I: net_types::ip::Ip + fnet_routes_ext::FidlRouteIpExt>(
name: &str,
) {
let sandbox = netemul::TestSandbox::new().expect("create sandbox");
let realm = sandbox.create_netstack_realm::<N, _>(name).expect("create realm");
let device = sandbox.create_endpoint(name).await.expect("create endpoint");
let interface = device.into_interface_in_realm(&realm).await.expect("add endpoint to Netstack");
let subnet: net_types::ip::Subnet<I::Addr> =
I::map_ip((), |()| TEST_SUBNET_V4, |()| TEST_SUBNET_V6);
// Connect to the watcher protocol and consume all existing events.
let state_proxy =
realm.connect_to_protocol::<I::StateMarker>().expect("failed to connect to routes/State");
let event_stream = fnet_routes_ext::event_stream_from_state::<I>(&state_proxy)
.expect("failed to connect to routes watcher");
let mut event_stream = pin!(event_stream);
// Skip all `existing` events.
let _existing_routes =
fnet_routes_ext::collect_routes_until_idle::<I, Vec<_>>(event_stream.by_ref())
.await
.expect("failed to collect existing routes");
// Add a test route.
interface.add_subnet_route(subnet.into_ext()).await.expect("failed to add route");
// Verify the `Added` event is observed.
let added_route = assert_matches!(
event_stream.next().await,
Some(Ok(fnet_routes_ext::Event::<I>::Added(route))) => route
);
let expected_route =
new_installed_route(subnet, interface.id(), DEFAULT_INTERFACE_METRIC, true);
assert_eq!(added_route, expected_route);
// Remove the test route.
interface.del_subnet_route(subnet.into_ext()).await.expect("failed to remove route");
// Verify the removed event is observed.
let removed_route = assert_matches!(
event_stream.next().await,
Some(Ok(fnet_routes_ext::Event::<I>::Removed(route))) => route
);
assert_eq!(removed_route, expected_route);
}
// Verifies the watcher protocols close if the client incorrectly calls `Watch()`
// while there is already a pending `Watch()` call parked in the server.
#[netstack_test]
async fn watcher_already_pending<
N: Netstack,
I: net_types::ip::Ip + fnet_routes_ext::FidlRouteIpExt,
>(
name: &str,
) {
let sandbox = netemul::TestSandbox::new().expect("create sandbox");
let realm = sandbox.create_netstack_realm::<N, _>(name).expect("create realm");
let state_proxy =
realm.connect_to_protocol::<I::StateMarker>().expect("failed to connect to routes/State");
let watcher_proxy = fnet_routes_ext::get_watcher::<I>(&state_proxy)
.expect("failed to connect to watcher protocol");
// Call `Watch` in a loop until the idle event is observed.
while fnet_routes_ext::watch::<I>(&watcher_proxy)
.map(|event_batch| {
event_batch.expect("error while calling watch").into_iter().all(|event| {
use fnet_routes_ext::Event::*;
match event.try_into().expect("failed to process event") {
Existing(_) => true,
Idle => false,
e @ Unknown | e @ Added(_) | e @ Removed(_) => {
panic!("unexpected event received from the routes watcher: {e:?}")
}
}
})
})
.await
{}
// Call `Watch` twice and observe the protocol close.
assert_matches!(
futures::future::join(
fnet_routes_ext::watch::<I>(&watcher_proxy),
fnet_routes_ext::watch::<I>(&watcher_proxy),
)
.await,
(
Err(fidl::Error::ClientChannelClosed { status: zx_status::Status::PEER_CLOSED, .. }),
Err(fidl::Error::ClientChannelClosed { status: zx_status::Status::PEER_CLOSED, .. }),
)
);
assert!(watcher_proxy.is_closed());
}
// Verifies the watcher protocol does not get torn down when the `State`
// protocol is closed.
#[netstack_test]
async fn watcher_outlives_state<
N: Netstack,
I: net_types::ip::Ip + fnet_routes_ext::FidlRouteIpExt,
>(
name: &str,
) {
let sandbox = netemul::TestSandbox::new().expect("create sandbox");
let realm = sandbox.create_netstack_realm::<N, _>(name).expect("create realm");
// Connect to the watcher protocol and consume all existing events.
let state_proxy =
realm.connect_to_protocol::<I::StateMarker>().expect("failed to connect to routes/State");
let event_stream = fnet_routes_ext::event_stream_from_state::<I>(&state_proxy)
.expect("failed to connect to routes watcher");
let event_stream = pin!(event_stream);
// Drop the state proxy and verify the event_stream stays open
drop(state_proxy);
event_stream
// Ignore `Ok` events; the stream closing will generate an `Err`.
.filter(|event| futures::future::ready(event.is_err()))
.next()
.map(Err)
.on_timeout(
fuchsia_async::Time::after(netstack_testing_common::ASYNC_EVENT_NEGATIVE_CHECK_TIMEOUT),
|| Ok(()),
)
.await
.expect("Unexpected event in event stream");
}
/// Verifies several instantiations of the watcher protocol can exist independent
/// of one another.
#[netstack_test]
async fn watcher_multiple_instances<
N: Netstack,
I: net_types::ip::Ip + fnet_routes_ext::FidlRouteIpExt,
>(
name: &str,
) {
const NUM_INSTANCES: u8 = 10;
let sandbox = netemul::TestSandbox::new().expect("create sandbox");
let realm = sandbox.create_netstack_realm::<N, _>(name).expect("create realm");
let device = sandbox.create_endpoint(name).await.expect("create endpoint");
let interface = device.into_interface_in_realm(&realm).await.expect("add endpoint to Netstack");
let state_proxy =
realm.connect_to_protocol::<I::StateMarker>().expect("failed to connect to routes/State");
let mut watchers = Vec::new();
let mut expected_existing_routes = Vec::new();
// For each iteration, instantiate a watcher and add a unique route. The
// route will appear as `added` for all "already-existing" watcher clients,
// but `existing` for all "not-yet-instantiated" watcher clients in future
// iterations. This ensures that each client is operating over a unique
// event stream.
for i in 0..NUM_INSTANCES {
// Connect to the watcher protocol and observe all expected existing
// events
let mut event_stream = fnet_routes_ext::event_stream_from_state::<I>(&state_proxy)
.expect("failed to connect to routes watcher")
.boxed_local();
let existing =
fnet_routes_ext::collect_routes_until_idle::<I, Vec<_>>(event_stream.by_ref())
.await
.expect("failed to collect existing routes");
for route in &expected_existing_routes {
assert!(existing.contains(&route), "route: {:?}, existing: {:?}", route, existing)
}
watchers.push(event_stream);
// Add a test route whose subnet is unique based on `i`.
let subnet: net_types::ip::Subnet<I::Addr> = I::map_ip(
IpInvariant(i),
|IpInvariant(i)| {
net_types::ip::Subnet::new(net_types::ip::Ipv4Addr::new([192, 168, i, 0]), 24)
.unwrap()
},
|IpInvariant(i)| {
net_types::ip::Subnet::new(
net_types::ip::Ipv6Addr::from_bytes([
0xfd, 0, i, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
]),
64,
)
.unwrap()
},
);
interface.add_subnet_route(subnet.into_ext()).await.expect("failed to add route");
let expected_route =
new_installed_route(subnet, interface.id(), DEFAULT_INTERFACE_METRIC, true);
expected_existing_routes.push(expected_route);
// Observe an `added` event on all connected watchers.
for event_stream in watchers.iter_mut() {
let added_route = assert_matches!(
event_stream.next().await,
Some(Ok(fnet_routes_ext::Event::<I>::Added(route))) => route
);
assert_eq!(added_route, expected_route);
}
}
}