blob: 2af694f32c9df65b8179e4000a17a8d9ca99ca46 [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 }
Jason Jeremy Iman54d3f452021-06-02 17:29:41 +0900139
140 if (it == lines.cend())
141 break;
Jie Jianged0b1cc2020-07-10 15:55:33 +0900142 }
143 return true;
144}
145
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900146} // namespace
147
Jie Jianged0b1cc2020-07-10 15:55:33 +0900148Counter::Counter(uint64_t rx_bytes,
149 uint64_t rx_packets,
150 uint64_t tx_bytes,
151 uint64_t tx_packets)
152 : rx_bytes(rx_bytes),
153 rx_packets(rx_packets),
154 tx_bytes(tx_bytes),
155 tx_packets(tx_packets) {}
156
Hugo Benichi058a26a2020-11-26 10:10:39 +0900157CountersService::CountersService(Datapath* datapath,
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900158 MinijailedProcessRunner* runner)
Hugo Benichi058a26a2020-11-26 10:10:39 +0900159 : datapath_(datapath), runner_(runner) {}
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900160
Jie Jianged0b1cc2020-07-10 15:55:33 +0900161std::map<SourceDevice, Counter> CountersService::GetCounters(
162 const std::set<std::string>& devices) {
163 std::map<SourceDevice, Counter> counters;
164
165 // Handles counters for IPv4 and IPv6 separately and returns failure if either
166 // of the procession fails, since counters for only IPv4 or IPv6 are biased.
167 std::string iptables_result;
168 int ret = runner_->iptables(kMangleTable, {"-L", "-x", "-v", "-w"},
169 true /*log_failures*/, &iptables_result);
170 if (ret != 0 || iptables_result.empty()) {
171 LOG(ERROR) << "Failed to query IPv4 counters";
172 return {};
173 }
174 if (!ParseOutput(iptables_result, devices, &counters)) {
175 LOG(ERROR) << "Failed to parse IPv4 counters";
176 return {};
177 }
178
179 std::string ip6tables_result;
180 ret = runner_->ip6tables(kMangleTable, {"-L", "-x", "-v", "-w"},
181 true /*log_failures*/, &ip6tables_result);
182 if (ret != 0 || ip6tables_result.empty()) {
183 LOG(ERROR) << "Failed to query IPv6 counters";
184 return {};
185 }
186 if (!ParseOutput(ip6tables_result, devices, &counters)) {
187 LOG(ERROR) << "Failed to parse IPv6 counters";
188 return {};
189 }
190
191 return counters;
192}
193
Hugo Benichi058a26a2020-11-26 10:10:39 +0900194void CountersService::OnPhysicalDeviceAdded(const std::string& ifname) {
Hugo Benichidf5e1022021-03-23 15:48:31 +0900195 SetupAccountingRules(ifname);
196 SetupJumpRules("-A", ifname, ifname);
Hugo Benichi058a26a2020-11-26 10:10:39 +0900197}
198
199void CountersService::OnPhysicalDeviceRemoved(const std::string& ifname) {
200 SetupJumpRules("-D", ifname, ifname);
201}
202
203void CountersService::OnVpnDeviceAdded(const std::string& ifname) {
Hugo Benichidf5e1022021-03-23 15:48:31 +0900204 SetupAccountingRules(kVpnChainTag);
Hugo Benichi058a26a2020-11-26 10:10:39 +0900205 SetupJumpRules("-A", ifname, kVpnChainTag);
206}
207
208void CountersService::OnVpnDeviceRemoved(const std::string& ifname) {
209 SetupJumpRules("-D", ifname, kVpnChainTag);
Jie Jiangcf749152020-07-09 22:20:58 +0900210}
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900211
Hugo Benichicd27f4e2020-11-19 18:32:23 +0900212bool CountersService::MakeAccountingChain(const std::string& chain_name) {
213 return datapath_->ModifyChain(IpFamily::Dual, kMangleTable, "-N", chain_name,
214 false /*log_failures*/);
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900215}
216
Hugo Benichiaf2021b2020-11-20 14:07:16 +0900217bool CountersService::AddAccountingRule(const std::string& chain_name,
218 TrafficSource source) {
219 std::vector<std::string> args = {"-A",
220 chain_name,
221 "-m",
222 "mark",
223 "--mark",
224 Fwmark::FromSource(source).ToString() + "/" +
225 kFwmarkAllSourcesMask.ToString(),
226 "-j",
227 "RETURN",
228 "-w"};
229 return datapath_->ModifyIptables(IpFamily::Dual, kMangleTable, args);
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900230}
231
Hugo Benichidf5e1022021-03-23 15:48:31 +0900232void CountersService::SetupAccountingRules(const std::string& chain_tag) {
Hugo Benichi058a26a2020-11-26 10:10:39 +0900233 // For a new target accounting chain, create
Hugo Benichi33af8f72020-11-20 10:08:47 +0900234 // 1) an accounting chain to jump to,
Hugo Benichi058a26a2020-11-26 10:10:39 +0900235 // 2) source accounting rules in the chain.
Jie Jiangcf749152020-07-09 22:20:58 +0900236 // Note that the length of a chain name must less than 29 chars and IFNAMSIZ
237 // is 16 so we can only use at most 12 chars for the prefix.
Hugo Benichi058a26a2020-11-26 10:10:39 +0900238 const std::string ingress_chain = kRxTag + chain_tag;
239 const std::string egress_chain = kTxTag + chain_tag;
Hugo Benichiddf00842020-11-20 10:24:08 +0900240
241 // Creates egress and ingress traffic chains, or stops if they already exist.
Hugo Benichi058a26a2020-11-26 10:10:39 +0900242 if (!MakeAccountingChain(egress_chain) ||
243 !MakeAccountingChain(ingress_chain)) {
244 LOG(INFO) << "Traffic accounting chains already exist for " << chain_tag;
Hugo Benichidf5e1022021-03-23 15:48:31 +0900245 return;
Hugo Benichiddf00842020-11-20 10:24:08 +0900246 }
247
Hugo Benichiaf2021b2020-11-20 14:07:16 +0900248 // Add source accounting rules.
249 for (TrafficSource source : kAllSources) {
250 AddAccountingRule(ingress_chain, source);
251 AddAccountingRule(egress_chain, source);
252 }
Hugo Benichiae5803d2020-12-08 10:49:48 +0900253 // Add catch-all accounting rule for any remaining and untagged traffic.
254 datapath_->ModifyIptables(IpFamily::Dual, kMangleTable,
255 {"-A", ingress_chain, "-w"});
256 datapath_->ModifyIptables(IpFamily::Dual, kMangleTable,
257 {"-A", egress_chain, "-w"});
Hugo Benichi058a26a2020-11-26 10:10:39 +0900258}
259
260void CountersService::SetupJumpRules(const std::string& op,
261 const std::string& ifname,
262 const std::string& chain_tag) {
263 // For each device create a jumping rule in mangle POSTROUTING for egress
264 // traffic, and two jumping rules in mangle INPUT and FORWARD for ingress
265 // traffic.
266 datapath_->ModifyIptables(
267 IpFamily::Dual, kMangleTable,
Hugo Benichi2f8a1ea2020-12-14 14:19:21 +0900268 {op, "FORWARD", "-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, "INPUT", "-i", ifname, "-j", kRxTag + chain_tag, "-w"});
Hugo Benichi058a26a2020-11-26 10:10:39 +0900272 datapath_->ModifyIptables(
273 IpFamily::Dual, kMangleTable,
Hugo Benichi2f8a1ea2020-12-14 14:19:21 +0900274 {op, "POSTROUTING", "-o", ifname, "-j", kTxTag + chain_tag, "-w"});
Jie Jiangcf749152020-07-09 22:20:58 +0900275}
276
Hugo Benichi92fa2032020-11-20 17:47:32 +0900277TrafficCounter::Source TrafficSourceToProto(TrafficSource source) {
278 switch (source) {
279 case CHROME:
280 return TrafficCounter::CHROME;
281 case USER:
282 return TrafficCounter::USER;
283 case UPDATE_ENGINE:
284 return TrafficCounter::UPDATE_ENGINE;
285 case SYSTEM:
286 return TrafficCounter::SYSTEM;
287 case HOST_VPN:
288 return TrafficCounter::VPN;
289 case ARC:
290 return TrafficCounter::ARC;
291 case CROSVM:
292 return TrafficCounter::CROSVM;
293 case PLUGINVM:
294 return TrafficCounter::PLUGINVM;
295 case TETHER_DOWNSTREAM:
296 return TrafficCounter::SYSTEM;
297 case ARC_VPN:
298 return TrafficCounter::VPN;
299 case UNKNOWN:
300 default:
301 return TrafficCounter::UNKNOWN;
302 }
303}
304
Hugo Benichi93306e52020-12-04 16:08:00 +0900305TrafficSource ProtoToTrafficSource(TrafficCounter::Source source) {
306 switch (source) {
307 case TrafficCounter::CHROME:
308 return CHROME;
309 case TrafficCounter::USER:
310 return USER;
311 case TrafficCounter::UPDATE_ENGINE:
312 return UPDATE_ENGINE;
313 case TrafficCounter::SYSTEM:
314 return SYSTEM;
315 case TrafficCounter::VPN:
316 return HOST_VPN;
317 case TrafficCounter::ARC:
318 return ARC;
319 case TrafficCounter::CROSVM:
320 return CROSVM;
321 case TrafficCounter::PLUGINVM:
322 return PLUGINVM;
323 default:
324 case TrafficCounter::UNKNOWN:
325 return UNKNOWN;
326 }
327}
328
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900329} // namespace patchpanel