| // 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. |
| |
| #include "src/connectivity/lib/network-device/cpp/network_device_client.h" |
| |
| #include <fidl/fuchsia.net.tun/cpp/wire.h> |
| #include <lib/component/incoming/cpp/protocol.h> |
| #include <lib/fpromise/bridge.h> |
| #include <lib/zx/time.h> |
| #include <zircon/status.h> |
| |
| #include <memory> |
| #include <unordered_set> |
| |
| #include <fbl/unique_fd.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| |
| #include "src/lib/testing/loop_fixture/real_loop_fixture.h" |
| #include "src/lib/testing/predicates/status.h" |
| |
| namespace { |
| |
| // Enable timeouts only to test things locally, committed code should not use timeouts. |
| constexpr zx::duration kTimeout = zx::duration::infinite(); |
| |
| namespace netdev = fuchsia_hardware_network; |
| namespace tun = fuchsia_net_tun; |
| using NetworkDeviceClient = network::client::NetworkDeviceClient; |
| |
| using ::testing::ContainerEq; |
| |
| template <class T> |
| class TestEventHandler : public fidl::WireAsyncEventHandler<T> { |
| public: |
| explicit TestEventHandler(const char* name) : name_(name) {} |
| |
| void on_fidl_error(fidl::UnbindInfo info) override { |
| FAIL() << "Lost connection to " << name_ << ": " << info; |
| } |
| |
| private: |
| const char* name_; |
| }; |
| |
| class NetDeviceTest : public gtest::RealLoopFixture { |
| public: |
| // Use a non zero number to prevent default memory initialization from hiding bugs. |
| static constexpr uint8_t kPortId = 4; |
| NetDeviceTest() : gtest::RealLoopFixture() {} |
| |
| bool RunLoopUntilOrFailure(fit::function<bool()> f) { |
| return RunLoopWithTimeoutOrUntil([f = std::move(f)] { return HasFailure() || f(); }, kTimeout, |
| zx::duration::infinite()); |
| } |
| |
| tun::wire::DeviceConfig DefaultDeviceConfig() { |
| tun::wire::DeviceConfig config(alloc_); |
| config.set_blocking(true); |
| return config; |
| } |
| |
| tun::wire::DevicePairConfig DefaultPairConfig() { |
| tun::wire::DevicePairConfig config(alloc_); |
| return config; |
| } |
| |
| tun::wire::BasePortConfig DefaultBasePortConfig() { |
| tun::wire::BasePortConfig config(alloc_); |
| config.set_id(kPortId); |
| config.set_mtu(1500); |
| const netdev::wire::FrameType rx_types[] = { |
| netdev::wire::FrameType::kEthernet, |
| }; |
| fidl::VectorView<netdev::wire::FrameType> rx_types_view(alloc_, std::size(rx_types)); |
| std::copy(std::begin(rx_types), std::end(rx_types), rx_types_view.data()); |
| const netdev::wire::FrameTypeSupport tx_types[] = { |
| netdev::wire::FrameTypeSupport{ |
| .type = netdev::wire::FrameType::kEthernet, |
| .features = netdev::wire::kFrameFeaturesRaw, |
| }, |
| }; |
| fidl::VectorView<netdev::wire::FrameTypeSupport> tx_types_view(alloc_, std::size(tx_types)); |
| std::copy(std::begin(tx_types), std::end(tx_types), tx_types_view.data()); |
| config.set_rx_types(alloc_, rx_types_view); |
| config.set_tx_types(alloc_, tx_types_view); |
| return config; |
| } |
| |
| tun::wire::DevicePairPortConfig DefaultPairPortConfig() { |
| tun::wire::DevicePairPortConfig config(alloc_); |
| config.set_base(alloc_, DefaultBasePortConfig()); |
| return config; |
| } |
| |
| tun::wire::DevicePortConfig DefaultDevicePortConfig() { |
| tun::wire::DevicePortConfig config(alloc_); |
| config.set_base(alloc_, DefaultBasePortConfig()); |
| config.set_online(true); |
| return config; |
| } |
| |
| zx::result<fidl::ClientEnd<tun::Device>> OpenTunDevice(tun::wire::DeviceConfig config) { |
| zx::result device_endpoints = fidl::CreateEndpoints<tun::Device>(); |
| if (device_endpoints.is_error()) { |
| return device_endpoints.take_error(); |
| } |
| zx::result tunctl = component::Connect<tun::Control>(); |
| if (tunctl.is_error()) { |
| return tunctl.take_error(); |
| } |
| fidl::WireSyncClient tun{std::move(tunctl.value())}; |
| if (zx_status_t status = |
| tun->CreateDevice(std::move(config), std::move(device_endpoints->server)).status(); |
| status != ZX_OK) { |
| return zx::error(status); |
| } |
| return zx::ok(std::move(device_endpoints->client)); |
| } |
| |
| zx::result<fidl::ClientEnd<tun::Port>> AddTunPort(const fidl::ClientEnd<tun::Device>& client, |
| tun::wire::DevicePortConfig config) { |
| zx::result port_endpoints = fidl::CreateEndpoints<tun::Port>(); |
| if (port_endpoints.is_error()) { |
| return port_endpoints.take_error(); |
| } |
| fidl::Status result = |
| fidl::WireCall(client)->AddPort(std::move(config), std::move(port_endpoints->server)); |
| if (result.status() != ZX_OK) { |
| return zx::error(result.status()); |
| } |
| return zx::ok(std::move(port_endpoints->client)); |
| } |
| |
| zx::result<std::tuple<fidl::WireSharedClient<tun::Device>, fidl::WireSharedClient<tun::Port>, |
| netdev::wire::PortId>> |
| OpenTunDeviceAndPort(tun::wire::DeviceConfig device_config, |
| tun::wire::DevicePortConfig port_config) { |
| zx::result device = OpenTunDevice(std::move(device_config)); |
| if (device.is_error()) { |
| return device.take_error(); |
| } |
| zx::result port = AddTunPort(*device, std::move(port_config)); |
| if (port.is_error()) { |
| return port.take_error(); |
| } |
| fidl::ClientEnd port_client = std::move(port.value()); |
| zx::result port_id = GetPortId([&port_client](fidl::ServerEnd<netdev::Port> server) { |
| return fidl::WireCall(port_client)->GetPort(std::move(server)).status(); |
| }); |
| if (port_id.is_error()) { |
| return port_id.take_error(); |
| } |
| |
| return zx::ok(std::make_tuple( |
| fidl::WireSharedClient(std::move(*device), dispatcher(), |
| std::make_unique<TestEventHandler<tun::Device>>("tun device")), |
| fidl::WireSharedClient(std::move(port_client), dispatcher(), |
| std::make_unique<TestEventHandler<tun::Port>>("tun port")), |
| port_id.value())); |
| } |
| |
| zx::result<std::tuple<fidl::WireSharedClient<tun::Device>, fidl::WireSharedClient<tun::Port>, |
| netdev::wire::PortId>> |
| OpenTunDeviceAndPort() { |
| return OpenTunDeviceAndPort(DefaultDeviceConfig(), DefaultDevicePortConfig()); |
| } |
| |
| static zx::result<fuchsia_hardware_network::wire::PortId> GetPortId( |
| fit::function<zx_status_t(fidl::ServerEnd<fuchsia_hardware_network::Port>)> get_port) { |
| zx::result endpoints = fidl::CreateEndpoints<fuchsia_hardware_network::Port>(); |
| if (endpoints.is_error()) { |
| return endpoints.take_error(); |
| } |
| auto [client, server] = std::move(endpoints.value()); |
| if (zx_status_t status = get_port(std::move(server)); status != ZX_OK) { |
| return zx::error(status); |
| } |
| |
| fidl::WireResult result = fidl::WireCall(client)->GetInfo(); |
| if (!result.ok()) { |
| return zx::error(result.status()); |
| } |
| fuchsia_hardware_network::wire::PortInfo& port_info = result.value().info; |
| if (!port_info.has_id()) { |
| return zx::error(ZX_ERR_INTERNAL); |
| } |
| return zx::ok(port_info.id()); |
| } |
| |
| zx::result<fidl::WireSharedClient<tun::DevicePair>> OpenTunPair( |
| tun::wire::DevicePairConfig config) { |
| zx::result device_pair_endpoints = fidl::CreateEndpoints<tun::DevicePair>(); |
| if (device_pair_endpoints.is_error()) { |
| return device_pair_endpoints.take_error(); |
| } |
| zx::result tunctl = component::Connect<tun::Control>(); |
| if (tunctl.is_error()) { |
| return tunctl.take_error(); |
| } |
| fidl::WireSyncClient tun{std::move(tunctl.value())}; |
| if (zx_status_t status = |
| tun->CreatePair(std::move(config), std::move(device_pair_endpoints->server)).status(); |
| status != ZX_OK) { |
| return zx::error(status); |
| } |
| return zx::ok(fidl::WireSharedClient( |
| std::move(device_pair_endpoints->client), dispatcher(), |
| std::make_unique<TestEventHandler<tun::DevicePair>>("tun device pair"))); |
| } |
| |
| zx::result<fidl::WireSharedClient<tun::DevicePair>> OpenTunPair() { |
| return OpenTunPair(DefaultPairConfig()); |
| } |
| |
| static void WaitTapOnlineInner(fidl::WireSharedClient<tun::Port>& tun_port, |
| fit::callback<void()> complete) { |
| tun_port->WatchState().ThenExactlyOnce( |
| [&tun_port, complete = std::move(complete)]( |
| fidl::WireUnownedResult<tun::Port::WatchState>& result) mutable { |
| if (!result.ok()) |
| return; |
| fidl::WireResponse<tun::Port::WatchState>* response = result.Unwrap(); |
| if (response->state.has_session()) { |
| complete(); |
| } else { |
| WaitTapOnlineInner(tun_port, std::move(complete)); |
| } |
| }); |
| } |
| |
| bool WaitTapOnline(fidl::WireSharedClient<tun::Port>& tun_port) { |
| bool online = false; |
| WaitTapOnlineInner(tun_port, [&online]() { online = true; }); |
| return RunLoopUntilOrFailure([&online]() { return online; }); |
| } |
| |
| fidl::ServerEnd<fuchsia_hardware_network::Device> CreateClientRequest( |
| std::unique_ptr<NetworkDeviceClient>* out_client) { |
| zx::result device_endpoints = fidl::CreateEndpoints<fuchsia_hardware_network::Device>(); |
| EXPECT_OK(device_endpoints.status_value()); |
| std::unique_ptr client = |
| std::make_unique<NetworkDeviceClient>(std::move(device_endpoints->client)); |
| client->SetErrorCallback([](zx_status_t error) { |
| FAIL() << "Client experienced error " << zx_status_get_string(error); |
| }); |
| *out_client = std::move(client); |
| return std::move(device_endpoints->server); |
| } |
| |
| zx_status_t StartSession(NetworkDeviceClient& client) { |
| std::optional<zx_status_t> opt; |
| client.OpenSession("netdev_unittest", [&opt](zx_status_t status) { opt = status; }); |
| if (!RunLoopUntilOrFailure([&opt]() { return opt.has_value(); })) { |
| return ZX_ERR_TIMED_OUT; |
| } |
| return opt.value(); |
| } |
| |
| zx_status_t AttachPort(NetworkDeviceClient& client, fuchsia_hardware_network::wire::PortId port, |
| std::vector<netdev::wire::FrameType> frame_types = { |
| netdev::wire::FrameType::kEthernet}) { |
| std::optional<zx_status_t> opt; |
| client.AttachPort(port, frame_types, [&opt](zx_status_t status) { opt = status; }); |
| if (!RunLoopUntilOrFailure([&opt] { return opt.has_value(); })) { |
| return ZX_ERR_TIMED_OUT; |
| } |
| return opt.value(); |
| } |
| |
| protected: |
| fidl::Arena<> alloc_; |
| }; |
| |
| TEST_F(NetDeviceTest, TestRxTx) { |
| auto tun_device_result = OpenTunDeviceAndPort(); |
| ASSERT_OK(tun_device_result.status_value()); |
| auto& [tun_device, tun_port, port_id] = tun_device_result.value(); |
| std::unique_ptr<NetworkDeviceClient> client; |
| ASSERT_OK(tun_device->GetDevice(CreateClientRequest(&client)).status()); |
| |
| ASSERT_OK(StartSession(*client)); |
| ASSERT_OK(AttachPort(*client, port_id)); |
| ASSERT_TRUE(WaitTapOnline(tun_port)); |
| ASSERT_TRUE(client->HasSession()); |
| |
| bool done = false; |
| std::vector<uint8_t> send_data({0x01, 0x02, 0x03}); |
| client->SetRxCallback([&done, &send_data](NetworkDeviceClient::Buffer buffer) { |
| done = true; |
| auto& data = buffer.data(); |
| ASSERT_EQ(data.frame_type(), fuchsia_hardware_network::wire::FrameType::kEthernet); |
| ASSERT_EQ(data.len(), send_data.size()); |
| ASSERT_EQ(data.parts(), 1u); |
| ASSERT_EQ(memcmp(send_data.data(), data.part(0).data().data(), data.len()), 0); |
| }); |
| |
| bool wrote_frame = false; |
| tun::wire::Frame frame(alloc_); |
| frame.set_frame_type(netdev::wire::FrameType::kEthernet); |
| frame.set_data(alloc_, fidl::VectorView<uint8_t>::FromExternal(send_data)); |
| frame.set_port(kPortId); |
| tun_device->WriteFrame(frame).ThenExactlyOnce( |
| [&wrote_frame](fidl::WireUnownedResult<tun::Device::WriteFrame>& call_result) { |
| if (!call_result.ok()) { |
| ADD_FAILURE() << "WriteFrame failed: " << call_result.error(); |
| return; |
| } |
| const fit::result<zx_status_t>* result = call_result.Unwrap(); |
| wrote_frame = true; |
| if (result->is_error()) { |
| FAIL() << "Failed to write to device " << zx_status_get_string(result->error_value()); |
| } |
| }); |
| ASSERT_TRUE(RunLoopUntilOrFailure([&done, &wrote_frame]() { return done && wrote_frame; })) |
| << "Timed out waiting for frame; done=" << done << ", wrote_frame=" << wrote_frame; |
| |
| done = false; |
| tun_device->ReadFrame().ThenExactlyOnce( |
| [&done, &send_data](fidl::WireUnownedResult<tun::Device::ReadFrame>& call_result) { |
| if (!call_result.ok()) { |
| ADD_FAILURE() << "ReadFrame failed: " << call_result.error(); |
| return; |
| } |
| done = true; |
| const ::fit::result<zx_status_t, ::fuchsia_net_tun::wire::DeviceReadFrameResponse*>* |
| result = call_result.Unwrap(); |
| if (result->is_error()) { |
| FAIL() << "Failed to read from device " << zx_status_get_string(result->error_value()); |
| } |
| ASSERT_EQ(result->value()->frame.frame_type(), netdev::wire::FrameType::kEthernet); |
| const fidl::VectorView<uint8_t>& data = result->value()->frame.data(); |
| ASSERT_TRUE( |
| std::equal(std::begin(data), std::end(data), send_data.begin(), send_data.end())); |
| }); |
| |
| auto tx = client->AllocTx(); |
| ASSERT_TRUE(tx.is_valid()); |
| tx.data().SetFrameType(fuchsia_hardware_network::wire::FrameType::kEthernet); |
| tx.data().SetPortId(port_id); |
| ASSERT_EQ(tx.data().Write(send_data.data(), send_data.size()), send_data.size()); |
| ASSERT_OK(tx.Send()); |
| |
| ASSERT_TRUE(RunLoopUntilOrFailure([&done]() { return done; })); |
| } |
| |
| TEST_F(NetDeviceTest, TestEcho) { |
| auto tun_device_result = OpenTunDeviceAndPort(); |
| ASSERT_OK(tun_device_result.status_value()); |
| auto& [tun_device_bind, tun_port, port_id_bind] = tun_device_result.value(); |
| // Move into variable so we can capture in lambdas. |
| fidl::WireSharedClient tun_device = std::move(tun_device_bind); |
| const netdev::wire::PortId port_id = port_id_bind; |
| std::unique_ptr<NetworkDeviceClient> client; |
| ASSERT_OK(tun_device->GetDevice(CreateClientRequest(&client)).status()); |
| |
| ASSERT_OK(StartSession(*client)); |
| ASSERT_OK(AttachPort(*client, port_id)); |
| ASSERT_TRUE(WaitTapOnline(tun_port)); |
| ASSERT_TRUE(client->HasSession()); |
| |
| constexpr uint32_t kTestFrames = 128; |
| fit::function<void()> write_frame; |
| uint32_t frame_count = 0; |
| fpromise::bridge<void, zx_status_t> write_bridge; |
| write_frame = [this, &frame_count, &tun_device, &write_bridge, &write_frame]() { |
| if (frame_count == kTestFrames) { |
| write_bridge.completer.complete_ok(); |
| } else { |
| tun::wire::Frame frame(alloc_); |
| frame.set_frame_type(netdev::wire::FrameType::kEthernet); |
| frame.set_data(alloc_, fidl::VectorView<uint8_t>::FromExternal( |
| reinterpret_cast<uint8_t*>(&frame_count), sizeof(frame_count))); |
| frame.set_port(kPortId); |
| tun_device->WriteFrame(frame).ThenExactlyOnce( |
| [&write_bridge, |
| &write_frame](fidl::WireUnownedResult<tun::Device::WriteFrame>& call_result) { |
| if (!call_result.ok()) { |
| ADD_FAILURE() << "WriteFrame failed: " << call_result.error(); |
| return; |
| } |
| fit::result<zx_status_t>* result = call_result.Unwrap(); |
| if (result->is_error()) { |
| write_bridge.completer.complete_error(result->error_value()); |
| } |
| write_frame(); |
| }); |
| frame_count++; |
| } |
| }; |
| |
| uint32_t echoed = 0; |
| client->SetRxCallback([&client, &echoed, &port_id](NetworkDeviceClient::Buffer buffer) { |
| echoed++; |
| // Alternate between echoing with a copy and just descriptor swap. |
| if (echoed % 2 == 0) { |
| auto tx = client->AllocTx(); |
| ASSERT_TRUE(tx.is_valid()) << "Tx alloc failed at echo " << echoed; |
| tx.data().SetFrameType(buffer.data().frame_type()); |
| tx.data().SetTxRequest(fuchsia_hardware_network::wire::TxFlags()); |
| tx.data().SetPortId(port_id); |
| auto wr = tx.data().Write(buffer.data()); |
| EXPECT_EQ(wr, buffer.data().len()); |
| EXPECT_EQ(tx.Send(), ZX_OK); |
| } else { |
| buffer.data().SetTxRequest(fuchsia_hardware_network::wire::TxFlags()); |
| EXPECT_EQ(buffer.Send(), ZX_OK); |
| } |
| }); |
| write_frame(); |
| fpromise::result write_result = RunPromise(write_bridge.consumer.promise()); |
| ASSERT_TRUE(write_result.is_ok()) |
| << "WriteFrame error: " << zx_status_get_string(write_result.error()); |
| |
| uint32_t waiting = 0; |
| fit::function<void()> receive_frame; |
| fpromise::bridge<void, zx_status_t> read_bridge; |
| receive_frame = [&waiting, &tun_device, &read_bridge, &receive_frame]() { |
| tun_device->ReadFrame().ThenExactlyOnce( |
| [&read_bridge, &receive_frame, |
| &waiting](fidl::WireUnownedResult<tun::Device::ReadFrame>& call_result) { |
| if (!call_result.ok()) { |
| ADD_FAILURE() << "ReadFrame failed: " << call_result.error(); |
| return; |
| } |
| const fit::result<zx_status_t, ::fuchsia_net_tun::wire::DeviceReadFrameResponse*>* |
| result = call_result.Unwrap(); |
| if (result->is_error()) { |
| read_bridge.completer.complete_error(result->error_value()); |
| } |
| EXPECT_EQ(result->value()->frame.frame_type(), netdev::wire::FrameType::kEthernet); |
| EXPECT_FALSE(result->value()->frame.has_meta()); |
| if (size_t count = result->value()->frame.data().count(); count != sizeof(uint32_t)) { |
| ADD_FAILURE() << "Unexpected data size " << count; |
| } else { |
| uint32_t payload; |
| memcpy(&payload, result->value()->frame.data().data(), sizeof(uint32_t)); |
| EXPECT_EQ(payload, waiting); |
| } |
| waiting++; |
| if (waiting == kTestFrames) { |
| read_bridge.completer.complete_ok(); |
| } else { |
| receive_frame(); |
| } |
| }); |
| }; |
| receive_frame(); |
| fpromise::result read_result = RunPromise(read_bridge.consumer.promise()); |
| ASSERT_TRUE(read_result.is_ok()) |
| << "ReadFrame failed " << zx_status_get_string(read_result.error()); |
| EXPECT_EQ(echoed, frame_count); |
| } |
| |
| TEST_F(NetDeviceTest, TestEchoPair) { |
| auto tun_pair_result = OpenTunPair(); |
| ASSERT_OK(tun_pair_result.status_value()); |
| auto& tun_pair = tun_pair_result.value(); |
| std::unique_ptr<NetworkDeviceClient> left, right; |
| ASSERT_OK(tun_pair->GetLeft(CreateClientRequest(&left)).status()); |
| ASSERT_OK(tun_pair->GetRight(CreateClientRequest(&right)).status()); |
| { |
| fidl::WireResult result = tun_pair.sync()->AddPort(DefaultPairPortConfig()); |
| ASSERT_OK(result.status()); |
| ASSERT_TRUE(result->is_ok()) << zx_status_get_string(result->error_value()); |
| } |
| zx::result port_id = GetPortId([&tun_pair](fidl::ServerEnd<netdev::Port> port) { |
| return tun_pair->GetLeftPort(kPortId, std::move(port)).status(); |
| }); |
| ASSERT_OK(port_id.status_value()); |
| const netdev::wire::PortId left_port_id = port_id.value(); |
| port_id = GetPortId([&tun_pair](fidl::ServerEnd<netdev::Port> port) { |
| return tun_pair->GetRightPort(kPortId, std::move(port)).status(); |
| }); |
| ASSERT_OK(port_id.status_value()); |
| const netdev::wire::PortId right_port_id = port_id.value(); |
| |
| ASSERT_OK(StartSession(*left)); |
| ASSERT_OK(StartSession(*right)); |
| ASSERT_OK(AttachPort(*left, left_port_id)); |
| ASSERT_OK(AttachPort(*right, right_port_id)); |
| |
| left->SetRxCallback([&left, &left_port_id](NetworkDeviceClient::Buffer buffer) { |
| auto tx = left->AllocTx(); |
| ASSERT_TRUE(tx.is_valid()) << "Tx alloc failed at echo"; |
| tx.data().SetFrameType(buffer.data().frame_type()); |
| tx.data().SetTxRequest(fuchsia_hardware_network::wire::TxFlags()); |
| tx.data().SetPortId(left_port_id); |
| uint32_t pload; |
| EXPECT_EQ(buffer.data().Read(&pload, sizeof(pload)), sizeof(pload)); |
| pload = ~pload; |
| EXPECT_EQ(tx.data().Write(&pload, sizeof(pload)), sizeof(pload)); |
| EXPECT_EQ(tx.Send(), ZX_OK); |
| }); |
| |
| constexpr uint32_t kBufferCount = 128; |
| uint32_t rx_counter = 0; |
| |
| fpromise::bridge completed_bridge; |
| right->SetRxCallback([&rx_counter, &completed_bridge](NetworkDeviceClient::Buffer buffer) { |
| uint32_t pload; |
| EXPECT_EQ(buffer.data().frame_type(), fuchsia_hardware_network::wire::FrameType::kEthernet); |
| EXPECT_EQ(buffer.data().len(), sizeof(pload)); |
| EXPECT_EQ(buffer.data().Read(&pload, sizeof(pload)), sizeof(pload)); |
| EXPECT_EQ(pload, ~rx_counter); |
| rx_counter++; |
| if (rx_counter == kBufferCount) { |
| completed_bridge.completer.complete_ok(); |
| } |
| }); |
| |
| { |
| fpromise::bridge online_bridge; |
| auto status_handle = right->WatchStatus( |
| right_port_id, [completer = std::move(online_bridge.completer)]( |
| fuchsia_hardware_network::wire::PortStatus status) mutable { |
| if (status.flags() & fuchsia_hardware_network::wire::StatusFlags::kOnline) { |
| completer.complete_ok(); |
| } |
| }); |
| ASSERT_TRUE(RunPromise(online_bridge.consumer.promise()).is_ok()); |
| } |
| |
| for (uint32_t i = 0; i < kBufferCount; i++) { |
| auto tx = right->AllocTx(); |
| ASSERT_TRUE(tx.is_valid()); |
| tx.data().SetFrameType(fuchsia_hardware_network::wire::FrameType::kEthernet); |
| tx.data().SetPortId(right_port_id); |
| ASSERT_EQ(tx.data().Write(&i, sizeof(i)), sizeof(i)); |
| ASSERT_OK(tx.Send()); |
| } |
| |
| ASSERT_TRUE(RunPromise(completed_bridge.consumer.promise()).is_ok()); |
| } |
| |
| TEST_F(NetDeviceTest, StatusWatcher) { |
| auto tun_device_result = OpenTunDeviceAndPort(); |
| ASSERT_OK(tun_device_result.status_value()); |
| auto& [tun_device, tun_port, port_id] = tun_device_result.value(); |
| std::unique_ptr<NetworkDeviceClient> client; |
| ASSERT_OK(tun_device->GetDevice(CreateClientRequest(&client)).status()); |
| uint32_t call_count1 = 0; |
| uint32_t call_count2 = 0; |
| bool expect_online = true; |
| |
| auto watcher1 = client->WatchStatus( |
| port_id, [&call_count1, &expect_online](fuchsia_hardware_network::wire::PortStatus status) { |
| call_count1++; |
| ASSERT_EQ(static_cast<bool>(status.flags() & |
| fuchsia_hardware_network::wire::StatusFlags::kOnline), |
| expect_online) |
| << "Unexpected status flags " << static_cast<uint32_t>(status.flags()) |
| << ", online should be " << expect_online; |
| }); |
| |
| { |
| auto watcher2 = client->WatchStatus( |
| port_id, |
| [&call_count2](fuchsia_hardware_network::wire::PortStatus status) { call_count2++; }); |
| |
| // Run loop with both watchers attached. |
| ASSERT_TRUE(RunLoopUntilOrFailure([&call_count1, &call_count2]() { |
| return call_count1 == 1 && call_count2 == 1; |
| })) << "call_count1=" |
| << call_count1 << ", call_count2=" << call_count2; |
| |
| // Set online to false and wait for both watchers again. |
| ASSERT_OK(tun_port.sync()->SetOnline(false).status()); |
| expect_online = false; |
| ASSERT_TRUE(RunLoopUntilOrFailure([&call_count1, &call_count2]() { |
| return call_count1 == 2 && call_count2 == 2; |
| })) << "call_count1=" |
| << call_count1 << ", call_count2=" << call_count2; |
| } |
| // watcher2 goes out of scope, Toggle online 3 times and expect that call_count2 will |
| // not increase. |
| for (uint32_t i = 0; i < 3; i++) { |
| expect_online = !expect_online; |
| ASSERT_OK(tun_port.sync()->SetOnline(expect_online).status()); |
| ASSERT_TRUE(RunLoopUntilOrFailure([&call_count1, &i]() { return call_count1 == 3 + i; })) |
| << "call_count1=" << call_count1 << ", call_count2=" << call_count2; |
| // call_count2 mustn't change. |
| ASSERT_EQ(call_count2, 2u); |
| } |
| } |
| |
| TEST_F(NetDeviceTest, ErrorCallback) { |
| auto tun_device_result = OpenTunDeviceAndPort(); |
| ASSERT_OK(tun_device_result.status_value()); |
| auto& [tun_device, tun_port, port_id] = tun_device_result.value(); |
| std::unique_ptr<NetworkDeviceClient> client; |
| ASSERT_OK(tun_device->GetDevice(CreateClientRequest(&client)).status()); |
| |
| ASSERT_OK(StartSession(*client)); |
| ASSERT_OK(AttachPort(*client, port_id)); |
| ASSERT_TRUE(WaitTapOnline(tun_port)); |
| ASSERT_TRUE(client->HasSession()); |
| |
| // Test error callback gets called when the session is killed. |
| { |
| std::optional<zx_status_t> opt; |
| client->SetErrorCallback([&opt](zx_status_t status) { opt = status; }); |
| ASSERT_OK(client->KillSession()); |
| ASSERT_TRUE(RunLoopUntilOrFailure([&opt]() { return opt.has_value(); })); |
| ASSERT_STATUS(opt.value(), ZX_ERR_CANCELED); |
| ASSERT_FALSE(client->HasSession()); |
| } |
| |
| // Test error callback gets called when the device disappears. |
| { |
| std::optional<zx_status_t> opt; |
| client->SetErrorCallback([&opt](zx_status_t status) { opt = status; }); |
| tun_device = {}; |
| ASSERT_TRUE(RunLoopUntilOrFailure([&opt]() { return opt.has_value(); })); |
| ASSERT_STATUS(opt.value(), ZX_ERR_PEER_CLOSED); |
| } |
| } |
| |
| TEST_F(NetDeviceTest, PadTxFrames) { |
| constexpr uint8_t kPayload[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A}; |
| constexpr uint32_t kMinBufferLength = static_cast<uint32_t>(sizeof(kPayload) - 2); |
| constexpr size_t kSmallPayloadLength = kMinBufferLength - 2; |
| auto device_config = DefaultDeviceConfig(); |
| tun::wire::BaseDeviceConfig base_config(alloc_); |
| base_config.set_min_tx_buffer_length(kMinBufferLength); |
| device_config.set_base(alloc_, std::move(base_config)); |
| |
| auto tun_device_result = |
| OpenTunDeviceAndPort(std::move(device_config), DefaultDevicePortConfig()); |
| ASSERT_OK(tun_device_result.status_value()); |
| auto& [tun_device, tun_port, port_id] = tun_device_result.value(); |
| |
| std::unique_ptr<NetworkDeviceClient> client; |
| ASSERT_OK(tun_device->GetDevice(CreateClientRequest(&client)).status()); |
| |
| ASSERT_OK(StartSession(*client)); |
| ASSERT_OK(AttachPort(*client, port_id)); |
| ASSERT_TRUE(WaitTapOnline(tun_port)); |
| ASSERT_TRUE(client->HasSession()); |
| |
| // Send three frames: one too small, one exactly minimum length, and one larger than minimum |
| // length. |
| for (auto& frame : { |
| cpp20::span(kPayload, kSmallPayloadLength), |
| cpp20::span(kPayload, kMinBufferLength), |
| cpp20::span<const uint8_t>(kPayload), |
| }) { |
| auto tx = client->AllocTx(); |
| // Pollute buffer data first to check zero-padding. |
| for (auto& b : tx.data().part(0).data()) { |
| b = 0xAA; |
| } |
| ASSERT_TRUE(tx.is_valid()); |
| tx.data().SetFrameType(fuchsia_hardware_network::wire::FrameType::kEthernet); |
| tx.data().SetPortId(port_id); |
| EXPECT_EQ(tx.data().Write(frame.data(), frame.size()), frame.size()); |
| EXPECT_EQ(tx.Send(), ZX_OK); |
| |
| std::vector<uint8_t> expect(frame.begin(), frame.end()); |
| while (expect.size() < kMinBufferLength) { |
| expect.push_back(0); |
| } |
| |
| // Retrieve the frame and assert it's what we expect. |
| bool done = false; |
| tun_device->ReadFrame().ThenExactlyOnce( |
| [&done, &expect](fidl::WireUnownedResult<tun::Device::ReadFrame>& call_result) { |
| if (!call_result.ok()) { |
| ADD_FAILURE() << "ReadFrame failed: " << call_result.error(); |
| return; |
| } |
| done = true; |
| const fit::result<zx_status_t, ::fuchsia_net_tun::wire::DeviceReadFrameResponse*>* |
| result = call_result.Unwrap(); |
| if (result->is_error()) { |
| ADD_FAILURE() << "Read frame failed " << zx_status_get_string(result->error_value()); |
| } |
| auto& frame = result->value()->frame; |
| ASSERT_EQ(frame.frame_type(), netdev::wire::FrameType::kEthernet); |
| ASSERT_TRUE(std::equal(std::begin(frame.data()), std::end(frame.data()), expect.begin(), |
| expect.end())); |
| }); |
| ASSERT_TRUE(RunLoopUntilOrFailure([&done]() { return done; })); |
| } |
| } |
| |
| TEST_F(NetDeviceTest, TestPortInfoNoMac) { |
| // Create the tun device and client. |
| auto tun_device_result = OpenTunDeviceAndPort(); |
| ASSERT_OK(tun_device_result.status_value()); |
| auto& [tun_device, tun_port, port_id] = tun_device_result.value(); |
| std::unique_ptr<NetworkDeviceClient> client; |
| ASSERT_OK(tun_device->GetDevice(CreateClientRequest(&client)).status()); |
| |
| // Fetch the port's details. |
| std::optional<zx::result<network::client::PortInfoAndMac>> response; |
| client->GetPortInfoWithMac(port_id, |
| [&response](zx::result<network::client::PortInfoAndMac> result) { |
| response = std::move(result); |
| }); |
| ASSERT_TRUE(RunLoopUntilOrFailure([&response]() { return response.has_value(); })); |
| |
| // Ensure the values are correct. |
| ASSERT_OK(response->status_value()); |
| const network::client::PortInfoAndMac& port_details = response->value(); |
| EXPECT_EQ(port_details.id.base, port_id.base); |
| EXPECT_EQ(port_details.id.salt, port_id.salt); |
| ASSERT_EQ(port_details.rx_types.size(), 1u); |
| EXPECT_EQ(port_details.rx_types.at(0), netdev::wire::FrameType::kEthernet); |
| ASSERT_EQ(port_details.tx_types.size(), 1u); |
| EXPECT_EQ(port_details.tx_types.at(0).type, netdev::wire::FrameType::kEthernet); |
| EXPECT_EQ(port_details.unicast_address, std::nullopt); |
| } |
| |
| TEST_F(NetDeviceTest, TestPortInfoWithMac) { |
| static constexpr fuchsia_net::wire::MacAddress kMacAddress = { |
| .octets = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66}, |
| }; |
| |
| // Setup up the tun port's mac address. |
| tun::wire::DevicePortConfig config = DefaultDevicePortConfig(); |
| config.set_mac(alloc_, kMacAddress); |
| |
| // Create the tun device and client. |
| auto tun_device_result = OpenTunDeviceAndPort(DefaultDeviceConfig(), config); |
| ASSERT_OK(tun_device_result.status_value()); |
| auto& [tun_device, tun_port, port_id] = tun_device_result.value(); |
| std::unique_ptr<NetworkDeviceClient> client; |
| ASSERT_OK(tun_device->GetDevice(CreateClientRequest(&client)).status()); |
| |
| // Fetch the port's details. |
| std::optional<zx::result<network::client::PortInfoAndMac>> response; |
| client->GetPortInfoWithMac(port_id, |
| [&response](zx::result<network::client::PortInfoAndMac> result) { |
| response = std::move(result); |
| }); |
| ASSERT_TRUE(RunLoopUntilOrFailure([&response]() { return response.has_value(); })); |
| |
| // Verify the unicast address was correctly fetched. |
| ASSERT_OK(response->status_value()); |
| const network::client::PortInfoAndMac& port_details = response->value(); |
| ASSERT_TRUE(port_details.unicast_address.has_value()); |
| fidl::Array expected_mac = kMacAddress.octets; |
| fidl::Array actual_mac = port_details.unicast_address.value().octets; |
| EXPECT_THAT(expected_mac, ContainerEq(actual_mac)); |
| } |
| |
| TEST_F(NetDeviceTest, TestPortInfoInvalidPort) { |
| // Create the tun device and client. |
| auto tun_device_result = OpenTunDeviceAndPort(); |
| ASSERT_OK(tun_device_result.status_value()); |
| auto& [tun_device, tun_port, port_id] = tun_device_result.value(); |
| std::unique_ptr<NetworkDeviceClient> client; |
| ASSERT_OK(tun_device->GetDevice(CreateClientRequest(&client)).status()); |
| |
| // Query an invalid port. |
| std::optional<zx::result<network::client::PortInfoAndMac>> response; |
| client->GetPortInfoWithMac(netdev::wire::PortId{.base = 17}, |
| [&response](zx::result<network::client::PortInfoAndMac> result) { |
| response = std::move(result); |
| }); |
| ASSERT_TRUE(RunLoopUntilOrFailure([&response]() { return response.has_value(); })); |
| |
| // Ensure we received the correct error. |
| ASSERT_STATUS(response->status_value(), ZX_ERR_NOT_FOUND); |
| } |
| |
| // Guards against a regression where in-flight tx and rx frames could cause a |
| // page fault when the device is closed. |
| TEST_F(NetDeviceTest, CancelsWaitOnTeardown) { |
| auto tun_device_result = OpenTunDeviceAndPort(); |
| ASSERT_OK(tun_device_result.status_value()); |
| auto& [tun_device, tun_port, port_id] = tun_device_result.value(); |
| std::unique_ptr<NetworkDeviceClient> client; |
| ASSERT_OK(tun_device->GetDevice(CreateClientRequest(&client)).status()); |
| |
| ASSERT_OK(StartSession(*client)); |
| ASSERT_OK(AttachPort(*client, port_id)); |
| |
| // Generate some rx and tx traffic. |
| for (size_t i = 0; i < 10; i++) { |
| // NB: Not a constant because FromExternal doesn't like constant pointers. |
| uint8_t kSendData[] = {1, 2, 3}; |
| tun::wire::Frame frame(alloc_); |
| frame.set_frame_type(netdev::wire::FrameType::kEthernet); |
| frame.set_data(alloc_, |
| fidl::VectorView<uint8_t>::FromExternal(kSendData, std::size(kSendData))); |
| frame.set_port(kPortId); |
| tun_device->WriteFrame(frame).ThenExactlyOnce( |
| [](fidl::WireUnownedResult<tun::Device::WriteFrame>& call_result) { |
| if (!call_result.ok()) { |
| if (call_result.is_canceled()) { |
| // Expected error. |
| return; |
| } |
| ADD_FAILURE() << "WriteFrame failed: " << call_result.error(); |
| return; |
| } |
| const fit::result<zx_status_t>* result = call_result.Unwrap(); |
| zx_status_t status = [&result]() { |
| if (result->is_error()) { |
| return result->error_value(); |
| } |
| return ZX_OK; |
| }(); |
| EXPECT_OK(status); |
| }); |
| auto tx = client->AllocTx(); |
| ASSERT_TRUE(tx.is_valid()); |
| tx.data().SetFrameType(fuchsia_hardware_network::wire::FrameType::kEthernet); |
| tx.data().SetPortId(port_id); |
| ASSERT_EQ(tx.data().Write(kSendData, std::size(kSendData)), std::size(kSendData)); |
| ASSERT_OK(tx.Send()); |
| } |
| |
| std::optional<zx_status_t> err; |
| client->SetErrorCallback([&err](zx_status_t status) { err = status; }); |
| // Drop the device, wait for termination, then run the loop until idle. |
| tun_port.AsyncTeardown(); |
| tun_device.AsyncTeardown(); |
| ASSERT_TRUE(RunLoopUntilOrFailure([&err]() { return err.has_value(); })); |
| ASSERT_STATUS(err.value(), ZX_ERR_PEER_CLOSED); |
| RunLoopUntilIdle(); |
| } |
| |
| struct SessionConfigTestCase { |
| std::string name; |
| uint32_t max_buffer_length; |
| uint32_t alignment; |
| uint64_t expected_buffer_stride; |
| uint16_t min_tx_buffer_head; |
| uint16_t min_tx_buffer_tail; |
| zx_status_t expected_config_validity; |
| }; |
| |
| SessionConfigTestCase config_test_cases[] = { |
| { |
| .name = "DefaultBufferSizeWithDefaultAlignment", |
| .max_buffer_length = NetworkDeviceClient::kDefaultBufferLength, |
| .alignment = 1, |
| .expected_buffer_stride = uint64_t{NetworkDeviceClient::kDefaultBufferLength}, |
| .min_tx_buffer_head = 0, |
| .min_tx_buffer_tail = 0, |
| .expected_config_validity = ZX_OK, |
| }, |
| { |
| .name = "BufferSizeIsMultipleOfAlignment", |
| .max_buffer_length = 256, |
| .alignment = 8, |
| .expected_buffer_stride = uint64_t{256}, |
| .min_tx_buffer_head = 0, |
| .min_tx_buffer_tail = 0, |
| .expected_config_validity = ZX_OK, |
| }, |
| { |
| .name = "AlignUpWhenBufferSizeIsNotMultipleOfAlignment", |
| .max_buffer_length = 64 + 112, |
| .alignment = 64, |
| .expected_buffer_stride = uint64_t{64 + 128}, |
| .min_tx_buffer_head = 0, |
| .min_tx_buffer_tail = 0, |
| .expected_config_validity = ZX_OK, |
| }, |
| { |
| .name = "BufferSizeSmallerThanAlignment", |
| .max_buffer_length = 64, |
| .alignment = 128, |
| .expected_buffer_stride = uint64_t{128}, |
| .min_tx_buffer_head = 0, |
| .min_tx_buffer_tail = 0, |
| .expected_config_validity = ZX_OK, |
| }, |
| { |
| .name = "SessionBufferLengthFitsHeaderTail", |
| .max_buffer_length = NetworkDeviceClient::kDefaultBufferLength, |
| .alignment = 128, |
| .expected_buffer_stride = NetworkDeviceClient::kDefaultBufferLength, |
| .min_tx_buffer_head = 32, |
| .min_tx_buffer_tail = 32, |
| .expected_config_validity = ZX_OK, |
| }, |
| { |
| .name = "SessionBufferLengthTooShort", |
| .max_buffer_length = 64, |
| .alignment = 64, |
| .expected_buffer_stride = 64, |
| .min_tx_buffer_head = 32, |
| .min_tx_buffer_tail = 32, |
| .expected_config_validity = ZX_ERR_INVALID_ARGS, |
| }, |
| }; |
| |
| class SessionConfigTest : public testing::TestWithParam<SessionConfigTestCase> {}; |
| |
| TEST_P(SessionConfigTest, GeneratesCorrectSessionConfig) { |
| const auto params = GetParam(); |
| auto config = NetworkDeviceClient::DefaultSessionConfig({ |
| // These values are copied from the default network tun device. |
| .min_descriptor_length = 0, |
| .descriptor_version = 0, |
| .rx_depth = 255, |
| .tx_depth = 255, |
| .buffer_alignment = params.alignment, |
| .max_buffer_length = params.max_buffer_length, |
| .min_rx_buffer_length = 0, |
| .min_tx_buffer_length = 0, |
| .min_tx_buffer_head = params.min_tx_buffer_head, |
| .min_tx_buffer_tail = params.min_tx_buffer_tail, |
| .max_buffer_parts = 255, |
| }); |
| EXPECT_EQ(config.buffer_stride, params.expected_buffer_stride); |
| EXPECT_EQ(config.tx_header_length, params.min_tx_buffer_head); |
| EXPECT_EQ(config.tx_tail_length, params.min_tx_buffer_tail); |
| EXPECT_STATUS(config.Validate(), params.expected_config_validity); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(SessionConfigSuite, SessionConfigTest, |
| testing::ValuesIn<SessionConfigTestCase>(config_test_cases), |
| [](const testing::TestParamInfo<SessionConfigTest::ParamType>& info) { |
| return info.param.name; |
| }); |
| |
| class GetPortsTest : public NetDeviceTest, public testing::WithParamInterface<size_t> {}; |
| |
| TEST_P(GetPortsTest, GetPortsWithPortCount) { |
| const size_t port_count = GetParam(); |
| auto tun_device_result = OpenTunDevice(DefaultDeviceConfig()); |
| ASSERT_OK(tun_device_result.status_value()); |
| fidl::ClientEnd tun_device = std::move(tun_device_result.value()); |
| std::unique_ptr<NetworkDeviceClient> client; |
| ASSERT_OK(fidl::WireCall(tun_device)->GetDevice(CreateClientRequest(&client)).status()); |
| |
| // Sidestep having to write a custom hasher to put things in a set. |
| union PortId { |
| netdev::wire::PortId id; |
| uint16_t v; |
| }; |
| static_assert(sizeof(netdev::wire::PortId) == sizeof(uint16_t)); |
| |
| std::vector<fidl::ClientEnd<tun::Port>> ports; |
| std::unordered_set<uint16_t> expect_ids; |
| for (size_t i = 0; i < port_count; i++) { |
| SCOPED_TRACE(i); |
| tun::wire::DevicePortConfig config = DefaultDevicePortConfig(); |
| config.base().set_id(static_cast<uint8_t>(i + 1)); |
| zx::result status = AddTunPort(tun_device, std::move(config)); |
| ASSERT_OK(status.status_value()); |
| fidl::ClientEnd port = std::move(status.value()); |
| zx::result endpoints = fidl::CreateEndpoints<netdev::Port>(); |
| ASSERT_OK(endpoints.status_value()); |
| auto [client, server] = std::move(endpoints.value()); |
| ASSERT_OK(fidl::WireCall(port)->GetPort(std::move(server)).status()); |
| ports.push_back(std::move(port)); |
| |
| fidl::WireResult result = fidl::WireCall(client)->GetInfo(); |
| ASSERT_OK(result.status()); |
| const netdev::wire::PortInfo& info = result.value().info; |
| ASSERT_TRUE(info.has_id()); |
| expect_ids.insert(PortId{.id = info.id()}.v); |
| } |
| |
| std::optional<zx::result<std::vector<netdev::wire::PortId>>> response; |
| client->GetPorts( |
| [&response](zx::result<std::vector<netdev::wire::PortId>> result) { response = result; }); |
| ASSERT_TRUE(RunLoopUntilOrFailure([&response]() { return response.has_value(); })); |
| zx::result status = std::move(response.value()); |
| ASSERT_OK(status.status_value()); |
| std::vector<netdev::wire::PortId> got_ids = std::move(status.value()); |
| EXPECT_EQ(got_ids.size(), expect_ids.size()); |
| for (netdev::wire::PortId id : got_ids) { |
| auto it = expect_ids.find(PortId{.id = id}.v); |
| EXPECT_NE(it, expect_ids.end()) |
| << " couldn't find ID " << id.base << ", " << id.salt << " in set "; |
| expect_ids.erase(it); |
| } |
| } |
| |
| INSTANTIATE_TEST_SUITE_P(NetDeviceTest, GetPortsTest, testing::Values(0, 1, 3), |
| [](const testing::TestParamInfo<size_t>& info) { |
| return std::to_string(info.param); |
| }); |
| |
| } // namespace |