blob: bee60cc111f24c66561d07eae9a0fb54b4d69a8c [file] [log] [blame]
/*
* Copyright (c) 2017, The OpenThread Authors.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of the copyright holder nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @file
* This file implements mDNS service based on avahi.
*/
#define OTBR_LOG_TAG "MDNS"
#include "mdns/mdns_avahi.hpp"
#include <algorithm>
#include <avahi-client/client.h>
#include <avahi-common/alternative.h>
#include <avahi-common/error.h>
#include <avahi-common/malloc.h>
#include <avahi-common/timeval.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include "common/code_utils.hpp"
#include "common/logging.hpp"
#include "common/time.hpp"
#include "utils/strcpy_utils.hpp"
AvahiTimeout::AvahiTimeout(const struct timeval *aTimeout,
AvahiTimeoutCallback aCallback,
void * aContext,
void * aPoller)
: mCallback(aCallback)
, mContext(aContext)
, mPoller(aPoller)
{
if (aTimeout)
{
mTimeout = otbr::Clock::now() + otbr::FromTimeval<otbr::Microseconds>(*aTimeout);
}
else
{
mTimeout = otbr::Timepoint::min();
}
}
namespace otbr {
namespace Mdns {
Poller::Poller(void)
{
mAvahiPoller.userdata = this;
mAvahiPoller.watch_new = WatchNew;
mAvahiPoller.watch_update = WatchUpdate;
mAvahiPoller.watch_get_events = WatchGetEvents;
mAvahiPoller.watch_free = WatchFree;
mAvahiPoller.timeout_new = TimeoutNew;
mAvahiPoller.timeout_update = TimeoutUpdate;
mAvahiPoller.timeout_free = TimeoutFree;
}
AvahiWatch *Poller::WatchNew(const struct AvahiPoll *aPoller,
int aFd,
AvahiWatchEvent aEvent,
AvahiWatchCallback aCallback,
void * aContext)
{
return reinterpret_cast<Poller *>(aPoller->userdata)->WatchNew(aFd, aEvent, aCallback, aContext);
}
AvahiWatch *Poller::WatchNew(int aFd, AvahiWatchEvent aEvent, AvahiWatchCallback aCallback, void *aContext)
{
assert(aEvent && aCallback && aFd >= 0);
mWatches.push_back(new AvahiWatch(aFd, aEvent, aCallback, aContext, this));
return mWatches.back();
}
void Poller::WatchUpdate(AvahiWatch *aWatch, AvahiWatchEvent aEvent)
{
aWatch->mEvents = aEvent;
}
AvahiWatchEvent Poller::WatchGetEvents(AvahiWatch *aWatch)
{
return static_cast<AvahiWatchEvent>(aWatch->mHappened);
}
void Poller::WatchFree(AvahiWatch *aWatch)
{
reinterpret_cast<Poller *>(aWatch->mPoller)->WatchFree(*aWatch);
}
void Poller::WatchFree(AvahiWatch &aWatch)
{
for (Watches::iterator it = mWatches.begin(); it != mWatches.end(); ++it)
{
if (*it == &aWatch)
{
mWatches.erase(it);
delete &aWatch;
break;
}
}
}
AvahiTimeout *Poller::TimeoutNew(const AvahiPoll * aPoller,
const struct timeval *aTimeout,
AvahiTimeoutCallback aCallback,
void * aContext)
{
assert(aPoller && aCallback);
return static_cast<Poller *>(aPoller->userdata)->TimeoutNew(aTimeout, aCallback, aContext);
}
AvahiTimeout *Poller::TimeoutNew(const struct timeval *aTimeout, AvahiTimeoutCallback aCallback, void *aContext)
{
mTimers.push_back(new AvahiTimeout(aTimeout, aCallback, aContext, this));
return mTimers.back();
}
void Poller::TimeoutUpdate(AvahiTimeout *aTimer, const struct timeval *aTimeout)
{
if (aTimeout == nullptr)
{
aTimer->mTimeout = Timepoint::min();
}
else
{
aTimer->mTimeout = Clock::now() + FromTimeval<Microseconds>(*aTimeout);
}
}
void Poller::TimeoutFree(AvahiTimeout *aTimer)
{
static_cast<Poller *>(aTimer->mPoller)->TimeoutFree(*aTimer);
}
void Poller::TimeoutFree(AvahiTimeout &aTimer)
{
for (Timers::iterator it = mTimers.begin(); it != mTimers.end(); ++it)
{
if (*it == &aTimer)
{
mTimers.erase(it);
delete &aTimer;
break;
}
}
}
void Poller::Update(MainloopContext &aMainloop)
{
Timepoint now = Clock::now();
for (Watches::iterator it = mWatches.begin(); it != mWatches.end(); ++it)
{
int fd = (*it)->mFd;
AvahiWatchEvent events = (*it)->mEvents;
if (AVAHI_WATCH_IN & events)
{
FD_SET(fd, &aMainloop.mReadFdSet);
}
if (AVAHI_WATCH_OUT & events)
{
FD_SET(fd, &aMainloop.mWriteFdSet);
}
if (AVAHI_WATCH_ERR & events)
{
FD_SET(fd, &aMainloop.mErrorFdSet);
}
if (AVAHI_WATCH_HUP & events)
{
// TODO what do with this event type?
}
aMainloop.mMaxFd = std::max(aMainloop.mMaxFd, fd);
(*it)->mHappened = 0;
}
for (Timers::iterator it = mTimers.begin(); it != mTimers.end(); ++it)
{
Timepoint timeout = (*it)->mTimeout;
if (timeout == Timepoint::min())
{
continue;
}
if (timeout <= now)
{
aMainloop.mTimeout = ToTimeval(Microseconds::zero());
break;
}
else
{
auto delay = std::chrono::duration_cast<Microseconds>(timeout - now);
if (delay < FromTimeval<Microseconds>(aMainloop.mTimeout))
{
aMainloop.mTimeout = ToTimeval(delay);
}
}
}
}
void Poller::Process(const MainloopContext &aMainloop)
{
Timepoint now = Clock::now();
std::vector<AvahiTimeout *> expired;
for (Watches::iterator it = mWatches.begin(); it != mWatches.end(); ++it)
{
int fd = (*it)->mFd;
AvahiWatchEvent events = (*it)->mEvents;
(*it)->mHappened = 0;
if ((AVAHI_WATCH_IN & events) && FD_ISSET(fd, &aMainloop.mReadFdSet))
{
(*it)->mHappened |= AVAHI_WATCH_IN;
}
if ((AVAHI_WATCH_OUT & events) && FD_ISSET(fd, &aMainloop.mWriteFdSet))
{
(*it)->mHappened |= AVAHI_WATCH_OUT;
}
if ((AVAHI_WATCH_ERR & events) && FD_ISSET(fd, &aMainloop.mErrorFdSet))
{
(*it)->mHappened |= AVAHI_WATCH_ERR;
}
// TODO hup events
if ((*it)->mHappened)
{
(*it)->mCallback(*it, (*it)->mFd, static_cast<AvahiWatchEvent>((*it)->mHappened), (*it)->mContext);
}
}
for (Timers::iterator it = mTimers.begin(); it != mTimers.end(); ++it)
{
if ((*it)->mTimeout == Timepoint::min())
{
continue;
}
if ((*it)->mTimeout <= now)
{
expired.push_back(*it);
}
}
for (std::vector<AvahiTimeout *>::iterator it = expired.begin(); it != expired.end(); ++it)
{
AvahiTimeout *avahiTimeout = *it;
avahiTimeout->mCallback(avahiTimeout, avahiTimeout->mContext);
}
}
PublisherAvahi::PublisherAvahi(int aProtocol, const char *aDomain, StateHandler aHandler, void *aContext)
: mClient(nullptr)
, mProtocol(aProtocol == AF_INET6 ? AVAHI_PROTO_INET6
: aProtocol == AF_INET ? AVAHI_PROTO_INET : AVAHI_PROTO_UNSPEC)
, mDomain(aDomain)
, mState(State::kIdle)
, mStateHandler(aHandler)
, mContext(aContext)
{
}
PublisherAvahi::~PublisherAvahi(void)
{
}
otbrError PublisherAvahi::Start(void)
{
otbrError error = OTBR_ERROR_NONE;
int avahiError = 0;
mClient = avahi_client_new(mPoller.GetAvahiPoll(), AVAHI_CLIENT_NO_FAIL, HandleClientState, this, &avahiError);
if (avahiError)
{
otbrLogErr("Failed to create avahi client: %s!", avahi_strerror(avahiError));
error = OTBR_ERROR_MDNS;
}
return error;
}
bool PublisherAvahi::IsStarted(void) const
{
return mClient != nullptr;
}
void PublisherAvahi::Stop(void)
{
FreeAllGroups();
if (mClient)
{
avahi_client_free(mClient);
mClient = nullptr;
mState = State::kIdle;
mStateHandler(mContext, mState);
}
}
void PublisherAvahi::HandleClientState(AvahiClient *aClient, AvahiClientState aState, void *aContext)
{
static_cast<PublisherAvahi *>(aContext)->HandleClientState(aClient, aState);
}
void PublisherAvahi::HandleGroupState(AvahiEntryGroup *aGroup, AvahiEntryGroupState aState, void *aContext)
{
static_cast<PublisherAvahi *>(aContext)->HandleGroupState(aGroup, aState);
}
void PublisherAvahi::HandleGroupState(AvahiEntryGroup *aGroup, AvahiEntryGroupState aState)
{
otbrLogInfo("Avahi group change to state %d.", aState);
/* Called whenever the entry group state changes */
switch (aState)
{
case AVAHI_ENTRY_GROUP_ESTABLISHED:
/* The entry group has been established successfully */
otbrLogInfo("Group established.");
CallHostOrServiceCallback(aGroup, OTBR_ERROR_NONE);
break;
case AVAHI_ENTRY_GROUP_COLLISION:
{
otbrLogErr("Name collision!");
Services::iterator serviceIt = FindService(aGroup);
if (serviceIt != mServices.end())
{
char *alternativeName = avahi_alternative_service_name(serviceIt->mName.c_str());
assert(alternativeName != nullptr);
otbrLogInfo("Re-publishing service %s using an alternative name: %s", serviceIt->mName.c_str(),
alternativeName);
serviceIt->mName = alternativeName;
avahi_free(alternativeName);
RetryPublishService(serviceIt);
}
break;
}
case AVAHI_ENTRY_GROUP_FAILURE:
/* Some kind of failure happened while we were registering our services */
otbrLogErr("Group failed: %s!", avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(aGroup))));
CallHostOrServiceCallback(aGroup, OTBR_ERROR_MDNS);
break;
case AVAHI_ENTRY_GROUP_UNCOMMITED:
case AVAHI_ENTRY_GROUP_REGISTERING:
otbrLogErr("Group ready.");
break;
default:
assert(false);
break;
}
}
void PublisherAvahi::CallHostOrServiceCallback(AvahiEntryGroup *aGroup, otbrError aError) const
{
if (mHostHandler != nullptr)
{
const auto hostIt =
std::find_if(mHosts.begin(), mHosts.end(), [aGroup](const Host &aHost) { return aHost.mGroup == aGroup; });
if (hostIt != mHosts.end())
{
mHostHandler(hostIt->mHostName.c_str(), aError, mHostHandlerContext);
}
}
if (mServiceHandler != nullptr)
{
const auto serviceIt = std::find_if(mServices.begin(), mServices.end(),
[aGroup](const Service &aService) { return aService.mGroup == aGroup; });
if (serviceIt != mServices.end())
{
mServiceHandler(serviceIt->mName.c_str(), serviceIt->mType.c_str(), aError, mServiceHandlerContext);
}
}
}
otbrError PublisherAvahi::RetryPublishService(const Services::iterator &aServiceIt)
{
otbrError error = OTBR_ERROR_NONE;
int avahiError = 0;
Service & service = *aServiceIt;
// aligned with AvahiStringList
AvahiStringList buffer[(kMaxSizeOfTxtRecord - 1) / sizeof(AvahiStringList) + 1];
AvahiStringList *head = nullptr;
SuccessOrExit(error = TxtListToAvahiStringList(service.mTxtList, buffer, sizeof(buffer), head));
avahiError = avahi_entry_group_add_service_strlst(service.mGroup, AVAHI_IF_UNSPEC, mProtocol, AvahiPublishFlags{},
service.mName.c_str(), service.mType.c_str(), mDomain,
service.mHostName.c_str(), service.mPort, head);
SuccessOrExit(avahiError);
for (const std::string &subType : service.mSubTypeList)
{
otbrLogInfo("Add subtype %s for service %s.%s", subType.c_str(), service.mName.c_str(), service.mType.c_str());
std::string fullSubType = subType + "._sub." + service.mType;
avahiError = avahi_entry_group_add_service_subtype(service.mGroup, AVAHI_IF_UNSPEC, mProtocol,
AvahiPublishFlags{}, service.mName.c_str(),
service.mType.c_str(), mDomain, fullSubType.c_str());
SuccessOrExit(avahiError);
}
otbrLogInfo("Commit service %s.%s", service.mName.c_str(), service.mType.c_str());
avahiError = avahi_entry_group_commit(service.mGroup);
SuccessOrExit(avahiError);
exit:
if (avahiError)
{
error = OTBR_ERROR_MDNS;
otbrLogErr("Failed to retry publishing service for avahi error: %s!", avahi_strerror(avahiError));
}
else if (error != OTBR_ERROR_NONE)
{
otbrLogErr("Failed to retry publishing service: %s!", otbrErrorString(error));
FreeGroup(service.mGroup);
mServices.erase(aServiceIt);
}
return error;
}
PublisherAvahi::Hosts::iterator PublisherAvahi::FindHost(const char *aHostName)
{
assert(aHostName != nullptr);
return std::find_if(mHosts.begin(), mHosts.end(),
[aHostName](const Host &aHost) { return aHost.mHostName == aHostName; });
}
otbrError PublisherAvahi::CreateHost(AvahiClient &aClient, const char *aHostName, Hosts::iterator &aOutHostIt)
{
assert(aHostName != nullptr);
otbrError error = OTBR_ERROR_NONE;
Host newHost;
newHost.mHostName = aHostName;
SuccessOrExit(error = CreateGroup(aClient, newHost.mGroup));
mHosts.push_back(newHost);
aOutHostIt = mHosts.end() - 1;
exit:
return error;
}
PublisherAvahi::Services::iterator PublisherAvahi::FindService(const char *aName, const char *aType)
{
assert(aName != nullptr);
assert(aType != nullptr);
return std::find_if(mServices.begin(), mServices.end(), [aName, aType](const Service &aService) {
return aService.mName == aName && aService.mType == aType;
});
}
PublisherAvahi::Services::iterator PublisherAvahi::FindService(AvahiEntryGroup *aGroup)
{
return std::find_if(mServices.begin(), mServices.end(),
[aGroup](const Service &aService) { return aService.mGroup == aGroup; });
}
otbrError PublisherAvahi::CreateService(AvahiClient & aClient,
const char * aName,
const char * aType,
Services::iterator &aOutServiceIt)
{
assert(aName != nullptr);
assert(aType != nullptr);
otbrError error = OTBR_ERROR_NONE;
Service newService;
newService.mName = aName;
newService.mType = aType;
SuccessOrExit(error = CreateGroup(aClient, newService.mGroup));
mServices.push_back(newService);
aOutServiceIt = mServices.end() - 1;
exit:
return error;
}
bool PublisherAvahi::IsServiceOutdated(const Service & aService,
const char * aNewHostName,
uint16_t aNewPort,
const SubTypeList &aNewSubTypeList)
{
return aService.mHostName != aNewHostName || aService.mPort != aNewPort || aService.mSubTypeList != aNewSubTypeList;
}
otbrError PublisherAvahi::CreateGroup(AvahiClient &aClient, AvahiEntryGroup *&aOutGroup)
{
otbrError error = OTBR_ERROR_NONE;
assert(aOutGroup == nullptr);
aOutGroup = avahi_entry_group_new(&aClient, HandleGroupState, this);
VerifyOrExit(aOutGroup != nullptr, error = OTBR_ERROR_MDNS);
exit:
if (error == OTBR_ERROR_MDNS)
{
otbrLogErr("Failed to create entry group for avahi error: %s", avahi_strerror(avahi_client_errno(&aClient)));
}
return error;
}
otbrError PublisherAvahi::ResetGroup(AvahiEntryGroup *aGroup)
{
assert(aGroup != nullptr);
otbrError error = OTBR_ERROR_NONE;
int avahiError = avahi_entry_group_reset(aGroup);
if (avahiError)
{
error = OTBR_ERROR_MDNS;
otbrLogErr("Failed to reset entry group for avahi error: %s", avahi_strerror(avahiError));
}
return error;
}
otbrError PublisherAvahi::FreeGroup(AvahiEntryGroup *aGroup)
{
assert(aGroup != nullptr);
otbrError error = OTBR_ERROR_NONE;
int avahiError = avahi_entry_group_free(aGroup);
if (avahiError)
{
error = OTBR_ERROR_MDNS;
otbrLogErr("Failed to free entry group for avahi error: %s", avahi_strerror(avahiError));
}
return error;
}
void PublisherAvahi::FreeAllGroups(void)
{
for (Service &service : mServices)
{
FreeGroup(service.mGroup);
}
mServices.clear();
for (Host &host : mHosts)
{
FreeGroup(host.mGroup);
}
mHosts.clear();
}
void PublisherAvahi::HandleClientState(AvahiClient *aClient, AvahiClientState aState)
{
otbrLogInfo("Avahi client state changed to %d.", aState);
switch (aState)
{
case AVAHI_CLIENT_S_RUNNING:
/* The server has startup successfully and registered its host
* name on the network, so it's time to create our services */
otbrLogInfo("Avahi client ready.");
mState = State::kReady;
mClient = aClient;
mStateHandler(mContext, mState);
break;
case AVAHI_CLIENT_FAILURE:
otbrLogErr("Client failure: %s", avahi_strerror(avahi_client_errno(aClient)));
mState = State::kIdle;
mStateHandler(mContext, mState);
break;
case AVAHI_CLIENT_S_COLLISION:
/* Let's drop our registered services. When the server is back
* in AVAHI_SERVER_RUNNING state we will register them
* again with the new host name. */
otbrLogErr("Client collision: %s", avahi_strerror(avahi_client_errno(aClient)));
// fall through
case AVAHI_CLIENT_S_REGISTERING:
/* The server records are now being established. This
* might be caused by a host name change. We need to wait
* for our own records to register until the host name is
* properly esatblished. */
FreeAllGroups();
break;
case AVAHI_CLIENT_CONNECTING:
otbrLogDebug("Connecting to avahi server");
break;
default:
assert(false);
break;
}
}
void PublisherAvahi::Update(MainloopContext &aMainloop)
{
mPoller.Update(aMainloop);
}
void PublisherAvahi::Process(const MainloopContext &aMainloop)
{
mPoller.Process(aMainloop);
}
otbrError PublisherAvahi::PublishService(const char * aHostName,
uint16_t aPort,
const char * aName,
const char * aType,
const SubTypeList &aSubTypeList,
const TxtList & aTxtList)
{
otbrError error = OTBR_ERROR_NONE;
int avahiError = 0;
Services::iterator serviceIt = mServices.end();
const char * safeHostName = (aHostName != nullptr) ? aHostName : "";
const char * logHostName = (aHostName != nullptr) ? aHostName : "localhost";
std::string fullHostName;
// aligned with AvahiStringList
AvahiStringList buffer[(kMaxSizeOfTxtRecord - 1) / sizeof(AvahiStringList) + 1];
AvahiStringList *head = nullptr;
SuccessOrExit(error = TxtListToAvahiStringList(aTxtList, buffer, sizeof(buffer), head));
VerifyOrExit(mState == State::kReady, errno = EAGAIN, error = OTBR_ERROR_ERRNO);
VerifyOrExit(mClient != nullptr, errno = EAGAIN, error = OTBR_ERROR_ERRNO);
VerifyOrExit(aName != nullptr, error = OTBR_ERROR_INVALID_ARGS);
VerifyOrExit(aType != nullptr, error = OTBR_ERROR_INVALID_ARGS);
if (aHostName != nullptr)
{
fullHostName = MakeFullName(aHostName);
aHostName = fullHostName.c_str();
}
serviceIt = FindService(aName, aType);
if (serviceIt == mServices.end())
{
SuccessOrExit(error = CreateService(*mClient, aName, aType, serviceIt));
}
else if (IsServiceOutdated(*serviceIt, safeHostName, aPort, aSubTypeList))
{
SuccessOrExit(error = ResetGroup(serviceIt->mGroup));
}
else
{
otbrLogInfo("Update service %s.%s for host %s", aName, aType, logHostName);
avahiError = avahi_entry_group_update_service_txt_strlst(serviceIt->mGroup, AVAHI_IF_UNSPEC, mProtocol,
AvahiPublishFlags{}, aName, aType, mDomain, head);
if (avahiError == 0 && mServiceHandler != nullptr)
{
// The handler should be called even if the request can be processed synchronously
mServiceHandler(aName, aType, OTBR_ERROR_NONE, mServiceHandlerContext);
}
serviceIt->mTxtList = aTxtList;
ExitNow();
}
otbrLogInfo("Create service %s.%s for host %s", aName, aType, logHostName);
avahiError =
avahi_entry_group_add_service_strlst(serviceIt->mGroup, AVAHI_IF_UNSPEC, mProtocol, AvahiPublishFlags{}, aName,
aType, mDomain, aHostName, aPort, head);
SuccessOrExit(avahiError);
for (const std::string &subType : aSubTypeList)
{
otbrLogInfo("Add subtype %s for service %s.%s", subType.c_str(), aName, aType);
std::string fullSubType = subType + "._sub." + aType;
avahiError =
avahi_entry_group_add_service_subtype(serviceIt->mGroup, AVAHI_IF_UNSPEC, mProtocol, AvahiPublishFlags{},
aName, aType, mDomain, fullSubType.c_str());
SuccessOrExit(avahiError);
}
otbrLogInfo("Commit service %s.%s", aName, aType);
avahiError = avahi_entry_group_commit(serviceIt->mGroup);
SuccessOrExit(avahiError);
serviceIt->mSubTypeList = aSubTypeList;
serviceIt->mHostName = safeHostName;
serviceIt->mPort = aPort;
serviceIt->mTxtList = aTxtList;
exit:
if (avahiError)
{
error = OTBR_ERROR_MDNS;
otbrLogErr("Failed to publish service for avahi error: %s!", avahi_strerror(avahiError));
}
else if (error != OTBR_ERROR_NONE)
{
otbrLogErr("Failed to publish service: %s!", otbrErrorString(error));
}
if (error != OTBR_ERROR_NONE && serviceIt != mServices.end())
{
FreeGroup(serviceIt->mGroup);
mServices.erase(serviceIt);
}
return error;
}
otbrError PublisherAvahi::UnpublishService(const char *aName, const char *aType)
{
otbrError error = OTBR_ERROR_NONE;
Services::iterator serviceIt;
VerifyOrExit(aName != nullptr, error = OTBR_ERROR_INVALID_ARGS);
VerifyOrExit(aType != nullptr, error = OTBR_ERROR_INVALID_ARGS);
serviceIt = FindService(aName, aType);
VerifyOrExit(serviceIt != mServices.end());
otbrLogInfo("Unpublish service %s.%s", aName, aType);
error = FreeGroup(serviceIt->mGroup);
mServices.erase(serviceIt);
exit:
return error;
}
otbrError PublisherAvahi::PublishHost(const char *aName, const uint8_t *aAddress, uint8_t aAddressLength)
{
otbrError error = OTBR_ERROR_NONE;
int avahiError = 0;
Hosts::iterator hostIt = mHosts.end();
std::string fullHostName;
AvahiAddress address;
VerifyOrExit(mState == State::kReady, errno = EAGAIN, error = OTBR_ERROR_ERRNO);
VerifyOrExit(mClient != nullptr, errno = EAGAIN, error = OTBR_ERROR_ERRNO);
VerifyOrExit(aName != nullptr, error = OTBR_ERROR_INVALID_ARGS);
VerifyOrExit(aAddress != nullptr, error = OTBR_ERROR_INVALID_ARGS);
VerifyOrExit(aAddressLength == sizeof(address.data.ipv6.address), error = OTBR_ERROR_INVALID_ARGS);
fullHostName = MakeFullName(aName);
hostIt = FindHost(aName);
if (hostIt == mHosts.end())
{
SuccessOrExit(error = CreateHost(*mClient, aName, hostIt));
}
else if (memcmp(hostIt->mAddress.data.ipv6.address, aAddress, aAddressLength))
{
SuccessOrExit(error = ResetGroup(hostIt->mGroup));
}
else
{
if (mHostHandler != nullptr)
{
// The handler should be called even if the request can be processed synchronously
mHostHandler(aName, OTBR_ERROR_NONE, mHostHandlerContext);
}
ExitNow();
}
address.proto = AVAHI_PROTO_INET6;
memcpy(&address.data.ipv6.address[0], aAddress, aAddressLength);
otbrLogInfo("Create host %s", aName);
avahiError = avahi_entry_group_add_address(hostIt->mGroup, AVAHI_IF_UNSPEC, mProtocol, AVAHI_PUBLISH_NO_REVERSE,
fullHostName.c_str(), &address);
SuccessOrExit(avahiError);
otbrLogInfo("Commit host %s", aName);
avahiError = avahi_entry_group_commit(hostIt->mGroup);
SuccessOrExit(avahiError);
hostIt->mAddress = address;
exit:
if (avahiError)
{
error = OTBR_ERROR_MDNS;
otbrLogErr("Failed to publish host for avahi error: %s!", avahi_strerror(avahiError));
}
else if (error != OTBR_ERROR_NONE)
{
otbrLogErr("Failed to publish host: %s!", otbrErrorString(error));
}
if (error != OTBR_ERROR_NONE && hostIt != mHosts.end())
{
FreeGroup(hostIt->mGroup);
mHosts.erase(hostIt);
}
return error;
}
otbrError PublisherAvahi::UnpublishHost(const char *aName)
{
otbrError error = OTBR_ERROR_NONE;
Hosts::iterator hostIt;
VerifyOrExit(aName != nullptr, error = OTBR_ERROR_INVALID_ARGS);
hostIt = FindHost(aName);
VerifyOrExit(hostIt != mHosts.end());
otbrLogInfo("Delete host %s", aName);
error = FreeGroup(hostIt->mGroup);
mHosts.erase(hostIt);
exit:
return error;
}
otbrError PublisherAvahi::TxtListToAvahiStringList(const TxtList & aTxtList,
AvahiStringList * aBuffer,
size_t aBufferSize,
AvahiStringList *&aHead)
{
otbrError error = OTBR_ERROR_NONE;
size_t used = 0;
AvahiStringList *last = nullptr;
AvahiStringList *curr = aBuffer;
aHead = nullptr;
for (const auto &txtEntry : aTxtList)
{
const char * name = txtEntry.mName.c_str();
size_t nameLength = txtEntry.mName.length();
const uint8_t *value = txtEntry.mValue.data();
size_t valueLength = txtEntry.mValue.size();
// +1 for the size of "=", avahi doesn't need '\0' at the end of the entry
size_t needed = sizeof(AvahiStringList) - sizeof(AvahiStringList::text) + nameLength + valueLength + 1;
VerifyOrExit(used + needed <= aBufferSize, errno = EMSGSIZE, error = OTBR_ERROR_ERRNO);
curr->next = last;
last = curr;
memcpy(curr->text, name, nameLength);
curr->text[nameLength] = '=';
memcpy(curr->text + nameLength + 1, value, valueLength);
curr->size = nameLength + valueLength + 1;
{
const uint8_t *next = curr->text + curr->size;
curr = OTBR_ALIGNED(next, AvahiStringList *);
}
used = static_cast<size_t>(reinterpret_cast<uint8_t *>(curr) - reinterpret_cast<uint8_t *>(aBuffer));
}
SuccessOrExit(error);
aHead = last;
exit:
return error;
}
std::string PublisherAvahi::MakeFullName(const char *aName)
{
assert(aName != nullptr);
std::string fullHostName(aName);
fullHostName += '.';
fullHostName += (mDomain == nullptr ? "local." : mDomain);
return fullHostName;
}
void PublisherAvahi::SubscribeService(const std::string &aType, const std::string &aInstanceName)
{
mSubscribedServices.emplace_back(*this, aType, aInstanceName);
otbrLogInfo("subscribe service %s.%s (total %zu)", aInstanceName.c_str(), aType.c_str(),
mSubscribedServices.size());
if (aInstanceName.empty())
{
mSubscribedServices.back().Browse();
}
else
{
mSubscribedServices.back().Resolve(AVAHI_IF_UNSPEC, aInstanceName.c_str(), aType.c_str(), "local.");
}
}
void PublisherAvahi::UnsubscribeService(const std::string &aType, const std::string &aInstanceName)
{
ServiceSubscriptionList::iterator it =
std::find_if(mSubscribedServices.begin(), mSubscribedServices.end(),
[&aType, &aInstanceName](const ServiceSubscription &aService) {
return aService.mType == aType && aService.mInstanceName == aInstanceName;
});
assert(it != mSubscribedServices.end());
it->Release();
mSubscribedServices.erase(it);
otbrLogInfo("unsubscribe service %s.%s (left %zu)", aInstanceName.c_str(), aType.c_str(),
mSubscribedServices.size());
}
void PublisherAvahi::OnServiceResolved(ServiceSubscription &aService)
{
otbrLogInfo("Service %s is resolved successfully: %s host %s addresses %zu", aService.mType.c_str(),
aService.mInstanceInfo.mName.c_str(), aService.mInstanceInfo.mHostName.c_str(),
aService.mInstanceInfo.mAddresses.size());
if (mDiscoveredServiceInstanceCallback != nullptr)
{
mDiscoveredServiceInstanceCallback(aService.mType, aService.mInstanceInfo);
}
}
void PublisherAvahi::OnServiceResolveFailed(const ServiceSubscription &aService, int aErrorCode)
{
otbrLogWarning("Service %s resolving failed: code=%d", aService.mType.c_str(), aErrorCode);
}
void PublisherAvahi::OnHostResolved(HostSubscription &aHost)
{
otbrLogInfo("Host %s is resolved successfully: host %s addresses %zu ttl %u", aHost.mHostName.c_str(),
aHost.mHostInfo.mHostName.c_str(), aHost.mHostInfo.mAddresses.size(), aHost.mHostInfo.mTtl);
if (mDiscoveredHostCallback != nullptr)
{
mDiscoveredHostCallback(aHost.mHostName, aHost.mHostInfo);
}
}
void PublisherAvahi::OnHostResolveFailed(const HostSubscription &aHost, int aErrorCode)
{
otbrLogWarning("Host %s resolving failed: code=%d", aHost.mHostName.c_str(), aErrorCode);
}
void PublisherAvahi::SubscribeHost(const std::string &aHostName)
{
mSubscribedHosts.emplace_back(*this, aHostName);
otbrLogInfo("subscribe host %s (total %zu)", aHostName.c_str(), mSubscribedHosts.size());
mSubscribedHosts.back().Resolve();
}
void PublisherAvahi::UnsubscribeHost(const std::string &aHostName)
{
HostSubscriptionList::iterator it =
std::find_if(mSubscribedHosts.begin(), mSubscribedHosts.end(),
[&aHostName](const HostSubscription &aHost) { return aHost.mHostName == aHostName; });
assert(it != mSubscribedHosts.end());
it->Release();
mSubscribedHosts.erase(it);
otbrLogInfo("unsubscribe host %s (remaining %d)", aHostName.c_str(), mSubscribedHosts.size());
}
Publisher *Publisher::Create(int aFamily, const char *aDomain, StateHandler aHandler, void *aContext)
{
return new PublisherAvahi(aFamily, aDomain, aHandler, aContext);
}
void Publisher::Destroy(Publisher *aPublisher)
{
delete static_cast<PublisherAvahi *>(aPublisher);
}
void PublisherAvahi::ServiceSubscription::Browse(void)
{
assert(mPublisherAvahi->mClient != nullptr);
otbrLogInfo("browse service %s", mType.c_str());
mServiceBrowser =
avahi_service_browser_new(mPublisherAvahi->mClient, AVAHI_IF_UNSPEC, AVAHI_PROTO_INET6, mType.c_str(),
mPublisherAvahi->mDomain, static_cast<AvahiLookupFlags>(0), HandleBrowseResult, this);
if (!mServiceBrowser)
{
otbrLogWarning("failed to browse service %s: %s", mType.c_str(),
avahi_strerror(avahi_client_errno(mPublisherAvahi->mClient)));
}
}
void PublisherAvahi::ServiceSubscription::Release(void)
{
if (mServiceBrowser != nullptr)
{
avahi_service_browser_free(mServiceBrowser);
mServiceBrowser = nullptr;
}
if (mServiceResolver != nullptr)
{
avahi_service_resolver_free(mServiceResolver);
mServiceResolver = nullptr;
}
}
void PublisherAvahi::ServiceSubscription::HandleBrowseResult(AvahiServiceBrowser * aServiceBrowser,
AvahiIfIndex aInterfaceIndex,
AvahiProtocol aProtocol,
AvahiBrowserEvent aEvent,
const char * aName,
const char * aType,
const char * aDomain,
AvahiLookupResultFlags aFlags,
void * aContext)
{
static_cast<PublisherAvahi::ServiceSubscription *>(aContext)->HandleBrowseResult(
aServiceBrowser, aInterfaceIndex, aProtocol, aEvent, aName, aType, aDomain, aFlags);
}
void PublisherAvahi::ServiceSubscription::HandleBrowseResult(AvahiServiceBrowser * aServiceBrowser,
AvahiIfIndex aInterfaceIndex,
AvahiProtocol aProtocol,
AvahiBrowserEvent aEvent,
const char * aName,
const char * aType,
const char * aDomain,
AvahiLookupResultFlags aFlags)
{
OTBR_UNUSED_VARIABLE(aServiceBrowser);
OTBR_UNUSED_VARIABLE(aProtocol);
assert(mServiceBrowser == aServiceBrowser);
otbrLogInfo("browse service reply: %s.%s%s inf %u, flags=%u", aName, aType, aDomain, aInterfaceIndex, aFlags);
if (aEvent == AVAHI_BROWSER_FAILURE)
{
mPublisherAvahi->OnServiceResolveFailed(*this, avahi_client_errno(mPublisherAvahi->mClient));
}
else
{
Resolve(aInterfaceIndex, aName, aType, aDomain);
}
if (mServiceBrowser != nullptr)
{
avahi_service_browser_free(mServiceBrowser);
mServiceBrowser = nullptr;
}
}
void PublisherAvahi::ServiceSubscription::Resolve(uint32_t aInterfaceIndex,
const char *aInstanceName,
const char *aType,
const char *aDomain)
{
otbrLogInfo("resolve service %s %s %s inf %d", aInstanceName, aType, aDomain, aInterfaceIndex);
mServiceResolver = avahi_service_resolver_new(mPublisherAvahi->mClient, aInterfaceIndex, AVAHI_PROTO_INET6,
aInstanceName, aType, aDomain, AVAHI_PROTO_INET6,
static_cast<AvahiLookupFlags>(0), HandleResolveResult, this);
if (!mServiceResolver)
{
otbrLogErr("failed to resolve serivce %s: %s", mType.c_str(),
avahi_strerror(avahi_client_errno(mPublisherAvahi->mClient)));
}
}
void PublisherAvahi::ServiceSubscription::HandleResolveResult(AvahiServiceResolver * aServiceResolver,
AvahiIfIndex aInterfaceIndex,
AvahiProtocol aProtocol,
AvahiResolverEvent aEvent,
const char * aName,
const char * aType,
const char * aDomain,
const char * aHostName,
const AvahiAddress * aAddress,
uint16_t aPort,
AvahiStringList * aTxt,
AvahiLookupResultFlags aFlags,
void * aContext)
{
static_cast<PublisherAvahi::ServiceSubscription *>(aContext)->HandleResolveResult(
aServiceResolver, aInterfaceIndex, aProtocol, aEvent, aName, aType, aDomain, aHostName, aAddress, aPort, aTxt,
aFlags);
}
void PublisherAvahi::ServiceSubscription::HandleResolveResult(AvahiServiceResolver * aServiceResolver,
AvahiIfIndex aInterfaceIndex,
AvahiProtocol aProtocol,
AvahiResolverEvent aEvent,
const char * aName,
const char * aType,
const char * aDomain,
const char * aHostName,
const AvahiAddress * aAddress,
uint16_t aPort,
AvahiStringList * aTxt,
AvahiLookupResultFlags aFlags)
{
OT_UNUSED_VARIABLE(aServiceResolver);
OT_UNUSED_VARIABLE(aInterfaceIndex);
OT_UNUSED_VARIABLE(aProtocol);
OT_UNUSED_VARIABLE(aType);
OT_UNUSED_VARIABLE(aDomain);
char buf[AVAHI_ADDRESS_STR_MAX];
Ip6Address address;
size_t totalTxtSize = 0;
assert(mServiceResolver == aServiceResolver);
VerifyOrExit(
aEvent == AVAHI_RESOLVER_FOUND,
otbrLogErr("failed to resolve service: %s", avahi_strerror(avahi_client_errno(mPublisherAvahi->mClient))));
VerifyOrExit(aHostName != nullptr, otbrLogErr("host name is null"));
mInstanceInfo.mName = aName;
mInstanceInfo.mHostName = std::string(aHostName) + ".";
mInstanceInfo.mPort = aPort;
avahi_address_snprint(buf, sizeof(buf), aAddress);
VerifyOrExit(otbrError::OTBR_ERROR_NONE == Ip6Address::FromString(buf, address),
otbrLogErr("failed to parse the IP address: %s", buf));
otbrLogDebug("resolve service reply: flags=%u, host=%s", aFlags, aHostName);
VerifyOrExit(!address.IsLinkLocal() && !address.IsMulticast() && !address.IsLoopback() && !address.IsUnspecified(),
otbrLogDebug("ignoring address %s", address.ToString().c_str()));
mInstanceInfo.mAddresses.push_back(address);
// TODO priority
// TODO weight
// TODO use a more proper TTL
mInstanceInfo.mTtl = kDefaultTtl;
for (auto p = aTxt; p; p = avahi_string_list_get_next(p))
{
totalTxtSize += avahi_string_list_get_size(p) + 1;
}
mInstanceInfo.mTxtData.resize(totalTxtSize);
avahi_string_list_serialize(aTxt, mInstanceInfo.mTxtData.data(), totalTxtSize);
otbrLogDebug("resolve service reply: address=%s, ttl=%u", address.ToString().c_str(), mInstanceInfo.mTtl);
mPublisherAvahi->OnServiceResolved(*this);
exit:
if (avahi_client_errno(mPublisherAvahi->mClient) != AVAHI_OK)
{
mPublisherAvahi->OnServiceResolveFailed(*this, avahi_client_errno(mPublisherAvahi->mClient));
}
if (mServiceBrowser != nullptr)
{
avahi_service_resolver_free(mServiceResolver);
mServiceResolver = nullptr;
}
}
void PublisherAvahi::HostSubscription::Release(void)
{
if (mRecordBrowser != nullptr)
{
avahi_record_browser_free(mRecordBrowser);
mRecordBrowser = nullptr;
}
}
void PublisherAvahi::HostSubscription::Resolve(void)
{
std::string fullHostName = mHostName + ".local.";
otbrLogDebug("resolve host %s inf %d", fullHostName.c_str(), AVAHI_IF_UNSPEC);
mRecordBrowser = avahi_record_browser_new(mPublisherAvahi->mClient, AVAHI_IF_UNSPEC, AVAHI_PROTO_INET6,
fullHostName.c_str(), AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_AAAA,
static_cast<AvahiLookupFlags>(0), HandleResolveResult, this);
if (!mRecordBrowser)
{
otbrLogErr("failed to resolve host %s: %s", fullHostName.c_str(),
avahi_strerror(avahi_client_errno(mPublisherAvahi->mClient)));
}
}
void PublisherAvahi::HostSubscription::HandleResolveResult(AvahiRecordBrowser * aRecordBrowser,
AvahiIfIndex aInterfaceIndex,
AvahiProtocol aProtocol,
AvahiBrowserEvent aEvent,
const char * aName,
uint16_t aClazz,
uint16_t aType,
const void * aRdata,
size_t aSize,
AvahiLookupResultFlags aFlags,
void * aContext)
{
static_cast<PublisherAvahi::HostSubscription *>(aContext)->HandleResolveResult(
aRecordBrowser, aInterfaceIndex, aProtocol, aEvent, aName, aClazz, aType, aRdata, aSize, aFlags);
}
void PublisherAvahi::HostSubscription::HandleResolveResult(AvahiRecordBrowser * aRecordBrowser,
AvahiIfIndex aInterfaceIndex,
AvahiProtocol aProtocol,
AvahiBrowserEvent aEvent,
const char * aName,
uint16_t aClazz,
uint16_t aType,
const void * aRdata,
size_t aSize,
AvahiLookupResultFlags aFlags)
{
OTBR_UNUSED_VARIABLE(aRecordBrowser);
OTBR_UNUSED_VARIABLE(aInterfaceIndex);
OTBR_UNUSED_VARIABLE(aProtocol);
OTBR_UNUSED_VARIABLE(aEvent);
OTBR_UNUSED_VARIABLE(aClazz);
OTBR_UNUSED_VARIABLE(aType);
OTBR_UNUSED_VARIABLE(aFlags);
Ip6Address address = *static_cast<const uint8_t(*)[16]>(aRdata);
assert(mRecordBrowser == aRecordBrowser);
VerifyOrExit(!address.IsLinkLocal() && !address.IsMulticast() && !address.IsLoopback() && !address.IsUnspecified());
VerifyOrExit(aSize == 16, otbrLogErr("unexpected address data length: %u", aSize));
otbrLogInfo("resolved host address: %s", address.ToString().c_str());
mHostInfo.mHostName = std::string(aName) + ".";
mHostInfo.mAddresses.push_back(std::move(address));
// TODO: Use a more proper TTL
mHostInfo.mTtl = kDefaultTtl;
mPublisherAvahi->OnHostResolved(*this);
exit:
if (avahi_client_errno(mPublisherAvahi->mClient) != AVAHI_OK)
{
mPublisherAvahi->OnHostResolveFailed(*this, avahi_client_errno(mPublisherAvahi->mClient));
}
if (mRecordBrowser != nullptr)
{
avahi_record_browser_free(mRecordBrowser);
mRecordBrowser = nullptr;
}
}
} // namespace Mdns
} // namespace otbr