| // 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_responder.h" |
| |
| #include <string> |
| #include <utility> |
| |
| #include "discovery/common/config.h" |
| #include "discovery/mdns/mdns_probe_manager.h" |
| #include "discovery/mdns/mdns_publisher.h" |
| #include "discovery/mdns/mdns_querier.h" |
| #include "discovery/mdns/mdns_random.h" |
| #include "discovery/mdns/mdns_receiver.h" |
| #include "discovery/mdns/mdns_sender.h" |
| #include "platform/api/task_runner.h" |
| |
| namespace openscreen { |
| namespace discovery { |
| namespace { |
| |
| const std::array<std::string, 3> kServiceEnumerationDomainLabels{ |
| "_services", "_dns-sd", "_udp"}; |
| |
| enum AddResult { kNonePresent = 0, kAdded, kAlreadyKnown }; |
| |
| std::chrono::seconds GetTtlForNsecTargetingType(DnsType type) { |
| // NOTE: A 'default' switch statement has intentionally been avoided below to |
| // enforce that new DnsTypes added must be added below through a compile-time |
| // check. |
| switch (type) { |
| case DnsType::kA: |
| return kARecordTtl; |
| case DnsType::kAAAA: |
| return kAAAARecordTtl; |
| case DnsType::kPTR: |
| return kPtrRecordTtl; |
| case DnsType::kSRV: |
| return kSrvRecordTtl; |
| case DnsType::kTXT: |
| return kTXTRecordTtl; |
| case DnsType::kANY: |
| // If no records are present, re-querying should happen at the minimum |
| // of any record that might be retrieved at that time. |
| return kSrvRecordTtl; |
| case DnsType::kNSEC: |
| case DnsType::kOPT: |
| // Neither of these types should ever be hit. We should never be creating |
| // an NSEC record for type NSEC, and OPT record querying is not supported, |
| // so creating NSEC records for type OPT is not valid. |
| break; |
| } |
| |
| OSP_NOTREACHED(); |
| } |
| |
| MdnsRecord CreateNsecRecord(DomainName target_name, |
| DnsType target_type, |
| DnsClass target_class) { |
| auto rdata = NsecRecordRdata(target_name, target_type); |
| std::chrono::seconds ttl = GetTtlForNsecTargetingType(target_type); |
| return MdnsRecord(std::move(target_name), DnsType::kNSEC, target_class, |
| RecordType::kUnique, ttl, std::move(rdata)); |
| } |
| |
| inline bool IsValidAdditionalRecordType(DnsType type) { |
| return type == DnsType::kSRV || type == DnsType::kTXT || |
| type == DnsType::kA || type == DnsType::kAAAA; |
| } |
| |
| AddResult AddRecords(std::function<void(MdnsRecord record)> add_func, |
| MdnsResponder::RecordHandler* record_handler, |
| const DomainName& domain, |
| const std::vector<MdnsRecord>& known_answers, |
| DnsType type, |
| DnsClass clazz, |
| bool add_negative_on_unknown) { |
| auto records = record_handler->GetRecords(domain, type, clazz); |
| if (records.empty()) { |
| if (add_negative_on_unknown) { |
| // TODO(rwkeane): Aggregate all NSEC records together into a single NSEC |
| // record to reduce traffic. |
| add_func(CreateNsecRecord(domain, type, clazz)); |
| } |
| return AddResult::kNonePresent; |
| } else { |
| bool added_any_records = false; |
| for (auto it = records.begin(); it != records.end(); it++) { |
| if (std::find(known_answers.begin(), known_answers.end(), *it) == |
| known_answers.end()) { |
| added_any_records = true; |
| add_func(std::move(*it)); |
| } |
| } |
| return added_any_records ? AddResult::kAdded : AddResult::kAlreadyKnown; |
| } |
| } |
| |
| inline AddResult AddAdditionalRecords( |
| MdnsMessage* message, |
| MdnsResponder::RecordHandler* record_handler, |
| const DomainName& domain, |
| const std::vector<MdnsRecord>& known_answers, |
| DnsType type, |
| DnsClass clazz, |
| bool add_negative_on_unknown) { |
| OSP_DCHECK(IsValidAdditionalRecordType(type)); |
| |
| auto add_func = [message](MdnsRecord record) { |
| message->AddAdditionalRecord(std::move(record)); |
| }; |
| return AddRecords(std::move(add_func), record_handler, domain, known_answers, |
| type, clazz, add_negative_on_unknown); |
| } |
| |
| inline AddResult AddResponseRecords( |
| MdnsMessage* message, |
| MdnsResponder::RecordHandler* record_handler, |
| const DomainName& domain, |
| const std::vector<MdnsRecord>& known_answers, |
| DnsType type, |
| DnsClass clazz, |
| bool add_negative_on_unknown) { |
| auto add_func = [message](MdnsRecord record) { |
| message->AddAnswer(std::move(record)); |
| }; |
| return AddRecords(std::move(add_func), record_handler, domain, known_answers, |
| type, clazz, add_negative_on_unknown); |
| } |
| |
| void ApplyQueryResults(MdnsMessage* message, |
| MdnsResponder::RecordHandler* record_handler, |
| const DomainName& domain, |
| const std::vector<MdnsRecord>& known_answers, |
| DnsType type, |
| DnsClass clazz, |
| bool is_exclusive_owner) { |
| OSP_DCHECK(type != DnsType::kNSEC); |
| |
| // All records matching the provided query which have been published by this |
| // host should be added to the response message per RFC 6762 section 6. If |
| // this host is the exclusive owner of the queried domain name, then a |
| // negative response NSEC record should be added in the case where the queried |
| // record does not exist, per RFC 6762 section 6.1. |
| if (AddResponseRecords(message, record_handler, domain, known_answers, type, |
| clazz, is_exclusive_owner) != AddResult::kAdded) { |
| return; |
| } |
| |
| // Per RFC 6763 section 12.1, when querying for a PTR record, all SRV records |
| // and TXT records named in the PTR record's rdata should be added to the |
| // messages additional records, as well as the address records of types A and |
| // AAAA associated with the added SRV records. Per RFC 6762 section 6.1, |
| // records with names matching those of reverse address mappings for PTR |
| // records may be added as negative response NSEC records if they do not |
| // exist. |
| if (type == DnsType::kPTR) { |
| // Add all SRV and TXT records to the additional records section. |
| for (const MdnsRecord& record : message->answers()) { |
| OSP_DCHECK(record.dns_type() == DnsType::kPTR); |
| |
| const DomainName& target = |
| absl::get<PtrRecordRdata>(record.rdata()).ptr_domain(); |
| AddAdditionalRecords(message, record_handler, target, known_answers, |
| DnsType::kSRV, clazz, true); |
| AddAdditionalRecords(message, record_handler, target, known_answers, |
| DnsType::kTXT, clazz, true); |
| } |
| |
| // Add A and AAAA records associated with an added SRV record to the |
| // additional records section. |
| const int max = message->additional_records().size(); |
| for (int i = 0; i < max; i++) { |
| if (message->additional_records()[i].dns_type() != DnsType::kSRV) { |
| continue; |
| } |
| |
| { |
| const MdnsRecord& srv_record = message->additional_records()[i]; |
| const DomainName& target = |
| absl::get<SrvRecordRdata>(srv_record.rdata()).target(); |
| AddAdditionalRecords(message, record_handler, target, known_answers, |
| DnsType::kA, clazz, target == domain); |
| } |
| |
| // Must re-calculate the |srv_record|, |target| refs in case a resize of |
| // the additional_records() vector has invalidated them. |
| { |
| const MdnsRecord& srv_record = message->additional_records()[i]; |
| const DomainName& target = |
| absl::get<SrvRecordRdata>(srv_record.rdata()).target(); |
| AddAdditionalRecords(message, record_handler, target, known_answers, |
| DnsType::kAAAA, clazz, target == domain); |
| } |
| } |
| } else if (type == DnsType::kSRV) { |
| // Per RFC 6763 section 12.2, when querying for an SRV record, all address |
| // records of type A and AAAA should be added to the additional records |
| // section. Per RFC 6762 section 6.1, if these records are not present and |
| // their name and class match that which is being queried for, a negative |
| // response NSEC record may be added to show their non-existence. |
| for (const auto& srv_record : message->answers()) { |
| OSP_DCHECK(srv_record.dns_type() == DnsType::kSRV); |
| |
| const DomainName& target = |
| absl::get<SrvRecordRdata>(srv_record.rdata()).target(); |
| AddAdditionalRecords(message, record_handler, target, known_answers, |
| DnsType::kA, clazz, target == domain); |
| AddAdditionalRecords(message, record_handler, target, known_answers, |
| DnsType::kAAAA, clazz, target == domain); |
| } |
| } else if (type == DnsType::kA) { |
| // Per RFC 6762 section 6.2, when querying for an address record of type A |
| // or AAAA, the record of the opposite type should be added to the |
| // additional records section if present. Else, a negative response NSEC |
| // record should be added to show its non-existence. |
| AddAdditionalRecords(message, record_handler, domain, known_answers, |
| DnsType::kAAAA, clazz, true); |
| } else if (type == DnsType::kAAAA) { |
| AddAdditionalRecords(message, record_handler, domain, known_answers, |
| DnsType::kA, clazz, true); |
| } |
| |
| // The remaining supported records types are TXT, NSEC, and ANY. RFCs 6762 and |
| // 6763 do not recommend sending any records in the additional records section |
| // for queries of types TXT or ANY, and NSEC records are not supported for |
| // queries. |
| } |
| |
| // Determines if the provided query is a type enumeration query as described in |
| // RFC 6763 section 9. |
| bool IsServiceTypeEnumerationQuery(const MdnsQuestion& question) { |
| if (question.dns_type() != DnsType::kPTR) { |
| return false; |
| } |
| |
| if (question.name().labels().size() < |
| kServiceEnumerationDomainLabels.size()) { |
| return false; |
| } |
| |
| const auto question_it = question.name().labels().begin(); |
| return std::equal(question_it, |
| question_it + kServiceEnumerationDomainLabels.size(), |
| kServiceEnumerationDomainLabels.begin(), |
| kServiceEnumerationDomainLabels.end()); |
| } |
| |
| // Creates the expected response to a type enumeration query as described in RFC |
| // 6763 section 9. |
| void ApplyServiceTypeEnumerationResults( |
| MdnsMessage* message, |
| MdnsResponder::RecordHandler* record_handler, |
| const DomainName& name, |
| DnsClass clazz) { |
| if (name.labels().size() < kServiceEnumerationDomainLabels.size()) { |
| return; |
| } |
| |
| std::vector<MdnsRecord::ConstRef> records = |
| record_handler->GetPtrRecords(clazz); |
| |
| // skip "_services._dns-sd._udp." which was already checked for in above |
| // method and just use the domain. |
| const auto domain_it = |
| name.labels().begin() + kServiceEnumerationDomainLabels.size(); |
| for (const MdnsRecord& record : records) { |
| // Skip the 2 label service name in the PTR record's name. |
| const auto record_it = record.name().labels().begin() + 2; |
| if (std::equal(domain_it, name.labels().end(), record_it, |
| record.name().labels().end())) { |
| message->AddAnswer(MdnsRecord(name, DnsType::kPTR, record.dns_class(), |
| RecordType::kShared, record.ttl(), |
| PtrRecordRdata(record.name()))); |
| } |
| } |
| } |
| |
| bool IsMultiPacketTruncatedQueryMessage(const MdnsMessage& message) { |
| return message.is_truncated() || message.questions().empty(); |
| } |
| |
| } // namespace |
| |
| MdnsResponder::RecordHandler::~RecordHandler() = default; |
| |
| MdnsResponder::TruncatedQuery::TruncatedQuery(MdnsResponder* responder, |
| TaskRunner* task_runner, |
| ClockNowFunctionPtr now_function, |
| IPEndpoint src, |
| const MdnsMessage& message, |
| const Config& config) |
| : max_allowed_messages_(config.maximum_truncated_messages_per_query), |
| max_allowed_records_(config.maximum_known_answer_records_per_query), |
| src_(std::move(src)), |
| responder_(responder), |
| questions_(message.questions()), |
| known_answers_(message.answers()), |
| alarm_(now_function, task_runner) { |
| OSP_DCHECK(responder_); |
| OSP_DCHECK_GT(max_allowed_messages_, 0); |
| OSP_DCHECK_GT(max_allowed_records_, 0); |
| |
| RescheduleSend(); |
| } |
| |
| void MdnsResponder::TruncatedQuery::SetQuery(const MdnsMessage& message) { |
| OSP_DCHECK(questions_.empty()); |
| questions_.insert(questions_.end(), message.questions().begin(), |
| message.questions().end()); |
| |
| // |messages_received_so_far| does not need to be validated here because it is |
| // checked as part of RescheduleSend(). |
| known_answers_.insert(known_answers_.end(), message.answers().begin(), |
| message.answers().end()); |
| messages_received_so_far++; |
| |
| RescheduleSend(); |
| } |
| |
| void MdnsResponder::TruncatedQuery::AddKnownAnswers( |
| const std::vector<MdnsRecord>& records) { |
| // |messages_received_so_far| does not need to be validated here because it is |
| // checked as part of RescheduleSend(). |
| known_answers_.insert(known_answers_.end(), records.begin(), records.end()); |
| messages_received_so_far++; |
| |
| RescheduleSend(); |
| } |
| |
| void MdnsResponder::TruncatedQuery::RescheduleSend() { |
| alarm_.Cancel(); |
| |
| Clock::duration send_delay; |
| if (messages_received_so_far >= max_allowed_messages_) { |
| // Maximum number of truncated messages have already been received for this |
| // query. |
| send_delay = Clock::duration(0); |
| } else if (known_answers_.size() >= |
| static_cast<size_t>(max_allowed_records_)) { |
| // Maximum number of known answer records have already been received for |
| // this query. |
| send_delay = Clock::duration(0); |
| } else { |
| // Reschedule to send after a random delay, per RFC 6762. |
| send_delay = responder_->random_delay_->GetTruncatedQueryResponseDelay(); |
| } |
| |
| alarm_.ScheduleFromNow([this]() { SendResponse(); }, send_delay); |
| } |
| |
| void MdnsResponder::TruncatedQuery::SendResponse() { |
| alarm_.Cancel(); |
| |
| if (questions_.empty()) { |
| OSP_DVLOG << "Known answers received for unknown query, and non received " |
| "after delay. Dropping them..."; |
| return; |
| } |
| |
| responder_->RespondToTruncatedQuery(this); |
| } |
| |
| MdnsResponder::MdnsResponder(RecordHandler* record_handler, |
| MdnsProbeManager* ownership_handler, |
| MdnsSender* sender, |
| MdnsReceiver* receiver, |
| TaskRunner* task_runner, |
| ClockNowFunctionPtr now_function, |
| MdnsRandom* random_delay, |
| const Config& config) |
| : record_handler_(record_handler), |
| ownership_handler_(ownership_handler), |
| sender_(sender), |
| receiver_(receiver), |
| task_runner_(task_runner), |
| now_function_(now_function), |
| random_delay_(random_delay), |
| config_(config) { |
| OSP_DCHECK(record_handler_); |
| OSP_DCHECK(ownership_handler_); |
| OSP_DCHECK(sender_); |
| OSP_DCHECK(receiver_); |
| OSP_DCHECK(task_runner_); |
| OSP_DCHECK(random_delay_); |
| OSP_DCHECK_GT(config_.maximum_truncated_messages_per_query, 0); |
| OSP_DCHECK_GT(config_.maximum_concurrent_truncated_queries_per_interface, 0); |
| |
| auto func = [this](const MdnsMessage& message, const IPEndpoint& src) { |
| OnMessageReceived(message, src); |
| }; |
| receiver_->SetQueryCallback(std::move(func)); |
| } |
| |
| MdnsResponder::~MdnsResponder() { |
| receiver_->SetQueryCallback(nullptr); |
| } |
| |
| void MdnsResponder::OnMessageReceived(const MdnsMessage& message, |
| const IPEndpoint& src) { |
| OSP_DCHECK(task_runner_->IsRunningOnTaskRunner()); |
| OSP_DCHECK(message.type() == MessageType::Query); |
| |
| // Handle multi-packet known answer suppression. |
| if (IsMultiPacketTruncatedQueryMessage(message)) { |
| // If there have been an excessive number of known answers received already, |
| // then skip them. This would most likely mean that: |
| // - A host on the network is misbehaving. |
| // - There is a malicious actor on the network. |
| // In either of these cases, optimize for this host's resource usage. |
| if (truncated_queries_.size() > |
| static_cast<size_t>( |
| config_.maximum_concurrent_truncated_queries_per_interface)) { |
| OSP_DVLOG << "Too many truncated queries have been received. Treating " |
| "new multi-packet known answer message as normal query"; |
| } else { |
| ProcessMultiPacketTruncatedMessage(message, src); |
| return; |
| } |
| } |
| |
| // If the query is a probe query, it will be handled separately by the |
| // MdnsProbeManager. Ignore it here. |
| if (message.IsProbeQuery()) { |
| ownership_handler_->RespondToProbeQuery(message, src); |
| return; |
| } |
| |
| // Else, this is a normal query. Process it as such. |
| // This is the case that should be hit 95+% of the time. |
| OSP_DVLOG << "Received mDNS Query with " << message.questions().size() |
| << " questions. Processing..."; |
| const std::vector<MdnsRecord>& known_answers = message.answers(); |
| const std::vector<MdnsQuestion>& questions = message.questions(); |
| ProcessQueries(src, questions, known_answers); |
| } |
| |
| void MdnsResponder::ProcessMultiPacketTruncatedMessage( |
| const MdnsMessage& message, |
| const IPEndpoint& src) { |
| OSP_DVLOG << "Multi-packet truncated message received. Processing..."; |
| |
| const bool message_has_question = !message.questions().empty(); |
| const bool message_is_truncated = message.is_truncated(); |
| OSP_DCHECK(!message_has_question || message_is_truncated); |
| |
| auto pair = |
| truncated_queries_.emplace(src, std::unique_ptr<TruncatedQuery>()); |
| std::unique_ptr<TruncatedQuery>& stored_query = pair.first->second; |
| |
| // First, handle the case where this host doesn't have a known answer query |
| // tracked yet. In this case, start tracking the new query. |
| if (pair.second) { |
| // Create a new query and swap it with the old one to save an extra lookup. |
| auto new_query = std::make_unique<TruncatedQuery>( |
| this, task_runner_, now_function_, src, message, config_); |
| stored_query.swap(new_query); |
| return; |
| } |
| |
| // Else, there was already a message received from this host. |
| const bool are_questions_already_stored = !stored_query->questions().empty(); |
| |
| // If the new message doesn't have a question, then it must be additional |
| // known answers. Add them to the set of known answers for this truncated |
| // query. |
| if (!message_has_question) { |
| stored_query->AddKnownAnswers(message.answers()); |
| return; |
| } |
| |
| // Alternatively, if a record for this host existed, it might be because the |
| // messages were received out-of-order and known answers have already been |
| // received. In this case, associate the new message's query with the known |
| // answers already received. |
| if (!are_questions_already_stored) { |
| stored_query->SetQuery(message); |
| return; |
| } |
| |
| // Else, an ongoing truncated query is already associated with this host and a |
| // new one has also been received. This implies one of the following occurred: |
| // - The sender must have finished sending packets. |
| // - The known answers completing this query somehow got lost on the network. |
| // - A second truncated query was started by the same host, and this host |
| // won't be able to differentiate which query future known answers are |
| // associated with. |
| // In any of these cases, there's no reason to continue tracking the old |
| // query. So process it. |
| // |
| // Create a new query and swap it with the old one to save an extra lookup. |
| auto new_query = std::make_unique<TruncatedQuery>( |
| this, task_runner_, now_function_, src, message, config_); |
| stored_query.swap(new_query); |
| |
| // Now that the pointers have been swapped, process the previously stored |
| // query. |
| new_query->SendResponse(); |
| } |
| |
| void MdnsResponder::RespondToTruncatedQuery(TruncatedQuery* query) { |
| ProcessQueries(query->src(), query->questions(), query->known_answers()); |
| auto it = truncated_queries_.find(query->src()); |
| |
| if (it == truncated_queries_.end()) { |
| return; |
| } |
| |
| // If a second query for this same host arrives, then the question found may |
| // not match what is being sent due to the swap done in OnMessageReceived(). |
| if (it->second.get() == query) { |
| truncated_queries_.erase(it); |
| } |
| } |
| |
| void MdnsResponder::ProcessQueries( |
| const IPEndpoint& src, |
| const std::vector<MdnsQuestion>& questions, |
| const std::vector<MdnsRecord>& known_answers) { |
| for (const auto& question : questions) { |
| OSP_DVLOG << "\tProcessing mDNS Query for domain: '" |
| << question.name().ToString() << "', type: '" |
| << question.dns_type() << "' from '" << src << "'"; |
| |
| // NSEC records should not be queried for. |
| if (question.dns_type() == DnsType::kNSEC) { |
| continue; |
| } |
| |
| // Only respond to queries for which one of the following is true: |
| // - This host is the sole owner of that domain. |
| // - A record corresponding to this question has been published. |
| // - The query is a service enumeration query. |
| const bool is_service_enumeration = IsServiceTypeEnumerationQuery(question); |
| const bool is_exclusive_owner = |
| ownership_handler_->IsDomainClaimed(question.name()); |
| if (!is_service_enumeration && !is_exclusive_owner && |
| !record_handler_->HasRecords(question.name(), question.dns_type(), |
| question.dns_class())) { |
| OSP_DVLOG << "\tmDNS Query processed and no relevant records found!"; |
| continue; |
| } else if (is_service_enumeration) { |
| OSP_DVLOG << "\tmDNS Query is for service type enumeration!"; |
| } |
| |
| // Relevant records are published, so send them out using the response type |
| // dictated in the question. |
| std::function<void(const MdnsMessage&)> send_response; |
| if (question.response_type() == ResponseType::kMulticast) { |
| send_response = [this](const MdnsMessage& message) { |
| sender_->SendMulticast(message); |
| }; |
| } else { |
| OSP_DCHECK(question.response_type() == ResponseType::kUnicast); |
| send_response = [this, src](const MdnsMessage& message) { |
| sender_->SendMessage(message, src); |
| }; |
| } |
| |
| // If this host is the exclusive owner, respond immediately. Else, there may |
| // be network contention if all hosts respond simultaneously, so delay the |
| // response as dictated by RFC 6762. |
| if (is_exclusive_owner) { |
| SendResponse(question, known_answers, send_response, is_exclusive_owner); |
| } else { |
| const auto delay = random_delay_->GetSharedRecordResponseDelay(); |
| std::function<void()> response = [this, question, known_answers, |
| send_response, is_exclusive_owner]() { |
| SendResponse(question, known_answers, send_response, |
| is_exclusive_owner); |
| }; |
| task_runner_->PostTaskWithDelay(response, delay); |
| } |
| } |
| } |
| |
| void MdnsResponder::SendResponse( |
| const MdnsQuestion& question, |
| const std::vector<MdnsRecord>& known_answers, |
| std::function<void(const MdnsMessage&)> send_response, |
| bool is_exclusive_owner) { |
| OSP_DCHECK(task_runner_->IsRunningOnTaskRunner()); |
| |
| MdnsMessage message(CreateMessageId(), MessageType::Response); |
| |
| if (IsServiceTypeEnumerationQuery(question)) { |
| // This is a special case defined in RFC 6763 section 9, so handle it |
| // separately. |
| ApplyServiceTypeEnumerationResults(&message, record_handler_, |
| question.name(), question.dns_class()); |
| } else { |
| // NOTE: The exclusive ownership of this record cannot change before this |
| // method is called. Exclusive ownership cannot be gained for a record which |
| // has previously been published, and if this host is the exclusive owner |
| // then this method will have been called without any delay on the task |
| // runner. |
| ApplyQueryResults(&message, record_handler_, question.name(), known_answers, |
| question.dns_type(), question.dns_class(), |
| is_exclusive_owner); |
| } |
| |
| // Send the response only if it contains answers to the query. |
| OSP_DVLOG << "\tCompleted Processing mDNS Query for domain: '" |
| << question.name().ToString() << "', type: '" << question.dns_type() |
| << "', with " << message.answers().size() << " results:"; |
| for (const auto& record : message.answers()) { |
| OSP_DVLOG << "\t\tanswer (" << record.ToString() << ")"; |
| } |
| for (const auto& record : message.additional_records()) { |
| OSP_DVLOG << "\t\tadditional record ('" << record.ToString() << ")"; |
| } |
| |
| if (!message.answers().empty()) { |
| send_response(message); |
| } |
| } |
| |
| } // namespace discovery |
| } // namespace openscreen |