diff --git a/WHATSNEW b/WHATSNEW index 5fdd7668423..6ad192716e7 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -29,6 +29,77 @@ 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. + 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 + 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. + +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 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 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 + 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/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/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/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/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/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/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/aggregation/BasicMsduAggregationPolicy.cc b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc index 2d06484c6a4..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 @@ -56,40 +68,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 = computeSerializedAmsduLength(aMsduLength, dataPacket, dataHeader, dataTrailer); + 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/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/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index a516527bc34..25bad8d77a2 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,27 @@ 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; + // 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) : + 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 +60,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,21 +69,29 @@ 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; } + 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 */ } /* 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..f69625097c3 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -7,24 +7,38 @@ #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/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.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; 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()); @@ -33,30 +47,91 @@ simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime() return earliestTime; } -void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) +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); +} + +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->getExpirationTime() == now) { + 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) { 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()); @@ -74,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) { + auto agreement = getActiveAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); + if (agreement != nullptr) { agreement->setStartingSequenceNumber(basicBlockAck->getStartingSequenceNumber()); agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); @@ -88,8 +163,13 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const PtrscheduleInactivityTimer(earliestExpirationTime); + if (callback != nullptr) + callback->scheduleInactivityTimer(BlockAckAgreementRole::ORIGINATOR, earliestExpirationTime); +} + +void OriginatorBlockAckAgreementHandler::scheduleAddbaResponseTimer(IBlockAckAgreementHandlerCallback *callback) +{ + callback->scheduleAddbaResponseTimer(computeEarliestAddbaResponseDeadline()); } OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(MacAddress receiverAddr, Tid tid) @@ -99,6 +179,18 @@ 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)); + return it != blockAckAgreements.end() && it->second->isPending(); +} + const Ptr OriginatorBlockAckAgreementHandler::buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode) { auto delba = makeShared(); @@ -110,37 +202,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 +309,168 @@ 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 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) { 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 = 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 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; + 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())); + if (terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getReceiverAddress(), delba->getTid()); + scheduleInactivityTimer(callback); + 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(); + 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) + callback->cancelAddbaTransaction(transactionId, nullptr); + return terminatedAgreement; +} + +bool OriginatorBlockAckAgreementHandler::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 it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it == pendingTeardownTransactionIds.end() || it->second != agreementTag->getGenerationId()) + return false; + auto transactionId = it->second; + pendingTeardownTransactionIds.erase(it); + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); + return true; +} + +OriginatorBlockAckAgreementAbortResult OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = findFragmentedActionContext(packet); + if (!delba->getInitiator()) + return {}; + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { + 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); + 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())); + 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); + return result; + } + } + return {}; +} + +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; + 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())); + if (terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getTransmitterAddress(), delba->getTid()); + scheduleInactivityTimer(callback); + 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; } OriginatorBlockAckAgreementHandler::~OriginatorBlockAckAgreementHandler() @@ -180,4 +481,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..96e6ca12721 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -21,31 +21,50 @@ 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; 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 void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) 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 OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) 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; }; } // 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/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 40a4186fead..85ef1f9e613 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -21,11 +21,14 @@ class INET_API RecipientBlockAckAgreement : public cObject SequenceNumberCyclic startingSequenceNumber; int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; - bool isAddbaResponseSent = false; 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: - 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); @@ -34,10 +37,15 @@ 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 addbaResposneSent() { isAddbaResponseSent = true; } - 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); }; @@ -45,4 +53,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..4dcad4f4bea 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 { @@ -17,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; } @@ -25,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 @@ -38,53 +41,48 @@ 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) + auto agreement = getActiveAgreement(tid, originatorAddr); + if (agreement != nullptr) { + agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); + } } } -void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) +// 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 = getActiveAgreement(blockAckReq->getTidInfo(), blockAckReq->getTransmitterAddress()); + if (agreement != nullptr) { + agreement->calculateExpirationTime(); + scheduleInactivityTimer(callback); + } +} + +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->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); 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); -} - -// -// 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; + return expired; } // @@ -102,41 +100,42 @@ const Ptr RecipientBlockAckAgreementHandler::buildDelba(MacAddre return delba; } -const Ptr RecipientBlockAckAgreementHandler::buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) +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(); 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); 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; } -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 +145,186 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getAgreement(Tid return it != blockAckAgreements.end() ? it->second : nullptr; } -void RecipientBlockAckAgreementHandler::processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getActiveAgreement(Tid tid, MacAddress originatorAddr) { - updateAgreement(addbaResp); - scheduleInactivityTimer(callback); + auto agreement = getAgreement(tid, originatorAddr); + return agreement != nullptr && !agreement->isInactivityExpired() ? agreement : nullptr; } -void RecipientBlockAckAgreementHandler::processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { 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); + 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. + 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; + } + else + blockAckAgreements[id] = agreement; + scheduleInactivityTimer(agreementHandlerCallback); } + procedureCallback->processMgmtFrame(addbaResponsePacket, addbaResponse); + return agreement; } -void RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) +void RecipientBlockAckAgreementHandler::processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) { - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + 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); + } +} + +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, IBlockAckAgreementHandlerCallback *callback) +{ + 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())); + scheduleInactivityTimer(callback); + pendingTeardownGenerationIds[agreementId] = agreementTag->getGenerationId(); + return terminatedAgreement; + } + auto teardownIt = pendingTeardownGenerationIds.find(agreementId); + if (teardownIt == pendingTeardownGenerationIds.end() || teardownIt->second != agreementTag->getGenerationId()) + return nullptr; + return nullptr; + } + auto terminatedAgreement = std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); + return terminatedAgreement; +} + +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; +} + +RecipientBlockAckAgreementAbortResult RecipientBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = findFragmentedActionContext(packet); + if (delba->getInitiator()) + return {}; + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) + return {}; + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + auto it = pendingTeardownGenerationIds.find(agreementId); + 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 +{ + auto it = pendingTeardownGenerationIds.find(std::make_pair(originatorAddr, tid)); + return it == pendingTeardownGenerationIds.end() ? 0 : it->second; } -void RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) +std::unique_ptr RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) { - if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - terminateAgreement(delba->getReceiverAddress(), 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())); + scheduleInactivityTimer(callback); + return terminatedAgreement; + } + return nullptr; } RecipientBlockAckAgreementHandler::~RecipientBlockAckAgreementHandler() @@ -185,4 +335,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..7c4ce21b5b9 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -26,30 +26,39 @@ 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 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 uint64_t allocateAgreementGenerationId(); 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, IBlockAckAgreementHandlerCallback *callback = nullptr) override; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processTransmittedDelba(const Ptr& delba) override; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback = nullptr) override; + 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 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; }; } // namespace ieee80211 } // namespace inet #endif - 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/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 47837ead38a..3f4801ab76f 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -7,22 +7,37 @@ #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. [...] // -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); @@ -32,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; } // @@ -112,8 +148,8 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::collectCompletePrecedingMp auto sequenceNumber = it.first; auto fragments = it.second; if (SequenceNumberCyclic(sequenceNumber) < startingSequenceNumber) - if (isComplete(fragments)) - completePrecedingMpdus[sequenceNumber] = fragments; + if (ReceiveBuffer::isComplete(fragments)) + completePrecedingMpdus[sequenceNumber] = sortFragmentsByFragmentNumber(fragments); } return completePrecedingMpdus; } @@ -139,8 +175,8 @@ bool BlockAckReordering::addMsduIfComplete(ReceiveBuffer *receiveBuffer, Reorder auto it = buffer.find(seqNum.get()); if (it != buffer.end()) { auto fragments = it->second; - if (isComplete(fragments)) { - reorderBuffer[seqNum.get()] = fragments; + if (ReceiveBuffer::isComplete(fragments)) { + reorderBuffer[seqNum.get()] = sortFragmentsByFragmentNumber(fragments); return true; } } @@ -155,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(); @@ -185,18 +208,40 @@ ReceiveBuffer *BlockAckReordering::createReceiveBufferIfNecessary(RecipientBlock return it->second; } -void BlockAckReordering::processReceivedDelba(const Ptr& delba) +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) { - Tid tid = delba->getTid(); - MacAddress originatorAddr = delba->getTransmitterAddress(); + 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; 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) @@ -216,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; @@ -226,14 +271,14 @@ 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; } } } } - return earliestFragments; + return sortFragmentsByFragmentNumber(earliestFragments); } BlockAckReordering::~BlockAckReordering() @@ -244,4 +289,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..8dd641c7439 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -26,24 +26,39 @@ 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; + simtime_t maxReceiveLifetime = SIMTIME_MAX; protected: + static Fragments sortFragmentsByFragmentNumber(const Fragments& fragments); ReorderBuffer collectCompletePrecedingMpdus(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); 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(); - void processReceivedDelba(const Ptr& delba); + 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); }; @@ -52,4 +67,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..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,12 +120,68 @@ 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 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(); + 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) { @@ -87,4 +192,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..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,19 +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; } }; @@ -50,4 +73,3 @@ class INET_API ReceiveBuffer } /* namespace inet */ #endif - 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..f73bcda128e 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -9,20 +9,44 @@ #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 { + +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; + // 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. + virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) {} }; } // namespace ieee80211 } // 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/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..64a8302f9db 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,56 @@ 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; +}; + +struct INET_API OriginatorBlockAckAgreementAbortResult +{ + bool handled = false; + std::unique_ptr terminatedAgreement; + + explicit operator bool() const { return handled; } +}; + 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 blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 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 OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 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; } }; } // 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/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/IReassembly.h b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h index 23796ec3556..a2187b76f32 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; @@ -33,15 +38,27 @@ class INET_API IReassembly virtual Packet *addFragment(Packet *frame) = 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. + * 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 void purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) = 0; + virtual std::vector removeExpiredFragments(simtime_t currentTime) = 0; + + /** + * 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 std::vector purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) = 0; }; } // namespace ieee80211 } // 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..8811e70f411 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" @@ -17,25 +19,44 @@ #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" namespace inet { + +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: 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, IBlockAckAgreementHandlerCallback *callback = nullptr) = 0; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback = nullptr) = 0; + virtual bool processAcknowledgedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return false; } + virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return {}; } virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; + virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) = 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; } }; } // namespace ieee80211 } // 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..2e7436303de 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: + 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; + }; + 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/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 ddb1cf9aa65..350e551ee30 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -7,11 +7,14 @@ #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" #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" @@ -53,7 +56,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); } } @@ -80,6 +83,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()); } @@ -121,19 +130,34 @@ 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 (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 Dcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) { - if (signalID == packetDroppedSignal) { - Enter_Method("%s", cComponent::getSignalName(signalID)); - auto packet = check_and_cast(obj); - if (packet->findTag() != nullptr) { + Enter_Method("handlePacketRemoved"); + 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); } } - else - ModeSetListener::receiveSignal(source, signalID, obj, details); +} + +void Dcf::cancelManagementTransaction(uint64_t transactionId) +{ + Enter_Method("cancelManagementTransaction"); + cancelManagementTransaction(transactionId, nullptr); } void Dcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header) @@ -147,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"); @@ -214,6 +367,7 @@ void Dcf::frameSequenceFinished() if (hasFrameToTransmit()) channelAccess->requestChannel(this); mac->sendDownPendingRadioConfigMsg(); // TODO review + cancelledManagementTransactions.clear(); } bool Dcf::isReceptionInProgress() @@ -276,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"; @@ -290,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); @@ -301,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; @@ -356,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); @@ -369,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"; @@ -377,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); @@ -429,4 +611,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..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" @@ -28,6 +30,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 { @@ -79,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; @@ -87,7 +101,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(); @@ -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; @@ -118,12 +136,16 @@ 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; 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; @@ -134,4 +156,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..e2f25d68921 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -7,13 +7,21 @@ #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + +#include + #include "inet/common/ModuleAccess.h" #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/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" +#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" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" @@ -28,6 +36,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); @@ -38,6 +47,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"))); @@ -63,14 +73,241 @@ void Hcf::initialize(int stage) originatorBlockAckAgreementHandler = new OriginatorBlockAckAgreementHandler(); originatorBlockAckProcedure = new OriginatorBlockAckProcedure(); recipientBlockAckProcedure = new RecipientBlockAckProcedure(); + originatorDataService->setFrameEligibilityFunction([this](const Packet *packet) { + if (auto addbaReq = findFragmentedActionContext(packet)) + return originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq); + 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 + // 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(packetDroppedSignal, 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 = findFragmentedActionContext(packet); + 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 == 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 = 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; + } + 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) { + 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); + } + shouldResume |= processDroppedBlockAckSetupFrame(packet); + shouldResume |= processDroppedBlockAckTeardownFrame(packet); } + if (shouldResume) + 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 @@ -101,8 +338,23 @@ 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"); + } + else if (msg == addbaResponseTimer) { + if (originatorBlockAckAgreementHandler) { + originatorBlockAckAgreementHandler->addbaResponseTimeoutExpired(originatorBlockAckAgreementPolicy, this); + rebuildPendingFrameEligibility(); + resumeEligibleChannelAccess(); } else throw cRuntimeError("Unknown event"); @@ -144,8 +396,9 @@ void Hcf::processUpperFrame(Packet *packet, const PtrgetEdcaf(ac)->getPendingQueue(); + trackPendingFrame(packet, ac); pendingQueue->enqueuePacket(packet); - if (!pendingQueue->isEmpty()) { + if (!blockAckInactivityExpiryInProgress && hasFrameToTransmit(ac)) { auto edcaf = edca->getChannelOwner(); if (edcaf == nullptr || edcaf->getAccessCategory() != ac) { EV_DETAIL << "Requesting channel for access category " << printAccessCategory(ac) << endl; @@ -154,19 +407,49 @@ 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 (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; } - else - ModeSetListener::receiveSignal(source, signalID, obj, details); + 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) @@ -175,10 +458,151 @@ 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) +{ + Enter_Method("scheduleAddbaResponseTimer"); + if (deadline == SIMTIME_MAX) { + if (addbaResponseTimer->isScheduled()) + cancelEvent(addbaResponseTimer); + } + else + 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"); + // 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::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) @@ -225,13 +649,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 @@ -251,12 +684,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; @@ -277,14 +716,19 @@ void 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); edcaf->getAckHandler()->dropFrame(internallyCollidedHeader); PacketDropDetails details; details.setReason(RETRY_LIMIT_REACHED); 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); @@ -295,6 +739,7 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) else edcaf->requestChannel(this); } + return handledCollisions; } /* @@ -309,12 +754,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); @@ -323,6 +767,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) @@ -337,8 +782,10 @@ 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); + if (receptionResult.completeHeader != nullptr) + recipientProcessReceivedManagementFrame(receptionResult.completeHeader, receptionResult.duplicate); } else { // TODO else if (auto ctrlFrame = dynamic_cast(frame)) sendUp(recipientDataService->controlFrameReceived(packet, header, recipientBlockAckAgreementHandler)); @@ -352,6 +799,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); } @@ -361,29 +810,82 @@ 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); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); + } } else if (auto addbaResp = dynamicPtrCast(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()->setGenerationId(response.teardownTransactionId); + processMgmtFrame(delbaPacket, response.teardownDelba); + } + resumeEligibleChannelAccess(); } else if (auto delba = dynamicPtrCast(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 pendingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); + auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy, this); + 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 { - 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) { + if (agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); + } + resumeEligibleChannelAccess(); } } else @@ -409,6 +911,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) { @@ -424,24 +939,38 @@ 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 = findFragmentedActionContext(packet); + 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); + auto transactionTag = packet->findTag(); + bool notifyManagement = retryLimitReached && dynamicPtrCast(protectedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), packet)); 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 (notifyManagement) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } } } } @@ -452,11 +981,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(); @@ -466,7 +999,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); } @@ -480,28 +1013,50 @@ void Hcf::originatorProcessTransmittedDataFrame(Packet *packet, const PtrgetEdcaf(ac); edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(dataHeader); - if (originatorBlockAckAgreementHandler) - originatorBlockAckAgreementHandler->processTransmittedDataFrame(packet, dataHeader, originatorBlockAckAgreementPolicy, this); + 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); } -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 (auto addbaReq = findFragmentedActionContext(packet)) { 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); - else - recipientBlockAckAgreementHandler->processTransmittedDelba(delba); + else if (findFragmentedActionContext(packet)) + ; // Recipient agreement was established when the successful response was formed. + else if (auto delba = findFragmentedActionContext(packet)) { + if (delba->getInitiator()) { + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getReceiverAddress(), delba->getTid()); + auto agreement = originatorBlockAckAgreementHandler->processTransmittedDelba(packet, this); + if (wasPending) + rebuildPendingFrameEligibility(); + if (agreement != nullptr) { + if (agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); + } + } + else { + 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 + // 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(); + } + } } else ; // TODO other mgmt frames if needed } @@ -520,6 +1075,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) { @@ -551,23 +1119,37 @@ 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 = findFragmentedActionContext(failedPacket); + 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); + auto transactionTag = failedPacket->findTag(); + bool notifyManagement = retryLimitReached && dynamicPtrCast(failedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), failedPacket)); 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 (notifyManagement) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); + } } } else { @@ -633,6 +1215,11 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); + if (auto delba = findFragmentedActionContext(lastTransmittedPacket)) { + bool acknowledged = delba->getInitiator() ? originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this) : recipientBlockAckAgreementHandler != nullptr && recipientBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this); + if (acknowledged) + rebuildPendingFrameEligibility(); + } edcaf->getInProgressFrames()->dropFrame(lastTransmittedPacket); edcaf->getAckHandler()->dropFrame(lastTransmittedDataOrMgmtHeader); if (dynamicPtrCast(lastTransmittedHeader)) { @@ -640,13 +1227,28 @@ 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) { + cancelBlockAckTeardown(true, dataHeader->getReceiverAddress(), dataHeader->getTid(), obsoleteTeardownTransactionId, nullptr); + rebuildPendingFrameEligibility(); + } + if (!wasPending && originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid())) + rebuildPendingFrameEligibility(); + } + } } else if (auto blockAck = dynamicPtrCast(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; @@ -674,7 +1276,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"); } @@ -683,11 +1285,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) @@ -704,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); @@ -814,8 +1431,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; @@ -828,4 +1448,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..8f2da6d6aaa 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -8,6 +8,11 @@ #ifndef __INET_HCF_H #define __INET_HCF_H +#include +#include +#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" @@ -29,13 +34,13 @@ #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" #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 { @@ -51,6 +56,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; @@ -58,6 +64,13 @@ 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; + bool blockAckInactivityExpiryInProgress = false; // Transmission and Reception IRx *rx = nullptr; @@ -96,9 +109,28 @@ 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; + // 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; + std::set> blockAckTeardownsBeingCancelled; + // Protection mechanisms SingleProtectionMechanism *singleProtectionMechanism = nullptr; @@ -107,26 +139,38 @@ 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); - 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 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); + virtual void untrackPendingFrame(const Packet *packet); + virtual void rebuildPendingFrameEligibility(); virtual bool isReceptionInProgress(); // 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 - 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); @@ -158,13 +202,19 @@ 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 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; 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; @@ -175,4 +225,3 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: } /* namespace inet */ #endif - 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/fragmentation/BasicFragmentationPolicy.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc index e47ce922aab..a2febbfe211 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc @@ -29,23 +29,44 @@ 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)) { + 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; } + 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 +82,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..1639fdd3c8a 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc @@ -17,45 +17,231 @@ 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; - // FIXME temporary fix for mgmt frames - if (dynamicPtrCast(header)) - return packet; - // find entry for this frame - Key key; - key.macAddress = header->getTransmitterAddress(); - 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); - auto& value = fragmentsMap[key]; + 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); + + 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); + } + 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(); + it = fragmentsMap.emplace(key, value).first; + } + auto& value = it->second; value.fragments.resize(16); // 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); @@ -65,35 +251,82 @@ 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; + } } -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); - - if (endSeqNumber < startSeqNumber) { - for (auto it = itStart; it != fragmentsMap.end();) { + simtime_t nextExpirationTime = SIMTIME_MAX; + for (const auto& entry : fragmentsMap) + nextExpirationTime = std::min(nextExpirationTime, entry.second.receptionStartTime + maxReceiveLifetime); + return nextExpirationTime; +} + +std::vector BasicReassembly::removeExpiredFragments(simtime_t currentTime) +{ + std::vector expiredFragments; + for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { + auto& entry = *it; + auto& value = entry.second; + if (currentTime >= value.receptionStartTime + maxReceiveLifetime) { + for (auto fragment : value.fragments) + if (fragment != nullptr) + expiredFragments.push_back(fragment); + 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; +} + +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 : + sequenceNumber >= startSeqNumber || sequenceNumber <= endSeqNumber; + }; + 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; + if (fragment != nullptr) + purgedFragments.push_back(fragment); it = fragmentsMap.erase(it); } - itStart = fragmentsMap.begin(); + else + it++; } - for (auto it = itStart; it != itEnd;) { - for (auto fragment : it->second.fragments) - delete fragment; - it = fragmentsMap.erase(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; } + return purgedFragments; } BasicReassembly::~BasicReassembly() @@ -105,4 +338,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..9d9d572bc08 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h @@ -8,7 +8,12 @@ #ifndef __INET_BASICREASSEMBLY_H #define __INET_BASICREASSEMBLY_H +#include +#include +#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" @@ -18,29 +23,81 @@ 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; - 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))); } + 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(); } }; 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 + int terminalFragmentNumber = -1; + bool hasContradictoryTerminalFragmentNumbers = false; + simtime_t receptionStartTime; }; 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) {} 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; }; } // 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..2f5897ebf0c 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,22 @@ 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); + 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 management body out of INET's combined + // typed header before slicing it into fragment bodies. + 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()))); + } + 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 +75,17 @@ 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 (managementHeader != nullptr) + fragmentHeader = copyManagementHeader(managementHeader); + else + fragmentHeader = staticPtrCast(frameHeader->dupShared()); + if (actionFrame != nullptr) { + auto actionContext = staticPtrCast(actionFrame->dupShared()); + actionContext->setFragmentNumber(i); + actionContext->setMoreFragments(!lastFragment); + fragment->addTag()->setActionFrame(actionContext); + } 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/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/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/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/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/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/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/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/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/mac/recipient/RecipientMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc index 5075d391e89..73df71b3c27 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc @@ -18,13 +18,46 @@ 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(); + basicReassembly = new BasicReassembly(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 +105,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..aaa4a8cb177 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); // 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); @@ -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/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 77e677bb147..ec3abe7b820 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -23,28 +23,97 @@ 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(); + basicReassembly = new BasicReassembly(maxReceiveLifetime); aMsduDeaggregation = new MsduDeaggregation(); aMpduDeaggregation = new MpduDeaggregation(); - blockAckReordering = new BlockAckReordering(); + blockAckReordering = new BlockAckReordering(maxReceiveLifetime); + receiveLifetimeTimer = new cMessage("receiveLifetimeTimer"); +} + +void RecipientQosMacDataService::handleMessage(cMessage *message) +{ + if (message != receiveLifetimeTimer) + throw cRuntimeError("Unknown message"); + expireReceiveLifetime(); + scheduleReceiveLifetimeTimer(); +} + +void RecipientQosMacDataService::expireReceiveLifetime() +{ + for (auto packet : basicReassembly->removeExpiredFragments(simTime())) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + 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::scheduleReceiveLifetimeTimer() +{ + if (receiveLifetimeTimer->isScheduled()) + cancelEvent(receiveLifetimeTimer); + auto nextExpirationTime = std::min(basicReassembly->getNextExpirationTime(), blockAckReordering->getNextExpirationTime()); + if (nextExpirationTime != SIMTIME_MAX) + scheduleAt(nextExpirationTime, receiveLifetimeTimer); +} + +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; + } + } + 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) { + expireReceiveLifetime(); + 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; + scheduleReceiveLifetimeTimer(); + if (defragmentedPacket != nullptr) + emit(packetDefragmentedSignal, defragmentedPacket); + return defragmentedPacket; } Packet *RecipientQosMacDataService::defragment(Packet *mgmtFragment) { + expireReceiveLifetime(); auto packet = basicReassembly->addFragment(mgmtFragment); + scheduleReceiveLifetimeTimer(); if (packet && packet->hasAtFront()) { emit(packetDefragmentedSignal, packet); return packet; @@ -57,13 +126,29 @@ 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 + 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; details.setReason(DUPLICATE_DETECTED); emit(packetDroppedSignal, dataPacket, &details); delete dataPacket; + scheduleReceiveLifetimeTimer(); return std::vector(); } BlockAckReordering::ReorderBuffer frames; @@ -71,9 +156,18 @@ 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) - frames = blockAckReordering->processReceivedQoSFrame(agreement, dataPacket, dataHeader); + if (agreement == nullptr) + agreement = blockAckAgreementHandler->getActiveAgreement(tid, originatorAddr); + 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 @@ -93,6 +187,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data else ; // TODO drop? } } + scheduleReceiveLifetimeTimer(); std::vector deaggregatedFrames; if (aMsduDeaggregation) { for (auto defragmentedFrame : defragmentedFrames) { @@ -112,46 +207,68 @@ 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); + expireReceiveLifetime(); // 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; + // 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()) { + scheduleReceiveLifetimeTimer(); + return { {}, mgmtHeader, true }; + } + else { + scheduleReceiveLifetimeTimer(); + return { {}, nullptr, true }; + } + } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); } - if (auto delba = dynamicPtrCast(mgmtHeader)) - blockAckReordering->processReceivedDelba(delba); + 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 std::vector(); + scheduleReceiveLifetimeTimer(); + return { {}, completeHeader, false }; + } + else { + scheduleReceiveLifetimeTimer(); + return { { mgmtPacket }, completeHeader, false }; } - else - return std::vector({ mgmtPacket }); } 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) { 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 + else { + scheduleReceiveLifetimeTimer(); return std::vector(); + } } std::vector defragmentedFrames; 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 { @@ -180,13 +297,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(receiveLifetimeTimer); delete duplicateRemoval; delete basicReassembly; delete aMsduDeaggregation; @@ -196,4 +316,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..b3c6cdc5d4f 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -27,6 +27,8 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, { protected: IReassembly *basicReassembly = nullptr; + cMessage *receiveLifetimeTimer = nullptr; + simtime_t maxReceiveLifetime = SIMTIME_MAX; IMpduDeaggregation *aMpduDeaggregation = nullptr; // MpduHeaderAndFcsValidation *mpduHeaderAndFcsValidation = nullptr; @@ -41,18 +43,21 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, protected: virtual ~RecipientQosMacDataService(); virtual void initialize() override; + virtual void handleMessage(cMessage *message) override; + virtual void expireReceiveLifetime(); + virtual void scheduleReceiveLifetimeTimer(); virtual Packet *defragment(std::vector completeFragments); virtual Packet *defragment(Packet *mgmtFragment); 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/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned index 307fb523210..49af399d6b6 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); // 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); @@ -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/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/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/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/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/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 696ba2cb33b..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 @@ -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, 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, 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 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/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 843fb1ade0b..27f8d874cac 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++) @@ -186,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 44a499578c2..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: @@ -105,7 +110,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); @@ -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/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. diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test new file mode 100644 index 00000000000..0043bdb1605 --- /dev/null +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -0,0 +1,6748 @@ +%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/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" +#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/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" +#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/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/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" +#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" +#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 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 TestRecipientAckPolicy : public IRecipientAckPolicy, public IRecipientQosAckPolicy +{ + public: + bool blockAckNeeded = false; + + virtual bool isAckNeeded(const Ptr&) const override { return true; } + 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; } +}; + +class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandlerCallback +{ + public: + Packet *managementPacket = nullptr; + 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; + 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()); } + 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(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); + 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 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: + using OriginatorBlockAckAgreementHandler::buildAddbaRequest; + 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; } + 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; } + 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: + 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 TestRecipientHandler : public RecipientBlockAckAgreementHandler +{ + public: + 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: + 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)); + return it == receiveBuffers.end() ? nullptr : it->second; + } +}; + +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 TestImmediateRemovalQueue : public TestPacketQueue +{ + public: + PacketRemovalReason removalReason = PacketRemovalReason::DROPPED; + + virtual void enqueuePacket(Packet *packet) override { + notifyPacketRemoved(packet, removalReason); + delete packet; + } +}; + +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: + 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 TestSignalListener : public cListener +{ + public: + int numSignals = 0; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *) override { numSignals++; } +}; + +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 TestOrderedSignalListener : public cListener +{ + public: + std::vector signals; + + virtual void receiveSignal(cComponent *, simsignal_t signal, cObject *, cObject *) override { signals.push_back(signal); } +}; + +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 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: + 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 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: + virtual ~TestHcf() { + for (auto listener : getLocalSignalListeners(Ieee80211Mac::frameTransmissionOutcomeSignal)) + unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, listener); + } + int numProcessedDroppedSetupFrames = 0; + int numResumedEligibleChannelAccess = 0; + int numCancelledAddbaTransactions = 0; + int numCancelledBlockAckTeardowns = 0; + std::vector cancelledBlockAckTeardownGenerationIds; + int numRebuildEligibilityCalls = 0; + int numTransmittedControlResponses = 0; + bool managementProcessedDuringBlockAckExpiry = false; + 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 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; + 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, 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 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); } + 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); + } + void processInactivityTimeout() { + if (inactivityTimer == nullptr) + inactivityTimer = new cMessage("BlockAckInactivityTimer"); + handleMessage(inactivityTimer); + } + + protected: + virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr&, Packet *, const Ptr&) override { + numTransmittedControlResponses++; + delete responsePacket; + } + virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { + managementProcessedDuringBlockAckExpiry |= blockAckInactivityExpiryInProgress; + 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 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(BlockAckAgreementRole, simtime_t) override {} + 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: + 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 +{ + 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: + virtual std::vector computeFragmentSizes(Packet *) override { return { 8, 5 }; } +}; + +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: + 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; + } +}; + +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; + bool cancellationRequested = 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 {} + virtual void cancelFrameSequence() override { cancellationRequested = true; } +}; + +class TestDcaf : public Dcaf +{ + public: + int numReleasedChannels = 0; + + 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++; } +}; + +class TestDcf : public Dcf +{ + public: + virtual ~TestDcf() { + for (auto listener : getLocalSignalListeners(Ieee80211Mac::frameTransmissionOutcomeSignal)) + unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, listener); + } + 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 { + 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(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(receiverAddress); + header->setTid(tid); + 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()->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; +} + +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(); + response->setTransmitterAddress(transmitterAddress); + response->setTid(tid); + response->setDialogToken(dialogToken); + response->setStatusCode(statusCode); + response->setBufferSize(32); + response->setBlockAckTimeoutValue(0); + 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. +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); + module->par("dropperClass").setStringValue(dropperClass); + module->callInitialize(); + 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); + 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()->setGenerationId(77); + finalDelbaPacket.addTag()->setGenerationId(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.cancelledTeardownGenerationIds.back() == 77); + ASSERT(fragmentedDelbaCallback.excludedTeardownPackets.back() == &finalDelbaPacket); + + fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 78); + firstDelbaPacket.getTagForUpdate()->setGenerationId(78); + ASSERT(fragmentedDelbaHandler.processAbortedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); + ASSERT(!fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); + 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 +// 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 = findFragmentedActionContext(packet); + 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 = findFragmentedActionContext(packet); + 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 completeDelba = makeShared(); + completeDelba->setReceiverAddress(peer1); + completeDelba->setTid(6); + completeDelba->setInitiator(true); + completeDelba->setSequenceNumber(SequenceNumberCyclic(204)); + auto completeDelbaPacket = new Packet("acknowledgedDelba", completeDelba); + completeDelbaPacket->addTag()->setGenerationId(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, findFragmentedActionContext(finalPacket))); + 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.numCancelledBlockAckTeardowns == 1); + 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 +// 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 = findFragmentedActionContext(packet); + 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.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.numCancelledBlockAckTeardowns == 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()->setGenerationId(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; +} + +// 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 +// 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); + transmittedDelba->setInitiator(true); + transmittedDelba->setMoreFragments(true); + auto transmittedFinalDelba = staticPtrCast(transmittedDelba->dupShared()); + transmittedFinalDelba->setMoreFragments(false); + transmittedFinalDelba->setFragmentNumber(1); + Packet transmittedDelbaPacket("transmittedDelba", transmittedDelba); + 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) + 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. +// Transaction-scoped cancellation removes all exact siblings while retaining +// the dropped fragment and an unrelated transaction. +{ + auto fragmentedHandler = new TestOriginatorHandler(); + 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); + auto unfragmentedRequestHeader = unfragmentedRequestPacket->removeAtFront(); + unfragmentedRequestHeader->setSequenceNumber(SequenceNumberCyclic(50)); + unfragmentedRequestPacket->insertAtFront(unfragmentedRequestHeader); + 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); + 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); + auto siblingFragment = fragments->at(1); + TestPacketQueue fragmentedPendingQueue; + 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); + 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()); + 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); +} + +// 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. +{ + 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); + 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); + 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); + ASSERT(duplicateReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateReception.completeHeader, 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 firstAgreementCreationTime = simTime(); + auto firstReception = recipientDataService->managementFrameReceived(new Packet("firstAddbaRequest", firstRequest), firstRequest); + ASSERT(!firstReception.duplicate); + ASSERT(firstReception.completeFrames.empty()); + ASSERT(firstReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(firstReception.completeHeader, 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() == 1); + ASSERT(firstResponse->getAMsduSupported()); + ASSERT(firstAgreement->getBlockAckTimeoutValue() == 1); + ASSERT(firstAgreement->getExpirationTime() == firstAgreementCreationTime + 1); + + 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()); + ASSERT(duplicateReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateReception.completeHeader, 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); + ASSERT(rejectedReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(rejectedReception.completeHeader, 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() == 1); + 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); + ASSERT(duplicateRejectedReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateRejectedReception.completeHeader, 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()); + ASSERT(replacementReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(replacementReception.completeHeader, 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, 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(); + + // 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); + 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; + + // 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); + 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); + 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); + 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; +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 != 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. +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(); +} + +// 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 }); +} + +// 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 +// 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, AckPolicy ackPolicy) { + auto dataHeader = makeQosHeader(peer2, 6, sequenceNumber); + dataHeader->setTransmitterAddress(transmitterAddress); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + 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, BLOCK_ACK); + ASSERT(service->dataFrameReceived(reorderFirstFragment, reorderFirstFragment->peekAtFront(), &agreementHandler).empty()); + 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, NORMAL_ACK); + 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, BLOCK_ACK); + 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. +{ + 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()); + + 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(NORMAL_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); + 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(); +} + +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)); + 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); + + 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()->getGenerationId()); + heldTeardownHcf.cancelTeardown(true, peer1, 7, 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. +{ + 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. +{ + 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(); +} + +// 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(); +} + +// 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. +{ + 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; +} + +// 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. +{ + 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(); +} + +// 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. +{ + 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(); +} + +// 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); +} + +// 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); +} + +// 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; + + // 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 +// 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. +{ + 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 = 2; + TestCallback expiryCallback; + auto request = makeRequest(peer2, 10, 101, SequenceNumberCyclic(310), 32); + request->setBlockAckTimeoutValue(3); + auto firstAgreement = expiryHandler.processReceivedAddbaRequest(request, &expiryPolicy, &expiryCallback, &expiryCallback); + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getGenerationId(); + wait(2); + 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; +} + +// 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 = 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); + 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); + basicBlockAckReq->setTidInfo(12); + wait(1); + auto barReceptionTime = simTime(); + recipientHandler.blockAckReqReceived(basicBlockAckReq, &setupCallback); + ASSERT(agreement->getExpirationTime() == barReceptionTime + 2); + ASSERT(agreement->getExpirationTime() > creationTime + 2); + ASSERT(setupCallback.recipientInactivityDeadline == barReceptionTime + 2); + + wait(4); + 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); + + // 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); + ASSERT(originatorHandler.getActiveAgreement(peer1, 13) == originatorAgreement); + 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(originatorHandler.getActiveAgreement(peer1, 13) == nullptr); + 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); + +} + +// 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. +{ + 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 +// 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); + + // 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); + 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 +ADDBA transaction and negotiated SSN integrity checks passed. 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 { 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