blob: 8b1a1d62d1ddcae3bb80d9c6e7d5a6eb8c23dd07 [file] [log] [blame]
// Copyright 2017 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 "ethertap.h"
#include <lib/fidl/cpp/message.h>
#include <lib/fidl/cpp/message_builder.h>
#include <lib/fidl/llcpp/message.h>
#include <lib/fidl/txn_header.h>
#include <lib/operation/ethernet.h>
#include <stdio.h>
#include <string.h>
#include <memory>
#include <ddk/debug.h>
#include <ddk/driver.h>
#include <fbl/auto_lock.h>
#include <pretty/hexdump.h>
#include "src/connectivity/ethernet/drivers/ethertap/ethertap-bind.h"
// This macro allows for per-device tracing rather than enabling tracing for the whole driver
#define ethertap_trace(args...) \
do { \
if (unlikely(options_ & ETHERTAP_OPT_TRACE)) \
zxlogf(INFO, "ethertap: " args); \
} while (0)
#define ETHERTAP_OPT_TRACE (fuchsia_hardware_ethertap_OPT_TRACE)
#define ETHERTAP_OPT_TRACE_PACKETS (fuchsia_hardware_ethertap_OPT_TRACE_PACKETS)
#define ETHERTAP_OPT_REPORT_PARAM (fuchsia_hardware_ethertap_OPT_REPORT_PARAM)
#define ETHERTAP_OPT_ONLINE (fuchsia_hardware_ethertap_OPT_ONLINE)
namespace eth {
static zx_status_t fidl_tap_ctl_open_device(void* ctx, const char* name_data, size_t name_size,
const fuchsia_hardware_ethertap_Config* config,
zx_handle_t device_handle, fidl_txn_t* txn) {
auto ctl = static_cast<TapCtl*>(ctx);
// copy provided name so we can add a null termination:
ZX_DEBUG_ASSERT(name_size <= fuchsia_hardware_ethertap_MAX_NAME_LENGTH);
char name[fuchsia_hardware_ethertap_MAX_NAME_LENGTH + 1];
// NOTE(brunodalbo): if name_data contains inline null characters, displaying it may not match
// exactly what we got from FIDL. Because this is mostly used for debugging, we can let it pass.
// Issue 38101 to tracks having sized strings in DDK API.
memcpy(name, name_data, name_size);
name[name_size] = '\0';
auto status = ctl->OpenDevice(name, config, zx::channel(device_handle));
return fuchsia_hardware_ethertap_TapControlOpenDevice_reply(txn, status);
}
static const fuchsia_hardware_ethertap_TapControl_ops_t tap_ctl_ops_ = {
.OpenDevice = fidl_tap_ctl_open_device};
TapCtl::TapCtl(zx_device_t* device) : ddk::Device<TapCtl, ddk::Messageable>(device) {}
zx_status_t TapCtl::Create(void* ctx, zx_device_t* parent) {
auto dev = std::unique_ptr<TapCtl>(new TapCtl(parent));
zx_status_t status = dev->DdkAdd("tapctl");
if (status != ZX_OK) {
zxlogf(ERROR, "%s: could not add device: %d", __func__, status);
} else {
// devmgr owns the memory now
__UNUSED auto* ptr = dev.release();
}
return status;
}
void TapCtl::DdkRelease() { delete this; }
zx_status_t TapCtl::DdkMessage(fidl_incoming_msg_t* msg, fidl_txn_t* txn) {
return fuchsia_hardware_ethertap_TapControl_dispatch(this, txn, msg, &tap_ctl_ops_);
}
zx_status_t TapCtl::OpenDevice(const char* name, const fuchsia_hardware_ethertap_Config* config,
zx::channel device) {
if (config->mtu > fuchsia_hardware_ethertap_MAX_MTU) {
return ZX_ERR_INVALID_ARGS;
}
auto tap =
std::unique_ptr<eth::TapDevice>(new eth::TapDevice(zxdev(), config, std::move(device)));
auto status = tap->DdkAdd(name);
if (status != ZX_OK) {
zxlogf(ERROR, "tapctl: could not add tap device: %d", status);
} else {
// devmgr owns the memory until release is called
__UNUSED auto ptr = tap.release();
zxlogf(INFO, "tapctl: created ethertap device '%s'", name);
}
return status;
}
int tap_device_thread(void* arg) {
TapDevice* device = reinterpret_cast<TapDevice*>(arg);
return device->Thread();
}
#define TAP_SHUTDOWN ZX_USER_SIGNAL_7
static zx_status_t fidl_tap_device_write_frame(void* ctx, const uint8_t* data_data,
size_t data_count) {
static_cast<TapDevice*>(ctx)->Recv(data_data, static_cast<uint32_t>(data_count));
return ZX_OK;
}
static zx_status_t fidl_tap_device_set_online(void* ctx, bool online) {
static_cast<TapDevice*>(ctx)->UpdateLinkStatus(online);
return ZX_OK;
}
static const fuchsia_hardware_ethertap_TapDevice_ops_t tap_device_ops_ = {
.WriteFrame = fidl_tap_device_write_frame, .SetOnline = fidl_tap_device_set_online};
TapDevice::TapDevice(zx_device_t* device, const fuchsia_hardware_ethertap_Config* config,
zx::channel server)
: ddk::Device<TapDevice, ddk::Unbindable>(device),
options_(config->options),
features_(config->features | ETHERNET_FEATURE_SYNTH),
mtu_(config->mtu),
online_((config->options & ETHERTAP_OPT_ONLINE) != 0),
channel_(std::move(server)) {
ZX_DEBUG_ASSERT(channel_.is_valid());
memcpy(mac_, config->mac.octets, 6);
int ret = thrd_create_with_name(&thread_, tap_device_thread, reinterpret_cast<void*>(this),
"ethertap-thread");
ZX_DEBUG_ASSERT(ret == thrd_success);
}
void TapDevice::DdkRelease() {
ethertap_trace("DdkRelease\n");
int ret = thrd_join(thread_, nullptr);
ZX_DEBUG_ASSERT(ret == thrd_success);
delete this;
}
void TapDevice::DdkUnbind(ddk::UnbindTxn txn) {
ethertap_trace("DdkUnbind\n");
fbl::AutoLock lock(&lock_);
if (dead_) {
// If the worker thread is already dead, we can reply to the unbind immediately.
txn.Reply();
return;
}
unbind_txn_ = std::move(txn);
zx_status_t status = channel_.signal(0, TAP_SHUTDOWN);
ZX_DEBUG_ASSERT(status == ZX_OK);
// When the thread exits after the channel is closed, it will reply to the unbind txn.
}
zx_status_t TapDevice::EthernetImplQuery(uint32_t options, ethernet_info_t* info) {
memset(info, 0, sizeof(*info));
info->features = features_;
info->mtu = mtu_;
memcpy(info->mac, mac_, 6);
info->netbuf_size = eth::BorrowedOperation<>::OperationSize(sizeof(ethernet_netbuf_t));
return ZX_OK;
}
void TapDevice::EthernetImplStop() {
ethertap_trace("EthernetImplStop\n");
fbl::AutoLock lock(&lock_);
ethernet_client_.clear();
}
zx_status_t TapDevice::EthernetImplStart(const ethernet_ifc_protocol_t* ifc) {
ethertap_trace("EthernetImplStart\n");
fbl::AutoLock lock(&lock_);
if (ethernet_client_.is_valid()) {
return ZX_ERR_ALREADY_BOUND;
} else {
ethernet_client_ = ddk::EthernetIfcProtocolClient(ifc);
ethernet_client_.Status(online_ ? ETHERNET_STATUS_ONLINE : 0u);
}
return ZX_OK;
}
void TapDevice::EthernetImplQueueTx(uint32_t options, ethernet_netbuf_t* netbuf,
ethernet_impl_queue_tx_callback completion_cb, void* cookie) {
eth::BorrowedOperation<> op(netbuf, completion_cb, cookie, sizeof(ethernet_netbuf_t));
fbl::AutoLock lock(&lock_);
if (dead_) {
op.Complete(ZX_ERR_PEER_CLOSED);
return;
} else if (!online_) {
ethertap_trace("dropping packet, device offline\n");
op.Complete(ZX_ERR_UNAVAILABLE);
return;
}
size_t length = op.operation()->data_size;
ZX_DEBUG_ASSERT(length <= mtu_);
FIDL_ALIGNDECL uint8_t temp_buff[sizeof(fuchsia_hardware_ethertap_TapDeviceOnFrameEvent) +
FIDL_ALIGN(fuchsia_hardware_ethertap_MAX_MTU)];
fidl::Builder builder(temp_buff, sizeof(temp_buff));
auto* event = builder.New<fuchsia_hardware_ethertap_TapDeviceOnFrameEvent>();
fidl_init_txn_header(&event->hdr, FIDL_TXID_NO_RESPONSE,
fuchsia_hardware_ethertap_TapDeviceOnFrameOrdinal);
event->data.count = length;
auto* data = builder.NewArray<uint8_t>(static_cast<uint32_t>(length));
event->data.data = data;
memcpy(data, op.operation()->data_buffer, length);
const char* err = nullptr;
fidl::HLCPPOutgoingMessage msg(builder.Finalize(), fidl::HandleDispositionPart());
auto status = msg.Encode(&fuchsia_hardware_ethertap_TapDeviceOnFrameEventTable, &err);
if (status != ZX_OK) {
zxlogf(ERROR, "ethertap: EthernetImplQueueTx error encoding: %d %s", status, err);
} else {
if (unlikely(options_ & ETHERTAP_OPT_TRACE_PACKETS)) {
ethertap_trace("sending %zu bytes\n", length);
hexdump8_ex(op.operation()->data_buffer, length, 0);
}
status = msg.Write(channel_.get(), 0);
if (status != ZX_OK) {
zxlogf(WARNING, "ethertap: EthernetImplQueueTx error writing: %d", status);
}
}
// returning ZX_ERR_SHOULD_WAIT indicates that we will call complete_tx(), which we will not
op.Complete(status == ZX_ERR_SHOULD_WAIT ? ZX_ERR_UNAVAILABLE : status);
}
zx_status_t TapDevice::EthernetImplSetParam(uint32_t param, int32_t value, const uint8_t* data,
size_t data_size) {
fbl::AutoLock lock(&lock_);
if (!(options_ & ETHERTAP_OPT_REPORT_PARAM) || dead_) {
return ZX_ERR_NOT_SUPPORTED;
}
FIDL_ALIGNDECL uint8_t temp_buff[sizeof(fuchsia_hardware_ethertap_TapDeviceOnReportParamsEvent) +
FIDL_ALIGN(fuchsia_hardware_ethertap_MAX_PARAM_DATA)];
fidl::Builder builder(temp_buff, sizeof(temp_buff));
auto* event = builder.New<fuchsia_hardware_ethertap_TapDeviceOnReportParamsEvent>();
fidl_init_txn_header(&event->hdr, FIDL_TXID_NO_RESPONSE,
fuchsia_hardware_ethertap_TapDeviceOnReportParamsOrdinal);
event->param = param;
event->value = value;
event->data.data = nullptr;
event->data.count = 0;
switch (param) {
case ETHERNET_SETPARAM_MULTICAST_FILTER:
if (value == ETHERNET_MULTICAST_FILTER_OVERFLOW) {
break;
} else {
// Send the final byte of each address, sorted lowest-to-highest.
auto size = static_cast<uint32_t>(value) < fuchsia_hardware_ethertap_MAX_PARAM_DATA
? static_cast<uint32_t>(value)
: fuchsia_hardware_ethertap_MAX_PARAM_DATA;
auto* report = builder.NewArray<uint8_t>(size);
event->data.data = report;
event->data.count = size;
uint32_t i;
for (i = 0; i < size; i++) {
report[i] = static_cast<const uint8_t*>(data)[i * ETH_MAC_SIZE + 5];
}
qsort(report, size, 1, [](const void* ap, const void* bp) {
int a = *static_cast<const uint8_t*>(ap);
int b = *static_cast<const uint8_t*>(bp);
return a < b ? -1 : (a > 1 ? 1 : 0);
});
}
break;
default:
break;
}
// A failure of sending the event data is not a simulated failure of hardware under test,
// so log it but don't report failure on the SetParam attempt.
const char* err = nullptr;
fidl::HLCPPOutgoingMessage msg(builder.Finalize(), fidl::HandleDispositionPart());
auto status = msg.Encode(&fuchsia_hardware_ethertap_TapDeviceOnReportParamsEventTable, &err);
if (status != ZX_OK) {
zxlogf(ERROR, "ethertap: EthernetImplSetParam error encoding: %d %s", status, err);
} else {
msg.Write(channel_.get(), 0);
}
return ZX_OK;
}
void TapDevice::EthernetImplGetBti(zx::bti* bti) { bti->reset(); }
void TapDevice::UpdateLinkStatus(bool online) {
bool was_online = online_;
if (online) {
ethertap_trace("online asserted\n");
online_ = true;
} else {
ethertap_trace("offline asserted\n");
online_ = false;
}
if (was_online != online_) {
fbl::AutoLock lock(&lock_);
if (ethernet_client_.is_valid()) {
ethernet_client_.Status(online_ ? ETHERNET_STATUS_ONLINE : 0u);
}
ethertap_trace("device '%s' is now %s\n", name(), online_ ? "online" : "offline");
}
}
zx_status_t TapDevice::Recv(const uint8_t* buffer, uint32_t length) {
fbl::AutoLock lock(&lock_);
if (!online_) {
ethertap_trace("attempted to push bytes to an offline device\n");
return ZX_OK;
}
if (unlikely(options_ & ETHERTAP_OPT_TRACE_PACKETS)) {
ethertap_trace("received %u bytes\n", length);
hexdump8_ex(buffer, length, 0);
}
if (ethernet_client_.is_valid()) {
ethernet_client_.Recv(buffer, length, 0u);
}
return ZX_OK;
}
typedef struct tap_device_txn {
fidl_txn_t txn;
zx_txid_t txid;
TapDevice* device;
} tap_device_txn_t;
static zx_status_t tap_device_reply(fidl_txn_t* txn, const fidl_outgoing_msg_t* msg) {
static_assert(offsetof(tap_device_txn_t, txn) == 0, "FidlConnection must be convertable to txn");
auto* ptr = reinterpret_cast<tap_device_txn_t*>(txn);
return ptr->device->Reply(ptr->txid, msg);
}
zx_status_t TapDevice::Reply(zx_txid_t txid, const fidl_outgoing_msg_t* msg) {
auto adapter = fidl::OutgoingMessageAdaptorFromC(msg);
fidl::OutgoingMessage& message = adapter.GetOutgoingMessage();
message.set_txid(txid);
message.Write(channel_);
return message.status();
}
int TapDevice::Thread() {
ethertap_trace("starting main thread\n");
zx_signals_t pending;
const uint32_t buff_size = 2 * mtu_;
constexpr uint32_t handle_count = 8;
std::unique_ptr<uint8_t[]> data_buff(new uint8_t[buff_size]);
zx_handle_info_t handles_buff[handle_count];
fidl_incoming_msg_t msg = {
.bytes = data_buff.get(),
.handles = handles_buff,
.num_bytes = buff_size,
.num_handles = handle_count,
};
tap_device_txn_t txn = {
.txn = {.reply = tap_device_reply},
.txid = 0,
.device = this,
};
zx_status_t status = ZX_OK;
const zx_signals_t wait = ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED | TAP_SHUTDOWN;
while (true) {
status = channel_.wait_one(wait, zx::time::infinite(), &pending);
if (status != ZX_OK) {
ethertap_trace("error waiting on channel: %d\n", status);
break;
}
if (pending & ZX_CHANNEL_READABLE) {
status = channel_.read_etc(0, msg.bytes, msg.handles, buff_size, handle_count, &msg.num_bytes,
&msg.num_handles);
if (status != ZX_OK) {
ethertap_trace("message read failed: %d\n", status);
break;
}
txn.txid = reinterpret_cast<const fidl_message_header_t*>(msg.bytes)->txid;
status = fuchsia_hardware_ethertap_TapDevice_dispatch(this, &txn.txn, &msg, &tap_device_ops_);
if (status != ZX_OK) {
ethertap_trace("failed to dispatch ethertap message: %d\n", status);
break;
}
}
if (pending & ZX_CHANNEL_PEER_CLOSED) {
ethertap_trace("channel closed (peer)\n");
break;
}
if (pending & TAP_SHUTDOWN) {
ethertap_trace("channel closed (self)\n");
break;
}
}
{
fbl::AutoLock lock(&lock_);
dead_ = true;
zxlogf(INFO, "ethertap: device '%s' destroyed", name());
channel_.reset();
// Check if the unbind hook is expecting a response.
if (unbind_txn_) {
unbind_txn_->Reply();
} else {
// Schedule unbinding to begin.
DdkAsyncRemove();
}
}
return static_cast<int>(status);
}
static constexpr zx_driver_ops_t driver_ops = []() {
zx_driver_ops_t ops = {};
ops.version = DRIVER_OPS_VERSION;
ops.bind = TapCtl::Create;
return ops;
}();
} // namespace eth
ZIRCON_DRIVER(tapctl, eth::driver_ops, "zircon", "0.1");