blob: d07a7ed8ecdf114b2a12f0f666dede895111a01e [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "net/dns/dns_udp_tracker.h"
#include <algorithm>
#include <utility>
#include "base/metrics/histogram_macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/time/tick_clock.h"
#include "net/base/net_errors.h"
namespace net {
namespace {
// Used in UMA (DNS.UdpLowEntropyReason). Do not renumber or remove values.
enum class LowEntropyReason {
kPortReuse = 0,
kRecognizedIdMismatch = 1,
kUnrecognizedIdMismatch = 2,
kSocketLimitExhaustion = 3,
kMaxValue = kSocketLimitExhaustion,
};
void RecordLowEntropyUma(LowEntropyReason reason) {
UMA_HISTOGRAM_ENUMERATION("Net.DNS.DnsTransaction.UDP.LowEntropyReason",
reason);
}
} // namespace
// static
constexpr base::TimeDelta DnsUdpTracker::kMaxAge;
// static
constexpr size_t DnsUdpTracker::kMaxRecordedQueries;
// static
constexpr base::TimeDelta DnsUdpTracker::kMaxRecognizedIdAge;
// static
constexpr size_t DnsUdpTracker::kUnrecognizedIdMismatchThreshold;
// static
constexpr size_t DnsUdpTracker::kRecognizedIdMismatchThreshold;
// static
constexpr int DnsUdpTracker::kPortReuseThreshold;
struct DnsUdpTracker::QueryData {
uint16_t port;
uint16_t query_id;
base::TimeTicks time;
};
DnsUdpTracker::DnsUdpTracker() = default;
DnsUdpTracker::~DnsUdpTracker() = default;
DnsUdpTracker::DnsUdpTracker(DnsUdpTracker&&) = default;
DnsUdpTracker& DnsUdpTracker::operator=(DnsUdpTracker&&) = default;
void DnsUdpTracker::RecordQuery(uint16_t port, uint16_t query_id) {
PurgeOldRecords();
int reused_port_count = base::checked_cast<int>(std::count_if(
recent_queries_.cbegin(), recent_queries_.cend(),
[port](const auto& recent_query) { return port == recent_query.port; }));
UMA_HISTOGRAM_CUSTOM_COUNTS("Net.DNS.DnsTransaction.UDP.ReusedPort.Count",
reused_port_count, 1, kMaxRecordedQueries, 50);
base::TimeTicks now = tick_clock_->NowTicks();
if (reused_port_count > 0) {
auto most_recent_match = std::find_if(
recent_queries_.crbegin(), recent_queries_.crend(),
[port](const auto& recent_query) { return port == recent_query.port; });
DCHECK(most_recent_match != recent_queries_.crend());
UMA_HISTOGRAM_LONG_TIMES(
"Net.DNS.DnsTransaction.UDP.ReusedPort.MostRecentAge",
now - most_recent_match->time);
}
if (reused_port_count >= kPortReuseThreshold && !low_entropy_) {
low_entropy_ = true;
RecordLowEntropyUma(LowEntropyReason::kPortReuse);
}
SaveQuery({port, query_id, now});
}
void DnsUdpTracker::RecordResponseId(uint16_t query_id, uint16_t response_id) {
PurgeOldRecords();
// Used in UMA (DNS.UdpIdMismatchStatus). Do not renumber or remove values.
enum class MismatchStatus {
kSuccessfulParse = 0,
kMismatchPreviouslyQueried = 1,
kMismatchUnknown = 2,
kMaxValue = kMismatchUnknown,
};
MismatchStatus status;
if (query_id == response_id) {
status = MismatchStatus::kSuccessfulParse;
} else {
SaveIdMismatch(response_id);
auto oldest_matching_id =
std::find_if(recent_queries_.cbegin(), recent_queries_.cend(),
[&](const auto& recent_query) {
return response_id == recent_query.query_id;
});
if (oldest_matching_id == recent_queries_.cend()) {
status = MismatchStatus::kMismatchUnknown;
} else {
status = MismatchStatus::kMismatchPreviouslyQueried;
UMA_HISTOGRAM_LONG_TIMES(
"Net.DNS.DnsTransaction.UDP.IdMismatch.OldestMatchTime",
tick_clock_->NowTicks() - oldest_matching_id->time);
}
}
UMA_HISTOGRAM_ENUMERATION("Net.DNS.DnsTransaction.UDP.IdMismatch", status);
}
void DnsUdpTracker::RecordConnectionError(int connection_error) {
if (!low_entropy_ && connection_error == ERR_INSUFFICIENT_RESOURCES) {
// On UDP connection, this error signifies that the process is using an
// unreasonably large number of UDP sockets, potentially a deliberate
// attack to reduce DNS port entropy.
low_entropy_ = true;
RecordLowEntropyUma(LowEntropyReason::kSocketLimitExhaustion);
}
}
void DnsUdpTracker::PurgeOldRecords() {
base::TimeTicks now = tick_clock_->NowTicks();
while (!recent_queries_.empty() &&
(now - recent_queries_.front().time) > kMaxAge) {
recent_queries_.pop_front();
}
while (!recent_unrecognized_id_hits_.empty() &&
now - recent_unrecognized_id_hits_.front() > kMaxAge) {
recent_unrecognized_id_hits_.pop_front();
}
while (!recent_recognized_id_hits_.empty() &&
now - recent_recognized_id_hits_.front() > kMaxAge) {
recent_recognized_id_hits_.pop_front();
}
}
void DnsUdpTracker::SaveQuery(QueryData query) {
if (recent_queries_.size() == kMaxRecordedQueries)
recent_queries_.pop_front();
DCHECK_LT(recent_queries_.size(), kMaxRecordedQueries);
DCHECK(recent_queries_.empty() || query.time >= recent_queries_.back().time);
recent_queries_.push_back(std::move(query));
}
void DnsUdpTracker::SaveIdMismatch(uint16_t id) {
// No need to track mismatches if already flagged for low entropy.
if (low_entropy_)
return;
base::TimeTicks now = tick_clock_->NowTicks();
base::TimeTicks time_cutoff = now - kMaxRecognizedIdAge;
bool is_recognized = std::any_of(
recent_queries_.cbegin(), recent_queries_.cend(),
[&](const auto& recent_query) {
return recent_query.query_id == id && recent_query.time >= time_cutoff;
});
if (is_recognized) {
DCHECK_LT(recent_recognized_id_hits_.size(),
kRecognizedIdMismatchThreshold);
if (recent_recognized_id_hits_.size() ==
kRecognizedIdMismatchThreshold - 1) {
low_entropy_ = true;
RecordLowEntropyUma(LowEntropyReason::kRecognizedIdMismatch);
return;
}
DCHECK(recent_recognized_id_hits_.empty() ||
now >= recent_recognized_id_hits_.back());
recent_recognized_id_hits_.push_back(now);
} else {
DCHECK_LT(recent_unrecognized_id_hits_.size(),
kUnrecognizedIdMismatchThreshold);
if (recent_unrecognized_id_hits_.size() ==
kUnrecognizedIdMismatchThreshold - 1) {
low_entropy_ = true;
RecordLowEntropyUma(LowEntropyReason::kUnrecognizedIdMismatch);
return;
}
DCHECK(recent_unrecognized_id_hits_.empty() ||
now >= recent_unrecognized_id_hits_.back());
recent_unrecognized_id_hits_.push_back(now);
}
}
} // namespace net