| // Copyright 2020 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/counters_service.h" | |
| #include <compare> | |
| #include <memory> | |
| #include <set> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include <base/check.h> | |
| #include <base/logging.h> | |
| #include <base/strings/strcat.h> | |
| #include <base/strings/string_split.h> | |
| #include <re2/re2.h> | |
| #include "patchpanel/datapath.h" | |
| #include "patchpanel/iptables.h" | |
| #include "patchpanel/proto_utils.h" | |
| namespace patchpanel { | |
| namespace { | |
| using Counter = CountersService::Counter; | |
| using CounterKey = CountersService::CounterKey; | |
| constexpr char kVpnRxChainName[] = "rx_vpn"; | |
| constexpr char kVpnTxChainName[] = "tx_vpn"; | |
| constexpr char kRxTag[] = "rx_"; | |
| constexpr char kTxTag[] = "tx_"; | |
| // The following regexs and code is written and tested for iptables v1.6.2. | |
| // Output code of iptables can be found at: | |
| // https://git.netfilter.org/iptables/tree/iptables/iptables.c?h=v1.6.2 | |
| // The chain line looks like: | |
| // "Chain tx_eth0 (2 references)". | |
| // This regex extracts "tx" (direction), "eth0" (ifname) from this example. | |
| constexpr LazyRE2 kChainLine = {R"(Chain (rx|tx)_(\w+).*)"}; | |
| // The counter line for a defined source looks like (some spaces are deleted to | |
| // make it fit in one line): | |
| // " 5374 6172 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0 mark match 0x2000/0x3f00" | |
| // for IPv4. | |
| // " 5374 6172 RETURN all -- * * ::/0 ::/0 mark match 0x2000/0x3f00" for IPv6. | |
| // The final counter line for catching untagged traffic looks like: | |
| // " 5374 6172 all -- * * 0.0.0.0/0 0.0.0.0/0" for IPv4. | |
| // " 5374 6172 all -- * * ::/0 ::/0" for IPv6. | |
| // The first two counters are captured for pkts and bytes. For lines with a mark | |
| // matcher, the source is also captured. | |
| constexpr LazyRE2 kCounterLine = {R"( *(\d+) +(\d+).*mark match (.*)/0x3f00)"}; | |
| constexpr LazyRE2 kFinalCounterLine = { | |
| R"( *(\d+) +(\d+).*(?:0\.0\.0\.0/0|::/0)\s*)"}; | |
| bool MatchCounterLine(const std::string& line, | |
| uint64_t* pkts, | |
| uint64_t* bytes, | |
| TrafficSource* source) { | |
| Fwmark mark; | |
| if (RE2::FullMatch(line, *kCounterLine, pkts, bytes, | |
| RE2::Hex(&mark.fwmark))) { | |
| *source = mark.Source(); | |
| return true; | |
| } | |
| if (RE2::FullMatch(line, *kFinalCounterLine, pkts, bytes)) { | |
| *source = TrafficSource::kUnknown; | |
| return true; | |
| } | |
| return false; | |
| } | |
| // Parses the output of `iptables -L -x -v` (or `ip6tables`) and adds the parsed | |
| // values into the corresponding counters in |counters|. An example of |output| | |
| // can be found in the test file. This function will try to find the pattern of: | |
| // <one chain line for an accounting chain> | |
| // <one header line> | |
| // <one counter line for an accounting rule> | |
| // The interface name and direction (rx or tx) will be extracted from the chain | |
| // line, and then the values extracted from the counter line will be added into | |
| // the counter for that interface. Note that this function will not fully | |
| // validate if |output| is an output from iptables. | |
| bool ParseOutput(const std::string& output, | |
| const std::set<std::string>& devices, | |
| const TrafficCounter::IpFamily ip_family, | |
| std::map<CounterKey, Counter>* counters) { | |
| DCHECK(counters); | |
| const std::vector<std::string> lines = base::SplitString( | |
| output, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL); | |
| // Finds the chain line for an accounting chain first, and then parse the | |
| // following line(s) to get the counters for this chain. Repeats this process | |
| // until we reach the end of |output|. | |
| for (auto it = lines.cbegin(); it != lines.cend(); it++) { | |
| // Finds the chain name line. | |
| std::string direction, ifname; | |
| while (it != lines.cend() && | |
| !RE2::FullMatch(*it, *kChainLine, &direction, &ifname)) | |
| it++; | |
| if (it == lines.cend()) | |
| break; | |
| // Skips this group if this ifname is not requested. | |
| if (!devices.empty() && devices.find(ifname) == devices.end()) | |
| continue; | |
| // Skips if this chain is for multicast traffic counting. | |
| if (ifname.find("mdns") != std::string::npos || | |
| ifname.find("ssdp") != std::string::npos) { | |
| continue; | |
| } | |
| // Skips the chain name line and the header line. | |
| if (lines.cend() - it <= 2) { | |
| LOG(ERROR) << "Invalid iptables output for " << direction << "_" | |
| << ifname; | |
| return false; | |
| } | |
| it += 2; | |
| // Checks that there are some counter rules defined. | |
| if (it == lines.cend() || it->empty()) { | |
| LOG(ERROR) << "No counter rule defined for " << direction << "_" | |
| << ifname; | |
| return false; | |
| } | |
| // The next block of lines are the counters lines for individual sources. | |
| for (; it != lines.cend() && !it->empty(); it++) { | |
| uint64_t pkts, bytes; | |
| TrafficSource source; | |
| if (!MatchCounterLine(*it, &pkts, &bytes, &source)) { | |
| LOG(ERROR) << "Cannot parse counter line \"" << *it << "\" for " | |
| << direction << "_" << ifname; | |
| return false; | |
| } | |
| if (pkts == 0 && bytes == 0) | |
| continue; | |
| CounterKey key = {}; | |
| key.ifname = ifname; | |
| key.source = TrafficSourceToProto(source); | |
| key.ip_family = ip_family; | |
| auto& counter = (*counters)[key]; | |
| if (direction == "rx") { | |
| counter.rx_bytes += bytes; | |
| counter.rx_packets += pkts; | |
| } else { | |
| counter.tx_bytes += bytes; | |
| counter.tx_packets += pkts; | |
| } | |
| } | |
| if (it == lines.cend()) | |
| break; | |
| } | |
| return true; | |
| } | |
| } // namespace | |
| CountersService::CountersService(Datapath* datapath, ConntrackMonitor* monitor) | |
| : datapath_(datapath) { | |
| connmark_updater_ = std::make_unique<ConnmarkUpdater>(monitor); | |
| } | |
| CountersService::~CountersService() = default; | |
| std::map<CounterKey, Counter> CountersService::GetCounters( | |
| const std::set<std::string>& devices) { | |
| std::map<CounterKey, Counter> counters; | |
| // Handles counters for IPv4 and IPv6 separately and returns failure if either | |
| // of the procession fails, since counters for only IPv4 or IPv6 are biased. | |
| std::string iptables_result = | |
| datapath_->DumpIptables(IpFamily::kIPv4, Iptables::Table::kMangle); | |
| if (iptables_result.empty()) { | |
| LOG(ERROR) << "Failed to query IPv4 counters"; | |
| return {}; | |
| } | |
| if (!ParseOutput(iptables_result, devices, TrafficCounter::IPV4, &counters)) { | |
| LOG(ERROR) << "Failed to parse IPv4 counters"; | |
| return {}; | |
| } | |
| std::string ip6tables_result = | |
| datapath_->DumpIptables(IpFamily::kIPv6, Iptables::Table::kMangle); | |
| if (ip6tables_result.empty()) { | |
| LOG(ERROR) << "Failed to query IPv6 counters"; | |
| return {}; | |
| } | |
| if (!ParseOutput(ip6tables_result, devices, TrafficCounter::IPV6, | |
| &counters)) { | |
| LOG(ERROR) << "Failed to parse IPv6 counters"; | |
| return {}; | |
| } | |
| return counters; | |
| } | |
| void CountersService::HandleARCVPNSocketConnectionEvent( | |
| const SocketConnectionEvent& msg) { | |
| const auto conn = GetConntrack5Tuple(msg); | |
| if (!conn) { | |
| LOG(ERROR) << __func__ << ": failed to get conntrack 5 tuple"; | |
| return; | |
| } | |
| connmark_updater_->UpdateConnmark( | |
| *conn, Fwmark::FromSource(TrafficSource::kArcVpn), kFwmarkAllSourcesMask); | |
| } | |
| void CountersService::SetConnmarkUpdaterForTesting( | |
| std::unique_ptr<ConnmarkUpdater> updater) { | |
| connmark_updater_ = std::move(updater); | |
| } | |
| void CountersService::OnPhysicalDeviceAdded(const std::string& ifname) { | |
| std::string rx_chain = kRxTag + ifname; | |
| std::string tx_chain = kTxTag + ifname; | |
| SetupAccountingRules(rx_chain); | |
| SetupAccountingRules(tx_chain); | |
| SetupJumpRules(Iptables::Command::kA, ifname, rx_chain, tx_chain); | |
| } | |
| void CountersService::OnPhysicalDeviceRemoved(const std::string& ifname) { | |
| std::string rx_chain = kRxTag + ifname; | |
| std::string tx_chain = kTxTag + ifname; | |
| SetupJumpRules(Iptables::Command::kD, ifname, rx_chain, tx_chain); | |
| } | |
| void CountersService::OnVpnDeviceAdded(const std::string& ifname) { | |
| SetupAccountingRules(kVpnRxChainName); | |
| SetupAccountingRules(kVpnTxChainName); | |
| SetupJumpRules(Iptables::Command::kA, ifname, kVpnRxChainName, | |
| kVpnTxChainName); | |
| } | |
| void CountersService::OnVpnDeviceRemoved(const std::string& ifname) { | |
| SetupJumpRules(Iptables::Command::kD, ifname, kVpnRxChainName, | |
| kVpnTxChainName); | |
| } | |
| bool CountersService::AddAccountingRule(const std::string& chain_name, | |
| TrafficSource source) { | |
| std::string mark = base::StrCat({Fwmark::FromSource(source).ToString(), "/", | |
| kFwmarkAllSourcesMask.ToString()}); | |
| std::vector<std::string_view> args = {"-m", "mark", "--mark", mark, | |
| "-j", "RETURN", "-w"}; | |
| return datapath_->ModifyIptables(IpFamily::kDual, Iptables::Table::kMangle, | |
| Iptables::Command::kA, chain_name, args); | |
| } | |
| void CountersService::SetupAccountingRules(const std::string& chain) { | |
| // Stops if |chain| already exist. | |
| if (datapath_->CheckChain(IpFamily::kDual, Iptables::Table::kMangle, chain)) { | |
| return; | |
| } | |
| auto batch_mode = | |
| MinijailedProcessRunner::GetInstance()->AcquireIptablesBatchMode(); | |
| // Creates |chain|. | |
| if (!datapath_->AddChain(IpFamily::kDual, Iptables::Table::kMangle, chain)) { | |
| return; | |
| } | |
| // Add source accounting rules. | |
| for (TrafficSource source : kAllSources) { | |
| AddAccountingRule(chain, source); | |
| } | |
| // Add catch-all accounting rule for any remaining and untagged traffic. | |
| datapath_->ModifyIptables(IpFamily::kDual, Iptables::Table::kMangle, | |
| Iptables::Command::kA, chain, {"-w"}); | |
| } | |
| void CountersService::SetupJumpRules(Iptables::Command command, | |
| const std::string& ifname, | |
| const std::string& rx_chain, | |
| const std::string& tx_chain) { | |
| auto batch_mode = | |
| MinijailedProcessRunner::GetInstance()->AcquireIptablesBatchMode(); | |
| // For each device create a jumping rule in mangle POSTROUTING for egress | |
| // traffic, and two jumping rules in mangle INPUT and FORWARD for ingress | |
| // traffic. | |
| datapath_->ModifyIptables(IpFamily::kDual, Iptables::Table::kMangle, command, | |
| "FORWARD", {"-i", ifname, "-j", rx_chain, "-w"}); | |
| datapath_->ModifyIptables(IpFamily::kDual, Iptables::Table::kMangle, command, | |
| "INPUT", {"-i", ifname, "-j", rx_chain, "-w"}); | |
| datapath_->ModifyIptables(IpFamily::kDual, Iptables::Table::kMangle, command, | |
| "POSTROUTING", | |
| {"-o", ifname, "-j", tx_chain, "-w"}); | |
| } | |
| TrafficCounter::Source TrafficSourceToProto(TrafficSource source) { | |
| switch (source) { | |
| case TrafficSource::kChrome: | |
| return TrafficCounter::CHROME; | |
| case TrafficSource::kUser: | |
| return TrafficCounter::USER; | |
| case TrafficSource::kUpdateEngine: | |
| return TrafficCounter::UPDATE_ENGINE; | |
| case TrafficSource::kSystem: | |
| return TrafficCounter::SYSTEM; | |
| case TrafficSource::kHostVpn: | |
| return TrafficCounter::VPN; | |
| case TrafficSource::kArc: | |
| return TrafficCounter::ARC; | |
| case TrafficSource::kCrostiniVM: | |
| return TrafficCounter::CROSTINI_VM; | |
| case TrafficSource::kBruschettaVM: | |
| return TrafficCounter::BRUSCHETTA_VM; | |
| case TrafficSource::kBorealisVM: | |
| return TrafficCounter::BOREALIS_VM; | |
| case TrafficSource::kParallelsVM: | |
| return TrafficCounter::PARALLELS_VM; | |
| case TrafficSource::kTetherDownstream: | |
| return TrafficCounter::TETHERING; | |
| case TrafficSource::kWiFiDirect: | |
| return TrafficCounter::WIFI_DIRECT; | |
| case TrafficSource::kWiFiLOHS: | |
| return TrafficCounter::WIFI_LOHS; | |
| case TrafficSource::kArcVpn: | |
| return TrafficCounter::VPN; | |
| case TrafficSource::kUnknown: | |
| default: | |
| return TrafficCounter::UNKNOWN; | |
| } | |
| } | |
| TrafficSource ProtoToTrafficSource(TrafficCounter::Source source) { | |
| switch (source) { | |
| case TrafficCounter::CHROME: | |
| return TrafficSource::kChrome; | |
| case TrafficCounter::USER: | |
| return TrafficSource::kUser; | |
| case TrafficCounter::UPDATE_ENGINE: | |
| return TrafficSource::kUpdateEngine; | |
| case TrafficCounter::SYSTEM: | |
| return TrafficSource::kSystem; | |
| case TrafficCounter::VPN: | |
| return TrafficSource::kHostVpn; | |
| case TrafficCounter::ARC: | |
| return TrafficSource::kArc; | |
| case TrafficCounter::BOREALIS_VM: | |
| return TrafficSource::kBorealisVM; | |
| case TrafficCounter::BRUSCHETTA_VM: | |
| return TrafficSource::kBruschettaVM; | |
| case TrafficCounter::CROSTINI_VM: | |
| return TrafficSource::kCrostiniVM; | |
| case TrafficCounter::PARALLELS_VM: | |
| return TrafficSource::kParallelsVM; | |
| case TrafficCounter::TETHERING: | |
| return TrafficSource::kTetherDownstream; | |
| case TrafficCounter::WIFI_DIRECT: | |
| return TrafficSource::kWiFiDirect; | |
| case TrafficCounter::WIFI_LOHS: | |
| return TrafficSource::kWiFiLOHS; | |
| default: | |
| case TrafficCounter::UNKNOWN: | |
| return TrafficSource::kUnknown; | |
| } | |
| } | |
| std::strong_ordering operator<=>(const CountersService::CounterKey&, | |
| const CountersService::CounterKey&) = default; | |
| bool operator==(const CountersService::Counter&, | |
| const CountersService::Counter&) = default; | |
| } // namespace patchpanel |