| // 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/httpssvc_metrics.h" |
| |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram.h" |
| #include "base/metrics/histogram_base.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/string_util.h" |
| #include "net/base/features.h" |
| #include "net/dns/dns_util.h" |
| #include "net/dns/public/dns_protocol.h" |
| |
| namespace net { |
| |
| enum HttpssvcDnsRcode TranslateDnsRcodeForHttpssvcExperiment(uint8_t rcode) { |
| switch (rcode) { |
| case dns_protocol::kRcodeNOERROR: |
| return HttpssvcDnsRcode::kNoError; |
| case dns_protocol::kRcodeFORMERR: |
| return HttpssvcDnsRcode::kFormErr; |
| case dns_protocol::kRcodeSERVFAIL: |
| return HttpssvcDnsRcode::kServFail; |
| case dns_protocol::kRcodeNXDOMAIN: |
| return HttpssvcDnsRcode::kNxDomain; |
| case dns_protocol::kRcodeNOTIMP: |
| return HttpssvcDnsRcode::kNotImp; |
| case dns_protocol::kRcodeREFUSED: |
| return HttpssvcDnsRcode::kRefused; |
| default: |
| return HttpssvcDnsRcode::kUnrecognizedRcode; |
| } |
| NOTREACHED(); |
| } |
| |
| HttpssvcExperimentDomainCache::HttpssvcExperimentDomainCache() = default; |
| HttpssvcExperimentDomainCache::~HttpssvcExperimentDomainCache() = default; |
| |
| bool HttpssvcExperimentDomainCache::ListContainsDomain( |
| const std::string& domain_list, |
| base::StringPiece domain, |
| base::Optional<base::flat_set<std::string>>& in_out_cached_list) { |
| if (!in_out_cached_list) { |
| in_out_cached_list = base::SplitString( |
| domain_list, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); |
| } |
| return in_out_cached_list->find(domain) != in_out_cached_list->end(); |
| } |
| |
| bool HttpssvcExperimentDomainCache::IsExperimental(base::StringPiece domain) { |
| if (!base::FeatureList::IsEnabled(features::kDnsHttpssvc)) |
| return false; |
| return ListContainsDomain(features::kDnsHttpssvcExperimentDomains.Get(), |
| domain, experimental_list_); |
| } |
| |
| bool HttpssvcExperimentDomainCache::IsControl(base::StringPiece domain) { |
| if (!base::FeatureList::IsEnabled(features::kDnsHttpssvc)) |
| return false; |
| if (features::kDnsHttpssvcControlDomainWildcard.Get()) |
| return !IsExperimental(domain); |
| return ListContainsDomain(features::kDnsHttpssvcControlDomains.Get(), domain, |
| control_list_); |
| } |
| |
| HttpssvcMetrics::HttpssvcMetrics(bool expect_intact) |
| : expect_intact_(expect_intact) {} |
| |
| HttpssvcMetrics::~HttpssvcMetrics() { |
| RecordMetrics(); |
| } |
| |
| void HttpssvcMetrics::SaveForAddressQuery( |
| base::Optional<std::string> new_doh_provider_id, |
| base::TimeDelta resolve_time, |
| enum HttpssvcDnsRcode rcode) { |
| set_doh_provider_id(new_doh_provider_id); |
| |
| address_resolve_times_.push_back(resolve_time); |
| |
| if (rcode != HttpssvcDnsRcode::kNoError) |
| disqualified_ = true; |
| } |
| |
| void HttpssvcMetrics::SaveAddressQueryFailure() { |
| disqualified_ = true; |
| } |
| |
| void HttpssvcMetrics::SaveForIntegrity( |
| base::Optional<std::string> new_doh_provider_id, |
| enum HttpssvcDnsRcode rcode_integrity, |
| const std::vector<bool>& condensed_records, |
| base::TimeDelta integrity_resolve_time) { |
| DCHECK(!rcode_integrity_.has_value()); |
| set_doh_provider_id(new_doh_provider_id); |
| |
| rcode_integrity_ = rcode_integrity; |
| |
| num_integrity_records_ = condensed_records.size(); |
| |
| // We only record one "Integrity" sample per INTEGRITY query. In case multiple |
| // matching records are present in the response, we combine their intactness |
| // values with logical AND. |
| const bool intact = |
| std::all_of(condensed_records.cbegin(), condensed_records.cend(), |
| [](bool b) { return b; }); |
| |
| DCHECK(!is_integrity_intact_.has_value()); |
| is_integrity_intact_ = intact; |
| |
| DCHECK(!integrity_resolve_time_.has_value()); |
| integrity_resolve_time_ = integrity_resolve_time; |
| } |
| |
| void HttpssvcMetrics::SaveForHttps(base::Optional<std::string> doh_provider_id, |
| enum HttpssvcDnsRcode rcode, |
| const std::vector<bool>& condensed_records, |
| base::TimeDelta https_resolve_time) { |
| DCHECK(!rcode_https_.has_value()); |
| set_doh_provider_id(doh_provider_id); |
| |
| rcode_https_ = rcode; |
| |
| num_https_records_ = condensed_records.size(); |
| |
| // We only record one "parsable" sample per HTTPS query. In case multiple |
| // matching records are present in the response, we combine their parsable |
| // values with logical AND. |
| const bool parsable = |
| std::all_of(condensed_records.cbegin(), condensed_records.cend(), |
| [](bool b) { return b; }); |
| |
| DCHECK(!is_https_parsable_.has_value()); |
| is_https_parsable_ = parsable; |
| |
| DCHECK(!https_resolve_time_.has_value()); |
| https_resolve_time_ = https_resolve_time; |
| } |
| |
| void HttpssvcMetrics::set_doh_provider_id( |
| base::Optional<std::string> new_doh_provider_id) { |
| // "Other" never gets updated. |
| if (doh_provider_id_.has_value() && *doh_provider_id_ == "Other") |
| return; |
| |
| // If provider IDs mismatch, downgrade the new provider ID to "Other". |
| if ((doh_provider_id_.has_value() && !new_doh_provider_id.has_value()) || |
| (doh_provider_id_.has_value() && new_doh_provider_id.has_value() && |
| *doh_provider_id_ != *new_doh_provider_id)) { |
| new_doh_provider_id = "Other"; |
| } |
| |
| doh_provider_id_ = new_doh_provider_id; |
| } |
| |
| std::string HttpssvcMetrics::BuildMetricName( |
| RecordType type, |
| base::StringPiece leaf_name) const { |
| // Build shared pieces of the metric names. |
| base::StringPiece type_str; |
| switch (type) { |
| case RecordType::kIntegrity: |
| type_str = "RecordIntegrity"; |
| break; |
| case RecordType::kHttps: |
| type_str = "RecordHttps"; |
| break; |
| } |
| const base::StringPiece expectation = |
| expect_intact_ ? "ExpectIntact" : "ExpectNoerror"; |
| const std::string provider_id = doh_provider_id_.value_or("Other"); |
| |
| // Example INTEGRITY metric name: |
| // Net.DNS.HTTPSSVC.RecordIntegrity.CleanBrowsingAdult.ExpectIntact.DnsRcode |
| return base::JoinString({"Net.DNS.HTTPSSVC", type_str, provider_id.c_str(), |
| expectation, leaf_name}, |
| "."); |
| } |
| |
| void HttpssvcMetrics::RecordMetrics() { |
| // The HTTPSSVC experiment and its feature param indicating INTEGRITY must |
| // both be enabled. |
| DCHECK(base::FeatureList::IsEnabled(features::kDnsHttpssvc)); |
| DCHECK(features::kDnsHttpssvcUseIntegrity.Get() || |
| features::kDnsHttpssvcUseHttpssvc.Get()); |
| |
| DCHECK(!already_recorded_); |
| already_recorded_ = true; |
| |
| // We really have no metrics to record without an experimental query resolve |
| // time and `address_resolve_times_`. If this HttpssvcMetrics is in an |
| // inconsistent state, disqualify any metrics from being recorded. |
| if ((!integrity_resolve_time_.has_value() && |
| !https_resolve_time_.has_value()) || |
| address_resolve_times_.empty()) { |
| disqualified_ = true; |
| } |
| if (disqualified_) |
| return; |
| |
| // Record the metrics that the "ExpectIntact" and "ExpectNoerror" branches |
| // have in common. |
| RecordCommonMetrics(); |
| |
| if (expect_intact_) { |
| // Record metrics that are unique to the "ExpectIntact" branch. |
| RecordExpectIntactMetrics(); |
| } else { |
| // Record metrics that are unique to the "ExpectNoerror" branch. |
| RecordExpectNoerrorMetrics(); |
| } |
| } |
| |
| void HttpssvcMetrics::RecordCommonMetrics() { |
| DCHECK(integrity_resolve_time_.has_value() || |
| https_resolve_time_.has_value()); |
| if (integrity_resolve_time_.has_value()) { |
| base::UmaHistogramMediumTimes( |
| BuildMetricName(RecordType::kIntegrity, "ResolveTimeIntegrityRecord"), |
| *integrity_resolve_time_); |
| } |
| if (https_resolve_time_.has_value()) { |
| base::UmaHistogramMediumTimes( |
| BuildMetricName(RecordType::kHttps, "ResolveTimeHttpsRecord"), |
| *https_resolve_time_); |
| } |
| |
| DCHECK(!address_resolve_times_.empty()); |
| // Not specific to INTEGRITY or HTTPS, but for the sake of picking one for the |
| // metric name and only recording the time once, always record the address |
| // resolve times under `kIntegrity`. |
| const std::string kMetricResolveTimeAddressRecord = |
| BuildMetricName(RecordType::kIntegrity, "ResolveTimeNonIntegrityRecord"); |
| for (base::TimeDelta resolve_time_other : address_resolve_times_) { |
| base::UmaHistogramMediumTimes(kMetricResolveTimeAddressRecord, |
| resolve_time_other); |
| } |
| |
| // ResolveTimeRatio is the experimental query resolve time divided by the |
| // slower of the A or AAAA resolve times. Arbitrarily choosing precision at |
| // two decimal places. |
| std::vector<base::TimeDelta>::iterator slowest_address_resolve = |
| std::max_element(address_resolve_times_.begin(), |
| address_resolve_times_.end()); |
| DCHECK(slowest_address_resolve != address_resolve_times_.end()); |
| |
| // It's possible to get here with a zero resolve time in tests. Avoid |
| // divide-by-zero below by returning early; this data point is invalid anyway. |
| if (slowest_address_resolve->is_zero()) |
| return; |
| |
| // Compute a percentage showing how much larger the experimental query resolve |
| // time was compared to the slowest A or AAAA query. |
| // |
| // Computation happens on TimeDelta objects, which use CheckedNumeric. This |
| // will crash if the system clock leaps forward several hundred millennia |
| // (numeric_limits<int64_t>::max() microseconds ~= 292,000 years). |
| // |
| // Then scale the value of the percent by dividing by `kPercentScale`. Sample |
| // values are bounded between 1 and 20. A recorded sample of 10 means that the |
| // experimental query resolve time took 100% of the slower A/AAAA resolve |
| // time. A sample of 20 means that the experimental query resolve time was |
| // 200% relative to the A/AAAA resolve time, twice as long. |
| constexpr int64_t kMaxRatio = 20; |
| constexpr int64_t kPercentScale = 10; |
| if (integrity_resolve_time_.has_value()) { |
| const int64_t resolve_time_percent = base::ClampFloor<int64_t>( |
| *integrity_resolve_time_ / *slowest_address_resolve * 100); |
| base::UmaHistogramExactLinear( |
| BuildMetricName(RecordType::kIntegrity, "ResolveTimeRatio"), |
| resolve_time_percent / kPercentScale, kMaxRatio); |
| } |
| if (https_resolve_time_.has_value()) { |
| const int64_t resolve_time_percent = base::ClampFloor<int64_t>( |
| *https_resolve_time_ / *slowest_address_resolve * 100); |
| base::UmaHistogramExactLinear( |
| BuildMetricName(RecordType::kHttps, "ResolveTimeRatio"), |
| resolve_time_percent / kPercentScale, kMaxRatio); |
| } |
| } |
| |
| void HttpssvcMetrics::RecordExpectIntactMetrics() { |
| // Without an experimental query rcode, we can't make progress on any of these |
| // metrics. |
| DCHECK(rcode_integrity_.has_value() || rcode_https_.has_value()); |
| |
| // The ExpectIntact variant of the "DnsRcode" metric is only recorded when no |
| // records are received. |
| if (num_integrity_records_ == 0 && rcode_integrity_.has_value()) { |
| base::UmaHistogramEnumeration( |
| BuildMetricName(RecordType::kIntegrity, "DnsRcode"), *rcode_integrity_); |
| } |
| if (num_https_records_ == 0 && rcode_https_.has_value()) { |
| base::UmaHistogramEnumeration( |
| BuildMetricName(RecordType::kHttps, "DnsRcode"), *rcode_https_); |
| } |
| |
| if (num_integrity_records_ > 0) { |
| DCHECK(rcode_integrity_.has_value()); |
| if (*rcode_integrity_ == HttpssvcDnsRcode::kNoError) { |
| base::UmaHistogramBoolean( |
| BuildMetricName(RecordType::kIntegrity, "Integrity"), |
| is_integrity_intact_.value_or(false)); |
| } else { |
| // Record boolean indicating whether we received an INTEGRITY record and |
| // an error simultaneously. |
| base::UmaHistogramBoolean( |
| BuildMetricName(RecordType::kIntegrity, "RecordWithError"), true); |
| } |
| } |
| if (num_https_records_ > 0) { |
| DCHECK(rcode_https_.has_value()); |
| if (*rcode_https_ == HttpssvcDnsRcode::kNoError) { |
| base::UmaHistogramBoolean(BuildMetricName(RecordType::kHttps, "Parsable"), |
| is_https_parsable_.value_or(false)); |
| } else { |
| // Record boolean indicating whether we received an HTTPS record and |
| // an error simultaneously. |
| base::UmaHistogramBoolean( |
| BuildMetricName(RecordType::kHttps, "RecordWithError"), true); |
| } |
| } |
| } |
| |
| void HttpssvcMetrics::RecordExpectNoerrorMetrics() { |
| if (rcode_integrity_.has_value()) { |
| base::UmaHistogramEnumeration( |
| BuildMetricName(RecordType::kIntegrity, "DnsRcode"), *rcode_integrity_); |
| } |
| if (rcode_https_.has_value()) { |
| base::UmaHistogramEnumeration( |
| BuildMetricName(RecordType::kHttps, "DnsRcode"), *rcode_https_); |
| } |
| |
| // INTEGRITY only records a simple boolean when an unexpected record is |
| // received because it is extremely unlikely to be an actual INTEGRITY record. |
| if (num_integrity_records_ > 0) { |
| base::UmaHistogramBoolean( |
| BuildMetricName(RecordType::kIntegrity, "RecordReceived"), true); |
| } |
| |
| // HTTPS records received for expect-noerror domains are actual in-the-wild |
| // records not specific to Chrome experiments. Record some extra metrics on |
| // seen records, but not broken out by DNS provider. |
| if (num_https_records_ > 0) { |
| if (*rcode_https_ == HttpssvcDnsRcode::kNoError) { |
| UMA_HISTOGRAM_BOOLEAN( |
| "Net.DNS.HTTPSSVC.RecordHttps.AnyProvider.ExpectNoerror.Parsable", |
| is_https_parsable_.value_or(false)); |
| } else { |
| // Record boolean indicating whether we received an HTTPS record and |
| // an error simultaneously. |
| UMA_HISTOGRAM_BOOLEAN( |
| "Net.DNS.HTTPSSVC.RecordHttps.AnyProvider.ExpectNoerror." |
| "RecordWithError", |
| true); |
| } |
| } |
| } |
| |
| } // namespace net |