blob: 7c631bb38e22dbfe51c25017998728b2898ac82d [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
Hugo Benichicd27f4e2020-11-19 18:32:23 +090016#include "patchpanel/routing_service.h"
17
Jie Jiang31a0b4e2020-07-09 15:06:16 +090018namespace patchpanel {
19
20namespace {
21
Jie Jianged0b1cc2020-07-10 15:55:33 +090022using Counter = CountersService::Counter;
23using SourceDevice = CountersService::SourceDevice;
24
Jie Jiang31a0b4e2020-07-09 15:06:16 +090025constexpr char kMangleTable[] = "mangle";
26
Jie Jianged0b1cc2020-07-10 15:55:33 +090027// The following regexs and code is written and tested for iptables v1.6.2.
28// Output code of iptables can be found at:
29// https://git.netfilter.org/iptables/tree/iptables/iptables.c?h=v1.6.2
30
31// The chain line looks like:
Hugo Benichi33af8f72020-11-20 10:08:47 +090032// "Chain tx_eth0 (2 references)".
33// This regex extracts "tx" (direction), "eth0" (ifname) from this example.
34constexpr LazyRE2 kChainLine = {R"(Chain (rx|tx)_(\w+).*)"};
Jie Jianged0b1cc2020-07-10 15:55:33 +090035
36// The counter line looks like (some spaces are deleted to make it fit in one
37// line):
38// " 6511 68041668 all -- any any anywhere anywhere"
39// The first two counters are captured for pkts and bytes.
40constexpr LazyRE2 kCounterLine = {R"( *(\d+) +(\d+).*)"};
41
42// Parses the output of `iptables -L -x -v` (or `ip6tables`) and adds the parsed
43// values into the corresponding counters in |counters|. An example of |output|
44// can be found in the test file. This function will try to find the pattern of:
45// <one chain line for an accounting chain>
46// <one header line>
47// <one counter line for an accounting rule>
48// The interface name and direction (rx or tx) will be extracted from the chain
49// line, and then the values extracted from the counter line will be added into
50// the counter for that interface. Note that this function will not fully
51// validate if |output| is an output from iptables.
52bool ParseOutput(const std::string& output,
53 const std::set<std::string>& devices,
54 std::map<SourceDevice, Counter>* counters) {
55 DCHECK(counters);
56 const std::vector<std::string> lines = base::SplitString(
57 output, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
58
59 // Finds the chain line for an accounting chain first, and then parse the
60 // following line(s) to get the counters for this chain. Repeats this process
61 // until we reach the end of |output|.
62 for (auto it = lines.cbegin(); it != lines.cend(); it++) {
63 // Finds the chain name line.
64 std::string direction, ifname;
65 while (it != lines.cend() &&
66 !RE2::FullMatch(*it, *kChainLine, &direction, &ifname))
67 it++;
68
69 if (it == lines.cend())
70 break;
71
72 // Skips this group if this ifname is not requested.
73 if (!devices.empty() && devices.find(ifname) == devices.end())
74 continue;
75
76 // Skips the chain name line and the header line.
77 if (lines.cend() - it <= 2) {
78 LOG(ERROR) << "Invalid iptables output";
79 return false;
80 }
81 it += 2;
82
83 // The current line should be the accounting rule containing the counters.
84 // Currently we only have one accounting rule (UNKNOWN source) for each
85 // chain.
86 // TODO(jiejiang): The following part will be extended to a loop when we
87 // have more accounting rules.
88 uint64_t pkts, bytes;
89 if (!RE2::FullMatch(*it, *kCounterLine, &pkts, &bytes)) {
90 LOG(ERROR) << "Cannot parse \"" << *it << "\"";
91 return false;
92 }
93
94 TrafficCounter::Source source = TrafficCounter::UNKNOWN;
95 auto& counter = (*counters)[std::make_pair(source, ifname)];
96 if (direction == "rx") {
97 counter.rx_packets += pkts;
98 counter.rx_bytes += bytes;
99 } else {
100 counter.tx_packets += pkts;
101 counter.tx_bytes += bytes;
102 }
103 }
104 return true;
105}
106
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900107} // namespace
108
Jie Jianged0b1cc2020-07-10 15:55:33 +0900109Counter::Counter(uint64_t rx_bytes,
110 uint64_t rx_packets,
111 uint64_t tx_bytes,
112 uint64_t tx_packets)
113 : rx_bytes(rx_bytes),
114 rx_packets(rx_packets),
115 tx_bytes(tx_bytes),
116 tx_packets(tx_packets) {}
117
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900118CountersService::CountersService(ShillClient* shill_client,
Hugo Benichicd27f4e2020-11-19 18:32:23 +0900119 Datapath* datapath,
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900120 MinijailedProcessRunner* runner)
Hugo Benichicd27f4e2020-11-19 18:32:23 +0900121 : shill_client_(shill_client), datapath_(datapath), runner_(runner) {
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900122 // Triggers the callback manually to make sure no device is missed.
123 OnDeviceChanged(shill_client_->get_devices(), {});
124 shill_client_->RegisterDevicesChangedHandler(base::BindRepeating(
125 &CountersService::OnDeviceChanged, weak_factory_.GetWeakPtr()));
126}
127
Jie Jianged0b1cc2020-07-10 15:55:33 +0900128std::map<SourceDevice, Counter> CountersService::GetCounters(
129 const std::set<std::string>& devices) {
130 std::map<SourceDevice, Counter> counters;
131
132 // Handles counters for IPv4 and IPv6 separately and returns failure if either
133 // of the procession fails, since counters for only IPv4 or IPv6 are biased.
134 std::string iptables_result;
135 int ret = runner_->iptables(kMangleTable, {"-L", "-x", "-v", "-w"},
136 true /*log_failures*/, &iptables_result);
137 if (ret != 0 || iptables_result.empty()) {
138 LOG(ERROR) << "Failed to query IPv4 counters";
139 return {};
140 }
141 if (!ParseOutput(iptables_result, devices, &counters)) {
142 LOG(ERROR) << "Failed to parse IPv4 counters";
143 return {};
144 }
145
146 std::string ip6tables_result;
147 ret = runner_->ip6tables(kMangleTable, {"-L", "-x", "-v", "-w"},
148 true /*log_failures*/, &ip6tables_result);
149 if (ret != 0 || ip6tables_result.empty()) {
150 LOG(ERROR) << "Failed to query IPv6 counters";
151 return {};
152 }
153 if (!ParseOutput(ip6tables_result, devices, &counters)) {
154 LOG(ERROR) << "Failed to parse IPv6 counters";
155 return {};
156 }
157
158 return counters;
159}
160
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900161void CountersService::OnDeviceChanged(const std::set<std::string>& added,
Jie Jiangcf749152020-07-09 22:20:58 +0900162 const std::set<std::string>& removed) {
163 for (const auto& ifname : added)
164 SetupChainsAndRules(ifname);
165}
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900166
Hugo Benichicd27f4e2020-11-19 18:32:23 +0900167bool CountersService::MakeAccountingChain(const std::string& chain_name) {
168 return datapath_->ModifyChain(IpFamily::Dual, kMangleTable, "-N", chain_name,
169 false /*log_failures*/);
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900170}
171
172void CountersService::IptablesNewRule(std::vector<std::string> params) {
Hugo Benichiddf00842020-11-20 10:24:08 +0900173 runner_->iptables(kMangleTable, params);
174 runner_->ip6tables(kMangleTable, params);
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900175}
176
Jie Jiangcf749152020-07-09 22:20:58 +0900177void CountersService::SetupChainsAndRules(const std::string& ifname) {
Hugo Benichi33af8f72020-11-20 10:08:47 +0900178 // For each device and traffic direction, we need to create:
179 // 1) an accounting chain to jump to,
180 // 2) jumping rules in mangle POSTROUTING for egress traffic, and in mangle
181 // INPUT and FORWARD for ingress traffic.
182 // 3) source accounting rules in the chain.
Jie Jiangcf749152020-07-09 22:20:58 +0900183 // Note that the length of a chain name must less than 29 chars and IFNAMSIZ
184 // is 16 so we can only use at most 12 chars for the prefix.
185
Hugo Benichi33af8f72020-11-20 10:08:47 +0900186 const std::string ingress_chain = "rx_" + ifname;
Hugo Benichi33af8f72020-11-20 10:08:47 +0900187 const std::string egress_chain = "tx_" + ifname;
Hugo Benichiddf00842020-11-20 10:24:08 +0900188
189 // Creates egress and ingress traffic chains, or stops if they already exist.
190 if (!MakeAccountingChain(ingress_chain) ||
191 !MakeAccountingChain(egress_chain)) {
192 LOG(INFO) << "Traffic accounting chains already exist for " << ifname;
193 return;
194 }
195
196 // Add jump rules
197 datapath_->ModifyIptables(
198 IpFamily::Dual, kMangleTable,
199 {"-A", "FORWARD", "-i", ifname, "-j", ingress_chain, "-w"});
200 datapath_->ModifyIptables(
201 IpFamily::Dual, kMangleTable,
202 {"-A", "INPUT", "-i", ifname, "-j", ingress_chain, "-w"});
203 datapath_->ModifyIptables(
204 IpFamily::Dual, kMangleTable,
205 {"-A", "POSTROUTING", "-o", ifname, "-j", egress_chain, "-w"});
206
207 SetupAccountingRules(ingress_chain);
Hugo Benichi33af8f72020-11-20 10:08:47 +0900208 SetupAccountingRules(egress_chain);
Jie Jiangcf749152020-07-09 22:20:58 +0900209}
210
211void CountersService::SetupAccountingRules(const std::string& chain_name) {
212 // TODO(jiejiang): This function will be extended to matching on fwmark for
213 // different sources.
Hugo Benichiddf00842020-11-20 10:24:08 +0900214 IptablesNewRule({"-A", chain_name, "-w"});
Jie Jiangcf749152020-07-09 22:20:58 +0900215}
216
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900217} // namespace patchpanel