1 // Copyright 2020 The Chromium 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 "net/dns/dns_udp_tracker.h"
6
7 #include <algorithm>
8 #include <utility>
9
10 #include "base/metrics/histogram_macros.h"
11 #include "base/numerics/safe_conversions.h"
12 #include "base/time/tick_clock.h"
13 #include "net/base/net_errors.h"
14
15 namespace net {
16
17 namespace {
18 // Used in UMA (DNS.UdpLowEntropyReason). Do not renumber or remove values.
19 enum class LowEntropyReason {
20 kPortReuse = 0,
21 kRecognizedIdMismatch = 1,
22 kUnrecognizedIdMismatch = 2,
23 kSocketLimitExhaustion = 3,
24 kMaxValue = kSocketLimitExhaustion,
25 };
26
RecordLowEntropyUma(LowEntropyReason reason)27 void RecordLowEntropyUma(LowEntropyReason reason) {
28 UMA_HISTOGRAM_ENUMERATION("Net.DNS.DnsTransaction.UDP.LowEntropyReason",
29 reason);
30 }
31
32 } // namespace
33
34 // static
35 constexpr base::TimeDelta DnsUdpTracker::kMaxAge;
36
37 // static
38 constexpr size_t DnsUdpTracker::kMaxRecordedQueries;
39
40 // static
41 constexpr base::TimeDelta DnsUdpTracker::kMaxRecognizedIdAge;
42
43 // static
44 constexpr size_t DnsUdpTracker::kUnrecognizedIdMismatchThreshold;
45
46 // static
47 constexpr size_t DnsUdpTracker::kRecognizedIdMismatchThreshold;
48
49 // static
50 constexpr int DnsUdpTracker::kPortReuseThreshold;
51
52 struct DnsUdpTracker::QueryData {
53 uint16_t port;
54 uint16_t query_id;
55 base::TimeTicks time;
56 };
57
58 DnsUdpTracker::DnsUdpTracker() = default;
59 DnsUdpTracker::~DnsUdpTracker() = default;
60 DnsUdpTracker::DnsUdpTracker(DnsUdpTracker&&) = default;
61 DnsUdpTracker& DnsUdpTracker::operator=(DnsUdpTracker&&) = default;
62
RecordQuery(uint16_t port,uint16_t query_id)63 void DnsUdpTracker::RecordQuery(uint16_t port, uint16_t query_id) {
64 PurgeOldRecords();
65
66 int reused_port_count = base::checked_cast<int>(std::count_if(
67 recent_queries_.cbegin(), recent_queries_.cend(),
68 [port](const auto& recent_query) { return port == recent_query.port; }));
69 UMA_HISTOGRAM_CUSTOM_COUNTS("Net.DNS.DnsTransaction.UDP.ReusedPort.Count",
70 reused_port_count, 1, kMaxRecordedQueries, 50);
71
72 base::TimeTicks now = tick_clock_->NowTicks();
73 if (reused_port_count > 0) {
74 auto most_recent_match = std::find_if(
75 recent_queries_.crbegin(), recent_queries_.crend(),
76 [port](const auto& recent_query) { return port == recent_query.port; });
77 DCHECK(most_recent_match != recent_queries_.crend());
78 UMA_HISTOGRAM_LONG_TIMES(
79 "Net.DNS.DnsTransaction.UDP.ReusedPort.MostRecentAge",
80 now - most_recent_match->time);
81 }
82
83 if (reused_port_count >= kPortReuseThreshold && !low_entropy_) {
84 low_entropy_ = true;
85 RecordLowEntropyUma(LowEntropyReason::kPortReuse);
86 }
87
88 SaveQuery({port, query_id, now});
89 }
90
RecordResponseId(uint16_t query_id,uint16_t response_id)91 void DnsUdpTracker::RecordResponseId(uint16_t query_id, uint16_t response_id) {
92 PurgeOldRecords();
93
94 // Used in UMA (DNS.UdpIdMismatchStatus). Do not renumber or remove values.
95 enum class MismatchStatus {
96 kSuccessfulParse = 0,
97 kMismatchPreviouslyQueried = 1,
98 kMismatchUnknown = 2,
99 kMaxValue = kMismatchUnknown,
100 };
101
102 MismatchStatus status;
103 if (query_id == response_id) {
104 status = MismatchStatus::kSuccessfulParse;
105 } else {
106 SaveIdMismatch(response_id);
107
108 auto oldest_matching_id =
109 std::find_if(recent_queries_.cbegin(), recent_queries_.cend(),
110 [&](const auto& recent_query) {
111 return response_id == recent_query.query_id;
112 });
113
114 if (oldest_matching_id == recent_queries_.cend()) {
115 status = MismatchStatus::kMismatchUnknown;
116 } else {
117 status = MismatchStatus::kMismatchPreviouslyQueried;
118 UMA_HISTOGRAM_LONG_TIMES(
119 "Net.DNS.DnsTransaction.UDP.IdMismatch.OldestMatchTime",
120 tick_clock_->NowTicks() - oldest_matching_id->time);
121 }
122 }
123
124 UMA_HISTOGRAM_ENUMERATION("Net.DNS.DnsTransaction.UDP.IdMismatch", status);
125 }
126
RecordConnectionError(int connection_error)127 void DnsUdpTracker::RecordConnectionError(int connection_error) {
128 if (!low_entropy_ && connection_error == ERR_INSUFFICIENT_RESOURCES) {
129 // On UDP connection, this error signifies that the process is using an
130 // unreasonably large number of UDP sockets, potentially a deliberate
131 // attack to reduce DNS port entropy.
132 low_entropy_ = true;
133 RecordLowEntropyUma(LowEntropyReason::kSocketLimitExhaustion);
134 }
135 }
136
PurgeOldRecords()137 void DnsUdpTracker::PurgeOldRecords() {
138 base::TimeTicks now = tick_clock_->NowTicks();
139
140 while (!recent_queries_.empty() &&
141 (now - recent_queries_.front().time) > kMaxAge) {
142 recent_queries_.pop_front();
143 }
144 while (!recent_unrecognized_id_hits_.empty() &&
145 now - recent_unrecognized_id_hits_.front() > kMaxAge) {
146 recent_unrecognized_id_hits_.pop_front();
147 }
148 while (!recent_recognized_id_hits_.empty() &&
149 now - recent_recognized_id_hits_.front() > kMaxAge) {
150 recent_recognized_id_hits_.pop_front();
151 }
152 }
153
SaveQuery(QueryData query)154 void DnsUdpTracker::SaveQuery(QueryData query) {
155 if (recent_queries_.size() == kMaxRecordedQueries)
156 recent_queries_.pop_front();
157 DCHECK_LT(recent_queries_.size(), kMaxRecordedQueries);
158
159 DCHECK(recent_queries_.empty() || query.time >= recent_queries_.back().time);
160 recent_queries_.push_back(std::move(query));
161 }
162
SaveIdMismatch(uint16_t id)163 void DnsUdpTracker::SaveIdMismatch(uint16_t id) {
164 // No need to track mismatches if already flagged for low entropy.
165 if (low_entropy_)
166 return;
167
168 base::TimeTicks now = tick_clock_->NowTicks();
169 base::TimeTicks time_cutoff = now - kMaxRecognizedIdAge;
170 bool is_recognized = std::any_of(
171 recent_queries_.cbegin(), recent_queries_.cend(),
172 [&](const auto& recent_query) {
173 return recent_query.query_id == id && recent_query.time >= time_cutoff;
174 });
175
176 if (is_recognized) {
177 DCHECK_LT(recent_recognized_id_hits_.size(),
178 kRecognizedIdMismatchThreshold);
179 if (recent_recognized_id_hits_.size() ==
180 kRecognizedIdMismatchThreshold - 1) {
181 low_entropy_ = true;
182 RecordLowEntropyUma(LowEntropyReason::kRecognizedIdMismatch);
183 return;
184 }
185
186 DCHECK(recent_recognized_id_hits_.empty() ||
187 now >= recent_recognized_id_hits_.back());
188 recent_recognized_id_hits_.push_back(now);
189 } else {
190 DCHECK_LT(recent_unrecognized_id_hits_.size(),
191 kUnrecognizedIdMismatchThreshold);
192 if (recent_unrecognized_id_hits_.size() ==
193 kUnrecognizedIdMismatchThreshold - 1) {
194 low_entropy_ = true;
195 RecordLowEntropyUma(LowEntropyReason::kUnrecognizedIdMismatch);
196 return;
197 }
198
199 DCHECK(recent_unrecognized_id_hits_.empty() ||
200 now >= recent_unrecognized_id_hits_.back());
201 recent_unrecognized_id_hits_.push_back(now);
202 }
203 }
204
205 } // namespace net
206