You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
433 lines
14 KiB
433 lines
14 KiB
// Copyright 2019 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 "discovery/mdns/mdns_trackers.h"
|
|
|
|
#include <array>
|
|
#include <limits>
|
|
#include <utility>
|
|
|
|
#include "discovery/common/config.h"
|
|
#include "discovery/mdns/mdns_random.h"
|
|
#include "discovery/mdns/mdns_record_changed_callback.h"
|
|
#include "discovery/mdns/mdns_sender.h"
|
|
#include "util/std_util.h"
|
|
|
|
namespace openscreen {
|
|
namespace discovery {
|
|
|
|
namespace {
|
|
|
|
// RFC 6762 Section 5.2
|
|
// https://tools.ietf.org/html/rfc6762#section-5.2
|
|
|
|
// Attempt to refresh a record should be performed at 80%, 85%, 90% and 95% TTL.
|
|
constexpr double kTtlFractions[] = {0.80, 0.85, 0.90, 0.95, 1.00};
|
|
|
|
// Intervals between successive queries must increase by at least a factor of 2.
|
|
constexpr int kIntervalIncreaseFactor = 2;
|
|
|
|
// The interval between the first two queries must be at least one second.
|
|
constexpr std::chrono::seconds kMinimumQueryInterval{1};
|
|
|
|
// The querier may cap the question refresh interval to a maximum of 60 minutes.
|
|
constexpr std::chrono::minutes kMaximumQueryInterval{60};
|
|
|
|
// RFC 6762 Section 10.1
|
|
// https://tools.ietf.org/html/rfc6762#section-10.1
|
|
|
|
// A goodbye record is a record with TTL of 0.
|
|
bool IsGoodbyeRecord(const MdnsRecord& record) {
|
|
return record.ttl() == std::chrono::seconds(0);
|
|
}
|
|
|
|
bool IsNegativeResponseForType(const MdnsRecord& record, DnsType dns_type) {
|
|
if (record.dns_type() != DnsType::kNSEC) {
|
|
return false;
|
|
}
|
|
|
|
const auto& nsec_types = absl::get<NsecRecordRdata>(record.rdata()).types();
|
|
return std::find_if(nsec_types.begin(), nsec_types.end(),
|
|
[dns_type](DnsType type) {
|
|
return type == dns_type || type == DnsType::kANY;
|
|
}) != nsec_types.end();
|
|
}
|
|
|
|
// RFC 6762 Section 10.1
|
|
// https://tools.ietf.org/html/rfc6762#section-10.1
|
|
// In case of a goodbye record, the querier should set TTL to 1 second
|
|
constexpr std::chrono::seconds kGoodbyeRecordTtl{1};
|
|
|
|
} // namespace
|
|
|
|
MdnsTracker::MdnsTracker(MdnsSender* sender,
|
|
TaskRunner* task_runner,
|
|
ClockNowFunctionPtr now_function,
|
|
MdnsRandom* random_delay,
|
|
TrackerType tracker_type)
|
|
: sender_(sender),
|
|
task_runner_(task_runner),
|
|
now_function_(now_function),
|
|
send_alarm_(now_function, task_runner),
|
|
random_delay_(random_delay),
|
|
tracker_type_(tracker_type) {
|
|
OSP_DCHECK(task_runner_);
|
|
OSP_DCHECK(now_function_);
|
|
OSP_DCHECK(random_delay_);
|
|
OSP_DCHECK(sender_);
|
|
}
|
|
|
|
MdnsTracker::~MdnsTracker() {
|
|
send_alarm_.Cancel();
|
|
|
|
for (const MdnsTracker* node : adjacent_nodes_) {
|
|
node->RemovedReverseAdjacency(this);
|
|
}
|
|
}
|
|
|
|
bool MdnsTracker::AddAdjacentNode(const MdnsTracker* node) const {
|
|
OSP_DCHECK(node);
|
|
OSP_DCHECK(task_runner_->IsRunningOnTaskRunner());
|
|
|
|
auto it = std::find(adjacent_nodes_.begin(), adjacent_nodes_.end(), node);
|
|
if (it != adjacent_nodes_.end()) {
|
|
return false;
|
|
}
|
|
|
|
adjacent_nodes_.push_back(node);
|
|
node->AddReverseAdjacency(this);
|
|
return true;
|
|
}
|
|
|
|
bool MdnsTracker::RemoveAdjacentNode(const MdnsTracker* node) const {
|
|
OSP_DCHECK(node);
|
|
OSP_DCHECK(task_runner_->IsRunningOnTaskRunner());
|
|
|
|
auto it = std::find(adjacent_nodes_.begin(), adjacent_nodes_.end(), node);
|
|
if (it == adjacent_nodes_.end()) {
|
|
return false;
|
|
}
|
|
|
|
adjacent_nodes_.erase(it);
|
|
node->RemovedReverseAdjacency(this);
|
|
return true;
|
|
}
|
|
|
|
void MdnsTracker::AddReverseAdjacency(const MdnsTracker* node) const {
|
|
OSP_DCHECK(std::find(adjacent_nodes_.begin(), adjacent_nodes_.end(), node) ==
|
|
adjacent_nodes_.end());
|
|
|
|
adjacent_nodes_.push_back(node);
|
|
}
|
|
|
|
void MdnsTracker::RemovedReverseAdjacency(const MdnsTracker* node) const {
|
|
auto it = std::find(adjacent_nodes_.begin(), adjacent_nodes_.end(), node);
|
|
OSP_DCHECK(it != adjacent_nodes_.end());
|
|
|
|
adjacent_nodes_.erase(it);
|
|
}
|
|
|
|
MdnsRecordTracker::MdnsRecordTracker(
|
|
MdnsRecord record,
|
|
DnsType dns_type,
|
|
MdnsSender* sender,
|
|
TaskRunner* task_runner,
|
|
ClockNowFunctionPtr now_function,
|
|
MdnsRandom* random_delay,
|
|
RecordExpiredCallback record_expired_callback)
|
|
: MdnsTracker(sender,
|
|
task_runner,
|
|
now_function,
|
|
random_delay,
|
|
TrackerType::kRecordTracker),
|
|
record_(std::move(record)),
|
|
dns_type_(dns_type),
|
|
start_time_(now_function_()),
|
|
record_expired_callback_(std::move(record_expired_callback)) {
|
|
OSP_DCHECK(record_expired_callback_);
|
|
|
|
// RecordTrackers cannot be created for tracking NSEC types or ANY types.
|
|
OSP_DCHECK(dns_type_ != DnsType::kNSEC);
|
|
OSP_DCHECK(dns_type_ != DnsType::kANY);
|
|
|
|
// Validate that, if the provided |record| is an NSEC record, then it provides
|
|
// a negative response for |dns_type|.
|
|
OSP_DCHECK(record_.dns_type() != DnsType::kNSEC ||
|
|
IsNegativeResponseForType(record_, dns_type_));
|
|
|
|
ScheduleFollowUpQuery();
|
|
}
|
|
|
|
MdnsRecordTracker::~MdnsRecordTracker() = default;
|
|
|
|
ErrorOr<MdnsRecordTracker::UpdateType> MdnsRecordTracker::Update(
|
|
const MdnsRecord& new_record) {
|
|
OSP_DCHECK(task_runner_->IsRunningOnTaskRunner());
|
|
const bool has_same_rdata = record_.dns_type() == new_record.dns_type() &&
|
|
record_.rdata() == new_record.rdata();
|
|
const bool new_is_negative_response = new_record.dns_type() == DnsType::kNSEC;
|
|
const bool current_is_negative_response =
|
|
record_.dns_type() == DnsType::kNSEC;
|
|
|
|
if ((record_.dns_class() != new_record.dns_class()) ||
|
|
(record_.name() != new_record.name())) {
|
|
// The new record has been passed to a wrong tracker.
|
|
return Error::Code::kParameterInvalid;
|
|
}
|
|
|
|
// New response record must correspond to the correct type.
|
|
if ((!new_is_negative_response && new_record.dns_type() != dns_type_) ||
|
|
(new_is_negative_response &&
|
|
!IsNegativeResponseForType(new_record, dns_type_))) {
|
|
// The new record has been passed to a wrong tracker.
|
|
return Error::Code::kParameterInvalid;
|
|
}
|
|
|
|
// Goodbye records must have the same RDATA but TTL of 0.
|
|
// RFC 6762 Section 10.1.
|
|
// https://tools.ietf.org/html/rfc6762#section-10.1
|
|
if (!new_is_negative_response && !current_is_negative_response &&
|
|
IsGoodbyeRecord(new_record) && !has_same_rdata) {
|
|
// The new record has been passed to a wrong tracker.
|
|
return Error::Code::kParameterInvalid;
|
|
}
|
|
|
|
UpdateType result = UpdateType::kGoodbye;
|
|
if (IsGoodbyeRecord(new_record)) {
|
|
record_ = MdnsRecord(new_record.name(), new_record.dns_type(),
|
|
new_record.dns_class(), new_record.record_type(),
|
|
kGoodbyeRecordTtl, new_record.rdata());
|
|
|
|
// Goodbye records do not need to be re-queried, set the attempt count to
|
|
// the last item, which is 100% of TTL, i.e. record expiration.
|
|
attempt_count_ = countof(kTtlFractions) - 1;
|
|
} else {
|
|
record_ = new_record;
|
|
attempt_count_ = 0;
|
|
result = has_same_rdata ? UpdateType::kTTLOnly : UpdateType::kRdata;
|
|
}
|
|
|
|
start_time_ = now_function_();
|
|
ScheduleFollowUpQuery();
|
|
|
|
return result;
|
|
}
|
|
|
|
bool MdnsRecordTracker::AddAssociatedQuery(
|
|
const MdnsQuestionTracker* question_tracker) const {
|
|
return AddAdjacentNode(question_tracker);
|
|
}
|
|
|
|
bool MdnsRecordTracker::RemoveAssociatedQuery(
|
|
const MdnsQuestionTracker* question_tracker) const {
|
|
return RemoveAdjacentNode(question_tracker);
|
|
}
|
|
|
|
void MdnsRecordTracker::ExpireSoon() {
|
|
OSP_DCHECK(task_runner_->IsRunningOnTaskRunner());
|
|
|
|
record_ =
|
|
MdnsRecord(record_.name(), record_.dns_type(), record_.dns_class(),
|
|
record_.record_type(), kGoodbyeRecordTtl, record_.rdata());
|
|
|
|
// Set the attempt count to the last item, which is 100% of TTL, i.e. record
|
|
// expiration, to prevent any re-queries
|
|
attempt_count_ = countof(kTtlFractions) - 1;
|
|
start_time_ = now_function_();
|
|
ScheduleFollowUpQuery();
|
|
}
|
|
|
|
void MdnsRecordTracker::ExpireNow() {
|
|
record_expired_callback_(this, record_);
|
|
}
|
|
|
|
bool MdnsRecordTracker::IsNearingExpiry() const {
|
|
return (now_function_() - start_time_) > record_.ttl() / 2;
|
|
}
|
|
|
|
bool MdnsRecordTracker::SendQuery() const {
|
|
const Clock::time_point expiration_time = start_time_ + record_.ttl();
|
|
bool is_expired = (now_function_() >= expiration_time);
|
|
if (!is_expired) {
|
|
for (const MdnsTracker* tracker : adjacent_nodes()) {
|
|
tracker->SendQuery();
|
|
}
|
|
} else {
|
|
record_expired_callback_(this, record_);
|
|
}
|
|
|
|
return !is_expired;
|
|
}
|
|
|
|
void MdnsRecordTracker::ScheduleFollowUpQuery() {
|
|
send_alarm_.Schedule(
|
|
[this] {
|
|
if (SendQuery()) {
|
|
ScheduleFollowUpQuery();
|
|
}
|
|
},
|
|
GetNextSendTime());
|
|
}
|
|
|
|
std::vector<MdnsRecord> MdnsRecordTracker::GetRecords() const {
|
|
return {record_};
|
|
}
|
|
|
|
Clock::time_point MdnsRecordTracker::GetNextSendTime() {
|
|
OSP_DCHECK(attempt_count_ < countof(kTtlFractions));
|
|
|
|
double ttl_fraction = kTtlFractions[attempt_count_++];
|
|
|
|
// Do not add random variation to the expiration time (last fraction of TTL)
|
|
if (attempt_count_ != countof(kTtlFractions)) {
|
|
ttl_fraction += random_delay_->GetRecordTtlVariation();
|
|
}
|
|
|
|
const Clock::duration delay =
|
|
Clock::to_duration(record_.ttl() * ttl_fraction);
|
|
return start_time_ + delay;
|
|
}
|
|
|
|
MdnsQuestionTracker::MdnsQuestionTracker(MdnsQuestion question,
|
|
MdnsSender* sender,
|
|
TaskRunner* task_runner,
|
|
ClockNowFunctionPtr now_function,
|
|
MdnsRandom* random_delay,
|
|
const Config& config,
|
|
QueryType query_type)
|
|
: MdnsTracker(sender,
|
|
task_runner,
|
|
now_function,
|
|
random_delay,
|
|
TrackerType::kQuestionTracker),
|
|
question_(std::move(question)),
|
|
send_delay_(kMinimumQueryInterval),
|
|
query_type_(query_type),
|
|
maximum_announcement_count_(config.new_query_announcement_count < 0
|
|
? INT_MAX
|
|
: config.new_query_announcement_count) {
|
|
// Initialize the last send time to time_point::min() so that the next call to
|
|
// SendQuery() is guaranteed to query the network.
|
|
last_send_time_ = TrivialClockTraits::time_point::min();
|
|
|
|
// The initial query has to be sent after a random delay of 20-120
|
|
// milliseconds.
|
|
if (announcements_so_far_ < maximum_announcement_count_) {
|
|
announcements_so_far_++;
|
|
|
|
if (query_type_ == QueryType::kOneShot) {
|
|
task_runner_->PostTask([this] { MdnsQuestionTracker::SendQuery(); });
|
|
} else {
|
|
OSP_DCHECK(query_type_ == QueryType::kContinuous);
|
|
send_alarm_.ScheduleFromNow(
|
|
[this]() {
|
|
MdnsQuestionTracker::SendQuery();
|
|
ScheduleFollowUpQuery();
|
|
},
|
|
random_delay_->GetInitialQueryDelay());
|
|
}
|
|
}
|
|
}
|
|
|
|
MdnsQuestionTracker::~MdnsQuestionTracker() = default;
|
|
|
|
bool MdnsQuestionTracker::AddAssociatedRecord(
|
|
const MdnsRecordTracker* record_tracker) const {
|
|
return AddAdjacentNode(record_tracker);
|
|
}
|
|
|
|
bool MdnsQuestionTracker::RemoveAssociatedRecord(
|
|
const MdnsRecordTracker* record_tracker) const {
|
|
return RemoveAdjacentNode(record_tracker);
|
|
}
|
|
|
|
std::vector<MdnsRecord> MdnsQuestionTracker::GetRecords() const {
|
|
std::vector<MdnsRecord> records;
|
|
for (const MdnsTracker* tracker : adjacent_nodes()) {
|
|
OSP_DCHECK(tracker->tracker_type() == TrackerType::kRecordTracker);
|
|
|
|
// This call cannot result in an infinite loop because MdnsRecordTracker
|
|
// instances only return a single record from this call.
|
|
std::vector<MdnsRecord> node_records = tracker->GetRecords();
|
|
OSP_DCHECK(node_records.size() == 1);
|
|
|
|
records.push_back(std::move(node_records[0]));
|
|
}
|
|
|
|
return records;
|
|
}
|
|
|
|
bool MdnsQuestionTracker::SendQuery() const {
|
|
// NOTE: The RFC does not specify the minimum interval between queries for
|
|
// multiple records of the same query when initiated for different reasons
|
|
// (such as for different record refreshes or for one record refresh and the
|
|
// periodic re-querying for a continuous query). For this reason, a constant
|
|
// outside of scope of the RFC has been chosen.
|
|
TrivialClockTraits::time_point now = now_function_();
|
|
if (now < last_send_time_ + kMinimumQueryInterval) {
|
|
return true;
|
|
}
|
|
last_send_time_ = now;
|
|
|
|
MdnsMessage message(CreateMessageId(), MessageType::Query);
|
|
message.AddQuestion(question_);
|
|
|
|
// Send the message and additional known answer packets as needed.
|
|
for (auto it = adjacent_nodes().begin(); it != adjacent_nodes().end();) {
|
|
OSP_DCHECK((*it)->tracker_type() == TrackerType::kRecordTracker);
|
|
|
|
const MdnsRecordTracker* record_tracker =
|
|
static_cast<const MdnsRecordTracker*>(*it);
|
|
if (record_tracker->IsNearingExpiry()) {
|
|
it++;
|
|
continue;
|
|
}
|
|
|
|
// A record tracker should only contain one record.
|
|
std::vector<MdnsRecord> node_records = (*it)->GetRecords();
|
|
OSP_DCHECK(node_records.size() == 1);
|
|
MdnsRecord node_record = std::move(node_records[0]);
|
|
|
|
if (message.CanAddRecord(node_record)) {
|
|
message.AddAnswer(std::move(node_record));
|
|
it++;
|
|
} else if (message.questions().empty() && message.answers().empty()) {
|
|
// This case should never happen, because it means a record is too large
|
|
// to fit into its own message.
|
|
OSP_LOG_INFO
|
|
<< "Encountered unreasonably large message in cache. Skipping "
|
|
<< "known answer in suppressions...";
|
|
it++;
|
|
} else {
|
|
message.set_truncated();
|
|
sender_->SendMulticast(message);
|
|
message = MdnsMessage(CreateMessageId(), MessageType::Query);
|
|
}
|
|
}
|
|
sender_->SendMulticast(message);
|
|
return true;
|
|
}
|
|
|
|
void MdnsQuestionTracker::ScheduleFollowUpQuery() {
|
|
if (announcements_so_far_ >= maximum_announcement_count_) {
|
|
return;
|
|
}
|
|
announcements_so_far_++;
|
|
|
|
send_alarm_.ScheduleFromNow(
|
|
[this] {
|
|
if (SendQuery()) {
|
|
ScheduleFollowUpQuery();
|
|
}
|
|
},
|
|
send_delay_);
|
|
send_delay_ = send_delay_ * kIntervalIncreaseFactor;
|
|
if (send_delay_ > kMaximumQueryInterval) {
|
|
send_delay_ = kMaximumQueryInterval;
|
|
}
|
|
}
|
|
|
|
} // namespace discovery
|
|
} // namespace openscreen
|