| // Copyright 2020 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 "patchpanel/counters_service.h" |
| |
| #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> |
| |
| namespace patchpanel { |
| |
| namespace { |
| |
| using Counter = CountersService::Counter; |
| using CounterKey = CountersService::CounterKey; |
| |
| constexpr char kMangleTable[] = "mangle"; |
| constexpr char kVpnChainTag[] = "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::UNKNOWN; |
| 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 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) : datapath_(datapath) {} |
| |
| 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::IPv4, kMangleTable); |
| 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::IPv6, kMangleTable); |
| 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::OnPhysicalDeviceAdded(const std::string& ifname) { |
| SetupAccountingRules(ifname); |
| SetupJumpRules("-A", ifname, ifname); |
| } |
| |
| void CountersService::OnPhysicalDeviceRemoved(const std::string& ifname) { |
| SetupJumpRules("-D", ifname, ifname); |
| } |
| |
| void CountersService::OnVpnDeviceAdded(const std::string& ifname) { |
| SetupAccountingRules(kVpnChainTag); |
| SetupJumpRules("-A", ifname, kVpnChainTag); |
| } |
| |
| void CountersService::OnVpnDeviceRemoved(const std::string& ifname) { |
| SetupJumpRules("-D", ifname, kVpnChainTag); |
| } |
| |
| bool CountersService::MakeAccountingChain(const std::string& chain_name) { |
| return datapath_->ModifyChain(IpFamily::Dual, kMangleTable, "-N", chain_name, |
| false /*log_failures*/); |
| } |
| |
| bool CountersService::AddAccountingRule(const std::string& chain_name, |
| TrafficSource source) { |
| std::vector<std::string> args = {"-A", |
| chain_name, |
| "-m", |
| "mark", |
| "--mark", |
| Fwmark::FromSource(source).ToString() + "/" + |
| kFwmarkAllSourcesMask.ToString(), |
| "-j", |
| "RETURN", |
| "-w"}; |
| return datapath_->ModifyIptables(IpFamily::Dual, kMangleTable, args); |
| } |
| |
| void CountersService::SetupAccountingRules(const std::string& chain_tag) { |
| // For a new target accounting chain, create |
| // 1) an accounting chain to jump to, |
| // 2) source accounting rules in the chain. |
| // Note that the length of a chain name must less than 29 chars and IFNAMSIZ |
| // is 16 so we can only use at most 12 chars for the prefix. |
| const std::string ingress_chain = kRxTag + chain_tag; |
| const std::string egress_chain = kTxTag + chain_tag; |
| |
| // Creates egress and ingress traffic chains, or stops if they already exist. |
| if (!MakeAccountingChain(egress_chain) || |
| !MakeAccountingChain(ingress_chain)) { |
| LOG(INFO) << "Traffic accounting chains already exist for " << chain_tag; |
| return; |
| } |
| |
| // Add source accounting rules. |
| for (TrafficSource source : kAllSources) { |
| AddAccountingRule(ingress_chain, source); |
| AddAccountingRule(egress_chain, source); |
| } |
| // Add catch-all accounting rule for any remaining and untagged traffic. |
| datapath_->ModifyIptables(IpFamily::Dual, kMangleTable, |
| {"-A", ingress_chain, "-w"}); |
| datapath_->ModifyIptables(IpFamily::Dual, kMangleTable, |
| {"-A", egress_chain, "-w"}); |
| } |
| |
| void CountersService::SetupJumpRules(const std::string& op, |
| const std::string& ifname, |
| const std::string& chain_tag) { |
| // 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::Dual, kMangleTable, |
| {op, "FORWARD", "-i", ifname, "-j", kRxTag + chain_tag, "-w"}); |
| datapath_->ModifyIptables( |
| IpFamily::Dual, kMangleTable, |
| {op, "INPUT", "-i", ifname, "-j", kRxTag + chain_tag, "-w"}); |
| datapath_->ModifyIptables( |
| IpFamily::Dual, kMangleTable, |
| {op, "POSTROUTING", "-o", ifname, "-j", kTxTag + chain_tag, "-w"}); |
| } |
| |
| TrafficCounter::Source TrafficSourceToProto(TrafficSource source) { |
| switch (source) { |
| case CHROME: |
| return TrafficCounter::CHROME; |
| case USER: |
| return TrafficCounter::USER; |
| case UPDATE_ENGINE: |
| return TrafficCounter::UPDATE_ENGINE; |
| case SYSTEM: |
| return TrafficCounter::SYSTEM; |
| case HOST_VPN: |
| return TrafficCounter::VPN; |
| case ARC: |
| return TrafficCounter::ARC; |
| case CROSVM: |
| return TrafficCounter::CROSVM; |
| case PLUGINVM: |
| return TrafficCounter::PLUGINVM; |
| case TETHER_DOWNSTREAM: |
| return TrafficCounter::SYSTEM; |
| case ARC_VPN: |
| return TrafficCounter::VPN; |
| case UNKNOWN: |
| default: |
| return TrafficCounter::UNKNOWN; |
| } |
| } |
| |
| TrafficSource ProtoToTrafficSource(TrafficCounter::Source source) { |
| switch (source) { |
| case TrafficCounter::CHROME: |
| return CHROME; |
| case TrafficCounter::USER: |
| return USER; |
| case TrafficCounter::UPDATE_ENGINE: |
| return UPDATE_ENGINE; |
| case TrafficCounter::SYSTEM: |
| return SYSTEM; |
| case TrafficCounter::VPN: |
| return HOST_VPN; |
| case TrafficCounter::ARC: |
| return ARC; |
| case TrafficCounter::CROSVM: |
| return CROSVM; |
| case TrafficCounter::PLUGINVM: |
| return PLUGINVM; |
| default: |
| case TrafficCounter::UNKNOWN: |
| return UNKNOWN; |
| } |
| } |
| |
| bool CountersService::CounterKey::operator<(const CounterKey& rhs) const { |
| if (ifname < rhs.ifname) { |
| return true; |
| } |
| if (ifname > rhs.ifname) { |
| return false; |
| } |
| if (source < rhs.source) { |
| return true; |
| } |
| if (source > rhs.source) { |
| return false; |
| } |
| return ip_family < rhs.ip_family; |
| } |
| |
| } // namespace patchpanel |