From ff486eb62e68710a41d8edd220ab3a3507b499b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:02 +0200 Subject: [PATCH 01/16] queueing: support selective extraction and departure signals Allow consumers to select packets through the owning provider while preserving priority, WRR, label, gate, and compound-queue scheduling. Publish logical departures through packetQueueDeparture with typed dequeue, removal, and drop reasons so consumers can distinguish ownership transfer from terminal disposal. Detach shared-buffer overflow victims before notification and forward logical departures once across compound boundaries. Cover borrowed packet lifetime during external-buffer eviction. --- WHATSNEW | 30 +++++ doc/src/users-guide/ch-diffserv.rst | 9 +- src/inet/queueing/base/PacketFlowBase.cc | 20 ++- src/inet/queueing/base/PacketFlowBase.h | 7 +- src/inet/queueing/base/PacketGateBase.cc | 15 ++- src/inet/queueing/base/PacketGateBase.h | 3 +- src/inet/queueing/base/PacketQueueBase.cc | 20 ++- src/inet/queueing/base/PacketQueueBase.h | 4 +- src/inet/queueing/base/PacketQueueBase.ned | 1 + src/inet/queueing/buffer/PacketBuffer.cc | 19 ++- src/inet/queueing/contract/IPacketBuffer.h | 15 ++- src/inet/queueing/contract/IPacketExtractor.h | 38 ++++++ src/inet/queueing/contract/IPacketQueue.cc | 11 ++ src/inet/queueing/contract/IPacketQueue.h | 26 +++- src/inet/queueing/contract/IPacketQueue.ned | 1 + .../contract/PacketQueueRemovalDetails.h | 24 ++++ .../queueing/queue/CompoundPacketQueueBase.cc | 116 ++++++++++++++++- .../queueing/queue/CompoundPacketQueueBase.h | 14 ++ src/inet/queueing/queue/PacketQueue.cc | 67 ++++++++-- src/inet/queueing/queue/PacketQueue.h | 7 +- src/inet/queueing/scheduler/LabelScheduler.cc | 103 ++++++++++++++- src/inet/queueing/scheduler/LabelScheduler.h | 8 +- .../queueing/scheduler/PriorityScheduler.cc | 87 ++++++++++--- .../queueing/scheduler/PriorityScheduler.h | 7 +- src/inet/queueing/scheduler/WrrScheduler.cc | 120 +++++++++++++++++- src/inet/queueing/scheduler/WrrScheduler.h | 13 +- .../PacketQueueDepartureSignal_1.test | 98 ++++++++++++++ 27 files changed, 812 insertions(+), 71 deletions(-) create mode 100644 src/inet/queueing/contract/IPacketExtractor.h create mode 100644 src/inet/queueing/contract/IPacketQueue.cc create mode 100644 src/inet/queueing/contract/PacketQueueRemovalDetails.h create mode 100644 tests/queueing/PacketQueueDepartureSignal_1.test diff --git a/WHATSNEW b/WHATSNEW index 5fdd7668423..10210553c8d 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -29,6 +29,36 @@ Notable backward incompatible changes are the following: adaptive rate control, either where a station transmits to several peers or where it sends group-addressed traffic. +2. Packet queue extraction and lifecycle signals + + IPacketQueue now supports queue-lifecycle signals and predicate-based extraction. + Direct C++ implementations of IPacketQueue must implement findPacket(), the + predicate overload of dequeuePacket(), and must emit packetQueueDeparture + with PacketQueueRemovalDetails exactly once whenever a packet is dequeued, + explicitly removed, or destructively dropped. Listeners subscribe to the queue + module and filter the source to exclude descendant queue emissions. Providers + connected to CompoundPacketQueueBase must also implement IPacketExtractor so + that a compound queue preserves the provider's scheduling policy when it + extracts a matching packet. Direct C++ + implementations of IPacketExtractor must replace selected-pointer extraction + with findPacket(predicate) and dequeuePacket(predicate); predicates may be + evaluated repeatedly and must be stable and side-effect free throughout one + logical selection. WrrScheduler, + LabelScheduler, and PriorityScheduler accept ordinary IPassivePacketSource + inputs; they require IPacketCollection only when collection/aggregate access is + used and IPacketExtractor only when predicate extraction is used, reporting the + unsupported operation lazily. + PriorityScheduler aggregate queries no longer return -1 when an input lacks + IPacketCollection; getNumPackets() and getTotalLength() now report that + unsupported operation with cRuntimeError. Update callers that treated -1 as + an unknown aggregate size, or connect collection-capable providers. + IPacketBuffer::ICallback also provides an optional handlePacketDropped() + notification. Shared buffers invoke it only after all victims selected by one + overload operation have been detached from their owning queues. Compound + queues propagate destructive drops through arbitrary non-queue wrappers and + nested compound queues exactly once. PacketBuffer rejects packets owned by a + cPacketQueue whose owner cannot participate in the buffer callback contract. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/doc/src/users-guide/ch-diffserv.rst b/doc/src/users-guide/ch-diffserv.rst index 03302b6e30c..ac601d0d30e 100644 --- a/doc/src/users-guide/ch-diffserv.rst +++ b/doc/src/users-guide/ch-diffserv.rst @@ -282,9 +282,11 @@ interface is ready to transmit one. They have several input gates and one output gate. Modules that are connected to the inputs of a scheduler must implement -the :cpp:`IPacketQueue` C++ interface. Schedulers also implement the -:cpp:`IPacketQueue` interface, so they can be cascaded to other -schedulers and used as the output module of :ned:`IPacketQueue`'s. +the :cpp:`IPassivePacketSource` C++ interface. Collection and predicate +extraction operations additionally require the corresponding input provider +to implement :cpp:`IPacketCollection` and :cpp:`IPacketExtractor`, respectively. +Schedulers can be cascaded and used as the output module of compound packet +queues when those additional interfaces are available. There are several possible scheduling disciplines (first come/first served, priority, weighted fair, weighted round-robin, deadline-based, @@ -589,4 +591,3 @@ implement three different drop priorities within the class. BE packets are stored in a drop tail queue. Packets from AFxy and BE queues are scheduled by a WRR scheduler, which ensures that the remaining bandwidth is allocated among the classes according to the specified weights. - diff --git a/src/inet/queueing/base/PacketFlowBase.cc b/src/inet/queueing/base/PacketFlowBase.cc index 3ccc24cb908..96eb2ca9716 100644 --- a/src/inet/queueing/base/PacketFlowBase.cc +++ b/src/inet/queueing/base/PacketFlowBase.cc @@ -24,6 +24,7 @@ void PacketFlowBase::initialize(int stage) provider.reference(inputGate, false); collector.reference(outputGate, false); collection.reference(inputGate, false); + packetExtractor.reference(inputGate, false); WATCH(inProgressStreamId); } else if (stage == INITSTAGE_QUEUEING) { @@ -168,6 +169,24 @@ Packet *PacketFlowBase::pullPacket(const cGate *gate) return packet; } +Packet *PacketFlowBase::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + checkPacketStreaming(nullptr); + auto packet = packetExtractor->dequeuePacket(predicate); + if (packet == nullptr) + return nullptr; + take(packet); + emit(packetPulledInSignal, packet); + processPacket(packet); + handlePacketProcessed(packet); + emit(packetPulledOutSignal, packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; +} + Packet *PacketFlowBase::pullPacketStart(const cGate *gate, bps datarate) { Enter_Method("pullPacketStart"); @@ -234,4 +253,3 @@ void PacketFlowBase::handlePullPacketProcessed(Packet *packet, const cGate *gate } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/base/PacketFlowBase.h b/src/inet/queueing/base/PacketFlowBase.h index d0725d5e173..ffaa505784f 100644 --- a/src/inet/queueing/base/PacketFlowBase.h +++ b/src/inet/queueing/base/PacketFlowBase.h @@ -16,17 +16,19 @@ #include "inet/queueing/common/PassivePacketSourceRef.h" #include "inet/queueing/contract/IPacketCollection.h" #include "inet/queueing/contract/IPacketFlow.h" +#include "inet/queueing/contract/IPacketExtractor.h" namespace inet { namespace queueing { -class INET_API PacketFlowBase : public PacketProcessorBase, public virtual IPacketFlow, public virtual IPacketCollection +class INET_API PacketFlowBase : public PacketProcessorBase, public virtual IPacketFlow, public virtual IPacketCollection, public virtual IPacketExtractor { protected: cGate *inputGate = nullptr; ActivePacketSourceRef producer; PassivePacketSourceRef provider; ModuleRef collection; + ModuleRef packetExtractor; cGate *outputGate = nullptr; PassivePacketSinkRef consumer; @@ -82,6 +84,8 @@ class INET_API PacketFlowBase : public PacketProcessorBase, public virtual IPack virtual Packet *getPacket(int index) const override { return collection->getPacket(index); } virtual bool isEmpty() const override { return collection->isEmpty(); } virtual void removePacket(Packet *packet) override { collection->removePacket(packet); } + virtual Packet *findPacket(const PacketPredicate& predicate) const override { return packetExtractor->findPacket(predicate); } + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override { collection->removeAllPackets(); } }; @@ -89,4 +93,3 @@ class INET_API PacketFlowBase : public PacketProcessorBase, public virtual IPack } // namespace inet #endif - diff --git a/src/inet/queueing/base/PacketGateBase.cc b/src/inet/queueing/base/PacketGateBase.cc index 69d237af107..126e2340345 100644 --- a/src/inet/queueing/base/PacketGateBase.cc +++ b/src/inet/queueing/base/PacketGateBase.cc @@ -98,6 +98,20 @@ void PacketGateBase::removePacket(Packet *packet) PacketFlowBase::removePacket(packet); } +Packet *PacketGateBase::findPacket(const PacketPredicate& predicate) const +{ + if (!isOpen()) + return nullptr; + auto packet = PacketFlowBase::findPacket(predicate); + return packet != nullptr && canPacketFlowThrough(packet) ? packet : nullptr; +} + +Packet *PacketGateBase::dequeuePacket(const PacketPredicate& predicate) +{ + auto packet = findPacket(predicate); + return packet == nullptr ? nullptr : PacketFlowBase::dequeuePacket([packet](const Packet *candidate) { return candidate == packet; }); +} + void PacketGateBase::removeAllPackets() { if (isOpen()) @@ -162,4 +176,3 @@ void PacketGateBase::refreshDisplay() const } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/base/PacketGateBase.h b/src/inet/queueing/base/PacketGateBase.h index 9ccb2701836..c94cc662777 100644 --- a/src/inet/queueing/base/PacketGateBase.h +++ b/src/inet/queueing/base/PacketGateBase.h @@ -53,6 +53,8 @@ class INET_API PacketGateBase : public PacketFlowBase, public TransparentProtoco virtual Packet* getPacket(int index) const override; virtual bool isEmpty() const override; virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; virtual IPassivePacketSink *getConsumer(const cGate *gate) override { return this; } @@ -75,4 +77,3 @@ class INET_API PacketGateBase : public PacketFlowBase, public TransparentProtoco } // namespace inet #endif - diff --git a/src/inet/queueing/base/PacketQueueBase.cc b/src/inet/queueing/base/PacketQueueBase.cc index 4780fa44fb7..6c0a2b5e8a7 100644 --- a/src/inet/queueing/base/PacketQueueBase.cc +++ b/src/inet/queueing/base/PacketQueueBase.cc @@ -7,8 +7,12 @@ #include "inet/queueing/base/PacketQueueBase.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + #include "inet/common/Simsignals.h" +#include "inet/common/PacketEventTag.h" #include "inet/common/StringFormat.h" +#include "inet/common/TimeTag.h" namespace inet { namespace queueing { @@ -53,6 +57,21 @@ Packet *PacketQueueBase::dequeuePacket() return packet; } +void PacketQueueBase::notifyPacketRemoved(Packet *packet, IPacketQueue::PacketRemovalReason reason) +{ + PacketQueueRemovalDetails details(reason); + emit(IPacketQueue::packetQueueDepartureSignal, packet, &details); +} + +void PacketQueueBase::recordPacketDequeued(Packet *packet) +{ + auto queueingTime = simTime() - packet->getArrivalTime(); + auto packetEvent = new PacketEvent(); + insertPacketEvent(this, packet, PEK_QUEUED, 0, queueingTime, packetEvent); + increaseTimeTag(packet, queueingTime, queueingTime); + emit(packetPulledSignal, packet); +} + void PacketQueueBase::emit(simsignal_t signal, cObject *object, cObject *details) { if (signal == packetPushedSignal || signal == packetPushStartedSignal) @@ -92,4 +111,3 @@ std::string PacketQueueBase::resolveDirective(char directive) const } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/base/PacketQueueBase.h b/src/inet/queueing/base/PacketQueueBase.h index 97fcaffd133..b7cc572a995 100644 --- a/src/inet/queueing/base/PacketQueueBase.h +++ b/src/inet/queueing/base/PacketQueueBase.h @@ -31,6 +31,8 @@ class INET_API PacketQueueBase : public PacketProcessorBase, public virtual IPac virtual void handleMessage(cMessage *message) override; virtual void emit(simsignal_t signal, cObject *object, cObject *details = nullptr) override; + virtual void recordPacketDequeued(Packet *packet); + virtual void notifyPacketRemoved(Packet *packet, IPacketQueue::PacketRemovalReason reason); virtual std::string resolveDirective(char directive) const override; @@ -41,6 +43,7 @@ class INET_API PacketQueueBase : public PacketProcessorBase, public virtual IPac virtual void enqueuePacket(Packet *packet) override; virtual Packet *dequeuePacket() override; + public: virtual void pushPacketStart(Packet *packet, const cGate *gate, bps datarate) override { throw cRuntimeError("Invalid operation"); } virtual void pushPacketEnd(Packet *packet, const cGate *gate) override { throw cRuntimeError("Invalid operation"); } @@ -55,4 +58,3 @@ class INET_API PacketQueueBase : public PacketProcessorBase, public virtual IPac } // namespace inet #endif - diff --git a/src/inet/queueing/base/PacketQueueBase.ned b/src/inet/queueing/base/PacketQueueBase.ned index 9075cc9745a..a583ec1ce67 100644 --- a/src/inet/queueing/base/PacketQueueBase.ned +++ b/src/inet/queueing/base/PacketQueueBase.ned @@ -25,6 +25,7 @@ package inet.queueing.base; simple PacketQueueBase extends PacketProcessorBase { parameters: + @signal[packetQueueDeparture](type=inet::Packet); // Details: PacketQueueRemovalDetails; source identifies the logical queue displayStringTextFormat = default("contains {numPackets} pk ({totalLength})\npushed {numPushedPackets} pulled {numPulledPackets} dropped {numDroppedPackets}"); // Determines display string text above the submodule @display("i=block/queue"); gates: diff --git a/src/inet/queueing/buffer/PacketBuffer.cc b/src/inet/queueing/buffer/PacketBuffer.cc index a67da4be8ca..f554f5f12f9 100644 --- a/src/inet/queueing/buffer/PacketBuffer.cc +++ b/src/inet/queueing/buffer/PacketBuffer.cc @@ -53,21 +53,29 @@ b PacketBuffer::getTotalLength() const void PacketBuffer::addPacket(Packet *packet) { Enter_Method("addPacket"); + auto ownerQueue = dynamic_cast(packet->getOwner()); + if (ownerQueue != nullptr && dynamic_cast(ownerQueue->getOwner()) == nullptr) + throw cRuntimeError("Cannot buffer packet owned by cPacketQueue whose owner does not implement IPacketBuffer::ICallback"); EV_INFO << "Adding packet" << EV_FIELD(packet) << EV_ENDL; emit(packetAddedSignal, packet); packets.push_back(packet); if (isOverloaded()) { if (packetDropperFunction != nullptr) { + std::vector> droppedPackets; while (!isEmpty() && isOverloaded()) { auto packet = packetDropperFunction->selectPacket(this); EV_INFO << "Dropping packet" << EV_FIELD(packet) << EV_ENDL; packets.erase(find(packets, packet)); auto queue = dynamic_cast(packet->getOwner()); - if (queue != nullptr) { - ICallback *callback = dynamic_cast(queue->getOwner()); - if (callback != nullptr) - callback->handlePacketRemoved(packet); - } + auto callback = queue != nullptr ? check_and_cast(queue->getOwner()) : nullptr; + droppedPackets.emplace_back(packet, callback); + } + for (auto& [packet, callback] : droppedPackets) + if (callback != nullptr) + callback->handlePacketDropping(packet); + for (auto& [packet, callback] : droppedPackets) { + if (callback != nullptr) + callback->handlePacketDropped(packet); // TODO maybe the buffer should take ownership and queues should be aware of it take(packet); dropPacket(packet, QUEUE_OVERFLOW); @@ -118,4 +126,3 @@ Packet *PacketBuffer::getPacket(int index) const } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/contract/IPacketBuffer.h b/src/inet/queueing/contract/IPacketBuffer.h index ced7f9556f0..4e0bf6302c8 100644 --- a/src/inet/queueing/contract/IPacketBuffer.h +++ b/src/inet/queueing/contract/IPacketBuffer.h @@ -27,6 +27,20 @@ class INET_API IPacketBuffer : public virtual IPacketCollection * The packet is never nullptr. */ virtual void handlePacketRemoved(Packet *packet) = 0; + + /** + * Detaches a packet selected for an overload drop from its owner before + * drop observers are notified. The default preserves compatibility for + * owners which do not distinguish the two removal phases. + */ + virtual void handlePacketDropping(Packet *packet) { handlePacketRemoved(packet); } + + /** + * Notifies the packet owner after all packets selected by one overload + * operation have been removed from their owners. The default implementation + * preserves compatibility for owners which do not distinguish buffer drops. + */ + virtual void handlePacketDropped(Packet *packet) {} }; public: @@ -47,4 +61,3 @@ class INET_API IPacketBuffer : public virtual IPacketCollection } // namespace inet #endif - diff --git a/src/inet/queueing/contract/IPacketExtractor.h b/src/inet/queueing/contract/IPacketExtractor.h new file mode 100644 index 00000000000..cf2287ceeed --- /dev/null +++ b/src/inet/queueing/contract/IPacketExtractor.h @@ -0,0 +1,38 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IPACKETEXTRACTOR_H +#define __INET_IPACKETEXTRACTOR_H + +#include + +#include "inet/common/packet/Packet.h" + +namespace inet { +namespace queueing { + +/** Provides semantic pull accounting while extracting a selected packet. */ +class INET_API IPacketExtractor +{ + public: + /** + * Predicates must be stable and side-effect free from an initial + * findPacket() through the corresponding dequeuePacket() selection. + * Composite extractors may evaluate the predicate multiple times and on + * multiple candidates while preserving their scheduling policy. + */ + using PacketPredicate = std::function; + + public: + virtual ~IPacketExtractor() {} + virtual Packet *findPacket(const PacketPredicate& predicate) const = 0; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) = 0; +}; + +} // namespace queueing +} // namespace inet + +#endif diff --git a/src/inet/queueing/contract/IPacketQueue.cc b/src/inet/queueing/contract/IPacketQueue.cc new file mode 100644 index 00000000000..0be875f6df5 --- /dev/null +++ b/src/inet/queueing/contract/IPacketQueue.cc @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "inet/queueing/contract/IPacketQueue.h" + +namespace inet { +namespace queueing { + +simsignal_t IPacketQueue::packetQueueDepartureSignal = cComponent::registerSignal("packetQueueDeparture"); + +} // namespace queueing +} // namespace inet diff --git a/src/inet/queueing/contract/IPacketQueue.h b/src/inet/queueing/contract/IPacketQueue.h index 159fbb9fb91..6d481b6f359 100644 --- a/src/inet/queueing/contract/IPacketQueue.h +++ b/src/inet/queueing/contract/IPacketQueue.h @@ -9,6 +9,7 @@ #define __INET_IPACKETQUEUE_H #include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/contract/IPacketExtractor.h" #include "inet/queueing/contract/IPassivePacketSink.h" #include "inet/queueing/contract/IPassivePacketSource.h" @@ -18,8 +19,22 @@ namespace queueing { /** * This class defines the interface for packet queues. */ -class INET_API IPacketQueue : public virtual IPacketCollection, public virtual IPassivePacketSink, public virtual IPassivePacketSource +class INET_API IPacketQueue : public virtual IPacketCollection, public virtual IPacketExtractor, public virtual IPassivePacketSink, public virtual IPassivePacketSource { + public: + enum class PacketRemovalReason { + DEQUEUED, // Normal queue processing transferred ownership out of the queue. + REMOVED, // Explicit removal outside normal queue processing. + DROPPED, // The queue destructively removed the packet. + }; + + /** + * Emitted once per logical departure, after detachment and before transfer/deletion. + * The Packet and PacketQueueRemovalDetails are borrowed for synchronous delivery. + * Listeners must filter the source to their subscribed logical queue. + */ + static simsignal_t packetQueueDepartureSignal; + public: /** * Enqueues the packet into the packet queue. The onwership of the packet @@ -36,10 +51,17 @@ class INET_API IPacketQueue : public virtual IPacketCollection, public virtual I * The queue must not be empty. The returned packet must not be nullptr. */ virtual Packet *dequeuePacket() = 0; + + virtual Packet *findPacket(const PacketPredicate& predicate) const = 0; + + /** + * Dequeues the first matching packet according to the queue provider's + * scheduling policy. Ownership is transferred to the caller. + */ + virtual Packet *dequeuePacket(const PacketPredicate& predicate) = 0; }; } // namespace queueing } // namespace inet #endif - diff --git a/src/inet/queueing/contract/IPacketQueue.ned b/src/inet/queueing/contract/IPacketQueue.ned index 40120433323..2911872be9d 100644 --- a/src/inet/queueing/contract/IPacketQueue.ned +++ b/src/inet/queueing/contract/IPacketQueue.ned @@ -16,6 +16,7 @@ package inet.queueing.contract; moduleinterface IPacketQueue extends IPassivePacketSink, IPassivePacketSource { parameters: + @signal[packetQueueDeparture](type=inet::Packet); // Details: PacketQueueRemovalDetails; source identifies the logical queue @omittedTypename(OmittedPacketQueue); @display("i=block/queue"); } diff --git a/src/inet/queueing/contract/PacketQueueRemovalDetails.h b/src/inet/queueing/contract/PacketQueueRemovalDetails.h new file mode 100644 index 00000000000..12989ba45bb --- /dev/null +++ b/src/inet/queueing/contract/PacketQueueRemovalDetails.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_PACKETQUEUEREMOVALDETAILS_H +#define __INET_PACKETQUEUEREMOVALDETAILS_H + +#include "inet/queueing/contract/IPacketQueue.h" + +namespace inet { +namespace queueing { + +class INET_API PacketQueueRemovalDetails : public cObject +{ + protected: + IPacketQueue::PacketRemovalReason reason; + + public: + explicit PacketQueueRemovalDetails(IPacketQueue::PacketRemovalReason reason) : reason(reason) {} + IPacketQueue::PacketRemovalReason getReason() const { return reason; } +}; + +} // namespace queueing +} // namespace inet + +#endif diff --git a/src/inet/queueing/queue/CompoundPacketQueueBase.cc b/src/inet/queueing/queue/CompoundPacketQueueBase.cc index 7d290d0cc1e..2f77ec4c23b 100644 --- a/src/inet/queueing/queue/CompoundPacketQueueBase.cc +++ b/src/inet/queueing/queue/CompoundPacketQueueBase.cc @@ -7,6 +7,8 @@ #include "inet/queueing/queue/CompoundPacketQueueBase.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + #include "inet/common/Simsignals.h" namespace inet { @@ -14,6 +16,22 @@ namespace queueing { Define_Module(CompoundPacketQueueBase); +class ScopedPacketRemoval +{ + protected: + Packet *&packetBeingRemoved; + Packet *previousPacket; + + public: + ScopedPacketRemoval(Packet *&packetBeingRemoved, Packet *packet) : + packetBeingRemoved(packetBeingRemoved), previousPacket(packetBeingRemoved) + { + packetBeingRemoved = packet; + } + + ~ScopedPacketRemoval() { packetBeingRemoved = previousPacket; } +}; + void CompoundPacketQueueBase::initialize(int stage) { PacketQueueBase::initialize(stage); @@ -23,6 +41,11 @@ void CompoundPacketQueueBase::initialize(int stage) consumer.reference(inputGate, true, 1); provider.reference(outputGate, true, -1); collection = check_and_cast(provider.get()); + packetExtractor = check_and_cast(provider.get()); + // Observe the nearest queue on every descendant branch. Nested + // compound queues forward their own frontier, so stopping at a queue + // avoids duplicate notifications while traversing non-queue wrappers. + registerQueueFrontier(this); packetDropperFunction = createDropperFunction(par("dropperClass")); subscribe(packetDroppedSignal, this); subscribe(packetCreatedSignal, this); @@ -34,6 +57,39 @@ void CompoundPacketQueueBase::initialize(int stage) } } +void CompoundPacketQueueBase::registerQueueFrontier(cModule *module) +{ + for (cModule::SubmoduleIterator it(module); !it.end(); it++) { + auto childModule = *it; + auto childQueue = dynamic_cast(childModule); + if (childQueue != nullptr) { + childQueues.push_back(childQueue); + childModule->subscribe(IPacketQueue::packetQueueDepartureSignal, this); + } + else + registerQueueFrontier(childModule); + } +} + +void CompoundPacketQueueBase::finish() +{ + unsubscribeChildQueues(); + PacketQueueBase::finish(); +} + +void CompoundPacketQueueBase::preDelete(cComponent *root) +{ + unsubscribeChildQueues(); + PacketQueueBase::preDelete(root); +} + +void CompoundPacketQueueBase::unsubscribeChildQueues() +{ + for (auto childQueue : childQueues) + check_and_cast(childQueue)->unsubscribe(IPacketQueue::packetQueueDepartureSignal, this); + childQueues.clear(); +} + IPacketDropperFunction *CompoundPacketQueueBase::createDropperFunction(const char *dropperClass) const { if (strlen(dropperClass) == 0) @@ -58,11 +114,20 @@ void CompoundPacketQueueBase::pushPacket(Packet *packet, const cGate *gate) EV_INFO << "Pushing packet" << EV_FIELD(packet) << EV_ENDL; consumer.pushPacket(packet); if (packetDropperFunction != nullptr) { + std::vector droppedPackets; while (isOverloaded()) { auto packet = packetDropperFunction->selectPacket(this); EV_INFO << "Dropping packet" << EV_FIELD(packet) << EV_ENDL; - removePacket(packet); + { + ScopedPacketRemoval scopedPacketRemoval(packetBeingRemoved, packet); + collection->removePacket(packet); + } + emit(packetRemovedSignal, packet); take(packet); + droppedPackets.push_back(packet); + } + for (auto packet : droppedPackets) { + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DROPPED); dropPacket(packet, QUEUE_OVERFLOW); } } @@ -76,6 +141,7 @@ Packet *CompoundPacketQueueBase::pullPacket(const cGate *gate) Enter_Method("pullPacket"); auto packet = provider.pullPacket(); take(packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DEQUEUED); emit(packetPulledSignal, packet); return packet; } @@ -83,14 +149,43 @@ Packet *CompoundPacketQueueBase::pullPacket(const cGate *gate) void CompoundPacketQueueBase::removePacket(Packet *packet) { Enter_Method("removePacket"); - collection->removePacket(packet); + { + ScopedPacketRemoval scopedPacketRemoval(packetBeingRemoved, packet); + collection->removePacket(packet); + } + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::REMOVED); emit(packetRemovedSignal, packet); } +Packet *CompoundPacketQueueBase::findPacket(const PacketPredicate& predicate) const +{ + return packetExtractor->findPacket(predicate); +} + +Packet *CompoundPacketQueueBase::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + auto packet = packetExtractor->dequeuePacket(predicate); + if (packet == nullptr) + return nullptr; + take(packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DEQUEUED); + // The owning leaf/provider has already recorded queue residence. The + // compound boundary mirrors pullPacket() and emits only its pull event. + emit(packetPulledSignal, packet); + drop(packet); + return packet; +} + void CompoundPacketQueueBase::removeAllPackets() { Enter_Method("removeAllPacket"); - collection->removeAllPackets(); + while (getNumPackets() != 0) { + auto packet = getPacket(0); + removePacket(packet); + take(packet); + delete packet; + } } bool CompoundPacketQueueBase::canPushSomePacket(const cGate *gate) const @@ -118,7 +213,11 @@ bool CompoundPacketQueueBase::canPushPacket(Packet *packet, const cGate *gate) c void CompoundPacketQueueBase::receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signal)); - if (signal == packetDroppedSignal) + if (signal == IPacketQueue::packetQueueDepartureSignal) { + if (source != this && source->isSubscribed(signal, this)) + handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); + } + else if (signal == packetDroppedSignal) numDroppedPackets++; else if (signal == packetCreatedSignal) numCreatedPackets++; @@ -126,6 +225,13 @@ void CompoundPacketQueueBase::receiveSignal(cComponent *source, simsignal_t sign throw cRuntimeError("Unknown signal"); } +void CompoundPacketQueueBase::handlePacketRemoved(Packet *packet, IPacketQueue::PacketRemovalReason reason) +{ + Enter_Method("handlePacketRemoved"); + if (reason == IPacketQueue::PacketRemovalReason::DROPPED || + (reason == IPacketQueue::PacketRemovalReason::REMOVED && packet != packetBeingRemoved)) + notifyPacketRemoved(packet, reason); +} + } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/queue/CompoundPacketQueueBase.h b/src/inet/queueing/queue/CompoundPacketQueueBase.h index 706d9fb32c5..c89072bdf10 100644 --- a/src/inet/queueing/queue/CompoundPacketQueueBase.h +++ b/src/inet/queueing/queue/CompoundPacketQueueBase.h @@ -26,17 +26,28 @@ class INET_API CompoundPacketQueueBase : public PacketQueueBase, public cListene PassivePacketSinkRef consumer; PassivePacketSourceRef provider; IPacketCollection *collection = nullptr; + IPacketExtractor *packetExtractor = nullptr; + std::vector childQueues; + Packet *packetBeingRemoved = nullptr; IPacketDropperFunction *packetDropperFunction = nullptr; protected: + using cListener::finish; + virtual void initialize(int stage) override; + virtual void finish() override; + virtual void preDelete(cComponent *root) override; + virtual void registerQueueFrontier(cModule *module); + virtual void unsubscribeChildQueues(); virtual IPacketDropperFunction *createDropperFunction(const char *dropperClass) const; virtual bool isOverloaded() const; public: + using PacketQueueBase::dequeuePacket; + virtual ~CompoundPacketQueueBase() { delete packetDropperFunction; } virtual int getMaxNumPackets() const override { return packetCapacity; } @@ -48,6 +59,8 @@ class INET_API CompoundPacketQueueBase : public PacketQueueBase, public cListene virtual bool isEmpty() const override { return collection->isEmpty(); } virtual Packet *getPacket(int index) const override { return collection->getPacket(index); } virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; virtual bool supportsPacketPushing(const cGate *gate) const override { return inputGate == gate; } @@ -61,6 +74,7 @@ class INET_API CompoundPacketQueueBase : public PacketQueueBase, public cListene virtual Packet *pullPacket(const cGate *gate) override; virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; + virtual void handlePacketRemoved(Packet *packet, IPacketQueue::PacketRemovalReason reason); }; } // namespace queueing diff --git a/src/inet/queueing/queue/PacketQueue.cc b/src/inet/queueing/queue/PacketQueue.cc index 5cb89c3a638..9a6f553ce48 100644 --- a/src/inet/queueing/queue/PacketQueue.cc +++ b/src/inet/queueing/queue/PacketQueue.cc @@ -8,9 +8,7 @@ #include "inet/queueing/queue/PacketQueue.h" #include "inet/common/ModuleAccess.h" -#include "inet/common/PacketEventTag.h" #include "inet/common/Simsignals.h" -#include "inet/common/TimeTag.h" #include "inet/queueing/function/PacketComparatorFunction.h" #include "inet/queueing/function/PacketDropperFunction.h" @@ -100,10 +98,15 @@ void PacketQueue::pushPacket(Packet *packet, const cGate *gate) throw cRuntimeError("Queue is overloaded while using a packet buffer"); } else if (packetDropperFunction != nullptr) { + std::vector droppedPackets; while (isOverloaded()) { auto packet = packetDropperFunction->selectPacket(this); EV_INFO << "Dropping packet" << EV_FIELD(packet) << EV_ENDL; queue.remove(packet); + droppedPackets.push_back(packet); + } + for (auto packet : droppedPackets) { + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DROPPED); dropPacket(packet, QUEUE_OVERFLOW); } } @@ -126,16 +129,41 @@ Packet *PacketQueue::pullPacket(const cGate *gate) } else queue.pop(); - auto queueingTime = simTime() - packet->getArrivalTime(); - auto packetEvent = new PacketEvent(); - insertPacketEvent(this, packet, PEK_QUEUED, 0, queueingTime, packetEvent); - increaseTimeTag(packet, queueingTime, queueingTime); - emit(packetPulledSignal, packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DEQUEUED); + recordPacketDequeued(packet); if (collector != nullptr) animatePullPacket(packet, outputGate, collector.getReferencedGate()); return packet; } +Packet *PacketQueue::findPacket(const PacketPredicate& predicate) const +{ + for (int i = 0; i < queue.getLength(); i++) { + auto packet = check_and_cast(queue.get(i)); + if (predicate(packet)) + return packet; + } + return nullptr; +} + +Packet *PacketQueue::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + auto packet = findPacket(predicate); + if (packet == nullptr) + return nullptr; + EV_INFO << "Dequeuing packet" << EV_FIELD(packet) << EV_ENDL; + queue.remove(packet); + if (buffer != nullptr) + buffer->removePacket(packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DEQUEUED); + recordPacketDequeued(packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; +} + void PacketQueue::removePacket(Packet *packet) { Enter_Method("removePacket"); @@ -143,6 +171,7 @@ void PacketQueue::removePacket(Packet *packet) queue.remove(packet); if (buffer != nullptr) buffer->removePacket(packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::REMOVED); emit(packetRemovedSignal, packet); } @@ -151,11 +180,13 @@ void PacketQueue::removeAllPackets() Enter_Method("removeAllPackets"); EV_INFO << "Removing all packets" << EV_ENDL; std::vector packets; - for (int i = 0; i < getNumPackets(); i++) + while (!queue.isEmpty()) packets.push_back(check_and_cast(queue.pop())); if (buffer != nullptr) - buffer->removeAllPackets(); + for (auto packet : packets) + buffer->removePacket(packet); for (auto packet : packets) { + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::REMOVED); emit(packetRemovedSignal, packet); delete packet; } @@ -190,9 +221,25 @@ void PacketQueue::handlePacketRemoved(Packet *packet) EV_INFO << "Removing packet" << EV_FIELD(packet) << EV_ENDL; queue.remove(packet); emit(packetRemovedSignal, packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::REMOVED); } } +void PacketQueue::handlePacketDropping(Packet *packet) +{ + Enter_Method("handlePacketDropping"); + if (queue.contains(packet)) { + EV_INFO << "Removing packet" << EV_FIELD(packet) << EV_ENDL; + queue.remove(packet); + emit(packetRemovedSignal, packet); + } +} + +void PacketQueue::handlePacketDropped(Packet *packet) +{ + Enter_Method("handlePacketDropped"); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DROPPED); +} + } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/queue/PacketQueue.h b/src/inet/queueing/queue/PacketQueue.h index 4debf8541ec..2892da932ec 100644 --- a/src/inet/queueing/queue/PacketQueue.h +++ b/src/inet/queueing/queue/PacketQueue.h @@ -45,6 +45,8 @@ class INET_API PacketQueue : public PacketQueueBase, public IPacketBuffer::ICall virtual bool isOverloaded() const; public: + using PacketQueueBase::dequeuePacket; + virtual ~PacketQueue() { delete packetDropperFunction; } virtual cGate *getRegistrationForwardingGate(cGate *gate) override; @@ -58,6 +60,8 @@ class INET_API PacketQueue : public PacketQueueBase, public IPacketBuffer::ICall virtual bool isEmpty() const override { return getNumPackets() == 0; } virtual Packet *getPacket(int index) const override; virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; virtual bool supportsPacketPushing(const cGate *gate) const override { return inputGate == gate; } @@ -71,10 +75,11 @@ class INET_API PacketQueue : public PacketQueueBase, public IPacketBuffer::ICall virtual Packet *pullPacket(const cGate *gate) override; virtual void handlePacketRemoved(Packet *packet) override; + virtual void handlePacketDropping(Packet *packet) override; + virtual void handlePacketDropped(Packet *packet) override; }; } // namespace queueing } // namespace inet #endif - diff --git a/src/inet/queueing/scheduler/LabelScheduler.cc b/src/inet/queueing/scheduler/LabelScheduler.cc index af30138c225..f46dd2d2794 100644 --- a/src/inet/queueing/scheduler/LabelScheduler.cc +++ b/src/inet/queueing/scheduler/LabelScheduler.cc @@ -20,40 +20,130 @@ void LabelScheduler::initialize(int stage) if (stage == INITSTAGE_LOCAL) { defaultGateIndex = par("defaultGateIndex"); labels = cStringTokenizer(par("labels")).asVector(); - for (auto provider : providers) - collections.push_back(dynamic_cast(provider.get())); + for (size_t i = 0; i < providers.size(); i++) { + auto provider = providers[i].get(); + auto collection = dynamic_cast(provider); + auto packetExtractor = dynamic_cast(provider); + collections.push_back(collection); + packetExtractors.push_back(packetExtractor); + } } } int LabelScheduler::getNumPackets() const { int size = 0; - for (auto collection : collections) + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getNumPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); size += collection->getNumPackets(); + } return size; } b LabelScheduler::getTotalLength() const { b totalLength(0); - for (auto collection : collections) + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getTotalLength(): input provider at gate index %d does not implement IPacketCollection", (int)i); totalLength += collection->getTotalLength(); + } return totalLength; } Packet *LabelScheduler::getPacket(int index) const { - throw cRuntimeError("TODO"); + int originalIndex = index; + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot getPacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + auto numPackets = collection->getNumPackets(); + if (index < numPackets) + return collection->getPacket(index); + index -= numPackets; + } + throw cRuntimeError("Index %i out of range", originalIndex); } void LabelScheduler::removePacket(Packet *packet) { - throw cRuntimeError("TODO"); + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removePacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + for (int i = 0; i < collection->getNumPackets(); i++) { + if (collection->getPacket(i) == packet) { + collection->removePacket(packet); + return; + } + } + } + throw cRuntimeError("Cannot find packet"); +} + +int LabelScheduler::findInput(const PacketPredicate& predicate) const +{ + std::vector candidates; + for (size_t i = 0; i < packetExtractors.size(); i++) { + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + candidates.push_back(packetExtractors[i]->findPacket(predicate)); + } + for (auto label : labels) { + for (size_t i = 0; i < candidates.size(); i++) { + auto packet = candidates[i]; + if (packet == nullptr) + continue; + const auto& labelsTag = packet->findTag(); + if (labelsTag != nullptr) { + for (size_t j = 0; j < labelsTag->getLabelsArraySize(); j++) + if (label == labelsTag->getLabels(j)) + return i; + } + } + } + return defaultGateIndex >= 0 && defaultGateIndex < (int)candidates.size() && candidates[defaultGateIndex] != nullptr ? defaultGateIndex : -1; +} + +Packet *LabelScheduler::findPacket(const PacketPredicate& predicate) const +{ + auto index = findInput(predicate); + return index == -1 ? nullptr : packetExtractors[index]->findPacket(predicate); +} + +Packet *LabelScheduler::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot dequeuePacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + auto index = findInput(predicate); + if (index == -1) + return nullptr; + auto packet = packetExtractors[index]->dequeuePacket(predicate); + ASSERT(packet != nullptr); + take(packet); + handlePacketProcessed(packet); + emit(packetPulledSignal, packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; } void LabelScheduler::removeAllPackets() { Enter_Method("removeAllPackets"); + for (size_t i = 0; i < collections.size(); i++) { + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removeAllPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); + } for (auto collection : collections) collection->removeAllPackets(); } @@ -76,4 +166,3 @@ int LabelScheduler::schedulePacket() } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/scheduler/LabelScheduler.h b/src/inet/queueing/scheduler/LabelScheduler.h index bb03cd04af6..aca5ce7b84d 100644 --- a/src/inet/queueing/scheduler/LabelScheduler.h +++ b/src/inet/queueing/scheduler/LabelScheduler.h @@ -10,20 +10,23 @@ #include "inet/queueing/base/PacketSchedulerBase.h" #include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/contract/IPacketExtractor.h" namespace inet { namespace queueing { -class INET_API LabelScheduler : public PacketSchedulerBase, public virtual IPacketCollection +class INET_API LabelScheduler : public PacketSchedulerBase, public virtual IPacketCollection, public virtual IPacketExtractor { protected: int defaultGateIndex = -1; std::vector labels; std::vector collections; + std::vector packetExtractors; protected: virtual void initialize(int stage) override; virtual int schedulePacket() override; + virtual int findInput(const PacketPredicate& predicate) const; public: virtual int getMaxNumPackets() const override { return -1; } @@ -35,6 +38,8 @@ class INET_API LabelScheduler : public PacketSchedulerBase, public virtual IPack virtual bool isEmpty() const override { return getNumPackets() == 0; } virtual Packet *getPacket(int index) const override; virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; }; @@ -42,4 +47,3 @@ class INET_API LabelScheduler : public PacketSchedulerBase, public virtual IPack } // namespace inet #endif - diff --git a/src/inet/queueing/scheduler/PriorityScheduler.cc b/src/inet/queueing/scheduler/PriorityScheduler.cc index 346d2b313c2..be772b00d99 100644 --- a/src/inet/queueing/scheduler/PriorityScheduler.cc +++ b/src/inet/queueing/scheduler/PriorityScheduler.cc @@ -16,37 +16,47 @@ void PriorityScheduler::initialize(int stage) { PacketSchedulerBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { - for (auto provider : providers) - collections.push_back(dynamic_cast(provider.get())); + for (size_t i = 0; i < providers.size(); i++) { + auto provider = providers[i].get(); + collections.push_back(dynamic_cast(provider)); + auto packetExtractor = dynamic_cast(provider); + packetExtractors.push_back(packetExtractor); + } } } int PriorityScheduler::getNumPackets() const { int size = 0; - for (auto collection : collections) - if (collection != nullptr) - size += collection->getNumPackets(); - else - return -1; + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getNumPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); + size += collection->getNumPackets(); + } return size; } b PriorityScheduler::getTotalLength() const { b totalLength(0); - for (auto collection : collections) - if (collection != nullptr) - totalLength += collection->getTotalLength(); - else - return b(-1); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getTotalLength(): input provider at gate index %d does not implement IPacketCollection", (int)i); + totalLength += collection->getTotalLength(); + } return totalLength; } Packet *PriorityScheduler::getPacket(int index) const { int origIndex = index; - for (auto collection : collections) { + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot getPacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; auto numPackets = collection->getNumPackets(); if (index < numPackets) return collection->getPacket(index); @@ -59,7 +69,11 @@ Packet *PriorityScheduler::getPacket(int index) const void PriorityScheduler::removePacket(Packet *packet) { Enter_Method("removePacket"); - for (auto collection : collections) { + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removePacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; int numPackets = collection->getNumPackets(); for (int j = 0; j < numPackets; j++) { if (collection->getPacket(j) == packet) { @@ -71,9 +85,53 @@ void PriorityScheduler::removePacket(Packet *packet) throw cRuntimeError("Cannot find packet"); } +Packet *PriorityScheduler::findPacket(const PacketPredicate& predicate) const +{ + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto index = reverseOrder ? collections.size() - i - 1 : i; + if (packetExtractors[index] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)index); + auto packet = packetExtractors[index]->findPacket(predicate); + if (packet != nullptr) + return packet; + } + return nullptr; +} + +Packet *PriorityScheduler::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot dequeuePacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto index = reverseOrder ? collections.size() - i - 1 : i; + if (packetExtractors[index] == nullptr) + throw cRuntimeError("Cannot dequeuePacket(): input provider at gate index %d does not implement IPacketExtractor", (int)index); + auto packet = packetExtractors[index]->dequeuePacket(predicate); + if (packet == nullptr) + continue; + take(packet); + handlePacketProcessed(packet); + emit(packetPulledSignal, packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; + } + return nullptr; +} + void PriorityScheduler::removeAllPackets() { Enter_Method("removeAllPackets"); + for (size_t i = 0; i < collections.size(); i++) { + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removeAllPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); + } for (auto collection : collections) collection->removeAllPackets(); } @@ -107,4 +165,3 @@ void PriorityScheduler::handleCanPullPacketChanged(const cGate *gate) } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/scheduler/PriorityScheduler.h b/src/inet/queueing/scheduler/PriorityScheduler.h index 09d6af3ffb9..703cc6a0ada 100644 --- a/src/inet/queueing/scheduler/PriorityScheduler.h +++ b/src/inet/queueing/scheduler/PriorityScheduler.h @@ -10,14 +10,16 @@ #include "inet/queueing/base/PacketSchedulerBase.h" #include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/contract/IPacketExtractor.h" namespace inet { namespace queueing { -class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IPacketCollection +class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IPacketCollection, public virtual IPacketExtractor { protected: std::vector collections; + std::vector packetExtractors; protected: virtual void initialize(int stage) override; @@ -33,6 +35,8 @@ class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IP virtual bool isEmpty() const override { return getNumPackets() == 0; } virtual Packet *getPacket(int index) const override; virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; virtual void handleCanPullPacketChanged(const cGate *gate) override; @@ -42,4 +46,3 @@ class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IP } // namespace inet #endif - diff --git a/src/inet/queueing/scheduler/WrrScheduler.cc b/src/inet/queueing/scheduler/WrrScheduler.cc index a551bb69526..fbe6535eca2 100644 --- a/src/inet/queueing/scheduler/WrrScheduler.cc +++ b/src/inet/queueing/scheduler/WrrScheduler.cc @@ -37,30 +37,141 @@ void WrrScheduler::initialize(int stage) if (tokenizer.hasMoreTokens()) throw cRuntimeError("Too many values given in the weights parameter."); - for (auto provider : providers) - collections.push_back(dynamic_cast(provider.get())); + for (size_t i = 0; i < providers.size(); i++) { + auto provider = providers[i].get(); + auto collection = dynamic_cast(provider); + auto packetExtractor = dynamic_cast(provider); + collections.push_back(collection); + packetExtractors.push_back(packetExtractor); + } } } int WrrScheduler::getNumPackets() const { int size = 0; - for (auto collection : collections) + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getNumPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); size += collection->getNumPackets(); + } return size; } b WrrScheduler::getTotalLength() const { b totalLength(0); - for (auto collection : collections) + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getTotalLength(): input provider at gate index %d does not implement IPacketCollection", (int)i); totalLength += collection->getTotalLength(); + } return totalLength; } +Packet *WrrScheduler::getPacket(int index) const +{ + int originalIndex = index; + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot getPacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + auto numPackets = collection->getNumPackets(); + if (index < numPackets) + return collection->getPacket(index); + index -= numPackets; + } + throw cRuntimeError("Index %i out of range", originalIndex); +} + +void WrrScheduler::removePacket(Packet *packet) +{ + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removePacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + for (int i = 0; i < collection->getNumPackets(); i++) { + if (collection->getPacket(i) == packet) { + collection->removePacket(packet); + return; + } + } + } + throw cRuntimeError("Cannot find packet"); +} + +int WrrScheduler::findInput(const PacketPredicate& predicate) const +{ + int firstWeighted = -1; + int firstNonWeighted = -1; + for (size_t i = 0; i < collections.size(); ++i) { + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + if (packetExtractors[i]->findPacket(predicate) != nullptr) { + if (buckets[i] > 0) + return i; + else if (firstWeighted == -1 && weights[i] > 0) + firstWeighted = i; + else if (firstNonWeighted == -1 && weights[i] == 0) + firstNonWeighted = i; + } + } + return firstWeighted != -1 ? firstWeighted : firstNonWeighted; +} + +void WrrScheduler::consumeBucket(int index) +{ + if (weights[index] == 0) + return; + if (buckets[index] == 0) { + for (size_t i = 0; i < collections.size(); ++i) + buckets[i] = weights[i]; + } + ASSERT(buckets[index] > 0); + buckets[index]--; +} + +Packet *WrrScheduler::findPacket(const PacketPredicate& predicate) const +{ + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + auto index = findInput(predicate); + return index == -1 ? nullptr : packetExtractors[index]->findPacket(predicate); +} + +Packet *WrrScheduler::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot dequeuePacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + auto index = findInput(predicate); + if (index == -1) + return nullptr; + auto packet = packetExtractors[index]->dequeuePacket(predicate); + ASSERT(packet != nullptr); + consumeBucket(index); + take(packet); + handlePacketProcessed(packet); + emit(packetPulledSignal, packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; +} + void WrrScheduler::removeAllPackets() { Enter_Method("removeAllPackets"); + for (size_t i = 0; i < collections.size(); i++) { + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removeAllPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); + } for (auto collection : collections) collection->removeAllPackets(); } @@ -97,4 +208,3 @@ int WrrScheduler::schedulePacket() } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/scheduler/WrrScheduler.h b/src/inet/queueing/scheduler/WrrScheduler.h index d14f19b6b78..dc5907ac794 100644 --- a/src/inet/queueing/scheduler/WrrScheduler.h +++ b/src/inet/queueing/scheduler/WrrScheduler.h @@ -10,6 +10,7 @@ #include "inet/queueing/base/PacketSchedulerBase.h" #include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/contract/IPacketExtractor.h" namespace inet { namespace queueing { @@ -17,17 +18,20 @@ namespace queueing { /** * This module implements a Weighted Round Robin Scheduler. */ -class INET_API WrrScheduler : public PacketSchedulerBase, public virtual IPacketCollection +class INET_API WrrScheduler : public PacketSchedulerBase, public virtual IPacketCollection, public virtual IPacketExtractor { protected: unsigned int *weights = nullptr; // array of weights (has numInputs elements) unsigned int *buckets = nullptr; // array of tokens in buckets (has numInputs elements) std::vector collections; + std::vector packetExtractors; protected: virtual void initialize(int stage) override; virtual int schedulePacket() override; + virtual int findInput(const PacketPredicate& predicate) const; + virtual void consumeBucket(int index); public: virtual ~WrrScheduler(); @@ -39,8 +43,10 @@ class INET_API WrrScheduler : public PacketSchedulerBase, public virtual IPacket virtual b getTotalLength() const override; virtual bool isEmpty() const override { return getNumPackets() == 0; } - virtual Packet *getPacket(int index) const override { throw cRuntimeError("Invalid operation"); } - virtual void removePacket(Packet *packet) override { throw cRuntimeError("Invalid operation"); } + virtual Packet *getPacket(int index) const override; + virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; }; @@ -48,4 +54,3 @@ class INET_API WrrScheduler : public PacketSchedulerBase, public virtual IPacket } // namespace inet #endif - diff --git a/tests/queueing/PacketQueueDepartureSignal_1.test b/tests/queueing/PacketQueueDepartureSignal_1.test new file mode 100644 index 00000000000..20df6b35c0c --- /dev/null +++ b/tests/queueing/PacketQueueDepartureSignal_1.test @@ -0,0 +1,98 @@ +%description: +Verify that a PacketQueue using an external PacketBuffer reports one typed +departure signal while the evicted packet is still valid, before the buffer +deletes it. + +%file: TestPacketQueueDepartureSignal.cc + +#include + +#include "inet/common/packet/Packet.h" +#include "inet/queueing/buffer/PacketBuffer.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" +#include "inet/queueing/queue/PacketQueue.h" + +namespace inet { +namespace queueing { + +class TestPacketQueueDepartureSignal : public cSimpleModule, public cListener +{ + public: + TestPacketQueueDepartureSignal() : cSimpleModule(65536) {} + + protected: + int notificationCount = 0; + std::string lastPacketName; + bool packetWasOwned = false; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *details) override + { + auto packet = check_and_cast(object); + auto reason = check_and_cast(details)->getReason(); + ASSERT(reason == IPacketQueue::PacketRemovalReason::DROPPED); + notificationCount++; + lastPacketName = packet->getName(); + packetWasOwned = packet->getOwner() != nullptr; + } + + virtual void activity() override + { + auto queue = check_and_cast(getModuleByPath("^.queue")); + auto buffer = check_and_cast(getModuleByPath("^.buffer")); + queue->subscribe(IPacketQueue::packetQueueDepartureSignal, this); + ASSERT(queue->getMaxNumPackets() == -1); + ASSERT(buffer->getMaxNumPackets() == 0); + + auto packet = new Packet("external-buffer-packet"); + queue->enqueuePacket(packet); + + queue->unsubscribe(IPacketQueue::packetQueueDepartureSignal, this); + ASSERT(notificationCount == 1); + ASSERT(lastPacketName == "external-buffer-packet"); + ASSERT(packetWasOwned); + ASSERT(queue->isEmpty()); + ASSERT(buffer->isEmpty()); + std::cout << "External PacketBuffer eviction notified the queue signal exactly once before deletion.\n"; + } +}; + +Define_Module(TestPacketQueueDepartureSignal); + +} // namespace queueing +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; +import inet.queueing.buffer.PacketBuffer; +import inet.queueing.queue.PacketQueue; + +simple TestPacketQueueDepartureSignal extends SimpleModule +{ + parameters: + @class(::inet::queueing::TestPacketQueueDepartureSignal); +} + +network PacketQueueDepartureSignalTestNetwork +{ + submodules: + buffer: PacketBuffer; + queue: PacketQueue; + test: TestPacketQueueDepartureSignal; +} + +%inifile: omnetpp.ini + +[General] +network = PacketQueueDepartureSignalTestNetwork +sim-time-limit = 1us +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +*.queue.bufferModule = "^.buffer" +*.buffer.packetCapacity = 0 +*.buffer.dropperClass = "inet::queueing::PacketAtCollectionEndDropper" + +%contains: stdout +External PacketBuffer eviction notified the queue signal exactly once before deletion. From cdd5026ddec39bb42d8a9d6a9054e439b1891a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:02 +0200 Subject: [PATCH 02/16] ieee80211: track pending queue departures through signals Subscribe DCF and HCF to logical queue departure signals so destructive queue removal reports a management transmission outcome while the packet is still alive. Filter descendant emissions to avoid duplicate logical notifications. Exercise provider-directed extraction, queue accounting, shared buffers, nested notifications, and reentrant removal using signal listeners. --- .../ieee80211/mac/coordinationfunction/Dcf.cc | 31 +- .../ieee80211/mac/coordinationfunction/Dcf.h | 5 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 31 +- .../ieee80211/mac/coordinationfunction/Hcf.h | 5 +- .../module/Ieee80211MgmtApHcfQueueDrop_1.test | 6 +- tests/module/Ieee80211MgmtApQueueDrop_1.test | 2 +- tests/unit/Ieee80211AddbaTransaction_1.test | 947 ++++++++++++++++++ 7 files changed, 997 insertions(+), 30 deletions(-) create mode 100644 tests/unit/Ieee80211AddbaTransaction_1.test diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index ddb1cf9aa65..675b26be37a 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" @@ -53,7 +55,7 @@ void Dcf::initialize(int stage) else if (stage == INITSTAGE_LAST) { // Dcaf resolves its pending queue at the link-layer stage. Install // this signal listener after all child initialization has completed. - check_and_cast(channelAccess->getPendingQueue())->subscribe(packetDroppedSignal, this); + check_and_cast(channelAccess->getPendingQueue())->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); } } @@ -121,19 +123,25 @@ void Dcf::processMgmtFrame(Packet *packet, const Ptr& throw cRuntimeError("Unknown management frame"); } -void Dcf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) +void Dcf::receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) { - if (signalID == packetDroppedSignal) { - Enter_Method("%s", cComponent::getSignalName(signalID)); - auto packet = check_and_cast(obj); - if (packet->findTag() != nullptr) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); - } + if (signal == queueing::IPacketQueue::packetQueueDepartureSignal) { + Enter_Method("packetQueueDeparture"); + if (source->isSubscribed(signal, this)) + handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); } else - ModeSetListener::receiveSignal(source, signalID, obj, details); + ModeSetListener::receiveSignal(source, signal, object, details); +} + +void Dcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) +{ + Enter_Method("handlePacketRemoved"); + if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } } void Dcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header) @@ -429,4 +437,3 @@ Dcf::~Dcf() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h index 51ce7172954..87a4d1ab227 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h @@ -28,6 +28,7 @@ #include "inet/linklayer/ieee80211/mac/originator/AckHandler.h" #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.h" +#include "inet/queueing/contract/IPacketQueue.h" namespace inet { namespace ieee80211 { @@ -87,7 +88,6 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void initialize(int stage) override; virtual void forEachChild(cVisitor *v) override; virtual void handleMessage(cMessage *msg) override; - virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; virtual void sendUp(const std::vector& completeFrames); virtual bool hasFrameToTransmit(); @@ -118,6 +118,8 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr& responseHeader, Packet *receivedPacket, const Ptr& receivedHeader) override; virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override; + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; + virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); virtual bool isSentByUs(const Ptr& header) const; virtual bool isForUs(const Ptr& header) const; @@ -134,4 +136,3 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index e64ce4db852..5659cfdc3f5 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" @@ -69,7 +71,7 @@ void Hcf::initialize(int stage) // Edca resolves its Edcaf array at the link-layer stage. Install the // queue signal listeners after all child initialization has completed. for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) - check_and_cast(edca->getEdcaf(static_cast(ac))->getPendingQueue())->subscribe(packetDroppedSignal, this); + check_and_cast(edca->getEdcaf(static_cast(ac))->getPendingQueue())->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); } } @@ -154,19 +156,25 @@ void Hcf::processUpperFrame(Packet *packet, const Ptr(obj); - if (packet->findTag() != nullptr) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); - } + if (signal == queueing::IPacketQueue::packetQueueDepartureSignal) { + Enter_Method("packetQueueDeparture"); + if (source->isSubscribed(signal, this)) + handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); } else - ModeSetListener::receiveSignal(source, signalID, obj, details); + ModeSetListener::receiveSignal(source, signal, object, details); +} + +void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) +{ + Enter_Method("handlePacketRemoved"); + if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } } void Hcf::scheduleStartRxTimer(simtime_t timeout) @@ -828,4 +836,3 @@ Hcf::~Hcf() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 72f7af70fc4..c05a7a071af 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -36,6 +36,7 @@ #include "inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/recipient/CtsProcedure.h" +#include "inet/queueing/contract/IPacketQueue.h" namespace inet { namespace ieee80211 { @@ -107,7 +108,6 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void initialize(int stage) override; virtual void forEachChild(cVisitor *v) override; virtual void handleMessage(cMessage *msg) override; - virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; virtual void refreshDisplay() const override; void startFrameSequence(AccessCategory ac); @@ -157,6 +157,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr& responseHeader, Packet *receivedPacket, const Ptr& receivedHeader) override; virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override; + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; + virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); // IProcedureCallback virtual void scheduleInactivityTimer(simtime_t timeout) override; @@ -175,4 +177,3 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: } /* namespace inet */ #endif - diff --git a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test index 843fb1ade0b..62c1d4fc629 100644 --- a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test @@ -1,7 +1,7 @@ %description: Verify that a tagged Association or Reassociation Response dropped from an HCF EDCA compound pending queue reaches the AP as a terminal outcome. The -child management queue drop signal is propagated through the compound queue, +child management queue departure signal is propagated through the compound queue, pending state is cleared, and a retransmission completes without disturbing a committed association. @@ -23,6 +23,7 @@ class TestHcfManagementQueue : public queueing::PacketQueue { public: void setPacketCapacityForTest(int capacity) { packetCapacity = capacity; } + bool hasPacketDepartureListenerForTest() const { return !getLocalSignalListeners(queueing::IPacketQueue::packetQueueDepartureSignal).empty(); } }; Define_Module(TestHcfManagementQueue); @@ -31,6 +32,7 @@ class TestCompoundPendingQueueForHcf : public queueing::CompoundPacketQueueBase { public: void setPacketCapacityForTest(int capacity) { packetCapacity = capacity; } + bool hasPacketDepartureListenerForTest() const { return !getLocalSignalListeners(queueing::IPacketQueue::packetQueueDepartureSignal).empty(); } }; Define_Module(TestCompoundPendingQueueForHcf); @@ -129,6 +131,8 @@ class Ieee80211MgmtApHcfQueueDropTest : public cSimpleModule ASSERT(compoundQueue->getMaxNumPackets() == -1); ASSERT(managementQueue->getMaxNumPackets() == 0); + ASSERT(compoundQueue->hasPacketDepartureListenerForTest()); + ASSERT(managementQueue->hasPacketDepartureListenerForTest()); mgmt->markAuthenticated(station); mgmt->submitAssociationRequest(station); for (int i = 0; i < 100 && mgmt->statuses.empty(); i++) diff --git a/tests/module/Ieee80211MgmtApQueueDrop_1.test b/tests/module/Ieee80211MgmtApQueueDrop_1.test index 44a499578c2..d0b0a4838e4 100644 --- a/tests/module/Ieee80211MgmtApQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApQueueDrop_1.test @@ -105,7 +105,7 @@ class Ieee80211MgmtApQueueDropTest : public cSimpleModule ASSERT(!mgmt->hasPendingAssociation(station)); ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::AUTHENTICATED); - // The existing packetDroppedSignal is delivered before deletion, so the reservation + // The queue removal notification is delivered before deletion, so the reservation // is released synchronously and can be reused by another STA. ASSERT(mgmt->reserveAssociationId(reusable) == 1); mgmt->cancelAssociationIdReservation(reusable); diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test new file mode 100644 index 00000000000..91a9667ac13 --- /dev/null +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -0,0 +1,947 @@ +%description: +Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DCF/HCF continuation, and queue accounting. + +%includes: +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" +#include +#include + +#include "inet/common/PacketEventTag.h" +#include "inet/common/Simsignals.h" +#include "inet/common/TimeTag_m.h" +#include "inet/common/LabelsTag_m.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/queueing/gate/PacketGate.h" +#include "inet/queueing/gate/PeriodicGate.h" +#include "inet/queueing/queue/CompoundPacketQueueBase.h" +#include "inet/queueing/queue/PacketQueue.h" +#include "inet/queueing/scheduler/LabelScheduler.h" +#include "inet/queueing/scheduler/PriorityScheduler.h" +#include "inet/queueing/scheduler/WrrScheduler.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; +class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue +{ + public: + virtual ~TestPacketQueue() { + for (auto listener : getLocalSignalListeners(packetQueueDepartureSignal)) + unsubscribe(packetQueueDepartureSignal, listener); + } + std::vector packets; + int numSelectedDequeues = 0; + mutable int numGetPacketCalls = 0; + + void notifyPacketRemoved(Packet *packet, PacketRemovalReason reason) { + queueing::PacketQueueRemovalDetails details(reason); + emit(queueing::IPacketQueue::packetQueueDepartureSignal, packet, &details); + } + + virtual int getMaxNumPackets() const override { return -1; } + virtual int getNumPackets() const override { return packets.size(); } + virtual b getMaxTotalLength() const override { return b(-1); } + virtual b getTotalLength() const override { return b(0); } + virtual Packet *getPacket(int index) const override { numGetPacketCalls++; return packets.at(index); } + virtual bool isEmpty() const override { return packets.empty(); } + virtual void removePacket(Packet *packet) override { + packets.erase(std::find(packets.begin(), packets.end(), packet)); + notifyPacketRemoved(packet, PacketRemovalReason::REMOVED); + } + virtual void removeAllPackets() override { + auto removedPackets = packets; + packets.clear(); + for (auto packet : removedPackets) + notifyPacketRemoved(packet, PacketRemovalReason::REMOVED); + } + virtual void enqueuePacket(Packet *packet) override { packets.push_back(packet); } + virtual Packet *dequeuePacket() override { + auto packet = packets.front(); + packets.erase(packets.begin()); + notifyPacketRemoved(packet, PacketRemovalReason::DEQUEUED); + return packet; + } + virtual Packet *findPacket(const PacketPredicate& predicate) const override { + auto it = std::find_if(packets.begin(), packets.end(), predicate); + return it == packets.end() ? nullptr : *it; + } + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override { + auto packet = findPacket(predicate); + if (packet != nullptr) { + numSelectedDequeues++; + packets.erase(std::find(packets.begin(), packets.end(), packet)); + notifyPacketRemoved(packet, PacketRemovalReason::DEQUEUED); + } + return packet; + } + void dropPacketFromQueue(Packet *packet) { + packets.erase(std::find(packets.begin(), packets.end(), packet)); + notifyPacketRemoved(packet, PacketRemovalReason::DROPPED); + } + virtual bool canPullSomePacket(const cGate *) const override { return !packets.empty(); } + virtual Packet *canPullPacket(const cGate *) const override { return packets.empty() ? nullptr : packets.front(); } + virtual Packet *pullPacket(const cGate *) override { return dequeuePacket(); } + virtual Packet *pullPacketStart(const cGate *, bps) override { throw cRuntimeError("Unsupported"); } + virtual Packet *pullPacketEnd(const cGate *) override { throw cRuntimeError("Unsupported"); } + virtual Packet *pullPacketProgress(const cGate *, bps, b, b) override { throw cRuntimeError("Unsupported"); } + virtual bool canPushSomePacket(const cGate *) const override { return true; } + virtual bool canPushPacket(Packet *, const cGate *) const override { return true; } + virtual void pushPacket(Packet *packet, const cGate *) override { enqueuePacket(packet); } + virtual void pushPacketStart(Packet *, const cGate *, bps) override { throw cRuntimeError("Unsupported"); } + virtual void pushPacketEnd(Packet *, const cGate *) override { throw cRuntimeError("Unsupported"); } + virtual void pushPacketProgress(Packet *, const cGate *, bps, b, b) override { throw cRuntimeError("Unsupported"); } +}; +class TestCompoundPacketQueue : public queueing::CompoundPacketQueueBase +{ + public: + void configure(queueing::IPacketCollection *collection, queueing::IPacketExtractor *packetExtractor) { + this->collection = collection; + this->packetExtractor = packetExtractor; + } + void release(Packet *packet) { + take(packet); + drop(packet); + } + void observeDescendantQueues(cModule *module) { registerQueueFrontier(module); } + void observeQueue(queueing::IPacketQueue *queue) { childQueues.push_back(queue); check_and_cast(queue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); } + void addObserver(cListener *callback) { subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, callback); } + void stopObserving() { + for (auto queue : childQueues) + check_and_cast(queue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + childQueues.clear(); + for (auto listener : getLocalSignalListeners(queueing::IPacketQueue::packetQueueDepartureSignal)) + unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, listener); + } +}; + +class TestPriorityScheduler : public queueing::PriorityScheduler +{ + public: + void configure(const std::vector& collections, bool reverseOrder = false) { + std::vector packetExtractors; + for (auto collection : collections) + packetExtractors.push_back(check_and_cast(collection)); + configure(collections, packetExtractors, reverseOrder); + } + void configure(const std::vector& collections, const std::vector& packetExtractors, bool reverseOrder = false) { + ASSERT(collections.size() == packetExtractors.size()); + this->collections = collections; + this->packetExtractors = packetExtractors; + this->reverseOrder = reverseOrder; + numProcessedPackets = 0; + processedTotalLength = b(0); + } + int getNumProcessedPackets() const { return numProcessedPackets; } + void release(Packet *packet) { take(packet); drop(packet); } +}; + +class TestWrrScheduler : public queueing::WrrScheduler +{ + public: + void configure(const std::vector& collections, const std::vector& weights, const std::vector& buckets) { + ASSERT(collections.size() == weights.size()); + ASSERT(collections.size() == buckets.size()); + this->collections = collections; + for (auto collection : collections) + packetExtractors.push_back(check_and_cast(collection)); + this->weights = new unsigned int[weights.size()]; + this->buckets = new unsigned int[buckets.size()]; + for (size_t i = 0; i < collections.size(); i++) { + this->weights[i] = weights[i]; + this->buckets[i] = buckets[i]; + } + numProcessedPackets = 0; + processedTotalLength = b(0); + } + unsigned int getBucket(int index) const { return buckets[index]; } + int getNumProcessedPackets() const { return numProcessedPackets; } + void release(Packet *packet) { take(packet); drop(packet); } +}; + +class TestLabelScheduler : public queueing::LabelScheduler +{ + public: + void configure(const std::vector& collections, const std::vector& labels, int defaultGateIndex) { + this->collections = collections; + for (auto collection : collections) + packetExtractors.push_back(check_and_cast(collection)); + this->labels = labels; + this->defaultGateIndex = defaultGateIndex; + } + void release(Packet *packet) { take(packet); drop(packet); } +}; + +class TestPacketDepartureListener : public cListener +{ + public: + int numDequeuedPackets = 0; + int numRemovedPackets = 0; + int numDroppedPackets = 0; + int64_t lastDroppedPacketId = -1; + std::vector> expectedPacketCountsAtFirstDrop; + bool allExpectedPacketsDetachedAtFirstDrop = true; + + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (!source->isSubscribed(signal, this)) + return; + auto packet = check_and_cast(object); + auto reason = check_and_cast(details)->getReason(); + if (reason == queueing::IPacketQueue::PacketRemovalReason::DEQUEUED) + numDequeuedPackets++; + else if (reason == queueing::IPacketQueue::PacketRemovalReason::REMOVED) + numRemovedPackets++; + else if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED) { + if (numDroppedPackets == 0) + for (auto& [queue, expectedPacketCount] : expectedPacketCountsAtFirstDrop) + allExpectedPacketsDetachedAtFirstDrop &= queue->getNumPackets() == expectedPacketCount; + numDroppedPackets++; + lastDroppedPacketId = packet->getId(); + } + } +}; + +class TestReentrantRemovalListener : public cListener +{ + public: + queueing::IPacketQueue *queue = nullptr; + Packet *triggerPacket = nullptr; + Packet *packetToRemove = nullptr; + queueing::IPacketQueue::PacketRemovalReason triggerReason = queueing::IPacketQueue::PacketRemovalReason::REMOVED; + bool removedPacket = false; + + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (!source->isSubscribed(signal, this)) + return; + auto packet = check_and_cast(object); + auto reason = check_and_cast(details)->getReason(); + if (!removedPacket && packet == triggerPacket && reason == triggerReason) { + removedPacket = true; + queue->removePacket(packetToRemove); + } + } +}; + +class TestPacketPulledListener : public cListener +{ + public: + cComponent *leaf = nullptr; + cComponent *compound = nullptr; + int numLeafPulls = 0; + int numCompoundPulls = 0; + + virtual void receiveSignal(cComponent *source, simsignal_t, cObject *, cObject *) override { + if (source == leaf) + numLeafPulls++; + else if (source == compound) + numCompoundPulls++; + } +}; + +class TestPacketArrivalListener : public cListener +{ + public: + int numSignals = 0; + cGate *lastArrivalGate = nullptr; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *value, cObject *) override { + numSignals++; + lastArrivalGate = check_and_cast(value)->getArrivalGate(); + } +}; +class TestOnlyPacketExtractor : public queueing::IPacketExtractor +{ + public: + std::vector packets; + + virtual Packet *findPacket(const PacketPredicate& predicate) const override { + auto it = std::find_if(packets.begin(), packets.end(), predicate); + return it == packets.end() ? nullptr : *it; + } + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override { + auto packet = findPacket(predicate); + if (packet != nullptr) + packets.erase(std::find(packets.begin(), packets.end(), packet)); + return packet; + } +}; +static Ptr makeQosHeader(MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber) +{ + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(receiverAddress); + header->setTid(tid); + header->setSequenceNumber(sequenceNumber); + return header; +} +%activity: +MacAddress peer1("00:00:00:00:00:01"); +auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const char *dropperClass = "") { + auto module = cModuleType::get("inet.queueing.queue.PacketQueue")->create(name, this); + module->par("packetCapacity").setIntValue(packetCapacity); + module->par("dropperClass").setStringValue(dropperClass); + module->callInitialize(); + return check_and_cast(module); +}; +// Predicate extraction follows each provider's scheduling policy instead of +// the compound collection's gate-order enumeration. +{ + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto firstPacket = new Packet("priorityFirstPacket", makeShared(B(1))); + auto secondPacket = new Packet("prioritySecondPacket", makeShared(B(1))); + firstQueue.packets = { firstPacket }; + secondQueue.packets = { secondPacket }; + TestPriorityScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, true); + auto predicate = [](const Packet *) { return true; }; + ASSERT(scheduler.findPacket(predicate) == secondPacket); + auto dequeuedPacket = scheduler.dequeuePacket(predicate); + ASSERT(dequeuedPacket == secondPacket); + scheduler.release(secondPacket); + delete secondPacket; + firstQueue.removePacket(firstPacket); + delete firstPacket; +} + +// Priority extraction is independent of collection enumeration, so an input +// may support predicate extraction even when it cannot report aggregate size. +{ + TestOnlyPacketExtractor packetExtractor; + auto packet = new Packet("extractorOnlyPacket", makeShared(B(1))); + packetExtractor.packets = { packet }; + TestPriorityScheduler scheduler; + scheduler.configure(std::vector({ nullptr }), std::vector({ &packetExtractor })); + bool aggregateQueryFailed = false; + try { + scheduler.getNumPackets(); + } + catch (cRuntimeError& error) { + aggregateQueryFailed = std::string(error.what()).find("getNumPackets") != std::string::npos && std::string(error.what()).find("IPacketCollection") != std::string::npos; + } + ASSERT(aggregateQueryFailed); + bool aggregateLengthQueryFailed = false; + try { + scheduler.getTotalLength(); + } + catch (cRuntimeError& error) { + aggregateLengthQueryFailed = std::string(error.what()).find("getTotalLength") != std::string::npos && std::string(error.what()).find("IPacketCollection") != std::string::npos; + } + ASSERT(aggregateLengthQueryFailed); + ASSERT(scheduler.findPacket([](const Packet *) { return true; }) == packet); + auto dequeuedPacket = scheduler.dequeuePacket([](const Packet *) { return true; }); + ASSERT(dequeuedPacket == packet); + ASSERT(packetExtractor.packets.empty()); + scheduler.release(packet); + delete packet; +} +// Collection-only providers remain usable for aggregate queries and fail only +// when predicate extraction is actually requested. +{ + TestPacketQueue firstCollection; + TestPacketQueue collectionOnly; + auto firstPacket = new Packet("extractablePacket", makeShared(B(1))); + auto collectionOnlyPacket = new Packet("collectionOnlyPacket", makeShared(B(1))); + firstCollection.packets = { firstPacket }; + collectionOnly.packets = { collectionOnlyPacket }; + TestPriorityScheduler scheduler; + scheduler.configure(std::vector({ &firstCollection, &collectionOnly }), std::vector({ &firstCollection, nullptr })); + ASSERT(scheduler.getNumPackets() == 2); + bool extractionFailed = false; + try { + scheduler.findPacket([](const Packet *) { return true; }); + } + catch (cRuntimeError& error) { + extractionFailed = std::string(error.what()).find("findPacket") != std::string::npos && std::string(error.what()).find("IPacketExtractor") != std::string::npos; + } + ASSERT(extractionFailed); + firstCollection.removePacket(firstPacket); + collectionOnly.removePacket(collectionOnlyPacket); + delete firstPacket; + delete collectionOnlyPacket; +} +{ + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto firstPacket = new Packet("wrrFirstPacket", makeShared(B(1))); + auto secondPacket = new Packet("wrrSecondPacket", makeShared(B(1))); + firstQueue.packets = { firstPacket }; + secondQueue.packets = { secondPacket }; + TestWrrScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, { 1, 2 }, { 0, 2 }); + auto predicate = [](const Packet *) { return true; }; + ASSERT(scheduler.findPacket(predicate) == secondPacket); + ASSERT(scheduler.findPacket(predicate) == secondPacket); + ASSERT(scheduler.getBucket(1) == 2); + auto dequeuedPacket = scheduler.dequeuePacket(predicate); + ASSERT(dequeuedPacket == secondPacket); + ASSERT(scheduler.getBucket(1) == 1); + scheduler.release(secondPacket); + delete secondPacket; + firstQueue.removePacket(firstPacket); + delete firstPacket; +} +{ + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto blockedPacket = new Packet("labelBlockedPacket", makeShared(B(1))); + auto lowPacket = new Packet("labelLowPacket", makeShared(B(1))); + auto highPacket = new Packet("labelHighPacket", makeShared(B(1))); + auto lowLabels = lowPacket->addTag(); + lowLabels->setLabelsArraySize(1); + lowLabels->setLabels(0, "low"); + auto highLabels = highPacket->addTag(); + highLabels->setLabelsArraySize(1); + highLabels->setLabels(0, "high"); + firstQueue.packets = { blockedPacket, lowPacket }; + secondQueue.packets = { highPacket }; + TestLabelScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, { "high", "low" }, 0); + auto predicate = [blockedPacket](const Packet *packet) { return packet != blockedPacket; }; + ASSERT(scheduler.findPacket(predicate) == highPacket); + auto dequeuedPacket = scheduler.dequeuePacket(predicate); + ASSERT(dequeuedPacket == highPacket); + scheduler.release(highPacket); + delete highPacket; + firstQueue.removePacket(blockedPacket); + firstQueue.removePacket(lowPacket); + delete blockedPacket; + delete lowPacket; +} + + +// Closed gates expose no predicate-selected packet and remove nothing. Once +// opened, the same exact candidate crosses through PacketFlowBase accounting. +{ + auto leafQueue = createPacketQueue("closedGateLeaf"); + auto gateModule = cModuleType::get("inet.queueing.gate.PacketGate")->create("closedPredicateGate", this); + gateModule->par("initiallyOpen").setBoolValue(false); + gateModule->par("openTime").setDoubleValue(10); + gateModule->par("closeTime").setDoubleValue(20); + leafQueue->gate("out")->connectTo(gateModule->gate("in")); + gateModule->callInitialize(); + auto packet = new Packet("closedGatePacket", makeShared(B(1))); + leafQueue->pushPacket(packet, nullptr); + auto packetExtractor = check_and_cast(gateModule); + auto closedFoundPacket = packetExtractor->findPacket([](const Packet *) { return true; }); + auto closedDequeuedPacket = packetExtractor->dequeuePacket([](const Packet *) { return true; }); + ASSERT(closedFoundPacket == nullptr); + ASSERT(closedDequeuedPacket == nullptr); + ASSERT(leafQueue->getNumPackets() == 1); + check_and_cast(gateModule)->open(); + ASSERT(packetExtractor->findPacket([](const Packet *) { return true; }) == packet); + auto dequeuedPacket = packetExtractor->dequeuePacket([](const Packet *) { return true; }); + ASSERT(dequeuedPacket == packet); + ASSERT(leafQueue->getNumPackets() == 0); + take(packet); + delete packet; + gateModule->callFinish(); + leafQueue->callFinish(); + gateModule->deleteModule(); + leafQueue->deleteModule(); +} + +// An implicit guard band rejects the provider's exact first match instead of +// skipping it and extracting a later packet that happens to fit. +{ + auto leafQueue = createPacketQueue("guardBandLeaf"); + auto gateModule = cModuleType::get("inet.queueing.gate.PeriodicGate")->create("predicateGuardBandGate", this); + auto durations = new cValueArray(); + durations->add(cValue(1, "s")); + durations->add(cValue(1, "s")); + gateModule->par("durations").setObjectValue(durations); + gateModule->par("initiallyOpen").setBoolValue(true); + gateModule->par("bitrate").setDoubleValue(8); + leafQueue->gate("out")->connectTo(gateModule->gate("in")); + gateModule->callInitialize(); + auto blockedPacket = new Packet("guardBandBlockedPacket", makeShared(B(2))); + auto fittingPacket = new Packet("guardBandFittingPacket", makeShared(B(1))); + leafQueue->pushPacket(blockedPacket, nullptr); + leafQueue->pushPacket(fittingPacket, nullptr); + auto packetExtractor = check_and_cast(gateModule); + auto blockedFoundPacket = packetExtractor->findPacket([](const Packet *) { return true; }); + auto blockedDequeuedPacket = packetExtractor->dequeuePacket([](const Packet *) { return true; }); + ASSERT(blockedFoundPacket == nullptr); + ASSERT(blockedDequeuedPacket == nullptr); + ASSERT(leafQueue->getNumPackets() == 2); + auto dequeuedPacket = packetExtractor->dequeuePacket([blockedPacket](const Packet *packet) { return packet != blockedPacket; }); + ASSERT(dequeuedPacket == fittingPacket); + ASSERT(leafQueue->getNumPackets() == 1); + take(fittingPacket); + delete fittingPacket; + auto retainedPacket = leafQueue->dequeuePacket([blockedPacket](const Packet *packet) { return packet == blockedPacket; }); + ASSERT(retainedPacket == blockedPacket); + take(blockedPacket); + delete blockedPacket; + gateModule->callFinish(); + leafQueue->callFinish(); + gateModule->deleteModule(); + leafQueue->deleteModule(); +} +// A real queue with a downstream collector predicate-dequeues a non-front +// packet, records its residence and semantic pull once, and animates it to the +// collector while retaining the front packet. +{ + TestPacketPulledListener pullListener; + TestPacketDepartureListener removalCallback; + auto leafQueueModule = cModuleType::get("inet.queueing.queue.PacketQueue")->create("selectedDequeueQueue", this); + auto sinkModule = cModuleType::get("inet.queueing.sink.FullPacketSink")->create("selectedDequeueSink", this); + leafQueueModule->gate("out")->connectTo(sinkModule->gate("in")); + leafQueueModule->callInitialize(); + sinkModule->callInitialize(); + auto leafQueue = check_and_cast(leafQueueModule); + pullListener.leaf = leafQueue; + leafQueue->subscribe(packetPulledSignal, &pullListener); + check_and_cast(leafQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &removalCallback); + auto frontPacket = new Packet("selectedDequeueFront", makeShared(B(1))); + auto selectedPacket = new Packet("selectedDequeueMatch", makeQosHeader(peer1, 1, SequenceNumberCyclic(1))); + auto queueingTimeTag = selectedPacket->addRegionTag(); + queueingTimeTag->setBitTotalTimesArraySize(1); + queueingTimeTag->setBitTotalTimes(0, 0); + queueingTimeTag->setPacketTotalTimesArraySize(1); + queueingTimeTag->setPacketTotalTimes(0, 0); + selectedPacket->addRegionTag(); + selectedPacket->setArrivalTime(simTime() - 1); + leafQueue->pushPacket(frontPacket, nullptr); + leafQueue->pushPacket(selectedPacket, nullptr); + auto dequeuedPacket = leafQueue->dequeuePacket([selectedPacket](const Packet *packet) { return packet == selectedPacket; }); + ASSERT(dequeuedPacket == selectedPacket); + int numQueuedEvents = 0; + selectedPacket->mapAllRegionTags(b(0), selectedPacket->getTotalLength(), [&](b, b, const Ptr& tag) { + for (size_t i = 0; i < tag->getPacketEventsArraySize(); i++) + if (tag->getPacketEvents(i)->getKind() == PEK_QUEUED) + numQueuedEvents++; + }); + simtime_t totalQueueingTime = -1; + selectedPacket->mapAllRegionTags(b(0), selectedPacket->getTotalLength(), [&](b, b, const Ptr& tag) { + ASSERT(tag->getPacketTotalTimesArraySize() == 1); + totalQueueingTime = tag->getPacketTotalTimes(0); + }); + ASSERT(numQueuedEvents == 1); + ASSERT(totalQueueingTime == 1); + ASSERT(leafQueue->getNumPackets() == 1); + ASSERT(leafQueue->getPacket(0) == frontPacket); + ASSERT(pullListener.numLeafPulls == 1); + ASSERT(removalCallback.numDequeuedPackets == 1); + ASSERT(selectedPacket->getArrivalGate() == sinkModule->gate("in")); + leafQueue->unsubscribe(packetPulledSignal, &pullListener); + check_and_cast(leafQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &removalCallback); + leafQueue->removePacket(frontPacket); + take(frontPacket); + take(selectedPacket); + delete frontPacket; + delete selectedPacket; + sinkModule->callFinish(); + leafQueueModule->callFinish(); + sinkModule->deleteModule(); + leafQueueModule->deleteModule(); +} + +// Predicate extraction through a concrete PacketFlowBase module processes a +// non-front packet once. The queue animates it to the flow input before the +// flow signals, and the flow then animates it to the final collector. +{ + TestPacketPulledListener pullListener; + TestPacketDepartureListener removalCallback; + TestPacketArrivalListener flowInputListener; + TestPacketArrivalListener flowOutputListener; + auto leafQueueModule = cModuleType::get("inet.queueing.queue.PacketQueue")->create("selectedFlowLeaf", this); + auto gateModule = cModuleType::get("inet.queueing.gate.PacketGate")->create("selectedFlowGate", this); + auto sinkModule = cModuleType::get("inet.queueing.sink.FullPacketSink")->create("selectedFlowSink", this); + gateModule->par("initiallyOpen").setBoolValue(true); + gateModule->par("openTime").setDoubleValue(10); + gateModule->par("closeTime").setDoubleValue(20); + leafQueueModule->gate("out")->connectTo(gateModule->gate("in")); + gateModule->gate("out")->connectTo(sinkModule->gate("in")); + leafQueueModule->callInitialize(); + gateModule->callInitialize(); + sinkModule->callInitialize(); + auto leafQueue = check_and_cast(leafQueueModule); + auto packetGate = check_and_cast(gateModule); + auto packetExtractor = check_and_cast(gateModule); + pullListener.leaf = leafQueue; + leafQueue->subscribe(packetPulledSignal, &pullListener); + check_and_cast(leafQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &removalCallback); + gateModule->subscribe(packetPulledInSignal, &flowInputListener); + gateModule->subscribe(packetPulledOutSignal, &flowOutputListener); + auto frontPacket = new Packet("selectedFlowFront", makeShared(B(1))); + auto selectedPacket = new Packet("selectedFlowMatch", makeShared(B(1))); + leafQueue->pushPacket(frontPacket, nullptr); + leafQueue->pushPacket(selectedPacket, nullptr); + auto dequeuedPacket = packetExtractor->dequeuePacket([selectedPacket](const Packet *packet) { return packet == selectedPacket; }); + ASSERT(dequeuedPacket == selectedPacket); + ASSERT(leafQueue->getNumPackets() == 1); + ASSERT(leafQueue->getPacket(0) == frontPacket); + ASSERT(pullListener.numLeafPulls == 1); + ASSERT(removalCallback.numDequeuedPackets == 1); + ASSERT(flowInputListener.numSignals == 1); + ASSERT(flowInputListener.lastArrivalGate == gateModule->gate("in")); + ASSERT(flowOutputListener.numSignals == 1); + ASSERT(flowOutputListener.lastArrivalGate == gateModule->gate("in")); + ASSERT(packetGate->resolveDirective('p') == "1"); + ASSERT(selectedPacket->getArrivalGate() == sinkModule->gate("in")); + leafQueue->unsubscribe(packetPulledSignal, &pullListener); + check_and_cast(leafQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &removalCallback); + gateModule->unsubscribe(packetPulledInSignal, &flowInputListener); + gateModule->unsubscribe(packetPulledOutSignal, &flowOutputListener); + leafQueue->removePacket(frontPacket); + take(frontPacket); + take(selectedPacket); + delete frontPacket; + delete selectedPacket; + sinkModule->callFinish(); + gateModule->callFinish(); + leafQueueModule->callFinish(); + sinkModule->deleteModule(); + gateModule->deleteModule(); + leafQueueModule->deleteModule(); +} +// The typed queue callback covers every non-destructive ownership departure, +// including bulk removal, exactly once at the queue boundary. +{ + auto lifecycleQueue = createPacketQueue("lifecycleQueue"); + TestPacketDepartureListener lifecycleCallback; + check_and_cast(lifecycleQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &lifecycleCallback); + auto pulledPacket = new Packet("lifecyclePulled", makeShared(B(1))); + auto selectedPacket = new Packet("lifecycleSelected", makeShared(B(1))); + auto removedPacket = new Packet("lifecycleRemoved", makeShared(B(1))); + auto firstBulkRemovedPacket = new Packet("lifecycleBulkRemovedFirst", makeShared(B(1))); + auto secondBulkRemovedPacket = new Packet("lifecycleBulkRemovedSecond", makeShared(B(1))); + auto thirdBulkRemovedPacket = new Packet("lifecycleBulkRemovedThird", makeShared(B(1))); + lifecycleQueue->pushPacket(pulledPacket, nullptr); + lifecycleQueue->pushPacket(selectedPacket, nullptr); + lifecycleQueue->pushPacket(removedPacket, nullptr); + lifecycleQueue->pushPacket(firstBulkRemovedPacket, nullptr); + lifecycleQueue->pushPacket(secondBulkRemovedPacket, nullptr); + lifecycleQueue->pushPacket(thirdBulkRemovedPacket, nullptr); + auto pulledResult = lifecycleQueue->dequeuePacket(); + auto selectedResult = lifecycleQueue->dequeuePacket([selectedPacket](const Packet *packet) { return packet == selectedPacket; }); + lifecycleQueue->removePacket(removedPacket); + take(pulledResult); + take(selectedResult); + take(removedPacket); + delete pulledResult; + delete selectedResult; + delete removedPacket; + lifecycleQueue->removeAllPackets(); + if (lifecycleCallback.numDequeuedPackets != 2 || lifecycleCallback.numRemovedPackets != 4 || lifecycleCallback.numDroppedPackets != 0 || !lifecycleQueue->isEmpty()) + throw cRuntimeError("Typed queue lifecycle callback did not cover every departure exactly once"); + check_and_cast(lifecycleQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &lifecycleCallback); + lifecycleQueue->callFinish(); + lifecycleQueue->deleteModule(); +} + + +// Bulk removal detaches only this queue's packets from a shared buffer. A +// direct buffer removal then notifies the owning queue exactly once. +{ + auto bufferModule = cModuleType::get("inet.queueing.buffer.PacketBuffer")->create("sharedRemovalBuffer", this); + bufferModule->callInitialize(); + auto createBufferedQueue = [&](const char *name) { + auto module = cModuleType::get("inet.queueing.queue.PacketQueue")->create(name, this); + module->par("bufferModule").setStringValue("^.sharedRemovalBuffer"); + module->callInitialize(); + return check_and_cast(module); + }; + auto firstQueue = createBufferedQueue("sharedRemovalFirstQueue"); + auto secondQueue = createBufferedQueue("sharedRemovalSecondQueue"); + TestPacketDepartureListener firstCallback; + TestPacketDepartureListener secondCallback; + TestPacketDepartureListener compoundCallback; + TestCompoundPacketQueue bufferedCompound; + bufferedCompound.observeQueue(secondQueue); + bufferedCompound.addObserver(&compoundCallback); + check_and_cast(firstQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &firstCallback); + check_and_cast(secondQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &secondCallback); + firstQueue->pushPacket(new Packet("sharedRemovalFirstA", makeShared(B(1))), nullptr); + firstQueue->pushPacket(new Packet("sharedRemovalFirstB", makeShared(B(1))), nullptr); + auto retainedPacket = new Packet("sharedRemovalRetained", makeShared(B(1))); + secondQueue->pushPacket(retainedPacket, nullptr); + firstQueue->removeAllPackets(); + auto buffer = check_and_cast(bufferModule); + ASSERT(firstQueue->isEmpty()); + ASSERT(secondQueue->getNumPackets() == 1); + ASSERT(buffer->getNumPackets() == 1); + ASSERT(firstCallback.numRemovedPackets == 2); + ASSERT(secondCallback.numRemovedPackets == 0); + buffer->removePacket(retainedPacket); + ASSERT(secondQueue->isEmpty()); + ASSERT(buffer->getNumPackets() == 0); + ASSERT(secondCallback.numRemovedPackets == 1); + ASSERT(secondCallback.numDroppedPackets == 0); + ASSERT(compoundCallback.numRemovedPackets == 1); + ASSERT(compoundCallback.numDroppedPackets == 0); + take(retainedPacket); + delete retainedPacket; + bufferedCompound.stopObserving(); + check_and_cast(firstQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &firstCallback); + check_and_cast(secondQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &secondCallback); + firstQueue->callFinish(); + secondQueue->callFinish(); + bufferModule->callFinish(); + firstQueue->deleteModule(); + secondQueue->deleteModule(); + bufferModule->deleteModule(); +} + +// A shared buffer rejects cPacketQueue ownership it cannot detach before any +// mutation, preventing a later overload drop from corrupting the owner queue. +{ + auto bufferModule = cModuleType::get("inet.queueing.buffer.PacketBuffer")->create("unsupportedOwnerBuffer", this); + bufferModule->callInitialize(); + auto buffer = check_and_cast(bufferModule); + cPacketQueue unsupportedOwnerQueue("unsupportedOwnerQueue"); + auto packet = new Packet("unsupportedOwnerPacket", makeShared(B(1))); + unsupportedOwnerQueue.insert(packet); + bool unsupportedOwnerRejected = false; + try { + buffer->addPacket(packet); + } + catch (cRuntimeError& error) { + unsupportedOwnerRejected = std::string(error.what()).find("IPacketBuffer::ICallback") != std::string::npos; + } + ASSERT(unsupportedOwnerRejected); + ASSERT(buffer->getNumPackets() == 0); + ASSERT(unsupportedOwnerQueue.contains(packet)); + unsupportedOwnerQueue.remove(packet); + take(packet); + delete packet; + bufferModule->callFinish(); + bufferModule->deleteModule(); +} + +// Internal overflow invokes the typed queue callback once for the destructive +// discard and never for the packet later removed normally. +{ + auto overflowQueue = createPacketQueue("overflowQueue", 1, "inet::queueing::PacketAtCollectionEndDropper"); + TestPacketDepartureListener dropCallback; + check_and_cast(overflowQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + auto retainedPacket = new Packet("retainedPacket", makeShared(B(1))); + auto overflowPacket = new Packet("overflowPacket", makeShared(B(1))); + auto overflowPacketId = overflowPacket->getId(); + overflowQueue->pushPacket(retainedPacket, nullptr); + overflowQueue->pushPacket(overflowPacket, nullptr); + ASSERT(dropCallback.numDroppedPackets == 1); + ASSERT(dropCallback.lastDroppedPacketId == overflowPacketId); + check_and_cast(overflowQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + auto dequeuedPacket = overflowQueue->dequeuePacket([retainedPacket](const Packet *packet) { return packet == retainedPacket; }); + ASSERT(dequeuedPacket == retainedPacket); + ASSERT(dropCallback.numDroppedPackets == 1); + overflowQueue->callFinish(); + overflowQueue->deleteModule(); +} + +// A compound unregisters from child queues during pre-delete even when finish +// is skipped, and repeated lifecycle cleanup is harmless. +{ + auto pendingQueueModule = cModuleType::get("inet.linklayer.ieee80211.mac.queue.CompoundPendingQueue")->create("preDeletePendingQueue", this); + pendingQueueModule->callInitialize(); + auto pendingQueue = check_and_cast(pendingQueueModule); + auto leafQueue = check_and_cast(pendingQueueModule->getSubmodule("managementQueue")); + TestPacketDepartureListener callback; + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &callback); + + auto observedPacket = new Packet("preDeleteObservedPacket", makeShared(B(1))); + leafQueue->pushPacket(observedPacket, nullptr); + leafQueue->removePacket(observedPacket); + ASSERT(callback.numRemovedPackets == 1); + take(observedPacket); + delete observedPacket; + + pendingQueueModule->callPreDelete(pendingQueueModule); + auto unobservedPacket = new Packet("preDeleteUnobservedPacket", makeShared(B(1))); + leafQueue->pushPacket(unobservedPacket, nullptr); + leafQueue->removePacket(unobservedPacket); + ASSERT(callback.numRemovedPackets == 1); + take(unobservedPacket); + delete unobservedPacket; + + pendingQueueModule->deleteModule(); +} + +// A compound's direct frontier and a nested compound boundary each forward +// descendant removals exactly once. Boundary-initiated removals suppress the +// descendant signal for the same packet, including during reentrant removal +// of a different packet. +{ + auto pendingQueueModule = cModuleType::get("inet.linklayer.ieee80211.mac.queue.CompoundPendingQueue")->create("callbackPendingQueue", this); + pendingQueueModule->callInitialize(); + auto pendingQueue = check_and_cast(pendingQueueModule); + auto directLeaf = check_and_cast(pendingQueueModule->getSubmodule("managementQueue")); + TestPacketDepartureListener directCallback; + TestPacketDepartureListener nestedCallback; + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &directCallback); + TestCompoundPacketQueue outerCompound; + outerCompound.observeQueue(pendingQueue); + outerCompound.addObserver(&nestedCallback); + + auto directPacket = new Packet("directCompoundDrop", makeShared(B(1))); + directLeaf->handlePacketDropped(directPacket); + ASSERT(directCallback.numDroppedPackets == 1); + ASSERT(nestedCallback.numDroppedPackets == 1); + delete directPacket; + + auto leafRemovedPacket = new Packet("leafRemovedPacket", makeShared(B(1))); + directLeaf->pushPacket(leafRemovedPacket, nullptr); + directLeaf->removePacket(leafRemovedPacket); + ASSERT(directCallback.numRemovedPackets == 1); + ASSERT(nestedCallback.numRemovedPackets == 1); + take(leafRemovedPacket); + delete leafRemovedPacket; + + directLeaf->pushPacket(new Packet("leafBulkRemovedFirst", makeShared(B(1))), nullptr); + directLeaf->pushPacket(new Packet("leafBulkRemovedSecond", makeShared(B(1))), nullptr); + directLeaf->removeAllPackets(); + ASSERT(directCallback.numRemovedPackets == 3); + ASSERT(nestedCallback.numRemovedPackets == 3); + + auto boundaryRemovedPacket = new Packet("boundaryRemovedPacket", makeShared(B(1))); + directLeaf->pushPacket(boundaryRemovedPacket, nullptr); + pendingQueue->removePacket(boundaryRemovedPacket); + ASSERT(directCallback.numRemovedPackets == 4); + ASSERT(nestedCallback.numRemovedPackets == 4); + take(boundaryRemovedPacket); + delete boundaryRemovedPacket; + + directLeaf->pushPacket(new Packet("boundaryBulkRemovedFirst", makeShared(B(1))), nullptr); + directLeaf->pushPacket(new Packet("boundaryBulkRemovedSecond", makeShared(B(1))), nullptr); + pendingQueue->removeAllPackets(); + ASSERT(directCallback.numRemovedPackets == 6); + ASSERT(nestedCallback.numRemovedPackets == 6); + + auto boundaryDequeuedPacket = new Packet("boundaryDequeuedPacket", makeShared(B(1))); + directLeaf->pushPacket(boundaryDequeuedPacket, nullptr); + auto dequeuedPacket = pendingQueue->dequeuePacket([boundaryDequeuedPacket](const Packet *packet) { return packet == boundaryDequeuedPacket; }); + ASSERT(dequeuedPacket == boundaryDequeuedPacket); + ASSERT(directCallback.numDequeuedPackets == 1); + ASSERT(nestedCallback.numDequeuedPackets == 0); + take(dequeuedPacket); + delete dequeuedPacket; + + auto boundaryPulledPacket = new Packet("boundaryPulledPacket", makeShared(B(1))); + directLeaf->pushPacket(boundaryPulledPacket, nullptr); + auto pulledPacket = pendingQueue->pullPacket(nullptr); + ASSERT(pulledPacket == boundaryPulledPacket); + ASSERT(directCallback.numDequeuedPackets == 2); + ASSERT(nestedCallback.numDequeuedPackets == 0); + take(pulledPacket); + delete pulledPacket; + + auto reentrantTriggerPacket = new Packet("reentrantTriggerPacket", makeShared(B(1))); + auto reentrantRemovedPacket = new Packet("reentrantRemovedPacket", makeShared(B(1))); + directLeaf->pushPacket(reentrantTriggerPacket, nullptr); + directLeaf->pushPacket(reentrantRemovedPacket, nullptr); + TestReentrantRemovalListener reentrantCallback; + reentrantCallback.queue = directLeaf; + reentrantCallback.triggerPacket = reentrantTriggerPacket; + reentrantCallback.packetToRemove = reentrantRemovedPacket; + check_and_cast(directLeaf)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &reentrantCallback); + pendingQueue->removePacket(reentrantTriggerPacket); + ASSERT(reentrantCallback.removedPacket); + ASSERT(directCallback.numRemovedPackets == 8); + ASSERT(nestedCallback.numRemovedPackets == 8); + check_and_cast(directLeaf)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &reentrantCallback); + take(reentrantTriggerPacket); + take(reentrantRemovedPacket); + delete reentrantTriggerPacket; + delete reentrantRemovedPacket; + + outerCompound.stopObserving(); + check_and_cast(pendingQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &directCallback); + pendingQueueModule->callFinish(); + pendingQueueModule->deleteModule(); + + // Starting the frontier at the enclosing module forces traversal through + // PacketShaper, which is not an IPacketQueue, before reaching its leaf. + auto packetShaperModule = cModuleType::get("inet.queueing.shaper.PacketShaper")->create("callbackPacketShaper", this); + packetShaperModule->callInitialize(); + auto wrappedLeaf = check_and_cast(packetShaperModule->getSubmodule("queue")); + TestPacketDepartureListener wrappedCallback; + TestCompoundPacketQueue wrappedCompound; + wrappedCompound.observeDescendantQueues(this); + wrappedCompound.addObserver(&wrappedCallback); + auto wrappedPacket = new Packet("wrappedCompoundDrop", makeShared(B(1))); + wrappedLeaf->handlePacketDropped(wrappedPacket); + ASSERT(wrappedCallback.numDroppedPackets == 1); + delete wrappedPacket; + wrappedCompound.stopObserving(); + packetShaperModule->callFinish(); + packetShaperModule->deleteModule(); +} + +// Compound capacity enforcement detaches its leaf victim without leaking a +// REMOVED callback before reporting the single destructive departure. +{ + auto pendingQueueModule = cModuleType::get("inet.linklayer.ieee80211.mac.queue.CompoundPendingQueue")->create("callbackOverflowPendingQueue", this); + pendingQueueModule->par("packetCapacity").setIntValue(1); + pendingQueueModule->par("dropperClass").setStringValue("inet::queueing::PacketAtCollectionEndDropper"); + pendingQueueModule->callInitialize(); + auto pendingQueue = check_and_cast(pendingQueueModule); + TestPacketDepartureListener directCallback; + TestPacketDepartureListener nestedCallback; + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &directCallback); + TestCompoundPacketQueue outerCompound; + outerCompound.observeQueue(pendingQueue); + outerCompound.addObserver(&nestedCallback); + pendingQueue->pushPacket(new Packet("compoundRetainedPacket", makeQosHeader(peer1, 1, SequenceNumberCyclic(1))), nullptr); + pendingQueue->pushPacket(new Packet("compoundOverflowPacket", makeQosHeader(peer1, 1, SequenceNumberCyclic(2))), nullptr); + ASSERT(pendingQueue->getNumPackets() == 1); + ASSERT(directCallback.numDroppedPackets == 1); + ASSERT(directCallback.numRemovedPackets == 0); + ASSERT(nestedCallback.numDroppedPackets == 1); + ASSERT(nestedCallback.numRemovedPackets == 0); + outerCompound.stopObserving(); + check_and_cast(pendingQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &directCallback); + pendingQueue->removeAllPackets(); + pendingQueueModule->callFinish(); + pendingQueueModule->deleteModule(); +} + +// A shared buffer detaches every victim from its owning queue before the first +// typed drop callback. This keeps protocol cleanup from reentering while a +// later victim is still selectable from another queue. +{ + auto bufferModule = cModuleType::get("inet.queueing.buffer.PacketBuffer")->create("sharedOverflowBuffer", this); + bufferModule->par("dataCapacity").setIntValue(24); + bufferModule->par("dropperClass").setStringValue("inet::queueing::PacketAtCollectionBeginDropper"); + bufferModule->callInitialize(); + auto createBufferedQueue = [&](const char *name) { + auto module = cModuleType::get("inet.queueing.queue.PacketQueue")->create(name, this); + module->par("bufferModule").setStringValue("^.sharedOverflowBuffer"); + module->callInitialize(); + return check_and_cast(module); + }; + auto firstQueue = createBufferedQueue("sharedOverflowFirstQueue"); + auto secondQueue = createBufferedQueue("sharedOverflowSecondQueue"); + TestPacketDepartureListener dropCallback; + dropCallback.expectedPacketCountsAtFirstDrop = { { firstQueue, 1 }, { secondQueue, 0 } }; + check_and_cast(firstQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + check_and_cast(secondQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + auto firstVictim = new Packet("sharedFirstVictim", makeShared(B(1))); + auto retainedPacket = new Packet("sharedRetainedPacket", makeShared(B(3))); + TestReentrantRemovalListener reentrantCallback; + reentrantCallback.queue = firstQueue; + reentrantCallback.triggerPacket = firstVictim; + reentrantCallback.packetToRemove = retainedPacket; + reentrantCallback.triggerReason = queueing::IPacketQueue::PacketRemovalReason::DROPPED; + check_and_cast(firstQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &reentrantCallback); + firstQueue->pushPacket(firstVictim, nullptr); + secondQueue->pushPacket(new Packet("sharedSecondVictim", makeShared(B(1))), nullptr); + firstQueue->pushPacket(retainedPacket, nullptr); + if (dropCallback.numDroppedPackets != 2 || dropCallback.numRemovedPackets != 1 || !dropCallback.allExpectedPacketsDetachedAtFirstDrop || !reentrantCallback.removedPacket || firstQueue->getNumPackets() != 0 || secondQueue->getNumPackets() != 0) + throw cRuntimeError("Shared buffer reentrant removal violated the detached victim batch contract"); + take(retainedPacket); + delete retainedPacket; + check_and_cast(firstQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &reentrantCallback); + check_and_cast(firstQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + check_and_cast(secondQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + firstQueue->callFinish(); + secondQueue->callFinish(); + bufferModule->callFinish(); + firstQueue->deleteModule(); + secondQueue->deleteModule(); + bufferModule->deleteModule(); +} +EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; +%contains: stdout +ADDBA transaction and negotiated SSN integrity checks passed. From e4f08f3e84b3dc507b1d0a03f7ee886ffc9b86f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:02 +0200 Subject: [PATCH 03/16] ieee80211: make originator ADDBA transactions explicit Represent an originator ADDBA exchange as explicit state keyed by peer and TID, with a transaction identity carried by every request fragment. Start the exchange only after the final trigger MPDU fragment is acknowledged, so the advertised starting sequence number is valid. Match responses by dialog token, keep same-TID traffic ineligible while a request is pending, and handle response timeout, retry backoff, DELBA, and terminal cancellation without allowing stale fragments to affect a newer exchange. Keep A-MSDU selection and HCF continuation aligned with that state. Cover trigger acknowledgement, fragmented requests and teardowns, timeout and retry paths, cancellation ownership, queue eligibility, and provider-aware A-MSDU extraction. --- .../aggregation/BasicMsduAggregationPolicy.cc | 61 +- .../aggregation/BasicMsduAggregationPolicy.h | 2 +- .../blockack/Ieee80211AddbaTransactionTag.msg | 16 + .../blockack/OriginatorBlockAckAgreement.h | 27 +- .../OriginatorBlockAckAgreementHandler.cc | 287 ++- .../OriginatorBlockAckAgreementHandler.h | 30 +- .../OriginatorBlockAckAgreementPolicy.cc | 17 +- .../OriginatorBlockAckAgreementPolicy.h | 7 +- .../OriginatorBlockAckAgreementPolicy.ned | 3 +- .../ieee80211/mac/channelaccess/Edca.h | 2 +- .../IBlockAckAgreementHandlerCallback.h | 6 +- .../mac/contract/IMsduAggregationPolicy.h | 7 +- .../IOriginatorBlockAckAgreementHandler.h | 35 +- .../IOriginatorBlockAckAgreementPolicy.h | 4 +- .../mac/contract/IOriginatorMacDataService.h | 9 +- .../ieee80211/mac/coordinationfunction/Dcf.cc | 6 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 401 +++- .../ieee80211/mac/coordinationfunction/Hcf.h | 25 +- .../ieee80211/mac/framesequence/HcfFs.cc | 23 +- .../mac/originator/NonQosRecoveryProcedure.cc | 34 +- .../mac/originator/NonQosRecoveryProcedure.h | 4 +- .../originator/OriginatorMacDataService.cc | 22 +- .../mac/originator/OriginatorMacDataService.h | 5 +- .../originator/OriginatorQosMacDataService.cc | 80 +- .../originator/OriginatorQosMacDataService.h | 7 +- .../ieee80211/mac/queue/InProgressFrames.cc | 25 +- .../ieee80211/mac/queue/InProgressFrames.h | 3 +- .../ieee80211/mgmt/Ieee80211MgmtFrame.msg | 1 + tests/unit/Ieee80211AddbaTransaction_1.test | 1609 +++++++++++++++++ 29 files changed, 2558 insertions(+), 200 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag.msg diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc index 2d06484c6a4..951f011c5ee 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc @@ -56,40 +56,61 @@ bool BasicMsduAggregationPolicy::isEligible(Packet *packet, const Ptr *BasicMsduAggregationPolicy::computeAggregateFrames(queueing::IPacketQueue *queue) +std::vector *BasicMsduAggregationPolicy::computeAggregateFrames(queueing::IPacketQueue *queue, Packet *candidate, const std::function& isFrameEligible) { Enter_Method("computeAggregateFrames"); - ASSERT(!queue->isEmpty()); + ASSERT(candidate != nullptr); b aMsduLength = b(0); - Ptr firstHeader = nullptr; - auto frames = new std::vector(); + int candidateIndex = -1; for (int i = 0; i < queue->getNumPackets(); i++) { - auto dataPacket = queue->getPacket(i); + if (queue->getPacket(i) == candidate) { candidateIndex = i; break; } + } + if (candidateIndex == -1) + return nullptr; + const auto& firstHeader = dynamicPtrCast(candidate->peekAtFront()); + if (firstHeader == nullptr || !isFrameEligible(candidate)) + return nullptr; + auto frames = new std::vector(); + auto hasSameFlow = [&](const Ptr& dataHeader) { + return dataHeader != nullptr && dataHeader->getTid() == firstHeader->getTid() && + dataHeader->getReceiverAddress() == firstHeader->getReceiverAddress() && + dataHeader->getTransmitterAddress() == firstHeader->getTransmitterAddress(); + }; + auto appendIfEligible = [&](Packet *dataPacket) { const auto& dataHeader = dynamicPtrCast(dataPacket->peekAtFront()); - if (dataHeader == nullptr) - break; - if (firstHeader == nullptr) - firstHeader = dataHeader; + if (!hasSameFlow(dataHeader)) + return true; + // IEEE Std 802.11-2024, 5.1.3: preserve the ordering of MSDUs with the + // same traffic identifier. Enumeration order is the conservative + // intra-flow order for this built-in policy. Never overtake a + // same-flow packet which is held by transaction eligibility or cannot + // fit in the current A-MSDU. + if (!isFrameEligible(dataPacket)) + return false; const auto& dataTrailer = dataPacket->peekAtBack(B(4)); - if (!isEligible(dataPacket, staticPtrCast(dataHeader), dataTrailer, firstHeader, aMsduLength)) { - EV_TRACE << "Queued " << *dataPacket << " is not eligible for A-MSDU aggregation.\n"; - break; - } - EV_TRACE << "Queued " << *dataPacket << " is eligible for A-MSDU aggregation.\n"; + if (!isEligible(dataPacket, dataHeader, dataTrailer, firstHeader, aMsduLength)) + return false; frames->push_back(dataPacket); - aMsduLength += dataPacket->getDataLength() - dataHeader->getChunkLength() - dataTrailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER); // sum of MSDU lengths + subframe header + aMsduLength += dataPacket->getDataLength() - dataHeader->getChunkLength() - dataTrailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER); + return true; + }; + if (!appendIfEligible(candidate)) { + delete frames; + return nullptr; } + // Do not wrap around: providers may schedule in an order different from + // their IPacketCollection enumeration (for example reverse priority). + for (int i = candidateIndex + 1; i < queue->getNumPackets(); i++) + if (!appendIfEligible(queue->getPacket(i))) + break; if (frames->size() <= 1 || !isAggregationPossible(frames->size(), aMsduLength.get())) { EV_DEBUG << "A-MSDU aggregation is not possible, collected " << frames->size() << " packets.\n"; delete frames; return nullptr; } - else { - EV_DEBUG << "A-MSDU aggregation is possible, collected " << frames->size() << " packets.\n"; - return frames; - } + EV_DEBUG << "A-MSDU aggregation is possible, collected " << frames->size() << " packets.\n"; + return frames; } } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h index 0534914eaf4..22ddab978ea 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h @@ -28,7 +28,7 @@ class INET_API BasicMsduAggregationPolicy : public IMsduAggregationPolicy, publi virtual bool isEligible(Packet *packet, const Ptr& header, const Ptr& trailer, const Ptr& testHeader, b aMsduLength); public: - virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *queue) override; + virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *queue, Packet *candidate, const std::function& isFrameEligible) override; }; } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag.msg b/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag.msg new file mode 100644 index 00000000000..43d9033c743 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag.msg @@ -0,0 +1,16 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +import inet.common.INETDefs; +import inet.common.TagBase; + +namespace inet::ieee80211; + +// Identifies one local originator ADDBA transaction across packet transformations. +class Ieee80211AddbaTransactionTag extends TagBase +{ + uint64_t transactionId; +} diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index a516527bc34..8099d7c1bdb 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -15,6 +15,12 @@ namespace ieee80211 { class OriginatorBlockAckAgreementHandler; +enum class OriginatorBlockAckAgreementState +{ + PENDING, + ESTABLISHED, +}; + class INET_API OriginatorBlockAckAgreement : public cObject { protected: @@ -25,19 +31,24 @@ class INET_API OriginatorBlockAckAgreement : public cObject int bufferSize = -1; bool isAMsduSupported = false; bool isDelayedBlockAckPolicySupported = false; - bool isAddbaResponseReceived = false; + OriginatorBlockAckAgreementState state = OriginatorBlockAckAgreementState::PENDING; bool isAddbaRequestSent = false; + uint8_t dialogToken = 0; + uint64_t transactionId = 0; + simtime_t addbaResponseDeadline = -1; simtime_t blockAckTimeoutValue = -1; simtime_t expirationTime = -1; public: - OriginatorBlockAckAgreement(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, bool isAMsduSupported, bool isDelayedBlockAckPolicySupported) : + OriginatorBlockAckAgreement(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, bool isAMsduSupported, bool isDelayedBlockAckPolicySupported, uint8_t dialogToken, uint64_t transactionId) : receiverAddr(receiverAddr), tid(tid), startingSequenceNumber(startingSequenceNumber), bufferSize(bufferSize), isAMsduSupported(isAMsduSupported), - isDelayedBlockAckPolicySupported(isDelayedBlockAckPolicySupported) + isDelayedBlockAckPolicySupported(isDelayedBlockAckPolicySupported), + dialogToken(dialogToken), + transactionId(transactionId) { } @@ -46,7 +57,8 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual int getBufferSize() const { return bufferSize; } virtual SequenceNumberCyclic getStartingSequenceNumber() { return startingSequenceNumber; } virtual void setStartingSequenceNumber(SequenceNumberCyclic sequenceNumber) { startingSequenceNumber = sequenceNumber; } - virtual bool getIsAddbaResponseReceived() const { return isAddbaResponseReceived; } + virtual bool getIsAddbaResponseReceived() const { return state == OriginatorBlockAckAgreementState::ESTABLISHED; } + virtual bool isPending() const { return state == OriginatorBlockAckAgreementState::PENDING; } virtual bool getIsAddbaRequestSent() const { return isAddbaRequestSent; } virtual bool getIsAMsduSupported() const { return isAMsduSupported; } virtual bool getIsDelayedBlockAckPolicySupported() const { return isDelayedBlockAckPolicySupported; } @@ -54,13 +66,17 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual Tid getTid() const { return tid; } virtual const simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } virtual int getNumSentBaPolicyFrames() const { return numSentBaPolicyFrames; } + virtual uint8_t getDialogToken() const { return dialogToken; } + virtual uint64_t getTransactionId() const { return transactionId; } + virtual simtime_t getAddbaResponseDeadline() const { return addbaResponseDeadline; } virtual void setBufferSize(int bufferSize) { this->bufferSize = bufferSize; } - virtual void setIsAddbaResponseReceived(bool isAddbaResponseReceived) { this->isAddbaResponseReceived = isAddbaResponseReceived; } + virtual void setIsAddbaResponseReceived(bool isAddbaResponseReceived) { state = isAddbaResponseReceived ? OriginatorBlockAckAgreementState::ESTABLISHED : OriginatorBlockAckAgreementState::PENDING; } virtual void setIsAddbaRequestSent(bool isAddbaRequestSent) { this->isAddbaRequestSent = isAddbaRequestSent; } virtual void setIsAMsduSupported(bool isAMsduSupported) { this->isAMsduSupported = isAMsduSupported; } virtual void setIsDelayedBlockAckPolicySupported(bool isDelayedBlockAckPolicySupported) { this->isDelayedBlockAckPolicySupported = isDelayedBlockAckPolicySupported; } virtual void setBlockAckTimeoutValue(const simtime_t blockAckTimeoutValue) { this->blockAckTimeoutValue = blockAckTimeoutValue; } + virtual void setAddbaResponseDeadline(simtime_t addbaResponseDeadline) { this->addbaResponseDeadline = addbaResponseDeadline; } virtual void baPolicyFrameSent() { numSentBaPolicyFrames++; } virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } @@ -71,4 +87,3 @@ class INET_API OriginatorBlockAckAgreement : public cObject } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 2f11dc365db..d03bae7b498 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -7,18 +7,30 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include + #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" namespace inet { namespace ieee80211 { -void OriginatorBlockAckAgreementHandler::createAgreement(const Ptr& addbaRequest) +void OriginatorBlockAckAgreementHandler::createAgreement(const Ptr& addbaRequest, uint64_t transactionId) { - OriginatorBlockAckAgreement *blockAckAgreement = new OriginatorBlockAckAgreement(addbaRequest->getReceiverAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaRequest->getBufferSize(), addbaRequest->getAMsduSupported(), addbaRequest->getBlockAckPolicy() == 0); + ASSERT(addbaRequest->getDialogToken() != 0); + OriginatorBlockAckAgreement *blockAckAgreement = new OriginatorBlockAckAgreement(addbaRequest->getReceiverAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaRequest->getBufferSize(), addbaRequest->getAMsduSupported(), addbaRequest->getBlockAckPolicy() == 0, addbaRequest->getDialogToken(), transactionId); auto agreementId = std::make_pair(addbaRequest->getReceiverAddress(), addbaRequest->getTid()); blockAckAgreements[agreementId] = blockAckAgreement; } +uint8_t OriginatorBlockAckAgreementHandler::allocateDialogToken() +{ + auto dialogToken = nextDialogToken; + nextDialogToken = nextDialogToken == 255 ? 1 : nextDialogToken + 1; + return dialogToken; +} + simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime() { simtime_t earliestTime = SIMTIME_MAX; @@ -33,6 +45,57 @@ simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime() return earliestTime; } +simtime_t OriginatorBlockAckAgreementHandler::computeEarliestAddbaResponseDeadline() const +{ + simtime_t earliestDeadline = SIMTIME_MAX; + for (const auto& entry : blockAckAgreements) { + auto agreement = entry.second; + if (agreement->isPending() && agreement->getIsAddbaRequestSent()) { + ASSERT(agreement->getAddbaResponseDeadline() >= 0); + earliestDeadline = std::min(earliestDeadline, agreement->getAddbaResponseDeadline()); + } + } + return earliestDeadline; +} + +simtime_t OriginatorBlockAckAgreementHandler::getAddbaResponseTimeout(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) const +{ + auto timeout = blockAckAgreementPolicy->getAddbaResponseTimeout(); + if (timeout <= 0) + throw cRuntimeError("ADDBA response timeout must be greater than zero"); + return timeout; +} + +void OriginatorBlockAckAgreementHandler::recordAddbaFailure(MacAddress receiverAddr, Tid tid, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) +{ + auto retryBackoff = blockAckAgreementPolicy->computeAddbaRetryBackoff(); + if (retryBackoff < 0) + throw cRuntimeError("ADDBA retry backoff must not be negative"); + addbaRetryDeadlines[std::make_pair(receiverAddr, tid)] = simTime() + retryBackoff; +} + +void OriginatorBlockAckAgreementHandler::addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) +{ + auto now = simTime(); + std::vector expiredTransactionIds; + for (auto it = blockAckAgreements.begin(); it != blockAckAgreements.end();) { + auto agreement = it->second; + if (agreement->isPending() && agreement->getIsAddbaRequestSent() && agreement->getAddbaResponseDeadline() <= now) { + EV_INFO << "ADDBA transaction timeout for receiver=" << agreement->getReceiverAddr() << " tid=" << (int)agreement->getTid() << endl; + auto transactionId = agreement->getTransactionId(); + recordAddbaFailure(agreement->getReceiverAddr(), agreement->getTid(), blockAckAgreementPolicy); + it = blockAckAgreements.erase(it); + delete agreement; + expiredTransactionIds.push_back(transactionId); + } + else + it++; + } + for (auto transactionId : expiredTransactionIds) + callback->cancelAddbaTransaction(transactionId, nullptr); + scheduleAddbaResponseTimer(callback); +} + void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the @@ -57,6 +120,9 @@ const Ptr OriginatorBlockAckAgreementHandler::buildAddbaR { auto addbaRequest = makeShared(); addbaRequest->setReceiverAddress(receiverAddr); + // IEEE Std 802.11-2024, 9.6.4.2: a solicited ADDBA Request uses a nonzero + // Dialog Token, and the corresponding response copies that token. + addbaRequest->setDialogToken(allocateDialogToken()); addbaRequest->setTid(tid); addbaRequest->setAMsduSupported(blockAckAgreementPolicy->isMsduSupported()); addbaRequest->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue()); @@ -92,6 +158,11 @@ void OriginatorBlockAckAgreementHandler::scheduleInactivityTimer(IBlockAckAgreem callback->scheduleInactivityTimer(earliestExpirationTime); } +void OriginatorBlockAckAgreementHandler::scheduleAddbaResponseTimer(IBlockAckAgreementHandlerCallback *callback) +{ + callback->scheduleAddbaResponseTimer(computeEarliestAddbaResponseDeadline()); +} + OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(MacAddress receiverAddr, Tid tid) { auto agreementId = std::make_pair(receiverAddr, tid); @@ -99,6 +170,12 @@ OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(Ma return it != blockAckAgreements.end() ? it->second : nullptr; } +bool OriginatorBlockAckAgreementHandler::isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const +{ + auto it = blockAckAgreements.find(std::make_pair(receiverAddr, tid)); + return it != blockAckAgreements.end() && it->second->isPending(); +} + const Ptr OriginatorBlockAckAgreementHandler::buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode) { auto delba = makeShared(); @@ -110,37 +187,102 @@ const Ptr OriginatorBlockAckAgreementHandler::buildDelba(MacAddr return delba; } -void OriginatorBlockAckAgreementHandler::terminateAgreement(MacAddress originatorAddr, Tid tid) +OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::removeAgreement(MacAddress originatorAddr, Tid tid) { auto agreementId = std::make_pair(originatorAddr, tid); auto it = blockAckAgreements.find(agreementId); if (it != blockAckAgreements.end()) { - OriginatorBlockAckAgreement *agreement = it->second; + auto agreement = it->second; blockAckAgreements.erase(it); - delete agreement; + return agreement; } + return nullptr; +} + +void OriginatorBlockAckAgreementHandler::terminateAgreement(MacAddress originatorAddr, Tid tid) +{ + delete removeAgreement(originatorAddr, tid); } -void OriginatorBlockAckAgreementHandler::processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) +uint64_t OriginatorBlockAckAgreementHandler::processAcknowledgedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback) { - auto agreement = getAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid()); - if (blockAckAgreementPolicy->isAddbaReqNeeded(packet, dataHeader) && agreement == nullptr) { - auto addbaReq = buildAddbaRequest(dataHeader->getReceiverAddress(), dataHeader->getTid(), dataHeader->getSequenceNumber() + 1, blockAckAgreementPolicy); - createAgreement(addbaReq); + // IEEE Std 802.11-2024, Table 9-466: the Starting Sequence Number identifies + // the first or next MSDU/A-MSDU sent under the agreement. Wait until the + // final fragment is acknowledged so no remaining fragment precedes the SSN. + if (dataHeader->getMoreFragments()) + return 0; + auto receiverAddr = dataHeader->getReceiverAddress(); + auto tid = dataHeader->getTid(); + auto agreementId = std::make_pair(receiverAddr, tid); + auto agreement = getAgreement(receiverAddr, tid); + auto retryIt = addbaRetryDeadlines.find(agreementId); + bool retryAllowed = retryIt == addbaRetryDeadlines.end() || retryIt->second <= simTime(); + uint64_t obsoleteTeardownTransactionId = 0; + if (blockAckAgreementPolicy->isAddbaReqNeeded(packet, dataHeader) && agreement == nullptr && retryAllowed) { + if (retryIt != addbaRetryDeadlines.end()) + addbaRetryDeadlines.erase(retryIt); + // A replacement agreement makes an older best-effort DELBA stale. Drop + // its eligibility instead of waiting indefinitely for a queue callback + // that every custom packet provider may not deliver. + auto teardownIt = pendingTeardownTransactionIds.find(agreementId); + if (teardownIt != pendingTeardownTransactionIds.end()) { + obsoleteTeardownTransactionId = teardownIt->second; + pendingTeardownTransactionIds.erase(teardownIt); + } + // IEEE Std 802.11-2024, 10.25.2 and 11.5.2.2: Normal Ack data is + // permitted before an agreement exists, and the requested SSN starts + // after the acknowledged trigger MPDU. + auto addbaReq = buildAddbaRequest(receiverAddr, tid, dataHeader->getSequenceNumber() + 1, blockAckAgreementPolicy); + auto transactionId = nextTransactionId++; + createAgreement(addbaReq, transactionId); auto addbaPacket = new Packet("AddbaReq", addbaReq); - callback->processMgmtFrame(addbaPacket, addbaReq); + addbaPacket->addTag()->setTransactionId(transactionId); + procedureCallback->processMgmtFrame(addbaPacket, addbaReq); } + return obsoleteTeardownTransactionId; } -void OriginatorBlockAckAgreementHandler::processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) +OriginatorBlockAckAgreementResponse OriginatorBlockAckAgreementHandler::processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) { + OriginatorBlockAckAgreementResponse response; auto agreement = getAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid()); - if (blockAckAgreementPolicy->isAddbaReqAccepted(addbaResp, agreement)) { + // IEEE Std 802.11-2024, 11.5.2.2: only a successful response matching the + // outstanding peer, TID, and Dialog Token establishes the agreement. + if (agreement == nullptr || !agreement->isPending() || !agreement->getIsAddbaRequestSent() || agreement->getDialogToken() != addbaResp->getDialogToken()) + return response; + bool acceptedByLocalPolicy = addbaResp->getStatusCode() == 0 && blockAckAgreementPolicy->isAddbaReqAccepted(addbaResp, agreement); + if (addbaResp->getStatusCode() == 0) { + auto transactionId = agreement->getTransactionId(); updateAgreement(agreement, addbaResp); + if (acceptedByLocalPolicy) + addbaRetryDeadlines.erase(std::make_pair(addbaResp->getTransmitterAddress(), addbaResp->getTid())); + else + recordAddbaFailure(addbaResp->getTransmitterAddress(), addbaResp->getTid(), blockAckAgreementPolicy); scheduleInactivityTimer(callback); + scheduleAddbaResponseTimer(callback); + callback->cancelAddbaTransaction(transactionId, nullptr); + if (!acceptedByLocalPolicy) { + // IEEE Std 802.11-2024, 10.25.2 Note 3: delete a successful + // agreement rejected by local policy and continue with Normal Ack. + response.terminatedAgreement.reset(removeAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid())); + if (response.terminatedAgreement == nullptr) + throw cRuntimeError("Cannot terminate locally vetoed Block Ack agreement"); + response.teardownDelba = buildDelba(addbaResp->getTransmitterAddress(), addbaResp->getTid(), RC_END_BA); + pendingTeardownTransactionIds[std::make_pair(addbaResp->getTransmitterAddress(), addbaResp->getTid())] = transactionId; + response.teardownTransactionId = transactionId; + scheduleInactivityTimer(callback); + } + else + response.establishedAgreement = agreement; + return response; } else { - // TODO send a new one? + auto transactionId = agreement->getTransactionId(); + recordAddbaFailure(addbaResp->getTransmitterAddress(), addbaResp->getTid(), blockAckAgreementPolicy); + terminateAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid()); + scheduleAddbaResponseTimer(callback); + callback->cancelAddbaTransaction(transactionId, nullptr); + return response; } } @@ -152,24 +294,122 @@ void OriginatorBlockAckAgreementHandler::updateAgreement(OriginatorBlockAckAgree agreement->calculateExpirationTime(); } -void OriginatorBlockAckAgreementHandler::processTransmittedAddbaReq(const Ptr& addbaReq) +bool OriginatorBlockAckAgreementHandler::isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const +{ + auto it = blockAckAgreements.find(std::make_pair(addbaReq->getReceiverAddress(), addbaReq->getTid())); + auto transactionTag = packet->findTag(); + return it != blockAckAgreements.end() && it->second->isPending() && transactionTag != nullptr && + it->second->getDialogToken() == addbaReq->getDialogToken() && it->second->getTransactionId() == transactionTag->getTransactionId(); +} + +bool OriginatorBlockAckAgreementHandler::isDelbaPending(const Packet *packet, const Ptr& delba) const +{ + if (!delba->getInitiator()) + return true; + auto transactionTag = packet->findTag(); + if (transactionTag == nullptr) + return true; + auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + return it != pendingTeardownTransactionIds.end() && it->second == transactionTag->getTransactionId(); +} + +void OriginatorBlockAckAgreementHandler::processTransmittedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) { auto agreement = getAgreement(addbaReq->getReceiverAddress(), addbaReq->getTid()); - if (agreement) + if (isAddbaRequestPending(packet, addbaReq) && !addbaReq->getMoreFragments() && !agreement->getIsAddbaRequestSent()) { + agreement->setAddbaResponseDeadline(simTime() + getAddbaResponseTimeout(blockAckAgreementPolicy)); agreement->setIsAddbaRequestSent(true); - else - throw cRuntimeError("Block Ack Agreement should have already been added"); + scheduleAddbaResponseTimer(callback); + } + else if (!isAddbaRequestPending(packet, addbaReq)) + EV_WARN << "Ignoring stale transmitted ADDBA Request for receiver=" << addbaReq->getReceiverAddress() << " tid=" << (int)addbaReq->getTid() << " dialogToken=" << (int)addbaReq->getDialogToken() << endl; } -void OriginatorBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) +void OriginatorBlockAckAgreementHandler::processDroppedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) { - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + if (isAddbaRequestPending(packet, addbaReq)) { + auto transactionId = packet->getTag()->getTransactionId(); + recordAddbaFailure(addbaReq->getReceiverAddress(), addbaReq->getTid(), blockAckAgreementPolicy); + terminateAgreement(addbaReq->getReceiverAddress(), addbaReq->getTid()); + scheduleAddbaResponseTimer(callback); + callback->cancelAddbaTransaction(transactionId, packet); + } } -void OriginatorBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) +std::unique_ptr OriginatorBlockAckAgreementHandler::processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - terminateAgreement(delba->getTransmitterAddress(), delba->getTid()); + auto delba = packet->peekAtFront(); + auto transactionTag = packet->findTag(); + if (transactionTag != nullptr) { + auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it == pendingTeardownTransactionIds.end() || it->second != transactionTag->getTransactionId()) + return nullptr; + // IEEE Std 802.11-2024, 11.5.3.2: teardown is performed by + // transmitting DELBA. IEEE Std 802.11-2024, 10.23.2.12.1 and + // 10.3.4.4 require unsuccessful MMPDU attempts to be retried until + // success or the applicable retry limit. Keep the local transaction + // live across ordinary MAC retries and retire it only after the final + // fragment is acknowledged or the frame is terminally aborted. + return nullptr; + } + auto agreement = getAgreement(delba->getReceiverAddress(), delba->getTid()); + bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); + auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; + std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleAddbaResponseTimer(callback); + if (cancelPendingTransaction) + callback->cancelAddbaTransaction(transactionId, nullptr); + return terminatedAgreement; +} + +bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = packet->peekAtFront(); + if (!delba->getInitiator() || delba->getMoreFragments()) + return false; + auto transactionTag = packet->findTag(); + if (transactionTag == nullptr) + return false; + auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it == pendingTeardownTransactionIds.end() || it->second != transactionTag->getTransactionId()) + return false; + auto transactionId = it->second; + pendingTeardownTransactionIds.erase(it); + callback->cancelAddbaTransaction(transactionId, packet); + return true; +} + +bool OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = packet->peekAtFront(); + if (!delba->getInitiator()) + return false; + auto transactionTag = packet->findTag(); + if (transactionTag != nullptr) { + auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it != pendingTeardownTransactionIds.end() && it->second == transactionTag->getTransactionId()) { + auto transactionId = it->second; + pendingTeardownTransactionIds.erase(it); + callback->cancelAddbaTransaction(transactionId, packet); + return true; + } + } + return false; +} + +std::unique_ptr OriginatorBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) +{ + if (blockAckAgreementPolicy->isDelbaAccepted(delba)) { + auto agreement = getAgreement(delba->getTransmitterAddress(), delba->getTid()); + bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); + auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; + std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + scheduleAddbaResponseTimer(callback); + if (cancelPendingTransaction) + callback->cancelAddbaTransaction(transactionId, nullptr); + return terminatedAgreement; + } + return nullptr; } OriginatorBlockAckAgreementHandler::~OriginatorBlockAckAgreementHandler() @@ -180,4 +420,3 @@ OriginatorBlockAckAgreementHandler::~OriginatorBlockAckAgreementHandler() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 4a449d17da3..8be627c5135 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -21,31 +21,47 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg { protected: std::map, OriginatorBlockAckAgreement *> blockAckAgreements; + std::map, simtime_t> addbaRetryDeadlines; + std::map, uint64_t> pendingTeardownTransactionIds; + uint8_t nextDialogToken = 1; + uint64_t nextTransactionId = 1; protected: virtual const Ptr buildAddbaRequest(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); - virtual void createAgreement(const Ptr& addbaRequest); + virtual uint8_t allocateDialogToken(); + virtual void createAgreement(const Ptr& addbaRequest, uint64_t transactionId); virtual void updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp); + virtual OriginatorBlockAckAgreement *removeAgreement(MacAddress originatorAddr, Tid tid); virtual void terminateAgreement(MacAddress originatorAddr, Tid tid); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); virtual simtime_t computeEarliestExpirationTime(); + virtual simtime_t computeEarliestAddbaResponseDeadline() const; virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback); + virtual void scheduleAddbaResponseTimer(IBlockAckAgreementHandlerCallback *callback); + virtual simtime_t getAddbaResponseTimeout(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) const; + virtual void recordAddbaFailure(MacAddress receiverAddr, Tid tid, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); public: virtual ~OriginatorBlockAckAgreementHandler(); - virtual void processTransmittedAddbaReq(const Ptr& addbaReq) override; - virtual void processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) override; + virtual void processTransmittedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; + virtual void processDroppedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; + virtual uint64_t processAcknowledgedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback) override; virtual void processReceivedBlockAck(const Ptr& blockAck, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) override; - virtual void processTransmittedDelba(const Ptr& delba) override; + virtual OriginatorBlockAckAgreementResponse processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; virtual OriginatorBlockAckAgreement *getAgreement(MacAddress receiverAddr, Tid tid) override; + virtual bool isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const override; + virtual bool isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const override; + virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const override; }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc index 44b4c0874fa..0382f53e378 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc @@ -24,17 +24,16 @@ void OriginatorBlockAckAgreementPolicy::initialize(int stage) aMsduSupported = par("aMsduSupported"); maximumAllowedBufferSize = par("maximumAllowedBufferSize"); blockAckTimeoutValue = par("blockAckTimeoutValue"); - // TODO addbaFailureTimeout = par("addbaFailureTimeout"); + addbaResponseTimeout = par("addbaResponseTimeout"); + addbaRetryBackoff = par("addbaRetryBackoff"); + if (addbaResponseTimeout <= 0) + throw cRuntimeError("addbaResponseTimeout must be greater than zero"); + if (addbaRetryBackoff < 0) + throw cRuntimeError("addbaRetryBackoff must not be negative"); WATCH(blockAckReqThreshold); } } -simtime_t OriginatorBlockAckAgreementPolicy::computeAddbaFailureTimeout() const -{ - // TODO ADDBAFailureTimeout -- 6.3.29.2.2 Semantics of the service primitive - throw cRuntimeError("Unimplemented"); -} - bool OriginatorBlockAckAgreementPolicy::isAddbaReqNeeded(Packet *packet, const Ptr& header) { return ackPolicy->isBlockAckPolicyEligibleFrame(packet, header); @@ -42,8 +41,7 @@ bool OriginatorBlockAckAgreementPolicy::isAddbaReqNeeded(Packet *packet, const P bool OriginatorBlockAckAgreementPolicy::isAddbaReqAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) { - ASSERT(agreement); - return true; + return agreement != nullptr && addbaResp->getStatusCode() == 0; } bool OriginatorBlockAckAgreementPolicy::isDelbaAccepted(const Ptr& delba) @@ -53,4 +51,3 @@ bool OriginatorBlockAckAgreementPolicy::isDelbaAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) override; virtual bool isDelbaAccepted(const Ptr& delba) override; - virtual simtime_t computeAddbaFailureTimeout() const override; + virtual simtime_t getAddbaResponseTimeout() const override { return addbaResponseTimeout; } + virtual simtime_t computeAddbaRetryBackoff() const override { return addbaRetryBackoff; } virtual bool isMsduSupported() const override { return aMsduSupported; } virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } @@ -48,4 +50,3 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetListener, publi } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned index bbb569b4b20..89e42723736 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned @@ -23,6 +23,7 @@ simple OriginatorBlockAckAgreementPolicy extends SimpleModule like IOriginatorBl bool aMsduSupported = default(true); int maximumAllowedBufferSize = default(64); double blockAckTimeoutValue @unit(s) = default(0s); // 0 means that it depends on the originator + double addbaResponseTimeout @unit(s) = default(1s); // Model response-wait timeout after the ADDBA Request is transmitted + double addbaRetryBackoff @unit(s) = default(1s); // Model retry suppression after a failed or discarded ADDBA transaction; not an IEEE 802.11 timer @display("i=block/control"); } - diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h index c481e9a82c8..69436da824d 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h @@ -38,6 +38,7 @@ class INET_API Edca : public SimpleModule virtual ~Edca(); virtual AccessCategory classifyFrame(const Ptr& header); + virtual int getNumEdcafs() const { return numEdcafs; } virtual Edcaf *getEdcaf(AccessCategory ac) const { return edcafs[ac]; } virtual Edcaf *getChannelOwner(); virtual std::vector getInternallyCollidedEdcafs(); @@ -51,4 +52,3 @@ class INET_API Edca : public SimpleModule } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index cc4a7b86b92..09abd395fd3 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -11,6 +11,9 @@ #include "inet/common/INETDefs.h" namespace inet { + +class Packet; + namespace ieee80211 { class INET_API IBlockAckAgreementHandlerCallback @@ -19,10 +22,11 @@ class INET_API IBlockAckAgreementHandlerCallback virtual ~IBlockAckAgreementHandlerCallback() {} virtual void scheduleInactivityTimer(simtime_t timeout) = 0; + virtual void scheduleAddbaResponseTimer(simtime_t deadline) = 0; + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) = 0; }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h index 8260a067cef..3a08e3a8cbe 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h @@ -8,6 +8,8 @@ #ifndef __INET_IMSDUAGGREGATIONPOLICY_H #define __INET_IMSDUAGGREGATIONPOLICY_H +#include + #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/queueing/contract/IPacketQueue.h" @@ -19,11 +21,12 @@ class INET_API IMsduAggregationPolicy public: virtual ~IMsduAggregationPolicy() {} - virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *queue) = 0; + // A non-null result is caller-owned and must contain at least two unique, + // discoverable, eligible frames with candidate as its first element. + virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *queue, Packet *candidate, const std::function& isFrameEligible) = 0; }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h index 4a01e89b6c3..fbe0918749a 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h @@ -8,9 +8,12 @@ #ifndef __INET_IORIGINATORBLOCKACKAGREEMENTHANDLER_H #define __INET_IORIGINATORBLOCKACKAGREEMENTHANDLER_H +#include + #include "inet/common/packet/Packet.h" #include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" #include "inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h" @@ -19,24 +22,44 @@ namespace inet { namespace ieee80211 { +struct INET_API OriginatorBlockAckAgreementResponse +{ + // Borrowed from the handler; valid while the established agreement remains installed. + OriginatorBlockAckAgreement *establishedAgreement = nullptr; + // Owns the agreement that was established and immediately terminated after a local veto. + std::unique_ptr terminatedAgreement; + Ptr teardownDelba; + uint64_t teardownTransactionId = 0; +}; + class INET_API IOriginatorBlockAckAgreementHandler { public: virtual ~IOriginatorBlockAckAgreementHandler() {} virtual void processReceivedBlockAck(const Ptr& blockAck, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void processTransmittedAddbaReq(const Ptr& addbaReq) = 0; - virtual void processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) = 0; - virtual void processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; - virtual void processTransmittedDelba(const Ptr& delba) = 0; + virtual void processTransmittedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual void processDroppedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; + // Returns the transaction identity of an obsolete teardown whose packets + // must be cancelled by the caller, or 0 when there is none. + virtual uint64_t processAcknowledgedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback) = 0; + virtual OriginatorBlockAckAgreementResponse processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; + // Returns true when the packet completed or aborted its tagged teardown + // transaction and sibling packets were cancelled through the callback. + virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; + virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; virtual OriginatorBlockAckAgreement *getAgreement(MacAddress receiverAddr, Tid tid) = 0; + virtual bool isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const = 0; + virtual bool isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const = 0; + virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const { return true; } }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h index 40ea82aa95e..11a5f26dfe6 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h @@ -26,7 +26,8 @@ class INET_API IOriginatorBlockAckAgreementPolicy virtual bool isDelbaAccepted(const Ptr& delba) = 0; virtual bool isMsduSupported() const = 0; - virtual simtime_t computeAddbaFailureTimeout() const = 0; + virtual simtime_t getAddbaResponseTimeout() const = 0; + virtual simtime_t computeAddbaRetryBackoff() const = 0; virtual simtime_t getBlockAckTimeoutValue() const = 0; virtual bool isDelayedAckPolicySupported() const = 0; virtual int getMaximumAllowedBufferSize() const = 0; @@ -36,4 +37,3 @@ class INET_API IOriginatorBlockAckAgreementPolicy } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h index 4e66ec6c190..9f0f8aad18a 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h @@ -8,6 +8,8 @@ #ifndef __INET_IORIGINATORMACDATASERVICE_H #define __INET_IORIGINATORMACDATASERVICE_H +#include + #include "inet/queueing/contract/IPacketQueue.h" namespace inet { @@ -15,6 +17,9 @@ namespace ieee80211 { class INET_API IOriginatorMacDataService { + public: + using FrameEligibilityFunction = std::function; + public: static simsignal_t packetFragmentedSignal; static simsignal_t packetAggregatedSignal; @@ -22,6 +27,9 @@ class INET_API IOriginatorMacDataService public: virtual ~IOriginatorMacDataService() {} + virtual void setFrameEligibilityFunction(const FrameEligibilityFunction& frameEligibilityFunction) = 0; + virtual bool isFrameEligible(const Packet *packet) const = 0; + virtual bool hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const = 0; virtual std::vector *extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) = 0; }; @@ -29,4 +37,3 @@ class INET_API IOriginatorMacDataService } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index 675b26be37a..1aa1a7ba496 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -82,6 +82,12 @@ void Dcf::channelGranted(IChannelAccess *channelAccess) Enter_Method("channelGranted"); ASSERT(this->channelAccess == channelAccess); if (!frameSequenceHandler->isSequenceRunning()) { + if (this->channelAccess->getInProgressFrames()->getFrameToTransmit() == nullptr) { + EV_DETAIL << "Releasing channel because no frame is available.\n"; + channelAccess->releaseChannel(this); + mac->sendDownPendingRadioConfigMsg(); + return; + } frameSequenceHandler->startFrameSequence(new DcfFs(), buildContext(), this); emit(IFrameSequenceHandler::frameSequenceStartedSignal, frameSequenceHandler->getContext()); } diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 5659cfdc3f5..010529ff44b 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -13,9 +13,11 @@ #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" @@ -40,6 +42,7 @@ void Hcf::initialize(int stage) mac = check_and_cast(getContainingNicModule(this)->getSubmodule("mac")); startRxTimer = new cMessage("startRxTimeout"); inactivityTimer = new cMessage("blockAckInactivityTimer"); + addbaResponseTimer = new cMessage("addbaResponseTimer"); edca = check_and_cast(getSubmodule("edca")); hcca = check_and_cast(getSubmodule("hcca")); tx = check_and_cast(getModuleByPath(par("txModule"))); @@ -65,14 +68,121 @@ void Hcf::initialize(int stage) originatorBlockAckAgreementHandler = new OriginatorBlockAckAgreementHandler(); originatorBlockAckProcedure = new OriginatorBlockAckProcedure(); recipientBlockAckProcedure = new RecipientBlockAckProcedure(); + originatorDataService->setFrameEligibilityFunction([this](const Packet *packet) { + if (auto addbaReq = dynamicPtrCast(packet->peekAtFront())) + return originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq); + if (auto delba = dynamicPtrCast(packet->peekAtFront())) + return originatorBlockAckAgreementHandler->isDelbaPending(packet, delba); + auto dataHeader = dynamicPtrCast(packet->peekAtFront()); + // Hold this peer/TID while its ADDBA response is pending so no + // already-sequenced MPDU can precede the advertised SSN. The + // response timeout starts only after the request is transmitted. + return dataHeader == nullptr || dataHeader->getType() != ST_DATA_WITH_QOS || (!originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid())); + }); } } else if (stage == INITSTAGE_LAST) { // Edca resolves its Edcaf array at the link-layer stage. Install the // queue signal listeners after all child initialization has completed. - for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) - check_and_cast(edca->getEdcaf(static_cast(ac))->getPendingQueue())->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto pendingQueue = edca->getEdcaf(AccessCategory(ac))->getPendingQueue(); + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + } + rebuildPendingFrameEligibility(); + } +} + +void Hcf::trackPendingFrame(Packet *packet, AccessCategory accessCategory) +{ + untrackPendingFrame(packet); + bool eligible = originatorDataService->isFrameEligible(packet); + pendingFrameEligibility.emplace(packet, PendingFrameEligibility { accessCategory, eligible }); + if (eligible) + numEligiblePendingFrames[accessCategory]++; +} + +void Hcf::untrackPendingFrame(const Packet *packet) +{ + auto it = pendingFrameEligibility.find(packet); + if (it != pendingFrameEligibility.end()) { + if (it->second.eligible) { + ASSERT(numEligiblePendingFrames[it->second.accessCategory] > 0); + numEligiblePendingFrames[it->second.accessCategory]--; + } + pendingFrameEligibility.erase(it); + } +} + +void Hcf::rebuildPendingFrameEligibility() +{ + pendingFrameEligibility.clear(); + numEligiblePendingFrames.fill(0); + int numPendingFrames = 0; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto accessCategory = AccessCategory(ac); + auto pendingQueue = edca->getEdcaf(accessCategory)->getPendingQueue(); + for (int i = 0; i < pendingQueue->getNumPackets(); i++) { + trackPendingFrame(pendingQueue->getPacket(i), accessCategory); + numPendingFrames++; + } } + ASSERT((int)pendingFrameEligibility.size() == numPendingFrames); +} + +bool Hcf::processDroppedBlockAckSetupFrame(Packet *packet) +{ + if (originatorBlockAckAgreementHandler) { + auto addbaReq = dynamicPtrCast(packet->peekAtFront()); + if (addbaReq != nullptr && originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq)) { + originatorBlockAckAgreementHandler->processDroppedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); + rebuildPendingFrameEligibility(); + return true; + } + } + return false; +} + +bool Hcf::processDroppedBlockAckTeardownFrame(Packet *packet) +{ + if (originatorBlockAckAgreementHandler) { + auto delba = dynamicPtrCast(packet->peekAtFront()); + if (delba != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this)) { + rebuildPendingFrameEligibility(); + return true; + } + } + return false; +} + +void Hcf::receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) +{ + if (signal == queueing::IPacketQueue::packetQueueDepartureSignal) { + Enter_Method("packetQueueDeparture"); + if (source->isSubscribed(signal, this)) + handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); + } + else + ModeSetListener::receiveSignal(source, signal, object, details); +} + +void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) +{ + Enter_Method("handlePacketRemoved"); + untrackPendingFrame(packet); + bool shouldResume = false; + // HCF treats explicit REMOVED notifications as terminal transaction + // disposal; code relocating a packet must use dequeuePacket(). + if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED || reason == queueing::IPacketQueue::PacketRemovalReason::REMOVED) { + if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } + shouldResume |= processDroppedBlockAckSetupFrame(packet); + shouldResume |= processDroppedBlockAckTeardownFrame(packet); + } + if (shouldResume) + resumeEligibleChannelAccess(); } std::string Hcf::getFrameSequenceInfo() const @@ -109,6 +219,15 @@ void Hcf::handleMessage(cMessage *msg) else throw cRuntimeError("Unknown event"); } + else if (msg == addbaResponseTimer) { + if (originatorBlockAckAgreementHandler) { + originatorBlockAckAgreementHandler->addbaResponseTimeoutExpired(originatorBlockAckAgreementPolicy, this); + rebuildPendingFrameEligibility(); + resumeEligibleChannelAccess(); + } + else + throw cRuntimeError("Unknown event"); + } else throw cRuntimeError("Unknown msg type"); } @@ -146,8 +265,9 @@ void Hcf::processUpperFrame(Packet *packet, const PtrgetEdcaf(ac)->getPendingQueue(); + trackPendingFrame(packet, ac); pendingQueue->enqueuePacket(packet); - if (!pendingQueue->isEmpty()) { + if (hasFrameToTransmit(ac)) { auto edcaf = edca->getChannelOwner(); if (edcaf == nullptr || edcaf->getAccessCategory() != ac) { EV_DETAIL << "Requesting channel for access category " << printAccessCategory(ac) << endl; @@ -156,25 +276,22 @@ void Hcf::processUpperFrame(Packet *packet, const PtrisSubscribed(signal, this)) - handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); - } - else - ModeSetListener::receiveSignal(source, signal, object, details); -} - -void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) -{ - Enter_Method("handlePacketRemoved"); - if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + if (frameSequenceHandler == nullptr || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + for (int i = 0; i < context->getNumSteps(); i++) { + auto transmitStep = dynamic_cast(context->getStep(i)); + if (transmitStep != nullptr && transmitStep->getFrameToTransmit() == packet) + return true; + auto rtsTransmitStep = dynamic_cast(transmitStep); + if (rtsTransmitStep != nullptr && rtsTransmitStep->getProtectedFrame() == packet) + return true; } + return false; } void Hcf::scheduleStartRxTimer(simtime_t timeout) @@ -189,6 +306,58 @@ void Hcf::scheduleInactivityTimer(simtime_t timeout) rescheduleAfter(timeout, inactivityTimer); } +void Hcf::scheduleAddbaResponseTimer(simtime_t deadline) +{ + Enter_Method("scheduleAddbaResponseTimer"); + if (deadline == SIMTIME_MAX) { + if (addbaResponseTimer->isScheduled()) + cancelEvent(addbaResponseTimer); + } + else + rescheduleAt(deadline, addbaResponseTimer); +} + +void Hcf::cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) +{ + Enter_Method("cancelAddbaTransaction"); + // Frames borrowed by the active sequence cannot be removed here. The + // sequence's failure paths detect their now-stale transaction and discard them. + auto belongsToTransaction = [this, transactionId, excludedPacket](Packet *packet) { + auto transactionTag = packet->findTag(); + return packet != excludedPacket && !isPacketReferencedByCurrentFrameSequence(packet) && transactionTag != nullptr && transactionTag->getTransactionId() == transactionId; + }; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto edcaf = edca->getEdcaf(AccessCategory(ac)); + auto pendingQueue = edcaf->getPendingQueue(); + for (int i = pendingQueue->getNumPackets() - 1; i >= 0; i--) { + auto packet = pendingQueue->getPacket(i); + if (belongsToTransaction(packet)) { + pendingQueue->removePacket(packet); + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + auto inProgressFrames = edcaf->getInProgressFrames(); + for (int i = inProgressFrames->getLength() - 1; i >= 0; i--) { + auto packet = inProgressFrames->getFrames(i); + if (belongsToTransaction(packet)) { + auto header = packet->peekAtFront(); + auto extractedPacket = inProgressFrames->extractFrame(packet); + ASSERT(extractedPacket == packet); + take(packet); + edcaf->getAckHandler()->dropFrame(header); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + } +} + void Hcf::processLowerFrame(Packet *packet, const Ptr& header) { Enter_Method("processLowerFrame(%s)", packet->getName()); @@ -233,13 +402,22 @@ void Hcf::channelGranted(IChannelAccess *channelAccess) if (edcaf) { AccessCategory ac = edcaf->getAccessCategory(); EV_DETAIL << "Channel access granted to the " << printAccessCategory(ac) << " queue" << std::endl; - edcaf->getTxopProcedure()->startTxop(ac); auto internallyCollidedEdcafs = edca->getInternallyCollidedEdcafs(); if (internallyCollidedEdcafs.size() > 0) { EV_INFO << "Internal collision happened with the following queues:" << std::endl; - handleInternalCollision(internallyCollidedEdcafs); - emit(edcaCollisionDetectedSignal, (unsigned long)internallyCollidedEdcafs.size()); + // IEEE Std 802.11-2024, 10.23.2.4: an EDCAF with no eligible + // frame has no collision recovery action to perform. + auto handledCollisions = handleInternalCollision(internallyCollidedEdcafs); + if (handledCollisions > 0) + emit(edcaCollisionDetectedSignal, (unsigned long)handledCollisions); } + if (!hasFrameToTransmit(ac)) { + EV_DETAIL << "Releasing channel because no eligible frame is available.\n"; + edcaf->releaseChannel(this); + mac->sendDownPendingRadioConfigMsg(); + return; + } + edcaf->getTxopProcedure()->startTxop(ac); startFrameSequence(ac); } else @@ -259,12 +437,18 @@ void Hcf::startFrameSequence(AccessCategory ac) emit(IFrameSequenceHandler::frameSequenceStartedSignal, frameSequenceHandler->getContext()); } -void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) +int Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) { + int handledCollisions = 0; for (auto edcaf : internallyCollidedEdcafs) { AccessCategory ac = edcaf->getAccessCategory(); auto dataRecoveryProcedure = edcaf->getRecoveryProcedure(); Packet *internallyCollidedFrame = edcaf->getInProgressFrames()->getFrameToTransmit(); + if (internallyCollidedFrame == nullptr) { + EV_DETAIL << "Ignoring internal collision because no eligible frame is available for " << printAccessCategory(ac) << ".\n"; + continue; + } + handledCollisions++; auto internallyCollidedHeader = internallyCollidedFrame->peekAtFront(); EV_INFO << printAccessCategory(ac) << " (" << internallyCollidedFrame->getName() << ")" << endl; bool retryLimitReached = false; @@ -286,6 +470,8 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(internallyCollidedFrame, mgmtHeader); else ; // TODO + NonQoSDataFrame edcaf->getInProgressFrames()->dropFrame(internallyCollidedFrame); + processDroppedBlockAckSetupFrame(internallyCollidedFrame); + processDroppedBlockAckTeardownFrame(internallyCollidedFrame); edcaf->getAckHandler()->dropFrame(internallyCollidedHeader); PacketDropDetails details; details.setReason(RETRY_LIMIT_REACHED); @@ -303,6 +489,7 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) else edcaf->requestChannel(this); } + return handledCollisions; } /* @@ -317,12 +504,11 @@ void Hcf::frameSequenceFinished() emit(IFrameSequenceHandler::frameSequenceFinishedSignal, frameSequenceHandler->getContext()); auto edcaf = edca->getChannelOwner(); if (edcaf) { - bool startContention = hasFrameToTransmit(); // TODO outstanding frame edcaf->releaseChannel(this); mac->sendDownPendingRadioConfigMsg(); // TODO review edcaf->getTxopProcedure()->endTxop(); - if (startContention) - edcaf->requestChannel(this); + // Agreement transitions may have made frames in any AC eligible. + requestEligibleChannelAccess(); } else if (hcca->isOwning()) { hcca->releaseChannel(this); @@ -378,9 +564,24 @@ void Hcf::recipientProcessReceivedManagementFrame(const Ptr(header)) { - originatorBlockAckAgreementHandler->processReceivedAddbaResp(addbaResp, originatorBlockAckAgreementPolicy, this); - auto agreement = originatorBlockAckAgreementHandler->getAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid()); - emit(blockAckAgreementAddedSignal, agreement); + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(addbaResp->getTransmitterAddress(), addbaResp->getTid()); + auto response = originatorBlockAckAgreementHandler->processReceivedAddbaResp(addbaResp, originatorBlockAckAgreementPolicy, this); + if (wasPending && !originatorBlockAckAgreementHandler->isAddbaResponsePending(addbaResp->getTransmitterAddress(), addbaResp->getTid())) + rebuildPendingFrameEligibility(); + if (response.teardownDelba != nullptr && (response.terminatedAgreement == nullptr || response.teardownTransactionId == 0)) + throw cRuntimeError("Invalid locally vetoed ADDBA response outcome"); + if (response.establishedAgreement != nullptr) + emit(blockAckAgreementAddedSignal, response.establishedAgreement); + if (response.terminatedAgreement != nullptr) { + emit(blockAckAgreementAddedSignal, response.terminatedAgreement.get()); + emit(blockAckAgreementDeletedSignal, response.terminatedAgreement.get()); + } + if (response.teardownDelba != nullptr) { + auto delbaPacket = new Packet("Delba", response.teardownDelba); + delbaPacket->addTag()->setTransactionId(response.teardownTransactionId); + processMgmtFrame(delbaPacket, response.teardownDelba); + } + resumeEligibleChannelAccess(); } else if (auto delba = dynamicPtrCast(header)) { if (delba->getInitiator()) { @@ -389,9 +590,13 @@ void Hcf::recipientProcessReceivedManagementFrame(const PtrprocessReceivedDelba(delba, recipientBlockAckAgreementPolicy); } else { - auto agreement = originatorBlockAckAgreementHandler->getAgreement(delba->getReceiverAddress(), delba->getTid()); - emit(blockAckAgreementDeletedSignal, agreement); - originatorBlockAckAgreementHandler->processReceivedDelba(delba, originatorBlockAckAgreementPolicy); + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid()); + auto agreement = originatorBlockAckAgreementHandler->processReceivedDelba(delba, originatorBlockAckAgreementPolicy, this); + if (wasPending && !originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid())) + rebuildPendingFrameEligibility(); + if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + resumeEligibleChannelAccess(); } } else @@ -432,24 +637,35 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) } else throw cRuntimeError("Unknown frame"); // TODO QoSDataFrame, NonQoSDataFrame - if (retryLimitReached) { - if (auto dataHeader = dynamicPtrCast(protectedHeader)) - edcaf->getRecoveryProcedure()->retryLimitReached(packet, dataHeader); - else if (auto mgmtHeader = dynamicPtrCast(protectedHeader)) - edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(packet, mgmtHeader); - else ; // TODO nonqos data + auto addbaRequest = dynamicPtrCast(protectedHeader); + bool staleAddbaRequest = addbaRequest != nullptr && originatorBlockAckAgreementHandler != nullptr && !originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaRequest); + if (retryLimitReached || staleAddbaRequest) { + if (retryLimitReached) { + if (auto dataHeader = dynamicPtrCast(protectedHeader)) + edcaf->getRecoveryProcedure()->retryLimitReached(packet, dataHeader); + else if (auto mgmtHeader = dynamicPtrCast(protectedHeader)) + edca->getMgmtAndNonQoSRecoveryProcedure()->rtsFrameRetryLimitReached(packet, mgmtHeader); + else ; // TODO nonqos data + } + else + edca->getMgmtAndNonQoSRecoveryProcedure()->discardRtsFrame(addbaRequest); edcaf->getInProgressFrames()->dropFrame(packet); + processDroppedBlockAckSetupFrame(packet); + processDroppedBlockAckTeardownFrame(packet); edcaf->getAckHandler()->dropFrame(protectedHeader); - EV_INFO << "Dropping RTS/CTS protected frame " << packet->getName() << ", because retry limit is reached.\n"; + EV_INFO << "Dropping RTS/CTS protected frame " << packet->getName() << (retryLimitReached ? ", because retry limit is reached.\n" : ", because its ADDBA transaction is no longer pending.\n"); PacketDropDetails details; - details.setReason(RETRY_LIMIT_REACHED); - details.setLimit(-1); // TODO + details.setReason(retryLimitReached ? RETRY_LIMIT_REACHED : OTHER_PACKET_DROP); + if (retryLimitReached) + details.setLimit(-1); // TODO emit(packetDroppedSignal, packet, &details); - emit(linkBrokenSignal, packet); - if (dynamicPtrCast(protectedHeader)) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + if (retryLimitReached) { + emit(linkBrokenSignal, packet); + if (dynamicPtrCast(protectedHeader)) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } } } } @@ -474,7 +690,7 @@ void Hcf::originatorProcessTransmittedFrame(Packet *packet) else if (auto dataHeader = dynamicPtrCast(transmittedHeader)) originatorProcessTransmittedDataFrame(packet, dataHeader, ac); else if (auto mgmtHeader = dynamicPtrCast(transmittedHeader)) - originatorProcessTransmittedManagementFrame(mgmtHeader, ac); + originatorProcessTransmittedManagementFrame(packet, mgmtHeader, ac); else // TODO Ieee80211ControlFrame originatorProcessTransmittedControlFrame(transmittedHeader, ac); } @@ -488,26 +704,30 @@ void Hcf::originatorProcessTransmittedDataFrame(Packet *packet, const PtrgetEdcaf(ac); edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(dataHeader); - if (originatorBlockAckAgreementHandler) - originatorBlockAckAgreementHandler->processTransmittedDataFrame(packet, dataHeader, originatorBlockAckAgreementPolicy, this); if (dataHeader->getAckPolicy() == NO_ACK) edcaf->getInProgressFrames()->dropFrame(packet); } -void Hcf::originatorProcessTransmittedManagementFrame(const Ptr& mgmtHeader, AccessCategory ac) +void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr& mgmtHeader, AccessCategory ac) { auto edcaf = edca->getEdcaf(ac); if (originatorAckPolicy->isAckNeeded(mgmtHeader)) edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(mgmtHeader); if (auto addbaReq = dynamicPtrCast(mgmtHeader)) { if (originatorBlockAckAgreementHandler) - originatorBlockAckAgreementHandler->processTransmittedAddbaReq(addbaReq); + originatorBlockAckAgreementHandler->processTransmittedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); } else if (auto addbaResp = dynamicPtrCast(mgmtHeader)) recipientBlockAckAgreementHandler->processTransmittedAddbaResp(addbaResp, this); else if (auto delba = dynamicPtrCast(mgmtHeader)) { - if (delba->getInitiator()) - originatorBlockAckAgreementHandler->processTransmittedDelba(delba); + if (delba->getInitiator()) { + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getReceiverAddress(), delba->getTid()); + auto agreement = originatorBlockAckAgreementHandler->processTransmittedDelba(packet, this); + if (wasPending) + rebuildPendingFrameEligibility(); + if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + } else recipientBlockAckAgreementHandler->processTransmittedDelba(delba); } @@ -559,23 +779,34 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) } else throw cRuntimeError("Unknown frame"); // TODO qos, nonqos - if (retryLimitReached) { - if (auto dataHeader = dynamicPtrCast(failedHeader)) - edcaf->getRecoveryProcedure()->retryLimitReached(failedPacket, dataHeader); - else if (auto mgmtHeader = dynamicPtrCast(failedHeader)) - edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(failedPacket, mgmtHeader); + auto addbaRequest = dynamicPtrCast(failedHeader); + bool staleAddbaRequest = addbaRequest != nullptr && originatorBlockAckAgreementHandler != nullptr && !originatorBlockAckAgreementHandler->isAddbaRequestPending(failedPacket, addbaRequest); + if (retryLimitReached || staleAddbaRequest) { + if (retryLimitReached) { + if (auto dataHeader = dynamicPtrCast(failedHeader)) + edcaf->getRecoveryProcedure()->retryLimitReached(failedPacket, dataHeader); + else if (auto mgmtHeader = dynamicPtrCast(failedHeader)) + edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(failedPacket, mgmtHeader); + } + else + edca->getMgmtAndNonQoSRecoveryProcedure()->discardFrame(failedPacket, addbaRequest); edcaf->getInProgressFrames()->dropFrame(failedPacket); + processDroppedBlockAckSetupFrame(failedPacket); + processDroppedBlockAckTeardownFrame(failedPacket); edcaf->getAckHandler()->dropFrame(dynamicPtrCast(failedHeader)); - EV_INFO << "Dropping frame " << failedPacket->getName() << ", because retry limit is reached.\n"; + EV_INFO << "Dropping frame " << failedPacket->getName() << (retryLimitReached ? ", because retry limit is reached.\n" : ", because its ADDBA transaction is no longer pending.\n"); PacketDropDetails details; - details.setReason(RETRY_LIMIT_REACHED); - details.setLimit(-1); // TODO + details.setReason(retryLimitReached ? RETRY_LIMIT_REACHED : OTHER_PACKET_DROP); + if (retryLimitReached) + details.setLimit(-1); // TODO emit(packetDroppedSignal, failedPacket, &details); - emit(linkBrokenSignal, failedPacket); - if (dynamicPtrCast(failedHeader)) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); + if (retryLimitReached) { + emit(linkBrokenSignal, failedPacket); + if (dynamicPtrCast(failedHeader)) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); + } } } else { @@ -641,6 +872,10 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); + if (auto delba = dynamicPtrCast(lastTransmittedHeader)) { + if (delba->getInitiator() && originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this)) + rebuildPendingFrameEligibility(); + } edcaf->getInProgressFrames()->dropFrame(lastTransmittedPacket); edcaf->getAckHandler()->dropFrame(lastTransmittedDataOrMgmtHeader); if (dynamicPtrCast(lastTransmittedHeader)) { @@ -648,6 +883,16 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader)) { + if (originatorBlockAckAgreementHandler) { + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); + auto obsoleteTeardownTransactionId = originatorBlockAckAgreementHandler->processAcknowledgedDataFrame(lastTransmittedPacket, dataHeader, originatorBlockAckAgreementPolicy, this); + if (obsoleteTeardownTransactionId != 0) + cancelAddbaTransaction(obsoleteTeardownTransactionId, nullptr); + if (!wasPending && originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid())) + rebuildPendingFrameEligibility(); + } + } } else if (auto blockAck = dynamicPtrCast(header)) { EV_INFO << "BasicBlockAck has arrived" << std::endl; @@ -682,7 +927,7 @@ bool Hcf::hasFrameToTransmit(AccessCategory ac) { auto edcaf = edca->getEdcaf(ac); if (edcaf) - return !edcaf->getPendingQueue()->isEmpty() || edcaf->getInProgressFrames()->hasInProgressFrames(); + return numEligiblePendingFrames[ac] != 0 || edcaf->getInProgressFrames()->hasEligibleInProgressFrames(); else throw cRuntimeError("Hcca is unimplemented"); } @@ -691,11 +936,26 @@ bool Hcf::hasFrameToTransmit() { auto edcaf = edca->getChannelOwner(); if (edcaf) - return !edcaf->getPendingQueue()->isEmpty() || edcaf->getInProgressFrames()->hasInProgressFrames(); + return numEligiblePendingFrames[edcaf->getAccessCategory()] != 0 || edcaf->getInProgressFrames()->hasEligibleInProgressFrames(); else throw cRuntimeError("Hcca is unimplemented"); } +void Hcf::requestEligibleChannelAccess() +{ + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto accessCategory = AccessCategory(ac); + if (hasFrameToTransmit(accessCategory)) + edca->requestChannelAccess(accessCategory, this); + } +} + +void Hcf::resumeEligibleChannelAccess() +{ + if (edca->getChannelOwner() == nullptr && !frameSequenceHandler->isSequenceRunning()) + requestEligibleChannelAccess(); +} + void Hcf::sendUp(const std::vector& completeFrames) { for (auto frame : completeFrames) @@ -822,8 +1082,11 @@ void Hcf::corruptedFrameReceived() Hcf::~Hcf() { + // Callback pointers are stored by child queues, which are destroyed with + // this compound module. Traversing edca here may reach deleted children. cancelAndDelete(startRxTimer); cancelAndDelete(inactivityTimer); + cancelAndDelete(addbaResponseTimer); delete recipientAckProcedure; delete ctsProcedure; delete rtsProcedure; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index c05a7a071af..7bf6631e302 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -8,6 +8,9 @@ #ifndef __INET_HCF_H #define __INET_HCF_H +#include +#include + #include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h" #include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" @@ -59,6 +62,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: cMessage *startRxTimer = nullptr; cMessage *inactivityTimer = nullptr; + cMessage *addbaResponseTimer = nullptr; // Transmission and Reception IRx *rx = nullptr; @@ -97,6 +101,13 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: // Queues InProgressFrames *hccaInProgressFrame = nullptr; + struct PendingFrameEligibility { + AccessCategory accessCategory; + bool eligible; + }; + std::map pendingFrameEligibility; + std::array numEligiblePendingFrames = {}; + // Frame sequence handler IFrameSequenceHandler *frameSequenceHandler = nullptr; @@ -111,12 +122,20 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void refreshDisplay() const override; void startFrameSequence(AccessCategory ac); - void handleInternalCollision(std::vector internallyCollidedEdcafs); + int handleInternalCollision(std::vector internallyCollidedEdcafs); void sendUp(const std::vector& completeFrames); FrameSequenceContext *buildContext(AccessCategory ac); virtual bool hasFrameToTransmit(); virtual bool hasFrameToTransmit(AccessCategory ac); + virtual void requestEligibleChannelAccess(); + virtual void resumeEligibleChannelAccess(); + virtual bool processDroppedBlockAckSetupFrame(Packet *packet); + virtual bool processDroppedBlockAckTeardownFrame(Packet *packet); + virtual bool isPacketReferencedByCurrentFrameSequence(const Packet *packet) const; + virtual void trackPendingFrame(Packet *packet, AccessCategory accessCategory); + virtual void untrackPendingFrame(const Packet *packet); + virtual void rebuildPendingFrameEligibility(); virtual bool isReceptionInProgress(); // Recipient @@ -126,7 +145,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header); // Originator - virtual void originatorProcessTransmittedManagementFrame(const Ptr& mgmtHeader, AccessCategory ac); + virtual void originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr& mgmtHeader, AccessCategory ac); virtual void originatorProcessTransmittedControlFrame(const Ptr& controlHeader, AccessCategory ac); virtual void originatorProcessTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, AccessCategory ac); virtual void originatorProcessReceivedManagementFrame(const Ptr& header, const Ptr& lastTransmittedHeader, AccessCategory ac); @@ -161,6 +180,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); // IProcedureCallback virtual void scheduleInactivityTimer(simtime_t timeout) override; + virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override; std::string getFrameSequenceInfo() const; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc index fc81d4d863c..2ae6c036527 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc @@ -13,6 +13,14 @@ namespace inet { namespace ieee80211 { +static bool hasMoreTxOpsForFrame(RepeatingFs *frameSequence, FrameSequenceContext *context, Packet *nextFrameToTransmit) +{ + if (nextFrameToTransmit == nullptr) + return false; + const auto& nextHeader = nextFrameToTransmit->peekAtFront(); + return frameSequence->getCount() == 0 || (!nextHeader->getReceiverAddress().isMulticast() && context->getQoSContext()->txopProcedure->getRemaining() > 0); +} + HcfFs::HcfFs() : // G.3 EDCA and HCCA sequences // hcf-sequence = @@ -58,20 +66,17 @@ bool HcfFs::isSelfCtsNeeded(OptionalFs *frameSequence, FrameSequenceContext *con bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *context) { - bool hasFrameToTransmit = context->getInProgressFrames()->hasInProgressFrames(); - if (hasFrameToTransmit) { - auto nextFrameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); - const auto& nextHeader = nextFrameToTransmit->peekAtFront(); - return frameSequence->getCount() == 0 || (!nextHeader->getReceiverAddress().isMulticast() && context->getQoSContext()->txopProcedure->getRemaining() > 0); - } - return false; + // This predicate is the TXOP continuation boundary, so it may materialize + // an eligible pending frame. Availability queries elsewhere remain pure. + auto nextFrameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); + return hasMoreTxOpsForFrame(frameSequence, context, nextFrameToTransmit); } bool HcfFs::hasMoreTxOpsAndMulticast(RepeatingFs *frameSequence, FrameSequenceContext *context) { - return hasMoreTxOps(frameSequence, context) && context->getInProgressFrames()->getFrameToTransmit()->peekAtFront()->getReceiverAddress().isMulticast(); + auto nextFrameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); + return hasMoreTxOpsForFrame(frameSequence, context, nextFrameToTransmit) && nextFrameToTransmit->peekAtFront()->getReceiverAddress().isMulticast(); } } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc index 4074ff0004c..f5d01a0b70a 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc @@ -127,23 +127,34 @@ void NonQosRecoveryProcedure::ackFrameReceived(Packet *packet, const Ptr& header) +{ + auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()); + shortRetryCounter.erase(id); + longRetryCounter.erase(id); +} + +void NonQosRecoveryProcedure::discardRtsFrame(const Ptr& protectedHeader) +{ + auto id = SequenceControlField(protectedHeader->getSequenceNumber().get(), protectedHeader->getFragmentNumber()); + shortRetryCounter.erase(id); + longRetryCounter.erase(id); +} + // After dropping a frame because it reached its retry limit we need to clear the // retry counters // void NonQosRecoveryProcedure::retryLimitReached(Packet *packet, const Ptr& header) { EV_WARN << "Retry limit reached for " << *packet << ".\n"; - auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()); - if (packet->getByteLength() >= rtsThreshold) { - auto it = longRetryCounter.find(id); - if (it != longRetryCounter.end()) - longRetryCounter.erase(it); - } - else { - auto it = shortRetryCounter.find(id); - if (it != shortRetryCounter.end()) - shortRetryCounter.erase(it); - } + discardFrame(packet, header); + emit(retryLimitReachedSignal, packet); +} + +void NonQosRecoveryProcedure::rtsFrameRetryLimitReached(Packet *packet, const Ptr& protectedHeader) +{ + EV_WARN << "RTS retry limit reached for " << *packet << ".\n"; + discardRtsFrame(protectedHeader); emit(retryLimitReachedSignal, packet); } @@ -247,4 +258,3 @@ bool NonQosRecoveryProcedure::isMulticastFrame(const Ptr& failedHeader); virtual bool isRtsFrameRetryLimitReached(Packet *packet, const Ptr& protectedHeader); + virtual void discardFrame(Packet *packet, const Ptr& header); + virtual void discardRtsFrame(const Ptr& protectedHeader); virtual void retryLimitReached(Packet *packet, const Ptr& header); + virtual void rtsFrameRetryLimitReached(Packet *packet, const Ptr& protectedHeader); virtual int getLongRetryLimit() { return longRetryLimit; } virtual int getShortRetryLimit() { return shortRetryLimit; } @@ -74,4 +77,3 @@ class INET_API NonQosRecoveryProcedure : public SimpleModule, public IRecoveryPr } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.cc index 12d6c5cd21e..4bbfc0ea6d3 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.cc @@ -7,6 +7,7 @@ #include "inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/NonQoSSequenceNumberAssignment.h" @@ -32,21 +33,37 @@ std::vector *OriginatorMacDataService::fragmentIfNeeded(Packet *frame) auto fragmentSizes = fragmentationPolicy->computeFragmentSizes(frame); if (fragmentSizes.size() != 0) { emit(packetFragmentedSignal, frame); + auto transactionTag = frame->findTag(); + bool hasTransactionTag = transactionTag != nullptr; + auto transactionId = hasTransactionTag ? transactionTag->getTransactionId() : 0; auto fragmentFrames = fragmentation->fragmentFrame(frame, fragmentSizes); + if (hasTransactionTag) + for (auto fragment : *fragmentFrames) + fragment->addTagIfAbsent()->setTransactionId(transactionId); return fragmentFrames; } return nullptr; } +bool OriginatorMacDataService::isFrameEligible(const Packet *packet) const +{ + return !frameEligibilityFunction || frameEligibilityFunction(packet); +} + +bool OriginatorMacDataService::hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const +{ + return pendingQueue->findPacket([this](const Packet *packet) { return isFrameEligible(packet); }) != nullptr; +} + std::vector *OriginatorMacDataService::extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) { Enter_Method("extractFramesToTransmit"); - if (pendingQueue->isEmpty()) + auto packet = pendingQueue->dequeuePacket([this](const Packet *packet) { return isFrameEligible(packet); }); + if (packet == nullptr) return nullptr; else { // if (msduRateLimiting) // txRateLimitingIfNeeded(); - Packet *packet = pendingQueue->dequeuePacket(); take(packet); if (sequenceNumberAssignment) { auto frame = packet->removeAtFront(); @@ -76,4 +93,3 @@ OriginatorMacDataService::~OriginatorMacDataService() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h index 2a75735b598..7bf62fa66ad 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h @@ -30,6 +30,7 @@ class INET_API OriginatorMacDataService : public IOriginatorMacDataService, publ // MsduIntegrityAndProtection *msduIntegrityAndProtection = nullptr; IFragmentationPolicy *fragmentationPolicy = nullptr; IFragmentation *fragmentation = nullptr; + FrameEligibilityFunction frameEligibilityFunction; // MpduEncryptionAndIntegrity *mpduEncryptionAndIntegrity = nullptr; // MpduHeaderPlusFcs *mpduHeaderPlusFcs = nullptr; @@ -42,6 +43,9 @@ class INET_API OriginatorMacDataService : public IOriginatorMacDataService, publ public: virtual ~OriginatorMacDataService(); + virtual void setFrameEligibilityFunction(const FrameEligibilityFunction& frameEligibilityFunction) override { this->frameEligibilityFunction = frameEligibilityFunction; } + virtual bool isFrameEligible(const Packet *packet) const override; + virtual bool hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const override; virtual std::vector *extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) override; }; @@ -49,4 +53,3 @@ class INET_API OriginatorMacDataService : public IOriginatorMacDataService, publ } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc index f374452e933..0ccd4415e61 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc @@ -8,9 +8,12 @@ #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include +#include +#include #include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h" #include "inet/linklayer/ieee80211/mac/aggregation/MsduAggregation.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" @@ -32,17 +35,53 @@ void OriginatorQosMacDataService::initialize() fragmentation = new Fragmentation(); } -Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue) +Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *candidate) { - auto subframes = aMsduAggregationPolicy->computeAggregateFrames(pendingQueue); + auto predicate = [this](const Packet *packet) { return isFrameEligible(packet); }; + std::unique_ptr> subframes(aMsduAggregationPolicy->computeAggregateFrames(pendingQueue, candidate, predicate)); if (subframes) { + if (subframes->size() < 2 || subframes->front() != candidate) + throw cRuntimeError("A-MSDU policy must return at least two frames with the selected candidate first"); + std::unordered_set uniqueFrames; for (auto subframe : *subframes) { - pendingQueue->removePacket(subframe); - take(subframe); + auto identity = [subframe](const Packet *packet) { return packet == subframe; }; + if (subframe == nullptr || !uniqueFrames.insert(subframe).second || pendingQueue->findPacket(identity) != subframe || !isFrameEligible(subframe)) + throw cRuntimeError("A-MSDU policy returned a frame that is unavailable, ineligible, or duplicated"); } - auto aggregatedFrame = aMsduAggregation->aggregateFrames(subframes); + struct AggregateFrameState { Tid tid; MacAddress receiver; MacAddress transmitter; MacAddress address3; MacAddress address4; int type; bool toDS; bool fromDS; b dataLength; b headerLength; b trailerLength; }; + std::vector states; + for (auto subframe : *subframes) { + auto dataHeader = dynamicPtrCast(subframe->peekAtFront()); + auto dataTrailer = subframe->peekAtBack(B(4)); + if (dataHeader == nullptr || dataTrailer == nullptr) + throw cRuntimeError("A-MSDU policy selected a frame without a valid data header/trailer"); + states.push_back({static_cast(dataHeader->getTid()), dataHeader->getReceiverAddress(), dataHeader->getTransmitterAddress(), dataHeader->getAddress3(), dataHeader->getAddress4(), dataHeader->getType(), dataHeader->getToDS(), dataHeader->getFromDS(), subframe->getDataLength(), dataHeader->getChunkLength(), dataTrailer->getChunkLength()}); + } + std::vector> extractedSubframes; + extractedSubframes.reserve(subframes->size()); + for (auto subframe : *subframes) { + auto dequeuedSubframe = pendingQueue->dequeuePacket([subframe](const Packet *packet) { return packet == subframe; }); + if (dequeuedSubframe != subframe) { + bool alreadyExtracted = std::any_of(extractedSubframes.begin(), extractedSubframes.end(), [dequeuedSubframe](const auto& extractedSubframe) { return extractedSubframe.get() == dequeuedSubframe; }); + if (dequeuedSubframe != nullptr && !alreadyExtracted) { + take(dequeuedSubframe); + extractedSubframes.emplace_back(dequeuedSubframe); + } + throw cRuntimeError("A-MSDU policy-selected subframe is no longer available in scheduling order"); + } + take(dequeuedSubframe); + extractedSubframes.emplace_back(dequeuedSubframe); + } + for (size_t i = 0; i < subframes->size(); i++) { + auto dataHeader = dynamicPtrCast((*subframes)[i]->peekAtFront()); + auto dataTrailer = (*subframes)[i]->peekAtBack(B(4)); + if (dataHeader == nullptr || dataTrailer == nullptr || dataHeader->getTid() != states[i].tid || dataHeader->getReceiverAddress() != states[i].receiver || dataHeader->getTransmitterAddress() != states[i].transmitter || dataHeader->getAddress3() != states[i].address3 || dataHeader->getAddress4() != states[i].address4 || dataHeader->getType() != states[i].type || dataHeader->getToDS() != states[i].toDS || dataHeader->getFromDS() != states[i].fromDS || (*subframes)[i]->getDataLength() != states[i].dataLength || dataHeader->getChunkLength() != states[i].headerLength || dataTrailer->getChunkLength() != states[i].trailerLength) + throw cRuntimeError("A-MSDU provider changed aggregation-critical frame fields during extraction"); + } + for (auto& subframe : extractedSubframes) + subframe.release(); + auto aggregatedFrame = aMsduAggregation->aggregateFrames(subframes.get()); emit(packetAggregatedSignal, aggregatedFrame); - delete subframes; return aggregatedFrame; } return nullptr; @@ -72,27 +111,49 @@ std::vector *OriginatorQosMacDataService::fragmentIfNeeded(Packet *fra auto fragmentSizes = fragmentationPolicy->computeFragmentSizes(frame); if (fragmentSizes.size() != 0) { emit(packetFragmentedSignal, frame); + auto transactionTag = frame->findTag(); + bool hasTransactionTag = transactionTag != nullptr; + auto transactionId = hasTransactionTag ? transactionTag->getTransactionId() : 0; auto fragmentFrames = fragmentation->fragmentFrame(frame, fragmentSizes); + if (hasTransactionTag) + for (auto fragment : *fragmentFrames) + fragment->addTagIfAbsent()->setTransactionId(transactionId); return fragmentFrames; } return nullptr; } +bool OriginatorQosMacDataService::isFrameEligible(const Packet *packet) const +{ + return !frameEligibilityFunction || frameEligibilityFunction(packet); +} + +bool OriginatorQosMacDataService::hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const +{ + return pendingQueue->findPacket([this](const Packet *packet) { return isFrameEligible(packet); }) != nullptr; +} + std::vector *OriginatorQosMacDataService::extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) { Enter_Method("extractFramesToTransmit"); - if (pendingQueue->isEmpty()) + auto predicate = [this](const Packet *packet) { return isFrameEligible(packet); }; + auto candidate = pendingQueue->findPacket(predicate); + if (candidate == nullptr) return nullptr; else { // if (msduRateLimiting) // txRateLimitingIfNeeded(); Packet *packet = nullptr; + // Scheduling selects the anchor; the policy may select additional + // eligible members, which are all extracted through the provider. if (aMsduAggregationPolicy) - packet = aMsduAggregateIfNeeded(pendingQueue); + packet = aMsduAggregateIfNeeded(pendingQueue, candidate); if (!packet) { - packet = pendingQueue->dequeuePacket(); + packet = pendingQueue->dequeuePacket(predicate); + ASSERT(packet == candidate); take(packet); } + ASSERT(packet != nullptr); // PS Defer Queueing if (sequenceNumberAssignment) { auto header = packet->removeAtFront(); @@ -129,4 +190,3 @@ OriginatorQosMacDataService::~OriginatorQosMacDataService() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h index 0d162c213be..8ba2b581f0b 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h @@ -38,18 +38,22 @@ class INET_API OriginatorQosMacDataService : public IOriginatorMacDataService, p // PsDeferQueueing *psDeferQueueing = nullptr; IMpduAggregationPolicy *aMpduAggregationPolicy = nullptr; IMpduAggregation *aMpduAggregation = nullptr; + FrameEligibilityFunction frameEligibilityFunction; protected: virtual void initialize() override; virtual void assignSequenceNumber(const Ptr& header); virtual std::vector *fragmentIfNeeded(Packet *frame); - virtual Packet *aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue); + virtual Packet *aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *candidate); virtual Packet *aMpduAggregateIfNeeded(std::vector *fragments); public: virtual ~OriginatorQosMacDataService(); + virtual void setFrameEligibilityFunction(const FrameEligibilityFunction& frameEligibilityFunction) override { this->frameEligibilityFunction = frameEligibilityFunction; } + virtual bool isFrameEligible(const Packet *packet) const override; + virtual bool hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const override; virtual std::vector *extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) override; }; @@ -57,4 +61,3 @@ class INET_API OriginatorQosMacDataService : public IOriginatorMacDataService, p } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc index c1bb688f218..0d842aebdea 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc @@ -49,7 +49,7 @@ void InProgressFrames::forEachChild(cVisitor *v) bool InProgressFrames::hasEligibleFrameToTransmit() { for (auto frame : inProgressFrames) { - if (ackHandler->isEligibleToTransmit(frame->peekAtFront())) + if (dataService->isFrameEligible(frame) && ackHandler->isEligibleToTransmit(frame->peekAtFront())) return true; } return false; @@ -82,7 +82,7 @@ Packet *InProgressFrames::getFrameToTransmit() { ensureHasFrameToTransmit(); for (auto frame : inProgressFrames) { - if (ackHandler->isEligibleToTransmit(frame->peekAtFront())) + if (dataService->isFrameEligible(frame) && ackHandler->isEligibleToTransmit(frame->peekAtFront())) return frame; } return nullptr; @@ -95,7 +95,7 @@ Packet *InProgressFrames::getPendingFrameFor(Packet *frame) return frameToTransmit; else { for (auto frame : inProgressFrames) { - if (ackHandler->isEligibleToTransmit(frame->peekAtFront()) && frameToTransmit != frame) + if (dataService->isFrameEligible(frame) && ackHandler->isEligibleToTransmit(frame->peekAtFront()) && frameToTransmit != frame) return frame; } auto frames = dataService->extractFramesToTransmit(pendingQueue); @@ -120,12 +120,28 @@ Packet *InProgressFrames::getPendingFrameFor(Packet *frame) void InProgressFrames::dropFrame(Packet *packet) { + auto it = std::find(inProgressFrames.begin(), inProgressFrames.end(), packet); + if (it == inProgressFrames.end()) + return; EV_DEBUG << "Dropping frame " << packet->getName() << ".\n"; - inProgressFrames.erase(std::remove(inProgressFrames.begin(), inProgressFrames.end(), packet), inProgressFrames.end()); + inProgressFrames.erase(it); droppedFrames.push_back(packet); emit(packetDequeuedSignal, packet); } +Packet *InProgressFrames::extractFrame(Packet *packet) +{ + Enter_Method("extractFrame"); + auto it = std::find(inProgressFrames.begin(), inProgressFrames.end(), packet); + if (it == inProgressFrames.end()) + return nullptr; + EV_DEBUG << "Extracting frame " << packet->getName() << ".\n"; + inProgressFrames.erase(it); + emit(packetDequeuedSignal, packet); + drop(packet); + return packet; +} + void InProgressFrames::dropFrames(std::set>> seqAndFragNums) { for (auto it = inProgressFrames.begin(); it != inProgressFrames.end();) { @@ -175,4 +191,3 @@ InProgressFrames::~InProgressFrames() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h index 91c4a10cca8..98063fd07ce 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h @@ -47,8 +47,10 @@ class INET_API InProgressFrames : public SimpleModule virtual Packet *getFrameToTransmit(); virtual Packet *getPendingFrameFor(Packet *frame); virtual void dropFrame(Packet *packet); + virtual Packet *extractFrame(Packet *packet); virtual void dropFrames(std::set>> seqAndFragNums); + virtual bool hasEligibleInProgressFrames() { return hasEligibleFrameToTransmit(); } virtual bool hasInProgressFrames() { ensureHasFrameToTransmit(); return hasEligibleFrameToTransmit(); } virtual std::vector getOutstandingFrames(); @@ -59,4 +61,3 @@ class INET_API InProgressFrames : public SimpleModule } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg index 47632a7a5cd..cb343b94059 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg @@ -61,6 +61,7 @@ enum Ieee80211ReasonCode RC_NONASS_CLASS3 = 7; RC_DIASS_MS_LEAVING = 8; RC_NONAUTH_ASS_REQUEST = 9; + RC_END_BA = 37; RC_MESH_PEERING_CANCELLED = 52; RC_MESH_MAX_PEERS = 53; RC_MESH_CONFIGURATION_POLICY_VIOLATION = 54; diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 91a9667ac13..725e33108fc 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -11,6 +11,23 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/common/TimeTag_m.h" #include "inet/common/LabelsTag_m.h" #include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MsduAggregation.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" +#include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" #include "inet/queueing/gate/PacketGate.h" #include "inet/queueing/gate/PeriodicGate.h" @@ -23,6 +40,100 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC %global: using namespace inet; using namespace inet::ieee80211; + +class TestOriginatorPolicy : public IOriginatorBlockAckAgreementPolicy +{ + public: + simtime_t addbaResponseTimeout = 1; + simtime_t addbaRetryBackoff = 2; + bool addbaAccepted = true; + bool delbaAccepted = true; + + virtual bool isAddbaReqNeeded(Packet *, const Ptr&) override { return true; } + virtual bool isAddbaReqAccepted(const Ptr&, OriginatorBlockAckAgreement *) override { return addbaAccepted; } + virtual bool isDelbaAccepted(const Ptr&) override { return delbaAccepted; } + virtual bool isMsduSupported() const override { return true; } + virtual simtime_t getAddbaResponseTimeout() const override { return addbaResponseTimeout; } + virtual simtime_t computeAddbaRetryBackoff() const override { return addbaRetryBackoff; } + virtual simtime_t getBlockAckTimeoutValue() const override { return 0; } + virtual bool isDelayedAckPolicySupported() const override { return false; } + virtual int getMaximumAllowedBufferSize() const override { return 64; } +}; + +class TestRecipientPolicy : public IRecipientBlockAckAgreementPolicy +{ + public: + bool accepted = true; + bool delbaAccepted = true; + simtime_t blockAckTimeoutValue = 0; + bool aMsduSupportedValue = true; + bool delayedBlockAckPolicySupportedValue = false; + int maximumAllowedBufferSize = 64; + + virtual bool isAddbaReqAccepted(const Ptr&) override { return accepted; } + virtual bool isDelbaAccepted(const Ptr&) override { return delbaAccepted; } + virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } + virtual bool aMsduSupported() const override { return aMsduSupportedValue; } + virtual bool delayedBlockAckPolicySupported() const override { return delayedBlockAckPolicySupportedValue; } + virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } +}; + +class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandlerCallback +{ + public: + Packet *managementPacket = nullptr; + std::vector managementPackets; + Ptr managementHeader; + simtime_t addbaDeadline = SIMTIME_MAX; + std::vector cancelledTransactionIds; + std::vector excludedPackets; + + virtual ~TestCallback() { for (auto packet : managementPackets) delete packet; } + void forgetManagementPacket(Packet *packet) { managementPackets.erase(std::remove(managementPackets.begin(), managementPackets.end(), packet), managementPackets.end()); } + virtual void transmitControlResponseFrame(Packet *, const Ptr&, Packet *, const Ptr&) override { throw cRuntimeError("Unexpected control response"); } + virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { + managementPacket = packet; + managementPackets.push_back(packet); + managementHeader = header; + } + virtual void scheduleInactivityTimer(simtime_t) override {} + virtual void scheduleAddbaResponseTimer(simtime_t deadline) override { addbaDeadline = deadline; } + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { + cancelledTransactionIds.push_back(transactionId); + excludedPackets.push_back(excludedPacket); + } +}; + +class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler +{ + public: + using OriginatorBlockAckAgreementHandler::buildAddbaRequest; + void addEstablishedAgreement(MacAddress receiverAddress, Tid tid) { + auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(0), 64, true, false, 1, 1); + agreement->setIsAddbaResponseReceived(true); + blockAckAgreements[std::make_pair(receiverAddress, tid)] = agreement; + } + void setNextDialogToken(uint8_t token) { nextDialogToken = token; } + void expireNow(MacAddress receiverAddress, Tid tid) { getAgreement(receiverAddress, tid)->setAddbaResponseDeadline(simTime()); } + void allowRetryNow(MacAddress receiverAddress, Tid tid) { addbaRetryDeadlines[std::make_pair(receiverAddress, tid)] = simTime(); } + void setPendingTeardownTransaction(MacAddress receiverAddress, Tid tid, uint64_t transactionId) { pendingTeardownTransactionIds[std::make_pair(receiverAddress, tid)] = transactionId; } + simtime_t getRetryDeadline(MacAddress receiverAddress, Tid tid) const { auto it = addbaRetryDeadlines.find(std::make_pair(receiverAddress, tid)); return it == addbaRetryDeadlines.end() ? SIMTIME_MAX : it->second; } +}; + +class TestTimeoutCancellationCallback : public TestCallback +{ + public: + TestOriginatorHandler *handler = nullptr; + std::vector> expiredAgreementIds; + bool allExpiredAgreementsGoneBeforeCallbacks = true; + + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { + for (const auto& agreementId : expiredAgreementIds) + allExpiredAgreementsGoneBeforeCallbacks &= handler->getAgreement(agreementId.first, agreementId.second) == nullptr; + TestCallback::cancelAddbaTransaction(transactionId, excludedPacket); + } +}; + class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue { public: @@ -92,6 +203,37 @@ class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue virtual void pushPacketEnd(Packet *, const cGate *) override { throw cRuntimeError("Unsupported"); } virtual void pushPacketProgress(Packet *, const cGate *, bps, b, b) override { throw cRuntimeError("Unsupported"); } }; + +class TestWrongDequeueQueue : public TestPacketQueue +{ + public: + Packet *wrongPacket = nullptr; + int numPredicateDequeues = 0; + + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override { + if (numPredicateDequeues++ == 0) + return TestPacketQueue::dequeuePacket(predicate); + packets.erase(std::find(packets.begin(), packets.end(), wrongPacket)); + notifyPacketRemoved(wrongPacket, PacketRemovalReason::DEQUEUED); + return wrongPacket; + } +}; + +class TestRepeatedDequeueQueue : public TestPacketQueue +{ + public: + Packet *repeatedPacket = nullptr; + int numPredicateDequeues = 0; + + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override { + if (numPredicateDequeues++ == 0) { + repeatedPacket = TestPacketQueue::dequeuePacket(predicate); + return repeatedPacket; + } + return repeatedPacket; + } +}; + class TestCompoundPacketQueue : public queueing::CompoundPacketQueueBase { public: @@ -249,6 +391,261 @@ class TestPacketArrivalListener : public cListener lastArrivalGate = check_and_cast(value)->getArrivalGate(); } }; + +class TestSignalListener : public cListener +{ + public: + int numSignals = 0; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *) override { numSignals++; } +}; + +class TestPacketDropSignalListener : public cListener +{ + public: + int numSignals = 0; + int numOtherPacketDrops = 0; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *details) override { + numSignals++; + auto packetDropDetails = check_and_cast(details); + if (packetDropDetails->getReason() == OTHER_PACKET_DROP) + numOtherPacketDrops++; + } +}; + +class TestAckHandler : public IAckHandler +{ + public: + virtual bool isEligibleToTransmit(const Ptr&) override { return true; } + virtual bool isOutstandingFrame(const Ptr&) override { return false; } + virtual void frameGotInProgress(const Ptr&) override {} +}; + +class TestQosAckHandler : public QosAckHandler +{ + public: + bool hasMgmtEntry(const Ptr& header) const { + auto id = std::make_pair(header->getReceiverAddress(), SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())); + return mgmtAckStatuses.find(id) != mgmtAckStatuses.end(); + } +}; + +class TestInProgressFrames : public InProgressFrames +{ + public: + void configure(IOriginatorMacDataService *dataService, IAckHandler *ackHandler, queueing::IPacketQueue *pendingQueue = nullptr) { + this->dataService = dataService; + this->ackHandler = ackHandler; + this->pendingQueue = pendingQueue; + } + void addFrame(Packet *packet) { inProgressFrames.push_back(packet); } + void addOwnedFrame(Packet *packet) { take(packet); inProgressFrames.push_back(packet); } + bool hasEligibleFrame() { return hasEligibleFrameToTransmit(); } + int getNumDroppedFrames() const { return droppedFrames.size(); } + std::vector releaseFrames() { + std::vector frames = inProgressFrames; + frames.insert(frames.end(), droppedFrames.begin(), droppedFrames.end()); + for (auto frame : frames) + if (frame->getOwner() == this) + drop(frame); + inProgressFrames.clear(); + droppedFrames.clear(); + return frames; + } +}; + +class TestEdcaf : public Edcaf +{ + public: + queueing::IPacketQueue *pendingQueue = nullptr; + InProgressFrames *inProgressFrames = nullptr; + QosAckHandler *qosAckHandler = nullptr; + QosRecoveryProcedure *qosRecoveryProcedure = nullptr; + StationRetryCounters stationRetryCounters; + int numReleaseChannelCalls = 0; + std::vector *eventOrder = nullptr; + bool contentionActive = false; + int numRequestChannelCalls = 0; + int numContentionStarts = 0; + + virtual queueing::IPacketQueue *getPendingQueue() const override { return pendingQueue; } + virtual InProgressFrames *getInProgressFrames() const override { return inProgressFrames; } + virtual QosAckHandler *getAckHandler() const override { return qosAckHandler; } + virtual QosRecoveryProcedure *getRecoveryProcedure() const override { return qosRecoveryProcedure; } + virtual StationRetryCounters *getStationRetryCounters() const override { return const_cast(&stationRetryCounters); } + virtual void releaseChannel(IChannelAccess::ICallback *) override { numReleaseChannelCalls++; if (eventOrder != nullptr) eventOrder->push_back(1); } + virtual void requestChannel(IChannelAccess::ICallback *) override { numRequestChannelCalls++; if (!contentionActive) numContentionStarts++; } + void setAccessCategory(AccessCategory accessCategory) { ac = accessCategory; } + void setTxopProcedure(TxopProcedure *txopProcedure) { this->txopProcedure = txopProcedure; } +}; + +class TestEdca : public Edca +{ + public: + Edcaf *edcaf = nullptr; + std::array edcafs = {}; + int numEdcafs = 1; + NonQosRecoveryProcedure *nonQosRecoveryProcedure = nullptr; + std::vector requestedAccessCategories; + std::vector internallyCollidedEdcafs; + std::vector *eventOrder = nullptr; + + virtual int getNumEdcafs() const override { return numEdcafs; } + virtual Edcaf *getEdcaf(AccessCategory accessCategory) const override { return numEdcafs == 1 ? edcaf : edcafs[accessCategory]; } + virtual Edcaf *getChannelOwner() override { return edcaf; } + virtual std::vector getInternallyCollidedEdcafs() override { return internallyCollidedEdcafs; } + virtual void requestChannelAccess(AccessCategory accessCategory, IChannelAccess::ICallback *callback) override { requestedAccessCategories.push_back(accessCategory); if (eventOrder != nullptr) eventOrder->push_back(3); getEdcaf(accessCategory)->requestChannel(callback); } + virtual NonQosRecoveryProcedure *getMgmtAndNonQoSRecoveryProcedure() const override { return nonQosRecoveryProcedure; } +}; + +class TestHcf : public Hcf +{ + public: + virtual ~TestHcf() { + for (auto listener : getLocalSignalListeners(Ieee80211Mac::frameTransmissionOutcomeSignal)) + unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, listener); + } + int numProcessedDroppedSetupFrames = 0; + int numResumedEligibleChannelAccess = 0; + int numCancelledAddbaTransactions = 0; + int numRebuildEligibilityCalls = 0; + bool droppedSetupCancelled = false; + bool delegateDroppedSetupHandling = false; + TestCallback *managementCallback = nullptr; + + void configure(Edca *edca) { this->edca = edca; } + void configureMac(Ieee80211Mac *mac) { this->mac = mac; subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, check_and_cast(mac)); } + void configureEligibilityIndex(Edca *edca, IOriginatorMacDataService *originatorDataService) { + this->edca = edca; + this->originatorDataService = originatorDataService; + } + void rebuildEligibilityIndex() { rebuildPendingFrameEligibility(); } + void trackFrame(Packet *packet, AccessCategory accessCategory) { trackPendingFrame(packet, accessCategory); } + void untrackFrame(Packet *packet) { untrackPendingFrame(packet); } + bool hasFrame(AccessCategory accessCategory) { return hasFrameToTransmit(accessCategory); } + void requestEligible() { requestEligibleChannelAccess(); } + int processInternalCollision(const std::vector& edcafs) { return handleInternalCollision(edcafs); } + void grantChannel(IChannelAccess *channelAccess) { channelGranted(channelAccess); } + void finishSequence() { frameSequenceFinished(); } + int getNumEligiblePendingFrames(AccessCategory accessCategory) const { return numEligiblePendingFrames[accessCategory]; } + int getNumTrackedPendingFrames() const { return pendingFrameEligibility.size(); } + void observePendingQueue(queueing::IPacketQueue *pendingQueue) { + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + } + void configureFrameSequenceHandler(IFrameSequenceHandler *frameSequenceHandler) { this->frameSequenceHandler = frameSequenceHandler; } + void configureRecipientDataService(IRecipientQosMacDataService *recipientDataService) { this->recipientDataService = recipientDataService; } + void configureManagementCallback(TestCallback *managementCallback) { this->managementCallback = managementCallback; } + void configureTransmittedData(Edca *edca, IOriginatorBlockAckAgreementHandler *handler, IOriginatorBlockAckAgreementPolicy *policy) { + this->edca = edca; + originatorBlockAckAgreementHandler = handler; + originatorBlockAckAgreementPolicy = policy; + } + void configureTransmittedManagement(Edca *edca, IOriginatorQoSAckPolicy *originatorAckPolicy) { + this->edca = edca; + this->originatorAckPolicy = originatorAckPolicy; + } + void configureBlockAckHandlers(IOriginatorBlockAckAgreementHandler *originatorHandler, IOriginatorBlockAckAgreementPolicy *originatorPolicy, IRecipientBlockAckAgreementHandler *recipientHandler, IRecipientBlockAckAgreementPolicy *recipientPolicy) { + originatorBlockAckAgreementHandler = originatorHandler; + originatorBlockAckAgreementPolicy = originatorPolicy; + recipientBlockAckAgreementHandler = recipientHandler; + recipientBlockAckAgreementPolicy = recipientPolicy; + } + void processTransmittedData(Packet *packet, const Ptr& dataHeader, AccessCategory ac) { originatorProcessTransmittedDataFrame(packet, dataHeader, ac); } + void processTransmittedManagement(Packet *packet, const Ptr& managementHeader, AccessCategory ac) { originatorProcessTransmittedManagementFrame(packet, managementHeader, ac); } + void dropPacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DROPPED); } + void removePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::REMOVED); } + void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } + void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } + void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } + void processFailedFrame(Packet *packet) { originatorProcessFailedFrame(packet); } + void processReceivedAck(Packet *packet, Packet *lastTransmittedPacket, AccessCategory accessCategory) { + auto header = packet->peekAtFront(); + auto lastTransmittedHeader = lastTransmittedPacket->peekAtFront(); + originatorProcessReceivedControlFrame(packet, header, lastTransmittedPacket, lastTransmittedHeader, accessCategory); + } + + protected: + virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (managementCallback != nullptr) + managementCallback->processMgmtFrame(packet, header); + else + Hcf::processMgmtFrame(packet, header); + } + virtual bool processDroppedBlockAckSetupFrame(Packet *packet) override { numProcessedDroppedSetupFrames++; return delegateDroppedSetupHandling ? Hcf::processDroppedBlockAckSetupFrame(packet) : droppedSetupCancelled; } + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { numCancelledAddbaTransactions++; Hcf::cancelAddbaTransaction(transactionId, excludedPacket); } + virtual void rebuildPendingFrameEligibility() override { numRebuildEligibilityCalls++; Hcf::rebuildPendingFrameEligibility(); } + virtual void resumeEligibleChannelAccess() override { numResumedEligibleChannelAccess++; } + virtual void scheduleInactivityTimer(simtime_t) override {} + virtual void scheduleAddbaResponseTimer(simtime_t) override {} +}; + +class TestMac : public Ieee80211Mac +{ + public: + using Ieee80211Mac::receiveSignal; + int numPendingRadioConfigSends = 0; + virtual void sendDownPendingRadioConfigMsg() override { numPendingRadioConfigSends++; } +}; + +class TestFragmentationPolicy : public IFragmentationPolicy +{ + public: + virtual std::vector computeFragmentSizes(Packet *) override { return { 2, 2 }; } +}; + +class TestMsduAggregationPolicy : public IMsduAggregationPolicy +{ + public: + int numCalls = 0; + + virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *, Packet *, const std::function&) override { numCalls++; return nullptr; } +}; + +class TestSelectedMsduAggregationPolicy : public IMsduAggregationPolicy +{ + public: + int numCalls = 0; + std::vector selectedPackets; + + virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *, Packet *, const std::function&) override { numCalls++; return new std::vector(selectedPackets); } +}; + +class TestBasicMsduAggregationPolicy : public BasicMsduAggregationPolicy +{ + public: + void configure(b maximumSize) { + qOsCheck = true; + subframeNumThreshold = -1; + aggregationLengthThreshold = -1; + maxAMsduSize = maximumSize; + } +}; + +class TestNonQosRecoveryProcedure : public NonQosRecoveryProcedure +{ + public: + void addRetry(const Ptr& header) { + rtsThreshold = INT_MAX; + shortRetryCounter[SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())] = 1; + } + void addShortRetry(const Ptr& header, int count = 1) { + shortRetryCounter[SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())] = count; + } + void addLongRetry(const Ptr& header, int count = 1) { + longRetryCounter[SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())] = count; + } + bool hasRetry(const Ptr& header) const { + return shortRetryCounter.find(SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())) != shortRetryCounter.end(); + } + void configureRts(int threshold, int retryLimit, ICwCalculator *cwCalculator) { + rtsThreshold = threshold; + shortRetryLimit = retryLimit; + longRetryLimit = retryLimit; + this->cwCalculator = cwCalculator; + } +}; + class TestOnlyPacketExtractor : public queueing::IPacketExtractor { public: @@ -265,6 +662,59 @@ class TestOnlyPacketExtractor : public queueing::IPacketExtractor return packet; } }; + +class TestCwCalculator : public IRecoveryProcedure::ICwCalculator +{ + public: + int cw = 1; + virtual void incrementCw() override { cw++; } + virtual void resetCw() override { cw = 1; } + virtual int getCw() override { return cw; } +}; + +class TestOriginatorQosMacDataService : public OriginatorQosMacDataService +{ + public: + void enableMsduAggregationPolicy(IMsduAggregationPolicy *policy) { + aMsduAggregationPolicy = policy; + if (aMsduAggregation == nullptr) + aMsduAggregation = new MsduAggregation(); + } + void enableSequenceNumberAssignment() { sequenceNumberAssignment = new QoSSequenceNumberAssignment(); } + void enableFragmentation(IFragmentationPolicy *policy) { + fragmentationPolicy = policy; + fragmentation = new Fragmentation(); + } + std::vector *fragment(Packet *packet) { return fragmentIfNeeded(packet); } + void release(Packet *packet) { drop(packet); } +}; + +class TestFrameSequenceHandler : public IFrameSequenceHandler +{ + public: + IFrameSequence *frameSequence = nullptr; + FrameSequenceContext *context = nullptr; + bool running = false; + int numStartedSequences = 0; + + virtual ~TestFrameSequenceHandler() { + delete frameSequence; + delete context; + } + virtual const FrameSequenceContext *getContext() const override { return context; } + virtual const IFrameSequence *getFrameSequence() const override { return frameSequence; } + virtual void startFrameSequence(IFrameSequence *frameSequence, FrameSequenceContext *context, ICallback *) override { + this->frameSequence = frameSequence; + this->context = context; + running = true; + numStartedSequences++; + } + virtual void processResponse(Packet *) override {} + virtual void transmissionComplete() override {} + virtual bool isSequenceRunning() override { return running; } + virtual void handleStartRxTimeout() override {} +}; + static Ptr makeQosHeader(MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber) { auto header = makeShared(); @@ -274,8 +724,41 @@ static Ptr makeQosHeader(MacAddress receiverAddress, Tid ti header->setSequenceNumber(sequenceNumber); return header; } + +static Packet *makeTaggedDelbaPacket(const char *name, MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint64_t transactionId) +{ + auto delba = makeShared(); + delba->setReceiverAddress(receiverAddress); + delba->setTid(tid); + delba->setInitiator(true); + delba->setSequenceNumber(sequenceNumber); + delba->setFragmentNumber(fragmentNumber); + delba->setMoreFragments(moreFragments); + auto packet = new Packet(name, delba); + packet->addTag()->setTransactionId(transactionId); + return packet; +} + +static Ptr makeResponse(MacAddress transmitterAddress, Tid tid, uint8_t dialogToken, uint16_t statusCode) +{ + auto response = makeShared(); + response->setTransmitterAddress(transmitterAddress); + response->setTid(tid); + response->setDialogToken(dialogToken); + response->setStatusCode(statusCode); + response->setBufferSize(32); + response->setBlockAckTimeoutValue(0); + return response; +} + + %activity: +// IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.2. +TestOriginatorPolicy originatorPolicy; +TestCallback callback; MacAddress peer1("00:00:00:00:00:01"); +MacAddress peer2("00:00:00:00:00:02"); +MacAddress peer3("00:00:00:00:00:03"); auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const char *dropperClass = "") { auto module = cModuleType::get("inet.queueing.queue.PacketQueue")->create(name, this); module->par("packetCapacity").setIntValue(packetCapacity); @@ -283,6 +766,749 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const module->callInitialize(); return check_and_cast(module); }; + +{ + TestOriginatorHandler tokenHandler; + tokenHandler.setNextDialogToken(255); + auto request255 = tokenHandler.buildAddbaRequest(peer1, 0, SequenceNumberCyclic(0), &originatorPolicy); + auto request1 = tokenHandler.buildAddbaRequest(peer1, 0, SequenceNumberCyclic(0), &originatorPolicy); + ASSERT(request255->getDialogToken() == 255); + ASSERT(request1->getDialogToken() == 1); +} + +TestOriginatorHandler handler; +Packet triggerPacket("trigger"); +auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); + +// Transmitting the trigger MPDU does not create an agreement; initiation is +// deferred until the HCF successful-ACK path calls processAcknowledgedDataFrame(). +{ + auto preAckHandler = new TestOriginatorHandler(); + QosAckHandler preAckAckHandler; + TestEdcaf preAckEdcaf; + preAckEdcaf.qosAckHandler = &preAckAckHandler; + TestEdca preAckEdca; + preAckEdca.edcaf = &preAckEdcaf; + TestHcf preAckHcf; + preAckHcf.configureTransmittedData(&preAckEdca, preAckHandler, &originatorPolicy); + Packet preAckPacket("preAckPacket"); + auto preAckHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); + preAckHeader->setAckPolicy(NORMAL_ACK); + preAckHcf.processTransmittedData(&preAckPacket, preAckHeader, AC_BE); + if (preAckHandler->getAgreement(peer1, 3) != nullptr) + throw cRuntimeError("HCF initiated ADDBA before the trigger MPDU was acknowledged"); + ASSERT(preAckHandler->getAgreement(peer1, 3) == nullptr); +} + +// A fragmented MSDU cannot trigger ADDBA until its final fragment is ACKed, +// otherwise the remaining fragments would precede the advertised SSN. +{ + TestOriginatorHandler fragmentedDataHandler; + TestCallback fragmentedDataCallback; + Packet fragmentedDataPacket("fragmentedDataPacket"); + auto firstFragmentHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(100)); + firstFragmentHeader->setFragmentNumber(0); + firstFragmentHeader->setMoreFragments(true); + fragmentedDataHandler.processAcknowledgedDataFrame(&fragmentedDataPacket, firstFragmentHeader, &originatorPolicy, &fragmentedDataCallback); + if (fragmentedDataHandler.getAgreement(peer1, 6) != nullptr || fragmentedDataCallback.managementPacket != nullptr) + throw cRuntimeError("ADDBA was initiated before the final data fragment was acknowledged"); + auto finalFragmentHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(100)); + finalFragmentHeader->setFragmentNumber(1); + finalFragmentHeader->setMoreFragments(false); + fragmentedDataHandler.processAcknowledgedDataFrame(&fragmentedDataPacket, finalFragmentHeader, &originatorPolicy, &fragmentedDataCallback); + auto fragmentedDataRequest = dynamicPtrCast(fragmentedDataCallback.managementHeader); + ASSERT(fragmentedDataRequest != nullptr); + ASSERT(fragmentedDataRequest->getStartingSequenceNumber() == SequenceNumberCyclic(101)); +} + +// A fragmented, transaction-tagged DELBA remains eligible through every +// nonfinal fragment. Aborting one fragment invalidates the transaction and asks +// the HCF callback to remove its sibling fragments. +{ + TestOriginatorHandler fragmentedDelbaHandler; + TestCallback fragmentedDelbaCallback; + auto firstDelba = makeShared(); + firstDelba->setReceiverAddress(peer1); + firstDelba->setTid(6); + firstDelba->setInitiator(true); + firstDelba->setFragmentNumber(0); + firstDelba->setMoreFragments(true); + auto finalDelba = staticPtrCast(firstDelba->dupShared()); + finalDelba->setFragmentNumber(1); + finalDelba->setMoreFragments(false); + Packet firstDelbaPacket("firstDelbaFragment", firstDelba); + Packet finalDelbaPacket("finalDelbaFragment", finalDelba); + firstDelbaPacket.addTag()->setTransactionId(77); + finalDelbaPacket.addTag()->setTransactionId(77); + fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 77); + ASSERT(fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); + ASSERT(fragmentedDelbaHandler.processTransmittedDelba(&firstDelbaPacket, &fragmentedDelbaCallback) == nullptr); + ASSERT(fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); + ASSERT(fragmentedDelbaHandler.processTransmittedDelba(&finalDelbaPacket, &fragmentedDelbaCallback) == nullptr); + // A failed attempt remains eligible for retransmission. + ASSERT(fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); + ASSERT(fragmentedDelbaHandler.processTransmittedDelba(&finalDelbaPacket, &fragmentedDelbaCallback) == nullptr); + ASSERT(fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); + ASSERT(!fragmentedDelbaHandler.processAcknowledgedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); + ASSERT(fragmentedDelbaHandler.processAcknowledgedDelba(&finalDelbaPacket, &fragmentedDelbaCallback)); + ASSERT(!fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); + ASSERT(fragmentedDelbaCallback.cancelledTransactionIds.back() == 77); + ASSERT(fragmentedDelbaCallback.excludedPackets.back() == &finalDelbaPacket); + + fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 78); + firstDelbaPacket.getTagForUpdate()->setTransactionId(78); + ASSERT(fragmentedDelbaHandler.processAbortedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); + ASSERT(!fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); + ASSERT(fragmentedDelbaCallback.cancelledTransactionIds.back() == 78); + ASSERT(fragmentedDelbaCallback.excludedPackets.back() == &firstDelbaPacket); +} + +// IEEE Std 802.11-2024, 10.23.2.12.1 and 11.5.3.2: the HCF retry path keeps a +// transaction-tagged DELBA selectable and preserves its negative ACK state. +{ + constexpr uint64_t transactionId = 79; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + TestQosAckHandler ackHandler; + TestPacketQueue pendingQueue; + TestOriginatorQosMacDataService dataService; + auto originatorHandler = new TestOriginatorHandler(); + dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto delba = dynamicPtrCast(packet->peekAtFront()); + return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); + }); + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + TestOriginatorPolicy policy; + OriginatorQosAckPolicy ackPolicy; + TestHcf hcf; + hcf.configureTransmittedData(&edca, originatorHandler, &policy); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + + auto packet = makeTaggedDelbaPacket("failedTransactionalDelba", peer1, 6, SequenceNumberCyclic(203), 0, false, transactionId); + auto delba = packet->peekAtFront(); + originatorHandler->setPendingTeardownTransaction(peer1, 6, transactionId); + inProgressFrames.addOwnedFrame(packet); + ackHandler.frameGotInProgress(delba); + hcf.processTransmittedManagement(packet, delba, AC_BE); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(delba) == QosAckHandler::Status::WAITING_FOR_NORMAL_ACK); + hcf.processFailedFrame(packet); + ASSERT(originatorHandler->isDelbaPending(packet, packet->peekAtFront())); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(delba) == QosAckHandler::Status::NORMAL_ACK_NOT_ARRIVED); + ASSERT(ackHandler.hasMgmtEntry(delba)); + ASSERT(inProgressFrames.getFrameToTransmit() == packet); + ASSERT(inProgressFrames.getNumDroppedFrames() == 0); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames == std::vector({ packet })); + delete packet; +} + +// A final-fragment ACK retires the DELBA transaction through HCF exactly once, +// removes its in-progress sibling, and clears both ACK-handler entries. +{ + constexpr uint64_t transactionId = 80; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + TestQosAckHandler ackHandler; + TestPacketQueue pendingQueue; + TestOriginatorQosMacDataService dataService; + auto originatorHandler = new TestOriginatorHandler(); + dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto delba = dynamicPtrCast(packet->peekAtFront()); + return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); + }); + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + TestOriginatorPolicy policy; + OriginatorQosAckPolicy ackPolicy; + TestHcf hcf; + TestMac acknowledgedDelbaMac; + hcf.configureMac(&acknowledgedDelbaMac); + hcf.configureTransmittedData(&edca, originatorHandler, &policy); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + + auto sibling = makeTaggedDelbaPacket("acknowledgedDelbaSibling", peer1, 6, SequenceNumberCyclic(204), 0, true, transactionId); + auto finalPacket = makeTaggedDelbaPacket("acknowledgedFinalDelba", peer1, 6, SequenceNumberCyclic(204), 1, false, transactionId); + auto siblingHeader = sibling->peekAtFront(); + auto finalHeader = finalPacket->peekAtFront(); + originatorHandler->setPendingTeardownTransaction(peer1, 6, transactionId); + inProgressFrames.addOwnedFrame(sibling); + inProgressFrames.addOwnedFrame(finalPacket); + ackHandler.frameGotInProgress(siblingHeader); + ackHandler.frameGotInProgress(finalHeader); + hcf.processTransmittedManagement(finalPacket, finalHeader, AC_BE); + Packet ackPacket("transactionalDelbaAck", makeShared()); + hcf.processReceivedAck(&ackPacket, finalPacket, AC_BE); + ASSERT(!originatorHandler->isDelbaPending(finalPacket, finalHeader)); + ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 1); + ASSERT(!ackHandler.hasMgmtEntry(siblingHeader)); + ASSERT(!ackHandler.hasMgmtEntry(finalHeader)); + ASSERT(!originatorHandler->processAcknowledgedDelba(finalPacket, &hcf)); + ASSERT(hcf.numCancelledAddbaTransactions == 1); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames == std::vector({ finalPacket })); + delete finalPacket; +} + +// Reaching the retry limit aborts the teardown through HCF, clears its sibling +// and both ACK-handler entries, and records one terminal cancellation. +{ + constexpr uint64_t transactionId = 81; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 1, &cwCalculator); + TestQosAckHandler ackHandler; + TestPacketQueue pendingQueue; + TestOriginatorQosMacDataService dataService; + auto originatorHandler = new TestOriginatorHandler(); + dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto delba = dynamicPtrCast(packet->peekAtFront()); + return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); + }); + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + TestOriginatorPolicy policy; + OriginatorQosAckPolicy ackPolicy; + TestHcf hcf; + TestMac retryLimitedDelbaMac; + hcf.configureMac(&retryLimitedDelbaMac); + hcf.configureTransmittedData(&edca, originatorHandler, &policy); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + + auto sibling = makeTaggedDelbaPacket("retryLimitedDelbaSibling", peer1, 6, SequenceNumberCyclic(205), 0, true, transactionId); + auto finalPacket = makeTaggedDelbaPacket("retryLimitedFinalDelba", peer1, 6, SequenceNumberCyclic(205), 1, false, transactionId); + auto siblingHeader = sibling->peekAtFront(); + auto finalHeader = finalPacket->peekAtFront(); + originatorHandler->setPendingTeardownTransaction(peer1, 6, transactionId); + inProgressFrames.addOwnedFrame(sibling); + inProgressFrames.addOwnedFrame(finalPacket); + ackHandler.frameGotInProgress(siblingHeader); + ackHandler.frameGotInProgress(finalHeader); + hcf.processTransmittedManagement(finalPacket, finalHeader, AC_BE); + hcf.processFailedFrame(finalPacket); + ASSERT(!originatorHandler->isDelbaPending(finalPacket, finalHeader)); + ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 1); + ASSERT(!ackHandler.hasMgmtEntry(siblingHeader)); + ASSERT(!ackHandler.hasMgmtEntry(finalHeader)); + ASSERT(!originatorHandler->processAbortedDelba(finalPacket, &hcf)); + ASSERT(hcf.numCancelledAddbaTransactions == 1); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames == std::vector({ finalPacket })); + delete finalPacket; +} + +handler.processAcknowledgedDataFrame(&triggerPacket, triggerHeader, &originatorPolicy, &callback); +auto request = dynamicPtrCast(callback.managementHeader); +ASSERT(request != nullptr); +ASSERT(request->getDialogToken() != 0); +ASSERT(request->getStartingSequenceNumber() == SequenceNumberCyclic(0)); +handler.processTransmittedAddbaReq(callback.managementPacket, request, &originatorPolicy, &callback); +ASSERT(callback.addbaDeadline == simTime() + 1); +auto requestTransactionId = callback.managementPacket->getTag()->getTransactionId(); + +auto wrongToken = makeResponse(peer1, 3, request->getDialogToken() + 1, 0); +ASSERT(handler.processReceivedAddbaResp(wrongToken, &originatorPolicy, &callback).establishedAgreement == nullptr); +ASSERT(handler.isAddbaResponsePending(peer1, 3)); +auto wrongTid = makeResponse(peer1, 4, request->getDialogToken(), 0); +ASSERT(handler.processReceivedAddbaResp(wrongTid, &originatorPolicy, &callback).establishedAgreement == nullptr); +auto wrongPeer = makeResponse(peer2, 3, request->getDialogToken(), 0); +ASSERT(handler.processReceivedAddbaResp(wrongPeer, &originatorPolicy, &callback).establishedAgreement == nullptr); + +auto success = makeResponse(peer1, 3, request->getDialogToken(), 0); +auto establishedResponse = handler.processReceivedAddbaResp(success, &originatorPolicy, &callback); +auto established = establishedResponse.establishedAgreement; +ASSERT(established != nullptr); +ASSERT(establishedResponse.terminatedAgreement == nullptr); +ASSERT(establishedResponse.teardownDelba == nullptr); +ASSERT(establishedResponse.teardownTransactionId == 0); +ASSERT(callback.cancelledTransactionIds.back() == requestTransactionId); +ASSERT(callback.excludedPackets.back() == nullptr); +ASSERT(established->getStartingSequenceNumber() == SequenceNumberCyclic(0)); +ASSERT(!handler.isAddbaResponsePending(peer1, 3)); +ASSERT(handler.processReceivedAddbaResp(success, &originatorPolicy, &callback).establishedAgreement == nullptr); + +// IEEE Std 802.11-2024, 10.25.2 and Figure 11-32: a successful matching +// response establishes the agreement even when local policy rejects the +// negotiated parameters; the originator immediately deletes its local state +// and sends END_BA on a best-effort basis. +{ + TestOriginatorHandler vetoHandler; + TestCallback vetoCallback; + TestOriginatorPolicy vetoPolicy; + vetoPolicy.addbaAccepted = false; + Packet vetoTrigger("vetoTrigger"); + auto vetoHeader = makeQosHeader(peer3, 6, SequenceNumberCyclic(30)); + vetoHandler.processAcknowledgedDataFrame(&vetoTrigger, vetoHeader, &vetoPolicy, &vetoCallback); + auto vetoRequest = dynamicPtrCast(vetoCallback.managementHeader); + auto vetoTransactionId = vetoCallback.managementPacket->getTag()->getTransactionId(); + vetoHandler.processTransmittedAddbaReq(vetoCallback.managementPacket, vetoRequest, &vetoPolicy, &vetoCallback); + auto vetoResponse = makeResponse(peer3, 6, vetoRequest->getDialogToken(), 0); + auto vetoResponseOutcome = vetoHandler.processReceivedAddbaResp(vetoResponse, &vetoPolicy, &vetoCallback); + auto vetoAgreement = vetoResponseOutcome.terminatedAgreement.get(); + ASSERT(vetoAgreement != nullptr); + ASSERT(vetoResponseOutcome.establishedAgreement == nullptr); + ASSERT(vetoResponseOutcome.teardownTransactionId == vetoTransactionId); + ASSERT(vetoAgreement->getIsAddbaResponseReceived()); + ASSERT(vetoHandler.getAgreement(peer3, 6) == nullptr); + auto vetoDelba = vetoResponseOutcome.teardownDelba; + ASSERT(vetoDelba != nullptr); + ASSERT(vetoDelba->getInitiator()); + ASSERT(vetoDelba->getReceiverAddress() == peer3); + ASSERT(vetoDelba->getTid() == 6); + ASSERT(vetoDelba->getReasonCode() == RC_END_BA); + Packet vetoDelbaPacket("vetoDelba", vetoDelba); + vetoDelbaPacket.addTag()->setTransactionId(vetoResponseOutcome.teardownTransactionId); + ASSERT(vetoHandler.isDelbaPending(&vetoDelbaPacket, vetoDelba)); + ASSERT(vetoHandler.getAgreement(peer3, 6) == nullptr); + ASSERT(vetoHandler.getRetryDeadline(peer3, 6) > simTime()); + TestCallback vetoRetryCallback; + Packet vetoRetryTrigger("vetoRetryTrigger"); + auto vetoRetryHeader = makeQosHeader(peer3, 6, SequenceNumberCyclic(31)); + vetoHandler.processAcknowledgedDataFrame(&vetoRetryTrigger, vetoRetryHeader, &vetoPolicy, &vetoRetryCallback); + ASSERT(vetoRetryCallback.managementPacket == nullptr); + vetoHandler.allowRetryNow(peer3, 6); + auto obsoleteVetoTransactionId = vetoHandler.processAcknowledgedDataFrame(&vetoRetryTrigger, vetoRetryHeader, &vetoPolicy, &vetoRetryCallback); + ASSERT(obsoleteVetoTransactionId == vetoTransactionId); + ASSERT(vetoRetryCallback.managementPacket != nullptr); + auto newerAgreement = vetoHandler.getAgreement(peer3, 6); + ASSERT(newerAgreement != nullptr); + ASSERT(newerAgreement->getTransactionId() != vetoTransactionId); + ASSERT(!vetoHandler.isDelbaPending(&vetoDelbaPacket, vetoDelba)); + ASSERT(!vetoHandler.processAbortedDelba(&vetoDelbaPacket, &vetoCallback)); + ASSERT(!vetoHandler.isDelbaPending(&vetoDelbaPacket, vetoDelba)); + auto vetoTerminated = vetoHandler.processTransmittedDelba(&vetoDelbaPacket, &vetoCallback); + ASSERT(vetoTerminated == nullptr); + ASSERT(vetoHandler.getAgreement(peer3, 6) == newerAgreement); +} + +Packet rejectedTrigger("rejectedTrigger"); +auto rejectedHeader = makeQosHeader(peer2, 4, SequenceNumberCyclic(10)); +handler.processAcknowledgedDataFrame(&rejectedTrigger, rejectedHeader, &originatorPolicy, &callback); +auto rejectedRequest = dynamicPtrCast(callback.managementHeader); +handler.processTransmittedAddbaReq(callback.managementPacket, rejectedRequest, &originatorPolicy, &callback); +auto rejection = makeResponse(peer2, 4, rejectedRequest->getDialogToken(), 1); +ASSERT(handler.processReceivedAddbaResp(rejection, &originatorPolicy, &callback).establishedAgreement == nullptr); +ASSERT(handler.getAgreement(peer2, 4) == nullptr); +handler.processAcknowledgedDataFrame(&rejectedTrigger, rejectedHeader, &originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer2, 4) == nullptr); +handler.allowRetryNow(peer2, 4); +handler.processAcknowledgedDataFrame(&rejectedTrigger, rejectedHeader, &originatorPolicy, &callback); +ASSERT(handler.isAddbaResponsePending(peer2, 4)); + +Packet timeoutTrigger("timeoutTrigger"); +auto timeoutHeader = makeQosHeader(peer3, 5, SequenceNumberCyclic(20)); +handler.processAcknowledgedDataFrame(&timeoutTrigger, timeoutHeader, &originatorPolicy, &callback); +auto timedRequest = dynamicPtrCast(callback.managementHeader); +auto timedRequestPacket = callback.managementPacket; +auto timedTransactionId = timedRequestPacket->getTag()->getTransactionId(); +handler.processTransmittedAddbaReq(timedRequestPacket, timedRequest, &originatorPolicy, &callback); +handler.expireNow(peer3, 5); +handler.addbaResponseTimeoutExpired(&originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer3, 5) == nullptr); +ASSERT(callback.cancelledTransactionIds.back() == timedTransactionId); +ASSERT(callback.excludedPackets.back() == nullptr); +handler.processTransmittedAddbaReq(timedRequestPacket, timedRequest, &originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer3, 5) == nullptr); +auto staleResponse = makeResponse(peer3, 5, timedRequest->getDialogToken(), 0); +ASSERT(handler.processReceivedAddbaResp(staleResponse, &originatorPolicy, &callback).establishedAgreement == nullptr); +handler.processAcknowledgedDataFrame(&timeoutTrigger, timeoutHeader, &originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer3, 5) == nullptr); +handler.allowRetryNow(peer3, 5); +handler.processAcknowledgedDataFrame(&timeoutTrigger, timeoutHeader, &originatorPolicy, &callback); +ASSERT(handler.isAddbaResponsePending(peer3, 5)); + +// All simultaneously expired agreements are erased before cancellation can +// reenter the MAC and query or mutate transaction state. +{ + TestOriginatorHandler timeoutHandler; + TestOriginatorPolicy timeoutPolicy; + TestTimeoutCancellationCallback timeoutCallback; + timeoutCallback.handler = &timeoutHandler; + timeoutCallback.expiredAgreementIds = { { peer1, 1 }, { peer2, 2 } }; + + Packet firstTimeoutTrigger("firstTimeoutTrigger"); + auto firstTimeoutHeader = makeQosHeader(peer1, 1, SequenceNumberCyclic(40)); + timeoutHandler.processAcknowledgedDataFrame(&firstTimeoutTrigger, firstTimeoutHeader, &timeoutPolicy, &timeoutCallback); + auto firstTimeoutRequestPacket = timeoutCallback.managementPacket; + auto firstTimeoutRequest = dynamicPtrCast(timeoutCallback.managementHeader); + auto firstTimeoutTransactionId = firstTimeoutRequestPacket->getTag()->getTransactionId(); + timeoutHandler.processTransmittedAddbaReq(firstTimeoutRequestPacket, firstTimeoutRequest, &timeoutPolicy, &timeoutCallback); + + Packet secondTimeoutTrigger("secondTimeoutTrigger"); + auto secondTimeoutHeader = makeQosHeader(peer2, 2, SequenceNumberCyclic(50)); + timeoutHandler.processAcknowledgedDataFrame(&secondTimeoutTrigger, secondTimeoutHeader, &timeoutPolicy, &timeoutCallback); + auto secondTimeoutRequestPacket = timeoutCallback.managementPacket; + auto secondTimeoutRequest = dynamicPtrCast(timeoutCallback.managementHeader); + auto secondTimeoutTransactionId = secondTimeoutRequestPacket->getTag()->getTransactionId(); + timeoutHandler.processTransmittedAddbaReq(secondTimeoutRequestPacket, secondTimeoutRequest, &timeoutPolicy, &timeoutCallback); + + timeoutHandler.expireNow(peer1, 1); + timeoutHandler.expireNow(peer2, 2); + timeoutHandler.addbaResponseTimeoutExpired(&timeoutPolicy, &timeoutCallback); + ASSERT(timeoutCallback.allExpiredAgreementsGoneBeforeCallbacks); + ASSERT(timeoutCallback.cancelledTransactionIds.size() == 2); + ASSERT(std::find(timeoutCallback.cancelledTransactionIds.begin(), timeoutCallback.cancelledTransactionIds.end(), firstTimeoutTransactionId) != timeoutCallback.cancelledTransactionIds.end()); + ASSERT(std::find(timeoutCallback.cancelledTransactionIds.begin(), timeoutCallback.cancelledTransactionIds.end(), secondTimeoutTransactionId) != timeoutCallback.cancelledTransactionIds.end()); + ASSERT(timeoutCallback.addbaDeadline == SIMTIME_MAX); +} + +// Transaction cancellation leaves packets borrowed by the active frame +// sequence under that sequence's ownership, including RTS-protected packets. +{ + const uint64_t transactionId = 900; + TestPacketQueue pendingQueue; + TestInProgressFrames activeFrames; + auto directRequest = makeShared(); + auto directPacket = new Packet("activeAddbaRequest", directRequest); + directPacket->addTag()->setTransactionId(transactionId); + auto protectedRequest = makeShared(); + auto protectedPacket = new Packet("rtsProtectedAddbaRequest", protectedRequest); + protectedPacket->addTag()->setTransactionId(transactionId); + activeFrames.addFrame(directPacket); + activeFrames.addFrame(protectedPacket); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &activeFrames; + TestEdca edca; + edca.edcaf = &edcaf; + auto frameSequenceHandler = new TestFrameSequenceHandler(); + frameSequenceHandler->running = true; + frameSequenceHandler->context = new FrameSequenceContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &activeFrames, nullptr, nullptr, nullptr, nullptr); + frameSequenceHandler->context->addStep(new TransmitStep(directPacket, 0)); + frameSequenceHandler->context->addStep(new RtsTransmitStep(protectedPacket, new Packet("rts", makeShared()), 0)); + TestHcf hcf; + hcf.configure(&edca); + hcf.configureFrameSequenceHandler(frameSequenceHandler); + hcf.cancelTransaction(transactionId); + ASSERT(activeFrames.getLength() == 2); + auto releasedFrames = activeFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 2); + for (auto frame : releasedFrames) + delete frame; +} + + +// A transaction frame that is not referenced by an active sequence is removed +// from in-progress ownership and reclaimed immediately, without entering the +// deferred dropped-frame list. +{ + const uint64_t transactionId = 901; + TestPacketQueue pendingQueue; + QosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + auto request = makeShared(); + request->setReceiverAddress(peer1); + request->setSequenceNumber(SequenceNumberCyclic(31)); + auto packet = new Packet("idleAddbaRequest", request); + packet->addTag()->setTransactionId(transactionId); + inProgressFrames.addOwnedFrame(packet); + ackHandler.frameGotInProgress(request); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestHcf hcf; + hcf.configure(&edca); + TestPacketDropSignalListener dropListener; + hcf.subscribe(packetDroppedSignal, &dropListener); + hcf.cancelTransaction(transactionId); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 0); + ASSERT(dropListener.numSignals == 1); + ASSERT(dropListener.numOtherPacketDrops == 1); + hcf.unsubscribe(packetDroppedSignal, &dropListener); +} + +// An unsent transaction has no response deadline and is retired by a typed drop callback. +Packet unsentTrigger("unsentTrigger"); +auto unsentHeader = makeQosHeader(peer2, 5, SequenceNumberCyclic(40)); +handler.processAcknowledgedDataFrame(&unsentTrigger, unsentHeader, &originatorPolicy, &callback); +ASSERT(handler.isAddbaResponsePending(peer2, 5)); + +// Real observed pending queues treat REMOVED as terminal for an unsent tagged +// ADDBA Request, while DEQUEUED is only ownership transfer. The held same-TID +// data frame becomes eligible exactly once after terminal cleanup. +auto verifyObservedPendingRemoval = [&](queueing::IPacketQueue::PacketRemovalReason removalReason, bool removeAll) { + auto observedHandler = new TestOriginatorHandler(); + TestOriginatorQosMacDataService observedDataService; + observedDataService.setFrameEligibilityFunction([observedHandler](const Packet *packet) { + auto header = packet->peekAtFront(); + if (auto addbaReq = dynamicPtrCast(header)) + return observedHandler->isAddbaRequestPending(packet, addbaReq); + auto dataHeader = dynamicPtrCast(header); + return dataHeader == nullptr || !observedHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); + }); + TestPacketQueue observedQueue; + TestAckHandler observedAckHandler; + TestInProgressFrames observedInProgressFrames; + observedInProgressFrames.configure(&observedDataService, &observedAckHandler, &observedQueue); + TestEdcaf observedEdcaf; + observedEdcaf.pendingQueue = &observedQueue; + observedEdcaf.inProgressFrames = &observedInProgressFrames; + TestEdca observedEdca; + observedEdca.edcaf = &observedEdcaf; + TestHcf observedHcf; + observedHcf.configureEligibilityIndex(&observedEdca, &observedDataService); + auto observedRecipientHandler = new RecipientBlockAckAgreementHandler(); + TestRecipientPolicy observedRecipientPolicy; + observedHcf.configureBlockAckHandlers(observedHandler, &originatorPolicy, observedRecipientHandler, &observedRecipientPolicy); + observedHcf.delegateDroppedSetupHandling = true; + observedHcf.observePendingQueue(&observedQueue); + TestCallback observedCallback; + Packet observedTrigger("observedTrigger"); + auto observedHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(60)); + observedHandler->processAcknowledgedDataFrame(&observedTrigger, observedHeader, &originatorPolicy, &observedCallback); + auto observedRequestPacket = observedCallback.managementPacket; + auto observedRequest = dynamicPtrCast(observedCallback.managementHeader); + observedCallback.forgetManagementPacket(observedRequestPacket); + auto heldDataPacket = new Packet("heldSameTidData", makeQosHeader(peer2, 6, SequenceNumberCyclic(61))); + observedQueue.enqueuePacket(observedRequestPacket); + observedQueue.enqueuePacket(heldDataPacket); + observedHcf.trackFrame(observedRequestPacket, AC_BK); + observedHcf.trackFrame(heldDataPacket, AC_BK); + ASSERT(observedHcf.getNumEligiblePendingFrames(AC_BK) == 1); + if (removalReason == queueing::IPacketQueue::PacketRemovalReason::DEQUEUED) { + auto dequeuedPacket = observedQueue.dequeuePacket(); + ASSERT(dequeuedPacket == observedRequestPacket); + ASSERT(observedHandler->isAddbaResponsePending(peer2, 6)); + ASSERT(observedHcf.numResumedEligibleChannelAccess == 0); + observedHandler->processDroppedAddbaReq(dequeuedPacket, observedRequest, &originatorPolicy, &observedCallback); + delete dequeuedPacket; + } + else { + if (removeAll) + observedQueue.removeAllPackets(); + else + observedQueue.removePacket(observedRequestPacket); + ASSERT(observedHandler->getAgreement(peer2, 6) == nullptr); + ASSERT(observedHcf.getNumEligiblePendingFrames(AC_BK) == (removeAll ? 0 : 1)); + ASSERT(observedHcf.numResumedEligibleChannelAccess == 1); + if (!removeAll) + observedQueue.removePacket(heldDataPacket); + delete observedRequestPacket; + } + delete heldDataPacket; +}; +verifyObservedPendingRemoval(queueing::IPacketQueue::PacketRemovalReason::REMOVED, false); +verifyObservedPendingRemoval(queueing::IPacketQueue::PacketRemovalReason::REMOVED, true); +verifyObservedPendingRemoval(queueing::IPacketQueue::PacketRemovalReason::DEQUEUED, false); + +ASSERT(callback.addbaDeadline == SIMTIME_MAX); +auto unsentRequest = dynamicPtrCast(callback.managementHeader); +auto unsentRequestPacket = callback.managementPacket; +auto unsentTransactionId = unsentRequestPacket->getTag()->getTransactionId(); +handler.processDroppedAddbaReq(unsentRequestPacket, unsentRequest, &originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer2, 5) == nullptr); +ASSERT(callback.cancelledTransactionIds.back() == unsentTransactionId); +ASSERT(callback.excludedPackets.back() == unsentRequestPacket); + +// A DELBA that terminates a pending transaction also cancels its tagged ADDBA +// Request and recomputes the response timer. +{ + TestOriginatorHandler receivedDelbaHandler; + TestCallback receivedDelbaCallback; + Packet receivedDelbaTrigger("receivedDelbaTrigger"); + auto receivedDelbaTriggerHeader = makeQosHeader(peer1, 2, SequenceNumberCyclic(60)); + receivedDelbaHandler.processAcknowledgedDataFrame(&receivedDelbaTrigger, receivedDelbaTriggerHeader, &originatorPolicy, &receivedDelbaCallback); + auto receivedDelbaTransactionId = receivedDelbaCallback.managementPacket->getTag()->getTransactionId(); + auto receivedDelba = makeShared(); + receivedDelba->setTransmitterAddress(peer1); + receivedDelba->setTid(2); + auto terminatedAgreement = receivedDelbaHandler.processReceivedDelba(receivedDelba, &originatorPolicy, &receivedDelbaCallback); + ASSERT(terminatedAgreement != nullptr); + if (receivedDelbaHandler.getAgreement(peer1, 2) != nullptr || receivedDelbaCallback.cancelledTransactionIds.size() != 1 || receivedDelbaCallback.cancelledTransactionIds.back() != receivedDelbaTransactionId) + throw cRuntimeError("Received DELBA did not cancel the pending ADDBA transaction"); + ASSERT(receivedDelbaHandler.getAgreement(peer1, 2) == nullptr); + ASSERT(receivedDelbaCallback.cancelledTransactionIds.back() == receivedDelbaTransactionId); + ASSERT(receivedDelbaCallback.excludedPackets.back() == nullptr); + ASSERT(receivedDelbaCallback.addbaDeadline == SIMTIME_MAX); +} +{ + TestOriginatorHandler transmittedDelbaHandler; + TestCallback transmittedDelbaCallback; + Packet transmittedDelbaTrigger("transmittedDelbaTrigger"); + auto transmittedDelbaTriggerHeader = makeQosHeader(peer2, 2, SequenceNumberCyclic(70)); + transmittedDelbaHandler.processAcknowledgedDataFrame(&transmittedDelbaTrigger, transmittedDelbaTriggerHeader, &originatorPolicy, &transmittedDelbaCallback); + auto transmittedDelbaTransactionId = transmittedDelbaCallback.managementPacket->getTag()->getTransactionId(); + auto transmittedDelba = makeShared(); + transmittedDelba->setReceiverAddress(peer2); + transmittedDelba->setTid(2); + Packet transmittedDelbaPacket("transmittedDelba", transmittedDelba); + auto terminatedPendingAgreement = transmittedDelbaHandler.processTransmittedDelba(&transmittedDelbaPacket, &transmittedDelbaCallback); + ASSERT(terminatedPendingAgreement != nullptr); + ASSERT(terminatedPendingAgreement->isPending()); + if (transmittedDelbaHandler.getAgreement(peer2, 2) != nullptr || transmittedDelbaCallback.cancelledTransactionIds.size() != 1 || transmittedDelbaCallback.cancelledTransactionIds.back() != transmittedDelbaTransactionId) + throw cRuntimeError("Transmitted DELBA did not cancel the pending ADDBA transaction"); + ASSERT(transmittedDelbaHandler.getAgreement(peer2, 2) == nullptr); + ASSERT(transmittedDelbaCallback.cancelledTransactionIds.back() == transmittedDelbaTransactionId); + ASSERT(transmittedDelbaCallback.excludedPackets.back() == nullptr); + ASSERT(transmittedDelbaCallback.addbaDeadline == SIMTIME_MAX); +} + +// Fragmentation preserves the local transaction identity on every MPDU, and +// typed cancellation removes all exact siblings while retaining the dropped +// fragment and an unrelated transaction. +{ + TestOriginatorHandler fragmentedHandler; + TestCallback fragmentedCallback; + Packet fragmentedTrigger("fragmentedTrigger"); + auto fragmentedTriggerHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(50)); + fragmentedHandler.processAcknowledgedDataFrame(&fragmentedTrigger, fragmentedTriggerHeader, &originatorPolicy, &fragmentedCallback); + auto unfragmentedRequestPacket = fragmentedCallback.managementPacket; + auto fragmentedTransactionId = unfragmentedRequestPacket->getTag()->getTransactionId(); + fragmentedCallback.forgetManagementPacket(unfragmentedRequestPacket); + unfragmentedRequestPacket->insertAtBack(makeShared(B(4))); + unfragmentedRequestPacket->insertAtBack(makeShared()); + TestFragmentationPolicy fragmentationPolicy; + TestOriginatorQosMacDataService fragmentationDataService; + fragmentationDataService.enableFragmentation(&fragmentationPolicy); + auto fragments = fragmentationDataService.fragment(unfragmentedRequestPacket); + ASSERT(fragments != nullptr); + ASSERT(fragments->size() == 2); + for (auto fragment : *fragments) + ASSERT(fragment->getTag()->getTransactionId() == fragmentedTransactionId); + auto unrelatedPacket = new Packet("unrelatedTransaction", makeShared(B(1))); + unrelatedPacket->addTag()->setTransactionId(fragmentedTransactionId + 1); + auto droppedFragment = fragments->at(0); + auto siblingFragment = fragments->at(1); + TestPacketQueue fragmentedPendingQueue; + fragmentedPendingQueue.packets = { droppedFragment, siblingFragment, unrelatedPacket }; + TestInProgressFrames fragmentedInProgressFrames; + TestEdcaf fragmentedEdcaf; + fragmentedEdcaf.pendingQueue = &fragmentedPendingQueue; + fragmentedEdcaf.inProgressFrames = &fragmentedInProgressFrames; + TestEdca fragmentedEdca; + fragmentedEdca.edcaf = &fragmentedEdcaf; + TestHcf fragmentedHcf; + fragmentedHcf.configure(&fragmentedEdca); + auto droppedRequest = droppedFragment->peekAtFront(); + fragmentedHandler.processDroppedAddbaReq(droppedFragment, droppedRequest, &originatorPolicy, &fragmentedHcf); + ASSERT(fragmentedHandler.getAgreement(peer1, 7) == nullptr); + ASSERT(fragmentedPendingQueue.getNumPackets() == 2); + ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), droppedFragment) != fragmentedPendingQueue.packets.end()); + ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), siblingFragment) == fragmentedPendingQueue.packets.end()); + ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), unrelatedPacket) != fragmentedPendingQueue.packets.end()); + fragmentedPendingQueue.removePacket(droppedFragment); + fragmentedPendingQueue.removePacket(unrelatedPacket); + delete droppedFragment; + delete fragments; + delete unrelatedPacket; +} + +{ + TestOriginatorHandler invalidTimeoutHandler; + TestOriginatorPolicy invalidTimeoutPolicy; + TestCallback invalidTimeoutCallback; + invalidTimeoutPolicy.addbaResponseTimeout = 0; + Packet invalidTimeoutTrigger("invalidTimeoutTrigger"); + auto invalidTimeoutHeader = makeQosHeader(peer2, 1, SequenceNumberCyclic(30)); + bool threw = false; + try { + invalidTimeoutHandler.processAcknowledgedDataFrame(&invalidTimeoutTrigger, invalidTimeoutHeader, &invalidTimeoutPolicy, &invalidTimeoutCallback); + auto invalidRequest = dynamicPtrCast(invalidTimeoutCallback.managementHeader); + invalidTimeoutHandler.processTransmittedAddbaReq(invalidTimeoutCallback.managementPacket, invalidRequest, &invalidTimeoutPolicy, &invalidTimeoutCallback); + } + catch (cRuntimeError&) { + threw = true; + } + ASSERT(threw); +} + +// A direct IPacketQueue implementation without OMNeT++ signals cannot leave +// the index stale because ownership departures use the typed queue callback. +{ + TestOriginatorQosMacDataService customQueueDataService; + TestPacketQueue customQueue; + TestAckHandler customQueueAckHandler; + TestInProgressFrames customQueueInProgressFrames; + customQueueInProgressFrames.configure(&customQueueDataService, &customQueueAckHandler, &customQueue); + TestEdcaf customQueueEdcaf; + customQueueEdcaf.pendingQueue = &customQueue; + customQueueEdcaf.inProgressFrames = &customQueueInProgressFrames; + TestEdca customQueueEdca; + customQueueEdca.edcaf = &customQueueEdcaf; + TestHcf customQueueHcf; + customQueueHcf.configureEligibilityIndex(&customQueueEdca, &customQueueDataService); + customQueueHcf.observePendingQueue(&customQueue); + customQueueHcf.rebuildEligibilityIndex(); + auto customPacket = new Packet("customQueuePacket", makeShared(B(1))); + customQueueHcf.trackFrame(customPacket, AC_BK); + customQueue.enqueuePacket(customPacket); + if (customQueueHcf.getNumEligiblePendingFrames(AC_BK) != 1) + throw cRuntimeError("Custom queue enqueue was not indexed"); + auto customDequeuedPacket = customQueue.dequeuePacket(); + if (customDequeuedPacket != customPacket || customQueueHcf.getNumTrackedPendingFrames() != 0) + throw cRuntimeError("Custom queue departure callback left the index stale"); + delete customDequeuedPacket; +} + +// Top-level queue lifecycle signals maintain the same index incrementally; +// destructive drops are distinguished from ordinary departures. +{ + TestOriginatorQosMacDataService indexedDataService; + indexedDataService.setFrameEligibilityFunction([](const Packet *) { return true; }); + QosAckHandler indexedAckHandler; + TestInProgressFrames indexedInProgressFrames; + indexedInProgressFrames.configure(&indexedDataService, &indexedAckHandler); + TestEdcaf indexedEdcaf; + TestEdca indexedEdca; + indexedEdca.edcaf = &indexedEdcaf; + indexedEdcaf.inProgressFrames = &indexedInProgressFrames; + auto overflowQueue = createPacketQueue("indexedOverflowQueue", 1, "inet::queueing::PacketAtCollectionEndDropper"); + indexedEdcaf.pendingQueue = overflowQueue; + TestHcf indexedHcf; + indexedHcf.configureEligibilityIndex(&indexedEdca, &indexedDataService); + indexedHcf.observePendingQueue(overflowQueue); + indexedHcf.rebuildEligibilityIndex(); + auto retainedPacket = new Packet("indexedRetainedPacket", makeShared(B(1))); + indexedHcf.trackFrame(retainedPacket, AC_BK); + overflowQueue->pushPacket(retainedPacket, nullptr); + auto droppedArrival = new Packet("indexedDroppedArrival", makeShared(B(1))); + indexedHcf.trackFrame(droppedArrival, AC_BK); + overflowQueue->pushPacket(droppedArrival, nullptr); + if (indexedHcf.getNumTrackedPendingFrames() != 1 || indexedHcf.getNumEligiblePendingFrames(AC_BK) != 1) + throw cRuntimeError("Eligibility index did not account for overflow"); + auto selectedPacket = overflowQueue->dequeuePacket([](const Packet *) { return true; }); + if (selectedPacket != retainedPacket || indexedHcf.getNumTrackedPendingFrames() != 0) + throw cRuntimeError("Eligibility index did not account for selected dequeue"); + bool hasFrameAfterSelectedDequeue = indexedHcf.hasFrame(AC_BK); + if (hasFrameAfterSelectedDequeue) + throw cRuntimeError("Eligibility index reports a frame after selected dequeue"); + take(selectedPacket); + delete selectedPacket; + overflowQueue->callFinish(); + overflowQueue->deleteModule(); +} + // Predicate extraction follows each provider's scheduling policy instead of // the compound collection's gate-order enumeration. { @@ -478,6 +1704,367 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const gateModule->deleteModule(); leafQueue->deleteModule(); } + +// Every A-MSDU member is extracted through the scheduler, so provider +// accounting runs for all policy-selected subframes. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto firstSubframe = makeAggregatePacket("schedulerAggregateFirst", SequenceNumberCyclic(10)); + auto secondSubframe = makeAggregatePacket("schedulerAggregateSecond", SequenceNumberCyclic(11)); + firstQueue.enqueuePacket(firstSubframe); + secondQueue.enqueuePacket(secondSubframe); + TestPriorityScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(&scheduler, &scheduler); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { firstSubframe, secondSubframe }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + auto frames = service.extractFramesToTransmit(&compoundQueue); + if (frames == nullptr || frames->size() != 1 || scheduler.getNumProcessedPackets() != 2 || !firstQueue.isEmpty() || !secondQueue.isEmpty()) + throw cRuntimeError("A-MSDU extraction bypassed scheduler processing"); + service.release(frames->front()); + delete frames->front(); + delete frames; +} + +// A-MSDU validation discovers selected members by identity without enumerating +// an unrelated trailing backlog through the packet collection API. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue queue; + auto firstSubframe = makeAggregatePacket("backlogAggregateFirst", SequenceNumberCyclic(14)); + auto secondSubframe = makeAggregatePacket("backlogAggregateSecond", SequenceNumberCyclic(15)); + queue.packets = { firstSubframe, secondSubframe }; + for (int i = 0; i < 128; i++) + queue.packets.push_back(makeAggregatePacket("backlogAggregateTail", SequenceNumberCyclic(16 + i))); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { firstSubframe, secondSubframe }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + auto frames = service.extractFramesToTransmit(&queue); + ASSERT(frames != nullptr); + ASSERT(frames->size() == 1); + ASSERT(frames->front()->peekAtFront()->getAMsduPresent()); + ASSERT(queue.getNumPackets() == 128); + ASSERT(queue.numGetPacketCalls == 0); + service.release(frames->front()); + delete frames->front(); + delete frames; + for (auto packet : queue.packets) + delete packet; + queue.packets.clear(); +} + +// The built-in policy is anchored on the provider-selected candidate. Held +// enumeration members do not hide later compatible frames, while an oversized +// anchor falls back to ordinary dequeue without aggregating its followers. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber, int payloadLength) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(payloadLength))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue queue; + auto anchor = makeAggregatePacket("basicPolicyAnchor", SequenceNumberCyclic(20), 8); + auto held = makeAggregatePacket("basicPolicyHeld", SequenceNumberCyclic(21), 8); + auto compatible = makeAggregatePacket("basicPolicyCompatible", SequenceNumberCyclic(22), 8); + auto heldHeader = held->removeAtFront(); + heldHeader->setTid(2); + held->insertAtFront(heldHeader); + queue.packets = { anchor, held, compatible }; + TestBasicMsduAggregationPolicy policy; + policy.configure(B(4065)); + auto selected = policy.computeAggregateFrames(&queue, anchor, [held](const Packet *packet) { return packet != held; }); + ASSERT(selected != nullptr); + ASSERT(selected->size() == 2); + ASSERT(selected->at(0) == anchor); + ASSERT(selected->at(1) == compatible); + delete selected; + auto oversizedAnchor = makeAggregatePacket("basicPolicyOversizedAnchor", SequenceNumberCyclic(23), 64); + auto smallFollower1 = makeAggregatePacket("basicPolicySmallFollower1", SequenceNumberCyclic(24), 1); + auto smallFollower2 = makeAggregatePacket("basicPolicySmallFollower2", SequenceNumberCyclic(25), 1); + queue.packets = { oversizedAnchor, smallFollower1, smallFollower2 }; + policy.configure(B(30)); + ASSERT(policy.computeAggregateFrames(&queue, oversizedAnchor, [](const Packet *) { return true; }) == nullptr); + auto prefix = makeAggregatePacket("basicPolicyPrefix", SequenceNumberCyclic(26), 8); + auto middleAnchor = makeAggregatePacket("basicPolicyMiddleAnchor", SequenceNumberCyclic(27), 8); + auto suffix = makeAggregatePacket("basicPolicySuffix", SequenceNumberCyclic(28), 8); + queue.packets = { prefix, middleAnchor, suffix }; + policy.configure(B(4065)); + selected = policy.computeAggregateFrames(&queue, middleAnchor, [prefix](const Packet *packet) { return packet != prefix; }); + ASSERT(selected != nullptr); + ASSERT(selected->size() == 2); + ASSERT(selected->at(0) == middleAnchor); + ASSERT(selected->at(1) == suffix); + delete selected; + auto capacityAnchor = makeAggregatePacket("basicPolicyCapacityAnchor", SequenceNumberCyclic(29), 8); + auto oversizedMiddle = makeAggregatePacket("basicPolicyOversizedMiddle", SequenceNumberCyclic(30), 64); + auto smallSuffix = makeAggregatePacket("basicPolicySmallSuffix", SequenceNumberCyclic(31), 1); + queue.packets = { capacityAnchor, oversizedMiddle, smallSuffix }; + policy.configure(B(40)); + ASSERT(policy.computeAggregateFrames(&queue, capacityAnchor, [](const Packet *) { return true; }) == nullptr); + queue.packets.clear(); + for (auto packet : { anchor, held, compatible, oversizedAnchor, smallFollower1, smallFollower2, prefix, middleAnchor, suffix, capacityAnchor, oversizedMiddle, smallSuffix }) + delete packet; +} + +// Contract validation owns both already-extracted frames and an unexpected +// non-null provider result, so a caught provider error leaves only the frame +// which was never extracted in the queue. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestWrongDequeueQueue queue; + auto first = makeAggregatePacket("badProviderFirst", SequenceNumberCyclic(35)); + auto second = makeAggregatePacket("badProviderSecond", SequenceNumberCyclic(36)); + auto wrong = makeAggregatePacket("badProviderWrong", SequenceNumberCyclic(37)); + queue.packets = { first, second, wrong }; + queue.wrongPacket = wrong; + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { first, second }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + bool contractError = false; + try { + service.extractFramesToTransmit(&queue); + } + catch (cRuntimeError& error) { + contractError = std::string(error.what()).find("no longer available") != std::string::npos; + } + ASSERT(contractError); + ASSERT(queue.packets.size() == 1); + ASSERT(queue.packets.front() == second); + queue.packets.clear(); + delete second; +} + +// Returning an already-extracted pointer for a later exact dequeue is also a +// provider contract violation, but the cleanup guard must adopt it only once. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestRepeatedDequeueQueue queue; + auto first = makeAggregatePacket("repeatedProviderFirst", SequenceNumberCyclic(38)); + auto second = makeAggregatePacket("repeatedProviderSecond", SequenceNumberCyclic(39)); + queue.packets = { first, second }; + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { first, second }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + bool contractError = false; + try { + service.extractFramesToTransmit(&queue); + } + catch (cRuntimeError& error) { + contractError = std::string(error.what()).find("no longer available") != std::string::npos; + } + ASSERT(contractError); + ASSERT(queue.packets.size() == 1); + ASSERT(queue.packets.front() == second); + queue.packets.clear(); + delete second; +} + +// Reverse-priority scheduling may select an anchor after an eligible packet in +// collection enumeration. The built-in policy must never wrap and append that +// earlier packet after the anchor. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto earlierPacket = makeAggregatePacket("reverseEarlier", SequenceNumberCyclic(32)); + auto reverseAnchor = makeAggregatePacket("reverseAnchor", SequenceNumberCyclic(33)); + auto reverseSuffix = makeAggregatePacket("reverseSuffix", SequenceNumberCyclic(34)); + firstQueue.packets = { earlierPacket }; + secondQueue.packets = { reverseAnchor, reverseSuffix }; + TestPriorityScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, true); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(&scheduler, &scheduler); + auto candidate = compoundQueue.findPacket([](const Packet *) { return true; }); + ASSERT(candidate == reverseAnchor); + TestBasicMsduAggregationPolicy policy; + policy.configure(B(4065)); + auto selected = policy.computeAggregateFrames(&compoundQueue, candidate, [](const Packet *) { return true; }); + ASSERT(selected != nullptr); + ASSERT(selected->size() == 2); + ASSERT(selected->at(0) == reverseAnchor); + ASSERT(selected->at(1) == reverseSuffix); + delete selected; + firstQueue.packets.clear(); + secondQueue.packets.clear(); + delete earlierPacket; + delete reverseAnchor; + delete reverseSuffix; +} + +// A flow provider may expose an enumeration order different from its +// scheduling behavior. Candidate-aware aggregation still extracts every +// selected member through the provider and runs its processing exactly once. +{ + auto leafQueue = createPacketQueue("aggregationFlowLeaf"); + auto flowModule = cModuleType::get("inet.queueing.common.BackPressureBarrier")->create("aggregationFlow", this); + leafQueue->gate("out")->connectTo(flowModule->gate("in")); + flowModule->callInitialize(); + TestSignalListener flowListener; + flowModule->subscribe(packetPulledOutSignal, &flowListener); + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto firstSubframe = makeAggregatePacket("flowAggregateFirst", SequenceNumberCyclic(12)); + auto secondSubframe = makeAggregatePacket("flowAggregateSecond", SequenceNumberCyclic(13)); + leafQueue->pushPacket(firstSubframe, nullptr); + leafQueue->pushPacket(secondSubframe, nullptr); + auto flowCollection = check_and_cast(flowModule); + auto flowExtractor = check_and_cast(flowModule); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(flowCollection, flowExtractor); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { firstSubframe, secondSubframe }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + auto frames = service.extractFramesToTransmit(&compoundQueue); + if (frames == nullptr || frames->size() != 1 || !frames->front()->peekAtFront()->getAMsduPresent() || aggregationPolicy.numCalls != 1 || flowListener.numSignals != 2 || leafQueue->getNumPackets() != 0) + throw cRuntimeError("A-MSDU aggregation bypassed PacketFlowBase extraction"); + service.release(frames->front()); + delete frames->front(); + delete frames; + flowModule->unsubscribe(packetPulledOutSignal, &flowListener); + flowModule->callFinish(); + flowModule->deleteModule(); + leafQueue->callFinish(); + leafQueue->deleteModule(); +} + +// End-to-end compound extraction uses the provider's reverse-priority choice +// as the aggregate anchor while leaving an ineligible higher-level frame held. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto heldPacket = makeAggregatePacket("compoundHeldPacket", SequenceNumberCyclic(1)); + auto selectedPacket = makeAggregatePacket("compoundSelectedPacket", SequenceNumberCyclic(2)); + auto companionPacket = makeAggregatePacket("compoundCompanionPacket", SequenceNumberCyclic(3)); + firstQueue.packets = { heldPacket }; + secondQueue.packets = { selectedPacket, companionPacket }; + TestPriorityScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, true); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(&scheduler, &scheduler); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { selectedPacket, companionPacket }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + service.setFrameEligibilityFunction([heldPacket](const Packet *packet) { return packet != heldPacket; }); + auto frames = service.extractFramesToTransmit(&compoundQueue); + ASSERT(frames != nullptr); + ASSERT(frames->size() == 1); + ASSERT(frames->front()->peekAtFront()->getAMsduPresent()); + ASSERT(aggregationPolicy.numCalls == 1); + ASSERT(firstQueue.getNumPackets() == 1); + ASSERT(secondQueue.isEmpty()); + service.release(frames->front()); + delete frames->front(); + delete frames; + firstQueue.removePacket(heldPacket); + delete heldPacket; +} + +// A-MSDU aggregation may select multiple WRR members, but each exact dequeue +// must still update WRR accounting. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto selectedPacket = makeAggregatePacket("wrrAggregateSelectedPacket", SequenceNumberCyclic(3)); + auto sameInputPacket = makeAggregatePacket("wrrAggregateSameInputPacket", SequenceNumberCyclic(4)); + auto otherInputPacket = makeAggregatePacket("wrrAggregateOtherInputPacket", SequenceNumberCyclic(5)); + firstQueue.packets = { selectedPacket, sameInputPacket }; + secondQueue.packets = { otherInputPacket }; + TestWrrScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, { 1, 1 }, { 1, 1 }); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(&scheduler, &scheduler); + ASSERT(compoundQueue.getPacket(0) == selectedPacket); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { selectedPacket, sameInputPacket }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + auto frames = service.extractFramesToTransmit(&compoundQueue); + ASSERT(frames != nullptr); + ASSERT(frames->size() == 1); + ASSERT(frames->front()->peekAtFront()->getAMsduPresent()); + ASSERT(aggregationPolicy.numCalls == 1); + ASSERT(firstQueue.isEmpty()); + ASSERT(secondQueue.getNumPackets() == 1); + ASSERT(scheduler.getNumProcessedPackets() == 2); + ASSERT(scheduler.getBucket(0) == 0); + ASSERT(scheduler.getBucket(1) == 1); + service.release(frames->front()); + delete frames->front(); + delete frames; + secondQueue.removePacket(otherInputPacket); + delete otherInputPacket; +} // A real queue with a downstream collector predicate-dequeues a non-front // packet, records its residence and semantic pull once, and animates it to the // collector while retaining the front packet. @@ -596,6 +2183,27 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const gateModule->deleteModule(); leafQueueModule->deleteModule(); } + +// Ordinary queue overflow must not trigger channel-access re-evaluation, while +// cancelling a dropped setup transaction must immediately release held traffic. +{ + TestHcf droppedSetupHcf; + Packet droppedSetupPacket("droppedSetupPacket", makeShared(B(1))); + droppedSetupHcf.dropPacket(&droppedSetupPacket); + ASSERT(droppedSetupHcf.numProcessedDroppedSetupFrames == 1); + ASSERT(droppedSetupHcf.numResumedEligibleChannelAccess == 0); + droppedSetupHcf.droppedSetupCancelled = true; + droppedSetupHcf.dropPacket(&droppedSetupPacket); + ASSERT(droppedSetupHcf.numProcessedDroppedSetupFrames == 2); + ASSERT(droppedSetupHcf.numResumedEligibleChannelAccess == 1); + droppedSetupHcf.removePacket(&droppedSetupPacket); + ASSERT(droppedSetupHcf.numProcessedDroppedSetupFrames == 3); + ASSERT(droppedSetupHcf.numResumedEligibleChannelAccess == 2); + droppedSetupHcf.dequeuePacket(&droppedSetupPacket); + ASSERT(droppedSetupHcf.numProcessedDroppedSetupFrames == 3); + ASSERT(droppedSetupHcf.numResumedEligibleChannelAccess == 2); +} + // The typed queue callback covers every non-destructive ownership departure, // including bulk removal, exactly once at the queue boundary. { @@ -943,5 +2551,6 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const bufferModule->deleteModule(); } EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; + %contains: stdout ADDBA transaction and negotiated SSN integrity checks passed. From 96773078e6126a45122775b7148ca5932f08a1ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:03 +0200 Subject: [PATCH 04/16] ieee80211: reset recipient Block Ack state on renegotiation Treat an accepted ADDBA request for an existing peer and TID as a replacement agreement. Cancel teardown state belonging to the old agreement, install the negotiated parameters, and reset the receive reordering window before frames are admitted under the new agreement. Replay the cached response for a duplicate request without resetting receive state, and preserve the current agreement when renegotiation is rejected. Emit distinct agreement-added, changed, and deleted events so the HCF lifecycle remains observable. Add recipient lifecycle coverage together with a transactional QoS example and its focused fingerprint entry. --- WHATSNEW | 44 +- examples/wireless/qos/omnetpp.ini | 23 + .../mac/blockack/RecipientBlockAckAgreement.h | 3 - .../RecipientBlockAckAgreementHandler.cc | 118 +- .../RecipientBlockAckAgreementHandler.h | 16 +- .../blockackreordering/BlockAckReordering.cc | 10 +- .../blockackreordering/BlockAckReordering.h | 3 +- .../mac/blockackreordering/ReceiveBuffer.cc | 11 +- .../mac/blockackreordering/ReceiveBuffer.h | 2 +- .../IRecipientBlockAckAgreementHandler.h | 11 +- .../contract/IRecipientQosMacDataService.h | 12 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 54 +- .../ieee80211/mac/coordinationfunction/Hcf.h | 8 +- .../mac/coordinationfunction/Hcf.ned | 2 + .../recipient/RecipientQosMacDataService.cc | 30 +- .../recipient/RecipientQosMacDataService.h | 4 +- tests/fingerprint/examples.csv | 11 +- tests/unit/Ieee80211AddbaTransaction_1.test | 1417 ++++++++++++++++- 18 files changed, 1653 insertions(+), 126 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 10210553c8d..c84aeb708da 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -43,15 +43,20 @@ Notable backward incompatible changes are the following: implementations of IPacketExtractor must replace selected-pointer extraction with findPacket(predicate) and dequeuePacket(predicate); predicates may be evaluated repeatedly and must be stable and side-effect free throughout one - logical selection. WrrScheduler, - LabelScheduler, and PriorityScheduler accept ordinary IPassivePacketSource - inputs; they require IPacketCollection only when collection/aggregate access is - used and IPacketExtractor only when predicate extraction is used, reporting the - unsupported operation lazily. + logical selection. WrrScheduler, LabelScheduler, and PriorityScheduler accept + ordinary IPassivePacketSource inputs; they require IPacketCollection only when + collection/aggregate access is used and IPacketExtractor only when predicate + extraction is used, reporting the unsupported operation lazily. PriorityScheduler aggregate queries no longer return -1 when an input lacks IPacketCollection; getNumPackets() and getTotalLength() now report that unsupported operation with cRuntimeError. Update callers that treated -1 as an unknown aggregate size, or connect collection-capable providers. + A-MSDU policies now receive the provider-selected anchor and frame-eligibility + predicate. Additional policy-selected members are removed through exact + predicate dequeues, preserving scheduler/flow accounting without requiring + collection enumeration order to match scheduling order. + BasicMsduAggregationPolicy conservatively considers only collection members + after the anchor and never bypasses an earlier blocked same-flow member. IPacketBuffer::ICallback also provides an optional handlePacketDropped() notification. Shared buffers invoke it only after all victims selected by one overload operation have been detached from their owning queues. Compound @@ -59,6 +64,35 @@ Notable backward incompatible changes are the following: nested compound queues exactly once. PacketBuffer rejects packets owned by a cPacketQueue whose owner cannot participate in the buffer callback contract. +3. Block Ack DELBA agreement ownership + + IOriginatorBlockAckAgreementHandler::processReceivedDelba(), + IOriginatorBlockAckAgreementHandler::processTransmittedDelba(), and + IRecipientBlockAckAgreementHandler::processReceivedDelba() now return the + terminated agreement as a unique_ptr. Originator ADDBA response processing + now returns a typed outcome containing the established agreement and, when + local policy vetoes a successful response, its immediately terminated local + agreement and a best-effort initiator DELBA for Hcf to enqueue after emitting + the Added and Deleted signals. Such DELBAs carry the ADDBA transaction identity + and ordinary data continues with Normal Ack. They remain eligible through the + final fragment; aborting one fragment cancels its siblings. After retry backoff, + a replacement ADDBA setup invalidates an older queued DELBA, so an unreported + disposal cannot suppress setup indefinitely and a delayed frame cannot + terminate a newer peer/TID agreement. The replacement setup reports that + obsolete identity so Hcf also removes all of its queued or in-progress packets. + Originator DELBA transmission handlers now receive the full Packet so this + identity is retained. A transaction-tagged initiator DELBA remains eligible + across MAC retries and is retired only when its final fragment is acknowledged + or the transaction is terminally aborted. Custom handler implementations and + callers must adopt processAcknowledgedDelba() and the boolean + processAbortedDelba() outcome; processTransmittedDelba() still returns the + agreement removed by an untagged DELBA, or null when none was removed. + + OriginatorBlockAckAgreementPolicy now exposes the new `addbaResponseTimeout` + and `addbaRetryBackoff` NED parameters. C++ implementations of + IOriginatorBlockAckAgreementPolicy must replace computeAddbaFailureTimeout() + with getAddbaResponseTimeout() and implement computeAddbaRetryBackoff(). + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/examples/wireless/qos/omnetpp.ini b/examples/wireless/qos/omnetpp.ini index 887e0683fc6..dccf94b1347 100644 --- a/examples/wireless/qos/omnetpp.ini +++ b/examples/wireless/qos/omnetpp.ini @@ -85,3 +85,26 @@ extends = MacQos # radio medium *.radioMedium.sameTransmissionStartTimeCheck = "ignore" + +[Config MacQosWithTransactionalBlockAck] +description = "Exercises successful and timed-out ADDBA transactions" +extends = MacQosWithoutAggregation +sim-time-limit = 3s + +# Use one voice flow so each wireless hop has a single peer/TID data flow. +*.cliHost.numApps = 1 +*.cliHost.app[0].destPort = 5000 +*.cliHost.app[0].packetName = "TransactionalBlockAck" +*.cliHost.app[0].startTime = 1s +*.cliHost.app[0].stopTime = 2s +*.cliHost.app[0].sendInterval = 10ms +*.srvHost.numApps = 1 +*.srvHost.app[0].localPort = 5000 + +# The client-to-AP transaction succeeds. The AP-to-server transaction times +# out, because the server deliberately does not support Block Ack. +*.cliHost.wlan[0].mac.hcf.isBlockAckSupported = true +*.ap.wlan[0].mac.hcf.isBlockAckSupported = true +*.srvHost.wlan[0].mac.hcf.isBlockAckSupported = false +**.mac.hcf.originatorAckPolicy.blockAckReqThreshold = 2 +**.mac.hcf.originatorBlockAckAgreementPolicy.addbaResponseTimeout = 250ms diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 40a4186fead..98f9ba589c4 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -21,7 +21,6 @@ class INET_API RecipientBlockAckAgreement : public cObject SequenceNumberCyclic startingSequenceNumber; int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; - bool isAddbaResponseSent = false; simtime_t expirationTime = -1; public: @@ -35,7 +34,6 @@ class INET_API RecipientBlockAckAgreement : public cObject virtual int getBufferSize() const { return bufferSize; } virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } - virtual void addbaResposneSent() { isAddbaResponseSent = true; } virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } virtual simtime_t getExpirationTime() { return expirationTime; } friend std::ostream& operator<<(std::ostream& os, const RecipientBlockAckAgreement& agreement); @@ -45,4 +43,3 @@ class INET_API RecipientBlockAckAgreement : public cObject } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 6219ac77140..8956059d80c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -64,29 +64,6 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb scheduleInactivityTimer(agreementHandlerCallback); } -// -// An originator that intends to use the Block Ack mechanism for the transmission of QoS data frames to an -// intended recipient should first check whether the intended recipient STA is capable of participating in Block -// Ack mechanism by discovering and examining its Delayed Block Ack and Immediate Block Ack capability -// bits. If the intended recipient STA is capable of participating, the originator sends an ADDBA Request frame -// indicating the TID for which the Block Ack is being set up. -// -RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::addAgreement(const Ptr& addbaReq) -{ - MacAddress originatorAddr = addbaReq->getTransmitterAddress(); - auto id = std::make_pair(originatorAddr, addbaReq->getTid()); - auto it = blockAckAgreements.find(id); - if (it == blockAckAgreements.end()) { - RecipientBlockAckAgreement *agreement = new RecipientBlockAckAgreement(originatorAddr, addbaReq->getTid(), addbaReq->getStartingSequenceNumber(), addbaReq->getBufferSize(), addbaReq->getBlockAckTimeoutValue()); - blockAckAgreements[id] = agreement; - EV_DETAIL << "Block Ack Agreement is added with the following parameters: " << *agreement << endl; - return agreement; - } - else - // TODO update? - return it->second; -} - // // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the peer STA with the Reason Code // field set to TIMEOUT and shall issue a MLME-DELBA.indication primitive with the ReasonCode @@ -102,10 +79,14 @@ const Ptr RecipientBlockAckAgreementHandler::buildDelba(MacAddre return delba; } -const Ptr RecipientBlockAckAgreementHandler::buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) +const Ptr RecipientBlockAckAgreementHandler::buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, bool accepted) { auto addbaResponse = makeShared(); addbaResponse->setReceiverAddress(addbaRequest->getTransmitterAddress()); + // IEEE Std 802.11-2024, 9.6.4.2 and 11.5.2.3: the response copies the + // request's Dialog Token and reports whether the agreement was accepted. + addbaResponse->setDialogToken(addbaRequest->getDialogToken()); + addbaResponse->setStatusCode(accepted ? 0 : 1); // 1: REFUSED_REASON_UNSPECIFIED // The Block Ack Policy subfield is set to 1 for immediate Block Ack and 0 for delayed Block Ack. Tid tid = addbaRequest->getTid(); addbaResponse->setTid(tid); @@ -116,27 +97,17 @@ const Ptr RecipientBlockAckAgreementHandler::buildAddbaR return addbaResponse; } -void RecipientBlockAckAgreementHandler::updateAgreement(const Ptr& addbaResponse) -{ - auto id = std::make_pair(addbaResponse->getReceiverAddress(), addbaResponse->getTid()); - auto it = blockAckAgreements.find(id); - if (it != blockAckAgreements.end()) { - RecipientBlockAckAgreement *agreement = it->second; - agreement->addbaResposneSent(); - } - else - throw cRuntimeError("Agreement is not found"); -} - -void RecipientBlockAckAgreementHandler::terminateAgreement(MacAddress originatorAddr, Tid tid) +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::removeAgreement(MacAddress originatorAddr, Tid tid) { auto agreementId = std::make_pair(originatorAddr, tid); + lastAddbaResponses.erase(agreementId); auto it = blockAckAgreements.find(agreementId); if (it != blockAckAgreements.end()) { - RecipientBlockAckAgreement *agreement = it->second; + auto agreement = it->second; blockAckAgreements.erase(it); - delete agreement; + return agreement; } + return nullptr; } RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getAgreement(Tid tid, MacAddress originatorAddr) @@ -146,35 +117,69 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getAgreement(Tid return it != blockAckAgreements.end() ? it->second : nullptr; } -void RecipientBlockAckAgreementHandler::processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { - updateAgreement(addbaResp); - scheduleInactivityTimer(callback); + EV_INFO << "Processing Addba Request from " << addbaRequest->getTransmitterAddress() << endl; + bool accepted = addbaRequest->getDialogToken() != 0 && blockAckAgreementPolicy->isAddbaReqAccepted(addbaRequest); + EV_DETAIL << "Building Addba Response" << endl; + auto addbaResponse = buildAddbaResponse(addbaRequest, blockAckAgreementPolicy, accepted); + auto id = std::make_pair(addbaRequest->getTransmitterAddress(), addbaRequest->getTid()); + bool hadAgreement = blockAckAgreements.find(id) != blockAckAgreements.end(); + // Keep the immutable response body that corresponds to the most recently + // processed request identity. This is response replay state, not a second + // duplicate detector; RecipientQosMacDataService remains authoritative. + if (accepted || hadAgreement) + lastAddbaResponses[id] = addbaResponse; + else + lastAddbaResponses.erase(id); + auto addbaResponsePacket = new Packet("AddbaResponse", addbaResponse); + RecipientBlockAckAgreement *agreement = nullptr; + if (accepted) { + // IEEE Std 802.11-2024, 10.25.2 and 11.5.2.3: accepting the + // request establishes or modifies the recipient agreement when the + // successful response is formed; transmission is not a state gate. + agreement = new RecipientBlockAckAgreement(addbaRequest->getTransmitterAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaResponse->getBufferSize(), addbaResponse->getBlockAckTimeoutValue()); + auto it = blockAckAgreements.find(id); + if (it != blockAckAgreements.end()) { + delete it->second; + it->second = agreement; + } + else + blockAckAgreements[id] = agreement; + scheduleInactivityTimer(agreementHandlerCallback); + } + procedureCallback->processMgmtFrame(addbaResponsePacket, addbaResponse); + return agreement; } -void RecipientBlockAckAgreementHandler::processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) +void RecipientBlockAckAgreementHandler::processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) { - EV_INFO << "Processing Addba Request from " << addbaRequest->getTransmitterAddress() << endl; - if (blockAckAgreementPolicy->isAddbaReqAccepted(addbaRequest)) { - EV_DETAIL << "Addba Request has been accepted. Creating a new Block Ack Agreement." << endl; - auto agreement = addAgreement(addbaRequest); - EV_DETAIL << "Agreement is added with the following parameters: " << *agreement << endl; - EV_DETAIL << "Building Addba Response" << endl; - auto addbaResponse = buildAddbaResponse(addbaRequest, blockAckAgreementPolicy); - auto addbaResponsePacket = new Packet("AddbaResponse", addbaResponse); - callback->processMgmtFrame(addbaResponsePacket, addbaResponse); + auto agreement = getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()); + if (agreement != nullptr) { + // IEEE Std 802.11-2024, 10.3.2.14.3 normally discards duplicate management bodies. + // Replaying the already generated response is an explicit robustness/model + // extension; it does not modify the agreement, reorder window, or inactivity timer. + auto id = std::make_pair(addbaRequest->getTransmitterAddress(), addbaRequest->getTid()); + auto it = lastAddbaResponses.find(id); + if (it == lastAddbaResponses.end()) + return; + // Copy the immutable snapshot so outbound sequence assignment uses COW + // and cannot modify the cached body used by a later retransmission. + auto addbaResponse = staticPtrCast(it->second->dupShared()); + procedureCallback->processMgmtFrame(new Packet("AddbaResponse", addbaResponse), addbaResponse); } } -void RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) +std::unique_ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) { - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + return std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); } -void RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) +std::unique_ptr RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) { if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + return std::unique_ptr(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + return nullptr; } RecipientBlockAckAgreementHandler::~RecipientBlockAckAgreementHandler() @@ -185,4 +190,3 @@ RecipientBlockAckAgreementHandler::~RecipientBlockAckAgreementHandler() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index 1e65a1557ef..f642c0a06b9 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -26,23 +26,22 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre { protected: std::map, RecipientBlockAckAgreement *> blockAckAgreements; + std::map, Ptr> lastAddbaResponses; protected: - virtual void terminateAgreement(MacAddress originatorAddr, Tid tid); - virtual RecipientBlockAckAgreement *addAgreement(const Ptr& addbaReq); - virtual void updateAgreement(const Ptr& addbaResponse); - virtual const Ptr buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy); + virtual RecipientBlockAckAgreement *removeAgreement(MacAddress originatorAddr, Tid tid); + virtual const Ptr buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, bool accepted); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); virtual simtime_t computeEarliestExpirationTime(); virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback); public: virtual ~RecipientBlockAckAgreementHandler(); - virtual void processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) override; - virtual void processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) override; + virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) override; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) override; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processTransmittedDelba(const Ptr& delba) override; + virtual std::unique_ptr processTransmittedDelba(const Ptr& delba) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; @@ -52,4 +51,3 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 47837ead38a..1dbff9f3f17 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -185,18 +185,17 @@ ReceiveBuffer *BlockAckReordering::createReceiveBufferIfNecessary(RecipientBlock return it->second; } -void BlockAckReordering::processReceivedDelba(const Ptr& delba) +std::vector BlockAckReordering::resetReceiveBuffer(Tid tid, MacAddress originatorAddr) { - Tid tid = delba->getTid(); - MacAddress originatorAddr = delba->getTransmitterAddress(); + std::vector frames; auto id = std::make_pair(tid, originatorAddr); auto it = receiveBuffers.find(id); if (it != receiveBuffers.end()) { + frames = it->second->extractFrames(); delete it->second; receiveBuffers.erase(it); } - else - EV_DETAIL << "Receive buffer is not found" << endl; + return frames; } void BlockAckReordering::passedUp(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber) @@ -244,4 +243,3 @@ BlockAckReordering::~BlockAckReordering() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index a50606f8a53..014f2c0a318 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -43,7 +43,7 @@ class INET_API BlockAckReordering public: virtual ~BlockAckReordering(); - void processReceivedDelba(const Ptr& delba); + std::vector resetReceiveBuffer(Tid tid, MacAddress originatorAddr); ReorderBuffer processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader); ReorderBuffer processReceivedBlockAckReq(RecipientBlockAckAgreement *agreement, const Ptr& blockAckReq); }; @@ -52,4 +52,3 @@ class INET_API BlockAckReordering } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc index 1dd9c959c8f..3bf4115a4b1 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc @@ -77,6 +77,16 @@ void ReceiveBuffer::removeFrame(SequenceNumberCyclic sequenceNumber) throw cRuntimeError("Unknown sequence number: %d", sequenceNumber.get()); } +ReceiveBuffer::Fragments ReceiveBuffer::extractFrames() +{ + Fragments frames; + for (auto& [sequenceNumber, fragments] : buffer) + frames.insert(frames.end(), fragments.begin(), fragments.end()); + buffer.clear(); + length = 0; + return frames; +} + ReceiveBuffer::~ReceiveBuffer() { for (auto fragments : buffer) { @@ -87,4 +97,3 @@ ReceiveBuffer::~ReceiveBuffer() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h index a499465d198..84e8fd5b6c5 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h @@ -37,6 +37,7 @@ class INET_API ReceiveBuffer bool insertFrame(Packet *dataPacket, const Ptr& dataHeader); void dropFramesUntil(SequenceNumberCyclic sequenceNumber); void removeFrame(SequenceNumberCyclic sequenceNumber); + Fragments extractFrames(); const ReorderBuffer& getBuffer() { return buffer; } int getLength() { return length; } @@ -50,4 +51,3 @@ class INET_API ReceiveBuffer } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 357745c3ddb..4eba49314c1 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -8,6 +8,8 @@ #ifndef __INET_IRECIPIENTBLOCKACKAGREEMENTHANDLER_H #define __INET_IRECIPIENTBLOCKACKAGREEMENTHANDLER_H +#include + #include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" @@ -24,10 +26,10 @@ class INET_API IRecipientBlockAckAgreementHandler public: virtual ~IRecipientBlockAckAgreementHandler() {} - virtual void processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) = 0; - virtual void processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; - virtual void processTransmittedDelba(const Ptr& delba) = 0; + virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; + virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) = 0; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; + virtual std::unique_ptr processTransmittedDelba(const Ptr& delba) = 0; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; @@ -38,4 +40,3 @@ class INET_API IRecipientBlockAckAgreementHandler } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h index 8a3ff4c0b7c..108a499727f 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h @@ -18,6 +18,14 @@ namespace ieee80211 { class INET_API IRecipientQosMacDataService { public: + // A duplicate has already been consumed and must not be processed as a + // management body; the coordination function may apply a subtype-specific + // response rule using the duplicate flag. + struct ManagementFrameReceptionResult { + std::vector completeFrames; + bool duplicate = false; + }; + static simsignal_t packetDefragmentedSignal; static simsignal_t packetDeaggregatedSignal; @@ -26,11 +34,11 @@ class INET_API IRecipientQosMacDataService virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; - virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) = 0; + virtual ManagementFrameReceptionResult managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) = 0; + virtual void resetBlockAckReordering(Tid tid, MacAddress originatorAddr) = 0; }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 010529ff44b..3f8cefd9e20 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -32,6 +32,7 @@ using namespace inet::physicallayer; simsignal_t Hcf::edcaCollisionDetectedSignal = cComponent::registerSignal("edcaCollisionDetected"); simsignal_t Hcf::blockAckAgreementAddedSignal = cComponent::registerSignal("blockAckAgreementAdded"); simsignal_t Hcf::blockAckAgreementDeletedSignal = cComponent::registerSignal("blockAckAgreementDeleted"); +simsignal_t Hcf::blockAckAgreementChangedSignal = cComponent::registerSignal("blockAckAgreementChanged"); Define_Module(Hcf); @@ -531,8 +532,9 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const PtrdataFrameReceived(packet, dataHeader, recipientBlockAckAgreementHandler)); } else if (auto mgmtHeader = dynamicPtrCast(header)) { - sendUp(recipientDataService->managementFrameReceived(packet, mgmtHeader)); - recipientProcessReceivedManagementFrame(mgmtHeader); + auto receptionResult = recipientDataService->managementFrameReceived(packet, mgmtHeader); + sendUp(receptionResult.completeFrames); + recipientProcessReceivedManagementFrame(mgmtHeader, receptionResult.duplicate); } else { // TODO else if (auto ctrlFrame = dynamic_cast(frame)) sendUp(recipientDataService->controlFrameReceived(packet, header, recipientBlockAckAgreementHandler)); @@ -555,13 +557,27 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header) +void Hcf::recipientProcessReceivedManagementFrame(const Ptr& header, bool duplicate) { + if (duplicate) { + if (recipientBlockAckAgreementHandler) { + if (auto addbaRequest = dynamicPtrCast(header)) + recipientBlockAckAgreementHandler->processDuplicateAddbaRequest(addbaRequest, this); + } + return; + } if (recipientBlockAckAgreementHandler && originatorBlockAckAgreementHandler) { if (auto addbaRequest = dynamicPtrCast(header)) { - recipientBlockAckAgreementHandler->processReceivedAddbaRequest(addbaRequest, recipientBlockAckAgreementPolicy, this); - auto agreement = recipientBlockAckAgreementHandler->getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()); - emit(blockAckAgreementAddedSignal, agreement); + bool hadAgreement = recipientBlockAckAgreementHandler->getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()) != nullptr; + auto agreement = recipientBlockAckAgreementHandler->processReceivedAddbaRequest(addbaRequest, recipientBlockAckAgreementPolicy, this, this); + if (agreement != nullptr) { + if (hadAgreement) { + recipientDataService->resetBlockAckReordering(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()); + emit(blockAckAgreementChangedSignal, agreement); + } + else + emit(blockAckAgreementAddedSignal, agreement); + } } else if (auto addbaResp = dynamicPtrCast(header)) { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(addbaResp->getTransmitterAddress(), addbaResp->getTid()); @@ -584,10 +600,14 @@ void Hcf::recipientProcessReceivedManagementFrame(const Ptr(header)) { + // IEEE Std 802.11-2024, 9.4.1.16, 10.25.4, and 11.5.3.3: + // Initiator selects the agreement direction; the transmitter is the peer. if (delba->getInitiator()) { - auto agreement = recipientBlockAckAgreementHandler->getAgreement(delba->getTid(), delba->getReceiverAddress()); - emit(blockAckAgreementDeletedSignal, agreement); - recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); + auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); + if (agreement != nullptr) { + recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getTransmitterAddress()); + emit(blockAckAgreementDeletedSignal, agreement.get()); + } } else { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid()); @@ -717,8 +737,8 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< if (originatorBlockAckAgreementHandler) originatorBlockAckAgreementHandler->processTransmittedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); } - else if (auto addbaResp = dynamicPtrCast(mgmtHeader)) - recipientBlockAckAgreementHandler->processTransmittedAddbaResp(addbaResp, this); + else if (dynamicPtrCast(mgmtHeader)) + ; // Recipient agreement was established when the successful response was formed. else if (auto delba = dynamicPtrCast(mgmtHeader)) { if (delba->getInitiator()) { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getReceiverAddress(), delba->getTid()); @@ -728,8 +748,16 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) emit(blockAckAgreementDeletedSignal, agreement.get()); } - else - recipientBlockAckAgreementHandler->processTransmittedDelba(delba); + else { + auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(delba); + if (agreement != nullptr) { + // IEEE Std 802.11-2024, 10.25.4 and 11.5.3.5: recipient + // resources are released whether the recipient transmitted or + // received DELBA. The reorder window is such a resource. + recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getReceiverAddress()); + emit(blockAckAgreementDeletedSignal, agreement.get()); + } + } } else ; // TODO other mgmt frames if needed } diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 7bf6631e302..fc9b5d49eff 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -32,7 +32,6 @@ #include "inet/linklayer/ieee80211/mac/contract/ITx.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h" -#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" #include "inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" @@ -55,6 +54,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: static simsignal_t edcaCollisionDetectedSignal; static simsignal_t blockAckAgreementAddedSignal; static simsignal_t blockAckAgreementDeletedSignal; + static simsignal_t blockAckAgreementChangedSignal; protected: Ieee80211Mac *mac = nullptr; @@ -133,6 +133,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual bool processDroppedBlockAckSetupFrame(Packet *packet); virtual bool processDroppedBlockAckTeardownFrame(Packet *packet); virtual bool isPacketReferencedByCurrentFrameSequence(const Packet *packet) const; + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; + virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); virtual void trackPendingFrame(Packet *packet, AccessCategory accessCategory); virtual void untrackPendingFrame(const Packet *packet); virtual void rebuildPendingFrameEligibility(); @@ -141,7 +143,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: // Recipient virtual void recipientProcessReceivedFrame(Packet *packet, const Ptr& header); virtual void recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header); - virtual void recipientProcessReceivedManagementFrame(const Ptr& header); + virtual void recipientProcessReceivedManagementFrame(const Ptr& header, bool duplicate); virtual void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header); // Originator @@ -176,8 +178,6 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr& responseHeader, Packet *receivedPacket, const Ptr& receivedHeader) override; virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override; - virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; - virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); // IProcedureCallback virtual void scheduleInactivityTimer(simtime_t timeout) override; virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned index 31b8bfc7047..8b07814b1a6 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned @@ -54,6 +54,7 @@ module Hcf extends Module like IHcf @signal[datarateSelected](type=double); @signal[blockAckAgreementAdded]; @signal[blockAckAgreementDeleted]; + @signal[blockAckAgreementChanged]; @statistic[packetSentToPeer](title="packets sent"; record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @statistic[packetSentToPeerUnicast](title="packets sent: unicast"; source=ieee80211Unicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @statistic[packetSentToPeerMulticast](title="packets sent: multicast"; source=ieee80211Multicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @@ -79,6 +80,7 @@ module Hcf extends Module like IHcf @statistic[datarateSelected](title="datarates selected"; record=vector; interpolationmode=none); @statistic[blockAckAgreementAdded](title="added block ack agreements"; record=count); @statistic[blockAckAgreementDeleted](title="deleted block ack agreements"; record=count); + @statistic[blockAckAgreementChanged](title="changed block ack agreements"; record=count); @statistic[blockAckAgreementActive](title="active block ack agreements"; source=warmup(count(blockAckAgreementAdded)-count(blockAckAgreementDeleted)); record=vector; interpolationmode=sample-hold; autoWarmupFilter=false); submodules: diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 77e677bb147..00dbfe970e0 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -30,6 +30,21 @@ void RecipientQosMacDataService::initialize() blockAckReordering = new BlockAckReordering(); } +void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress originatorAddr) +{ + Enter_Method("resetBlockAckReordering"); + if (blockAckReordering) { + auto droppedFrames = blockAckReordering->resetReceiveBuffer(tid, originatorAddr); + for (auto packet : droppedFrames) { + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } +} + Packet *RecipientQosMacDataService::defragment(std::vector completeFragments) { for (auto fragment : completeFragments) { @@ -112,25 +127,25 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data return deaggregatedFrames; } -std::vector RecipientQosMacDataService::managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) +IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataService::managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) { Enter_Method("managementFrameReceived"); take(mgmtPacket); // TODO MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption - if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) - return std::vector(); + if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) { + delete mgmtPacket; + return { {}, true }; + } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); } - if (auto delba = dynamicPtrCast(mgmtHeader)) - blockAckReordering->processReceivedDelba(delba); // TODO Defrag, MSDU Integrity, Replay Detection, RX MSDU Rate Limiting if (dynamicPtrCast(mgmtHeader)) { delete mgmtPacket; - return std::vector(); + return { {}, false }; } else - return std::vector({ mgmtPacket }); + return { { mgmtPacket }, false }; } std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) @@ -196,4 +211,3 @@ RecipientQosMacDataService::~RecipientQosMacDataService() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index e4b8ee28865..cb27ff09543 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -48,11 +48,11 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, public: virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; - virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) override; + virtual ManagementFrameReceptionResult managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) override; + virtual void resetBlockAckReordering(Tid tid, MacAddress originatorAddr) override; }; } /* namespace ieee80211 */ } /* namespace inet */ #endif - diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 696ba2cb33b..640e3c86289 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -654,11 +654,12 @@ /examples/wireless/power/, -f omnetpp.ini -c General -r 0, 100s, 6fae-d558/tplx;b8ea-b2fc/~tNl;5ea8-2cea/~tND, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, f252-8a4b/tplx;481d-4747/~tNl;5962-e45d/~tND;6097-a429/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 31b0-6212/tplx;82ec-9fde/~tNl;540d-c0a4/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, acd6-0108/tplx;b339-294a/~tNl;75d4-11f9/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 9ece-fbfb/tplx;c1af-29ff/~tNl;8fe3-d7a6/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, d094-b008/tplx;173a-e3fb/~tNl;7147-4f5b/~tND;08b5-d005/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, f252-8a4b/tplx;481d-4747/~tNl;7061-7f46/~tND;6097-a429/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 31b0-6212/tplx;82ec-9fde/~tNl;70c1-79a0/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, acd6-0108/tplx;b339-294a/~tNl;58ed-6137/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 9ece-fbfb/tplx;c1af-29ff/~tNl;01a6-2535/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, d094-b008/tplx;173a-e3fb/~tNl;82c5-b383/~tND;08b5-d005/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithTransactionalBlockAck -r 0, 3s, c4ff-d71f/tplx;68ba-2827/~tNl;9a98-cc02/~tND, PASS, wireless Ipv4 /examples/wireless/ratecontrol/, -f omnetpp.ini -c Mac -r 0, 100s, bf30-2f13/tplx;7b2f-653d/~tNl;6e1e-3b7b/~tND;19fe-8b0e/tyf, PASS, wireless diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 725e33108fc..9e3a5a153a3 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -16,17 +16,23 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h" #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" +#include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" #include "inet/queueing/gate/PacketGate.h" @@ -134,6 +140,22 @@ class TestTimeoutCancellationCallback : public TestCallback } }; +class TestRecipientHandler : public RecipientBlockAckAgreementHandler +{ + public: + int getNumCachedAddbaResponses() const { return lastAddbaResponses.size(); } +}; + +class TestBlockAckReordering : public BlockAckReordering +{ + public: + int getNumReceiveBuffers() const { return receiveBuffers.size(); } + ReceiveBuffer *getReceiveBuffer(Tid tid, MacAddress originatorAddress) const { + auto it = receiveBuffers.find(std::make_pair(tid, originatorAddress)); + return it == receiveBuffers.end() ? nullptr : it->second; + } +}; + class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue { public: @@ -204,6 +226,17 @@ class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue virtual void pushPacketProgress(Packet *, const cGate *, bps, b, b) override { throw cRuntimeError("Unsupported"); } }; +class TestImmediateRemovalQueue : public TestPacketQueue +{ + public: + PacketRemovalReason removalReason = PacketRemovalReason::DROPPED; + + virtual void enqueuePacket(Packet *packet) override { + notifyPacketRemoved(packet, removalReason); + delete packet; + } +}; + class TestWrongDequeueQueue : public TestPacketQueue { public: @@ -380,6 +413,14 @@ class TestPacketPulledListener : public cListener } }; +class TestSignalListener : public cListener +{ + public: + int numSignals = 0; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *) override { numSignals++; } +}; + class TestPacketArrivalListener : public cListener { public: @@ -392,12 +433,12 @@ class TestPacketArrivalListener : public cListener } }; -class TestSignalListener : public cListener +class TestOrderedSignalListener : public cListener { public: - int numSignals = 0; + std::vector signals; - virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *) override { numSignals++; } + virtual void receiveSignal(cComponent *, simsignal_t signal, cObject *, cObject *) override { signals.push_back(signal); } }; class TestPacketDropSignalListener : public cListener @@ -499,6 +540,23 @@ class TestEdca : public Edca virtual NonQosRecoveryProcedure *getMgmtAndNonQoSRecoveryProcedure() const override { return nonQosRecoveryProcedure; } }; +class TestRecipientDataService : public IRecipientQosMacDataService +{ + public: + int numReorderingResets = 0; + Tid lastTid = -1; + MacAddress lastOriginatorAddress; + + virtual std::vector dataFrameReceived(Packet *, const Ptr&, IRecipientBlockAckAgreementHandler *) override { return {}; } + virtual std::vector controlFrameReceived(Packet *, const Ptr&, IRecipientBlockAckAgreementHandler *) override { return {}; } + virtual ManagementFrameReceptionResult managementFrameReceived(Packet *, const Ptr&) override { return {}; } + virtual void resetBlockAckReordering(Tid tid, MacAddress originatorAddress) override { + numReorderingResets++; + lastTid = tid; + lastOriginatorAddress = originatorAddress; + } +}; + class TestHcf : public Hcf { public: @@ -557,6 +615,7 @@ class TestHcf : public Hcf void removePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::REMOVED); } void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } + void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } void processFailedFrame(Packet *packet) { originatorProcessFailedFrame(packet); } void processReceivedAck(Packet *packet, Packet *lastTransmittedPacket, AccessCategory accessCategory) { @@ -580,6 +639,14 @@ class TestHcf : public Hcf virtual void scheduleAddbaResponseTimer(simtime_t) override {} }; +class TestTxopProcedure : public TxopProcedure +{ + public: + std::vector *eventOrder = nullptr; + virtual simtime_t getRemaining() const override { return 1; } + virtual void endTxop() override { if (eventOrder != nullptr) eventOrder->push_back(2); } +}; + class TestMac : public Ieee80211Mac { public: @@ -588,6 +655,15 @@ class TestMac : public Ieee80211Mac virtual void sendDownPendingRadioConfigMsg() override { numPendingRadioConfigSends++; } }; +class TestEmptyFrameSequence : public IFrameSequence +{ + public: + virtual void startSequence(FrameSequenceContext *, int) override {} + virtual IFrameSequenceStep *prepareStep(FrameSequenceContext *) override { return nullptr; } + virtual bool completeStep(FrameSequenceContext *) override { return true; } + virtual std::string getHistory() const override { return ""; } +}; + class TestFragmentationPolicy : public IFragmentationPolicy { public: @@ -715,6 +791,38 @@ class TestFrameSequenceHandler : public IFrameSequenceHandler virtual void handleStartRxTimeout() override {} }; +class TestDcaf : public Dcaf +{ + public: + int numReleasedChannels = 0; + + void configure(InProgressFrames *inProgressFrames) { this->inProgressFrames = inProgressFrames; } + virtual void releaseChannel(IChannelAccess::ICallback *) override { numReleasedChannels++; } +}; + +class TestDcf : public Dcf +{ + public: + virtual ~TestDcf() { + for (auto listener : getLocalSignalListeners(Ieee80211Mac::frameTransmissionOutcomeSignal)) + unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, listener); + } + TestFrameSequenceHandler *configure(TestDcaf *channelAccess, Ieee80211Mac *mac) { + this->channelAccess = channelAccess; + this->mac = mac; + subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, check_and_cast(mac)); + auto handler = new TestFrameSequenceHandler(); + frameSequenceHandler = handler; + return handler; + } + void grant(TestDcaf *channelAccess) { channelGranted(channelAccess); } + + protected: + virtual FrameSequenceContext *buildContext() override { + return new FrameSequenceContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, channelAccess->getInProgressFrames(), nullptr, nullptr, new NonQoSContext(nullptr), nullptr); + } +}; + static Ptr makeQosHeader(MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber) { auto header = makeShared(); @@ -751,6 +859,18 @@ static Ptr makeResponse(MacAddress transmitterAddress, T return response; } +static Ptr makeRequest(MacAddress transmitterAddress, Tid tid, uint8_t dialogToken, SequenceNumberCyclic startingSequenceNumber, int bufferSize) +{ + auto request = makeShared(); + request->setTransmitterAddress(transmitterAddress); + request->setTid(tid); + request->setDialogToken(dialogToken); + request->setStartingSequenceNumber(startingSequenceNumber); + request->setBufferSize(bufferSize); + request->setBlockAckPolicy(1); + request->setBlockAckTimeoutValue(0); + return request; +} %activity: // IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.2. @@ -1443,6 +1563,1018 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(threw); } +// An initially rejected request has no agreement, so its response is never +// replayable and must not consume replay-cache state. +{ + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("initiallyRejectedAddbaDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestRecipientPolicy rejectingPolicy; + rejectingPolicy.accepted = false; + TestCallback responseCallback; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &rejectingPolicy); + hcf.configureRecipientDataService(recipientDataService); + hcf.configureManagementCallback(&responseCallback); + + auto rejectedRequest = makeRequest(peer3, 7, 89, SequenceNumberCyclic(90), 16); + rejectedRequest->setSequenceNumber(SequenceNumberCyclic(40)); + auto rejectedReception = recipientDataService->managementFrameReceived(new Packet("initiallyRejectedAddbaRequest", rejectedRequest), rejectedRequest); + ASSERT(!rejectedReception.duplicate); + hcf.processReceivedManagementFrame(rejectedRequest, rejectedReception.duplicate); + ASSERT(recipientHandler->getAgreement(7, peer3) == nullptr); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); + ASSERT(responseCallback.managementPackets.size() == 1); + ASSERT(dynamicPtrCast(responseCallback.managementHeader)->getStatusCode() != 0); + + auto duplicateRejectedRequest = makeRequest(peer3, 7, 89, SequenceNumberCyclic(90), 16); + duplicateRejectedRequest->setSequenceNumber(SequenceNumberCyclic(40)); + duplicateRejectedRequest->setRetry(true); + auto duplicateReception = recipientDataService->managementFrameReceived(new Packet("duplicateInitiallyRejectedAddbaRequest", duplicateRejectedRequest), duplicateRejectedRequest); + ASSERT(duplicateReception.duplicate); + hcf.processReceivedManagementFrame(duplicateRejectedRequest, duplicateReception.duplicate); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); + ASSERT(responseCallback.managementPackets.size() == 1); + + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + +// IEEE Std 802.11-2024, 9.2.4.1.6 and 10.3.2.14.3/Table 10-6: +// duplicate identity comes from Retry + TA + MAC sequence/fragment, not from +// equal ADDBA parameters. Re-sending the cached exact response is an +// explicit robustness/model extension; it leaves recipient state untouched. +{ + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("duplicateAddbaDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + TestPacketDropSignalListener reorderDropListener; + recipientDataService->subscribe(packetDroppedSignal, &reorderDropListener); + + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestRecipientPolicy duplicatePolicy; + duplicatePolicy.blockAckTimeoutValue = 1; + TestCallback responseCallback; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &duplicatePolicy); + hcf.configureRecipientDataService(recipientDataService); + hcf.configureManagementCallback(&responseCallback); + TestSignalListener addedListener; + TestSignalListener changedListener; + hcf.subscribe(Hcf::blockAckAgreementAddedSignal, &addedListener); + hcf.subscribe(Hcf::blockAckAgreementChangedSignal, &changedListener); + + auto firstRequest = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); + firstRequest->setSequenceNumber(SequenceNumberCyclic(50)); + firstRequest->setBlockAckTimeoutValue(3); + auto firstReception = recipientDataService->managementFrameReceived(new Packet("firstAddbaRequest", firstRequest), firstRequest); + ASSERT(!firstReception.duplicate); + ASSERT(firstReception.completeFrames.empty()); + hcf.processReceivedManagementFrame(firstRequest, firstReception.duplicate); + auto firstAgreement = recipientHandler->getAgreement(6, peer2); + ASSERT(firstAgreement != nullptr); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); + auto firstBlockAckRecord = firstAgreement->getBlockAckRecord(); + auto firstExpirationTime = firstAgreement->getExpirationTime(); + ASSERT(addedListener.numSignals == 1); + ASSERT(changedListener.numSignals == 0); + ASSERT(responseCallback.managementPackets.size() == 1); + auto firstResponse = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(firstResponse != nullptr); + ASSERT(firstResponse->getDialogToken() == 90); + ASSERT(firstResponse->getStatusCode() == 0); + ASSERT(firstResponse->getTid() == 6); + ASSERT(firstResponse->getBlockAckPolicy()); + ASSERT(firstResponse->getBufferSize() == 32); + ASSERT(firstResponse->getBlockAckTimeoutValue() == 3); + ASSERT(firstResponse->getAMsduSupported()); + + auto bufferedHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(101)); + bufferedHeader->setTransmitterAddress(peer2); + auto bufferedPacket = new Packet("duplicateAddbaBufferedData", bufferedHeader); + ASSERT(recipientDataService->dataFrameReceived(bufferedPacket, bufferedHeader, recipientHandler).empty()); + + // Mutable policy is deliberately changed after negotiation. A true MAC + // duplicate must replay the original successful response, not rebuild it. + duplicatePolicy.accepted = false; + duplicatePolicy.blockAckTimeoutValue = 0; + duplicatePolicy.aMsduSupportedValue = false; + duplicatePolicy.maximumAllowedBufferSize = 1; + auto duplicateRequest = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); + duplicateRequest->setSequenceNumber(SequenceNumberCyclic(50)); + duplicateRequest->setRetry(true); + duplicateRequest->setBlockAckTimeoutValue(3); + auto duplicateReception = recipientDataService->managementFrameReceived(new Packet("duplicateAddbaRequest", duplicateRequest), duplicateRequest); + ASSERT(duplicateReception.duplicate); + ASSERT(duplicateReception.completeFrames.empty()); + hcf.processReceivedManagementFrame(duplicateRequest, duplicateReception.duplicate); + ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); + ASSERT(firstAgreement->getBlockAckRecord() == firstBlockAckRecord); + ASSERT(firstAgreement->getExpirationTime() == firstExpirationTime); + ASSERT(addedListener.numSignals == 1); + ASSERT(changedListener.numSignals == 0); + ASSERT(reorderDropListener.numSignals == 0); + ASSERT(responseCallback.managementPackets.size() == 2); + auto duplicateResponse = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(duplicateResponse->getDialogToken() == firstResponse->getDialogToken()); + ASSERT(duplicateResponse->getStatusCode() == firstResponse->getStatusCode()); + ASSERT(duplicateResponse->getTid() == firstResponse->getTid()); + ASSERT(duplicateResponse->getBlockAckPolicy() == firstResponse->getBlockAckPolicy()); + ASSERT(duplicateResponse->getBufferSize() == firstResponse->getBufferSize()); + ASSERT(duplicateResponse->getBlockAckTimeoutValue() == firstResponse->getBlockAckTimeoutValue()); + ASSERT(duplicateResponse->getAMsduSupported() == firstResponse->getAMsduSupported()); + + // A rejected fresh renegotiation leaves agreement A in place, but its + // retransmission must replay B's rejection rather than A's old success. + duplicatePolicy.blockAckTimeoutValue = 1; + auto rejectedRequest = makeRequest(peer2, 6, 91, SequenceNumberCyclic(200), 16); + rejectedRequest->setSequenceNumber(SequenceNumberCyclic(51)); + rejectedRequest->setBlockAckTimeoutValue(4); + auto rejectedReception = recipientDataService->managementFrameReceived(new Packet("rejectedAddbaRequest", rejectedRequest), rejectedRequest); + ASSERT(!rejectedReception.duplicate); + hcf.processReceivedManagementFrame(rejectedRequest, rejectedReception.duplicate); + ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); + ASSERT(changedListener.numSignals == 0); + ASSERT(reorderDropListener.numSignals == 0); + ASSERT(responseCallback.managementPackets.size() == 3); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); + auto rejectedResponse = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(rejectedResponse->getDialogToken() == 91); + ASSERT(rejectedResponse->getStatusCode() != 0); + ASSERT(rejectedResponse->getTid() == 6); + ASSERT(rejectedResponse->getBufferSize() == 1); + ASSERT(rejectedResponse->getBlockAckTimeoutValue() == 4); + ASSERT(!rejectedResponse->getAMsduSupported()); + + auto duplicateRejectedRequest = makeRequest(peer2, 6, 91, SequenceNumberCyclic(200), 16); + duplicateRejectedRequest->setSequenceNumber(SequenceNumberCyclic(51)); + duplicateRejectedRequest->setRetry(true); + duplicateRejectedRequest->setBlockAckTimeoutValue(4); + auto duplicateRejectedReception = recipientDataService->managementFrameReceived(new Packet("duplicateRejectedAddbaRequest", duplicateRejectedRequest), duplicateRejectedRequest); + ASSERT(duplicateRejectedReception.duplicate); + hcf.processReceivedManagementFrame(duplicateRejectedRequest, duplicateRejectedReception.duplicate); + ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); + ASSERT(changedListener.numSignals == 0); + ASSERT(reorderDropListener.numSignals == 0); + ASSERT(responseCallback.managementPackets.size() == 4); + auto duplicateRejectedResponse = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(duplicateRejectedResponse->getDialogToken() == rejectedResponse->getDialogToken()); + ASSERT(duplicateRejectedResponse->getStatusCode() == rejectedResponse->getStatusCode()); + ASSERT(duplicateRejectedResponse->getTid() == rejectedResponse->getTid()); + ASSERT(duplicateRejectedResponse->getBlockAckPolicy() == rejectedResponse->getBlockAckPolicy()); + ASSERT(duplicateRejectedResponse->getBufferSize() == rejectedResponse->getBufferSize()); + ASSERT(duplicateRejectedResponse->getBlockAckTimeoutValue() == rejectedResponse->getBlockAckTimeoutValue()); + ASSERT(duplicateRejectedResponse->getAMsduSupported() == rejectedResponse->getAMsduSupported()); + + auto expectedHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(100)); + expectedHeader->setTransmitterAddress(peer2); + auto expectedPacket = new Packet("duplicateAddbaExpectedData", expectedHeader); + auto expectedFrames = recipientDataService->dataFrameReceived(expectedPacket, expectedHeader, recipientHandler); + ASSERT(expectedFrames.size() == 1); + ASSERT(expectedFrames.front() == expectedPacket); + auto followingHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(102)); + followingHeader->setTransmitterAddress(peer2); + auto followingPacket = new Packet("duplicateAddbaFollowingData", followingHeader); + auto followingFrames = recipientDataService->dataFrameReceived(followingPacket, followingHeader, recipientHandler); + ASSERT(followingFrames.size() == 1); + ASSERT(followingFrames.front() == bufferedPacket); + for (auto packet : expectedFrames) { + take(packet); + drop(packet); + delete packet; + } + for (auto packet : followingFrames) { + take(packet); + drop(packet); + delete packet; + } + + // A new MAC identity with the exact same ADDBA body is a genuine 10.25.2 + // renegotiation: replace the agreement and reset the reorder window, + // which still contains followingPacket (sequence number 102). + duplicatePolicy.accepted = true; + duplicatePolicy.blockAckTimeoutValue = 1; + duplicatePolicy.aMsduSupportedValue = true; + duplicatePolicy.maximumAllowedBufferSize = 64; + auto replacementRequest = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); + replacementRequest->setSequenceNumber(SequenceNumberCyclic(52)); + replacementRequest->setBlockAckTimeoutValue(3); + auto replacementReception = recipientDataService->managementFrameReceived(new Packet("replacementAddbaRequest", replacementRequest), replacementRequest); + ASSERT(!replacementReception.duplicate); + ASSERT(replacementReception.completeFrames.empty()); + hcf.processReceivedManagementFrame(replacementRequest, replacementReception.duplicate); + ASSERT(recipientHandler->getAgreement(6, peer2) != firstAgreement); + ASSERT(addedListener.numSignals == 1); + ASSERT(changedListener.numSignals == 1); + ASSERT(reorderDropListener.numSignals == 1); + ASSERT(reorderDropListener.numOtherPacketDrops == 1); + ASSERT(responseCallback.managementPackets.size() == 5); + + auto duplicateWithoutAgreement = makeRequest(peer3, 7, 91, SequenceNumberCyclic(200), 16); + hcf.processReceivedManagementFrame(duplicateWithoutAgreement, true); + ASSERT(responseCallback.managementPackets.size() == 5); + + auto duplicateDelba = makeShared(); + duplicateDelba->setTransmitterAddress(peer2); + duplicateDelba->setTid(6); + duplicateDelba->setInitiator(true); + auto replacementAgreement = recipientHandler->getAgreement(6, peer2); + hcf.processReceivedManagementFrame(duplicateDelba, true); + ASSERT(recipientHandler->getAgreement(6, peer2) == replacementAgreement); + + hcf.processReceivedManagementFrame(duplicateDelba, false); + ASSERT(recipientHandler->getAgreement(6, peer2) == nullptr); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); + auto duplicateAfterTeardown = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); + duplicateAfterTeardown->setSequenceNumber(SequenceNumberCyclic(52)); + duplicateAfterTeardown->setRetry(true); + hcf.processReceivedManagementFrame(duplicateAfterTeardown, true); + ASSERT(responseCallback.managementPackets.size() == 5); + + hcf.unsubscribe(Hcf::blockAckAgreementAddedSignal, &addedListener); + hcf.unsubscribe(Hcf::blockAckAgreementChangedSignal, &changedListener); + recipientDataService->unsubscribe(packetDroppedSignal, &reorderDropListener); + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + +// IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.3. +RecipientBlockAckAgreementHandler immediateRecipientHandler; +TestRecipientPolicy recipientPolicy; +TestCallback recipientCallback; +auto recipientRequest = makeShared(); +recipientRequest->setTransmitterAddress(peer1); +recipientRequest->setTid(6); +recipientRequest->setDialogToken(77); +recipientRequest->setStartingSequenceNumber(SequenceNumberCyclic(0)); +recipientRequest->setBufferSize(64); +recipientRequest->setBlockAckPolicy(1); +recipientRequest->setBlockAckTimeoutValue(0); +auto firstImmediateAgreement = immediateRecipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientCallback, &recipientCallback); +ASSERT(firstImmediateAgreement != nullptr); +ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == firstImmediateAgreement); +ASSERT(firstImmediateAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(0)); +auto acceptedResponse = dynamicPtrCast(recipientCallback.managementHeader); +ASSERT(acceptedResponse->getDialogToken() == 77); +ASSERT(acceptedResponse->getStatusCode() == 0); + +// Renegotiation replaces the recipient agreement and the HCF reset boundary +// must discard the old reorder window before frames use the new SSN. +RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; +{ + TestBlockAckReordering reordering; + auto oldWindowHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(1)); + auto oldWindowPacket = new Packet("oldWindowPacket", oldWindowHeader); + auto oldWindowFrames = reordering.processReceivedQoSFrame(firstImmediateAgreement, oldWindowPacket, oldWindowHeader); + ASSERT(oldWindowFrames.empty()); + ASSERT(reordering.getNumReceiveBuffers() == 1); + ASSERT(reordering.getReceiveBuffer(6, peer1)->getLength() == 1); + + auto discardedOldWindowFrames = reordering.resetReceiveBuffer(6, peer1); + ASSERT(discardedOldWindowFrames.size() == 1); + ASSERT(discardedOldWindowFrames.front() == oldWindowPacket); + take(discardedOldWindowFrames.front()); + drop(discardedOldWindowFrames.front()); + delete discardedOldWindowFrames.front(); + ASSERT(reordering.getNumReceiveBuffers() == 0); + + recipientRequest->setDialogToken(79); + recipientRequest->setStartingSequenceNumber(SequenceNumberCyclic(9)); + replacementImmediateAgreement = immediateRecipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientCallback, &recipientCallback); + ASSERT(replacementImmediateAgreement != nullptr); + ASSERT(replacementImmediateAgreement != firstImmediateAgreement); + ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == replacementImmediateAgreement); + ASSERT(replacementImmediateAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(9)); + auto newWindowHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(10)); + auto newWindowPacket = new Packet("newWindowPacket", newWindowHeader); + auto newWindowFrames = reordering.processReceivedQoSFrame(replacementImmediateAgreement, newWindowPacket, newWindowHeader); + ASSERT(newWindowFrames.empty()); + ASSERT(reordering.getReceiveBuffer(6, peer1)->getNextExpectedSequenceNumber() == SequenceNumberCyclic(9)); + ASSERT(reordering.getReceiveBuffer(6, peer1)->getLength() == 1); + auto discardedNewWindowFrames = reordering.resetReceiveBuffer(6, peer1); + ASSERT(discardedNewWindowFrames.size() == 1); + ASSERT(discardedNewWindowFrames.front() == newWindowPacket); + take(discardedNewWindowFrames.front()); + drop(discardedNewWindowFrames.front()); + delete discardedNewWindowFrames.front(); +} + +// The recipient data service owns reorder-buffer reset observability: every +// buffered MPDU is reported exactly once before it is deleted. +{ + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("reorderingDropDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + TestPacketDropSignalListener dropListener; + recipientDataService->subscribe(packetDroppedSignal, &dropListener); + auto bufferedHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(11)); + bufferedHeader->setTransmitterAddress(peer1); + auto bufferedPacket = new Packet("signaledOldWindowPacket", bufferedHeader); + auto signaledOldWindowFrames = recipientDataService->dataFrameReceived(bufferedPacket, bufferedHeader, &immediateRecipientHandler); + ASSERT(signaledOldWindowFrames.empty()); + recipientDataService->resetBlockAckReordering(6, peer1); + ASSERT(dropListener.numSignals == 1); + ASSERT(dropListener.numOtherPacketDrops == 1); + recipientDataService->resetBlockAckReordering(6, peer1); + ASSERT(dropListener.numSignals == 1); + recipientDataService->unsubscribe(packetDroppedSignal, &dropListener); + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + +recipientPolicy.accepted = false; +recipientRequest->setDialogToken(80); +recipientRequest->setStartingSequenceNumber(SequenceNumberCyclic(12)); +ASSERT(immediateRecipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientCallback, &recipientCallback) == nullptr); +ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == replacementImmediateAgreement); + +// A received DELBA is keyed by its transmitter (the peer), not by its receiver +// address (the local station). +{ + RecipientBlockAckAgreementHandler delbaRecipientHandler; + TestRecipientPolicy delbaRecipientPolicy; + TestCallback delbaRecipientCallback; + auto request = makeRequest(peer2, 4, 81, SequenceNumberCyclic(12), 32); + auto establishedAgreement = delbaRecipientHandler.processReceivedAddbaRequest(request, &delbaRecipientPolicy, &delbaRecipientCallback, &delbaRecipientCallback); + ASSERT(establishedAgreement != nullptr); + auto delba = makeShared(); + delba->setTransmitterAddress(peer2); + delba->setReceiverAddress(peer1); + delba->setTid(4); + delba->setInitiator(true); + auto terminatedAgreement = delbaRecipientHandler.processReceivedDelba(delba, &delbaRecipientPolicy); + ASSERT(terminatedAgreement != nullptr); + ASSERT(delbaRecipientHandler.getAgreement(4, peer2) == nullptr); +} + +// HCF emits one deletion event for the peer-keyed recipient agreement and can +// then be torn down without walking an EDCA child hierarchy. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestCallback delbaCallback; + auto request = makeRequest(peer2, 2, 82, SequenceNumberCyclic(14), 32); + recipientPolicy.accepted = true; + auto establishedAgreement = recipientHandler->processReceivedAddbaRequest(request, &recipientPolicy, &delbaCallback, &delbaCallback); + ASSERT(establishedAgreement != nullptr); + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + TestRecipientDataService recipientDataService; + hcf.configureRecipientDataService(&recipientDataService); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + auto delba = makeShared(); + delba->setTransmitterAddress(peer2); + delba->setReceiverAddress(peer1); + delba->setTid(2); + delba->setInitiator(true); + hcf.processReceivedManagementFrame(delba); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(recipientDataService.lastTid == 2); + ASSERT(recipientDataService.lastOriginatorAddress == peer2); + ASSERT(recipientHandler->getAgreement(2, peer2) == nullptr); + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); +} + +// A rejected DELBA leaves the recipient agreement, reordering state, and +// agreement statistics unchanged. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestRecipientPolicy rejectingPolicy; + TestCallback delbaCallback; + auto request = makeRequest(peer2, 2, 83, SequenceNumberCyclic(15), 32); + auto establishedAgreement = recipientHandler->processReceivedAddbaRequest(request, &rejectingPolicy, &delbaCallback, &delbaCallback); + ASSERT(establishedAgreement != nullptr); + rejectingPolicy.delbaAccepted = false; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &rejectingPolicy); + TestRecipientDataService recipientDataService; + hcf.configureRecipientDataService(&recipientDataService); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + auto delba = makeShared(); + delba->setTransmitterAddress(peer2); + delba->setReceiverAddress(peer1); + delba->setTid(2); + delba->setInitiator(true); + hcf.processReceivedManagementFrame(delba); + ASSERT(deletionListener.numSignals == 0); + ASSERT(recipientDataService.numReorderingResets == 0); + ASSERT(recipientHandler->getAgreement(2, peer2) != nullptr); + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); +} + +// IEEE Std 802.11-2024, 10.25.4 and 11.5.3.5: locally transmitting a +// recipient DELBA tears down the agreement and its reorder resources before a +// replacement agreement for the same peer and TID starts a fresh window. +{ + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("transmittedDelbaDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + TestPacketDropSignalListener reorderDropListener; + recipientDataService->subscribe(packetDroppedSignal, &reorderDropListener); + + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestRecipientPolicy transmittedDelbaPolicy; + TestCallback transmittedDelbaCallback; + auto firstRequest = makeRequest(peer2, 3, 84, SequenceNumberCyclic(100), 32); + auto firstAgreement = recipientHandler->processReceivedAddbaRequest(firstRequest, &transmittedDelbaPolicy, &transmittedDelbaCallback, &transmittedDelbaCallback); + ASSERT(firstAgreement != nullptr); + auto oldWindowHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(101)); + oldWindowHeader->setTransmitterAddress(peer2); + auto oldWindowPacket = new Packet("transmittedDelbaOldWindow", oldWindowHeader); + ASSERT(recipientDataService->dataFrameReceived(oldWindowPacket, oldWindowHeader, recipientHandler).empty()); + + QosAckHandler transmittedDelbaAckHandler; + TestEdcaf transmittedDelbaEdcaf; + transmittedDelbaEdcaf.qosAckHandler = &transmittedDelbaAckHandler; + TestEdca transmittedDelbaEdca; + transmittedDelbaEdca.edcaf = &transmittedDelbaEdcaf; + OriginatorQosAckPolicy transmittedDelbaAckPolicy; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &transmittedDelbaPolicy); + hcf.configureRecipientDataService(recipientDataService); + hcf.configureTransmittedManagement(&transmittedDelbaEdca, &transmittedDelbaAckPolicy); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + + auto delba = makeShared(); + delba->setReceiverAddress(peer2); + delba->setTid(3); + delba->setInitiator(false); + delba->setSequenceNumber(SequenceNumberCyclic(200)); + Packet delbaPacket("transmittedRecipientDelba", delba); + hcf.processTransmittedManagement(&delbaPacket, delba, AC_BE); + ASSERT(recipientHandler->getAgreement(3, peer2) == nullptr); + ASSERT(deletionListener.numSignals == 1); + ASSERT(reorderDropListener.numOtherPacketDrops == 1); + + auto replacementRequest = makeRequest(peer2, 3, 85, SequenceNumberCyclic(10), 32); + auto replacementAgreement = recipientHandler->processReceivedAddbaRequest(replacementRequest, &transmittedDelbaPolicy, &transmittedDelbaCallback, &transmittedDelbaCallback); + ASSERT(replacementAgreement != nullptr); + auto newWindowHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(11)); + newWindowHeader->setTransmitterAddress(peer2); + auto newWindowPacket = new Packet("transmittedDelbaNewWindow", newWindowHeader); + ASSERT(recipientDataService->dataFrameReceived(newWindowPacket, newWindowHeader, recipientHandler).empty()); + recipientDataService->resetBlockAckReordering(3, peer2); + ASSERT(reorderDropListener.numOtherPacketDrops == 2); + + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + recipientDataService->unsubscribe(packetDroppedSignal, &reorderDropListener); + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + +// The reverse DELBA direction terminates the originator agreement and emits +// exactly one deletion event; policy rejection emits nothing and preserves it. +{ + auto originatorHandler = new TestOriginatorHandler(); + originatorHandler->addEstablishedAgreement(peer3, 5); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestOriginatorPolicy delbaPolicy; + TestRecipientPolicy delbaRecipientPolicy; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &delbaPolicy, recipientHandler, &delbaRecipientPolicy); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + auto acceptedDelba = makeShared(); + acceptedDelba->setTransmitterAddress(peer3); + acceptedDelba->setReceiverAddress(peer1); + acceptedDelba->setTid(5); + acceptedDelba->setInitiator(false); + hcf.processReceivedManagementFrame(acceptedDelba); + ASSERT(deletionListener.numSignals == 1); + ASSERT(originatorHandler->getAgreement(peer3, 5) == nullptr); + originatorHandler->addEstablishedAgreement(peer3, 5); + delbaPolicy.delbaAccepted = false; + auto rejectedDelba = makeShared(*acceptedDelba); + hcf.processReceivedManagementFrame(rejectedDelba); + ASSERT(deletionListener.numSignals == 1); + ASSERT(originatorHandler->getAgreement(peer3, 5) != nullptr); + + QosAckHandler transmittedOriginatorAckHandler; + TestEdcaf transmittedOriginatorEdcaf; + transmittedOriginatorEdcaf.qosAckHandler = &transmittedOriginatorAckHandler; + TestPacketQueue transmittedOriginatorPendingQueue; + TestInProgressFrames transmittedOriginatorInProgressFrames; + TestOriginatorQosMacDataService transmittedOriginatorDataService; + transmittedOriginatorInProgressFrames.configure(&transmittedOriginatorDataService, &transmittedOriginatorAckHandler, &transmittedOriginatorPendingQueue); + transmittedOriginatorEdcaf.pendingQueue = &transmittedOriginatorPendingQueue; + transmittedOriginatorEdcaf.inProgressFrames = &transmittedOriginatorInProgressFrames; + TestEdca transmittedOriginatorEdca; + transmittedOriginatorEdca.edcaf = &transmittedOriginatorEdcaf; + OriginatorQosAckPolicy transmittedOriginatorAckPolicy; + hcf.configureTransmittedManagement(&transmittedOriginatorEdca, &transmittedOriginatorAckPolicy); + originatorHandler->addEstablishedAgreement(peer3, 6); + auto transmittedEstablishedDelba = makeShared(); + transmittedEstablishedDelba->setReceiverAddress(peer3); + transmittedEstablishedDelba->setTid(6); + transmittedEstablishedDelba->setInitiator(true); + transmittedEstablishedDelba->setSequenceNumber(SequenceNumberCyclic(201)); + Packet transmittedEstablishedDelbaPacket("transmittedEstablishedOriginatorDelba", transmittedEstablishedDelba); + hcf.processTransmittedManagement(&transmittedEstablishedDelbaPacket, transmittedEstablishedDelba, AC_BE); + ASSERT(deletionListener.numSignals == 2); + ASSERT(originatorHandler->getAgreement(peer3, 6) == nullptr); + hcf.processTransmittedManagement(&transmittedEstablishedDelbaPacket, transmittedEstablishedDelba, AC_BE); + ASSERT(deletionListener.numSignals == 2); + + TestCallback pendingDelbaCallback; + Packet pendingDelbaTrigger("pendingDelbaTrigger"); + auto pendingDelbaTriggerHeader = makeQosHeader(peer3, 7, SequenceNumberCyclic(0)); + originatorHandler->processAcknowledgedDataFrame(&pendingDelbaTrigger, pendingDelbaTriggerHeader, &delbaPolicy, &pendingDelbaCallback); + ASSERT(originatorHandler->isAddbaResponsePending(peer3, 7)); + auto transmittedPendingDelba = makeShared(); + transmittedPendingDelba->setReceiverAddress(peer3); + transmittedPendingDelba->setTid(7); + transmittedPendingDelba->setInitiator(true); + transmittedPendingDelba->setSequenceNumber(SequenceNumberCyclic(202)); + Packet transmittedPendingDelbaPacket("transmittedPendingOriginatorDelba", transmittedPendingDelba); + hcf.processTransmittedManagement(&transmittedPendingDelbaPacket, transmittedPendingDelba, AC_BE); + ASSERT(deletionListener.numSignals == 2); + ASSERT(originatorHandler->getAgreement(peer3, 7) == nullptr); + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); +} + +// IEEE Std 802.11-2024, 11.5.2.2 and 11.5.3.2: a received DELBA can cancel +// an originator transaction before ADDBA Response success; that pending state +// was never observable as an active agreement and must not emit Deleted. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestOriginatorPolicy pendingPolicy; + TestRecipientPolicy pendingRecipientPolicy; + TestOriginatorQosMacDataService pendingDataService; + pendingDataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto header = packet->peekAtFront(); + if (auto addbaReq = dynamicPtrCast(header)) + return originatorHandler->isAddbaRequestPending(packet, addbaReq); + auto dataHeader = dynamicPtrCast(header); + return dataHeader == nullptr || !originatorHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); + }); + TestPacketQueue pendingQueue; + QosAckHandler pendingAckHandler; + TestInProgressFrames pendingInProgressFrames; + pendingInProgressFrames.configure(&pendingDataService, &pendingAckHandler, &pendingQueue); + TestEdcaf pendingEdcaf; + pendingEdcaf.pendingQueue = &pendingQueue; + pendingEdcaf.inProgressFrames = &pendingInProgressFrames; + pendingEdcaf.qosAckHandler = &pendingAckHandler; + TestEdca pendingEdca; + pendingEdca.edcaf = &pendingEdcaf; + TestHcf pendingHcf; + pendingHcf.configureBlockAckHandlers(originatorHandler, &pendingPolicy, recipientHandler, &pendingRecipientPolicy); + pendingHcf.configureEligibilityIndex(&pendingEdca, &pendingDataService); + pendingHcf.observePendingQueue(&pendingQueue); + TestCallback pendingCallback; + Packet pendingTrigger("receivedPendingDelbaTrigger"); + auto pendingHeader = makeQosHeader(peer3, 8, SequenceNumberCyclic(203)); + originatorHandler->processAcknowledgedDataFrame(&pendingTrigger, pendingHeader, &pendingPolicy, &pendingCallback); + auto pendingRequestPacket = pendingCallback.managementPacket; + auto pendingRequest = dynamicPtrCast(pendingCallback.managementHeader); + pendingCallback.forgetManagementPacket(pendingRequestPacket); + auto heldPacket = new Packet("receivedPendingDelbaHeld", makeQosHeader(peer3, 8, SequenceNumberCyclic(204))); + pendingQueue.enqueuePacket(pendingRequestPacket); + pendingQueue.enqueuePacket(heldPacket); + pendingHcf.trackFrame(pendingRequestPacket, AC_BK); + pendingHcf.trackFrame(heldPacket, AC_BK); + ASSERT(originatorHandler->isAddbaResponsePending(peer3, 8)); + ASSERT(pendingHcf.getNumEligiblePendingFrames(AC_BK) == 1); + TestSignalListener pendingDeletionListener; + pendingHcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &pendingDeletionListener); + auto receivedPendingDelba = makeShared(); + receivedPendingDelba->setTransmitterAddress(peer3); + receivedPendingDelba->setReceiverAddress(peer1); + receivedPendingDelba->setTid(8); + receivedPendingDelba->setInitiator(false); + pendingHcf.processReceivedManagementFrame(receivedPendingDelba); + ASSERT(pendingDeletionListener.numSignals == 0); + ASSERT(originatorHandler->getAgreement(peer3, 8) == nullptr); + ASSERT(pendingHcf.numCancelledAddbaTransactions == 1); + ASSERT(pendingHcf.numRebuildEligibilityCalls > 0); + ASSERT(pendingQueue.getNumPackets() == 1); + ASSERT(pendingQueue.getPacket(0) == heldPacket); + ASSERT(pendingDataService.isFrameEligible(heldPacket)); + ASSERT(pendingHcf.getNumEligiblePendingFrames(AC_BK) == 1); + ASSERT(pendingHcf.numResumedEligibleChannelAccess == 1); + pendingHcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &pendingDeletionListener); + pendingQueue.removePacket(heldPacket); + delete heldPacket; +} + +// A locally vetoed successful response immediately restores data eligibility +// even while its best-effort DELBA remains queued. Once retry backoff expires, +// a replacement ADDBA invalidates the old DELBA without requiring a disposal +// callback from the queue. +{ + TestPacketQueue heldTeardownQueue; + auto heldTeardownOriginatorHandler = new TestOriginatorHandler(); + auto heldTeardownRecipientHandler = new RecipientBlockAckAgreementHandler(); + TestOriginatorPolicy heldTeardownOriginatorPolicy; + heldTeardownOriginatorPolicy.addbaAccepted = false; + TestRecipientPolicy heldTeardownRecipientPolicy; + TestOriginatorQosMacDataService heldTeardownDataService; + heldTeardownDataService.setFrameEligibilityFunction([heldTeardownOriginatorHandler](const Packet *packet) { + auto header = packet->peekAtFront(); + if (auto addbaReq = dynamicPtrCast(header)) + return heldTeardownOriginatorHandler->isAddbaRequestPending(packet, addbaReq); + if (auto delba = dynamicPtrCast(header)) + return heldTeardownOriginatorHandler->isDelbaPending(packet, delba); + auto dataHeader = dynamicPtrCast(header); + return dataHeader == nullptr || !heldTeardownOriginatorHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); + }); + QosAckHandler heldTeardownAckHandler; + TestInProgressFrames heldTeardownInProgressFrames; + heldTeardownInProgressFrames.configure(&heldTeardownDataService, &heldTeardownAckHandler, &heldTeardownQueue); + TestEdcaf heldTeardownEdcaf; + heldTeardownEdcaf.pendingQueue = &heldTeardownQueue; + heldTeardownEdcaf.inProgressFrames = &heldTeardownInProgressFrames; + heldTeardownEdcaf.qosAckHandler = &heldTeardownAckHandler; + TestEdca heldTeardownEdca; + heldTeardownEdca.edcaf = &heldTeardownEdcaf; + TestHcf heldTeardownHcf; + heldTeardownHcf.configureBlockAckHandlers(heldTeardownOriginatorHandler, &heldTeardownOriginatorPolicy, heldTeardownRecipientHandler, &heldTeardownRecipientPolicy); + heldTeardownHcf.configureEligibilityIndex(&heldTeardownEdca, &heldTeardownDataService); + heldTeardownHcf.observePendingQueue(&heldTeardownQueue); + TestCallback heldTeardownCallback; + Packet heldTeardownTrigger("heldTeardownTrigger"); + auto heldTeardownTriggerHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(210)); + heldTeardownOriginatorHandler->processAcknowledgedDataFrame(&heldTeardownTrigger, heldTeardownTriggerHeader, &heldTeardownOriginatorPolicy, &heldTeardownCallback); + auto heldTeardownRequest = dynamicPtrCast(heldTeardownCallback.managementHeader); + heldTeardownOriginatorHandler->processTransmittedAddbaReq(heldTeardownCallback.managementPacket, heldTeardownRequest, &heldTeardownOriginatorPolicy, &heldTeardownCallback); + auto heldDataPacket = new Packet("heldTeardownData", makeQosHeader(peer1, 7, SequenceNumberCyclic(211))); + heldTeardownQueue.enqueuePacket(heldDataPacket); + heldTeardownHcf.trackFrame(heldDataPacket, AC_BK); + ASSERT(!heldTeardownDataService.isFrameEligible(heldDataPacket)); + TestOrderedSignalListener transitionListener; + heldTeardownHcf.subscribe(Hcf::blockAckAgreementAddedSignal, &transitionListener); + heldTeardownHcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &transitionListener); + auto heldTeardownResponse = makeResponse(peer1, 7, heldTeardownRequest->getDialogToken(), 0); + heldTeardownHcf.processReceivedManagementFrame(heldTeardownResponse); + ASSERT(transitionListener.signals == std::vector({ Hcf::blockAckAgreementAddedSignal, Hcf::blockAckAgreementDeletedSignal })); + ASSERT(heldTeardownOriginatorHandler->getAgreement(peer1, 7) == nullptr); + ASSERT(heldTeardownHcf.numRebuildEligibilityCalls > 0); + ASSERT(heldTeardownDataService.isFrameEligible(heldDataPacket)); + ASSERT(heldTeardownHcf.hasFrame(AC_BK)); + ASSERT(heldTeardownQueue.getNumPackets() == 2); + auto heldDelbaPacket = heldTeardownQueue.getPacket(1); + auto heldDelba = dynamicPtrCast(heldDelbaPacket->peekAtFront()); + ASSERT(heldDelba != nullptr); + ASSERT(heldTeardownOriginatorHandler->isDelbaPending(heldDelbaPacket, heldDelba)); + OriginatorQosAckPolicy heldTeardownAckPolicy; + ASSERT(heldTeardownAckPolicy.computeAckPolicy(heldDataPacket, heldDataPacket->peekAtFront(), heldTeardownOriginatorHandler->getAgreement(peer1, 7)) == NORMAL_ACK); + heldTeardownOriginatorHandler->allowRetryNow(peer1, 7); + TestCallback heldTeardownRetryCallback; + Packet heldTeardownRetryTrigger("heldTeardownRetryTrigger"); + auto heldTeardownRetryHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(212)); + auto obsoleteHeldTeardownTransactionId = heldTeardownOriginatorHandler->processAcknowledgedDataFrame(&heldTeardownRetryTrigger, heldTeardownRetryHeader, &heldTeardownOriginatorPolicy, &heldTeardownRetryCallback); + ASSERT(heldTeardownRetryCallback.managementPacket != nullptr); + ASSERT(obsoleteHeldTeardownTransactionId == heldDelbaPacket->getTag()->getTransactionId()); + heldTeardownHcf.cancelTransaction(obsoleteHeldTeardownTransactionId); + ASSERT(heldTeardownQueue.getNumPackets() == 1); + ASSERT(heldTeardownQueue.getPacket(0) == heldDataPacket); + heldTeardownHcf.rebuildEligibilityIndex(); + heldTeardownHcf.unsubscribe(Hcf::blockAckAgreementAddedSignal, &transitionListener); + heldTeardownHcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &transitionListener); + while (!heldTeardownQueue.isEmpty()) { + auto packet = heldTeardownQueue.getPacket(0); + heldTeardownQueue.removePacket(packet); + delete packet; + } +} + +// Synchronous DELBA drop/removal clears the separate teardown transaction, and +// the already-completed agreement transition remains idempotent. +for (auto removalReason : { queueing::IPacketQueue::PacketRemovalReason::DROPPED, queueing::IPacketQueue::PacketRemovalReason::REMOVED }) { + TestImmediateRemovalQueue teardownQueue; + teardownQueue.removalReason = removalReason; + TestOriginatorQosMacDataService teardownDataService; + teardownDataService.setFrameEligibilityFunction([](const Packet *) { return true; }); + QosAckHandler teardownAckHandler; + TestInProgressFrames teardownInProgressFrames; + teardownInProgressFrames.configure(&teardownDataService, &teardownAckHandler, &teardownQueue); + TestEdcaf teardownEdcaf; + teardownEdcaf.pendingQueue = &teardownQueue; + teardownEdcaf.inProgressFrames = &teardownInProgressFrames; + teardownEdcaf.qosAckHandler = &teardownAckHandler; + TestEdca teardownEdca; + teardownEdca.edcaf = &teardownEdcaf; + auto teardownOriginatorHandler = new TestOriginatorHandler(); + auto teardownRecipientHandler = new RecipientBlockAckAgreementHandler(); + TestOriginatorPolicy teardownOriginatorPolicy; + teardownOriginatorPolicy.addbaAccepted = false; + TestRecipientPolicy teardownRecipientPolicy; + TestHcf teardownHcf; + teardownHcf.configureBlockAckHandlers(teardownOriginatorHandler, &teardownOriginatorPolicy, teardownRecipientHandler, &teardownRecipientPolicy); + teardownHcf.configureEligibilityIndex(&teardownEdca, &teardownDataService); + teardownHcf.observePendingQueue(&teardownQueue); + TestCallback teardownCallback; + Packet teardownTrigger("teardownTrigger"); + auto teardownTriggerHeader = makeQosHeader(peer1, removalReason == queueing::IPacketQueue::PacketRemovalReason::DROPPED ? 8 : 9, SequenceNumberCyclic(0)); + teardownOriginatorHandler->processAcknowledgedDataFrame(&teardownTrigger, teardownTriggerHeader, &teardownOriginatorPolicy, &teardownCallback); + auto teardownRequest = dynamicPtrCast(teardownCallback.managementHeader); + teardownOriginatorHandler->processTransmittedAddbaReq(teardownCallback.managementPacket, teardownRequest, &teardownOriginatorPolicy, &teardownCallback); + auto teardownResponse = makeResponse(peer1, teardownTriggerHeader->getTid(), teardownRequest->getDialogToken(), 0); + TestSignalListener addedListener; + TestSignalListener deletedListener; + teardownHcf.subscribe(Hcf::blockAckAgreementAddedSignal, &addedListener); + teardownHcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletedListener); + teardownHcf.processReceivedManagementFrame(teardownResponse); + ASSERT(addedListener.numSignals == 1); + ASSERT(deletedListener.numSignals == 1); + ASSERT(teardownOriginatorHandler->getAgreement(peer1, teardownTriggerHeader->getTid()) == nullptr); + ASSERT(teardownHcf.getNumTrackedPendingFrames() == 0); + TestCallback teardownRetryCallback; + Packet teardownRetryTrigger("teardownRetryTrigger"); + auto teardownRetryHeader = makeQosHeader(peer1, teardownTriggerHeader->getTid(), SequenceNumberCyclic(1)); + teardownOriginatorHandler->processAcknowledgedDataFrame(&teardownRetryTrigger, teardownRetryHeader, &teardownOriginatorPolicy, &teardownRetryCallback); + ASSERT(teardownRetryCallback.managementPacket == nullptr); + teardownOriginatorHandler->allowRetryNow(peer1, teardownTriggerHeader->getTid()); + teardownOriginatorHandler->processAcknowledgedDataFrame(&teardownRetryTrigger, teardownRetryHeader, &teardownOriginatorPolicy, &teardownRetryCallback); + ASSERT(teardownRetryCallback.managementPacket != nullptr); + teardownHcf.unsubscribe(Hcf::blockAckAgreementAddedSignal, &addedListener); + teardownHcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletedListener); +} + +OriginatorQosMacDataService dataService; +dataService.setFrameEligibilityFunction([&handler](const Packet *packet) { + auto header = dynamicPtrCast(packet->peekAtFront()); + return header == nullptr || header->getType() != ST_DATA_WITH_QOS || !handler.isAddbaResponsePending(header->getReceiverAddress(), header->getTid()); +}); +Packet heldFrame("heldFrame", makeQosHeader(peer3, 5, SequenceNumberCyclic(0))); +Packet otherTidFrame("otherTidFrame", makeQosHeader(peer3, 4, SequenceNumberCyclic(0))); +Packet otherPeerFrame("otherPeerFrame", makeQosHeader(peer2, 5, SequenceNumberCyclic(0))); +auto managementHeader = makeShared(); +managementHeader->setType(ST_ACTION); +Packet managementFrame("managementFrame", managementHeader); +TestPacketQueue mixedQueue; +mixedQueue.packets = { &heldFrame, &otherTidFrame, &otherPeerFrame, &managementFrame }; +ASSERT(!dataService.isFrameEligible(&heldFrame)); +ASSERT(dataService.isFrameEligible(&otherTidFrame)); +ASSERT(dataService.isFrameEligible(&otherPeerFrame)); +ASSERT(dataService.isFrameEligible(&managementFrame)); +ASSERT(dataService.hasEligibleFrame(&mixedQueue)); +mixedQueue.packets = { &heldFrame }; +ASSERT(!dataService.hasEligibleFrame(&mixedQueue)); + +// HCF availability is a count lookup: rebuilding evaluates each pending frame +// once, while repeated availability checks neither enumerate queues nor +// reevaluate the Block Ack eligibility predicate. +{ + TestOriginatorQosMacDataService indexedDataService; + bool addbaResponsePending = true; + int numEligibilityChecks = 0; + Packet *setupFrame = nullptr; + indexedDataService.setFrameEligibilityFunction([&](const Packet *packet) { + numEligibilityChecks++; + return !addbaResponsePending || packet == setupFrame; + }); + TestAckHandler indexedAckHandler; + TestPacketQueue pendingQueues[AC_NUMCATEGORIES]; + TestInProgressFrames inProgressFrames[AC_NUMCATEGORIES]; + TestEdcaf edcafs[AC_NUMCATEGORIES]; + TestEdca indexedEdca; + indexedEdca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + inProgressFrames[ac].configure(&indexedDataService, &indexedAckHandler); + edcafs[ac].pendingQueue = &pendingQueues[ac]; + edcafs[ac].inProgressFrames = &inProgressFrames[ac]; + indexedEdca.edcafs[ac] = &edcafs[ac]; + } + std::vector indexedPackets; + for (int i = 0; i < 128; i++) { + auto packet = new Packet("heldIndexedFrame", makeShared(B(1))); + indexedPackets.push_back(packet); + pendingQueues[AC_BK].enqueuePacket(packet); + } + auto voiceFrame = new Packet("eligibleVoiceFrame", makeShared(B(1))); + indexedPackets.push_back(voiceFrame); + pendingQueues[AC_VO].enqueuePacket(voiceFrame); + TestHcf indexedHcf; + indexedHcf.configureEligibilityIndex(&indexedEdca, &indexedDataService); + indexedHcf.rebuildEligibilityIndex(); + auto requireIndex = [](bool condition, const char *message) { + if (!condition) + throw cRuntimeError("Eligibility index test failed: %s", message); + }; + requireIndex(indexedHcf.getNumTrackedPendingFrames() == 129, "initial cardinality"); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_BK) == 0, "initial background count"); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_VO) == 0, "initial voice count"); + int eligibilityChecksAfterRebuild = numEligibilityChecks; + int queueReadsAfterRebuild = 0; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + queueReadsAfterRebuild += pendingQueues[ac].numGetPacketCalls; + bool allHeldQueriesUnavailable = true; + for (int i = 0; i < 1000; i++) { + bool hasBackgroundFrame = indexedHcf.hasFrame(AC_BK); + bool hasVoiceFrame = indexedHcf.hasFrame(AC_VO); + allHeldQueriesUnavailable &= !hasBackgroundFrame && !hasVoiceFrame; + } + requireIndex(allHeldQueriesUnavailable, "all-held availability"); + ASSERT(allHeldQueriesUnavailable); + bool predicateWasNotReevaluated = numEligibilityChecks == eligibilityChecksAfterRebuild; + requireIndex(predicateWasNotReevaluated, "availability reevaluated the predicate"); + ASSERT(predicateWasNotReevaluated); + int queueReadsAfterQueries = 0; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + queueReadsAfterQueries += pendingQueues[ac].numGetPacketCalls; + bool queueWasNotEnumerated = queueReadsAfterQueries == queueReadsAfterRebuild; + requireIndex(queueWasNotEnumerated, "availability enumerated a pending queue"); + ASSERT(queueWasNotEnumerated); + + auto originalSetupFrame = new Packet("eligibleSetupFrame", makeShared(B(1))); + setupFrame = originalSetupFrame; + indexedPackets.push_back(originalSetupFrame); + indexedHcf.trackFrame(originalSetupFrame, AC_BK); + pendingQueues[AC_BK].enqueuePacket(originalSetupFrame); + bool hasSetupFrame = indexedHcf.hasFrame(AC_BK); + requireIndex(hasSetupFrame, "tracked setup frame unavailable"); + auto replacementFrame = new Packet("eligibleReplacementFrame", makeShared(B(1))); + setupFrame = replacementFrame; + indexedPackets.push_back(replacementFrame); + indexedHcf.trackFrame(replacementFrame, AC_BK); + pendingQueues[AC_BK].enqueuePacket(replacementFrame); + indexedHcf.untrackFrame(indexedPackets.front()); + pendingQueues[AC_BK].removePacket(indexedPackets.front()); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_BK) == 2, "existing-victim count"); + auto tailDroppedArrival = new Packet("tailDroppedArrival", makeShared(B(1))); + indexedHcf.trackFrame(tailDroppedArrival, AC_BK); + indexedHcf.untrackFrame(tailDroppedArrival); + indexedHcf.untrackFrame(tailDroppedArrival); + delete tailDroppedArrival; + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_BK) == 2, "tail-drop count"); + indexedHcf.untrackFrame(replacementFrame); + pendingQueues[AC_BK].removePacket(replacementFrame); + indexedHcf.untrackFrame(originalSetupFrame); + pendingQueues[AC_BK].removePacket(originalSetupFrame); + bool hasFrameAfterSelectedRemovals = indexedHcf.hasFrame(AC_BK); + requireIndex(!hasFrameAfterSelectedRemovals, "selected removals left availability"); + + addbaResponsePending = false; + indexedHcf.rebuildEligibilityIndex(); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_BK) == pendingQueues[AC_BK].getNumPackets(), "terminal-transition background count"); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_VO) == 1, "terminal-transition voice count"); + addbaResponsePending = true; + setupFrame = nullptr; + indexedHcf.rebuildEligibilityIndex(); + bool hasBackgroundFrameAfterCreation = indexedHcf.hasFrame(AC_BK); + bool hasVoiceFrameAfterCreation = indexedHcf.hasFrame(AC_VO); + requireIndex(!hasBackgroundFrameAfterCreation && !hasVoiceFrameAfterCreation, "creation-transition availability"); + auto inProgressFrame = new Packet("inProgressOnlyFrame", makeQosHeader(peer1, 3, SequenceNumberCyclic(1))); + setupFrame = inProgressFrame; + inProgressFrames[AC_VI].addFrame(inProgressFrame); + bool hasInProgressFrame = indexedHcf.hasFrame(AC_VI); + requireIndex(hasInProgressFrame, "eligible in-progress frame unavailable"); + requireIndex(indexedHcf.getNumTrackedPendingFrames() == 128, "in-progress frame entered pending index"); + auto releasedFrames = inProgressFrames[AC_VI].releaseFrames(); + requireIndex(releasedFrames.size() == 1, "in-progress release count"); + delete releasedFrames.front(); + setupFrame = nullptr; + bool hasFrameAfterInProgressRelease = indexedHcf.hasFrame(AC_VI); + requireIndex(!hasFrameAfterInProgressRelease, "released in-progress frame remained available"); + + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + pendingQueues[ac].removeAllPackets(); + for (auto packet : indexedPackets) + delete packet; +} + +// IEEE Std 802.11-2024, 10.23.2.4: after the current owner releases the +// channel, Hcf re-arms every AC that has an eligible frame, and no idle AC. +{ + TestOriginatorQosMacDataService armingDataService; + armingDataService.setFrameEligibilityFunction([](const Packet *) { return true; }); + TestAckHandler armingAckHandler; + TestPacketQueue armingPendingQueues[AC_NUMCATEGORIES]; + TestInProgressFrames armingInProgressFrames[AC_NUMCATEGORIES]; + TestEdcaf armingEdcafs[AC_NUMCATEGORIES]; + TestEdca armingEdca; + armingEdca.numEdcafs = AC_NUMCATEGORIES; + armingEdca.edcaf = nullptr; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + armingInProgressFrames[ac].configure(&armingDataService, &armingAckHandler, &armingPendingQueues[ac]); + armingEdcafs[ac].pendingQueue = &armingPendingQueues[ac]; + armingEdcafs[ac].inProgressFrames = &armingInProgressFrames[ac]; + armingEdca.edcafs[ac] = &armingEdcafs[ac]; + } + auto backgroundFrame = new Packet("armingBackgroundFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(1))); + auto voiceFrame = new Packet("armingVoiceFrame", makeQosHeader(peer1, 2, SequenceNumberCyclic(1))); + armingInProgressFrames[AC_BK].addOwnedFrame(backgroundFrame); + armingInProgressFrames[AC_VO].addOwnedFrame(voiceFrame); + TestHcf armingHcf; + armingHcf.configureEligibilityIndex(&armingEdca, &armingDataService); + armingHcf.requestEligible(); + ASSERT(armingEdca.requestedAccessCategories.size() == 2); + ASSERT(armingEdca.requestedAccessCategories[0] == AC_BK); + ASSERT(armingEdca.requestedAccessCategories[1] == AC_VO); + auto releasedBackground = armingInProgressFrames[AC_BK].releaseFrames(); + auto releasedVoice = armingInProgressFrames[AC_VO].releaseFrames(); + for (auto frame : releasedBackground) + delete frame; + for (auto frame : releasedVoice) + delete frame; +} + +// The frame-sequence boundary releases the owner and ends its TXOP before +// re-arming eligible ACs; an already-active AC receives the callback without +// restarting contention. +{ + TestOriginatorQosMacDataService sequenceDataService; + sequenceDataService.setFrameEligibilityFunction([](const Packet *) { return true; }); + TestAckHandler sequenceAckHandler; + TestPacketQueue sequencePendingQueues[AC_NUMCATEGORIES]; + TestInProgressFrames sequenceInProgressFrames[AC_NUMCATEGORIES]; + TestEdcaf sequenceEdcafs[AC_NUMCATEGORIES]; + TestEdca sequenceEdca; + sequenceEdca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + sequenceInProgressFrames[ac].configure(&sequenceDataService, &sequenceAckHandler, &sequencePendingQueues[ac]); + sequenceEdcafs[ac].pendingQueue = &sequencePendingQueues[ac]; + sequenceEdcafs[ac].inProgressFrames = &sequenceInProgressFrames[ac]; + sequenceEdcafs[ac].setAccessCategory(AccessCategory(ac)); + sequenceEdca.edcafs[ac] = &sequenceEdcafs[ac]; + } + sequenceEdca.edcaf = &sequenceEdcafs[AC_BE]; + sequenceInProgressFrames[AC_BK].addFrame(new Packet("sequenceBackgroundFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(70)))); + sequenceInProgressFrames[AC_VO].addFrame(new Packet("sequenceVoiceFrame", makeQosHeader(peer1, 2, SequenceNumberCyclic(71)))); + sequenceEdcafs[AC_VO].contentionActive = true; + std::vector eventOrder; + sequenceEdcafs[AC_BE].eventOrder = &eventOrder; + TestTxopProcedure sequenceTxop; + sequenceTxop.eventOrder = &eventOrder; + sequenceEdcafs[AC_BE].setTxopProcedure(&sequenceTxop); + sequenceEdca.eventOrder = &eventOrder; + TestHcf sequenceHcf; + sequenceHcf.configureEligibilityIndex(&sequenceEdca, &sequenceDataService); + auto sequenceHandler = new TestFrameSequenceHandler(); + sequenceHcf.configureFrameSequenceHandler(sequenceHandler); + TestMac sequenceMac; + sequenceHcf.configureMac(&sequenceMac); + sequenceHcf.finishSequence(); + ASSERT(eventOrder == std::vector({ 1, 2, 3, 3 })); + ASSERT(sequenceEdca.requestedAccessCategories.size() == 2); + ASSERT(sequenceEdca.requestedAccessCategories[0] == AC_BK); + ASSERT(sequenceEdca.requestedAccessCategories[1] == AC_VO); + ASSERT(sequenceEdcafs[AC_VO].numRequestChannelCalls == 1); + ASSERT(sequenceEdcafs[AC_VO].numContentionStarts == 0); + auto releasedBackground = sequenceInProgressFrames[AC_BK].releaseFrames(); + auto releasedVoice = sequenceInProgressFrames[AC_VO].releaseFrames(); + for (auto frame : releasedBackground) + delete frame; + for (auto frame : releasedVoice) + delete frame; +} + +// IEEE Std 802.11-2024, 10.23.2.4: a collided AC with no eligible frame is +// not a handled collision and must not mutate recovery state or be counted. +{ + TestEdcaf emptyCollidedEdcaf; + emptyCollidedEdcaf.setAccessCategory(AC_BK); + TestInProgressFrames emptyInProgressFrames; + TestOriginatorQosMacDataService emptyDataService; + TestAckHandler emptyAckHandler; + TestPacketQueue emptyPendingQueue; + emptyInProgressFrames.configure(&emptyDataService, &emptyAckHandler, &emptyPendingQueue); + emptyCollidedEdcaf.inProgressFrames = &emptyInProgressFrames; + TestHcf collisionHcf; + ASSERT(collisionHcf.processInternalCollision({ &emptyCollidedEdcaf }) == 0); +} + +// A channel grant whose collision-controller vector contains only an AC with +// no eligible frame releases the channel without opening a TXOP or emitting a +// phantom collision statistic. +{ + TestPacketQueue grantedPendingQueue; + TestOriginatorQosMacDataService grantedDataService; + TestAckHandler grantedAckHandler; + TestInProgressFrames grantedInProgressFrames; + grantedInProgressFrames.configure(&grantedDataService, &grantedAckHandler, &grantedPendingQueue); + TestEdcaf grantedEdcaf; + grantedEdcaf.setAccessCategory(AC_BE); + grantedEdcaf.pendingQueue = &grantedPendingQueue; + grantedEdcaf.inProgressFrames = &grantedInProgressFrames; + TestEdca grantedEdca; + grantedEdca.edcaf = &grantedEdcaf; + grantedEdca.internallyCollidedEdcafs = { &grantedEdcaf }; + TestHcf grantedHcf; + grantedHcf.configure(&grantedEdca); + TestMac grantedMac; + grantedHcf.configureMac(&grantedMac); + TestSignalListener collisionListener; + grantedHcf.subscribe(Hcf::edcaCollisionDetectedSignal, &collisionListener); + grantedHcf.grantChannel(&grantedEdcaf); + ASSERT(grantedEdcaf.numReleaseChannelCalls == 1); + ASSERT(grantedMac.numPendingRadioConfigSends == 1); + ASSERT(collisionListener.numSignals == 0); + grantedHcf.unsubscribe(Hcf::edcaCollisionDetectedSignal, &collisionListener); +} + // A direct IPacketQueue implementation without OMNeT++ signals cannot leave // the index stale because ownership departures use the typed queue callback. { @@ -2065,6 +3197,192 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); secondQueue.removePacket(otherInputPacket); delete otherInputPacket; } + +// Discarding a stale setup frame clears its per-frame retry state without +// emitting the retry-limit semantic event. +{ + TestNonQosRecoveryProcedure recoveryProcedure; + TestSignalListener retryLimitListener; + recoveryProcedure.subscribe(IRecoveryProcedure::retryLimitReachedSignal, &retryLimitListener); + auto header = makeShared(); + header->setSequenceNumber(SequenceNumberCyclic(42)); + Packet packet("staleRecoveryPacket", header); + recoveryProcedure.addRetry(header); + ASSERT(recoveryProcedure.hasRetry(header)); + recoveryProcedure.discardFrame(&packet, header); + ASSERT(!recoveryProcedure.hasRetry(header)); + ASSERT(retryLimitListener.numSignals == 0); + recoveryProcedure.unsubscribe(IRecoveryProcedure::retryLimitReachedSignal, &retryLimitListener); +} + + +// Either terminal discard path clears both per-frame retry maps. Reusing the +// same sequence/fragment identity therefore starts from zero after either +// data-first or RTS-first failure history. +{ + TestNonQosRecoveryProcedure recoveryProcedure; + auto header = makeShared(); + header->setSequenceNumber(SequenceNumberCyclic(43)); + Packet shortPacket("shortTerminalDiscard", header); + recoveryProcedure.addShortRetry(header, 2); + recoveryProcedure.addLongRetry(header, 3); + ASSERT(recoveryProcedure.getShortRetryCount(&shortPacket, header) == 2); + ASSERT(recoveryProcedure.getLongRetryCount(&shortPacket, header) == 3); + recoveryProcedure.discardFrame(&shortPacket, header); + ASSERT(recoveryProcedure.getShortRetryCount(&shortPacket, header) == 0); + ASSERT(recoveryProcedure.getLongRetryCount(&shortPacket, header) == 0); + + Packet reusedPacket("reusedTerminalDiscard", header); + recoveryProcedure.addLongRetry(header, 4); + recoveryProcedure.addShortRetry(header, 5); + ASSERT(recoveryProcedure.getShortRetryCount(&reusedPacket, header) == 5); + ASSERT(recoveryProcedure.getLongRetryCount(&reusedPacket, header) == 4); + recoveryProcedure.discardRtsFrame(header); + ASSERT(recoveryProcedure.getShortRetryCount(&reusedPacket, header) == 0); + ASSERT(recoveryProcedure.getLongRetryCount(&reusedPacket, header) == 0); +} + +// A stale RTS-protected ADDBA request uses the management header for its retry +// check and clears the RTS short-retry entry even when the protected frame is +// long. It is a transaction discard, not a retry-limit or link-break event. +{ + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(10, 4, &cwCalculator); + QosAckHandler ackHandler; + TestOriginatorQosMacDataService originatorDataService; + TestInProgressFrames activeFrames; + activeFrames.configure(&originatorDataService, &ackHandler); + TestEdcaf edcaf; + edcaf.inProgressFrames = &activeFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + auto staleHandler = new TestOriginatorHandler(); + TestHcf hcf; + hcf.configureTransmittedData(&edca, staleHandler, &originatorPolicy); + TestSignalListener packetDropListener; + TestSignalListener linkBrokenListener; + hcf.subscribe(packetDroppedSignal, &packetDropListener); + hcf.subscribe(linkBrokenSignal, &linkBrokenListener); + auto request = makeShared(); + request->setReceiverAddress(peer1); + request->setTid(3); + request->setDialogToken(91); + request->setSequenceNumber(SequenceNumberCyclic(77)); + auto packet = new Packet("staleRtsProtectedAddba", request); + packet->insertAtBack(makeShared(B(100))); + ASSERT(packet->getByteLength() >= 10); + activeFrames.addFrame(packet); + ackHandler.processTransmittedDataOrMgmtFrame(request); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(request) == QosAckHandler::Status::WAITING_FOR_NORMAL_ACK); + hcf.processRtsProtectionFailure(packet); + ASSERT(recoveryProcedure.getShortRetryCount(packet, request) == 0); + ASSERT(recoveryProcedure.getLongRetryCount(packet, request) == 0); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(request) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(packetDropListener.numSignals == 1); + ASSERT(linkBrokenListener.numSignals == 0); + hcf.unsubscribe(packetDroppedSignal, &packetDropListener); + hcf.unsubscribe(linkBrokenSignal, &linkBrokenListener); + auto releasedFrames = activeFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 1); + delete releasedFrames.front(); +} + +// At the RTS retry limit, the same long protected management frame still +// clears its short-retry entry and emits each terminal event exactly once. +{ + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(10, 1, &cwCalculator); + QosAckHandler ackHandler; + TestOriginatorQosMacDataService originatorDataService; + TestInProgressFrames activeFrames; + activeFrames.configure(&originatorDataService, &ackHandler); + TestEdcaf edcaf; + edcaf.inProgressFrames = &activeFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + TestHcf hcf; + TestMac retryLimitedRtsProtectedAddbaMac; + hcf.configureMac(&retryLimitedRtsProtectedAddbaMac); + hcf.configureTransmittedData(&edca, new TestOriginatorHandler(), &originatorPolicy); + TestSignalListener retryLimitListener; + TestSignalListener packetDropListener; + TestSignalListener linkBrokenListener; + recoveryProcedure.subscribe(IRecoveryProcedure::retryLimitReachedSignal, &retryLimitListener); + hcf.subscribe(packetDroppedSignal, &packetDropListener); + hcf.subscribe(linkBrokenSignal, &linkBrokenListener); + auto request = makeShared(); + request->setReceiverAddress(peer1); + request->setTid(3); + request->setDialogToken(92); + request->setSequenceNumber(SequenceNumberCyclic(78)); + auto packet = new Packet("retryLimitedRtsProtectedAddba", request); + packet->insertAtBack(makeShared(B(100))); + ASSERT(packet->getByteLength() >= 10); + activeFrames.addFrame(packet); + ackHandler.processTransmittedDataOrMgmtFrame(request); + hcf.processRtsProtectionFailure(packet); + ASSERT(recoveryProcedure.getShortRetryCount(packet, request) == 0); + ASSERT(recoveryProcedure.getLongRetryCount(packet, request) == 0); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(request) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(retryLimitListener.numSignals == 1); + ASSERT(packetDropListener.numSignals == 1); + ASSERT(linkBrokenListener.numSignals == 1); + recoveryProcedure.unsubscribe(IRecoveryProcedure::retryLimitReachedSignal, &retryLimitListener); + hcf.unsubscribe(packetDroppedSignal, &packetDropListener); + hcf.unsubscribe(linkBrokenSignal, &linkBrokenListener); + auto releasedFrames = activeFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 1); + delete releasedFrames.front(); +} + +// A frame prefetched for duration calculation already has an SSN, but it must +// remain in progress without being selected while the same transaction is pending. +TestAckHandler ackHandler; +TestInProgressFrames inProgressFrames; +inProgressFrames.configure(&dataService, &ackHandler); +auto prefetchedHeldFrame = new Packet("prefetchedHeldFrame", makeQosHeader(peer3, 5, SequenceNumberCyclic(21))); +auto prefetchedOtherTidFrame = new Packet("prefetchedOtherTidFrame", makeQosHeader(peer3, 4, SequenceNumberCyclic(1))); +inProgressFrames.addFrame(prefetchedHeldFrame); +inProgressFrames.addFrame(prefetchedOtherTidFrame); +ASSERT(inProgressFrames.hasEligibleFrame()); +ASSERT(inProgressFrames.getFrameToTransmit() == prefetchedOtherTidFrame); + +// Repeated cleanup requests move an in-progress frame to the discard list once. +{ + TestInProgressFrames idempotentDropFrames; + auto frame = new Packet("idempotentDropFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(22))); + idempotentDropFrames.addFrame(frame); + idempotentDropFrames.dropFrame(frame); + idempotentDropFrames.dropFrame(frame); + auto releasedFrames = idempotentDropFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 1); + ASSERT(releasedFrames.front() == frame); + delete frame; +} + +// DCF sequence predicates materialize the next eligible pending frame. +TestPacketQueue deferredQueue; +auto deferredFrame = new Packet("deferredFrame", makeQosHeader(peer2, 4, SequenceNumberCyclic(123))); +deferredQueue.packets = { deferredFrame }; +TestOriginatorQosMacDataService deferredDataService; +TestInProgressFrames deferredInProgressFrames; +deferredInProgressFrames.configure(&deferredDataService, &ackHandler, &deferredQueue); +bool hasDeferredInProgressFrame = deferredInProgressFrames.hasInProgressFrames(); +if (!hasDeferredInProgressFrame || deferredQueue.getNumPackets() != 0) + throw cRuntimeError("In-progress frame predicate did not materialize the eligible pending frame"); +ASSERT(hasDeferredInProgressFrame); +ASSERT(deferredQueue.getNumPackets() == 0); +ASSERT(deferredFrame->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(123)); +auto deferredFrames = deferredInProgressFrames.releaseFrames(); +ASSERT(deferredFrames.size() == 1); +delete deferredFrames.at(0); + // A real queue with a downstream collector predicate-dequeues a non-front // packet, records its residence and semantic pull once, and animates it to the // collector while retaining the front packet. @@ -2550,6 +3868,99 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); secondQueue->deleteModule(); bufferModule->deleteModule(); } + +// The multicast continuation predicate materializes its candidate once and +// reuses that result for both the TXOP and receiver-address decisions. +{ + TestOriginatorQosMacDataService multicastDataService; + TestPacketQueue multicastPendingQueue; + TestAckHandler multicastAckHandler; + auto multicastFrame = new Packet("multicastTxopFrame", makeQosHeader(MacAddress::BROADCAST_ADDRESS, 1, SequenceNumberCyclic(4))); + multicastPendingQueue.packets = { multicastFrame }; + TestInProgressFrames multicastInProgressFrames; + multicastInProgressFrames.configure(&multicastDataService, &multicastAckHandler, &multicastPendingQueue); + TestTxopProcedure multicastTxopProcedure; + FrameSequenceContext multicastContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &multicastInProgressFrames, nullptr, nullptr, nullptr, new QoSContext(nullptr, nullptr, nullptr, &multicastTxopProcedure)); + HcfFs multicastHcfFs; + RepeatingFs multicastRepeatingFs(new TestEmptyFrameSequence(), [](RepeatingFs *, FrameSequenceContext *) { return false; }); + ASSERT(multicastHcfFs.hasMoreTxOpsAndMulticast(&multicastRepeatingFs, &multicastContext)); + ASSERT(multicastPendingQueue.numSelectedDequeues == 1); + ASSERT(multicastPendingQueue.getNumPackets() == 0); + auto multicastFrames = multicastInProgressFrames.releaseFrames(); + ASSERT(multicastFrames == std::vector({ multicastFrame })); + delete multicastFrame; +} + +// At the TXOP continuation boundary, an eligible pending frame is +// materialized by the real repeating-sequence lifecycle. +{ + TestOriginatorQosMacDataService txopDataService; + TestPacketQueue txopPendingQueue; + auto initialTxopFrame = new Packet("initialTxopFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(2))); + auto continuedTxopFrame = new Packet("continuedTxopFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(3))); + txopPendingQueue.packets = { continuedTxopFrame }; + TestInProgressFrames txopInProgressFrames; + txopInProgressFrames.configure(&txopDataService, &ackHandler, &txopPendingQueue); + txopInProgressFrames.addFrame(initialTxopFrame); + TestTxopProcedure txopProcedure; + FrameSequenceContext txopContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &txopInProgressFrames, nullptr, nullptr, nullptr, new QoSContext(nullptr, nullptr, nullptr, &txopProcedure)); + HcfFs hcfFs; + RepeatingFs repeatingFs(new TestEmptyFrameSequence(), [&hcfFs](RepeatingFs *frameSequence, FrameSequenceContext *context) { return hcfFs.hasMoreTxOps(frameSequence, context); }); + repeatingFs.startSequence(&txopContext, 0); + ASSERT(repeatingFs.getCount() == 1); + ASSERT(txopPendingQueue.getNumPackets() == 1); + txopInProgressFrames.dropFrame(initialTxopFrame); + repeatingFs.repeatSequence(&txopContext); + ASSERT(repeatingFs.getCount() == 2); + ASSERT(txopPendingQueue.getNumPackets() == 0); + ASSERT(txopPendingQueue.numSelectedDequeues == 1); + ASSERT(txopInProgressFrames.getFrameToTransmit() == continuedTxopFrame); + auto txopFrames = txopInProgressFrames.releaseFrames(); + ASSERT(txopFrames.size() == 2); + for (auto frame : txopFrames) + delete frame; +} + +// Legacy DCF materializes its first queued frame before starting DcfFs, and a +// stale empty grant is released without dereferencing a null frame. +{ + TestOriginatorQosMacDataService dcfDataService; + TestPacketQueue dcfPendingQueue; + TestInProgressFrames dcfInProgressFrames; + dcfInProgressFrames.configure(&dcfDataService, &ackHandler, &dcfPendingQueue); + TestDcaf dcaf; + dcaf.configure(&dcfInProgressFrames); + TestDcf dcf; + TestMac dcfMac; + auto dcfHandler = dcf.configure(&dcaf, &dcfMac); + dcf.grant(&dcaf); + ASSERT(dcaf.numReleasedChannels == 1); + ASSERT(dcfMac.numPendingRadioConfigSends == 1); + ASSERT(dcfHandler->numStartedSequences == 0); +} +{ + TestOriginatorQosMacDataService dcfDataService; + TestPacketQueue dcfPendingQueue; + auto firstDcfFrame = new Packet("firstDcfFrame", makeQosHeader(peer1, 0, SequenceNumberCyclic(0))); + dcfPendingQueue.packets = { firstDcfFrame }; + TestInProgressFrames dcfInProgressFrames; + dcfInProgressFrames.configure(&dcfDataService, &ackHandler, &dcfPendingQueue); + TestDcaf dcaf; + dcaf.configure(&dcfInProgressFrames); + TestDcf dcf; + TestMac dcfMac; + auto dcfHandler = dcf.configure(&dcaf, &dcfMac); + dcf.grant(&dcaf); + ASSERT(dcaf.numReleasedChannels == 0); + ASSERT(dcfMac.numPendingRadioConfigSends == 0); + ASSERT(dcfHandler->numStartedSequences == 1); + ASSERT(dcfPendingQueue.getNumPackets() == 0); + ASSERT(dcfInProgressFrames.getFrameToTransmit() == firstDcfFrame); + auto dcfFrames = dcfInProgressFrames.releaseFrames(); + ASSERT(dcfFrames.size() == 1); + delete dcfFrames.at(0); +} + EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; %contains: stdout From 32b0dd7b4427bd34f7779faa39bc38358ff84720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:03 +0200 Subject: [PATCH 05/16] ieee80211: reassemble fragmented action frames before dispatch Carry Action-frame-specific context in a local tag while transmitting fragments with a generic management header and a serialized body. Extend the serializer, dissector, fragmentation, and defragmentation paths so the original Action header can be reconstructed after all fragments arrive. Route fragmented management frames through recipient reassembly and call ADDBA or DELBA handlers only with the complete header. This prevents a partial fragment from starting, changing, or tearing down a Block Ack agreement. Cover on-air representation, out-of-order fragments, duplicates, expiration, and both QoS and non-QoS recipient dispatch. --- .../ieee80211/mac/Ieee80211Frame.msg | 2 +- .../mac/Ieee80211MacHeaderSerializer.cc | 16 + .../mac/Ieee80211MacProtocolDissector.cc | 13 +- .../OriginatorBlockAckAgreementHandler.cc | 11 +- .../RecipientBlockAckAgreementHandler.cc | 4 + .../ieee80211/mac/contract/IReassembly.h | 18 +- .../contract/IRecipientQosMacDataService.h | 6 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 24 +- .../fragmentation/BasicFragmentationPolicy.cc | 28 +- .../mac/fragmentation/BasicReassembly.cc | 77 +++- .../mac/fragmentation/BasicReassembly.h | 18 +- .../mac/fragmentation/Defragmentation.cc | 14 +- .../mac/fragmentation/Fragmentation.cc | 55 ++- .../Ieee80211FragmentedActionContextTag.cc | 15 + .../Ieee80211FragmentedActionContextTag.h | 48 +++ .../mac/recipient/RecipientMacDataService.cc | 34 +- .../mac/recipient/RecipientMacDataService.h | 5 +- .../mac/recipient/RecipientMacDataService.ned | 2 +- .../recipient/RecipientQosMacDataService.cc | 61 ++- .../recipient/RecipientQosMacDataService.h | 4 + .../recipient/RecipientQosMacDataService.ned | 2 +- tests/unit/Ieee80211AddbaTransaction_1.test | 408 ++++++++++++++++-- 22 files changed, 769 insertions(+), 96 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.cc create mode 100644 src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg index 033ee7141be..184c836eb30 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg @@ -312,7 +312,7 @@ class Ieee80211AddbaResponse extends Ieee80211ActionFrame // class Ieee80211Delba extends Ieee80211ActionFrame { - chunkLength = LENGTH_DELBA; + chunkLength = LENGTH_DELBA - B(4); category = 3; // Category field is set to 3 (representing DELBA). (1 byte) blockAckAction = 2; // Block Ack Action field is set to 2 (representing DELBA). (1 byte) diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc index d6c90abeb3c..b9937549479 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc @@ -184,6 +184,10 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c stream.writeUint32Be(0); if (type == ST_ACTION) { auto actionFrame = dynamicPtrCast(chunk); + // A fragmented action MPDU uses a generic management header; + // its action-body slice is a separate packet chunk. + if (actionFrame == nullptr) + break; switch (actionFrame->getCategory()) { case 3: { stream.writeByte(actionFrame->getCategory()); @@ -423,6 +427,17 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre actionFrame->setSequenceNumber(sequenceNumber); if (order) stream.readUint32Be(); + if (actionFrame->getMoreFragments() || actionFrame->getFragmentNumber() != 0) { + auto mgmtHeader = makeShared(); + copyBasicFields(mgmtHeader, macHeader); + mgmtHeader->setDurationField(actionFrame->getDurationField()); + mgmtHeader->setReceiverAddress(actionFrame->getReceiverAddress()); + mgmtHeader->setTransmitterAddress(actionFrame->getTransmitterAddress()); + mgmtHeader->setAddress3(actionFrame->getAddress3()); + mgmtHeader->setFragmentNumber(actionFrame->getFragmentNumber()); + mgmtHeader->setSequenceNumber(actionFrame->getSequenceNumber()); + return mgmtHeader; + } actionFrame->setCategory(stream.readByte()); switch (actionFrame->getCategory()) { case 3: { @@ -460,6 +475,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre case 2: { auto delba = makeShared(); copyBasicFields(delba, macHeader); + copyActionFrameFields(delba, actionFrame); delba->setBlockAckAction(blockAckAction); delba->setReserved(stream.readNBitsToUint64Be(11)); delba->setInitiator(stream.readBit()); diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacProtocolDissector.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacProtocolDissector.cc index 36ec42ce51d..de27699aefd 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacProtocolDissector.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacProtocolDissector.cc @@ -65,10 +65,14 @@ void Ieee80211MacProtocolDissector::dissect(Packet *packet, const Protocol *prot else callback.dissectPacket(packet, computeLlcProtocol(packet)); } - else if (dynamicPtrCast(header)) - ASSERT(packet->getDataLength() == b(0)); - else if (dynamicPtrCast(header)) - callback.dissectPacket(packet, &Protocol::ieee80211Mgmt); + else if (auto mgmtHeader = dynamicPtrCast(header)) { + if (mgmtHeader->getMoreFragments() || mgmtHeader->getFragmentNumber() != 0) + callback.dissectPacket(packet, nullptr); + else if (dynamicPtrCast(header)) + ASSERT(packet->getDataLength() == b(0)); + else + callback.dissectPacket(packet, &Protocol::ieee80211Mgmt); + } // TODO else if (dynamicPtrCast(header)) else ASSERT(packet->getDataLength() == b(0)); @@ -77,4 +81,3 @@ void Ieee80211MacProtocolDissector::dissect(Packet *packet, const Protocol *prot } } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index d03bae7b498..2094435bf43 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -11,6 +11,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" namespace inet { @@ -338,7 +339,11 @@ void OriginatorBlockAckAgreementHandler::processDroppedAddbaReq(Packet *packet, std::unique_ptr OriginatorBlockAckAgreementHandler::processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - auto delba = packet->peekAtFront(); + auto delba = findFragmentedActionContext(packet); + // IEEE Std 802.11-2024, 10.4 and 11.5.3.2: an untagged DELBA MMPDU + // likewise cannot tear down the agreement before its final fragment. + if (delba->getMoreFragments()) + return nullptr; auto transactionTag = packet->findTag(); if (transactionTag != nullptr) { auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); @@ -364,7 +369,7 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - auto delba = packet->peekAtFront(); + auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator() || delba->getMoreFragments()) return false; auto transactionTag = packet->findTag(); @@ -381,7 +386,7 @@ bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet bool OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - auto delba = packet->peekAtFront(); + auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator()) return false; auto transactionTag = packet->findTag(); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 8956059d80c..bd4851bb0a6 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -172,6 +172,10 @@ void RecipientBlockAckAgreementHandler::processDuplicateAddbaRequest(const Ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) { + // IEEE Std 802.11-2024, 10.4 and 11.5.3.5: the DELBA MMPDU has not + // been transmitted while a later fragment is still outstanding. + if (delba->getMoreFragments()) + return nullptr; return std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); } diff --git a/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h index 23796ec3556..14285b3689a 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h @@ -8,9 +8,14 @@ #ifndef __INET_IREASSEMBLY_H #define __INET_IREASSEMBLY_H +#include + #include "inet/linklayer/common/MacAddress.h" namespace inet { + +class Packet; + namespace ieee80211 { class Ieee80211DataOrMgmtHeader; @@ -32,6 +37,18 @@ class INET_API IReassembly */ virtual Packet *addFragment(Packet *frame) = 0; + /** + * Return the earliest receive-lifetime deadline among incomplete frames, + * or SIMTIME_MAX when the reassembly buffer is empty. + */ + virtual simtime_t getNextExpirationTime() const = 0; + + /** + * Remove incomplete frames whose receive lifetime has elapsed and return + * their fragments to the caller for drop signaling and deletion. + */ + virtual std::vector removeExpiredFragments(simtime_t currentTime) = 0; + /** * Discard fragments from the reassembly buffer. Frames are identified by the transmitter * address, the TID, and the sequence number range [startSeqNumber, endSeqNumber[. @@ -44,4 +61,3 @@ class INET_API IReassembly } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h index 108a499727f..2e7436303de 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h @@ -18,11 +18,11 @@ namespace ieee80211 { class INET_API IRecipientQosMacDataService { public: - // A duplicate has already been consumed and must not be processed as a - // management body; the coordination function may apply a subtype-specific - // response rule using the duplicate flag. struct ManagementFrameReceptionResult { std::vector completeFrames; + // IEEE Std 802.11-2024, 10.5: a management body becomes available to + // the coordination function only after the complete MMPDU is present. + Ptr completeHeader; bool duplicate = false; }; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 3f8cefd9e20..37c11a6de03 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -17,6 +17,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" @@ -70,9 +71,9 @@ void Hcf::initialize(int stage) originatorBlockAckProcedure = new OriginatorBlockAckProcedure(); recipientBlockAckProcedure = new RecipientBlockAckProcedure(); originatorDataService->setFrameEligibilityFunction([this](const Packet *packet) { - if (auto addbaReq = dynamicPtrCast(packet->peekAtFront())) + if (auto addbaReq = findFragmentedActionContext(packet)) return originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq); - if (auto delba = dynamicPtrCast(packet->peekAtFront())) + if (auto delba = findFragmentedActionContext(packet)) return originatorBlockAckAgreementHandler->isDelbaPending(packet, delba); auto dataHeader = dynamicPtrCast(packet->peekAtFront()); // Hold this peer/TID while its ADDBA response is pending so no @@ -133,7 +134,7 @@ void Hcf::rebuildPendingFrameEligibility() bool Hcf::processDroppedBlockAckSetupFrame(Packet *packet) { if (originatorBlockAckAgreementHandler) { - auto addbaReq = dynamicPtrCast(packet->peekAtFront()); + auto addbaReq = findFragmentedActionContext(packet); if (addbaReq != nullptr && originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq)) { originatorBlockAckAgreementHandler->processDroppedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); rebuildPendingFrameEligibility(); @@ -146,7 +147,7 @@ bool Hcf::processDroppedBlockAckSetupFrame(Packet *packet) bool Hcf::processDroppedBlockAckTeardownFrame(Packet *packet) { if (originatorBlockAckAgreementHandler) { - auto delba = dynamicPtrCast(packet->peekAtFront()); + auto delba = findFragmentedActionContext(packet); if (delba != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this)) { rebuildPendingFrameEligibility(); return true; @@ -534,7 +535,8 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr(header)) { auto receptionResult = recipientDataService->managementFrameReceived(packet, mgmtHeader); sendUp(receptionResult.completeFrames); - recipientProcessReceivedManagementFrame(mgmtHeader, receptionResult.duplicate); + if (receptionResult.completeHeader != nullptr) + recipientProcessReceivedManagementFrame(receptionResult.completeHeader, receptionResult.duplicate); } else { // TODO else if (auto ctrlFrame = dynamic_cast(frame)) sendUp(recipientDataService->controlFrameReceived(packet, header, recipientBlockAckAgreementHandler)); @@ -657,7 +659,7 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) } else throw cRuntimeError("Unknown frame"); // TODO QoSDataFrame, NonQoSDataFrame - auto addbaRequest = dynamicPtrCast(protectedHeader); + auto addbaRequest = findFragmentedActionContext(packet); bool staleAddbaRequest = addbaRequest != nullptr && originatorBlockAckAgreementHandler != nullptr && !originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaRequest); if (retryLimitReached || staleAddbaRequest) { if (retryLimitReached) { @@ -733,13 +735,13 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< auto edcaf = edca->getEdcaf(ac); if (originatorAckPolicy->isAckNeeded(mgmtHeader)) edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(mgmtHeader); - if (auto addbaReq = dynamicPtrCast(mgmtHeader)) { + if (auto addbaReq = findFragmentedActionContext(packet)) { if (originatorBlockAckAgreementHandler) originatorBlockAckAgreementHandler->processTransmittedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); } - else if (dynamicPtrCast(mgmtHeader)) + else if (findFragmentedActionContext(packet)) ; // Recipient agreement was established when the successful response was formed. - else if (auto delba = dynamicPtrCast(mgmtHeader)) { + else if (auto delba = findFragmentedActionContext(packet)) { if (delba->getInitiator()) { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getReceiverAddress(), delba->getTid()); auto agreement = originatorBlockAckAgreementHandler->processTransmittedDelba(packet, this); @@ -807,7 +809,7 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) } else throw cRuntimeError("Unknown frame"); // TODO qos, nonqos - auto addbaRequest = dynamicPtrCast(failedHeader); + auto addbaRequest = findFragmentedActionContext(failedPacket); bool staleAddbaRequest = addbaRequest != nullptr && originatorBlockAckAgreementHandler != nullptr && !originatorBlockAckAgreementHandler->isAddbaRequestPending(failedPacket, addbaRequest); if (retryLimitReached || staleAddbaRequest) { if (retryLimitReached) { @@ -900,7 +902,7 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); - if (auto delba = dynamicPtrCast(lastTransmittedHeader)) { + if (auto delba = findFragmentedActionContext(lastTransmittedPacket)) { if (delba->getInitiator() && originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this)) rebuildPendingFrameEligibility(); } diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc index e47ce922aab..9af560f9b3e 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc @@ -29,23 +29,38 @@ std::vector BasicFragmentationPolicy::computeFragmentSizes(Packet *frame) std::vector sizes; int payloadLength = 0; int headerLength = 0; - // Mgmt frames don't have payload const auto& header = frame->peekAtFront(); + // IEEE Std 802.11-2024, 10.4: only individually addressed MPDUs + // carrying an MSDU or MMPDU are eligible for fragmentation. + if (header->getReceiverAddress().isMulticast()) + return {}; const auto& trailer = frame->peekAtBack(B(4)); int trailerLength = trailer->getChunkLength().get(); if (dynamicPtrCast(header)) { headerLength = header->getChunkLength().get(); payloadLength = frame->getByteLength() - headerLength - trailerLength; } + else if (dynamicPtrCast(header)) { + // Management subclasses currently combine the common MAC header + // and typed MMPDU body. Only the common header is repeated. + headerLength = makeShared()->getChunkLength().get(); + payloadLength = frame->getByteLength() - headerLength - trailerLength; + } else - headerLength = frame->getByteLength(); + return {}; int maxFragmentPayload = fragmentationThreshold - headerLength - trailerLength; + if (maxFragmentPayload <= 0) + throw cRuntimeError("Fragmentation threshold %d is not larger than the %d byte header and trailer", fragmentationThreshold, headerLength + trailerLength); + // IEEE Std 802.11-2024, 10.4: all non-final fragments have the same + // even number of body octets; only the final fragment may be odd. + maxFragmentPayload &= ~1; + if (maxFragmentPayload == 0) + throw cRuntimeError("Fragmentation threshold %d leaves no even-length fragment body", fragmentationThreshold); if (payloadLength > maxFragmentPayload * MAX_NUM_FRAGMENTS) throw cRuntimeError("Fragmentation: frame \"%s\" too large, won't fit into %d fragments", frame->getName(), MAX_NUM_FRAGMENTS); - for (int i = 0; headerLength + trailerLength + payloadLength > fragmentationThreshold; i++) { - auto size = fragmentationThreshold - headerLength - trailerLength; - EV_TRACE << "Computed fragment: i = " << i << ", size = " << size << ".\n"; - sizes.push_back(size); + for (int i = 0; payloadLength > maxFragmentPayload; i++) { + EV_TRACE << "Computed fragment: i = " << i << ", size = " << maxFragmentPayload << ".\n"; + sizes.push_back(maxFragmentPayload); payloadLength -= maxFragmentPayload; } if (payloadLength != 0) { @@ -61,4 +76,3 @@ std::vector BasicFragmentationPolicy::computeFragmentSizes(Packet *frame) } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc index 2f32f0207b9..0b000dab927 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc @@ -26,12 +26,11 @@ Packet *BasicReassembly::addFragment(Packet *packet) // Frame is not fragmented if (!header->getMoreFragments() && header->getFragmentNumber() == 0) return packet; - // FIXME temporary fix for mgmt frames - if (dynamicPtrCast(header)) - return packet; // find entry for this frame Key key; key.macAddress = header->getTransmitterAddress(); + key.receiverAddress = header->getReceiverAddress(); + key.type = header->getType(); key.tid = -1; if (header->getType() == ST_DATA_WITH_QOS) if (const Ptr& qosDataHeader = dynamicPtrCast(header)) @@ -39,7 +38,25 @@ Packet *BasicReassembly::addFragment(Packet *packet) key.seqNum = header->getSequenceNumber().get(); short fragNum = header->getFragmentNumber(); ASSERT(fragNum >= 0 && fragNum < MAX_NUM_FRAGMENTS); - auto& value = fragmentsMap[key]; + auto it = fragmentsMap.find(key); + if (it != fragmentsMap.end() && it->second.expired) { + // A non-Retry fragment 0 can be a new MMPDU after sequence-number + // reuse. All other fragments of the expired MMPDU are discarded. + if (fragNum == 0 && !header->getRetry()) { + fragmentsMap.erase(it); + it = fragmentsMap.end(); + } + else { + delete packet; + return nullptr; + } + } + if (it == fragmentsMap.end()) { + Value value; + value.receptionStartTime = simTime(); + it = fragmentsMap.emplace(key, value).first; + } + auto& value = it->second; value.fragments.resize(16); // update entry @@ -71,28 +88,47 @@ Packet *BasicReassembly::addFragment(Packet *packet) return nullptr; } -void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) +simtime_t BasicReassembly::getNextExpirationTime() const { - Key key; - key.macAddress = address; - key.tid = tid; - key.seqNum = startSeqNumber; - auto itStart = fragmentsMap.lower_bound(key); - key.seqNum = endSeqNumber; - auto itEnd = fragmentsMap.upper_bound(key); + simtime_t nextExpirationTime = SIMTIME_MAX; + for (const auto& entry : fragmentsMap) + if (!entry.second.expired) + nextExpirationTime = std::min(nextExpirationTime, entry.second.receptionStartTime + maxReceiveLifetime); + return nextExpirationTime; +} - if (endSeqNumber < startSeqNumber) { - for (auto it = itStart; it != fragmentsMap.end();) { +std::vector BasicReassembly::removeExpiredFragments(simtime_t currentTime) +{ + std::vector expiredFragments; + for (auto& entry : fragmentsMap) { + auto& value = entry.second; + if (!value.expired && currentTime >= value.receptionStartTime + maxReceiveLifetime) { + for (auto fragment : value.fragments) + if (fragment != nullptr) + expiredFragments.push_back(fragment); + value.fragments.clear(); + value.receivedFragments = 0; + value.allFragments = 0; + value.expired = true; + } + } + return expiredFragments; +} + +void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) +{ + for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { + auto sequenceNumber = it->first.seqNum; + bool isInSequenceRange = startSeqNumber <= endSeqNumber ? + sequenceNumber >= startSeqNumber && sequenceNumber <= endSeqNumber : + sequenceNumber >= startSeqNumber || sequenceNumber <= endSeqNumber; + if (it->first.macAddress == address && it->first.tid == tid && isInSequenceRange) { for (auto fragment : it->second.fragments) delete fragment; it = fragmentsMap.erase(it); } - itStart = fragmentsMap.begin(); - } - for (auto it = itStart; it != itEnd;) { - for (auto fragment : it->second.fragments) - delete fragment; - it = fragmentsMap.erase(it); + else + it++; } } @@ -105,4 +141,3 @@ BasicReassembly::~BasicReassembly() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h index 0c1d7d810cc..cdcfd693727 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h @@ -8,7 +8,10 @@ #ifndef __INET_BASICREASSEMBLY_H #define __INET_BASICREASSEMBLY_H +#include + #include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" #include "inet/linklayer/ieee80211/mac/contract/IReassembly.h" @@ -20,27 +23,36 @@ class INET_API BasicReassembly : public IReassembly, public cObject protected: struct Key { MacAddress macAddress; + MacAddress receiverAddress; + Ieee80211FrameType type; Tid tid; SequenceNumber seqNum; - bool operator==(const Key& o) const { return macAddress == o.macAddress && tid == o.tid && seqNum == o.seqNum; } - bool operator<(const Key& o) const { return macAddress < o.macAddress || (macAddress == o.macAddress && (tid < o.tid || (tid == o.tid && seqNum < o.seqNum))); } + auto asTuple() const { return std::tie(macAddress, receiverAddress, type, tid, seqNum); } + bool operator==(const Key& other) const { return asTuple() == other.asTuple(); } + bool operator<(const Key& other) const { return asTuple() < other.asTuple(); } }; struct Value { std::vector fragments; uint16_t receivedFragments = 0; // each bit corresponds to a fragment number uint16_t allFragments = 0; // bits for all fragments set to one (0..numFragments-1); 0 means unfilled + simtime_t receptionStartTime; + bool expired = false; }; typedef std::map FragmentsMap; FragmentsMap fragmentsMap; + simtime_t maxReceiveLifetime; public: + BasicReassembly() : maxReceiveLifetime(SimTime(512 * 1024, SIMTIME_US)) {} + BasicReassembly(simtime_t maxReceiveLifetime) : maxReceiveLifetime(maxReceiveLifetime) {} virtual ~BasicReassembly(); virtual Packet *addFragment(Packet *packet) override; virtual void purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) override; + virtual simtime_t getNextExpirationTime() const override; + virtual std::vector removeExpiredFragments(simtime_t currentTime) override; }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.cc index 091c635504a..b370cf1bcc9 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.cc @@ -7,6 +7,7 @@ #include "inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.h" +#include "inet/common/packet/chunk/BytesChunk.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { @@ -33,10 +34,21 @@ Packet *Defragmentation::defragmentFrames(std::vector *fragmentFrames) defragmentedHeader->setMoreFragments(false); defragmentedFrame->insertAtFront(defragmentedHeader); defragmentedFrame->insertAtBack(makeShared()); + if (defragmentedHeader->getType() == ST_ACTION && defragmentedHeader->getChunkLength() == makeShared()->getChunkLength()) { + // Decode the completed action from the reassembled on-air bytes; an + // individual fragment intentionally has no typed action body. + const auto& trailer = defragmentedFrame->popAtBack(B(4)); + auto decodedFrame = new Packet(defragmentedFrame->getName(), defragmentedFrame->peekDataAsBytes()); + decodedFrame->insertAtBack(trailer); + decodedFrame->copyTags(*defragmentedFrame); + decodedFrame->getRegionTags().copyTags(defragmentedFrame->getRegionTags(), defragmentedFrame->getFrontOffset(), decodedFrame->getFrontOffset(), defragmentedFrame->getDataLength()); + decodedFrame->peekAtFront(); + delete defragmentedFrame; + defragmentedFrame = decodedFrame; + } EV_TRACE << "Created " << *defragmentedFrame << ".\n"; return defragmentedFrame; } } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc index 96ed3de0d9b..4465c22ecf3 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc @@ -7,11 +7,40 @@ #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/common/packet/chunk/BytesChunk.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" namespace inet { namespace ieee80211 { +namespace { + +const Ptr copyManagementHeader(const Ptr& source) +{ + auto destination = makeShared(); + destination->setType(source->getType()); + destination->setToDS(source->getToDS()); + destination->setFromDS(source->getFromDS()); + destination->setMoreFragments(source->getMoreFragments()); + destination->setRetry(source->getRetry()); + destination->setPowerMgmt(source->getPowerMgmt()); + destination->setMoreData(source->getMoreData()); + destination->setProtectedFrame(source->getProtectedFrame()); + destination->setOrder(source->getOrder()); + destination->setDurationField(source->getDurationField()); + destination->setAID(source->getAID()); + destination->setReceiverAddress(source->getReceiverAddress()); + destination->setMACArrive(source->getMACArrive()); + destination->setTransmitterAddress(source->getTransmitterAddress()); + destination->setAddress3(source->getAddress3()); + destination->setFragmentNumber(source->getFragmentNumber()); + destination->setSequenceNumber(source->getSequenceNumber()); + return destination; +} + +} // namespace + Register_Class(Fragmentation); std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::vector& fragmentSizes) @@ -21,6 +50,21 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve std::vector *fragments = new std::vector(); const auto& frameHeader = frame->popAtFront(); frame->popAtBack(B(4)); + const auto& actionFrame = dynamicPtrCast(frameHeader); + if (actionFrame != nullptr) { + // IEEE Std 802.11-2024, 10.4: a fragment frame body carries only a + // portion of the MMPDU. Move the action body out of INET's combined + // typed header before slicing it into fragment bodies. + Packet serializedHeader("serializedActionHeader", frameHeader); + const auto& headerBytes = serializedHeader.peekDataAsBytes()->getBytes(); + auto bodyOffset = makeShared()->getChunkLength().get(); + frame->insertAtFront(makeShared(std::vector(headerBytes.begin() + bodyOffset, headerBytes.end()))); + } + B totalFragmentBodyLength = B(0); + for (auto fragmentSize : fragmentSizes) + totalFragmentBodyLength += B(fragmentSize); + if (totalFragmentBodyLength != frame->getDataLength()) + throw cRuntimeError("Fragment sizes total %s but frame body length is %s", totalFragmentBodyLength.str().c_str(), frame->getDataLength().str().c_str()); for (size_t i = 0; i < fragmentSizes.size(); i++) { bool lastFragment = i == fragmentSizes.size() - 1; std::string name = std::string(frame->getName()) + "-frag" + std::to_string(i); @@ -30,7 +74,16 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve fragment->copyTags(*frame); fragment->getRegionTags().copyTags(frame->getRegionTags(), frame->getFrontOffset() + offset, fragment->getFrontOffset(), length); offset += length; - const auto& fragmentHeader = staticPtrCast(frameHeader->dupShared()); + Ptr fragmentHeader; + if (actionFrame != nullptr) { + fragmentHeader = copyManagementHeader(actionFrame); + auto actionContext = staticPtrCast(actionFrame->dupShared()); + actionContext->setFragmentNumber(i); + actionContext->setMoreFragments(!lastFragment); + fragment->addTag()->setActionFrame(actionContext); + } + else + fragmentHeader = staticPtrCast(frameHeader->dupShared()); fragmentHeader->setSequenceNumber(frameHeader->getSequenceNumber()); fragmentHeader->setFragmentNumber(i); fragmentHeader->setMoreFragments(!lastFragment); diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.cc new file mode 100644 index 00000000000..1256c20cb64 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.cc @@ -0,0 +1,15 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" + +namespace inet { +namespace ieee80211 { + +Register_Class(Ieee80211FragmentedActionContextTag); + +} // namespace ieee80211 +} // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h b/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h new file mode 100644 index 00000000000..4c853dc3f2f --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h @@ -0,0 +1,48 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IEEE80211FRAGMENTEDACTIONCONTEXTTAG_H +#define __INET_IEEE80211FRAGMENTEDACTIONCONTEXTTAG_H + +#include "inet/common/TagBase.h" +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" + +namespace inet { +namespace ieee80211 { + +/** + * Sender-local action context for an MPDU fragment whose packet content + * contains only the common management header and its action-body slice. + */ +class INET_API Ieee80211FragmentedActionContextTag : public TagBase +{ + protected: + Ptr actionFrame; + + public: + Ieee80211FragmentedActionContextTag() {} + Ieee80211FragmentedActionContextTag(const Ieee80211FragmentedActionContextTag& other) : TagBase(other), actionFrame(other.actionFrame) {} + + virtual Ieee80211FragmentedActionContextTag *dup() const override { return new Ieee80211FragmentedActionContextTag(*this); } + + const Ptr& getActionFrame() const { return actionFrame; } + void setActionFrame(const Ptr& actionFrame) { this->actionFrame = actionFrame; } +}; + +template +const Ptr findFragmentedActionContext(const Packet *packet) +{ + if (auto contextTag = packet->findTag()) + return dynamicPtrCast(contextTag->getActionFrame()); + else + return dynamicPtrCast(packet->peekAtFront()); +} + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc index 5075d391e89..e903e860596 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc @@ -19,12 +19,42 @@ Define_Module(RecipientMacDataService); void RecipientMacDataService::initialize() { duplicateRemoval = new LegacyDuplicateRemoval(); - basicReassembly = new BasicReassembly(); + basicReassembly = new BasicReassembly(par("maxReceiveLifetime")); + reassemblyTimer = new cMessage("reassemblyTimer"); +} + +void RecipientMacDataService::handleMessage(cMessage *message) +{ + if (message != reassemblyTimer) + throw cRuntimeError("Unknown message"); + expireReassemblyFragments(); + scheduleReassemblyTimer(); +} + +void RecipientMacDataService::expireReassemblyFragments() +{ + for (auto packet : basicReassembly->removeExpiredFragments(simTime())) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } +} + +void RecipientMacDataService::scheduleReassemblyTimer() +{ + if (reassemblyTimer->isScheduled()) + cancelEvent(reassemblyTimer); + auto nextExpirationTime = basicReassembly->getNextExpirationTime(); + if (nextExpirationTime != SIMTIME_MAX) + scheduleAt(nextExpirationTime, reassemblyTimer); } Packet *RecipientMacDataService::defragment(Packet *dataOrMgmtFrame) { + expireReassemblyFragments(); Packet *packet = basicReassembly->addFragment(dataOrMgmtFrame); + scheduleReassemblyTimer(); if (packet && packet->peekAtFront()) { emit(packetDefragmentedSignal, packet); return packet; @@ -72,10 +102,10 @@ std::vector RecipientMacDataService::controlFrameReceived(Packet *cont RecipientMacDataService::~RecipientMacDataService() { + cancelAndDelete(reassemblyTimer); delete duplicateRemoval; delete basicReassembly; } } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h index 63fa2007877..44342ad6569 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h @@ -27,6 +27,7 @@ class INET_API RecipientMacDataService : public SimpleModule, public IRecipientM protected: IReassembly *basicReassembly = nullptr; // FIXME use Defragmentation + cMessage *reassemblyTimer = nullptr; // MpduHeaderAndFcsValidation *mpduHeaderAndFcsValidation = nullptr; // Address1Filtering *address1Filtering = nullptr; @@ -37,6 +38,9 @@ class INET_API RecipientMacDataService : public SimpleModule, public IRecipientM protected: virtual void initialize() override; + virtual void handleMessage(cMessage *message) override; + virtual void expireReassemblyFragments(); + virtual void scheduleReassemblyTimer(); virtual Packet *defragment(Packet *dataOrMgmtFrame); virtual std::vector dataOrMgmtFrameReceived(Packet *packet, const Ptr& header); @@ -52,4 +56,3 @@ class INET_API RecipientMacDataService : public SimpleModule, public IRecipientM } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned index eff3279ebca..8e2b3b35f88 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned @@ -19,6 +19,7 @@ module RecipientMacDataService extends Module { parameters: @class(RecipientMacDataService); + double maxReceiveLifetime @unit(s) = default(524288us); // IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs @display("i=block/join"); @signal[packetDefragmented](type=inet::Packet); @signal[packetDeaggregated](type=inet::Packet); @@ -27,4 +28,3 @@ module RecipientMacDataService extends Module @statistic[packetDeaggregated](title="packets deaggregated"; record=count); @statistic[packetDropDuplicateDetected](title="packet drops: duplicate detected"; source=packetDropReasonIsDuplicateDetected(packetDropped); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); } - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 00dbfe970e0..d7efe6322bd 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -24,10 +24,38 @@ Define_Module(RecipientQosMacDataService); void RecipientQosMacDataService::initialize() { duplicateRemoval = new QoSDuplicateRemoval(); - basicReassembly = new BasicReassembly(); + basicReassembly = new BasicReassembly(par("maxReceiveLifetime")); aMsduDeaggregation = new MsduDeaggregation(); aMpduDeaggregation = new MpduDeaggregation(); blockAckReordering = new BlockAckReordering(); + reassemblyTimer = new cMessage("reassemblyTimer"); +} + +void RecipientQosMacDataService::handleMessage(cMessage *message) +{ + if (message != reassemblyTimer) + throw cRuntimeError("Unknown message"); + expireReassemblyFragments(); + scheduleReassemblyTimer(); +} + +void RecipientQosMacDataService::expireReassemblyFragments() +{ + for (auto packet : basicReassembly->removeExpiredFragments(simTime())) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } +} + +void RecipientQosMacDataService::scheduleReassemblyTimer() +{ + if (reassemblyTimer->isScheduled()) + cancelEvent(reassemblyTimer); + auto nextExpirationTime = basicReassembly->getNextExpirationTime(); + if (nextExpirationTime != SIMTIME_MAX) + scheduleAt(nextExpirationTime, reassemblyTimer); } void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress originatorAddr) @@ -47,19 +75,26 @@ void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress ori Packet *RecipientQosMacDataService::defragment(std::vector completeFragments) { + expireReassemblyFragments(); + Packet *defragmentedPacket = nullptr; for (auto fragment : completeFragments) { auto packet = basicReassembly->addFragment(fragment); if (packet != nullptr) { - emit(packetDefragmentedSignal, packet); - return packet; + defragmentedPacket = packet; + break; } } - return nullptr; + scheduleReassemblyTimer(); + if (defragmentedPacket != nullptr) + emit(packetDefragmentedSignal, defragmentedPacket); + return defragmentedPacket; } Packet *RecipientQosMacDataService::defragment(Packet *mgmtFragment) { + expireReassemblyFragments(); auto packet = basicReassembly->addFragment(mgmtFragment); + scheduleReassemblyTimer(); if (packet && packet->hasAtFront()) { emit(packetDefragmentedSignal, packet); return packet; @@ -134,18 +169,27 @@ IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataS // TODO MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) { delete mgmtPacket; - return { {}, true }; + // A duplicate fragment is acknowledged by HCF but is not a complete + // MMPDU. Preserve the existing subtype-specific handling only for an + // unfragmented duplicate management frame. + if (mgmtHeader->getFragmentNumber() == 0 && !mgmtHeader->getMoreFragments()) + return { {}, mgmtHeader, true }; + else + return { {}, nullptr, true }; } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); } + if (mgmtPacket == nullptr) + return {}; + const auto& completeHeader = mgmtPacket->peekAtFront(); // TODO Defrag, MSDU Integrity, Replay Detection, RX MSDU Rate Limiting - if (dynamicPtrCast(mgmtHeader)) { + if (dynamicPtrCast(completeHeader)) { delete mgmtPacket; - return { {}, false }; + return { {}, completeHeader, false }; } else - return { { mgmtPacket }, false }; + return { { mgmtPacket }, completeHeader, false }; } std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) @@ -202,6 +246,7 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c RecipientQosMacDataService::~RecipientQosMacDataService() { + cancelAndDelete(reassemblyTimer); delete duplicateRemoval; delete basicReassembly; delete aMsduDeaggregation; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index cb27ff09543..76b27d8cbc7 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -27,6 +27,7 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, { protected: IReassembly *basicReassembly = nullptr; + cMessage *reassemblyTimer = nullptr; IMpduDeaggregation *aMpduDeaggregation = nullptr; // MpduHeaderAndFcsValidation *mpduHeaderAndFcsValidation = nullptr; @@ -41,7 +42,10 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, protected: virtual ~RecipientQosMacDataService(); virtual void initialize() override; + virtual void handleMessage(cMessage *message) override; + virtual void expireReassemblyFragments(); + virtual void scheduleReassemblyTimer(); virtual Packet *defragment(std::vector completeFragments); virtual Packet *defragment(Packet *mgmtFragment); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned index 307fb523210..d02d7e7ff00 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned @@ -22,6 +22,7 @@ module RecipientQosMacDataService extends Module { parameters: @class(RecipientQosMacDataService); + double maxReceiveLifetime @unit(s) = default(524288us); // IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs @display("i=block/join"); @signal[packetDefragmented](type=inet::Packet); @signal[packetDeaggregated](type=inet::Packet); @@ -30,4 +31,3 @@ module RecipientQosMacDataService extends Module @statistic[packetDeaggregated](title="packets deaggregated"; record=count); @statistic[packetDropDuplicateDetected](title="packet drops: duplicate detected"; source=packetDropReasonIsDuplicateDetected(packetDropped); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); } - diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 9e3a5a153a3..72e7714ddf5 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -25,13 +25,17 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" @@ -84,6 +88,15 @@ class TestRecipientPolicy : public IRecipientBlockAckAgreementPolicy virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } }; +class TestRecipientAckPolicy : public IRecipientAckPolicy, public IRecipientQosAckPolicy +{ + public: + virtual bool isAckNeeded(const Ptr&) const override { return true; } + virtual bool isBlockAckNeeded(const Ptr&, RecipientBlockAckAgreement *) const override { return false; } + virtual simtime_t computeAckDurationField(Packet *, const Ptr&) const override { return 0; } + virtual simtime_t computeBasicBlockAckDurationField(Packet *, const Ptr&) const override { return 0; } +}; + class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandlerCallback { public: @@ -568,6 +581,7 @@ class TestHcf : public Hcf int numResumedEligibleChannelAccess = 0; int numCancelledAddbaTransactions = 0; int numRebuildEligibilityCalls = 0; + int numTransmittedControlResponses = 0; bool droppedSetupCancelled = false; bool delegateDroppedSetupHandling = false; TestCallback *managementCallback = nullptr; @@ -593,6 +607,10 @@ class TestHcf : public Hcf } void configureFrameSequenceHandler(IFrameSequenceHandler *frameSequenceHandler) { this->frameSequenceHandler = frameSequenceHandler; } void configureRecipientDataService(IRecipientQosMacDataService *recipientDataService) { this->recipientDataService = recipientDataService; } + void configureRecipientAck(IRecipientQosAckPolicy *recipientAckPolicy) { + this->recipientAckPolicy = recipientAckPolicy; + recipientAckProcedure = new RecipientAckProcedure(); + } void configureManagementCallback(TestCallback *managementCallback) { this->managementCallback = managementCallback; } void configureTransmittedData(Edca *edca, IOriginatorBlockAckAgreementHandler *handler, IOriginatorBlockAckAgreementPolicy *policy) { this->edca = edca; @@ -616,6 +634,7 @@ class TestHcf : public Hcf void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } + void processReceivedFrame(Packet *packet) { recipientProcessReceivedFrame(packet, packet->peekAtFront()); } void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } void processFailedFrame(Packet *packet) { originatorProcessFailedFrame(packet); } void processReceivedAck(Packet *packet, Packet *lastTransmittedPacket, AccessCategory accessCategory) { @@ -625,6 +644,10 @@ class TestHcf : public Hcf } protected: + virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr&, Packet *, const Ptr&) override { + numTransmittedControlResponses++; + delete responsePacket; + } virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { if (managementCallback != nullptr) managementCallback->processMgmtFrame(packet, header); @@ -667,7 +690,7 @@ class TestEmptyFrameSequence : public IFrameSequence class TestFragmentationPolicy : public IFragmentationPolicy { public: - virtual std::vector computeFragmentSizes(Packet *) override { return { 2, 2 }; } + virtual std::vector computeFragmentSizes(Packet *) override { return { 8, 5 }; } }; class TestMsduAggregationPolicy : public IMsduAggregationPolicy @@ -995,7 +1018,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); TestOriginatorQosMacDataService dataService; auto originatorHandler = new TestOriginatorHandler(); dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { - auto delba = dynamicPtrCast(packet->peekAtFront()); + auto delba = findFragmentedActionContext(packet); return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); }); TestInProgressFrames inProgressFrames; @@ -1043,7 +1066,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); TestOriginatorQosMacDataService dataService; auto originatorHandler = new TestOriginatorHandler(); dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { - auto delba = dynamicPtrCast(packet->peekAtFront()); + auto delba = findFragmentedActionContext(packet); return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); }); TestInProgressFrames inProgressFrames; @@ -1063,19 +1086,33 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); hcf.configureTransmittedData(&edca, originatorHandler, &policy); hcf.configureTransmittedManagement(&edca, &ackPolicy); - auto sibling = makeTaggedDelbaPacket("acknowledgedDelbaSibling", peer1, 6, SequenceNumberCyclic(204), 0, true, transactionId); - auto finalPacket = makeTaggedDelbaPacket("acknowledgedFinalDelba", peer1, 6, SequenceNumberCyclic(204), 1, false, transactionId); - auto siblingHeader = sibling->peekAtFront(); - auto finalHeader = finalPacket->peekAtFront(); + auto completeDelba = makeShared(); + completeDelba->setReceiverAddress(peer1); + completeDelba->setTid(6); + completeDelba->setInitiator(true); + completeDelba->setSequenceNumber(SequenceNumberCyclic(204)); + auto completeDelbaPacket = new Packet("acknowledgedDelba", completeDelba); + completeDelbaPacket->addTag()->setTransactionId(transactionId); + completeDelbaPacket->insertAtBack(makeShared()); + Fragmentation fragmentation; + auto delbaFragments = fragmentation.fragmentFrame(completeDelbaPacket, { 4, 2 }); + auto sibling = delbaFragments->at(0); + auto finalPacket = delbaFragments->at(1); + auto siblingHeader = sibling->peekAtFront(); + auto finalHeader = finalPacket->peekAtFront(); + ASSERT(dynamicPtrCast(siblingHeader) == nullptr); + ASSERT(dynamicPtrCast(finalHeader) == nullptr); originatorHandler->setPendingTeardownTransaction(peer1, 6, transactionId); inProgressFrames.addOwnedFrame(sibling); inProgressFrames.addOwnedFrame(finalPacket); ackHandler.frameGotInProgress(siblingHeader); ackHandler.frameGotInProgress(finalHeader); + hcf.processTransmittedManagement(sibling, siblingHeader, AC_BE); + ASSERT(originatorHandler->isDelbaPending(sibling, findFragmentedActionContext(sibling))); hcf.processTransmittedManagement(finalPacket, finalHeader, AC_BE); Packet ackPacket("transactionalDelbaAck", makeShared()); hcf.processReceivedAck(&ackPacket, finalPacket, AC_BE); - ASSERT(!originatorHandler->isDelbaPending(finalPacket, finalHeader)); + ASSERT(!originatorHandler->isDelbaPending(finalPacket, findFragmentedActionContext(finalPacket))); ASSERT(hcf.numCancelledAddbaTransactions == 1); ASSERT(inProgressFrames.getLength() == 0); ASSERT(inProgressFrames.getNumDroppedFrames() == 1); @@ -1086,6 +1123,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); auto releasedFrames = inProgressFrames.releaseFrames(); ASSERT(releasedFrames == std::vector({ finalPacket })); delete finalPacket; + delete delbaFragments; } // Reaching the retry limit aborts the teardown through HCF, clears its sibling @@ -1100,7 +1138,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); TestOriginatorQosMacDataService dataService; auto originatorHandler = new TestOriginatorHandler(); dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { - auto delba = dynamicPtrCast(packet->peekAtFront()); + auto delba = findFragmentedActionContext(packet); return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); }); TestInProgressFrames inProgressFrames; @@ -1482,8 +1520,17 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto transmittedDelba = makeShared(); transmittedDelba->setReceiverAddress(peer2); transmittedDelba->setTid(2); + transmittedDelba->setInitiator(true); + transmittedDelba->setMoreFragments(true); + auto transmittedFinalDelba = staticPtrCast(transmittedDelba->dupShared()); + transmittedFinalDelba->setMoreFragments(false); + transmittedFinalDelba->setFragmentNumber(1); Packet transmittedDelbaPacket("transmittedDelba", transmittedDelba); - auto terminatedPendingAgreement = transmittedDelbaHandler.processTransmittedDelba(&transmittedDelbaPacket, &transmittedDelbaCallback); + ASSERT(transmittedDelbaHandler.processTransmittedDelba(&transmittedDelbaPacket, &transmittedDelbaCallback) == nullptr); + ASSERT(transmittedDelbaHandler.getAgreement(peer2, 2) != nullptr); + ASSERT(transmittedDelbaCallback.cancelledTransactionIds.empty()); + Packet transmittedFinalDelbaPacket("transmittedFinalDelba", transmittedFinalDelba); + auto terminatedPendingAgreement = transmittedDelbaHandler.processTransmittedDelba(&transmittedFinalDelbaPacket, &transmittedDelbaCallback); ASSERT(terminatedPendingAgreement != nullptr); ASSERT(terminatedPendingAgreement->isPending()); if (transmittedDelbaHandler.getAgreement(peer2, 2) != nullptr || transmittedDelbaCallback.cancelledTransactionIds.size() != 1 || transmittedDelbaCallback.cancelledTransactionIds.back() != transmittedDelbaTransactionId) @@ -1494,18 +1541,21 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(transmittedDelbaCallback.addbaDeadline == SIMTIME_MAX); } -// Fragmentation preserves the local transaction identity on every MPDU, and -// typed cancellation removes all exact siblings while retaining the dropped -// fragment and an unrelated transaction. +// Fragmentation preserves the local transaction identity on every MPDU. +// Transaction-scoped cancellation removes all exact siblings while retaining +// the dropped fragment and an unrelated transaction. { - TestOriginatorHandler fragmentedHandler; + auto fragmentedHandler = new TestOriginatorHandler(); TestCallback fragmentedCallback; Packet fragmentedTrigger("fragmentedTrigger"); auto fragmentedTriggerHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(50)); - fragmentedHandler.processAcknowledgedDataFrame(&fragmentedTrigger, fragmentedTriggerHeader, &originatorPolicy, &fragmentedCallback); + fragmentedHandler->processAcknowledgedDataFrame(&fragmentedTrigger, fragmentedTriggerHeader, &originatorPolicy, &fragmentedCallback); auto unfragmentedRequestPacket = fragmentedCallback.managementPacket; auto fragmentedTransactionId = unfragmentedRequestPacket->getTag()->getTransactionId(); fragmentedCallback.forgetManagementPacket(unfragmentedRequestPacket); + auto unfragmentedRequestHeader = unfragmentedRequestPacket->removeAtFront(); + unfragmentedRequestHeader->setSequenceNumber(SequenceNumberCyclic(50)); + unfragmentedRequestPacket->insertAtFront(unfragmentedRequestHeader); unfragmentedRequestPacket->insertAtBack(makeShared(B(4))); unfragmentedRequestPacket->insertAtBack(makeShared()); TestFragmentationPolicy fragmentationPolicy; @@ -1514,8 +1564,11 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto fragments = fragmentationDataService.fragment(unfragmentedRequestPacket); ASSERT(fragments != nullptr); ASSERT(fragments->size() == 2); - for (auto fragment : *fragments) + for (auto fragment : *fragments) { ASSERT(fragment->getTag()->getTransactionId() == fragmentedTransactionId); + ASSERT(dynamicPtrCast(fragment->peekAtFront()) == nullptr); + ASSERT(findFragmentedActionContext(fragment) != nullptr); + } auto unrelatedPacket = new Packet("unrelatedTransaction", makeShared(B(1))); unrelatedPacket->addTag()->setTransactionId(fragmentedTransactionId + 1); auto droppedFragment = fragments->at(0); @@ -1524,15 +1577,30 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); fragmentedPendingQueue.packets = { droppedFragment, siblingFragment, unrelatedPacket }; TestInProgressFrames fragmentedInProgressFrames; TestEdcaf fragmentedEdcaf; + TestQosAckHandler fragmentedAckHandler; fragmentedEdcaf.pendingQueue = &fragmentedPendingQueue; fragmentedEdcaf.inProgressFrames = &fragmentedInProgressFrames; + fragmentedEdcaf.qosAckHandler = &fragmentedAckHandler; TestEdca fragmentedEdca; fragmentedEdca.edcaf = &fragmentedEdcaf; TestHcf fragmentedHcf; fragmentedHcf.configure(&fragmentedEdca); - auto droppedRequest = droppedFragment->peekAtFront(); - fragmentedHandler.processDroppedAddbaReq(droppedFragment, droppedRequest, &originatorPolicy, &fragmentedHcf); - ASSERT(fragmentedHandler.getAgreement(peer1, 7) == nullptr); + OriginatorQosAckPolicy fragmentedAckPolicy; + TestRecipientPolicy fragmentedRecipientPolicy; + fragmentedHcf.configureBlockAckHandlers(fragmentedHandler, &originatorPolicy, new TestRecipientHandler(), &fragmentedRecipientPolicy); + fragmentedHcf.configureTransmittedManagement(&fragmentedEdca, &fragmentedAckPolicy); + auto fragmentedAgreement = fragmentedHandler->getAgreement(peer1, 7); + ASSERT(fragmentedAgreement != nullptr); + ASSERT(!fragmentedAgreement->getIsAddbaRequestSent()); + auto firstFragmentHeader = droppedFragment->peekAtFront(); + auto finalFragmentHeader = siblingFragment->peekAtFront(); + fragmentedHcf.processTransmittedManagement(droppedFragment, firstFragmentHeader, AC_BE); + ASSERT(!fragmentedAgreement->getIsAddbaRequestSent()); + fragmentedHcf.processTransmittedManagement(siblingFragment, finalFragmentHeader, AC_BE); + ASSERT(fragmentedAgreement->getIsAddbaRequestSent()); + auto droppedRequest = findFragmentedActionContext(droppedFragment); + fragmentedHandler->processDroppedAddbaReq(droppedFragment, droppedRequest, &originatorPolicy, &fragmentedHcf); + ASSERT(fragmentedHandler->getAgreement(peer1, 7) == nullptr); ASSERT(fragmentedPendingQueue.getNumPackets() == 2); ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), droppedFragment) != fragmentedPendingQueue.packets.end()); ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), siblingFragment) == fragmentedPendingQueue.packets.end()); @@ -1583,7 +1651,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); rejectedRequest->setSequenceNumber(SequenceNumberCyclic(40)); auto rejectedReception = recipientDataService->managementFrameReceived(new Packet("initiallyRejectedAddbaRequest", rejectedRequest), rejectedRequest); ASSERT(!rejectedReception.duplicate); - hcf.processReceivedManagementFrame(rejectedRequest, rejectedReception.duplicate); + ASSERT(rejectedReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(rejectedReception.completeHeader, rejectedReception.duplicate); ASSERT(recipientHandler->getAgreement(7, peer3) == nullptr); ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); ASSERT(responseCallback.managementPackets.size() == 1); @@ -1594,7 +1663,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); duplicateRejectedRequest->setRetry(true); auto duplicateReception = recipientDataService->managementFrameReceived(new Packet("duplicateInitiallyRejectedAddbaRequest", duplicateRejectedRequest), duplicateRejectedRequest); ASSERT(duplicateReception.duplicate); - hcf.processReceivedManagementFrame(duplicateRejectedRequest, duplicateReception.duplicate); + ASSERT(duplicateReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateReception.completeHeader, duplicateReception.duplicate); ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); ASSERT(responseCallback.managementPackets.size() == 1); @@ -1633,7 +1703,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto firstReception = recipientDataService->managementFrameReceived(new Packet("firstAddbaRequest", firstRequest), firstRequest); ASSERT(!firstReception.duplicate); ASSERT(firstReception.completeFrames.empty()); - hcf.processReceivedManagementFrame(firstRequest, firstReception.duplicate); + ASSERT(firstReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(firstReception.completeHeader, firstReception.duplicate); auto firstAgreement = recipientHandler->getAgreement(6, peer2); ASSERT(firstAgreement != nullptr); ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); @@ -1670,7 +1741,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto duplicateReception = recipientDataService->managementFrameReceived(new Packet("duplicateAddbaRequest", duplicateRequest), duplicateRequest); ASSERT(duplicateReception.duplicate); ASSERT(duplicateReception.completeFrames.empty()); - hcf.processReceivedManagementFrame(duplicateRequest, duplicateReception.duplicate); + ASSERT(duplicateReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateReception.completeHeader, duplicateReception.duplicate); ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); ASSERT(firstAgreement->getBlockAckRecord() == firstBlockAckRecord); ASSERT(firstAgreement->getExpirationTime() == firstExpirationTime); @@ -1695,7 +1767,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); rejectedRequest->setBlockAckTimeoutValue(4); auto rejectedReception = recipientDataService->managementFrameReceived(new Packet("rejectedAddbaRequest", rejectedRequest), rejectedRequest); ASSERT(!rejectedReception.duplicate); - hcf.processReceivedManagementFrame(rejectedRequest, rejectedReception.duplicate); + ASSERT(rejectedReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(rejectedReception.completeHeader, rejectedReception.duplicate); ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); ASSERT(changedListener.numSignals == 0); ASSERT(reorderDropListener.numSignals == 0); @@ -1715,7 +1788,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); duplicateRejectedRequest->setBlockAckTimeoutValue(4); auto duplicateRejectedReception = recipientDataService->managementFrameReceived(new Packet("duplicateRejectedAddbaRequest", duplicateRejectedRequest), duplicateRejectedRequest); ASSERT(duplicateRejectedReception.duplicate); - hcf.processReceivedManagementFrame(duplicateRejectedRequest, duplicateRejectedReception.duplicate); + ASSERT(duplicateRejectedReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateRejectedReception.completeHeader, duplicateRejectedReception.duplicate); ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); ASSERT(changedListener.numSignals == 0); @@ -1766,7 +1840,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto replacementReception = recipientDataService->managementFrameReceived(new Packet("replacementAddbaRequest", replacementRequest), replacementRequest); ASSERT(!replacementReception.duplicate); ASSERT(replacementReception.completeFrames.empty()); - hcf.processReceivedManagementFrame(replacementRequest, replacementReception.duplicate); + ASSERT(replacementReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(replacementReception.completeHeader, replacementReception.duplicate); ASSERT(recipientHandler->getAgreement(6, peer2) != firstAgreement); ASSERT(addedListener.numSignals == 1); ASSERT(changedListener.numSignals == 1); @@ -1802,6 +1877,278 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); recipientDataServiceModule->deleteModule(); } +// IEEE Std 802.11-2024, 10.4-10.5: each management MPDU fragment is +// acknowledged independently, but its action body is dispatched only after +// the final fragment and every preceding fragment have been received. +{ + auto fragmentationPolicyModule = cModuleType::get("inet.linklayer.ieee80211.mac.fragmentation.BasicFragmentationPolicy")->create("addbaFragmentationPolicy", this); + fragmentationPolicyModule->par("fragmentationThreshold").setIntValue(34); + fragmentationPolicyModule->callInitialize(); + auto fragmentationPolicy = check_and_cast(fragmentationPolicyModule); + auto policyRequest = makeRequest(peer2, 2, 72, SequenceNumberCyclic(102), 32); + auto policyPacket = new Packet("policyAddbaRequest", policyRequest); + policyPacket->insertAtBack(makeShared()); + ASSERT(fragmentationPolicy->computeFragmentSizes(policyPacket) == std::vector({ 6, 3 })); + delete policyPacket; + auto groupAddressedRequest = makeRequest(peer2, 2, 72, SequenceNumberCyclic(102), 32); + groupAddressedRequest->setReceiverAddress(MacAddress::BROADCAST_ADDRESS); + auto groupAddressedPacket = new Packet("groupAddressedAddbaRequest", groupAddressedRequest); + groupAddressedPacket->insertAtBack(makeShared()); + ASSERT(fragmentationPolicy->computeFragmentSizes(groupAddressedPacket).empty()); + delete groupAddressedPacket; + fragmentationPolicyModule->callFinish(); + fragmentationPolicyModule->deleteModule(); + + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("fragmentedAddbaDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestRecipientPolicy fragmentedPolicy; + TestCallback fragmentedCallback; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &fragmentedPolicy); + hcf.configureRecipientDataService(recipientDataService); + hcf.configureManagementCallback(&fragmentedCallback); + + auto makeFragments = [&](const char *name, Tid tid, int sequenceNumber) { + auto request = makeRequest(peer2, tid, 70 + tid, SequenceNumberCyclic(100 + tid), 32); + request->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + auto packet = new Packet(name, request); + packet->insertAtBack(makeShared(B(4))); + packet->insertAtBack(makeShared()); + Fragmentation fragmentation; + return fragmentation.fragmentFrame(packet, { 8, 5 }); + }; + auto receiveFragment = [&](Packet *fragment) { + auto header = fragment->peekAtFront(); + return recipientDataService->managementFrameReceived(fragment, header); + }; + auto convertToWireRepresentation = [](Packet *fragment) { + std::string name = fragment->getName(); + const auto& trailer = fragment->popAtBack(B(4)); + auto wireFragment = new Packet(name.c_str(), fragment->peekDataAsBytes()); + wireFragment->copyTags(*fragment); + wireFragment->insertAtBack(trailer); + delete fragment; + return wireFragment; + }; + + auto fragments = makeFragments("fragmentedAddbaRequest", 3, 60); + for (auto& fragment : *fragments) + fragment = convertToWireRepresentation(fragment); + auto commonManagementHeaderLength = makeShared()->getChunkLength(); + ASSERT(fragments->at(0)->getByteLength() == commonManagementHeaderLength.get() + 8 + 4); + ASSERT(fragments->at(1)->getByteLength() == commonManagementHeaderLength.get() + 5 + 4); + ASSERT(dynamicPtrCast(fragments->at(0)->peekAtFront()) == nullptr); + ASSERT(dynamicPtrCast(fragments->at(1)->peekAtFront()) == nullptr); + auto duplicateFirstFragment = fragments->at(0)->dup(); + auto duplicateHeader = duplicateFirstFragment->removeAtFront(); + duplicateHeader->setRetry(true); + duplicateFirstFragment->insertAtFront(duplicateHeader); + + auto firstReception = receiveFragment(fragments->at(0)); + ASSERT(firstReception.completeHeader == nullptr); + ASSERT(recipientHandler->getAgreement(3, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.empty()); + + auto duplicateReception = receiveFragment(duplicateFirstFragment); + ASSERT(duplicateReception.duplicate); + ASSERT(duplicateReception.completeHeader == nullptr); + ASSERT(recipientHandler->getAgreement(3, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.empty()); + + auto completeReception = receiveFragment(fragments->at(1)); + ASSERT(!completeReception.duplicate); + ASSERT(completeReception.completeHeader != nullptr); + ASSERT(completeReception.completeHeader->getFragmentNumber() == 0); + ASSERT(!completeReception.completeHeader->getMoreFragments()); + auto completeRequest = dynamicPtrCast(completeReception.completeHeader); + ASSERT(completeRequest != nullptr); + ASSERT(completeRequest->getTid() == 3); + ASSERT(completeRequest->getDialogToken() == 73); + ASSERT(completeRequest->getStartingSequenceNumber() == SequenceNumberCyclic(103)); + hcf.processReceivedManagementFrame(completeReception.completeHeader); + ASSERT(recipientHandler->getAgreement(3, peer2) != nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == 1); + delete fragments; + + // Seeing the final fragment first does not imply completeness. Dispatch + // occurs only when the missing lower-numbered fragment later arrives. + auto outOfOrderFragments = makeFragments("outOfOrderAddbaRequest", 4, 61); + for (auto& fragment : *outOfOrderFragments) + fragment = convertToWireRepresentation(fragment); + auto finalFirstReception = receiveFragment(outOfOrderFragments->at(1)); + ASSERT(finalFirstReception.completeHeader == nullptr); + ASSERT(recipientHandler->getAgreement(4, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == 1); + auto outOfOrderCompleteReception = receiveFragment(outOfOrderFragments->at(0)); + ASSERT(outOfOrderCompleteReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(outOfOrderCompleteReception.completeHeader); + ASSERT(recipientHandler->getAgreement(4, peer2) != nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == 2); + delete outOfOrderFragments; + + // The receive timer releases an incomplete MMPDU at + // dot11MaxReceiveLifetime even if no matching fragment arrives. + { + auto expiringDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("expiringFragmentDataService", this); + expiringDataServiceModule->par("maxReceiveLifetime").setDoubleValue(1e-6); + expiringDataServiceModule->callInitialize(); + auto expiringDataService = check_and_cast(expiringDataServiceModule); + TestPacketDropSignalListener expiryDropListener; + expiringDataService->subscribe(packetDroppedSignal, &expiryDropListener); + auto expiredFragments = makeFragments("expiredAddbaRequest", 1, 66); + auto firstExpiredReception = expiringDataService->managementFrameReceived(expiredFragments->at(0), expiredFragments->at(0)->peekAtFront()); + ASSERT(firstExpiredReception.completeHeader == nullptr); + ASSERT(expiryDropListener.numOtherPacketDrops == 0); + wait(2e-6); + ASSERT(expiryDropListener.numOtherPacketDrops == 1); + auto lateFinalReception = expiringDataService->managementFrameReceived(expiredFragments->at(1), expiredFragments->at(1)->peekAtFront()); + ASSERT(lateFinalReception.completeHeader == nullptr); + // The late final fragment must be discarded, not buffered as the + // beginning of another reassembly. A subsequent non-Retry fragment 0 + // starts a new sequence reuse and therefore cannot combine with it. + auto restartedFragments = makeFragments("restartedAddbaRequest", 1, 66); + auto restartedReception = expiringDataService->managementFrameReceived(restartedFragments->at(0), restartedFragments->at(0)->peekAtFront()); + ASSERT(restartedReception.completeHeader == nullptr); + delete restartedFragments->at(1); + delete restartedFragments; + expiringDataServiceModule->callFinish(); + expiringDataServiceModule->deleteModule(); + delete expiredFragments; + } + + // Frame type and destination address are part of the reassembly identity; + // fragments sharing only TA and sequence number cannot cross-associate. + { + BasicReassembly isolatedReassembly; + auto firstMmpdu = makeFragments("isolatedAddbaRequest", 0, 67); + auto otherDestinationMmpdu = makeFragments("otherDestinationAddbaRequest", 0, 67); + auto setReceiverAddress = [](Packet *fragment, const MacAddress& receiverAddress) { + auto header = fragment->removeAtFront(); + header->setReceiverAddress(receiverAddress); + fragment->insertAtFront(header); + }; + for (auto fragment : *firstMmpdu) + setReceiverAddress(fragment, peer1); + for (auto fragment : *otherDestinationMmpdu) + setReceiverAddress(fragment, peer3); + auto dataHeader = makeShared(); + dataHeader->setType(ST_DATA); + dataHeader->setTransmitterAddress(peer2); + dataHeader->setReceiverAddress(peer1); + dataHeader->setSequenceNumber(SequenceNumberCyclic(67)); + dataHeader->setFragmentNumber(1); + dataHeader->setMoreFragments(false); + auto dataFragment = new Packet("sameSequenceDataFragment", dataHeader); + dataFragment->insertAtBack(makeShared(B(5))); + dataFragment->insertAtBack(makeShared()); + ASSERT(isolatedReassembly.addFragment(firstMmpdu->at(0)) == nullptr); + ASSERT(isolatedReassembly.addFragment(otherDestinationMmpdu->at(1)) == nullptr); + ASSERT(isolatedReassembly.addFragment(dataFragment) == nullptr); + auto completeMmpdu = isolatedReassembly.addFragment(firstMmpdu->at(1)); + ASSERT(completeMmpdu != nullptr); + delete completeMmpdu; + delete otherDestinationMmpdu->at(0); + delete firstMmpdu; + delete otherDestinationMmpdu; + } + + // Drive the real HCF recipient entry point: every MPDU, including an + // ignored duplicate, is ACKed before the data service gates action-body + // dispatch on complete reassembly. + TestRecipientAckPolicy recipientAckPolicy; + hcf.configureRecipientAck(&recipientAckPolicy); + auto hcfFragments = makeFragments("hcfFragmentedAddbaRequest", 7, 65); + for (auto& fragment : *hcfFragments) + fragment = convertToWireRepresentation(fragment); + auto hcfDuplicateFirstFragment = hcfFragments->at(0)->dup(); + auto hcfDuplicateHeader = hcfDuplicateFirstFragment->removeAtFront(); + hcfDuplicateHeader->setRetry(true); + hcfDuplicateFirstFragment->insertAtFront(hcfDuplicateHeader); + auto numManagementPacketsBeforeHcfReception = fragmentedCallback.managementPackets.size(); + hcf.processReceivedFrame(hcfFragments->at(0)); + ASSERT(hcf.numTransmittedControlResponses == 1); + ASSERT(recipientHandler->getAgreement(7, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == numManagementPacketsBeforeHcfReception); + hcf.processReceivedFrame(hcfDuplicateFirstFragment); + ASSERT(hcf.numTransmittedControlResponses == 2); + ASSERT(recipientHandler->getAgreement(7, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == numManagementPacketsBeforeHcfReception); + hcf.processReceivedFrame(hcfFragments->at(1)); + ASSERT(hcf.numTransmittedControlResponses == 3); + ASSERT(recipientHandler->getAgreement(7, peer2) != nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == numManagementPacketsBeforeHcfReception + 1); + delete hcfFragments; + + auto verifyFragmentedActionRoundTrip = [&](const char *name, const Ptr& action, const std::function&)>& verify) { + auto bodyLength = (int)(action->getChunkLength() - commonManagementHeaderLength).get(); + auto firstFragmentLength = (bodyLength - 1) & ~1; + ASSERT(firstFragmentLength > 0 && firstFragmentLength < bodyLength); + auto packet = new Packet(name, action); + packet->insertAtBack(makeShared()); + Fragmentation fragmentation; + auto actionFragments = fragmentation.fragmentFrame(packet, { firstFragmentLength, bodyLength - firstFragmentLength }); + for (auto& fragment : *actionFragments) { + ASSERT(dynamicPtrCast(fragment->peekAtFront()) == nullptr); + ASSERT(fragment->getTag()->getActionFrame() != nullptr); + fragment = convertToWireRepresentation(fragment); + } + auto incompleteReception = receiveFragment(actionFragments->at(0)); + ASSERT(incompleteReception.completeHeader == nullptr); + auto completedReception = receiveFragment(actionFragments->at(1)); + ASSERT(completedReception.completeHeader != nullptr); + auto completedAction = dynamicPtrCast(completedReception.completeHeader); + ASSERT(completedAction != nullptr); + ASSERT(completedAction->getReceiverAddress() == action->getReceiverAddress()); + ASSERT(completedAction->getTransmitterAddress() == action->getTransmitterAddress()); + ASSERT(completedAction->getAddress3() == action->getAddress3()); + ASSERT(completedAction->getSequenceNumber() == action->getSequenceNumber()); + verify(completedAction); + delete actionFragments; + return completedAction; + }; + + auto fragmentedResponse = makeResponse(peer2, 5, 81, 0); + fragmentedResponse->setSequenceNumber(SequenceNumberCyclic(62)); + verifyFragmentedActionRoundTrip("fragmentedAddbaResponse", fragmentedResponse, [](const Ptr& action) { + auto response = dynamicPtrCast(action); + ASSERT(response != nullptr); + ASSERT(response->getTid() == 5); + ASSERT(response->getDialogToken() == 81); + ASSERT(response->getStatusCode() == 0); + }); + + for (bool initiator : { false, true }) { + if (initiator) { + auto delbaAgreementRequest = makeRequest(peer2, 6, 86, SequenceNumberCyclic(106), 32); + ASSERT(recipientHandler->processReceivedAddbaRequest(delbaAgreementRequest, &fragmentedPolicy, &fragmentedCallback, &fragmentedCallback) != nullptr); + ASSERT(recipientHandler->getAgreement(6, peer2) != nullptr); + } + auto fragmentedDelba = makeShared(); + fragmentedDelba->setTransmitterAddress(peer2); + fragmentedDelba->setSequenceNumber(SequenceNumberCyclic(initiator ? 64 : 63)); + fragmentedDelba->setInitiator(initiator); + fragmentedDelba->setTid(6); + fragmentedDelba->setReasonCode(39); + auto completeDelba = verifyFragmentedActionRoundTrip(initiator ? "fragmentedOriginatorDelba" : "fragmentedRecipientDelba", fragmentedDelba, [initiator](const Ptr& action) { + auto delba = dynamicPtrCast(action); + ASSERT(delba != nullptr); + ASSERT(delba->getInitiator() == initiator); + ASSERT(delba->getTid() == 6); + ASSERT(delba->getReasonCode() == 39); + }); + if (initiator) { + hcf.processReceivedManagementFrame(completeDelba); + ASSERT(recipientHandler->getAgreement(6, peer2) == nullptr); + } + } + + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + // IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.3. RecipientBlockAckAgreementHandler immediateRecipientHandler; TestRecipientPolicy recipientPolicy; @@ -2010,8 +2357,17 @@ ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == replacementImmediateA delba->setTid(3); delba->setInitiator(false); delba->setSequenceNumber(SequenceNumberCyclic(200)); + delba->setMoreFragments(true); + auto finalDelba = staticPtrCast(delba->dupShared()); + finalDelba->setMoreFragments(false); + finalDelba->setFragmentNumber(1); Packet delbaPacket("transmittedRecipientDelba", delba); hcf.processTransmittedManagement(&delbaPacket, delba, AC_BE); + ASSERT(recipientHandler->getAgreement(3, peer2) != nullptr); + ASSERT(deletionListener.numSignals == 0); + ASSERT(reorderDropListener.numOtherPacketDrops == 0); + Packet finalDelbaPacket("transmittedRecipientDelbaFinal", finalDelba); + hcf.processTransmittedManagement(&finalDelbaPacket, finalDelba, AC_BE); ASSERT(recipientHandler->getAgreement(3, peer2) == nullptr); ASSERT(deletionListener.numSignals == 1); ASSERT(reorderDropListener.numOtherPacketDrops == 1); From 74283b174284dc59b01255bf3fc3fbf786d93e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:03 +0200 Subject: [PATCH 06/16] ieee80211: cancel superseded management transactions Add an end-to-end management transaction cancellation contract from the AP through Ieee80211Mac to DCF or HCF. A terminal failure removes queued and in-progress sibling fragments across access categories and retires retry, delayed-IFS, and pending-transmission state exactly once. Keep a frame borrowed by the active frame sequence alive until a safe sequence boundary, then abort or release it through the owning component. Clear AP association state before cancellation so reentrant callbacks cannot observe or revive the superseded response. Cover DCF and HCF supersession, queue removal and overflow, delayed IFS, RTS protection, active frame sequences, and successful replacement. --- .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 9 + .../linklayer/ieee80211/mac/Ieee80211Mac.h | 5 +- src/inet/linklayer/ieee80211/mac/Tx.cc | 13 +- src/inet/linklayer/ieee80211/mac/Tx.h | 2 +- .../mac/contract/IFrameSequenceHandler.h | 16 +- .../IManagementFrameTransactionHandler.h | 37 + .../linklayer/ieee80211/mac/contract/ITx.h | 8 +- .../ieee80211/mac/coordinationfunction/Dcf.cc | 180 ++++- .../ieee80211/mac/coordinationfunction/Dcf.h | 20 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 169 ++++- .../ieee80211/mac/coordinationfunction/Hcf.h | 17 + .../mac/framesequence/FrameSequenceHandler.cc | 20 +- .../mac/framesequence/FrameSequenceHandler.h | 5 +- .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 20 +- .../ieee80211/mgmt/Ieee80211MgmtAp.h | 3 + .../module/Ieee80211MgmtApCancellation_1.test | 656 ++++++++++++++++++ .../module/Ieee80211MgmtApHcfQueueDrop_1.test | 58 ++ tests/module/Ieee80211MgmtApQueueDrop_1.test | 58 ++ tests/unit/Ieee80211AddbaTransaction_1.test | 449 +++++++++++- 19 files changed, 1720 insertions(+), 25 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h create mode 100644 tests/module/Ieee80211MgmtApCancellation_1.test diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index d87f581231f..c2900a704d0 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -393,6 +393,15 @@ void Ieee80211Mac::sendDownPendingRadioConfigMsg() } } +void Ieee80211Mac::cancelManagementTransaction(uint64_t transactionId) +{ + Enter_Method("cancelManagementTransaction"); + if (mib->qos) + hcf->cancelManagementTransaction(transactionId); + else + dcf->cancelManagementTransaction(transactionId); +} + void Ieee80211Mac::processUpperFrame(Packet *packet, const Ptr& header) { Enter_Method("processUpperFrame(\"%s\")", packet->getName()); diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h index 1114deaed00..e22f7e52e67 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h @@ -15,6 +15,7 @@ #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" #include "inet/linklayer/ieee80211/mac/contract/IRx.h" #include "inet/linklayer/ieee80211/mac/contract/ITx.h" +#include "inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h" #include "inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h" #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" #include "inet/linklayer/ieee80211/mac/coordinationfunction/Mcf.h" @@ -35,7 +36,7 @@ class Ieee80211MacHeader; * exact operation of the MAC depend on the plugged-in components (see IUpperMac, * IRx, ITx, IContention and other interface classes). */ -class INET_API Ieee80211Mac : public MacProtocolBase +class INET_API Ieee80211Mac : public MacProtocolBase, public IManagementFrameTransactionHandler { public: static simsignal_t frameTransmissionOutcomeSignal; @@ -107,6 +108,8 @@ class INET_API Ieee80211Mac : public MacProtocolBase virtual void sendDownFrame(Packet *frame); virtual void sendDownPendingRadioConfigMsg(); + virtual void cancelManagementTransaction(uint64_t transactionId) override; + virtual void processUpperFrame(Packet *packet, const Ptr& header); virtual void processLowerFrame(Packet *packet, const Ptr& header); }; diff --git a/src/inet/linklayer/ieee80211/mac/Tx.cc b/src/inet/linklayer/ieee80211/mac/Tx.cc index 9c0d906802b..053b2ae2519 100644 --- a/src/inet/linklayer/ieee80211/mac/Tx.cc +++ b/src/inet/linklayer/ieee80211/mac/Tx.cc @@ -82,6 +82,18 @@ void Tx::transmitFrame(Packet *packet, const Ptr& head scheduleAfter(ifs, endIfsTimer); } +bool Tx::cancelPendingTransmission(ITx::ICallback *owner) +{ + Enter_Method("cancelPendingTransmission"); + if (this->txCallback != owner || transmitting || frame == nullptr || !endIfsTimer->isScheduled()) + return false; + cancelEvent(endIfsTimer); + delete frame; + frame = nullptr; + txCallback = nullptr; + return true; +} + void Tx::radioTransmissionFinished() { Enter_Method("radioTransmissionFinished"); @@ -114,4 +126,3 @@ void Tx::handleMessage(cMessage *msg) } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/Tx.h b/src/inet/linklayer/ieee80211/mac/Tx.h index ae4c37308b6..c05151d28f5 100644 --- a/src/inet/linklayer/ieee80211/mac/Tx.h +++ b/src/inet/linklayer/ieee80211/mac/Tx.h @@ -41,6 +41,7 @@ class INET_API Tx : public SimpleModule, public ITx virtual void transmitFrame(Packet *packet, const Ptr& header, ITx::ICallback *txCallback) override; virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, ITx::ICallback *txCallback) override; + virtual bool cancelPendingTransmission(ITx::ICallback *owner) override; virtual void radioTransmissionFinished() override; }; @@ -48,4 +49,3 @@ class INET_API Tx : public SimpleModule, public ITx } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h index 46f9a820f28..d16cf826483 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h @@ -46,10 +46,24 @@ class INET_API IFrameSequenceHandler virtual void transmissionComplete() = 0; virtual bool isSequenceRunning() = 0; virtual void handleStartRxTimeout() = 0; + + /** + * Requests cancellation at the next ownership-safe frame-sequence + * boundary. An in-flight transmit step remains owned by the sequence + * until its physical transmission completion is delivered. + */ + virtual void cancelFrameSequence() {} + + /** + * Aborts a frame sequence immediately after its pending transmission has + * been cancelled before lower-layer handoff. This is separate from + * cancelFrameSequence() because an already handed-down copy must be + * allowed to complete first. + */ + virtual void abortFrameSequence() {} }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h new file mode 100644 index 00000000000..7b7c806bfef --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h @@ -0,0 +1,37 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IMANAGEMENTFRAMETRANSACTIONHANDLER_H +#define __INET_IMANAGEMENTFRAMETRANSACTIONHANDLER_H + +#include + +#include "inet/common/INETDefs.h" + +namespace inet { +namespace ieee80211 { + +/** + * Provides management-frame transaction lifecycle operations to management + * protocols without exposing coordination-function implementation details. + */ +class INET_API IManagementFrameTransactionHandler +{ + public: + virtual ~IManagementFrameTransactionHandler() {} + + /** + * Cancels all locally queued fragments for the given management + * transaction. An active transmission is retired at an ownership-safe + * frame-sequence boundary. + */ + virtual void cancelManagementTransaction(uint64_t transactionId) = 0; +}; + +} +} + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/contract/ITx.h b/src/inet/linklayer/ieee80211/mac/contract/ITx.h index 090969da65c..9bab6504727 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/ITx.h +++ b/src/inet/linklayer/ieee80211/mac/contract/ITx.h @@ -33,6 +33,13 @@ class INET_API ITx virtual void transmitFrame(Packet *packet, const Ptr& header, ICallback *callback) = 0; virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, ICallback *callback) = 0; + /** + * Cancels a transmission that is still waiting for its inter-frame + * space. The owner must be the callback supplied to transmitFrame(). + * Returns true only when no copy has been handed to the lower layer yet. + * A transmission that is already in progress is left untouched. + */ + virtual bool cancelPendingTransmission(ICallback *owner) = 0; virtual void radioTransmissionFinished() = 0; }; @@ -40,4 +47,3 @@ class INET_API ITx } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index 1aa1a7ba496..350e551ee30 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -14,6 +14,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" #include "inet/linklayer/ieee80211/mac/framesequence/DcfFs.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" @@ -143,13 +144,22 @@ void Dcf::receiveSignal(cComponent *source, simsignal_t signal, cObject *object, void Dcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) { Enter_Method("handlePacketRemoved"); - if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + auto transactionTag = packet->findTag(); + if ((reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED || reason == queueing::IPacketQueue::PacketRemovalReason::REMOVED) && transactionTag != nullptr) { + if (cancelManagementTransaction(transactionTag->getTransactionId(), packet)) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } } } +void Dcf::cancelManagementTransaction(uint64_t transactionId) +{ + Enter_Method("cancelManagementTransaction"); + cancelManagementTransaction(transactionId, nullptr); +} + void Dcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header) { emit(packetSentToPeerSignal, packet); @@ -161,6 +171,135 @@ void Dcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const throw cRuntimeError("Unknown control response frame"); } +bool Dcf::isPacketReferencedByCurrentFrameSequence(const Packet *packet) const +{ + if (frameSequenceHandler == nullptr || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + for (int i = 0; i < context->getNumSteps(); i++) { + auto transmitStep = dynamic_cast(context->getStep(i)); + if (transmitStep != nullptr && transmitStep->getFrameToTransmit() == packet) + return true; + auto rtsTransmitStep = dynamic_cast(transmitStep); + if (rtsTransmitStep != nullptr && rtsTransmitStep->getProtectedFrame() == packet) + return true; + } + return false; +} + +bool Dcf::isManagementTransactionCancelled(const Packet *packet) const +{ + if (packet == nullptr) + return false; + auto transactionTag = packet->findTag(); + return transactionTag != nullptr && cancelledManagementTransactions.find(transactionTag->getTransactionId()) != cancelledManagementTransactions.end(); +} + +bool Dcf::isCurrentFrameSequenceCancelled(const Packet *packet) const +{ + if (packet == nullptr || frameSequenceHandler == nullptr || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + auto transmitStep = dynamic_cast(context->getLastStep()); + if (transmitStep == nullptr) + transmitStep = dynamic_cast(context->getStepBeforeLast()); + if (transmitStep == nullptr) + return false; + if (transmitStep->getFrameToTransmit() == packet) + return isManagementTransactionCancelled(packet) || (dynamic_cast(transmitStep) != nullptr && + isManagementTransactionCancelled(dynamic_cast(transmitStep)->getProtectedFrame())); + auto rtsTransmitStep = dynamic_cast(transmitStep); + return rtsTransmitStep != nullptr && rtsTransmitStep->getProtectedFrame() == packet && isManagementTransactionCancelled(packet); +} + +bool Dcf::cancelManagementTransaction(uint64_t transactionId, Packet *excludedPacket) +{ + Enter_Method("cancelManagementTransaction"); + auto eventNumber = cSimulation::getActiveSimulation()->getEventNumber(); + if (completedManagementTransactionsEventNumber != eventNumber) { + completedManagementTransactions.clear(); + completedManagementTransactionsEventNumber = eventNumber; + } + if (completedManagementTransactions.find(transactionId) != completedManagementTransactions.end()) + return false; + if (!managementTransactionsBeingCancelled.insert(transactionId).second) + return false; + + // IEEE Std 802.11-2024, 10.3.4.4 and 10.4: terminal retry/lifetime + // failure discards the MMPDU and all remaining fragments. The callback's + // packet is still borrowed by the active frame sequence, so it is left + // for the caller to retire after this helper returns. + + auto belongsToTransaction = [transactionId, excludedPacket](Packet *packet) { + auto transactionTag = packet->findTag(); + return packet != excludedPacket && transactionTag != nullptr && transactionTag->getTransactionId() == transactionId; + }; + + auto pendingQueue = channelAccess->getPendingQueue(); + for (int i = pendingQueue->getNumPackets() - 1; i >= 0; i--) { + auto packet = pendingQueue->getPacket(i); + if (belongsToTransaction(packet)) { + pendingQueue->removePacket(packet); + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + + auto inProgressFrames = channelAccess->getInProgressFrames(); + bool frameSequenceCancellationRequested = false; + bool pendingTransmissionCancelled = false; + for (int i = inProgressFrames->getLength() - 1; i >= 0; i--) { + auto packet = inProgressFrames->getFrames(i); + if (belongsToTransaction(packet)) { + if (isPacketReferencedByCurrentFrameSequence(packet)) { + // Keep the packet alive for raw pointers held by the active + // sequence, but remove it from eligibility immediately. The + // sequence retires it at its next safe boundary. + inProgressFrames->dropFrame(packet); + cancelledManagementTransactions.insert(transactionId); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + bool currentFrameSequenceCancelled = isCurrentFrameSequenceCancelled(packet); + frameSequenceCancellationRequested |= currentFrameSequenceCancelled; + // A sequence can retain completed steps in its context. Only + // cancel Tx when the current transmit/protected step belongs + // to this transaction; the callback owner alone is not enough + // to identify a historical frame. + if (currentFrameSequenceCancelled && tx != nullptr) + pendingTransmissionCancelled |= tx->cancelPendingTransmission(this); + continue; + } + auto header = packet->peekAtFront(); + auto extractedPacket = inProgressFrames->extractFrame(packet); + ASSERT(extractedPacket == packet); + take(packet); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardFrame(packet, header); + ackHandler->dropFrame(header); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + if (frameSequenceCancellationRequested && frameSequenceHandler != nullptr) { + frameSequenceHandler->cancelFrameSequence(); + if (pendingTransmissionCancelled) + frameSequenceHandler->abortFrameSequence(); + } + managementTransactionsBeingCancelled.erase(transactionId); + completedManagementTransactions.insert(transactionId); + return true; +} + void Dcf::scheduleStartRxTimer(simtime_t timeout) { Enter_Method("scheduleStartRxTimer"); @@ -228,6 +367,7 @@ void Dcf::frameSequenceFinished() if (hasFrameToTransmit()) channelAccess->requestChannel(this); mac->sendDownPendingRadioConfigMsg(); // TODO review + cancelledManagementTransactions.clear(); } bool Dcf::isReceptionInProgress() @@ -290,12 +430,24 @@ bool Dcf::hasFrameToTransmit() void Dcf::originatorProcessRtsProtectionFailed(Packet *packet) { Enter_Method("originatorProcessRtsProtectionFailed"); + if (isManagementTransactionCancelled(packet)) { + auto protectedHeader = packet->peekAtFront(); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardRtsFrame(protectedHeader); + channelAccess->getInProgressFrames()->dropFrame(packet); + if (ackHandler != nullptr) + ackHandler->dropFrame(protectedHeader); + return; + } EV_INFO << "RTS frame transmission failed\n"; auto protectedHeader = packet->peekAtFront(); recoveryProcedure->rtsFrameTransmissionFailed(protectedHeader, stationRetryCounters); EV_INFO << "For the current frame exchange, we have CW = " << channelAccess->getCw() << " SRC = " << recoveryProcedure->getShortRetryCount(packet, protectedHeader) << " LRC = " << recoveryProcedure->getLongRetryCount(packet, protectedHeader) << " SSRC = " << stationRetryCounters->getStationShortRetryCount() << " and SLRC = " << stationRetryCounters->getStationLongRetryCount() << std::endl; if (recoveryProcedure->isRtsFrameRetryLimitReached(packet, protectedHeader)) { recoveryProcedure->retryLimitReached(packet, protectedHeader); + auto transactionTag = packet->findTag(); + bool notifyManagement = dynamicPtrCast(protectedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), packet)); channelAccess->getInProgressFrames()->dropFrame(packet); ackHandler->dropFrame(protectedHeader); EV_INFO << "Dropping RTS/CTS protected frame " << packet->getName() << ", because retry limit is reached.\n"; @@ -304,7 +456,7 @@ void Dcf::originatorProcessRtsProtectionFailed(Packet *packet) details.setLimit(recoveryProcedure->getShortRetryLimit()); emit(packetDroppedSignal, packet, &details); emit(linkBrokenSignal, packet); - if (dynamicPtrCast(protectedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); @@ -315,8 +467,12 @@ void Dcf::originatorProcessRtsProtectionFailed(Packet *packet) void Dcf::originatorProcessTransmittedFrame(Packet *packet) { Enter_Method("originatorProcessTransmittedFrame"); + if (isCurrentFrameSequenceCancelled(packet)) + return; EV_INFO << "Processing transmitted frame " << packet->getName() << " as originator in frame sequence.\n"; emit(packetSentToPeerSignal, packet); + if (isCurrentFrameSequenceCancelled(packet)) + return; auto transmittedHeader = packet->peekAtFront(); if (auto dataOrMgmtHeader = dynamicPtrCast(transmittedHeader)) { EV_INFO << "For the current frame exchange, we have CW = " << channelAccess->getCw() << " SRC = " << recoveryProcedure->getShortRetryCount(packet, dataOrMgmtHeader) << " LRC = " << recoveryProcedure->getLongRetryCount(packet, dataOrMgmtHeader) << " SSRC = " << stationRetryCounters->getStationShortRetryCount() << " and SLRC = " << stationRetryCounters->getStationLongRetryCount() << std::endl; @@ -370,6 +526,15 @@ void Dcf::originatorProcessReceivedFrame(Packet *receivedPacket, Packet *lastTra void Dcf::originatorProcessFailedFrame(Packet *failedPacket) { Enter_Method("originatorProcessFailedFrame"); + if (isManagementTransactionCancelled(failedPacket)) { + auto failedHeader = failedPacket->peekAtFront(); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardFrame(failedPacket, failedHeader); + channelAccess->getInProgressFrames()->dropFrame(failedPacket); + if (ackHandler != nullptr) + ackHandler->dropFrame(failedHeader); + return; + } EV_INFO << "Data/Mgmt frame transmission failed\n"; const auto& failedHeader = failedPacket->peekAtFront(); ASSERT(failedHeader->getType() != ST_DATA_WITH_QOS); @@ -383,6 +548,9 @@ void Dcf::originatorProcessFailedFrame(Packet *failedPacket) ackHandler->processFailedFrame(failedHeader); if (retryLimitReached) { recoveryProcedure->retryLimitReached(failedPacket, failedHeader); + auto transactionTag = failedPacket->findTag(); + bool notifyManagement = dynamicPtrCast(failedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), failedPacket)); channelAccess->getInProgressFrames()->dropFrame(failedPacket); ackHandler->dropFrame(failedHeader); EV_INFO << "Dropping frame " << failedPacket->getName() << ", because retry limit is reached.\n"; @@ -391,7 +559,7 @@ void Dcf::originatorProcessFailedFrame(Packet *failedPacket) details.setLimit(-1); // TODO emit(packetDroppedSignal, failedPacket, &details); emit(linkBrokenSignal, failedPacket); - if (dynamicPtrCast(failedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h index 87a4d1ab227..215c34eaab0 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h @@ -8,6 +8,8 @@ #ifndef __INET_DCF_H #define __INET_DCF_H +#include + #include "inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h" #include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" #include "inet/linklayer/ieee80211/mac/contract/ICoordinationFunction.h" @@ -80,6 +82,17 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: // Frame sequence handler IFrameSequenceHandler *frameSequenceHandler = nullptr; + // A management transaction may have several fragmented MPDUs in the + // pending/in-progress queues. Keep the transaction identity only while + // removing its siblings so queue notifications cannot report the same logical + // transaction recursively. A transaction has one pending original before + // fragmentation; the completed set additionally spans the synchronous + // notifications of a bulk queue removal and is cleared at the next event. + std::set managementTransactionsBeingCancelled; + std::set completedManagementTransactions; + eventnumber_t completedManagementTransactionsEventNumber = -1; + std::set cancelledManagementTransactions; + // Station counters StationRetryCounters *stationRetryCounters = nullptr; @@ -98,6 +111,11 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header); virtual void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header); + virtual bool isPacketReferencedByCurrentFrameSequence(const Packet *packet) const; + virtual bool isManagementTransactionCancelled(const Packet *packet) const; + virtual bool isCurrentFrameSequenceCancelled(const Packet *packet) const; + virtual bool cancelManagementTransaction(uint64_t transactionId, Packet *excludedPacket); + protected: // IChannelAccess::ICallback virtual void channelGranted(IChannelAccess *channelAccess) override; @@ -126,6 +144,8 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: public: virtual ~Dcf(); + virtual void cancelManagementTransaction(uint64_t transactionId); + // ICoordinationFunction virtual void processUpperFrame(Packet *packet, const Ptr& header) override; virtual void processLowerFrame(Packet *packet, const Ptr& header) override; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 37c11a6de03..b4b7f41a623 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -175,7 +175,8 @@ void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemo // HCF treats explicit REMOVED notifications as terminal transaction // disposal; code relocating a packet must use dequeuePacket(). if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED || reason == queueing::IPacketQueue::PacketRemovalReason::REMOVED) { - if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { + auto transactionTag = packet->findTag(); + if (transactionTag != nullptr && cancelManagementTransaction(transactionTag->getTransactionId(), packet)) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); @@ -187,6 +188,99 @@ void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemo resumeEligibleChannelAccess(); } +void Hcf::cancelManagementTransaction(uint64_t transactionId) +{ + Enter_Method("cancelManagementTransaction"); + cancelManagementTransaction(transactionId, nullptr); +} + +bool Hcf::cancelManagementTransaction(uint64_t transactionId, Packet *excludedPacket) +{ + Enter_Method("cancelManagementTransaction"); + auto eventNumber = cSimulation::getActiveSimulation()->getEventNumber(); + if (completedManagementTransactionsEventNumber != eventNumber) { + completedManagementTransactions.clear(); + completedManagementTransactionsEventNumber = eventNumber; + } + if (completedManagementTransactions.find(transactionId) != completedManagementTransactions.end()) + return false; + if (!managementTransactionsBeingCancelled.insert(transactionId).second) + return false; + + // IEEE Std 802.11-2024, 10.3.4.4 and 10.4: terminal retry/lifetime + // failure discards the MMPDU and all remaining fragments. The callback's + // packet is still borrowed by the active frame sequence, so it is left + // for the caller to retire after this helper returns. + + auto belongsToTransaction = [transactionId, excludedPacket](Packet *packet) { + auto transactionTag = packet->findTag(); + return packet != excludedPacket && transactionTag != nullptr && transactionTag->getTransactionId() == transactionId; + }; + bool frameSequenceCancellationRequested = false; + bool pendingTransmissionCancelled = false; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto edcaf = edca->getEdcaf(AccessCategory(ac)); + auto pendingQueue = edcaf->getPendingQueue(); + for (int i = pendingQueue->getNumPackets() - 1; i >= 0; i--) { + auto packet = pendingQueue->getPacket(i); + if (belongsToTransaction(packet)) { + pendingQueue->removePacket(packet); + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + auto inProgressFrames = edcaf->getInProgressFrames(); + for (int i = inProgressFrames->getLength() - 1; i >= 0; i--) { + auto packet = inProgressFrames->getFrames(i); + if (belongsToTransaction(packet)) { + if (isPacketReferencedByCurrentFrameSequence(packet)) { + // Keep the packet alive for raw pointers held by the + // active sequence, but remove it from eligibility + // immediately. The sequence retires it at its next safe + // boundary. + inProgressFrames->dropFrame(packet); + cancelledManagementTransactions.insert(transactionId); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + bool currentFrameSequenceCancelled = isCurrentFrameSequenceCancelled(packet); + frameSequenceCancellationRequested |= currentFrameSequenceCancelled; + // A sequence can retain completed steps in its context. + // Only cancel Tx when the current transmit/protected step + // belongs to this transaction; the callback owner alone + // is not enough to identify a historical frame. + if (currentFrameSequenceCancelled && tx != nullptr) + pendingTransmissionCancelled |= tx->cancelPendingTransmission(this); + continue; + } + auto header = packet->peekAtFront(); + auto extractedPacket = inProgressFrames->extractFrame(packet); + ASSERT(extractedPacket == packet); + take(packet); + auto managementRecoveryProcedure = edca->getMgmtAndNonQoSRecoveryProcedure(); + if (managementRecoveryProcedure != nullptr) + managementRecoveryProcedure->discardFrame(packet, header); + edcaf->getAckHandler()->dropFrame(header); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + } + if (frameSequenceCancellationRequested && frameSequenceHandler != nullptr) { + frameSequenceHandler->cancelFrameSequence(); + if (pendingTransmissionCancelled) + frameSequenceHandler->abortFrameSequence(); + } + managementTransactionsBeingCancelled.erase(transactionId); + completedManagementTransactions.insert(transactionId); + return true; +} + std::string Hcf::getFrameSequenceInfo() const { if (!frameSequenceHandler->isSequenceRunning()) @@ -296,6 +390,33 @@ bool Hcf::isPacketReferencedByCurrentFrameSequence(const Packet *packet) const return false; } +bool Hcf::isManagementTransactionCancelled(const Packet *packet) const +{ + if (packet == nullptr) + return false; + auto transactionTag = packet->findTag(); + return transactionTag != nullptr && cancelledManagementTransactions.find(transactionTag->getTransactionId()) != cancelledManagementTransactions.end(); +} + +bool Hcf::isCurrentFrameSequenceCancelled(const Packet *packet) const +{ + if (packet == nullptr || frameSequenceHandler == nullptr || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + auto transmitStep = dynamic_cast(context->getLastStep()); + if (transmitStep == nullptr) + transmitStep = dynamic_cast(context->getStepBeforeLast()); + if (transmitStep == nullptr) + return false; + if (transmitStep->getFrameToTransmit() == packet) + return isManagementTransactionCancelled(packet) || (dynamic_cast(transmitStep) != nullptr && + isManagementTransactionCancelled(dynamic_cast(transmitStep)->getProtectedFrame())); + auto rtsTransmitStep = dynamic_cast(transmitStep); + return rtsTransmitStep != nullptr && rtsTransmitStep->getProtectedFrame() == packet && isManagementTransactionCancelled(packet); +} + void Hcf::scheduleStartRxTimer(simtime_t timeout) { Enter_Method("scheduleStartRxTimer"); @@ -471,6 +592,9 @@ int Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) else if (auto mgmtHeader = dynamicPtrCast(internallyCollidedHeader)) edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(internallyCollidedFrame, mgmtHeader); else ; // TODO + NonQoSDataFrame + auto transactionTag = internallyCollidedFrame->findTag(); + bool notifyManagement = dynamicPtrCast(internallyCollidedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), internallyCollidedFrame)); edcaf->getInProgressFrames()->dropFrame(internallyCollidedFrame); processDroppedBlockAckSetupFrame(internallyCollidedFrame); processDroppedBlockAckTeardownFrame(internallyCollidedFrame); @@ -480,7 +604,7 @@ int Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) details.setLimit(-1); // TODO emit(packetDroppedSignal, internallyCollidedFrame, &details); emit(linkBrokenSignal, internallyCollidedFrame); - if (dynamicPtrCast(internallyCollidedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, internallyCollidedFrame, &transmissionDetails); @@ -519,6 +643,7 @@ void Hcf::frameSequenceFinished() } else throw cRuntimeError("Frame sequence finished but channel owner not found!"); + cancelledManagementTransactions.clear(); } void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr& header) @@ -644,6 +769,19 @@ void Hcf::transmissionComplete(Packet *packet, const PtrpeekAtFront(); + auto edcaf = edca->getChannelOwner(); + if (edcaf != nullptr) { + auto recoveryProcedure = edca->getMgmtAndNonQoSRecoveryProcedure(); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardRtsFrame(protectedHeader); + edcaf->getInProgressFrames()->dropFrame(packet); + if (edcaf->getAckHandler() != nullptr) + edcaf->getAckHandler()->dropFrame(protectedHeader); + } + return; + } auto protectedHeader = packet->peekAtFront(); auto edcaf = edca->getChannelOwner(); if (edcaf) { @@ -671,6 +809,9 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) } else edca->getMgmtAndNonQoSRecoveryProcedure()->discardRtsFrame(addbaRequest); + auto transactionTag = packet->findTag(); + bool notifyManagement = retryLimitReached && dynamicPtrCast(protectedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), packet)); edcaf->getInProgressFrames()->dropFrame(packet); processDroppedBlockAckSetupFrame(packet); processDroppedBlockAckTeardownFrame(packet); @@ -683,7 +824,7 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) emit(packetDroppedSignal, packet, &details); if (retryLimitReached) { emit(linkBrokenSignal, packet); - if (dynamicPtrCast(protectedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); @@ -698,11 +839,15 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) void Hcf::originatorProcessTransmittedFrame(Packet *packet) { Enter_Method("originatorProcessTransmittedFrame"); + if (isCurrentFrameSequenceCancelled(packet)) + return; EV_INFO << "Processing transmitted frame " << packet->getName() << " as originator in frame sequence.\n"; auto transmittedHeader = packet->peekAtFront(); auto edcaf = edca->getChannelOwner(); if (edcaf) { edcaf->emit(packetSentToPeerSignal, packet); + if (isCurrentFrameSequenceCancelled(packet)) + return; AccessCategory ac = edcaf->getAccessCategory(); if (transmittedHeader->getReceiverAddress().isMulticast()) { edcaf->getRecoveryProcedure()->multicastFrameTransmitted(); @@ -778,6 +923,19 @@ void Hcf::originatorProcessTransmittedControlFrame(const PtrpeekAtFront(); + auto edcaf = edca->getChannelOwner(); + if (edcaf != nullptr) { + auto recoveryProcedure = edca->getMgmtAndNonQoSRecoveryProcedure(); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardFrame(failedPacket, failedHeader); + edcaf->getInProgressFrames()->dropFrame(failedPacket); + if (edcaf->getAckHandler() != nullptr) + edcaf->getAckHandler()->dropFrame(failedHeader); + } + return; + } auto failedHeader = failedPacket->peekAtFront(); auto edcaf = edca->getChannelOwner(); if (edcaf) { @@ -820,6 +978,9 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) } else edca->getMgmtAndNonQoSRecoveryProcedure()->discardFrame(failedPacket, addbaRequest); + auto transactionTag = failedPacket->findTag(); + bool notifyManagement = retryLimitReached && dynamicPtrCast(failedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), failedPacket)); edcaf->getInProgressFrames()->dropFrame(failedPacket); processDroppedBlockAckSetupFrame(failedPacket); processDroppedBlockAckTeardownFrame(failedPacket); @@ -832,7 +993,7 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) emit(packetDroppedSignal, failedPacket, &details); if (retryLimitReached) { emit(linkBrokenSignal, failedPacket); - if (dynamicPtrCast(failedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index fc9b5d49eff..b2230bff27b 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -10,6 +10,7 @@ #include #include +#include #include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h" @@ -111,6 +112,17 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: // Frame sequence handler IFrameSequenceHandler *frameSequenceHandler = nullptr; + // A management transaction may have several fragmented MPDUs in the + // pending/in-progress queues. Keep the transaction identity only while + // removing its siblings so queue notifications cannot report the same logical + // transaction recursively. A transaction has one pending original before + // fragmentation; the completed set additionally spans the synchronous + // notifications of a bulk queue removal and is cleared at the next event. + std::set managementTransactionsBeingCancelled; + std::set completedManagementTransactions; + eventnumber_t completedManagementTransactionsEventNumber = -1; + std::set cancelledManagementTransactions; + // Protection mechanisms SingleProtectionMechanism *singleProtectionMechanism = nullptr; @@ -133,6 +145,9 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual bool processDroppedBlockAckSetupFrame(Packet *packet); virtual bool processDroppedBlockAckTeardownFrame(Packet *packet); virtual bool isPacketReferencedByCurrentFrameSequence(const Packet *packet) const; + virtual bool isManagementTransactionCancelled(const Packet *packet) const; + virtual bool isCurrentFrameSequenceCancelled(const Packet *packet) const; + virtual bool cancelManagementTransaction(uint64_t transactionId, Packet *excludedPacket); virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); virtual void trackPendingFrame(Packet *packet, AccessCategory accessCategory); @@ -188,6 +203,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: public: virtual ~Hcf(); + virtual void cancelManagementTransaction(uint64_t transactionId); + // ICoordinationFunction virtual void processUpperFrame(Packet *packet, const Ptr& header) override; virtual void processLowerFrame(Packet *packet, const Ptr& header) override; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc index 1eddb2b2048..a2dda4b570f 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc @@ -31,6 +31,11 @@ void FrameSequenceHandler::handleStartRxTimeout() void FrameSequenceHandler::processResponse(Packet *frame) { ASSERT(callback != nullptr); + if (frameSequenceCancellationRequested) { + delete frame; + abortFrameSequence(); + return; + } auto lastStep = context->getLastStep(); switch (lastStep->getType()) { case IFrameSequenceStep::Type::RECEIVE: { @@ -52,7 +57,10 @@ void FrameSequenceHandler::processResponse(Packet *frame) void FrameSequenceHandler::transmissionComplete() { if (isSequenceRunning()) { - finishFrameSequenceStep(); + if (frameSequenceCancellationRequested) + abortFrameSequence(); + else + finishFrameSequenceStep(); if (isSequenceRunning()) startFrameSequenceStep(); } @@ -63,6 +71,7 @@ void FrameSequenceHandler::startFrameSequence(IFrameSequence *frameSequence, Fra EV_INFO << "Starting frame sequence.\n"; this->callback = callback; if (!isSequenceRunning()) { + frameSequenceCancellationRequested = false; this->frameSequence = frameSequence; this->context = context; frameSequence->startSequence(context, 0); @@ -75,6 +84,10 @@ void FrameSequenceHandler::startFrameSequence(IFrameSequence *frameSequence, Fra void FrameSequenceHandler::startFrameSequenceStep() { ASSERT(isSequenceRunning()); + if (frameSequenceCancellationRequested) { + abortFrameSequence(); + return; + } auto nextStep = frameSequence->prepareStep(context); EV_INFO << "Starting next frame sequence step: history = " << frameSequence->getHistory() << "\n"; if (nextStep == nullptr) @@ -119,6 +132,8 @@ void FrameSequenceHandler::finishFrameSequenceStep() case IFrameSequenceStep::Type::TRANSMIT: { auto transmitStep = static_cast(lastStep); callback->originatorProcessTransmittedFrame(transmitStep->getFrameToTransmit()); + if (frameSequenceCancellationRequested && isSequenceRunning()) + abortFrameSequence(); break; } case IFrameSequenceStep::Type::RECEIVE: { @@ -143,6 +158,7 @@ void FrameSequenceHandler::finishFrameSequence() context = nullptr; frameSequence = nullptr; callback = nullptr; + frameSequenceCancellationRequested = false; inProgressFrames->clearDroppedFrames(); } @@ -166,6 +182,7 @@ void FrameSequenceHandler::abortFrameSequence() context = nullptr; frameSequence = nullptr; callback = nullptr; + frameSequenceCancellationRequested = false; inProgressFrames->clearDroppedFrames(); } @@ -177,4 +194,3 @@ FrameSequenceHandler::~FrameSequenceHandler() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h index 370295cc6f7..9b423609170 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h @@ -21,12 +21,12 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler IFrameSequenceHandler::ICallback *callback = nullptr; IFrameSequence *frameSequence = nullptr; FrameSequenceContext *context = nullptr; + bool frameSequenceCancellationRequested = false; protected: virtual void startFrameSequenceStep(); virtual void finishFrameSequenceStep(); virtual void finishFrameSequence(); - virtual void abortFrameSequence(); public: virtual const FrameSequenceContext *getContext() const override { return context; } @@ -36,6 +36,8 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler virtual void transmissionComplete() override; virtual void handleStartRxTimeout() override; virtual bool isSequenceRunning() override { return frameSequence != nullptr; } + virtual void cancelFrameSequence() override { if (isSequenceRunning()) frameSequenceCancellationRequested = true; } + virtual void abortFrameSequence() override; virtual ~FrameSequenceHandler(); }; @@ -44,4 +46,3 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index baad0d6e357..a4a09ce78b6 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -62,6 +62,7 @@ void Ieee80211MgmtAp::initialize(int stage) beaconTimer = new cMessage("beaconTimer"); auto macModule = getModuleFromPar(par("macModule"), this); macModule->subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, this); + managementFrameTransactionHandler.reference(this, "macModule", true); } } @@ -206,6 +207,17 @@ void Ieee80211MgmtAp::clearPendingAssociation(StaInfo *sta) sta->pendingHtOperation = Ieee80211HtOperation(); } +void Ieee80211MgmtAp::supersedePendingAssociation(StaInfo *sta) +{ + auto transactionId = sta->pendingAssociationTransactionId; + // Clear the AP bookkeeping before entering the MAC so synchronous queue + // callbacks cannot mistake this superseded frame for the active + // transaction. + clearPendingAssociation(sta); + if (transactionId != 0 && managementFrameTransactionHandler) + managementFrameTransactionHandler->cancelManagementTransaction(transactionId); +} + void Ieee80211MgmtAp::sendBeacon() { EV << "Sending beacon\n"; @@ -250,7 +262,7 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; if (wasAssociated) mib->releaseAssociationId(sta->address); @@ -313,7 +325,7 @@ void Ieee80211MgmtAp::handleDeauthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; // mark STA as not authenticated; alternatively, it could also be removed from staList if (wasAssociated) @@ -494,7 +506,7 @@ void Ieee80211MgmtAp::handleDisassociationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; if (wasAssociated) mib->releaseAssociationId(sta->address); @@ -569,6 +581,8 @@ void Ieee80211MgmtAp::start() void Ieee80211MgmtAp::stop() { cancelEvent(beaconTimer); + for (auto& entry : staList) + supersedePendingAssociation(&entry.second); staList.clear(); nextAssociationTransactionId = 0; mib->clearAssociationIds(); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h index fe8c0c03192..afe48285491 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h @@ -11,6 +11,7 @@ #include #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.h" namespace inet { @@ -73,6 +74,7 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase StaList staList; ///< list of STAs cMessage *beaconTimer = nullptr; uint64_t nextAssociationTransactionId = 0; + ModuleRefByPar managementFrameTransactionHandler; public: Ieee80211MgmtAp() {} @@ -101,6 +103,7 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase virtual uint64_t createAssociationTransactionId(); virtual void clearPendingAssociation(StaInfo *sta); + virtual void supersedePendingAssociation(StaInfo *sta); /** Classifies a terminal management-MPDU result using its transaction tag and fragment state. */ static AssociationResponseDisposition getAssociationResponseDisposition(const Packet *responseFrame, diff --git a/tests/module/Ieee80211MgmtApCancellation_1.test b/tests/module/Ieee80211MgmtApCancellation_1.test new file mode 100644 index 00000000000..19da98e10df --- /dev/null +++ b/tests/module/Ieee80211MgmtApCancellation_1.test @@ -0,0 +1,656 @@ +%description: +Verify that superseding an AP association response cancels every tagged +fragment owned by the real DCF or HCF MAC. A deterministic contention module +holds the response until the test grants access, and a test MAC holds the +first frame handoff so authentication restart, deauthentication, and +disassociation can all cancel an active response before it reaches the PHY. +The test also cancels a DCF response after CTS and a HCF response during its +IFS, before either response crosses the Tx-to-MAC handoff. Replacement +responses are then completed through the ordinary ACK path. + +%file: TestIeee80211MgmtApCancellation.cc + +#include +#include +#include + +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/Tx.h" +#include "inet/linklayer/ieee80211/mac/contention/Contention.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/contract/IContention.h" +#include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" +#include "inet/queueing/contract/IPacketQueue.h" + +namespace inet { +namespace ieee80211 { + +class TestBlockedContention : public Contention +{ + protected: + IContention::ICallback *blockedCallback = nullptr; + + public: + virtual void startContention(int, simtime_t, simtime_t, simtime_t, IContention::ICallback *callback) override + { + Enter_Method("startContention"); + ASSERT(blockedCallback == nullptr); + blockedCallback = callback; + } + + virtual bool isContentionInProgress() override { return blockedCallback != nullptr; } + + bool hasBlockedContention() const { return blockedCallback != nullptr; } + + void grantAccess() + { + Enter_Method("grantAccess"); + ASSERT(blockedCallback != nullptr); + auto callback = blockedCallback; + blockedCallback = nullptr; + callback->channelAccessGranted(); + } +}; + +Define_Module(TestBlockedContention); + +class TestInspectableTx : public Tx +{ + protected: + bool holdDelayedAssociationResponse = false; + + public: + bool isWaitingForIfs() const { return frame != nullptr && !transmitting && endIfsTimer != nullptr && endIfsTimer->isScheduled(); } + + void setHoldDelayedAssociationResponse(bool hold) { holdDelayedAssociationResponse = hold; } + + virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, ITx::ICallback *callback) override + { + Enter_Method("transmitFrame"); + bool hold = holdDelayedAssociationResponse && ifs > 0 && (header->getType() == ST_ASSOCIATIONRESPONSE || header->getType() == ST_REASSOCIATIONRESPONSE); + Tx::transmitFrame(packet, header, ifs, callback); + if (hold && !transmitting && endIfsTimer->isScheduled()) { + cancelEvent(endIfsTimer); + scheduleAfter(SimTime(1, SIMTIME_S), endIfsTimer); + } + } +}; + +Define_Module(TestInspectableTx); + +class TestHoldingIeee80211Mac : public Ieee80211Mac +{ + protected: + Packet *heldFrame = nullptr; + bool holdNextFrame = true; + int rtsHandoffCount = 0; + int ctsReceivedCount = 0; + std::vector managementHandoffTransactionIds; + + void discardHeldFrame() + { + if (heldFrame != nullptr) { + auto frame = heldFrame; + heldFrame = nullptr; + if (frame->getOwner() == this) + drop(frame); + delete frame; + } + } + + public: + virtual ~TestHoldingIeee80211Mac() { discardHeldFrame(); } + + virtual void sendDownFrame(Packet *frame) override + { + auto header = frame->peekAtFront(); + if (header->getType() == ST_RTS) + rtsHandoffCount++; + if (header->getType() == ST_ASSOCIATIONRESPONSE || header->getType() == ST_REASSOCIATIONRESPONSE) { + auto transactionTag = frame->findTag(); + if (transactionTag != nullptr) + managementHandoffTransactionIds.push_back(transactionTag->getTransactionId()); + } + if (holdNextFrame) { + ASSERT(heldFrame == nullptr); + holdNextFrame = false; + take(frame); + heldFrame = frame; + } + else + Ieee80211Mac::sendDownFrame(frame); + } + + virtual void processLowerFrame(Packet *packet, const Ptr& header) override + { + if (header->getType() == ST_CTS) + ctsReceivedCount++; + Ieee80211Mac::processLowerFrame(packet, header); + } + + bool hasHeldFrame() const { return heldFrame != nullptr; } + + bool isWaitingForIfs() const + { + auto tx = check_and_cast(getSubmodule("tx")); + return tx->isWaitingForIfs(); + } + + int getRtsHandoffCount() const { return rtsHandoffCount; } + int getCtsReceivedCount() const { return ctsReceivedCount; } + int getManagementHandoffCount(uint64_t transactionId) const { return std::count(managementHandoffTransactionIds.begin(), managementHandoffTransactionIds.end(), transactionId); } + + void disarmHold() + { + Enter_Method("disarmHold"); + ASSERT(heldFrame == nullptr); + holdNextFrame = false; + } + + void armDelayedAssociationResponseHold() + { + Enter_Method("armDelayedAssociationResponseHold"); + auto tx = check_and_cast(getSubmodule("tx")); + tx->setHoldDelayedAssociationResponse(true); + } + + void disarmDelayedAssociationResponseHold() + { + Enter_Method("disarmDelayedAssociationResponseHold"); + auto tx = check_and_cast(getSubmodule("tx")); + tx->setHoldDelayedAssociationResponse(false); + } + + void armHold() + { + Enter_Method("armHold"); + ASSERT(heldFrame == nullptr); + holdNextFrame = true; + } + + void completeHeldTransmission() + { + Enter_Method("completeHeldTransmission"); + ASSERT(heldFrame != nullptr); + auto tx = check_and_cast(getSubmodule("tx")); + tx->radioTransmissionFinished(); + discardHeldFrame(); + } +}; + +Define_Module(TestHoldingIeee80211Mac); + +class TestIeee80211MgmtApCancellation : public Ieee80211MgmtAp +{ + public: + std::vector responseStatuses; + std::vector responseTransactionIds; + + virtual void start() override + { + // Keep beacon traffic out of the deterministic transaction exchange. + Ieee80211MgmtApBase::start(); + } + + virtual void sendManagementFrame(const char *name, const Ptr& body, int subtype, + const MacAddress& destAddr, uint64_t transactionId = 0) override + { + // Authentication is driven directly by the test and its response is + // irrelevant to the AP association-response transaction. + if (subtype == ST_AUTHENTICATION) + return; + Ieee80211MgmtAp::sendManagementFrame(name, body, subtype, destAddr, transactionId); + } + + void markAuthenticated(const MacAddress& address) + { + Enter_Method("markAuthenticated"); + auto& sta = staList[address]; + sta.address = address; + sta.authSeqExpected = 1; + mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + } + + void submitAssociationRequest(const MacAddress& address) + { + Enter_Method("submitAssociationRequest"); + auto packet = new Packet("AssociationRequest"); + auto body = makeShared(); + body->setSSID("SSID"); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 1; + body->setSupportedRates(rates); + body->setChunkLength(B(1)); + packet->insertAtBack(body); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleAssociationRequestFrame(packet, header); + } + + void submitAuthentication(const MacAddress& address, int sequenceNumber) + { + Enter_Method("submitAuthentication"); + auto packet = new Packet("Authentication"); + auto body = makeShared(); + body->setSequenceNumber(sequenceNumber); + body->setStatusCode(SC_SUCCESSFUL); + packet->insertAtBack(body); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleAuthenticationFrame(packet, header); + } + + void submitDeauthentication(const MacAddress& address) + { + Enter_Method("submitDeauthentication"); + auto packet = new Packet("Deauthentication"); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleDeauthenticationFrame(packet, header); + } + + void submitDisassociation(const MacAddress& address) + { + Enter_Method("submitDisassociation"); + auto packet = new Packet("Disassociation"); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleDisassociationFrame(packet, header); + } + + bool hasPendingAssociation(const MacAddress& address) const + { + auto it = staList.find(address); + return it != staList.end() && it->second.pendingAssociationTransactionId != 0; + } + + uint64_t getPendingAssociationTransactionId(const MacAddress& address) const + { + return staList.at(address).pendingAssociationTransactionId; + } + + short reserveAssociationId(const MacAddress& address) { return mib->reserveAssociationId(address); } + void cancelAssociationIdReservation(const MacAddress& address) { mib->cancelAssociationIdReservation(address); } + bool hasCommittedAssociationId(const MacAddress& address) const + { + return mib->bssAccessPointData.associationIds.find(address) != mib->bssAccessPointData.associationIds.end(); + } + + Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const + { + return mib->bssAccessPointData.stations.at(address); + } + + protected: + virtual void frameTransmissionFinished(const Packet *frame, FrameTransmissionStatus status) override + { + responseStatuses.push_back(status); + auto transactionTag = frame == nullptr ? nullptr : frame->findTag(); + responseTransactionIds.push_back(transactionTag == nullptr ? 0 : transactionTag->getTransactionId()); + Ieee80211MgmtAp::frameTransmissionFinished(frame, status); + } +}; + +Define_Module(TestIeee80211MgmtApCancellation); + +class TransactionSignalListener : public cListener +{ + public: + std::vector sentTransactionIds; + std::vector lowerTransactionIds; + std::vector droppedTransactionIds; + + virtual void receiveSignal(cComponent *, simsignal_t signalID, cObject *value, cObject *) override + { + auto packet = dynamic_cast(value); + if (packet == nullptr || (signalID != packetSentToPeerSignal && signalID != packetSentToLowerSignal && signalID != packetDroppedSignal)) + return; + auto header = packet->peekAtFront(); + if (header->getType() != ST_ASSOCIATIONRESPONSE && header->getType() != ST_REASSOCIATIONRESPONSE) + return; + auto transactionTag = packet->findTag(); + if (transactionTag == nullptr) + return; + if (signalID == packetSentToPeerSignal) + sentTransactionIds.push_back(transactionTag->getTransactionId()); + else if (signalID == packetSentToLowerSignal) + lowerTransactionIds.push_back(transactionTag->getTransactionId()); + else + droppedTransactionIds.push_back(transactionTag->getTransactionId()); + } +}; + +class Ieee80211MgmtApCancellationTest : public cSimpleModule +{ + protected: + enum class Supersession { + AUTHENTICATION_RESTART, + DEAUTHENTICATION, + DISASSOCIATION, + }; + + static int countTransaction(queueing::IPacketQueue *queue, uint64_t transactionId) + { + int count = 0; + for (int i = 0; i < queue->getNumPackets(); i++) { + auto transactionTag = queue->getPacket(i)->findTag(); + if (transactionTag != nullptr && transactionTag->getTransactionId() == transactionId) + count++; + } + return count; + } + + static int countTransaction(InProgressFrames *frames, uint64_t transactionId) + { + int count = 0; + for (int i = 0; i < frames->getLength(); i++) { + auto transactionTag = frames->getFrames(i)->findTag(); + if (transactionTag != nullptr && transactionTag->getTransactionId() == transactionId) + count++; + } + return count; + } + + void runActiveCancellation(const char *apName, bool qos, const MacAddress& station, Supersession supersession, bool expectReplacement, bool cancelDuringIfs = false) + { + auto apBase = std::string("^.") + apName + ".wlan[0]"; + auto mgmt = check_and_cast(getModuleByPath((apBase + ".mgmt").c_str())); + auto mac = check_and_cast(getModuleByPath((apBase + ".mac").c_str())); + auto pendingQueuePath = apBase + (qos ? ".mac.hcf.edca.edcaf[3].pendingQueue" : ".mac.dcf.channelAccess.pendingQueue"); + auto inProgressFramesPath = apBase + (qos ? ".mac.hcf.edca.edcaf[3].inProgressFrames" : ".mac.dcf.channelAccess.inProgressFrames"); + auto contentionPath = apBase + (qos ? ".mac.hcf.edca.edcaf[3].contention" : ".mac.dcf.channelAccess.contention"); + auto dropSignalSourcePath = apBase + (qos ? ".mac.hcf" : ".mac.dcf"); + auto sentSignalSourcePath = apBase + (qos ? ".mac.hcf.edca.edcaf[3]" : ".mac.dcf"); + auto pendingQueue = check_and_cast(getModuleByPath(pendingQueuePath.c_str())); + auto inProgressFrames = check_and_cast(getModuleByPath(inProgressFramesPath.c_str())); + auto contention = check_and_cast(getModuleByPath(contentionPath.c_str())); + auto dropSignalSource = getModuleByPath(dropSignalSourcePath.c_str()); + auto sentSignalSource = getModuleByPath(sentSignalSourcePath.c_str()); + TransactionSignalListener signalListener; + sentSignalSource->subscribe(packetSentToPeerSignal, &signalListener); + mac->subscribe(packetSentToLowerSignal, &signalListener); + dropSignalSource->subscribe(packetDroppedSignal, &signalListener); + + if (supersession != Supersession::DISASSOCIATION) + mgmt->markAuthenticated(station); + mgmt->submitAssociationRequest(station); + auto oldTransactionId = mgmt->getPendingAssociationTransactionId(station); + auto oldAssociationId = mgmt->reserveAssociationId(station); + ASSERT(oldTransactionId != 0); + ASSERT(oldAssociationId != 0); + for (int i = 0; i < 1000 && countTransaction(pendingQueue, oldTransactionId) == 0; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(countTransaction(pendingQueue, oldTransactionId) > 0); + ASSERT(inProgressFrames->getLength() == 0); + ASSERT(contention->hasBlockedContention()); + + auto oldRtsHandoffCount = mac->getRtsHandoffCount(); + auto oldCtsReceivedCount = mac->getCtsReceivedCount(); + auto oldManagementHandoffCount = mac->getManagementHandoffCount(oldTransactionId); + if (cancelDuringIfs) { + mac->disarmHold(); + mac->armDelayedAssociationResponseHold(); + } + contention->grantAccess(); + if (cancelDuringIfs) { + for (int i = 0; i < 1000 && (!mac->isWaitingForIfs() || mac->getCtsReceivedCount() == oldCtsReceivedCount || mac->getRtsHandoffCount() == oldRtsHandoffCount); i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mac->isWaitingForIfs()); + ASSERT(mac->getCtsReceivedCount() > oldCtsReceivedCount); + ASSERT(mac->getRtsHandoffCount() > oldRtsHandoffCount); + ASSERT(!mac->hasHeldFrame()); + } + else { + for (int i = 0; i < 1000 && !mac->hasHeldFrame(); i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mac->hasHeldFrame()); + } + auto oldInProgressCount = countTransaction(inProgressFrames, oldTransactionId); + ASSERT(oldInProgressCount > 0); + auto oldDroppedCount = signalListener.droppedTransactionIds.size(); + auto oldSentCount = signalListener.sentTransactionIds.size(); + auto oldLowerCount = signalListener.lowerTransactionIds.size(); + auto oldCallbackCount = mgmt->responseStatuses.size(); + + if (supersession == Supersession::AUTHENTICATION_RESTART) + mgmt->submitAuthentication(station, 1); + else if (supersession == Supersession::DEAUTHENTICATION) + mgmt->submitDeauthentication(station); + else + mgmt->submitDisassociation(station); + ASSERT(!mgmt->hasPendingAssociation(station)); + ASSERT(countTransaction(pendingQueue, oldTransactionId) == 0); + ASSERT(countTransaction(inProgressFrames, oldTransactionId) == 0); + ASSERT(signalListener.droppedTransactionIds.size() - oldDroppedCount == (size_t)oldInProgressCount); + ASSERT(signalListener.sentTransactionIds.size() == oldSentCount); + ASSERT(signalListener.lowerTransactionIds.size() == oldLowerCount); + ASSERT(mgmt->responseStatuses.size() == oldCallbackCount); + ASSERT(mac->getManagementHandoffCount(oldTransactionId) == oldManagementHandoffCount); + if (cancelDuringIfs) + mac->disarmDelayedAssociationResponseHold(); + + auto reusableAddress = MacAddress((std::string("02:00:00:00:10:") + (qos ? "02" : "01")).c_str()); + if (!mgmt->hasCommittedAssociationId(station)) { + ASSERT(mgmt->reserveAssociationId(reusableAddress) == oldAssociationId); + mgmt->cancelAssociationIdReservation(reusableAddress); + } + + uint64_t replacementTransactionId = 0; + if (expectReplacement) { + if (supersession == Supersession::DEAUTHENTICATION) + mgmt->submitAuthentication(station, 1); + ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::AUTHENTICATED); + mgmt->submitAssociationRequest(station); + replacementTransactionId = mgmt->getPendingAssociationTransactionId(station); + ASSERT(replacementTransactionId != 0 && replacementTransactionId != oldTransactionId); + ASSERT(mgmt->hasPendingAssociation(station)); + } + + if (!cancelDuringIfs) { + mac->completeHeldTransmission(); + ASSERT(!mac->hasHeldFrame()); + } + else { + ASSERT(!mac->hasHeldFrame()); + ASSERT(!mac->isWaitingForIfs()); + } + if (expectReplacement) { + for (int i = 0; i < 1000 && countTransaction(pendingQueue, replacementTransactionId) == 0; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(countTransaction(pendingQueue, replacementTransactionId) > 0); + ASSERT(contention->hasBlockedContention()); + // The DCF delayed case has already sent its RTS and the station's + // NAV covers the cancelled exchange. Let that reservation expire + // before starting the replacement; no replacement frame is + // eligible to cross the handoff during this interval. + if (cancelDuringIfs && !qos) + wait(SimTime(1, SIMTIME_MS)); + contention->grantAccess(); + for (int i = 0; i < 100000 && mgmt->hasPendingAssociation(station); i++) { + if (contention->hasBlockedContention()) + contention->grantAccess(); + wait(SimTime(1, SIMTIME_US)); + } + ASSERT(mgmt->responseStatuses.size() > oldCallbackCount); + ASSERT(!mgmt->hasPendingAssociation(station)); + ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::ASSOCIATED); + ASSERT(mgmt->hasCommittedAssociationId(station)); + ASSERT(mgmt->reserveAssociationId(station) == oldAssociationId); + for (size_t i = oldCallbackCount; i < mgmt->responseTransactionIds.size(); i++) + ASSERT(mgmt->responseTransactionIds[i] == replacementTransactionId); + ASSERT(std::find(signalListener.sentTransactionIds.begin(), signalListener.sentTransactionIds.end(), oldTransactionId) == signalListener.sentTransactionIds.end()); + ASSERT(std::find(signalListener.lowerTransactionIds.begin(), signalListener.lowerTransactionIds.end(), oldTransactionId) == signalListener.lowerTransactionIds.end()); + ASSERT(std::find(signalListener.sentTransactionIds.begin(), signalListener.sentTransactionIds.end(), replacementTransactionId) != signalListener.sentTransactionIds.end()); + } + else { + for (int i = 0; i < 100; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->responseStatuses.size() == oldCallbackCount); + ASSERT(!mgmt->hasCommittedAssociationId(station)); + ASSERT(std::find(signalListener.sentTransactionIds.begin(), signalListener.sentTransactionIds.end(), oldTransactionId) == signalListener.sentTransactionIds.end()); + ASSERT(std::find(signalListener.lowerTransactionIds.begin(), signalListener.lowerTransactionIds.end(), oldTransactionId) == signalListener.lowerTransactionIds.end()); + } + + sentSignalSource->unsubscribe(packetSentToPeerSignal, &signalListener); + mac->unsubscribe(packetSentToLowerSignal, &signalListener); + dropSignalSource->unsubscribe(packetDroppedSignal, &signalListener); + } + + public: + Ieee80211MgmtApCancellationTest() : cSimpleModule(65536) {} + + protected: + virtual void activity() override + { + runActiveCancellation("apDcf", false, MacAddress("02:00:00:00:00:09"), Supersession::AUTHENTICATION_RESTART, true); + runActiveCancellation("apDcf", false, MacAddress("02:00:00:00:00:09"), Supersession::AUTHENTICATION_RESTART, true, true); + runActiveCancellation("apHcf", true, MacAddress("02:00:00:00:00:0a"), Supersession::DEAUTHENTICATION, true); + runActiveCancellation("apHcf", true, MacAddress("02:00:00:00:00:0a"), Supersession::AUTHENTICATION_RESTART, true, true); + auto hcfMac = check_and_cast(getModuleByPath("^.apHcf.wlan[0].mac")); + hcfMac->armHold(); + runActiveCancellation("apHcf", true, MacAddress("02:00:00:00:00:0a"), Supersession::DISASSOCIATION, false); + std::cout << "AP DCF/HCF association transaction supersession cancellation and replacement completion verified.\n"; + } +}; + +Define_Module(Ieee80211MgmtApCancellationTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.Ieee80211Mac; +import inet.linklayer.ieee80211.mac.Tx; +import inet.linklayer.ieee80211.mac.contention.Contention; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtAp; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple TestBlockedContention extends Contention +{ + parameters: + @class(::inet::ieee80211::TestBlockedContention); +} + +simple TestInspectableTx extends Tx +{ + parameters: + @class(::inet::ieee80211::TestInspectableTx); +} + +module TestHoldingIeee80211Mac extends Ieee80211Mac +{ + parameters: + @class(::inet::ieee80211::TestHoldingIeee80211Mac); +} + +simple TestIeee80211MgmtApCancellation extends Ieee80211MgmtAp +{ + parameters: + @class(::inet::ieee80211::TestIeee80211MgmtApCancellation); +} + +simple Ieee80211MgmtApCancellationTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211MgmtApCancellationTest); +} + +network Ieee80211MgmtApCancellationTestNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + apDcf: AccessPoint { + parameters: + wlan[*].mgmt.typename = "TestIeee80211MgmtApCancellation"; + wlan[*].mac.typename = "TestHoldingIeee80211Mac"; + wlan[*].mac.tx.typename = "TestInspectableTx"; + wlan[*].mac.dcf.channelAccess.contention.typename = "TestBlockedContention"; + } + staDcf: WirelessHost { + parameters: + wlan[*].mac.typename = "Ieee80211TesterMac"; + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + apHcf: AccessPoint { + parameters: + wlan[*].mgmt.typename = "TestIeee80211MgmtApCancellation"; + wlan[*].mac.typename = "TestHoldingIeee80211Mac"; + wlan[*].mac.tx.typename = "TestInspectableTx"; + wlan[*].mac.qosStation = true; + wlan[*].mac.hcf.edca.edcaf[*].contention.typename = "TestBlockedContention"; + } + staHcf: WirelessHost { + parameters: + wlan[*].mac.typename = "Ieee80211TesterMac"; + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + test: Ieee80211MgmtApCancellationTest; + connections allowunconnected: + // The wireless interfaces connect to the shared radio medium through + // their radioIn gates; no wired traffic is needed by this regression. +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211MgmtApCancellationTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 100ms +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +*.apDcf.wlan[0].mgmt.beaconInterval = 10s +*.apHcf.wlan[0].mgmt.beaconInterval = 10s +*.apDcf.wlan[0].mgmt.numAuthSteps = 2 +*.apHcf.wlan[0].mgmt.numAuthSteps = 2 +*.apDcf.wlan[0].address = "02:00:00:00:00:01" +*.apHcf.wlan[0].address = "02:00:00:00:00:02" +*.staDcf.wlan[0].address = "02:00:00:00:00:09" +*.staHcf.wlan[0].address = "02:00:00:00:00:0a" +*.staDcf.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:01" +*.staHcf.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:02" +*.staDcf.wlan[0].mac.actions = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +*.staHcf.wlan[0].mac.actions = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +*.apDcf.wlan[0].mac.dcf.rtsPolicy.rtsThreshold = 0B +*.apHcf.wlan[0].mac.hcf.rtsPolicy.rtsThreshold = 0B +*.apDcf.wlan[0].mac.dcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 32B +*.apHcf.wlan[0].mac.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 32B +*.apDcf.mobility.initialX = 0m +*.apDcf.mobility.initialY = 0m +*.staDcf.mobility.initialX = 1m +*.staDcf.mobility.initialY = 0m +*.apHcf.mobility.initialX = 0m +*.apHcf.mobility.initialY = 20m +*.staHcf.mobility.initialX = 1m +*.staHcf.mobility.initialY = 20m +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.wlan[*].opMode = "g(mixed)" +**.wlan[*].bitrate = 11Mbps +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.centerFrequency = 2.4GHz +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].radio.receiver.sensitivity = -85dBm +**.wlan[*].radio.receiver.snirThreshold = 4dB + +%contains: stdout +AP DCF/HCF association transaction supersession cancellation and replacement completion verified. diff --git a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test index 62c1d4fc629..27f8d874cac 100644 --- a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test @@ -190,6 +190,64 @@ class Ieee80211MgmtApHcfQueueDropTest : public cSimpleModule ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::ASSOCIATED); ASSERT(mgmt->getCommittedAssociationId(station) == committedAid); + // An explicit single-packet removal follows the same terminal path as + // a queue drop: the AP must release both the pending transaction and + // its reserved AID before another station can reserve it. + const MacAddress explicitlyRemovedStation("02:00:00:00:00:0b"); + mgmt->markAuthenticated(explicitlyRemovedStation); + mgmt->submitAssociationRequest(explicitlyRemovedStation); + for (int i = 0; i < 100 && managementQueue->isEmpty(); i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->hasPendingAssociation(explicitlyRemovedStation)); + auto explicitlyRemovedPacket = managementQueue->findPacket([](const Packet *packet) { + return packet->findTag() != nullptr; + }); + ASSERT(explicitlyRemovedPacket != nullptr); + managementQueue->removePacket(explicitlyRemovedPacket); + take(explicitlyRemovedPacket); + delete explicitlyRemovedPacket; + for (int i = 0; i < 100 && mgmt->statuses.size() < 5; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->statuses.size() == 5); + ASSERT(mgmt->statuses.back() == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(!mgmt->hasPendingAssociation(explicitlyRemovedStation)); + ASSERT(mgmt->getCommittedAssociationId(explicitlyRemovedStation) == 0); + auto explicitlyRemovedAid = mgmt->reserveAssociationId(explicitlyRemovedStation); + ASSERT(explicitlyRemovedAid != 0); + mgmt->cancelAssociationIdReservation(explicitlyRemovedStation); + + // Compound removeAllPackets exercises explicit bulk departure for two + // independent association transactions and must clear both states. + const MacAddress bulkRemovedStation1("02:00:00:00:00:0c"); + const MacAddress bulkRemovedStation2("02:00:00:00:00:0d"); + mgmt->markAuthenticated(bulkRemovedStation1); + mgmt->markAuthenticated(bulkRemovedStation2); + mgmt->submitAssociationRequest(bulkRemovedStation1); + mgmt->submitAssociationRequest(bulkRemovedStation2); + for (int i = 0; i < 100 && managementQueue->getNumPackets() < 2; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->hasPendingAssociation(bulkRemovedStation1)); + ASSERT(mgmt->hasPendingAssociation(bulkRemovedStation2)); + // The compound boundary is the queue observed by HCF. Its + // removeAllPackets() implementation removes each child packet while + // suppressing the child's duplicate REMOVED notification, then + // reports the terminal departure once at the boundary. + compoundQueue->removeAllPackets(); + for (int i = 0; i < 100 && mgmt->statuses.size() < 7; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->statuses.size() == 7); + ASSERT(mgmt->statuses[5] == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(mgmt->statuses[6] == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(!mgmt->hasPendingAssociation(bulkRemovedStation1)); + ASSERT(!mgmt->hasPendingAssociation(bulkRemovedStation2)); + ASSERT(mgmt->getCommittedAssociationId(bulkRemovedStation1) == 0); + ASSERT(mgmt->getCommittedAssociationId(bulkRemovedStation2) == 0); + auto bulkAid1 = mgmt->reserveAssociationId(bulkRemovedStation1); + auto bulkAid2 = mgmt->reserveAssociationId(bulkRemovedStation2); + ASSERT(bulkAid1 != 0 && bulkAid2 != 0 && bulkAid1 != bulkAid2); + mgmt->cancelAssociationIdReservation(bulkRemovedStation1); + mgmt->cancelAssociationIdReservation(bulkRemovedStation2); + std::cout << "HCF compound pending-queue drop cleanup, AID preservation, and reassociation recovery verified.\n"; } }; diff --git a/tests/module/Ieee80211MgmtApQueueDrop_1.test b/tests/module/Ieee80211MgmtApQueueDrop_1.test index d0b0a4838e4..b78a85529ca 100644 --- a/tests/module/Ieee80211MgmtApQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApQueueDrop_1.test @@ -66,6 +66,11 @@ class TestIeee80211MgmtApQueueDrop : public Ieee80211MgmtAp short reserveAssociationId(const MacAddress& address) { return mib->reserveAssociationId(address); } void cancelAssociationIdReservation(const MacAddress& address) { mib->cancelAssociationIdReservation(address); } + short getCommittedAssociationId(const MacAddress& address) const + { + auto it = mib->bssAccessPointData.associationIds.find(address); + return it == mib->bssAccessPointData.associationIds.end() ? 0 : it->second; + } Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const { return mib->bssAccessPointData.stations.at(address); } protected: @@ -129,6 +134,59 @@ class Ieee80211MgmtApQueueDropTest : public cSimpleModule ASSERT(!mgmt->hasPendingAssociation(station)); ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::ASSOCIATED); + // Explicit single removal from the real DCF pending queue is a + // terminal management outcome and releases the AP-side reservation. + const MacAddress explicitlyRemovedStation("02:00:00:00:00:0b"); + mgmt->markAuthenticated(explicitlyRemovedStation); + mgmt->submitAssociationRequest(explicitlyRemovedStation); + for (int i = 0; i < 100 && queue->isEmpty(); i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->hasPendingAssociation(explicitlyRemovedStation)); + auto explicitlyRemovedPacket = queue->findPacket([](const Packet *packet) { + return packet->findTag() != nullptr; + }); + ASSERT(explicitlyRemovedPacket != nullptr); + queue->removePacket(explicitlyRemovedPacket); + take(explicitlyRemovedPacket); + delete explicitlyRemovedPacket; + for (int i = 0; i < 100 && mgmt->statuses.size() < 3; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->statuses.size() == 3); + ASSERT(mgmt->statuses.back() == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(!mgmt->hasPendingAssociation(explicitlyRemovedStation)); + ASSERT(mgmt->getCommittedAssociationId(explicitlyRemovedStation) == 0); + auto explicitlyRemovedAid = mgmt->reserveAssociationId(explicitlyRemovedStation); + ASSERT(explicitlyRemovedAid != 0); + mgmt->cancelAssociationIdReservation(explicitlyRemovedStation); + + // Bulk removal invokes the callback once per independent transaction; + // both pending states and both reservations must be released. + const MacAddress bulkRemovedStation1("02:00:00:00:00:0c"); + const MacAddress bulkRemovedStation2("02:00:00:00:00:0d"); + mgmt->markAuthenticated(bulkRemovedStation1); + mgmt->markAuthenticated(bulkRemovedStation2); + mgmt->submitAssociationRequest(bulkRemovedStation1); + mgmt->submitAssociationRequest(bulkRemovedStation2); + for (int i = 0; i < 100 && queue->getNumPackets() < 2; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->hasPendingAssociation(bulkRemovedStation1)); + ASSERT(mgmt->hasPendingAssociation(bulkRemovedStation2)); + queue->removeAllPackets(); + for (int i = 0; i < 100 && mgmt->statuses.size() < 5; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->statuses.size() == 5); + ASSERT(mgmt->statuses[3] == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(mgmt->statuses[4] == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(!mgmt->hasPendingAssociation(bulkRemovedStation1)); + ASSERT(!mgmt->hasPendingAssociation(bulkRemovedStation2)); + ASSERT(mgmt->getCommittedAssociationId(bulkRemovedStation1) == 0); + ASSERT(mgmt->getCommittedAssociationId(bulkRemovedStation2) == 0); + auto bulkAid1 = mgmt->reserveAssociationId(bulkRemovedStation1); + auto bulkAid2 = mgmt->reserveAssociationId(bulkRemovedStation2); + ASSERT(bulkAid1 != 0 && bulkAid2 != 0 && bulkAid1 != bulkAid2); + mgmt->cancelAssociationIdReservation(bulkRemovedStation1); + mgmt->cancelAssociationIdReservation(bulkRemovedStation2); + std::cout << "DCF pending-queue overflow terminal cleanup and retransmission recovery verified.\n"; } }; diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 72e7714ddf5..f2ea78f6c0d 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -39,6 +39,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" #include "inet/queueing/gate/PacketGate.h" #include "inet/queueing/gate/PeriodicGate.h" #include "inet/queueing/queue/CompoundPacketQueueBase.h" @@ -630,8 +631,9 @@ class TestHcf : public Hcf void processTransmittedData(Packet *packet, const Ptr& dataHeader, AccessCategory ac) { originatorProcessTransmittedDataFrame(packet, dataHeader, ac); } void processTransmittedManagement(Packet *packet, const Ptr& managementHeader, AccessCategory ac) { originatorProcessTransmittedManagementFrame(packet, managementHeader, ac); } void dropPacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DROPPED); } - void removePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::REMOVED); } + void removePacket(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason = queueing::IPacketQueue::PacketRemovalReason::REMOVED) { handlePacketRemoved(packet, reason); } void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } + bool cancelManagement(uint64_t transactionId, Packet *excludedPacket = nullptr) { return cancelManagementTransaction(transactionId, excludedPacket); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } void processReceivedFrame(Packet *packet) { recipientProcessReceivedFrame(packet, packet->peekAtFront()); } @@ -675,7 +677,13 @@ class TestMac : public Ieee80211Mac public: using Ieee80211Mac::receiveSignal; int numPendingRadioConfigSends = 0; + std::vector transmissionStatuses; + std::vector transmissionFrames; virtual void sendDownPendingRadioConfigMsg() override { numPendingRadioConfigSends++; } + virtual void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *details) override { + transmissionFrames.push_back(check_and_cast(object)); + transmissionStatuses.push_back(check_and_cast(details)->getStatus()); + } }; class TestEmptyFrameSequence : public IFrameSequence @@ -794,6 +802,7 @@ class TestFrameSequenceHandler : public IFrameSequenceHandler IFrameSequence *frameSequence = nullptr; FrameSequenceContext *context = nullptr; bool running = false; + bool cancellationRequested = false; int numStartedSequences = 0; virtual ~TestFrameSequenceHandler() { @@ -812,6 +821,7 @@ class TestFrameSequenceHandler : public IFrameSequenceHandler virtual void transmissionComplete() override {} virtual bool isSequenceRunning() override { return running; } virtual void handleStartRxTimeout() override {} + virtual void cancelFrameSequence() override { cancellationRequested = true; } }; class TestDcaf : public Dcaf @@ -819,7 +829,11 @@ class TestDcaf : public Dcaf public: int numReleasedChannels = 0; - void configure(InProgressFrames *inProgressFrames) { this->inProgressFrames = inProgressFrames; } + void configure(InProgressFrames *inProgressFrames, queueing::IPacketQueue *pendingQueue = nullptr) { + this->inProgressFrames = inProgressFrames; + this->pendingQueue = pendingQueue; + } + virtual queueing::IPacketQueue *getPendingQueue() const override { return pendingQueue; } virtual void releaseChannel(IChannelAccess::ICallback *) override { numReleasedChannels++; } }; @@ -830,15 +844,24 @@ class TestDcf : public Dcf for (auto listener : getLocalSignalListeners(Ieee80211Mac::frameTransmissionOutcomeSignal)) unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, listener); } - TestFrameSequenceHandler *configure(TestDcaf *channelAccess, Ieee80211Mac *mac) { + TestFrameSequenceHandler *configure(TestDcaf *channelAccess, Ieee80211Mac *mac, AckHandler *ackHandler = nullptr) { this->channelAccess = channelAccess; this->mac = mac; subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, check_and_cast(mac)); + this->ackHandler = ackHandler; auto handler = new TestFrameSequenceHandler(); frameSequenceHandler = handler; return handler; } void grant(TestDcaf *channelAccess) { channelGranted(channelAccess); } + bool cancelManagement(uint64_t transactionId, Packet *excludedPacket) { return cancelManagementTransaction(transactionId, excludedPacket); } + void notifyRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) { handlePacketRemoved(packet, reason); } + void configureFailurePath(NonQosRecoveryProcedure *recoveryProcedure) { + this->recoveryProcedure = recoveryProcedure; + this->stationRetryCounters = new StationRetryCounters(); + } + void processFailedFrame(Packet *packet) { originatorProcessFailedFrame(packet); } + void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } protected: virtual FrameSequenceContext *buildContext() override { @@ -870,6 +893,22 @@ static Packet *makeTaggedDelbaPacket(const char *name, MacAddress receiverAddres return packet; } +static Packet *makeTaggedManagementPacket(const char *name, Ieee80211FrameType type, MacAddress transmitterAddress, MacAddress receiverAddress, + SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint64_t transactionId, bool retry = false) +{ + auto header = makeShared(); + header->setType(type); + header->setTransmitterAddress(transmitterAddress); + header->setReceiverAddress(receiverAddress); + header->setSequenceNumber(sequenceNumber); + header->setFragmentNumber(fragmentNumber); + header->setMoreFragments(moreFragments); + header->setRetry(retry); + auto packet = new Packet(name, header); + packet->addTag()->setTransactionId(transactionId); + return packet; +} + static Ptr makeResponse(MacAddress transmitterAddress, Tid tid, uint8_t dialogToken, uint16_t statusCode) { auto response = makeShared(); @@ -1372,6 +1411,85 @@ ASSERT(handler.isAddbaResponsePending(peer3, 5)); delete frame; } +// Management transaction cancellation keeps active direct and RTS-protected +// frames alive until the sequence retires them, while making both frames +// ineligible immediately and suppressing terminal callbacks/retries. +for (auto coordination : { 0, 1 }) { + const uint64_t transactionId = 902 + coordination; + TestPacketQueue pendingQueue; + TestInProgressFrames inProgressFrames; + AckHandler dcfAckHandler; + TestQosAckHandler hcfAckHandler; + inProgressFrames.configure(nullptr, &dcfAckHandler, &pendingQueue); + auto directPacket = makeTaggedManagementPacket("activeManagementDirect", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(32 + coordination), 0, true, transactionId); + auto protectedPacket = makeTaggedManagementPacket("activeManagementRtsProtected", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(32 + coordination), 1, false, transactionId); + inProgressFrames.addOwnedFrame(directPacket); + inProgressFrames.addOwnedFrame(protectedPacket); + + TestFrameSequenceHandler *frameSequenceHandler; + TestMac mac; + if (coordination == 0) { + TestDcaf channelAccess; + channelAccess.configure(&inProgressFrames, &pendingQueue); + TestDcf dcf; + frameSequenceHandler = dcf.configure(&channelAccess, &mac, &dcfAckHandler); + frameSequenceHandler->running = true; + frameSequenceHandler->context = new FrameSequenceContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &inProgressFrames, nullptr, nullptr, new NonQoSContext(nullptr), nullptr); + frameSequenceHandler->context->addStep(new TransmitStep(directPacket, 0)); + frameSequenceHandler->context->addStep(new RtsTransmitStep(protectedPacket, new Packet("activeManagementRts", makeShared()), 0)); + TestSignalListener dropListener; + dcf.subscribe(packetDroppedSignal, &dropListener); + ASSERT(dcf.cancelManagement(transactionId, nullptr)); + ASSERT(frameSequenceHandler->cancellationRequested); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 2); + ASSERT(dropListener.numSignals == 2); + ASSERT(mac.transmissionStatuses.empty()); + dcf.processFailedFrame(directPacket); + dcf.processRtsProtectionFailure(protectedPacket); + ASSERT(!directPacket->peekAtFront()->getRetry()); + ASSERT(!protectedPacket->peekAtFront()->getRetry()); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 2); + for (auto frame : releasedFrames) + delete frame; + } + else { + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + inProgressFrames.configure(nullptr, &hcfAckHandler, &pendingQueue); + edcaf.qosAckHandler = &hcfAckHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestHcf hcf; + hcf.configure(&edca); + hcf.configureMac(&mac); + frameSequenceHandler = new TestFrameSequenceHandler(); + frameSequenceHandler->running = true; + frameSequenceHandler->context = new FrameSequenceContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &inProgressFrames, nullptr, nullptr, nullptr, nullptr); + frameSequenceHandler->context->addStep(new TransmitStep(directPacket, 0)); + frameSequenceHandler->context->addStep(new RtsTransmitStep(protectedPacket, new Packet("activeManagementRts", makeShared()), 0)); + hcf.configureFrameSequenceHandler(frameSequenceHandler); + TestSignalListener dropListener; + hcf.subscribe(packetDroppedSignal, &dropListener); + ASSERT(hcf.cancelManagement(transactionId, nullptr)); + ASSERT(frameSequenceHandler->cancellationRequested); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 2); + ASSERT(dropListener.numSignals == 2); + ASSERT(mac.transmissionStatuses.empty()); + hcf.processFailedFrame(directPacket); + hcf.processRtsProtectionFailure(protectedPacket); + ASSERT(!directPacket->peekAtFront()->getRetry()); + ASSERT(!protectedPacket->peekAtFront()->getRetry()); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 2); + for (auto frame : releasedFrames) + delete frame; + } +} + // A transaction frame that is not referenced by an active sequence is removed // from in-progress ownership and reclaimed immediately, without entering the @@ -4317,6 +4435,331 @@ delete deferredFrames.at(0); delete dcfFrames.at(0); } +// A terminal management-frame failure cancels all fragmented siblings in DCF, +// including frames held by a different queue stage, while leaving the current +// borrowed frame and an unrelated transaction untouched. +for (auto responseType : { ST_ASSOCIATIONRESPONSE, ST_REASSOCIATIONRESPONSE }) { + TestOriginatorQosMacDataService managementDcfDataService; + TestPacketQueue managementDcfPendingQueue; + AckHandler managementDcfAckHandler; + TestInProgressFrames managementDcfInProgressFrames; + managementDcfInProgressFrames.configure(&managementDcfDataService, &managementDcfAckHandler, &managementDcfPendingQueue); + TestDcaf managementDcaf; + managementDcaf.configure(&managementDcfInProgressFrames, &managementDcfPendingQueue); + TestMac managementDcfMac; + TestDcf managementDcf; + managementDcf.configure(&managementDcaf, &managementDcfMac, &managementDcfAckHandler); + auto currentFrame = makeTaggedManagementPacket("managementDcfCurrent", responseType, peer1, peer2, SequenceNumberCyclic(301 + responseType), 0, true, 500 + responseType); + auto inProgressSibling = makeTaggedManagementPacket("managementDcfInProgressSibling", responseType, peer1, peer2, SequenceNumberCyclic(301 + responseType), 1, false, 500 + responseType); + auto pendingSibling = makeTaggedManagementPacket("managementDcfPendingSibling", responseType, peer1, peer2, SequenceNumberCyclic(301 + responseType), 2, false, 500 + responseType); + auto unrelatedFrame = makeTaggedManagementPacket("managementDcfUnrelated", responseType, peer1, peer2, SequenceNumberCyclic(302 + responseType), 0, false, 700 + responseType); + managementDcfInProgressFrames.addOwnedFrame(currentFrame); + managementDcfInProgressFrames.addOwnedFrame(inProgressSibling); + managementDcfPendingQueue.packets = { pendingSibling, unrelatedFrame }; + ASSERT(managementDcf.cancelManagement(500 + responseType, currentFrame)); + ASSERT(managementDcfInProgressFrames.getLength() == 1); + ASSERT(managementDcfInProgressFrames.getFrames(0) == currentFrame); + ASSERT(managementDcfPendingQueue.getNumPackets() == 1); + ASSERT(managementDcfPendingQueue.getPacket(0) == unrelatedFrame); + ASSERT(managementDcfMac.transmissionStatuses.empty()); + auto remainingDcfFrames = managementDcfInProgressFrames.releaseFrames(); + ASSERT(remainingDcfFrames.size() == 1); + delete remainingDcfFrames.front(); + managementDcfPendingQueue.removePacket(unrelatedFrame); + delete unrelatedFrame; +} + +// HCF cancellation spans every EDCA, but does not cross transaction identity +// boundaries. The active sequence's current frame is deliberately excluded. +{ + std::array managementHcfPendingQueues; + std::array managementHcfAckHandlers; + std::array managementHcfDataServices; + std::array managementHcfInProgressFrames; + std::array managementHcfEdcafs; + TestEdca managementHcfEdca; + managementHcfEdca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + managementHcfInProgressFrames[ac].configure(&managementHcfDataServices[ac], &managementHcfAckHandlers[ac], &managementHcfPendingQueues[ac]); + managementHcfEdcafs[ac].pendingQueue = &managementHcfPendingQueues[ac]; + managementHcfEdcafs[ac].inProgressFrames = &managementHcfInProgressFrames[ac]; + managementHcfEdcafs[ac].qosAckHandler = &managementHcfAckHandlers[ac]; + managementHcfEdcafs[ac].setAccessCategory(AccessCategory(ac)); + managementHcfEdca.edcafs[ac] = &managementHcfEdcafs[ac]; + } + managementHcfEdca.edcaf = &managementHcfEdcafs[AC_BE]; + TestMac managementHcfMac; + TestHcf managementHcf; + managementHcf.configure(&managementHcfEdca); + managementHcf.configureMac(&managementHcfMac); + auto currentFrame = makeTaggedManagementPacket("managementHcfCurrent", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(401), 0, true, 800); + auto crossAcSibling = makeTaggedManagementPacket("managementHcfCrossAcSibling", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(401), 1, false, 800); + auto pendingSibling = makeTaggedManagementPacket("managementHcfPendingSibling", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(401), 2, false, 800); + auto unrelatedFrame = makeTaggedManagementPacket("managementHcfUnrelated", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(402), 0, false, 801); + managementHcfInProgressFrames[AC_BE].addOwnedFrame(currentFrame); + managementHcfInProgressFrames[AC_BK].addOwnedFrame(crossAcSibling); + managementHcfPendingQueues[AC_VO].packets = { pendingSibling }; + managementHcfPendingQueues[AC_BK].packets = { unrelatedFrame }; + ASSERT(managementHcf.cancelManagement(800, currentFrame)); + ASSERT(managementHcfInProgressFrames[AC_BE].getLength() == 1); + ASSERT(managementHcfInProgressFrames[AC_BE].getFrames(0) == currentFrame); + ASSERT(managementHcfInProgressFrames[AC_BK].getLength() == 0); + ASSERT(managementHcfPendingQueues[AC_VO].isEmpty()); + ASSERT(managementHcfPendingQueues[AC_BK].getNumPackets() == 1); + ASSERT(managementHcfPendingQueues[AC_BK].getPacket(0) == unrelatedFrame); + auto remainingHcfFrames = managementHcfInProgressFrames[AC_BE].releaseFrames(); + ASSERT(remainingHcfFrames.size() == 1); + delete remainingHcfFrames.front(); + managementHcfPendingQueues[AC_BK].removePacket(unrelatedFrame); + delete unrelatedFrame; + + auto reassociationCurrentFrame = makeTaggedManagementPacket("managementHcfReassociationCurrent", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(403), 0, true, 802); + auto reassociationInProgressSibling = makeTaggedManagementPacket("managementHcfReassociationInProgressSibling", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(403), 1, false, 802); + auto reassociationPendingSibling = makeTaggedManagementPacket("managementHcfReassociationPendingSibling", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(403), 2, false, 802); + managementHcfInProgressFrames[AC_BE].addOwnedFrame(reassociationCurrentFrame); + managementHcfInProgressFrames[AC_VI].addOwnedFrame(reassociationInProgressSibling); + managementHcfPendingQueues[AC_BK].packets = { reassociationPendingSibling }; + ASSERT(managementHcf.cancelManagement(802, reassociationCurrentFrame)); + ASSERT(managementHcfInProgressFrames[AC_BE].getLength() == 1); + ASSERT(managementHcfInProgressFrames[AC_BE].getFrames(0) == reassociationCurrentFrame); + ASSERT(managementHcfInProgressFrames[AC_VI].getLength() == 0); + ASSERT(managementHcfPendingQueues[AC_BK].isEmpty()); + auto remainingReassociationFrames = managementHcfInProgressFrames[AC_BE].releaseFrames(); + ASSERT(remainingReassociationFrames.size() == 1); + delete remainingReassociationFrames.front(); +} + +// DROPPED and explicit REMOVED management departures report one terminal +// callback, including a same-token bulk removal; DEQUEUED remains a normal +// hand-off into transmission processing. +{ + TestOriginatorQosMacDataService dcfRemovalDataService; + TestPacketQueue dcfRemovalQueue; + AckHandler dcfRemovalAckHandler; + TestInProgressFrames dcfRemovalInProgressFrames; + dcfRemovalInProgressFrames.configure(&dcfRemovalDataService, &dcfRemovalAckHandler, &dcfRemovalQueue); + TestDcaf dcfRemovalDcaf; + dcfRemovalDcaf.configure(&dcfRemovalInProgressFrames, &dcfRemovalQueue); + TestMac dcfRemovalMac; + TestDcf dcfRemoval; + dcfRemoval.configure(&dcfRemovalDcaf, &dcfRemovalMac, &dcfRemovalAckHandler); + dcfRemovalQueue.subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dcfRemoval); + auto droppedPacket = makeTaggedManagementPacket("dcfDroppedManagement", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(410), 0, false, 810); + dcfRemovalQueue.packets = { droppedPacket }; + dcfRemovalQueue.dropPacketFromQueue(droppedPacket); + delete droppedPacket; + ASSERT(dcfRemovalMac.transmissionStatuses.size() == 1); + ASSERT(dcfRemovalMac.transmissionStatuses.back() == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + auto removedPacket = makeTaggedManagementPacket("dcfRemovedManagement", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(411), 0, false, 811); + dcfRemovalQueue.packets = { removedPacket }; + dcfRemovalQueue.removePacket(removedPacket); + delete removedPacket; + ASSERT(dcfRemovalMac.transmissionStatuses.size() == 2); + auto bulkFirst = makeTaggedManagementPacket("dcfBulkManagementFirst", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(412), 0, true, 812); + auto bulkSecond = makeTaggedManagementPacket("dcfBulkManagementSecond", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(412), 1, false, 812); + auto dequeuedPacket = makeTaggedManagementPacket("dcfDequeuedManagement", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(413), 0, false, 813); + dcfRemovalQueue.packets = { bulkFirst, bulkSecond }; + dcfRemovalQueue.removeAllPackets(); + ASSERT(dcfRemovalMac.transmissionStatuses.size() == 3); + delete bulkFirst; + delete bulkSecond; + dcfRemovalQueue.packets = { dequeuedPacket }; + ASSERT(dcfRemovalQueue.dequeuePacket() == dequeuedPacket); + delete dequeuedPacket; + ASSERT(dcfRemovalMac.transmissionStatuses.size() == 3); + dcfRemovalQueue.unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dcfRemoval); +} + +{ + std::array hcfRemovalQueues; + std::array hcfRemovalAckHandlers; + std::array hcfRemovalDataServices; + std::array hcfRemovalInProgressFrames; + std::array hcfRemovalEdcafs; + TestEdca hcfRemovalEdca; + hcfRemovalEdca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + hcfRemovalInProgressFrames[ac].configure(&hcfRemovalDataServices[ac], &hcfRemovalAckHandlers[ac], &hcfRemovalQueues[ac]); + hcfRemovalEdcafs[ac].pendingQueue = &hcfRemovalQueues[ac]; + hcfRemovalEdcafs[ac].inProgressFrames = &hcfRemovalInProgressFrames[ac]; + hcfRemovalEdcafs[ac].qosAckHandler = &hcfRemovalAckHandlers[ac]; + hcfRemovalEdcafs[ac].setAccessCategory(AccessCategory(ac)); + hcfRemovalEdca.edcafs[ac] = &hcfRemovalEdcafs[ac]; + } + hcfRemovalEdca.edcaf = &hcfRemovalEdcafs[AC_BE]; + TestMac hcfRemovalMac; + TestHcf hcfRemoval; + hcfRemoval.configure(&hcfRemovalEdca); + hcfRemoval.configureMac(&hcfRemovalMac); + hcfRemovalQueues[AC_VO].subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcfRemoval); + auto removedPacket = makeTaggedManagementPacket("hcfRemovedManagement", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(420), 0, false, 820); + hcfRemovalQueues[AC_VO].packets = { removedPacket }; + hcfRemovalQueues[AC_VO].removePacket(removedPacket); + delete removedPacket; + ASSERT(hcfRemovalMac.transmissionStatuses.size() == 1); + auto bulkFirst = makeTaggedManagementPacket("hcfBulkManagementFirst", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(421), 0, true, 821); + auto bulkSecond = makeTaggedManagementPacket("hcfBulkManagementSecond", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(421), 1, false, 821); + auto untaggedPacket = new Packet("hcfBulkUntaggedManagement", makeShared()); + hcfRemovalQueues[AC_VO].packets = { bulkFirst, bulkSecond, untaggedPacket }; + hcfRemovalQueues[AC_VO].removeAllPackets(); + ASSERT(hcfRemovalMac.transmissionStatuses.size() == 2); + delete bulkFirst; + delete bulkSecond; + delete untaggedPacket; + auto dequeuedPacket = makeTaggedManagementPacket("hcfDequeuedManagement", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(422), 0, false, 822); + hcfRemovalQueues[AC_VO].packets = { dequeuedPacket }; + ASSERT(hcfRemovalQueues[AC_VO].dequeuePacket() == dequeuedPacket); + delete dequeuedPacket; + ASSERT(hcfRemovalMac.transmissionStatuses.size() == 2); + hcfRemovalQueues[AC_VO].unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcfRemoval); +} + +// A retry-exhausted fragmented association/reassociation response cancels its +// siblings through the real DCF failure callback path. The current failed +// frame is retired by DCF, while unrelated transaction state survives. +for (int responseIndex = 0; responseIndex < 2; responseIndex++) { + auto responseType = responseIndex == 0 ? ST_ASSOCIATIONRESPONSE : ST_REASSOCIATIONRESPONSE; + constexpr int firstSequenceNumber = 440; + uint64_t transactionId = 830 + responseIndex; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 1, &cwCalculator); + AckHandler ackHandler; + TestPacketQueue pendingQueue; + TestOriginatorQosMacDataService dataService; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestDcaf dcaf; + dcaf.configure(&inProgressFrames, &pendingQueue); + TestMac mac; + TestDcf dcf; + dcf.configure(&dcaf, &mac, &ackHandler); + dcf.configureFailurePath(&recoveryProcedure); + pendingQueue.subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dcf); + + auto current = makeTaggedManagementPacket("dcfFailureCurrent", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 0, true, transactionId); + auto inProgressSibling = makeTaggedManagementPacket("dcfFailureInProgressSibling", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 1, false, transactionId); + auto pendingSibling = makeTaggedManagementPacket("dcfFailurePendingSibling", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 2, false, transactionId); + auto unrelated = makeTaggedManagementPacket("dcfFailureUnrelated", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex + 1), 0, false, transactionId + 100); + auto currentHeader = current->peekAtFront(); + auto inProgressSiblingHeader = inProgressSibling->peekAtFront(); + inProgressFrames.addOwnedFrame(current); + inProgressFrames.addOwnedFrame(inProgressSibling); + pendingQueue.packets = { pendingSibling, unrelated }; + ackHandler.frameGotInProgress(currentHeader); + ackHandler.frameGotInProgress(inProgressSiblingHeader); + ackHandler.processTransmittedDataOrMgmtFrame(currentHeader); + ackHandler.processTransmittedDataOrMgmtFrame(inProgressSiblingHeader); + recoveryProcedure.addShortRetry(inProgressSiblingHeader, 2); + + dcf.processFailedFrame(current); + + ASSERT(mac.transmissionStatuses.size() == 1); + ASSERT(mac.transmissionStatuses.front() == FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + ASSERT(mac.transmissionFrames.front() == current); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 1); + ASSERT(pendingQueue.getNumPackets() == 1); + ASSERT(pendingQueue.getPacket(0) == unrelated); + ASSERT(ackHandler.getAckStatus(currentHeader) == AckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(ackHandler.getAckStatus(inProgressSiblingHeader) == AckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(!recoveryProcedure.hasRetry(currentHeader)); + ASSERT(!recoveryProcedure.hasRetry(inProgressSiblingHeader)); + auto retiredFrames = inProgressFrames.releaseFrames(); + ASSERT(retiredFrames == std::vector({ current })); + delete current; + pendingQueue.packets.clear(); + delete unrelated; + pendingQueue.unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dcf); +} + +// The same retry-exhaustion path in HCF spans EDCA queues, clears each +// sibling's QoS ACK/retry bookkeeping, and emits one terminal callback for +// both association response variants. +for (int responseIndex = 0; responseIndex < 2; responseIndex++) { + auto responseType = responseIndex == 0 ? ST_ASSOCIATIONRESPONSE : ST_REASSOCIATIONRESPONSE; + constexpr int firstSequenceNumber = 450; + uint64_t transactionId = 840 + responseIndex; + std::array pendingQueues; + std::array ackHandlers; + std::array dataServices; + std::array inProgressFrames; + std::array edcafs; + TestEdca edca; + edca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + inProgressFrames[ac].configure(&dataServices[ac], &ackHandlers[ac], &pendingQueues[ac]); + edcafs[ac].pendingQueue = &pendingQueues[ac]; + edcafs[ac].inProgressFrames = &inProgressFrames[ac]; + edcafs[ac].qosAckHandler = &ackHandlers[ac]; + edcafs[ac].setAccessCategory(AccessCategory(ac)); + edca.edcafs[ac] = &edcafs[ac]; + } + edca.edcaf = &edcafs[AC_BE]; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 1, &cwCalculator); + edca.nonQosRecoveryProcedure = &recoveryProcedure; + OriginatorQosAckPolicy ackPolicy; + TestMac mac; + TestHcf hcf; + hcf.configure(&edca); + hcf.configureMac(&mac); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + pendingQueues[ac].subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcf); + + auto current = makeTaggedManagementPacket("hcfFailureCurrent", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 0, true, transactionId); + auto inProgressSibling = makeTaggedManagementPacket("hcfFailureInProgressSibling", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 1, false, transactionId); + auto pendingSibling = makeTaggedManagementPacket("hcfFailurePendingSibling", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 2, false, transactionId); + auto unrelated = makeTaggedManagementPacket("hcfFailureUnrelated", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex + 1), 0, false, transactionId + 100); + auto currentHeader = current->peekAtFront(); + auto inProgressSiblingHeader = inProgressSibling->peekAtFront(); + inProgressFrames[AC_BE].addOwnedFrame(current); + inProgressFrames[AC_VI].addOwnedFrame(inProgressSibling); + pendingQueues[AC_VO].packets = { pendingSibling }; + pendingQueues[AC_BK].packets = { unrelated }; + ackHandlers[AC_BE].frameGotInProgress(currentHeader); + ackHandlers[AC_VI].frameGotInProgress(inProgressSiblingHeader); + hcf.processTransmittedManagement(current, currentHeader, AC_BE); + hcf.processTransmittedManagement(inProgressSibling, inProgressSiblingHeader, AC_VI); + ASSERT(ackHandlers[AC_BE].getMgmtOrNonQoSAckStatus(currentHeader) == QosAckHandler::Status::WAITING_FOR_NORMAL_ACK); + ASSERT(ackHandlers[AC_VI].getMgmtOrNonQoSAckStatus(inProgressSiblingHeader) == QosAckHandler::Status::WAITING_FOR_NORMAL_ACK); + recoveryProcedure.addShortRetry(inProgressSiblingHeader, 2); + + hcf.processFailedFrame(current); + + ASSERT(mac.transmissionStatuses.size() == 1); + ASSERT(mac.transmissionStatuses.front() == FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + ASSERT(mac.transmissionFrames.front() == current); + ASSERT(inProgressFrames[AC_BE].getLength() == 0); + ASSERT(inProgressFrames[AC_BE].getNumDroppedFrames() == 1); + ASSERT(inProgressFrames[AC_VI].getLength() == 0); + ASSERT(pendingQueues[AC_VO].isEmpty()); + ASSERT(pendingQueues[AC_BK].getNumPackets() == 1); + ASSERT(pendingQueues[AC_BK].getPacket(0) == unrelated); + ASSERT(ackHandlers[AC_BE].getMgmtOrNonQoSAckStatus(currentHeader) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(ackHandlers[AC_VI].getMgmtOrNonQoSAckStatus(inProgressSiblingHeader) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(!recoveryProcedure.hasRetry(currentHeader)); + ASSERT(!recoveryProcedure.hasRetry(inProgressSiblingHeader)); + auto retiredFrames = inProgressFrames[AC_BE].releaseFrames(); + ASSERT(retiredFrames == std::vector({ current })); + delete current; + pendingQueues[AC_VO].packets.clear(); + pendingQueues[AC_BK].packets.clear(); + delete unrelated; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + pendingQueues[ac].unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcf); +} + EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; %contains: stdout From d9ba185a9385622edb1a61bacd6bb0c56f039c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:03 +0200 Subject: [PATCH 07/16] ieee80211: protect Block Ack teardown generations Tag each locally generated DELBA with its agreement role, peer, TID, and generation. Route acknowledgement, retry exhaustion, cancellation, and abort handling back to that exact agreement instance. Track pending teardowns by generation and cancel only the matching transaction. A delayed or retried DELBA from an older agreement can no longer delete a replacement agreement that reused the same peer and TID. Cover originator and recipient teardown, replacement during an in-flight DELBA, retry and abort paths, and stale completion callbacks. --- WHATSNEW | 25 +- .../Ieee80211BlockAckAgreementTag.msg | 17 + .../OriginatorBlockAckAgreementHandler.cc | 69 +- .../OriginatorBlockAckAgreementHandler.h | 2 + .../blockack/RecipientBlockAckAgreement.cc | 6 +- .../mac/blockack/RecipientBlockAckAgreement.h | 4 +- .../RecipientBlockAckAgreementHandler.cc | 110 +++- .../RecipientBlockAckAgreementHandler.h | 11 +- .../IBlockAckAgreementHandlerCallback.h | 6 + .../IRecipientBlockAckAgreementHandler.h | 9 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 135 +++- .../ieee80211/mac/coordinationfunction/Hcf.h | 3 + tests/unit/Ieee80211AddbaTransaction_1.test | 597 +++++++++++++++++- 13 files changed, 917 insertions(+), 77 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag.msg diff --git a/WHATSNEW b/WHATSNEW index c84aeb708da..6ad192716e7 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -73,20 +73,27 @@ Notable backward incompatible changes are the following: now returns a typed outcome containing the established agreement and, when local policy vetoes a successful response, its immediately terminated local agreement and a best-effort initiator DELBA for Hcf to enqueue after emitting - the Added and Deleted signals. Such DELBAs carry the ADDBA transaction identity - and ordinary data continues with Normal Ack. They remain eligible through the + the Added and Deleted signals. Such locally generated DELBAs carry + sender-local agreement-generation metadata (the originator reuses its ADDBA + transaction identity), and ordinary data continues with Normal Ack. They remain + eligible through the final fragment; aborting one fragment cancels its siblings. After retry backoff, a replacement ADDBA setup invalidates an older queued DELBA, so an unreported disposal cannot suppress setup indefinitely and a delayed frame cannot terminate a newer peer/TID agreement. The replacement setup reports that obsolete identity so Hcf also removes all of its queued or in-progress packets. - Originator DELBA transmission handlers now receive the full Packet so this - identity is retained. A transaction-tagged initiator DELBA remains eligible - across MAC retries and is retired only when its final fragment is acknowledged - or the transaction is terminally aborted. Custom handler implementations and - callers must adopt processAcknowledgedDelba() and the boolean - processAbortedDelba() outcome; processTransmittedDelba() still returns the - agreement removed by an untagged DELBA, or null when none was removed. + Originator and recipient DELBA transmission handlers now receive the full + Packet so sender-local agreement-generation metadata is retained through + fragmentation and MAC retries. This changes + IRecipientBlockAckAgreementHandler::processTransmittedDelba() from accepting + only a DELBA header to accepting Packet*; custom handler implementations and + callers must migrate to the Packet form. A locally tagged DELBA carrying + agreement-generation metadata remains eligible across MAC retries and is + retired only when its final fragment is acknowledged or the teardown is + terminally aborted. Custom handler + implementations and callers must adopt processAcknowledgedDelba() and the + boolean processAbortedDelba() outcome; processTransmittedDelba() still returns + the agreement removed by an untagged DELBA, or null when none was removed. OriginatorBlockAckAgreementPolicy now exposes the new `addbaResponseTimeout` and `addbaRetryBackoff` NED parameters. C++ implementations of diff --git a/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag.msg b/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag.msg new file mode 100644 index 00000000000..01a57c8aa59 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag.msg @@ -0,0 +1,17 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +import inet.common.INETDefs; +import inet.common.TagBase; + +namespace inet::ieee80211; + +// Identifies one local Block Ack agreement generation across packet transformations. +// This metadata is not transmitted as part of the DELBA frame. +class Ieee80211BlockAckAgreementTag extends TagBase +{ + uint64_t generationId; +} diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 2094435bf43..0f84c21a671 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -11,6 +11,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" @@ -111,6 +112,7 @@ void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCall Tid tid = id.first.second; const auto& delba = buildDelba(receiverAddr, tid, 39); auto delbaPacket = new Packet("Delba", delba); + delbaPacket->addTag()->setGenerationId(agreement->getTransactionId()); procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes } } @@ -307,11 +309,16 @@ bool OriginatorBlockAckAgreementHandler::isDelbaPending(const Packet *packet, co { if (!delba->getInitiator()) return true; - auto transactionTag = packet->findTag(); - if (transactionTag == nullptr) - return true; - auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); - return it != pendingTeardownTransactionIds.end() && it->second == transactionTag->getTransactionId(); + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { + auto generationId = agreementTag->getGenerationId(); + auto agreementIt = blockAckAgreements.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (agreementIt != blockAckAgreements.end()) + return agreementIt->second->getTransactionId() == generationId; + auto teardownIt = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + return teardownIt != pendingTeardownTransactionIds.end() && teardownIt->second == generationId; + } + return true; } void OriginatorBlockAckAgreementHandler::processTransmittedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) @@ -344,18 +351,23 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: // likewise cannot tear down the agreement before its final fragment. if (delba->getMoreFragments()) return nullptr; - auto transactionTag = packet->findTag(); - if (transactionTag != nullptr) { - auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); - if (it == pendingTeardownTransactionIds.end() || it->second != transactionTag->getTransactionId()) + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { + auto generationId = agreementTag->getGenerationId(); + auto teardownIt = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (teardownIt != pendingTeardownTransactionIds.end() && teardownIt->second == generationId) return nullptr; - // IEEE Std 802.11-2024, 11.5.3.2: teardown is performed by - // transmitting DELBA. IEEE Std 802.11-2024, 10.23.2.12.1 and - // 10.3.4.4 require unsuccessful MMPDU attempts to be retried until - // success or the applicable retry limit. Keep the local transaction - // live across ordinary MAC retries and retire it only after the final - // fragment is acknowledged or the frame is terminally aborted. - return nullptr; + auto agreement = getAgreement(delba->getReceiverAddress(), delba->getTid()); + if (agreement == nullptr || agreement->getTransactionId() != generationId) + return nullptr; + bool cancelPendingTransaction = agreement->isPending(); + std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + pendingTeardownTransactionIds[std::make_pair(delba->getReceiverAddress(), delba->getTid())] = generationId; + scheduleAddbaResponseTimer(callback); + if (cancelPendingTransaction) + callback->cancelAddbaTransaction(generationId, nullptr); + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), generationId, packet); + return terminatedAgreement; } auto agreement = getAgreement(delba->getReceiverAddress(), delba->getTid()); bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); @@ -372,15 +384,15 @@ bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator() || delba->getMoreFragments()) return false; - auto transactionTag = packet->findTag(); - if (transactionTag == nullptr) + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) return false; auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); - if (it == pendingTeardownTransactionIds.end() || it->second != transactionTag->getTransactionId()) + if (it == pendingTeardownTransactionIds.end() || it->second != agreementTag->getGenerationId()) return false; auto transactionId = it->second; pendingTeardownTransactionIds.erase(it); - callback->cancelAddbaTransaction(transactionId, packet); + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); return true; } @@ -389,13 +401,13 @@ bool OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBl auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator()) return false; - auto transactionTag = packet->findTag(); - if (transactionTag != nullptr) { + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); - if (it != pendingTeardownTransactionIds.end() && it->second == transactionTag->getTransactionId()) { + if (it != pendingTeardownTransactionIds.end() && it->second == agreementTag->getGenerationId()) { auto transactionId = it->second; pendingTeardownTransactionIds.erase(it); - callback->cancelAddbaTransaction(transactionId, packet); + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); return true; } } @@ -408,10 +420,19 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: auto agreement = getAgreement(delba->getTransmitterAddress(), delba->getTid()); bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; + auto agreementId = std::make_pair(delba->getTransmitterAddress(), delba->getTid()); + auto pendingTeardownIt = pendingTeardownTransactionIds.find(agreementId); + auto pendingTeardownTransactionId = pendingTeardownIt == pendingTeardownTransactionIds.end() ? 0 : pendingTeardownIt->second; + if (pendingTeardownIt != pendingTeardownTransactionIds.end()) + pendingTeardownTransactionIds.erase(pendingTeardownIt); std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) callback->cancelAddbaTransaction(transactionId, nullptr); + if (pendingTeardownTransactionId != 0) + callback->cancelBlockAckTeardown(true, delba->getTransmitterAddress(), delba->getTid(), pendingTeardownTransactionId, nullptr); + if (terminatedAgreement != nullptr) + callback->cancelBlockAckTeardown(true, delba->getTransmitterAddress(), delba->getTid(), terminatedAgreement->getTransactionId(), nullptr); return terminatedAgreement; } return nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 8be627c5135..252c30bff02 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -22,6 +22,8 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg protected: std::map, OriginatorBlockAckAgreement *> blockAckAgreements; std::map, simtime_t> addbaRetryDeadlines; + // A tagged local DELBA remains eligible after its agreement is removed + // until the final fragment is acknowledged or terminally aborted. std::map, uint64_t> pendingTeardownTransactionIds; uint8_t nextDialogToken = 1; uint64_t nextTransactionId = 1; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc index d93711d2839..becc47e3d70 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc @@ -12,10 +12,11 @@ namespace inet { namespace ieee80211 { -RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t lastUsedTime) : +RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t lastUsedTime, uint64_t generationId) : startingSequenceNumber(startingSequenceNumber), bufferSize(bufferSize), - blockAckTimeoutValue(lastUsedTime) + blockAckTimeoutValue(lastUsedTime), + generationId(generationId) { calculateExpirationTime(); blockAckRecord = new BlockAckRecord(originatorAddress, tid); @@ -39,4 +40,3 @@ std::ostream& operator<<(std::ostream& os, const RecipientBlockAckAgreement& agr } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 98f9ba589c4..cb7932399bb 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -22,9 +22,10 @@ class INET_API RecipientBlockAckAgreement : public cObject int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; simtime_t expirationTime = -1; + uint64_t generationId = 0; public: - RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t blockAckTimeoutValue); + RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t blockAckTimeoutValue, uint64_t generationId = 0); virtual ~RecipientBlockAckAgreement() { delete blockAckRecord; } virtual void blockAckPolicyFrameReceived(const Ptr& header); @@ -33,6 +34,7 @@ class INET_API RecipientBlockAckAgreement : public cObject virtual simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } virtual int getBufferSize() const { return bufferSize; } virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } + virtual uint64_t getGenerationId() const { return generationId; } virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } virtual simtime_t getExpirationTime() { return expirationTime; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index bd4851bb0a6..0f5eb2ab66b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -8,6 +8,8 @@ #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" namespace inet { namespace ieee80211 { @@ -58,6 +60,7 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb Tid tid = id.first.second; const auto& delba = buildDelba(receiverAddr, tid, 39); auto delbaPacket = new Packet("Delba", delba); + delbaPacket->addTag()->setGenerationId(agreement->getGenerationId()); procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes } } @@ -79,6 +82,13 @@ const Ptr RecipientBlockAckAgreementHandler::buildDelba(MacAddre return delba; } +uint64_t RecipientBlockAckAgreementHandler::allocateAgreementGenerationId() +{ + if (nextAgreementGenerationId == 0) + throw cRuntimeError("Block Ack agreement generation ID exhausted"); + return nextAgreementGenerationId++; +} + const Ptr RecipientBlockAckAgreementHandler::buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, bool accepted) { auto addbaResponse = makeShared(); @@ -138,9 +148,18 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAd // IEEE Std 802.11-2024, 10.25.2 and 11.5.2.3: accepting the // request establishes or modifies the recipient agreement when the // successful response is formed; transmission is not a state gate. - agreement = new RecipientBlockAckAgreement(addbaRequest->getTransmitterAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaResponse->getBufferSize(), addbaResponse->getBlockAckTimeoutValue()); + auto pendingTeardownIt = pendingTeardownGenerationIds.find(id); + auto pendingTeardownGenerationId = pendingTeardownIt == pendingTeardownGenerationIds.end() ? 0 : pendingTeardownIt->second; + if (pendingTeardownIt != pendingTeardownGenerationIds.end()) + pendingTeardownGenerationIds.erase(pendingTeardownIt); + if (pendingTeardownGenerationId != 0 && agreementHandlerCallback != nullptr) + agreementHandlerCallback->cancelBlockAckTeardown(false, id.first, id.second, pendingTeardownGenerationId, nullptr); + auto generationId = allocateAgreementGenerationId(); + agreement = new RecipientBlockAckAgreement(addbaRequest->getTransmitterAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaResponse->getBufferSize(), addbaResponse->getBlockAckTimeoutValue(), generationId); auto it = blockAckAgreements.find(id); if (it != blockAckAgreements.end()) { + if (agreementHandlerCallback != nullptr && (pendingTeardownGenerationId == 0 || pendingTeardownGenerationId != it->second->getGenerationId())) + agreementHandlerCallback->cancelBlockAckTeardown(false, id.first, id.second, it->second->getGenerationId(), nullptr); delete it->second; it->second = agreement; } @@ -170,19 +189,102 @@ void RecipientBlockAckAgreementHandler::processDuplicateAddbaRequest(const Ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) +bool RecipientBlockAckAgreementHandler::isDelbaPending(const Packet *packet, const Ptr& delba) const +{ + if (delba->getInitiator()) + return false; + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) + return true; + auto it = blockAckAgreements.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it != blockAckAgreements.end()) + return it->second->getGenerationId() == agreementTag->getGenerationId(); + auto teardownIt = pendingTeardownGenerationIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + return teardownIt != pendingTeardownGenerationIds.end() && teardownIt->second == agreementTag->getGenerationId(); +} + +std::unique_ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(Packet *packet) { + auto delba = findFragmentedActionContext(packet); + if (delba->getInitiator()) + return nullptr; // IEEE Std 802.11-2024, 10.4 and 11.5.3.5: the DELBA MMPDU has not // been transmitted while a later fragment is still outstanding. if (delba->getMoreFragments()) return nullptr; + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { + auto agreement = getAgreement(delba->getTid(), delba->getReceiverAddress()); + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + if (agreement != nullptr) { + if (agreement->getGenerationId() != agreementTag->getGenerationId()) + return nullptr; + auto terminatedAgreement = std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + pendingTeardownGenerationIds[agreementId] = agreementTag->getGenerationId(); + return terminatedAgreement; + } + auto teardownIt = pendingTeardownGenerationIds.find(agreementId); + if (teardownIt == pendingTeardownGenerationIds.end() || teardownIt->second != agreementTag->getGenerationId()) + return nullptr; + return nullptr; + } return std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); } +bool RecipientBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = findFragmentedActionContext(packet); + if (delba->getInitiator() || delba->getMoreFragments()) + return false; + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) + return false; + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + auto it = pendingTeardownGenerationIds.find(agreementId); + if (it == pendingTeardownGenerationIds.end() || it->second != agreementTag->getGenerationId()) + return false; + auto generationId = it->second; + pendingTeardownGenerationIds.erase(it); + if (callback != nullptr) + callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), generationId, packet); + return true; +} + +bool RecipientBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = findFragmentedActionContext(packet); + if (delba->getInitiator()) + return false; + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) + return false; + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + auto it = pendingTeardownGenerationIds.find(agreementId); + if (it == pendingTeardownGenerationIds.end() || it->second != agreementTag->getGenerationId()) + return false; + auto generationId = it->second; + pendingTeardownGenerationIds.erase(it); + if (callback != nullptr) + callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), generationId, packet); + return true; +} + +uint64_t RecipientBlockAckAgreementHandler::getPendingTeardownGenerationId(Tid tid, MacAddress originatorAddr) const +{ + auto it = pendingTeardownGenerationIds.find(std::make_pair(originatorAddr, tid)); + return it == pendingTeardownGenerationIds.end() ? 0 : it->second; +} + std::unique_ptr RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) { - if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - return std::unique_ptr(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + if (blockAckAgreementPolicy->isDelbaAccepted(delba)) { + auto agreementId = std::make_pair(delba->getTransmitterAddress(), delba->getTid()); + auto pendingTeardownIt = pendingTeardownGenerationIds.find(agreementId); + if (pendingTeardownIt != pendingTeardownGenerationIds.end()) + pendingTeardownGenerationIds.erase(pendingTeardownIt); + std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + return terminatedAgreement; + } return nullptr; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index f642c0a06b9..6d3af3e1d30 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -27,11 +27,16 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre protected: std::map, RecipientBlockAckAgreement *> blockAckAgreements; std::map, Ptr> lastAddbaResponses; + // A tagged local DELBA remains eligible after its agreement is removed + // until the final fragment is acknowledged or terminally aborted. + std::map, uint64_t> pendingTeardownGenerationIds; + uint64_t nextAgreementGenerationId = 1; protected: virtual RecipientBlockAckAgreement *removeAgreement(MacAddress originatorAddr, Tid tid); virtual const Ptr buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, bool accepted); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); + virtual uint64_t allocateAgreementGenerationId(); virtual simtime_t computeEarliestExpirationTime(); virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback); @@ -41,10 +46,14 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) override; virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) override; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) override; - virtual std::unique_ptr processTransmittedDelba(const Ptr& delba) override; + virtual std::unique_ptr processTransmittedDelba(Packet *packet) override; + virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; + virtual uint64_t getPendingTeardownGenerationId(Tid tid, MacAddress originatorAddr) const override; + virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const override; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index 09abd395fd3..d130e3cda52 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -9,6 +9,8 @@ #define __INET_IBLOCKACKAGREEMENTHANDLERCALLBACK_H #include "inet/common/INETDefs.h" +#include "inet/linklayer/common/MacAddress.h" +#include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" namespace inet { @@ -24,6 +26,10 @@ class INET_API IBlockAckAgreementHandlerCallback virtual void scheduleInactivityTimer(simtime_t timeout) = 0; virtual void scheduleAddbaResponseTimer(simtime_t deadline) = 0; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) = 0; + // Removes queued siblings of a sender-local DELBA without assuming that + // the frame is still removable from the active frame sequence. The + // agreement owner remains responsible for rejecting stale packets. + virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) {} }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 4eba49314c1..f0b969353d9 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -19,6 +19,9 @@ #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" namespace inet { + +class Packet; + namespace ieee80211 { class INET_API IRecipientBlockAckAgreementHandler @@ -29,11 +32,15 @@ class INET_API IRecipientBlockAckAgreementHandler virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) = 0; virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; - virtual std::unique_ptr processTransmittedDelba(const Ptr& delba) = 0; + virtual std::unique_ptr processTransmittedDelba(Packet *packet) = 0; + virtual bool processAcknowledgedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return false; } + virtual bool processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return false; } virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) = 0; + virtual uint64_t getPendingTeardownGenerationId(Tid, MacAddress) const { return 0; } + virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const { return true; } }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index b4b7f41a623..63d1fedcd6d 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -14,6 +14,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" @@ -73,8 +74,11 @@ void Hcf::initialize(int stage) originatorDataService->setFrameEligibilityFunction([this](const Packet *packet) { if (auto addbaReq = findFragmentedActionContext(packet)) return originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq); - if (auto delba = findFragmentedActionContext(packet)) - return originatorBlockAckAgreementHandler->isDelbaPending(packet, delba); + if (auto delba = findFragmentedActionContext(packet)) { + if (delba->getInitiator()) + return originatorBlockAckAgreementHandler->isDelbaPending(packet, delba); + return recipientBlockAckAgreementHandler->isDelbaPending(packet, delba); + } auto dataHeader = dynamicPtrCast(packet->peekAtFront()); // Hold this peer/TID while its ADDBA response is pending so no // already-sequenced MPDU can precede the advertised SSN. The @@ -146,12 +150,18 @@ bool Hcf::processDroppedBlockAckSetupFrame(Packet *packet) bool Hcf::processDroppedBlockAckTeardownFrame(Packet *packet) { - if (originatorBlockAckAgreementHandler) { - auto delba = findFragmentedActionContext(packet); - if (delba != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this)) { - rebuildPendingFrameEligibility(); - return true; - } + if (originatorBlockAckAgreementHandler == nullptr && recipientBlockAckAgreementHandler == nullptr) + return false; + auto delba = findFragmentedActionContext(packet); + if (delba == nullptr) + return false; + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr && blockAckTeardownsBeingCancelled.find(std::make_tuple(delba->getInitiator(), delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId())) != blockAckTeardownsBeingCancelled.end()) + return false; + bool aborted = delba->getInitiator() ? originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this) : recipientBlockAckAgreementHandler != nullptr && recipientBlockAckAgreementHandler->processAbortedDelba(packet, this); + if (aborted) { + rebuildPendingFrameEligibility(); + return true; } return false; } @@ -481,6 +491,71 @@ void Hcf::cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) } } +void Hcf::cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) +{ + Enter_Method("cancelBlockAckTeardown"); + if (edca == nullptr) + return; + auto teardownId = std::make_tuple(initiator, peerAddress, tid, generationId); + bool outerCancellation = blockAckTeardownsBeingCancelled.insert(teardownId).second; + auto belongsToTeardown = [initiator, peerAddress, tid, generationId, excludedPacket](Packet *packet) { + if (packet == excludedPacket) + return false; + auto delba = findFragmentedActionContext(packet); + auto agreementTag = packet->findTag(); + return delba != nullptr && agreementTag != nullptr && delba->getInitiator() == initiator && + delba->getReceiverAddress() == peerAddress && delba->getTid() == tid && + agreementTag->getGenerationId() == generationId; + }; + bool removedPacket = false; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto edcaf = edca->getEdcaf(AccessCategory(ac)); + auto pendingQueue = edcaf->getPendingQueue(); + if (pendingQueue != nullptr) { + for (int i = pendingQueue->getNumPackets() - 1; i >= 0; i--) { + auto packet = pendingQueue->getPacket(i); + if (belongsToTeardown(packet)) { + pendingQueue->removePacket(packet); + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + removedPacket = true; + } + } + } + auto inProgressFrames = edcaf->getInProgressFrames(); + if (inProgressFrames != nullptr) { + for (int i = inProgressFrames->getLength() - 1; i >= 0; i--) { + auto packet = inProgressFrames->getFrames(i); + if (belongsToTeardown(packet) && !isPacketReferencedByCurrentFrameSequence(packet)) { + auto header = packet->peekAtFront(); + auto extractedPacket = inProgressFrames->extractFrame(packet); + ASSERT(extractedPacket == packet); + take(packet); + auto managementRecoveryProcedure = edca->getMgmtAndNonQoSRecoveryProcedure(); + if (managementRecoveryProcedure != nullptr) + managementRecoveryProcedure->discardFrame(packet, header); + if (edcaf->getAckHandler() != nullptr) + edcaf->getAckHandler()->dropFrame(header); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + removedPacket = true; + } + } + } + } + if (removedPacket && originatorDataService != nullptr) { + rebuildPendingFrameEligibility(); + resumeEligibleChannelAccess(); + } + if (outerCancellation) + blockAckTeardownsBeingCancelled.erase(teardownId); +} + void Hcf::processLowerFrame(Packet *packet, const Ptr& header) { Enter_Method("processLowerFrame(%s)", packet->getName()); @@ -704,6 +779,8 @@ void Hcf::recipientProcessReceivedManagementFrame(const Ptr(header)) { @@ -721,7 +798,7 @@ void Hcf::recipientProcessReceivedManagementFrame(const PtraddTag()->setTransactionId(response.teardownTransactionId); + delbaPacket->addTag()->setGenerationId(response.teardownTransactionId); processMgmtFrame(delbaPacket, response.teardownDelba); } resumeEligibleChannelAccess(); @@ -730,19 +807,33 @@ void Hcf::recipientProcessReceivedManagementFrame(const PtrgetInitiator()) { + auto pendingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); + auto remainingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); if (agreement != nullptr) { recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getTransmitterAddress()); emit(blockAckAgreementDeletedSignal, agreement.get()); + cancelBlockAckTeardown(false, delba->getTransmitterAddress(), delba->getTid(), agreement->getGenerationId(), nullptr); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); } + bool pendingTeardownRetired = pendingTeardownGenerationId != 0 && remainingTeardownGenerationId != pendingTeardownGenerationId; + if (pendingTeardownRetired && (agreement == nullptr || agreement->getGenerationId() != pendingTeardownGenerationId)) + cancelBlockAckTeardown(false, delba->getTransmitterAddress(), delba->getTid(), pendingTeardownGenerationId, nullptr); + if (pendingTeardownRetired && agreement == nullptr && edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); } else { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid()); auto agreement = originatorBlockAckAgreementHandler->processReceivedDelba(delba, originatorBlockAckAgreementPolicy, this); if (wasPending && !originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid())) rebuildPendingFrameEligibility(); - if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) - emit(blockAckAgreementDeletedSignal, agreement.get()); + if (agreement != nullptr) { + if (agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); + } resumeEligibleChannelAccess(); } } @@ -892,17 +983,24 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< auto agreement = originatorBlockAckAgreementHandler->processTransmittedDelba(packet, this); if (wasPending) rebuildPendingFrameEligibility(); - if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) - emit(blockAckAgreementDeletedSignal, agreement.get()); + if (agreement != nullptr) { + if (agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); + } } else { - auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(delba); + auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(packet); if (agreement != nullptr) { // IEEE Std 802.11-2024, 10.25.4 and 11.5.3.5: recipient // resources are released whether the recipient transmitted or // received DELBA. The reorder window is such a resource. recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getReceiverAddress()); emit(blockAckAgreementDeletedSignal, agreement.get()); + cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), agreement->getGenerationId(), packet); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); } } } @@ -1064,7 +1162,8 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); if (auto delba = findFragmentedActionContext(lastTransmittedPacket)) { - if (delba->getInitiator() && originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this)) + bool acknowledged = delba->getInitiator() ? originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this) : recipientBlockAckAgreementHandler != nullptr && recipientBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this); + if (acknowledged) rebuildPendingFrameEligibility(); } edcaf->getInProgressFrames()->dropFrame(lastTransmittedPacket); @@ -1078,8 +1177,10 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const PtrisAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); auto obsoleteTeardownTransactionId = originatorBlockAckAgreementHandler->processAcknowledgedDataFrame(lastTransmittedPacket, dataHeader, originatorBlockAckAgreementPolicy, this); - if (obsoleteTeardownTransactionId != 0) - cancelAddbaTransaction(obsoleteTeardownTransactionId, nullptr); + if (obsoleteTeardownTransactionId != 0) { + cancelBlockAckTeardown(true, dataHeader->getReceiverAddress(), dataHeader->getTid(), obsoleteTeardownTransactionId, nullptr); + rebuildPendingFrameEligibility(); + } if (!wasPending && originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid())) rebuildPendingFrameEligibility(); } diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index b2230bff27b..a446e1bb07d 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -11,6 +11,7 @@ #include #include #include +#include #include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h" @@ -122,6 +123,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: std::set completedManagementTransactions; eventnumber_t completedManagementTransactionsEventNumber = -1; std::set cancelledManagementTransactions; + std::set> blockAckTeardownsBeingCancelled; // Protection mechanisms SingleProtectionMechanism *singleProtectionMechanism = nullptr; @@ -197,6 +199,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void scheduleInactivityTimer(simtime_t timeout) override; virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override; + virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override; std::string getFrameSequenceInfo() const; diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index f2ea78f6c0d..0523c2f0dad 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -16,6 +16,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" @@ -107,6 +108,9 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler simtime_t addbaDeadline = SIMTIME_MAX; std::vector cancelledTransactionIds; std::vector excludedPackets; + std::vector cancelledTeardownGenerationIds; + std::vector excludedTeardownPackets; + std::vector cancelledTeardownInitiators; virtual ~TestCallback() { for (auto packet : managementPackets) delete packet; } void forgetManagementPacket(Packet *packet) { managementPackets.erase(std::remove(managementPackets.begin(), managementPackets.end(), packet), managementPackets.end()); } @@ -122,6 +126,11 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler cancelledTransactionIds.push_back(transactionId); excludedPackets.push_back(excludedPacket); } + virtual void cancelBlockAckTeardown(bool initiator, MacAddress, Tid, uint64_t generationId, Packet *excludedPacket) override { + cancelledTeardownInitiators.push_back(initiator); + cancelledTeardownGenerationIds.push_back(generationId); + excludedTeardownPackets.push_back(excludedPacket); + } }; class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler @@ -129,7 +138,7 @@ class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler public: using OriginatorBlockAckAgreementHandler::buildAddbaRequest; void addEstablishedAgreement(MacAddress receiverAddress, Tid tid) { - auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(0), 64, true, false, 1, 1); + auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(0), 64, true, false, 1, nextTransactionId++); agreement->setIsAddbaResponseReceived(true); blockAckAgreements[std::make_pair(receiverAddress, tid)] = agreement; } @@ -581,6 +590,8 @@ class TestHcf : public Hcf int numProcessedDroppedSetupFrames = 0; int numResumedEligibleChannelAccess = 0; int numCancelledAddbaTransactions = 0; + int numCancelledBlockAckTeardowns = 0; + std::vector cancelledBlockAckTeardownGenerationIds; int numRebuildEligibilityCalls = 0; int numTransmittedControlResponses = 0; bool droppedSetupCancelled = false; @@ -635,6 +646,7 @@ class TestHcf : public Hcf void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } bool cancelManagement(uint64_t transactionId, Packet *excludedPacket = nullptr) { return cancelManagementTransaction(transactionId, excludedPacket); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } + void cancelTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket = nullptr) { cancelBlockAckTeardown(initiator, peerAddress, tid, generationId, excludedPacket); } void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } void processReceivedFrame(Packet *packet) { recipientProcessReceivedFrame(packet, packet->peekAtFront()); } void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } @@ -658,6 +670,7 @@ class TestHcf : public Hcf } virtual bool processDroppedBlockAckSetupFrame(Packet *packet) override { numProcessedDroppedSetupFrames++; return delegateDroppedSetupHandling ? Hcf::processDroppedBlockAckSetupFrame(packet) : droppedSetupCancelled; } virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { numCancelledAddbaTransactions++; Hcf::cancelAddbaTransaction(transactionId, excludedPacket); } + virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override { numCancelledBlockAckTeardowns++; cancelledBlockAckTeardownGenerationIds.push_back(generationId); Hcf::cancelBlockAckTeardown(initiator, peerAddress, tid, generationId, excludedPacket); } virtual void rebuildPendingFrameEligibility() override { numRebuildEligibilityCalls++; Hcf::rebuildPendingFrameEligibility(); } virtual void resumeEligibleChannelAccess() override { numResumedEligibleChannelAccess++; } virtual void scheduleInactivityTimer(simtime_t) override {} @@ -889,7 +902,21 @@ static Packet *makeTaggedDelbaPacket(const char *name, MacAddress receiverAddres delba->setFragmentNumber(fragmentNumber); delba->setMoreFragments(moreFragments); auto packet = new Packet(name, delba); - packet->addTag()->setTransactionId(transactionId); + packet->addTag()->setGenerationId(transactionId); + return packet; +} + +static Packet *makeTaggedRecipientDelbaPacket(const char *name, MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint64_t generationId) +{ + auto delba = makeShared(); + delba->setReceiverAddress(receiverAddress); + delba->setTid(tid); + delba->setInitiator(false); + delba->setSequenceNumber(sequenceNumber); + delba->setFragmentNumber(fragmentNumber); + delba->setMoreFragments(moreFragments); + auto packet = new Packet(name, delba); + packet->addTag()->setGenerationId(generationId); return packet; } @@ -1020,8 +1047,8 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); finalDelba->setMoreFragments(false); Packet firstDelbaPacket("firstDelbaFragment", firstDelba); Packet finalDelbaPacket("finalDelbaFragment", finalDelba); - firstDelbaPacket.addTag()->setTransactionId(77); - finalDelbaPacket.addTag()->setTransactionId(77); + firstDelbaPacket.addTag()->setGenerationId(77); + finalDelbaPacket.addTag()->setGenerationId(77); fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 77); ASSERT(fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); ASSERT(fragmentedDelbaHandler.processTransmittedDelba(&firstDelbaPacket, &fragmentedDelbaCallback) == nullptr); @@ -1034,15 +1061,15 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); ASSERT(!fragmentedDelbaHandler.processAcknowledgedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); ASSERT(fragmentedDelbaHandler.processAcknowledgedDelba(&finalDelbaPacket, &fragmentedDelbaCallback)); ASSERT(!fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); - ASSERT(fragmentedDelbaCallback.cancelledTransactionIds.back() == 77); - ASSERT(fragmentedDelbaCallback.excludedPackets.back() == &finalDelbaPacket); + ASSERT(fragmentedDelbaCallback.cancelledTeardownGenerationIds.back() == 77); + ASSERT(fragmentedDelbaCallback.excludedTeardownPackets.back() == &finalDelbaPacket); fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 78); - firstDelbaPacket.getTagForUpdate()->setTransactionId(78); + firstDelbaPacket.getTagForUpdate()->setGenerationId(78); ASSERT(fragmentedDelbaHandler.processAbortedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); ASSERT(!fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); - ASSERT(fragmentedDelbaCallback.cancelledTransactionIds.back() == 78); - ASSERT(fragmentedDelbaCallback.excludedPackets.back() == &firstDelbaPacket); + ASSERT(fragmentedDelbaCallback.cancelledTeardownGenerationIds.back() == 78); + ASSERT(fragmentedDelbaCallback.excludedTeardownPackets.back() == &firstDelbaPacket); } // IEEE Std 802.11-2024, 10.23.2.12.1 and 11.5.3.2: the HCF retry path keeps a @@ -1131,7 +1158,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); completeDelba->setInitiator(true); completeDelba->setSequenceNumber(SequenceNumberCyclic(204)); auto completeDelbaPacket = new Packet("acknowledgedDelba", completeDelba); - completeDelbaPacket->addTag()->setTransactionId(transactionId); + completeDelbaPacket->addTag()->setGenerationId(transactionId); completeDelbaPacket->insertAtBack(makeShared()); Fragmentation fragmentation; auto delbaFragments = fragmentation.fragmentFrame(completeDelbaPacket, { 4, 2 }); @@ -1152,13 +1179,13 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); Packet ackPacket("transactionalDelbaAck", makeShared()); hcf.processReceivedAck(&ackPacket, finalPacket, AC_BE); ASSERT(!originatorHandler->isDelbaPending(finalPacket, findFragmentedActionContext(finalPacket))); - ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); ASSERT(inProgressFrames.getLength() == 0); ASSERT(inProgressFrames.getNumDroppedFrames() == 1); ASSERT(!ackHandler.hasMgmtEntry(siblingHeader)); ASSERT(!ackHandler.hasMgmtEntry(finalHeader)); ASSERT(!originatorHandler->processAcknowledgedDelba(finalPacket, &hcf)); - ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); auto releasedFrames = inProgressFrames.releaseFrames(); ASSERT(releasedFrames == std::vector({ finalPacket })); delete finalPacket; @@ -1209,13 +1236,13 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); hcf.processTransmittedManagement(finalPacket, finalHeader, AC_BE); hcf.processFailedFrame(finalPacket); ASSERT(!originatorHandler->isDelbaPending(finalPacket, finalHeader)); - ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); ASSERT(inProgressFrames.getLength() == 0); ASSERT(inProgressFrames.getNumDroppedFrames() == 1); ASSERT(!ackHandler.hasMgmtEntry(siblingHeader)); ASSERT(!ackHandler.hasMgmtEntry(finalHeader)); ASSERT(!originatorHandler->processAbortedDelba(finalPacket, &hcf)); - ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); auto releasedFrames = inProgressFrames.releaseFrames(); ASSERT(releasedFrames == std::vector({ finalPacket })); delete finalPacket; @@ -1281,7 +1308,7 @@ ASSERT(handler.processReceivedAddbaResp(success, &originatorPolicy, &callback).e ASSERT(vetoDelba->getTid() == 6); ASSERT(vetoDelba->getReasonCode() == RC_END_BA); Packet vetoDelbaPacket("vetoDelba", vetoDelba); - vetoDelbaPacket.addTag()->setTransactionId(vetoResponseOutcome.teardownTransactionId); + vetoDelbaPacket.addTag()->setGenerationId(vetoResponseOutcome.teardownTransactionId); ASSERT(vetoHandler.isDelbaPending(&vetoDelbaPacket, vetoDelba)); ASSERT(vetoHandler.getAgreement(peer3, 6) == nullptr); ASSERT(vetoHandler.getRetryDeadline(peer3, 6) > simTime()); @@ -2709,8 +2736,8 @@ ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == replacementImmediateA auto heldTeardownRetryHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(212)); auto obsoleteHeldTeardownTransactionId = heldTeardownOriginatorHandler->processAcknowledgedDataFrame(&heldTeardownRetryTrigger, heldTeardownRetryHeader, &heldTeardownOriginatorPolicy, &heldTeardownRetryCallback); ASSERT(heldTeardownRetryCallback.managementPacket != nullptr); - ASSERT(obsoleteHeldTeardownTransactionId == heldDelbaPacket->getTag()->getTransactionId()); - heldTeardownHcf.cancelTransaction(obsoleteHeldTeardownTransactionId); + ASSERT(obsoleteHeldTeardownTransactionId == heldDelbaPacket->getTag()->getGenerationId()); + heldTeardownHcf.cancelTeardown(true, peer1, 7, obsoleteHeldTeardownTransactionId); ASSERT(heldTeardownQueue.getNumPackets() == 1); ASSERT(heldTeardownQueue.getPacket(0) == heldDataPacket); heldTeardownHcf.rebuildEligibilityIndex(); @@ -4760,6 +4787,542 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { pendingQueues[ac].unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcf); } +// An originator inactivity DELBA retains the agreement transaction identity. +// Once that agreement is replaced, every stale fragment is rejected while a +// final DELBA carrying the current generation removes the replacement once. +{ + TestOriginatorHandler expiryHandler; + TestOriginatorPolicy expiryPolicy; + TestCallback setupCallback; + Packet trigger("originatorExpiryTrigger"); + auto triggerHeader = makeQosHeader(peer1, 9, SequenceNumberCyclic(300)); + expiryHandler.processAcknowledgedDataFrame(&trigger, triggerHeader, &expiryPolicy, &setupCallback); + auto setupRequest = dynamicPtrCast(setupCallback.managementHeader); + expiryHandler.processTransmittedAddbaReq(setupCallback.managementPacket, setupRequest, &expiryPolicy, &setupCallback); + auto setupResponse = makeResponse(peer1, 9, setupRequest->getDialogToken(), 0); + setupResponse->setBlockAckTimeoutValue(1); + auto establishedOutcome = expiryHandler.processReceivedAddbaResp(setupResponse, &expiryPolicy, &setupCallback); + auto firstAgreement = establishedOutcome.establishedAgreement; + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getTransactionId(); + firstAgreement->setBlockAckTimeoutValue(1); + firstAgreement->calculateExpirationTime(); + wait(1); + TestCallback expiryCallback; + expiryHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + auto stalePacket = expiryCallback.managementPacket; + auto staleDelba = dynamicPtrCast(expiryCallback.managementHeader); + ASSERT(stalePacket != nullptr && staleDelba != nullptr); + ASSERT(stalePacket->getTag()->getGenerationId() == firstGenerationId); + + auto receivedDelba = makeShared(); + receivedDelba->setTransmitterAddress(peer1); + receivedDelba->setTid(9); + auto terminatedFirstAgreement = expiryHandler.processReceivedDelba(receivedDelba, &expiryPolicy, &expiryCallback); + ASSERT(terminatedFirstAgreement != nullptr); + TestCallback replacementCallback; + Packet replacementTrigger("originatorReplacementTrigger"); + auto replacementHeader = makeQosHeader(peer1, 9, SequenceNumberCyclic(301)); + expiryHandler.processAcknowledgedDataFrame(&replacementTrigger, replacementHeader, &expiryPolicy, &replacementCallback); + auto replacementRequest = dynamicPtrCast(replacementCallback.managementHeader); + expiryHandler.processTransmittedAddbaReq(replacementCallback.managementPacket, replacementRequest, &expiryPolicy, &replacementCallback); + auto replacementResponse = makeResponse(peer1, 9, replacementRequest->getDialogToken(), 0); + auto replacementOutcome = expiryHandler.processReceivedAddbaResp(replacementResponse, &expiryPolicy, &replacementCallback); + auto secondAgreement = replacementOutcome.establishedAgreement; + ASSERT(secondAgreement != nullptr); + auto secondGenerationId = secondAgreement->getTransactionId(); + ASSERT(secondGenerationId != firstGenerationId); + ASSERT(!expiryHandler.isDelbaPending(stalePacket, staleDelba)); + + auto staleFragmentBody = staticPtrCast(staleDelba->dupShared()); + staleFragmentBody->setSequenceNumber(SequenceNumberCyclic(302)); + auto staleFragmentSource = new Packet("originatorStaleDelbaSource", staleFragmentBody); + staleFragmentSource->addTag()->setGenerationId(firstGenerationId); + staleFragmentSource->insertAtBack(makeShared()); + Fragmentation fragmentation; + auto staleFragments = fragmentation.fragmentFrame(staleFragmentSource, { 4, 2 }); + for (auto fragment : *staleFragments) + ASSERT(fragment->getTag()->getGenerationId() == firstGenerationId); + ASSERT(!expiryHandler.isDelbaPending(staleFragments->at(0), findFragmentedActionContext(staleFragments->at(0)))); + ASSERT(expiryHandler.processTransmittedDelba(staleFragments->at(1), &expiryCallback) == nullptr); + ASSERT(expiryHandler.getAgreement(peer1, 9) == secondAgreement); + delete staleFragments->at(0); + delete staleFragments->at(1); + delete staleFragments; + + auto currentDelba = makeShared(); + currentDelba->setReceiverAddress(peer1); + currentDelba->setTid(9); + currentDelba->setInitiator(true); + auto currentPacket = new Packet("originatorCurrentDelba", currentDelba); + currentPacket->addTag()->setGenerationId(secondGenerationId); + auto terminatedSecondAgreement = expiryHandler.processTransmittedDelba(currentPacket, &replacementCallback); + ASSERT(terminatedSecondAgreement != nullptr); + ASSERT(expiryHandler.getAgreement(peer1, 9) == nullptr); + ASSERT(expiryHandler.processTransmittedDelba(currentPacket, &replacementCallback) == nullptr); + delete currentPacket; +} + +// A recipient inactivity DELBA gets a handler-owned generation on every +// accepted replacement. Stale final fragments cannot remove the replacement, +// while a current-generation final DELBA removes it exactly once. +{ + TestRecipientHandler expiryHandler; + TestRecipientPolicy expiryPolicy; + expiryPolicy.blockAckTimeoutValue = 1; + TestCallback expiryCallback; + auto request = makeRequest(peer2, 10, 101, SequenceNumberCyclic(310), 32); + request->setBlockAckTimeoutValue(1); + auto firstAgreement = expiryHandler.processReceivedAddbaRequest(request, &expiryPolicy, &expiryCallback, &expiryCallback); + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getGenerationId(); + wait(1); + expiryHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + auto stalePacket = expiryCallback.managementPacket; + auto staleDelba = dynamicPtrCast(expiryCallback.managementHeader); + ASSERT(stalePacket != nullptr && staleDelba != nullptr); + ASSERT(stalePacket->getTag()->getGenerationId() == firstGenerationId); + + auto replacementRequest = makeRequest(peer2, 10, 102, SequenceNumberCyclic(311), 32); + replacementRequest->setBlockAckTimeoutValue(1); + auto secondAgreement = expiryHandler.processReceivedAddbaRequest(replacementRequest, &expiryPolicy, &expiryCallback, &expiryCallback); + ASSERT(secondAgreement != nullptr); + auto secondGenerationId = secondAgreement->getGenerationId(); + ASSERT(secondGenerationId != firstGenerationId); + ASSERT(!expiryHandler.isDelbaPending(stalePacket, staleDelba)); + ASSERT(expiryHandler.processTransmittedDelba(stalePacket) == nullptr); + ASSERT(expiryHandler.getAgreement(10, peer2) == secondAgreement); + ASSERT(std::find(expiryCallback.cancelledTeardownGenerationIds.begin(), expiryCallback.cancelledTeardownGenerationIds.end(), firstGenerationId) != expiryCallback.cancelledTeardownGenerationIds.end()); + + auto staleFragmentBody = staticPtrCast(staleDelba->dupShared()); + staleFragmentBody->setSequenceNumber(SequenceNumberCyclic(312)); + auto staleFragmentSource = new Packet("recipientStaleDelbaSource", staleFragmentBody); + staleFragmentSource->addTag()->setGenerationId(firstGenerationId); + staleFragmentSource->insertAtBack(makeShared()); + Fragmentation fragmentation; + auto staleFragments = fragmentation.fragmentFrame(staleFragmentSource, { 4, 2 }); + for (auto fragment : *staleFragments) + ASSERT(fragment->getTag()->getGenerationId() == firstGenerationId); + ASSERT(!expiryHandler.isDelbaPending(staleFragments->at(0), findFragmentedActionContext(staleFragments->at(0)))); + ASSERT(expiryHandler.processTransmittedDelba(staleFragments->at(1)) == nullptr); + ASSERT(expiryHandler.getAgreement(10, peer2) == secondAgreement); + delete staleFragments->at(0); + delete staleFragments->at(1); + delete staleFragments; + + auto currentPacket = makeTaggedRecipientDelbaPacket("recipientCurrentDelba", peer2, 10, SequenceNumberCyclic(313), 0, false, secondGenerationId); + auto terminatedAgreement = expiryHandler.processTransmittedDelba(currentPacket); + ASSERT(terminatedAgreement != nullptr); + ASSERT(expiryHandler.getAgreement(10, peer2) == nullptr); + ASSERT(expiryHandler.processTransmittedDelba(currentPacket) == nullptr); + delete currentPacket; +} + +// HCF routes recipient DELBAs to the recipient owner and rebuilds its +// eligibility index when replacement or final teardown changes the generation. +// The replacement callback removes queued stale siblings; the active current +// frame is then consumed exactly once. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy hcfOriginatorPolicy; + TestRecipientPolicy hcfRecipientPolicy; + TestCallback managementCallback; + auto request = makeRequest(peer2, 11, 103, SequenceNumberCyclic(320), 32); + auto firstAgreement = recipientHandler->processReceivedAddbaRequest(request, &hcfRecipientPolicy, &managementCallback, &managementCallback); + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getGenerationId(); + + TestOriginatorQosMacDataService dataService; + dataService.setFrameEligibilityFunction([recipientHandler](const Packet *packet) { + auto delba = findFragmentedActionContext(packet); + return delba == nullptr || recipientHandler->isDelbaPending(packet, delba); + }); + TestPacketQueue pendingQueue; + QosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestRecipientDataService recipientDataService; + OriginatorQosAckPolicy ackPolicy; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &hcfOriginatorPolicy, recipientHandler, &hcfRecipientPolicy); + hcf.configureRecipientDataService(&recipientDataService); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + hcf.configureManagementCallback(&managementCallback); + hcf.observePendingQueue(&pendingQueue); + + auto stalePacket = makeTaggedRecipientDelbaPacket("hcfStaleRecipientDelba", peer2, 11, SequenceNumberCyclic(321), 0, false, firstGenerationId); + pendingQueue.enqueuePacket(stalePacket); + hcf.trackFrame(stalePacket, AC_BE); + ASSERT(hcf.getNumEligiblePendingFrames(AC_BE) == 1); + + auto replacementRequest = makeRequest(peer2, 11, 104, SequenceNumberCyclic(322), 32); + hcf.processReceivedManagementFrame(replacementRequest); + auto secondAgreement = recipientHandler->getAgreement(11, peer2); + ASSERT(secondAgreement != nullptr); + ASSERT(secondAgreement->getGenerationId() != firstGenerationId); + ASSERT(pendingQueue.isEmpty()); + ASSERT(hcf.getNumEligiblePendingFrames(AC_BE) == 0); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); + ASSERT(hcf.numRebuildEligibilityCalls > 0); + + auto currentPacket = makeTaggedRecipientDelbaPacket("hcfCurrentRecipientDelba", peer2, 11, SequenceNumberCyclic(323), 0, false, secondAgreement->getGenerationId()); + pendingQueue.enqueuePacket(currentPacket); + hcf.trackFrame(currentPacket, AC_BE); + ASSERT(hcf.getNumEligiblePendingFrames(AC_BE) == 1); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.processTransmittedManagement(currentPacket, currentPacket->peekAtFront(), AC_BE); + ASSERT(recipientHandler->getAgreement(11, peer2) == nullptr); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 2); + ASSERT(hcf.getNumEligiblePendingFrames(AC_BE) == 0); + hcf.processTransmittedManagement(currentPacket, currentPacket->peekAtFront(), AC_BE); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 2); + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + pendingQueue.removePacket(currentPacket); + delete currentPacket; +} + +// A rejected peer DELBA must not retire a locally pending recipient teardown. +// Once the same DELBA is accepted, HCF cancels exactly that generation without +// repeating the first-transmission reset or deletion notification. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestCallback responseCallback; + TestOriginatorQosMacDataService dataService; + dataService.setFrameEligibilityFunction([recipientHandler](const Packet *packet) { + auto delba = findFragmentedActionContext(packet); + return delba == nullptr || recipientHandler->isDelbaPending(packet, delba); + }); + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + edca.nonQosRecoveryProcedure = &recoveryProcedure; + OriginatorQosAckPolicy ackPolicy; + TestMac mac; + TestRecipientDataService recipientDataService; + TestHcf hcf; + hcf.configureMac(&mac); + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureRecipientDataService(&recipientDataService); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + TestSignalListener deletionListener; + TestPacketDropSignalListener dropListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.subscribe(packetDroppedSignal, &dropListener); + + auto request = makeRequest(peer2, 14, 109, SequenceNumberCyclic(370), 32); + auto agreement = recipientHandler->processReceivedAddbaRequest(request, &recipientPolicy, &responseCallback, &hcf); + ASSERT(agreement != nullptr); + auto generationId = agreement->getGenerationId(); + auto pendingPacket = makeTaggedRecipientDelbaPacket("recipientRejectedPeerDelba", peer2, 14, SequenceNumberCyclic(371), 0, false, generationId); + auto pendingHeader = pendingPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(pendingPacket); + ackHandler.frameGotInProgress(pendingHeader); + hcf.trackFrame(pendingPacket, AC_BE); + hcf.processTransmittedManagement(pendingPacket, pendingHeader, AC_BE); + ASSERT(recipientHandler->getAgreement(14, peer2) == nullptr); + ASSERT(recipientHandler->isDelbaPending(pendingPacket, pendingHeader)); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(deletionListener.numSignals == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); + ASSERT(hcf.cancelledBlockAckTeardownGenerationIds.back() == generationId); + hcf.processFailedFrame(pendingPacket); + + recipientPolicy.delbaAccepted = false; + auto rejectedDelba = makeShared(); + rejectedDelba->setTransmitterAddress(peer2); + rejectedDelba->setReceiverAddress(peer1); + rejectedDelba->setTid(14); + rejectedDelba->setInitiator(true); + hcf.processReceivedManagementFrame(rejectedDelba); + ASSERT(recipientHandler->isDelbaPending(pendingPacket, pendingHeader)); + ASSERT(inProgressFrames.getLength() == 1); + ASSERT(inProgressFrames.getFrameToTransmit() == pendingPacket); + ASSERT(hcf.hasFrame(AC_BE)); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); + ASSERT(hcf.cancelledBlockAckTeardownGenerationIds.back() == generationId); + ASSERT(dropListener.numOtherPacketDrops == 0); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(deletionListener.numSignals == 1); + + recipientPolicy.delbaAccepted = true; + auto acceptedDelba = makeShared(); + acceptedDelba->setTransmitterAddress(peer2); + acceptedDelba->setReceiverAddress(peer1); + acceptedDelba->setTid(14); + acceptedDelba->setInitiator(true); + hcf.processReceivedManagementFrame(acceptedDelba); + ASSERT(recipientHandler->getPendingTeardownGenerationId(14, peer2) == 0); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(!hcf.hasFrame(AC_BE)); + ASSERT(hcf.numCancelledBlockAckTeardowns == 2); + ASSERT(hcf.cancelledBlockAckTeardownGenerationIds.back() == generationId); + ASSERT(dropListener.numOtherPacketDrops == 1); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(deletionListener.numSignals == 1); + auto staleProbe = makeTaggedRecipientDelbaPacket("recipientRejectedPeerDelbaProbe", peer2, 14, SequenceNumberCyclic(372), 0, false, generationId); + ASSERT(!recipientHandler->isDelbaPending(staleProbe, staleProbe->peekAtFront())); + delete staleProbe; + + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.unsubscribe(packetDroppedSignal, &dropListener); +} + +// A tagged originator DELBA removes the agreement on its first transmission, +// but remains selectable through an unacknowledged retry. ACK, terminal abort, +// and replacement each retire only their exact teardown generation. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestOriginatorQosMacDataService dataService; + dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto delba = findFragmentedActionContext(packet); + return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); + }); + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + edca.nonQosRecoveryProcedure = &recoveryProcedure; + OriginatorQosAckPolicy ackPolicy; + TestMac mac; + TestHcf hcf; + hcf.configureMac(&mac); + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + TestSignalListener deletionListener; + TestPacketDropSignalListener dropListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.subscribe(packetDroppedSignal, &dropListener); + + originatorHandler->addEstablishedAgreement(peer1, 12); + auto firstGenerationId = originatorHandler->getAgreement(peer1, 12)->getTransactionId(); + auto acknowledgedPacket = makeTaggedDelbaPacket("originatorRetryAcknowledged", peer1, 12, SequenceNumberCyclic(350), 0, false, firstGenerationId); + auto acknowledgedHeader = acknowledgedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(acknowledgedPacket); + ackHandler.frameGotInProgress(acknowledgedHeader); + hcf.trackFrame(acknowledgedPacket, AC_BE); + hcf.processTransmittedManagement(acknowledgedPacket, acknowledgedHeader, AC_BE); + ASSERT(originatorHandler->getAgreement(peer1, 12) == nullptr); + ASSERT(originatorHandler->isDelbaPending(acknowledgedPacket, acknowledgedHeader)); + ASSERT(deletionListener.numSignals == 1); + hcf.processFailedFrame(acknowledgedPacket); + ASSERT(originatorHandler->isDelbaPending(acknowledgedPacket, acknowledgedPacket->peekAtFront())); + ASSERT(inProgressFrames.getFrameToTransmit() == acknowledgedPacket); + hcf.processTransmittedManagement(acknowledgedPacket, acknowledgedPacket->peekAtFront(), AC_BE); + Packet acknowledgedAck("originatorRetryAck", makeShared()); + hcf.processReceivedAck(&acknowledgedAck, acknowledgedPacket, AC_BE); + ASSERT(!originatorHandler->isDelbaPending(acknowledgedPacket, acknowledgedPacket->peekAtFront())); + ASSERT(deletionListener.numSignals == 1); + ASSERT(inProgressFrames.getLength() == 0); + auto acknowledgedFrames = inProgressFrames.releaseFrames(); + ASSERT(acknowledgedFrames == std::vector({ acknowledgedPacket })); + delete acknowledgedPacket; + + originatorHandler->addEstablishedAgreement(peer1, 12); + auto abortedGenerationId = originatorHandler->getAgreement(peer1, 12)->getTransactionId(); + auto abortedPacket = makeTaggedDelbaPacket("originatorRetryAborted", peer1, 12, SequenceNumberCyclic(351), 0, false, abortedGenerationId); + auto abortedHeader = abortedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(abortedPacket); + ackHandler.frameGotInProgress(abortedHeader); + hcf.trackFrame(abortedPacket, AC_BE); + hcf.processTransmittedManagement(abortedPacket, abortedHeader, AC_BE); + ASSERT(originatorHandler->isDelbaPending(abortedPacket, abortedHeader)); + hcf.processFailedFrame(abortedPacket); + ASSERT(originatorHandler->isDelbaPending(abortedPacket, abortedPacket->peekAtFront())); + ASSERT(inProgressFrames.getFrameToTransmit() == abortedPacket); + hcf.processTransmittedManagement(abortedPacket, abortedPacket->peekAtFront(), AC_BE); + hcf.processFailedFrame(abortedPacket); + ASSERT(!originatorHandler->isDelbaPending(abortedPacket, abortedPacket->peekAtFront())); + ASSERT(deletionListener.numSignals == 2); + auto abortedFrames = inProgressFrames.releaseFrames(); + ASSERT(abortedFrames == std::vector({ abortedPacket })); + delete abortedPacket; + + originatorHandler->addEstablishedAgreement(peer1, 12); + auto replacedGenerationId = originatorHandler->getAgreement(peer1, 12)->getTransactionId(); + auto replacedPacket = makeTaggedDelbaPacket("originatorRetryReplaced", peer1, 12, SequenceNumberCyclic(352), 0, false, replacedGenerationId); + auto replacedHeader = replacedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(replacedPacket); + ackHandler.frameGotInProgress(replacedHeader); + hcf.trackFrame(replacedPacket, AC_BE); + hcf.processTransmittedManagement(replacedPacket, replacedHeader, AC_BE); + ASSERT(originatorHandler->isDelbaPending(replacedPacket, replacedHeader)); + TestCallback replacementCallback; + Packet replacementTrigger("originatorRetryReplacementTrigger"); + auto replacementHeader = makeQosHeader(peer1, 12, SequenceNumberCyclic(353)); + auto obsoleteGenerationId = originatorHandler->processAcknowledgedDataFrame(&replacementTrigger, replacementHeader, &originatorPolicy, &replacementCallback); + ASSERT(obsoleteGenerationId == replacedGenerationId); + hcf.cancelTeardown(true, peer1, 12, obsoleteGenerationId); + auto replacedProbe = makeTaggedDelbaPacket("originatorRetryReplacedProbe", peer1, 12, SequenceNumberCyclic(354), 0, false, replacedGenerationId); + ASSERT(!originatorHandler->isDelbaPending(replacedProbe, replacedProbe->peekAtFront())); + delete replacedProbe; + ASSERT(originatorHandler->getAgreement(peer1, 12) != nullptr); + ASSERT(originatorHandler->getAgreement(peer1, 12)->getTransactionId() != replacedGenerationId); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(deletionListener.numSignals == 3); + ASSERT(dropListener.numOtherPacketDrops == 1); + + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.unsubscribe(packetDroppedSignal, &dropListener); +} + +// Recipient teardown generations follow the same retry lifetime as +// originator generations, including recipient-side ACK/abort callbacks and +// replacement disposal while an old final DELBA is still in progress. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestCallback responseCallback; + TestOriginatorQosMacDataService dataService; + dataService.setFrameEligibilityFunction([recipientHandler](const Packet *packet) { + auto delba = findFragmentedActionContext(packet); + return delba == nullptr || recipientHandler->isDelbaPending(packet, delba); + }); + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + edca.nonQosRecoveryProcedure = &recoveryProcedure; + OriginatorQosAckPolicy ackPolicy; + TestMac mac; + TestRecipientDataService recipientDataService; + TestHcf hcf; + hcf.configureMac(&mac); + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureRecipientDataService(&recipientDataService); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + TestSignalListener deletionListener; + TestPacketDropSignalListener dropListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.subscribe(packetDroppedSignal, &dropListener); + + auto firstRequest = makeRequest(peer2, 13, 105, SequenceNumberCyclic(360), 32); + auto firstAgreement = recipientHandler->processReceivedAddbaRequest(firstRequest, &recipientPolicy, &responseCallback, &hcf); + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getGenerationId(); + auto acknowledgedPacket = makeTaggedRecipientDelbaPacket("recipientRetryAcknowledged", peer2, 13, SequenceNumberCyclic(361), 0, false, firstGenerationId); + auto acknowledgedHeader = acknowledgedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(acknowledgedPacket); + ackHandler.frameGotInProgress(acknowledgedHeader); + hcf.trackFrame(acknowledgedPacket, AC_BE); + hcf.processTransmittedManagement(acknowledgedPacket, acknowledgedHeader, AC_BE); + ASSERT(recipientHandler->getAgreement(13, peer2) == nullptr); + ASSERT(recipientHandler->isDelbaPending(acknowledgedPacket, acknowledgedHeader)); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 1); + hcf.processFailedFrame(acknowledgedPacket); + ASSERT(recipientHandler->isDelbaPending(acknowledgedPacket, acknowledgedPacket->peekAtFront())); + ASSERT(inProgressFrames.getFrameToTransmit() == acknowledgedPacket); + hcf.processTransmittedManagement(acknowledgedPacket, acknowledgedPacket->peekAtFront(), AC_BE); + Packet acknowledgedAck("recipientRetryAck", makeShared()); + hcf.processReceivedAck(&acknowledgedAck, acknowledgedPacket, AC_BE); + ASSERT(!recipientHandler->isDelbaPending(acknowledgedPacket, acknowledgedPacket->peekAtFront())); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 1); + auto acknowledgedFrames = inProgressFrames.releaseFrames(); + ASSERT(acknowledgedFrames == std::vector({ acknowledgedPacket })); + delete acknowledgedPacket; + + auto abortedRequest = makeRequest(peer2, 13, 106, SequenceNumberCyclic(362), 32); + auto abortedAgreement = recipientHandler->processReceivedAddbaRequest(abortedRequest, &recipientPolicy, &responseCallback, &hcf); + ASSERT(abortedAgreement != nullptr); + auto abortedGenerationId = abortedAgreement->getGenerationId(); + auto abortedPacket = makeTaggedRecipientDelbaPacket("recipientRetryAborted", peer2, 13, SequenceNumberCyclic(363), 0, false, abortedGenerationId); + auto abortedHeader = abortedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(abortedPacket); + ackHandler.frameGotInProgress(abortedHeader); + hcf.trackFrame(abortedPacket, AC_BE); + hcf.processTransmittedManagement(abortedPacket, abortedHeader, AC_BE); + ASSERT(recipientHandler->isDelbaPending(abortedPacket, abortedHeader)); + hcf.processFailedFrame(abortedPacket); + ASSERT(recipientHandler->isDelbaPending(abortedPacket, abortedPacket->peekAtFront())); + ASSERT(inProgressFrames.getFrameToTransmit() == abortedPacket); + hcf.processTransmittedManagement(abortedPacket, abortedPacket->peekAtFront(), AC_BE); + hcf.processFailedFrame(abortedPacket); + ASSERT(!recipientHandler->isDelbaPending(abortedPacket, abortedPacket->peekAtFront())); + ASSERT(deletionListener.numSignals == 2); + ASSERT(recipientDataService.numReorderingResets == 2); + auto abortedFrames = inProgressFrames.releaseFrames(); + ASSERT(abortedFrames == std::vector({ abortedPacket })); + delete abortedPacket; + + auto replacedRequest = makeRequest(peer2, 13, 107, SequenceNumberCyclic(364), 32); + auto replacedAgreement = recipientHandler->processReceivedAddbaRequest(replacedRequest, &recipientPolicy, &responseCallback, &hcf); + ASSERT(replacedAgreement != nullptr); + auto replacedGenerationId = replacedAgreement->getGenerationId(); + auto replacedPacket = makeTaggedRecipientDelbaPacket("recipientRetryReplaced", peer2, 13, SequenceNumberCyclic(365), 0, false, replacedGenerationId); + auto replacedHeader = replacedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(replacedPacket); + ackHandler.frameGotInProgress(replacedHeader); + hcf.trackFrame(replacedPacket, AC_BE); + hcf.processTransmittedManagement(replacedPacket, replacedHeader, AC_BE); + ASSERT(recipientHandler->isDelbaPending(replacedPacket, replacedHeader)); + auto replacementRequest = makeRequest(peer2, 13, 108, SequenceNumberCyclic(366), 32); + auto replacementAgreement = recipientHandler->processReceivedAddbaRequest(replacementRequest, &recipientPolicy, &responseCallback, &hcf); + ASSERT(replacementAgreement != nullptr); + ASSERT(replacementAgreement->getGenerationId() != replacedGenerationId); + auto replacedProbe = makeTaggedRecipientDelbaPacket("recipientRetryReplacedProbe", peer2, 13, SequenceNumberCyclic(367), 0, false, replacedGenerationId); + ASSERT(!recipientHandler->isDelbaPending(replacedProbe, replacedProbe->peekAtFront())); + delete replacedProbe; + ASSERT(recipientHandler->getAgreement(13, peer2) == replacementAgreement); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(deletionListener.numSignals == 3); + ASSERT(recipientDataService.numReorderingResets == 3); + ASSERT(dropListener.numOtherPacketDrops == 1); + + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.unsubscribe(packetDroppedSignal, &dropListener); +} + EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; %contains: stdout From 3d72fefd40844ddea11d71b60fab3f558e132935 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:04 +0200 Subject: [PATCH 08/16] ieee80211: schedule Block Ack inactivity with absolute deadlines Make originator and recipient agreement handlers report absolute, role-specific inactivity deadlines. Have HCF schedule the earliest one with rescheduleAt() and expire only agreements whose recorded deadline has actually elapsed. Refresh the correct role on QoS data, BAR, and Block Ack activity, and retire the matching generation once when inactivity expires or teardown is aborted. This avoids treating an absolute timestamp as a relative delay and repeatedly rearming an already expired agreement. Cover independent originator and recipient deadlines, activity refresh, simultaneous expiry, stale generations, and terminal cleanup. --- .../blockack/OriginatorBlockAckAgreement.h | 10 +- .../OriginatorBlockAckAgreementHandler.cc | 38 +++- .../OriginatorBlockAckAgreementHandler.h | 2 +- .../mac/blockack/RecipientBlockAckAgreement.h | 10 +- .../RecipientBlockAckAgreementHandler.cc | 66 ++++-- .../RecipientBlockAckAgreementHandler.h | 7 +- .../IBlockAckAgreementHandlerCallback.h | 10 +- .../IOriginatorBlockAckAgreementHandler.h | 10 +- .../IRecipientBlockAckAgreementHandler.h | 15 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 41 +++- .../ieee80211/mac/coordinationfunction/Hcf.h | 7 +- .../Ieee80211BlockAckInactivityTimer_1.test | 129 +++++++++++ tests/unit/Ieee80211AddbaTransaction_1.test | 213 +++++++++++++++++- 13 files changed, 510 insertions(+), 48 deletions(-) create mode 100644 tests/module/Ieee80211BlockAckInactivityTimer_1.test diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index 8099d7c1bdb..25bad8d77a2 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -38,6 +38,9 @@ class INET_API OriginatorBlockAckAgreement : public cObject simtime_t addbaResponseDeadline = -1; simtime_t blockAckTimeoutValue = -1; simtime_t expirationTime = -1; + // The agreement stays installed until the timeout DELBA is transmitted; + // prevent that pending teardown from being re-armed by late activity. + bool inactivityExpired = false; public: OriginatorBlockAckAgreement(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, bool isAMsduSupported, bool isDelayedBlockAckPolicySupported, uint8_t dialogToken, uint64_t transactionId) : @@ -79,8 +82,13 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual void setAddbaResponseDeadline(simtime_t addbaResponseDeadline) { this->addbaResponseDeadline = addbaResponseDeadline; } virtual void baPolicyFrameSent() { numSentBaPolicyFrames++; } - virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } + virtual void calculateExpirationTime() { + if (!inactivityExpired) + expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; + } virtual simtime_t getExpirationTime() { return expirationTime; } + virtual bool isInactivityExpired() const { return inactivityExpired; } + virtual void markInactivityExpired() { inactivityExpired = true; expirationTime = SIMTIME_MAX; } }; } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 0f84c21a671..ad22a8fce99 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -38,7 +38,7 @@ simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime() simtime_t earliestTime = SIMTIME_MAX; for (auto id : blockAckAgreements) { auto agreement = id.second; - if (agreement->getIsAddbaResponseReceived()) { + if (agreement->getIsAddbaResponseReceived() && !agreement->isInactivityExpired()) { ASSERT(earliestTime >= 0); ASSERT(agreement->getExpirationTime() >= 0); earliestTime = std::min(earliestTime, agreement->getExpirationTime()); @@ -107,7 +107,8 @@ void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCall simtime_t now = simTime(); for (auto id : blockAckAgreements) { auto agreement = id.second; - if (agreement->getExpirationTime() == now) { + if (agreement->getIsAddbaResponseReceived() && !agreement->isInactivityExpired() && agreement->getExpirationTime() <= now) { + agreement->markInactivityExpired(); MacAddress receiverAddr = id.first.first; Tid tid = id.first.second; const auto& delba = buildDelba(receiverAddr, tid, 39); @@ -144,7 +145,7 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const Ptr(blockAck)) { auto agreement = getAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); - if (agreement) { + if (agreement && !agreement->isInactivityExpired()) { agreement->setStartingSequenceNumber(basicBlockAck->getStartingSequenceNumber()); agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); @@ -157,8 +158,8 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const PtrscheduleInactivityTimer(earliestExpirationTime); + if (callback != nullptr) + callback->scheduleInactivityTimer(BlockAckAgreementRole::ORIGINATOR, earliestExpirationTime); } void OriginatorBlockAckAgreementHandler::scheduleAddbaResponseTimer(IBlockAckAgreementHandlerCallback *callback) @@ -362,6 +363,7 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: return nullptr; bool cancelPendingTransaction = agreement->isPending(); std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); pendingTeardownTransactionIds[std::make_pair(delba->getReceiverAddress(), delba->getTid())] = generationId; scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) @@ -373,6 +375,7 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) callback->cancelAddbaTransaction(transactionId, nullptr); @@ -396,22 +399,34 @@ bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet return true; } -bool OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +OriginatorBlockAckAgreementAbortResult OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator()) - return false; + return {}; auto agreementTag = packet->findTag(); if (agreementTag != nullptr) { - auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + auto it = pendingTeardownTransactionIds.find(agreementId); if (it != pendingTeardownTransactionIds.end() && it->second == agreementTag->getGenerationId()) { auto transactionId = it->second; pendingTeardownTransactionIds.erase(it); - callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); - return true; + if (callback != nullptr) + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); + return { true, nullptr }; + } + auto agreement = getAgreement(delba->getReceiverAddress(), delba->getTid()); + if (agreement != nullptr && agreement->getTransactionId() == agreementTag->getGenerationId() && agreement->isInactivityExpired()) { + OriginatorBlockAckAgreementAbortResult result; + result.handled = true; + result.terminatedAgreement.reset(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); + if (callback != nullptr) + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId(), packet); + return result; } } - return false; + return {}; } std::unique_ptr OriginatorBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) @@ -426,6 +441,7 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: if (pendingTeardownIt != pendingTeardownTransactionIds.end()) pendingTeardownTransactionIds.erase(pendingTeardownIt); std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + scheduleInactivityTimer(callback); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) callback->cancelAddbaTransaction(transactionId, nullptr); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 252c30bff02..4b155bbbe34 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -53,7 +53,7 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; - virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index cb7932399bb..85ef1f9e613 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -22,6 +22,9 @@ class INET_API RecipientBlockAckAgreement : public cObject int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; simtime_t expirationTime = -1; + // The agreement stays installed until the timeout DELBA is transmitted; + // prevent that pending teardown from being re-armed by late activity. + bool inactivityExpired = false; uint64_t generationId = 0; public: @@ -36,8 +39,13 @@ class INET_API RecipientBlockAckAgreement : public cObject virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } virtual uint64_t getGenerationId() const { return generationId; } - virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } + virtual void calculateExpirationTime() { + if (!inactivityExpired) + expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; + } virtual simtime_t getExpirationTime() { return expirationTime; } + virtual bool isInactivityExpired() const { return inactivityExpired; } + virtual void markInactivityExpired() { inactivityExpired = true; expirationTime = SIMTIME_MAX; } friend std::ostream& operator<<(std::ostream& os, const RecipientBlockAckAgreement& agreement); }; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 0f5eb2ab66b..5648b95a90a 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -19,7 +19,8 @@ simtime_t RecipientBlockAckAgreementHandler::computeEarliestExpirationTime() simtime_t earliestTime = SIMTIME_MAX; for (auto id : blockAckAgreements) { auto agreement = id.second; - earliestTime = std::min(earliestTime, agreement->getExpirationTime()); + if (!agreement->isInactivityExpired()) + earliestTime = std::min(earliestTime, agreement->getExpirationTime()); } return earliestTime; } @@ -27,8 +28,8 @@ simtime_t RecipientBlockAckAgreementHandler::computeEarliestExpirationTime() void RecipientBlockAckAgreementHandler::scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback) { simtime_t earliestExpirationTime = computeEarliestExpirationTime(); - if (earliestExpirationTime != SIMTIME_MAX) - callback->scheduleInactivityTimer(earliestExpirationTime); + if (callback != nullptr) + callback->scheduleInactivityTimer(BlockAckAgreementRole::RECIPIENT, earliestExpirationTime); } // The inactivity timer at a recipient is reset when MPDUs corresponding to the TID for which the Block Ack @@ -41,8 +42,21 @@ void RecipientBlockAckAgreementHandler::qosFrameReceived(const PtrgetTid(); MacAddress originatorAddr = qosHeader->getTransmitterAddress(); auto agreement = getAgreement(tid, originatorAddr); - if (agreement) + if (agreement && !agreement->isInactivityExpired()) { + agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); + } + } +} + +// IEEE Std 802.11-2024, 11.5.4: a Basic BlockAckReq for an agreement's TID +// also resets the recipient inactivity timer. +void RecipientBlockAckAgreementHandler::blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) +{ + auto agreement = getAgreement(blockAckReq->getTidInfo(), blockAckReq->getTransmitterAddress()); + if (agreement != nullptr && !agreement->isInactivityExpired()) { + agreement->calculateExpirationTime(); + scheduleInactivityTimer(callback); } } @@ -55,7 +69,8 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb simtime_t now = simTime(); for (auto id : blockAckAgreements) { auto agreement = id.second; - if (agreement->getExpirationTime() == now) { + if (!agreement->isInactivityExpired() && agreement->getExpirationTime() <= now) { + agreement->markInactivityExpired(); MacAddress receiverAddr = id.first.first; Tid tid = id.first.second; const auto& delba = buildDelba(receiverAddr, tid, 39); @@ -203,7 +218,7 @@ bool RecipientBlockAckAgreementHandler::isDelbaPending(const Packet *packet, con return teardownIt != pendingTeardownGenerationIds.end() && teardownIt->second == agreementTag->getGenerationId(); } -std::unique_ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(Packet *packet) +std::unique_ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { auto delba = findFragmentedActionContext(packet); if (delba->getInitiator()) @@ -220,6 +235,7 @@ std::unique_ptr RecipientBlockAckAgreementHandler::p if (agreement->getGenerationId() != agreementTag->getGenerationId()) return nullptr; auto terminatedAgreement = std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); pendingTeardownGenerationIds[agreementId] = agreementTag->getGenerationId(); return terminatedAgreement; } @@ -228,7 +244,9 @@ std::unique_ptr RecipientBlockAckAgreementHandler::p return nullptr; return nullptr; } - return std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + auto terminatedAgreement = std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); + return terminatedAgreement; } bool RecipientBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) @@ -250,23 +268,34 @@ bool RecipientBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, return true; } -bool RecipientBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +RecipientBlockAckAgreementAbortResult RecipientBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { auto delba = findFragmentedActionContext(packet); if (delba->getInitiator()) - return false; + return {}; auto agreementTag = packet->findTag(); if (agreementTag == nullptr) - return false; + return {}; auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); auto it = pendingTeardownGenerationIds.find(agreementId); - if (it == pendingTeardownGenerationIds.end() || it->second != agreementTag->getGenerationId()) - return false; - auto generationId = it->second; - pendingTeardownGenerationIds.erase(it); - if (callback != nullptr) - callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), generationId, packet); - return true; + if (it != pendingTeardownGenerationIds.end() && it->second == agreementTag->getGenerationId()) { + auto generationId = it->second; + pendingTeardownGenerationIds.erase(it); + if (callback != nullptr) + callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), generationId, packet); + return { true, nullptr }; + } + auto agreement = getAgreement(delba->getTid(), delba->getReceiverAddress()); + if (agreement != nullptr && agreement->getGenerationId() == agreementTag->getGenerationId() && agreement->isInactivityExpired()) { + RecipientBlockAckAgreementAbortResult result; + result.handled = true; + result.terminatedAgreement.reset(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); + if (callback != nullptr) + callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId(), packet); + return result; + } + return {}; } uint64_t RecipientBlockAckAgreementHandler::getPendingTeardownGenerationId(Tid tid, MacAddress originatorAddr) const @@ -275,7 +304,7 @@ uint64_t RecipientBlockAckAgreementHandler::getPendingTeardownGenerationId(Tid t return it == pendingTeardownGenerationIds.end() ? 0 : it->second; } -std::unique_ptr RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) +std::unique_ptr RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) { if (blockAckAgreementPolicy->isDelbaAccepted(delba)) { auto agreementId = std::make_pair(delba->getTransmitterAddress(), delba->getTid()); @@ -283,6 +312,7 @@ std::unique_ptr RecipientBlockAckAgreementHandler::p if (pendingTeardownIt != pendingTeardownGenerationIds.end()) pendingTeardownGenerationIds.erase(pendingTeardownIt); std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + scheduleInactivityTimer(callback); return terminatedAgreement; } return nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index 6d3af3e1d30..da90268c368 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -44,11 +44,12 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre virtual ~RecipientBlockAckAgreementHandler(); virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) override; - virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) override; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback = nullptr) override; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) override; - virtual std::unique_ptr processTransmittedDelba(Packet *packet) override; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback = nullptr) override; virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; - virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index d130e3cda52..cd2a3859699 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -18,12 +18,20 @@ class Packet; namespace ieee80211 { +enum class BlockAckAgreementRole +{ + ORIGINATOR, + RECIPIENT, +}; + class INET_API IBlockAckAgreementHandlerCallback { public: virtual ~IBlockAckAgreementHandlerCallback() {} - virtual void scheduleInactivityTimer(simtime_t timeout) = 0; + // The deadline is an absolute simulation timestamp. The role identifies + // which half of the shared HCF inactivity timer supplied the deadline. + virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) = 0; virtual void scheduleAddbaResponseTimer(simtime_t deadline) = 0; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) = 0; // Removes queued siblings of a sender-local DELBA without assuming that diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h index fbe0918749a..9a1a2af753e 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h @@ -32,6 +32,14 @@ struct INET_API OriginatorBlockAckAgreementResponse uint64_t teardownTransactionId = 0; }; +struct INET_API OriginatorBlockAckAgreementAbortResult +{ + bool handled = false; + std::unique_ptr terminatedAgreement; + + explicit operator bool() const { return handled; } +}; + class INET_API IOriginatorBlockAckAgreementHandler { public: @@ -49,7 +57,7 @@ class INET_API IOriginatorBlockAckAgreementHandler // Returns true when the packet completed or aborted its tagged teardown // transaction and sibling packets were cancelled through the callback. virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index f0b969353d9..31f6b478078 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -24,6 +24,14 @@ class Packet; namespace ieee80211 { +struct INET_API RecipientBlockAckAgreementAbortResult +{ + bool handled = false; + std::unique_ptr terminatedAgreement; + + explicit operator bool() const { return handled; } +}; + class INET_API IRecipientBlockAckAgreementHandler { public: @@ -31,11 +39,12 @@ class INET_API IRecipientBlockAckAgreementHandler virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) = 0; - virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; - virtual std::unique_ptr processTransmittedDelba(Packet *packet) = 0; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback = nullptr) = 0; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback = nullptr) = 0; virtual bool processAcknowledgedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return false; } - virtual bool processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return false; } + virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return {}; } virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 63d1fedcd6d..0a36f69d9e8 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -9,6 +9,8 @@ #include "inet/queueing/contract/PacketQueueRemovalDetails.h" +#include + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" @@ -158,7 +160,24 @@ bool Hcf::processDroppedBlockAckTeardownFrame(Packet *packet) auto agreementTag = packet->findTag(); if (agreementTag != nullptr && blockAckTeardownsBeingCancelled.find(std::make_tuple(delba->getInitiator(), delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId())) != blockAckTeardownsBeingCancelled.end()) return false; - bool aborted = delba->getInitiator() ? originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this) : recipientBlockAckAgreementHandler != nullptr && recipientBlockAckAgreementHandler->processAbortedDelba(packet, this); + bool aborted = false; + if (delba->getInitiator()) { + if (originatorBlockAckAgreementHandler != nullptr) { + auto result = originatorBlockAckAgreementHandler->processAbortedDelba(packet, this); + aborted = result.handled; + if (result.terminatedAgreement != nullptr && result.terminatedAgreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, result.terminatedAgreement.get()); + } + } + else if (recipientBlockAckAgreementHandler != nullptr) { + auto result = recipientBlockAckAgreementHandler->processAbortedDelba(packet, this); + aborted = result.handled; + if (result.terminatedAgreement != nullptr) { + if (recipientDataService != nullptr) + recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getReceiverAddress()); + emit(blockAckAgreementDeletedSignal, result.terminatedAgreement.get()); + } + } if (aborted) { rebuildPendingFrameEligibility(); return true; @@ -433,10 +452,20 @@ void Hcf::scheduleStartRxTimer(simtime_t timeout) scheduleAfter(timeout, startRxTimer); } -void Hcf::scheduleInactivityTimer(simtime_t timeout) +void Hcf::scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) { Enter_Method("scheduleInactivityTimer"); - rescheduleAfter(timeout, inactivityTimer); + if (role == BlockAckAgreementRole::ORIGINATOR) + originatorInactivityDeadline = deadline; + else + recipientInactivityDeadline = deadline; + auto earliestDeadline = std::min(originatorInactivityDeadline, recipientInactivityDeadline); + if (earliestDeadline == SIMTIME_MAX) { + if (inactivityTimer->isScheduled()) + cancelEvent(inactivityTimer); + } + else + rescheduleAt(std::max(earliestDeadline, simTime()), inactivityTimer); } void Hcf::scheduleAddbaResponseTimer(simtime_t deadline) @@ -750,6 +779,8 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr(header)) ctsProcedure->processReceivedRts(packet, rtsFrame, ctsPolicy, this); else if (auto blockAckRequest = dynamicPtrCast(header)) { + if (recipientBlockAckAgreementHandler) + recipientBlockAckAgreementHandler->blockAckReqReceived(blockAckRequest, this); if (recipientBlockAckProcedure) recipientBlockAckProcedure->processReceivedBlockAckReq(packet, blockAckRequest, recipientAckPolicy, recipientBlockAckAgreementHandler, this); } @@ -808,7 +839,7 @@ void Hcf::recipientProcessReceivedManagementFrame(const PtrgetInitiator()) { auto pendingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); - auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); + auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy, this); auto remainingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); if (agreement != nullptr) { recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getTransmitterAddress()); @@ -991,7 +1022,7 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< } } else { - auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(packet); + auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(packet, this); if (agreement != nullptr) { // IEEE Std 802.11-2024, 10.25.4 and 11.5.3.5: recipient // resources are released whether the recipient transmitted or diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index a446e1bb07d..052524378e3 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -65,6 +65,11 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: cMessage *startRxTimer = nullptr; cMessage *inactivityTimer = nullptr; cMessage *addbaResponseTimer = nullptr; + // The two agreement handlers share one timer but publish independent + // absolute deadlines. Keep both until the handlers explicitly retire + // their role so one role cannot cancel the other's timeout. + simtime_t originatorInactivityDeadline = SIMTIME_MAX; + simtime_t recipientInactivityDeadline = SIMTIME_MAX; // Transmission and Reception IRx *rx = nullptr; @@ -196,7 +201,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override; // IProcedureCallback - virtual void scheduleInactivityTimer(simtime_t timeout) override; + virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) override; virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override; virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override; diff --git a/tests/module/Ieee80211BlockAckInactivityTimer_1.test b/tests/module/Ieee80211BlockAckInactivityTimer_1.test new file mode 100644 index 00000000000..bdd20495297 --- /dev/null +++ b/tests/module/Ieee80211BlockAckInactivityTimer_1.test @@ -0,0 +1,129 @@ +%description: +Exercise the shared HCF Block Ack inactivity timer with independent originator +and recipient deadlines. Verify that the timer keeps the earliest absolute +deadline, preserves the other role when one role is cleared, and is cancelled +only after both roles publish SIMTIME_MAX. + +%file: TestIeee80211BlockAckInactivityTimer.cc + +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" + +namespace inet { +namespace ieee80211 { + +class TestHcfInactivityTimer : public Hcf +{ + public: + void updateInactivityDeadline(BlockAckAgreementRole role, simtime_t deadline) { scheduleInactivityTimer(role, deadline); } + simtime_t getOriginatorDeadline() const { return originatorInactivityDeadline; } + simtime_t getRecipientDeadline() const { return recipientInactivityDeadline; } + bool isInactivityTimerScheduled() const { return inactivityTimer->isScheduled(); } + simtime_t getInactivityTimerArrivalTime() const { return inactivityTimer->getArrivalTime(); } + + protected: + virtual int numInitStages() const override { return 1; } + + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LOCAL) + inactivityTimer = new cMessage("blockAckInactivityTimer"); + } +}; + +Define_Module(TestHcfInactivityTimer); + +class Ieee80211BlockAckInactivityTimerTest : public cSimpleModule +{ + protected: + TestHcfInactivityTimer *hcf = nullptr; + + protected: + virtual void initialize() override + { + hcf = check_and_cast(getModuleByPath("^.hcf")); + scheduleAt(SimTime(1, SIMTIME_US), new cMessage("checkInactivityTimer")); + } + + virtual void handleMessage(cMessage *message) override + { + auto startTime = simTime(); + ASSERT(startTime > 0); + + auto originatorDeadline = startTime + SimTime(2, SIMTIME_US); + auto recipientDeadline = startTime + SimTime(5, SIMTIME_US); + hcf->updateInactivityDeadline(BlockAckAgreementRole::ORIGINATOR, originatorDeadline); + ASSERT(hcf->getOriginatorDeadline() == originatorDeadline); + ASSERT(hcf->getRecipientDeadline() == SIMTIME_MAX); + ASSERT(hcf->isInactivityTimerScheduled()); + ASSERT(hcf->getInactivityTimerArrivalTime() == originatorDeadline); + + // A later recipient deadline must not replace the earlier originator + // deadline in the shared timer. + hcf->updateInactivityDeadline(BlockAckAgreementRole::RECIPIENT, recipientDeadline); + ASSERT(hcf->getRecipientDeadline() == recipientDeadline); + ASSERT(hcf->getInactivityTimerArrivalTime() == originatorDeadline); + + // Clearing one role leaves the other role's deadline armed. + hcf->updateInactivityDeadline(BlockAckAgreementRole::ORIGINATOR, SIMTIME_MAX); + ASSERT(hcf->getOriginatorDeadline() == SIMTIME_MAX); + ASSERT(hcf->getInactivityTimerArrivalTime() == recipientDeadline); + hcf->updateInactivityDeadline(BlockAckAgreementRole::RECIPIENT, SIMTIME_MAX); + ASSERT(hcf->getRecipientDeadline() == SIMTIME_MAX); + ASSERT(!hcf->isInactivityTimerScheduled()); + + // The reverse update order still replaces the shared timer with an + // earlier deadline and preserves the later role when it is cleared. + auto reverseRecipientDeadline = startTime + SimTime(8, SIMTIME_US); + auto reverseOriginatorDeadline = startTime + SimTime(1, SIMTIME_US); + hcf->updateInactivityDeadline(BlockAckAgreementRole::RECIPIENT, reverseRecipientDeadline); + ASSERT(hcf->getInactivityTimerArrivalTime() == reverseRecipientDeadline); + hcf->updateInactivityDeadline(BlockAckAgreementRole::ORIGINATOR, reverseOriginatorDeadline); + ASSERT(hcf->getInactivityTimerArrivalTime() == reverseOriginatorDeadline); + hcf->updateInactivityDeadline(BlockAckAgreementRole::ORIGINATOR, SIMTIME_MAX); + ASSERT(hcf->getInactivityTimerArrivalTime() == reverseRecipientDeadline); + hcf->updateInactivityDeadline(BlockAckAgreementRole::RECIPIENT, SIMTIME_MAX); + ASSERT(!hcf->isInactivityTimerScheduled()); + + delete message; + endSimulation(); + } +}; + +Define_Module(Ieee80211BlockAckInactivityTimerTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; + +simple TestHcfInactivityTimer +{ + parameters: + @class(::inet::ieee80211::TestHcfInactivityTimer); +} + +simple Ieee80211BlockAckInactivityTimerTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211BlockAckInactivityTimerTest); +} + +network Ieee80211BlockAckInactivityTimerTestNetwork +{ + submodules: + hcf: TestHcfInactivityTimer; + test: Ieee80211BlockAckInactivityTimerTest; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211BlockAckInactivityTimerTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 10us +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 0523c2f0dad..8c8a9818e03 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -106,6 +106,9 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler std::vector managementPackets; Ptr managementHeader; simtime_t addbaDeadline = SIMTIME_MAX; + simtime_t originatorInactivityDeadline = SIMTIME_MAX; + simtime_t recipientInactivityDeadline = SIMTIME_MAX; + std::vector> inactivityDeadlineUpdates; std::vector cancelledTransactionIds; std::vector excludedPackets; std::vector cancelledTeardownGenerationIds; @@ -120,7 +123,13 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler managementPackets.push_back(packet); managementHeader = header; } - virtual void scheduleInactivityTimer(simtime_t) override {} + virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) override { + inactivityDeadlineUpdates.emplace_back(role, deadline); + if (role == BlockAckAgreementRole::ORIGINATOR) + originatorInactivityDeadline = deadline; + else + recipientInactivityDeadline = deadline; + } virtual void scheduleAddbaResponseTimer(simtime_t deadline) override { addbaDeadline = deadline; } virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { cancelledTransactionIds.push_back(transactionId); @@ -140,6 +149,8 @@ class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler void addEstablishedAgreement(MacAddress receiverAddress, Tid tid) { auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(0), 64, true, false, 1, nextTransactionId++); agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(0); + agreement->calculateExpirationTime(); blockAckAgreements[std::make_pair(receiverAddress, tid)] = agreement; } void setNextDialogToken(uint8_t token) { nextDialogToken = token; } @@ -147,6 +158,7 @@ class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler void allowRetryNow(MacAddress receiverAddress, Tid tid) { addbaRetryDeadlines[std::make_pair(receiverAddress, tid)] = simTime(); } void setPendingTeardownTransaction(MacAddress receiverAddress, Tid tid, uint64_t transactionId) { pendingTeardownTransactionIds[std::make_pair(receiverAddress, tid)] = transactionId; } simtime_t getRetryDeadline(MacAddress receiverAddress, Tid tid) const { auto it = addbaRetryDeadlines.find(std::make_pair(receiverAddress, tid)); return it == addbaRetryDeadlines.end() ? SIMTIME_MAX : it->second; } + void publishInactivityDeadline(IBlockAckAgreementHandlerCallback *callback) { scheduleInactivityTimer(callback); } }; class TestTimeoutCancellationCallback : public TestCallback @@ -647,6 +659,7 @@ class TestHcf : public Hcf bool cancelManagement(uint64_t transactionId, Packet *excludedPacket = nullptr) { return cancelManagementTransaction(transactionId, excludedPacket); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } void cancelTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket = nullptr) { cancelBlockAckTeardown(initiator, peerAddress, tid, generationId, excludedPacket); } + bool processDroppedTeardown(Packet *packet) { return processDroppedBlockAckTeardownFrame(packet); } void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } void processReceivedFrame(Packet *packet) { recipientProcessReceivedFrame(packet, packet->peekAtFront()); } void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } @@ -673,7 +686,7 @@ class TestHcf : public Hcf virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override { numCancelledBlockAckTeardowns++; cancelledBlockAckTeardownGenerationIds.push_back(generationId); Hcf::cancelBlockAckTeardown(initiator, peerAddress, tid, generationId, excludedPacket); } virtual void rebuildPendingFrameEligibility() override { numRebuildEligibilityCalls++; Hcf::rebuildPendingFrameEligibility(); } virtual void resumeEligibleChannelAccess() override { numResumedEligibleChannelAccess++; } - virtual void scheduleInactivityTimer(simtime_t) override {} + virtual void scheduleInactivityTimer(BlockAckAgreementRole, simtime_t) override {} virtual void scheduleAddbaResponseTimer(simtime_t) override {} }; @@ -4918,6 +4931,168 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { delete currentPacket; } +// Inactivity deadlines are absolute simulation times, and expiry is +// one-shot while the tagged DELBA waits in the local transmission queues. +// IEEE Std 802.11-2024, 11.5.4 and 10.25.4. +{ + wait(1); + TestRecipientHandler recipientHandler; + TestRecipientPolicy recipientPolicy; + recipientPolicy.blockAckTimeoutValue = 3; + TestCallback setupCallback; + auto request = makeRequest(peer2, 12, 109, SequenceNumberCyclic(330), 32); + request->setBlockAckTimeoutValue(3); + auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &setupCallback, &setupCallback); + ASSERT(agreement != nullptr); + auto creationTime = simTime(); + ASSERT(creationTime > 0); + ASSERT(agreement->getExpirationTime() == creationTime + 3); + ASSERT(setupCallback.recipientInactivityDeadline == creationTime + 3); + + auto basicBlockAckReq = makeShared(); + basicBlockAckReq->setTransmitterAddress(peer2); + basicBlockAckReq->setTidInfo(12); + wait(1); + auto barReceptionTime = simTime(); + recipientHandler.blockAckReqReceived(basicBlockAckReq, &setupCallback); + ASSERT(agreement->getExpirationTime() == barReceptionTime + 3); + ASSERT(agreement->getExpirationTime() > creationTime + 3); + ASSERT(setupCallback.recipientInactivityDeadline == barReceptionTime + 3); + + wait(4); + TestCallback expiryCallback; + recipientHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + ASSERT(recipientHandler.getAgreement(12, peer2) == agreement); + ASSERT(agreement->isInactivityExpired()); + ASSERT(expiryCallback.managementPackets.size() == 1); + ASSERT(expiryCallback.recipientInactivityDeadline == SIMTIME_MAX); + + // The same overdue agreement is not emitted a second time, and a late + // data frame cannot re-arm its inactivity deadline. + recipientHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + ASSERT(expiryCallback.managementPackets.size() == 1); + auto lateQosHeader = makeQosHeader(peer2, 12, SequenceNumberCyclic(331)); + lateQosHeader->setTransmitterAddress(peer2); + lateQosHeader->setAckPolicy(AckPolicy::BLOCK_ACK); + recipientHandler.qosFrameReceived(lateQosHeader, &expiryCallback); + ASSERT(expiryCallback.recipientInactivityDeadline == SIMTIME_MAX); + TestCallback lateBarCallback; + recipientHandler.blockAckReqReceived(basicBlockAckReq, &lateBarCallback); + ASSERT(lateBarCallback.recipientInactivityDeadline == SIMTIME_MAX); + + // The originator uses the same absolute-deadline and one-shot rules. + TestOriginatorHandler originatorHandler; + originatorHandler.addEstablishedAgreement(peer1, 13); + auto originatorAgreement = originatorHandler.getAgreement(peer1, 13); + originatorAgreement->setBlockAckTimeoutValue(2); + originatorAgreement->calculateExpirationTime(); + auto originatorCreationTime = simTime(); + TestCallback originatorCallback; + originatorHandler.publishInactivityDeadline(&originatorCallback); + ASSERT(originatorCallback.originatorInactivityDeadline == originatorCreationTime + 2); + wait(3); + originatorHandler.blockAckAgreementExpired(&originatorCallback, &originatorCallback); + ASSERT(originatorHandler.getAgreement(peer1, 13) == originatorAgreement); + ASSERT(originatorAgreement->isInactivityExpired()); + ASSERT(originatorCallback.managementPackets.size() == 1); + ASSERT(originatorCallback.originatorInactivityDeadline == SIMTIME_MAX); + originatorHandler.blockAckAgreementExpired(&originatorCallback, &originatorCallback); + ASSERT(originatorCallback.managementPackets.size() == 1); + + // A role with no finite deadline publishes SIMTIME_MAX independently; + // Hcf can therefore retain a finite deadline from the other role. + TestOriginatorHandler noDeadlineOriginatorHandler; + noDeadlineOriginatorHandler.addEstablishedAgreement(peer1, 14); + TestCallback roleCallback; + roleCallback.scheduleInactivityTimer(BlockAckAgreementRole::RECIPIENT, simTime() + 10); + noDeadlineOriginatorHandler.publishInactivityDeadline(&roleCallback); + ASSERT(roleCallback.originatorInactivityDeadline == SIMTIME_MAX); + ASSERT(roleCallback.recipientInactivityDeadline == simTime() + 10); + roleCallback.scheduleInactivityTimer(BlockAckAgreementRole::RECIPIENT, SIMTIME_MAX); + ASSERT(roleCallback.recipientInactivityDeadline == SIMTIME_MAX); + +} + +// A timeout DELBA can be aborted before its final fragment is transmitted. +// The matching generation is retired locally, and the returned ownership lets +// HCF release role-specific resources without rearming a timed-out agreement. +{ + TestOriginatorHandler originatorHandler; + originatorHandler.addEstablishedAgreement(peer1, 15); + auto originatorAgreement = originatorHandler.getAgreement(peer1, 15); + originatorAgreement->setBlockAckTimeoutValue(2); + originatorAgreement->calculateExpirationTime(); + originatorAgreement->markInactivityExpired(); + auto originatorGenerationId = originatorAgreement->getTransactionId(); + auto originatorFirstFragment = makeTaggedDelbaPacket("originatorExpiredDelbaFirst", peer1, 15, SequenceNumberCyclic(340), 0, true, originatorGenerationId); + auto originatorFinalFragment = makeTaggedDelbaPacket("originatorExpiredDelbaFinal", peer1, 15, SequenceNumberCyclic(340), 1, false, originatorGenerationId); + TestCallback originatorAbortCallback; + ASSERT(originatorHandler.processTransmittedDelba(originatorFirstFragment, &originatorAbortCallback) == nullptr); + auto originatorAbortResult = originatorHandler.processAbortedDelba(originatorFinalFragment, &originatorAbortCallback); + ASSERT(originatorAbortResult.handled); + ASSERT(originatorAbortResult.terminatedAgreement != nullptr); + ASSERT(originatorAbortResult.terminatedAgreement->getTransactionId() == originatorGenerationId); + ASSERT(originatorHandler.getAgreement(peer1, 15) == nullptr); + ASSERT(originatorAbortCallback.originatorInactivityDeadline == SIMTIME_MAX); + ASSERT(originatorAbortCallback.cancelledTeardownGenerationIds.back() == originatorGenerationId); + auto staleOriginatorAbortResult = originatorHandler.processAbortedDelba(originatorFinalFragment, &originatorAbortCallback); + ASSERT(!staleOriginatorAbortResult.handled); + ASSERT(staleOriginatorAbortResult.terminatedAgreement == nullptr); + // Retiring the agreement is terminal for this generation: a late BlockAck + // must not resurrect it or publish a new inactivity deadline. + auto lateOriginatorBlockAck = makeShared(); + lateOriginatorBlockAck->setTransmitterAddress(peer1); + lateOriginatorBlockAck->setTidInfo(15); + lateOriginatorBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(340)); + TestCallback lateOriginatorActivityCallback; + originatorHandler.processReceivedBlockAck(lateOriginatorBlockAck, &lateOriginatorActivityCallback); + ASSERT(originatorHandler.getAgreement(peer1, 15) == nullptr); + ASSERT(lateOriginatorActivityCallback.originatorInactivityDeadline == SIMTIME_MAX); + ASSERT(lateOriginatorActivityCallback.inactivityDeadlineUpdates.empty()); + delete originatorFirstFragment; + delete originatorFinalFragment; + + TestRecipientHandler recipientHandler; + TestRecipientPolicy recipientPolicy; + recipientPolicy.blockAckTimeoutValue = 2; + TestCallback recipientSetupCallback; + auto recipientRequest = makeRequest(peer2, 16, 110, SequenceNumberCyclic(341), 32); + recipientRequest->setBlockAckTimeoutValue(2); + auto recipientAgreement = recipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientSetupCallback, &recipientSetupCallback); + ASSERT(recipientAgreement != nullptr); + recipientAgreement->markInactivityExpired(); + auto recipientGenerationId = recipientAgreement->getGenerationId(); + auto recipientFirstFragment = makeTaggedRecipientDelbaPacket("recipientExpiredDelbaFirst", peer2, 16, SequenceNumberCyclic(342), 0, true, recipientGenerationId); + auto recipientFinalFragment = makeTaggedRecipientDelbaPacket("recipientExpiredDelbaFinal", peer2, 16, SequenceNumberCyclic(342), 1, false, recipientGenerationId); + TestCallback recipientAbortCallback; + ASSERT(recipientHandler.processTransmittedDelba(recipientFirstFragment, &recipientAbortCallback) == nullptr); + auto recipientAbortResult = recipientHandler.processAbortedDelba(recipientFinalFragment, &recipientAbortCallback); + ASSERT(recipientAbortResult.handled); + ASSERT(recipientAbortResult.terminatedAgreement != nullptr); + ASSERT(recipientAbortResult.terminatedAgreement->getGenerationId() == recipientGenerationId); + ASSERT(recipientHandler.getAgreement(16, peer2) == nullptr); + ASSERT(recipientAbortCallback.recipientInactivityDeadline == SIMTIME_MAX); + ASSERT(recipientAbortCallback.cancelledTeardownGenerationIds.back() == recipientGenerationId); + auto staleRecipientAbortResult = recipientHandler.processAbortedDelba(recipientFinalFragment, &recipientAbortCallback); + ASSERT(!staleRecipientAbortResult.handled); + ASSERT(staleRecipientAbortResult.terminatedAgreement == nullptr); + // Likewise, late recipient traffic cannot recreate a retired agreement. + auto lateRecipientQosHeader = makeQosHeader(peer1, 16, SequenceNumberCyclic(343)); + lateRecipientQosHeader->setTransmitterAddress(peer2); + lateRecipientQosHeader->setAckPolicy(AckPolicy::BLOCK_ACK); + auto lateRecipientBlockAckReq = makeShared(); + lateRecipientBlockAckReq->setTransmitterAddress(peer2); + lateRecipientBlockAckReq->setTidInfo(16); + TestCallback lateRecipientActivityCallback; + recipientHandler.qosFrameReceived(lateRecipientQosHeader, &lateRecipientActivityCallback); + recipientHandler.blockAckReqReceived(lateRecipientBlockAckReq, &lateRecipientActivityCallback); + ASSERT(recipientHandler.getAgreement(16, peer2) == nullptr); + ASSERT(lateRecipientActivityCallback.recipientInactivityDeadline == SIMTIME_MAX); + ASSERT(lateRecipientActivityCallback.inactivityDeadlineUpdates.empty()); + delete recipientFirstFragment; + delete recipientFinalFragment; +} + // HCF routes recipient DELBAs to the recipient owner and rebuilds its // eligibility index when replacement or final teardown changes the generation. // The replacement callback removes queued stale siblings; the active current @@ -4958,6 +5133,40 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { hcf.configureManagementCallback(&managementCallback); hcf.observePendingQueue(&pendingQueue); + // HCF owns the terminal cleanup when a fragmented timeout DELBA is + // aborted before its final fragment is transmitted. The handler returns + // the retired agreement, allowing HCF to emit the deletion signal and to + // release recipient reorder resources exactly once. + TestSignalListener expiredDeletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &expiredDeletionListener); + originatorHandler->addEstablishedAgreement(peer1, 15); + auto expiredOriginatorAgreement = originatorHandler->getAgreement(peer1, 15); + expiredOriginatorAgreement->markInactivityExpired(); + auto expiredOriginatorGenerationId = expiredOriginatorAgreement->getTransactionId(); + auto expiredOriginatorDelba = makeTaggedDelbaPacket("hcfExpiredOriginatorDelba", peer1, 15, SequenceNumberCyclic(324), 1, false, expiredOriginatorGenerationId); + ASSERT(hcf.processDroppedTeardown(expiredOriginatorDelba)); + ASSERT(originatorHandler->getAgreement(peer1, 15) == nullptr); + delete expiredOriginatorDelba; + + auto expiredRecipientRequest = makeRequest(peer2, 15, 105, SequenceNumberCyclic(325), 32); + auto expiredRecipientAgreement = recipientHandler->processReceivedAddbaRequest(expiredRecipientRequest, &hcfRecipientPolicy, &managementCallback, &managementCallback); + ASSERT(expiredRecipientAgreement != nullptr); + expiredRecipientAgreement->markInactivityExpired(); + auto expiredRecipientGenerationId = expiredRecipientAgreement->getGenerationId(); + auto expiredRecipientDelba = makeTaggedRecipientDelbaPacket("hcfExpiredRecipientDelba", peer2, 15, SequenceNumberCyclic(326), 1, false, expiredRecipientGenerationId); + ASSERT(hcf.processDroppedTeardown(expiredRecipientDelba)); + ASSERT(recipientHandler->getAgreement(15, peer2) == nullptr); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(expiredDeletionListener.numSignals == 2); + delete expiredRecipientDelba; + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &expiredDeletionListener); + // Keep the following generation-lifecycle assertions focused on their + // own transactions rather than on the setup cleanup above. + hcf.numCancelledBlockAckTeardowns = 0; + hcf.cancelledBlockAckTeardownGenerationIds.clear(); + hcf.numRebuildEligibilityCalls = 0; + recipientDataService.numReorderingResets = 0; + auto stalePacket = makeTaggedRecipientDelbaPacket("hcfStaleRecipientDelba", peer2, 11, SequenceNumberCyclic(321), 0, false, firstGenerationId); pendingQueue.enqueuePacket(stalePacket); hcf.trackFrame(stalePacket, AC_BE); From d8bc79ba8d3a86b62a4f2bfcb0243b808fc0b069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:04 +0200 Subject: [PATCH 09/16] ieee80211: make fragment reassembly generation-safe Replace the fragment-zero reset heuristic with generation-aware reassembly state. Track extended sequence generations per receive flow, preserve later-only fragments, and sort completed fragments by fragment number instead of arrival order. Quarantine half-space ambiguity, retain tombstones for retired sequences, recover when a raw sequence number is reused after wrap, and reject completion when contradictory terminal fragment numbers were observed. Stale fragments therefore cannot corrupt a newer MSDU. Cover out-of-order delivery, duplicates, ambiguous generations, sequence-number wrap, delayed stale fragments, and contradictory terminal markers. --- .../blockackreordering/BlockAckReordering.cc | 19 +- .../blockackreordering/BlockAckReordering.h | 6 + .../mac/fragmentation/BasicReassembly.cc | 274 +++++++++-- .../mac/fragmentation/BasicReassembly.h | 51 +- tests/unit/Ieee80211AddbaTransaction_1.test | 434 +++++++++++++++++- 5 files changed, 736 insertions(+), 48 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 1dbff9f3f17..c70bfcb570b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -7,11 +7,24 @@ #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" +#include + #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" namespace inet { namespace ieee80211 { +BlockAckReordering::Fragments BlockAckReordering::sortFragmentsByFragmentNumber(const Fragments& fragments) +{ + auto sortedFragments = fragments; + std::stable_sort(sortedFragments.begin(), sortedFragments.end(), [](const Packet *first, const Packet *second) { + auto firstFragmentNumber = first->peekAtFront()->getFragmentNumber(); + auto secondFragmentNumber = second->peekAtFront()->getFragmentNumber(); + return firstFragmentNumber < secondFragmentNumber; + }); + return sortedFragments; +} + // // The recipient flushes received MSDUs from its receive buffer as described in this subclause. [...] // @@ -113,7 +126,7 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::collectCompletePrecedingMp auto fragments = it.second; if (SequenceNumberCyclic(sequenceNumber) < startingSequenceNumber) if (isComplete(fragments)) - completePrecedingMpdus[sequenceNumber] = fragments; + completePrecedingMpdus[sequenceNumber] = sortFragmentsByFragmentNumber(fragments); } return completePrecedingMpdus; } @@ -140,7 +153,7 @@ bool BlockAckReordering::addMsduIfComplete(ReceiveBuffer *receiveBuffer, Reorder if (it != buffer.end()) { auto fragments = it->second; if (isComplete(fragments)) { - reorderBuffer[seqNum.get()] = fragments; + reorderBuffer[seqNum.get()] = sortFragmentsByFragmentNumber(fragments); return true; } } @@ -232,7 +245,7 @@ std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists } } } - return earliestFragments; + return sortFragmentsByFragmentNumber(earliestFragments); } BlockAckReordering::~BlockAckReordering() diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index 014f2c0a318..5e02a452d44 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -26,10 +26,16 @@ class INET_API BlockAckReordering typedef std::vector Fragments; typedef std::map ReorderBuffer; + // Values in ReorderBuffer results from processReceivedQoSFrame() and + // processReceivedBlockAckReq() contain complete fragment vectors ordered + // by ascending Fragment Number. The vectors only reorder packet pointers; + // packet ownership remains with the caller of those methods. + protected: std::map, ReceiveBuffer *> receiveBuffers; protected: + static Fragments sortFragmentsByFragmentNumber(const Fragments& fragments); ReorderBuffer collectCompletePrecedingMpdus(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); ReorderBuffer collectConsecutiveCompleteFollowingMpdus(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc index 0b000dab927..23986c4361e 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc @@ -17,40 +17,202 @@ namespace ieee80211 { Register_Class(BasicReassembly); -/* - * FIXME this function needs a serious review - */ +BasicReassembly::SequenceSpaceKey BasicReassembly::getSequenceSpaceKey(const ContextKey& contextKey) const +{ + // IEEE Std 802.11-2024, Table 10-5 defines one baseline sequence-number + // space for frames not covered by a more specific space, and a separate + // space for individually addressed QoS Data indexed by RA and TID. + if (contextKey.type == ST_DATA_WITH_QOS && !contextKey.receiverAddress.isMulticast()) + return { contextKey.macAddress, contextKey.receiverAddress, contextKey.tid }; + else + return { contextKey.macAddress, MacAddress::UNSPECIFIED_ADDRESS, -1 }; +} + +SequenceNumber BasicReassembly::getRawSequenceNumber(ExtendedSequenceNumber extendedSequenceNumber) +{ + auto rawSequenceNumber = extendedSequenceNumber % NUM_SEQUENCE_NUMBERS; + if (rawSequenceNumber < 0) + rawSequenceNumber += NUM_SEQUENCE_NUMBERS; + return static_cast(rawSequenceNumber); +} + +BasicReassembly::SequenceObservation BasicReassembly::observeSequenceNumber(const SequenceSpaceKey& sequenceSpaceKey, SequenceNumber sequenceNumber) +{ + auto sequenceSpaceIt = sequenceSpacesMap.find(sequenceSpaceKey); + if (sequenceSpaceIt == sequenceSpacesMap.end()) { + sequenceSpacesMap.emplace(sequenceSpaceKey, SequenceSpaceValue{ sequenceNumber }); + return { sequenceNumber, false }; + } + + auto& highWatermark = sequenceSpaceIt->second.highWatermark; + auto highWaterSequenceNumber = getRawSequenceNumber(highWatermark); + auto highWaterCyclic = SequenceNumberCyclic(highWaterSequenceNumber); + auto sequenceCyclic = SequenceNumberCyclic(sequenceNumber); + + if (sequenceCyclic == highWaterCyclic) + return { highWatermark, false }; + + // SequenceNumberCyclic uses the IEEE 802.11 half-space ordering: a + // sequence number is ahead when its forward distance is in 1..2047. + if (highWaterCyclic < sequenceCyclic) { + auto forwardDistance = (sequenceNumber - highWaterSequenceNumber + NUM_SEQUENCE_NUMBERS) % NUM_SEQUENCE_NUMBERS; + ASSERT(forwardDistance > 0 && forwardDistance < NUM_SEQUENCE_NUMBERS / 2); + highWatermark += forwardDistance; + return { highWatermark, false }; + } + else if (sequenceCyclic < highWaterCyclic) { + auto backwardDistance = (highWaterSequenceNumber - sequenceNumber + NUM_SEQUENCE_NUMBERS) % NUM_SEQUENCE_NUMBERS; + ASSERT(backwardDistance > 0 && backwardDistance < NUM_SEQUENCE_NUMBERS / 2); + return { highWatermark - backwardDistance, false }; + } + + // Exactly half a sequence-number space has no ordering under the cyclic + // comparison. A fragmented MPDU must not be assigned a generation on + // guesswork. An unfragmented MPDU is safe to accept because it is never + // combined with buffered fragments; leave the high-water unchanged. + return { highWatermark, true }; +} + +void BasicReassembly::pruneExpiredSequenceNumbers(const SequenceSpaceKey& sequenceSpaceKey) +{ + auto sequenceSpaceIt = sequenceSpacesMap.find(sequenceSpaceKey); + if (sequenceSpaceIt == sequenceSpacesMap.end()) + return; + auto oldestRetainedGeneration = sequenceSpaceIt->second.highWatermark - NUM_SEQUENCE_NUMBERS; + for (auto contextIt = expiredSequenceNumbersMap.begin(); contextIt != expiredSequenceNumbersMap.end();) { + if (!(getSequenceSpaceKey(contextIt->first) == sequenceSpaceKey)) { + ++contextIt; + continue; + } + auto& expiredSequenceNumbers = contextIt->second; + for (auto sequenceIt = expiredSequenceNumbers.begin(); sequenceIt != expiredSequenceNumbers.end();) { + if (*sequenceIt < oldestRetainedGeneration) + sequenceIt = expiredSequenceNumbers.erase(sequenceIt); + else + ++sequenceIt; + } + if (expiredSequenceNumbers.empty()) + contextIt = expiredSequenceNumbersMap.erase(contextIt); + else + ++contextIt; + } +} + Packet *BasicReassembly::addFragment(Packet *packet) { const auto& header = packet->peekAtFront(); - // Frame is not fragmented - if (!header->getMoreFragments() && header->getFragmentNumber() == 0) - return packet; - // find entry for this frame - Key key; - key.macAddress = header->getTransmitterAddress(); - key.receiverAddress = header->getReceiverAddress(); - key.type = header->getType(); - key.tid = -1; + ContextKey contextKey; + contextKey.macAddress = header->getTransmitterAddress(); + contextKey.receiverAddress = header->getReceiverAddress(); + contextKey.type = header->getType(); + contextKey.tid = -1; if (header->getType() == ST_DATA_WITH_QOS) if (const Ptr& qosDataHeader = dynamicPtrCast(header)) - key.tid = qosDataHeader->getTid(); - key.seqNum = header->getSequenceNumber().get(); + contextKey.tid = qosDataHeader->getTid(); short fragNum = header->getFragmentNumber(); ASSERT(fragNum >= 0 && fragNum < MAX_NUM_FRAGMENTS); + bool isFragmented = header->getMoreFragments() || fragNum != 0; + auto sequenceSpaceKey = getSequenceSpaceKey(contextKey); + // This observed-epoch policy can separate only wraps for which sequence + // progress was received. A completely unseen wrap is indistinguishable + // from the same generation with the MAC metadata available here, so an + // equal raw sequence remains in the current observed epoch to preserve + // valid out-of-order completion. + auto sequenceObservation = observeSequenceNumber(sequenceSpaceKey, header->getSequenceNumber().get()); + + if (sequenceObservation.ambiguous) { + if (isFragmented) { + delete packet; + return nullptr; + } + return packet; + } + + Key key; + key.macAddress = contextKey.macAddress; + key.receiverAddress = contextKey.receiverAddress; + key.type = contextKey.type; + key.tid = contextKey.tid; + key.extendedSequenceNumber = sequenceObservation.extendedSequenceNumber; + auto it = fragmentsMap.find(key); - if (it != fragmentsMap.end() && it->second.expired) { - // A non-Retry fragment 0 can be a new MMPDU after sequence-number - // reuse. All other fragments of the expired MMPDU are discarded. - if (fragNum == 0 && !header->getRetry()) { + + auto expiredIt = expiredSequenceNumbersMap.find(contextKey); + if (isFragmented && expiredIt != expiredSequenceNumbersMap.end() && + expiredIt->second.find(key.extendedSequenceNumber) != expiredIt->second.end()) { + // A tombstone is scoped to the observed extended generation. Keep it + // even when a non-Retry fragment 0 arrives: without an observed + // sequence-space advance that fragment is not distinguishable from a + // late fragment of the expired MPDU. + delete packet; + return nullptr; + } + + // An unfragmented MPDU is never assembled with a fragmented MPDU. A new + // non-Retry frame retires an active same-generation reassembly, while a + // Retry leaves it available for its original fragments. + if (!isFragmented) { + if (it != fragmentsMap.end() && !header->getRetry()) { + for (auto fragment : it->second.fragments) + if (fragment != nullptr) + delete fragment; fragmentsMap.erase(it); - it = fragmentsMap.end(); } - else { - delete packet; - return nullptr; + pruneExpiredSequenceNumbers(sequenceSpaceKey); + return packet; + } + + // A same-raw active or expired entry in another observed epoch makes all + // fragments of the new epoch ambiguous, including fragment 0. Quarantine + // the new extended identity rather than allowing a delayed old fragment + // to seed a hybrid frame. IEEE Std 802.11-2024, 9.2.4.1.6, 10.3.2.14.2, + // and 10.5 + // define the modulo-4096 sequence identity, fragment identity, and + // receive-lifetime discard boundary; the following recovery policy keeps + // the ambiguous generation rejected while allowing a later observed + // generation to make progress. A fragmented non-Retry fragment 0 is the + // only marker that can retire older same-raw tombstones, and only when no + // active same-raw generation remains. A tombstone for the incoming + // generation is never cleared by this path. + bool hasOtherGeneration = false; + for (const auto& entry : fragmentsMap) { + const auto& otherKey = entry.first; + if (otherKey.getContextKey() == contextKey && + getRawSequenceNumber(otherKey.extendedSequenceNumber) == header->getSequenceNumber().get() && + otherKey.extendedSequenceNumber != key.extendedSequenceNumber) { + hasOtherGeneration = true; + break; + } + } + bool isNewGenerationMarker = fragNum == 0 && !header->getRetry(); + if (!hasOtherGeneration && isNewGenerationMarker && expiredIt != expiredSequenceNumbersMap.end()) { + for (auto sequenceIt = expiredIt->second.begin(); sequenceIt != expiredIt->second.end();) { + if (*sequenceIt < key.extendedSequenceNumber && + getRawSequenceNumber(*sequenceIt) == header->getSequenceNumber().get()) + sequenceIt = expiredIt->second.erase(sequenceIt); + else + ++sequenceIt; + } + if (expiredIt->second.empty()) + expiredSequenceNumbersMap.erase(expiredIt); + expiredIt = expiredSequenceNumbersMap.find(contextKey); + } + if (!hasOtherGeneration && expiredIt != expiredSequenceNumbersMap.end()) { + for (auto extendedSequenceNumber : expiredIt->second) { + if (extendedSequenceNumber != key.extendedSequenceNumber && + getRawSequenceNumber(extendedSequenceNumber) == header->getSequenceNumber().get()) { + hasOtherGeneration = true; + break; + } } } + if (hasOtherGeneration) { + expiredSequenceNumbersMap[contextKey].insert(key.extendedSequenceNumber); + pruneExpiredSequenceNumbers(sequenceSpaceKey); + delete packet; + return nullptr; + } + if (it == fragmentsMap.end()) { Value value; value.receptionStartTime = simTime(); @@ -61,18 +223,25 @@ Packet *BasicReassembly::addFragment(Packet *packet) // update entry uint16_t fragmentBit = 1 << fragNum; - value.receivedFragments |= fragmentBit; - if (!header->getMoreFragments()) - value.allFragments = (fragmentBit << 1) - 1; - if (!value.fragments[fragNum]) + if (!header->getMoreFragments()) { + if (value.terminalFragmentNumber == -1) + value.terminalFragmentNumber = fragNum; + else if (value.terminalFragmentNumber != fragNum) + value.hasContradictoryTerminalFragmentNumbers = true; + } + if (!value.fragments[fragNum]) { + value.receivedFragments |= fragmentBit; + if (!header->getMoreFragments() && value.allFragments == 0) + value.allFragments = (fragmentBit << 1) - 1; value.fragments[fragNum] = packet; + } else delete packet; // MacAddress txAddress = header->getTransmitterAddress(); // if all fragments arrived, return assembled frame - if (value.allFragments != 0 && value.allFragments == value.receivedFragments) { + if (!value.hasContradictoryTerminalFragmentNumbers && value.allFragments != 0 && value.allFragments == value.receivedFragments) { Defragmentation defragmentation; value.fragments.erase(std::remove(value.fragments.begin(), value.fragments.end(), nullptr), value.fragments.end()); auto defragmentedFrame = defragmentation.defragmentFrames(&value.fragments); @@ -82,47 +251,55 @@ Packet *BasicReassembly::addFragment(Packet *packet) for (auto fragment : value.fragments) delete fragment; fragmentsMap.erase(key); + pruneExpiredSequenceNumbers(sequenceSpaceKey); return defragmentedFrame; } - else + else { + pruneExpiredSequenceNumbers(sequenceSpaceKey); return nullptr; + } } simtime_t BasicReassembly::getNextExpirationTime() const { simtime_t nextExpirationTime = SIMTIME_MAX; for (const auto& entry : fragmentsMap) - if (!entry.second.expired) - nextExpirationTime = std::min(nextExpirationTime, entry.second.receptionStartTime + maxReceiveLifetime); + nextExpirationTime = std::min(nextExpirationTime, entry.second.receptionStartTime + maxReceiveLifetime); return nextExpirationTime; } std::vector BasicReassembly::removeExpiredFragments(simtime_t currentTime) { std::vector expiredFragments; - for (auto& entry : fragmentsMap) { + for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { + auto& entry = *it; auto& value = entry.second; - if (!value.expired && currentTime >= value.receptionStartTime + maxReceiveLifetime) { + if (currentTime >= value.receptionStartTime + maxReceiveLifetime) { for (auto fragment : value.fragments) if (fragment != nullptr) expiredFragments.push_back(fragment); - value.fragments.clear(); - value.receivedFragments = 0; - value.allFragments = 0; - value.expired = true; + auto contextKey = entry.first.getContextKey(); + auto sequenceSpaceKey = getSequenceSpaceKey(contextKey); + expiredSequenceNumbersMap[contextKey].insert(entry.first.extendedSequenceNumber); + it = fragmentsMap.erase(it); + pruneExpiredSequenceNumbers(sequenceSpaceKey); } + else + ++it; } return expiredFragments; } void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) { - for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { - auto sequenceNumber = it->first.seqNum; - bool isInSequenceRange = startSeqNumber <= endSeqNumber ? + auto isInSequenceRange = [startSeqNumber, endSeqNumber](int sequenceNumber) { + return startSeqNumber <= endSeqNumber ? sequenceNumber >= startSeqNumber && sequenceNumber <= endSeqNumber : sequenceNumber >= startSeqNumber || sequenceNumber <= endSeqNumber; - if (it->first.macAddress == address && it->first.tid == tid && isInSequenceRange) { + }; + for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { + auto sequenceNumber = getRawSequenceNumber(it->first.extendedSequenceNumber); + if (it->first.macAddress == address && it->first.tid == tid && isInSequenceRange(sequenceNumber)) { for (auto fragment : it->second.fragments) delete fragment; it = fragmentsMap.erase(it); @@ -130,6 +307,23 @@ void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumb else it++; } + + for (auto it = expiredSequenceNumbersMap.begin(); it != expiredSequenceNumbersMap.end();) { + if (it->first.macAddress == address && it->first.tid == tid) { + for (auto sequenceIt = it->second.begin(); sequenceIt != it->second.end();) { + if (isInSequenceRange(getRawSequenceNumber(*sequenceIt))) + sequenceIt = it->second.erase(sequenceIt); + else + ++sequenceIt; + } + if (it->second.empty()) + it = expiredSequenceNumbersMap.erase(it); + else + ++it; + } + else + ++it; + } } BasicReassembly::~BasicReassembly() diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h index cdcfd693727..e100f6ba74f 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h @@ -8,6 +8,8 @@ #ifndef __INET_BASICREASSEMBLY_H #define __INET_BASICREASSEMBLY_H +#include +#include #include #include "inet/common/packet/Packet.h" @@ -21,13 +23,37 @@ namespace ieee80211 { class INET_API BasicReassembly : public IReassembly, public cObject { protected: + static constexpr int NUM_SEQUENCE_NUMBERS = 1 << 12; + using ExtendedSequenceNumber = int64_t; + + struct ContextKey { + MacAddress macAddress; + MacAddress receiverAddress; + Ieee80211FrameType type; + Tid tid; + auto asTuple() const { return std::tie(macAddress, receiverAddress, type, tid); } + bool operator==(const ContextKey& other) const { return asTuple() == other.asTuple(); } + bool operator<(const ContextKey& other) const { return asTuple() < other.asTuple(); } + }; + struct SequenceSpaceKey { + MacAddress transmitterAddress; + MacAddress receiverAddress; + Tid tid; + auto asTuple() const { return std::tie(transmitterAddress, receiverAddress, tid); } + bool operator==(const SequenceSpaceKey& other) const { return asTuple() == other.asTuple(); } + bool operator<(const SequenceSpaceKey& other) const { return asTuple() < other.asTuple(); } + }; + struct SequenceSpaceValue { + ExtendedSequenceNumber highWatermark; + }; struct Key { MacAddress macAddress; MacAddress receiverAddress; Ieee80211FrameType type; Tid tid; - SequenceNumber seqNum; - auto asTuple() const { return std::tie(macAddress, receiverAddress, type, tid, seqNum); } + ExtendedSequenceNumber extendedSequenceNumber; + auto asTuple() const { return std::tie(macAddress, receiverAddress, type, tid, extendedSequenceNumber); } + ContextKey getContextKey() const { return { macAddress, receiverAddress, type, tid }; } bool operator==(const Key& other) const { return asTuple() == other.asTuple(); } bool operator<(const Key& other) const { return asTuple() < other.asTuple(); } }; @@ -35,13 +61,32 @@ class INET_API BasicReassembly : public IReassembly, public cObject std::vector fragments; uint16_t receivedFragments = 0; // each bit corresponds to a fragment number uint16_t allFragments = 0; // bits for all fragments set to one (0..numFragments-1); 0 means unfilled + int terminalFragmentNumber = -1; + bool hasContradictoryTerminalFragmentNumbers = false; simtime_t receptionStartTime; - bool expired = false; }; typedef std::map FragmentsMap; + typedef std::map SequenceSpacesMap; + using ExpiredSequenceNumbers = std::set; + // Generation-scoped tombstones reject late or ambiguous fragments and + // retain identities across one raw-sequence reuse. Older entries are + // pruned as the observed sequence-space high-water advances. + typedef std::map ExpiredSequenceNumbersMap; FragmentsMap fragmentsMap; + SequenceSpacesMap sequenceSpacesMap; + ExpiredSequenceNumbersMap expiredSequenceNumbersMap; simtime_t maxReceiveLifetime; + struct SequenceObservation { + ExtendedSequenceNumber extendedSequenceNumber; + bool ambiguous = false; + }; + + SequenceSpaceKey getSequenceSpaceKey(const ContextKey& contextKey) const; + SequenceObservation observeSequenceNumber(const SequenceSpaceKey& sequenceSpaceKey, SequenceNumber sequenceNumber); + void pruneExpiredSequenceNumbers(const SequenceSpaceKey& sequenceSpaceKey); + static SequenceNumber getRawSequenceNumber(ExtendedSequenceNumber extendedSequenceNumber); + public: BasicReassembly() : maxReceiveLifetime(SimTime(512 * 1024, SIMTIME_US)) {} BasicReassembly(simtime_t maxReceiveLifetime) : maxReceiveLifetime(maxReceiveLifetime) {} diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 8c8a9818e03..99b39c46bc3 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -11,6 +11,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/common/TimeTag_m.h" #include "inet/common/LabelsTag_m.h" #include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/common/packet/chunk/BytesChunk.h" #include "inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h" #include "inet/linklayer/ieee80211/mac/aggregation/MsduAggregation.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" @@ -531,6 +532,29 @@ class TestInProgressFrames : public InProgressFrames } }; +class TestBasicReassembly : public BasicReassembly +{ + public: + using BasicReassembly::BasicReassembly; + + size_t getNumActiveReassemblies() const { return fragmentsMap.size(); } + bool hasContradictoryTerminalFragmentNumbers() const { + ASSERT(fragmentsMap.size() == 1); + return fragmentsMap.begin()->second.hasContradictoryTerminalFragmentNumbers; + } + uint16_t getAllFragments() const { + ASSERT(fragmentsMap.size() == 1); + return fragmentsMap.begin()->second.allFragments; + } + size_t getNumExpiredContexts() const { return expiredSequenceNumbersMap.size(); } + size_t getNumExpiredSequenceNumbers() const { + size_t count = 0; + for (const auto& entry : expiredSequenceNumbersMap) + count += entry.second.size(); + return count; + } +}; + class TestEdcaf : public Edcaf { public: @@ -2131,8 +2155,9 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(fragmentedCallback.managementPackets.size() == 1); delete fragments; - // Seeing the final fragment first does not imply completeness. Dispatch - // occurs only when the missing lower-numbered fragment later arrives. + // IEEE Std 802.11-2024, 10.5 permits fragments to be received out of + // order. The final fragment therefore starts a reassembly and fragment 0 + // completes it when it arrives later. auto outOfOrderFragments = makeFragments("outOfOrderAddbaRequest", 4, 61); for (auto& fragment : *outOfOrderFragments) fragment = convertToWireRepresentation(fragment); @@ -2142,6 +2167,13 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(fragmentedCallback.managementPackets.size() == 1); auto outOfOrderCompleteReception = receiveFragment(outOfOrderFragments->at(0)); ASSERT(outOfOrderCompleteReception.completeHeader != nullptr); + ASSERT(outOfOrderCompleteReception.completeHeader->getFragmentNumber() == 0); + ASSERT(!outOfOrderCompleteReception.completeHeader->getMoreFragments()); + auto outOfOrderRequest = dynamicPtrCast(outOfOrderCompleteReception.completeHeader); + ASSERT(outOfOrderRequest != nullptr); + ASSERT(outOfOrderRequest->getTid() == 4); + ASSERT(outOfOrderRequest->getDialogToken() == 74); + ASSERT(outOfOrderRequest->getStartingSequenceNumber() == SequenceNumberCyclic(104)); hcf.processReceivedManagementFrame(outOfOrderCompleteReception.completeHeader); ASSERT(recipientHandler->getAgreement(4, peer2) != nullptr); ASSERT(fragmentedCallback.managementPackets.size() == 2); @@ -2368,6 +2400,83 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; delete discardedNewWindowFrames.front(); } +// IEEE Std 802.11-2024, 10.4 and 10.5: fragments are transmitted from +// fragment 0 upward and the destination reconstructs an MMPDU in Fragment +// Number order. BlockAckReordering receives fragments in arrival order, so +// both immediate and BAR-triggered complete vectors must be normalized before +// they reach BasicReassembly. +{ + auto makeQosFragment = [&](const char *name, SequenceNumberCyclic sequenceNumber, Tid tid, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer2, tid, sequenceNumber); + dataHeader->setTransmitterAddress(peer1); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto assertFragmentOrder = [](const BlockAckReordering::Fragments& fragments) { + ASSERT(fragments.size() == 4); + for (int fragmentNumber = 0; fragmentNumber < 4; fragmentNumber++) + ASSERT(fragments.at(fragmentNumber)->peekAtFront()->getFragmentNumber() == fragmentNumber); + }; + auto reassembleOrderedFragments = [&](const BlockAckReordering::Fragments& fragments, const std::vector& expectedPayload) { + TestBasicReassembly reassembly; + Packet *completedPacket = nullptr; + int numCompletedPackets = 0; + for (auto fragment : fragments) { + auto packet = reassembly.addFragment(fragment); + if (packet != nullptr) { + completedPacket = packet; + numCompletedPackets++; + } + } + ASSERT(numCompletedPackets == 1); + ASSERT(completedPacket != nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + auto completedHeader = completedPacket->peekAtFront(); + ASSERT(completedPacket->peekAt(completedHeader->getChunkLength(), B(expectedPayload.size()))->getBytes() == expectedPayload); + delete completedPacket; + }; + + RecipientBlockAckAgreement immediateAgreement(peer1, 6, SequenceNumberCyclic(100), 64, 0); + TestBlockAckReordering immediateReordering; + BlockAckReordering::ReorderBuffer immediateFrames; + for (int fragmentNumber : { 1, 2, 3, 0 }) { + auto packet = makeQosFragment("immediateOutOfOrderFragment", SequenceNumberCyclic(100), 6, fragmentNumber, fragmentNumber != 3, 0x40 + fragmentNumber); + auto dataHeader = packet->peekAtFront(); + auto frames = immediateReordering.processReceivedQoSFrame(&immediateAgreement, packet, dataHeader); + if (fragmentNumber == 0) + immediateFrames = frames; + else + ASSERT(frames.empty()); + } + ASSERT(immediateFrames.size() == 1); + auto immediateFragments = immediateFrames.at(100); + assertFragmentOrder(immediateFragments); + reassembleOrderedFragments(immediateFragments, { 0x40, 0x41, 0x42, 0x43 }); + + RecipientBlockAckAgreement barAgreement(peer1, 6, SequenceNumberCyclic(200), 64, 0); + TestBlockAckReordering barReordering; + for (int fragmentNumber : { 1, 2, 3, 0 }) { + auto packet = makeQosFragment("barOutOfOrderFragment", SequenceNumberCyclic(201), 6, fragmentNumber, fragmentNumber != 3, 0x50 + fragmentNumber); + auto dataHeader = packet->peekAtFront(); + ASSERT(barReordering.processReceivedQoSFrame(&barAgreement, packet, dataHeader).empty()); + } + auto barRequest = makeShared(); + barRequest->setTransmitterAddress(peer1); + barRequest->setReceiverAddress(peer2); + barRequest->setTidInfo(6); + barRequest->setStartingSequenceNumber(SequenceNumberCyclic(201)); + auto barFrames = barReordering.processReceivedBlockAckReq(&barAgreement, barRequest); + ASSERT(barFrames.size() == 1); + auto barFragments = barFrames.at(201); + assertFragmentOrder(barFragments); + reassembleOrderedFragments(barFragments, { 0x50, 0x51, 0x52, 0x53 }); +} + // The recipient data service owns reorder-buffer reset observability: every // buffered MPDU is reported exactly once before it is deleted. { @@ -4800,6 +4909,327 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { pendingQueues[ac].unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcf); } +// IEEE Std 802.11-2024, 10.5 requires reconstruction in Fragment Number +// order. A later fragment can arrive before fragment 0, and the receiver's +// observed sequence-number generation keeps a later sequence reuse separate. +{ + TestBasicReassembly reassembly; + auto makeResponseFragments = [&](const char *name, uint8_t dialogToken, uint16_t statusCode, const std::vector& payload, int sequenceNumber, int fragmentCount) { + auto response = makeResponse(peer1, 3, dialogToken, statusCode); + response->setReceiverAddress(peer2); + response->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + auto packet = new Packet(name, response); + packet->insertAtBack(makeShared(payload)); + packet->insertAtBack(makeShared()); + auto managementHeaderLength = makeShared()->getChunkLength(); + auto bodyLength = (int)(response->getChunkLength() - managementHeaderLength).get() + (int)payload.size(); + Fragmentation fragmentation; + auto fragmentSizes = std::vector({ 8, bodyLength - 8 }); + if (fragmentCount == 3) + fragmentSizes = { 4, 4, bodyLength - 8 }; + ASSERT(fragmentCount == (int)fragmentSizes.size()); + return fragmentation.fragmentFrame(packet, fragmentSizes); + }; + + const std::vector outOfOrderPayload = { 0x14, 0x15, 0x16, 0x17 }; + auto outOfOrderFragments = makeResponseFragments("reassemblyOutOfOrderResponse", 23, 5, outOfOrderPayload, 4094, 2); + ASSERT(reassembly.addFragment(outOfOrderFragments->at(1)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto outOfOrderExpiration = reassembly.getNextExpirationTime(); + ASSERT(outOfOrderExpiration != SIMTIME_MAX); + auto retryOutOfOrderFirst = outOfOrderFragments->at(0)->dup(); + auto retryOutOfOrderHeader = retryOutOfOrderFirst->removeAtFront(); + retryOutOfOrderHeader->setRetry(true); + retryOutOfOrderFirst->insertAtFront(retryOutOfOrderHeader); + auto outOfOrderCompleted = reassembly.addFragment(retryOutOfOrderFirst); + ASSERT(outOfOrderCompleted != nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + auto outOfOrderHeader = outOfOrderCompleted->peekAtFront(); + auto outOfOrderResponse = dynamicPtrCast(outOfOrderHeader); + ASSERT(outOfOrderResponse != nullptr); + ASSERT(outOfOrderResponse->getDialogToken() == 23); + ASSERT(outOfOrderResponse->getStatusCode() == 5); + ASSERT(outOfOrderResponse->getRetry()); + ASSERT(outOfOrderCompleted->peekAt(outOfOrderResponse->getChunkLength(), B(outOfOrderPayload.size()))->getBytes() == outOfOrderPayload); + delete outOfOrderCompleted; + delete outOfOrderFragments->at(0); + delete outOfOrderFragments; + + // A sequence number exactly half a space away has no cyclic ordering. + // Neither an independently accepted unfragmented frame nor a rejected + // fragmented frame at that boundary may disturb an active generation. + const std::vector halfSpacePayload = { 0x18, 0x19, 0x1a }; + auto preservedFragments = makeResponseFragments("reassemblyPreservedResponse", 25, 6, halfSpacePayload, 4093, 2); + ASSERT(reassembly.addFragment(preservedFragments->at(1)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto halfSpaceStandalone = makeTaggedManagementPacket("reassemblyHalfSpaceStandalone", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(2046), 0, false, 0); + ASSERT(reassembly.addFragment(halfSpaceStandalone) == halfSpaceStandalone); + delete halfSpaceStandalone; + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto halfSpaceFragments = makeResponseFragments("reassemblyHalfSpaceResponse", 27, 7, halfSpacePayload, 2046, 2); + ASSERT(reassembly.addFragment(halfSpaceFragments->at(0)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto preservedCompleted = reassembly.addFragment(preservedFragments->at(0)); + ASSERT(preservedCompleted != nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + delete preservedCompleted; + delete preservedFragments; + delete halfSpaceFragments->at(1); + delete halfSpaceFragments; + + // The old final fragment remains buffered while the receiver observes the + // numeric wrap and enough baseline sequence-space progress to identify a + // reused raw number. A delayed old fragment 0 and the real new fragment 0 + // are indistinguishable, so their new extended generation is quarantined. + const std::vector oldPayload = { 0x20, 0x21, 0x22, 0x23 }; + auto oldFragments = makeResponseFragments("reassemblyOldResponse", 17, 1, oldPayload, 4095, 2); + auto lateOldFirstFragment = oldFragments->at(0)->dup(); + auto lateOldRetryFragment = oldFragments->at(0)->dup(); + auto lateOldRetryHeader = lateOldRetryFragment->removeAtFront(); + lateOldRetryHeader->setRetry(true); + lateOldRetryFragment->insertAtFront(lateOldRetryHeader); + ASSERT(reassembly.addFragment(oldFragments->at(1)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto oldExpiration = reassembly.getNextExpirationTime(); + wait(1e-9); + for (int sequenceNumber = 0; sequenceNumber <= 2048; sequenceNumber++) { + auto standalone = makeTaggedManagementPacket("reassemblySequenceAdvance", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(sequenceNumber), 0, false, sequenceNumber); + ASSERT(reassembly.addFragment(standalone) == standalone); + delete standalone; + } + + const std::vector newPayload = { 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6 }; + auto newFragments = makeResponseFragments("reassemblyNewResponse", 29, 37, newPayload, 4095, 3); + ASSERT(reassembly.addFragment(lateOldFirstFragment) == nullptr); + ASSERT(reassembly.addFragment(lateOldRetryFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + ASSERT(reassembly.addFragment(newFragments->at(0)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.addFragment(newFragments->at(1)) == nullptr); + ASSERT(reassembly.addFragment(newFragments->at(2)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto expiredOld = reassembly.removeExpiredFragments(oldExpiration); + ASSERT(expiredOld.size() == 1); + delete expiredOld.front(); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 2); + delete oldFragments->at(0); + delete oldFragments; + delete newFragments; + + // Once the old active entry and the quarantined generation are gone, the + // receiver can observe one more modulo-4096 wrap. The later non-Retry + // fragment 0 retires the older same-raw tombstone without an external + // purge, and only the later generation's bytes are reconstructed. + for (int sequenceNumber = 0; sequenceNumber <= 2048; sequenceNumber++) { + auto standalone = makeTaggedManagementPacket("reassemblySecondSequenceAdvance", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(sequenceNumber), 0, false, sequenceNumber); + ASSERT(reassembly.addFragment(standalone) == standalone); + delete standalone; + } + auto recoveredFragments = makeResponseFragments("reassemblyRecoveredResponse", 29, 37, newPayload, 4095, 3); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + ASSERT(reassembly.addFragment(recoveredFragments->at(0)) == nullptr); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 0); + ASSERT(reassembly.addFragment(recoveredFragments->at(1)) == nullptr); + auto completed = reassembly.addFragment(recoveredFragments->at(2)); + ASSERT(completed != nullptr); + auto completedHeader = completed->peekAtFront(); + auto completedResponse = dynamicPtrCast(completedHeader); + ASSERT(completedResponse != nullptr); + ASSERT(completedResponse->getDialogToken() == 29); + ASSERT(completedResponse->getStatusCode() == 37); + ASSERT(completedResponse->getSequenceNumber() == SequenceNumberCyclic(4095)); + ASSERT(completedResponse->getFragmentNumber() == 0); + ASSERT(!completedResponse->getMoreFragments()); + ASSERT(completed->peekAt(completedResponse->getChunkLength(), B(newPayload.size()))->getBytes() == newPayload); + delete completed; + delete recoveredFragments; +} + +// A terminal duplicate on an occupied nonterminal slot is still observed for +// contradiction tracking, but cannot establish an affirmative completion mask. +{ + TestBasicReassembly reassembly(SimTime(1, SIMTIME_US)); + auto firstFragment = makeTaggedManagementPacket("reassemblyDuplicateTerminalFirst", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(601), 0, true, 1); + auto secondFragment = makeTaggedManagementPacket("reassemblyDuplicateTerminalSecond", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(601), 1, true, 2); + auto duplicateTerminal = makeTaggedManagementPacket("reassemblyDuplicateTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(601), 1, false, 3); + ASSERT(reassembly.addFragment(firstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto expiration = reassembly.getNextExpirationTime(); + ASSERT(expiration != SIMTIME_MAX); + ASSERT(reassembly.getAllFragments() == 0); + ASSERT(reassembly.addFragment(secondFragment) == nullptr); + ASSERT(reassembly.addFragment(duplicateTerminal) == nullptr); + ASSERT(!reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getAllFragments() == 0); + ASSERT(reassembly.getNextExpirationTime() == expiration); + auto expiredFragments = reassembly.removeExpiredFragments(expiration); + ASSERT(expiredFragments.size() == 2); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + for (auto fragment : expiredFragments) + delete fragment; +} + +// A terminal duplicate on an occupied slot with a different number from the +// retained terminal marker invalidates completion, even when all slots arrive. +{ + TestBasicReassembly reassembly(SimTime(1, SIMTIME_US)); + auto retainedTerminal = makeTaggedManagementPacket("reassemblyRetainedTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(602), 2, false, 1); + auto retainedFragment = makeTaggedManagementPacket("reassemblyRetainedFragment", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(602), 1, true, 2); + auto duplicateTerminal = makeTaggedManagementPacket("reassemblyContradictoryDuplicateTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(602), 1, false, 3); + auto firstFragment = makeTaggedManagementPacket("reassemblyContradictoryDuplicateFirst", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(602), 0, true, 4); + ASSERT(reassembly.addFragment(retainedTerminal) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto expiration = reassembly.getNextExpirationTime(); + ASSERT(expiration != SIMTIME_MAX); + ASSERT(reassembly.getAllFragments() == 7); + ASSERT(reassembly.addFragment(retainedFragment) == nullptr); + ASSERT(reassembly.addFragment(duplicateTerminal) == nullptr); + ASSERT(reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getAllFragments() == 7); + ASSERT(reassembly.getNextExpirationTime() == expiration); + ASSERT(reassembly.addFragment(firstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getNextExpirationTime() == expiration); + auto expiredFragments = reassembly.removeExpiredFragments(expiration); + ASSERT(expiredFragments.size() == 3); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + for (auto fragment : expiredFragments) + delete fragment; +} + +// Contradictory terminal markers invalidate completion independent of their +// arrival order. The invalid entry remains owned until receive-lifetime +// expiry, which returns each uniquely retained fragment to the caller. +for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) }) { + TestBasicReassembly reassembly(SimTime(1, SIMTIME_US)); + auto firstTerminal = makeTaggedManagementPacket("reassemblyContradictoryFirstTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(600), terminalOrder.first, false, 1); + auto secondTerminal = makeTaggedManagementPacket("reassemblyContradictorySecondTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(600), terminalOrder.second, false, 2); + auto firstFragment = makeTaggedManagementPacket("reassemblyContradictoryFirstFragment", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(600), 0, true, 3); + ASSERT(reassembly.addFragment(firstTerminal) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(!reassembly.hasContradictoryTerminalFragmentNumbers()); + auto expiration = reassembly.getNextExpirationTime(); + ASSERT(expiration != SIMTIME_MAX); + ASSERT(reassembly.addFragment(secondTerminal) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getNextExpirationTime() == expiration); + ASSERT(reassembly.addFragment(firstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getNextExpirationTime() == expiration); + + auto expiredFragments = reassembly.removeExpiredFragments(expiration); + ASSERT(expiredFragments.size() == 3); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNextExpirationTime() == SIMTIME_MAX); + for (auto fragment : expiredFragments) + delete fragment; +} + +// Expired reassembly stores exact observed generations, rejects late +// fragments, and does not clear a tombstone when a same-generation fragment 0 +// arrives without an observed sequence-space advance. Unfragmented frames are +// accepted independently and never mixed with the tombstone. +{ + TestBasicReassembly reassembly(SimTime(1, SIMTIME_US)); + auto firstFragment = makeTaggedManagementPacket("reassemblyExpiredFirst", ST_ACTION, peer1, peer2, SequenceNumberCyclic(430), 0, true, 0); + ASSERT(reassembly.addFragment(firstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto expiredFragments = reassembly.removeExpiredFragments(simTime() + SimTime(2, SIMTIME_US)); + ASSERT(expiredFragments.size() == 1); + delete expiredFragments.front(); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredContexts() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + + // A receive-lifetime tombstone does not require an external purge for + // recovery. Once the receiver observes a later generation in the same + // sequence space, its non-Retry fragment 0 retires the older tombstone. + { + TestBasicReassembly wrapRecovery(SimTime(1, SIMTIME_US)); + auto expiredFirst = makeTaggedManagementPacket("reassemblyExpiredWrapFirst", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(430), 0, true, 0); + ASSERT(wrapRecovery.addFragment(expiredFirst) == nullptr); + auto expiration = wrapRecovery.getNextExpirationTime(); + auto expired = wrapRecovery.removeExpiredFragments(expiration); + ASSERT(expired.size() == 1); + delete expired.front(); + for (int sequenceNumber = 431; sequenceNumber <= 2479; sequenceNumber++) { + auto standalone = makeTaggedManagementPacket("reassemblyExpiredWrapAdvance", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(sequenceNumber), 0, false, sequenceNumber); + ASSERT(wrapRecovery.addFragment(standalone) == standalone); + delete standalone; + } + auto recoveredFirst = makeTaggedManagementPacket("reassemblyExpiredWrapRecoveredFirst", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(430), 0, true, 1); + ASSERT(wrapRecovery.addFragment(recoveredFirst) == nullptr); + ASSERT(wrapRecovery.getNumExpiredSequenceNumbers() == 0); + ASSERT(wrapRecovery.getNumActiveReassemblies() == 1); + // Leave the recovered, incomplete reassembly for the destructor to own + // and clean up; completion is covered by the exact-payload recovery test above. + } + + auto lateFinalFragment = makeTaggedManagementPacket("reassemblyLateFinal", ST_ACTION, peer1, peer2, SequenceNumberCyclic(430), 1, false, 0, true); + ASSERT(reassembly.addFragment(lateFinalFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + auto reusedFirstFragment = makeTaggedManagementPacket("reassemblyReusedFirst", ST_ACTION, peer1, peer2, SequenceNumberCyclic(430), 0, true, 0); + ASSERT(reassembly.addFragment(reusedFirstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredContexts() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + auto unfragmentedPacket = makeTaggedManagementPacket("reassemblyUnfragmentedAfterExpiry", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(430), 0, false, 0); + ASSERT(reassembly.addFragment(unfragmentedPacket) == unfragmentedPacket); + delete unfragmentedPacket; + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + for (int sequenceNumber = 0; sequenceNumber < 64; sequenceNumber++) { + auto fragment = makeTaggedManagementPacket("reassemblyManyExpiredFirst", ST_ACTION, peer1, peer2, SequenceNumberCyclic(500 + sequenceNumber), 0, true, 0); + ASSERT(reassembly.addFragment(fragment) == nullptr); + } + ASSERT(reassembly.getNumActiveReassemblies() == 64); + auto manyExpiredFragments = reassembly.removeExpiredFragments(simTime() + SimTime(3, SIMTIME_US)); + ASSERT(manyExpiredFragments.size() == 64); + for (auto fragment : manyExpiredFragments) + delete fragment; + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNextExpirationTime() == SIMTIME_MAX); + ASSERT(reassembly.getNumExpiredContexts() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 65); + auto otherContextFirstFragment = makeTaggedManagementPacket("reassemblyOtherContextFirst", ST_ACTION, peer2, peer3, + SequenceNumberCyclic(600), 0, true, 0); + ASSERT(reassembly.addFragment(otherContextFirstFragment) == nullptr); + auto otherContextExpiredFragments = reassembly.removeExpiredFragments(simTime() + SimTime(4, SIMTIME_US)); + ASSERT(otherContextExpiredFragments.size() == 1); + delete otherContextExpiredFragments.front(); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredContexts() == 2); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 66); + reassembly.purge(peer2, -1, 0, 4095); + ASSERT(reassembly.getNumExpiredContexts() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 65); + reassembly.purge(peer1, -1, 0, 4095); + ASSERT(reassembly.getNumExpiredContexts() == 0); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 0); +} + // An originator inactivity DELBA retains the agreement transaction identity. // Once that agreement is replaced, every stale fragment is rejected while a // final DELBA carrying the current generation removes the replacement once. From 5e0f87102b184c55ab2bbb9fbf947581e38d3795 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:04 +0200 Subject: [PATCH 10/16] ieee80211: enforce receive lifetime during Block Ack reordering Record an immutable receive deadline when the first fragment is retained in a Block Ack receive buffer. Return inserted, released, and expired frames explicitly so recipient services, rather than the reorder buffer, own every final drop, signal, and deletion. Drive reordering and scalar reassembly from one receive-lifetime timer. Expired late fragments remain visible to Block Ack bookkeeping but cannot seed a new reassembly context, while peer and TID reset purges every owned fragment without leaking or deleting it twice. Return ownership from IReassembly::purge(), reject negative maxReceiveLifetime values while keeping zero valid, and cover expiry, reset, wrap recovery, fragmented sequences, and timer rescheduling. --- .../blockackreordering/BlockAckReordering.cc | 79 ++++-- .../blockackreordering/BlockAckReordering.h | 11 +- .../mac/blockackreordering/ReceiveBuffer.cc | 103 +++++++- .../mac/blockackreordering/ReceiveBuffer.h | 26 +- .../ieee80211/mac/contract/IReassembly.h | 9 +- .../mac/fragmentation/BasicReassembly.cc | 7 +- .../mac/fragmentation/BasicReassembly.h | 2 +- .../mac/recipient/RecipientMacDataService.cc | 5 +- .../mac/recipient/RecipientMacDataService.ned | 2 +- .../recipient/RecipientQosMacDataService.cc | 89 +++++-- .../recipient/RecipientQosMacDataService.h | 7 +- .../recipient/RecipientQosMacDataService.ned | 2 +- tests/unit/Ieee80211AddbaTransaction_1.test | 242 +++++++++++++++++- 13 files changed, 517 insertions(+), 67 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index c70bfcb570b..3f4801ab76f 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -28,14 +28,16 @@ BlockAckReordering::Fragments BlockAckReordering::sortFragmentsByFragmentNumber( // // The recipient flushes received MSDUs from its receive buffer as described in this subclause. [...] // -BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader) +BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceivedQoSFrameWithResult(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader) { + QosFrameProcessingResult result; ReceiveBuffer *receiveBuffer = createReceiveBufferIfNecessary(agreement); // The reception of QoS data frames using Normal Ack policy shall not be used by the // recipient to reset the timer to detect Block Ack timeout (see 10.5.4). // This allows the recipient to delete the Block Ack if the originator does not switch // back to using Block Ack. - if (receiveBuffer->insertFrame(dataPacket, dataHeader)) { + auto insertionResult = receiveBuffer->insertFrameWithResult(dataPacket, dataHeader); + if (insertionResult == ReceiveBuffer::FrameInsertionResult::INSERTED) { if (dataHeader->getAckPolicy() == BLOCK_ACK) agreement->blockAckPolicyFrameReceived(dataHeader); auto earliestCompleteMsduOrAMsdu = getEarliestCompleteMsduOrAMsduIfExists(receiveBuffer); @@ -45,20 +47,41 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedQoSFrame(Re // sequence number shall be passed up to the next MAC process. if (receiveBuffer->isFull()) { passedUp(agreement, receiveBuffer, earliestSequenceNumber); - return ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); + result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); + return result; } // If, after an MPDU is received, the receive buffer is not full, but the sequence number of the complete MSDU or // A-MSDU in the buffer with the lowest sequence number is equal to the NextExpectedSequenceNumber for // that Block Ack agreement, then the MPDU shall be passed up to the next MAC process. else if (earliestSequenceNumber == receiveBuffer->getNextExpectedSequenceNumber()) { passedUp(agreement, receiveBuffer, earliestSequenceNumber); - return ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); + result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); + return result; } } } - else + else { + if (insertionResult == ReceiveBuffer::FrameInsertionResult::REJECTED_EXPIRED) { + // A later fragment of a receive-timer-expired MPDU is still a + // received BLOCK_ACK frame, even though the fragment is rejected + // by the tombstone and must be reported to the service for drop + // observability. + if (dataHeader->getAckPolicy() == BLOCK_ACK) + agreement->blockAckPolicyFrameReceived(dataHeader); + result.tombstonedFragments.push_back(dataPacket); + return result; + } delete dataPacket; - return ReorderBuffer({}); + } + return result; +} + +BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader) +{ + auto result = processReceivedQoSFrameWithResult(agreement, dataPacket, dataHeader); + for (auto packet : result.tombstonedFragments) + delete packet; + return result.frames; } // @@ -125,7 +148,7 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::collectCompletePrecedingMp auto sequenceNumber = it.first; auto fragments = it.second; if (SequenceNumberCyclic(sequenceNumber) < startingSequenceNumber) - if (isComplete(fragments)) + if (ReceiveBuffer::isComplete(fragments)) completePrecedingMpdus[sequenceNumber] = sortFragmentsByFragmentNumber(fragments); } return completePrecedingMpdus; @@ -152,7 +175,7 @@ bool BlockAckReordering::addMsduIfComplete(ReceiveBuffer *receiveBuffer, Reorder auto it = buffer.find(seqNum.get()); if (it != buffer.end()) { auto fragments = it->second; - if (isComplete(fragments)) { + if (ReceiveBuffer::isComplete(fragments)) { reorderBuffer[seqNum.get()] = sortFragmentsByFragmentNumber(fragments); return true; } @@ -168,19 +191,6 @@ void BlockAckReordering::releaseReceiveBuffer(RecipientBlockAckAgreement *agreem } } -bool BlockAckReordering::isComplete(const std::vector& fragments) -{ - int largestFragmentNumber = -1; - std::set fragNums; // possible duplicate frames - for (auto fragment : fragments) { - const auto& header = fragment->peekAtFront(); - if (!header->getMoreFragments()) - largestFragmentNumber = header->getFragmentNumber(); - fragNums.insert(header->getFragmentNumber()); - } - return largestFragmentNumber != -1 && largestFragmentNumber + 1 == (int)fragNums.size(); -} - ReceiveBuffer *BlockAckReordering::createReceiveBufferIfNecessary(RecipientBlockAckAgreement *agreement) { SequenceNumberCyclic startingSequenceNumber = agreement->getStartingSequenceNumber(); @@ -198,6 +208,29 @@ ReceiveBuffer *BlockAckReordering::createReceiveBufferIfNecessary(RecipientBlock return it->second; } +simtime_t BlockAckReordering::getNextExpirationTime() const +{ + simtime_t nextExpirationTime = SIMTIME_MAX; + for (const auto& [id, receiveBuffer] : receiveBuffers) + nextExpirationTime = std::min(nextExpirationTime, receiveBuffer->getNextExpirationTime(maxReceiveLifetime)); + return nextExpirationTime; +} + +std::vector BlockAckReordering::removeExpiredFragments(simtime_t currentTime) +{ + std::vector expiredFragments; + for (const auto& [id, receiveBuffer] : receiveBuffers) { + // IEEE Std 802.11-2024, 10.5 requires incomplete fragmented MPDUs + // to be discarded when dot11MaxReceiveLifetime elapses. The receive + // timer does not advance NextExpectedSequenceNumber or alter the + // Block Ack record; those variables are progressed by normal Data/BAR + // processing, and the scoreboard is independent of reassembly state. + auto bufferExpiredFragments = receiveBuffer->removeExpiredFragments(currentTime, maxReceiveLifetime); + expiredFragments.insert(expiredFragments.end(), bufferExpiredFragments.begin(), bufferExpiredFragments.end()); + } + return expiredFragments; +} + std::vector BlockAckReordering::resetReceiveBuffer(Tid tid, MacAddress originatorAddr) { std::vector frames; @@ -228,7 +261,7 @@ std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists SequenceNumberCyclic earliestSeqNum = SequenceNumberCyclic(0); const auto& buffer = receiveBuffer->getBuffer(); for (auto it : buffer) { - if (isComplete(it.second)) { + if (ReceiveBuffer::isComplete(it.second)) { earliestFragments = it.second; earliestSeqNum = earliestFragments.at(0)->peekAtFront()->getSequenceNumber(); break; @@ -238,7 +271,7 @@ std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists for (auto it : buffer) { SequenceNumberCyclic currentSeqNum = it.second.at(0)->peekAtFront()->getSequenceNumber(); if (currentSeqNum < earliestSeqNum) { - if (isComplete(it.second)) { + if (ReceiveBuffer::isComplete(it.second)) { earliestFragments = it.second; earliestSeqNum = currentSeqNum; } diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index 5e02a452d44..8dd641c7439 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -33,6 +33,7 @@ class INET_API BlockAckReordering protected: std::map, ReceiveBuffer *> receiveBuffers; + simtime_t maxReceiveLifetime = SIMTIME_MAX; protected: static Fragments sortFragmentsByFragmentNumber(const Fragments& fragments); @@ -40,16 +41,24 @@ class INET_API BlockAckReordering ReorderBuffer collectConsecutiveCompleteFollowingMpdus(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); std::vector getEarliestCompleteMsduOrAMsduIfExists(ReceiveBuffer *receiveBuffer); - bool isComplete(const Fragments& fragments); void passedUp(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber); void releaseReceiveBuffer(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, const ReorderBuffer& reorderBuffer); ReceiveBuffer *createReceiveBufferIfNecessary(RecipientBlockAckAgreement *agreement); bool addMsduIfComplete(ReceiveBuffer *receiveBuffer, ReorderBuffer& reorderBuffer, SequenceNumberCyclic seqNum); public: + struct QosFrameProcessingResult { + ReorderBuffer frames; + Fragments tombstonedFragments; + }; + + explicit BlockAckReordering(simtime_t maxReceiveLifetime = SIMTIME_MAX) : maxReceiveLifetime(maxReceiveLifetime) {} virtual ~BlockAckReordering(); std::vector resetReceiveBuffer(Tid tid, MacAddress originatorAddr); + simtime_t getNextExpirationTime() const; + std::vector removeExpiredFragments(simtime_t currentTime); + QosFrameProcessingResult processReceivedQoSFrameWithResult(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader); ReorderBuffer processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader); ReorderBuffer processReceivedBlockAckReq(RecipientBlockAckAgreement *agreement, const Ptr& blockAckReq); }; diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc index 3bf4115a4b1..f886058cf5c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h" +#include + namespace inet { namespace ieee80211 { @@ -16,16 +18,55 @@ ReceiveBuffer::ReceiveBuffer(int bufferSize, SequenceNumberCyclic nextExpectedSe { } +bool ReceiveBuffer::isComplete(const Fragments& fragments) +{ + FragmentNumber terminalFragmentNumber = -1; + bool hasContradictoryTerminalMarkers = false; + std::set fragmentNumbers; + for (auto fragment : fragments) { + if (fragment == nullptr) + continue; + const auto& header = fragment->peekAtFront(); + if (!header->getMoreFragments()) { + if (terminalFragmentNumber == -1) + terminalFragmentNumber = header->getFragmentNumber(); + else if (terminalFragmentNumber != header->getFragmentNumber()) + hasContradictoryTerminalMarkers = true; + } + fragmentNumbers.insert(header->getFragmentNumber()); + } + if (terminalFragmentNumber < 0 || hasContradictoryTerminalMarkers || fragmentNumbers.size() != (size_t)terminalFragmentNumber + 1) + return false; + for (FragmentNumber fragmentNumber = 0; fragmentNumber <= terminalFragmentNumber; fragmentNumber++) + if (fragmentNumbers.find(fragmentNumber) == fragmentNumbers.end()) + return false; + return true; +} + +void ReceiveBuffer::pruneExpiredFragmentSequences() +{ + for (auto it = expiredFragmentSequences.begin(); it != expiredFragmentSequences.end();) { + if (SequenceNumberCyclic(*it) < nextExpectedSequenceNumber) + it = expiredFragmentSequences.erase(it); + else + ++it; + } +} + // // Upon the receipt of a QoS data frame from the originator for which a Block Ack agreement exists, the recipient // buffers the MSDU regardless of the value of the Ack Policy subfield within the QoS Control field of the QoS // data frame, unless the sequence number of the frame is older than the NextExpectedSequenceNumber for that // Block Ack agreement, in which case the frame is discarded because it is either old or a duplicate. // -bool ReceiveBuffer::insertFrame(Packet *dataPacket, const Ptr& dataHeader) +ReceiveBuffer::FrameInsertionResult ReceiveBuffer::insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader) { auto sequenceNumber = dataHeader->getSequenceNumber(); auto fragmentNumber = dataHeader->getFragmentNumber(); + bool isFragmented = dataHeader->getMoreFragments() || fragmentNumber != 0; + pruneExpiredFragmentSequences(); + if (isFragmented && expiredFragmentSequences.find(sequenceNumber.get()) != expiredFragmentSequences.end()) + return FrameInsertionResult::REJECTED_EXPIRED; // The total number of MPDUs in these MSDUs may not // exceed the reorder buffer size in the receiver. if (length < bufferSize && nextExpectedSequenceNumber <= sequenceNumber && sequenceNumber < nextExpectedSequenceNumber + bufferSize) { @@ -36,19 +77,26 @@ bool ReceiveBuffer::insertFrame(Packet *dataPacket, const PtrpeekAtFront(); if (fragmentHeader->getSequenceNumber() == sequenceNumber && fragmentHeader->getFragmentNumber() == fragmentNumber) - return false; + return FrameInsertionResult::REJECTED; } fragments.push_back(dataPacket); } else { buffer[sequenceNumber.get()].push_back(dataPacket); + bufferEntries[sequenceNumber.get()] = { simTime(), isFragmented, false }; } // The total number of frames that can be sent depends on the total // number of MPDUs in all the outstanding MSDUs. length++; - return true; + // Once an entry has received a fragmented MPDU, keep that identity + // tied to the generation even if a later malformed or + // unfragmented-shaped header is accepted into the same sequence slot. + auto& bufferEntry = bufferEntries[sequenceNumber.get()]; + bufferEntry.hasFragmentedIdentity |= isFragmented; + bufferEntry.receiveLifetimeActive = bufferEntry.hasFragmentedIdentity && !isComplete(buffer[sequenceNumber.get()]); + return FrameInsertionResult::INSERTED; } - return false; + return FrameInsertionResult::REJECTED; } void ReceiveBuffer::dropFramesUntil(SequenceNumberCyclic sequenceNumber) @@ -59,6 +107,7 @@ void ReceiveBuffer::dropFramesUntil(SequenceNumberCyclic sequenceNumber) length -= it->second.size(); for (auto fragment : it->second) delete fragment; + bufferEntries.erase(it->first); it = buffer.erase(it); } else @@ -71,6 +120,7 @@ void ReceiveBuffer::removeFrame(SequenceNumberCyclic sequenceNumber) auto it = buffer.find(sequenceNumber.get()); if (it != buffer.end()) { length -= it->second.size(); + bufferEntries.erase(sequenceNumber.get()); buffer.erase(sequenceNumber.get()); } else @@ -83,10 +133,55 @@ ReceiveBuffer::Fragments ReceiveBuffer::extractFrames() for (auto& [sequenceNumber, fragments] : buffer) frames.insert(frames.end(), fragments.begin(), fragments.end()); buffer.clear(); + bufferEntries.clear(); + expiredFragmentSequences.clear(); length = 0; return frames; } +simtime_t ReceiveBuffer::getNextExpirationTime(simtime_t maxReceiveLifetime) const +{ + if (maxReceiveLifetime == SIMTIME_MAX) + return SIMTIME_MAX; + simtime_t nextExpirationTime = SIMTIME_MAX; + for (const auto& [sequenceNumber, entry] : bufferEntries) + if (entry.receiveLifetimeActive) + nextExpirationTime = std::min(nextExpirationTime, entry.receptionStartTime + maxReceiveLifetime); + return nextExpirationTime; +} + +ReceiveBuffer::Fragments ReceiveBuffer::removeExpiredFragments(simtime_t currentTime, simtime_t maxReceiveLifetime) +{ + Fragments expiredFragments; + if (maxReceiveLifetime == SIMTIME_MAX) + return expiredFragments; + for (auto it = buffer.begin(); it != buffer.end();) { + auto metadataIt = bufferEntries.find(it->first); + ASSERT(metadataIt != bufferEntries.end()); + auto& entry = metadataIt->second; + if (entry.receiveLifetimeActive && currentTime >= entry.receptionStartTime + maxReceiveLifetime) { + for (auto fragment : it->second) + if (fragment != nullptr) + expiredFragments.push_back(fragment); + length -= it->second.size(); + ASSERT(length >= 0); + expiredFragmentSequences.insert(it->first); + bufferEntries.erase(metadataIt); + it = buffer.erase(it); + } + else + ++it; + } + pruneExpiredFragmentSequences(); + return expiredFragments; +} + +void ReceiveBuffer::setNextExpectedSequenceNumber(SequenceNumberCyclic nextExpectedSequenceNumber) +{ + this->nextExpectedSequenceNumber = nextExpectedSequenceNumber; + pruneExpiredFragmentSequences(); +} + ReceiveBuffer::~ReceiveBuffer() { for (auto fragments : buffer) { diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h index 84e8fd5b6c5..f6af968c519 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h @@ -8,6 +8,9 @@ #ifndef __INET_RECEIVEBUFFER_H #define __INET_RECEIVEBUFFER_H +#include +#include + #include "inet/common/packet/Packet.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" @@ -20,9 +23,23 @@ class INET_API ReceiveBuffer public: typedef std::vector Fragments; typedef std::map ReorderBuffer; + enum class FrameInsertionResult { + INSERTED, + REJECTED, + REJECTED_EXPIRED + }; + static bool isComplete(const Fragments& fragments); protected: + struct BufferEntry { + simtime_t receptionStartTime; + bool hasFragmentedIdentity; + bool receiveLifetimeActive; + }; + ReorderBuffer buffer; + std::map bufferEntries; + std::set expiredFragmentSequences; // For each Block Ack agreement, the recipient maintains a MAC variable NextExpectedSequenceNumber. The // NextExpectedSequenceNumber is initialized to to the value of the Starting Block Ack Starting Sequence // Control field of the ADDBA Request frame of the accepted Block Ack agreement. (IEEE 802.11­-11/0381r0) @@ -30,20 +47,25 @@ class INET_API ReceiveBuffer int length = 0; SequenceNumberCyclic nextExpectedSequenceNumber; + void pruneExpiredFragmentSequences(); + public: ReceiveBuffer(int bufferSize, SequenceNumberCyclic nextExpectedSequenceNumber); virtual ~ReceiveBuffer(); - bool insertFrame(Packet *dataPacket, const Ptr& dataHeader); + FrameInsertionResult insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader); + bool insertFrame(Packet *dataPacket, const Ptr& dataHeader) { return insertFrameWithResult(dataPacket, dataHeader) == FrameInsertionResult::INSERTED; } void dropFramesUntil(SequenceNumberCyclic sequenceNumber); void removeFrame(SequenceNumberCyclic sequenceNumber); Fragments extractFrames(); + simtime_t getNextExpirationTime(simtime_t maxReceiveLifetime) const; + Fragments removeExpiredFragments(simtime_t currentTime, simtime_t maxReceiveLifetime); const ReorderBuffer& getBuffer() { return buffer; } int getLength() { return length; } int getBufferSize() { return bufferSize; } SequenceNumberCyclic getNextExpectedSequenceNumber() { return nextExpectedSequenceNumber; } - void setNextExpectedSequenceNumber(SequenceNumberCyclic nextExpectedSequenceNumber) { this->nextExpectedSequenceNumber = nextExpectedSequenceNumber; } + void setNextExpectedSequenceNumber(SequenceNumberCyclic nextExpectedSequenceNumber); bool isFull() { ASSERT(length <= bufferSize); return length == bufferSize; } }; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h index 14285b3689a..a2187b76f32 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h @@ -50,11 +50,12 @@ class INET_API IReassembly virtual std::vector removeExpiredFragments(simtime_t currentTime) = 0; /** - * Discard fragments from the reassembly buffer. Frames are identified by the transmitter - * address, the TID, and the sequence number range [startSeqNumber, endSeqNumber[. - * Set tid=-1 for non-QoS frames. + * Discard fragments from the reassembly buffer and return the detached + * fragments to the caller. Frames are identified by the transmitter + * address, the TID, and the inclusive sequence number range + * [startSeqNumber, endSeqNumber]. Set tid=-1 for non-QoS frames. */ - virtual void purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) = 0; + virtual std::vector purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc index 23986c4361e..1639fdd3c8a 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc @@ -290,8 +290,9 @@ std::vector BasicReassembly::removeExpiredFragments(simtime_t currentT return expiredFragments; } -void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) +std::vector BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) { + std::vector purgedFragments; auto isInSequenceRange = [startSeqNumber, endSeqNumber](int sequenceNumber) { return startSeqNumber <= endSeqNumber ? sequenceNumber >= startSeqNumber && sequenceNumber <= endSeqNumber : @@ -301,7 +302,8 @@ void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumb auto sequenceNumber = getRawSequenceNumber(it->first.extendedSequenceNumber); if (it->first.macAddress == address && it->first.tid == tid && isInSequenceRange(sequenceNumber)) { for (auto fragment : it->second.fragments) - delete fragment; + if (fragment != nullptr) + purgedFragments.push_back(fragment); it = fragmentsMap.erase(it); } else @@ -324,6 +326,7 @@ void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumb else ++it; } + return purgedFragments; } BasicReassembly::~BasicReassembly() diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h index e100f6ba74f..9d9d572bc08 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h @@ -92,7 +92,7 @@ class INET_API BasicReassembly : public IReassembly, public cObject BasicReassembly(simtime_t maxReceiveLifetime) : maxReceiveLifetime(maxReceiveLifetime) {} virtual ~BasicReassembly(); virtual Packet *addFragment(Packet *packet) override; - virtual void purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) override; + virtual std::vector purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) override; virtual simtime_t getNextExpirationTime() const override; virtual std::vector removeExpiredFragments(simtime_t currentTime) override; }; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc index e903e860596..73df71b3c27 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc @@ -18,8 +18,11 @@ Define_Module(RecipientMacDataService); void RecipientMacDataService::initialize() { + simtime_t maxReceiveLifetime = par("maxReceiveLifetime"); + if (maxReceiveLifetime < SIMTIME_ZERO) + throw cRuntimeError("maxReceiveLifetime must not be negative"); duplicateRemoval = new LegacyDuplicateRemoval(); - basicReassembly = new BasicReassembly(par("maxReceiveLifetime")); + basicReassembly = new BasicReassembly(maxReceiveLifetime); reassemblyTimer = new cMessage("reassemblyTimer"); } diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned index 8e2b3b35f88..aaa4a8cb177 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned @@ -19,7 +19,7 @@ module RecipientMacDataService extends Module { parameters: @class(RecipientMacDataService); - double maxReceiveLifetime @unit(s) = default(524288us); // IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs + double maxReceiveLifetime @unit(s) = default(524288us); // Nonnegative relative receive lifetime; IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs @display("i=block/join"); @signal[packetDefragmented](type=inet::Packet); @signal[packetDeaggregated](type=inet::Packet); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index d7efe6322bd..624c38c448e 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -23,23 +23,26 @@ Define_Module(RecipientQosMacDataService); // TODO refactor to avoid code duplication void RecipientQosMacDataService::initialize() { + maxReceiveLifetime = par("maxReceiveLifetime"); + if (maxReceiveLifetime < SIMTIME_ZERO) + throw cRuntimeError("maxReceiveLifetime must not be negative"); duplicateRemoval = new QoSDuplicateRemoval(); - basicReassembly = new BasicReassembly(par("maxReceiveLifetime")); + basicReassembly = new BasicReassembly(maxReceiveLifetime); aMsduDeaggregation = new MsduDeaggregation(); aMpduDeaggregation = new MpduDeaggregation(); - blockAckReordering = new BlockAckReordering(); - reassemblyTimer = new cMessage("reassemblyTimer"); + blockAckReordering = new BlockAckReordering(maxReceiveLifetime); + receiveLifetimeTimer = new cMessage("receiveLifetimeTimer"); } void RecipientQosMacDataService::handleMessage(cMessage *message) { - if (message != reassemblyTimer) + if (message != receiveLifetimeTimer) throw cRuntimeError("Unknown message"); - expireReassemblyFragments(); - scheduleReassemblyTimer(); + expireReceiveLifetime(); + scheduleReceiveLifetimeTimer(); } -void RecipientQosMacDataService::expireReassemblyFragments() +void RecipientQosMacDataService::expireReceiveLifetime() { for (auto packet : basicReassembly->removeExpiredFragments(simTime())) { PacketDropDetails details; @@ -47,15 +50,21 @@ void RecipientQosMacDataService::expireReassemblyFragments() emit(packetDroppedSignal, packet, &details); delete packet; } + for (auto packet : blockAckReordering->removeExpiredFragments(simTime())) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } } -void RecipientQosMacDataService::scheduleReassemblyTimer() +void RecipientQosMacDataService::scheduleReceiveLifetimeTimer() { - if (reassemblyTimer->isScheduled()) - cancelEvent(reassemblyTimer); - auto nextExpirationTime = basicReassembly->getNextExpirationTime(); + if (receiveLifetimeTimer->isScheduled()) + cancelEvent(receiveLifetimeTimer); + auto nextExpirationTime = std::min(basicReassembly->getNextExpirationTime(), blockAckReordering->getNextExpirationTime()); if (nextExpirationTime != SIMTIME_MAX) - scheduleAt(nextExpirationTime, reassemblyTimer); + scheduleAt(nextExpirationTime, receiveLifetimeTimer); } void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress originatorAddr) @@ -71,11 +80,21 @@ void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress ori delete packet; } } + if (basicReassembly) { + auto droppedFragments = basicReassembly->purge(originatorAddr, tid, 0, 4095); + for (auto packet : droppedFragments) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + scheduleReceiveLifetimeTimer(); } Packet *RecipientQosMacDataService::defragment(std::vector completeFragments) { - expireReassemblyFragments(); + expireReceiveLifetime(); Packet *defragmentedPacket = nullptr; for (auto fragment : completeFragments) { auto packet = basicReassembly->addFragment(fragment); @@ -84,7 +103,7 @@ Packet *RecipientQosMacDataService::defragment(std::vector completeFra break; } } - scheduleReassemblyTimer(); + scheduleReceiveLifetimeTimer(); if (defragmentedPacket != nullptr) emit(packetDefragmentedSignal, defragmentedPacket); return defragmentedPacket; @@ -92,9 +111,9 @@ Packet *RecipientQosMacDataService::defragment(std::vector completeFra Packet *RecipientQosMacDataService::defragment(Packet *mgmtFragment) { - expireReassemblyFragments(); + expireReceiveLifetime(); auto packet = basicReassembly->addFragment(mgmtFragment); - scheduleReassemblyTimer(); + scheduleReceiveLifetimeTimer(); if (packet && packet->hasAtFront()) { emit(packetDefragmentedSignal, packet); return packet; @@ -107,6 +126,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data { Enter_Method("dataFrameReceived"); take(dataPacket); + expireReceiveLifetime(); // TODO A-MPDU Deaggregation, MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(dataHeader)) { EV_WARN << "Dropping duplicate packet " << *dataPacket << ".\n"; @@ -114,6 +134,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data details.setReason(DUPLICATE_DETECTED); emit(packetDroppedSignal, dataPacket, &details); delete dataPacket; + scheduleReceiveLifetimeTimer(); return std::vector(); } BlockAckReordering::ReorderBuffer frames; @@ -122,8 +143,16 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data Tid tid = dataHeader->getTid(); MacAddress originatorAddr = dataHeader->getTransmitterAddress(); RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); - if (agreement) - frames = blockAckReordering->processReceivedQoSFrame(agreement, dataPacket, dataHeader); + if (agreement) { + auto processingResult = blockAckReordering->processReceivedQoSFrameWithResult(agreement, dataPacket, dataHeader); + frames = processingResult.frames; + for (auto packet : processingResult.tombstonedFragments) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } } std::vector defragmentedFrames; if (basicReassembly) { // FIXME defragmentation @@ -143,6 +172,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data else ; // TODO drop? } } + scheduleReceiveLifetimeTimer(); std::vector deaggregatedFrames; if (aMsduDeaggregation) { for (auto defragmentedFrame : defragmentedFrames) { @@ -166,16 +196,21 @@ IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataS { Enter_Method("managementFrameReceived"); take(mgmtPacket); + expireReceiveLifetime(); // TODO MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) { delete mgmtPacket; // A duplicate fragment is acknowledged by HCF but is not a complete // MMPDU. Preserve the existing subtype-specific handling only for an // unfragmented duplicate management frame. - if (mgmtHeader->getFragmentNumber() == 0 && !mgmtHeader->getMoreFragments()) + if (mgmtHeader->getFragmentNumber() == 0 && !mgmtHeader->getMoreFragments()) { + scheduleReceiveLifetimeTimer(); return { {}, mgmtHeader, true }; - else + } + else { + scheduleReceiveLifetimeTimer(); return { {}, nullptr, true }; + } } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); @@ -186,15 +221,19 @@ IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataS // TODO Defrag, MSDU Integrity, Replay Detection, RX MSDU Rate Limiting if (dynamicPtrCast(completeHeader)) { delete mgmtPacket; + scheduleReceiveLifetimeTimer(); return { {}, completeHeader, false }; } - else + else { + scheduleReceiveLifetimeTimer(); return { { mgmtPacket }, completeHeader, false }; + } } std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) { Enter_Method("controlFrameReceived"); + expireReceiveLifetime(); if (auto blockAckReq = dynamicPtrCast(controlHeader)) { BlockAckReordering::ReorderBuffer frames; if (blockAckReordering) { @@ -203,8 +242,10 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); if (agreement) frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReq); - else + else { + scheduleReceiveLifetimeTimer(); return std::vector(); + } } std::vector defragmentedFrames; if (basicReassembly) { // FIXME defragmentation @@ -239,14 +280,16 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c } } // TODO MSDU Integrity, Replay Detection, RX MSDU Rate Limiting + scheduleReceiveLifetimeTimer(); return deaggregatedFrames; } + scheduleReceiveLifetimeTimer(); return std::vector(); } RecipientQosMacDataService::~RecipientQosMacDataService() { - cancelAndDelete(reassemblyTimer); + cancelAndDelete(receiveLifetimeTimer); delete duplicateRemoval; delete basicReassembly; delete aMsduDeaggregation; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index 76b27d8cbc7..b3c6cdc5d4f 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -27,7 +27,8 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, { protected: IReassembly *basicReassembly = nullptr; - cMessage *reassemblyTimer = nullptr; + cMessage *receiveLifetimeTimer = nullptr; + simtime_t maxReceiveLifetime = SIMTIME_MAX; IMpduDeaggregation *aMpduDeaggregation = nullptr; // MpduHeaderAndFcsValidation *mpduHeaderAndFcsValidation = nullptr; @@ -44,8 +45,8 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, virtual void initialize() override; virtual void handleMessage(cMessage *message) override; - virtual void expireReassemblyFragments(); - virtual void scheduleReassemblyTimer(); + virtual void expireReceiveLifetime(); + virtual void scheduleReceiveLifetimeTimer(); virtual Packet *defragment(std::vector completeFragments); virtual Packet *defragment(Packet *mgmtFragment); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned index d02d7e7ff00..49af399d6b6 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned @@ -22,7 +22,7 @@ module RecipientQosMacDataService extends Module { parameters: @class(RecipientQosMacDataService); - double maxReceiveLifetime @unit(s) = default(524288us); // IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs + double maxReceiveLifetime @unit(s) = default(524288us); // Nonnegative relative receive lifetime; IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs @display("i=block/join"); @signal[packetDefragmented](type=inet::Packet); @signal[packetDeaggregated](type=inet::Packet); diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 99b39c46bc3..e33fd1ff2bf 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -38,6 +38,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" @@ -185,6 +186,8 @@ class TestRecipientHandler : public RecipientBlockAckAgreementHandler class TestBlockAckReordering : public BlockAckReordering { public: + using BlockAckReordering::BlockAckReordering; + int getNumReceiveBuffers() const { return receiveBuffers.size(); } ReceiveBuffer *getReceiveBuffer(Tid tid, MacAddress originatorAddress) const { auto it = receiveBuffers.find(std::make_pair(tid, originatorAddress)); @@ -1013,6 +1016,30 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const return check_and_cast(module); }; +// maxReceiveLifetime is a relative duration and must be nonnegative before +// either recipient allocates a receive-lifetime consumer. Zero remains valid. +auto assertMaxReceiveLifetimeValidation = [this](const char *typeName, const char *namePrefix) { + auto invalidModule = cModuleType::get(typeName)->create((std::string(namePrefix) + "Negative").c_str(), this); + invalidModule->par("maxReceiveLifetime").setDoubleValue(-1e-6); + bool threw = false; + try { + invalidModule->callInitialize(); + } + catch (cRuntimeError&) { + threw = true; + } + ASSERT(threw); + invalidModule->deleteModule(); + + auto zeroModule = cModuleType::get(typeName)->create((std::string(namePrefix) + "Zero").c_str(), this); + zeroModule->par("maxReceiveLifetime").setDoubleValue(0); + zeroModule->callInitialize(); + zeroModule->callFinish(); + zeroModule->deleteModule(); +}; +assertMaxReceiveLifetimeValidation("inet.linklayer.ieee80211.mac.recipient.RecipientMacDataService", "recipientMacDataServiceLifetime"); +assertMaxReceiveLifetimeValidation("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService", "recipientQosMacDataServiceLifetime"); + { TestOriginatorHandler tokenHandler; tokenHandler.setNextDialogToken(255); @@ -2477,6 +2504,174 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; reassembleOrderedFragments(barFragments, { 0x50, 0x51, 0x52, 0x53 }); } +// Receive-buffer lifetime starts when the first fragment of an incomplete +// fragmented MPDU is retained. Later fragments do not refresh that deadline; +// complete MPDUs held only for ordering remain available until normal window +// processing releases them. +{ + auto makeReorderFragment = [&](const char *name, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer2, 6, sequenceNumber); + dataHeader->setTransmitterAddress(peer1); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + + auto lifetime = SimTime(1, SIMTIME_US); + RecipientBlockAckAgreement agreement(peer1, 6, SequenceNumberCyclic(100), 64, 0); + TestBlockAckReordering reordering(lifetime); + auto firstFragment = makeReorderFragment("receiveLifetimeFirstFragment", SequenceNumberCyclic(100), 1, true, 0x61); + auto firstReceptionTime = simTime(); + ASSERT(reordering.processReceivedQoSFrame(&agreement, firstFragment, firstFragment->peekAtFront()).empty()); + auto firstDeadline = reordering.getNextExpirationTime(); + ASSERT(firstDeadline == firstReceptionTime + lifetime); + wait(0.5e-6); + auto laterFragment = makeReorderFragment("receiveLifetimeLaterFragment", SequenceNumberCyclic(100), 2, false, 0x62); + ASSERT(reordering.processReceivedQoSFrame(&agreement, laterFragment, laterFragment->peekAtFront()).empty()); + ASSERT(reordering.getNextExpirationTime() == firstDeadline); + + auto completeEntry = makeReorderFragment("receiveLifetimeCompleteEntry", SequenceNumberCyclic(101), 0, false, 0x63); + ASSERT(reordering.processReceivedQoSFrame(&agreement, completeEntry, completeEntry->peekAtFront()).empty()); + auto receiveBuffer = reordering.getReceiveBuffer(6, peer1); + ASSERT(receiveBuffer->getLength() == 3); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(100)); + auto expiredFragments = reordering.removeExpiredFragments(firstDeadline); + ASSERT(expiredFragments.size() == 2); + ASSERT(receiveBuffer->getLength() == 1); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(100)); + ASSERT(receiveBuffer->getBuffer().find(101) != receiveBuffer->getBuffer().end()); + for (auto packet : expiredFragments) + delete packet; + + auto lateFragment = makeReorderFragment("receiveLifetimeLateFragment", SequenceNumberCyclic(100), 0, true, 0x64); + ASSERT(reordering.processReceivedQoSFrame(&agreement, lateFragment, lateFragment->peekAtFront()).empty()); + ASSERT(receiveBuffer->getLength() == 1); + auto retainedCompleteEntry = reordering.resetReceiveBuffer(6, peer1); + ASSERT(retainedCompleteEntry.size() == 1); + delete retainedCompleteEntry.front(); + + // The tombstone is retired by normal modulo-window progress, so a later + // complete wrap can reuse the raw sequence number without permanent loss. + RecipientBlockAckAgreement wrapAgreement(peer1, 6, SequenceNumberCyclic(4094), 64, 0); + TestBlockAckReordering wrapReordering(lifetime); + auto wrapFragment = makeReorderFragment("receiveLifetimeWrapFragment", SequenceNumberCyclic(4095), 1, false, 0x65); + ASSERT(wrapReordering.processReceivedQoSFrame(&wrapAgreement, wrapFragment, wrapFragment->peekAtFront()).empty()); + auto wrapDeadline = wrapReordering.getNextExpirationTime(); + auto expiredWrapFragments = wrapReordering.removeExpiredFragments(wrapDeadline); + ASSERT(expiredWrapFragments.size() == 1); + delete expiredWrapFragments.front(); + auto wrapReceiveBuffer = wrapReordering.getReceiveBuffer(6, peer1); + wrapReceiveBuffer->setNextExpectedSequenceNumber(SequenceNumberCyclic(0)); + for (int sequenceNumber = 1; sequenceNumber <= 4095; sequenceNumber++) + wrapReceiveBuffer->setNextExpectedSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + auto reusedWrapFragment = makeReorderFragment("receiveLifetimeReusedWrapFragment", SequenceNumberCyclic(4095), 0, true, 0x66); + ASSERT(wrapReordering.processReceivedQoSFrame(&wrapAgreement, reusedWrapFragment, reusedWrapFragment->peekAtFront()).empty()); + auto reusedWrapFragments = wrapReordering.resetReceiveBuffer(6, peer1); + ASSERT(reusedWrapFragments.size() == 1); + delete reusedWrapFragments.front(); +} + +// Different terminal markers for one sequence cannot be interpreted as a +// complete MPDU. Keep the entry incomplete and expiring instead of releasing +// a hybrid or clearing its receive-lifetime deadline. +{ + auto makeContradictoryFragment = [&](const char *name, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(250)); + dataHeader->setTransmitterAddress(peer1); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto lifetime = SimTime(1, SIMTIME_US); + RecipientBlockAckAgreement agreement(peer1, 6, SequenceNumberCyclic(250), 64, 0); + TestBlockAckReordering reordering(lifetime); + auto finalFragment = makeContradictoryFragment("contradictoryFinalFragment", 1, false, 0x67); + auto receptionTime = simTime(); + ASSERT(reordering.processReceivedQoSFrame(&agreement, finalFragment, finalFragment->peekAtFront()).empty()); + auto fragmentZero = makeContradictoryFragment("contradictoryFragmentZero", 0, false, 0x68); + ASSERT(reordering.processReceivedQoSFrame(&agreement, fragmentZero, fragmentZero->peekAtFront()).empty()); + ASSERT(reordering.getNextExpirationTime() == receptionTime + lifetime); + auto receiveBuffer = reordering.getReceiveBuffer(6, peer1); + ASSERT(receiveBuffer->getLength() == 2); + auto expiredFragments = reordering.removeExpiredFragments(receptionTime + lifetime); + ASSERT(expiredFragments.size() == 2); + for (auto packet : expiredFragments) + delete packet; + ASSERT(receiveBuffer->getLength() == 0); +} + +// A retained reorder-buffer fragment arms the recipient's common lifetime +// timer. Reorder expiry and BasicReassembly expiry are staggered, and every +// detached packet is reported exactly once as OTHER_PACKET_DROP. +{ + auto serviceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("reorderLifetimeDataService", this); + auto lifetime = SimTime(2, SIMTIME_US); + serviceModule->par("maxReceiveLifetime").setDoubleValue(lifetime.dbl()); + serviceModule->callInitialize(); + auto service = check_and_cast(serviceModule); + TestPacketDropSignalListener dropListener; + service->subscribe(packetDroppedSignal, &dropListener); + + RecipientBlockAckAgreementHandler agreementHandler; + TestRecipientPolicy policy; + TestCallback callback; + auto request = makeRequest(peer1, 6, 92, SequenceNumberCyclic(300), 64); + auto agreement = agreementHandler.processReceivedAddbaRequest(request, &policy, &callback, &callback); + ASSERT(agreement != nullptr); + + auto makeServiceFragment = [&](const char *name, MacAddress transmitterAddress, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer2, 6, sequenceNumber); + dataHeader->setTransmitterAddress(transmitterAddress); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + + auto reorderFirstFragment = makeServiceFragment("serviceReorderFirstFragment", peer1, SequenceNumberCyclic(300), 1, true, 0x71); + ASSERT(service->dataFrameReceived(reorderFirstFragment, reorderFirstFragment->peekAtFront(), &agreementHandler).empty()); + auto reorderFinalFragment = makeServiceFragment("serviceReorderFinalFragment", peer1, SequenceNumberCyclic(300), 2, false, 0x72); + ASSERT(service->dataFrameReceived(reorderFinalFragment, reorderFinalFragment->peekAtFront(), &agreementHandler).empty()); + ASSERT(dropListener.numSignals == 0); + wait(0.5e-6); + + // No agreement for peer3: this fragment is retained by BasicReassembly, + // with a later deadline than the already retained reorder fragments. + auto basicFragment = makeServiceFragment("serviceBasicFragment", peer3, SequenceNumberCyclic(350), 0, true, 0x73); + ASSERT(service->dataFrameReceived(basicFragment, basicFragment->peekAtFront(), &agreementHandler).empty()); + wait(1.6e-6); + ASSERT(dropListener.numSignals == 2); + ASSERT(dropListener.numOtherPacketDrops == 2); + + // The expired sequence remains tombstoned until the reorder window moves + // past it. A late fragment is rejected and reported once by the service, + // while no hybrid or partial frame is delivered. + auto lateReorderFragment = makeServiceFragment("serviceLateReorderFragment", peer1, SequenceNumberCyclic(300), 0, true, 0x74); + ASSERT(service->dataFrameReceived(lateReorderFragment, lateReorderFragment->peekAtFront(), &agreementHandler).empty()); + ASSERT(dropListener.numSignals == 3); + ASSERT(dropListener.numOtherPacketDrops == 3); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(300), 0)); + wait(0.3e-6); + ASSERT(dropListener.numSignals == 3); + wait(0.2e-6); + ASSERT(dropListener.numSignals == 4); + ASSERT(dropListener.numOtherPacketDrops == 4); + service->unsubscribe(packetDroppedSignal, &dropListener); + serviceModule->callFinish(); + serviceModule->deleteModule(); +} + // The recipient data service owns reorder-buffer reset observability: every // buffered MPDU is reported exactly once before it is deleted. { @@ -2490,11 +2685,46 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; auto bufferedPacket = new Packet("signaledOldWindowPacket", bufferedHeader); auto signaledOldWindowFrames = recipientDataService->dataFrameReceived(bufferedPacket, bufferedHeader, &immediateRecipientHandler); ASSERT(signaledOldWindowFrames.empty()); + + auto makeBasicFragment = [&](const char *name, MacAddress transmitterAddress, Tid tid, int sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer1, tid, SequenceNumberCyclic(sequenceNumber)); + dataHeader->setTransmitterAddress(transmitterAddress); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto targetFirst = makeBasicFragment("signaledTargetBasicFragment", peer2, 7, 20, 0, true, 0x81); + ASSERT(recipientDataService->dataFrameReceived(targetFirst, targetFirst->peekAtFront(), &immediateRecipientHandler).empty()); + auto samePeerDifferentTidFirst = makeBasicFragment("signaledDifferentTidBasicFragment", peer2, 8, 21, 0, true, 0x82); + ASSERT(recipientDataService->dataFrameReceived(samePeerDifferentTidFirst, samePeerDifferentTidFirst->peekAtFront(), &immediateRecipientHandler).empty()); + auto differentPeerSameTidFirst = makeBasicFragment("signaledDifferentPeerBasicFragment", peer3, 7, 22, 0, true, 0x83); + ASSERT(recipientDataService->dataFrameReceived(differentPeerSameTidFirst, differentPeerSameTidFirst->peekAtFront(), &immediateRecipientHandler).empty()); + recipientDataService->resetBlockAckReordering(6, peer1); ASSERT(dropListener.numSignals == 1); ASSERT(dropListener.numOtherPacketDrops == 1); + // Resetting one originator/TID agreement also purges its BasicReassembly + // state, while retaining fragments for a different TID or peer. + recipientDataService->resetBlockAckReordering(7, peer2); + ASSERT(dropListener.numSignals == 2); + ASSERT(dropListener.numOtherPacketDrops == 2); recipientDataService->resetBlockAckReordering(6, peer1); - ASSERT(dropListener.numSignals == 1); + recipientDataService->resetBlockAckReordering(7, peer2); + ASSERT(dropListener.numSignals == 2); + auto samePeerDifferentTidFinal = makeBasicFragment("signaledDifferentTidBasicFinal", peer2, 8, 21, 1, false, 0x92); + auto samePeerDifferentTidFrames = recipientDataService->dataFrameReceived(samePeerDifferentTidFinal, + samePeerDifferentTidFinal->peekAtFront(), &immediateRecipientHandler); + ASSERT(samePeerDifferentTidFrames.size() == 1); + delete samePeerDifferentTidFrames.front(); + auto differentPeerSameTidFinal = makeBasicFragment("signaledDifferentPeerBasicFinal", peer3, 7, 22, 1, false, 0x93); + auto differentPeerSameTidFrames = recipientDataService->dataFrameReceived(differentPeerSameTidFinal, + differentPeerSameTidFinal->peekAtFront(), &immediateRecipientHandler); + ASSERT(differentPeerSameTidFrames.size() == 1); + delete differentPeerSameTidFrames.front(); recipientDataService->unsubscribe(packetDroppedSignal, &dropListener); recipientDataServiceModule->callFinish(); recipientDataServiceModule->deleteModule(); @@ -5048,6 +5278,16 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { ASSERT(completed->peekAt(completedResponse->getChunkLength(), B(newPayload.size()))->getBytes() == newPayload); delete completed; delete recoveredFragments; + + // purge() remains the explicit agreement/lifecycle boundary and returns + // detached fragments to its caller for observable drop handling. + auto purgeFragments = makeResponseFragments("reassemblyPurgedResponse", 31, 41, { 0xb0, 0xb1, 0xb2 }, 4094, 2); + ASSERT(reassembly.addFragment(purgeFragments->at(0)) == nullptr); + auto purgedFragments = reassembly.purge(peer1, -1, 4094, 4094); + ASSERT(purgedFragments.size() == 1); + delete purgedFragments.front(); + delete purgeFragments->at(1); + delete purgeFragments; } // A terminal duplicate on an occupied nonterminal slot is still observed for From cfe1c02351a1c405b2cce33e23f4372fa4ff394e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:05 +0200 Subject: [PATCH 11/16] ieee80211: quarantine expired Block Ack agreements An inactivity-expired Block Ack agreement must remain installed until its generation-matched DELBA teardown completes. Previously, data-plane users treated that retained object as active, so they could continue selecting Block Ack, sending BARs, buffering frames, and producing Block Ack responses after expiry. Separate raw lifecycle lookup from active agreement lookup and use the active form throughout originator and recipient data paths. Fall back to Normal Ack, suppress BAR and Block Ack processing, discard Block-Ack-policy data without mutating reorder state, and ignore late responses once an agreement is unavailable. Release matching originator acknowledgement state for retry when an agreement expires or is removed. Also cover the race where a frame finishes transmission after expiry, while preserving the retained agreement object for generation-safe DELBA handling. Extend the ADDBA transaction tests with active-versus-expired policy, selection, teardown, reordering, late-frame, and transmission-completion cases. Remove the trailing blank line from the QoS example configuration. Validation completed in debug mode with the full build, the focused ADDBA unit test, the Block Ack inactivity-timer module test, and git diff --check. --- .../OriginatorBlockAckAgreementHandler.cc | 25 +- .../OriginatorBlockAckAgreementHandler.h | 3 +- .../RecipientBlockAckAgreementHandler.cc | 19 +- .../RecipientBlockAckAgreementHandler.h | 3 +- .../blockack/RecipientBlockAckProcedure.cc | 3 +- .../IBlockAckAgreementHandlerCallback.h | 6 + .../IOriginatorBlockAckAgreementHandler.h | 6 +- .../mac/contract/IOriginatorQoSAckPolicy.h | 7 +- .../IRecipientBlockAckAgreementHandler.h | 6 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 38 ++- .../ieee80211/mac/coordinationfunction/Hcf.h | 2 + .../framesequence/PrimitiveFrameSequences.cc | 3 +- .../ieee80211/mac/framesequence/TxOpFs.cc | 5 +- .../mac/originator/OriginatorQosAckPolicy.cc | 54 +-- .../mac/originator/OriginatorQosAckPolicy.h | 7 +- .../ieee80211/mac/originator/QosAckHandler.cc | 17 +- .../ieee80211/mac/originator/QosAckHandler.h | 5 +- .../mac/recipient/RecipientQosAckPolicy.cc | 4 +- .../recipient/RecipientQosMacDataService.cc | 19 +- tests/unit/Ieee80211AddbaTransaction_1.test | 308 +++++++++++++++++- 20 files changed, 470 insertions(+), 70 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index ad22a8fce99..f69625097c3 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -98,26 +98,31 @@ void OriginatorBlockAckAgreementHandler::addbaResponseTimeoutExpired(IOriginator scheduleAddbaResponseTimer(callback); } -void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) +bool OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the // peer STA with the Reason Code field set to TIMEOUT and shall issue a MLME-DELBA.indication // primitive with the ReasonCode parameter having a value of TIMEOUT. // The procedure is illustrated in Figure 10-14. simtime_t now = simTime(); + bool expired = false; for (auto id : blockAckAgreements) { auto agreement = id.second; if (agreement->getIsAddbaResponseReceived() && !agreement->isInactivityExpired() && agreement->getExpirationTime() <= now) { agreement->markInactivityExpired(); MacAddress receiverAddr = id.first.first; Tid tid = id.first.second; + if (agreementHandlerCallback != nullptr) + agreementHandlerCallback->releaseBlockAckAgreementFrames(receiverAddr, tid); const auto& delba = buildDelba(receiverAddr, tid, 39); auto delbaPacket = new Packet("Delba", delba); delbaPacket->addTag()->setGenerationId(agreement->getTransactionId()); procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes + expired = true; } } scheduleInactivityTimer(agreementHandlerCallback); + return expired; } const Ptr OriginatorBlockAckAgreementHandler::buildAddbaRequest(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) @@ -144,8 +149,8 @@ const Ptr OriginatorBlockAckAgreementHandler::buildAddbaR void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const Ptr& blockAck, IBlockAckAgreementHandlerCallback *callback) { if (auto basicBlockAck = dynamicPtrCast(blockAck)) { - auto agreement = getAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); - if (agreement && !agreement->isInactivityExpired()) { + auto agreement = getActiveAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); + if (agreement != nullptr) { agreement->setStartingSequenceNumber(basicBlockAck->getStartingSequenceNumber()); agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); @@ -174,6 +179,12 @@ OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(Ma return it != blockAckAgreements.end() ? it->second : nullptr; } +OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getActiveAgreement(MacAddress receiverAddr, Tid tid) +{ + auto agreement = getAgreement(receiverAddr, tid); + return agreement != nullptr && agreement->getIsAddbaResponseReceived() && !agreement->isInactivityExpired() ? agreement : nullptr; +} + bool OriginatorBlockAckAgreementHandler::isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const { auto it = blockAckAgreements.find(std::make_pair(receiverAddr, tid)); @@ -363,6 +374,8 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: return nullptr; bool cancelPendingTransaction = agreement->isPending(); std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + if (terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getReceiverAddress(), delba->getTid()); scheduleInactivityTimer(callback); pendingTeardownTransactionIds[std::make_pair(delba->getReceiverAddress(), delba->getTid())] = generationId; scheduleAddbaResponseTimer(callback); @@ -375,6 +388,8 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + if (terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getReceiverAddress(), delba->getTid()); scheduleInactivityTimer(callback); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) @@ -420,6 +435,8 @@ OriginatorBlockAckAgreementAbortResult OriginatorBlockAckAgreementHandler::proce OriginatorBlockAckAgreementAbortResult result; result.handled = true; result.terminatedAgreement.reset(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + if (result.terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getReceiverAddress(), delba->getTid()); scheduleInactivityTimer(callback); if (callback != nullptr) callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId(), packet); @@ -441,6 +458,8 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: if (pendingTeardownIt != pendingTeardownTransactionIds.end()) pendingTeardownTransactionIds.erase(pendingTeardownIt); std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + if (terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getTransmitterAddress(), delba->getTid()); scheduleInactivityTimer(callback); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 4b155bbbe34..96e6ca12721 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -54,10 +54,11 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; virtual OriginatorBlockAckAgreement *getAgreement(MacAddress receiverAddr, Tid tid) override; + virtual OriginatorBlockAckAgreement *getActiveAgreement(MacAddress receiverAddr, Tid tid) override; virtual bool isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const override; virtual bool isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const override; virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 5648b95a90a..e8a6cce5749 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -41,8 +41,8 @@ void RecipientBlockAckAgreementHandler::qosFrameReceived(const PtrgetAckPolicy() == AckPolicy::BLOCK_ACK) { // TODO + Implicit Block Ack Tid tid = qosHeader->getTid(); MacAddress originatorAddr = qosHeader->getTransmitterAddress(); - auto agreement = getAgreement(tid, originatorAddr); - if (agreement && !agreement->isInactivityExpired()) { + auto agreement = getActiveAgreement(tid, originatorAddr); + if (agreement != nullptr) { agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); } @@ -53,20 +53,21 @@ void RecipientBlockAckAgreementHandler::qosFrameReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) { - auto agreement = getAgreement(blockAckReq->getTidInfo(), blockAckReq->getTransmitterAddress()); - if (agreement != nullptr && !agreement->isInactivityExpired()) { + auto agreement = getActiveAgreement(blockAckReq->getTidInfo(), blockAckReq->getTransmitterAddress()); + if (agreement != nullptr) { agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); } } -void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) +bool RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the // peer STA with the Reason Code field set to TIMEOUT and shall issue a MLME-DELBA.indication // primitive with the ReasonCode parameter having a value of TIMEOUT. // The procedure is illustrated in Figure 10-14. simtime_t now = simTime(); + bool expired = false; for (auto id : blockAckAgreements) { auto agreement = id.second; if (!agreement->isInactivityExpired() && agreement->getExpirationTime() <= now) { @@ -77,9 +78,11 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb auto delbaPacket = new Packet("Delba", delba); delbaPacket->addTag()->setGenerationId(agreement->getGenerationId()); procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes + expired = true; } } scheduleInactivityTimer(agreementHandlerCallback); + return expired; } // @@ -142,6 +145,12 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getAgreement(Tid return it != blockAckAgreements.end() ? it->second : nullptr; } +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getActiveAgreement(Tid tid, MacAddress originatorAddr) +{ + auto agreement = getAgreement(tid, originatorAddr); + return agreement != nullptr && !agreement->isInactivityExpired() ? agreement : nullptr; +} + RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { EV_INFO << "Processing Addba Request from " << addbaRequest->getTransmitterAddress() << endl; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index da90268c368..7c4ce21b5b9 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -50,9 +50,10 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) override; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; + virtual RecipientBlockAckAgreement *getActiveAgreement(Tid tid, MacAddress originatorAddr) override; virtual uint64_t getPendingTeardownGenerationId(Tid tid, MacAddress originatorAddr) const override; virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const override; }; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc index ab7cb050e5f..6796539c4f5 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc @@ -21,7 +21,7 @@ void RecipientBlockAckProcedure::processReceivedBlockAckReq(Packet *blockAckPack { numReceivedBlockAckReq++; if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) { - auto agreement = blockAckAgreementHandler->getAgreement(basicBlockAckReq->getTidInfo(), basicBlockAckReq->getTransmitterAddress()); + auto agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(basicBlockAckReq->getTidInfo(), basicBlockAckReq->getTransmitterAddress()); if (ackPolicy->isBlockAckNeeded(basicBlockAckReq, agreement)) { auto blockAck = buildBlockAck(basicBlockAckReq, agreement); auto duration = ackPolicy->computeBasicBlockAckDurationField(blockAckPacketReq, basicBlockAckReq); @@ -71,4 +71,3 @@ const Ptr RecipientBlockAckProcedure::buildBlockAck(const Ptr } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index cd2a3859699..f73bcda128e 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -34,6 +34,12 @@ class INET_API IBlockAckAgreementHandlerCallback virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) = 0; virtual void scheduleAddbaResponseTimer(simtime_t deadline) = 0; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) = 0; + // Marks originator data frames that were waiting for this agreement's + // Block Ack exchange as eligible for retry with an alternative + // acknowledgment policy. This is needed whenever an agreement becomes + // unavailable, including before its retained teardown object is removed. + // The return value reports whether any ACK state changed. + virtual bool releaseBlockAckAgreementFrames(MacAddress peerAddress, Tid tid) { return false; } // Removes queued siblings of a sender-local DELBA without assuming that // the frame is still removable from the active frame sequence. The // agreement owner remains responsible for rejecting stale packets. diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h index 9a1a2af753e..64a8302f9db 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h @@ -58,10 +58,14 @@ class INET_API IOriginatorBlockAckAgreementHandler // transaction and sibling packets were cancelled through the callback. virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; virtual OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; + virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; virtual OriginatorBlockAckAgreement *getAgreement(MacAddress receiverAddr, Tid tid) = 0; + // Returns the installed agreement only while it can be used by the data + // plane. Lifecycle code must use getAgreement() to retain generation-safe + // teardown state after inactivity expiry. + virtual OriginatorBlockAckAgreement *getActiveAgreement(MacAddress receiverAddr, Tid tid) = 0; virtual bool isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const = 0; virtual bool isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const = 0; virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const { return true; } diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h index 8f03c2fd052..18b934badc5 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h @@ -16,6 +16,8 @@ namespace inet { namespace ieee80211 { +class IOriginatorBlockAckAgreementHandler; + class INET_API IOriginatorQoSAckPolicy { public: @@ -23,9 +25,9 @@ class INET_API IOriginatorQoSAckPolicy virtual bool isAckNeeded(const Ptr& header) const = 0; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const = 0; - virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const = 0; + virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const = 0; virtual bool isBlockAckPolicyEligibleFrame(Packet *packet, const Ptr& header) const = 0; - virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const = 0; + virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const = 0; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const = 0; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const = 0; @@ -35,4 +37,3 @@ class INET_API IOriginatorQoSAckPolicy } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 31f6b478078..8811e70f411 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -45,9 +45,13 @@ class INET_API IRecipientBlockAckAgreementHandler virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return {}; } virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; + virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) = 0; + // Returns the installed agreement only while it can be used by the data + // plane. Lifecycle code must use getAgreement() to retain generation-safe + // teardown state after inactivity expiry. + virtual RecipientBlockAckAgreement *getActiveAgreement(Tid tid, MacAddress originatorAddr) = 0; virtual uint64_t getPendingTeardownGenerationId(Tid, MacAddress) const { return 0; } virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const { return true; } }; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 0a36f69d9e8..e2f25d68921 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -338,8 +338,14 @@ void Hcf::handleMessage(cMessage *msg) } else if (msg == inactivityTimer) { if (originatorBlockAckAgreementHandler && recipientBlockAckAgreementHandler) { - originatorBlockAckAgreementHandler->blockAckAgreementExpired(this, this); - recipientBlockAckAgreementHandler->blockAckAgreementExpired(this, this); + blockAckInactivityExpiryInProgress = true; + bool changed = originatorBlockAckAgreementHandler->blockAckAgreementExpired(this, this); + changed |= recipientBlockAckAgreementHandler->blockAckAgreementExpired(this, this); + blockAckInactivityExpiryInProgress = false; + if (changed) { + rebuildPendingFrameEligibility(); + resumeEligibleChannelAccess(); + } } else throw cRuntimeError("Unknown event"); @@ -392,7 +398,7 @@ void Hcf::processUpperFrame(Packet *packet, const PtrgetEdcaf(ac)->getPendingQueue(); trackPendingFrame(packet, ac); pendingQueue->enqueuePacket(packet); - if (hasFrameToTransmit(ac)) { + if (!blockAckInactivityExpiryInProgress && hasFrameToTransmit(ac)) { auto edcaf = edca->getChannelOwner(); if (edcaf == nullptr || edcaf->getAccessCategory() != ac) { EV_DETAIL << "Requesting channel for access category " << printAccessCategory(ac) << endl; @@ -479,6 +485,20 @@ void Hcf::scheduleAddbaResponseTimer(simtime_t deadline) rescheduleAt(deadline, addbaResponseTimer); } +bool Hcf::releaseBlockAckAgreementFrames(MacAddress peerAddress, Tid tid) +{ + Enter_Method("releaseBlockAckAgreementFrames"); + if (edca == nullptr) + return false; + bool changed = false; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto edcaf = edca->getEdcaf(AccessCategory(ac)); + if (edcaf != nullptr && edcaf->getAckHandler() != nullptr) + changed |= edcaf->getAckHandler()->releaseBlockAckAgreementFrames(peerAddress, tid); + } + return changed; +} + void Hcf::cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) { Enter_Method("cancelAddbaTransaction"); @@ -993,6 +1013,9 @@ void Hcf::originatorProcessTransmittedDataFrame(Packet *packet, const PtrgetEdcaf(ac); edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(dataHeader); + if (dataHeader->getAckPolicy() == BLOCK_ACK && + (originatorBlockAckAgreementHandler == nullptr || originatorBlockAckAgreementHandler->getActiveAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid()) == nullptr)) + edcaf->getAckHandler()->releaseBlockAckAgreementFrames(dataHeader->getReceiverAddress(), dataHeader->getTid()); if (dataHeader->getAckPolicy() == NO_ACK) edcaf->getInProgressFrames()->dropFrame(packet); } @@ -1219,10 +1242,13 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(header)) { EV_INFO << "BasicBlockAck has arrived" << std::endl; + if (originatorBlockAckAgreementHandler == nullptr || originatorBlockAckAgreementHandler->getActiveAgreement(blockAck->getTransmitterAddress(), blockAck->getTidInfo()) == nullptr) { + EV_INFO << "Ignoring BasicBlockAck without an active Block Ack agreement.\n"; + return; + } edcaf->getRecoveryProcedure()->blockAckFrameReceived(); auto ackedSeqAndFragNums = edcaf->getAckHandler()->processReceivedBlockAck(blockAck); - if (originatorBlockAckAgreementHandler) - originatorBlockAckAgreementHandler->processReceivedBlockAck(blockAck, this); + originatorBlockAckAgreementHandler->processReceivedBlockAck(blockAck, this); EV_TRACE << "It has acknowledged the following frames:" << std::endl; for (auto it : ackedSeqAndFragNums) EV_TRACE << " sequenceNumber = " << it.second.second.getSequenceNumber() << ", fragmentNumber = " << (int)it.second.second.getFragmentNumber() << std::endl; @@ -1295,7 +1321,7 @@ void Hcf::transmitFrame(Packet *packet, simtime_t ifs) if (auto dataFrame = dynamicPtrCast(header)) { OriginatorBlockAckAgreement *agreement = nullptr; if (originatorBlockAckAgreementHandler) - agreement = originatorBlockAckAgreementHandler->getAgreement(dataFrame->getReceiverAddress(), dataFrame->getTid()); + agreement = originatorBlockAckAgreementHandler->getActiveAgreement(dataFrame->getReceiverAddress(), dataFrame->getTid()); auto ackPolicy = originatorAckPolicy->computeAckPolicy(packet, dataFrame, agreement); auto dataHeader = packet->removeAtFront(); dataHeader->setAckPolicy(ackPolicy); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 052524378e3..8f2da6d6aaa 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -70,6 +70,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: // their role so one role cannot cancel the other's timeout. simtime_t originatorInactivityDeadline = SIMTIME_MAX; simtime_t recipientInactivityDeadline = SIMTIME_MAX; + bool blockAckInactivityExpiryInProgress = false; // Transmission and Reception IRx *rx = nullptr; @@ -204,6 +205,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) override; virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override; + virtual bool releaseBlockAckAgreementFrames(MacAddress peerAddress, Tid tid) override; virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override; std::string getFrameSequenceInfo() const; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index 9ad68ed2969..ac5ecb84f5e 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -392,7 +392,7 @@ IFrameSequenceStep *BlockAckReqBlockAckFs::prepareStep(FrameSequenceContext *con { switch (step) { case 0: { - auto blockAckReqParams = context->getQoSContext()->ackPolicy->computeBlockAckReqParameters(context->getInProgressFrames(), context->getQoSContext()->txopProcedure); + auto blockAckReqParams = context->getQoSContext()->ackPolicy->computeBlockAckReqParameters(context->getInProgressFrames(), context->getQoSContext()->txopProcedure, context->getQoSContext()->blockAckAgreementHandler); auto receiverAddr = std::get<0>(blockAckReqParams); auto startingSequenceNumber = std::get<1>(blockAckReqParams); auto tid = std::get<2>(blockAckReqParams); @@ -434,4 +434,3 @@ bool BlockAckReqBlockAckFs::completeStep(FrameSequenceContext *context) } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc index 302ab7c32af..ca2d861615f 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc @@ -47,7 +47,7 @@ int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceConte { auto frameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); const auto& macHeader = frameToTransmit->peekAtFront(); - if (context->getQoSContext()->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), context->getQoSContext()->txopProcedure)) + if (context->getQoSContext()->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), context->getQoSContext()->txopProcedure, context->getQoSContext()->blockAckAgreementHandler)) return 2; if (dynamicPtrCast(macHeader)) return 3; @@ -55,7 +55,7 @@ int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceConte auto dataHeaderToTransmit = dynamicPtrCast(macHeader); OriginatorBlockAckAgreement *agreement = nullptr; if (context->getQoSContext()->blockAckAgreementHandler) - agreement = context->getQoSContext()->blockAckAgreementHandler->getAgreement(dataHeaderToTransmit->getReceiverAddress(), dataHeaderToTransmit->getTid()); + agreement = context->getQoSContext()->blockAckAgreementHandler->getActiveAgreement(dataHeaderToTransmit->getReceiverAddress(), dataHeaderToTransmit->getTid()); auto ackPolicy = context->getQoSContext()->ackPolicy->computeAckPolicy(frameToTransmit, dataHeaderToTransmit, agreement); if (ackPolicy == AckPolicy::BLOCK_ACK) return 0; @@ -79,4 +79,3 @@ bool TxOpFs::isBlockAckReqRtsCtsNeeded(OptionalFs *frameSequence, FrameSequenceC } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index 90cc7602199..229e57502ba 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -9,6 +9,8 @@ #include +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h" + namespace inet { namespace ieee80211 { @@ -31,13 +33,20 @@ bool OriginatorQosAckPolicy::isAckNeeded(const Ptr& h return !header->getReceiverAddress().isMulticast(); } -std::map> OriginatorQosAckPolicy::getOutstandingFramesPerReceiver(InProgressFrames *inProgressFrames) const +std::map, std::vector> OriginatorQosAckPolicy::getOutstandingFramesPerAgreement(InProgressFrames *inProgressFrames, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const { auto outstandingFrames = inProgressFrames->getOutstandingFrames(); - std::map> outstandingFramesPerReceiver; - for (auto frame : outstandingFrames) - outstandingFramesPerReceiver[frame->peekAtFront()->getReceiverAddress()].push_back(frame); - return outstandingFramesPerReceiver; + std::map, std::vector> outstandingFramesPerAgreement; + if (blockAckAgreementHandler == nullptr) + return outstandingFramesPerAgreement; + for (auto frame : outstandingFrames) { + auto dataHeader = frame->peekAtFront(); + auto receiverAddress = dataHeader->getReceiverAddress(); + auto tid = dataHeader->getTid(); + if (blockAckAgreementHandler->getActiveAgreement(receiverAddress, tid) != nullptr) + outstandingFramesPerAgreement[std::make_pair(receiverAddress, tid)].push_back(frame); + } + return outstandingFramesPerAgreement; } SequenceNumberCyclic OriginatorQosAckPolicy::computeStartingSequenceNumber(const std::vector& outstandingFrames) const @@ -64,10 +73,10 @@ bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector } // FIXME -bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const +bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const { - auto outstandingFramesPerReceiver = getOutstandingFramesPerReceiver(inProgressFrames); - for (auto outstandingFrames : outstandingFramesPerReceiver) { + auto outstandingFramesPerAgreement = getOutstandingFramesPerAgreement(inProgressFrames, blockAckAgreementHandler); + for (auto outstandingFrames : outstandingFramesPerAgreement) { if ((int)outstandingFrames.second.size() >= blockAckReqThreshold) return true; } @@ -75,28 +84,26 @@ bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *inProgressFra } // FIXME -std::tuple OriginatorQosAckPolicy::computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const +std::tuple OriginatorQosAckPolicy::computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const { - auto outstandingFramesPerReceiver = getOutstandingFramesPerReceiver(inProgressFrames); - for (auto outstandingFrames : outstandingFramesPerReceiver) { - if ((int)outstandingFrames.second.size() >= blockAckReqThreshold) { - auto largestOutstandingFrames = outstandingFramesPerReceiver.begin(); - for (auto it = outstandingFramesPerReceiver.begin(); it != outstandingFramesPerReceiver.end(); it++) { - if (it->second.size() > largestOutstandingFrames->second.size()) - largestOutstandingFrames = it; - } - MacAddress receiverAddress = largestOutstandingFrames->first; - SequenceNumberCyclic startingSequenceNumber = computeStartingSequenceNumber(largestOutstandingFrames->second); - Tid tid = largestOutstandingFrames->second.at(0)->peekAtFront()->getTid(); - return std::make_tuple(receiverAddress, startingSequenceNumber, tid); - } + auto outstandingFramesPerAgreement = getOutstandingFramesPerAgreement(inProgressFrames, blockAckAgreementHandler); + auto largestOutstandingFrames = outstandingFramesPerAgreement.end(); + for (auto it = outstandingFramesPerAgreement.begin(); it != outstandingFramesPerAgreement.end(); it++) { + if ((int)it->second.size() >= blockAckReqThreshold && (largestOutstandingFrames == outstandingFramesPerAgreement.end() || it->second.size() > largestOutstandingFrames->second.size())) + largestOutstandingFrames = it; + } + if (largestOutstandingFrames != outstandingFramesPerAgreement.end()) { + MacAddress receiverAddress = largestOutstandingFrames->first.first; + Tid tid = largestOutstandingFrames->first.second; + SequenceNumberCyclic startingSequenceNumber = computeStartingSequenceNumber(largestOutstandingFrames->second); + return std::make_tuple(receiverAddress, startingSequenceNumber, tid); } return std::make_tuple(MacAddress::UNSPECIFIED_ADDRESS, SequenceNumberCyclic(), -1); } AckPolicy OriginatorQosAckPolicy::computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const { - if (agreement == nullptr) + if (agreement == nullptr || agreement->isInactivityExpired()) return AckPolicy::NORMAL_ACK; if (agreement->getIsAddbaResponseReceived() && isBlockAckPolicyEligibleFrame(packet, header)) { if (checkAgreementPolicy(header, agreement)) @@ -140,4 +147,3 @@ simtime_t OriginatorQosAckPolicy::getBlockAckTimeout(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const; - virtual std::map> getOutstandingFramesPerReceiver(InProgressFrames *inProgressFrames) const; + virtual std::map, std::vector> getOutstandingFramesPerAgreement(InProgressFrames *inProgressFrames, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const; virtual SequenceNumberCyclic computeStartingSequenceNumber(const std::vector& outstandingFrames) const; virtual bool isCompressedBlockAckReq(const std::vector& outstandingFrames, int startingSequenceNumber) const; @@ -39,8 +39,8 @@ class INET_API OriginatorQosAckPolicy : public ModeSetListener, public IOriginat virtual bool isAckNeeded(const Ptr& header) const override; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const override; virtual bool isBlockAckPolicyEligibleFrame(Packet *packet, const Ptr& header) const override; - virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const override; - virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const override; + virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const override; + virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const override; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const override; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const override; @@ -50,4 +50,3 @@ class INET_API OriginatorQosAckPolicy : public ModeSetListener, public IOriginat } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index 59bdd8b4372..e0320e59c2b 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -178,6 +178,21 @@ void QosAckHandler::processFailedBlockAckReq(const Ptr& header) { if (header->getType() == ST_DATA_WITH_QOS) { @@ -276,6 +291,7 @@ std::string QosAckHandler::getStatusString(Status status) case Status::BLOCK_ACK_ARRIVED_ACKED: return "BLOCK_ACK_ARRIVED_ACKED"; case Status::WAITING_FOR_BLOCK_ACK: return "WAITING_FOR_BLOCK_ACK"; case Status::NORMAL_ACK_ARRIVED: return "NORMAL_ACK_ARRIVED"; + case Status::BLOCK_ACK_NOT_ARRIVED: return "BLOCK_ACK_NOT_ARRIVED"; default: throw cRuntimeError("Unknown status"); } } @@ -291,4 +307,3 @@ void QosAckHandler::printAckStatuses() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h index 7c1e0bce61c..f2b7749e99d 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h @@ -57,6 +57,10 @@ class INET_API QosAckHandler : public SimpleModule, public IAckHandler virtual void processReceivedAck(const Ptr& ack, const Ptr& ackedHeader); virtual std::set>> processReceivedBlockAck(const Ptr& blockAck); virtual void processFailedBlockAckReq(const Ptr& blockAckReq); + // Makes all outstanding BA-policy frames for this peer/TID retryable + // through the normal acknowledgment path when the agreement is no longer + // available to the data plane. + virtual bool releaseBlockAckAgreementFrames(MacAddress peerAddress, Tid tid); virtual void frameGotInProgress(const Ptr& dataOrMgmtHeader) override; virtual void processTransmittedDataOrMgmtFrame(const Ptr& header); @@ -78,4 +82,3 @@ class INET_API QosAckHandler : public SimpleModule, public IAckHandler } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index 29f793f0110..48e33da6c92 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -8,6 +8,7 @@ #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" #include "inet/common/ModuleAccess.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" namespace inet { namespace ieee80211 { @@ -62,7 +63,7 @@ bool RecipientQosAckPolicy::isAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) const { if (dynamicPtrCast(blockAckReq)) { - return agreement != nullptr; + return agreement != nullptr && !agreement->isInactivityExpired(); // TODO The Basic BlockAckReq frame shall be discarded if all MSDUs referenced by this // frame have been discarded from the transmit buffer due to expiry of their lifetime limit. } @@ -96,4 +97,3 @@ simtime_t RecipientQosAckPolicy::computeBasicBlockAckDurationField(Packet *packe } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 624c38c448e..a190318c919 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -128,6 +128,20 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data take(dataPacket); expireReceiveLifetime(); // TODO A-MPDU Deaggregation, MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption + RecipientBlockAckAgreement *agreement = nullptr; + if (dataHeader->getAckPolicy() == AckPolicy::BLOCK_ACK) { + if (blockAckAgreementHandler != nullptr) + agreement = blockAckAgreementHandler->getActiveAgreement(dataHeader->getTid(), dataHeader->getTransmitterAddress()); + if (agreement == nullptr) { + EV_INFO << "Dropping Block Ack policy data without an active Block Ack agreement.\n"; + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, dataPacket, &details); + delete dataPacket; + scheduleReceiveLifetimeTimer(); + return std::vector(); + } + } if (duplicateRemoval && duplicateRemoval->isDuplicate(dataHeader)) { EV_WARN << "Dropping duplicate packet " << *dataPacket << ".\n"; PacketDropDetails details; @@ -142,7 +156,8 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data if (blockAckReordering && blockAckAgreementHandler) { Tid tid = dataHeader->getTid(); MacAddress originatorAddr = dataHeader->getTransmitterAddress(); - RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); + if (agreement == nullptr) + agreement = blockAckAgreementHandler->getActiveAgreement(tid, originatorAddr); if (agreement) { auto processingResult = blockAckReordering->processReceivedQoSFrameWithResult(agreement, dataPacket, dataHeader); frames = processingResult.frames; @@ -239,7 +254,7 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c if (blockAckReordering) { Tid tid = blockAckReq->getTidInfo(); MacAddress originatorAddr = blockAckReq->getTransmitterAddress(); - RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); + RecipientBlockAckAgreement *agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(tid, originatorAddr); if (agreement) frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReq); else { diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index e33fd1ff2bf..b3db38f75e4 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -20,6 +20,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" @@ -33,12 +34,14 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" +#include "inet/linklayer/ieee80211/mac/framesequence/TxOpFs.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" @@ -95,8 +98,10 @@ class TestRecipientPolicy : public IRecipientBlockAckAgreementPolicy class TestRecipientAckPolicy : public IRecipientAckPolicy, public IRecipientQosAckPolicy { public: + bool blockAckNeeded = false; + virtual bool isAckNeeded(const Ptr&) const override { return true; } - virtual bool isBlockAckNeeded(const Ptr&, RecipientBlockAckAgreement *) const override { return false; } + virtual bool isBlockAckNeeded(const Ptr&, RecipientBlockAckAgreement *agreement) const override { return blockAckNeeded && agreement != nullptr; } virtual simtime_t computeAckDurationField(Packet *, const Ptr&) const override { return 0; } virtual simtime_t computeBasicBlockAckDurationField(Packet *, const Ptr&) const override { return 0; } }; @@ -144,6 +149,17 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler } }; +class TestControlResponseCallback : public TestCallback +{ + public: + int numControlResponses = 0; + + virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr&, Packet *, const Ptr&) override { + numControlResponses++; + delete responsePacket; + } +}; + class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler { public: @@ -163,6 +179,15 @@ class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler void publishInactivityDeadline(IBlockAckAgreementHandlerCallback *callback) { scheduleInactivityTimer(callback); } }; +class TestOriginatorQosAckPolicy : public OriginatorQosAckPolicy +{ + public: + void configureBlockAckSelection(int maxFrameLength, int threshold) { + maxBlockAckPolicyFrameLength = maxFrameLength; + blockAckReqThreshold = threshold; + } +}; + class TestTimeoutCancellationCallback : public TestCallback { public: @@ -183,6 +208,13 @@ class TestRecipientHandler : public RecipientBlockAckAgreementHandler int getNumCachedAddbaResponses() const { return lastAddbaResponses.size(); } }; +class TestRecipientBlockAckProcedure : public RecipientBlockAckProcedure +{ + public: + int getNumReceivedBlockAckReq() const { return numReceivedBlockAckReq; } + int getNumSentBlockAck() const { return numSentBlockAck; } +}; + class TestBlockAckReordering : public BlockAckReordering { public: @@ -633,6 +665,7 @@ class TestHcf : public Hcf std::vector cancelledBlockAckTeardownGenerationIds; int numRebuildEligibilityCalls = 0; int numTransmittedControlResponses = 0; + bool managementProcessedDuringBlockAckExpiry = false; bool droppedSetupCancelled = false; bool delegateDroppedSetupHandling = false; TestCallback *managementCallback = nullptr; @@ -696,6 +729,11 @@ class TestHcf : public Hcf auto lastTransmittedHeader = lastTransmittedPacket->peekAtFront(); originatorProcessReceivedControlFrame(packet, header, lastTransmittedPacket, lastTransmittedHeader, accessCategory); } + void processInactivityTimeout() { + if (inactivityTimer == nullptr) + inactivityTimer = new cMessage("BlockAckInactivityTimer"); + handleMessage(inactivityTimer); + } protected: virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr&, Packet *, const Ptr&) override { @@ -703,6 +741,7 @@ class TestHcf : public Hcf delete responsePacket; } virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { + managementProcessedDuringBlockAckExpiry |= blockAckInactivityExpiryInProgress; if (managementCallback != nullptr) managementCallback->processMgmtFrame(packet, header); else @@ -2627,28 +2666,28 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; auto agreement = agreementHandler.processReceivedAddbaRequest(request, &policy, &callback, &callback); ASSERT(agreement != nullptr); - auto makeServiceFragment = [&](const char *name, MacAddress transmitterAddress, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto makeServiceFragment = [&](const char *name, MacAddress transmitterAddress, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte, AckPolicy ackPolicy) { auto dataHeader = makeQosHeader(peer2, 6, sequenceNumber); dataHeader->setTransmitterAddress(transmitterAddress); dataHeader->setFragmentNumber(fragmentNumber); dataHeader->setMoreFragments(moreFragments); - dataHeader->setAckPolicy(BLOCK_ACK); + dataHeader->setAckPolicy(ackPolicy); auto packet = new Packet(name, dataHeader); packet->insertAtBack(makeShared(std::vector({ payloadByte }))); packet->insertAtBack(makeShared()); return packet; }; - auto reorderFirstFragment = makeServiceFragment("serviceReorderFirstFragment", peer1, SequenceNumberCyclic(300), 1, true, 0x71); + auto reorderFirstFragment = makeServiceFragment("serviceReorderFirstFragment", peer1, SequenceNumberCyclic(300), 1, true, 0x71, BLOCK_ACK); ASSERT(service->dataFrameReceived(reorderFirstFragment, reorderFirstFragment->peekAtFront(), &agreementHandler).empty()); - auto reorderFinalFragment = makeServiceFragment("serviceReorderFinalFragment", peer1, SequenceNumberCyclic(300), 2, false, 0x72); + auto reorderFinalFragment = makeServiceFragment("serviceReorderFinalFragment", peer1, SequenceNumberCyclic(300), 2, false, 0x72, BLOCK_ACK); ASSERT(service->dataFrameReceived(reorderFinalFragment, reorderFinalFragment->peekAtFront(), &agreementHandler).empty()); ASSERT(dropListener.numSignals == 0); wait(0.5e-6); // No agreement for peer3: this fragment is retained by BasicReassembly, // with a later deadline than the already retained reorder fragments. - auto basicFragment = makeServiceFragment("serviceBasicFragment", peer3, SequenceNumberCyclic(350), 0, true, 0x73); + auto basicFragment = makeServiceFragment("serviceBasicFragment", peer3, SequenceNumberCyclic(350), 0, true, 0x73, NORMAL_ACK); ASSERT(service->dataFrameReceived(basicFragment, basicFragment->peekAtFront(), &agreementHandler).empty()); wait(1.6e-6); ASSERT(dropListener.numSignals == 2); @@ -2657,7 +2696,7 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; // The expired sequence remains tombstoned until the reorder window moves // past it. A late fragment is rejected and reported once by the service, // while no hybrid or partial frame is delivered. - auto lateReorderFragment = makeServiceFragment("serviceLateReorderFragment", peer1, SequenceNumberCyclic(300), 0, true, 0x74); + auto lateReorderFragment = makeServiceFragment("serviceLateReorderFragment", peer1, SequenceNumberCyclic(300), 0, true, 0x74, BLOCK_ACK); ASSERT(service->dataFrameReceived(lateReorderFragment, lateReorderFragment->peekAtFront(), &agreementHandler).empty()); ASSERT(dropListener.numSignals == 3); ASSERT(dropListener.numOtherPacketDrops == 3); @@ -2691,7 +2730,7 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; dataHeader->setTransmitterAddress(transmitterAddress); dataHeader->setFragmentNumber(fragmentNumber); dataHeader->setMoreFragments(moreFragments); - dataHeader->setAckPolicy(BLOCK_ACK); + dataHeader->setAckPolicy(NORMAL_ACK); auto packet = new Packet(name, dataHeader); packet->insertAtBack(makeShared(std::vector({ payloadByte }))); packet->insertAtBack(makeShared()); @@ -5614,6 +5653,7 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) request->setBlockAckTimeoutValue(3); auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &setupCallback, &setupCallback); ASSERT(agreement != nullptr); + ASSERT(recipientHandler.getActiveAgreement(12, peer2) == agreement); auto creationTime = simTime(); ASSERT(creationTime > 0); ASSERT(agreement->getExpirationTime() == creationTime + 3); @@ -5633,6 +5673,7 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) TestCallback expiryCallback; recipientHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); ASSERT(recipientHandler.getAgreement(12, peer2) == agreement); + ASSERT(recipientHandler.getActiveAgreement(12, peer2) == nullptr); ASSERT(agreement->isInactivityExpired()); ASSERT(expiryCallback.managementPackets.size() == 1); ASSERT(expiryCallback.recipientInactivityDeadline == SIMTIME_MAX); @@ -5654,6 +5695,7 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) TestOriginatorHandler originatorHandler; originatorHandler.addEstablishedAgreement(peer1, 13); auto originatorAgreement = originatorHandler.getAgreement(peer1, 13); + ASSERT(originatorHandler.getActiveAgreement(peer1, 13) == originatorAgreement); originatorAgreement->setBlockAckTimeoutValue(2); originatorAgreement->calculateExpirationTime(); auto originatorCreationTime = simTime(); @@ -5663,6 +5705,7 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) wait(3); originatorHandler.blockAckAgreementExpired(&originatorCallback, &originatorCallback); ASSERT(originatorHandler.getAgreement(peer1, 13) == originatorAgreement); + ASSERT(originatorHandler.getActiveAgreement(peer1, 13) == nullptr); ASSERT(originatorAgreement->isInactivityExpired()); ASSERT(originatorCallback.managementPackets.size() == 1); ASSERT(originatorCallback.originatorInactivityDeadline == SIMTIME_MAX); @@ -5683,6 +5726,255 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) } +// Expiry quarantines the retained originator agreement from ACK/BAR selection. +// HCF also releases both kinds of outstanding Block Ack state before resuming +// channel access, so those frames can be retried with Normal Ack. +{ + auto originatorHandler = new TestOriginatorHandler(); + originatorHandler->addEstablishedAgreement(peer1, 4); + auto agreement = originatorHandler->getAgreement(peer1, 4); + agreement->setBlockAckTimeoutValue(1); + agreement->calculateExpirationTime(); + + TestOriginatorQosAckPolicy ackPolicy; + ackPolicy.configureBlockAckSelection(1000, 1); + auto makeDataPacket = [&](const char *name, int sequenceNumber, AckPolicy ackPolicyValue) { + auto header = makeQosHeader(peer1, 4, SequenceNumberCyclic(sequenceNumber)); + header->setAckPolicy(ackPolicyValue); + return new Packet(name, header); + }; + + auto waitingFrame = makeDataPacket("expiryWaitingForBlockAck", 500, BLOCK_ACK); + auto waitingHeader = waitingFrame->peekAtFront(); + auto notRequestedFrame = makeDataPacket("expiryBlockAckNotRequested", 501, BLOCK_ACK); + auto notRequestedHeader = notRequestedFrame->peekAtFront(); + auto eligibleFrame = makeDataPacket("expiryEligibleFrame", 502, NORMAL_ACK); + auto eligibleHeader = eligibleFrame->peekAtFront(); + auto completingFrame = makeDataPacket("expiryDuringTransmission", 503, BLOCK_ACK); + auto completingHeader = completingFrame->peekAtFront(); + ASSERT(ackPolicy.computeAckPolicy(eligibleFrame, eligibleHeader, agreement) == BLOCK_ACK); + + TestOriginatorQosMacDataService dataService; + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + inProgressFrames.addFrame(waitingFrame); + inProgressFrames.addFrame(notRequestedFrame); + inProgressFrames.addFrame(eligibleFrame); + inProgressFrames.addFrame(completingFrame); + ackHandler.frameGotInProgress(waitingHeader); + ackHandler.processTransmittedDataOrMgmtFrame(waitingHeader); + auto blockAckReq = makeShared(); + blockAckReq->setReceiverAddress(peer1); + blockAckReq->setTidInfo(4); + blockAckReq->setStartingSequenceNumber(SequenceNumberCyclic(500)); + ackHandler.processTransmittedBlockAckReq(blockAckReq); + ackHandler.frameGotInProgress(notRequestedHeader); + ackHandler.processTransmittedDataOrMgmtFrame(notRequestedHeader); + ackHandler.frameGotInProgress(completingHeader); + ASSERT(ackHandler.getQoSDataAckStatus(waitingHeader) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(ackHandler.getQoSDataAckStatus(notRequestedHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(ackHandler.getQoSDataAckStatus(completingHeader) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + + TestTxopProcedure txopProcedure; + FrameSequenceContext context(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &inProgressFrames, nullptr, nullptr, nullptr, + new QoSContext(&ackPolicy, nullptr, originatorHandler, &txopProcedure)); + TxOpFs txOpFs; + ASSERT(txOpFs.selectTxOpSequence(&txOpFs, &context) == 2); + + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestCallback managementCallback; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureManagementCallback(&managementCallback); + + wait(1); + hcf.processInactivityTimeout(); + ASSERT(originatorHandler->getAgreement(peer1, 4) == agreement); + ASSERT(originatorHandler->getActiveAgreement(peer1, 4) == nullptr); + ASSERT(agreement->isInactivityExpired()); + ASSERT(managementCallback.managementPackets.size() == 1); + ASSERT(hcf.managementProcessedDuringBlockAckExpiry); + ASSERT(ackHandler.getQoSDataAckStatus(waitingHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.getQoSDataAckStatus(notRequestedHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.getQoSDataAckStatus(completingHeader) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + hcf.processTransmittedData(completingFrame, completingHeader, AC_BE); + ASSERT(ackHandler.getQoSDataAckStatus(completingHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.isEligibleToTransmit(waitingHeader)); + ASSERT(ackHandler.isEligibleToTransmit(notRequestedHeader)); + ASSERT(ackHandler.isEligibleToTransmit(completingHeader)); + ASSERT(hcf.numRebuildEligibilityCalls == 1); + ASSERT(hcf.numResumedEligibleChannelAccess == 1); + ASSERT(ackPolicy.computeAckPolicy(eligibleFrame, eligibleHeader, agreement) == NORMAL_ACK); + ASSERT(txOpFs.selectTxOpSequence(&txOpFs, &context) == 1); + + auto lateBlockAck = makeShared(); + lateBlockAck->setTransmitterAddress(peer1); + lateBlockAck->setTidInfo(4); + lateBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(500)); + auto lateBlockAckPacket = new Packet("postExpiryBlockAck", lateBlockAck); + auto transmittedBlockAckReqPacket = new Packet("preExpiryBlockAckReq", blockAckReq); + hcf.processReceivedAck(lateBlockAckPacket, transmittedBlockAckReqPacket, AC_BE); + ASSERT(ackHandler.getQoSDataAckStatus(waitingHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.getQoSDataAckStatus(notRequestedHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + delete lateBlockAckPacket; + delete transmittedBlockAckReqPacket; + + auto frames = inProgressFrames.releaseFrames(); + for (auto frame : frames) + delete frame; +} + +// Any terminal originator agreement removal releases outstanding BA-policy +// frames as well. Otherwise active-agreement BAR filtering would leave those +// frames permanently ineligible after peer or local DELBA teardown. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestOriginatorQosAckPolicy ackPolicy; + ackPolicy.configureBlockAckSelection(1000, 1); + TestOriginatorQosMacDataService dataService; + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + + originatorHandler->addEstablishedAgreement(peer1, 6); + auto peerTeardownHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(520)); + peerTeardownHeader->setAckPolicy(BLOCK_ACK); + auto peerTeardownFrame = new Packet("peerDelbaOutstandingData", peerTeardownHeader); + inProgressFrames.addFrame(peerTeardownFrame); + ackHandler.frameGotInProgress(peerTeardownHeader); + ackHandler.processTransmittedDataOrMgmtFrame(peerTeardownHeader); + ASSERT(ackHandler.getQoSDataAckStatus(peerTeardownHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + auto peerDelba = makeShared(); + peerDelba->setTransmitterAddress(peer1); + peerDelba->setTid(6); + peerDelba->setInitiator(false); + hcf.processReceivedManagementFrame(peerDelba); + ASSERT(originatorHandler->getAgreement(peer1, 6) == nullptr); + ASSERT(ackHandler.getQoSDataAckStatus(peerTeardownHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.isEligibleToTransmit(peerTeardownHeader)); + + originatorHandler->addEstablishedAgreement(peer1, 7); + auto localTeardownAgreement = originatorHandler->getAgreement(peer1, 7); + auto localTeardownHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(530)); + localTeardownHeader->setAckPolicy(BLOCK_ACK); + auto localTeardownFrame = new Packet("localDelbaOutstandingData", localTeardownHeader); + inProgressFrames.addFrame(localTeardownFrame); + ackHandler.frameGotInProgress(localTeardownHeader); + ackHandler.processTransmittedDataOrMgmtFrame(localTeardownHeader); + ASSERT(ackHandler.getQoSDataAckStatus(localTeardownHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + auto localDelbaPacket = makeTaggedDelbaPacket("localTerminalDelba", peer1, 7, SequenceNumberCyclic(531), 0, false, localTeardownAgreement->getTransactionId()); + auto localDelba = localDelbaPacket->peekAtFront(); + hcf.processTransmittedManagement(localDelbaPacket, localDelba, AC_BE); + ASSERT(originatorHandler->getAgreement(peer1, 7) == nullptr); + ASSERT(ackHandler.getQoSDataAckStatus(localTeardownHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.isEligibleToTransmit(localTeardownHeader)); + delete localDelbaPacket; + + auto frames = inProgressFrames.releaseFrames(); + for (auto frame : frames) + delete frame; +} + +// An expired recipient agreement remains available to the DELBA lifecycle, +// but BA-policy data is dropped, BAR cannot release reordering state, and no +// Block Ack response is generated. +{ + TestRecipientHandler recipientHandler; + TestRecipientPolicy recipientPolicy; + recipientPolicy.blockAckTimeoutValue = 1; + TestCallback setupCallback; + auto request = makeRequest(peer2, 5, 111, SequenceNumberCyclic(600), 32); + request->setBlockAckTimeoutValue(1); + auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &setupCallback, &setupCallback); + ASSERT(agreement != nullptr); + ASSERT(recipientHandler.getActiveAgreement(5, peer2) == agreement); + + auto serviceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("expiredAgreementDataService", this); + serviceModule->callInitialize(); + auto service = check_and_cast(serviceModule); + TestPacketDropSignalListener dropListener; + service->subscribe(packetDroppedSignal, &dropListener); + + auto bufferedHeader = makeQosHeader(peer1, 5, SequenceNumberCyclic(601)); + bufferedHeader->setTransmitterAddress(peer2); + bufferedHeader->setAckPolicy(BLOCK_ACK); + auto bufferedPacket = new Packet("preExpiryBufferedData", bufferedHeader); + bufferedPacket->insertAtBack(makeShared()); + ASSERT(service->dataFrameReceived(bufferedPacket, bufferedHeader, &recipientHandler).empty()); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(601), 0)); + + auto blockAckReq = makeShared(); + blockAckReq->setTransmitterAddress(peer2); + blockAckReq->setReceiverAddress(peer1); + blockAckReq->setTidInfo(5); + blockAckReq->setStartingSequenceNumber(SequenceNumberCyclic(601)); + auto blockAckReqPacket = new Packet("expiredAgreementBar", blockAckReq); + RecipientQosAckPolicy productionAckPolicy; + ASSERT(productionAckPolicy.isBlockAckNeeded(blockAckReq, agreement)); + TestRecipientAckPolicy ackPolicy; + ackPolicy.blockAckNeeded = true; + TestRecipientBlockAckProcedure blockAckProcedure; + TestControlResponseCallback responseCallback; + blockAckProcedure.processReceivedBlockAckReq(blockAckReqPacket, blockAckReq, &ackPolicy, &recipientHandler, &responseCallback); + ASSERT(responseCallback.numControlResponses == 1); + + wait(1); + TestCallback expiryCallback; + recipientHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + ASSERT(recipientHandler.getAgreement(5, peer2) == agreement); + ASSERT(recipientHandler.getActiveAgreement(5, peer2) == nullptr); + ASSERT(agreement->isInactivityExpired()); + ASSERT(!productionAckPolicy.isBlockAckNeeded(blockAckReq, agreement)); + + auto expiredHeader = makeQosHeader(peer1, 5, SequenceNumberCyclic(602)); + expiredHeader->setTransmitterAddress(peer2); + expiredHeader->setAckPolicy(BLOCK_ACK); + auto expiredPacket = new Packet("postExpiryBlockAckData", expiredHeader); + expiredPacket->insertAtBack(makeShared()); + ASSERT(service->dataFrameReceived(expiredPacket, expiredHeader, &recipientHandler).empty()); + ASSERT(dropListener.numSignals == 1); + ASSERT(dropListener.numOtherPacketDrops == 1); + ASSERT(!agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(602), 0)); + + ASSERT(service->controlFrameReceived(blockAckReqPacket, blockAckReq, &recipientHandler).empty()); + ASSERT(dropListener.numSignals == 1); + blockAckProcedure.processReceivedBlockAckReq(blockAckReqPacket, blockAckReq, &ackPolicy, &recipientHandler, &responseCallback); + ASSERT(responseCallback.numControlResponses == 1); + + service->resetBlockAckReordering(5, peer2); + ASSERT(dropListener.numSignals == 2); + ASSERT(dropListener.numOtherPacketDrops == 2); + service->unsubscribe(packetDroppedSignal, &dropListener); + serviceModule->callFinish(); + serviceModule->deleteModule(); + delete blockAckReqPacket; +} + // A timeout DELBA can be aborted before its final fragment is transmitted. // The matching generation is retired locally, and the returned ownership lets // HCF release role-specific resources without rearming a timed-out agreement. From 8dbee7ba322d29b0c815cdf39c161bede06b54c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:05 +0200 Subject: [PATCH 12/16] ieee80211: fix recipient BAR and ADDBA timeout handling Ignore null defragmentation results when a BAR releases buffered fragments. This prevents half-sequence-space reassembly rejections from reaching A-MSDU deaggregation and crashing the recipient data path. Resolve the recipient Block Ack timeout according to the policy contract: inherit the originator request when the recipient policy is zero, otherwise use the configured recipient override. The negotiated response value continues to drive agreement state and inactivity deadlines. Add focused production-path coverage for the exact 2048 sequence boundary, subsequent reorder progress, timeout inheritance and override, the zero/no-timeout case, expiration timing, and cached ADDBA responses. --- .../RecipientBlockAckAgreementHandler.cc | 2 +- .../recipient/RecipientQosMacDataService.cc | 4 +- tests/unit/Ieee80211AddbaTransaction_1.test | 163 ++++++++++++++++-- 3 files changed, 156 insertions(+), 13 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index e8a6cce5749..4dcad4f4bea 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -120,7 +120,7 @@ const Ptr RecipientBlockAckAgreementHandler::buildAddbaR addbaResponse->setTid(tid); addbaResponse->setBlockAckPolicy(!addbaRequest->getBlockAckPolicy() && blockAckAgreementPolicy->delayedBlockAckPolicySupported() ? false : true); addbaResponse->setBufferSize(addbaRequest->getBufferSize() <= blockAckAgreementPolicy->getMaximumAllowedBufferSize() ? addbaRequest->getBufferSize() : blockAckAgreementPolicy->getMaximumAllowedBufferSize()); - addbaResponse->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue() == 0 ? blockAckAgreementPolicy->getBlockAckTimeoutValue() : addbaRequest->getBlockAckTimeoutValue()); + addbaResponse->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue() == 0 ? addbaRequest->getBlockAckTimeoutValue() : blockAckAgreementPolicy->getBlockAckTimeoutValue()); addbaResponse->setAMsduSupported(blockAckAgreementPolicy->aMsduSupported()); return addbaResponse; } diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index a190318c919..ec3abe7b820 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -266,7 +266,9 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c if (basicReassembly) { // FIXME defragmentation for (auto it : frames) { auto fragments = it.second; - defragmentedFrames.push_back(defragment(fragments)); + auto frame = defragment(fragments); + if (frame != nullptr) + defragmentedFrames.push_back(frame); } } else { diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index b3db38f75e4..458eae5b8f7 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -1879,6 +1879,27 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(threw); } +// IEEE Std 802.11-2024, 10.25.2: the recipient policy may inherit the +// originator's advisory timeout when it does not configure an override. The +// response, installed agreement, and inactivity deadline must use that one +// negotiated value. +{ + RecipientBlockAckAgreementHandler recipientHandler; + TestRecipientPolicy recipientPolicy; + TestCallback responseCallback; + auto request = makeRequest(peer1, 8, 93, SequenceNumberCyclic(200), 32); + request->setBlockAckTimeoutValue(3); + auto agreementCreationTime = simTime(); + auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &responseCallback, &responseCallback); + ASSERT(agreement != nullptr); + auto response = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(response != nullptr); + ASSERT(response->getBlockAckTimeoutValue() == 3); + ASSERT(agreement->getBlockAckTimeoutValue() == 3); + ASSERT(agreement->getExpirationTime() == agreementCreationTime + 3); + ASSERT(responseCallback.recipientInactivityDeadline == agreementCreationTime + 3); +} + // An initially rejected request has no agreement, so its response is never // replayable and must not consume replay-cache state. { @@ -1948,6 +1969,7 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto firstRequest = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); firstRequest->setSequenceNumber(SequenceNumberCyclic(50)); firstRequest->setBlockAckTimeoutValue(3); + auto firstAgreementCreationTime = simTime(); auto firstReception = recipientDataService->managementFrameReceived(new Packet("firstAddbaRequest", firstRequest), firstRequest); ASSERT(!firstReception.duplicate); ASSERT(firstReception.completeFrames.empty()); @@ -1968,8 +1990,10 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(firstResponse->getTid() == 6); ASSERT(firstResponse->getBlockAckPolicy()); ASSERT(firstResponse->getBufferSize() == 32); - ASSERT(firstResponse->getBlockAckTimeoutValue() == 3); + ASSERT(firstResponse->getBlockAckTimeoutValue() == 1); ASSERT(firstResponse->getAMsduSupported()); + ASSERT(firstAgreement->getBlockAckTimeoutValue() == 1); + ASSERT(firstAgreement->getExpirationTime() == firstAgreementCreationTime + 1); auto bufferedHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(101)); bufferedHeader->setTransmitterAddress(peer2); @@ -2027,7 +2051,7 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(rejectedResponse->getStatusCode() != 0); ASSERT(rejectedResponse->getTid() == 6); ASSERT(rejectedResponse->getBufferSize() == 1); - ASSERT(rejectedResponse->getBlockAckTimeoutValue() == 4); + ASSERT(rejectedResponse->getBlockAckTimeoutValue() == 1); ASSERT(!rejectedResponse->getAMsduSupported()); auto duplicateRejectedRequest = makeRequest(peer2, 6, 91, SequenceNumberCyclic(200), 16); @@ -2422,8 +2446,13 @@ ASSERT(firstImmediateAgreement != nullptr); ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == firstImmediateAgreement); ASSERT(firstImmediateAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(0)); auto acceptedResponse = dynamicPtrCast(recipientCallback.managementHeader); +ASSERT(acceptedResponse != nullptr); ASSERT(acceptedResponse->getDialogToken() == 77); ASSERT(acceptedResponse->getStatusCode() == 0); +ASSERT(acceptedResponse->getBlockAckTimeoutValue() == 0); +ASSERT(firstImmediateAgreement->getBlockAckTimeoutValue() == 0); +ASSERT(firstImmediateAgreement->getExpirationTime() == SIMTIME_MAX); +ASSERT(recipientCallback.recipientInactivityDeadline == SIMTIME_MAX); // Renegotiation replaces the recipient agreement and the HCF reset boundary // must discard the old reorder window before frames use the new SSN. @@ -2543,6 +2572,112 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; reassembleOrderedFragments(barFragments, { 0x50, 0x51, 0x52, 0x53 }); } +// A complete fragmented MPDU released by a BAR at the exact sequence-space +// half boundary must be consumed safely when BasicReassembly rejects its +// ambiguous generation. The production QoS data service must not expose the +// resulting null packet to deaggregation or retain the released fragments. +{ + auto serviceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("halfSpaceBarDataService", this); + serviceModule->callInitialize(); + auto service = check_and_cast(serviceModule); + RecipientBlockAckAgreementHandler agreementHandler; + TestRecipientPolicy policy; + TestCallback callback; + + // Establish the BasicReassembly sequence-space high watermark at zero + // before the Block Ack receive buffer is created. + auto seedHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(0)); + seedHeader->setTransmitterAddress(peer1); + seedHeader->setAckPolicy(NORMAL_ACK); + seedHeader->setAMsduPresent(false); + auto seedPacket = new Packet("halfSpaceReassemblySeed", seedHeader); + auto seedFrames = service->dataFrameReceived(seedPacket, seedHeader, &agreementHandler); + ASSERT(seedFrames.size() == 1); + ASSERT(seedFrames.front() == seedPacket); + for (auto frame : seedFrames) { + take(frame); + drop(frame); + delete frame; + } + + auto request = makeRequest(peer1, 6, 94, SequenceNumberCyclic(2047), 64); + request->setReceiverAddress(peer2); + auto agreement = agreementHandler.processReceivedAddbaRequest(request, &policy, &callback, &callback); + ASSERT(agreement != nullptr); + ASSERT(agreement->getStartingSequenceNumber() == SequenceNumberCyclic(2047)); + + auto makeFragment = [&](const char *name, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto header = makeQosHeader(peer2, 6, SequenceNumberCyclic(2048)); + header->setTransmitterAddress(peer1); + header->setFragmentNumber(fragmentNumber); + header->setMoreFragments(moreFragments); + header->setAckPolicy(BLOCK_ACK); + header->setAMsduPresent(false); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + + auto firstFragment = makeFragment("halfSpaceBarFragment0", 0, true, 0x81); + auto secondFragment = makeFragment("halfSpaceBarFragment1", 1, false, 0x82); + ASSERT(service->dataFrameReceived(firstFragment, firstFragment->peekAtFront(), &agreementHandler).empty()); + ASSERT(service->dataFrameReceived(secondFragment, secondFragment->peekAtFront(), &agreementHandler).empty()); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(2048), 0)); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(2048), 1)); + + TestSignalListener defragmentedListener; + TestSignalListener deaggregatedListener; + TestPacketDropSignalListener resetDropListener; + service->subscribe(IRecipientQosMacDataService::packetDefragmentedSignal, &defragmentedListener); + service->subscribe(IRecipientQosMacDataService::packetDeaggregatedSignal, &deaggregatedListener); + service->subscribe(packetDroppedSignal, &resetDropListener); + + auto barHeader = makeShared(); + barHeader->setTransmitterAddress(peer1); + barHeader->setReceiverAddress(peer2); + barHeader->setTidInfo(6); + barHeader->setStartingSequenceNumber(SequenceNumberCyclic(2048)); + auto barPacket = new Packet("halfSpaceBar", barHeader); + auto releasedFrames = service->controlFrameReceived(barPacket, barHeader, &agreementHandler); + ASSERT(releasedFrames.empty()); + ASSERT(defragmentedListener.numSignals == 0); + ASSERT(deaggregatedListener.numSignals == 0); + delete barPacket; + + // getAckState() intentionally reports older or absent sequence controls + // as acknowledged, so it cannot expose removal of sequence 2048. A + // complete next sequence released immediately proves that the BAR did + // advance the reorder window and the new Block Ack state is recorded. + auto nextHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(2049)); + nextHeader->setTransmitterAddress(peer1); + nextHeader->setAckPolicy(BLOCK_ACK); + nextHeader->setAMsduPresent(false); + auto nextPacket = new Packet("halfSpaceBarFollowingFrame", nextHeader); + auto nextFrames = service->dataFrameReceived(nextPacket, nextHeader, &agreementHandler); + ASSERT(nextFrames.size() == 1); + ASSERT(nextFrames.front() == nextPacket); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(2049), 0)); + ASSERT(defragmentedListener.numSignals == 1); + ASSERT(deaggregatedListener.numSignals == 0); + for (auto frame : nextFrames) { + take(frame); + drop(frame); + delete frame; + } + + // resetBlockAckReordering is a public owner-side observation: no drops + // prove that the BAR already removed the complete MPDU from the reorder + // buffer before BasicReassembly consumed its rejected fragments. + service->resetBlockAckReordering(6, peer1); + ASSERT(resetDropListener.numSignals == 0); + service->unsubscribe(packetDroppedSignal, &resetDropListener); + service->unsubscribe(IRecipientQosMacDataService::packetDeaggregatedSignal, &deaggregatedListener); + service->unsubscribe(IRecipientQosMacDataService::packetDefragmentedSignal, &defragmentedListener); + serviceModule->callFinish(); + serviceModule->deleteModule(); +} + // Receive-buffer lifetime starts when the first fragment of an incomplete // fragmented MPDU is retained. Later fragments do not refresh that deadline; // complete MPDUs held only for ordering remain available until normal window @@ -5591,14 +5726,14 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) { TestRecipientHandler expiryHandler; TestRecipientPolicy expiryPolicy; - expiryPolicy.blockAckTimeoutValue = 1; + expiryPolicy.blockAckTimeoutValue = 2; TestCallback expiryCallback; auto request = makeRequest(peer2, 10, 101, SequenceNumberCyclic(310), 32); - request->setBlockAckTimeoutValue(1); + request->setBlockAckTimeoutValue(3); auto firstAgreement = expiryHandler.processReceivedAddbaRequest(request, &expiryPolicy, &expiryCallback, &expiryCallback); ASSERT(firstAgreement != nullptr); auto firstGenerationId = firstAgreement->getGenerationId(); - wait(1); + wait(2); expiryHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); auto stalePacket = expiryCallback.managementPacket; auto staleDelba = dynamicPtrCast(expiryCallback.managementHeader); @@ -5647,17 +5782,23 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) wait(1); TestRecipientHandler recipientHandler; TestRecipientPolicy recipientPolicy; - recipientPolicy.blockAckTimeoutValue = 3; + recipientPolicy.blockAckTimeoutValue = 2; TestCallback setupCallback; auto request = makeRequest(peer2, 12, 109, SequenceNumberCyclic(330), 32); request->setBlockAckTimeoutValue(3); + auto agreementCreationTime = simTime(); auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &setupCallback, &setupCallback); ASSERT(agreement != nullptr); ASSERT(recipientHandler.getActiveAgreement(12, peer2) == agreement); auto creationTime = simTime(); ASSERT(creationTime > 0); - ASSERT(agreement->getExpirationTime() == creationTime + 3); - ASSERT(setupCallback.recipientInactivityDeadline == creationTime + 3); + auto response = dynamicPtrCast(setupCallback.managementHeader); + ASSERT(response != nullptr); + ASSERT(response->getBlockAckTimeoutValue() == 2); + ASSERT(agreement->getBlockAckTimeoutValue() == 2); + ASSERT(agreement->getExpirationTime() == agreementCreationTime + 2); + ASSERT(agreement->getExpirationTime() == creationTime + 2); + ASSERT(setupCallback.recipientInactivityDeadline == creationTime + 2); auto basicBlockAckReq = makeShared(); basicBlockAckReq->setTransmitterAddress(peer2); @@ -5665,9 +5806,9 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) wait(1); auto barReceptionTime = simTime(); recipientHandler.blockAckReqReceived(basicBlockAckReq, &setupCallback); - ASSERT(agreement->getExpirationTime() == barReceptionTime + 3); - ASSERT(agreement->getExpirationTime() > creationTime + 3); - ASSERT(setupCallback.recipientInactivityDeadline == barReceptionTime + 3); + ASSERT(agreement->getExpirationTime() == barReceptionTime + 2); + ASSERT(agreement->getExpirationTime() > creationTime + 2); + ASSERT(setupCallback.recipientInactivityDeadline == barReceptionTime + 2); wait(4); TestCallback expiryCallback; From 47e01bff0d619ee1abb7d354c033f8e2cf9f057f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:05 +0200 Subject: [PATCH 13/16] ieee80211: enforce valid A-MSDU fragmentation and sizing Keep A-MSDUs intact in BasicFragmentationPolicy even when their MPDU length exceeds the configured fragmentation threshold. The basic policy does not implement the capability-gated HE dynamic fragmentation procedure, so splitting these aggregates would produce an invalid fragment sequence. Track the exact serialized A-MSDU body length in BasicMsduAggregationPolicy. Account for the 4-octet alignment padding added after each subframe that becomes non-final, leave the final subframe unpadded, accept an aggregate exactly at the configured maximum, and preserve -1 as the unlimited setting. Add focused regression coverage for oversized A-MSDUs versus ordinary QoS frames, one-, two-, and three-octet padding boundaries, exact-limit acceptance, final-subframe layout, and the unlimited size configuration. --- .../aggregation/BasicMsduAggregationPolicy.cc | 22 +++- .../fragmentation/BasicFragmentationPolicy.cc | 8 +- tests/unit/Ieee80211AddbaTransaction_1.test | 107 ++++++++++++++++++ 3 files changed, 131 insertions(+), 6 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc index 951f011c5ee..a71aaf58c34 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc @@ -10,6 +10,16 @@ namespace inet { namespace ieee80211 { +namespace { + +b computeSerializedAmsduLength(b aMsduLength, const Packet *packet, const Ptr& header, const Ptr& trailer) +{ + int paddingLength = (4 - aMsduLength.get() % 4) % 4; + return aMsduLength + B(paddingLength) + packet->getDataLength() - header->getChunkLength() - trailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER); +} + +} + Define_Module(BasicMsduAggregationPolicy); void BasicMsduAggregationPolicy::initialize() @@ -32,10 +42,12 @@ bool BasicMsduAggregationPolicy::isEligible(Packet *packet, const PtrgetType() != ST_DATA_WITH_QOS) return false; - // The maximum MPDU length that can be transported using A-MPDU aggregation is 4095 octets. An - // A-MSDU cannot be fragmented. Therefore, an A-MSDU of a length that exceeds 4065 octets ( - // 4095 minus the QoS data MPDU overhead) cannot be transported in an A-MPDU. - if (aMsduLength + packet->getDataLength() - header->getChunkLength() - trailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER) > maxAMsduSize) // default value of maxAMsduSize is 4065 + // IEEE Std 802.11-2024, 9.3.2.2.1-9.3.2.2.2 and Figure 9-123: every + // non-final Basic A-MSDU subframe is padded to a 4-octet boundary. The + // final subframe is not padded. An A-MSDU cannot be fragmented by this + // policy, so its complete serialized body must fit the configured limit. + auto serializedAmsduLength = computeSerializedAmsduLength(aMsduLength, packet, header, trailer); + if (maxAMsduSize >= b(0) && serializedAmsduLength > maxAMsduSize) // -1 means infinity return false; // The value of TID present in the QoS Control field of the MPDU carrying the A-MSDU indicates the TID for @@ -91,7 +103,7 @@ std::vector *BasicMsduAggregationPolicy::computeAggregateFrames(queuei if (!isEligible(dataPacket, dataHeader, dataTrailer, firstHeader, aMsduLength)) return false; frames->push_back(dataPacket); - aMsduLength += dataPacket->getDataLength() - dataHeader->getChunkLength() - dataTrailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER); + aMsduLength = computeSerializedAmsduLength(aMsduLength, dataPacket, dataHeader, dataTrailer); return true; }; if (!appendIfEligible(candidate)) { diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc index 9af560f9b3e..a2febbfe211 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc @@ -36,7 +36,13 @@ std::vector BasicFragmentationPolicy::computeFragmentSizes(Packet *frame) return {}; const auto& trailer = frame->peekAtBack(B(4)); int trailerLength = trailer->getChunkLength().get(); - if (dynamicPtrCast(header)) { + if (const auto& dataHeader = dynamicPtrCast(header)) { + // IEEE Std 802.11-2024, 10.2.7 Note 2, 10.4, and 10.11: A-MSDUs are + // fragmented only through the capability-gated HE dynamic + // fragmentation procedure (26.3.2), which this basic policy does + // not implement. + if (dataHeader->getAMsduPresent()) + return {}; headerLength = header->getChunkLength().get(); payloadLength = frame->getByteLength() - headerLength - trailerLength; } diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 458eae5b8f7..0043bdb1605 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -2171,6 +2171,44 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); fragmentationPolicyModule->callFinish(); fragmentationPolicyModule->deleteModule(); + // IEEE Std 802.11-2024, 10.2.7 Note 2, 10.4, and 10.11: A-MSDUs are fragmented + // only with capability-gated HE dynamic fragmentation (26.3.2). The + // basic policy does not implement that procedure, so an oversized + // A-MSDU must remain a single MPDU candidate. + auto makeQosDataPacket = [&](const char *name, SequenceNumberCyclic sequenceNumber, int payloadLength) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(payloadLength))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto firstMsdu = makeQosDataPacket("oversizedAmsduFirst", SequenceNumberCyclic(110), 20); + auto secondMsdu = makeQosDataPacket("oversizedAmsduSecond", SequenceNumberCyclic(111), 20); + auto amsduMembers = new std::vector({ firstMsdu, secondMsdu }); + MsduAggregation msduAggregation; + auto oversizedAmsdu = msduAggregation.aggregateFrames(amsduMembers); + delete amsduMembers; + const auto& oversizedAmsduHeader = oversizedAmsdu->peekAtFront(); + const auto& oversizedAmsduTrailer = oversizedAmsdu->peekAtBack(B(4)); + ASSERT(oversizedAmsduHeader->getAMsduPresent()); + ASSERT((oversizedAmsdu->getDataLength() - oversizedAmsduHeader->getChunkLength() - oversizedAmsduTrailer->getChunkLength()).get() == 70); + ASSERT(oversizedAmsdu->getByteLength() == 100); + + auto amsduFragmentationPolicyModule = cModuleType::get("inet.linklayer.ieee80211.mac.fragmentation.BasicFragmentationPolicy")->create("oversizedAmsduFragmentationPolicy", this); + amsduFragmentationPolicyModule->par("fragmentationThreshold").setIntValue(90); + amsduFragmentationPolicyModule->callInitialize(); + auto amsduFragmentationPolicy = check_and_cast(amsduFragmentationPolicyModule); + ASSERT(oversizedAmsdu->getByteLength() > 90); + ASSERT(amsduFragmentationPolicy->computeFragmentSizes(oversizedAmsdu).empty()); + auto ordinaryQosData = makeQosDataPacket("ordinaryQosData", SequenceNumberCyclic(112), 70); + ASSERT(amsduFragmentationPolicy->computeFragmentSizes(ordinaryQosData) == std::vector({ 60, 10 })); + delete oversizedAmsdu; + delete ordinaryQosData; + amsduFragmentationPolicyModule->callFinish(); + amsduFragmentationPolicyModule->deleteModule(); + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("fragmentedAddbaDataService", this); recipientDataServiceModule->callInitialize(); auto recipientDataService = check_and_cast(recipientDataServiceModule); @@ -3932,6 +3970,75 @@ ASSERT(!dataService.hasEligibleFrame(&mixedQueue)); queue.packets.clear(); } +// IEEE Std 802.11-2024, 9.3.2.2.1-9.3.2.2.2 and Figure 9-123: only +// non-final Basic A-MSDU subframes carry 0-3 octets of alignment padding. +// The policy's size check must include that padding and accept the exact +// serialized limit. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber, int payloadLength) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(payloadLength))); + packet->insertAtBack(makeShared()); + return packet; + }; + const int firstPayloadLengths[] = { 5, 4, 7 }; + const int expectedAmsduLengths[] = { 39, 39, 43 }; + for (int i = 0; i < 3; i++) { + TestPacketQueue queue; + auto first = makeAggregatePacket("paddingBoundaryFirst", SequenceNumberCyclic(15 + i * 2), firstPayloadLengths[i]); + auto second = makeAggregatePacket("paddingBoundarySecond", SequenceNumberCyclic(16 + i * 2), 5); + queue.packets = { first, second }; + TestBasicMsduAggregationPolicy policy; + policy.configure(B(expectedAmsduLengths[i] - 1)); + ASSERT(policy.computeAggregateFrames(&queue, first, [](const Packet *) { return true; }) == nullptr); + policy.configure(B(expectedAmsduLengths[i])); + auto selected = policy.computeAggregateFrames(&queue, first, [](const Packet *) { return true; }); + ASSERT(selected != nullptr); + ASSERT(selected->size() == 2); + ASSERT(selected->at(0) == first); + ASSERT(selected->at(1) == second); + MsduAggregation aggregation; + auto aggregated = aggregation.aggregateFrames(selected); + delete selected; + aggregated->popAtFront(); + aggregated->popAtBack(B(4)); + ASSERT(aggregated->getDataLength().get() == expectedAmsduLengths[i]); + // The expected length ends with the second 14-byte header and its + // 5-byte MSDU; any final padding would make this assertion fail. + const auto& finalSubframeHeader = aggregated->peekDataAt(B(expectedAmsduLengths[i] - 14 - 5), B(14)); + ASSERT(finalSubframeHeader != nullptr); + ASSERT(finalSubframeHeader->getLength() == 5); + delete aggregated; + queue.packets.clear(); + } + + // BasicMsduAggregationPolicy.ned documents -1 as an unlimited maximum. + TestPacketQueue unlimitedQueue; + auto unlimitedFirst = makeAggregatePacket("unlimitedPaddingFirst", SequenceNumberCyclic(21), 7); + auto unlimitedSecond = makeAggregatePacket("unlimitedPaddingSecond", SequenceNumberCyclic(22), 5); + unlimitedQueue.packets = { unlimitedFirst, unlimitedSecond }; + auto unlimitedPolicyModule = cModuleType::get("inet.linklayer.ieee80211.mac.aggregation.BasicMsduAggregationPolicy")->create("unlimitedAmsduPolicy", this); + unlimitedPolicyModule->par("maxAMsduSize").setIntValue(-1); + unlimitedPolicyModule->callInitialize(); + auto unlimitedPolicy = check_and_cast(unlimitedPolicyModule); + auto unlimitedSelection = unlimitedPolicy->computeAggregateFrames(&unlimitedQueue, unlimitedFirst, [](const Packet *) { return true; }); + ASSERT(unlimitedSelection != nullptr); + ASSERT(unlimitedSelection->size() == 2); + MsduAggregation unlimitedAggregation; + auto unlimitedAggregate = unlimitedAggregation.aggregateFrames(unlimitedSelection); + delete unlimitedSelection; + unlimitedAggregate->popAtFront(); + unlimitedAggregate->popAtBack(B(4)); + ASSERT(unlimitedAggregate->getDataLength().get() == 43); + delete unlimitedAggregate; + unlimitedQueue.packets.clear(); + unlimitedPolicyModule->callFinish(); + unlimitedPolicyModule->deleteModule(); +} + // The built-in policy is anchored on the provider-selected candidate. Held // enumeration members do not hide later compatible frames, while an oversized // anchor falls back to ordinary dequeue without aggregating its followers. From 43a47efaedeb5bc9d3da32ab7135dc2eac640739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:06 +0200 Subject: [PATCH 14/16] tests: record wireless fingerprints for the ADDBA rework Record the existing expected fingerprints for the combined ADDBA, Block Ack, reassembly, aggregation, and HCF changes. These wireless QoS configurations exercise several preceding changes together, so this commit records their combined expectations. The affected configurations cover adhoc and wireless QoS, Block Ack, aggregation, fragmentation, and TXOP. Preserve the previously recorded values; no new baseline values are generated during history cleanup. --- tests/fingerprint/examples.csv | 12 ++++++------ tests/fingerprint/showcases.csv | 21 ++++++++++----------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 640e3c86289..4b0e7ba21aa 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -6,8 +6,8 @@ /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping1 -r 0, 1000s, 8668-d4e7/tplx;e2b1-205b/~tNl;4083-075c/~tND;e1f1-f93c/tyf, PASS, wireless adhoc Ipv4 # /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping2 -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, ERROR, wireless adhoc # [Config Ping2] # interactive config, needed a *.numHosts parameter /examples/adhoc/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, 5749-0281/tplx;f38a-cb93/~tNl;fd9b-683f/~tND;0eb8-3e3b/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 1486-cbae/tplx;521b-977a/~tNl;91c2-9aa9/~tND;2c1e-66e5/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, 783f-d09a/tplx;13d1-ec15/~tNl;5aaf-cc7c/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 7852-a806/tplx;6caf-ac5a/~tNl;623f-e442/~tND;2c1e-66e5/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, b403-1b77/tplx;5f3c-e4ec/~tNl;6acf-af46/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c Fragmentation, 10s, 9c66-e6f0/tplx;cc9e-d1a6/~tNl;53c8-e1b0/~tND;33b7-00f9/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MsduAggregation, 10s, 85bd-85c6/tplx;22f6-085d/~tNl;dc0d-4e19/~tND;df04-18f7/tyf, PASS, wireless adhoc Ipv4 @@ -655,11 +655,11 @@ /examples/wireless/power/, -f omnetpp.ini -c General -r 0, 100s, 6fae-d558/tplx;b8ea-b2fc/~tNl;5ea8-2cea/~tND, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, f252-8a4b/tplx;481d-4747/~tNl;7061-7f46/~tND;6097-a429/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 31b0-6212/tplx;82ec-9fde/~tNl;70c1-79a0/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 9433-0c25/tplx;c766-ffd5/~tNl;2895-311c/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, acd6-0108/tplx;b339-294a/~tNl;58ed-6137/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 9ece-fbfb/tplx;c1af-29ff/~tNl;01a6-2535/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, d094-b008/tplx;173a-e3fb/~tNl;82c5-b383/~tND;08b5-d005/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithTransactionalBlockAck -r 0, 3s, c4ff-d71f/tplx;68ba-2827/~tNl;9a98-cc02/~tND, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 488a-06bf/tplx;e9ff-6499/~tNl;3059-d2c3/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, 194e-63be/tplx;160c-9b74/~tNl;54bf-44f9/~tND;08b5-d005/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithTransactionalBlockAck -r 0, 3s, 25a9-9bce/tplx;ef3a-49ad/~tNl;a969-5b95/~tND, PASS, wireless Ipv4 /examples/wireless/ratecontrol/, -f omnetpp.ini -c Mac -r 0, 100s, bf30-2f13/tplx;7b2f-653d/~tNl;6e1e-3b7b/~tND;19fe-8b0e/tyf, PASS, wireless diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index ef401f4edb1..5e10dfe4b26 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -192,16 +192,16 @@ /showcases/visualizer/canvas/transportpathactivity/, -f omnetpp.ini -c Filtering -r 0, 100s, 8bcf-d426/tplx;8f09-7a80/~tNl;92a4-c3ec/~tND;0694-d5be/tyf, PASS, wireless EthernetMac Ipv4 /showcases/wireless/aggregation/, -f omnetpp.ini -c NoAggregation -r 0, 1s, 97f6-a969/tplx;ecf2-6e33/~tNl;9dbf-129b/~tND;27f2-4a52/tyf, PASS, wireless Ipv4 -/showcases/wireless/aggregation/, -f omnetpp.ini -c Aggregation -r 0, 1s, 671b-274c/tplx;6d84-9720/~tNl;0c45-ca5f/~tND;95cc-1859/tyf, PASS, wireless Ipv4 -/showcases/wireless/aggregation/, -f omnetpp.ini -c VoicePriorityAggregation -r 0, 1s, 1dbe-6672/tplx;a253-dbe3/~tNl;d167-ba3e/~tND;bd88-5ba6/tyf, PASS, wireless Ipv4 +/showcases/wireless/aggregation/, -f omnetpp.ini -c Aggregation -r 0, 1s, 58d0-b4a4/tplx;2c02-b385/~tNl;c0e1-cb62/~tND;95cc-1859/tyf, PASS, wireless Ipv4 +/showcases/wireless/aggregation/, -f omnetpp.ini -c VoicePriorityAggregation -r 0, 1s, 3f9d-465e/tplx;78b5-09d7/~tNl;5940-c3fb/~tND;bd88-5ba6/tyf, PASS, wireless Ipv4 /showcases/wireless/analogmodel/, -f omnetpp.ini -c Routing -r 0, 5s, 56b0-3510/tplx;baeb-0d2c/~tNl;378f-fb30/~tND;00a2-e4ee/tyf, PASS, wireless Ipv4 /showcases/wireless/analogmodel/, -f omnetpp.ini -c Distance -r 0, 2.5s, 1e75-270e/tplx;6d7a-d84c/~tNl;b380-6cd5/~tND;5575-fd8f/tyf, PASS, wireless Ipv4 /showcases/wireless/analogmodel/, -f omnetpp.ini -c Noise -r 0, 0.1s, dd08-a63c/tplx;e167-9c84/~tNl;643d-41a6/~tND;0d1f-df73/tyf, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, aa2d-5d35/tplx;2094-1f2a/~tNl;1470-1e1b/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, 7ae9-e07d/tplx;db8b-3b81/~tNl;9c41-dc97/~tND, PASS, wireless Ipv4 -/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 462d-10c7/tplx;727b-d26a/~tNl;62c4-cbc2/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c NoFragmentation -r 0, 1s, 7ecf-66f5/tplx;193b-2155/~tNl;fa4f-92fc/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c Fragmentation -r 0, 1s, fda2-be01/tplx;be55-bf3e/~tNl;0edc-72bf/~tND, PASS, wireless Ipv4 +/showcases/wireless/blockack/, -f omnetpp.ini -c MixedTraffic -r 0, 1s, 3530-5a7d/tplx;4bee-ff14/~tNl;7588-8d92/~tND, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c CompletelyOverlappingFrequencyBands -r 0, 1s, d0d7-43e0/tplx;867a-07a4/~tNl;df78-8445/~tND;3ea3-43da/tyf, PASS, wireless Ipv4 /showcases/wireless/crosstalk/, -f omnetpp.ini -c IndependentFrequencyBandsOneRadioMediumModule -r 0, 1s, 70c6-72b6/tplx;cf96-5e4d/~tNl;3cba-ae59/~tND;d1b2-9fd4/tyf, PASS, wireless Ipv4 @@ -266,8 +266,8 @@ /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFnofrag -r 0, 1s, 52b9-628f/tplx;3fec-74a2/~tNl;6073-4582/~tND;8871-1dd1/tyf, PASS, wireless Ipv4 /showcases/wireless/fragmentation/, -f omnetpp.ini -c DCFfrag -r 0, 1s, 57ee-7ddf/tplx;dab9-5e8d/~tNl;7f9b-00fc/~tND;f985-34fb/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, f7a5-cf0a/tplx;a24a-a070/~tNl;7743-bba4/~tND;335d-6687/tyf, PASS, wireless Ipv4 -/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 0702-c692/tplx;1bf9-b035/~tNl;9a04-4420/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfrag -r 0, 1s, 4b00-315e/tplx;a24a-a070/~tNl;7743-bba4/~tND;335d-6687/tyf, PASS, wireless Ipv4 +/showcases/wireless/fragmentation/, -f omnetpp.ini -c HCFfragblockack -r 0, 1s, 5e75-c958/tplx;99a8-4fc5/~tNl;a034-1f59/~tND;6f6e-b101/tyf, PASS, wireless Ipv4 /showcases/wireless/handover/, -f omnetpp.ini -c General -r 0, 250s, b287-6940/tplx;45c3-a292/~tNl;99ae-332d/~tND;02e9-9ad3/tyf, PASS, wireless @@ -313,8 +313,8 @@ /showcases/wireless/power/, -f omnetpp.ini -c General -r 0, 100s, 498f-b665/tplx;6f50-5caf/~tNl;0ad4-1089/~tND, PASS, wireless Ipv4 -/showcases/wireless/qos/, -f omnetpp.ini -c NonQos -r 0, 10s, 37fd-5401/tplx;92f7-198c/~tNl;955c-ee96/~tND;093e-1ca4/tyf, PASS, wireless Ipv4 -/showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, 1a49-72b3/tplx;e605-d79d/~tNl;10fc-7bb0/~tND;bda9-15d1/tyf, PASS, wireless Ipv4 +/showcases/wireless/qos/, -f omnetpp.ini -c NonQos -r 0, 10s, 37fd-5401/tplx;92f7-198c/~tNl;972d-a61c/~tND;093e-1ca4/tyf, PASS, wireless Ipv4 +/showcases/wireless/qos/, -f omnetpp.ini -c Qos -r 0, 10s, b58c-3dc7/tplx;2b64-87ff/~tNl;9f61-f2fe/~tND;bda9-15d1/tyf, PASS, wireless Ipv4 /showcases/wireless/ratecontrol/, -f omnetpp.ini -c NoRateControl -r 0, 14s, 7ee9-503a/tplx;0816-e58f/~tNl;648e-6e84/~tND;dad3-7f89/tyf, PASS, wireless Ipv4 /showcases/wireless/ratecontrol/, -f omnetpp.ini -c AarfRateControl -r 0, 12s, a7bc-05bb/tplx;9de0-4dd3/~tNl;1209-101b/~tND;7539-d32d/tyf, PASS, wireless Ipv4 @@ -341,5 +341,4 @@ /showcases/wireless/throughput/, -f omnetpp.ini -c General -r 0, 1s, 030e-c416/tplx;66e6-0bca/~tNl;3cb6-43bd/~tND;e6a5-bde0/tyf, PASS, wireless Ipv4 -/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, d2b6-a5d1/tplx;3a6f-4c28/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 - +/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, 1f97-be23/tplx;22a3-c268/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 From c632762b9a5269b7865c0e387ab18d7cc8f2db45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:06 +0200 Subject: [PATCH 15/16] ieee80211: preserve management bodies during fragmentation Generalize embedded body extraction to management headers so fragmentation repeats only the common MAC header. Keep local action context specific to action frames. Exercise a typed association response with a 32-byte fragmentation threshold and verify the reconstructed body bytes and fragment headers. --- .../mac/fragmentation/Fragmentation.cc | 14 ++--- tests/unit/Ieee80211MgmtTransactionTag_1.test | 51 +++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc index 4465c22ecf3..2f5897ebf0c 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc @@ -51,11 +51,12 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve const auto& frameHeader = frame->popAtFront(); frame->popAtBack(B(4)); const auto& actionFrame = dynamicPtrCast(frameHeader); - if (actionFrame != nullptr) { + const auto& managementHeader = dynamicPtrCast(frameHeader); + if (managementHeader != nullptr && managementHeader->getChunkLength() > makeShared()->getChunkLength()) { // IEEE Std 802.11-2024, 10.4: a fragment frame body carries only a - // portion of the MMPDU. Move the action body out of INET's combined + // portion of the MMPDU. Move the management body out of INET's combined // typed header before slicing it into fragment bodies. - Packet serializedHeader("serializedActionHeader", frameHeader); + Packet serializedHeader("serializedManagementHeader", frameHeader); const auto& headerBytes = serializedHeader.peekDataAsBytes()->getBytes(); auto bodyOffset = makeShared()->getChunkLength().get(); frame->insertAtFront(makeShared(std::vector(headerBytes.begin() + bodyOffset, headerBytes.end()))); @@ -75,15 +76,16 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve fragment->getRegionTags().copyTags(frame->getRegionTags(), frame->getFrontOffset() + offset, fragment->getFrontOffset(), length); offset += length; Ptr fragmentHeader; + if (managementHeader != nullptr) + fragmentHeader = copyManagementHeader(managementHeader); + else + fragmentHeader = staticPtrCast(frameHeader->dupShared()); if (actionFrame != nullptr) { - fragmentHeader = copyManagementHeader(actionFrame); auto actionContext = staticPtrCast(actionFrame->dupShared()); actionContext->setFragmentNumber(i); actionContext->setMoreFragments(!lastFragment); fragment->addTag()->setActionFrame(actionContext); } - else - fragmentHeader = staticPtrCast(frameHeader->dupShared()); fragmentHeader->setSequenceNumber(frameHeader->getSequenceNumber()); fragmentHeader->setFragmentNumber(i); fragmentHeader->setMoreFragments(!lastFragment); diff --git a/tests/unit/Ieee80211MgmtTransactionTag_1.test b/tests/unit/Ieee80211MgmtTransactionTag_1.test index 568716711fc..90ff486609b 100644 --- a/tests/unit/Ieee80211MgmtTransactionTag_1.test +++ b/tests/unit/Ieee80211MgmtTransactionTag_1.test @@ -9,6 +9,9 @@ generic body region tag is clipped and rebased for each fragment. #include "inet/common/TimeTag_m.h" #include "inet/common/packet/chunk/BytesChunk.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" @@ -53,6 +56,54 @@ for (auto fragment : *fragments) delete fragment; delete fragments; +// Exercise the policy and fragmentation with an actual typed non-action response. +auto policyModule = cModuleType::get("inet.linklayer.ieee80211.mac.fragmentation.BasicFragmentationPolicy")->create("fragmentationPolicy", this); +policyModule->par("fragmentationThreshold").setIntValue(32); +policyModule->callInitialize(); +auto policy = check_and_cast(policyModule); +auto response = makeShared(); +response->setStatusCode(SC_SUCCESSFUL); +response->setAid(291); +Ieee80211SupportedRatesElement rates; +rates.numRates = 1; +rates.rate[0] = 6; +response->setSupportedRates(rates); +response->setChunkLength(B(9)); +auto responsePacket = new Packet("typedAssociationResponse", response); +auto responseHeader = makeShared(); +responseHeader->setType(ST_ASSOCIATIONRESPONSE); +responseHeader->setReceiverAddress(MacAddress("02:00:00:00:00:01")); +responseHeader->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); +responseHeader->setSequenceNumber(SequenceNumberCyclic(17)); +responsePacket->insertAtFront(responseHeader); +responsePacket->insertAtBack(makeShared()); +auto sizes = policy->computeFragmentSizes(responsePacket); +ASSERT(sizes == std::vector({4, 4, 1})); +auto responseFragments = fragmentation.fragmentFrame(responsePacket, sizes); +std::vector reassembledBody; +for (size_t i = 0; i < responseFragments->size(); i++) { + auto fragment = responseFragments->at(i); + ASSERT(fragment->getByteLength() <= 32); + ASSERT(fragment->findTag() == nullptr); + auto fragmentHeader = fragment->popAtFront(); + ASSERT(fragmentHeader->getChunkLength() == B(24)); + ASSERT(fragmentHeader->getType() == ST_ASSOCIATIONRESPONSE); + ASSERT(fragmentHeader->getReceiverAddress() == responseHeader->getReceiverAddress()); + ASSERT(fragmentHeader->getTransmitterAddress() == responseHeader->getTransmitterAddress()); + ASSERT(fragmentHeader->getSequenceNumber() == SequenceNumberCyclic(17)); + ASSERT(fragmentHeader->getFragmentNumber() == i); + ASSERT(fragmentHeader->getMoreFragments() == (i + 1 < responseFragments->size())); + fragment->popAtBack(B(4)); + auto bytes = fragment->peekDataAsBytes()->getBytes(); + ASSERT(bytes.size() == sizes[i]); + reassembledBody.insert(reassembledBody.end(), bytes.begin(), bytes.end()); + delete fragment; +} +ASSERT(reassembledBody == std::vector({0, 0, 0, 0, 0x23, 0xC1, 1, 1, 0x0C})); +delete responseFragments; +policyModule->callFinish(); +policyModule->deleteModule(); + EV << "Management transaction packet tag survived frame replacement.\n"; %contains: stdout From 88eac86941bd010270e589461cc49cbaa84848d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:06 +0200 Subject: [PATCH 16/16] ieee80211: flag malformed peer association IDs without throwing Malformed association and reassociation response AIDs must be represented as incorrect peer input instead of escaping deserialization as cRuntimeError. Return a safe zero AID while allowing subsequent elements to decode. Cover missing marker bits, invalid successful AIDs, and nonzero unsuccessful AIDs for both response types. Retain strict validation of locally serialized AIDs. --- .../mgmt/Ieee80211MgmtFrameSerializer.cc | 16 ++++----- .../unit/Ieee80211MgmtFrameSerializer_1.test | 34 +++++++++++++------ 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc index 09f3ffbd05b..efe8648f805 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc @@ -452,18 +452,18 @@ static uint16_t encodeAssociationId(Ieee80211StatusCode statusCode, int aid) return 0; } -static int decodeAssociationId(Ieee80211StatusCode statusCode, uint16_t wireAid) +static int decodeAssociationId(Ieee80211MgmtFrame& frame, Ieee80211StatusCode statusCode, uint16_t wireAid) { if (statusCode == SC_SUCCESSFUL) { - if ((wireAid & ASSOCIATION_ID_MARKER) != ASSOCIATION_ID_MARKER) - throw cRuntimeError("Malformed successful Association Response AID: missing marker 0xC000"); const int aid = wireAid & ASSOCIATION_ID_MASK; - if (aid < 1 || aid > MAX_LOGICAL_ASSOCIATION_ID) - throw cRuntimeError("Malformed successful Association Response AID: %d", aid); + if ((wireAid & ASSOCIATION_ID_MARKER) != ASSOCIATION_ID_MARKER || aid < 1 || aid > MAX_LOGICAL_ASSOCIATION_ID) { + frame.markIncorrect(); + return 0; + } return aid; } if (wireAid != 0) - throw cRuntimeError("Malformed unsuccessful Association Response AID: expected zero, got 0x%04x", wireAid); + frame.markIncorrect(); return 0; } @@ -735,7 +735,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre auto frame = makeShared(); stream.readUint16Le(); frame->setStatusCode((Ieee80211StatusCode)stream.readUint16Le()); - frame->setAid(decodeAssociationId(frame->getStatusCode(), stream.readUint16Le())); + frame->setAid(decodeAssociationId(*frame, frame->getStatusCode(), stream.readUint16Le())); Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); @@ -747,7 +747,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserializeFields(MemoryInputStre auto frame = makeShared(); stream.readUint16Le(); frame->setStatusCode((Ieee80211StatusCode)stream.readUint16Le()); - frame->setAid(decodeAssociationId(frame->getStatusCode(), stream.readUint16Le())); + frame->setAid(decodeAssociationId(*frame, frame->getStatusCode(), stream.readUint16Le())); Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); diff --git a/tests/unit/Ieee80211MgmtFrameSerializer_1.test b/tests/unit/Ieee80211MgmtFrameSerializer_1.test index f387d6b4c81..6d250c6bcdb 100644 --- a/tests/unit/Ieee80211MgmtFrameSerializer_1.test +++ b/tests/unit/Ieee80211MgmtFrameSerializer_1.test @@ -172,6 +172,27 @@ static void checkMalformedSupportedRates(const std::vector& prefix) ASSERT(zeroLength->getSupportedRates().numRates == 0); } +template +static void checkMalformedAssociationIds() +{ + for (uint16_t wireAid : {0x0001, 0x4001, 0x8001, 0xC000, 0xC7D8, 0xFFFF}) { + auto frame = deserializeMalformedBody({0, 0, 0, 0, + static_cast(wireAid), static_cast(wireAid >> 8), 1, 1, 0x0C}); + ASSERT(frame->isIncorrect()); + ASSERT(frame->isComplete()); + ASSERT(frame->getAid() == 0); + ASSERT(frame->getSupportedRates().numRates == 1); + } + for (uint16_t wireAid : {0x0001, 0xC000, 0xC001}) { + auto frame = deserializeMalformedBody({0, 0, 1, 0, + static_cast(wireAid), static_cast(wireAid >> 8), 1, 1, 0x0C}); + ASSERT(frame->isIncorrect()); + ASSERT(frame->isComplete()); + ASSERT(frame->getAid() == 0); + ASSERT(frame->getSupportedRates().numRates == 1); + } +} + %activity: { @@ -267,17 +288,8 @@ static void checkMalformedSupportedRates(const std::vector& prefix) ASSERT(frame->getAid() == 0x0245); } -bool missingAssociationIdMarkerRejected = false; -try { - deserializeBody({ - 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, - 0x01, 0x01, 0x0C - }); -} -catch (const cRuntimeError&) { - missingAssociationIdMarkerRejected = true; -} -ASSERT(missingAssociationIdMarkerRejected); +checkMalformedAssociationIds(); +checkMalformedAssociationIds(); bool successfulZeroAssociationIdRejected = false; try {