| // 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/ethernet/drivers/rndis-function/rndis_function.h" |
| |
| #include <zircon/status.h> |
| |
| #include <algorithm> |
| |
| #include <ddk/debug.h> |
| #include <ddk/metadata.h> |
| #include <ddktl/protocol/usb/function.h> |
| #include <usb/request-cpp.h> |
| |
| #include "src/connectivity/ethernet/drivers/rndis-function/rndis_function_bind.h" |
| #include "src/connectivity/ethernet/lib/rndis/rndis.h" |
| |
| size_t RndisFunction::UsbFunctionInterfaceGetDescriptorsSize() { return sizeof(descriptors_); } |
| |
| void RndisFunction::UsbFunctionInterfaceGetDescriptors(void* out_descriptors_buffer, |
| size_t descriptors_size, |
| size_t* out_descriptors_actual) { |
| memcpy(out_descriptors_buffer, &descriptors_, |
| std::min(descriptors_size, UsbFunctionInterfaceGetDescriptorsSize())); |
| *out_descriptors_actual = UsbFunctionInterfaceGetDescriptorsSize(); |
| } |
| |
| std::optional<std::vector<uint8_t>> RndisFunction::QueryOid(uint32_t oid, void* input, |
| size_t length) { |
| zxlogf(INFO, "Query OID %x", oid); |
| std::optional<std::vector<uint8_t>> response; |
| switch (oid) { |
| case OID_GEN_SUPPORTED_LIST: { |
| static constexpr uint32_t supported[] = { |
| // General OIDs. |
| OID_GEN_SUPPORTED_LIST, |
| OID_GEN_HARDWARE_STATUS, |
| OID_GEN_MEDIA_SUPPORTED, |
| OID_GEN_MEDIA_IN_USE, |
| OID_GEN_MAXIMUM_FRAME_SIZE, |
| OID_GEN_LINK_SPEED, |
| OID_GEN_TRANSMIT_BLOCK_SIZE, |
| OID_GEN_RECEIVE_BLOCK_SIZE, |
| OID_GEN_VENDOR_ID, |
| OID_GEN_VENDOR_DESCRIPTION, |
| OID_GEN_VENDOR_DRIVER_VERSION, |
| OID_GEN_CURRENT_PACKET_FILTER, |
| OID_GEN_MAXIMUM_TOTAL_SIZE, |
| OID_GEN_PHYSICAL_MEDIUM, |
| OID_GEN_MEDIA_CONNECT_STATUS, |
| |
| // General statistic OIDs. |
| OID_GEN_XMIT_OK, |
| OID_GEN_RCV_OK, |
| OID_GEN_XMIT_ERROR, |
| OID_GEN_RCV_ERROR, |
| OID_GEN_RCV_NO_BUFFER, |
| |
| // 802.3 OIDs. |
| OID_802_3_PERMANENT_ADDRESS, |
| OID_802_3_CURRENT_ADDRESS, |
| OID_802_3_MULTICAST_LIST, |
| OID_802_3_MAXIMUM_LIST_SIZE, |
| }; |
| std::vector<uint8_t> buffer(sizeof(supported)); |
| memcpy(buffer.data(), &supported, sizeof(supported)); |
| response.emplace(buffer); |
| break; |
| } |
| case OID_GEN_HARDWARE_STATUS: { |
| uint32_t status = RNDIS_HW_STATUS_READY; |
| std::vector<uint8_t> buffer(sizeof(status)); |
| memcpy(buffer.data(), &status, sizeof(status)); |
| response.emplace(buffer); |
| break; |
| } |
| case OID_GEN_TRANSMIT_BLOCK_SIZE: |
| case OID_GEN_RECEIVE_BLOCK_SIZE: |
| case OID_GEN_MAXIMUM_FRAME_SIZE: { |
| uint32_t frame_size = kMtu - sizeof(rndis_packet_header); |
| response.emplace( |
| std::vector<uint8_t>(reinterpret_cast<uint8_t*>(&frame_size), |
| reinterpret_cast<uint8_t*>(&frame_size) + sizeof(uint32_t))); |
| break; |
| } |
| case OID_GEN_LINK_SPEED: { |
| static_assert(sizeof(link_speed_) == sizeof(uint32_t)); |
| response.emplace( |
| std::vector<uint8_t>(reinterpret_cast<uint8_t*>(&link_speed_), |
| reinterpret_cast<uint8_t*>(&link_speed_) + sizeof(uint32_t))); |
| break; |
| } |
| case OID_GEN_VENDOR_ID: { |
| static_assert(sizeof(kVendorId) == sizeof(uint32_t)); |
| response.emplace( |
| std::vector<uint8_t>(reinterpret_cast<const uint8_t*>(&kVendorId), |
| reinterpret_cast<const uint8_t*>(&kVendorId) + sizeof(uint32_t))); |
| break; |
| } |
| case OID_GEN_VENDOR_DESCRIPTION: { |
| std::vector<uint8_t> buffer(sizeof(kVendorDescription)); |
| memcpy(buffer.data(), &kVendorDescription, sizeof(kVendorDescription)); |
| response.emplace(buffer); |
| break; |
| } |
| case OID_GEN_VENDOR_DRIVER_VERSION: { |
| static_assert(sizeof(kVendorDriverVersionMajor) == sizeof(uint16_t)); |
| static_assert(sizeof(kVendorDriverVersionMinor) == sizeof(uint16_t)); |
| uint32_t version = (kVendorDriverVersionMajor << 16) | kVendorDriverVersionMinor; |
| response.emplace( |
| std::vector<uint8_t>(reinterpret_cast<uint8_t*>(&version), |
| reinterpret_cast<uint8_t*>(&version) + sizeof(uint32_t))); |
| break; |
| } |
| case OID_GEN_MEDIA_CONNECT_STATUS: { |
| uint32_t status = RNDIS_STATUS_MEDIA_CONNECT; |
| response.emplace( |
| std::vector<uint8_t>(reinterpret_cast<uint8_t*>(&status), |
| reinterpret_cast<uint8_t*>(&status) + sizeof(uint32_t))); |
| break; |
| } |
| case OID_GEN_MEDIA_SUPPORTED: |
| case OID_GEN_MEDIA_IN_USE: |
| case OID_GEN_PHYSICAL_MEDIUM: { |
| uint32_t medium = RNDIS_MEDIUM_802_3; |
| response.emplace( |
| std::vector<uint8_t>(reinterpret_cast<uint8_t*>(&medium), |
| reinterpret_cast<uint8_t*>(&medium) + sizeof(uint32_t))); |
| break; |
| } |
| case OID_GEN_MAXIMUM_TOTAL_SIZE: { |
| uint32_t total_size = RNDIS_MAX_DATA_SIZE; |
| response.emplace( |
| std::vector<uint8_t>(reinterpret_cast<uint8_t*>(&total_size), |
| reinterpret_cast<uint8_t*>(&total_size) + sizeof(uint32_t))); |
| break; |
| } |
| |
| case OID_802_3_PERMANENT_ADDRESS: |
| case OID_802_3_CURRENT_ADDRESS: { |
| std::vector<uint8_t> buffer; |
| buffer.insert(buffer.end(), mac_addr_.begin(), mac_addr_.end()); |
| // Make the host and device addresses different so packets are routed correctly. |
| buffer[5] ^= 1; |
| response.emplace(buffer); |
| break; |
| } |
| case OID_802_3_MULTICAST_LIST: { |
| static constexpr uint32_t list[] = {0xE0000000}; |
| std::vector<uint8_t> buffer(sizeof(list)); |
| memcpy(buffer.data(), &list, sizeof(list)); |
| response.emplace(buffer); |
| break; |
| } |
| case OID_802_3_MAXIMUM_LIST_SIZE: { |
| uint32_t list_size = 1; |
| response.emplace( |
| std::vector<uint8_t>(reinterpret_cast<uint8_t*>(&list_size), |
| reinterpret_cast<uint8_t*>(&list_size) + sizeof(uint32_t))); |
| break; |
| } |
| |
| // These stats are from the perspective of the host, so transmit and receive are flipped. |
| case OID_GEN_XMIT_OK: { |
| static_assert(sizeof(receive_ok_) == sizeof(uint32_t)); |
| response.emplace( |
| std::vector<uint8_t>(reinterpret_cast<uint8_t*>(&receive_ok_), |
| reinterpret_cast<uint8_t*>(&receive_ok_) + sizeof(uint32_t))); |
| break; |
| } |
| case OID_GEN_RCV_OK: { |
| static_assert(sizeof(transmit_ok_) == sizeof(uint32_t)); |
| response.emplace( |
| std::vector<uint8_t>(reinterpret_cast<uint8_t*>(&transmit_ok_), |
| reinterpret_cast<uint8_t*>(&transmit_ok_) + sizeof(uint32_t))); |
| |
| break; |
| } |
| case OID_GEN_XMIT_ERROR: { |
| static_assert(sizeof(receive_errors_) == sizeof(uint32_t)); |
| response.emplace( |
| std::vector<uint8_t>(reinterpret_cast<uint8_t*>(&receive_errors_), |
| reinterpret_cast<uint8_t*>(&receive_errors_) + sizeof(uint32_t))); |
| break; |
| } |
| case OID_GEN_RCV_ERROR: { |
| static_assert(sizeof(transmit_errors_) == sizeof(uint32_t)); |
| response.emplace( |
| std::vector<uint8_t>(reinterpret_cast<uint8_t*>(&transmit_errors_), |
| reinterpret_cast<uint8_t*>(&transmit_errors_) + sizeof(uint32_t))); |
| break; |
| } |
| case OID_GEN_RCV_NO_BUFFER: { |
| static_assert(sizeof(transmit_no_buffer_) == sizeof(uint32_t)); |
| response.emplace(std::vector<uint8_t>( |
| reinterpret_cast<uint8_t*>(&transmit_no_buffer_), |
| reinterpret_cast<uint8_t*>(&transmit_no_buffer_) + sizeof(uint32_t))); |
| break; |
| } |
| |
| default: |
| break; |
| } |
| if (!response.has_value()) { |
| zxlogf(WARNING, "Did not generate a response to OID query %x.", oid); |
| } |
| return response; |
| } |
| |
| zx_status_t RndisFunction::SetOid(uint32_t oid, const uint8_t* buffer, size_t length) { |
| switch (oid) { |
| case OID_GEN_CURRENT_PACKET_FILTER: { |
| bool indicate_status = false; |
| { |
| fbl::AutoLock lock(&lock_); |
| rndis_ready_ = true; |
| if (ifc_.is_valid()) { |
| ifc_.Status(ETHERNET_STATUS_ONLINE); |
| // Call IndicateConnectionStatus outside the lock. |
| indicate_status = true; |
| } |
| |
| std::optional<usb::Request<>> pending_request; |
| size_t request_length = usb::Request<>::RequestSize(usb_request_size_); |
| while ((pending_request = free_read_pool_.Get(request_length))) { |
| pending_requests_++; |
| function_.RequestQueue(pending_request->take(), &read_request_complete_); |
| } |
| } |
| |
| if (indicate_status) { |
| zxlogf(ERROR, "IndidcateStatus from SetOid"); |
| IndicateConnectionStatus(true); |
| } else { |
| zxlogf(ERROR, "No IndidcateStatus from SetOid"); |
| } |
| return ZX_OK; |
| } |
| case OID_802_3_MULTICAST_LIST: { |
| // Ignore |
| zxlogf(WARNING, "Host set multicast list (buffer len %zd).", length); |
| return ZX_OK; |
| } |
| default: |
| zxlogf(WARNING, "Unhandled OID: %x", oid); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| } |
| |
| std::vector<uint8_t> InvalidMessageResponse(const void* invalid_data, size_t size) { |
| zxlogf(WARNING, "Host sent an invalid message."); |
| |
| std::vector<uint8_t> buffer(sizeof(rndis_indicate_status) + sizeof(rndis_diagnostic_info) + size); |
| |
| rndis_indicate_status status{ |
| .msg_type = RNDIS_INDICATE_STATUS_MSG, |
| .msg_length = static_cast<uint32_t>(buffer.size()), |
| .status = RNDIS_STATUS_INVALID_DATA, |
| .status_buffer_length = static_cast<uint32_t>(size), |
| .status_buffer_offset = static_cast<uint32_t>(sizeof(rndis_indicate_status) - |
| offsetof(rndis_indicate_status, status)), |
| }; |
| |
| rndis_diagnostic_info info{ |
| .diagnostic_status = RNDIS_STATUS_INVALID_DATA, |
| // TODO: This is supposed to an offset to the error in |invalid_data|. |
| .error_offset = 0, |
| }; |
| |
| memcpy(buffer.data(), &status, sizeof(status)); |
| uintptr_t offset = sizeof(status); |
| memcpy(buffer.data() + offset, &info, sizeof(info)); |
| offset += sizeof(info); |
| memcpy(buffer.data() + offset, invalid_data, size); |
| |
| return buffer; |
| } |
| |
| std::vector<uint8_t> InitResponse(uint32_t request_id, uint32_t status) { |
| rndis_init_complete response{.msg_type = RNDIS_INITIALIZE_CMPLT, |
| .msg_length = sizeof(rndis_init_complete), |
| .request_id = request_id, |
| .status = status, |
| .major_version = RNDIS_MAJOR_VERSION, |
| .minor_version = RNDIS_MINOR_VERSION, |
| .device_flags = RNDIS_DF_CONNECTIONLESS, |
| .medium = RNDIS_MEDIUM_802_3, |
| .max_packets_per_xfer = 1, |
| .max_xfer_size = RNDIS_MAX_XFER_SIZE, |
| .packet_alignment = 0, |
| .reserved0 = 0, |
| .reserved1 = 0}; |
| |
| std::vector<uint8_t> buffer(sizeof(rndis_init_complete)); |
| memcpy(buffer.data(), &response, sizeof(rndis_init_complete)); |
| return buffer; |
| } |
| |
| std::vector<uint8_t> ResetResponse(uint32_t status) { |
| rndis_reset_complete response{.msg_type = RNDIS_RESET_CMPLT, |
| .msg_length = sizeof(rndis_reset_complete), |
| .status = status, |
| .addressing_reset = 1}; |
| |
| std::vector<uint8_t> buffer(sizeof(rndis_reset_complete)); |
| memcpy(buffer.data(), &response, sizeof(rndis_reset_complete)); |
| return buffer; |
| } |
| |
| std::vector<uint8_t> QueryResponse(uint32_t request_id, |
| const std::optional<std::vector<uint8_t>>& oid_response) { |
| size_t buffer_size = sizeof(rndis_query_complete); |
| if (oid_response.has_value()) { |
| buffer_size += oid_response->size(); |
| } |
| std::vector<uint8_t> buffer(buffer_size); |
| |
| rndis_query_complete response; |
| response.msg_type = RNDIS_QUERY_CMPLT; |
| response.msg_length = static_cast<uint32_t>(buffer.size()); |
| response.request_id = request_id; |
| |
| if (oid_response.has_value()) { |
| response.status = RNDIS_STATUS_SUCCESS; |
| response.info_buffer_offset = |
| sizeof(rndis_query_complete) - offsetof(rndis_query_complete, request_id); |
| response.info_buffer_length = static_cast<uint32_t>(oid_response->size()); |
| |
| memcpy(buffer.data() + sizeof(rndis_query_complete), oid_response->data(), |
| oid_response->size()); |
| } else { |
| response.status = RNDIS_STATUS_NOT_SUPPORTED; |
| response.info_buffer_offset = 0; |
| response.info_buffer_length = 0; |
| } |
| |
| memcpy(buffer.data(), &response, sizeof(rndis_query_complete)); |
| |
| return buffer; |
| } |
| |
| std::vector<uint8_t> SetResponse(uint32_t request_id, uint32_t status) { |
| rndis_set_complete response{ |
| .msg_type = RNDIS_SET_CMPLT, |
| .msg_length = static_cast<uint32_t>(sizeof(rndis_set_complete)), |
| .request_id = request_id, |
| .status = status, |
| }; |
| |
| std::vector<uint8_t> buffer(sizeof(rndis_set_complete)); |
| memcpy(buffer.data(), &response, sizeof(rndis_set_complete)); |
| return buffer; |
| } |
| |
| std::vector<uint8_t> KeepaliveResponse(uint32_t request_id, uint32_t status) { |
| rndis_header_complete response{ |
| .msg_type = RNDIS_KEEPALIVE_CMPLT, |
| .msg_length = sizeof(rndis_header_complete), |
| .request_id = request_id, |
| .status = status, |
| }; |
| |
| std::vector<uint8_t> buffer(sizeof(rndis_header_complete)); |
| memcpy(buffer.data(), &response, sizeof(rndis_header_complete)); |
| return buffer; |
| } |
| |
| zx_status_t RndisFunction::HandleCommand(const void* buffer, size_t size) { |
| if (size < sizeof(rndis_header)) { |
| fbl::AutoLock lock(&lock_); |
| control_responses_.push(InvalidMessageResponse(buffer, size)); |
| NotifyLocked(); |
| return ZX_OK; |
| } |
| |
| auto header = static_cast<const rndis_header*>(buffer); |
| std::optional<std::vector<uint8_t>> response; |
| |
| switch (header->msg_type) { |
| case RNDIS_INITIALIZE_MSG: { |
| if (size < sizeof(rndis_init)) { |
| response.emplace(InvalidMessageResponse(buffer, size)); |
| break; |
| } |
| |
| auto init = static_cast<const rndis_init*>(buffer); |
| if (init->major_version != RNDIS_MAJOR_VERSION) { |
| zxlogf(WARNING, "Invalid RNDIS major version. Expected %x, got %x.", RNDIS_MAJOR_VERSION, |
| init->major_version); |
| response.emplace(InitResponse(init->request_id, RNDIS_STATUS_NOT_SUPPORTED)); |
| } else if (init->minor_version != RNDIS_MINOR_VERSION) { |
| zxlogf(WARNING, "Invalid RNDIS minor version. Expected %x, got %x.", RNDIS_MINOR_VERSION, |
| init->minor_version); |
| response.emplace(InitResponse(init->request_id, RNDIS_STATUS_NOT_SUPPORTED)); |
| } |
| |
| response.emplace(InitResponse(init->request_id, RNDIS_STATUS_SUCCESS)); |
| break; |
| } |
| case RNDIS_QUERY_MSG: { |
| if (size < sizeof(rndis_query)) { |
| response.emplace(InvalidMessageResponse(buffer, size)); |
| break; |
| } |
| |
| auto query = static_cast<const rndis_query*>(buffer); |
| auto oid_response = QueryOid(query->oid, nullptr, 0); |
| response.emplace(QueryResponse(query->request_id, oid_response)); |
| break; |
| } |
| case RNDIS_SET_MSG: { |
| if (size < sizeof(rndis_set)) { |
| response.emplace(InvalidMessageResponse(buffer, size)); |
| break; |
| } |
| |
| auto set = static_cast<const rndis_set*>(buffer); |
| if (set->info_buffer_length > RNDIS_SET_INFO_BUFFER_LENGTH) { |
| response.emplace(SetResponse(set->request_id, RNDIS_STATUS_INVALID_DATA)); |
| break; |
| } |
| |
| size_t offset = offsetof(rndis_set, request_id) + set->info_buffer_offset; |
| if (offset + set->info_buffer_length > size) { |
| response.emplace(SetResponse(set->request_id, RNDIS_STATUS_INVALID_DATA)); |
| break; |
| } |
| |
| zx_status_t status = SetOid(set->oid, reinterpret_cast<const uint8_t*>(buffer) + offset, |
| set->info_buffer_length); |
| |
| uint32_t rndis_status = RNDIS_STATUS_SUCCESS; |
| if (status == ZX_ERR_NOT_SUPPORTED) { |
| rndis_status = RNDIS_STATUS_NOT_SUPPORTED; |
| } else if (status != ZX_OK) { |
| rndis_status = RNDIS_STATUS_FAILURE; |
| } |
| response.emplace(SetResponse(set->request_id, rndis_status)); |
| break; |
| } |
| case RNDIS_KEEPALIVE_MSG: |
| response.emplace(KeepaliveResponse(header->request_id, RNDIS_STATUS_SUCCESS)); |
| break; |
| case RNDIS_HALT_MSG: { |
| zx_status_t status = Halt(); |
| if (status != ZX_OK) { |
| zxlogf(WARNING, "Failed to handle HALT command: %s", zx_status_get_string(status)); |
| } |
| break; |
| } |
| case RNDIS_RESET_MSG: |
| Reset(); |
| response.emplace(ResetResponse(RNDIS_STATUS_SUCCESS)); |
| break; |
| case RNDIS_PACKET_MSG: |
| // The should only send packets on the data channel. |
| // TODO: How should we respond to this? |
| zxlogf(WARNING, "Host sent a data packet on the control channel."); |
| break; |
| default: |
| zxlogf(WARNING, "Host sent an unrecognised message: %x.", header->msg_type); |
| response.emplace(InvalidMessageResponse(buffer, size)); |
| break; |
| } |
| |
| if (!response.has_value()) { |
| return ZX_OK; |
| } |
| fbl::AutoLock lock(&lock_); |
| control_responses_.push(std::move(response.value())); |
| NotifyLocked(); |
| return ZX_OK; |
| } |
| |
| zx_status_t ErrorResponse(void* buffer, size_t size, size_t* actual) { |
| if (size < 1) { |
| *actual = 0; |
| return ZX_ERR_BUFFER_TOO_SMALL; |
| } |
| // From |
| // https://docs.microsoft.com/en-au/windows-hardware/drivers/network/control-channel-characteristics: |
| // If for some reason the device receives a GET_ENCAPSULATED_RESPONSE and is unable to respond |
| // with a valid data on the Control endpoint, then it should return a one-byte packet set to |
| // 0x00, rather than stalling the Control endpoint. |
| memset(buffer, 0x00, 1); |
| *actual = 1; |
| return ZX_OK; |
| } |
| |
| zx_status_t RndisFunction::HandleResponse(void* buffer, size_t size, size_t* actual) { |
| fbl::AutoLock lock(&lock_); |
| if (control_responses_.empty()) { |
| zxlogf(WARNING, "Host tried to read a control response when none was available."); |
| return ErrorResponse(buffer, size, actual); |
| } |
| |
| auto packet = control_responses_.front(); |
| if (size < packet.size()) { |
| zxlogf(WARNING, |
| "Buffer too small to read a control response. Packet size is %zd but the buffer is %zd.", |
| packet.size(), size); |
| return ErrorResponse(buffer, size, actual); |
| } |
| |
| memcpy(buffer, packet.data(), packet.size()); |
| *actual = packet.size(); |
| |
| control_responses_.pop(); |
| return ZX_OK; |
| } |
| |
| zx_status_t RndisFunction::Halt() { |
| Reset(); |
| |
| fbl::AutoLock lock(&lock_); |
| zx_status_t status = function_.DisableEp(NotificationAddress()); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to disable control endpoint: %s", zx_status_get_string(status)); |
| return status; |
| } |
| status = function_.DisableEp(BulkInAddress()); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to disable data in endpoint: %s", zx_status_get_string(status)); |
| return status; |
| } |
| status = function_.DisableEp(BulkOutAddress()); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to disable data out endpoint: %s", zx_status_get_string(status)); |
| return status; |
| } |
| return ZX_OK; |
| } |
| |
| void RndisFunction::Reset() { |
| fbl::AutoLock lock(&lock_); |
| |
| function_.CancelAll(BulkInAddress()); |
| function_.CancelAll(BulkOutAddress()); |
| function_.CancelAll(NotificationAddress()); |
| |
| while (!control_responses_.empty()) { |
| control_responses_.pop(); |
| } |
| |
| rndis_ready_ = false; |
| link_speed_ = 0; |
| if (ifc_.is_valid()) { |
| ifc_.Status(0); |
| } |
| } |
| |
| zx_status_t RndisFunction::UsbFunctionInterfaceControl(const usb_setup_t* setup, |
| const void* write_buffer, size_t write_size, |
| void* out_read_buffer, size_t read_size, |
| size_t* out_read_actual) { |
| if (setup->bmRequestType == (USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE) && |
| setup->bRequest == USB_CDC_SEND_ENCAPSULATED_COMMAND) { |
| if (out_read_actual) { |
| *out_read_actual = 0; |
| } |
| zx_status_t status = HandleCommand(write_buffer, write_size); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Error handling command: %s", zx_status_get_string(status)); |
| return status; |
| } |
| return ZX_OK; |
| } else if (setup->bmRequestType == (USB_DIR_IN | USB_TYPE_CLASS | USB_RECIP_INTERFACE) && |
| setup->bRequest == USB_CDC_GET_ENCAPSULATED_RESPONSE) { |
| size_t actual; |
| zx_status_t status = HandleResponse(out_read_buffer, read_size, &actual); |
| if (out_read_actual) { |
| *out_read_actual = actual; |
| } |
| return status; |
| } |
| |
| zxlogf(WARNING, "Unrecognised control interface transfer: bmRequestType %x bRequest %x", |
| setup->bmRequestType, setup->bRequest); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t RndisFunction::UsbFunctionInterfaceSetConfigured(bool configured, usb_speed_t speed) { |
| if (!configured) { |
| return Halt(); |
| } |
| |
| zx_status_t status = function_.ConfigEp(&descriptors_.notification_ep, nullptr); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to configure control endpoint: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| status = function_.ConfigEp(&descriptors_.in_ep, nullptr); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to configure bulk in endpoint: %s", zx_status_get_string(status)); |
| return status; |
| } |
| status = function_.ConfigEp(&descriptors_.out_ep, nullptr); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to configure bulk out endpoint: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| fbl::AutoLock lock(&lock_); |
| // Set the speed optimistically to roughly the capacity of the bus. We report link speed in |
| // units of 100bps. |
| switch (speed) { |
| case USB_SPEED_LOW: |
| link_speed_ = 15'000; |
| break; |
| case USB_SPEED_FULL: |
| link_speed_ = 120'000; |
| break; |
| case USB_SPEED_HIGH: |
| link_speed_ = 4'800'000; |
| break; |
| case USB_SPEED_SUPER: |
| link_speed_ = 50'000'000; |
| break; |
| default: |
| link_speed_ = 0; |
| break; |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t RndisFunction::UsbFunctionInterfaceSetInterface(uint8_t interface, |
| uint8_t alt_setting) { |
| return ZX_OK; |
| } |
| |
| zx_status_t RndisFunction::EthernetImplQuery(uint32_t options, ethernet_info_t* info) { |
| if (options) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| if (info) { |
| *info = {}; |
| info->mtu = kMtu - sizeof(rndis_packet_header); |
| memcpy(info->mac, mac_addr_.data(), mac_addr_.size()); |
| info->netbuf_size = eth::BorrowedOperation<>::OperationSize(sizeof(ethernet_netbuf_t)); |
| } |
| |
| return ZX_OK; |
| } |
| |
| void RndisFunction::EthernetImplStop() { |
| IndicateConnectionStatus(false); |
| fbl::AutoLock lock(&lock_); |
| ifc_.clear(); |
| } |
| |
| zx_status_t RndisFunction::EthernetImplStart(const ethernet_ifc_protocol_t* ifc) { |
| { |
| fbl::AutoLock lock(&lock_); |
| if (ifc_.is_valid()) { |
| return ZX_ERR_ALREADY_BOUND; |
| } |
| |
| ifc_ = ddk::EthernetIfcProtocolClient(ifc); |
| ifc_.Status(Online() ? ETHERNET_STATUS_ONLINE : 0); |
| } |
| IndicateConnectionStatus(true); |
| return ZX_OK; |
| } |
| |
| void RndisFunction::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)); |
| |
| size_t length = op.operation()->data_size; |
| if (length > kMtu - sizeof(rndis_packet_header)) { |
| op.Complete(ZX_ERR_INVALID_ARGS); |
| transmit_errors_ += 1; |
| return; |
| } |
| |
| fbl::AutoLock lock(&lock_); |
| if (!Online()) { |
| op.Complete(ZX_ERR_SHOULD_WAIT); |
| return; |
| } |
| |
| std::optional<usb::Request<>> request; |
| request = free_write_pool_.Get(usb::Request<>::RequestSize(usb_request_size_)); |
| if (!request) { |
| zxlogf(DEBUG, "No available TX requests"); |
| op.Complete(ZX_ERR_SHOULD_WAIT); |
| transmit_no_buffer_ += 1; |
| return; |
| } |
| pending_requests_++; |
| |
| rndis_packet_header header{}; |
| header.msg_type = RNDIS_PACKET_MSG; |
| header.msg_length = static_cast<uint32_t>(sizeof(header) + length); |
| header.data_offset = sizeof(header) - offsetof(rndis_packet_header, data_offset); |
| header.data_length = static_cast<uint32_t>(length); |
| |
| size_t offset = 0; |
| ssize_t copied = request->CopyTo(&header, sizeof(header), 0); |
| if (copied < 0) { |
| zxlogf(ERROR, "Failed to copy TX header: %zd", copied); |
| op.Complete(ZX_ERR_INTERNAL); |
| transmit_errors_ += 1; |
| free_write_pool_.Add(*std::move(request)); |
| pending_requests_--; |
| return; |
| } |
| offset += copied; |
| |
| request->CopyTo(op.operation()->data_buffer, length, offset); |
| if (copied < 0) { |
| zxlogf(ERROR, "Failed to copy TX data: %zd", copied); |
| op.Complete(ZX_ERR_INTERNAL); |
| transmit_errors_ += 1; |
| free_write_pool_.Add(*std::move(request)); |
| pending_requests_--; |
| return; |
| } |
| request->request()->header.length = sizeof(header) + length; |
| |
| function_.RequestQueue(request->take(), &write_request_complete_); |
| op.Complete(ZX_OK); |
| transmit_ok_ += 1; |
| } |
| |
| zx_status_t RndisFunction::EthernetImplSetParam(uint32_t param, int32_t value, const void* data, |
| size_t data_size) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| void RndisFunction::ReceiveLocked(usb::Request<>& request) { |
| auto& response = request.request()->response; |
| |
| uint8_t* data; |
| zx_status_t status = request.Mmap(reinterpret_cast<void**>(&data)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to map RX data: %s", zx_status_get_string(status)); |
| receive_errors_ += 1; |
| return; |
| } |
| |
| size_t remaining = response.actual; |
| while (remaining >= sizeof(rndis_packet_header)) { |
| const auto* header = reinterpret_cast<const rndis_packet_header*>(data); |
| if (header->msg_type != RNDIS_PACKET_MSG) { |
| zxlogf(WARNING, "Received invalid packet type %x.", header->msg_type); |
| zxlogf(WARNING, "header length %lu.", request.request()->header.length); |
| zxlogf(WARNING, "actual size %lu.", response.actual); |
| zxlogf(WARNING, "header->msg_length %u.", header->msg_length); |
| zxlogf(WARNING, "header->data_offset %x.", header->data_offset); |
| receive_errors_ += 1; |
| return; |
| } |
| if (header->msg_length > remaining) { |
| zxlogf(WARNING, "Received packet with invalid length %u: only %zd bytes left in frame.", |
| header->msg_length, remaining); |
| receive_errors_ += 1; |
| return; |
| } |
| if (header->msg_length < sizeof(rndis_packet_header)) { |
| zxlogf(WARNING, "Received packet with invalid length %u: less than header length.", |
| header->msg_length); |
| receive_errors_ += 1; |
| return; |
| } |
| if (header->data_offset > header->msg_length - offsetof(rndis_packet_header, data_offset) || |
| header->data_length > |
| header->msg_length - offsetof(rndis_packet_header, data_offset) - header->data_offset) { |
| zxlogf(WARNING, "Received packet with invalid data."); |
| receive_errors_ += 1; |
| return; |
| } |
| |
| size_t offset = offsetof(rndis_packet_header, data_offset) + header->data_offset; |
| ifc_.Recv(data + offset, header->data_length, /*flags=*/0); |
| receive_ok_ += 1; |
| |
| if (header->oob_data_offset != 0) { |
| zxlogf(WARNING, "Packet contained unsupported out of band data."); |
| } |
| if (header->per_packet_info_offset != 0) { |
| zxlogf(WARNING, "Packet contained unsupported per packet information."); |
| } |
| |
| data = data + header->msg_length; |
| remaining -= header->msg_length; |
| } |
| } |
| |
| void RndisFunction::ReadComplete(usb_request_t* usb_request) { |
| fbl::AutoLock lock(&lock_); |
| usb::Request<> request(usb_request, usb_request_size_); |
| if (usb_request->response.status == ZX_ERR_IO_NOT_PRESENT) { |
| pending_requests_--; |
| if (shutting_down_) { |
| request.Release(); |
| if (pending_requests_ == 0) { |
| ShutdownComplete(); |
| } |
| return; |
| } |
| free_read_pool_.Add(std::move(request)); |
| return; |
| } |
| |
| if (usb_request->response.status == ZX_ERR_IO_REFUSED) { |
| zxlogf(ERROR, "ReadComplete refused"); |
| } else if (usb_request->response.status != ZX_OK) { |
| zxlogf(ERROR, "ReadComplete not ok"); |
| } else if (ifc_.is_valid()) { |
| ReceiveLocked(request); |
| } |
| |
| if (Online()) { |
| function_.RequestQueue(request.take(), &read_request_complete_); |
| } else { |
| if (shutting_down_) { |
| request.Release(); |
| pending_requests_--; |
| if (pending_requests_ == 0) { |
| ShutdownComplete(); |
| } |
| return; |
| } |
| free_read_pool_.Add(std::move(request)); |
| } |
| } |
| |
| void RndisFunction::NotifyLocked() { |
| std::optional<usb::Request<>> request; |
| request = free_notify_pool_.Get(usb::Request<>::RequestSize(usb_request_size_)); |
| if (!request) { |
| zxlogf(ERROR, "No notify request available"); |
| return; |
| } |
| pending_requests_++; |
| |
| rndis_notification notification{ |
| .notification = htole32(1), |
| .reserved = 0, |
| }; |
| |
| ssize_t copied = request->CopyTo(¬ification, sizeof(notification), 0); |
| if (copied < 0) { |
| zxlogf(ERROR, "Failed to copy notification"); |
| pending_requests_--; |
| free_notify_pool_.Add(*std::move(request)); |
| return; |
| } |
| request->request()->header.length = sizeof(notification); |
| function_.RequestQueue(request->take(), ¬ification_request_complete_); |
| } |
| |
| void RndisFunction::IndicateConnectionStatus(bool connected) { |
| fbl::AutoLock lock(&lock_); |
| if (!rndis_ready_) { |
| return; |
| } |
| |
| rndis_indicate_status status; |
| status.msg_type = RNDIS_INDICATE_STATUS_MSG; |
| status.msg_length = static_cast<uint32_t>(sizeof(rndis_indicate_status)); |
| if (connected) { |
| status.status = RNDIS_STATUS_MEDIA_CONNECT; |
| } else { |
| status.status = RNDIS_STATUS_MEDIA_DISCONNECT; |
| } |
| status.status_buffer_length = 0; |
| status.status_buffer_offset = 0; |
| |
| std::vector<uint8_t> buffer(sizeof(rndis_indicate_status)); |
| memcpy(buffer.data(), &status, sizeof(rndis_indicate_status)); |
| |
| control_responses_.push(std::move(buffer)); |
| NotifyLocked(); |
| } |
| |
| void RndisFunction::WriteComplete(usb_request_t* usb_request) { |
| usb::Request<> request(usb_request, usb_request_size_); |
| fbl::AutoLock lock(&lock_); |
| pending_requests_--; |
| if (shutting_down_) { |
| request.Release(); |
| if (pending_requests_ == 0) { |
| ShutdownComplete(); |
| } |
| return; |
| } |
| free_write_pool_.Add(std::move(request)); |
| } |
| |
| void RndisFunction::NotificationComplete(usb_request_t* usb_request) { |
| usb::Request<> request(usb_request, usb_request_size_); |
| fbl::AutoLock lock(&lock_); |
| pending_requests_--; |
| if (shutting_down_) { |
| request.Release(); |
| if (pending_requests_ == 0) { |
| ShutdownComplete(); |
| } |
| return; |
| } |
| free_notify_pool_.Add(std::move(request)); |
| } |
| |
| zx_status_t RndisFunction::Bind() { |
| descriptors_.assoc = { |
| .bLength = sizeof(usb_interface_assoc_descriptor_t), |
| .bDescriptorType = USB_DT_INTERFACE_ASSOCIATION, |
| .bFirstInterface = 0, // set later |
| .bInterfaceCount = 2, |
| .bFunctionClass = USB_CLASS_WIRELESS, |
| .bFunctionSubClass = USB_SUBCLASS_WIRELESS_MISC, |
| .bFunctionProtocol = USB_PROTOCOL_WIRELESS_MISC_RNDIS, |
| .iFunction = 0, // set later |
| }; |
| descriptors_.communication_interface = usb_interface_descriptor_t{ |
| .bLength = sizeof(usb_interface_descriptor_t), |
| .bDescriptorType = USB_DT_INTERFACE, |
| .bInterfaceNumber = 0, // set later |
| .bAlternateSetting = 0, |
| .bNumEndpoints = 1, |
| .bInterfaceClass = USB_CLASS_WIRELESS, |
| .bInterfaceSubClass = USB_SUBCLASS_WIRELESS_MISC, |
| .bInterfaceProtocol = USB_PROTOCOL_WIRELESS_MISC_RNDIS, |
| .iInterface = 0, |
| }; |
| descriptors_.cdc_header = |
| usb_cs_header_interface_descriptor_t{ |
| .bLength = sizeof(usb_cs_header_interface_descriptor_t), |
| .bDescriptorType = USB_DT_CS_INTERFACE, |
| .bDescriptorSubType = USB_CDC_DST_HEADER, |
| .bcdCDC = htole16(0x0110), |
| }, |
| descriptors_.call_mgmt = |
| usb_cs_call_mgmt_interface_descriptor_t{ |
| .bLength = sizeof(usb_cs_call_mgmt_interface_descriptor_t), |
| .bDescriptorType = USB_DT_CS_INTERFACE, |
| .bDescriptorSubType = USB_CDC_DST_CALL_MGMT, |
| .bmCapabilities = 0x00, |
| .bDataInterface = 0x01, |
| }, |
| descriptors_.acm = usb_cs_abstract_ctrl_mgmt_interface_descriptor_t{ |
| .bLength = sizeof(usb_cs_abstract_ctrl_mgmt_interface_descriptor_t), |
| .bDescriptorType = USB_DT_CS_INTERFACE, |
| .bDescriptorSubType = USB_CDC_DST_ABSTRACT_CTRL_MGMT, |
| .bmCapabilities = 0, |
| }; |
| descriptors_.cdc_union = usb_cs_union_interface_descriptor_1_t{ |
| .bLength = sizeof(usb_cs_union_interface_descriptor_1_t), |
| .bDescriptorType = USB_DT_CS_INTERFACE, |
| .bDescriptorSubType = USB_CDC_DST_UNION, |
| .bControlInterface = 0, // set later |
| .bSubordinateInterface = 0, // set later |
| }; |
| descriptors_.notification_ep = usb_endpoint_descriptor_t{ |
| .bLength = sizeof(usb_endpoint_descriptor_t), |
| .bDescriptorType = USB_DT_ENDPOINT, |
| .bEndpointAddress = 0, // set later |
| .bmAttributes = USB_ENDPOINT_INTERRUPT, |
| .wMaxPacketSize = htole16(kNotificationMaxPacketSize), |
| .bInterval = 1, |
| }; |
| |
| descriptors_.data_interface = usb_interface_descriptor_t{ |
| .bLength = sizeof(usb_interface_descriptor_t), |
| .bDescriptorType = USB_DT_INTERFACE, |
| .bInterfaceNumber = 0, // set later |
| .bAlternateSetting = 0, |
| .bNumEndpoints = 2, |
| .bInterfaceClass = USB_CLASS_CDC, |
| .bInterfaceSubClass = 0, |
| .bInterfaceProtocol = 0, |
| .iInterface = 0, |
| }; |
| descriptors_.in_ep = usb_endpoint_descriptor_t{ |
| .bLength = sizeof(usb_endpoint_descriptor_t), |
| .bDescriptorType = USB_DT_ENDPOINT, |
| .bEndpointAddress = 0, // set later |
| .bmAttributes = USB_ENDPOINT_BULK, |
| .wMaxPacketSize = htole16(512), |
| .bInterval = 0, |
| }; |
| descriptors_.out_ep = usb_endpoint_descriptor_t{ |
| .bLength = sizeof(usb_endpoint_descriptor_t), |
| .bDescriptorType = USB_DT_ENDPOINT, |
| .bEndpointAddress = 0, // set later |
| .bmAttributes = USB_ENDPOINT_BULK, |
| .wMaxPacketSize = htole16(512), |
| .bInterval = 0, |
| }; |
| |
| zx_status_t status = function_.AllocStringDesc("RNDIS Communications Control", |
| &descriptors_.communication_interface.iInterface); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to allocate string descriptor: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| status = |
| function_.AllocStringDesc("RNDIS Ethernet Data", &descriptors_.data_interface.iInterface); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to allocate string descriptor: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| status = function_.AllocStringDesc("RNDIS", &descriptors_.assoc.iFunction); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to allocate string descriptor: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| status = function_.AllocInterface(&descriptors_.communication_interface.bInterfaceNumber); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to allocate communication interface: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| status = function_.AllocInterface(&descriptors_.data_interface.bInterfaceNumber); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to allocate data interface: %s", zx_status_get_string(status)); |
| return status; |
| } |
| descriptors_.assoc.bFirstInterface = descriptors_.communication_interface.bInterfaceNumber; |
| descriptors_.cdc_union.bControlInterface = descriptors_.communication_interface.bInterfaceNumber; |
| descriptors_.cdc_union.bSubordinateInterface = descriptors_.data_interface.bInterfaceNumber; |
| |
| status = function_.AllocEp(USB_DIR_OUT, &descriptors_.out_ep.bEndpointAddress); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to allocate bulk out interface: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| status = function_.AllocEp(USB_DIR_IN, &descriptors_.in_ep.bEndpointAddress); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to allocate bulk in interface: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| status = function_.AllocEp(USB_DIR_IN, &descriptors_.notification_ep.bEndpointAddress); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to allocate notification interface: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| size_t actual; |
| status = DdkGetMetadata(DEVICE_METADATA_MAC_ADDRESS, mac_addr_.data(), mac_addr_.size(), &actual); |
| if (status != ZX_OK || actual != mac_addr_.size()) { |
| zxlogf(WARNING, "CDC: MAC address metadata not found. Generating random address"); |
| |
| zx_cprng_draw(mac_addr_.data(), mac_addr_.size()); |
| mac_addr_[0] = 0x02; |
| } |
| |
| zxlogf(INFO, "MAC address: %02x:%02x:%02x:%02x:%02x:%02x", mac_addr_[0], mac_addr_[1], |
| mac_addr_[2], mac_addr_[3], mac_addr_[4], mac_addr_[5]); |
| |
| usb_request_size_ = function_.GetRequestSize(); |
| |
| fbl::AutoLock lock(&lock_); |
| for (size_t i = 0; i < kRequestPoolSize; i++) { |
| std::optional<usb::Request<>> request; |
| status = usb::Request<>::Alloc(&request, kNotificationMaxPacketSize, NotificationAddress(), |
| usb_request_size_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Allocating notify request failed %d", status); |
| return status; |
| } |
| free_notify_pool_.Add(*std::move(request)); |
| } |
| |
| for (size_t i = 0; i < kRequestPoolSize; i++) { |
| std::optional<usb::Request<>> request; |
| status = |
| usb::Request<>::Alloc(&request, RNDIS_MAX_XFER_SIZE, BulkOutAddress(), usb_request_size_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Allocating reads failed %d", status); |
| return status; |
| } |
| free_read_pool_.Add(*std::move(request)); |
| } |
| |
| for (size_t i = 0; i < kRequestPoolSize; i++) { |
| std::optional<usb::Request<>> request; |
| status = |
| usb::Request<>::Alloc(&request, RNDIS_MAX_XFER_SIZE, BulkInAddress(), usb_request_size_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Allocating writes failed %d", status); |
| return status; |
| } |
| free_write_pool_.Add(*std::move(request)); |
| } |
| |
| status = loop_.StartThread("rndis-function"); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to start thread: %s", zx_status_get_string(status)); |
| return status; |
| } |
| |
| status = DdkAdd("rndis-function"); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| function_.SetInterface(this, &usb_function_interface_protocol_ops_); |
| |
| return ZX_OK; |
| } |
| |
| void RndisFunction::Shutdown() { |
| fbl::AutoLock lock(&lock_); |
| function_.CancelAll(BulkInAddress()); |
| function_.CancelAll(BulkOutAddress()); |
| function_.CancelAll(NotificationAddress()); |
| |
| free_notify_pool_.Release(); |
| free_read_pool_.Release(); |
| free_write_pool_.Release(); |
| |
| shutting_down_ = true; |
| ifc_.clear(); |
| |
| if (pending_requests_ == 0) { |
| ShutdownComplete(); |
| } else { |
| zxlogf(ERROR, "Shutdown with %zd pending", pending_requests_); |
| } |
| } |
| |
| void RndisFunction::ShutdownComplete() { |
| ZX_DEBUG_ASSERT(pending_requests_ == 0); |
| |
| if (shutdown_callback_.has_value()) { |
| (*shutdown_callback_)(); |
| } else { |
| zxlogf(WARNING, "ShutdownComplete called but there was no shutdown callback"); |
| } |
| } |
| |
| void RndisFunction::DdkUnbind(ddk::UnbindTxn txn) { |
| if (shutdown_callback_.has_value()) { |
| txn.Reply(); |
| return; |
| } |
| shutdown_callback_.emplace([unbind_txn = std::move(txn)]() mutable { unbind_txn.Reply(); }); |
| Shutdown(); |
| } |
| |
| void RndisFunction::DdkSuspend(ddk::SuspendTxn txn) { |
| shutdown_callback_.emplace( |
| [suspend_txn = std::move(txn)]() mutable { suspend_txn.Reply(ZX_OK, 0); }); |
| Shutdown(); |
| } |
| |
| void RndisFunction::DdkRelease() { delete this; } |
| |
| zx_status_t RndisFunction::Create(void* ctx, zx_device_t* parent) { |
| auto device = std::make_unique<RndisFunction>(parent); |
| |
| device->Bind(); |
| |
| // Intentionally leak this device because it's owned by the driver framework. |
| __UNUSED auto unused = device.release(); |
| return ZX_OK; |
| } |
| |
| static zx_driver_ops_t rndis_function_driver_ops = []() -> zx_driver_ops_t { |
| zx_driver_ops_t ops{}; |
| ops.version = DRIVER_OPS_VERSION; |
| ops.bind = RndisFunction::Create; |
| return ops; |
| }(); |
| |
| ZIRCON_DRIVER(rndis_function, rndis_function_driver_ops, "fuchsia", "0.1") |