| // 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 "tun_device.h" |
| |
| #include <lib/async-loop/default.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/syslog/global.h> |
| #include <zircon/status.h> |
| |
| #include <fbl/auto_lock.h> |
| |
| #include "buffer.h" |
| |
| namespace network { |
| namespace tun { |
| |
| namespace { |
| template <typename F> |
| void WithWireState(F fn, InternalState& state) { |
| fidl::WireTableFrame<fuchsia_net_tun::wire::InternalState> frame; |
| fuchsia_net_tun::wire::InternalState wire_state( |
| fidl::ObjectView<fidl::WireTableFrame<fuchsia_net_tun::wire::InternalState>>::FromExternal( |
| &frame)); |
| wire_state.set_has_session(state.has_session); |
| |
| fidl::WireTableFrame<fuchsia_net_tun::wire::MacState> frame_mac; |
| fuchsia_net_tun::wire::MacState wire_mac( |
| fidl::ObjectView<fidl::WireTableFrame<fuchsia_net_tun::wire::MacState>>::FromExternal( |
| &frame_mac)); |
| fidl::VectorView<fuchsia_net::wire::MacAddress> multicast_filters; |
| if (state.mac.has_value()) { |
| MacState& mac = state.mac.value(); |
| wire_mac.set_mode(mac.mode); |
| multicast_filters = |
| fidl::VectorView<fuchsia_net::wire::MacAddress>::FromExternal(mac.multicast_filters); |
| wire_mac.set_multicast_filters( |
| fidl::ObjectView<fidl::VectorView<fuchsia_net::wire::MacAddress>>::FromExternal( |
| &multicast_filters)); |
| wire_state.set_mac(fidl::ObjectView<fuchsia_net_tun::wire::MacState>::FromExternal(&wire_mac)); |
| } |
| |
| fn(std::move(wire_state)); |
| } |
| } // namespace |
| |
| TunDevice::TunDevice(fdf::Dispatcher* dispatcher, fit::callback<void(TunDevice*)> teardown, |
| DeviceConfig&& config) |
| : teardown_callback_(std::move(teardown)), |
| config_(std::move(config)), |
| loop_(&kAsyncLoopConfigNoAttachToCurrentThread), |
| dispatcher_(dispatcher) {} |
| |
| zx::result<std::unique_ptr<TunDevice>> TunDevice::Create( |
| const DeviceInterfaceDispatchers& dispatchers, const ShimDispatchers& shim_dispatchers, |
| fit::callback<void(TunDevice*)> teardown, DeviceConfig&& config) { |
| fbl::AllocChecker ac; |
| std::unique_ptr<TunDevice> tun(new (&ac) |
| TunDevice(nullptr, std::move(teardown), std::move(config))); |
| if (!ac.check()) { |
| return zx::error(ZX_ERR_NO_MEMORY); |
| } |
| |
| if (zx_status_t status = zx::eventpair::create(0, &tun->signals_peer_, &tun->signals_self_); |
| status != ZX_OK) { |
| FX_LOGF(ERROR, "tun", "TunDevice::Init failed to create eventpair %s", |
| zx_status_get_string(status)); |
| return zx::error(status); |
| } |
| |
| zx::result device = DeviceAdapter::Create(dispatchers, shim_dispatchers, tun.get()); |
| if (device.is_error()) { |
| FX_LOGF(ERROR, "tun", "TunDevice::Init device init failed with %s", device.status_string()); |
| return device.take_error(); |
| } |
| tun->device_ = std::move(device.value()); |
| |
| thrd_t thread; |
| if (zx_status_t status = tun->loop_.StartThread("tun-device", &thread); status != ZX_OK) { |
| return zx::error(status); |
| } |
| tun->loop_thread_ = thread; |
| |
| return zx::ok(std::move(tun)); |
| } |
| |
| TunDevice::~TunDevice() { |
| if (loop_thread_.has_value()) { |
| // not allowed to destroy a tun device on the loop thread, will cause deadlock |
| ZX_ASSERT(loop_thread_.value() != thrd_current()); |
| } |
| // make sure that device is torn down: |
| if (device_) { |
| device_->TeardownSync(); |
| } |
| loop_.Shutdown(); |
| |
| FX_VLOG(1, "tun", "TunDevice destroyed"); |
| } |
| |
| void TunDevice::Bind(fidl::ServerEnd<fuchsia_net_tun::Device> req) { |
| binding_ = fidl::BindServer(loop_.dispatcher(), std::move(req), this, |
| [](TunDevice* impl, fidl::UnbindInfo, |
| fidl::ServerEnd<fuchsia_net_tun::Device>) { impl->Teardown(); }); |
| } |
| |
| void TunDevice::Teardown() { |
| if (teardown_callback_) { |
| teardown_callback_(this); |
| } |
| } |
| |
| template <typename F, typename C> |
| bool TunDevice::WriteWith(F fn, C& completer) { |
| zx::result avail = fn(); |
| switch (zx_status_t status = avail.status_value(); status) { |
| case ZX_OK: |
| if (avail.value() == 0) { |
| // Clear the writable signal if no more buffers are available afterwards. |
| signals_self_.signal_peer(uint32_t(fuchsia_net_tun::wire::Signals::kWritable), 0); |
| } |
| completer.ReplySuccess(); |
| return true; |
| case ZX_ERR_SHOULD_WAIT: |
| if (IsBlocking()) { |
| return false; |
| } |
| __FALLTHROUGH; |
| default: |
| completer.ReplyError(status); |
| return true; |
| } |
| } |
| |
| bool TunDevice::RunWriteFrame() { |
| while (!pending_write_frame_.empty()) { |
| auto& pending = pending_write_frame_.front(); |
| bool handled = WriteWith( |
| [this, &pending]() -> zx::result<size_t> { |
| std::unique_ptr<Port>& port = ports_[pending.port_id]; |
| if (!port) { |
| return zx::error(ZX_ERR_NOT_FOUND); |
| } |
| return device_->WriteRxFrame(port->adapter(), pending.frame_type, pending.data, |
| pending.meta); |
| }, |
| pending.completer); |
| if (!handled) { |
| return false; |
| } |
| pending_write_frame_.pop(); |
| } |
| return true; |
| } |
| |
| void TunDevice::RunReadFrame() { |
| while (!pending_read_frame_.empty()) { |
| bool success = device_->TryGetTxBuffer([this](TxBuffer& buff, size_t avail) { |
| uint8_t port_id = buff.port_id(); |
| if (!ports_[port_id] || !ports_[port_id]->adapter().online()) { |
| return ZX_ERR_UNAVAILABLE; |
| } |
| std::vector<uint8_t> data; |
| zx_status_t status = buff.Read(data); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, "tun", "Failed to read from tx buffer: %s", zx_status_get_string(status)); |
| // The error reported here is relayed back to clients as an errored tx frame. There's a |
| // contract about specific meanings of errors returned in a tx frame through the netdevice |
| // banjo API and it might not match the semantics of the buffer API that generated this |
| // error. To avoid the possible impedance mismatch, return a fixed error. |
| return ZX_ERR_INTERNAL; |
| } |
| if (data.empty()) { |
| FX_LOG(WARNING, "tun", "Ignoring empty tx buffer"); |
| return ZX_OK; |
| } |
| fidl::WireTableFrame<fuchsia_net_tun::wire::Frame> fidl_frame; |
| fuchsia_net_tun::wire::Frame frame( |
| fidl::ObjectView<fidl::WireTableFrame<fuchsia_net_tun::wire::Frame>>::FromExternal( |
| &fidl_frame)); |
| fidl::VectorView data_view = fidl::VectorView<uint8_t>::FromExternal(data); |
| frame.set_data(fidl::ObjectView<fidl::VectorView<uint8_t>>::FromExternal(&data_view)); |
| frame.set_frame_type(buff.frame_type()); |
| frame.set_port(port_id); |
| std::optional meta = buff.TakeMetadata(); |
| if (meta.has_value()) { |
| frame.set_meta( |
| fidl::ObjectView<fuchsia_net_tun::wire::FrameMetadata>::FromExternal(&meta.value())); |
| } |
| pending_read_frame_.front().ReplySuccess(frame); |
| pending_read_frame_.pop(); |
| if (avail == 0) { |
| // clear Signals::READABLE if we don't have any more tx buffers. |
| signals_self_.signal_peer(uint32_t(fuchsia_net_tun::wire::Signals::kReadable), 0); |
| } |
| return ZX_OK; |
| }); |
| if (!success) { |
| if (IsBlocking()) { |
| return; |
| } |
| pending_read_frame_.front().ReplyError(ZX_ERR_SHOULD_WAIT); |
| pending_read_frame_.pop(); |
| } |
| } |
| } |
| |
| InternalState TunDevice::Port::State() const { |
| InternalState state = { |
| .has_session = adapter_->has_sessions(), |
| }; |
| |
| const std::unique_ptr<MacAdapter>& mac = adapter_->mac(); |
| if (mac) { |
| state.mac = mac->GetMacState(); |
| } |
| return state; |
| } |
| |
| void TunDevice::Port::RunStateChange() { |
| if (!pending_watch_state_.has_value()) { |
| return; |
| } |
| |
| InternalState state = State(); |
| // only continue if any changes actually occurred compared to the last observed state |
| if (last_state_.has_value() && last_state_.value() == state) { |
| return; |
| } |
| |
| WatchStateCompleter::Async completer = std::exchange(pending_watch_state_, std::nullopt).value(); |
| |
| WithWireState( |
| [&completer](fuchsia_net_tun::wire::InternalState state) { completer.Reply(state); }, state); |
| |
| // store the last informed state through WatchState |
| last_state_ = std::move(state); |
| } |
| |
| void TunDevice::Port::PostRunStateChange() { |
| // Skip any spurious calls that might happen before initialization. |
| if (!adapter_) { |
| return; |
| } |
| async::PostTask(parent_->loop_.dispatcher(), [parent = parent_, port_id = adapter_->id()]() { |
| // Port could be destroyed between callback and dispatch. |
| const std::unique_ptr<Port>& port = parent->ports_[port_id]; |
| if (port) { |
| port->RunStateChange(); |
| } |
| }); |
| } |
| |
| void TunDevice::WriteFrame(WriteFrameRequestView request, WriteFrameCompleter::Sync& completer) { |
| if (pending_write_frame_.size() >= kMaxPendingOps) { |
| completer.ReplyError(ZX_ERR_NO_RESOURCES); |
| return; |
| } |
| fuchsia_net_tun::wire::Frame& frame = request->frame; |
| if (!frame.has_frame_type()) { |
| completer.ReplyError(ZX_ERR_INVALID_ARGS); |
| return; |
| } |
| fuchsia_hardware_network::wire::FrameType& frame_type = frame.frame_type(); |
| if (!frame.has_data() || frame.data().empty()) { |
| completer.ReplyError(ZX_ERR_INVALID_ARGS); |
| return; |
| } |
| fidl::VectorView<uint8_t>& frame_data = frame.data(); |
| if (!frame.has_port() || frame.port() >= fuchsia_hardware_network::wire::kMaxPorts) { |
| completer.ReplyError(ZX_ERR_INVALID_ARGS); |
| return; |
| } |
| uint8_t& port_id = frame.port(); |
| |
| bool ready = RunWriteFrame(); |
| if (ready) { |
| std::optional<fuchsia_net_tun::wire::FrameMetadata> meta; |
| if (frame.has_meta()) { |
| meta = frame.meta(); |
| } |
| bool handled = WriteWith( |
| [this, &frame_type, &frame_data, &port_id, &meta]() -> zx::result<size_t> { |
| std::unique_ptr<Port>& port = ports_[port_id]; |
| if (!port) { |
| return zx::error(ZX_ERR_NOT_FOUND); |
| } |
| return device_->WriteRxFrame(port->adapter(), frame_type, frame_data, meta); |
| }, |
| completer); |
| if (handled) { |
| return; |
| } |
| } |
| pending_write_frame_.emplace(frame, completer.ToAsync()); |
| } |
| |
| void TunDevice::ReadFrame(ReadFrameCompleter::Sync& completer) { |
| if (pending_read_frame_.size() >= kMaxPendingOps) { |
| completer.ReplyError(ZX_ERR_NO_RESOURCES); |
| return; |
| } |
| pending_read_frame_.push(completer.ToAsync()); |
| RunReadFrame(); |
| } |
| |
| void TunDevice::GetSignals(GetSignalsCompleter::Sync& completer) { |
| zx::eventpair dup; |
| signals_peer_.duplicate(ZX_RIGHTS_BASIC, &dup); |
| completer.Reply(std::move(dup)); |
| } |
| |
| void TunDevice::AddPort(AddPortRequestView request, AddPortCompleter::Sync& _completer) { |
| zx_status_t status = [&request, this]() { |
| std::optional maybe_port_config = DevicePortConfig::Create(request->config); |
| |
| if (!maybe_port_config.has_value()) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| DevicePortConfig& port_config = maybe_port_config.value(); |
| std::unique_ptr<Port>& port_slot = ports_[port_config.port_id]; |
| if (port_slot) { |
| FX_LOGF(WARNING, "tun", "port %d already exists", port_config.port_id); |
| return ZX_ERR_ALREADY_EXISTS; |
| } |
| |
| zx::result maybe_port = Port::Create(this, port_config); |
| if (maybe_port.is_error()) { |
| return maybe_port.status_value(); |
| } |
| port_slot = std::move(maybe_port.value()); |
| port_slot->Bind(std::move(request->port)); |
| return ZX_OK; |
| }(); |
| if (status != ZX_OK) { |
| request->port.Close(status); |
| } |
| } |
| |
| void TunDevice::GetDevice(GetDeviceRequestView request, GetDeviceCompleter::Sync& _completer) { |
| zx_status_t status = device_->Bind(std::move(request->device)); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, "tun", "Failed to bind to network device: %s", zx_status_get_string(status)); |
| } |
| } |
| |
| void TunDevice::OnTxAvail(DeviceAdapter* device) { |
| signals_self_.signal_peer(0, uint32_t(fuchsia_net_tun::wire::Signals::kReadable)); |
| async::PostTask(loop_.dispatcher(), [this]() { RunReadFrame(); }); |
| } |
| |
| void TunDevice::OnRxAvail(DeviceAdapter* device) { |
| signals_self_.signal_peer(0, uint32_t(fuchsia_net_tun::wire::Signals::kWritable)); |
| async::PostTask(loop_.dispatcher(), [this]() { RunWriteFrame(); }); |
| } |
| |
| zx::result<std::unique_ptr<TunDevice::Port>> TunDevice::Port::Create( |
| TunDevice* parent, const DevicePortConfig& config) { |
| std::unique_ptr<Port> port(new Port(parent)); |
| std::unique_ptr<MacAdapter> mac; |
| if (config.mac.has_value()) { |
| zx::result status = MacAdapter::Create(port.get(), config.mac.value(), false); |
| if (status.is_error()) { |
| return status.take_error(); |
| } |
| mac = std::move(*status); |
| } |
| port->adapter_ = std::make_unique<PortAdapter>(port.get(), config, std::move(mac)); |
| zx_status_t status = parent->device_->AddPort(port->adapter()); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, "tun", "Failed to add port: %s", zx_status_get_string(status)); |
| return zx::error(status); |
| } |
| // At this point the port has been added to netdevice and the Port object should only complete |
| // destruction once the netdevice port removal notification has been received in OnPortDestroyed. |
| port->port_destroyed_.emplace(); |
| port->SetOnline(config.online); |
| return zx::ok(std::move(port)); |
| } |
| |
| TunDevice::Port::~Port() { |
| if (binding_.has_value()) { |
| binding_->Unbind(); |
| } |
| if (port_destroyed_.has_value()) { |
| port_destroyed_->Wait(); |
| } |
| } |
| |
| void TunDevice::Port::OnHasSessionsChanged(PortAdapter& port) { PostRunStateChange(); } |
| |
| void TunDevice::Port::OnPortStatusChanged(PortAdapter& port, const port_status_t& new_status) { |
| parent_->device_->OnPortStatusChanged(port.id(), new_status); |
| } |
| |
| void TunDevice::Port::OnPortDestroyed(PortAdapter& port) { |
| TunDevice& parent = *parent_; |
| async::PostTask(parent.loop_.dispatcher(), |
| [&parent, port_id = adapter_->id()]() { parent.ports_[port_id] = nullptr; }); |
| if (port_destroyed_.has_value()) { |
| port_destroyed_->Signal(); |
| } |
| } |
| |
| void TunDevice::Port::OnMacStateChanged(MacAdapter* adapter) { PostRunStateChange(); } |
| |
| void TunDevice::Port::GetState(GetStateCompleter::Sync& completer) { |
| InternalState state = State(); |
| WithWireState( |
| [&completer](fuchsia_net_tun::wire::InternalState state) { |
| completer.Reply(std::move(state)); |
| }, |
| state); |
| } |
| |
| void TunDevice::Port::WatchState(WatchStateCompleter::Sync& completer) { |
| if (pending_watch_state_) { |
| // this is a programming error, we enforce that clients don't do this by closing their channel. |
| completer.Close(ZX_ERR_INTERNAL); |
| return; |
| } |
| pending_watch_state_ = completer.ToAsync(); |
| RunStateChange(); |
| } |
| |
| void TunDevice::Port::SetOnline(SetOnlineRequestView request, SetOnlineCompleter::Sync& completer) { |
| SetOnline(request->online); |
| completer.Reply(); |
| } |
| |
| void TunDevice::Port::SetOnline(bool online) { |
| if (!adapter_->SetOnline(online) || online) { |
| return; |
| } |
| // If we just went offline, we may need to complete all pending writes. |
| parent_->RunWriteFrame(); |
| // Discard pending tx frames for all offline ports. |
| auto& ports = parent_->ports_; |
| parent_->device_->RetainTxBuffers([&ports](TxBuffer& buffer) { |
| if (!ports[buffer.port_id()] || !ports[buffer.port_id()]->adapter().online()) { |
| return ZX_ERR_UNAVAILABLE; |
| } |
| return ZX_OK; |
| }); |
| } |
| |
| void TunDevice::Port::GetPort(GetPortRequestView request, GetPortCompleter::Sync& _completer) { |
| zx_status_t status = parent_->device_->BindPort(adapter_->id(), std::move(request->port)); |
| if (status != ZX_OK) { |
| FX_LOGF(ERROR, "tun", "BindPort %d failed: %s", adapter_->id(), zx_status_get_string(status)); |
| } |
| } |
| |
| void TunDevice::Port::Remove(RemoveCompleter::Sync& _completer) { |
| parent_->device_->RemovePort(adapter().id()); |
| } |
| |
| void TunDevice::Port::Bind(fidl::ServerEnd<fuchsia_net_tun::Port> req) { |
| binding_ = |
| fidl::BindServer(parent_->loop_.dispatcher(), std::move(req), this, |
| [parent = parent_, id = adapter().id()]( |
| Port*, fidl::UnbindInfo info, fidl::ServerEnd<fuchsia_net_tun::Port>) { |
| // User initiated unbinds are only triggered by the |
| // destructor, which means the port was already |
| // removed. |
| if (!info.is_user_initiated()) { |
| parent->device_->RemovePort(id); |
| } |
| }); |
| } |
| |
| } // namespace tun |
| } // namespace network |