blob: 441844d0dc25643b3e86378e11980886ff14a664 [file] [log] [blame]
// Copyright 2015 The Chromium OS 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 "permission_broker/port_tracker.h"
#include <arpa/inet.h>
#include <net/if.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <memory>
#include <ostream>
#include <string>
#include <vector>
#include <base/bind.h>
#include <base/logging.h>
#include <base/posix/eintr_wrapper.h>
#include <base/strings/string_util.h>
#include <base/threading/thread_task_runner_handle.h>
#include <base/time/time.h>
#include <chromeos/patchpanel/dbus/client.h>
namespace permission_broker {
namespace {
// Port forwarding is only allowed for non-reserved ports.
constexpr const uint16_t kLastSystemPort = 1023;
// Port forwarding is only allowed for some physical interfaces: Ethernet, USB
// tethering, and WiFi.
constexpr std::array<const char*, 4> kAllowedInterfacePrefixes{
{"eth", "usb", "wlan", "mlan"}};
constexpr const char kLocalhost[] = "lo";
// Returns the network-byte order int32 representation of the IPv4 address given
// byte per byte, most significant bytes first.
constexpr uint32_t Ipv4Addr(uint8_t b0, uint8_t b1, uint8_t b2, uint8_t b3) {
return (b3 << 24) | (b2 << 16) | (b1 << 8) | b0;
}
// TODO(hugobenichi): eventually import these values from
// platform2/arc/network/address_manager.cc
// Port forwarding can only forward to IPv4 addresses within the IPv4 prefix
// used for static IPv4 subnet assignment to guest OSs and App platforms.
constexpr const char* kGuestSubnetCidr = "100.115.92.0/23";
constexpr const struct in_addr kGuestBaseAddr = {.s_addr =
Ipv4Addr(100, 115, 92, 0)};
constexpr const struct in_addr kGuestNetmask = {.s_addr =
Ipv4Addr(255, 255, 254, 0)};
const std::string ProtocolName(Protocol proto) {
if (proto == ModifyPortRuleRequest::INVALID_PROTOCOL) {
NOTREACHED() << "Unexpected L4 protocol value";
}
return base::ToLowerASCII(ModifyPortRuleRequest::Protocol_Name(proto));
}
std::string RuleTypeName(PortTracker::PortRuleType type) {
switch (type) {
case PortTracker::kUnknownRule:
return "UnknownRule";
case PortTracker::kAccessRule:
return "AccessRule";
case PortTracker::kLockdownRule:
return "LockdownRule";
case PortTracker::kForwardingRule:
return "ForwardingRule";
default:
NOTREACHED() << "Unknown rule type " << type;
return std::to_string(type);
}
}
std::ostream& operator<<(std::ostream& stream,
const PortTracker::PortRuleKey key) {
stream << "{ " << ProtocolName(key.proto) << " :"
<< std::to_string(key.input_dst_port) << "/" << key.input_ifname
<< " }";
return stream;
}
std::ostream& operator<<(std::ostream& stream,
const PortTracker::PortRule rule) {
stream << "{ " << RuleTypeName(rule.type) << " " << ProtocolName(rule.proto)
<< " :" << std::to_string(rule.input_dst_port) << "/"
<< rule.input_ifname << " -> " << rule.dst_ip << ":" << rule.dst_port
<< " }";
return stream;
}
} // namespace
PortTracker::PortTracker()
: task_runner_{base::ThreadTaskRunnerHandle::Get()} {}
// Test-only.
PortTracker::PortTracker(scoped_refptr<base::SequencedTaskRunner> task_runner)
: task_runner_{task_runner} {}
PortTracker::~PortTracker() {
RevokeAllPortRules();
}
bool PortTracker::ModifyPortRule(Operation op, const PortRule& rule) {
std::unique_ptr<patchpanel::Client> patchpanel_client =
patchpanel::Client::New();
if (!patchpanel_client) {
LOG(ERROR) << "Failed to open patchpanel client";
return false;
}
RuleType type;
switch (rule.type) {
case kAccessRule:
type = ModifyPortRuleRequest::ACCESS;
break;
case kLockdownRule:
type = ModifyPortRuleRequest::LOCKDOWN;
break;
case kForwardingRule:
type = ModifyPortRuleRequest::FORWARDING;
break;
default:
type = ModifyPortRuleRequest::INVALID_RULE_TYPE;
break;
}
return patchpanel_client->ModifyPortRule(
op, type, rule.proto, rule.input_ifname, rule.input_dst_ip,
rule.input_dst_port, rule.dst_ip, rule.dst_port);
}
bool PortTracker::AllowTcpPortAccess(uint16_t port,
const std::string& iface,
int dbus_fd) {
PortRule rule = {
.type = kAccessRule,
.proto = ModifyPortRuleRequest::TCP,
.input_dst_port = port,
.input_ifname = iface,
};
return AddPortRule(rule, dbus_fd);
}
bool PortTracker::AllowUdpPortAccess(uint16_t port,
const std::string& iface,
int dbus_fd) {
PortRule rule = {
.type = kAccessRule,
.proto = ModifyPortRuleRequest::UDP,
.input_dst_port = port,
.input_ifname = iface,
};
return AddPortRule(rule, dbus_fd);
}
bool PortTracker::RevokeTcpPortAccess(uint16_t port, const std::string& iface) {
PortRuleKey key = {
.proto = ModifyPortRuleRequest::TCP,
.input_dst_port = port,
.input_ifname = iface,
};
return RevokePortRule(key);
}
bool PortTracker::RevokeUdpPortAccess(uint16_t port, const std::string& iface) {
PortRuleKey key = {
.proto = ModifyPortRuleRequest::UDP,
.input_dst_port = port,
.input_ifname = iface,
};
return RevokePortRule(key);
}
bool PortTracker::AddPortRule(const PortRule& rule, int dbus_fd) {
if (!ValidatePortRule(rule)) {
return false;
}
PortRuleKey key = {
.proto = rule.proto,
.input_dst_port = rule.input_dst_port,
.input_ifname = rule.input_ifname,
};
// Check if the port is not already being forwarded or allowed for access.
if (port_rules_.find(key) != port_rules_.end()) {
// There is a very very small chance of a race here: a process exits without
// closing a firewall hole, and before the lifeline fd can trigger, another
// process requests the same port. This should be allowed, but if the
// lifeline fd hasn't triggered yet, it won't.
// Since permission_broker is single-threaded, this race is extremely
// unlikely to happen: the second request needs to come in at exactly the
// right time, after the first process exits but before the lifeline fd
// has triggered.
return false;
}
// We use |lifeline_fd| to track the lifetime of the process requesting
// port access.
int lifeline_fd = AddLifelineFd(dbus_fd);
if (lifeline_fd < 0) {
LOG(ERROR) << "Tracking lifeline fd for rule " << rule << " failed";
return false;
}
// Track the port rule.
port_rules_[key] = rule;
port_rules_[key].lifeline_fd = lifeline_fd;
lifeline_fds_[lifeline_fd] = key;
if (!ModifyPortRule(ModifyPortRuleRequest::CREATE, rule)) {
// If we fail to punch the hole in the firewall, stop tracking the lifetime
// of the process.
LOG(ERROR) << "Failed to create rule " << rule;
DeleteLifelineFd(lifeline_fd);
lifeline_fds_.erase(lifeline_fd);
port_rules_.erase(key);
return false;
}
return true;
}
void PortTracker::RevokeAllPortRules() {
VLOG(1) << "Revoking all port rules";
// Copy the container so that we can remove elements from the original.
std::vector<PortRuleKey> all_rules;
all_rules.reserve(lifeline_fds_.size());
for (const auto& kv : lifeline_fds_) {
all_rules.push_back(kv.second);
}
for (const PortRuleKey& key : all_rules) {
RevokePortRule(key);
}
CHECK(!HasActiveRules()) << "Failed to revoke all port rules";
}
bool PortTracker::LockDownLoopbackTcpPort(uint16_t port, int dbus_fd) {
PortRule rule = {
.type = kLockdownRule,
.proto = ModifyPortRuleRequest::TCP,
.input_dst_port = port,
.input_ifname = kLocalhost,
};
return AddPortRule(rule, dbus_fd);
}
bool PortTracker::ReleaseLoopbackTcpPort(uint16_t port) {
PortRuleKey key = {
.proto = ModifyPortRuleRequest::TCP,
.input_dst_port = port,
.input_ifname = kLocalhost,
};
return RevokePortRule(key);
}
bool PortTracker::StartTcpPortForwarding(uint16_t input_dst_port,
const std::string& input_ifname,
const std::string& dst_ip,
uint16_t dst_port,
int dbus_fd) {
PortRule rule = {
.type = kForwardingRule,
.proto = ModifyPortRuleRequest::TCP,
.input_dst_port = input_dst_port,
.input_ifname = input_ifname,
.dst_ip = dst_ip,
.dst_port = dst_port,
};
return AddPortRule(rule, dbus_fd);
}
bool PortTracker::StartUdpPortForwarding(uint16_t input_dst_port,
const std::string& input_ifname,
const std::string& dst_ip,
uint16_t dst_port,
int dbus_fd) {
PortRule rule = {
.type = kForwardingRule,
.proto = ModifyPortRuleRequest::UDP,
.input_dst_port = input_dst_port,
.input_ifname = input_ifname,
.dst_ip = dst_ip,
.dst_port = dst_port,
};
return AddPortRule(rule, dbus_fd);
}
bool PortTracker::StopTcpPortForwarding(uint16_t input_dst_port,
const std::string& input_ifname) {
PortRuleKey key = {
.proto = ModifyPortRuleRequest::TCP,
.input_dst_port = input_dst_port,
.input_ifname = input_ifname,
};
return RevokePortRule(key);
}
bool PortTracker::StopUdpPortForwarding(uint16_t input_dst_port,
const std::string& input_ifname) {
PortRuleKey key = {
.proto = ModifyPortRuleRequest::UDP,
.input_dst_port = input_dst_port,
.input_ifname = input_ifname,
};
return RevokePortRule(key);
}
bool PortTracker::ValidatePortRule(const PortRule& rule) {
switch (rule.type) {
case kAccessRule:
case kLockdownRule:
case kForwardingRule:
break;
default:
CHECK(false) << "Unknown port rule type value " << rule.type;
return false;
}
switch (rule.proto) {
case ModifyPortRuleRequest::TCP:
case ModifyPortRuleRequest::UDP:
break;
default:
CHECK(false) << "Unknown L4 protocol value " << rule.proto;
return false;
}
// TODO(hugobenichi): add some validation for port access and port lockdown
// rules as well.
switch (rule.type) {
case kForwardingRule: {
// Redirecting a reserved port is not allowed.
// Forwarding into a reserved port of the guest is allowed.
if (rule.input_dst_port <= kLastSystemPort) {
LOG(ERROR) << "Cannot forward system port " << rule.input_dst_port;
return false;
}
struct in_addr addr;
if (inet_pton(AF_INET, rule.dst_ip.c_str(), &addr) != 1) {
LOG(ERROR) << "Cannot forward to invalid IPv4 address " << rule.dst_ip;
return false;
}
if ((addr.s_addr & kGuestNetmask.s_addr) != kGuestBaseAddr.s_addr) {
LOG(ERROR) << "Cannot forward to IPv4 address " << rule.dst_ip
<< " outside of " << kGuestSubnetCidr;
return false;
}
if (rule.input_ifname.empty()) {
PLOG(ERROR) << "No interface name provided";
return false;
}
bool allowedInputIface = false;
for (const auto& prefix : kAllowedInterfacePrefixes) {
if (base::StartsWith(rule.input_ifname, prefix,
base::CompareCase::SENSITIVE)) {
allowedInputIface = true;
break;
}
}
if (!allowedInputIface) {
PLOG(ERROR) << "Cannot forward traffic from interface "
<< rule.input_ifname;
return false;
}
break;
}
default:
break;
}
return true;
}
void PortTracker::OnFileDescriptorReadable(int fd) {
// The process that requested this port has died/exited, so we need to plug
// the hole.
if (lifeline_fds_.find(fd) == lifeline_fds_.end()) {
LOG(ERROR) << "File descriptor " << fd << " was not being tracked";
DeleteLifelineFd(fd);
} else {
if (!RevokePortRule(lifeline_fds_[fd])) {
DeleteLifelineFd(fd);
}
}
}
int PortTracker::AddLifelineFd(int dbus_fd) {
int fd = dup(dbus_fd);
if (fd < 0) {
PLOG(ERROR) << "dup(dbus_fd) failed";
return -1;
}
lifeline_fd_controllers_[fd] = base::FileDescriptorWatcher::WatchReadable(
fd, base::BindRepeating(&PortTracker::OnFileDescriptorReadable,
// The callback will not outlive the object.
base::Unretained(this), fd));
return fd;
}
bool PortTracker::DeleteLifelineFd(int fd) {
auto iter = lifeline_fd_controllers_.find(fd);
if (iter == lifeline_fd_controllers_.end()) {
return false;
}
iter->second.reset(); // Destruct the controller, which removes the callback.
lifeline_fd_controllers_.erase(iter);
// AddLifelineFd() calls dup(), so this function should close the fd.
// We still return true since at this point the FileDescriptorWatcher object
// has been destructed.
if (IGNORE_EINTR(close(fd)) < 0) {
PLOG(ERROR) << "close(lifeline_fd)";
}
return true;
}
bool PortTracker::HasActiveRules() {
return !lifeline_fds_.empty();
}
bool PortTracker::RevokePortRule(const PortRuleKey key) {
if (port_rules_.find(key) == port_rules_.end()) {
LOG(ERROR) << "No port rule found for " << key;
return false;
}
PortRule rule = port_rules_[key];
bool deleted = DeleteLifelineFd(rule.lifeline_fd);
if (!deleted) {
LOG(ERROR) << "Failed to delete watcher for file descriptor "
<< rule.lifeline_fd << " from task runner";
}
port_rules_.erase(key);
lifeline_fds_.erase(rule.lifeline_fd);
return deleted && ModifyPortRule(ModifyPortRuleRequest::DELETE, rule);
}
} // namespace permission_broker