• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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