[netstack] Add first neighbor integration test
Adds integration test for fuchsia.net.neighbor/View.ListEntries.
Test: fx test netstack_neighbor_integration_test
Change-Id: Ibdc4d4a8692d9cfb073d9cf3896171a5c8d9a308
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/423121
Commit-Queue: Bruno Dal Bo <brunodalbo@google.com>
Reviewed-by: Ghanan Gowripalan <ghanan@google.com>
Reviewed-by: Sam Balana <sbalana@google.com>
Reviewed-by: Tamir Duberstein <tamird@google.com>
Testability-Review: Ghanan Gowripalan <ghanan@google.com>
Testability-Review: Tamir Duberstein <tamird@google.com>
diff --git a/src/connectivity/network/tests/integration/BUILD.gn b/src/connectivity/network/tests/integration/BUILD.gn
index 42cfda1..6fa0922 100644
--- a/src/connectivity/network/tests/integration/BUILD.gn
+++ b/src/connectivity/network/tests/integration/BUILD.gn
@@ -21,6 +21,7 @@
"inspect",
"ipv6",
"management",
+ "neighbor",
"routes",
"socket",
]
diff --git a/src/connectivity/network/tests/integration/common/BUILD.gn b/src/connectivity/network/tests/integration/common/BUILD.gn
index 27c106b..e42f2e9 100644
--- a/src/connectivity/network/tests/integration/common/BUILD.gn
+++ b/src/connectivity/network/tests/integration/common/BUILD.gn
@@ -14,6 +14,7 @@
"//sdk/fidl/fuchsia.net.filter:fuchsia.net.filter-rustc",
"//sdk/fidl/fuchsia.net.interfaces:fuchsia.net.interfaces-rustc",
"//sdk/fidl/fuchsia.net.name:fuchsia.net.name-rustc",
+ "//sdk/fidl/fuchsia.net.neighbor:fuchsia.net.neighbor-rustc",
"//sdk/fidl/fuchsia.net.routes:fuchsia.net.routes-rustc",
"//sdk/fidl/fuchsia.net.stack:fuchsia.net.stack-rustc",
"//sdk/fidl/fuchsia.netstack:fuchsia.netstack-rustc",
diff --git a/src/connectivity/network/tests/integration/common/src/environments.rs b/src/connectivity/network/tests/integration/common/src/environments.rs
index d9e8bce..78206d1 100644
--- a/src/connectivity/network/tests/integration/common/src/environments.rs
+++ b/src/connectivity/network/tests/integration/common/src/environments.rs
@@ -59,6 +59,8 @@
RoutesState(NetstackVersion),
InterfaceState(NetstackVersion),
Log(NetstackVersion),
+ NeighborView(NetstackVersion),
+ NeighborController(NetstackVersion),
MockCobalt,
SecureStash,
DhcpServer,
@@ -86,6 +88,10 @@
v.get_url()),
KnownServices::Log(v) => (<fidl_fuchsia_net_stack::LogMarker as fidl::endpoints::DiscoverableService>::SERVICE_NAME,
v.get_url()),
+ KnownServices::NeighborView(v) => (<fidl_fuchsia_net_neighbor::ViewMarker as fidl::endpoints::DiscoverableService>::SERVICE_NAME,
+ v.get_url()),
+ KnownServices::NeighborController(v) => (<fidl_fuchsia_net_neighbor::ControllerMarker as fidl::endpoints::DiscoverableService>::SERVICE_NAME,
+ v.get_url()),
KnownServices::SecureStash => (<fidl_fuchsia_stash::SecureStoreMarker as fidl::endpoints::DiscoverableService>::SERVICE_NAME,
"fuchsia-pkg://fuchsia.com/netstack-integration-tests#meta/stash_secure.cmx"),
KnownServices::DhcpServer => (<fidl_fuchsia_net_dhcp::Server_Marker as fidl::endpoints::DiscoverableService>::SERVICE_NAME,
@@ -267,6 +273,8 @@
KnownServices::SocketProvider(N::VERSION),
KnownServices::InterfaceState(N::VERSION),
KnownServices::Log(N::VERSION),
+ KnownServices::NeighborView(N::VERSION),
+ KnownServices::NeighborController(N::VERSION),
KnownServices::MockCobalt,
]
.iter()
diff --git a/src/connectivity/network/tests/integration/meta/netstack_neighbor_integration_test.cmx b/src/connectivity/network/tests/integration/meta/netstack_neighbor_integration_test.cmx
new file mode 100644
index 0000000..947a90c
--- /dev/null
+++ b/src/connectivity/network/tests/integration/meta/netstack_neighbor_integration_test.cmx
@@ -0,0 +1,18 @@
+{
+ "facets": {
+ "fuchsia.test": {
+ "injected-services": {
+ "fuchsia.netemul.sandbox.Sandbox": "fuchsia-pkg://fuchsia.com/netstack-integration-tests#meta/netemul-sandbox.cmx"
+ }
+ }
+ },
+ "program": {
+ "binary": "bin/netstack_neighbor_integration_test"
+ },
+ "sandbox": {
+ "services": [
+ "fuchsia.netemul.sandbox.Sandbox",
+ "fuchsia.sys.Launcher"
+ ]
+ }
+}
diff --git a/src/connectivity/network/tests/integration/neighbor/BUILD.gn b/src/connectivity/network/tests/integration/neighbor/BUILD.gn
new file mode 100644
index 0000000..d9e921c
--- /dev/null
+++ b/src/connectivity/network/tests/integration/neighbor/BUILD.gn
@@ -0,0 +1,25 @@
+# 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.
+
+import("//build/rust/rustc_test.gni")
+
+rustc_test("neighbor") {
+ output_name = "netstack_neighbor_integration_test"
+ deps = [
+ "//sdk/fidl/fuchsia.net:fuchsia.net-rustc",
+ "//sdk/fidl/fuchsia.net.interfaces:fuchsia.net.interfaces-rustc",
+ "//sdk/fidl/fuchsia.net.neighbor:fuchsia.net.neighbor-rustc",
+ "//src/connectivity/lib/net-declare",
+ "//src/connectivity/network/testing/netemul/rust:lib",
+ "//src/connectivity/network/tests/integration/common:netstack_testing_common",
+ "//src/lib/async-utils",
+ "//src/lib/fidl/rust/fidl",
+ "//src/lib/fuchsia-async",
+ "//src/lib/network/fidl_fuchsia_net_ext",
+ "//src/lib/network/fidl_fuchsia_net_interfaces_ext",
+ "//third_party/rust_crates:anyhow",
+ "//third_party/rust_crates:futures",
+ ]
+ sources = [ "src/lib.rs" ]
+}
diff --git a/src/connectivity/network/tests/integration/neighbor/src/lib.rs b/src/connectivity/network/tests/integration/neighbor/src/lib.rs
new file mode 100644
index 0000000..d6bc06b
--- /dev/null
+++ b/src/connectivity/network/tests/integration/neighbor/src/lib.rs
@@ -0,0 +1,338 @@
+// 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::convert::From as _;
+
+use fuchsia_async as fasync;
+
+use anyhow::Context as _;
+use futures::{FutureExt as _, TryFutureExt as _, TryStreamExt as _};
+use net_declare::{fidl_ip, fidl_mac};
+use netemul::{
+ Endpoint as _, EnvironmentUdpSocket as _, TestEnvironment, TestInterface, TestNetwork,
+ TestSandbox,
+};
+use netstack_testing_common::environments::*;
+use netstack_testing_common::Result;
+
+const ALICE_MAC: fidl_fuchsia_net::MacAddress = fidl_mac!("02:00:01:02:03:04");
+const ALICE_IP: fidl_fuchsia_net::IpAddress = fidl_ip!(192.168.0.100);
+const BOB_MAC: fidl_fuchsia_net::MacAddress = fidl_mac!("02:0A:0B:0C:0D:0E");
+const BOB_IP: fidl_fuchsia_net::IpAddress = fidl_ip!(192.168.0.1);
+const SUBNET_PREFIX: u8 = 24;
+
+/// Helper function to create an environment with a static IP address and an
+/// endpoint with a set MAC address.
+///
+/// Returns the created environment, ep, and the first observed assigned IPv6
+/// address.
+async fn create_environment<'a>(
+ sandbox: &'a TestSandbox,
+ network: &'a TestNetwork<'a>,
+ test_name: &'static str,
+ variant_name: &'static str,
+ static_addr: fidl_fuchsia_net::Subnet,
+ mac: fidl_fuchsia_net::MacAddress,
+) -> Result<(TestEnvironment<'a>, TestInterface<'a>, fidl_fuchsia_net::IpAddress)> {
+ let env = sandbox
+ .create_netstack_environment::<Netstack2, _>(format!("{}_{}", test_name, variant_name))
+ .context("failed to create environment")?;
+ let ep = env
+ .join_network_with(
+ &network,
+ format!("ep-{}", variant_name),
+ netemul::NetworkDevice::make_config(netemul::DEFAULT_MTU, Some(mac)),
+ &netemul::InterfaceConfig::StaticIp(static_addr),
+ )
+ .await
+ .context("failed to join network")?;
+
+ // Get IPv6 address.
+ let interfaces = env
+ .connect_to_service::<fidl_fuchsia_net_interfaces::StateMarker>()
+ .context("failed to connect to interfaces.State")?;
+ let addr = fidl_fuchsia_net_interfaces_ext::wait_interface_with_id(
+ fidl_fuchsia_net_interfaces_ext::event_stream_from_state(&interfaces)
+ .context("failed to get interfaces stream")?,
+ &mut fidl_fuchsia_net_interfaces_ext::InterfaceState::Unknown(ep.id()),
+ |props| {
+ props.addresses.as_ref()?.iter().find_map(|addr| match addr.addr?.addr {
+ a @ fidl_fuchsia_net::IpAddress::Ipv6(_) => Some(a.clone()),
+ fidl_fuchsia_net::IpAddress::Ipv4(_) => None,
+ })
+ },
+ )
+ .await
+ .context("failed to retrieve IPv6 address")?;
+
+ Ok((env, ep, addr))
+}
+
+/// Gets a neighbor entry iterator stream with `options` in `env`.
+fn get_entry_iterator(
+ env: &TestEnvironment<'_>,
+ options: fidl_fuchsia_net_neighbor::EntryIteratorOptions,
+) -> Result<impl futures::Stream<Item = Result<fidl_fuchsia_net_neighbor::EntryIteratorItem>>> {
+ let view = env
+ .connect_to_service::<fidl_fuchsia_net_neighbor::ViewMarker>()
+ .context("failed to connect to fuchsia.net.neighbor/View")?;
+ let (proxy, server_end) =
+ fidl::endpoints::create_proxy::<fidl_fuchsia_net_neighbor::EntryIteratorMarker>()
+ .context("failed to create EntryIterator proxy")?;
+ let () =
+ view.open_entry_iterator(server_end, options).context("failed to open EntryIterator")?;
+ Ok(futures::stream::try_unfold(proxy, |proxy| {
+ proxy
+ .get_next()
+ .map(|r| r.context("fuchsia.net.neighbor/EntryIterator.GetNext FIDL error"))
+ .map_ok(|it| Some((futures::stream::iter(it.into_iter().map(Result::Ok)), proxy)))
+ })
+ .try_flatten())
+}
+
+/// Retrieves all existing neighbor entries in `env`.
+///
+/// Entries are identified by unique `(interface_id, ip_address)` tuples in the
+/// returned map.
+async fn list_existing_entries(
+ env: &TestEnvironment<'_>,
+) -> Result<HashMap<(u64, fidl_fuchsia_net::IpAddress), fidl_fuchsia_net_neighbor::Entry>> {
+ use async_utils::fold::*;
+ try_fold_while(
+ get_entry_iterator(env, fidl_fuchsia_net_neighbor::EntryIteratorOptions::empty())?,
+ HashMap::new(),
+ |mut map, item| {
+ futures::future::ready(match item {
+ fidl_fuchsia_net_neighbor::EntryIteratorItem::Existing(e) => {
+ if let fidl_fuchsia_net_neighbor::Entry {
+ interface: Some(interface),
+ neighbor: Some(neighbor),
+ ..
+ } = &e
+ {
+ if let Some(e) = map.insert((*interface, neighbor.clone()), e) {
+ Err(anyhow::anyhow!("duplicate entry detected in map: {:?}", e))
+ } else {
+ Ok(FoldWhile::Continue(map))
+ }
+ } else {
+ Err(anyhow::anyhow!(
+ "missing interface or neighbor in existing entry: {:?}",
+ e
+ ))
+ }
+ }
+ fidl_fuchsia_net_neighbor::EntryIteratorItem::Idle(
+ fidl_fuchsia_net_neighbor::IdleEvent {},
+ ) => Ok(FoldWhile::Done(map)),
+ x @ fidl_fuchsia_net_neighbor::EntryIteratorItem::Added(_)
+ | x @ fidl_fuchsia_net_neighbor::EntryIteratorItem::Changed(_)
+ | x @ fidl_fuchsia_net_neighbor::EntryIteratorItem::Removed(_) => {
+ Err(anyhow::anyhow!("unexpected EntryIteratorItem before Idle: {:?}", x))
+ }
+ })
+ },
+ )
+ .await
+ .and_then(|r| {
+ r.short_circuited().map_err(|e| {
+ anyhow::anyhow!("entry iterator stream ended unexpectedly with state {:?}", e)
+ })
+ })
+}
+
+/// Helper function to exchange a single UDP datagram.
+///
+/// `alice` will send a single UDP datagram to `bob`. This function will block
+/// until `bob` receives the datagram.
+async fn exchange_dgram(
+ alice: &TestEnvironment<'_>,
+ alice_addr: fidl_fuchsia_net::IpAddress,
+ bob: &TestEnvironment<'_>,
+ bob_addr: fidl_fuchsia_net::IpAddress,
+) -> Result {
+ let fidl_fuchsia_net_ext::IpAddress(alice_addr) =
+ fidl_fuchsia_net_ext::IpAddress::from(alice_addr);
+ let alice_addr = std::net::SocketAddr::new(alice_addr, 1234);
+
+ let fidl_fuchsia_net_ext::IpAddress(bob_addr) = fidl_fuchsia_net_ext::IpAddress::from(bob_addr);
+ let bob_addr = std::net::SocketAddr::new(bob_addr, 8080);
+
+ let alice_sock = fuchsia_async::net::UdpSocket::bind_in_env(alice, alice_addr)
+ .await
+ .context("failed to create client socket")?;
+
+ let bob_sock = fuchsia_async::net::UdpSocket::bind_in_env(bob, bob_addr)
+ .await
+ .context("failed to create server socket")?;
+
+ const PAYLOAD: &'static str = "Hello Neighbor";
+ let mut buf = [0u8; 512];
+ let (sent, (rcvd, from)) = futures::future::try_join(
+ alice_sock.send_to(PAYLOAD.as_bytes(), bob_addr).map(|r| r.context("UDP send_to failed")),
+ bob_sock.recv_from(&mut buf[..]).map(|r| r.context("UDP recv_from failed")),
+ )
+ .await?;
+ assert_eq!(sent, PAYLOAD.as_bytes().len());
+ assert_eq!(rcvd, PAYLOAD.as_bytes().len());
+ assert_eq!(&buf[..rcvd], PAYLOAD.as_bytes());
+ // Check equality on IP and port separately since for IPv6 the scope ID may
+ // differ, making a direct equality fail.
+ assert_eq!(from.ip(), alice_addr.ip());
+ assert_eq!(from.port(), alice_addr.port());
+ Ok(())
+}
+
+/// Helper function to assert validity of a reachable entry.
+fn assert_reachable_entry(
+ entry: fidl_fuchsia_net_neighbor::Entry,
+ match_iface: u64,
+ match_neighbor: fidl_fuchsia_net::IpAddress,
+ match_mac: fidl_fuchsia_net::MacAddress,
+) {
+ match entry {
+ fidl_fuchsia_net_neighbor::Entry {
+ interface: Some(iface),
+ neighbor: Some(neighbor),
+ state:
+ Some(fidl_fuchsia_net_neighbor::EntryState::Reachable(
+ // TODO(fxbug.dev/59372): Capture and assert expiration
+ // value
+ fidl_fuchsia_net_neighbor::ReachableState { expires_at: None },
+ )),
+ mac: Some(mac),
+ updated_at: Some(updated),
+ } => {
+ assert_eq!(iface, match_iface);
+ assert_eq!(neighbor, match_neighbor);
+ assert_eq!(mac, match_mac);
+ assert!(updated > 0, "expected greater than 0, got: {}", updated);
+ }
+ x => panic!("incomplete or bad state reachable neighbor entry: {:?}", x),
+ }
+}
+
+/// Helper function to assert validity of a stale entry.
+fn assert_stale_entry(
+ entry: fidl_fuchsia_net_neighbor::Entry,
+ match_iface: u64,
+ match_neighbor: fidl_fuchsia_net::IpAddress,
+ match_mac: fidl_fuchsia_net::MacAddress,
+) {
+ match entry {
+ fidl_fuchsia_net_neighbor::Entry {
+ interface: Some(iface),
+ neighbor: Some(neighbor),
+ state:
+ Some(fidl_fuchsia_net_neighbor::EntryState::Stale(
+ fidl_fuchsia_net_neighbor::StaleState {},
+ )),
+ mac: Some(mac),
+ updated_at: Some(updated),
+ } => {
+ assert_eq!(iface, match_iface);
+ assert_eq!(neighbor, match_neighbor);
+ assert_eq!(mac, match_mac);
+ assert!(updated > 0, "expected greater than 0, got: {}", updated);
+ }
+ x => panic!("incomplete or bad state stale neighbor entry: {:?}", x),
+ }
+}
+
+#[fasync::run_singlethreaded(test)]
+async fn neigh_list_entries() -> Result {
+ // TODO(fxbug.dev/59425): Extend this test with hanging get.
+
+ const TEST_NAME: &'static str = "neigh_list_entries";
+ let sandbox = TestSandbox::new().context("failed to create sandbox")?;
+ let network = sandbox.create_network("net").await.context("failed to create network")?;
+
+ let (alice_env, alice_iface, alice_ipv6) = create_environment(
+ &sandbox,
+ &network,
+ TEST_NAME,
+ "alice",
+ fidl_fuchsia_net::Subnet { addr: ALICE_IP, prefix_len: SUBNET_PREFIX },
+ ALICE_MAC,
+ )
+ .await
+ .context("failed to setup alice environment")?;
+
+ let (bob_env, bob_iface, bob_ipv6) = create_environment(
+ &sandbox,
+ &network,
+ TEST_NAME,
+ "bob",
+ fidl_fuchsia_net::Subnet { addr: BOB_IP, prefix_len: SUBNET_PREFIX },
+ BOB_MAC,
+ )
+ .await
+ .context("failed to setup bob environment")?;
+
+ // No Neighbors should exist initially.
+ let alice_entries =
+ list_existing_entries(&alice_env).await.context("failed to get entries for alice")?;
+ assert!(alice_entries.is_empty(), "expected empty set of entries: {:?}", alice_entries);
+ let bob_entries =
+ list_existing_entries(&bob_env).await.context("failed to get entries for bob")?;
+ assert!(bob_entries.is_empty(), "expected empty set of entries: {:?}", bob_entries);
+
+ // Send a single UDP datagram between alice and bob.
+ let () = exchange_dgram(&alice_env, ALICE_IP, &bob_env, BOB_IP)
+ .await
+ .context("IPv4 exchange failed")?;
+ let () = exchange_dgram(&alice_env, alice_ipv6, &bob_env, bob_ipv6)
+ .await
+ .context("IPv6 exchange failed")?;
+
+ // Check that bob is listed as a neighbor for alice.
+ let mut alice_entries =
+ list_existing_entries(&alice_env).await.context("failed to get entries for alice")?;
+ // IPv4 entry.
+ let () = assert_reachable_entry(
+ alice_entries.remove(&(alice_iface.id(), BOB_IP)).expect("missing neighbor entry"),
+ alice_iface.id(),
+ BOB_IP,
+ BOB_MAC,
+ );
+ // IPv6 entry.
+ let () = assert_reachable_entry(
+ alice_entries.remove(&(alice_iface.id(), bob_ipv6)).expect("missing neighbor entry"),
+ alice_iface.id(),
+ bob_ipv6,
+ BOB_MAC,
+ );
+ assert!(
+ alice_entries.is_empty(),
+ "unexpected neighbors remaining in list: {:?}",
+ alice_entries
+ );
+
+ // Check that alice is listed as a neighbor for bob. Bob should have alice
+ // listed as STALE entries due to having received solicitations as part of
+ // the UDP exchange.
+ let mut bob_entries =
+ list_existing_entries(&bob_env).await.context("failed to get entries for bob")?;
+
+ // IPv4 entry.
+ let () = assert_stale_entry(
+ bob_entries.remove(&(bob_iface.id(), ALICE_IP)).expect("missing neighbor entry"),
+ bob_iface.id(),
+ ALICE_IP,
+ ALICE_MAC,
+ );
+ // IPv6 entry.
+ let () = assert_stale_entry(
+ bob_entries.remove(&(bob_iface.id(), alice_ipv6)).expect("missing neighbor entry"),
+ bob_iface.id(),
+ alice_ipv6,
+ ALICE_MAC,
+ );
+ assert!(bob_entries.is_empty(), "unexpected neighbors remaining in list: {:?}", bob_entries);
+
+ Ok(())
+}