blob: 66cbd537890b257bcdf6e5a459c29b4859b88a35 [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::Init(const std::set<std::string>& devices) {
192 SetupAccountingRules(kVpnChainTag);
193 for (const auto& device : devices) {
194 OnPhysicalDeviceAdded(device);
195 }
196}
197
198void CountersService::OnPhysicalDeviceAdded(const std::string& ifname) {
199 if (SetupAccountingRules(ifname))
200 SetupJumpRules("-A", ifname, ifname);
201}
202
203void CountersService::OnPhysicalDeviceRemoved(const std::string& ifname) {
204 SetupJumpRules("-D", ifname, ifname);
205}
206
207void CountersService::OnVpnDeviceAdded(const std::string& ifname) {
208 SetupJumpRules("-A", ifname, kVpnChainTag);
209}
210
211void CountersService::OnVpnDeviceRemoved(const std::string& ifname) {
212 SetupJumpRules("-D", ifname, kVpnChainTag);
Jie Jiangcf749152020-07-09 22:20:58 +0900213}
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900214
Hugo Benichicd27f4e2020-11-19 18:32:23 +0900215bool CountersService::MakeAccountingChain(const std::string& chain_name) {
216 return datapath_->ModifyChain(IpFamily::Dual, kMangleTable, "-N", chain_name,
217 false /*log_failures*/);
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900218}
219
Hugo Benichiaf2021b2020-11-20 14:07:16 +0900220bool CountersService::AddAccountingRule(const std::string& chain_name,
221 TrafficSource source) {
222 std::vector<std::string> args = {"-A",
223 chain_name,
224 "-m",
225 "mark",
226 "--mark",
227 Fwmark::FromSource(source).ToString() + "/" +
228 kFwmarkAllSourcesMask.ToString(),
229 "-j",
230 "RETURN",
231 "-w"};
232 return datapath_->ModifyIptables(IpFamily::Dual, kMangleTable, args);
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900233}
234
Hugo Benichi058a26a2020-11-26 10:10:39 +0900235bool CountersService::SetupAccountingRules(const std::string& chain_tag) {
236 // For a new target accounting chain, create
Hugo Benichi33af8f72020-11-20 10:08:47 +0900237 // 1) an accounting chain to jump to,
Hugo Benichi058a26a2020-11-26 10:10:39 +0900238 // 2) source accounting rules in the chain.
Jie Jiangcf749152020-07-09 22:20:58 +0900239 // Note that the length of a chain name must less than 29 chars and IFNAMSIZ
240 // is 16 so we can only use at most 12 chars for the prefix.
Hugo Benichi058a26a2020-11-26 10:10:39 +0900241 const std::string ingress_chain = kRxTag + chain_tag;
242 const std::string egress_chain = kTxTag + chain_tag;
Hugo Benichiddf00842020-11-20 10:24:08 +0900243
244 // Creates egress and ingress traffic chains, or stops if they already exist.
Hugo Benichi058a26a2020-11-26 10:10:39 +0900245 if (!MakeAccountingChain(egress_chain) ||
246 !MakeAccountingChain(ingress_chain)) {
247 LOG(INFO) << "Traffic accounting chains already exist for " << chain_tag;
248 return false;
Hugo Benichiddf00842020-11-20 10:24:08 +0900249 }
250
Hugo Benichiaf2021b2020-11-20 14:07:16 +0900251 // Add source accounting rules.
252 for (TrafficSource source : kAllSources) {
253 AddAccountingRule(ingress_chain, source);
254 AddAccountingRule(egress_chain, source);
255 }
Hugo Benichiae5803d2020-12-08 10:49:48 +0900256 // Add catch-all accounting rule for any remaining and untagged traffic.
257 datapath_->ModifyIptables(IpFamily::Dual, kMangleTable,
258 {"-A", ingress_chain, "-w"});
259 datapath_->ModifyIptables(IpFamily::Dual, kMangleTable,
260 {"-A", egress_chain, "-w"});
Hugo Benichi058a26a2020-11-26 10:10:39 +0900261
262 return true;
263}
264
265void CountersService::SetupJumpRules(const std::string& op,
266 const std::string& ifname,
267 const std::string& chain_tag) {
268 // For each device create a jumping rule in mangle POSTROUTING for egress
269 // traffic, and two jumping rules in mangle INPUT and FORWARD for ingress
270 // traffic.
271 datapath_->ModifyIptables(
272 IpFamily::Dual, kMangleTable,
Hugo Benichi2f8a1ea2020-12-14 14:19:21 +0900273 {op, "FORWARD", "-i", ifname, "-j", kRxTag + chain_tag, "-w"});
Hugo Benichi058a26a2020-11-26 10:10:39 +0900274 datapath_->ModifyIptables(
275 IpFamily::Dual, kMangleTable,
Hugo Benichi2f8a1ea2020-12-14 14:19:21 +0900276 {op, "INPUT", "-i", ifname, "-j", kRxTag + chain_tag, "-w"});
Hugo Benichi058a26a2020-11-26 10:10:39 +0900277 datapath_->ModifyIptables(
278 IpFamily::Dual, kMangleTable,
Hugo Benichi2f8a1ea2020-12-14 14:19:21 +0900279 {op, "POSTROUTING", "-o", ifname, "-j", kTxTag + chain_tag, "-w"});
Jie Jiangcf749152020-07-09 22:20:58 +0900280}
281
Hugo Benichi92fa2032020-11-20 17:47:32 +0900282TrafficCounter::Source TrafficSourceToProto(TrafficSource source) {
283 switch (source) {
284 case CHROME:
285 return TrafficCounter::CHROME;
286 case USER:
287 return TrafficCounter::USER;
288 case UPDATE_ENGINE:
289 return TrafficCounter::UPDATE_ENGINE;
290 case SYSTEM:
291 return TrafficCounter::SYSTEM;
292 case HOST_VPN:
293 return TrafficCounter::VPN;
294 case ARC:
295 return TrafficCounter::ARC;
296 case CROSVM:
297 return TrafficCounter::CROSVM;
298 case PLUGINVM:
299 return TrafficCounter::PLUGINVM;
300 case TETHER_DOWNSTREAM:
301 return TrafficCounter::SYSTEM;
302 case ARC_VPN:
303 return TrafficCounter::VPN;
304 case UNKNOWN:
305 default:
306 return TrafficCounter::UNKNOWN;
307 }
308}
309
Hugo Benichi93306e52020-12-04 16:08:00 +0900310TrafficSource ProtoToTrafficSource(TrafficCounter::Source source) {
311 switch (source) {
312 case TrafficCounter::CHROME:
313 return CHROME;
314 case TrafficCounter::USER:
315 return USER;
316 case TrafficCounter::UPDATE_ENGINE:
317 return UPDATE_ENGINE;
318 case TrafficCounter::SYSTEM:
319 return SYSTEM;
320 case TrafficCounter::VPN:
321 return HOST_VPN;
322 case TrafficCounter::ARC:
323 return ARC;
324 case TrafficCounter::CROSVM:
325 return CROSVM;
326 case TrafficCounter::PLUGINVM:
327 return PLUGINVM;
328 default:
329 case TrafficCounter::UNKNOWN:
330 return UNKNOWN;
331 }
332}
333
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900334} // namespace patchpanel