blob: 4a4789364d3dfffba9ae55194947cdac81c801e9 [file] [log] [blame]
/*
*
* Copyright (c) 2021 Project CHIP Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include <lib/core/Optional.h>
#include <lib/core/PeerId.h>
#include <lib/dnssd/Resolver.h>
#include <lib/dnssd/minimal_mdns/core/HeapQName.h>
#include <lib/support/Variant.h>
#include <system/SystemClock.h>
namespace mdns {
namespace Minimal {
/// Keeps track of active resolve attempts
///
/// Maintains a list of 'pending mdns resolve queries' and provides operations
/// for:
/// - add/remove to the list
/// - figuring out a 'next query time' for items in the list
/// - iterating through the 'schedule now' items of the list
///
class ActiveResolveAttempts
{
public:
static constexpr size_t kRetryQueueSize = 4;
static constexpr chip::System::Clock::Timeout kMaxRetryDelay = chip::System::Clock::Seconds16(16);
struct ScheduledAttempt
{
struct Browse
{
Browse(const chip::Dnssd::DiscoveryFilter discoveryFilter, const chip::Dnssd::DiscoveryType discoveryType) :
filter(discoveryFilter), type(discoveryType)
{}
chip::Dnssd::DiscoveryFilter filter;
chip::Dnssd::DiscoveryType type;
};
struct Resolve
{
chip::PeerId peerId;
uint32_t consumerCount = 0;
Resolve(chip::PeerId id) : peerId(id) {}
};
struct IpResolve
{
HeapQName hostName;
IpResolve(HeapQName && host) : hostName(std::move(host)) {}
};
ScheduledAttempt()
{
static_assert(sizeof(Resolve) <= sizeof(Browse) || sizeof(Resolve) <= sizeof(HeapQName),
"Figure out where to put the Resolve counter so that Resolve is not making ScheduledAttempt bigger than "
"it has to be anyway to handle the other attempt types.");
}
ScheduledAttempt(const chip::PeerId & peer, bool first) :
resolveData(chip::InPlaceTemplateType<Resolve>(), peer), firstSend(first)
{}
ScheduledAttempt(const chip::Dnssd::DiscoveryFilter discoveryFilter, const chip::Dnssd::DiscoveryType type, bool first) :
resolveData(chip::InPlaceTemplateType<Browse>(), discoveryFilter, type), firstSend(first)
{}
ScheduledAttempt(IpResolve && ipResolve, bool first) :
resolveData(chip::InPlaceTemplateType<IpResolve>(), ipResolve), firstSend(first)
{}
bool operator==(const ScheduledAttempt & other) const { return Matches(other) && other.firstSend == firstSend; }
bool Matches(const ScheduledAttempt & other) const
{
if (!resolveData.Valid())
{
return !other.resolveData.Valid();
}
if (resolveData.Is<Browse>())
{
if (!other.resolveData.Is<Browse>())
{
return false;
}
auto & a = resolveData.Get<Browse>();
auto & b = other.resolveData.Get<Browse>();
return (a.filter == b.filter && a.type == b.type);
}
if (resolveData.Is<Resolve>())
{
if (!other.resolveData.Is<Resolve>())
{
return false;
}
auto & a = resolveData.Get<Resolve>();
auto & b = other.resolveData.Get<Resolve>();
return a.peerId == b.peerId;
}
if (resolveData.Is<IpResolve>())
{
if (!other.resolveData.Is<IpResolve>())
{
return false;
}
auto & a = resolveData.Get<IpResolve>();
auto & b = other.resolveData.Get<IpResolve>();
return a.hostName == b.hostName;
}
return false;
}
bool MatchesIpResolve(SerializedQNameIterator hostName) const
{
return resolveData.Is<IpResolve>() && (hostName == resolveData.Get<IpResolve>().hostName.Content());
}
bool Matches(const chip::PeerId & peer) const
{
return resolveData.Is<Resolve>() && (resolveData.Get<Resolve>().peerId == peer);
}
bool Matches(const chip::Dnssd::DiscoveredNodeData & data) const
{
if (!resolveData.Is<Browse>())
{
return false;
}
auto & browse = resolveData.Get<Browse>();
// TODO: we should mark returned node data based on the query
if (browse.type != chip::Dnssd::DiscoveryType::kCommissionableNode)
{
// We don't currently have markers in the returned DiscoveredNodeData to differentiate these, so assume all returned
// packets match
return true;
}
switch (browse.filter.type)
{
case chip::Dnssd::DiscoveryFilterType::kNone:
return true;
case chip::Dnssd::DiscoveryFilterType::kShortDiscriminator:
return browse.filter.code == static_cast<uint64_t>((data.commissionData.longDiscriminator >> 8) & 0x0F);
case chip::Dnssd::DiscoveryFilterType::kLongDiscriminator:
return browse.filter.code == data.commissionData.longDiscriminator;
case chip::Dnssd::DiscoveryFilterType::kVendorId:
return browse.filter.code == data.commissionData.vendorId;
case chip::Dnssd::DiscoveryFilterType::kDeviceType:
return browse.filter.code == data.commissionData.deviceType;
case chip::Dnssd::DiscoveryFilterType::kCommissioningMode:
return browse.filter.code == data.commissionData.commissioningMode;
case chip::Dnssd::DiscoveryFilterType::kInstanceName:
return strncmp(browse.filter.instanceName, data.commissionData.instanceName,
chip::Dnssd::Commission::kInstanceNameMaxLength + 1) == 0;
case chip::Dnssd::DiscoveryFilterType::kCommissioner:
case chip::Dnssd::DiscoveryFilterType::kCompressedFabricId:
default:
// These are for other discovery types.
return false;
}
}
bool IsEmpty() const { return !resolveData.Valid(); }
bool IsResolve() const { return resolveData.Is<Resolve>(); }
bool IsBrowse() const { return resolveData.Is<Browse>(); }
bool IsIpResolve() const { return resolveData.Is<IpResolve>(); }
void Clear() { resolveData = DataType(); }
// Called when this scheduled attempt will replace an existing scheduled
// attempt, because either they match or we have run out of attempt
// slots. When this happens, we want to propagate the consumer count
// from the existing attempt to the new one, if they're matching resolve
// attempts.
void WillCoalesceWith(const ScheduledAttempt & existing)
{
if (!IsResolve())
{
// Consumer count is only tracked for resolve requests
return;
}
if (!existing.Matches(*this))
{
// Out of attempt slots, so just dropping the existing attempt.
return;
}
// Adding another consumer to the same query. Propagate along the
// consumer count to our new attempt, which will replace the
// existing one.
resolveData.Get<Resolve>().consumerCount = existing.resolveData.Get<Resolve>().consumerCount + 1;
}
void ConsumerRemoved()
{
if (!IsResolve())
{
return;
}
auto & count = resolveData.Get<Resolve>().consumerCount;
if (count > 0)
{
--count;
}
if (count == 0)
{
Clear();
}
}
const Browse & BrowseData() const { return resolveData.Get<Browse>(); }
const Resolve & ResolveData() const { return resolveData.Get<Resolve>(); }
const IpResolve & IpResolveData() const { return resolveData.Get<IpResolve>(); }
using DataType = chip::Variant<Browse, Resolve, IpResolve>;
DataType resolveData;
// First packet send is marked separately: minMDNS logic can choose
// to first send a unicast query followed by a multicast one.
bool firstSend = false;
};
ActiveResolveAttempts(chip::System::Clock::ClockBase * clock) : mClock(clock) { Reset(); }
/// Clear out the internal queue
void Reset();
/// Mark a resolution as a success, removing it from the internal list
void Complete(const chip::PeerId & peerId);
void Complete(const chip::Dnssd::DiscoveredNodeData & data);
void CompleteIpResolution(SerializedQNameIterator targetHostName);
/// Mark all browse-type scheduled attemptes as a success, removing them
/// from the internal list.
CHIP_ERROR CompleteAllBrowses();
/// Note that resolve attempts for the given peer id now have one fewer
/// consumer.
void NodeIdResolutionNoLongerNeeded(const chip::PeerId & peerId);
/// Mark that a resolution is pending, adding it to the internal list
///
/// Once this complete, this peer id will be returned immediately
/// by NextScheduled (potentially with others as well)
void MarkPending(const chip::PeerId & peerId);
void MarkPending(const chip::Dnssd::DiscoveryFilter & filter, const chip::Dnssd::DiscoveryType type);
void MarkPending(ScheduledAttempt::IpResolve && resolve);
// Get minimum time until the next pending reply is required.
//
// Returns missing if no actively tracked elements exist.
chip::Optional<chip::System::Clock::Timeout> GetTimeUntilNextExpectedResponse() const;
// Get the peer Id that needs scheduling for a query
//
// Assumes that the resolution is being sent and will apply internal
// query logic. This means:
// - internal tracking of 'next due time' will updated as 'request sent
// now'
// - there is NO sorting implied by this call. Returned value will be
// any peer that needs a new request sent
chip::Optional<ScheduledAttempt> NextScheduled();
/// Check if any of the pending queries are for the given host name for
/// IP resolution.
bool IsWaitingForIpResolutionFor(SerializedQNameIterator hostName) const;
private:
struct RetryEntry
{
ScheduledAttempt attempt;
// When a reply is expected for this item
chip::System::Clock::Timestamp queryDueTime;
// Next expected delay for sending if reply is not reached by
// 'queryDueTimeMs'
//
// Based on RFC 6762 expectations are:
// - the interval between the first two queries MUST be at least
// one second
// - the intervals between successive queries MUST increase by at
// least a factor of two
chip::System::Clock::Timeout nextRetryDelay = chip::System::Clock::Seconds16(1);
};
void MarkPending(ScheduledAttempt && attempt);
chip::System::Clock::ClockBase * mClock;
RetryEntry mRetryQueue[kRetryQueueSize];
};
} // namespace Minimal
} // namespace mdns