[network/tests] add DHCP integration test

...after porting similar tests to Rust.

Also harden netcfg against disappearing devices.

NET-2115 #done

Change-Id: Id7ed5d148d6f2930aae50c6e133c0087b753e8d6
diff --git a/garnet/bin/dhcpd/data/test_server_config.json b/garnet/bin/dhcpd/data/test_server_config.json
index 0308cbf..e7b45b3 100644
--- a/garnet/bin/dhcpd/data/test_server_config.json
+++ b/garnet/bin/dhcpd/data/test_server_config.json
@@ -1,5 +1,5 @@
 {
-    "server_ip": "127.0.0.1",
+    "server_ip": "192.168.0.1",
     "default_lease_time": 86400,
     "subnet_mask": 24,
     "managed_addrs": [
diff --git a/garnet/bin/netcfg/src/main.rs b/garnet/bin/netcfg/src/main.rs
index ed2c8cf..37217a9 100644
--- a/garnet/bin/netcfg/src/main.rs
+++ b/garnet/bin/netcfg/src/main.rs
@@ -248,75 +248,80 @@
                         fidl_fuchsia_hardware_ethernet::DeviceMarker,
                     >::new(client)
                     .into_proxy()?;
-                    let device_info = await!(device.get_info())?;
-                    let device_info: fidl_fuchsia_hardware_ethernet_ext::EthernetInfo =
-                        device_info.into();
 
-                    if device_info.features.is_physical() {
-                        let client = device
-                            .into_channel()
-                            .map_err(|fidl_fuchsia_hardware_ethernet::DeviceProxy { .. }| {
-                                failure::err_msg("failed to convert device proxy into channel")
-                            })?
-                            .into_zx_channel();
+                    if let Ok(device_info) = await!(device.get_info()) {
+                        let device_info: fidl_fuchsia_hardware_ethernet_ext::EthernetInfo =
+                            device_info.into();
 
-                        let name = persisted_interface_config.get_stable_name(
-                            topological_path.clone(), /* TODO(tamird): we can probably do
-                                                       * better with std::borrow::Cow. */
-                            device_info.mac,
-                            device_info.features.contains(
+                        if device_info.features.is_physical() {
+                            let client = device
+                                .into_channel()
+                                .map_err(
+                                    |fidl_fuchsia_hardware_ethernet::DeviceProxy { .. }| {
+                                        failure::err_msg(
+                                            "failed to convert device proxy into channel",
+                                        )
+                                    },
+                                )?
+                                .into_zx_channel();
+
+                            let name = persisted_interface_config.get_stable_name(
+                                topological_path.clone(), /* TODO(tamird): we can probably do
+                                                           * better with std::borrow::Cow. */
+                                device_info.mac,
+                                device_info.features.contains(
+                                    fidl_fuchsia_hardware_ethernet_ext::EthernetFeatures::WLAN,
+                                ),
+                            )?;
+
+                            // Hardcode the interface metric. Eventually this should
+                            // be part of the config file.
+                            let metric: u32 = match device_info.features.contains(
                                 fidl_fuchsia_hardware_ethernet_ext::EthernetFeatures::WLAN,
-                            ),
-                        )?;
+                            ) {
+                                true => INTF_METRIC_WLAN,
+                                false => INTF_METRIC_ETH,
+                            };
+                            let mut derived_interface_config = matchers::config_for_device(
+                                &device_info,
+                                name.to_string(),
+                                &topological_path,
+                                metric,
+                                &default_config_rules,
+                            );
+                            let nic_id = await!(netstack.add_ethernet_device(
+                                &topological_path,
+                                &mut derived_interface_config,
+                                fidl::endpoints::ClientEnd::<
+                                    fidl_fuchsia_hardware_ethernet::DeviceMarker,
+                                >::new(client),
+                            ))
+                            .with_context(|_| {
+                                format!(
+                                    "fidl_netstack::Netstack::add_ethernet_device({})",
+                                    filename.display()
+                                )
+                            })?;
 
-                        // Hardcode the interface metric. Eventually this should
-                        // be part of the config file.
-                        let metric: u32 = match device_info
-                            .features
-                            .contains(fidl_fuchsia_hardware_ethernet_ext::EthernetFeatures::WLAN)
-                        {
-                            true => INTF_METRIC_WLAN,
-                            false => INTF_METRIC_ETH,
-                        };
-                        let mut derived_interface_config = matchers::config_for_device(
-                            &device_info,
-                            name.to_string(),
-                            &topological_path,
-                            metric,
-                            &default_config_rules,
-                        );
-                        let nic_id = await!(netstack.add_ethernet_device(
-                            &topological_path,
-                            &mut derived_interface_config,
-                            fidl::endpoints::ClientEnd::<
-                                fidl_fuchsia_hardware_ethernet::DeviceMarker,
-                            >::new(client),
-                        ))
-                        .with_context(|_| {
-                            format!(
-                                "fidl_netstack::Netstack::add_ethernet_device({})",
-                                filename.display()
-                            )
-                        })?;
+                            await!(match derived_interface_config.ip_address_config {
+                                fidl_fuchsia_netstack::IpAddressConfig::Dhcp(_) => {
+                                    netstack.set_dhcp_client_status(nic_id as u32, true)
+                                }
+                                fidl_fuchsia_netstack::IpAddressConfig::StaticIp(
+                                    fidl_fuchsia_net::Subnet { addr: mut address, prefix_len },
+                                ) => netstack.set_interface_address(
+                                    nic_id as u32,
+                                    &mut address,
+                                    prefix_len
+                                ),
+                            })?;
+                            let () = netstack.set_interface_status(nic_id as u32, true)?;
 
-                        await!(match derived_interface_config.ip_address_config {
-                            fidl_fuchsia_netstack::IpAddressConfig::Dhcp(_) => {
-                                netstack.set_dhcp_client_status(nic_id as u32, true)
-                            }
-                            fidl_fuchsia_netstack::IpAddressConfig::StaticIp(
-                                fidl_fuchsia_net::Subnet { addr: mut address, prefix_len },
-                            ) => netstack.set_interface_address(
-                                nic_id as u32,
-                                &mut address,
-                                prefix_len
-                            ),
-                        })?;
-                        let () = netstack.set_interface_status(nic_id as u32, true)?;
-
-                        // TODO(chunyingw): when netcfg switches to stack.add_ethernet_interface,
-                        // remove casting nic_id to u64.
-                        await!(interface_ids.lock())
-                            .insert(derived_interface_config.name, nic_id as u64);
+                            // TODO(chunyingw): when netcfg switches to stack.add_ethernet_interface,
+                            // remove casting nic_id to u64.
+                            await!(interface_ids.lock())
+                                .insert(derived_interface_config.name, nic_id as u64);
+                        }
                     }
                 }
 
diff --git a/garnet/packages/tests/BUILD.gn b/garnet/packages/tests/BUILD.gn
index 7d22d0f..1073c42 100644
--- a/garnet/packages/tests/BUILD.gn
+++ b/garnet/packages/tests/BUILD.gn
@@ -478,9 +478,19 @@
   ]
 }
 
+group("netstack_integration_tests") {
+  testonly = true
+  public_deps = [
+    "//garnet/bin/dhcpd",
+    "//src/connectivity/network/testing/netemul",
+    "//src/connectivity/network/tests:netstack_integration_tests",
+  ]
+}
+
 group("netstack") {
   testonly = true
   public_deps = [
+    ":netstack_integration_tests",
     "//garnet/packages/prod:netstack",
     "//src/connectivity/network/netstack:netstack_gotests",
     "//src/connectivity/network/netstack:netstack_non_component_gotests",
@@ -489,9 +499,7 @@
     "//src/connectivity/network/netstack/routes:netstack_routes_gotests",
     "//src/connectivity/network/netstack/tests:netstack_manual_tests",
     "//src/connectivity/network/netstack/util:netstack_util_test($host_toolchain)",
-    "//src/connectivity/network/tests:netstack_bsdsocket_c_api_test($host_toolchain)",
     "//src/connectivity/network/tests:netstack_c_api_tests",
-    "//src/connectivity/network/tests:netstack_integration_tests",
     "//src/connectivity/network/tests/test_filter_client",
     "//src/connectivity/network/tests/test_ioctl_client",
     "//src/connectivity/network/tests/test_no_network_client",
diff --git a/src/connectivity/network/tests/BUILD.gn b/src/connectivity/network/tests/BUILD.gn
index 29d04f4..24ecf1c 100644
--- a/src/connectivity/network/tests/BUILD.gn
+++ b/src/connectivity/network/tests/BUILD.gn
@@ -11,7 +11,6 @@
   testonly = true
 
   sources = [
-    "netstack_add_eth_test.cc",
     "netstack_filter_test.cc",
     "netstack_ioctl_test.cc",
     "netstack_no_network_test.cc",
@@ -37,9 +36,16 @@
   edition = "2018"
 
   deps = [
+    "//garnet/public/lib/fidl/rust/fidl",
     "//garnet/public/rust/fuchsia-async",
     "//garnet/public/rust/fuchsia-component",
     "//garnet/public/rust/fuchsia-zircon",
+    "//sdk/fidl/fuchsia.netstack:fuchsia.netstack-rustc",
+    "//sdk/fidl/fuchsia.sys:fuchsia.sys-rustc",
+    "//src/connectivity/network/testing/netemul/lib/fidl:environment-rustc",
+    "//src/connectivity/network/testing/netemul/lib/fidl:network-rustc",
+    "//src/connectivity/network/testing/netemul/lib/fidl:sandbox-rustc",
+    "//third_party/rust_crates:failure",
     "//third_party/rust_crates:futures-preview",
     "//zircon/public/fidl/fuchsia-hardware-ethernet:fuchsia-hardware-ethernet-rustc",
     "//zircon/public/fidl/fuchsia-net:fuchsia-net-rustc",
diff --git a/src/connectivity/network/tests/meta/netstack_fidl_integration_lib_test.cmx b/src/connectivity/network/tests/meta/netstack_fidl_integration_lib_test.cmx
index d2c35390..f9c4c8c 100644
--- a/src/connectivity/network/tests/meta/netstack_fidl_integration_lib_test.cmx
+++ b/src/connectivity/network/tests/meta/netstack_fidl_integration_lib_test.cmx
@@ -2,7 +2,7 @@
     "facets": {
         "fuchsia.test": {
             "injected-services": {
-                "fuchsia.net.stack.Stack": "fuchsia-pkg://fuchsia.com/netstack#meta/netstack.cmx"
+                "fuchsia.netemul.sandbox.Sandbox": "fuchsia-pkg://fuchsia.com/netemul_sandbox#meta/netemul_sandbox.cmx"
             }
         }
     },
@@ -11,7 +11,7 @@
     },
     "sandbox": {
         "services": [
-            "fuchsia.net.stack.Stack"
+            "fuchsia.netemul.sandbox.Sandbox"
         ]
     }
 }
diff --git a/src/connectivity/network/tests/netstack_add_eth_test.cc b/src/connectivity/network/tests/netstack_add_eth_test.cc
deleted file mode 100644
index 1b1b659..0000000
--- a/src/connectivity/network/tests/netstack_add_eth_test.cc
+++ /dev/null
@@ -1,262 +0,0 @@
-// Copyright 2018 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.
-
-#include <fuchsia/hardware/ethernet/cpp/fidl.h>
-#include <fuchsia/net/stack/cpp/fidl.h>
-#include <fuchsia/netstack/cpp/fidl.h>
-#include <lib/sys/cpp/file_descriptor.h>
-#include <lib/sys/cpp/testing/test_with_environment.h>
-#include <src/connectivity/network/testing/netemul/lib/network/ethernet_client.h>
-#include <src/connectivity/network/testing/netemul/lib/network/ethertap_client.h>
-#include <zircon/status.h>
-
-#include "gtest/gtest.h"
-
-namespace {
-class NetstackLaunchTest : public sys::testing::TestWithEnvironment {};
-
-const char kNetstackUrl[] =
-    "fuchsia-pkg://fuchsia.com/netstack#meta/netstack.cmx";
-
-TEST_F(NetstackLaunchTest, AddEthernetInterface) {
-  auto services = CreateServices();
-
-  // TODO(NET-1818): parameterize this over multiple netstack implementations
-  fuchsia::sys::LaunchInfo launch_info;
-  launch_info.url = kNetstackUrl;
-  launch_info.out = sys::CloneFileDescriptor(1);
-  launch_info.err = sys::CloneFileDescriptor(2);
-  services->AddServiceWithLaunchInfo(std::move(launch_info),
-                                     fuchsia::net::stack::Stack::Name_);
-
-  auto env = CreateNewEnclosingEnvironment("NetstackLaunchTest_AddEth",
-                                           std::move(services));
-  ASSERT_TRUE(WaitForEnclosingEnvToStart(env.get()));
-
-  auto eth_config = netemul::EthertapConfig("AddEthernetInterface");
-  auto tap = netemul::EthertapClient::Create(eth_config);
-  ASSERT_TRUE(tap) << "failed to create ethertap device";
-
-  netemul::EthernetClientFactory eth_factory;
-  auto eth = eth_factory.RetrieveWithMAC(eth_config.tap_cfg.mac);
-  ASSERT_TRUE(eth) << "failed to retrieve ethernet client";
-
-  bool list_ifs = false;
-  fuchsia::net::stack::StackPtr stack;
-  env->ConnectToService(stack.NewRequest());
-  stack->ListInterfaces(
-      [&](::std::vector<::fuchsia::net::stack::InterfaceInfo> interfaces) {
-        for (const auto& iface : interfaces) {
-          ASSERT_TRUE(iface.properties.features &
-                      ::fuchsia::hardware::ethernet::INFO_FEATURE_LOOPBACK);
-        }
-        list_ifs = true;
-      });
-  ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&] { return list_ifs; }, zx::sec(5)));
-
-  uint64_t eth_id = 0;
-  fidl::StringPtr topo_path = "/fake/device";
-  stack->AddEthernetInterface(
-      std::move(topo_path), std::move(eth->device()),
-      [&](std::unique_ptr<::fuchsia::net::stack::Error> err, uint64_t id) {
-        if (err != nullptr) {
-          fprintf(stderr, "error adding ethernet interface: %u\n",
-                  static_cast<uint32_t>(err->type));
-        } else {
-          eth_id = id;
-        }
-      });
-  ASSERT_TRUE(
-      RunLoopWithTimeoutOrUntil([&] { return eth_id > 0; }, zx::sec(5)));
-
-  list_ifs = false;
-  stack->ListInterfaces(
-      [&](::std::vector<::fuchsia::net::stack::InterfaceInfo> interfaces) {
-        for (const auto& iface : interfaces) {
-          if (iface.properties.features &
-              ::fuchsia::hardware::ethernet::INFO_FEATURE_LOOPBACK) {
-            continue;
-          }
-          ASSERT_EQ(eth_id, iface.id);
-          // tap device is created with link down, so we expect physical status
-          // to be DOWN.
-          EXPECT_EQ(iface.properties.physicalStatus,
-                    fuchsia::net::stack::PhysicalStatus::DOWN);
-        }
-        list_ifs = true;
-      });
-  ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&] { return list_ifs; }, zx::sec(5)));
-}
-
-TEST_F(NetstackLaunchTest, AddEthernetDevice) {
-  auto services = CreateServices();
-
-  // TODO(NET-1818): parameterize this over multiple netstack implementations
-  fuchsia::sys::LaunchInfo launch_info;
-  launch_info.url = kNetstackUrl;
-  launch_info.out = sys::CloneFileDescriptor(1);
-  launch_info.err = sys::CloneFileDescriptor(2);
-  services->AddServiceWithLaunchInfo(std::move(launch_info),
-                                     fuchsia::netstack::Netstack::Name_);
-
-  auto env = CreateNewEnclosingEnvironment("NetstackLaunchTest_AddEth",
-                                           std::move(services));
-  ASSERT_TRUE(WaitForEnclosingEnvToStart(env.get()));
-
-  auto eth_config = netemul::EthertapConfig("AddEthernetDevice");
-  auto tap = netemul::EthertapClient::Create(eth_config);
-  ASSERT_TRUE(tap) << "failed to create ethertap device";
-
-  netemul::EthernetClientFactory eth_factory;
-  auto eth = eth_factory.RetrieveWithMAC(eth_config.tap_cfg.mac);
-  ASSERT_TRUE(eth) << "failed to retrieve ethernet client";
-
-  bool list_ifs = false;
-  fuchsia::netstack::NetstackPtr netstack;
-  env->ConnectToService(netstack.NewRequest());
-  fidl::StringPtr topo_path = "/fake/device";
-  fidl::StringPtr interface_name = "en0";
-  fuchsia::netstack::InterfaceConfig config =
-      fuchsia::netstack::InterfaceConfig{};
-  config.name = interface_name;
-  config.ip_address_config.set_dhcp(true);
-  netstack->GetInterfaces(
-      [&](::std::vector<::fuchsia::netstack::NetInterface> interfaces) {
-        for (const auto& iface : interfaces) {
-          ASSERT_TRUE(iface.features &
-                      ::fuchsia::hardware::ethernet::INFO_FEATURE_LOOPBACK);
-        }
-        list_ifs = true;
-      });
-  ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&] { return list_ifs; }, zx::sec(5)));
-
-  uint32_t eth_id = 0;
-  netstack->AddEthernetDevice(std::move(topo_path), std::move(config),
-                              std::move(eth->device()),
-                              [&](uint32_t id) { eth_id = id; });
-  ASSERT_TRUE(
-      RunLoopWithTimeoutOrUntil([&] { return eth_id > 0; }, zx::sec(5)));
-
-  list_ifs = false;
-  netstack->GetInterfaces(
-      [&](::std::vector<::fuchsia::netstack::NetInterface> interfaces) {
-        for (const auto& iface : interfaces) {
-          if (iface.features &
-              ::fuchsia::hardware::ethernet::INFO_FEATURE_LOOPBACK) {
-            continue;
-          }
-          ASSERT_EQ(eth_id, iface.id);
-          // tap device is created with link down, so we expect physical status
-          // to be DOWN.
-          EXPECT_EQ(iface.flags & fuchsia::netstack::NetInterfaceFlagUp, 0u);
-        }
-        list_ifs = true;
-      });
-  ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&] { return list_ifs; }, zx::sec(5)));
-}
-
-TEST_F(NetstackLaunchTest, DHCPRequestSent) {
-  auto services = CreateServices();
-
-  // TODO(NET-1818): parameterize this over multiple netstack implementations
-  fuchsia::sys::LaunchInfo launch_info;
-  launch_info.url = kNetstackUrl;
-  launch_info.out = sys::CloneFileDescriptor(1);
-  launch_info.err = sys::CloneFileDescriptor(2);
-  zx_status_t status = services->AddServiceWithLaunchInfo(
-      std::move(launch_info), fuchsia::netstack::Netstack::Name_);
-  ASSERT_EQ(status, ZX_OK) << zx_status_get_string(status);
-
-  auto env = CreateNewEnclosingEnvironment("NetstackDHCPTest_RequestSent",
-                                           std::move(services));
-  ASSERT_TRUE(WaitForEnclosingEnvToStart(env.get()));
-
-  auto eth_config = netemul::EthertapConfig("DHCPRequestSent");
-  auto tap = netemul::EthertapClient::Create(eth_config);
-  ASSERT_TRUE(tap) << "failed to create ethertap device";
-  tap->SetLinkUp(true);
-
-  netemul::EthernetClientFactory eth_factory;
-  auto eth = eth_factory.RetrieveWithMAC(eth_config.tap_cfg.mac);
-  ASSERT_TRUE(eth) << "failed to retrieve ethernet client";
-
-  fuchsia::netstack::NetstackPtr netstack;
-  env->ConnectToService(netstack.NewRequest());
-  fidl::StringPtr topo_path = "/fake/device";
-
-  fidl::StringPtr interface_name = "dhcp_test_interface";
-  fuchsia::netstack::InterfaceConfig config =
-      fuchsia::netstack::InterfaceConfig{};
-  config.name = interface_name;
-  config.ip_address_config.set_dhcp(true);
-
-  bool data_callback_run = false;
-  auto f = [&data_callback_run](std::vector<uint8_t> data) {
-    auto len = data.size();
-    const std::byte* ethbuf = reinterpret_cast<const std::byte*>(&data[0]);
-    size_t expected_len = 302;
-    size_t parsed = 0;
-
-    EXPECT_EQ(len, (size_t)expected_len)
-        << "got " << len << " bytes of " << expected_len << " requested\n";
-
-    const std::byte ethertype = ethbuf[12];
-    EXPECT_EQ((int)ethertype, 0x08);
-
-    // TODO(stijlist): add an ETH_FRAME_MIN_HDR_SIZE to ddk's ethernet.h
-    size_t eth_frame_min_hdr_size = 14;
-    const std::byte* ip = &ethbuf[eth_frame_min_hdr_size];
-    parsed += eth_frame_min_hdr_size;
-    const std::byte protocol_number = ip[9];
-    EXPECT_EQ((int)protocol_number, 17);
-
-    size_t ihl = (size_t)(ip[0] & (std::byte)0x0f);
-    size_t ip_bytes = (ihl * 32u) / 8u;
-
-    const std::byte* udp = &ip[ip_bytes];
-    parsed += ip_bytes;
-
-    uint16_t src_port = (uint16_t)udp[0] << 8 | (uint8_t)udp[1];
-    uint16_t dst_port = (uint16_t)udp[2] << 8 | (uint8_t)udp[3];
-
-    // DHCP requests from netstack should come from port 68 (DHCP client) to
-    // port 67 (DHCP server).
-    EXPECT_EQ(src_port, 68u);
-    EXPECT_EQ(dst_port, 67u);
-
-    const std::byte* dhcp = &udp[8];
-    // Assert the DHCP op type is DHCP request.
-    const std::byte dhcp_op_type = dhcp[0];
-    EXPECT_EQ((int)dhcp_op_type, 0x01);
-
-    data_callback_run = true;
-  };
-
-  tap->SetPacketCallback(f);
-
-  uint32_t nicid = 0;
-  // TODO(NET-1864): migrate to fuchsia.net.stack.AddEthernetInterface when we
-  // migrate netcfg to use AddEthernetInterface.
-  netstack->AddEthernetDevice(std::move(topo_path), std::move(config),
-                              std::move(eth->device()),
-                              [&nicid](uint32_t id) { nicid = id; });
-
-  ASSERT_TRUE(
-      RunLoopWithTimeoutOrUntil([&] { return nicid != 0; }, zx::sec(5)));
-
-  netstack->SetInterfaceStatus(nicid, true);
-  fuchsia::netstack::Status net_status =
-      fuchsia::netstack::Status::UNKNOWN_ERROR;
-  netstack->SetDhcpClientStatus(
-      nicid, true, [&net_status](fuchsia::netstack::NetErr result) {
-        net_status = result.status;
-      });
-
-  ASSERT_TRUE(RunLoopWithTimeoutOrUntil(
-      [&] { return net_status == fuchsia::netstack::Status::OK; }, zx::sec(5)));
-
-  ASSERT_TRUE(RunLoopWithTimeoutOrUntil(
-      [&data_callback_run] { return data_callback_run; }, zx::sec(5)));
-}
-}  // namespace
diff --git a/src/connectivity/network/tests/src/lib.rs b/src/connectivity/network/tests/src/lib.rs
index ce1446a..cd807cf 100644
--- a/src/connectivity/network/tests/src/lib.rs
+++ b/src/connectivity/network/tests/src/lib.rs
@@ -2,70 +2,416 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#![cfg(test)]
 #![deny(warnings)]
 #![feature(async_await, await_macro, futures_api)]
 
-#[test]
-fn add_interface_address_not_found() {
-    let mut executor = fuchsia_async::Executor::new().expect("failed to create an executor");
-    let stack =
-        fuchsia_component::client::connect_to_service::<fidl_fuchsia_net_stack::StackMarker>()
-            .expect("failed to connect to stack");
-    let () = executor.run_singlethreaded(
-        async {
-            let interfaces = await!(stack.list_interfaces()).expect("failed to list interfaces");
-            let max_id = interfaces.iter().map(|interface| interface.id).max().unwrap_or(0);
-            let mut interface_address = fidl_fuchsia_net_stack::InterfaceAddress {
-                ip_address: fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address {
-                    addr: [0, 0, 0, 0],
-                }),
-                prefix_len: 0,
-            };
-            let error = await!(stack.add_interface_address(max_id + 1, &mut interface_address,))
-                .expect("failed to add interface address")
-                .expect("failed to get add interface address response");
-            assert_eq!(
-                error.as_ref(),
-                &fidl_fuchsia_net_stack::Error {
-                    type_: fidl_fuchsia_net_stack::ErrorType::NotFound
-                }
-            )
-        },
-    );
+use failure::ResultExt;
+
+type Result = std::result::Result<(), failure::Error>;
+
+fn connect_to_service<S: fidl::endpoints::ServiceMarker>(
+    managed_environment: &fidl_fuchsia_netemul_environment::ManagedEnvironmentProxy,
+) -> std::result::Result<S::Proxy, failure::Error> {
+    let (proxy, server) = fuchsia_zircon::Channel::create()?;
+    let () = managed_environment.connect_to_service(S::NAME, server)?;
+    let proxy = fuchsia_async::Channel::from_channel(proxy)?;
+    Ok(<S::Proxy as fidl::endpoints::Proxy>::from_channel(proxy))
 }
 
-#[test]
-fn disable_interface_loopback() {
-    let mut executor = fuchsia_async::Executor::new().expect("failed to create an executor");
-    let stack =
-        fuchsia_component::client::connect_to_service::<fidl_fuchsia_net_stack::StackMarker>()
-            .expect("failed to connect to stack");
-    let () = executor.run_singlethreaded(
-        async {
-            let interfaces = await!(stack.list_interfaces()).expect("failed to list interfaces");
-            let localhost = interfaces
-                .iter()
-                .find(|interface| {
-                    interface.properties.features
-                        & fidl_fuchsia_hardware_ethernet::INFO_FEATURE_LOOPBACK
-                        != 0
-                })
-                .expect("failed to find loopback interface");
-            assert_eq!(
-                localhost.properties.administrative_status,
-                fidl_fuchsia_net_stack::AdministrativeStatus::Enabled
-            );
-            assert_eq!(
-                await!(stack.disable_interface(localhost.id)).expect("failed to disable interface"),
-                None
-            );
-            let (info, error) = await!(stack.get_interface_info(localhost.id))
-                .expect("failed to get interface info");
-            assert_eq!(error, None);
-            assert_eq!(
-                info.expect("expected interface info to be present").properties.administrative_status,
-                fidl_fuchsia_net_stack::AdministrativeStatus::Disabled
-            );
+fn get_network_context(
+    sandbox: &fidl_fuchsia_netemul_sandbox::SandboxProxy,
+) -> std::result::Result<fidl_fuchsia_netemul_network::NetworkContextProxy, failure::Error> {
+    let (client, server) =
+        fidl::endpoints::create_proxy::<fidl_fuchsia_netemul_network::NetworkContextMarker>()
+            .context("failed to create network context proxy")?;
+    let () = sandbox.get_network_context(server).context("failed to get network context")?;
+    Ok(client)
+}
+
+fn get_endpoint_manager(
+    network_context: &fidl_fuchsia_netemul_network::NetworkContextProxy,
+) -> std::result::Result<fidl_fuchsia_netemul_network::EndpointManagerProxy, failure::Error> {
+    let (client, server) =
+        fidl::endpoints::create_proxy::<fidl_fuchsia_netemul_network::EndpointManagerMarker>()
+            .context("failed to create endpoint manager proxy")?;
+    let () =
+        network_context.get_endpoint_manager(server).context("failed to get endpoint manager")?;
+    Ok(client)
+}
+
+async fn create_endpoint<'a>(
+    name: &'static str,
+    endpoint_manager: &'a fidl_fuchsia_netemul_network::EndpointManagerProxy,
+) -> std::result::Result<fidl_fuchsia_netemul_network::EndpointProxy, failure::Error> {
+    let (status, endpoint) = await!(endpoint_manager.create_endpoint(
+        name,
+        &mut fidl_fuchsia_netemul_network::EndpointConfig {
+            mtu: 1500,
+            mac: None,
+            backing: fidl_fuchsia_netemul_network::EndpointBacking::Ethertap,
         },
+    ))
+    .context("failed to create endpoint")?;
+    let () = fuchsia_zircon::Status::ok(status).context("failed to create endpoint")?;
+    let endpoint = endpoint
+        .ok_or(failure::err_msg("failed to create endpoint"))?
+        .into_proxy()
+        .context("failed to get endpoint proxy")?;
+    Ok(endpoint)
+}
+
+fn create_netstack_environment(
+    sandbox: &fidl_fuchsia_netemul_sandbox::SandboxProxy,
+    name: String,
+) -> std::result::Result<fidl_fuchsia_netemul_environment::ManagedEnvironmentProxy, failure::Error>
+{
+    let (client, server) = fidl::endpoints::create_proxy::<
+        fidl_fuchsia_netemul_environment::ManagedEnvironmentMarker,
+    >()
+    .context("failed to create managed environment proxy")?;
+    let () = sandbox
+            .create_environment(
+                server,
+                fidl_fuchsia_netemul_environment::EnvironmentOptions {
+                    name: Some(name),
+                    services:  Some([
+                    <fidl_fuchsia_netstack::NetstackMarker as fidl::endpoints::ServiceMarker>::NAME,
+                    <fidl_fuchsia_net::SocketProviderMarker as fidl::endpoints::ServiceMarker>::NAME,
+                    <fidl_fuchsia_net_stack::StackMarker as fidl::endpoints::ServiceMarker>::NAME,
+                ]
+                    // TODO(tamird): use into_iter after
+                    // https://github.com/rust-lang/rust/issues/25725.
+                    .iter()
+                    .map(std::ops::Deref::deref)
+                    .map(str::to_string)
+            .map(|name| fidl_fuchsia_netemul_environment::LaunchService {
+                name,
+                url: fuchsia_component::fuchsia_single_component_package_url!("netstack")
+                    .to_string(),
+                arguments: Some(vec!["--sniff".to_string()]),
+            })
+            .collect()),
+                    devices: None,
+                    inherit_parent_launch_services: None,
+                    logger_options: Some(fidl_fuchsia_netemul_environment::LoggerOptions {
+                        enabled: Some(true),
+                        klogs_enabled: None,
+                        filter_options: None,
+                        syslog_output: Some(true),
+                    }),
+                },
+            )
+            .context("failed to create environment")?;
+    Ok(client)
+}
+
+async fn with_netstack_and_device<F, T, S>(name: &'static str, async_fn: T) -> Result
+where
+    F: futures::Future<Output = Result>,
+    T: FnOnce(
+        S::Proxy,
+        fidl::endpoints::ClientEnd<fidl_fuchsia_hardware_ethernet::DeviceMarker>,
+    ) -> F,
+    S: fidl::endpoints::ServiceMarker,
+{
+    let sandbox = fuchsia_component::client::connect_to_service::<
+        fidl_fuchsia_netemul_sandbox::SandboxMarker,
+    >()
+    .context("failed to connect to sandbox")?;
+    let network_context = get_network_context(&sandbox).context("failed to get network context")?;
+    let endpoint_manager =
+        get_endpoint_manager(&network_context).context("failed to get endpoint manager")?;
+    let endpoint =
+        await!(create_endpoint(name, &endpoint_manager)).context("failed to create endpoint")?;
+    let device = await!(endpoint.get_ethernet_device()).context("failed to get ethernet device")?;
+    let managed_environment = create_netstack_environment(&sandbox, name.to_string())
+        .context("failed to create netstack environment")?;
+    let netstack_proxy =
+        connect_to_service::<S>(&managed_environment).context("failed to connect to netstack")?;
+    await!(async_fn(netstack_proxy, device))
+}
+
+#[fuchsia_async::run_singlethreaded(test)]
+async fn add_ethernet_device() -> Result {
+    let name = stringify!(add_ethernet_device);
+
+    await!(with_netstack_and_device::<_, _, fidl_fuchsia_netstack::NetstackMarker>(
+        name,
+        async move |netstack, device| -> Result {
+            let id = await!(netstack.add_ethernet_device(
+                name,
+                &mut fidl_fuchsia_netstack::InterfaceConfig {
+                    name: name.to_string(),
+                    metric: 0,
+                    ip_address_config: fidl_fuchsia_netstack::IpAddressConfig::Dhcp(true),
+                },
+                device,
+            ))
+            .context("failed to add ethernet device")?;
+            let interface = await!(netstack.get_interfaces2())
+                .context("failed to get interfaces")?
+                .into_iter()
+                .find(|interface| interface.id == id)
+                .ok_or(failure::err_msg("failed to find added ethernet device"))?;
+            assert_eq!(
+                interface.features & fidl_fuchsia_hardware_ethernet::INFO_FEATURE_LOOPBACK,
+                0
+            );
+            assert_eq!(interface.flags & fidl_fuchsia_netstack::NET_INTERFACE_FLAG_UP, 0,);
+            Ok(())
+        },
+    ))
+}
+
+#[fuchsia_async::run_singlethreaded(test)]
+async fn add_ethernet_interface() -> Result {
+    let name = stringify!(add_ethernet_interface);
+
+    await!(with_netstack_and_device::<_, _, fidl_fuchsia_net_stack::StackMarker>(
+        name,
+        async move |stack, device| -> Result {
+            let (error, id) = await!(stack.add_ethernet_interface(name, device))
+                .context("failed to add ethernet interface")?;
+            assert_eq!(error, None);
+            let interface = await!(stack.list_interfaces())
+                .context("failed to list interfaces")?
+                .into_iter()
+                .find(|interface| interface.id == id)
+                .ok_or(failure::err_msg("failed to find added ethernet interface"))?;
+            assert_eq!(
+                interface.properties.features
+                    & fidl_fuchsia_hardware_ethernet::INFO_FEATURE_LOOPBACK,
+                0
+            );
+            assert_eq!(
+                interface.properties.physical_status,
+                fidl_fuchsia_net_stack::PhysicalStatus::Down
+            );
+            Ok(())
+        },
+    ))
+}
+
+#[fuchsia_async::run_singlethreaded(test)]
+async fn add_interface_address_not_found() -> Result {
+    let name = stringify!(add_interface_address_not_found);
+
+    let sandbox = fuchsia_component::client::connect_to_service::<
+        fidl_fuchsia_netemul_sandbox::SandboxMarker,
+    >()
+    .context("failed to connect to sandbox")?;
+    let managed_environment = create_netstack_environment(&sandbox, name.to_string())
+        .context("failed to create netstack environment")?;
+    let stack = connect_to_service::<fidl_fuchsia_net_stack::StackMarker>(&managed_environment)
+        .context("failed to connect to netstack")?;
+    let interfaces = await!(stack.list_interfaces()).context("failed to list interfaces")?;
+    let max_id = interfaces.iter().map(|interface| interface.id).max().unwrap_or(0);
+    let mut interface_address = fidl_fuchsia_net_stack::InterfaceAddress {
+        ip_address: fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address {
+            addr: [0, 0, 0, 0],
+        }),
+        prefix_len: 0,
+    };
+    let error = await!(stack.add_interface_address(max_id + 1, &mut interface_address,))
+        .context("failed to add interface address")?
+        .ok_or(failure::err_msg("failed to get add interface address response"))?;
+    assert_eq!(
+        error.as_ref(),
+        &fidl_fuchsia_net_stack::Error { type_: fidl_fuchsia_net_stack::ErrorType::NotFound }
     );
+    Ok(())
+}
+
+#[fuchsia_async::run_singlethreaded(test)]
+async fn disable_interface_loopback() -> Result {
+    let name = stringify!(disable_interface_loopback);
+
+    let sandbox = fuchsia_component::client::connect_to_service::<
+        fidl_fuchsia_netemul_sandbox::SandboxMarker,
+    >()
+    .context("failed to connect to sandbox")?;
+    let managed_environment = create_netstack_environment(&sandbox, name.to_string())
+        .context("failed to create netstack environment")?;
+    let stack = connect_to_service::<fidl_fuchsia_net_stack::StackMarker>(&managed_environment)
+        .context("failed to connect to netstack")?;
+    let interfaces = await!(stack.list_interfaces()).context("failed to list interfaces")?;
+    let localhost = interfaces
+        .iter()
+        .find(|interface| {
+            interface.properties.features & fidl_fuchsia_hardware_ethernet::INFO_FEATURE_LOOPBACK
+                != 0
+        })
+        .ok_or(failure::err_msg("failed to find loopback interface"))?;
+    assert_eq!(
+        localhost.properties.administrative_status,
+        fidl_fuchsia_net_stack::AdministrativeStatus::Enabled
+    );
+    assert_eq!(
+        await!(stack.disable_interface(localhost.id)).context("failed to disable interface")?,
+        None
+    );
+    let (info, error) =
+        await!(stack.get_interface_info(localhost.id)).context("failed to get interface info")?;
+    assert_eq!(error, None);
+    assert_eq!(
+        info.ok_or(failure::err_msg("expected interface info to be present"))?
+            .properties
+            .administrative_status,
+        fidl_fuchsia_net_stack::AdministrativeStatus::Disabled
+    );
+    Ok(())
+}
+
+// TODO(tamird): could this be done with a single stack and bridged interfaces?
+#[fuchsia_async::run_singlethreaded(test)]
+async fn acquire_dhcp() -> Result {
+    let name = stringify!(acquire_dhcp);
+
+    let sandbox = fuchsia_component::client::connect_to_service::<
+        fidl_fuchsia_netemul_sandbox::SandboxMarker,
+    >()
+    .context("failed to connect to sandbox")?;
+    let network_context = get_network_context(&sandbox).context("failed to get network context")?;
+    let endpoint_manager =
+        get_endpoint_manager(&network_context).context("failed to get endpoint manager")?;
+    let server_environment = create_netstack_environment(&sandbox, format!("{}_server", name))
+        .context("failed to create server environment")?;
+    let server_endpoint_name = "server";
+    let server_endpoint = await!(create_endpoint(server_endpoint_name, &endpoint_manager))
+        .context("failed to create endpoint")?;
+    let () =
+        await!(server_endpoint.set_link_up(true)).context("failed to start server endpoint")?;
+    {
+        let server_device = await!(server_endpoint.get_ethernet_device())
+            .context("failed to get server ethernet device")?;
+        let server_stack =
+            connect_to_service::<fidl_fuchsia_net_stack::StackMarker>(&server_environment)
+                .context("failed to connect to server stack")?;
+        let (error, id) = await!(server_stack.add_ethernet_interface(name, server_device))
+            .context("failed to add server ethernet interface")?;
+        assert_eq!(error, None);
+        let error = await!(server_stack.add_interface_address(
+            id,
+            &mut fidl_fuchsia_net_stack::InterfaceAddress {
+                ip_address: fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address {
+                    addr: [192, 168, 0, 1]
+                }),
+                prefix_len: 24,
+            }
+        ))
+        .context("failed to add interface address")?;
+        assert_eq!(error, None);
+        let error = await!(server_stack.enable_interface(id))
+            .context("failed to enable server interface")?;
+        assert_eq!(error, None);
+    }
+    let launcher = {
+        let (client, server) = fidl::endpoints::create_proxy::<fidl_fuchsia_sys::LauncherMarker>()
+            .context("failed to create launcher proxy")?;
+        let () = server_environment.get_launcher(server).context("failed to get launcher")?;
+        client
+    };
+    let dhcpd = fuchsia_component::client::launch(
+        &launcher,
+        fuchsia_component::fuchsia_single_component_package_url!("dhcpd").to_string(),
+        None,
+    )
+    .context("failed to start dhcpd")?;
+    let client_environment = create_netstack_environment(&sandbox, format!("{}_client", name))
+        .context("failed to create client environment")?;
+    let client_endpoint_name = "client";
+    let client_endpoint = await!(create_endpoint(client_endpoint_name, &endpoint_manager))
+        .context("failed to create endpoint")?;
+    let () =
+        await!(client_endpoint.set_link_up(true)).context("failed to start client endpoint")?;
+
+    let network_manager = {
+        let (client, server) =
+            fidl::endpoints::create_proxy::<fidl_fuchsia_netemul_network::NetworkManagerMarker>()
+                .context("failed to create network manager proxy")?;
+        let () =
+            network_context.get_network_manager(server).context("failed to get network manager")?;
+        client
+    };
+
+    let (status, network) = await!(network_manager.create_network(
+        name,
+        fidl_fuchsia_netemul_network::NetworkConfig {
+            latency: None,
+            packet_loss: None,
+            reorder: None,
+        },
+    ))
+    .context("failed to create network")?;
+    let network = network
+        .ok_or(failure::err_msg("failed to create network"))?
+        .into_proxy()
+        .context("failed to get network proxy")?;
+    let () = fuchsia_zircon::Status::ok(status).context("failed to create network")?;
+    let status = await!(network.attach_endpoint(server_endpoint_name))
+        .context("failed to attach server endpoint")?;
+    let () = fuchsia_zircon::Status::ok(status).context("failed to attach server endpoint")?;
+    let status = await!(network.attach_endpoint(client_endpoint_name))
+        .context("failed to attach client endpoint")?;
+    let () = fuchsia_zircon::Status::ok(status).context("failed to attach client endpoint")?;
+
+    {
+        let client_device = await!(client_endpoint.get_ethernet_device())
+            .context("failed to get client ethernet device")?;
+        let client_stack =
+            connect_to_service::<fidl_fuchsia_net_stack::StackMarker>(&client_environment)
+                .context("failed to connect to client stack")?;
+        let (error, id) = await!(client_stack.add_ethernet_interface(name, client_device))
+            .context("failed to add client ethernet interface")?;
+        assert_eq!(error, None);
+        let error = await!(client_stack.enable_interface(id))
+            .context("failed to enable client interface")?;
+        assert_eq!(error, None);
+        let client_netstack =
+            connect_to_service::<fidl_fuchsia_netstack::NetstackMarker>(&client_environment)
+                .context("failed to connect to client netstack")?;
+        let error = await!(client_netstack.set_dhcp_client_status(id as u32, true))
+            .context("failed to set DHCP client status")?;
+        assert_eq!(error.status, fidl_fuchsia_netstack::Status::Ok, "{}", error.message);
+
+        let mut address_change_stream = futures::TryStreamExt::try_filter_map(
+            client_netstack.take_event_stream(),
+            |fidl_fuchsia_netstack::NetstackEvent::OnInterfacesChanged { interfaces }| {
+                futures::future::ok(
+                    interfaces.into_iter().find(|interface| interface.id as u64 == id).and_then(
+                        |interface| match interface.addr {
+                            fidl_fuchsia_net::IpAddress::Ipv4(fidl_fuchsia_net::Ipv4Address {
+                                addr,
+                            }) => {
+                                if addr == std::net::Ipv4Addr::UNSPECIFIED.octets() {
+                                    None
+                                } else {
+                                    Some(interface.addr)
+                                }
+                            }
+                            fidl_fuchsia_net::IpAddress::Ipv6(fidl_fuchsia_net::Ipv6Address {
+                                ..
+                            }) => None,
+                        },
+                    ),
+                )
+            },
+        );
+        let address_change = futures::StreamExt::next(&mut address_change_stream);
+        let address_change = fuchsia_async::TimeoutExt::on_timeout(
+            address_change,
+            // Netstack's DHCP client retries every 3 seconds. At the time of writing, dhcpd loses
+            // the race here and only starts after the first request from the DHCP client, which
+            // results in a 3 second toll. This test typically takes ~4.5 seconds; we apply a large
+            // multiple to be safe.
+            fuchsia_zircon::Time::after(fuchsia_zircon::Duration::from_seconds(60)),
+            || None,
+        );
+        let _: fidl_fuchsia_net::IpAddress = await!(address_change)
+            .ok_or(failure::err_msg("failed to observe DHCP acquisition"))?
+            .context("failed to observe DHCP acquisition")?;
+    }
+    drop(dhcpd);
+    Ok(())
 }