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