// 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 #include #include #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(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::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 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 MdnsQuestionTracker::GetRecords() const { std::vector 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 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(*it); if (record_tracker->IsNearingExpiry()) { it++; continue; } // A record tracker should only contain one record. std::vector 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