1 // Copyright 2020 The Chromium Authors
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 <utility>
8
9 #include "base/metrics/histogram_macros.h"
10 #include "base/numerics/safe_conversions.h"
11 #include "base/ranges/algorithm.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>(
67 base::ranges::count(recent_queries_, port, &QueryData::port));
68
69 if (reused_port_count >= kPortReuseThreshold && !low_entropy_) {
70 low_entropy_ = true;
71 RecordLowEntropyUma(LowEntropyReason::kPortReuse);
72 }
73
74 SaveQuery({port, query_id, tick_clock_->NowTicks()});
75 }
76
RecordResponseId(uint16_t query_id,uint16_t response_id)77 void DnsUdpTracker::RecordResponseId(uint16_t query_id, uint16_t response_id) {
78 PurgeOldRecords();
79
80 if (query_id != response_id) {
81 SaveIdMismatch(response_id);
82 }
83 }
84
RecordConnectionError(int connection_error)85 void DnsUdpTracker::RecordConnectionError(int connection_error) {
86 if (!low_entropy_ && connection_error == ERR_INSUFFICIENT_RESOURCES) {
87 // On UDP connection, this error signifies that the process is using an
88 // unreasonably large number of UDP sockets, potentially a deliberate
89 // attack to reduce DNS port entropy.
90 low_entropy_ = true;
91 RecordLowEntropyUma(LowEntropyReason::kSocketLimitExhaustion);
92 }
93 }
94
PurgeOldRecords()95 void DnsUdpTracker::PurgeOldRecords() {
96 base::TimeTicks now = tick_clock_->NowTicks();
97
98 while (!recent_queries_.empty() &&
99 (now - recent_queries_.front().time) > kMaxAge) {
100 recent_queries_.pop_front();
101 }
102 while (!recent_unrecognized_id_hits_.empty() &&
103 now - recent_unrecognized_id_hits_.front() > kMaxAge) {
104 recent_unrecognized_id_hits_.pop_front();
105 }
106 while (!recent_recognized_id_hits_.empty() &&
107 now - recent_recognized_id_hits_.front() > kMaxAge) {
108 recent_recognized_id_hits_.pop_front();
109 }
110 }
111
SaveQuery(QueryData query)112 void DnsUdpTracker::SaveQuery(QueryData query) {
113 if (recent_queries_.size() == kMaxRecordedQueries)
114 recent_queries_.pop_front();
115 DCHECK_LT(recent_queries_.size(), kMaxRecordedQueries);
116
117 DCHECK(recent_queries_.empty() || query.time >= recent_queries_.back().time);
118 recent_queries_.push_back(std::move(query));
119 }
120
SaveIdMismatch(uint16_t id)121 void DnsUdpTracker::SaveIdMismatch(uint16_t id) {
122 // No need to track mismatches if already flagged for low entropy.
123 if (low_entropy_)
124 return;
125
126 base::TimeTicks now = tick_clock_->NowTicks();
127 base::TimeTicks time_cutoff = now - kMaxRecognizedIdAge;
128 bool is_recognized =
129 base::ranges::any_of(recent_queries_, [&](const auto& recent_query) {
130 return recent_query.query_id == id && recent_query.time >= time_cutoff;
131 });
132
133 if (is_recognized) {
134 DCHECK_LT(recent_recognized_id_hits_.size(),
135 kRecognizedIdMismatchThreshold);
136 if (recent_recognized_id_hits_.size() ==
137 kRecognizedIdMismatchThreshold - 1) {
138 low_entropy_ = true;
139 RecordLowEntropyUma(LowEntropyReason::kRecognizedIdMismatch);
140 return;
141 }
142
143 DCHECK(recent_recognized_id_hits_.empty() ||
144 now >= recent_recognized_id_hits_.back());
145 recent_recognized_id_hits_.push_back(now);
146 } else {
147 DCHECK_LT(recent_unrecognized_id_hits_.size(),
148 kUnrecognizedIdMismatchThreshold);
149 if (recent_unrecognized_id_hits_.size() ==
150 kUnrecognizedIdMismatchThreshold - 1) {
151 low_entropy_ = true;
152 RecordLowEntropyUma(LowEntropyReason::kUnrecognizedIdMismatch);
153 return;
154 }
155
156 DCHECK(recent_unrecognized_id_hits_.empty() ||
157 now >= recent_unrecognized_id_hits_.back());
158 recent_unrecognized_id_hits_.push_back(now);
159 }
160 }
161
162 } // namespace net
163