// 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