blob: d551b924e237704ebf6f3f7e4130c70218271a4a [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 "rndishost.h"
#include <ddk/binding.h>
#include <ddk/debug.h>
#include <ddk/device.h>
#include <ddk/driver.h>
#include <ddk/protocol/ethernet.h>
#include <ddk/protocol/usb-old.h>
#include <ddk/usb/usb.h>
#include <usb/usb-request.h>
#include <zircon/hw/usb/cdc.h>
#include <zircon/hw/usb.h>
#include <zircon/listnode.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <threads.h>
#define READ_REQ_COUNT 8
#define WRITE_REQ_COUNT 4
#define ETH_HEADER_SIZE 4
#define ETHMAC_MAX_TRANSMIT_DELAY 100
#define ETHMAC_MAX_RECV_DELAY 100
#define ETHMAC_TRANSMIT_DELAY 10
#define ETHMAC_RECV_DELAY 10
#define ETHMAC_INITIAL_TRANSMIT_DELAY 0
#define ETHMAC_INITIAL_RECV_DELAY 0
typedef struct {
zx_device_t* zxdev;
zx_device_t* usb_zxdev;
usb_protocol_t usb;
uint8_t mac_addr[6];
uint8_t control_intf;
uint32_t request_id;
uint32_t mtu;
uint8_t bulk_in_addr;
uint8_t bulk_out_addr;
list_node_t free_read_reqs;
list_node_t free_write_reqs;
uint64_t rx_endpoint_delay; // wait time between 2 recv requests
uint64_t tx_endpoint_delay; // wait time between 2 transmit requests
// Interface to the ethernet layer.
ethmac_ifc_t ifc;
thrd_t thread;
bool thread_started;
size_t parent_req_size;
mtx_t mutex;
} rndishost_t;
static void dump_buffer(void* buf) {
uint8_t* p = buf;
for (int i = 0; i < RNDIS_BUFFER_SIZE; i += 4) {
if (i != 0 && i % 24 == 0) {
zxlogf(DEBUG1, "\n");
}
zxlogf(DEBUG1, "%08x ", p[i] | p[i + 1] << 8 | p[i + 2] << 16 | p[i + 3] << 24);
}
zxlogf(DEBUG1, "\n");
}
static bool command_succeeded(void* buf, uint32_t type, uint32_t length) {
rndis_header_complete* header = buf;
if (header->msg_type != type) {
zxlogf(DEBUG1, "Bad type: Actual: %x, Expected: %x.\n",
header->msg_type, type);
return false;
}
if (header->msg_length != length) {
zxlogf(DEBUG1, "Bad length: Actual: %d, Expected: %d.\n",
header->msg_length, length);
return false;
}
if (header->status != RNDIS_STATUS_SUCCESS) {
zxlogf(DEBUG1, "Bad status: %x.\n", header->status);
return false;
}
return true;
}
static zx_status_t rndis_command(rndishost_t* eth, void* buf) {
rndis_header* header = buf;
uint32_t request_id = eth->request_id++;
header->request_id = request_id;
zx_status_t status;
status = usb_control(&eth->usb, USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
USB_CDC_SEND_ENCAPSULATED_COMMAND,
0, eth->control_intf, buf, header->msg_length, RNDIS_CONTROL_TIMEOUT,
NULL);
if (status < 0) {
return status;
}
status = usb_control(&eth->usb, USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE,
USB_CDC_GET_ENCAPSULATED_RESPONSE,
0, eth->control_intf, buf, RNDIS_BUFFER_SIZE, RNDIS_CONTROL_TIMEOUT, NULL);
if (header->request_id != request_id) {
return ZX_ERR_IO_DATA_INTEGRITY;
}
return status;
}
static void rndishost_recv(rndishost_t* eth, usb_request_t* request) {
size_t len = request->response.actual;
uint8_t* read_data;
zx_status_t status = usb_request_mmap(request, (void*)&read_data);
if (status != ZX_OK) {
zxlogf(ERROR, "rndishost receive: usb_request_mmap failed: %d\n", status);
return;
}
while (len > sizeof(rndis_packet_header)) {
rndis_packet_header* header = (rndis_packet_header*)read_data;
// The |data_offset| field contains the offset to the payload measured from the start of
// the field itself.
size_t data_offset = offsetof(rndis_packet_header, data_offset) + header->data_offset;
if (header->msg_type != RNDIS_PACKET_MSG || len < header->msg_length ||
len < data_offset + header->data_length) {
zxlogf(DEBUG1, "rndis bad packet\n");
return;
}
if (header->data_length == 0) {
// No more data.
return;
}
ethmac_ifc_recv(&eth->ifc, read_data + data_offset, header->data_length, 0);
read_data += header->msg_length;
len -= header->msg_length;
}
}
static void rndis_read_complete(usb_request_t* request, void* cookie) {
rndishost_t* eth = (rndishost_t*)cookie;
if (request->response.status == ZX_ERR_IO_NOT_PRESENT) {
usb_request_release(request);
return;
}
mtx_lock(&eth->mutex);
if (request->response.status == ZX_ERR_IO_REFUSED) {
zxlogf(TRACE, "rndis_read_complete usb_reset_endpoint\n");
usb_reset_endpoint(&eth->usb, eth->bulk_in_addr);
} else if (request->response.status == ZX_ERR_IO_INVALID) {
zxlogf(TRACE, "rndis_read_complete Slowing down the requests by %d usec"
" and resetting the recv endpoint\n",
ETHMAC_RECV_DELAY);
if (eth->rx_endpoint_delay < ETHMAC_MAX_RECV_DELAY) {
eth->rx_endpoint_delay += ETHMAC_RECV_DELAY;
}
usb_reset_endpoint(&eth->usb, eth->bulk_in_addr);
}
if ((request->response.status == ZX_OK) && eth->ifc.ops) {
rndishost_recv(eth, request);
} else {
zxlogf(DEBUG1, "rndis read complete: bad status = %d\n", request->response.status);
}
// TODO: Only usb_request_queue if the device is online.
zx_nanosleep(zx_deadline_after(ZX_USEC(eth->rx_endpoint_delay)));
usb_request_queue(&eth->usb, request, rndis_read_complete, eth);
mtx_unlock(&eth->mutex);
}
static void rndis_write_complete(usb_request_t* request, void* cookie) {
rndishost_t* eth = (rndishost_t*)cookie;
if (request->response.status == ZX_ERR_IO_NOT_PRESENT) {
zxlogf(ERROR, "rndis_write_complete zx_err_io_not_present\n");
usb_request_release(request);
return;
}
mtx_lock(&eth->mutex);
if (request->response.status == ZX_ERR_IO_REFUSED) {
zxlogf(TRACE, "rndishost usb_reset_endpoint\n");
usb_reset_endpoint(&eth->usb, eth->bulk_out_addr);
} else if (request->response.status == ZX_ERR_IO_INVALID) {
zxlogf(TRACE, "rndis_write_complete Slowing down the requests by %d usec"
" and resetting the transmit endpoint\n",
ETHMAC_TRANSMIT_DELAY);
if (eth->tx_endpoint_delay < ETHMAC_MAX_TRANSMIT_DELAY) {
eth->tx_endpoint_delay += ETHMAC_TRANSMIT_DELAY;
}
usb_reset_endpoint(&eth->usb, eth->bulk_out_addr);
}
zx_status_t status = usb_req_list_add_tail(&eth->free_write_reqs, request,
eth->parent_req_size);
ZX_DEBUG_ASSERT(status == ZX_OK);
mtx_unlock(&eth->mutex);
}
static void rndishost_free(rndishost_t* eth) {
usb_request_t* txn;
while ((txn = usb_req_list_remove_head(&eth->free_read_reqs, eth->parent_req_size)) != NULL) {
usb_request_release(txn);
}
while ((txn = usb_req_list_remove_head(&eth->free_write_reqs, eth->parent_req_size)) != NULL) {
usb_request_release(txn);
}
free(eth);
}
static zx_status_t rndishost_query(void* ctx, uint32_t options, ethmac_info_t* info) {
rndishost_t* eth = (rndishost_t*)ctx;
if (options) {
return ZX_ERR_INVALID_ARGS;
}
memset(info, 0, sizeof(*info));
info->mtu = eth->mtu;
memcpy(info->mac, eth->mac_addr, sizeof(eth->mac_addr));
info->netbuf_size = sizeof(ethmac_netbuf_t);
return ZX_OK;
}
static void rndishost_stop(void* ctx) {
rndishost_t* eth = (rndishost_t*)ctx;
mtx_lock(&eth->mutex);
eth->ifc.ops = NULL;
mtx_unlock(&eth->mutex);
}
static zx_status_t rndishost_start(void* ctx, const ethmac_ifc_t* ifc) {
rndishost_t* eth = (rndishost_t*)ctx;
zx_status_t status = ZX_OK;
mtx_lock(&eth->mutex);
if (eth->ifc.ops) {
status = ZX_ERR_ALREADY_BOUND;
} else {
eth->ifc = *ifc;
// TODO: Check that the device is online before sending ETH_STATUS_ONLINE.
ethmac_ifc_status(&eth->ifc, ETHMAC_STATUS_ONLINE);
}
mtx_unlock(&eth->mutex);
return status;
}
static zx_status_t rndishost_queue_tx(void* ctx, uint32_t options, ethmac_netbuf_t* netbuf) {
size_t length = netbuf->data_size;
rndishost_t* eth = (rndishost_t*)ctx;
const uint8_t* byte_data = netbuf->data_buffer;
zx_status_t status = ZX_OK;
mtx_lock(&eth->mutex);
usb_request_t* req = usb_req_list_remove_head(&eth->free_write_reqs, eth->parent_req_size);
if (req == NULL) {
zxlogf(TRACE, "rndishost dropped a packet\n");
status = ZX_ERR_NO_RESOURCES;
goto done;
}
if (length + sizeof(rndis_packet_header) > RNDIS_MAX_XFER_SIZE) {
zxlogf(TRACE, "rndishost attempted to send a packet that's too large.\n");
status = usb_req_list_add_tail(&eth->free_write_reqs, req, eth->parent_req_size);
ZX_DEBUG_ASSERT(status == ZX_OK);
status = ZX_ERR_INVALID_ARGS;
goto done;
}
rndis_packet_header header;
uint8_t* header_data = (uint8_t*)&header;
memset(header_data, 0, sizeof(rndis_packet_header));
header.msg_type = RNDIS_PACKET_MSG;
header.msg_length = sizeof(rndis_packet_header) + length;
// The offset should be given from the beginning of the data_offset field.
// So subtract 8 bytes for msg_type and msg_length.
header.data_offset = sizeof(rndis_packet_header) - 8;
header.data_length = length;
usb_request_copy_to(req, header_data, sizeof(rndis_packet_header), 0);
ssize_t bytes_copied = usb_request_copy_to(req, byte_data, length,
sizeof(rndis_packet_header));
req->header.length = sizeof(rndis_packet_header) + length;
if (bytes_copied < 0) {
printf("rndishost: failed to copy data into send txn (error %zd)\n", bytes_copied);
status = usb_req_list_add_tail(&eth->free_write_reqs, req, eth->parent_req_size);
ZX_DEBUG_ASSERT(status == ZX_OK);
goto done;
}
zx_nanosleep(zx_deadline_after(ZX_USEC(eth->tx_endpoint_delay)));
usb_request_queue(&eth->usb, req, rndis_write_complete, eth);
done:
mtx_unlock(&eth->mutex);
return status;
}
static void rndishost_unbind(void* ctx) {
rndishost_t* eth = (rndishost_t*)ctx;
device_remove(eth->zxdev);
}
static void rndishost_release(void* ctx) {
rndishost_t* eth = (rndishost_t*)ctx;
mtx_lock(&eth->mutex);
bool should_join = eth->thread_started;
mtx_unlock(&eth->mutex);
if (should_join) {
thrd_join(eth->thread, NULL);
}
rndishost_free(eth);
}
static zx_status_t rndishost_set_param(void *ctx, uint32_t param, int32_t value, const void* data,
size_t data_size) {
return ZX_ERR_NOT_SUPPORTED;
}
static ethmac_protocol_ops_t ethmac_ops = {
.query = rndishost_query,
.stop = rndishost_stop,
.start = rndishost_start,
.queue_tx = rndishost_queue_tx,
.set_param = rndishost_set_param,
};
static zx_protocol_device_t rndishost_device_proto = {
.version = DEVICE_OPS_VERSION,
.unbind = rndishost_unbind,
.release = rndishost_release,
};
static int rndis_start_thread(void* arg) {
rndishost_t* eth = (rndishost_t*)arg;
void* buf = malloc(RNDIS_BUFFER_SIZE);
memset(buf, 0, RNDIS_BUFFER_SIZE);
// Send an initialization message to the device.
rndis_init* init = buf;
init->msg_type = RNDIS_INITIALIZE_MSG;
init->msg_length = sizeof(rndis_init);
init->major_version = RNDIS_MAJOR_VERSION;
init->minor_version = RNDIS_MINOR_VERSION;
init->max_xfer_size = RNDIS_MAX_XFER_SIZE;
zx_status_t status = rndis_command(eth, buf);
if (status < 0) {
zxlogf(ERROR, "rndishost bad status on initial message. %d\n", status);
goto fail;
}
rndis_init_complete* init_cmplt = buf;
if (!command_succeeded(buf, RNDIS_INITIALIZE_CMPLT, sizeof(*init_cmplt))) {
zxlogf(ERROR, "rndishost initialization failed.\n");
status = ZX_ERR_IO;
goto fail;
}
eth->mtu = init_cmplt->max_xfer_size;
// Check the PHY, this is optional and may not be supported by the device.
uint32_t* phy;
memset(buf, 0, RNDIS_BUFFER_SIZE);
rndis_query* query = buf;
query->msg_type = RNDIS_QUERY_MSG;
query->msg_length = sizeof(rndis_query) + sizeof(*phy);
query->oid = OID_GEN_PHYSICAL_MEDIUM;
query->info_buffer_length = sizeof(*phy);
query->info_buffer_offset = RNDIS_QUERY_BUFFER_OFFSET;
status = rndis_command(eth, buf);
if (status == ZX_OK) {
// TODO: Do something with this information.
rndis_query_complete* phy_query_cmplt = buf;
if (command_succeeded(buf,
RNDIS_QUERY_CMPLT,
sizeof(*phy_query_cmplt) + phy_query_cmplt->info_buffer_length)) {
// The offset given in the reply is from the beginning of the request_id
// field. So, add 8 for the msg_type and msg_length fields.
phy = buf + 8 + phy_query_cmplt->info_buffer_offset;
}
}
// Query the device for a MAC address.
memset(buf, 0, RNDIS_BUFFER_SIZE);
query->msg_type = RNDIS_QUERY_MSG;
query->msg_length = sizeof(rndis_query) + 48;
query->oid = OID_802_3_PERMANENT_ADDRESS;
query->info_buffer_length = 48;
query->info_buffer_offset = RNDIS_QUERY_BUFFER_OFFSET;
status = rndis_command(eth, buf);
if (status < 0) {
zxlogf(ERROR, "Couldn't get device physical address\n");
goto fail;
}
rndis_query_complete* mac_query_cmplt = buf;
if (!command_succeeded(buf, RNDIS_QUERY_CMPLT, sizeof(*mac_query_cmplt) +
mac_query_cmplt->info_buffer_length)) {
zxlogf(ERROR, "rndishost MAC query failed.\n");
status = ZX_ERR_IO;
goto fail;
}
uint8_t* mac_addr = buf + 8 + mac_query_cmplt->info_buffer_offset;
memcpy(eth->mac_addr, mac_addr, sizeof(eth->mac_addr));
zxlogf(INFO, "rndishost MAC address: %02x:%02x:%02x:%02x:%02x:%02x\n",
eth->mac_addr[0], eth->mac_addr[1], eth->mac_addr[2],
eth->mac_addr[3], eth->mac_addr[4], eth->mac_addr[5]);
// Enable data transfers
memset(buf, 0, RNDIS_BUFFER_SIZE);
rndis_set* set = buf;
set->msg_type = RNDIS_SET_MSG;
set->msg_length = sizeof(rndis_set) + 4; // 4 bytes for the filter
set->oid = OID_GEN_CURRENT_PACKET_FILTER;
set->info_buffer_length = 4;
// Offset should begin at oid, so subtract 8 bytes for msg_type and msg_length.
set->info_buffer_offset = sizeof(rndis_set) - 8;
uint8_t* filter = buf + sizeof(rndis_set);
*filter = RNDIS_PACKET_TYPE_DIRECTED |
RNDIS_PACKET_TYPE_BROADCAST |
RNDIS_PACKET_TYPE_ALL_MULTICAST |
RNDIS_PACKET_TYPE_PROMISCUOUS;
status = rndis_command(eth, buf);
if (status < 0) {
zxlogf(ERROR, "Couldn't set the packet filter.\n");
goto fail;
}
if (!command_succeeded(buf, RNDIS_SET_CMPLT, sizeof(rndis_set_complete))) {
zxlogf(ERROR, "rndishost set filter failed.\n");
status = ZX_ERR_IO;
goto fail;
}
// Queue read requests
mtx_lock(&eth->mutex);
usb_request_t* txn;
while ((txn = usb_req_list_remove_head(&eth->free_read_reqs, eth->parent_req_size)) != NULL) {
usb_request_queue(&eth->usb, txn, rndis_read_complete, eth);
}
mtx_unlock(&eth->mutex);
free(buf);
device_make_visible(eth->zxdev);
return ZX_OK;
fail:
free(buf);
device_remove(eth->zxdev);
return status;
}
static zx_status_t rndishost_bind(void* ctx, zx_device_t* device) {
usb_protocol_t usb;
zx_status_t status = device_get_protocol(device, ZX_PROTOCOL_USB_OLD, &usb);
if (status != ZX_OK) {
return status;
}
// Find our endpoints.
usb_desc_iter_t iter;
status = usb_desc_iter_init(&usb, &iter);
if (status < 0) {
return status;
}
// We should have two interfaces: the CDC classified interface the bulk in
// and out endpoints, and the RNDIS interface for control. The RNDIS
// interface will be classified as USB_CLASS_WIRELESS when the device is
// used for tethering.
// TODO: Figure out how to handle other RNDIS use cases.
usb_interface_descriptor_t* intf = usb_desc_iter_next_interface(&iter, false);
uint8_t bulk_in_addr = 0;
uint8_t bulk_out_addr = 0;
uint8_t intr_addr = 0;
uint8_t control_intf = 0;
while (intf) {
if (intf->bInterfaceClass == USB_CLASS_WIRELESS) {
control_intf = intf->bInterfaceNumber;
if (intf->bNumEndpoints != 1) {
usb_desc_iter_release(&iter);
return ZX_ERR_NOT_SUPPORTED;
}
usb_endpoint_descriptor_t* endp = usb_desc_iter_next_endpoint(&iter);
while (endp) {
if (usb_ep_direction(endp) == USB_ENDPOINT_IN &&
usb_ep_type(endp) == USB_ENDPOINT_INTERRUPT) {
intr_addr = endp->bEndpointAddress;
}
endp = usb_desc_iter_next_endpoint(&iter);
}
} else if (intf->bInterfaceClass == USB_CLASS_CDC) {
if (intf->bNumEndpoints != 2) {
usb_desc_iter_release(&iter);
return ZX_ERR_NOT_SUPPORTED;
}
usb_endpoint_descriptor_t* endp = usb_desc_iter_next_endpoint(&iter);
while (endp) {
if (usb_ep_direction(endp) == USB_ENDPOINT_OUT) {
if (usb_ep_type(endp) == USB_ENDPOINT_BULK) {
bulk_out_addr = endp->bEndpointAddress;
}
} else if (usb_ep_direction(endp) == USB_ENDPOINT_IN) {
if (usb_ep_type(endp) == USB_ENDPOINT_BULK) {
bulk_in_addr = endp->bEndpointAddress;
}
}
endp = usb_desc_iter_next_endpoint(&iter);
}
} else {
usb_desc_iter_release(&iter);
return ZX_ERR_NOT_SUPPORTED;
}
intf = usb_desc_iter_next_interface(&iter, false);
}
usb_desc_iter_release(&iter);
if (!bulk_in_addr || !bulk_out_addr || !intr_addr) {
zxlogf(ERROR, "rndishost couldn't find endpoints\n");
return ZX_ERR_NOT_SUPPORTED;
}
rndishost_t* eth = calloc(1, sizeof(rndishost_t));
if (!eth) {
return ZX_ERR_NO_MEMORY;
}
list_initialize(&eth->free_read_reqs);
list_initialize(&eth->free_write_reqs);
mtx_init(&eth->mutex, mtx_plain);
eth->usb_zxdev = device;
eth->control_intf = control_intf;
eth->bulk_in_addr = bulk_in_addr;
eth->bulk_out_addr = bulk_out_addr;
eth->ifc.ops = NULL;
memcpy(&eth->usb, &usb, sizeof(eth->usb));
eth->parent_req_size = usb_get_request_size(&eth->usb);
uint64_t req_size = eth->parent_req_size + sizeof(usb_req_internal_t);
for (int i = 0; i < READ_REQ_COUNT; i++) {
usb_request_t* req;
zx_status_t alloc_result = usb_request_alloc(&req, RNDIS_BUFFER_SIZE, bulk_in_addr,
req_size);
if (alloc_result != ZX_OK) {
status = alloc_result;
goto fail;
}
status = usb_req_list_add_head(&eth->free_read_reqs, req, eth->parent_req_size);
ZX_DEBUG_ASSERT(status == ZX_OK);
}
for (int i = 0; i < WRITE_REQ_COUNT; i++) {
usb_request_t* req;
// TODO: Allocate based on mtu.
zx_status_t alloc_result = usb_request_alloc(&req, RNDIS_BUFFER_SIZE, bulk_out_addr,
req_size);
if (alloc_result != ZX_OK) {
status = alloc_result;
goto fail;
}
status = usb_req_list_add_head(&eth->free_write_reqs, req, eth->parent_req_size);
ZX_DEBUG_ASSERT(status == ZX_OK);
}
device_add_args_t args = {
.version = DEVICE_ADD_ARGS_VERSION,
.name = "rndishost",
.ctx = eth,
.ops = &rndishost_device_proto,
.proto_id = ZX_PROTOCOL_ETHMAC,
.proto_ops = &ethmac_ops,
.flags = DEVICE_ADD_INVISIBLE,
};
mtx_lock(&eth->mutex);
status = device_add(eth->usb_zxdev, &args, &eth->zxdev);
if (status < 0) {
mtx_unlock(&eth->mutex);
zxlogf(ERROR, "rndishost: failed to create device: %d\n", status);
goto fail;
}
eth->thread_started = true;
int ret = thrd_create_with_name(&eth->thread, rndis_start_thread,
eth, "rndishost_start_thread");
if (ret != thrd_success) {
eth->thread_started = false;
mtx_unlock(&eth->mutex);
device_remove(eth->zxdev);
return ZX_ERR_NO_RESOURCES;
}
mtx_unlock(&eth->mutex);
return ZX_OK;
fail:
zxlogf(ERROR, "rndishost_bind failed: %d\n", status);
rndishost_free(eth);
return status;
}
static zx_driver_ops_t rndis_driver_ops = {
.version = DRIVER_OPS_VERSION,
.bind = rndishost_bind,
};
// TODO: Make sure we can bind to all RNDIS use cases. USB_CLASS_WIRELESS only
// covers the tethered device case.
// clang-format off
ZIRCON_DRIVER_BEGIN(rndishost, rndis_driver_ops, "zircon", "0.1", 4)
BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_USB_OLD),
BI_ABORT_IF(NE, BIND_USB_CLASS, USB_CLASS_WIRELESS),
BI_ABORT_IF(NE, BIND_USB_SUBCLASS, RNDIS_SUBCLASS),
BI_MATCH_IF(EQ, BIND_USB_PROTOCOL, RNDIS_PROTOCOL),
ZIRCON_DRIVER_END(rndishost)