blob: 1d83d4483241d2523003b04021bc55d82c345584 [file] [log] [blame]
// Copyright 2019 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 <endian.h>
#include <fidl/fuchsia.hardware.network/cpp/wire.h>
#include <fidl/fuchsia.hardware.usb.peripheral/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async-loop/testing/cpp/real_loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/fdio/cpp/caller.h>
#include <lib/fdio/directory.h>
#include <lib/fdio/watcher.h>
#include <lib/fzl/vmo-mapper.h>
#include <lib/hid/boot.h>
#include <lib/usb-virtual-bus-launcher/usb-virtual-bus-launcher.h>
#include <lib/zx/fifo.h>
#include <lib/zx/vmo.h>
#include <unistd.h>
#include <zircon/status.h>
#include <zircon/syscalls.h>
#include <algorithm>
#include <vector>
#include <fbl/string.h>
#include <src/devices/lib/client/device_topology.h>
#include <usb/cdc.h>
#include <usb/usb.h>
#include <zxtest/zxtest.h>
#include "src/connectivity/lib/network-device/cpp/network_device_client.h"
namespace usb_virtual_bus {
namespace {
using usb_virtual::BusLauncher;
constexpr const char kManufacturer[] = "Google";
constexpr const char kProduct[] = "CDC Ethernet";
constexpr const char kSerial[] = "ebfd5ad49d2a";
constexpr size_t kEthernetMtu = 1500;
struct DevicePaths {
std::optional<std::string> path;
std::string subdir;
std::string query;
};
zx_status_t WaitForDevice(int dirfd, int event, const char* name, void* cookie) {
if (std::string_view{name} == ".") {
return ZX_OK;
}
if (event != WATCH_EVENT_ADD_FILE) {
return ZX_OK;
}
fdio_cpp::UnownedFdioCaller caller(dirfd);
const fit::result response = fdf_topology::GetTopologicalPath(caller.directory(), name);
if (response.is_error()) {
return response.error_value();
}
std::string_view topological_path = response.value();
DevicePaths* paths = reinterpret_cast<DevicePaths*>(cookie);
if (topological_path.find(paths->query) != std::string::npos) {
paths->path = paths->subdir + std::string{name};
return ZX_ERR_STOP;
}
return ZX_OK;
}
class NetworkDeviceInterface {
public:
explicit NetworkDeviceInterface(fidl::UnownedClientEnd<fuchsia_io::Directory> directory,
const fbl::String& path, const std::string& session_name)
: loop_(&kAsyncLoopConfigNoAttachToCurrentThread) {
zx::result controller =
component::ConnectAt<fuchsia_hardware_network::DeviceInstance>(directory, path);
ASSERT_OK(controller);
auto [client_end, server_end] = fidl::Endpoints<fuchsia_hardware_network::Device>::Create();
fidl::Status device_status =
fidl::WireCall(controller.value())->GetDevice(std::move(server_end));
ASSERT_OK(device_status.status());
netdevice_client_.emplace(std::move(client_end), loop_.dispatcher());
network::client::NetworkDeviceClient& client = netdevice_client_.value();
{
std::optional<zx_status_t> result;
client.OpenSession(session_name, [&result](zx_status_t status) { result = status; });
RunLoopUntil([&result] { return result.has_value(); });
ASSERT_OK(result.value());
}
ASSERT_TRUE(client.HasSession());
client.SetRxCallback([&](network::client::NetworkDeviceClient::Buffer buf) {
rx_queue_.emplace(std::move(buf));
});
{
client.GetPorts(
[this](zx::result<std::vector<network::client::netdev::wire::PortId>> ports_status) {
ASSERT_OK(ports_status.status_value());
std::vector<network::client::netdev::wire::PortId> ports =
std::move(ports_status.value());
ASSERT_EQ(ports.size(), 1);
port_id_ = ports[0];
});
RunLoopUntil([this] { return port_id_.has_value(); });
}
{
std::optional<zx_status_t> result;
client.AttachPort(port_id_.value(), {fuchsia_hardware_network::wire::FrameType::kEthernet},
[&result](zx_status_t status) { result = status; });
RunLoopUntil([&result] { return result.has_value(); });
ASSERT_OK(result.value());
}
network::client::DeviceInfo device_info = client.device_info();
tx_depth_ = device_info.tx_depth;
rx_depth_ = device_info.rx_depth;
{
bool checked_mtu = false;
zx::result<std::unique_ptr<network::client::NetworkDeviceClient::StatusWatchHandle>> result =
client.WatchStatus(
port_id_.value(),
[this, &checked_mtu](fuchsia_hardware_network::wire::PortStatus status) {
if (status.has_mtu()) {
mtu_ = status.mtu();
}
checked_mtu = true;
});
RunLoopUntil([&checked_mtu] { return checked_mtu; });
ASSERT_TRUE(mtu_.has_value());
}
}
zx_status_t SendData(std::vector<uint8_t>& data) {
network::client::NetworkDeviceClient::Buffer buffer = netdevice_client_.value().AllocTx();
if (!buffer.is_valid()) {
return ZX_ERR_NO_MEMORY;
}
// Populate the buffer data and metadata
buffer.data().SetFrameType(fuchsia_hardware_network::wire::FrameType::kEthernet);
buffer.data().SetPortId(port_id_.value());
EXPECT_EQ(buffer.data().Write(data.data(), data.size()), data.size());
zx_status_t status = buffer.Send();
// Run the loop to give the Netdevice client an opportunity to send.
RunLoopUntilIdle();
return status;
}
zx::result<std::vector<uint8_t>> ReceiveData() {
// Wait for the read callback registered with the Netdevice client to fill the queue.
RunLoopUntil([this] { return !rx_queue_.empty(); });
network::client::NetworkDeviceClient::Buffer buffer = std::move(rx_queue_.front());
rx_queue_.pop();
if (!buffer.is_valid()) {
return zx::error(ZX_ERR_IO_DATA_INTEGRITY);
}
// Check that the port ID and frame type match what we expect.
if ((buffer.data().port_id().base != port_id_.value().base) ||
(buffer.data().port_id().salt != port_id_.value().salt) ||
(buffer.data().frame_type() != fuchsia_hardware_network::wire::FrameType::kEthernet)) {
ADD_FAILURE(
"Frame metadata does not match. Received frame port ID base: %hu \
Expected base: %hu \
Received frame port ID salt: %hu \
Expected salt: %hu \
Received frame type: %hu\
Expected frame type: %hu",
buffer.data().port_id().base, port_id_.value().base, buffer.data().port_id().salt,
buffer.data().port_id().salt, static_cast<uint8_t>(buffer.data().frame_type()),
static_cast<uint8_t>(fuchsia_hardware_network::wire::FrameType::kEthernet));
return zx::error(ZX_ERR_INVALID_ARGS);
}
std::vector<uint8_t> output;
output.resize(buffer.data().len());
size_t read = buffer.data().Read(output.data(), output.size());
if (read != output.size()) {
return zx::error(ZX_ERR_INTERNAL);
}
return zx::ok(std::move(output));
}
uint32_t tx_depth() const { return tx_depth_; }
uint32_t rx_depth() const { return rx_depth_; }
size_t mtu() { return mtu_.value(); }
private:
async::Loop loop_;
std::optional<network::client::NetworkDeviceClient> netdevice_client_;
// Since the client receives data in a callback, we need to store the frames we receive.
std::queue<network::client::NetworkDeviceClient::Buffer> rx_queue_;
std::optional<network::client::netdev::wire::PortId> port_id_;
uint32_t tx_depth_;
uint32_t rx_depth_;
std::optional<size_t> mtu_;
// TODO(https://fxbug.dev/42065375): remove this hand-rolled implementation (adapted
// from //sdk/lib/async-loop/testing/real_loop.cc) once `loop_fixture::RealLoop`
// supports configuration.
void RunLoopUntil(fit::function<bool()> condition) {
while (loop_.GetState() == ASYNC_LOOP_RUNNABLE) {
if (condition()) {
loop_.ResetQuit();
return;
}
loop_.Run(zx::deadline_after(zx::msec(1)), true);
}
loop_.ResetQuit();
}
void RunLoopUntilIdle() {
loop_.RunUntilIdle();
loop_.ResetQuit();
}
};
class UsbCdcEcmTest : public zxtest::Test {
public:
void SetUp() override {
auto bus = BusLauncher::Create();
ASSERT_OK(bus.status_value());
bus_ = std::move(bus.value());
ASSERT_NO_FATAL_FAILURE(InitUsbCdcEcm(&peripheral_path_, &host_path_));
}
void TearDown() override {
ASSERT_OK(bus_->ClearPeripheralDeviceFunctions());
ASSERT_OK(bus_->Disable());
}
protected:
std::optional<BusLauncher> bus_;
std::string peripheral_path_;
std::string host_path_;
void InitUsbCdcEcm(std::string* peripheral_path, std::string* host_path) {
namespace usb_peripheral = fuchsia_hardware_usb_peripheral;
using ConfigurationDescriptor =
::fidl::VectorView<fuchsia_hardware_usb_peripheral::wire::FunctionDescriptor>;
usb_peripheral::wire::DeviceDescriptor device_desc = {};
device_desc.bcd_usb = htole16(0x0200);
device_desc.b_device_class = 0;
device_desc.b_device_sub_class = 0;
device_desc.b_device_protocol = 0;
device_desc.b_max_packet_size0 = 64;
device_desc.bcd_device = htole16(0x0100);
device_desc.b_num_configurations = 2;
device_desc.manufacturer = fidl::StringView(kManufacturer);
device_desc.product = fidl::StringView(kProduct);
device_desc.serial = fidl::StringView(kSerial);
device_desc.id_vendor = htole16(0x0BDA);
device_desc.id_product = htole16(0x8152);
usb_peripheral::wire::FunctionDescriptor usb_cdc_ecm_function_desc = {
.interface_class = USB_CLASS_COMM,
.interface_subclass = USB_CDC_SUBCLASS_ETHERNET,
.interface_protocol = 0,
};
std::vector<usb_peripheral::wire::FunctionDescriptor> function_descs;
function_descs.push_back(usb_cdc_ecm_function_desc);
std::vector<ConfigurationDescriptor> config_descs;
config_descs.emplace_back(
fidl::VectorView<usb_peripheral::wire::FunctionDescriptor>::FromExternal(function_descs));
config_descs.emplace_back(
fidl::VectorView<usb_peripheral::wire::FunctionDescriptor>::FromExternal(function_descs));
ASSERT_OK(bus_->SetupPeripheralDevice(std::move(device_desc), std::move(config_descs)));
const auto wait_for_device = [this](DevicePaths& paths) {
fbl::unique_fd fd;
ASSERT_OK(
fdio_open3_fd_at(bus_->GetRootFd(), paths.subdir.c_str(), 0, fd.reset_and_get_address()));
ASSERT_STATUS(fdio_watch_directory(fd.get(), WaitForDevice, ZX_TIME_INFINITE, &paths),
ZX_ERR_STOP);
};
DevicePaths host_device_paths{
.subdir = "class/network/",
.query = "/usb-bus/",
};
// Attach to function-001, because it implements usb-cdc-ecm.
DevicePaths peripheral_device_paths{
.subdir = "class/network/",
.query = "/usb-peripheral/function-001",
};
wait_for_device(host_device_paths);
wait_for_device(peripheral_device_paths);
*host_path = host_device_paths.path.value();
*peripheral_path = peripheral_device_paths.path.value();
}
};
void TransmitAndReceive(NetworkDeviceInterface& a, NetworkDeviceInterface& b,
const uint32_t fifo_depth) {
uint8_t fill_data = 0;
for (size_t i = 0; i < fifo_depth; ++i) {
std::vector<uint8_t> data;
for (size_t j = 0; j < kEthernetMtu; ++j) {
data.push_back(fill_data++);
}
ASSERT_OK(a.SendData(data));
}
size_t received_bytes = 0;
uint8_t read_data = 0;
while (received_bytes < fifo_depth * kEthernetMtu) {
zx::result<std::vector<uint8_t>> received_data = b.ReceiveData();
ASSERT_OK(received_data.status_value());
const std::vector<uint8_t>& data = received_data.value();
ASSERT_EQ(kEthernetMtu, data.size());
received_bytes += data.size();
for (const auto& b : data) {
ASSERT_EQ(b, read_data++);
}
}
}
// TODO(b/316176095): Re-enable test after ensuring it works with DFv2.
TEST_F(UsbCdcEcmTest, DISABLED_TransmitReceive) {
fdio_cpp::UnownedFdioCaller caller(bus_->GetRootFd());
NetworkDeviceInterface peripheral(caller.directory(), peripheral_path_,
"usb-cdc-function-peripheral");
NetworkDeviceInterface host(caller.directory(), host_path_, "usb-cdc-ecm-host");
ASSERT_EQ(peripheral.mtu(), kEthernetMtu);
ASSERT_EQ(host.mtu(), kEthernetMtu);
auto pick_transmit_depth = [](const NetworkDeviceInterface& sender,
const NetworkDeviceInterface& receiver) -> uint32_t {
// NOTE: Netdevice core will double the number of device buffers to account
// for in-flight buffers with the higher layers. This is encoding knowledge
// at a distance here, but basically we don't want to send more than the
// actual underlying device's depth, which makes the transfer racy/fallible.
//
// TODO(https://fxbug.dev/42067498): Improve this when we add a signal that we
// can observe for buffer readiness.
return std::min(sender.tx_depth(), receiver.rx_depth() / 2);
};
TransmitAndReceive(peripheral, host, pick_transmit_depth(peripheral, host));
TransmitAndReceive(host, peripheral, pick_transmit_depth(host, peripheral));
ASSERT_NO_FATAL_FAILURE();
}
} // namespace
} // namespace usb_virtual_bus