blob: e2560d30f8b95052bb789a5f5be522fac2708c34 [file] [log] [blame]
// 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 <ddk/protocol/ethernet.h>
#include <fbl/auto_call.h>
#include <fuchsia/hardware/ethernet/cpp/fidl.h>
#include <fuchsia/net/stack/cpp/fidl.h>
#include <fuchsia/netstack/cpp/fidl.h>
#include <lib/fdio/util.h>
#include <lib/fdio/watcher.h>
#include <lib/fidl/cpp/interface_handle.h>
#include <lib/zx/socket.h>
#include <zircon/device/ethertap.h>
#include <zircon/status.h>
#include <zircon/types.h>
#include <string>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include "gtest/gtest.h"
#include "lib/component/cpp/testing/test_util.h"
#include "lib/component/cpp/testing/test_with_environment.h"
namespace {
class NetstackLaunchTest : public component::testing::TestWithEnvironment {};
const char kEthernetDir[] = "/dev/class/ethernet";
const char kTapctl[] = "/dev/misc/tapctl";
const uint8_t kTapMac[] = {0x12, 0x20, 0x30, 0x40, 0x50, 0x60};
zx_status_t CreateEthertap(zx::socket* sock) {
int ctlfd = open(kTapctl, O_RDONLY);
if (ctlfd < 0) {
fprintf(stderr, "could not open %s: %s\n", kTapctl, strerror(errno));
return ZX_ERR_IO;
}
auto closer = fbl::MakeAutoCall([ctlfd]() { close(ctlfd); });
ethertap_ioctl_config_t config = {};
strlcpy(config.name, "netstack_add_eth_test", ETHERTAP_MAX_NAME_LEN);
config.mtu = 1500;
memcpy(config.mac, kTapMac, 6);
ssize_t rc =
ioctl_ethertap_config(ctlfd, &config, sock->reset_and_get_address());
if (rc < 0) {
zx_status_t status = static_cast<zx_status_t>(rc);
fprintf(stderr, "could not configure ethertap device: %s\n",
zx_status_get_string(status));
return status;
}
return ZX_OK;
}
zx_status_t WatchCb(int dirfd, int event, const char* fn, void* cookie) {
if (event != WATCH_EVENT_ADD_FILE) {
return ZX_OK;
}
if (!strcmp(fn, ".") || !strcmp(fn, "..")) {
return ZX_OK;
}
zx::channel svc;
{
int devfd = openat(dirfd, fn, O_RDONLY);
if (devfd < 0) {
return ZX_OK;
}
zx_status_t status =
fdio_get_service_handle(devfd, svc.reset_and_get_address());
if (status != ZX_OK) {
return status;
}
}
::fuchsia::hardware::ethernet::Device_SyncProxy dev(std::move(svc));
// See if this device is our ethertap device
fuchsia::hardware::ethernet::Info info;
zx_status_t status = dev.GetInfo(&info);
if (status != ZX_OK) {
fprintf(stderr, "could not get ethernet info for %s/%s: %s\n", kEthernetDir,
fn, zx_status_get_string(status));
// Return ZX_OK to keep watching for devices.
return ZX_OK;
}
if (!(info.features & fuchsia::hardware::ethernet::INFO_FEATURE_SYNTH)) {
// Not a match, keep looking.
return ZX_OK;
}
// Found it!
// TODO(tkilbourn): this might not be the test device we created; need a
// robust way of getting the name of the tap device to check. Note that
// ioctl_device_get_device_name just returns "ethernet" since that's the child
// of the tap device that we've opened here.
auto svcp = reinterpret_cast<zx_handle_t*>(cookie);
*svcp = dev.proxy().TakeChannel().release();
return ZX_ERR_STOP;
}
zx_status_t OpenEthertapDev(zx::channel* svc) {
if (svc == nullptr) {
return ZX_ERR_INVALID_ARGS;
}
int ethdir = open(kEthernetDir, O_RDONLY);
if (ethdir < 0) {
fprintf(stderr, "could not open %s: %s\n", kEthernetDir, strerror(errno));
return ZX_ERR_IO;
}
zx_status_t status;
status = fdio_watch_directory(
ethdir, WatchCb, zx_deadline_after(ZX_SEC(2)),
reinterpret_cast<void*>(svc->reset_and_get_address()));
if (status == ZX_ERR_STOP) {
return ZX_OK;
} else {
return status;
}
}
TEST_F(NetstackLaunchTest, AddEthernetInterface) {
auto services = CreateServices();
// TODO(NET-1818): parameterize this over multiple netstack implementations
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = "netstack";
launch_info.out = component::testing::CloneFileDescriptor(1);
launch_info.err = component::testing::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()));
zx::socket sock;
zx_status_t status = CreateEthertap(&sock);
ASSERT_EQ(ZX_OK, status) << zx_status_get_string(status);
zx::channel svc;
status = OpenEthertapDev(&svc);
ASSERT_EQ(ZX_OK, status) << zx_status_get_string(status);
fprintf(stderr, "found tap device\n");
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),
fidl::InterfaceHandle<::fuchsia::hardware::ethernet::Device>(
std::move(svc)),
[&](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);
}
list_ifs = true;
});
ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&] { return list_ifs; }, zx::sec(5)));
}
TEST_F(NetstackLaunchTest, DISABLED_AddEthernetDevice) {
auto services = CreateServices();
// TODO(NET-1818): parameterize this over multiple netstack implementations
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = "netstack";
launch_info.out = component::testing::CloneFileDescriptor(1);
launch_info.err = component::testing::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()));
zx::socket sock;
zx_status_t status = CreateEthertap(&sock);
ASSERT_EQ(ZX_OK, status) << zx_status_get_string(status);
zx::channel svc;
status = OpenEthertapDev(&svc);
ASSERT_EQ(ZX_OK, status) << zx_status_get_string(status);
fprintf(stderr, "found tap device\n");
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),
fidl::InterfaceHandle<::fuchsia::hardware::ethernet::Device>(
std::move(svc)),
[&](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);
}
list_ifs = true;
});
ASSERT_TRUE(RunLoopWithTimeoutOrUntil([&] { return list_ifs; }, zx::sec(5)));
}
TEST_F(NetstackLaunchTest, DISABLED_DHCPRequestSent) {
auto services = CreateServices();
// TODO(NET-1818): parameterize this over multiple netstack implementations
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = "netstack";
launch_info.out = component::testing::CloneFileDescriptor(1);
launch_info.err = component::testing::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()));
zx::socket sock;
status = CreateEthertap(&sock);
ASSERT_EQ(ZX_OK, status) << zx_status_get_string(status);
zx::channel svc;
status = OpenEthertapDev(&svc);
ASSERT_EQ(ZX_OK, status) << zx_status_get_string(status);
fprintf(stderr, "found tap device\n");
status = sock.signal_peer(0u, ETHERTAP_SIGNAL_ONLINE);
ASSERT_EQ(ZX_OK, status) << zx_status_get_string(status);
fprintf(stderr, "set ethertap link status online\n");
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);
// 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),
fidl::InterfaceHandle<::fuchsia::hardware::ethernet::Device>(
std::move(svc)),
[&](uint32_t id) {});
// Give zx_channel_write 10ms to enqueue whatever it needs, then run
// until idle (reduces flake rate to zero).
//
// TODO(NET-1967): Figure out why this sleep is required. The call stack in
// AddEthernetDevice is AddEthernetDevice -> ProxyController#Send ->
// Message#Write -> zx_channel_write, none of which are asynchronous.
zx::nanosleep(zx::deadline_after(zx::msec(10)));
RealLoopFixture::RunLoopUntilIdle();
std::byte buf[1500];
size_t attempt_to_read = 1500;
size_t read;
size_t parsed = 0;
zx_signals_t pending = 0;
// Expected to take about ~150ms; we're being conservative to avoid flakes.
status = sock.wait_one(ZX_SOCKET_READABLE | ZX_SOCKET_PEER_CLOSED |
ZX_SOCKET_PEER_WRITE_DISABLED,
zx::clock::get_monotonic() + zx::msec(500), &pending);
ASSERT_EQ(ZX_OK, status) << zx_status_get_string(status);
ASSERT_GT((pending & ZX_SOCKET_READABLE), 0u)
<< "socket was not readable; signals: " << pending;
status = sock.read(0, buf, attempt_to_read, &read);
ASSERT_EQ(ZX_OK, status) << zx_status_get_string(status);
ASSERT_GT(read, (size_t)0);
EXPECT_EQ(read, (size_t)310)
<< "read " << read << " bytes of " << attempt_to_read << " requested\n";
EXPECT_EQ((unsigned int)buf[0], ETHERTAP_MSG_PACKET)
<< "ethertap packet header incorrect";
std::byte* eth = &buf[sizeof(ethertap_socket_header_t)];
parsed += sizeof(ethertap_socket_header_t);
std::byte ethertype = eth[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;
std::byte* ip = &eth[eth_frame_min_hdr_size];
parsed += eth_frame_min_hdr_size;
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;
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);
std::byte* dhcp = &udp[8];
// Assert the DHCP op type is DHCP request.
std::byte dhcp_op_type = dhcp[0];
EXPECT_EQ((int)dhcp_op_type, 0x01);
}
} // namespace