blob: 5a1f9981521569532198a4a41f8476658d43b075 [file] [log] [blame]
// Copyright 2019 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "patchpanel/crostini_service.h"
#include <memory>
#include <optional>
#include <ostream>
#include <utility>
#include "base/task/single_thread_task_runner.h"
#include <base/check.h>
#include <base/logging.h>
#include <chromeos/constants/vm_tools.h>
#include <chromeos/dbus/service_constants.h>
// Ignore Wconversion warnings in dbus headers.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wconversion"
#include <dbus/message.h>
#include <dbus/object_path.h>
#include <dbus/object_proxy.h>
#pragma GCC diagnostic pop
#include <patchpanel/proto_bindings/patchpanel_service.pb.h>
#include "patchpanel/adb_proxy.h"
#include "patchpanel/address_manager.h"
#include "patchpanel/device.h"
#include "patchpanel/ipc.h"
#include "patchpanel/net_util.h"
#include "patchpanel/proto_utils.h"
namespace patchpanel {
namespace {
constexpr int32_t kInvalidID = 0;
constexpr int kDbusTimeoutMs = 200;
// The maximum number of ADB sideloading query failures before stopping.
constexpr int kAdbSideloadMaxTry = 5;
constexpr base::TimeDelta kAdbSideloadUpdateDelay = base::Milliseconds(5000);
std::ostream& operator<<(
std::ostream& stream,
const std::pair<uint64_t, CrostiniService::VMType>& vm_info) {
return stream << "{id: " << vm_info.first << ", vm_type: " << vm_info.second
<< "}";
}
AutoDNATTarget GetAutoDNATTarget(CrostiniService::VMType guest_type) {
switch (guest_type) {
case CrostiniService::VMType::kTermina:
return AutoDNATTarget::kCrostini;
case CrostiniService::VMType::kParallels:
return AutoDNATTarget::kParallels;
}
}
} // namespace
CrostiniService::CrostiniDevice::CrostiniDevice(
VMType type,
std::string_view tap_device_ifname,
const MacAddress& mac_address,
std::unique_ptr<Subnet> vm_ipv4_subnet,
std::unique_ptr<Subnet> lxd_ipv4_subnet)
: type_(type),
tap_device_ifname_(tap_device_ifname),
mac_address_(mac_address),
vm_ipv4_subnet_(std::move(vm_ipv4_subnet)),
lxd_ipv4_subnet_(std::move(lxd_ipv4_subnet)) {
DCHECK(vm_ipv4_subnet_);
gateway_ipv4_address_ = vm_ipv4_subnet_->CIDRAtOffset(1)->address();
vm_ipv4_address_ = vm_ipv4_subnet_->CIDRAtOffset(2)->address();
lxd_ipv4_address_ = std::nullopt;
if (lxd_ipv4_subnet_) {
lxd_ipv4_address_ =
lxd_ipv4_subnet_->CIDRAtOffset(kTerminaContainerAddressOffset)
->address();
}
}
CrostiniService::CrostiniDevice::~CrostiniDevice() {}
void CrostiniService::CrostiniDevice::ConvertToProto(
NetworkDevice* output) const {
output->set_ifname(tap_device_ifname());
// Legacy compatibility: fill |phys_ifname| with the tap device interface
// name.
output->set_phys_ifname(tap_device_ifname());
// For non-ARC VMs, the guest virtio interface name is not known.
output->set_phys_ifname(tap_device_ifname());
output->set_guest_ifname("");
output->set_ipv4_addr(vm_ipv4_address().ToInAddr().s_addr);
output->set_host_ipv4_addr(gateway_ipv4_address().ToInAddr().s_addr);
switch (type()) {
case VMType::kTermina:
output->set_guest_type(NetworkDevice::TERMINA_VM);
break;
case VMType::kParallels:
output->set_guest_type(NetworkDevice::PARALLELS_VM);
break;
}
FillSubnetProto(vm_ipv4_subnet(), output->mutable_ipv4_subnet());
// Do no copy LXD container subnet data: patchpanel_service.proto's
// NetworkDevice does not have a field for the LXD container IPv4 allocation.
}
CrostiniService::CrostiniService(
AddressManager* addr_mgr,
Datapath* datapath,
CrostiniService::CrostiniDeviceEventHandler event_handler)
: addr_mgr_(addr_mgr),
datapath_(datapath),
event_handler_(event_handler),
adb_sideloading_enabled_(false) {
DCHECK(addr_mgr_);
DCHECK(datapath_);
dbus::Bus::Options options;
options.bus_type = dbus::Bus::SYSTEM;
bus_ = new dbus::Bus(options);
if (!bus_->Connect()) {
LOG(ERROR) << "Failed to connect to system bus";
} else {
CheckAdbSideloadingStatus();
}
}
CrostiniService::~CrostiniService() {
if (bus_)
bus_->ShutdownAndBlock();
}
const CrostiniService::CrostiniDevice* CrostiniService::Start(
uint64_t vm_id, CrostiniService::VMType vm_type, uint32_t subnet_index) {
const auto vm_info = std::make_pair(vm_id, vm_type);
if (vm_id == kInvalidID) {
LOG(ERROR) << __func__ << " " << vm_info << ": Invalid VM id";
return nullptr;
}
if (devices_.find(vm_id) != devices_.end()) {
LOG(WARNING) << __func__ << " " << vm_info << ": Datapath already started";
return nullptr;
}
auto dev = AddTAP(vm_type, subnet_index);
if (!dev) {
LOG(ERROR) << __func__ << " " << vm_info << ": Failed to create TAP device";
return nullptr;
}
datapath_->StartRoutingDeviceAsUser(dev->tap_device_ifname(),
dev->vm_ipv4_address(),
TrafficSourceFromVMType(vm_type));
if (adb_sideloading_enabled_) {
StartAdbPortForwarding(dev->tap_device_ifname());
}
if (vm_type == VMType::kParallels) {
StartAutoDNAT(dev.get());
}
LOG(INFO) << __func__ << " " << vm_info
<< ": Crostini network service started on "
<< dev->tap_device_ifname();
event_handler_.Run(*dev, CrostiniDeviceEvent::kAdded);
auto [it, _] = devices_.emplace(vm_id, std::move(dev));
return it->second.get();
}
void CrostiniService::Stop(uint64_t vm_id) {
const auto it = devices_.find(vm_id);
if (it == devices_.end()) {
LOG(WARNING) << __func__ << " {id: " << vm_id << "}: Unknown VM";
return;
}
auto vm_type = it->second->type();
const auto vm_info = std::make_pair(vm_id, vm_type);
event_handler_.Run(*it->second, CrostiniDeviceEvent::kRemoved);
const std::string tap_ifname = it->second->tap_device_ifname();
datapath_->StopRoutingDevice(tap_ifname);
if (adb_sideloading_enabled_) {
StopAdbPortForwarding(tap_ifname);
}
if (vm_type == VMType::kParallels) {
StopAutoDNAT(it->second.get());
}
datapath_->RemoveInterface(tap_ifname);
devices_.erase(vm_id);
LOG(INFO) << __func__ << " " << vm_info
<< ": Crostini network service stopped on " << tap_ifname;
}
const CrostiniService::CrostiniDevice* const CrostiniService::GetDevice(
uint64_t vm_id) const {
const auto it = devices_.find(vm_id);
if (it == devices_.end()) {
return nullptr;
}
return it->second.get();
}
std::vector<const CrostiniService::CrostiniDevice*>
CrostiniService::GetDevices() const {
std::vector<const CrostiniDevice*> devices;
for (const auto& [_, dev] : devices_) {
devices.push_back(dev.get());
}
return devices;
}
std::unique_ptr<CrostiniService::CrostiniDevice> CrostiniService::AddTAP(
CrostiniService::VMType vm_type, uint32_t subnet_index) {
auto guest_type = GuestTypeFromVMType(vm_type);
auto ipv4_subnet = addr_mgr_->AllocateIPv4Subnet(guest_type, subnet_index);
if (!ipv4_subnet) {
LOG(ERROR) << "Subnet already in use or unavailable.";
return nullptr;
}
// Verify addresses can be allocated in the VM IPv4 subnet.
auto gateway_ipv4_cidr = ipv4_subnet->CIDRAtOffset(1);
if (!gateway_ipv4_cidr) {
LOG(ERROR) << "Gateway address already in use or unavailable.";
return nullptr;
}
auto vm_ipv4_cidr = ipv4_subnet->CIDRAtOffset(2);
if (!vm_ipv4_cidr) {
LOG(ERROR) << "VM address already in use or unavailable.";
return nullptr;
}
std::unique_ptr<Subnet> lxd_subnet;
if (vm_type == VMType::kTermina) {
lxd_subnet =
addr_mgr_->AllocateIPv4Subnet(AddressManager::GuestType::kLXDContainer);
if (!lxd_subnet) {
LOG(ERROR) << "LXD subnet already in use or unavailable.";
return nullptr;
}
// Verify the LXD address can be allocated in the VM IPv4 subnet.
if (!lxd_subnet->CIDRAtOffset(kTerminaContainerAddressOffset)) {
LOG(ERROR) << "LXD address already in use or unavailable.";
return nullptr;
}
}
const auto mac_addr = addr_mgr_->GenerateMacAddress(subnet_index);
// Name is autogenerated.
const std::string tap = datapath_->AddTAP(
/*name=*/"", &mac_addr, gateway_ipv4_cidr.operator->(),
vm_tools::kCrosVmUser);
if (tap.empty()) {
LOG(ERROR) << "Failed to create TAP device.";
return nullptr;
}
if (lxd_subnet) {
// Setup route to the LXD the container using the VM as a gateway into the
// LXD container.
const auto lxd_cidr =
lxd_subnet->CIDRAtOffset(kTerminaContainerAddressOffset);
if (!datapath_->AddIPv4Route(vm_ipv4_cidr->address(), *lxd_cidr)) {
LOG(ERROR) << "Failed to setup route to the Termina LXD container";
return nullptr;
}
}
return std::make_unique<CrostiniDevice>(
vm_type, tap, mac_addr, std::move(ipv4_subnet), std::move(lxd_subnet));
}
void CrostiniService::StartAdbPortForwarding(const std::string& ifname) {
if (!datapath_->AddAdbPortForwardRule(ifname)) {
LOG(ERROR) << __func__ << ": Error adding ADB port forwarding rule for "
<< ifname;
return;
}
if (!datapath_->AddAdbPortAccessRule(ifname)) {
datapath_->DeleteAdbPortForwardRule(ifname);
LOG(ERROR) << __func__ << ": Error adding ADB port access rule for "
<< ifname;
return;
}
if (!datapath_->SetRouteLocalnet(ifname, true)) {
LOG(ERROR) << __func__ << ": Failed to set up route localnet for "
<< ifname;
return;
}
}
void CrostiniService::StopAdbPortForwarding(const std::string& ifname) {
datapath_->DeleteAdbPortForwardRule(ifname);
datapath_->DeleteAdbPortAccessRule(ifname);
datapath_->SetRouteLocalnet(ifname, false);
}
void CrostiniService::CheckAdbSideloadingStatus() {
static int num_try = 0;
if (num_try >= kAdbSideloadMaxTry) {
LOG(WARNING) << __func__
<< ": Failed getting feature enablement status after "
<< num_try << " tries.";
return;
}
dbus::ObjectProxy* proxy = bus_->GetObjectProxy(
login_manager::kSessionManagerServiceName,
dbus::ObjectPath(login_manager::kSessionManagerServicePath));
dbus::MethodCall method_call(login_manager::kSessionManagerInterface,
login_manager::kSessionManagerQueryAdbSideload);
std::unique_ptr<dbus::Response> dbus_response =
proxy->CallMethodAndBlock(&method_call, kDbusTimeoutMs);
if (!dbus_response) {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&CrostiniService::CheckAdbSideloadingStatus,
weak_factory_.GetWeakPtr()),
kAdbSideloadUpdateDelay);
num_try++;
return;
}
dbus::MessageReader reader(dbus_response.get());
reader.PopBool(&adb_sideloading_enabled_);
if (!adb_sideloading_enabled_)
return;
// If ADB sideloading is enabled, start ADB forwarding on all configured
// Crostini's TAP interfaces.
for (const auto& [_, dev] : devices_) {
StartAdbPortForwarding(dev->tap_device_ifname());
}
}
void CrostiniService::OnShillDefaultLogicalDeviceChanged(
const ShillClient::Device* new_device,
const ShillClient::Device* prev_device) {
// b/197930417: Update Auto DNAT rules if a Parallels VM is running.
const CrostiniDevice* parallels_device = nullptr;
for (const auto& [_, dev] : devices_) {
if (dev->type() == VMType::kParallels) {
parallels_device = dev.get();
break;
}
}
if (parallels_device) {
StopAutoDNAT(parallels_device);
}
if (new_device) {
default_logical_device_ = *new_device;
} else {
default_logical_device_ = std::nullopt;
}
if (parallels_device) {
StartAutoDNAT(parallels_device);
}
}
void CrostiniService::StartAutoDNAT(
const CrostiniService::CrostiniDevice* virtual_device) {
if (!default_logical_device_) {
return;
}
datapath_->AddInboundIPv4DNAT(GetAutoDNATTarget(virtual_device->type()),
*default_logical_device_,
virtual_device->vm_ipv4_address());
}
void CrostiniService::StopAutoDNAT(
const CrostiniService::CrostiniDevice* virtual_device) {
if (!default_logical_device_) {
return;
}
datapath_->RemoveInboundIPv4DNAT(GetAutoDNATTarget(virtual_device->type()),
*default_logical_device_,
virtual_device->vm_ipv4_address());
}
// static
std::optional<CrostiniService::VMType> CrostiniService::VMTypeFromDeviceType(
Device::Type device_type) {
switch (device_type) {
case Device::Type::kTerminaVM:
return VMType::kTermina;
case Device::Type::kParallelsVM:
return VMType::kParallels;
default:
return std::nullopt;
}
}
// static
std::optional<CrostiniService::VMType>
CrostiniService::VMTypeFromProtoGuestType(NetworkDevice::GuestType guest_type) {
switch (guest_type) {
case NetworkDevice::TERMINA_VM:
return VMType::kTermina;
case NetworkDevice::PARALLELS_VM:
return VMType::kParallels;
default:
return std::nullopt;
}
}
// static
TrafficSource CrostiniService::TrafficSourceFromVMType(
CrostiniService::VMType vm_type) {
switch (vm_type) {
case VMType::kTermina:
return TrafficSource::kCrosVM;
case VMType::kParallels:
return TrafficSource::kParallelsVM;
}
}
// static
GuestMessage::GuestType CrostiniService::GuestMessageTypeFromVMType(
CrostiniService::VMType vm_type) {
switch (vm_type) {
case VMType::kTermina:
return GuestMessage::TERMINA_VM;
case VMType::kParallels:
return GuestMessage::PARALLELS_VM;
}
}
// static
AddressManager::GuestType CrostiniService::GuestTypeFromVMType(
CrostiniService::VMType vm_type) {
switch (vm_type) {
case VMType::kTermina:
return AddressManager::GuestType::kTerminaVM;
case VMType::kParallels:
return AddressManager::GuestType::kParallelsVM;
}
}
// static
Device::Type CrostiniService::VirtualDeviceTypeFromVMType(
CrostiniService::VMType vm_type) {
switch (vm_type) {
case VMType::kTermina:
return Device::Type::kTerminaVM;
case VMType::kParallels:
return Device::Type::kParallelsVM;
}
}
std::ostream& operator<<(std::ostream& stream,
const CrostiniService::VMType vm_type) {
switch (vm_type) {
case CrostiniService::VMType::kTermina:
return stream << "Termina";
case CrostiniService::VMType::kParallels:
return stream << "Parallels";
}
}
} // namespace patchpanel