| // 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)] |
| #![deny(clippy::await_holding_refcell_ref)] |
| |
| use fidl_fuchsia_net as fnet; |
| use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin; |
| use fidl_fuchsia_net_neighbor as fnet_neighbor; |
| use fidl_fuchsia_net_reachability as fnet_reachability; |
| use fidl_fuchsia_net_stack as fnet_stack; |
| use fidl_fuchsia_testing as ftesting; |
| use fuchsia_async as fasync; |
| use fuchsia_zircon as zx; |
| |
| use assert_matches::assert_matches; |
| use fidl::endpoints::Proxy; |
| use futures::{ |
| channel::mpsc, future::FusedFuture, FutureExt as _, StreamExt as _, TryFutureExt as _, |
| }; |
| use net_declare::{fidl_subnet, net_ip_v4, net_ip_v6, net_mac}; |
| use netstack_testing_common::{ |
| constants::{ipv4 as ipv4_consts, ipv6 as ipv6_consts}, |
| get_inspect_data, interfaces, |
| realms::{constants, KnownServiceProvider, Netstack, TestSandboxExt as _}, |
| wait_for_component_stopped, |
| }; |
| use netstack_testing_macros::netstack_test; |
| use packet::{Buf, InnerPacketBuilder as _, Serializer as _}; |
| use packet_formats::{ |
| ethernet::{ |
| EtherType, EthernetFrameBuilder, EthernetFrameLengthCheck, ETHERNET_MIN_BODY_LEN_NO_TAG, |
| }, |
| icmp::{IcmpEchoRequest, IcmpPacketBuilder, IcmpUnusedCode, MessageBody as _}, |
| ip::{Ipv4Proto, Ipv6Proto}, |
| ipv4::Ipv4PacketBuilder, |
| ipv6::Ipv6PacketBuilder, |
| testutil::parse_icmp_packet_in_ip_packet_in_ethernet_frame, |
| }; |
| use reachability_core::{LinkState, State, FIDL_TIMEOUT_ID}; |
| use std::{cell::RefCell, collections::HashMap, pin::pin, rc::Rc}; |
| use test_case::test_case; |
| use tracing::info; |
| |
| const INTERNET_V4: net_types::ip::Ipv4Addr = net_ip_v4!("8.8.8.8"); |
| const INTERNET_V6: net_types::ip::Ipv6Addr = net_ip_v6!("2001:4860:4860::8888"); |
| |
| const PREFIX_V4: u8 = 24; |
| const PREFIX_V6: u8 = 64; |
| |
| const LOWER_METRIC: u32 = 0; |
| const HIGHER_METRIC: u32 = 100; |
| |
| #[derive(Debug, Clone)] |
| struct InterfaceConfig { |
| name_suffix: &'static str, |
| gateway_v4: net_types::ip::Ipv4Addr, |
| addr_v4: net_types::ip::Ipv4Addr, |
| gateway_v6: net_types::ip::Ipv6Addr, |
| addr_v6: net_types::ip::Ipv6Addr, |
| gateway_mac: net_types::ethernet::Mac, |
| metric: u32, |
| } |
| |
| impl InterfaceConfig { |
| const fn new_primary(metric: u32) -> Self { |
| Self { |
| name_suffix: "primary", |
| gateway_v4: net_ip_v4!("192.168.0.1"), |
| addr_v4: net_ip_v4!("192.168.0.2"), |
| gateway_v6: net_ip_v6!("fe80::1"), |
| addr_v6: net_ip_v6!("fe80::2"), |
| gateway_mac: net_mac!("02:00:00:00:00:01"), |
| metric, |
| } |
| } |
| |
| const fn new_secondary(metric: u32) -> Self { |
| Self { |
| name_suffix: "secondary", |
| gateway_v4: net_ip_v4!("192.168.1.1"), |
| addr_v4: net_ip_v4!("192.168.1.2"), |
| gateway_v6: net_ip_v6!("fe81::1"), |
| addr_v6: net_ip_v6!("fe81::2"), |
| gateway_mac: net_mac!("04:00:00:00:00:01"), |
| metric, |
| } |
| } |
| } |
| |
| /// Try to parse `frame` as an ICMP or ICMPv6 Echo Request message, and if successful returns the |
| /// Echo Reply message that the netstack would expect as a reply. |
| #[track_caller] |
| fn reply_if_echo_request( |
| frame: Vec<u8>, |
| want: State, |
| gateway_v4: &net_types::ip::Ipv4Addr, |
| gateway_v6: &net_types::ip::Ipv6Addr, |
| ) -> Option<Buf<Vec<u8>>> { |
| let mut icmp_body = Vec::new(); |
| let r = parse_icmp_packet_in_ip_packet_in_ethernet_frame::< |
| net_types::ip::Ipv4, |
| _, |
| IcmpEchoRequest, |
| _, |
| >(&frame, EthernetFrameLengthCheck::Check, |p| { |
| icmp_body.extend(p.body().bytes()); |
| }); |
| match r { |
| Ok((src_mac, dst_mac, src_ip, dst_ip, _ttl, message, _code)) => { |
| return match want.link { |
| LinkState::Gateway => dst_ip == *gateway_v4, |
| LinkState::Internet => dst_ip == *gateway_v4 || dst_ip == INTERNET_V4, |
| _ => false, |
| } |
| .then(|| { |
| icmp_body |
| .into_serializer() |
| .encapsulate(IcmpPacketBuilder::<net_types::ip::Ipv4, _>::new( |
| dst_ip, |
| src_ip, |
| IcmpUnusedCode, |
| message.reply(), |
| )) |
| .encapsulate(Ipv4PacketBuilder::new( |
| dst_ip, |
| src_ip, |
| ipv4_consts::DEFAULT_TTL, |
| Ipv4Proto::Icmp, |
| )) |
| .encapsulate(EthernetFrameBuilder::new( |
| dst_mac, |
| src_mac, |
| EtherType::Ipv4, |
| ETHERNET_MIN_BODY_LEN_NO_TAG, |
| )) |
| .serialize_vec_outer() |
| .expect("failed to serialize ICMPv4 packet") |
| .unwrap_b() |
| }); |
| } |
| Err(packet_formats::error::IpParseError::Parse { |
| error: packet_formats::error::ParseError::NotExpected, |
| }) => {} |
| Err(e) => { |
| panic!("parse packet as ICMPv4 error: {}\n{:02x?}", e, frame); |
| } |
| } |
| |
| let mut icmp_body = Vec::new(); |
| let r = parse_icmp_packet_in_ip_packet_in_ethernet_frame::< |
| net_types::ip::Ipv6, |
| _, |
| IcmpEchoRequest, |
| _, |
| >(&frame, EthernetFrameLengthCheck::Check, |p| { |
| icmp_body.extend(p.body().bytes()); |
| }); |
| match r { |
| Ok((src_mac, dst_mac, src_ip, dst_ip, _ttl, message, _code)) => { |
| return match want.link { |
| LinkState::Gateway => dst_ip == *gateway_v6, |
| LinkState::Internet => dst_ip == *gateway_v6 || dst_ip == INTERNET_V6, |
| _ => false, |
| } |
| .then(|| { |
| icmp_body |
| .into_serializer() |
| .encapsulate(IcmpPacketBuilder::<net_types::ip::Ipv6, _>::new( |
| dst_ip, |
| src_ip, |
| IcmpUnusedCode, |
| message.reply(), |
| )) |
| .encapsulate(Ipv6PacketBuilder::new( |
| dst_ip, |
| src_ip, |
| ipv6_consts::DEFAULT_HOP_LIMIT, |
| Ipv6Proto::Icmpv6, |
| )) |
| .encapsulate(EthernetFrameBuilder::new( |
| dst_mac, |
| src_mac, |
| EtherType::Ipv6, |
| ETHERNET_MIN_BODY_LEN_NO_TAG, |
| )) |
| .serialize_vec_outer() |
| .expect("failed to serialize ICMPv6 packet") |
| .unwrap_b() |
| }) |
| } |
| Err(packet_formats::error::IpParseError::Parse { |
| error: packet_formats::error::ParseError::NotExpected, |
| }) => {} |
| Err(e) => { |
| panic!("parse packet as ICMPv6 error: {}\n{:02x?}", e, frame); |
| } |
| } |
| None |
| } |
| |
| #[derive(Copy, Clone, Default, Debug, PartialEq, Eq)] |
| struct IpStates { |
| ipv4: State, |
| ipv6: State, |
| } |
| |
| impl IpStates { |
| fn new(state: State) -> Self { |
| Self { ipv4: state, ipv6: state } |
| } |
| } |
| |
| /// Extract the most recent reachability states for IPv4 and IPv6 from the inspect data. |
| fn extract_reachability_states( |
| data: &diagnostics_hierarchy::DiagnosticsHierarchy, |
| ) -> (HashMap<u64, IpStates>, IpStates) { |
| let get_per_ip_state = |states: &diagnostics_hierarchy::DiagnosticsHierarchy| { |
| let (_, state): (i64, _) = states.children.iter().fold( |
| (-1, State::from(LinkState::None)), |
| |(latest_seqnum, latest_state), state| { |
| let seqnum = state |
| .name |
| .parse::<i64>() |
| .expect("failed to parse reachability state sequence number as integer"); |
| // NB: Since node creation is not atomic, it's possible to read a node with a |
| // sequence number but no state value, so must ensure that state is `Some` before |
| // using it as the latest state. |
| match state.properties.iter().find_map(|p| { |
| if p.key() == "state" { |
| p.string().map(|s| { |
| s.parse::<LinkState>() |
| .unwrap_or_else(|e| { |
| panic!("failed to parse reachability state: {}: {:?}", s, e) |
| }) |
| .into() |
| }) |
| } else { |
| None |
| } |
| }) { |
| Some(state) if seqnum > latest_seqnum => (seqnum, state), |
| Some(_) | None => (latest_seqnum, latest_state), |
| } |
| }, |
| ); |
| state |
| }; |
| let get_state = |data: &diagnostics_hierarchy::DiagnosticsHierarchy| { |
| data.children.iter().fold(IpStates::default(), |IpStates { ipv4, ipv6 }, info| { |
| if info.name == "IPv4" { |
| IpStates { ipv4: get_per_ip_state(info), ipv6 } |
| } else if info.name == "IPv6" { |
| IpStates { ipv4, ipv6: get_per_ip_state(info) } |
| } else { |
| IpStates { ipv4, ipv6 } |
| } |
| }) |
| }; |
| data.children.iter().fold( |
| (HashMap::new(), IpStates::default()), |
| |(mut interfaces, mut system), data| { |
| if data.name == "system" { |
| system = get_state(data); |
| } else { |
| match data.name.parse::<u64>() { |
| Ok(id) => match interfaces.insert(id, get_state(data)) { |
| Some(state) => { |
| panic!( |
| "duplicate interface found in inspect data; id: {} state: {:?}", |
| id, state |
| ); |
| } |
| None => {} |
| }, |
| Err(_) => {} |
| } |
| } |
| (interfaces, system) |
| }, |
| ) |
| } |
| |
| struct NetemulInterface<'a> { |
| _network: netemul::TestNetwork<'a>, |
| interface: netemul::TestInterface<'a>, |
| fake_ep: netemul::TestFakeEndpoint<'a>, |
| state: Rc<RefCell<State>>, |
| } |
| |
| impl<'a> NetemulInterface<'a> { |
| fn id(&self) -> u64 { |
| self.interface.id() |
| } |
| } |
| |
| async fn configure_interface<'a>( |
| iface: &'a netemul::TestInterface<'a>, |
| controller: &fnet_neighbor::ControllerProxy, |
| stack: &fnet_stack::StackProxy, |
| InterfaceConfig { |
| name_suffix: _, |
| gateway_v4, |
| addr_v4, |
| gateway_v6, |
| addr_v6, |
| gateway_mac, |
| metric, |
| }: InterfaceConfig, |
| ) { |
| // Add addresses. |
| futures::stream::iter([ |
| fnet::Subnet { |
| addr: fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: addr_v4.ipv4_bytes() }), |
| prefix_len: PREFIX_V4, |
| }, |
| fnet::Subnet { |
| addr: fnet::IpAddress::Ipv6(fnet::Ipv6Address { addr: addr_v6.ipv6_bytes() }), |
| prefix_len: PREFIX_V6, |
| }, |
| ]) |
| .for_each_concurrent(None, |subnet| async move { |
| let address_state_provider = interfaces::add_subnet_address_and_route_wait_assigned( |
| iface, |
| subnet, |
| fnet_interfaces_admin::AddressParameters::default(), |
| ) |
| .await |
| .expect("add subnet address and route"); |
| let () = address_state_provider.detach().expect("address detach"); |
| }) |
| .await; |
| |
| let device_id = iface.id(); |
| |
| // Add neighbor table entries for the gateway addresses. |
| let gateway_v4 = fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr: gateway_v4.ipv4_bytes() }); |
| let gateway_v6 = fnet::IpAddress::Ipv6(fnet::Ipv6Address { addr: gateway_v6.ipv6_bytes() }); |
| let gateway_mac = fnet::MacAddress { octets: gateway_mac.bytes() }; |
| let () = controller |
| .add_entry(device_id, &gateway_v6, &gateway_mac) |
| .await |
| .expect("neighbor add_entry FIDL error") |
| .map_err(zx::Status::from_raw) |
| .expect("add IPv6 gateway neighbor table entry failed"); |
| let () = controller |
| .add_entry(device_id, &gateway_v4, &gateway_mac) |
| .await |
| .expect("neighbor add_entry FIDL error") |
| .map_err(zx::Status::from_raw) |
| .expect("add IPv4 gateway neighbor table entry failed"); |
| |
| // Add default routes. |
| let () = stack |
| .add_forwarding_entry(&fnet_stack::ForwardingEntry { |
| subnet: fidl_subnet!("0.0.0.0/0"), |
| device_id, |
| next_hop: Some(Box::new(gateway_v4)), |
| metric, |
| }) |
| .await |
| .expect("add IPv4 default route FIDL error") |
| .expect("add IPv4 default route error"); |
| let () = stack |
| .add_forwarding_entry(&fnet_stack::ForwardingEntry { |
| subnet: fidl_subnet!("::/0"), |
| device_id, |
| next_hop: Some(Box::new(gateway_v6)), |
| metric, |
| }) |
| .await |
| .expect("add IPv6 default route FIDL error") |
| .expect("add IPv6 default route error"); |
| } |
| |
| async fn handle_frame_stream<'a>( |
| fake_ep: &'a netemul::TestFakeEndpoint<'_>, |
| gateway_v4: net_types::ip::Ipv4Addr, |
| gateway_v6: net_types::ip::Ipv6Addr, |
| state: Rc<RefCell<State>>, |
| echo_notifier: &mpsc::UnboundedSender<()>, |
| ) { |
| let mut frame_stream = fake_ep.frame_stream(); |
| loop { |
| if let Some(Ok((frame, dropped))) = frame_stream.next().await { |
| echo_notifier.unbounded_send(()).expect("failed to send echo notice"); |
| assert_eq!(dropped, 0); |
| let reply = { |
| let state_ref = state.borrow(); |
| reply_if_echo_request(frame, *state_ref, &gateway_v4, &gateway_v6) |
| }; |
| if let Some(reply) = reply { |
| fake_ep |
| .write(reply.as_ref()) |
| .await |
| .expect("failed to write echo reply to fake endpoint"); |
| } |
| } |
| } |
| } |
| |
| struct ReachabilityEnv<'a> { |
| sandbox: &'a netemul::TestSandbox, |
| realm: netemul::TestRealm<'a>, |
| controller: fnet_neighbor::ControllerProxy, |
| stack: fnet_stack::StackProxy, |
| fake_clock: ftesting::FakeClockControlProxy, |
| } |
| |
| async fn setup_reachability_env<'a, N: Netstack>( |
| name: &'a str, |
| sandbox: &'a netemul::TestSandbox, |
| start_reachability_as_eager: bool, |
| ) -> ReachabilityEnv<'a> { |
| let realm = sandbox |
| .create_netstack_realm_with::<N, _, _>( |
| name, |
| &[ |
| KnownServiceProvider::Reachability { eager: start_reachability_as_eager }, |
| KnownServiceProvider::FakeClock, |
| KnownServiceProvider::DnsResolver, |
| ], |
| ) |
| .expect("failed to create realm"); |
| let controller = realm |
| .connect_to_protocol::<fnet_neighbor::ControllerMarker>() |
| .expect("failed to connect to Controller"); |
| let stack = |
| realm.connect_to_protocol::<fnet_stack::StackMarker>().expect("failed to connect to Stack"); |
| let fake_clock = realm |
| .connect_to_protocol::<ftesting::FakeClockControlMarker>() |
| .expect("failed to connect to FakeClockControl"); |
| let () = initialize_fake_clock(&fake_clock).await; |
| |
| ReachabilityEnv { sandbox, realm, controller, stack, fake_clock } |
| } |
| |
| async fn create_netemul_interfaces<'a>( |
| name: &'a str, |
| sandbox: &'a netemul::TestSandbox, |
| env: &'a ReachabilityEnv<'_>, |
| configs: &[(InterfaceConfig, State)], |
| ) -> Vec<NetemulInterface<'a>> { |
| futures::stream::iter(configs.iter()) |
| .then(|(config, init_state): &(InterfaceConfig, State)| { |
| let config = config.clone(); |
| async { |
| let name = format!("{}_{}", name, config.name_suffix); |
| let network = |
| sandbox.create_network(name.clone()).await.expect("failed to create network"); |
| let fake_ep = |
| network.create_fake_endpoint().expect("failed to create fake endpoint"); |
| let interface = env |
| .realm |
| .join_network(&network, name) |
| .await |
| .expect("failed to join network with netdevice endpoint"); |
| let state = Rc::new(RefCell::new(*init_state)); |
| |
| configure_interface(&interface, &env.controller, &env.stack, config).await; |
| NetemulInterface { _network: network, interface, fake_ep, state } |
| } |
| }) |
| .collect::<_>() |
| .await |
| } |
| |
| async fn echo_reply_streams( |
| interfaces: Vec<NetemulInterface<'_>>, |
| configs: Vec<InterfaceConfig>, |
| echo_notifier: mpsc::UnboundedSender<()>, |
| ) { |
| // TODO(https://fxbug.dev/42071036): Combine configs with the NetemulInterface struct |
| let stream = interfaces |
| .iter() |
| .zip(configs.iter()) |
| .map(|(NetemulInterface { _network, interface: _, fake_ep, state }, config)| { |
| Box::pin(handle_frame_stream( |
| &fake_ep, |
| config.gateway_v4, |
| config.gateway_v6, |
| state.clone(), |
| &echo_notifier, |
| )) |
| }) |
| .collect::<futures::stream::FuturesUnordered<_>>(); |
| let _: Vec<()> = stream.collect::<Vec<_>>().await; |
| } |
| |
| async fn initialize_fake_clock(fake_clock: &ftesting::FakeClockControlProxy) -> () { |
| // Pause the fake clock and ignore the FIDL timeout named deadline. |
| fake_clock.pause().await.expect("failed to pause the clock"); |
| fake_clock |
| .ignore_named_deadline(&FIDL_TIMEOUT_ID.into()) |
| .await |
| .expect("ignore named deadline failed"); |
| |
| // Resume clock at normal increments. |
| fake_clock |
| .resume_with_increments( |
| zx::Duration::from_millis(1).into_nanos(), |
| &ftesting::Increment::Determined(zx::Duration::from_millis(1).into_nanos()), |
| ) |
| .await |
| .expect("resume with increment failed") |
| .expect("resume with increment returned error"); |
| info!("deadline ignored and time resumed"); |
| } |
| |
| async fn accelerate_fake_clock(fake_clock: &ftesting::FakeClockControlProxy) -> () { |
| // Pause the fake clock. |
| fake_clock.pause().await.expect("failed to pause the clock"); |
| |
| // Fast increment speed after pausing. Acceleration past 5x doesn't reduce test runtime, |
| // because it is dominated by setup/teardown. |
| fake_clock |
| .resume_with_increments( |
| zx::Duration::from_millis(1).into_nanos(), |
| &ftesting::Increment::Determined(zx::Duration::from_millis(5).into_nanos()), |
| ) |
| .await |
| .expect("resume with increment failed") |
| .expect("resume with increment returned error"); |
| info!("time accelerated"); |
| } |
| |
| #[netstack_test] |
| #[test_case( |
| "gateway", |
| &[(InterfaceConfig::new_primary(LOWER_METRIC), LinkState::Gateway.into())]; |
| "gateway")] |
| #[test_case( |
| "internet", |
| &[(InterfaceConfig::new_primary(LOWER_METRIC), LinkState::Internet.into())]; |
| "internet")] |
| #[test_case( |
| "gateway_gateway", |
| &[ |
| (InterfaceConfig::new_primary(LOWER_METRIC), LinkState::Gateway.into()), |
| (InterfaceConfig::new_secondary(HIGHER_METRIC), LinkState::Gateway.into()), |
| ]; |
| "gateway_gateway")] |
| #[test_case( |
| "gateway_internet", |
| &[ |
| (InterfaceConfig::new_primary(LOWER_METRIC), LinkState::Gateway.into()), |
| (InterfaceConfig::new_secondary(HIGHER_METRIC), LinkState::Internet.into()), |
| ]; |
| "gateway_internet")] |
| // This test case guards against the regression where if ICMP echo requests are routed according |
| // to the default route rather than explicitly through each interface, packets destined for the |
| // internet intended to be sent out the secondary interface will actually be sent out the primary |
| // interface due to the lower metric, resulting in the reachability state of the secondary |
| // interface to be Internet rather than Gateway, causing the test to fail. |
| #[test_case( |
| "internet_gateway", |
| &[ |
| (InterfaceConfig::new_primary(LOWER_METRIC), LinkState::Internet.into()), |
| (InterfaceConfig::new_secondary(HIGHER_METRIC), LinkState::Gateway.into()), |
| ]; |
| "internet_gateway")] |
| #[test_case( |
| "internet_internet", |
| &[ |
| (InterfaceConfig::new_primary(LOWER_METRIC), LinkState::Internet.into()), |
| (InterfaceConfig::new_secondary(HIGHER_METRIC), LinkState::Internet.into()), |
| ]; |
| "internet_internet")] |
| async fn test_state<N: Netstack>( |
| name: &str, |
| sub_test_name: &str, |
| configs: &[(InterfaceConfig, State)], |
| ) { |
| let name = format!("{}_{}", name, sub_test_name); |
| let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox"); |
| let env = setup_reachability_env::<N>(&name, &sandbox, false).await; |
| |
| // Initialize a connection to the Monitor marker to start the reachability component. |
| let _monitor = env |
| .realm |
| .connect_to_protocol::<fnet_reachability::MonitorMarker>() |
| .expect("failed to connect to fuchsia.net.reachability.Monitor"); |
| |
| let interfaces = create_netemul_interfaces(&name, &env.sandbox, &env, &configs).await; |
| let interfaces_count = interfaces.len(); |
| let want_interfaces = interfaces |
| .iter() |
| .zip(configs.iter()) |
| .map(|(interface, (_, want_state)): (&NetemulInterface<'_>, &(InterfaceConfig, State))| { |
| (interface.id(), IpStates::new(*want_state)) |
| }) |
| .collect::<HashMap<_, _>>(); |
| |
| let interface_configs = |
| configs.iter().cloned().map(|(c, _)| c).collect::<Vec<InterfaceConfig>>(); |
| let (sender, receiver) = mpsc::unbounded(); |
| let echo_receiver = receiver.fuse(); |
| let mut echo_receiver = pin!(echo_receiver); |
| let echo_reply_streams = echo_reply_streams(interfaces, interface_configs, sender).fuse(); |
| let mut echo_reply_streams = pin!(echo_reply_streams); |
| let () = accelerate_fake_clock(&env.fake_clock).await; |
| |
| // TODO(https://fxbug.dev/42144302): Get reachability monitor's reachability state over FIDL rather |
| // than the inspect data. Watching for updates to inspect data is currently not supported, so |
| // poll instead. |
| const INSPECT_COMPONENT: &str = "reachability"; |
| const INSPECT_TREE_SELECTOR: &str = "root"; |
| let realm_ref = &env.realm; |
| let inspect_data_stream = futures::stream::unfold(None, |duration| async move { |
| let () = match duration { |
| None => {} |
| Some(duration) => fasync::Timer::new(duration).await, |
| }; |
| let duration = std::time::Duration::from_millis(100); |
| let yielded = loop { |
| match get_inspect_data(realm_ref, INSPECT_COMPONENT, INSPECT_TREE_SELECTOR, "") |
| .map_ok(|data| extract_reachability_states(&data)) |
| .await |
| { |
| Ok(yielded) => break yielded, |
| // Archivist can sometimes return transient `InconsistentSnapshot` errors. Our |
| // wrapper function discards type information so we can't match on the specific |
| // error. |
| Err(err @ anyhow::Error { .. }) => println!("inspect data stream error: {:?}", err), |
| } |
| let () = fasync::Timer::new(duration).await; |
| }; |
| Some((yielded, Some(duration))) |
| }) |
| .fuse(); |
| let mut inspect_data_stream = pin!(inspect_data_stream); |
| |
| // Ensure that at least one echo request has been replied to before polling the inspect data |
| // stream to guarantee that reachability monitor has initialized its inspect data tree. |
| futures::select! { |
| _ = echo_reply_streams => panic!("echo reply streams ended unexpectedly"), |
| _ = echo_receiver.next() => (), |
| } |
| |
| let IpStates { ipv4: want_system_ipv4, ipv6: want_system_ipv6 } = |
| want_interfaces.values().fold( |
| IpStates::new(LinkState::None.into()), |
| |IpStates { ipv4: best_ipv4, ipv6: best_ipv6 }, &IpStates { ipv4, ipv6 }| IpStates { |
| ipv4: if ipv4 > best_ipv4 { ipv4 } else { best_ipv4 }, |
| ipv6: if ipv6 > best_ipv6 { ipv6 } else { best_ipv6 }, |
| }, |
| ); |
| |
| let reachability_monitor_wait_fut = |
| wait_for_component_stopped(&env.realm, constants::reachability::COMPONENT_NAME, None) |
| .fuse(); |
| let mut reachability_monitor_wait_fut = pin!(reachability_monitor_wait_fut); |
| |
| loop { |
| futures::select! { |
| _ = echo_reply_streams => { |
| panic!("interface echo reply stream ended unexpectedly"); |
| } |
| r = inspect_data_stream.next() => { |
| let (got_interfaces, IpStates { ipv4: got_system_ipv4, ipv6: got_system_ipv6 }) = r |
| .expect("inspect data stream ended unexpectedly"); |
| if got_system_ipv4 > want_system_ipv4 { |
| panic!( |
| "system IPv4 state exceeded; got: {:?}, want: {:?}", |
| got_system_ipv4, want_system_ipv4, |
| ) |
| } |
| if got_system_ipv6 > want_system_ipv6 { |
| panic!( |
| "system IPv6 state exceeded; got: {:?}, want: {:?}", |
| got_system_ipv6, want_system_ipv6, |
| ) |
| } |
| let equal_count = got_interfaces |
| .iter() |
| .filter_map(|(id, got)| { |
| let IpStates { ipv4: want_ipv4, ipv6: want_ipv6 } = |
| want_interfaces.get(&id).unwrap_or_else(|| { |
| panic!( |
| "unknown interface {} with state {:?} found in inspect data", |
| id, got, |
| ) |
| }); |
| let IpStates { ipv4: got_ipv4, ipv6: got_ipv6 } = got; |
| if got_ipv4 > want_ipv4 { |
| panic!( |
| "interface {} IPv4 state exceeded; got: {:?}, want: {:?}", |
| id, got_ipv4, want_ipv4, |
| ) |
| } |
| if got_ipv6 > want_ipv6 { |
| panic!( |
| "interface {} IPv6 state exceeded; got: {:?}, want: {:?}", |
| id, got_ipv6, want_ipv6, |
| ) |
| } |
| (got_ipv4 == want_ipv4 && got_ipv6 == want_ipv6).then(|| ()) |
| }) |
| .count(); |
| if got_system_ipv4 == want_system_ipv4 |
| && got_system_ipv6 == want_system_ipv6 |
| && equal_count == interfaces_count |
| { |
| break; |
| } |
| } |
| event = reachability_monitor_wait_fut => { |
| panic!("reachability monitor terminated unexpectedly with event: {:?}", event); |
| } |
| } |
| } |
| } |
| |
| struct ReachabilityTestHelper<'a> { |
| env: &'a ReachabilityEnv<'a>, |
| monitor: fnet_reachability::MonitorProxy, |
| iface_states: Vec<Rc<RefCell<State>>>, |
| echo_reply_streams: std::pin::Pin<Box<dyn FusedFuture<Output = ()> + 'a>>, |
| _echo_reply_receiver: mpsc::UnboundedReceiver<()>, |
| } |
| |
| impl<'a> ReachabilityTestHelper<'a> { |
| async fn new( |
| name: &'a str, |
| env: &'a ReachabilityEnv<'a>, |
| configs: Vec<(InterfaceConfig, State)>, |
| ) -> ReachabilityTestHelper<'a> { |
| let monitor = env |
| .realm |
| .connect_to_protocol::<fnet_reachability::MonitorMarker>() |
| .expect("failed to connect to fuchsia.net.reachability.Monitor"); |
| |
| let interface_configs = |
| configs.iter().cloned().map(|(c, _)| c).collect::<Vec<InterfaceConfig>>(); |
| let interfaces = create_netemul_interfaces(name, &env.sandbox, &env, &configs).await; |
| |
| let iface_states = interfaces.iter().map(|iface| iface.state.clone()).collect(); |
| |
| let (sender, receiver) = mpsc::unbounded(); |
| |
| let echo_reply_streams = |
| Box::pin(echo_reply_streams(interfaces, interface_configs, sender).fuse()); |
| |
| let () = accelerate_fake_clock(&env.fake_clock).await; |
| |
| Self { env, monitor, iface_states, echo_reply_streams, _echo_reply_receiver: receiver } |
| } |
| |
| async fn next_snapshot(&mut self) -> fnet_reachability::Snapshot { |
| let reachability_monitor_wait_fut = wait_for_component_stopped( |
| &self.env.realm, |
| constants::reachability::COMPONENT_NAME, |
| None, |
| ) |
| .fuse(); |
| let mut reachability_monitor_wait_fut = pin!(reachability_monitor_wait_fut); |
| |
| let snapshot_fut = self.monitor.watch(); |
| let mut snapshot_fut = pin!(snapshot_fut); |
| |
| futures::select! { |
| _ = self.echo_reply_streams.as_mut() => { |
| panic!("interface echo reply stream ended unexpectedly"); |
| } |
| r = snapshot_fut => { |
| match r { |
| Ok(snapshot) => { |
| return snapshot; |
| }, |
| Err(e) => panic!("failed to fetch updated snapshot {}", e), |
| } |
| } |
| event = reachability_monitor_wait_fut => { |
| panic!("reachability monitor terminated unexpectedly with event: {:?}", event); |
| } |
| } |
| } |
| |
| // Due to the DNS lookup probe running independently from the network check probes, |
| // intermediary states may be inconsistently present depending on the test environment. The |
| // integration tests specify the network configuration under test, and these helper functions |
| // assume that the condition is eventually met. |
| async fn next_snapshot_with_internet( |
| &mut self, |
| is_available: bool, |
| ) -> fnet_reachability::Snapshot { |
| loop { |
| let snapshot = self.next_snapshot().await; |
| if snapshot.internet_available == Some(is_available) { |
| break snapshot; |
| } |
| } |
| } |
| |
| async fn next_snapshot_with_gateway( |
| &mut self, |
| is_reachable: bool, |
| ) -> fnet_reachability::Snapshot { |
| loop { |
| let snapshot = self.next_snapshot().await; |
| if snapshot.gateway_reachable == Some(is_reachable) { |
| break snapshot; |
| } |
| } |
| } |
| |
| fn set_iface_states(&mut self, states: Vec<State>) { |
| assert!(states.len() == self.iface_states.len()); |
| self.iface_states |
| .iter() |
| .zip(states) |
| .for_each(|(iface_state, new_state)| *iface_state.borrow_mut() = new_state); |
| } |
| } |
| |
| #[netstack_test] |
| #[test_case(LinkState::Internet.into(), LinkState::Internet.into())] |
| #[test_case(LinkState::Internet.into(), LinkState::Gateway.into())] |
| #[test_case(LinkState::Internet.into(), LinkState::Up.into())] |
| async fn test_internet_available<N: Netstack>(name: &str, state1: State, state2: State) { |
| let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox"); |
| let env = setup_reachability_env::<N>(name, &sandbox, false).await; |
| let configs = vec![ |
| (InterfaceConfig::new_primary(LOWER_METRIC), state1), |
| (InterfaceConfig::new_secondary(HIGHER_METRIC), state2), |
| ]; |
| let mut helper = ReachabilityTestHelper::new(name, &env, configs).await; |
| |
| let snapshot = helper.next_snapshot_with_internet(true).await; |
| // Gateway reachability is a condition of internet availability, |
| // therefore, when internet is available, a gateway is reachable. |
| assert_eq!(snapshot.gateway_reachable, Some(true)); |
| assert_eq!(snapshot.internet_available, Some(true)); |
| } |
| |
| #[netstack_test] |
| #[test_case(LinkState::Internet.into(), LinkState::Internet.into())] |
| #[test_case(LinkState::Internet.into(), LinkState::Gateway.into())] |
| #[test_case(LinkState::Internet.into(), LinkState::Up.into())] |
| async fn test_internet_comes_up<N: Netstack>(name: &str, state1: State, state2: State) { |
| let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox"); |
| let env = setup_reachability_env::<N>(name, &sandbox, false).await; |
| let configs = vec![ |
| (InterfaceConfig::new_primary(LOWER_METRIC), LinkState::Up.into()), |
| (InterfaceConfig::new_secondary(HIGHER_METRIC), LinkState::Up.into()), |
| ]; |
| let mut helper = ReachabilityTestHelper::new(name, &env, configs).await; |
| |
| helper.set_iface_states(vec![state1, state2]); |
| let snapshot = helper.next_snapshot_with_internet(true).await; |
| assert_eq!(snapshot.gateway_reachable, Some(true)); |
| assert_eq!(snapshot.internet_available, Some(true)); |
| } |
| |
| #[netstack_test] |
| #[test_case(LinkState::Gateway.into(), LinkState::Gateway.into())] |
| #[test_case(LinkState::Up.into(), LinkState::Up.into())] |
| async fn test_internet_goes_down<N: Netstack>(name: &str, state1: State, state2: State) { |
| let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox"); |
| let env = setup_reachability_env::<N>(name, &sandbox, false).await; |
| let configs = vec![ |
| (InterfaceConfig::new_primary(LOWER_METRIC), LinkState::Internet.into()), |
| (InterfaceConfig::new_secondary(HIGHER_METRIC), LinkState::Internet.into()), |
| ]; |
| let mut helper = ReachabilityTestHelper::new(name, &env, configs).await; |
| |
| let snapshot = helper.next_snapshot_with_internet(true).await; |
| assert_eq!(snapshot.gateway_reachable, Some(true)); |
| assert_eq!(snapshot.internet_available, Some(true)); |
| |
| helper.set_iface_states(vec![state1, state2]); |
| let snapshot = helper.next_snapshot_with_internet(false).await; |
| assert_eq!(snapshot.internet_available, Some(false)); |
| } |
| |
| #[netstack_test] |
| #[test_case(LinkState::Gateway.into(), LinkState::Gateway.into())] |
| #[test_case(LinkState::Gateway.into(), LinkState::Up.into())] |
| async fn test_gateway_goes_down<N: Netstack>(name: &str, state1: State, state2: State) { |
| let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox"); |
| let env = setup_reachability_env::<N>(name, &sandbox, false).await; |
| let configs = vec![ |
| (InterfaceConfig::new_primary(LOWER_METRIC), state1), |
| (InterfaceConfig::new_secondary(HIGHER_METRIC), state2), |
| ]; |
| let mut helper = ReachabilityTestHelper::new(name, &env, configs).await; |
| |
| let snapshot = helper.next_snapshot_with_gateway(true).await; |
| assert_eq!(snapshot.gateway_reachable, Some(true)); |
| assert_eq!(snapshot.internet_available, Some(false)); |
| |
| helper.set_iface_states(vec![LinkState::Up.into(), LinkState::Up.into()]); |
| let snapshot = helper.next_snapshot_with_gateway(false).await; |
| assert_eq!(snapshot.gateway_reachable, Some(false)); |
| assert_eq!(snapshot.internet_available, Some(false)); |
| } |
| |
| #[netstack_test] |
| async fn test_internet_to_gateway_state<N: Netstack>(name: &str) { |
| let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox"); |
| let env = setup_reachability_env::<N>(name, &sandbox, false).await; |
| let configs = vec![ |
| (InterfaceConfig::new_primary(LOWER_METRIC), LinkState::Internet.into()), |
| (InterfaceConfig::new_secondary(HIGHER_METRIC), LinkState::Gateway.into()), |
| ]; |
| let mut helper = ReachabilityTestHelper::new(name, &env, configs).await; |
| |
| let snapshot = helper.next_snapshot_with_internet(true).await; |
| assert_eq!(snapshot.gateway_reachable, Some(true)); |
| assert_eq!(snapshot.internet_available, Some(true)); |
| |
| helper.set_iface_states(vec![LinkState::Gateway.into(), LinkState::Gateway.into()]); |
| let snapshot = helper.next_snapshot_with_gateway(true).await; |
| assert_eq!(snapshot.gateway_reachable, Some(true)); |
| assert_eq!(snapshot.internet_available, Some(false)); |
| } |
| |
| #[netstack_test] |
| async fn test_hanging_get_multiple_clients<N: Netstack>(name: &str) { |
| let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox"); |
| let realm = sandbox |
| .create_netstack_realm_with::<N, _, _>( |
| name, |
| &[ |
| KnownServiceProvider::Reachability { eager: true }, |
| KnownServiceProvider::FakeClock, |
| KnownServiceProvider::DnsResolver, |
| ], |
| ) |
| .expect("failed to create realm"); |
| |
| let monitor_client1 = realm |
| .connect_to_protocol::<fnet_reachability::MonitorMarker>() |
| .expect("client 1: failed to connect to fuchsia.net.reachability.Monitor"); |
| |
| let monitor_client2 = realm |
| .connect_to_protocol::<fnet_reachability::MonitorMarker>() |
| .expect("client 2: failed to connect to fuchsia.net.reachability.Monitor"); |
| |
| assert_eq!( |
| monitor_client1 |
| .watch() |
| .await |
| .expect("client 1: failed to fetch updated snapshot") |
| .internet_available, |
| Some(false) |
| ); |
| assert_eq!( |
| monitor_client2 |
| .watch() |
| .await |
| .expect("client 2: failed to fetch updated snapshot") |
| .internet_available, |
| Some(false) |
| ); |
| } |
| |
| #[netstack_test] |
| async fn test_cannot_call_set_options_after_watch<N: Netstack>(name: &str) { |
| let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox"); |
| let realm = sandbox |
| .create_netstack_realm_with::<N, _, _>( |
| name, |
| &[ |
| KnownServiceProvider::Reachability { eager: true }, |
| KnownServiceProvider::FakeClock, |
| KnownServiceProvider::DnsResolver, |
| ], |
| ) |
| .expect("failed to create realm"); |
| |
| let monitor = realm |
| .connect_to_protocol::<fnet_reachability::MonitorMarker>() |
| .expect("failed to connect to fuchsia.net.reachability.Monitor"); |
| |
| assert_matches!( |
| monitor.watch().await.expect("failed to fetch updated snapshot").internet_available, |
| Some(_) |
| ); |
| assert_matches!(monitor.set_options(&fnet_reachability::MonitorOptions::default()), Ok(())); |
| |
| // The request stream should be closed if the client calls SetOptions after calling Watch |
| // previously. |
| assert_matches!(monitor.on_closed().await, Ok(_)); |
| } |
| |
| #[netstack_test] |
| async fn test_cannot_call_set_options_twice<N: Netstack>(name: &str) { |
| let sandbox = netemul::TestSandbox::new().expect("failed to create sandbox"); |
| let realm = sandbox |
| .create_netstack_realm_with::<N, _, _>( |
| name, |
| &[ |
| KnownServiceProvider::Reachability { eager: true }, |
| KnownServiceProvider::FakeClock, |
| KnownServiceProvider::DnsResolver, |
| ], |
| ) |
| .expect("failed to create realm"); |
| |
| let monitor = realm |
| .connect_to_protocol::<fnet_reachability::MonitorMarker>() |
| .expect("failed to connect to fuchsia.net.reachability.Monitor"); |
| |
| let () = monitor |
| .set_options(&fnet_reachability::MonitorOptions::default()) |
| .expect("failed to set reachability API options"); |
| let () = monitor |
| .set_options(&fnet_reachability::MonitorOptions::default()) |
| .expect("failed to set reachability API options"); |
| |
| // The request stream should be closed if the client calls SetOptions after calling it once |
| // already. |
| assert_matches!(monitor.on_closed().await, Ok(_)); |
| } |