blob: 0b103b2bb5f979f27583787c9fcce46f633725db [file] [log] [blame]
Jie Jiang31a0b4e2020-07-09 15:06:16 +09001// Copyright 2020 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "patchpanel/counters_service.h"
6
7#include <set>
8#include <string>
Jie Jianged0b1cc2020-07-10 15:55:33 +09009#include <utility>
Jie Jiang31a0b4e2020-07-09 15:06:16 +090010#include <vector>
11
Jie Jianged0b1cc2020-07-10 15:55:33 +090012#include <base/strings/strcat.h>
13#include <base/strings/string_split.h>
14#include <re2/re2.h>
15
Jie Jiang31a0b4e2020-07-09 15:06:16 +090016namespace patchpanel {
17
18namespace {
19
Jie Jianged0b1cc2020-07-10 15:55:33 +090020using Counter = CountersService::Counter;
21using SourceDevice = CountersService::SourceDevice;
22
Jie Jiang31a0b4e2020-07-09 15:06:16 +090023constexpr char kMangleTable[] = "mangle";
24
Jie Jianged0b1cc2020-07-10 15:55:33 +090025// The following regexs and code is written and tested for iptables v1.6.2.
26// Output code of iptables can be found at:
27// https://git.netfilter.org/iptables/tree/iptables/iptables.c?h=v1.6.2
28
29// The chain line looks like:
Hugo Benichi33af8f72020-11-20 10:08:47 +090030// "Chain tx_eth0 (2 references)".
31// This regex extracts "tx" (direction), "eth0" (ifname) from this example.
32constexpr LazyRE2 kChainLine = {R"(Chain (rx|tx)_(\w+).*)"};
Jie Jianged0b1cc2020-07-10 15:55:33 +090033
34// The counter line looks like (some spaces are deleted to make it fit in one
35// line):
36// " 6511 68041668 all -- any any anywhere anywhere"
37// The first two counters are captured for pkts and bytes.
38constexpr LazyRE2 kCounterLine = {R"( *(\d+) +(\d+).*)"};
39
40// Parses the output of `iptables -L -x -v` (or `ip6tables`) and adds the parsed
41// values into the corresponding counters in |counters|. An example of |output|
42// can be found in the test file. This function will try to find the pattern of:
43// <one chain line for an accounting chain>
44// <one header line>
45// <one counter line for an accounting rule>
46// The interface name and direction (rx or tx) will be extracted from the chain
47// line, and then the values extracted from the counter line will be added into
48// the counter for that interface. Note that this function will not fully
49// validate if |output| is an output from iptables.
50bool ParseOutput(const std::string& output,
51 const std::set<std::string>& devices,
52 std::map<SourceDevice, Counter>* counters) {
53 DCHECK(counters);
54 const std::vector<std::string> lines = base::SplitString(
55 output, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
56
57 // Finds the chain line for an accounting chain first, and then parse the
58 // following line(s) to get the counters for this chain. Repeats this process
59 // until we reach the end of |output|.
60 for (auto it = lines.cbegin(); it != lines.cend(); it++) {
61 // Finds the chain name line.
62 std::string direction, ifname;
63 while (it != lines.cend() &&
64 !RE2::FullMatch(*it, *kChainLine, &direction, &ifname))
65 it++;
66
67 if (it == lines.cend())
68 break;
69
70 // Skips this group if this ifname is not requested.
71 if (!devices.empty() && devices.find(ifname) == devices.end())
72 continue;
73
74 // Skips the chain name line and the header line.
75 if (lines.cend() - it <= 2) {
76 LOG(ERROR) << "Invalid iptables output";
77 return false;
78 }
79 it += 2;
80
81 // The current line should be the accounting rule containing the counters.
82 // Currently we only have one accounting rule (UNKNOWN source) for each
83 // chain.
84 // TODO(jiejiang): The following part will be extended to a loop when we
85 // have more accounting rules.
86 uint64_t pkts, bytes;
87 if (!RE2::FullMatch(*it, *kCounterLine, &pkts, &bytes)) {
88 LOG(ERROR) << "Cannot parse \"" << *it << "\"";
89 return false;
90 }
91
92 TrafficCounter::Source source = TrafficCounter::UNKNOWN;
93 auto& counter = (*counters)[std::make_pair(source, ifname)];
94 if (direction == "rx") {
95 counter.rx_packets += pkts;
96 counter.rx_bytes += bytes;
97 } else {
98 counter.tx_packets += pkts;
99 counter.tx_bytes += bytes;
100 }
101 }
102 return true;
103}
104
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900105} // namespace
106
Jie Jianged0b1cc2020-07-10 15:55:33 +0900107Counter::Counter(uint64_t rx_bytes,
108 uint64_t rx_packets,
109 uint64_t tx_bytes,
110 uint64_t tx_packets)
111 : rx_bytes(rx_bytes),
112 rx_packets(rx_packets),
113 tx_bytes(tx_bytes),
114 tx_packets(tx_packets) {}
115
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900116CountersService::CountersService(ShillClient* shill_client,
Hugo Benichicd27f4e2020-11-19 18:32:23 +0900117 Datapath* datapath,
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900118 MinijailedProcessRunner* runner)
Hugo Benichicd27f4e2020-11-19 18:32:23 +0900119 : shill_client_(shill_client), datapath_(datapath), runner_(runner) {
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900120 // Triggers the callback manually to make sure no device is missed.
121 OnDeviceChanged(shill_client_->get_devices(), {});
122 shill_client_->RegisterDevicesChangedHandler(base::BindRepeating(
123 &CountersService::OnDeviceChanged, weak_factory_.GetWeakPtr()));
124}
125
Jie Jianged0b1cc2020-07-10 15:55:33 +0900126std::map<SourceDevice, Counter> CountersService::GetCounters(
127 const std::set<std::string>& devices) {
128 std::map<SourceDevice, Counter> counters;
129
130 // Handles counters for IPv4 and IPv6 separately and returns failure if either
131 // of the procession fails, since counters for only IPv4 or IPv6 are biased.
132 std::string iptables_result;
133 int ret = runner_->iptables(kMangleTable, {"-L", "-x", "-v", "-w"},
134 true /*log_failures*/, &iptables_result);
135 if (ret != 0 || iptables_result.empty()) {
136 LOG(ERROR) << "Failed to query IPv4 counters";
137 return {};
138 }
139 if (!ParseOutput(iptables_result, devices, &counters)) {
140 LOG(ERROR) << "Failed to parse IPv4 counters";
141 return {};
142 }
143
144 std::string ip6tables_result;
145 ret = runner_->ip6tables(kMangleTable, {"-L", "-x", "-v", "-w"},
146 true /*log_failures*/, &ip6tables_result);
147 if (ret != 0 || ip6tables_result.empty()) {
148 LOG(ERROR) << "Failed to query IPv6 counters";
149 return {};
150 }
151 if (!ParseOutput(ip6tables_result, devices, &counters)) {
152 LOG(ERROR) << "Failed to parse IPv6 counters";
153 return {};
154 }
155
156 return counters;
157}
158
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900159void CountersService::OnDeviceChanged(const std::set<std::string>& added,
Jie Jiangcf749152020-07-09 22:20:58 +0900160 const std::set<std::string>& removed) {
161 for (const auto& ifname : added)
162 SetupChainsAndRules(ifname);
163}
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900164
Hugo Benichicd27f4e2020-11-19 18:32:23 +0900165bool CountersService::MakeAccountingChain(const std::string& chain_name) {
166 return datapath_->ModifyChain(IpFamily::Dual, kMangleTable, "-N", chain_name,
167 false /*log_failures*/);
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900168}
169
Hugo Benichiaf2021b2020-11-20 14:07:16 +0900170bool CountersService::AddAccountingRule(const std::string& chain_name,
171 TrafficSource source) {
172 std::vector<std::string> args = {"-A",
173 chain_name,
174 "-m",
175 "mark",
176 "--mark",
177 Fwmark::FromSource(source).ToString() + "/" +
178 kFwmarkAllSourcesMask.ToString(),
179 "-j",
180 "RETURN",
181 "-w"};
182 return datapath_->ModifyIptables(IpFamily::Dual, kMangleTable, args);
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900183}
184
Jie Jiangcf749152020-07-09 22:20:58 +0900185void CountersService::SetupChainsAndRules(const std::string& ifname) {
Hugo Benichi33af8f72020-11-20 10:08:47 +0900186 // For each device and traffic direction, we need to create:
187 // 1) an accounting chain to jump to,
188 // 2) jumping rules in mangle POSTROUTING for egress traffic, and in mangle
189 // INPUT and FORWARD for ingress traffic.
190 // 3) source accounting rules in the chain.
Jie Jiangcf749152020-07-09 22:20:58 +0900191 // Note that the length of a chain name must less than 29 chars and IFNAMSIZ
192 // is 16 so we can only use at most 12 chars for the prefix.
193
Hugo Benichi33af8f72020-11-20 10:08:47 +0900194 const std::string ingress_chain = "rx_" + ifname;
Hugo Benichi33af8f72020-11-20 10:08:47 +0900195 const std::string egress_chain = "tx_" + ifname;
Hugo Benichiddf00842020-11-20 10:24:08 +0900196
197 // Creates egress and ingress traffic chains, or stops if they already exist.
198 if (!MakeAccountingChain(ingress_chain) ||
199 !MakeAccountingChain(egress_chain)) {
200 LOG(INFO) << "Traffic accounting chains already exist for " << ifname;
201 return;
202 }
203
204 // Add jump rules
205 datapath_->ModifyIptables(
206 IpFamily::Dual, kMangleTable,
207 {"-A", "FORWARD", "-i", ifname, "-j", ingress_chain, "-w"});
208 datapath_->ModifyIptables(
209 IpFamily::Dual, kMangleTable,
210 {"-A", "INPUT", "-i", ifname, "-j", ingress_chain, "-w"});
211 datapath_->ModifyIptables(
212 IpFamily::Dual, kMangleTable,
213 {"-A", "POSTROUTING", "-o", ifname, "-j", egress_chain, "-w"});
214
Hugo Benichiaf2021b2020-11-20 14:07:16 +0900215 // Add source accounting rules.
216 for (TrafficSource source : kAllSources) {
217 AddAccountingRule(ingress_chain, source);
218 AddAccountingRule(egress_chain, source);
219 }
220 // TODO(b/160112868): add default rules for counting any traffic left as
221 // UNKNOWN.
Jie Jiangcf749152020-07-09 22:20:58 +0900222}
223
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900224} // namespace patchpanel