| // Copyright 2018 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 <fuchsia/netstack/cpp/fidl.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/async/cpp/executor.h> |
| #include <lib/fit/bridge.h> |
| #include <lib/fit/defer.h> |
| #include <lib/fit/promise.h> |
| #include <lib/fit/scope.h> |
| #include <lib/syslog/cpp/log_settings.h> |
| #include <lib/trace-provider/provider.h> |
| #include <lib/zx/fifo.h> |
| #include <zircon/device/ethernet.h> |
| #include <zircon/status.h> |
| |
| #include <memory> |
| #include <queue> |
| |
| #include <virtio/net.h> |
| |
| #include "guest_ethernet.h" |
| #include "src/virtualization/bin/vmm/device/device_base.h" |
| #include "src/virtualization/bin/vmm/device/stream_base.h" |
| |
| static constexpr char kInterfaceName[] = "ethv0"; |
| |
| enum class Queue : uint16_t { |
| RECEIVE = 0, |
| TRANSMIT = 1, |
| }; |
| |
| class RxStream : public StreamBase { |
| public: |
| void Init(GuestEthernet* guest_ethernet, const PhysMem& phys_mem, |
| VirtioQueue::InterruptFn interrupt) { |
| guest_ethernet_ = guest_ethernet; |
| phys_mem_ = &phys_mem; |
| StreamBase::Init(phys_mem, std::move(interrupt)); |
| } |
| |
| void Notify() { |
| for (; !packet_queue_.empty() && queue_.NextChain(&chain_); chain_.Return()) { |
| Packet pkt = packet_queue_.front(); |
| chain_.NextDescriptor(&desc_); |
| if (desc_.len < sizeof(virtio_net_hdr_t)) { |
| FX_LOGS(ERROR) << "Malformed descriptor"; |
| continue; |
| } |
| auto header = static_cast<virtio_net_hdr_t*>(desc_.addr); |
| // Section 5.1.6.4.1 Device Requirements: Processing of Incoming Packets |
| |
| // If VIRTIO_NET_F_MRG_RXBUF has not been negotiated, the device MUST |
| // set num_buffers to 1. |
| header->num_buffers = 1; |
| |
| // If none of the VIRTIO_NET_F_GUEST_TSO4, TSO6 or UFO options have been |
| // negotiated, the device MUST set gso_type to VIRTIO_NET_HDR_GSO_NONE. |
| header->gso_type = VIRTIO_NET_HDR_GSO_NONE; |
| |
| // If VIRTIO_NET_F_GUEST_CSUM is not negotiated, the device MUST set |
| // flags to zero and SHOULD supply a fully checksummed packet to the |
| // driver. |
| header->flags = 0; |
| |
| uintptr_t offset = phys_mem_->offset(header + 1); |
| uintptr_t length = desc_.len - sizeof(*header); |
| packet_queue_.pop(); |
| if (length < pkt.length) { |
| // 5.1.6.3.1 Driver Requirements: Setting Up Receive Buffers: the driver |
| // SHOULD populate the receive queue(s) with buffers of at least 1526 |
| // bytes. |
| |
| // If the descriptor is too small for the packet then the driver is |
| // misbehaving (our MTU is 1500). |
| FX_LOGS(ERROR) << "Dropping packet that's too large for the descriptor"; |
| continue; |
| } |
| memcpy(phys_mem_->as<void>(offset, length), reinterpret_cast<void*>(pkt.addr), pkt.length); |
| *chain_.Used() = pkt.length + sizeof(*header); |
| pkt.entry.flags = ETH_FIFO_TX_OK; |
| guest_ethernet_->Complete(pkt.entry); |
| } |
| } |
| |
| void Receive(uintptr_t addr, size_t length, const eth_fifo_entry_t& entry) { |
| packet_queue_.push(Packet{addr, length, entry}); |
| Notify(); |
| } |
| |
| private: |
| struct Packet { |
| uintptr_t addr; |
| size_t length; |
| eth_fifo_entry_t entry; |
| }; |
| |
| GuestEthernet* guest_ethernet_ = nullptr; |
| const PhysMem* phys_mem_ = nullptr; |
| std::queue<Packet> packet_queue_; |
| }; |
| |
| class TxStream : public StreamBase { |
| public: |
| void Init(GuestEthernet* guest_ethernet, const PhysMem& phys_mem, |
| VirtioQueue::InterruptFn interrupt) { |
| guest_ethernet_ = guest_ethernet; |
| phys_mem_ = &phys_mem; |
| StreamBase::Init(phys_mem, std::move(interrupt)); |
| } |
| |
| bool ProcessDescriptor() { |
| auto header = static_cast<virtio_net_hdr_t*>(desc_.addr); |
| uintptr_t offset = phys_mem_->offset(header + 1); |
| uintptr_t length = desc_.len - sizeof(*header); |
| |
| zx_status_t status = guest_ethernet_->Send(phys_mem_->as<void>(offset, length), length); |
| return status != ZX_ERR_SHOULD_WAIT; |
| } |
| |
| void Notify() { |
| // If Send returned ZX_ERR_SHOULD_WAIT last time Notify was called, then we should process that |
| // descriptor first. |
| if (chain_.IsValid()) { |
| bool processed = ProcessDescriptor(); |
| if (!processed) { |
| return; |
| } |
| chain_.Return(); |
| } |
| |
| for (; queue_.NextChain(&chain_); chain_.Return()) { |
| chain_.NextDescriptor(&desc_); |
| if (desc_.has_next) { |
| // Section 5.1.6.2 Packet Transmission: The header and packet are added |
| // as one output descriptor to the transmitq. |
| if (!warned_) { |
| warned_ = true; |
| FX_LOGS(WARNING) << "Transmit packet and header must be on a single descriptor"; |
| } |
| continue; |
| } |
| if (desc_.len < sizeof(virtio_net_hdr_t)) { |
| FX_LOGS(ERROR) << "Failed to read descriptor header"; |
| continue; |
| } |
| |
| bool processed = ProcessDescriptor(); |
| if (!processed) { |
| // Stop processing and wait for GuestEthernet to notify us again. Do not return the |
| // descriptor to the guest. |
| return; |
| } |
| } |
| } |
| |
| private: |
| GuestEthernet* guest_ethernet_ = nullptr; |
| const PhysMem* phys_mem_ = nullptr; |
| bool warned_ = false; |
| }; |
| |
| class VirtioNetImpl : public DeviceBase<VirtioNetImpl>, |
| public fuchsia::virtualization::hardware::VirtioNet, |
| public GuestEthernetDevice { |
| public: |
| VirtioNetImpl(sys::ComponentContext* context) : DeviceBase(context), context_(*context) { |
| netstack_ = context_.svc()->Connect<fuchsia::netstack::Netstack>(); |
| } |
| |
| // |fuchsia::virtualization::hardware::VirtioDevice| |
| void NotifyQueue(uint16_t queue) override { |
| switch (static_cast<Queue>(queue)) { |
| case Queue::RECEIVE: |
| rx_stream_.Notify(); |
| break; |
| case Queue::TRANSMIT: |
| tx_stream_.Notify(); |
| break; |
| default: |
| FX_CHECK(false) << "Queue index " << queue << " out of range"; |
| __UNREACHABLE; |
| } |
| } |
| |
| // Called by GuestEthernet to notify us when the netstack is trying to send a packet to the guest. |
| void Receive(uintptr_t addr, size_t length, const eth_fifo_entry_t& entry) override { |
| rx_stream_.Receive(addr, length, entry); |
| } |
| |
| // Called by GuestEthernet to notify us when the netstack is ready to receive packets. |
| void ReadyToSend() override { tx_stream_.Notify(); } |
| |
| fuchsia::hardware::ethernet::MacAddress GetMacAddress() override { return mac_address_; } |
| |
| private: |
| // |fuchsia::virtualization::hardware::VirtioNet| |
| void Start(fuchsia::virtualization::hardware::StartInfo start_info, |
| fuchsia::hardware::ethernet::MacAddress mac_address, bool enable_bridge, |
| StartCallback callback) override { |
| PrepStart(std::move(start_info)); |
| rx_stream_.Init(&guest_ethernet_, phys_mem_, |
| fit::bind_member<zx_status_t, DeviceBase>(this, &VirtioNetImpl::Interrupt)); |
| tx_stream_.Init(&guest_ethernet_, phys_mem_, |
| fit::bind_member<zx_status_t, DeviceBase>(this, &VirtioNetImpl::Interrupt)); |
| |
| mac_address_ = std::move(mac_address); |
| |
| fit::promise<uint32_t> guest_interface = |
| CreateGuestInterface().and_then(fit::bind_member(this, &VirtioNetImpl::EnableInterface)); |
| |
| if (enable_bridge) { |
| fit::promise<uint32_t> bridge = |
| fit::join_promises(FindHostInterface(), std::move(guest_interface)) |
| .and_then(fit::bind_member(this, &VirtioNetImpl::CreateBridgeInterface)) |
| .and_then(fit::bind_member(this, &VirtioNetImpl::EnableInterface)); |
| guest_interface = std::move(bridge); |
| } |
| |
| executor_.schedule_task( |
| guest_interface |
| .and_then([callback = std::move(callback)](const uint32_t& nic_id) { callback(); }) |
| .or_else([] { FX_LOGS(FATAL) << "Failed to setup guest ethernet"; }) |
| .wrap_with(scope_)); |
| } |
| |
| fit::promise<uint32_t> CreateGuestInterface() { |
| fit::bridge<uint32_t> bridge; |
| |
| fuchsia::netstack::InterfaceConfig config; |
| config.name = kInterfaceName; |
| auto callback = [completer = std::move(bridge.completer)]( |
| fuchsia::netstack::Netstack_AddEthernetDevice_Result result) mutable { |
| if (result.is_err()) { |
| FX_LOGS(ERROR) << "Failed to create guest interface"; |
| completer.complete_error(); |
| } else { |
| completer.complete_ok(result.response().nicid); |
| } |
| }; |
| netstack_->AddEthernetDevice("", std::move(config), device_binding_.NewBinding(), |
| std::move(callback)); |
| |
| return bridge.consumer.promise(); |
| } |
| |
| fit::promise<uint32_t> EnableInterface(const uint32_t& nic_id) { |
| netstack_->SetInterfaceStatus(nic_id, true); |
| return fit::make_ok_promise(nic_id); |
| } |
| |
| fit::promise<uint32_t> FindHostInterface() { |
| fit::bridge<uint32_t> bridge; |
| |
| auto callback = [completer = std::move(bridge.completer)]( |
| std::vector<fuchsia::netstack::NetInterface> interfaces) mutable { |
| for (auto& interface : interfaces) { |
| if (!(static_cast<uint32_t>(interface.flags) & |
| static_cast<uint32_t>(fuchsia::netstack::Flags::UP)) || |
| static_cast<uint32_t>(interface.features) != 0) { |
| continue; |
| } |
| completer.complete_ok(interface.id); |
| return; |
| } |
| FX_LOGS(ERROR) << "Failed to find host interface"; |
| completer.complete_error(); |
| }; |
| netstack_->GetInterfaces(std::move(callback)); |
| |
| return bridge.consumer.promise(); |
| } |
| |
| fit::promise<uint32_t> CreateBridgeInterface( |
| const std::tuple<fit::result<uint32_t>, fit::result<uint32_t>>& nic_ids) { |
| auto& [host_id, guest_id] = nic_ids; |
| if (host_id.is_error() || guest_id.is_error()) { |
| return fit::make_result_promise<uint32_t>(fit::error()); |
| } |
| fit::bridge<uint32_t> bridge; |
| |
| auto callback = [completer = std::move(bridge.completer)](fuchsia::netstack::NetErr result, |
| uint32_t nic_id) mutable { |
| if (result.status != fuchsia::netstack::Status::OK) { |
| FX_LOGS(ERROR) << "Failed to create bridge interface: " << result.message; |
| completer.complete_error(); |
| } else { |
| completer.complete_ok(nic_id); |
| } |
| }; |
| netstack_->BridgeInterfaces({host_id.value(), guest_id.value()}, std::move(callback)); |
| |
| return bridge.consumer.promise(); |
| } |
| |
| // |fuchsia::virtualization::hardware::VirtioDevice| |
| void ConfigureQueue(uint16_t queue, uint16_t size, zx_gpaddr_t desc, zx_gpaddr_t avail, |
| zx_gpaddr_t used, ConfigureQueueCallback callback) override { |
| auto deferred = fit::defer(std::move(callback)); |
| switch (static_cast<Queue>(queue)) { |
| case Queue::RECEIVE: |
| rx_stream_.Configure(size, desc, avail, used); |
| break; |
| case Queue::TRANSMIT: |
| tx_stream_.Configure(size, desc, avail, used); |
| break; |
| default: |
| FX_CHECK(false) << "Queue index " << queue << " out of range"; |
| __UNREACHABLE; |
| } |
| } |
| |
| // |fuchsia::virtualization::hardware::VirtioDevice| |
| void Ready(uint32_t negotiated_features, ReadyCallback callback) override { |
| negotiated_features_ = negotiated_features; |
| callback(); |
| } |
| |
| sys::ComponentContext& context_; |
| GuestEthernet guest_ethernet_{this}; |
| fidl::Binding<fuchsia::hardware::ethernet::Device> device_binding_ = |
| fidl::Binding<fuchsia::hardware::ethernet::Device>(&guest_ethernet_); |
| fuchsia::netstack::NetstackPtr netstack_; |
| |
| RxStream rx_stream_; |
| TxStream tx_stream_; |
| |
| uint32_t negotiated_features_; |
| fit::scope scope_; |
| async::Executor executor_ = async::Executor(async_get_default_dispatcher()); |
| |
| fuchsia::hardware::ethernet::MacAddress mac_address_; |
| }; |
| |
| int main(int argc, char** argv) { |
| syslog::SetTags({"virtio_net"}); |
| |
| async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread); |
| trace::TraceProviderWithFdio trace_provider(loop.dispatcher()); |
| std::unique_ptr<sys::ComponentContext> context = |
| sys::ComponentContext::CreateAndServeOutgoingDirectory(); |
| |
| VirtioNetImpl virtio_net(context.get()); |
| return loop.Run(); |
| } |