2202 lines
70 KiB
C++
2202 lines
70 KiB
C++
/*
|
|
* Copyright (c) 2020, 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 includes implementation for SRP server.
|
|
*/
|
|
|
|
#include "srp_server.hpp"
|
|
|
|
#if OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
|
|
|
|
#include "common/as_core_type.hpp"
|
|
#include "common/const_cast.hpp"
|
|
#include "common/instance.hpp"
|
|
#include "common/locator_getters.hpp"
|
|
#include "common/log.hpp"
|
|
#include "common/new.hpp"
|
|
#include "common/random.hpp"
|
|
#include "net/dns_types.hpp"
|
|
#include "thread/thread_netif.hpp"
|
|
|
|
namespace ot {
|
|
namespace Srp {
|
|
|
|
RegisterLogModule("SrpServer");
|
|
|
|
static const char kDefaultDomain[] = "default.service.arpa.";
|
|
static const char kServiceSubTypeLabel[] = "._sub.";
|
|
|
|
static Dns::UpdateHeader::Response ErrorToDnsResponseCode(Error aError)
|
|
{
|
|
Dns::UpdateHeader::Response responseCode;
|
|
|
|
switch (aError)
|
|
{
|
|
case kErrorNone:
|
|
responseCode = Dns::UpdateHeader::kResponseSuccess;
|
|
break;
|
|
case kErrorNoBufs:
|
|
responseCode = Dns::UpdateHeader::kResponseServerFailure;
|
|
break;
|
|
case kErrorParse:
|
|
responseCode = Dns::UpdateHeader::kResponseFormatError;
|
|
break;
|
|
case kErrorDuplicated:
|
|
responseCode = Dns::UpdateHeader::kResponseNameExists;
|
|
break;
|
|
default:
|
|
responseCode = Dns::UpdateHeader::kResponseRefused;
|
|
break;
|
|
}
|
|
|
|
return responseCode;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------------
|
|
// Server
|
|
|
|
Server::Server(Instance &aInstance)
|
|
: InstanceLocator(aInstance)
|
|
, mSocket(aInstance)
|
|
, mServiceUpdateHandler(nullptr)
|
|
, mServiceUpdateHandlerContext(nullptr)
|
|
, mLeaseTimer(aInstance, HandleLeaseTimer)
|
|
, mOutstandingUpdatesTimer(aInstance, HandleOutstandingUpdatesTimer)
|
|
, mServiceUpdateId(Random::NonCrypto::GetUint32())
|
|
, mPort(kUdpPortMin)
|
|
, mState(kStateDisabled)
|
|
, mAddressMode(kDefaultAddressMode)
|
|
, mAnycastSequenceNumber(0)
|
|
, mHasRegisteredAnyService(false)
|
|
{
|
|
IgnoreError(SetDomain(kDefaultDomain));
|
|
}
|
|
|
|
void Server::SetServiceHandler(otSrpServerServiceUpdateHandler aServiceHandler, void *aServiceHandlerContext)
|
|
{
|
|
mServiceUpdateHandler = aServiceHandler;
|
|
mServiceUpdateHandlerContext = aServiceHandlerContext;
|
|
}
|
|
|
|
Error Server::SetAddressMode(AddressMode aMode)
|
|
{
|
|
Error error = kErrorNone;
|
|
|
|
VerifyOrExit(mState == kStateDisabled, error = kErrorInvalidState);
|
|
VerifyOrExit(mAddressMode != aMode);
|
|
LogInfo("Address Mode: %s -> %s", AddressModeToString(mAddressMode), AddressModeToString(aMode));
|
|
mAddressMode = aMode;
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
Error Server::SetAnycastModeSequenceNumber(uint8_t aSequenceNumber)
|
|
{
|
|
Error error = kErrorNone;
|
|
|
|
VerifyOrExit(mState == kStateDisabled, error = kErrorInvalidState);
|
|
mAnycastSequenceNumber = aSequenceNumber;
|
|
|
|
LogInfo("Set Anycast Address Mode Seq Number to %d", aSequenceNumber);
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
void Server::SetEnabled(bool aEnabled)
|
|
{
|
|
if (aEnabled)
|
|
{
|
|
VerifyOrExit(mState == kStateDisabled);
|
|
mState = kStateStopped;
|
|
|
|
// Request publishing of "DNS/SRP Address Service" entry in the
|
|
// Thread Network Data based of `mAddressMode`. Then wait for
|
|
// callback `HandleNetDataPublisherEntryChange()` from the
|
|
// `Publisher` to start the SRP server.
|
|
|
|
switch (mAddressMode)
|
|
{
|
|
case kAddressModeUnicast:
|
|
SelectPort();
|
|
Get<NetworkData::Publisher>().PublishDnsSrpServiceUnicast(mPort);
|
|
break;
|
|
|
|
case kAddressModeAnycast:
|
|
mPort = kAnycastAddressModePort;
|
|
Get<NetworkData::Publisher>().PublishDnsSrpServiceAnycast(mAnycastSequenceNumber);
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
VerifyOrExit(mState != kStateDisabled);
|
|
Get<NetworkData::Publisher>().UnpublishDnsSrpService();
|
|
Stop();
|
|
mState = kStateDisabled;
|
|
}
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
Server::TtlConfig::TtlConfig(void)
|
|
{
|
|
mMinTtl = kDefaultMinTtl;
|
|
mMaxTtl = kDefaultMaxTtl;
|
|
}
|
|
|
|
Error Server::SetTtlConfig(const TtlConfig &aTtlConfig)
|
|
{
|
|
Error error = kErrorNone;
|
|
|
|
VerifyOrExit(aTtlConfig.IsValid(), error = kErrorInvalidArgs);
|
|
mTtlConfig = aTtlConfig;
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
uint32_t Server::TtlConfig::GrantTtl(uint32_t aLease, uint32_t aTtl) const
|
|
{
|
|
OT_ASSERT(mMinTtl <= mMaxTtl);
|
|
|
|
return OT_MAX(mMinTtl, OT_MIN(OT_MIN(mMaxTtl, aLease), aTtl));
|
|
}
|
|
|
|
Server::LeaseConfig::LeaseConfig(void)
|
|
{
|
|
mMinLease = kDefaultMinLease;
|
|
mMaxLease = kDefaultMaxLease;
|
|
mMinKeyLease = kDefaultMinKeyLease;
|
|
mMaxKeyLease = kDefaultMaxKeyLease;
|
|
}
|
|
|
|
bool Server::LeaseConfig::IsValid(void) const
|
|
{
|
|
bool valid = false;
|
|
|
|
// TODO: Support longer LEASE.
|
|
// We use milliseconds timer for LEASE & KEY-LEASE, this is to avoid overflow.
|
|
VerifyOrExit(mMaxKeyLease <= Time::MsecToSec(TimerMilli::kMaxDelay));
|
|
VerifyOrExit(mMinLease <= mMaxLease);
|
|
VerifyOrExit(mMinKeyLease <= mMaxKeyLease);
|
|
VerifyOrExit(mMinLease <= mMinKeyLease);
|
|
VerifyOrExit(mMaxLease <= mMaxKeyLease);
|
|
|
|
valid = true;
|
|
|
|
exit:
|
|
return valid;
|
|
}
|
|
|
|
uint32_t Server::LeaseConfig::GrantLease(uint32_t aLease) const
|
|
{
|
|
OT_ASSERT(mMinLease <= mMaxLease);
|
|
|
|
return (aLease == 0) ? 0 : OT_MAX(mMinLease, OT_MIN(mMaxLease, aLease));
|
|
}
|
|
|
|
uint32_t Server::LeaseConfig::GrantKeyLease(uint32_t aKeyLease) const
|
|
{
|
|
OT_ASSERT(mMinKeyLease <= mMaxKeyLease);
|
|
|
|
return (aKeyLease == 0) ? 0 : OT_MAX(mMinKeyLease, OT_MIN(mMaxKeyLease, aKeyLease));
|
|
}
|
|
|
|
Error Server::SetLeaseConfig(const LeaseConfig &aLeaseConfig)
|
|
{
|
|
Error error = kErrorNone;
|
|
|
|
VerifyOrExit(aLeaseConfig.IsValid(), error = kErrorInvalidArgs);
|
|
mLeaseConfig = aLeaseConfig;
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
Error Server::SetDomain(const char *aDomain)
|
|
{
|
|
Error error = kErrorNone;
|
|
uint16_t length;
|
|
|
|
VerifyOrExit(mState == kStateDisabled, error = kErrorInvalidState);
|
|
|
|
length = StringLength(aDomain, Dns::Name::kMaxNameSize);
|
|
VerifyOrExit((length > 0) && (length < Dns::Name::kMaxNameSize), error = kErrorInvalidArgs);
|
|
|
|
if (aDomain[length - 1] == '.')
|
|
{
|
|
error = mDomain.Set(aDomain);
|
|
}
|
|
else
|
|
{
|
|
// Need to append dot at the end
|
|
|
|
char buf[Dns::Name::kMaxNameSize];
|
|
|
|
VerifyOrExit(length < Dns::Name::kMaxNameSize - 1, error = kErrorInvalidArgs);
|
|
|
|
memcpy(buf, aDomain, length);
|
|
buf[length] = '.';
|
|
buf[length + 1] = '\0';
|
|
|
|
error = mDomain.Set(buf);
|
|
}
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
const Server::Host *Server::GetNextHost(const Server::Host *aHost)
|
|
{
|
|
return (aHost == nullptr) ? mHosts.GetHead() : aHost->GetNext();
|
|
}
|
|
|
|
// This method adds a SRP service host and takes ownership of it.
|
|
// The caller MUST make sure that there is no existing host with the same hostname.
|
|
void Server::AddHost(Host &aHost)
|
|
{
|
|
LogInfo("Add new host %s", aHost.GetFullName());
|
|
|
|
OT_ASSERT(mHosts.FindMatching(aHost.GetFullName()) == nullptr);
|
|
IgnoreError(mHosts.Add(aHost));
|
|
}
|
|
void Server::RemoveHost(Host *aHost, RetainName aRetainName, NotifyMode aNotifyServiceHandler)
|
|
{
|
|
VerifyOrExit(aHost != nullptr);
|
|
|
|
aHost->mLease = 0;
|
|
aHost->ClearResources();
|
|
|
|
if (aRetainName)
|
|
{
|
|
LogInfo("Remove host %s (but retain its name)", aHost->GetFullName());
|
|
}
|
|
else
|
|
{
|
|
aHost->mKeyLease = 0;
|
|
IgnoreError(mHosts.Remove(*aHost));
|
|
LogInfo("Fully remove host %s", aHost->GetFullName());
|
|
}
|
|
|
|
if (aNotifyServiceHandler && mServiceUpdateHandler != nullptr)
|
|
{
|
|
uint32_t updateId = AllocateId();
|
|
|
|
LogInfo("SRP update handler is notified (updatedId = %u)", updateId);
|
|
mServiceUpdateHandler(updateId, aHost, kDefaultEventsHandlerTimeout, mServiceUpdateHandlerContext);
|
|
// We don't wait for the reply from the service update handler,
|
|
// but always remove the host (and its services) regardless of
|
|
// host/service update result. Because removing a host should fail
|
|
// only when there is system failure of the platform mDNS implementation
|
|
// and in which case the host is not expected to be still registered.
|
|
}
|
|
|
|
if (!aRetainName)
|
|
{
|
|
aHost->Free();
|
|
}
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
bool Server::HasNameConflictsWith(Host &aHost) const
|
|
{
|
|
bool hasConflicts = false;
|
|
const Host *existingHost = mHosts.FindMatching(aHost.GetFullName());
|
|
|
|
if (existingHost != nullptr && aHost.GetKeyRecord()->GetKey() != existingHost->GetKeyRecord()->GetKey())
|
|
{
|
|
LogWarn("Name conflict: host name %s has already been allocated", aHost.GetFullName());
|
|
ExitNow(hasConflicts = true);
|
|
}
|
|
|
|
for (const Service &service : aHost.mServices)
|
|
{
|
|
// Check on all hosts for a matching service with the same
|
|
// instance name and if found, verify that it has the same
|
|
// key.
|
|
|
|
for (const Host &host : mHosts)
|
|
{
|
|
if (host.HasServiceInstance(service.GetInstanceName()) &&
|
|
aHost.GetKeyRecord()->GetKey() != host.GetKeyRecord()->GetKey())
|
|
{
|
|
LogWarn("Name conflict: service name %s has already been allocated", service.GetInstanceName());
|
|
ExitNow(hasConflicts = true);
|
|
}
|
|
}
|
|
}
|
|
|
|
exit:
|
|
return hasConflicts;
|
|
}
|
|
|
|
void Server::HandleServiceUpdateResult(ServiceUpdateId aId, Error aError)
|
|
{
|
|
UpdateMetadata *update = mOutstandingUpdates.FindMatching(aId);
|
|
|
|
if (update != nullptr)
|
|
{
|
|
HandleServiceUpdateResult(update, aError);
|
|
}
|
|
else
|
|
{
|
|
LogInfo("Delayed SRP host update result, the SRP update has been committed (updateId = %u)", aId);
|
|
}
|
|
}
|
|
|
|
void Server::HandleServiceUpdateResult(UpdateMetadata *aUpdate, Error aError)
|
|
{
|
|
LogInfo("Handler result of SRP update (id = %u) is received: %s", aUpdate->GetId(), ErrorToString(aError));
|
|
|
|
IgnoreError(mOutstandingUpdates.Remove(*aUpdate));
|
|
CommitSrpUpdate(aError, *aUpdate);
|
|
aUpdate->Free();
|
|
|
|
if (mOutstandingUpdates.IsEmpty())
|
|
{
|
|
mOutstandingUpdatesTimer.Stop();
|
|
}
|
|
else
|
|
{
|
|
mOutstandingUpdatesTimer.FireAt(mOutstandingUpdates.GetTail()->GetExpireTime());
|
|
}
|
|
}
|
|
|
|
void Server::CommitSrpUpdate(Error aError, Host &aHost, const MessageMetadata &aMessageMetadata)
|
|
{
|
|
CommitSrpUpdate(aError, aHost, aMessageMetadata.mDnsHeader, aMessageMetadata.mMessageInfo,
|
|
aMessageMetadata.mTtlConfig, aMessageMetadata.mLeaseConfig);
|
|
}
|
|
|
|
void Server::CommitSrpUpdate(Error aError, UpdateMetadata &aUpdateMetadata)
|
|
{
|
|
CommitSrpUpdate(aError, aUpdateMetadata.GetHost(), aUpdateMetadata.GetDnsHeader(),
|
|
aUpdateMetadata.IsDirectRxFromClient() ? &aUpdateMetadata.GetMessageInfo() : nullptr,
|
|
aUpdateMetadata.GetTtlConfig(), aUpdateMetadata.GetLeaseConfig());
|
|
}
|
|
|
|
void Server::CommitSrpUpdate(Error aError,
|
|
Host & aHost,
|
|
const Dns::UpdateHeader &aDnsHeader,
|
|
const Ip6::MessageInfo * aMessageInfo,
|
|
const TtlConfig & aTtlConfig,
|
|
const LeaseConfig & aLeaseConfig)
|
|
{
|
|
Host * existingHost;
|
|
uint32_t hostLease;
|
|
uint32_t hostKeyLease;
|
|
uint32_t grantedLease;
|
|
uint32_t grantedKeyLease;
|
|
uint32_t grantedTtl;
|
|
bool shouldFreeHost = true;
|
|
|
|
SuccessOrExit(aError);
|
|
|
|
hostLease = aHost.GetLease();
|
|
hostKeyLease = aHost.GetKeyLease();
|
|
grantedLease = aLeaseConfig.GrantLease(hostLease);
|
|
grantedKeyLease = aLeaseConfig.GrantKeyLease(hostKeyLease);
|
|
grantedTtl = aTtlConfig.GrantTtl(grantedLease, aHost.GetTtl());
|
|
|
|
aHost.SetLease(grantedLease);
|
|
aHost.SetKeyLease(grantedKeyLease);
|
|
aHost.SetTtl(grantedTtl);
|
|
|
|
for (Service &service : aHost.mServices)
|
|
{
|
|
service.mDescription->mLease = grantedLease;
|
|
service.mDescription->mKeyLease = grantedKeyLease;
|
|
service.mDescription->mTtl = grantedTtl;
|
|
}
|
|
|
|
existingHost = mHosts.FindMatching(aHost.GetFullName());
|
|
|
|
if (aHost.GetLease() == 0)
|
|
{
|
|
if (aHost.GetKeyLease() == 0)
|
|
{
|
|
LogInfo("Remove key of host %s", aHost.GetFullName());
|
|
RemoveHost(existingHost, kDeleteName, kDoNotNotifyServiceHandler);
|
|
}
|
|
else if (existingHost != nullptr)
|
|
{
|
|
existingHost->SetKeyLease(aHost.GetKeyLease());
|
|
RemoveHost(existingHost, kRetainName, kDoNotNotifyServiceHandler);
|
|
|
|
for (Service &service : existingHost->mServices)
|
|
{
|
|
existingHost->RemoveService(&service, kRetainName, kDoNotNotifyServiceHandler);
|
|
}
|
|
}
|
|
}
|
|
else if (existingHost != nullptr)
|
|
{
|
|
SuccessOrExit(aError = existingHost->MergeServicesAndResourcesFrom(aHost));
|
|
}
|
|
else
|
|
{
|
|
AddHost(aHost);
|
|
shouldFreeHost = false;
|
|
|
|
for (Service &service : aHost.GetServices())
|
|
{
|
|
service.mIsCommitted = true;
|
|
service.Log(Service::kAddNew);
|
|
}
|
|
|
|
#if OPENTHREAD_CONFIG_SRP_SERVER_PORT_SWITCH_ENABLE
|
|
if (!mHasRegisteredAnyService && (mAddressMode == kAddressModeUnicast))
|
|
{
|
|
Settings::SrpServerInfo info;
|
|
|
|
mHasRegisteredAnyService = true;
|
|
info.SetPort(GetSocket().mSockName.mPort);
|
|
IgnoreError(Get<Settings>().Save(info));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Re-schedule the lease timer.
|
|
HandleLeaseTimer();
|
|
|
|
exit:
|
|
if (aMessageInfo != nullptr)
|
|
{
|
|
if (aError == kErrorNone && !(grantedLease == hostLease && grantedKeyLease == hostKeyLease))
|
|
{
|
|
SendResponse(aDnsHeader, grantedLease, grantedKeyLease, *aMessageInfo);
|
|
}
|
|
else
|
|
{
|
|
SendResponse(aDnsHeader, ErrorToDnsResponseCode(aError), *aMessageInfo);
|
|
}
|
|
}
|
|
|
|
if (shouldFreeHost)
|
|
{
|
|
aHost.Free();
|
|
}
|
|
}
|
|
|
|
void Server::SelectPort(void)
|
|
{
|
|
mPort = kUdpPortMin;
|
|
|
|
#if OPENTHREAD_CONFIG_SRP_SERVER_PORT_SWITCH_ENABLE
|
|
{
|
|
Settings::SrpServerInfo info;
|
|
|
|
if (Get<Settings>().Read(info) == kErrorNone)
|
|
{
|
|
mPort = info.GetPort() + 1;
|
|
if (mPort < kUdpPortMin || mPort > kUdpPortMax)
|
|
{
|
|
mPort = kUdpPortMin;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
LogInfo("Selected port %u", mPort);
|
|
}
|
|
|
|
void Server::Start(void)
|
|
{
|
|
VerifyOrExit(mState == kStateStopped);
|
|
|
|
mState = kStateRunning;
|
|
PrepareSocket();
|
|
LogInfo("Start listening on port %u", mPort);
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
void Server::PrepareSocket(void)
|
|
{
|
|
Error error = kErrorNone;
|
|
|
|
#if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
|
|
Ip6::Udp::Socket &dnsSocket = Get<Dns::ServiceDiscovery::Server>().mSocket;
|
|
|
|
if (dnsSocket.GetSockName().GetPort() == mPort)
|
|
{
|
|
// If the DNS-SD socket matches our port number, we use the
|
|
// same socket so we close our own socket (in case it was
|
|
// open). `GetSocket()` will now return the DNS-SD socket.
|
|
|
|
IgnoreError(mSocket.Close());
|
|
ExitNow();
|
|
}
|
|
#endif
|
|
|
|
VerifyOrExit(!mSocket.IsOpen());
|
|
SuccessOrExit(error = mSocket.Open(HandleUdpReceive, this));
|
|
error = mSocket.Bind(mPort, OT_NETIF_THREAD);
|
|
|
|
exit:
|
|
if (error != kErrorNone)
|
|
{
|
|
LogCrit("Failed to prepare socket: %s", ErrorToString(error));
|
|
Stop();
|
|
}
|
|
}
|
|
|
|
Ip6::Udp::Socket &Server::GetSocket(void)
|
|
{
|
|
Ip6::Udp::Socket *socket = &mSocket;
|
|
|
|
#if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
|
|
Ip6::Udp::Socket &dnsSocket = Get<Dns::ServiceDiscovery::Server>().mSocket;
|
|
|
|
if (dnsSocket.GetSockName().GetPort() == mPort)
|
|
{
|
|
socket = &dnsSocket;
|
|
}
|
|
#endif
|
|
|
|
return *socket;
|
|
}
|
|
|
|
#if OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
|
|
|
|
void Server::HandleDnssdServerStateChange(void)
|
|
{
|
|
// This is called from` Dns::ServiceDiscovery::Server` to notify
|
|
// that it has started or stopped. We check whether we need to
|
|
// share the socket.
|
|
|
|
if (mState == kStateRunning)
|
|
{
|
|
PrepareSocket();
|
|
}
|
|
}
|
|
|
|
Error Server::HandleDnssdServerUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
|
|
{
|
|
// This is called from` Dns::ServiceDiscovery::Server` when a UDP
|
|
// message is received on its socket. We check whether we are
|
|
// sharing socket and if so we process the received message. We
|
|
// return `kErrorNone` to indicate that message was successfully
|
|
// processed by `Srp::Server`, otherwise `kErrorDrop` is returned.
|
|
|
|
Error error = kErrorDrop;
|
|
|
|
VerifyOrExit((mState == kStateRunning) && !mSocket.IsOpen());
|
|
|
|
error = ProcessMessage(aMessage, aMessageInfo);
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
#endif // OPENTHREAD_CONFIG_DNSSD_SERVER_ENABLE
|
|
|
|
void Server::Stop(void)
|
|
{
|
|
VerifyOrExit(mState == kStateRunning);
|
|
|
|
mState = kStateStopped;
|
|
|
|
while (!mHosts.IsEmpty())
|
|
{
|
|
RemoveHost(mHosts.GetHead(), kDeleteName, kNotifyServiceHandler);
|
|
}
|
|
|
|
// TODO: We should cancel any outstanding service updates, but current
|
|
// OTBR mDNS publisher cannot properly handle it.
|
|
while (!mOutstandingUpdates.IsEmpty())
|
|
{
|
|
mOutstandingUpdates.Pop()->Free();
|
|
}
|
|
|
|
mLeaseTimer.Stop();
|
|
mOutstandingUpdatesTimer.Stop();
|
|
|
|
LogInfo("Stop listening on %u", mPort);
|
|
IgnoreError(mSocket.Close());
|
|
mHasRegisteredAnyService = false;
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
void Server::HandleNetDataPublisherEvent(NetworkData::Publisher::Event aEvent)
|
|
{
|
|
switch (aEvent)
|
|
{
|
|
case NetworkData::Publisher::kEventEntryAdded:
|
|
Start();
|
|
break;
|
|
|
|
case NetworkData::Publisher::kEventEntryRemoved:
|
|
Stop();
|
|
break;
|
|
}
|
|
}
|
|
|
|
const Server::UpdateMetadata *Server::FindOutstandingUpdate(const MessageMetadata &aMessageMetadata) const
|
|
{
|
|
const UpdateMetadata *ret = nullptr;
|
|
|
|
VerifyOrExit(aMessageMetadata.IsDirectRxFromClient());
|
|
|
|
for (const UpdateMetadata &update : mOutstandingUpdates)
|
|
{
|
|
if (aMessageMetadata.mDnsHeader.GetMessageId() == update.GetDnsHeader().GetMessageId() &&
|
|
aMessageMetadata.mMessageInfo->GetPeerAddr() == update.GetMessageInfo().GetPeerAddr() &&
|
|
aMessageMetadata.mMessageInfo->GetPeerPort() == update.GetMessageInfo().GetPeerPort())
|
|
{
|
|
ExitNow(ret = &update);
|
|
}
|
|
}
|
|
|
|
exit:
|
|
return ret;
|
|
}
|
|
|
|
void Server::ProcessDnsUpdate(Message &aMessage, MessageMetadata &aMetadata)
|
|
{
|
|
Error error = kErrorNone;
|
|
Host *host = nullptr;
|
|
|
|
LogInfo("Received DNS update from %s", aMetadata.IsDirectRxFromClient()
|
|
? aMetadata.mMessageInfo->GetPeerAddr().ToString().AsCString()
|
|
: "an SRPL Partner");
|
|
|
|
SuccessOrExit(error = ProcessZoneSection(aMessage, aMetadata));
|
|
|
|
if (FindOutstandingUpdate(aMetadata) != nullptr)
|
|
{
|
|
LogInfo("Drop duplicated SRP update request: MessageId=%hu", aMetadata.mDnsHeader.GetMessageId());
|
|
|
|
// Silently drop duplicate requests.
|
|
// This could rarely happen, because the outstanding SRP update timer should
|
|
// be shorter than the SRP update retransmission timer.
|
|
ExitNow(error = kErrorNone);
|
|
}
|
|
|
|
// Per 2.3.2 of SRP draft 6, no prerequisites should be included in a SRP update.
|
|
VerifyOrExit(aMetadata.mDnsHeader.GetPrerequisiteRecordCount() == 0, error = kErrorFailed);
|
|
|
|
host = Host::Allocate(GetInstance(), aMetadata.mRxTime);
|
|
VerifyOrExit(host != nullptr, error = kErrorNoBufs);
|
|
SuccessOrExit(error = ProcessUpdateSection(*host, aMessage, aMetadata));
|
|
|
|
// Parse lease time and validate signature.
|
|
SuccessOrExit(error = ProcessAdditionalSection(host, aMessage, aMetadata));
|
|
|
|
SuccessOrExit(error = ValidateServiceSubTypes(*host, aMetadata));
|
|
|
|
HandleUpdate(*host, aMetadata);
|
|
|
|
exit:
|
|
if (error != kErrorNone)
|
|
{
|
|
if (host != nullptr)
|
|
{
|
|
host->Free();
|
|
}
|
|
|
|
if (aMetadata.IsDirectRxFromClient())
|
|
{
|
|
SendResponse(aMetadata.mDnsHeader, ErrorToDnsResponseCode(error), *aMetadata.mMessageInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
Error Server::ProcessZoneSection(const Message &aMessage, MessageMetadata &aMetadata) const
|
|
{
|
|
Error error = kErrorNone;
|
|
char name[Dns::Name::kMaxNameSize];
|
|
uint16_t offset = aMetadata.mOffset;
|
|
|
|
VerifyOrExit(aMetadata.mDnsHeader.GetZoneRecordCount() == 1, error = kErrorParse);
|
|
|
|
SuccessOrExit(error = Dns::Name::ReadName(aMessage, offset, name, sizeof(name)));
|
|
// TODO: return `Dns::kResponseNotAuth` for not authorized zone names.
|
|
VerifyOrExit(StringMatch(name, GetDomain(), kStringCaseInsensitiveMatch), error = kErrorSecurity);
|
|
SuccessOrExit(error = aMessage.Read(offset, aMetadata.mDnsZone));
|
|
offset += sizeof(Dns::Zone);
|
|
|
|
VerifyOrExit(aMetadata.mDnsZone.GetType() == Dns::ResourceRecord::kTypeSoa, error = kErrorParse);
|
|
aMetadata.mOffset = offset;
|
|
|
|
exit:
|
|
if (error != kErrorNone)
|
|
{
|
|
LogWarn("Failed to process DNS Zone section: %s", ErrorToString(error));
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
Error Server::ProcessUpdateSection(Host &aHost, const Message &aMessage, MessageMetadata &aMetadata) const
|
|
{
|
|
Error error = kErrorNone;
|
|
|
|
// Process Service Discovery, Host and Service Description Instructions with
|
|
// 3 times iterations over all DNS update RRs. The order of those processes matters.
|
|
|
|
// 0. Enumerate over all Service Discovery Instructions before processing any other records.
|
|
// So that we will know whether a name is a hostname or service instance name when processing
|
|
// a "Delete All RRsets from a name" record.
|
|
SuccessOrExit(error = ProcessServiceDiscoveryInstructions(aHost, aMessage, aMetadata));
|
|
|
|
// 1. Enumerate over all RRs to build the Host Description Instruction.
|
|
SuccessOrExit(error = ProcessHostDescriptionInstruction(aHost, aMessage, aMetadata));
|
|
|
|
// 2. Enumerate over all RRs to build the Service Description Instructions.
|
|
SuccessOrExit(error = ProcessServiceDescriptionInstructions(aHost, aMessage, aMetadata));
|
|
|
|
// 3. Verify that there are no name conflicts.
|
|
VerifyOrExit(!HasNameConflictsWith(aHost), error = kErrorDuplicated);
|
|
|
|
exit:
|
|
if (error != kErrorNone)
|
|
{
|
|
LogWarn("Failed to process DNS Update section: %s", ErrorToString(error));
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
Error Server::ProcessHostDescriptionInstruction(Host & aHost,
|
|
const Message & aMessage,
|
|
const MessageMetadata &aMetadata) const
|
|
{
|
|
Error error;
|
|
uint16_t offset = aMetadata.mOffset;
|
|
|
|
OT_ASSERT(aHost.GetFullName() == nullptr);
|
|
|
|
for (uint16_t numRecords = aMetadata.mDnsHeader.GetUpdateRecordCount(); numRecords > 0; numRecords--)
|
|
{
|
|
char name[Dns::Name::kMaxNameSize];
|
|
Dns::ResourceRecord record;
|
|
|
|
SuccessOrExit(error = Dns::Name::ReadName(aMessage, offset, name, sizeof(name)));
|
|
|
|
SuccessOrExit(error = aMessage.Read(offset, record));
|
|
|
|
if (record.GetClass() == Dns::ResourceRecord::kClassAny)
|
|
{
|
|
// Delete All RRsets from a name.
|
|
VerifyOrExit(IsValidDeleteAllRecord(record), error = kErrorFailed);
|
|
|
|
// A "Delete All RRsets from a name" RR can only apply to a Service or Host Description.
|
|
|
|
if (!aHost.HasServiceInstance(name))
|
|
{
|
|
// If host name is already set to a different name, `SetFullName()`
|
|
// will return `kErrorFailed`.
|
|
SuccessOrExit(error = aHost.SetFullName(name));
|
|
aHost.ClearResources();
|
|
}
|
|
}
|
|
else if (record.GetType() == Dns::ResourceRecord::kTypeAaaa)
|
|
{
|
|
Dns::AaaaRecord aaaaRecord;
|
|
|
|
VerifyOrExit(record.GetClass() == aMetadata.mDnsZone.GetClass(), error = kErrorFailed);
|
|
|
|
SuccessOrExit(error = aHost.ProcessTtl(record.GetTtl()));
|
|
|
|
SuccessOrExit(error = aHost.SetFullName(name));
|
|
|
|
SuccessOrExit(error = aMessage.Read(offset, aaaaRecord));
|
|
VerifyOrExit(aaaaRecord.IsValid(), error = kErrorParse);
|
|
|
|
// Tolerate kErrorDrop for AAAA Resources.
|
|
VerifyOrExit(aHost.AddIp6Address(aaaaRecord.GetAddress()) != kErrorNoBufs, error = kErrorNoBufs);
|
|
}
|
|
else if (record.GetType() == Dns::ResourceRecord::kTypeKey)
|
|
{
|
|
// We currently support only ECDSA P-256.
|
|
Dns::Ecdsa256KeyRecord keyRecord;
|
|
|
|
VerifyOrExit(record.GetClass() == aMetadata.mDnsZone.GetClass(), error = kErrorFailed);
|
|
|
|
SuccessOrExit(error = aHost.ProcessTtl(record.GetTtl()));
|
|
|
|
SuccessOrExit(error = aMessage.Read(offset, keyRecord));
|
|
VerifyOrExit(keyRecord.IsValid(), error = kErrorParse);
|
|
|
|
VerifyOrExit(aHost.GetKeyRecord() == nullptr || *aHost.GetKeyRecord() == keyRecord, error = kErrorSecurity);
|
|
aHost.SetKeyRecord(keyRecord);
|
|
}
|
|
|
|
offset += record.GetSize();
|
|
}
|
|
|
|
// Verify that we have a complete Host Description Instruction.
|
|
|
|
VerifyOrExit(aHost.GetFullName() != nullptr, error = kErrorFailed);
|
|
VerifyOrExit(aHost.GetKeyRecord() != nullptr, error = kErrorFailed);
|
|
|
|
// We check the number of host addresses after processing of the
|
|
// Lease Option in the Addition Section and determining whether
|
|
// the host is being removed or registered.
|
|
|
|
exit:
|
|
if (error != kErrorNone)
|
|
{
|
|
LogWarn("Failed to process Host Description instructions: %s", ErrorToString(error));
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
Error Server::ProcessServiceDiscoveryInstructions(Host & aHost,
|
|
const Message & aMessage,
|
|
const MessageMetadata &aMetadata) const
|
|
{
|
|
Error error = kErrorNone;
|
|
uint16_t offset = aMetadata.mOffset;
|
|
|
|
for (uint16_t numRecords = aMetadata.mDnsHeader.GetUpdateRecordCount(); numRecords > 0; numRecords--)
|
|
{
|
|
char serviceName[Dns::Name::kMaxNameSize];
|
|
char instanceName[Dns::Name::kMaxNameSize];
|
|
Dns::PtrRecord ptrRecord;
|
|
const char * subServiceName;
|
|
Service * service;
|
|
bool isSubType;
|
|
|
|
SuccessOrExit(error = Dns::Name::ReadName(aMessage, offset, serviceName, sizeof(serviceName)));
|
|
VerifyOrExit(Dns::Name::IsSubDomainOf(serviceName, GetDomain()), error = kErrorSecurity);
|
|
|
|
error = Dns::ResourceRecord::ReadRecord(aMessage, offset, ptrRecord);
|
|
|
|
if (error == kErrorNotFound)
|
|
{
|
|
// `ReadRecord()` updates `aOffset` to skip over a
|
|
// non-matching record.
|
|
error = kErrorNone;
|
|
continue;
|
|
}
|
|
|
|
SuccessOrExit(error);
|
|
|
|
SuccessOrExit(error = Dns::Name::ReadName(aMessage, offset, instanceName, sizeof(instanceName)));
|
|
|
|
VerifyOrExit(ptrRecord.GetClass() == Dns::ResourceRecord::kClassNone ||
|
|
ptrRecord.GetClass() == aMetadata.mDnsZone.GetClass(),
|
|
error = kErrorFailed);
|
|
|
|
// Check if the `serviceName` is a subtype with the name
|
|
// format: "<sub-label>._sub.<service-labels>.<domain>."
|
|
|
|
subServiceName = StringFind(serviceName, kServiceSubTypeLabel, kStringCaseInsensitiveMatch);
|
|
isSubType = (subServiceName != nullptr);
|
|
|
|
if (isSubType)
|
|
{
|
|
// Skip over the "._sub." label to get to the base
|
|
// service name.
|
|
subServiceName += sizeof(kServiceSubTypeLabel) - 1;
|
|
}
|
|
|
|
// Verify that instance name and service name are related.
|
|
|
|
VerifyOrExit(
|
|
StringEndsWith(instanceName, isSubType ? subServiceName : serviceName, kStringCaseInsensitiveMatch),
|
|
error = kErrorFailed);
|
|
|
|
// Ensure the same service does not exist already.
|
|
VerifyOrExit(aHost.FindService(serviceName, instanceName) == nullptr, error = kErrorFailed);
|
|
|
|
service = aHost.AddNewService(serviceName, instanceName, isSubType, aMetadata.mRxTime);
|
|
VerifyOrExit(service != nullptr, error = kErrorNoBufs);
|
|
|
|
// This RR is a "Delete an RR from an RRset" update when the CLASS is NONE.
|
|
service->mIsDeleted = (ptrRecord.GetClass() == Dns::ResourceRecord::kClassNone);
|
|
|
|
if (!service->mIsDeleted)
|
|
{
|
|
SuccessOrExit(error = aHost.ProcessTtl(ptrRecord.GetTtl()));
|
|
}
|
|
}
|
|
|
|
exit:
|
|
if (error != kErrorNone)
|
|
{
|
|
LogWarn("Failed to process Service Discovery instructions: %s", ErrorToString(error));
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
Error Server::ProcessServiceDescriptionInstructions(Host & aHost,
|
|
const Message & aMessage,
|
|
MessageMetadata &aMetadata) const
|
|
{
|
|
Error error = kErrorNone;
|
|
uint16_t offset = aMetadata.mOffset;
|
|
|
|
for (uint16_t numRecords = aMetadata.mDnsHeader.GetUpdateRecordCount(); numRecords > 0; numRecords--)
|
|
{
|
|
RetainPtr<Service::Description> desc;
|
|
char name[Dns::Name::kMaxNameSize];
|
|
Dns::ResourceRecord record;
|
|
|
|
SuccessOrExit(error = Dns::Name::ReadName(aMessage, offset, name, sizeof(name)));
|
|
SuccessOrExit(error = aMessage.Read(offset, record));
|
|
|
|
if (record.GetClass() == Dns::ResourceRecord::kClassAny)
|
|
{
|
|
// Delete All RRsets from a name.
|
|
VerifyOrExit(IsValidDeleteAllRecord(record), error = kErrorFailed);
|
|
|
|
desc = aHost.FindServiceDescription(name);
|
|
|
|
if (desc != nullptr)
|
|
{
|
|
desc->ClearResources();
|
|
desc->mUpdateTime = aMetadata.mRxTime;
|
|
}
|
|
|
|
offset += record.GetSize();
|
|
continue;
|
|
}
|
|
|
|
if (record.GetType() == Dns::ResourceRecord::kTypeSrv)
|
|
{
|
|
Dns::SrvRecord srvRecord;
|
|
char hostName[Dns::Name::kMaxNameSize];
|
|
uint16_t hostNameLength = sizeof(hostName);
|
|
|
|
VerifyOrExit(record.GetClass() == aMetadata.mDnsZone.GetClass(), error = kErrorFailed);
|
|
|
|
SuccessOrExit(error = aHost.ProcessTtl(record.GetTtl()));
|
|
|
|
SuccessOrExit(error = aMessage.Read(offset, srvRecord));
|
|
offset += sizeof(srvRecord);
|
|
|
|
SuccessOrExit(error = Dns::Name::ReadName(aMessage, offset, hostName, hostNameLength));
|
|
VerifyOrExit(Dns::Name::IsSubDomainOf(name, GetDomain()), error = kErrorSecurity);
|
|
VerifyOrExit(aHost.Matches(hostName), error = kErrorFailed);
|
|
|
|
desc = aHost.FindServiceDescription(name);
|
|
VerifyOrExit(desc != nullptr, error = kErrorFailed);
|
|
|
|
// Make sure that this is the first SRV RR for this service description
|
|
VerifyOrExit(desc->mPort == 0, error = kErrorFailed);
|
|
desc->mTtl = srvRecord.GetTtl();
|
|
desc->mPriority = srvRecord.GetPriority();
|
|
desc->mWeight = srvRecord.GetWeight();
|
|
desc->mPort = srvRecord.GetPort();
|
|
desc->mUpdateTime = aMetadata.mRxTime;
|
|
}
|
|
else if (record.GetType() == Dns::ResourceRecord::kTypeTxt)
|
|
{
|
|
VerifyOrExit(record.GetClass() == aMetadata.mDnsZone.GetClass(), error = kErrorFailed);
|
|
|
|
SuccessOrExit(error = aHost.ProcessTtl(record.GetTtl()));
|
|
|
|
desc = aHost.FindServiceDescription(name);
|
|
VerifyOrExit(desc != nullptr, error = kErrorFailed);
|
|
|
|
offset += sizeof(record);
|
|
SuccessOrExit(error = desc->SetTxtDataFromMessage(aMessage, offset, record.GetLength()));
|
|
offset += record.GetLength();
|
|
}
|
|
else
|
|
{
|
|
offset += record.GetSize();
|
|
}
|
|
}
|
|
|
|
// Verify that all service descriptions on `aHost` are updated. Note
|
|
// that `mUpdateTime` on a new `Service::Description` is set to
|
|
// `GetNow().GetDistantPast()`.
|
|
|
|
for (Service &service : aHost.mServices)
|
|
{
|
|
VerifyOrExit(service.mDescription->mUpdateTime == aMetadata.mRxTime, error = kErrorFailed);
|
|
|
|
// Check that either both `mPort` and `mTxtData` are set
|
|
// (i.e., we saw both SRV and TXT record) or both are default
|
|
// (cleared) value (i.e., we saw neither of them).
|
|
|
|
VerifyOrExit((service.mDescription->mPort == 0) == service.mDescription->mTxtData.IsNull(),
|
|
error = kErrorFailed);
|
|
}
|
|
|
|
aMetadata.mOffset = offset;
|
|
|
|
exit:
|
|
if (error != kErrorNone)
|
|
{
|
|
LogWarn("Failed to process Service Description instructions: %s", ErrorToString(error));
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
bool Server::IsValidDeleteAllRecord(const Dns::ResourceRecord &aRecord)
|
|
{
|
|
return aRecord.GetClass() == Dns::ResourceRecord::kClassAny && aRecord.GetType() == Dns::ResourceRecord::kTypeAny &&
|
|
aRecord.GetTtl() == 0 && aRecord.GetLength() == 0;
|
|
}
|
|
|
|
Error Server::ProcessAdditionalSection(Host *aHost, const Message &aMessage, MessageMetadata &aMetadata) const
|
|
{
|
|
Error error = kErrorNone;
|
|
Dns::OptRecord optRecord;
|
|
Dns::LeaseOption leaseOption;
|
|
Dns::SigRecord sigRecord;
|
|
char name[2]; // The root domain name (".") is expected.
|
|
uint16_t offset = aMetadata.mOffset;
|
|
uint16_t sigOffset;
|
|
uint16_t sigRdataOffset;
|
|
char signerName[Dns::Name::kMaxNameSize];
|
|
uint16_t signatureLength;
|
|
|
|
VerifyOrExit(aMetadata.mDnsHeader.GetAdditionalRecordCount() == 2, error = kErrorFailed);
|
|
|
|
// EDNS(0) Update Lease Option.
|
|
|
|
SuccessOrExit(error = Dns::Name::ReadName(aMessage, offset, name, sizeof(name)));
|
|
SuccessOrExit(error = aMessage.Read(offset, optRecord));
|
|
SuccessOrExit(error = aMessage.Read(offset + sizeof(optRecord), leaseOption));
|
|
VerifyOrExit(leaseOption.IsValid(), error = kErrorFailed);
|
|
VerifyOrExit(optRecord.GetSize() == sizeof(optRecord) + sizeof(leaseOption), error = kErrorParse);
|
|
|
|
offset += optRecord.GetSize();
|
|
|
|
aHost->SetLease(leaseOption.GetLeaseInterval());
|
|
aHost->SetKeyLease(leaseOption.GetKeyLeaseInterval());
|
|
|
|
if (aHost->GetLease() > 0)
|
|
{
|
|
uint8_t hostAddressesNum;
|
|
|
|
aHost->GetAddresses(hostAddressesNum);
|
|
|
|
// There MUST be at least one valid address if we have nonzero lease.
|
|
VerifyOrExit(hostAddressesNum > 0, error = kErrorFailed);
|
|
}
|
|
|
|
// SIG(0).
|
|
|
|
sigOffset = offset;
|
|
SuccessOrExit(error = Dns::Name::ReadName(aMessage, offset, name, sizeof(name)));
|
|
SuccessOrExit(error = aMessage.Read(offset, sigRecord));
|
|
VerifyOrExit(sigRecord.IsValid(), error = kErrorParse);
|
|
|
|
sigRdataOffset = offset + sizeof(Dns::ResourceRecord);
|
|
offset += sizeof(sigRecord);
|
|
|
|
// TODO: Verify that the signature doesn't expire. This is not
|
|
// implemented because the end device may not be able to get
|
|
// the synchronized date/time.
|
|
|
|
SuccessOrExit(error = Dns::Name::ReadName(aMessage, offset, signerName, sizeof(signerName)));
|
|
|
|
signatureLength = sigRecord.GetLength() - (offset - sigRdataOffset);
|
|
offset += signatureLength;
|
|
|
|
// Verify the signature. Currently supports only ECDSA.
|
|
|
|
VerifyOrExit(sigRecord.GetAlgorithm() == Dns::KeyRecord::kAlgorithmEcdsaP256Sha256, error = kErrorFailed);
|
|
VerifyOrExit(sigRecord.GetTypeCovered() == 0, error = kErrorFailed);
|
|
VerifyOrExit(signatureLength == Crypto::Ecdsa::P256::Signature::kSize, error = kErrorParse);
|
|
|
|
SuccessOrExit(error = VerifySignature(*aHost->GetKeyRecord(), aMessage, aMetadata.mDnsHeader, sigOffset,
|
|
sigRdataOffset, sigRecord.GetLength(), signerName));
|
|
|
|
aMetadata.mOffset = offset;
|
|
|
|
exit:
|
|
if (error != kErrorNone)
|
|
{
|
|
LogWarn("Failed to process DNS Additional section: %s", ErrorToString(error));
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
Error Server::VerifySignature(const Dns::Ecdsa256KeyRecord &aKeyRecord,
|
|
const Message & aMessage,
|
|
Dns::UpdateHeader aDnsHeader,
|
|
uint16_t aSigOffset,
|
|
uint16_t aSigRdataOffset,
|
|
uint16_t aSigRdataLength,
|
|
const char * aSignerName) const
|
|
{
|
|
Error error;
|
|
uint16_t offset = aMessage.GetOffset();
|
|
uint16_t signatureOffset;
|
|
Crypto::Sha256 sha256;
|
|
Crypto::Sha256::Hash hash;
|
|
Crypto::Ecdsa::P256::Signature signature;
|
|
Message * signerNameMessage = nullptr;
|
|
|
|
VerifyOrExit(aSigRdataLength >= Crypto::Ecdsa::P256::Signature::kSize, error = kErrorInvalidArgs);
|
|
|
|
sha256.Start();
|
|
|
|
// SIG RDATA less signature.
|
|
sha256.Update(aMessage, aSigRdataOffset, sizeof(Dns::SigRecord) - sizeof(Dns::ResourceRecord));
|
|
|
|
// The uncompressed (canonical) form of the signer name should be used for signature
|
|
// verification. See https://tools.ietf.org/html/rfc2931#section-3.1 for details.
|
|
signerNameMessage = Get<Ip6::Udp>().NewMessage(0);
|
|
VerifyOrExit(signerNameMessage != nullptr, error = kErrorNoBufs);
|
|
SuccessOrExit(error = Dns::Name::AppendName(aSignerName, *signerNameMessage));
|
|
sha256.Update(*signerNameMessage, signerNameMessage->GetOffset(), signerNameMessage->GetLength());
|
|
|
|
// We need the DNS header before appending the SIG RR.
|
|
aDnsHeader.SetAdditionalRecordCount(aDnsHeader.GetAdditionalRecordCount() - 1);
|
|
sha256.Update(aDnsHeader);
|
|
sha256.Update(aMessage, offset + sizeof(aDnsHeader), aSigOffset - offset - sizeof(aDnsHeader));
|
|
|
|
sha256.Finish(hash);
|
|
|
|
signatureOffset = aSigRdataOffset + aSigRdataLength - Crypto::Ecdsa::P256::Signature::kSize;
|
|
SuccessOrExit(error = aMessage.Read(signatureOffset, signature));
|
|
|
|
error = aKeyRecord.GetKey().Verify(hash, signature);
|
|
|
|
exit:
|
|
if (error != kErrorNone)
|
|
{
|
|
LogWarn("Failed to verify message signature: %s", ErrorToString(error));
|
|
}
|
|
|
|
FreeMessage(signerNameMessage);
|
|
return error;
|
|
}
|
|
|
|
Error Server::ValidateServiceSubTypes(Host &aHost, const MessageMetadata &aMetadata)
|
|
{
|
|
Error error = kErrorNone;
|
|
Host *existingHost;
|
|
|
|
// Verify that there is a matching base type service for all
|
|
// sub-type services in `aHost` (which is from the received
|
|
// and parsed SRP Update message).
|
|
|
|
for (const Service &service : aHost.GetServices())
|
|
{
|
|
if (service.IsSubType() && (aHost.FindBaseService(service.GetInstanceName()) == nullptr))
|
|
{
|
|
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_WARN)
|
|
char subLabel[Dns::Name::kMaxLabelSize];
|
|
|
|
IgnoreError(service.GetServiceSubTypeLabel(subLabel, sizeof(subLabel)));
|
|
LogWarn("Message contains instance %s with subtype %s without base type", service.GetInstanceName(),
|
|
subLabel);
|
|
#endif
|
|
|
|
ExitNow(error = kErrorParse);
|
|
}
|
|
}
|
|
|
|
// SRP server must treat the update instructions for a service type
|
|
// and all its sub-types as atomic, i.e., when a service and its
|
|
// sub-types are being updated, whatever information appears in the
|
|
// SRP Update is the entirety of information about that service and
|
|
// its sub-types. Any previously registered sub-type that does not
|
|
// appear in a new SRP Update, must be removed.
|
|
//
|
|
// We go though the list of registered services for the same host
|
|
// and if the base service is included in the new SRP Update
|
|
// message, we add any previously registered service sub-type that
|
|
// does not appear in new Update message as "deleted".
|
|
|
|
existingHost = mHosts.FindMatching(aHost.GetFullName());
|
|
VerifyOrExit(existingHost != nullptr);
|
|
|
|
for (const Service &baseService : existingHost->GetServices())
|
|
{
|
|
if (baseService.IsSubType() || (aHost.FindBaseService(baseService.GetInstanceName()) == nullptr))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
for (const Service &subService : existingHost->GetServices())
|
|
{
|
|
if (!subService.IsSubType() || !subService.MatchesInstanceName(baseService.GetInstanceName()))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
SuccessOrExit(error = aHost.AddCopyOfServiceAsDeletedIfNotPresent(subService, aMetadata.mRxTime));
|
|
}
|
|
}
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
void Server::HandleUpdate(Host &aHost, const MessageMetadata &aMetadata)
|
|
{
|
|
Error error = kErrorNone;
|
|
Host *existingHost;
|
|
|
|
// Check whether the SRP update wants to remove `aHost`.
|
|
|
|
VerifyOrExit(aHost.GetLease() == 0);
|
|
|
|
aHost.ClearResources();
|
|
|
|
existingHost = mHosts.FindMatching(aHost.GetFullName());
|
|
VerifyOrExit(existingHost != nullptr);
|
|
|
|
// The client may not include all services it has registered before
|
|
// when removing a host. We copy and append any missing services to
|
|
// `aHost` from the `existingHost` and mark them as deleted.
|
|
|
|
for (Service &service : existingHost->mServices)
|
|
{
|
|
if (service.mIsDeleted)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
SuccessOrExit(error = aHost.AddCopyOfServiceAsDeletedIfNotPresent(service, aMetadata.mRxTime));
|
|
}
|
|
|
|
exit:
|
|
InformUpdateHandlerOrCommit(error, aHost, aMetadata);
|
|
}
|
|
|
|
void Server::InformUpdateHandlerOrCommit(Error aError, Host &aHost, const MessageMetadata &aMetadata)
|
|
{
|
|
if ((aError == kErrorNone) && (mServiceUpdateHandler != nullptr))
|
|
{
|
|
UpdateMetadata *update = UpdateMetadata::Allocate(GetInstance(), aHost, aMetadata);
|
|
|
|
if (update != nullptr)
|
|
{
|
|
mOutstandingUpdates.Push(*update);
|
|
mOutstandingUpdatesTimer.FireAtIfEarlier(update->GetExpireTime());
|
|
|
|
LogInfo("SRP update handler is notified (updatedId = %u)", update->GetId());
|
|
mServiceUpdateHandler(update->GetId(), &aHost, kDefaultEventsHandlerTimeout, mServiceUpdateHandlerContext);
|
|
ExitNow();
|
|
}
|
|
|
|
aError = kErrorNoBufs;
|
|
}
|
|
|
|
CommitSrpUpdate(aError, aHost, aMetadata);
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
void Server::SendResponse(const Dns::UpdateHeader & aHeader,
|
|
Dns::UpdateHeader::Response aResponseCode,
|
|
const Ip6::MessageInfo & aMessageInfo)
|
|
{
|
|
Error error;
|
|
Message * response = nullptr;
|
|
Dns::UpdateHeader header;
|
|
|
|
response = GetSocket().NewMessage(0);
|
|
VerifyOrExit(response != nullptr, error = kErrorNoBufs);
|
|
|
|
header.SetMessageId(aHeader.GetMessageId());
|
|
header.SetType(Dns::UpdateHeader::kTypeResponse);
|
|
header.SetQueryType(aHeader.GetQueryType());
|
|
header.SetResponseCode(aResponseCode);
|
|
SuccessOrExit(error = response->Append(header));
|
|
|
|
SuccessOrExit(error = GetSocket().SendTo(*response, aMessageInfo));
|
|
|
|
if (aResponseCode != Dns::UpdateHeader::kResponseSuccess)
|
|
{
|
|
LogWarn("Send fail response: %d", aResponseCode);
|
|
}
|
|
else
|
|
{
|
|
LogInfo("Send success response");
|
|
}
|
|
|
|
UpdateResponseCounters(aResponseCode);
|
|
|
|
exit:
|
|
if (error != kErrorNone)
|
|
{
|
|
LogWarn("Failed to send response: %s", ErrorToString(error));
|
|
FreeMessage(response);
|
|
}
|
|
}
|
|
|
|
void Server::SendResponse(const Dns::UpdateHeader &aHeader,
|
|
uint32_t aLease,
|
|
uint32_t aKeyLease,
|
|
const Ip6::MessageInfo & aMessageInfo)
|
|
{
|
|
Error error;
|
|
Message * response = nullptr;
|
|
Dns::UpdateHeader header;
|
|
Dns::OptRecord optRecord;
|
|
Dns::LeaseOption leaseOption;
|
|
|
|
response = GetSocket().NewMessage(0);
|
|
VerifyOrExit(response != nullptr, error = kErrorNoBufs);
|
|
|
|
header.SetMessageId(aHeader.GetMessageId());
|
|
header.SetType(Dns::UpdateHeader::kTypeResponse);
|
|
header.SetQueryType(aHeader.GetQueryType());
|
|
header.SetResponseCode(Dns::UpdateHeader::kResponseSuccess);
|
|
header.SetAdditionalRecordCount(1);
|
|
SuccessOrExit(error = response->Append(header));
|
|
|
|
// Append the root domain (".").
|
|
SuccessOrExit(error = Dns::Name::AppendTerminator(*response));
|
|
|
|
optRecord.Init();
|
|
optRecord.SetUdpPayloadSize(kUdpPayloadSize);
|
|
optRecord.SetDnsSecurityFlag();
|
|
optRecord.SetLength(sizeof(Dns::LeaseOption));
|
|
SuccessOrExit(error = response->Append(optRecord));
|
|
|
|
leaseOption.Init();
|
|
leaseOption.SetLeaseInterval(aLease);
|
|
leaseOption.SetKeyLeaseInterval(aKeyLease);
|
|
SuccessOrExit(error = response->Append(leaseOption));
|
|
|
|
SuccessOrExit(error = GetSocket().SendTo(*response, aMessageInfo));
|
|
|
|
LogInfo("Send success response with granted lease: %u and key lease: %u", aLease, aKeyLease);
|
|
|
|
UpdateResponseCounters(Dns::UpdateHeader::kResponseSuccess);
|
|
|
|
exit:
|
|
if (error != kErrorNone)
|
|
{
|
|
LogWarn("Failed to send response: %s", ErrorToString(error));
|
|
FreeMessage(response);
|
|
}
|
|
}
|
|
|
|
void Server::HandleUdpReceive(void *aContext, otMessage *aMessage, const otMessageInfo *aMessageInfo)
|
|
{
|
|
static_cast<Server *>(aContext)->HandleUdpReceive(AsCoreType(aMessage), AsCoreType(aMessageInfo));
|
|
}
|
|
|
|
void Server::HandleUdpReceive(Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
|
|
{
|
|
Error error = ProcessMessage(aMessage, aMessageInfo);
|
|
|
|
if (error != kErrorNone)
|
|
{
|
|
LogInfo("Failed to handle DNS message: %s", ErrorToString(error));
|
|
}
|
|
}
|
|
|
|
Error Server::ProcessMessage(Message &aMessage, const Ip6::MessageInfo &aMessageInfo)
|
|
{
|
|
return ProcessMessage(aMessage, TimerMilli::GetNow(), mTtlConfig, mLeaseConfig, &aMessageInfo);
|
|
}
|
|
|
|
Error Server::ProcessMessage(Message & aMessage,
|
|
TimeMilli aRxTime,
|
|
const TtlConfig & aTtlConfig,
|
|
const LeaseConfig & aLeaseConfig,
|
|
const Ip6::MessageInfo *aMessageInfo)
|
|
{
|
|
Error error;
|
|
MessageMetadata metadata;
|
|
|
|
metadata.mOffset = aMessage.GetOffset();
|
|
metadata.mRxTime = aRxTime;
|
|
metadata.mTtlConfig = aTtlConfig;
|
|
metadata.mLeaseConfig = aLeaseConfig;
|
|
metadata.mMessageInfo = aMessageInfo;
|
|
|
|
SuccessOrExit(error = aMessage.Read(metadata.mOffset, metadata.mDnsHeader));
|
|
metadata.mOffset += sizeof(Dns::UpdateHeader);
|
|
|
|
VerifyOrExit(metadata.mDnsHeader.GetType() == Dns::UpdateHeader::Type::kTypeQuery, error = kErrorDrop);
|
|
VerifyOrExit(metadata.mDnsHeader.GetQueryType() == Dns::UpdateHeader::kQueryTypeUpdate, error = kErrorDrop);
|
|
|
|
ProcessDnsUpdate(aMessage, metadata);
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
void Server::HandleLeaseTimer(Timer &aTimer)
|
|
{
|
|
aTimer.Get<Server>().HandleLeaseTimer();
|
|
}
|
|
|
|
void Server::HandleLeaseTimer(void)
|
|
{
|
|
TimeMilli now = TimerMilli::GetNow();
|
|
TimeMilli earliestExpireTime = now.GetDistantFuture();
|
|
Host * nextHost;
|
|
|
|
for (Host *host = mHosts.GetHead(); host != nullptr; host = nextHost)
|
|
{
|
|
nextHost = host->GetNext();
|
|
|
|
if (host->GetKeyExpireTime() <= now)
|
|
{
|
|
LogInfo("KEY LEASE of host %s expired", host->GetFullName());
|
|
|
|
// Removes the whole host and all services if the KEY RR expired.
|
|
RemoveHost(host, kDeleteName, kNotifyServiceHandler);
|
|
}
|
|
else if (host->IsDeleted())
|
|
{
|
|
// The host has been deleted, but the hostname & service instance names retain.
|
|
|
|
Service *next;
|
|
|
|
earliestExpireTime = OT_MIN(earliestExpireTime, host->GetKeyExpireTime());
|
|
|
|
// Check if any service instance name expired.
|
|
for (Service *service = host->mServices.GetHead(); service != nullptr; service = next)
|
|
{
|
|
next = service->GetNext();
|
|
|
|
OT_ASSERT(service->mIsDeleted);
|
|
|
|
if (service->GetKeyExpireTime() <= now)
|
|
{
|
|
service->Log(Service::kKeyLeaseExpired);
|
|
host->RemoveService(service, kDeleteName, kNotifyServiceHandler);
|
|
}
|
|
else
|
|
{
|
|
earliestExpireTime = OT_MIN(earliestExpireTime, service->GetKeyExpireTime());
|
|
}
|
|
}
|
|
}
|
|
else if (host->GetExpireTime() <= now)
|
|
{
|
|
LogInfo("LEASE of host %s expired", host->GetFullName());
|
|
|
|
// If the host expired, delete all resources of this host and its services.
|
|
for (Service &service : host->mServices)
|
|
{
|
|
// Don't need to notify the service handler as `RemoveHost` at below will do.
|
|
host->RemoveService(&service, kRetainName, kDoNotNotifyServiceHandler);
|
|
}
|
|
|
|
RemoveHost(host, kRetainName, kNotifyServiceHandler);
|
|
|
|
earliestExpireTime = OT_MIN(earliestExpireTime, host->GetKeyExpireTime());
|
|
}
|
|
else
|
|
{
|
|
// The host doesn't expire, check if any service expired or is explicitly removed.
|
|
|
|
Service *next;
|
|
|
|
OT_ASSERT(!host->IsDeleted());
|
|
|
|
earliestExpireTime = OT_MIN(earliestExpireTime, host->GetExpireTime());
|
|
|
|
for (Service *service = host->mServices.GetHead(); service != nullptr; service = next)
|
|
{
|
|
next = service->GetNext();
|
|
|
|
if (service->GetKeyExpireTime() <= now)
|
|
{
|
|
service->Log(Service::kKeyLeaseExpired);
|
|
host->RemoveService(service, kDeleteName, kNotifyServiceHandler);
|
|
}
|
|
else if (service->mIsDeleted)
|
|
{
|
|
// The service has been deleted but the name retains.
|
|
earliestExpireTime = OT_MIN(earliestExpireTime, service->GetKeyExpireTime());
|
|
}
|
|
else if (service->GetExpireTime() <= now)
|
|
{
|
|
service->Log(Service::kLeaseExpired);
|
|
|
|
// The service is expired, delete it.
|
|
host->RemoveService(service, kRetainName, kNotifyServiceHandler);
|
|
earliestExpireTime = OT_MIN(earliestExpireTime, service->GetKeyExpireTime());
|
|
}
|
|
else
|
|
{
|
|
earliestExpireTime = OT_MIN(earliestExpireTime, service->GetExpireTime());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (earliestExpireTime != now.GetDistantFuture())
|
|
{
|
|
OT_ASSERT(earliestExpireTime >= now);
|
|
if (!mLeaseTimer.IsRunning() || earliestExpireTime <= mLeaseTimer.GetFireTime())
|
|
{
|
|
LogInfo("Lease timer is scheduled for %u seconds", Time::MsecToSec(earliestExpireTime - now));
|
|
mLeaseTimer.StartAt(earliestExpireTime, 0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LogInfo("Lease timer is stopped");
|
|
mLeaseTimer.Stop();
|
|
}
|
|
}
|
|
|
|
void Server::HandleOutstandingUpdatesTimer(Timer &aTimer)
|
|
{
|
|
aTimer.Get<Server>().HandleOutstandingUpdatesTimer();
|
|
}
|
|
|
|
void Server::HandleOutstandingUpdatesTimer(void)
|
|
{
|
|
while (!mOutstandingUpdates.IsEmpty() && mOutstandingUpdates.GetTail()->GetExpireTime() <= TimerMilli::GetNow())
|
|
{
|
|
LogInfo("Outstanding service update timeout (updateId = %u)", mOutstandingUpdates.GetTail()->GetId());
|
|
HandleServiceUpdateResult(mOutstandingUpdates.GetTail(), kErrorResponseTimeout);
|
|
}
|
|
}
|
|
|
|
const char *Server::AddressModeToString(AddressMode aMode)
|
|
{
|
|
static const char *const kAddressModeStrings[] = {
|
|
"unicast", // (0) kAddressModeUnicast
|
|
"anycast", // (1) kAddressModeAnycast
|
|
};
|
|
|
|
static_assert(kAddressModeUnicast == 0, "kAddressModeUnicast value is incorrect");
|
|
static_assert(kAddressModeAnycast == 1, "kAddressModeAnycast value is incorrect");
|
|
|
|
return kAddressModeStrings[aMode];
|
|
}
|
|
|
|
void Server::UpdateResponseCounters(Dns::UpdateHeader::Response aResponseCode)
|
|
{
|
|
switch (aResponseCode)
|
|
{
|
|
case Dns::UpdateHeader::kResponseSuccess:
|
|
++mResponseCounters.mSuccess;
|
|
break;
|
|
case Dns::UpdateHeader::kResponseServerFailure:
|
|
++mResponseCounters.mServerFailure;
|
|
break;
|
|
case Dns::UpdateHeader::kResponseFormatError:
|
|
++mResponseCounters.mFormatError;
|
|
break;
|
|
case Dns::UpdateHeader::kResponseNameExists:
|
|
++mResponseCounters.mNameExists;
|
|
break;
|
|
case Dns::UpdateHeader::kResponseRefused:
|
|
++mResponseCounters.mRefused;
|
|
break;
|
|
default:
|
|
++mResponseCounters.mOther;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------------
|
|
// Server::Service
|
|
|
|
Error Server::Service::Init(const char *aServiceName, Description &aDescription, bool aIsSubType, TimeMilli aUpdateTime)
|
|
{
|
|
mDescription.Reset(&aDescription);
|
|
mNext = nullptr;
|
|
mUpdateTime = aUpdateTime;
|
|
mIsDeleted = false;
|
|
mIsSubType = aIsSubType;
|
|
mIsCommitted = false;
|
|
|
|
return mServiceName.Set(aServiceName);
|
|
}
|
|
|
|
Error Server::Service::GetServiceSubTypeLabel(char *aLabel, uint8_t aMaxSize) const
|
|
{
|
|
Error error = kErrorNone;
|
|
const char *serviceName = GetServiceName();
|
|
const char *subServiceName;
|
|
uint8_t labelLength;
|
|
|
|
memset(aLabel, 0, aMaxSize);
|
|
|
|
VerifyOrExit(IsSubType(), error = kErrorInvalidArgs);
|
|
|
|
subServiceName = StringFind(serviceName, kServiceSubTypeLabel, kStringCaseInsensitiveMatch);
|
|
OT_ASSERT(subServiceName != nullptr);
|
|
|
|
if (subServiceName - serviceName < aMaxSize)
|
|
{
|
|
labelLength = static_cast<uint8_t>(subServiceName - serviceName);
|
|
}
|
|
else
|
|
{
|
|
labelLength = aMaxSize - 1;
|
|
error = kErrorNoBufs;
|
|
}
|
|
|
|
memcpy(aLabel, serviceName, labelLength);
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
TimeMilli Server::Service::GetExpireTime(void) const
|
|
{
|
|
OT_ASSERT(!mIsDeleted);
|
|
OT_ASSERT(!GetHost().IsDeleted());
|
|
|
|
return mUpdateTime + Time::SecToMsec(mDescription->mLease);
|
|
}
|
|
|
|
TimeMilli Server::Service::GetKeyExpireTime(void) const
|
|
{
|
|
return mUpdateTime + Time::SecToMsec(mDescription->mKeyLease);
|
|
}
|
|
|
|
void Server::Service::GetLeaseInfo(LeaseInfo &aLeaseInfo) const
|
|
{
|
|
TimeMilli now = TimerMilli::GetNow();
|
|
TimeMilli expireTime = GetExpireTime();
|
|
TimeMilli keyExpireTime = GetKeyExpireTime();
|
|
|
|
aLeaseInfo.mLease = Time::SecToMsec(GetLease());
|
|
aLeaseInfo.mKeyLease = Time::SecToMsec(GetKeyLease());
|
|
aLeaseInfo.mRemainingLease = (now <= expireTime) ? (expireTime - now) : 0;
|
|
aLeaseInfo.mRemainingKeyLease = (now <= keyExpireTime) ? (keyExpireTime - now) : 0;
|
|
}
|
|
|
|
bool Server::Service::MatchesInstanceName(const char *aInstanceName) const
|
|
{
|
|
return StringMatch(mDescription->mInstanceName.AsCString(), aInstanceName, kStringCaseInsensitiveMatch);
|
|
}
|
|
|
|
bool Server::Service::MatchesServiceName(const char *aServiceName) const
|
|
{
|
|
return StringMatch(mServiceName.AsCString(), aServiceName, kStringCaseInsensitiveMatch);
|
|
}
|
|
|
|
bool Server::Service::MatchesFlags(Flags aFlags) const
|
|
{
|
|
bool matches = false;
|
|
|
|
if (IsSubType())
|
|
{
|
|
VerifyOrExit(aFlags & kFlagSubType);
|
|
}
|
|
else
|
|
{
|
|
VerifyOrExit(aFlags & kFlagBaseType);
|
|
}
|
|
|
|
if (IsDeleted())
|
|
{
|
|
VerifyOrExit(aFlags & kFlagDeleted);
|
|
}
|
|
else
|
|
{
|
|
VerifyOrExit(aFlags & kFlagActive);
|
|
}
|
|
|
|
matches = true;
|
|
|
|
exit:
|
|
return matches;
|
|
}
|
|
|
|
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
|
|
void Server::Service::Log(Action aAction) const
|
|
{
|
|
static const char *const kActionStrings[] = {
|
|
"Add new", // (0) kAddNew
|
|
"Update existing", // (1) kUpdateExisting
|
|
"Remove but retain name of", // (2) kRemoveButRetainName
|
|
"Fully remove", // (3) kFullyRemove
|
|
"LEASE expired for ", // (4) kLeaseExpired
|
|
"KEY LEASE expired for", // (5) kKeyLeaseExpired
|
|
};
|
|
|
|
char subLabel[Dns::Name::kMaxLabelSize];
|
|
|
|
static_assert(0 == kAddNew, "kAddNew value is incorrect");
|
|
static_assert(1 == kUpdateExisting, "kUpdateExisting value is incorrect");
|
|
static_assert(2 == kRemoveButRetainName, "kRemoveButRetainName value is incorrect");
|
|
static_assert(3 == kFullyRemove, "kFullyRemove value is incorrect");
|
|
static_assert(4 == kLeaseExpired, "kLeaseExpired value is incorrect");
|
|
static_assert(5 == kKeyLeaseExpired, "kKeyLeaseExpired value is incorrect");
|
|
|
|
// We only log if the `Service` is marked as committed. This
|
|
// ensures that temporary `Service` entries associated with a
|
|
// newly received SRP update message are not logged (e.g., when
|
|
// associated `Host` is being freed).
|
|
|
|
if (mIsCommitted)
|
|
{
|
|
IgnoreError(GetServiceSubTypeLabel(subLabel, sizeof(subLabel)));
|
|
|
|
LogInfo("%s service '%s'%s%s", kActionStrings[aAction], GetInstanceName(), IsSubType() ? " subtype:" : "",
|
|
subLabel);
|
|
}
|
|
}
|
|
#else
|
|
void Server::Service::Log(Action) const
|
|
{
|
|
}
|
|
#endif // #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
|
|
|
|
//---------------------------------------------------------------------------------------------------------------------
|
|
// Server::Service::Description
|
|
|
|
Error Server::Service::Description::Init(const char *aInstanceName, Host &aHost)
|
|
{
|
|
mNext = nullptr;
|
|
mHost = &aHost;
|
|
mPriority = 0;
|
|
mWeight = 0;
|
|
mTtl = 0;
|
|
mPort = 0;
|
|
mLease = 0;
|
|
mKeyLease = 0;
|
|
mUpdateTime = TimerMilli::GetNow().GetDistantPast();
|
|
mTxtData.Free();
|
|
|
|
return mInstanceName.Set(aInstanceName);
|
|
}
|
|
|
|
bool Server::Service::Description::Matches(const char *aInstanceName) const
|
|
{
|
|
return StringMatch(mInstanceName.AsCString(), aInstanceName, kStringCaseInsensitiveMatch);
|
|
}
|
|
|
|
void Server::Service::Description::ClearResources(void)
|
|
{
|
|
mPort = 0;
|
|
mTxtData.Free();
|
|
}
|
|
|
|
void Server::Service::Description::TakeResourcesFrom(Description &aDescription)
|
|
{
|
|
mTxtData.SetFrom(static_cast<Heap::Data &&>(aDescription.mTxtData));
|
|
|
|
mPriority = aDescription.mPriority;
|
|
mWeight = aDescription.mWeight;
|
|
mPort = aDescription.mPort;
|
|
|
|
mTtl = aDescription.mTtl;
|
|
mLease = aDescription.mLease;
|
|
mKeyLease = aDescription.mKeyLease;
|
|
mUpdateTime = TimerMilli::GetNow();
|
|
}
|
|
|
|
Error Server::Service::Description::SetTxtDataFromMessage(const Message &aMessage, uint16_t aOffset, uint16_t aLength)
|
|
{
|
|
Error error;
|
|
|
|
SuccessOrExit(error = mTxtData.SetFrom(aMessage, aOffset, aLength));
|
|
VerifyOrExit(Dns::TxtRecord::VerifyTxtData(mTxtData.GetBytes(), mTxtData.GetLength(), /* aAllowEmpty */ false),
|
|
error = kErrorParse);
|
|
|
|
exit:
|
|
if (error != kErrorNone)
|
|
{
|
|
mTxtData.Free();
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------------
|
|
// Server::Host
|
|
|
|
Server::Host::Host(Instance &aInstance, TimeMilli aUpdateTime)
|
|
: InstanceLocator(aInstance)
|
|
, mNext(nullptr)
|
|
, mTtl(0)
|
|
, mLease(0)
|
|
, mKeyLease(0)
|
|
, mUpdateTime(aUpdateTime)
|
|
{
|
|
mKeyRecord.Clear();
|
|
}
|
|
|
|
Server::Host::~Host(void)
|
|
{
|
|
FreeAllServices();
|
|
}
|
|
|
|
Error Server::Host::SetFullName(const char *aFullName)
|
|
{
|
|
// `mFullName` becomes immutable after it is set, so if it is
|
|
// already set, we only accept a `aFullName` that matches the
|
|
// current name.
|
|
|
|
Error error;
|
|
|
|
if (mFullName.IsNull())
|
|
{
|
|
error = mFullName.Set(aFullName);
|
|
}
|
|
else
|
|
{
|
|
error = Matches(aFullName) ? kErrorNone : kErrorFailed;
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
bool Server::Host::Matches(const char *aFullName) const
|
|
{
|
|
return StringMatch(mFullName.AsCString(), aFullName, kStringCaseInsensitiveMatch);
|
|
}
|
|
|
|
void Server::Host::SetKeyRecord(Dns::Ecdsa256KeyRecord &aKeyRecord)
|
|
{
|
|
OT_ASSERT(aKeyRecord.IsValid());
|
|
|
|
mKeyRecord = aKeyRecord;
|
|
}
|
|
|
|
TimeMilli Server::Host::GetExpireTime(void) const
|
|
{
|
|
OT_ASSERT(!IsDeleted());
|
|
|
|
return mUpdateTime + Time::SecToMsec(mLease);
|
|
}
|
|
|
|
TimeMilli Server::Host::GetKeyExpireTime(void) const
|
|
{
|
|
return mUpdateTime + Time::SecToMsec(mKeyLease);
|
|
}
|
|
|
|
void Server::Host::GetLeaseInfo(LeaseInfo &aLeaseInfo) const
|
|
{
|
|
TimeMilli now = TimerMilli::GetNow();
|
|
TimeMilli expireTime = GetExpireTime();
|
|
TimeMilli keyExpireTime = GetKeyExpireTime();
|
|
|
|
aLeaseInfo.mLease = Time::SecToMsec(GetLease());
|
|
aLeaseInfo.mKeyLease = Time::SecToMsec(GetKeyLease());
|
|
aLeaseInfo.mRemainingLease = (now <= expireTime) ? (expireTime - now) : 0;
|
|
aLeaseInfo.mRemainingKeyLease = (now <= keyExpireTime) ? (keyExpireTime - now) : 0;
|
|
}
|
|
|
|
Error Server::Host::ProcessTtl(uint32_t aTtl)
|
|
{
|
|
// This method processes the TTL value received in a resource record.
|
|
//
|
|
// If no TTL value is stored, this method wil set the stored value to @p aTtl and return `kErrorNone`.
|
|
// If a TTL value is stored and @p aTtl equals the stored value, this method returns `kErrorNone`.
|
|
// Otherwise, this method returns `kErrorRejected`.
|
|
|
|
Error error = kErrorRejected;
|
|
|
|
VerifyOrExit(aTtl && (mTtl == 0 || mTtl == aTtl));
|
|
|
|
mTtl = aTtl;
|
|
|
|
error = kErrorNone;
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
const Server::Service *Server::Host::FindNextService(const Service *aPrevService,
|
|
Service::Flags aFlags,
|
|
const char * aServiceName,
|
|
const char * aInstanceName) const
|
|
{
|
|
const Service *service = (aPrevService == nullptr) ? GetServices().GetHead() : aPrevService->GetNext();
|
|
|
|
for (; service != nullptr; service = service->GetNext())
|
|
{
|
|
if (!service->MatchesFlags(aFlags))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ((aServiceName != nullptr) && !service->MatchesServiceName(aServiceName))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ((aInstanceName != nullptr) && !service->MatchesInstanceName(aInstanceName))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return service;
|
|
}
|
|
|
|
Server::Service *Server::Host::AddNewService(const char *aServiceName,
|
|
const char *aInstanceName,
|
|
bool aIsSubType,
|
|
TimeMilli aUpdateTime)
|
|
{
|
|
Service * service = nullptr;
|
|
RetainPtr<Service::Description> desc(FindServiceDescription(aInstanceName));
|
|
|
|
if (desc == nullptr)
|
|
{
|
|
desc.Reset(Service::Description::AllocateAndInit(aInstanceName, *this));
|
|
VerifyOrExit(desc != nullptr);
|
|
}
|
|
|
|
service = Service::AllocateAndInit(aServiceName, *desc, aIsSubType, aUpdateTime);
|
|
VerifyOrExit(service != nullptr);
|
|
|
|
mServices.Push(*service);
|
|
|
|
exit:
|
|
return service;
|
|
}
|
|
|
|
void Server::Host::RemoveService(Service *aService, RetainName aRetainName, NotifyMode aNotifyServiceHandler)
|
|
{
|
|
Server &server = Get<Server>();
|
|
|
|
VerifyOrExit(aService != nullptr);
|
|
|
|
aService->mIsDeleted = true;
|
|
|
|
aService->Log(aRetainName ? Service::kRemoveButRetainName : Service::kFullyRemove);
|
|
|
|
if (aNotifyServiceHandler && server.mServiceUpdateHandler != nullptr)
|
|
{
|
|
uint32_t updateId = server.AllocateId();
|
|
|
|
LogInfo("SRP update handler is notified (updatedId = %u)", updateId);
|
|
server.mServiceUpdateHandler(updateId, this, kDefaultEventsHandlerTimeout, server.mServiceUpdateHandlerContext);
|
|
// We don't wait for the reply from the service update handler,
|
|
// but always remove the service regardless of service update result.
|
|
// Because removing a service should fail only when there is system
|
|
// failure of the platform mDNS implementation and in which case the
|
|
// service is not expected to be still registered.
|
|
}
|
|
|
|
if (!aRetainName)
|
|
{
|
|
IgnoreError(mServices.Remove(*aService));
|
|
aService->Free();
|
|
}
|
|
|
|
exit:
|
|
return;
|
|
}
|
|
|
|
Error Server::Host::AddCopyOfServiceAsDeletedIfNotPresent(const Service &aService, TimeMilli aUpdateTime)
|
|
{
|
|
Error error = kErrorNone;
|
|
Service *newService;
|
|
|
|
VerifyOrExit(FindService(aService.GetServiceName(), aService.GetInstanceName()) == nullptr);
|
|
|
|
newService =
|
|
AddNewService(aService.GetServiceName(), aService.GetInstanceName(), aService.IsSubType(), aUpdateTime);
|
|
|
|
VerifyOrExit(newService != nullptr, error = kErrorNoBufs);
|
|
|
|
newService->mDescription->mUpdateTime = aUpdateTime;
|
|
newService->mIsDeleted = true;
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
void Server::Host::FreeAllServices(void)
|
|
{
|
|
while (!mServices.IsEmpty())
|
|
{
|
|
RemoveService(mServices.GetHead(), kDeleteName, kDoNotNotifyServiceHandler);
|
|
}
|
|
}
|
|
|
|
void Server::Host::ClearResources(void)
|
|
{
|
|
mAddresses.Free();
|
|
}
|
|
|
|
Error Server::Host::MergeServicesAndResourcesFrom(Host &aHost)
|
|
{
|
|
// This method merges services, service descriptions, and other
|
|
// resources from another `aHost` into current host. It can
|
|
// possibly take ownership of some items from `aHost`.
|
|
|
|
Error error = kErrorNone;
|
|
|
|
LogInfo("Update host %s", GetFullName());
|
|
|
|
mAddresses.TakeFrom(static_cast<Heap::Array<Ip6::Address> &&>(aHost.mAddresses));
|
|
mKeyRecord = aHost.mKeyRecord;
|
|
mTtl = aHost.mTtl;
|
|
mLease = aHost.mLease;
|
|
mKeyLease = aHost.mKeyLease;
|
|
mUpdateTime = TimerMilli::GetNow();
|
|
|
|
for (Service &service : aHost.mServices)
|
|
{
|
|
Service *existingService = FindService(service.GetServiceName(), service.GetInstanceName());
|
|
Service *newService;
|
|
|
|
if (service.mIsDeleted)
|
|
{
|
|
// `RemoveService()` does nothing if `exitsingService` is `nullptr`.
|
|
RemoveService(existingService, kRetainName, kDoNotNotifyServiceHandler);
|
|
continue;
|
|
}
|
|
|
|
// Add/Merge `service` into the existing service or a allocate a new one
|
|
|
|
newService = (existingService != nullptr) ? existingService
|
|
: AddNewService(service.GetServiceName(), service.GetInstanceName(),
|
|
service.IsSubType(), service.GetUpdateTime());
|
|
|
|
VerifyOrExit(newService != nullptr, error = kErrorNoBufs);
|
|
|
|
newService->mIsDeleted = false;
|
|
newService->mIsCommitted = true;
|
|
newService->mUpdateTime = TimerMilli::GetNow();
|
|
|
|
if (!service.mIsSubType)
|
|
{
|
|
// (1) Service description is shared across a base type and all its subtypes.
|
|
// (2) `TakeResourcesFrom()` releases resources pinned to its argument.
|
|
// Therefore, make sure the function is called only for the base type.
|
|
newService->mDescription->TakeResourcesFrom(*service.mDescription);
|
|
}
|
|
|
|
newService->Log((existingService != nullptr) ? Service::kUpdateExisting : Service::kAddNew);
|
|
}
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
bool Server::Host::HasServiceInstance(const char *aInstanceName) const
|
|
{
|
|
return (FindServiceDescription(aInstanceName) != nullptr);
|
|
}
|
|
|
|
const RetainPtr<Server::Service::Description> Server::Host::FindServiceDescription(const char *aInstanceName) const
|
|
{
|
|
const Service::Description *desc = nullptr;
|
|
|
|
for (const Service &service : mServices)
|
|
{
|
|
if (service.mDescription->Matches(aInstanceName))
|
|
{
|
|
desc = service.mDescription.Get();
|
|
break;
|
|
}
|
|
}
|
|
|
|
return RetainPtr<Service::Description>(AsNonConst(desc));
|
|
}
|
|
|
|
RetainPtr<Server::Service::Description> Server::Host::FindServiceDescription(const char *aInstanceName)
|
|
{
|
|
return AsNonConst(AsConst(this)->FindServiceDescription(aInstanceName));
|
|
}
|
|
|
|
const Server::Service *Server::Host::FindService(const char *aServiceName, const char *aInstanceName) const
|
|
{
|
|
return FindNextService(/* aPrevService */ nullptr, kFlagsAnyService, aServiceName, aInstanceName);
|
|
}
|
|
|
|
Server::Service *Server::Host::FindService(const char *aServiceName, const char *aInstanceName)
|
|
{
|
|
return AsNonConst(AsConst(this)->FindService(aServiceName, aInstanceName));
|
|
}
|
|
|
|
const Server::Service *Server::Host::FindBaseService(const char *aInstanceName) const
|
|
{
|
|
return FindNextService(/*a PrevService */ nullptr, kFlagsBaseTypeServiceOnly, /* aServiceName */ nullptr,
|
|
aInstanceName);
|
|
}
|
|
|
|
Server::Service *Server::Host::FindBaseService(const char *aInstanceName)
|
|
{
|
|
return AsNonConst(AsConst(this)->FindBaseService(aInstanceName));
|
|
}
|
|
|
|
Error Server::Host::AddIp6Address(const Ip6::Address &aIp6Address)
|
|
{
|
|
Error error = kErrorNone;
|
|
|
|
if (aIp6Address.IsMulticast() || aIp6Address.IsUnspecified() || aIp6Address.IsLoopback())
|
|
{
|
|
// We don't like those address because they cannot be used
|
|
// for communication with exterior devices.
|
|
ExitNow(error = kErrorDrop);
|
|
}
|
|
|
|
// Drop duplicate addresses.
|
|
VerifyOrExit(!mAddresses.Contains(aIp6Address), error = kErrorDrop);
|
|
|
|
error = mAddresses.PushBack(aIp6Address);
|
|
|
|
if (error == kErrorNoBufs)
|
|
{
|
|
LogWarn("Too many addresses for host %s", GetFullName());
|
|
}
|
|
|
|
exit:
|
|
return error;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------------------------------------------------
|
|
// Server::UpdateMetadata
|
|
|
|
Server::UpdateMetadata::UpdateMetadata(Instance &aInstance, Host &aHost, const MessageMetadata &aMessageMetadata)
|
|
: InstanceLocator(aInstance)
|
|
, mNext(nullptr)
|
|
, mExpireTime(TimerMilli::GetNow() + kDefaultEventsHandlerTimeout)
|
|
, mDnsHeader(aMessageMetadata.mDnsHeader)
|
|
, mId(Get<Server>().AllocateId())
|
|
, mTtlConfig(aMessageMetadata.mTtlConfig)
|
|
, mLeaseConfig(aMessageMetadata.mLeaseConfig)
|
|
, mHost(aHost)
|
|
, mIsDirectRxFromClient(aMessageMetadata.IsDirectRxFromClient())
|
|
{
|
|
if (aMessageMetadata.mMessageInfo != nullptr)
|
|
{
|
|
mMessageInfo = *aMessageMetadata.mMessageInfo;
|
|
}
|
|
}
|
|
|
|
} // namespace Srp
|
|
} // namespace ot
|
|
|
|
#endif // OPENTHREAD_CONFIG_SRP_SERVER_ENABLE
|