blob: 2318ebfeacbc0a58765c4cf44f79afc5a552fd08 [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 <sys/epoll.h>
#include <unistd.h>
#include <string>
#include <vector>
#include <base/bind.h>
#include <base/logging.h>
#include <base/posix/eintr_wrapper.h>
namespace {
const size_t kMaxInterfaceNameLen = 16;
const int kMaxEvents = 10;
const int64_t kLifelineIntervalSeconds = 5;
const int kInvalidHandle = -1;
}
namespace permission_broker {
PortTracker::PortTracker(org::chromium::FirewalldProxyInterface* firewalld)
: task_runner_{base::MessageLoopForIO::current()->task_runner()},
epfd_{kInvalidHandle},
vpn_lifeline_{kInvalidHandle},
firewalld_{firewalld} {}
// Test-only.
PortTracker::PortTracker(scoped_refptr<base::SequencedTaskRunner> task_runner,
org::chromium::FirewalldProxyInterface* firewalld)
: task_runner_{task_runner},
epfd_{kInvalidHandle},
vpn_lifeline_{kInvalidHandle},
firewalld_{firewalld} {}
PortTracker::~PortTracker() {
if (epfd_ >= 0) {
close(epfd_);
}
}
bool PortTracker::ProcessTcpPort(uint16_t port,
const std::string& iface,
int dbus_fd) {
// |iface| should be a short string.
if (iface.length() >= kMaxInterfaceNameLen) {
LOG(ERROR) << "Interface name '" << iface << "' is too long";
return false;
}
Hole hole = std::make_pair(port, iface);
if (tcp_fds_.find(hole) != tcp_fds_.end()) {
// This could potentially happen when a requesting process has just been
// restarted but scheduled lifeline FD check hasn't been performed yet, so
// we might have stale descriptors around. Force the FD check to see
// if they will be removed now.
CheckLifelineFds(false);
// Then try again. If this still fails, we know it's an invalid request.
if (tcp_fds_.find(hole) != tcp_fds_.end()) {
LOG(ERROR) << "Hole already punched";
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 TCP port " << port << " failed";
return false;
}
// Track the hole.
tcp_holes_[lifeline_fd] = hole;
tcp_fds_[hole] = lifeline_fd;
bool success;
firewalld_->PunchTcpHole(port, iface, &success, nullptr);
if (!success) {
// If we fail to punch the hole in the firewall, stop tracking the lifetime
// of the process.
LOG(ERROR) << "Failed to punch hole for TCP port " << port;
DeleteLifelineFd(lifeline_fd);
tcp_holes_.erase(lifeline_fd);
tcp_fds_.erase(hole);
return false;
}
return true;
}
bool PortTracker::ProcessUdpPort(uint16_t port,
const std::string& iface,
int dbus_fd) {
// |iface| should be a short string.
if (iface.length() >= kMaxInterfaceNameLen) {
LOG(ERROR) << "Interface name '" << iface << "' is too long";
return false;
}
Hole hole = std::make_pair(port, iface);
if (udp_fds_.find(hole) != udp_fds_.end()) {
// This could potentially happen when a requesting process has just been
// restarted but scheduled lifeline FD check hasn't been performed yet, so
// we might have stale descriptors around. Force the FD check to see
// if they will be removed now.
CheckLifelineFds(false);
// Then try again. If this still fails, we know it's an invalid request.
if (udp_fds_.find(hole) != udp_fds_.end()) {
LOG(ERROR) << "Hole already punched";
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 UDP port " << port << " failed";
return false;
}
// Track the hole.
udp_holes_[lifeline_fd] = hole;
udp_fds_[hole] = lifeline_fd;
bool success;
firewalld_->PunchUdpHole(port, iface, &success, nullptr);
if (!success) {
// If we fail to punch the hole in the firewall, stop tracking the lifetime
// of the process.
LOG(ERROR) << "Failed to punch hole for UDP port " << port;
DeleteLifelineFd(lifeline_fd);
udp_holes_.erase(lifeline_fd);
udp_fds_.erase(hole);
return false;
}
return true;
}
bool PortTracker::ReleaseTcpPort(uint16_t port, const std::string& iface) {
Hole hole = std::make_pair(port, iface);
auto p = tcp_fds_.find(hole);
if (p == tcp_fds_.end()) {
LOG(ERROR) << "Not tracking TCP port " << port << " on interface '" << iface
<< "'";
return false;
}
int fd = p->second;
bool plugged = PlugFirewallHole(fd);
bool deleted = DeleteLifelineFd(fd);
// PlugFirewallHole() prints an error message on failure,
// but DeleteLifelineFd() does not, and even if it did,
// we mock it out in tests.
if (!deleted) {
LOG(ERROR) << "Failed to delete file descriptor " << fd
<< " from epoll instance";
}
return plugged && deleted;
}
bool PortTracker::ReleaseUdpPort(uint16_t port, const std::string& iface) {
Hole hole = std::make_pair(port, iface);
auto p = udp_fds_.find(hole);
if (p == udp_fds_.end()) {
LOG(ERROR) << "Not tracking UDP port " << port << " on interface '" << iface
<< "'";
return false;
}
int fd = p->second;
bool plugged = PlugFirewallHole(fd);
bool deleted = DeleteLifelineFd(fd);
// PlugFirewallHole() prints an error message on failure,
// but DeleteLifelineFd() does not, and even if it did,
// we mock it out in tests.
if (!deleted) {
LOG(ERROR) << "Failed to delete file descriptor " << fd
<< " from epoll instance";
}
return plugged && deleted;
}
bool PortTracker::ProcessVpnSetup(
const std::vector<std::string>& usernames,
const std::string& interface,
int dbus_fd) {
if (vpn_lifeline_ != kInvalidHandle) {
LOG(ERROR) << "Already tracking a VPN lifeline";
return false;
}
// We use |lifeline_fd| to track the lifetime of the process requesting
// VPN setup.
int lifeline_fd = AddLifelineFd(dbus_fd);
if (lifeline_fd < 0) {
LOG(ERROR) << "Tracking lifeline fd for VPN failed";
return false;
}
bool success;
firewalld_->RequestVpnSetup(usernames, interface, &success, nullptr);
if (!success) {
LOG(ERROR) << "Failed to set up rules for VPN";
DeleteVpnRules();
DeleteLifelineFd(lifeline_fd);
return false;
}
vpn_usernames_ = usernames;
vpn_interface_ = interface;
vpn_lifeline_ = lifeline_fd;
return true;
}
bool PortTracker::DeleteVpnRules() {
bool success = true;
firewalld_->RemoveVpnSetup(vpn_usernames_, vpn_interface_, &success, nullptr);
vpn_usernames_.clear();
vpn_interface_.clear();
vpn_lifeline_ = kInvalidHandle;
return success;
}
bool PortTracker::RemoveVpnSetup() {
if (vpn_lifeline_ == kInvalidHandle) {
LOG(ERROR) << "RemoveVpnSetup called without VPN rules set";
return false;
}
DeleteLifelineFd(vpn_lifeline_);
if (!DeleteVpnRules()) {
LOG(ERROR) << "Failed to delete VPN rules";
return false;
}
return true;
}
int PortTracker::AddLifelineFd(int dbus_fd) {
if (!InitializeEpollOnce()) {
return kInvalidHandle;
}
int fd = dup(dbus_fd);
struct epoll_event epevent;
epevent.events = EPOLLIN; // EPOLLERR | EPOLLHUP are always waited for.
epevent.data.fd = fd;
LOG(INFO) << "Adding file descriptor " << fd << " to epoll instance";
if (epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &epevent) != 0) {
PLOG(ERROR) << "epoll_ctl(EPOLL_CTL_ADD)";
return kInvalidHandle;
}
// If this is the first port request, start lifeline checks.
if ((tcp_holes_.size() + udp_holes_.size() == 0) &&
vpn_lifeline_ == kInvalidHandle) {
LOG(INFO) << "Starting lifeline checks";
ScheduleLifelineCheck();
}
return fd;
}
bool PortTracker::DeleteLifelineFd(int fd) {
if (epfd_ < 0) {
LOG(ERROR) << "epoll instance not created";
return false;
}
LOG(INFO) << "Deleting file descriptor " << fd << " from epoll instance";
if (epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, nullptr) != 0) {
PLOG(ERROR) << "epoll_ctl(EPOLL_CTL_DEL)";
return false;
}
// AddLifelineFd() calls dup(), so this function should close the fd.
// We still return true since at this point the fd has been deleted from the
// epoll instance.
if (IGNORE_EINTR(close(fd)) < 0) {
PLOG(ERROR) << "close(lifeline_fd)";
}
return true;
}
void PortTracker::CheckLifelineFds(bool reschedule_check) {
if (epfd_ < 0) {
return;
}
struct epoll_event epevents[kMaxEvents];
int nready = epoll_wait(epfd_, epevents, kMaxEvents, 0 /* do not block */);
if (nready < 0) {
PLOG(ERROR) << "epoll_wait(0)";
return;
}
if (nready == 0) {
if (reschedule_check)
ScheduleLifelineCheck();
return;
}
for (size_t eidx = 0; eidx < (size_t)nready; eidx++) {
uint32_t events = epevents[eidx].events;
int fd = epevents[eidx].data.fd;
if ((events & (EPOLLHUP | EPOLLERR))) {
// The process that requested this port has died/exited,
// so we need to plug the hole.
PlugFirewallHole(fd);
DeleteLifelineFd(fd);
}
}
if (reschedule_check) {
// If there's still processes to track, schedule lifeline checks.
if (tcp_holes_.size() + tcp_holes_.size() > 0 ||
vpn_lifeline_ != kInvalidHandle) {
ScheduleLifelineCheck();
} else {
LOG(INFO) << "Stopping lifeline checks";
}
}
}
void PortTracker::ScheduleLifelineCheck() {
task_runner_->PostDelayedTask(
FROM_HERE,
base::Bind(&PortTracker::CheckLifelineFds, base::Unretained(this), true),
base::TimeDelta::FromSeconds(kLifelineIntervalSeconds));
}
bool PortTracker::PlugFirewallHole(int fd) {
bool dbus_sucess = false;
Hole hole;
if (tcp_holes_.find(fd) != tcp_holes_.end()) {
// It was a TCP hole.
hole = tcp_holes_[fd];
firewalld_->PlugTcpHole(hole.first, hole.second, &dbus_sucess, nullptr);
tcp_holes_.erase(fd);
tcp_fds_.erase(hole);
if (!dbus_sucess) {
LOG(ERROR) << "Failed to plug hole for TCP port " << hole.first
<< " on interface '" << hole.second << "'";
return false;
}
} else if (udp_holes_.find(fd) != udp_holes_.end()) {
// It was a UDP hole.
hole = udp_holes_[fd];
firewalld_->PlugUdpHole(hole.first, hole.second, &dbus_sucess, nullptr);
udp_holes_.erase(fd);
udp_fds_.erase(hole);
if (!dbus_sucess) {
LOG(ERROR) << "Failed to plug hole for UDP port " << hole.first
<< " on interface '" << hole.second << "'";
return false;
}
} else if (fd == vpn_lifeline_) {
if (!DeleteVpnRules()) {
LOG(ERROR) << "Failed to delete VPN rules";
return false;
}
} else {
LOG(ERROR) << "File descriptor " << fd << " was not being tracked";
return false;
}
return true;
}
bool PortTracker::InitializeEpollOnce() {
if (epfd_ < 0) {
// |size| needs to be > 0, but is otherwise ignored.
LOG(INFO) << "Creating epoll instance";
epfd_ = epoll_create(1 /* size */);
if (epfd_ < 0) {
PLOG(ERROR) << "epoll_create()";
return false;
}
}
return true;
}
} // namespace permission_broker