blob: 282e5f5a209cc198394cedd7dac33373eeb10853 [file] [log] [blame]
// Copyright 2017 The Fuchsia 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 <limits>
#include <unordered_set>
#include "garnet/bin/netconnector/mdns/mdns.h"
#include "garnet/bin/netconnector/mdns/address_prober.h"
#include "garnet/bin/netconnector/mdns/address_responder.h"
#include "garnet/bin/netconnector/mdns/dns_formatting.h"
#include "garnet/bin/netconnector/mdns/host_name_resolver.h"
#include "garnet/bin/netconnector/mdns/instance_prober.h"
#include "garnet/bin/netconnector/mdns/instance_responder.h"
#include "garnet/bin/netconnector/mdns/instance_subscriber.h"
#include "garnet/bin/netconnector/mdns/mdns_addresses.h"
#include "garnet/bin/netconnector/mdns/mdns_names.h"
#include "garnet/bin/netconnector/mdns/resource_renewer.h"
#include "lib/fsl/tasks/message_loop.h"
#include "lib/fxl/logging.h"
#include "lib/fxl/time/time_delta.h"
namespace netconnector {
namespace mdns {
Mdns::Mdns() : task_runner_(fsl::MessageLoop::GetCurrent()->task_runner()) {}
Mdns::~Mdns() {}
void Mdns::EnableInterface(const std::string& name, sa_family_t family) {
transceiver_.EnableInterface(name, family);
}
void Mdns::SetVerbose(bool verbose) {
verbose_ = verbose;
}
void Mdns::Start(const std::string& host_name, const fxl::Closure& callback) {
FXL_DCHECK(!host_name.empty());
start_callback_ = callback;
original_host_name_ = host_name;
// Create a resource renewer agent to keep resources alive.
resource_renewer_ = std::make_shared<ResourceRenewer>(this);
// Create an address responder agent to respond to address queries.
AddAgent(std::make_shared<AddressResponder>(this));
transceiver_.Start(
[this]() {
// TODO(dalesat): Link changes that create host name conflicts.
// Once we have a NIC and we've decided on a unique host name, we
// don't do any more address probes. This means that we could have link
// changes that cause two hosts with the same name to be on the same
// subnet. To improve matters, we need to be prepared to change a host
// name we've been using for awhile.
if (!started_ && transceiver_.has_interfaces()) {
StartAddressProbe(original_host_name_);
}
},
[this](std::unique_ptr<DnsMessage> message,
const ReplyAddress& reply_address) {
if (verbose_) {
FXL_LOG(INFO) << "Inbound message from " << reply_address << ":"
<< *message;
}
for (auto& question : message->questions_) {
// We reply to questions using unicast if specifically requested in
// the question or if the sender's port isn't 5353.
ReceiveQuestion(*question, (question->unicast_response_ ||
reply_address.socket_address().port() !=
MdnsAddresses::kMdnsPort)
? reply_address
: MdnsAddresses::kV4MulticastReply);
}
for (auto& resource : message->answers_) {
ReceiveResource(*resource, MdnsResourceSection::kAnswer);
}
for (auto& resource : message->authorities_) {
ReceiveResource(*resource, MdnsResourceSection::kAuthority);
}
for (auto& resource : message->additionals_) {
ReceiveResource(*resource, MdnsResourceSection::kAdditional);
}
resource_renewer_->EndOfMessage();
DPROHIBIT_AGENT_REMOVAL();
for (auto& pair : agents_) {
pair.second->EndOfMessage();
}
DALLOW_AGENT_REMOVAL();
SendMessages();
});
if (transceiver_.has_interfaces()) {
StartAddressProbe(original_host_name_);
}
}
void Mdns::Stop() {
transceiver_.Stop();
started_ = false;
}
void Mdns::ResolveHostName(const std::string& host_name,
fxl::TimePoint timeout,
const ResolveHostNameCallback& callback) {
FXL_DCHECK(MdnsNames::IsValidHostName(host_name));
FXL_DCHECK(callback);
AddAgent(
std::make_shared<HostNameResolver>(this, host_name, timeout, callback));
}
std::shared_ptr<MdnsAgent> Mdns::SubscribeToService(
const std::string& service_name,
const ServiceInstanceCallback& callback) {
FXL_DCHECK(MdnsNames::IsValidServiceName(service_name));
FXL_DCHECK(callback);
auto agent =
std::make_shared<InstanceSubscriber>(this, service_name, callback);
AddAgent(agent);
return agent;
}
bool Mdns::PublishServiceInstance(const std::string& service_name,
const std::string& instance_name,
IpPort port,
const std::vector<std::string>& text,
const PublishCallback& callback) {
MdnsPublicationPtr publication = MdnsPublication::New();
publication->port = port.as_uint16_t();
publication->text = fidl::Array<fidl::String>::From(text);
auto agent = std::make_shared<InstanceResponder>(
this, service_name, instance_name, std::move(publication), callback);
return ProbeAndAddInstanceResponder(service_name, instance_name, port, agent);
}
bool Mdns::UnpublishServiceInstance(const std::string& service_name,
const std::string& instance_name) {
FXL_DCHECK(MdnsNames::IsValidServiceName(service_name));
FXL_DCHECK(MdnsNames::IsValidInstanceName(instance_name));
std::string instance_full_name =
MdnsNames::LocalInstanceFullName(instance_name, service_name);
auto iter =
instance_publishers_by_instance_full_name_.find(instance_full_name);
if (iter == instance_publishers_by_instance_full_name_.end()) {
return false;
}
iter->second->Quit();
return true;
}
bool Mdns::AddResponder(const std::string& service_name,
const std::string& instance_name,
fidl::InterfaceHandle<MdnsResponder> responder) {
auto agent = std::make_shared<InstanceResponder>(
this, service_name, instance_name, std::move(responder));
// We're using a bogus port number here, which is OK, because the 'proposed'
// resource created from it is only used for collision resolution.
return ProbeAndAddInstanceResponder(service_name, instance_name,
IpPort::From_uint16_t(0), agent);
}
bool Mdns::SetSubtypes(const std::string& service_name,
const std::string& instance_name,
std::vector<std::string> subtypes) {
FXL_DCHECK(MdnsNames::IsValidServiceName(service_name));
FXL_DCHECK(MdnsNames::IsValidInstanceName(instance_name));
std::string instance_full_name =
MdnsNames::LocalInstanceFullName(instance_name, service_name);
auto iter =
instance_publishers_by_instance_full_name_.find(instance_full_name);
if (iter == instance_publishers_by_instance_full_name_.end()) {
return false;
}
iter->second->SetSubtypes(std::move(subtypes));
SendMessages();
return true;
}
bool Mdns::ReannounceInstance(const std::string& service_name,
const std::string& instance_name) {
FXL_DCHECK(MdnsNames::IsValidServiceName(service_name));
FXL_DCHECK(MdnsNames::IsValidInstanceName(instance_name));
std::string instance_full_name =
MdnsNames::LocalInstanceFullName(instance_name, service_name);
auto iter =
instance_publishers_by_instance_full_name_.find(instance_full_name);
if (iter == instance_publishers_by_instance_full_name_.end()) {
return false;
}
iter->second->Reannounce();
SendMessages();
return true;
}
void Mdns::StartAddressProbe(const std::string& host_name) {
host_name_ = host_name;
host_full_name_ = MdnsNames::LocalHostFullName(host_name);
FXL_LOG(INFO) << "Verifying uniqueness of host name " << host_full_name_;
transceiver_.SetHostFullName(host_full_name_);
address_placeholder_ =
std::make_shared<DnsResource>(host_full_name_, DnsType::kA);
// Create an address prober to look for host name conflicts. The address
// prober removes itself immediately before it calls the callback.
auto address_prober =
std::make_shared<AddressProber>(this, [this](bool successful) {
FXL_DCHECK(agents_.empty());
if (!successful) {
FXL_LOG(WARNING) << "Another host is using name " << host_full_name_;
OnHostNameConflict();
return;
}
FXL_LOG(INFO) << "Using unique host name " << host_full_name_;
// Start all the agents.
started_ = true;
// |resource_renewer_| doesn't need to be started, but we do it
// anyway in case that changes.
resource_renewer_->Start(host_full_name_);
for (auto agent : agents_awaiting_start_) {
AddAgent(agent);
}
agents_awaiting_start_.clear();
if (start_callback_) {
start_callback_();
start_callback_ = nullptr;
}
});
// We don't use |AddAgent| here, because agents added that way don't actually
// participate until we're done probing for host name conflicts.
agents_.emplace(address_prober.get(), address_prober);
address_prober->Start(host_full_name_);
SendMessages();
}
void Mdns::OnHostNameConflict() {
// TODO(dalesat): Support other renaming strategies?
std::ostringstream os;
os << original_host_name_ << next_host_name_deduplicator_;
++next_host_name_deduplicator_;
StartAddressProbe(os.str());
}
void Mdns::PostTaskForTime(MdnsAgent* agent,
fxl::Closure task,
fxl::TimePoint target_time) {
task_queue_.emplace(agent, task, target_time);
PostTask();
}
void Mdns::SendQuestion(std::shared_ptr<DnsQuestion> question) {
FXL_DCHECK(question);
DnsMessage& message =
outbound_messages_by_reply_address_[MdnsAddresses::kV4MulticastReply];
message.questions_.push_back(question);
}
void Mdns::SendResource(std::shared_ptr<DnsResource> resource,
MdnsResourceSection section,
const ReplyAddress& reply_address) {
FXL_DCHECK(resource);
if (section == MdnsResourceSection::kExpired) {
// Expirations are distributed to local agents. We handle this case
// separately so we don't create an empty outbound message.
prohibit_agent_removal_ = true;
for (auto& pair : agents_) {
pair.second->ReceiveResource(*resource, MdnsResourceSection::kExpired);
}
prohibit_agent_removal_ = false;
return;
}
DnsMessage& message = outbound_messages_by_reply_address_[reply_address];
switch (section) {
case MdnsResourceSection::kAnswer:
message.answers_.push_back(resource);
break;
case MdnsResourceSection::kAuthority:
message.authorities_.push_back(resource);
break;
case MdnsResourceSection::kAdditional:
message.additionals_.push_back(resource);
break;
case MdnsResourceSection::kExpired:
FXL_DCHECK(false);
break;
}
}
void Mdns::SendAddresses(MdnsResourceSection section,
const ReplyAddress& reply_address) {
SendResource(address_placeholder_, section, reply_address);
}
void Mdns::Renew(const DnsResource& resource) {
resource_renewer_->Renew(resource);
}
void Mdns::RemoveAgent(const MdnsAgent* agent,
const std::string& published_instance_full_name) {
FXL_DCHECK(agent);
FXL_DCHECK(!prohibit_agent_removal_);
agents_.erase(agent);
// Remove all pending tasks posted by this agent.
std::priority_queue<TaskQueueEntry> temp;
task_queue_.swap(temp);
while (!temp.empty()) {
if (temp.top().agent_ != agent) {
task_queue_.emplace(temp.top().agent_, temp.top().task_,
temp.top().time_);
}
temp.pop();
}
if (!published_instance_full_name.empty()) {
instance_probers_by_instance_full_name_.erase(published_instance_full_name);
instance_publishers_by_instance_full_name_.erase(
published_instance_full_name);
}
// In case the agent sent an epitaph.
SendMessages();
}
void Mdns::AddAgent(std::shared_ptr<MdnsAgent> agent) {
if (started_) {
agents_.emplace(agent.get(), agent);
FXL_DCHECK(!host_full_name_.empty());
agent->Start(host_full_name_);
SendMessages();
} else {
agents_awaiting_start_.push_back(agent);
}
}
bool Mdns::ProbeAndAddInstanceResponder(
const std::string& service_name,
const std::string& instance_name,
IpPort port,
std::shared_ptr<InstanceResponder> agent) {
FXL_DCHECK(MdnsNames::IsValidServiceName(service_name));
FXL_DCHECK(MdnsNames::IsValidInstanceName(instance_name));
std::string instance_full_name =
MdnsNames::LocalInstanceFullName(instance_name, service_name);
if (instance_probers_by_instance_full_name_.find(instance_full_name) !=
instance_probers_by_instance_full_name_.end() ||
instance_publishers_by_instance_full_name_.find(instance_full_name) !=
instance_publishers_by_instance_full_name_.end()) {
agent->UpdateStatus(MdnsResult::ALREADY_PUBLISHED_LOCALLY);
return false;
}
auto prober = std::make_shared<InstanceProber>(
this, service_name, instance_name, port,
[this, instance_full_name, agent](bool successful) {
if (!successful) {
agent->UpdateStatus(MdnsResult::ALREADY_PUBLISHED_ON_SUBNET);
return;
}
agent->UpdateStatus(MdnsResult::OK);
AddAgent(agent);
instance_publishers_by_instance_full_name_.emplace(instance_full_name,
agent);
});
AddAgent(prober);
instance_probers_by_instance_full_name_.emplace(instance_full_name, prober);
return true;
}
void Mdns::SendMessages() {
for (auto& pair : outbound_messages_by_reply_address_) {
const ReplyAddress& reply_address = pair.first;
DnsMessage& message = pair.second;
message.UpdateCounts();
if (message.questions_.empty()) {
message.header_.SetResponse(true);
message.header_.SetAuthoritativeAnswer(true);
}
if (verbose_) {
if (reply_address == MdnsAddresses::kV4MulticastReply) {
FXL_LOG(INFO) << "Outbound message (multicast): " << message;
} else {
FXL_LOG(INFO) << "Outbound message to " << reply_address << ":"
<< message;
}
}
transceiver_.SendMessage(&message, reply_address);
}
outbound_messages_by_reply_address_.clear();
}
void Mdns::ReceiveQuestion(const DnsQuestion& question,
const ReplyAddress& reply_address) {
// Renewer doesn't need questions.
DPROHIBIT_AGENT_REMOVAL();
for (auto& pair : agents_) {
pair.second->ReceiveQuestion(question, reply_address);
}
DALLOW_AGENT_REMOVAL();
}
void Mdns::ReceiveResource(const DnsResource& resource,
MdnsResourceSection section) {
// Renewer is always first.
resource_renewer_->ReceiveResource(resource, section);
DPROHIBIT_AGENT_REMOVAL();
for (auto& pair : agents_) {
pair.second->ReceiveResource(resource, section);
}
DALLOW_AGENT_REMOVAL();
}
void Mdns::PostTask() {
FXL_DCHECK(!task_queue_.empty());
if (task_queue_.top().time_ >= posted_task_time_) {
return;
}
posted_task_time_ = task_queue_.top().time_;
task_runner_->PostTaskForTime(
[this]() {
// Suppress recursive calls to this method.
posted_task_time_ = fxl::TimePoint::Min();
fxl::TimePoint now = fxl::TimePoint::Now();
while (!task_queue_.empty() && task_queue_.top().time_ <= now) {
fxl::Closure task = task_queue_.top().task_;
task_queue_.pop();
task();
}
SendMessages();
posted_task_time_ = fxl::TimePoint::Max();
if (!task_queue_.empty()) {
PostTask();
}
},
posted_task_time_);
}
} // namespace mdns
} // namespace netconnector