blob: 8a1bfdafc2fc87afdd6ff76378f5840b0743e716 [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:
30// "Chain tx_fwd_eth0 (1 references)".
31// This regex extracts "tx" (direction), "eth0" (ifname) from this example, and
32// "fwd" (prebuilt chain) is matched but not captured because it is not required
33// for counters.
34constexpr LazyRE2 kChainLine = {
35 R"(Chain (rx|tx)_(?:input|fwd|postrt)_(\w+).*)"};
36
37// The counter line looks like (some spaces are deleted to make it fit in one
38// line):
39// " 6511 68041668 all -- any any anywhere anywhere"
40// The first two counters are captured for pkts and bytes.
41constexpr LazyRE2 kCounterLine = {R"( *(\d+) +(\d+).*)"};
42
43// Parses the output of `iptables -L -x -v` (or `ip6tables`) and adds the parsed
44// values into the corresponding counters in |counters|. An example of |output|
45// can be found in the test file. This function will try to find the pattern of:
46// <one chain line for an accounting chain>
47// <one header line>
48// <one counter line for an accounting rule>
49// The interface name and direction (rx or tx) will be extracted from the chain
50// line, and then the values extracted from the counter line will be added into
51// the counter for that interface. Note that this function will not fully
52// validate if |output| is an output from iptables.
53bool ParseOutput(const std::string& output,
54 const std::set<std::string>& devices,
55 std::map<SourceDevice, Counter>* counters) {
56 DCHECK(counters);
57 const std::vector<std::string> lines = base::SplitString(
58 output, "\n", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
59
60 // Finds the chain line for an accounting chain first, and then parse the
61 // following line(s) to get the counters for this chain. Repeats this process
62 // until we reach the end of |output|.
63 for (auto it = lines.cbegin(); it != lines.cend(); it++) {
64 // Finds the chain name line.
65 std::string direction, ifname;
66 while (it != lines.cend() &&
67 !RE2::FullMatch(*it, *kChainLine, &direction, &ifname))
68 it++;
69
70 if (it == lines.cend())
71 break;
72
73 // Skips this group if this ifname is not requested.
74 if (!devices.empty() && devices.find(ifname) == devices.end())
75 continue;
76
77 // Skips the chain name line and the header line.
78 if (lines.cend() - it <= 2) {
79 LOG(ERROR) << "Invalid iptables output";
80 return false;
81 }
82 it += 2;
83
84 // The current line should be the accounting rule containing the counters.
85 // Currently we only have one accounting rule (UNKNOWN source) for each
86 // chain.
87 // TODO(jiejiang): The following part will be extended to a loop when we
88 // have more accounting rules.
89 uint64_t pkts, bytes;
90 if (!RE2::FullMatch(*it, *kCounterLine, &pkts, &bytes)) {
91 LOG(ERROR) << "Cannot parse \"" << *it << "\"";
92 return false;
93 }
94
95 TrafficCounter::Source source = TrafficCounter::UNKNOWN;
96 auto& counter = (*counters)[std::make_pair(source, ifname)];
97 if (direction == "rx") {
98 counter.rx_packets += pkts;
99 counter.rx_bytes += bytes;
100 } else {
101 counter.tx_packets += pkts;
102 counter.tx_bytes += bytes;
103 }
104 }
105 return true;
106}
107
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900108} // namespace
109
Jie Jianged0b1cc2020-07-10 15:55:33 +0900110Counter::Counter(uint64_t rx_bytes,
111 uint64_t rx_packets,
112 uint64_t tx_bytes,
113 uint64_t tx_packets)
114 : rx_bytes(rx_bytes),
115 rx_packets(rx_packets),
116 tx_bytes(tx_bytes),
117 tx_packets(tx_packets) {}
118
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900119CountersService::CountersService(ShillClient* shill_client,
120 MinijailedProcessRunner* runner)
121 : shill_client_(shill_client), runner_(runner) {
122 // 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
167void CountersService::IptablesNewChain(const std::string& chain_name) {
168 // There is no straightforward way to check if a chain exists or not.
169 runner_->iptables(kMangleTable, {"-N", chain_name, "-w"},
170 false /*log_failures*/);
171 runner_->ip6tables(kMangleTable, {"-N", chain_name, "-w"},
172 false /*log_failures*/);
173}
174
175void CountersService::IptablesNewRule(std::vector<std::string> params) {
176 DCHECK_GT(params.size(), 0);
177 const std::string action = params[0];
178 DCHECK(action == "-I" || action == "-A");
179 params.emplace_back("-w");
180
181 params[0] = "-C";
182 if (runner_->iptables(kMangleTable, params, false /*log_failures*/) != 0) {
183 params[0] = action;
184 runner_->iptables(kMangleTable, params);
185 }
186
187 params[0] = "-C";
188 if (runner_->ip6tables(kMangleTable, params, false /*log_failures*/) != 0) {
189 params[0] = action;
190 runner_->ip6tables(kMangleTable, params);
191 }
192}
193
Jie Jiangcf749152020-07-09 22:20:58 +0900194void CountersService::SetupChainsAndRules(const std::string& ifname) {
195 // For each group, we need to create 1) an accounting chain, 2) a jumping rule
196 // matching |ifname|, and 3) accounting rule(s) in the chain.
197 // Note that the length of a chain name must less than 29 chars and IFNAMSIZ
198 // is 16 so we can only use at most 12 chars for the prefix.
199
200 // Egress traffic in FORWARD chain. Only traffic for interface-type sources
201 // will be counted by these rules.
202 const std::string egress_forward_chain = "tx_fwd_" + ifname;
203 IptablesNewChain(egress_forward_chain);
204 IptablesNewRule({"-A", "FORWARD", "-o", ifname, "-j", egress_forward_chain});
205 SetupAccountingRules(egress_forward_chain);
206
207 // Egress traffic in POSTROUTING chain. Only traffic for host-type sources
208 // will be counted by these rules, by having a "-m owner --socket-exists" in
209 // the jumping rule. Traffic via "FORWARD -> POSTROUTING" does not have a
210 // socket so will only be counted in FORWARD, while traffic from OUTPUT will
211 // always have an associated socket.
212 const std::string egress_postrouting_chain = "tx_postrt_" + ifname;
213 IptablesNewChain(egress_postrouting_chain);
214 IptablesNewRule({"-A", "POSTROUTING", "-o", ifname, "-m", "owner",
215 "--socket-exists", "-j", egress_postrouting_chain});
216 SetupAccountingRules(egress_postrouting_chain);
217
218 // Ingress traffic in FORWARD chain. Only traffic for interface-type sources
219 // will be counted by these rules.
220 const std::string ingress_forward_chain = "rx_fwd_" + ifname;
221 IptablesNewChain(ingress_forward_chain);
222 IptablesNewRule({"-A", "FORWARD", "-i", ifname, "-j", ingress_forward_chain});
223 SetupAccountingRules(ingress_forward_chain);
224
225 // Ingress traffic in INPUT chain. Only traffic for host-type sources will be
226 // counted by these rules.
227 const std::string ingress_input_chain = "rx_input_" + ifname;
228 IptablesNewChain(ingress_input_chain);
229 IptablesNewRule({"-A", "INPUT", "-i", ifname, "-j", ingress_input_chain});
230 SetupAccountingRules(ingress_input_chain);
231}
232
233void CountersService::SetupAccountingRules(const std::string& chain_name) {
234 // TODO(jiejiang): This function will be extended to matching on fwmark for
235 // different sources.
236 IptablesNewRule({"-A", chain_name});
237}
238
Jie Jiang31a0b4e2020-07-09 15:06:16 +0900239} // namespace patchpanel