[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(())
+}