| // 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_publisher.h" |
| |
| #include <chrono> |
| #include <cmath> |
| |
| #include "discovery/common/config.h" |
| #include "discovery/mdns/mdns_probe_manager.h" |
| #include "discovery/mdns/mdns_records.h" |
| #include "discovery/mdns/mdns_sender.h" |
| #include "platform/api/task_runner.h" |
| #include "platform/base/trivial_clock_traits.h" |
| |
| namespace openscreen { |
| namespace discovery { |
| namespace { |
| |
| // Minimum delay between announcements of a given record in seconds. |
| constexpr std::chrono::seconds kMinAnnounceDelay{1}; |
| |
| // Intervals between successive announcements must increase by at least a |
| // factor of 2. |
| constexpr int kIntervalIncreaseFactor = 2; |
| |
| // TTL for a goodbye record in seconds. This constant is called out in RFC 6762 |
| // section 10.1. |
| constexpr std::chrono::seconds kGoodbyeTtl{0}; |
| |
| // Timespan between sending batches of announcement and goodbye records, in |
| // microseconds. |
| constexpr Clock::duration kDelayBetweenBatchedRecords = |
| std::chrono::milliseconds(20); |
| |
| inline MdnsRecord CreateGoodbyeRecord(const MdnsRecord& record) { |
| if (record.ttl() == kGoodbyeTtl) { |
| return record; |
| } |
| return MdnsRecord(record.name(), record.dns_type(), record.dns_class(), |
| record.record_type(), kGoodbyeTtl, record.rdata()); |
| } |
| |
| } // namespace |
| |
| MdnsPublisher::MdnsPublisher(MdnsSender* sender, |
| MdnsProbeManager* ownership_manager, |
| TaskRunner* task_runner, |
| ClockNowFunctionPtr now_function, |
| const Config& config) |
| : sender_(sender), |
| ownership_manager_(ownership_manager), |
| task_runner_(task_runner), |
| now_function_(now_function), |
| max_announcement_attempts_(config.new_record_announcement_count) { |
| OSP_DCHECK(ownership_manager_); |
| OSP_DCHECK(sender_); |
| OSP_DCHECK(task_runner_); |
| OSP_DCHECK_GE(max_announcement_attempts_, 0); |
| } |
| |
| MdnsPublisher::~MdnsPublisher() { |
| if (batch_records_alarm_.has_value()) { |
| batch_records_alarm_.value().Cancel(); |
| ProcessRecordQueue(); |
| } |
| } |
| |
| Error MdnsPublisher::RegisterRecord(const MdnsRecord& record) { |
| OSP_DCHECK(task_runner_->IsRunningOnTaskRunner()); |
| OSP_DCHECK(record.dns_class() != DnsClass::kANY); |
| |
| if (!CanBePublished(record.dns_type())) { |
| return Error::Code::kParameterInvalid; |
| } |
| |
| if (!IsRecordNameClaimed(record)) { |
| return Error::Code::kParameterInvalid; |
| } |
| |
| const DomainName& name = record.name(); |
| auto it = records_.emplace(name, std::vector<RecordAnnouncerPtr>{}).first; |
| for (const RecordAnnouncerPtr& publisher : it->second) { |
| if (publisher->record() == record) { |
| return Error::Code::kItemAlreadyExists; |
| } |
| } |
| |
| OSP_DVLOG << "Registering record of type '" << record.dns_type() << "'"; |
| |
| it->second.push_back(CreateAnnouncer(record)); |
| |
| return Error::None(); |
| } |
| |
| Error MdnsPublisher::UnregisterRecord(const MdnsRecord& record) { |
| OSP_DCHECK(task_runner_->IsRunningOnTaskRunner()); |
| OSP_DCHECK(record.dns_class() != DnsClass::kANY); |
| |
| if (!CanBePublished(record.dns_type())) { |
| return Error::Code::kParameterInvalid; |
| } |
| |
| OSP_DVLOG << "Unregistering record of type '" << record.dns_type() << "'"; |
| |
| return RemoveRecord(record, true); |
| } |
| |
| Error MdnsPublisher::UpdateRegisteredRecord(const MdnsRecord& old_record, |
| const MdnsRecord& new_record) { |
| OSP_DCHECK(task_runner_->IsRunningOnTaskRunner()); |
| |
| if (!CanBePublished(new_record.dns_type())) { |
| return Error::Code::kParameterInvalid; |
| } |
| |
| if (old_record.dns_type() == DnsType::kPTR) { |
| return Error::Code::kParameterInvalid; |
| } |
| |
| // Check that the old record and new record are compatible. |
| if (old_record.name() != new_record.name() || |
| old_record.dns_type() != new_record.dns_type() || |
| old_record.dns_class() != new_record.dns_class() || |
| old_record.record_type() != new_record.record_type()) { |
| return Error::Code::kParameterInvalid; |
| } |
| |
| OSP_DVLOG << "Updating record of type '" << new_record.dns_type() << "'"; |
| |
| // Remove the old record. Per RFC 6762 section 8.4, a goodbye message will not |
| // be sent, as all records which can be removed here are unique records, which |
| // will be overwritten during the announcement phase when the updated record |
| // is re-registered due to the cache-flush-bit's presence. |
| const Error remove_result = RemoveRecord(old_record, false); |
| if (!remove_result.ok()) { |
| return remove_result; |
| } |
| |
| // Register the new record. |
| return RegisterRecord(new_record); |
| } |
| |
| size_t MdnsPublisher::GetRecordCount() const { |
| OSP_DCHECK(task_runner_->IsRunningOnTaskRunner()); |
| |
| size_t count = 0; |
| for (const auto& pair : records_) { |
| count += pair.second.size(); |
| } |
| |
| return count; |
| } |
| |
| bool MdnsPublisher::HasRecords(const DomainName& name, |
| DnsType type, |
| DnsClass clazz) { |
| return !GetRecords(name, type, clazz).empty(); |
| } |
| |
| std::vector<MdnsRecord::ConstRef> MdnsPublisher::GetRecords( |
| const DomainName& name, |
| DnsType type, |
| DnsClass clazz) { |
| OSP_DCHECK(task_runner_->IsRunningOnTaskRunner()); |
| |
| std::vector<MdnsRecord::ConstRef> records; |
| auto it = records_.find(name); |
| if (it != records_.end()) { |
| for (const RecordAnnouncerPtr& announcer : it->second) { |
| OSP_DCHECK(announcer.get()); |
| const DnsType record_dns_type = announcer->record().dns_type(); |
| const DnsClass record_dns_class = announcer->record().dns_class(); |
| if ((type == DnsType::kANY || type == record_dns_type) && |
| (clazz == DnsClass::kANY || clazz == record_dns_class)) { |
| records.push_back(announcer->record()); |
| } |
| } |
| } |
| |
| return records; |
| } |
| |
| std::vector<MdnsRecord::ConstRef> MdnsPublisher::GetPtrRecords(DnsClass clazz) { |
| std::vector<MdnsRecord::ConstRef> records; |
| |
| // There should be few records associated with any given domain name, so it is |
| // simpler and less error prone to iterate across all records than to check |
| // the domain name against format '[^.]+\.(_tcp)|(_udp)\..*'' |
| for (auto it = records_.begin(); it != records_.end(); it++) { |
| for (const RecordAnnouncerPtr& announcer : it->second) { |
| OSP_DCHECK(announcer.get()); |
| const DnsType record_dns_type = announcer->record().dns_type(); |
| if (record_dns_type != DnsType::kPTR) { |
| continue; |
| } |
| |
| const DnsClass record_dns_class = announcer->record().dns_class(); |
| if ((clazz == DnsClass::kANY || clazz == record_dns_class)) { |
| records.push_back(announcer->record()); |
| } |
| } |
| } |
| |
| return records; |
| } |
| |
| Error MdnsPublisher::RemoveRecord(const MdnsRecord& record, |
| bool should_announce_deletion) { |
| const DomainName& name = record.name(); |
| |
| // Check for the domain and fail if it's not found. |
| const auto it = records_.find(name); |
| if (it == records_.end()) { |
| return Error::Code::kItemNotFound; |
| } |
| |
| // Check for the record to be removed. |
| const auto records_it = |
| std::find_if(it->second.begin(), it->second.end(), |
| [&record](const RecordAnnouncerPtr& publisher) { |
| return publisher->record() == record; |
| }); |
| if (records_it == it->second.end()) { |
| return Error::Code::kItemNotFound; |
| } |
| if (!should_announce_deletion) { |
| (*records_it)->DisableGoodbyeMessageTransmission(); |
| } |
| |
| it->second.erase(records_it); |
| if (it->second.empty()) { |
| records_.erase(it); |
| } |
| |
| return Error::None(); |
| } |
| |
| bool MdnsPublisher::IsRecordNameClaimed(const MdnsRecord& record) const { |
| const DomainName& name = |
| record.dns_type() == DnsType::kPTR |
| ? absl::get<PtrRecordRdata>(record.rdata()).ptr_domain() |
| : record.name(); |
| return ownership_manager_->IsDomainClaimed(name); |
| } |
| |
| MdnsPublisher::RecordAnnouncer::RecordAnnouncer( |
| MdnsRecord record, |
| MdnsPublisher* publisher, |
| TaskRunner* task_runner, |
| ClockNowFunctionPtr now_function, |
| int target_announcement_attempts) |
| : publisher_(publisher), |
| task_runner_(task_runner), |
| now_function_(now_function), |
| record_(std::move(record)), |
| alarm_(now_function_, task_runner_), |
| target_announcement_attempts_(target_announcement_attempts) { |
| OSP_DCHECK(publisher_); |
| OSP_DCHECK(task_runner_); |
| OSP_DCHECK(record_.ttl() != Clock::duration::zero()); |
| |
| QueueAnnouncement(); |
| } |
| |
| MdnsPublisher::RecordAnnouncer::~RecordAnnouncer() { |
| alarm_.Cancel(); |
| if (should_send_goodbye_message_) { |
| QueueGoodbye(); |
| } |
| } |
| |
| void MdnsPublisher::RecordAnnouncer::QueueGoodbye() { |
| OSP_DCHECK(task_runner_->IsRunningOnTaskRunner()); |
| |
| publisher_->QueueRecord(CreateGoodbyeRecord(record_)); |
| } |
| |
| void MdnsPublisher::RecordAnnouncer::QueueAnnouncement() { |
| OSP_DCHECK(task_runner_->IsRunningOnTaskRunner()); |
| |
| if (attempts_ >= target_announcement_attempts_) { |
| return; |
| } |
| |
| publisher_->QueueRecord(record_); |
| |
| const Clock::duration new_delay = GetNextAnnounceDelay(); |
| attempts_++; |
| alarm_.ScheduleFromNow([this]() { QueueAnnouncement(); }, new_delay); |
| } |
| |
| void MdnsPublisher::QueueRecord(MdnsRecord record) { |
| if (!batch_records_alarm_.has_value()) { |
| OSP_DCHECK(records_to_send_.empty()); |
| batch_records_alarm_.emplace(now_function_, task_runner_); |
| batch_records_alarm_.value().ScheduleFromNow( |
| [this]() { ProcessRecordQueue(); }, kDelayBetweenBatchedRecords); |
| } |
| |
| // Check that we aren't announcing and goodbye'ing a record in the same batch. |
| // We expect to be sending no more than 5 records at a time, so don't worry |
| // about iterating across this vector for each insert. |
| auto goodbye = CreateGoodbyeRecord(record); |
| auto existing_record_it = |
| std::find_if(records_to_send_.begin(), records_to_send_.end(), |
| [&goodbye](const MdnsRecord& record) { |
| return goodbye == CreateGoodbyeRecord(record); |
| }); |
| |
| // If we didn't find it, simply add it to the queue. Else, only send the |
| // goodbye record. |
| if (existing_record_it == records_to_send_.end()) { |
| records_to_send_.push_back(std::move(record)); |
| } else if (*existing_record_it == goodbye) { |
| // This means that the goodbye record is already queued to be sent. This |
| // means that there is no reason to also announce it, so exit early. |
| return; |
| } else if (record == goodbye) { |
| // This means that we are sending a goodbye record right as it would also |
| // be announced. Skip the announcement since the record is being |
| // unregistered. |
| *existing_record_it = std::move(record); |
| } else if (record == *existing_record_it) { |
| // This case shouldn't happen, but there is no work to do if it does. Log |
| // to surface that something weird is going on. |
| OSP_LOG_INFO << "Same record being announced multiple times."; |
| } else { |
| // This case should never occur. Support it just in case, but log to |
| // surface that something weird is happening. |
| OSP_LOG_INFO << "Updating the same record multiple times with multiple " |
| "TTL values."; |
| } |
| } |
| |
| void MdnsPublisher::ProcessRecordQueue() { |
| OSP_DCHECK(task_runner_->IsRunningOnTaskRunner()); |
| |
| if (records_to_send_.empty()) { |
| return; |
| } |
| |
| MdnsMessage message(CreateMessageId(), MessageType::Response); |
| for (auto it = records_to_send_.begin(); it != records_to_send_.end();) { |
| if (message.CanAddRecord(*it)) { |
| message.AddAnswer(std::move(*it++)); |
| } else if (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 { |
| sender_->SendMulticast(message); |
| message = MdnsMessage(CreateMessageId(), MessageType::Response); |
| } |
| } |
| |
| if (!message.answers().empty()) { |
| sender_->SendMulticast(message); |
| } |
| |
| batch_records_alarm_ = absl::nullopt; |
| records_to_send_.clear(); |
| } |
| |
| Clock::duration MdnsPublisher::RecordAnnouncer::GetNextAnnounceDelay() { |
| return Clock::to_duration(kMinAnnounceDelay * |
| pow(kIntervalIncreaseFactor, attempts_)); |
| } |
| |
| } // namespace discovery |
| } // namespace openscreen |