blob: 2a57c9217ebad89d307795fa9861b7b649727675 [file] [log] [blame]
/*
* 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/num_utils.hpp"
#include "common/random.hpp"
#include "common/string.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)
, mLeaseTimer(aInstance)
, mOutstandingUpdatesTimer(aInstance)
, mCompletedUpdateTask(aInstance)
, mServiceUpdateId(Random::NonCrypto::GetUint32())
, mPort(kUdpPortMin)
, mState(kStateDisabled)
, mAddressMode(kDefaultAddressMode)
, mAnycastSequenceNumber(0)
, mHasRegisteredAnyService(false)
#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
, mAutoEnable(false)
#endif
{
IgnoreError(SetDomain(kDefaultDomain));
}
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 OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
mAutoEnable = false;
#endif
if (aEnabled)
{
Enable();
}
else
{
Disable();
}
}
void Server::Enable(void)
{
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;
}
exit:
return;
}
void Server::Disable(void)
{
VerifyOrExit(mState != kStateDisabled);
Get<NetworkData::Publisher>().UnpublishDnsSrpService();
Stop();
mState = kStateDisabled;
exit:
return;
}
#if OPENTHREAD_CONFIG_BORDER_ROUTING_ENABLE
void Server::SetAutoEnableMode(bool aEnabled)
{
VerifyOrExit(mAutoEnable != aEnabled);
mAutoEnable = aEnabled;
Get<BorderRouter::RoutingManager>().HandleSrpServerAutoEnableMode();
exit:
return;
}
#endif
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 Clamp(Min(aTtl, aLease), mMinTtl, mMaxTtl);
}
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 : Clamp(aLease, mMinLease, mMaxLease);
}
uint32_t Server::LeaseConfig::GrantKeyLease(uint32_t aKeyLease) const
{
OT_ASSERT(mMinKeyLease <= mMaxKeyLease);
return (aKeyLease == 0) ? 0 : Clamp(aKeyLease, mMinKeyLease, mMaxKeyLease);
}
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();
}
void Server::RemoveHost(Host *aHost, RetainName aRetainName)
{
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 (mServiceUpdateHandler.IsSet())
{
uint32_t updateId = AllocateId();
LogInfo("SRP update handler is notified (updatedId = %lu)", ToUlong(updateId));
mServiceUpdateHandler.Invoke(updateId, aHost, static_cast<uint32_t>(kDefaultEventsHandlerTimeout));
// 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.mKey != existingHost->mKey))
{
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.HasService(service.GetInstanceName()) && (aHost.mKey != host.mKey))
{
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.RemoveMatching(aId);
if (update == nullptr)
{
LogInfo("Delayed SRP host update result, the SRP update has been committed (updateId = %lu)", ToUlong(aId));
ExitNow();
}
update->SetError(aError);
LogInfo("Handler result of SRP update (id = %lu) is received: %s", ToUlong(update->GetId()), ErrorToString(aError));
// We add new `update` at the tail of the `mCompletedUpdates` list
// so that updates are processed in the order we receive the
// `HandleServiceUpdateResult()` callbacks for them. The
// completed updates are processed from `mCompletedUpdateTask`
// and `ProcessCompletedUpdates()`.
mCompletedUpdates.PushAfterTail(*update);
mCompletedUpdateTask.Post();
if (mOutstandingUpdates.IsEmpty())
{
mOutstandingUpdatesTimer.Stop();
}
else
{
mOutstandingUpdatesTimer.FireAt(mOutstandingUpdates.GetTail()->GetExpireTime());
}
exit:
return;
}
void Server::ProcessCompletedUpdates(void)
{
UpdateMetadata *update;
while ((update = mCompletedUpdates.Pop()) != nullptr)
{
CommitSrpUpdate(*update);
}
}
void Server::CommitSrpUpdate(Error aError, Host &aHost, const MessageMetadata &aMessageMetadata)
{
CommitSrpUpdate(aError, aHost, aMessageMetadata.mDnsHeader, aMessageMetadata.mMessageInfo,
aMessageMetadata.mTtlConfig, aMessageMetadata.mLeaseConfig);
}
void Server::CommitSrpUpdate(UpdateMetadata &aUpdateMetadata)
{
CommitSrpUpdate(aUpdateMetadata.GetError(), aUpdateMetadata.GetHost(), aUpdateMetadata.GetDnsHeader(),
aUpdateMetadata.IsDirectRxFromClient() ? &aUpdateMetadata.GetMessageInfo() : nullptr,
aUpdateMetadata.GetTtlConfig(), aUpdateMetadata.GetLeaseConfig());
aUpdateMetadata.Free();
}
void Server::CommitSrpUpdate(Error aError,
Host &aHost,
const Dns::UpdateHeader &aDnsHeader,
const Ip6::MessageInfo *aMessageInfo,
const TtlConfig &aTtlConfig,
const LeaseConfig &aLeaseConfig)
{
Host *existingHost = nullptr;
uint32_t grantedTtl = 0;
uint32_t hostLease = 0;
uint32_t hostKeyLease = 0;
uint32_t grantedLease = 0;
uint32_t grantedKeyLease = 0;
bool useShortLease = aHost.ShouldUseShortLeaseOption();
if (aError != kErrorNone)
{
aHost.Free();
ExitNow();
}
hostLease = aHost.GetLease();
hostKeyLease = aHost.GetKeyLease();
grantedLease = aLeaseConfig.GrantLease(hostLease);
grantedKeyLease = useShortLease ? grantedLease : aLeaseConfig.GrantKeyLease(hostKeyLease);
grantedTtl = aTtlConfig.GrantTtl(grantedLease, aHost.GetTtl());
existingHost = mHosts.RemoveMatching(aHost.GetFullName());
LogInfo("Committing update for %s host %s", (existingHost != nullptr) ? "existing" : "new", aHost.GetFullName());
LogInfo(" Granted lease:%lu, key-lease:%lu, ttl:%lu", ToUlong(grantedLease), ToUlong(grantedKeyLease),
ToUlong(grantedTtl));
aHost.SetLease(grantedLease);
aHost.SetKeyLease(grantedKeyLease);
aHost.SetTtl(grantedTtl);
if (grantedKeyLease == 0)
{
VerifyOrExit(existingHost != nullptr);
LogInfo("Fully remove host %s", aHost.GetFullName());
aHost.Free();
ExitNow();
}
mHosts.Push(aHost);
for (Service &service : aHost.mServices)
{
service.mLease = grantedLease;
service.mKeyLease = grantedKeyLease;
service.mTtl = grantedTtl;
service.mIsCommitted = true;
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
{
Service::Action action = Service::kAddNew;
if (service.mIsDeleted)
{
action = Service::kRemoveButRetainName;
}
else if ((existingHost != nullptr) && existingHost->HasService(service.GetInstanceName()))
{
action = Service::kUpdateExisting;
}
service.Log(action);
}
#endif
}
if (existingHost != nullptr)
{
// Move any existing service that is not included in the new
// update into `aHost`.
Service *existingService;
while ((existingService = existingHost->mServices.Pop()) != nullptr)
{
if (!aHost.HasService(existingService->GetInstanceName()))
{
aHost.AddService(*existingService);
existingService->Log(Service::kKeepUnchanged);
}
else
{
existingService->Free();
}
}
}
#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
if (!aHost.IsDeleted())
{
mLeaseTimer.FireAtIfEarlier(Min(aHost.GetExpireTime(), aHost.GetKeyExpireTime()));
}
exit:
if (aMessageInfo != nullptr)
{
if (aError == kErrorNone && !(grantedLease == hostLease && grantedKeyLease == hostKeyLease))
{
SendResponse(aDnsHeader, grantedLease, grantedKeyLease, useShortLease, *aMessageInfo);
}
else
{
SendResponse(aDnsHeader, ErrorToDnsResponseCode(aError), *aMessageInfo);
}
}
if (existingHost != nullptr)
{
existingHost->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, Ip6::kNetifThread);
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);
}
// 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=%u", 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));
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 = kErrorNone;
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.HasService(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);
if (aHost.mParsedKey)
{
VerifyOrExit(aHost.mKey == keyRecord.GetKey(), error = kErrorSecurity);
}
else
{
aHost.mParsedKey = true;
aHost.mKey = keyRecord.GetKey();
}
}
offset += record.GetSize();
}
// Verify that we have a complete Host Description Instruction.
VerifyOrExit(aHost.GetFullName() != nullptr, error = kErrorFailed);
VerifyOrExit(aHost.mParsedKey, 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 instanceLabel[Dns::Name::kMaxLabelSize];
char instanceServiceName[Dns::Name::kMaxNameSize];
String<Dns::Name::kMaxNameSize> instanceName;
Dns::PtrRecord ptrRecord;
const char *subServiceName;
Service *service;
bool isSubType;
bool isDelete;
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 = ptrRecord.ReadPtrName(aMessage, offset, instanceLabel, sizeof(instanceLabel),
instanceServiceName, sizeof(instanceServiceName)));
instanceName.Append("%s.%s", instanceLabel, instanceServiceName);
// Class None indicates "Delete an RR from an RRset".
isDelete = (ptrRecord.GetClass() == Dns::ResourceRecord::kClassNone);
VerifyOrExit(isDelete || ptrRecord.GetClass() == aMetadata.mDnsZone.GetClass(), error = kErrorParse);
// Check if the `serviceName` is a sub-type with 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(Dns::Name::IsSubDomainOf(instanceName.AsCString(), isSubType ? subServiceName : serviceName),
error = kErrorFailed);
// Find a matching existing service or allocate a new one.
service = aHost.FindService(instanceName.AsCString());
if (service == nullptr)
{
service = aHost.AddNewService(instanceName.AsCString(), instanceLabel, aMetadata.mRxTime);
VerifyOrExit(service != nullptr, error = kErrorNoBufs);
}
if (isSubType)
{
VerifyOrExit(!service->HasSubTypeServiceName(serviceName), error = kErrorFailed);
// Ignore a sub-type service delete.
if (!isDelete)
{
Heap::String *newSubTypeLabel = service->mSubTypes.PushBack();
VerifyOrExit(newSubTypeLabel != nullptr, error = kErrorNoBufs);
SuccessOrExit(error = newSubTypeLabel->Set(serviceName));
}
}
else
{
// Processed PTR record is the base service (not a
// sub-type). `mServiceName` is only set when base
// service is processed.
VerifyOrExit(service->mServiceName.IsNull(), error = kErrorFailed);
SuccessOrExit(error = service->mServiceName.Set(serviceName));
service->mIsDeleted = isDelete;
}
if (!isDelete)
{
SuccessOrExit(error = aHost.ProcessTtl(ptrRecord.GetTtl()));
}
}
// Verify that for all services, a PTR record was processed for
// the base service (`mServiceName` is set), and for a deleted
// service, no PTR record was seen adding a sub-type.
for (const Service &service : aHost.mServices)
{
VerifyOrExit(!service.mServiceName.IsNull(), error = kErrorParse);
if (service.mIsDeleted)
{
VerifyOrExit(service.mSubTypes.GetLength() == 0, error = kErrorParse);
}
}
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--)
{
char name[Dns::Name::kMaxNameSize];
Dns::ResourceRecord record;
Service *service;
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);
service = aHost.FindService(name);
if (service != nullptr)
{
VerifyOrExit(!service->mParsedDeleteAllRrset);
service->mParsedDeleteAllRrset = true;
}
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);
service = aHost.FindService(name);
VerifyOrExit(service != nullptr, error = kErrorFailed);
VerifyOrExit(!service->mParsedSrv, error = kErrorParse);
service->mParsedSrv = true;
service->mTtl = srvRecord.GetTtl();
service->mPriority = srvRecord.GetPriority();
service->mWeight = srvRecord.GetWeight();
service->mPort = srvRecord.GetPort();
}
else if (record.GetType() == Dns::ResourceRecord::kTypeTxt)
{
VerifyOrExit(record.GetClass() == aMetadata.mDnsZone.GetClass(), error = kErrorFailed);
SuccessOrExit(error = aHost.ProcessTtl(record.GetTtl()));
service = aHost.FindService(name);
VerifyOrExit(service != nullptr, error = kErrorFailed);
service->mParsedTxt = true;
offset += sizeof(record);
SuccessOrExit(error = service->SetTxtDataFromMessage(aMessage, offset, record.GetLength()));
offset += record.GetLength();
}
else
{
offset += record.GetSize();
}
}
for (const Service &service : aHost.mServices)
{
VerifyOrExit(service.mParsedDeleteAllRrset, error = kErrorFailed);
VerifyOrExit(service.mParsedSrv == service.mParsedTxt, error = kErrorFailed);
if (!service.mIsDeleted)
{
VerifyOrExit(service.mParsedSrv, 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 = leaseOption.ReadFrom(aMessage, offset + sizeof(optRecord), optRecord.GetLength()));
offset += optRecord.GetSize();
aHost->SetLease(leaseOption.GetLeaseInterval());
aHost->SetKeyLease(leaseOption.GetKeyLeaseInterval());
// If the client included the short variant of Lease Option,
// server must also use the short variant in its response.
aHost->SetUseShortLeaseOption(leaseOption.IsShortVariant());
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->mKey, 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 Host::Key &aKey,
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();
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 = aKey.Verify(hash, signature);
exit:
if (error != kErrorNone)
{
LogWarn("Failed to verify message signature: %s", ErrorToString(error));
}
FreeMessage(signerNameMessage);
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 (const Service &existingService : existingHost->mServices)
{
Service *service;
if (existingService.mIsDeleted || aHost.HasService(existingService.GetInstanceName()))
{
continue;
}
service = aHost.AddNewService(existingService.GetInstanceName(), existingService.GetInstanceLabel(),
aMetadata.mRxTime);
VerifyOrExit(service != nullptr, error = kErrorNoBufs);
SuccessOrExit(error = service->mServiceName.Set(existingService.GetServiceName()));
service->mIsDeleted = true;
}
exit:
InformUpdateHandlerOrCommit(error, aHost, aMetadata);
}
void Server::InformUpdateHandlerOrCommit(Error aError, Host &aHost, const MessageMetadata &aMetadata)
{
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
if (aError == kErrorNone)
{
uint8_t numAddrs;
const Ip6::Address *addrs;
LogInfo("Processed SRP update info");
LogInfo(" Host:%s", aHost.GetFullName());
LogInfo(" Lease:%lu, key-lease:%lu, ttl:%lu", ToUlong(aHost.GetLease()), ToUlong(aHost.GetKeyLease()),
ToUlong(aHost.GetTtl()));
addrs = aHost.GetAddresses(numAddrs);
if (numAddrs == 0)
{
LogInfo(" No host address");
}
else
{
LogInfo(" %d host address(es):", numAddrs);
for (; numAddrs > 0; addrs++, numAddrs--)
{
LogInfo(" %s", addrs->ToString().AsCString());
}
}
for (const Service &service : aHost.mServices)
{
LogInfo(" %s service '%s'", service.IsDeleted() ? "Deleting" : "Adding", service.GetInstanceName());
for (const Heap::String &subType : service.mSubTypes)
{
char label[Dns::Name::kMaxLabelSize];
IgnoreError(Service::ParseSubTypeServiceName(subType.AsCString(), label, sizeof(label)));
LogInfo(" sub-type: %s", label);
}
}
}
else
{
LogInfo("Error %s processing received SRP update", ErrorToString(aError));
}
#endif // OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
if ((aError == kErrorNone) && mServiceUpdateHandler.IsSet())
{
UpdateMetadata *update = UpdateMetadata::Allocate(GetInstance(), aHost, aMetadata);
if (update != nullptr)
{
mOutstandingUpdates.Push(*update);
mOutstandingUpdatesTimer.FireAtIfEarlier(update->GetExpireTime());
LogInfo("SRP update handler is notified (updatedId = %lu)", ToUlong(update->GetId()));
mServiceUpdateHandler.Invoke(update->GetId(), &aHost, static_cast<uint32_t>(kDefaultEventsHandlerTimeout));
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();
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,
bool mUseShortLeaseOption,
const Ip6::MessageInfo &aMessageInfo)
{
Error error;
Message *response = nullptr;
Dns::UpdateHeader header;
Dns::OptRecord optRecord;
Dns::LeaseOption leaseOption;
uint16_t optionSize;
response = GetSocket().NewMessage();
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();
if (mUseShortLeaseOption)
{
leaseOption.InitAsShortVariant(aLease);
}
else
{
leaseOption.InitAsLongVariant(aLease, aKeyLease);
}
optionSize = static_cast<uint16_t>(leaseOption.GetSize());
optRecord.SetLength(optionSize);
SuccessOrExit(error = response->Append(optRecord));
SuccessOrExit(error = response->AppendBytes(&leaseOption, optionSize));
SuccessOrExit(error = GetSocket().SendTo(*response, aMessageInfo));
LogInfo("Send success response with granted lease: %lu and key lease: %lu", ToUlong(aLease), ToUlong(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(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);
}
else if (host->IsDeleted())
{
// The host has been deleted, but the hostname & service instance names retain.
Service *next;
earliestExpireTime = 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 = 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);
earliestExpireTime = 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 = 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 = 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 = Min(earliestExpireTime, service->GetKeyExpireTime());
}
else
{
earliestExpireTime = Min(earliestExpireTime, service->GetExpireTime());
}
}
}
}
if (earliestExpireTime != now.GetDistantFuture())
{
OT_ASSERT(earliestExpireTime >= now);
if (!mLeaseTimer.IsRunning() || earliestExpireTime <= mLeaseTimer.GetFireTime())
{
LogInfo("Lease timer is scheduled for %lu seconds", ToUlong(Time::MsecToSec(earliestExpireTime - now)));
mLeaseTimer.StartAt(earliestExpireTime, 0);
}
}
else
{
LogInfo("Lease timer is stopped");
mLeaseTimer.Stop();
}
}
void Server::HandleOutstandingUpdatesTimer(void)
{
TimeMilli now = TimerMilli::GetNow();
UpdateMetadata *update;
while ((update = mOutstandingUpdates.GetTail()) != nullptr)
{
if (update->GetExpireTime() > now)
{
mOutstandingUpdatesTimer.FireAtIfEarlier(update->GetExpireTime());
break;
}
LogInfo("Outstanding service update timeout (updateId = %lu)", ToUlong(update->GetId()));
IgnoreError(mOutstandingUpdates.Remove(*update));
update->SetError(kErrorResponseTimeout);
CommitSrpUpdate(*update);
}
}
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 *aInstanceName, const char *aInstanceLabel, Host &aHost, TimeMilli aUpdateTime)
{
Error error;
mNext = nullptr;
mHost = &aHost;
mPriority = 0;
mWeight = 0;
mTtl = 0;
mPort = 0;
mLease = 0;
mKeyLease = 0;
mUpdateTime = aUpdateTime;
mIsDeleted = false;
mIsCommitted = false;
mParsedDeleteAllRrset = false;
mParsedSrv = false;
mParsedTxt = false;
SuccessOrExit(error = mInstanceLabel.Set(aInstanceLabel));
error = mInstanceName.Set(aInstanceName);
exit:
return error;
}
const char *Server::Service::GetSubTypeServiceNameAt(uint16_t aIndex) const
{
const Heap::String *subType = mSubTypes.At(aIndex);
return (subType == nullptr) ? nullptr : subType->AsCString();
}
Error Server::Service::ParseSubTypeServiceName(const char *aSubTypeServiceName, char *aLabel, uint8_t aLabelSize)
{
Error error = kErrorNone;
const char *subPos;
uint8_t labelLength;
aLabel[0] = kNullChar;
subPos = StringFind(aSubTypeServiceName, kServiceSubTypeLabel, kStringCaseInsensitiveMatch);
VerifyOrExit(subPos != nullptr, error = kErrorInvalidArgs);
if (subPos - aSubTypeServiceName < aLabelSize)
{
labelLength = static_cast<uint8_t>(subPos - aSubTypeServiceName);
}
else
{
labelLength = aLabelSize - 1;
error = kErrorNoBufs;
}
memcpy(aLabel, aSubTypeServiceName, labelLength);
aLabel[labelLength] = kNullChar;
exit:
return error;
}
TimeMilli Server::Service::GetExpireTime(void) const
{
OT_ASSERT(!mIsDeleted);
OT_ASSERT(!GetHost().IsDeleted());
return mUpdateTime + Time::SecToMsec(mLease);
}
TimeMilli Server::Service::GetKeyExpireTime(void) const { return mUpdateTime + Time::SecToMsec(mKeyLease); }
void Server::Service::GetLeaseInfo(LeaseInfo &aLeaseInfo) const
{
TimeMilli now = TimerMilli::GetNow();
TimeMilli keyExpireTime = GetKeyExpireTime();
aLeaseInfo.mLease = Time::SecToMsec(GetLease());
aLeaseInfo.mKeyLease = Time::SecToMsec(GetKeyLease());
aLeaseInfo.mRemainingKeyLease = (now <= keyExpireTime) ? (keyExpireTime - now) : 0;
if (!mIsDeleted)
{
TimeMilli expireTime = GetExpireTime();
aLeaseInfo.mRemainingLease = (now <= expireTime) ? (expireTime - now) : 0;
}
else
{
aLeaseInfo.mRemainingLease = 0;
}
}
bool Server::Service::MatchesInstanceName(const char *aInstanceName) const { return Matches(aInstanceName); }
bool Server::Service::MatchesServiceName(const char *aServiceName) const
{
return StringMatch(mServiceName.AsCString(), aServiceName, kStringCaseInsensitiveMatch);
}
bool Server::Service::Matches(const char *aInstanceName) const
{
return StringMatch(mInstanceName.AsCString(), aInstanceName, kStringCaseInsensitiveMatch);
}
bool Server::Service::HasSubTypeServiceName(const char *aSubTypeServiceName) const
{
bool has = false;
for (const Heap::String &subType : mSubTypes)
{
if (StringMatch(subType.AsCString(), aSubTypeServiceName, kStringCaseInsensitiveMatch))
{
has = true;
break;
}
}
return has;
}
#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
"Keep unchanged", // (2) kKeepUnchanged
"Remove but retain name of", // (3) kRemoveButRetainName
"Fully remove", // (4) kFullyRemove
"LEASE expired for", // (5) kLeaseExpired
"KEY LEASE expired for", // (6) kKeyLeaseExpired
};
static_assert(0 == kAddNew, "kAddNew value is incorrect");
static_assert(1 == kUpdateExisting, "kUpdateExisting value is incorrect");
static_assert(2 == kKeepUnchanged, "kKeepUnchanged value is incorrect");
static_assert(3 == kRemoveButRetainName, "kRemoveButRetainName value is incorrect");
static_assert(4 == kFullyRemove, "kFullyRemove value is incorrect");
static_assert(5 == kLeaseExpired, "kLeaseExpired value is incorrect");
static_assert(6 == 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)
{
LogInfo("%s service '%s'", kActionStrings[aAction], GetInstanceName());
for (const Heap::String &subType : mSubTypes)
{
char label[Dns::Name::kMaxLabelSize];
IgnoreError(ParseSubTypeServiceName(subType.AsCString(), label, sizeof(label)));
LogInfo(" sub-type: %s", subType.AsCString());
}
}
}
#else
void Server::Service::Log(Action) const {}
#endif // #if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_INFO)
Error Server::Service::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)
, mParsedKey(false)
, mUseShortLeaseOption(false)
{
}
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);
}
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 keyExpireTime = GetKeyExpireTime();
aLeaseInfo.mLease = Time::SecToMsec(GetLease());
aLeaseInfo.mKeyLease = Time::SecToMsec(GetKeyLease());
aLeaseInfo.mRemainingKeyLease = (now <= keyExpireTime) ? (keyExpireTime - now) : 0;
if (!IsDeleted())
{
TimeMilli expireTime = GetExpireTime();
aLeaseInfo.mRemainingLease = (now <= expireTime) ? (expireTime - now) : 0;
}
else
{
aLeaseInfo.mRemainingLease = 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 will 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::GetNextService(const Service *aPrevService) const
{
return (aPrevService == nullptr) ? mServices.GetHead() : aPrevService->GetNext();
}
Server::Service *Server::Host::AddNewService(const char *aInstanceName,
const char *aInstanceLabel,
TimeMilli aUpdateTime)
{
Service *service = Service::AllocateAndInit(aInstanceName, aInstanceLabel, *this, aUpdateTime);
VerifyOrExit(service != nullptr);
AddService(*service);
exit:
return service;
}
void Server::Host::AddService(Service &aService)
{
aService.mHost = this;
mServices.Push(aService);
}
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.IsSet())
{
uint32_t updateId = server.AllocateId();
LogInfo("SRP update handler is notified (updatedId = %lu)", ToUlong(updateId));
server.mServiceUpdateHandler.Invoke(updateId, this, static_cast<uint32_t>(kDefaultEventsHandlerTimeout));
// 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;
}
void Server::Host::FreeAllServices(void)
{
while (!mServices.IsEmpty())
{
RemoveService(mServices.GetHead(), kDeleteName, kDoNotNotifyServiceHandler);
}
}
void Server::Host::ClearResources(void) { mAddresses.Free(); }
Server::Service *Server::Host::FindService(const char *aInstanceName) { return mServices.FindMatching(aInstanceName); }
const Server::Service *Server::Host::FindService(const char *aInstanceName) const
{
return mServices.FindMatching(aInstanceName);
}
bool Server::Host::HasService(const char *aInstanceName) const { return mServices.ContainsMatching(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)
, mError(kErrorNone)
, mIsDirectRxFromClient(aMessageMetadata.IsDirectRxFromClient())
{
if (aMessageMetadata.mMessageInfo != nullptr)
{
mMessageInfo = *aMessageMetadata.mMessageInfo;
}
}
} // namespace Srp
} // namespace ot
#endif // OPENTHREAD_CONFIG_SRP_SERVER_ENABLE