472 lines
15 KiB
C++
472 lines
15 KiB
C++
// Copyright 2014 The Android Open Source Project
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#include "iptables.h"
|
|
|
|
#include <linux/capability.h>
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include <base/bind.h>
|
|
#include <base/bind_helpers.h>
|
|
#include <base/callback.h>
|
|
#include <base/logging.h>
|
|
#include <base/strings/string_number_conversions.h>
|
|
#include <base/strings/string_util.h>
|
|
#include <base/strings/stringprintf.h>
|
|
#include <brillo/minijail/minijail.h>
|
|
#include <brillo/process.h>
|
|
|
|
namespace {
|
|
|
|
using IpTablesCallback = base::Callback<bool(const std::string&, bool)>;
|
|
|
|
#if defined(__ANDROID__)
|
|
const char kIpTablesPath[] = "/system/bin/iptables";
|
|
const char kIp6TablesPath[] = "/system/bin/ip6tables";
|
|
const char kIpPath[] = "/system/bin/ip";
|
|
#else
|
|
const char kIpTablesPath[] = "/sbin/iptables";
|
|
const char kIp6TablesPath[] = "/sbin/ip6tables";
|
|
const char kIpPath[] = "/bin/ip";
|
|
const char kUnprivilegedUser[] = "nobody";
|
|
#endif // __ANDROID__
|
|
|
|
const char kIPv4[] = "IPv4";
|
|
const char kIPv6[] = "IPv6";
|
|
|
|
const uint64_t kIpTablesCapMask =
|
|
CAP_TO_MASK(CAP_NET_ADMIN) | CAP_TO_MASK(CAP_NET_RAW);
|
|
|
|
// Interface names must be shorter than 'IFNAMSIZ' chars.
|
|
// See http://man7.org/linux/man-pages/man7/netdevice.7.html
|
|
// 'IFNAMSIZ' is 16 in recent kernels.
|
|
// See http://lxr.free-electrons.com/source/include/uapi/linux/if.h#L26
|
|
const size_t kInterfaceNameSize = 16;
|
|
|
|
const char kMarkForUserTraffic[] = "1";
|
|
|
|
const char kTableIdForUserTraffic[] = "1";
|
|
|
|
bool IsValidInterfaceName(const std::string& iface) {
|
|
// |iface| should be shorter than |kInterfaceNameSize| chars and have only
|
|
// alphanumeric characters (embedded hypens and periods are also permitted).
|
|
if (iface.length() >= kInterfaceNameSize) {
|
|
return false;
|
|
}
|
|
if (base::StartsWith(iface, "-", base::CompareCase::SENSITIVE) ||
|
|
base::EndsWith(iface, "-", base::CompareCase::SENSITIVE) ||
|
|
base::StartsWith(iface, ".", base::CompareCase::SENSITIVE) ||
|
|
base::EndsWith(iface, ".", base::CompareCase::SENSITIVE)) {
|
|
return false;
|
|
}
|
|
for (auto c : iface) {
|
|
if (!std::isalnum(c) && (c != '-') && (c != '.')) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool RunForAllArguments(const IpTablesCallback& iptables_cmd,
|
|
const std::vector<std::string>& arguments,
|
|
bool add) {
|
|
bool success = true;
|
|
for (const auto& argument : arguments) {
|
|
if (!iptables_cmd.Run(argument, add)) {
|
|
// On failure, only abort if rules are being added.
|
|
// If removing a rule fails, attempt the remaining removals but still
|
|
// return 'false'.
|
|
success = false;
|
|
if (add)
|
|
break;
|
|
}
|
|
}
|
|
return success;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
namespace firewalld {
|
|
|
|
IpTables::IpTables() {
|
|
}
|
|
|
|
IpTables::~IpTables() {
|
|
// Plug all holes when destructed.
|
|
PlugAllHoles();
|
|
}
|
|
|
|
bool IpTables::PunchTcpHole(uint16_t in_port, const std::string& in_interface) {
|
|
return PunchHole(in_port, in_interface, &tcp_holes_, kProtocolTcp);
|
|
}
|
|
|
|
bool IpTables::PunchUdpHole(uint16_t in_port, const std::string& in_interface) {
|
|
return PunchHole(in_port, in_interface, &udp_holes_, kProtocolUdp);
|
|
}
|
|
|
|
bool IpTables::PlugTcpHole(uint16_t in_port, const std::string& in_interface) {
|
|
return PlugHole(in_port, in_interface, &tcp_holes_, kProtocolTcp);
|
|
}
|
|
|
|
bool IpTables::PlugUdpHole(uint16_t in_port, const std::string& in_interface) {
|
|
return PlugHole(in_port, in_interface, &udp_holes_, kProtocolUdp);
|
|
}
|
|
|
|
bool IpTables::RequestVpnSetup(const std::vector<std::string>& usernames,
|
|
const std::string& interface) {
|
|
return ApplyVpnSetup(usernames, interface, true /* add */);
|
|
}
|
|
|
|
bool IpTables::RemoveVpnSetup(const std::vector<std::string>& usernames,
|
|
const std::string& interface) {
|
|
return ApplyVpnSetup(usernames, interface, false /* delete */);
|
|
}
|
|
|
|
bool IpTables::PunchHole(uint16_t port,
|
|
const std::string& interface,
|
|
std::set<Hole>* holes,
|
|
ProtocolEnum protocol) {
|
|
if (port == 0) {
|
|
// Port 0 is not a valid TCP/UDP port.
|
|
return false;
|
|
}
|
|
|
|
if (!IsValidInterfaceName(interface)) {
|
|
LOG(ERROR) << "Invalid interface name '" << interface << "'";
|
|
return false;
|
|
}
|
|
|
|
Hole hole = std::make_pair(port, interface);
|
|
if (holes->find(hole) != holes->end()) {
|
|
// We have already punched a hole for |port| on |interface|.
|
|
// Be idempotent: do nothing and succeed.
|
|
return true;
|
|
}
|
|
|
|
std::string sprotocol = protocol == kProtocolTcp ? "TCP" : "UDP";
|
|
LOG(INFO) << "Punching hole for " << sprotocol << " port " << port
|
|
<< " on interface '" << interface << "'";
|
|
if (!AddAcceptRules(protocol, port, interface)) {
|
|
// If the 'iptables' command fails, this method fails.
|
|
LOG(ERROR) << "Adding ACCEPT rules failed.";
|
|
return false;
|
|
}
|
|
|
|
// Track the hole we just punched.
|
|
holes->insert(hole);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IpTables::PlugHole(uint16_t port,
|
|
const std::string& interface,
|
|
std::set<Hole>* holes,
|
|
ProtocolEnum protocol) {
|
|
if (port == 0) {
|
|
// Port 0 is not a valid TCP/UDP port.
|
|
return false;
|
|
}
|
|
|
|
Hole hole = std::make_pair(port, interface);
|
|
|
|
if (holes->find(hole) == holes->end()) {
|
|
// There is no firewall hole for |port| on |interface|.
|
|
// Even though this makes |PlugHole| not idempotent,
|
|
// and Punch/Plug not entirely symmetrical, fail. It might help catch bugs.
|
|
return false;
|
|
}
|
|
|
|
std::string sprotocol = protocol == kProtocolTcp ? "TCP" : "UDP";
|
|
LOG(INFO) << "Plugging hole for " << sprotocol << " port " << port
|
|
<< " on interface '" << interface << "'";
|
|
if (!DeleteAcceptRules(protocol, port, interface)) {
|
|
// If the 'iptables' command fails, this method fails.
|
|
LOG(ERROR) << "Deleting ACCEPT rules failed.";
|
|
return false;
|
|
}
|
|
|
|
// Stop tracking the hole we just plugged.
|
|
holes->erase(hole);
|
|
|
|
return true;
|
|
}
|
|
|
|
void IpTables::PlugAllHoles() {
|
|
// Copy the container so that we can remove elements from the original.
|
|
// TCP
|
|
std::set<Hole> holes = tcp_holes_;
|
|
for (auto hole : holes) {
|
|
PlugHole(hole.first /* port */, hole.second /* interface */, &tcp_holes_,
|
|
kProtocolTcp);
|
|
}
|
|
|
|
// UDP
|
|
holes = udp_holes_;
|
|
for (auto hole : holes) {
|
|
PlugHole(hole.first /* port */, hole.second /* interface */, &udp_holes_,
|
|
kProtocolUdp);
|
|
}
|
|
|
|
CHECK(tcp_holes_.size() == 0) << "Failed to plug all TCP holes.";
|
|
CHECK(udp_holes_.size() == 0) << "Failed to plug all UDP holes.";
|
|
}
|
|
|
|
bool IpTables::AddAcceptRules(ProtocolEnum protocol,
|
|
uint16_t port,
|
|
const std::string& interface) {
|
|
if (!AddAcceptRule(kIpTablesPath, protocol, port, interface)) {
|
|
LOG(ERROR) << "Could not add ACCEPT rule using '" << kIpTablesPath << "'";
|
|
return false;
|
|
}
|
|
|
|
if (AddAcceptRule(kIp6TablesPath, protocol, port, interface)) {
|
|
// This worked, record this fact and insist that it works thereafter.
|
|
ip6_enabled_ = true;
|
|
} else if (ip6_enabled_) {
|
|
// It's supposed to work, fail.
|
|
LOG(ERROR) << "Could not add ACCEPT rule using '" << kIp6TablesPath
|
|
<< "', aborting operation.";
|
|
DeleteAcceptRule(kIpTablesPath, protocol, port, interface);
|
|
return false;
|
|
} else {
|
|
// It never worked, just ignore it.
|
|
LOG(WARNING) << "Could not add ACCEPT rule using '" << kIp6TablesPath
|
|
<< "', ignoring.";
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IpTables::DeleteAcceptRules(ProtocolEnum protocol,
|
|
uint16_t port,
|
|
const std::string& interface) {
|
|
bool ip4_success = DeleteAcceptRule(kIpTablesPath, protocol, port,
|
|
interface);
|
|
bool ip6_success = !ip6_enabled_ || DeleteAcceptRule(kIp6TablesPath, protocol,
|
|
port, interface);
|
|
return ip4_success && ip6_success;
|
|
}
|
|
|
|
bool IpTables::ApplyVpnSetup(const std::vector<std::string>& usernames,
|
|
const std::string& interface,
|
|
bool add) {
|
|
bool success = true;
|
|
std::vector<std::string> added_usernames;
|
|
|
|
if (!ApplyRuleForUserTraffic(add)) {
|
|
if (add) {
|
|
ApplyRuleForUserTraffic(false /* remove */);
|
|
return false;
|
|
}
|
|
success = false;
|
|
}
|
|
|
|
if (!ApplyMasquerade(interface, add)) {
|
|
if (add) {
|
|
ApplyVpnSetup(added_usernames, interface, false /* remove */);
|
|
return false;
|
|
}
|
|
success = false;
|
|
}
|
|
|
|
for (const auto& username : usernames) {
|
|
if (!ApplyMarkForUserTraffic(username, add)) {
|
|
if (add) {
|
|
ApplyVpnSetup(added_usernames, interface, false /* remove */);
|
|
return false;
|
|
}
|
|
success = false;
|
|
}
|
|
if (add) {
|
|
added_usernames.push_back(username);
|
|
}
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
bool IpTables::ApplyMasquerade(const std::string& interface, bool add) {
|
|
const IpTablesCallback apply_masquerade =
|
|
base::Bind(&IpTables::ApplyMasqueradeWithExecutable,
|
|
base::Unretained(this),
|
|
interface);
|
|
|
|
return RunForAllArguments(
|
|
apply_masquerade, {kIpTablesPath, kIp6TablesPath}, add);
|
|
}
|
|
|
|
bool IpTables::ApplyMarkForUserTraffic(const std::string& username, bool add) {
|
|
const IpTablesCallback apply_mark =
|
|
base::Bind(&IpTables::ApplyMarkForUserTrafficWithExecutable,
|
|
base::Unretained(this),
|
|
username);
|
|
|
|
return RunForAllArguments(apply_mark, {kIpTablesPath, kIp6TablesPath}, add);
|
|
}
|
|
|
|
bool IpTables::ApplyRuleForUserTraffic(bool add) {
|
|
const IpTablesCallback apply_rule = base::Bind(
|
|
&IpTables::ApplyRuleForUserTrafficWithVersion, base::Unretained(this));
|
|
|
|
return RunForAllArguments(apply_rule, {kIPv4, kIPv6}, add);
|
|
}
|
|
|
|
bool IpTables::AddAcceptRule(const std::string& executable_path,
|
|
ProtocolEnum protocol,
|
|
uint16_t port,
|
|
const std::string& interface) {
|
|
std::vector<std::string> argv;
|
|
argv.push_back(executable_path);
|
|
argv.push_back("-I"); // insert
|
|
argv.push_back("INPUT");
|
|
argv.push_back("-p"); // protocol
|
|
argv.push_back(protocol == kProtocolTcp ? "tcp" : "udp");
|
|
argv.push_back("--dport"); // destination port
|
|
argv.push_back(std::to_string(port));
|
|
if (!interface.empty()) {
|
|
argv.push_back("-i"); // interface
|
|
argv.push_back(interface);
|
|
}
|
|
argv.push_back("-j");
|
|
argv.push_back("ACCEPT");
|
|
argv.push_back("-w"); // Wait for xtables lock.
|
|
|
|
// Use CAP_NET_ADMIN|CAP_NET_RAW.
|
|
return ExecvNonRoot(argv, kIpTablesCapMask) == 0;
|
|
}
|
|
|
|
bool IpTables::DeleteAcceptRule(const std::string& executable_path,
|
|
ProtocolEnum protocol,
|
|
uint16_t port,
|
|
const std::string& interface) {
|
|
std::vector<std::string> argv;
|
|
argv.push_back(executable_path);
|
|
argv.push_back("-D"); // delete
|
|
argv.push_back("INPUT");
|
|
argv.push_back("-p"); // protocol
|
|
argv.push_back(protocol == kProtocolTcp ? "tcp" : "udp");
|
|
argv.push_back("--dport"); // destination port
|
|
argv.push_back(std::to_string(port));
|
|
if (interface != "") {
|
|
argv.push_back("-i"); // interface
|
|
argv.push_back(interface);
|
|
}
|
|
argv.push_back("-j");
|
|
argv.push_back("ACCEPT");
|
|
argv.push_back("-w"); // Wait for xtables lock.
|
|
|
|
// Use CAP_NET_ADMIN|CAP_NET_RAW.
|
|
return ExecvNonRoot(argv, kIpTablesCapMask) == 0;
|
|
}
|
|
|
|
bool IpTables::ApplyMasqueradeWithExecutable(const std::string& interface,
|
|
const std::string& executable_path,
|
|
bool add) {
|
|
std::vector<std::string> argv;
|
|
argv.push_back(executable_path);
|
|
argv.push_back("-t"); // table
|
|
argv.push_back("nat");
|
|
argv.push_back(add ? "-A" : "-D"); // rule
|
|
argv.push_back("POSTROUTING");
|
|
argv.push_back("-o"); // output interface
|
|
argv.push_back(interface);
|
|
argv.push_back("-j");
|
|
argv.push_back("MASQUERADE");
|
|
|
|
// Use CAP_NET_ADMIN|CAP_NET_RAW.
|
|
bool success = ExecvNonRoot(argv, kIpTablesCapMask) == 0;
|
|
|
|
if (!success) {
|
|
LOG(ERROR) << (add ? "Adding" : "Removing")
|
|
<< " masquerade failed for interface " << interface
|
|
<< " using '" << executable_path << "'";
|
|
}
|
|
return success;
|
|
}
|
|
|
|
bool IpTables::ApplyMarkForUserTrafficWithExecutable(
|
|
const std::string& username, const std::string& executable_path, bool add) {
|
|
std::vector<std::string> argv;
|
|
argv.push_back(executable_path);
|
|
argv.push_back("-t"); // table
|
|
argv.push_back("mangle");
|
|
argv.push_back(add ? "-A" : "-D"); // rule
|
|
argv.push_back("OUTPUT");
|
|
argv.push_back("-m");
|
|
argv.push_back("owner");
|
|
argv.push_back("--uid-owner");
|
|
argv.push_back(username);
|
|
argv.push_back("-j");
|
|
argv.push_back("MARK");
|
|
argv.push_back("--set-mark");
|
|
argv.push_back(kMarkForUserTraffic);
|
|
|
|
// Use CAP_NET_ADMIN|CAP_NET_RAW.
|
|
bool success = ExecvNonRoot(argv, kIpTablesCapMask) == 0;
|
|
|
|
if (!success) {
|
|
LOG(ERROR) << (add ? "Adding" : "Removing")
|
|
<< " mark failed for user " << username
|
|
<< " using '" << kIpTablesPath << "'";
|
|
}
|
|
return success;
|
|
}
|
|
|
|
bool IpTables::ApplyRuleForUserTrafficWithVersion(const std::string& ip_version,
|
|
bool add) {
|
|
brillo::ProcessImpl ip;
|
|
ip.AddArg(kIpPath);
|
|
if (ip_version == kIPv6)
|
|
ip.AddArg("-6");
|
|
ip.AddArg("rule");
|
|
ip.AddArg(add ? "add" : "delete");
|
|
ip.AddArg("fwmark");
|
|
ip.AddArg(kMarkForUserTraffic);
|
|
ip.AddArg("table");
|
|
ip.AddArg(kTableIdForUserTraffic);
|
|
|
|
bool success = ip.Run() == 0;
|
|
|
|
if (!success) {
|
|
LOG(ERROR) << (add ? "Adding" : "Removing") << " rule for " << ip_version
|
|
<< " user traffic failed";
|
|
}
|
|
return success;
|
|
}
|
|
|
|
int IpTables::ExecvNonRoot(const std::vector<std::string>& argv,
|
|
uint64_t capmask) {
|
|
brillo::Minijail* m = brillo::Minijail::GetInstance();
|
|
minijail* jail = m->New();
|
|
#if !defined(__ANDROID__)
|
|
// TODO(garnold) This needs to be re-enabled once we figure out which
|
|
// unprivileged user we want to use.
|
|
m->DropRoot(jail, kUnprivilegedUser, kUnprivilegedUser);
|
|
#endif // __ANDROID__
|
|
m->UseCapabilities(jail, capmask);
|
|
|
|
std::vector<char*> args;
|
|
for (const auto& arg : argv) {
|
|
args.push_back(const_cast<char*>(arg.c_str()));
|
|
}
|
|
args.push_back(nullptr);
|
|
|
|
int status;
|
|
bool ran = m->RunSyncAndDestroy(jail, args, &status);
|
|
return ran ? status : -1;
|
|
}
|
|
|
|
} // namespace firewalld
|