blob: 5f3565a4aeee284f4633c8d390a008ecd5030a18 [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
Qijiang Fan713061e2021-03-08 15:45:12 +090012#include <base/check.h>
Jie Jianged0b1cc2020-07-10 15:55:33 +090013#include <base/strings/strcat.h>
14#include <base/strings/string_split.h>
15#include <re2/re2.h>
16
Jie Jiang31a0b4e2020-07-09 15:06:16 +090017namespace patchpanel {
18
19namespace {
20
Jie Jianged0b1cc2020-07-10 15:55:33 +090021using Counter = CountersService::Counter;
22using SourceDevice = CountersService::SourceDevice;
23
Jie Jiang31a0b4e2020-07-09 15:06:16 +090024constexpr char kMangleTable[] = "mangle";
Hugo Benichi058a26a2020-11-26 10:10:39 +090025constexpr char kVpnChainTag[] = "vpn";
26constexpr char kRxTag[] = "rx_";
27constexpr char kTxTag[] = "tx_";
Jie Jiang31a0b4e2020-07-09 15:06:16 +090028
Jie Jianged0b1cc2020-07-10 15:55:33 +090029// The following regexs and code is written and tested for iptables v1.6.2.
30// Output code of iptables can be found at:
31// https://git.netfilter.org/iptables/tree/iptables/iptables.c?h=v1.6.2
32
33// The chain line looks like:
Hugo Benichi33af8f72020-11-20 10:08:47 +090034// "Chain tx_eth0 (2 references)".
35// This regex extracts "tx" (direction), "eth0" (ifname) from this example.
36constexpr LazyRE2 kChainLine = {R"(Chain (rx|tx)_(\w+).*)"};
Jie Jianged0b1cc2020-07-10 15:55:33 +090037
Hugo Benichiae5803d2020-12-08 10:49:48 +090038// The counter line for a defined source looks like (some spaces are deleted to
39// make it fit in one line):
40// " 5374 6172 RETURN all -- any any anywhere anywhere mark match 0x2000/0x3f00"
41// The final counter line for catching untagged traffic looks like:
42// " 5374 6172 all -- any any anywhere anywhere"
43// The first two counters are captured for pkts and bytes. For lines with a mark
44// matcher, the source is also captured.
Hugo Benichi92fa2032020-11-20 17:47:32 +090045constexpr LazyRE2 kCounterLine = {R"( *(\d+) +(\d+).*mark match (.*)/0x3f00)"};
Hugo Benichiae5803d2020-12-08 10:49:48 +090046constexpr LazyRE2 kFinalCounterLine = {R"( *(\d+) +(\d+).*anywhere\s*)"};
47
48bool MatchCounterLine(const std::string& line,
49 uint64_t* pkts,
50 uint64_t* bytes,
51 TrafficSource* source) {
52 Fwmark mark;
53 if (RE2::FullMatch(line, *kCounterLine, pkts, bytes,
54 RE2::Hex(&mark.fwmark))) {
55 *source = mark.Source();
56 return true;
57 }
58
59 if (RE2::FullMatch(line, *kFinalCounterLine, pkts, bytes)) {
60 *source = TrafficSource::UNKNOWN;
61 return true;
62 }
63
64 return false;
65}
Jie Jianged0b1cc2020-07-10 15:55:33 +090066
67// Parses the output of `iptables -L -x -v` (or `ip6tables`) and adds the parsed
68// values into the corresponding counters in |counters|. An example of |output|
69// can be found in the test file. This function will try to find the pattern of:
70// <one chain line for an accounting chain>
71// <one header line>
72// <one counter line for an accounting rule>
73// The interface name and direction (rx or tx) will be extracted from the chain
74// line, and then the values extracted from the counter line will be added into
75// the counter for that interface. Note that this function will not fully
76// validate if |output| is an output from iptables.
77bool ParseOutput(const std::string& output,
78 const std::set<std::string>& devices,
79 std::map<SourceDevice, Counter>* counters) {
80 DCHECK(counters);
81 const std::vector<std::string> lines = base::SplitString(
82 output, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
83
84 // Finds the chain line for an accounting chain first, and then parse the
85 // following line(s) to get the counters for this chain. Repeats this process
86 // until we reach the end of |output|.
87 for (auto it = lines.cbegin(); it != lines.cend(); it++) {
88 // Finds the chain name line.
89 std::string direction, ifname;
90 while (it != lines.cend() &&
91 !RE2::FullMatch(*it, *kChainLine, &direction, &ifname))
92 it++;
93
94 if (it == lines.cend())
95 break;
96
97 // Skips this group if this ifname is not requested.
98 if (!devices.empty() && devices.find(ifname) == devices.end())
99 continue;
100
101 // Skips the chain name line and the header line.
102 if (lines.cend() - it <= 2) {
Hugo Benichiae5803d2020-12-08 10:49:48 +0900103 LOG(ERROR) << "Invalid iptables output for " << direction << "_"
104 << ifname;
Jie Jianged0b1cc2020-07-10 15:55:33 +0900105 return false;
106 }
107 it += 2;
108
Hugo Benichiae5803d2020-12-08 10:49:48 +0900109 // Checks that there are some counter rules defined.
110 if (it == lines.cend() || it->empty()) {
111 LOG(ERROR) << "No counter rule defined for " << direction << "_"
112 << ifname;
113 return false;
114 }
115
Hugo Benichi92fa2032020-11-20 17:47:32 +0900116 // The next block of lines are the counters lines for individual sources.
117 for (; it != lines.cend() && !it->empty(); it++) {
118 uint64_t pkts, bytes;
Hugo Benichiae5803d2020-12-08 10:49:48 +0900119 TrafficSource source;
120 if (!MatchCounterLine(*it, &pkts, &bytes, &source)) {
121 LOG(ERROR) << "Cannot parse counter line \"" << *it << "\" for "
122 << direction << "_" << ifname;
Hugo Benichi92fa2032020-11-20 17:47:32 +0900123 return false;
124 }
Jie Jianged0b1cc2020-07-10 15:55:33 +0900125
Hugo Benichi92fa2032020-11-20 17:47:32 +0900126 if (pkts == 0 && bytes == 0)
127 continue;
128
Hugo Benichiae5803d2020-12-08 10:49:48 +0900129 auto& counter =
130 (*counters)[std::make_pair(TrafficSourceToProto(source), ifname)];
Hugo Benichi92fa2032020-11-20 17:47:32 +0900131 if (direction == "rx") {
Hugo Benichi92fa2032020-11-20 17:47:32 +0900132 counter.rx_bytes += bytes;
Hugo Benichiae5803d2020-12-08 10:49:48 +0900133 counter.rx_packets += pkts;
Hugo Benichi92fa2032020-11-20 17:47:32 +0900134 } else {
Hugo Benichi92fa2032020-11-20 17:47:32 +0900135 counter.tx_bytes += bytes;
Hugo Benichiae5803d2020-12-08 10:49:48 +0900136 counter.tx_packets += pkts;
Hugo Benichi92fa2032020-11-20 17:47:32 +0900137 }
Jie Jianged0b1cc2020-07-10 15:55:33 +0900138 }
139 }
140 return true;
141}
142
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900143} // namespace
144
Jie Jianged0b1cc2020-07-10 15:55:33 +0900145Counter::Counter(uint64_t rx_bytes,
146 uint64_t rx_packets,
147 uint64_t tx_bytes,
148 uint64_t tx_packets)
149 : rx_bytes(rx_bytes),
150 rx_packets(rx_packets),
151 tx_bytes(tx_bytes),
152 tx_packets(tx_packets) {}
153
Hugo Benichi058a26a2020-11-26 10:10:39 +0900154CountersService::CountersService(Datapath* datapath,
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900155 MinijailedProcessRunner* runner)
Hugo Benichi058a26a2020-11-26 10:10:39 +0900156 : datapath_(datapath), runner_(runner) {}
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900157
Jie Jianged0b1cc2020-07-10 15:55:33 +0900158std::map<SourceDevice, Counter> CountersService::GetCounters(
159 const std::set<std::string>& devices) {
160 std::map<SourceDevice, Counter> counters;
161
162 // Handles counters for IPv4 and IPv6 separately and returns failure if either
163 // of the procession fails, since counters for only IPv4 or IPv6 are biased.
164 std::string iptables_result;
165 int ret = runner_->iptables(kMangleTable, {"-L", "-x", "-v", "-w"},
166 true /*log_failures*/, &iptables_result);
167 if (ret != 0 || iptables_result.empty()) {
168 LOG(ERROR) << "Failed to query IPv4 counters";
169 return {};
170 }
171 if (!ParseOutput(iptables_result, devices, &counters)) {
172 LOG(ERROR) << "Failed to parse IPv4 counters";
173 return {};
174 }
175
176 std::string ip6tables_result;
177 ret = runner_->ip6tables(kMangleTable, {"-L", "-x", "-v", "-w"},
178 true /*log_failures*/, &ip6tables_result);
179 if (ret != 0 || ip6tables_result.empty()) {
180 LOG(ERROR) << "Failed to query IPv6 counters";
181 return {};
182 }
183 if (!ParseOutput(ip6tables_result, devices, &counters)) {
184 LOG(ERROR) << "Failed to parse IPv6 counters";
185 return {};
186 }
187
188 return counters;
189}
190
Hugo Benichi058a26a2020-11-26 10:10:39 +0900191void CountersService::OnPhysicalDeviceAdded(const std::string& ifname) {
Hugo Benichidf5e1022021-03-23 15:48:31 +0900192 SetupAccountingRules(ifname);
193 SetupJumpRules("-A", ifname, ifname);
Hugo Benichi058a26a2020-11-26 10:10:39 +0900194}
195
196void CountersService::OnPhysicalDeviceRemoved(const std::string& ifname) {
197 SetupJumpRules("-D", ifname, ifname);
198}
199
200void CountersService::OnVpnDeviceAdded(const std::string& ifname) {
Hugo Benichidf5e1022021-03-23 15:48:31 +0900201 SetupAccountingRules(kVpnChainTag);
Hugo Benichi058a26a2020-11-26 10:10:39 +0900202 SetupJumpRules("-A", ifname, kVpnChainTag);
203}
204
205void CountersService::OnVpnDeviceRemoved(const std::string& ifname) {
206 SetupJumpRules("-D", ifname, kVpnChainTag);
Jie Jiangcf749152020-07-09 22:20:58 +0900207}
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900208
Hugo Benichicd27f4e2020-11-19 18:32:23 +0900209bool CountersService::MakeAccountingChain(const std::string& chain_name) {
210 return datapath_->ModifyChain(IpFamily::Dual, kMangleTable, "-N", chain_name,
211 false /*log_failures*/);
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900212}
213
Hugo Benichiaf2021b2020-11-20 14:07:16 +0900214bool CountersService::AddAccountingRule(const std::string& chain_name,
215 TrafficSource source) {
216 std::vector<std::string> args = {"-A",
217 chain_name,
218 "-m",
219 "mark",
220 "--mark",
221 Fwmark::FromSource(source).ToString() + "/" +
222 kFwmarkAllSourcesMask.ToString(),
223 "-j",
224 "RETURN",
225 "-w"};
226 return datapath_->ModifyIptables(IpFamily::Dual, kMangleTable, args);
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900227}
228
Hugo Benichidf5e1022021-03-23 15:48:31 +0900229void CountersService::SetupAccountingRules(const std::string& chain_tag) {
Hugo Benichi058a26a2020-11-26 10:10:39 +0900230 // For a new target accounting chain, create
Hugo Benichi33af8f72020-11-20 10:08:47 +0900231 // 1) an accounting chain to jump to,
Hugo Benichi058a26a2020-11-26 10:10:39 +0900232 // 2) source accounting rules in the chain.
Jie Jiangcf749152020-07-09 22:20:58 +0900233 // Note that the length of a chain name must less than 29 chars and IFNAMSIZ
234 // is 16 so we can only use at most 12 chars for the prefix.
Hugo Benichi058a26a2020-11-26 10:10:39 +0900235 const std::string ingress_chain = kRxTag + chain_tag;
236 const std::string egress_chain = kTxTag + chain_tag;
Hugo Benichiddf00842020-11-20 10:24:08 +0900237
238 // Creates egress and ingress traffic chains, or stops if they already exist.
Hugo Benichi058a26a2020-11-26 10:10:39 +0900239 if (!MakeAccountingChain(egress_chain) ||
240 !MakeAccountingChain(ingress_chain)) {
241 LOG(INFO) << "Traffic accounting chains already exist for " << chain_tag;
Hugo Benichidf5e1022021-03-23 15:48:31 +0900242 return;
Hugo Benichiddf00842020-11-20 10:24:08 +0900243 }
244
Hugo Benichiaf2021b2020-11-20 14:07:16 +0900245 // Add source accounting rules.
246 for (TrafficSource source : kAllSources) {
247 AddAccountingRule(ingress_chain, source);
248 AddAccountingRule(egress_chain, source);
249 }
Hugo Benichiae5803d2020-12-08 10:49:48 +0900250 // Add catch-all accounting rule for any remaining and untagged traffic.
251 datapath_->ModifyIptables(IpFamily::Dual, kMangleTable,
252 {"-A", ingress_chain, "-w"});
253 datapath_->ModifyIptables(IpFamily::Dual, kMangleTable,
254 {"-A", egress_chain, "-w"});
Hugo Benichi058a26a2020-11-26 10:10:39 +0900255}
256
257void CountersService::SetupJumpRules(const std::string& op,
258 const std::string& ifname,
259 const std::string& chain_tag) {
260 // For each device create a jumping rule in mangle POSTROUTING for egress
261 // traffic, and two jumping rules in mangle INPUT and FORWARD for ingress
262 // traffic.
263 datapath_->ModifyIptables(
264 IpFamily::Dual, kMangleTable,
Hugo Benichi2f8a1ea2020-12-14 14:19:21 +0900265 {op, "FORWARD", "-i", ifname, "-j", kRxTag + chain_tag, "-w"});
Hugo Benichi058a26a2020-11-26 10:10:39 +0900266 datapath_->ModifyIptables(
267 IpFamily::Dual, kMangleTable,
Hugo Benichi2f8a1ea2020-12-14 14:19:21 +0900268 {op, "INPUT", "-i", ifname, "-j", kRxTag + chain_tag, "-w"});
Hugo Benichi058a26a2020-11-26 10:10:39 +0900269 datapath_->ModifyIptables(
270 IpFamily::Dual, kMangleTable,
Hugo Benichi2f8a1ea2020-12-14 14:19:21 +0900271 {op, "POSTROUTING", "-o", ifname, "-j", kTxTag + chain_tag, "-w"});
Jie Jiangcf749152020-07-09 22:20:58 +0900272}
273
Hugo Benichi92fa2032020-11-20 17:47:32 +0900274TrafficCounter::Source TrafficSourceToProto(TrafficSource source) {
275 switch (source) {
276 case CHROME:
277 return TrafficCounter::CHROME;
278 case USER:
279 return TrafficCounter::USER;
280 case UPDATE_ENGINE:
281 return TrafficCounter::UPDATE_ENGINE;
282 case SYSTEM:
283 return TrafficCounter::SYSTEM;
284 case HOST_VPN:
285 return TrafficCounter::VPN;
286 case ARC:
287 return TrafficCounter::ARC;
288 case CROSVM:
289 return TrafficCounter::CROSVM;
290 case PLUGINVM:
291 return TrafficCounter::PLUGINVM;
292 case TETHER_DOWNSTREAM:
293 return TrafficCounter::SYSTEM;
294 case ARC_VPN:
295 return TrafficCounter::VPN;
296 case UNKNOWN:
297 default:
298 return TrafficCounter::UNKNOWN;
299 }
300}
301
Hugo Benichi93306e52020-12-04 16:08:00 +0900302TrafficSource ProtoToTrafficSource(TrafficCounter::Source source) {
303 switch (source) {
304 case TrafficCounter::CHROME:
305 return CHROME;
306 case TrafficCounter::USER:
307 return USER;
308 case TrafficCounter::UPDATE_ENGINE:
309 return UPDATE_ENGINE;
310 case TrafficCounter::SYSTEM:
311 return SYSTEM;
312 case TrafficCounter::VPN:
313 return HOST_VPN;
314 case TrafficCounter::ARC:
315 return ARC;
316 case TrafficCounter::CROSVM:
317 return CROSVM;
318 case TrafficCounter::PLUGINVM:
319 return PLUGINVM;
320 default:
321 case TrafficCounter::UNKNOWN:
322 return UNKNOWN;
323 }
324}
325
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900326} // namespace patchpanel