Commit 5d21f151 authored by Eric Orth's avatar Eric Orth Committed by Commit Bot

Add experimental HTTPS query support to DNS

Parsing and metrics left as TODOs for now.

Bug: 1138620
Change-Id: Id8e8ab3947601276479a0970efd215fe550a1d22
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2473162Reviewed-by: default avatarTom Sepez <tsepez@chromium.org>
Reviewed-by: default avatarDan McArdle <dmcardle@chromium.org>
Commit-Queue: Eric Orth <ericorth@chromium.org>
Cr-Commit-Position: refs/heads/master@{#819472}
parent 19b5e183
...@@ -80,7 +80,9 @@ DnsResourceRecord BuildTestDnsRecord(std::string name, ...@@ -80,7 +80,9 @@ DnsResourceRecord BuildTestDnsRecord(std::string name,
record.type = type; record.type = type;
record.klass = dns_protocol::kClassIN; record.klass = dns_protocol::kClassIN;
record.ttl = ttl.InSeconds(); record.ttl = ttl.InSeconds();
record.SetOwnedRdata(std::move(rdata));
if (!rdata.empty())
record.SetOwnedRdata(std::move(rdata));
return record; return record;
} }
......
...@@ -291,6 +291,8 @@ uint16_t DnsQueryTypeToQtype(DnsQueryType dns_query_type) { ...@@ -291,6 +291,8 @@ uint16_t DnsQueryTypeToQtype(DnsQueryType dns_query_type) {
return dns_protocol::kTypeSRV; return dns_protocol::kTypeSRV;
case DnsQueryType::INTEGRITY: case DnsQueryType::INTEGRITY:
return dns_protocol::kExperimentalTypeIntegrity; return dns_protocol::kExperimentalTypeIntegrity;
case DnsQueryType::HTTPS:
return dns_protocol::kTypeHttps;
} }
} }
......
...@@ -100,6 +100,8 @@ class NET_EXPORT HostResolver { ...@@ -100,6 +100,8 @@ class NET_EXPORT HostResolver {
// INTEGRITY results for an initial experiment related to HTTPSSVC. Each // INTEGRITY results for an initial experiment related to HTTPSSVC. Each
// boolean value indicates the intactness of an INTEGRITY record. // boolean value indicates the intactness of an INTEGRITY record.
// TODO(crbug.com/1138620): Generalize to be used for other experimental
// records.
NET_EXPORT virtual const base::Optional<std::vector<bool>>& NET_EXPORT virtual const base::Optional<std::vector<bool>>&
GetIntegrityResultsForTesting() const; GetIntegrityResultsForTesting() const;
......
...@@ -1124,15 +1124,15 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> { ...@@ -1124,15 +1124,15 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> {
const bool is_httpssvc_control_domain = const bool is_httpssvc_control_domain =
httpssvc_domain_cache_.IsControl(hostname); httpssvc_domain_cache_.IsControl(hostname);
if (base::FeatureList::IsEnabled(features::kDnsHttpssvc) && if (base::FeatureList::IsEnabled(features::kDnsHttpssvc) &&
features::kDnsHttpssvcUseIntegrity.Get() &&
(secure_ || features::kDnsHttpssvcEnableQueryOverInsecure.Get()) && (secure_ || features::kDnsHttpssvcEnableQueryOverInsecure.Get()) &&
(is_httpssvc_experiment_domain || is_httpssvc_control_domain)) { (is_httpssvc_experiment_domain || is_httpssvc_control_domain)) {
// We should not be configured to query HTTPSSVC *and* INTEGRITY.
DCHECK(!features::kDnsHttpssvcUseHttpssvc.Get());
httpssvc_metrics_.emplace( httpssvc_metrics_.emplace(
is_httpssvc_experiment_domain /* expect_intact */); is_httpssvc_experiment_domain /* expect_intact */);
transactions_needed_.push(DnsQueryType::INTEGRITY);
if (features::kDnsHttpssvcUseIntegrity.Get())
transactions_needed_.push(DnsQueryType::INTEGRITY);
if (features::kDnsHttpssvcUseHttpssvc.Get())
transactions_needed_.push(DnsQueryType::HTTPS);
} }
} }
num_needed_transactions_ = transactions_needed_.size(); num_needed_transactions_ = transactions_needed_.size();
...@@ -1194,25 +1194,31 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> { ...@@ -1194,25 +1194,31 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> {
return trans; return trans;
} }
void OnExperimentalQueryTimeout(uint16_t qtype, void OnExperimentalQueryTimeout(base::Optional<std::string> doh_provider_id) {
base::Optional<std::string> doh_provider_id) { for (std::unique_ptr<DnsTransaction>& transaction : transactions_started_) {
// The experimental query timer is only started when all other transactions DCHECK(httpssvc_metrics_);
// have completed. base::TimeDelta elapsed_time = tick_clock_->NowTicks() - task_start_time_;
DCHECK(TaskIsCompleteOrOnlyQtypeTransactionsRemain(qtype));
switch (transaction->GetType()) {
case dns_protocol::kExperimentalTypeIntegrity:
httpssvc_metrics_->SaveForIntegrity(
doh_provider_id, HttpssvcDnsRcode::kTimedOut, {}, elapsed_time);
break;
case dns_protocol::kTypeHttps:
httpssvc_metrics_->SaveForHttps(
doh_provider_id, HttpssvcDnsRcode::kTimedOut, elapsed_time);
break;
default:
// The experimental query timer is only started when all other
// transactions have completed.
NOTREACHED();
}
}
num_completed_transactions_ += transactions_started_.size(); num_completed_transactions_ += transactions_started_.size();
DCHECK(num_completed_transactions_ == num_needed_transactions()); DCHECK(num_completed_transactions_ == num_needed_transactions());
transactions_started_.clear(); transactions_started_.clear();
if (qtype == dns_protocol::kExperimentalTypeIntegrity) {
DCHECK(httpssvc_metrics_);
// Record that this INTEGRITY query timed out in the metrics.
base::TimeDelta elapsed_time = tick_clock_->NowTicks() - task_start_time_;
httpssvc_metrics_->SaveForIntegrity(
doh_provider_id, HttpssvcDnsRcode::kTimedOut, {}, elapsed_time);
}
ProcessResultsOnCompletion(); ProcessResultsOnCompletion();
} }
...@@ -1254,8 +1260,9 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> { ...@@ -1254,8 +1260,9 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> {
if (net_error != OK && !(net_error == ERR_NAME_NOT_RESOLVED && response && if (net_error != OK && !(net_error == ERR_NAME_NOT_RESOLVED && response &&
response->IsValid())) { response->IsValid())) {
if (dns_query_type == DnsQueryType::INTEGRITY) { if (dns_query_type == DnsQueryType::INTEGRITY ||
// Do not allow an INTEGRITY query to fail the whole DnsTask. dns_query_type == DnsQueryType::HTTPS) {
// Do not allow an experimental query to fail the whole DnsTask.
response = nullptr; response = nullptr;
} else { } else {
OnFailure(net_error, DnsResponse::DNS_PARSE_OK, base::nullopt); OnFailure(net_error, DnsResponse::DNS_PARSE_OK, base::nullopt);
...@@ -1287,6 +1294,9 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> { ...@@ -1287,6 +1294,9 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> {
// Parse the INTEGRITY records, condensing them into a vector<bool>. // Parse the INTEGRITY records, condensing them into a vector<bool>.
parse_result = ParseIntegrityDnsResponse(response, &results); parse_result = ParseIntegrityDnsResponse(response, &results);
break; break;
case DnsQueryType::HTTPS:
parse_result = ParseHttpsDnsResponse(response, &results);
break;
} }
DCHECK_LT(parse_result, DnsResponse::DNS_PARSE_RESULT_MAX); DCHECK_LT(parse_result, DnsResponse::DNS_PARSE_RESULT_MAX);
...@@ -1296,10 +1306,7 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> { ...@@ -1296,10 +1306,7 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> {
} }
if (httpssvc_metrics_) { if (httpssvc_metrics_) {
if (dns_query_type != DnsQueryType::INTEGRITY) { if (dns_query_type == DnsQueryType::INTEGRITY) {
httpssvc_metrics_->SaveForNonIntegrity(doh_provider_id, elapsed_time,
rcode_for_httpssvc);
} else {
const base::Optional<std::vector<bool>>& condensed = const base::Optional<std::vector<bool>>& condensed =
results.integrity_data(); results.integrity_data();
CHECK(condensed.has_value()); CHECK(condensed.has_value());
...@@ -1307,6 +1314,12 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> { ...@@ -1307,6 +1314,12 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> {
// experimental query timer runs out (OnExperimentalQueryTimeout). // experimental query timer runs out (OnExperimentalQueryTimeout).
httpssvc_metrics_->SaveForIntegrity(doh_provider_id, rcode_for_httpssvc, httpssvc_metrics_->SaveForIntegrity(doh_provider_id, rcode_for_httpssvc,
*condensed, elapsed_time); *condensed, elapsed_time);
} else if (dns_query_type == DnsQueryType::HTTPS) {
httpssvc_metrics_->SaveForHttps(doh_provider_id, rcode_for_httpssvc,
elapsed_time);
} else {
httpssvc_metrics_->SaveForNonIntegrity(doh_provider_id, elapsed_time,
rcode_for_httpssvc);
} }
} }
...@@ -1332,6 +1345,9 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> { ...@@ -1332,6 +1345,9 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> {
results = HostCache::Entry::MergeEntries( results = HostCache::Entry::MergeEntries(
std::move(results), std::move(saved_results_).value()); std::move(results), std::move(saved_results_).value());
break; break;
case DnsQueryType::HTTPS:
results = std::move(saved_results_).value();
break;
default: default:
// Only expect address query types with multiple transactions. // Only expect address query types with multiple transactions.
NOTREACHED(); NOTREACHED();
...@@ -1534,6 +1550,14 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> { ...@@ -1534,6 +1550,14 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> {
return parse_result; return parse_result;
} }
DnsResponse::Result ParseHttpsDnsResponse(const DnsResponse* response,
HostCache::Entry* out_results) {
// TODO(crbug.com/1138620): Add actual parse implementation.
*out_results = HostCache::Entry(
ERR_NAME_NOT_RESOLVED, HostCache::Entry::SOURCE_DNS, base::nullopt);
return DnsResponse::Result::DNS_PARSE_OK;
}
// Sort service targets per RFC2782. In summary, sort first by |priority|, // Sort service targets per RFC2782. In summary, sort first by |priority|,
// lowest first. For targets with the same priority, secondary sort randomly // lowest first. For targets with the same priority, secondary sort randomly
// using |weight| with higher weighted objects more likely to go first. // using |weight| with higher weighted objects more likely to go first.
...@@ -1705,7 +1729,8 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> { ...@@ -1705,7 +1729,8 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> {
// |qtype|. (In particular, this is the case if all transactions are // |qtype|. (In particular, this is the case if all transactions are
// complete.) Used for logging and starting the experimental query timer (see // complete.) Used for logging and starting the experimental query timer (see
// MaybeStartExperimentalQueryTimer). // MaybeStartExperimentalQueryTimer).
bool TaskIsCompleteOrOnlyQtypeTransactionsRemain(uint16_t qtype) const { bool TaskIsCompleteOrOnlyQtypeTransactionsRemain(
std::initializer_list<uint16_t> qtypes) const {
// Since DoH runs all transactions concurrently and experimental types are // Since DoH runs all transactions concurrently and experimental types are
// only queried over DoH, this method only needs to check the transactions // only queried over DoH, this method only needs to check the transactions
// in transactions_started_ because transactions_needed_ is empty from the // in transactions_started_ because transactions_needed_ is empty from the
...@@ -1714,13 +1739,14 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> { ...@@ -1714,13 +1739,14 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> {
return std::all_of( return std::all_of(
transactions_started_.begin(), transactions_started_.end(), transactions_started_.begin(), transactions_started_.end(),
[&](const std::unique_ptr<DnsTransaction>& p) { [&](const std::unique_ptr<DnsTransaction>& transaction) {
DCHECK(p); DCHECK(transaction);
return p->GetType() == qtype; return std::any_of(qtypes.begin(), qtypes.end(), [&](uint16_t qtype) {
return transaction->GetType() == qtype;
});
}); });
} }
void MaybeStartExperimentalQueryTimer( void MaybeStartExperimentalQueryTimer(
base::Optional<std::string> doh_provider_id) { base::Optional<std::string> doh_provider_id) {
DCHECK(!transactions_started_.empty()); DCHECK(!transactions_started_.empty());
...@@ -1734,7 +1760,8 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> { ...@@ -1734,7 +1760,8 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> {
if (!experimental_query_cancellation_timer_.IsRunning() && if (!experimental_query_cancellation_timer_.IsRunning() &&
TaskIsCompleteOrOnlyQtypeTransactionsRemain( TaskIsCompleteOrOnlyQtypeTransactionsRemain(
dns_protocol::kExperimentalTypeIntegrity)) { {dns_protocol::kExperimentalTypeIntegrity,
dns_protocol::kTypeHttps})) {
const base::TimeDelta kExtraTimeAbsolute = const base::TimeDelta kExtraTimeAbsolute =
features::dns_httpssvc_experiment::GetExtraTimeAbsolute(); features::dns_httpssvc_experiment::GetExtraTimeAbsolute();
const int kExtraTimePercent = const int kExtraTimePercent =
...@@ -1744,14 +1771,16 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> { ...@@ -1744,14 +1771,16 @@ class HostResolverManager::DnsTask : public base::SupportsWeakPtr<DnsTask> {
tick_clock_->NowTicks() - task_start_time_; tick_clock_->NowTicks() - task_start_time_;
base::TimeDelta relative_timeout = base::TimeDelta relative_timeout =
total_time_for_other_transactions * kExtraTimePercent / 100; total_time_for_other_transactions * kExtraTimePercent / 100;
// Use at least 1ms to ensure timeout doesn't occur immediately in tests.
relative_timeout =
std::max(relative_timeout, base::TimeDelta::FromMilliseconds(1));
base::TimeDelta timeout = std::min(kExtraTimeAbsolute, relative_timeout); base::TimeDelta timeout = std::min(kExtraTimeAbsolute, relative_timeout);
experimental_query_cancellation_timer_.Start( experimental_query_cancellation_timer_.Start(
FROM_HERE, timeout, FROM_HERE, timeout,
base::BindOnce( base::BindOnce(&DnsTask::OnExperimentalQueryTimeout,
&DnsTask::OnExperimentalQueryTimeout, base::Unretained(this), base::Unretained(this), doh_provider_id));
dns_protocol::kExperimentalTypeIntegrity, doh_provider_id));
} }
} }
......
This diff is collapsed.
...@@ -74,6 +74,7 @@ void HostResolverMdnsListenerImpl::OnRecordUpdate( ...@@ -74,6 +74,7 @@ void HostResolverMdnsListenerImpl::OnRecordUpdate(
switch (query_type_) { switch (query_type_) {
case DnsQueryType::UNSPECIFIED: case DnsQueryType::UNSPECIFIED:
case DnsQueryType::INTEGRITY: case DnsQueryType::INTEGRITY:
case DnsQueryType::HTTPS:
NOTREACHED(); NOTREACHED();
break; break;
case DnsQueryType::A: case DnsQueryType::A:
......
...@@ -198,6 +198,10 @@ HostCache::Entry HostResolverMdnsTask::ParseResult( ...@@ -198,6 +198,10 @@ HostCache::Entry HostResolverMdnsTask::ParseResult(
switch (query_type) { switch (query_type) {
case DnsQueryType::UNSPECIFIED: case DnsQueryType::UNSPECIFIED:
// Should create two separate transactions with specified type. // Should create two separate transactions with specified type.
case DnsQueryType::HTTPS:
// Not supported.
// TODO(ericorth@chromium.org): Consider support for HTTPS in mDNS if it
// is ever decided to support HTTPS via non-DoH.
case DnsQueryType::INTEGRITY: case DnsQueryType::INTEGRITY:
// INTEGRITY queries are not expected to be useful in mDNS, so they're not // INTEGRITY queries are not expected to be useful in mDNS, so they're not
// supported. // supported.
......
...@@ -116,6 +116,12 @@ void HttpssvcMetrics::SaveForIntegrity( ...@@ -116,6 +116,12 @@ void HttpssvcMetrics::SaveForIntegrity(
integrity_resolve_time_ = integrity_resolve_time; integrity_resolve_time_ = integrity_resolve_time;
} }
void HttpssvcMetrics::SaveForHttps(base::Optional<std::string> doh_provider_id,
enum HttpssvcDnsRcode rcode,
base::TimeDelta https_resolve_time) {
// TODO(crbug.com/1138620): Implement.
}
void HttpssvcMetrics::set_doh_provider_id( void HttpssvcMetrics::set_doh_provider_id(
base::Optional<std::string> new_doh_provider_id) { base::Optional<std::string> new_doh_provider_id) {
// "Other" never gets updated. // "Other" never gets updated.
...@@ -150,7 +156,8 @@ void HttpssvcMetrics::RecordIntegrityMetrics() { ...@@ -150,7 +156,8 @@ void HttpssvcMetrics::RecordIntegrityMetrics() {
// The HTTPSSVC experiment and its feature param indicating INTEGRITY must // The HTTPSSVC experiment and its feature param indicating INTEGRITY must
// both be enabled. // both be enabled.
DCHECK(base::FeatureList::IsEnabled(features::kDnsHttpssvc)); DCHECK(base::FeatureList::IsEnabled(features::kDnsHttpssvc));
DCHECK(features::kDnsHttpssvcUseIntegrity.Get()); DCHECK(features::kDnsHttpssvcUseIntegrity.Get() ||
features::kDnsHttpssvcUseHttpssvc.Get());
DCHECK(!already_recorded_); DCHECK(!already_recorded_);
already_recorded_ = true; already_recorded_ = true;
......
...@@ -81,6 +81,9 @@ class NET_EXPORT_PRIVATE HttpssvcMetrics { ...@@ -81,6 +81,9 @@ class NET_EXPORT_PRIVATE HttpssvcMetrics {
enum HttpssvcDnsRcode rcode, enum HttpssvcDnsRcode rcode,
const std::vector<bool>& condensed_records, const std::vector<bool>& condensed_records,
base::TimeDelta integrity_resolve_time); base::TimeDelta integrity_resolve_time);
void SaveForHttps(base::Optional<std::string> doh_provider_id,
enum HttpssvcDnsRcode rcode,
base::TimeDelta https_resolve_time);
private: private:
std::string BuildMetricName(base::StringPiece leaf_name) const; std::string BuildMetricName(base::StringPiece leaf_name) const;
......
...@@ -149,6 +149,7 @@ static const uint16_t kTypeAAAA = 28; ...@@ -149,6 +149,7 @@ static const uint16_t kTypeAAAA = 28;
static const uint16_t kTypeSRV = 33; static const uint16_t kTypeSRV = 33;
static const uint16_t kTypeOPT = 41; static const uint16_t kTypeOPT = 41;
static const uint16_t kTypeNSEC = 47; static const uint16_t kTypeNSEC = 47;
static const uint16_t kTypeHttps = 65;
static const uint16_t kTypeANY = 255; static const uint16_t kTypeANY = 255;
// Experimental DNS record types pending IANA assignment. // Experimental DNS record types pending IANA assignment.
......
...@@ -21,14 +21,14 @@ enum class DnsQueryType { ...@@ -21,14 +21,14 @@ enum class DnsQueryType {
PTR, PTR,
SRV, SRV,
INTEGRITY, INTEGRITY,
MAX = INTEGRITY HTTPS,
MAX = HTTPS
}; };
const DnsQueryType kDnsQueryTypes[] = { const DnsQueryType kDnsQueryTypes[] = {
DnsQueryType::UNSPECIFIED, DnsQueryType::A, DnsQueryType::AAAA, DnsQueryType::UNSPECIFIED, DnsQueryType::A, DnsQueryType::AAAA,
DnsQueryType::TXT, DnsQueryType::PTR, DnsQueryType::SRV, DnsQueryType::TXT, DnsQueryType::PTR, DnsQueryType::SRV,
DnsQueryType::INTEGRITY, DnsQueryType::INTEGRITY, DnsQueryType::HTTPS};
};
static_assert(base::size(kDnsQueryTypes) == static_assert(base::size(kDnsQueryTypes) ==
static_cast<unsigned>(DnsQueryType::MAX) + 1, static_cast<unsigned>(DnsQueryType::MAX) + 1,
......
...@@ -35,6 +35,9 @@ bool RecordRdata::HasValidSize(const base::StringPiece& data, uint16_t type) { ...@@ -35,6 +35,9 @@ bool RecordRdata::HasValidSize(const base::StringPiece& data, uint16_t type) {
return data.size() == IPAddress::kIPv6AddressSize; return data.size() == IPAddress::kIPv6AddressSize;
case dns_protocol::kExperimentalTypeIntegrity: case dns_protocol::kExperimentalTypeIntegrity:
return data.size() >= kIntegrityMinimumSize; return data.size() >= kIntegrityMinimumSize;
case dns_protocol::kTypeHttps:
// TODO(crbug.com/1138620): Implement actual size minimum.
return data.size() == 0;
case dns_protocol::kTypeCNAME: case dns_protocol::kTypeCNAME:
case dns_protocol::kTypePTR: case dns_protocol::kTypePTR:
case dns_protocol::kTypeTXT: case dns_protocol::kTypeTXT:
......
...@@ -214,6 +214,8 @@ DnsQueryType EnumTraits<DnsQueryType, net::DnsQueryType>::ToMojom( ...@@ -214,6 +214,8 @@ DnsQueryType EnumTraits<DnsQueryType, net::DnsQueryType>::ToMojom(
return DnsQueryType::PTR; return DnsQueryType::PTR;
case net::DnsQueryType::SRV: case net::DnsQueryType::SRV:
return DnsQueryType::SRV; return DnsQueryType::SRV;
case net::DnsQueryType::HTTPS:
return DnsQueryType::HTTPS;
case net::DnsQueryType::INTEGRITY: case net::DnsQueryType::INTEGRITY:
NOTIMPLEMENTED(); NOTIMPLEMENTED();
return DnsQueryType::UNSPECIFIED; return DnsQueryType::UNSPECIFIED;
...@@ -243,6 +245,9 @@ bool EnumTraits<DnsQueryType, net::DnsQueryType>::FromMojom( ...@@ -243,6 +245,9 @@ bool EnumTraits<DnsQueryType, net::DnsQueryType>::FromMojom(
case DnsQueryType::SRV: case DnsQueryType::SRV:
*output = net::DnsQueryType::SRV; *output = net::DnsQueryType::SRV;
return true; return true;
case DnsQueryType::HTTPS:
*output = net::DnsQueryType::HTTPS;
return true;
} }
} }
......
...@@ -163,6 +163,7 @@ enum DnsQueryType { ...@@ -163,6 +163,7 @@ enum DnsQueryType {
TXT, TXT,
PTR, PTR,
SRV, SRV,
HTTPS,
}; };
// Parameter-grouping struct for additional optional parameters for // Parameter-grouping struct for additional optional parameters for
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment