From c647b2fa7b5f0883406df69482efc96fbefe0cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 12:12:19 +0200 Subject: [PATCH 01/56] ieee80211: advertise AP HT discovery state from the MIB Beacon and probe responses need the authoritative primary channel and HT operation advertised by the AP. Read that state from the MIB and include the HT elements in the encoded frame length, so discovery can negotiate the advertised BSS operation. The old AP-local channel cache is redundant. From c5efa92a707aa09c3c51ef57d062e4a28666c192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 8 Sep 2026 13:02:26 +0200 Subject: [PATCH 02/56] ieee80211: refresh associated HT state from accepted beacons Accepted Beacons from the associated AP can change HT width, guard-interval, and MCS constraints while the association remains active. Refresh the authoritative associated-AP snapshot and peer constraints from those Beacons; a Probe Response must not overwrite that state. --- .../ieee80211/mgmt/Ieee80211MgmtSta.cc | 25 +- .../Ieee80211MgmtStaBeaconUpdate_1.test | 504 ++++++++++++++++++ 2 files changed, 528 insertions(+), 1 deletion(-) create mode 100644 tests/module/Ieee80211MgmtStaBeaconUpdate_1.test diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc index 6cb9aeb741c..e397eee436d 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc @@ -1235,7 +1235,30 @@ bool Ieee80211MgmtSta::storeAPInfo(Packet *packet, const PtrisAuthenticated = candidate.isAuthenticated; ap->authSeqExpected = candidate.authSeqExpected; ap->authTimeoutMsg = candidate.authTimeoutMsg; - if (signalPowerInd != nullptr && currentAp) + bool currentAssociatedAp = mib != nullptr && mib->bssStationData.isAssociated && address == assocAP.address; + bool isBeacon = header->getType() == ST_BEACON || + (header->getType() != ST_PROBERESPONSE && dynamicPtrCast(body) == nullptr); + if (currentAssociatedAp && isBeacon) { + (ApInfo&)assocAP = *ap; + mib->bssData.ssid = ap->ssid; + if (mib->isHtOperationSupported() && candidate.htCapabilitiesPresent && candidate.htOperationPresent) { + auto negotiated = negotiateHtCapabilities(mib->localHtCapabilities, candidate.htCapabilities, candidate.htOperation); + if (supportsBasicHtMcsSet(mib->localHtCapabilities, candidate.htOperation) && + negotiated.localTxPeerRx.valid && negotiated.localRxPeerTx.valid) { + EV_INFO << "Refreshing authoritative HT state for associated AP address=" << address << "\n"; + mib->setPeerHtCapabilities(address, candidate.htCapabilities, candidate.htOperation); + } + else { + EV_WARN << "Beacon from associated AP has unusable HT advertisement: removing peer HT state for AP address=" << address << "\n"; + mib->removePeerHtCapabilities(address); + } + } + else { + EV_INFO << "Beacon from associated AP has no usable HT advertisement or STA is legacy: removing peer HT state for AP address=" << address << "\n"; + mib->removePeerHtCapabilities(address); + } + } + else if (signalPowerInd != nullptr && currentAp) assocAP.rxPower = candidate.rxPower; return true; } diff --git a/tests/module/Ieee80211MgmtStaBeaconUpdate_1.test b/tests/module/Ieee80211MgmtStaBeaconUpdate_1.test new file mode 100644 index 00000000000..af38f0eb4a8 --- /dev/null +++ b/tests/module/Ieee80211MgmtStaBeaconUpdate_1.test @@ -0,0 +1,504 @@ +%description: +Verify that an accepted Beacon from the currently associated AP refreshes +the authoritative associated-AP snapshot and MIB peer HT state, that unicast +rate selection dynamically follows changed channel-width, guard-interval, and +MCS constraints, and that legacy or unusable updates preserve the association +while falling back to legacy operational modes. + +%file: TestIeee80211MgmtStaBeaconUpdate.cc + +#include +#include + +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Channel.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +namespace inet { +namespace ieee80211 { + +class TestIeee80211MgmtStaBeaconUpdate : public Ieee80211MgmtSta +{ + public: + int changedChannel = -1; + std::vector changedChannels; + int associationConfirms = 0; + + virtual void changeChannel(int channel) override + { + changedChannel = channel; + changedChannels.push_back(channel); + } + + virtual void sendManagementFrame(const char *name, const Ptr& body, + int subtype, const MacAddress& address) override + { + } + + virtual void sendAssociationConfirm(ApInfo *ap, Ieee80211PrimResultCode resultCode) override + { + associationConfirms++; + } + + void setPrimaryChannelForTest(int channel) + { + Enter_Method("setPrimaryChannelForTest"); + mib->setPrimaryChannel(channel); + } + + void enable40MhzLocalCapabilities() + { + Enter_Method("enable40MhzLocalCapabilities"); + mib->localHtCapabilities.supportedChannelWidths.insert(MHz(40)); + mib->localHtCapabilities.shortGi20 = true; + mib->localHtCapabilities.shortGi40 = true; + } + + const ApInfo *getCachedAp(const MacAddress& address) const + { + return const_cast(this)->lookupAP(address); + } + + bool isAssociatedForTest() const { return mib->bssStationData.isAssociated; } + const AssociatedApInfo& getAssocApForTest() const { return assocAP; } + bool hasPeerHtState(const MacAddress& address) const { return mib->findPeerHtState(address) != nullptr; } + const Ieee80211Mib::PeerHtState *getPeerHtState(const MacAddress& address) const { return mib->findPeerHtState(address); } + bool hasBeaconTimeoutForTest() const { return assocAP.beaconTimeoutMsg != nullptr; } + simtime_t getBeaconTimeoutArrivalForTest() const { return assocAP.beaconTimeoutMsg->getArrivalTime(); } + simtime_t getBeaconIntervalForTest() const { return assocAP.beaconInterval; } + + void prepareResponse(const MacAddress& address, bool reassociation) + { + Enter_Method("prepareResponse"); + auto ap = lookupAP(address); + if (ap == nullptr) { + apList.push_back(ApInfo()); + ap = &apList.back(); + ap->address = address; + ap->channel = mib->getHtOperation().primaryChannel; + ap->ssid = "test-bss"; + ap->beaconInterval = SimTime(100, SIMTIME_MS); + } + ap->isAuthenticated = true; + assocTimeoutMsg = new cMessage("assocTimeout", 2); + assocTimeoutMsg->setContextPointer(ap); + reassociationInProgress = reassociation; + } + + void deliverResponse(const MacAddress& address, bool reassociation, Ieee80211StatusCode statusCode, + int responseOperatingChannelWidth = 40, int responseSecondaryChannelOffset = 1, + bool shortGi20 = true, bool shortGi40 = true) + { + Enter_Method("deliverResponse"); + Ptr body; + if (reassociation) + body = makeShared(); + else + body = makeShared(); + body->setStatusCode(statusCode); + body->setAid(1); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 6; + body->setSupportedRates(rates); + + auto capabilities = mib->localHtCapabilities; + auto operation = mib->getHtOperation(); + operation.operatingChannelWidth = MHz(responseOperatingChannelWidth); + operation.secondaryChannelOffset = responseSecondaryChannelOffset; + capabilities.supportedChannelWidths.insert(MHz(20)); + if (responseOperatingChannelWidth == 40) + capabilities.supportedChannelWidths.insert(MHz(40)); + capabilities.shortGi20 = shortGi20; + capabilities.shortGi40 = shortGi40; + for (int i = 0; i < 8; i++) { + capabilities.rxMcsSupported[i] = true; + operation.basicMcsSupported[i] = true; + } + capabilities.txMcsNss.maxMcsPerNss[0] = 7; + setHtCapabilities(body, capabilities); + const auto *band = &physicallayer::Ieee80211CompliantBands::band2_4GHz; + setHtOperation(body, band, operation); + + body->setChunkLength(B(9) + getHtMgmtElementsLength(body)); + auto packet = new Packet("AssociationResponse"); + packet->insertAtBack(body); + int channelNumber = mib->getHtOperation().primaryChannel; + physicallayer::Ieee80211Channel channel(band, channelNumber); + packet->addTag()->setChannel(&channel); + auto header = makeShared(); + header->setTransmitterAddress(address); + processAssociationResponse(packet, header, reassociation); + } + + void deliverBeaconFrame(const MacAddress& address, + bool htPresent, + int operatingChannelWidth = 20, + int secondaryChannelOffset = 0, + bool shortGi20 = true, + bool shortGi40 = false, + const std::vector& supportedMcs = {0, 1, 2, 3, 4, 5, 6, 7}, + const std::vector& basicMcs = {0, 1, 2, 3, 4, 5, 6, 7}, + simtime_t beaconInterval = SimTime(100, SIMTIME_MS), + bool malformedOperation = false) + { + Enter_Method("deliverBeaconFrame"); + auto body = makeShared(); + body->setSSID("test-bss"); + int primaryChannel = mib->getHtOperation().primaryChannel; + body->setChannelNumber(primaryChannel); + body->setBeaconInterval(beaconInterval); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 6; + body->setSupportedRates(rates); + + if (htPresent) { + Ieee80211HtCapabilities capabilities; + capabilities.supportedChannelWidths.insert(MHz(20)); + if (operatingChannelWidth == 40) + capabilities.supportedChannelWidths.insert(MHz(40)); + capabilities.shortGi20 = shortGi20; + capabilities.shortGi40 = shortGi40; + int maxMcs = -1; + for (int mcs : supportedMcs) { + if (mcs >= 0 && mcs < 77) { + capabilities.rxMcsSupported[mcs] = true; + if (mcs < 8 && mcs > maxMcs) + maxMcs = mcs; + } + } + capabilities.txMcsNss.maxMcsPerNss[0] = maxMcs; + setHtCapabilities(body, capabilities); + + Ieee80211HtOperation operation; + const auto *band = &physicallayer::Ieee80211CompliantBands::band2_4GHz; + operation.primaryChannel = primaryChannel; + operation.operatingChannelWidth = MHz(operatingChannelWidth); + operation.secondaryChannelOffset = secondaryChannelOffset; + for (int mcs : basicMcs) { + if (mcs >= 0 && mcs < 77) + operation.basicMcsSupported[mcs] = true; + } + setHtOperation(body, band, operation); + + if (malformedOperation) { + auto opElement = body->getHtOperation(); + opElement.secondaryChannelOffset = 2; // invalid offset + body->setHtOperation(opElement); + } + } + body->setChunkLength(B(8 + 2 + 2 + (2 + 10)) + B(3) + getHtMgmtElementsLength(body)); + auto packet = new Packet("Beacon"); + packet->insertAtBack(body); + packet->addTag()->setPower(mW(1)); + const auto *band = &physicallayer::Ieee80211CompliantBands::band2_4GHz; + physicallayer::Ieee80211Channel channel(band, primaryChannel); + packet->addTag()->setChannel(&channel); + auto header = makeShared(); + header->setType(ST_BEACON); + header->setTransmitterAddress(address); + handleBeaconFrame(packet, header); + } + + void deliverProbeResponseFrame(const MacAddress& address, + bool htPresent, + int operatingChannelWidth = 20, + int secondaryChannelOffset = 0, + bool shortGi20 = true, + bool shortGi40 = false) + { + Enter_Method("deliverProbeResponseFrame"); + auto body = makeShared(); + body->setSSID("test-bss"); + int primaryChannel = mib->getHtOperation().primaryChannel; + body->setChannelNumber(primaryChannel); + body->setBeaconInterval(SimTime(100, SIMTIME_MS)); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 6; + body->setSupportedRates(rates); + + if (htPresent) { + Ieee80211HtCapabilities capabilities; + capabilities.supportedChannelWidths.insert(MHz(20)); + if (operatingChannelWidth == 40) + capabilities.supportedChannelWidths.insert(MHz(40)); + capabilities.shortGi20 = shortGi20; + capabilities.shortGi40 = shortGi40; + for (int i = 0; i < 8; i++) + capabilities.rxMcsSupported[i] = true; + capabilities.txMcsNss.maxMcsPerNss[0] = 7; + setHtCapabilities(body, capabilities); + + Ieee80211HtOperation operation; + const auto *band = &physicallayer::Ieee80211CompliantBands::band2_4GHz; + operation.primaryChannel = primaryChannel; + operation.operatingChannelWidth = MHz(operatingChannelWidth); + operation.secondaryChannelOffset = secondaryChannelOffset; + for (int i = 0; i < 8; i++) + operation.basicMcsSupported[i] = true; + setHtOperation(body, band, operation); + } + body->setChunkLength(B(8 + 2 + 2 + (2 + 10)) + B(3) + getHtMgmtElementsLength(body)); + auto packet = new Packet("ProbeResponse"); + packet->insertAtBack(body); + packet->addTag()->setPower(mW(1)); + const auto *band = &physicallayer::Ieee80211CompliantBands::band2_4GHz; + physicallayer::Ieee80211Channel channel(band, primaryChannel); + packet->addTag()->setChannel(&channel); + auto header = makeShared(); + header->setType(ST_PROBERESPONSE); + header->setTransmitterAddress(address); + handleProbeResponseFrame(packet, header); + } +}; + +Define_Module(TestIeee80211MgmtStaBeaconUpdate); + +class Ieee80211MgmtStaBeaconUpdateTest : public cSimpleModule +{ + public: + Ieee80211MgmtStaBeaconUpdateTest() : cSimpleModule(65536) {} + + protected: + virtual void activity() override + { + auto mgmt = check_and_cast(getModuleByPath("^.sta.wlan[0].mgmt")); + auto dcfRateSelection = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.dcf.rateSelection")); + auto qosRateSelection = check_and_cast(getModuleByPath("^.sta.wlan[0].mac.hcf.rateSelection")); + const auto *modeSet = physicallayer::Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + + const MacAddress apAddress("02:00:00:00:00:01"); + auto dataHeader = makeShared(); + dataHeader->setReceiverAddress(apAddress); + Packet ratePacket("rate-selection"); + + auto checkUnicastMode = [&](Hz expectedBandwidth, bool expectedSgi, int expectedMcs) { + const auto *dcfMode = dcfRateSelection->computeMode(&ratePacket, dataHeader); + const auto *qosMode = qosRateSelection->computeMode(&ratePacket, dataHeader, nullptr); + ASSERT(dcfMode == qosMode); + if (expectedMcs >= 0) { + ASSERT(dcfMode->getHtMcsIndex() == expectedMcs); + ASSERT(dcfMode->getDataMode()->getBandwidth() == expectedBandwidth); + ASSERT(dcfMode->isHtShortGuardInterval() == expectedSgi); + } + else { + ASSERT(dcfMode->getHtMcsIndex() < 0); + ASSERT(dcfMode == modeSet->getFastestLegacyOperationalMode()); + } + }; + + mgmt->setPrimaryChannelForTest(6); + mgmt->enable40MhzLocalCapabilities(); + + // 1. Initial Beacon discovery (40 MHz, Short GI 20 & 40, MCS 0..7) + mgmt->deliverBeaconFrame(apAddress, true, 40, 1, true, true); + ASSERT(mgmt->getCachedAp(apAddress) != nullptr); + ASSERT(mgmt->getCachedAp(apAddress)->htOperationPresent); + ASSERT(mgmt->getCachedAp(apAddress)->htOperation.operatingChannelWidth == MHz(40)); + + // 2. Initial Association (40 MHz, Short GI 20 & 40) + mgmt->prepareResponse(apAddress, false); + mgmt->deliverResponse(apAddress, false, SC_SUCCESSFUL, 40, 1, true, true); + ASSERT(mgmt->isAssociatedForTest()); + ASSERT(mgmt->hasPeerHtState(apAddress)); + ASSERT(mgmt->getAssocApForTest().htOperation.operatingChannelWidth == MHz(40)); + ASSERT(mgmt->getAssocApForTest().htCapabilities.shortGi20); + ASSERT(mgmt->getAssocApForTest().htCapabilities.shortGi40); + ASSERT(mgmt->hasBeaconTimeoutForTest()); + + // Verify rate selection selects 40 MHz Short GI MCS 7 (150 Mbps) + checkUnicastMode(MHz(40), true, 7); + + // 3. Subsequent Beacon from associated AP switches HT Operation to 20 MHz + wait(SimTime(1, SIMTIME_US)); + mgmt->deliverBeaconFrame(apAddress, true, 20, 0, true, true); + ASSERT(mgmt->isAssociatedForTest()); + ASSERT(mgmt->hasPeerHtState(apAddress)); + ASSERT(mgmt->getAssocApForTest().htOperation.operatingChannelWidth == MHz(20)); + ASSERT(mgmt->getAssocApForTest().htOperation.secondaryChannelOffset == 0); + ASSERT(mgmt->getPeerHtState(apAddress)->negotiatedCapabilities.operation.operatingChannelWidth == MHz(20)); + // Rate selection must demote to 20 MHz Short GI MCS 7; cannot retain obsolete 40 MHz constraint + checkUnicastMode(MHz(20), true, 7); + + // 4. Subsequent Beacon disables Short Guard Interval (20 MHz, Long GI) + wait(SimTime(1, SIMTIME_US)); + mgmt->deliverBeaconFrame(apAddress, true, 20, 0, false, false); + ASSERT(mgmt->isAssociatedForTest()); + ASSERT(mgmt->hasPeerHtState(apAddress)); + ASSERT(!mgmt->getAssocApForTest().htCapabilities.shortGi20); + ASSERT(!mgmt->getPeerHtState(apAddress)->negotiatedCapabilities.localTxPeerRx.receiverShortGi20); + // Rate selection must demote to 20 MHz Long GI MCS 7; cannot retain obsolete Short GI constraint + checkUnicastMode(MHz(20), false, 7); + + // 5. Subsequent Beacon restricts supported MCS to MCS 0 only + wait(SimTime(1, SIMTIME_US)); + mgmt->deliverBeaconFrame(apAddress, true, 20, 0, false, false, {0}, {0}); + ASSERT(mgmt->isAssociatedForTest()); + ASSERT(mgmt->hasPeerHtState(apAddress)); + ASSERT(mgmt->getAssocApForTest().htCapabilities.rxMcsSupported[0]); + ASSERT(!mgmt->getAssocApForTest().htCapabilities.rxMcsSupported[7]); + ASSERT(mgmt->getPeerHtState(apAddress)->negotiatedCapabilities.localTxPeerRx.supportedMcs[0]); + ASSERT(!mgmt->getPeerHtState(apAddress)->negotiatedCapabilities.localTxPeerRx.supportedMcs[7]); + // Rate selection must demote to MCS 0; cannot retain obsolete MCS 7 constraint + checkUnicastMode(MHz(20), false, 0); + + // 6. Subsequent Beacon switches to Legacy advertisement (no HT elements) + wait(SimTime(1, SIMTIME_US)); + mgmt->deliverBeaconFrame(apAddress, false); + // Association is PRESERVED, beacon timeout timer still active + ASSERT(mgmt->isAssociatedForTest()); + ASSERT(mgmt->hasBeaconTimeoutForTest()); + ASSERT(!mgmt->getAssocApForTest().htCapabilitiesPresent); + ASSERT(!mgmt->getAssocApForTest().htOperationPresent); + // MIB peer HT state is REMOVED + ASSERT(!mgmt->hasPeerHtState(apAddress)); + // Rate selection falls back to fastest legacy operational mode + checkUnicastMode(MHz(20), false, -1); + + // 7. Subsequent Beacon advertises unusable HT (unsupported Basic MCS 32 only) + wait(SimTime(1, SIMTIME_US)); + mgmt->deliverBeaconFrame(apAddress, true, 20, 0, true, false, {0, 1, 2, 3, 4, 5, 6, 7}, {32}); + // Association is PRESERVED + ASSERT(mgmt->isAssociatedForTest()); + ASSERT(mgmt->hasBeaconTimeoutForTest()); + ASSERT(mgmt->getAssocApForTest().htOperationPresent); + ASSERT(mgmt->getAssocApForTest().htOperation.basicMcsSupported[32]); + // MIB peer HT state remains absent because Basic MCS is unsupported + ASSERT(!mgmt->hasPeerHtState(apAddress)); + // Rate selection remains on legacy mode + checkUnicastMode(MHz(20), false, -1); + + // 8. Subsequent Beacon restores full 40 MHz Short GI HT advertisement + updates beacon interval + wait(SimTime(1, SIMTIME_US)); + simtime_t newBeaconInterval = SimTime(200, SIMTIME_MS); + mgmt->deliverBeaconFrame(apAddress, true, 40, 1, true, true, + {0, 1, 2, 3, 4, 5, 6, 7}, {0, 1, 2, 3, 4, 5, 6, 7}, newBeaconInterval); + ASSERT(mgmt->isAssociatedForTest()); + ASSERT(mgmt->hasPeerHtState(apAddress)); + ASSERT(mgmt->getAssocApForTest().htOperation.operatingChannelWidth == MHz(40)); + ASSERT(mgmt->getAssocApForTest().htCapabilities.shortGi20); + ASSERT(mgmt->getAssocApForTest().htCapabilities.shortGi40); + ASSERT(mgmt->getAssocApForTest().beaconInterval == newBeaconInterval); + // Beacon timeout timer rescheduled with new beacon interval + ASSERT(mgmt->getBeaconTimeoutArrivalForTest() == simTime() + 3.5 * newBeaconInterval); + // Rate selection restores 40 MHz Short GI MCS 7 (150 Mbps) + checkUnicastMode(MHz(40), true, 7); + + // 9. Subsequent malformed Beacon (invalid secondaryChannelOffset = 2) is rejected + wait(SimTime(1, SIMTIME_US)); + simtime_t deadlineBeforeMalformed = mgmt->getBeaconTimeoutArrivalForTest(); + mgmt->deliverBeaconFrame(apAddress, true, 40, 1, true, true, + {0, 1, 2, 3, 4, 5, 6, 7}, {0, 1, 2, 3, 4, 5, 6, 7}, newBeaconInterval, true); + // Beacon deadline must NOT be extended for rejected beacon + ASSERT(mgmt->getBeaconTimeoutArrivalForTest() == deadlineBeforeMalformed); + // Authoritative state must NOT be corrupted + ASSERT(mgmt->getAssocApForTest().htOperation.operatingChannelWidth == MHz(40)); + ASSERT(mgmt->hasPeerHtState(apAddress)); + checkUnicastMode(MHz(40), true, 7); + + // 10. Probe Response from associated AP does NOT update authoritative HT snapshot or MIB state + wait(SimTime(1, SIMTIME_US)); + mgmt->deliverProbeResponseFrame(apAddress, true, 20, 0, false, false); + // Cached AP list has the probe response, but assocAP and MIB peer HT state retain 40 MHz SGI + ASSERT(mgmt->getCachedAp(apAddress)->htOperation.operatingChannelWidth == MHz(20)); + ASSERT(mgmt->getAssocApForTest().htOperation.operatingChannelWidth == MHz(40)); + ASSERT(mgmt->getPeerHtState(apAddress)->negotiatedCapabilities.operation.operatingChannelWidth == MHz(40)); + checkUnicastMode(MHz(40), true, 7); + + std::cout << "Subsequent Beacon updates authoritative associated-AP snapshot and MIB peer HT state.\n"; + std::cout << "Unicast rate selection follows dynamic channel-width, guard-interval, and MCS updates.\n"; + std::cout << "Legacy and unusable HT updates preserve association while falling back to legacy modes.\n"; + } +}; + +Define_Module(Ieee80211MgmtStaBeaconUpdateTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned + +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtSta; +import inet.node.inet.WirelessHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple TestIeee80211MgmtStaBeaconUpdate extends Ieee80211MgmtSta +{ + parameters: + @class(::inet::ieee80211::TestIeee80211MgmtStaBeaconUpdate); +} + +simple Ieee80211MgmtStaBeaconUpdateTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211MgmtStaBeaconUpdateTest); +} + +network Ieee80211MgmtStaBeaconUpdateTestNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: WirelessHost { + parameters: + wlan[*].mgmt.typename = "TestIeee80211MgmtStaBeaconUpdate"; + wlan[*].agent.typename = "Ieee80211AgentSta"; + } + test: Ieee80211MgmtStaBeaconUpdateTest; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211MgmtStaBeaconUpdateTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 10ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +*.sta.wlan[0].address = "02:00:00:00:00:09" +*.sta.wlan[0].agent.startingTime = 100s +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 150Mbps +*.sta.wlan[0].mac.*.rateSelection.dataFrameBitrate = 150Mbps +*.sta.wlan[0].mac.*.rateSelection.dataFrameBandwidth = 40MHz +*.sta.wlan[0].mac.*.rateSelection.dataFrameNumSpatialStreams = 1 +*.sta.wlan[0].mac.qosStation = true +**.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 +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +Subsequent Beacon updates authoritative associated-AP snapshot and MIB peer HT state. + +%contains: stdout +Unicast rate selection follows dynamic channel-width, guard-interval, and MCS updates. + +%contains: stdout +Legacy and unusable HT updates preserve association while falling back to legacy modes. From 843995a4d0569c281b93f02ad9f12b8834c32405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 8 Sep 2026 13:02:52 +0200 Subject: [PATCH 03/56] ieee80211: reconcile HT operation across runtime channel changes A runtime band or channel change can invalidate an HT40 pair and leave negotiated peer state on the old operation. Revalidate the operation, fall back or restore width as appropriate, and refresh peer constraints even when a band change retains the same internal channel index. When a pending association response completes, reconcile committed peer state with the current BSS operation while preserving the operation element already advertised in that response. --- .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 9 +- .../ieee80211/mgmt/Ieee80211MgmtApBase.cc | 6 +- .../linklayer/ieee80211/mib/Ieee80211Mib.cc | 65 ++- .../linklayer/ieee80211/mib/Ieee80211Mib.h | 4 + .../ieee80211/packetlevel/Ieee80211Radio.cc | 3 + tests/module/Ieee80211HtAssociation_1.test | 2 +- .../Ieee80211MgmtApChannelChange_1.test | 461 ++++++++++++++++++ ...eee80211MgmtApReassociationSnapshot_1.test | 190 ++++++++ 8 files changed, 730 insertions(+), 10 deletions(-) create mode 100644 tests/module/Ieee80211MgmtApChannelChange_1.test create mode 100644 tests/module/Ieee80211MgmtApReassociationSnapshot_1.test diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index b940d99b0f9..baad0d6e357 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -135,9 +135,12 @@ void Ieee80211MgmtAp::frameTransmissionFinished(const Packet *responseFrame, Fra mib->bssAccessPointData.stations[address] = Ieee80211Mib::ASSOCIATED; if (sta->second.pendingHtStateAvailable) { // IEEE Std 802.11-2024, 11.3.5.3: association state becomes effective only after the successful response exchange. - if (sta->second.pendingHtCapabilitiesValid) { - ASSERT(sta->second.pendingHtOperationValid); - mib->setPeerHtCapabilities(address, sta->second.pendingHtCapabilities, sta->second.pendingHtOperation); + if (sta->second.pendingHtCapabilitiesValid && mib->isHtOperationSupported()) { + const auto& currentOperation = mib->getHtOperation(); + if (supportsBasicHtMcsSet(sta->second.pendingHtCapabilities, currentOperation)) + mib->setPeerHtCapabilities(address, sta->second.pendingHtCapabilities, currentOperation); + else + mib->removePeerHtCapabilities(address); } else mib->removePeerHtCapabilities(address); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc index eb4e5f54d9f..329f439ccc0 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.cc @@ -43,6 +43,7 @@ void Ieee80211MgmtApBase::initialize(int stage) else if (stage == INITSTAGE_LINK_LAYER) mib->bssData.bssid = mib->address; else if (stage == INITSTAGE_LAST && mib->isHtOperationSupported()) { + mib->setPrimaryChannel(mib->requirePrimaryChannel(), getHtOperationBand()); const auto& operation = mib->getHtOperation(); if (operation.operatingChannelWidth == MHz(40) && !getHtOperationBand()->isHt40OperationSupported(operation.primaryChannel, operation.secondaryChannelOffset)) @@ -57,7 +58,10 @@ void Ieee80211MgmtApBase::receiveSignal(cComponent *source, simsignal_t signalID if (source == radio && signalID == ieee80211RadioChannelChangedSignal) { EV << "Updating AP primary channel to " << value << ".\n"; - mib->setPrimaryChannel(value); + if (mib->isHtOperationSupported()) + mib->setPrimaryChannel(value, getHtOperationBand()); + else + mib->setPrimaryChannel(value); } } diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index 27933f04a7a..e5c8fd008a3 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -9,6 +9,7 @@ #include +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" namespace inet { @@ -20,10 +21,12 @@ Define_Module(Ieee80211Mib); void Ieee80211Mib::initialize(int stage) { if (stage == INITSTAGE_LOCAL) { + configuredSecondaryChannelOffset = par("htSecondaryChannelOffset"); WATCH(address); WATCH(mode); WATCH(qos); WATCH(localHtCapabilitiesValid); + WATCH(configuredSecondaryChannelOffset); WATCH(primaryChannelAvailable); WATCH(bssData.bssid); WATCH(bssStationData.stationType); @@ -49,11 +52,58 @@ int Ieee80211Mib::requirePrimaryChannel() const } void Ieee80211Mib::setPrimaryChannel(int primaryChannel) +{ + setPrimaryChannel(primaryChannel, nullptr); +} + +void Ieee80211Mib::setPrimaryChannel(int primaryChannel, const physicallayer::IIeee80211Band *band) { if (primaryChannel < 0 || primaryChannel > 255) throw cRuntimeError("IEEE 802.11 primary channel must be in the range 0..255, not %d", primaryChannel); + + if (band != nullptr) { + try { + band->getStandardChannelNumber(primaryChannel); + } + catch (const cRuntimeError&) { + throw cRuntimeError("Invalid primary channel %d for band '%s'", primaryChannel, band->getName()); + } + + if (localHtCapabilitiesValid) { + if (configuredSecondaryChannelOffset != 0) { + if (band->isHt40OperationSupported(primaryChannel, configuredSecondaryChannelOffset)) { + htOperation.secondaryChannelOffset = configuredSecondaryChannelOffset; + htOperation.operatingChannelWidth = MHz(40); + } + else { + // IEEE Std 802.11-2024, 11.15.2 and 11.15.3.1: fallback to 20 MHz BSS operation + EV_WARN << "Configured 40 MHz HT operation (offset " << configuredSecondaryChannelOffset + << ") is unsupported on primary channel " << primaryChannel + << " in band '" << band->getName() << "'; falling back to 20 MHz BSS operation.\n"; + htOperation.secondaryChannelOffset = 0; + htOperation.operatingChannelWidth = MHz(20); + } + } + else { + htOperation.secondaryChannelOffset = 0; + htOperation.operatingChannelWidth = MHz(20); + } + } + } + htOperation.primaryChannel = primaryChannel; primaryChannelAvailable = true; + + if (localHtCapabilitiesValid) { + for (auto& entry : peerHtStates) { + if (entry.second.valid) { + entry.second.negotiatedCapabilities = negotiateHtCapabilities(localHtCapabilities, + entry.second.advertisedCapabilities, htOperation); + if (++entry.second.generation == 0) + entry.second.generation = 1; + } + } + } } const Ieee80211HtOperation& Ieee80211Mib::getHtOperation() const @@ -120,9 +170,10 @@ void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeS if (localHtCapabilities.maxAmpduLengthExponent < 0 || localHtCapabilities.maxAmpduLengthExponent > 3) throw cRuntimeError("htMaxAmpduLengthExponent must be between 0 and 3"); - htOperation.secondaryChannelOffset = par("htSecondaryChannelOffset"); - if (htOperation.secondaryChannelOffset != 0 && htOperation.secondaryChannelOffset != 1 && htOperation.secondaryChannelOffset != 3) + configuredSecondaryChannelOffset = par("htSecondaryChannelOffset"); + if (configuredSecondaryChannelOffset != 0 && configuredSecondaryChannelOffset != 1 && configuredSecondaryChannelOffset != 3) throw cRuntimeError("htSecondaryChannelOffset must be 0, 1, or 3"); + htOperation.secondaryChannelOffset = configuredSecondaryChannelOffset; bool use40Mhz = htOperation.secondaryChannelOffset != 0; if (use40Mhz && localHtCapabilities.supportedChannelWidths.count(MHz(40)) == 0) throw cRuntimeError("40 MHz HT operation requires a configured PHY that can operate a 40 MHz channel width"); @@ -131,10 +182,14 @@ void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeS if (protectionMode < 0 || protectionMode > 3) throw cRuntimeError("htProtectionMode must be between 0 and 3"); htOperation.protectionMode = static_cast(protectionMode); - for (auto& entry : peerHtStates) - if (entry.second.valid) + for (auto& entry : peerHtStates) { + if (entry.second.valid) { entry.second.negotiatedCapabilities = negotiateHtCapabilities(localHtCapabilities, - entry.second.advertisedCapabilities, entry.second.negotiatedCapabilities.operation); + entry.second.advertisedCapabilities, htOperation); + if (++entry.second.generation == 0) + entry.second.generation = 1; + } + } } const Ieee80211Mib::PeerHtState *Ieee80211Mib::findPeerHtState(const MacAddress& address) const diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h index c28ec872393..974bdd41158 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h @@ -16,6 +16,7 @@ namespace inet { namespace physicallayer { class Ieee80211ModeSet; +class IIeee80211Band; } namespace ieee80211 { @@ -81,6 +82,7 @@ class INET_API Ieee80211Mib : public SimpleModule private: Ieee80211HtOperation htOperation; + int configuredSecondaryChannelOffset = 0; bool primaryChannelAvailable = false; std::map associationIdReservations; std::map peerHtStates; @@ -103,7 +105,9 @@ class INET_API Ieee80211Mib : public SimpleModule bool isHtOperationSupported() const { return localHtCapabilitiesValid; } bool hasPrimaryChannel() const { return primaryChannelAvailable; } int requirePrimaryChannel() const; + int getConfiguredSecondaryChannelOffset() const { return configuredSecondaryChannelOffset; } void setPrimaryChannel(int primaryChannel); + void setPrimaryChannel(int primaryChannel, const physicallayer::IIeee80211Band *band); const Ieee80211HtOperation& getHtOperation() const; const PeerHtState *findPeerHtState(const MacAddress& address) const; void setPeerHtCapabilities(const MacAddress& address, const Ieee80211HtCapabilities& capabilities, const Ieee80211HtOperation& operation); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index ea2b61c06c4..56114b6c2c6 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -108,6 +108,9 @@ void Ieee80211Radio::setBand(const IIeee80211Band *band) ieee80211Receiver->setBand(band); EV << "Changing radio band to " << band << endl; receptionTimer = nullptr; + const auto *channel = ieee80211Transmitter->getChannel(); + if (channel != nullptr) + emit(radioChannelChangedSignal, channel->getChannelNumber()); emit(listeningChangedSignal, 0); } diff --git a/tests/module/Ieee80211HtAssociation_1.test b/tests/module/Ieee80211HtAssociation_1.test index 996683c30e2..aeee34722e2 100644 --- a/tests/module/Ieee80211HtAssociation_1.test +++ b/tests/module/Ieee80211HtAssociation_1.test @@ -128,7 +128,7 @@ class Ieee80211HtAssociationChecker : public SimpleModule, public cListener ASSERT(staTunedToInternalChannelSix); ASSERT(apMib->hasPrimaryChannel()); ASSERT(apMib->requirePrimaryChannel() == 11); - ASSERT(apMib->findPeerHtState(staAddress)->negotiatedCapabilities.operation.primaryChannel == 6); + ASSERT(apMib->findPeerHtState(staAddress)->negotiatedCapabilities.operation.primaryChannel == 11); ASSERT(staMib->bssStationData.isAssociated); // IEEE Std 802.11-2024, 9.4.2.54.2/Figure 9-456 and Table 9-224: the n(mixed-2.4Ghz) // profile exposes short GI for the PHY-supported 20 MHz width. The diff --git a/tests/module/Ieee80211MgmtApChannelChange_1.test b/tests/module/Ieee80211MgmtApChannelChange_1.test new file mode 100644 index 00000000000..5067b64dce5 --- /dev/null +++ b/tests/module/Ieee80211MgmtApChannelChange_1.test @@ -0,0 +1,461 @@ +%description: +Verify that runtime radioChannelChanged notifications revalidate channel and +band changes against the active IIeee80211Band, dynamically fall back to 20 MHz +BSS operation when a configured secondary channel offset is unsupported, restore +40 MHz operation when returning to a capable channel, reject invalid channels +with cRuntimeError before committing, defer HT-only validation until local HT +operation is known, and keep advertised HT Operation and existing negotiated +peer state consistent throughout. + +%file: TestIeee80211MgmtApChannelChange.cc + +#include + +#include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" + +namespace inet { +namespace ieee80211 { + +class TestHt40Transmitter : public physicallayer::Ieee80211Transmitter +{ + public: + virtual bool isHtChannelWidthSupported(Hz channelWidth) const override + { + return (channelWidth == MHz(20) || channelWidth == MHz(40)) && + modeSet != nullptr && modeSet->getHtSupportedChannelWidths().count(channelWidth) != 0; + } +}; + +Define_Module(TestHt40Transmitter); + +class TestHt40Receiver : public physicallayer::Ieee80211Receiver +{ + public: + virtual bool isHtChannelWidthSupported(Hz channelWidth) const override + { + return (channelWidth == MHz(20) || channelWidth == MHz(40)) && + modeSet != nullptr && modeSet->getHtSupportedChannelWidths().count(channelWidth) != 0; + } +}; + +Define_Module(TestHt40Receiver); + +class TestIeee80211MgmtAp : public Ieee80211MgmtAp +{ + public: + std::vector> sentBeacons; + + using Ieee80211MgmtApBase::getHtOperationBand; + + protected: + virtual void start() override + { + // Suppress automatic periodic beacons so test controls frame generation. + Ieee80211MgmtApBase::start(); + } + + virtual void sendManagementFrame(const char *name, const Ptr& body, + int subtype, const MacAddress& destAddr, uint64_t transactionId = 0) override + { + if (subtype == ST_BEACON) { + auto beacon = dynamicPtrCast(body); + if (beacon != nullptr) + sentBeacons.push_back(constPtrCast(beacon)); + } + } + + public: + void emitBeaconNow() + { + Enter_Method("emitBeaconNow"); + sendBeacon(); + } + + void submitAuthenticationRequest(const MacAddress& address) + { + Enter_Method("submitAuthenticationRequest"); + auto packet = new Packet("AuthenticationRequest"); + auto body = makeShared(); + body->setSequenceNumber(1); + body->setStatusCode(SC_SUCCESSFUL); + body->setChunkLength(B(6)); + packet->insertAtBack(body); + auto header = makeShared(); + header->setType(ST_AUTHENTICATION); + header->setTransmitterAddress(address); + header->setReceiverAddress(mib->address); + handleAuthenticationFrame(packet, header); + } + + void submitAssociationRequest(const MacAddress& address) + { + Enter_Method("submitAssociationRequest"); + auto packet = new Packet("AssociationRequest"); + auto body = makeShared(); + body->setSSID(ssid.c_str()); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 6; + body->setSupportedRates(rates); + if (mib->isHtOperationSupported()) { + body->setHtCapabilitiesPresent(true); + auto staCaps = mib->localHtCapabilities; + staCaps.supportedChannelWidths.insert(MHz(20)); + staCaps.supportedChannelWidths.insert(MHz(40)); + body->setHtCapabilities(makeHtCapabilitiesElement(staCaps)); + } + body->setChunkLength(B(100)); + packet->insertAtBack(body); + auto header = makeShared(); + header->setType(ST_ASSOCIATIONREQUEST); + header->setTransmitterAddress(address); + header->setReceiverAddress(mib->address); + handleAssociationRequestFrame(packet, header); + } + + uint64_t getPendingTransactionId(const MacAddress& address) const + { + auto it = staList.find(address); + return it == staList.end() ? 0 : it->second.pendingAssociationTransactionId; + } + + void finishAssociationResponse(const MacAddress& address) + { + Enter_Method("finishAssociationResponse"); + auto response = new Packet("AssociationResponse"); + auto header = makeShared(); + header->setType(ST_ASSOCIATIONRESPONSE); + header->setReceiverAddress(address); + response->insertAtBack(header); + response->addTag()->setTransactionId(getPendingTransactionId(address)); + frameTransmissionFinished(response, FRAME_TRANSMISSION_STATUS_ACKNOWLEDGED); + delete response; + } +}; + +Define_Module(TestIeee80211MgmtAp); + +static const physicallayer::IIeee80211Mode *findHtMode(const physicallayer::Ieee80211ModeSet *modeSet, int mcsIndex, Hz bandwidth) +{ + for (int i = 0; i < modeSet->getNumModes(); i++) { + const auto *mode = modeSet->getMode(i); + if (mode->getHtMcsIndex() == mcsIndex && mode->getDataMode()->getBandwidth() == bandwidth && + !mode->isHtShortGuardInterval()) + return mode; + } + return nullptr; +} + +class Ieee80211MgmtApChannelChangeTest : public cSimpleModule +{ + public: + Ieee80211MgmtApChannelChangeTest() : cSimpleModule(65536) {} + + protected: + virtual void activity() override + { + auto mgmt = check_and_cast(getModuleByPath("^.ap.wlan[0].mgmt")); + auto mib = check_and_cast(getModuleByPath("^.ap.wlan[0].mib")); + auto radio = check_and_cast(getModuleByPath("^.ap.wlan[0].radio")); + const auto *modeSet = physicallayer::Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + const MacAddress staAddress("02:00:00:00:00:09"); + + // 1. Initial State: AP initialized on channel 0 (std channel 1) with HT40 SCA (+1) + ASSERT(mib->hasPrimaryChannel()); + ASSERT(mib->requirePrimaryChannel() == 0); + ASSERT(mib->getHtOperation().primaryChannel == 0); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 1); + ASSERT(mib->getHtOperation().operatingChannelWidth == MHz(40)); + ASSERT(mib->getConfiguredSecondaryChannelOffset() == 1); + std::cout << "AP initialized with 40 MHz HT operation on channel 0.\n"; + + // 2. Station authenticates and associates + mgmt->submitAuthenticationRequest(staAddress); + mgmt->submitAssociationRequest(staAddress); + mgmt->finishAssociationResponse(staAddress); + + const auto *peerState = mib->findPeerHtState(staAddress); + ASSERT(peerState != nullptr); + ASSERT(peerState->valid); + ASSERT(peerState->negotiatedCapabilities.operation.primaryChannel == 0); + ASSERT(peerState->negotiatedCapabilities.operation.secondaryChannelOffset == 1); + ASSERT(peerState->negotiatedCapabilities.operation.operatingChannelWidth == MHz(40)); + + // Verify beacon advertised HT Operation matches 40 MHz SCA operation + mgmt->emitBeaconNow(); + ASSERT(!mgmt->sentBeacons.empty()); + auto beaconElem = mgmt->sentBeacons.back()->getHtOperation(); + ASSERT(mgmt->sentBeacons.back()->getHtOperationPresent()); + ASSERT(beaconElem.primaryChannel == 1); // standard channel 1 on wire + ASSERT(beaconElem.secondaryChannelOffset == 1); // SCA + ASSERT(beaconElem.staChannelWidth40Mhz); + + // Verify rate selection selects 40 MHz mode for peer + const auto *mcs7_40 = findHtMode(modeSet, 7, MHz(40)); + ASSERT(mcs7_40 != nullptr); + const auto *selectedMode = selectPeerCompatibleMode(modeSet, peerState, mcs7_40, staAddress); + ASSERT(selectedMode == mcs7_40); + std::cout << "Station associated and negotiated 40 MHz HT operation.\n"; + + // 3. Dynamic runtime channel change to channel index 10 (std channel 11) + // In 2.4 GHz band, channel 11 + offset 1 would require channel 15, which is outside the band. + radio->setChannelNumber(10); + + // Verify MIB downgraded to 20 MHz BSS operation while keeping configured offset policy + ASSERT(mib->requirePrimaryChannel() == 10); + ASSERT(mib->getHtOperation().primaryChannel == 10); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 0); + ASSERT(mib->getHtOperation().operatingChannelWidth == MHz(20)); + ASSERT(mib->getConfiguredSecondaryChannelOffset() == 1); + + // Verify existing negotiated peer state is consistently updated + peerState = mib->findPeerHtState(staAddress); + ASSERT(peerState != nullptr); + ASSERT(peerState->valid); + ASSERT(peerState->negotiatedCapabilities.operation.primaryChannel == 10); + ASSERT(peerState->negotiatedCapabilities.operation.secondaryChannelOffset == 0); + ASSERT(peerState->negotiatedCapabilities.operation.operatingChannelWidth == MHz(20)); + + // Verify beacon advertised HT Operation reflects 20 MHz BSS operation + mgmt->emitBeaconNow(); + beaconElem = mgmt->sentBeacons.back()->getHtOperation(); + ASSERT(beaconElem.primaryChannel == 11); // standard channel 11 on wire + ASSERT(beaconElem.secondaryChannelOffset == 0); // SCN + ASSERT(!beaconElem.staChannelWidth40Mhz); + + // Verify rate selection for existing peer restricts to 20 MHz + selectedMode = selectPeerCompatibleMode(modeSet, peerState, mcs7_40, staAddress); + ASSERT(selectedMode != nullptr); + ASSERT(selectedMode != mcs7_40); + ASSERT(selectedMode->getDataMode()->getBandwidth() == MHz(20)); + std::cout << "Dynamic channel change to 11 downgraded to 20 MHz and updated peer state.\n"; + + // 4. Dynamic runtime channel change back to channel index 0 (std channel 1) + radio->setChannelNumber(0); + + // Verify 40 MHz HT operation is restored + ASSERT(mib->requirePrimaryChannel() == 0); + ASSERT(mib->getHtOperation().primaryChannel == 0); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 1); + ASSERT(mib->getHtOperation().operatingChannelWidth == MHz(40)); + + // Verify existing peer state is restored to 40 MHz + peerState = mib->findPeerHtState(staAddress); + ASSERT(peerState != nullptr); + ASSERT(peerState->valid); + ASSERT(peerState->negotiatedCapabilities.operation.primaryChannel == 0); + ASSERT(peerState->negotiatedCapabilities.operation.secondaryChannelOffset == 1); + ASSERT(peerState->negotiatedCapabilities.operation.operatingChannelWidth == MHz(40)); + + // Verify beacon advertised HT Operation restored to 40 MHz + mgmt->emitBeaconNow(); + beaconElem = mgmt->sentBeacons.back()->getHtOperation(); + ASSERT(beaconElem.primaryChannel == 1); + ASSERT(beaconElem.secondaryChannelOffset == 1); + ASSERT(beaconElem.staChannelWidth40Mhz); + + // Verify rate selection permits 40 MHz again + selectedMode = selectPeerCompatibleMode(modeSet, peerState, mcs7_40, staAddress); + ASSERT(selectedMode == mcs7_40); + std::cout << "Dynamic channel change back to 0 restored 40 MHz HT operation.\n"; + + // 5. Invalid primary channel rejected before committing + // Both signal-driven notifications and direct MIB calls must validate against active band. + bool threwSignalInvalidChannel = false; + try { + radio->emit(physicallayer::Ieee80211Radio::radioChannelChangedSignal, 99); + } + catch (const cRuntimeError& e) { + threwSignalInvalidChannel = true; + } + ASSERT(threwSignalInvalidChannel); + + bool threwMibInvalidChannel = false; + try { + mib->setPrimaryChannel(99, mgmt->getHtOperationBand()); + } + catch (const cRuntimeError& e) { + threwMibInvalidChannel = true; + } + ASSERT(threwMibInvalidChannel); + + // Prior state remains untouched + ASSERT(mib->requirePrimaryChannel() == 0); + ASSERT(mib->getHtOperation().primaryChannel == 0); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 1); + ASSERT(mib->getHtOperation().operatingChannelWidth == MHz(40)); + std::cout << "Invalid channel change rejected before commit.\n"; + + // 6. Dynamic band change + static const physicallayer::Ieee80211EnumeratedBand testBand5GHz("Test 5 GHz", + { GHz(5.180), GHz(5.200), GHz(5.220) }, + { 36, 40, 44 }); + radio->setBand(&testBand5GHz); + ASSERT(mgmt->getHtOperationBand() == &testBand5GHz); + ASSERT(mib->hasPrimaryChannel()); + ASSERT(mib->requirePrimaryChannel() == 0); + ASSERT(mib->getHtOperation().primaryChannel == 0); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 1); + ASSERT(mib->getHtOperation().operatingChannelWidth == MHz(40)); + + // Existing peer HT state should be re-negotiated against the new band + peerState = mib->findPeerHtState(staAddress); + ASSERT(peerState != nullptr); + ASSERT(peerState->valid); + ASSERT(peerState->negotiatedCapabilities.operation.primaryChannel == 0); + ASSERT(peerState->negotiatedCapabilities.operation.secondaryChannelOffset == 1); + ASSERT(peerState->negotiatedCapabilities.operation.operatingChannelWidth == MHz(40)); + + // Advertised beacon reflects new band standard channel number (36) + mgmt->emitBeaconNow(); + beaconElem = mgmt->sentBeacons.back()->getHtOperation(); + ASSERT(beaconElem.primaryChannel == 36); // standard channel 36 on wire + ASSERT(beaconElem.secondaryChannelOffset == 1); + ASSERT(beaconElem.staChannelWidth40Mhz); + + // Also verify that band change to a band without standard channel numbers throws + bool threwInvalidBand = false; + try { + mib->setPrimaryChannel(0, &physicallayer::Ieee80211CompliantBands::band5GHz); + } + catch (const cRuntimeError& e) { + threwInvalidBand = true; + } + ASSERT(threwInvalidBand); + ASSERT(mib->requirePrimaryChannel() == 0); + ASSERT(mib->getHtOperation().primaryChannel == 0); + ASSERT(mib->getHtOperation().secondaryChannelOffset == 1); + + std::cout << "Dynamic band change revalidated against new active band.\n"; + + // 7. Band notifications may arrive during initialization before the MAC + // publishes whether HT operation is supported. A legacy/non-HT AP must + // retain the radio's channel without applying HT-only band validation. + mib->updateLocalHtCapabilities(nullptr, {}, 0); + ASSERT(!mib->isHtOperationSupported()); + bool threwNonHtBandChange = false; + try { + radio->setBand(&physicallayer::Ieee80211CompliantBands::band5GHz); + } + catch (const cRuntimeError&) { + threwNonHtBandChange = true; + } + ASSERT(!threwNonHtBandChange); + ASSERT(mib->hasPrimaryChannel()); + ASSERT(mib->requirePrimaryChannel() == 0); + std::cout << "Non-HT band notification deferred HT-only validation.\n"; + } +}; + +Define_Module(Ieee80211MgmtApChannelChangeTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtAp; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Receiver; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Transmitter; + +module TestHt40Transmitter extends Ieee80211Transmitter +{ + parameters: + @class(::inet::ieee80211::TestHt40Transmitter); +} + +module TestHt40Receiver extends Ieee80211Receiver +{ + parameters: + @class(::inet::ieee80211::TestHt40Receiver); +} + +simple TestIeee80211MgmtAp extends Ieee80211MgmtAp +{ + parameters: + @class(::inet::ieee80211::TestIeee80211MgmtAp); +} + +simple Ieee80211MgmtApChannelChangeTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211MgmtApChannelChangeTest); +} + +network Ieee80211MgmtApChannelChangeTestNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint { + parameters: + wlan[*].mgmt.typename = "TestIeee80211MgmtAp"; + } + test: Ieee80211MgmtApChannelChangeTest; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211MgmtApChannelChangeTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 50us +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false + +**.hasStatus = true + +*.ap.wlan[0].mgmt.numAuthSteps = 2 +*.ap.wlan[0].address = "02:00:00:00:00:01" +*.ap.wlan[0].radio.channelNumber = 0 +*.ap.wlan[0].mib.htSecondaryChannelOffset = 1 +*.ap.wlan[0].radio.transmitter.typename = "TestHt40Transmitter" +*.ap.wlan[0].radio.receiver.typename = "TestHt40Receiver" +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 65Mbps +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.centerFrequency = 2.4GHz +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.ap.mobility.initialX = 0m +*.ap.mobility.initialY = 0m + +%contains: stdout +AP initialized with 40 MHz HT operation on channel 0. + +%contains: stdout +Station associated and negotiated 40 MHz HT operation. + +%contains: stdout +Dynamic channel change to 11 downgraded to 20 MHz and updated peer state. + +%contains: stdout +Dynamic channel change back to 0 restored 40 MHz HT operation. + +%contains: stdout +Invalid channel change rejected before commit. + +%contains: stdout +Dynamic band change revalidated against new active band. + +%contains: stdout +Non-HT band notification deferred HT-only validation. diff --git a/tests/module/Ieee80211MgmtApReassociationSnapshot_1.test b/tests/module/Ieee80211MgmtApReassociationSnapshot_1.test new file mode 100644 index 00000000000..fa3e2efcbd0 --- /dev/null +++ b/tests/module/Ieee80211MgmtApReassociationSnapshot_1.test @@ -0,0 +1,190 @@ +%description: +Verify that an AP reassociation response reconciles the committed peer +HT Operation with runtime channel changes at completion while preserving +the advertised response element. + +%file: TestIeee80211MgmtApReassociationSnapshot.cc + +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" +#include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" + +namespace inet { +namespace ieee80211 { + +class Ieee80211MgmtApReassociationSnapshotAp : public Ieee80211MgmtAp +{ + protected: + int advertisedPrimaryChannel = -1; + + virtual void start() override + { + // The response is injected directly below; keep beacon traffic out + // of this focused transaction. + Ieee80211MgmtApBase::start(); + } + + virtual void sendManagementFrame(const char *name, const Ptr& body, + int subtype, const MacAddress& destAddr, uint64_t transactionId = 0) override + { + if (subtype == ST_REASSOCIATIONRESPONSE) { + ASSERT(body->getHtOperationPresent()); + advertisedPrimaryChannel = body->getHtOperation().primaryChannel; + } + } + + public: + void markAuthenticated(const MacAddress& address) + { + auto& sta = staList[address]; + sta.address = address; + mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + } + + void submitReassociationRequest(const MacAddress& address) + { + Enter_Method("submitReassociationRequest"); + auto packet = new Packet("ReassociationRequest"); + auto body = makeShared(); + body->setCurrentAP(mib->address); + body->setSSID("SSID"); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 1; + body->setSupportedRates(rates); + body->setHtCapabilitiesPresent(true); + body->setHtCapabilities(makeHtCapabilitiesElement(mib->localHtCapabilities)); + body->setChunkLength(B(1)); + packet->insertAtBack(body); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleReassociationRequestFrame(packet, header); + } + + int getAdvertisedPrimaryChannel() const { return advertisedPrimaryChannel; } + + uint64_t getPendingTransactionId(const MacAddress& address) const + { + return staList.at(address).pendingAssociationTransactionId; + } + + void finishReassociationResponse(const MacAddress& address) + { + Enter_Method("finishReassociationResponse"); + auto response = new Packet("ReassociationResponse"); + auto header = makeShared(); + header->setType(ST_REASSOCIATIONRESPONSE); + header->setReceiverAddress(address); + response->insertAtBack(header); + response->addTag()->setTransactionId(getPendingTransactionId(address)); + frameTransmissionFinished(response, FRAME_TRANSMISSION_STATUS_ACKNOWLEDGED); + delete response; + } + + bool hasPendingAssociation(const MacAddress& address) const + { + auto it = staList.find(address); + return it != staList.end() && it->second.pendingAssociationTransactionId != 0; + } +}; + +Define_Module(Ieee80211MgmtApReassociationSnapshotAp); + +class Ieee80211MgmtApReassociationSnapshotTest : public cSimpleModule +{ + public: + Ieee80211MgmtApReassociationSnapshotTest() : cSimpleModule(65536) {} + + protected: + virtual void activity() override + { + auto mgmt = check_and_cast(getModuleByPath("^.ap.wlan[0].mgmt")); + auto mib = check_and_cast(getModuleByPath("^.ap.wlan[0].mib")); + auto radio = check_and_cast(getModuleByPath("^.ap.wlan[0].radio")); + const MacAddress staAddress("02:00:00:00:00:09"); + + mgmt->markAuthenticated(staAddress); + mgmt->submitReassociationRequest(staAddress); + ASSERT(mgmt->getAdvertisedPrimaryChannel() == 7); + ASSERT(mib->requirePrimaryChannel() == 6); + + radio->setChannelNumber(11); + ASSERT(mib->requirePrimaryChannel() == 11); + mgmt->finishReassociationResponse(staAddress); + + ASSERT(!mgmt->hasPendingAssociation(staAddress)); + ASSERT(mib->bssAccessPointData.stations.at(staAddress) == Ieee80211Mib::ASSOCIATED); + const auto *peerState = mib->findPeerHtState(staAddress); + ASSERT(peerState != nullptr); + ASSERT(peerState->negotiatedCapabilities.operation.primaryChannel == 11); + std::cout << "AP reassociation reconciles committed HT Operation with current channel after channel change.\n"; + } +}; + +Define_Module(Ieee80211MgmtApReassociationSnapshotTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtAp; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple Ieee80211MgmtApReassociationSnapshotAp extends Ieee80211MgmtAp +{ + parameters: + @class(::inet::ieee80211::Ieee80211MgmtApReassociationSnapshotAp); +} + +simple Ieee80211MgmtApReassociationSnapshotTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211MgmtApReassociationSnapshotTest); +} + +network Ieee80211MgmtApReassociationSnapshotNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint { + parameters: + wlan[*].mgmt.typename = "Ieee80211MgmtApReassociationSnapshotAp"; + } + test: Ieee80211MgmtApReassociationSnapshotTest; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211MgmtApReassociationSnapshotNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 1ms +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false + +*.ap.wlan[0].mgmt.beaconInterval = 10s +*.ap.wlan[0].address = "02:00:00:00:00:01" +*.ap.wlan[0].radio.channelNumber = 6 +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 65Mbps +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.centerFrequency = 2.4GHz +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +*.ap.mobility.initialX = 0m +*.ap.mobility.initialY = 0m + +%contains: stdout +AP reassociation reconciles committed HT Operation with current channel after channel change. From 1384084bb0c8994f84c2f4bdefc77c900610b027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 29 Aug 2026 23:11:32 +0200 Subject: [PATCH 04/56] fix(physicallayer): fix IEEE 802.11 HT/VHT MCS definitions and guard interval timing Correct HT MCS 32 (1 stream BPSK), MCS 76 (stream 4 16-QAM), and MCS 73 (stream 3 16-QAM) table definitions per IEEE 802.11-2024. Implement data guard-interval queries (getGuardInterval()) and symbol intervals (getSymbolInterval()). Fix HT and VHT signal mode symbol timing to use long symbol duration independently of data GI (Table 19-6 and Table 21-5). Round mixed-format HT and VHT short-GI data airtimes up to 4 us / long-GI symbol boundaries (Eq. 19-90 and Eq. 21-109) while keeping greenfield short-GI data airtimes raw (Eq. 19-92). Extend mode cache keys in Ieee80211HtCompliantModes and Ieee80211VhtCompliantModes with band mode and preamble format to prevent cache key collisions. --- .../Ieee80211LayeredOfdmTransmitter.cc | 3 +- .../wireless/ieee80211/mode/IIeee80211Mode.h | 8 +++ .../ieee80211/mode/Ieee80211HtMode.cc | 61 +++++++++++++++---- .../wireless/ieee80211/mode/Ieee80211HtMode.h | 11 +++- .../ieee80211/mode/Ieee80211OfdmMode.h | 2 +- .../ieee80211/mode/Ieee80211VhtMode.cc | 39 +++++++++--- .../ieee80211/mode/Ieee80211VhtMode.h | 15 ++++- 7 files changed, 109 insertions(+), 30 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc b/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc index 30eaa47132d..31d98a3cab4 100644 --- a/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmTransmitter.cc @@ -312,7 +312,7 @@ const ITransmission *Ieee80211LayeredOfdmTransmitter::createTransmission(const I // TODO: compute channel const simtime_t preambleDuration = mode->getPreambleLength(); const simtime_t headerDuration = mode->getHeaderMode()->getDuration(); - const simtime_t dataDuration = mode->getDataMode()->getDuration(packet->getDataLength()); + const simtime_t dataDuration = mode->getDataDuration(packet->getDataLength()); return new Ieee80211Transmission(transmitter, packet, startTime, endTime, preambleDuration, headerDuration, dataDuration, startPosition, endPosition, startOrientation, endOrientation, packetModel, bitModel, symbolModel, sampleModel, analogModel, mode, nullptr); } @@ -331,4 +331,3 @@ Ieee80211LayeredOfdmTransmitter::~Ieee80211LayeredOfdmTransmitter() } // namespace physicallayer } // namespace inet - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h index aa615b7d19e..8fa1b77bbc4 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h @@ -42,8 +42,13 @@ class INET_API IIeee80211DataMode : public cObject, public IPrintableObject virtual bps getGrossBitrate() const = 0; virtual b getPaddingLength(b dataLength) const = 0; virtual b getCompleteLength(b dataLength) const = 0; + // Returns the raw duration of the encoded data symbol train. PPDU-format + // rules may round this duration at the enclosing mode level. virtual const simtime_t getDuration(b dataLength) const = 0; virtual const simtime_t getSymbolInterval() const = 0; + // Returns the guard interval used by the data symbols, or -1 when the PHY + // has no meaningful guard interval (for example, non-OFDM modes). + virtual const simtime_t getGuardInterval() const { return -1; } virtual const IModulation *getModulation() const = 0; virtual int getNumberOfSpatialStreams() const = 0; }; @@ -68,6 +73,9 @@ class INET_API IIeee80211Mode : public cObject, public IPrintableObject IIeee80211HeaderMode *_getHeaderMode() const { return const_cast(getHeaderMode()); } IIeee80211DataMode *_getDataMode() const { return const_cast(getDataMode()); } virtual const simtime_t getDuration(b dataLength) const = 0; + virtual const simtime_t getPreambleDuration() const { return getPreambleMode()->getDuration(); } + virtual const simtime_t getHeaderDuration() const { return getHeaderMode()->getDuration(); } + virtual const simtime_t getDataDuration(b dataLength) const { return getDuration(dataLength) - getPreambleDuration() - getHeaderDuration(); } virtual const simtime_t getSlotTime() const = 0; virtual const simtime_t getSifsTime() const = 0; virtual const simtime_t getRifsTime() const = 0; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc index 319128cce20..d4498568496 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc @@ -161,6 +161,16 @@ unsigned int Ieee80211HtPreambleMode::computeNumberOfHTLongTrainings(unsigned in return numberOfSpaceTimeStreams == 3 ? 4 : numberOfSpaceTimeStreams; } +const simtime_t Ieee80211HtPreambleMode::getDurationBeforeHeader() const +{ + if (preambleFormat == HT_PREAMBLE_MIXED) + return getNonHTShortTrainingSequenceDuration() + getNonHTLongTrainingFieldDuration() + legacySignalMode->getDuration(); + else if (preambleFormat == HT_PREAMBLE_GREENFIELD) + return getHTGreenfieldShortTrainingFieldDuration() + getFirstHTLongTrainingFieldDuration(); + else + throw cRuntimeError("Unknown preamble format"); +} + const simtime_t Ieee80211HtPreambleMode::getDuration() const { // 20.3.7 Mathematical description of signals @@ -178,12 +188,8 @@ const simtime_t Ieee80211HtPreambleMode::getDuration() const bps Ieee80211HtSignalMode::computeGrossBitrate() const { unsigned int numberOfCodedBitsPerSymbol = modulation->getSubcarrierModulation()->getCodeWordSize() * getNumberOfDataSubcarriers(); - if (guardIntervalType == HT_GUARD_INTERVAL_LONG) - return bps(numberOfCodedBitsPerSymbol / getSymbolInterval()); - else if (guardIntervalType == HT_GUARD_INTERVAL_SHORT) - return bps(numberOfCodedBitsPerSymbol / getShortGISymbolInterval()); - else - throw cRuntimeError("Unknown guard interval type"); + // IEEE Std 802.11-2024, 19.3.11.11.6: the short GI applies only to the Data field. + return bps(numberOfCodedBitsPerSymbol / getSymbolInterval()); } bps Ieee80211HtSignalMode::computeNetBitrate() const @@ -239,6 +245,16 @@ bps Ieee80211HtModeBase::getGrossBitrate() const return grossBitrate; } +const simtime_t Ieee80211HtDataMode::getGuardInterval() const +{ + if (guardIntervalType == HT_GUARD_INTERVAL_LONG) + return getGIDuration(); + else if (guardIntervalType == HT_GUARD_INTERVAL_SHORT) + return getShortGIDuration(); + else + throw cRuntimeError("Unknown guard interval type"); +} + int Ieee80211HtModeBase::getNumberOfDataSubcarriers() const { return Ieee80211Htmcs::getNumberOfDataSubcarriers(bandwidth, mcsIndex); @@ -285,6 +301,22 @@ const simtime_t Ieee80211HtDataMode::getDuration(b dataLength) const return numberOfSymbols * getSymbolInterval(); } +const simtime_t Ieee80211HtMode::getDuration(b dataLength) const +{ + auto dataDuration = dataMode->getDuration(dataLength); + if (preambleMode->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED && + dataMode->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) + { + // IEEE Std 802.11-2024, 19.4.3, Eq. (19-90): mixed-format short-GI + // Data airtime is rounded up to a 4 us boundary. Eq. (19-92) leaves + // greenfield short-GI Data airtime at its raw 3.6 us symbol duration. + auto longGiSymbolInterval = dataMode->getDFTPeriod() + dataMode->getGIDuration(); + auto numberOfLongGiSymbols = (dataDuration.raw() + longGiSymbolInterval.raw() - 1) / longGiSymbolInterval.raw(); + dataDuration = SimTime::fromRaw(numberOfLongGiSymbols * longGiSymbolInterval.raw()); + } + return preambleMode->getDuration() + dataDuration; +} + const simtime_t Ieee80211HtMode::getSlotTime() const { if (centerFrequencyMode == BAND_2_4GHZ) @@ -326,7 +358,7 @@ Ieee80211HtCompliantModes::~Ieee80211HtCompliantModes() const Ieee80211HtMode *Ieee80211HtCompliantModes::getCompliantMode(const Ieee80211Htmcs *mcsMode, Ieee80211HtMode::BandMode centerFrequencyMode, Ieee80211HtPreambleMode::HighTroughputPreambleFormat preambleFormat, Ieee80211HtModeBase::GuardIntervalType guardIntervalType) { const char *name = ""; // TODO - auto htModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), guardIntervalType); + auto htModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), centerFrequencyMode, preambleFormat, guardIntervalType); auto mode = singleton.modeCache.find(htModeId); if (mode == singleton.modeCache.end()) { const Ieee80211OfdmModulation *modulation = nullptr; @@ -348,7 +380,7 @@ const Ieee80211HtMode *Ieee80211HtCompliantModes::getCompliantMode(const Ieee802 const Ieee80211HtDataMode *dataMode = new Ieee80211HtDataMode(mcsMode, mcsMode->getBandwidth(), guardIntervalType); const Ieee80211HtPreambleMode *preambleMode = new Ieee80211HtPreambleMode(htSignal, legacySignal, preambleFormat, dataMode->getNumberOfSpatialStreams()); const Ieee80211HtMode *htMode = new Ieee80211HtMode(name, preambleMode, dataMode, centerFrequencyMode); - singleton.modeCache.insert(std::pair, const Ieee80211HtMode *>(htModeId, htMode)); + singleton.modeCache.insert(std::pair, const Ieee80211HtMode *>(htModeId, htMode)); return htMode; } return mode->second; @@ -442,7 +474,9 @@ const DI Ieee80211HtmcsTable::htMcs29BW40MHz([](){ return new Ie const DI Ieee80211HtmcsTable::htMcs30BW40MHz([](){ return new Ieee80211Htmcs(30, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs31BW40MHz([](){ return new Ieee80211Htmcs(31, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Ieee80211HtCompliantCodes::htConvolutionalCode5_6, MHz(40));}); -const DI Ieee80211HtmcsTable::htMcs32BW40MHz([](){ return new Ieee80211Htmcs(32, &BpskModulation::singleton, &BpskModulation::singleton, &BpskModulation::singleton, &BpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(40));}); +// IEEE Std 802.11-2024, Table 19-35: optional MCS 32 is one BPSK stream. +// This corrects the previous incorrect 4-stream all-BPSK constructor. +const DI Ieee80211HtmcsTable::htMcs32BW40MHz([](){ return new Ieee80211Htmcs(32, &BpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs33BW20MHz([](){ return new Ieee80211Htmcs(33, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(20));}); const DI Ieee80211HtmcsTable::htMcs34BW20MHz([](){ return new Ieee80211Htmcs(34, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(20));}); @@ -490,7 +524,9 @@ const DI Ieee80211HtmcsTable::htMcs72BW20MHz([](){ return new Ie const DI Ieee80211HtmcsTable::htMcs73BW20MHz([](){ return new Ieee80211Htmcs(73, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); const DI Ieee80211HtmcsTable::htMcs74BW20MHz([](){ return new Ieee80211Htmcs(74, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); const DI Ieee80211HtmcsTable::htMcs75BW20MHz([](){ return new Ieee80211Htmcs(75, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); -const DI Ieee80211HtmcsTable::htMcs76BW20MHz([](){ return new Ieee80211Htmcs(76, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); +// IEEE Std 802.11-2024, Table 19-38 (continued): MCS 76 uses 64-QAM for streams 1-3 and 16-QAM for stream 4. +// This corrects the previous incorrect QPSK modulation for the 4th stream. +const DI Ieee80211HtmcsTable::htMcs76BW20MHz([](){ return new Ieee80211Htmcs(76, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(20));}); const DI Ieee80211HtmcsTable::htMcs33BW40MHz([](){ return new Ieee80211Htmcs(33, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs34BW40MHz([](){ return new Ieee80211Htmcs(34, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode1_2, MHz(40));}); @@ -536,7 +572,9 @@ const DI Ieee80211HtmcsTable::htMcs69BW40MHz([](){ return new Ie const DI Ieee80211HtmcsTable::htMcs70BW40MHz([](){ return new Ieee80211Htmcs(70, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs71BW40MHz([](){ return new Ieee80211Htmcs(71, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs72BW40MHz([](){ return new Ieee80211Htmcs(72, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); -const DI Ieee80211HtmcsTable::htMcs73BW40MHz([](){ return new Ieee80211Htmcs(73, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); +// IEEE Std 802.11-2024, Table 19-40: MCS 73 uses 64-QAM for streams 1-2 and 16-QAM for stream 3. +// This corrects the previous incorrect 64-QAM modulation for the 3rd stream. +const DI Ieee80211HtmcsTable::htMcs73BW40MHz([](){ return new Ieee80211Htmcs(73, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs74BW40MHz([](){ return new Ieee80211Htmcs(74, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam16Modulation::singleton, &Qam16Modulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); const DI Ieee80211HtmcsTable::htMcs75BW40MHz([](){ return new Ieee80211Htmcs(75, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &Qam64Modulation::singleton, &QpskModulation::singleton, &Ieee80211OfdmCompliantCodes::ofdmConvolutionalCode3_4, MHz(40));}); @@ -544,4 +582,3 @@ const DI Ieee80211HtmcsTable::htMcs76BW40MHz([](){ return new Ie } /* namespace physicallayer */ } /* namespace inet */ - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h index 9184765f083..5e5d43b6fa3 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h @@ -153,6 +153,7 @@ class INET_API Ieee80211HtPreambleMode : public IIeee80211PreambleMode, public I virtual const simtime_t getSecondAndSubsequentHTLongTrainingFielDuration() const { return 4E-6; } // HT-LTFs, s = 2,3,..,n virtual unsigned int getNumberOfHtLongTrainings() const { return numberOfHTLongTrainings; } + virtual const simtime_t getDurationBeforeHeader() const; virtual const simtime_t getDuration() const override; virtual Ptr createPreamble() const override { return makeShared(); } @@ -218,7 +219,8 @@ class INET_API Ieee80211HtDataMode : public IIeee80211DataMode, public Ieee80211 virtual bps getGrossBitrate() const override { return Ieee80211HtModeBase::getGrossBitrate(); } virtual const Ieee80211Htmcs *getModulationAndCodingScheme() const { return modulationAndCodingScheme; } virtual const Ieee80211HtCode *getCode() const { return modulationAndCodingScheme->getCode(); } - virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } + virtual const simtime_t getGuardInterval() const override; + virtual const simtime_t getSymbolInterval() const override { return getDFTPeriod() + getGuardInterval(); } virtual const Ieee80211OfdmModulation *getModulation() const override { return modulationAndCodingScheme->getModulation(); } }; @@ -263,7 +265,10 @@ class INET_API Ieee80211HtMode : public Ieee80211ModeBase virtual int getMpduMaxLength() const override { return 65535; } // in octets virtual BandMode getCenterFrequencyMode() const { return centerFrequencyMode; } - virtual const simtime_t getDuration(b dataLength) const override { return preambleMode->getDuration() + dataMode->getDuration(dataLength); } + virtual const simtime_t getDuration(b dataLength) const override; + virtual const simtime_t getPreambleDuration() const override { return preambleMode->getDurationBeforeHeader(); } + virtual const simtime_t getHeaderDuration() const override { return preambleMode->getDuration() - getPreambleDuration(); } + virtual const simtime_t getDataDuration(b dataLength) const override { return getDuration(dataLength) - preambleMode->getDuration(); } }; // A specification of the high-throughput (HT) physical layer (PHY) @@ -462,7 +467,7 @@ class INET_API Ieee80211HtCompliantModes protected: static OPP_THREAD_LOCAL const Ieee80211HtCompliantModes singleton; - mutable std::map, const Ieee80211HtMode *> modeCache; + mutable std::map, const Ieee80211HtMode *> modeCache; public: Ieee80211HtCompliantModes(); diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h index 6ca84f780f3..608943efba3 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h @@ -124,6 +124,7 @@ class INET_API Ieee80211OfdmDataMode : public IIeee80211DataMode, public Ieee802 virtual b getPaddingLength(b dataLength) const override; virtual b getCompleteLength(b dataLength) const override; virtual const simtime_t getDuration(b dataLength) const override; + virtual const simtime_t getGuardInterval() const override { return getGIDuration(); } const Ieee80211OfdmCode *getCode() const { return code; } virtual const simtime_t getSymbolInterval() const override { return Ieee80211OfdmTimingRelatedParametersBase::getSymbolInterval(); } @@ -265,4 +266,3 @@ class INET_API Ieee80211OfdmCompliantModes } // namespace inet #endif - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc index 89b142e37cb..ea12fd18697 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc @@ -241,6 +241,13 @@ unsigned int Ieee80211VhtPreambleMode::computeNumberOfHTLongTrainings(unsigned i return numberOfSpaceTimeStreams == 3 ? 4 : numberOfSpaceTimeStreams; } +const simtime_t Ieee80211VhtPreambleMode::getDurationBeforeHeader() const +{ + // IEEE Std 802.11-2024, 21.3.2: the L-SIG duration is part of the + // pre-header timing of the supported VHT mixed format. + return getNonHTShortTrainingSequenceDuration() + getNonHTLongTrainingFieldDuration() + getLSIGDuration(); +} + const simtime_t Ieee80211VhtPreambleMode::getDuration() const { // 21.3.4 Mathematical description of signals @@ -251,12 +258,9 @@ const simtime_t Ieee80211VhtPreambleMode::getDuration() const bps Ieee80211VhtSignalMode::computeGrossBitrate() const { unsigned int numberOfCodedBitsPerSymbol = modulation->getSubcarrierModulation()->getCodeWordSize() * getNumberOfDataSubcarriers(); - if (guardIntervalType == HT_GUARD_INTERVAL_LONG) - return bps(numberOfCodedBitsPerSymbol / getSymbolInterval()); - else if (guardIntervalType == HT_GUARD_INTERVAL_SHORT) - return bps(numberOfCodedBitsPerSymbol / getShortGISymbolInterval()); - else - throw cRuntimeError("Unknown guard interval type"); + // IEEE Std 802.11-2024, Table 21-5: VHT-SIG fields use TSYML even + // when the Data field uses short GI; their signaling rate is GI-independent. + return bps(numberOfCodedBitsPerSymbol / getSymbolInterval()); } bps Ieee80211VhtSignalMode::computeNetBitrate() const @@ -639,6 +643,20 @@ const simtime_t Ieee80211VhtDataMode::getDuration(b dataLength) const return numberOfSymbols * getSymbolInterval(); } +const simtime_t Ieee80211VhtMode::getDataDuration(b dataBitLength) const +{ + auto dataDuration = dataMode->getDuration(dataBitLength); + if (dataMode->getGuardInterval() == dataMode->getShortGIDuration()) { + // IEEE Std 802.11-2024, 21.4.3, Eq. (21-109): short-GI VHT data + // airtime is the raw TSYMS train rounded up to a TSYML boundary. + // This corrects the previous implementation that used the raw short-GI symbol train. + const auto longGiSymbolInterval = dataMode->getDFTPeriod() + dataMode->getGIDuration(); + const auto numberOfLongGiSymbols = (dataDuration.raw() + longGiSymbolInterval.raw() - 1) / longGiSymbolInterval.raw(); + dataDuration = SimTime::fromRaw(numberOfLongGiSymbols * longGiSymbolInterval.raw()); + } + return dataDuration; +} + const simtime_t Ieee80211VhtMode::getSlotTime() const { if (centerFrequencyMode == BAND_5GHZ) @@ -672,9 +690,13 @@ Ieee80211VhtCompliantModes::~Ieee80211VhtCompliantModes() const Ieee80211VhtMode *Ieee80211VhtCompliantModes::getCompliantMode(const Ieee80211Vhtmcs *mcsMode, Ieee80211VhtMode::BandMode centerFrequencyMode, Ieee80211VhtPreambleMode::HighTroughputPreambleFormat preambleFormat, Ieee80211VhtModeBase::GuardIntervalType guardIntervalType) { + // IEEE Std 802.11-2024, 21.3.2 permits VHT PPDUs only in the mixed + // preamble format represented by this mode implementation. + if (preambleFormat != Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED) + throw cRuntimeError("Unsupported VHT preamble format: only HT_PREAMBLE_MIXED is supported (IEEE Std 802.11-2024, 21.3.2)"); const char *name = ""; // TODO unsigned int nss = mcsMode->getNumNss(); - auto htModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), guardIntervalType, nss); + auto htModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), guardIntervalType, nss, centerFrequencyMode, preambleFormat); auto mode = singleton.modeCache.find(htModeId); if (mode == singleton.modeCache.end()) { const Ieee80211OfdmSignalMode *legacySignal = nullptr; @@ -693,7 +715,7 @@ const Ieee80211VhtMode *Ieee80211VhtCompliantModes::getCompliantMode(const Ieee8 const Ieee80211VhtDataMode *dataMode = new Ieee80211VhtDataMode(mcsMode, mcsMode->getBandwidth(), guardIntervalType); const Ieee80211VhtPreambleMode *preambleMode = new Ieee80211VhtPreambleMode(htSignal, legacySignal, preambleFormat, dataMode->getNumberOfSpatialStreams()); const Ieee80211VhtMode *htMode = new Ieee80211VhtMode(name, preambleMode, dataMode, centerFrequencyMode); - singleton.modeCache.insert(std::pair, const Ieee80211VhtMode *>(htModeId, htMode)); + singleton.modeCache.insert(std::pair(htModeId, htMode)); return htMode; } return mode->second; @@ -1072,4 +1094,3 @@ const DI Ieee80211VhtmcsTable::vhtMcs9BW160MHzNss8([](){ return } /* namespace physicallayer */ } /* namespace inet */ - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h index aa4aa3639b0..394086ec06f 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h @@ -96,6 +96,8 @@ class INET_API Ieee80211VhtSignalMode : public IIeee80211HeaderMode, public Ieee virtual b getLength() const override; virtual bps getNetBitrate() const override { return Ieee80211VhtModeBase::getNetBitrate(); } virtual bps getGrossBitrate() const override { return Ieee80211VhtModeBase::getGrossBitrate(); } + // IEEE Std 802.11-2024, Table 21-5: VHT-SIG uses the long-GI symbol + // interval independently of the data field's selected guard interval. virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } virtual const Ieee80211OfdmModulation *getModulation() const override { return modulation; } virtual const Ieee80211VhtCode *getCode() const { return code; } @@ -148,6 +150,7 @@ class INET_API Ieee80211VhtPreambleMode : public IIeee80211PreambleMode, public virtual const simtime_t getSecondAndSubsequentHTLongTrainingFielDuration() const { return 4E-6; } // HT-LTFs, s = 2,3,..,n virtual unsigned int getNumberOfHtLongTrainings() const { return numberOfHTLongTrainings; } + virtual const simtime_t getDurationBeforeHeader() const; virtual const simtime_t getDuration() const override; virtual Ptr createPreamble() const override { return makeShared(); } @@ -237,7 +240,10 @@ class INET_API Ieee80211VhtDataMode : public IIeee80211DataMode, public Ieee8021 virtual bps getGrossBitrate() const override { return Ieee80211VhtModeBase::getGrossBitrate(); } virtual const Ieee80211Vhtmcs *getModulationAndCodingScheme() const { return modulationAndCodingScheme; } virtual const Ieee80211VhtCode *getCode() const { return modulationAndCodingScheme->getCode(); } - virtual const simtime_t getSymbolInterval() const override { return Ieee80211HtTimingRelatedParametersBase::getSymbolInterval(); } + virtual const simtime_t getGuardInterval() const override { return guardIntervalType == HT_GUARD_INTERVAL_LONG ? getGIDuration() : getShortGIDuration(); } + // IEEE Std 802.11-2024, Tables 21-5 and 21-8: the VHT Data symbol + // interval is TSYML for long GI and TSYMS for short GI. + virtual const simtime_t getSymbolInterval() const override { return getDFTPeriod() + getGuardInterval(); } virtual const Ieee80211OfdmModulation *getModulation() const override { return modulationAndCodingScheme->getModulation(); } }; @@ -280,7 +286,10 @@ class INET_API Ieee80211VhtMode : public Ieee80211ModeBase virtual int getMpduMaxLength() const override { return 65535; } // in octets virtual BandMode getCenterFrequencyMode() const { return centerFrequencyMode; } - virtual const simtime_t getDuration(b dataBitLength) const override { return preambleMode->getDuration() + dataMode->getDuration(dataBitLength); } + virtual const simtime_t getDuration(b dataBitLength) const override { return preambleMode->getDuration() + getDataDuration(dataBitLength); } + virtual const simtime_t getPreambleDuration() const override { return preambleMode->getDurationBeforeHeader(); } + virtual const simtime_t getHeaderDuration() const override { return preambleMode->getDuration() - getPreambleDuration(); } + virtual const simtime_t getDataDuration(b dataBitLength) const override; }; // A specification of the high-throughput (HT) physical layer (PHY) @@ -679,7 +688,7 @@ class INET_API Ieee80211VhtCompliantModes protected: static OPP_THREAD_LOCAL const Ieee80211VhtCompliantModes singleton; - mutable std::map, const Ieee80211VhtMode *> modeCache; + mutable std::map, const Ieee80211VhtMode *> modeCache; public: Ieee80211VhtCompliantModes(); From 630fa0eb260699b04797bd33ac8d2e750dcb8f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 29 Aug 2026 23:11:37 +0200 Subject: [PATCH 05/56] fix(physicallayer): complete HT mode set guard interval catalog and lookup Complete Ieee80211ModeSet entries for "n(mixed-2.4Ghz)" with short and long guard-interval variants via completeHtGuardIntervalVariants. Add guard-interval qualified lookups in findMode and getMode. Add findCompatibleMode for exact PHY parameter matching across mode sets (treating negative guard intervals as wildcards for non-OFDM modes). Ensure strict rate monotonicity in getSlowerMode, getFasterMode, getSlowerMandatoryMode, and getFasterMandatoryMode. Implement getMandatoryModeAtOrBelow to find the highest-bitrate mandatory mode at or below a target rate. Add null check in Ieee80211MgmtBase before updating local HT capabilities. Update Ieee80211HtModeSet_1.test and Ieee80211PeerModeSelection_1.test for short-GI mode set awareness. --- .../ieee80211/mode/Ieee80211ModeSet.cc | 125 +++++++++++++----- .../ieee80211/mode/Ieee80211ModeSet.h | 14 +- tests/unit/Ieee80211PeerModeSelection_1.test | 4 + 3 files changed, 108 insertions(+), 35 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index 5a53166e2cc..e21dbf9d613 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -578,6 +578,8 @@ Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector en if (this->referenceMode->getLegacyCwMin() < 0 || this->referenceMode->getLegacyCwMax() < this->referenceMode->getLegacyCwMin()) throw cRuntimeError("Reference mode '%s' in IEEE 802.11 mode set '%s' has invalid contention window bounds", this->referenceMode->getName(), this->name.c_str()); std::vector *nonConstEntries = const_cast *>(&this->entries); + // Keep equal-bitrate modes in declaration order because unqualified lookups + // intentionally preserve the historically preferred mode. std::stable_sort(nonConstEntries->begin(), nonConstEntries->end(), EntryNetBitrateComparator()); // Explicit Supported-Rates eligibility on the authoritative Entry keeps // HT/VHT MCSs out without concrete-type or name-based inference. The @@ -663,20 +665,52 @@ bool Ieee80211ModeSet::getIsMandatory(const IIeee80211Mode *mode) const return entries[getModeIndex(mode)].isMandatory; } -const IIeee80211Mode *Ieee80211ModeSet::findMode(bps bitrate, Hz bandwidth, int numSpatialStreams) const +const IIeee80211Mode *Ieee80211ModeSet::findCompatibleMode(const IIeee80211Mode *mode) const { - return findMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams); + if (mode == nullptr) + return nullptr; + + const auto sourceDataMode = mode->getDataMode(); + const auto sourceBitrate = sourceDataMode->getNetBitrate(); + const auto sourceBandwidth = sourceDataMode->getBandwidth(); + const auto sourceGuardInterval = sourceDataMode->getGuardInterval(); + const auto minBitrate = sourceBitrate - Mbps(0.05); + const auto maxBitrate = sourceBitrate + Mbps(0.05); + for (const auto& entry : entries) { + const auto candidateDataMode = entry.mode->getDataMode(); + const auto candidateBandwidth = candidateDataMode->getBandwidth(); + const auto candidateGuardInterval = candidateDataMode->getGuardInterval(); + const bool bandwidthMatches = (std::isnan(sourceBandwidth.get()) && std::isnan(candidateBandwidth.get())) || + (!std::isnan(sourceBandwidth.get()) && !std::isnan(candidateBandwidth.get()) && sourceBandwidth == candidateBandwidth); + // GI = -1 indicates unconstrained guard interval (e.g., non-OFDM modes). + // Treat GI = -1 as matching any candidate GI, and require exact match when both are >= 0. + const bool guardIntervalMatches = (sourceGuardInterval < SIMTIME_ZERO) || (candidateGuardInterval < SIMTIME_ZERO) || + (sourceGuardInterval >= SIMTIME_ZERO && candidateGuardInterval == sourceGuardInterval); + if (minBitrate <= candidateDataMode->getNetBitrate() && candidateDataMode->getNetBitrate() <= maxBitrate && + bandwidthMatches && candidateDataMode->getNumberOfSpatialStreams() == sourceDataMode->getNumberOfSpatialStreams() && + guardIntervalMatches) + return entry.mode; + } + return nullptr; } -const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams) const +const IIeee80211Mode *Ieee80211ModeSet::findMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +{ + return findMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval); +} + +const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const { for (size_t index = 0; index < entries.size(); index++) { auto mode = entries[index].mode; auto dataMode = mode->getDataMode(); auto bitrate = dataMode->getNetBitrate(); + bool guardIntervalMatches = guardInterval < SIMTIME_ZERO || + dataMode->getGuardInterval() == guardInterval; if (minBitrate <= bitrate && bitrate <= maxBitrate && (std::isnan(bandwidth.get()) || dataMode->getBandwidth() == bandwidth) && - (numSpatialStreams == -1 || dataMode->getNumberOfSpatialStreams() == numSpatialStreams)) + (numSpatialStreams == -1 || dataMode->getNumberOfSpatialStreams() == numSpatialStreams) && + guardIntervalMatches) { return entries[index].mode; } @@ -684,20 +718,22 @@ const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, return nullptr; } -const IIeee80211Mode *Ieee80211ModeSet::getMode(bps bitrate, Hz bandwidth, int numSpatialStreams) const +const IIeee80211Mode *Ieee80211ModeSet::getMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const { - const IIeee80211Mode *mode = getMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams); + const IIeee80211Mode *mode = getMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval); if (mode == nullptr) - throw cRuntimeError("Unknown bitrate: %g in operation mode: '%s'", bitrate.get(), getName()); + throw cRuntimeError("Unknown mode for bitrate %g bps, bandwidth %g Hz, %d spatial streams, and %s guard interval in operation mode '%s'", + bitrate.get(), bandwidth.get(), numSpatialStreams, guardInterval.str().c_str(), getName()); else return mode; } -const IIeee80211Mode *Ieee80211ModeSet::getMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams) const +const IIeee80211Mode *Ieee80211ModeSet::getMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const { - const IIeee80211Mode *mode = findMode(minBitrate, maxBitrate, bandwidth, numSpatialStreams); + const IIeee80211Mode *mode = findMode(minBitrate, maxBitrate, bandwidth, numSpatialStreams, guardInterval); if (mode == nullptr) - throw cRuntimeError("Unknown bitrate: (%g - %g) in operation mode: '%s'", minBitrate.get(), maxBitrate.get(), getName()); + throw cRuntimeError("Unknown mode for bitrate range (%g - %g) bps, bandwidth %g Hz, %d spatial streams, and %s guard interval in operation mode '%s'", + minBitrate.get(), maxBitrate.get(), bandwidth.get(), numSpatialStreams, guardInterval.str().c_str(), getName()); else return mode; } @@ -715,19 +751,25 @@ const IIeee80211Mode *Ieee80211ModeSet::getFastestMode() const const IIeee80211Mode *Ieee80211ModeSet::getSlowerMode(const IIeee80211Mode *mode) const { int index = findModeIndex(mode); - if (index > 0) - return entries[index - 1].mode; - else - return nullptr; + if (index > 0) { + auto bitrate = mode->getDataMode()->getNetBitrate(); + for (int i = index - 1; i >= 0; i--) + if (entries[i].mode->getDataMode()->getNetBitrate() < bitrate) + return entries[i].mode; + } + return nullptr; } const IIeee80211Mode *Ieee80211ModeSet::getFasterMode(const IIeee80211Mode *mode) const { int index = findModeIndex(mode); - if (index >= 0 && index < (int)entries.size() - 1) - return entries[index + 1].mode; - else - return nullptr; + if (index >= 0) { + auto bitrate = mode->getDataMode()->getNetBitrate(); + for (size_t i = index + 1; i < entries.size(); i++) + if (entries[i].mode->getDataMode()->getNetBitrate() > bitrate) + return entries[i].mode; + } + return nullptr; } const IIeee80211Mode *Ieee80211ModeSet::getSlowestMandatoryMode() const @@ -758,24 +800,47 @@ const IIeee80211Mode *Ieee80211ModeSet::getFastestLegacyOperationalMode() const return legacyOperationalModes.empty() ? nullptr : legacyOperationalModes.back(); } +const IIeee80211Mode *Ieee80211ModeSet::getMandatoryModeAtOrBelow(const IIeee80211Mode *mode) const +{ + // Returns the highest-bitrate mandatory mode whose bitrate is <= the given mode's bitrate. + // For equal-bitrate mandatory modes, returns the first-encountered entry (strict > comparison). + // This may return a different mode object than the input when the input is mandatory and + // shares bitrate with another mandatory mode, but the resulting rate is behavior-equivalent. + const auto bitrate = mode->getDataMode()->getNetBitrate(); + const IIeee80211Mode *result = nullptr; + for (const auto& entry : entries) { + const auto entryBitrate = entry.mode->getDataMode()->getNetBitrate(); + if (entry.isMandatory && entryBitrate <= bitrate && + (result == nullptr || entryBitrate > result->getDataMode()->getNetBitrate())) + result = entry.mode; + } + return result; +} + const IIeee80211Mode *Ieee80211ModeSet::getSlowerMandatoryMode(const IIeee80211Mode *mode) const { - int index = findModeIndex(mode); - if (index > 0) - for (int i = index - 1; i >= 0; i--) - if (entries[i].isMandatory) - return entries[i].mode; - return nullptr; + const auto bitrate = mode->getDataMode()->getNetBitrate(); + const IIeee80211Mode *result = nullptr; + for (const auto& entry : entries) { + const auto entryBitrate = entry.mode->getDataMode()->getNetBitrate(); + if (entry.isMandatory && entryBitrate < bitrate && + (result == nullptr || entryBitrate > result->getDataMode()->getNetBitrate())) + result = entry.mode; + } + return result; } const IIeee80211Mode *Ieee80211ModeSet::getFasterMandatoryMode(const IIeee80211Mode *mode) const { - int index = findModeIndex(mode); - if (index >= 0) - for (size_t i = index + 1; i < entries.size(); i++) - if (entries[i].isMandatory) - return entries[i].mode; - return nullptr; + const auto bitrate = mode->getDataMode()->getNetBitrate(); + const IIeee80211Mode *result = nullptr; + for (const auto& entry : entries) { + const auto entryBitrate = entry.mode->getDataMode()->getNetBitrate(); + if (entry.isMandatory && entryBitrate > bitrate && + (result == nullptr || entryBitrate < result->getDataMode()->getNetBitrate())) + result = entry.mode; + } + return result; } const Ieee80211ModeSet *Ieee80211ModeSet::findModeSet(const char *mode) diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h index e14627dbcbe..0e1a3b6254f 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -88,17 +88,21 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject bool containsMode(const IIeee80211Mode *mode) const { return findModeIndex(mode) != -1; } bool getIsMandatory(const IIeee80211Mode *mode) const; - - const IIeee80211Mode *findMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1) const; - const IIeee80211Mode *findMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1) const; - const IIeee80211Mode *getMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1) const; - const IIeee80211Mode *getMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1) const; + // Finds a mode with the same PHY tuple as mode. Unlike findMode(), this + // treats an absent guard interval (negative value) as an exact value. + const IIeee80211Mode *findCompatibleMode(const IIeee80211Mode *mode) const; + + const IIeee80211Mode *findMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; + const IIeee80211Mode *findMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; + const IIeee80211Mode *getMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; + const IIeee80211Mode *getMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; const IIeee80211Mode *getSlowestMode() const; const IIeee80211Mode *getFastestMode() const; const IIeee80211Mode *getSlowerMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *getFasterMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *getSlowestMandatoryMode() const; const IIeee80211Mode *getFastestMandatoryMode() const; + const IIeee80211Mode *getMandatoryModeAtOrBelow(const IIeee80211Mode *mode) const; const IIeee80211Mode *getSlowerMandatoryMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *getFasterMandatoryMode(const IIeee80211Mode *mode) const; diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index b472fd4e291..6d2b7fbe770 100644 --- a/tests/unit/Ieee80211PeerModeSelection_1.test +++ b/tests/unit/Ieee80211PeerModeSelection_1.test @@ -134,6 +134,10 @@ const auto *shortGiFallback = selectPeerCompatibleMode(modeSet, &shortGiDisabled ASSERT(shortGiFallback == mcs8Long); ASSERT(!shortGiFallback->isHtShortGuardInterval()); +auto shortGiDisabledOnlyMcs0Peer = makePeerState({0}, {MHz(20)}, MHz(20)); +const auto *shortGiMcs0Fallback = selectPeerCompatibleMode(modeSet, &shortGiDisabledOnlyMcs0Peer, mcs8Short, peer); +ASSERT(shortGiMcs0Fallback == mcs0Long); + auto compatiblePeer = makePeerState({2}, {MHz(20)}, MHz(40)); ASSERT(selectPeerCompatibleMode(modeSet, &compatiblePeer, mcs2Long, peer) == mcs2Long); From e4c6b45800b042f6470a628f591a25ed1b83036a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 29 Aug 2026 23:11:43 +0200 Subject: [PATCH 06/56] fix(physicallayer): support atomic radio and transmitter mode-set switching Document modeSet precedence over opMode in Ieee80211ConfigureRadioCommand. Implement setModeSetAndMode in Ieee80211Radio and Ieee80211Transmitter to allow atomic reconfiguration of mode set and mode while validating mode membership. Update Ieee80211Transmitter::setModeSet to re-select compatible modes via findCompatibleMode (preserving bitrate, bandwidth, NSS, and GI) or throw when an incompatible transition occurs. In Ieee80211Transmitter::createTransmission, query preamble, header, and data durations directly through IIeee80211Mode methods. --- .../packetlevel/Ieee80211ControlInfo.msg | 6 ++-- .../ieee80211/packetlevel/Ieee80211Radio.cc | 28 +++++++++++++++---- .../ieee80211/packetlevel/Ieee80211Radio.h | 2 +- .../packetlevel/Ieee80211Transmitter.cc | 28 ++++++++++++++----- .../packetlevel/Ieee80211Transmitter.h | 4 +++ 5 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo.msg b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo.msg index 84d97310414..b819d14e951 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo.msg +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211ControlInfo.msg @@ -17,9 +17,9 @@ namespace inet::physicallayer; // class Ieee80211ConfigureRadioCommand extends ConfigureRadioCommand { - string opMode; // new default operation mode or "" if not set. - const Ieee80211ModeSet *modeSet; // new default mode set or nullptr if not set. - const IIeee80211Mode *mode; // new default transmission mode or nullptr if not set. + string opMode; // new default operation mode or "" if not set; ignored when modeSet is set. + const Ieee80211ModeSet *modeSet; // new default mode set or nullptr if not set; takes precedence over opMode. + const IIeee80211Mode *mode; // new default transmission mode or nullptr if not set; atomically validated against the resolved mode set. IIeee80211Band *band; // new default band or nullptr if not set. Ieee80211Channel *channel; // new default band and channel or nullptr if not set. int channelNumber = -1; // new default channel number in the range [0, numChannels] or -1 if not set. diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 56114b6c2c6..d95d17a929d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -58,13 +58,17 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) Ieee80211ConfigureRadioCommand *configureCommand = dynamic_cast(message->getControlInfo()); if (configureCommand != nullptr) { const char *opMode = configureCommand->getOpMode(); - if (*opMode) - setModeSet(Ieee80211ModeSet::getModeSet(opMode)); const Ieee80211ModeSet *modeSet = configureCommand->getModeSet(); - if (modeSet != nullptr) - setModeSet(modeSet); + // NOTE: When both modeSet and opMode are present, modeSet takes precedence + // and opMode is silently ignored. This differs from the previous behavior + // where both were applied sequentially (with modeSet as final state). + const Ieee80211ModeSet *newModeSet = modeSet != nullptr ? modeSet : (*opMode ? Ieee80211ModeSet::getModeSet(opMode) : nullptr); const IIeee80211Mode *mode = configureCommand->getMode(); - if (mode != nullptr) + if (newModeSet != nullptr && mode != nullptr) + setModeSetAndMode(newModeSet, mode); + else if (newModeSet != nullptr) + setModeSet(newModeSet); + else if (mode != nullptr) setMode(mode); const IIeee80211Band *band = configureCommand->getBand(); if (band != nullptr) @@ -91,6 +95,19 @@ void Ieee80211Radio::setModeSet(const Ieee80211ModeSet *modeSet) emit(listeningChangedSignal, 0); } +void Ieee80211Radio::setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) +{ + if (modeSet != nullptr && mode != nullptr && !modeSet->containsMode(mode)) + throw cRuntimeError("Invalid mode"); + Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); + Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); + ieee80211Transmitter->setModeSetAndMode(modeSet, mode); + ieee80211Receiver->setModeSet(modeSet); + EV << "Changing radio mode set to " << modeSet << " and mode to " << mode << endl; + receptionTimer = nullptr; + emit(listeningChangedSignal, 0); +} + void Ieee80211Radio::setMode(const IIeee80211Mode *mode) { Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); @@ -334,4 +351,3 @@ const Ptr Ieee80211Radio::peekIeee80211PhyHeaderAtFron } // namespace physicallayer } // namespace inet - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h index aa416de6ed8..874a7890319 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h @@ -47,6 +47,7 @@ class INET_API Ieee80211Radio : public FlatRadioBase Ieee80211Radio(); virtual void setModeSet(const Ieee80211ModeSet *modeSet); + virtual void setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode); virtual void setMode(const IIeee80211Mode *mode); virtual void setBand(const IIeee80211Band *band); virtual void setChannel(const Ieee80211Channel *channel); @@ -57,4 +58,3 @@ class INET_API Ieee80211Radio : public FlatRadioBase } // namespace inet #endif - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index 6c76381a790..7011071f1d5 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -75,16 +75,31 @@ const Ieee80211Channel *Ieee80211Transmitter::computeTransmissionChannel(const P void Ieee80211Transmitter::setModeSet(const Ieee80211ModeSet *modeSet) { if (this->modeSet != modeSet) { + auto newMode = mode; + if (mode != nullptr && modeSet != nullptr && !modeSet->containsMode(mode)) { + newMode = modeSet->findCompatibleMode(mode); + if (newMode == nullptr) + throw cRuntimeError("Cannot map current mode to operation mode '%s' without changing bitrate, bandwidth, spatial streams, or guard interval", modeSet->getName()); + } + else if (modeSet == nullptr) + newMode = nullptr; this->modeSet = modeSet; - if (mode != nullptr) - mode = modeSet != nullptr ? modeSet->getMode(mode->getDataMode()->getNetBitrate()) : nullptr; + mode = newMode; } } +void Ieee80211Transmitter::setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) +{ + if (modeSet != nullptr && mode != nullptr && !modeSet->containsMode(mode)) + throw cRuntimeError("Invalid mode"); + this->modeSet = modeSet; + this->mode = mode; +} + void Ieee80211Transmitter::setMode(const IIeee80211Mode *mode) { if (this->mode != mode) { - if (modeSet->findMode(mode->getDataMode()->getNetBitrate(), mode->getDataMode()->getBandwidth()) == nullptr) + if (modeSet != nullptr && mode != nullptr && !modeSet->containsMode(mode)) throw cRuntimeError("Invalid mode"); this->mode = mode; } @@ -151,9 +166,9 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran const Coord& endPosition = mobility->getCurrentPosition(); const Quaternion& startOrientation = mobility->getCurrentAngularPosition(); const Quaternion& endOrientation = mobility->getCurrentAngularPosition(); - const simtime_t preambleDuration = transmissionMode->getPreambleMode()->getDuration(); - const simtime_t headerDuration = transmissionMode->getHeaderMode()->getDuration(); - const simtime_t dataDuration = duration - headerDuration - preambleDuration; + const simtime_t preambleDuration = transmissionMode->getPreambleDuration(); + const simtime_t headerDuration = transmissionMode->getHeaderDuration(); + const simtime_t dataDuration = transmissionMode->getDataDuration(B(phyHeader->getLengthField())); auto analogModel = getAnalogModel()->createAnalogModel(preambleDuration, headerDuration, dataDuration, centerFrequency, transmissionBandwidth, transmissionPower); return new Ieee80211Transmission(transmitter, packet, startTime, endTime, preambleDuration, headerDuration, dataDuration, startPosition, endPosition, startOrientation, endOrientation, nullptr, nullptr, nullptr, nullptr, analogModel, transmissionMode, transmissionChannel); } @@ -161,4 +176,3 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran } // namespace physicallayer } // namespace inet - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h index 05b6f29194c..b013fe888b3 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h @@ -38,7 +38,11 @@ class INET_API Ieee80211Transmitter : public FlatTransmitterBase virtual const IIeee80211Mode *computeTransmissionMode(const Packet *packet) const; virtual const Ieee80211Channel *computeTransmissionChannel(const Packet *packet) const; + // Re-selects the current mode only when bitrate, bandwidth, NSS, and GI + // remain compatible. Use setModeSetAndMode for an explicit transition. virtual void setModeSet(const Ieee80211ModeSet *modeSet); + // Applies a mode set and an explicitly selected mode as one validated update. + virtual void setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode); virtual void setMode(const IIeee80211Mode *mode); virtual void setBand(const IIeee80211Band *band); virtual void setChannel(const Ieee80211Channel *channel); From e6d30c451081688aaddcaa130f92e29bf4258981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 29 Aug 2026 23:11:47 +0200 Subject: [PATCH 07/56] fix(linklayer): add guard interval configuration to rate selection and add unit tests Add dataFrameGuardInterval parameter to RateSelection and QosRateSelection NED and C++ initialization, allowing explicit guard interval qualification when fixed bitrates are configured. In computeResponseAckFrameMode and computeResponseCtsFrameMode, use modeSet->getMandatoryModeAtOrBelow(mode) for proper mandatory fallback. Add Ieee80211HtGuardInterval_1.test covering HT/VHT guard-interval catalog verification, timing calculations, lookups, transmitter mode-set switching, and rate selection mandatory fallback. --- .../mac/rateselection/QosRateSelection.cc | 14 +- .../mac/rateselection/QosRateSelection.ned | 5 +- .../mac/rateselection/RateSelection.cc | 6 +- .../mac/rateselection/RateSelection.ned | 5 +- tests/unit/Ieee80211HtGuardInterval_1.test | 369 ++++++++++++++++++ 5 files changed, 383 insertions(+), 16 deletions(-) create mode 100644 tests/unit/Ieee80211HtGuardInterval_1.test diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index add5ec700f1..e98e49df9df 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -30,7 +30,7 @@ void QosRateSelection::initialize(int stage) double multicastFrameBitrate = par("multicastFrameBitrate"); multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams")); + dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); double mgmtFrameBitrate = par("mgmtFrameBitrate"); mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); double controlFrameBitrate = par("controlFrameBitrate"); @@ -101,10 +101,8 @@ const IIeee80211Mode *QosRateSelection::computeResponseAckFrameMode(Packet *pack ASSERT(modeSet->containsMode(mode)); const IIeee80211Mode *responseMode; if (!responseAckFrameMode) { - if (modeSet->getIsMandatory(mode)) - responseMode = mode; - else if (auto slowerMode = modeSet->getSlowerMandatoryMode(mode)) - responseMode = slowerMode; + if (auto mandatoryMode = modeSet->getMandatoryModeAtOrBelow(mode)) + responseMode = mandatoryMode; else throw cRuntimeError("Mandatory mode not found"); } @@ -120,10 +118,8 @@ const IIeee80211Mode *QosRateSelection::computeResponseCtsFrameMode(Packet *pack ASSERT(modeSet->containsMode(mode)); const IIeee80211Mode *responseMode; if (!responseCtsFrameMode) { - if (modeSet->getIsMandatory(mode)) - responseMode = mode; - else if (auto slowerMode = modeSet->getSlowerMandatoryMode(mode)) - responseMode = slowerMode; + if (auto mandatoryMode = modeSet->getMandatoryModeAtOrBelow(mode)) + responseMode = mandatoryMode; else throw cRuntimeError("Mandatory mode not found"); } diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned index 35bf0e1ace5..88ec4706b60 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned @@ -32,8 +32,9 @@ simple QosRateSelection extends SimpleModule double responseCtsFrameBitrate @unit(bps) = default(-1bps); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest - double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Unspecified by default - int dataFrameNumSpatialStreams = default(-1); // Unspecified by default + double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Explicit mode qualifier when dataFrameBitrate is specified + int dataFrameNumSpatialStreams = default(-1); // Explicit mode qualifier when dataFrameBitrate is specified + double dataFrameGuardInterval @unit(s) = default(-1s); // Explicit mode qualifier when dataFrameBitrate is specified; negative means unspecified, otherwise it must equal the selected PHY mode's modeled GI // Per-receiver unicast data-frame rate. Keys are peer interface module paths (e.g. // "host1.wlan[0]"), resolved to MAC addresses at run time; values are bitrates (bps). diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 9d28a5142cd..5049ce5a1f4 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -35,7 +35,7 @@ void RateSelection::initialize(int stage) double multicastFrameBitrate = par("multicastFrameBitrate"); multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams")); + dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); double mgmtFrameBitrate = par("mgmtFrameBitrate"); mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); double controlFrameBitrate = par("controlFrameBitrate"); @@ -108,7 +108,7 @@ const IIeee80211Mode *RateSelection::computeResponseAckFrameMode(Packet *packet, else { auto mode = getMode(packet, dataOrMgmtHeader); ASSERT(modeSet->containsMode(mode)); - auto responseMode = modeSet->getIsMandatory(mode) ? mode : modeSet->getSlowerMandatoryMode(mode); // TODO BSSBasicRateSet + auto responseMode = modeSet->getMandatoryModeAtOrBelow(mode); // TODO BSSBasicRateSet return getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode); } } @@ -120,7 +120,7 @@ const IIeee80211Mode *RateSelection::computeResponseCtsFrameMode(Packet *packet, else { auto mode = getMode(packet, rtsFrame); ASSERT(modeSet->containsMode(mode)); - auto responseMode = modeSet->getIsMandatory(mode) ? mode : modeSet->getSlowerMandatoryMode(mode); // TODO BSSBasicRateSet + auto responseMode = modeSet->getMandatoryModeAtOrBelow(mode); // TODO BSSBasicRateSet return getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode); } } diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned index 89f63916421..82938a1df50 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned @@ -27,8 +27,9 @@ simple RateSelection extends SimpleModule like IRateSelection double responseCtsFrameBitrate @unit(bps) = default(-1bps); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest - double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Unspecified by default - int dataFrameNumSpatialStreams = default(-1); // Unspecified by default + double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Explicit mode qualifier when dataFrameBitrate is specified + int dataFrameNumSpatialStreams = default(-1); // Explicit mode qualifier when dataFrameBitrate is specified + double dataFrameGuardInterval @unit(s) = default(-1s); // Explicit mode qualifier when dataFrameBitrate is specified; negative means unspecified, otherwise it must equal the selected PHY mode's modeled GI // Per-receiver unicast data-frame rate. Keys are peer interface module paths (e.g. // "host1.wlan[0]"), resolved to MAC addresses at run time; values are bitrates (bps). diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test new file mode 100644 index 00000000000..cd4bbc4d2a5 --- /dev/null +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -0,0 +1,369 @@ +%description: +Validate complete IEEE 802.11 HT long/short guard-interval catalog, lookup, and airtime. + +%includes: +#include +#include +#include + +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; + +class TestIeee80211Transmitter : public Ieee80211Transmitter +{ + public: + const IIeee80211Mode *getSelectedMode() const { return mode; } + const Ieee80211ModeSet *getSelectedModeSet() const { return modeSet; } +}; + +class TestIeee80211Mgmt : public ieee80211::Ieee80211MgmtBase +{ + protected: + virtual void handleTimer(cMessage *frame) override {} + virtual void handleCommand(int msgkind, cObject *ctrl) override {} + virtual void handleAuthenticationFrame(Packet *packet, const Ptr& header) override {} + virtual void handleDeauthenticationFrame(Packet *packet, const Ptr& header) override {} + virtual void handleAssociationRequestFrame(Packet *packet, const Ptr& header) override {} + virtual void handleAssociationResponseFrame(Packet *packet, const Ptr& header) override {} + virtual void handleReassociationRequestFrame(Packet *packet, const Ptr& header) override {} + virtual void handleReassociationResponseFrame(Packet *packet, const Ptr& header) override {} + virtual void handleDisassociationFrame(Packet *packet, const Ptr& header) override {} + virtual void handleBeaconFrame(Packet *packet, const Ptr& header) override {} + virtual void handleProbeRequestFrame(Packet *packet, const Ptr& header) override {} + virtual void handleProbeResponseFrame(Packet *packet, const Ptr& header) override {} + + public: + void applyModeSet(const Ieee80211ModeSet *modeSet) + { + receiveSignal(nullptr, modesetChangedSignal, const_cast(modeSet), nullptr); + } + + const ieee80211::Ieee80211SupportedRatesElement& getSupportedRates() const { return supportedRates; } +}; + +%activity: +// Reject unsupported VHT greenfield before any mixed-format cache request. +bool rejectedVhtGreenfieldBeforeMixed = false; +try { + Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +} +catch (cRuntimeError&) { + rejectedVhtGreenfieldBeforeMixed = true; +} +ASSERT(rejectedVhtGreenfieldBeforeMixed); + +const auto modeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); +ASSERT(modeSet->getNumModes() == 135); + +TestIeee80211Mgmt mgmt; +mgmt.applyModeSet(modeSet); +const auto& htSupportedRates = mgmt.getSupportedRates(); +const double expectedHtSupportedRates[] = {1, 2, 5.5, 6, 11, 12, 24}; +ASSERT(htSupportedRates.numRates == 7); +for (int i = 0; i < htSupportedRates.numRates; i++) { + ASSERT(htSupportedRates.rate[i] == expectedHtSupportedRates[i]); + if (i > 0) + ASSERT(htSupportedRates.rate[i - 1] < htSupportedRates.rate[i]); +} + +mgmt.applyModeSet(Ieee80211ModeSet::getModeSet("a")); +const auto& legacySupportedRates = mgmt.getSupportedRates(); +const double expectedLegacySupportedRates[] = {6, 12, 24, 9, 18, 36, 48, 54}; +ASSERT(legacySupportedRates.numRates == 8); +for (int i = 0; i < legacySupportedRates.numRates; i++) { + ASSERT(legacySupportedRates.rate[i] == expectedLegacySupportedRates[i]); + ASSERT(legacySupportedRates.basicRate[i] == (i < 3)); +} + +using Key = std::tuple; +std::map modes; +int mandatoryCount = 0; +for (int index = 0; index < modeSet->getNumModes(); index++) { + auto mode = dynamic_cast(modeSet->getMode(index)); + if (mode == nullptr) + continue; + auto dataMode = mode->getDataMode(); + int bandwidth = dataMode->getBandwidth() == MHz(20) ? 20 : dataMode->getBandwidth() == MHz(40) ? 40 : 0; + int mcs = dataMode->getMcsIndex(); + auto guardIntervalType = dataMode->getGuardIntervalType(); + ASSERT(bandwidth != 0); + ASSERT((bandwidth == 20 || bandwidth == 40) && 0 <= mcs && mcs <= 31); + ASSERT(modes.emplace(Key(bandwidth, mcs, guardIntervalType), mode).second); + + bool mustBeMandatory = bandwidth == 20 && mcs <= 7 && + guardIntervalType == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG; + ASSERT(modeSet->isMandatory(index) == mustBeMandatory); + if (modeSet->isMandatory(index)) + mandatoryCount++; +} +ASSERT(mandatoryCount == 8); + +for (int index = 0; index < modeSet->getNumModes(); index++) { + auto mode = modeSet->getMode(index); + auto slowerMode = modeSet->getSlowerMode(mode); + auto fasterMode = modeSet->getFasterMode(mode); + ASSERT(slowerMode == nullptr || slowerMode->getDataMode()->getNetBitrate() < mode->getDataMode()->getNetBitrate()); + ASSERT(fasterMode == nullptr || fasterMode->getDataMode()->getNetBitrate() > mode->getDataMode()->getNetBitrate()); +} + +for (int bandwidth : {20, 40}) { + for (int mcs = 0; mcs <= 31; mcs++) { + auto longMode = modes.at(Key(bandwidth, mcs, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)); + auto shortMode = modes.at(Key(bandwidth, mcs, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); + auto longData = longMode->getDataMode(); + auto shortData = shortMode->getDataMode(); + ASSERT(longData->getGuardInterval() == SimTime(800, SIMTIME_NS)); + ASSERT(shortData->getGuardInterval() == SimTime(400, SIMTIME_NS)); + ASSERT(longData->getSymbolInterval() == SimTime(4, SIMTIME_US)); + ASSERT(shortData->getSymbolInterval() == SimTime(3600, SIMTIME_NS)); + ASSERT(std::fabs(shortData->getNetBitrate().get() * 9 - longData->getNetBitrate().get() * 10) < 1); + + // IEEE Std 802.11-2024, 19.3.11.11.6: short GI is Data-only. + auto longSignal = longMode->getHeaderMode(); + auto shortSignal = shortMode->getHeaderMode(); + ASSERT(longSignal->getSymbolInterval() == SimTime(4, SIMTIME_US)); + ASSERT(shortSignal->getSymbolInterval() == SimTime(4, SIMTIME_US)); + ASSERT(longSignal->getDuration() == SimTime(8, SIMTIME_US)); + ASSERT(shortSignal->getDuration() == SimTime(8, SIMTIME_US)); + ASSERT(longSignal->getNetBitrate() == shortSignal->getNetBitrate()); + ASSERT(longSignal->getGrossBitrate() == shortSignal->getGrossBitrate()); + } +} +ASSERT(modes.find(Key(20, 32, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)) == modes.end()); + +auto mixedLong = modes.at(Key(20, 0, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)); +auto mixedShort = modes.at(Key(20, 0, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +ASSERT(mixedLong->getDataMode()->getDuration(B(24)) == SimTime(36, SIMTIME_US)); +ASSERT(mixedShort->getDataMode()->getDuration(B(24)) == SimTime(32400, SIMTIME_NS)); +ASSERT(mixedShort->getDataMode()->getDuration(B(27)) == SimTime(36, SIMTIME_US)); +ASSERT(mixedLong->getDataMode()->getDuration(B(30)) == SimTime(44, SIMTIME_US)); +ASSERT(mixedShort->getDataMode()->getDuration(B(30)) == SimTime(39600, SIMTIME_NS)); + +// IEEE Std 802.11-2024, 19.4.3: Eq. (19-90) rounds mixed-format +// short-GI Data to 4 us, while Eq. (19-92) keeps greenfield Data raw. +ASSERT(mixedShort->getDataDuration(B(24)) == SimTime(36, SIMTIME_US)); +ASSERT(mixedShort->getDataDuration(B(27)) == SimTime(36, SIMTIME_US)); +ASSERT(mixedShort->getDataDuration(B(30)) == SimTime(40, SIMTIME_US)); + +// Preserve representative historical optional-rate timing and bitrate values. +auto mixedShortMcs8 = modes.at(Key(20, 8, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +auto mixedShortMcs0Bw40 = modes.at(Key(40, 0, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +ASSERT(std::fabs(mixedShortMcs8->getDataMode()->getNetBitrate().get() - 14.4444444444444e6) < 1); +ASSERT(mixedShortMcs8->getDataMode()->getDuration(B(24)) == SimTime(18, SIMTIME_US)); +ASSERT(mixedShortMcs8->getDataDuration(B(24)) == SimTime(20, SIMTIME_US)); +ASSERT(mixedShortMcs8->getDuration(B(24)) == SimTime(60, SIMTIME_US)); +ASSERT(mixedShortMcs0Bw40->getDataMode()->getNetBitrate() == Mbps(15)); +ASSERT(mixedShortMcs0Bw40->getDataMode()->getDuration(B(24)) == SimTime(14400, SIMTIME_NS)); +ASSERT(mixedShortMcs0Bw40->getDataDuration(B(24)) == SimTime(16, SIMTIME_US)); +ASSERT(mixedShortMcs0Bw40->getDuration(B(24)) == SimTime(52, SIMTIME_US)); +auto greenfieldShort = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs8BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +ASSERT(greenfieldShort->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD); +ASSERT(greenfieldShort->getDataMode()->getNumberOfSpatialStreams() == 2); +ASSERT(greenfieldShort->getDataDuration(B(24)) == SimTime(18, SIMTIME_US)); +ASSERT(greenfieldShort->getDataDuration(B(30)) == SimTime(21600, SIMTIME_NS)); + +auto mixedLongOneSymbol = modes.at(Key(20, 15, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)); +auto mixedShortOneSymbol = modes.at(Key(20, 15, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +auto greenfieldShortOneSymbol = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs15BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +for (auto mode : {mixedLongOneSymbol, mixedShortOneSymbol, greenfieldShortOneSymbol}) { + auto dataDuration = mode->getDataDuration(B(24)); + ASSERT(dataDuration >= SIMTIME_ZERO); + ASSERT(mode->getDuration(B(24)) == mode->getPreambleDuration() + mode->getHeaderDuration() + dataDuration); +} +ASSERT(mixedLongOneSymbol->getDataDuration(B(24)) == SimTime(4, SIMTIME_US)); +ASSERT(mixedShortOneSymbol->getDataDuration(B(24)) == SimTime(4, SIMTIME_US)); +ASSERT(greenfieldShortOneSymbol->getDataDuration(B(24)) == SimTime(3600, SIMTIME_NS)); +ASSERT(mixedShortOneSymbol->getPreambleDuration() == SimTime(20, SIMTIME_US)); +ASSERT(mixedShortOneSymbol->getHeaderDuration() == SimTime(20, SIMTIME_US)); +ASSERT(greenfieldShortOneSymbol->getPreambleDuration() == SimTime(16, SIMTIME_US)); +ASSERT(greenfieldShortOneSymbol->getHeaderDuration() == SimTime(12, SIMTIME_US)); + +auto unspecified65 = dynamic_cast(modeSet->getMode(Mbps(65), MHz(20), 1)); +auto long65 = dynamic_cast(modeSet->getMode(Mbps(65), MHz(20), 1, SimTime(800, SIMTIME_NS))); +auto short65 = dynamic_cast(modeSet->getMode(Mbps(65), MHz(20), 1, SimTime(400, SIMTIME_NS))); +ASSERT(unspecified65->getDataMode()->getMcsIndex() == 7); +ASSERT(unspecified65->getDataMode()->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +ASSERT(long65->getDataMode()->getMcsIndex() == 7); +ASSERT(short65->getDataMode()->getMcsIndex() == 6); + +TestIeee80211Transmitter transmitter; +transmitter.setModeSet(modeSet); +transmitter.setMode(short65); +ASSERT(transmitter.getSelectedMode() == short65); +bool rejectedModeOutsideSet = false; +try { + transmitter.setMode(greenfieldShortOneSymbol); +} +catch (cRuntimeError&) { + rejectedModeOutsideSet = true; +} +ASSERT(rejectedModeOutsideSet); +ASSERT(transmitter.getSelectedMode() == short65); + +auto unspecified135 = dynamic_cast(modeSet->getMode(Mbps(135), MHz(40), 1)); +ASSERT(unspecified135->getDataMode()->getMcsIndex() == 6); +ASSERT(unspecified135->getDataMode()->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +ASSERT(modeSet->findMode(Mbps(65), MHz(20), 1, SimTime(600, SIMTIME_NS)) == nullptr); +const auto legacyOfdmModeSet = Ieee80211ModeSet::getModeSet("a"); +ASSERT(legacyOfdmModeSet->findMode(Mbps(6), Hz(NaN), -1, SimTime(800, SIMTIME_NS)) != nullptr); +ASSERT(legacyOfdmModeSet->findMode(Mbps(6), Hz(NaN), -1, SimTime(400, SIMTIME_NS)) == nullptr); + +// Mode-set remapping preserves the complete modeled PHY tuple. In +// particular, a negative GI is an actual absence for compatibility lookup, +// not findMode()'s public wildcard. +const auto erpModeSet = Ieee80211ModeSet::getModeSet("g(erp)"); +const auto pModeSet = Ieee80211ModeSet::getModeSet("p"); +const auto aMode = legacyOfdmModeSet->getMode(Mbps(6), MHz(20), 1, SimTime(800, SIMTIME_NS)); +const auto erpMode = erpModeSet->findCompatibleMode(aMode); +ASSERT(erpMode != nullptr); +ASSERT(erpMode->getDataMode()->getNetBitrate() == aMode->getDataMode()->getNetBitrate()); +ASSERT(erpMode->getDataMode()->getBandwidth() == aMode->getDataMode()->getBandwidth()); +ASSERT(erpMode->getDataMode()->getGuardInterval() == aMode->getDataMode()->getGuardInterval()); + +const auto dsssModeSet = Ieee80211ModeSet::getModeSet("b"); +const auto mixedModeSet = Ieee80211ModeSet::getModeSet("g(mixed)"); +const auto dsssMode = dsssModeSet->getMode(Mbps(1)); +ASSERT(dsssMode->getDataMode()->getGuardInterval() < SIMTIME_ZERO); +ASSERT(mixedModeSet->findCompatibleMode(dsssMode) != nullptr); + +TestIeee80211Transmitter legacyTransmitter; +legacyTransmitter.setModeSet(legacyOfdmModeSet); +legacyTransmitter.setMode(aMode); +legacyTransmitter.setModeSet(erpModeSet); +ASSERT(legacyTransmitter.getSelectedModeSet() == erpModeSet); +ASSERT(legacyTransmitter.getSelectedMode() == erpMode); +const auto modeBeforeRejectedTransition = legacyTransmitter.getSelectedMode(); +bool rejectedIncompatibleModeSet = false; +try { + legacyTransmitter.setModeSet(pModeSet); +} +catch (cRuntimeError&) { + rejectedIncompatibleModeSet = true; +} +ASSERT(rejectedIncompatibleModeSet); +ASSERT(legacyTransmitter.getSelectedModeSet() == erpModeSet); +ASSERT(legacyTransmitter.getSelectedMode() == modeBeforeRejectedTransition); +const auto pMode = pModeSet->getMode(2); // the declared 6 Mbps 10 MHz mode +legacyTransmitter.setModeSetAndMode(pModeSet, pMode); +ASSERT(legacyTransmitter.getSelectedModeSet() == pModeSet); +ASSERT(legacyTransmitter.getSelectedMode() == pMode); + +Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +bool rejectedVhtGreenfieldAfterMixed = false; +try { + Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +} +catch (cRuntimeError&) { + rejectedVhtGreenfieldAfterMixed = true; +} +ASSERT(rejectedVhtGreenfieldAfterMixed); +const auto vhtModeSet = Ieee80211ModeSet::getModeSet("ac"); +const auto vhtLongMcs0 = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +const auto vhtShortMcs0 = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT); +// IEEE Std 802.11-2024, Tables 21-5/21-8 and 21.4.3, Eqs. (21-109)/(21-110). +ASSERT(vhtLongMcs0->getDataMode()->getSymbolInterval() == SimTime(4, SIMTIME_US)); +ASSERT(vhtShortMcs0->getDataMode()->getSymbolInterval() == SimTime(3600, SIMTIME_NS)); +ASSERT(vhtLongMcs0->getHeaderMode()->getSymbolInterval() == SimTime(4, SIMTIME_US)); +ASSERT(vhtShortMcs0->getHeaderMode()->getSymbolInterval() == SimTime(4, SIMTIME_US)); +ASSERT(vhtLongMcs0->getHeaderMode()->getNetBitrate() == vhtShortMcs0->getHeaderMode()->getNetBitrate()); +ASSERT(vhtLongMcs0->getHeaderMode()->getGrossBitrate() == vhtShortMcs0->getHeaderMode()->getGrossBitrate()); +ASSERT(vhtShortMcs0->getDataMode()->getDuration(B(24)) == SimTime(32400, SIMTIME_NS)); +ASSERT(vhtShortMcs0->getDataDuration(B(24)) == SimTime(36, SIMTIME_US)); +ASSERT(vhtShortMcs0->getDataMode()->getDuration(B(27)) == SimTime(36, SIMTIME_US)); +ASSERT(vhtShortMcs0->getDataDuration(B(27)) == SimTime(36, SIMTIME_US)); +ASSERT(vhtShortMcs0->getDataMode()->getDuration(B(30)) == SimTime(39600, SIMTIME_NS)); +ASSERT(vhtShortMcs0->getDataDuration(B(30)) == SimTime(40, SIMTIME_US)); +ASSERT(vhtLongMcs0->getDataMode()->getDuration(B(24)) == SimTime(36, SIMTIME_US)); +ASSERT(vhtLongMcs0->getDataDuration(B(24)) == SimTime(36, SIMTIME_US)); +for (auto vhtMode : {vhtLongMcs0, vhtShortMcs0}) + for (auto dataLength : {B(24), B(27), B(30)}) + ASSERT(vhtMode->getDuration(dataLength) == vhtMode->getPreambleDuration() + vhtMode->getHeaderDuration() + vhtMode->getDataDuration(dataLength)); +bool foundVhtLongGuardInterval = false; +bool foundVhtShortGuardInterval = false; +for (int index = 0; index < vhtModeSet->getNumModes(); index++) { + auto vhtMode = dynamic_cast(vhtModeSet->getMode(index)); + if (vhtMode == nullptr) + continue; + auto vhtDataMode = vhtMode->getDataMode(); + auto guardInterval = vhtDataMode->getGuardInterval(); + auto resolvedMode = vhtModeSet->findMode(vhtDataMode->getNetBitrate(), vhtDataMode->getBandwidth(), vhtDataMode->getNumberOfSpatialStreams(), guardInterval); + ASSERT(resolvedMode != nullptr); + ASSERT(resolvedMode->getDataMode()->getGuardInterval() == guardInterval); + foundVhtLongGuardInterval |= guardInterval == SimTime(800, SIMTIME_NS); + foundVhtShortGuardInterval |= guardInterval == SimTime(400, SIMTIME_NS); +} +ASSERT(foundVhtLongGuardInterval); +ASSERT(foundVhtShortGuardInterval); + +auto vhtOneSymbol = dynamic_cast(vhtModeSet->getFastestMode()); +ASSERT(vhtOneSymbol != nullptr); +ASSERT(vhtOneSymbol->getDataDuration(B(24)) == SimTime(4, SIMTIME_US)); +ASSERT(vhtOneSymbol->getPreambleDuration() == SimTime(20, SIMTIME_US)); +ASSERT(vhtOneSymbol->getHeaderDuration() >= SIMTIME_ZERO); +ASSERT(vhtOneSymbol->getDuration(B(24)) == vhtOneSymbol->getPreambleDuration() + vhtOneSymbol->getHeaderDuration() + vhtOneSymbol->getDataDuration(B(24))); + +for (const auto candidateSet : {modeSet, vhtModeSet}) { + for (int index = 0; index < candidateSet->getNumModes(); index++) { + auto mode = candidateSet->getMode(index); + auto slowerMode = candidateSet->getSlowerMode(mode); + auto fasterMode = candidateSet->getFasterMode(mode); + auto slowerMandatoryMode = candidateSet->getSlowerMandatoryMode(mode); + auto fasterMandatoryMode = candidateSet->getFasterMandatoryMode(mode); + ASSERT(slowerMode == nullptr || slowerMode->getDataMode()->getNetBitrate() < mode->getDataMode()->getNetBitrate()); + ASSERT(fasterMode == nullptr || fasterMode->getDataMode()->getNetBitrate() > mode->getDataMode()->getNetBitrate()); + ASSERT(slowerMandatoryMode == nullptr || slowerMandatoryMode->getDataMode()->getNetBitrate() < mode->getDataMode()->getNetBitrate()); + ASSERT(fasterMandatoryMode == nullptr || fasterMandatoryMode->getDataMode()->getNetBitrate() > mode->getDataMode()->getNetBitrate()); + } +} + +// The equal-rate mandatory candidate is selected by bitrate, not entry index. +auto equalRateVhtMode = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss2, Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +auto mandatoryAtEqualRate = vhtModeSet->getMandatoryModeAtOrBelow(equalRateVhtMode); +ASSERT(mandatoryAtEqualRate != nullptr); +ASSERT(mandatoryAtEqualRate->getDataMode()->getNetBitrate() == equalRateVhtMode->getDataMode()->getNetBitrate()); +ASSERT(vhtModeSet->getIsMandatory(mandatoryAtEqualRate)); + +transmitter.setModeSetAndMode(vhtModeSet, vhtOneSymbol); +ASSERT(transmitter.getSelectedModeSet() == vhtModeSet); +ASSERT(transmitter.getSelectedMode() == vhtOneSymbol); +bool rejectedCombinedModeUpdate = false; +try { + transmitter.setModeSetAndMode(modeSet, greenfieldShortOneSymbol); +} +catch (cRuntimeError&) { + rejectedCombinedModeUpdate = true; +} +ASSERT(rejectedCombinedModeUpdate); +ASSERT(transmitter.getSelectedModeSet() == vhtModeSet); +ASSERT(transmitter.getSelectedMode() == vhtOneSymbol); + +EV << "HT guard interval catalog, timing, and lookup checks passed.\n"; + +%contains: stdout +HT guard interval catalog, timing, and lookup checks passed. From 1328892ef7521ebd8cd093b4deafa0b1ea389ff2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 29 Aug 2026 23:50:04 +0200 Subject: [PATCH 08/56] fix(ieee80211): coordinate dynamic radio mode switches and enforce SSID 32-octet bounds - Physicallayer / MAC mode-set synchronization: - Emit `modesetChangedSignal` from `Ieee80211Radio::setModeSet` and `Ieee80211Radio::setModeSetAndMode` to propagate dynamic mode set changes across the containing NIC. - Subscribe `Ieee80211Mac` to `modesetChangedSignal` at `INITSTAGE_LINK_LAYER` and implement `receiveSignal` for `cObject *` to synchronize `modeSet` with the physical layer. - Ensures MAC rate selection, rate control adaptors, channel access contention parameters, and management listeners reflect the current physical mode catalog. - Management frame serializer SSID bounds enforcement: - Enforce IEEE Std 802.11-2024 Clause 9.4.2.2 SSID length bounds (0 to 32 octets) during both serialization and deserialization. - Encapsulate SSID wire codec logic into `writeSsidElement` and `readSsidElement` helpers across Probe Request, Association Request, Reassociation Request, Beacon, and Probe Response frames. - Reject overlength SSIDs and truncated wire streams with `cRuntimeError`. - Rate selection documentation: - Add IEEE Std 802.11-2024 Clause 10.6.5.8 reference comments in `RateSelection` and `QosRateSelection` explaining why `getHtMcsIndex() < 0` gates HT peer filtering and non-HT/VHT modes pass through unchanged until VHT MIB state is supported. - Testing: - Add test cases in `Ieee80211SupportedRates_1.test` for valid SSID bounds (0, 1, 32 octets), 33-octet serialization rejection, and truncated stream rejection. - Add test cases in `Ieee80211HtGuardInterval_1.test` verifying radio mode-set signal publication and subscriber notification. --- .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 9 +++ .../linklayer/ieee80211/mac/Ieee80211Mac.h | 1 + .../mac/rateselection/QosRateSelection.cc | 4 + .../mac/rateselection/RateSelection.cc | 4 + .../mgmt/Ieee80211MgmtFrameSerializer.cc | 50 ++++++------ .../ieee80211/packetlevel/Ieee80211Radio.cc | 5 ++ tests/unit/Ieee80211HtGuardInterval_1.test | 67 ++++++++++++++++ tests/unit/Ieee80211SupportedRates_1.test | 78 +++++++++++++++++++ 8 files changed, 192 insertions(+), 26 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index d87f581231f..a33cec0d236 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -67,6 +67,7 @@ void Ieee80211Mac::initialize(int stage) radioModule->subscribe(IRadio::receptionStateChangedSignal, this); radioModule->subscribe(IRadio::transmissionStateChangedSignal, this); radioModule->subscribe(IRadio::receivedSignalPartChangedSignal, this); + getContainingNicModule(this)->subscribe(modesetChangedSignal, this); radio = check_and_cast(radioModule); ds = check_and_cast(getSubmodule("ds")); rx = check_and_cast(getSubmodule("rx")); @@ -346,6 +347,14 @@ void Ieee80211Mac::receiveSignal(cComponent *source, simsignal_t signalID, intva } } +void Ieee80211Mac::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) +{ + Enter_Method("%s", cComponent::getSignalName(signalID)); + + if (signalID == modesetChangedSignal) + modeSet = check_and_cast(obj); +} + void Ieee80211Mac::configureRadioMode(IRadio::RadioMode radioMode) { if (radio->getRadioMode() != radioMode) { diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h index 1114deaed00..6adc6570762 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h @@ -67,6 +67,7 @@ class INET_API Ieee80211Mac : public MacProtocolBase virtual void initializeRadioMode(); virtual void receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) override; + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; using MacProtocolBase::receiveSignal; virtual void configureRadioMode(physicallayer::IRadio::RadioMode radioMode); virtual void configureNetworkInterface() override; diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index e98e49df9df..141ed3696c1 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -284,6 +284,10 @@ void QosRateSelection::frameTransmitted(Packet *packet, const PtrgetHtMcsIndex() < 0) return mode; return selectPeerCompatibleMode(modeSet, mib->findPeerHtState(peerAddress), mode, peerAddress); diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 5049ce5a1f4..30d8e881e8e 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -221,6 +221,10 @@ void RateSelection::emitDatarateSelected(cComponent *emitter, const PtrgetHtMcsIndex() < 0) return mode; return selectPeerCompatibleMode(modeSet, mib->findPeerHtState(peerAddress), mode, peerAddress); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc index 09f3ffbd05b..480c291e11a 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc @@ -33,8 +33,10 @@ Register_Serializer(Ieee80211ReassociationResponseFrame, Ieee80211MgmtFrameSeria static constexpr uint8_t HT_CAPABILITIES_ELEMENT_ID = 45; static constexpr uint8_t HT_OPERATION_ELEMENT_ID = 61; +static constexpr uint8_t SSID_ELEMENT_ID = 0; static constexpr uint8_t SUPPORTED_RATES_ELEMENT_ID = 1; static constexpr uint8_t EXTENDED_SUPPORTED_RATES_ELEMENT_ID = 50; +static constexpr uint8_t MAX_SSID_LENGTH = 32; static constexpr uint8_t MAX_SUPPORTED_RATES = 8; static constexpr uint16_t MAX_EXTENDED_SUPPORTED_RATES = 255; static constexpr double SUPPORTED_RATE_UNIT = 0.5; @@ -43,6 +45,23 @@ static constexpr uint16_t ASSOCIATION_ID_MARKER = 0xC000; static constexpr uint16_t ASSOCIATION_ID_MASK = 0x3FFF; static constexpr int MAX_LOGICAL_ASSOCIATION_ID = 2007; +static void validateSsidLength(size_t length) +{ + // IEEE Std 802.11-2024, 9.4.2.2: the SSID field contains zero to + // 32 octets. Zero octets indicates the wildcard SSID. + if (length > MAX_SSID_LENGTH) + throw cRuntimeError("Malformed SSID element length: %zu exceeds maximum %d", length, MAX_SSID_LENGTH); +} + +static void writeSsidElement(MemoryOutputStream& stream, const char *SSID) +{ + size_t length = strlen(SSID); + validateSsidLength(length); + stream.writeByte(SSID_ELEMENT_ID); + stream.writeByte(static_cast(length)); + stream.writeBytes(reinterpret_cast(SSID), B(length)); +} + static void validateSupportedRatesCount(int numRates) { // IEEE Std 802.11-2024, 9.4.2.3: the Supported Rates field contains @@ -498,11 +517,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c else if (auto probeRequestFrame = dynamicPtrCast(chunk)) { // type = ST_PROBEREQUEST; // 1 SSID - const char *SSID = probeRequestFrame->getSSID(); - unsigned int length = strlen(SSID); - stream.writeByte(0); // FIXME dummy, what is it? - stream.writeByte(length); - stream.writeBytes((uint8_t *)SSID, B(length)); + writeSsidElement(stream, probeRequestFrame->getSSID()); // 2 Supported rates writeSupportedRateElements(stream, probeRequestFrame); writeHtElements(stream, probeRequestFrame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); @@ -519,12 +534,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c // 3 Current AP address stream.writeMacAddress(reassociationRequestFrame->getCurrentAP()); // 4 SSID - const char *SSID = reassociationRequestFrame->getSSID(); - unsigned int length = strlen(SSID); - // FIXME buffer.writeByte(buf + packetLength, ???); - stream.writeByte(0); // FIXME - stream.writeByte(length); - stream.writeBytes((uint8_t *)SSID, B(length)); + writeSsidElement(stream, reassociationRequestFrame->getSSID()); // 5 Supported rates writeSupportedRateElements(stream, reassociationRequestFrame); writeHtElements(stream, reassociationRequestFrame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); @@ -542,11 +552,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c // 2 Listen interval stream.writeUint16Le(0); // FIXME // 3 SSID - const char *SSID = associationRequestFrame->getSSID(); - unsigned int length = strlen(SSID); - stream.writeByte(0); // FIXME dummy, what is it? - stream.writeByte(length); - stream.writeBytes((uint8_t *)SSID, B(length)); + writeSsidElement(stream, associationRequestFrame->getSSID()); // 4 Supported rates writeSupportedRateElements(stream, associationRequestFrame); writeHtElements(stream, associationRequestFrame, HT_CAPABILITIES_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED); @@ -597,11 +603,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c // 3 Capability stream.writeUint16Le(0); // FIXME set capability // 4 Service Set Identifier (SSID) - const char *SSID = beaconFrame->getSSID(); - unsigned int length = strlen(SSID); - stream.writeByte(0); // FIXME - stream.writeByte(length); - stream.writeBytes((uint8_t *)SSID, B(length)); + writeSsidElement(stream, beaconFrame->getSSID()); // 5 Supported rates writeSupportedRateElements(stream, beaconFrame); writeHtElements(stream, beaconFrame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED | BASIC_HT_MCS_SET_PRESENT); @@ -636,11 +638,7 @@ void Ieee80211MgmtFrameSerializer::serializeFields(MemoryOutputStream& stream, c // 3 Capability stream.writeUint16Le(0); // FIXME // 4 SSID - const char *SSID = probeResponseFrame->getSSID(); - unsigned int length = strlen(SSID); - stream.writeByte(0); // FIXME - stream.writeByte(length); - stream.writeBytes((uint8_t *)SSID, B(length)); + writeSsidElement(stream, probeResponseFrame->getSSID()); // 5 Supported rates writeSupportedRateElements(stream, probeResponseFrame); writeHtElements(stream, probeResponseFrame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED | EXTENDED_SUPPORTED_RATES_ALLOWED | BASIC_HT_MCS_SET_PRESENT); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index d95d17a929d..93582fa25af 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -9,6 +9,7 @@ #include "inet/common/packet/chunk/BitCountChunk.h" #include "inet/common/ProtocolTag_m.h" +#include "inet/common/Simsignals.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssOfdmMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" @@ -93,6 +94,8 @@ void Ieee80211Radio::setModeSet(const Ieee80211ModeSet *modeSet) EV << "Changing radio mode set to " << modeSet << endl; receptionTimer = nullptr; emit(listeningChangedSignal, 0); + if (modeSet != nullptr) + emit(modesetChangedSignal, const_cast(modeSet)); } void Ieee80211Radio::setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) @@ -106,6 +109,8 @@ void Ieee80211Radio::setModeSetAndMode(const Ieee80211ModeSet *modeSet, const II EV << "Changing radio mode set to " << modeSet << " and mode to " << mode << endl; receptionTimer = nullptr; emit(listeningChangedSignal, 0); + if (modeSet != nullptr) + emit(modesetChangedSignal, const_cast(modeSet)); } void Ieee80211Radio::setMode(const IIeee80211Mode *mode) diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index cd4bbc4d2a5..42c655a0426 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -11,6 +11,8 @@ Validate complete IEEE 802.11 HT long/short guard-interval catalog, lookup, and #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" %global: @@ -24,6 +26,37 @@ class TestIeee80211Transmitter : public Ieee80211Transmitter const Ieee80211ModeSet *getSelectedModeSet() const { return modeSet; } }; +class TestIeee80211Receiver : public Ieee80211Receiver +{ + public: + const Ieee80211ModeSet *getSelectedModeSet() const { return modeSet; } +}; + +class TestIeee80211Radio : public Ieee80211Radio +{ + public: + void setup(ITransmitter *tx, IReceiver *rx) + { + transmitter = tx; + receiver = rx; + } +}; + +class TestModeSetListener : public cListener +{ + public: + const Ieee80211ModeSet *receivedModeSet = nullptr; + int notifications = 0; + + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override + { + if (signalID == modesetChangedSignal) { + receivedModeSet = dynamic_cast(obj); + notifications++; + } + } +}; + class TestIeee80211Mgmt : public ieee80211::Ieee80211MgmtBase { protected: @@ -363,6 +396,40 @@ ASSERT(rejectedCombinedModeUpdate); ASSERT(transmitter.getSelectedModeSet() == vhtModeSet); ASSERT(transmitter.getSelectedMode() == vhtOneSymbol); +// Verify Ieee80211Radio setModeSet and setModeSetAndMode publish modesetChangedSignal +TestIeee80211Transmitter radioTx; +TestIeee80211Receiver radioRx; +TestIeee80211Radio radio; +radio.setup(&radioTx, &radioRx); + +TestModeSetListener radioListener; +radio.subscribe(modesetChangedSignal, &radioListener); + +radio.setModeSet(vhtModeSet); +ASSERT(radioTx.getSelectedModeSet() == vhtModeSet); +ASSERT(radioRx.getSelectedModeSet() == vhtModeSet); +ASSERT(radioListener.notifications == 1); +ASSERT(radioListener.receivedModeSet == vhtModeSet); + +radio.setModeSetAndMode(modeSet, modeSet->getMode(0)); +ASSERT(radioTx.getSelectedModeSet() == modeSet); +ASSERT(radioTx.getSelectedMode() == modeSet->getMode(0)); +ASSERT(radioRx.getSelectedModeSet() == modeSet); +ASSERT(radioListener.notifications == 2); +ASSERT(radioListener.receivedModeSet == modeSet); + +bool radioRejectedInvalidMode = false; +try { + radio.setModeSetAndMode(modeSet, vhtOneSymbol); +} +catch (cRuntimeError&) { + radioRejectedInvalidMode = true; +} +ASSERT(radioRejectedInvalidMode); +ASSERT(radioTx.getSelectedModeSet() == modeSet); +ASSERT(radioRx.getSelectedModeSet() == modeSet); +ASSERT(radioListener.notifications == 2); + EV << "HT guard interval catalog, timing, and lookup checks passed.\n"; %contains: stdout diff --git a/tests/unit/Ieee80211SupportedRates_1.test b/tests/unit/Ieee80211SupportedRates_1.test index a0e82cec048..e25270baab5 100644 --- a/tests/unit/Ieee80211SupportedRates_1.test +++ b/tests/unit/Ieee80211SupportedRates_1.test @@ -412,6 +412,84 @@ assertTiming(pModeSet, SimTime(32, SIMTIME_US), SimTime(13, SIMTIME_US), SimTime assertTiming(nModeSet, SimTime(10, SIMTIME_US), SimTime(20, SIMTIME_US), SimTime(24, SIMTIME_US), 15, 1023); assertTiming(acModeSet, SimTime(16, SIMTIME_US), SimTime(9, SIMTIME_US), SimTime(24, SIMTIME_US), 15, 1023); +// IEEE Std 802.11-2024, 9.4.2.2: SSID length verification (0 to 32 octets) +for (size_t len : {size_t(0), size_t(1), size_t(32)}) { + std::string ssid(len, 'x'); + auto probeReq = makeShared(); + probeReq->setSSID(ssid.c_str()); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 1.0; + rates.basicRate[0] = true; + probeReq->setSupportedRates(rates); + probeReq->setChunkLength(B(2 + len + 3)); + + auto bytes = serializeFrame(probeReq); + auto decoded = deserializeFrame(bytes); + ASSERT(std::string(decoded->getSSID()) == ssid); +} + +// 33-octet SSID exceeds standard maximum and must be rejected on serialization +{ + std::string overlengthSsid(33, 'x'); + auto probeReq = makeShared(); + probeReq->setSSID(overlengthSsid.c_str()); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 1.0; + rates.basicRate[0] = true; + probeReq->setSupportedRates(rates); + probeReq->setChunkLength(B(2 + 33 + 3)); + + bool overlengthRejected = false; + try { + serializeFrame(probeReq); + } + catch (const cRuntimeError&) { + overlengthRejected = true; + } + ASSERT(overlengthRejected); +} + +// Deserializing a frame with element length = 33 must throw +{ + std::vector rawProbeReq = { + 0x00, 33 // Element 0 (SSID), length 33 + }; + for (int i = 0; i < 33; i++) + rawProbeReq.push_back('x'); + rawProbeReq.push_back(0x01); // Element 1 (Supported Rates) + rawProbeReq.push_back(1); // Length 1 + rawProbeReq.push_back(0x82); // 1.0 Mbps basic + + bool deserializationRejected = false; + try { + deserializeFrame(rawProbeReq); + } + catch (const cRuntimeError&) { + deserializationRejected = true; + } + ASSERT(deserializationRejected); +} + +// Deserializing a truncated SSID element must throw +{ + std::vector truncatedProbeReq = { + 0x00, 10 // Element 0 (SSID), length 10 but only 3 payload bytes follow + }; + for (int i = 0; i < 3; i++) + truncatedProbeReq.push_back('a'); + + bool truncatedRejected = false; + try { + deserializeFrame(truncatedProbeReq); + } + catch (const cRuntimeError&) { + truncatedRejected = true; + } + ASSERT(truncatedRejected); +} + EV << "Supported Rates and AID validation checks passed.\n"; %contains: stdout From 17ac6a14534014c6de48baf83a64f0480f4095fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 00:09:29 +0200 Subject: [PATCH 09/56] fix(linklayer): rebuild configured fixed modes on dynamic radio mode-set changes - Rate selection dynamic mode-set synchronization: - Factor out fixed mode lookup and mandatory rate computation into updateModes() in RateSelection and QosRateSelection. - Rebuild all configured fixed mode pointers (multicastFrameMode, dataFrameMode, mgmtFrameMode, controlFrameMode, responseAckFrameMode, responseCtsFrameMode, and responseBlockAckFrameMode) from their NED parameters when modesetChangedSignal arrives, preserving data mode bandwidth, spatial-stream, and guard-interval qualifiers. - Refresh fastestMandatoryMode from the new mode set and clear lastTransmittedFrameMode peer rate history to prevent stale mode pointer reuse across mode-set transitions. - Invoke updateModes() on both INITSTAGE_LINK_LAYER initialization and modesetChangedSignal reception. - Testing: - Add unit test cases in Ieee80211HtGuardInterval_1.test verifying that dynamic switching between mode sets (g(erp) and n(mixed-2.4Ghz)) updates all configured mode pointers and fastest mandatory modes in RateSelection and QosRateSelection to reference the active catalog. --- .../mac/rateselection/QosRateSelection.cc | 43 ++++--- .../mac/rateselection/QosRateSelection.h | 1 + .../mac/rateselection/RateSelection.cc | 40 +++--- .../mac/rateselection/RateSelection.h | 1 + tests/unit/Ieee80211HtGuardInterval_1.test | 115 ++++++++++++++++++ 5 files changed, 169 insertions(+), 31 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 141ed3696c1..602d4acda25 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -27,20 +27,7 @@ void QosRateSelection::initialize(int stage) mib.reference(this, "mibModule", true); if (stage == INITSTAGE_LINK_LAYER) { dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); - double multicastFrameBitrate = par("multicastFrameBitrate"); - multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); - double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); - double mgmtFrameBitrate = par("mgmtFrameBitrate"); - mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); - double controlFrameBitrate = par("controlFrameBitrate"); - controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); - double responseAckFrameBitrate = par("responseAckFrameBitrate"); - responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); - double responseBlockAckFrameBitrate = par("responseBlockAckFrameBitrate"); - responseBlockAckFrameMode = (responseBlockAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate)); - double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); + updateModes(); } } @@ -56,7 +43,7 @@ void QosRateSelection::ensurePerReceiverModesResolved() throw cRuntimeError("dataFrameBitratePerReceiver: cannot resolve receiver interface module path '%s'", path.c_str()); auto networkInterface = check_and_cast(module); try { - auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams")); + auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); perReceiverDataFrameMode[networkInterface->getMacAddress()] = mode; } catch (const cRuntimeError& e) { @@ -65,6 +52,30 @@ void QosRateSelection::ensurePerReceiverModesResolved() } } +void QosRateSelection::updateModes() +{ + if (modeSet == nullptr) + return; + double multicastFrameBitrate = par("multicastFrameBitrate"); + multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); + double dataFrameBitrate = par("dataFrameBitrate"); + dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); + double mgmtFrameBitrate = par("mgmtFrameBitrate"); + mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); + double controlFrameBitrate = par("controlFrameBitrate"); + controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); + double responseAckFrameBitrate = par("responseAckFrameBitrate"); + responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + double responseBlockAckFrameBitrate = par("responseBlockAckFrameBitrate"); + responseBlockAckFrameMode = (responseBlockAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate)); + double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); + responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); + fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + lastTransmittedFrameMode.clear(); + perReceiverDataFrameMode.clear(); + perReceiverResolved = false; +} + const IIeee80211Mode *QosRateSelection::getMode(Packet *packet, const Ptr& header) { const auto& modeReqTag = packet->findTag(); @@ -272,7 +283,7 @@ void QosRateSelection::receiveSignal(cComponent *source, simsignal_t signalID, c if (signalID == modesetChangedSignal) { modeSet = check_and_cast(obj); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + updateModes(); } } diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 569ef742d37..6d503834797 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -59,6 +59,7 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; + virtual void updateModes(); // Builds perReceiverDataFrameMode on first use. Deferred out of initialize() because peer // MAC addresses are assigned during INITSTAGE_LINK_LAYER with undefined intra-stage module diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 30d8e881e8e..8a437bde89f 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -32,19 +32,7 @@ void RateSelection::initialize(int stage) } else if (stage == INITSTAGE_LINK_LAYER) { dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); - double multicastFrameBitrate = par("multicastFrameBitrate"); - multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); - double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); - double mgmtFrameBitrate = par("mgmtFrameBitrate"); - mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); - double controlFrameBitrate = par("controlFrameBitrate"); - controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); - double responseAckFrameBitrate = par("responseAckFrameBitrate"); - responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); - double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + updateModes(); // WATCH(dataOrMgmtRateControl); // WATCH(*((cObject**)&fastestMandatoryMode)); @@ -75,7 +63,7 @@ void RateSelection::ensurePerReceiverModesResolved() throw cRuntimeError("dataFrameBitratePerReceiver: cannot resolve receiver interface module path '%s'", path.c_str()); auto networkInterface = check_and_cast(module); try { - auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams")); + auto mode = modeSet->getMode(bps(value.doubleValueInUnit("bps")), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); perReceiverDataFrameMode[networkInterface->getMacAddress()] = mode; } catch (const cRuntimeError& e) { @@ -84,6 +72,28 @@ void RateSelection::ensurePerReceiverModesResolved() } } +void RateSelection::updateModes() +{ + if (modeSet == nullptr) + return; + double multicastFrameBitrate = par("multicastFrameBitrate"); + multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); + double dataFrameBitrate = par("dataFrameBitrate"); + dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); + double mgmtFrameBitrate = par("mgmtFrameBitrate"); + mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); + double controlFrameBitrate = par("controlFrameBitrate"); + controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); + double responseAckFrameBitrate = par("responseAckFrameBitrate"); + responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); + responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); + fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + lastTransmittedFrameMode.clear(); + perReceiverDataFrameMode.clear(); + perReceiverResolved = false; +} + const IIeee80211Mode *RateSelection::getMode(Packet *packet, const Ptr& header) { const auto& modeReqTag = packet->findTag(); @@ -190,7 +200,7 @@ void RateSelection::receiveSignal(cComponent *source, simsignal_t signalID, cObj if (signalID == modesetChangedSignal) { modeSet = check_and_cast(obj); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + updateModes(); } } diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h index 69441331453..7909f9f22e6 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h @@ -59,6 +59,7 @@ class INET_API RateSelection : public IRateSelection, public SimpleModule, publi virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; + virtual void updateModes(); // Builds perReceiverDataFrameMode on first use. Deferred out of initialize() because peer // MAC addresses are assigned during INITSTAGE_LINK_LAYER with undefined intra-stage module diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 42c655a0426..3fb4ee8a5bc 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -7,6 +7,8 @@ Validate complete IEEE 802.11 HT long/short guard-interval catalog, lookup, and #include #include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" @@ -57,6 +59,34 @@ class TestModeSetListener : public cListener } }; +class TestRateSelectionAccessor : public ieee80211::RateSelection +{ + public: + static const IIeee80211Mode *getDataFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->dataFrameMode; } + static const IIeee80211Mode *getMulticastFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->multicastFrameMode; } + static const IIeee80211Mode *getMgmtFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->mgmtFrameMode; } + static const IIeee80211Mode *getControlFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->controlFrameMode; } + static const IIeee80211Mode *getResponseAckFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->responseAckFrameMode; } + static const IIeee80211Mode *getResponseCtsFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->responseCtsFrameMode; } + static const IIeee80211Mode *getFastestMandatoryMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->fastestMandatoryMode; } + static const Ieee80211ModeSet *getModeSet(const ieee80211::RateSelection *rs) { return static_cast(rs)->modeSet; } + static void applyModeSet(ieee80211::RateSelection *rs, const Ieee80211ModeSet *ms) { + static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); + } +}; + +class TestQosRateSelectionAccessor : public ieee80211::QosRateSelection +{ + public: + static const IIeee80211Mode *getDataFrameMode(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->dataFrameMode; } + static const IIeee80211Mode *getResponseBlockAckFrameMode(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->responseBlockAckFrameMode; } + static const IIeee80211Mode *getFastestMandatoryMode(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->fastestMandatoryMode; } + static const Ieee80211ModeSet *getModeSet(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->modeSet; } + static void applyModeSet(ieee80211::QosRateSelection *rs, const Ieee80211ModeSet *ms) { + static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); + } +}; + class TestIeee80211Mgmt : public ieee80211::Ieee80211MgmtBase { protected: @@ -430,6 +460,91 @@ ASSERT(radioTx.getSelectedModeSet() == modeSet); ASSERT(radioRx.getSelectedModeSet() == modeSet); ASSERT(radioListener.notifications == 2); +// Verify RateSelection and QosRateSelection dynamic mode-set updates +auto *rateSelection = check_and_cast(cModuleType::get("inet.linklayer.ieee80211.mac.rateselection.RateSelection")->create("rateSelection", this)); +rateSelection->par("rateControlModule").setStringValue(""); +rateSelection->par("mibModule").setStringValue(""); +rateSelection->par("dataFrameBitrate").setDoubleValue(54e6); +rateSelection->par("multicastFrameBitrate").setDoubleValue(6e6); +rateSelection->par("mgmtFrameBitrate").setDoubleValue(6e6); +rateSelection->par("controlFrameBitrate").setDoubleValue(6e6); +rateSelection->par("responseAckFrameBitrate").setDoubleValue(6e6); +rateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); +rateSelection->finalizeParameters(); +rateSelection->buildInside(); + +TestRateSelectionAccessor::applyModeSet(rateSelection, erpModeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == erpModeSet); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getControlFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getResponseAckFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection))); +ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection))); +ASSERT(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection) == erpModeSet->getMode(Mbps(24))); + +// Dynamically switch rateSelection to HT mode set +TestRateSelectionAccessor::applyModeSet(rateSelection, modeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == modeSet); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); +ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); +ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getControlFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseAckFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection))); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection))); + +// Also verify HT-specific fixed mode configured with bandwidth, stream, and guard interval +auto *rateSelectionHt = check_and_cast(cModuleType::get("inet.linklayer.ieee80211.mac.rateselection.RateSelection")->create("rateSelectionHt", this)); +rateSelectionHt->par("rateControlModule").setStringValue(""); +rateSelectionHt->par("mibModule").setStringValue(""); +rateSelectionHt->par("dataFrameBitrate").setDoubleValue(65e6); +rateSelectionHt->par("dataFrameBandwidth").setDoubleValue(20e6); +rateSelectionHt->par("dataFrameNumSpatialStreams").setIntValue(1); +rateSelectionHt->par("dataFrameGuardInterval").setDoubleValue(400e-9); +rateSelectionHt->par("multicastFrameBitrate").setDoubleValue(6e6); +rateSelectionHt->par("mgmtFrameBitrate").setDoubleValue(6e6); +rateSelectionHt->par("controlFrameBitrate").setDoubleValue(6e6); +rateSelectionHt->par("responseAckFrameBitrate").setDoubleValue(6e6); +rateSelectionHt->par("responseCtsFrameBitrate").setDoubleValue(6e6); +rateSelectionHt->finalizeParameters(); +rateSelectionHt->buildInside(); + +TestRateSelectionAccessor::applyModeSet(rateSelectionHt, modeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelectionHt) == modeSet); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt))); +ASSERT(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt) == short65); + +auto *qosRateSelection = check_and_cast(cModuleType::get("inet.linklayer.ieee80211.mac.rateselection.QosRateSelection")->create("qosRateSelection", this)); +qosRateSelection->par("rateControlModule").setStringValue(""); +qosRateSelection->par("mibModule").setStringValue(""); +qosRateSelection->par("dataFrameBitrate").setDoubleValue(54e6); +qosRateSelection->par("multicastFrameBitrate").setDoubleValue(6e6); +qosRateSelection->par("mgmtFrameBitrate").setDoubleValue(6e6); +qosRateSelection->par("controlFrameBitrate").setDoubleValue(6e6); +qosRateSelection->par("responseAckFrameBitrate").setDoubleValue(6e6); +qosRateSelection->par("responseBlockAckFrameBitrate").setDoubleValue(6e6); +qosRateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); +qosRateSelection->finalizeParameters(); +qosRateSelection->buildInside(); + +TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, erpModeSet); +ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == erpModeSet); +ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); +ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); +ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getFastestMandatoryMode(qosRateSelection))); + +TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, modeSet); +ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == modeSet); +ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); +ASSERT(!erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); +ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); +ASSERT(!erpModeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); +ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getFastestMandatoryMode(qosRateSelection))); + EV << "HT guard interval catalog, timing, and lookup checks passed.\n"; %contains: stdout From abad1d063b6ccedfdffa86e64f821db2ab8ddd34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 12:46:47 +0200 Subject: [PATCH 10/56] ieee80211: use legacy basic rates for group-addressed frames Mixed HT catalogs place mandatory HT MCS entries above legacy rates, so the fastest mandatory mode makes Beacons invisible to legacy stations. Constrain both rate selectors to mandatory legacy operational modes when the advertised basic legacy set is nonempty, preserving eligible configured rates. Add passive-discovery coverage for legacy stations associating with mixed HT DCF and HCF access points. --- .../Ieee80211PeerModeSelection.cc | 16 ++ .../Ieee80211PeerModeSelection.h | 3 + .../mac/rateselection/QosRateSelection.cc | 51 ++----- .../mac/rateselection/RateSelection.cc | 18 +-- tests/module/Ieee80211MixedHtDiscovery_1.test | 141 ++++++++++++++++++ 5 files changed, 179 insertions(+), 50 deletions(-) create mode 100644 tests/module/Ieee80211MixedHtDiscovery_1.test diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 6408b443687..048699e5980 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -14,6 +14,22 @@ namespace ieee80211 { using namespace inet::physicallayer; +const IIeee80211Mode *selectGroupAddressedMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *requestedMode) +{ + // IEEE Std 802.11-2024, 10.6.5.1 and 10.6.5.4. The model advertises + // mandatory legacy operational modes as its BSS basic legacy rate set. + const IIeee80211Mode *legacyMode = nullptr; + for (const auto *candidate : modeSet->getLegacyOperationalModes()) { + if (!modeSet->getIsMandatory(candidate)) + continue; + if (candidate == requestedMode) + return candidate; + if (legacyMode == nullptr || candidate->getDataMode()->getNetBitrate() > legacyMode->getDataMode()->getNetBitrate()) + legacyMode = candidate; + } + return legacyMode != nullptr ? legacyMode : requestedMode; +} + namespace { static const IIeee80211Mode *getLegacyFallback(const Ieee80211ModeSet *modeSet, diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h index ba5eaebf764..e89cf445dd8 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h @@ -14,6 +14,9 @@ namespace inet { namespace ieee80211 { +INET_API const physicallayer::IIeee80211Mode *selectGroupAddressedMode( + const physicallayer::Ieee80211ModeSet *modeSet, const physicallayer::IIeee80211Mode *requestedMode); + /** * Selects a mode that is compatible with the negotiated receive capabilities * of a peer. Non-HT modes are returned unchanged. A null peer state denotes diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 602d4acda25..7803a558bcc 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -157,9 +157,15 @@ const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) { + if (dataOrMgmtHeader->getReceiverAddress().isMulticast()) { + const auto *requestedMode = multicastFrameMode; + if (requestedMode == nullptr) + requestedMode = dynamicPtrCast(dataOrMgmtHeader) ? dataFrameMode : mgmtFrameMode; + return selectGroupAddressedMode(modeSet, requestedMode != nullptr ? requestedMode : fastestMandatoryMode); + } // Per-receiver override for originated unicast data frames (see dataFrameBitratePerReceiver). // Wins over the interface-wide dataFrameMode / rate control; group-addressed and management - // frames are left to the existing rules below. + // frames were handled above. if (dynamicPtrCast(dataOrMgmtHeader) && !dataOrMgmtHeader->getReceiverAddress().isMulticast()) { ensurePerReceiverModesResolved(); auto it = perReceiverDataFrameMode.find(dataOrMgmtHeader->getReceiverAddress()); @@ -170,46 +176,9 @@ const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const PtrgetReceiverAddress(), dataFrameMode); if (dynamicPtrCast(dataOrMgmtHeader) && mgmtFrameMode) return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), mgmtFrameMode); - // This subclause describes the rate selection rules for group addressed data and management frames, excluding - // the following: - // — Non-STBC Beacon and non-STBC PSMP frames - // — STBC group addressed data and management frames - // — Data frames located in an FMS stream (see 10.23.7) - if (dataOrMgmtHeader->getReceiverAddress().isMulticast()) { - // If the BSSBasicRateSet parameter is not empty, a data or management frame (excluding the frames listed - // above) with a group address in the Address 1 field shall be transmitted in a non-HT PPDU using one of the - // rates included in the BSSBasicRateSet parameter or the rate chosen by the AP, described in 10.23.7, if the data - // frames are part of an FMS stream. - // TODO BSSBasicRateSet - // If the BSSBasicRateSet parameter is empty and the BSSBasicMCSSet parameter is not empty, the frame shall - // be transmitted in an HT PPDU using one of the MCSs included in the BSSBasicMCSSet parameter. - - // If both the BSSBasicRateSet parameter and the BSSBasicMCSSet parameter are empty (e.g., a scanning STA - // that is not yet associated with a BSS), the frame shall be transmitted in a non-HT PPDU using one of the - // mandatory PHY rates. - // The rate control is not consulted for these frames. It adapts to the feedback of one - // peer, and a group-addressed frame has no peer: it is never acknowledged, so nothing - // would ever correct a rate chosen for it. - return fastestMandatoryMode; - } - // A data or management frame not identified in 9.7.5.1 through 9.7.5.5 shall be sent using any data rate or MCS - // subject to the following constraints: - // — A STA shall not transmit a frame using a rate or MCS that is not supported by the receiver STA or - // STAs, as reported in any Supported Rates element, Extended Supported Rates element, or - // Supported MCS field in management frames transmitted by the receiver STA. - // — A STA shall not transmit a frame using a value for the CH_BANDWIDTH parameter of the - // TXVECTOR that is not supported by the receiver STA. - // — A STA shall not initiate transmission of a frame at a data rate higher than the greatest rate in the - // OperationalRateSet or the HTOperationalMCSset, which are parameters of the MLME- - // JOIN.request primitive. - else { - // TODO Supported Rates element, Extended Supported Rates element - // TODO OperationalRateSet or the HTOperationalMCSset - if (dataOrMgmtRateControl) - return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), dataOrMgmtRateControl->getRate(dataOrMgmtHeader->getReceiverAddress())); - else - return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), fastestMandatoryMode); - } + if (dataOrMgmtRateControl) + return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), dataOrMgmtRateControl->getRate(dataOrMgmtHeader->getReceiverAddress())); + return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), fastestMandatoryMode); } const IIeee80211Mode *QosRateSelection::computeControlFrameMode(const Ptr& header, TxopProcedure *txopProcedure) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 8a437bde89f..fe6e062ec7a 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -150,28 +150,28 @@ const IIeee80211Mode *RateSelection::computeResponseCtsFrameMode(Packet *packet, // const IIeee80211Mode *RateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) { + if (dataOrMgmtHeader->getReceiverAddress().isMulticast()) { + const auto *requestedMode = multicastFrameMode; + if (requestedMode == nullptr) + requestedMode = dynamicPtrCast(dataOrMgmtHeader) ? dataFrameMode : mgmtFrameMode; + return selectGroupAddressedMode(modeSet, requestedMode != nullptr ? requestedMode : fastestMandatoryMode); + } // Per-receiver override for originated unicast data frames (see dataFrameBitratePerReceiver). // Wins over the interface-wide dataFrameMode / rate control; group-addressed and management - // frames are left to the existing rules below. + // frames were handled above. if (dynamicPtrCast(dataOrMgmtHeader) && !dataOrMgmtHeader->getReceiverAddress().isMulticast()) { ensurePerReceiverModesResolved(); auto it = perReceiverDataFrameMode.find(dataOrMgmtHeader->getReceiverAddress()); if (it != perReceiverDataFrameMode.end()) return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), it->second); } - if (dataOrMgmtHeader->getReceiverAddress().isMulticast() && multicastFrameMode) - return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), multicastFrameMode); if (dynamicPtrCast(dataOrMgmtHeader) && dataFrameMode) return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), dataFrameMode); if (dynamicPtrCast(dataOrMgmtHeader) && mgmtFrameMode) return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), mgmtFrameMode); - // Rate control adapts to the feedback of one peer, and a group-addressed frame has no peer: - // it is never acknowledged, so nothing would ever correct a rate chosen for it. Group-addressed - // frames therefore take a mandatory rate, as the clause above requires. - if (dataOrMgmtRateControl && !dataOrMgmtHeader->getReceiverAddress().isMulticast()) + if (dataOrMgmtRateControl) return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), dataOrMgmtRateControl->getRate(dataOrMgmtHeader->getReceiverAddress())); - else - return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), fastestMandatoryMode); + return getPeerCompatibleMode(dataOrMgmtHeader->getReceiverAddress(), fastestMandatoryMode); } // 802.11-1999 Std. diff --git a/tests/module/Ieee80211MixedHtDiscovery_1.test b/tests/module/Ieee80211MixedHtDiscovery_1.test new file mode 100644 index 00000000000..ae7c47c571b --- /dev/null +++ b/tests/module/Ieee80211MixedHtDiscovery_1.test @@ -0,0 +1,141 @@ +%description: +Two mixed HT APs (DCF and HCF) advertise legacy basic-rate Beacons over the +radio medium. Legacy STAs discover them by passive scanning and associate. +The observation includes selected transmit modes, actual Beacon reception, +and terminal association state, so an HT Beacon cannot pass this test. + +%file: Ieee80211MixedHtDiscovery.cc +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class DiscoveryMac : public Ieee80211Mac +{ + public: + int beaconsSent = 0; + int beaconsReceived = 0; + protected: + virtual void sendDownFrame(Packet *packet) override + { + const auto& header = packet->peekAtFront(); + if (header->getType() == ST_BEACON) { + auto mode = packet->getTag()->getMode(); + ASSERT(mode->getHtMcsIndex() < 0); + ASSERT(modeSet->containsMode(mode)); + ASSERT(modeSet->getIsMandatory(mode)); + beaconsSent++; + } + Ieee80211Mac::sendDownFrame(packet); + } + virtual void handleLowerPacket(Packet *packet) override + { + const auto& header = packet->peekAtFront(); + if (header->getType() == ST_BEACON) { + ASSERT(packet->getTag()->getMode()->getHtMcsIndex() < 0); + beaconsReceived++; + } + Ieee80211Mac::handleLowerPacket(packet); + } +}; +Define_Module(DiscoveryMac); + +class Ieee80211MixedHtDiscoveryTest : public cSimpleModule +{ + protected: + virtual void finish() override + { + for (int i = 0; i < 2; i++) { + auto apNic = getParentModule()->getSubmodule("ap", i)->getSubmodule("wlan", 0); + auto staNic = getParentModule()->getSubmodule("sta", i)->getSubmodule("wlan", 0); + auto ap = check_and_cast(apNic->getSubmodule("mib")); + auto sta = check_and_cast(staNic->getSubmodule("mib")); + ASSERT(ap->isHtOperationSupported()); + ASSERT(!sta->isHtOperationSupported()); + ASSERT(sta->bssStationData.isAssociated); + ASSERT(sta->bssData.bssid == ap->address); + ASSERT(ap->bssAccessPointData.stations.at(sta->address) == Ieee80211Mib::ASSOCIATED); + ASSERT(ap->findPeerHtState(sta->address) == nullptr); + ASSERT(sta->findPeerHtState(ap->address) == nullptr); + ASSERT(check_and_cast(apNic->getSubmodule("mac"))->beaconsSent > 0); + ASSERT(check_and_cast(staNic->getSubmodule("mac"))->beaconsReceived > 0); + } + std::cout << "Legacy passive discovery and association with mixed HT DCF and HCF APs verified.\n"; + } +}; +Define_Module(Ieee80211MixedHtDiscoveryTest); + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.Ieee80211Mac; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +module DiscoveryMac extends Ieee80211Mac +{ + parameters: + @class(::DiscoveryMac); +} + +simple Ieee80211MixedHtDiscoveryTest extends SimpleModule +{ + parameters: + @class(::Ieee80211MixedHtDiscoveryTest); +} + +network DiscoveryNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap[2]: AccessPoint; + sta[2]: WirelessHost; + test: Ieee80211MixedHtDiscoveryTest; +} + +%inifile: omnetpp.ini +[General] +network = DiscoveryNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 100ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +*.ap[*].wlan[*].opMode = "n(mixed-2.4Ghz)" +*.sta[*].wlan[*].opMode = "g(mixed)" +**.wlan[*].bitrate = -1bps +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.channelNumber = 6 +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].radio.receiver.sensitivity = -85dBm +**.wlan[*].radio.receiver.snirThreshold = 4dB +**.wlan[*].mac.typename = "DiscoveryMac" +*.ap[0].wlan[0].mac.qosStation = false +*.ap[1].wlan[0].mac.qosStation = true +*.ap[*].wlan[0].mgmt.typename = "Ieee80211MgmtAp" +*.ap[0].wlan[0].mgmt.ssid = "mixed-dcf" +*.ap[1].wlan[0].mgmt.ssid = "mixed-hcf" +*.ap[*].wlan[0].mgmt.beaconInterval = 5ms +*.sta[*].wlan[0].mgmt.typename = "Ieee80211MgmtSta" +*.sta[*].wlan[0].mgmt.numChannels = 1 +*.sta[*].wlan[0].agent.typename = "Ieee80211AgentSta" +*.sta[*].wlan[0].agent.activeScan = false +*.sta[0].wlan[0].agent.startingTime = 0s +*.sta[1].wlan[0].agent.startingTime = 1ms +*.sta[0].wlan[0].agent.defaultSsid = "mixed-dcf" +*.sta[1].wlan[0].agent.defaultSsid = "mixed-hcf" +*.sta[*].wlan[0].agent.channelsToScan = "6" +*.sta[*].wlan[0].agent.minChannelTime = 10ms +*.sta[*].wlan[0].agent.maxChannelTime = 10ms +*.sta[*].wlan[0].agent.authenticationTimeout = 20ms +*.sta[*].wlan[0].agent.associationTimeout = 20ms + +%contains: stdout +Legacy passive discovery and association with mixed HT DCF and HCF APs verified. From 6a08467315d38615cff63df61bde61fe42f12cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 12:46:47 +0200 Subject: [PATCH 11/56] ieee80211: make radio mode-set transitions transactional A failed mode-set listener could leave the radio and an arbitrary prefix of consumers on a new catalog. Coordinate dependent updates through a typed transaction interface and restore their snapshots if an update or synchronous notification fails. Reuse the MAC's antenna- and channel-width-limited HT capability derivation during transitions, completing MIB and peer updates before notifying observers. Preserve transmitter-owned compatible-mode mapping and reject reentrant catalog changes. Cover HT-to-legacy and legacy-to-HT transitions, Beacon advertisements, both selectors, fixed-rate rejection through both radio setters, and observer-failure rollback. Rename existing unit-test helpers to avoid collisions with the transaction API. --- .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 55 +++- .../linklayer/ieee80211/mac/Ieee80211Mac.h | 7 +- .../ieee80211/mac/channelaccess/Dcaf.cc | 23 +- .../ieee80211/mac/channelaccess/Dcaf.h | 5 +- .../ieee80211/mac/channelaccess/Edcaf.cc | 23 +- .../ieee80211/mac/channelaccess/Edcaf.h | 4 + .../ieee80211/mac/common/ModeSetListener.cc | 20 +- .../ieee80211/mac/common/ModeSetListener.h | 8 +- .../mac/ratecontrol/AarfRateControl.cc | 10 + .../mac/ratecontrol/AarfRateControl.h | 1 + .../mac/ratecontrol/OnoeRateControl.cc | 10 + .../mac/ratecontrol/OnoeRateControl.h | 1 + .../mac/ratecontrol/RateControlBase.cc | 14 +- .../mac/ratecontrol/RateControlBase.h | 1 + .../mac/rateselection/QosRateSelection.cc | 28 +- .../mac/rateselection/QosRateSelection.h | 5 +- .../mac/rateselection/RateSelection.cc | 28 +- .../mac/rateselection/RateSelection.h | 8 +- .../ieee80211/mgmt/Ieee80211MgmtBase.cc | 66 ++-- .../ieee80211/mgmt/Ieee80211MgmtBase.h | 8 +- .../linklayer/ieee80211/mib/Ieee80211Mib.cc | 10 +- .../linklayer/ieee80211/mib/Ieee80211Mib.h | 3 + .../packetlevel/IIeee80211ModeSetListener.h | 39 +++ .../ieee80211/packetlevel/Ieee80211Radio.cc | 79 ++++- .../ieee80211/packetlevel/Ieee80211Radio.h | 6 + .../ieee80211/packetlevel/Ieee80211Receiver.h | 1 + .../packetlevel/Ieee80211Transmitter.h | 2 + .../module/Ieee80211ModeSetTransition_1.test | 287 ++++++++++++++++++ tests/unit/Ieee80211HtGuardInterval_1.test | 34 +-- 29 files changed, 682 insertions(+), 104 deletions(-) create mode 100644 src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h create mode 100644 tests/module/Ieee80211ModeSetTransition_1.test diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index a33cec0d236..c5b7502c9a0 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include + #include #include "inet/common/INETUtils.h" @@ -72,20 +74,7 @@ void Ieee80211Mac::initialize(int stage) ds = check_and_cast(getSubmodule("ds")); rx = check_and_cast(getSubmodule("rx")); tx = check_and_cast(getSubmodule("tx")); - int operationalHtSpatialStreamLimit = std::min(radio->getAntenna()->getNumAntennas(), - modeSet->getMaximumNumberOfSpatialStreams()); - std::set operationalChannelWidths; - if (modeSet->isHtOperationSupported()) { - const auto *transmitter = dynamic_cast(radio->getTransmitter()); - const auto *receiver = dynamic_cast(radio->getReceiver()); - if (transmitter == nullptr || receiver == nullptr) - throw cRuntimeError("HT operation requires Ieee80211Transmitter and Ieee80211Receiver"); - for (auto channelWidth : modeSet->getHtSupportedChannelWidths()) - if (transmitter->isHtChannelWidthSupported(channelWidth) && - receiver->isHtChannelWidthSupported(channelWidth)) - operationalChannelWidths.insert(channelWidth); - } - mib->updateLocalHtCapabilities(modeSet, operationalChannelWidths, operationalHtSpatialStreamLimit); + updateLocalHtCapabilities(); emit(modesetChangedSignal, modeSet); if (isUp()) initializeRadioMode(); @@ -98,6 +87,24 @@ void Ieee80211Mac::initialize(int stage) } } +void Ieee80211Mac::updateLocalHtCapabilities() +{ + int operationalHtSpatialStreamLimit = std::min(radio->getAntenna()->getNumAntennas(), + modeSet->getMaximumNumberOfSpatialStreams()); + std::set operationalChannelWidths; + if (modeSet->isHtOperationSupported()) { + const auto *transmitter = dynamic_cast(radio->getTransmitter()); + const auto *receiver = dynamic_cast(radio->getReceiver()); + if (transmitter == nullptr || receiver == nullptr) + throw cRuntimeError("HT operation requires Ieee80211Transmitter and Ieee80211Receiver"); + for (auto channelWidth : modeSet->getHtSupportedChannelWidths()) + if (transmitter->isHtChannelWidthSupported(channelWidth) && + receiver->isHtChannelWidthSupported(channelWidth)) + operationalChannelWidths.insert(channelWidth); + } + mib->updateLocalHtCapabilities(modeSet, operationalChannelWidths, operationalHtSpatialStreamLimit); +} + void Ieee80211Mac::initializeRadioMode() { const char *initialRadioMode = par("initialRadioMode"); @@ -350,9 +357,25 @@ void Ieee80211Mac::receiveSignal(cComponent *source, simsignal_t signalID, intva void Ieee80211Mac::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signalID)); + if (signalID == modesetChangedSignal && obj != modeSet) + applyModeSet(check_and_cast(obj)); +} - if (signalID == modesetChangedSignal) - modeSet = check_and_cast(obj); +void Ieee80211Mac::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + updateLocalHtCapabilities(); +} + +std::function Ieee80211Mac::saveModeSetState() +{ + Enter_Method_Silent(); + auto restoreMib = mib->saveHtState(); + return [this, state = std::make_tuple(modeSet), restoreMib]() mutable { + std::tie(modeSet) = std::move(state); + restoreMib(); + }; } void Ieee80211Mac::configureRadioMode(IRadio::RadioMode radioMode) diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h index 6adc6570762..3fb5c2ec704 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.h @@ -21,6 +21,7 @@ #include "inet/linklayer/ieee80211/mac/coordinationfunction/Pcf.h" #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/IRadio.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" namespace inet { namespace ieee80211 { @@ -35,10 +36,13 @@ 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 physicallayer::IIeee80211ModeSetListener { public: static simsignal_t frameTransmissionOutcomeSignal; + virtual const physicallayer::Ieee80211ModeSet *getModeSet() const override { return modeSet; } + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; protected: FcsMode fcsMode; @@ -65,6 +69,7 @@ class INET_API Ieee80211Mac : public MacProtocolBase virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int) override; virtual void initializeRadioMode(); + void updateLocalHtCapabilities(); virtual void receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc index 1f6ea0615cb..ec065a96cb6 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h" +#include + #include "inet/common/ModuleAccess.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/contract/IRx.h" @@ -125,13 +127,26 @@ void Dcaf::expectedChannelAccess(simtime_t time) void Dcaf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signalID)); + if (signalID == modesetChangedSignal && obj != modeSet) + applyModeSet(check_and_cast(obj)); +} - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - calculateTimingParameters(); - } +void Dcaf::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + calculateTimingParameters(); } +std::function Dcaf::saveModeSetState() +{ + Enter_Method_Silent(); + return [this, state = std::make_tuple(modeSet, slotTime, sifs, ifs, eifs, cw, cwMin, cwMax)]() mutable { + std::tie(modeSet, slotTime, sifs, ifs, eifs, cw, cwMin, cwMax) = std::move(state); + }; +} + + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h index 2fab0d312c8..eb15a2dbb7d 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h @@ -19,8 +19,11 @@ namespace ieee80211 { class INET_API Dcaf : public IChannelAccess, public IContention::ICallback, public IRecoveryProcedure::ICwCalculator, public ModeSetListener { + public: + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: - physicallayer::Ieee80211ModeSet *modeSet = nullptr; IContention *contention = nullptr; IChannelAccess::ICallback *callback = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc index e220635a876..0d8dff39627 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h" +#include + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/networklayer/common/NetworkInterface.h" @@ -189,13 +191,26 @@ int Edcaf::getCwMin(AccessCategory ac, int aCwMin) void Edcaf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signalID)); + if (signalID == modesetChangedSignal && obj != modeSet) + applyModeSet(check_and_cast(obj)); +} - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - calculateTimingParameters(); - } +void Edcaf::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + calculateTimingParameters(); } +std::function Edcaf::saveModeSetState() +{ + Enter_Method_Silent(); + return [this, state = std::make_tuple(modeSet, slotTime, sifs, ifs, eifs, cw, cwMin, cwMax)]() mutable { + std::tie(modeSet, slotTime, sifs, ifs, eifs, cw, cwMin, cwMax) = std::move(state); + }; +} + + } // namespace ieee80211 } // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h index e737cad9162..8e25569caa0 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h @@ -30,6 +30,10 @@ namespace ieee80211 { */ class INET_API Edcaf : public IChannelAccess, public IContention::ICallback, public IRecoveryProcedure::ICwCalculator, public ModeSetListener { + public: + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: IContention *contention = nullptr; IChannelAccess::ICallback *callback = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc index 15dd3838eb5..f84574d5447 100644 --- a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" +#include + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/networklayer/common/NetworkInterface.h" @@ -23,11 +25,25 @@ void ModeSetListener::initialize(int stage) void ModeSetListener::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signalID)); + if (signalID == modesetChangedSignal && obj != modeSet) + applyModeSet(check_and_cast(obj)); +} - if (signalID == modesetChangedSignal) - modeSet = check_and_cast(obj); +void ModeSetListener::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); } +std::function ModeSetListener::saveModeSetState() +{ + Enter_Method_Silent(); + return [this, state = std::make_tuple(modeSet)]() mutable { + std::tie(modeSet) = std::move(state); + }; +} + + } /* namespace ieee80211 */ } /* namespace inet */ diff --git a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h index 05581613050..0a2ecc63001 100644 --- a/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h +++ b/src/inet/linklayer/ieee80211/mac/common/ModeSetListener.h @@ -10,12 +10,18 @@ #include "inet/common/SimpleModule.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" namespace inet { namespace ieee80211 { -class INET_API ModeSetListener : public SimpleModule, public cListener +class INET_API ModeSetListener : public SimpleModule, public cListener, public physicallayer::IIeee80211ModeSetListener { + public: + virtual const physicallayer::Ieee80211ModeSet *getModeSet() const override { return modeSet; } + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: physicallayer::Ieee80211ModeSet *modeSet = nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc index 0b2e47b79f7..f43245df0b0 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.cc @@ -14,6 +14,16 @@ using namespace inet::physicallayer; Define_Module(AarfRateControl); +std::function AarfRateControl::saveModeSetState() +{ + Enter_Method_Silent(); + auto restoreBase = ModeSetListener::saveModeSetState(); + return [this, savedStations = stations, restoreBase]() mutable { + stations.swap(savedStations); + restoreBase(); + }; +} + void AarfRateControl::initialize(int stage) { RateControlBase::initialize(stage); diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h index f9ddd7f5222..1fa323b11f5 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/AarfRateControl.h @@ -50,6 +50,7 @@ class INET_API AarfRateControl : public RateControlBase virtual void increaseRateIfTimerIsExpired(State& state); public: + virtual std::function saveModeSetState() override; virtual const physicallayer::IIeee80211Mode *getRate(const MacAddress& receiverAddress) override; virtual void frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) override; virtual void frameReceived(Packet *frame) override; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc index a49f35d0c30..be2acddff39 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.cc @@ -14,6 +14,16 @@ using namespace inet::physicallayer; Define_Module(OnoeRateControl); +std::function OnoeRateControl::saveModeSetState() +{ + Enter_Method_Silent(); + auto restoreBase = ModeSetListener::saveModeSetState(); + return [this, savedStations = stations, restoreBase]() mutable { + stations.swap(savedStations); + restoreBase(); + }; +} + void OnoeRateControl::initialize(int stage) { RateControlBase::initialize(stage); diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h index ecee1d849e4..5995abb8c61 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/OnoeRateControl.h @@ -48,6 +48,7 @@ class INET_API OnoeRateControl : public RateControlBase virtual void computeModeIfTimerIsExpired(State& state); public: + virtual std::function saveModeSetState() override; virtual const physicallayer::IIeee80211Mode *getRate(const MacAddress& receiverAddress) override; virtual void frameTransmitted(Packet *frame, int retryCount, bool isSuccessful, bool isGivenUp) override; virtual void frameReceived(Packet *frame) override; diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc index 551a31c4c9e..e91291f31ed 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.cc @@ -64,10 +64,16 @@ void RateControlBase::receiveSignal(cComponent *source, simsignal_t signalID, cO { Enter_Method("%s", cComponent::getSignalName(signalID)); - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - resetRateControl(); - } + if (signalID == modesetChangedSignal && obj != modeSet) + applyModeSet(check_and_cast(obj)); +} + +void RateControlBase::applyModeSet(const Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + getInitialMode(); // Validate fixed initial rates even before the first peer is used. + resetRateControl(); } } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h index 2a61b5028fa..9ceb1e18d54 100644 --- a/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h +++ b/src/inet/linklayer/ieee80211/mac/ratecontrol/RateControlBase.h @@ -18,6 +18,7 @@ namespace ieee80211 { class INET_API RateControlBase : public ModeSetListener, public IRateControl { public: + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; static simsignal_t datarateChangedSignal; protected: diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 7803a558bcc..7aa9b3aaec3 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h" @@ -249,11 +251,29 @@ const IIeee80211Mode *QosRateSelection::computeMode(Packet *packet, const Ptr(obj)); +} - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - updateModes(); - } +void QosRateSelection::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + updateModes(); + if (getSimulation()->getContextType() != CTX_INITIALIZE) + ensurePerReceiverModesResolved(); +} + +std::function QosRateSelection::saveModeSetState() +{ + Enter_Method_Silent(); + return [this, state = std::make_tuple(modeSet, fastestMandatoryMode, multicastFrameMode, dataFrameMode, mgmtFrameMode, + controlFrameMode, responseAckFrameMode, responseCtsFrameMode, responseBlockAckFrameMode, + lastTransmittedFrameMode, perReceiverDataFrameMode, perReceiverResolved)]() mutable { + std::tie(modeSet, fastestMandatoryMode, multicastFrameMode, dataFrameMode, mgmtFrameMode, + controlFrameMode, responseAckFrameMode, responseCtsFrameMode, responseBlockAckFrameMode, + lastTransmittedFrameMode, perReceiverDataFrameMode, perReceiverResolved) = std::move(state); + }; } void QosRateSelection::frameTransmitted(Packet *packet, const Ptr& header) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 6d503834797..53cb75b94e1 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -32,11 +32,14 @@ namespace ieee80211 { */ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListener { + public: + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: IRateControl *dataOrMgmtRateControl = nullptr; ModuleRefByPar mib; - const physicallayer::Ieee80211ModeSet *modeSet = nullptr; std::map lastTransmittedFrameMode; // originator frame modes diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index fe6e062ec7a..e2ef635a78e 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/contract/IRateControl.h" @@ -197,11 +199,29 @@ const IIeee80211Mode *RateSelection::computeMode(Packet *packet, const Ptr(obj)); +} - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - updateModes(); - } +void RateSelection::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + updateModes(); + if (getSimulation()->getContextType() != CTX_INITIALIZE) + ensurePerReceiverModesResolved(); +} + +std::function RateSelection::saveModeSetState() +{ + Enter_Method_Silent(); + return [this, state = std::make_tuple(modeSet, fastestMandatoryMode, multicastFrameMode, dataFrameMode, mgmtFrameMode, + controlFrameMode, responseAckFrameMode, responseCtsFrameMode, + lastTransmittedFrameMode, perReceiverDataFrameMode, perReceiverResolved)]() mutable { + std::tie(modeSet, fastestMandatoryMode, multicastFrameMode, dataFrameMode, mgmtFrameMode, + controlFrameMode, responseAckFrameMode, responseCtsFrameMode, + lastTransmittedFrameMode, perReceiverDataFrameMode, perReceiverResolved) = std::move(state); + }; } void RateSelection::frameTransmitted(Packet *packet, const Ptr& header) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h index 7909f9f22e6..32294eae93c 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h @@ -14,6 +14,7 @@ #include "inet/linklayer/ieee80211/mac/contract/IRateSelection.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" namespace inet { namespace ieee80211 { @@ -31,8 +32,13 @@ namespace ieee80211 { * 9.7.6.4 Rate selection for control frames that are not control response frames * 9.7.6.5 Rate selection for control response frames */ -class INET_API RateSelection : public IRateSelection, public SimpleModule, public cListener // FIXME +class INET_API RateSelection : public IRateSelection, public SimpleModule, public cListener, public physicallayer::IIeee80211ModeSetListener // FIXME { + public: + virtual const physicallayer::Ieee80211ModeSet *getModeSet() const override { return modeSet; } + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: IRateControl *dataOrMgmtRateControl = nullptr; ModuleRefByPar mib; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc index 3c3bc33903a..acf01610c92 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h" +#include + #include "inet/common/INETUtils.h" #include "inet/common/ModuleAccess.h" #include "inet/common/ProtocolTag_m.h" @@ -43,35 +45,47 @@ void Ieee80211MgmtBase::initialize(int stage) void Ieee80211MgmtBase::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signalID)); + if (signalID == modesetChangedSignal && obj != modeSet) + applyModeSet(check_and_cast(obj)); +} - if (signalID == modesetChangedSignal) { - modeSet = check_and_cast(obj); - supportedRates = Ieee80211SupportedRatesElement(); - extendedSupportedRates = Ieee80211ExtendedSupportedRatesElement(); - int rateIndex = 0; - int extendedRateIndex = 0; - // Supported Rates carries the legacy OperationalRateSet only. HT/VHT - // MCS support is advertised through the corresponding capabilities - // elements (IEEE Std 802.11-2024, 9.4.2.3, 9.4.2.54.4, 11.1.4.6). - for (const auto *mode : modeSet->getLegacyOperationalModes()) { - bool isBasicRate = modeSet->getIsMandatory(mode); - double rate = mode->getDataMode()->getNetBitrate().get(); - if (rateIndex < 8) { - supportedRates.rate[rateIndex] = rate; - supportedRates.basicRate[rateIndex] = isBasicRate; - rateIndex++; - } - else if (extendedRateIndex < 255) { - extendedSupportedRates.rate[extendedRateIndex] = rate; - extendedSupportedRates.basicRate[extendedRateIndex] = isBasicRate; - extendedRateIndex++; - } - else - throw cRuntimeError("Mode set '%s' contains more than 263 legacy operational rates", modeSet->getName()); +void Ieee80211MgmtBase::applyModeSet(const physicallayer::Ieee80211ModeSet *newModeSet) +{ + Enter_Method_Silent(); + modeSet = const_cast(newModeSet); + supportedRates = Ieee80211SupportedRatesElement(); + extendedSupportedRates = Ieee80211ExtendedSupportedRatesElement(); + int rateIndex = 0; + int extendedRateIndex = 0; + // Supported Rates carries the legacy OperationalRateSet only. HT/VHT + // MCS support is advertised through the corresponding capabilities + // elements (IEEE Std 802.11-2024, 9.4.2.3, 9.4.2.54.4, 11.1.4.6). + for (const auto *mode : modeSet->getLegacyOperationalModes()) { + bool isBasicRate = modeSet->getIsMandatory(mode); + double rate = mode->getDataMode()->getNetBitrate().get(); + if (rateIndex < 8) { + supportedRates.rate[rateIndex] = rate; + supportedRates.basicRate[rateIndex] = isBasicRate; + rateIndex++; + } + else if (extendedRateIndex < 255) { + extendedSupportedRates.rate[extendedRateIndex] = rate; + extendedSupportedRates.basicRate[extendedRateIndex] = isBasicRate; + extendedRateIndex++; } - supportedRates.numRates = rateIndex; - extendedSupportedRates.numRates = extendedRateIndex; + else + throw cRuntimeError("Mode set '%s' contains more than 263 legacy operational rates", modeSet->getName()); } + supportedRates.numRates = rateIndex; + extendedSupportedRates.numRates = extendedRateIndex; +} + +std::function Ieee80211MgmtBase::saveModeSetState() +{ + Enter_Method_Silent(); + return [this, state = std::make_tuple(modeSet, supportedRates, extendedSupportedRates)]() mutable { + std::tie(modeSet, supportedRates, extendedSupportedRates) = std::move(state); + }; } void Ieee80211MgmtBase::addHtCapabilities(const Ptr& frame) const diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h index 145da89e902..79c41175a8a 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h @@ -19,6 +19,7 @@ #include "inet/networklayer/contract/IInterfaceTable.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" namespace inet { @@ -28,8 +29,13 @@ namespace ieee80211 { * Abstract base class for 802.11 infrastructure mode management components. * */ -class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener +class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener, public physicallayer::IIeee80211ModeSetListener { + public: + virtual const physicallayer::Ieee80211ModeSet *getModeSet() const override { return modeSet; } + virtual std::function saveModeSetState() override; + virtual void applyModeSet(const physicallayer::Ieee80211ModeSet *modeSet) override; + protected: // configuration ModuleRefByPar mib; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index e5c8fd008a3..0d28b31a9c8 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -7,8 +7,9 @@ #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" -#include +#include +#include #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211Band.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" @@ -112,6 +113,13 @@ const Ieee80211HtOperation& Ieee80211Mib::getHtOperation() const return htOperation; } +std::function Ieee80211Mib::saveHtState() +{ + return [this, state = std::make_tuple(localHtCapabilitiesValid, localHtCapabilities, htOperation, configuredSecondaryChannelOffset, primaryChannelAvailable, peerHtStates)]() mutable { + std::tie(localHtCapabilitiesValid, localHtCapabilities, htOperation, configuredSecondaryChannelOffset, primaryChannelAvailable, peerHtStates) = std::move(state); + }; +} + void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeSet *modeSet, const std::set& operationalChannelWidths, int operationalHtSpatialStreamLimit) { diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h index 974bdd41158..5de6dea51d6 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h @@ -8,6 +8,8 @@ #ifndef __INET_IEEE80211MIB_H #define __INET_IEEE80211MIB_H +#include + #include "inet/common/SimpleModule.h" #include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h" @@ -100,6 +102,7 @@ class INET_API Ieee80211Mib : public SimpleModule short allocateAssociationId(const MacAddress& address); void releaseAssociationId(const MacAddress& address); void clearAssociationIds(); + std::function saveHtState(); void updateLocalHtCapabilities(const physicallayer::Ieee80211ModeSet *modeSet, const std::set& operationalChannelWidths, int operationalHtSpatialStreamLimit); bool isHtOperationSupported() const { return localHtCapabilitiesValid; } diff --git a/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h new file mode 100644 index 00000000000..d714adf87aa --- /dev/null +++ b/src/inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h @@ -0,0 +1,39 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IIEEE80211MODESETLISTENER_H +#define __INET_IIEEE80211MODESETLISTENER_H + +#include + +#include "inet/common/INETDefs.h" + +namespace inet { +namespace physicallayer { + +class Ieee80211ModeSet; + +/** + * Transaction participant for a radio's mode-set changes. Stateful subscribers + * to modesetChanged must implement this role to support recoverable changes. + * The radio captures every participant before applying any change, applies all + * participants before publishing the signal, and restores them if a call fails. + * Snapshot callbacks are one-shot, must not throw or emit signals, and restore + * all state affected by applyModeSet. Applying a change must not notify observers. + */ +class INET_API IIeee80211ModeSetListener +{ + public: + virtual ~IIeee80211ModeSetListener() = default; + virtual const Ieee80211ModeSet *getModeSet() const = 0; + virtual std::function saveModeSetState() = 0; + virtual void applyModeSet(const Ieee80211ModeSet *modeSet) = 0; +}; + +} // namespace physicallayer +} // namespace inet + +#endif diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 93582fa25af..b0abedb5211 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -7,6 +7,10 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include + +#include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" + #include "inet/common/packet/chunk/BitCountChunk.h" #include "inet/common/ProtocolTag_m.h" #include "inet/common/Simsignals.h" @@ -87,30 +91,73 @@ void Ieee80211Radio::handleUpperCommand(cMessage *message) void Ieee80211Radio::setModeSet(const Ieee80211ModeSet *modeSet) { - Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); - Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); - ieee80211Transmitter->setModeSet(modeSet); - ieee80211Receiver->setModeSet(modeSet); - EV << "Changing radio mode set to " << modeSet << endl; - receptionTimer = nullptr; - emit(listeningChangedSignal, 0); - if (modeSet != nullptr) - emit(modesetChangedSignal, const_cast(modeSet)); + Enter_Method("setModeSet"); + changeModeSet(modeSet, nullptr, false); } void Ieee80211Radio::setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) { + Enter_Method("setModeSetAndMode"); + changeModeSet(modeSet, mode, true); +} + +void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode, bool explicitMode) +{ + if (changingModeSet) + throw cRuntimeError("Reentrant radio mode-set change"); if (modeSet != nullptr && mode != nullptr && !modeSet->containsMode(mode)) throw cRuntimeError("Invalid mode"); - Ieee80211Transmitter *ieee80211Transmitter = const_cast(check_and_cast(transmitter)); - Ieee80211Receiver *ieee80211Receiver = const_cast(check_and_cast(receiver)); - ieee80211Transmitter->setModeSetAndMode(modeSet, mode); - ieee80211Receiver->setModeSet(modeSet); - EV << "Changing radio mode set to " << modeSet << " and mode to " << mode << endl; + auto transmitter = const_cast(check_and_cast(this->transmitter)); + auto receiver = const_cast(check_and_cast(this->receiver)); + const auto *oldTransmitterModeSet = transmitter->getModeSet(); + const auto *oldReceiverModeSet = receiver->getModeSet(); + const auto *oldMode = transmitter->getMode(); + auto oldReceptionTimer = receptionTimer; + + // Discover the same subscribers that receive the hierarchical notification, + // deduplicating participants subscribed at more than one level. Capture all + // state first: a failing participant may have partially changed itself. + std::vector participants; + std::vector> restore; + for (cComponent *component = this; component != nullptr; component = component->getParentModule()) { + for (auto listener : component->getLocalSignalListeners(modesetChangedSignal)) { + auto participant = dynamic_cast(listener); + if (participant != nullptr && std::find(participants.begin(), participants.end(), participant) == participants.end()) + participants.push_back(participant); + } + } + if (modeSet == nullptr && !participants.empty()) + throw cRuntimeError("Cannot clear the radio mode set while MAC mode-set consumers are attached"); + for (auto participant : participants) + restore.push_back(participant->saveModeSetState()); + + changingModeSet = true; + try { + if (explicitMode) + transmitter->setModeSetAndMode(modeSet, mode); + else + transmitter->setModeSet(modeSet); + receiver->setModeSet(modeSet); + for (auto participant : participants) + participant->applyModeSet(modeSet); + // Every built-in dependent, including the MIB, is now current before + // any observer runs. Built-in signal adapters skip this applied set. + if (modeSet != nullptr) + emit(modesetChangedSignal, const_cast(modeSet)); + } + catch (...) { + transmitter->setModeSetAndMode(oldTransmitterModeSet, oldMode); + receiver->setModeSet(oldReceiverModeSet); + for (auto it = restore.rbegin(); it != restore.rend(); ++it) + (*it)(); + receptionTimer = oldReceptionTimer; + changingModeSet = false; + throw; + } + changingModeSet = false; + EV << "Changing radio mode set to " << modeSet << " and mode to " << transmitter->getMode() << endl; receptionTimer = nullptr; emit(listeningChangedSignal, 0); - if (modeSet != nullptr) - emit(modesetChangedSignal, const_cast(modeSet)); } void Ieee80211Radio::setMode(const IIeee80211Mode *mode) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h index 874a7890319..841c6ac2e63 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h @@ -30,11 +30,14 @@ class INET_API Ieee80211Radio : public FlatRadioBase static const Ptr peekIeee80211PhyHeaderAtFront(const Packet *packet, b length = b(-1), int flags = 0); protected: + bool changingModeSet = false; FcsMode fcsMode = FCS_MODE_UNDEFINED; protected: virtual void initialize(int stage) override; + void changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode, bool explicitMode); + virtual void handleUpperCommand(cMessage *message) override; virtual void insertFcs(const Ptr& phyHeader) const; @@ -46,6 +49,9 @@ class INET_API Ieee80211Radio : public FlatRadioBase public: Ieee80211Radio(); + // These setters snapshot transactional mode-set consumers before applying + // the catalog, and restore them if an update or synchronous notification + // throws. Stateful modesetChanged subscribers implement IIeee80211ModeSetListener. virtual void setModeSet(const Ieee80211ModeSet *modeSet); virtual void setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode); virtual void setMode(const IIeee80211Mode *mode); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h index 450e8b36c45..49de67c6980 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.h @@ -37,6 +37,7 @@ class INET_API Ieee80211Receiver : public FlatReceiverBase virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override; + const Ieee80211ModeSet *getModeSet() const { return modeSet; } virtual void setModeSet(const Ieee80211ModeSet *modeSet); virtual void setBand(const IIeee80211Band *band); virtual void setChannel(const Ieee80211Channel *channel); diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h index b013fe888b3..3ac8ebcea24 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h @@ -40,6 +40,8 @@ class INET_API Ieee80211Transmitter : public FlatTransmitterBase // Re-selects the current mode only when bitrate, bandwidth, NSS, and GI // remain compatible. Use setModeSetAndMode for an explicit transition. + const Ieee80211ModeSet *getModeSet() const { return modeSet; } + const IIeee80211Mode *getMode() const { return mode; } virtual void setModeSet(const Ieee80211ModeSet *modeSet); // Applies a mode set and an explicitly selected mode as one validated update. virtual void setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode); diff --git a/tests/module/Ieee80211ModeSetTransition_1.test b/tests/module/Ieee80211ModeSetTransition_1.test new file mode 100644 index 00000000000..6ec9a84d8da --- /dev/null +++ b/tests/module/Ieee80211ModeSetTransition_1.test @@ -0,0 +1,287 @@ +%description: +Exercise both radio mode-set setters with DCF and HCF selectors. Successful +HT/legacy transitions refresh PHY-limited capabilities and actual Beacon bodies. +Fixed-rate and observer failures restore all subscribed catalogs, caches, peer +negotiations, and channel-access windows. No signal replay is used for rollback. + +%file: Ieee80211ModeSetTransition.cc + +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h" +#include "inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +class TransitionMgmtAp : public Ieee80211MgmtAp +{ + bool probing = false; + public: + void checkAdvertisement() + { + Enter_Method_Silent(); + probing = true; + sendBeacon(); + probing = false; + } + protected: + virtual void sendDown(Packet *packet) override + { + if (!probing) { + Ieee80211MgmtAp::sendDown(packet); + return; + } + const auto& body = packet->peekAtFront(); + ASSERT(body->getHtCapabilitiesPresent() == modeSet->isHtOperationSupported()); + ASSERT(body->getHtOperationPresent() == modeSet->isHtOperationSupported()); + const auto& rates = body->getSupportedRates(); + const auto& extended = body->getExtendedSupportedRates(); + const auto& modes = modeSet->getLegacyOperationalModes(); + ASSERT(rates.numRates + extended.numRates == (int)modes.size()); + for (int i = 0; i < (int)modes.size(); i++) { + double rate = i < rates.numRates ? rates.rate[i] : extended.rate[i - rates.numRates]; + bool basic = i < rates.numRates ? rates.basicRate[i] : extended.basicRate[i - rates.numRates]; + ASSERT(rate == modes[i]->getDataMode()->getNetBitrate().get()); + ASSERT(basic == modeSet->getIsMandatory(modes[i])); + } + if (modeSet->isHtOperationSupported()) { + for (int i = 0; i < 77; i++) + ASSERT(body->getHtCapabilities().rxMcsSupported[i] == (i < 8)); + ASSERT(!body->getHtCapabilities().supportedChannelWidth40Mhz); + ASSERT(!body->getHtCapabilities().shortGi40); + } + delete packet; + } +}; +Define_Module(TransitionMgmtAp); + +class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener +{ + bool rejectNotification = false; + int notifications = 0; + MacAddress peer = MacAddress("02:00:00:00:01:ff"); + + std::vector participants(cModule *radio) + { + std::vector result; + for (cComponent *component = radio; component; component = component->getParentModule()) + for (auto listener : component->getLocalSignalListeners(modesetChangedSignal)) + if (auto participant = dynamic_cast(listener)) + result.push_back(participant); + return result; + } + + void check(cModule *nic, const Ieee80211ModeSet *expected, bool peerExpected) + { + auto radio = check_and_cast(nic->getSubmodule("radio")); + ASSERT(check_and_cast(radio->getTransmitter())->getModeSet() == expected); + ASSERT(check_and_cast(radio->getReceiver())->getModeSet() == expected); + for (auto participant : participants(radio)) + ASSERT(participant->getModeSet() == expected); + auto mib = check_and_cast(nic->getSubmodule("mib")); + ASSERT(mib->isHtOperationSupported() == expected->isHtOperationSupported()); + ASSERT(mib->requirePrimaryChannel() == 6); + ASSERT((mib->findPeerHtState(peer) != nullptr) == peerExpected); + for (int i = 0; i < 77; i++) { + ASSERT(mib->localHtCapabilities.rxMcsSupported[i] == (expected->isHtOperationSupported() && i < 8)); + ASSERT(mib->getHtOperation().basicMcsSupported[i] == (expected->isHtOperationSupported() && i < 8)); + } + if (peerExpected) { + auto state = mib->findPeerHtState(peer); + ASSERT(state->negotiatedCapabilities.localTxPeerRx.valid); + for (int i = 8; i < 77; i++) + ASSERT(!state->negotiatedCapabilities.localTxPeerRx.supportedMcs[i]); + } + check_and_cast(nic->getSubmodule("mgmt"))->checkAdvertisement(); + auto data = makeShared(); + auto beacon = makeShared(); + beacon->setType(ST_BEACON); + for (auto address : {MacAddress::BROADCAST_ADDRESS, MacAddress("01:00:5e:00:00:01"), peer}) { + data->setReceiverAddress(address); + beacon->setReceiverAddress(address); + Packet packet("selection probe"); + auto mac = nic->getSubmodule("mac"); + auto dcf = check_and_cast(mac->getSubmodule("dcf")->getSubmodule("rateSelection")); + auto qos = check_and_cast(mac->getSubmodule("hcf")->getSubmodule("rateSelection")); + for (const auto& header : {Ptr(data), Ptr(beacon)}) { + auto nonQosMode = dcf->computeMode(&packet, header); + auto qosMode = qos->computeMode(&packet, header, nullptr); + ASSERT(expected->containsMode(nonQosMode)); + ASSERT(expected->containsMode(qosMode)); + if (address.isMulticast()) { + ASSERT(nonQosMode->getHtMcsIndex() < 0); + ASSERT(qosMode->getHtMcsIndex() < 0); + ASSERT(expected->getIsMandatory(nonQosMode)); + ASSERT(expected->getIsMandatory(qosMode)); + } + } + } + } + + protected: + virtual void initialize() override { scheduleAt(SimTime(1, SIMTIME_US), new cMessage("switch")); } + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override + { + Enter_Method_Silent(); + notifications++; + auto radio = check_and_cast(source); + auto target = check_and_cast(value); + // Publication must follow *all* internal updates, irrespective of where + // this observer appears in the subscriber list. + auto nic = radio->getParentModule(); + for (auto participant : participants(radio)) + ASSERT(participant->getModeSet() == target); + ASSERT(check_and_cast(nic->getSubmodule("mib"))->isHtOperationSupported() == target->isHtOperationSupported()); + check_and_cast(nic->getSubmodule("mgmt"))->checkAdvertisement(); + if (rejectNotification) + throw cRuntimeError("test observer rejects mode set"); + } + + virtual void handleMessage(cMessage *message) override + { + delete message; + for (int index : {1, 2, 0}) { + auto nic = getParentModule()->getSubmodule("ap", index)->getSubmodule("wlan", 0); + auto radio = check_and_cast(nic->getSubmodule("radio")); + auto mib = check_and_cast(nic->getSubmodule("mib")); + auto ht = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); + auto legacy = Ieee80211ModeSet::getModeSet("g(mixed)"); + radio->setMode(ht->getMode(Mbps(24))); + radio->subscribe(modesetChangedSignal, this); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + check(nic, ht, true); + auto oldGeneration = mib->findPeerHtState(peer)->generation; + auto tx = check_and_cast(radio->getTransmitter()); + auto oldMode = tx->getMode(); + auto dcf = nic->getSubmodule("mac")->getSubmodule("dcf"); + auto hcf = nic->getSubmodule("mac")->getSubmodule("hcf"); + auto channelAccess = check_and_cast(dcf->getSubmodule("channelAccess")); + auto edcaf = check_and_cast(hcf->getSubmodule("edca")->getSubmodule("edcaf", 0)); + channelAccess->incrementCw(); + edcaf->incrementCw(); + int oldCw = channelAccess->getCw(); + int oldQosCw = edcaf->getCw(); + // Populate both adaptive histories before any failed transition. + auto arf = check_and_cast(dcf->getSubmodule("rateControl")); + auto onoe = check_and_cast(hcf->getSubmodule("rateControl")); + auto oldArf = arf->getRate(peer); + auto oldOnoe = onoe->getRate(peer); + if (index != 0) { + for (bool explicitMode : {false, true}) { + bool rejected = false; + try { + if (explicitMode) + radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); + else + radio->setModeSet(legacy); + } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); + ASSERT(notifications == 0); + check(nic, ht, true); + ASSERT(tx->getMode() == oldMode); + ASSERT(mib->findPeerHtState(peer)->generation == oldGeneration); + ASSERT(channelAccess->getCw() == oldCw); + ASSERT(edcaf->getCw() == oldQosCw); + ASSERT(arf->getRate(peer) == oldArf); + ASSERT(onoe->getRate(peer) == oldOnoe); + } + radio->unsubscribe(modesetChangedSignal, this); + continue; + } + // Reject after all internal updates have succeeded, to cover arbitrary + // notification-prefix failure in addition to fixed-rate lookup failure. + rejectNotification = true; + bool rejected = false; + try { radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); + ASSERT(notifications == 1); + check(nic, ht, true); + ASSERT(mib->findPeerHtState(peer)->generation == oldGeneration); + ASSERT(channelAccess->getCw() == oldCw); + ASSERT(edcaf->getCw() == oldQosCw); + rejectNotification = false; + radio->setModeSet(legacy); + check(nic, legacy, false); + radio->setModeSetAndMode(ht, ht->getMode(Mbps(65), MHz(20), 1, 800e-9)); + check(nic, ht, false); + mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); + check(nic, ht, true); + ASSERT(notifications == 3); + radio->unsubscribe(modesetChangedSignal, this); + } + std::cout << "Mode-set switching, advertisements, peer state, both selectors, and rollback verified.\n"; + endSimulation(); + } +}; +Define_Module(Ieee80211ModeSetTransitionTest); + +%file: test.ned +import inet.common.SimpleModule; +import inet.node.wireless.AccessPoint; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtAp; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple TransitionMgmtAp extends Ieee80211MgmtAp +{ + parameters: + @class(::TransitionMgmtAp); +} + +simple Ieee80211ModeSetTransitionTest extends SimpleModule +{ + parameters: + @class(::Ieee80211ModeSetTransitionTest); +} + +network TransitionNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap[3]: AccessPoint; + test: Ieee80211ModeSetTransitionTest; +} + +%inifile: omnetpp.ini +[General] +network = TransitionNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 1ms +seed-set = 0 +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.initialX = 10m +**.mobility.initialY = 10m +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = -1bps +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.channelNumber = 6 +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].mac.qosStation = true +*.ap[*].wlan[0].mgmt.typename = "TransitionMgmtAp" +*.ap[*].wlan[0].mgmt.ssid = "transition" +*.ap[*].wlan[0].mgmt.beaconInterval = 1s +*.ap[*].wlan[0].mac.dcf.rateControl.typename = "AarfRateControl" +*.ap[*].wlan[0].mac.hcf.rateControl.typename = "OnoeRateControl" + +*.ap[1].wlan[0].mac.dcf.rateSelection.dataFrameBitrate = 65Mbps +*.ap[1].wlan[0].mac.dcf.rateSelection.dataFrameBandwidth = 20MHz +*.ap[1].wlan[0].mac.dcf.rateSelection.dataFrameNumSpatialStreams = 1 +*.ap[1].wlan[0].mac.dcf.rateSelection.dataFrameGuardInterval = 800ns +*.ap[2].wlan[0].mac.hcf.rateSelection.dataFrameBitrate = 65Mbps +*.ap[2].wlan[0].mac.hcf.rateSelection.dataFrameBandwidth = 20MHz +*.ap[2].wlan[0].mac.hcf.rateSelection.dataFrameNumSpatialStreams = 1 +*.ap[2].wlan[0].mac.hcf.rateSelection.dataFrameGuardInterval = 800ns + +%contains: stdout +Mode-set switching, advertisements, peer state, both selectors, and rollback verified. diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 3fb4ee8a5bc..11e854a6a0c 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -69,8 +69,8 @@ class TestRateSelectionAccessor : public ieee80211::RateSelection static const IIeee80211Mode *getResponseAckFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->responseAckFrameMode; } static const IIeee80211Mode *getResponseCtsFrameMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->responseCtsFrameMode; } static const IIeee80211Mode *getFastestMandatoryMode(const ieee80211::RateSelection *rs) { return static_cast(rs)->fastestMandatoryMode; } - static const Ieee80211ModeSet *getModeSet(const ieee80211::RateSelection *rs) { return static_cast(rs)->modeSet; } - static void applyModeSet(ieee80211::RateSelection *rs, const Ieee80211ModeSet *ms) { + static const Ieee80211ModeSet *readModeSet(const ieee80211::RateSelection *rs) { return static_cast(rs)->modeSet; } + static void notifyModeSet(ieee80211::RateSelection *rs, const Ieee80211ModeSet *ms) { static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); } }; @@ -81,8 +81,8 @@ class TestQosRateSelectionAccessor : public ieee80211::QosRateSelection static const IIeee80211Mode *getDataFrameMode(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->dataFrameMode; } static const IIeee80211Mode *getResponseBlockAckFrameMode(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->responseBlockAckFrameMode; } static const IIeee80211Mode *getFastestMandatoryMode(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->fastestMandatoryMode; } - static const Ieee80211ModeSet *getModeSet(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->modeSet; } - static void applyModeSet(ieee80211::QosRateSelection *rs, const Ieee80211ModeSet *ms) { + static const Ieee80211ModeSet *readModeSet(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->modeSet; } + static void notifyModeSet(ieee80211::QosRateSelection *rs, const Ieee80211ModeSet *ms) { static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); } }; @@ -104,7 +104,7 @@ class TestIeee80211Mgmt : public ieee80211::Ieee80211MgmtBase virtual void handleProbeResponseFrame(Packet *packet, const Ptr& header) override {} public: - void applyModeSet(const Ieee80211ModeSet *modeSet) + void notifyModeSet(const Ieee80211ModeSet *modeSet) { receiveSignal(nullptr, modesetChangedSignal, const_cast(modeSet), nullptr); } @@ -129,7 +129,7 @@ const auto modeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); ASSERT(modeSet->getNumModes() == 135); TestIeee80211Mgmt mgmt; -mgmt.applyModeSet(modeSet); +mgmt.notifyModeSet(modeSet); const auto& htSupportedRates = mgmt.getSupportedRates(); const double expectedHtSupportedRates[] = {1, 2, 5.5, 6, 11, 12, 24}; ASSERT(htSupportedRates.numRates == 7); @@ -139,7 +139,7 @@ for (int i = 0; i < htSupportedRates.numRates; i++) { ASSERT(htSupportedRates.rate[i - 1] < htSupportedRates.rate[i]); } -mgmt.applyModeSet(Ieee80211ModeSet::getModeSet("a")); +mgmt.notifyModeSet(Ieee80211ModeSet::getModeSet("a")); const auto& legacySupportedRates = mgmt.getSupportedRates(); const double expectedLegacySupportedRates[] = {6, 12, 24, 9, 18, 36, 48, 54}; ASSERT(legacySupportedRates.numRates == 8); @@ -473,8 +473,8 @@ rateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); rateSelection->finalizeParameters(); rateSelection->buildInside(); -TestRateSelectionAccessor::applyModeSet(rateSelection, erpModeSet); -ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == erpModeSet); +TestRateSelectionAccessor::notifyModeSet(rateSelection, erpModeSet); +ASSERT(TestRateSelectionAccessor::readModeSet(rateSelection) == erpModeSet); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); @@ -485,8 +485,8 @@ ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getFastestMandatoryMo ASSERT(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection) == erpModeSet->getMode(Mbps(24))); // Dynamically switch rateSelection to HT mode set -TestRateSelectionAccessor::applyModeSet(rateSelection, modeSet); -ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == modeSet); +TestRateSelectionAccessor::notifyModeSet(rateSelection, modeSet); +ASSERT(TestRateSelectionAccessor::readModeSet(rateSelection) == modeSet); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); @@ -513,8 +513,8 @@ rateSelectionHt->par("responseCtsFrameBitrate").setDoubleValue(6e6); rateSelectionHt->finalizeParameters(); rateSelectionHt->buildInside(); -TestRateSelectionAccessor::applyModeSet(rateSelectionHt, modeSet); -ASSERT(TestRateSelectionAccessor::getModeSet(rateSelectionHt) == modeSet); +TestRateSelectionAccessor::notifyModeSet(rateSelectionHt, modeSet); +ASSERT(TestRateSelectionAccessor::readModeSet(rateSelectionHt) == modeSet); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt))); ASSERT(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt) == short65); @@ -531,14 +531,14 @@ qosRateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); qosRateSelection->finalizeParameters(); qosRateSelection->buildInside(); -TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, erpModeSet); -ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == erpModeSet); +TestQosRateSelectionAccessor::notifyModeSet(qosRateSelection, erpModeSet); +ASSERT(TestQosRateSelectionAccessor::readModeSet(qosRateSelection) == erpModeSet); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getFastestMandatoryMode(qosRateSelection))); -TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, modeSet); -ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == modeSet); +TestQosRateSelectionAccessor::notifyModeSet(qosRateSelection, modeSet); +ASSERT(TestQosRateSelectionAccessor::readModeSet(qosRateSelection) == modeSet); ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(!erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); From b11f35c7b2e001548c2a8b2f7f629784d4a6d57f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 22:03:46 +0200 Subject: [PATCH 12/56] ieee80211: mark malformed response AIDs incorrect Invalid wire AIDs in association and reassociation responses used to throw during deserialization. Mark the frame incorrect and substitute zero so parsing preserves the status, rates, trailing elements, and stream position. Add raw-byte coverage for missing markers, successful AIDs outside the valid range, and nonzero unsuccessful-response AIDs in both response types. The focused management serializer unit test passes against the rebuilt debug library. --- .../mgmt/Ieee80211MgmtFrameSerializer.cc | 20 ++++----- .../unit/Ieee80211MgmtFrameSerializer_1.test | 43 ++++++++++++++----- 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc index 480c291e11a..72e1d578b46 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc @@ -471,18 +471,16 @@ static uint16_t encodeAssociationId(Ieee80211StatusCode statusCode, int aid) return 0; } -static int decodeAssociationId(Ieee80211StatusCode statusCode, uint16_t wireAid) +static int decodeAssociationId(Ieee80211AssociationResponseFrame& frame, 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"); + if (frame.getStatusCode() == SC_SUCCESSFUL) { const int aid = wireAid & ASSOCIATION_ID_MASK; - if (aid < 1 || aid > MAX_LOGICAL_ASSOCIATION_ID) - throw cRuntimeError("Malformed successful Association Response AID: %d", aid); - return aid; + if ((wireAid & ASSOCIATION_ID_MARKER) == ASSOCIATION_ID_MARKER && aid >= 1 && aid <= MAX_LOGICAL_ASSOCIATION_ID) + return aid; + frame.markIncorrect(); } - if (wireAid != 0) - throw cRuntimeError("Malformed unsuccessful Association Response AID: expected zero, got 0x%04x", wireAid); + else if (wireAid != 0) + frame.markIncorrect(); return 0; } @@ -733,7 +731,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, stream.readUint16Le())); Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); @@ -745,7 +743,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, stream.readUint16Le())); Ieee80211SupportedRatesElement supRat; deserializeSupportedRates(stream, *frame, supRat); diff --git a/tests/unit/Ieee80211MgmtFrameSerializer_1.test b/tests/unit/Ieee80211MgmtFrameSerializer_1.test index f387d6b4c81..96467753136 100644 --- a/tests/unit/Ieee80211MgmtFrameSerializer_1.test +++ b/tests/unit/Ieee80211MgmtFrameSerializer_1.test @@ -172,6 +172,36 @@ static void checkMalformedSupportedRates(const std::vector& prefix) ASSERT(zeroLength->getSupportedRates().numRates == 0); } +template +static void checkMalformedAssociationIds() +{ + for (uint16_t status : {0, 0x1234}) { + const std::vector invalidAids = status == 0 ? + std::vector{0x0001, 0x4001, 0x8001, 0xC000, 0xC7D8, 0xFFFF} : + std::vector{0x0001, 0xC000, 0xC001}; + for (auto aid : invalidAids) { + auto bytes = appendTrailingInformationElement({ + 0x00, 0x00, uint8_t(status), uint8_t(status >> 8), uint8_t(aid), uint8_t(aid >> 8), + 0x01, 0x01, 0x0C, 0x32, 0x01, 0x18 + }); + auto frame = deserializeMalformedBody(bytes); + MemoryInputStream stream(bytes); + Ieee80211MgmtFrameSerializer serializer; + auto decoded = serializer.deserialize(stream, typeid(T)); + ASSERT(decoded->isIncorrect()); + ASSERT(stream.getPosition() == B(bytes.size())); + ASSERT(frame->isIncorrect()); + ASSERT(frame->isComplete()); + ASSERT(frame->getStatusCode() == status); + ASSERT(frame->getAid() == 0); + ASSERT(frame->getSupportedRates().numRates == 1); + ASSERT(frame->getSupportedRates().rate[0] == 6); + ASSERT(frame->getExtendedSupportedRates().numRates == 1); + ASSERT(frame->getExtendedSupportedRates().rate[0] == 12); + } + } +} + %activity: { @@ -267,17 +297,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 { From 8d5bd8eef6b3f09121cc30a0aef401bf8e49f62e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 22:03:58 +0200 Subject: [PATCH 13/56] ieee80211: commit mode-set changes before observer notification An observer exception could roll back a mode set already cached by an earlier listener. Complete the radio and behavioral participant transaction before publishing its result, and propagate listener exceptions without undoing the announced state. Retain the reentrancy guard during publication. Extend the transition test with an ordinary caching listener followed by an ordinary throwing listener. Verify observer and radio consistency after publication failure while retaining DCF and HCF rollback coverage for update failures. The focused transition module test passes against the rebuilt debug library. --- .../ieee80211/packetlevel/Ieee80211Radio.cc | 19 +++++--- .../ieee80211/packetlevel/Ieee80211Radio.h | 5 +- .../module/Ieee80211ModeSetTransition_1.test | 48 ++++++++++++++----- 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index b0abedb5211..81e4996c737 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -140,10 +140,6 @@ void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee8 receiver->setModeSet(modeSet); for (auto participant : participants) participant->applyModeSet(modeSet); - // Every built-in dependent, including the MIB, is now current before - // any observer runs. Built-in signal adapters skip this applied set. - if (modeSet != nullptr) - emit(modesetChangedSignal, const_cast(modeSet)); } catch (...) { transmitter->setModeSetAndMode(oldTransmitterModeSet, oldMode); @@ -154,10 +150,21 @@ void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee8 changingModeSet = false; throw; } + receptionTimer = nullptr; + // The transaction is committed. Observer failures must not undo a state + // already published to earlier listeners. Keep the reentrancy guard during + // publication so every listener observes the same committed mode set. + try { + if (modeSet != nullptr) + emit(modesetChangedSignal, const_cast(modeSet)); + emit(listeningChangedSignal, 0); + } + catch (...) { + changingModeSet = false; + throw; + } changingModeSet = false; EV << "Changing radio mode set to " << modeSet << " and mode to " << transmitter->getMode() << endl; - receptionTimer = nullptr; - emit(listeningChangedSignal, 0); } void Ieee80211Radio::setMode(const IIeee80211Mode *mode) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h index 841c6ac2e63..976b43b23db 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h @@ -50,8 +50,9 @@ class INET_API Ieee80211Radio : public FlatRadioBase Ieee80211Radio(); // These setters snapshot transactional mode-set consumers before applying - // the catalog, and restore them if an update or synchronous notification - // throws. Stateful modesetChanged subscribers implement IIeee80211ModeSetListener. + // the catalog, and restore them if an update throws. Notifications publish + // committed state; listener exceptions propagate without rolling it back. + // Behavioral consumers implement IIeee80211ModeSetListener. virtual void setModeSet(const Ieee80211ModeSet *modeSet); virtual void setModeSetAndMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode); virtual void setMode(const IIeee80211Mode *mode); diff --git a/tests/module/Ieee80211ModeSetTransition_1.test b/tests/module/Ieee80211ModeSetTransition_1.test index 6ec9a84d8da..50e9669bf32 100644 --- a/tests/module/Ieee80211ModeSetTransition_1.test +++ b/tests/module/Ieee80211ModeSetTransition_1.test @@ -1,8 +1,8 @@ %description: Exercise both radio mode-set setters with DCF and HCF selectors. Successful HT/legacy transitions refresh PHY-limited capabilities and actual Beacon bodies. -Fixed-rate and observer failures restore all subscribed catalogs, caches, peer -negotiations, and channel-access windows. No signal replay is used for rollback. +Fixed-rate failures restore all subscribed catalogs, caches, peer negotiations, +and channel-access windows. Observer failures leave published state committed. %file: Ieee80211ModeSetTransition.cc @@ -62,10 +62,24 @@ class TransitionMgmtAp : public Ieee80211MgmtAp }; Define_Module(TransitionMgmtAp); +class CachingModeSetObserver : public cListener +{ + public: + const Ieee80211ModeSet *modeSet = nullptr; + int notifications = 0; + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override + { + modeSet = check_and_cast(value); + notifications++; + } +}; + class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener { bool rejectNotification = false; int notifications = 0; + const Ieee80211ModeSet *observedModeSet = nullptr; + CachingModeSetObserver cachingObserver; MacAddress peer = MacAddress("02:00:00:00:01:ff"); std::vector participants(cModule *radio) @@ -133,6 +147,9 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener notifications++; auto radio = check_and_cast(source); auto target = check_and_cast(value); + observedModeSet = target; + ASSERT(cachingObserver.modeSet == target); + ASSERT(cachingObserver.notifications == notifications); // Publication must follow *all* internal updates, irrespective of where // this observer appears in the subscriber list. auto nic = radio->getParentModule(); @@ -154,7 +171,12 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener auto ht = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); auto legacy = Ieee80211ModeSet::getModeSet("g(mixed)"); radio->setMode(ht->getMode(Mbps(24))); - radio->subscribe(modesetChangedSignal, this); + cachingObserver.modeSet = ht; + observedModeSet = ht; + radio->subscribe(modesetChangedSignal, &cachingObserver); + // Hierarchical delivery guarantees the radio's ordinary caching + // listener runs before this ordinary throwing listener on its parent. + nic->subscribe(modesetChangedSignal, this); mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); check(nic, ht, true); auto oldGeneration = mib->findPeerHtState(peer)->generation; @@ -193,21 +215,22 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener ASSERT(arf->getRate(peer) == oldArf); ASSERT(onoe->getRate(peer) == oldOnoe); } - radio->unsubscribe(modesetChangedSignal, this); + nic->unsubscribe(modesetChangedSignal, this); + radio->unsubscribe(modesetChangedSignal, &cachingObserver); continue; } - // Reject after all internal updates have succeeded, to cover arbitrary - // notification-prefix failure in addition to fixed-rate lookup failure. + // Throw after an ordinary observer has cached the published set. + // Both observers and all behavioral participants must retain it. rejectNotification = true; bool rejected = false; try { radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); } catch (const cRuntimeError&) { rejected = true; } ASSERT(rejected); ASSERT(notifications == 1); - check(nic, ht, true); - ASSERT(mib->findPeerHtState(peer)->generation == oldGeneration); - ASSERT(channelAccess->getCw() == oldCw); - ASSERT(edcaf->getCw() == oldQosCw); + check(nic, legacy, false); + ASSERT(tx->getMode() == legacy->getMode(Mbps(24))); + ASSERT(cachingObserver.modeSet == tx->getModeSet()); + ASSERT(observedModeSet == tx->getModeSet()); rejectNotification = false; radio->setModeSet(legacy); check(nic, legacy, false); @@ -216,7 +239,10 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener mib->setPeerHtCapabilities(peer, mib->localHtCapabilities, mib->getHtOperation()); check(nic, ht, true); ASSERT(notifications == 3); - radio->unsubscribe(modesetChangedSignal, this); + ASSERT(cachingObserver.modeSet == tx->getModeSet()); + ASSERT(observedModeSet == tx->getModeSet()); + nic->unsubscribe(modesetChangedSignal, this); + radio->unsubscribe(modesetChangedSignal, &cachingObserver); } std::cout << "Mode-set switching, advertisements, peer state, both selectors, and rollback verified.\n"; endSimulation(); From c05e44a9d99e98319922fcaae1a8055e54f470e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 22:22:09 +0200 Subject: [PATCH 14/56] Ieee80211Radio: publish listening changes after observer failures A throwing mode-set observer prevented RadioMedium from receiving the listening change after radio state had already committed. Attempt both independent notifications before rethrowing the first observer failure, retaining committed state and the reentrancy guard during publication. Extend the module test to verify RadioMedium processes the listening change and cover listening-only and simultaneous observer failures through both mode-set setters. The regression fails before the fix and passes afterward. Debug build and scoped architecture checks pass. --- .../ieee80211/packetlevel/Ieee80211Radio.cc | 15 +++- .../module/Ieee80211ModeSetTransition_1.test | 83 ++++++++++++++++++- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 81e4996c737..3dd0cb1ee7f 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -8,6 +8,7 @@ #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" #include +#include #include "inet/physicallayer/wireless/ieee80211/contract/packetlevel/IIeee80211ModeSetListener.h" @@ -154,16 +155,26 @@ void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee8 // The transaction is committed. Observer failures must not undo a state // already published to earlier listeners. Keep the reentrancy guard during // publication so every listener observes the same committed mode set. + std::exception_ptr observerFailure; try { if (modeSet != nullptr) emit(modesetChangedSignal, const_cast(modeSet)); + } + catch (...) { + observerFailure = std::current_exception(); + } + // Listening changes are independent committed facts: the medium must get + // its publication attempt even when a mode-set observer throws. + try { emit(listeningChangedSignal, 0); } catch (...) { - changingModeSet = false; - throw; + if (!observerFailure) + observerFailure = std::current_exception(); } changingModeSet = false; + if (observerFailure) + std::rethrow_exception(observerFailure); EV << "Changing radio mode set to " << modeSet << " and mode to " << transmitter->getMode() << endl; } diff --git a/tests/module/Ieee80211ModeSetTransition_1.test b/tests/module/Ieee80211ModeSetTransition_1.test index 50e9669bf32..fbb09387ff6 100644 --- a/tests/module/Ieee80211ModeSetTransition_1.test +++ b/tests/module/Ieee80211ModeSetTransition_1.test @@ -2,11 +2,13 @@ Exercise both radio mode-set setters with DCF and HCF selectors. Successful HT/legacy transitions refresh PHY-limited capabilities and actual Beacon bodies. Fixed-rate failures restore all subscribed catalogs, caches, peer negotiations, -and channel-access windows. Observer failures leave published state committed. +and channel-access windows. Observer failures leave published state committed, +publish both notifications, and reach RadioMedium's listening-change handler. %file: Ieee80211ModeSetTransition.cc #include "inet/common/Simsignals.h" +#include "inet/physicallayer/wireless/common/medium/RadioMedium.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h" @@ -20,6 +22,24 @@ using namespace inet; using namespace inet::ieee80211; using namespace inet::physicallayer; +class TransitionRadioMedium : public RadioMedium +{ + public: + int listeningNotifications = 0; + cComponent *lastListeningSource = nullptr; + protected: + virtual void receiveSignal(cComponent *source, simsignal_t signal, intval_t value, cObject *details) override + { + Enter_Method_Silent(); + RadioMedium::receiveSignal(source, signal, value, details); + if (signal == IRadio::listeningChangedSignal) { + listeningNotifications++; + lastListeningSource = source; + } + } +}; +Define_Module(TransitionRadioMedium); + class TransitionMgmtAp : public Ieee80211MgmtAp { bool probing = false; @@ -77,6 +97,8 @@ class CachingModeSetObserver : public cListener class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener { bool rejectNotification = false; + bool rejectListeningNotification = false; + int listeningNotifications = 0; int notifications = 0; const Ieee80211ModeSet *observedModeSet = nullptr; CachingModeSetObserver cachingObserver; @@ -141,6 +163,14 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener protected: virtual void initialize() override { scheduleAt(SimTime(1, SIMTIME_US), new cMessage("switch")); } + virtual void receiveSignal(cComponent *source, simsignal_t signal, intval_t value, cObject *details) override + { + Enter_Method_Silent(); + ASSERT(signal == IRadio::listeningChangedSignal); + listeningNotifications++; + if (rejectListeningNotification) + throw cRuntimeError("test observer rejects listening change"); + } virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *value, cObject *details) override { Enter_Method_Silent(); @@ -222,10 +252,14 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener // Throw after an ordinary observer has cached the published set. // Both observers and all behavioral participants must retain it. rejectNotification = true; + auto medium = check_and_cast(getParentModule()->getSubmodule("radioMedium")); + int oldListeningNotifications = medium->listeningNotifications; bool rejected = false; try { radio->setModeSetAndMode(legacy, legacy->getMode(Mbps(24))); } catch (const cRuntimeError&) { rejected = true; } ASSERT(rejected); + ASSERT(medium->listeningNotifications == oldListeningNotifications + 1); + ASSERT(medium->lastListeningSource == radio); ASSERT(notifications == 1); check(nic, legacy, false); ASSERT(tx->getMode() == legacy->getMode(Mbps(24))); @@ -241,6 +275,44 @@ class Ieee80211ModeSetTransitionTest : public cSimpleModule, public cListener ASSERT(notifications == 3); ASSERT(cachingObserver.modeSet == tx->getModeSet()); ASSERT(observedModeSet == tx->getModeSet()); + // The medium subscribes on the radio, so it processes listening + // changes before the throwing observer on the parent interface. + radio->setMode(ht->getMode(Mbps(24))); + nic->subscribe(IRadio::listeningChangedSignal, this); + for (bool explicitMode : {false, true}) { + for (bool rejectModeSet : {false, true}) { + rejectNotification = rejectModeSet; + rejectListeningNotification = true; + int oldNotifications = notifications; + int oldListening = listeningNotifications; + int oldMediumListening = medium->listeningNotifications; + auto target = rejectModeSet ? ht : legacy; + bool caught = false; + try { + if (explicitMode) + radio->setModeSetAndMode(target, target->getMode(Mbps(24))); + else + radio->setModeSet(target); + } + catch (const cRuntimeError& error) { + caught = true; + const char *expected = rejectModeSet ? "test observer rejects mode set" : "test observer rejects listening change"; + ASSERT(std::string(error.what()).find(expected) != std::string::npos); + } + ASSERT(caught); + ASSERT(notifications == oldNotifications + 1); + ASSERT(listeningNotifications == oldListening + 1); + ASSERT(medium->listeningNotifications == oldMediumListening + 1); + ASSERT(medium->lastListeningSource == radio); + check(nic, target, false); + } + } + rejectNotification = false; + rejectListeningNotification = false; + // A subsequent transition also proves the reentrancy guard cleared. + radio->setModeSet(ht); + check(nic, ht, false); + nic->unsubscribe(IRadio::listeningChangedSignal, this); nic->unsubscribe(modesetChangedSignal, this); radio->unsubscribe(modesetChangedSignal, &cachingObserver); } @@ -262,6 +334,12 @@ simple TransitionMgmtAp extends Ieee80211MgmtAp @class(::TransitionMgmtAp); } +module TransitionRadioMedium extends Ieee80211ScalarRadioMedium +{ + parameters: + @class(::TransitionRadioMedium); +} + simple Ieee80211ModeSetTransitionTest extends SimpleModule { parameters: @@ -271,7 +349,7 @@ simple Ieee80211ModeSetTransitionTest extends SimpleModule network TransitionNetwork { submodules: - radioMedium: Ieee80211ScalarRadioMedium; + radioMedium: TransitionRadioMedium; ap[3]: AccessPoint; test: Ieee80211ModeSetTransitionTest; } @@ -285,6 +363,7 @@ seed-set = 0 cmdenv-express-mode = false record-vector-results = false record-scalar-results = false +*.radioMedium.listeningFilter = true **.mobility.initFromDisplayString = false **.mobility.initialX = 10m **.mobility.initialY = 10m From 09c02fd4afa2b35dd1e22e3c1acca1e9acf07a68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 01:48:24 +0200 Subject: [PATCH 15/56] feature(physicallayer): add HT Greenfield mode set and PHY transmission support Register the n(greenfield-2.4Ghz) mode set and distinguish selectable operational modes (containsMode) from supported PHY capabilities (supportsMode). HT Greenfield profiles now explicitly support non-HT and HT-mixed response modes without making them selectable for data transmissions. Precompute immutable control-response mappings in Ieee80211ModeSet and make the mode-set registry thread-local to ensure thread safety. In the physical layer, validate per-packet transmission requests and reception feasibility against supported mode capabilities. Decompose transmission duration into preamble, header, and data intervals, correctly accounting for HT/VHT SIG field integration in the preamble. Publish modesetChangedSignal upon radio mode set transitions. Add unit test coverage for HT Greenfield compliant modes, duration decomposition, transmitter transition invariants, and ERP mode isolation. --- .../ieee80211/mode/Ieee80211HtMode.cc | 2 +- .../ieee80211/mode/Ieee80211ModeSet.cc | 439 +++++++++++++++++- .../ieee80211/mode/Ieee80211ModeSet.h | 36 +- .../ieee80211/mode/Ieee80211VhtMode.cc | 6 +- .../ieee80211/packetlevel/Ieee80211Radio.cc | 33 +- .../ieee80211/packetlevel/Ieee80211Radio.ned | 4 +- .../packetlevel/Ieee80211Receiver.cc | 4 +- .../packetlevel/Ieee80211Receiver.ned | 3 +- .../packetlevel/Ieee80211Transmitter.cc | 17 +- .../packetlevel/Ieee80211Transmitter.ned | 2 +- tests/unit/Ieee80211HtGreenfield_1.test | 354 ++++++++++++++ tests/unit/Ieee80211HtGuardInterval_1.test | 3 +- 12 files changed, 856 insertions(+), 47 deletions(-) create mode 100644 tests/unit/Ieee80211HtGreenfield_1.test diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc index d4498568496..884fc3864f0 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.cc @@ -380,7 +380,7 @@ const Ieee80211HtMode *Ieee80211HtCompliantModes::getCompliantMode(const Ieee802 const Ieee80211HtDataMode *dataMode = new Ieee80211HtDataMode(mcsMode, mcsMode->getBandwidth(), guardIntervalType); const Ieee80211HtPreambleMode *preambleMode = new Ieee80211HtPreambleMode(htSignal, legacySignal, preambleFormat, dataMode->getNumberOfSpatialStreams()); const Ieee80211HtMode *htMode = new Ieee80211HtMode(name, preambleMode, dataMode, centerFrequencyMode); - singleton.modeCache.insert(std::pair, const Ieee80211HtMode *>(htModeId, htMode)); + singleton.modeCache.emplace(htModeId, htMode); return htMode; } return mode->second; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index e21dbf9d613..e58712e453a 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -9,6 +9,8 @@ #include #include +#include +#include #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h" #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" @@ -25,7 +27,147 @@ namespace physicallayer { Register_Abstract_Class(Ieee80211ModeSet); -const DelayedInitializer> Ieee80211ModeSet::modeSets([]() { return new std::vector { +std::vector Ieee80211ModeSet::completeHtGuardIntervalVariants(const char *name, const std::vector& entries) +{ + if (strcmp(name, "n(mixed-2.4Ghz)") && strcmp(name, "n(greenfield-2.4Ghz)")) + return entries; + + std::vector completeEntries = entries; + // IEEE Std 802.11-2024, Table 19-6 defines the 800 ns and 400 ns GIs. + // Add only the alternate GI for each mode explicitly declared above; + // declaration order, mandatory flags, and the historical catalog remain + // authoritative for this operation mode. + auto numberOfBaseEntries = completeEntries.size(); + for (size_t index = 0; index < numberOfBaseEntries; index++) { + auto htMode = dynamic_cast(completeEntries[index].mode); + if (htMode == nullptr) + continue; + auto dataMode = htMode->getDataMode(); + auto alternateGuardInterval = dataMode->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG ? + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT : Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG; + auto alternateMode = Ieee80211HtCompliantModes::getCompliantMode( + dataMode->getModulationAndCodingScheme(), htMode->getCenterFrequencyMode(), + htMode->getPreambleMode()->getPreambleFormat(), alternateGuardInterval); + auto alternateAlreadyPresent = std::any_of(completeEntries.begin(), completeEntries.end(), [alternateMode](const Entry& entry) { return entry.mode == alternateMode; }); + if (!alternateAlreadyPresent) + completeEntries.push_back({false, alternateMode}); + } + return completeEntries; +} + +#define HT_MODE_ENTRY(WIDTH, MCS, MANDATORY, FORMAT, GUARD_INTERVAL) \ + { MANDATORY, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs##MCS##BW##WIDTH##MHz, Ieee80211HtMode::BAND_2_4GHZ, FORMAT, GUARD_INTERVAL) }, +#define HT_MODE_ENTRIES_20(FORMAT) \ + HT_MODE_ENTRY(20, 0, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 1, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 2, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 3, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 4, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 5, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 6, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 7, true, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) \ + HT_MODE_ENTRY(20, 8, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 9, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 10, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 11, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 12, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 13, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 14, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 15, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 16, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 17, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 18, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 19, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 20, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 21, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 22, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 23, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 24, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 25, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 26, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 27, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 28, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 29, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 30, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(20, 31, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) +#define HT_MODE_ENTRIES_40(FORMAT) \ + HT_MODE_ENTRY(40, 0, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 1, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 2, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 3, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 4, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 5, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 6, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 7, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 8, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 9, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 10, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 11, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 12, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 13, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 14, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 15, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 16, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 17, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 18, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 19, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 20, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 21, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 22, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 23, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 24, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 25, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 26, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 27, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 28, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 29, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 30, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) \ + HT_MODE_ENTRY(40, 31, false, FORMAT, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) + +static std::vector createHtEntries(Ieee80211HtPreambleMode::HighTroughputPreambleFormat preambleFormat) +{ + return { + HT_MODE_ENTRIES_20(preambleFormat) + HT_MODE_ENTRIES_40(preambleFormat) + }; +} + +static std::vector createHtSupportedEntries(Ieee80211HtPreambleMode::HighTroughputPreambleFormat preambleFormat) +{ + auto result = createHtEntries(preambleFormat); + if (preambleFormat == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD) { + auto mixedEntries = createHtEntries(Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED); + result.insert(result.end(), mixedEntries.begin(), mixedEntries.end()); + } + // Every 2.4 GHz HT STA supports the mandatory Clause 16/18 modes, and a + // Greenfield STA additionally supports HT-mixed PPDUs (IEEE 802.11-2024 + // 19.1.1 and 19.1.4). These are supplementary capabilities rather than + // selectable operating modes, so they are kept out of createHtEntries(). + result.insert(result.end(), { + { true, &Ieee80211DsssCompliantModes::dsssMode1Mbps, true }, + { true, &Ieee80211DsssCompliantModes::dsssMode2Mbps, true }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode2MbpsShortPreamble }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckLongPreamble, true }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckShortPreamble }, + { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps, true }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode9Mbps }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckLongPreamble, true }, + { true, &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckShortPreamble }, + { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode12Mbps, true }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode18Mbps }, + { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps, true }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode36Mbps }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode48Mbps }, + { false, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode54Mbps }, + }); + return result; +} + +#undef HT_MODE_ENTRIES_40 +#undef HT_MODE_ENTRIES_20 +#undef HT_MODE_ENTRY + +OPP_THREAD_LOCAL const DelayedInitializer> Ieee80211ModeSet::modeSets([]() { return new std::vector { Ieee80211ModeSet("a", { { true, &Ieee80211OfdmCompliantModes::ofdmMode6MbpsCS20MHz, true }, { false, &Ieee80211OfdmCompliantModes::ofdmMode9MbpsCS20MHz, true }, @@ -220,7 +362,73 @@ const DelayedInitializer> Ieee80211ModeSet::modeSe { true, &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckLongPreamble, true }, { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode12Mbps, true }, { true, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps, true } - }, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::HT, true), + }, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::HT, true, createHtSupportedEntries(Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED)), + Ieee80211ModeSet("n(greenfield-2.4Ghz)", { + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs1BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs2BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs3BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs4BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs5BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs6BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { true, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs7BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs8BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs9BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs10BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs11BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs12BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs13BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs14BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs15BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs16BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs17BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs18BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs19BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs20BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs21BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs22BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs23BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs24BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs25BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs26BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs27BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs28BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs29BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs30BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs31BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs1BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs2BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs3BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs4BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs5BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs6BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs7BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs8BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs9BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs10BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs11BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs12BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs13BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs14BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs15BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs16BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs17BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs18BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs19BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs20BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs21BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs22BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs23BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs24BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs25BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs26BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs27BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs28BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs29BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs30BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, + { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs31BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) } + }, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::HT, true, createHtSupportedEntries(Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD)), Ieee80211ModeSet("ac", { { true, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG) }, { true, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs1BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG) }, @@ -542,12 +750,16 @@ const DelayedInitializer> Ieee80211ModeSet::modeSe }, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG), PhyType::VHT),}; }); Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector entries, const IIeee80211Mode *referenceMode, - PhyType phyType, bool htOperationSupported) : + PhyType phyType, bool htOperationSupported, const std::vector supportedEntries) : name(name), - entries(entries), + entries(completeHtGuardIntervalVariants(name, entries)), phyType(phyType), referenceMode(referenceMode), - htOperationSupported(htOperationSupported) + htOperationSupported(htOperationSupported), + supportedEntries(supportedEntries.empty() ? this->entries : completeHtGuardIntervalVariants(name, supportedEntries)), + controlResponseModes(createControlResponseModes(this->supportedEntries)), + htMixedControlResponseModes(createHtMixedControlResponseModes(this->supportedEntries)), + nonHtControlResponseEntries(createNonHtControlResponseEntries(this->supportedEntries)) { if (this->entries.empty()) throw cRuntimeError("IEEE 802.11 mode set '%s' must contain at least one mode", this->name.c_str()); @@ -587,7 +799,7 @@ Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector en // 9.4.2.3 and 11.1.4.6); management splits it across the primary and // Extended Supported Rates elements when necessary. for (bool mandatory : {true, false}) { - for (const auto& entry : this->entries) { + for (const auto& entry : this->supportedEntries) { if (entry.isLegacyOperational && entry.isMandatory == mandatory) this->legacyOperationalModes.push_back(entry.mode); } @@ -600,10 +812,14 @@ Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector en for (const auto *mode : this->legacyOperationalModes) { if (mode == nullptr) throw cRuntimeError("IEEE 802.11 mode set '%s' contains a null legacy operational mode", this->name.c_str()); - int modeIndex = findModeIndex(mode); - if (modeIndex < 0) - throw cRuntimeError("Legacy operational mode '%s' is not contained in IEEE 802.11 mode set '%s'", mode->getName(), this->name.c_str()); - if (!this->entries[modeIndex].isLegacyOperational) + bool found = false; + for (const auto& entry : this->supportedEntries) { + if (entry.mode == mode && entry.isLegacyOperational) { + found = true; + break; + } + } + if (!found) throw cRuntimeError("Legacy operational mode '%s' is not marked eligible in IEEE 802.11 mode set '%s'", mode->getName(), this->name.c_str()); } for (const auto& entry : this->entries) { @@ -651,6 +867,109 @@ int Ieee80211ModeSet::findModeIndex(const IIeee80211Mode *mode) const return -1; } +std::map Ieee80211ModeSet::createControlResponseModes(const std::vector& supportedEntries) +{ + std::map result; + // IEEE 802.11-2024 Table 9-230 defines the Basic HT-MCS Set as a BSS- + // configured bitmap of MCS indexes. It is not modelled here, so use the + // mandatory entries (currently the 20 MHz entries) as a bounded fallback + // for the candidate indexes. Clause 10.6.6.5.3 selects CH_BANDWIDTH + // separately, and the candidate filter below then keeps only modes at the + // source bandwidth. Consequently, optional 40 MHz MCS 0..7 are treated as + // candidate MCSs. A modelled Basic HT-MCS Set would replace this mandatory- + // index fallback with the BSS-configured indexes; bandwidth filtering would + // remain a separate step. + std::set mandatoryHtMcsIndexes; + for (const auto& entry : supportedEntries) { + auto mode = dynamic_cast(entry.mode); + if (entry.isMandatory && mode != nullptr) + mandatoryHtMcsIndexes.insert(mode->getDataMode()->getMcsIndex()); + } + for (const auto& sourceEntry : supportedEntries) { + auto source = dynamic_cast(sourceEntry.mode); + if (source == nullptr) + continue; + std::vector candidates; + for (const auto& candidateEntry : supportedEntries) { + auto candidate = dynamic_cast(candidateEntry.mode); + if (candidate != nullptr && candidate->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED && + candidate->getCenterFrequencyMode() == source->getCenterFrequencyMode() && + candidate->getDataMode()->getBandwidth() == source->getDataMode()->getBandwidth() && + mandatoryHtMcsIndexes.find(candidate->getDataMode()->getMcsIndex()) != mandatoryHtMcsIndexes.end() && + candidate->getDataMode()->getMcsIndex() <= source->getDataMode()->getMcsIndex() && + candidate->getDataMode()->getNumberOfSpatialStreams() <= source->getDataMode()->getNumberOfSpatialStreams()) + candidates.push_back(candidate); + } + // IEEE 802.11-2024 10.6.6.5.3: with no Basic HT-MCS Set modelled, + // CandidateMCSSet uses the mandatory-index fallback. After the + // bandwidth and MCS-index bounds, + // retain the highest NSS not exceeding the received NSS, then select the + // highest indexed MCS whose per-stream modulation and coding rate do not + // exceed those of the received MCS. The modelled MCS 0..31 are EQM. + int highestNss = -1; + for (auto candidate : candidates) + highestNss = std::max(highestNss, candidate->getDataMode()->getNumberOfSpatialStreams()); + const Ieee80211HtMode *response = nullptr; + auto sourceMcs = source->getDataMode()->getModulationAndCodingScheme(); + int sourceModulation = sourceMcs->getModulation()->getSubcarrierModulation()->getCodeWordSize(); + double sourceCodeRate = sourceMcs->getCode()->getForwardErrorCorrection()->getCodeRate(); + for (auto candidate : candidates) { + auto candidateDataMode = candidate->getDataMode(); + auto candidateMcs = candidateDataMode->getModulationAndCodingScheme(); + if (candidateDataMode->getNumberOfSpatialStreams() == highestNss && + candidateMcs->getModulation()->getSubcarrierModulation()->getCodeWordSize() <= sourceModulation && + candidateMcs->getCode()->getForwardErrorCorrection()->getCodeRate() <= sourceCodeRate && + (response == nullptr || candidateDataMode->getMcsIndex() > response->getDataMode()->getMcsIndex())) + response = candidate; + } + if (response != nullptr) + result.emplace(sourceEntry.mode, response); + } + return result; +} + +std::map Ieee80211ModeSet::createHtMixedControlResponseModes(const std::vector& supportedEntries) +{ + std::map result; + for (const auto& sourceEntry : supportedEntries) { + auto source = dynamic_cast(sourceEntry.mode); + if (source == nullptr) + continue; + for (const auto& candidateEntry : supportedEntries) { + auto candidate = dynamic_cast(candidateEntry.mode); + if (candidate != nullptr && candidate->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED && + candidate->getCenterFrequencyMode() == source->getCenterFrequencyMode() && + candidate->getDataMode()->getMcsIndex() == source->getDataMode()->getMcsIndex() && + candidate->getDataMode()->getBandwidth() == source->getDataMode()->getBandwidth() && + candidate->getDataMode()->getNumberOfSpatialStreams() == source->getDataMode()->getNumberOfSpatialStreams() && + candidate->getDataMode()->getGuardIntervalType() == source->getDataMode()->getGuardIntervalType()) + { + result.emplace(sourceEntry.mode, candidateEntry.mode); + break; + } + } + } + return result; +} + +std::vector Ieee80211ModeSet::createNonHtControlResponseEntries(const std::vector& supportedEntries) +{ + std::vector result; + for (const auto& entry : supportedEntries) + if (entry.isMandatory && dynamic_cast(entry.mode) == nullptr && dynamic_cast(entry.mode) == nullptr) + result.push_back(entry); + std::stable_sort(result.begin(), result.end(), EntryNetBitrateComparator()); + return result; +} + +bool Ieee80211ModeSet::supportsMode(const IIeee80211Mode *mode) const +{ + for (const auto& entry : supportedEntries) + if (entry.mode == mode) + return true; + return false; +} + int Ieee80211ModeSet::getModeIndex(const IIeee80211Mode *mode) const { int index = findModeIndex(mode); @@ -662,7 +981,32 @@ int Ieee80211ModeSet::getModeIndex(const IIeee80211Mode *mode) const bool Ieee80211ModeSet::getIsMandatory(const IIeee80211Mode *mode) const { - return entries[getModeIndex(mode)].isMandatory; + int index = findModeIndex(mode); + if (index >= 0) + return entries[index].isMandatory; + for (const auto *legacyMode : legacyOperationalModes) { + if (legacyMode == mode) { + for (const auto& entry : supportedEntries) { + if (entry.mode == mode) + return entry.isMandatory; + } + } + } + throw cRuntimeError("Unknown mode"); +} + +const IIeee80211Mode *Ieee80211ModeSet::findMode(const IIeee80211Mode *mode) const +{ + int index = findModeIndex(mode); + return index >= 0 ? entries[index].mode : nullptr; +} + +const IIeee80211Mode *Ieee80211ModeSet::getMode(const IIeee80211Mode *mode) const +{ + auto result = findMode(mode); + if (result == nullptr) + throw cRuntimeError("Unknown mode in operation mode: '%s'", getName()); + return result; } const IIeee80211Mode *Ieee80211ModeSet::findCompatibleMode(const IIeee80211Mode *mode) const @@ -843,6 +1187,78 @@ const IIeee80211Mode *Ieee80211ModeSet::getFasterMandatoryMode(const IIeee80211M return result; } +const IIeee80211Mode *Ieee80211ModeSet::getControlResponseMode(const IIeee80211Mode *mode, const IIeee80211Mode *configuredMode) const +{ + if (!supportsMode(mode)) + throw cRuntimeError("Control response mode is not supported by operation mode %s: %s", getName(), mode->getName()); + auto it = controlResponseModes.find(mode); + if (it == controlResponseModes.end()) { + if (configuredMode != nullptr) + return getNonHtControlResponseMode(configuredMode, false); + return getMandatoryControlResponseMode(mode); + } + auto primaryMode = it->second; + if (configuredMode == nullptr) + return primaryMode; + // A configured HT response is a deliberate model extension beyond the + // automatic response constraints in IEEE 802.11-2024 10.6.6.5.3 and + // 10.6.6.5.7. Translate only its preamble to HT-mixed, preserving the + // resolved configured mode's MCS, bandwidth, NSS, GI, and band. + if (!supportsMode(configuredMode)) + throw cRuntimeError("Configured control response mode is not supported by operation mode %s: %s", getName(), configuredMode->getName()); + auto configuredIt = htMixedControlResponseModes.find(configuredMode); + if (configuredIt == htMixedControlResponseModes.end()) + throw cRuntimeError("An HT RTS requires an HT-mixed CTS response, configured mode is non-HT: %s", configuredMode->getName()); + return configuredIt->second; +} + +const IIeee80211Mode *Ieee80211ModeSet::getMandatoryControlResponseMode(const IIeee80211Mode *mode) const +{ + if (!supportsMode(mode)) + throw cRuntimeError("Control response mode is not supported by operation mode %s: %s", getName(), mode->getName()); + if (!nonHtControlResponseEntries.empty() && (controlResponseModes.find(mode) != controlResponseModes.end() || !containsMode(mode))) + return getNonHtControlResponseMode(mode); + if (getIsMandatory(mode)) + return mode; + if (auto slowerMode = getSlowerMandatoryMode(mode)) + return slowerMode; + return getNonHtControlResponseMode(mode); +} + +const IIeee80211Mode *Ieee80211ModeSet::getNonHtControlResponseMode(const IIeee80211Mode *mode, bool mandatory) const +{ + if (!supportsMode(mode)) + throw cRuntimeError("Control response mode is not supported by operation mode %s: %s", getName(), mode->getName()); + if (nonHtControlResponseEntries.empty()) { + if (!containsMode(mode)) + throw cRuntimeError("No non-HT control response mode for %s", mode->getName()); + if (!mandatory) + return mode; + if (getIsMandatory(mode)) + return mode; + if (auto slowerMode = getSlowerMandatoryMode(mode)) + return slowerMode; + throw cRuntimeError("No mandatory control response mode for %s", mode->getName()); + } + if (controlResponseModes.find(mode) == controlResponseModes.end()) { + // VHT response-format selection remains unchanged; this fallback is HT-scoped. + if (dynamic_cast(mode) != nullptr) + return mode; + if (!mandatory) + return mode; + } + const IIeee80211Mode *result = nullptr; + for (const auto& entry : nonHtControlResponseEntries) { + auto candidate = entry.mode; + if (candidate->getDataMode()->getNetBitrate() <= mode->getDataMode()->getNetBitrate() && + (result == nullptr || candidate->getDataMode()->getNetBitrate() > result->getDataMode()->getNetBitrate())) + result = candidate; + } + if (result == nullptr) + throw cRuntimeError("No mandatory non-HT control response mode for %s", mode->getName()); + return result; +} + const Ieee80211ModeSet *Ieee80211ModeSet::findModeSet(const char *mode) { for (size_t index = 0; index < (&modeSets)->size(); index++) { @@ -871,4 +1287,3 @@ const Ieee80211ModeSet *Ieee80211ModeSet::getModeSet(const char *mode) } // namespace physicallayer } // namespace inet - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h index 0e1a3b6254f..5061b1a827e 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -28,7 +28,6 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject VHT, }; - protected: class INET_API Entry { public: bool isMandatory; @@ -53,17 +52,26 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject std::set htSupportedChannelWidths; std::set htShortGuardIntervalChannelWidths; bool htOperationSupported = false; + // Entries are selectable modes; supportedEntries also contains immutable PHY capabilities needed for mandatory control responses. + const std::vector supportedEntries; + const std::map controlResponseModes; + const std::map htMixedControlResponseModes; + const std::vector nonHtControlResponseEntries; public: - static const DelayedInitializer> modeSets; + static OPP_THREAD_LOCAL const DelayedInitializer> modeSets; protected: + static std::vector completeHtGuardIntervalVariants(const char *name, const std::vector& entries); int findModeIndex(const IIeee80211Mode *mode) const; int getModeIndex(const IIeee80211Mode *mode) const; + static std::map createControlResponseModes(const std::vector& supportedEntries); + static std::map createHtMixedControlResponseModes(const std::vector& supportedEntries); + static std::vector createNonHtControlResponseEntries(const std::vector& supportedEntries); public: Ieee80211ModeSet(const char *name, const std::vector entries, const IIeee80211Mode *referenceMode, - PhyType phyType, bool htOperationSupported = false); + PhyType phyType, bool htOperationSupported = false, const std::vector supportedEntries = {}); virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream << "Ieee80211ModeSet, name = " << name; } @@ -86,12 +94,21 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject const std::set& getHtShortGuardIntervalChannelWidths() const { return htShortGuardIntervalChannelWidths; } bool isHtShortGuardIntervalSupported(Hz bandwidth) const { return htShortGuardIntervalChannelWidths.count(bandwidth) != 0; } + // containsMode() covers entries selectable as the persistent operating mode + // (for example through Ieee80211Transmitter::setMode()). supportsMode() + // additionally covers immutable PHY capabilities that may be selected per + // packet through Ieee80211ModeReq, such as legacy control responses in a + // 2.4 GHz HT profile. bool containsMode(const IIeee80211Mode *mode) const { return findModeIndex(mode) != -1; } + bool supportsMode(const IIeee80211Mode *mode) const; bool getIsMandatory(const IIeee80211Mode *mode) const; // Finds a mode with the same PHY tuple as mode. Unlike findMode(), this // treats an absent guard interval (negative value) as an exact value. const IIeee80211Mode *findCompatibleMode(const IIeee80211Mode *mode) const; + // Pointer lookup is intentionally strict. Use getControlResponseMode() for an explicitly requested response that needs HT-mixed translation. + const IIeee80211Mode *findMode(const IIeee80211Mode *mode) const; + const IIeee80211Mode *getMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *findMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; const IIeee80211Mode *findMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; const IIeee80211Mode *getMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; @@ -106,6 +123,18 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject const IIeee80211Mode *getSlowerMandatoryMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *getFasterMandatoryMode(const IIeee80211Mode *mode) const; + // Automatic responses follow IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7. + // A configured CTS mode must be selectable and, for an HT RTS, must be HT; + // it is translated only to the corresponding HT-mixed response. An explicitly + // configured HT mode is a deliberate override and may bypass those response + // constraints. A legacy configured CTS rate is rejected by rate-selection + // initialization, while this API keeps the direct HT/legacy combination fatal. + const IIeee80211Mode *getControlResponseMode(const IIeee80211Mode *mode, const IIeee80211Mode *configuredMode = nullptr) const; + const IIeee80211Mode *getMandatoryControlResponseMode(const IIeee80211Mode *mode) const; + // 2.4 GHz HT modes are converted to non-HT responses. With mandatory=false, + // the input bitrate is used as a ceiling for selecting that non-HT mode. + const IIeee80211Mode *getNonHtControlResponseMode(const IIeee80211Mode *mode, bool mandatory = true) const; + static const Ieee80211ModeSet *findModeSet(const char *mode); static const Ieee80211ModeSet *getModeSet(const char *mode); @@ -129,4 +158,3 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject } // namespace inet #endif - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc index ea12fd18697..cd3b3eb82e4 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.cc @@ -696,8 +696,8 @@ const Ieee80211VhtMode *Ieee80211VhtCompliantModes::getCompliantMode(const Ieee8 throw cRuntimeError("Unsupported VHT preamble format: only HT_PREAMBLE_MIXED is supported (IEEE Std 802.11-2024, 21.3.2)"); const char *name = ""; // TODO unsigned int nss = mcsMode->getNumNss(); - auto htModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), guardIntervalType, nss, centerFrequencyMode, preambleFormat); - auto mode = singleton.modeCache.find(htModeId); + auto vhtModeId = std::make_tuple(mcsMode->getBandwidth(), mcsMode->getMcsIndex(), guardIntervalType, nss, centerFrequencyMode, preambleFormat); + auto mode = singleton.modeCache.find(vhtModeId); if (mode == singleton.modeCache.end()) { const Ieee80211OfdmSignalMode *legacySignal = nullptr; const Ieee80211VhtSignalMode *htSignal = nullptr; @@ -715,7 +715,7 @@ const Ieee80211VhtMode *Ieee80211VhtCompliantModes::getCompliantMode(const Ieee8 const Ieee80211VhtDataMode *dataMode = new Ieee80211VhtDataMode(mcsMode, mcsMode->getBandwidth(), guardIntervalType); const Ieee80211VhtPreambleMode *preambleMode = new Ieee80211VhtPreambleMode(htSignal, legacySignal, preambleFormat, dataMode->getNumberOfSpatialStreams()); const Ieee80211VhtMode *htMode = new Ieee80211VhtMode(name, preambleMode, dataMode, centerFrequencyMode); - singleton.modeCache.insert(std::pair(htModeId, htMode)); + singleton.modeCache.emplace(vhtModeId, htMode); return htMode; } return mode->second; diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc index 3dd0cb1ee7f..eea8e96af86 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.cc @@ -156,21 +156,23 @@ void Ieee80211Radio::changeModeSet(const Ieee80211ModeSet *modeSet, const IIeee8 // already published to earlier listeners. Keep the reentrancy guard during // publication so every listener observes the same committed mode set. std::exception_ptr observerFailure; - try { - if (modeSet != nullptr) - emit(modesetChangedSignal, const_cast(modeSet)); - } - catch (...) { - observerFailure = std::current_exception(); - } - // Listening changes are independent committed facts: the medium must get - // its publication attempt even when a mode-set observer throws. - try { - emit(listeningChangedSignal, 0); - } - catch (...) { - if (!observerFailure) + if (getComponentType() != nullptr) { + try { + if (modeSet != nullptr) + emit(modesetChangedSignal, const_cast(modeSet)); + } + catch (...) { observerFailure = std::current_exception(); + } + // Listening changes are independent committed facts: the medium must get + // its publication attempt even when a mode-set observer throws. + try { + emit(listeningChangedSignal, 0); + } + catch (...) { + if (!observerFailure) + observerFailure = std::current_exception(); + } } changingModeSet = false; if (observerFailure) @@ -184,7 +186,8 @@ void Ieee80211Radio::setMode(const IIeee80211Mode *mode) ieee80211Transmitter->setMode(mode); EV << "Changing radio mode to " << mode << endl; receptionTimer = nullptr; - emit(listeningChangedSignal, 0); + if (getComponentType() != nullptr) + emit(listeningChangedSignal, 0); } void Ieee80211Radio::setBand(const IIeee80211Band *band) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned index c32061114d3..0ac071271e4 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.ned @@ -25,7 +25,7 @@ import inet.physicallayer.wireless.common.base.packetlevel.FlatRadioBase; module Ieee80211Radio extends FlatRadioBase { parameters: - string opMode @enum("a", "b", "g(erp)", "g(mixed)", "n(mixed-2.4Ghz)", "p", "ac") = default("g(mixed)"); // Operation mode + string opMode @enum("a", "b", "g(erp)", "g(mixed)", "n(mixed-2.4Ghz)", "n(greenfield-2.4Ghz)", "p", "ac") = default("g(mixed)"); // Operation mode string bandName @enum("2.4 GHz", "5 GHz", "5 GHz (20 MHz)", "5 GHz (40 MHz)", "5 GHz (80 MHz)", "5 GHz (160 MHz)", "5.9 GHz") = default("2.4 GHz"); // Band name int channelNumber = default(0); // Initial channel number within the band (TODO this is offset by 1) string fcsMode @enum("declared","computed") = default("declared"); @@ -47,6 +47,6 @@ module Ieee80211Radio extends FlatRadioBase *.channelNumber = this.channelNumber; @class(Ieee80211Radio); @signal[radioChannelChanged](type=long); + @signal[modesetChanged](type=inet::physicallayer::Ieee80211ModeSet); @statistic[radioChannel](title="Radio channel"; source=radioChannelChanged; record=histogram,vector; interpolationmode=sample-hold); } - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc index 4c9d2ecd6d1..ae6804b4a84 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.cc @@ -50,13 +50,13 @@ std::ostream& Ieee80211Receiver::printToStream(std::ostream& stream, int level, bool Ieee80211Receiver::computeIsReceptionPossible(const IListening *listening, const ITransmission *transmission) const { auto ieee80211Transmission = dynamic_cast(transmission); - return ieee80211Transmission && modeSet->containsMode(ieee80211Transmission->getMode()) && NarrowbandReceiverBase::computeIsReceptionPossible(listening, transmission); + return ieee80211Transmission && modeSet->supportsMode(ieee80211Transmission->getMode()) && NarrowbandReceiverBase::computeIsReceptionPossible(listening, transmission); } bool Ieee80211Receiver::computeIsReceptionPossible(const IListening *listening, const IReception *reception, IRadioSignal::SignalPart part) const { auto ieee80211Transmission = dynamic_cast(reception->getTransmission()); - return ieee80211Transmission && modeSet->containsMode(ieee80211Transmission->getMode()) && getAnalogModel()->computeIsReceptionPossible(listening, reception, sensitivity); + return ieee80211Transmission && modeSet->supportsMode(ieee80211Transmission->getMode()) && getAnalogModel()->computeIsReceptionPossible(listening, reception, sensitivity); } const IReceptionResult *Ieee80211Receiver::computeReceptionResult(const IListening *listening, const IReception *reception, const IInterference *interference, const ISnir *snir, const std::vector *decisions) const diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned index c0c19e2afc2..29e9fc5cd80 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Receiver.ned @@ -23,7 +23,7 @@ import inet.physicallayer.wireless.common.base.packetlevel.NarrowbandReceiverBas module Ieee80211Receiver extends NarrowbandReceiverBase { parameters: - string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac"); string bandName @enum("2.4 GHz","5 GHz","5 GHz (20 MHz)","5 GHz (40 MHz)","5 GHz (80 MHz)","5 GHz (160 MHz)","5.9 GHz"); int channelNumber; modulation = default("BPSK"); // TODO this is simply wrong @@ -32,4 +32,3 @@ module Ieee80211Receiver extends NarrowbandReceiverBase *.opMode = this.opMode; @class(Ieee80211Receiver); } - diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index 7011071f1d5..8ab34442799 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -49,7 +49,7 @@ const IIeee80211Mode *Ieee80211Transmitter::computeTransmissionMode(const Packet const auto& modeReq = const_cast(packet)->findTag(); const auto& bitrateReq = const_cast(packet)->findTag(); if (modeReq != nullptr) { - if (modeSet != nullptr && !modeSet->containsMode(modeReq->getMode())) + if (modeSet != nullptr && !modeSet->supportsMode(modeReq->getMode())) throw cRuntimeError("Unsupported mode requested"); transmissionMode = modeReq->getMode(); } @@ -166,9 +166,18 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran const Coord& endPosition = mobility->getCurrentPosition(); const Quaternion& startOrientation = mobility->getCurrentAngularPosition(); const Quaternion& endOrientation = mobility->getCurrentAngularPosition(); - const simtime_t preambleDuration = transmissionMode->getPreambleDuration(); - const simtime_t headerDuration = transmissionMode->getHeaderDuration(); - const simtime_t dataDuration = transmissionMode->getDataDuration(B(phyHeader->getLengthField())); + const simtime_t preambleDuration = transmissionMode->getPreambleMode()->getDuration(); + const simtime_t modeledDataDuration = transmissionMode->getDataMode()->getDuration(B(phyHeader->getLengthField())); + // HT/VHT include their SIG fields in the PHY preamble duration, so their + // mode duration is exactly preamble + modeled data. Other PHYs expose a + // separate header; their residual data interval may also include a trailing + // signal extension (ERP). Keep that extension in chronological data time. + const bool headerIncludedInPreamble = duration == preambleDuration + modeledDataDuration; + const simtime_t headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : transmissionMode->getHeaderMode()->getDuration(); + const simtime_t dataDuration = headerIncludedInPreamble ? modeledDataDuration : duration - headerDuration - preambleDuration; + if (preambleDuration < SIMTIME_ZERO || headerDuration < SIMTIME_ZERO || dataDuration < SIMTIME_ZERO || + preambleDuration + headerDuration + dataDuration != duration) + throw cRuntimeError("Invalid transmission duration decomposition for mode %s", transmissionMode->getName()); auto analogModel = getAnalogModel()->createAnalogModel(preambleDuration, headerDuration, dataDuration, centerFrequency, transmissionBandwidth, transmissionPower); return new Ieee80211Transmission(transmitter, packet, startTime, endTime, preambleDuration, headerDuration, dataDuration, startPosition, endPosition, startOrientation, endOrientation, nullptr, nullptr, nullptr, nullptr, analogModel, transmissionMode, transmissionChannel); } diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned index 3d50ab670e5..9c8b409b751 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.ned @@ -17,7 +17,7 @@ import inet.physicallayer.wireless.common.base.packetlevel.NarrowbandTransmitter module Ieee80211Transmitter extends NarrowbandTransmitterBase { parameters: - string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac"); string bandName @enum("2.4 GHz","5 GHz","5 GHz (20 MHz)","5 GHz (40 MHz)","5 GHz (80 MHz)","5 GHz (160 MHz)","5.9 GHz"); int channelNumber; modulation = default("BPSK"); // TODO: This is simply wrong diff --git a/tests/unit/Ieee80211HtGreenfield_1.test b/tests/unit/Ieee80211HtGreenfield_1.test new file mode 100644 index 00000000000..11a5efa886f --- /dev/null +++ b/tests/unit/Ieee80211HtGreenfield_1.test @@ -0,0 +1,354 @@ +%description: +Checks that the 802.11n mixed and Greenfield mode profiles preserve distinct +preamble formats and timing, retain strict membership, and support the legacy +2.4 GHz HT capabilities used for non-HT control responses. Also checks VHT cache identity. + +%includes: +#include +#include + +#include "inet/common/packet/Packet.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211DsssOfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HrDsssMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; + +class TestIeee80211Transmitter : public Ieee80211Transmitter +{ + public: + const Ieee80211ModeSet *getTestModeSet() const { return modeSet; } + const IIeee80211Mode *getTestMode() const { return mode; } + const IIeee80211Mode *getTestTransmissionMode(const Packet *packet) const { return computeTransmissionMode(packet); } +}; + +static const Ieee80211HtMode *findMcs0LongGiMode(const Ieee80211ModeSet *modeSet) +{ + auto mode = dynamic_cast(modeSet->getMode(Mbps(6.5), MHz(20), 1)); + return mode != nullptr && mode->getDataMode()->getModulationAndCodingScheme()->getMcsIndex() == 0 && + mode->getDataMode()->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG ? mode : nullptr; +} + +static bool rejectsUnknownMode(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) +{ + try { + modeSet->getMode(mode); + return false; + } + catch (const cRuntimeError&) { + return true; + } +} + +static bool rejectsMandatoryLookup(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *mode) +{ + try { + modeSet->getIsMandatory(mode); + return false; + } + catch (const cRuntimeError&) { + return true; + } +} + +static bool rejectsLegacyConfiguredCts(const Ieee80211ModeSet *modeSet, const IIeee80211Mode *htMode, const IIeee80211Mode *legacyMode) +{ + try { + modeSet->getControlResponseMode(htMode, legacyMode); + return false; + } + catch (const cRuntimeError& error) { + return std::string(error.getFormattedMessage()).find("HT RTS requires an HT-mixed CTS response") != std::string::npos; + } +} + +static void assertTransmitterDurationDecomposition(const IIeee80211Mode *mode) +{ + auto duration = mode->getDuration(B(0)); + auto preambleDuration = mode->getPreambleMode()->getDuration(); + auto modeledDataDuration = mode->getDataMode()->getDuration(B(0)); + bool headerIncludedInPreamble = duration == preambleDuration + modeledDataDuration; + auto headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : mode->getHeaderMode()->getDuration(); + auto dataDuration = headerIncludedInPreamble ? modeledDataDuration : duration - headerDuration - preambleDuration; + ASSERT(preambleDuration >= SIMTIME_ZERO); + ASSERT(headerDuration >= SIMTIME_ZERO); + ASSERT(dataDuration >= SIMTIME_ZERO); + ASSERT(preambleDuration + headerDuration + dataDuration == duration); + if (dynamic_cast(mode) != nullptr) + ASSERT(headerDuration == SIMTIME_ZERO); + if (dynamic_cast(mode) != nullptr) + ASSERT(dataDuration > modeledDataDuration); + if (dynamic_cast(mode) != nullptr && dynamic_cast(mode) == nullptr) + ASSERT(headerDuration > SIMTIME_ZERO); +} + +%activity: +auto mixedProfile = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); +auto greenfieldProfile = Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)"); +ASSERT(mixedProfile != nullptr); +ASSERT(greenfieldProfile != nullptr); +ASSERT(mixedProfile == Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")); +ASSERT(greenfieldProfile == Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)")); + +auto mixedMode = findMcs0LongGiMode(mixedProfile); +auto greenfieldMode = findMcs0LongGiMode(greenfieldProfile); +ASSERT(mixedMode != nullptr); +ASSERT(greenfieldMode != nullptr); +ASSERT(mixedMode != greenfieldMode); +ASSERT(mixedMode->getCenterFrequencyMode() == Ieee80211HtMode::BAND_2_4GHZ); +ASSERT(greenfieldMode->getCenterFrequencyMode() == Ieee80211HtMode::BAND_2_4GHZ); +ASSERT(mixedMode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED); +ASSERT(greenfieldMode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD); +ASSERT(mixedMode->getPreambleMode()->getDuration() == SimTime(36, SIMTIME_US)); +ASSERT(greenfieldMode->getPreambleMode()->getDuration() == SimTime(24, SIMTIME_US)); +ASSERT(mixedMode->getDuration(B(0)) == SimTime(40, SIMTIME_US)); +ASSERT(greenfieldMode->getDuration(B(0)) == SimTime(28, SIMTIME_US)); +ASSERT(mixedMode->getDuration(B(0)) - greenfieldMode->getDuration(B(0)) == SimTime(12, SIMTIME_US)); +ASSERT(mixedMode->getDataMode()->getDuration(B(0)) == SimTime(4, SIMTIME_US)); +ASSERT(greenfieldMode->getDataMode()->getDuration(B(0)) == SimTime(4, SIMTIME_US)); +ASSERT(mixedMode->getDuration(B(0)) == mixedMode->getPreambleMode()->getDuration() + mixedMode->getDataMode()->getDuration(B(0))); +ASSERT(greenfieldMode->getDuration(B(0)) == greenfieldMode->getPreambleMode()->getDuration() + greenfieldMode->getDataMode()->getDuration(B(0))); +ASSERT(!mixedProfile->containsMode(greenfieldMode)); +ASSERT(!greenfieldProfile->containsMode(mixedMode)); +ASSERT(mixedProfile->getSlowerMode(greenfieldMode) == nullptr); +ASSERT(greenfieldProfile->getFasterMode(mixedMode) == nullptr); +ASSERT(rejectsMandatoryLookup(mixedProfile, greenfieldMode)); +ASSERT(rejectsMandatoryLookup(greenfieldProfile, mixedMode)); +ASSERT(rejectsUnknownMode(mixedProfile, greenfieldMode)); +ASSERT(rejectsUnknownMode(greenfieldProfile, mixedMode)); +ASSERT(mixedProfile->supportsMode(mixedMode)); +ASSERT(greenfieldProfile->supportsMode(greenfieldMode)); +ASSERT(greenfieldProfile->supportsMode(mixedMode)); +ASSERT(!mixedProfile->supportsMode(greenfieldMode)); +ASSERT(mixedProfile->getSifsTime() == SimTime(10, SIMTIME_US)); +ASSERT(greenfieldProfile->getSifsTime() == SimTime(10, SIMTIME_US)); +ASSERT(mixedProfile->getSlotTime() == SimTime(20, SIMTIME_US)); +ASSERT(greenfieldProfile->getSlotTime() == SimTime(20, SIMTIME_US)); +ASSERT(mixedProfile->getCwMin() == 15); +ASSERT(greenfieldProfile->getCwMin() == 15); +const IIeee80211Mode *mandatoryLegacyModes[] = { + &Ieee80211DsssCompliantModes::dsssMode1Mbps, + &Ieee80211DsssCompliantModes::dsssMode2Mbps, + &Ieee80211HrDsssCompliantModes::hrDsssMode2MbpsShortPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckLongPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckShortPreamble, + &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps, + &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckLongPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckShortPreamble, + &Ieee80211ErpOfdmCompliantModes::erpOfdmMode12Mbps, + &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps, +}; +for (auto legacyMode : mandatoryLegacyModes) { + ASSERT(mixedProfile->supportsMode(legacyMode)); + ASSERT(greenfieldProfile->supportsMode(legacyMode)); +} +const IIeee80211Mode *supplementaryOnlyLegacyModes[] = { + &Ieee80211HrDsssCompliantModes::hrDsssMode2MbpsShortPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckShortPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckShortPreamble, +}; +for (auto legacyMode : supplementaryOnlyLegacyModes) { + ASSERT(!mixedProfile->containsMode(legacyMode)); + ASSERT(!greenfieldProfile->containsMode(legacyMode)); +} +ASSERT(Ieee80211DsssCompliantModes::dsssMode1Mbps.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211DsssCompliantModes::dsssMode2Mbps.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode2MbpsShortPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckLongPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode5_5MbpsCckShortPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckLongPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckShortPreamble.getDataMode()->getBandwidth() == MHz(22)); +ASSERT(mixedProfile->getNonHtControlResponseMode(mixedMode) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(greenfieldProfile->getNonHtControlResponseMode(greenfieldMode) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(greenfieldProfile->getNonHtControlResponseMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode9Mbps) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(mixedProfile->getControlResponseMode(mixedMode) == mixedMode); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMode) == mixedMode); + +auto greenfieldMcs8 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs8BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +ASSERT(greenfieldProfile->supportsMode(greenfieldMcs8)); +// MCS 8 is optional and uses two BPSK 1/2 streams. With no Basic HT-MCS Set +// modelled, 10.6.6.5.3 selects mandatory one-stream MCS 0 for the CTS. +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs8) == mixedMode); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs8, greenfieldMode) == mixedMode); +ASSERT(rejectsLegacyConfiguredCts(greenfieldProfile, greenfieldMode, &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps)); + +auto greenfieldMcs7 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs7BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +auto mixedMcs7 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs7BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +auto greenfieldMcs15 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs15BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs7) == mixedMcs7); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs15) == mixedMcs7); +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs7, greenfieldMode) == mixedMode); + +auto mixedMcs15 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs15BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +auto configuredMixedResponse = dynamic_cast(greenfieldProfile->getControlResponseMode(greenfieldMcs7, greenfieldMcs15)); +ASSERT(configuredMixedResponse == mixedMcs15); +ASSERT(configuredMixedResponse->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED); +ASSERT(configuredMixedResponse->getCenterFrequencyMode() == greenfieldMcs15->getCenterFrequencyMode()); +ASSERT(configuredMixedResponse->getDataMode()->getMcsIndex() == greenfieldMcs15->getDataMode()->getMcsIndex()); +ASSERT(configuredMixedResponse->getDataMode()->getBandwidth() == greenfieldMcs15->getDataMode()->getBandwidth()); +ASSERT(configuredMixedResponse->getDataMode()->getNumberOfSpatialStreams() == greenfieldMcs15->getDataMode()->getNumberOfSpatialStreams()); +ASSERT(configuredMixedResponse->getDataMode()->getGuardIntervalType() == greenfieldMcs15->getDataMode()->getGuardIntervalType()); +ASSERT(mixedProfile->getMandatoryControlResponseMode(mixedMcs7) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps); +ASSERT(mixedProfile->getMandatoryControlResponseMode(mixedMcs15) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps); +ASSERT(mixedProfile->getNonHtControlResponseMode(mixedMcs15, false) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode24Mbps); + +auto greenfieldMcs8Bw40 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs8BW40MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +auto mixedMcs0Bw40 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs0BW40MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); +// The bounded no-Basic-HT-MCS-Set approximation reuses mandatory 20 MHz MCS +// indexes for candidates at the source bandwidth, so optional 40 MHz MCS 8 +// maps to 40 MHz MCS 0 (without changing the established response behavior). +ASSERT(greenfieldProfile->getControlResponseMode(greenfieldMcs8Bw40) == mixedMcs0Bw40); + +auto transmitter = new TestIeee80211Transmitter; +transmitter->setModeSet(greenfieldProfile); +transmitter->setMode(greenfieldMode); +transmitter->setModeSet(mixedProfile); +ASSERT(transmitter->getTestModeSet() == mixedProfile); +ASSERT(transmitter->getTestMode() == mixedMode); +transmitter->setModeSet(greenfieldProfile); +ASSERT(transmitter->getTestModeSet() == greenfieldProfile); +ASSERT(transmitter->getTestMode() == greenfieldMode); +Packet supportedLegacyPacket("supportedLegacy"); +supportedLegacyPacket.addTag()->setMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(transmitter->getTestTransmissionMode(&supportedLegacyPacket) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +transmitter->setModeSet(mixedProfile); +ASSERT(transmitter->getTestTransmissionMode(&supportedLegacyPacket) == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +transmitter->setMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +ASSERT(transmitter->getTestMode() == &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); +transmitter->setMode(mixedMode); +transmitter->setModeSet(greenfieldProfile); +try { + transmitter->setMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); + ASSERT(false); +} +catch (const cRuntimeError&) { + ASSERT(transmitter->getTestMode() == greenfieldMode); +} +try { + transmitter->setMode(mixedMode); + ASSERT(false); +} +catch (const cRuntimeError&) { + ASSERT(transmitter->getTestMode() == greenfieldMode); +} +transmitter->setModeSet(nullptr); +ASSERT(transmitter->getTestModeSet() == nullptr); +ASSERT(transmitter->getTestMode() == nullptr); +delete transmitter; + +auto ofdmProfile = Ieee80211ModeSet::getModeSet("a"); +auto halfRateProfile = Ieee80211ModeSet::getModeSet("p"); +auto ofdm6 = ofdmProfile->getMode(Mbps(6), MHz(20), 1); +transmitter = new TestIeee80211Transmitter; +transmitter->setModeSet(ofdmProfile); +transmitter->setMode(ofdm6); +try { + transmitter->setModeSet(halfRateProfile); + ASSERT(false); +} +catch (const cRuntimeError&) { + ASSERT(transmitter->getTestModeSet() == ofdmProfile); + ASSERT(transmitter->getTestMode() == ofdm6); +} +delete transmitter; + +auto greenfield5GhzMode = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs0BW20MHz, + Ieee80211HtMode::BAND_5GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +ASSERT(greenfield5GhzMode->getCenterFrequencyMode() == Ieee80211HtMode::BAND_5GHZ); +ASSERT(greenfield5GhzMode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD); +ASSERT(!greenfieldProfile->containsMode(greenfield5GhzMode)); +ASSERT(!greenfieldProfile->supportsMode(greenfield5GhzMode)); + +auto vhtMixed5GhzMode = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +bool rejectedVhtGreenfield = false; +try { + Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +} +catch (const cRuntimeError&) { + rejectedVhtGreenfield = true; +} +ASSERT(rejectedVhtGreenfield); +auto vhtMixed2GhzMode = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_2_4GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG); +ASSERT(vhtMixed5GhzMode != vhtMixed2GhzMode); +ASSERT(vhtMixed5GhzMode->getCenterFrequencyMode() == Ieee80211VhtMode::BAND_5GHZ); +ASSERT(vhtMixed2GhzMode->getCenterFrequencyMode() == Ieee80211VhtMode::BAND_2_4GHZ); +ASSERT(vhtMixed5GhzMode->getDuration(B(0)) == vhtMixed5GhzMode->getPreambleMode()->getDuration() + vhtMixed5GhzMode->getDataMode()->getDuration(B(0))); +ASSERT(vhtMixed5GhzMode->getDataMode()->getDuration(B(0)) >= SIMTIME_ZERO); +assertTransmitterDurationDecomposition(ofdmProfile->getMode(Mbps(6))); +assertTransmitterDurationDecomposition(vhtMixed5GhzMode); + +auto erpMixedProfile = Ieee80211ModeSet::getModeSet("g(mixed)"); +auto erpOnlyProfile = Ieee80211ModeSet::getModeSet("g(erp)"); +auto erpMixedMode = &Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps; +auto erpOnlyMode = &Ieee80211ErpOfdmCompliantModes::erpOnlyOfdmMode6Mbps; +Ieee80211DsssOfdmMode dsssOfdmMode("dsss-ofdm", nullptr, nullptr, nullptr, nullptr, &Ieee80211OfdmCompliantModes::ofdmDataMode6MbpsCS20MHz); +ASSERT(erpMixedProfile != nullptr); +ASSERT(erpOnlyProfile != nullptr); +assertTransmitterDurationDecomposition(erpMixedProfile->getMode(Mbps(6))); +ASSERT(erpMixedMode != erpOnlyMode); +ASSERT(erpMixedMode->getSlotTime() == SimTime(20, SIMTIME_US)); +ASSERT(erpOnlyMode->getSlotTime() == SimTime(9, SIMTIME_US)); +ASSERT(!erpMixedProfile->containsMode(erpOnlyMode)); +ASSERT(!erpOnlyProfile->containsMode(erpMixedMode)); +ASSERT(erpMixedProfile->findMode(erpOnlyMode) == nullptr); +ASSERT(erpOnlyProfile->findMode(erpMixedMode) == nullptr); +ASSERT(erpMixedProfile->findMode(&dsssOfdmMode) == nullptr); +ASSERT(rejectsUnknownMode(erpMixedProfile, erpOnlyMode)); +ASSERT(rejectsUnknownMode(erpOnlyProfile, erpMixedMode)); + +EV << "HT mixed and Greenfield modes have distinct cached identities and timing.\n"; + +%contains: stdout +HT mixed and Greenfield modes have distinct cached identities and timing. diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 11e854a6a0c..05f3ca83153 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -37,10 +37,11 @@ class TestIeee80211Receiver : public Ieee80211Receiver class TestIeee80211Radio : public Ieee80211Radio { public: - void setup(ITransmitter *tx, IReceiver *rx) + void setup(Ieee80211Transmitter *tx, Ieee80211Receiver *rx) { transmitter = tx; receiver = rx; + setComponentType(cModuleType::get("inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio")); } }; From 9fa0f00dbdcac4a639648afb2f1cdab6a7196a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 01:48:34 +0200 Subject: [PATCH 16/56] feature(linklayer): implement HT control-response rate selection and Greenfield integration Apply IEEE 802.11-2024 control-response rules in DCF and QoS rate selection: ordinary HT ACK and Basic BlockAck responses use mandatory non-HT rates, and CTS responses to HT-carried RTS frames use the HT-mixed format. Translate configured CTS response rates to their corresponding HT-mixed counterparts while preserving MCS, bandwidth, NSS, and guard interval. Enforce mode set initialization invariants by failing fast if RateSelection has no mode set at link-layer initialization, and rebuild configured modes atomically on dynamic mode-set changes via modesetChangedSignal. Add "n(greenfield-2.4Ghz)" to wireless interface and MAC module NEDs, and provide an end-to-end Greenfield ping simulation example in omnetpp-ht-greenfield.ini. Document backward compatibility notes in WHATSNEW regarding 802.11n control-response rate selection. Add comprehensive unit and module test coverage for rate selection, dynamic mode-set rebinding, and HT Greenfield and Mixed runtime exchanges. --- WHATSNEW | 7 + .../lan80211/omnetpp-ht-greenfield.ini | 37 ++++ .../ieee80211/ExtUpperIeee80211Interface.ned | 3 +- .../ieee80211/Ieee80211Interface.ned | 3 +- .../linklayer/ieee80211/mac/Ieee80211Mac.ned | 3 +- .../mac/rateselection/QosRateSelection.cc | 103 +++++---- .../mac/rateselection/QosRateSelection.h | 2 - .../mac/rateselection/QosRateSelection.ned | 10 + .../mac/rateselection/RateSelection.cc | 74 +++++-- .../mac/rateselection/RateSelection.h | 2 - .../mac/rateselection/RateSelection.ned | 7 + ...e80211ConfiguredResponseRateSelection.test | 206 ++++++++++++++++++ .../module/Ieee80211HtGreenfieldRuntime.test | 132 +++++++++++ tests/module/Ieee80211HtMixedRuntime.test | 131 +++++++++++ .../Ieee80211InvalidConfiguredCtsRate.test | 134 ++++++++++++ .../Ieee80211LegacyResponseRateSelection.test | 110 ++++++++++ ...ateSelectionNullModeSetInitialization.test | 54 +++++ ...ateSelectionNullModeSetInitialization.test | 54 +++++ tests/unit/Ieee80211HtGuardInterval_1.test | 41 ++-- 19 files changed, 1027 insertions(+), 86 deletions(-) create mode 100644 examples/wireless/lan80211/omnetpp-ht-greenfield.ini create mode 100644 tests/module/Ieee80211ConfiguredResponseRateSelection.test create mode 100644 tests/module/Ieee80211HtGreenfieldRuntime.test create mode 100644 tests/module/Ieee80211HtMixedRuntime.test create mode 100644 tests/module/Ieee80211InvalidConfiguredCtsRate.test create mode 100644 tests/module/Ieee80211LegacyResponseRateSelection.test create mode 100644 tests/module/QosRateSelectionNullModeSetInitialization.test create mode 100644 tests/module/RateSelectionNullModeSetInitialization.test diff --git a/WHATSNEW b/WHATSNEW index 5fdd7668423..e98e81ea3c0 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -186,6 +186,13 @@ Notable backward incompatible changes are the following: These changes are backward incompatible for C++ code that directly references the old combined ICMP error indication or tag types. +7. 802.11n control-response rate selection + + Ordinary ACK and Basic BlockAck responses in the n(mixed-2.4Ghz) profile now + use the mandatory non-HT fallback because BSSBasicRateSet is not modelled. + This can change airtime, timeout trajectories, throughput, and fingerprints + in existing simulations. An HT RTS continues to receive an HT-mixed CTS. + Notable backward compatible changes are the following: 1. IPv6 network configurator diff --git a/examples/wireless/lan80211/omnetpp-ht-greenfield.ini b/examples/wireless/lan80211/omnetpp-ht-greenfield.ini new file mode 100644 index 00000000000..9d59b48fcc6 --- /dev/null +++ b/examples/wireless/lan80211/omnetpp-ht-greenfield.ini @@ -0,0 +1,37 @@ +[General] +network = Lan80211 +abstract = true +cmdenv-express-mode = true + +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMinZ = 0m +**.constraintAreaMaxX = 600m +**.constraintAreaMaxY = 400m +**.constraintAreaMaxZ = 0m + +**.arp.typename = "GlobalArp" +**.opMode = "n(greenfield-2.4Ghz)" +**.wlan[*].bitrate = 13Mbps + +*.ap.wlan[*].address = "10:00:00:00:00:00" +*.host[*].**.mgmt.accessPointAddress = "10:00:00:00:00:00" + +*.ap.mobility.typename = "StationaryMobility" +*.ap.mobility.initialX = 250m +*.ap.mobility.initialY = 200m +*.host[*].mobility.typename = "StationaryMobility" +*.host[*].mobility.initialY = 200m +*.host[0].mobility.initialX = 260m +*.host[1].mobility.initialX = 270m + +[Config HtGreenfield] +description = "HT Greenfield mode exchanging ping traffic through an access point" +sim-time-limit = 2s +*.numHosts = 2 +*.host[0].numApps = 0 +*.host[1].numApps = 1 +*.host[1].app[0].typename = "PingApp" +*.host[1].app[0].destAddr = "host[0]" +*.host[1].app[0].sendInterval = 100ms +*.host[1].app[0].printPing = true diff --git a/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned b/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned index 83b958c0f9f..57161c6354f 100644 --- a/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned +++ b/src/inet/emulation/linklayer/ieee80211/ExtUpperIeee80211Interface.ned @@ -27,7 +27,7 @@ module ExtUpperIeee80211Interface extends ExtInterface like IWirelessInterface { parameters: string energySourceModule = default(""); - string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac") = default("g(mixed)"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac") = default("g(mixed)"); double bitrate @unit(bps) = default(-1bps); **.opMode = this.opMode; **.bitrate = this.bitrate; @@ -92,4 +92,3 @@ module ExtUpperIeee80211Interface extends ExtInterface like IWirelessInterface classifier.in <-- { @display("m=n"); } <-- tap.lowerLayerOut; } - diff --git a/src/inet/linklayer/ieee80211/Ieee80211Interface.ned b/src/inet/linklayer/ieee80211/Ieee80211Interface.ned index e32eaba59fc..3534c8d7c18 100644 --- a/src/inet/linklayer/ieee80211/Ieee80211Interface.ned +++ b/src/inet/linklayer/ieee80211/Ieee80211Interface.ned @@ -63,7 +63,7 @@ module Ieee80211Interface extends NetworkInterface like IWirelessInterface parameters: string interfaceTableModule; string energySourceModule = default(""); - string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac") = default("g(mixed)"); + string opMode @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac") = default("g(mixed)"); string address @mutable = default("auto"); // MAC address as hex string (12 hex digits), or // "auto". "auto" values will be replaced by // a generated MAC address in init stage 0. @@ -129,4 +129,3 @@ module Ieee80211Interface extends NetworkInterface like IWirelessInterface classifier.in <-- { @display("m=n"); } <-- upperLayerIn; } - diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned index 51f4198660a..b3ffdfb777e 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.ned @@ -78,7 +78,7 @@ module Ieee80211Mac extends MacProtocolBase like IIeee80211Mac { parameters: string mibModule; - string modeSet @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","p","ac") = default("g(mixed)"); + string modeSet @enum("a","b","g(erp)","g(mixed)","n(mixed-2.4Ghz)","n(greenfield-2.4Ghz)","p","ac") = default("g(mixed)"); string fcsMode @enum("declared","computed") = default("declared"); string initialRadioMode @enum("off","sleep","receiver","transmitter","transceiver") = default("receiver"); @@ -135,4 +135,3 @@ module Ieee80211Mac extends MacProtocolBase like IIeee80211Mac @display("p=250,200"); } } - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 7aa9b3aaec3..b885503275d 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -20,6 +20,23 @@ namespace ieee80211 { using namespace inet::physicallayer; +static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, const char *modulePath) +{ + if (bitrate == -1) + return nullptr; + try { + auto result = modeSet->getMode(bps(bitrate)); + if (modeSet->isHtOperationSupported() && result->getHtMcsIndex() < 0) + throw cRuntimeError("legacy mode '%s' is not selectable for HT CTS responses", result->getName()); + return result; + } + catch (const cRuntimeError& error) { + throw cRuntimeError("%s has invalid responseCtsFrameBitrate=%g bps for operation mode '%s'; " + "the configured CTS rate must resolve to a selectable mode (HT RTS responses require an HT mode): %s", + modulePath, bitrate, modeSet->getName(), error.getFormattedMessage().c_str()); + } +} + Define_Module(QosRateSelection); void QosRateSelection::initialize(int stage) @@ -29,6 +46,8 @@ void QosRateSelection::initialize(int stage) mib.reference(this, "mibModule", true); if (stage == INITSTAGE_LINK_LAYER) { dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); + if (modeSet == nullptr) + throw cRuntimeError("QosRateSelection module %s has no mode set at link-layer initialization", getFullPath().c_str()); updateModes(); } } @@ -59,20 +78,29 @@ void QosRateSelection::updateModes() if (modeSet == nullptr) return; double multicastFrameBitrate = par("multicastFrameBitrate"); - multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); + auto newMulticastFrameMode = multicastFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); + auto newDataFrameMode = dataFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); double mgmtFrameBitrate = par("mgmtFrameBitrate"); - mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); + auto newMgmtFrameMode = mgmtFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); double controlFrameBitrate = par("controlFrameBitrate"); - controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); + auto newControlFrameMode = controlFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); double responseAckFrameBitrate = par("responseAckFrameBitrate"); - responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); double responseBlockAckFrameBitrate = par("responseBlockAckFrameBitrate"); - responseBlockAckFrameMode = (responseBlockAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate)); + auto newResponseBlockAckFrameMode = responseBlockAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate)); double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, getFullPath().c_str()); + auto newFastestMandatoryMode = modeSet->getFastestMandatoryMode(); + + multicastFrameMode = newMulticastFrameMode; + dataFrameMode = newDataFrameMode; + mgmtFrameMode = newMgmtFrameMode; + controlFrameMode = newControlFrameMode; + responseAckFrameMode = newResponseAckFrameMode; + responseBlockAckFrameMode = newResponseBlockAckFrameMode; + responseCtsFrameMode = newResponseCtsFrameMode; + fastestMandatoryMode = newFastestMandatoryMode; lastTransmittedFrameMode.clear(); perReceiverDataFrameMode.clear(); perReceiverResolved = false; @@ -109,36 +137,27 @@ bool QosRateSelection::isControlResponseFrame(const Ptr& dataOrMgmtHeader) { - // TODO BSSBasicRateSet, alternate rate - auto mode = getMode(packet, dataOrMgmtHeader); - ASSERT(modeSet->containsMode(mode)); - const IIeee80211Mode *responseMode; - if (!responseAckFrameMode) { - if (auto mandatoryMode = modeSet->getMandatoryModeAtOrBelow(mode)) - responseMode = mandatoryMode; - else - throw cRuntimeError("Mandatory mode not found"); + const IIeee80211Mode *responseMode = nullptr; + if (responseAckFrameMode) + responseMode = modeSet->getNonHtControlResponseMode(responseAckFrameMode, false); + else { + auto mode = getMode(packet, dataOrMgmtHeader); + ASSERT(modeSet->supportsMode(mode)); + // IEEE 802.11-2024 10.6.6.1/10.6.6.5.2: this bounded model uses a + // mandatory non-HT rate for ordinary ACK responses; BSSBasicRateSet is not modelled. + responseMode = modeSet->getMandatoryControlResponseMode(mode); } - else - responseMode = responseAckFrameMode; - return getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode); + return dataOrMgmtHeader ? getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode) : responseMode; } const IIeee80211Mode *QosRateSelection::computeResponseCtsFrameMode(Packet *packet, const Ptr& rtsFrame) { - // TODO BSSBasicRateSet, alternate rate auto mode = getMode(packet, rtsFrame); - ASSERT(modeSet->containsMode(mode)); - const IIeee80211Mode *responseMode; - if (!responseCtsFrameMode) { - if (auto mandatoryMode = modeSet->getMandatoryModeAtOrBelow(mode)) - responseMode = mandatoryMode; - else - throw cRuntimeError("Mandatory mode not found"); - } - else - responseMode = responseCtsFrameMode; - return getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode); + ASSERT(modeSet->supportsMode(mode)); + // The eliciting mode is required even when a CTS rate is configured because + // the response format and CandidateMCSSet depend on the received PPDU. + auto responseMode = modeSet->getControlResponseMode(mode, responseCtsFrameMode); + return rtsFrame ? getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode) : responseMode; } // @@ -149,12 +168,19 @@ const IIeee80211Mode *QosRateSelection::computeResponseCtsFrameMode(Packet *pack // const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet *packet, const Ptr& blockAckReq) { - if (dynamicPtrCast(blockAckReq)) { - auto mode = responseBlockAckFrameMode ? responseBlockAckFrameMode : getMode(packet, blockAckReq); - return getPeerCompatibleMode(blockAckReq->getTransmitterAddress(), mode); - } - else + if (!dynamicPtrCast(blockAckReq)) throw cRuntimeError("Unknown BlockAckReq frame type"); + const IIeee80211Mode *responseMode = nullptr; + if (responseBlockAckFrameMode) + responseMode = modeSet->getNonHtControlResponseMode(responseBlockAckFrameMode, false); + else { + auto mode = getMode(packet, blockAckReq); + ASSERT(modeSet->supportsMode(mode)); + // IEEE 802.11-2024 10.6.6.5.2 permits non-HT Basic BlockAck responses; + // this model has no BSSBasicRateSet/HT Control context to select another PPDU. + responseMode = modeSet->getNonHtControlResponseMode(mode, false); + } + return blockAckReq ? getPeerCompatibleMode(blockAckReq->getTransmitterAddress(), responseMode) : responseMode; } const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) @@ -288,11 +314,10 @@ const IIeee80211Mode *QosRateSelection::getPeerCompatibleMode(const MacAddress& // supported for HT (802.11n) modes using negotiated PeerHtState. Non-HT // modes (legacy and VHT) return unchanged because VHT capability negotiation // (VHT Capabilities/Operation elements) is not yet modeled in MIB. - if (mode == nullptr || peerAddress.isMulticast() || !mib || mode->getHtMcsIndex() < 0) + if (mode == nullptr || peerAddress.isMulticast() || !mib || mib->mode == Ieee80211Mib::INDEPENDENT || mode->getHtMcsIndex() < 0) return mode; return selectPeerCompatibleMode(modeSet, mib->findPeerHtState(peerAddress), mode, peerAddress); } } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h index 53cb75b94e1..9914fe825b5 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h @@ -63,7 +63,6 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen virtual void initialize(int stage) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; virtual void updateModes(); - // Builds perReceiverDataFrameMode on first use. Deferred out of initialize() because peer // MAC addresses are assigned during INITSTAGE_LINK_LAYER with undefined intra-stage module // ordering; the first transmitted data frame occurs after all init stages, so this is race-free. @@ -96,4 +95,3 @@ class INET_API QosRateSelection : public IQosRateSelection, public ModeSetListen } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned index 88ec4706b60..e3fb6369c95 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned @@ -27,8 +27,18 @@ simple QosRateSelection extends SimpleModule double multicastFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, + // a configured HT bitrate is an upper bound mapped to the highest mandatory + // non-HT rate at or below it; it does not select an exact HT ACK PPDU. double responseAckFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, + // a configured HT bitrate is an upper bound mapped to the highest mandatory + // non-HT rate at or below it; it does not select an exact HT BlockAck PPDU. double responseBlockAckFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. A configured value must resolve to a + // selectable mode; for an HT RTS it must be HT and is translated to the corresponding HT-mixed + // CTS while preserving MCS, bandwidth, NSS, and GI. An explicitly configured HT value is a + // deliberate override and may bypass IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7 response constraints. double responseCtsFrameBitrate @unit(bps) = default(-1bps); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index e2ef635a78e..6094d101bd6 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -24,6 +24,23 @@ namespace ieee80211 { using namespace inet::physicallayer; +static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, const char *modulePath) +{ + if (bitrate == -1) + return nullptr; + try { + auto result = modeSet->getMode(bps(bitrate)); + if (modeSet->isHtOperationSupported() && result->getHtMcsIndex() < 0) + throw cRuntimeError("legacy mode '%s' is not selectable for HT CTS responses", result->getName()); + return result; + } + catch (const cRuntimeError& error) { + throw cRuntimeError("%s has invalid responseCtsFrameBitrate=%g bps for operation mode '%s'; " + "the configured CTS rate must resolve to a selectable mode (HT RTS responses require an HT mode): %s", + modulePath, bitrate, modeSet->getName(), error.getFormattedMessage().c_str()); + } +} + Define_Module(RateSelection); void RateSelection::initialize(int stage) @@ -34,6 +51,8 @@ void RateSelection::initialize(int stage) } else if (stage == INITSTAGE_LINK_LAYER) { dataOrMgmtRateControl = dynamic_cast(findModuleByPath(par("rateControlModule"))); + if (modeSet == nullptr) + throw cRuntimeError("RateSelection module %s has no mode set at link-layer initialization", getFullPath().c_str()); updateModes(); // WATCH(dataOrMgmtRateControl); @@ -79,18 +98,28 @@ void RateSelection::updateModes() if (modeSet == nullptr) return; double multicastFrameBitrate = par("multicastFrameBitrate"); - multicastFrameMode = (multicastFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); + auto newMulticastFrameMode = multicastFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(multicastFrameBitrate)); double dataFrameBitrate = par("dataFrameBitrate"); - dataFrameMode = (dataFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); + auto newDataFrameMode = dataFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(dataFrameBitrate), Hz(par("dataFrameBandwidth")), par("dataFrameNumSpatialStreams"), par("dataFrameGuardInterval")); double mgmtFrameBitrate = par("mgmtFrameBitrate"); - mgmtFrameMode = (mgmtFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); + auto newMgmtFrameMode = mgmtFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(mgmtFrameBitrate)); double controlFrameBitrate = par("controlFrameBitrate"); - controlFrameMode = (controlFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); + auto newControlFrameMode = controlFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); double responseAckFrameBitrate = par("responseAckFrameBitrate"); - responseAckFrameMode = (responseAckFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - responseCtsFrameMode = (responseCtsFrameBitrate == -1) ? nullptr : modeSet->getMode(bps(responseCtsFrameBitrate)); - fastestMandatoryMode = modeSet->getFastestMandatoryMode(); + auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, getFullPath().c_str()); + auto newFastestMandatoryMode = modeSet->getFastestMandatoryMode(); + + // Commit only after every configured mode has been resolved, so a failed + // runtime operation-mode change leaves the previous state intact. + multicastFrameMode = newMulticastFrameMode; + dataFrameMode = newDataFrameMode; + mgmtFrameMode = newMgmtFrameMode; + controlFrameMode = newControlFrameMode; + responseAckFrameMode = newResponseAckFrameMode; + responseCtsFrameMode = newResponseCtsFrameMode; + fastestMandatoryMode = newFastestMandatoryMode; lastTransmittedFrameMode.clear(); perReceiverDataFrameMode.clear(); perReceiverResolved = false; @@ -115,26 +144,30 @@ const IIeee80211Mode *RateSelection::getMode(Packet *packet, const Ptr& dataOrMgmtHeader) { + // Keep configured responses independent of the eliciting packet; callers may + // resolve a configured response while constructing a test packet. + const IIeee80211Mode *responseMode = nullptr; if (responseAckFrameMode) - return getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseAckFrameMode); + responseMode = modeSet->getNonHtControlResponseMode(responseAckFrameMode, false); else { auto mode = getMode(packet, dataOrMgmtHeader); - ASSERT(modeSet->containsMode(mode)); - auto responseMode = modeSet->getMandatoryModeAtOrBelow(mode); // TODO BSSBasicRateSet - return getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode); + ASSERT(modeSet->supportsMode(mode)); + // IEEE 802.11-2024 10.6.6.1/10.6.6.5.2: this bounded model uses a + // mandatory non-HT rate for ordinary ACK responses; BSSBasicRateSet is not modelled. + responseMode = modeSet->getMandatoryControlResponseMode(mode); } + return dataOrMgmtHeader ? getPeerCompatibleMode(dataOrMgmtHeader->getTransmitterAddress(), responseMode) : responseMode; } const IIeee80211Mode *RateSelection::computeResponseCtsFrameMode(Packet *packet, const Ptr& rtsFrame) { - if (responseCtsFrameMode) - return getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseCtsFrameMode); - else { - auto mode = getMode(packet, rtsFrame); - ASSERT(modeSet->containsMode(mode)); - auto responseMode = modeSet->getMandatoryModeAtOrBelow(mode); // TODO BSSBasicRateSet - return getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode); - } + auto mode = getMode(packet, rtsFrame); + ASSERT(modeSet->supportsMode(mode)); + // IEEE 802.11-2024 10.6.6.1 and 10.6.6.5.7 require an HT response to an + // HT RTS and forbid HT-GF for the response. Consequently, even a configured + // CTS rate needs the eliciting PPDU's mode tag to select a legal format/MCS. + auto responseMode = modeSet->getControlResponseMode(mode, responseCtsFrameMode); + return rtsFrame ? getPeerCompatibleMode(rtsFrame->getTransmitterAddress(), responseMode) : responseMode; } // 802.11-1999 Std. @@ -255,11 +288,10 @@ const IIeee80211Mode *RateSelection::getPeerCompatibleMode(const MacAddress& pee // supported for HT (802.11n) modes using negotiated PeerHtState. Non-HT // modes (legacy and VHT) return unchanged because VHT capability negotiation // (VHT Capabilities/Operation elements) is not yet modeled in MIB. - if (mode == nullptr || peerAddress.isMulticast() || !mib || mode->getHtMcsIndex() < 0) + if (mode == nullptr || peerAddress.isMulticast() || !mib || mib->mode == Ieee80211Mib::INDEPENDENT || mode->getHtMcsIndex() < 0) return mode; return selectPeerCompatibleMode(modeSet, mib->findPeerHtState(peerAddress), mode, peerAddress); } } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h index 32294eae93c..9a3ba460e44 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.h @@ -66,7 +66,6 @@ class INET_API RateSelection : public IRateSelection, public SimpleModule, publi virtual void initialize(int stage) override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; virtual void updateModes(); - // Builds perReceiverDataFrameMode on first use. Deferred out of initialize() because peer // MAC addresses are assigned during INITSTAGE_LINK_LAYER with undefined intra-stage module // ordering; the first transmitted data frame occurs after all init stages, so this is race-free. @@ -107,4 +106,3 @@ class INET_API RateSelection : public IRateSelection, public SimpleModule, publi } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned index 82938a1df50..60ae6786a07 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned @@ -23,7 +23,14 @@ simple RateSelection extends SimpleModule like IRateSelection double multicastFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, + // a configured HT bitrate is an upper bound mapped to the highest mandatory + // non-HT rate at or below it; it does not select an exact HT ACK PPDU. double responseAckFrameBitrate @unit(bps) = default(-1bps); + // -1 selects the standard-derived automatic response. A configured value must resolve to a + // selectable mode; for an HT RTS it must be HT and is translated to the corresponding HT-mixed + // CTS while preserving MCS, bandwidth, NSS, and GI. An explicitly configured HT value is a + // deliberate override and may bypass IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7 response constraints. double responseCtsFrameBitrate @unit(bps) = default(-1bps); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest diff --git a/tests/module/Ieee80211ConfiguredResponseRateSelection.test b/tests/module/Ieee80211ConfiguredResponseRateSelection.test new file mode 100644 index 00000000000..e8212250c11 --- /dev/null +++ b/tests/module/Ieee80211ConfiguredResponseRateSelection.test @@ -0,0 +1,206 @@ +%description: +Checks through the public rate-selection API that 2.4 GHz HT ACK/BlockAck responses use +non-HT rates and configured HT CTS rates use exact HT-mixed counterparts. + +%file: Test.cc +#include +#include + +#include "inet/common/InitStages.h" +#include "inet/common/Simsignals.h" +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211ConfiguredResponseRateSelection { + +static void assertHtMixedResponse(const char *name, const IIeee80211Mode *mode) +{ + auto htMode = dynamic_cast(mode); + if (htMode == nullptr || htMode->getPreambleMode()->getPreambleFormat() != Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED || htMode->getDataMode()->getNetBitrate() != Mbps(6.5)) + throw cRuntimeError("%s did not return HT-mixed 6.5 Mbps (actual mode: %s, bitrate: %g)", name, mode->getName(), mode->getDataMode()->getNetBitrate().get()); +} + +static void assertNonHtResponse(const char *name, const IIeee80211Mode *mode) +{ + if (dynamic_cast(mode) != nullptr || mode->getDataMode()->getNetBitrate() != Mbps(6)) + throw cRuntimeError("%s did not return non-HT 6 Mbps", name); +} + +class ConfiguredResponseProbe : public cSimpleModule +{ + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + + virtual void initialize(int stage) override + { + if (stage != INITSTAGE_LAST) + return; + + auto dcfRateSelection = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[0].wlan[0].mac.dcf.rateSelection")); + auto qosRateSelection = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[0].wlan[0].mac.hcf.rateSelection")); + auto dynamicQosRateSelection = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[1].wlan[0].mac.hcf.rateSelection")); + auto dynamicWlan = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[1].wlan[0]")); + auto dynamicMac = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[1].wlan[0].mac")); + if (strcmp(dynamicWlan->par("opMode"), "n(mixed-2.4Ghz)") != 0 || strcmp(dynamicMac->par("modeSet"), "n(mixed-2.4Ghz)") != 0) + throw cRuntimeError("Dynamic response probe is not using the mixed profile (wlan: %s, mac: %s)", dynamicWlan->par("opMode").stringValue(), dynamicMac->par("modeSet").stringValue()); + + if (double(dcfRateSelection->par("responseAckFrameBitrate")) != 6.5e6 + || double(qosRateSelection->par("responseAckFrameBitrate")) != 6.5e6) + throw cRuntimeError("Configured response bitrate was not applied to both rate selections"); + + auto sourceMode = Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)")->getMode(Mbps(6.5)); + auto mixedSourceMode = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")->getMode(Mbps(6.5)); + Packet packet("receivedFrame"); + packet.addTag()->setMode(sourceMode); + Packet mixedPacket("receivedMixedFrame"); + mixedPacket.addTag()->setMode(mixedSourceMode); + Packet nonHtPacket("receivedNonHtFrame"); + nonHtPacket.addTag()->setMode(&Ieee80211ErpOfdmCompliantModes::erpOfdmMode6Mbps); + auto dataHeader = makeShared(); + dataHeader->setReceiverAddress(MacAddress("10:00:00:00:00:00")); + Packet dataPacket("configuredDataFrame"); + + ASSERT(dcfRateSelection->computeMode(&dataPacket, dataHeader) == sourceMode); + ASSERT(qosRateSelection->computeMode(&dataPacket, dataHeader, nullptr) == sourceMode); + + assertNonHtResponse("DCF ACK", dcfRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + Packet untaggedPacket("untaggedRts"); + try { + dcfRateSelection->computeResponseCtsFrameMode(&untaggedPacket, nullptr); + throw cRuntimeError("Configured DCF CTS unexpectedly accepted an untagged eliciting packet"); + } + catch (const cRuntimeError& error) { + if (error.getFormattedMessage().find("Missing mode") == std::string::npos) + throw; + } + try { + qosRateSelection->computeResponseCtsFrameMode(&untaggedPacket, nullptr); + throw cRuntimeError("Configured QoS CTS unexpectedly accepted an untagged eliciting packet"); + } + catch (const cRuntimeError& error) { + if (error.getFormattedMessage().find("Missing mode") == std::string::npos) + throw; + } + assertHtMixedResponse("DCF CTS HT", dcfRateSelection->computeResponseCtsFrameMode(&packet, nullptr)); + assertNonHtResponse("DCF CTS non-HT", dcfRateSelection->computeResponseCtsFrameMode(&nonHtPacket, nullptr)); + assertNonHtResponse("QoS ACK", qosRateSelection->computeResponseAckFrameMode(&packet, nullptr)); + assertHtMixedResponse("QoS CTS HT", qosRateSelection->computeResponseCtsFrameMode(&packet, nullptr)); + assertNonHtResponse("QoS CTS non-HT", qosRateSelection->computeResponseCtsFrameMode(&nonHtPacket, nullptr)); + + auto basicBlockAckReq = makeShared(); + assertNonHtResponse("QoS Basic BlockAck", qosRateSelection->computeResponseBlockAckFrameMode(&packet, basicBlockAckReq)); + assertNonHtResponse("Dynamic mixed QoS ACK", dynamicQosRateSelection->computeResponseAckFrameMode(&mixedPacket, nullptr)); + assertHtMixedResponse("Dynamic mixed QoS CTS", dynamicQosRateSelection->computeResponseCtsFrameMode(&mixedPacket, nullptr)); + assertNonHtResponse("Dynamic mixed QoS Basic BlockAck", dynamicQosRateSelection->computeResponseBlockAckFrameMode(&mixedPacket, basicBlockAckReq)); + auto optionalHtMode = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs8BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT); + Packet optionalHtPacket("receivedOptionalHtFrame"); + optionalHtPacket.addTag()->setMode(optionalHtMode); + assertHtMixedResponse("Configured CTS optional HT MCS", qosRateSelection->computeResponseCtsFrameMode(&optionalHtPacket, nullptr)); + auto greenfieldMcs7 = Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs7BW20MHz, + Ieee80211HtMode::BAND_2_4GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD, + Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); + Packet greenfieldMcs7Packet("receivedGreenfieldMcs7Frame"); + greenfieldMcs7Packet.addTag()->setMode(greenfieldMcs7); + assertHtMixedResponse("Configured DCF CTS MCS 0 override for MCS 7", dcfRateSelection->computeResponseCtsFrameMode(&greenfieldMcs7Packet, nullptr)); + assertHtMixedResponse("Configured CTS MCS 0 override for MCS 7", qosRateSelection->computeResponseCtsFrameMode(&greenfieldMcs7Packet, nullptr)); + + // Exercise the actual signal path with a compatible mode-set transition. + // The configured data cache must bind to the exact profile object while + // the configured response operations remain usable after each refresh. + auto wlan = check_and_cast(getSimulation()->getModuleByPath("TestConfiguredResponses.host[0].wlan[0]")); + wlan->emit(modesetChangedSignal, const_cast(Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"))); + ASSERT(dcfRateSelection->computeMode(&dataPacket, dataHeader) == mixedSourceMode); + ASSERT(qosRateSelection->computeMode(&dataPacket, dataHeader, nullptr) == mixedSourceMode); + assertHtMixedResponse("Rebound DCF CTS mixed", dcfRateSelection->computeResponseCtsFrameMode(&mixedPacket, nullptr)); + assertHtMixedResponse("Rebound QoS CTS mixed", qosRateSelection->computeResponseCtsFrameMode(&mixedPacket, nullptr)); + assertNonHtResponse("Rebound DCF ACK mixed", dcfRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + assertNonHtResponse("Rebound QoS ACK mixed", qosRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + assertNonHtResponse("Rebound QoS BlockAck mixed", qosRateSelection->computeResponseBlockAckFrameMode(nullptr, basicBlockAckReq)); + wlan->emit(modesetChangedSignal, const_cast(Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)"))); + ASSERT(dcfRateSelection->computeMode(&dataPacket, dataHeader) == sourceMode); + ASSERT(qosRateSelection->computeMode(&dataPacket, dataHeader, nullptr) == sourceMode); + assertHtMixedResponse("Rebound DCF CTS Greenfield", dcfRateSelection->computeResponseCtsFrameMode(&packet, nullptr)); + assertHtMixedResponse("Rebound QoS CTS Greenfield", qosRateSelection->computeResponseCtsFrameMode(&packet, nullptr)); + assertNonHtResponse("Rebound DCF ACK Greenfield", dcfRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + assertNonHtResponse("Rebound QoS ACK Greenfield", qosRateSelection->computeResponseAckFrameMode(nullptr, nullptr)); + assertNonHtResponse("Rebound QoS BlockAck Greenfield", qosRateSelection->computeResponseBlockAckFrameMode(nullptr, basicBlockAckReq)); + + std::cout << "Mixed and Greenfield ACK/Basic BlockAck use non-HT 6 Mbps; configured HT CTS uses the exact HT-mixed MCS override and requires an eliciting mode tag.\n"; + } +}; + +Define_Module(ConfiguredResponseProbe); + +} // namespace Ieee80211ConfiguredResponseRateSelection + +%file: test.ned + +import inet.examples.wireless.lan80211.Lan80211; + +simple ConfiguredResponseProbe +{ + @class(ConfiguredResponseProbe); +} + +network TestConfiguredResponses extends Lan80211 +{ + parameters: + numHosts = default(2); + submodules: + configuredResponseProbe: ConfiguredResponseProbe; +} + +%inifile: omnetpp.ini + +[General] +include ../../../../examples/wireless/lan80211/omnetpp-ht-greenfield.ini +network = TestConfiguredResponses +abstract = false +ned-path = .;../../../../src;../../../../examples;../../lib +cmdenv-express-mode = true +record-vector-results = false +record-eventlog = false +seed-set = 0 + +[Config ConfiguredResponses] +abstract = false +network = TestConfiguredResponses +sim-time-limit = 1s +*.host[1].wlan[*].opMode = "n(mixed-2.4Ghz)" +**.opMode = "n(greenfield-2.4Ghz)" +**.qosStation = true +*.host[1].wlan[0].mac.hcf.rateSelection.responseAckFrameBitrate = -1bps +*.host[1].wlan[0].mac.hcf.rateSelection.responseCtsFrameBitrate = -1bps +*.host[1].wlan[0].mac.hcf.rateSelection.responseBlockAckFrameBitrate = -1bps +**.wlan[*].mac.dcf.rateSelection.responseAckFrameBitrate = 6.5Mbps +**.wlan[*].mac.dcf.rateSelection.responseCtsFrameBitrate = 6.5Mbps +**.wlan[*].mac.hcf.rateSelection.responseAckFrameBitrate = 6.5Mbps +**.wlan[*].mac.hcf.rateSelection.responseCtsFrameBitrate = 6.5Mbps +**.wlan[*].mac.hcf.rateSelection.responseBlockAckFrameBitrate = 6.5Mbps +*.host[0].wlan[0].mac.dcf.rateSelection.dataFrameBitrate = 6.5Mbps +*.host[0].wlan[0].mac.dcf.rateSelection.dataFrameBandwidth = 20MHz +*.host[0].wlan[0].mac.dcf.rateSelection.dataFrameNumSpatialStreams = 1 +*.host[0].wlan[0].mac.hcf.rateSelection.dataFrameBitrate = 6.5Mbps +*.host[0].wlan[0].mac.hcf.rateSelection.dataFrameBandwidth = 20MHz +*.host[0].wlan[0].mac.hcf.rateSelection.dataFrameNumSpatialStreams = 1 + +%extraargs: -c ConfiguredResponses + +%contains: stdout +Mixed and Greenfield ACK/Basic BlockAck use non-HT 6 Mbps; configured HT CTS uses the exact HT-mixed MCS override and requires an eliciting mode tag. diff --git a/tests/module/Ieee80211HtGreenfieldRuntime.test b/tests/module/Ieee80211HtGreenfieldRuntime.test new file mode 100644 index 00000000000..60d013e74f3 --- /dev/null +++ b/tests/module/Ieee80211HtGreenfieldRuntime.test @@ -0,0 +1,132 @@ +%description: + +Checks forced HT-Greenfield RTS/HT-mixed CTS plus unicast HT-Greenfield data and non-HT ACK responses between two ad hoc stations. + +%file: Test.cc +#include + +#include "inet/common/Simsignals.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" + +using namespace inet; +using namespace inet::physicallayer; +using namespace inet::ieee80211; + +namespace Ieee80211HtGreenfieldRuntime { + +static OPP_THREAD_LOCAL int htGreenfieldRtsCount = 0; +static OPP_THREAD_LOCAL int htMixedCtsCount = 0; +static OPP_THREAD_LOCAL int htGreenfieldDataCount = 0; +static OPP_THREAD_LOCAL int nonHtAckCount = 0; + +class HtPreambleCheckingRadio : public Ieee80211Radio, public cListener +{ + protected: + virtual void initialize(int stage) override + { + Ieee80211Radio::initialize(stage); + if (stage == INITSTAGE_LOCAL) + subscribe(transmissionStartedSignal, this); + } + + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override + { + if (signalID != transmissionStartedSignal) + return; + auto transmission = check_and_cast(obj); + auto mode = dynamic_cast(transmission->getMode()); + auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(transmission->getPacket()); + if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || + transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) + throw cRuntimeError("Invalid preamble/header/data duration decomposition"); + if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); + auto isHtGreenfield = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD; + auto isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; + if (dynamicPtrCast(macHeader) && isHtGreenfield) + htGreenfieldRtsCount++; + if (dynamicPtrCast(macHeader) && isHtMixed) + htMixedCtsCount++; + if (dynamicPtrCast(macHeader) && isHtGreenfield) + htGreenfieldDataCount++; + if (dynamicPtrCast(macHeader) && mode == nullptr) + nonHtAckCount++; + } + + virtual void finish() override + { + Ieee80211Radio::finish(); + if (htGreenfieldRtsCount == 0 || htMixedCtsCount == 0 || htGreenfieldDataCount == 0 || nonHtAckCount == 0) + throw cRuntimeError("Expected HT-GF RTS, HT-MF CTS, HT-GF data, and non-HT ACK transmissions"); + std::cout << "Observed " << htGreenfieldRtsCount << " HT-Greenfield RTS, " << htMixedCtsCount << " HT-mixed CTS, " << htGreenfieldDataCount << " HT-Greenfield data, and " << nonHtAckCount << " non-HT ACK response(s).\n"; + } +}; + +Define_Module(HtPreambleCheckingRadio); + +} // namespace Ieee80211HtGreenfieldRuntime + +%file: test.ned + +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; + +module HtPreambleCheckingRadio extends Ieee80211Radio +{ + parameters: + @class(HtPreambleCheckingRadio); +} + +network TestHtGreenfield +{ + parameters: + int numHosts = default(2); + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host[numHosts]: AdhocHost; +} + +%inifile: omnetpp.ini + +[General] +include ../../../../examples/wireless/lan80211/omnetpp-ht-greenfield.ini +network = TestHtGreenfield +abstract = false +ned-path = .;../../../../src;../../../../examples;../../lib +cmdenv-express-mode = true +record-vector-results = false +record-eventlog = false +seed-set = 0 + +**.wlan[*].radio.typename = "HtPreambleCheckingRadio" + +[Config HtPreambleRuntime] +abstract = false +network = TestHtGreenfield +sim-time-limit = 2s +*.numHosts = 2 +*.host[0].numApps = 1 +*.host[0].app[0].typename = "UdpBasicApp" +*.host[0].app[0].destAddresses = "host[1]" +*.host[0].app[0].destPort = 1000 +*.host[0].app[0].messageLength = 100B +*.host[0].app[0].startTime = 100ms +*.host[1].numApps = 1 +*.host[1].app[0].typename = "UdpSink" +*.host[1].app[0].localPort = 1000 +*.host[0].app[0].sendInterval = 100ms +**.wlan[*].opMode = "n(greenfield-2.4Ghz)" +**.wlan[*].mac.dcf.rtsPolicy.rtsThreshold = 1B + +%extraargs: -c HtPreambleRuntime + +%contains-regex: stdout +Observed [1-9][0-9]* HT-Greenfield RTS, [1-9][0-9]* HT-mixed CTS, [1-9][0-9]* HT-Greenfield data, and [1-9][0-9]* non-HT ACK response\(s\). diff --git a/tests/module/Ieee80211HtMixedRuntime.test b/tests/module/Ieee80211HtMixedRuntime.test new file mode 100644 index 00000000000..cd81007886e --- /dev/null +++ b/tests/module/Ieee80211HtMixedRuntime.test @@ -0,0 +1,131 @@ +%description: +Checks a deterministic n(mixed-2.4Ghz) exchange with HT-mixed RTS/CTS and data +followed by an ERP-OFDM ACK. Basic BlockAck response selection is covered by +the focused public QoS API test because enabling a bounded BAR exchange would +require additional association and agreement setup. + +%file: Test.cc +#include + +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ErpOfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" + +using namespace inet; +using namespace inet::physicallayer; +using namespace inet::ieee80211; + +namespace Ieee80211HtMixedRuntime { + +static OPP_THREAD_LOCAL int htMixedRtsCount = 0; +static OPP_THREAD_LOCAL int htMixedCtsCount = 0; +static OPP_THREAD_LOCAL int htMixedDataCount = 0; +static OPP_THREAD_LOCAL int erpOfdmAckCount = 0; + +class HtMixedCheckingRadio : public Ieee80211Radio, public cListener +{ + protected: + virtual void initialize(int stage) override + { + Ieee80211Radio::initialize(stage); + if (stage == INITSTAGE_LOCAL) + subscribe(transmissionStartedSignal, this); + } + + virtual void receiveSignal(cComponent *, simsignal_t signalID, cObject *obj, cObject *) override + { + if (signalID != transmissionStartedSignal) + return; + auto transmission = check_and_cast(obj); + auto mode = dynamic_cast(transmission->getMode()); + auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(transmission->getPacket()); + if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || + transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) + throw cRuntimeError("Invalid preamble/header/data duration decomposition"); + if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); + bool isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; + if (dynamicPtrCast(macHeader) && isHtMixed) + htMixedRtsCount++; + if (dynamicPtrCast(macHeader) && isHtMixed) + htMixedCtsCount++; + if (dynamicPtrCast(macHeader) && isHtMixed) + htMixedDataCount++; + if (dynamicPtrCast(macHeader) && dynamic_cast(transmission->getMode()) != nullptr) { + if (transmission->getMode()->getDataMode()->getBandwidth() != MHz(20)) + throw cRuntimeError("ERP-OFDM ACK did not use the 20 MHz channel bandwidth"); + erpOfdmAckCount++; + } + } + + virtual void finish() override + { + Ieee80211Radio::finish(); + if (htMixedRtsCount == 0 || htMixedCtsCount == 0 || htMixedDataCount == 0 || erpOfdmAckCount == 0) + throw cRuntimeError("Expected HT-mixed RTS/CTS and data with ERP-OFDM ACK response"); + std::cout << "Observed " << htMixedRtsCount << " HT-mixed RTS, " << htMixedCtsCount << " HT-mixed CTS, " << htMixedDataCount << " HT-mixed data, and " << erpOfdmAckCount << " ERP-OFDM ACK response(s).\n"; + } +}; + +Define_Module(HtMixedCheckingRadio); + +} // namespace Ieee80211HtMixedRuntime + +%file: test.ned +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +module HtMixedCheckingRadio extends Ieee80211Radio +{ + parameters: + @class(HtMixedCheckingRadio); +} + +network TestHtMixedRuntime +{ + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host[2]: AdhocHost; +} + +%inifile: omnetpp.ini +[General] +include ../../../../examples/wireless/lan80211/omnetpp-ht-greenfield.ini +network = TestHtMixedRuntime +abstract = false +ned-path = .;../../../../src;../../../../examples;../../lib +cmdenv-express-mode = true +record-vector-results = false +record-eventlog = false +seed-set = 0 +**.wlan[*].radio.typename = "HtMixedCheckingRadio" + +[Config HtMixedRuntime] +abstract = false +network = TestHtMixedRuntime +sim-time-limit = 2s +*.host[0].numApps = 1 +*.host[0].app[0].typename = "UdpBasicApp" +*.host[0].app[0].destAddresses = "host[1]" +*.host[0].app[0].destPort = 1000 +*.host[0].app[0].messageLength = 100B +*.host[0].app[0].startTime = 100ms +*.host[0].app[0].sendInterval = 100ms +*.host[1].numApps = 1 +*.host[1].app[0].typename = "UdpSink" +*.host[1].app[0].localPort = 1000 +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].mac.dcf.rtsPolicy.rtsThreshold = 1B + +%extraargs: -c HtMixedRuntime + +%contains-regex: stdout +Observed [1-9][0-9]* HT-mixed RTS, [1-9][0-9]* HT-mixed CTS, [1-9][0-9]* HT-mixed data, and [1-9][0-9]* ERP-OFDM ACK response\(s\). diff --git a/tests/module/Ieee80211InvalidConfiguredCtsRate.test b/tests/module/Ieee80211InvalidConfiguredCtsRate.test new file mode 100644 index 00000000000..294f3939aa0 --- /dev/null +++ b/tests/module/Ieee80211InvalidConfiguredCtsRate.test @@ -0,0 +1,134 @@ +%description: +Checks that a non-selectable legacy responseCtsFrameBitrate is rejected during +rate-selection resolution for both DCF and QoS selections and both 2.4 GHz HT profiles. + +%file: Test.cc +#include +#include +#include + +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211InvalidConfiguredCtsRate { + +template +static void checkRejected(const char *selectorName, Selector& selector) +{ + double bitrate = selector.par("responseCtsFrameBitrate"); + char formattedBitrate[64]; + std::snprintf(formattedBitrate, sizeof(formattedBitrate), "%g", bitrate); + for (const char *profile : {"n(mixed-2.4Ghz)", "n(greenfield-2.4Ghz)"}) { + bool rejected = false; + try { + selector.resolveConfiguredModesForTest(Ieee80211ModeSet::getModeSet(profile)); + } + catch (const cRuntimeError& error) { + rejected = true; + auto message = error.getFormattedMessage(); + if (message.find(std::string("responseCtsFrameBitrate=") + formattedBitrate) == std::string::npos || + message.find("operation mode '" + std::string(profile) + "'") == std::string::npos || + message.find("selectable mode") == std::string::npos) + throw cRuntimeError("%s diagnostic for %s at %g bps is incomplete: %s", selectorName, profile, bitrate, message.c_str()); + } + if (!rejected) + throw cRuntimeError("%s accepted legacy CTS bitrate %g bps in %s", selectorName, bitrate, profile); + } +} + +class TestRateSelection : public RateSelection +{ + public: + void resolveConfiguredModesForTest(const Ieee80211ModeSet *modeSet) { applyModeSet(modeSet); } + + protected: + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LINK_LAYER) { + checkRejected("DCF RateSelection", *this); + std::cout << "DCF rejected configured legacy responseCtsFrameBitrate for both HT profiles.\n"; + } + } +}; + +class TestQosRateSelection : public QosRateSelection +{ + public: + void resolveConfiguredModesForTest(const Ieee80211ModeSet *modeSet) { applyModeSet(modeSet); } + + protected: + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LINK_LAYER) { + checkRejected("QoS RateSelection", *this); + std::cout << "QoS rejected configured legacy responseCtsFrameBitrate for both HT profiles.\n"; + } + } +}; + +Define_Module(TestRateSelection); +Define_Module(TestQosRateSelection); + +} // namespace Ieee80211InvalidConfiguredCtsRate + +%file: test.ned +import inet.linklayer.ieee80211.mac.rateselection.QosRateSelection; +import inet.linklayer.ieee80211.mac.rateselection.RateSelection; + +simple TestRateSelection extends RateSelection +{ + parameters: + @class(TestRateSelection); + rateControlModule = ""; + mibModule = ""; + responseCtsFrameBitrate = default(6Mbps); +} + +simple TestQosRateSelection extends QosRateSelection +{ + parameters: + @class(TestQosRateSelection); + rateControlModule = ""; + mibModule = ""; + responseCtsFrameBitrate = default(6Mbps); +} + +simple TestRateSelection24 extends TestRateSelection +{ + parameters: + responseCtsFrameBitrate = default(24Mbps); +} + +simple TestQosRateSelection24 extends TestQosRateSelection +{ + parameters: + responseCtsFrameBitrate = default(24Mbps); +} + +network TestInvalidConfiguredCtsRate +{ + submodules: + dcfRateSelection6: TestRateSelection; + dcfRateSelection24: TestRateSelection24; + qosRateSelection6: TestQosRateSelection; + qosRateSelection24: TestQosRateSelection24; +} + +%inifile: omnetpp.ini +[General] +network = TestInvalidConfiguredCtsRate +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +seed-set = 0 + +%contains: stdout +DCF rejected configured legacy responseCtsFrameBitrate for both HT profiles. +%contains: stdout +QoS rejected configured legacy responseCtsFrameBitrate for both HT profiles. diff --git a/tests/module/Ieee80211LegacyResponseRateSelection.test b/tests/module/Ieee80211LegacyResponseRateSelection.test new file mode 100644 index 00000000000..5fc607b867b --- /dev/null +++ b/tests/module/Ieee80211LegacyResponseRateSelection.test @@ -0,0 +1,110 @@ +%description: +Checks the public DCF and QoS response-rate APIs for representative legacy +operation modes, preserving mandatory ACK/CTS and Basic BlockAck selections. + +%file: Test.cc +#include + +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211LegacyResponseRateSelection { + +class TestRateSelection : public RateSelection +{ + public: + void setModeSetForTest(const Ieee80211ModeSet *newModeSet) { modeSet = newModeSet; } +}; + +class TestQosRateSelection : public QosRateSelection +{ + public: + void setModeSetForTest(const Ieee80211ModeSet *newModeSet) { modeSet = const_cast(newModeSet); } +}; + +struct LegacyProfileCase { + const char *name; + bps sourceBitrate; + bps expectedResponseBitrate; + bps expectedBlockAckBitrate; +}; + +static void checkCase(const LegacyProfileCase& testCase) +{ + auto modeSet = Ieee80211ModeSet::getModeSet(testCase.name); + auto sourceMode = modeSet->getMode(testCase.sourceBitrate); + auto expectedMode = modeSet->getMode(testCase.expectedResponseBitrate); + auto expectedBlockAckMode = modeSet->getMode(testCase.expectedBlockAckBitrate); + Packet packet("elicitingFrame"); + packet.addTag()->setMode(sourceMode); + auto basicBlockAckReq = makeShared(); + + TestRateSelection dcfRateSelection; + dcfRateSelection.setModeSetForTest(modeSet); + ASSERT(dcfRateSelection.computeResponseAckFrameMode(&packet, nullptr) == expectedMode); + ASSERT(dcfRateSelection.computeResponseCtsFrameMode(&packet, nullptr) == expectedMode); + + TestQosRateSelection qosRateSelection; + qosRateSelection.setModeSetForTest(modeSet); + ASSERT(qosRateSelection.computeResponseAckFrameMode(&packet, nullptr) == expectedMode); + ASSERT(qosRateSelection.computeResponseCtsFrameMode(&packet, nullptr) == expectedMode); + auto blockAckMode = qosRateSelection.computeResponseBlockAckFrameMode(&packet, basicBlockAckReq); + if (blockAckMode != expectedBlockAckMode) + throw cRuntimeError("%s Basic BlockAck returned %s, expected %s", testCase.name, blockAckMode->getName(), expectedBlockAckMode->getName()); +} + +class LegacyResponseProbe : public cSimpleModule +{ + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + + virtual void initialize(int stage) override + { + if (stage != INITSTAGE_LAST) + return; + const LegacyProfileCase cases[] = { + {"a", Mbps(54), Mbps(24), Mbps(54)}, + {"b", Mbps(11), Mbps(11), Mbps(11)}, + {"g(mixed)", Mbps(54), Mbps(24), Mbps(54)}, + }; + for (const auto& testCase : cases) + checkCase(testCase); + std::cout << "Legacy a, b, and g(mixed) DCF/QoS ACK/CTS and Basic BlockAck responses preserved.\n"; + } +}; + +Define_Module(LegacyResponseProbe); + +} // namespace Ieee80211LegacyResponseRateSelection + +%file: test.ned +simple LegacyResponseProbe +{ + parameters: + @class(LegacyResponseProbe); +} + +network TestLegacyResponseRateSelection +{ + submodules: + probe: LegacyResponseProbe; +} + +%inifile: omnetpp.ini +[General] +network = TestLegacyResponseRateSelection +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +seed-set = 0 + +%contains: stdout +Legacy a, b, and g(mixed) DCF/QoS ACK/CTS and Basic BlockAck responses preserved. diff --git a/tests/module/QosRateSelectionNullModeSetInitialization.test b/tests/module/QosRateSelectionNullModeSetInitialization.test new file mode 100644 index 00000000000..976921dadc1 --- /dev/null +++ b/tests/module/QosRateSelectionNullModeSetInitialization.test @@ -0,0 +1,54 @@ +%description: +QosRateSelection must reject reaching link-layer initialization without a mode set. + +%file: Test.cc +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" + +using namespace inet; +using namespace inet::ieee80211; + +namespace QosRateSelectionNullModeSetInitialization { + +class NullModeSetQosRateSelection : public QosRateSelection +{ + protected: + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LINK_LAYER) + QosRateSelection::initialize(stage); + } +}; + +Define_Module(NullModeSetQosRateSelection); + +} // namespace QosRateSelectionNullModeSetInitialization + +%file: test.ned +import inet.linklayer.ieee80211.mac.rateselection.QosRateSelection; + +simple NullModeSetQosRateSelection extends QosRateSelection +{ + parameters: + @class(NullModeSetQosRateSelection); + rateControlModule = ""; + mibModule = ""; +} + +network TestQosRateSelectionNullModeSetInitialization +{ + submodules: + rateSelection: NullModeSetQosRateSelection; +} + +%inifile: omnetpp.ini +[General] +network = TestQosRateSelectionNullModeSetInitialization +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false + +%exitcode: 1 + +%contains-regex: stderr +QosRateSelection module TestQosRateSelectionNullModeSetInitialization\.rateSelection has no mode set at link-layer initialization diff --git a/tests/module/RateSelectionNullModeSetInitialization.test b/tests/module/RateSelectionNullModeSetInitialization.test new file mode 100644 index 00000000000..8358beb9972 --- /dev/null +++ b/tests/module/RateSelectionNullModeSetInitialization.test @@ -0,0 +1,54 @@ +%description: +RateSelection must reject reaching link-layer initialization without a mode set. + +%file: Test.cc +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" + +using namespace inet; +using namespace inet::ieee80211; + +namespace RateSelectionNullModeSetInitialization { + +class NullModeSetRateSelection : public RateSelection +{ + protected: + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LINK_LAYER) + RateSelection::initialize(stage); + } +}; + +Define_Module(NullModeSetRateSelection); + +} // namespace RateSelectionNullModeSetInitialization + +%file: test.ned +import inet.linklayer.ieee80211.mac.rateselection.RateSelection; + +simple NullModeSetRateSelection extends RateSelection +{ + parameters: + @class(NullModeSetRateSelection); + rateControlModule = ""; + mibModule = ""; +} + +network TestRateSelectionNullModeSetInitialization +{ + submodules: + rateSelection: NullModeSetRateSelection; +} + +%inifile: omnetpp.ini +[General] +network = TestRateSelectionNullModeSetInitialization +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false + +%exitcode: 1 + +%contains-regex: stderr +RateSelection module TestRateSelectionNullModeSetInitialization\.rateSelection has no mode set at link-layer initialization diff --git a/tests/unit/Ieee80211HtGuardInterval_1.test b/tests/unit/Ieee80211HtGuardInterval_1.test index 05f3ca83153..aa9d38b49b7 100644 --- a/tests/unit/Ieee80211HtGuardInterval_1.test +++ b/tests/unit/Ieee80211HtGuardInterval_1.test @@ -37,7 +37,7 @@ class TestIeee80211Receiver : public Ieee80211Receiver class TestIeee80211Radio : public Ieee80211Radio { public: - void setup(Ieee80211Transmitter *tx, Ieee80211Receiver *rx) + void setup(ITransmitter *tx, IReceiver *rx) { transmitter = tx; receiver = rx; @@ -74,6 +74,10 @@ class TestRateSelectionAccessor : public ieee80211::RateSelection static void notifyModeSet(ieee80211::RateSelection *rs, const Ieee80211ModeSet *ms) { static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); } + static const Ieee80211ModeSet *getModeSet(const ieee80211::RateSelection *rs) { return static_cast(rs)->modeSet; } + static void applyModeSet(ieee80211::RateSelection *rs, const Ieee80211ModeSet *ms) { + static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); + } }; class TestQosRateSelectionAccessor : public ieee80211::QosRateSelection @@ -86,6 +90,10 @@ class TestQosRateSelectionAccessor : public ieee80211::QosRateSelection static void notifyModeSet(ieee80211::QosRateSelection *rs, const Ieee80211ModeSet *ms) { static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); } + static const Ieee80211ModeSet *getModeSet(const ieee80211::QosRateSelection *rs) { return static_cast(rs)->modeSet; } + static void applyModeSet(ieee80211::QosRateSelection *rs, const Ieee80211ModeSet *ms) { + static_cast(rs)->receiveSignal(nullptr, modesetChangedSignal, const_cast(ms), nullptr); + } }; class TestIeee80211Mgmt : public ieee80211::Ieee80211MgmtBase @@ -470,24 +478,24 @@ rateSelection->par("multicastFrameBitrate").setDoubleValue(6e6); rateSelection->par("mgmtFrameBitrate").setDoubleValue(6e6); rateSelection->par("controlFrameBitrate").setDoubleValue(6e6); rateSelection->par("responseAckFrameBitrate").setDoubleValue(6e6); -rateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); +rateSelection->par("responseCtsFrameBitrate").setDoubleValue(-1); rateSelection->finalizeParameters(); rateSelection->buildInside(); -TestRateSelectionAccessor::notifyModeSet(rateSelection, erpModeSet); -ASSERT(TestRateSelectionAccessor::readModeSet(rateSelection) == erpModeSet); +TestRateSelectionAccessor::applyModeSet(rateSelection, erpModeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == erpModeSet); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getControlFrameMode(rateSelection))); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getResponseAckFrameMode(rateSelection))); -ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection))); +ASSERT(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection) == nullptr); ASSERT(erpModeSet->containsMode(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection))); ASSERT(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection) == erpModeSet->getMode(Mbps(24))); // Dynamically switch rateSelection to HT mode set -TestRateSelectionAccessor::notifyModeSet(rateSelection, modeSet); -ASSERT(TestRateSelectionAccessor::readModeSet(rateSelection) == modeSet); +TestRateSelectionAccessor::applyModeSet(rateSelection, modeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelection) == modeSet); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelection))); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMode(rateSelection))); @@ -495,7 +503,7 @@ ASSERT(!erpModeSet->containsMode(TestRateSelectionAccessor::getMulticastFrameMod ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getMgmtFrameMode(rateSelection))); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getControlFrameMode(rateSelection))); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseAckFrameMode(rateSelection))); -ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection))); +ASSERT(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelection) == nullptr); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getFastestMandatoryMode(rateSelection))); // Also verify HT-specific fixed mode configured with bandwidth, stream, and guard interval @@ -510,14 +518,15 @@ rateSelectionHt->par("multicastFrameBitrate").setDoubleValue(6e6); rateSelectionHt->par("mgmtFrameBitrate").setDoubleValue(6e6); rateSelectionHt->par("controlFrameBitrate").setDoubleValue(6e6); rateSelectionHt->par("responseAckFrameBitrate").setDoubleValue(6e6); -rateSelectionHt->par("responseCtsFrameBitrate").setDoubleValue(6e6); +rateSelectionHt->par("responseCtsFrameBitrate").setDoubleValue(6.5e6); rateSelectionHt->finalizeParameters(); rateSelectionHt->buildInside(); -TestRateSelectionAccessor::notifyModeSet(rateSelectionHt, modeSet); -ASSERT(TestRateSelectionAccessor::readModeSet(rateSelectionHt) == modeSet); +TestRateSelectionAccessor::applyModeSet(rateSelectionHt, modeSet); +ASSERT(TestRateSelectionAccessor::getModeSet(rateSelectionHt) == modeSet); ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt))); ASSERT(TestRateSelectionAccessor::getDataFrameMode(rateSelectionHt) == short65); +ASSERT(modeSet->containsMode(TestRateSelectionAccessor::getResponseCtsFrameMode(rateSelectionHt))); auto *qosRateSelection = check_and_cast(cModuleType::get("inet.linklayer.ieee80211.mac.rateselection.QosRateSelection")->create("qosRateSelection", this)); qosRateSelection->par("rateControlModule").setStringValue(""); @@ -528,18 +537,18 @@ qosRateSelection->par("mgmtFrameBitrate").setDoubleValue(6e6); qosRateSelection->par("controlFrameBitrate").setDoubleValue(6e6); qosRateSelection->par("responseAckFrameBitrate").setDoubleValue(6e6); qosRateSelection->par("responseBlockAckFrameBitrate").setDoubleValue(6e6); -qosRateSelection->par("responseCtsFrameBitrate").setDoubleValue(6e6); +qosRateSelection->par("responseCtsFrameBitrate").setDoubleValue(-1); qosRateSelection->finalizeParameters(); qosRateSelection->buildInside(); -TestQosRateSelectionAccessor::notifyModeSet(qosRateSelection, erpModeSet); -ASSERT(TestQosRateSelectionAccessor::readModeSet(qosRateSelection) == erpModeSet); +TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, erpModeSet); +ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == erpModeSet); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); ASSERT(erpModeSet->containsMode(TestQosRateSelectionAccessor::getFastestMandatoryMode(qosRateSelection))); -TestQosRateSelectionAccessor::notifyModeSet(qosRateSelection, modeSet); -ASSERT(TestQosRateSelectionAccessor::readModeSet(qosRateSelection) == modeSet); +TestQosRateSelectionAccessor::applyModeSet(qosRateSelection, modeSet); +ASSERT(TestQosRateSelectionAccessor::getModeSet(qosRateSelection) == modeSet); ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(!erpModeSet->containsMode(TestQosRateSelectionAccessor::getDataFrameMode(qosRateSelection))); ASSERT(modeSet->containsMode(TestQosRateSelectionAccessor::getResponseBlockAckFrameMode(qosRateSelection))); From 0857b233cb6260da71837650826510c2b39e7c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 02:22:16 +0200 Subject: [PATCH 17/56] fix(ieee80211): negotiate HT Greenfield capability and fall back to HT-mixed for mixed peers - Mode & physical layer introspection: - Add virtual isHtGreenfield() query to IIeee80211Mode and override it in Ieee80211HtMode to identify Greenfield preamble modes. - Track htGreenfieldSupported in Ieee80211ModeSet constructor and expose isHtGreenfieldSupported() accessor. - Expose findHtMixedMode() on Ieee80211ModeSet to resolve the HT-mixed equivalent of any HT mode based on precomputed response tables. - MIB & directional capability negotiation: - Populate localHtCapabilities.greenfield from modeSet->isHtGreenfieldSupported() in Ieee80211Mib::updateLocalHtCapabilities. - Add receiverGreenfield to Ieee80211HtDirectionalCapabilities and populate directional flags during negotiateHtCapabilities(). - Rate selection peer filtering & HT-mixed fallback: - In isCompatibleHtMode(), reject candidate Greenfield modes when the negotiated receiver did not advertise Greenfield support. - In selectPeerCompatibleMode(), dynamically map Greenfield candidate modes to their legal HT-mixed equivalents, allowing Greenfield stations to communicate with mixed-format peers at high throughput before falling back to legacy rates. - Testing: - Update tests/unit/Ieee80211HtCapabilities_1.test with directional Greenfield capability assertions. - Add tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test verifying bidirectional unicast data delivery and dynamic HT-mixed frame format selection in a heterogeneous Greenfield/Mixed BSS. --- .../Ieee80211PeerModeSelection.cc | 17 +- .../ieee80211/mib/Ieee80211HtCapabilities.h | 5 +- .../linklayer/ieee80211/mib/Ieee80211Mib.cc | 1 + .../wireless/ieee80211/mode/IIeee80211Mode.h | 4 +- .../wireless/ieee80211/mode/Ieee80211HtMode.h | 1 + .../ieee80211/mode/Ieee80211ModeSet.cc | 10 + .../ieee80211/mode/Ieee80211ModeSet.h | 4 + ...80211HtHeterogeneousGreenfieldRuntime.test | 199 ++++++++++++++++++ tests/unit/Ieee80211HtCapabilities_1.test | 3 + 9 files changed, 239 insertions(+), 5 deletions(-) create mode 100644 tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 048699e5980..93fc50b453a 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -84,6 +84,11 @@ static bool isCompatibleHtMode(const IIeee80211Mode *mode, const Ieee80211Mib::P bandwidth > negotiated.operation.operatingChannelWidth) return false; + // IEEE Std 802.11-2024, 19.1.1 and 19.1.4: an HT-greenfield PPDU may only + // be transmitted to a receiver that advertised HT-greenfield support. + if (mode->isHtGreenfield() && !receiverCapabilities.receiverGreenfield) + return false; + // IEEE Std 802.11-2024, 10.17 and Table 9-224: a short guard interval // is usable only when the receiver advertised it for this channel width. if (mode->isHtShortGuardInterval()) { @@ -133,14 +138,20 @@ const IIeee80211Mode *selectPeerCompatibleMode(const Ieee80211ModeSet *modeSet, if (peerHtState == nullptr || !peerHtState->valid || !peerHtState->negotiatedCapabilities.localTxPeerRx.valid) return getLegacyFallback(modeSet, mode, peerAddress); - if (isCompatibleHtMode(mode, peerHtState)) - return mode; + + const IIeee80211Mode *effectiveMode = mode; + if (effectiveMode->isHtGreenfield() && !peerHtState->negotiatedCapabilities.localTxPeerRx.receiverGreenfield) + effectiveMode = modeSet->findHtMixedMode(mode); + if (effectiveMode != nullptr && isCompatibleHtMode(effectiveMode, peerHtState)) + return effectiveMode; auto candidateBitrate = mode->getDataMode()->getNetBitrate(); const IIeee80211Mode *bestMode = nullptr; for (int i = 0; i < modeSet->getNumModes(); i++) { const auto *candidate = modeSet->getMode(i); - if (candidate->getHtMcsIndex() < 0 || + if (candidate->isHtGreenfield() && !peerHtState->negotiatedCapabilities.localTxPeerRx.receiverGreenfield) + candidate = modeSet->findHtMixedMode(candidate); + if (candidate == nullptr || candidate->getHtMcsIndex() < 0 || candidate->getDataMode()->getNetBitrate() > candidateBitrate || !isCompatibleHtMode(candidate, peerHtState)) continue; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h index 2174ce5c387..3b62b3399d4 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h @@ -72,6 +72,7 @@ struct Ieee80211HtDirectionalCapabilities Ieee80211HtMcsNssMap mcsNss; std::array supportedMcs = {}; bool receiverLdpc = false; + bool receiverGreenfield = false; bool receiverShortGi20 = false; bool receiverShortGi40 = false; int receiverMaxAmpduLengthExponent = 0; @@ -116,9 +117,11 @@ inline Ieee80211NegotiatedHtCapabilities negotiateHtCapabilities(const Ieee80211 negotiated.localRxPeerTx.mcsNss.maxMcsPerNss[nss] = mcs; } } - // LDPC and short-GI bits advertise receiver capability, so they are directional. + // LDPC, Greenfield, and short-GI bits advertise receiver capability, so they are directional. negotiated.localTxPeerRx.receiverLdpc = peer.ldpc; negotiated.localRxPeerTx.receiverLdpc = local.ldpc; + negotiated.localTxPeerRx.receiverGreenfield = peer.greenfield; + negotiated.localRxPeerTx.receiverGreenfield = local.greenfield; negotiated.localTxPeerRx.receiverShortGi20 = peer.shortGi20; negotiated.localRxPeerTx.receiverShortGi20 = local.shortGi20; negotiated.localTxPeerRx.receiverShortGi40 = peer.shortGi40; diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index 0d28b31a9c8..4ec66ed169c 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -172,6 +172,7 @@ void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeS localHtCapabilities.txMcsNss.maxMcsPerNss[nss] = std::max(localHtCapabilities.txMcsNss.maxMcsPerNss[nss], mcs % 8); } } + localHtCapabilities.greenfield = modeSet->isHtGreenfieldSupported(); if (localHtCapabilities.supportedChannelWidths.empty()) throw cRuntimeError("HT operation mode set '%s' does not provide an HT channel width", modeSet->getName()); localHtCapabilities.maxAmpduLengthExponent = par("htMaxAmpduLengthExponent"); diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h index 8fa1b77bbc4..4a104cb221d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/IIeee80211Mode.h @@ -63,6 +63,9 @@ class INET_API IIeee80211Mode : public cObject, public IPrintableObject // Returns whether this mode uses the optional 400 ns HT guard interval. // Non-HT modes deliberately report false. virtual bool isHtShortGuardInterval() const = 0; + // Returns whether this mode uses the HT Greenfield preamble format. + // Non-HT modes and HT-mixed modes report false. + virtual bool isHtGreenfield() const { return false; } virtual int getLegacyCwMin() const = 0; virtual int getLegacyCwMax() const = 0; virtual const char *getName() const = 0; @@ -92,4 +95,3 @@ class INET_API IIeee80211Mode : public cObject, public IPrintableObject } // namespace inet #endif - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h index 5e5d43b6fa3..b35858ad69d 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h @@ -248,6 +248,7 @@ class INET_API Ieee80211HtMode : public Ieee80211ModeBase virtual const Ieee80211HtDataMode *getDataMode() const override { return dataMode; } virtual int getHtMcsIndex() const override { return dataMode->getMcsIndex(); } virtual bool isHtShortGuardInterval() const override { return dataMode->getGuardIntervalType() == Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT; } + virtual bool isHtGreenfield() const override { return preambleMode->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD; } virtual const Ieee80211HtPreambleMode *getPreambleMode() const override { return preambleMode; } virtual const Ieee80211HtSignalMode *getHeaderMode() const override { return preambleMode->getSignalMode(); } virtual const Ieee80211OfdmSignalMode *getLegacySignalMode() const { return preambleMode->getLegacySignalMode(); } diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index e58712e453a..763f6d622da 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -835,6 +835,8 @@ Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector en htSupportedChannelWidths.insert(bandwidth); if (entry.mode->isHtShortGuardInterval()) htShortGuardIntervalChannelWidths.insert(bandwidth); + if (entry.mode->isHtGreenfield()) + htGreenfieldSupported = true; } if (htOperationSupported) { for (int mcsIndex = 0; mcsIndex < 8; mcsIndex++) @@ -1259,6 +1261,14 @@ const IIeee80211Mode *Ieee80211ModeSet::getNonHtControlResponseMode(const IIeee8 return result; } +const IIeee80211Mode *Ieee80211ModeSet::findHtMixedMode(const IIeee80211Mode *mode) const +{ + if (mode == nullptr) + return nullptr; + auto it = htMixedControlResponseModes.find(mode); + return it != htMixedControlResponseModes.end() ? it->second : nullptr; +} + const Ieee80211ModeSet *Ieee80211ModeSet::findModeSet(const char *mode) { for (size_t index = 0; index < (&modeSets)->size(); index++) { diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h index 5061b1a827e..ebf96321679 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -51,6 +51,7 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject std::array htMcsMandatory = {}; std::set htSupportedChannelWidths; std::set htShortGuardIntervalChannelWidths; + bool htGreenfieldSupported = false; bool htOperationSupported = false; // Entries are selectable modes; supportedEntries also contains immutable PHY capabilities needed for mandatory control responses. const std::vector supportedEntries; @@ -93,6 +94,7 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject const std::set& getHtSupportedChannelWidths() const { return htSupportedChannelWidths; } const std::set& getHtShortGuardIntervalChannelWidths() const { return htShortGuardIntervalChannelWidths; } bool isHtShortGuardIntervalSupported(Hz bandwidth) const { return htShortGuardIntervalChannelWidths.count(bandwidth) != 0; } + bool isHtGreenfieldSupported() const { return htGreenfieldSupported; } // containsMode() covers entries selectable as the persistent operating mode // (for example through Ieee80211Transmitter::setMode()). supportsMode() @@ -134,6 +136,8 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject // 2.4 GHz HT modes are converted to non-HT responses. With mandatory=false, // the input bitrate is used as a ceiling for selecting that non-HT mode. const IIeee80211Mode *getNonHtControlResponseMode(const IIeee80211Mode *mode, bool mandatory = true) const; + // Returns the HT-mixed equivalent mode for an HT mode, or nullptr if not an HT mode or not mapped. + const IIeee80211Mode *findHtMixedMode(const IIeee80211Mode *mode) const; static const Ieee80211ModeSet *findModeSet(const char *mode); static const Ieee80211ModeSet *getModeSet(const char *mode); diff --git a/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test new file mode 100644 index 00000000000..37040b28817 --- /dev/null +++ b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test @@ -0,0 +1,199 @@ +%description: +Verifies bidirectional unicast delivery and dynamic PPDU format selection in a +heterogeneous BSS with one n(greenfield-2.4Ghz) station, an n(mixed-2.4Ghz) +access point, and an n(mixed-2.4Ghz) station. Because the AP is mixed-only, the +Greenfield station must recognize that the peer cannot receive Greenfield PPDUs +and dynamically select the compatible HT-mixed format for uplink transmissions. + +%file: Test.cc +#include + +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" + +using namespace inet; +using namespace inet::physicallayer; +using namespace inet::ieee80211; + +namespace Ieee80211HtHeterogeneousGreenfieldRuntime { + +static OPP_THREAD_LOCAL int greenfieldStaMixedDataCount = 0; +static OPP_THREAD_LOCAL int greenfieldStaGreenfieldDataCount = 0; +static OPP_THREAD_LOCAL int mixedStaDataCount = 0; +static OPP_THREAD_LOCAL int apForwardedDataCount = 0; +static OPP_THREAD_LOCAL int nonHtAckCount = 0; + +class HeterogeneousHtCheckingRadio : public Ieee80211Radio, public cListener +{ + using cListener::finish; + + protected: + virtual void initialize(int stage) override + { + Ieee80211Radio::initialize(stage); + if (stage == INITSTAGE_LOCAL) + subscribe(transmissionStartedSignal, this); + } + + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override + { + if (signalID != transmissionStartedSignal) + return; + auto transmission = check_and_cast(obj); + auto mode = dynamic_cast(transmission->getMode()); + auto phyHeader = Ieee80211Radio::peekIeee80211PhyHeaderAtFront(transmission->getPacket()); + if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || + transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) + throw cRuntimeError("Invalid preamble/header/data duration decomposition"); + if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); + bool isHtGreenfield = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD; + bool isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; + + std::string fullPath = getFullPath(); + bool isHost0 = fullPath.find("host[0]") != std::string::npos; + bool isHost1 = fullPath.find("host[1]") != std::string::npos; + bool isAp = fullPath.find("ap") != std::string::npos; + + if (dynamicPtrCast(macHeader)) { + if (isHost0) { + if (isHtGreenfield) + greenfieldStaGreenfieldDataCount++; + else if (isHtMixed) + greenfieldStaMixedDataCount++; + } + else if (isHost1 && isHtMixed) + mixedStaDataCount++; + else if (isAp && isHtMixed) + apForwardedDataCount++; + } + if (dynamicPtrCast(macHeader) && mode == nullptr) + nonHtAckCount++; + } + + virtual void finish() override + { + Ieee80211Radio::finish(); + if (greenfieldStaGreenfieldDataCount > 0) + throw cRuntimeError("Greenfield station sent %d HT-Greenfield frame(s) to mixed-only AP", greenfieldStaGreenfieldDataCount); + if (greenfieldStaMixedDataCount == 0 || mixedStaDataCount == 0 || apForwardedDataCount == 0 || nonHtAckCount == 0) + throw cRuntimeError("Expected bidirectional HT-mixed data and non-HT ACK exchanges"); + std::cout << "Observed " << greenfieldStaMixedDataCount << " GF-STA HT-mixed data, " + << mixedStaDataCount << " Mixed-STA data, " + << apForwardedDataCount << " AP forwarded data, 0 Greenfield data, and " + << nonHtAckCount << " non-HT ACK response(s).\n"; + } +}; + +Define_Module(HeterogeneousHtCheckingRadio); + +} // namespace Ieee80211HtHeterogeneousGreenfieldRuntime + +%file: test.ned + +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +module HeterogeneousHtCheckingRadio extends Ieee80211Radio +{ + parameters: + @class(HeterogeneousHtCheckingRadio); +} + +network TestHeterogeneousHtGreenfield +{ + parameters: + int numHosts = default(2); + submodules: + configurator: Ipv4NetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + host[numHosts]: WirelessHost { + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + ap: AccessPoint { + wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified"; + } +} + +%inifile: omnetpp.ini + +[General] +network = TestHeterogeneousHtGreenfield +abstract = false +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +record-eventlog = false +seed-set = 0 + +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMinZ = 0m +**.constraintAreaMaxX = 600m +**.constraintAreaMaxY = 400m +**.constraintAreaMaxZ = 0m + +**.arp.typename = "GlobalArp" + +*.ap.wlan[*].address = "10:00:00:00:00:00" +*.host[*].**.mgmt.accessPointAddress = "10:00:00:00:00:00" + +*.ap.mobility.typename = "StationaryMobility" +*.ap.mobility.initialX = 250m +*.ap.mobility.initialY = 200m +*.host[*].mobility.typename = "StationaryMobility" +*.host[*].mobility.initialY = 200m +*.host[0].mobility.initialX = 240m +*.host[1].mobility.initialX = 260m + +**.wlan[*].radio.typename = "HeterogeneousHtCheckingRadio" + +[Config HeterogeneousHtRuntime] +abstract = false +sim-time-limit = 2s +*.numHosts = 2 + +# Node configuration: host[0] is Greenfield, AP and host[1] are Mixed-HT +*.host[0].wlan[*].opMode = "n(greenfield-2.4Ghz)" +*.host[0].wlan[*].bitrate = 13Mbps +*.host[1].wlan[*].opMode = "n(mixed-2.4Ghz)" +*.host[1].wlan[*].bitrate = 13Mbps +*.ap.wlan[*].opMode = "n(mixed-2.4Ghz)" +*.ap.wlan[*].bitrate = 13Mbps + +# Bidirectional UDP traffic between host[0] and host[1] through AP +*.host[0].numApps = 2 +*.host[0].app[0].typename = "UdpBasicApp" +*.host[0].app[0].destAddresses = "host[1]" +*.host[0].app[0].destPort = 1000 +*.host[0].app[0].messageLength = 100B +*.host[0].app[0].startTime = 100ms +*.host[0].app[0].sendInterval = 100ms + +*.host[0].app[1].typename = "UdpSink" +*.host[0].app[1].localPort = 2000 + +*.host[1].numApps = 2 +*.host[1].app[0].typename = "UdpSink" +*.host[1].app[0].localPort = 1000 + +*.host[1].app[1].typename = "UdpBasicApp" +*.host[1].app[1].destAddresses = "host[0]" +*.host[1].app[1].destPort = 2000 +*.host[1].app[1].messageLength = 100B +*.host[1].app[1].startTime = 150ms +*.host[1].app[1].sendInterval = 100ms + +%extraargs: -c HeterogeneousHtRuntime + +%contains-regex: stdout +Observed [1-9][0-9]* GF-STA HT-mixed data, [1-9][0-9]* Mixed-STA data, [1-9][0-9]* AP forwarded data, 0 Greenfield data, and [1-9][0-9]* non-HT ACK response\(s\). diff --git a/tests/unit/Ieee80211HtCapabilities_1.test b/tests/unit/Ieee80211HtCapabilities_1.test index dee6a669f79..a56cb51a211 100644 --- a/tests/unit/Ieee80211HtCapabilities_1.test +++ b/tests/unit/Ieee80211HtCapabilities_1.test @@ -20,6 +20,7 @@ local.rxMcsSupported[2] = true; local.rxMcsSupported[3] = true; local.txMcsNss.maxMcsPerNss[0] = 3; local.ldpc = true; +local.greenfield = true; local.shortGi20 = true; local.maxAmpduLengthExponent = 1; @@ -47,6 +48,8 @@ ASSERT(!negotiated.localRxPeerTx.supportedMcs[1]); ASSERT(negotiated.localRxPeerTx.supportedMcs[2]); ASSERT(!negotiated.localTxPeerRx.receiverLdpc); ASSERT(negotiated.localRxPeerTx.receiverLdpc); +ASSERT(!negotiated.localTxPeerRx.receiverGreenfield); +ASSERT(negotiated.localRxPeerTx.receiverGreenfield); ASSERT(negotiated.localTxPeerRx.receiverMaxAmpduLengthExponent == 3); ASSERT(negotiated.localRxPeerTx.receiverMaxAmpduLengthExponent == 1); From e8a70b8016798e886db9704b8da02e30eb1986f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 02:37:23 +0200 Subject: [PATCH 18/56] fix(linklayer): accept supported HT-mixed CTS responses in Greenfield peer mode selection - Rate selection peer filtering: - In selectPeerCompatibleMode(), use modeSet->supportsMode(mode) instead of modeSet->containsMode(mode) to validate candidate modes. - While containsMode() verifies persistent selectable operating modes (which for Greenfield mode sets only include Greenfield PPDUs), supportsMode() correctly covers supported supplementary modes, such as mandatory HT-mixed CTS responses required by IEEE 802.11-2024 subclauses 10.6.6.1 and 10.6.6.5.7. - This prevents runtime simulation aborts when Greenfield stations respond to HT RTS frames in infrastructure networks. - Testing: - Add assertions in tests/unit/Ieee80211PeerModeSelection_1.test verifying that selectPeerCompatibleMode() accepts HT-mixed CTS modes for n(greenfield-2.4Ghz) mode sets while still rejecting unsupported MCS indices. - Update tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test with RTS/CTS enabled (rtsThreshold = 1B) to verify end-to-end HT-mixed CTS generation and transmission by Greenfield stations. --- .../Ieee80211PeerModeSelection.cc | 4 ++-- ...80211HtHeterogeneousGreenfieldRuntime.test | 11 ++++++++--- tests/unit/Ieee80211PeerModeSelection_1.test | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 93fc50b453a..55869e0c04c 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -132,8 +132,8 @@ const IIeee80211Mode *selectPeerCompatibleMode(const Ieee80211ModeSet *modeSet, return mode; if (modeSet == nullptr) throw cRuntimeError("Cannot select a peer-compatible HT mode without an IEEE 802.11 mode set"); - if (!modeSet->containsMode(mode)) - throw cRuntimeError("HT mode '%s' is not contained in IEEE 802.11 mode set '%s'", + if (!modeSet->supportsMode(mode)) + throw cRuntimeError("HT mode '%s' is not supported by IEEE 802.11 mode set '%s'", mode->getName(), modeSet->getName()); if (peerHtState == nullptr || !peerHtState->valid || !peerHtState->negotiatedCapabilities.localTxPeerRx.valid) diff --git a/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test index 37040b28817..e753cf7ce9e 100644 --- a/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test +++ b/tests/module/Ieee80211HtHeterogeneousGreenfieldRuntime.test @@ -22,6 +22,7 @@ namespace Ieee80211HtHeterogeneousGreenfieldRuntime { static OPP_THREAD_LOCAL int greenfieldStaMixedDataCount = 0; static OPP_THREAD_LOCAL int greenfieldStaGreenfieldDataCount = 0; +static OPP_THREAD_LOCAL int greenfieldStaMixedCtsCount = 0; static OPP_THREAD_LOCAL int mixedStaDataCount = 0; static OPP_THREAD_LOCAL int apForwardedDataCount = 0; static OPP_THREAD_LOCAL int nonHtAckCount = 0; @@ -72,6 +73,8 @@ class HeterogeneousHtCheckingRadio : public Ieee80211Radio, public cListener else if (isAp && isHtMixed) apForwardedDataCount++; } + if (dynamicPtrCast(macHeader) && isHtMixed && isHost0) + greenfieldStaMixedCtsCount++; if (dynamicPtrCast(macHeader) && mode == nullptr) nonHtAckCount++; } @@ -81,9 +84,10 @@ class HeterogeneousHtCheckingRadio : public Ieee80211Radio, public cListener Ieee80211Radio::finish(); if (greenfieldStaGreenfieldDataCount > 0) throw cRuntimeError("Greenfield station sent %d HT-Greenfield frame(s) to mixed-only AP", greenfieldStaGreenfieldDataCount); - if (greenfieldStaMixedDataCount == 0 || mixedStaDataCount == 0 || apForwardedDataCount == 0 || nonHtAckCount == 0) - throw cRuntimeError("Expected bidirectional HT-mixed data and non-HT ACK exchanges"); + if (greenfieldStaMixedDataCount == 0 || greenfieldStaMixedCtsCount == 0 || mixedStaDataCount == 0 || apForwardedDataCount == 0 || nonHtAckCount == 0) + throw cRuntimeError("Expected bidirectional HT-mixed data, GF-STA HT-mixed CTS responses, and non-HT ACK exchanges"); std::cout << "Observed " << greenfieldStaMixedDataCount << " GF-STA HT-mixed data, " + << greenfieldStaMixedCtsCount << " GF-STA HT-mixed CTS, " << mixedStaDataCount << " Mixed-STA data, " << apForwardedDataCount << " AP forwarded data, 0 Greenfield data, and " << nonHtAckCount << " non-HT ACK response(s).\n"; @@ -156,6 +160,7 @@ seed-set = 0 *.host[1].mobility.initialX = 260m **.wlan[*].radio.typename = "HeterogeneousHtCheckingRadio" +**.wlan[*].mac.dcf.rtsPolicy.rtsThreshold = 1B [Config HeterogeneousHtRuntime] abstract = false @@ -196,4 +201,4 @@ sim-time-limit = 2s %extraargs: -c HeterogeneousHtRuntime %contains-regex: stdout -Observed [1-9][0-9]* GF-STA HT-mixed data, [1-9][0-9]* Mixed-STA data, [1-9][0-9]* AP forwarded data, 0 Greenfield data, and [1-9][0-9]* non-HT ACK response\(s\). +Observed [1-9][0-9]* GF-STA HT-mixed data, [1-9][0-9]* GF-STA HT-mixed CTS, [1-9][0-9]* Mixed-STA data, [1-9][0-9]* AP forwarded data, 0 Greenfield data, and [1-9][0-9]* non-HT ACK response\(s\). diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index 6d2b7fbe770..18d18f39f51 100644 --- a/tests/unit/Ieee80211PeerModeSelection_1.test +++ b/tests/unit/Ieee80211PeerModeSelection_1.test @@ -172,6 +172,25 @@ ASSERT(firstSelection == secondSelection); ASSERT(firstSelection->getHtMcsIndex() == 8); ASSERT(!firstSelection->isHtShortGuardInterval()); +// Greenfield mode set supports HT-mixed control responses (e.g. CTS) per +// IEEE 802.11-2024 10.6.6.5.7 even though HT-mixed modes are not selectable entries. +const auto *greenfieldModeSet = Ieee80211ModeSet::getModeSet("n(greenfield-2.4Ghz)"); +const auto *htMixedCtsMode = mcs0Long; // htMcs0BW20MHz mixed format +ASSERT(!greenfieldModeSet->containsMode(htMixedCtsMode)); +ASSERT(greenfieldModeSet->supportsMode(htMixedCtsMode)); +auto greenfieldPeer = makePeerState({0}, {MHz(20)}, MHz(20)); +const auto *greenfieldCtsSelection = selectPeerCompatibleMode(greenfieldModeSet, &greenfieldPeer, htMixedCtsMode, peer); +ASSERT(greenfieldCtsSelection == htMixedCtsMode); + +bool outsideGreenfieldModeRejected = false; +try { + selectPeerCompatibleMode(greenfieldModeSet, &greenfieldPeer, modeOutsideSet, peer); +} +catch (const cRuntimeError&) { + outsideGreenfieldModeRejected = true; +} +ASSERT(outsideGreenfieldModeRejected); + EV << "Peer-compatible HT mode selection checks verified.\n"; %contains: stdout From 05c1e307dc199941e501c771cd48f5dd4a68a85f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 13:11:49 +0200 Subject: [PATCH 19/56] test(linklayer): add unit tests for Greenfield receiver capability and HT-mixed fallback - Peer mode selection testing: - Add optional greenfield receiver capability parameter to makePeerState() helper in Ieee80211PeerModeSelection_1.test. - Verify selectPeerCompatibleMode() selects HT-Greenfield modes when communicating with a Greenfield-capable peer. - Verify isCompatibleHtMode() rejects HT-Greenfield modes when the destination station did not advertise Greenfield reception, causing selectPeerCompatibleMode() to fall back to the compatible HT-mixed equivalent mode. - Verify candidate HT-mixed modes remain unaffected regardless of the destination station's Greenfield reception capability. --- tests/unit/Ieee80211PeerModeSelection_1.test | 25 +++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index 18d18f39f51..f1fe6b04d88 100644 --- a/tests/unit/Ieee80211PeerModeSelection_1.test +++ b/tests/unit/Ieee80211PeerModeSelection_1.test @@ -28,7 +28,7 @@ static const IIeee80211Mode *findHtMode(const Ieee80211ModeSet *modeSet, int mcs } static Ieee80211Mib::PeerHtState makePeerState(std::initializer_list mcsIndexes, - std::initializer_list bandwidths, Hz operatingChannelWidth, bool shortGi20 = false, bool shortGi40 = false) + std::initializer_list bandwidths, Hz operatingChannelWidth, bool shortGi20 = false, bool shortGi40 = false, bool greenfield = false) { Ieee80211Mib::PeerHtState state; state.valid = true; @@ -40,6 +40,7 @@ static Ieee80211Mib::PeerHtState makePeerState(std::initializer_list mcsInd receiverCapabilities.supportedChannelWidths.insert(bandwidth); receiverCapabilities.receiverShortGi20 = shortGi20; receiverCapabilities.receiverShortGi40 = shortGi40; + receiverCapabilities.receiverGreenfield = greenfield; state.negotiatedCapabilities.operation.operatingChannelWidth = operatingChannelWidth; return state; } @@ -191,6 +192,28 @@ catch (const cRuntimeError&) { } ASSERT(outsideGreenfieldModeRejected); +const auto *mcs0Greenfield = findHtMode(greenfieldModeSet, 0, MHz(20), Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); +ASSERT(mcs0Greenfield != nullptr); +ASSERT(mcs0Greenfield->isHtGreenfield()); + +// When destination advertises Greenfield reception, HT-Greenfield candidate mode is selected. +auto greenfieldCapablePeer = makePeerState({0}, {MHz(20)}, MHz(20), false, false, true); +const auto *greenfieldSelection = selectPeerCompatibleMode(greenfieldModeSet, &greenfieldCapablePeer, mcs0Greenfield, peer); +ASSERT(greenfieldSelection == mcs0Greenfield); +ASSERT(greenfieldSelection->isHtGreenfield()); + +// When destination did not advertise Greenfield reception, isCompatibleHtMode rejects Greenfield mode, +// falling back to the legal HT-mixed equivalent mode. +auto mixedOnlyDestinationPeer = makePeerState({0}, {MHz(20)}, MHz(20), false, false, false); +const auto *mixedFallbackSelection = selectPeerCompatibleMode(greenfieldModeSet, &mixedOnlyDestinationPeer, mcs0Greenfield, peer); +ASSERT(mixedFallbackSelection == htMixedCtsMode); +ASSERT(!mixedFallbackSelection->isHtGreenfield()); + +// HT-mixed mode candidates remain unaffected regardless of Greenfield receiver capability. +const auto *mixedCandidateSelection = selectPeerCompatibleMode(modeSet, &mixedOnlyDestinationPeer, mcs0Long, peer); +ASSERT(mixedCandidateSelection == mcs0Long); +ASSERT(!mixedCandidateSelection->isHtGreenfield()); + EV << "Peer-compatible HT mode selection checks verified.\n"; %contains: stdout From eeceebe85d4bfb0f780414bdddd19d15beb66fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 13:58:06 +0200 Subject: [PATCH 20/56] fix(physicallayer): fix transmission duration decomposition for Short GI HT and VHT modes - Physical layer transmission duration decomposition: - In Ieee80211Transmitter::createTransmission(), compute dataDuration using transmissionMode->getDataDuration(B(phyHeader->getLengthField())) instead of raw transmissionMode->getDataMode()->getDuration(...). - For HT mixed format and VHT modes configured with Short Guard Interval (Short GI / 400 ns), standard IEEE 802.11 symbol-boundary rounding (IEEE Std 802.11-2024, Eq. 19-90 and Eq. 21-109) rounds data airtime up to the 4 us symbol boundary. - Comparing duration == preambleDuration + dataDuration now correctly evaluates to true for HT/VHT Short GI transmissions, setting headerDuration to zero (since SIG fields are included in the preamble) and preventing negative dataDuration calculation and runtime simulation errors. - For legacy PHY modes with standalone PLCP headers (OFDM, ERP, DSSS, HR-DSSS), duration == preambleDuration + dataDuration remains false, preserving the header duration allocation. - Testing: - Update assertTransmitterDurationDecomposition() helper in tests/unit/Ieee80211HtGreenfield_1.test to verify duration decomposition with getDataDuration() and accept zero-header HT/VHT modes. - Add test assertions covering VHT Short GI, HT Mixed Short GI, and HT Greenfield Short GI modes with both 0-byte and 64-byte payloads. --- .../packetlevel/Ieee80211Transmitter.cc | 7 ++--- tests/unit/Ieee80211HtGreenfield_1.test | 29 ++++++++++++++----- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index 8ab34442799..2be14391371 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -167,14 +167,13 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran const Quaternion& startOrientation = mobility->getCurrentAngularPosition(); const Quaternion& endOrientation = mobility->getCurrentAngularPosition(); const simtime_t preambleDuration = transmissionMode->getPreambleMode()->getDuration(); - const simtime_t modeledDataDuration = transmissionMode->getDataMode()->getDuration(B(phyHeader->getLengthField())); + const simtime_t dataDuration = transmissionMode->getDataDuration(B(phyHeader->getLengthField())); // HT/VHT include their SIG fields in the PHY preamble duration, so their - // mode duration is exactly preamble + modeled data. Other PHYs expose a + // mode duration is exactly preamble + data duration. Other PHYs expose a // separate header; their residual data interval may also include a trailing // signal extension (ERP). Keep that extension in chronological data time. - const bool headerIncludedInPreamble = duration == preambleDuration + modeledDataDuration; + const bool headerIncludedInPreamble = duration == preambleDuration + dataDuration; const simtime_t headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : transmissionMode->getHeaderMode()->getDuration(); - const simtime_t dataDuration = headerIncludedInPreamble ? modeledDataDuration : duration - headerDuration - preambleDuration; if (preambleDuration < SIMTIME_ZERO || headerDuration < SIMTIME_ZERO || dataDuration < SIMTIME_ZERO || preambleDuration + headerDuration + dataDuration != duration) throw cRuntimeError("Invalid transmission duration decomposition for mode %s", transmissionMode->getName()); diff --git a/tests/unit/Ieee80211HtGreenfield_1.test b/tests/unit/Ieee80211HtGreenfield_1.test index 11a5efa886f..d6a515de3e8 100644 --- a/tests/unit/Ieee80211HtGreenfield_1.test +++ b/tests/unit/Ieee80211HtGreenfield_1.test @@ -71,19 +71,19 @@ static bool rejectsLegacyConfiguredCts(const Ieee80211ModeSet *modeSet, const II } } -static void assertTransmitterDurationDecomposition(const IIeee80211Mode *mode) +static void assertTransmitterDurationDecomposition(const IIeee80211Mode *mode, b dataLength = B(0)) { - auto duration = mode->getDuration(B(0)); + auto duration = mode->getDuration(dataLength); auto preambleDuration = mode->getPreambleMode()->getDuration(); - auto modeledDataDuration = mode->getDataMode()->getDuration(B(0)); - bool headerIncludedInPreamble = duration == preambleDuration + modeledDataDuration; + auto modeledDataDuration = mode->getDataMode()->getDuration(dataLength); + auto dataDuration = mode->getDataDuration(dataLength); + bool headerIncludedInPreamble = duration == preambleDuration + dataDuration; auto headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : mode->getHeaderMode()->getDuration(); - auto dataDuration = headerIncludedInPreamble ? modeledDataDuration : duration - headerDuration - preambleDuration; ASSERT(preambleDuration >= SIMTIME_ZERO); ASSERT(headerDuration >= SIMTIME_ZERO); ASSERT(dataDuration >= SIMTIME_ZERO); ASSERT(preambleDuration + headerDuration + dataDuration == duration); - if (dynamic_cast(mode) != nullptr) + if (dynamic_cast(mode) != nullptr || dynamic_cast(mode) != nullptr) ASSERT(headerDuration == SIMTIME_ZERO); if (dynamic_cast(mode) != nullptr) ASSERT(dataDuration > modeledDataDuration); @@ -326,8 +326,21 @@ ASSERT(vhtMixed5GhzMode->getCenterFrequencyMode() == Ieee80211VhtMode::BAND_5GHZ ASSERT(vhtMixed2GhzMode->getCenterFrequencyMode() == Ieee80211VhtMode::BAND_2_4GHZ); ASSERT(vhtMixed5GhzMode->getDuration(B(0)) == vhtMixed5GhzMode->getPreambleMode()->getDuration() + vhtMixed5GhzMode->getDataMode()->getDuration(B(0))); ASSERT(vhtMixed5GhzMode->getDataMode()->getDuration(B(0)) >= SIMTIME_ZERO); -assertTransmitterDurationDecomposition(ofdmProfile->getMode(Mbps(6))); -assertTransmitterDurationDecomposition(vhtMixed5GhzMode); +auto vhtShortGi5GhzMode = Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, + Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, + Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT); +assertTransmitterDurationDecomposition(ofdmProfile->getMode(Mbps(6)), B(0)); +assertTransmitterDurationDecomposition(ofdmProfile->getMode(Mbps(6)), B(64)); +assertTransmitterDurationDecomposition(vhtMixed5GhzMode, B(0)); +assertTransmitterDurationDecomposition(vhtMixed5GhzMode, B(64)); +assertTransmitterDurationDecomposition(vhtShortGi5GhzMode, B(0)); +assertTransmitterDurationDecomposition(vhtShortGi5GhzMode, B(64)); +assertTransmitterDurationDecomposition(mixedMode, B(0)); +assertTransmitterDurationDecomposition(mixedMode, B(64)); +assertTransmitterDurationDecomposition(greenfieldMode, B(0)); +assertTransmitterDurationDecomposition(greenfieldMode, B(64)); auto erpMixedProfile = Ieee80211ModeSet::getModeSet("g(mixed)"); auto erpOnlyProfile = Ieee80211ModeSet::getModeSet("g(erp)"); From da7c92d986163b9dfe7889be141823d6b22520c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 30 Aug 2026 14:10:05 +0200 Subject: [PATCH 21/56] tests(fingerprint): update baseline for wireless TXOP showcase Update the fingerprint baseline for /showcases/wireless/txop/ (General config, run 0) from d2b6-a5d1/tplx;3a6f-4c28/~tNl to 1ecd-df80/tplx;86dd-208d/~tNl. The simulation trajectory changed due to corrections in IEEE 802.11 High Throughput (HT) physical layer airtime calculations and transmission duration decomposition: 1. Transmission Duration Decomposition (Ieee80211Transmitter): HT/VHT modes integrate their SIG fields into the PHY preamble duration (preambleMode->getDuration()). Previously, an 8 us header duration was queried from getHeaderMode()->getDuration() and subtracted a second time from data airtime. Ieee80211Transmitter now recognizes that HT header duration is contained in the preamble (headerDuration = 0), preserving the full modeled data duration. 2. HT Mixed Short-GI Airtime Rounding (Ieee80211HtMode): Data airtime for mixed-format HT short-GI transmissions is now rounded up to a 4 us symbol boundary per IEEE Std 802.11-2024 Eq. (19-90), while symbol interval lookups and guard intervals reflect normative timing. 3. Control-Response Rate Selection (RateSelection / QosRateSelection): Mandatory mode lookups and response rate fallback for RTS/CTS and Block Ack exchanges now use compliant mandatory modes at or below the target rate. Because txop is the showcase exercising 802.11n HT data (A-MSDU) and control frames (RTS/CTS, AddbaReq, WlanAck), these physical layer airtime corrections modify the transmission boundaries and subsequent contention scheduling into the intended, standard-compliant trajectory. --- tests/fingerprint/showcases.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index ef401f4edb1..5162a8b12f2 100644 --- a/tests/fingerprint/showcases.csv +++ b/tests/fingerprint/showcases.csv @@ -341,5 +341,5 @@ /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, 1ecd-df80/tplx;86dd-208d/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 From b7c8be8641ac10c55960ef92fd582c56a81ab147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 22/56] physicallayer: preserve scalar signal phase durations Scalar signal models forwarded header and data durations to their base constructor in reverse order. Preserve chronological phase boundaries for both transmission and reception analog models. Add direct TX/RX coverage with distinct preamble, header and data durations. --- .../scalar/ScalarSignalAnalogModel.cc | 2 +- tests/unit/ScalarSignalAnalogModel_1.test | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/unit/ScalarSignalAnalogModel_1.test diff --git a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSignalAnalogModel.cc b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSignalAnalogModel.cc index 459463d7c98..4d0a86c78c7 100644 --- a/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSignalAnalogModel.cc +++ b/src/inet/physicallayer/wireless/common/analogmodel/scalar/ScalarSignalAnalogModel.cc @@ -12,7 +12,7 @@ namespace inet { namespace physicallayer { ScalarSignalAnalogModel::ScalarSignalAnalogModel(const simtime_t preambleDuration, simtime_t headerDuration, simtime_t dataDuration, Hz centerFrequency, Hz bandwidth, W power) : - NarrowbandSignalAnalogModel(preambleDuration, dataDuration, headerDuration, centerFrequency, bandwidth), + NarrowbandSignalAnalogModel(preambleDuration, headerDuration, dataDuration, centerFrequency, bandwidth), power(power) { } diff --git a/tests/unit/ScalarSignalAnalogModel_1.test b/tests/unit/ScalarSignalAnalogModel_1.test new file mode 100644 index 00000000000..1b7e85c256a --- /dev/null +++ b/tests/unit/ScalarSignalAnalogModel_1.test @@ -0,0 +1,26 @@ +%description: +Scalar transmission and reception analog models preserve distinct chronological +preamble, header, and data durations when forwarding to their shared base. + +%includes: +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarTransmissionAnalogModel.h" +#include "inet/physicallayer/wireless/common/analogmodel/scalar/ScalarReceptionAnalogModel.h" +using namespace inet; +using namespace inet::physicallayer; + +%activity: +const auto preamble = SimTime(16, SIMTIME_US); +const auto header = SimTime(4, SIMTIME_US); +const auto data = SimTime(120, SIMTIME_US); +ScalarTransmissionAnalogModel tx(preamble, header, data, GHz(2.4), MHz(20), mW(1)); +ScalarReceptionAnalogModel rx(preamble, header, data, GHz(2.4), MHz(20), mW(1)); +for (const ISignalAnalogModel *model : {static_cast(&tx), static_cast(&rx)}) { + ASSERT(model->getPreambleDuration() == preamble); + ASSERT(model->getHeaderDuration() == header); + ASSERT(model->getDataDuration() == data); + ASSERT(model->getDuration() == preamble + header + data); +} +EV << "Scalar TX/RX analog phase durations preserved.\n"; + +%contains: stdout +Scalar TX/RX analog phase durations preserved. From ef2593a2d2511e6ffb529c7cfb3b1937d961824f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 23/56] ieee80211: restore the timed HT and VHT header phase Using the full PHY prefix as the preamble left HT and VHT transmissions with no timed header phase. Use the mode's chronological duration accessors while retaining the exact total-duration check. Check positive HT header durations in mixed and Greenfield runtime cases, and verify packet and analog phase boundaries across HT, VHT and legacy catalog variants. --- .../packetlevel/Ieee80211Transmitter.cc | 9 +- .../module/Ieee80211HtGreenfieldRuntime.test | 7 +- tests/module/Ieee80211HtMixedRuntime.test | 7 +- tests/module/Ieee80211TransmissionTiming.test | 106 ++++++++++++++++++ 4 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 tests/module/Ieee80211TransmissionTiming.test diff --git a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc index 2be14391371..1ba824545ea 100644 --- a/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc +++ b/src/inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.cc @@ -166,14 +166,9 @@ const ITransmission *Ieee80211Transmitter::createTransmission(const IRadio *tran const Coord& endPosition = mobility->getCurrentPosition(); const Quaternion& startOrientation = mobility->getCurrentAngularPosition(); const Quaternion& endOrientation = mobility->getCurrentAngularPosition(); - const simtime_t preambleDuration = transmissionMode->getPreambleMode()->getDuration(); + const simtime_t preambleDuration = transmissionMode->getPreambleDuration(); + const simtime_t headerDuration = transmissionMode->getHeaderDuration(); const simtime_t dataDuration = transmissionMode->getDataDuration(B(phyHeader->getLengthField())); - // HT/VHT include their SIG fields in the PHY preamble duration, so their - // mode duration is exactly preamble + data duration. Other PHYs expose a - // separate header; their residual data interval may also include a trailing - // signal extension (ERP). Keep that extension in chronological data time. - const bool headerIncludedInPreamble = duration == preambleDuration + dataDuration; - const simtime_t headerDuration = headerIncludedInPreamble ? SIMTIME_ZERO : transmissionMode->getHeaderMode()->getDuration(); if (preambleDuration < SIMTIME_ZERO || headerDuration < SIMTIME_ZERO || dataDuration < SIMTIME_ZERO || preambleDuration + headerDuration + dataDuration != duration) throw cRuntimeError("Invalid transmission duration decomposition for mode %s", transmissionMode->getName()); diff --git a/tests/module/Ieee80211HtGreenfieldRuntime.test b/tests/module/Ieee80211HtGreenfieldRuntime.test index 60d013e74f3..7648df48f17 100644 --- a/tests/module/Ieee80211HtGreenfieldRuntime.test +++ b/tests/module/Ieee80211HtGreenfieldRuntime.test @@ -42,9 +42,10 @@ class HtPreambleCheckingRadio : public Ieee80211Radio, public cListener if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) throw cRuntimeError("Invalid preamble/header/data duration decomposition"); - if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || - transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) - throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + if (mode != nullptr && (transmission->getPreambleDuration() != mode->getPreambleDuration() || + transmission->getHeaderDuration() != mode->getHeaderDuration() || transmission->getHeaderDuration() <= SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not preserve the mode's timed header phase"); auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); auto isHtGreenfield = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_GREENFIELD; auto isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; diff --git a/tests/module/Ieee80211HtMixedRuntime.test b/tests/module/Ieee80211HtMixedRuntime.test index cd81007886e..297ae4de410 100644 --- a/tests/module/Ieee80211HtMixedRuntime.test +++ b/tests/module/Ieee80211HtMixedRuntime.test @@ -45,9 +45,10 @@ class HtMixedCheckingRadio : public Ieee80211Radio, public cListener if (transmission->getPreambleDuration() < SIMTIME_ZERO || transmission->getHeaderDuration() < SIMTIME_ZERO || transmission->getDataDuration() < SIMTIME_ZERO || transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() != transmission->getDuration()) throw cRuntimeError("Invalid preamble/header/data duration decomposition"); - if (mode != nullptr && (transmission->getHeaderDuration() != SIMTIME_ZERO || - transmission->getDataDuration() != mode->getDataMode()->getDuration(B(phyHeader->getLengthField())))) - throw cRuntimeError("HT transmission did not keep SIG in the preamble and data in the data interval"); + if (mode != nullptr && (transmission->getPreambleDuration() != mode->getPreambleDuration() || + transmission->getHeaderDuration() != mode->getHeaderDuration() || transmission->getHeaderDuration() <= SIMTIME_ZERO || + transmission->getDataDuration() != mode->getDataDuration(B(phyHeader->getLengthField())))) + throw cRuntimeError("HT transmission did not preserve the mode's timed header phase"); auto macHeader = transmission->getPacket()->peekDataAt(phyHeader->getChunkLength()); bool isHtMixed = mode != nullptr && mode->getPreambleMode()->getPreambleFormat() == Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED; if (dynamicPtrCast(macHeader) && isHtMixed) diff --git a/tests/module/Ieee80211TransmissionTiming.test b/tests/module/Ieee80211TransmissionTiming.test new file mode 100644 index 00000000000..563072ddea0 --- /dev/null +++ b/tests/module/Ieee80211TransmissionTiming.test @@ -0,0 +1,106 @@ +%description: +Verify packet transmitter chronological preamble/header/data durations and analog +model boundaries for HT-mixed, HT-Greenfield, VHT, and legacy modes. + +%file: Test.cc +#include +#include "inet/common/InitStages.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmitter.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/common/contract/bitlevel/ISignalAnalogModel.h" +using namespace inet; +using namespace inet::physicallayer; +namespace Ieee80211TransmissionTiming { +class TimingRadio : public Ieee80211Radio +{ + public: + void encapsulateForTest(Packet *packet) const { encapsulate(packet); } +}; +Define_Module(TimingRadio); +class TimingProbe : public cSimpleModule +{ + protected: + int numInitStages() const override { return NUM_INIT_STAGES; } + void initialize(int stage) override { + if (stage != INITSTAGE_LAST) + return; + auto radio = check_and_cast(getSimulation()->getModuleByPath("Test.host.wlan[0].radio")); + auto transmitter = check_and_cast(radio->getSubmodule("transmitter")); + int count = 0; + for (const char *profile : {"n(mixed-2.4Ghz)", "n(greenfield-2.4Ghz)", "ac", "a", "b", "g(mixed)"}) { + auto modes = Ieee80211ModeSet::getModeSet(profile); + for (int i = 0; i < modes->getNumModes(); i++) { + auto mode = modes->getMode(i); + // One antenna suffices; use every one-stream catalog variant. + if (mode->getDataMode()->getNumberOfSpatialStreams() > 1) + continue; + transmitter->setModeSetAndMode(modes, mode); + for (int length : {1, 100, 1500}) { + Packet packet("timing"); + packet.insertAtBack(makeShared(B(length))); + packet.addTag()->setMode(mode); + radio->encapsulateForTest(&packet); + auto transmission = transmitter->createTransmission(radio, &packet, SimTime(1)); + ASSERT(transmission->getPreambleDuration() == mode->getPreambleDuration()); + ASSERT(transmission->getHeaderDuration() == mode->getHeaderDuration()); + ASSERT(transmission->getHeaderDuration() > SIMTIME_ZERO); + ASSERT(transmission->getDataDuration() == mode->getDataDuration(B(length))); + ASSERT(transmission->getDuration() == mode->getDuration(B(length))); + ASSERT(transmission->getPreambleDuration() + transmission->getHeaderDuration() + transmission->getDataDuration() == transmission->getDuration()); + auto analog = transmission->getAnalogModel(); + ASSERT(analog->getPreambleDuration() == transmission->getPreambleDuration()); + ASSERT(analog->getHeaderDuration() == transmission->getHeaderDuration()); + ASSERT(analog->getDataDuration() == transmission->getDataDuration()); + delete transmission; + count++; + } + } + } + ASSERT(count > 100); + std::cout << "HT, VHT, and legacy packet transmission phases verified.\n"; + } +}; +Define_Module(TimingProbe); +} +%file: test.ned +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211Radio; +module TimingRadio extends Ieee80211Radio +{ + parameters: + @class(TimingRadio); +} +simple TimingProbe +{ + parameters: + @class(TimingProbe); +} +network Test +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + host: AdhocHost; + probe: TimingProbe; +} +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +seed-set = 0 +sim-time-limit = 0s +**.host.wlan[0].opMode = "a" +**.host.wlan[0].radio.typename = "TimingRadio" +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMinZ = 0m +**.constraintAreaMaxX = 100m +**.constraintAreaMaxY = 100m +**.constraintAreaMaxZ = 0m +%contains: stdout +HT, VHT, and legacy packet transmission phases verified. From 834a554a879cf15f62abb460040eb0b832ced13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 24/56] ieee80211: bound mandatory multicast rates by the requested rate Optional multicast requests such as 9 and 18 Mb/s could select the fastest mandatory legacy rate and exceed the requested bitrate. Select the highest mandatory legacy rate within the ceiling, preserving exact mandatory requests. Report an error when mandatory legacy modes exist but none meets the bound. Preserve the request when the mandatory legacy set is empty. Cover optional rates, exact requests and both fallback boundaries. --- .../Ieee80211PeerModeSelection.cc | 7 +++++ .../Ieee80211PeerModeSelection.h | 5 ++++ tests/unit/Ieee80211PeerModeSelection_1.test | 26 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc index 55869e0c04c..c43806f9f60 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.cc @@ -18,15 +18,22 @@ const IIeee80211Mode *selectGroupAddressedMode(const Ieee80211ModeSet *modeSet, { // IEEE Std 802.11-2024, 10.6.5.1 and 10.6.5.4. The model advertises // mandatory legacy operational modes as its BSS basic legacy rate set. + bool hasMandatoryLegacyMode = false; const IIeee80211Mode *legacyMode = nullptr; for (const auto *candidate : modeSet->getLegacyOperationalModes()) { if (!modeSet->getIsMandatory(candidate)) continue; + hasMandatoryLegacyMode = true; + if (candidate->getDataMode()->getNetBitrate() > requestedMode->getDataMode()->getNetBitrate()) + continue; if (candidate == requestedMode) return candidate; if (legacyMode == nullptr || candidate->getDataMode()->getNetBitrate() > legacyMode->getDataMode()->getNetBitrate()) legacyMode = candidate; } + if (legacyMode == nullptr && hasMandatoryLegacyMode) + throw cRuntimeError("No mandatory legacy mode at or below requested group-addressed mode '%s' in mode set '%s'", + requestedMode->getName(), modeSet->getName()); return legacyMode != nullptr ? legacyMode : requestedMode; } diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h index e89cf445dd8..d00ea491dbc 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h +++ b/src/inet/linklayer/ieee80211/mac/rateselection/Ieee80211PeerModeSelection.h @@ -14,6 +14,11 @@ namespace inet { namespace ieee80211 { +/** + * Selects the fastest mandatory legacy mode at or below the requested bitrate, + * preserving an exact mandatory request. Throws if mandatory legacy modes exist + * but none meets the bound; preserves the request if that mandatory set is empty. + */ INET_API const physicallayer::IIeee80211Mode *selectGroupAddressedMode( const physicallayer::Ieee80211ModeSet *modeSet, const physicallayer::IIeee80211Mode *requestedMode); diff --git a/tests/unit/Ieee80211PeerModeSelection_1.test b/tests/unit/Ieee80211PeerModeSelection_1.test index f1fe6b04d88..9ba90515ba3 100644 --- a/tests/unit/Ieee80211PeerModeSelection_1.test +++ b/tests/unit/Ieee80211PeerModeSelection_1.test @@ -78,6 +78,32 @@ const auto *modeOutsideSet = Ieee80211HtCompliantModes::getCompliantMode(&Ieee80 Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG); ASSERT(!modeSet->containsMode(modeOutsideSet)); +// Group-addressed configured optional rates are upper bounds over the basic +// legacy set (modeled by mandatory legacy operational modes). +for (const char *profile : {"a", "g(mixed)"}) { + const auto *groupSet = Ieee80211ModeSet::getModeSet(profile); + for (int rate : {6, 9, 12, 18, 24}) { + const auto *requested = groupSet->getMode(Mbps(rate)); + auto expected = Mbps(rate == 9 ? 6 : rate == 18 ? 12 : rate); + const auto *selected = selectGroupAddressedMode(groupSet, requested); + ASSERT(selected->getDataMode()->getNetBitrate() == expected); + if (rate != 9 && rate != 18) + ASSERT(selected == requested); + } +} +const auto *legacySet = Ieee80211ModeSet::getModeSet("a"); +const auto *six = legacySet->getMode(Mbps(6)); +const auto *nine = legacySet->getMode(Mbps(9)); +Ieee80211ModeSet aboveBound("aboveBound", {{true, nine, true}}, nine, Ieee80211ModeSet::PhyType::OFDM); +bool groupBoundRejected = false; +try { selectGroupAddressedMode(&aboveBound, six); } +catch (const cRuntimeError& error) { + groupBoundRejected = error.getFormattedMessage().find("No mandatory legacy mode at or below") != std::string::npos; +} +ASSERT(groupBoundRejected); +Ieee80211ModeSet noMandatory("noMandatory", {{false, nine, true}}, nine, Ieee80211ModeSet::PhyType::OFDM); +ASSERT(selectGroupAddressedMode(&noMandatory, nine) == nine); + const MacAddress peer("02:00:00:00:00:01"); auto shortGiPeer = makePeerState({8}, {MHz(20)}, MHz(20), true); From a38044cfa25c99cc28d596f5d6e1915ae5dda4df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 25/56] ieee80211: require unambiguous configured response modes A response bitrate can identify multiple HT MCS, bandwidth, stream-count and guard-interval combinations. Expose matching qualifiers for DCF/QoS ACK and CTS, plus QoS BlockAck, and reject ambiguous configured matches instead of silently depending on catalog order. Mode-set lookup enforces uniqueness only when requested. Automatic responses and other lookup callers retain their existing behavior. Verify qualified CTS conversion, invalid and automatic configurations, and configured multicast ceilings through both production selectors. --- .../mac/rateselection/QosRateSelection.cc | 10 +- .../mac/rateselection/QosRateSelection.ned | 13 ++ .../mac/rateselection/RateSelection.cc | 8 +- .../mac/rateselection/RateSelection.ned | 10 + .../ieee80211/mode/Ieee80211ModeSet.cc | 24 ++- .../ieee80211/mode/Ieee80211ModeSet.h | 9 +- ...ee80211QualifiedResponseRateSelection.test | 189 ++++++++++++++++++ tests/unit/Ieee80211HtModeSet_1.test | 11 + 8 files changed, 252 insertions(+), 22 deletions(-) create mode 100644 tests/module/Ieee80211QualifiedResponseRateSelection.test diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index b885503275d..788a6bab198 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -20,12 +20,12 @@ namespace ieee80211 { using namespace inet::physicallayer; -static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, const char *modulePath) +static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, const char *modulePath) { if (bitrate == -1) return nullptr; try { - auto result = modeSet->getMode(bps(bitrate)); + auto result = modeSet->getMode(bps(bitrate), bandwidth, numSpatialStreams, guardInterval, true); if (modeSet->isHtOperationSupported() && result->getHtMcsIndex() < 0) throw cRuntimeError("legacy mode '%s' is not selectable for HT CTS responses", result->getName()); return result; @@ -86,11 +86,11 @@ void QosRateSelection::updateModes() double controlFrameBitrate = par("controlFrameBitrate"); auto newControlFrameMode = controlFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); double responseAckFrameBitrate = par("responseAckFrameBitrate"); - auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate), Hz(par("responseAckFrameBandwidth")), par("responseAckFrameNumSpatialStreams"), par("responseAckFrameGuardInterval"), true); double responseBlockAckFrameBitrate = par("responseBlockAckFrameBitrate"); - auto newResponseBlockAckFrameMode = responseBlockAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate)); + auto newResponseBlockAckFrameMode = responseBlockAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseBlockAckFrameBitrate), Hz(par("responseBlockAckFrameBandwidth")), par("responseBlockAckFrameNumSpatialStreams"), par("responseBlockAckFrameGuardInterval"), true); double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, getFullPath().c_str()); + auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, Hz(par("responseCtsFrameBandwidth")), par("responseCtsFrameNumSpatialStreams"), par("responseCtsFrameGuardInterval"), getFullPath().c_str()); auto newFastestMandatoryMode = modeSet->getFastestMandatoryMode(); multicastFrameMode = newMulticastFrameMode; diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned index e3fb6369c95..eb62ded5346 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.ned @@ -27,19 +27,32 @@ simple QosRateSelection extends SimpleModule double multicastFrameBitrate @unit(bps) = default(-1bps); + // Configured response rates must identify one selectable PHY mode. Use the matching + // Bandwidth, NumSpatialStreams, and GuardInterval qualifiers to disambiguate rates. + // Unspecified qualifiers are wildcards; ambiguous matches are rejected. Qualifiers + // are ignored for an automatic (-1bps) response. GI must match the modeled mode. // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, // a configured HT bitrate is an upper bound mapped to the highest mandatory // non-HT rate at or below it; it does not select an exact HT ACK PPDU. double responseAckFrameBitrate @unit(bps) = default(-1bps); + double responseAckFrameBandwidth @unit(Hz) = default(nan Hz); + int responseAckFrameNumSpatialStreams = default(-1); + double responseAckFrameGuardInterval @unit(s) = default(-1s); // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, // a configured HT bitrate is an upper bound mapped to the highest mandatory // non-HT rate at or below it; it does not select an exact HT BlockAck PPDU. double responseBlockAckFrameBitrate @unit(bps) = default(-1bps); + double responseBlockAckFrameBandwidth @unit(Hz) = default(nan Hz); + int responseBlockAckFrameNumSpatialStreams = default(-1); + double responseBlockAckFrameGuardInterval @unit(s) = default(-1s); // -1 selects the standard-derived automatic response. A configured value must resolve to a // selectable mode; for an HT RTS it must be HT and is translated to the corresponding HT-mixed // CTS while preserving MCS, bandwidth, NSS, and GI. An explicitly configured HT value is a // deliberate override and may bypass IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7 response constraints. double responseCtsFrameBitrate @unit(bps) = default(-1bps); + double responseCtsFrameBandwidth @unit(Hz) = default(nan Hz); + int responseCtsFrameNumSpatialStreams = default(-1); + double responseCtsFrameGuardInterval @unit(s) = default(-1s); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Explicit mode qualifier when dataFrameBitrate is specified diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc index 6094d101bd6..b901e505593 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.cc @@ -24,12 +24,12 @@ namespace ieee80211 { using namespace inet::physicallayer; -static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, const char *modulePath) +static const IIeee80211Mode *resolveConfiguredResponseCtsFrameMode(const Ieee80211ModeSet *modeSet, double bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, const char *modulePath) { if (bitrate == -1) return nullptr; try { - auto result = modeSet->getMode(bps(bitrate)); + auto result = modeSet->getMode(bps(bitrate), bandwidth, numSpatialStreams, guardInterval, true); if (modeSet->isHtOperationSupported() && result->getHtMcsIndex() < 0) throw cRuntimeError("legacy mode '%s' is not selectable for HT CTS responses", result->getName()); return result; @@ -106,9 +106,9 @@ void RateSelection::updateModes() double controlFrameBitrate = par("controlFrameBitrate"); auto newControlFrameMode = controlFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(controlFrameBitrate)); double responseAckFrameBitrate = par("responseAckFrameBitrate"); - auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate)); + auto newResponseAckFrameMode = responseAckFrameBitrate == -1 ? nullptr : modeSet->getMode(bps(responseAckFrameBitrate), Hz(par("responseAckFrameBandwidth")), par("responseAckFrameNumSpatialStreams"), par("responseAckFrameGuardInterval"), true); double responseCtsFrameBitrate = par("responseCtsFrameBitrate"); - auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, getFullPath().c_str()); + auto newResponseCtsFrameMode = resolveConfiguredResponseCtsFrameMode(modeSet, responseCtsFrameBitrate, Hz(par("responseCtsFrameBandwidth")), par("responseCtsFrameNumSpatialStreams"), par("responseCtsFrameGuardInterval"), getFullPath().c_str()); auto newFastestMandatoryMode = modeSet->getFastestMandatoryMode(); // Commit only after every configured mode has been resolved, so a failed diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned index 60ae6786a07..0943cf6bed3 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned +++ b/src/inet/linklayer/ieee80211/mac/rateselection/RateSelection.ned @@ -23,15 +23,25 @@ simple RateSelection extends SimpleModule like IRateSelection double multicastFrameBitrate @unit(bps) = default(-1bps); + // Configured response rates must identify one selectable PHY mode. Use the matching + // Bandwidth, NumSpatialStreams, and GuardInterval qualifiers to disambiguate rates. + // Unspecified qualifiers are wildcards; ambiguous matches are rejected. Qualifiers + // are ignored for an automatic (-1bps) response. GI must match the modeled mode. // -1 selects the standard-derived automatic response. In a 2.4 GHz HT profile, // a configured HT bitrate is an upper bound mapped to the highest mandatory // non-HT rate at or below it; it does not select an exact HT ACK PPDU. double responseAckFrameBitrate @unit(bps) = default(-1bps); + double responseAckFrameBandwidth @unit(Hz) = default(nan Hz); + int responseAckFrameNumSpatialStreams = default(-1); + double responseAckFrameGuardInterval @unit(s) = default(-1s); // -1 selects the standard-derived automatic response. A configured value must resolve to a // selectable mode; for an HT RTS it must be HT and is translated to the corresponding HT-mixed // CTS while preserving MCS, bandwidth, NSS, and GI. An explicitly configured HT value is a // deliberate override and may bypass IEEE 802.11-2024 10.6.6.5.3/10.6.6.5.7 response constraints. double responseCtsFrameBitrate @unit(bps) = default(-1bps); + double responseCtsFrameBandwidth @unit(Hz) = default(nan Hz); + int responseCtsFrameNumSpatialStreams = default(-1); + double responseCtsFrameGuardInterval @unit(s) = default(-1s); double dataFrameBitrate @unit(bps) = default(-1bps); // Fastest double dataFrameBandwidth @unit(Hz) = default(nan Hz); // Explicit mode qualifier when dataFrameBitrate is specified diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index 763f6d622da..49f00bec69a 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -1040,13 +1040,14 @@ const IIeee80211Mode *Ieee80211ModeSet::findCompatibleMode(const IIeee80211Mode return nullptr; } -const IIeee80211Mode *Ieee80211ModeSet::findMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +const IIeee80211Mode *Ieee80211ModeSet::findMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, bool requireUnique) const { - return findMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval); + return findMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval, requireUnique); } -const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, bool requireUnique) const { + const IIeee80211Mode *result = nullptr; for (size_t index = 0; index < entries.size(); index++) { auto mode = entries[index].mode; auto dataMode = mode->getDataMode(); @@ -1058,15 +1059,20 @@ const IIeee80211Mode *Ieee80211ModeSet::findMode(bps minBitrate, bps maxBitrate, (numSpatialStreams == -1 || dataMode->getNumberOfSpatialStreams() == numSpatialStreams) && guardIntervalMatches) { - return entries[index].mode; + if (!requireUnique) + return mode; + if (result != nullptr && result != mode) + throw cRuntimeError("Ambiguous mode for bitrate range (%g - %g) bps in operation mode '%s'; specify bandwidth, number of spatial streams, and guard interval", + minBitrate.get(), maxBitrate.get(), getName()); + result = mode; } } - return nullptr; + return result; } -const IIeee80211Mode *Ieee80211ModeSet::getMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +const IIeee80211Mode *Ieee80211ModeSet::getMode(bps bitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, bool requireUnique) const { - const IIeee80211Mode *mode = getMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval); + const IIeee80211Mode *mode = getMode(bitrate - Mbps(0.05), bitrate + Mbps(0.05), bandwidth, numSpatialStreams, guardInterval, requireUnique); if (mode == nullptr) throw cRuntimeError("Unknown mode for bitrate %g bps, bandwidth %g Hz, %d spatial streams, and %s guard interval in operation mode '%s'", bitrate.get(), bandwidth.get(), numSpatialStreams, guardInterval.str().c_str(), getName()); @@ -1074,9 +1080,9 @@ const IIeee80211Mode *Ieee80211ModeSet::getMode(bps bitrate, Hz bandwidth, int n return mode; } -const IIeee80211Mode *Ieee80211ModeSet::getMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval) const +const IIeee80211Mode *Ieee80211ModeSet::getMode(bps minBitrate, bps maxBitrate, Hz bandwidth, int numSpatialStreams, simtime_t guardInterval, bool requireUnique) const { - const IIeee80211Mode *mode = findMode(minBitrate, maxBitrate, bandwidth, numSpatialStreams, guardInterval); + const IIeee80211Mode *mode = findMode(minBitrate, maxBitrate, bandwidth, numSpatialStreams, guardInterval, requireUnique); if (mode == nullptr) throw cRuntimeError("Unknown mode for bitrate range (%g - %g) bps, bandwidth %g Hz, %d spatial streams, and %s guard interval in operation mode '%s'", minBitrate.get(), maxBitrate.get(), bandwidth.get(), numSpatialStreams, guardInterval.str().c_str(), getName()); diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h index ebf96321679..d3218205b9c 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -111,10 +111,11 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject // Pointer lookup is intentionally strict. Use getControlResponseMode() for an explicitly requested response that needs HT-mixed translation. const IIeee80211Mode *findMode(const IIeee80211Mode *mode) const; const IIeee80211Mode *getMode(const IIeee80211Mode *mode) const; - const IIeee80211Mode *findMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; - const IIeee80211Mode *findMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; - const IIeee80211Mode *getMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; - const IIeee80211Mode *getMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1) const; + // requireUnique rejects multiple matching entries instead of selecting the first. + const IIeee80211Mode *findMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1, bool requireUnique = false) const; + const IIeee80211Mode *findMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1, bool requireUnique = false) const; + const IIeee80211Mode *getMode(bps bitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1, bool requireUnique = false) const; + const IIeee80211Mode *getMode(bps minBitrate, bps maxBitrate, Hz bandwidth = Hz(NaN), int numSpatialStreams = -1, simtime_t guardInterval = -1, bool requireUnique = false) const; const IIeee80211Mode *getSlowestMode() const; const IIeee80211Mode *getFastestMode() const; const IIeee80211Mode *getSlowerMode(const IIeee80211Mode *mode) const; diff --git a/tests/module/Ieee80211QualifiedResponseRateSelection.test b/tests/module/Ieee80211QualifiedResponseRateSelection.test new file mode 100644 index 00000000000..61c17a11672 --- /dev/null +++ b/tests/module/Ieee80211QualifiedResponseRateSelection.test @@ -0,0 +1,189 @@ +%description: +Exercise production DCF/QoS configured response resolution, ambiguity rejection, +qualifier preservation for HT-mixed CTS, and optional multicast rate ceilings. + +%file: Test.cc +#include +#include +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +namespace Ieee80211QualifiedResponseRateSelection { + +template +static void checkSelector(Selector& selector, bool qos) +{ + for (const char *profile : {"n(mixed-2.4Ghz)", "n(greenfield-2.4Ghz)"}) { + auto modes = Ieee80211ModeSet::getModeSet(profile); + for (const char *kind : {"Ack", "Cts", "BlockAck"}) { + if (!qos && std::string(kind) == "BlockAck") + continue; + std::string stem = std::string("response") + kind + "Frame"; + auto bitrate = stem + "Bitrate"; + auto bandwidth = stem + "Bandwidth"; + auto streams = stem + "NumSpatialStreams"; + auto gi = stem + "GuardInterval"; + selector.par(bitrate.c_str()).setDoubleValue(13e6); + bool rejected = false; + try { selector.applyModeSet(modes); } + catch (const cRuntimeError& error) { + rejected = error.getFormattedMessage().find("Ambiguous mode") != std::string::npos; + } + ASSERT(rejected); + selector.par(bandwidth.c_str()).setDoubleValue(20e6); + selector.par(gi.c_str()).setDoubleValue(800e-9); + for (int nss : {1, 2}) { + selector.par(streams.c_str()).setIntValue(nss); + selector.applyModeSet(modes); + auto configured = selector.configured(kind); + ASSERT(configured->getHtMcsIndex() == (nss == 1 ? 1 : 8)); + ASSERT(configured->getDataMode()->getBandwidth() == MHz(20)); + ASSERT(configured->getDataMode()->getGuardInterval() == SimTime(800, SIMTIME_NS)); + if (std::string(kind) == "Cts") { + Packet rts("rts"); + rts.addTag()->setMode(configured); + auto response = selector.computeResponseCtsFrameMode(&rts, nullptr); + ASSERT(!response->isHtGreenfield()); + ASSERT(response->getHtMcsIndex() == configured->getHtMcsIndex()); + ASSERT(response->getDataMode()->getBandwidth() == configured->getDataMode()->getBandwidth()); + ASSERT(response->getDataMode()->getNumberOfSpatialStreams() == nss); + ASSERT(response->getDataMode()->getGuardInterval() == configured->getDataMode()->getGuardInterval()); + } + } + // A conflicting explicit GI must not silently choose another tuple. + selector.par(gi.c_str()).setDoubleValue(400e-9); + rejected = false; + try { selector.applyModeSet(modes); } + catch (const cRuntimeError&) { rejected = true; } + ASSERT(rejected); + // Width and short GI also distinguish a configured HT tuple. This + // exercises selection only; packet-level HT40 operation is unsupported. + selector.par(bitrate.c_str()).setDoubleValue(15e6); + selector.par(bandwidth.c_str()).setDoubleValue(40e6); + selector.par(streams.c_str()).setIntValue(1); + selector.applyModeSet(modes); + auto wideShort = selector.configured(kind); + ASSERT(wideShort->getHtMcsIndex() == 0); + ASSERT(wideShort->getDataMode()->getBandwidth() == MHz(40)); + ASSERT(wideShort->isHtShortGuardInterval()); + if (std::string(kind) == "Cts") { + Packet rts("wideShortRts"); + rts.addTag()->setMode(wideShort); + auto response = selector.computeResponseCtsFrameMode(&rts, nullptr); + ASSERT(!response->isHtGreenfield()); + ASSERT(response->getHtMcsIndex() == 0); + ASSERT(response->getDataMode()->getBandwidth() == MHz(40)); + ASSERT(response->getDataMode()->getNumberOfSpatialStreams() == 1); + ASSERT(response->isHtShortGuardInterval()); + } + selector.par(bitrate.c_str()).setDoubleValue(-1); + selector.applyModeSet(modes); // automatic ignores the qualifiers + ASSERT(selector.configured(kind) == nullptr); + selector.par(bandwidth.c_str()).setDoubleValue(NaN); + selector.par(streams.c_str()).setIntValue(-1); + selector.par(gi.c_str()).setDoubleValue(-1); + } + } + for (const char *profile : {"a", "g(mixed)"}) { + for (int rate : {6, 9, 12, 18, 24}) { + selector.par("multicastFrameBitrate").setDoubleValue(rate * 1e6); + selector.applyModeSet(Ieee80211ModeSet::getModeSet(profile)); + auto header = makeShared(); + header->setReceiverAddress(MacAddress::BROADCAST_ADDRESS); + auto selected = selector.groupMode(header); + ASSERT(selected->getDataMode()->getNetBitrate() == Mbps(rate == 9 ? 6 : rate == 18 ? 12 : rate)); + } + } +} + +class DcfProbe : public RateSelection +{ + public: + const IIeee80211Mode *configured(const char *kind) const { return std::string(kind) == "Ack" ? responseAckFrameMode : responseCtsFrameMode; } + const IIeee80211Mode *groupMode(const Ptr& header) { return computeMode(nullptr, header); } + protected: + void initialize(int stage) override { + if (stage == INITSTAGE_LINK_LAYER) { + checkSelector(*this, false); + std::cout << "DCF qualified responses and multicast ceilings verified.\n"; + } + } +}; +class QosProbe : public QosRateSelection +{ + public: + const IIeee80211Mode *configured(const char *kind) const { return std::string(kind) == "Ack" ? responseAckFrameMode : std::string(kind) == "Cts" ? responseCtsFrameMode : responseBlockAckFrameMode; } + const IIeee80211Mode *groupMode(const Ptr& header) { return computeMode(nullptr, header, nullptr); } + protected: + void initialize(int stage) override { + if (stage == INITSTAGE_LINK_LAYER) { + checkSelector(*this, true); + std::cout << "QoS qualified responses and multicast ceilings verified.\n"; + } + } +}; +Define_Module(DcfProbe); +Define_Module(QosProbe); +} + +%file: test.ned +import inet.linklayer.ieee80211.mac.rateselection.RateSelection; +import inet.linklayer.ieee80211.mac.rateselection.QosRateSelection; +simple DcfProbe extends RateSelection +{ + parameters: + @class(DcfProbe); + rateControlModule = ""; + mibModule = ""; + multicastFrameBitrate @mutable; + responseAckFrameBitrate @mutable; + responseAckFrameBandwidth @mutable; + responseAckFrameNumSpatialStreams @mutable; + responseAckFrameGuardInterval @mutable; + responseCtsFrameBitrate @mutable; + responseCtsFrameBandwidth @mutable; + responseCtsFrameNumSpatialStreams @mutable; + responseCtsFrameGuardInterval @mutable; +} +simple QosProbe extends QosRateSelection +{ + parameters: + @class(QosProbe); + rateControlModule = ""; + mibModule = ""; + multicastFrameBitrate @mutable; + responseAckFrameBitrate @mutable; + responseAckFrameBandwidth @mutable; + responseAckFrameNumSpatialStreams @mutable; + responseAckFrameGuardInterval @mutable; + responseCtsFrameBitrate @mutable; + responseCtsFrameBandwidth @mutable; + responseCtsFrameNumSpatialStreams @mutable; + responseCtsFrameGuardInterval @mutable; + responseBlockAckFrameBitrate @mutable; + responseBlockAckFrameBandwidth @mutable; + responseBlockAckFrameNumSpatialStreams @mutable; + responseBlockAckFrameGuardInterval @mutable; +} +network Test +{ + submodules: + dcf: DcfProbe; + qos: QosProbe; +} +%inifile: omnetpp.ini +[General] +network = Test +ned-path = .;../../../../src;../../lib +cmdenv-express-mode = true +record-vector-results = false +seed-set = 0 +%contains: stdout +DCF qualified responses and multicast ceilings verified. +%contains: stdout +QoS qualified responses and multicast ceilings verified. diff --git a/tests/unit/Ieee80211HtModeSet_1.test b/tests/unit/Ieee80211HtModeSet_1.test index 44877fd9d68..67a2ba58f8b 100644 --- a/tests/unit/Ieee80211HtModeSet_1.test +++ b/tests/unit/Ieee80211HtModeSet_1.test @@ -43,6 +43,17 @@ static const IIeee80211Mode *findHtMode(const Ieee80211ModeSet *modeSet, int mcs const auto *htModeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); const auto *vhtOnlyModeSet = Ieee80211ModeSet::getModeSet("ac"); +// 13 Mbps identifies both one-stream MCS 1 and two-stream MCS 8. +bool ambiguousRejected = false; +try { htModeSet->getMode(Mbps(13), MHz(20), -1, SimTime(800, SIMTIME_NS), true); } +catch (const cRuntimeError& error) { + ambiguousRejected = error.getFormattedMessage().find("Ambiguous mode") != std::string::npos; +} +ASSERT(ambiguousRejected); +ASSERT(htModeSet->getMode(Mbps(13), MHz(20), 1, SimTime(800, SIMTIME_NS), true)->getHtMcsIndex() == 1); +ASSERT(htModeSet->getMode(Mbps(13), MHz(20), 2, SimTime(800, SIMTIME_NS), true)->getHtMcsIndex() == 8); +ASSERT(htModeSet->findMode(Mbps(9999), MHz(20), 1, -1, true) == nullptr); + ASSERT(htModeSet->isHtOperationSupported()); ASSERT(!vhtOnlyModeSet->isHtOperationSupported()); From eb9d3a8df860018d1c6f531d928ef5bab23f6e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 5 Sep 2026 23:55:27 +0200 Subject: [PATCH 26/56] ieee80211: document the packet PHY HT40 limitation The HT catalog can represent 40 MHz modes, but the packet-level transmitter and receiver advertise only 20 MHz operation. Clarify that nonzero secondary-channel offsets require PHY width support and are rejected by these providers. --- src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned index 70474b3d95b..c8afc96c288 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned @@ -22,7 +22,7 @@ simple Ieee80211Mib extends SimpleModule @class(Ieee80211Mib); // Model-backed subset of IEEE Std 802.11-2024 HT capability/operation state; not a full Annex C MIB. int htMaxAmpduLengthExponent = default(0); // maximum received A-MPDU length exponent (0..3) - int htSecondaryChannelOffset = default(0); // BSS operation policy: 0=20 MHz, 1=40 MHz above, 3=40 MHz below; bounded by the mode set + int htSecondaryChannelOffset = default(0); // BSS operation policy: 0=20 MHz, 1=40 MHz above, 3=40 MHz below; requires PHY width support. The packet-level Ieee80211Transmitter/Receiver support only 20 MHz and reject nonzero offsets. int htProtectionMode = default(0); // 0=none, 1=nonmember, 2=20 MHz, 3=non-HT mixed displayStringTextFormat = default("Address: {address}{ssidStr}\n{modeStr}{stationTypeStr}{qosStr}{associatedStr}"); @display("i=block/table"); From 9298bee5d000ffd0456a51c204fe67ac5f794960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:02 +0200 Subject: [PATCH 27/56] queueing: support selective extraction and departure signals Allow consumers to select packets through the owning provider while preserving priority, WRR, label, gate, and compound-queue scheduling. Publish logical departures through packetQueueDeparture with typed dequeue, removal, and drop reasons so consumers can distinguish ownership transfer from terminal disposal. Detach shared-buffer overflow victims before notification and forward logical departures once across compound boundaries. Cover borrowed packet lifetime during external-buffer eviction. --- WHATSNEW | 30 +++++ doc/src/users-guide/ch-diffserv.rst | 9 +- src/inet/queueing/base/PacketFlowBase.cc | 20 ++- src/inet/queueing/base/PacketFlowBase.h | 7 +- src/inet/queueing/base/PacketGateBase.cc | 15 ++- src/inet/queueing/base/PacketGateBase.h | 3 +- src/inet/queueing/base/PacketQueueBase.cc | 20 ++- src/inet/queueing/base/PacketQueueBase.h | 4 +- src/inet/queueing/base/PacketQueueBase.ned | 1 + src/inet/queueing/buffer/PacketBuffer.cc | 19 ++- src/inet/queueing/contract/IPacketBuffer.h | 15 ++- src/inet/queueing/contract/IPacketExtractor.h | 38 ++++++ src/inet/queueing/contract/IPacketQueue.cc | 11 ++ src/inet/queueing/contract/IPacketQueue.h | 26 +++- src/inet/queueing/contract/IPacketQueue.ned | 1 + .../contract/PacketQueueRemovalDetails.h | 24 ++++ .../queueing/queue/CompoundPacketQueueBase.cc | 116 ++++++++++++++++- .../queueing/queue/CompoundPacketQueueBase.h | 14 ++ src/inet/queueing/queue/PacketQueue.cc | 67 ++++++++-- src/inet/queueing/queue/PacketQueue.h | 7 +- src/inet/queueing/scheduler/LabelScheduler.cc | 103 ++++++++++++++- src/inet/queueing/scheduler/LabelScheduler.h | 8 +- .../queueing/scheduler/PriorityScheduler.cc | 87 ++++++++++--- .../queueing/scheduler/PriorityScheduler.h | 7 +- src/inet/queueing/scheduler/WrrScheduler.cc | 120 +++++++++++++++++- src/inet/queueing/scheduler/WrrScheduler.h | 13 +- .../PacketQueueDepartureSignal_1.test | 98 ++++++++++++++ 27 files changed, 812 insertions(+), 71 deletions(-) create mode 100644 src/inet/queueing/contract/IPacketExtractor.h create mode 100644 src/inet/queueing/contract/IPacketQueue.cc create mode 100644 src/inet/queueing/contract/PacketQueueRemovalDetails.h create mode 100644 tests/queueing/PacketQueueDepartureSignal_1.test diff --git a/WHATSNEW b/WHATSNEW index e98e81ea3c0..0da1dad4927 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -29,6 +29,36 @@ Notable backward incompatible changes are the following: adaptive rate control, either where a station transmits to several peers or where it sends group-addressed traffic. +2. Packet queue extraction and lifecycle signals + + IPacketQueue now supports queue-lifecycle signals and predicate-based extraction. + Direct C++ implementations of IPacketQueue must implement findPacket(), the + predicate overload of dequeuePacket(), and must emit packetQueueDeparture + with PacketQueueRemovalDetails exactly once whenever a packet is dequeued, + explicitly removed, or destructively dropped. Listeners subscribe to the queue + module and filter the source to exclude descendant queue emissions. Providers + connected to CompoundPacketQueueBase must also implement IPacketExtractor so + that a compound queue preserves the provider's scheduling policy when it + extracts a matching packet. Direct C++ + implementations of IPacketExtractor must replace selected-pointer extraction + with findPacket(predicate) and dequeuePacket(predicate); predicates may be + evaluated repeatedly and must be stable and side-effect free throughout one + logical selection. WrrScheduler, + LabelScheduler, and PriorityScheduler accept ordinary IPassivePacketSource + inputs; they require IPacketCollection only when collection/aggregate access is + used and IPacketExtractor only when predicate extraction is used, reporting the + unsupported operation lazily. + PriorityScheduler aggregate queries no longer return -1 when an input lacks + IPacketCollection; getNumPackets() and getTotalLength() now report that + unsupported operation with cRuntimeError. Update callers that treated -1 as + an unknown aggregate size, or connect collection-capable providers. + IPacketBuffer::ICallback also provides an optional handlePacketDropped() + notification. Shared buffers invoke it only after all victims selected by one + overload operation have been detached from their owning queues. Compound + queues propagate destructive drops through arbitrary non-queue wrappers and + nested compound queues exactly once. PacketBuffer rejects packets owned by a + cPacketQueue whose owner cannot participate in the buffer callback contract. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/doc/src/users-guide/ch-diffserv.rst b/doc/src/users-guide/ch-diffserv.rst index 03302b6e30c..ac601d0d30e 100644 --- a/doc/src/users-guide/ch-diffserv.rst +++ b/doc/src/users-guide/ch-diffserv.rst @@ -282,9 +282,11 @@ interface is ready to transmit one. They have several input gates and one output gate. Modules that are connected to the inputs of a scheduler must implement -the :cpp:`IPacketQueue` C++ interface. Schedulers also implement the -:cpp:`IPacketQueue` interface, so they can be cascaded to other -schedulers and used as the output module of :ned:`IPacketQueue`'s. +the :cpp:`IPassivePacketSource` C++ interface. Collection and predicate +extraction operations additionally require the corresponding input provider +to implement :cpp:`IPacketCollection` and :cpp:`IPacketExtractor`, respectively. +Schedulers can be cascaded and used as the output module of compound packet +queues when those additional interfaces are available. There are several possible scheduling disciplines (first come/first served, priority, weighted fair, weighted round-robin, deadline-based, @@ -589,4 +591,3 @@ implement three different drop priorities within the class. BE packets are stored in a drop tail queue. Packets from AFxy and BE queues are scheduled by a WRR scheduler, which ensures that the remaining bandwidth is allocated among the classes according to the specified weights. - diff --git a/src/inet/queueing/base/PacketFlowBase.cc b/src/inet/queueing/base/PacketFlowBase.cc index 3ccc24cb908..96eb2ca9716 100644 --- a/src/inet/queueing/base/PacketFlowBase.cc +++ b/src/inet/queueing/base/PacketFlowBase.cc @@ -24,6 +24,7 @@ void PacketFlowBase::initialize(int stage) provider.reference(inputGate, false); collector.reference(outputGate, false); collection.reference(inputGate, false); + packetExtractor.reference(inputGate, false); WATCH(inProgressStreamId); } else if (stage == INITSTAGE_QUEUEING) { @@ -168,6 +169,24 @@ Packet *PacketFlowBase::pullPacket(const cGate *gate) return packet; } +Packet *PacketFlowBase::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + checkPacketStreaming(nullptr); + auto packet = packetExtractor->dequeuePacket(predicate); + if (packet == nullptr) + return nullptr; + take(packet); + emit(packetPulledInSignal, packet); + processPacket(packet); + handlePacketProcessed(packet); + emit(packetPulledOutSignal, packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; +} + Packet *PacketFlowBase::pullPacketStart(const cGate *gate, bps datarate) { Enter_Method("pullPacketStart"); @@ -234,4 +253,3 @@ void PacketFlowBase::handlePullPacketProcessed(Packet *packet, const cGate *gate } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/base/PacketFlowBase.h b/src/inet/queueing/base/PacketFlowBase.h index d0725d5e173..ffaa505784f 100644 --- a/src/inet/queueing/base/PacketFlowBase.h +++ b/src/inet/queueing/base/PacketFlowBase.h @@ -16,17 +16,19 @@ #include "inet/queueing/common/PassivePacketSourceRef.h" #include "inet/queueing/contract/IPacketCollection.h" #include "inet/queueing/contract/IPacketFlow.h" +#include "inet/queueing/contract/IPacketExtractor.h" namespace inet { namespace queueing { -class INET_API PacketFlowBase : public PacketProcessorBase, public virtual IPacketFlow, public virtual IPacketCollection +class INET_API PacketFlowBase : public PacketProcessorBase, public virtual IPacketFlow, public virtual IPacketCollection, public virtual IPacketExtractor { protected: cGate *inputGate = nullptr; ActivePacketSourceRef producer; PassivePacketSourceRef provider; ModuleRef collection; + ModuleRef packetExtractor; cGate *outputGate = nullptr; PassivePacketSinkRef consumer; @@ -82,6 +84,8 @@ class INET_API PacketFlowBase : public PacketProcessorBase, public virtual IPack virtual Packet *getPacket(int index) const override { return collection->getPacket(index); } virtual bool isEmpty() const override { return collection->isEmpty(); } virtual void removePacket(Packet *packet) override { collection->removePacket(packet); } + virtual Packet *findPacket(const PacketPredicate& predicate) const override { return packetExtractor->findPacket(predicate); } + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override { collection->removeAllPackets(); } }; @@ -89,4 +93,3 @@ class INET_API PacketFlowBase : public PacketProcessorBase, public virtual IPack } // namespace inet #endif - diff --git a/src/inet/queueing/base/PacketGateBase.cc b/src/inet/queueing/base/PacketGateBase.cc index 69d237af107..126e2340345 100644 --- a/src/inet/queueing/base/PacketGateBase.cc +++ b/src/inet/queueing/base/PacketGateBase.cc @@ -98,6 +98,20 @@ void PacketGateBase::removePacket(Packet *packet) PacketFlowBase::removePacket(packet); } +Packet *PacketGateBase::findPacket(const PacketPredicate& predicate) const +{ + if (!isOpen()) + return nullptr; + auto packet = PacketFlowBase::findPacket(predicate); + return packet != nullptr && canPacketFlowThrough(packet) ? packet : nullptr; +} + +Packet *PacketGateBase::dequeuePacket(const PacketPredicate& predicate) +{ + auto packet = findPacket(predicate); + return packet == nullptr ? nullptr : PacketFlowBase::dequeuePacket([packet](const Packet *candidate) { return candidate == packet; }); +} + void PacketGateBase::removeAllPackets() { if (isOpen()) @@ -162,4 +176,3 @@ void PacketGateBase::refreshDisplay() const } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/base/PacketGateBase.h b/src/inet/queueing/base/PacketGateBase.h index 9ccb2701836..c94cc662777 100644 --- a/src/inet/queueing/base/PacketGateBase.h +++ b/src/inet/queueing/base/PacketGateBase.h @@ -53,6 +53,8 @@ class INET_API PacketGateBase : public PacketFlowBase, public TransparentProtoco virtual Packet* getPacket(int index) const override; virtual bool isEmpty() const override; virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; virtual IPassivePacketSink *getConsumer(const cGate *gate) override { return this; } @@ -75,4 +77,3 @@ class INET_API PacketGateBase : public PacketFlowBase, public TransparentProtoco } // namespace inet #endif - diff --git a/src/inet/queueing/base/PacketQueueBase.cc b/src/inet/queueing/base/PacketQueueBase.cc index 4780fa44fb7..6c0a2b5e8a7 100644 --- a/src/inet/queueing/base/PacketQueueBase.cc +++ b/src/inet/queueing/base/PacketQueueBase.cc @@ -7,8 +7,12 @@ #include "inet/queueing/base/PacketQueueBase.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + #include "inet/common/Simsignals.h" +#include "inet/common/PacketEventTag.h" #include "inet/common/StringFormat.h" +#include "inet/common/TimeTag.h" namespace inet { namespace queueing { @@ -53,6 +57,21 @@ Packet *PacketQueueBase::dequeuePacket() return packet; } +void PacketQueueBase::notifyPacketRemoved(Packet *packet, IPacketQueue::PacketRemovalReason reason) +{ + PacketQueueRemovalDetails details(reason); + emit(IPacketQueue::packetQueueDepartureSignal, packet, &details); +} + +void PacketQueueBase::recordPacketDequeued(Packet *packet) +{ + auto queueingTime = simTime() - packet->getArrivalTime(); + auto packetEvent = new PacketEvent(); + insertPacketEvent(this, packet, PEK_QUEUED, 0, queueingTime, packetEvent); + increaseTimeTag(packet, queueingTime, queueingTime); + emit(packetPulledSignal, packet); +} + void PacketQueueBase::emit(simsignal_t signal, cObject *object, cObject *details) { if (signal == packetPushedSignal || signal == packetPushStartedSignal) @@ -92,4 +111,3 @@ std::string PacketQueueBase::resolveDirective(char directive) const } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/base/PacketQueueBase.h b/src/inet/queueing/base/PacketQueueBase.h index 97fcaffd133..b7cc572a995 100644 --- a/src/inet/queueing/base/PacketQueueBase.h +++ b/src/inet/queueing/base/PacketQueueBase.h @@ -31,6 +31,8 @@ class INET_API PacketQueueBase : public PacketProcessorBase, public virtual IPac virtual void handleMessage(cMessage *message) override; virtual void emit(simsignal_t signal, cObject *object, cObject *details = nullptr) override; + virtual void recordPacketDequeued(Packet *packet); + virtual void notifyPacketRemoved(Packet *packet, IPacketQueue::PacketRemovalReason reason); virtual std::string resolveDirective(char directive) const override; @@ -41,6 +43,7 @@ class INET_API PacketQueueBase : public PacketProcessorBase, public virtual IPac virtual void enqueuePacket(Packet *packet) override; virtual Packet *dequeuePacket() override; + public: virtual void pushPacketStart(Packet *packet, const cGate *gate, bps datarate) override { throw cRuntimeError("Invalid operation"); } virtual void pushPacketEnd(Packet *packet, const cGate *gate) override { throw cRuntimeError("Invalid operation"); } @@ -55,4 +58,3 @@ class INET_API PacketQueueBase : public PacketProcessorBase, public virtual IPac } // namespace inet #endif - diff --git a/src/inet/queueing/base/PacketQueueBase.ned b/src/inet/queueing/base/PacketQueueBase.ned index 9075cc9745a..a583ec1ce67 100644 --- a/src/inet/queueing/base/PacketQueueBase.ned +++ b/src/inet/queueing/base/PacketQueueBase.ned @@ -25,6 +25,7 @@ package inet.queueing.base; simple PacketQueueBase extends PacketProcessorBase { parameters: + @signal[packetQueueDeparture](type=inet::Packet); // Details: PacketQueueRemovalDetails; source identifies the logical queue displayStringTextFormat = default("contains {numPackets} pk ({totalLength})\npushed {numPushedPackets} pulled {numPulledPackets} dropped {numDroppedPackets}"); // Determines display string text above the submodule @display("i=block/queue"); gates: diff --git a/src/inet/queueing/buffer/PacketBuffer.cc b/src/inet/queueing/buffer/PacketBuffer.cc index a67da4be8ca..f554f5f12f9 100644 --- a/src/inet/queueing/buffer/PacketBuffer.cc +++ b/src/inet/queueing/buffer/PacketBuffer.cc @@ -53,21 +53,29 @@ b PacketBuffer::getTotalLength() const void PacketBuffer::addPacket(Packet *packet) { Enter_Method("addPacket"); + auto ownerQueue = dynamic_cast(packet->getOwner()); + if (ownerQueue != nullptr && dynamic_cast(ownerQueue->getOwner()) == nullptr) + throw cRuntimeError("Cannot buffer packet owned by cPacketQueue whose owner does not implement IPacketBuffer::ICallback"); EV_INFO << "Adding packet" << EV_FIELD(packet) << EV_ENDL; emit(packetAddedSignal, packet); packets.push_back(packet); if (isOverloaded()) { if (packetDropperFunction != nullptr) { + std::vector> droppedPackets; while (!isEmpty() && isOverloaded()) { auto packet = packetDropperFunction->selectPacket(this); EV_INFO << "Dropping packet" << EV_FIELD(packet) << EV_ENDL; packets.erase(find(packets, packet)); auto queue = dynamic_cast(packet->getOwner()); - if (queue != nullptr) { - ICallback *callback = dynamic_cast(queue->getOwner()); - if (callback != nullptr) - callback->handlePacketRemoved(packet); - } + auto callback = queue != nullptr ? check_and_cast(queue->getOwner()) : nullptr; + droppedPackets.emplace_back(packet, callback); + } + for (auto& [packet, callback] : droppedPackets) + if (callback != nullptr) + callback->handlePacketDropping(packet); + for (auto& [packet, callback] : droppedPackets) { + if (callback != nullptr) + callback->handlePacketDropped(packet); // TODO maybe the buffer should take ownership and queues should be aware of it take(packet); dropPacket(packet, QUEUE_OVERFLOW); @@ -118,4 +126,3 @@ Packet *PacketBuffer::getPacket(int index) const } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/contract/IPacketBuffer.h b/src/inet/queueing/contract/IPacketBuffer.h index ced7f9556f0..4e0bf6302c8 100644 --- a/src/inet/queueing/contract/IPacketBuffer.h +++ b/src/inet/queueing/contract/IPacketBuffer.h @@ -27,6 +27,20 @@ class INET_API IPacketBuffer : public virtual IPacketCollection * The packet is never nullptr. */ virtual void handlePacketRemoved(Packet *packet) = 0; + + /** + * Detaches a packet selected for an overload drop from its owner before + * drop observers are notified. The default preserves compatibility for + * owners which do not distinguish the two removal phases. + */ + virtual void handlePacketDropping(Packet *packet) { handlePacketRemoved(packet); } + + /** + * Notifies the packet owner after all packets selected by one overload + * operation have been removed from their owners. The default implementation + * preserves compatibility for owners which do not distinguish buffer drops. + */ + virtual void handlePacketDropped(Packet *packet) {} }; public: @@ -47,4 +61,3 @@ class INET_API IPacketBuffer : public virtual IPacketCollection } // namespace inet #endif - diff --git a/src/inet/queueing/contract/IPacketExtractor.h b/src/inet/queueing/contract/IPacketExtractor.h new file mode 100644 index 00000000000..cf2287ceeed --- /dev/null +++ b/src/inet/queueing/contract/IPacketExtractor.h @@ -0,0 +1,38 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IPACKETEXTRACTOR_H +#define __INET_IPACKETEXTRACTOR_H + +#include + +#include "inet/common/packet/Packet.h" + +namespace inet { +namespace queueing { + +/** Provides semantic pull accounting while extracting a selected packet. */ +class INET_API IPacketExtractor +{ + public: + /** + * Predicates must be stable and side-effect free from an initial + * findPacket() through the corresponding dequeuePacket() selection. + * Composite extractors may evaluate the predicate multiple times and on + * multiple candidates while preserving their scheduling policy. + */ + using PacketPredicate = std::function; + + public: + virtual ~IPacketExtractor() {} + virtual Packet *findPacket(const PacketPredicate& predicate) const = 0; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) = 0; +}; + +} // namespace queueing +} // namespace inet + +#endif diff --git a/src/inet/queueing/contract/IPacketQueue.cc b/src/inet/queueing/contract/IPacketQueue.cc new file mode 100644 index 00000000000..0be875f6df5 --- /dev/null +++ b/src/inet/queueing/contract/IPacketQueue.cc @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include "inet/queueing/contract/IPacketQueue.h" + +namespace inet { +namespace queueing { + +simsignal_t IPacketQueue::packetQueueDepartureSignal = cComponent::registerSignal("packetQueueDeparture"); + +} // namespace queueing +} // namespace inet diff --git a/src/inet/queueing/contract/IPacketQueue.h b/src/inet/queueing/contract/IPacketQueue.h index 159fbb9fb91..6d481b6f359 100644 --- a/src/inet/queueing/contract/IPacketQueue.h +++ b/src/inet/queueing/contract/IPacketQueue.h @@ -9,6 +9,7 @@ #define __INET_IPACKETQUEUE_H #include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/contract/IPacketExtractor.h" #include "inet/queueing/contract/IPassivePacketSink.h" #include "inet/queueing/contract/IPassivePacketSource.h" @@ -18,8 +19,22 @@ namespace queueing { /** * This class defines the interface for packet queues. */ -class INET_API IPacketQueue : public virtual IPacketCollection, public virtual IPassivePacketSink, public virtual IPassivePacketSource +class INET_API IPacketQueue : public virtual IPacketCollection, public virtual IPacketExtractor, public virtual IPassivePacketSink, public virtual IPassivePacketSource { + public: + enum class PacketRemovalReason { + DEQUEUED, // Normal queue processing transferred ownership out of the queue. + REMOVED, // Explicit removal outside normal queue processing. + DROPPED, // The queue destructively removed the packet. + }; + + /** + * Emitted once per logical departure, after detachment and before transfer/deletion. + * The Packet and PacketQueueRemovalDetails are borrowed for synchronous delivery. + * Listeners must filter the source to their subscribed logical queue. + */ + static simsignal_t packetQueueDepartureSignal; + public: /** * Enqueues the packet into the packet queue. The onwership of the packet @@ -36,10 +51,17 @@ class INET_API IPacketQueue : public virtual IPacketCollection, public virtual I * The queue must not be empty. The returned packet must not be nullptr. */ virtual Packet *dequeuePacket() = 0; + + virtual Packet *findPacket(const PacketPredicate& predicate) const = 0; + + /** + * Dequeues the first matching packet according to the queue provider's + * scheduling policy. Ownership is transferred to the caller. + */ + virtual Packet *dequeuePacket(const PacketPredicate& predicate) = 0; }; } // namespace queueing } // namespace inet #endif - diff --git a/src/inet/queueing/contract/IPacketQueue.ned b/src/inet/queueing/contract/IPacketQueue.ned index 40120433323..2911872be9d 100644 --- a/src/inet/queueing/contract/IPacketQueue.ned +++ b/src/inet/queueing/contract/IPacketQueue.ned @@ -16,6 +16,7 @@ package inet.queueing.contract; moduleinterface IPacketQueue extends IPassivePacketSink, IPassivePacketSource { parameters: + @signal[packetQueueDeparture](type=inet::Packet); // Details: PacketQueueRemovalDetails; source identifies the logical queue @omittedTypename(OmittedPacketQueue); @display("i=block/queue"); } diff --git a/src/inet/queueing/contract/PacketQueueRemovalDetails.h b/src/inet/queueing/contract/PacketQueueRemovalDetails.h new file mode 100644 index 00000000000..12989ba45bb --- /dev/null +++ b/src/inet/queueing/contract/PacketQueueRemovalDetails.h @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later + +#ifndef __INET_PACKETQUEUEREMOVALDETAILS_H +#define __INET_PACKETQUEUEREMOVALDETAILS_H + +#include "inet/queueing/contract/IPacketQueue.h" + +namespace inet { +namespace queueing { + +class INET_API PacketQueueRemovalDetails : public cObject +{ + protected: + IPacketQueue::PacketRemovalReason reason; + + public: + explicit PacketQueueRemovalDetails(IPacketQueue::PacketRemovalReason reason) : reason(reason) {} + IPacketQueue::PacketRemovalReason getReason() const { return reason; } +}; + +} // namespace queueing +} // namespace inet + +#endif diff --git a/src/inet/queueing/queue/CompoundPacketQueueBase.cc b/src/inet/queueing/queue/CompoundPacketQueueBase.cc index 7d290d0cc1e..2f77ec4c23b 100644 --- a/src/inet/queueing/queue/CompoundPacketQueueBase.cc +++ b/src/inet/queueing/queue/CompoundPacketQueueBase.cc @@ -7,6 +7,8 @@ #include "inet/queueing/queue/CompoundPacketQueueBase.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + #include "inet/common/Simsignals.h" namespace inet { @@ -14,6 +16,22 @@ namespace queueing { Define_Module(CompoundPacketQueueBase); +class ScopedPacketRemoval +{ + protected: + Packet *&packetBeingRemoved; + Packet *previousPacket; + + public: + ScopedPacketRemoval(Packet *&packetBeingRemoved, Packet *packet) : + packetBeingRemoved(packetBeingRemoved), previousPacket(packetBeingRemoved) + { + packetBeingRemoved = packet; + } + + ~ScopedPacketRemoval() { packetBeingRemoved = previousPacket; } +}; + void CompoundPacketQueueBase::initialize(int stage) { PacketQueueBase::initialize(stage); @@ -23,6 +41,11 @@ void CompoundPacketQueueBase::initialize(int stage) consumer.reference(inputGate, true, 1); provider.reference(outputGate, true, -1); collection = check_and_cast(provider.get()); + packetExtractor = check_and_cast(provider.get()); + // Observe the nearest queue on every descendant branch. Nested + // compound queues forward their own frontier, so stopping at a queue + // avoids duplicate notifications while traversing non-queue wrappers. + registerQueueFrontier(this); packetDropperFunction = createDropperFunction(par("dropperClass")); subscribe(packetDroppedSignal, this); subscribe(packetCreatedSignal, this); @@ -34,6 +57,39 @@ void CompoundPacketQueueBase::initialize(int stage) } } +void CompoundPacketQueueBase::registerQueueFrontier(cModule *module) +{ + for (cModule::SubmoduleIterator it(module); !it.end(); it++) { + auto childModule = *it; + auto childQueue = dynamic_cast(childModule); + if (childQueue != nullptr) { + childQueues.push_back(childQueue); + childModule->subscribe(IPacketQueue::packetQueueDepartureSignal, this); + } + else + registerQueueFrontier(childModule); + } +} + +void CompoundPacketQueueBase::finish() +{ + unsubscribeChildQueues(); + PacketQueueBase::finish(); +} + +void CompoundPacketQueueBase::preDelete(cComponent *root) +{ + unsubscribeChildQueues(); + PacketQueueBase::preDelete(root); +} + +void CompoundPacketQueueBase::unsubscribeChildQueues() +{ + for (auto childQueue : childQueues) + check_and_cast(childQueue)->unsubscribe(IPacketQueue::packetQueueDepartureSignal, this); + childQueues.clear(); +} + IPacketDropperFunction *CompoundPacketQueueBase::createDropperFunction(const char *dropperClass) const { if (strlen(dropperClass) == 0) @@ -58,11 +114,20 @@ void CompoundPacketQueueBase::pushPacket(Packet *packet, const cGate *gate) EV_INFO << "Pushing packet" << EV_FIELD(packet) << EV_ENDL; consumer.pushPacket(packet); if (packetDropperFunction != nullptr) { + std::vector droppedPackets; while (isOverloaded()) { auto packet = packetDropperFunction->selectPacket(this); EV_INFO << "Dropping packet" << EV_FIELD(packet) << EV_ENDL; - removePacket(packet); + { + ScopedPacketRemoval scopedPacketRemoval(packetBeingRemoved, packet); + collection->removePacket(packet); + } + emit(packetRemovedSignal, packet); take(packet); + droppedPackets.push_back(packet); + } + for (auto packet : droppedPackets) { + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DROPPED); dropPacket(packet, QUEUE_OVERFLOW); } } @@ -76,6 +141,7 @@ Packet *CompoundPacketQueueBase::pullPacket(const cGate *gate) Enter_Method("pullPacket"); auto packet = provider.pullPacket(); take(packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DEQUEUED); emit(packetPulledSignal, packet); return packet; } @@ -83,14 +149,43 @@ Packet *CompoundPacketQueueBase::pullPacket(const cGate *gate) void CompoundPacketQueueBase::removePacket(Packet *packet) { Enter_Method("removePacket"); - collection->removePacket(packet); + { + ScopedPacketRemoval scopedPacketRemoval(packetBeingRemoved, packet); + collection->removePacket(packet); + } + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::REMOVED); emit(packetRemovedSignal, packet); } +Packet *CompoundPacketQueueBase::findPacket(const PacketPredicate& predicate) const +{ + return packetExtractor->findPacket(predicate); +} + +Packet *CompoundPacketQueueBase::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + auto packet = packetExtractor->dequeuePacket(predicate); + if (packet == nullptr) + return nullptr; + take(packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DEQUEUED); + // The owning leaf/provider has already recorded queue residence. The + // compound boundary mirrors pullPacket() and emits only its pull event. + emit(packetPulledSignal, packet); + drop(packet); + return packet; +} + void CompoundPacketQueueBase::removeAllPackets() { Enter_Method("removeAllPacket"); - collection->removeAllPackets(); + while (getNumPackets() != 0) { + auto packet = getPacket(0); + removePacket(packet); + take(packet); + delete packet; + } } bool CompoundPacketQueueBase::canPushSomePacket(const cGate *gate) const @@ -118,7 +213,11 @@ bool CompoundPacketQueueBase::canPushPacket(Packet *packet, const cGate *gate) c void CompoundPacketQueueBase::receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signal)); - if (signal == packetDroppedSignal) + if (signal == IPacketQueue::packetQueueDepartureSignal) { + if (source != this && source->isSubscribed(signal, this)) + handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); + } + else if (signal == packetDroppedSignal) numDroppedPackets++; else if (signal == packetCreatedSignal) numCreatedPackets++; @@ -126,6 +225,13 @@ void CompoundPacketQueueBase::receiveSignal(cComponent *source, simsignal_t sign throw cRuntimeError("Unknown signal"); } +void CompoundPacketQueueBase::handlePacketRemoved(Packet *packet, IPacketQueue::PacketRemovalReason reason) +{ + Enter_Method("handlePacketRemoved"); + if (reason == IPacketQueue::PacketRemovalReason::DROPPED || + (reason == IPacketQueue::PacketRemovalReason::REMOVED && packet != packetBeingRemoved)) + notifyPacketRemoved(packet, reason); +} + } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/queue/CompoundPacketQueueBase.h b/src/inet/queueing/queue/CompoundPacketQueueBase.h index 706d9fb32c5..c89072bdf10 100644 --- a/src/inet/queueing/queue/CompoundPacketQueueBase.h +++ b/src/inet/queueing/queue/CompoundPacketQueueBase.h @@ -26,17 +26,28 @@ class INET_API CompoundPacketQueueBase : public PacketQueueBase, public cListene PassivePacketSinkRef consumer; PassivePacketSourceRef provider; IPacketCollection *collection = nullptr; + IPacketExtractor *packetExtractor = nullptr; + std::vector childQueues; + Packet *packetBeingRemoved = nullptr; IPacketDropperFunction *packetDropperFunction = nullptr; protected: + using cListener::finish; + virtual void initialize(int stage) override; + virtual void finish() override; + virtual void preDelete(cComponent *root) override; + virtual void registerQueueFrontier(cModule *module); + virtual void unsubscribeChildQueues(); virtual IPacketDropperFunction *createDropperFunction(const char *dropperClass) const; virtual bool isOverloaded() const; public: + using PacketQueueBase::dequeuePacket; + virtual ~CompoundPacketQueueBase() { delete packetDropperFunction; } virtual int getMaxNumPackets() const override { return packetCapacity; } @@ -48,6 +59,8 @@ class INET_API CompoundPacketQueueBase : public PacketQueueBase, public cListene virtual bool isEmpty() const override { return collection->isEmpty(); } virtual Packet *getPacket(int index) const override { return collection->getPacket(index); } virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; virtual bool supportsPacketPushing(const cGate *gate) const override { return inputGate == gate; } @@ -61,6 +74,7 @@ class INET_API CompoundPacketQueueBase : public PacketQueueBase, public cListene virtual Packet *pullPacket(const cGate *gate) override; virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; + virtual void handlePacketRemoved(Packet *packet, IPacketQueue::PacketRemovalReason reason); }; } // namespace queueing diff --git a/src/inet/queueing/queue/PacketQueue.cc b/src/inet/queueing/queue/PacketQueue.cc index 5cb89c3a638..9a6f553ce48 100644 --- a/src/inet/queueing/queue/PacketQueue.cc +++ b/src/inet/queueing/queue/PacketQueue.cc @@ -8,9 +8,7 @@ #include "inet/queueing/queue/PacketQueue.h" #include "inet/common/ModuleAccess.h" -#include "inet/common/PacketEventTag.h" #include "inet/common/Simsignals.h" -#include "inet/common/TimeTag.h" #include "inet/queueing/function/PacketComparatorFunction.h" #include "inet/queueing/function/PacketDropperFunction.h" @@ -100,10 +98,15 @@ void PacketQueue::pushPacket(Packet *packet, const cGate *gate) throw cRuntimeError("Queue is overloaded while using a packet buffer"); } else if (packetDropperFunction != nullptr) { + std::vector droppedPackets; while (isOverloaded()) { auto packet = packetDropperFunction->selectPacket(this); EV_INFO << "Dropping packet" << EV_FIELD(packet) << EV_ENDL; queue.remove(packet); + droppedPackets.push_back(packet); + } + for (auto packet : droppedPackets) { + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DROPPED); dropPacket(packet, QUEUE_OVERFLOW); } } @@ -126,16 +129,41 @@ Packet *PacketQueue::pullPacket(const cGate *gate) } else queue.pop(); - auto queueingTime = simTime() - packet->getArrivalTime(); - auto packetEvent = new PacketEvent(); - insertPacketEvent(this, packet, PEK_QUEUED, 0, queueingTime, packetEvent); - increaseTimeTag(packet, queueingTime, queueingTime); - emit(packetPulledSignal, packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DEQUEUED); + recordPacketDequeued(packet); if (collector != nullptr) animatePullPacket(packet, outputGate, collector.getReferencedGate()); return packet; } +Packet *PacketQueue::findPacket(const PacketPredicate& predicate) const +{ + for (int i = 0; i < queue.getLength(); i++) { + auto packet = check_and_cast(queue.get(i)); + if (predicate(packet)) + return packet; + } + return nullptr; +} + +Packet *PacketQueue::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + auto packet = findPacket(predicate); + if (packet == nullptr) + return nullptr; + EV_INFO << "Dequeuing packet" << EV_FIELD(packet) << EV_ENDL; + queue.remove(packet); + if (buffer != nullptr) + buffer->removePacket(packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DEQUEUED); + recordPacketDequeued(packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; +} + void PacketQueue::removePacket(Packet *packet) { Enter_Method("removePacket"); @@ -143,6 +171,7 @@ void PacketQueue::removePacket(Packet *packet) queue.remove(packet); if (buffer != nullptr) buffer->removePacket(packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::REMOVED); emit(packetRemovedSignal, packet); } @@ -151,11 +180,13 @@ void PacketQueue::removeAllPackets() Enter_Method("removeAllPackets"); EV_INFO << "Removing all packets" << EV_ENDL; std::vector packets; - for (int i = 0; i < getNumPackets(); i++) + while (!queue.isEmpty()) packets.push_back(check_and_cast(queue.pop())); if (buffer != nullptr) - buffer->removeAllPackets(); + for (auto packet : packets) + buffer->removePacket(packet); for (auto packet : packets) { + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::REMOVED); emit(packetRemovedSignal, packet); delete packet; } @@ -190,9 +221,25 @@ void PacketQueue::handlePacketRemoved(Packet *packet) EV_INFO << "Removing packet" << EV_FIELD(packet) << EV_ENDL; queue.remove(packet); emit(packetRemovedSignal, packet); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::REMOVED); } } +void PacketQueue::handlePacketDropping(Packet *packet) +{ + Enter_Method("handlePacketDropping"); + if (queue.contains(packet)) { + EV_INFO << "Removing packet" << EV_FIELD(packet) << EV_ENDL; + queue.remove(packet); + emit(packetRemovedSignal, packet); + } +} + +void PacketQueue::handlePacketDropped(Packet *packet) +{ + Enter_Method("handlePacketDropped"); + notifyPacketRemoved(packet, IPacketQueue::PacketRemovalReason::DROPPED); +} + } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/queue/PacketQueue.h b/src/inet/queueing/queue/PacketQueue.h index 4debf8541ec..2892da932ec 100644 --- a/src/inet/queueing/queue/PacketQueue.h +++ b/src/inet/queueing/queue/PacketQueue.h @@ -45,6 +45,8 @@ class INET_API PacketQueue : public PacketQueueBase, public IPacketBuffer::ICall virtual bool isOverloaded() const; public: + using PacketQueueBase::dequeuePacket; + virtual ~PacketQueue() { delete packetDropperFunction; } virtual cGate *getRegistrationForwardingGate(cGate *gate) override; @@ -58,6 +60,8 @@ class INET_API PacketQueue : public PacketQueueBase, public IPacketBuffer::ICall virtual bool isEmpty() const override { return getNumPackets() == 0; } virtual Packet *getPacket(int index) const override; virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; virtual bool supportsPacketPushing(const cGate *gate) const override { return inputGate == gate; } @@ -71,10 +75,11 @@ class INET_API PacketQueue : public PacketQueueBase, public IPacketBuffer::ICall virtual Packet *pullPacket(const cGate *gate) override; virtual void handlePacketRemoved(Packet *packet) override; + virtual void handlePacketDropping(Packet *packet) override; + virtual void handlePacketDropped(Packet *packet) override; }; } // namespace queueing } // namespace inet #endif - diff --git a/src/inet/queueing/scheduler/LabelScheduler.cc b/src/inet/queueing/scheduler/LabelScheduler.cc index af30138c225..f46dd2d2794 100644 --- a/src/inet/queueing/scheduler/LabelScheduler.cc +++ b/src/inet/queueing/scheduler/LabelScheduler.cc @@ -20,40 +20,130 @@ void LabelScheduler::initialize(int stage) if (stage == INITSTAGE_LOCAL) { defaultGateIndex = par("defaultGateIndex"); labels = cStringTokenizer(par("labels")).asVector(); - for (auto provider : providers) - collections.push_back(dynamic_cast(provider.get())); + for (size_t i = 0; i < providers.size(); i++) { + auto provider = providers[i].get(); + auto collection = dynamic_cast(provider); + auto packetExtractor = dynamic_cast(provider); + collections.push_back(collection); + packetExtractors.push_back(packetExtractor); + } } } int LabelScheduler::getNumPackets() const { int size = 0; - for (auto collection : collections) + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getNumPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); size += collection->getNumPackets(); + } return size; } b LabelScheduler::getTotalLength() const { b totalLength(0); - for (auto collection : collections) + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getTotalLength(): input provider at gate index %d does not implement IPacketCollection", (int)i); totalLength += collection->getTotalLength(); + } return totalLength; } Packet *LabelScheduler::getPacket(int index) const { - throw cRuntimeError("TODO"); + int originalIndex = index; + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot getPacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + auto numPackets = collection->getNumPackets(); + if (index < numPackets) + return collection->getPacket(index); + index -= numPackets; + } + throw cRuntimeError("Index %i out of range", originalIndex); } void LabelScheduler::removePacket(Packet *packet) { - throw cRuntimeError("TODO"); + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removePacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + for (int i = 0; i < collection->getNumPackets(); i++) { + if (collection->getPacket(i) == packet) { + collection->removePacket(packet); + return; + } + } + } + throw cRuntimeError("Cannot find packet"); +} + +int LabelScheduler::findInput(const PacketPredicate& predicate) const +{ + std::vector candidates; + for (size_t i = 0; i < packetExtractors.size(); i++) { + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + candidates.push_back(packetExtractors[i]->findPacket(predicate)); + } + for (auto label : labels) { + for (size_t i = 0; i < candidates.size(); i++) { + auto packet = candidates[i]; + if (packet == nullptr) + continue; + const auto& labelsTag = packet->findTag(); + if (labelsTag != nullptr) { + for (size_t j = 0; j < labelsTag->getLabelsArraySize(); j++) + if (label == labelsTag->getLabels(j)) + return i; + } + } + } + return defaultGateIndex >= 0 && defaultGateIndex < (int)candidates.size() && candidates[defaultGateIndex] != nullptr ? defaultGateIndex : -1; +} + +Packet *LabelScheduler::findPacket(const PacketPredicate& predicate) const +{ + auto index = findInput(predicate); + return index == -1 ? nullptr : packetExtractors[index]->findPacket(predicate); +} + +Packet *LabelScheduler::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot dequeuePacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + auto index = findInput(predicate); + if (index == -1) + return nullptr; + auto packet = packetExtractors[index]->dequeuePacket(predicate); + ASSERT(packet != nullptr); + take(packet); + handlePacketProcessed(packet); + emit(packetPulledSignal, packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; } void LabelScheduler::removeAllPackets() { Enter_Method("removeAllPackets"); + for (size_t i = 0; i < collections.size(); i++) { + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removeAllPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); + } for (auto collection : collections) collection->removeAllPackets(); } @@ -76,4 +166,3 @@ int LabelScheduler::schedulePacket() } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/scheduler/LabelScheduler.h b/src/inet/queueing/scheduler/LabelScheduler.h index bb03cd04af6..aca5ce7b84d 100644 --- a/src/inet/queueing/scheduler/LabelScheduler.h +++ b/src/inet/queueing/scheduler/LabelScheduler.h @@ -10,20 +10,23 @@ #include "inet/queueing/base/PacketSchedulerBase.h" #include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/contract/IPacketExtractor.h" namespace inet { namespace queueing { -class INET_API LabelScheduler : public PacketSchedulerBase, public virtual IPacketCollection +class INET_API LabelScheduler : public PacketSchedulerBase, public virtual IPacketCollection, public virtual IPacketExtractor { protected: int defaultGateIndex = -1; std::vector labels; std::vector collections; + std::vector packetExtractors; protected: virtual void initialize(int stage) override; virtual int schedulePacket() override; + virtual int findInput(const PacketPredicate& predicate) const; public: virtual int getMaxNumPackets() const override { return -1; } @@ -35,6 +38,8 @@ class INET_API LabelScheduler : public PacketSchedulerBase, public virtual IPack virtual bool isEmpty() const override { return getNumPackets() == 0; } virtual Packet *getPacket(int index) const override; virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; }; @@ -42,4 +47,3 @@ class INET_API LabelScheduler : public PacketSchedulerBase, public virtual IPack } // namespace inet #endif - diff --git a/src/inet/queueing/scheduler/PriorityScheduler.cc b/src/inet/queueing/scheduler/PriorityScheduler.cc index 346d2b313c2..be772b00d99 100644 --- a/src/inet/queueing/scheduler/PriorityScheduler.cc +++ b/src/inet/queueing/scheduler/PriorityScheduler.cc @@ -16,37 +16,47 @@ void PriorityScheduler::initialize(int stage) { PacketSchedulerBase::initialize(stage); if (stage == INITSTAGE_LOCAL) { - for (auto provider : providers) - collections.push_back(dynamic_cast(provider.get())); + for (size_t i = 0; i < providers.size(); i++) { + auto provider = providers[i].get(); + collections.push_back(dynamic_cast(provider)); + auto packetExtractor = dynamic_cast(provider); + packetExtractors.push_back(packetExtractor); + } } } int PriorityScheduler::getNumPackets() const { int size = 0; - for (auto collection : collections) - if (collection != nullptr) - size += collection->getNumPackets(); - else - return -1; + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getNumPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); + size += collection->getNumPackets(); + } return size; } b PriorityScheduler::getTotalLength() const { b totalLength(0); - for (auto collection : collections) - if (collection != nullptr) - totalLength += collection->getTotalLength(); - else - return b(-1); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getTotalLength(): input provider at gate index %d does not implement IPacketCollection", (int)i); + totalLength += collection->getTotalLength(); + } return totalLength; } Packet *PriorityScheduler::getPacket(int index) const { int origIndex = index; - for (auto collection : collections) { + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot getPacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; auto numPackets = collection->getNumPackets(); if (index < numPackets) return collection->getPacket(index); @@ -59,7 +69,11 @@ Packet *PriorityScheduler::getPacket(int index) const void PriorityScheduler::removePacket(Packet *packet) { Enter_Method("removePacket"); - for (auto collection : collections) { + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removePacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; int numPackets = collection->getNumPackets(); for (int j = 0; j < numPackets; j++) { if (collection->getPacket(j) == packet) { @@ -71,9 +85,53 @@ void PriorityScheduler::removePacket(Packet *packet) throw cRuntimeError("Cannot find packet"); } +Packet *PriorityScheduler::findPacket(const PacketPredicate& predicate) const +{ + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto index = reverseOrder ? collections.size() - i - 1 : i; + if (packetExtractors[index] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)index); + auto packet = packetExtractors[index]->findPacket(predicate); + if (packet != nullptr) + return packet; + } + return nullptr; +} + +Packet *PriorityScheduler::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot dequeuePacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto index = reverseOrder ? collections.size() - i - 1 : i; + if (packetExtractors[index] == nullptr) + throw cRuntimeError("Cannot dequeuePacket(): input provider at gate index %d does not implement IPacketExtractor", (int)index); + auto packet = packetExtractors[index]->dequeuePacket(predicate); + if (packet == nullptr) + continue; + take(packet); + handlePacketProcessed(packet); + emit(packetPulledSignal, packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; + } + return nullptr; +} + void PriorityScheduler::removeAllPackets() { Enter_Method("removeAllPackets"); + for (size_t i = 0; i < collections.size(); i++) { + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removeAllPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); + } for (auto collection : collections) collection->removeAllPackets(); } @@ -107,4 +165,3 @@ void PriorityScheduler::handleCanPullPacketChanged(const cGate *gate) } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/scheduler/PriorityScheduler.h b/src/inet/queueing/scheduler/PriorityScheduler.h index 09d6af3ffb9..703cc6a0ada 100644 --- a/src/inet/queueing/scheduler/PriorityScheduler.h +++ b/src/inet/queueing/scheduler/PriorityScheduler.h @@ -10,14 +10,16 @@ #include "inet/queueing/base/PacketSchedulerBase.h" #include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/contract/IPacketExtractor.h" namespace inet { namespace queueing { -class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IPacketCollection +class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IPacketCollection, public virtual IPacketExtractor { protected: std::vector collections; + std::vector packetExtractors; protected: virtual void initialize(int stage) override; @@ -33,6 +35,8 @@ class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IP virtual bool isEmpty() const override { return getNumPackets() == 0; } virtual Packet *getPacket(int index) const override; virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; virtual void handleCanPullPacketChanged(const cGate *gate) override; @@ -42,4 +46,3 @@ class INET_API PriorityScheduler : public PacketSchedulerBase, public virtual IP } // namespace inet #endif - diff --git a/src/inet/queueing/scheduler/WrrScheduler.cc b/src/inet/queueing/scheduler/WrrScheduler.cc index a551bb69526..fbe6535eca2 100644 --- a/src/inet/queueing/scheduler/WrrScheduler.cc +++ b/src/inet/queueing/scheduler/WrrScheduler.cc @@ -37,30 +37,141 @@ void WrrScheduler::initialize(int stage) if (tokenizer.hasMoreTokens()) throw cRuntimeError("Too many values given in the weights parameter."); - for (auto provider : providers) - collections.push_back(dynamic_cast(provider.get())); + for (size_t i = 0; i < providers.size(); i++) { + auto provider = providers[i].get(); + auto collection = dynamic_cast(provider); + auto packetExtractor = dynamic_cast(provider); + collections.push_back(collection); + packetExtractors.push_back(packetExtractor); + } } } int WrrScheduler::getNumPackets() const { int size = 0; - for (auto collection : collections) + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getNumPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); size += collection->getNumPackets(); + } return size; } b WrrScheduler::getTotalLength() const { b totalLength(0); - for (auto collection : collections) + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + if (collection == nullptr) + throw cRuntimeError("Cannot getTotalLength(): input provider at gate index %d does not implement IPacketCollection", (int)i); totalLength += collection->getTotalLength(); + } return totalLength; } +Packet *WrrScheduler::getPacket(int index) const +{ + int originalIndex = index; + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot getPacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + auto numPackets = collection->getNumPackets(); + if (index < numPackets) + return collection->getPacket(index); + index -= numPackets; + } + throw cRuntimeError("Index %i out of range", originalIndex); +} + +void WrrScheduler::removePacket(Packet *packet) +{ + for (size_t i = 0; i < collections.size(); i++) + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removePacket(): input provider at gate index %d does not implement IPacketCollection", (int)i); + for (size_t i = 0; i < collections.size(); i++) { + auto collection = collections[i]; + for (int i = 0; i < collection->getNumPackets(); i++) { + if (collection->getPacket(i) == packet) { + collection->removePacket(packet); + return; + } + } + } + throw cRuntimeError("Cannot find packet"); +} + +int WrrScheduler::findInput(const PacketPredicate& predicate) const +{ + int firstWeighted = -1; + int firstNonWeighted = -1; + for (size_t i = 0; i < collections.size(); ++i) { + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + if (packetExtractors[i]->findPacket(predicate) != nullptr) { + if (buckets[i] > 0) + return i; + else if (firstWeighted == -1 && weights[i] > 0) + firstWeighted = i; + else if (firstNonWeighted == -1 && weights[i] == 0) + firstNonWeighted = i; + } + } + return firstWeighted != -1 ? firstWeighted : firstNonWeighted; +} + +void WrrScheduler::consumeBucket(int index) +{ + if (weights[index] == 0) + return; + if (buckets[index] == 0) { + for (size_t i = 0; i < collections.size(); ++i) + buckets[i] = weights[i]; + } + ASSERT(buckets[index] > 0); + buckets[index]--; +} + +Packet *WrrScheduler::findPacket(const PacketPredicate& predicate) const +{ + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot findPacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + auto index = findInput(predicate); + return index == -1 ? nullptr : packetExtractors[index]->findPacket(predicate); +} + +Packet *WrrScheduler::dequeuePacket(const PacketPredicate& predicate) +{ + Enter_Method("dequeuePacket"); + for (size_t i = 0; i < packetExtractors.size(); i++) + if (packetExtractors[i] == nullptr) + throw cRuntimeError("Cannot dequeuePacket(): input provider at gate index %d does not implement IPacketExtractor", (int)i); + auto index = findInput(predicate); + if (index == -1) + return nullptr; + auto packet = packetExtractors[index]->dequeuePacket(predicate); + ASSERT(packet != nullptr); + consumeBucket(index); + take(packet); + handlePacketProcessed(packet); + emit(packetPulledSignal, packet); + if (collector != nullptr) + animatePullPacket(packet, outputGate, collector.getReferencedGate()); + drop(packet); + return packet; +} + void WrrScheduler::removeAllPackets() { Enter_Method("removeAllPackets"); + for (size_t i = 0; i < collections.size(); i++) { + if (collections[i] == nullptr) + throw cRuntimeError("Cannot removeAllPackets(): input provider at gate index %d does not implement IPacketCollection", (int)i); + } for (auto collection : collections) collection->removeAllPackets(); } @@ -97,4 +208,3 @@ int WrrScheduler::schedulePacket() } // namespace queueing } // namespace inet - diff --git a/src/inet/queueing/scheduler/WrrScheduler.h b/src/inet/queueing/scheduler/WrrScheduler.h index d14f19b6b78..dc5907ac794 100644 --- a/src/inet/queueing/scheduler/WrrScheduler.h +++ b/src/inet/queueing/scheduler/WrrScheduler.h @@ -10,6 +10,7 @@ #include "inet/queueing/base/PacketSchedulerBase.h" #include "inet/queueing/contract/IPacketCollection.h" +#include "inet/queueing/contract/IPacketExtractor.h" namespace inet { namespace queueing { @@ -17,17 +18,20 @@ namespace queueing { /** * This module implements a Weighted Round Robin Scheduler. */ -class INET_API WrrScheduler : public PacketSchedulerBase, public virtual IPacketCollection +class INET_API WrrScheduler : public PacketSchedulerBase, public virtual IPacketCollection, public virtual IPacketExtractor { protected: unsigned int *weights = nullptr; // array of weights (has numInputs elements) unsigned int *buckets = nullptr; // array of tokens in buckets (has numInputs elements) std::vector collections; + std::vector packetExtractors; protected: virtual void initialize(int stage) override; virtual int schedulePacket() override; + virtual int findInput(const PacketPredicate& predicate) const; + virtual void consumeBucket(int index); public: virtual ~WrrScheduler(); @@ -39,8 +43,10 @@ class INET_API WrrScheduler : public PacketSchedulerBase, public virtual IPacket virtual b getTotalLength() const override; virtual bool isEmpty() const override { return getNumPackets() == 0; } - virtual Packet *getPacket(int index) const override { throw cRuntimeError("Invalid operation"); } - virtual void removePacket(Packet *packet) override { throw cRuntimeError("Invalid operation"); } + virtual Packet *getPacket(int index) const override; + virtual void removePacket(Packet *packet) override; + virtual Packet *findPacket(const PacketPredicate& predicate) const override; + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override; virtual void removeAllPackets() override; }; @@ -48,4 +54,3 @@ class INET_API WrrScheduler : public PacketSchedulerBase, public virtual IPacket } // namespace inet #endif - diff --git a/tests/queueing/PacketQueueDepartureSignal_1.test b/tests/queueing/PacketQueueDepartureSignal_1.test new file mode 100644 index 00000000000..20df6b35c0c --- /dev/null +++ b/tests/queueing/PacketQueueDepartureSignal_1.test @@ -0,0 +1,98 @@ +%description: +Verify that a PacketQueue using an external PacketBuffer reports one typed +departure signal while the evicted packet is still valid, before the buffer +deletes it. + +%file: TestPacketQueueDepartureSignal.cc + +#include + +#include "inet/common/packet/Packet.h" +#include "inet/queueing/buffer/PacketBuffer.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" +#include "inet/queueing/queue/PacketQueue.h" + +namespace inet { +namespace queueing { + +class TestPacketQueueDepartureSignal : public cSimpleModule, public cListener +{ + public: + TestPacketQueueDepartureSignal() : cSimpleModule(65536) {} + + protected: + int notificationCount = 0; + std::string lastPacketName; + bool packetWasOwned = false; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *details) override + { + auto packet = check_and_cast(object); + auto reason = check_and_cast(details)->getReason(); + ASSERT(reason == IPacketQueue::PacketRemovalReason::DROPPED); + notificationCount++; + lastPacketName = packet->getName(); + packetWasOwned = packet->getOwner() != nullptr; + } + + virtual void activity() override + { + auto queue = check_and_cast(getModuleByPath("^.queue")); + auto buffer = check_and_cast(getModuleByPath("^.buffer")); + queue->subscribe(IPacketQueue::packetQueueDepartureSignal, this); + ASSERT(queue->getMaxNumPackets() == -1); + ASSERT(buffer->getMaxNumPackets() == 0); + + auto packet = new Packet("external-buffer-packet"); + queue->enqueuePacket(packet); + + queue->unsubscribe(IPacketQueue::packetQueueDepartureSignal, this); + ASSERT(notificationCount == 1); + ASSERT(lastPacketName == "external-buffer-packet"); + ASSERT(packetWasOwned); + ASSERT(queue->isEmpty()); + ASSERT(buffer->isEmpty()); + std::cout << "External PacketBuffer eviction notified the queue signal exactly once before deletion.\n"; + } +}; + +Define_Module(TestPacketQueueDepartureSignal); + +} // namespace queueing +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; +import inet.queueing.buffer.PacketBuffer; +import inet.queueing.queue.PacketQueue; + +simple TestPacketQueueDepartureSignal extends SimpleModule +{ + parameters: + @class(::inet::queueing::TestPacketQueueDepartureSignal); +} + +network PacketQueueDepartureSignalTestNetwork +{ + submodules: + buffer: PacketBuffer; + queue: PacketQueue; + test: TestPacketQueueDepartureSignal; +} + +%inifile: omnetpp.ini + +[General] +network = PacketQueueDepartureSignalTestNetwork +sim-time-limit = 1us +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +*.queue.bufferModule = "^.buffer" +*.buffer.packetCapacity = 0 +*.buffer.dropperClass = "inet::queueing::PacketAtCollectionEndDropper" + +%contains: stdout +External PacketBuffer eviction notified the queue signal exactly once before deletion. From 80ee7536ab216a50d0603179e234b36aa291e192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:02 +0200 Subject: [PATCH 28/56] ieee80211: track pending queue departures through signals Subscribe DCF and HCF to logical queue departure signals so destructive queue removal reports a management transmission outcome while the packet is still alive. Filter descendant emissions to avoid duplicate logical notifications. Exercise provider-directed extraction, queue accounting, shared buffers, nested notifications, and reentrant removal using signal listeners. --- .../ieee80211/mac/coordinationfunction/Dcf.cc | 31 +- .../ieee80211/mac/coordinationfunction/Dcf.h | 5 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 31 +- .../ieee80211/mac/coordinationfunction/Hcf.h | 5 +- .../module/Ieee80211MgmtApHcfQueueDrop_1.test | 6 +- tests/module/Ieee80211MgmtApQueueDrop_1.test | 2 +- tests/unit/Ieee80211AddbaTransaction_1.test | 947 ++++++++++++++++++ 7 files changed, 997 insertions(+), 30 deletions(-) create mode 100644 tests/unit/Ieee80211AddbaTransaction_1.test diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index ddb1cf9aa65..675b26be37a 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" @@ -53,7 +55,7 @@ void Dcf::initialize(int stage) else if (stage == INITSTAGE_LAST) { // Dcaf resolves its pending queue at the link-layer stage. Install // this signal listener after all child initialization has completed. - check_and_cast(channelAccess->getPendingQueue())->subscribe(packetDroppedSignal, this); + check_and_cast(channelAccess->getPendingQueue())->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); } } @@ -121,19 +123,25 @@ void Dcf::processMgmtFrame(Packet *packet, const Ptr& throw cRuntimeError("Unknown management frame"); } -void Dcf::receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) +void Dcf::receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) { - if (signalID == packetDroppedSignal) { - Enter_Method("%s", cComponent::getSignalName(signalID)); - auto packet = check_and_cast(obj); - if (packet->findTag() != nullptr) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); - } + if (signal == queueing::IPacketQueue::packetQueueDepartureSignal) { + Enter_Method("packetQueueDeparture"); + if (source->isSubscribed(signal, this)) + handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); } else - ModeSetListener::receiveSignal(source, signalID, obj, details); + ModeSetListener::receiveSignal(source, signal, object, details); +} + +void Dcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) +{ + Enter_Method("handlePacketRemoved"); + if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } } void Dcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header) @@ -429,4 +437,3 @@ Dcf::~Dcf() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h index 51ce7172954..87a4d1ab227 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h @@ -28,6 +28,7 @@ #include "inet/linklayer/ieee80211/mac/originator/AckHandler.h" #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/protectionmechanism/OriginatorProtectionMechanism.h" +#include "inet/queueing/contract/IPacketQueue.h" namespace inet { namespace ieee80211 { @@ -87,7 +88,6 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void initialize(int stage) override; virtual void forEachChild(cVisitor *v) override; virtual void handleMessage(cMessage *msg) override; - virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; virtual void sendUp(const std::vector& completeFrames); virtual bool hasFrameToTransmit(); @@ -118,6 +118,8 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr& responseHeader, Packet *receivedPacket, const Ptr& receivedHeader) override; virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override; + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; + virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); virtual bool isSentByUs(const Ptr& header) const; virtual bool isForUs(const Ptr& header) const; @@ -134,4 +136,3 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index e64ce4db852..5659cfdc3f5 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" @@ -69,7 +71,7 @@ void Hcf::initialize(int stage) // Edca resolves its Edcaf array at the link-layer stage. Install the // queue signal listeners after all child initialization has completed. for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) - check_and_cast(edca->getEdcaf(static_cast(ac))->getPendingQueue())->subscribe(packetDroppedSignal, this); + check_and_cast(edca->getEdcaf(static_cast(ac))->getPendingQueue())->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); } } @@ -154,19 +156,25 @@ void Hcf::processUpperFrame(Packet *packet, const Ptr(obj); - if (packet->findTag() != nullptr) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); - } + if (signal == queueing::IPacketQueue::packetQueueDepartureSignal) { + Enter_Method("packetQueueDeparture"); + if (source->isSubscribed(signal, this)) + handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); } else - ModeSetListener::receiveSignal(source, signalID, obj, details); + ModeSetListener::receiveSignal(source, signal, object, details); +} + +void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) +{ + Enter_Method("handlePacketRemoved"); + if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } } void Hcf::scheduleStartRxTimer(simtime_t timeout) @@ -828,4 +836,3 @@ Hcf::~Hcf() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 72f7af70fc4..c05a7a071af 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -36,6 +36,7 @@ #include "inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/recipient/CtsProcedure.h" +#include "inet/queueing/contract/IPacketQueue.h" namespace inet { namespace ieee80211 { @@ -107,7 +108,6 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void initialize(int stage) override; virtual void forEachChild(cVisitor *v) override; virtual void handleMessage(cMessage *msg) override; - virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; virtual void refreshDisplay() const override; void startFrameSequence(AccessCategory ac); @@ -157,6 +157,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr& responseHeader, Packet *receivedPacket, const Ptr& receivedHeader) override; virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override; + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; + virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); // IProcedureCallback virtual void scheduleInactivityTimer(simtime_t timeout) override; @@ -175,4 +177,3 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: } /* namespace inet */ #endif - diff --git a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test index 843fb1ade0b..62c1d4fc629 100644 --- a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test @@ -1,7 +1,7 @@ %description: Verify that a tagged Association or Reassociation Response dropped from an HCF EDCA compound pending queue reaches the AP as a terminal outcome. The -child management queue drop signal is propagated through the compound queue, +child management queue departure signal is propagated through the compound queue, pending state is cleared, and a retransmission completes without disturbing a committed association. @@ -23,6 +23,7 @@ class TestHcfManagementQueue : public queueing::PacketQueue { public: void setPacketCapacityForTest(int capacity) { packetCapacity = capacity; } + bool hasPacketDepartureListenerForTest() const { return !getLocalSignalListeners(queueing::IPacketQueue::packetQueueDepartureSignal).empty(); } }; Define_Module(TestHcfManagementQueue); @@ -31,6 +32,7 @@ class TestCompoundPendingQueueForHcf : public queueing::CompoundPacketQueueBase { public: void setPacketCapacityForTest(int capacity) { packetCapacity = capacity; } + bool hasPacketDepartureListenerForTest() const { return !getLocalSignalListeners(queueing::IPacketQueue::packetQueueDepartureSignal).empty(); } }; Define_Module(TestCompoundPendingQueueForHcf); @@ -129,6 +131,8 @@ class Ieee80211MgmtApHcfQueueDropTest : public cSimpleModule ASSERT(compoundQueue->getMaxNumPackets() == -1); ASSERT(managementQueue->getMaxNumPackets() == 0); + ASSERT(compoundQueue->hasPacketDepartureListenerForTest()); + ASSERT(managementQueue->hasPacketDepartureListenerForTest()); mgmt->markAuthenticated(station); mgmt->submitAssociationRequest(station); for (int i = 0; i < 100 && mgmt->statuses.empty(); i++) diff --git a/tests/module/Ieee80211MgmtApQueueDrop_1.test b/tests/module/Ieee80211MgmtApQueueDrop_1.test index 44a499578c2..d0b0a4838e4 100644 --- a/tests/module/Ieee80211MgmtApQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApQueueDrop_1.test @@ -105,7 +105,7 @@ class Ieee80211MgmtApQueueDropTest : public cSimpleModule ASSERT(!mgmt->hasPendingAssociation(station)); ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::AUTHENTICATED); - // The existing packetDroppedSignal is delivered before deletion, so the reservation + // The queue removal notification is delivered before deletion, so the reservation // is released synchronously and can be reused by another STA. ASSERT(mgmt->reserveAssociationId(reusable) == 1); mgmt->cancelAssociationIdReservation(reusable); diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test new file mode 100644 index 00000000000..91a9667ac13 --- /dev/null +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -0,0 +1,947 @@ +%description: +Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DCF/HCF continuation, and queue accounting. + +%includes: +#include "inet/queueing/contract/PacketQueueRemovalDetails.h" +#include +#include + +#include "inet/common/PacketEventTag.h" +#include "inet/common/Simsignals.h" +#include "inet/common/TimeTag_m.h" +#include "inet/common/LabelsTag_m.h" +#include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/queueing/gate/PacketGate.h" +#include "inet/queueing/gate/PeriodicGate.h" +#include "inet/queueing/queue/CompoundPacketQueueBase.h" +#include "inet/queueing/queue/PacketQueue.h" +#include "inet/queueing/scheduler/LabelScheduler.h" +#include "inet/queueing/scheduler/PriorityScheduler.h" +#include "inet/queueing/scheduler/WrrScheduler.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; +class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue +{ + public: + virtual ~TestPacketQueue() { + for (auto listener : getLocalSignalListeners(packetQueueDepartureSignal)) + unsubscribe(packetQueueDepartureSignal, listener); + } + std::vector packets; + int numSelectedDequeues = 0; + mutable int numGetPacketCalls = 0; + + void notifyPacketRemoved(Packet *packet, PacketRemovalReason reason) { + queueing::PacketQueueRemovalDetails details(reason); + emit(queueing::IPacketQueue::packetQueueDepartureSignal, packet, &details); + } + + virtual int getMaxNumPackets() const override { return -1; } + virtual int getNumPackets() const override { return packets.size(); } + virtual b getMaxTotalLength() const override { return b(-1); } + virtual b getTotalLength() const override { return b(0); } + virtual Packet *getPacket(int index) const override { numGetPacketCalls++; return packets.at(index); } + virtual bool isEmpty() const override { return packets.empty(); } + virtual void removePacket(Packet *packet) override { + packets.erase(std::find(packets.begin(), packets.end(), packet)); + notifyPacketRemoved(packet, PacketRemovalReason::REMOVED); + } + virtual void removeAllPackets() override { + auto removedPackets = packets; + packets.clear(); + for (auto packet : removedPackets) + notifyPacketRemoved(packet, PacketRemovalReason::REMOVED); + } + virtual void enqueuePacket(Packet *packet) override { packets.push_back(packet); } + virtual Packet *dequeuePacket() override { + auto packet = packets.front(); + packets.erase(packets.begin()); + notifyPacketRemoved(packet, PacketRemovalReason::DEQUEUED); + return packet; + } + virtual Packet *findPacket(const PacketPredicate& predicate) const override { + auto it = std::find_if(packets.begin(), packets.end(), predicate); + return it == packets.end() ? nullptr : *it; + } + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override { + auto packet = findPacket(predicate); + if (packet != nullptr) { + numSelectedDequeues++; + packets.erase(std::find(packets.begin(), packets.end(), packet)); + notifyPacketRemoved(packet, PacketRemovalReason::DEQUEUED); + } + return packet; + } + void dropPacketFromQueue(Packet *packet) { + packets.erase(std::find(packets.begin(), packets.end(), packet)); + notifyPacketRemoved(packet, PacketRemovalReason::DROPPED); + } + virtual bool canPullSomePacket(const cGate *) const override { return !packets.empty(); } + virtual Packet *canPullPacket(const cGate *) const override { return packets.empty() ? nullptr : packets.front(); } + virtual Packet *pullPacket(const cGate *) override { return dequeuePacket(); } + virtual Packet *pullPacketStart(const cGate *, bps) override { throw cRuntimeError("Unsupported"); } + virtual Packet *pullPacketEnd(const cGate *) override { throw cRuntimeError("Unsupported"); } + virtual Packet *pullPacketProgress(const cGate *, bps, b, b) override { throw cRuntimeError("Unsupported"); } + virtual bool canPushSomePacket(const cGate *) const override { return true; } + virtual bool canPushPacket(Packet *, const cGate *) const override { return true; } + virtual void pushPacket(Packet *packet, const cGate *) override { enqueuePacket(packet); } + virtual void pushPacketStart(Packet *, const cGate *, bps) override { throw cRuntimeError("Unsupported"); } + virtual void pushPacketEnd(Packet *, const cGate *) override { throw cRuntimeError("Unsupported"); } + virtual void pushPacketProgress(Packet *, const cGate *, bps, b, b) override { throw cRuntimeError("Unsupported"); } +}; +class TestCompoundPacketQueue : public queueing::CompoundPacketQueueBase +{ + public: + void configure(queueing::IPacketCollection *collection, queueing::IPacketExtractor *packetExtractor) { + this->collection = collection; + this->packetExtractor = packetExtractor; + } + void release(Packet *packet) { + take(packet); + drop(packet); + } + void observeDescendantQueues(cModule *module) { registerQueueFrontier(module); } + void observeQueue(queueing::IPacketQueue *queue) { childQueues.push_back(queue); check_and_cast(queue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); } + void addObserver(cListener *callback) { subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, callback); } + void stopObserving() { + for (auto queue : childQueues) + check_and_cast(queue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + childQueues.clear(); + for (auto listener : getLocalSignalListeners(queueing::IPacketQueue::packetQueueDepartureSignal)) + unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, listener); + } +}; + +class TestPriorityScheduler : public queueing::PriorityScheduler +{ + public: + void configure(const std::vector& collections, bool reverseOrder = false) { + std::vector packetExtractors; + for (auto collection : collections) + packetExtractors.push_back(check_and_cast(collection)); + configure(collections, packetExtractors, reverseOrder); + } + void configure(const std::vector& collections, const std::vector& packetExtractors, bool reverseOrder = false) { + ASSERT(collections.size() == packetExtractors.size()); + this->collections = collections; + this->packetExtractors = packetExtractors; + this->reverseOrder = reverseOrder; + numProcessedPackets = 0; + processedTotalLength = b(0); + } + int getNumProcessedPackets() const { return numProcessedPackets; } + void release(Packet *packet) { take(packet); drop(packet); } +}; + +class TestWrrScheduler : public queueing::WrrScheduler +{ + public: + void configure(const std::vector& collections, const std::vector& weights, const std::vector& buckets) { + ASSERT(collections.size() == weights.size()); + ASSERT(collections.size() == buckets.size()); + this->collections = collections; + for (auto collection : collections) + packetExtractors.push_back(check_and_cast(collection)); + this->weights = new unsigned int[weights.size()]; + this->buckets = new unsigned int[buckets.size()]; + for (size_t i = 0; i < collections.size(); i++) { + this->weights[i] = weights[i]; + this->buckets[i] = buckets[i]; + } + numProcessedPackets = 0; + processedTotalLength = b(0); + } + unsigned int getBucket(int index) const { return buckets[index]; } + int getNumProcessedPackets() const { return numProcessedPackets; } + void release(Packet *packet) { take(packet); drop(packet); } +}; + +class TestLabelScheduler : public queueing::LabelScheduler +{ + public: + void configure(const std::vector& collections, const std::vector& labels, int defaultGateIndex) { + this->collections = collections; + for (auto collection : collections) + packetExtractors.push_back(check_and_cast(collection)); + this->labels = labels; + this->defaultGateIndex = defaultGateIndex; + } + void release(Packet *packet) { take(packet); drop(packet); } +}; + +class TestPacketDepartureListener : public cListener +{ + public: + int numDequeuedPackets = 0; + int numRemovedPackets = 0; + int numDroppedPackets = 0; + int64_t lastDroppedPacketId = -1; + std::vector> expectedPacketCountsAtFirstDrop; + bool allExpectedPacketsDetachedAtFirstDrop = true; + + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (!source->isSubscribed(signal, this)) + return; + auto packet = check_and_cast(object); + auto reason = check_and_cast(details)->getReason(); + if (reason == queueing::IPacketQueue::PacketRemovalReason::DEQUEUED) + numDequeuedPackets++; + else if (reason == queueing::IPacketQueue::PacketRemovalReason::REMOVED) + numRemovedPackets++; + else if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED) { + if (numDroppedPackets == 0) + for (auto& [queue, expectedPacketCount] : expectedPacketCountsAtFirstDrop) + allExpectedPacketsDetachedAtFirstDrop &= queue->getNumPackets() == expectedPacketCount; + numDroppedPackets++; + lastDroppedPacketId = packet->getId(); + } + } +}; + +class TestReentrantRemovalListener : public cListener +{ + public: + queueing::IPacketQueue *queue = nullptr; + Packet *triggerPacket = nullptr; + Packet *packetToRemove = nullptr; + queueing::IPacketQueue::PacketRemovalReason triggerReason = queueing::IPacketQueue::PacketRemovalReason::REMOVED; + bool removedPacket = false; + + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override { + if (!source->isSubscribed(signal, this)) + return; + auto packet = check_and_cast(object); + auto reason = check_and_cast(details)->getReason(); + if (!removedPacket && packet == triggerPacket && reason == triggerReason) { + removedPacket = true; + queue->removePacket(packetToRemove); + } + } +}; + +class TestPacketPulledListener : public cListener +{ + public: + cComponent *leaf = nullptr; + cComponent *compound = nullptr; + int numLeafPulls = 0; + int numCompoundPulls = 0; + + virtual void receiveSignal(cComponent *source, simsignal_t, cObject *, cObject *) override { + if (source == leaf) + numLeafPulls++; + else if (source == compound) + numCompoundPulls++; + } +}; + +class TestPacketArrivalListener : public cListener +{ + public: + int numSignals = 0; + cGate *lastArrivalGate = nullptr; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *value, cObject *) override { + numSignals++; + lastArrivalGate = check_and_cast(value)->getArrivalGate(); + } +}; +class TestOnlyPacketExtractor : public queueing::IPacketExtractor +{ + public: + std::vector packets; + + virtual Packet *findPacket(const PacketPredicate& predicate) const override { + auto it = std::find_if(packets.begin(), packets.end(), predicate); + return it == packets.end() ? nullptr : *it; + } + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override { + auto packet = findPacket(predicate); + if (packet != nullptr) + packets.erase(std::find(packets.begin(), packets.end(), packet)); + return packet; + } +}; +static Ptr makeQosHeader(MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber) +{ + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(receiverAddress); + header->setTid(tid); + header->setSequenceNumber(sequenceNumber); + return header; +} +%activity: +MacAddress peer1("00:00:00:00:00:01"); +auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const char *dropperClass = "") { + auto module = cModuleType::get("inet.queueing.queue.PacketQueue")->create(name, this); + module->par("packetCapacity").setIntValue(packetCapacity); + module->par("dropperClass").setStringValue(dropperClass); + module->callInitialize(); + return check_and_cast(module); +}; +// Predicate extraction follows each provider's scheduling policy instead of +// the compound collection's gate-order enumeration. +{ + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto firstPacket = new Packet("priorityFirstPacket", makeShared(B(1))); + auto secondPacket = new Packet("prioritySecondPacket", makeShared(B(1))); + firstQueue.packets = { firstPacket }; + secondQueue.packets = { secondPacket }; + TestPriorityScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, true); + auto predicate = [](const Packet *) { return true; }; + ASSERT(scheduler.findPacket(predicate) == secondPacket); + auto dequeuedPacket = scheduler.dequeuePacket(predicate); + ASSERT(dequeuedPacket == secondPacket); + scheduler.release(secondPacket); + delete secondPacket; + firstQueue.removePacket(firstPacket); + delete firstPacket; +} + +// Priority extraction is independent of collection enumeration, so an input +// may support predicate extraction even when it cannot report aggregate size. +{ + TestOnlyPacketExtractor packetExtractor; + auto packet = new Packet("extractorOnlyPacket", makeShared(B(1))); + packetExtractor.packets = { packet }; + TestPriorityScheduler scheduler; + scheduler.configure(std::vector({ nullptr }), std::vector({ &packetExtractor })); + bool aggregateQueryFailed = false; + try { + scheduler.getNumPackets(); + } + catch (cRuntimeError& error) { + aggregateQueryFailed = std::string(error.what()).find("getNumPackets") != std::string::npos && std::string(error.what()).find("IPacketCollection") != std::string::npos; + } + ASSERT(aggregateQueryFailed); + bool aggregateLengthQueryFailed = false; + try { + scheduler.getTotalLength(); + } + catch (cRuntimeError& error) { + aggregateLengthQueryFailed = std::string(error.what()).find("getTotalLength") != std::string::npos && std::string(error.what()).find("IPacketCollection") != std::string::npos; + } + ASSERT(aggregateLengthQueryFailed); + ASSERT(scheduler.findPacket([](const Packet *) { return true; }) == packet); + auto dequeuedPacket = scheduler.dequeuePacket([](const Packet *) { return true; }); + ASSERT(dequeuedPacket == packet); + ASSERT(packetExtractor.packets.empty()); + scheduler.release(packet); + delete packet; +} +// Collection-only providers remain usable for aggregate queries and fail only +// when predicate extraction is actually requested. +{ + TestPacketQueue firstCollection; + TestPacketQueue collectionOnly; + auto firstPacket = new Packet("extractablePacket", makeShared(B(1))); + auto collectionOnlyPacket = new Packet("collectionOnlyPacket", makeShared(B(1))); + firstCollection.packets = { firstPacket }; + collectionOnly.packets = { collectionOnlyPacket }; + TestPriorityScheduler scheduler; + scheduler.configure(std::vector({ &firstCollection, &collectionOnly }), std::vector({ &firstCollection, nullptr })); + ASSERT(scheduler.getNumPackets() == 2); + bool extractionFailed = false; + try { + scheduler.findPacket([](const Packet *) { return true; }); + } + catch (cRuntimeError& error) { + extractionFailed = std::string(error.what()).find("findPacket") != std::string::npos && std::string(error.what()).find("IPacketExtractor") != std::string::npos; + } + ASSERT(extractionFailed); + firstCollection.removePacket(firstPacket); + collectionOnly.removePacket(collectionOnlyPacket); + delete firstPacket; + delete collectionOnlyPacket; +} +{ + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto firstPacket = new Packet("wrrFirstPacket", makeShared(B(1))); + auto secondPacket = new Packet("wrrSecondPacket", makeShared(B(1))); + firstQueue.packets = { firstPacket }; + secondQueue.packets = { secondPacket }; + TestWrrScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, { 1, 2 }, { 0, 2 }); + auto predicate = [](const Packet *) { return true; }; + ASSERT(scheduler.findPacket(predicate) == secondPacket); + ASSERT(scheduler.findPacket(predicate) == secondPacket); + ASSERT(scheduler.getBucket(1) == 2); + auto dequeuedPacket = scheduler.dequeuePacket(predicate); + ASSERT(dequeuedPacket == secondPacket); + ASSERT(scheduler.getBucket(1) == 1); + scheduler.release(secondPacket); + delete secondPacket; + firstQueue.removePacket(firstPacket); + delete firstPacket; +} +{ + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto blockedPacket = new Packet("labelBlockedPacket", makeShared(B(1))); + auto lowPacket = new Packet("labelLowPacket", makeShared(B(1))); + auto highPacket = new Packet("labelHighPacket", makeShared(B(1))); + auto lowLabels = lowPacket->addTag(); + lowLabels->setLabelsArraySize(1); + lowLabels->setLabels(0, "low"); + auto highLabels = highPacket->addTag(); + highLabels->setLabelsArraySize(1); + highLabels->setLabels(0, "high"); + firstQueue.packets = { blockedPacket, lowPacket }; + secondQueue.packets = { highPacket }; + TestLabelScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, { "high", "low" }, 0); + auto predicate = [blockedPacket](const Packet *packet) { return packet != blockedPacket; }; + ASSERT(scheduler.findPacket(predicate) == highPacket); + auto dequeuedPacket = scheduler.dequeuePacket(predicate); + ASSERT(dequeuedPacket == highPacket); + scheduler.release(highPacket); + delete highPacket; + firstQueue.removePacket(blockedPacket); + firstQueue.removePacket(lowPacket); + delete blockedPacket; + delete lowPacket; +} + + +// Closed gates expose no predicate-selected packet and remove nothing. Once +// opened, the same exact candidate crosses through PacketFlowBase accounting. +{ + auto leafQueue = createPacketQueue("closedGateLeaf"); + auto gateModule = cModuleType::get("inet.queueing.gate.PacketGate")->create("closedPredicateGate", this); + gateModule->par("initiallyOpen").setBoolValue(false); + gateModule->par("openTime").setDoubleValue(10); + gateModule->par("closeTime").setDoubleValue(20); + leafQueue->gate("out")->connectTo(gateModule->gate("in")); + gateModule->callInitialize(); + auto packet = new Packet("closedGatePacket", makeShared(B(1))); + leafQueue->pushPacket(packet, nullptr); + auto packetExtractor = check_and_cast(gateModule); + auto closedFoundPacket = packetExtractor->findPacket([](const Packet *) { return true; }); + auto closedDequeuedPacket = packetExtractor->dequeuePacket([](const Packet *) { return true; }); + ASSERT(closedFoundPacket == nullptr); + ASSERT(closedDequeuedPacket == nullptr); + ASSERT(leafQueue->getNumPackets() == 1); + check_and_cast(gateModule)->open(); + ASSERT(packetExtractor->findPacket([](const Packet *) { return true; }) == packet); + auto dequeuedPacket = packetExtractor->dequeuePacket([](const Packet *) { return true; }); + ASSERT(dequeuedPacket == packet); + ASSERT(leafQueue->getNumPackets() == 0); + take(packet); + delete packet; + gateModule->callFinish(); + leafQueue->callFinish(); + gateModule->deleteModule(); + leafQueue->deleteModule(); +} + +// An implicit guard band rejects the provider's exact first match instead of +// skipping it and extracting a later packet that happens to fit. +{ + auto leafQueue = createPacketQueue("guardBandLeaf"); + auto gateModule = cModuleType::get("inet.queueing.gate.PeriodicGate")->create("predicateGuardBandGate", this); + auto durations = new cValueArray(); + durations->add(cValue(1, "s")); + durations->add(cValue(1, "s")); + gateModule->par("durations").setObjectValue(durations); + gateModule->par("initiallyOpen").setBoolValue(true); + gateModule->par("bitrate").setDoubleValue(8); + leafQueue->gate("out")->connectTo(gateModule->gate("in")); + gateModule->callInitialize(); + auto blockedPacket = new Packet("guardBandBlockedPacket", makeShared(B(2))); + auto fittingPacket = new Packet("guardBandFittingPacket", makeShared(B(1))); + leafQueue->pushPacket(blockedPacket, nullptr); + leafQueue->pushPacket(fittingPacket, nullptr); + auto packetExtractor = check_and_cast(gateModule); + auto blockedFoundPacket = packetExtractor->findPacket([](const Packet *) { return true; }); + auto blockedDequeuedPacket = packetExtractor->dequeuePacket([](const Packet *) { return true; }); + ASSERT(blockedFoundPacket == nullptr); + ASSERT(blockedDequeuedPacket == nullptr); + ASSERT(leafQueue->getNumPackets() == 2); + auto dequeuedPacket = packetExtractor->dequeuePacket([blockedPacket](const Packet *packet) { return packet != blockedPacket; }); + ASSERT(dequeuedPacket == fittingPacket); + ASSERT(leafQueue->getNumPackets() == 1); + take(fittingPacket); + delete fittingPacket; + auto retainedPacket = leafQueue->dequeuePacket([blockedPacket](const Packet *packet) { return packet == blockedPacket; }); + ASSERT(retainedPacket == blockedPacket); + take(blockedPacket); + delete blockedPacket; + gateModule->callFinish(); + leafQueue->callFinish(); + gateModule->deleteModule(); + leafQueue->deleteModule(); +} +// A real queue with a downstream collector predicate-dequeues a non-front +// packet, records its residence and semantic pull once, and animates it to the +// collector while retaining the front packet. +{ + TestPacketPulledListener pullListener; + TestPacketDepartureListener removalCallback; + auto leafQueueModule = cModuleType::get("inet.queueing.queue.PacketQueue")->create("selectedDequeueQueue", this); + auto sinkModule = cModuleType::get("inet.queueing.sink.FullPacketSink")->create("selectedDequeueSink", this); + leafQueueModule->gate("out")->connectTo(sinkModule->gate("in")); + leafQueueModule->callInitialize(); + sinkModule->callInitialize(); + auto leafQueue = check_and_cast(leafQueueModule); + pullListener.leaf = leafQueue; + leafQueue->subscribe(packetPulledSignal, &pullListener); + check_and_cast(leafQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &removalCallback); + auto frontPacket = new Packet("selectedDequeueFront", makeShared(B(1))); + auto selectedPacket = new Packet("selectedDequeueMatch", makeQosHeader(peer1, 1, SequenceNumberCyclic(1))); + auto queueingTimeTag = selectedPacket->addRegionTag(); + queueingTimeTag->setBitTotalTimesArraySize(1); + queueingTimeTag->setBitTotalTimes(0, 0); + queueingTimeTag->setPacketTotalTimesArraySize(1); + queueingTimeTag->setPacketTotalTimes(0, 0); + selectedPacket->addRegionTag(); + selectedPacket->setArrivalTime(simTime() - 1); + leafQueue->pushPacket(frontPacket, nullptr); + leafQueue->pushPacket(selectedPacket, nullptr); + auto dequeuedPacket = leafQueue->dequeuePacket([selectedPacket](const Packet *packet) { return packet == selectedPacket; }); + ASSERT(dequeuedPacket == selectedPacket); + int numQueuedEvents = 0; + selectedPacket->mapAllRegionTags(b(0), selectedPacket->getTotalLength(), [&](b, b, const Ptr& tag) { + for (size_t i = 0; i < tag->getPacketEventsArraySize(); i++) + if (tag->getPacketEvents(i)->getKind() == PEK_QUEUED) + numQueuedEvents++; + }); + simtime_t totalQueueingTime = -1; + selectedPacket->mapAllRegionTags(b(0), selectedPacket->getTotalLength(), [&](b, b, const Ptr& tag) { + ASSERT(tag->getPacketTotalTimesArraySize() == 1); + totalQueueingTime = tag->getPacketTotalTimes(0); + }); + ASSERT(numQueuedEvents == 1); + ASSERT(totalQueueingTime == 1); + ASSERT(leafQueue->getNumPackets() == 1); + ASSERT(leafQueue->getPacket(0) == frontPacket); + ASSERT(pullListener.numLeafPulls == 1); + ASSERT(removalCallback.numDequeuedPackets == 1); + ASSERT(selectedPacket->getArrivalGate() == sinkModule->gate("in")); + leafQueue->unsubscribe(packetPulledSignal, &pullListener); + check_and_cast(leafQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &removalCallback); + leafQueue->removePacket(frontPacket); + take(frontPacket); + take(selectedPacket); + delete frontPacket; + delete selectedPacket; + sinkModule->callFinish(); + leafQueueModule->callFinish(); + sinkModule->deleteModule(); + leafQueueModule->deleteModule(); +} + +// Predicate extraction through a concrete PacketFlowBase module processes a +// non-front packet once. The queue animates it to the flow input before the +// flow signals, and the flow then animates it to the final collector. +{ + TestPacketPulledListener pullListener; + TestPacketDepartureListener removalCallback; + TestPacketArrivalListener flowInputListener; + TestPacketArrivalListener flowOutputListener; + auto leafQueueModule = cModuleType::get("inet.queueing.queue.PacketQueue")->create("selectedFlowLeaf", this); + auto gateModule = cModuleType::get("inet.queueing.gate.PacketGate")->create("selectedFlowGate", this); + auto sinkModule = cModuleType::get("inet.queueing.sink.FullPacketSink")->create("selectedFlowSink", this); + gateModule->par("initiallyOpen").setBoolValue(true); + gateModule->par("openTime").setDoubleValue(10); + gateModule->par("closeTime").setDoubleValue(20); + leafQueueModule->gate("out")->connectTo(gateModule->gate("in")); + gateModule->gate("out")->connectTo(sinkModule->gate("in")); + leafQueueModule->callInitialize(); + gateModule->callInitialize(); + sinkModule->callInitialize(); + auto leafQueue = check_and_cast(leafQueueModule); + auto packetGate = check_and_cast(gateModule); + auto packetExtractor = check_and_cast(gateModule); + pullListener.leaf = leafQueue; + leafQueue->subscribe(packetPulledSignal, &pullListener); + check_and_cast(leafQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &removalCallback); + gateModule->subscribe(packetPulledInSignal, &flowInputListener); + gateModule->subscribe(packetPulledOutSignal, &flowOutputListener); + auto frontPacket = new Packet("selectedFlowFront", makeShared(B(1))); + auto selectedPacket = new Packet("selectedFlowMatch", makeShared(B(1))); + leafQueue->pushPacket(frontPacket, nullptr); + leafQueue->pushPacket(selectedPacket, nullptr); + auto dequeuedPacket = packetExtractor->dequeuePacket([selectedPacket](const Packet *packet) { return packet == selectedPacket; }); + ASSERT(dequeuedPacket == selectedPacket); + ASSERT(leafQueue->getNumPackets() == 1); + ASSERT(leafQueue->getPacket(0) == frontPacket); + ASSERT(pullListener.numLeafPulls == 1); + ASSERT(removalCallback.numDequeuedPackets == 1); + ASSERT(flowInputListener.numSignals == 1); + ASSERT(flowInputListener.lastArrivalGate == gateModule->gate("in")); + ASSERT(flowOutputListener.numSignals == 1); + ASSERT(flowOutputListener.lastArrivalGate == gateModule->gate("in")); + ASSERT(packetGate->resolveDirective('p') == "1"); + ASSERT(selectedPacket->getArrivalGate() == sinkModule->gate("in")); + leafQueue->unsubscribe(packetPulledSignal, &pullListener); + check_and_cast(leafQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &removalCallback); + gateModule->unsubscribe(packetPulledInSignal, &flowInputListener); + gateModule->unsubscribe(packetPulledOutSignal, &flowOutputListener); + leafQueue->removePacket(frontPacket); + take(frontPacket); + take(selectedPacket); + delete frontPacket; + delete selectedPacket; + sinkModule->callFinish(); + gateModule->callFinish(); + leafQueueModule->callFinish(); + sinkModule->deleteModule(); + gateModule->deleteModule(); + leafQueueModule->deleteModule(); +} +// The typed queue callback covers every non-destructive ownership departure, +// including bulk removal, exactly once at the queue boundary. +{ + auto lifecycleQueue = createPacketQueue("lifecycleQueue"); + TestPacketDepartureListener lifecycleCallback; + check_and_cast(lifecycleQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &lifecycleCallback); + auto pulledPacket = new Packet("lifecyclePulled", makeShared(B(1))); + auto selectedPacket = new Packet("lifecycleSelected", makeShared(B(1))); + auto removedPacket = new Packet("lifecycleRemoved", makeShared(B(1))); + auto firstBulkRemovedPacket = new Packet("lifecycleBulkRemovedFirst", makeShared(B(1))); + auto secondBulkRemovedPacket = new Packet("lifecycleBulkRemovedSecond", makeShared(B(1))); + auto thirdBulkRemovedPacket = new Packet("lifecycleBulkRemovedThird", makeShared(B(1))); + lifecycleQueue->pushPacket(pulledPacket, nullptr); + lifecycleQueue->pushPacket(selectedPacket, nullptr); + lifecycleQueue->pushPacket(removedPacket, nullptr); + lifecycleQueue->pushPacket(firstBulkRemovedPacket, nullptr); + lifecycleQueue->pushPacket(secondBulkRemovedPacket, nullptr); + lifecycleQueue->pushPacket(thirdBulkRemovedPacket, nullptr); + auto pulledResult = lifecycleQueue->dequeuePacket(); + auto selectedResult = lifecycleQueue->dequeuePacket([selectedPacket](const Packet *packet) { return packet == selectedPacket; }); + lifecycleQueue->removePacket(removedPacket); + take(pulledResult); + take(selectedResult); + take(removedPacket); + delete pulledResult; + delete selectedResult; + delete removedPacket; + lifecycleQueue->removeAllPackets(); + if (lifecycleCallback.numDequeuedPackets != 2 || lifecycleCallback.numRemovedPackets != 4 || lifecycleCallback.numDroppedPackets != 0 || !lifecycleQueue->isEmpty()) + throw cRuntimeError("Typed queue lifecycle callback did not cover every departure exactly once"); + check_and_cast(lifecycleQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &lifecycleCallback); + lifecycleQueue->callFinish(); + lifecycleQueue->deleteModule(); +} + + +// Bulk removal detaches only this queue's packets from a shared buffer. A +// direct buffer removal then notifies the owning queue exactly once. +{ + auto bufferModule = cModuleType::get("inet.queueing.buffer.PacketBuffer")->create("sharedRemovalBuffer", this); + bufferModule->callInitialize(); + auto createBufferedQueue = [&](const char *name) { + auto module = cModuleType::get("inet.queueing.queue.PacketQueue")->create(name, this); + module->par("bufferModule").setStringValue("^.sharedRemovalBuffer"); + module->callInitialize(); + return check_and_cast(module); + }; + auto firstQueue = createBufferedQueue("sharedRemovalFirstQueue"); + auto secondQueue = createBufferedQueue("sharedRemovalSecondQueue"); + TestPacketDepartureListener firstCallback; + TestPacketDepartureListener secondCallback; + TestPacketDepartureListener compoundCallback; + TestCompoundPacketQueue bufferedCompound; + bufferedCompound.observeQueue(secondQueue); + bufferedCompound.addObserver(&compoundCallback); + check_and_cast(firstQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &firstCallback); + check_and_cast(secondQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &secondCallback); + firstQueue->pushPacket(new Packet("sharedRemovalFirstA", makeShared(B(1))), nullptr); + firstQueue->pushPacket(new Packet("sharedRemovalFirstB", makeShared(B(1))), nullptr); + auto retainedPacket = new Packet("sharedRemovalRetained", makeShared(B(1))); + secondQueue->pushPacket(retainedPacket, nullptr); + firstQueue->removeAllPackets(); + auto buffer = check_and_cast(bufferModule); + ASSERT(firstQueue->isEmpty()); + ASSERT(secondQueue->getNumPackets() == 1); + ASSERT(buffer->getNumPackets() == 1); + ASSERT(firstCallback.numRemovedPackets == 2); + ASSERT(secondCallback.numRemovedPackets == 0); + buffer->removePacket(retainedPacket); + ASSERT(secondQueue->isEmpty()); + ASSERT(buffer->getNumPackets() == 0); + ASSERT(secondCallback.numRemovedPackets == 1); + ASSERT(secondCallback.numDroppedPackets == 0); + ASSERT(compoundCallback.numRemovedPackets == 1); + ASSERT(compoundCallback.numDroppedPackets == 0); + take(retainedPacket); + delete retainedPacket; + bufferedCompound.stopObserving(); + check_and_cast(firstQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &firstCallback); + check_and_cast(secondQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &secondCallback); + firstQueue->callFinish(); + secondQueue->callFinish(); + bufferModule->callFinish(); + firstQueue->deleteModule(); + secondQueue->deleteModule(); + bufferModule->deleteModule(); +} + +// A shared buffer rejects cPacketQueue ownership it cannot detach before any +// mutation, preventing a later overload drop from corrupting the owner queue. +{ + auto bufferModule = cModuleType::get("inet.queueing.buffer.PacketBuffer")->create("unsupportedOwnerBuffer", this); + bufferModule->callInitialize(); + auto buffer = check_and_cast(bufferModule); + cPacketQueue unsupportedOwnerQueue("unsupportedOwnerQueue"); + auto packet = new Packet("unsupportedOwnerPacket", makeShared(B(1))); + unsupportedOwnerQueue.insert(packet); + bool unsupportedOwnerRejected = false; + try { + buffer->addPacket(packet); + } + catch (cRuntimeError& error) { + unsupportedOwnerRejected = std::string(error.what()).find("IPacketBuffer::ICallback") != std::string::npos; + } + ASSERT(unsupportedOwnerRejected); + ASSERT(buffer->getNumPackets() == 0); + ASSERT(unsupportedOwnerQueue.contains(packet)); + unsupportedOwnerQueue.remove(packet); + take(packet); + delete packet; + bufferModule->callFinish(); + bufferModule->deleteModule(); +} + +// Internal overflow invokes the typed queue callback once for the destructive +// discard and never for the packet later removed normally. +{ + auto overflowQueue = createPacketQueue("overflowQueue", 1, "inet::queueing::PacketAtCollectionEndDropper"); + TestPacketDepartureListener dropCallback; + check_and_cast(overflowQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + auto retainedPacket = new Packet("retainedPacket", makeShared(B(1))); + auto overflowPacket = new Packet("overflowPacket", makeShared(B(1))); + auto overflowPacketId = overflowPacket->getId(); + overflowQueue->pushPacket(retainedPacket, nullptr); + overflowQueue->pushPacket(overflowPacket, nullptr); + ASSERT(dropCallback.numDroppedPackets == 1); + ASSERT(dropCallback.lastDroppedPacketId == overflowPacketId); + check_and_cast(overflowQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + auto dequeuedPacket = overflowQueue->dequeuePacket([retainedPacket](const Packet *packet) { return packet == retainedPacket; }); + ASSERT(dequeuedPacket == retainedPacket); + ASSERT(dropCallback.numDroppedPackets == 1); + overflowQueue->callFinish(); + overflowQueue->deleteModule(); +} + +// A compound unregisters from child queues during pre-delete even when finish +// is skipped, and repeated lifecycle cleanup is harmless. +{ + auto pendingQueueModule = cModuleType::get("inet.linklayer.ieee80211.mac.queue.CompoundPendingQueue")->create("preDeletePendingQueue", this); + pendingQueueModule->callInitialize(); + auto pendingQueue = check_and_cast(pendingQueueModule); + auto leafQueue = check_and_cast(pendingQueueModule->getSubmodule("managementQueue")); + TestPacketDepartureListener callback; + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &callback); + + auto observedPacket = new Packet("preDeleteObservedPacket", makeShared(B(1))); + leafQueue->pushPacket(observedPacket, nullptr); + leafQueue->removePacket(observedPacket); + ASSERT(callback.numRemovedPackets == 1); + take(observedPacket); + delete observedPacket; + + pendingQueueModule->callPreDelete(pendingQueueModule); + auto unobservedPacket = new Packet("preDeleteUnobservedPacket", makeShared(B(1))); + leafQueue->pushPacket(unobservedPacket, nullptr); + leafQueue->removePacket(unobservedPacket); + ASSERT(callback.numRemovedPackets == 1); + take(unobservedPacket); + delete unobservedPacket; + + pendingQueueModule->deleteModule(); +} + +// A compound's direct frontier and a nested compound boundary each forward +// descendant removals exactly once. Boundary-initiated removals suppress the +// descendant signal for the same packet, including during reentrant removal +// of a different packet. +{ + auto pendingQueueModule = cModuleType::get("inet.linklayer.ieee80211.mac.queue.CompoundPendingQueue")->create("callbackPendingQueue", this); + pendingQueueModule->callInitialize(); + auto pendingQueue = check_and_cast(pendingQueueModule); + auto directLeaf = check_and_cast(pendingQueueModule->getSubmodule("managementQueue")); + TestPacketDepartureListener directCallback; + TestPacketDepartureListener nestedCallback; + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &directCallback); + TestCompoundPacketQueue outerCompound; + outerCompound.observeQueue(pendingQueue); + outerCompound.addObserver(&nestedCallback); + + auto directPacket = new Packet("directCompoundDrop", makeShared(B(1))); + directLeaf->handlePacketDropped(directPacket); + ASSERT(directCallback.numDroppedPackets == 1); + ASSERT(nestedCallback.numDroppedPackets == 1); + delete directPacket; + + auto leafRemovedPacket = new Packet("leafRemovedPacket", makeShared(B(1))); + directLeaf->pushPacket(leafRemovedPacket, nullptr); + directLeaf->removePacket(leafRemovedPacket); + ASSERT(directCallback.numRemovedPackets == 1); + ASSERT(nestedCallback.numRemovedPackets == 1); + take(leafRemovedPacket); + delete leafRemovedPacket; + + directLeaf->pushPacket(new Packet("leafBulkRemovedFirst", makeShared(B(1))), nullptr); + directLeaf->pushPacket(new Packet("leafBulkRemovedSecond", makeShared(B(1))), nullptr); + directLeaf->removeAllPackets(); + ASSERT(directCallback.numRemovedPackets == 3); + ASSERT(nestedCallback.numRemovedPackets == 3); + + auto boundaryRemovedPacket = new Packet("boundaryRemovedPacket", makeShared(B(1))); + directLeaf->pushPacket(boundaryRemovedPacket, nullptr); + pendingQueue->removePacket(boundaryRemovedPacket); + ASSERT(directCallback.numRemovedPackets == 4); + ASSERT(nestedCallback.numRemovedPackets == 4); + take(boundaryRemovedPacket); + delete boundaryRemovedPacket; + + directLeaf->pushPacket(new Packet("boundaryBulkRemovedFirst", makeShared(B(1))), nullptr); + directLeaf->pushPacket(new Packet("boundaryBulkRemovedSecond", makeShared(B(1))), nullptr); + pendingQueue->removeAllPackets(); + ASSERT(directCallback.numRemovedPackets == 6); + ASSERT(nestedCallback.numRemovedPackets == 6); + + auto boundaryDequeuedPacket = new Packet("boundaryDequeuedPacket", makeShared(B(1))); + directLeaf->pushPacket(boundaryDequeuedPacket, nullptr); + auto dequeuedPacket = pendingQueue->dequeuePacket([boundaryDequeuedPacket](const Packet *packet) { return packet == boundaryDequeuedPacket; }); + ASSERT(dequeuedPacket == boundaryDequeuedPacket); + ASSERT(directCallback.numDequeuedPackets == 1); + ASSERT(nestedCallback.numDequeuedPackets == 0); + take(dequeuedPacket); + delete dequeuedPacket; + + auto boundaryPulledPacket = new Packet("boundaryPulledPacket", makeShared(B(1))); + directLeaf->pushPacket(boundaryPulledPacket, nullptr); + auto pulledPacket = pendingQueue->pullPacket(nullptr); + ASSERT(pulledPacket == boundaryPulledPacket); + ASSERT(directCallback.numDequeuedPackets == 2); + ASSERT(nestedCallback.numDequeuedPackets == 0); + take(pulledPacket); + delete pulledPacket; + + auto reentrantTriggerPacket = new Packet("reentrantTriggerPacket", makeShared(B(1))); + auto reentrantRemovedPacket = new Packet("reentrantRemovedPacket", makeShared(B(1))); + directLeaf->pushPacket(reentrantTriggerPacket, nullptr); + directLeaf->pushPacket(reentrantRemovedPacket, nullptr); + TestReentrantRemovalListener reentrantCallback; + reentrantCallback.queue = directLeaf; + reentrantCallback.triggerPacket = reentrantTriggerPacket; + reentrantCallback.packetToRemove = reentrantRemovedPacket; + check_and_cast(directLeaf)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &reentrantCallback); + pendingQueue->removePacket(reentrantTriggerPacket); + ASSERT(reentrantCallback.removedPacket); + ASSERT(directCallback.numRemovedPackets == 8); + ASSERT(nestedCallback.numRemovedPackets == 8); + check_and_cast(directLeaf)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &reentrantCallback); + take(reentrantTriggerPacket); + take(reentrantRemovedPacket); + delete reentrantTriggerPacket; + delete reentrantRemovedPacket; + + outerCompound.stopObserving(); + check_and_cast(pendingQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &directCallback); + pendingQueueModule->callFinish(); + pendingQueueModule->deleteModule(); + + // Starting the frontier at the enclosing module forces traversal through + // PacketShaper, which is not an IPacketQueue, before reaching its leaf. + auto packetShaperModule = cModuleType::get("inet.queueing.shaper.PacketShaper")->create("callbackPacketShaper", this); + packetShaperModule->callInitialize(); + auto wrappedLeaf = check_and_cast(packetShaperModule->getSubmodule("queue")); + TestPacketDepartureListener wrappedCallback; + TestCompoundPacketQueue wrappedCompound; + wrappedCompound.observeDescendantQueues(this); + wrappedCompound.addObserver(&wrappedCallback); + auto wrappedPacket = new Packet("wrappedCompoundDrop", makeShared(B(1))); + wrappedLeaf->handlePacketDropped(wrappedPacket); + ASSERT(wrappedCallback.numDroppedPackets == 1); + delete wrappedPacket; + wrappedCompound.stopObserving(); + packetShaperModule->callFinish(); + packetShaperModule->deleteModule(); +} + +// Compound capacity enforcement detaches its leaf victim without leaking a +// REMOVED callback before reporting the single destructive departure. +{ + auto pendingQueueModule = cModuleType::get("inet.linklayer.ieee80211.mac.queue.CompoundPendingQueue")->create("callbackOverflowPendingQueue", this); + pendingQueueModule->par("packetCapacity").setIntValue(1); + pendingQueueModule->par("dropperClass").setStringValue("inet::queueing::PacketAtCollectionEndDropper"); + pendingQueueModule->callInitialize(); + auto pendingQueue = check_and_cast(pendingQueueModule); + TestPacketDepartureListener directCallback; + TestPacketDepartureListener nestedCallback; + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &directCallback); + TestCompoundPacketQueue outerCompound; + outerCompound.observeQueue(pendingQueue); + outerCompound.addObserver(&nestedCallback); + pendingQueue->pushPacket(new Packet("compoundRetainedPacket", makeQosHeader(peer1, 1, SequenceNumberCyclic(1))), nullptr); + pendingQueue->pushPacket(new Packet("compoundOverflowPacket", makeQosHeader(peer1, 1, SequenceNumberCyclic(2))), nullptr); + ASSERT(pendingQueue->getNumPackets() == 1); + ASSERT(directCallback.numDroppedPackets == 1); + ASSERT(directCallback.numRemovedPackets == 0); + ASSERT(nestedCallback.numDroppedPackets == 1); + ASSERT(nestedCallback.numRemovedPackets == 0); + outerCompound.stopObserving(); + check_and_cast(pendingQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &directCallback); + pendingQueue->removeAllPackets(); + pendingQueueModule->callFinish(); + pendingQueueModule->deleteModule(); +} + +// A shared buffer detaches every victim from its owning queue before the first +// typed drop callback. This keeps protocol cleanup from reentering while a +// later victim is still selectable from another queue. +{ + auto bufferModule = cModuleType::get("inet.queueing.buffer.PacketBuffer")->create("sharedOverflowBuffer", this); + bufferModule->par("dataCapacity").setIntValue(24); + bufferModule->par("dropperClass").setStringValue("inet::queueing::PacketAtCollectionBeginDropper"); + bufferModule->callInitialize(); + auto createBufferedQueue = [&](const char *name) { + auto module = cModuleType::get("inet.queueing.queue.PacketQueue")->create(name, this); + module->par("bufferModule").setStringValue("^.sharedOverflowBuffer"); + module->callInitialize(); + return check_and_cast(module); + }; + auto firstQueue = createBufferedQueue("sharedOverflowFirstQueue"); + auto secondQueue = createBufferedQueue("sharedOverflowSecondQueue"); + TestPacketDepartureListener dropCallback; + dropCallback.expectedPacketCountsAtFirstDrop = { { firstQueue, 1 }, { secondQueue, 0 } }; + check_and_cast(firstQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + check_and_cast(secondQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + auto firstVictim = new Packet("sharedFirstVictim", makeShared(B(1))); + auto retainedPacket = new Packet("sharedRetainedPacket", makeShared(B(3))); + TestReentrantRemovalListener reentrantCallback; + reentrantCallback.queue = firstQueue; + reentrantCallback.triggerPacket = firstVictim; + reentrantCallback.packetToRemove = retainedPacket; + reentrantCallback.triggerReason = queueing::IPacketQueue::PacketRemovalReason::DROPPED; + check_and_cast(firstQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &reentrantCallback); + firstQueue->pushPacket(firstVictim, nullptr); + secondQueue->pushPacket(new Packet("sharedSecondVictim", makeShared(B(1))), nullptr); + firstQueue->pushPacket(retainedPacket, nullptr); + if (dropCallback.numDroppedPackets != 2 || dropCallback.numRemovedPackets != 1 || !dropCallback.allExpectedPacketsDetachedAtFirstDrop || !reentrantCallback.removedPacket || firstQueue->getNumPackets() != 0 || secondQueue->getNumPackets() != 0) + throw cRuntimeError("Shared buffer reentrant removal violated the detached victim batch contract"); + take(retainedPacket); + delete retainedPacket; + check_and_cast(firstQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &reentrantCallback); + check_and_cast(firstQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + check_and_cast(secondQueue)->unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dropCallback); + firstQueue->callFinish(); + secondQueue->callFinish(); + bufferModule->callFinish(); + firstQueue->deleteModule(); + secondQueue->deleteModule(); + bufferModule->deleteModule(); +} +EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; +%contains: stdout +ADDBA transaction and negotiated SSN integrity checks passed. From 51699bf70a89d38f4fe10e2bc440e473a96e071f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:02 +0200 Subject: [PATCH 29/56] ieee80211: make originator ADDBA transactions explicit Represent an originator ADDBA exchange as explicit state keyed by peer and TID, with a transaction identity carried by every request fragment. Start the exchange only after the final trigger MPDU fragment is acknowledged, so the advertised starting sequence number is valid. Match responses by dialog token, keep same-TID traffic ineligible while a request is pending, and handle response timeout, retry backoff, DELBA, and terminal cancellation without allowing stale fragments to affect a newer exchange. Keep A-MSDU selection and HCF continuation aligned with that state. Cover trigger acknowledgement, fragmented requests and teardowns, timeout and retry paths, cancellation ownership, queue eligibility, and provider-aware A-MSDU extraction. --- .../aggregation/BasicMsduAggregationPolicy.cc | 61 +- .../aggregation/BasicMsduAggregationPolicy.h | 2 +- .../blockack/Ieee80211AddbaTransactionTag.msg | 16 + .../blockack/OriginatorBlockAckAgreement.h | 27 +- .../OriginatorBlockAckAgreementHandler.cc | 287 ++- .../OriginatorBlockAckAgreementHandler.h | 30 +- .../OriginatorBlockAckAgreementPolicy.cc | 17 +- .../OriginatorBlockAckAgreementPolicy.h | 7 +- .../OriginatorBlockAckAgreementPolicy.ned | 3 +- .../ieee80211/mac/channelaccess/Edca.h | 2 +- .../IBlockAckAgreementHandlerCallback.h | 6 +- .../mac/contract/IMsduAggregationPolicy.h | 7 +- .../IOriginatorBlockAckAgreementHandler.h | 35 +- .../IOriginatorBlockAckAgreementPolicy.h | 4 +- .../mac/contract/IOriginatorMacDataService.h | 9 +- .../ieee80211/mac/coordinationfunction/Dcf.cc | 6 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 401 +++- .../ieee80211/mac/coordinationfunction/Hcf.h | 25 +- .../ieee80211/mac/framesequence/HcfFs.cc | 23 +- .../mac/originator/NonQosRecoveryProcedure.cc | 34 +- .../mac/originator/NonQosRecoveryProcedure.h | 4 +- .../originator/OriginatorMacDataService.cc | 22 +- .../mac/originator/OriginatorMacDataService.h | 5 +- .../originator/OriginatorQosMacDataService.cc | 80 +- .../originator/OriginatorQosMacDataService.h | 7 +- .../ieee80211/mac/queue/InProgressFrames.cc | 25 +- .../ieee80211/mac/queue/InProgressFrames.h | 3 +- .../ieee80211/mgmt/Ieee80211MgmtFrame.msg | 1 + tests/unit/Ieee80211AddbaTransaction_1.test | 1609 +++++++++++++++++ 29 files changed, 2558 insertions(+), 200 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag.msg diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc index 2d06484c6a4..951f011c5ee 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc @@ -56,40 +56,61 @@ bool BasicMsduAggregationPolicy::isEligible(Packet *packet, const Ptr *BasicMsduAggregationPolicy::computeAggregateFrames(queueing::IPacketQueue *queue) +std::vector *BasicMsduAggregationPolicy::computeAggregateFrames(queueing::IPacketQueue *queue, Packet *candidate, const std::function& isFrameEligible) { Enter_Method("computeAggregateFrames"); - ASSERT(!queue->isEmpty()); + ASSERT(candidate != nullptr); b aMsduLength = b(0); - Ptr firstHeader = nullptr; - auto frames = new std::vector(); + int candidateIndex = -1; for (int i = 0; i < queue->getNumPackets(); i++) { - auto dataPacket = queue->getPacket(i); + if (queue->getPacket(i) == candidate) { candidateIndex = i; break; } + } + if (candidateIndex == -1) + return nullptr; + const auto& firstHeader = dynamicPtrCast(candidate->peekAtFront()); + if (firstHeader == nullptr || !isFrameEligible(candidate)) + return nullptr; + auto frames = new std::vector(); + auto hasSameFlow = [&](const Ptr& dataHeader) { + return dataHeader != nullptr && dataHeader->getTid() == firstHeader->getTid() && + dataHeader->getReceiverAddress() == firstHeader->getReceiverAddress() && + dataHeader->getTransmitterAddress() == firstHeader->getTransmitterAddress(); + }; + auto appendIfEligible = [&](Packet *dataPacket) { const auto& dataHeader = dynamicPtrCast(dataPacket->peekAtFront()); - if (dataHeader == nullptr) - break; - if (firstHeader == nullptr) - firstHeader = dataHeader; + if (!hasSameFlow(dataHeader)) + return true; + // IEEE Std 802.11-2024, 5.1.3: preserve the ordering of MSDUs with the + // same traffic identifier. Enumeration order is the conservative + // intra-flow order for this built-in policy. Never overtake a + // same-flow packet which is held by transaction eligibility or cannot + // fit in the current A-MSDU. + if (!isFrameEligible(dataPacket)) + return false; const auto& dataTrailer = dataPacket->peekAtBack(B(4)); - if (!isEligible(dataPacket, staticPtrCast(dataHeader), dataTrailer, firstHeader, aMsduLength)) { - EV_TRACE << "Queued " << *dataPacket << " is not eligible for A-MSDU aggregation.\n"; - break; - } - EV_TRACE << "Queued " << *dataPacket << " is eligible for A-MSDU aggregation.\n"; + if (!isEligible(dataPacket, dataHeader, dataTrailer, firstHeader, aMsduLength)) + return false; frames->push_back(dataPacket); - aMsduLength += dataPacket->getDataLength() - dataHeader->getChunkLength() - dataTrailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER); // sum of MSDU lengths + subframe header + aMsduLength += dataPacket->getDataLength() - dataHeader->getChunkLength() - dataTrailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER); + return true; + }; + if (!appendIfEligible(candidate)) { + delete frames; + return nullptr; } + // Do not wrap around: providers may schedule in an order different from + // their IPacketCollection enumeration (for example reverse priority). + for (int i = candidateIndex + 1; i < queue->getNumPackets(); i++) + if (!appendIfEligible(queue->getPacket(i))) + break; if (frames->size() <= 1 || !isAggregationPossible(frames->size(), aMsduLength.get())) { EV_DEBUG << "A-MSDU aggregation is not possible, collected " << frames->size() << " packets.\n"; delete frames; return nullptr; } - else { - EV_DEBUG << "A-MSDU aggregation is possible, collected " << frames->size() << " packets.\n"; - return frames; - } + EV_DEBUG << "A-MSDU aggregation is possible, collected " << frames->size() << " packets.\n"; + return frames; } } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h index 0534914eaf4..22ddab978ea 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h @@ -28,7 +28,7 @@ class INET_API BasicMsduAggregationPolicy : public IMsduAggregationPolicy, publi virtual bool isEligible(Packet *packet, const Ptr& header, const Ptr& trailer, const Ptr& testHeader, b aMsduLength); public: - virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *queue) override; + virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *queue, Packet *candidate, const std::function& isFrameEligible) override; }; } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag.msg b/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag.msg new file mode 100644 index 00000000000..43d9033c743 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag.msg @@ -0,0 +1,16 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +import inet.common.INETDefs; +import inet.common.TagBase; + +namespace inet::ieee80211; + +// Identifies one local originator ADDBA transaction across packet transformations. +class Ieee80211AddbaTransactionTag extends TagBase +{ + uint64_t transactionId; +} diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index a516527bc34..8099d7c1bdb 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -15,6 +15,12 @@ namespace ieee80211 { class OriginatorBlockAckAgreementHandler; +enum class OriginatorBlockAckAgreementState +{ + PENDING, + ESTABLISHED, +}; + class INET_API OriginatorBlockAckAgreement : public cObject { protected: @@ -25,19 +31,24 @@ class INET_API OriginatorBlockAckAgreement : public cObject int bufferSize = -1; bool isAMsduSupported = false; bool isDelayedBlockAckPolicySupported = false; - bool isAddbaResponseReceived = false; + OriginatorBlockAckAgreementState state = OriginatorBlockAckAgreementState::PENDING; bool isAddbaRequestSent = false; + uint8_t dialogToken = 0; + uint64_t transactionId = 0; + simtime_t addbaResponseDeadline = -1; simtime_t blockAckTimeoutValue = -1; simtime_t expirationTime = -1; public: - OriginatorBlockAckAgreement(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, bool isAMsduSupported, bool isDelayedBlockAckPolicySupported) : + OriginatorBlockAckAgreement(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, bool isAMsduSupported, bool isDelayedBlockAckPolicySupported, uint8_t dialogToken, uint64_t transactionId) : receiverAddr(receiverAddr), tid(tid), startingSequenceNumber(startingSequenceNumber), bufferSize(bufferSize), isAMsduSupported(isAMsduSupported), - isDelayedBlockAckPolicySupported(isDelayedBlockAckPolicySupported) + isDelayedBlockAckPolicySupported(isDelayedBlockAckPolicySupported), + dialogToken(dialogToken), + transactionId(transactionId) { } @@ -46,7 +57,8 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual int getBufferSize() const { return bufferSize; } virtual SequenceNumberCyclic getStartingSequenceNumber() { return startingSequenceNumber; } virtual void setStartingSequenceNumber(SequenceNumberCyclic sequenceNumber) { startingSequenceNumber = sequenceNumber; } - virtual bool getIsAddbaResponseReceived() const { return isAddbaResponseReceived; } + virtual bool getIsAddbaResponseReceived() const { return state == OriginatorBlockAckAgreementState::ESTABLISHED; } + virtual bool isPending() const { return state == OriginatorBlockAckAgreementState::PENDING; } virtual bool getIsAddbaRequestSent() const { return isAddbaRequestSent; } virtual bool getIsAMsduSupported() const { return isAMsduSupported; } virtual bool getIsDelayedBlockAckPolicySupported() const { return isDelayedBlockAckPolicySupported; } @@ -54,13 +66,17 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual Tid getTid() const { return tid; } virtual const simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } virtual int getNumSentBaPolicyFrames() const { return numSentBaPolicyFrames; } + virtual uint8_t getDialogToken() const { return dialogToken; } + virtual uint64_t getTransactionId() const { return transactionId; } + virtual simtime_t getAddbaResponseDeadline() const { return addbaResponseDeadline; } virtual void setBufferSize(int bufferSize) { this->bufferSize = bufferSize; } - virtual void setIsAddbaResponseReceived(bool isAddbaResponseReceived) { this->isAddbaResponseReceived = isAddbaResponseReceived; } + virtual void setIsAddbaResponseReceived(bool isAddbaResponseReceived) { state = isAddbaResponseReceived ? OriginatorBlockAckAgreementState::ESTABLISHED : OriginatorBlockAckAgreementState::PENDING; } virtual void setIsAddbaRequestSent(bool isAddbaRequestSent) { this->isAddbaRequestSent = isAddbaRequestSent; } virtual void setIsAMsduSupported(bool isAMsduSupported) { this->isAMsduSupported = isAMsduSupported; } virtual void setIsDelayedBlockAckPolicySupported(bool isDelayedBlockAckPolicySupported) { this->isDelayedBlockAckPolicySupported = isDelayedBlockAckPolicySupported; } virtual void setBlockAckTimeoutValue(const simtime_t blockAckTimeoutValue) { this->blockAckTimeoutValue = blockAckTimeoutValue; } + virtual void setAddbaResponseDeadline(simtime_t addbaResponseDeadline) { this->addbaResponseDeadline = addbaResponseDeadline; } virtual void baPolicyFrameSent() { numSentBaPolicyFrames++; } virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } @@ -71,4 +87,3 @@ class INET_API OriginatorBlockAckAgreement : public cObject } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 2f11dc365db..d03bae7b498 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -7,18 +7,30 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include + #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" namespace inet { namespace ieee80211 { -void OriginatorBlockAckAgreementHandler::createAgreement(const Ptr& addbaRequest) +void OriginatorBlockAckAgreementHandler::createAgreement(const Ptr& addbaRequest, uint64_t transactionId) { - OriginatorBlockAckAgreement *blockAckAgreement = new OriginatorBlockAckAgreement(addbaRequest->getReceiverAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaRequest->getBufferSize(), addbaRequest->getAMsduSupported(), addbaRequest->getBlockAckPolicy() == 0); + ASSERT(addbaRequest->getDialogToken() != 0); + OriginatorBlockAckAgreement *blockAckAgreement = new OriginatorBlockAckAgreement(addbaRequest->getReceiverAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaRequest->getBufferSize(), addbaRequest->getAMsduSupported(), addbaRequest->getBlockAckPolicy() == 0, addbaRequest->getDialogToken(), transactionId); auto agreementId = std::make_pair(addbaRequest->getReceiverAddress(), addbaRequest->getTid()); blockAckAgreements[agreementId] = blockAckAgreement; } +uint8_t OriginatorBlockAckAgreementHandler::allocateDialogToken() +{ + auto dialogToken = nextDialogToken; + nextDialogToken = nextDialogToken == 255 ? 1 : nextDialogToken + 1; + return dialogToken; +} + simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime() { simtime_t earliestTime = SIMTIME_MAX; @@ -33,6 +45,57 @@ simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime() return earliestTime; } +simtime_t OriginatorBlockAckAgreementHandler::computeEarliestAddbaResponseDeadline() const +{ + simtime_t earliestDeadline = SIMTIME_MAX; + for (const auto& entry : blockAckAgreements) { + auto agreement = entry.second; + if (agreement->isPending() && agreement->getIsAddbaRequestSent()) { + ASSERT(agreement->getAddbaResponseDeadline() >= 0); + earliestDeadline = std::min(earliestDeadline, agreement->getAddbaResponseDeadline()); + } + } + return earliestDeadline; +} + +simtime_t OriginatorBlockAckAgreementHandler::getAddbaResponseTimeout(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) const +{ + auto timeout = blockAckAgreementPolicy->getAddbaResponseTimeout(); + if (timeout <= 0) + throw cRuntimeError("ADDBA response timeout must be greater than zero"); + return timeout; +} + +void OriginatorBlockAckAgreementHandler::recordAddbaFailure(MacAddress receiverAddr, Tid tid, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) +{ + auto retryBackoff = blockAckAgreementPolicy->computeAddbaRetryBackoff(); + if (retryBackoff < 0) + throw cRuntimeError("ADDBA retry backoff must not be negative"); + addbaRetryDeadlines[std::make_pair(receiverAddr, tid)] = simTime() + retryBackoff; +} + +void OriginatorBlockAckAgreementHandler::addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) +{ + auto now = simTime(); + std::vector expiredTransactionIds; + for (auto it = blockAckAgreements.begin(); it != blockAckAgreements.end();) { + auto agreement = it->second; + if (agreement->isPending() && agreement->getIsAddbaRequestSent() && agreement->getAddbaResponseDeadline() <= now) { + EV_INFO << "ADDBA transaction timeout for receiver=" << agreement->getReceiverAddr() << " tid=" << (int)agreement->getTid() << endl; + auto transactionId = agreement->getTransactionId(); + recordAddbaFailure(agreement->getReceiverAddr(), agreement->getTid(), blockAckAgreementPolicy); + it = blockAckAgreements.erase(it); + delete agreement; + expiredTransactionIds.push_back(transactionId); + } + else + it++; + } + for (auto transactionId : expiredTransactionIds) + callback->cancelAddbaTransaction(transactionId, nullptr); + scheduleAddbaResponseTimer(callback); +} + void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the @@ -57,6 +120,9 @@ const Ptr OriginatorBlockAckAgreementHandler::buildAddbaR { auto addbaRequest = makeShared(); addbaRequest->setReceiverAddress(receiverAddr); + // IEEE Std 802.11-2024, 9.6.4.2: a solicited ADDBA Request uses a nonzero + // Dialog Token, and the corresponding response copies that token. + addbaRequest->setDialogToken(allocateDialogToken()); addbaRequest->setTid(tid); addbaRequest->setAMsduSupported(blockAckAgreementPolicy->isMsduSupported()); addbaRequest->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue()); @@ -92,6 +158,11 @@ void OriginatorBlockAckAgreementHandler::scheduleInactivityTimer(IBlockAckAgreem callback->scheduleInactivityTimer(earliestExpirationTime); } +void OriginatorBlockAckAgreementHandler::scheduleAddbaResponseTimer(IBlockAckAgreementHandlerCallback *callback) +{ + callback->scheduleAddbaResponseTimer(computeEarliestAddbaResponseDeadline()); +} + OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(MacAddress receiverAddr, Tid tid) { auto agreementId = std::make_pair(receiverAddr, tid); @@ -99,6 +170,12 @@ OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(Ma return it != blockAckAgreements.end() ? it->second : nullptr; } +bool OriginatorBlockAckAgreementHandler::isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const +{ + auto it = blockAckAgreements.find(std::make_pair(receiverAddr, tid)); + return it != blockAckAgreements.end() && it->second->isPending(); +} + const Ptr OriginatorBlockAckAgreementHandler::buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode) { auto delba = makeShared(); @@ -110,37 +187,102 @@ const Ptr OriginatorBlockAckAgreementHandler::buildDelba(MacAddr return delba; } -void OriginatorBlockAckAgreementHandler::terminateAgreement(MacAddress originatorAddr, Tid tid) +OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::removeAgreement(MacAddress originatorAddr, Tid tid) { auto agreementId = std::make_pair(originatorAddr, tid); auto it = blockAckAgreements.find(agreementId); if (it != blockAckAgreements.end()) { - OriginatorBlockAckAgreement *agreement = it->second; + auto agreement = it->second; blockAckAgreements.erase(it); - delete agreement; + return agreement; } + return nullptr; +} + +void OriginatorBlockAckAgreementHandler::terminateAgreement(MacAddress originatorAddr, Tid tid) +{ + delete removeAgreement(originatorAddr, tid); } -void OriginatorBlockAckAgreementHandler::processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) +uint64_t OriginatorBlockAckAgreementHandler::processAcknowledgedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback) { - auto agreement = getAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid()); - if (blockAckAgreementPolicy->isAddbaReqNeeded(packet, dataHeader) && agreement == nullptr) { - auto addbaReq = buildAddbaRequest(dataHeader->getReceiverAddress(), dataHeader->getTid(), dataHeader->getSequenceNumber() + 1, blockAckAgreementPolicy); - createAgreement(addbaReq); + // IEEE Std 802.11-2024, Table 9-466: the Starting Sequence Number identifies + // the first or next MSDU/A-MSDU sent under the agreement. Wait until the + // final fragment is acknowledged so no remaining fragment precedes the SSN. + if (dataHeader->getMoreFragments()) + return 0; + auto receiverAddr = dataHeader->getReceiverAddress(); + auto tid = dataHeader->getTid(); + auto agreementId = std::make_pair(receiverAddr, tid); + auto agreement = getAgreement(receiverAddr, tid); + auto retryIt = addbaRetryDeadlines.find(agreementId); + bool retryAllowed = retryIt == addbaRetryDeadlines.end() || retryIt->second <= simTime(); + uint64_t obsoleteTeardownTransactionId = 0; + if (blockAckAgreementPolicy->isAddbaReqNeeded(packet, dataHeader) && agreement == nullptr && retryAllowed) { + if (retryIt != addbaRetryDeadlines.end()) + addbaRetryDeadlines.erase(retryIt); + // A replacement agreement makes an older best-effort DELBA stale. Drop + // its eligibility instead of waiting indefinitely for a queue callback + // that every custom packet provider may not deliver. + auto teardownIt = pendingTeardownTransactionIds.find(agreementId); + if (teardownIt != pendingTeardownTransactionIds.end()) { + obsoleteTeardownTransactionId = teardownIt->second; + pendingTeardownTransactionIds.erase(teardownIt); + } + // IEEE Std 802.11-2024, 10.25.2 and 11.5.2.2: Normal Ack data is + // permitted before an agreement exists, and the requested SSN starts + // after the acknowledged trigger MPDU. + auto addbaReq = buildAddbaRequest(receiverAddr, tid, dataHeader->getSequenceNumber() + 1, blockAckAgreementPolicy); + auto transactionId = nextTransactionId++; + createAgreement(addbaReq, transactionId); auto addbaPacket = new Packet("AddbaReq", addbaReq); - callback->processMgmtFrame(addbaPacket, addbaReq); + addbaPacket->addTag()->setTransactionId(transactionId); + procedureCallback->processMgmtFrame(addbaPacket, addbaReq); } + return obsoleteTeardownTransactionId; } -void OriginatorBlockAckAgreementHandler::processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) +OriginatorBlockAckAgreementResponse OriginatorBlockAckAgreementHandler::processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) { + OriginatorBlockAckAgreementResponse response; auto agreement = getAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid()); - if (blockAckAgreementPolicy->isAddbaReqAccepted(addbaResp, agreement)) { + // IEEE Std 802.11-2024, 11.5.2.2: only a successful response matching the + // outstanding peer, TID, and Dialog Token establishes the agreement. + if (agreement == nullptr || !agreement->isPending() || !agreement->getIsAddbaRequestSent() || agreement->getDialogToken() != addbaResp->getDialogToken()) + return response; + bool acceptedByLocalPolicy = addbaResp->getStatusCode() == 0 && blockAckAgreementPolicy->isAddbaReqAccepted(addbaResp, agreement); + if (addbaResp->getStatusCode() == 0) { + auto transactionId = agreement->getTransactionId(); updateAgreement(agreement, addbaResp); + if (acceptedByLocalPolicy) + addbaRetryDeadlines.erase(std::make_pair(addbaResp->getTransmitterAddress(), addbaResp->getTid())); + else + recordAddbaFailure(addbaResp->getTransmitterAddress(), addbaResp->getTid(), blockAckAgreementPolicy); scheduleInactivityTimer(callback); + scheduleAddbaResponseTimer(callback); + callback->cancelAddbaTransaction(transactionId, nullptr); + if (!acceptedByLocalPolicy) { + // IEEE Std 802.11-2024, 10.25.2 Note 3: delete a successful + // agreement rejected by local policy and continue with Normal Ack. + response.terminatedAgreement.reset(removeAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid())); + if (response.terminatedAgreement == nullptr) + throw cRuntimeError("Cannot terminate locally vetoed Block Ack agreement"); + response.teardownDelba = buildDelba(addbaResp->getTransmitterAddress(), addbaResp->getTid(), RC_END_BA); + pendingTeardownTransactionIds[std::make_pair(addbaResp->getTransmitterAddress(), addbaResp->getTid())] = transactionId; + response.teardownTransactionId = transactionId; + scheduleInactivityTimer(callback); + } + else + response.establishedAgreement = agreement; + return response; } else { - // TODO send a new one? + auto transactionId = agreement->getTransactionId(); + recordAddbaFailure(addbaResp->getTransmitterAddress(), addbaResp->getTid(), blockAckAgreementPolicy); + terminateAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid()); + scheduleAddbaResponseTimer(callback); + callback->cancelAddbaTransaction(transactionId, nullptr); + return response; } } @@ -152,24 +294,122 @@ void OriginatorBlockAckAgreementHandler::updateAgreement(OriginatorBlockAckAgree agreement->calculateExpirationTime(); } -void OriginatorBlockAckAgreementHandler::processTransmittedAddbaReq(const Ptr& addbaReq) +bool OriginatorBlockAckAgreementHandler::isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const +{ + auto it = blockAckAgreements.find(std::make_pair(addbaReq->getReceiverAddress(), addbaReq->getTid())); + auto transactionTag = packet->findTag(); + return it != blockAckAgreements.end() && it->second->isPending() && transactionTag != nullptr && + it->second->getDialogToken() == addbaReq->getDialogToken() && it->second->getTransactionId() == transactionTag->getTransactionId(); +} + +bool OriginatorBlockAckAgreementHandler::isDelbaPending(const Packet *packet, const Ptr& delba) const +{ + if (!delba->getInitiator()) + return true; + auto transactionTag = packet->findTag(); + if (transactionTag == nullptr) + return true; + auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + return it != pendingTeardownTransactionIds.end() && it->second == transactionTag->getTransactionId(); +} + +void OriginatorBlockAckAgreementHandler::processTransmittedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) { auto agreement = getAgreement(addbaReq->getReceiverAddress(), addbaReq->getTid()); - if (agreement) + if (isAddbaRequestPending(packet, addbaReq) && !addbaReq->getMoreFragments() && !agreement->getIsAddbaRequestSent()) { + agreement->setAddbaResponseDeadline(simTime() + getAddbaResponseTimeout(blockAckAgreementPolicy)); agreement->setIsAddbaRequestSent(true); - else - throw cRuntimeError("Block Ack Agreement should have already been added"); + scheduleAddbaResponseTimer(callback); + } + else if (!isAddbaRequestPending(packet, addbaReq)) + EV_WARN << "Ignoring stale transmitted ADDBA Request for receiver=" << addbaReq->getReceiverAddress() << " tid=" << (int)addbaReq->getTid() << " dialogToken=" << (int)addbaReq->getDialogToken() << endl; } -void OriginatorBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) +void OriginatorBlockAckAgreementHandler::processDroppedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) { - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + if (isAddbaRequestPending(packet, addbaReq)) { + auto transactionId = packet->getTag()->getTransactionId(); + recordAddbaFailure(addbaReq->getReceiverAddress(), addbaReq->getTid(), blockAckAgreementPolicy); + terminateAgreement(addbaReq->getReceiverAddress(), addbaReq->getTid()); + scheduleAddbaResponseTimer(callback); + callback->cancelAddbaTransaction(transactionId, packet); + } } -void OriginatorBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) +std::unique_ptr OriginatorBlockAckAgreementHandler::processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - terminateAgreement(delba->getTransmitterAddress(), delba->getTid()); + auto delba = packet->peekAtFront(); + auto transactionTag = packet->findTag(); + if (transactionTag != nullptr) { + auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it == pendingTeardownTransactionIds.end() || it->second != transactionTag->getTransactionId()) + return nullptr; + // IEEE Std 802.11-2024, 11.5.3.2: teardown is performed by + // transmitting DELBA. IEEE Std 802.11-2024, 10.23.2.12.1 and + // 10.3.4.4 require unsuccessful MMPDU attempts to be retried until + // success or the applicable retry limit. Keep the local transaction + // live across ordinary MAC retries and retire it only after the final + // fragment is acknowledged or the frame is terminally aborted. + return nullptr; + } + auto agreement = getAgreement(delba->getReceiverAddress(), delba->getTid()); + bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); + auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; + std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleAddbaResponseTimer(callback); + if (cancelPendingTransaction) + callback->cancelAddbaTransaction(transactionId, nullptr); + return terminatedAgreement; +} + +bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = packet->peekAtFront(); + if (!delba->getInitiator() || delba->getMoreFragments()) + return false; + auto transactionTag = packet->findTag(); + if (transactionTag == nullptr) + return false; + auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it == pendingTeardownTransactionIds.end() || it->second != transactionTag->getTransactionId()) + return false; + auto transactionId = it->second; + pendingTeardownTransactionIds.erase(it); + callback->cancelAddbaTransaction(transactionId, packet); + return true; +} + +bool OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = packet->peekAtFront(); + if (!delba->getInitiator()) + return false; + auto transactionTag = packet->findTag(); + if (transactionTag != nullptr) { + auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it != pendingTeardownTransactionIds.end() && it->second == transactionTag->getTransactionId()) { + auto transactionId = it->second; + pendingTeardownTransactionIds.erase(it); + callback->cancelAddbaTransaction(transactionId, packet); + return true; + } + } + return false; +} + +std::unique_ptr OriginatorBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) +{ + if (blockAckAgreementPolicy->isDelbaAccepted(delba)) { + auto agreement = getAgreement(delba->getTransmitterAddress(), delba->getTid()); + bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); + auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; + std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + scheduleAddbaResponseTimer(callback); + if (cancelPendingTransaction) + callback->cancelAddbaTransaction(transactionId, nullptr); + return terminatedAgreement; + } + return nullptr; } OriginatorBlockAckAgreementHandler::~OriginatorBlockAckAgreementHandler() @@ -180,4 +420,3 @@ OriginatorBlockAckAgreementHandler::~OriginatorBlockAckAgreementHandler() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 4a449d17da3..8be627c5135 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -21,31 +21,47 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg { protected: std::map, OriginatorBlockAckAgreement *> blockAckAgreements; + std::map, simtime_t> addbaRetryDeadlines; + std::map, uint64_t> pendingTeardownTransactionIds; + uint8_t nextDialogToken = 1; + uint64_t nextTransactionId = 1; protected: virtual const Ptr buildAddbaRequest(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); - virtual void createAgreement(const Ptr& addbaRequest); + virtual uint8_t allocateDialogToken(); + virtual void createAgreement(const Ptr& addbaRequest, uint64_t transactionId); virtual void updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp); + virtual OriginatorBlockAckAgreement *removeAgreement(MacAddress originatorAddr, Tid tid); virtual void terminateAgreement(MacAddress originatorAddr, Tid tid); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); virtual simtime_t computeEarliestExpirationTime(); + virtual simtime_t computeEarliestAddbaResponseDeadline() const; virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback); + virtual void scheduleAddbaResponseTimer(IBlockAckAgreementHandlerCallback *callback); + virtual simtime_t getAddbaResponseTimeout(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) const; + virtual void recordAddbaFailure(MacAddress receiverAddr, Tid tid, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); public: virtual ~OriginatorBlockAckAgreementHandler(); - virtual void processTransmittedAddbaReq(const Ptr& addbaReq) override; - virtual void processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) override; + virtual void processTransmittedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; + virtual void processDroppedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; + virtual uint64_t processAcknowledgedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback) override; virtual void processReceivedBlockAck(const Ptr& blockAck, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) override; - virtual void processTransmittedDelba(const Ptr& delba) override; + virtual OriginatorBlockAckAgreementResponse processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; virtual OriginatorBlockAckAgreement *getAgreement(MacAddress receiverAddr, Tid tid) override; + virtual bool isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const override; + virtual bool isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const override; + virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const override; }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc index 44b4c0874fa..0382f53e378 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc @@ -24,17 +24,16 @@ void OriginatorBlockAckAgreementPolicy::initialize(int stage) aMsduSupported = par("aMsduSupported"); maximumAllowedBufferSize = par("maximumAllowedBufferSize"); blockAckTimeoutValue = par("blockAckTimeoutValue"); - // TODO addbaFailureTimeout = par("addbaFailureTimeout"); + addbaResponseTimeout = par("addbaResponseTimeout"); + addbaRetryBackoff = par("addbaRetryBackoff"); + if (addbaResponseTimeout <= 0) + throw cRuntimeError("addbaResponseTimeout must be greater than zero"); + if (addbaRetryBackoff < 0) + throw cRuntimeError("addbaRetryBackoff must not be negative"); WATCH(blockAckReqThreshold); } } -simtime_t OriginatorBlockAckAgreementPolicy::computeAddbaFailureTimeout() const -{ - // TODO ADDBAFailureTimeout -- 6.3.29.2.2 Semantics of the service primitive - throw cRuntimeError("Unimplemented"); -} - bool OriginatorBlockAckAgreementPolicy::isAddbaReqNeeded(Packet *packet, const Ptr& header) { return ackPolicy->isBlockAckPolicyEligibleFrame(packet, header); @@ -42,8 +41,7 @@ bool OriginatorBlockAckAgreementPolicy::isAddbaReqNeeded(Packet *packet, const P bool OriginatorBlockAckAgreementPolicy::isAddbaReqAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) { - ASSERT(agreement); - return true; + return agreement != nullptr && addbaResp->getStatusCode() == 0; } bool OriginatorBlockAckAgreementPolicy::isDelbaAccepted(const Ptr& delba) @@ -53,4 +51,3 @@ bool OriginatorBlockAckAgreementPolicy::isDelbaAccepted(const Ptr& addbaResp, OriginatorBlockAckAgreement *agreement) override; virtual bool isDelbaAccepted(const Ptr& delba) override; - virtual simtime_t computeAddbaFailureTimeout() const override; + virtual simtime_t getAddbaResponseTimeout() const override { return addbaResponseTimeout; } + virtual simtime_t computeAddbaRetryBackoff() const override { return addbaRetryBackoff; } virtual bool isMsduSupported() const override { return aMsduSupported; } virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } @@ -48,4 +50,3 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetListener, publi } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned index bbb569b4b20..89e42723736 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned @@ -23,6 +23,7 @@ simple OriginatorBlockAckAgreementPolicy extends SimpleModule like IOriginatorBl bool aMsduSupported = default(true); int maximumAllowedBufferSize = default(64); double blockAckTimeoutValue @unit(s) = default(0s); // 0 means that it depends on the originator + double addbaResponseTimeout @unit(s) = default(1s); // Model response-wait timeout after the ADDBA Request is transmitted + double addbaRetryBackoff @unit(s) = default(1s); // Model retry suppression after a failed or discarded ADDBA transaction; not an IEEE 802.11 timer @display("i=block/control"); } - diff --git a/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h b/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h index c481e9a82c8..69436da824d 100644 --- a/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h +++ b/src/inet/linklayer/ieee80211/mac/channelaccess/Edca.h @@ -38,6 +38,7 @@ class INET_API Edca : public SimpleModule virtual ~Edca(); virtual AccessCategory classifyFrame(const Ptr& header); + virtual int getNumEdcafs() const { return numEdcafs; } virtual Edcaf *getEdcaf(AccessCategory ac) const { return edcafs[ac]; } virtual Edcaf *getChannelOwner(); virtual std::vector getInternallyCollidedEdcafs(); @@ -51,4 +52,3 @@ class INET_API Edca : public SimpleModule } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index cc4a7b86b92..09abd395fd3 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -11,6 +11,9 @@ #include "inet/common/INETDefs.h" namespace inet { + +class Packet; + namespace ieee80211 { class INET_API IBlockAckAgreementHandlerCallback @@ -19,10 +22,11 @@ class INET_API IBlockAckAgreementHandlerCallback virtual ~IBlockAckAgreementHandlerCallback() {} virtual void scheduleInactivityTimer(simtime_t timeout) = 0; + virtual void scheduleAddbaResponseTimer(simtime_t deadline) = 0; + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) = 0; }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h index 8260a067cef..3a08e3a8cbe 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IMsduAggregationPolicy.h @@ -8,6 +8,8 @@ #ifndef __INET_IMSDUAGGREGATIONPOLICY_H #define __INET_IMSDUAGGREGATIONPOLICY_H +#include + #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/queueing/contract/IPacketQueue.h" @@ -19,11 +21,12 @@ class INET_API IMsduAggregationPolicy public: virtual ~IMsduAggregationPolicy() {} - virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *queue) = 0; + // A non-null result is caller-owned and must contain at least two unique, + // discoverable, eligible frames with candidate as its first element. + virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *queue, Packet *candidate, const std::function& isFrameEligible) = 0; }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h index 4a01e89b6c3..fbe0918749a 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h @@ -8,9 +8,12 @@ #ifndef __INET_IORIGINATORBLOCKACKAGREEMENTHANDLER_H #define __INET_IORIGINATORBLOCKACKAGREEMENTHANDLER_H +#include + #include "inet/common/packet/Packet.h" #include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" #include "inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h" @@ -19,24 +22,44 @@ namespace inet { namespace ieee80211 { +struct INET_API OriginatorBlockAckAgreementResponse +{ + // Borrowed from the handler; valid while the established agreement remains installed. + OriginatorBlockAckAgreement *establishedAgreement = nullptr; + // Owns the agreement that was established and immediately terminated after a local veto. + std::unique_ptr terminatedAgreement; + Ptr teardownDelba; + uint64_t teardownTransactionId = 0; +}; + class INET_API IOriginatorBlockAckAgreementHandler { public: virtual ~IOriginatorBlockAckAgreementHandler() {} virtual void processReceivedBlockAck(const Ptr& blockAck, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void processTransmittedAddbaReq(const Ptr& addbaReq) = 0; - virtual void processTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) = 0; - virtual void processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; - virtual void processTransmittedDelba(const Ptr& delba) = 0; + virtual void processTransmittedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual void processDroppedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; + // Returns the transaction identity of an obsolete teardown whose packets + // must be cancelled by the caller, or 0 when there is none. + virtual uint64_t processAcknowledgedDataFrame(Packet *packet, const Ptr& dataHeader, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback) = 0; + virtual OriginatorBlockAckAgreementResponse processReceivedAddbaResp(const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; + // Returns true when the packet completed or aborted its tagged teardown + // transaction and sibling packets were cancelled through the callback. + virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; + virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; virtual OriginatorBlockAckAgreement *getAgreement(MacAddress receiverAddr, Tid tid) = 0; + virtual bool isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const = 0; + virtual bool isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const = 0; + virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const { return true; } }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h index 40ea82aa95e..11a5f26dfe6 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h @@ -26,7 +26,8 @@ class INET_API IOriginatorBlockAckAgreementPolicy virtual bool isDelbaAccepted(const Ptr& delba) = 0; virtual bool isMsduSupported() const = 0; - virtual simtime_t computeAddbaFailureTimeout() const = 0; + virtual simtime_t getAddbaResponseTimeout() const = 0; + virtual simtime_t computeAddbaRetryBackoff() const = 0; virtual simtime_t getBlockAckTimeoutValue() const = 0; virtual bool isDelayedAckPolicySupported() const = 0; virtual int getMaximumAllowedBufferSize() const = 0; @@ -36,4 +37,3 @@ class INET_API IOriginatorBlockAckAgreementPolicy } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h index 4e66ec6c190..9f0f8aad18a 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorMacDataService.h @@ -8,6 +8,8 @@ #ifndef __INET_IORIGINATORMACDATASERVICE_H #define __INET_IORIGINATORMACDATASERVICE_H +#include + #include "inet/queueing/contract/IPacketQueue.h" namespace inet { @@ -15,6 +17,9 @@ namespace ieee80211 { class INET_API IOriginatorMacDataService { + public: + using FrameEligibilityFunction = std::function; + public: static simsignal_t packetFragmentedSignal; static simsignal_t packetAggregatedSignal; @@ -22,6 +27,9 @@ class INET_API IOriginatorMacDataService public: virtual ~IOriginatorMacDataService() {} + virtual void setFrameEligibilityFunction(const FrameEligibilityFunction& frameEligibilityFunction) = 0; + virtual bool isFrameEligible(const Packet *packet) const = 0; + virtual bool hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const = 0; virtual std::vector *extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) = 0; }; @@ -29,4 +37,3 @@ class INET_API IOriginatorMacDataService } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index 675b26be37a..1aa1a7ba496 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -82,6 +82,12 @@ void Dcf::channelGranted(IChannelAccess *channelAccess) Enter_Method("channelGranted"); ASSERT(this->channelAccess == channelAccess); if (!frameSequenceHandler->isSequenceRunning()) { + if (this->channelAccess->getInProgressFrames()->getFrameToTransmit() == nullptr) { + EV_DETAIL << "Releasing channel because no frame is available.\n"; + channelAccess->releaseChannel(this); + mac->sendDownPendingRadioConfigMsg(); + return; + } frameSequenceHandler->startFrameSequence(new DcfFs(), buildContext(), this); emit(IFrameSequenceHandler::frameSequenceStartedSignal, frameSequenceHandler->getContext()); } diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 5659cfdc3f5..010529ff44b 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -13,9 +13,11 @@ #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" @@ -40,6 +42,7 @@ void Hcf::initialize(int stage) mac = check_and_cast(getContainingNicModule(this)->getSubmodule("mac")); startRxTimer = new cMessage("startRxTimeout"); inactivityTimer = new cMessage("blockAckInactivityTimer"); + addbaResponseTimer = new cMessage("addbaResponseTimer"); edca = check_and_cast(getSubmodule("edca")); hcca = check_and_cast(getSubmodule("hcca")); tx = check_and_cast(getModuleByPath(par("txModule"))); @@ -65,14 +68,121 @@ void Hcf::initialize(int stage) originatorBlockAckAgreementHandler = new OriginatorBlockAckAgreementHandler(); originatorBlockAckProcedure = new OriginatorBlockAckProcedure(); recipientBlockAckProcedure = new RecipientBlockAckProcedure(); + originatorDataService->setFrameEligibilityFunction([this](const Packet *packet) { + if (auto addbaReq = dynamicPtrCast(packet->peekAtFront())) + return originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq); + if (auto delba = dynamicPtrCast(packet->peekAtFront())) + return originatorBlockAckAgreementHandler->isDelbaPending(packet, delba); + auto dataHeader = dynamicPtrCast(packet->peekAtFront()); + // Hold this peer/TID while its ADDBA response is pending so no + // already-sequenced MPDU can precede the advertised SSN. The + // response timeout starts only after the request is transmitted. + return dataHeader == nullptr || dataHeader->getType() != ST_DATA_WITH_QOS || (!originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid())); + }); } } else if (stage == INITSTAGE_LAST) { // Edca resolves its Edcaf array at the link-layer stage. Install the // queue signal listeners after all child initialization has completed. - for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) - check_and_cast(edca->getEdcaf(static_cast(ac))->getPendingQueue())->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto pendingQueue = edca->getEdcaf(AccessCategory(ac))->getPendingQueue(); + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + } + rebuildPendingFrameEligibility(); + } +} + +void Hcf::trackPendingFrame(Packet *packet, AccessCategory accessCategory) +{ + untrackPendingFrame(packet); + bool eligible = originatorDataService->isFrameEligible(packet); + pendingFrameEligibility.emplace(packet, PendingFrameEligibility { accessCategory, eligible }); + if (eligible) + numEligiblePendingFrames[accessCategory]++; +} + +void Hcf::untrackPendingFrame(const Packet *packet) +{ + auto it = pendingFrameEligibility.find(packet); + if (it != pendingFrameEligibility.end()) { + if (it->second.eligible) { + ASSERT(numEligiblePendingFrames[it->second.accessCategory] > 0); + numEligiblePendingFrames[it->second.accessCategory]--; + } + pendingFrameEligibility.erase(it); + } +} + +void Hcf::rebuildPendingFrameEligibility() +{ + pendingFrameEligibility.clear(); + numEligiblePendingFrames.fill(0); + int numPendingFrames = 0; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto accessCategory = AccessCategory(ac); + auto pendingQueue = edca->getEdcaf(accessCategory)->getPendingQueue(); + for (int i = 0; i < pendingQueue->getNumPackets(); i++) { + trackPendingFrame(pendingQueue->getPacket(i), accessCategory); + numPendingFrames++; + } } + ASSERT((int)pendingFrameEligibility.size() == numPendingFrames); +} + +bool Hcf::processDroppedBlockAckSetupFrame(Packet *packet) +{ + if (originatorBlockAckAgreementHandler) { + auto addbaReq = dynamicPtrCast(packet->peekAtFront()); + if (addbaReq != nullptr && originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq)) { + originatorBlockAckAgreementHandler->processDroppedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); + rebuildPendingFrameEligibility(); + return true; + } + } + return false; +} + +bool Hcf::processDroppedBlockAckTeardownFrame(Packet *packet) +{ + if (originatorBlockAckAgreementHandler) { + auto delba = dynamicPtrCast(packet->peekAtFront()); + if (delba != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this)) { + rebuildPendingFrameEligibility(); + return true; + } + } + return false; +} + +void Hcf::receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) +{ + if (signal == queueing::IPacketQueue::packetQueueDepartureSignal) { + Enter_Method("packetQueueDeparture"); + if (source->isSubscribed(signal, this)) + handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); + } + else + ModeSetListener::receiveSignal(source, signal, object, details); +} + +void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) +{ + Enter_Method("handlePacketRemoved"); + untrackPendingFrame(packet); + bool shouldResume = false; + // HCF treats explicit REMOVED notifications as terminal transaction + // disposal; code relocating a packet must use dequeuePacket(). + if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED || reason == queueing::IPacketQueue::PacketRemovalReason::REMOVED) { + if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } + shouldResume |= processDroppedBlockAckSetupFrame(packet); + shouldResume |= processDroppedBlockAckTeardownFrame(packet); + } + if (shouldResume) + resumeEligibleChannelAccess(); } std::string Hcf::getFrameSequenceInfo() const @@ -109,6 +219,15 @@ void Hcf::handleMessage(cMessage *msg) else throw cRuntimeError("Unknown event"); } + else if (msg == addbaResponseTimer) { + if (originatorBlockAckAgreementHandler) { + originatorBlockAckAgreementHandler->addbaResponseTimeoutExpired(originatorBlockAckAgreementPolicy, this); + rebuildPendingFrameEligibility(); + resumeEligibleChannelAccess(); + } + else + throw cRuntimeError("Unknown event"); + } else throw cRuntimeError("Unknown msg type"); } @@ -146,8 +265,9 @@ void Hcf::processUpperFrame(Packet *packet, const PtrgetEdcaf(ac)->getPendingQueue(); + trackPendingFrame(packet, ac); pendingQueue->enqueuePacket(packet); - if (!pendingQueue->isEmpty()) { + if (hasFrameToTransmit(ac)) { auto edcaf = edca->getChannelOwner(); if (edcaf == nullptr || edcaf->getAccessCategory() != ac) { EV_DETAIL << "Requesting channel for access category " << printAccessCategory(ac) << endl; @@ -156,25 +276,22 @@ void Hcf::processUpperFrame(Packet *packet, const PtrisSubscribed(signal, this)) - handlePacketRemoved(check_and_cast(object), check_and_cast(details)->getReason()); - } - else - ModeSetListener::receiveSignal(source, signal, object, details); -} - -void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) -{ - Enter_Method("handlePacketRemoved"); - if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + if (frameSequenceHandler == nullptr || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + for (int i = 0; i < context->getNumSteps(); i++) { + auto transmitStep = dynamic_cast(context->getStep(i)); + if (transmitStep != nullptr && transmitStep->getFrameToTransmit() == packet) + return true; + auto rtsTransmitStep = dynamic_cast(transmitStep); + if (rtsTransmitStep != nullptr && rtsTransmitStep->getProtectedFrame() == packet) + return true; } + return false; } void Hcf::scheduleStartRxTimer(simtime_t timeout) @@ -189,6 +306,58 @@ void Hcf::scheduleInactivityTimer(simtime_t timeout) rescheduleAfter(timeout, inactivityTimer); } +void Hcf::scheduleAddbaResponseTimer(simtime_t deadline) +{ + Enter_Method("scheduleAddbaResponseTimer"); + if (deadline == SIMTIME_MAX) { + if (addbaResponseTimer->isScheduled()) + cancelEvent(addbaResponseTimer); + } + else + rescheduleAt(deadline, addbaResponseTimer); +} + +void Hcf::cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) +{ + Enter_Method("cancelAddbaTransaction"); + // Frames borrowed by the active sequence cannot be removed here. The + // sequence's failure paths detect their now-stale transaction and discard them. + auto belongsToTransaction = [this, transactionId, excludedPacket](Packet *packet) { + auto transactionTag = packet->findTag(); + return packet != excludedPacket && !isPacketReferencedByCurrentFrameSequence(packet) && transactionTag != nullptr && transactionTag->getTransactionId() == transactionId; + }; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto edcaf = edca->getEdcaf(AccessCategory(ac)); + auto pendingQueue = edcaf->getPendingQueue(); + for (int i = pendingQueue->getNumPackets() - 1; i >= 0; i--) { + auto packet = pendingQueue->getPacket(i); + if (belongsToTransaction(packet)) { + pendingQueue->removePacket(packet); + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + auto inProgressFrames = edcaf->getInProgressFrames(); + for (int i = inProgressFrames->getLength() - 1; i >= 0; i--) { + auto packet = inProgressFrames->getFrames(i); + if (belongsToTransaction(packet)) { + auto header = packet->peekAtFront(); + auto extractedPacket = inProgressFrames->extractFrame(packet); + ASSERT(extractedPacket == packet); + take(packet); + edcaf->getAckHandler()->dropFrame(header); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + } +} + void Hcf::processLowerFrame(Packet *packet, const Ptr& header) { Enter_Method("processLowerFrame(%s)", packet->getName()); @@ -233,13 +402,22 @@ void Hcf::channelGranted(IChannelAccess *channelAccess) if (edcaf) { AccessCategory ac = edcaf->getAccessCategory(); EV_DETAIL << "Channel access granted to the " << printAccessCategory(ac) << " queue" << std::endl; - edcaf->getTxopProcedure()->startTxop(ac); auto internallyCollidedEdcafs = edca->getInternallyCollidedEdcafs(); if (internallyCollidedEdcafs.size() > 0) { EV_INFO << "Internal collision happened with the following queues:" << std::endl; - handleInternalCollision(internallyCollidedEdcafs); - emit(edcaCollisionDetectedSignal, (unsigned long)internallyCollidedEdcafs.size()); + // IEEE Std 802.11-2024, 10.23.2.4: an EDCAF with no eligible + // frame has no collision recovery action to perform. + auto handledCollisions = handleInternalCollision(internallyCollidedEdcafs); + if (handledCollisions > 0) + emit(edcaCollisionDetectedSignal, (unsigned long)handledCollisions); } + if (!hasFrameToTransmit(ac)) { + EV_DETAIL << "Releasing channel because no eligible frame is available.\n"; + edcaf->releaseChannel(this); + mac->sendDownPendingRadioConfigMsg(); + return; + } + edcaf->getTxopProcedure()->startTxop(ac); startFrameSequence(ac); } else @@ -259,12 +437,18 @@ void Hcf::startFrameSequence(AccessCategory ac) emit(IFrameSequenceHandler::frameSequenceStartedSignal, frameSequenceHandler->getContext()); } -void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) +int Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) { + int handledCollisions = 0; for (auto edcaf : internallyCollidedEdcafs) { AccessCategory ac = edcaf->getAccessCategory(); auto dataRecoveryProcedure = edcaf->getRecoveryProcedure(); Packet *internallyCollidedFrame = edcaf->getInProgressFrames()->getFrameToTransmit(); + if (internallyCollidedFrame == nullptr) { + EV_DETAIL << "Ignoring internal collision because no eligible frame is available for " << printAccessCategory(ac) << ".\n"; + continue; + } + handledCollisions++; auto internallyCollidedHeader = internallyCollidedFrame->peekAtFront(); EV_INFO << printAccessCategory(ac) << " (" << internallyCollidedFrame->getName() << ")" << endl; bool retryLimitReached = false; @@ -286,6 +470,8 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(internallyCollidedFrame, mgmtHeader); else ; // TODO + NonQoSDataFrame edcaf->getInProgressFrames()->dropFrame(internallyCollidedFrame); + processDroppedBlockAckSetupFrame(internallyCollidedFrame); + processDroppedBlockAckTeardownFrame(internallyCollidedFrame); edcaf->getAckHandler()->dropFrame(internallyCollidedHeader); PacketDropDetails details; details.setReason(RETRY_LIMIT_REACHED); @@ -303,6 +489,7 @@ void Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) else edcaf->requestChannel(this); } + return handledCollisions; } /* @@ -317,12 +504,11 @@ void Hcf::frameSequenceFinished() emit(IFrameSequenceHandler::frameSequenceFinishedSignal, frameSequenceHandler->getContext()); auto edcaf = edca->getChannelOwner(); if (edcaf) { - bool startContention = hasFrameToTransmit(); // TODO outstanding frame edcaf->releaseChannel(this); mac->sendDownPendingRadioConfigMsg(); // TODO review edcaf->getTxopProcedure()->endTxop(); - if (startContention) - edcaf->requestChannel(this); + // Agreement transitions may have made frames in any AC eligible. + requestEligibleChannelAccess(); } else if (hcca->isOwning()) { hcca->releaseChannel(this); @@ -378,9 +564,24 @@ void Hcf::recipientProcessReceivedManagementFrame(const Ptr(header)) { - originatorBlockAckAgreementHandler->processReceivedAddbaResp(addbaResp, originatorBlockAckAgreementPolicy, this); - auto agreement = originatorBlockAckAgreementHandler->getAgreement(addbaResp->getTransmitterAddress(), addbaResp->getTid()); - emit(blockAckAgreementAddedSignal, agreement); + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(addbaResp->getTransmitterAddress(), addbaResp->getTid()); + auto response = originatorBlockAckAgreementHandler->processReceivedAddbaResp(addbaResp, originatorBlockAckAgreementPolicy, this); + if (wasPending && !originatorBlockAckAgreementHandler->isAddbaResponsePending(addbaResp->getTransmitterAddress(), addbaResp->getTid())) + rebuildPendingFrameEligibility(); + if (response.teardownDelba != nullptr && (response.terminatedAgreement == nullptr || response.teardownTransactionId == 0)) + throw cRuntimeError("Invalid locally vetoed ADDBA response outcome"); + if (response.establishedAgreement != nullptr) + emit(blockAckAgreementAddedSignal, response.establishedAgreement); + if (response.terminatedAgreement != nullptr) { + emit(blockAckAgreementAddedSignal, response.terminatedAgreement.get()); + emit(blockAckAgreementDeletedSignal, response.terminatedAgreement.get()); + } + if (response.teardownDelba != nullptr) { + auto delbaPacket = new Packet("Delba", response.teardownDelba); + delbaPacket->addTag()->setTransactionId(response.teardownTransactionId); + processMgmtFrame(delbaPacket, response.teardownDelba); + } + resumeEligibleChannelAccess(); } else if (auto delba = dynamicPtrCast(header)) { if (delba->getInitiator()) { @@ -389,9 +590,13 @@ void Hcf::recipientProcessReceivedManagementFrame(const PtrprocessReceivedDelba(delba, recipientBlockAckAgreementPolicy); } else { - auto agreement = originatorBlockAckAgreementHandler->getAgreement(delba->getReceiverAddress(), delba->getTid()); - emit(blockAckAgreementDeletedSignal, agreement); - originatorBlockAckAgreementHandler->processReceivedDelba(delba, originatorBlockAckAgreementPolicy); + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid()); + auto agreement = originatorBlockAckAgreementHandler->processReceivedDelba(delba, originatorBlockAckAgreementPolicy, this); + if (wasPending && !originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid())) + rebuildPendingFrameEligibility(); + if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + resumeEligibleChannelAccess(); } } else @@ -432,24 +637,35 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) } else throw cRuntimeError("Unknown frame"); // TODO QoSDataFrame, NonQoSDataFrame - if (retryLimitReached) { - if (auto dataHeader = dynamicPtrCast(protectedHeader)) - edcaf->getRecoveryProcedure()->retryLimitReached(packet, dataHeader); - else if (auto mgmtHeader = dynamicPtrCast(protectedHeader)) - edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(packet, mgmtHeader); - else ; // TODO nonqos data + auto addbaRequest = dynamicPtrCast(protectedHeader); + bool staleAddbaRequest = addbaRequest != nullptr && originatorBlockAckAgreementHandler != nullptr && !originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaRequest); + if (retryLimitReached || staleAddbaRequest) { + if (retryLimitReached) { + if (auto dataHeader = dynamicPtrCast(protectedHeader)) + edcaf->getRecoveryProcedure()->retryLimitReached(packet, dataHeader); + else if (auto mgmtHeader = dynamicPtrCast(protectedHeader)) + edca->getMgmtAndNonQoSRecoveryProcedure()->rtsFrameRetryLimitReached(packet, mgmtHeader); + else ; // TODO nonqos data + } + else + edca->getMgmtAndNonQoSRecoveryProcedure()->discardRtsFrame(addbaRequest); edcaf->getInProgressFrames()->dropFrame(packet); + processDroppedBlockAckSetupFrame(packet); + processDroppedBlockAckTeardownFrame(packet); edcaf->getAckHandler()->dropFrame(protectedHeader); - EV_INFO << "Dropping RTS/CTS protected frame " << packet->getName() << ", because retry limit is reached.\n"; + EV_INFO << "Dropping RTS/CTS protected frame " << packet->getName() << (retryLimitReached ? ", because retry limit is reached.\n" : ", because its ADDBA transaction is no longer pending.\n"); PacketDropDetails details; - details.setReason(RETRY_LIMIT_REACHED); - details.setLimit(-1); // TODO + details.setReason(retryLimitReached ? RETRY_LIMIT_REACHED : OTHER_PACKET_DROP); + if (retryLimitReached) + details.setLimit(-1); // TODO emit(packetDroppedSignal, packet, &details); - emit(linkBrokenSignal, packet); - if (dynamicPtrCast(protectedHeader)) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + if (retryLimitReached) { + emit(linkBrokenSignal, packet); + if (dynamicPtrCast(protectedHeader)) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } } } } @@ -474,7 +690,7 @@ void Hcf::originatorProcessTransmittedFrame(Packet *packet) else if (auto dataHeader = dynamicPtrCast(transmittedHeader)) originatorProcessTransmittedDataFrame(packet, dataHeader, ac); else if (auto mgmtHeader = dynamicPtrCast(transmittedHeader)) - originatorProcessTransmittedManagementFrame(mgmtHeader, ac); + originatorProcessTransmittedManagementFrame(packet, mgmtHeader, ac); else // TODO Ieee80211ControlFrame originatorProcessTransmittedControlFrame(transmittedHeader, ac); } @@ -488,26 +704,30 @@ void Hcf::originatorProcessTransmittedDataFrame(Packet *packet, const PtrgetEdcaf(ac); edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(dataHeader); - if (originatorBlockAckAgreementHandler) - originatorBlockAckAgreementHandler->processTransmittedDataFrame(packet, dataHeader, originatorBlockAckAgreementPolicy, this); if (dataHeader->getAckPolicy() == NO_ACK) edcaf->getInProgressFrames()->dropFrame(packet); } -void Hcf::originatorProcessTransmittedManagementFrame(const Ptr& mgmtHeader, AccessCategory ac) +void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr& mgmtHeader, AccessCategory ac) { auto edcaf = edca->getEdcaf(ac); if (originatorAckPolicy->isAckNeeded(mgmtHeader)) edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(mgmtHeader); if (auto addbaReq = dynamicPtrCast(mgmtHeader)) { if (originatorBlockAckAgreementHandler) - originatorBlockAckAgreementHandler->processTransmittedAddbaReq(addbaReq); + originatorBlockAckAgreementHandler->processTransmittedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); } else if (auto addbaResp = dynamicPtrCast(mgmtHeader)) recipientBlockAckAgreementHandler->processTransmittedAddbaResp(addbaResp, this); else if (auto delba = dynamicPtrCast(mgmtHeader)) { - if (delba->getInitiator()) - originatorBlockAckAgreementHandler->processTransmittedDelba(delba); + if (delba->getInitiator()) { + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getReceiverAddress(), delba->getTid()); + auto agreement = originatorBlockAckAgreementHandler->processTransmittedDelba(packet, this); + if (wasPending) + rebuildPendingFrameEligibility(); + if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + } else recipientBlockAckAgreementHandler->processTransmittedDelba(delba); } @@ -559,23 +779,34 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) } else throw cRuntimeError("Unknown frame"); // TODO qos, nonqos - if (retryLimitReached) { - if (auto dataHeader = dynamicPtrCast(failedHeader)) - edcaf->getRecoveryProcedure()->retryLimitReached(failedPacket, dataHeader); - else if (auto mgmtHeader = dynamicPtrCast(failedHeader)) - edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(failedPacket, mgmtHeader); + auto addbaRequest = dynamicPtrCast(failedHeader); + bool staleAddbaRequest = addbaRequest != nullptr && originatorBlockAckAgreementHandler != nullptr && !originatorBlockAckAgreementHandler->isAddbaRequestPending(failedPacket, addbaRequest); + if (retryLimitReached || staleAddbaRequest) { + if (retryLimitReached) { + if (auto dataHeader = dynamicPtrCast(failedHeader)) + edcaf->getRecoveryProcedure()->retryLimitReached(failedPacket, dataHeader); + else if (auto mgmtHeader = dynamicPtrCast(failedHeader)) + edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(failedPacket, mgmtHeader); + } + else + edca->getMgmtAndNonQoSRecoveryProcedure()->discardFrame(failedPacket, addbaRequest); edcaf->getInProgressFrames()->dropFrame(failedPacket); + processDroppedBlockAckSetupFrame(failedPacket); + processDroppedBlockAckTeardownFrame(failedPacket); edcaf->getAckHandler()->dropFrame(dynamicPtrCast(failedHeader)); - EV_INFO << "Dropping frame " << failedPacket->getName() << ", because retry limit is reached.\n"; + EV_INFO << "Dropping frame " << failedPacket->getName() << (retryLimitReached ? ", because retry limit is reached.\n" : ", because its ADDBA transaction is no longer pending.\n"); PacketDropDetails details; - details.setReason(RETRY_LIMIT_REACHED); - details.setLimit(-1); // TODO + details.setReason(retryLimitReached ? RETRY_LIMIT_REACHED : OTHER_PACKET_DROP); + if (retryLimitReached) + details.setLimit(-1); // TODO emit(packetDroppedSignal, failedPacket, &details); - emit(linkBrokenSignal, failedPacket); - if (dynamicPtrCast(failedHeader)) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); + if (retryLimitReached) { + emit(linkBrokenSignal, failedPacket); + if (dynamicPtrCast(failedHeader)) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); + } } } else { @@ -641,6 +872,10 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); + if (auto delba = dynamicPtrCast(lastTransmittedHeader)) { + if (delba->getInitiator() && originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this)) + rebuildPendingFrameEligibility(); + } edcaf->getInProgressFrames()->dropFrame(lastTransmittedPacket); edcaf->getAckHandler()->dropFrame(lastTransmittedDataOrMgmtHeader); if (dynamicPtrCast(lastTransmittedHeader)) { @@ -648,6 +883,16 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader)) { + if (originatorBlockAckAgreementHandler) { + bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); + auto obsoleteTeardownTransactionId = originatorBlockAckAgreementHandler->processAcknowledgedDataFrame(lastTransmittedPacket, dataHeader, originatorBlockAckAgreementPolicy, this); + if (obsoleteTeardownTransactionId != 0) + cancelAddbaTransaction(obsoleteTeardownTransactionId, nullptr); + if (!wasPending && originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid())) + rebuildPendingFrameEligibility(); + } + } } else if (auto blockAck = dynamicPtrCast(header)) { EV_INFO << "BasicBlockAck has arrived" << std::endl; @@ -682,7 +927,7 @@ bool Hcf::hasFrameToTransmit(AccessCategory ac) { auto edcaf = edca->getEdcaf(ac); if (edcaf) - return !edcaf->getPendingQueue()->isEmpty() || edcaf->getInProgressFrames()->hasInProgressFrames(); + return numEligiblePendingFrames[ac] != 0 || edcaf->getInProgressFrames()->hasEligibleInProgressFrames(); else throw cRuntimeError("Hcca is unimplemented"); } @@ -691,11 +936,26 @@ bool Hcf::hasFrameToTransmit() { auto edcaf = edca->getChannelOwner(); if (edcaf) - return !edcaf->getPendingQueue()->isEmpty() || edcaf->getInProgressFrames()->hasInProgressFrames(); + return numEligiblePendingFrames[edcaf->getAccessCategory()] != 0 || edcaf->getInProgressFrames()->hasEligibleInProgressFrames(); else throw cRuntimeError("Hcca is unimplemented"); } +void Hcf::requestEligibleChannelAccess() +{ + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto accessCategory = AccessCategory(ac); + if (hasFrameToTransmit(accessCategory)) + edca->requestChannelAccess(accessCategory, this); + } +} + +void Hcf::resumeEligibleChannelAccess() +{ + if (edca->getChannelOwner() == nullptr && !frameSequenceHandler->isSequenceRunning()) + requestEligibleChannelAccess(); +} + void Hcf::sendUp(const std::vector& completeFrames) { for (auto frame : completeFrames) @@ -822,8 +1082,11 @@ void Hcf::corruptedFrameReceived() Hcf::~Hcf() { + // Callback pointers are stored by child queues, which are destroyed with + // this compound module. Traversing edca here may reach deleted children. cancelAndDelete(startRxTimer); cancelAndDelete(inactivityTimer); + cancelAndDelete(addbaResponseTimer); delete recipientAckProcedure; delete ctsProcedure; delete rtsProcedure; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index c05a7a071af..7bf6631e302 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -8,6 +8,9 @@ #ifndef __INET_HCF_H #define __INET_HCF_H +#include +#include + #include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h" #include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" @@ -59,6 +62,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: cMessage *startRxTimer = nullptr; cMessage *inactivityTimer = nullptr; + cMessage *addbaResponseTimer = nullptr; // Transmission and Reception IRx *rx = nullptr; @@ -97,6 +101,13 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: // Queues InProgressFrames *hccaInProgressFrame = nullptr; + struct PendingFrameEligibility { + AccessCategory accessCategory; + bool eligible; + }; + std::map pendingFrameEligibility; + std::array numEligiblePendingFrames = {}; + // Frame sequence handler IFrameSequenceHandler *frameSequenceHandler = nullptr; @@ -111,12 +122,20 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void refreshDisplay() const override; void startFrameSequence(AccessCategory ac); - void handleInternalCollision(std::vector internallyCollidedEdcafs); + int handleInternalCollision(std::vector internallyCollidedEdcafs); void sendUp(const std::vector& completeFrames); FrameSequenceContext *buildContext(AccessCategory ac); virtual bool hasFrameToTransmit(); virtual bool hasFrameToTransmit(AccessCategory ac); + virtual void requestEligibleChannelAccess(); + virtual void resumeEligibleChannelAccess(); + virtual bool processDroppedBlockAckSetupFrame(Packet *packet); + virtual bool processDroppedBlockAckTeardownFrame(Packet *packet); + virtual bool isPacketReferencedByCurrentFrameSequence(const Packet *packet) const; + virtual void trackPendingFrame(Packet *packet, AccessCategory accessCategory); + virtual void untrackPendingFrame(const Packet *packet); + virtual void rebuildPendingFrameEligibility(); virtual bool isReceptionInProgress(); // Recipient @@ -126,7 +145,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header); // Originator - virtual void originatorProcessTransmittedManagementFrame(const Ptr& mgmtHeader, AccessCategory ac); + virtual void originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr& mgmtHeader, AccessCategory ac); virtual void originatorProcessTransmittedControlFrame(const Ptr& controlHeader, AccessCategory ac); virtual void originatorProcessTransmittedDataFrame(Packet *packet, const Ptr& dataHeader, AccessCategory ac); virtual void originatorProcessReceivedManagementFrame(const Ptr& header, const Ptr& lastTransmittedHeader, AccessCategory ac); @@ -161,6 +180,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); // IProcedureCallback virtual void scheduleInactivityTimer(simtime_t timeout) override; + virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override; std::string getFrameSequenceInfo() const; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc index fc81d4d863c..2ae6c036527 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/HcfFs.cc @@ -13,6 +13,14 @@ namespace inet { namespace ieee80211 { +static bool hasMoreTxOpsForFrame(RepeatingFs *frameSequence, FrameSequenceContext *context, Packet *nextFrameToTransmit) +{ + if (nextFrameToTransmit == nullptr) + return false; + const auto& nextHeader = nextFrameToTransmit->peekAtFront(); + return frameSequence->getCount() == 0 || (!nextHeader->getReceiverAddress().isMulticast() && context->getQoSContext()->txopProcedure->getRemaining() > 0); +} + HcfFs::HcfFs() : // G.3 EDCA and HCCA sequences // hcf-sequence = @@ -58,20 +66,17 @@ bool HcfFs::isSelfCtsNeeded(OptionalFs *frameSequence, FrameSequenceContext *con bool HcfFs::hasMoreTxOps(RepeatingFs *frameSequence, FrameSequenceContext *context) { - bool hasFrameToTransmit = context->getInProgressFrames()->hasInProgressFrames(); - if (hasFrameToTransmit) { - auto nextFrameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); - const auto& nextHeader = nextFrameToTransmit->peekAtFront(); - return frameSequence->getCount() == 0 || (!nextHeader->getReceiverAddress().isMulticast() && context->getQoSContext()->txopProcedure->getRemaining() > 0); - } - return false; + // This predicate is the TXOP continuation boundary, so it may materialize + // an eligible pending frame. Availability queries elsewhere remain pure. + auto nextFrameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); + return hasMoreTxOpsForFrame(frameSequence, context, nextFrameToTransmit); } bool HcfFs::hasMoreTxOpsAndMulticast(RepeatingFs *frameSequence, FrameSequenceContext *context) { - return hasMoreTxOps(frameSequence, context) && context->getInProgressFrames()->getFrameToTransmit()->peekAtFront()->getReceiverAddress().isMulticast(); + auto nextFrameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); + return hasMoreTxOpsForFrame(frameSequence, context, nextFrameToTransmit) && nextFrameToTransmit->peekAtFront()->getReceiverAddress().isMulticast(); } } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc index 4074ff0004c..f5d01a0b70a 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.cc @@ -127,23 +127,34 @@ void NonQosRecoveryProcedure::ackFrameReceived(Packet *packet, const Ptr& header) +{ + auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()); + shortRetryCounter.erase(id); + longRetryCounter.erase(id); +} + +void NonQosRecoveryProcedure::discardRtsFrame(const Ptr& protectedHeader) +{ + auto id = SequenceControlField(protectedHeader->getSequenceNumber().get(), protectedHeader->getFragmentNumber()); + shortRetryCounter.erase(id); + longRetryCounter.erase(id); +} + // After dropping a frame because it reached its retry limit we need to clear the // retry counters // void NonQosRecoveryProcedure::retryLimitReached(Packet *packet, const Ptr& header) { EV_WARN << "Retry limit reached for " << *packet << ".\n"; - auto id = SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber()); - if (packet->getByteLength() >= rtsThreshold) { - auto it = longRetryCounter.find(id); - if (it != longRetryCounter.end()) - longRetryCounter.erase(it); - } - else { - auto it = shortRetryCounter.find(id); - if (it != shortRetryCounter.end()) - shortRetryCounter.erase(it); - } + discardFrame(packet, header); + emit(retryLimitReachedSignal, packet); +} + +void NonQosRecoveryProcedure::rtsFrameRetryLimitReached(Packet *packet, const Ptr& protectedHeader) +{ + EV_WARN << "RTS retry limit reached for " << *packet << ".\n"; + discardRtsFrame(protectedHeader); emit(retryLimitReachedSignal, packet); } @@ -247,4 +258,3 @@ bool NonQosRecoveryProcedure::isMulticastFrame(const Ptr& failedHeader); virtual bool isRtsFrameRetryLimitReached(Packet *packet, const Ptr& protectedHeader); + virtual void discardFrame(Packet *packet, const Ptr& header); + virtual void discardRtsFrame(const Ptr& protectedHeader); virtual void retryLimitReached(Packet *packet, const Ptr& header); + virtual void rtsFrameRetryLimitReached(Packet *packet, const Ptr& protectedHeader); virtual int getLongRetryLimit() { return longRetryLimit; } virtual int getShortRetryLimit() { return shortRetryLimit; } @@ -74,4 +77,3 @@ class INET_API NonQosRecoveryProcedure : public SimpleModule, public IRecoveryPr } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.cc index 12d6c5cd21e..4bbfc0ea6d3 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.cc @@ -7,6 +7,7 @@ #include "inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/NonQoSSequenceNumberAssignment.h" @@ -32,21 +33,37 @@ std::vector *OriginatorMacDataService::fragmentIfNeeded(Packet *frame) auto fragmentSizes = fragmentationPolicy->computeFragmentSizes(frame); if (fragmentSizes.size() != 0) { emit(packetFragmentedSignal, frame); + auto transactionTag = frame->findTag(); + bool hasTransactionTag = transactionTag != nullptr; + auto transactionId = hasTransactionTag ? transactionTag->getTransactionId() : 0; auto fragmentFrames = fragmentation->fragmentFrame(frame, fragmentSizes); + if (hasTransactionTag) + for (auto fragment : *fragmentFrames) + fragment->addTagIfAbsent()->setTransactionId(transactionId); return fragmentFrames; } return nullptr; } +bool OriginatorMacDataService::isFrameEligible(const Packet *packet) const +{ + return !frameEligibilityFunction || frameEligibilityFunction(packet); +} + +bool OriginatorMacDataService::hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const +{ + return pendingQueue->findPacket([this](const Packet *packet) { return isFrameEligible(packet); }) != nullptr; +} + std::vector *OriginatorMacDataService::extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) { Enter_Method("extractFramesToTransmit"); - if (pendingQueue->isEmpty()) + auto packet = pendingQueue->dequeuePacket([this](const Packet *packet) { return isFrameEligible(packet); }); + if (packet == nullptr) return nullptr; else { // if (msduRateLimiting) // txRateLimitingIfNeeded(); - Packet *packet = pendingQueue->dequeuePacket(); take(packet); if (sequenceNumberAssignment) { auto frame = packet->removeAtFront(); @@ -76,4 +93,3 @@ OriginatorMacDataService::~OriginatorMacDataService() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h index 2a75735b598..7bf62fa66ad 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorMacDataService.h @@ -30,6 +30,7 @@ class INET_API OriginatorMacDataService : public IOriginatorMacDataService, publ // MsduIntegrityAndProtection *msduIntegrityAndProtection = nullptr; IFragmentationPolicy *fragmentationPolicy = nullptr; IFragmentation *fragmentation = nullptr; + FrameEligibilityFunction frameEligibilityFunction; // MpduEncryptionAndIntegrity *mpduEncryptionAndIntegrity = nullptr; // MpduHeaderPlusFcs *mpduHeaderPlusFcs = nullptr; @@ -42,6 +43,9 @@ class INET_API OriginatorMacDataService : public IOriginatorMacDataService, publ public: virtual ~OriginatorMacDataService(); + virtual void setFrameEligibilityFunction(const FrameEligibilityFunction& frameEligibilityFunction) override { this->frameEligibilityFunction = frameEligibilityFunction; } + virtual bool isFrameEligible(const Packet *packet) const override; + virtual bool hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const override; virtual std::vector *extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) override; }; @@ -49,4 +53,3 @@ class INET_API OriginatorMacDataService : public IOriginatorMacDataService, publ } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc index f374452e933..0ccd4415e61 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.cc @@ -8,9 +8,12 @@ #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include +#include +#include #include "inet/linklayer/ieee80211/mac/aggregation/MpduAggregation.h" #include "inet/linklayer/ieee80211/mac/aggregation/MsduAggregation.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" @@ -32,17 +35,53 @@ void OriginatorQosMacDataService::initialize() fragmentation = new Fragmentation(); } -Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue) +Packet *OriginatorQosMacDataService::aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *candidate) { - auto subframes = aMsduAggregationPolicy->computeAggregateFrames(pendingQueue); + auto predicate = [this](const Packet *packet) { return isFrameEligible(packet); }; + std::unique_ptr> subframes(aMsduAggregationPolicy->computeAggregateFrames(pendingQueue, candidate, predicate)); if (subframes) { + if (subframes->size() < 2 || subframes->front() != candidate) + throw cRuntimeError("A-MSDU policy must return at least two frames with the selected candidate first"); + std::unordered_set uniqueFrames; for (auto subframe : *subframes) { - pendingQueue->removePacket(subframe); - take(subframe); + auto identity = [subframe](const Packet *packet) { return packet == subframe; }; + if (subframe == nullptr || !uniqueFrames.insert(subframe).second || pendingQueue->findPacket(identity) != subframe || !isFrameEligible(subframe)) + throw cRuntimeError("A-MSDU policy returned a frame that is unavailable, ineligible, or duplicated"); } - auto aggregatedFrame = aMsduAggregation->aggregateFrames(subframes); + struct AggregateFrameState { Tid tid; MacAddress receiver; MacAddress transmitter; MacAddress address3; MacAddress address4; int type; bool toDS; bool fromDS; b dataLength; b headerLength; b trailerLength; }; + std::vector states; + for (auto subframe : *subframes) { + auto dataHeader = dynamicPtrCast(subframe->peekAtFront()); + auto dataTrailer = subframe->peekAtBack(B(4)); + if (dataHeader == nullptr || dataTrailer == nullptr) + throw cRuntimeError("A-MSDU policy selected a frame without a valid data header/trailer"); + states.push_back({static_cast(dataHeader->getTid()), dataHeader->getReceiverAddress(), dataHeader->getTransmitterAddress(), dataHeader->getAddress3(), dataHeader->getAddress4(), dataHeader->getType(), dataHeader->getToDS(), dataHeader->getFromDS(), subframe->getDataLength(), dataHeader->getChunkLength(), dataTrailer->getChunkLength()}); + } + std::vector> extractedSubframes; + extractedSubframes.reserve(subframes->size()); + for (auto subframe : *subframes) { + auto dequeuedSubframe = pendingQueue->dequeuePacket([subframe](const Packet *packet) { return packet == subframe; }); + if (dequeuedSubframe != subframe) { + bool alreadyExtracted = std::any_of(extractedSubframes.begin(), extractedSubframes.end(), [dequeuedSubframe](const auto& extractedSubframe) { return extractedSubframe.get() == dequeuedSubframe; }); + if (dequeuedSubframe != nullptr && !alreadyExtracted) { + take(dequeuedSubframe); + extractedSubframes.emplace_back(dequeuedSubframe); + } + throw cRuntimeError("A-MSDU policy-selected subframe is no longer available in scheduling order"); + } + take(dequeuedSubframe); + extractedSubframes.emplace_back(dequeuedSubframe); + } + for (size_t i = 0; i < subframes->size(); i++) { + auto dataHeader = dynamicPtrCast((*subframes)[i]->peekAtFront()); + auto dataTrailer = (*subframes)[i]->peekAtBack(B(4)); + if (dataHeader == nullptr || dataTrailer == nullptr || dataHeader->getTid() != states[i].tid || dataHeader->getReceiverAddress() != states[i].receiver || dataHeader->getTransmitterAddress() != states[i].transmitter || dataHeader->getAddress3() != states[i].address3 || dataHeader->getAddress4() != states[i].address4 || dataHeader->getType() != states[i].type || dataHeader->getToDS() != states[i].toDS || dataHeader->getFromDS() != states[i].fromDS || (*subframes)[i]->getDataLength() != states[i].dataLength || dataHeader->getChunkLength() != states[i].headerLength || dataTrailer->getChunkLength() != states[i].trailerLength) + throw cRuntimeError("A-MSDU provider changed aggregation-critical frame fields during extraction"); + } + for (auto& subframe : extractedSubframes) + subframe.release(); + auto aggregatedFrame = aMsduAggregation->aggregateFrames(subframes.get()); emit(packetAggregatedSignal, aggregatedFrame); - delete subframes; return aggregatedFrame; } return nullptr; @@ -72,27 +111,49 @@ std::vector *OriginatorQosMacDataService::fragmentIfNeeded(Packet *fra auto fragmentSizes = fragmentationPolicy->computeFragmentSizes(frame); if (fragmentSizes.size() != 0) { emit(packetFragmentedSignal, frame); + auto transactionTag = frame->findTag(); + bool hasTransactionTag = transactionTag != nullptr; + auto transactionId = hasTransactionTag ? transactionTag->getTransactionId() : 0; auto fragmentFrames = fragmentation->fragmentFrame(frame, fragmentSizes); + if (hasTransactionTag) + for (auto fragment : *fragmentFrames) + fragment->addTagIfAbsent()->setTransactionId(transactionId); return fragmentFrames; } return nullptr; } +bool OriginatorQosMacDataService::isFrameEligible(const Packet *packet) const +{ + return !frameEligibilityFunction || frameEligibilityFunction(packet); +} + +bool OriginatorQosMacDataService::hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const +{ + return pendingQueue->findPacket([this](const Packet *packet) { return isFrameEligible(packet); }) != nullptr; +} + std::vector *OriginatorQosMacDataService::extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) { Enter_Method("extractFramesToTransmit"); - if (pendingQueue->isEmpty()) + auto predicate = [this](const Packet *packet) { return isFrameEligible(packet); }; + auto candidate = pendingQueue->findPacket(predicate); + if (candidate == nullptr) return nullptr; else { // if (msduRateLimiting) // txRateLimitingIfNeeded(); Packet *packet = nullptr; + // Scheduling selects the anchor; the policy may select additional + // eligible members, which are all extracted through the provider. if (aMsduAggregationPolicy) - packet = aMsduAggregateIfNeeded(pendingQueue); + packet = aMsduAggregateIfNeeded(pendingQueue, candidate); if (!packet) { - packet = pendingQueue->dequeuePacket(); + packet = pendingQueue->dequeuePacket(predicate); + ASSERT(packet == candidate); take(packet); } + ASSERT(packet != nullptr); // PS Defer Queueing if (sequenceNumberAssignment) { auto header = packet->removeAtFront(); @@ -129,4 +190,3 @@ OriginatorQosMacDataService::~OriginatorQosMacDataService() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h index 0d162c213be..8ba2b581f0b 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h @@ -38,18 +38,22 @@ class INET_API OriginatorQosMacDataService : public IOriginatorMacDataService, p // PsDeferQueueing *psDeferQueueing = nullptr; IMpduAggregationPolicy *aMpduAggregationPolicy = nullptr; IMpduAggregation *aMpduAggregation = nullptr; + FrameEligibilityFunction frameEligibilityFunction; protected: virtual void initialize() override; virtual void assignSequenceNumber(const Ptr& header); virtual std::vector *fragmentIfNeeded(Packet *frame); - virtual Packet *aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue); + virtual Packet *aMsduAggregateIfNeeded(queueing::IPacketQueue *pendingQueue, Packet *candidate); virtual Packet *aMpduAggregateIfNeeded(std::vector *fragments); public: virtual ~OriginatorQosMacDataService(); + virtual void setFrameEligibilityFunction(const FrameEligibilityFunction& frameEligibilityFunction) override { this->frameEligibilityFunction = frameEligibilityFunction; } + virtual bool isFrameEligible(const Packet *packet) const override; + virtual bool hasEligibleFrame(queueing::IPacketQueue *pendingQueue) const override; virtual std::vector *extractFramesToTransmit(queueing::IPacketQueue *pendingQueue) override; }; @@ -57,4 +61,3 @@ class INET_API OriginatorQosMacDataService : public IOriginatorMacDataService, p } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc index c1bb688f218..0d842aebdea 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.cc @@ -49,7 +49,7 @@ void InProgressFrames::forEachChild(cVisitor *v) bool InProgressFrames::hasEligibleFrameToTransmit() { for (auto frame : inProgressFrames) { - if (ackHandler->isEligibleToTransmit(frame->peekAtFront())) + if (dataService->isFrameEligible(frame) && ackHandler->isEligibleToTransmit(frame->peekAtFront())) return true; } return false; @@ -82,7 +82,7 @@ Packet *InProgressFrames::getFrameToTransmit() { ensureHasFrameToTransmit(); for (auto frame : inProgressFrames) { - if (ackHandler->isEligibleToTransmit(frame->peekAtFront())) + if (dataService->isFrameEligible(frame) && ackHandler->isEligibleToTransmit(frame->peekAtFront())) return frame; } return nullptr; @@ -95,7 +95,7 @@ Packet *InProgressFrames::getPendingFrameFor(Packet *frame) return frameToTransmit; else { for (auto frame : inProgressFrames) { - if (ackHandler->isEligibleToTransmit(frame->peekAtFront()) && frameToTransmit != frame) + if (dataService->isFrameEligible(frame) && ackHandler->isEligibleToTransmit(frame->peekAtFront()) && frameToTransmit != frame) return frame; } auto frames = dataService->extractFramesToTransmit(pendingQueue); @@ -120,12 +120,28 @@ Packet *InProgressFrames::getPendingFrameFor(Packet *frame) void InProgressFrames::dropFrame(Packet *packet) { + auto it = std::find(inProgressFrames.begin(), inProgressFrames.end(), packet); + if (it == inProgressFrames.end()) + return; EV_DEBUG << "Dropping frame " << packet->getName() << ".\n"; - inProgressFrames.erase(std::remove(inProgressFrames.begin(), inProgressFrames.end(), packet), inProgressFrames.end()); + inProgressFrames.erase(it); droppedFrames.push_back(packet); emit(packetDequeuedSignal, packet); } +Packet *InProgressFrames::extractFrame(Packet *packet) +{ + Enter_Method("extractFrame"); + auto it = std::find(inProgressFrames.begin(), inProgressFrames.end(), packet); + if (it == inProgressFrames.end()) + return nullptr; + EV_DEBUG << "Extracting frame " << packet->getName() << ".\n"; + inProgressFrames.erase(it); + emit(packetDequeuedSignal, packet); + drop(packet); + return packet; +} + void InProgressFrames::dropFrames(std::set>> seqAndFragNums) { for (auto it = inProgressFrames.begin(); it != inProgressFrames.end();) { @@ -175,4 +191,3 @@ InProgressFrames::~InProgressFrames() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h index 91c4a10cca8..98063fd07ce 100644 --- a/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h +++ b/src/inet/linklayer/ieee80211/mac/queue/InProgressFrames.h @@ -47,8 +47,10 @@ class INET_API InProgressFrames : public SimpleModule virtual Packet *getFrameToTransmit(); virtual Packet *getPendingFrameFor(Packet *frame); virtual void dropFrame(Packet *packet); + virtual Packet *extractFrame(Packet *packet); virtual void dropFrames(std::set>> seqAndFragNums); + virtual bool hasEligibleInProgressFrames() { return hasEligibleFrameToTransmit(); } virtual bool hasInProgressFrames() { ensureHasFrameToTransmit(); return hasEligibleFrameToTransmit(); } virtual std::vector getOutstandingFrames(); @@ -59,4 +61,3 @@ class INET_API InProgressFrames : public SimpleModule } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg index 47632a7a5cd..cb343b94059 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg @@ -61,6 +61,7 @@ enum Ieee80211ReasonCode RC_NONASS_CLASS3 = 7; RC_DIASS_MS_LEAVING = 8; RC_NONAUTH_ASS_REQUEST = 9; + RC_END_BA = 37; RC_MESH_PEERING_CANCELLED = 52; RC_MESH_MAX_PEERS = 53; RC_MESH_CONFIGURATION_POLICY_VIOLATION = 54; diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 91a9667ac13..725e33108fc 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -11,6 +11,23 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/common/TimeTag_m.h" #include "inet/common/LabelsTag_m.h" #include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h" +#include "inet/linklayer/ieee80211/mac/aggregation/MsduAggregation.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" +#include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" +#include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" #include "inet/queueing/gate/PacketGate.h" #include "inet/queueing/gate/PeriodicGate.h" @@ -23,6 +40,100 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC %global: using namespace inet; using namespace inet::ieee80211; + +class TestOriginatorPolicy : public IOriginatorBlockAckAgreementPolicy +{ + public: + simtime_t addbaResponseTimeout = 1; + simtime_t addbaRetryBackoff = 2; + bool addbaAccepted = true; + bool delbaAccepted = true; + + virtual bool isAddbaReqNeeded(Packet *, const Ptr&) override { return true; } + virtual bool isAddbaReqAccepted(const Ptr&, OriginatorBlockAckAgreement *) override { return addbaAccepted; } + virtual bool isDelbaAccepted(const Ptr&) override { return delbaAccepted; } + virtual bool isMsduSupported() const override { return true; } + virtual simtime_t getAddbaResponseTimeout() const override { return addbaResponseTimeout; } + virtual simtime_t computeAddbaRetryBackoff() const override { return addbaRetryBackoff; } + virtual simtime_t getBlockAckTimeoutValue() const override { return 0; } + virtual bool isDelayedAckPolicySupported() const override { return false; } + virtual int getMaximumAllowedBufferSize() const override { return 64; } +}; + +class TestRecipientPolicy : public IRecipientBlockAckAgreementPolicy +{ + public: + bool accepted = true; + bool delbaAccepted = true; + simtime_t blockAckTimeoutValue = 0; + bool aMsduSupportedValue = true; + bool delayedBlockAckPolicySupportedValue = false; + int maximumAllowedBufferSize = 64; + + virtual bool isAddbaReqAccepted(const Ptr&) override { return accepted; } + virtual bool isDelbaAccepted(const Ptr&) override { return delbaAccepted; } + virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } + virtual bool aMsduSupported() const override { return aMsduSupportedValue; } + virtual bool delayedBlockAckPolicySupported() const override { return delayedBlockAckPolicySupportedValue; } + virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } +}; + +class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandlerCallback +{ + public: + Packet *managementPacket = nullptr; + std::vector managementPackets; + Ptr managementHeader; + simtime_t addbaDeadline = SIMTIME_MAX; + std::vector cancelledTransactionIds; + std::vector excludedPackets; + + virtual ~TestCallback() { for (auto packet : managementPackets) delete packet; } + void forgetManagementPacket(Packet *packet) { managementPackets.erase(std::remove(managementPackets.begin(), managementPackets.end(), packet), managementPackets.end()); } + virtual void transmitControlResponseFrame(Packet *, const Ptr&, Packet *, const Ptr&) override { throw cRuntimeError("Unexpected control response"); } + virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { + managementPacket = packet; + managementPackets.push_back(packet); + managementHeader = header; + } + virtual void scheduleInactivityTimer(simtime_t) override {} + virtual void scheduleAddbaResponseTimer(simtime_t deadline) override { addbaDeadline = deadline; } + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { + cancelledTransactionIds.push_back(transactionId); + excludedPackets.push_back(excludedPacket); + } +}; + +class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler +{ + public: + using OriginatorBlockAckAgreementHandler::buildAddbaRequest; + void addEstablishedAgreement(MacAddress receiverAddress, Tid tid) { + auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(0), 64, true, false, 1, 1); + agreement->setIsAddbaResponseReceived(true); + blockAckAgreements[std::make_pair(receiverAddress, tid)] = agreement; + } + void setNextDialogToken(uint8_t token) { nextDialogToken = token; } + void expireNow(MacAddress receiverAddress, Tid tid) { getAgreement(receiverAddress, tid)->setAddbaResponseDeadline(simTime()); } + void allowRetryNow(MacAddress receiverAddress, Tid tid) { addbaRetryDeadlines[std::make_pair(receiverAddress, tid)] = simTime(); } + void setPendingTeardownTransaction(MacAddress receiverAddress, Tid tid, uint64_t transactionId) { pendingTeardownTransactionIds[std::make_pair(receiverAddress, tid)] = transactionId; } + simtime_t getRetryDeadline(MacAddress receiverAddress, Tid tid) const { auto it = addbaRetryDeadlines.find(std::make_pair(receiverAddress, tid)); return it == addbaRetryDeadlines.end() ? SIMTIME_MAX : it->second; } +}; + +class TestTimeoutCancellationCallback : public TestCallback +{ + public: + TestOriginatorHandler *handler = nullptr; + std::vector> expiredAgreementIds; + bool allExpiredAgreementsGoneBeforeCallbacks = true; + + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { + for (const auto& agreementId : expiredAgreementIds) + allExpiredAgreementsGoneBeforeCallbacks &= handler->getAgreement(agreementId.first, agreementId.second) == nullptr; + TestCallback::cancelAddbaTransaction(transactionId, excludedPacket); + } +}; + class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue { public: @@ -92,6 +203,37 @@ class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue virtual void pushPacketEnd(Packet *, const cGate *) override { throw cRuntimeError("Unsupported"); } virtual void pushPacketProgress(Packet *, const cGate *, bps, b, b) override { throw cRuntimeError("Unsupported"); } }; + +class TestWrongDequeueQueue : public TestPacketQueue +{ + public: + Packet *wrongPacket = nullptr; + int numPredicateDequeues = 0; + + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override { + if (numPredicateDequeues++ == 0) + return TestPacketQueue::dequeuePacket(predicate); + packets.erase(std::find(packets.begin(), packets.end(), wrongPacket)); + notifyPacketRemoved(wrongPacket, PacketRemovalReason::DEQUEUED); + return wrongPacket; + } +}; + +class TestRepeatedDequeueQueue : public TestPacketQueue +{ + public: + Packet *repeatedPacket = nullptr; + int numPredicateDequeues = 0; + + virtual Packet *dequeuePacket(const PacketPredicate& predicate) override { + if (numPredicateDequeues++ == 0) { + repeatedPacket = TestPacketQueue::dequeuePacket(predicate); + return repeatedPacket; + } + return repeatedPacket; + } +}; + class TestCompoundPacketQueue : public queueing::CompoundPacketQueueBase { public: @@ -249,6 +391,261 @@ class TestPacketArrivalListener : public cListener lastArrivalGate = check_and_cast(value)->getArrivalGate(); } }; + +class TestSignalListener : public cListener +{ + public: + int numSignals = 0; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *) override { numSignals++; } +}; + +class TestPacketDropSignalListener : public cListener +{ + public: + int numSignals = 0; + int numOtherPacketDrops = 0; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *details) override { + numSignals++; + auto packetDropDetails = check_and_cast(details); + if (packetDropDetails->getReason() == OTHER_PACKET_DROP) + numOtherPacketDrops++; + } +}; + +class TestAckHandler : public IAckHandler +{ + public: + virtual bool isEligibleToTransmit(const Ptr&) override { return true; } + virtual bool isOutstandingFrame(const Ptr&) override { return false; } + virtual void frameGotInProgress(const Ptr&) override {} +}; + +class TestQosAckHandler : public QosAckHandler +{ + public: + bool hasMgmtEntry(const Ptr& header) const { + auto id = std::make_pair(header->getReceiverAddress(), SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())); + return mgmtAckStatuses.find(id) != mgmtAckStatuses.end(); + } +}; + +class TestInProgressFrames : public InProgressFrames +{ + public: + void configure(IOriginatorMacDataService *dataService, IAckHandler *ackHandler, queueing::IPacketQueue *pendingQueue = nullptr) { + this->dataService = dataService; + this->ackHandler = ackHandler; + this->pendingQueue = pendingQueue; + } + void addFrame(Packet *packet) { inProgressFrames.push_back(packet); } + void addOwnedFrame(Packet *packet) { take(packet); inProgressFrames.push_back(packet); } + bool hasEligibleFrame() { return hasEligibleFrameToTransmit(); } + int getNumDroppedFrames() const { return droppedFrames.size(); } + std::vector releaseFrames() { + std::vector frames = inProgressFrames; + frames.insert(frames.end(), droppedFrames.begin(), droppedFrames.end()); + for (auto frame : frames) + if (frame->getOwner() == this) + drop(frame); + inProgressFrames.clear(); + droppedFrames.clear(); + return frames; + } +}; + +class TestEdcaf : public Edcaf +{ + public: + queueing::IPacketQueue *pendingQueue = nullptr; + InProgressFrames *inProgressFrames = nullptr; + QosAckHandler *qosAckHandler = nullptr; + QosRecoveryProcedure *qosRecoveryProcedure = nullptr; + StationRetryCounters stationRetryCounters; + int numReleaseChannelCalls = 0; + std::vector *eventOrder = nullptr; + bool contentionActive = false; + int numRequestChannelCalls = 0; + int numContentionStarts = 0; + + virtual queueing::IPacketQueue *getPendingQueue() const override { return pendingQueue; } + virtual InProgressFrames *getInProgressFrames() const override { return inProgressFrames; } + virtual QosAckHandler *getAckHandler() const override { return qosAckHandler; } + virtual QosRecoveryProcedure *getRecoveryProcedure() const override { return qosRecoveryProcedure; } + virtual StationRetryCounters *getStationRetryCounters() const override { return const_cast(&stationRetryCounters); } + virtual void releaseChannel(IChannelAccess::ICallback *) override { numReleaseChannelCalls++; if (eventOrder != nullptr) eventOrder->push_back(1); } + virtual void requestChannel(IChannelAccess::ICallback *) override { numRequestChannelCalls++; if (!contentionActive) numContentionStarts++; } + void setAccessCategory(AccessCategory accessCategory) { ac = accessCategory; } + void setTxopProcedure(TxopProcedure *txopProcedure) { this->txopProcedure = txopProcedure; } +}; + +class TestEdca : public Edca +{ + public: + Edcaf *edcaf = nullptr; + std::array edcafs = {}; + int numEdcafs = 1; + NonQosRecoveryProcedure *nonQosRecoveryProcedure = nullptr; + std::vector requestedAccessCategories; + std::vector internallyCollidedEdcafs; + std::vector *eventOrder = nullptr; + + virtual int getNumEdcafs() const override { return numEdcafs; } + virtual Edcaf *getEdcaf(AccessCategory accessCategory) const override { return numEdcafs == 1 ? edcaf : edcafs[accessCategory]; } + virtual Edcaf *getChannelOwner() override { return edcaf; } + virtual std::vector getInternallyCollidedEdcafs() override { return internallyCollidedEdcafs; } + virtual void requestChannelAccess(AccessCategory accessCategory, IChannelAccess::ICallback *callback) override { requestedAccessCategories.push_back(accessCategory); if (eventOrder != nullptr) eventOrder->push_back(3); getEdcaf(accessCategory)->requestChannel(callback); } + virtual NonQosRecoveryProcedure *getMgmtAndNonQoSRecoveryProcedure() const override { return nonQosRecoveryProcedure; } +}; + +class TestHcf : public Hcf +{ + public: + virtual ~TestHcf() { + for (auto listener : getLocalSignalListeners(Ieee80211Mac::frameTransmissionOutcomeSignal)) + unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, listener); + } + int numProcessedDroppedSetupFrames = 0; + int numResumedEligibleChannelAccess = 0; + int numCancelledAddbaTransactions = 0; + int numRebuildEligibilityCalls = 0; + bool droppedSetupCancelled = false; + bool delegateDroppedSetupHandling = false; + TestCallback *managementCallback = nullptr; + + void configure(Edca *edca) { this->edca = edca; } + void configureMac(Ieee80211Mac *mac) { this->mac = mac; subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, check_and_cast(mac)); } + void configureEligibilityIndex(Edca *edca, IOriginatorMacDataService *originatorDataService) { + this->edca = edca; + this->originatorDataService = originatorDataService; + } + void rebuildEligibilityIndex() { rebuildPendingFrameEligibility(); } + void trackFrame(Packet *packet, AccessCategory accessCategory) { trackPendingFrame(packet, accessCategory); } + void untrackFrame(Packet *packet) { untrackPendingFrame(packet); } + bool hasFrame(AccessCategory accessCategory) { return hasFrameToTransmit(accessCategory); } + void requestEligible() { requestEligibleChannelAccess(); } + int processInternalCollision(const std::vector& edcafs) { return handleInternalCollision(edcafs); } + void grantChannel(IChannelAccess *channelAccess) { channelGranted(channelAccess); } + void finishSequence() { frameSequenceFinished(); } + int getNumEligiblePendingFrames(AccessCategory accessCategory) const { return numEligiblePendingFrames[accessCategory]; } + int getNumTrackedPendingFrames() const { return pendingFrameEligibility.size(); } + void observePendingQueue(queueing::IPacketQueue *pendingQueue) { + check_and_cast(pendingQueue)->subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, this); + } + void configureFrameSequenceHandler(IFrameSequenceHandler *frameSequenceHandler) { this->frameSequenceHandler = frameSequenceHandler; } + void configureRecipientDataService(IRecipientQosMacDataService *recipientDataService) { this->recipientDataService = recipientDataService; } + void configureManagementCallback(TestCallback *managementCallback) { this->managementCallback = managementCallback; } + void configureTransmittedData(Edca *edca, IOriginatorBlockAckAgreementHandler *handler, IOriginatorBlockAckAgreementPolicy *policy) { + this->edca = edca; + originatorBlockAckAgreementHandler = handler; + originatorBlockAckAgreementPolicy = policy; + } + void configureTransmittedManagement(Edca *edca, IOriginatorQoSAckPolicy *originatorAckPolicy) { + this->edca = edca; + this->originatorAckPolicy = originatorAckPolicy; + } + void configureBlockAckHandlers(IOriginatorBlockAckAgreementHandler *originatorHandler, IOriginatorBlockAckAgreementPolicy *originatorPolicy, IRecipientBlockAckAgreementHandler *recipientHandler, IRecipientBlockAckAgreementPolicy *recipientPolicy) { + originatorBlockAckAgreementHandler = originatorHandler; + originatorBlockAckAgreementPolicy = originatorPolicy; + recipientBlockAckAgreementHandler = recipientHandler; + recipientBlockAckAgreementPolicy = recipientPolicy; + } + void processTransmittedData(Packet *packet, const Ptr& dataHeader, AccessCategory ac) { originatorProcessTransmittedDataFrame(packet, dataHeader, ac); } + void processTransmittedManagement(Packet *packet, const Ptr& managementHeader, AccessCategory ac) { originatorProcessTransmittedManagementFrame(packet, managementHeader, ac); } + void dropPacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DROPPED); } + void removePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::REMOVED); } + void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } + void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } + void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } + void processFailedFrame(Packet *packet) { originatorProcessFailedFrame(packet); } + void processReceivedAck(Packet *packet, Packet *lastTransmittedPacket, AccessCategory accessCategory) { + auto header = packet->peekAtFront(); + auto lastTransmittedHeader = lastTransmittedPacket->peekAtFront(); + originatorProcessReceivedControlFrame(packet, header, lastTransmittedPacket, lastTransmittedHeader, accessCategory); + } + + protected: + virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { + if (managementCallback != nullptr) + managementCallback->processMgmtFrame(packet, header); + else + Hcf::processMgmtFrame(packet, header); + } + virtual bool processDroppedBlockAckSetupFrame(Packet *packet) override { numProcessedDroppedSetupFrames++; return delegateDroppedSetupHandling ? Hcf::processDroppedBlockAckSetupFrame(packet) : droppedSetupCancelled; } + virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { numCancelledAddbaTransactions++; Hcf::cancelAddbaTransaction(transactionId, excludedPacket); } + virtual void rebuildPendingFrameEligibility() override { numRebuildEligibilityCalls++; Hcf::rebuildPendingFrameEligibility(); } + virtual void resumeEligibleChannelAccess() override { numResumedEligibleChannelAccess++; } + virtual void scheduleInactivityTimer(simtime_t) override {} + virtual void scheduleAddbaResponseTimer(simtime_t) override {} +}; + +class TestMac : public Ieee80211Mac +{ + public: + using Ieee80211Mac::receiveSignal; + int numPendingRadioConfigSends = 0; + virtual void sendDownPendingRadioConfigMsg() override { numPendingRadioConfigSends++; } +}; + +class TestFragmentationPolicy : public IFragmentationPolicy +{ + public: + virtual std::vector computeFragmentSizes(Packet *) override { return { 2, 2 }; } +}; + +class TestMsduAggregationPolicy : public IMsduAggregationPolicy +{ + public: + int numCalls = 0; + + virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *, Packet *, const std::function&) override { numCalls++; return nullptr; } +}; + +class TestSelectedMsduAggregationPolicy : public IMsduAggregationPolicy +{ + public: + int numCalls = 0; + std::vector selectedPackets; + + virtual std::vector *computeAggregateFrames(queueing::IPacketQueue *, Packet *, const std::function&) override { numCalls++; return new std::vector(selectedPackets); } +}; + +class TestBasicMsduAggregationPolicy : public BasicMsduAggregationPolicy +{ + public: + void configure(b maximumSize) { + qOsCheck = true; + subframeNumThreshold = -1; + aggregationLengthThreshold = -1; + maxAMsduSize = maximumSize; + } +}; + +class TestNonQosRecoveryProcedure : public NonQosRecoveryProcedure +{ + public: + void addRetry(const Ptr& header) { + rtsThreshold = INT_MAX; + shortRetryCounter[SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())] = 1; + } + void addShortRetry(const Ptr& header, int count = 1) { + shortRetryCounter[SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())] = count; + } + void addLongRetry(const Ptr& header, int count = 1) { + longRetryCounter[SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())] = count; + } + bool hasRetry(const Ptr& header) const { + return shortRetryCounter.find(SequenceControlField(header->getSequenceNumber().get(), header->getFragmentNumber())) != shortRetryCounter.end(); + } + void configureRts(int threshold, int retryLimit, ICwCalculator *cwCalculator) { + rtsThreshold = threshold; + shortRetryLimit = retryLimit; + longRetryLimit = retryLimit; + this->cwCalculator = cwCalculator; + } +}; + class TestOnlyPacketExtractor : public queueing::IPacketExtractor { public: @@ -265,6 +662,59 @@ class TestOnlyPacketExtractor : public queueing::IPacketExtractor return packet; } }; + +class TestCwCalculator : public IRecoveryProcedure::ICwCalculator +{ + public: + int cw = 1; + virtual void incrementCw() override { cw++; } + virtual void resetCw() override { cw = 1; } + virtual int getCw() override { return cw; } +}; + +class TestOriginatorQosMacDataService : public OriginatorQosMacDataService +{ + public: + void enableMsduAggregationPolicy(IMsduAggregationPolicy *policy) { + aMsduAggregationPolicy = policy; + if (aMsduAggregation == nullptr) + aMsduAggregation = new MsduAggregation(); + } + void enableSequenceNumberAssignment() { sequenceNumberAssignment = new QoSSequenceNumberAssignment(); } + void enableFragmentation(IFragmentationPolicy *policy) { + fragmentationPolicy = policy; + fragmentation = new Fragmentation(); + } + std::vector *fragment(Packet *packet) { return fragmentIfNeeded(packet); } + void release(Packet *packet) { drop(packet); } +}; + +class TestFrameSequenceHandler : public IFrameSequenceHandler +{ + public: + IFrameSequence *frameSequence = nullptr; + FrameSequenceContext *context = nullptr; + bool running = false; + int numStartedSequences = 0; + + virtual ~TestFrameSequenceHandler() { + delete frameSequence; + delete context; + } + virtual const FrameSequenceContext *getContext() const override { return context; } + virtual const IFrameSequence *getFrameSequence() const override { return frameSequence; } + virtual void startFrameSequence(IFrameSequence *frameSequence, FrameSequenceContext *context, ICallback *) override { + this->frameSequence = frameSequence; + this->context = context; + running = true; + numStartedSequences++; + } + virtual void processResponse(Packet *) override {} + virtual void transmissionComplete() override {} + virtual bool isSequenceRunning() override { return running; } + virtual void handleStartRxTimeout() override {} +}; + static Ptr makeQosHeader(MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber) { auto header = makeShared(); @@ -274,8 +724,41 @@ static Ptr makeQosHeader(MacAddress receiverAddress, Tid ti header->setSequenceNumber(sequenceNumber); return header; } + +static Packet *makeTaggedDelbaPacket(const char *name, MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint64_t transactionId) +{ + auto delba = makeShared(); + delba->setReceiverAddress(receiverAddress); + delba->setTid(tid); + delba->setInitiator(true); + delba->setSequenceNumber(sequenceNumber); + delba->setFragmentNumber(fragmentNumber); + delba->setMoreFragments(moreFragments); + auto packet = new Packet(name, delba); + packet->addTag()->setTransactionId(transactionId); + return packet; +} + +static Ptr makeResponse(MacAddress transmitterAddress, Tid tid, uint8_t dialogToken, uint16_t statusCode) +{ + auto response = makeShared(); + response->setTransmitterAddress(transmitterAddress); + response->setTid(tid); + response->setDialogToken(dialogToken); + response->setStatusCode(statusCode); + response->setBufferSize(32); + response->setBlockAckTimeoutValue(0); + return response; +} + + %activity: +// IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.2. +TestOriginatorPolicy originatorPolicy; +TestCallback callback; MacAddress peer1("00:00:00:00:00:01"); +MacAddress peer2("00:00:00:00:00:02"); +MacAddress peer3("00:00:00:00:00:03"); auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const char *dropperClass = "") { auto module = cModuleType::get("inet.queueing.queue.PacketQueue")->create(name, this); module->par("packetCapacity").setIntValue(packetCapacity); @@ -283,6 +766,749 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const module->callInitialize(); return check_and_cast(module); }; + +{ + TestOriginatorHandler tokenHandler; + tokenHandler.setNextDialogToken(255); + auto request255 = tokenHandler.buildAddbaRequest(peer1, 0, SequenceNumberCyclic(0), &originatorPolicy); + auto request1 = tokenHandler.buildAddbaRequest(peer1, 0, SequenceNumberCyclic(0), &originatorPolicy); + ASSERT(request255->getDialogToken() == 255); + ASSERT(request1->getDialogToken() == 1); +} + +TestOriginatorHandler handler; +Packet triggerPacket("trigger"); +auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); + +// Transmitting the trigger MPDU does not create an agreement; initiation is +// deferred until the HCF successful-ACK path calls processAcknowledgedDataFrame(). +{ + auto preAckHandler = new TestOriginatorHandler(); + QosAckHandler preAckAckHandler; + TestEdcaf preAckEdcaf; + preAckEdcaf.qosAckHandler = &preAckAckHandler; + TestEdca preAckEdca; + preAckEdca.edcaf = &preAckEdcaf; + TestHcf preAckHcf; + preAckHcf.configureTransmittedData(&preAckEdca, preAckHandler, &originatorPolicy); + Packet preAckPacket("preAckPacket"); + auto preAckHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); + preAckHeader->setAckPolicy(NORMAL_ACK); + preAckHcf.processTransmittedData(&preAckPacket, preAckHeader, AC_BE); + if (preAckHandler->getAgreement(peer1, 3) != nullptr) + throw cRuntimeError("HCF initiated ADDBA before the trigger MPDU was acknowledged"); + ASSERT(preAckHandler->getAgreement(peer1, 3) == nullptr); +} + +// A fragmented MSDU cannot trigger ADDBA until its final fragment is ACKed, +// otherwise the remaining fragments would precede the advertised SSN. +{ + TestOriginatorHandler fragmentedDataHandler; + TestCallback fragmentedDataCallback; + Packet fragmentedDataPacket("fragmentedDataPacket"); + auto firstFragmentHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(100)); + firstFragmentHeader->setFragmentNumber(0); + firstFragmentHeader->setMoreFragments(true); + fragmentedDataHandler.processAcknowledgedDataFrame(&fragmentedDataPacket, firstFragmentHeader, &originatorPolicy, &fragmentedDataCallback); + if (fragmentedDataHandler.getAgreement(peer1, 6) != nullptr || fragmentedDataCallback.managementPacket != nullptr) + throw cRuntimeError("ADDBA was initiated before the final data fragment was acknowledged"); + auto finalFragmentHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(100)); + finalFragmentHeader->setFragmentNumber(1); + finalFragmentHeader->setMoreFragments(false); + fragmentedDataHandler.processAcknowledgedDataFrame(&fragmentedDataPacket, finalFragmentHeader, &originatorPolicy, &fragmentedDataCallback); + auto fragmentedDataRequest = dynamicPtrCast(fragmentedDataCallback.managementHeader); + ASSERT(fragmentedDataRequest != nullptr); + ASSERT(fragmentedDataRequest->getStartingSequenceNumber() == SequenceNumberCyclic(101)); +} + +// A fragmented, transaction-tagged DELBA remains eligible through every +// nonfinal fragment. Aborting one fragment invalidates the transaction and asks +// the HCF callback to remove its sibling fragments. +{ + TestOriginatorHandler fragmentedDelbaHandler; + TestCallback fragmentedDelbaCallback; + auto firstDelba = makeShared(); + firstDelba->setReceiverAddress(peer1); + firstDelba->setTid(6); + firstDelba->setInitiator(true); + firstDelba->setFragmentNumber(0); + firstDelba->setMoreFragments(true); + auto finalDelba = staticPtrCast(firstDelba->dupShared()); + finalDelba->setFragmentNumber(1); + finalDelba->setMoreFragments(false); + Packet firstDelbaPacket("firstDelbaFragment", firstDelba); + Packet finalDelbaPacket("finalDelbaFragment", finalDelba); + firstDelbaPacket.addTag()->setTransactionId(77); + finalDelbaPacket.addTag()->setTransactionId(77); + fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 77); + ASSERT(fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); + ASSERT(fragmentedDelbaHandler.processTransmittedDelba(&firstDelbaPacket, &fragmentedDelbaCallback) == nullptr); + ASSERT(fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); + ASSERT(fragmentedDelbaHandler.processTransmittedDelba(&finalDelbaPacket, &fragmentedDelbaCallback) == nullptr); + // A failed attempt remains eligible for retransmission. + ASSERT(fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); + ASSERT(fragmentedDelbaHandler.processTransmittedDelba(&finalDelbaPacket, &fragmentedDelbaCallback) == nullptr); + ASSERT(fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); + ASSERT(!fragmentedDelbaHandler.processAcknowledgedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); + ASSERT(fragmentedDelbaHandler.processAcknowledgedDelba(&finalDelbaPacket, &fragmentedDelbaCallback)); + ASSERT(!fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); + ASSERT(fragmentedDelbaCallback.cancelledTransactionIds.back() == 77); + ASSERT(fragmentedDelbaCallback.excludedPackets.back() == &finalDelbaPacket); + + fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 78); + firstDelbaPacket.getTagForUpdate()->setTransactionId(78); + ASSERT(fragmentedDelbaHandler.processAbortedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); + ASSERT(!fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); + ASSERT(fragmentedDelbaCallback.cancelledTransactionIds.back() == 78); + ASSERT(fragmentedDelbaCallback.excludedPackets.back() == &firstDelbaPacket); +} + +// IEEE Std 802.11-2024, 10.23.2.12.1 and 11.5.3.2: the HCF retry path keeps a +// transaction-tagged DELBA selectable and preserves its negative ACK state. +{ + constexpr uint64_t transactionId = 79; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + TestQosAckHandler ackHandler; + TestPacketQueue pendingQueue; + TestOriginatorQosMacDataService dataService; + auto originatorHandler = new TestOriginatorHandler(); + dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto delba = dynamicPtrCast(packet->peekAtFront()); + return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); + }); + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + TestOriginatorPolicy policy; + OriginatorQosAckPolicy ackPolicy; + TestHcf hcf; + hcf.configureTransmittedData(&edca, originatorHandler, &policy); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + + auto packet = makeTaggedDelbaPacket("failedTransactionalDelba", peer1, 6, SequenceNumberCyclic(203), 0, false, transactionId); + auto delba = packet->peekAtFront(); + originatorHandler->setPendingTeardownTransaction(peer1, 6, transactionId); + inProgressFrames.addOwnedFrame(packet); + ackHandler.frameGotInProgress(delba); + hcf.processTransmittedManagement(packet, delba, AC_BE); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(delba) == QosAckHandler::Status::WAITING_FOR_NORMAL_ACK); + hcf.processFailedFrame(packet); + ASSERT(originatorHandler->isDelbaPending(packet, packet->peekAtFront())); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(delba) == QosAckHandler::Status::NORMAL_ACK_NOT_ARRIVED); + ASSERT(ackHandler.hasMgmtEntry(delba)); + ASSERT(inProgressFrames.getFrameToTransmit() == packet); + ASSERT(inProgressFrames.getNumDroppedFrames() == 0); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames == std::vector({ packet })); + delete packet; +} + +// A final-fragment ACK retires the DELBA transaction through HCF exactly once, +// removes its in-progress sibling, and clears both ACK-handler entries. +{ + constexpr uint64_t transactionId = 80; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + TestQosAckHandler ackHandler; + TestPacketQueue pendingQueue; + TestOriginatorQosMacDataService dataService; + auto originatorHandler = new TestOriginatorHandler(); + dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto delba = dynamicPtrCast(packet->peekAtFront()); + return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); + }); + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + TestOriginatorPolicy policy; + OriginatorQosAckPolicy ackPolicy; + TestHcf hcf; + TestMac acknowledgedDelbaMac; + hcf.configureMac(&acknowledgedDelbaMac); + hcf.configureTransmittedData(&edca, originatorHandler, &policy); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + + auto sibling = makeTaggedDelbaPacket("acknowledgedDelbaSibling", peer1, 6, SequenceNumberCyclic(204), 0, true, transactionId); + auto finalPacket = makeTaggedDelbaPacket("acknowledgedFinalDelba", peer1, 6, SequenceNumberCyclic(204), 1, false, transactionId); + auto siblingHeader = sibling->peekAtFront(); + auto finalHeader = finalPacket->peekAtFront(); + originatorHandler->setPendingTeardownTransaction(peer1, 6, transactionId); + inProgressFrames.addOwnedFrame(sibling); + inProgressFrames.addOwnedFrame(finalPacket); + ackHandler.frameGotInProgress(siblingHeader); + ackHandler.frameGotInProgress(finalHeader); + hcf.processTransmittedManagement(finalPacket, finalHeader, AC_BE); + Packet ackPacket("transactionalDelbaAck", makeShared()); + hcf.processReceivedAck(&ackPacket, finalPacket, AC_BE); + ASSERT(!originatorHandler->isDelbaPending(finalPacket, finalHeader)); + ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 1); + ASSERT(!ackHandler.hasMgmtEntry(siblingHeader)); + ASSERT(!ackHandler.hasMgmtEntry(finalHeader)); + ASSERT(!originatorHandler->processAcknowledgedDelba(finalPacket, &hcf)); + ASSERT(hcf.numCancelledAddbaTransactions == 1); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames == std::vector({ finalPacket })); + delete finalPacket; +} + +// Reaching the retry limit aborts the teardown through HCF, clears its sibling +// and both ACK-handler entries, and records one terminal cancellation. +{ + constexpr uint64_t transactionId = 81; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 1, &cwCalculator); + TestQosAckHandler ackHandler; + TestPacketQueue pendingQueue; + TestOriginatorQosMacDataService dataService; + auto originatorHandler = new TestOriginatorHandler(); + dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto delba = dynamicPtrCast(packet->peekAtFront()); + return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); + }); + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + TestOriginatorPolicy policy; + OriginatorQosAckPolicy ackPolicy; + TestHcf hcf; + TestMac retryLimitedDelbaMac; + hcf.configureMac(&retryLimitedDelbaMac); + hcf.configureTransmittedData(&edca, originatorHandler, &policy); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + + auto sibling = makeTaggedDelbaPacket("retryLimitedDelbaSibling", peer1, 6, SequenceNumberCyclic(205), 0, true, transactionId); + auto finalPacket = makeTaggedDelbaPacket("retryLimitedFinalDelba", peer1, 6, SequenceNumberCyclic(205), 1, false, transactionId); + auto siblingHeader = sibling->peekAtFront(); + auto finalHeader = finalPacket->peekAtFront(); + originatorHandler->setPendingTeardownTransaction(peer1, 6, transactionId); + inProgressFrames.addOwnedFrame(sibling); + inProgressFrames.addOwnedFrame(finalPacket); + ackHandler.frameGotInProgress(siblingHeader); + ackHandler.frameGotInProgress(finalHeader); + hcf.processTransmittedManagement(finalPacket, finalHeader, AC_BE); + hcf.processFailedFrame(finalPacket); + ASSERT(!originatorHandler->isDelbaPending(finalPacket, finalHeader)); + ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 1); + ASSERT(!ackHandler.hasMgmtEntry(siblingHeader)); + ASSERT(!ackHandler.hasMgmtEntry(finalHeader)); + ASSERT(!originatorHandler->processAbortedDelba(finalPacket, &hcf)); + ASSERT(hcf.numCancelledAddbaTransactions == 1); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames == std::vector({ finalPacket })); + delete finalPacket; +} + +handler.processAcknowledgedDataFrame(&triggerPacket, triggerHeader, &originatorPolicy, &callback); +auto request = dynamicPtrCast(callback.managementHeader); +ASSERT(request != nullptr); +ASSERT(request->getDialogToken() != 0); +ASSERT(request->getStartingSequenceNumber() == SequenceNumberCyclic(0)); +handler.processTransmittedAddbaReq(callback.managementPacket, request, &originatorPolicy, &callback); +ASSERT(callback.addbaDeadline == simTime() + 1); +auto requestTransactionId = callback.managementPacket->getTag()->getTransactionId(); + +auto wrongToken = makeResponse(peer1, 3, request->getDialogToken() + 1, 0); +ASSERT(handler.processReceivedAddbaResp(wrongToken, &originatorPolicy, &callback).establishedAgreement == nullptr); +ASSERT(handler.isAddbaResponsePending(peer1, 3)); +auto wrongTid = makeResponse(peer1, 4, request->getDialogToken(), 0); +ASSERT(handler.processReceivedAddbaResp(wrongTid, &originatorPolicy, &callback).establishedAgreement == nullptr); +auto wrongPeer = makeResponse(peer2, 3, request->getDialogToken(), 0); +ASSERT(handler.processReceivedAddbaResp(wrongPeer, &originatorPolicy, &callback).establishedAgreement == nullptr); + +auto success = makeResponse(peer1, 3, request->getDialogToken(), 0); +auto establishedResponse = handler.processReceivedAddbaResp(success, &originatorPolicy, &callback); +auto established = establishedResponse.establishedAgreement; +ASSERT(established != nullptr); +ASSERT(establishedResponse.terminatedAgreement == nullptr); +ASSERT(establishedResponse.teardownDelba == nullptr); +ASSERT(establishedResponse.teardownTransactionId == 0); +ASSERT(callback.cancelledTransactionIds.back() == requestTransactionId); +ASSERT(callback.excludedPackets.back() == nullptr); +ASSERT(established->getStartingSequenceNumber() == SequenceNumberCyclic(0)); +ASSERT(!handler.isAddbaResponsePending(peer1, 3)); +ASSERT(handler.processReceivedAddbaResp(success, &originatorPolicy, &callback).establishedAgreement == nullptr); + +// IEEE Std 802.11-2024, 10.25.2 and Figure 11-32: a successful matching +// response establishes the agreement even when local policy rejects the +// negotiated parameters; the originator immediately deletes its local state +// and sends END_BA on a best-effort basis. +{ + TestOriginatorHandler vetoHandler; + TestCallback vetoCallback; + TestOriginatorPolicy vetoPolicy; + vetoPolicy.addbaAccepted = false; + Packet vetoTrigger("vetoTrigger"); + auto vetoHeader = makeQosHeader(peer3, 6, SequenceNumberCyclic(30)); + vetoHandler.processAcknowledgedDataFrame(&vetoTrigger, vetoHeader, &vetoPolicy, &vetoCallback); + auto vetoRequest = dynamicPtrCast(vetoCallback.managementHeader); + auto vetoTransactionId = vetoCallback.managementPacket->getTag()->getTransactionId(); + vetoHandler.processTransmittedAddbaReq(vetoCallback.managementPacket, vetoRequest, &vetoPolicy, &vetoCallback); + auto vetoResponse = makeResponse(peer3, 6, vetoRequest->getDialogToken(), 0); + auto vetoResponseOutcome = vetoHandler.processReceivedAddbaResp(vetoResponse, &vetoPolicy, &vetoCallback); + auto vetoAgreement = vetoResponseOutcome.terminatedAgreement.get(); + ASSERT(vetoAgreement != nullptr); + ASSERT(vetoResponseOutcome.establishedAgreement == nullptr); + ASSERT(vetoResponseOutcome.teardownTransactionId == vetoTransactionId); + ASSERT(vetoAgreement->getIsAddbaResponseReceived()); + ASSERT(vetoHandler.getAgreement(peer3, 6) == nullptr); + auto vetoDelba = vetoResponseOutcome.teardownDelba; + ASSERT(vetoDelba != nullptr); + ASSERT(vetoDelba->getInitiator()); + ASSERT(vetoDelba->getReceiverAddress() == peer3); + ASSERT(vetoDelba->getTid() == 6); + ASSERT(vetoDelba->getReasonCode() == RC_END_BA); + Packet vetoDelbaPacket("vetoDelba", vetoDelba); + vetoDelbaPacket.addTag()->setTransactionId(vetoResponseOutcome.teardownTransactionId); + ASSERT(vetoHandler.isDelbaPending(&vetoDelbaPacket, vetoDelba)); + ASSERT(vetoHandler.getAgreement(peer3, 6) == nullptr); + ASSERT(vetoHandler.getRetryDeadline(peer3, 6) > simTime()); + TestCallback vetoRetryCallback; + Packet vetoRetryTrigger("vetoRetryTrigger"); + auto vetoRetryHeader = makeQosHeader(peer3, 6, SequenceNumberCyclic(31)); + vetoHandler.processAcknowledgedDataFrame(&vetoRetryTrigger, vetoRetryHeader, &vetoPolicy, &vetoRetryCallback); + ASSERT(vetoRetryCallback.managementPacket == nullptr); + vetoHandler.allowRetryNow(peer3, 6); + auto obsoleteVetoTransactionId = vetoHandler.processAcknowledgedDataFrame(&vetoRetryTrigger, vetoRetryHeader, &vetoPolicy, &vetoRetryCallback); + ASSERT(obsoleteVetoTransactionId == vetoTransactionId); + ASSERT(vetoRetryCallback.managementPacket != nullptr); + auto newerAgreement = vetoHandler.getAgreement(peer3, 6); + ASSERT(newerAgreement != nullptr); + ASSERT(newerAgreement->getTransactionId() != vetoTransactionId); + ASSERT(!vetoHandler.isDelbaPending(&vetoDelbaPacket, vetoDelba)); + ASSERT(!vetoHandler.processAbortedDelba(&vetoDelbaPacket, &vetoCallback)); + ASSERT(!vetoHandler.isDelbaPending(&vetoDelbaPacket, vetoDelba)); + auto vetoTerminated = vetoHandler.processTransmittedDelba(&vetoDelbaPacket, &vetoCallback); + ASSERT(vetoTerminated == nullptr); + ASSERT(vetoHandler.getAgreement(peer3, 6) == newerAgreement); +} + +Packet rejectedTrigger("rejectedTrigger"); +auto rejectedHeader = makeQosHeader(peer2, 4, SequenceNumberCyclic(10)); +handler.processAcknowledgedDataFrame(&rejectedTrigger, rejectedHeader, &originatorPolicy, &callback); +auto rejectedRequest = dynamicPtrCast(callback.managementHeader); +handler.processTransmittedAddbaReq(callback.managementPacket, rejectedRequest, &originatorPolicy, &callback); +auto rejection = makeResponse(peer2, 4, rejectedRequest->getDialogToken(), 1); +ASSERT(handler.processReceivedAddbaResp(rejection, &originatorPolicy, &callback).establishedAgreement == nullptr); +ASSERT(handler.getAgreement(peer2, 4) == nullptr); +handler.processAcknowledgedDataFrame(&rejectedTrigger, rejectedHeader, &originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer2, 4) == nullptr); +handler.allowRetryNow(peer2, 4); +handler.processAcknowledgedDataFrame(&rejectedTrigger, rejectedHeader, &originatorPolicy, &callback); +ASSERT(handler.isAddbaResponsePending(peer2, 4)); + +Packet timeoutTrigger("timeoutTrigger"); +auto timeoutHeader = makeQosHeader(peer3, 5, SequenceNumberCyclic(20)); +handler.processAcknowledgedDataFrame(&timeoutTrigger, timeoutHeader, &originatorPolicy, &callback); +auto timedRequest = dynamicPtrCast(callback.managementHeader); +auto timedRequestPacket = callback.managementPacket; +auto timedTransactionId = timedRequestPacket->getTag()->getTransactionId(); +handler.processTransmittedAddbaReq(timedRequestPacket, timedRequest, &originatorPolicy, &callback); +handler.expireNow(peer3, 5); +handler.addbaResponseTimeoutExpired(&originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer3, 5) == nullptr); +ASSERT(callback.cancelledTransactionIds.back() == timedTransactionId); +ASSERT(callback.excludedPackets.back() == nullptr); +handler.processTransmittedAddbaReq(timedRequestPacket, timedRequest, &originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer3, 5) == nullptr); +auto staleResponse = makeResponse(peer3, 5, timedRequest->getDialogToken(), 0); +ASSERT(handler.processReceivedAddbaResp(staleResponse, &originatorPolicy, &callback).establishedAgreement == nullptr); +handler.processAcknowledgedDataFrame(&timeoutTrigger, timeoutHeader, &originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer3, 5) == nullptr); +handler.allowRetryNow(peer3, 5); +handler.processAcknowledgedDataFrame(&timeoutTrigger, timeoutHeader, &originatorPolicy, &callback); +ASSERT(handler.isAddbaResponsePending(peer3, 5)); + +// All simultaneously expired agreements are erased before cancellation can +// reenter the MAC and query or mutate transaction state. +{ + TestOriginatorHandler timeoutHandler; + TestOriginatorPolicy timeoutPolicy; + TestTimeoutCancellationCallback timeoutCallback; + timeoutCallback.handler = &timeoutHandler; + timeoutCallback.expiredAgreementIds = { { peer1, 1 }, { peer2, 2 } }; + + Packet firstTimeoutTrigger("firstTimeoutTrigger"); + auto firstTimeoutHeader = makeQosHeader(peer1, 1, SequenceNumberCyclic(40)); + timeoutHandler.processAcknowledgedDataFrame(&firstTimeoutTrigger, firstTimeoutHeader, &timeoutPolicy, &timeoutCallback); + auto firstTimeoutRequestPacket = timeoutCallback.managementPacket; + auto firstTimeoutRequest = dynamicPtrCast(timeoutCallback.managementHeader); + auto firstTimeoutTransactionId = firstTimeoutRequestPacket->getTag()->getTransactionId(); + timeoutHandler.processTransmittedAddbaReq(firstTimeoutRequestPacket, firstTimeoutRequest, &timeoutPolicy, &timeoutCallback); + + Packet secondTimeoutTrigger("secondTimeoutTrigger"); + auto secondTimeoutHeader = makeQosHeader(peer2, 2, SequenceNumberCyclic(50)); + timeoutHandler.processAcknowledgedDataFrame(&secondTimeoutTrigger, secondTimeoutHeader, &timeoutPolicy, &timeoutCallback); + auto secondTimeoutRequestPacket = timeoutCallback.managementPacket; + auto secondTimeoutRequest = dynamicPtrCast(timeoutCallback.managementHeader); + auto secondTimeoutTransactionId = secondTimeoutRequestPacket->getTag()->getTransactionId(); + timeoutHandler.processTransmittedAddbaReq(secondTimeoutRequestPacket, secondTimeoutRequest, &timeoutPolicy, &timeoutCallback); + + timeoutHandler.expireNow(peer1, 1); + timeoutHandler.expireNow(peer2, 2); + timeoutHandler.addbaResponseTimeoutExpired(&timeoutPolicy, &timeoutCallback); + ASSERT(timeoutCallback.allExpiredAgreementsGoneBeforeCallbacks); + ASSERT(timeoutCallback.cancelledTransactionIds.size() == 2); + ASSERT(std::find(timeoutCallback.cancelledTransactionIds.begin(), timeoutCallback.cancelledTransactionIds.end(), firstTimeoutTransactionId) != timeoutCallback.cancelledTransactionIds.end()); + ASSERT(std::find(timeoutCallback.cancelledTransactionIds.begin(), timeoutCallback.cancelledTransactionIds.end(), secondTimeoutTransactionId) != timeoutCallback.cancelledTransactionIds.end()); + ASSERT(timeoutCallback.addbaDeadline == SIMTIME_MAX); +} + +// Transaction cancellation leaves packets borrowed by the active frame +// sequence under that sequence's ownership, including RTS-protected packets. +{ + const uint64_t transactionId = 900; + TestPacketQueue pendingQueue; + TestInProgressFrames activeFrames; + auto directRequest = makeShared(); + auto directPacket = new Packet("activeAddbaRequest", directRequest); + directPacket->addTag()->setTransactionId(transactionId); + auto protectedRequest = makeShared(); + auto protectedPacket = new Packet("rtsProtectedAddbaRequest", protectedRequest); + protectedPacket->addTag()->setTransactionId(transactionId); + activeFrames.addFrame(directPacket); + activeFrames.addFrame(protectedPacket); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &activeFrames; + TestEdca edca; + edca.edcaf = &edcaf; + auto frameSequenceHandler = new TestFrameSequenceHandler(); + frameSequenceHandler->running = true; + frameSequenceHandler->context = new FrameSequenceContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &activeFrames, nullptr, nullptr, nullptr, nullptr); + frameSequenceHandler->context->addStep(new TransmitStep(directPacket, 0)); + frameSequenceHandler->context->addStep(new RtsTransmitStep(protectedPacket, new Packet("rts", makeShared()), 0)); + TestHcf hcf; + hcf.configure(&edca); + hcf.configureFrameSequenceHandler(frameSequenceHandler); + hcf.cancelTransaction(transactionId); + ASSERT(activeFrames.getLength() == 2); + auto releasedFrames = activeFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 2); + for (auto frame : releasedFrames) + delete frame; +} + + +// A transaction frame that is not referenced by an active sequence is removed +// from in-progress ownership and reclaimed immediately, without entering the +// deferred dropped-frame list. +{ + const uint64_t transactionId = 901; + TestPacketQueue pendingQueue; + QosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + auto request = makeShared(); + request->setReceiverAddress(peer1); + request->setSequenceNumber(SequenceNumberCyclic(31)); + auto packet = new Packet("idleAddbaRequest", request); + packet->addTag()->setTransactionId(transactionId); + inProgressFrames.addOwnedFrame(packet); + ackHandler.frameGotInProgress(request); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestHcf hcf; + hcf.configure(&edca); + TestPacketDropSignalListener dropListener; + hcf.subscribe(packetDroppedSignal, &dropListener); + hcf.cancelTransaction(transactionId); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 0); + ASSERT(dropListener.numSignals == 1); + ASSERT(dropListener.numOtherPacketDrops == 1); + hcf.unsubscribe(packetDroppedSignal, &dropListener); +} + +// An unsent transaction has no response deadline and is retired by a typed drop callback. +Packet unsentTrigger("unsentTrigger"); +auto unsentHeader = makeQosHeader(peer2, 5, SequenceNumberCyclic(40)); +handler.processAcknowledgedDataFrame(&unsentTrigger, unsentHeader, &originatorPolicy, &callback); +ASSERT(handler.isAddbaResponsePending(peer2, 5)); + +// Real observed pending queues treat REMOVED as terminal for an unsent tagged +// ADDBA Request, while DEQUEUED is only ownership transfer. The held same-TID +// data frame becomes eligible exactly once after terminal cleanup. +auto verifyObservedPendingRemoval = [&](queueing::IPacketQueue::PacketRemovalReason removalReason, bool removeAll) { + auto observedHandler = new TestOriginatorHandler(); + TestOriginatorQosMacDataService observedDataService; + observedDataService.setFrameEligibilityFunction([observedHandler](const Packet *packet) { + auto header = packet->peekAtFront(); + if (auto addbaReq = dynamicPtrCast(header)) + return observedHandler->isAddbaRequestPending(packet, addbaReq); + auto dataHeader = dynamicPtrCast(header); + return dataHeader == nullptr || !observedHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); + }); + TestPacketQueue observedQueue; + TestAckHandler observedAckHandler; + TestInProgressFrames observedInProgressFrames; + observedInProgressFrames.configure(&observedDataService, &observedAckHandler, &observedQueue); + TestEdcaf observedEdcaf; + observedEdcaf.pendingQueue = &observedQueue; + observedEdcaf.inProgressFrames = &observedInProgressFrames; + TestEdca observedEdca; + observedEdca.edcaf = &observedEdcaf; + TestHcf observedHcf; + observedHcf.configureEligibilityIndex(&observedEdca, &observedDataService); + auto observedRecipientHandler = new RecipientBlockAckAgreementHandler(); + TestRecipientPolicy observedRecipientPolicy; + observedHcf.configureBlockAckHandlers(observedHandler, &originatorPolicy, observedRecipientHandler, &observedRecipientPolicy); + observedHcf.delegateDroppedSetupHandling = true; + observedHcf.observePendingQueue(&observedQueue); + TestCallback observedCallback; + Packet observedTrigger("observedTrigger"); + auto observedHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(60)); + observedHandler->processAcknowledgedDataFrame(&observedTrigger, observedHeader, &originatorPolicy, &observedCallback); + auto observedRequestPacket = observedCallback.managementPacket; + auto observedRequest = dynamicPtrCast(observedCallback.managementHeader); + observedCallback.forgetManagementPacket(observedRequestPacket); + auto heldDataPacket = new Packet("heldSameTidData", makeQosHeader(peer2, 6, SequenceNumberCyclic(61))); + observedQueue.enqueuePacket(observedRequestPacket); + observedQueue.enqueuePacket(heldDataPacket); + observedHcf.trackFrame(observedRequestPacket, AC_BK); + observedHcf.trackFrame(heldDataPacket, AC_BK); + ASSERT(observedHcf.getNumEligiblePendingFrames(AC_BK) == 1); + if (removalReason == queueing::IPacketQueue::PacketRemovalReason::DEQUEUED) { + auto dequeuedPacket = observedQueue.dequeuePacket(); + ASSERT(dequeuedPacket == observedRequestPacket); + ASSERT(observedHandler->isAddbaResponsePending(peer2, 6)); + ASSERT(observedHcf.numResumedEligibleChannelAccess == 0); + observedHandler->processDroppedAddbaReq(dequeuedPacket, observedRequest, &originatorPolicy, &observedCallback); + delete dequeuedPacket; + } + else { + if (removeAll) + observedQueue.removeAllPackets(); + else + observedQueue.removePacket(observedRequestPacket); + ASSERT(observedHandler->getAgreement(peer2, 6) == nullptr); + ASSERT(observedHcf.getNumEligiblePendingFrames(AC_BK) == (removeAll ? 0 : 1)); + ASSERT(observedHcf.numResumedEligibleChannelAccess == 1); + if (!removeAll) + observedQueue.removePacket(heldDataPacket); + delete observedRequestPacket; + } + delete heldDataPacket; +}; +verifyObservedPendingRemoval(queueing::IPacketQueue::PacketRemovalReason::REMOVED, false); +verifyObservedPendingRemoval(queueing::IPacketQueue::PacketRemovalReason::REMOVED, true); +verifyObservedPendingRemoval(queueing::IPacketQueue::PacketRemovalReason::DEQUEUED, false); + +ASSERT(callback.addbaDeadline == SIMTIME_MAX); +auto unsentRequest = dynamicPtrCast(callback.managementHeader); +auto unsentRequestPacket = callback.managementPacket; +auto unsentTransactionId = unsentRequestPacket->getTag()->getTransactionId(); +handler.processDroppedAddbaReq(unsentRequestPacket, unsentRequest, &originatorPolicy, &callback); +ASSERT(handler.getAgreement(peer2, 5) == nullptr); +ASSERT(callback.cancelledTransactionIds.back() == unsentTransactionId); +ASSERT(callback.excludedPackets.back() == unsentRequestPacket); + +// A DELBA that terminates a pending transaction also cancels its tagged ADDBA +// Request and recomputes the response timer. +{ + TestOriginatorHandler receivedDelbaHandler; + TestCallback receivedDelbaCallback; + Packet receivedDelbaTrigger("receivedDelbaTrigger"); + auto receivedDelbaTriggerHeader = makeQosHeader(peer1, 2, SequenceNumberCyclic(60)); + receivedDelbaHandler.processAcknowledgedDataFrame(&receivedDelbaTrigger, receivedDelbaTriggerHeader, &originatorPolicy, &receivedDelbaCallback); + auto receivedDelbaTransactionId = receivedDelbaCallback.managementPacket->getTag()->getTransactionId(); + auto receivedDelba = makeShared(); + receivedDelba->setTransmitterAddress(peer1); + receivedDelba->setTid(2); + auto terminatedAgreement = receivedDelbaHandler.processReceivedDelba(receivedDelba, &originatorPolicy, &receivedDelbaCallback); + ASSERT(terminatedAgreement != nullptr); + if (receivedDelbaHandler.getAgreement(peer1, 2) != nullptr || receivedDelbaCallback.cancelledTransactionIds.size() != 1 || receivedDelbaCallback.cancelledTransactionIds.back() != receivedDelbaTransactionId) + throw cRuntimeError("Received DELBA did not cancel the pending ADDBA transaction"); + ASSERT(receivedDelbaHandler.getAgreement(peer1, 2) == nullptr); + ASSERT(receivedDelbaCallback.cancelledTransactionIds.back() == receivedDelbaTransactionId); + ASSERT(receivedDelbaCallback.excludedPackets.back() == nullptr); + ASSERT(receivedDelbaCallback.addbaDeadline == SIMTIME_MAX); +} +{ + TestOriginatorHandler transmittedDelbaHandler; + TestCallback transmittedDelbaCallback; + Packet transmittedDelbaTrigger("transmittedDelbaTrigger"); + auto transmittedDelbaTriggerHeader = makeQosHeader(peer2, 2, SequenceNumberCyclic(70)); + transmittedDelbaHandler.processAcknowledgedDataFrame(&transmittedDelbaTrigger, transmittedDelbaTriggerHeader, &originatorPolicy, &transmittedDelbaCallback); + auto transmittedDelbaTransactionId = transmittedDelbaCallback.managementPacket->getTag()->getTransactionId(); + auto transmittedDelba = makeShared(); + transmittedDelba->setReceiverAddress(peer2); + transmittedDelba->setTid(2); + Packet transmittedDelbaPacket("transmittedDelba", transmittedDelba); + auto terminatedPendingAgreement = transmittedDelbaHandler.processTransmittedDelba(&transmittedDelbaPacket, &transmittedDelbaCallback); + ASSERT(terminatedPendingAgreement != nullptr); + ASSERT(terminatedPendingAgreement->isPending()); + if (transmittedDelbaHandler.getAgreement(peer2, 2) != nullptr || transmittedDelbaCallback.cancelledTransactionIds.size() != 1 || transmittedDelbaCallback.cancelledTransactionIds.back() != transmittedDelbaTransactionId) + throw cRuntimeError("Transmitted DELBA did not cancel the pending ADDBA transaction"); + ASSERT(transmittedDelbaHandler.getAgreement(peer2, 2) == nullptr); + ASSERT(transmittedDelbaCallback.cancelledTransactionIds.back() == transmittedDelbaTransactionId); + ASSERT(transmittedDelbaCallback.excludedPackets.back() == nullptr); + ASSERT(transmittedDelbaCallback.addbaDeadline == SIMTIME_MAX); +} + +// Fragmentation preserves the local transaction identity on every MPDU, and +// typed cancellation removes all exact siblings while retaining the dropped +// fragment and an unrelated transaction. +{ + TestOriginatorHandler fragmentedHandler; + TestCallback fragmentedCallback; + Packet fragmentedTrigger("fragmentedTrigger"); + auto fragmentedTriggerHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(50)); + fragmentedHandler.processAcknowledgedDataFrame(&fragmentedTrigger, fragmentedTriggerHeader, &originatorPolicy, &fragmentedCallback); + auto unfragmentedRequestPacket = fragmentedCallback.managementPacket; + auto fragmentedTransactionId = unfragmentedRequestPacket->getTag()->getTransactionId(); + fragmentedCallback.forgetManagementPacket(unfragmentedRequestPacket); + unfragmentedRequestPacket->insertAtBack(makeShared(B(4))); + unfragmentedRequestPacket->insertAtBack(makeShared()); + TestFragmentationPolicy fragmentationPolicy; + TestOriginatorQosMacDataService fragmentationDataService; + fragmentationDataService.enableFragmentation(&fragmentationPolicy); + auto fragments = fragmentationDataService.fragment(unfragmentedRequestPacket); + ASSERT(fragments != nullptr); + ASSERT(fragments->size() == 2); + for (auto fragment : *fragments) + ASSERT(fragment->getTag()->getTransactionId() == fragmentedTransactionId); + auto unrelatedPacket = new Packet("unrelatedTransaction", makeShared(B(1))); + unrelatedPacket->addTag()->setTransactionId(fragmentedTransactionId + 1); + auto droppedFragment = fragments->at(0); + auto siblingFragment = fragments->at(1); + TestPacketQueue fragmentedPendingQueue; + fragmentedPendingQueue.packets = { droppedFragment, siblingFragment, unrelatedPacket }; + TestInProgressFrames fragmentedInProgressFrames; + TestEdcaf fragmentedEdcaf; + fragmentedEdcaf.pendingQueue = &fragmentedPendingQueue; + fragmentedEdcaf.inProgressFrames = &fragmentedInProgressFrames; + TestEdca fragmentedEdca; + fragmentedEdca.edcaf = &fragmentedEdcaf; + TestHcf fragmentedHcf; + fragmentedHcf.configure(&fragmentedEdca); + auto droppedRequest = droppedFragment->peekAtFront(); + fragmentedHandler.processDroppedAddbaReq(droppedFragment, droppedRequest, &originatorPolicy, &fragmentedHcf); + ASSERT(fragmentedHandler.getAgreement(peer1, 7) == nullptr); + ASSERT(fragmentedPendingQueue.getNumPackets() == 2); + ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), droppedFragment) != fragmentedPendingQueue.packets.end()); + ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), siblingFragment) == fragmentedPendingQueue.packets.end()); + ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), unrelatedPacket) != fragmentedPendingQueue.packets.end()); + fragmentedPendingQueue.removePacket(droppedFragment); + fragmentedPendingQueue.removePacket(unrelatedPacket); + delete droppedFragment; + delete fragments; + delete unrelatedPacket; +} + +{ + TestOriginatorHandler invalidTimeoutHandler; + TestOriginatorPolicy invalidTimeoutPolicy; + TestCallback invalidTimeoutCallback; + invalidTimeoutPolicy.addbaResponseTimeout = 0; + Packet invalidTimeoutTrigger("invalidTimeoutTrigger"); + auto invalidTimeoutHeader = makeQosHeader(peer2, 1, SequenceNumberCyclic(30)); + bool threw = false; + try { + invalidTimeoutHandler.processAcknowledgedDataFrame(&invalidTimeoutTrigger, invalidTimeoutHeader, &invalidTimeoutPolicy, &invalidTimeoutCallback); + auto invalidRequest = dynamicPtrCast(invalidTimeoutCallback.managementHeader); + invalidTimeoutHandler.processTransmittedAddbaReq(invalidTimeoutCallback.managementPacket, invalidRequest, &invalidTimeoutPolicy, &invalidTimeoutCallback); + } + catch (cRuntimeError&) { + threw = true; + } + ASSERT(threw); +} + +// A direct IPacketQueue implementation without OMNeT++ signals cannot leave +// the index stale because ownership departures use the typed queue callback. +{ + TestOriginatorQosMacDataService customQueueDataService; + TestPacketQueue customQueue; + TestAckHandler customQueueAckHandler; + TestInProgressFrames customQueueInProgressFrames; + customQueueInProgressFrames.configure(&customQueueDataService, &customQueueAckHandler, &customQueue); + TestEdcaf customQueueEdcaf; + customQueueEdcaf.pendingQueue = &customQueue; + customQueueEdcaf.inProgressFrames = &customQueueInProgressFrames; + TestEdca customQueueEdca; + customQueueEdca.edcaf = &customQueueEdcaf; + TestHcf customQueueHcf; + customQueueHcf.configureEligibilityIndex(&customQueueEdca, &customQueueDataService); + customQueueHcf.observePendingQueue(&customQueue); + customQueueHcf.rebuildEligibilityIndex(); + auto customPacket = new Packet("customQueuePacket", makeShared(B(1))); + customQueueHcf.trackFrame(customPacket, AC_BK); + customQueue.enqueuePacket(customPacket); + if (customQueueHcf.getNumEligiblePendingFrames(AC_BK) != 1) + throw cRuntimeError("Custom queue enqueue was not indexed"); + auto customDequeuedPacket = customQueue.dequeuePacket(); + if (customDequeuedPacket != customPacket || customQueueHcf.getNumTrackedPendingFrames() != 0) + throw cRuntimeError("Custom queue departure callback left the index stale"); + delete customDequeuedPacket; +} + +// Top-level queue lifecycle signals maintain the same index incrementally; +// destructive drops are distinguished from ordinary departures. +{ + TestOriginatorQosMacDataService indexedDataService; + indexedDataService.setFrameEligibilityFunction([](const Packet *) { return true; }); + QosAckHandler indexedAckHandler; + TestInProgressFrames indexedInProgressFrames; + indexedInProgressFrames.configure(&indexedDataService, &indexedAckHandler); + TestEdcaf indexedEdcaf; + TestEdca indexedEdca; + indexedEdca.edcaf = &indexedEdcaf; + indexedEdcaf.inProgressFrames = &indexedInProgressFrames; + auto overflowQueue = createPacketQueue("indexedOverflowQueue", 1, "inet::queueing::PacketAtCollectionEndDropper"); + indexedEdcaf.pendingQueue = overflowQueue; + TestHcf indexedHcf; + indexedHcf.configureEligibilityIndex(&indexedEdca, &indexedDataService); + indexedHcf.observePendingQueue(overflowQueue); + indexedHcf.rebuildEligibilityIndex(); + auto retainedPacket = new Packet("indexedRetainedPacket", makeShared(B(1))); + indexedHcf.trackFrame(retainedPacket, AC_BK); + overflowQueue->pushPacket(retainedPacket, nullptr); + auto droppedArrival = new Packet("indexedDroppedArrival", makeShared(B(1))); + indexedHcf.trackFrame(droppedArrival, AC_BK); + overflowQueue->pushPacket(droppedArrival, nullptr); + if (indexedHcf.getNumTrackedPendingFrames() != 1 || indexedHcf.getNumEligiblePendingFrames(AC_BK) != 1) + throw cRuntimeError("Eligibility index did not account for overflow"); + auto selectedPacket = overflowQueue->dequeuePacket([](const Packet *) { return true; }); + if (selectedPacket != retainedPacket || indexedHcf.getNumTrackedPendingFrames() != 0) + throw cRuntimeError("Eligibility index did not account for selected dequeue"); + bool hasFrameAfterSelectedDequeue = indexedHcf.hasFrame(AC_BK); + if (hasFrameAfterSelectedDequeue) + throw cRuntimeError("Eligibility index reports a frame after selected dequeue"); + take(selectedPacket); + delete selectedPacket; + overflowQueue->callFinish(); + overflowQueue->deleteModule(); +} + // Predicate extraction follows each provider's scheduling policy instead of // the compound collection's gate-order enumeration. { @@ -478,6 +1704,367 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const gateModule->deleteModule(); leafQueue->deleteModule(); } + +// Every A-MSDU member is extracted through the scheduler, so provider +// accounting runs for all policy-selected subframes. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto firstSubframe = makeAggregatePacket("schedulerAggregateFirst", SequenceNumberCyclic(10)); + auto secondSubframe = makeAggregatePacket("schedulerAggregateSecond", SequenceNumberCyclic(11)); + firstQueue.enqueuePacket(firstSubframe); + secondQueue.enqueuePacket(secondSubframe); + TestPriorityScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(&scheduler, &scheduler); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { firstSubframe, secondSubframe }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + auto frames = service.extractFramesToTransmit(&compoundQueue); + if (frames == nullptr || frames->size() != 1 || scheduler.getNumProcessedPackets() != 2 || !firstQueue.isEmpty() || !secondQueue.isEmpty()) + throw cRuntimeError("A-MSDU extraction bypassed scheduler processing"); + service.release(frames->front()); + delete frames->front(); + delete frames; +} + +// A-MSDU validation discovers selected members by identity without enumerating +// an unrelated trailing backlog through the packet collection API. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue queue; + auto firstSubframe = makeAggregatePacket("backlogAggregateFirst", SequenceNumberCyclic(14)); + auto secondSubframe = makeAggregatePacket("backlogAggregateSecond", SequenceNumberCyclic(15)); + queue.packets = { firstSubframe, secondSubframe }; + for (int i = 0; i < 128; i++) + queue.packets.push_back(makeAggregatePacket("backlogAggregateTail", SequenceNumberCyclic(16 + i))); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { firstSubframe, secondSubframe }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + auto frames = service.extractFramesToTransmit(&queue); + ASSERT(frames != nullptr); + ASSERT(frames->size() == 1); + ASSERT(frames->front()->peekAtFront()->getAMsduPresent()); + ASSERT(queue.getNumPackets() == 128); + ASSERT(queue.numGetPacketCalls == 0); + service.release(frames->front()); + delete frames->front(); + delete frames; + for (auto packet : queue.packets) + delete packet; + queue.packets.clear(); +} + +// The built-in policy is anchored on the provider-selected candidate. Held +// enumeration members do not hide later compatible frames, while an oversized +// anchor falls back to ordinary dequeue without aggregating its followers. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber, int payloadLength) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(payloadLength))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue queue; + auto anchor = makeAggregatePacket("basicPolicyAnchor", SequenceNumberCyclic(20), 8); + auto held = makeAggregatePacket("basicPolicyHeld", SequenceNumberCyclic(21), 8); + auto compatible = makeAggregatePacket("basicPolicyCompatible", SequenceNumberCyclic(22), 8); + auto heldHeader = held->removeAtFront(); + heldHeader->setTid(2); + held->insertAtFront(heldHeader); + queue.packets = { anchor, held, compatible }; + TestBasicMsduAggregationPolicy policy; + policy.configure(B(4065)); + auto selected = policy.computeAggregateFrames(&queue, anchor, [held](const Packet *packet) { return packet != held; }); + ASSERT(selected != nullptr); + ASSERT(selected->size() == 2); + ASSERT(selected->at(0) == anchor); + ASSERT(selected->at(1) == compatible); + delete selected; + auto oversizedAnchor = makeAggregatePacket("basicPolicyOversizedAnchor", SequenceNumberCyclic(23), 64); + auto smallFollower1 = makeAggregatePacket("basicPolicySmallFollower1", SequenceNumberCyclic(24), 1); + auto smallFollower2 = makeAggregatePacket("basicPolicySmallFollower2", SequenceNumberCyclic(25), 1); + queue.packets = { oversizedAnchor, smallFollower1, smallFollower2 }; + policy.configure(B(30)); + ASSERT(policy.computeAggregateFrames(&queue, oversizedAnchor, [](const Packet *) { return true; }) == nullptr); + auto prefix = makeAggregatePacket("basicPolicyPrefix", SequenceNumberCyclic(26), 8); + auto middleAnchor = makeAggregatePacket("basicPolicyMiddleAnchor", SequenceNumberCyclic(27), 8); + auto suffix = makeAggregatePacket("basicPolicySuffix", SequenceNumberCyclic(28), 8); + queue.packets = { prefix, middleAnchor, suffix }; + policy.configure(B(4065)); + selected = policy.computeAggregateFrames(&queue, middleAnchor, [prefix](const Packet *packet) { return packet != prefix; }); + ASSERT(selected != nullptr); + ASSERT(selected->size() == 2); + ASSERT(selected->at(0) == middleAnchor); + ASSERT(selected->at(1) == suffix); + delete selected; + auto capacityAnchor = makeAggregatePacket("basicPolicyCapacityAnchor", SequenceNumberCyclic(29), 8); + auto oversizedMiddle = makeAggregatePacket("basicPolicyOversizedMiddle", SequenceNumberCyclic(30), 64); + auto smallSuffix = makeAggregatePacket("basicPolicySmallSuffix", SequenceNumberCyclic(31), 1); + queue.packets = { capacityAnchor, oversizedMiddle, smallSuffix }; + policy.configure(B(40)); + ASSERT(policy.computeAggregateFrames(&queue, capacityAnchor, [](const Packet *) { return true; }) == nullptr); + queue.packets.clear(); + for (auto packet : { anchor, held, compatible, oversizedAnchor, smallFollower1, smallFollower2, prefix, middleAnchor, suffix, capacityAnchor, oversizedMiddle, smallSuffix }) + delete packet; +} + +// Contract validation owns both already-extracted frames and an unexpected +// non-null provider result, so a caught provider error leaves only the frame +// which was never extracted in the queue. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestWrongDequeueQueue queue; + auto first = makeAggregatePacket("badProviderFirst", SequenceNumberCyclic(35)); + auto second = makeAggregatePacket("badProviderSecond", SequenceNumberCyclic(36)); + auto wrong = makeAggregatePacket("badProviderWrong", SequenceNumberCyclic(37)); + queue.packets = { first, second, wrong }; + queue.wrongPacket = wrong; + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { first, second }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + bool contractError = false; + try { + service.extractFramesToTransmit(&queue); + } + catch (cRuntimeError& error) { + contractError = std::string(error.what()).find("no longer available") != std::string::npos; + } + ASSERT(contractError); + ASSERT(queue.packets.size() == 1); + ASSERT(queue.packets.front() == second); + queue.packets.clear(); + delete second; +} + +// Returning an already-extracted pointer for a later exact dequeue is also a +// provider contract violation, but the cleanup guard must adopt it only once. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestRepeatedDequeueQueue queue; + auto first = makeAggregatePacket("repeatedProviderFirst", SequenceNumberCyclic(38)); + auto second = makeAggregatePacket("repeatedProviderSecond", SequenceNumberCyclic(39)); + queue.packets = { first, second }; + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { first, second }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + bool contractError = false; + try { + service.extractFramesToTransmit(&queue); + } + catch (cRuntimeError& error) { + contractError = std::string(error.what()).find("no longer available") != std::string::npos; + } + ASSERT(contractError); + ASSERT(queue.packets.size() == 1); + ASSERT(queue.packets.front() == second); + queue.packets.clear(); + delete second; +} + +// Reverse-priority scheduling may select an anchor after an eligible packet in +// collection enumeration. The built-in policy must never wrap and append that +// earlier packet after the anchor. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto earlierPacket = makeAggregatePacket("reverseEarlier", SequenceNumberCyclic(32)); + auto reverseAnchor = makeAggregatePacket("reverseAnchor", SequenceNumberCyclic(33)); + auto reverseSuffix = makeAggregatePacket("reverseSuffix", SequenceNumberCyclic(34)); + firstQueue.packets = { earlierPacket }; + secondQueue.packets = { reverseAnchor, reverseSuffix }; + TestPriorityScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, true); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(&scheduler, &scheduler); + auto candidate = compoundQueue.findPacket([](const Packet *) { return true; }); + ASSERT(candidate == reverseAnchor); + TestBasicMsduAggregationPolicy policy; + policy.configure(B(4065)); + auto selected = policy.computeAggregateFrames(&compoundQueue, candidate, [](const Packet *) { return true; }); + ASSERT(selected != nullptr); + ASSERT(selected->size() == 2); + ASSERT(selected->at(0) == reverseAnchor); + ASSERT(selected->at(1) == reverseSuffix); + delete selected; + firstQueue.packets.clear(); + secondQueue.packets.clear(); + delete earlierPacket; + delete reverseAnchor; + delete reverseSuffix; +} + +// A flow provider may expose an enumeration order different from its +// scheduling behavior. Candidate-aware aggregation still extracts every +// selected member through the provider and runs its processing exactly once. +{ + auto leafQueue = createPacketQueue("aggregationFlowLeaf"); + auto flowModule = cModuleType::get("inet.queueing.common.BackPressureBarrier")->create("aggregationFlow", this); + leafQueue->gate("out")->connectTo(flowModule->gate("in")); + flowModule->callInitialize(); + TestSignalListener flowListener; + flowModule->subscribe(packetPulledOutSignal, &flowListener); + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto firstSubframe = makeAggregatePacket("flowAggregateFirst", SequenceNumberCyclic(12)); + auto secondSubframe = makeAggregatePacket("flowAggregateSecond", SequenceNumberCyclic(13)); + leafQueue->pushPacket(firstSubframe, nullptr); + leafQueue->pushPacket(secondSubframe, nullptr); + auto flowCollection = check_and_cast(flowModule); + auto flowExtractor = check_and_cast(flowModule); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(flowCollection, flowExtractor); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { firstSubframe, secondSubframe }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + auto frames = service.extractFramesToTransmit(&compoundQueue); + if (frames == nullptr || frames->size() != 1 || !frames->front()->peekAtFront()->getAMsduPresent() || aggregationPolicy.numCalls != 1 || flowListener.numSignals != 2 || leafQueue->getNumPackets() != 0) + throw cRuntimeError("A-MSDU aggregation bypassed PacketFlowBase extraction"); + service.release(frames->front()); + delete frames->front(); + delete frames; + flowModule->unsubscribe(packetPulledOutSignal, &flowListener); + flowModule->callFinish(); + flowModule->deleteModule(); + leafQueue->callFinish(); + leafQueue->deleteModule(); +} + +// End-to-end compound extraction uses the provider's reverse-priority choice +// as the aggregate anchor while leaving an ineligible higher-level frame held. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto heldPacket = makeAggregatePacket("compoundHeldPacket", SequenceNumberCyclic(1)); + auto selectedPacket = makeAggregatePacket("compoundSelectedPacket", SequenceNumberCyclic(2)); + auto companionPacket = makeAggregatePacket("compoundCompanionPacket", SequenceNumberCyclic(3)); + firstQueue.packets = { heldPacket }; + secondQueue.packets = { selectedPacket, companionPacket }; + TestPriorityScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, true); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(&scheduler, &scheduler); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { selectedPacket, companionPacket }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + service.setFrameEligibilityFunction([heldPacket](const Packet *packet) { return packet != heldPacket; }); + auto frames = service.extractFramesToTransmit(&compoundQueue); + ASSERT(frames != nullptr); + ASSERT(frames->size() == 1); + ASSERT(frames->front()->peekAtFront()->getAMsduPresent()); + ASSERT(aggregationPolicy.numCalls == 1); + ASSERT(firstQueue.getNumPackets() == 1); + ASSERT(secondQueue.isEmpty()); + service.release(frames->front()); + delete frames->front(); + delete frames; + firstQueue.removePacket(heldPacket); + delete heldPacket; +} + +// A-MSDU aggregation may select multiple WRR members, but each exact dequeue +// must still update WRR accounting. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(8))); + packet->insertAtBack(makeShared()); + return packet; + }; + TestPacketQueue firstQueue; + TestPacketQueue secondQueue; + auto selectedPacket = makeAggregatePacket("wrrAggregateSelectedPacket", SequenceNumberCyclic(3)); + auto sameInputPacket = makeAggregatePacket("wrrAggregateSameInputPacket", SequenceNumberCyclic(4)); + auto otherInputPacket = makeAggregatePacket("wrrAggregateOtherInputPacket", SequenceNumberCyclic(5)); + firstQueue.packets = { selectedPacket, sameInputPacket }; + secondQueue.packets = { otherInputPacket }; + TestWrrScheduler scheduler; + scheduler.configure({ &firstQueue, &secondQueue }, { 1, 1 }, { 1, 1 }); + TestCompoundPacketQueue compoundQueue; + compoundQueue.configure(&scheduler, &scheduler); + ASSERT(compoundQueue.getPacket(0) == selectedPacket); + TestSelectedMsduAggregationPolicy aggregationPolicy; + aggregationPolicy.selectedPackets = { selectedPacket, sameInputPacket }; + TestOriginatorQosMacDataService service; + service.enableMsduAggregationPolicy(&aggregationPolicy); + auto frames = service.extractFramesToTransmit(&compoundQueue); + ASSERT(frames != nullptr); + ASSERT(frames->size() == 1); + ASSERT(frames->front()->peekAtFront()->getAMsduPresent()); + ASSERT(aggregationPolicy.numCalls == 1); + ASSERT(firstQueue.isEmpty()); + ASSERT(secondQueue.getNumPackets() == 1); + ASSERT(scheduler.getNumProcessedPackets() == 2); + ASSERT(scheduler.getBucket(0) == 0); + ASSERT(scheduler.getBucket(1) == 1); + service.release(frames->front()); + delete frames->front(); + delete frames; + secondQueue.removePacket(otherInputPacket); + delete otherInputPacket; +} // A real queue with a downstream collector predicate-dequeues a non-front // packet, records its residence and semantic pull once, and animates it to the // collector while retaining the front packet. @@ -596,6 +2183,27 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const gateModule->deleteModule(); leafQueueModule->deleteModule(); } + +// Ordinary queue overflow must not trigger channel-access re-evaluation, while +// cancelling a dropped setup transaction must immediately release held traffic. +{ + TestHcf droppedSetupHcf; + Packet droppedSetupPacket("droppedSetupPacket", makeShared(B(1))); + droppedSetupHcf.dropPacket(&droppedSetupPacket); + ASSERT(droppedSetupHcf.numProcessedDroppedSetupFrames == 1); + ASSERT(droppedSetupHcf.numResumedEligibleChannelAccess == 0); + droppedSetupHcf.droppedSetupCancelled = true; + droppedSetupHcf.dropPacket(&droppedSetupPacket); + ASSERT(droppedSetupHcf.numProcessedDroppedSetupFrames == 2); + ASSERT(droppedSetupHcf.numResumedEligibleChannelAccess == 1); + droppedSetupHcf.removePacket(&droppedSetupPacket); + ASSERT(droppedSetupHcf.numProcessedDroppedSetupFrames == 3); + ASSERT(droppedSetupHcf.numResumedEligibleChannelAccess == 2); + droppedSetupHcf.dequeuePacket(&droppedSetupPacket); + ASSERT(droppedSetupHcf.numProcessedDroppedSetupFrames == 3); + ASSERT(droppedSetupHcf.numResumedEligibleChannelAccess == 2); +} + // The typed queue callback covers every non-destructive ownership departure, // including bulk removal, exactly once at the queue boundary. { @@ -943,5 +2551,6 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const bufferModule->deleteModule(); } EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; + %contains: stdout ADDBA transaction and negotiated SSN integrity checks passed. From e6b9d9c02e098ee4251f0b6a5ac2c20db7e1dfaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:03 +0200 Subject: [PATCH 30/56] ieee80211: reset recipient Block Ack state on renegotiation Treat an accepted ADDBA request for an existing peer and TID as a replacement agreement. Cancel teardown state belonging to the old agreement, install the negotiated parameters, and reset the receive reordering window before frames are admitted under the new agreement. Replay the cached response for a duplicate request without resetting receive state, and preserve the current agreement when renegotiation is rejected. Emit distinct agreement-added, changed, and deleted events so the HCF lifecycle remains observable. Add recipient lifecycle coverage together with a transactional QoS example and its focused fingerprint entry. --- WHATSNEW | 44 +- examples/wireless/qos/omnetpp.ini | 23 + .../mac/blockack/RecipientBlockAckAgreement.h | 3 - .../RecipientBlockAckAgreementHandler.cc | 118 +- .../RecipientBlockAckAgreementHandler.h | 16 +- .../blockackreordering/BlockAckReordering.cc | 10 +- .../blockackreordering/BlockAckReordering.h | 3 +- .../mac/blockackreordering/ReceiveBuffer.cc | 11 +- .../mac/blockackreordering/ReceiveBuffer.h | 2 +- .../IRecipientBlockAckAgreementHandler.h | 11 +- .../contract/IRecipientQosMacDataService.h | 12 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 54 +- .../ieee80211/mac/coordinationfunction/Hcf.h | 8 +- .../mac/coordinationfunction/Hcf.ned | 2 + .../recipient/RecipientQosMacDataService.cc | 30 +- .../recipient/RecipientQosMacDataService.h | 4 +- tests/fingerprint/examples.csv | 11 +- tests/unit/Ieee80211AddbaTransaction_1.test | 1417 ++++++++++++++++- 18 files changed, 1653 insertions(+), 126 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 0da1dad4927..5d5237316f4 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -43,15 +43,20 @@ Notable backward incompatible changes are the following: implementations of IPacketExtractor must replace selected-pointer extraction with findPacket(predicate) and dequeuePacket(predicate); predicates may be evaluated repeatedly and must be stable and side-effect free throughout one - logical selection. WrrScheduler, - LabelScheduler, and PriorityScheduler accept ordinary IPassivePacketSource - inputs; they require IPacketCollection only when collection/aggregate access is - used and IPacketExtractor only when predicate extraction is used, reporting the - unsupported operation lazily. + logical selection. WrrScheduler, LabelScheduler, and PriorityScheduler accept + ordinary IPassivePacketSource inputs; they require IPacketCollection only when + collection/aggregate access is used and IPacketExtractor only when predicate + extraction is used, reporting the unsupported operation lazily. PriorityScheduler aggregate queries no longer return -1 when an input lacks IPacketCollection; getNumPackets() and getTotalLength() now report that unsupported operation with cRuntimeError. Update callers that treated -1 as an unknown aggregate size, or connect collection-capable providers. + A-MSDU policies now receive the provider-selected anchor and frame-eligibility + predicate. Additional policy-selected members are removed through exact + predicate dequeues, preserving scheduler/flow accounting without requiring + collection enumeration order to match scheduling order. + BasicMsduAggregationPolicy conservatively considers only collection members + after the anchor and never bypasses an earlier blocked same-flow member. IPacketBuffer::ICallback also provides an optional handlePacketDropped() notification. Shared buffers invoke it only after all victims selected by one overload operation have been detached from their owning queues. Compound @@ -59,6 +64,35 @@ Notable backward incompatible changes are the following: nested compound queues exactly once. PacketBuffer rejects packets owned by a cPacketQueue whose owner cannot participate in the buffer callback contract. +3. Block Ack DELBA agreement ownership + + IOriginatorBlockAckAgreementHandler::processReceivedDelba(), + IOriginatorBlockAckAgreementHandler::processTransmittedDelba(), and + IRecipientBlockAckAgreementHandler::processReceivedDelba() now return the + terminated agreement as a unique_ptr. Originator ADDBA response processing + now returns a typed outcome containing the established agreement and, when + local policy vetoes a successful response, its immediately terminated local + agreement and a best-effort initiator DELBA for Hcf to enqueue after emitting + the Added and Deleted signals. Such DELBAs carry the ADDBA transaction identity + and ordinary data continues with Normal Ack. They remain eligible through the + final fragment; aborting one fragment cancels its siblings. After retry backoff, + a replacement ADDBA setup invalidates an older queued DELBA, so an unreported + disposal cannot suppress setup indefinitely and a delayed frame cannot + terminate a newer peer/TID agreement. The replacement setup reports that + obsolete identity so Hcf also removes all of its queued or in-progress packets. + Originator DELBA transmission handlers now receive the full Packet so this + identity is retained. A transaction-tagged initiator DELBA remains eligible + across MAC retries and is retired only when its final fragment is acknowledged + or the transaction is terminally aborted. Custom handler implementations and + callers must adopt processAcknowledgedDelba() and the boolean + processAbortedDelba() outcome; processTransmittedDelba() still returns the + agreement removed by an untagged DELBA, or null when none was removed. + + OriginatorBlockAckAgreementPolicy now exposes the new `addbaResponseTimeout` + and `addbaRetryBackoff` NED parameters. C++ implementations of + IOriginatorBlockAckAgreementPolicy must replace computeAddbaFailureTimeout() + with getAddbaResponseTimeout() and implement computeAddbaRetryBackoff(). + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/examples/wireless/qos/omnetpp.ini b/examples/wireless/qos/omnetpp.ini index 887e0683fc6..dccf94b1347 100644 --- a/examples/wireless/qos/omnetpp.ini +++ b/examples/wireless/qos/omnetpp.ini @@ -85,3 +85,26 @@ extends = MacQos # radio medium *.radioMedium.sameTransmissionStartTimeCheck = "ignore" + +[Config MacQosWithTransactionalBlockAck] +description = "Exercises successful and timed-out ADDBA transactions" +extends = MacQosWithoutAggregation +sim-time-limit = 3s + +# Use one voice flow so each wireless hop has a single peer/TID data flow. +*.cliHost.numApps = 1 +*.cliHost.app[0].destPort = 5000 +*.cliHost.app[0].packetName = "TransactionalBlockAck" +*.cliHost.app[0].startTime = 1s +*.cliHost.app[0].stopTime = 2s +*.cliHost.app[0].sendInterval = 10ms +*.srvHost.numApps = 1 +*.srvHost.app[0].localPort = 5000 + +# The client-to-AP transaction succeeds. The AP-to-server transaction times +# out, because the server deliberately does not support Block Ack. +*.cliHost.wlan[0].mac.hcf.isBlockAckSupported = true +*.ap.wlan[0].mac.hcf.isBlockAckSupported = true +*.srvHost.wlan[0].mac.hcf.isBlockAckSupported = false +**.mac.hcf.originatorAckPolicy.blockAckReqThreshold = 2 +**.mac.hcf.originatorBlockAckAgreementPolicy.addbaResponseTimeout = 250ms diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 40a4186fead..98f9ba589c4 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -21,7 +21,6 @@ class INET_API RecipientBlockAckAgreement : public cObject SequenceNumberCyclic startingSequenceNumber; int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; - bool isAddbaResponseSent = false; simtime_t expirationTime = -1; public: @@ -35,7 +34,6 @@ class INET_API RecipientBlockAckAgreement : public cObject virtual int getBufferSize() const { return bufferSize; } virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } - virtual void addbaResposneSent() { isAddbaResponseSent = true; } virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } virtual simtime_t getExpirationTime() { return expirationTime; } friend std::ostream& operator<<(std::ostream& os, const RecipientBlockAckAgreement& agreement); @@ -45,4 +43,3 @@ class INET_API RecipientBlockAckAgreement : public cObject } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 6219ac77140..8956059d80c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -64,29 +64,6 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb scheduleInactivityTimer(agreementHandlerCallback); } -// -// An originator that intends to use the Block Ack mechanism for the transmission of QoS data frames to an -// intended recipient should first check whether the intended recipient STA is capable of participating in Block -// Ack mechanism by discovering and examining its Delayed Block Ack and Immediate Block Ack capability -// bits. If the intended recipient STA is capable of participating, the originator sends an ADDBA Request frame -// indicating the TID for which the Block Ack is being set up. -// -RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::addAgreement(const Ptr& addbaReq) -{ - MacAddress originatorAddr = addbaReq->getTransmitterAddress(); - auto id = std::make_pair(originatorAddr, addbaReq->getTid()); - auto it = blockAckAgreements.find(id); - if (it == blockAckAgreements.end()) { - RecipientBlockAckAgreement *agreement = new RecipientBlockAckAgreement(originatorAddr, addbaReq->getTid(), addbaReq->getStartingSequenceNumber(), addbaReq->getBufferSize(), addbaReq->getBlockAckTimeoutValue()); - blockAckAgreements[id] = agreement; - EV_DETAIL << "Block Ack Agreement is added with the following parameters: " << *agreement << endl; - return agreement; - } - else - // TODO update? - return it->second; -} - // // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the peer STA with the Reason Code // field set to TIMEOUT and shall issue a MLME-DELBA.indication primitive with the ReasonCode @@ -102,10 +79,14 @@ const Ptr RecipientBlockAckAgreementHandler::buildDelba(MacAddre return delba; } -const Ptr RecipientBlockAckAgreementHandler::buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) +const Ptr RecipientBlockAckAgreementHandler::buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, bool accepted) { auto addbaResponse = makeShared(); addbaResponse->setReceiverAddress(addbaRequest->getTransmitterAddress()); + // IEEE Std 802.11-2024, 9.6.4.2 and 11.5.2.3: the response copies the + // request's Dialog Token and reports whether the agreement was accepted. + addbaResponse->setDialogToken(addbaRequest->getDialogToken()); + addbaResponse->setStatusCode(accepted ? 0 : 1); // 1: REFUSED_REASON_UNSPECIFIED // The Block Ack Policy subfield is set to 1 for immediate Block Ack and 0 for delayed Block Ack. Tid tid = addbaRequest->getTid(); addbaResponse->setTid(tid); @@ -116,27 +97,17 @@ const Ptr RecipientBlockAckAgreementHandler::buildAddbaR return addbaResponse; } -void RecipientBlockAckAgreementHandler::updateAgreement(const Ptr& addbaResponse) -{ - auto id = std::make_pair(addbaResponse->getReceiverAddress(), addbaResponse->getTid()); - auto it = blockAckAgreements.find(id); - if (it != blockAckAgreements.end()) { - RecipientBlockAckAgreement *agreement = it->second; - agreement->addbaResposneSent(); - } - else - throw cRuntimeError("Agreement is not found"); -} - -void RecipientBlockAckAgreementHandler::terminateAgreement(MacAddress originatorAddr, Tid tid) +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::removeAgreement(MacAddress originatorAddr, Tid tid) { auto agreementId = std::make_pair(originatorAddr, tid); + lastAddbaResponses.erase(agreementId); auto it = blockAckAgreements.find(agreementId); if (it != blockAckAgreements.end()) { - RecipientBlockAckAgreement *agreement = it->second; + auto agreement = it->second; blockAckAgreements.erase(it); - delete agreement; + return agreement; } + return nullptr; } RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getAgreement(Tid tid, MacAddress originatorAddr) @@ -146,35 +117,69 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getAgreement(Tid return it != blockAckAgreements.end() ? it->second : nullptr; } -void RecipientBlockAckAgreementHandler::processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { - updateAgreement(addbaResp); - scheduleInactivityTimer(callback); + EV_INFO << "Processing Addba Request from " << addbaRequest->getTransmitterAddress() << endl; + bool accepted = addbaRequest->getDialogToken() != 0 && blockAckAgreementPolicy->isAddbaReqAccepted(addbaRequest); + EV_DETAIL << "Building Addba Response" << endl; + auto addbaResponse = buildAddbaResponse(addbaRequest, blockAckAgreementPolicy, accepted); + auto id = std::make_pair(addbaRequest->getTransmitterAddress(), addbaRequest->getTid()); + bool hadAgreement = blockAckAgreements.find(id) != blockAckAgreements.end(); + // Keep the immutable response body that corresponds to the most recently + // processed request identity. This is response replay state, not a second + // duplicate detector; RecipientQosMacDataService remains authoritative. + if (accepted || hadAgreement) + lastAddbaResponses[id] = addbaResponse; + else + lastAddbaResponses.erase(id); + auto addbaResponsePacket = new Packet("AddbaResponse", addbaResponse); + RecipientBlockAckAgreement *agreement = nullptr; + if (accepted) { + // IEEE Std 802.11-2024, 10.25.2 and 11.5.2.3: accepting the + // request establishes or modifies the recipient agreement when the + // successful response is formed; transmission is not a state gate. + agreement = new RecipientBlockAckAgreement(addbaRequest->getTransmitterAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaResponse->getBufferSize(), addbaResponse->getBlockAckTimeoutValue()); + auto it = blockAckAgreements.find(id); + if (it != blockAckAgreements.end()) { + delete it->second; + it->second = agreement; + } + else + blockAckAgreements[id] = agreement; + scheduleInactivityTimer(agreementHandlerCallback); + } + procedureCallback->processMgmtFrame(addbaResponsePacket, addbaResponse); + return agreement; } -void RecipientBlockAckAgreementHandler::processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) +void RecipientBlockAckAgreementHandler::processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) { - EV_INFO << "Processing Addba Request from " << addbaRequest->getTransmitterAddress() << endl; - if (blockAckAgreementPolicy->isAddbaReqAccepted(addbaRequest)) { - EV_DETAIL << "Addba Request has been accepted. Creating a new Block Ack Agreement." << endl; - auto agreement = addAgreement(addbaRequest); - EV_DETAIL << "Agreement is added with the following parameters: " << *agreement << endl; - EV_DETAIL << "Building Addba Response" << endl; - auto addbaResponse = buildAddbaResponse(addbaRequest, blockAckAgreementPolicy); - auto addbaResponsePacket = new Packet("AddbaResponse", addbaResponse); - callback->processMgmtFrame(addbaResponsePacket, addbaResponse); + auto agreement = getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()); + if (agreement != nullptr) { + // IEEE Std 802.11-2024, 10.3.2.14.3 normally discards duplicate management bodies. + // Replaying the already generated response is an explicit robustness/model + // extension; it does not modify the agreement, reorder window, or inactivity timer. + auto id = std::make_pair(addbaRequest->getTransmitterAddress(), addbaRequest->getTid()); + auto it = lastAddbaResponses.find(id); + if (it == lastAddbaResponses.end()) + return; + // Copy the immutable snapshot so outbound sequence assignment uses COW + // and cannot modify the cached body used by a later retransmission. + auto addbaResponse = staticPtrCast(it->second->dupShared()); + procedureCallback->processMgmtFrame(new Packet("AddbaResponse", addbaResponse), addbaResponse); } } -void RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) +std::unique_ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) { - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + return std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); } -void RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) +std::unique_ptr RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) { if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - terminateAgreement(delba->getReceiverAddress(), delba->getTid()); + return std::unique_ptr(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + return nullptr; } RecipientBlockAckAgreementHandler::~RecipientBlockAckAgreementHandler() @@ -185,4 +190,3 @@ RecipientBlockAckAgreementHandler::~RecipientBlockAckAgreementHandler() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index 1e65a1557ef..f642c0a06b9 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -26,23 +26,22 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre { protected: std::map, RecipientBlockAckAgreement *> blockAckAgreements; + std::map, Ptr> lastAddbaResponses; protected: - virtual void terminateAgreement(MacAddress originatorAddr, Tid tid); - virtual RecipientBlockAckAgreement *addAgreement(const Ptr& addbaReq); - virtual void updateAgreement(const Ptr& addbaResponse); - virtual const Ptr buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy); + virtual RecipientBlockAckAgreement *removeAgreement(MacAddress originatorAddr, Tid tid); + virtual const Ptr buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, bool accepted); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); virtual simtime_t computeEarliestExpirationTime(); virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback); public: virtual ~RecipientBlockAckAgreementHandler(); - virtual void processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) override; - virtual void processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) override; + virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) override; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) override; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) override; - virtual void processTransmittedDelba(const Ptr& delba) override; + virtual std::unique_ptr processTransmittedDelba(const Ptr& delba) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; @@ -52,4 +51,3 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 47837ead38a..1dbff9f3f17 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -185,18 +185,17 @@ ReceiveBuffer *BlockAckReordering::createReceiveBufferIfNecessary(RecipientBlock return it->second; } -void BlockAckReordering::processReceivedDelba(const Ptr& delba) +std::vector BlockAckReordering::resetReceiveBuffer(Tid tid, MacAddress originatorAddr) { - Tid tid = delba->getTid(); - MacAddress originatorAddr = delba->getTransmitterAddress(); + std::vector frames; auto id = std::make_pair(tid, originatorAddr); auto it = receiveBuffers.find(id); if (it != receiveBuffers.end()) { + frames = it->second->extractFrames(); delete it->second; receiveBuffers.erase(it); } - else - EV_DETAIL << "Receive buffer is not found" << endl; + return frames; } void BlockAckReordering::passedUp(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber) @@ -244,4 +243,3 @@ BlockAckReordering::~BlockAckReordering() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index a50606f8a53..014f2c0a318 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -43,7 +43,7 @@ class INET_API BlockAckReordering public: virtual ~BlockAckReordering(); - void processReceivedDelba(const Ptr& delba); + std::vector resetReceiveBuffer(Tid tid, MacAddress originatorAddr); ReorderBuffer processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader); ReorderBuffer processReceivedBlockAckReq(RecipientBlockAckAgreement *agreement, const Ptr& blockAckReq); }; @@ -52,4 +52,3 @@ class INET_API BlockAckReordering } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc index 1dd9c959c8f..3bf4115a4b1 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc @@ -77,6 +77,16 @@ void ReceiveBuffer::removeFrame(SequenceNumberCyclic sequenceNumber) throw cRuntimeError("Unknown sequence number: %d", sequenceNumber.get()); } +ReceiveBuffer::Fragments ReceiveBuffer::extractFrames() +{ + Fragments frames; + for (auto& [sequenceNumber, fragments] : buffer) + frames.insert(frames.end(), fragments.begin(), fragments.end()); + buffer.clear(); + length = 0; + return frames; +} + ReceiveBuffer::~ReceiveBuffer() { for (auto fragments : buffer) { @@ -87,4 +97,3 @@ ReceiveBuffer::~ReceiveBuffer() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h index a499465d198..84e8fd5b6c5 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h @@ -37,6 +37,7 @@ class INET_API ReceiveBuffer bool insertFrame(Packet *dataPacket, const Ptr& dataHeader); void dropFramesUntil(SequenceNumberCyclic sequenceNumber); void removeFrame(SequenceNumberCyclic sequenceNumber); + Fragments extractFrames(); const ReorderBuffer& getBuffer() { return buffer; } int getLength() { return length; } @@ -50,4 +51,3 @@ class INET_API ReceiveBuffer } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 357745c3ddb..4eba49314c1 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -8,6 +8,8 @@ #ifndef __INET_IRECIPIENTBLOCKACKAGREEMENTHANDLER_H #define __INET_IRECIPIENTBLOCKACKAGREEMENTHANDLER_H +#include + #include "inet/linklayer/common/MacAddress.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" @@ -24,10 +26,10 @@ class INET_API IRecipientBlockAckAgreementHandler public: virtual ~IRecipientBlockAckAgreementHandler() {} - virtual void processTransmittedAddbaResp(const Ptr& addbaResp, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *callback) = 0; - virtual void processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; - virtual void processTransmittedDelba(const Ptr& delba) = 0; + virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; + virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) = 0; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; + virtual std::unique_ptr processTransmittedDelba(const Ptr& delba) = 0; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; @@ -38,4 +40,3 @@ class INET_API IRecipientBlockAckAgreementHandler } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h index 8a3ff4c0b7c..108a499727f 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h @@ -18,6 +18,14 @@ namespace ieee80211 { class INET_API IRecipientQosMacDataService { public: + // A duplicate has already been consumed and must not be processed as a + // management body; the coordination function may apply a subtype-specific + // response rule using the duplicate flag. + struct ManagementFrameReceptionResult { + std::vector completeFrames; + bool duplicate = false; + }; + static simsignal_t packetDefragmentedSignal; static simsignal_t packetDeaggregatedSignal; @@ -26,11 +34,11 @@ class INET_API IRecipientQosMacDataService virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) = 0; - virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) = 0; + virtual ManagementFrameReceptionResult managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) = 0; + virtual void resetBlockAckReordering(Tid tid, MacAddress originatorAddr) = 0; }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 010529ff44b..3f8cefd9e20 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -32,6 +32,7 @@ using namespace inet::physicallayer; simsignal_t Hcf::edcaCollisionDetectedSignal = cComponent::registerSignal("edcaCollisionDetected"); simsignal_t Hcf::blockAckAgreementAddedSignal = cComponent::registerSignal("blockAckAgreementAdded"); simsignal_t Hcf::blockAckAgreementDeletedSignal = cComponent::registerSignal("blockAckAgreementDeleted"); +simsignal_t Hcf::blockAckAgreementChangedSignal = cComponent::registerSignal("blockAckAgreementChanged"); Define_Module(Hcf); @@ -531,8 +532,9 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const PtrdataFrameReceived(packet, dataHeader, recipientBlockAckAgreementHandler)); } else if (auto mgmtHeader = dynamicPtrCast(header)) { - sendUp(recipientDataService->managementFrameReceived(packet, mgmtHeader)); - recipientProcessReceivedManagementFrame(mgmtHeader); + auto receptionResult = recipientDataService->managementFrameReceived(packet, mgmtHeader); + sendUp(receptionResult.completeFrames); + recipientProcessReceivedManagementFrame(mgmtHeader, receptionResult.duplicate); } else { // TODO else if (auto ctrlFrame = dynamic_cast(frame)) sendUp(recipientDataService->controlFrameReceived(packet, header, recipientBlockAckAgreementHandler)); @@ -555,13 +557,27 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header) +void Hcf::recipientProcessReceivedManagementFrame(const Ptr& header, bool duplicate) { + if (duplicate) { + if (recipientBlockAckAgreementHandler) { + if (auto addbaRequest = dynamicPtrCast(header)) + recipientBlockAckAgreementHandler->processDuplicateAddbaRequest(addbaRequest, this); + } + return; + } if (recipientBlockAckAgreementHandler && originatorBlockAckAgreementHandler) { if (auto addbaRequest = dynamicPtrCast(header)) { - recipientBlockAckAgreementHandler->processReceivedAddbaRequest(addbaRequest, recipientBlockAckAgreementPolicy, this); - auto agreement = recipientBlockAckAgreementHandler->getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()); - emit(blockAckAgreementAddedSignal, agreement); + bool hadAgreement = recipientBlockAckAgreementHandler->getAgreement(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()) != nullptr; + auto agreement = recipientBlockAckAgreementHandler->processReceivedAddbaRequest(addbaRequest, recipientBlockAckAgreementPolicy, this, this); + if (agreement != nullptr) { + if (hadAgreement) { + recipientDataService->resetBlockAckReordering(addbaRequest->getTid(), addbaRequest->getTransmitterAddress()); + emit(blockAckAgreementChangedSignal, agreement); + } + else + emit(blockAckAgreementAddedSignal, agreement); + } } else if (auto addbaResp = dynamicPtrCast(header)) { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(addbaResp->getTransmitterAddress(), addbaResp->getTid()); @@ -584,10 +600,14 @@ void Hcf::recipientProcessReceivedManagementFrame(const Ptr(header)) { + // IEEE Std 802.11-2024, 9.4.1.16, 10.25.4, and 11.5.3.3: + // Initiator selects the agreement direction; the transmitter is the peer. if (delba->getInitiator()) { - auto agreement = recipientBlockAckAgreementHandler->getAgreement(delba->getTid(), delba->getReceiverAddress()); - emit(blockAckAgreementDeletedSignal, agreement); - recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); + auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); + if (agreement != nullptr) { + recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getTransmitterAddress()); + emit(blockAckAgreementDeletedSignal, agreement.get()); + } } else { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid()); @@ -717,8 +737,8 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< if (originatorBlockAckAgreementHandler) originatorBlockAckAgreementHandler->processTransmittedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); } - else if (auto addbaResp = dynamicPtrCast(mgmtHeader)) - recipientBlockAckAgreementHandler->processTransmittedAddbaResp(addbaResp, this); + else if (dynamicPtrCast(mgmtHeader)) + ; // Recipient agreement was established when the successful response was formed. else if (auto delba = dynamicPtrCast(mgmtHeader)) { if (delba->getInitiator()) { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getReceiverAddress(), delba->getTid()); @@ -728,8 +748,16 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) emit(blockAckAgreementDeletedSignal, agreement.get()); } - else - recipientBlockAckAgreementHandler->processTransmittedDelba(delba); + else { + auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(delba); + if (agreement != nullptr) { + // IEEE Std 802.11-2024, 10.25.4 and 11.5.3.5: recipient + // resources are released whether the recipient transmitted or + // received DELBA. The reorder window is such a resource. + recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getReceiverAddress()); + emit(blockAckAgreementDeletedSignal, agreement.get()); + } + } } else ; // TODO other mgmt frames if needed } diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 7bf6631e302..fc9b5d49eff 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -32,7 +32,6 @@ #include "inet/linklayer/ieee80211/mac/contract/ITx.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h" -#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" #include "inet/linklayer/ieee80211/mac/originator/QosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" @@ -55,6 +54,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: static simsignal_t edcaCollisionDetectedSignal; static simsignal_t blockAckAgreementAddedSignal; static simsignal_t blockAckAgreementDeletedSignal; + static simsignal_t blockAckAgreementChangedSignal; protected: Ieee80211Mac *mac = nullptr; @@ -133,6 +133,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual bool processDroppedBlockAckSetupFrame(Packet *packet); virtual bool processDroppedBlockAckTeardownFrame(Packet *packet); virtual bool isPacketReferencedByCurrentFrameSequence(const Packet *packet) const; + virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; + virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); virtual void trackPendingFrame(Packet *packet, AccessCategory accessCategory); virtual void untrackPendingFrame(const Packet *packet); virtual void rebuildPendingFrameEligibility(); @@ -141,7 +143,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: // Recipient virtual void recipientProcessReceivedFrame(Packet *packet, const Ptr& header); virtual void recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header); - virtual void recipientProcessReceivedManagementFrame(const Ptr& header); + virtual void recipientProcessReceivedManagementFrame(const Ptr& header, bool duplicate); virtual void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header); // Originator @@ -176,8 +178,6 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr& responseHeader, Packet *receivedPacket, const Ptr& receivedHeader) override; virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override; - virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; - virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); // IProcedureCallback virtual void scheduleInactivityTimer(simtime_t timeout) override; virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned index 31b8bfc7047..8b07814b1a6 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.ned @@ -54,6 +54,7 @@ module Hcf extends Module like IHcf @signal[datarateSelected](type=double); @signal[blockAckAgreementAdded]; @signal[blockAckAgreementDeleted]; + @signal[blockAckAgreementChanged]; @statistic[packetSentToPeer](title="packets sent"; record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @statistic[packetSentToPeerUnicast](title="packets sent: unicast"; source=ieee80211Unicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @statistic[packetSentToPeerMulticast](title="packets sent: multicast"; source=ieee80211Multicast(packetSentToPeer); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); @@ -79,6 +80,7 @@ module Hcf extends Module like IHcf @statistic[datarateSelected](title="datarates selected"; record=vector; interpolationmode=none); @statistic[blockAckAgreementAdded](title="added block ack agreements"; record=count); @statistic[blockAckAgreementDeleted](title="deleted block ack agreements"; record=count); + @statistic[blockAckAgreementChanged](title="changed block ack agreements"; record=count); @statistic[blockAckAgreementActive](title="active block ack agreements"; source=warmup(count(blockAckAgreementAdded)-count(blockAckAgreementDeleted)); record=vector; interpolationmode=sample-hold; autoWarmupFilter=false); submodules: diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 77e677bb147..00dbfe970e0 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -30,6 +30,21 @@ void RecipientQosMacDataService::initialize() blockAckReordering = new BlockAckReordering(); } +void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress originatorAddr) +{ + Enter_Method("resetBlockAckReordering"); + if (blockAckReordering) { + auto droppedFrames = blockAckReordering->resetReceiveBuffer(tid, originatorAddr); + for (auto packet : droppedFrames) { + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } +} + Packet *RecipientQosMacDataService::defragment(std::vector completeFragments) { for (auto fragment : completeFragments) { @@ -112,25 +127,25 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data return deaggregatedFrames; } -std::vector RecipientQosMacDataService::managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) +IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataService::managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) { Enter_Method("managementFrameReceived"); take(mgmtPacket); // TODO MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption - if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) - return std::vector(); + if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) { + delete mgmtPacket; + return { {}, true }; + } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); } - if (auto delba = dynamicPtrCast(mgmtHeader)) - blockAckReordering->processReceivedDelba(delba); // TODO Defrag, MSDU Integrity, Replay Detection, RX MSDU Rate Limiting if (dynamicPtrCast(mgmtHeader)) { delete mgmtPacket; - return std::vector(); + return { {}, false }; } else - return std::vector({ mgmtPacket }); + return { { mgmtPacket }, false }; } std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) @@ -196,4 +211,3 @@ RecipientQosMacDataService::~RecipientQosMacDataService() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index e4b8ee28865..cb27ff09543 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -48,11 +48,11 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, public: virtual std::vector dataFrameReceived(Packet *dataPacket, const Ptr& dataHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; virtual std::vector controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) override; - virtual std::vector managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) override; + virtual ManagementFrameReceptionResult managementFrameReceived(Packet *mgmtPacket, const Ptr& mgmtHeader) override; + virtual void resetBlockAckReordering(Tid tid, MacAddress originatorAddr) override; }; } /* namespace ieee80211 */ } /* namespace inet */ #endif - diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 696ba2cb33b..640e3c86289 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -654,11 +654,12 @@ /examples/wireless/power/, -f omnetpp.ini -c General -r 0, 100s, 6fae-d558/tplx;b8ea-b2fc/~tNl;5ea8-2cea/~tND, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, f252-8a4b/tplx;481d-4747/~tNl;5962-e45d/~tND;6097-a429/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 31b0-6212/tplx;82ec-9fde/~tNl;540d-c0a4/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, acd6-0108/tplx;b339-294a/~tNl;75d4-11f9/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 9ece-fbfb/tplx;c1af-29ff/~tNl;8fe3-d7a6/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, d094-b008/tplx;173a-e3fb/~tNl;7147-4f5b/~tND;08b5-d005/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, f252-8a4b/tplx;481d-4747/~tNl;7061-7f46/~tND;6097-a429/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 31b0-6212/tplx;82ec-9fde/~tNl;70c1-79a0/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, acd6-0108/tplx;b339-294a/~tNl;58ed-6137/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 9ece-fbfb/tplx;c1af-29ff/~tNl;01a6-2535/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, d094-b008/tplx;173a-e3fb/~tNl;82c5-b383/~tND;08b5-d005/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithTransactionalBlockAck -r 0, 3s, c4ff-d71f/tplx;68ba-2827/~tNl;9a98-cc02/~tND, PASS, wireless Ipv4 /examples/wireless/ratecontrol/, -f omnetpp.ini -c Mac -r 0, 100s, bf30-2f13/tplx;7b2f-653d/~tNl;6e1e-3b7b/~tND;19fe-8b0e/tyf, PASS, wireless diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 725e33108fc..9e3a5a153a3 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -16,17 +16,23 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h" #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" +#include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" #include "inet/queueing/gate/PacketGate.h" @@ -134,6 +140,22 @@ class TestTimeoutCancellationCallback : public TestCallback } }; +class TestRecipientHandler : public RecipientBlockAckAgreementHandler +{ + public: + int getNumCachedAddbaResponses() const { return lastAddbaResponses.size(); } +}; + +class TestBlockAckReordering : public BlockAckReordering +{ + public: + int getNumReceiveBuffers() const { return receiveBuffers.size(); } + ReceiveBuffer *getReceiveBuffer(Tid tid, MacAddress originatorAddress) const { + auto it = receiveBuffers.find(std::make_pair(tid, originatorAddress)); + return it == receiveBuffers.end() ? nullptr : it->second; + } +}; + class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue { public: @@ -204,6 +226,17 @@ class TestPacketQueue : public cSimpleModule, public queueing::IPacketQueue virtual void pushPacketProgress(Packet *, const cGate *, bps, b, b) override { throw cRuntimeError("Unsupported"); } }; +class TestImmediateRemovalQueue : public TestPacketQueue +{ + public: + PacketRemovalReason removalReason = PacketRemovalReason::DROPPED; + + virtual void enqueuePacket(Packet *packet) override { + notifyPacketRemoved(packet, removalReason); + delete packet; + } +}; + class TestWrongDequeueQueue : public TestPacketQueue { public: @@ -380,6 +413,14 @@ class TestPacketPulledListener : public cListener } }; +class TestSignalListener : public cListener +{ + public: + int numSignals = 0; + + virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *) override { numSignals++; } +}; + class TestPacketArrivalListener : public cListener { public: @@ -392,12 +433,12 @@ class TestPacketArrivalListener : public cListener } }; -class TestSignalListener : public cListener +class TestOrderedSignalListener : public cListener { public: - int numSignals = 0; + std::vector signals; - virtual void receiveSignal(cComponent *, simsignal_t, cObject *, cObject *) override { numSignals++; } + virtual void receiveSignal(cComponent *, simsignal_t signal, cObject *, cObject *) override { signals.push_back(signal); } }; class TestPacketDropSignalListener : public cListener @@ -499,6 +540,23 @@ class TestEdca : public Edca virtual NonQosRecoveryProcedure *getMgmtAndNonQoSRecoveryProcedure() const override { return nonQosRecoveryProcedure; } }; +class TestRecipientDataService : public IRecipientQosMacDataService +{ + public: + int numReorderingResets = 0; + Tid lastTid = -1; + MacAddress lastOriginatorAddress; + + virtual std::vector dataFrameReceived(Packet *, const Ptr&, IRecipientBlockAckAgreementHandler *) override { return {}; } + virtual std::vector controlFrameReceived(Packet *, const Ptr&, IRecipientBlockAckAgreementHandler *) override { return {}; } + virtual ManagementFrameReceptionResult managementFrameReceived(Packet *, const Ptr&) override { return {}; } + virtual void resetBlockAckReordering(Tid tid, MacAddress originatorAddress) override { + numReorderingResets++; + lastTid = tid; + lastOriginatorAddress = originatorAddress; + } +}; + class TestHcf : public Hcf { public: @@ -557,6 +615,7 @@ class TestHcf : public Hcf void removePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::REMOVED); } void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } + void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } void processFailedFrame(Packet *packet) { originatorProcessFailedFrame(packet); } void processReceivedAck(Packet *packet, Packet *lastTransmittedPacket, AccessCategory accessCategory) { @@ -580,6 +639,14 @@ class TestHcf : public Hcf virtual void scheduleAddbaResponseTimer(simtime_t) override {} }; +class TestTxopProcedure : public TxopProcedure +{ + public: + std::vector *eventOrder = nullptr; + virtual simtime_t getRemaining() const override { return 1; } + virtual void endTxop() override { if (eventOrder != nullptr) eventOrder->push_back(2); } +}; + class TestMac : public Ieee80211Mac { public: @@ -588,6 +655,15 @@ class TestMac : public Ieee80211Mac virtual void sendDownPendingRadioConfigMsg() override { numPendingRadioConfigSends++; } }; +class TestEmptyFrameSequence : public IFrameSequence +{ + public: + virtual void startSequence(FrameSequenceContext *, int) override {} + virtual IFrameSequenceStep *prepareStep(FrameSequenceContext *) override { return nullptr; } + virtual bool completeStep(FrameSequenceContext *) override { return true; } + virtual std::string getHistory() const override { return ""; } +}; + class TestFragmentationPolicy : public IFragmentationPolicy { public: @@ -715,6 +791,38 @@ class TestFrameSequenceHandler : public IFrameSequenceHandler virtual void handleStartRxTimeout() override {} }; +class TestDcaf : public Dcaf +{ + public: + int numReleasedChannels = 0; + + void configure(InProgressFrames *inProgressFrames) { this->inProgressFrames = inProgressFrames; } + virtual void releaseChannel(IChannelAccess::ICallback *) override { numReleasedChannels++; } +}; + +class TestDcf : public Dcf +{ + public: + virtual ~TestDcf() { + for (auto listener : getLocalSignalListeners(Ieee80211Mac::frameTransmissionOutcomeSignal)) + unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, listener); + } + TestFrameSequenceHandler *configure(TestDcaf *channelAccess, Ieee80211Mac *mac) { + this->channelAccess = channelAccess; + this->mac = mac; + subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, check_and_cast(mac)); + auto handler = new TestFrameSequenceHandler(); + frameSequenceHandler = handler; + return handler; + } + void grant(TestDcaf *channelAccess) { channelGranted(channelAccess); } + + protected: + virtual FrameSequenceContext *buildContext() override { + return new FrameSequenceContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, channelAccess->getInProgressFrames(), nullptr, nullptr, new NonQoSContext(nullptr), nullptr); + } +}; + static Ptr makeQosHeader(MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber) { auto header = makeShared(); @@ -751,6 +859,18 @@ static Ptr makeResponse(MacAddress transmitterAddress, T return response; } +static Ptr makeRequest(MacAddress transmitterAddress, Tid tid, uint8_t dialogToken, SequenceNumberCyclic startingSequenceNumber, int bufferSize) +{ + auto request = makeShared(); + request->setTransmitterAddress(transmitterAddress); + request->setTid(tid); + request->setDialogToken(dialogToken); + request->setStartingSequenceNumber(startingSequenceNumber); + request->setBufferSize(bufferSize); + request->setBlockAckPolicy(1); + request->setBlockAckTimeoutValue(0); + return request; +} %activity: // IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.2. @@ -1443,6 +1563,1018 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(threw); } +// An initially rejected request has no agreement, so its response is never +// replayable and must not consume replay-cache state. +{ + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("initiallyRejectedAddbaDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestRecipientPolicy rejectingPolicy; + rejectingPolicy.accepted = false; + TestCallback responseCallback; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &rejectingPolicy); + hcf.configureRecipientDataService(recipientDataService); + hcf.configureManagementCallback(&responseCallback); + + auto rejectedRequest = makeRequest(peer3, 7, 89, SequenceNumberCyclic(90), 16); + rejectedRequest->setSequenceNumber(SequenceNumberCyclic(40)); + auto rejectedReception = recipientDataService->managementFrameReceived(new Packet("initiallyRejectedAddbaRequest", rejectedRequest), rejectedRequest); + ASSERT(!rejectedReception.duplicate); + hcf.processReceivedManagementFrame(rejectedRequest, rejectedReception.duplicate); + ASSERT(recipientHandler->getAgreement(7, peer3) == nullptr); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); + ASSERT(responseCallback.managementPackets.size() == 1); + ASSERT(dynamicPtrCast(responseCallback.managementHeader)->getStatusCode() != 0); + + auto duplicateRejectedRequest = makeRequest(peer3, 7, 89, SequenceNumberCyclic(90), 16); + duplicateRejectedRequest->setSequenceNumber(SequenceNumberCyclic(40)); + duplicateRejectedRequest->setRetry(true); + auto duplicateReception = recipientDataService->managementFrameReceived(new Packet("duplicateInitiallyRejectedAddbaRequest", duplicateRejectedRequest), duplicateRejectedRequest); + ASSERT(duplicateReception.duplicate); + hcf.processReceivedManagementFrame(duplicateRejectedRequest, duplicateReception.duplicate); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); + ASSERT(responseCallback.managementPackets.size() == 1); + + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + +// IEEE Std 802.11-2024, 9.2.4.1.6 and 10.3.2.14.3/Table 10-6: +// duplicate identity comes from Retry + TA + MAC sequence/fragment, not from +// equal ADDBA parameters. Re-sending the cached exact response is an +// explicit robustness/model extension; it leaves recipient state untouched. +{ + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("duplicateAddbaDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + TestPacketDropSignalListener reorderDropListener; + recipientDataService->subscribe(packetDroppedSignal, &reorderDropListener); + + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestRecipientPolicy duplicatePolicy; + duplicatePolicy.blockAckTimeoutValue = 1; + TestCallback responseCallback; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &duplicatePolicy); + hcf.configureRecipientDataService(recipientDataService); + hcf.configureManagementCallback(&responseCallback); + TestSignalListener addedListener; + TestSignalListener changedListener; + hcf.subscribe(Hcf::blockAckAgreementAddedSignal, &addedListener); + hcf.subscribe(Hcf::blockAckAgreementChangedSignal, &changedListener); + + auto firstRequest = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); + firstRequest->setSequenceNumber(SequenceNumberCyclic(50)); + firstRequest->setBlockAckTimeoutValue(3); + auto firstReception = recipientDataService->managementFrameReceived(new Packet("firstAddbaRequest", firstRequest), firstRequest); + ASSERT(!firstReception.duplicate); + ASSERT(firstReception.completeFrames.empty()); + hcf.processReceivedManagementFrame(firstRequest, firstReception.duplicate); + auto firstAgreement = recipientHandler->getAgreement(6, peer2); + ASSERT(firstAgreement != nullptr); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); + auto firstBlockAckRecord = firstAgreement->getBlockAckRecord(); + auto firstExpirationTime = firstAgreement->getExpirationTime(); + ASSERT(addedListener.numSignals == 1); + ASSERT(changedListener.numSignals == 0); + ASSERT(responseCallback.managementPackets.size() == 1); + auto firstResponse = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(firstResponse != nullptr); + ASSERT(firstResponse->getDialogToken() == 90); + ASSERT(firstResponse->getStatusCode() == 0); + ASSERT(firstResponse->getTid() == 6); + ASSERT(firstResponse->getBlockAckPolicy()); + ASSERT(firstResponse->getBufferSize() == 32); + ASSERT(firstResponse->getBlockAckTimeoutValue() == 3); + ASSERT(firstResponse->getAMsduSupported()); + + auto bufferedHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(101)); + bufferedHeader->setTransmitterAddress(peer2); + auto bufferedPacket = new Packet("duplicateAddbaBufferedData", bufferedHeader); + ASSERT(recipientDataService->dataFrameReceived(bufferedPacket, bufferedHeader, recipientHandler).empty()); + + // Mutable policy is deliberately changed after negotiation. A true MAC + // duplicate must replay the original successful response, not rebuild it. + duplicatePolicy.accepted = false; + duplicatePolicy.blockAckTimeoutValue = 0; + duplicatePolicy.aMsduSupportedValue = false; + duplicatePolicy.maximumAllowedBufferSize = 1; + auto duplicateRequest = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); + duplicateRequest->setSequenceNumber(SequenceNumberCyclic(50)); + duplicateRequest->setRetry(true); + duplicateRequest->setBlockAckTimeoutValue(3); + auto duplicateReception = recipientDataService->managementFrameReceived(new Packet("duplicateAddbaRequest", duplicateRequest), duplicateRequest); + ASSERT(duplicateReception.duplicate); + ASSERT(duplicateReception.completeFrames.empty()); + hcf.processReceivedManagementFrame(duplicateRequest, duplicateReception.duplicate); + ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); + ASSERT(firstAgreement->getBlockAckRecord() == firstBlockAckRecord); + ASSERT(firstAgreement->getExpirationTime() == firstExpirationTime); + ASSERT(addedListener.numSignals == 1); + ASSERT(changedListener.numSignals == 0); + ASSERT(reorderDropListener.numSignals == 0); + ASSERT(responseCallback.managementPackets.size() == 2); + auto duplicateResponse = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(duplicateResponse->getDialogToken() == firstResponse->getDialogToken()); + ASSERT(duplicateResponse->getStatusCode() == firstResponse->getStatusCode()); + ASSERT(duplicateResponse->getTid() == firstResponse->getTid()); + ASSERT(duplicateResponse->getBlockAckPolicy() == firstResponse->getBlockAckPolicy()); + ASSERT(duplicateResponse->getBufferSize() == firstResponse->getBufferSize()); + ASSERT(duplicateResponse->getBlockAckTimeoutValue() == firstResponse->getBlockAckTimeoutValue()); + ASSERT(duplicateResponse->getAMsduSupported() == firstResponse->getAMsduSupported()); + + // A rejected fresh renegotiation leaves agreement A in place, but its + // retransmission must replay B's rejection rather than A's old success. + duplicatePolicy.blockAckTimeoutValue = 1; + auto rejectedRequest = makeRequest(peer2, 6, 91, SequenceNumberCyclic(200), 16); + rejectedRequest->setSequenceNumber(SequenceNumberCyclic(51)); + rejectedRequest->setBlockAckTimeoutValue(4); + auto rejectedReception = recipientDataService->managementFrameReceived(new Packet("rejectedAddbaRequest", rejectedRequest), rejectedRequest); + ASSERT(!rejectedReception.duplicate); + hcf.processReceivedManagementFrame(rejectedRequest, rejectedReception.duplicate); + ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); + ASSERT(changedListener.numSignals == 0); + ASSERT(reorderDropListener.numSignals == 0); + ASSERT(responseCallback.managementPackets.size() == 3); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); + auto rejectedResponse = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(rejectedResponse->getDialogToken() == 91); + ASSERT(rejectedResponse->getStatusCode() != 0); + ASSERT(rejectedResponse->getTid() == 6); + ASSERT(rejectedResponse->getBufferSize() == 1); + ASSERT(rejectedResponse->getBlockAckTimeoutValue() == 4); + ASSERT(!rejectedResponse->getAMsduSupported()); + + auto duplicateRejectedRequest = makeRequest(peer2, 6, 91, SequenceNumberCyclic(200), 16); + duplicateRejectedRequest->setSequenceNumber(SequenceNumberCyclic(51)); + duplicateRejectedRequest->setRetry(true); + duplicateRejectedRequest->setBlockAckTimeoutValue(4); + auto duplicateRejectedReception = recipientDataService->managementFrameReceived(new Packet("duplicateRejectedAddbaRequest", duplicateRejectedRequest), duplicateRejectedRequest); + ASSERT(duplicateRejectedReception.duplicate); + hcf.processReceivedManagementFrame(duplicateRejectedRequest, duplicateRejectedReception.duplicate); + ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); + ASSERT(changedListener.numSignals == 0); + ASSERT(reorderDropListener.numSignals == 0); + ASSERT(responseCallback.managementPackets.size() == 4); + auto duplicateRejectedResponse = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(duplicateRejectedResponse->getDialogToken() == rejectedResponse->getDialogToken()); + ASSERT(duplicateRejectedResponse->getStatusCode() == rejectedResponse->getStatusCode()); + ASSERT(duplicateRejectedResponse->getTid() == rejectedResponse->getTid()); + ASSERT(duplicateRejectedResponse->getBlockAckPolicy() == rejectedResponse->getBlockAckPolicy()); + ASSERT(duplicateRejectedResponse->getBufferSize() == rejectedResponse->getBufferSize()); + ASSERT(duplicateRejectedResponse->getBlockAckTimeoutValue() == rejectedResponse->getBlockAckTimeoutValue()); + ASSERT(duplicateRejectedResponse->getAMsduSupported() == rejectedResponse->getAMsduSupported()); + + auto expectedHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(100)); + expectedHeader->setTransmitterAddress(peer2); + auto expectedPacket = new Packet("duplicateAddbaExpectedData", expectedHeader); + auto expectedFrames = recipientDataService->dataFrameReceived(expectedPacket, expectedHeader, recipientHandler); + ASSERT(expectedFrames.size() == 1); + ASSERT(expectedFrames.front() == expectedPacket); + auto followingHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(102)); + followingHeader->setTransmitterAddress(peer2); + auto followingPacket = new Packet("duplicateAddbaFollowingData", followingHeader); + auto followingFrames = recipientDataService->dataFrameReceived(followingPacket, followingHeader, recipientHandler); + ASSERT(followingFrames.size() == 1); + ASSERT(followingFrames.front() == bufferedPacket); + for (auto packet : expectedFrames) { + take(packet); + drop(packet); + delete packet; + } + for (auto packet : followingFrames) { + take(packet); + drop(packet); + delete packet; + } + + // A new MAC identity with the exact same ADDBA body is a genuine 10.25.2 + // renegotiation: replace the agreement and reset the reorder window, + // which still contains followingPacket (sequence number 102). + duplicatePolicy.accepted = true; + duplicatePolicy.blockAckTimeoutValue = 1; + duplicatePolicy.aMsduSupportedValue = true; + duplicatePolicy.maximumAllowedBufferSize = 64; + auto replacementRequest = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); + replacementRequest->setSequenceNumber(SequenceNumberCyclic(52)); + replacementRequest->setBlockAckTimeoutValue(3); + auto replacementReception = recipientDataService->managementFrameReceived(new Packet("replacementAddbaRequest", replacementRequest), replacementRequest); + ASSERT(!replacementReception.duplicate); + ASSERT(replacementReception.completeFrames.empty()); + hcf.processReceivedManagementFrame(replacementRequest, replacementReception.duplicate); + ASSERT(recipientHandler->getAgreement(6, peer2) != firstAgreement); + ASSERT(addedListener.numSignals == 1); + ASSERT(changedListener.numSignals == 1); + ASSERT(reorderDropListener.numSignals == 1); + ASSERT(reorderDropListener.numOtherPacketDrops == 1); + ASSERT(responseCallback.managementPackets.size() == 5); + + auto duplicateWithoutAgreement = makeRequest(peer3, 7, 91, SequenceNumberCyclic(200), 16); + hcf.processReceivedManagementFrame(duplicateWithoutAgreement, true); + ASSERT(responseCallback.managementPackets.size() == 5); + + auto duplicateDelba = makeShared(); + duplicateDelba->setTransmitterAddress(peer2); + duplicateDelba->setTid(6); + duplicateDelba->setInitiator(true); + auto replacementAgreement = recipientHandler->getAgreement(6, peer2); + hcf.processReceivedManagementFrame(duplicateDelba, true); + ASSERT(recipientHandler->getAgreement(6, peer2) == replacementAgreement); + + hcf.processReceivedManagementFrame(duplicateDelba, false); + ASSERT(recipientHandler->getAgreement(6, peer2) == nullptr); + ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); + auto duplicateAfterTeardown = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); + duplicateAfterTeardown->setSequenceNumber(SequenceNumberCyclic(52)); + duplicateAfterTeardown->setRetry(true); + hcf.processReceivedManagementFrame(duplicateAfterTeardown, true); + ASSERT(responseCallback.managementPackets.size() == 5); + + hcf.unsubscribe(Hcf::blockAckAgreementAddedSignal, &addedListener); + hcf.unsubscribe(Hcf::blockAckAgreementChangedSignal, &changedListener); + recipientDataService->unsubscribe(packetDroppedSignal, &reorderDropListener); + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + +// IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.3. +RecipientBlockAckAgreementHandler immediateRecipientHandler; +TestRecipientPolicy recipientPolicy; +TestCallback recipientCallback; +auto recipientRequest = makeShared(); +recipientRequest->setTransmitterAddress(peer1); +recipientRequest->setTid(6); +recipientRequest->setDialogToken(77); +recipientRequest->setStartingSequenceNumber(SequenceNumberCyclic(0)); +recipientRequest->setBufferSize(64); +recipientRequest->setBlockAckPolicy(1); +recipientRequest->setBlockAckTimeoutValue(0); +auto firstImmediateAgreement = immediateRecipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientCallback, &recipientCallback); +ASSERT(firstImmediateAgreement != nullptr); +ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == firstImmediateAgreement); +ASSERT(firstImmediateAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(0)); +auto acceptedResponse = dynamicPtrCast(recipientCallback.managementHeader); +ASSERT(acceptedResponse->getDialogToken() == 77); +ASSERT(acceptedResponse->getStatusCode() == 0); + +// Renegotiation replaces the recipient agreement and the HCF reset boundary +// must discard the old reorder window before frames use the new SSN. +RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; +{ + TestBlockAckReordering reordering; + auto oldWindowHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(1)); + auto oldWindowPacket = new Packet("oldWindowPacket", oldWindowHeader); + auto oldWindowFrames = reordering.processReceivedQoSFrame(firstImmediateAgreement, oldWindowPacket, oldWindowHeader); + ASSERT(oldWindowFrames.empty()); + ASSERT(reordering.getNumReceiveBuffers() == 1); + ASSERT(reordering.getReceiveBuffer(6, peer1)->getLength() == 1); + + auto discardedOldWindowFrames = reordering.resetReceiveBuffer(6, peer1); + ASSERT(discardedOldWindowFrames.size() == 1); + ASSERT(discardedOldWindowFrames.front() == oldWindowPacket); + take(discardedOldWindowFrames.front()); + drop(discardedOldWindowFrames.front()); + delete discardedOldWindowFrames.front(); + ASSERT(reordering.getNumReceiveBuffers() == 0); + + recipientRequest->setDialogToken(79); + recipientRequest->setStartingSequenceNumber(SequenceNumberCyclic(9)); + replacementImmediateAgreement = immediateRecipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientCallback, &recipientCallback); + ASSERT(replacementImmediateAgreement != nullptr); + ASSERT(replacementImmediateAgreement != firstImmediateAgreement); + ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == replacementImmediateAgreement); + ASSERT(replacementImmediateAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(9)); + auto newWindowHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(10)); + auto newWindowPacket = new Packet("newWindowPacket", newWindowHeader); + auto newWindowFrames = reordering.processReceivedQoSFrame(replacementImmediateAgreement, newWindowPacket, newWindowHeader); + ASSERT(newWindowFrames.empty()); + ASSERT(reordering.getReceiveBuffer(6, peer1)->getNextExpectedSequenceNumber() == SequenceNumberCyclic(9)); + ASSERT(reordering.getReceiveBuffer(6, peer1)->getLength() == 1); + auto discardedNewWindowFrames = reordering.resetReceiveBuffer(6, peer1); + ASSERT(discardedNewWindowFrames.size() == 1); + ASSERT(discardedNewWindowFrames.front() == newWindowPacket); + take(discardedNewWindowFrames.front()); + drop(discardedNewWindowFrames.front()); + delete discardedNewWindowFrames.front(); +} + +// The recipient data service owns reorder-buffer reset observability: every +// buffered MPDU is reported exactly once before it is deleted. +{ + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("reorderingDropDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + TestPacketDropSignalListener dropListener; + recipientDataService->subscribe(packetDroppedSignal, &dropListener); + auto bufferedHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(11)); + bufferedHeader->setTransmitterAddress(peer1); + auto bufferedPacket = new Packet("signaledOldWindowPacket", bufferedHeader); + auto signaledOldWindowFrames = recipientDataService->dataFrameReceived(bufferedPacket, bufferedHeader, &immediateRecipientHandler); + ASSERT(signaledOldWindowFrames.empty()); + recipientDataService->resetBlockAckReordering(6, peer1); + ASSERT(dropListener.numSignals == 1); + ASSERT(dropListener.numOtherPacketDrops == 1); + recipientDataService->resetBlockAckReordering(6, peer1); + ASSERT(dropListener.numSignals == 1); + recipientDataService->unsubscribe(packetDroppedSignal, &dropListener); + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + +recipientPolicy.accepted = false; +recipientRequest->setDialogToken(80); +recipientRequest->setStartingSequenceNumber(SequenceNumberCyclic(12)); +ASSERT(immediateRecipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientCallback, &recipientCallback) == nullptr); +ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == replacementImmediateAgreement); + +// A received DELBA is keyed by its transmitter (the peer), not by its receiver +// address (the local station). +{ + RecipientBlockAckAgreementHandler delbaRecipientHandler; + TestRecipientPolicy delbaRecipientPolicy; + TestCallback delbaRecipientCallback; + auto request = makeRequest(peer2, 4, 81, SequenceNumberCyclic(12), 32); + auto establishedAgreement = delbaRecipientHandler.processReceivedAddbaRequest(request, &delbaRecipientPolicy, &delbaRecipientCallback, &delbaRecipientCallback); + ASSERT(establishedAgreement != nullptr); + auto delba = makeShared(); + delba->setTransmitterAddress(peer2); + delba->setReceiverAddress(peer1); + delba->setTid(4); + delba->setInitiator(true); + auto terminatedAgreement = delbaRecipientHandler.processReceivedDelba(delba, &delbaRecipientPolicy); + ASSERT(terminatedAgreement != nullptr); + ASSERT(delbaRecipientHandler.getAgreement(4, peer2) == nullptr); +} + +// HCF emits one deletion event for the peer-keyed recipient agreement and can +// then be torn down without walking an EDCA child hierarchy. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestCallback delbaCallback; + auto request = makeRequest(peer2, 2, 82, SequenceNumberCyclic(14), 32); + recipientPolicy.accepted = true; + auto establishedAgreement = recipientHandler->processReceivedAddbaRequest(request, &recipientPolicy, &delbaCallback, &delbaCallback); + ASSERT(establishedAgreement != nullptr); + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + TestRecipientDataService recipientDataService; + hcf.configureRecipientDataService(&recipientDataService); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + auto delba = makeShared(); + delba->setTransmitterAddress(peer2); + delba->setReceiverAddress(peer1); + delba->setTid(2); + delba->setInitiator(true); + hcf.processReceivedManagementFrame(delba); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(recipientDataService.lastTid == 2); + ASSERT(recipientDataService.lastOriginatorAddress == peer2); + ASSERT(recipientHandler->getAgreement(2, peer2) == nullptr); + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); +} + +// A rejected DELBA leaves the recipient agreement, reordering state, and +// agreement statistics unchanged. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestRecipientPolicy rejectingPolicy; + TestCallback delbaCallback; + auto request = makeRequest(peer2, 2, 83, SequenceNumberCyclic(15), 32); + auto establishedAgreement = recipientHandler->processReceivedAddbaRequest(request, &rejectingPolicy, &delbaCallback, &delbaCallback); + ASSERT(establishedAgreement != nullptr); + rejectingPolicy.delbaAccepted = false; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &rejectingPolicy); + TestRecipientDataService recipientDataService; + hcf.configureRecipientDataService(&recipientDataService); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + auto delba = makeShared(); + delba->setTransmitterAddress(peer2); + delba->setReceiverAddress(peer1); + delba->setTid(2); + delba->setInitiator(true); + hcf.processReceivedManagementFrame(delba); + ASSERT(deletionListener.numSignals == 0); + ASSERT(recipientDataService.numReorderingResets == 0); + ASSERT(recipientHandler->getAgreement(2, peer2) != nullptr); + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); +} + +// IEEE Std 802.11-2024, 10.25.4 and 11.5.3.5: locally transmitting a +// recipient DELBA tears down the agreement and its reorder resources before a +// replacement agreement for the same peer and TID starts a fresh window. +{ + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("transmittedDelbaDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + TestPacketDropSignalListener reorderDropListener; + recipientDataService->subscribe(packetDroppedSignal, &reorderDropListener); + + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestRecipientPolicy transmittedDelbaPolicy; + TestCallback transmittedDelbaCallback; + auto firstRequest = makeRequest(peer2, 3, 84, SequenceNumberCyclic(100), 32); + auto firstAgreement = recipientHandler->processReceivedAddbaRequest(firstRequest, &transmittedDelbaPolicy, &transmittedDelbaCallback, &transmittedDelbaCallback); + ASSERT(firstAgreement != nullptr); + auto oldWindowHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(101)); + oldWindowHeader->setTransmitterAddress(peer2); + auto oldWindowPacket = new Packet("transmittedDelbaOldWindow", oldWindowHeader); + ASSERT(recipientDataService->dataFrameReceived(oldWindowPacket, oldWindowHeader, recipientHandler).empty()); + + QosAckHandler transmittedDelbaAckHandler; + TestEdcaf transmittedDelbaEdcaf; + transmittedDelbaEdcaf.qosAckHandler = &transmittedDelbaAckHandler; + TestEdca transmittedDelbaEdca; + transmittedDelbaEdca.edcaf = &transmittedDelbaEdcaf; + OriginatorQosAckPolicy transmittedDelbaAckPolicy; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &transmittedDelbaPolicy); + hcf.configureRecipientDataService(recipientDataService); + hcf.configureTransmittedManagement(&transmittedDelbaEdca, &transmittedDelbaAckPolicy); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + + auto delba = makeShared(); + delba->setReceiverAddress(peer2); + delba->setTid(3); + delba->setInitiator(false); + delba->setSequenceNumber(SequenceNumberCyclic(200)); + Packet delbaPacket("transmittedRecipientDelba", delba); + hcf.processTransmittedManagement(&delbaPacket, delba, AC_BE); + ASSERT(recipientHandler->getAgreement(3, peer2) == nullptr); + ASSERT(deletionListener.numSignals == 1); + ASSERT(reorderDropListener.numOtherPacketDrops == 1); + + auto replacementRequest = makeRequest(peer2, 3, 85, SequenceNumberCyclic(10), 32); + auto replacementAgreement = recipientHandler->processReceivedAddbaRequest(replacementRequest, &transmittedDelbaPolicy, &transmittedDelbaCallback, &transmittedDelbaCallback); + ASSERT(replacementAgreement != nullptr); + auto newWindowHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(11)); + newWindowHeader->setTransmitterAddress(peer2); + auto newWindowPacket = new Packet("transmittedDelbaNewWindow", newWindowHeader); + ASSERT(recipientDataService->dataFrameReceived(newWindowPacket, newWindowHeader, recipientHandler).empty()); + recipientDataService->resetBlockAckReordering(3, peer2); + ASSERT(reorderDropListener.numOtherPacketDrops == 2); + + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + recipientDataService->unsubscribe(packetDroppedSignal, &reorderDropListener); + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + +// The reverse DELBA direction terminates the originator agreement and emits +// exactly one deletion event; policy rejection emits nothing and preserves it. +{ + auto originatorHandler = new TestOriginatorHandler(); + originatorHandler->addEstablishedAgreement(peer3, 5); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestOriginatorPolicy delbaPolicy; + TestRecipientPolicy delbaRecipientPolicy; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &delbaPolicy, recipientHandler, &delbaRecipientPolicy); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + auto acceptedDelba = makeShared(); + acceptedDelba->setTransmitterAddress(peer3); + acceptedDelba->setReceiverAddress(peer1); + acceptedDelba->setTid(5); + acceptedDelba->setInitiator(false); + hcf.processReceivedManagementFrame(acceptedDelba); + ASSERT(deletionListener.numSignals == 1); + ASSERT(originatorHandler->getAgreement(peer3, 5) == nullptr); + originatorHandler->addEstablishedAgreement(peer3, 5); + delbaPolicy.delbaAccepted = false; + auto rejectedDelba = makeShared(*acceptedDelba); + hcf.processReceivedManagementFrame(rejectedDelba); + ASSERT(deletionListener.numSignals == 1); + ASSERT(originatorHandler->getAgreement(peer3, 5) != nullptr); + + QosAckHandler transmittedOriginatorAckHandler; + TestEdcaf transmittedOriginatorEdcaf; + transmittedOriginatorEdcaf.qosAckHandler = &transmittedOriginatorAckHandler; + TestPacketQueue transmittedOriginatorPendingQueue; + TestInProgressFrames transmittedOriginatorInProgressFrames; + TestOriginatorQosMacDataService transmittedOriginatorDataService; + transmittedOriginatorInProgressFrames.configure(&transmittedOriginatorDataService, &transmittedOriginatorAckHandler, &transmittedOriginatorPendingQueue); + transmittedOriginatorEdcaf.pendingQueue = &transmittedOriginatorPendingQueue; + transmittedOriginatorEdcaf.inProgressFrames = &transmittedOriginatorInProgressFrames; + TestEdca transmittedOriginatorEdca; + transmittedOriginatorEdca.edcaf = &transmittedOriginatorEdcaf; + OriginatorQosAckPolicy transmittedOriginatorAckPolicy; + hcf.configureTransmittedManagement(&transmittedOriginatorEdca, &transmittedOriginatorAckPolicy); + originatorHandler->addEstablishedAgreement(peer3, 6); + auto transmittedEstablishedDelba = makeShared(); + transmittedEstablishedDelba->setReceiverAddress(peer3); + transmittedEstablishedDelba->setTid(6); + transmittedEstablishedDelba->setInitiator(true); + transmittedEstablishedDelba->setSequenceNumber(SequenceNumberCyclic(201)); + Packet transmittedEstablishedDelbaPacket("transmittedEstablishedOriginatorDelba", transmittedEstablishedDelba); + hcf.processTransmittedManagement(&transmittedEstablishedDelbaPacket, transmittedEstablishedDelba, AC_BE); + ASSERT(deletionListener.numSignals == 2); + ASSERT(originatorHandler->getAgreement(peer3, 6) == nullptr); + hcf.processTransmittedManagement(&transmittedEstablishedDelbaPacket, transmittedEstablishedDelba, AC_BE); + ASSERT(deletionListener.numSignals == 2); + + TestCallback pendingDelbaCallback; + Packet pendingDelbaTrigger("pendingDelbaTrigger"); + auto pendingDelbaTriggerHeader = makeQosHeader(peer3, 7, SequenceNumberCyclic(0)); + originatorHandler->processAcknowledgedDataFrame(&pendingDelbaTrigger, pendingDelbaTriggerHeader, &delbaPolicy, &pendingDelbaCallback); + ASSERT(originatorHandler->isAddbaResponsePending(peer3, 7)); + auto transmittedPendingDelba = makeShared(); + transmittedPendingDelba->setReceiverAddress(peer3); + transmittedPendingDelba->setTid(7); + transmittedPendingDelba->setInitiator(true); + transmittedPendingDelba->setSequenceNumber(SequenceNumberCyclic(202)); + Packet transmittedPendingDelbaPacket("transmittedPendingOriginatorDelba", transmittedPendingDelba); + hcf.processTransmittedManagement(&transmittedPendingDelbaPacket, transmittedPendingDelba, AC_BE); + ASSERT(deletionListener.numSignals == 2); + ASSERT(originatorHandler->getAgreement(peer3, 7) == nullptr); + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); +} + +// IEEE Std 802.11-2024, 11.5.2.2 and 11.5.3.2: a received DELBA can cancel +// an originator transaction before ADDBA Response success; that pending state +// was never observable as an active agreement and must not emit Deleted. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new RecipientBlockAckAgreementHandler(); + TestOriginatorPolicy pendingPolicy; + TestRecipientPolicy pendingRecipientPolicy; + TestOriginatorQosMacDataService pendingDataService; + pendingDataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto header = packet->peekAtFront(); + if (auto addbaReq = dynamicPtrCast(header)) + return originatorHandler->isAddbaRequestPending(packet, addbaReq); + auto dataHeader = dynamicPtrCast(header); + return dataHeader == nullptr || !originatorHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); + }); + TestPacketQueue pendingQueue; + QosAckHandler pendingAckHandler; + TestInProgressFrames pendingInProgressFrames; + pendingInProgressFrames.configure(&pendingDataService, &pendingAckHandler, &pendingQueue); + TestEdcaf pendingEdcaf; + pendingEdcaf.pendingQueue = &pendingQueue; + pendingEdcaf.inProgressFrames = &pendingInProgressFrames; + pendingEdcaf.qosAckHandler = &pendingAckHandler; + TestEdca pendingEdca; + pendingEdca.edcaf = &pendingEdcaf; + TestHcf pendingHcf; + pendingHcf.configureBlockAckHandlers(originatorHandler, &pendingPolicy, recipientHandler, &pendingRecipientPolicy); + pendingHcf.configureEligibilityIndex(&pendingEdca, &pendingDataService); + pendingHcf.observePendingQueue(&pendingQueue); + TestCallback pendingCallback; + Packet pendingTrigger("receivedPendingDelbaTrigger"); + auto pendingHeader = makeQosHeader(peer3, 8, SequenceNumberCyclic(203)); + originatorHandler->processAcknowledgedDataFrame(&pendingTrigger, pendingHeader, &pendingPolicy, &pendingCallback); + auto pendingRequestPacket = pendingCallback.managementPacket; + auto pendingRequest = dynamicPtrCast(pendingCallback.managementHeader); + pendingCallback.forgetManagementPacket(pendingRequestPacket); + auto heldPacket = new Packet("receivedPendingDelbaHeld", makeQosHeader(peer3, 8, SequenceNumberCyclic(204))); + pendingQueue.enqueuePacket(pendingRequestPacket); + pendingQueue.enqueuePacket(heldPacket); + pendingHcf.trackFrame(pendingRequestPacket, AC_BK); + pendingHcf.trackFrame(heldPacket, AC_BK); + ASSERT(originatorHandler->isAddbaResponsePending(peer3, 8)); + ASSERT(pendingHcf.getNumEligiblePendingFrames(AC_BK) == 1); + TestSignalListener pendingDeletionListener; + pendingHcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &pendingDeletionListener); + auto receivedPendingDelba = makeShared(); + receivedPendingDelba->setTransmitterAddress(peer3); + receivedPendingDelba->setReceiverAddress(peer1); + receivedPendingDelba->setTid(8); + receivedPendingDelba->setInitiator(false); + pendingHcf.processReceivedManagementFrame(receivedPendingDelba); + ASSERT(pendingDeletionListener.numSignals == 0); + ASSERT(originatorHandler->getAgreement(peer3, 8) == nullptr); + ASSERT(pendingHcf.numCancelledAddbaTransactions == 1); + ASSERT(pendingHcf.numRebuildEligibilityCalls > 0); + ASSERT(pendingQueue.getNumPackets() == 1); + ASSERT(pendingQueue.getPacket(0) == heldPacket); + ASSERT(pendingDataService.isFrameEligible(heldPacket)); + ASSERT(pendingHcf.getNumEligiblePendingFrames(AC_BK) == 1); + ASSERT(pendingHcf.numResumedEligibleChannelAccess == 1); + pendingHcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &pendingDeletionListener); + pendingQueue.removePacket(heldPacket); + delete heldPacket; +} + +// A locally vetoed successful response immediately restores data eligibility +// even while its best-effort DELBA remains queued. Once retry backoff expires, +// a replacement ADDBA invalidates the old DELBA without requiring a disposal +// callback from the queue. +{ + TestPacketQueue heldTeardownQueue; + auto heldTeardownOriginatorHandler = new TestOriginatorHandler(); + auto heldTeardownRecipientHandler = new RecipientBlockAckAgreementHandler(); + TestOriginatorPolicy heldTeardownOriginatorPolicy; + heldTeardownOriginatorPolicy.addbaAccepted = false; + TestRecipientPolicy heldTeardownRecipientPolicy; + TestOriginatorQosMacDataService heldTeardownDataService; + heldTeardownDataService.setFrameEligibilityFunction([heldTeardownOriginatorHandler](const Packet *packet) { + auto header = packet->peekAtFront(); + if (auto addbaReq = dynamicPtrCast(header)) + return heldTeardownOriginatorHandler->isAddbaRequestPending(packet, addbaReq); + if (auto delba = dynamicPtrCast(header)) + return heldTeardownOriginatorHandler->isDelbaPending(packet, delba); + auto dataHeader = dynamicPtrCast(header); + return dataHeader == nullptr || !heldTeardownOriginatorHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); + }); + QosAckHandler heldTeardownAckHandler; + TestInProgressFrames heldTeardownInProgressFrames; + heldTeardownInProgressFrames.configure(&heldTeardownDataService, &heldTeardownAckHandler, &heldTeardownQueue); + TestEdcaf heldTeardownEdcaf; + heldTeardownEdcaf.pendingQueue = &heldTeardownQueue; + heldTeardownEdcaf.inProgressFrames = &heldTeardownInProgressFrames; + heldTeardownEdcaf.qosAckHandler = &heldTeardownAckHandler; + TestEdca heldTeardownEdca; + heldTeardownEdca.edcaf = &heldTeardownEdcaf; + TestHcf heldTeardownHcf; + heldTeardownHcf.configureBlockAckHandlers(heldTeardownOriginatorHandler, &heldTeardownOriginatorPolicy, heldTeardownRecipientHandler, &heldTeardownRecipientPolicy); + heldTeardownHcf.configureEligibilityIndex(&heldTeardownEdca, &heldTeardownDataService); + heldTeardownHcf.observePendingQueue(&heldTeardownQueue); + TestCallback heldTeardownCallback; + Packet heldTeardownTrigger("heldTeardownTrigger"); + auto heldTeardownTriggerHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(210)); + heldTeardownOriginatorHandler->processAcknowledgedDataFrame(&heldTeardownTrigger, heldTeardownTriggerHeader, &heldTeardownOriginatorPolicy, &heldTeardownCallback); + auto heldTeardownRequest = dynamicPtrCast(heldTeardownCallback.managementHeader); + heldTeardownOriginatorHandler->processTransmittedAddbaReq(heldTeardownCallback.managementPacket, heldTeardownRequest, &heldTeardownOriginatorPolicy, &heldTeardownCallback); + auto heldDataPacket = new Packet("heldTeardownData", makeQosHeader(peer1, 7, SequenceNumberCyclic(211))); + heldTeardownQueue.enqueuePacket(heldDataPacket); + heldTeardownHcf.trackFrame(heldDataPacket, AC_BK); + ASSERT(!heldTeardownDataService.isFrameEligible(heldDataPacket)); + TestOrderedSignalListener transitionListener; + heldTeardownHcf.subscribe(Hcf::blockAckAgreementAddedSignal, &transitionListener); + heldTeardownHcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &transitionListener); + auto heldTeardownResponse = makeResponse(peer1, 7, heldTeardownRequest->getDialogToken(), 0); + heldTeardownHcf.processReceivedManagementFrame(heldTeardownResponse); + ASSERT(transitionListener.signals == std::vector({ Hcf::blockAckAgreementAddedSignal, Hcf::blockAckAgreementDeletedSignal })); + ASSERT(heldTeardownOriginatorHandler->getAgreement(peer1, 7) == nullptr); + ASSERT(heldTeardownHcf.numRebuildEligibilityCalls > 0); + ASSERT(heldTeardownDataService.isFrameEligible(heldDataPacket)); + ASSERT(heldTeardownHcf.hasFrame(AC_BK)); + ASSERT(heldTeardownQueue.getNumPackets() == 2); + auto heldDelbaPacket = heldTeardownQueue.getPacket(1); + auto heldDelba = dynamicPtrCast(heldDelbaPacket->peekAtFront()); + ASSERT(heldDelba != nullptr); + ASSERT(heldTeardownOriginatorHandler->isDelbaPending(heldDelbaPacket, heldDelba)); + OriginatorQosAckPolicy heldTeardownAckPolicy; + ASSERT(heldTeardownAckPolicy.computeAckPolicy(heldDataPacket, heldDataPacket->peekAtFront(), heldTeardownOriginatorHandler->getAgreement(peer1, 7)) == NORMAL_ACK); + heldTeardownOriginatorHandler->allowRetryNow(peer1, 7); + TestCallback heldTeardownRetryCallback; + Packet heldTeardownRetryTrigger("heldTeardownRetryTrigger"); + auto heldTeardownRetryHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(212)); + auto obsoleteHeldTeardownTransactionId = heldTeardownOriginatorHandler->processAcknowledgedDataFrame(&heldTeardownRetryTrigger, heldTeardownRetryHeader, &heldTeardownOriginatorPolicy, &heldTeardownRetryCallback); + ASSERT(heldTeardownRetryCallback.managementPacket != nullptr); + ASSERT(obsoleteHeldTeardownTransactionId == heldDelbaPacket->getTag()->getTransactionId()); + heldTeardownHcf.cancelTransaction(obsoleteHeldTeardownTransactionId); + ASSERT(heldTeardownQueue.getNumPackets() == 1); + ASSERT(heldTeardownQueue.getPacket(0) == heldDataPacket); + heldTeardownHcf.rebuildEligibilityIndex(); + heldTeardownHcf.unsubscribe(Hcf::blockAckAgreementAddedSignal, &transitionListener); + heldTeardownHcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &transitionListener); + while (!heldTeardownQueue.isEmpty()) { + auto packet = heldTeardownQueue.getPacket(0); + heldTeardownQueue.removePacket(packet); + delete packet; + } +} + +// Synchronous DELBA drop/removal clears the separate teardown transaction, and +// the already-completed agreement transition remains idempotent. +for (auto removalReason : { queueing::IPacketQueue::PacketRemovalReason::DROPPED, queueing::IPacketQueue::PacketRemovalReason::REMOVED }) { + TestImmediateRemovalQueue teardownQueue; + teardownQueue.removalReason = removalReason; + TestOriginatorQosMacDataService teardownDataService; + teardownDataService.setFrameEligibilityFunction([](const Packet *) { return true; }); + QosAckHandler teardownAckHandler; + TestInProgressFrames teardownInProgressFrames; + teardownInProgressFrames.configure(&teardownDataService, &teardownAckHandler, &teardownQueue); + TestEdcaf teardownEdcaf; + teardownEdcaf.pendingQueue = &teardownQueue; + teardownEdcaf.inProgressFrames = &teardownInProgressFrames; + teardownEdcaf.qosAckHandler = &teardownAckHandler; + TestEdca teardownEdca; + teardownEdca.edcaf = &teardownEdcaf; + auto teardownOriginatorHandler = new TestOriginatorHandler(); + auto teardownRecipientHandler = new RecipientBlockAckAgreementHandler(); + TestOriginatorPolicy teardownOriginatorPolicy; + teardownOriginatorPolicy.addbaAccepted = false; + TestRecipientPolicy teardownRecipientPolicy; + TestHcf teardownHcf; + teardownHcf.configureBlockAckHandlers(teardownOriginatorHandler, &teardownOriginatorPolicy, teardownRecipientHandler, &teardownRecipientPolicy); + teardownHcf.configureEligibilityIndex(&teardownEdca, &teardownDataService); + teardownHcf.observePendingQueue(&teardownQueue); + TestCallback teardownCallback; + Packet teardownTrigger("teardownTrigger"); + auto teardownTriggerHeader = makeQosHeader(peer1, removalReason == queueing::IPacketQueue::PacketRemovalReason::DROPPED ? 8 : 9, SequenceNumberCyclic(0)); + teardownOriginatorHandler->processAcknowledgedDataFrame(&teardownTrigger, teardownTriggerHeader, &teardownOriginatorPolicy, &teardownCallback); + auto teardownRequest = dynamicPtrCast(teardownCallback.managementHeader); + teardownOriginatorHandler->processTransmittedAddbaReq(teardownCallback.managementPacket, teardownRequest, &teardownOriginatorPolicy, &teardownCallback); + auto teardownResponse = makeResponse(peer1, teardownTriggerHeader->getTid(), teardownRequest->getDialogToken(), 0); + TestSignalListener addedListener; + TestSignalListener deletedListener; + teardownHcf.subscribe(Hcf::blockAckAgreementAddedSignal, &addedListener); + teardownHcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletedListener); + teardownHcf.processReceivedManagementFrame(teardownResponse); + ASSERT(addedListener.numSignals == 1); + ASSERT(deletedListener.numSignals == 1); + ASSERT(teardownOriginatorHandler->getAgreement(peer1, teardownTriggerHeader->getTid()) == nullptr); + ASSERT(teardownHcf.getNumTrackedPendingFrames() == 0); + TestCallback teardownRetryCallback; + Packet teardownRetryTrigger("teardownRetryTrigger"); + auto teardownRetryHeader = makeQosHeader(peer1, teardownTriggerHeader->getTid(), SequenceNumberCyclic(1)); + teardownOriginatorHandler->processAcknowledgedDataFrame(&teardownRetryTrigger, teardownRetryHeader, &teardownOriginatorPolicy, &teardownRetryCallback); + ASSERT(teardownRetryCallback.managementPacket == nullptr); + teardownOriginatorHandler->allowRetryNow(peer1, teardownTriggerHeader->getTid()); + teardownOriginatorHandler->processAcknowledgedDataFrame(&teardownRetryTrigger, teardownRetryHeader, &teardownOriginatorPolicy, &teardownRetryCallback); + ASSERT(teardownRetryCallback.managementPacket != nullptr); + teardownHcf.unsubscribe(Hcf::blockAckAgreementAddedSignal, &addedListener); + teardownHcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletedListener); +} + +OriginatorQosMacDataService dataService; +dataService.setFrameEligibilityFunction([&handler](const Packet *packet) { + auto header = dynamicPtrCast(packet->peekAtFront()); + return header == nullptr || header->getType() != ST_DATA_WITH_QOS || !handler.isAddbaResponsePending(header->getReceiverAddress(), header->getTid()); +}); +Packet heldFrame("heldFrame", makeQosHeader(peer3, 5, SequenceNumberCyclic(0))); +Packet otherTidFrame("otherTidFrame", makeQosHeader(peer3, 4, SequenceNumberCyclic(0))); +Packet otherPeerFrame("otherPeerFrame", makeQosHeader(peer2, 5, SequenceNumberCyclic(0))); +auto managementHeader = makeShared(); +managementHeader->setType(ST_ACTION); +Packet managementFrame("managementFrame", managementHeader); +TestPacketQueue mixedQueue; +mixedQueue.packets = { &heldFrame, &otherTidFrame, &otherPeerFrame, &managementFrame }; +ASSERT(!dataService.isFrameEligible(&heldFrame)); +ASSERT(dataService.isFrameEligible(&otherTidFrame)); +ASSERT(dataService.isFrameEligible(&otherPeerFrame)); +ASSERT(dataService.isFrameEligible(&managementFrame)); +ASSERT(dataService.hasEligibleFrame(&mixedQueue)); +mixedQueue.packets = { &heldFrame }; +ASSERT(!dataService.hasEligibleFrame(&mixedQueue)); + +// HCF availability is a count lookup: rebuilding evaluates each pending frame +// once, while repeated availability checks neither enumerate queues nor +// reevaluate the Block Ack eligibility predicate. +{ + TestOriginatorQosMacDataService indexedDataService; + bool addbaResponsePending = true; + int numEligibilityChecks = 0; + Packet *setupFrame = nullptr; + indexedDataService.setFrameEligibilityFunction([&](const Packet *packet) { + numEligibilityChecks++; + return !addbaResponsePending || packet == setupFrame; + }); + TestAckHandler indexedAckHandler; + TestPacketQueue pendingQueues[AC_NUMCATEGORIES]; + TestInProgressFrames inProgressFrames[AC_NUMCATEGORIES]; + TestEdcaf edcafs[AC_NUMCATEGORIES]; + TestEdca indexedEdca; + indexedEdca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + inProgressFrames[ac].configure(&indexedDataService, &indexedAckHandler); + edcafs[ac].pendingQueue = &pendingQueues[ac]; + edcafs[ac].inProgressFrames = &inProgressFrames[ac]; + indexedEdca.edcafs[ac] = &edcafs[ac]; + } + std::vector indexedPackets; + for (int i = 0; i < 128; i++) { + auto packet = new Packet("heldIndexedFrame", makeShared(B(1))); + indexedPackets.push_back(packet); + pendingQueues[AC_BK].enqueuePacket(packet); + } + auto voiceFrame = new Packet("eligibleVoiceFrame", makeShared(B(1))); + indexedPackets.push_back(voiceFrame); + pendingQueues[AC_VO].enqueuePacket(voiceFrame); + TestHcf indexedHcf; + indexedHcf.configureEligibilityIndex(&indexedEdca, &indexedDataService); + indexedHcf.rebuildEligibilityIndex(); + auto requireIndex = [](bool condition, const char *message) { + if (!condition) + throw cRuntimeError("Eligibility index test failed: %s", message); + }; + requireIndex(indexedHcf.getNumTrackedPendingFrames() == 129, "initial cardinality"); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_BK) == 0, "initial background count"); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_VO) == 0, "initial voice count"); + int eligibilityChecksAfterRebuild = numEligibilityChecks; + int queueReadsAfterRebuild = 0; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + queueReadsAfterRebuild += pendingQueues[ac].numGetPacketCalls; + bool allHeldQueriesUnavailable = true; + for (int i = 0; i < 1000; i++) { + bool hasBackgroundFrame = indexedHcf.hasFrame(AC_BK); + bool hasVoiceFrame = indexedHcf.hasFrame(AC_VO); + allHeldQueriesUnavailable &= !hasBackgroundFrame && !hasVoiceFrame; + } + requireIndex(allHeldQueriesUnavailable, "all-held availability"); + ASSERT(allHeldQueriesUnavailable); + bool predicateWasNotReevaluated = numEligibilityChecks == eligibilityChecksAfterRebuild; + requireIndex(predicateWasNotReevaluated, "availability reevaluated the predicate"); + ASSERT(predicateWasNotReevaluated); + int queueReadsAfterQueries = 0; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + queueReadsAfterQueries += pendingQueues[ac].numGetPacketCalls; + bool queueWasNotEnumerated = queueReadsAfterQueries == queueReadsAfterRebuild; + requireIndex(queueWasNotEnumerated, "availability enumerated a pending queue"); + ASSERT(queueWasNotEnumerated); + + auto originalSetupFrame = new Packet("eligibleSetupFrame", makeShared(B(1))); + setupFrame = originalSetupFrame; + indexedPackets.push_back(originalSetupFrame); + indexedHcf.trackFrame(originalSetupFrame, AC_BK); + pendingQueues[AC_BK].enqueuePacket(originalSetupFrame); + bool hasSetupFrame = indexedHcf.hasFrame(AC_BK); + requireIndex(hasSetupFrame, "tracked setup frame unavailable"); + auto replacementFrame = new Packet("eligibleReplacementFrame", makeShared(B(1))); + setupFrame = replacementFrame; + indexedPackets.push_back(replacementFrame); + indexedHcf.trackFrame(replacementFrame, AC_BK); + pendingQueues[AC_BK].enqueuePacket(replacementFrame); + indexedHcf.untrackFrame(indexedPackets.front()); + pendingQueues[AC_BK].removePacket(indexedPackets.front()); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_BK) == 2, "existing-victim count"); + auto tailDroppedArrival = new Packet("tailDroppedArrival", makeShared(B(1))); + indexedHcf.trackFrame(tailDroppedArrival, AC_BK); + indexedHcf.untrackFrame(tailDroppedArrival); + indexedHcf.untrackFrame(tailDroppedArrival); + delete tailDroppedArrival; + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_BK) == 2, "tail-drop count"); + indexedHcf.untrackFrame(replacementFrame); + pendingQueues[AC_BK].removePacket(replacementFrame); + indexedHcf.untrackFrame(originalSetupFrame); + pendingQueues[AC_BK].removePacket(originalSetupFrame); + bool hasFrameAfterSelectedRemovals = indexedHcf.hasFrame(AC_BK); + requireIndex(!hasFrameAfterSelectedRemovals, "selected removals left availability"); + + addbaResponsePending = false; + indexedHcf.rebuildEligibilityIndex(); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_BK) == pendingQueues[AC_BK].getNumPackets(), "terminal-transition background count"); + requireIndex(indexedHcf.getNumEligiblePendingFrames(AC_VO) == 1, "terminal-transition voice count"); + addbaResponsePending = true; + setupFrame = nullptr; + indexedHcf.rebuildEligibilityIndex(); + bool hasBackgroundFrameAfterCreation = indexedHcf.hasFrame(AC_BK); + bool hasVoiceFrameAfterCreation = indexedHcf.hasFrame(AC_VO); + requireIndex(!hasBackgroundFrameAfterCreation && !hasVoiceFrameAfterCreation, "creation-transition availability"); + auto inProgressFrame = new Packet("inProgressOnlyFrame", makeQosHeader(peer1, 3, SequenceNumberCyclic(1))); + setupFrame = inProgressFrame; + inProgressFrames[AC_VI].addFrame(inProgressFrame); + bool hasInProgressFrame = indexedHcf.hasFrame(AC_VI); + requireIndex(hasInProgressFrame, "eligible in-progress frame unavailable"); + requireIndex(indexedHcf.getNumTrackedPendingFrames() == 128, "in-progress frame entered pending index"); + auto releasedFrames = inProgressFrames[AC_VI].releaseFrames(); + requireIndex(releasedFrames.size() == 1, "in-progress release count"); + delete releasedFrames.front(); + setupFrame = nullptr; + bool hasFrameAfterInProgressRelease = indexedHcf.hasFrame(AC_VI); + requireIndex(!hasFrameAfterInProgressRelease, "released in-progress frame remained available"); + + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + pendingQueues[ac].removeAllPackets(); + for (auto packet : indexedPackets) + delete packet; +} + +// IEEE Std 802.11-2024, 10.23.2.4: after the current owner releases the +// channel, Hcf re-arms every AC that has an eligible frame, and no idle AC. +{ + TestOriginatorQosMacDataService armingDataService; + armingDataService.setFrameEligibilityFunction([](const Packet *) { return true; }); + TestAckHandler armingAckHandler; + TestPacketQueue armingPendingQueues[AC_NUMCATEGORIES]; + TestInProgressFrames armingInProgressFrames[AC_NUMCATEGORIES]; + TestEdcaf armingEdcafs[AC_NUMCATEGORIES]; + TestEdca armingEdca; + armingEdca.numEdcafs = AC_NUMCATEGORIES; + armingEdca.edcaf = nullptr; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + armingInProgressFrames[ac].configure(&armingDataService, &armingAckHandler, &armingPendingQueues[ac]); + armingEdcafs[ac].pendingQueue = &armingPendingQueues[ac]; + armingEdcafs[ac].inProgressFrames = &armingInProgressFrames[ac]; + armingEdca.edcafs[ac] = &armingEdcafs[ac]; + } + auto backgroundFrame = new Packet("armingBackgroundFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(1))); + auto voiceFrame = new Packet("armingVoiceFrame", makeQosHeader(peer1, 2, SequenceNumberCyclic(1))); + armingInProgressFrames[AC_BK].addOwnedFrame(backgroundFrame); + armingInProgressFrames[AC_VO].addOwnedFrame(voiceFrame); + TestHcf armingHcf; + armingHcf.configureEligibilityIndex(&armingEdca, &armingDataService); + armingHcf.requestEligible(); + ASSERT(armingEdca.requestedAccessCategories.size() == 2); + ASSERT(armingEdca.requestedAccessCategories[0] == AC_BK); + ASSERT(armingEdca.requestedAccessCategories[1] == AC_VO); + auto releasedBackground = armingInProgressFrames[AC_BK].releaseFrames(); + auto releasedVoice = armingInProgressFrames[AC_VO].releaseFrames(); + for (auto frame : releasedBackground) + delete frame; + for (auto frame : releasedVoice) + delete frame; +} + +// The frame-sequence boundary releases the owner and ends its TXOP before +// re-arming eligible ACs; an already-active AC receives the callback without +// restarting contention. +{ + TestOriginatorQosMacDataService sequenceDataService; + sequenceDataService.setFrameEligibilityFunction([](const Packet *) { return true; }); + TestAckHandler sequenceAckHandler; + TestPacketQueue sequencePendingQueues[AC_NUMCATEGORIES]; + TestInProgressFrames sequenceInProgressFrames[AC_NUMCATEGORIES]; + TestEdcaf sequenceEdcafs[AC_NUMCATEGORIES]; + TestEdca sequenceEdca; + sequenceEdca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + sequenceInProgressFrames[ac].configure(&sequenceDataService, &sequenceAckHandler, &sequencePendingQueues[ac]); + sequenceEdcafs[ac].pendingQueue = &sequencePendingQueues[ac]; + sequenceEdcafs[ac].inProgressFrames = &sequenceInProgressFrames[ac]; + sequenceEdcafs[ac].setAccessCategory(AccessCategory(ac)); + sequenceEdca.edcafs[ac] = &sequenceEdcafs[ac]; + } + sequenceEdca.edcaf = &sequenceEdcafs[AC_BE]; + sequenceInProgressFrames[AC_BK].addFrame(new Packet("sequenceBackgroundFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(70)))); + sequenceInProgressFrames[AC_VO].addFrame(new Packet("sequenceVoiceFrame", makeQosHeader(peer1, 2, SequenceNumberCyclic(71)))); + sequenceEdcafs[AC_VO].contentionActive = true; + std::vector eventOrder; + sequenceEdcafs[AC_BE].eventOrder = &eventOrder; + TestTxopProcedure sequenceTxop; + sequenceTxop.eventOrder = &eventOrder; + sequenceEdcafs[AC_BE].setTxopProcedure(&sequenceTxop); + sequenceEdca.eventOrder = &eventOrder; + TestHcf sequenceHcf; + sequenceHcf.configureEligibilityIndex(&sequenceEdca, &sequenceDataService); + auto sequenceHandler = new TestFrameSequenceHandler(); + sequenceHcf.configureFrameSequenceHandler(sequenceHandler); + TestMac sequenceMac; + sequenceHcf.configureMac(&sequenceMac); + sequenceHcf.finishSequence(); + ASSERT(eventOrder == std::vector({ 1, 2, 3, 3 })); + ASSERT(sequenceEdca.requestedAccessCategories.size() == 2); + ASSERT(sequenceEdca.requestedAccessCategories[0] == AC_BK); + ASSERT(sequenceEdca.requestedAccessCategories[1] == AC_VO); + ASSERT(sequenceEdcafs[AC_VO].numRequestChannelCalls == 1); + ASSERT(sequenceEdcafs[AC_VO].numContentionStarts == 0); + auto releasedBackground = sequenceInProgressFrames[AC_BK].releaseFrames(); + auto releasedVoice = sequenceInProgressFrames[AC_VO].releaseFrames(); + for (auto frame : releasedBackground) + delete frame; + for (auto frame : releasedVoice) + delete frame; +} + +// IEEE Std 802.11-2024, 10.23.2.4: a collided AC with no eligible frame is +// not a handled collision and must not mutate recovery state or be counted. +{ + TestEdcaf emptyCollidedEdcaf; + emptyCollidedEdcaf.setAccessCategory(AC_BK); + TestInProgressFrames emptyInProgressFrames; + TestOriginatorQosMacDataService emptyDataService; + TestAckHandler emptyAckHandler; + TestPacketQueue emptyPendingQueue; + emptyInProgressFrames.configure(&emptyDataService, &emptyAckHandler, &emptyPendingQueue); + emptyCollidedEdcaf.inProgressFrames = &emptyInProgressFrames; + TestHcf collisionHcf; + ASSERT(collisionHcf.processInternalCollision({ &emptyCollidedEdcaf }) == 0); +} + +// A channel grant whose collision-controller vector contains only an AC with +// no eligible frame releases the channel without opening a TXOP or emitting a +// phantom collision statistic. +{ + TestPacketQueue grantedPendingQueue; + TestOriginatorQosMacDataService grantedDataService; + TestAckHandler grantedAckHandler; + TestInProgressFrames grantedInProgressFrames; + grantedInProgressFrames.configure(&grantedDataService, &grantedAckHandler, &grantedPendingQueue); + TestEdcaf grantedEdcaf; + grantedEdcaf.setAccessCategory(AC_BE); + grantedEdcaf.pendingQueue = &grantedPendingQueue; + grantedEdcaf.inProgressFrames = &grantedInProgressFrames; + TestEdca grantedEdca; + grantedEdca.edcaf = &grantedEdcaf; + grantedEdca.internallyCollidedEdcafs = { &grantedEdcaf }; + TestHcf grantedHcf; + grantedHcf.configure(&grantedEdca); + TestMac grantedMac; + grantedHcf.configureMac(&grantedMac); + TestSignalListener collisionListener; + grantedHcf.subscribe(Hcf::edcaCollisionDetectedSignal, &collisionListener); + grantedHcf.grantChannel(&grantedEdcaf); + ASSERT(grantedEdcaf.numReleaseChannelCalls == 1); + ASSERT(grantedMac.numPendingRadioConfigSends == 1); + ASSERT(collisionListener.numSignals == 0); + grantedHcf.unsubscribe(Hcf::edcaCollisionDetectedSignal, &collisionListener); +} + // A direct IPacketQueue implementation without OMNeT++ signals cannot leave // the index stale because ownership departures use the typed queue callback. { @@ -2065,6 +3197,192 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); secondQueue.removePacket(otherInputPacket); delete otherInputPacket; } + +// Discarding a stale setup frame clears its per-frame retry state without +// emitting the retry-limit semantic event. +{ + TestNonQosRecoveryProcedure recoveryProcedure; + TestSignalListener retryLimitListener; + recoveryProcedure.subscribe(IRecoveryProcedure::retryLimitReachedSignal, &retryLimitListener); + auto header = makeShared(); + header->setSequenceNumber(SequenceNumberCyclic(42)); + Packet packet("staleRecoveryPacket", header); + recoveryProcedure.addRetry(header); + ASSERT(recoveryProcedure.hasRetry(header)); + recoveryProcedure.discardFrame(&packet, header); + ASSERT(!recoveryProcedure.hasRetry(header)); + ASSERT(retryLimitListener.numSignals == 0); + recoveryProcedure.unsubscribe(IRecoveryProcedure::retryLimitReachedSignal, &retryLimitListener); +} + + +// Either terminal discard path clears both per-frame retry maps. Reusing the +// same sequence/fragment identity therefore starts from zero after either +// data-first or RTS-first failure history. +{ + TestNonQosRecoveryProcedure recoveryProcedure; + auto header = makeShared(); + header->setSequenceNumber(SequenceNumberCyclic(43)); + Packet shortPacket("shortTerminalDiscard", header); + recoveryProcedure.addShortRetry(header, 2); + recoveryProcedure.addLongRetry(header, 3); + ASSERT(recoveryProcedure.getShortRetryCount(&shortPacket, header) == 2); + ASSERT(recoveryProcedure.getLongRetryCount(&shortPacket, header) == 3); + recoveryProcedure.discardFrame(&shortPacket, header); + ASSERT(recoveryProcedure.getShortRetryCount(&shortPacket, header) == 0); + ASSERT(recoveryProcedure.getLongRetryCount(&shortPacket, header) == 0); + + Packet reusedPacket("reusedTerminalDiscard", header); + recoveryProcedure.addLongRetry(header, 4); + recoveryProcedure.addShortRetry(header, 5); + ASSERT(recoveryProcedure.getShortRetryCount(&reusedPacket, header) == 5); + ASSERT(recoveryProcedure.getLongRetryCount(&reusedPacket, header) == 4); + recoveryProcedure.discardRtsFrame(header); + ASSERT(recoveryProcedure.getShortRetryCount(&reusedPacket, header) == 0); + ASSERT(recoveryProcedure.getLongRetryCount(&reusedPacket, header) == 0); +} + +// A stale RTS-protected ADDBA request uses the management header for its retry +// check and clears the RTS short-retry entry even when the protected frame is +// long. It is a transaction discard, not a retry-limit or link-break event. +{ + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(10, 4, &cwCalculator); + QosAckHandler ackHandler; + TestOriginatorQosMacDataService originatorDataService; + TestInProgressFrames activeFrames; + activeFrames.configure(&originatorDataService, &ackHandler); + TestEdcaf edcaf; + edcaf.inProgressFrames = &activeFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + auto staleHandler = new TestOriginatorHandler(); + TestHcf hcf; + hcf.configureTransmittedData(&edca, staleHandler, &originatorPolicy); + TestSignalListener packetDropListener; + TestSignalListener linkBrokenListener; + hcf.subscribe(packetDroppedSignal, &packetDropListener); + hcf.subscribe(linkBrokenSignal, &linkBrokenListener); + auto request = makeShared(); + request->setReceiverAddress(peer1); + request->setTid(3); + request->setDialogToken(91); + request->setSequenceNumber(SequenceNumberCyclic(77)); + auto packet = new Packet("staleRtsProtectedAddba", request); + packet->insertAtBack(makeShared(B(100))); + ASSERT(packet->getByteLength() >= 10); + activeFrames.addFrame(packet); + ackHandler.processTransmittedDataOrMgmtFrame(request); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(request) == QosAckHandler::Status::WAITING_FOR_NORMAL_ACK); + hcf.processRtsProtectionFailure(packet); + ASSERT(recoveryProcedure.getShortRetryCount(packet, request) == 0); + ASSERT(recoveryProcedure.getLongRetryCount(packet, request) == 0); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(request) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(packetDropListener.numSignals == 1); + ASSERT(linkBrokenListener.numSignals == 0); + hcf.unsubscribe(packetDroppedSignal, &packetDropListener); + hcf.unsubscribe(linkBrokenSignal, &linkBrokenListener); + auto releasedFrames = activeFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 1); + delete releasedFrames.front(); +} + +// At the RTS retry limit, the same long protected management frame still +// clears its short-retry entry and emits each terminal event exactly once. +{ + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(10, 1, &cwCalculator); + QosAckHandler ackHandler; + TestOriginatorQosMacDataService originatorDataService; + TestInProgressFrames activeFrames; + activeFrames.configure(&originatorDataService, &ackHandler); + TestEdcaf edcaf; + edcaf.inProgressFrames = &activeFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + edca.nonQosRecoveryProcedure = &recoveryProcedure; + TestHcf hcf; + TestMac retryLimitedRtsProtectedAddbaMac; + hcf.configureMac(&retryLimitedRtsProtectedAddbaMac); + hcf.configureTransmittedData(&edca, new TestOriginatorHandler(), &originatorPolicy); + TestSignalListener retryLimitListener; + TestSignalListener packetDropListener; + TestSignalListener linkBrokenListener; + recoveryProcedure.subscribe(IRecoveryProcedure::retryLimitReachedSignal, &retryLimitListener); + hcf.subscribe(packetDroppedSignal, &packetDropListener); + hcf.subscribe(linkBrokenSignal, &linkBrokenListener); + auto request = makeShared(); + request->setReceiverAddress(peer1); + request->setTid(3); + request->setDialogToken(92); + request->setSequenceNumber(SequenceNumberCyclic(78)); + auto packet = new Packet("retryLimitedRtsProtectedAddba", request); + packet->insertAtBack(makeShared(B(100))); + ASSERT(packet->getByteLength() >= 10); + activeFrames.addFrame(packet); + ackHandler.processTransmittedDataOrMgmtFrame(request); + hcf.processRtsProtectionFailure(packet); + ASSERT(recoveryProcedure.getShortRetryCount(packet, request) == 0); + ASSERT(recoveryProcedure.getLongRetryCount(packet, request) == 0); + ASSERT(ackHandler.getMgmtOrNonQoSAckStatus(request) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(retryLimitListener.numSignals == 1); + ASSERT(packetDropListener.numSignals == 1); + ASSERT(linkBrokenListener.numSignals == 1); + recoveryProcedure.unsubscribe(IRecoveryProcedure::retryLimitReachedSignal, &retryLimitListener); + hcf.unsubscribe(packetDroppedSignal, &packetDropListener); + hcf.unsubscribe(linkBrokenSignal, &linkBrokenListener); + auto releasedFrames = activeFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 1); + delete releasedFrames.front(); +} + +// A frame prefetched for duration calculation already has an SSN, but it must +// remain in progress without being selected while the same transaction is pending. +TestAckHandler ackHandler; +TestInProgressFrames inProgressFrames; +inProgressFrames.configure(&dataService, &ackHandler); +auto prefetchedHeldFrame = new Packet("prefetchedHeldFrame", makeQosHeader(peer3, 5, SequenceNumberCyclic(21))); +auto prefetchedOtherTidFrame = new Packet("prefetchedOtherTidFrame", makeQosHeader(peer3, 4, SequenceNumberCyclic(1))); +inProgressFrames.addFrame(prefetchedHeldFrame); +inProgressFrames.addFrame(prefetchedOtherTidFrame); +ASSERT(inProgressFrames.hasEligibleFrame()); +ASSERT(inProgressFrames.getFrameToTransmit() == prefetchedOtherTidFrame); + +// Repeated cleanup requests move an in-progress frame to the discard list once. +{ + TestInProgressFrames idempotentDropFrames; + auto frame = new Packet("idempotentDropFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(22))); + idempotentDropFrames.addFrame(frame); + idempotentDropFrames.dropFrame(frame); + idempotentDropFrames.dropFrame(frame); + auto releasedFrames = idempotentDropFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 1); + ASSERT(releasedFrames.front() == frame); + delete frame; +} + +// DCF sequence predicates materialize the next eligible pending frame. +TestPacketQueue deferredQueue; +auto deferredFrame = new Packet("deferredFrame", makeQosHeader(peer2, 4, SequenceNumberCyclic(123))); +deferredQueue.packets = { deferredFrame }; +TestOriginatorQosMacDataService deferredDataService; +TestInProgressFrames deferredInProgressFrames; +deferredInProgressFrames.configure(&deferredDataService, &ackHandler, &deferredQueue); +bool hasDeferredInProgressFrame = deferredInProgressFrames.hasInProgressFrames(); +if (!hasDeferredInProgressFrame || deferredQueue.getNumPackets() != 0) + throw cRuntimeError("In-progress frame predicate did not materialize the eligible pending frame"); +ASSERT(hasDeferredInProgressFrame); +ASSERT(deferredQueue.getNumPackets() == 0); +ASSERT(deferredFrame->peekAtFront()->getSequenceNumber() == SequenceNumberCyclic(123)); +auto deferredFrames = deferredInProgressFrames.releaseFrames(); +ASSERT(deferredFrames.size() == 1); +delete deferredFrames.at(0); + // A real queue with a downstream collector predicate-dequeues a non-front // packet, records its residence and semantic pull once, and animates it to the // collector while retaining the front packet. @@ -2550,6 +3868,99 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); secondQueue->deleteModule(); bufferModule->deleteModule(); } + +// The multicast continuation predicate materializes its candidate once and +// reuses that result for both the TXOP and receiver-address decisions. +{ + TestOriginatorQosMacDataService multicastDataService; + TestPacketQueue multicastPendingQueue; + TestAckHandler multicastAckHandler; + auto multicastFrame = new Packet("multicastTxopFrame", makeQosHeader(MacAddress::BROADCAST_ADDRESS, 1, SequenceNumberCyclic(4))); + multicastPendingQueue.packets = { multicastFrame }; + TestInProgressFrames multicastInProgressFrames; + multicastInProgressFrames.configure(&multicastDataService, &multicastAckHandler, &multicastPendingQueue); + TestTxopProcedure multicastTxopProcedure; + FrameSequenceContext multicastContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &multicastInProgressFrames, nullptr, nullptr, nullptr, new QoSContext(nullptr, nullptr, nullptr, &multicastTxopProcedure)); + HcfFs multicastHcfFs; + RepeatingFs multicastRepeatingFs(new TestEmptyFrameSequence(), [](RepeatingFs *, FrameSequenceContext *) { return false; }); + ASSERT(multicastHcfFs.hasMoreTxOpsAndMulticast(&multicastRepeatingFs, &multicastContext)); + ASSERT(multicastPendingQueue.numSelectedDequeues == 1); + ASSERT(multicastPendingQueue.getNumPackets() == 0); + auto multicastFrames = multicastInProgressFrames.releaseFrames(); + ASSERT(multicastFrames == std::vector({ multicastFrame })); + delete multicastFrame; +} + +// At the TXOP continuation boundary, an eligible pending frame is +// materialized by the real repeating-sequence lifecycle. +{ + TestOriginatorQosMacDataService txopDataService; + TestPacketQueue txopPendingQueue; + auto initialTxopFrame = new Packet("initialTxopFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(2))); + auto continuedTxopFrame = new Packet("continuedTxopFrame", makeQosHeader(peer1, 1, SequenceNumberCyclic(3))); + txopPendingQueue.packets = { continuedTxopFrame }; + TestInProgressFrames txopInProgressFrames; + txopInProgressFrames.configure(&txopDataService, &ackHandler, &txopPendingQueue); + txopInProgressFrames.addFrame(initialTxopFrame); + TestTxopProcedure txopProcedure; + FrameSequenceContext txopContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &txopInProgressFrames, nullptr, nullptr, nullptr, new QoSContext(nullptr, nullptr, nullptr, &txopProcedure)); + HcfFs hcfFs; + RepeatingFs repeatingFs(new TestEmptyFrameSequence(), [&hcfFs](RepeatingFs *frameSequence, FrameSequenceContext *context) { return hcfFs.hasMoreTxOps(frameSequence, context); }); + repeatingFs.startSequence(&txopContext, 0); + ASSERT(repeatingFs.getCount() == 1); + ASSERT(txopPendingQueue.getNumPackets() == 1); + txopInProgressFrames.dropFrame(initialTxopFrame); + repeatingFs.repeatSequence(&txopContext); + ASSERT(repeatingFs.getCount() == 2); + ASSERT(txopPendingQueue.getNumPackets() == 0); + ASSERT(txopPendingQueue.numSelectedDequeues == 1); + ASSERT(txopInProgressFrames.getFrameToTransmit() == continuedTxopFrame); + auto txopFrames = txopInProgressFrames.releaseFrames(); + ASSERT(txopFrames.size() == 2); + for (auto frame : txopFrames) + delete frame; +} + +// Legacy DCF materializes its first queued frame before starting DcfFs, and a +// stale empty grant is released without dereferencing a null frame. +{ + TestOriginatorQosMacDataService dcfDataService; + TestPacketQueue dcfPendingQueue; + TestInProgressFrames dcfInProgressFrames; + dcfInProgressFrames.configure(&dcfDataService, &ackHandler, &dcfPendingQueue); + TestDcaf dcaf; + dcaf.configure(&dcfInProgressFrames); + TestDcf dcf; + TestMac dcfMac; + auto dcfHandler = dcf.configure(&dcaf, &dcfMac); + dcf.grant(&dcaf); + ASSERT(dcaf.numReleasedChannels == 1); + ASSERT(dcfMac.numPendingRadioConfigSends == 1); + ASSERT(dcfHandler->numStartedSequences == 0); +} +{ + TestOriginatorQosMacDataService dcfDataService; + TestPacketQueue dcfPendingQueue; + auto firstDcfFrame = new Packet("firstDcfFrame", makeQosHeader(peer1, 0, SequenceNumberCyclic(0))); + dcfPendingQueue.packets = { firstDcfFrame }; + TestInProgressFrames dcfInProgressFrames; + dcfInProgressFrames.configure(&dcfDataService, &ackHandler, &dcfPendingQueue); + TestDcaf dcaf; + dcaf.configure(&dcfInProgressFrames); + TestDcf dcf; + TestMac dcfMac; + auto dcfHandler = dcf.configure(&dcaf, &dcfMac); + dcf.grant(&dcaf); + ASSERT(dcaf.numReleasedChannels == 0); + ASSERT(dcfMac.numPendingRadioConfigSends == 0); + ASSERT(dcfHandler->numStartedSequences == 1); + ASSERT(dcfPendingQueue.getNumPackets() == 0); + ASSERT(dcfInProgressFrames.getFrameToTransmit() == firstDcfFrame); + auto dcfFrames = dcfInProgressFrames.releaseFrames(); + ASSERT(dcfFrames.size() == 1); + delete dcfFrames.at(0); +} + EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; %contains: stdout From 9d0bd2a7176a216eff0a7e14c87d3f53eaaeae29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:03 +0200 Subject: [PATCH 31/56] ieee80211: reassemble fragmented action frames before dispatch Carry Action-frame-specific context in a local tag while transmitting fragments with a generic management header and a serialized body. Extend the serializer, dissector, fragmentation, and defragmentation paths so the original Action header can be reconstructed after all fragments arrive. Route fragmented management frames through recipient reassembly and call ADDBA or DELBA handlers only with the complete header. This prevents a partial fragment from starting, changing, or tearing down a Block Ack agreement. Cover on-air representation, out-of-order fragments, duplicates, expiration, and both QoS and non-QoS recipient dispatch. --- .../ieee80211/mac/Ieee80211Frame.msg | 2 +- .../mac/Ieee80211MacHeaderSerializer.cc | 16 + .../mac/Ieee80211MacProtocolDissector.cc | 13 +- .../OriginatorBlockAckAgreementHandler.cc | 11 +- .../RecipientBlockAckAgreementHandler.cc | 4 + .../ieee80211/mac/contract/IReassembly.h | 18 +- .../contract/IRecipientQosMacDataService.h | 6 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 24 +- .../fragmentation/BasicFragmentationPolicy.cc | 28 +- .../mac/fragmentation/BasicReassembly.cc | 77 +++- .../mac/fragmentation/BasicReassembly.h | 18 +- .../mac/fragmentation/Defragmentation.cc | 14 +- .../mac/fragmentation/Fragmentation.cc | 55 ++- .../Ieee80211FragmentedActionContextTag.cc | 15 + .../Ieee80211FragmentedActionContextTag.h | 48 +++ .../mac/recipient/RecipientMacDataService.cc | 34 +- .../mac/recipient/RecipientMacDataService.h | 5 +- .../mac/recipient/RecipientMacDataService.ned | 2 +- .../recipient/RecipientQosMacDataService.cc | 61 ++- .../recipient/RecipientQosMacDataService.h | 4 + .../recipient/RecipientQosMacDataService.ned | 2 +- tests/unit/Ieee80211AddbaTransaction_1.test | 408 ++++++++++++++++-- 22 files changed, 769 insertions(+), 96 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.cc create mode 100644 src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg index 033ee7141be..184c836eb30 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg @@ -312,7 +312,7 @@ class Ieee80211AddbaResponse extends Ieee80211ActionFrame // class Ieee80211Delba extends Ieee80211ActionFrame { - chunkLength = LENGTH_DELBA; + chunkLength = LENGTH_DELBA - B(4); category = 3; // Category field is set to 3 (representing DELBA). (1 byte) blockAckAction = 2; // Block Ack Action field is set to 2 (representing DELBA). (1 byte) diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc index d6c90abeb3c..b9937549479 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc @@ -184,6 +184,10 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c stream.writeUint32Be(0); if (type == ST_ACTION) { auto actionFrame = dynamicPtrCast(chunk); + // A fragmented action MPDU uses a generic management header; + // its action-body slice is a separate packet chunk. + if (actionFrame == nullptr) + break; switch (actionFrame->getCategory()) { case 3: { stream.writeByte(actionFrame->getCategory()); @@ -423,6 +427,17 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre actionFrame->setSequenceNumber(sequenceNumber); if (order) stream.readUint32Be(); + if (actionFrame->getMoreFragments() || actionFrame->getFragmentNumber() != 0) { + auto mgmtHeader = makeShared(); + copyBasicFields(mgmtHeader, macHeader); + mgmtHeader->setDurationField(actionFrame->getDurationField()); + mgmtHeader->setReceiverAddress(actionFrame->getReceiverAddress()); + mgmtHeader->setTransmitterAddress(actionFrame->getTransmitterAddress()); + mgmtHeader->setAddress3(actionFrame->getAddress3()); + mgmtHeader->setFragmentNumber(actionFrame->getFragmentNumber()); + mgmtHeader->setSequenceNumber(actionFrame->getSequenceNumber()); + return mgmtHeader; + } actionFrame->setCategory(stream.readByte()); switch (actionFrame->getCategory()) { case 3: { @@ -460,6 +475,7 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre case 2: { auto delba = makeShared(); copyBasicFields(delba, macHeader); + copyActionFrameFields(delba, actionFrame); delba->setBlockAckAction(blockAckAction); delba->setReserved(stream.readNBitsToUint64Be(11)); delba->setInitiator(stream.readBit()); diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacProtocolDissector.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacProtocolDissector.cc index 36ec42ce51d..de27699aefd 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacProtocolDissector.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacProtocolDissector.cc @@ -65,10 +65,14 @@ void Ieee80211MacProtocolDissector::dissect(Packet *packet, const Protocol *prot else callback.dissectPacket(packet, computeLlcProtocol(packet)); } - else if (dynamicPtrCast(header)) - ASSERT(packet->getDataLength() == b(0)); - else if (dynamicPtrCast(header)) - callback.dissectPacket(packet, &Protocol::ieee80211Mgmt); + else if (auto mgmtHeader = dynamicPtrCast(header)) { + if (mgmtHeader->getMoreFragments() || mgmtHeader->getFragmentNumber() != 0) + callback.dissectPacket(packet, nullptr); + else if (dynamicPtrCast(header)) + ASSERT(packet->getDataLength() == b(0)); + else + callback.dissectPacket(packet, &Protocol::ieee80211Mgmt); + } // TODO else if (dynamicPtrCast(header)) else ASSERT(packet->getDataLength() == b(0)); @@ -77,4 +81,3 @@ void Ieee80211MacProtocolDissector::dissect(Packet *packet, const Protocol *prot } } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index d03bae7b498..2094435bf43 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -11,6 +11,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" namespace inet { @@ -338,7 +339,11 @@ void OriginatorBlockAckAgreementHandler::processDroppedAddbaReq(Packet *packet, std::unique_ptr OriginatorBlockAckAgreementHandler::processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - auto delba = packet->peekAtFront(); + auto delba = findFragmentedActionContext(packet); + // IEEE Std 802.11-2024, 10.4 and 11.5.3.2: an untagged DELBA MMPDU + // likewise cannot tear down the agreement before its final fragment. + if (delba->getMoreFragments()) + return nullptr; auto transactionTag = packet->findTag(); if (transactionTag != nullptr) { auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); @@ -364,7 +369,7 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - auto delba = packet->peekAtFront(); + auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator() || delba->getMoreFragments()) return false; auto transactionTag = packet->findTag(); @@ -381,7 +386,7 @@ bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet bool OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { - auto delba = packet->peekAtFront(); + auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator()) return false; auto transactionTag = packet->findTag(); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 8956059d80c..bd4851bb0a6 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -172,6 +172,10 @@ void RecipientBlockAckAgreementHandler::processDuplicateAddbaRequest(const Ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) { + // IEEE Std 802.11-2024, 10.4 and 11.5.3.5: the DELBA MMPDU has not + // been transmitted while a later fragment is still outstanding. + if (delba->getMoreFragments()) + return nullptr; return std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); } diff --git a/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h index 23796ec3556..14285b3689a 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h @@ -8,9 +8,14 @@ #ifndef __INET_IREASSEMBLY_H #define __INET_IREASSEMBLY_H +#include + #include "inet/linklayer/common/MacAddress.h" namespace inet { + +class Packet; + namespace ieee80211 { class Ieee80211DataOrMgmtHeader; @@ -32,6 +37,18 @@ class INET_API IReassembly */ virtual Packet *addFragment(Packet *frame) = 0; + /** + * Return the earliest receive-lifetime deadline among incomplete frames, + * or SIMTIME_MAX when the reassembly buffer is empty. + */ + virtual simtime_t getNextExpirationTime() const = 0; + + /** + * Remove incomplete frames whose receive lifetime has elapsed and return + * their fragments to the caller for drop signaling and deletion. + */ + virtual std::vector removeExpiredFragments(simtime_t currentTime) = 0; + /** * Discard fragments from the reassembly buffer. Frames are identified by the transmitter * address, the TID, and the sequence number range [startSeqNumber, endSeqNumber[. @@ -44,4 +61,3 @@ class INET_API IReassembly } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h index 108a499727f..2e7436303de 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosMacDataService.h @@ -18,11 +18,11 @@ namespace ieee80211 { class INET_API IRecipientQosMacDataService { public: - // A duplicate has already been consumed and must not be processed as a - // management body; the coordination function may apply a subtype-specific - // response rule using the duplicate flag. struct ManagementFrameReceptionResult { std::vector completeFrames; + // IEEE Std 802.11-2024, 10.5: a management body becomes available to + // the coordination function only after the complete MMPDU is present. + Ptr completeHeader; bool duplicate = false; }; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 3f8cefd9e20..37c11a6de03 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -17,6 +17,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" @@ -70,9 +71,9 @@ void Hcf::initialize(int stage) originatorBlockAckProcedure = new OriginatorBlockAckProcedure(); recipientBlockAckProcedure = new RecipientBlockAckProcedure(); originatorDataService->setFrameEligibilityFunction([this](const Packet *packet) { - if (auto addbaReq = dynamicPtrCast(packet->peekAtFront())) + if (auto addbaReq = findFragmentedActionContext(packet)) return originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq); - if (auto delba = dynamicPtrCast(packet->peekAtFront())) + if (auto delba = findFragmentedActionContext(packet)) return originatorBlockAckAgreementHandler->isDelbaPending(packet, delba); auto dataHeader = dynamicPtrCast(packet->peekAtFront()); // Hold this peer/TID while its ADDBA response is pending so no @@ -133,7 +134,7 @@ void Hcf::rebuildPendingFrameEligibility() bool Hcf::processDroppedBlockAckSetupFrame(Packet *packet) { if (originatorBlockAckAgreementHandler) { - auto addbaReq = dynamicPtrCast(packet->peekAtFront()); + auto addbaReq = findFragmentedActionContext(packet); if (addbaReq != nullptr && originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq)) { originatorBlockAckAgreementHandler->processDroppedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); rebuildPendingFrameEligibility(); @@ -146,7 +147,7 @@ bool Hcf::processDroppedBlockAckSetupFrame(Packet *packet) bool Hcf::processDroppedBlockAckTeardownFrame(Packet *packet) { if (originatorBlockAckAgreementHandler) { - auto delba = dynamicPtrCast(packet->peekAtFront()); + auto delba = findFragmentedActionContext(packet); if (delba != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this)) { rebuildPendingFrameEligibility(); return true; @@ -534,7 +535,8 @@ void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr(header)) { auto receptionResult = recipientDataService->managementFrameReceived(packet, mgmtHeader); sendUp(receptionResult.completeFrames); - recipientProcessReceivedManagementFrame(mgmtHeader, receptionResult.duplicate); + if (receptionResult.completeHeader != nullptr) + recipientProcessReceivedManagementFrame(receptionResult.completeHeader, receptionResult.duplicate); } else { // TODO else if (auto ctrlFrame = dynamic_cast(frame)) sendUp(recipientDataService->controlFrameReceived(packet, header, recipientBlockAckAgreementHandler)); @@ -657,7 +659,7 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) } else throw cRuntimeError("Unknown frame"); // TODO QoSDataFrame, NonQoSDataFrame - auto addbaRequest = dynamicPtrCast(protectedHeader); + auto addbaRequest = findFragmentedActionContext(packet); bool staleAddbaRequest = addbaRequest != nullptr && originatorBlockAckAgreementHandler != nullptr && !originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaRequest); if (retryLimitReached || staleAddbaRequest) { if (retryLimitReached) { @@ -733,13 +735,13 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< auto edcaf = edca->getEdcaf(ac); if (originatorAckPolicy->isAckNeeded(mgmtHeader)) edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(mgmtHeader); - if (auto addbaReq = dynamicPtrCast(mgmtHeader)) { + if (auto addbaReq = findFragmentedActionContext(packet)) { if (originatorBlockAckAgreementHandler) originatorBlockAckAgreementHandler->processTransmittedAddbaReq(packet, addbaReq, originatorBlockAckAgreementPolicy, this); } - else if (dynamicPtrCast(mgmtHeader)) + else if (findFragmentedActionContext(packet)) ; // Recipient agreement was established when the successful response was formed. - else if (auto delba = dynamicPtrCast(mgmtHeader)) { + else if (auto delba = findFragmentedActionContext(packet)) { if (delba->getInitiator()) { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getReceiverAddress(), delba->getTid()); auto agreement = originatorBlockAckAgreementHandler->processTransmittedDelba(packet, this); @@ -807,7 +809,7 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) } else throw cRuntimeError("Unknown frame"); // TODO qos, nonqos - auto addbaRequest = dynamicPtrCast(failedHeader); + auto addbaRequest = findFragmentedActionContext(failedPacket); bool staleAddbaRequest = addbaRequest != nullptr && originatorBlockAckAgreementHandler != nullptr && !originatorBlockAckAgreementHandler->isAddbaRequestPending(failedPacket, addbaRequest); if (retryLimitReached || staleAddbaRequest) { if (retryLimitReached) { @@ -900,7 +902,7 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); - if (auto delba = dynamicPtrCast(lastTransmittedHeader)) { + if (auto delba = findFragmentedActionContext(lastTransmittedPacket)) { if (delba->getInitiator() && originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this)) rebuildPendingFrameEligibility(); } diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc index e47ce922aab..9af560f9b3e 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc @@ -29,23 +29,38 @@ std::vector BasicFragmentationPolicy::computeFragmentSizes(Packet *frame) std::vector sizes; int payloadLength = 0; int headerLength = 0; - // Mgmt frames don't have payload const auto& header = frame->peekAtFront(); + // IEEE Std 802.11-2024, 10.4: only individually addressed MPDUs + // carrying an MSDU or MMPDU are eligible for fragmentation. + if (header->getReceiverAddress().isMulticast()) + return {}; const auto& trailer = frame->peekAtBack(B(4)); int trailerLength = trailer->getChunkLength().get(); if (dynamicPtrCast(header)) { headerLength = header->getChunkLength().get(); payloadLength = frame->getByteLength() - headerLength - trailerLength; } + else if (dynamicPtrCast(header)) { + // Management subclasses currently combine the common MAC header + // and typed MMPDU body. Only the common header is repeated. + headerLength = makeShared()->getChunkLength().get(); + payloadLength = frame->getByteLength() - headerLength - trailerLength; + } else - headerLength = frame->getByteLength(); + return {}; int maxFragmentPayload = fragmentationThreshold - headerLength - trailerLength; + if (maxFragmentPayload <= 0) + throw cRuntimeError("Fragmentation threshold %d is not larger than the %d byte header and trailer", fragmentationThreshold, headerLength + trailerLength); + // IEEE Std 802.11-2024, 10.4: all non-final fragments have the same + // even number of body octets; only the final fragment may be odd. + maxFragmentPayload &= ~1; + if (maxFragmentPayload == 0) + throw cRuntimeError("Fragmentation threshold %d leaves no even-length fragment body", fragmentationThreshold); if (payloadLength > maxFragmentPayload * MAX_NUM_FRAGMENTS) throw cRuntimeError("Fragmentation: frame \"%s\" too large, won't fit into %d fragments", frame->getName(), MAX_NUM_FRAGMENTS); - for (int i = 0; headerLength + trailerLength + payloadLength > fragmentationThreshold; i++) { - auto size = fragmentationThreshold - headerLength - trailerLength; - EV_TRACE << "Computed fragment: i = " << i << ", size = " << size << ".\n"; - sizes.push_back(size); + for (int i = 0; payloadLength > maxFragmentPayload; i++) { + EV_TRACE << "Computed fragment: i = " << i << ", size = " << maxFragmentPayload << ".\n"; + sizes.push_back(maxFragmentPayload); payloadLength -= maxFragmentPayload; } if (payloadLength != 0) { @@ -61,4 +76,3 @@ std::vector BasicFragmentationPolicy::computeFragmentSizes(Packet *frame) } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc index 2f32f0207b9..0b000dab927 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc @@ -26,12 +26,11 @@ Packet *BasicReassembly::addFragment(Packet *packet) // Frame is not fragmented if (!header->getMoreFragments() && header->getFragmentNumber() == 0) return packet; - // FIXME temporary fix for mgmt frames - if (dynamicPtrCast(header)) - return packet; // find entry for this frame Key key; key.macAddress = header->getTransmitterAddress(); + key.receiverAddress = header->getReceiverAddress(); + key.type = header->getType(); key.tid = -1; if (header->getType() == ST_DATA_WITH_QOS) if (const Ptr& qosDataHeader = dynamicPtrCast(header)) @@ -39,7 +38,25 @@ Packet *BasicReassembly::addFragment(Packet *packet) key.seqNum = header->getSequenceNumber().get(); short fragNum = header->getFragmentNumber(); ASSERT(fragNum >= 0 && fragNum < MAX_NUM_FRAGMENTS); - auto& value = fragmentsMap[key]; + auto it = fragmentsMap.find(key); + if (it != fragmentsMap.end() && it->second.expired) { + // A non-Retry fragment 0 can be a new MMPDU after sequence-number + // reuse. All other fragments of the expired MMPDU are discarded. + if (fragNum == 0 && !header->getRetry()) { + fragmentsMap.erase(it); + it = fragmentsMap.end(); + } + else { + delete packet; + return nullptr; + } + } + if (it == fragmentsMap.end()) { + Value value; + value.receptionStartTime = simTime(); + it = fragmentsMap.emplace(key, value).first; + } + auto& value = it->second; value.fragments.resize(16); // update entry @@ -71,28 +88,47 @@ Packet *BasicReassembly::addFragment(Packet *packet) return nullptr; } -void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) +simtime_t BasicReassembly::getNextExpirationTime() const { - Key key; - key.macAddress = address; - key.tid = tid; - key.seqNum = startSeqNumber; - auto itStart = fragmentsMap.lower_bound(key); - key.seqNum = endSeqNumber; - auto itEnd = fragmentsMap.upper_bound(key); + simtime_t nextExpirationTime = SIMTIME_MAX; + for (const auto& entry : fragmentsMap) + if (!entry.second.expired) + nextExpirationTime = std::min(nextExpirationTime, entry.second.receptionStartTime + maxReceiveLifetime); + return nextExpirationTime; +} - if (endSeqNumber < startSeqNumber) { - for (auto it = itStart; it != fragmentsMap.end();) { +std::vector BasicReassembly::removeExpiredFragments(simtime_t currentTime) +{ + std::vector expiredFragments; + for (auto& entry : fragmentsMap) { + auto& value = entry.second; + if (!value.expired && currentTime >= value.receptionStartTime + maxReceiveLifetime) { + for (auto fragment : value.fragments) + if (fragment != nullptr) + expiredFragments.push_back(fragment); + value.fragments.clear(); + value.receivedFragments = 0; + value.allFragments = 0; + value.expired = true; + } + } + return expiredFragments; +} + +void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) +{ + for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { + auto sequenceNumber = it->first.seqNum; + bool isInSequenceRange = startSeqNumber <= endSeqNumber ? + sequenceNumber >= startSeqNumber && sequenceNumber <= endSeqNumber : + sequenceNumber >= startSeqNumber || sequenceNumber <= endSeqNumber; + if (it->first.macAddress == address && it->first.tid == tid && isInSequenceRange) { for (auto fragment : it->second.fragments) delete fragment; it = fragmentsMap.erase(it); } - itStart = fragmentsMap.begin(); - } - for (auto it = itStart; it != itEnd;) { - for (auto fragment : it->second.fragments) - delete fragment; - it = fragmentsMap.erase(it); + else + it++; } } @@ -105,4 +141,3 @@ BasicReassembly::~BasicReassembly() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h index 0c1d7d810cc..cdcfd693727 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h @@ -8,7 +8,10 @@ #ifndef __INET_BASICREASSEMBLY_H #define __INET_BASICREASSEMBLY_H +#include + #include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" #include "inet/linklayer/ieee80211/mac/contract/IReassembly.h" @@ -20,27 +23,36 @@ class INET_API BasicReassembly : public IReassembly, public cObject protected: struct Key { MacAddress macAddress; + MacAddress receiverAddress; + Ieee80211FrameType type; Tid tid; SequenceNumber seqNum; - bool operator==(const Key& o) const { return macAddress == o.macAddress && tid == o.tid && seqNum == o.seqNum; } - bool operator<(const Key& o) const { return macAddress < o.macAddress || (macAddress == o.macAddress && (tid < o.tid || (tid == o.tid && seqNum < o.seqNum))); } + auto asTuple() const { return std::tie(macAddress, receiverAddress, type, tid, seqNum); } + bool operator==(const Key& other) const { return asTuple() == other.asTuple(); } + bool operator<(const Key& other) const { return asTuple() < other.asTuple(); } }; struct Value { std::vector fragments; uint16_t receivedFragments = 0; // each bit corresponds to a fragment number uint16_t allFragments = 0; // bits for all fragments set to one (0..numFragments-1); 0 means unfilled + simtime_t receptionStartTime; + bool expired = false; }; typedef std::map FragmentsMap; FragmentsMap fragmentsMap; + simtime_t maxReceiveLifetime; public: + BasicReassembly() : maxReceiveLifetime(SimTime(512 * 1024, SIMTIME_US)) {} + BasicReassembly(simtime_t maxReceiveLifetime) : maxReceiveLifetime(maxReceiveLifetime) {} virtual ~BasicReassembly(); virtual Packet *addFragment(Packet *packet) override; virtual void purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) override; + virtual simtime_t getNextExpirationTime() const override; + virtual std::vector removeExpiredFragments(simtime_t currentTime) override; }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.cc index 091c635504a..b370cf1bcc9 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.cc @@ -7,6 +7,7 @@ #include "inet/linklayer/ieee80211/mac/fragmentation/Defragmentation.h" +#include "inet/common/packet/chunk/BytesChunk.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" namespace inet { @@ -33,10 +34,21 @@ Packet *Defragmentation::defragmentFrames(std::vector *fragmentFrames) defragmentedHeader->setMoreFragments(false); defragmentedFrame->insertAtFront(defragmentedHeader); defragmentedFrame->insertAtBack(makeShared()); + if (defragmentedHeader->getType() == ST_ACTION && defragmentedHeader->getChunkLength() == makeShared()->getChunkLength()) { + // Decode the completed action from the reassembled on-air bytes; an + // individual fragment intentionally has no typed action body. + const auto& trailer = defragmentedFrame->popAtBack(B(4)); + auto decodedFrame = new Packet(defragmentedFrame->getName(), defragmentedFrame->peekDataAsBytes()); + decodedFrame->insertAtBack(trailer); + decodedFrame->copyTags(*defragmentedFrame); + decodedFrame->getRegionTags().copyTags(defragmentedFrame->getRegionTags(), defragmentedFrame->getFrontOffset(), decodedFrame->getFrontOffset(), defragmentedFrame->getDataLength()); + decodedFrame->peekAtFront(); + delete defragmentedFrame; + defragmentedFrame = decodedFrame; + } EV_TRACE << "Created " << *defragmentedFrame << ".\n"; return defragmentedFrame; } } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc index 96ed3de0d9b..4465c22ecf3 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc @@ -7,11 +7,40 @@ #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/common/packet/chunk/BytesChunk.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" namespace inet { namespace ieee80211 { +namespace { + +const Ptr copyManagementHeader(const Ptr& source) +{ + auto destination = makeShared(); + destination->setType(source->getType()); + destination->setToDS(source->getToDS()); + destination->setFromDS(source->getFromDS()); + destination->setMoreFragments(source->getMoreFragments()); + destination->setRetry(source->getRetry()); + destination->setPowerMgmt(source->getPowerMgmt()); + destination->setMoreData(source->getMoreData()); + destination->setProtectedFrame(source->getProtectedFrame()); + destination->setOrder(source->getOrder()); + destination->setDurationField(source->getDurationField()); + destination->setAID(source->getAID()); + destination->setReceiverAddress(source->getReceiverAddress()); + destination->setMACArrive(source->getMACArrive()); + destination->setTransmitterAddress(source->getTransmitterAddress()); + destination->setAddress3(source->getAddress3()); + destination->setFragmentNumber(source->getFragmentNumber()); + destination->setSequenceNumber(source->getSequenceNumber()); + return destination; +} + +} // namespace + Register_Class(Fragmentation); std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::vector& fragmentSizes) @@ -21,6 +50,21 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve std::vector *fragments = new std::vector(); const auto& frameHeader = frame->popAtFront(); frame->popAtBack(B(4)); + const auto& actionFrame = dynamicPtrCast(frameHeader); + if (actionFrame != nullptr) { + // IEEE Std 802.11-2024, 10.4: a fragment frame body carries only a + // portion of the MMPDU. Move the action body out of INET's combined + // typed header before slicing it into fragment bodies. + Packet serializedHeader("serializedActionHeader", frameHeader); + const auto& headerBytes = serializedHeader.peekDataAsBytes()->getBytes(); + auto bodyOffset = makeShared()->getChunkLength().get(); + frame->insertAtFront(makeShared(std::vector(headerBytes.begin() + bodyOffset, headerBytes.end()))); + } + B totalFragmentBodyLength = B(0); + for (auto fragmentSize : fragmentSizes) + totalFragmentBodyLength += B(fragmentSize); + if (totalFragmentBodyLength != frame->getDataLength()) + throw cRuntimeError("Fragment sizes total %s but frame body length is %s", totalFragmentBodyLength.str().c_str(), frame->getDataLength().str().c_str()); for (size_t i = 0; i < fragmentSizes.size(); i++) { bool lastFragment = i == fragmentSizes.size() - 1; std::string name = std::string(frame->getName()) + "-frag" + std::to_string(i); @@ -30,7 +74,16 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve fragment->copyTags(*frame); fragment->getRegionTags().copyTags(frame->getRegionTags(), frame->getFrontOffset() + offset, fragment->getFrontOffset(), length); offset += length; - const auto& fragmentHeader = staticPtrCast(frameHeader->dupShared()); + Ptr fragmentHeader; + if (actionFrame != nullptr) { + fragmentHeader = copyManagementHeader(actionFrame); + auto actionContext = staticPtrCast(actionFrame->dupShared()); + actionContext->setFragmentNumber(i); + actionContext->setMoreFragments(!lastFragment); + fragment->addTag()->setActionFrame(actionContext); + } + else + fragmentHeader = staticPtrCast(frameHeader->dupShared()); fragmentHeader->setSequenceNumber(frameHeader->getSequenceNumber()); fragmentHeader->setFragmentNumber(i); fragmentHeader->setMoreFragments(!lastFragment); diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.cc new file mode 100644 index 00000000000..1256c20cb64 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.cc @@ -0,0 +1,15 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" + +namespace inet { +namespace ieee80211 { + +Register_Class(Ieee80211FragmentedActionContextTag); + +} // namespace ieee80211 +} // namespace inet diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h b/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h new file mode 100644 index 00000000000..4c853dc3f2f --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h @@ -0,0 +1,48 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IEEE80211FRAGMENTEDACTIONCONTEXTTAG_H +#define __INET_IEEE80211FRAGMENTEDACTIONCONTEXTTAG_H + +#include "inet/common/TagBase.h" +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" + +namespace inet { +namespace ieee80211 { + +/** + * Sender-local action context for an MPDU fragment whose packet content + * contains only the common management header and its action-body slice. + */ +class INET_API Ieee80211FragmentedActionContextTag : public TagBase +{ + protected: + Ptr actionFrame; + + public: + Ieee80211FragmentedActionContextTag() {} + Ieee80211FragmentedActionContextTag(const Ieee80211FragmentedActionContextTag& other) : TagBase(other), actionFrame(other.actionFrame) {} + + virtual Ieee80211FragmentedActionContextTag *dup() const override { return new Ieee80211FragmentedActionContextTag(*this); } + + const Ptr& getActionFrame() const { return actionFrame; } + void setActionFrame(const Ptr& actionFrame) { this->actionFrame = actionFrame; } +}; + +template +const Ptr findFragmentedActionContext(const Packet *packet) +{ + if (auto contextTag = packet->findTag()) + return dynamicPtrCast(contextTag->getActionFrame()); + else + return dynamicPtrCast(packet->peekAtFront()); +} + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc index 5075d391e89..e903e860596 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc @@ -19,12 +19,42 @@ Define_Module(RecipientMacDataService); void RecipientMacDataService::initialize() { duplicateRemoval = new LegacyDuplicateRemoval(); - basicReassembly = new BasicReassembly(); + basicReassembly = new BasicReassembly(par("maxReceiveLifetime")); + reassemblyTimer = new cMessage("reassemblyTimer"); +} + +void RecipientMacDataService::handleMessage(cMessage *message) +{ + if (message != reassemblyTimer) + throw cRuntimeError("Unknown message"); + expireReassemblyFragments(); + scheduleReassemblyTimer(); +} + +void RecipientMacDataService::expireReassemblyFragments() +{ + for (auto packet : basicReassembly->removeExpiredFragments(simTime())) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } +} + +void RecipientMacDataService::scheduleReassemblyTimer() +{ + if (reassemblyTimer->isScheduled()) + cancelEvent(reassemblyTimer); + auto nextExpirationTime = basicReassembly->getNextExpirationTime(); + if (nextExpirationTime != SIMTIME_MAX) + scheduleAt(nextExpirationTime, reassemblyTimer); } Packet *RecipientMacDataService::defragment(Packet *dataOrMgmtFrame) { + expireReassemblyFragments(); Packet *packet = basicReassembly->addFragment(dataOrMgmtFrame); + scheduleReassemblyTimer(); if (packet && packet->peekAtFront()) { emit(packetDefragmentedSignal, packet); return packet; @@ -72,10 +102,10 @@ std::vector RecipientMacDataService::controlFrameReceived(Packet *cont RecipientMacDataService::~RecipientMacDataService() { + cancelAndDelete(reassemblyTimer); delete duplicateRemoval; delete basicReassembly; } } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h index 63fa2007877..44342ad6569 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h @@ -27,6 +27,7 @@ class INET_API RecipientMacDataService : public SimpleModule, public IRecipientM protected: IReassembly *basicReassembly = nullptr; // FIXME use Defragmentation + cMessage *reassemblyTimer = nullptr; // MpduHeaderAndFcsValidation *mpduHeaderAndFcsValidation = nullptr; // Address1Filtering *address1Filtering = nullptr; @@ -37,6 +38,9 @@ class INET_API RecipientMacDataService : public SimpleModule, public IRecipientM protected: virtual void initialize() override; + virtual void handleMessage(cMessage *message) override; + virtual void expireReassemblyFragments(); + virtual void scheduleReassemblyTimer(); virtual Packet *defragment(Packet *dataOrMgmtFrame); virtual std::vector dataOrMgmtFrameReceived(Packet *packet, const Ptr& header); @@ -52,4 +56,3 @@ class INET_API RecipientMacDataService : public SimpleModule, public IRecipientM } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned index eff3279ebca..8e2b3b35f88 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned @@ -19,6 +19,7 @@ module RecipientMacDataService extends Module { parameters: @class(RecipientMacDataService); + double maxReceiveLifetime @unit(s) = default(524288us); // IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs @display("i=block/join"); @signal[packetDefragmented](type=inet::Packet); @signal[packetDeaggregated](type=inet::Packet); @@ -27,4 +28,3 @@ module RecipientMacDataService extends Module @statistic[packetDeaggregated](title="packets deaggregated"; record=count); @statistic[packetDropDuplicateDetected](title="packet drops: duplicate detected"; source=packetDropReasonIsDuplicateDetected(packetDropped); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); } - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 00dbfe970e0..d7efe6322bd 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -24,10 +24,38 @@ Define_Module(RecipientQosMacDataService); void RecipientQosMacDataService::initialize() { duplicateRemoval = new QoSDuplicateRemoval(); - basicReassembly = new BasicReassembly(); + basicReassembly = new BasicReassembly(par("maxReceiveLifetime")); aMsduDeaggregation = new MsduDeaggregation(); aMpduDeaggregation = new MpduDeaggregation(); blockAckReordering = new BlockAckReordering(); + reassemblyTimer = new cMessage("reassemblyTimer"); +} + +void RecipientQosMacDataService::handleMessage(cMessage *message) +{ + if (message != reassemblyTimer) + throw cRuntimeError("Unknown message"); + expireReassemblyFragments(); + scheduleReassemblyTimer(); +} + +void RecipientQosMacDataService::expireReassemblyFragments() +{ + for (auto packet : basicReassembly->removeExpiredFragments(simTime())) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } +} + +void RecipientQosMacDataService::scheduleReassemblyTimer() +{ + if (reassemblyTimer->isScheduled()) + cancelEvent(reassemblyTimer); + auto nextExpirationTime = basicReassembly->getNextExpirationTime(); + if (nextExpirationTime != SIMTIME_MAX) + scheduleAt(nextExpirationTime, reassemblyTimer); } void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress originatorAddr) @@ -47,19 +75,26 @@ void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress ori Packet *RecipientQosMacDataService::defragment(std::vector completeFragments) { + expireReassemblyFragments(); + Packet *defragmentedPacket = nullptr; for (auto fragment : completeFragments) { auto packet = basicReassembly->addFragment(fragment); if (packet != nullptr) { - emit(packetDefragmentedSignal, packet); - return packet; + defragmentedPacket = packet; + break; } } - return nullptr; + scheduleReassemblyTimer(); + if (defragmentedPacket != nullptr) + emit(packetDefragmentedSignal, defragmentedPacket); + return defragmentedPacket; } Packet *RecipientQosMacDataService::defragment(Packet *mgmtFragment) { + expireReassemblyFragments(); auto packet = basicReassembly->addFragment(mgmtFragment); + scheduleReassemblyTimer(); if (packet && packet->hasAtFront()) { emit(packetDefragmentedSignal, packet); return packet; @@ -134,18 +169,27 @@ IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataS // TODO MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) { delete mgmtPacket; - return { {}, true }; + // A duplicate fragment is acknowledged by HCF but is not a complete + // MMPDU. Preserve the existing subtype-specific handling only for an + // unfragmented duplicate management frame. + if (mgmtHeader->getFragmentNumber() == 0 && !mgmtHeader->getMoreFragments()) + return { {}, mgmtHeader, true }; + else + return { {}, nullptr, true }; } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); } + if (mgmtPacket == nullptr) + return {}; + const auto& completeHeader = mgmtPacket->peekAtFront(); // TODO Defrag, MSDU Integrity, Replay Detection, RX MSDU Rate Limiting - if (dynamicPtrCast(mgmtHeader)) { + if (dynamicPtrCast(completeHeader)) { delete mgmtPacket; - return { {}, false }; + return { {}, completeHeader, false }; } else - return { { mgmtPacket }, false }; + return { { mgmtPacket }, completeHeader, false }; } std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) @@ -202,6 +246,7 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c RecipientQosMacDataService::~RecipientQosMacDataService() { + cancelAndDelete(reassemblyTimer); delete duplicateRemoval; delete basicReassembly; delete aMsduDeaggregation; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index cb27ff09543..76b27d8cbc7 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -27,6 +27,7 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, { protected: IReassembly *basicReassembly = nullptr; + cMessage *reassemblyTimer = nullptr; IMpduDeaggregation *aMpduDeaggregation = nullptr; // MpduHeaderAndFcsValidation *mpduHeaderAndFcsValidation = nullptr; @@ -41,7 +42,10 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, protected: virtual ~RecipientQosMacDataService(); virtual void initialize() override; + virtual void handleMessage(cMessage *message) override; + virtual void expireReassemblyFragments(); + virtual void scheduleReassemblyTimer(); virtual Packet *defragment(std::vector completeFragments); virtual Packet *defragment(Packet *mgmtFragment); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned index 307fb523210..d02d7e7ff00 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned @@ -22,6 +22,7 @@ module RecipientQosMacDataService extends Module { parameters: @class(RecipientQosMacDataService); + double maxReceiveLifetime @unit(s) = default(524288us); // IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs @display("i=block/join"); @signal[packetDefragmented](type=inet::Packet); @signal[packetDeaggregated](type=inet::Packet); @@ -30,4 +31,3 @@ module RecipientQosMacDataService extends Module @statistic[packetDeaggregated](title="packets deaggregated"; record=count); @statistic[packetDropDuplicateDetected](title="packet drops: duplicate detected"; source=packetDropReasonIsDuplicateDetected(packetDropped); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); } - diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 9e3a5a153a3..72e7714ddf5 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -25,13 +25,17 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" @@ -84,6 +88,15 @@ class TestRecipientPolicy : public IRecipientBlockAckAgreementPolicy virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } }; +class TestRecipientAckPolicy : public IRecipientAckPolicy, public IRecipientQosAckPolicy +{ + public: + virtual bool isAckNeeded(const Ptr&) const override { return true; } + virtual bool isBlockAckNeeded(const Ptr&, RecipientBlockAckAgreement *) const override { return false; } + virtual simtime_t computeAckDurationField(Packet *, const Ptr&) const override { return 0; } + virtual simtime_t computeBasicBlockAckDurationField(Packet *, const Ptr&) const override { return 0; } +}; + class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandlerCallback { public: @@ -568,6 +581,7 @@ class TestHcf : public Hcf int numResumedEligibleChannelAccess = 0; int numCancelledAddbaTransactions = 0; int numRebuildEligibilityCalls = 0; + int numTransmittedControlResponses = 0; bool droppedSetupCancelled = false; bool delegateDroppedSetupHandling = false; TestCallback *managementCallback = nullptr; @@ -593,6 +607,10 @@ class TestHcf : public Hcf } void configureFrameSequenceHandler(IFrameSequenceHandler *frameSequenceHandler) { this->frameSequenceHandler = frameSequenceHandler; } void configureRecipientDataService(IRecipientQosMacDataService *recipientDataService) { this->recipientDataService = recipientDataService; } + void configureRecipientAck(IRecipientQosAckPolicy *recipientAckPolicy) { + this->recipientAckPolicy = recipientAckPolicy; + recipientAckProcedure = new RecipientAckProcedure(); + } void configureManagementCallback(TestCallback *managementCallback) { this->managementCallback = managementCallback; } void configureTransmittedData(Edca *edca, IOriginatorBlockAckAgreementHandler *handler, IOriginatorBlockAckAgreementPolicy *policy) { this->edca = edca; @@ -616,6 +634,7 @@ class TestHcf : public Hcf void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } + void processReceivedFrame(Packet *packet) { recipientProcessReceivedFrame(packet, packet->peekAtFront()); } void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } void processFailedFrame(Packet *packet) { originatorProcessFailedFrame(packet); } void processReceivedAck(Packet *packet, Packet *lastTransmittedPacket, AccessCategory accessCategory) { @@ -625,6 +644,10 @@ class TestHcf : public Hcf } protected: + virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr&, Packet *, const Ptr&) override { + numTransmittedControlResponses++; + delete responsePacket; + } virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { if (managementCallback != nullptr) managementCallback->processMgmtFrame(packet, header); @@ -667,7 +690,7 @@ class TestEmptyFrameSequence : public IFrameSequence class TestFragmentationPolicy : public IFragmentationPolicy { public: - virtual std::vector computeFragmentSizes(Packet *) override { return { 2, 2 }; } + virtual std::vector computeFragmentSizes(Packet *) override { return { 8, 5 }; } }; class TestMsduAggregationPolicy : public IMsduAggregationPolicy @@ -995,7 +1018,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); TestOriginatorQosMacDataService dataService; auto originatorHandler = new TestOriginatorHandler(); dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { - auto delba = dynamicPtrCast(packet->peekAtFront()); + auto delba = findFragmentedActionContext(packet); return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); }); TestInProgressFrames inProgressFrames; @@ -1043,7 +1066,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); TestOriginatorQosMacDataService dataService; auto originatorHandler = new TestOriginatorHandler(); dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { - auto delba = dynamicPtrCast(packet->peekAtFront()); + auto delba = findFragmentedActionContext(packet); return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); }); TestInProgressFrames inProgressFrames; @@ -1063,19 +1086,33 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); hcf.configureTransmittedData(&edca, originatorHandler, &policy); hcf.configureTransmittedManagement(&edca, &ackPolicy); - auto sibling = makeTaggedDelbaPacket("acknowledgedDelbaSibling", peer1, 6, SequenceNumberCyclic(204), 0, true, transactionId); - auto finalPacket = makeTaggedDelbaPacket("acknowledgedFinalDelba", peer1, 6, SequenceNumberCyclic(204), 1, false, transactionId); - auto siblingHeader = sibling->peekAtFront(); - auto finalHeader = finalPacket->peekAtFront(); + auto completeDelba = makeShared(); + completeDelba->setReceiverAddress(peer1); + completeDelba->setTid(6); + completeDelba->setInitiator(true); + completeDelba->setSequenceNumber(SequenceNumberCyclic(204)); + auto completeDelbaPacket = new Packet("acknowledgedDelba", completeDelba); + completeDelbaPacket->addTag()->setTransactionId(transactionId); + completeDelbaPacket->insertAtBack(makeShared()); + Fragmentation fragmentation; + auto delbaFragments = fragmentation.fragmentFrame(completeDelbaPacket, { 4, 2 }); + auto sibling = delbaFragments->at(0); + auto finalPacket = delbaFragments->at(1); + auto siblingHeader = sibling->peekAtFront(); + auto finalHeader = finalPacket->peekAtFront(); + ASSERT(dynamicPtrCast(siblingHeader) == nullptr); + ASSERT(dynamicPtrCast(finalHeader) == nullptr); originatorHandler->setPendingTeardownTransaction(peer1, 6, transactionId); inProgressFrames.addOwnedFrame(sibling); inProgressFrames.addOwnedFrame(finalPacket); ackHandler.frameGotInProgress(siblingHeader); ackHandler.frameGotInProgress(finalHeader); + hcf.processTransmittedManagement(sibling, siblingHeader, AC_BE); + ASSERT(originatorHandler->isDelbaPending(sibling, findFragmentedActionContext(sibling))); hcf.processTransmittedManagement(finalPacket, finalHeader, AC_BE); Packet ackPacket("transactionalDelbaAck", makeShared()); hcf.processReceivedAck(&ackPacket, finalPacket, AC_BE); - ASSERT(!originatorHandler->isDelbaPending(finalPacket, finalHeader)); + ASSERT(!originatorHandler->isDelbaPending(finalPacket, findFragmentedActionContext(finalPacket))); ASSERT(hcf.numCancelledAddbaTransactions == 1); ASSERT(inProgressFrames.getLength() == 0); ASSERT(inProgressFrames.getNumDroppedFrames() == 1); @@ -1086,6 +1123,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); auto releasedFrames = inProgressFrames.releaseFrames(); ASSERT(releasedFrames == std::vector({ finalPacket })); delete finalPacket; + delete delbaFragments; } // Reaching the retry limit aborts the teardown through HCF, clears its sibling @@ -1100,7 +1138,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); TestOriginatorQosMacDataService dataService; auto originatorHandler = new TestOriginatorHandler(); dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { - auto delba = dynamicPtrCast(packet->peekAtFront()); + auto delba = findFragmentedActionContext(packet); return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); }); TestInProgressFrames inProgressFrames; @@ -1482,8 +1520,17 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto transmittedDelba = makeShared(); transmittedDelba->setReceiverAddress(peer2); transmittedDelba->setTid(2); + transmittedDelba->setInitiator(true); + transmittedDelba->setMoreFragments(true); + auto transmittedFinalDelba = staticPtrCast(transmittedDelba->dupShared()); + transmittedFinalDelba->setMoreFragments(false); + transmittedFinalDelba->setFragmentNumber(1); Packet transmittedDelbaPacket("transmittedDelba", transmittedDelba); - auto terminatedPendingAgreement = transmittedDelbaHandler.processTransmittedDelba(&transmittedDelbaPacket, &transmittedDelbaCallback); + ASSERT(transmittedDelbaHandler.processTransmittedDelba(&transmittedDelbaPacket, &transmittedDelbaCallback) == nullptr); + ASSERT(transmittedDelbaHandler.getAgreement(peer2, 2) != nullptr); + ASSERT(transmittedDelbaCallback.cancelledTransactionIds.empty()); + Packet transmittedFinalDelbaPacket("transmittedFinalDelba", transmittedFinalDelba); + auto terminatedPendingAgreement = transmittedDelbaHandler.processTransmittedDelba(&transmittedFinalDelbaPacket, &transmittedDelbaCallback); ASSERT(terminatedPendingAgreement != nullptr); ASSERT(terminatedPendingAgreement->isPending()); if (transmittedDelbaHandler.getAgreement(peer2, 2) != nullptr || transmittedDelbaCallback.cancelledTransactionIds.size() != 1 || transmittedDelbaCallback.cancelledTransactionIds.back() != transmittedDelbaTransactionId) @@ -1494,18 +1541,21 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(transmittedDelbaCallback.addbaDeadline == SIMTIME_MAX); } -// Fragmentation preserves the local transaction identity on every MPDU, and -// typed cancellation removes all exact siblings while retaining the dropped -// fragment and an unrelated transaction. +// Fragmentation preserves the local transaction identity on every MPDU. +// Transaction-scoped cancellation removes all exact siblings while retaining +// the dropped fragment and an unrelated transaction. { - TestOriginatorHandler fragmentedHandler; + auto fragmentedHandler = new TestOriginatorHandler(); TestCallback fragmentedCallback; Packet fragmentedTrigger("fragmentedTrigger"); auto fragmentedTriggerHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(50)); - fragmentedHandler.processAcknowledgedDataFrame(&fragmentedTrigger, fragmentedTriggerHeader, &originatorPolicy, &fragmentedCallback); + fragmentedHandler->processAcknowledgedDataFrame(&fragmentedTrigger, fragmentedTriggerHeader, &originatorPolicy, &fragmentedCallback); auto unfragmentedRequestPacket = fragmentedCallback.managementPacket; auto fragmentedTransactionId = unfragmentedRequestPacket->getTag()->getTransactionId(); fragmentedCallback.forgetManagementPacket(unfragmentedRequestPacket); + auto unfragmentedRequestHeader = unfragmentedRequestPacket->removeAtFront(); + unfragmentedRequestHeader->setSequenceNumber(SequenceNumberCyclic(50)); + unfragmentedRequestPacket->insertAtFront(unfragmentedRequestHeader); unfragmentedRequestPacket->insertAtBack(makeShared(B(4))); unfragmentedRequestPacket->insertAtBack(makeShared()); TestFragmentationPolicy fragmentationPolicy; @@ -1514,8 +1564,11 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto fragments = fragmentationDataService.fragment(unfragmentedRequestPacket); ASSERT(fragments != nullptr); ASSERT(fragments->size() == 2); - for (auto fragment : *fragments) + for (auto fragment : *fragments) { ASSERT(fragment->getTag()->getTransactionId() == fragmentedTransactionId); + ASSERT(dynamicPtrCast(fragment->peekAtFront()) == nullptr); + ASSERT(findFragmentedActionContext(fragment) != nullptr); + } auto unrelatedPacket = new Packet("unrelatedTransaction", makeShared(B(1))); unrelatedPacket->addTag()->setTransactionId(fragmentedTransactionId + 1); auto droppedFragment = fragments->at(0); @@ -1524,15 +1577,30 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); fragmentedPendingQueue.packets = { droppedFragment, siblingFragment, unrelatedPacket }; TestInProgressFrames fragmentedInProgressFrames; TestEdcaf fragmentedEdcaf; + TestQosAckHandler fragmentedAckHandler; fragmentedEdcaf.pendingQueue = &fragmentedPendingQueue; fragmentedEdcaf.inProgressFrames = &fragmentedInProgressFrames; + fragmentedEdcaf.qosAckHandler = &fragmentedAckHandler; TestEdca fragmentedEdca; fragmentedEdca.edcaf = &fragmentedEdcaf; TestHcf fragmentedHcf; fragmentedHcf.configure(&fragmentedEdca); - auto droppedRequest = droppedFragment->peekAtFront(); - fragmentedHandler.processDroppedAddbaReq(droppedFragment, droppedRequest, &originatorPolicy, &fragmentedHcf); - ASSERT(fragmentedHandler.getAgreement(peer1, 7) == nullptr); + OriginatorQosAckPolicy fragmentedAckPolicy; + TestRecipientPolicy fragmentedRecipientPolicy; + fragmentedHcf.configureBlockAckHandlers(fragmentedHandler, &originatorPolicy, new TestRecipientHandler(), &fragmentedRecipientPolicy); + fragmentedHcf.configureTransmittedManagement(&fragmentedEdca, &fragmentedAckPolicy); + auto fragmentedAgreement = fragmentedHandler->getAgreement(peer1, 7); + ASSERT(fragmentedAgreement != nullptr); + ASSERT(!fragmentedAgreement->getIsAddbaRequestSent()); + auto firstFragmentHeader = droppedFragment->peekAtFront(); + auto finalFragmentHeader = siblingFragment->peekAtFront(); + fragmentedHcf.processTransmittedManagement(droppedFragment, firstFragmentHeader, AC_BE); + ASSERT(!fragmentedAgreement->getIsAddbaRequestSent()); + fragmentedHcf.processTransmittedManagement(siblingFragment, finalFragmentHeader, AC_BE); + ASSERT(fragmentedAgreement->getIsAddbaRequestSent()); + auto droppedRequest = findFragmentedActionContext(droppedFragment); + fragmentedHandler->processDroppedAddbaReq(droppedFragment, droppedRequest, &originatorPolicy, &fragmentedHcf); + ASSERT(fragmentedHandler->getAgreement(peer1, 7) == nullptr); ASSERT(fragmentedPendingQueue.getNumPackets() == 2); ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), droppedFragment) != fragmentedPendingQueue.packets.end()); ASSERT(std::find(fragmentedPendingQueue.packets.begin(), fragmentedPendingQueue.packets.end(), siblingFragment) == fragmentedPendingQueue.packets.end()); @@ -1583,7 +1651,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); rejectedRequest->setSequenceNumber(SequenceNumberCyclic(40)); auto rejectedReception = recipientDataService->managementFrameReceived(new Packet("initiallyRejectedAddbaRequest", rejectedRequest), rejectedRequest); ASSERT(!rejectedReception.duplicate); - hcf.processReceivedManagementFrame(rejectedRequest, rejectedReception.duplicate); + ASSERT(rejectedReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(rejectedReception.completeHeader, rejectedReception.duplicate); ASSERT(recipientHandler->getAgreement(7, peer3) == nullptr); ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); ASSERT(responseCallback.managementPackets.size() == 1); @@ -1594,7 +1663,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); duplicateRejectedRequest->setRetry(true); auto duplicateReception = recipientDataService->managementFrameReceived(new Packet("duplicateInitiallyRejectedAddbaRequest", duplicateRejectedRequest), duplicateRejectedRequest); ASSERT(duplicateReception.duplicate); - hcf.processReceivedManagementFrame(duplicateRejectedRequest, duplicateReception.duplicate); + ASSERT(duplicateReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateReception.completeHeader, duplicateReception.duplicate); ASSERT(recipientHandler->getNumCachedAddbaResponses() == 0); ASSERT(responseCallback.managementPackets.size() == 1); @@ -1633,7 +1703,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto firstReception = recipientDataService->managementFrameReceived(new Packet("firstAddbaRequest", firstRequest), firstRequest); ASSERT(!firstReception.duplicate); ASSERT(firstReception.completeFrames.empty()); - hcf.processReceivedManagementFrame(firstRequest, firstReception.duplicate); + ASSERT(firstReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(firstReception.completeHeader, firstReception.duplicate); auto firstAgreement = recipientHandler->getAgreement(6, peer2); ASSERT(firstAgreement != nullptr); ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); @@ -1670,7 +1741,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto duplicateReception = recipientDataService->managementFrameReceived(new Packet("duplicateAddbaRequest", duplicateRequest), duplicateRequest); ASSERT(duplicateReception.duplicate); ASSERT(duplicateReception.completeFrames.empty()); - hcf.processReceivedManagementFrame(duplicateRequest, duplicateReception.duplicate); + ASSERT(duplicateReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateReception.completeHeader, duplicateReception.duplicate); ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); ASSERT(firstAgreement->getBlockAckRecord() == firstBlockAckRecord); ASSERT(firstAgreement->getExpirationTime() == firstExpirationTime); @@ -1695,7 +1767,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); rejectedRequest->setBlockAckTimeoutValue(4); auto rejectedReception = recipientDataService->managementFrameReceived(new Packet("rejectedAddbaRequest", rejectedRequest), rejectedRequest); ASSERT(!rejectedReception.duplicate); - hcf.processReceivedManagementFrame(rejectedRequest, rejectedReception.duplicate); + ASSERT(rejectedReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(rejectedReception.completeHeader, rejectedReception.duplicate); ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); ASSERT(changedListener.numSignals == 0); ASSERT(reorderDropListener.numSignals == 0); @@ -1715,7 +1788,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); duplicateRejectedRequest->setBlockAckTimeoutValue(4); auto duplicateRejectedReception = recipientDataService->managementFrameReceived(new Packet("duplicateRejectedAddbaRequest", duplicateRejectedRequest), duplicateRejectedRequest); ASSERT(duplicateRejectedReception.duplicate); - hcf.processReceivedManagementFrame(duplicateRejectedRequest, duplicateRejectedReception.duplicate); + ASSERT(duplicateRejectedReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(duplicateRejectedReception.completeHeader, duplicateRejectedReception.duplicate); ASSERT(recipientHandler->getAgreement(6, peer2) == firstAgreement); ASSERT(recipientHandler->getNumCachedAddbaResponses() == 1); ASSERT(changedListener.numSignals == 0); @@ -1766,7 +1840,8 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto replacementReception = recipientDataService->managementFrameReceived(new Packet("replacementAddbaRequest", replacementRequest), replacementRequest); ASSERT(!replacementReception.duplicate); ASSERT(replacementReception.completeFrames.empty()); - hcf.processReceivedManagementFrame(replacementRequest, replacementReception.duplicate); + ASSERT(replacementReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(replacementReception.completeHeader, replacementReception.duplicate); ASSERT(recipientHandler->getAgreement(6, peer2) != firstAgreement); ASSERT(addedListener.numSignals == 1); ASSERT(changedListener.numSignals == 1); @@ -1802,6 +1877,278 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); recipientDataServiceModule->deleteModule(); } +// IEEE Std 802.11-2024, 10.4-10.5: each management MPDU fragment is +// acknowledged independently, but its action body is dispatched only after +// the final fragment and every preceding fragment have been received. +{ + auto fragmentationPolicyModule = cModuleType::get("inet.linklayer.ieee80211.mac.fragmentation.BasicFragmentationPolicy")->create("addbaFragmentationPolicy", this); + fragmentationPolicyModule->par("fragmentationThreshold").setIntValue(34); + fragmentationPolicyModule->callInitialize(); + auto fragmentationPolicy = check_and_cast(fragmentationPolicyModule); + auto policyRequest = makeRequest(peer2, 2, 72, SequenceNumberCyclic(102), 32); + auto policyPacket = new Packet("policyAddbaRequest", policyRequest); + policyPacket->insertAtBack(makeShared()); + ASSERT(fragmentationPolicy->computeFragmentSizes(policyPacket) == std::vector({ 6, 3 })); + delete policyPacket; + auto groupAddressedRequest = makeRequest(peer2, 2, 72, SequenceNumberCyclic(102), 32); + groupAddressedRequest->setReceiverAddress(MacAddress::BROADCAST_ADDRESS); + auto groupAddressedPacket = new Packet("groupAddressedAddbaRequest", groupAddressedRequest); + groupAddressedPacket->insertAtBack(makeShared()); + ASSERT(fragmentationPolicy->computeFragmentSizes(groupAddressedPacket).empty()); + delete groupAddressedPacket; + fragmentationPolicyModule->callFinish(); + fragmentationPolicyModule->deleteModule(); + + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("fragmentedAddbaDataService", this); + recipientDataServiceModule->callInitialize(); + auto recipientDataService = check_and_cast(recipientDataServiceModule); + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestRecipientPolicy fragmentedPolicy; + TestCallback fragmentedCallback; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &fragmentedPolicy); + hcf.configureRecipientDataService(recipientDataService); + hcf.configureManagementCallback(&fragmentedCallback); + + auto makeFragments = [&](const char *name, Tid tid, int sequenceNumber) { + auto request = makeRequest(peer2, tid, 70 + tid, SequenceNumberCyclic(100 + tid), 32); + request->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + auto packet = new Packet(name, request); + packet->insertAtBack(makeShared(B(4))); + packet->insertAtBack(makeShared()); + Fragmentation fragmentation; + return fragmentation.fragmentFrame(packet, { 8, 5 }); + }; + auto receiveFragment = [&](Packet *fragment) { + auto header = fragment->peekAtFront(); + return recipientDataService->managementFrameReceived(fragment, header); + }; + auto convertToWireRepresentation = [](Packet *fragment) { + std::string name = fragment->getName(); + const auto& trailer = fragment->popAtBack(B(4)); + auto wireFragment = new Packet(name.c_str(), fragment->peekDataAsBytes()); + wireFragment->copyTags(*fragment); + wireFragment->insertAtBack(trailer); + delete fragment; + return wireFragment; + }; + + auto fragments = makeFragments("fragmentedAddbaRequest", 3, 60); + for (auto& fragment : *fragments) + fragment = convertToWireRepresentation(fragment); + auto commonManagementHeaderLength = makeShared()->getChunkLength(); + ASSERT(fragments->at(0)->getByteLength() == commonManagementHeaderLength.get() + 8 + 4); + ASSERT(fragments->at(1)->getByteLength() == commonManagementHeaderLength.get() + 5 + 4); + ASSERT(dynamicPtrCast(fragments->at(0)->peekAtFront()) == nullptr); + ASSERT(dynamicPtrCast(fragments->at(1)->peekAtFront()) == nullptr); + auto duplicateFirstFragment = fragments->at(0)->dup(); + auto duplicateHeader = duplicateFirstFragment->removeAtFront(); + duplicateHeader->setRetry(true); + duplicateFirstFragment->insertAtFront(duplicateHeader); + + auto firstReception = receiveFragment(fragments->at(0)); + ASSERT(firstReception.completeHeader == nullptr); + ASSERT(recipientHandler->getAgreement(3, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.empty()); + + auto duplicateReception = receiveFragment(duplicateFirstFragment); + ASSERT(duplicateReception.duplicate); + ASSERT(duplicateReception.completeHeader == nullptr); + ASSERT(recipientHandler->getAgreement(3, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.empty()); + + auto completeReception = receiveFragment(fragments->at(1)); + ASSERT(!completeReception.duplicate); + ASSERT(completeReception.completeHeader != nullptr); + ASSERT(completeReception.completeHeader->getFragmentNumber() == 0); + ASSERT(!completeReception.completeHeader->getMoreFragments()); + auto completeRequest = dynamicPtrCast(completeReception.completeHeader); + ASSERT(completeRequest != nullptr); + ASSERT(completeRequest->getTid() == 3); + ASSERT(completeRequest->getDialogToken() == 73); + ASSERT(completeRequest->getStartingSequenceNumber() == SequenceNumberCyclic(103)); + hcf.processReceivedManagementFrame(completeReception.completeHeader); + ASSERT(recipientHandler->getAgreement(3, peer2) != nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == 1); + delete fragments; + + // Seeing the final fragment first does not imply completeness. Dispatch + // occurs only when the missing lower-numbered fragment later arrives. + auto outOfOrderFragments = makeFragments("outOfOrderAddbaRequest", 4, 61); + for (auto& fragment : *outOfOrderFragments) + fragment = convertToWireRepresentation(fragment); + auto finalFirstReception = receiveFragment(outOfOrderFragments->at(1)); + ASSERT(finalFirstReception.completeHeader == nullptr); + ASSERT(recipientHandler->getAgreement(4, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == 1); + auto outOfOrderCompleteReception = receiveFragment(outOfOrderFragments->at(0)); + ASSERT(outOfOrderCompleteReception.completeHeader != nullptr); + hcf.processReceivedManagementFrame(outOfOrderCompleteReception.completeHeader); + ASSERT(recipientHandler->getAgreement(4, peer2) != nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == 2); + delete outOfOrderFragments; + + // The receive timer releases an incomplete MMPDU at + // dot11MaxReceiveLifetime even if no matching fragment arrives. + { + auto expiringDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("expiringFragmentDataService", this); + expiringDataServiceModule->par("maxReceiveLifetime").setDoubleValue(1e-6); + expiringDataServiceModule->callInitialize(); + auto expiringDataService = check_and_cast(expiringDataServiceModule); + TestPacketDropSignalListener expiryDropListener; + expiringDataService->subscribe(packetDroppedSignal, &expiryDropListener); + auto expiredFragments = makeFragments("expiredAddbaRequest", 1, 66); + auto firstExpiredReception = expiringDataService->managementFrameReceived(expiredFragments->at(0), expiredFragments->at(0)->peekAtFront()); + ASSERT(firstExpiredReception.completeHeader == nullptr); + ASSERT(expiryDropListener.numOtherPacketDrops == 0); + wait(2e-6); + ASSERT(expiryDropListener.numOtherPacketDrops == 1); + auto lateFinalReception = expiringDataService->managementFrameReceived(expiredFragments->at(1), expiredFragments->at(1)->peekAtFront()); + ASSERT(lateFinalReception.completeHeader == nullptr); + // The late final fragment must be discarded, not buffered as the + // beginning of another reassembly. A subsequent non-Retry fragment 0 + // starts a new sequence reuse and therefore cannot combine with it. + auto restartedFragments = makeFragments("restartedAddbaRequest", 1, 66); + auto restartedReception = expiringDataService->managementFrameReceived(restartedFragments->at(0), restartedFragments->at(0)->peekAtFront()); + ASSERT(restartedReception.completeHeader == nullptr); + delete restartedFragments->at(1); + delete restartedFragments; + expiringDataServiceModule->callFinish(); + expiringDataServiceModule->deleteModule(); + delete expiredFragments; + } + + // Frame type and destination address are part of the reassembly identity; + // fragments sharing only TA and sequence number cannot cross-associate. + { + BasicReassembly isolatedReassembly; + auto firstMmpdu = makeFragments("isolatedAddbaRequest", 0, 67); + auto otherDestinationMmpdu = makeFragments("otherDestinationAddbaRequest", 0, 67); + auto setReceiverAddress = [](Packet *fragment, const MacAddress& receiverAddress) { + auto header = fragment->removeAtFront(); + header->setReceiverAddress(receiverAddress); + fragment->insertAtFront(header); + }; + for (auto fragment : *firstMmpdu) + setReceiverAddress(fragment, peer1); + for (auto fragment : *otherDestinationMmpdu) + setReceiverAddress(fragment, peer3); + auto dataHeader = makeShared(); + dataHeader->setType(ST_DATA); + dataHeader->setTransmitterAddress(peer2); + dataHeader->setReceiverAddress(peer1); + dataHeader->setSequenceNumber(SequenceNumberCyclic(67)); + dataHeader->setFragmentNumber(1); + dataHeader->setMoreFragments(false); + auto dataFragment = new Packet("sameSequenceDataFragment", dataHeader); + dataFragment->insertAtBack(makeShared(B(5))); + dataFragment->insertAtBack(makeShared()); + ASSERT(isolatedReassembly.addFragment(firstMmpdu->at(0)) == nullptr); + ASSERT(isolatedReassembly.addFragment(otherDestinationMmpdu->at(1)) == nullptr); + ASSERT(isolatedReassembly.addFragment(dataFragment) == nullptr); + auto completeMmpdu = isolatedReassembly.addFragment(firstMmpdu->at(1)); + ASSERT(completeMmpdu != nullptr); + delete completeMmpdu; + delete otherDestinationMmpdu->at(0); + delete firstMmpdu; + delete otherDestinationMmpdu; + } + + // Drive the real HCF recipient entry point: every MPDU, including an + // ignored duplicate, is ACKed before the data service gates action-body + // dispatch on complete reassembly. + TestRecipientAckPolicy recipientAckPolicy; + hcf.configureRecipientAck(&recipientAckPolicy); + auto hcfFragments = makeFragments("hcfFragmentedAddbaRequest", 7, 65); + for (auto& fragment : *hcfFragments) + fragment = convertToWireRepresentation(fragment); + auto hcfDuplicateFirstFragment = hcfFragments->at(0)->dup(); + auto hcfDuplicateHeader = hcfDuplicateFirstFragment->removeAtFront(); + hcfDuplicateHeader->setRetry(true); + hcfDuplicateFirstFragment->insertAtFront(hcfDuplicateHeader); + auto numManagementPacketsBeforeHcfReception = fragmentedCallback.managementPackets.size(); + hcf.processReceivedFrame(hcfFragments->at(0)); + ASSERT(hcf.numTransmittedControlResponses == 1); + ASSERT(recipientHandler->getAgreement(7, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == numManagementPacketsBeforeHcfReception); + hcf.processReceivedFrame(hcfDuplicateFirstFragment); + ASSERT(hcf.numTransmittedControlResponses == 2); + ASSERT(recipientHandler->getAgreement(7, peer2) == nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == numManagementPacketsBeforeHcfReception); + hcf.processReceivedFrame(hcfFragments->at(1)); + ASSERT(hcf.numTransmittedControlResponses == 3); + ASSERT(recipientHandler->getAgreement(7, peer2) != nullptr); + ASSERT(fragmentedCallback.managementPackets.size() == numManagementPacketsBeforeHcfReception + 1); + delete hcfFragments; + + auto verifyFragmentedActionRoundTrip = [&](const char *name, const Ptr& action, const std::function&)>& verify) { + auto bodyLength = (int)(action->getChunkLength() - commonManagementHeaderLength).get(); + auto firstFragmentLength = (bodyLength - 1) & ~1; + ASSERT(firstFragmentLength > 0 && firstFragmentLength < bodyLength); + auto packet = new Packet(name, action); + packet->insertAtBack(makeShared()); + Fragmentation fragmentation; + auto actionFragments = fragmentation.fragmentFrame(packet, { firstFragmentLength, bodyLength - firstFragmentLength }); + for (auto& fragment : *actionFragments) { + ASSERT(dynamicPtrCast(fragment->peekAtFront()) == nullptr); + ASSERT(fragment->getTag()->getActionFrame() != nullptr); + fragment = convertToWireRepresentation(fragment); + } + auto incompleteReception = receiveFragment(actionFragments->at(0)); + ASSERT(incompleteReception.completeHeader == nullptr); + auto completedReception = receiveFragment(actionFragments->at(1)); + ASSERT(completedReception.completeHeader != nullptr); + auto completedAction = dynamicPtrCast(completedReception.completeHeader); + ASSERT(completedAction != nullptr); + ASSERT(completedAction->getReceiverAddress() == action->getReceiverAddress()); + ASSERT(completedAction->getTransmitterAddress() == action->getTransmitterAddress()); + ASSERT(completedAction->getAddress3() == action->getAddress3()); + ASSERT(completedAction->getSequenceNumber() == action->getSequenceNumber()); + verify(completedAction); + delete actionFragments; + return completedAction; + }; + + auto fragmentedResponse = makeResponse(peer2, 5, 81, 0); + fragmentedResponse->setSequenceNumber(SequenceNumberCyclic(62)); + verifyFragmentedActionRoundTrip("fragmentedAddbaResponse", fragmentedResponse, [](const Ptr& action) { + auto response = dynamicPtrCast(action); + ASSERT(response != nullptr); + ASSERT(response->getTid() == 5); + ASSERT(response->getDialogToken() == 81); + ASSERT(response->getStatusCode() == 0); + }); + + for (bool initiator : { false, true }) { + if (initiator) { + auto delbaAgreementRequest = makeRequest(peer2, 6, 86, SequenceNumberCyclic(106), 32); + ASSERT(recipientHandler->processReceivedAddbaRequest(delbaAgreementRequest, &fragmentedPolicy, &fragmentedCallback, &fragmentedCallback) != nullptr); + ASSERT(recipientHandler->getAgreement(6, peer2) != nullptr); + } + auto fragmentedDelba = makeShared(); + fragmentedDelba->setTransmitterAddress(peer2); + fragmentedDelba->setSequenceNumber(SequenceNumberCyclic(initiator ? 64 : 63)); + fragmentedDelba->setInitiator(initiator); + fragmentedDelba->setTid(6); + fragmentedDelba->setReasonCode(39); + auto completeDelba = verifyFragmentedActionRoundTrip(initiator ? "fragmentedOriginatorDelba" : "fragmentedRecipientDelba", fragmentedDelba, [initiator](const Ptr& action) { + auto delba = dynamicPtrCast(action); + ASSERT(delba != nullptr); + ASSERT(delba->getInitiator() == initiator); + ASSERT(delba->getTid() == 6); + ASSERT(delba->getReasonCode() == 39); + }); + if (initiator) { + hcf.processReceivedManagementFrame(completeDelba); + ASSERT(recipientHandler->getAgreement(6, peer2) == nullptr); + } + } + + recipientDataServiceModule->callFinish(); + recipientDataServiceModule->deleteModule(); +} + // IEEE Std 802.11-2024, 9.6.4.2, 10.25.2, and 11.5.2.3. RecipientBlockAckAgreementHandler immediateRecipientHandler; TestRecipientPolicy recipientPolicy; @@ -2010,8 +2357,17 @@ ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == replacementImmediateA delba->setTid(3); delba->setInitiator(false); delba->setSequenceNumber(SequenceNumberCyclic(200)); + delba->setMoreFragments(true); + auto finalDelba = staticPtrCast(delba->dupShared()); + finalDelba->setMoreFragments(false); + finalDelba->setFragmentNumber(1); Packet delbaPacket("transmittedRecipientDelba", delba); hcf.processTransmittedManagement(&delbaPacket, delba, AC_BE); + ASSERT(recipientHandler->getAgreement(3, peer2) != nullptr); + ASSERT(deletionListener.numSignals == 0); + ASSERT(reorderDropListener.numOtherPacketDrops == 0); + Packet finalDelbaPacket("transmittedRecipientDelbaFinal", finalDelba); + hcf.processTransmittedManagement(&finalDelbaPacket, finalDelba, AC_BE); ASSERT(recipientHandler->getAgreement(3, peer2) == nullptr); ASSERT(deletionListener.numSignals == 1); ASSERT(reorderDropListener.numOtherPacketDrops == 1); From c78058a6ed98cf3b3c36187d141fa97d03418f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:03 +0200 Subject: [PATCH 32/56] ieee80211: cancel superseded management transactions Add an end-to-end management transaction cancellation contract from the AP through Ieee80211Mac to DCF or HCF. A terminal failure removes queued and in-progress sibling fragments across access categories and retires retry, delayed-IFS, and pending-transmission state exactly once. Keep a frame borrowed by the active frame sequence alive until a safe sequence boundary, then abort or release it through the owning component. Clear AP association state before cancellation so reentrant callbacks cannot observe or revive the superseded response. Cover DCF and HCF supersession, queue removal and overflow, delayed IFS, RTS protection, active frame sequences, and successful replacement. --- .../linklayer/ieee80211/mac/Ieee80211Mac.cc | 9 + .../linklayer/ieee80211/mac/Ieee80211Mac.h | 5 +- src/inet/linklayer/ieee80211/mac/Tx.cc | 13 +- src/inet/linklayer/ieee80211/mac/Tx.h | 2 +- .../mac/contract/IFrameSequenceHandler.h | 16 +- .../IManagementFrameTransactionHandler.h | 37 + .../linklayer/ieee80211/mac/contract/ITx.h | 8 +- .../ieee80211/mac/coordinationfunction/Dcf.cc | 180 ++++- .../ieee80211/mac/coordinationfunction/Dcf.h | 20 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 169 ++++- .../ieee80211/mac/coordinationfunction/Hcf.h | 17 + .../mac/framesequence/FrameSequenceHandler.cc | 20 +- .../mac/framesequence/FrameSequenceHandler.h | 5 +- .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 20 +- .../ieee80211/mgmt/Ieee80211MgmtAp.h | 3 + .../module/Ieee80211MgmtApCancellation_1.test | 656 ++++++++++++++++++ .../module/Ieee80211MgmtApHcfQueueDrop_1.test | 58 ++ tests/module/Ieee80211MgmtApQueueDrop_1.test | 58 ++ tests/unit/Ieee80211AddbaTransaction_1.test | 449 +++++++++++- 19 files changed, 1720 insertions(+), 25 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h create mode 100644 tests/module/Ieee80211MgmtApCancellation_1.test diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc index c5b7502c9a0..f2661a23bf6 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Mac.cc @@ -425,6 +425,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 3fb5c2ec704..6a0df331585 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" @@ -36,7 +37,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, public physicallayer::IIeee80211ModeSetListener +class INET_API Ieee80211Mac : public MacProtocolBase, public physicallayer::IIeee80211ModeSetListener, public IManagementFrameTransactionHandler { public: static simsignal_t frameTransmissionOutcomeSignal; @@ -113,6 +114,8 @@ class INET_API Ieee80211Mac : public MacProtocolBase, public physicallayer::IIee virtual void sendDownFrame(Packet *frame); virtual void sendDownPendingRadioConfigMsg(); + virtual void cancelManagementTransaction(uint64_t transactionId) override; + virtual void processUpperFrame(Packet *packet, const Ptr& header); virtual void processLowerFrame(Packet *packet, const Ptr& header); }; diff --git a/src/inet/linklayer/ieee80211/mac/Tx.cc b/src/inet/linklayer/ieee80211/mac/Tx.cc index 9c0d906802b..053b2ae2519 100644 --- a/src/inet/linklayer/ieee80211/mac/Tx.cc +++ b/src/inet/linklayer/ieee80211/mac/Tx.cc @@ -82,6 +82,18 @@ void Tx::transmitFrame(Packet *packet, const Ptr& head scheduleAfter(ifs, endIfsTimer); } +bool Tx::cancelPendingTransmission(ITx::ICallback *owner) +{ + Enter_Method("cancelPendingTransmission"); + if (this->txCallback != owner || transmitting || frame == nullptr || !endIfsTimer->isScheduled()) + return false; + cancelEvent(endIfsTimer); + delete frame; + frame = nullptr; + txCallback = nullptr; + return true; +} + void Tx::radioTransmissionFinished() { Enter_Method("radioTransmissionFinished"); @@ -114,4 +126,3 @@ void Tx::handleMessage(cMessage *msg) } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/Tx.h b/src/inet/linklayer/ieee80211/mac/Tx.h index ae4c37308b6..c05151d28f5 100644 --- a/src/inet/linklayer/ieee80211/mac/Tx.h +++ b/src/inet/linklayer/ieee80211/mac/Tx.h @@ -41,6 +41,7 @@ class INET_API Tx : public SimpleModule, public ITx virtual void transmitFrame(Packet *packet, const Ptr& header, ITx::ICallback *txCallback) override; virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, ITx::ICallback *txCallback) override; + virtual bool cancelPendingTransmission(ITx::ICallback *owner) override; virtual void radioTransmissionFinished() override; }; @@ -48,4 +49,3 @@ class INET_API Tx : public SimpleModule, public ITx } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h index 46f9a820f28..d16cf826483 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h @@ -46,10 +46,24 @@ class INET_API IFrameSequenceHandler virtual void transmissionComplete() = 0; virtual bool isSequenceRunning() = 0; virtual void handleStartRxTimeout() = 0; + + /** + * Requests cancellation at the next ownership-safe frame-sequence + * boundary. An in-flight transmit step remains owned by the sequence + * until its physical transmission completion is delivered. + */ + virtual void cancelFrameSequence() {} + + /** + * Aborts a frame sequence immediately after its pending transmission has + * been cancelled before lower-layer handoff. This is separate from + * cancelFrameSequence() because an already handed-down copy must be + * allowed to complete first. + */ + virtual void abortFrameSequence() {} }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h new file mode 100644 index 00000000000..7b7c806bfef --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h @@ -0,0 +1,37 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IMANAGEMENTFRAMETRANSACTIONHANDLER_H +#define __INET_IMANAGEMENTFRAMETRANSACTIONHANDLER_H + +#include + +#include "inet/common/INETDefs.h" + +namespace inet { +namespace ieee80211 { + +/** + * Provides management-frame transaction lifecycle operations to management + * protocols without exposing coordination-function implementation details. + */ +class INET_API IManagementFrameTransactionHandler +{ + public: + virtual ~IManagementFrameTransactionHandler() {} + + /** + * Cancels all locally queued fragments for the given management + * transaction. An active transmission is retired at an ownership-safe + * frame-sequence boundary. + */ + virtual void cancelManagementTransaction(uint64_t transactionId) = 0; +}; + +} +} + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/contract/ITx.h b/src/inet/linklayer/ieee80211/mac/contract/ITx.h index 090969da65c..9bab6504727 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/ITx.h +++ b/src/inet/linklayer/ieee80211/mac/contract/ITx.h @@ -33,6 +33,13 @@ class INET_API ITx virtual void transmitFrame(Packet *packet, const Ptr& header, ICallback *callback) = 0; virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, ICallback *callback) = 0; + /** + * Cancels a transmission that is still waiting for its inter-frame + * space. The owner must be the callback supplied to transmitFrame(). + * Returns true only when no copy has been handed to the lower layer yet. + * A transmission that is already in progress is left untouched. + */ + virtual bool cancelPendingTransmission(ICallback *owner) = 0; virtual void radioTransmissionFinished() = 0; }; @@ -40,4 +47,3 @@ class INET_API ITx } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index 1aa1a7ba496..350e551ee30 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -14,6 +14,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" #include "inet/linklayer/ieee80211/mac/framesequence/DcfFs.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/rateselection/RateSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" @@ -143,13 +144,22 @@ void Dcf::receiveSignal(cComponent *source, simsignal_t signal, cObject *object, void Dcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) { Enter_Method("handlePacketRemoved"); - if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { - FrameTransmissionDetails transmissionDetails; - transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); - emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + auto transactionTag = packet->findTag(); + if ((reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED || reason == queueing::IPacketQueue::PacketRemovalReason::REMOVED) && transactionTag != nullptr) { + if (cancelManagementTransaction(transactionTag->getTransactionId(), packet)) { + FrameTransmissionDetails transmissionDetails; + transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); + } } } +void Dcf::cancelManagementTransaction(uint64_t transactionId) +{ + Enter_Method("cancelManagementTransaction"); + cancelManagementTransaction(transactionId, nullptr); +} + void Dcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header) { emit(packetSentToPeerSignal, packet); @@ -161,6 +171,135 @@ void Dcf::recipientProcessTransmittedControlResponseFrame(Packet *packet, const throw cRuntimeError("Unknown control response frame"); } +bool Dcf::isPacketReferencedByCurrentFrameSequence(const Packet *packet) const +{ + if (frameSequenceHandler == nullptr || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + for (int i = 0; i < context->getNumSteps(); i++) { + auto transmitStep = dynamic_cast(context->getStep(i)); + if (transmitStep != nullptr && transmitStep->getFrameToTransmit() == packet) + return true; + auto rtsTransmitStep = dynamic_cast(transmitStep); + if (rtsTransmitStep != nullptr && rtsTransmitStep->getProtectedFrame() == packet) + return true; + } + return false; +} + +bool Dcf::isManagementTransactionCancelled(const Packet *packet) const +{ + if (packet == nullptr) + return false; + auto transactionTag = packet->findTag(); + return transactionTag != nullptr && cancelledManagementTransactions.find(transactionTag->getTransactionId()) != cancelledManagementTransactions.end(); +} + +bool Dcf::isCurrentFrameSequenceCancelled(const Packet *packet) const +{ + if (packet == nullptr || frameSequenceHandler == nullptr || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + auto transmitStep = dynamic_cast(context->getLastStep()); + if (transmitStep == nullptr) + transmitStep = dynamic_cast(context->getStepBeforeLast()); + if (transmitStep == nullptr) + return false; + if (transmitStep->getFrameToTransmit() == packet) + return isManagementTransactionCancelled(packet) || (dynamic_cast(transmitStep) != nullptr && + isManagementTransactionCancelled(dynamic_cast(transmitStep)->getProtectedFrame())); + auto rtsTransmitStep = dynamic_cast(transmitStep); + return rtsTransmitStep != nullptr && rtsTransmitStep->getProtectedFrame() == packet && isManagementTransactionCancelled(packet); +} + +bool Dcf::cancelManagementTransaction(uint64_t transactionId, Packet *excludedPacket) +{ + Enter_Method("cancelManagementTransaction"); + auto eventNumber = cSimulation::getActiveSimulation()->getEventNumber(); + if (completedManagementTransactionsEventNumber != eventNumber) { + completedManagementTransactions.clear(); + completedManagementTransactionsEventNumber = eventNumber; + } + if (completedManagementTransactions.find(transactionId) != completedManagementTransactions.end()) + return false; + if (!managementTransactionsBeingCancelled.insert(transactionId).second) + return false; + + // IEEE Std 802.11-2024, 10.3.4.4 and 10.4: terminal retry/lifetime + // failure discards the MMPDU and all remaining fragments. The callback's + // packet is still borrowed by the active frame sequence, so it is left + // for the caller to retire after this helper returns. + + auto belongsToTransaction = [transactionId, excludedPacket](Packet *packet) { + auto transactionTag = packet->findTag(); + return packet != excludedPacket && transactionTag != nullptr && transactionTag->getTransactionId() == transactionId; + }; + + auto pendingQueue = channelAccess->getPendingQueue(); + for (int i = pendingQueue->getNumPackets() - 1; i >= 0; i--) { + auto packet = pendingQueue->getPacket(i); + if (belongsToTransaction(packet)) { + pendingQueue->removePacket(packet); + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + + auto inProgressFrames = channelAccess->getInProgressFrames(); + bool frameSequenceCancellationRequested = false; + bool pendingTransmissionCancelled = false; + for (int i = inProgressFrames->getLength() - 1; i >= 0; i--) { + auto packet = inProgressFrames->getFrames(i); + if (belongsToTransaction(packet)) { + if (isPacketReferencedByCurrentFrameSequence(packet)) { + // Keep the packet alive for raw pointers held by the active + // sequence, but remove it from eligibility immediately. The + // sequence retires it at its next safe boundary. + inProgressFrames->dropFrame(packet); + cancelledManagementTransactions.insert(transactionId); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + bool currentFrameSequenceCancelled = isCurrentFrameSequenceCancelled(packet); + frameSequenceCancellationRequested |= currentFrameSequenceCancelled; + // A sequence can retain completed steps in its context. Only + // cancel Tx when the current transmit/protected step belongs + // to this transaction; the callback owner alone is not enough + // to identify a historical frame. + if (currentFrameSequenceCancelled && tx != nullptr) + pendingTransmissionCancelled |= tx->cancelPendingTransmission(this); + continue; + } + auto header = packet->peekAtFront(); + auto extractedPacket = inProgressFrames->extractFrame(packet); + ASSERT(extractedPacket == packet); + take(packet); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardFrame(packet, header); + ackHandler->dropFrame(header); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + if (frameSequenceCancellationRequested && frameSequenceHandler != nullptr) { + frameSequenceHandler->cancelFrameSequence(); + if (pendingTransmissionCancelled) + frameSequenceHandler->abortFrameSequence(); + } + managementTransactionsBeingCancelled.erase(transactionId); + completedManagementTransactions.insert(transactionId); + return true; +} + void Dcf::scheduleStartRxTimer(simtime_t timeout) { Enter_Method("scheduleStartRxTimer"); @@ -228,6 +367,7 @@ void Dcf::frameSequenceFinished() if (hasFrameToTransmit()) channelAccess->requestChannel(this); mac->sendDownPendingRadioConfigMsg(); // TODO review + cancelledManagementTransactions.clear(); } bool Dcf::isReceptionInProgress() @@ -290,12 +430,24 @@ bool Dcf::hasFrameToTransmit() void Dcf::originatorProcessRtsProtectionFailed(Packet *packet) { Enter_Method("originatorProcessRtsProtectionFailed"); + if (isManagementTransactionCancelled(packet)) { + auto protectedHeader = packet->peekAtFront(); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardRtsFrame(protectedHeader); + channelAccess->getInProgressFrames()->dropFrame(packet); + if (ackHandler != nullptr) + ackHandler->dropFrame(protectedHeader); + return; + } EV_INFO << "RTS frame transmission failed\n"; auto protectedHeader = packet->peekAtFront(); recoveryProcedure->rtsFrameTransmissionFailed(protectedHeader, stationRetryCounters); EV_INFO << "For the current frame exchange, we have CW = " << channelAccess->getCw() << " SRC = " << recoveryProcedure->getShortRetryCount(packet, protectedHeader) << " LRC = " << recoveryProcedure->getLongRetryCount(packet, protectedHeader) << " SSRC = " << stationRetryCounters->getStationShortRetryCount() << " and SLRC = " << stationRetryCounters->getStationLongRetryCount() << std::endl; if (recoveryProcedure->isRtsFrameRetryLimitReached(packet, protectedHeader)) { recoveryProcedure->retryLimitReached(packet, protectedHeader); + auto transactionTag = packet->findTag(); + bool notifyManagement = dynamicPtrCast(protectedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), packet)); channelAccess->getInProgressFrames()->dropFrame(packet); ackHandler->dropFrame(protectedHeader); EV_INFO << "Dropping RTS/CTS protected frame " << packet->getName() << ", because retry limit is reached.\n"; @@ -304,7 +456,7 @@ void Dcf::originatorProcessRtsProtectionFailed(Packet *packet) details.setLimit(recoveryProcedure->getShortRetryLimit()); emit(packetDroppedSignal, packet, &details); emit(linkBrokenSignal, packet); - if (dynamicPtrCast(protectedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); @@ -315,8 +467,12 @@ void Dcf::originatorProcessRtsProtectionFailed(Packet *packet) void Dcf::originatorProcessTransmittedFrame(Packet *packet) { Enter_Method("originatorProcessTransmittedFrame"); + if (isCurrentFrameSequenceCancelled(packet)) + return; EV_INFO << "Processing transmitted frame " << packet->getName() << " as originator in frame sequence.\n"; emit(packetSentToPeerSignal, packet); + if (isCurrentFrameSequenceCancelled(packet)) + return; auto transmittedHeader = packet->peekAtFront(); if (auto dataOrMgmtHeader = dynamicPtrCast(transmittedHeader)) { EV_INFO << "For the current frame exchange, we have CW = " << channelAccess->getCw() << " SRC = " << recoveryProcedure->getShortRetryCount(packet, dataOrMgmtHeader) << " LRC = " << recoveryProcedure->getLongRetryCount(packet, dataOrMgmtHeader) << " SSRC = " << stationRetryCounters->getStationShortRetryCount() << " and SLRC = " << stationRetryCounters->getStationLongRetryCount() << std::endl; @@ -370,6 +526,15 @@ void Dcf::originatorProcessReceivedFrame(Packet *receivedPacket, Packet *lastTra void Dcf::originatorProcessFailedFrame(Packet *failedPacket) { Enter_Method("originatorProcessFailedFrame"); + if (isManagementTransactionCancelled(failedPacket)) { + auto failedHeader = failedPacket->peekAtFront(); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardFrame(failedPacket, failedHeader); + channelAccess->getInProgressFrames()->dropFrame(failedPacket); + if (ackHandler != nullptr) + ackHandler->dropFrame(failedHeader); + return; + } EV_INFO << "Data/Mgmt frame transmission failed\n"; const auto& failedHeader = failedPacket->peekAtFront(); ASSERT(failedHeader->getType() != ST_DATA_WITH_QOS); @@ -383,6 +548,9 @@ void Dcf::originatorProcessFailedFrame(Packet *failedPacket) ackHandler->processFailedFrame(failedHeader); if (retryLimitReached) { recoveryProcedure->retryLimitReached(failedPacket, failedHeader); + auto transactionTag = failedPacket->findTag(); + bool notifyManagement = dynamicPtrCast(failedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), failedPacket)); channelAccess->getInProgressFrames()->dropFrame(failedPacket); ackHandler->dropFrame(failedHeader); EV_INFO << "Dropping frame " << failedPacket->getName() << ", because retry limit is reached.\n"; @@ -391,7 +559,7 @@ void Dcf::originatorProcessFailedFrame(Packet *failedPacket) details.setLimit(-1); // TODO emit(packetDroppedSignal, failedPacket, &details); emit(linkBrokenSignal, failedPacket); - if (dynamicPtrCast(failedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h index 87a4d1ab227..215c34eaab0 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h @@ -8,6 +8,8 @@ #ifndef __INET_DCF_H #define __INET_DCF_H +#include + #include "inet/linklayer/ieee80211/mac/channelaccess/Dcaf.h" #include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" #include "inet/linklayer/ieee80211/mac/contract/ICoordinationFunction.h" @@ -80,6 +82,17 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: // Frame sequence handler IFrameSequenceHandler *frameSequenceHandler = nullptr; + // A management transaction may have several fragmented MPDUs in the + // pending/in-progress queues. Keep the transaction identity only while + // removing its siblings so queue notifications cannot report the same logical + // transaction recursively. A transaction has one pending original before + // fragmentation; the completed set additionally spans the synchronous + // notifications of a bulk queue removal and is cleared at the next event. + std::set managementTransactionsBeingCancelled; + std::set completedManagementTransactions; + eventnumber_t completedManagementTransactionsEventNumber = -1; + std::set cancelledManagementTransactions; + // Station counters StationRetryCounters *stationRetryCounters = nullptr; @@ -98,6 +111,11 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void recipientProcessReceivedControlFrame(Packet *packet, const Ptr& header); virtual void recipientProcessTransmittedControlResponseFrame(Packet *packet, const Ptr& header); + virtual bool isPacketReferencedByCurrentFrameSequence(const Packet *packet) const; + virtual bool isManagementTransactionCancelled(const Packet *packet) const; + virtual bool isCurrentFrameSequenceCancelled(const Packet *packet) const; + virtual bool cancelManagementTransaction(uint64_t transactionId, Packet *excludedPacket); + protected: // IChannelAccess::ICallback virtual void channelGranted(IChannelAccess *channelAccess) override; @@ -126,6 +144,8 @@ class INET_API Dcf : public ICoordinationFunction, public IFrameSequenceHandler: public: virtual ~Dcf(); + virtual void cancelManagementTransaction(uint64_t transactionId); + // ICoordinationFunction virtual void processUpperFrame(Packet *packet, const Ptr& header) override; virtual void processLowerFrame(Packet *packet, const Ptr& header) override; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 37c11a6de03..b4b7f41a623 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -175,7 +175,8 @@ void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemo // HCF treats explicit REMOVED notifications as terminal transaction // disposal; code relocating a packet must use dequeuePacket(). if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED || reason == queueing::IPacketQueue::PacketRemovalReason::REMOVED) { - if (reason == queueing::IPacketQueue::PacketRemovalReason::DROPPED && packet->findTag() != nullptr) { + auto transactionTag = packet->findTag(); + if (transactionTag != nullptr && cancelManagementTransaction(transactionTag->getTransactionId(), packet)) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); @@ -187,6 +188,99 @@ void Hcf::handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemo resumeEligibleChannelAccess(); } +void Hcf::cancelManagementTransaction(uint64_t transactionId) +{ + Enter_Method("cancelManagementTransaction"); + cancelManagementTransaction(transactionId, nullptr); +} + +bool Hcf::cancelManagementTransaction(uint64_t transactionId, Packet *excludedPacket) +{ + Enter_Method("cancelManagementTransaction"); + auto eventNumber = cSimulation::getActiveSimulation()->getEventNumber(); + if (completedManagementTransactionsEventNumber != eventNumber) { + completedManagementTransactions.clear(); + completedManagementTransactionsEventNumber = eventNumber; + } + if (completedManagementTransactions.find(transactionId) != completedManagementTransactions.end()) + return false; + if (!managementTransactionsBeingCancelled.insert(transactionId).second) + return false; + + // IEEE Std 802.11-2024, 10.3.4.4 and 10.4: terminal retry/lifetime + // failure discards the MMPDU and all remaining fragments. The callback's + // packet is still borrowed by the active frame sequence, so it is left + // for the caller to retire after this helper returns. + + auto belongsToTransaction = [transactionId, excludedPacket](Packet *packet) { + auto transactionTag = packet->findTag(); + return packet != excludedPacket && transactionTag != nullptr && transactionTag->getTransactionId() == transactionId; + }; + bool frameSequenceCancellationRequested = false; + bool pendingTransmissionCancelled = false; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto edcaf = edca->getEdcaf(AccessCategory(ac)); + auto pendingQueue = edcaf->getPendingQueue(); + for (int i = pendingQueue->getNumPackets() - 1; i >= 0; i--) { + auto packet = pendingQueue->getPacket(i); + if (belongsToTransaction(packet)) { + pendingQueue->removePacket(packet); + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + auto inProgressFrames = edcaf->getInProgressFrames(); + for (int i = inProgressFrames->getLength() - 1; i >= 0; i--) { + auto packet = inProgressFrames->getFrames(i); + if (belongsToTransaction(packet)) { + if (isPacketReferencedByCurrentFrameSequence(packet)) { + // Keep the packet alive for raw pointers held by the + // active sequence, but remove it from eligibility + // immediately. The sequence retires it at its next safe + // boundary. + inProgressFrames->dropFrame(packet); + cancelledManagementTransactions.insert(transactionId); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + bool currentFrameSequenceCancelled = isCurrentFrameSequenceCancelled(packet); + frameSequenceCancellationRequested |= currentFrameSequenceCancelled; + // A sequence can retain completed steps in its context. + // Only cancel Tx when the current transmit/protected step + // belongs to this transaction; the callback owner alone + // is not enough to identify a historical frame. + if (currentFrameSequenceCancelled && tx != nullptr) + pendingTransmissionCancelled |= tx->cancelPendingTransmission(this); + continue; + } + auto header = packet->peekAtFront(); + auto extractedPacket = inProgressFrames->extractFrame(packet); + ASSERT(extractedPacket == packet); + take(packet); + auto managementRecoveryProcedure = edca->getMgmtAndNonQoSRecoveryProcedure(); + if (managementRecoveryProcedure != nullptr) + managementRecoveryProcedure->discardFrame(packet, header); + edcaf->getAckHandler()->dropFrame(header); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + } + if (frameSequenceCancellationRequested && frameSequenceHandler != nullptr) { + frameSequenceHandler->cancelFrameSequence(); + if (pendingTransmissionCancelled) + frameSequenceHandler->abortFrameSequence(); + } + managementTransactionsBeingCancelled.erase(transactionId); + completedManagementTransactions.insert(transactionId); + return true; +} + std::string Hcf::getFrameSequenceInfo() const { if (!frameSequenceHandler->isSequenceRunning()) @@ -296,6 +390,33 @@ bool Hcf::isPacketReferencedByCurrentFrameSequence(const Packet *packet) const return false; } +bool Hcf::isManagementTransactionCancelled(const Packet *packet) const +{ + if (packet == nullptr) + return false; + auto transactionTag = packet->findTag(); + return transactionTag != nullptr && cancelledManagementTransactions.find(transactionTag->getTransactionId()) != cancelledManagementTransactions.end(); +} + +bool Hcf::isCurrentFrameSequenceCancelled(const Packet *packet) const +{ + if (packet == nullptr || frameSequenceHandler == nullptr || !frameSequenceHandler->isSequenceRunning()) + return false; + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + auto transmitStep = dynamic_cast(context->getLastStep()); + if (transmitStep == nullptr) + transmitStep = dynamic_cast(context->getStepBeforeLast()); + if (transmitStep == nullptr) + return false; + if (transmitStep->getFrameToTransmit() == packet) + return isManagementTransactionCancelled(packet) || (dynamic_cast(transmitStep) != nullptr && + isManagementTransactionCancelled(dynamic_cast(transmitStep)->getProtectedFrame())); + auto rtsTransmitStep = dynamic_cast(transmitStep); + return rtsTransmitStep != nullptr && rtsTransmitStep->getProtectedFrame() == packet && isManagementTransactionCancelled(packet); +} + void Hcf::scheduleStartRxTimer(simtime_t timeout) { Enter_Method("scheduleStartRxTimer"); @@ -471,6 +592,9 @@ int Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) else if (auto mgmtHeader = dynamicPtrCast(internallyCollidedHeader)) edca->getMgmtAndNonQoSRecoveryProcedure()->retryLimitReached(internallyCollidedFrame, mgmtHeader); else ; // TODO + NonQoSDataFrame + auto transactionTag = internallyCollidedFrame->findTag(); + bool notifyManagement = dynamicPtrCast(internallyCollidedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), internallyCollidedFrame)); edcaf->getInProgressFrames()->dropFrame(internallyCollidedFrame); processDroppedBlockAckSetupFrame(internallyCollidedFrame); processDroppedBlockAckTeardownFrame(internallyCollidedFrame); @@ -480,7 +604,7 @@ int Hcf::handleInternalCollision(std::vector internallyCollidedEdcafs) details.setLimit(-1); // TODO emit(packetDroppedSignal, internallyCollidedFrame, &details); emit(linkBrokenSignal, internallyCollidedFrame); - if (dynamicPtrCast(internallyCollidedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, internallyCollidedFrame, &transmissionDetails); @@ -519,6 +643,7 @@ void Hcf::frameSequenceFinished() } else throw cRuntimeError("Frame sequence finished but channel owner not found!"); + cancelledManagementTransactions.clear(); } void Hcf::recipientProcessReceivedFrame(Packet *packet, const Ptr& header) @@ -644,6 +769,19 @@ void Hcf::transmissionComplete(Packet *packet, const PtrpeekAtFront(); + auto edcaf = edca->getChannelOwner(); + if (edcaf != nullptr) { + auto recoveryProcedure = edca->getMgmtAndNonQoSRecoveryProcedure(); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardRtsFrame(protectedHeader); + edcaf->getInProgressFrames()->dropFrame(packet); + if (edcaf->getAckHandler() != nullptr) + edcaf->getAckHandler()->dropFrame(protectedHeader); + } + return; + } auto protectedHeader = packet->peekAtFront(); auto edcaf = edca->getChannelOwner(); if (edcaf) { @@ -671,6 +809,9 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) } else edca->getMgmtAndNonQoSRecoveryProcedure()->discardRtsFrame(addbaRequest); + auto transactionTag = packet->findTag(); + bool notifyManagement = retryLimitReached && dynamicPtrCast(protectedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), packet)); edcaf->getInProgressFrames()->dropFrame(packet); processDroppedBlockAckSetupFrame(packet); processDroppedBlockAckTeardownFrame(packet); @@ -683,7 +824,7 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) emit(packetDroppedSignal, packet, &details); if (retryLimitReached) { emit(linkBrokenSignal, packet); - if (dynamicPtrCast(protectedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, packet, &transmissionDetails); @@ -698,11 +839,15 @@ void Hcf::originatorProcessRtsProtectionFailed(Packet *packet) void Hcf::originatorProcessTransmittedFrame(Packet *packet) { Enter_Method("originatorProcessTransmittedFrame"); + if (isCurrentFrameSequenceCancelled(packet)) + return; EV_INFO << "Processing transmitted frame " << packet->getName() << " as originator in frame sequence.\n"; auto transmittedHeader = packet->peekAtFront(); auto edcaf = edca->getChannelOwner(); if (edcaf) { edcaf->emit(packetSentToPeerSignal, packet); + if (isCurrentFrameSequenceCancelled(packet)) + return; AccessCategory ac = edcaf->getAccessCategory(); if (transmittedHeader->getReceiverAddress().isMulticast()) { edcaf->getRecoveryProcedure()->multicastFrameTransmitted(); @@ -778,6 +923,19 @@ void Hcf::originatorProcessTransmittedControlFrame(const PtrpeekAtFront(); + auto edcaf = edca->getChannelOwner(); + if (edcaf != nullptr) { + auto recoveryProcedure = edca->getMgmtAndNonQoSRecoveryProcedure(); + if (recoveryProcedure != nullptr) + recoveryProcedure->discardFrame(failedPacket, failedHeader); + edcaf->getInProgressFrames()->dropFrame(failedPacket); + if (edcaf->getAckHandler() != nullptr) + edcaf->getAckHandler()->dropFrame(failedHeader); + } + return; + } auto failedHeader = failedPacket->peekAtFront(); auto edcaf = edca->getChannelOwner(); if (edcaf) { @@ -820,6 +978,9 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) } else edca->getMgmtAndNonQoSRecoveryProcedure()->discardFrame(failedPacket, addbaRequest); + auto transactionTag = failedPacket->findTag(); + bool notifyManagement = retryLimitReached && dynamicPtrCast(failedHeader) != nullptr && + (transactionTag == nullptr || cancelManagementTransaction(transactionTag->getTransactionId(), failedPacket)); edcaf->getInProgressFrames()->dropFrame(failedPacket); processDroppedBlockAckSetupFrame(failedPacket); processDroppedBlockAckTeardownFrame(failedPacket); @@ -832,7 +993,7 @@ void Hcf::originatorProcessFailedFrame(Packet *failedPacket) emit(packetDroppedSignal, failedPacket, &details); if (retryLimitReached) { emit(linkBrokenSignal, failedPacket); - if (dynamicPtrCast(failedHeader)) { + if (notifyManagement) { FrameTransmissionDetails transmissionDetails; transmissionDetails.setStatus(FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); emit(Ieee80211Mac::frameTransmissionOutcomeSignal, failedPacket, &transmissionDetails); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index fc9b5d49eff..b2230bff27b 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -10,6 +10,7 @@ #include #include +#include #include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h" @@ -111,6 +112,17 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: // Frame sequence handler IFrameSequenceHandler *frameSequenceHandler = nullptr; + // A management transaction may have several fragmented MPDUs in the + // pending/in-progress queues. Keep the transaction identity only while + // removing its siblings so queue notifications cannot report the same logical + // transaction recursively. A transaction has one pending original before + // fragmentation; the completed set additionally spans the synchronous + // notifications of a bulk queue removal and is cleared at the next event. + std::set managementTransactionsBeingCancelled; + std::set completedManagementTransactions; + eventnumber_t completedManagementTransactionsEventNumber = -1; + std::set cancelledManagementTransactions; + // Protection mechanisms SingleProtectionMechanism *singleProtectionMechanism = nullptr; @@ -133,6 +145,9 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual bool processDroppedBlockAckSetupFrame(Packet *packet); virtual bool processDroppedBlockAckTeardownFrame(Packet *packet); virtual bool isPacketReferencedByCurrentFrameSequence(const Packet *packet) const; + virtual bool isManagementTransactionCancelled(const Packet *packet) const; + virtual bool isCurrentFrameSequenceCancelled(const Packet *packet) const; + virtual bool cancelManagementTransaction(uint64_t transactionId, Packet *excludedPacket); virtual void receiveSignal(cComponent *source, simsignal_t signal, cObject *object, cObject *details) override; virtual void handlePacketRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason); virtual void trackPendingFrame(Packet *packet, AccessCategory accessCategory); @@ -188,6 +203,8 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: public: virtual ~Hcf(); + virtual void cancelManagementTransaction(uint64_t transactionId); + // ICoordinationFunction virtual void processUpperFrame(Packet *packet, const Ptr& header) override; virtual void processLowerFrame(Packet *packet, const Ptr& header) override; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc index 1eddb2b2048..a2dda4b570f 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc @@ -31,6 +31,11 @@ void FrameSequenceHandler::handleStartRxTimeout() void FrameSequenceHandler::processResponse(Packet *frame) { ASSERT(callback != nullptr); + if (frameSequenceCancellationRequested) { + delete frame; + abortFrameSequence(); + return; + } auto lastStep = context->getLastStep(); switch (lastStep->getType()) { case IFrameSequenceStep::Type::RECEIVE: { @@ -52,7 +57,10 @@ void FrameSequenceHandler::processResponse(Packet *frame) void FrameSequenceHandler::transmissionComplete() { if (isSequenceRunning()) { - finishFrameSequenceStep(); + if (frameSequenceCancellationRequested) + abortFrameSequence(); + else + finishFrameSequenceStep(); if (isSequenceRunning()) startFrameSequenceStep(); } @@ -63,6 +71,7 @@ void FrameSequenceHandler::startFrameSequence(IFrameSequence *frameSequence, Fra EV_INFO << "Starting frame sequence.\n"; this->callback = callback; if (!isSequenceRunning()) { + frameSequenceCancellationRequested = false; this->frameSequence = frameSequence; this->context = context; frameSequence->startSequence(context, 0); @@ -75,6 +84,10 @@ void FrameSequenceHandler::startFrameSequence(IFrameSequence *frameSequence, Fra void FrameSequenceHandler::startFrameSequenceStep() { ASSERT(isSequenceRunning()); + if (frameSequenceCancellationRequested) { + abortFrameSequence(); + return; + } auto nextStep = frameSequence->prepareStep(context); EV_INFO << "Starting next frame sequence step: history = " << frameSequence->getHistory() << "\n"; if (nextStep == nullptr) @@ -119,6 +132,8 @@ void FrameSequenceHandler::finishFrameSequenceStep() case IFrameSequenceStep::Type::TRANSMIT: { auto transmitStep = static_cast(lastStep); callback->originatorProcessTransmittedFrame(transmitStep->getFrameToTransmit()); + if (frameSequenceCancellationRequested && isSequenceRunning()) + abortFrameSequence(); break; } case IFrameSequenceStep::Type::RECEIVE: { @@ -143,6 +158,7 @@ void FrameSequenceHandler::finishFrameSequence() context = nullptr; frameSequence = nullptr; callback = nullptr; + frameSequenceCancellationRequested = false; inProgressFrames->clearDroppedFrames(); } @@ -166,6 +182,7 @@ void FrameSequenceHandler::abortFrameSequence() context = nullptr; frameSequence = nullptr; callback = nullptr; + frameSequenceCancellationRequested = false; inProgressFrames->clearDroppedFrames(); } @@ -177,4 +194,3 @@ FrameSequenceHandler::~FrameSequenceHandler() } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h index 370295cc6f7..9b423609170 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h @@ -21,12 +21,12 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler IFrameSequenceHandler::ICallback *callback = nullptr; IFrameSequence *frameSequence = nullptr; FrameSequenceContext *context = nullptr; + bool frameSequenceCancellationRequested = false; protected: virtual void startFrameSequenceStep(); virtual void finishFrameSequenceStep(); virtual void finishFrameSequence(); - virtual void abortFrameSequence(); public: virtual const FrameSequenceContext *getContext() const override { return context; } @@ -36,6 +36,8 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler virtual void transmissionComplete() override; virtual void handleStartRxTimeout() override; virtual bool isSequenceRunning() override { return frameSequence != nullptr; } + virtual void cancelFrameSequence() override { if (isSequenceRunning()) frameSequenceCancellationRequested = true; } + virtual void abortFrameSequence() override; virtual ~FrameSequenceHandler(); }; @@ -44,4 +46,3 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index baad0d6e357..a4a09ce78b6 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -62,6 +62,7 @@ void Ieee80211MgmtAp::initialize(int stage) beaconTimer = new cMessage("beaconTimer"); auto macModule = getModuleFromPar(par("macModule"), this); macModule->subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, this); + managementFrameTransactionHandler.reference(this, "macModule", true); } } @@ -206,6 +207,17 @@ void Ieee80211MgmtAp::clearPendingAssociation(StaInfo *sta) sta->pendingHtOperation = Ieee80211HtOperation(); } +void Ieee80211MgmtAp::supersedePendingAssociation(StaInfo *sta) +{ + auto transactionId = sta->pendingAssociationTransactionId; + // Clear the AP bookkeeping before entering the MAC so synchronous queue + // callbacks cannot mistake this superseded frame for the active + // transaction. + clearPendingAssociation(sta); + if (transactionId != 0 && managementFrameTransactionHandler) + managementFrameTransactionHandler->cancelManagementTransaction(transactionId); +} + void Ieee80211MgmtAp::sendBeacon() { EV << "Sending beacon\n"; @@ -250,7 +262,7 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; if (wasAssociated) mib->releaseAssociationId(sta->address); @@ -313,7 +325,7 @@ void Ieee80211MgmtAp::handleDeauthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; // mark STA as not authenticated; alternatively, it could also be removed from staList if (wasAssociated) @@ -494,7 +506,7 @@ void Ieee80211MgmtAp::handleDisassociationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED; if (wasAssociated) mib->releaseAssociationId(sta->address); @@ -569,6 +581,8 @@ void Ieee80211MgmtAp::start() void Ieee80211MgmtAp::stop() { cancelEvent(beaconTimer); + for (auto& entry : staList) + supersedePendingAssociation(&entry.second); staList.clear(); nextAssociationTransactionId = 0; mib->clearAssociationIds(); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h index fe8c0c03192..afe48285491 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h @@ -11,6 +11,7 @@ #include #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/contract/IManagementFrameTransactionHandler.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtApBase.h" namespace inet { @@ -73,6 +74,7 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase StaList staList; ///< list of STAs cMessage *beaconTimer = nullptr; uint64_t nextAssociationTransactionId = 0; + ModuleRefByPar managementFrameTransactionHandler; public: Ieee80211MgmtAp() {} @@ -101,6 +103,7 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase virtual uint64_t createAssociationTransactionId(); virtual void clearPendingAssociation(StaInfo *sta); + virtual void supersedePendingAssociation(StaInfo *sta); /** Classifies a terminal management-MPDU result using its transaction tag and fragment state. */ static AssociationResponseDisposition getAssociationResponseDisposition(const Packet *responseFrame, diff --git a/tests/module/Ieee80211MgmtApCancellation_1.test b/tests/module/Ieee80211MgmtApCancellation_1.test new file mode 100644 index 00000000000..19da98e10df --- /dev/null +++ b/tests/module/Ieee80211MgmtApCancellation_1.test @@ -0,0 +1,656 @@ +%description: +Verify that superseding an AP association response cancels every tagged +fragment owned by the real DCF or HCF MAC. A deterministic contention module +holds the response until the test grants access, and a test MAC holds the +first frame handoff so authentication restart, deauthentication, and +disassociation can all cancel an active response before it reaches the PHY. +The test also cancels a DCF response after CTS and a HCF response during its +IFS, before either response crosses the Tx-to-MAC handoff. Replacement +responses are then completed through the ordinary ACK path. + +%file: TestIeee80211MgmtApCancellation.cc + +#include +#include +#include + +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/Tx.h" +#include "inet/linklayer/ieee80211/mac/contention/Contention.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/contract/IContention.h" +#include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" +#include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" +#include "inet/queueing/contract/IPacketQueue.h" + +namespace inet { +namespace ieee80211 { + +class TestBlockedContention : public Contention +{ + protected: + IContention::ICallback *blockedCallback = nullptr; + + public: + virtual void startContention(int, simtime_t, simtime_t, simtime_t, IContention::ICallback *callback) override + { + Enter_Method("startContention"); + ASSERT(blockedCallback == nullptr); + blockedCallback = callback; + } + + virtual bool isContentionInProgress() override { return blockedCallback != nullptr; } + + bool hasBlockedContention() const { return blockedCallback != nullptr; } + + void grantAccess() + { + Enter_Method("grantAccess"); + ASSERT(blockedCallback != nullptr); + auto callback = blockedCallback; + blockedCallback = nullptr; + callback->channelAccessGranted(); + } +}; + +Define_Module(TestBlockedContention); + +class TestInspectableTx : public Tx +{ + protected: + bool holdDelayedAssociationResponse = false; + + public: + bool isWaitingForIfs() const { return frame != nullptr && !transmitting && endIfsTimer != nullptr && endIfsTimer->isScheduled(); } + + void setHoldDelayedAssociationResponse(bool hold) { holdDelayedAssociationResponse = hold; } + + virtual void transmitFrame(Packet *packet, const Ptr& header, simtime_t ifs, ITx::ICallback *callback) override + { + Enter_Method("transmitFrame"); + bool hold = holdDelayedAssociationResponse && ifs > 0 && (header->getType() == ST_ASSOCIATIONRESPONSE || header->getType() == ST_REASSOCIATIONRESPONSE); + Tx::transmitFrame(packet, header, ifs, callback); + if (hold && !transmitting && endIfsTimer->isScheduled()) { + cancelEvent(endIfsTimer); + scheduleAfter(SimTime(1, SIMTIME_S), endIfsTimer); + } + } +}; + +Define_Module(TestInspectableTx); + +class TestHoldingIeee80211Mac : public Ieee80211Mac +{ + protected: + Packet *heldFrame = nullptr; + bool holdNextFrame = true; + int rtsHandoffCount = 0; + int ctsReceivedCount = 0; + std::vector managementHandoffTransactionIds; + + void discardHeldFrame() + { + if (heldFrame != nullptr) { + auto frame = heldFrame; + heldFrame = nullptr; + if (frame->getOwner() == this) + drop(frame); + delete frame; + } + } + + public: + virtual ~TestHoldingIeee80211Mac() { discardHeldFrame(); } + + virtual void sendDownFrame(Packet *frame) override + { + auto header = frame->peekAtFront(); + if (header->getType() == ST_RTS) + rtsHandoffCount++; + if (header->getType() == ST_ASSOCIATIONRESPONSE || header->getType() == ST_REASSOCIATIONRESPONSE) { + auto transactionTag = frame->findTag(); + if (transactionTag != nullptr) + managementHandoffTransactionIds.push_back(transactionTag->getTransactionId()); + } + if (holdNextFrame) { + ASSERT(heldFrame == nullptr); + holdNextFrame = false; + take(frame); + heldFrame = frame; + } + else + Ieee80211Mac::sendDownFrame(frame); + } + + virtual void processLowerFrame(Packet *packet, const Ptr& header) override + { + if (header->getType() == ST_CTS) + ctsReceivedCount++; + Ieee80211Mac::processLowerFrame(packet, header); + } + + bool hasHeldFrame() const { return heldFrame != nullptr; } + + bool isWaitingForIfs() const + { + auto tx = check_and_cast(getSubmodule("tx")); + return tx->isWaitingForIfs(); + } + + int getRtsHandoffCount() const { return rtsHandoffCount; } + int getCtsReceivedCount() const { return ctsReceivedCount; } + int getManagementHandoffCount(uint64_t transactionId) const { return std::count(managementHandoffTransactionIds.begin(), managementHandoffTransactionIds.end(), transactionId); } + + void disarmHold() + { + Enter_Method("disarmHold"); + ASSERT(heldFrame == nullptr); + holdNextFrame = false; + } + + void armDelayedAssociationResponseHold() + { + Enter_Method("armDelayedAssociationResponseHold"); + auto tx = check_and_cast(getSubmodule("tx")); + tx->setHoldDelayedAssociationResponse(true); + } + + void disarmDelayedAssociationResponseHold() + { + Enter_Method("disarmDelayedAssociationResponseHold"); + auto tx = check_and_cast(getSubmodule("tx")); + tx->setHoldDelayedAssociationResponse(false); + } + + void armHold() + { + Enter_Method("armHold"); + ASSERT(heldFrame == nullptr); + holdNextFrame = true; + } + + void completeHeldTransmission() + { + Enter_Method("completeHeldTransmission"); + ASSERT(heldFrame != nullptr); + auto tx = check_and_cast(getSubmodule("tx")); + tx->radioTransmissionFinished(); + discardHeldFrame(); + } +}; + +Define_Module(TestHoldingIeee80211Mac); + +class TestIeee80211MgmtApCancellation : public Ieee80211MgmtAp +{ + public: + std::vector responseStatuses; + std::vector responseTransactionIds; + + virtual void start() override + { + // Keep beacon traffic out of the deterministic transaction exchange. + Ieee80211MgmtApBase::start(); + } + + virtual void sendManagementFrame(const char *name, const Ptr& body, int subtype, + const MacAddress& destAddr, uint64_t transactionId = 0) override + { + // Authentication is driven directly by the test and its response is + // irrelevant to the AP association-response transaction. + if (subtype == ST_AUTHENTICATION) + return; + Ieee80211MgmtAp::sendManagementFrame(name, body, subtype, destAddr, transactionId); + } + + void markAuthenticated(const MacAddress& address) + { + Enter_Method("markAuthenticated"); + auto& sta = staList[address]; + sta.address = address; + sta.authSeqExpected = 1; + mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + } + + void submitAssociationRequest(const MacAddress& address) + { + Enter_Method("submitAssociationRequest"); + auto packet = new Packet("AssociationRequest"); + auto body = makeShared(); + body->setSSID("SSID"); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 1; + body->setSupportedRates(rates); + body->setChunkLength(B(1)); + packet->insertAtBack(body); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleAssociationRequestFrame(packet, header); + } + + void submitAuthentication(const MacAddress& address, int sequenceNumber) + { + Enter_Method("submitAuthentication"); + auto packet = new Packet("Authentication"); + auto body = makeShared(); + body->setSequenceNumber(sequenceNumber); + body->setStatusCode(SC_SUCCESSFUL); + packet->insertAtBack(body); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleAuthenticationFrame(packet, header); + } + + void submitDeauthentication(const MacAddress& address) + { + Enter_Method("submitDeauthentication"); + auto packet = new Packet("Deauthentication"); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleDeauthenticationFrame(packet, header); + } + + void submitDisassociation(const MacAddress& address) + { + Enter_Method("submitDisassociation"); + auto packet = new Packet("Disassociation"); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleDisassociationFrame(packet, header); + } + + bool hasPendingAssociation(const MacAddress& address) const + { + auto it = staList.find(address); + return it != staList.end() && it->second.pendingAssociationTransactionId != 0; + } + + uint64_t getPendingAssociationTransactionId(const MacAddress& address) const + { + return staList.at(address).pendingAssociationTransactionId; + } + + short reserveAssociationId(const MacAddress& address) { return mib->reserveAssociationId(address); } + void cancelAssociationIdReservation(const MacAddress& address) { mib->cancelAssociationIdReservation(address); } + bool hasCommittedAssociationId(const MacAddress& address) const + { + return mib->bssAccessPointData.associationIds.find(address) != mib->bssAccessPointData.associationIds.end(); + } + + Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const + { + return mib->bssAccessPointData.stations.at(address); + } + + protected: + virtual void frameTransmissionFinished(const Packet *frame, FrameTransmissionStatus status) override + { + responseStatuses.push_back(status); + auto transactionTag = frame == nullptr ? nullptr : frame->findTag(); + responseTransactionIds.push_back(transactionTag == nullptr ? 0 : transactionTag->getTransactionId()); + Ieee80211MgmtAp::frameTransmissionFinished(frame, status); + } +}; + +Define_Module(TestIeee80211MgmtApCancellation); + +class TransactionSignalListener : public cListener +{ + public: + std::vector sentTransactionIds; + std::vector lowerTransactionIds; + std::vector droppedTransactionIds; + + virtual void receiveSignal(cComponent *, simsignal_t signalID, cObject *value, cObject *) override + { + auto packet = dynamic_cast(value); + if (packet == nullptr || (signalID != packetSentToPeerSignal && signalID != packetSentToLowerSignal && signalID != packetDroppedSignal)) + return; + auto header = packet->peekAtFront(); + if (header->getType() != ST_ASSOCIATIONRESPONSE && header->getType() != ST_REASSOCIATIONRESPONSE) + return; + auto transactionTag = packet->findTag(); + if (transactionTag == nullptr) + return; + if (signalID == packetSentToPeerSignal) + sentTransactionIds.push_back(transactionTag->getTransactionId()); + else if (signalID == packetSentToLowerSignal) + lowerTransactionIds.push_back(transactionTag->getTransactionId()); + else + droppedTransactionIds.push_back(transactionTag->getTransactionId()); + } +}; + +class Ieee80211MgmtApCancellationTest : public cSimpleModule +{ + protected: + enum class Supersession { + AUTHENTICATION_RESTART, + DEAUTHENTICATION, + DISASSOCIATION, + }; + + static int countTransaction(queueing::IPacketQueue *queue, uint64_t transactionId) + { + int count = 0; + for (int i = 0; i < queue->getNumPackets(); i++) { + auto transactionTag = queue->getPacket(i)->findTag(); + if (transactionTag != nullptr && transactionTag->getTransactionId() == transactionId) + count++; + } + return count; + } + + static int countTransaction(InProgressFrames *frames, uint64_t transactionId) + { + int count = 0; + for (int i = 0; i < frames->getLength(); i++) { + auto transactionTag = frames->getFrames(i)->findTag(); + if (transactionTag != nullptr && transactionTag->getTransactionId() == transactionId) + count++; + } + return count; + } + + void runActiveCancellation(const char *apName, bool qos, const MacAddress& station, Supersession supersession, bool expectReplacement, bool cancelDuringIfs = false) + { + auto apBase = std::string("^.") + apName + ".wlan[0]"; + auto mgmt = check_and_cast(getModuleByPath((apBase + ".mgmt").c_str())); + auto mac = check_and_cast(getModuleByPath((apBase + ".mac").c_str())); + auto pendingQueuePath = apBase + (qos ? ".mac.hcf.edca.edcaf[3].pendingQueue" : ".mac.dcf.channelAccess.pendingQueue"); + auto inProgressFramesPath = apBase + (qos ? ".mac.hcf.edca.edcaf[3].inProgressFrames" : ".mac.dcf.channelAccess.inProgressFrames"); + auto contentionPath = apBase + (qos ? ".mac.hcf.edca.edcaf[3].contention" : ".mac.dcf.channelAccess.contention"); + auto dropSignalSourcePath = apBase + (qos ? ".mac.hcf" : ".mac.dcf"); + auto sentSignalSourcePath = apBase + (qos ? ".mac.hcf.edca.edcaf[3]" : ".mac.dcf"); + auto pendingQueue = check_and_cast(getModuleByPath(pendingQueuePath.c_str())); + auto inProgressFrames = check_and_cast(getModuleByPath(inProgressFramesPath.c_str())); + auto contention = check_and_cast(getModuleByPath(contentionPath.c_str())); + auto dropSignalSource = getModuleByPath(dropSignalSourcePath.c_str()); + auto sentSignalSource = getModuleByPath(sentSignalSourcePath.c_str()); + TransactionSignalListener signalListener; + sentSignalSource->subscribe(packetSentToPeerSignal, &signalListener); + mac->subscribe(packetSentToLowerSignal, &signalListener); + dropSignalSource->subscribe(packetDroppedSignal, &signalListener); + + if (supersession != Supersession::DISASSOCIATION) + mgmt->markAuthenticated(station); + mgmt->submitAssociationRequest(station); + auto oldTransactionId = mgmt->getPendingAssociationTransactionId(station); + auto oldAssociationId = mgmt->reserveAssociationId(station); + ASSERT(oldTransactionId != 0); + ASSERT(oldAssociationId != 0); + for (int i = 0; i < 1000 && countTransaction(pendingQueue, oldTransactionId) == 0; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(countTransaction(pendingQueue, oldTransactionId) > 0); + ASSERT(inProgressFrames->getLength() == 0); + ASSERT(contention->hasBlockedContention()); + + auto oldRtsHandoffCount = mac->getRtsHandoffCount(); + auto oldCtsReceivedCount = mac->getCtsReceivedCount(); + auto oldManagementHandoffCount = mac->getManagementHandoffCount(oldTransactionId); + if (cancelDuringIfs) { + mac->disarmHold(); + mac->armDelayedAssociationResponseHold(); + } + contention->grantAccess(); + if (cancelDuringIfs) { + for (int i = 0; i < 1000 && (!mac->isWaitingForIfs() || mac->getCtsReceivedCount() == oldCtsReceivedCount || mac->getRtsHandoffCount() == oldRtsHandoffCount); i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mac->isWaitingForIfs()); + ASSERT(mac->getCtsReceivedCount() > oldCtsReceivedCount); + ASSERT(mac->getRtsHandoffCount() > oldRtsHandoffCount); + ASSERT(!mac->hasHeldFrame()); + } + else { + for (int i = 0; i < 1000 && !mac->hasHeldFrame(); i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mac->hasHeldFrame()); + } + auto oldInProgressCount = countTransaction(inProgressFrames, oldTransactionId); + ASSERT(oldInProgressCount > 0); + auto oldDroppedCount = signalListener.droppedTransactionIds.size(); + auto oldSentCount = signalListener.sentTransactionIds.size(); + auto oldLowerCount = signalListener.lowerTransactionIds.size(); + auto oldCallbackCount = mgmt->responseStatuses.size(); + + if (supersession == Supersession::AUTHENTICATION_RESTART) + mgmt->submitAuthentication(station, 1); + else if (supersession == Supersession::DEAUTHENTICATION) + mgmt->submitDeauthentication(station); + else + mgmt->submitDisassociation(station); + ASSERT(!mgmt->hasPendingAssociation(station)); + ASSERT(countTransaction(pendingQueue, oldTransactionId) == 0); + ASSERT(countTransaction(inProgressFrames, oldTransactionId) == 0); + ASSERT(signalListener.droppedTransactionIds.size() - oldDroppedCount == (size_t)oldInProgressCount); + ASSERT(signalListener.sentTransactionIds.size() == oldSentCount); + ASSERT(signalListener.lowerTransactionIds.size() == oldLowerCount); + ASSERT(mgmt->responseStatuses.size() == oldCallbackCount); + ASSERT(mac->getManagementHandoffCount(oldTransactionId) == oldManagementHandoffCount); + if (cancelDuringIfs) + mac->disarmDelayedAssociationResponseHold(); + + auto reusableAddress = MacAddress((std::string("02:00:00:00:10:") + (qos ? "02" : "01")).c_str()); + if (!mgmt->hasCommittedAssociationId(station)) { + ASSERT(mgmt->reserveAssociationId(reusableAddress) == oldAssociationId); + mgmt->cancelAssociationIdReservation(reusableAddress); + } + + uint64_t replacementTransactionId = 0; + if (expectReplacement) { + if (supersession == Supersession::DEAUTHENTICATION) + mgmt->submitAuthentication(station, 1); + ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::AUTHENTICATED); + mgmt->submitAssociationRequest(station); + replacementTransactionId = mgmt->getPendingAssociationTransactionId(station); + ASSERT(replacementTransactionId != 0 && replacementTransactionId != oldTransactionId); + ASSERT(mgmt->hasPendingAssociation(station)); + } + + if (!cancelDuringIfs) { + mac->completeHeldTransmission(); + ASSERT(!mac->hasHeldFrame()); + } + else { + ASSERT(!mac->hasHeldFrame()); + ASSERT(!mac->isWaitingForIfs()); + } + if (expectReplacement) { + for (int i = 0; i < 1000 && countTransaction(pendingQueue, replacementTransactionId) == 0; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(countTransaction(pendingQueue, replacementTransactionId) > 0); + ASSERT(contention->hasBlockedContention()); + // The DCF delayed case has already sent its RTS and the station's + // NAV covers the cancelled exchange. Let that reservation expire + // before starting the replacement; no replacement frame is + // eligible to cross the handoff during this interval. + if (cancelDuringIfs && !qos) + wait(SimTime(1, SIMTIME_MS)); + contention->grantAccess(); + for (int i = 0; i < 100000 && mgmt->hasPendingAssociation(station); i++) { + if (contention->hasBlockedContention()) + contention->grantAccess(); + wait(SimTime(1, SIMTIME_US)); + } + ASSERT(mgmt->responseStatuses.size() > oldCallbackCount); + ASSERT(!mgmt->hasPendingAssociation(station)); + ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::ASSOCIATED); + ASSERT(mgmt->hasCommittedAssociationId(station)); + ASSERT(mgmt->reserveAssociationId(station) == oldAssociationId); + for (size_t i = oldCallbackCount; i < mgmt->responseTransactionIds.size(); i++) + ASSERT(mgmt->responseTransactionIds[i] == replacementTransactionId); + ASSERT(std::find(signalListener.sentTransactionIds.begin(), signalListener.sentTransactionIds.end(), oldTransactionId) == signalListener.sentTransactionIds.end()); + ASSERT(std::find(signalListener.lowerTransactionIds.begin(), signalListener.lowerTransactionIds.end(), oldTransactionId) == signalListener.lowerTransactionIds.end()); + ASSERT(std::find(signalListener.sentTransactionIds.begin(), signalListener.sentTransactionIds.end(), replacementTransactionId) != signalListener.sentTransactionIds.end()); + } + else { + for (int i = 0; i < 100; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->responseStatuses.size() == oldCallbackCount); + ASSERT(!mgmt->hasCommittedAssociationId(station)); + ASSERT(std::find(signalListener.sentTransactionIds.begin(), signalListener.sentTransactionIds.end(), oldTransactionId) == signalListener.sentTransactionIds.end()); + ASSERT(std::find(signalListener.lowerTransactionIds.begin(), signalListener.lowerTransactionIds.end(), oldTransactionId) == signalListener.lowerTransactionIds.end()); + } + + sentSignalSource->unsubscribe(packetSentToPeerSignal, &signalListener); + mac->unsubscribe(packetSentToLowerSignal, &signalListener); + dropSignalSource->unsubscribe(packetDroppedSignal, &signalListener); + } + + public: + Ieee80211MgmtApCancellationTest() : cSimpleModule(65536) {} + + protected: + virtual void activity() override + { + runActiveCancellation("apDcf", false, MacAddress("02:00:00:00:00:09"), Supersession::AUTHENTICATION_RESTART, true); + runActiveCancellation("apDcf", false, MacAddress("02:00:00:00:00:09"), Supersession::AUTHENTICATION_RESTART, true, true); + runActiveCancellation("apHcf", true, MacAddress("02:00:00:00:00:0a"), Supersession::DEAUTHENTICATION, true); + runActiveCancellation("apHcf", true, MacAddress("02:00:00:00:00:0a"), Supersession::AUTHENTICATION_RESTART, true, true); + auto hcfMac = check_and_cast(getModuleByPath("^.apHcf.wlan[0].mac")); + hcfMac->armHold(); + runActiveCancellation("apHcf", true, MacAddress("02:00:00:00:00:0a"), Supersession::DISASSOCIATION, false); + std::cout << "AP DCF/HCF association transaction supersession cancellation and replacement completion verified.\n"; + } +}; + +Define_Module(Ieee80211MgmtApCancellationTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.Ieee80211Mac; +import inet.linklayer.ieee80211.mac.Tx; +import inet.linklayer.ieee80211.mac.contention.Contention; +import inet.linklayer.ieee80211.mgmt.Ieee80211MgmtAp; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple TestBlockedContention extends Contention +{ + parameters: + @class(::inet::ieee80211::TestBlockedContention); +} + +simple TestInspectableTx extends Tx +{ + parameters: + @class(::inet::ieee80211::TestInspectableTx); +} + +module TestHoldingIeee80211Mac extends Ieee80211Mac +{ + parameters: + @class(::inet::ieee80211::TestHoldingIeee80211Mac); +} + +simple TestIeee80211MgmtApCancellation extends Ieee80211MgmtAp +{ + parameters: + @class(::inet::ieee80211::TestIeee80211MgmtApCancellation); +} + +simple Ieee80211MgmtApCancellationTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211MgmtApCancellationTest); +} + +network Ieee80211MgmtApCancellationTestNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + apDcf: AccessPoint { + parameters: + wlan[*].mgmt.typename = "TestIeee80211MgmtApCancellation"; + wlan[*].mac.typename = "TestHoldingIeee80211Mac"; + wlan[*].mac.tx.typename = "TestInspectableTx"; + wlan[*].mac.dcf.channelAccess.contention.typename = "TestBlockedContention"; + } + staDcf: WirelessHost { + parameters: + wlan[*].mac.typename = "Ieee80211TesterMac"; + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + apHcf: AccessPoint { + parameters: + wlan[*].mgmt.typename = "TestIeee80211MgmtApCancellation"; + wlan[*].mac.typename = "TestHoldingIeee80211Mac"; + wlan[*].mac.tx.typename = "TestInspectableTx"; + wlan[*].mac.qosStation = true; + wlan[*].mac.hcf.edca.edcaf[*].contention.typename = "TestBlockedContention"; + } + staHcf: WirelessHost { + parameters: + wlan[*].mac.typename = "Ieee80211TesterMac"; + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + test: Ieee80211MgmtApCancellationTest; + connections allowunconnected: + // The wireless interfaces connect to the shared radio medium through + // their radioIn gates; no wired traffic is needed by this regression. +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211MgmtApCancellationTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 100ms +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +*.apDcf.wlan[0].mgmt.beaconInterval = 10s +*.apHcf.wlan[0].mgmt.beaconInterval = 10s +*.apDcf.wlan[0].mgmt.numAuthSteps = 2 +*.apHcf.wlan[0].mgmt.numAuthSteps = 2 +*.apDcf.wlan[0].address = "02:00:00:00:00:01" +*.apHcf.wlan[0].address = "02:00:00:00:00:02" +*.staDcf.wlan[0].address = "02:00:00:00:00:09" +*.staHcf.wlan[0].address = "02:00:00:00:00:0a" +*.staDcf.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:01" +*.staHcf.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:02" +*.staDcf.wlan[0].mac.actions = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +*.staHcf.wlan[0].mac.actions = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +*.apDcf.wlan[0].mac.dcf.rtsPolicy.rtsThreshold = 0B +*.apHcf.wlan[0].mac.hcf.rtsPolicy.rtsThreshold = 0B +*.apDcf.wlan[0].mac.dcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 32B +*.apHcf.wlan[0].mac.hcf.originatorMacDataService.fragmentationPolicy.fragmentationThreshold = 32B +*.apDcf.mobility.initialX = 0m +*.apDcf.mobility.initialY = 0m +*.staDcf.mobility.initialX = 1m +*.staDcf.mobility.initialY = 0m +*.apHcf.mobility.initialX = 0m +*.apHcf.mobility.initialY = 20m +*.staHcf.mobility.initialX = 1m +*.staHcf.mobility.initialY = 20m +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m +**.wlan[*].opMode = "g(mixed)" +**.wlan[*].bitrate = 11Mbps +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.centerFrequency = 2.4GHz +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].radio.receiver.sensitivity = -85dBm +**.wlan[*].radio.receiver.snirThreshold = 4dB + +%contains: stdout +AP DCF/HCF association transaction supersession cancellation and replacement completion verified. diff --git a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test index 62c1d4fc629..27f8d874cac 100644 --- a/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApHcfQueueDrop_1.test @@ -190,6 +190,64 @@ class Ieee80211MgmtApHcfQueueDropTest : public cSimpleModule ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::ASSOCIATED); ASSERT(mgmt->getCommittedAssociationId(station) == committedAid); + // An explicit single-packet removal follows the same terminal path as + // a queue drop: the AP must release both the pending transaction and + // its reserved AID before another station can reserve it. + const MacAddress explicitlyRemovedStation("02:00:00:00:00:0b"); + mgmt->markAuthenticated(explicitlyRemovedStation); + mgmt->submitAssociationRequest(explicitlyRemovedStation); + for (int i = 0; i < 100 && managementQueue->isEmpty(); i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->hasPendingAssociation(explicitlyRemovedStation)); + auto explicitlyRemovedPacket = managementQueue->findPacket([](const Packet *packet) { + return packet->findTag() != nullptr; + }); + ASSERT(explicitlyRemovedPacket != nullptr); + managementQueue->removePacket(explicitlyRemovedPacket); + take(explicitlyRemovedPacket); + delete explicitlyRemovedPacket; + for (int i = 0; i < 100 && mgmt->statuses.size() < 5; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->statuses.size() == 5); + ASSERT(mgmt->statuses.back() == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(!mgmt->hasPendingAssociation(explicitlyRemovedStation)); + ASSERT(mgmt->getCommittedAssociationId(explicitlyRemovedStation) == 0); + auto explicitlyRemovedAid = mgmt->reserveAssociationId(explicitlyRemovedStation); + ASSERT(explicitlyRemovedAid != 0); + mgmt->cancelAssociationIdReservation(explicitlyRemovedStation); + + // Compound removeAllPackets exercises explicit bulk departure for two + // independent association transactions and must clear both states. + const MacAddress bulkRemovedStation1("02:00:00:00:00:0c"); + const MacAddress bulkRemovedStation2("02:00:00:00:00:0d"); + mgmt->markAuthenticated(bulkRemovedStation1); + mgmt->markAuthenticated(bulkRemovedStation2); + mgmt->submitAssociationRequest(bulkRemovedStation1); + mgmt->submitAssociationRequest(bulkRemovedStation2); + for (int i = 0; i < 100 && managementQueue->getNumPackets() < 2; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->hasPendingAssociation(bulkRemovedStation1)); + ASSERT(mgmt->hasPendingAssociation(bulkRemovedStation2)); + // The compound boundary is the queue observed by HCF. Its + // removeAllPackets() implementation removes each child packet while + // suppressing the child's duplicate REMOVED notification, then + // reports the terminal departure once at the boundary. + compoundQueue->removeAllPackets(); + for (int i = 0; i < 100 && mgmt->statuses.size() < 7; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->statuses.size() == 7); + ASSERT(mgmt->statuses[5] == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(mgmt->statuses[6] == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(!mgmt->hasPendingAssociation(bulkRemovedStation1)); + ASSERT(!mgmt->hasPendingAssociation(bulkRemovedStation2)); + ASSERT(mgmt->getCommittedAssociationId(bulkRemovedStation1) == 0); + ASSERT(mgmt->getCommittedAssociationId(bulkRemovedStation2) == 0); + auto bulkAid1 = mgmt->reserveAssociationId(bulkRemovedStation1); + auto bulkAid2 = mgmt->reserveAssociationId(bulkRemovedStation2); + ASSERT(bulkAid1 != 0 && bulkAid2 != 0 && bulkAid1 != bulkAid2); + mgmt->cancelAssociationIdReservation(bulkRemovedStation1); + mgmt->cancelAssociationIdReservation(bulkRemovedStation2); + std::cout << "HCF compound pending-queue drop cleanup, AID preservation, and reassociation recovery verified.\n"; } }; diff --git a/tests/module/Ieee80211MgmtApQueueDrop_1.test b/tests/module/Ieee80211MgmtApQueueDrop_1.test index d0b0a4838e4..b78a85529ca 100644 --- a/tests/module/Ieee80211MgmtApQueueDrop_1.test +++ b/tests/module/Ieee80211MgmtApQueueDrop_1.test @@ -66,6 +66,11 @@ class TestIeee80211MgmtApQueueDrop : public Ieee80211MgmtAp short reserveAssociationId(const MacAddress& address) { return mib->reserveAssociationId(address); } void cancelAssociationIdReservation(const MacAddress& address) { mib->cancelAssociationIdReservation(address); } + short getCommittedAssociationId(const MacAddress& address) const + { + auto it = mib->bssAccessPointData.associationIds.find(address); + return it == mib->bssAccessPointData.associationIds.end() ? 0 : it->second; + } Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const { return mib->bssAccessPointData.stations.at(address); } protected: @@ -129,6 +134,59 @@ class Ieee80211MgmtApQueueDropTest : public cSimpleModule ASSERT(!mgmt->hasPendingAssociation(station)); ASSERT(mgmt->getStationStatus(station) == Ieee80211Mib::ASSOCIATED); + // Explicit single removal from the real DCF pending queue is a + // terminal management outcome and releases the AP-side reservation. + const MacAddress explicitlyRemovedStation("02:00:00:00:00:0b"); + mgmt->markAuthenticated(explicitlyRemovedStation); + mgmt->submitAssociationRequest(explicitlyRemovedStation); + for (int i = 0; i < 100 && queue->isEmpty(); i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->hasPendingAssociation(explicitlyRemovedStation)); + auto explicitlyRemovedPacket = queue->findPacket([](const Packet *packet) { + return packet->findTag() != nullptr; + }); + ASSERT(explicitlyRemovedPacket != nullptr); + queue->removePacket(explicitlyRemovedPacket); + take(explicitlyRemovedPacket); + delete explicitlyRemovedPacket; + for (int i = 0; i < 100 && mgmt->statuses.size() < 3; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->statuses.size() == 3); + ASSERT(mgmt->statuses.back() == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(!mgmt->hasPendingAssociation(explicitlyRemovedStation)); + ASSERT(mgmt->getCommittedAssociationId(explicitlyRemovedStation) == 0); + auto explicitlyRemovedAid = mgmt->reserveAssociationId(explicitlyRemovedStation); + ASSERT(explicitlyRemovedAid != 0); + mgmt->cancelAssociationIdReservation(explicitlyRemovedStation); + + // Bulk removal invokes the callback once per independent transaction; + // both pending states and both reservations must be released. + const MacAddress bulkRemovedStation1("02:00:00:00:00:0c"); + const MacAddress bulkRemovedStation2("02:00:00:00:00:0d"); + mgmt->markAuthenticated(bulkRemovedStation1); + mgmt->markAuthenticated(bulkRemovedStation2); + mgmt->submitAssociationRequest(bulkRemovedStation1); + mgmt->submitAssociationRequest(bulkRemovedStation2); + for (int i = 0; i < 100 && queue->getNumPackets() < 2; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->hasPendingAssociation(bulkRemovedStation1)); + ASSERT(mgmt->hasPendingAssociation(bulkRemovedStation2)); + queue->removeAllPackets(); + for (int i = 0; i < 100 && mgmt->statuses.size() < 5; i++) + wait(SimTime(1, SIMTIME_US)); + ASSERT(mgmt->statuses.size() == 5); + ASSERT(mgmt->statuses[3] == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(mgmt->statuses[4] == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + ASSERT(!mgmt->hasPendingAssociation(bulkRemovedStation1)); + ASSERT(!mgmt->hasPendingAssociation(bulkRemovedStation2)); + ASSERT(mgmt->getCommittedAssociationId(bulkRemovedStation1) == 0); + ASSERT(mgmt->getCommittedAssociationId(bulkRemovedStation2) == 0); + auto bulkAid1 = mgmt->reserveAssociationId(bulkRemovedStation1); + auto bulkAid2 = mgmt->reserveAssociationId(bulkRemovedStation2); + ASSERT(bulkAid1 != 0 && bulkAid2 != 0 && bulkAid1 != bulkAid2); + mgmt->cancelAssociationIdReservation(bulkRemovedStation1); + mgmt->cancelAssociationIdReservation(bulkRemovedStation2); + std::cout << "DCF pending-queue overflow terminal cleanup and retransmission recovery verified.\n"; } }; diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 72e7714ddf5..f2ea78f6c0d 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -39,6 +39,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" #include "inet/queueing/gate/PacketGate.h" #include "inet/queueing/gate/PeriodicGate.h" #include "inet/queueing/queue/CompoundPacketQueueBase.h" @@ -630,8 +631,9 @@ class TestHcf : public Hcf void processTransmittedData(Packet *packet, const Ptr& dataHeader, AccessCategory ac) { originatorProcessTransmittedDataFrame(packet, dataHeader, ac); } void processTransmittedManagement(Packet *packet, const Ptr& managementHeader, AccessCategory ac) { originatorProcessTransmittedManagementFrame(packet, managementHeader, ac); } void dropPacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DROPPED); } - void removePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::REMOVED); } + void removePacket(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason = queueing::IPacketQueue::PacketRemovalReason::REMOVED) { handlePacketRemoved(packet, reason); } void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } + bool cancelManagement(uint64_t transactionId, Packet *excludedPacket = nullptr) { return cancelManagementTransaction(transactionId, excludedPacket); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } void processReceivedFrame(Packet *packet) { recipientProcessReceivedFrame(packet, packet->peekAtFront()); } @@ -675,7 +677,13 @@ class TestMac : public Ieee80211Mac public: using Ieee80211Mac::receiveSignal; int numPendingRadioConfigSends = 0; + std::vector transmissionStatuses; + std::vector transmissionFrames; virtual void sendDownPendingRadioConfigMsg() override { numPendingRadioConfigSends++; } + virtual void receiveSignal(cComponent *, simsignal_t, cObject *object, cObject *details) override { + transmissionFrames.push_back(check_and_cast(object)); + transmissionStatuses.push_back(check_and_cast(details)->getStatus()); + } }; class TestEmptyFrameSequence : public IFrameSequence @@ -794,6 +802,7 @@ class TestFrameSequenceHandler : public IFrameSequenceHandler IFrameSequence *frameSequence = nullptr; FrameSequenceContext *context = nullptr; bool running = false; + bool cancellationRequested = false; int numStartedSequences = 0; virtual ~TestFrameSequenceHandler() { @@ -812,6 +821,7 @@ class TestFrameSequenceHandler : public IFrameSequenceHandler virtual void transmissionComplete() override {} virtual bool isSequenceRunning() override { return running; } virtual void handleStartRxTimeout() override {} + virtual void cancelFrameSequence() override { cancellationRequested = true; } }; class TestDcaf : public Dcaf @@ -819,7 +829,11 @@ class TestDcaf : public Dcaf public: int numReleasedChannels = 0; - void configure(InProgressFrames *inProgressFrames) { this->inProgressFrames = inProgressFrames; } + void configure(InProgressFrames *inProgressFrames, queueing::IPacketQueue *pendingQueue = nullptr) { + this->inProgressFrames = inProgressFrames; + this->pendingQueue = pendingQueue; + } + virtual queueing::IPacketQueue *getPendingQueue() const override { return pendingQueue; } virtual void releaseChannel(IChannelAccess::ICallback *) override { numReleasedChannels++; } }; @@ -830,15 +844,24 @@ class TestDcf : public Dcf for (auto listener : getLocalSignalListeners(Ieee80211Mac::frameTransmissionOutcomeSignal)) unsubscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, listener); } - TestFrameSequenceHandler *configure(TestDcaf *channelAccess, Ieee80211Mac *mac) { + TestFrameSequenceHandler *configure(TestDcaf *channelAccess, Ieee80211Mac *mac, AckHandler *ackHandler = nullptr) { this->channelAccess = channelAccess; this->mac = mac; subscribe(Ieee80211Mac::frameTransmissionOutcomeSignal, check_and_cast(mac)); + this->ackHandler = ackHandler; auto handler = new TestFrameSequenceHandler(); frameSequenceHandler = handler; return handler; } void grant(TestDcaf *channelAccess) { channelGranted(channelAccess); } + bool cancelManagement(uint64_t transactionId, Packet *excludedPacket) { return cancelManagementTransaction(transactionId, excludedPacket); } + void notifyRemoved(Packet *packet, queueing::IPacketQueue::PacketRemovalReason reason) { handlePacketRemoved(packet, reason); } + void configureFailurePath(NonQosRecoveryProcedure *recoveryProcedure) { + this->recoveryProcedure = recoveryProcedure; + this->stationRetryCounters = new StationRetryCounters(); + } + void processFailedFrame(Packet *packet) { originatorProcessFailedFrame(packet); } + void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } protected: virtual FrameSequenceContext *buildContext() override { @@ -870,6 +893,22 @@ static Packet *makeTaggedDelbaPacket(const char *name, MacAddress receiverAddres return packet; } +static Packet *makeTaggedManagementPacket(const char *name, Ieee80211FrameType type, MacAddress transmitterAddress, MacAddress receiverAddress, + SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint64_t transactionId, bool retry = false) +{ + auto header = makeShared(); + header->setType(type); + header->setTransmitterAddress(transmitterAddress); + header->setReceiverAddress(receiverAddress); + header->setSequenceNumber(sequenceNumber); + header->setFragmentNumber(fragmentNumber); + header->setMoreFragments(moreFragments); + header->setRetry(retry); + auto packet = new Packet(name, header); + packet->addTag()->setTransactionId(transactionId); + return packet; +} + static Ptr makeResponse(MacAddress transmitterAddress, Tid tid, uint8_t dialogToken, uint16_t statusCode) { auto response = makeShared(); @@ -1372,6 +1411,85 @@ ASSERT(handler.isAddbaResponsePending(peer3, 5)); delete frame; } +// Management transaction cancellation keeps active direct and RTS-protected +// frames alive until the sequence retires them, while making both frames +// ineligible immediately and suppressing terminal callbacks/retries. +for (auto coordination : { 0, 1 }) { + const uint64_t transactionId = 902 + coordination; + TestPacketQueue pendingQueue; + TestInProgressFrames inProgressFrames; + AckHandler dcfAckHandler; + TestQosAckHandler hcfAckHandler; + inProgressFrames.configure(nullptr, &dcfAckHandler, &pendingQueue); + auto directPacket = makeTaggedManagementPacket("activeManagementDirect", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(32 + coordination), 0, true, transactionId); + auto protectedPacket = makeTaggedManagementPacket("activeManagementRtsProtected", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(32 + coordination), 1, false, transactionId); + inProgressFrames.addOwnedFrame(directPacket); + inProgressFrames.addOwnedFrame(protectedPacket); + + TestFrameSequenceHandler *frameSequenceHandler; + TestMac mac; + if (coordination == 0) { + TestDcaf channelAccess; + channelAccess.configure(&inProgressFrames, &pendingQueue); + TestDcf dcf; + frameSequenceHandler = dcf.configure(&channelAccess, &mac, &dcfAckHandler); + frameSequenceHandler->running = true; + frameSequenceHandler->context = new FrameSequenceContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &inProgressFrames, nullptr, nullptr, new NonQoSContext(nullptr), nullptr); + frameSequenceHandler->context->addStep(new TransmitStep(directPacket, 0)); + frameSequenceHandler->context->addStep(new RtsTransmitStep(protectedPacket, new Packet("activeManagementRts", makeShared()), 0)); + TestSignalListener dropListener; + dcf.subscribe(packetDroppedSignal, &dropListener); + ASSERT(dcf.cancelManagement(transactionId, nullptr)); + ASSERT(frameSequenceHandler->cancellationRequested); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 2); + ASSERT(dropListener.numSignals == 2); + ASSERT(mac.transmissionStatuses.empty()); + dcf.processFailedFrame(directPacket); + dcf.processRtsProtectionFailure(protectedPacket); + ASSERT(!directPacket->peekAtFront()->getRetry()); + ASSERT(!protectedPacket->peekAtFront()->getRetry()); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 2); + for (auto frame : releasedFrames) + delete frame; + } + else { + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + inProgressFrames.configure(nullptr, &hcfAckHandler, &pendingQueue); + edcaf.qosAckHandler = &hcfAckHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestHcf hcf; + hcf.configure(&edca); + hcf.configureMac(&mac); + frameSequenceHandler = new TestFrameSequenceHandler(); + frameSequenceHandler->running = true; + frameSequenceHandler->context = new FrameSequenceContext(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &inProgressFrames, nullptr, nullptr, nullptr, nullptr); + frameSequenceHandler->context->addStep(new TransmitStep(directPacket, 0)); + frameSequenceHandler->context->addStep(new RtsTransmitStep(protectedPacket, new Packet("activeManagementRts", makeShared()), 0)); + hcf.configureFrameSequenceHandler(frameSequenceHandler); + TestSignalListener dropListener; + hcf.subscribe(packetDroppedSignal, &dropListener); + ASSERT(hcf.cancelManagement(transactionId, nullptr)); + ASSERT(frameSequenceHandler->cancellationRequested); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 2); + ASSERT(dropListener.numSignals == 2); + ASSERT(mac.transmissionStatuses.empty()); + hcf.processFailedFrame(directPacket); + hcf.processRtsProtectionFailure(protectedPacket); + ASSERT(!directPacket->peekAtFront()->getRetry()); + ASSERT(!protectedPacket->peekAtFront()->getRetry()); + auto releasedFrames = inProgressFrames.releaseFrames(); + ASSERT(releasedFrames.size() == 2); + for (auto frame : releasedFrames) + delete frame; + } +} + // A transaction frame that is not referenced by an active sequence is removed // from in-progress ownership and reclaimed immediately, without entering the @@ -4317,6 +4435,331 @@ delete deferredFrames.at(0); delete dcfFrames.at(0); } +// A terminal management-frame failure cancels all fragmented siblings in DCF, +// including frames held by a different queue stage, while leaving the current +// borrowed frame and an unrelated transaction untouched. +for (auto responseType : { ST_ASSOCIATIONRESPONSE, ST_REASSOCIATIONRESPONSE }) { + TestOriginatorQosMacDataService managementDcfDataService; + TestPacketQueue managementDcfPendingQueue; + AckHandler managementDcfAckHandler; + TestInProgressFrames managementDcfInProgressFrames; + managementDcfInProgressFrames.configure(&managementDcfDataService, &managementDcfAckHandler, &managementDcfPendingQueue); + TestDcaf managementDcaf; + managementDcaf.configure(&managementDcfInProgressFrames, &managementDcfPendingQueue); + TestMac managementDcfMac; + TestDcf managementDcf; + managementDcf.configure(&managementDcaf, &managementDcfMac, &managementDcfAckHandler); + auto currentFrame = makeTaggedManagementPacket("managementDcfCurrent", responseType, peer1, peer2, SequenceNumberCyclic(301 + responseType), 0, true, 500 + responseType); + auto inProgressSibling = makeTaggedManagementPacket("managementDcfInProgressSibling", responseType, peer1, peer2, SequenceNumberCyclic(301 + responseType), 1, false, 500 + responseType); + auto pendingSibling = makeTaggedManagementPacket("managementDcfPendingSibling", responseType, peer1, peer2, SequenceNumberCyclic(301 + responseType), 2, false, 500 + responseType); + auto unrelatedFrame = makeTaggedManagementPacket("managementDcfUnrelated", responseType, peer1, peer2, SequenceNumberCyclic(302 + responseType), 0, false, 700 + responseType); + managementDcfInProgressFrames.addOwnedFrame(currentFrame); + managementDcfInProgressFrames.addOwnedFrame(inProgressSibling); + managementDcfPendingQueue.packets = { pendingSibling, unrelatedFrame }; + ASSERT(managementDcf.cancelManagement(500 + responseType, currentFrame)); + ASSERT(managementDcfInProgressFrames.getLength() == 1); + ASSERT(managementDcfInProgressFrames.getFrames(0) == currentFrame); + ASSERT(managementDcfPendingQueue.getNumPackets() == 1); + ASSERT(managementDcfPendingQueue.getPacket(0) == unrelatedFrame); + ASSERT(managementDcfMac.transmissionStatuses.empty()); + auto remainingDcfFrames = managementDcfInProgressFrames.releaseFrames(); + ASSERT(remainingDcfFrames.size() == 1); + delete remainingDcfFrames.front(); + managementDcfPendingQueue.removePacket(unrelatedFrame); + delete unrelatedFrame; +} + +// HCF cancellation spans every EDCA, but does not cross transaction identity +// boundaries. The active sequence's current frame is deliberately excluded. +{ + std::array managementHcfPendingQueues; + std::array managementHcfAckHandlers; + std::array managementHcfDataServices; + std::array managementHcfInProgressFrames; + std::array managementHcfEdcafs; + TestEdca managementHcfEdca; + managementHcfEdca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + managementHcfInProgressFrames[ac].configure(&managementHcfDataServices[ac], &managementHcfAckHandlers[ac], &managementHcfPendingQueues[ac]); + managementHcfEdcafs[ac].pendingQueue = &managementHcfPendingQueues[ac]; + managementHcfEdcafs[ac].inProgressFrames = &managementHcfInProgressFrames[ac]; + managementHcfEdcafs[ac].qosAckHandler = &managementHcfAckHandlers[ac]; + managementHcfEdcafs[ac].setAccessCategory(AccessCategory(ac)); + managementHcfEdca.edcafs[ac] = &managementHcfEdcafs[ac]; + } + managementHcfEdca.edcaf = &managementHcfEdcafs[AC_BE]; + TestMac managementHcfMac; + TestHcf managementHcf; + managementHcf.configure(&managementHcfEdca); + managementHcf.configureMac(&managementHcfMac); + auto currentFrame = makeTaggedManagementPacket("managementHcfCurrent", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(401), 0, true, 800); + auto crossAcSibling = makeTaggedManagementPacket("managementHcfCrossAcSibling", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(401), 1, false, 800); + auto pendingSibling = makeTaggedManagementPacket("managementHcfPendingSibling", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(401), 2, false, 800); + auto unrelatedFrame = makeTaggedManagementPacket("managementHcfUnrelated", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(402), 0, false, 801); + managementHcfInProgressFrames[AC_BE].addOwnedFrame(currentFrame); + managementHcfInProgressFrames[AC_BK].addOwnedFrame(crossAcSibling); + managementHcfPendingQueues[AC_VO].packets = { pendingSibling }; + managementHcfPendingQueues[AC_BK].packets = { unrelatedFrame }; + ASSERT(managementHcf.cancelManagement(800, currentFrame)); + ASSERT(managementHcfInProgressFrames[AC_BE].getLength() == 1); + ASSERT(managementHcfInProgressFrames[AC_BE].getFrames(0) == currentFrame); + ASSERT(managementHcfInProgressFrames[AC_BK].getLength() == 0); + ASSERT(managementHcfPendingQueues[AC_VO].isEmpty()); + ASSERT(managementHcfPendingQueues[AC_BK].getNumPackets() == 1); + ASSERT(managementHcfPendingQueues[AC_BK].getPacket(0) == unrelatedFrame); + auto remainingHcfFrames = managementHcfInProgressFrames[AC_BE].releaseFrames(); + ASSERT(remainingHcfFrames.size() == 1); + delete remainingHcfFrames.front(); + managementHcfPendingQueues[AC_BK].removePacket(unrelatedFrame); + delete unrelatedFrame; + + auto reassociationCurrentFrame = makeTaggedManagementPacket("managementHcfReassociationCurrent", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(403), 0, true, 802); + auto reassociationInProgressSibling = makeTaggedManagementPacket("managementHcfReassociationInProgressSibling", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(403), 1, false, 802); + auto reassociationPendingSibling = makeTaggedManagementPacket("managementHcfReassociationPendingSibling", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(403), 2, false, 802); + managementHcfInProgressFrames[AC_BE].addOwnedFrame(reassociationCurrentFrame); + managementHcfInProgressFrames[AC_VI].addOwnedFrame(reassociationInProgressSibling); + managementHcfPendingQueues[AC_BK].packets = { reassociationPendingSibling }; + ASSERT(managementHcf.cancelManagement(802, reassociationCurrentFrame)); + ASSERT(managementHcfInProgressFrames[AC_BE].getLength() == 1); + ASSERT(managementHcfInProgressFrames[AC_BE].getFrames(0) == reassociationCurrentFrame); + ASSERT(managementHcfInProgressFrames[AC_VI].getLength() == 0); + ASSERT(managementHcfPendingQueues[AC_BK].isEmpty()); + auto remainingReassociationFrames = managementHcfInProgressFrames[AC_BE].releaseFrames(); + ASSERT(remainingReassociationFrames.size() == 1); + delete remainingReassociationFrames.front(); +} + +// DROPPED and explicit REMOVED management departures report one terminal +// callback, including a same-token bulk removal; DEQUEUED remains a normal +// hand-off into transmission processing. +{ + TestOriginatorQosMacDataService dcfRemovalDataService; + TestPacketQueue dcfRemovalQueue; + AckHandler dcfRemovalAckHandler; + TestInProgressFrames dcfRemovalInProgressFrames; + dcfRemovalInProgressFrames.configure(&dcfRemovalDataService, &dcfRemovalAckHandler, &dcfRemovalQueue); + TestDcaf dcfRemovalDcaf; + dcfRemovalDcaf.configure(&dcfRemovalInProgressFrames, &dcfRemovalQueue); + TestMac dcfRemovalMac; + TestDcf dcfRemoval; + dcfRemoval.configure(&dcfRemovalDcaf, &dcfRemovalMac, &dcfRemovalAckHandler); + dcfRemovalQueue.subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dcfRemoval); + auto droppedPacket = makeTaggedManagementPacket("dcfDroppedManagement", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(410), 0, false, 810); + dcfRemovalQueue.packets = { droppedPacket }; + dcfRemovalQueue.dropPacketFromQueue(droppedPacket); + delete droppedPacket; + ASSERT(dcfRemovalMac.transmissionStatuses.size() == 1); + ASSERT(dcfRemovalMac.transmissionStatuses.back() == FRAME_TRANSMISSION_STATUS_DROPPED_BEFORE_TRANSMISSION); + auto removedPacket = makeTaggedManagementPacket("dcfRemovedManagement", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(411), 0, false, 811); + dcfRemovalQueue.packets = { removedPacket }; + dcfRemovalQueue.removePacket(removedPacket); + delete removedPacket; + ASSERT(dcfRemovalMac.transmissionStatuses.size() == 2); + auto bulkFirst = makeTaggedManagementPacket("dcfBulkManagementFirst", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(412), 0, true, 812); + auto bulkSecond = makeTaggedManagementPacket("dcfBulkManagementSecond", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(412), 1, false, 812); + auto dequeuedPacket = makeTaggedManagementPacket("dcfDequeuedManagement", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(413), 0, false, 813); + dcfRemovalQueue.packets = { bulkFirst, bulkSecond }; + dcfRemovalQueue.removeAllPackets(); + ASSERT(dcfRemovalMac.transmissionStatuses.size() == 3); + delete bulkFirst; + delete bulkSecond; + dcfRemovalQueue.packets = { dequeuedPacket }; + ASSERT(dcfRemovalQueue.dequeuePacket() == dequeuedPacket); + delete dequeuedPacket; + ASSERT(dcfRemovalMac.transmissionStatuses.size() == 3); + dcfRemovalQueue.unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dcfRemoval); +} + +{ + std::array hcfRemovalQueues; + std::array hcfRemovalAckHandlers; + std::array hcfRemovalDataServices; + std::array hcfRemovalInProgressFrames; + std::array hcfRemovalEdcafs; + TestEdca hcfRemovalEdca; + hcfRemovalEdca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + hcfRemovalInProgressFrames[ac].configure(&hcfRemovalDataServices[ac], &hcfRemovalAckHandlers[ac], &hcfRemovalQueues[ac]); + hcfRemovalEdcafs[ac].pendingQueue = &hcfRemovalQueues[ac]; + hcfRemovalEdcafs[ac].inProgressFrames = &hcfRemovalInProgressFrames[ac]; + hcfRemovalEdcafs[ac].qosAckHandler = &hcfRemovalAckHandlers[ac]; + hcfRemovalEdcafs[ac].setAccessCategory(AccessCategory(ac)); + hcfRemovalEdca.edcafs[ac] = &hcfRemovalEdcafs[ac]; + } + hcfRemovalEdca.edcaf = &hcfRemovalEdcafs[AC_BE]; + TestMac hcfRemovalMac; + TestHcf hcfRemoval; + hcfRemoval.configure(&hcfRemovalEdca); + hcfRemoval.configureMac(&hcfRemovalMac); + hcfRemovalQueues[AC_VO].subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcfRemoval); + auto removedPacket = makeTaggedManagementPacket("hcfRemovedManagement", ST_REASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(420), 0, false, 820); + hcfRemovalQueues[AC_VO].packets = { removedPacket }; + hcfRemovalQueues[AC_VO].removePacket(removedPacket); + delete removedPacket; + ASSERT(hcfRemovalMac.transmissionStatuses.size() == 1); + auto bulkFirst = makeTaggedManagementPacket("hcfBulkManagementFirst", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(421), 0, true, 821); + auto bulkSecond = makeTaggedManagementPacket("hcfBulkManagementSecond", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(421), 1, false, 821); + auto untaggedPacket = new Packet("hcfBulkUntaggedManagement", makeShared()); + hcfRemovalQueues[AC_VO].packets = { bulkFirst, bulkSecond, untaggedPacket }; + hcfRemovalQueues[AC_VO].removeAllPackets(); + ASSERT(hcfRemovalMac.transmissionStatuses.size() == 2); + delete bulkFirst; + delete bulkSecond; + delete untaggedPacket; + auto dequeuedPacket = makeTaggedManagementPacket("hcfDequeuedManagement", ST_ASSOCIATIONRESPONSE, peer1, peer2, SequenceNumberCyclic(422), 0, false, 822); + hcfRemovalQueues[AC_VO].packets = { dequeuedPacket }; + ASSERT(hcfRemovalQueues[AC_VO].dequeuePacket() == dequeuedPacket); + delete dequeuedPacket; + ASSERT(hcfRemovalMac.transmissionStatuses.size() == 2); + hcfRemovalQueues[AC_VO].unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcfRemoval); +} + +// A retry-exhausted fragmented association/reassociation response cancels its +// siblings through the real DCF failure callback path. The current failed +// frame is retired by DCF, while unrelated transaction state survives. +for (int responseIndex = 0; responseIndex < 2; responseIndex++) { + auto responseType = responseIndex == 0 ? ST_ASSOCIATIONRESPONSE : ST_REASSOCIATIONRESPONSE; + constexpr int firstSequenceNumber = 440; + uint64_t transactionId = 830 + responseIndex; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 1, &cwCalculator); + AckHandler ackHandler; + TestPacketQueue pendingQueue; + TestOriginatorQosMacDataService dataService; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestDcaf dcaf; + dcaf.configure(&inProgressFrames, &pendingQueue); + TestMac mac; + TestDcf dcf; + dcf.configure(&dcaf, &mac, &ackHandler); + dcf.configureFailurePath(&recoveryProcedure); + pendingQueue.subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dcf); + + auto current = makeTaggedManagementPacket("dcfFailureCurrent", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 0, true, transactionId); + auto inProgressSibling = makeTaggedManagementPacket("dcfFailureInProgressSibling", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 1, false, transactionId); + auto pendingSibling = makeTaggedManagementPacket("dcfFailurePendingSibling", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 2, false, transactionId); + auto unrelated = makeTaggedManagementPacket("dcfFailureUnrelated", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex + 1), 0, false, transactionId + 100); + auto currentHeader = current->peekAtFront(); + auto inProgressSiblingHeader = inProgressSibling->peekAtFront(); + inProgressFrames.addOwnedFrame(current); + inProgressFrames.addOwnedFrame(inProgressSibling); + pendingQueue.packets = { pendingSibling, unrelated }; + ackHandler.frameGotInProgress(currentHeader); + ackHandler.frameGotInProgress(inProgressSiblingHeader); + ackHandler.processTransmittedDataOrMgmtFrame(currentHeader); + ackHandler.processTransmittedDataOrMgmtFrame(inProgressSiblingHeader); + recoveryProcedure.addShortRetry(inProgressSiblingHeader, 2); + + dcf.processFailedFrame(current); + + ASSERT(mac.transmissionStatuses.size() == 1); + ASSERT(mac.transmissionStatuses.front() == FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + ASSERT(mac.transmissionFrames.front() == current); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(inProgressFrames.getNumDroppedFrames() == 1); + ASSERT(pendingQueue.getNumPackets() == 1); + ASSERT(pendingQueue.getPacket(0) == unrelated); + ASSERT(ackHandler.getAckStatus(currentHeader) == AckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(ackHandler.getAckStatus(inProgressSiblingHeader) == AckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(!recoveryProcedure.hasRetry(currentHeader)); + ASSERT(!recoveryProcedure.hasRetry(inProgressSiblingHeader)); + auto retiredFrames = inProgressFrames.releaseFrames(); + ASSERT(retiredFrames == std::vector({ current })); + delete current; + pendingQueue.packets.clear(); + delete unrelated; + pendingQueue.unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &dcf); +} + +// The same retry-exhaustion path in HCF spans EDCA queues, clears each +// sibling's QoS ACK/retry bookkeeping, and emits one terminal callback for +// both association response variants. +for (int responseIndex = 0; responseIndex < 2; responseIndex++) { + auto responseType = responseIndex == 0 ? ST_ASSOCIATIONRESPONSE : ST_REASSOCIATIONRESPONSE; + constexpr int firstSequenceNumber = 450; + uint64_t transactionId = 840 + responseIndex; + std::array pendingQueues; + std::array ackHandlers; + std::array dataServices; + std::array inProgressFrames; + std::array edcafs; + TestEdca edca; + edca.numEdcafs = AC_NUMCATEGORIES; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) { + inProgressFrames[ac].configure(&dataServices[ac], &ackHandlers[ac], &pendingQueues[ac]); + edcafs[ac].pendingQueue = &pendingQueues[ac]; + edcafs[ac].inProgressFrames = &inProgressFrames[ac]; + edcafs[ac].qosAckHandler = &ackHandlers[ac]; + edcafs[ac].setAccessCategory(AccessCategory(ac)); + edca.edcafs[ac] = &edcafs[ac]; + } + edca.edcaf = &edcafs[AC_BE]; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 1, &cwCalculator); + edca.nonQosRecoveryProcedure = &recoveryProcedure; + OriginatorQosAckPolicy ackPolicy; + TestMac mac; + TestHcf hcf; + hcf.configure(&edca); + hcf.configureMac(&mac); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + pendingQueues[ac].subscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcf); + + auto current = makeTaggedManagementPacket("hcfFailureCurrent", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 0, true, transactionId); + auto inProgressSibling = makeTaggedManagementPacket("hcfFailureInProgressSibling", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 1, false, transactionId); + auto pendingSibling = makeTaggedManagementPacket("hcfFailurePendingSibling", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex), 2, false, transactionId); + auto unrelated = makeTaggedManagementPacket("hcfFailureUnrelated", responseType, peer1, peer2, + SequenceNumberCyclic(firstSequenceNumber + responseIndex + 1), 0, false, transactionId + 100); + auto currentHeader = current->peekAtFront(); + auto inProgressSiblingHeader = inProgressSibling->peekAtFront(); + inProgressFrames[AC_BE].addOwnedFrame(current); + inProgressFrames[AC_VI].addOwnedFrame(inProgressSibling); + pendingQueues[AC_VO].packets = { pendingSibling }; + pendingQueues[AC_BK].packets = { unrelated }; + ackHandlers[AC_BE].frameGotInProgress(currentHeader); + ackHandlers[AC_VI].frameGotInProgress(inProgressSiblingHeader); + hcf.processTransmittedManagement(current, currentHeader, AC_BE); + hcf.processTransmittedManagement(inProgressSibling, inProgressSiblingHeader, AC_VI); + ASSERT(ackHandlers[AC_BE].getMgmtOrNonQoSAckStatus(currentHeader) == QosAckHandler::Status::WAITING_FOR_NORMAL_ACK); + ASSERT(ackHandlers[AC_VI].getMgmtOrNonQoSAckStatus(inProgressSiblingHeader) == QosAckHandler::Status::WAITING_FOR_NORMAL_ACK); + recoveryProcedure.addShortRetry(inProgressSiblingHeader, 2); + + hcf.processFailedFrame(current); + + ASSERT(mac.transmissionStatuses.size() == 1); + ASSERT(mac.transmissionStatuses.front() == FRAME_TRANSMISSION_STATUS_RETRY_LIMIT_REACHED); + ASSERT(mac.transmissionFrames.front() == current); + ASSERT(inProgressFrames[AC_BE].getLength() == 0); + ASSERT(inProgressFrames[AC_BE].getNumDroppedFrames() == 1); + ASSERT(inProgressFrames[AC_VI].getLength() == 0); + ASSERT(pendingQueues[AC_VO].isEmpty()); + ASSERT(pendingQueues[AC_BK].getNumPackets() == 1); + ASSERT(pendingQueues[AC_BK].getPacket(0) == unrelated); + ASSERT(ackHandlers[AC_BE].getMgmtOrNonQoSAckStatus(currentHeader) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(ackHandlers[AC_VI].getMgmtOrNonQoSAckStatus(inProgressSiblingHeader) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + ASSERT(!recoveryProcedure.hasRetry(currentHeader)); + ASSERT(!recoveryProcedure.hasRetry(inProgressSiblingHeader)); + auto retiredFrames = inProgressFrames[AC_BE].releaseFrames(); + ASSERT(retiredFrames == std::vector({ current })); + delete current; + pendingQueues[AC_VO].packets.clear(); + pendingQueues[AC_BK].packets.clear(); + delete unrelated; + for (int ac = 0; ac < AC_NUMCATEGORIES; ac++) + pendingQueues[ac].unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcf); +} + EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; %contains: stdout From 103cd706109de629a7ad52a43c373fbce438b7e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:03 +0200 Subject: [PATCH 33/56] ieee80211: protect Block Ack teardown generations Tag each locally generated DELBA with its agreement role, peer, TID, and generation. Route acknowledgement, retry exhaustion, cancellation, and abort handling back to that exact agreement instance. Track pending teardowns by generation and cancel only the matching transaction. A delayed or retried DELBA from an older agreement can no longer delete a replacement agreement that reused the same peer and TID. Cover originator and recipient teardown, replacement during an in-flight DELBA, retry and abort paths, and stale completion callbacks. --- WHATSNEW | 25 +- .../Ieee80211BlockAckAgreementTag.msg | 17 + .../OriginatorBlockAckAgreementHandler.cc | 69 +- .../OriginatorBlockAckAgreementHandler.h | 2 + .../blockack/RecipientBlockAckAgreement.cc | 6 +- .../mac/blockack/RecipientBlockAckAgreement.h | 4 +- .../RecipientBlockAckAgreementHandler.cc | 110 +++- .../RecipientBlockAckAgreementHandler.h | 11 +- .../IBlockAckAgreementHandlerCallback.h | 6 + .../IRecipientBlockAckAgreementHandler.h | 9 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 135 +++- .../ieee80211/mac/coordinationfunction/Hcf.h | 3 + tests/unit/Ieee80211AddbaTransaction_1.test | 597 +++++++++++++++++- 13 files changed, 917 insertions(+), 77 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag.msg diff --git a/WHATSNEW b/WHATSNEW index 5d5237316f4..1c6b2a2ade0 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -73,20 +73,27 @@ Notable backward incompatible changes are the following: now returns a typed outcome containing the established agreement and, when local policy vetoes a successful response, its immediately terminated local agreement and a best-effort initiator DELBA for Hcf to enqueue after emitting - the Added and Deleted signals. Such DELBAs carry the ADDBA transaction identity - and ordinary data continues with Normal Ack. They remain eligible through the + the Added and Deleted signals. Such locally generated DELBAs carry + sender-local agreement-generation metadata (the originator reuses its ADDBA + transaction identity), and ordinary data continues with Normal Ack. They remain + eligible through the final fragment; aborting one fragment cancels its siblings. After retry backoff, a replacement ADDBA setup invalidates an older queued DELBA, so an unreported disposal cannot suppress setup indefinitely and a delayed frame cannot terminate a newer peer/TID agreement. The replacement setup reports that obsolete identity so Hcf also removes all of its queued or in-progress packets. - Originator DELBA transmission handlers now receive the full Packet so this - identity is retained. A transaction-tagged initiator DELBA remains eligible - across MAC retries and is retired only when its final fragment is acknowledged - or the transaction is terminally aborted. Custom handler implementations and - callers must adopt processAcknowledgedDelba() and the boolean - processAbortedDelba() outcome; processTransmittedDelba() still returns the - agreement removed by an untagged DELBA, or null when none was removed. + Originator and recipient DELBA transmission handlers now receive the full + Packet so sender-local agreement-generation metadata is retained through + fragmentation and MAC retries. This changes + IRecipientBlockAckAgreementHandler::processTransmittedDelba() from accepting + only a DELBA header to accepting Packet*; custom handler implementations and + callers must migrate to the Packet form. A locally tagged DELBA carrying + agreement-generation metadata remains eligible across MAC retries and is + retired only when its final fragment is acknowledged or the teardown is + terminally aborted. Custom handler + implementations and callers must adopt processAcknowledgedDelba() and the + boolean processAbortedDelba() outcome; processTransmittedDelba() still returns + the agreement removed by an untagged DELBA, or null when none was removed. OriginatorBlockAckAgreementPolicy now exposes the new `addbaResponseTimeout` and `addbaRetryBackoff` NED parameters. C++ implementations of diff --git a/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag.msg b/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag.msg new file mode 100644 index 00000000000..01a57c8aa59 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag.msg @@ -0,0 +1,17 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +import inet.common.INETDefs; +import inet.common.TagBase; + +namespace inet::ieee80211; + +// Identifies one local Block Ack agreement generation across packet transformations. +// This metadata is not transmitted as part of the DELBA frame. +class Ieee80211BlockAckAgreementTag extends TagBase +{ + uint64_t generationId; +} diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 2094435bf43..0f84c21a671 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -11,6 +11,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" @@ -111,6 +112,7 @@ void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCall Tid tid = id.first.second; const auto& delba = buildDelba(receiverAddr, tid, 39); auto delbaPacket = new Packet("Delba", delba); + delbaPacket->addTag()->setGenerationId(agreement->getTransactionId()); procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes } } @@ -307,11 +309,16 @@ bool OriginatorBlockAckAgreementHandler::isDelbaPending(const Packet *packet, co { if (!delba->getInitiator()) return true; - auto transactionTag = packet->findTag(); - if (transactionTag == nullptr) - return true; - auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); - return it != pendingTeardownTransactionIds.end() && it->second == transactionTag->getTransactionId(); + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { + auto generationId = agreementTag->getGenerationId(); + auto agreementIt = blockAckAgreements.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (agreementIt != blockAckAgreements.end()) + return agreementIt->second->getTransactionId() == generationId; + auto teardownIt = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + return teardownIt != pendingTeardownTransactionIds.end() && teardownIt->second == generationId; + } + return true; } void OriginatorBlockAckAgreementHandler::processTransmittedAddbaReq(Packet *packet, const Ptr& addbaReq, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) @@ -344,18 +351,23 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: // likewise cannot tear down the agreement before its final fragment. if (delba->getMoreFragments()) return nullptr; - auto transactionTag = packet->findTag(); - if (transactionTag != nullptr) { - auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); - if (it == pendingTeardownTransactionIds.end() || it->second != transactionTag->getTransactionId()) + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { + auto generationId = agreementTag->getGenerationId(); + auto teardownIt = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (teardownIt != pendingTeardownTransactionIds.end() && teardownIt->second == generationId) return nullptr; - // IEEE Std 802.11-2024, 11.5.3.2: teardown is performed by - // transmitting DELBA. IEEE Std 802.11-2024, 10.23.2.12.1 and - // 10.3.4.4 require unsuccessful MMPDU attempts to be retried until - // success or the applicable retry limit. Keep the local transaction - // live across ordinary MAC retries and retire it only after the final - // fragment is acknowledged or the frame is terminally aborted. - return nullptr; + auto agreement = getAgreement(delba->getReceiverAddress(), delba->getTid()); + if (agreement == nullptr || agreement->getTransactionId() != generationId) + return nullptr; + bool cancelPendingTransaction = agreement->isPending(); + std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + pendingTeardownTransactionIds[std::make_pair(delba->getReceiverAddress(), delba->getTid())] = generationId; + scheduleAddbaResponseTimer(callback); + if (cancelPendingTransaction) + callback->cancelAddbaTransaction(generationId, nullptr); + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), generationId, packet); + return terminatedAgreement; } auto agreement = getAgreement(delba->getReceiverAddress(), delba->getTid()); bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); @@ -372,15 +384,15 @@ bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator() || delba->getMoreFragments()) return false; - auto transactionTag = packet->findTag(); - if (transactionTag == nullptr) + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) return false; auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); - if (it == pendingTeardownTransactionIds.end() || it->second != transactionTag->getTransactionId()) + if (it == pendingTeardownTransactionIds.end() || it->second != agreementTag->getGenerationId()) return false; auto transactionId = it->second; pendingTeardownTransactionIds.erase(it); - callback->cancelAddbaTransaction(transactionId, packet); + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); return true; } @@ -389,13 +401,13 @@ bool OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBl auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator()) return false; - auto transactionTag = packet->findTag(); - if (transactionTag != nullptr) { + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); - if (it != pendingTeardownTransactionIds.end() && it->second == transactionTag->getTransactionId()) { + if (it != pendingTeardownTransactionIds.end() && it->second == agreementTag->getGenerationId()) { auto transactionId = it->second; pendingTeardownTransactionIds.erase(it); - callback->cancelAddbaTransaction(transactionId, packet); + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); return true; } } @@ -408,10 +420,19 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: auto agreement = getAgreement(delba->getTransmitterAddress(), delba->getTid()); bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; + auto agreementId = std::make_pair(delba->getTransmitterAddress(), delba->getTid()); + auto pendingTeardownIt = pendingTeardownTransactionIds.find(agreementId); + auto pendingTeardownTransactionId = pendingTeardownIt == pendingTeardownTransactionIds.end() ? 0 : pendingTeardownIt->second; + if (pendingTeardownIt != pendingTeardownTransactionIds.end()) + pendingTeardownTransactionIds.erase(pendingTeardownIt); std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) callback->cancelAddbaTransaction(transactionId, nullptr); + if (pendingTeardownTransactionId != 0) + callback->cancelBlockAckTeardown(true, delba->getTransmitterAddress(), delba->getTid(), pendingTeardownTransactionId, nullptr); + if (terminatedAgreement != nullptr) + callback->cancelBlockAckTeardown(true, delba->getTransmitterAddress(), delba->getTid(), terminatedAgreement->getTransactionId(), nullptr); return terminatedAgreement; } return nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 8be627c5135..252c30bff02 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -22,6 +22,8 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg protected: std::map, OriginatorBlockAckAgreement *> blockAckAgreements; std::map, simtime_t> addbaRetryDeadlines; + // A tagged local DELBA remains eligible after its agreement is removed + // until the final fragment is acknowledged or terminally aborted. std::map, uint64_t> pendingTeardownTransactionIds; uint8_t nextDialogToken = 1; uint64_t nextTransactionId = 1; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc index d93711d2839..becc47e3d70 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc @@ -12,10 +12,11 @@ namespace inet { namespace ieee80211 { -RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t lastUsedTime) : +RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t lastUsedTime, uint64_t generationId) : startingSequenceNumber(startingSequenceNumber), bufferSize(bufferSize), - blockAckTimeoutValue(lastUsedTime) + blockAckTimeoutValue(lastUsedTime), + generationId(generationId) { calculateExpirationTime(); blockAckRecord = new BlockAckRecord(originatorAddress, tid); @@ -39,4 +40,3 @@ std::ostream& operator<<(std::ostream& os, const RecipientBlockAckAgreement& agr } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 98f9ba589c4..cb7932399bb 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -22,9 +22,10 @@ class INET_API RecipientBlockAckAgreement : public cObject int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; simtime_t expirationTime = -1; + uint64_t generationId = 0; public: - RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t blockAckTimeoutValue); + RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t blockAckTimeoutValue, uint64_t generationId = 0); virtual ~RecipientBlockAckAgreement() { delete blockAckRecord; } virtual void blockAckPolicyFrameReceived(const Ptr& header); @@ -33,6 +34,7 @@ class INET_API RecipientBlockAckAgreement : public cObject virtual simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } virtual int getBufferSize() const { return bufferSize; } virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } + virtual uint64_t getGenerationId() const { return generationId; } virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } virtual simtime_t getExpirationTime() { return expirationTime; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index bd4851bb0a6..0f5eb2ab66b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -8,6 +8,8 @@ #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" namespace inet { namespace ieee80211 { @@ -58,6 +60,7 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb Tid tid = id.first.second; const auto& delba = buildDelba(receiverAddr, tid, 39); auto delbaPacket = new Packet("Delba", delba); + delbaPacket->addTag()->setGenerationId(agreement->getGenerationId()); procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes } } @@ -79,6 +82,13 @@ const Ptr RecipientBlockAckAgreementHandler::buildDelba(MacAddre return delba; } +uint64_t RecipientBlockAckAgreementHandler::allocateAgreementGenerationId() +{ + if (nextAgreementGenerationId == 0) + throw cRuntimeError("Block Ack agreement generation ID exhausted"); + return nextAgreementGenerationId++; +} + const Ptr RecipientBlockAckAgreementHandler::buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, bool accepted) { auto addbaResponse = makeShared(); @@ -138,9 +148,18 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAd // IEEE Std 802.11-2024, 10.25.2 and 11.5.2.3: accepting the // request establishes or modifies the recipient agreement when the // successful response is formed; transmission is not a state gate. - agreement = new RecipientBlockAckAgreement(addbaRequest->getTransmitterAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaResponse->getBufferSize(), addbaResponse->getBlockAckTimeoutValue()); + auto pendingTeardownIt = pendingTeardownGenerationIds.find(id); + auto pendingTeardownGenerationId = pendingTeardownIt == pendingTeardownGenerationIds.end() ? 0 : pendingTeardownIt->second; + if (pendingTeardownIt != pendingTeardownGenerationIds.end()) + pendingTeardownGenerationIds.erase(pendingTeardownIt); + if (pendingTeardownGenerationId != 0 && agreementHandlerCallback != nullptr) + agreementHandlerCallback->cancelBlockAckTeardown(false, id.first, id.second, pendingTeardownGenerationId, nullptr); + auto generationId = allocateAgreementGenerationId(); + agreement = new RecipientBlockAckAgreement(addbaRequest->getTransmitterAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaResponse->getBufferSize(), addbaResponse->getBlockAckTimeoutValue(), generationId); auto it = blockAckAgreements.find(id); if (it != blockAckAgreements.end()) { + if (agreementHandlerCallback != nullptr && (pendingTeardownGenerationId == 0 || pendingTeardownGenerationId != it->second->getGenerationId())) + agreementHandlerCallback->cancelBlockAckTeardown(false, id.first, id.second, it->second->getGenerationId(), nullptr); delete it->second; it->second = agreement; } @@ -170,19 +189,102 @@ void RecipientBlockAckAgreementHandler::processDuplicateAddbaRequest(const Ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(const Ptr& delba) +bool RecipientBlockAckAgreementHandler::isDelbaPending(const Packet *packet, const Ptr& delba) const +{ + if (delba->getInitiator()) + return false; + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) + return true; + auto it = blockAckAgreements.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + if (it != blockAckAgreements.end()) + return it->second->getGenerationId() == agreementTag->getGenerationId(); + auto teardownIt = pendingTeardownGenerationIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + return teardownIt != pendingTeardownGenerationIds.end() && teardownIt->second == agreementTag->getGenerationId(); +} + +std::unique_ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(Packet *packet) { + auto delba = findFragmentedActionContext(packet); + if (delba->getInitiator()) + return nullptr; // IEEE Std 802.11-2024, 10.4 and 11.5.3.5: the DELBA MMPDU has not // been transmitted while a later fragment is still outstanding. if (delba->getMoreFragments()) return nullptr; + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr) { + auto agreement = getAgreement(delba->getTid(), delba->getReceiverAddress()); + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + if (agreement != nullptr) { + if (agreement->getGenerationId() != agreementTag->getGenerationId()) + return nullptr; + auto terminatedAgreement = std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + pendingTeardownGenerationIds[agreementId] = agreementTag->getGenerationId(); + return terminatedAgreement; + } + auto teardownIt = pendingTeardownGenerationIds.find(agreementId); + if (teardownIt == pendingTeardownGenerationIds.end() || teardownIt->second != agreementTag->getGenerationId()) + return nullptr; + return nullptr; + } return std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); } +bool RecipientBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = findFragmentedActionContext(packet); + if (delba->getInitiator() || delba->getMoreFragments()) + return false; + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) + return false; + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + auto it = pendingTeardownGenerationIds.find(agreementId); + if (it == pendingTeardownGenerationIds.end() || it->second != agreementTag->getGenerationId()) + return false; + auto generationId = it->second; + pendingTeardownGenerationIds.erase(it); + if (callback != nullptr) + callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), generationId, packet); + return true; +} + +bool RecipientBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +{ + auto delba = findFragmentedActionContext(packet); + if (delba->getInitiator()) + return false; + auto agreementTag = packet->findTag(); + if (agreementTag == nullptr) + return false; + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + auto it = pendingTeardownGenerationIds.find(agreementId); + if (it == pendingTeardownGenerationIds.end() || it->second != agreementTag->getGenerationId()) + return false; + auto generationId = it->second; + pendingTeardownGenerationIds.erase(it); + if (callback != nullptr) + callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), generationId, packet); + return true; +} + +uint64_t RecipientBlockAckAgreementHandler::getPendingTeardownGenerationId(Tid tid, MacAddress originatorAddr) const +{ + auto it = pendingTeardownGenerationIds.find(std::make_pair(originatorAddr, tid)); + return it == pendingTeardownGenerationIds.end() ? 0 : it->second; +} + std::unique_ptr RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) { - if (blockAckAgreementPolicy->isDelbaAccepted(delba)) - return std::unique_ptr(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + if (blockAckAgreementPolicy->isDelbaAccepted(delba)) { + auto agreementId = std::make_pair(delba->getTransmitterAddress(), delba->getTid()); + auto pendingTeardownIt = pendingTeardownGenerationIds.find(agreementId); + if (pendingTeardownIt != pendingTeardownGenerationIds.end()) + pendingTeardownGenerationIds.erase(pendingTeardownIt); + std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + return terminatedAgreement; + } return nullptr; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index f642c0a06b9..6d3af3e1d30 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -27,11 +27,16 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre protected: std::map, RecipientBlockAckAgreement *> blockAckAgreements; std::map, Ptr> lastAddbaResponses; + // A tagged local DELBA remains eligible after its agreement is removed + // until the final fragment is acknowledged or terminally aborted. + std::map, uint64_t> pendingTeardownGenerationIds; + uint64_t nextAgreementGenerationId = 1; protected: virtual RecipientBlockAckAgreement *removeAgreement(MacAddress originatorAddr, Tid tid); virtual const Ptr buildAddbaResponse(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, bool accepted); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); + virtual uint64_t allocateAgreementGenerationId(); virtual simtime_t computeEarliestExpirationTime(); virtual void scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback); @@ -41,10 +46,14 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) override; virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) override; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) override; - virtual std::unique_ptr processTransmittedDelba(const Ptr& delba) override; + virtual std::unique_ptr processTransmittedDelba(Packet *packet) override; + virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; + virtual uint64_t getPendingTeardownGenerationId(Tid tid, MacAddress originatorAddr) const override; + virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const override; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index 09abd395fd3..d130e3cda52 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -9,6 +9,8 @@ #define __INET_IBLOCKACKAGREEMENTHANDLERCALLBACK_H #include "inet/common/INETDefs.h" +#include "inet/linklayer/common/MacAddress.h" +#include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" namespace inet { @@ -24,6 +26,10 @@ class INET_API IBlockAckAgreementHandlerCallback virtual void scheduleInactivityTimer(simtime_t timeout) = 0; virtual void scheduleAddbaResponseTimer(simtime_t deadline) = 0; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) = 0; + // Removes queued siblings of a sender-local DELBA without assuming that + // the frame is still removable from the active frame sequence. The + // agreement owner remains responsible for rejecting stale packets. + virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) {} }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 4eba49314c1..f0b969353d9 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -19,6 +19,9 @@ #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" namespace inet { + +class Packet; + namespace ieee80211 { class INET_API IRecipientBlockAckAgreementHandler @@ -29,11 +32,15 @@ class INET_API IRecipientBlockAckAgreementHandler virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) = 0; virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; - virtual std::unique_ptr processTransmittedDelba(const Ptr& delba) = 0; + virtual std::unique_ptr processTransmittedDelba(Packet *packet) = 0; + virtual bool processAcknowledgedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return false; } + virtual bool processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return false; } virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) = 0; + virtual uint64_t getPendingTeardownGenerationId(Tid, MacAddress) const { return 0; } + virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const { return true; } }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index b4b7f41a623..63d1fedcd6d 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -14,6 +14,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" #include "inet/linklayer/ieee80211/mac/contract/FrameTransmissionDetails_m.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" @@ -73,8 +74,11 @@ void Hcf::initialize(int stage) originatorDataService->setFrameEligibilityFunction([this](const Packet *packet) { if (auto addbaReq = findFragmentedActionContext(packet)) return originatorBlockAckAgreementHandler->isAddbaRequestPending(packet, addbaReq); - if (auto delba = findFragmentedActionContext(packet)) - return originatorBlockAckAgreementHandler->isDelbaPending(packet, delba); + if (auto delba = findFragmentedActionContext(packet)) { + if (delba->getInitiator()) + return originatorBlockAckAgreementHandler->isDelbaPending(packet, delba); + return recipientBlockAckAgreementHandler->isDelbaPending(packet, delba); + } auto dataHeader = dynamicPtrCast(packet->peekAtFront()); // Hold this peer/TID while its ADDBA response is pending so no // already-sequenced MPDU can precede the advertised SSN. The @@ -146,12 +150,18 @@ bool Hcf::processDroppedBlockAckSetupFrame(Packet *packet) bool Hcf::processDroppedBlockAckTeardownFrame(Packet *packet) { - if (originatorBlockAckAgreementHandler) { - auto delba = findFragmentedActionContext(packet); - if (delba != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this)) { - rebuildPendingFrameEligibility(); - return true; - } + if (originatorBlockAckAgreementHandler == nullptr && recipientBlockAckAgreementHandler == nullptr) + return false; + auto delba = findFragmentedActionContext(packet); + if (delba == nullptr) + return false; + auto agreementTag = packet->findTag(); + if (agreementTag != nullptr && blockAckTeardownsBeingCancelled.find(std::make_tuple(delba->getInitiator(), delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId())) != blockAckTeardownsBeingCancelled.end()) + return false; + bool aborted = delba->getInitiator() ? originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this) : recipientBlockAckAgreementHandler != nullptr && recipientBlockAckAgreementHandler->processAbortedDelba(packet, this); + if (aborted) { + rebuildPendingFrameEligibility(); + return true; } return false; } @@ -481,6 +491,71 @@ void Hcf::cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) } } +void Hcf::cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) +{ + Enter_Method("cancelBlockAckTeardown"); + if (edca == nullptr) + return; + auto teardownId = std::make_tuple(initiator, peerAddress, tid, generationId); + bool outerCancellation = blockAckTeardownsBeingCancelled.insert(teardownId).second; + auto belongsToTeardown = [initiator, peerAddress, tid, generationId, excludedPacket](Packet *packet) { + if (packet == excludedPacket) + return false; + auto delba = findFragmentedActionContext(packet); + auto agreementTag = packet->findTag(); + return delba != nullptr && agreementTag != nullptr && delba->getInitiator() == initiator && + delba->getReceiverAddress() == peerAddress && delba->getTid() == tid && + agreementTag->getGenerationId() == generationId; + }; + bool removedPacket = false; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto edcaf = edca->getEdcaf(AccessCategory(ac)); + auto pendingQueue = edcaf->getPendingQueue(); + if (pendingQueue != nullptr) { + for (int i = pendingQueue->getNumPackets() - 1; i >= 0; i--) { + auto packet = pendingQueue->getPacket(i); + if (belongsToTeardown(packet)) { + pendingQueue->removePacket(packet); + take(packet); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + removedPacket = true; + } + } + } + auto inProgressFrames = edcaf->getInProgressFrames(); + if (inProgressFrames != nullptr) { + for (int i = inProgressFrames->getLength() - 1; i >= 0; i--) { + auto packet = inProgressFrames->getFrames(i); + if (belongsToTeardown(packet) && !isPacketReferencedByCurrentFrameSequence(packet)) { + auto header = packet->peekAtFront(); + auto extractedPacket = inProgressFrames->extractFrame(packet); + ASSERT(extractedPacket == packet); + take(packet); + auto managementRecoveryProcedure = edca->getMgmtAndNonQoSRecoveryProcedure(); + if (managementRecoveryProcedure != nullptr) + managementRecoveryProcedure->discardFrame(packet, header); + if (edcaf->getAckHandler() != nullptr) + edcaf->getAckHandler()->dropFrame(header); + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + removedPacket = true; + } + } + } + } + if (removedPacket && originatorDataService != nullptr) { + rebuildPendingFrameEligibility(); + resumeEligibleChannelAccess(); + } + if (outerCancellation) + blockAckTeardownsBeingCancelled.erase(teardownId); +} + void Hcf::processLowerFrame(Packet *packet, const Ptr& header) { Enter_Method("processLowerFrame(%s)", packet->getName()); @@ -704,6 +779,8 @@ void Hcf::recipientProcessReceivedManagementFrame(const Ptr(header)) { @@ -721,7 +798,7 @@ void Hcf::recipientProcessReceivedManagementFrame(const PtraddTag()->setTransactionId(response.teardownTransactionId); + delbaPacket->addTag()->setGenerationId(response.teardownTransactionId); processMgmtFrame(delbaPacket, response.teardownDelba); } resumeEligibleChannelAccess(); @@ -730,19 +807,33 @@ void Hcf::recipientProcessReceivedManagementFrame(const PtrgetInitiator()) { + auto pendingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); + auto remainingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); if (agreement != nullptr) { recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getTransmitterAddress()); emit(blockAckAgreementDeletedSignal, agreement.get()); + cancelBlockAckTeardown(false, delba->getTransmitterAddress(), delba->getTid(), agreement->getGenerationId(), nullptr); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); } + bool pendingTeardownRetired = pendingTeardownGenerationId != 0 && remainingTeardownGenerationId != pendingTeardownGenerationId; + if (pendingTeardownRetired && (agreement == nullptr || agreement->getGenerationId() != pendingTeardownGenerationId)) + cancelBlockAckTeardown(false, delba->getTransmitterAddress(), delba->getTid(), pendingTeardownGenerationId, nullptr); + if (pendingTeardownRetired && agreement == nullptr && edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); } else { bool wasPending = originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid()); auto agreement = originatorBlockAckAgreementHandler->processReceivedDelba(delba, originatorBlockAckAgreementPolicy, this); if (wasPending && !originatorBlockAckAgreementHandler->isAddbaResponsePending(delba->getTransmitterAddress(), delba->getTid())) rebuildPendingFrameEligibility(); - if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) - emit(blockAckAgreementDeletedSignal, agreement.get()); + if (agreement != nullptr) { + if (agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); + } resumeEligibleChannelAccess(); } } @@ -892,17 +983,24 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< auto agreement = originatorBlockAckAgreementHandler->processTransmittedDelba(packet, this); if (wasPending) rebuildPendingFrameEligibility(); - if (agreement != nullptr && agreement->getIsAddbaResponseReceived()) - emit(blockAckAgreementDeletedSignal, agreement.get()); + if (agreement != nullptr) { + if (agreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, agreement.get()); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); + } } else { - auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(delba); + auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(packet); if (agreement != nullptr) { // IEEE Std 802.11-2024, 10.25.4 and 11.5.3.5: recipient // resources are released whether the recipient transmitted or // received DELBA. The reorder window is such a resource. recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getReceiverAddress()); emit(blockAckAgreementDeletedSignal, agreement.get()); + cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), agreement->getGenerationId(), packet); + if (edca != nullptr && originatorDataService != nullptr) + rebuildPendingFrameEligibility(); } } } @@ -1064,7 +1162,8 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(lastTransmittedHeader); edcaf->getAckHandler()->processReceivedAck(ackFrame, lastTransmittedDataOrMgmtHeader); if (auto delba = findFragmentedActionContext(lastTransmittedPacket)) { - if (delba->getInitiator() && originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this)) + bool acknowledged = delba->getInitiator() ? originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this) : recipientBlockAckAgreementHandler != nullptr && recipientBlockAckAgreementHandler->processAcknowledgedDelba(lastTransmittedPacket, this); + if (acknowledged) rebuildPendingFrameEligibility(); } edcaf->getInProgressFrames()->dropFrame(lastTransmittedPacket); @@ -1078,8 +1177,10 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const PtrisAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid()); auto obsoleteTeardownTransactionId = originatorBlockAckAgreementHandler->processAcknowledgedDataFrame(lastTransmittedPacket, dataHeader, originatorBlockAckAgreementPolicy, this); - if (obsoleteTeardownTransactionId != 0) - cancelAddbaTransaction(obsoleteTeardownTransactionId, nullptr); + if (obsoleteTeardownTransactionId != 0) { + cancelBlockAckTeardown(true, dataHeader->getReceiverAddress(), dataHeader->getTid(), obsoleteTeardownTransactionId, nullptr); + rebuildPendingFrameEligibility(); + } if (!wasPending && originatorBlockAckAgreementHandler->isAddbaResponsePending(dataHeader->getReceiverAddress(), dataHeader->getTid())) rebuildPendingFrameEligibility(); } diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index b2230bff27b..a446e1bb07d 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -11,6 +11,7 @@ #include #include #include +#include #include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" #include "inet/linklayer/ieee80211/mac/channelaccess/Hcca.h" @@ -122,6 +123,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: std::set completedManagementTransactions; eventnumber_t completedManagementTransactionsEventNumber = -1; std::set cancelledManagementTransactions; + std::set> blockAckTeardownsBeingCancelled; // Protection mechanisms SingleProtectionMechanism *singleProtectionMechanism = nullptr; @@ -197,6 +199,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void scheduleInactivityTimer(simtime_t timeout) override; virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override; + virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override; std::string getFrameSequenceInfo() const; diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index f2ea78f6c0d..0523c2f0dad 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -16,6 +16,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211AddbaTransactionTag_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" @@ -107,6 +108,9 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler simtime_t addbaDeadline = SIMTIME_MAX; std::vector cancelledTransactionIds; std::vector excludedPackets; + std::vector cancelledTeardownGenerationIds; + std::vector excludedTeardownPackets; + std::vector cancelledTeardownInitiators; virtual ~TestCallback() { for (auto packet : managementPackets) delete packet; } void forgetManagementPacket(Packet *packet) { managementPackets.erase(std::remove(managementPackets.begin(), managementPackets.end(), packet), managementPackets.end()); } @@ -122,6 +126,11 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler cancelledTransactionIds.push_back(transactionId); excludedPackets.push_back(excludedPacket); } + virtual void cancelBlockAckTeardown(bool initiator, MacAddress, Tid, uint64_t generationId, Packet *excludedPacket) override { + cancelledTeardownInitiators.push_back(initiator); + cancelledTeardownGenerationIds.push_back(generationId); + excludedTeardownPackets.push_back(excludedPacket); + } }; class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler @@ -129,7 +138,7 @@ class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler public: using OriginatorBlockAckAgreementHandler::buildAddbaRequest; void addEstablishedAgreement(MacAddress receiverAddress, Tid tid) { - auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(0), 64, true, false, 1, 1); + auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(0), 64, true, false, 1, nextTransactionId++); agreement->setIsAddbaResponseReceived(true); blockAckAgreements[std::make_pair(receiverAddress, tid)] = agreement; } @@ -581,6 +590,8 @@ class TestHcf : public Hcf int numProcessedDroppedSetupFrames = 0; int numResumedEligibleChannelAccess = 0; int numCancelledAddbaTransactions = 0; + int numCancelledBlockAckTeardowns = 0; + std::vector cancelledBlockAckTeardownGenerationIds; int numRebuildEligibilityCalls = 0; int numTransmittedControlResponses = 0; bool droppedSetupCancelled = false; @@ -635,6 +646,7 @@ class TestHcf : public Hcf void dequeuePacket(Packet *packet) { handlePacketRemoved(packet, queueing::IPacketQueue::PacketRemovalReason::DEQUEUED); } bool cancelManagement(uint64_t transactionId, Packet *excludedPacket = nullptr) { return cancelManagementTransaction(transactionId, excludedPacket); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } + void cancelTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket = nullptr) { cancelBlockAckTeardown(initiator, peerAddress, tid, generationId, excludedPacket); } void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } void processReceivedFrame(Packet *packet) { recipientProcessReceivedFrame(packet, packet->peekAtFront()); } void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } @@ -658,6 +670,7 @@ class TestHcf : public Hcf } virtual bool processDroppedBlockAckSetupFrame(Packet *packet) override { numProcessedDroppedSetupFrames++; return delegateDroppedSetupHandling ? Hcf::processDroppedBlockAckSetupFrame(packet) : droppedSetupCancelled; } virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { numCancelledAddbaTransactions++; Hcf::cancelAddbaTransaction(transactionId, excludedPacket); } + virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override { numCancelledBlockAckTeardowns++; cancelledBlockAckTeardownGenerationIds.push_back(generationId); Hcf::cancelBlockAckTeardown(initiator, peerAddress, tid, generationId, excludedPacket); } virtual void rebuildPendingFrameEligibility() override { numRebuildEligibilityCalls++; Hcf::rebuildPendingFrameEligibility(); } virtual void resumeEligibleChannelAccess() override { numResumedEligibleChannelAccess++; } virtual void scheduleInactivityTimer(simtime_t) override {} @@ -889,7 +902,21 @@ static Packet *makeTaggedDelbaPacket(const char *name, MacAddress receiverAddres delba->setFragmentNumber(fragmentNumber); delba->setMoreFragments(moreFragments); auto packet = new Packet(name, delba); - packet->addTag()->setTransactionId(transactionId); + packet->addTag()->setGenerationId(transactionId); + return packet; +} + +static Packet *makeTaggedRecipientDelbaPacket(const char *name, MacAddress receiverAddress, Tid tid, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint64_t generationId) +{ + auto delba = makeShared(); + delba->setReceiverAddress(receiverAddress); + delba->setTid(tid); + delba->setInitiator(false); + delba->setSequenceNumber(sequenceNumber); + delba->setFragmentNumber(fragmentNumber); + delba->setMoreFragments(moreFragments); + auto packet = new Packet(name, delba); + packet->addTag()->setGenerationId(generationId); return packet; } @@ -1020,8 +1047,8 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); finalDelba->setMoreFragments(false); Packet firstDelbaPacket("firstDelbaFragment", firstDelba); Packet finalDelbaPacket("finalDelbaFragment", finalDelba); - firstDelbaPacket.addTag()->setTransactionId(77); - finalDelbaPacket.addTag()->setTransactionId(77); + firstDelbaPacket.addTag()->setGenerationId(77); + finalDelbaPacket.addTag()->setGenerationId(77); fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 77); ASSERT(fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); ASSERT(fragmentedDelbaHandler.processTransmittedDelba(&firstDelbaPacket, &fragmentedDelbaCallback) == nullptr); @@ -1034,15 +1061,15 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); ASSERT(!fragmentedDelbaHandler.processAcknowledgedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); ASSERT(fragmentedDelbaHandler.processAcknowledgedDelba(&finalDelbaPacket, &fragmentedDelbaCallback)); ASSERT(!fragmentedDelbaHandler.isDelbaPending(&finalDelbaPacket, finalDelba)); - ASSERT(fragmentedDelbaCallback.cancelledTransactionIds.back() == 77); - ASSERT(fragmentedDelbaCallback.excludedPackets.back() == &finalDelbaPacket); + ASSERT(fragmentedDelbaCallback.cancelledTeardownGenerationIds.back() == 77); + ASSERT(fragmentedDelbaCallback.excludedTeardownPackets.back() == &finalDelbaPacket); fragmentedDelbaHandler.setPendingTeardownTransaction(peer1, 6, 78); - firstDelbaPacket.getTagForUpdate()->setTransactionId(78); + firstDelbaPacket.getTagForUpdate()->setGenerationId(78); ASSERT(fragmentedDelbaHandler.processAbortedDelba(&firstDelbaPacket, &fragmentedDelbaCallback)); ASSERT(!fragmentedDelbaHandler.isDelbaPending(&firstDelbaPacket, firstDelba)); - ASSERT(fragmentedDelbaCallback.cancelledTransactionIds.back() == 78); - ASSERT(fragmentedDelbaCallback.excludedPackets.back() == &firstDelbaPacket); + ASSERT(fragmentedDelbaCallback.cancelledTeardownGenerationIds.back() == 78); + ASSERT(fragmentedDelbaCallback.excludedTeardownPackets.back() == &firstDelbaPacket); } // IEEE Std 802.11-2024, 10.23.2.12.1 and 11.5.3.2: the HCF retry path keeps a @@ -1131,7 +1158,7 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); completeDelba->setInitiator(true); completeDelba->setSequenceNumber(SequenceNumberCyclic(204)); auto completeDelbaPacket = new Packet("acknowledgedDelba", completeDelba); - completeDelbaPacket->addTag()->setTransactionId(transactionId); + completeDelbaPacket->addTag()->setGenerationId(transactionId); completeDelbaPacket->insertAtBack(makeShared()); Fragmentation fragmentation; auto delbaFragments = fragmentation.fragmentFrame(completeDelbaPacket, { 4, 2 }); @@ -1152,13 +1179,13 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); Packet ackPacket("transactionalDelbaAck", makeShared()); hcf.processReceivedAck(&ackPacket, finalPacket, AC_BE); ASSERT(!originatorHandler->isDelbaPending(finalPacket, findFragmentedActionContext(finalPacket))); - ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); ASSERT(inProgressFrames.getLength() == 0); ASSERT(inProgressFrames.getNumDroppedFrames() == 1); ASSERT(!ackHandler.hasMgmtEntry(siblingHeader)); ASSERT(!ackHandler.hasMgmtEntry(finalHeader)); ASSERT(!originatorHandler->processAcknowledgedDelba(finalPacket, &hcf)); - ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); auto releasedFrames = inProgressFrames.releaseFrames(); ASSERT(releasedFrames == std::vector({ finalPacket })); delete finalPacket; @@ -1209,13 +1236,13 @@ auto triggerHeader = makeQosHeader(peer1, 3, SequenceNumberCyclic(4095)); hcf.processTransmittedManagement(finalPacket, finalHeader, AC_BE); hcf.processFailedFrame(finalPacket); ASSERT(!originatorHandler->isDelbaPending(finalPacket, finalHeader)); - ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); ASSERT(inProgressFrames.getLength() == 0); ASSERT(inProgressFrames.getNumDroppedFrames() == 1); ASSERT(!ackHandler.hasMgmtEntry(siblingHeader)); ASSERT(!ackHandler.hasMgmtEntry(finalHeader)); ASSERT(!originatorHandler->processAbortedDelba(finalPacket, &hcf)); - ASSERT(hcf.numCancelledAddbaTransactions == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); auto releasedFrames = inProgressFrames.releaseFrames(); ASSERT(releasedFrames == std::vector({ finalPacket })); delete finalPacket; @@ -1281,7 +1308,7 @@ ASSERT(handler.processReceivedAddbaResp(success, &originatorPolicy, &callback).e ASSERT(vetoDelba->getTid() == 6); ASSERT(vetoDelba->getReasonCode() == RC_END_BA); Packet vetoDelbaPacket("vetoDelba", vetoDelba); - vetoDelbaPacket.addTag()->setTransactionId(vetoResponseOutcome.teardownTransactionId); + vetoDelbaPacket.addTag()->setGenerationId(vetoResponseOutcome.teardownTransactionId); ASSERT(vetoHandler.isDelbaPending(&vetoDelbaPacket, vetoDelba)); ASSERT(vetoHandler.getAgreement(peer3, 6) == nullptr); ASSERT(vetoHandler.getRetryDeadline(peer3, 6) > simTime()); @@ -2709,8 +2736,8 @@ ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == replacementImmediateA auto heldTeardownRetryHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(212)); auto obsoleteHeldTeardownTransactionId = heldTeardownOriginatorHandler->processAcknowledgedDataFrame(&heldTeardownRetryTrigger, heldTeardownRetryHeader, &heldTeardownOriginatorPolicy, &heldTeardownRetryCallback); ASSERT(heldTeardownRetryCallback.managementPacket != nullptr); - ASSERT(obsoleteHeldTeardownTransactionId == heldDelbaPacket->getTag()->getTransactionId()); - heldTeardownHcf.cancelTransaction(obsoleteHeldTeardownTransactionId); + ASSERT(obsoleteHeldTeardownTransactionId == heldDelbaPacket->getTag()->getGenerationId()); + heldTeardownHcf.cancelTeardown(true, peer1, 7, obsoleteHeldTeardownTransactionId); ASSERT(heldTeardownQueue.getNumPackets() == 1); ASSERT(heldTeardownQueue.getPacket(0) == heldDataPacket); heldTeardownHcf.rebuildEligibilityIndex(); @@ -4760,6 +4787,542 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { pendingQueues[ac].unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcf); } +// An originator inactivity DELBA retains the agreement transaction identity. +// Once that agreement is replaced, every stale fragment is rejected while a +// final DELBA carrying the current generation removes the replacement once. +{ + TestOriginatorHandler expiryHandler; + TestOriginatorPolicy expiryPolicy; + TestCallback setupCallback; + Packet trigger("originatorExpiryTrigger"); + auto triggerHeader = makeQosHeader(peer1, 9, SequenceNumberCyclic(300)); + expiryHandler.processAcknowledgedDataFrame(&trigger, triggerHeader, &expiryPolicy, &setupCallback); + auto setupRequest = dynamicPtrCast(setupCallback.managementHeader); + expiryHandler.processTransmittedAddbaReq(setupCallback.managementPacket, setupRequest, &expiryPolicy, &setupCallback); + auto setupResponse = makeResponse(peer1, 9, setupRequest->getDialogToken(), 0); + setupResponse->setBlockAckTimeoutValue(1); + auto establishedOutcome = expiryHandler.processReceivedAddbaResp(setupResponse, &expiryPolicy, &setupCallback); + auto firstAgreement = establishedOutcome.establishedAgreement; + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getTransactionId(); + firstAgreement->setBlockAckTimeoutValue(1); + firstAgreement->calculateExpirationTime(); + wait(1); + TestCallback expiryCallback; + expiryHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + auto stalePacket = expiryCallback.managementPacket; + auto staleDelba = dynamicPtrCast(expiryCallback.managementHeader); + ASSERT(stalePacket != nullptr && staleDelba != nullptr); + ASSERT(stalePacket->getTag()->getGenerationId() == firstGenerationId); + + auto receivedDelba = makeShared(); + receivedDelba->setTransmitterAddress(peer1); + receivedDelba->setTid(9); + auto terminatedFirstAgreement = expiryHandler.processReceivedDelba(receivedDelba, &expiryPolicy, &expiryCallback); + ASSERT(terminatedFirstAgreement != nullptr); + TestCallback replacementCallback; + Packet replacementTrigger("originatorReplacementTrigger"); + auto replacementHeader = makeQosHeader(peer1, 9, SequenceNumberCyclic(301)); + expiryHandler.processAcknowledgedDataFrame(&replacementTrigger, replacementHeader, &expiryPolicy, &replacementCallback); + auto replacementRequest = dynamicPtrCast(replacementCallback.managementHeader); + expiryHandler.processTransmittedAddbaReq(replacementCallback.managementPacket, replacementRequest, &expiryPolicy, &replacementCallback); + auto replacementResponse = makeResponse(peer1, 9, replacementRequest->getDialogToken(), 0); + auto replacementOutcome = expiryHandler.processReceivedAddbaResp(replacementResponse, &expiryPolicy, &replacementCallback); + auto secondAgreement = replacementOutcome.establishedAgreement; + ASSERT(secondAgreement != nullptr); + auto secondGenerationId = secondAgreement->getTransactionId(); + ASSERT(secondGenerationId != firstGenerationId); + ASSERT(!expiryHandler.isDelbaPending(stalePacket, staleDelba)); + + auto staleFragmentBody = staticPtrCast(staleDelba->dupShared()); + staleFragmentBody->setSequenceNumber(SequenceNumberCyclic(302)); + auto staleFragmentSource = new Packet("originatorStaleDelbaSource", staleFragmentBody); + staleFragmentSource->addTag()->setGenerationId(firstGenerationId); + staleFragmentSource->insertAtBack(makeShared()); + Fragmentation fragmentation; + auto staleFragments = fragmentation.fragmentFrame(staleFragmentSource, { 4, 2 }); + for (auto fragment : *staleFragments) + ASSERT(fragment->getTag()->getGenerationId() == firstGenerationId); + ASSERT(!expiryHandler.isDelbaPending(staleFragments->at(0), findFragmentedActionContext(staleFragments->at(0)))); + ASSERT(expiryHandler.processTransmittedDelba(staleFragments->at(1), &expiryCallback) == nullptr); + ASSERT(expiryHandler.getAgreement(peer1, 9) == secondAgreement); + delete staleFragments->at(0); + delete staleFragments->at(1); + delete staleFragments; + + auto currentDelba = makeShared(); + currentDelba->setReceiverAddress(peer1); + currentDelba->setTid(9); + currentDelba->setInitiator(true); + auto currentPacket = new Packet("originatorCurrentDelba", currentDelba); + currentPacket->addTag()->setGenerationId(secondGenerationId); + auto terminatedSecondAgreement = expiryHandler.processTransmittedDelba(currentPacket, &replacementCallback); + ASSERT(terminatedSecondAgreement != nullptr); + ASSERT(expiryHandler.getAgreement(peer1, 9) == nullptr); + ASSERT(expiryHandler.processTransmittedDelba(currentPacket, &replacementCallback) == nullptr); + delete currentPacket; +} + +// A recipient inactivity DELBA gets a handler-owned generation on every +// accepted replacement. Stale final fragments cannot remove the replacement, +// while a current-generation final DELBA removes it exactly once. +{ + TestRecipientHandler expiryHandler; + TestRecipientPolicy expiryPolicy; + expiryPolicy.blockAckTimeoutValue = 1; + TestCallback expiryCallback; + auto request = makeRequest(peer2, 10, 101, SequenceNumberCyclic(310), 32); + request->setBlockAckTimeoutValue(1); + auto firstAgreement = expiryHandler.processReceivedAddbaRequest(request, &expiryPolicy, &expiryCallback, &expiryCallback); + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getGenerationId(); + wait(1); + expiryHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + auto stalePacket = expiryCallback.managementPacket; + auto staleDelba = dynamicPtrCast(expiryCallback.managementHeader); + ASSERT(stalePacket != nullptr && staleDelba != nullptr); + ASSERT(stalePacket->getTag()->getGenerationId() == firstGenerationId); + + auto replacementRequest = makeRequest(peer2, 10, 102, SequenceNumberCyclic(311), 32); + replacementRequest->setBlockAckTimeoutValue(1); + auto secondAgreement = expiryHandler.processReceivedAddbaRequest(replacementRequest, &expiryPolicy, &expiryCallback, &expiryCallback); + ASSERT(secondAgreement != nullptr); + auto secondGenerationId = secondAgreement->getGenerationId(); + ASSERT(secondGenerationId != firstGenerationId); + ASSERT(!expiryHandler.isDelbaPending(stalePacket, staleDelba)); + ASSERT(expiryHandler.processTransmittedDelba(stalePacket) == nullptr); + ASSERT(expiryHandler.getAgreement(10, peer2) == secondAgreement); + ASSERT(std::find(expiryCallback.cancelledTeardownGenerationIds.begin(), expiryCallback.cancelledTeardownGenerationIds.end(), firstGenerationId) != expiryCallback.cancelledTeardownGenerationIds.end()); + + auto staleFragmentBody = staticPtrCast(staleDelba->dupShared()); + staleFragmentBody->setSequenceNumber(SequenceNumberCyclic(312)); + auto staleFragmentSource = new Packet("recipientStaleDelbaSource", staleFragmentBody); + staleFragmentSource->addTag()->setGenerationId(firstGenerationId); + staleFragmentSource->insertAtBack(makeShared()); + Fragmentation fragmentation; + auto staleFragments = fragmentation.fragmentFrame(staleFragmentSource, { 4, 2 }); + for (auto fragment : *staleFragments) + ASSERT(fragment->getTag()->getGenerationId() == firstGenerationId); + ASSERT(!expiryHandler.isDelbaPending(staleFragments->at(0), findFragmentedActionContext(staleFragments->at(0)))); + ASSERT(expiryHandler.processTransmittedDelba(staleFragments->at(1)) == nullptr); + ASSERT(expiryHandler.getAgreement(10, peer2) == secondAgreement); + delete staleFragments->at(0); + delete staleFragments->at(1); + delete staleFragments; + + auto currentPacket = makeTaggedRecipientDelbaPacket("recipientCurrentDelba", peer2, 10, SequenceNumberCyclic(313), 0, false, secondGenerationId); + auto terminatedAgreement = expiryHandler.processTransmittedDelba(currentPacket); + ASSERT(terminatedAgreement != nullptr); + ASSERT(expiryHandler.getAgreement(10, peer2) == nullptr); + ASSERT(expiryHandler.processTransmittedDelba(currentPacket) == nullptr); + delete currentPacket; +} + +// HCF routes recipient DELBAs to the recipient owner and rebuilds its +// eligibility index when replacement or final teardown changes the generation. +// The replacement callback removes queued stale siblings; the active current +// frame is then consumed exactly once. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy hcfOriginatorPolicy; + TestRecipientPolicy hcfRecipientPolicy; + TestCallback managementCallback; + auto request = makeRequest(peer2, 11, 103, SequenceNumberCyclic(320), 32); + auto firstAgreement = recipientHandler->processReceivedAddbaRequest(request, &hcfRecipientPolicy, &managementCallback, &managementCallback); + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getGenerationId(); + + TestOriginatorQosMacDataService dataService; + dataService.setFrameEligibilityFunction([recipientHandler](const Packet *packet) { + auto delba = findFragmentedActionContext(packet); + return delba == nullptr || recipientHandler->isDelbaPending(packet, delba); + }); + TestPacketQueue pendingQueue; + QosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestRecipientDataService recipientDataService; + OriginatorQosAckPolicy ackPolicy; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &hcfOriginatorPolicy, recipientHandler, &hcfRecipientPolicy); + hcf.configureRecipientDataService(&recipientDataService); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + hcf.configureManagementCallback(&managementCallback); + hcf.observePendingQueue(&pendingQueue); + + auto stalePacket = makeTaggedRecipientDelbaPacket("hcfStaleRecipientDelba", peer2, 11, SequenceNumberCyclic(321), 0, false, firstGenerationId); + pendingQueue.enqueuePacket(stalePacket); + hcf.trackFrame(stalePacket, AC_BE); + ASSERT(hcf.getNumEligiblePendingFrames(AC_BE) == 1); + + auto replacementRequest = makeRequest(peer2, 11, 104, SequenceNumberCyclic(322), 32); + hcf.processReceivedManagementFrame(replacementRequest); + auto secondAgreement = recipientHandler->getAgreement(11, peer2); + ASSERT(secondAgreement != nullptr); + ASSERT(secondAgreement->getGenerationId() != firstGenerationId); + ASSERT(pendingQueue.isEmpty()); + ASSERT(hcf.getNumEligiblePendingFrames(AC_BE) == 0); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); + ASSERT(hcf.numRebuildEligibilityCalls > 0); + + auto currentPacket = makeTaggedRecipientDelbaPacket("hcfCurrentRecipientDelba", peer2, 11, SequenceNumberCyclic(323), 0, false, secondAgreement->getGenerationId()); + pendingQueue.enqueuePacket(currentPacket); + hcf.trackFrame(currentPacket, AC_BE); + ASSERT(hcf.getNumEligiblePendingFrames(AC_BE) == 1); + TestSignalListener deletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.processTransmittedManagement(currentPacket, currentPacket->peekAtFront(), AC_BE); + ASSERT(recipientHandler->getAgreement(11, peer2) == nullptr); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 2); + ASSERT(hcf.getNumEligiblePendingFrames(AC_BE) == 0); + hcf.processTransmittedManagement(currentPacket, currentPacket->peekAtFront(), AC_BE); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 2); + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + pendingQueue.removePacket(currentPacket); + delete currentPacket; +} + +// A rejected peer DELBA must not retire a locally pending recipient teardown. +// Once the same DELBA is accepted, HCF cancels exactly that generation without +// repeating the first-transmission reset or deletion notification. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestCallback responseCallback; + TestOriginatorQosMacDataService dataService; + dataService.setFrameEligibilityFunction([recipientHandler](const Packet *packet) { + auto delba = findFragmentedActionContext(packet); + return delba == nullptr || recipientHandler->isDelbaPending(packet, delba); + }); + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + edca.nonQosRecoveryProcedure = &recoveryProcedure; + OriginatorQosAckPolicy ackPolicy; + TestMac mac; + TestRecipientDataService recipientDataService; + TestHcf hcf; + hcf.configureMac(&mac); + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureRecipientDataService(&recipientDataService); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + TestSignalListener deletionListener; + TestPacketDropSignalListener dropListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.subscribe(packetDroppedSignal, &dropListener); + + auto request = makeRequest(peer2, 14, 109, SequenceNumberCyclic(370), 32); + auto agreement = recipientHandler->processReceivedAddbaRequest(request, &recipientPolicy, &responseCallback, &hcf); + ASSERT(agreement != nullptr); + auto generationId = agreement->getGenerationId(); + auto pendingPacket = makeTaggedRecipientDelbaPacket("recipientRejectedPeerDelba", peer2, 14, SequenceNumberCyclic(371), 0, false, generationId); + auto pendingHeader = pendingPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(pendingPacket); + ackHandler.frameGotInProgress(pendingHeader); + hcf.trackFrame(pendingPacket, AC_BE); + hcf.processTransmittedManagement(pendingPacket, pendingHeader, AC_BE); + ASSERT(recipientHandler->getAgreement(14, peer2) == nullptr); + ASSERT(recipientHandler->isDelbaPending(pendingPacket, pendingHeader)); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(deletionListener.numSignals == 1); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); + ASSERT(hcf.cancelledBlockAckTeardownGenerationIds.back() == generationId); + hcf.processFailedFrame(pendingPacket); + + recipientPolicy.delbaAccepted = false; + auto rejectedDelba = makeShared(); + rejectedDelba->setTransmitterAddress(peer2); + rejectedDelba->setReceiverAddress(peer1); + rejectedDelba->setTid(14); + rejectedDelba->setInitiator(true); + hcf.processReceivedManagementFrame(rejectedDelba); + ASSERT(recipientHandler->isDelbaPending(pendingPacket, pendingHeader)); + ASSERT(inProgressFrames.getLength() == 1); + ASSERT(inProgressFrames.getFrameToTransmit() == pendingPacket); + ASSERT(hcf.hasFrame(AC_BE)); + ASSERT(hcf.numCancelledBlockAckTeardowns == 1); + ASSERT(hcf.cancelledBlockAckTeardownGenerationIds.back() == generationId); + ASSERT(dropListener.numOtherPacketDrops == 0); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(deletionListener.numSignals == 1); + + recipientPolicy.delbaAccepted = true; + auto acceptedDelba = makeShared(); + acceptedDelba->setTransmitterAddress(peer2); + acceptedDelba->setReceiverAddress(peer1); + acceptedDelba->setTid(14); + acceptedDelba->setInitiator(true); + hcf.processReceivedManagementFrame(acceptedDelba); + ASSERT(recipientHandler->getPendingTeardownGenerationId(14, peer2) == 0); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(!hcf.hasFrame(AC_BE)); + ASSERT(hcf.numCancelledBlockAckTeardowns == 2); + ASSERT(hcf.cancelledBlockAckTeardownGenerationIds.back() == generationId); + ASSERT(dropListener.numOtherPacketDrops == 1); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(deletionListener.numSignals == 1); + auto staleProbe = makeTaggedRecipientDelbaPacket("recipientRejectedPeerDelbaProbe", peer2, 14, SequenceNumberCyclic(372), 0, false, generationId); + ASSERT(!recipientHandler->isDelbaPending(staleProbe, staleProbe->peekAtFront())); + delete staleProbe; + + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.unsubscribe(packetDroppedSignal, &dropListener); +} + +// A tagged originator DELBA removes the agreement on its first transmission, +// but remains selectable through an unacknowledged retry. ACK, terminal abort, +// and replacement each retire only their exact teardown generation. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestOriginatorQosMacDataService dataService; + dataService.setFrameEligibilityFunction([originatorHandler](const Packet *packet) { + auto delba = findFragmentedActionContext(packet); + return delba == nullptr || originatorHandler->isDelbaPending(packet, delba); + }); + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + edca.nonQosRecoveryProcedure = &recoveryProcedure; + OriginatorQosAckPolicy ackPolicy; + TestMac mac; + TestHcf hcf; + hcf.configureMac(&mac); + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + TestSignalListener deletionListener; + TestPacketDropSignalListener dropListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.subscribe(packetDroppedSignal, &dropListener); + + originatorHandler->addEstablishedAgreement(peer1, 12); + auto firstGenerationId = originatorHandler->getAgreement(peer1, 12)->getTransactionId(); + auto acknowledgedPacket = makeTaggedDelbaPacket("originatorRetryAcknowledged", peer1, 12, SequenceNumberCyclic(350), 0, false, firstGenerationId); + auto acknowledgedHeader = acknowledgedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(acknowledgedPacket); + ackHandler.frameGotInProgress(acknowledgedHeader); + hcf.trackFrame(acknowledgedPacket, AC_BE); + hcf.processTransmittedManagement(acknowledgedPacket, acknowledgedHeader, AC_BE); + ASSERT(originatorHandler->getAgreement(peer1, 12) == nullptr); + ASSERT(originatorHandler->isDelbaPending(acknowledgedPacket, acknowledgedHeader)); + ASSERT(deletionListener.numSignals == 1); + hcf.processFailedFrame(acknowledgedPacket); + ASSERT(originatorHandler->isDelbaPending(acknowledgedPacket, acknowledgedPacket->peekAtFront())); + ASSERT(inProgressFrames.getFrameToTransmit() == acknowledgedPacket); + hcf.processTransmittedManagement(acknowledgedPacket, acknowledgedPacket->peekAtFront(), AC_BE); + Packet acknowledgedAck("originatorRetryAck", makeShared()); + hcf.processReceivedAck(&acknowledgedAck, acknowledgedPacket, AC_BE); + ASSERT(!originatorHandler->isDelbaPending(acknowledgedPacket, acknowledgedPacket->peekAtFront())); + ASSERT(deletionListener.numSignals == 1); + ASSERT(inProgressFrames.getLength() == 0); + auto acknowledgedFrames = inProgressFrames.releaseFrames(); + ASSERT(acknowledgedFrames == std::vector({ acknowledgedPacket })); + delete acknowledgedPacket; + + originatorHandler->addEstablishedAgreement(peer1, 12); + auto abortedGenerationId = originatorHandler->getAgreement(peer1, 12)->getTransactionId(); + auto abortedPacket = makeTaggedDelbaPacket("originatorRetryAborted", peer1, 12, SequenceNumberCyclic(351), 0, false, abortedGenerationId); + auto abortedHeader = abortedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(abortedPacket); + ackHandler.frameGotInProgress(abortedHeader); + hcf.trackFrame(abortedPacket, AC_BE); + hcf.processTransmittedManagement(abortedPacket, abortedHeader, AC_BE); + ASSERT(originatorHandler->isDelbaPending(abortedPacket, abortedHeader)); + hcf.processFailedFrame(abortedPacket); + ASSERT(originatorHandler->isDelbaPending(abortedPacket, abortedPacket->peekAtFront())); + ASSERT(inProgressFrames.getFrameToTransmit() == abortedPacket); + hcf.processTransmittedManagement(abortedPacket, abortedPacket->peekAtFront(), AC_BE); + hcf.processFailedFrame(abortedPacket); + ASSERT(!originatorHandler->isDelbaPending(abortedPacket, abortedPacket->peekAtFront())); + ASSERT(deletionListener.numSignals == 2); + auto abortedFrames = inProgressFrames.releaseFrames(); + ASSERT(abortedFrames == std::vector({ abortedPacket })); + delete abortedPacket; + + originatorHandler->addEstablishedAgreement(peer1, 12); + auto replacedGenerationId = originatorHandler->getAgreement(peer1, 12)->getTransactionId(); + auto replacedPacket = makeTaggedDelbaPacket("originatorRetryReplaced", peer1, 12, SequenceNumberCyclic(352), 0, false, replacedGenerationId); + auto replacedHeader = replacedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(replacedPacket); + ackHandler.frameGotInProgress(replacedHeader); + hcf.trackFrame(replacedPacket, AC_BE); + hcf.processTransmittedManagement(replacedPacket, replacedHeader, AC_BE); + ASSERT(originatorHandler->isDelbaPending(replacedPacket, replacedHeader)); + TestCallback replacementCallback; + Packet replacementTrigger("originatorRetryReplacementTrigger"); + auto replacementHeader = makeQosHeader(peer1, 12, SequenceNumberCyclic(353)); + auto obsoleteGenerationId = originatorHandler->processAcknowledgedDataFrame(&replacementTrigger, replacementHeader, &originatorPolicy, &replacementCallback); + ASSERT(obsoleteGenerationId == replacedGenerationId); + hcf.cancelTeardown(true, peer1, 12, obsoleteGenerationId); + auto replacedProbe = makeTaggedDelbaPacket("originatorRetryReplacedProbe", peer1, 12, SequenceNumberCyclic(354), 0, false, replacedGenerationId); + ASSERT(!originatorHandler->isDelbaPending(replacedProbe, replacedProbe->peekAtFront())); + delete replacedProbe; + ASSERT(originatorHandler->getAgreement(peer1, 12) != nullptr); + ASSERT(originatorHandler->getAgreement(peer1, 12)->getTransactionId() != replacedGenerationId); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(deletionListener.numSignals == 3); + ASSERT(dropListener.numOtherPacketDrops == 1); + + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.unsubscribe(packetDroppedSignal, &dropListener); +} + +// Recipient teardown generations follow the same retry lifetime as +// originator generations, including recipient-side ACK/abort callbacks and +// replacement disposal while an old final DELBA is still in progress. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestCallback responseCallback; + TestOriginatorQosMacDataService dataService; + dataService.setFrameEligibilityFunction([recipientHandler](const Packet *packet) { + auto delba = findFragmentedActionContext(packet); + return delba == nullptr || recipientHandler->isDelbaPending(packet, delba); + }); + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestCwCalculator cwCalculator; + TestNonQosRecoveryProcedure recoveryProcedure; + recoveryProcedure.configureRts(INT_MAX, 2, &cwCalculator); + edca.nonQosRecoveryProcedure = &recoveryProcedure; + OriginatorQosAckPolicy ackPolicy; + TestMac mac; + TestRecipientDataService recipientDataService; + TestHcf hcf; + hcf.configureMac(&mac); + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureRecipientDataService(&recipientDataService); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + TestSignalListener deletionListener; + TestPacketDropSignalListener dropListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.subscribe(packetDroppedSignal, &dropListener); + + auto firstRequest = makeRequest(peer2, 13, 105, SequenceNumberCyclic(360), 32); + auto firstAgreement = recipientHandler->processReceivedAddbaRequest(firstRequest, &recipientPolicy, &responseCallback, &hcf); + ASSERT(firstAgreement != nullptr); + auto firstGenerationId = firstAgreement->getGenerationId(); + auto acknowledgedPacket = makeTaggedRecipientDelbaPacket("recipientRetryAcknowledged", peer2, 13, SequenceNumberCyclic(361), 0, false, firstGenerationId); + auto acknowledgedHeader = acknowledgedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(acknowledgedPacket); + ackHandler.frameGotInProgress(acknowledgedHeader); + hcf.trackFrame(acknowledgedPacket, AC_BE); + hcf.processTransmittedManagement(acknowledgedPacket, acknowledgedHeader, AC_BE); + ASSERT(recipientHandler->getAgreement(13, peer2) == nullptr); + ASSERT(recipientHandler->isDelbaPending(acknowledgedPacket, acknowledgedHeader)); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 1); + hcf.processFailedFrame(acknowledgedPacket); + ASSERT(recipientHandler->isDelbaPending(acknowledgedPacket, acknowledgedPacket->peekAtFront())); + ASSERT(inProgressFrames.getFrameToTransmit() == acknowledgedPacket); + hcf.processTransmittedManagement(acknowledgedPacket, acknowledgedPacket->peekAtFront(), AC_BE); + Packet acknowledgedAck("recipientRetryAck", makeShared()); + hcf.processReceivedAck(&acknowledgedAck, acknowledgedPacket, AC_BE); + ASSERT(!recipientHandler->isDelbaPending(acknowledgedPacket, acknowledgedPacket->peekAtFront())); + ASSERT(deletionListener.numSignals == 1); + ASSERT(recipientDataService.numReorderingResets == 1); + auto acknowledgedFrames = inProgressFrames.releaseFrames(); + ASSERT(acknowledgedFrames == std::vector({ acknowledgedPacket })); + delete acknowledgedPacket; + + auto abortedRequest = makeRequest(peer2, 13, 106, SequenceNumberCyclic(362), 32); + auto abortedAgreement = recipientHandler->processReceivedAddbaRequest(abortedRequest, &recipientPolicy, &responseCallback, &hcf); + ASSERT(abortedAgreement != nullptr); + auto abortedGenerationId = abortedAgreement->getGenerationId(); + auto abortedPacket = makeTaggedRecipientDelbaPacket("recipientRetryAborted", peer2, 13, SequenceNumberCyclic(363), 0, false, abortedGenerationId); + auto abortedHeader = abortedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(abortedPacket); + ackHandler.frameGotInProgress(abortedHeader); + hcf.trackFrame(abortedPacket, AC_BE); + hcf.processTransmittedManagement(abortedPacket, abortedHeader, AC_BE); + ASSERT(recipientHandler->isDelbaPending(abortedPacket, abortedHeader)); + hcf.processFailedFrame(abortedPacket); + ASSERT(recipientHandler->isDelbaPending(abortedPacket, abortedPacket->peekAtFront())); + ASSERT(inProgressFrames.getFrameToTransmit() == abortedPacket); + hcf.processTransmittedManagement(abortedPacket, abortedPacket->peekAtFront(), AC_BE); + hcf.processFailedFrame(abortedPacket); + ASSERT(!recipientHandler->isDelbaPending(abortedPacket, abortedPacket->peekAtFront())); + ASSERT(deletionListener.numSignals == 2); + ASSERT(recipientDataService.numReorderingResets == 2); + auto abortedFrames = inProgressFrames.releaseFrames(); + ASSERT(abortedFrames == std::vector({ abortedPacket })); + delete abortedPacket; + + auto replacedRequest = makeRequest(peer2, 13, 107, SequenceNumberCyclic(364), 32); + auto replacedAgreement = recipientHandler->processReceivedAddbaRequest(replacedRequest, &recipientPolicy, &responseCallback, &hcf); + ASSERT(replacedAgreement != nullptr); + auto replacedGenerationId = replacedAgreement->getGenerationId(); + auto replacedPacket = makeTaggedRecipientDelbaPacket("recipientRetryReplaced", peer2, 13, SequenceNumberCyclic(365), 0, false, replacedGenerationId); + auto replacedHeader = replacedPacket->peekAtFront(); + inProgressFrames.addOwnedFrame(replacedPacket); + ackHandler.frameGotInProgress(replacedHeader); + hcf.trackFrame(replacedPacket, AC_BE); + hcf.processTransmittedManagement(replacedPacket, replacedHeader, AC_BE); + ASSERT(recipientHandler->isDelbaPending(replacedPacket, replacedHeader)); + auto replacementRequest = makeRequest(peer2, 13, 108, SequenceNumberCyclic(366), 32); + auto replacementAgreement = recipientHandler->processReceivedAddbaRequest(replacementRequest, &recipientPolicy, &responseCallback, &hcf); + ASSERT(replacementAgreement != nullptr); + ASSERT(replacementAgreement->getGenerationId() != replacedGenerationId); + auto replacedProbe = makeTaggedRecipientDelbaPacket("recipientRetryReplacedProbe", peer2, 13, SequenceNumberCyclic(367), 0, false, replacedGenerationId); + ASSERT(!recipientHandler->isDelbaPending(replacedProbe, replacedProbe->peekAtFront())); + delete replacedProbe; + ASSERT(recipientHandler->getAgreement(13, peer2) == replacementAgreement); + ASSERT(inProgressFrames.getLength() == 0); + ASSERT(deletionListener.numSignals == 3); + ASSERT(recipientDataService.numReorderingResets == 3); + ASSERT(dropListener.numOtherPacketDrops == 1); + + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &deletionListener); + hcf.unsubscribe(packetDroppedSignal, &dropListener); +} + EV << "ADDBA transaction and negotiated SSN integrity checks passed.\n"; %contains: stdout From efbca139763f38767d4353e2c0ea72e52b840802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:04 +0200 Subject: [PATCH 34/56] ieee80211: schedule Block Ack inactivity with absolute deadlines Make originator and recipient agreement handlers report absolute, role-specific inactivity deadlines. Have HCF schedule the earliest one with rescheduleAt() and expire only agreements whose recorded deadline has actually elapsed. Refresh the correct role on QoS data, BAR, and Block Ack activity, and retire the matching generation once when inactivity expires or teardown is aborted. This avoids treating an absolute timestamp as a relative delay and repeatedly rearming an already expired agreement. Cover independent originator and recipient deadlines, activity refresh, simultaneous expiry, stale generations, and terminal cleanup. --- .../blockack/OriginatorBlockAckAgreement.h | 10 +- .../OriginatorBlockAckAgreementHandler.cc | 38 +++- .../OriginatorBlockAckAgreementHandler.h | 2 +- .../mac/blockack/RecipientBlockAckAgreement.h | 10 +- .../RecipientBlockAckAgreementHandler.cc | 66 ++++-- .../RecipientBlockAckAgreementHandler.h | 7 +- .../IBlockAckAgreementHandlerCallback.h | 10 +- .../IOriginatorBlockAckAgreementHandler.h | 10 +- .../IRecipientBlockAckAgreementHandler.h | 15 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 41 +++- .../ieee80211/mac/coordinationfunction/Hcf.h | 7 +- .../Ieee80211BlockAckInactivityTimer_1.test | 129 +++++++++++ tests/unit/Ieee80211AddbaTransaction_1.test | 213 +++++++++++++++++- 13 files changed, 510 insertions(+), 48 deletions(-) create mode 100644 tests/module/Ieee80211BlockAckInactivityTimer_1.test diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index 8099d7c1bdb..25bad8d77a2 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -38,6 +38,9 @@ class INET_API OriginatorBlockAckAgreement : public cObject simtime_t addbaResponseDeadline = -1; simtime_t blockAckTimeoutValue = -1; simtime_t expirationTime = -1; + // The agreement stays installed until the timeout DELBA is transmitted; + // prevent that pending teardown from being re-armed by late activity. + bool inactivityExpired = false; public: OriginatorBlockAckAgreement(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, bool isAMsduSupported, bool isDelayedBlockAckPolicySupported, uint8_t dialogToken, uint64_t transactionId) : @@ -79,8 +82,13 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual void setAddbaResponseDeadline(simtime_t addbaResponseDeadline) { this->addbaResponseDeadline = addbaResponseDeadline; } virtual void baPolicyFrameSent() { numSentBaPolicyFrames++; } - virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } + virtual void calculateExpirationTime() { + if (!inactivityExpired) + expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; + } virtual simtime_t getExpirationTime() { return expirationTime; } + virtual bool isInactivityExpired() const { return inactivityExpired; } + virtual void markInactivityExpired() { inactivityExpired = true; expirationTime = SIMTIME_MAX; } }; } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index 0f84c21a671..ad22a8fce99 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -38,7 +38,7 @@ simtime_t OriginatorBlockAckAgreementHandler::computeEarliestExpirationTime() simtime_t earliestTime = SIMTIME_MAX; for (auto id : blockAckAgreements) { auto agreement = id.second; - if (agreement->getIsAddbaResponseReceived()) { + if (agreement->getIsAddbaResponseReceived() && !agreement->isInactivityExpired()) { ASSERT(earliestTime >= 0); ASSERT(agreement->getExpirationTime() >= 0); earliestTime = std::min(earliestTime, agreement->getExpirationTime()); @@ -107,7 +107,8 @@ void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCall simtime_t now = simTime(); for (auto id : blockAckAgreements) { auto agreement = id.second; - if (agreement->getExpirationTime() == now) { + if (agreement->getIsAddbaResponseReceived() && !agreement->isInactivityExpired() && agreement->getExpirationTime() <= now) { + agreement->markInactivityExpired(); MacAddress receiverAddr = id.first.first; Tid tid = id.first.second; const auto& delba = buildDelba(receiverAddr, tid, 39); @@ -144,7 +145,7 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const Ptr(blockAck)) { auto agreement = getAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); - if (agreement) { + if (agreement && !agreement->isInactivityExpired()) { agreement->setStartingSequenceNumber(basicBlockAck->getStartingSequenceNumber()); agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); @@ -157,8 +158,8 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const PtrscheduleInactivityTimer(earliestExpirationTime); + if (callback != nullptr) + callback->scheduleInactivityTimer(BlockAckAgreementRole::ORIGINATOR, earliestExpirationTime); } void OriginatorBlockAckAgreementHandler::scheduleAddbaResponseTimer(IBlockAckAgreementHandlerCallback *callback) @@ -362,6 +363,7 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: return nullptr; bool cancelPendingTransaction = agreement->isPending(); std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); pendingTeardownTransactionIds[std::make_pair(delba->getReceiverAddress(), delba->getTid())] = generationId; scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) @@ -373,6 +375,7 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) callback->cancelAddbaTransaction(transactionId, nullptr); @@ -396,22 +399,34 @@ bool OriginatorBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet return true; } -bool OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +OriginatorBlockAckAgreementAbortResult OriginatorBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { auto delba = findFragmentedActionContext(packet); if (!delba->getInitiator()) - return false; + return {}; auto agreementTag = packet->findTag(); if (agreementTag != nullptr) { - auto it = pendingTeardownTransactionIds.find(std::make_pair(delba->getReceiverAddress(), delba->getTid())); + auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); + auto it = pendingTeardownTransactionIds.find(agreementId); if (it != pendingTeardownTransactionIds.end() && it->second == agreementTag->getGenerationId()) { auto transactionId = it->second; pendingTeardownTransactionIds.erase(it); - callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); - return true; + if (callback != nullptr) + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), transactionId, packet); + return { true, nullptr }; + } + auto agreement = getAgreement(delba->getReceiverAddress(), delba->getTid()); + if (agreement != nullptr && agreement->getTransactionId() == agreementTag->getGenerationId() && agreement->isInactivityExpired()) { + OriginatorBlockAckAgreementAbortResult result; + result.handled = true; + result.terminatedAgreement.reset(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); + if (callback != nullptr) + callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId(), packet); + return result; } } - return false; + return {}; } std::unique_ptr OriginatorBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) @@ -426,6 +441,7 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: if (pendingTeardownIt != pendingTeardownTransactionIds.end()) pendingTeardownTransactionIds.erase(pendingTeardownIt); std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + scheduleInactivityTimer(callback); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) callback->cancelAddbaTransaction(transactionId, nullptr); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 252c30bff02..4b155bbbe34 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -53,7 +53,7 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; - virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index cb7932399bb..85ef1f9e613 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -22,6 +22,9 @@ class INET_API RecipientBlockAckAgreement : public cObject int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; simtime_t expirationTime = -1; + // The agreement stays installed until the timeout DELBA is transmitted; + // prevent that pending teardown from being re-armed by late activity. + bool inactivityExpired = false; uint64_t generationId = 0; public: @@ -36,8 +39,13 @@ class INET_API RecipientBlockAckAgreement : public cObject virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } virtual uint64_t getGenerationId() const { return generationId; } - virtual void calculateExpirationTime() { expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } + virtual void calculateExpirationTime() { + if (!inactivityExpired) + expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; + } virtual simtime_t getExpirationTime() { return expirationTime; } + virtual bool isInactivityExpired() const { return inactivityExpired; } + virtual void markInactivityExpired() { inactivityExpired = true; expirationTime = SIMTIME_MAX; } friend std::ostream& operator<<(std::ostream& os, const RecipientBlockAckAgreement& agreement); }; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 0f5eb2ab66b..5648b95a90a 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -19,7 +19,8 @@ simtime_t RecipientBlockAckAgreementHandler::computeEarliestExpirationTime() simtime_t earliestTime = SIMTIME_MAX; for (auto id : blockAckAgreements) { auto agreement = id.second; - earliestTime = std::min(earliestTime, agreement->getExpirationTime()); + if (!agreement->isInactivityExpired()) + earliestTime = std::min(earliestTime, agreement->getExpirationTime()); } return earliestTime; } @@ -27,8 +28,8 @@ simtime_t RecipientBlockAckAgreementHandler::computeEarliestExpirationTime() void RecipientBlockAckAgreementHandler::scheduleInactivityTimer(IBlockAckAgreementHandlerCallback *callback) { simtime_t earliestExpirationTime = computeEarliestExpirationTime(); - if (earliestExpirationTime != SIMTIME_MAX) - callback->scheduleInactivityTimer(earliestExpirationTime); + if (callback != nullptr) + callback->scheduleInactivityTimer(BlockAckAgreementRole::RECIPIENT, earliestExpirationTime); } // The inactivity timer at a recipient is reset when MPDUs corresponding to the TID for which the Block Ack @@ -41,8 +42,21 @@ void RecipientBlockAckAgreementHandler::qosFrameReceived(const PtrgetTid(); MacAddress originatorAddr = qosHeader->getTransmitterAddress(); auto agreement = getAgreement(tid, originatorAddr); - if (agreement) + if (agreement && !agreement->isInactivityExpired()) { + agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); + } + } +} + +// IEEE Std 802.11-2024, 11.5.4: a Basic BlockAckReq for an agreement's TID +// also resets the recipient inactivity timer. +void RecipientBlockAckAgreementHandler::blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) +{ + auto agreement = getAgreement(blockAckReq->getTidInfo(), blockAckReq->getTransmitterAddress()); + if (agreement != nullptr && !agreement->isInactivityExpired()) { + agreement->calculateExpirationTime(); + scheduleInactivityTimer(callback); } } @@ -55,7 +69,8 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb simtime_t now = simTime(); for (auto id : blockAckAgreements) { auto agreement = id.second; - if (agreement->getExpirationTime() == now) { + if (!agreement->isInactivityExpired() && agreement->getExpirationTime() <= now) { + agreement->markInactivityExpired(); MacAddress receiverAddr = id.first.first; Tid tid = id.first.second; const auto& delba = buildDelba(receiverAddr, tid, 39); @@ -203,7 +218,7 @@ bool RecipientBlockAckAgreementHandler::isDelbaPending(const Packet *packet, con return teardownIt != pendingTeardownGenerationIds.end() && teardownIt->second == agreementTag->getGenerationId(); } -std::unique_ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(Packet *packet) +std::unique_ptr RecipientBlockAckAgreementHandler::processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { auto delba = findFragmentedActionContext(packet); if (delba->getInitiator()) @@ -220,6 +235,7 @@ std::unique_ptr RecipientBlockAckAgreementHandler::p if (agreement->getGenerationId() != agreementTag->getGenerationId()) return nullptr; auto terminatedAgreement = std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); pendingTeardownGenerationIds[agreementId] = agreementTag->getGenerationId(); return terminatedAgreement; } @@ -228,7 +244,9 @@ std::unique_ptr RecipientBlockAckAgreementHandler::p return nullptr; return nullptr; } - return std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + auto terminatedAgreement = std::unique_ptr(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); + return terminatedAgreement; } bool RecipientBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) @@ -250,23 +268,34 @@ bool RecipientBlockAckAgreementHandler::processAcknowledgedDelba(Packet *packet, return true; } -bool RecipientBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) +RecipientBlockAckAgreementAbortResult RecipientBlockAckAgreementHandler::processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) { auto delba = findFragmentedActionContext(packet); if (delba->getInitiator()) - return false; + return {}; auto agreementTag = packet->findTag(); if (agreementTag == nullptr) - return false; + return {}; auto agreementId = std::make_pair(delba->getReceiverAddress(), delba->getTid()); auto it = pendingTeardownGenerationIds.find(agreementId); - if (it == pendingTeardownGenerationIds.end() || it->second != agreementTag->getGenerationId()) - return false; - auto generationId = it->second; - pendingTeardownGenerationIds.erase(it); - if (callback != nullptr) - callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), generationId, packet); - return true; + if (it != pendingTeardownGenerationIds.end() && it->second == agreementTag->getGenerationId()) { + auto generationId = it->second; + pendingTeardownGenerationIds.erase(it); + if (callback != nullptr) + callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), generationId, packet); + return { true, nullptr }; + } + auto agreement = getAgreement(delba->getTid(), delba->getReceiverAddress()); + if (agreement != nullptr && agreement->getGenerationId() == agreementTag->getGenerationId() && agreement->isInactivityExpired()) { + RecipientBlockAckAgreementAbortResult result; + result.handled = true; + result.terminatedAgreement.reset(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + scheduleInactivityTimer(callback); + if (callback != nullptr) + callback->cancelBlockAckTeardown(false, delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId(), packet); + return result; + } + return {}; } uint64_t RecipientBlockAckAgreementHandler::getPendingTeardownGenerationId(Tid tid, MacAddress originatorAddr) const @@ -275,7 +304,7 @@ uint64_t RecipientBlockAckAgreementHandler::getPendingTeardownGenerationId(Tid t return it == pendingTeardownGenerationIds.end() ? 0 : it->second; } -std::unique_ptr RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) +std::unique_ptr RecipientBlockAckAgreementHandler::processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) { if (blockAckAgreementPolicy->isDelbaAccepted(delba)) { auto agreementId = std::make_pair(delba->getTransmitterAddress(), delba->getTid()); @@ -283,6 +312,7 @@ std::unique_ptr RecipientBlockAckAgreementHandler::p if (pendingTeardownIt != pendingTeardownGenerationIds.end()) pendingTeardownGenerationIds.erase(pendingTeardownIt); std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + scheduleInactivityTimer(callback); return terminatedAgreement; } return nullptr; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index 6d3af3e1d30..da90268c368 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -44,11 +44,12 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre virtual ~RecipientBlockAckAgreementHandler(); virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) override; - virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) override; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback = nullptr) override; virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) override; - virtual std::unique_ptr processTransmittedDelba(Packet *packet) override; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback = nullptr) override; virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; - virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; + virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index d130e3cda52..cd2a3859699 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -18,12 +18,20 @@ class Packet; namespace ieee80211 { +enum class BlockAckAgreementRole +{ + ORIGINATOR, + RECIPIENT, +}; + class INET_API IBlockAckAgreementHandlerCallback { public: virtual ~IBlockAckAgreementHandlerCallback() {} - virtual void scheduleInactivityTimer(simtime_t timeout) = 0; + // The deadline is an absolute simulation timestamp. The role identifies + // which half of the shared HCF inactivity timer supplied the deadline. + virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) = 0; virtual void scheduleAddbaResponseTimer(simtime_t deadline) = 0; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) = 0; // Removes queued siblings of a sender-local DELBA without assuming that diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h index fbe0918749a..9a1a2af753e 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h @@ -32,6 +32,14 @@ struct INET_API OriginatorBlockAckAgreementResponse uint64_t teardownTransactionId = 0; }; +struct INET_API OriginatorBlockAckAgreementAbortResult +{ + bool handled = false; + std::unique_ptr terminatedAgreement; + + explicit operator bool() const { return handled; } +}; + class INET_API IOriginatorBlockAckAgreementHandler { public: @@ -49,7 +57,7 @@ class INET_API IOriginatorBlockAckAgreementHandler // Returns true when the packet completed or aborted its tagged teardown // transaction and sibling packets were cancelled through the callback. virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual bool processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index f0b969353d9..31f6b478078 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -24,6 +24,14 @@ class Packet; namespace ieee80211 { +struct INET_API RecipientBlockAckAgreementAbortResult +{ + bool handled = false; + std::unique_ptr terminatedAgreement; + + explicit operator bool() const { return handled; } +}; + class INET_API IRecipientBlockAckAgreementHandler { public: @@ -31,11 +39,12 @@ class INET_API IRecipientBlockAckAgreementHandler virtual RecipientBlockAckAgreement *processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual void processDuplicateAddbaRequest(const Ptr& addbaRequest, IProcedureCallback *procedureCallback) = 0; - virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy) = 0; - virtual std::unique_ptr processTransmittedDelba(Packet *packet) = 0; + virtual std::unique_ptr processReceivedDelba(const Ptr& delba, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback = nullptr) = 0; + virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback = nullptr) = 0; virtual bool processAcknowledgedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return false; } - virtual bool processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return false; } + virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return {}; } virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 63d1fedcd6d..0a36f69d9e8 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -9,6 +9,8 @@ #include "inet/queueing/contract/PacketQueueRemovalDetails.h" +#include + #include "inet/common/ModuleAccess.h" #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Mac.h" @@ -158,7 +160,24 @@ bool Hcf::processDroppedBlockAckTeardownFrame(Packet *packet) auto agreementTag = packet->findTag(); if (agreementTag != nullptr && blockAckTeardownsBeingCancelled.find(std::make_tuple(delba->getInitiator(), delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId())) != blockAckTeardownsBeingCancelled.end()) return false; - bool aborted = delba->getInitiator() ? originatorBlockAckAgreementHandler != nullptr && originatorBlockAckAgreementHandler->processAbortedDelba(packet, this) : recipientBlockAckAgreementHandler != nullptr && recipientBlockAckAgreementHandler->processAbortedDelba(packet, this); + bool aborted = false; + if (delba->getInitiator()) { + if (originatorBlockAckAgreementHandler != nullptr) { + auto result = originatorBlockAckAgreementHandler->processAbortedDelba(packet, this); + aborted = result.handled; + if (result.terminatedAgreement != nullptr && result.terminatedAgreement->getIsAddbaResponseReceived()) + emit(blockAckAgreementDeletedSignal, result.terminatedAgreement.get()); + } + } + else if (recipientBlockAckAgreementHandler != nullptr) { + auto result = recipientBlockAckAgreementHandler->processAbortedDelba(packet, this); + aborted = result.handled; + if (result.terminatedAgreement != nullptr) { + if (recipientDataService != nullptr) + recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getReceiverAddress()); + emit(blockAckAgreementDeletedSignal, result.terminatedAgreement.get()); + } + } if (aborted) { rebuildPendingFrameEligibility(); return true; @@ -433,10 +452,20 @@ void Hcf::scheduleStartRxTimer(simtime_t timeout) scheduleAfter(timeout, startRxTimer); } -void Hcf::scheduleInactivityTimer(simtime_t timeout) +void Hcf::scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) { Enter_Method("scheduleInactivityTimer"); - rescheduleAfter(timeout, inactivityTimer); + if (role == BlockAckAgreementRole::ORIGINATOR) + originatorInactivityDeadline = deadline; + else + recipientInactivityDeadline = deadline; + auto earliestDeadline = std::min(originatorInactivityDeadline, recipientInactivityDeadline); + if (earliestDeadline == SIMTIME_MAX) { + if (inactivityTimer->isScheduled()) + cancelEvent(inactivityTimer); + } + else + rescheduleAt(std::max(earliestDeadline, simTime()), inactivityTimer); } void Hcf::scheduleAddbaResponseTimer(simtime_t deadline) @@ -750,6 +779,8 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr(header)) ctsProcedure->processReceivedRts(packet, rtsFrame, ctsPolicy, this); else if (auto blockAckRequest = dynamicPtrCast(header)) { + if (recipientBlockAckAgreementHandler) + recipientBlockAckAgreementHandler->blockAckReqReceived(blockAckRequest, this); if (recipientBlockAckProcedure) recipientBlockAckProcedure->processReceivedBlockAckReq(packet, blockAckRequest, recipientAckPolicy, recipientBlockAckAgreementHandler, this); } @@ -808,7 +839,7 @@ void Hcf::recipientProcessReceivedManagementFrame(const PtrgetInitiator()) { auto pendingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); - auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy); + auto agreement = recipientBlockAckAgreementHandler->processReceivedDelba(delba, recipientBlockAckAgreementPolicy, this); auto remainingTeardownGenerationId = recipientBlockAckAgreementHandler->getPendingTeardownGenerationId(delba->getTid(), delba->getTransmitterAddress()); if (agreement != nullptr) { recipientDataService->resetBlockAckReordering(delba->getTid(), delba->getTransmitterAddress()); @@ -991,7 +1022,7 @@ void Hcf::originatorProcessTransmittedManagementFrame(Packet *packet, const Ptr< } } else { - auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(packet); + auto agreement = recipientBlockAckAgreementHandler->processTransmittedDelba(packet, this); if (agreement != nullptr) { // IEEE Std 802.11-2024, 10.25.4 and 11.5.3.5: recipient // resources are released whether the recipient transmitted or diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index a446e1bb07d..052524378e3 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -65,6 +65,11 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: cMessage *startRxTimer = nullptr; cMessage *inactivityTimer = nullptr; cMessage *addbaResponseTimer = nullptr; + // The two agreement handlers share one timer but publish independent + // absolute deadlines. Keep both until the handlers explicitly retire + // their role so one role cannot cancel the other's timeout. + simtime_t originatorInactivityDeadline = SIMTIME_MAX; + simtime_t recipientInactivityDeadline = SIMTIME_MAX; // Transmission and Reception IRx *rx = nullptr; @@ -196,7 +201,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void processMgmtFrame(Packet *mgmtPacket, const Ptr& mgmtHeader) override; // IProcedureCallback - virtual void scheduleInactivityTimer(simtime_t timeout) override; + virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) override; virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override; virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override; diff --git a/tests/module/Ieee80211BlockAckInactivityTimer_1.test b/tests/module/Ieee80211BlockAckInactivityTimer_1.test new file mode 100644 index 00000000000..bdd20495297 --- /dev/null +++ b/tests/module/Ieee80211BlockAckInactivityTimer_1.test @@ -0,0 +1,129 @@ +%description: +Exercise the shared HCF Block Ack inactivity timer with independent originator +and recipient deadlines. Verify that the timer keeps the earliest absolute +deadline, preserves the other role when one role is cleared, and is cancelled +only after both roles publish SIMTIME_MAX. + +%file: TestIeee80211BlockAckInactivityTimer.cc + +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" + +namespace inet { +namespace ieee80211 { + +class TestHcfInactivityTimer : public Hcf +{ + public: + void updateInactivityDeadline(BlockAckAgreementRole role, simtime_t deadline) { scheduleInactivityTimer(role, deadline); } + simtime_t getOriginatorDeadline() const { return originatorInactivityDeadline; } + simtime_t getRecipientDeadline() const { return recipientInactivityDeadline; } + bool isInactivityTimerScheduled() const { return inactivityTimer->isScheduled(); } + simtime_t getInactivityTimerArrivalTime() const { return inactivityTimer->getArrivalTime(); } + + protected: + virtual int numInitStages() const override { return 1; } + + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LOCAL) + inactivityTimer = new cMessage("blockAckInactivityTimer"); + } +}; + +Define_Module(TestHcfInactivityTimer); + +class Ieee80211BlockAckInactivityTimerTest : public cSimpleModule +{ + protected: + TestHcfInactivityTimer *hcf = nullptr; + + protected: + virtual void initialize() override + { + hcf = check_and_cast(getModuleByPath("^.hcf")); + scheduleAt(SimTime(1, SIMTIME_US), new cMessage("checkInactivityTimer")); + } + + virtual void handleMessage(cMessage *message) override + { + auto startTime = simTime(); + ASSERT(startTime > 0); + + auto originatorDeadline = startTime + SimTime(2, SIMTIME_US); + auto recipientDeadline = startTime + SimTime(5, SIMTIME_US); + hcf->updateInactivityDeadline(BlockAckAgreementRole::ORIGINATOR, originatorDeadline); + ASSERT(hcf->getOriginatorDeadline() == originatorDeadline); + ASSERT(hcf->getRecipientDeadline() == SIMTIME_MAX); + ASSERT(hcf->isInactivityTimerScheduled()); + ASSERT(hcf->getInactivityTimerArrivalTime() == originatorDeadline); + + // A later recipient deadline must not replace the earlier originator + // deadline in the shared timer. + hcf->updateInactivityDeadline(BlockAckAgreementRole::RECIPIENT, recipientDeadline); + ASSERT(hcf->getRecipientDeadline() == recipientDeadline); + ASSERT(hcf->getInactivityTimerArrivalTime() == originatorDeadline); + + // Clearing one role leaves the other role's deadline armed. + hcf->updateInactivityDeadline(BlockAckAgreementRole::ORIGINATOR, SIMTIME_MAX); + ASSERT(hcf->getOriginatorDeadline() == SIMTIME_MAX); + ASSERT(hcf->getInactivityTimerArrivalTime() == recipientDeadline); + hcf->updateInactivityDeadline(BlockAckAgreementRole::RECIPIENT, SIMTIME_MAX); + ASSERT(hcf->getRecipientDeadline() == SIMTIME_MAX); + ASSERT(!hcf->isInactivityTimerScheduled()); + + // The reverse update order still replaces the shared timer with an + // earlier deadline and preserves the later role when it is cleared. + auto reverseRecipientDeadline = startTime + SimTime(8, SIMTIME_US); + auto reverseOriginatorDeadline = startTime + SimTime(1, SIMTIME_US); + hcf->updateInactivityDeadline(BlockAckAgreementRole::RECIPIENT, reverseRecipientDeadline); + ASSERT(hcf->getInactivityTimerArrivalTime() == reverseRecipientDeadline); + hcf->updateInactivityDeadline(BlockAckAgreementRole::ORIGINATOR, reverseOriginatorDeadline); + ASSERT(hcf->getInactivityTimerArrivalTime() == reverseOriginatorDeadline); + hcf->updateInactivityDeadline(BlockAckAgreementRole::ORIGINATOR, SIMTIME_MAX); + ASSERT(hcf->getInactivityTimerArrivalTime() == reverseRecipientDeadline); + hcf->updateInactivityDeadline(BlockAckAgreementRole::RECIPIENT, SIMTIME_MAX); + ASSERT(!hcf->isInactivityTimerScheduled()); + + delete message; + endSimulation(); + } +}; + +Define_Module(Ieee80211BlockAckInactivityTimerTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; + +simple TestHcfInactivityTimer +{ + parameters: + @class(::inet::ieee80211::TestHcfInactivityTimer); +} + +simple Ieee80211BlockAckInactivityTimerTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211BlockAckInactivityTimerTest); +} + +network Ieee80211BlockAckInactivityTimerTestNetwork +{ + submodules: + hcf: TestHcfInactivityTimer; + test: Ieee80211BlockAckInactivityTimerTest; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211BlockAckInactivityTimerTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 10us +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 0523c2f0dad..8c8a9818e03 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -106,6 +106,9 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler std::vector managementPackets; Ptr managementHeader; simtime_t addbaDeadline = SIMTIME_MAX; + simtime_t originatorInactivityDeadline = SIMTIME_MAX; + simtime_t recipientInactivityDeadline = SIMTIME_MAX; + std::vector> inactivityDeadlineUpdates; std::vector cancelledTransactionIds; std::vector excludedPackets; std::vector cancelledTeardownGenerationIds; @@ -120,7 +123,13 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler managementPackets.push_back(packet); managementHeader = header; } - virtual void scheduleInactivityTimer(simtime_t) override {} + virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) override { + inactivityDeadlineUpdates.emplace_back(role, deadline); + if (role == BlockAckAgreementRole::ORIGINATOR) + originatorInactivityDeadline = deadline; + else + recipientInactivityDeadline = deadline; + } virtual void scheduleAddbaResponseTimer(simtime_t deadline) override { addbaDeadline = deadline; } virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override { cancelledTransactionIds.push_back(transactionId); @@ -140,6 +149,8 @@ class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler void addEstablishedAgreement(MacAddress receiverAddress, Tid tid) { auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(0), 64, true, false, 1, nextTransactionId++); agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(0); + agreement->calculateExpirationTime(); blockAckAgreements[std::make_pair(receiverAddress, tid)] = agreement; } void setNextDialogToken(uint8_t token) { nextDialogToken = token; } @@ -147,6 +158,7 @@ class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler void allowRetryNow(MacAddress receiverAddress, Tid tid) { addbaRetryDeadlines[std::make_pair(receiverAddress, tid)] = simTime(); } void setPendingTeardownTransaction(MacAddress receiverAddress, Tid tid, uint64_t transactionId) { pendingTeardownTransactionIds[std::make_pair(receiverAddress, tid)] = transactionId; } simtime_t getRetryDeadline(MacAddress receiverAddress, Tid tid) const { auto it = addbaRetryDeadlines.find(std::make_pair(receiverAddress, tid)); return it == addbaRetryDeadlines.end() ? SIMTIME_MAX : it->second; } + void publishInactivityDeadline(IBlockAckAgreementHandlerCallback *callback) { scheduleInactivityTimer(callback); } }; class TestTimeoutCancellationCallback : public TestCallback @@ -647,6 +659,7 @@ class TestHcf : public Hcf bool cancelManagement(uint64_t transactionId, Packet *excludedPacket = nullptr) { return cancelManagementTransaction(transactionId, excludedPacket); } void cancelTransaction(uint64_t transactionId) { cancelAddbaTransaction(transactionId, nullptr); } void cancelTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket = nullptr) { cancelBlockAckTeardown(initiator, peerAddress, tid, generationId, excludedPacket); } + bool processDroppedTeardown(Packet *packet) { return processDroppedBlockAckTeardownFrame(packet); } void processReceivedManagementFrame(const Ptr& header, bool duplicate = false) { recipientProcessReceivedManagementFrame(header, duplicate); } void processReceivedFrame(Packet *packet) { recipientProcessReceivedFrame(packet, packet->peekAtFront()); } void processRtsProtectionFailure(Packet *packet) { originatorProcessRtsProtectionFailed(packet); } @@ -673,7 +686,7 @@ class TestHcf : public Hcf virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override { numCancelledBlockAckTeardowns++; cancelledBlockAckTeardownGenerationIds.push_back(generationId); Hcf::cancelBlockAckTeardown(initiator, peerAddress, tid, generationId, excludedPacket); } virtual void rebuildPendingFrameEligibility() override { numRebuildEligibilityCalls++; Hcf::rebuildPendingFrameEligibility(); } virtual void resumeEligibleChannelAccess() override { numResumedEligibleChannelAccess++; } - virtual void scheduleInactivityTimer(simtime_t) override {} + virtual void scheduleInactivityTimer(BlockAckAgreementRole, simtime_t) override {} virtual void scheduleAddbaResponseTimer(simtime_t) override {} }; @@ -4918,6 +4931,168 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { delete currentPacket; } +// Inactivity deadlines are absolute simulation times, and expiry is +// one-shot while the tagged DELBA waits in the local transmission queues. +// IEEE Std 802.11-2024, 11.5.4 and 10.25.4. +{ + wait(1); + TestRecipientHandler recipientHandler; + TestRecipientPolicy recipientPolicy; + recipientPolicy.blockAckTimeoutValue = 3; + TestCallback setupCallback; + auto request = makeRequest(peer2, 12, 109, SequenceNumberCyclic(330), 32); + request->setBlockAckTimeoutValue(3); + auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &setupCallback, &setupCallback); + ASSERT(agreement != nullptr); + auto creationTime = simTime(); + ASSERT(creationTime > 0); + ASSERT(agreement->getExpirationTime() == creationTime + 3); + ASSERT(setupCallback.recipientInactivityDeadline == creationTime + 3); + + auto basicBlockAckReq = makeShared(); + basicBlockAckReq->setTransmitterAddress(peer2); + basicBlockAckReq->setTidInfo(12); + wait(1); + auto barReceptionTime = simTime(); + recipientHandler.blockAckReqReceived(basicBlockAckReq, &setupCallback); + ASSERT(agreement->getExpirationTime() == barReceptionTime + 3); + ASSERT(agreement->getExpirationTime() > creationTime + 3); + ASSERT(setupCallback.recipientInactivityDeadline == barReceptionTime + 3); + + wait(4); + TestCallback expiryCallback; + recipientHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + ASSERT(recipientHandler.getAgreement(12, peer2) == agreement); + ASSERT(agreement->isInactivityExpired()); + ASSERT(expiryCallback.managementPackets.size() == 1); + ASSERT(expiryCallback.recipientInactivityDeadline == SIMTIME_MAX); + + // The same overdue agreement is not emitted a second time, and a late + // data frame cannot re-arm its inactivity deadline. + recipientHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + ASSERT(expiryCallback.managementPackets.size() == 1); + auto lateQosHeader = makeQosHeader(peer2, 12, SequenceNumberCyclic(331)); + lateQosHeader->setTransmitterAddress(peer2); + lateQosHeader->setAckPolicy(AckPolicy::BLOCK_ACK); + recipientHandler.qosFrameReceived(lateQosHeader, &expiryCallback); + ASSERT(expiryCallback.recipientInactivityDeadline == SIMTIME_MAX); + TestCallback lateBarCallback; + recipientHandler.blockAckReqReceived(basicBlockAckReq, &lateBarCallback); + ASSERT(lateBarCallback.recipientInactivityDeadline == SIMTIME_MAX); + + // The originator uses the same absolute-deadline and one-shot rules. + TestOriginatorHandler originatorHandler; + originatorHandler.addEstablishedAgreement(peer1, 13); + auto originatorAgreement = originatorHandler.getAgreement(peer1, 13); + originatorAgreement->setBlockAckTimeoutValue(2); + originatorAgreement->calculateExpirationTime(); + auto originatorCreationTime = simTime(); + TestCallback originatorCallback; + originatorHandler.publishInactivityDeadline(&originatorCallback); + ASSERT(originatorCallback.originatorInactivityDeadline == originatorCreationTime + 2); + wait(3); + originatorHandler.blockAckAgreementExpired(&originatorCallback, &originatorCallback); + ASSERT(originatorHandler.getAgreement(peer1, 13) == originatorAgreement); + ASSERT(originatorAgreement->isInactivityExpired()); + ASSERT(originatorCallback.managementPackets.size() == 1); + ASSERT(originatorCallback.originatorInactivityDeadline == SIMTIME_MAX); + originatorHandler.blockAckAgreementExpired(&originatorCallback, &originatorCallback); + ASSERT(originatorCallback.managementPackets.size() == 1); + + // A role with no finite deadline publishes SIMTIME_MAX independently; + // Hcf can therefore retain a finite deadline from the other role. + TestOriginatorHandler noDeadlineOriginatorHandler; + noDeadlineOriginatorHandler.addEstablishedAgreement(peer1, 14); + TestCallback roleCallback; + roleCallback.scheduleInactivityTimer(BlockAckAgreementRole::RECIPIENT, simTime() + 10); + noDeadlineOriginatorHandler.publishInactivityDeadline(&roleCallback); + ASSERT(roleCallback.originatorInactivityDeadline == SIMTIME_MAX); + ASSERT(roleCallback.recipientInactivityDeadline == simTime() + 10); + roleCallback.scheduleInactivityTimer(BlockAckAgreementRole::RECIPIENT, SIMTIME_MAX); + ASSERT(roleCallback.recipientInactivityDeadline == SIMTIME_MAX); + +} + +// A timeout DELBA can be aborted before its final fragment is transmitted. +// The matching generation is retired locally, and the returned ownership lets +// HCF release role-specific resources without rearming a timed-out agreement. +{ + TestOriginatorHandler originatorHandler; + originatorHandler.addEstablishedAgreement(peer1, 15); + auto originatorAgreement = originatorHandler.getAgreement(peer1, 15); + originatorAgreement->setBlockAckTimeoutValue(2); + originatorAgreement->calculateExpirationTime(); + originatorAgreement->markInactivityExpired(); + auto originatorGenerationId = originatorAgreement->getTransactionId(); + auto originatorFirstFragment = makeTaggedDelbaPacket("originatorExpiredDelbaFirst", peer1, 15, SequenceNumberCyclic(340), 0, true, originatorGenerationId); + auto originatorFinalFragment = makeTaggedDelbaPacket("originatorExpiredDelbaFinal", peer1, 15, SequenceNumberCyclic(340), 1, false, originatorGenerationId); + TestCallback originatorAbortCallback; + ASSERT(originatorHandler.processTransmittedDelba(originatorFirstFragment, &originatorAbortCallback) == nullptr); + auto originatorAbortResult = originatorHandler.processAbortedDelba(originatorFinalFragment, &originatorAbortCallback); + ASSERT(originatorAbortResult.handled); + ASSERT(originatorAbortResult.terminatedAgreement != nullptr); + ASSERT(originatorAbortResult.terminatedAgreement->getTransactionId() == originatorGenerationId); + ASSERT(originatorHandler.getAgreement(peer1, 15) == nullptr); + ASSERT(originatorAbortCallback.originatorInactivityDeadline == SIMTIME_MAX); + ASSERT(originatorAbortCallback.cancelledTeardownGenerationIds.back() == originatorGenerationId); + auto staleOriginatorAbortResult = originatorHandler.processAbortedDelba(originatorFinalFragment, &originatorAbortCallback); + ASSERT(!staleOriginatorAbortResult.handled); + ASSERT(staleOriginatorAbortResult.terminatedAgreement == nullptr); + // Retiring the agreement is terminal for this generation: a late BlockAck + // must not resurrect it or publish a new inactivity deadline. + auto lateOriginatorBlockAck = makeShared(); + lateOriginatorBlockAck->setTransmitterAddress(peer1); + lateOriginatorBlockAck->setTidInfo(15); + lateOriginatorBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(340)); + TestCallback lateOriginatorActivityCallback; + originatorHandler.processReceivedBlockAck(lateOriginatorBlockAck, &lateOriginatorActivityCallback); + ASSERT(originatorHandler.getAgreement(peer1, 15) == nullptr); + ASSERT(lateOriginatorActivityCallback.originatorInactivityDeadline == SIMTIME_MAX); + ASSERT(lateOriginatorActivityCallback.inactivityDeadlineUpdates.empty()); + delete originatorFirstFragment; + delete originatorFinalFragment; + + TestRecipientHandler recipientHandler; + TestRecipientPolicy recipientPolicy; + recipientPolicy.blockAckTimeoutValue = 2; + TestCallback recipientSetupCallback; + auto recipientRequest = makeRequest(peer2, 16, 110, SequenceNumberCyclic(341), 32); + recipientRequest->setBlockAckTimeoutValue(2); + auto recipientAgreement = recipientHandler.processReceivedAddbaRequest(recipientRequest, &recipientPolicy, &recipientSetupCallback, &recipientSetupCallback); + ASSERT(recipientAgreement != nullptr); + recipientAgreement->markInactivityExpired(); + auto recipientGenerationId = recipientAgreement->getGenerationId(); + auto recipientFirstFragment = makeTaggedRecipientDelbaPacket("recipientExpiredDelbaFirst", peer2, 16, SequenceNumberCyclic(342), 0, true, recipientGenerationId); + auto recipientFinalFragment = makeTaggedRecipientDelbaPacket("recipientExpiredDelbaFinal", peer2, 16, SequenceNumberCyclic(342), 1, false, recipientGenerationId); + TestCallback recipientAbortCallback; + ASSERT(recipientHandler.processTransmittedDelba(recipientFirstFragment, &recipientAbortCallback) == nullptr); + auto recipientAbortResult = recipientHandler.processAbortedDelba(recipientFinalFragment, &recipientAbortCallback); + ASSERT(recipientAbortResult.handled); + ASSERT(recipientAbortResult.terminatedAgreement != nullptr); + ASSERT(recipientAbortResult.terminatedAgreement->getGenerationId() == recipientGenerationId); + ASSERT(recipientHandler.getAgreement(16, peer2) == nullptr); + ASSERT(recipientAbortCallback.recipientInactivityDeadline == SIMTIME_MAX); + ASSERT(recipientAbortCallback.cancelledTeardownGenerationIds.back() == recipientGenerationId); + auto staleRecipientAbortResult = recipientHandler.processAbortedDelba(recipientFinalFragment, &recipientAbortCallback); + ASSERT(!staleRecipientAbortResult.handled); + ASSERT(staleRecipientAbortResult.terminatedAgreement == nullptr); + // Likewise, late recipient traffic cannot recreate a retired agreement. + auto lateRecipientQosHeader = makeQosHeader(peer1, 16, SequenceNumberCyclic(343)); + lateRecipientQosHeader->setTransmitterAddress(peer2); + lateRecipientQosHeader->setAckPolicy(AckPolicy::BLOCK_ACK); + auto lateRecipientBlockAckReq = makeShared(); + lateRecipientBlockAckReq->setTransmitterAddress(peer2); + lateRecipientBlockAckReq->setTidInfo(16); + TestCallback lateRecipientActivityCallback; + recipientHandler.qosFrameReceived(lateRecipientQosHeader, &lateRecipientActivityCallback); + recipientHandler.blockAckReqReceived(lateRecipientBlockAckReq, &lateRecipientActivityCallback); + ASSERT(recipientHandler.getAgreement(16, peer2) == nullptr); + ASSERT(lateRecipientActivityCallback.recipientInactivityDeadline == SIMTIME_MAX); + ASSERT(lateRecipientActivityCallback.inactivityDeadlineUpdates.empty()); + delete recipientFirstFragment; + delete recipientFinalFragment; +} + // HCF routes recipient DELBAs to the recipient owner and rebuilds its // eligibility index when replacement or final teardown changes the generation. // The replacement callback removes queued stale siblings; the active current @@ -4958,6 +5133,40 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { hcf.configureManagementCallback(&managementCallback); hcf.observePendingQueue(&pendingQueue); + // HCF owns the terminal cleanup when a fragmented timeout DELBA is + // aborted before its final fragment is transmitted. The handler returns + // the retired agreement, allowing HCF to emit the deletion signal and to + // release recipient reorder resources exactly once. + TestSignalListener expiredDeletionListener; + hcf.subscribe(Hcf::blockAckAgreementDeletedSignal, &expiredDeletionListener); + originatorHandler->addEstablishedAgreement(peer1, 15); + auto expiredOriginatorAgreement = originatorHandler->getAgreement(peer1, 15); + expiredOriginatorAgreement->markInactivityExpired(); + auto expiredOriginatorGenerationId = expiredOriginatorAgreement->getTransactionId(); + auto expiredOriginatorDelba = makeTaggedDelbaPacket("hcfExpiredOriginatorDelba", peer1, 15, SequenceNumberCyclic(324), 1, false, expiredOriginatorGenerationId); + ASSERT(hcf.processDroppedTeardown(expiredOriginatorDelba)); + ASSERT(originatorHandler->getAgreement(peer1, 15) == nullptr); + delete expiredOriginatorDelba; + + auto expiredRecipientRequest = makeRequest(peer2, 15, 105, SequenceNumberCyclic(325), 32); + auto expiredRecipientAgreement = recipientHandler->processReceivedAddbaRequest(expiredRecipientRequest, &hcfRecipientPolicy, &managementCallback, &managementCallback); + ASSERT(expiredRecipientAgreement != nullptr); + expiredRecipientAgreement->markInactivityExpired(); + auto expiredRecipientGenerationId = expiredRecipientAgreement->getGenerationId(); + auto expiredRecipientDelba = makeTaggedRecipientDelbaPacket("hcfExpiredRecipientDelba", peer2, 15, SequenceNumberCyclic(326), 1, false, expiredRecipientGenerationId); + ASSERT(hcf.processDroppedTeardown(expiredRecipientDelba)); + ASSERT(recipientHandler->getAgreement(15, peer2) == nullptr); + ASSERT(recipientDataService.numReorderingResets == 1); + ASSERT(expiredDeletionListener.numSignals == 2); + delete expiredRecipientDelba; + hcf.unsubscribe(Hcf::blockAckAgreementDeletedSignal, &expiredDeletionListener); + // Keep the following generation-lifecycle assertions focused on their + // own transactions rather than on the setup cleanup above. + hcf.numCancelledBlockAckTeardowns = 0; + hcf.cancelledBlockAckTeardownGenerationIds.clear(); + hcf.numRebuildEligibilityCalls = 0; + recipientDataService.numReorderingResets = 0; + auto stalePacket = makeTaggedRecipientDelbaPacket("hcfStaleRecipientDelba", peer2, 11, SequenceNumberCyclic(321), 0, false, firstGenerationId); pendingQueue.enqueuePacket(stalePacket); hcf.trackFrame(stalePacket, AC_BE); From 4d117d71d6b66034cc1cfe524ebf0fd2defdb47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:04 +0200 Subject: [PATCH 35/56] ieee80211: make fragment reassembly generation-safe Replace the fragment-zero reset heuristic with generation-aware reassembly state. Track extended sequence generations per receive flow, preserve later-only fragments, and sort completed fragments by fragment number instead of arrival order. Quarantine half-space ambiguity, retain tombstones for retired sequences, recover when a raw sequence number is reused after wrap, and reject completion when contradictory terminal fragment numbers were observed. Stale fragments therefore cannot corrupt a newer MSDU. Cover out-of-order delivery, duplicates, ambiguous generations, sequence-number wrap, delayed stale fragments, and contradictory terminal markers. --- .../blockackreordering/BlockAckReordering.cc | 19 +- .../blockackreordering/BlockAckReordering.h | 6 + .../mac/fragmentation/BasicReassembly.cc | 274 +++++++++-- .../mac/fragmentation/BasicReassembly.h | 51 +- tests/unit/Ieee80211AddbaTransaction_1.test | 434 +++++++++++++++++- 5 files changed, 736 insertions(+), 48 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 1dbff9f3f17..c70bfcb570b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -7,11 +7,24 @@ #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" +#include + #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" namespace inet { namespace ieee80211 { +BlockAckReordering::Fragments BlockAckReordering::sortFragmentsByFragmentNumber(const Fragments& fragments) +{ + auto sortedFragments = fragments; + std::stable_sort(sortedFragments.begin(), sortedFragments.end(), [](const Packet *first, const Packet *second) { + auto firstFragmentNumber = first->peekAtFront()->getFragmentNumber(); + auto secondFragmentNumber = second->peekAtFront()->getFragmentNumber(); + return firstFragmentNumber < secondFragmentNumber; + }); + return sortedFragments; +} + // // The recipient flushes received MSDUs from its receive buffer as described in this subclause. [...] // @@ -113,7 +126,7 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::collectCompletePrecedingMp auto fragments = it.second; if (SequenceNumberCyclic(sequenceNumber) < startingSequenceNumber) if (isComplete(fragments)) - completePrecedingMpdus[sequenceNumber] = fragments; + completePrecedingMpdus[sequenceNumber] = sortFragmentsByFragmentNumber(fragments); } return completePrecedingMpdus; } @@ -140,7 +153,7 @@ bool BlockAckReordering::addMsduIfComplete(ReceiveBuffer *receiveBuffer, Reorder if (it != buffer.end()) { auto fragments = it->second; if (isComplete(fragments)) { - reorderBuffer[seqNum.get()] = fragments; + reorderBuffer[seqNum.get()] = sortFragmentsByFragmentNumber(fragments); return true; } } @@ -232,7 +245,7 @@ std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists } } } - return earliestFragments; + return sortFragmentsByFragmentNumber(earliestFragments); } BlockAckReordering::~BlockAckReordering() diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index 014f2c0a318..5e02a452d44 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -26,10 +26,16 @@ class INET_API BlockAckReordering typedef std::vector Fragments; typedef std::map ReorderBuffer; + // Values in ReorderBuffer results from processReceivedQoSFrame() and + // processReceivedBlockAckReq() contain complete fragment vectors ordered + // by ascending Fragment Number. The vectors only reorder packet pointers; + // packet ownership remains with the caller of those methods. + protected: std::map, ReceiveBuffer *> receiveBuffers; protected: + static Fragments sortFragmentsByFragmentNumber(const Fragments& fragments); ReorderBuffer collectCompletePrecedingMpdus(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); ReorderBuffer collectConsecutiveCompleteFollowingMpdus(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc index 0b000dab927..23986c4361e 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc @@ -17,40 +17,202 @@ namespace ieee80211 { Register_Class(BasicReassembly); -/* - * FIXME this function needs a serious review - */ +BasicReassembly::SequenceSpaceKey BasicReassembly::getSequenceSpaceKey(const ContextKey& contextKey) const +{ + // IEEE Std 802.11-2024, Table 10-5 defines one baseline sequence-number + // space for frames not covered by a more specific space, and a separate + // space for individually addressed QoS Data indexed by RA and TID. + if (contextKey.type == ST_DATA_WITH_QOS && !contextKey.receiverAddress.isMulticast()) + return { contextKey.macAddress, contextKey.receiverAddress, contextKey.tid }; + else + return { contextKey.macAddress, MacAddress::UNSPECIFIED_ADDRESS, -1 }; +} + +SequenceNumber BasicReassembly::getRawSequenceNumber(ExtendedSequenceNumber extendedSequenceNumber) +{ + auto rawSequenceNumber = extendedSequenceNumber % NUM_SEQUENCE_NUMBERS; + if (rawSequenceNumber < 0) + rawSequenceNumber += NUM_SEQUENCE_NUMBERS; + return static_cast(rawSequenceNumber); +} + +BasicReassembly::SequenceObservation BasicReassembly::observeSequenceNumber(const SequenceSpaceKey& sequenceSpaceKey, SequenceNumber sequenceNumber) +{ + auto sequenceSpaceIt = sequenceSpacesMap.find(sequenceSpaceKey); + if (sequenceSpaceIt == sequenceSpacesMap.end()) { + sequenceSpacesMap.emplace(sequenceSpaceKey, SequenceSpaceValue{ sequenceNumber }); + return { sequenceNumber, false }; + } + + auto& highWatermark = sequenceSpaceIt->second.highWatermark; + auto highWaterSequenceNumber = getRawSequenceNumber(highWatermark); + auto highWaterCyclic = SequenceNumberCyclic(highWaterSequenceNumber); + auto sequenceCyclic = SequenceNumberCyclic(sequenceNumber); + + if (sequenceCyclic == highWaterCyclic) + return { highWatermark, false }; + + // SequenceNumberCyclic uses the IEEE 802.11 half-space ordering: a + // sequence number is ahead when its forward distance is in 1..2047. + if (highWaterCyclic < sequenceCyclic) { + auto forwardDistance = (sequenceNumber - highWaterSequenceNumber + NUM_SEQUENCE_NUMBERS) % NUM_SEQUENCE_NUMBERS; + ASSERT(forwardDistance > 0 && forwardDistance < NUM_SEQUENCE_NUMBERS / 2); + highWatermark += forwardDistance; + return { highWatermark, false }; + } + else if (sequenceCyclic < highWaterCyclic) { + auto backwardDistance = (highWaterSequenceNumber - sequenceNumber + NUM_SEQUENCE_NUMBERS) % NUM_SEQUENCE_NUMBERS; + ASSERT(backwardDistance > 0 && backwardDistance < NUM_SEQUENCE_NUMBERS / 2); + return { highWatermark - backwardDistance, false }; + } + + // Exactly half a sequence-number space has no ordering under the cyclic + // comparison. A fragmented MPDU must not be assigned a generation on + // guesswork. An unfragmented MPDU is safe to accept because it is never + // combined with buffered fragments; leave the high-water unchanged. + return { highWatermark, true }; +} + +void BasicReassembly::pruneExpiredSequenceNumbers(const SequenceSpaceKey& sequenceSpaceKey) +{ + auto sequenceSpaceIt = sequenceSpacesMap.find(sequenceSpaceKey); + if (sequenceSpaceIt == sequenceSpacesMap.end()) + return; + auto oldestRetainedGeneration = sequenceSpaceIt->second.highWatermark - NUM_SEQUENCE_NUMBERS; + for (auto contextIt = expiredSequenceNumbersMap.begin(); contextIt != expiredSequenceNumbersMap.end();) { + if (!(getSequenceSpaceKey(contextIt->first) == sequenceSpaceKey)) { + ++contextIt; + continue; + } + auto& expiredSequenceNumbers = contextIt->second; + for (auto sequenceIt = expiredSequenceNumbers.begin(); sequenceIt != expiredSequenceNumbers.end();) { + if (*sequenceIt < oldestRetainedGeneration) + sequenceIt = expiredSequenceNumbers.erase(sequenceIt); + else + ++sequenceIt; + } + if (expiredSequenceNumbers.empty()) + contextIt = expiredSequenceNumbersMap.erase(contextIt); + else + ++contextIt; + } +} + Packet *BasicReassembly::addFragment(Packet *packet) { const auto& header = packet->peekAtFront(); - // Frame is not fragmented - if (!header->getMoreFragments() && header->getFragmentNumber() == 0) - return packet; - // find entry for this frame - Key key; - key.macAddress = header->getTransmitterAddress(); - key.receiverAddress = header->getReceiverAddress(); - key.type = header->getType(); - key.tid = -1; + ContextKey contextKey; + contextKey.macAddress = header->getTransmitterAddress(); + contextKey.receiverAddress = header->getReceiverAddress(); + contextKey.type = header->getType(); + contextKey.tid = -1; if (header->getType() == ST_DATA_WITH_QOS) if (const Ptr& qosDataHeader = dynamicPtrCast(header)) - key.tid = qosDataHeader->getTid(); - key.seqNum = header->getSequenceNumber().get(); + contextKey.tid = qosDataHeader->getTid(); short fragNum = header->getFragmentNumber(); ASSERT(fragNum >= 0 && fragNum < MAX_NUM_FRAGMENTS); + bool isFragmented = header->getMoreFragments() || fragNum != 0; + auto sequenceSpaceKey = getSequenceSpaceKey(contextKey); + // This observed-epoch policy can separate only wraps for which sequence + // progress was received. A completely unseen wrap is indistinguishable + // from the same generation with the MAC metadata available here, so an + // equal raw sequence remains in the current observed epoch to preserve + // valid out-of-order completion. + auto sequenceObservation = observeSequenceNumber(sequenceSpaceKey, header->getSequenceNumber().get()); + + if (sequenceObservation.ambiguous) { + if (isFragmented) { + delete packet; + return nullptr; + } + return packet; + } + + Key key; + key.macAddress = contextKey.macAddress; + key.receiverAddress = contextKey.receiverAddress; + key.type = contextKey.type; + key.tid = contextKey.tid; + key.extendedSequenceNumber = sequenceObservation.extendedSequenceNumber; + auto it = fragmentsMap.find(key); - if (it != fragmentsMap.end() && it->second.expired) { - // A non-Retry fragment 0 can be a new MMPDU after sequence-number - // reuse. All other fragments of the expired MMPDU are discarded. - if (fragNum == 0 && !header->getRetry()) { + + auto expiredIt = expiredSequenceNumbersMap.find(contextKey); + if (isFragmented && expiredIt != expiredSequenceNumbersMap.end() && + expiredIt->second.find(key.extendedSequenceNumber) != expiredIt->second.end()) { + // A tombstone is scoped to the observed extended generation. Keep it + // even when a non-Retry fragment 0 arrives: without an observed + // sequence-space advance that fragment is not distinguishable from a + // late fragment of the expired MPDU. + delete packet; + return nullptr; + } + + // An unfragmented MPDU is never assembled with a fragmented MPDU. A new + // non-Retry frame retires an active same-generation reassembly, while a + // Retry leaves it available for its original fragments. + if (!isFragmented) { + if (it != fragmentsMap.end() && !header->getRetry()) { + for (auto fragment : it->second.fragments) + if (fragment != nullptr) + delete fragment; fragmentsMap.erase(it); - it = fragmentsMap.end(); } - else { - delete packet; - return nullptr; + pruneExpiredSequenceNumbers(sequenceSpaceKey); + return packet; + } + + // A same-raw active or expired entry in another observed epoch makes all + // fragments of the new epoch ambiguous, including fragment 0. Quarantine + // the new extended identity rather than allowing a delayed old fragment + // to seed a hybrid frame. IEEE Std 802.11-2024, 9.2.4.1.6, 10.3.2.14.2, + // and 10.5 + // define the modulo-4096 sequence identity, fragment identity, and + // receive-lifetime discard boundary; the following recovery policy keeps + // the ambiguous generation rejected while allowing a later observed + // generation to make progress. A fragmented non-Retry fragment 0 is the + // only marker that can retire older same-raw tombstones, and only when no + // active same-raw generation remains. A tombstone for the incoming + // generation is never cleared by this path. + bool hasOtherGeneration = false; + for (const auto& entry : fragmentsMap) { + const auto& otherKey = entry.first; + if (otherKey.getContextKey() == contextKey && + getRawSequenceNumber(otherKey.extendedSequenceNumber) == header->getSequenceNumber().get() && + otherKey.extendedSequenceNumber != key.extendedSequenceNumber) { + hasOtherGeneration = true; + break; + } + } + bool isNewGenerationMarker = fragNum == 0 && !header->getRetry(); + if (!hasOtherGeneration && isNewGenerationMarker && expiredIt != expiredSequenceNumbersMap.end()) { + for (auto sequenceIt = expiredIt->second.begin(); sequenceIt != expiredIt->second.end();) { + if (*sequenceIt < key.extendedSequenceNumber && + getRawSequenceNumber(*sequenceIt) == header->getSequenceNumber().get()) + sequenceIt = expiredIt->second.erase(sequenceIt); + else + ++sequenceIt; + } + if (expiredIt->second.empty()) + expiredSequenceNumbersMap.erase(expiredIt); + expiredIt = expiredSequenceNumbersMap.find(contextKey); + } + if (!hasOtherGeneration && expiredIt != expiredSequenceNumbersMap.end()) { + for (auto extendedSequenceNumber : expiredIt->second) { + if (extendedSequenceNumber != key.extendedSequenceNumber && + getRawSequenceNumber(extendedSequenceNumber) == header->getSequenceNumber().get()) { + hasOtherGeneration = true; + break; + } } } + if (hasOtherGeneration) { + expiredSequenceNumbersMap[contextKey].insert(key.extendedSequenceNumber); + pruneExpiredSequenceNumbers(sequenceSpaceKey); + delete packet; + return nullptr; + } + if (it == fragmentsMap.end()) { Value value; value.receptionStartTime = simTime(); @@ -61,18 +223,25 @@ Packet *BasicReassembly::addFragment(Packet *packet) // update entry uint16_t fragmentBit = 1 << fragNum; - value.receivedFragments |= fragmentBit; - if (!header->getMoreFragments()) - value.allFragments = (fragmentBit << 1) - 1; - if (!value.fragments[fragNum]) + if (!header->getMoreFragments()) { + if (value.terminalFragmentNumber == -1) + value.terminalFragmentNumber = fragNum; + else if (value.terminalFragmentNumber != fragNum) + value.hasContradictoryTerminalFragmentNumbers = true; + } + if (!value.fragments[fragNum]) { + value.receivedFragments |= fragmentBit; + if (!header->getMoreFragments() && value.allFragments == 0) + value.allFragments = (fragmentBit << 1) - 1; value.fragments[fragNum] = packet; + } else delete packet; // MacAddress txAddress = header->getTransmitterAddress(); // if all fragments arrived, return assembled frame - if (value.allFragments != 0 && value.allFragments == value.receivedFragments) { + if (!value.hasContradictoryTerminalFragmentNumbers && value.allFragments != 0 && value.allFragments == value.receivedFragments) { Defragmentation defragmentation; value.fragments.erase(std::remove(value.fragments.begin(), value.fragments.end(), nullptr), value.fragments.end()); auto defragmentedFrame = defragmentation.defragmentFrames(&value.fragments); @@ -82,47 +251,55 @@ Packet *BasicReassembly::addFragment(Packet *packet) for (auto fragment : value.fragments) delete fragment; fragmentsMap.erase(key); + pruneExpiredSequenceNumbers(sequenceSpaceKey); return defragmentedFrame; } - else + else { + pruneExpiredSequenceNumbers(sequenceSpaceKey); return nullptr; + } } simtime_t BasicReassembly::getNextExpirationTime() const { simtime_t nextExpirationTime = SIMTIME_MAX; for (const auto& entry : fragmentsMap) - if (!entry.second.expired) - nextExpirationTime = std::min(nextExpirationTime, entry.second.receptionStartTime + maxReceiveLifetime); + nextExpirationTime = std::min(nextExpirationTime, entry.second.receptionStartTime + maxReceiveLifetime); return nextExpirationTime; } std::vector BasicReassembly::removeExpiredFragments(simtime_t currentTime) { std::vector expiredFragments; - for (auto& entry : fragmentsMap) { + for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { + auto& entry = *it; auto& value = entry.second; - if (!value.expired && currentTime >= value.receptionStartTime + maxReceiveLifetime) { + if (currentTime >= value.receptionStartTime + maxReceiveLifetime) { for (auto fragment : value.fragments) if (fragment != nullptr) expiredFragments.push_back(fragment); - value.fragments.clear(); - value.receivedFragments = 0; - value.allFragments = 0; - value.expired = true; + auto contextKey = entry.first.getContextKey(); + auto sequenceSpaceKey = getSequenceSpaceKey(contextKey); + expiredSequenceNumbersMap[contextKey].insert(entry.first.extendedSequenceNumber); + it = fragmentsMap.erase(it); + pruneExpiredSequenceNumbers(sequenceSpaceKey); } + else + ++it; } return expiredFragments; } void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) { - for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { - auto sequenceNumber = it->first.seqNum; - bool isInSequenceRange = startSeqNumber <= endSeqNumber ? + auto isInSequenceRange = [startSeqNumber, endSeqNumber](int sequenceNumber) { + return startSeqNumber <= endSeqNumber ? sequenceNumber >= startSeqNumber && sequenceNumber <= endSeqNumber : sequenceNumber >= startSeqNumber || sequenceNumber <= endSeqNumber; - if (it->first.macAddress == address && it->first.tid == tid && isInSequenceRange) { + }; + for (auto it = fragmentsMap.begin(); it != fragmentsMap.end();) { + auto sequenceNumber = getRawSequenceNumber(it->first.extendedSequenceNumber); + if (it->first.macAddress == address && it->first.tid == tid && isInSequenceRange(sequenceNumber)) { for (auto fragment : it->second.fragments) delete fragment; it = fragmentsMap.erase(it); @@ -130,6 +307,23 @@ void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumb else it++; } + + for (auto it = expiredSequenceNumbersMap.begin(); it != expiredSequenceNumbersMap.end();) { + if (it->first.macAddress == address && it->first.tid == tid) { + for (auto sequenceIt = it->second.begin(); sequenceIt != it->second.end();) { + if (isInSequenceRange(getRawSequenceNumber(*sequenceIt))) + sequenceIt = it->second.erase(sequenceIt); + else + ++sequenceIt; + } + if (it->second.empty()) + it = expiredSequenceNumbersMap.erase(it); + else + ++it; + } + else + ++it; + } } BasicReassembly::~BasicReassembly() diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h index cdcfd693727..e100f6ba74f 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h @@ -8,6 +8,8 @@ #ifndef __INET_BASICREASSEMBLY_H #define __INET_BASICREASSEMBLY_H +#include +#include #include #include "inet/common/packet/Packet.h" @@ -21,13 +23,37 @@ namespace ieee80211 { class INET_API BasicReassembly : public IReassembly, public cObject { protected: + static constexpr int NUM_SEQUENCE_NUMBERS = 1 << 12; + using ExtendedSequenceNumber = int64_t; + + struct ContextKey { + MacAddress macAddress; + MacAddress receiverAddress; + Ieee80211FrameType type; + Tid tid; + auto asTuple() const { return std::tie(macAddress, receiverAddress, type, tid); } + bool operator==(const ContextKey& other) const { return asTuple() == other.asTuple(); } + bool operator<(const ContextKey& other) const { return asTuple() < other.asTuple(); } + }; + struct SequenceSpaceKey { + MacAddress transmitterAddress; + MacAddress receiverAddress; + Tid tid; + auto asTuple() const { return std::tie(transmitterAddress, receiverAddress, tid); } + bool operator==(const SequenceSpaceKey& other) const { return asTuple() == other.asTuple(); } + bool operator<(const SequenceSpaceKey& other) const { return asTuple() < other.asTuple(); } + }; + struct SequenceSpaceValue { + ExtendedSequenceNumber highWatermark; + }; struct Key { MacAddress macAddress; MacAddress receiverAddress; Ieee80211FrameType type; Tid tid; - SequenceNumber seqNum; - auto asTuple() const { return std::tie(macAddress, receiverAddress, type, tid, seqNum); } + ExtendedSequenceNumber extendedSequenceNumber; + auto asTuple() const { return std::tie(macAddress, receiverAddress, type, tid, extendedSequenceNumber); } + ContextKey getContextKey() const { return { macAddress, receiverAddress, type, tid }; } bool operator==(const Key& other) const { return asTuple() == other.asTuple(); } bool operator<(const Key& other) const { return asTuple() < other.asTuple(); } }; @@ -35,13 +61,32 @@ class INET_API BasicReassembly : public IReassembly, public cObject std::vector fragments; uint16_t receivedFragments = 0; // each bit corresponds to a fragment number uint16_t allFragments = 0; // bits for all fragments set to one (0..numFragments-1); 0 means unfilled + int terminalFragmentNumber = -1; + bool hasContradictoryTerminalFragmentNumbers = false; simtime_t receptionStartTime; - bool expired = false; }; typedef std::map FragmentsMap; + typedef std::map SequenceSpacesMap; + using ExpiredSequenceNumbers = std::set; + // Generation-scoped tombstones reject late or ambiguous fragments and + // retain identities across one raw-sequence reuse. Older entries are + // pruned as the observed sequence-space high-water advances. + typedef std::map ExpiredSequenceNumbersMap; FragmentsMap fragmentsMap; + SequenceSpacesMap sequenceSpacesMap; + ExpiredSequenceNumbersMap expiredSequenceNumbersMap; simtime_t maxReceiveLifetime; + struct SequenceObservation { + ExtendedSequenceNumber extendedSequenceNumber; + bool ambiguous = false; + }; + + SequenceSpaceKey getSequenceSpaceKey(const ContextKey& contextKey) const; + SequenceObservation observeSequenceNumber(const SequenceSpaceKey& sequenceSpaceKey, SequenceNumber sequenceNumber); + void pruneExpiredSequenceNumbers(const SequenceSpaceKey& sequenceSpaceKey); + static SequenceNumber getRawSequenceNumber(ExtendedSequenceNumber extendedSequenceNumber); + public: BasicReassembly() : maxReceiveLifetime(SimTime(512 * 1024, SIMTIME_US)) {} BasicReassembly(simtime_t maxReceiveLifetime) : maxReceiveLifetime(maxReceiveLifetime) {} diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 8c8a9818e03..99b39c46bc3 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -11,6 +11,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/common/TimeTag_m.h" #include "inet/common/LabelsTag_m.h" #include "inet/common/packet/chunk/ByteCountChunk.h" +#include "inet/common/packet/chunk/BytesChunk.h" #include "inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.h" #include "inet/linklayer/ieee80211/mac/aggregation/MsduAggregation.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" @@ -531,6 +532,29 @@ class TestInProgressFrames : public InProgressFrames } }; +class TestBasicReassembly : public BasicReassembly +{ + public: + using BasicReassembly::BasicReassembly; + + size_t getNumActiveReassemblies() const { return fragmentsMap.size(); } + bool hasContradictoryTerminalFragmentNumbers() const { + ASSERT(fragmentsMap.size() == 1); + return fragmentsMap.begin()->second.hasContradictoryTerminalFragmentNumbers; + } + uint16_t getAllFragments() const { + ASSERT(fragmentsMap.size() == 1); + return fragmentsMap.begin()->second.allFragments; + } + size_t getNumExpiredContexts() const { return expiredSequenceNumbersMap.size(); } + size_t getNumExpiredSequenceNumbers() const { + size_t count = 0; + for (const auto& entry : expiredSequenceNumbersMap) + count += entry.second.size(); + return count; + } +}; + class TestEdcaf : public Edcaf { public: @@ -2131,8 +2155,9 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(fragmentedCallback.managementPackets.size() == 1); delete fragments; - // Seeing the final fragment first does not imply completeness. Dispatch - // occurs only when the missing lower-numbered fragment later arrives. + // IEEE Std 802.11-2024, 10.5 permits fragments to be received out of + // order. The final fragment therefore starts a reassembly and fragment 0 + // completes it when it arrives later. auto outOfOrderFragments = makeFragments("outOfOrderAddbaRequest", 4, 61); for (auto& fragment : *outOfOrderFragments) fragment = convertToWireRepresentation(fragment); @@ -2142,6 +2167,13 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(fragmentedCallback.managementPackets.size() == 1); auto outOfOrderCompleteReception = receiveFragment(outOfOrderFragments->at(0)); ASSERT(outOfOrderCompleteReception.completeHeader != nullptr); + ASSERT(outOfOrderCompleteReception.completeHeader->getFragmentNumber() == 0); + ASSERT(!outOfOrderCompleteReception.completeHeader->getMoreFragments()); + auto outOfOrderRequest = dynamicPtrCast(outOfOrderCompleteReception.completeHeader); + ASSERT(outOfOrderRequest != nullptr); + ASSERT(outOfOrderRequest->getTid() == 4); + ASSERT(outOfOrderRequest->getDialogToken() == 74); + ASSERT(outOfOrderRequest->getStartingSequenceNumber() == SequenceNumberCyclic(104)); hcf.processReceivedManagementFrame(outOfOrderCompleteReception.completeHeader); ASSERT(recipientHandler->getAgreement(4, peer2) != nullptr); ASSERT(fragmentedCallback.managementPackets.size() == 2); @@ -2368,6 +2400,83 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; delete discardedNewWindowFrames.front(); } +// IEEE Std 802.11-2024, 10.4 and 10.5: fragments are transmitted from +// fragment 0 upward and the destination reconstructs an MMPDU in Fragment +// Number order. BlockAckReordering receives fragments in arrival order, so +// both immediate and BAR-triggered complete vectors must be normalized before +// they reach BasicReassembly. +{ + auto makeQosFragment = [&](const char *name, SequenceNumberCyclic sequenceNumber, Tid tid, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer2, tid, sequenceNumber); + dataHeader->setTransmitterAddress(peer1); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto assertFragmentOrder = [](const BlockAckReordering::Fragments& fragments) { + ASSERT(fragments.size() == 4); + for (int fragmentNumber = 0; fragmentNumber < 4; fragmentNumber++) + ASSERT(fragments.at(fragmentNumber)->peekAtFront()->getFragmentNumber() == fragmentNumber); + }; + auto reassembleOrderedFragments = [&](const BlockAckReordering::Fragments& fragments, const std::vector& expectedPayload) { + TestBasicReassembly reassembly; + Packet *completedPacket = nullptr; + int numCompletedPackets = 0; + for (auto fragment : fragments) { + auto packet = reassembly.addFragment(fragment); + if (packet != nullptr) { + completedPacket = packet; + numCompletedPackets++; + } + } + ASSERT(numCompletedPackets == 1); + ASSERT(completedPacket != nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + auto completedHeader = completedPacket->peekAtFront(); + ASSERT(completedPacket->peekAt(completedHeader->getChunkLength(), B(expectedPayload.size()))->getBytes() == expectedPayload); + delete completedPacket; + }; + + RecipientBlockAckAgreement immediateAgreement(peer1, 6, SequenceNumberCyclic(100), 64, 0); + TestBlockAckReordering immediateReordering; + BlockAckReordering::ReorderBuffer immediateFrames; + for (int fragmentNumber : { 1, 2, 3, 0 }) { + auto packet = makeQosFragment("immediateOutOfOrderFragment", SequenceNumberCyclic(100), 6, fragmentNumber, fragmentNumber != 3, 0x40 + fragmentNumber); + auto dataHeader = packet->peekAtFront(); + auto frames = immediateReordering.processReceivedQoSFrame(&immediateAgreement, packet, dataHeader); + if (fragmentNumber == 0) + immediateFrames = frames; + else + ASSERT(frames.empty()); + } + ASSERT(immediateFrames.size() == 1); + auto immediateFragments = immediateFrames.at(100); + assertFragmentOrder(immediateFragments); + reassembleOrderedFragments(immediateFragments, { 0x40, 0x41, 0x42, 0x43 }); + + RecipientBlockAckAgreement barAgreement(peer1, 6, SequenceNumberCyclic(200), 64, 0); + TestBlockAckReordering barReordering; + for (int fragmentNumber : { 1, 2, 3, 0 }) { + auto packet = makeQosFragment("barOutOfOrderFragment", SequenceNumberCyclic(201), 6, fragmentNumber, fragmentNumber != 3, 0x50 + fragmentNumber); + auto dataHeader = packet->peekAtFront(); + ASSERT(barReordering.processReceivedQoSFrame(&barAgreement, packet, dataHeader).empty()); + } + auto barRequest = makeShared(); + barRequest->setTransmitterAddress(peer1); + barRequest->setReceiverAddress(peer2); + barRequest->setTidInfo(6); + barRequest->setStartingSequenceNumber(SequenceNumberCyclic(201)); + auto barFrames = barReordering.processReceivedBlockAckReq(&barAgreement, barRequest); + ASSERT(barFrames.size() == 1); + auto barFragments = barFrames.at(201); + assertFragmentOrder(barFragments); + reassembleOrderedFragments(barFragments, { 0x50, 0x51, 0x52, 0x53 }); +} + // The recipient data service owns reorder-buffer reset observability: every // buffered MPDU is reported exactly once before it is deleted. { @@ -4800,6 +4909,327 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { pendingQueues[ac].unsubscribe(queueing::IPacketQueue::packetQueueDepartureSignal, &hcf); } +// IEEE Std 802.11-2024, 10.5 requires reconstruction in Fragment Number +// order. A later fragment can arrive before fragment 0, and the receiver's +// observed sequence-number generation keeps a later sequence reuse separate. +{ + TestBasicReassembly reassembly; + auto makeResponseFragments = [&](const char *name, uint8_t dialogToken, uint16_t statusCode, const std::vector& payload, int sequenceNumber, int fragmentCount) { + auto response = makeResponse(peer1, 3, dialogToken, statusCode); + response->setReceiverAddress(peer2); + response->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + auto packet = new Packet(name, response); + packet->insertAtBack(makeShared(payload)); + packet->insertAtBack(makeShared()); + auto managementHeaderLength = makeShared()->getChunkLength(); + auto bodyLength = (int)(response->getChunkLength() - managementHeaderLength).get() + (int)payload.size(); + Fragmentation fragmentation; + auto fragmentSizes = std::vector({ 8, bodyLength - 8 }); + if (fragmentCount == 3) + fragmentSizes = { 4, 4, bodyLength - 8 }; + ASSERT(fragmentCount == (int)fragmentSizes.size()); + return fragmentation.fragmentFrame(packet, fragmentSizes); + }; + + const std::vector outOfOrderPayload = { 0x14, 0x15, 0x16, 0x17 }; + auto outOfOrderFragments = makeResponseFragments("reassemblyOutOfOrderResponse", 23, 5, outOfOrderPayload, 4094, 2); + ASSERT(reassembly.addFragment(outOfOrderFragments->at(1)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto outOfOrderExpiration = reassembly.getNextExpirationTime(); + ASSERT(outOfOrderExpiration != SIMTIME_MAX); + auto retryOutOfOrderFirst = outOfOrderFragments->at(0)->dup(); + auto retryOutOfOrderHeader = retryOutOfOrderFirst->removeAtFront(); + retryOutOfOrderHeader->setRetry(true); + retryOutOfOrderFirst->insertAtFront(retryOutOfOrderHeader); + auto outOfOrderCompleted = reassembly.addFragment(retryOutOfOrderFirst); + ASSERT(outOfOrderCompleted != nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + auto outOfOrderHeader = outOfOrderCompleted->peekAtFront(); + auto outOfOrderResponse = dynamicPtrCast(outOfOrderHeader); + ASSERT(outOfOrderResponse != nullptr); + ASSERT(outOfOrderResponse->getDialogToken() == 23); + ASSERT(outOfOrderResponse->getStatusCode() == 5); + ASSERT(outOfOrderResponse->getRetry()); + ASSERT(outOfOrderCompleted->peekAt(outOfOrderResponse->getChunkLength(), B(outOfOrderPayload.size()))->getBytes() == outOfOrderPayload); + delete outOfOrderCompleted; + delete outOfOrderFragments->at(0); + delete outOfOrderFragments; + + // A sequence number exactly half a space away has no cyclic ordering. + // Neither an independently accepted unfragmented frame nor a rejected + // fragmented frame at that boundary may disturb an active generation. + const std::vector halfSpacePayload = { 0x18, 0x19, 0x1a }; + auto preservedFragments = makeResponseFragments("reassemblyPreservedResponse", 25, 6, halfSpacePayload, 4093, 2); + ASSERT(reassembly.addFragment(preservedFragments->at(1)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto halfSpaceStandalone = makeTaggedManagementPacket("reassemblyHalfSpaceStandalone", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(2046), 0, false, 0); + ASSERT(reassembly.addFragment(halfSpaceStandalone) == halfSpaceStandalone); + delete halfSpaceStandalone; + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto halfSpaceFragments = makeResponseFragments("reassemblyHalfSpaceResponse", 27, 7, halfSpacePayload, 2046, 2); + ASSERT(reassembly.addFragment(halfSpaceFragments->at(0)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto preservedCompleted = reassembly.addFragment(preservedFragments->at(0)); + ASSERT(preservedCompleted != nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + delete preservedCompleted; + delete preservedFragments; + delete halfSpaceFragments->at(1); + delete halfSpaceFragments; + + // The old final fragment remains buffered while the receiver observes the + // numeric wrap and enough baseline sequence-space progress to identify a + // reused raw number. A delayed old fragment 0 and the real new fragment 0 + // are indistinguishable, so their new extended generation is quarantined. + const std::vector oldPayload = { 0x20, 0x21, 0x22, 0x23 }; + auto oldFragments = makeResponseFragments("reassemblyOldResponse", 17, 1, oldPayload, 4095, 2); + auto lateOldFirstFragment = oldFragments->at(0)->dup(); + auto lateOldRetryFragment = oldFragments->at(0)->dup(); + auto lateOldRetryHeader = lateOldRetryFragment->removeAtFront(); + lateOldRetryHeader->setRetry(true); + lateOldRetryFragment->insertAtFront(lateOldRetryHeader); + ASSERT(reassembly.addFragment(oldFragments->at(1)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto oldExpiration = reassembly.getNextExpirationTime(); + wait(1e-9); + for (int sequenceNumber = 0; sequenceNumber <= 2048; sequenceNumber++) { + auto standalone = makeTaggedManagementPacket("reassemblySequenceAdvance", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(sequenceNumber), 0, false, sequenceNumber); + ASSERT(reassembly.addFragment(standalone) == standalone); + delete standalone; + } + + const std::vector newPayload = { 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6 }; + auto newFragments = makeResponseFragments("reassemblyNewResponse", 29, 37, newPayload, 4095, 3); + ASSERT(reassembly.addFragment(lateOldFirstFragment) == nullptr); + ASSERT(reassembly.addFragment(lateOldRetryFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + ASSERT(reassembly.addFragment(newFragments->at(0)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.addFragment(newFragments->at(1)) == nullptr); + ASSERT(reassembly.addFragment(newFragments->at(2)) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto expiredOld = reassembly.removeExpiredFragments(oldExpiration); + ASSERT(expiredOld.size() == 1); + delete expiredOld.front(); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 2); + delete oldFragments->at(0); + delete oldFragments; + delete newFragments; + + // Once the old active entry and the quarantined generation are gone, the + // receiver can observe one more modulo-4096 wrap. The later non-Retry + // fragment 0 retires the older same-raw tombstone without an external + // purge, and only the later generation's bytes are reconstructed. + for (int sequenceNumber = 0; sequenceNumber <= 2048; sequenceNumber++) { + auto standalone = makeTaggedManagementPacket("reassemblySecondSequenceAdvance", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(sequenceNumber), 0, false, sequenceNumber); + ASSERT(reassembly.addFragment(standalone) == standalone); + delete standalone; + } + auto recoveredFragments = makeResponseFragments("reassemblyRecoveredResponse", 29, 37, newPayload, 4095, 3); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + ASSERT(reassembly.addFragment(recoveredFragments->at(0)) == nullptr); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 0); + ASSERT(reassembly.addFragment(recoveredFragments->at(1)) == nullptr); + auto completed = reassembly.addFragment(recoveredFragments->at(2)); + ASSERT(completed != nullptr); + auto completedHeader = completed->peekAtFront(); + auto completedResponse = dynamicPtrCast(completedHeader); + ASSERT(completedResponse != nullptr); + ASSERT(completedResponse->getDialogToken() == 29); + ASSERT(completedResponse->getStatusCode() == 37); + ASSERT(completedResponse->getSequenceNumber() == SequenceNumberCyclic(4095)); + ASSERT(completedResponse->getFragmentNumber() == 0); + ASSERT(!completedResponse->getMoreFragments()); + ASSERT(completed->peekAt(completedResponse->getChunkLength(), B(newPayload.size()))->getBytes() == newPayload); + delete completed; + delete recoveredFragments; +} + +// A terminal duplicate on an occupied nonterminal slot is still observed for +// contradiction tracking, but cannot establish an affirmative completion mask. +{ + TestBasicReassembly reassembly(SimTime(1, SIMTIME_US)); + auto firstFragment = makeTaggedManagementPacket("reassemblyDuplicateTerminalFirst", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(601), 0, true, 1); + auto secondFragment = makeTaggedManagementPacket("reassemblyDuplicateTerminalSecond", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(601), 1, true, 2); + auto duplicateTerminal = makeTaggedManagementPacket("reassemblyDuplicateTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(601), 1, false, 3); + ASSERT(reassembly.addFragment(firstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto expiration = reassembly.getNextExpirationTime(); + ASSERT(expiration != SIMTIME_MAX); + ASSERT(reassembly.getAllFragments() == 0); + ASSERT(reassembly.addFragment(secondFragment) == nullptr); + ASSERT(reassembly.addFragment(duplicateTerminal) == nullptr); + ASSERT(!reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getAllFragments() == 0); + ASSERT(reassembly.getNextExpirationTime() == expiration); + auto expiredFragments = reassembly.removeExpiredFragments(expiration); + ASSERT(expiredFragments.size() == 2); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + for (auto fragment : expiredFragments) + delete fragment; +} + +// A terminal duplicate on an occupied slot with a different number from the +// retained terminal marker invalidates completion, even when all slots arrive. +{ + TestBasicReassembly reassembly(SimTime(1, SIMTIME_US)); + auto retainedTerminal = makeTaggedManagementPacket("reassemblyRetainedTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(602), 2, false, 1); + auto retainedFragment = makeTaggedManagementPacket("reassemblyRetainedFragment", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(602), 1, true, 2); + auto duplicateTerminal = makeTaggedManagementPacket("reassemblyContradictoryDuplicateTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(602), 1, false, 3); + auto firstFragment = makeTaggedManagementPacket("reassemblyContradictoryDuplicateFirst", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(602), 0, true, 4); + ASSERT(reassembly.addFragment(retainedTerminal) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto expiration = reassembly.getNextExpirationTime(); + ASSERT(expiration != SIMTIME_MAX); + ASSERT(reassembly.getAllFragments() == 7); + ASSERT(reassembly.addFragment(retainedFragment) == nullptr); + ASSERT(reassembly.addFragment(duplicateTerminal) == nullptr); + ASSERT(reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getAllFragments() == 7); + ASSERT(reassembly.getNextExpirationTime() == expiration); + ASSERT(reassembly.addFragment(firstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getNextExpirationTime() == expiration); + auto expiredFragments = reassembly.removeExpiredFragments(expiration); + ASSERT(expiredFragments.size() == 3); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + for (auto fragment : expiredFragments) + delete fragment; +} + +// Contradictory terminal markers invalidate completion independent of their +// arrival order. The invalid entry remains owned until receive-lifetime +// expiry, which returns each uniquely retained fragment to the caller. +for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) }) { + TestBasicReassembly reassembly(SimTime(1, SIMTIME_US)); + auto firstTerminal = makeTaggedManagementPacket("reassemblyContradictoryFirstTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(600), terminalOrder.first, false, 1); + auto secondTerminal = makeTaggedManagementPacket("reassemblyContradictorySecondTerminal", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(600), terminalOrder.second, false, 2); + auto firstFragment = makeTaggedManagementPacket("reassemblyContradictoryFirstFragment", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(600), 0, true, 3); + ASSERT(reassembly.addFragment(firstTerminal) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(!reassembly.hasContradictoryTerminalFragmentNumbers()); + auto expiration = reassembly.getNextExpirationTime(); + ASSERT(expiration != SIMTIME_MAX); + ASSERT(reassembly.addFragment(secondTerminal) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getNextExpirationTime() == expiration); + ASSERT(reassembly.addFragment(firstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + ASSERT(reassembly.hasContradictoryTerminalFragmentNumbers()); + ASSERT(reassembly.getNextExpirationTime() == expiration); + + auto expiredFragments = reassembly.removeExpiredFragments(expiration); + ASSERT(expiredFragments.size() == 3); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNextExpirationTime() == SIMTIME_MAX); + for (auto fragment : expiredFragments) + delete fragment; +} + +// Expired reassembly stores exact observed generations, rejects late +// fragments, and does not clear a tombstone when a same-generation fragment 0 +// arrives without an observed sequence-space advance. Unfragmented frames are +// accepted independently and never mixed with the tombstone. +{ + TestBasicReassembly reassembly(SimTime(1, SIMTIME_US)); + auto firstFragment = makeTaggedManagementPacket("reassemblyExpiredFirst", ST_ACTION, peer1, peer2, SequenceNumberCyclic(430), 0, true, 0); + ASSERT(reassembly.addFragment(firstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 1); + auto expiredFragments = reassembly.removeExpiredFragments(simTime() + SimTime(2, SIMTIME_US)); + ASSERT(expiredFragments.size() == 1); + delete expiredFragments.front(); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredContexts() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + + // A receive-lifetime tombstone does not require an external purge for + // recovery. Once the receiver observes a later generation in the same + // sequence space, its non-Retry fragment 0 retires the older tombstone. + { + TestBasicReassembly wrapRecovery(SimTime(1, SIMTIME_US)); + auto expiredFirst = makeTaggedManagementPacket("reassemblyExpiredWrapFirst", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(430), 0, true, 0); + ASSERT(wrapRecovery.addFragment(expiredFirst) == nullptr); + auto expiration = wrapRecovery.getNextExpirationTime(); + auto expired = wrapRecovery.removeExpiredFragments(expiration); + ASSERT(expired.size() == 1); + delete expired.front(); + for (int sequenceNumber = 431; sequenceNumber <= 2479; sequenceNumber++) { + auto standalone = makeTaggedManagementPacket("reassemblyExpiredWrapAdvance", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(sequenceNumber), 0, false, sequenceNumber); + ASSERT(wrapRecovery.addFragment(standalone) == standalone); + delete standalone; + } + auto recoveredFirst = makeTaggedManagementPacket("reassemblyExpiredWrapRecoveredFirst", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(430), 0, true, 1); + ASSERT(wrapRecovery.addFragment(recoveredFirst) == nullptr); + ASSERT(wrapRecovery.getNumExpiredSequenceNumbers() == 0); + ASSERT(wrapRecovery.getNumActiveReassemblies() == 1); + // Leave the recovered, incomplete reassembly for the destructor to own + // and clean up; completion is covered by the exact-payload recovery test above. + } + + auto lateFinalFragment = makeTaggedManagementPacket("reassemblyLateFinal", ST_ACTION, peer1, peer2, SequenceNumberCyclic(430), 1, false, 0, true); + ASSERT(reassembly.addFragment(lateFinalFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + auto reusedFirstFragment = makeTaggedManagementPacket("reassemblyReusedFirst", ST_ACTION, peer1, peer2, SequenceNumberCyclic(430), 0, true, 0); + ASSERT(reassembly.addFragment(reusedFirstFragment) == nullptr); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredContexts() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + auto unfragmentedPacket = makeTaggedManagementPacket("reassemblyUnfragmentedAfterExpiry", ST_ACTION, peer1, peer2, + SequenceNumberCyclic(430), 0, false, 0); + ASSERT(reassembly.addFragment(unfragmentedPacket) == unfragmentedPacket); + delete unfragmentedPacket; + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 1); + for (int sequenceNumber = 0; sequenceNumber < 64; sequenceNumber++) { + auto fragment = makeTaggedManagementPacket("reassemblyManyExpiredFirst", ST_ACTION, peer1, peer2, SequenceNumberCyclic(500 + sequenceNumber), 0, true, 0); + ASSERT(reassembly.addFragment(fragment) == nullptr); + } + ASSERT(reassembly.getNumActiveReassemblies() == 64); + auto manyExpiredFragments = reassembly.removeExpiredFragments(simTime() + SimTime(3, SIMTIME_US)); + ASSERT(manyExpiredFragments.size() == 64); + for (auto fragment : manyExpiredFragments) + delete fragment; + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNextExpirationTime() == SIMTIME_MAX); + ASSERT(reassembly.getNumExpiredContexts() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 65); + auto otherContextFirstFragment = makeTaggedManagementPacket("reassemblyOtherContextFirst", ST_ACTION, peer2, peer3, + SequenceNumberCyclic(600), 0, true, 0); + ASSERT(reassembly.addFragment(otherContextFirstFragment) == nullptr); + auto otherContextExpiredFragments = reassembly.removeExpiredFragments(simTime() + SimTime(4, SIMTIME_US)); + ASSERT(otherContextExpiredFragments.size() == 1); + delete otherContextExpiredFragments.front(); + ASSERT(reassembly.getNumActiveReassemblies() == 0); + ASSERT(reassembly.getNumExpiredContexts() == 2); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 66); + reassembly.purge(peer2, -1, 0, 4095); + ASSERT(reassembly.getNumExpiredContexts() == 1); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 65); + reassembly.purge(peer1, -1, 0, 4095); + ASSERT(reassembly.getNumExpiredContexts() == 0); + ASSERT(reassembly.getNumExpiredSequenceNumbers() == 0); +} + // An originator inactivity DELBA retains the agreement transaction identity. // Once that agreement is replaced, every stale fragment is rejected while a // final DELBA carrying the current generation removes the replacement once. From 5f1c3bf4f1c7aaf93a00b814ba24765f77bd0efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:04 +0200 Subject: [PATCH 36/56] ieee80211: enforce receive lifetime during Block Ack reordering Record an immutable receive deadline when the first fragment is retained in a Block Ack receive buffer. Return inserted, released, and expired frames explicitly so recipient services, rather than the reorder buffer, own every final drop, signal, and deletion. Drive reordering and scalar reassembly from one receive-lifetime timer. Expired late fragments remain visible to Block Ack bookkeeping but cannot seed a new reassembly context, while peer and TID reset purges every owned fragment without leaking or deleting it twice. Return ownership from IReassembly::purge(), reject negative maxReceiveLifetime values while keeping zero valid, and cover expiry, reset, wrap recovery, fragmented sequences, and timer rescheduling. --- .../blockackreordering/BlockAckReordering.cc | 79 ++++-- .../blockackreordering/BlockAckReordering.h | 11 +- .../mac/blockackreordering/ReceiveBuffer.cc | 103 +++++++- .../mac/blockackreordering/ReceiveBuffer.h | 26 +- .../ieee80211/mac/contract/IReassembly.h | 9 +- .../mac/fragmentation/BasicReassembly.cc | 7 +- .../mac/fragmentation/BasicReassembly.h | 2 +- .../mac/recipient/RecipientMacDataService.cc | 5 +- .../mac/recipient/RecipientMacDataService.ned | 2 +- .../recipient/RecipientQosMacDataService.cc | 89 +++++-- .../recipient/RecipientQosMacDataService.h | 7 +- .../recipient/RecipientQosMacDataService.ned | 2 +- tests/unit/Ieee80211AddbaTransaction_1.test | 242 +++++++++++++++++- 13 files changed, 517 insertions(+), 67 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index c70bfcb570b..3f4801ab76f 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -28,14 +28,16 @@ BlockAckReordering::Fragments BlockAckReordering::sortFragmentsByFragmentNumber( // // The recipient flushes received MSDUs from its receive buffer as described in this subclause. [...] // -BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader) +BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceivedQoSFrameWithResult(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader) { + QosFrameProcessingResult result; ReceiveBuffer *receiveBuffer = createReceiveBufferIfNecessary(agreement); // The reception of QoS data frames using Normal Ack policy shall not be used by the // recipient to reset the timer to detect Block Ack timeout (see 10.5.4). // This allows the recipient to delete the Block Ack if the originator does not switch // back to using Block Ack. - if (receiveBuffer->insertFrame(dataPacket, dataHeader)) { + auto insertionResult = receiveBuffer->insertFrameWithResult(dataPacket, dataHeader); + if (insertionResult == ReceiveBuffer::FrameInsertionResult::INSERTED) { if (dataHeader->getAckPolicy() == BLOCK_ACK) agreement->blockAckPolicyFrameReceived(dataHeader); auto earliestCompleteMsduOrAMsdu = getEarliestCompleteMsduOrAMsduIfExists(receiveBuffer); @@ -45,20 +47,41 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedQoSFrame(Re // sequence number shall be passed up to the next MAC process. if (receiveBuffer->isFull()) { passedUp(agreement, receiveBuffer, earliestSequenceNumber); - return ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); + result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); + return result; } // If, after an MPDU is received, the receive buffer is not full, but the sequence number of the complete MSDU or // A-MSDU in the buffer with the lowest sequence number is equal to the NextExpectedSequenceNumber for // that Block Ack agreement, then the MPDU shall be passed up to the next MAC process. else if (earliestSequenceNumber == receiveBuffer->getNextExpectedSequenceNumber()) { passedUp(agreement, receiveBuffer, earliestSequenceNumber); - return ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); + result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); + return result; } } } - else + else { + if (insertionResult == ReceiveBuffer::FrameInsertionResult::REJECTED_EXPIRED) { + // A later fragment of a receive-timer-expired MPDU is still a + // received BLOCK_ACK frame, even though the fragment is rejected + // by the tombstone and must be reported to the service for drop + // observability. + if (dataHeader->getAckPolicy() == BLOCK_ACK) + agreement->blockAckPolicyFrameReceived(dataHeader); + result.tombstonedFragments.push_back(dataPacket); + return result; + } delete dataPacket; - return ReorderBuffer({}); + } + return result; +} + +BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader) +{ + auto result = processReceivedQoSFrameWithResult(agreement, dataPacket, dataHeader); + for (auto packet : result.tombstonedFragments) + delete packet; + return result.frames; } // @@ -125,7 +148,7 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::collectCompletePrecedingMp auto sequenceNumber = it.first; auto fragments = it.second; if (SequenceNumberCyclic(sequenceNumber) < startingSequenceNumber) - if (isComplete(fragments)) + if (ReceiveBuffer::isComplete(fragments)) completePrecedingMpdus[sequenceNumber] = sortFragmentsByFragmentNumber(fragments); } return completePrecedingMpdus; @@ -152,7 +175,7 @@ bool BlockAckReordering::addMsduIfComplete(ReceiveBuffer *receiveBuffer, Reorder auto it = buffer.find(seqNum.get()); if (it != buffer.end()) { auto fragments = it->second; - if (isComplete(fragments)) { + if (ReceiveBuffer::isComplete(fragments)) { reorderBuffer[seqNum.get()] = sortFragmentsByFragmentNumber(fragments); return true; } @@ -168,19 +191,6 @@ void BlockAckReordering::releaseReceiveBuffer(RecipientBlockAckAgreement *agreem } } -bool BlockAckReordering::isComplete(const std::vector& fragments) -{ - int largestFragmentNumber = -1; - std::set fragNums; // possible duplicate frames - for (auto fragment : fragments) { - const auto& header = fragment->peekAtFront(); - if (!header->getMoreFragments()) - largestFragmentNumber = header->getFragmentNumber(); - fragNums.insert(header->getFragmentNumber()); - } - return largestFragmentNumber != -1 && largestFragmentNumber + 1 == (int)fragNums.size(); -} - ReceiveBuffer *BlockAckReordering::createReceiveBufferIfNecessary(RecipientBlockAckAgreement *agreement) { SequenceNumberCyclic startingSequenceNumber = agreement->getStartingSequenceNumber(); @@ -198,6 +208,29 @@ ReceiveBuffer *BlockAckReordering::createReceiveBufferIfNecessary(RecipientBlock return it->second; } +simtime_t BlockAckReordering::getNextExpirationTime() const +{ + simtime_t nextExpirationTime = SIMTIME_MAX; + for (const auto& [id, receiveBuffer] : receiveBuffers) + nextExpirationTime = std::min(nextExpirationTime, receiveBuffer->getNextExpirationTime(maxReceiveLifetime)); + return nextExpirationTime; +} + +std::vector BlockAckReordering::removeExpiredFragments(simtime_t currentTime) +{ + std::vector expiredFragments; + for (const auto& [id, receiveBuffer] : receiveBuffers) { + // IEEE Std 802.11-2024, 10.5 requires incomplete fragmented MPDUs + // to be discarded when dot11MaxReceiveLifetime elapses. The receive + // timer does not advance NextExpectedSequenceNumber or alter the + // Block Ack record; those variables are progressed by normal Data/BAR + // processing, and the scoreboard is independent of reassembly state. + auto bufferExpiredFragments = receiveBuffer->removeExpiredFragments(currentTime, maxReceiveLifetime); + expiredFragments.insert(expiredFragments.end(), bufferExpiredFragments.begin(), bufferExpiredFragments.end()); + } + return expiredFragments; +} + std::vector BlockAckReordering::resetReceiveBuffer(Tid tid, MacAddress originatorAddr) { std::vector frames; @@ -228,7 +261,7 @@ std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists SequenceNumberCyclic earliestSeqNum = SequenceNumberCyclic(0); const auto& buffer = receiveBuffer->getBuffer(); for (auto it : buffer) { - if (isComplete(it.second)) { + if (ReceiveBuffer::isComplete(it.second)) { earliestFragments = it.second; earliestSeqNum = earliestFragments.at(0)->peekAtFront()->getSequenceNumber(); break; @@ -238,7 +271,7 @@ std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists for (auto it : buffer) { SequenceNumberCyclic currentSeqNum = it.second.at(0)->peekAtFront()->getSequenceNumber(); if (currentSeqNum < earliestSeqNum) { - if (isComplete(it.second)) { + if (ReceiveBuffer::isComplete(it.second)) { earliestFragments = it.second; earliestSeqNum = currentSeqNum; } diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index 5e02a452d44..8dd641c7439 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -33,6 +33,7 @@ class INET_API BlockAckReordering protected: std::map, ReceiveBuffer *> receiveBuffers; + simtime_t maxReceiveLifetime = SIMTIME_MAX; protected: static Fragments sortFragmentsByFragmentNumber(const Fragments& fragments); @@ -40,16 +41,24 @@ class INET_API BlockAckReordering ReorderBuffer collectConsecutiveCompleteFollowingMpdus(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); std::vector getEarliestCompleteMsduOrAMsduIfExists(ReceiveBuffer *receiveBuffer); - bool isComplete(const Fragments& fragments); void passedUp(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber); void releaseReceiveBuffer(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, const ReorderBuffer& reorderBuffer); ReceiveBuffer *createReceiveBufferIfNecessary(RecipientBlockAckAgreement *agreement); bool addMsduIfComplete(ReceiveBuffer *receiveBuffer, ReorderBuffer& reorderBuffer, SequenceNumberCyclic seqNum); public: + struct QosFrameProcessingResult { + ReorderBuffer frames; + Fragments tombstonedFragments; + }; + + explicit BlockAckReordering(simtime_t maxReceiveLifetime = SIMTIME_MAX) : maxReceiveLifetime(maxReceiveLifetime) {} virtual ~BlockAckReordering(); std::vector resetReceiveBuffer(Tid tid, MacAddress originatorAddr); + simtime_t getNextExpirationTime() const; + std::vector removeExpiredFragments(simtime_t currentTime); + QosFrameProcessingResult processReceivedQoSFrameWithResult(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader); ReorderBuffer processReceivedQoSFrame(RecipientBlockAckAgreement *agreement, Packet *dataPacket, const Ptr& dataHeader); ReorderBuffer processReceivedBlockAckReq(RecipientBlockAckAgreement *agreement, const Ptr& blockAckReq); }; diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc index 3bf4115a4b1..f886058cf5c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h" +#include + namespace inet { namespace ieee80211 { @@ -16,16 +18,55 @@ ReceiveBuffer::ReceiveBuffer(int bufferSize, SequenceNumberCyclic nextExpectedSe { } +bool ReceiveBuffer::isComplete(const Fragments& fragments) +{ + FragmentNumber terminalFragmentNumber = -1; + bool hasContradictoryTerminalMarkers = false; + std::set fragmentNumbers; + for (auto fragment : fragments) { + if (fragment == nullptr) + continue; + const auto& header = fragment->peekAtFront(); + if (!header->getMoreFragments()) { + if (terminalFragmentNumber == -1) + terminalFragmentNumber = header->getFragmentNumber(); + else if (terminalFragmentNumber != header->getFragmentNumber()) + hasContradictoryTerminalMarkers = true; + } + fragmentNumbers.insert(header->getFragmentNumber()); + } + if (terminalFragmentNumber < 0 || hasContradictoryTerminalMarkers || fragmentNumbers.size() != (size_t)terminalFragmentNumber + 1) + return false; + for (FragmentNumber fragmentNumber = 0; fragmentNumber <= terminalFragmentNumber; fragmentNumber++) + if (fragmentNumbers.find(fragmentNumber) == fragmentNumbers.end()) + return false; + return true; +} + +void ReceiveBuffer::pruneExpiredFragmentSequences() +{ + for (auto it = expiredFragmentSequences.begin(); it != expiredFragmentSequences.end();) { + if (SequenceNumberCyclic(*it) < nextExpectedSequenceNumber) + it = expiredFragmentSequences.erase(it); + else + ++it; + } +} + // // Upon the receipt of a QoS data frame from the originator for which a Block Ack agreement exists, the recipient // buffers the MSDU regardless of the value of the Ack Policy subfield within the QoS Control field of the QoS // data frame, unless the sequence number of the frame is older than the NextExpectedSequenceNumber for that // Block Ack agreement, in which case the frame is discarded because it is either old or a duplicate. // -bool ReceiveBuffer::insertFrame(Packet *dataPacket, const Ptr& dataHeader) +ReceiveBuffer::FrameInsertionResult ReceiveBuffer::insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader) { auto sequenceNumber = dataHeader->getSequenceNumber(); auto fragmentNumber = dataHeader->getFragmentNumber(); + bool isFragmented = dataHeader->getMoreFragments() || fragmentNumber != 0; + pruneExpiredFragmentSequences(); + if (isFragmented && expiredFragmentSequences.find(sequenceNumber.get()) != expiredFragmentSequences.end()) + return FrameInsertionResult::REJECTED_EXPIRED; // The total number of MPDUs in these MSDUs may not // exceed the reorder buffer size in the receiver. if (length < bufferSize && nextExpectedSequenceNumber <= sequenceNumber && sequenceNumber < nextExpectedSequenceNumber + bufferSize) { @@ -36,19 +77,26 @@ bool ReceiveBuffer::insertFrame(Packet *dataPacket, const PtrpeekAtFront(); if (fragmentHeader->getSequenceNumber() == sequenceNumber && fragmentHeader->getFragmentNumber() == fragmentNumber) - return false; + return FrameInsertionResult::REJECTED; } fragments.push_back(dataPacket); } else { buffer[sequenceNumber.get()].push_back(dataPacket); + bufferEntries[sequenceNumber.get()] = { simTime(), isFragmented, false }; } // The total number of frames that can be sent depends on the total // number of MPDUs in all the outstanding MSDUs. length++; - return true; + // Once an entry has received a fragmented MPDU, keep that identity + // tied to the generation even if a later malformed or + // unfragmented-shaped header is accepted into the same sequence slot. + auto& bufferEntry = bufferEntries[sequenceNumber.get()]; + bufferEntry.hasFragmentedIdentity |= isFragmented; + bufferEntry.receiveLifetimeActive = bufferEntry.hasFragmentedIdentity && !isComplete(buffer[sequenceNumber.get()]); + return FrameInsertionResult::INSERTED; } - return false; + return FrameInsertionResult::REJECTED; } void ReceiveBuffer::dropFramesUntil(SequenceNumberCyclic sequenceNumber) @@ -59,6 +107,7 @@ void ReceiveBuffer::dropFramesUntil(SequenceNumberCyclic sequenceNumber) length -= it->second.size(); for (auto fragment : it->second) delete fragment; + bufferEntries.erase(it->first); it = buffer.erase(it); } else @@ -71,6 +120,7 @@ void ReceiveBuffer::removeFrame(SequenceNumberCyclic sequenceNumber) auto it = buffer.find(sequenceNumber.get()); if (it != buffer.end()) { length -= it->second.size(); + bufferEntries.erase(sequenceNumber.get()); buffer.erase(sequenceNumber.get()); } else @@ -83,10 +133,55 @@ ReceiveBuffer::Fragments ReceiveBuffer::extractFrames() for (auto& [sequenceNumber, fragments] : buffer) frames.insert(frames.end(), fragments.begin(), fragments.end()); buffer.clear(); + bufferEntries.clear(); + expiredFragmentSequences.clear(); length = 0; return frames; } +simtime_t ReceiveBuffer::getNextExpirationTime(simtime_t maxReceiveLifetime) const +{ + if (maxReceiveLifetime == SIMTIME_MAX) + return SIMTIME_MAX; + simtime_t nextExpirationTime = SIMTIME_MAX; + for (const auto& [sequenceNumber, entry] : bufferEntries) + if (entry.receiveLifetimeActive) + nextExpirationTime = std::min(nextExpirationTime, entry.receptionStartTime + maxReceiveLifetime); + return nextExpirationTime; +} + +ReceiveBuffer::Fragments ReceiveBuffer::removeExpiredFragments(simtime_t currentTime, simtime_t maxReceiveLifetime) +{ + Fragments expiredFragments; + if (maxReceiveLifetime == SIMTIME_MAX) + return expiredFragments; + for (auto it = buffer.begin(); it != buffer.end();) { + auto metadataIt = bufferEntries.find(it->first); + ASSERT(metadataIt != bufferEntries.end()); + auto& entry = metadataIt->second; + if (entry.receiveLifetimeActive && currentTime >= entry.receptionStartTime + maxReceiveLifetime) { + for (auto fragment : it->second) + if (fragment != nullptr) + expiredFragments.push_back(fragment); + length -= it->second.size(); + ASSERT(length >= 0); + expiredFragmentSequences.insert(it->first); + bufferEntries.erase(metadataIt); + it = buffer.erase(it); + } + else + ++it; + } + pruneExpiredFragmentSequences(); + return expiredFragments; +} + +void ReceiveBuffer::setNextExpectedSequenceNumber(SequenceNumberCyclic nextExpectedSequenceNumber) +{ + this->nextExpectedSequenceNumber = nextExpectedSequenceNumber; + pruneExpiredFragmentSequences(); +} + ReceiveBuffer::~ReceiveBuffer() { for (auto fragments : buffer) { diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h index 84e8fd5b6c5..f6af968c519 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h @@ -8,6 +8,9 @@ #ifndef __INET_RECEIVEBUFFER_H #define __INET_RECEIVEBUFFER_H +#include +#include + #include "inet/common/packet/Packet.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" @@ -20,9 +23,23 @@ class INET_API ReceiveBuffer public: typedef std::vector Fragments; typedef std::map ReorderBuffer; + enum class FrameInsertionResult { + INSERTED, + REJECTED, + REJECTED_EXPIRED + }; + static bool isComplete(const Fragments& fragments); protected: + struct BufferEntry { + simtime_t receptionStartTime; + bool hasFragmentedIdentity; + bool receiveLifetimeActive; + }; + ReorderBuffer buffer; + std::map bufferEntries; + std::set expiredFragmentSequences; // For each Block Ack agreement, the recipient maintains a MAC variable NextExpectedSequenceNumber. The // NextExpectedSequenceNumber is initialized to to the value of the Starting Block Ack Starting Sequence // Control field of the ADDBA Request frame of the accepted Block Ack agreement. (IEEE 802.11­-11/0381r0) @@ -30,20 +47,25 @@ class INET_API ReceiveBuffer int length = 0; SequenceNumberCyclic nextExpectedSequenceNumber; + void pruneExpiredFragmentSequences(); + public: ReceiveBuffer(int bufferSize, SequenceNumberCyclic nextExpectedSequenceNumber); virtual ~ReceiveBuffer(); - bool insertFrame(Packet *dataPacket, const Ptr& dataHeader); + FrameInsertionResult insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader); + bool insertFrame(Packet *dataPacket, const Ptr& dataHeader) { return insertFrameWithResult(dataPacket, dataHeader) == FrameInsertionResult::INSERTED; } void dropFramesUntil(SequenceNumberCyclic sequenceNumber); void removeFrame(SequenceNumberCyclic sequenceNumber); Fragments extractFrames(); + simtime_t getNextExpirationTime(simtime_t maxReceiveLifetime) const; + Fragments removeExpiredFragments(simtime_t currentTime, simtime_t maxReceiveLifetime); const ReorderBuffer& getBuffer() { return buffer; } int getLength() { return length; } int getBufferSize() { return bufferSize; } SequenceNumberCyclic getNextExpectedSequenceNumber() { return nextExpectedSequenceNumber; } - void setNextExpectedSequenceNumber(SequenceNumberCyclic nextExpectedSequenceNumber) { this->nextExpectedSequenceNumber = nextExpectedSequenceNumber; } + void setNextExpectedSequenceNumber(SequenceNumberCyclic nextExpectedSequenceNumber); bool isFull() { ASSERT(length <= bufferSize); return length == bufferSize; } }; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h index 14285b3689a..a2187b76f32 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IReassembly.h @@ -50,11 +50,12 @@ class INET_API IReassembly virtual std::vector removeExpiredFragments(simtime_t currentTime) = 0; /** - * Discard fragments from the reassembly buffer. Frames are identified by the transmitter - * address, the TID, and the sequence number range [startSeqNumber, endSeqNumber[. - * Set tid=-1 for non-QoS frames. + * Discard fragments from the reassembly buffer and return the detached + * fragments to the caller. Frames are identified by the transmitter + * address, the TID, and the inclusive sequence number range + * [startSeqNumber, endSeqNumber]. Set tid=-1 for non-QoS frames. */ - virtual void purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) = 0; + virtual std::vector purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) = 0; }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc index 23986c4361e..1639fdd3c8a 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.cc @@ -290,8 +290,9 @@ std::vector BasicReassembly::removeExpiredFragments(simtime_t currentT return expiredFragments; } -void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) +std::vector BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) { + std::vector purgedFragments; auto isInSequenceRange = [startSeqNumber, endSeqNumber](int sequenceNumber) { return startSeqNumber <= endSeqNumber ? sequenceNumber >= startSeqNumber && sequenceNumber <= endSeqNumber : @@ -301,7 +302,8 @@ void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumb auto sequenceNumber = getRawSequenceNumber(it->first.extendedSequenceNumber); if (it->first.macAddress == address && it->first.tid == tid && isInSequenceRange(sequenceNumber)) { for (auto fragment : it->second.fragments) - delete fragment; + if (fragment != nullptr) + purgedFragments.push_back(fragment); it = fragmentsMap.erase(it); } else @@ -324,6 +326,7 @@ void BasicReassembly::purge(const MacAddress& address, int tid, int startSeqNumb else ++it; } + return purgedFragments; } BasicReassembly::~BasicReassembly() diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h index e100f6ba74f..9d9d572bc08 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h @@ -92,7 +92,7 @@ class INET_API BasicReassembly : public IReassembly, public cObject BasicReassembly(simtime_t maxReceiveLifetime) : maxReceiveLifetime(maxReceiveLifetime) {} virtual ~BasicReassembly(); virtual Packet *addFragment(Packet *packet) override; - virtual void purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) override; + virtual std::vector purge(const MacAddress& address, int tid, int startSeqNumber, int endSeqNumber) override; virtual simtime_t getNextExpirationTime() const override; virtual std::vector removeExpiredFragments(simtime_t currentTime) override; }; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc index e903e860596..73df71b3c27 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.cc @@ -18,8 +18,11 @@ Define_Module(RecipientMacDataService); void RecipientMacDataService::initialize() { + simtime_t maxReceiveLifetime = par("maxReceiveLifetime"); + if (maxReceiveLifetime < SIMTIME_ZERO) + throw cRuntimeError("maxReceiveLifetime must not be negative"); duplicateRemoval = new LegacyDuplicateRemoval(); - basicReassembly = new BasicReassembly(par("maxReceiveLifetime")); + basicReassembly = new BasicReassembly(maxReceiveLifetime); reassemblyTimer = new cMessage("reassemblyTimer"); } diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned index 8e2b3b35f88..aaa4a8cb177 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.ned @@ -19,7 +19,7 @@ module RecipientMacDataService extends Module { parameters: @class(RecipientMacDataService); - double maxReceiveLifetime @unit(s) = default(524288us); // IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs + double maxReceiveLifetime @unit(s) = default(524288us); // Nonnegative relative receive lifetime; IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs @display("i=block/join"); @signal[packetDefragmented](type=inet::Packet); @signal[packetDeaggregated](type=inet::Packet); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index d7efe6322bd..624c38c448e 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -23,23 +23,26 @@ Define_Module(RecipientQosMacDataService); // TODO refactor to avoid code duplication void RecipientQosMacDataService::initialize() { + maxReceiveLifetime = par("maxReceiveLifetime"); + if (maxReceiveLifetime < SIMTIME_ZERO) + throw cRuntimeError("maxReceiveLifetime must not be negative"); duplicateRemoval = new QoSDuplicateRemoval(); - basicReassembly = new BasicReassembly(par("maxReceiveLifetime")); + basicReassembly = new BasicReassembly(maxReceiveLifetime); aMsduDeaggregation = new MsduDeaggregation(); aMpduDeaggregation = new MpduDeaggregation(); - blockAckReordering = new BlockAckReordering(); - reassemblyTimer = new cMessage("reassemblyTimer"); + blockAckReordering = new BlockAckReordering(maxReceiveLifetime); + receiveLifetimeTimer = new cMessage("receiveLifetimeTimer"); } void RecipientQosMacDataService::handleMessage(cMessage *message) { - if (message != reassemblyTimer) + if (message != receiveLifetimeTimer) throw cRuntimeError("Unknown message"); - expireReassemblyFragments(); - scheduleReassemblyTimer(); + expireReceiveLifetime(); + scheduleReceiveLifetimeTimer(); } -void RecipientQosMacDataService::expireReassemblyFragments() +void RecipientQosMacDataService::expireReceiveLifetime() { for (auto packet : basicReassembly->removeExpiredFragments(simTime())) { PacketDropDetails details; @@ -47,15 +50,21 @@ void RecipientQosMacDataService::expireReassemblyFragments() emit(packetDroppedSignal, packet, &details); delete packet; } + for (auto packet : blockAckReordering->removeExpiredFragments(simTime())) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } } -void RecipientQosMacDataService::scheduleReassemblyTimer() +void RecipientQosMacDataService::scheduleReceiveLifetimeTimer() { - if (reassemblyTimer->isScheduled()) - cancelEvent(reassemblyTimer); - auto nextExpirationTime = basicReassembly->getNextExpirationTime(); + if (receiveLifetimeTimer->isScheduled()) + cancelEvent(receiveLifetimeTimer); + auto nextExpirationTime = std::min(basicReassembly->getNextExpirationTime(), blockAckReordering->getNextExpirationTime()); if (nextExpirationTime != SIMTIME_MAX) - scheduleAt(nextExpirationTime, reassemblyTimer); + scheduleAt(nextExpirationTime, receiveLifetimeTimer); } void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress originatorAddr) @@ -71,11 +80,21 @@ void RecipientQosMacDataService::resetBlockAckReordering(Tid tid, MacAddress ori delete packet; } } + if (basicReassembly) { + auto droppedFragments = basicReassembly->purge(originatorAddr, tid, 0, 4095); + for (auto packet : droppedFragments) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } + scheduleReceiveLifetimeTimer(); } Packet *RecipientQosMacDataService::defragment(std::vector completeFragments) { - expireReassemblyFragments(); + expireReceiveLifetime(); Packet *defragmentedPacket = nullptr; for (auto fragment : completeFragments) { auto packet = basicReassembly->addFragment(fragment); @@ -84,7 +103,7 @@ Packet *RecipientQosMacDataService::defragment(std::vector completeFra break; } } - scheduleReassemblyTimer(); + scheduleReceiveLifetimeTimer(); if (defragmentedPacket != nullptr) emit(packetDefragmentedSignal, defragmentedPacket); return defragmentedPacket; @@ -92,9 +111,9 @@ Packet *RecipientQosMacDataService::defragment(std::vector completeFra Packet *RecipientQosMacDataService::defragment(Packet *mgmtFragment) { - expireReassemblyFragments(); + expireReceiveLifetime(); auto packet = basicReassembly->addFragment(mgmtFragment); - scheduleReassemblyTimer(); + scheduleReceiveLifetimeTimer(); if (packet && packet->hasAtFront()) { emit(packetDefragmentedSignal, packet); return packet; @@ -107,6 +126,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data { Enter_Method("dataFrameReceived"); take(dataPacket); + expireReceiveLifetime(); // TODO A-MPDU Deaggregation, MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(dataHeader)) { EV_WARN << "Dropping duplicate packet " << *dataPacket << ".\n"; @@ -114,6 +134,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data details.setReason(DUPLICATE_DETECTED); emit(packetDroppedSignal, dataPacket, &details); delete dataPacket; + scheduleReceiveLifetimeTimer(); return std::vector(); } BlockAckReordering::ReorderBuffer frames; @@ -122,8 +143,16 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data Tid tid = dataHeader->getTid(); MacAddress originatorAddr = dataHeader->getTransmitterAddress(); RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); - if (agreement) - frames = blockAckReordering->processReceivedQoSFrame(agreement, dataPacket, dataHeader); + if (agreement) { + auto processingResult = blockAckReordering->processReceivedQoSFrameWithResult(agreement, dataPacket, dataHeader); + frames = processingResult.frames; + for (auto packet : processingResult.tombstonedFragments) { + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } + } } std::vector defragmentedFrames; if (basicReassembly) { // FIXME defragmentation @@ -143,6 +172,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data else ; // TODO drop? } } + scheduleReceiveLifetimeTimer(); std::vector deaggregatedFrames; if (aMsduDeaggregation) { for (auto defragmentedFrame : defragmentedFrames) { @@ -166,16 +196,21 @@ IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataS { Enter_Method("managementFrameReceived"); take(mgmtPacket); + expireReceiveLifetime(); // TODO MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption if (duplicateRemoval && duplicateRemoval->isDuplicate(mgmtHeader)) { delete mgmtPacket; // A duplicate fragment is acknowledged by HCF but is not a complete // MMPDU. Preserve the existing subtype-specific handling only for an // unfragmented duplicate management frame. - if (mgmtHeader->getFragmentNumber() == 0 && !mgmtHeader->getMoreFragments()) + if (mgmtHeader->getFragmentNumber() == 0 && !mgmtHeader->getMoreFragments()) { + scheduleReceiveLifetimeTimer(); return { {}, mgmtHeader, true }; - else + } + else { + scheduleReceiveLifetimeTimer(); return { {}, nullptr, true }; + } } if (basicReassembly) { // FIXME defragmentation mgmtPacket = defragment(mgmtPacket); @@ -186,15 +221,19 @@ IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataS // TODO Defrag, MSDU Integrity, Replay Detection, RX MSDU Rate Limiting if (dynamicPtrCast(completeHeader)) { delete mgmtPacket; + scheduleReceiveLifetimeTimer(); return { {}, completeHeader, false }; } - else + else { + scheduleReceiveLifetimeTimer(); return { { mgmtPacket }, completeHeader, false }; + } } std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) { Enter_Method("controlFrameReceived"); + expireReceiveLifetime(); if (auto blockAckReq = dynamicPtrCast(controlHeader)) { BlockAckReordering::ReorderBuffer frames; if (blockAckReordering) { @@ -203,8 +242,10 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); if (agreement) frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReq); - else + else { + scheduleReceiveLifetimeTimer(); return std::vector(); + } } std::vector defragmentedFrames; if (basicReassembly) { // FIXME defragmentation @@ -239,14 +280,16 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c } } // TODO MSDU Integrity, Replay Detection, RX MSDU Rate Limiting + scheduleReceiveLifetimeTimer(); return deaggregatedFrames; } + scheduleReceiveLifetimeTimer(); return std::vector(); } RecipientQosMacDataService::~RecipientQosMacDataService() { - cancelAndDelete(reassemblyTimer); + cancelAndDelete(receiveLifetimeTimer); delete duplicateRemoval; delete basicReassembly; delete aMsduDeaggregation; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h index 76b27d8cbc7..b3c6cdc5d4f 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h @@ -27,7 +27,8 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, { protected: IReassembly *basicReassembly = nullptr; - cMessage *reassemblyTimer = nullptr; + cMessage *receiveLifetimeTimer = nullptr; + simtime_t maxReceiveLifetime = SIMTIME_MAX; IMpduDeaggregation *aMpduDeaggregation = nullptr; // MpduHeaderAndFcsValidation *mpduHeaderAndFcsValidation = nullptr; @@ -44,8 +45,8 @@ class INET_API RecipientQosMacDataService : public IRecipientQosMacDataService, virtual void initialize() override; virtual void handleMessage(cMessage *message) override; - virtual void expireReassemblyFragments(); - virtual void scheduleReassemblyTimer(); + virtual void expireReceiveLifetime(); + virtual void scheduleReceiveLifetimeTimer(); virtual Packet *defragment(std::vector completeFragments); virtual Packet *defragment(Packet *mgmtFragment); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned index d02d7e7ff00..49af399d6b6 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.ned @@ -22,7 +22,7 @@ module RecipientQosMacDataService extends Module { parameters: @class(RecipientQosMacDataService); - double maxReceiveLifetime @unit(s) = default(524288us); // IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs + double maxReceiveLifetime @unit(s) = default(524288us); // Nonnegative relative receive lifetime; IEEE Std 802.11-2024 dot11MaxReceiveLifetime default: 512 TUs @display("i=block/join"); @signal[packetDefragmented](type=inet::Packet); @signal[packetDeaggregated](type=inet::Packet); diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 99b39c46bc3..e33fd1ff2bf 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -38,6 +38,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" @@ -185,6 +186,8 @@ class TestRecipientHandler : public RecipientBlockAckAgreementHandler class TestBlockAckReordering : public BlockAckReordering { public: + using BlockAckReordering::BlockAckReordering; + int getNumReceiveBuffers() const { return receiveBuffers.size(); } ReceiveBuffer *getReceiveBuffer(Tid tid, MacAddress originatorAddress) const { auto it = receiveBuffers.find(std::make_pair(tid, originatorAddress)); @@ -1013,6 +1016,30 @@ auto createPacketQueue = [this](const char *name, int packetCapacity = -1, const return check_and_cast(module); }; +// maxReceiveLifetime is a relative duration and must be nonnegative before +// either recipient allocates a receive-lifetime consumer. Zero remains valid. +auto assertMaxReceiveLifetimeValidation = [this](const char *typeName, const char *namePrefix) { + auto invalidModule = cModuleType::get(typeName)->create((std::string(namePrefix) + "Negative").c_str(), this); + invalidModule->par("maxReceiveLifetime").setDoubleValue(-1e-6); + bool threw = false; + try { + invalidModule->callInitialize(); + } + catch (cRuntimeError&) { + threw = true; + } + ASSERT(threw); + invalidModule->deleteModule(); + + auto zeroModule = cModuleType::get(typeName)->create((std::string(namePrefix) + "Zero").c_str(), this); + zeroModule->par("maxReceiveLifetime").setDoubleValue(0); + zeroModule->callInitialize(); + zeroModule->callFinish(); + zeroModule->deleteModule(); +}; +assertMaxReceiveLifetimeValidation("inet.linklayer.ieee80211.mac.recipient.RecipientMacDataService", "recipientMacDataServiceLifetime"); +assertMaxReceiveLifetimeValidation("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService", "recipientQosMacDataServiceLifetime"); + { TestOriginatorHandler tokenHandler; tokenHandler.setNextDialogToken(255); @@ -2477,6 +2504,174 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; reassembleOrderedFragments(barFragments, { 0x50, 0x51, 0x52, 0x53 }); } +// Receive-buffer lifetime starts when the first fragment of an incomplete +// fragmented MPDU is retained. Later fragments do not refresh that deadline; +// complete MPDUs held only for ordering remain available until normal window +// processing releases them. +{ + auto makeReorderFragment = [&](const char *name, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer2, 6, sequenceNumber); + dataHeader->setTransmitterAddress(peer1); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + + auto lifetime = SimTime(1, SIMTIME_US); + RecipientBlockAckAgreement agreement(peer1, 6, SequenceNumberCyclic(100), 64, 0); + TestBlockAckReordering reordering(lifetime); + auto firstFragment = makeReorderFragment("receiveLifetimeFirstFragment", SequenceNumberCyclic(100), 1, true, 0x61); + auto firstReceptionTime = simTime(); + ASSERT(reordering.processReceivedQoSFrame(&agreement, firstFragment, firstFragment->peekAtFront()).empty()); + auto firstDeadline = reordering.getNextExpirationTime(); + ASSERT(firstDeadline == firstReceptionTime + lifetime); + wait(0.5e-6); + auto laterFragment = makeReorderFragment("receiveLifetimeLaterFragment", SequenceNumberCyclic(100), 2, false, 0x62); + ASSERT(reordering.processReceivedQoSFrame(&agreement, laterFragment, laterFragment->peekAtFront()).empty()); + ASSERT(reordering.getNextExpirationTime() == firstDeadline); + + auto completeEntry = makeReorderFragment("receiveLifetimeCompleteEntry", SequenceNumberCyclic(101), 0, false, 0x63); + ASSERT(reordering.processReceivedQoSFrame(&agreement, completeEntry, completeEntry->peekAtFront()).empty()); + auto receiveBuffer = reordering.getReceiveBuffer(6, peer1); + ASSERT(receiveBuffer->getLength() == 3); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(100)); + auto expiredFragments = reordering.removeExpiredFragments(firstDeadline); + ASSERT(expiredFragments.size() == 2); + ASSERT(receiveBuffer->getLength() == 1); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(100)); + ASSERT(receiveBuffer->getBuffer().find(101) != receiveBuffer->getBuffer().end()); + for (auto packet : expiredFragments) + delete packet; + + auto lateFragment = makeReorderFragment("receiveLifetimeLateFragment", SequenceNumberCyclic(100), 0, true, 0x64); + ASSERT(reordering.processReceivedQoSFrame(&agreement, lateFragment, lateFragment->peekAtFront()).empty()); + ASSERT(receiveBuffer->getLength() == 1); + auto retainedCompleteEntry = reordering.resetReceiveBuffer(6, peer1); + ASSERT(retainedCompleteEntry.size() == 1); + delete retainedCompleteEntry.front(); + + // The tombstone is retired by normal modulo-window progress, so a later + // complete wrap can reuse the raw sequence number without permanent loss. + RecipientBlockAckAgreement wrapAgreement(peer1, 6, SequenceNumberCyclic(4094), 64, 0); + TestBlockAckReordering wrapReordering(lifetime); + auto wrapFragment = makeReorderFragment("receiveLifetimeWrapFragment", SequenceNumberCyclic(4095), 1, false, 0x65); + ASSERT(wrapReordering.processReceivedQoSFrame(&wrapAgreement, wrapFragment, wrapFragment->peekAtFront()).empty()); + auto wrapDeadline = wrapReordering.getNextExpirationTime(); + auto expiredWrapFragments = wrapReordering.removeExpiredFragments(wrapDeadline); + ASSERT(expiredWrapFragments.size() == 1); + delete expiredWrapFragments.front(); + auto wrapReceiveBuffer = wrapReordering.getReceiveBuffer(6, peer1); + wrapReceiveBuffer->setNextExpectedSequenceNumber(SequenceNumberCyclic(0)); + for (int sequenceNumber = 1; sequenceNumber <= 4095; sequenceNumber++) + wrapReceiveBuffer->setNextExpectedSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + auto reusedWrapFragment = makeReorderFragment("receiveLifetimeReusedWrapFragment", SequenceNumberCyclic(4095), 0, true, 0x66); + ASSERT(wrapReordering.processReceivedQoSFrame(&wrapAgreement, reusedWrapFragment, reusedWrapFragment->peekAtFront()).empty()); + auto reusedWrapFragments = wrapReordering.resetReceiveBuffer(6, peer1); + ASSERT(reusedWrapFragments.size() == 1); + delete reusedWrapFragments.front(); +} + +// Different terminal markers for one sequence cannot be interpreted as a +// complete MPDU. Keep the entry incomplete and expiring instead of releasing +// a hybrid or clearing its receive-lifetime deadline. +{ + auto makeContradictoryFragment = [&](const char *name, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(250)); + dataHeader->setTransmitterAddress(peer1); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto lifetime = SimTime(1, SIMTIME_US); + RecipientBlockAckAgreement agreement(peer1, 6, SequenceNumberCyclic(250), 64, 0); + TestBlockAckReordering reordering(lifetime); + auto finalFragment = makeContradictoryFragment("contradictoryFinalFragment", 1, false, 0x67); + auto receptionTime = simTime(); + ASSERT(reordering.processReceivedQoSFrame(&agreement, finalFragment, finalFragment->peekAtFront()).empty()); + auto fragmentZero = makeContradictoryFragment("contradictoryFragmentZero", 0, false, 0x68); + ASSERT(reordering.processReceivedQoSFrame(&agreement, fragmentZero, fragmentZero->peekAtFront()).empty()); + ASSERT(reordering.getNextExpirationTime() == receptionTime + lifetime); + auto receiveBuffer = reordering.getReceiveBuffer(6, peer1); + ASSERT(receiveBuffer->getLength() == 2); + auto expiredFragments = reordering.removeExpiredFragments(receptionTime + lifetime); + ASSERT(expiredFragments.size() == 2); + for (auto packet : expiredFragments) + delete packet; + ASSERT(receiveBuffer->getLength() == 0); +} + +// A retained reorder-buffer fragment arms the recipient's common lifetime +// timer. Reorder expiry and BasicReassembly expiry are staggered, and every +// detached packet is reported exactly once as OTHER_PACKET_DROP. +{ + auto serviceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("reorderLifetimeDataService", this); + auto lifetime = SimTime(2, SIMTIME_US); + serviceModule->par("maxReceiveLifetime").setDoubleValue(lifetime.dbl()); + serviceModule->callInitialize(); + auto service = check_and_cast(serviceModule); + TestPacketDropSignalListener dropListener; + service->subscribe(packetDroppedSignal, &dropListener); + + RecipientBlockAckAgreementHandler agreementHandler; + TestRecipientPolicy policy; + TestCallback callback; + auto request = makeRequest(peer1, 6, 92, SequenceNumberCyclic(300), 64); + auto agreement = agreementHandler.processReceivedAddbaRequest(request, &policy, &callback, &callback); + ASSERT(agreement != nullptr); + + auto makeServiceFragment = [&](const char *name, MacAddress transmitterAddress, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer2, 6, sequenceNumber); + dataHeader->setTransmitterAddress(transmitterAddress); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + + auto reorderFirstFragment = makeServiceFragment("serviceReorderFirstFragment", peer1, SequenceNumberCyclic(300), 1, true, 0x71); + ASSERT(service->dataFrameReceived(reorderFirstFragment, reorderFirstFragment->peekAtFront(), &agreementHandler).empty()); + auto reorderFinalFragment = makeServiceFragment("serviceReorderFinalFragment", peer1, SequenceNumberCyclic(300), 2, false, 0x72); + ASSERT(service->dataFrameReceived(reorderFinalFragment, reorderFinalFragment->peekAtFront(), &agreementHandler).empty()); + ASSERT(dropListener.numSignals == 0); + wait(0.5e-6); + + // No agreement for peer3: this fragment is retained by BasicReassembly, + // with a later deadline than the already retained reorder fragments. + auto basicFragment = makeServiceFragment("serviceBasicFragment", peer3, SequenceNumberCyclic(350), 0, true, 0x73); + ASSERT(service->dataFrameReceived(basicFragment, basicFragment->peekAtFront(), &agreementHandler).empty()); + wait(1.6e-6); + ASSERT(dropListener.numSignals == 2); + ASSERT(dropListener.numOtherPacketDrops == 2); + + // The expired sequence remains tombstoned until the reorder window moves + // past it. A late fragment is rejected and reported once by the service, + // while no hybrid or partial frame is delivered. + auto lateReorderFragment = makeServiceFragment("serviceLateReorderFragment", peer1, SequenceNumberCyclic(300), 0, true, 0x74); + ASSERT(service->dataFrameReceived(lateReorderFragment, lateReorderFragment->peekAtFront(), &agreementHandler).empty()); + ASSERT(dropListener.numSignals == 3); + ASSERT(dropListener.numOtherPacketDrops == 3); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(300), 0)); + wait(0.3e-6); + ASSERT(dropListener.numSignals == 3); + wait(0.2e-6); + ASSERT(dropListener.numSignals == 4); + ASSERT(dropListener.numOtherPacketDrops == 4); + service->unsubscribe(packetDroppedSignal, &dropListener); + serviceModule->callFinish(); + serviceModule->deleteModule(); +} + // The recipient data service owns reorder-buffer reset observability: every // buffered MPDU is reported exactly once before it is deleted. { @@ -2490,11 +2685,46 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; auto bufferedPacket = new Packet("signaledOldWindowPacket", bufferedHeader); auto signaledOldWindowFrames = recipientDataService->dataFrameReceived(bufferedPacket, bufferedHeader, &immediateRecipientHandler); ASSERT(signaledOldWindowFrames.empty()); + + auto makeBasicFragment = [&](const char *name, MacAddress transmitterAddress, Tid tid, int sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto dataHeader = makeQosHeader(peer1, tid, SequenceNumberCyclic(sequenceNumber)); + dataHeader->setTransmitterAddress(transmitterAddress); + dataHeader->setFragmentNumber(fragmentNumber); + dataHeader->setMoreFragments(moreFragments); + dataHeader->setAckPolicy(BLOCK_ACK); + auto packet = new Packet(name, dataHeader); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto targetFirst = makeBasicFragment("signaledTargetBasicFragment", peer2, 7, 20, 0, true, 0x81); + ASSERT(recipientDataService->dataFrameReceived(targetFirst, targetFirst->peekAtFront(), &immediateRecipientHandler).empty()); + auto samePeerDifferentTidFirst = makeBasicFragment("signaledDifferentTidBasicFragment", peer2, 8, 21, 0, true, 0x82); + ASSERT(recipientDataService->dataFrameReceived(samePeerDifferentTidFirst, samePeerDifferentTidFirst->peekAtFront(), &immediateRecipientHandler).empty()); + auto differentPeerSameTidFirst = makeBasicFragment("signaledDifferentPeerBasicFragment", peer3, 7, 22, 0, true, 0x83); + ASSERT(recipientDataService->dataFrameReceived(differentPeerSameTidFirst, differentPeerSameTidFirst->peekAtFront(), &immediateRecipientHandler).empty()); + recipientDataService->resetBlockAckReordering(6, peer1); ASSERT(dropListener.numSignals == 1); ASSERT(dropListener.numOtherPacketDrops == 1); + // Resetting one originator/TID agreement also purges its BasicReassembly + // state, while retaining fragments for a different TID or peer. + recipientDataService->resetBlockAckReordering(7, peer2); + ASSERT(dropListener.numSignals == 2); + ASSERT(dropListener.numOtherPacketDrops == 2); recipientDataService->resetBlockAckReordering(6, peer1); - ASSERT(dropListener.numSignals == 1); + recipientDataService->resetBlockAckReordering(7, peer2); + ASSERT(dropListener.numSignals == 2); + auto samePeerDifferentTidFinal = makeBasicFragment("signaledDifferentTidBasicFinal", peer2, 8, 21, 1, false, 0x92); + auto samePeerDifferentTidFrames = recipientDataService->dataFrameReceived(samePeerDifferentTidFinal, + samePeerDifferentTidFinal->peekAtFront(), &immediateRecipientHandler); + ASSERT(samePeerDifferentTidFrames.size() == 1); + delete samePeerDifferentTidFrames.front(); + auto differentPeerSameTidFinal = makeBasicFragment("signaledDifferentPeerBasicFinal", peer3, 7, 22, 1, false, 0x93); + auto differentPeerSameTidFrames = recipientDataService->dataFrameReceived(differentPeerSameTidFinal, + differentPeerSameTidFinal->peekAtFront(), &immediateRecipientHandler); + ASSERT(differentPeerSameTidFrames.size() == 1); + delete differentPeerSameTidFrames.front(); recipientDataService->unsubscribe(packetDroppedSignal, &dropListener); recipientDataServiceModule->callFinish(); recipientDataServiceModule->deleteModule(); @@ -5048,6 +5278,16 @@ for (int responseIndex = 0; responseIndex < 2; responseIndex++) { ASSERT(completed->peekAt(completedResponse->getChunkLength(), B(newPayload.size()))->getBytes() == newPayload); delete completed; delete recoveredFragments; + + // purge() remains the explicit agreement/lifecycle boundary and returns + // detached fragments to its caller for observable drop handling. + auto purgeFragments = makeResponseFragments("reassemblyPurgedResponse", 31, 41, { 0xb0, 0xb1, 0xb2 }, 4094, 2); + ASSERT(reassembly.addFragment(purgeFragments->at(0)) == nullptr); + auto purgedFragments = reassembly.purge(peer1, -1, 4094, 4094); + ASSERT(purgedFragments.size() == 1); + delete purgedFragments.front(); + delete purgeFragments->at(1); + delete purgeFragments; } // A terminal duplicate on an occupied nonterminal slot is still observed for From e651247c97e02f1f67fa16b60db7932ce076fc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:05 +0200 Subject: [PATCH 37/56] ieee80211: quarantine expired Block Ack agreements An inactivity-expired Block Ack agreement must remain installed until its generation-matched DELBA teardown completes. Previously, data-plane users treated that retained object as active, so they could continue selecting Block Ack, sending BARs, buffering frames, and producing Block Ack responses after expiry. Separate raw lifecycle lookup from active agreement lookup and use the active form throughout originator and recipient data paths. Fall back to Normal Ack, suppress BAR and Block Ack processing, discard Block-Ack-policy data without mutating reorder state, and ignore late responses once an agreement is unavailable. Release matching originator acknowledgement state for retry when an agreement expires or is removed. Also cover the race where a frame finishes transmission after expiry, while preserving the retained agreement object for generation-safe DELBA handling. Extend the ADDBA transaction tests with active-versus-expired policy, selection, teardown, reordering, late-frame, and transmission-completion cases. Remove the trailing blank line from the QoS example configuration. Validation completed in debug mode with the full build, the focused ADDBA unit test, the Block Ack inactivity-timer module test, and git diff --check. --- .../OriginatorBlockAckAgreementHandler.cc | 25 +- .../OriginatorBlockAckAgreementHandler.h | 3 +- .../RecipientBlockAckAgreementHandler.cc | 19 +- .../RecipientBlockAckAgreementHandler.h | 3 +- .../blockack/RecipientBlockAckProcedure.cc | 3 +- .../IBlockAckAgreementHandlerCallback.h | 6 + .../IOriginatorBlockAckAgreementHandler.h | 6 +- .../mac/contract/IOriginatorQoSAckPolicy.h | 7 +- .../IRecipientBlockAckAgreementHandler.h | 6 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 38 ++- .../ieee80211/mac/coordinationfunction/Hcf.h | 2 + .../framesequence/PrimitiveFrameSequences.cc | 3 +- .../ieee80211/mac/framesequence/TxOpFs.cc | 5 +- .../mac/originator/OriginatorQosAckPolicy.cc | 54 +-- .../mac/originator/OriginatorQosAckPolicy.h | 7 +- .../ieee80211/mac/originator/QosAckHandler.cc | 17 +- .../ieee80211/mac/originator/QosAckHandler.h | 5 +- .../mac/recipient/RecipientQosAckPolicy.cc | 4 +- .../recipient/RecipientQosMacDataService.cc | 19 +- tests/unit/Ieee80211AddbaTransaction_1.test | 308 +++++++++++++++++- 20 files changed, 470 insertions(+), 70 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index ad22a8fce99..f69625097c3 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -98,26 +98,31 @@ void OriginatorBlockAckAgreementHandler::addbaResponseTimeoutExpired(IOriginator scheduleAddbaResponseTimer(callback); } -void OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) +bool OriginatorBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the // peer STA with the Reason Code field set to TIMEOUT and shall issue a MLME-DELBA.indication // primitive with the ReasonCode parameter having a value of TIMEOUT. // The procedure is illustrated in Figure 10-14. simtime_t now = simTime(); + bool expired = false; for (auto id : blockAckAgreements) { auto agreement = id.second; if (agreement->getIsAddbaResponseReceived() && !agreement->isInactivityExpired() && agreement->getExpirationTime() <= now) { agreement->markInactivityExpired(); MacAddress receiverAddr = id.first.first; Tid tid = id.first.second; + if (agreementHandlerCallback != nullptr) + agreementHandlerCallback->releaseBlockAckAgreementFrames(receiverAddr, tid); const auto& delba = buildDelba(receiverAddr, tid, 39); auto delbaPacket = new Packet("Delba", delba); delbaPacket->addTag()->setGenerationId(agreement->getTransactionId()); procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes + expired = true; } } scheduleInactivityTimer(agreementHandlerCallback); + return expired; } const Ptr OriginatorBlockAckAgreementHandler::buildAddbaRequest(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) @@ -144,8 +149,8 @@ const Ptr OriginatorBlockAckAgreementHandler::buildAddbaR void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const Ptr& blockAck, IBlockAckAgreementHandlerCallback *callback) { if (auto basicBlockAck = dynamicPtrCast(blockAck)) { - auto agreement = getAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); - if (agreement && !agreement->isInactivityExpired()) { + auto agreement = getActiveAgreement(basicBlockAck->getTransmitterAddress(), basicBlockAck->getTidInfo()); + if (agreement != nullptr) { agreement->setStartingSequenceNumber(basicBlockAck->getStartingSequenceNumber()); agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); @@ -174,6 +179,12 @@ OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getAgreement(Ma return it != blockAckAgreements.end() ? it->second : nullptr; } +OriginatorBlockAckAgreement *OriginatorBlockAckAgreementHandler::getActiveAgreement(MacAddress receiverAddr, Tid tid) +{ + auto agreement = getAgreement(receiverAddr, tid); + return agreement != nullptr && agreement->getIsAddbaResponseReceived() && !agreement->isInactivityExpired() ? agreement : nullptr; +} + bool OriginatorBlockAckAgreementHandler::isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const { auto it = blockAckAgreements.find(std::make_pair(receiverAddr, tid)); @@ -363,6 +374,8 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: return nullptr; bool cancelPendingTransaction = agreement->isPending(); std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + if (terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getReceiverAddress(), delba->getTid()); scheduleInactivityTimer(callback); pendingTeardownTransactionIds[std::make_pair(delba->getReceiverAddress(), delba->getTid())] = generationId; scheduleAddbaResponseTimer(callback); @@ -375,6 +388,8 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: bool cancelPendingTransaction = agreement != nullptr && agreement->isPending(); auto transactionId = cancelPendingTransaction ? agreement->getTransactionId() : 0; std::unique_ptr terminatedAgreement(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + if (terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getReceiverAddress(), delba->getTid()); scheduleInactivityTimer(callback); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) @@ -420,6 +435,8 @@ OriginatorBlockAckAgreementAbortResult OriginatorBlockAckAgreementHandler::proce OriginatorBlockAckAgreementAbortResult result; result.handled = true; result.terminatedAgreement.reset(removeAgreement(delba->getReceiverAddress(), delba->getTid())); + if (result.terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getReceiverAddress(), delba->getTid()); scheduleInactivityTimer(callback); if (callback != nullptr) callback->cancelBlockAckTeardown(true, delba->getReceiverAddress(), delba->getTid(), agreementTag->getGenerationId(), packet); @@ -441,6 +458,8 @@ std::unique_ptr OriginatorBlockAckAgreementHandler: if (pendingTeardownIt != pendingTeardownTransactionIds.end()) pendingTeardownTransactionIds.erase(pendingTeardownIt); std::unique_ptr terminatedAgreement(removeAgreement(delba->getTransmitterAddress(), delba->getTid())); + if (terminatedAgreement != nullptr && callback != nullptr) + callback->releaseBlockAckAgreementFrames(delba->getTransmitterAddress(), delba->getTid()); scheduleInactivityTimer(callback); scheduleAddbaResponseTimer(callback); if (cancelPendingTransaction) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 4b155bbbe34..96e6ca12721 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -54,10 +54,11 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg virtual std::unique_ptr processTransmittedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) override; virtual OriginatorBlockAckAgreement *getAgreement(MacAddress receiverAddr, Tid tid) override; + virtual OriginatorBlockAckAgreement *getActiveAgreement(MacAddress receiverAddr, Tid tid) override; virtual bool isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const override; virtual bool isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const override; virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 5648b95a90a..e8a6cce5749 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -41,8 +41,8 @@ void RecipientBlockAckAgreementHandler::qosFrameReceived(const PtrgetAckPolicy() == AckPolicy::BLOCK_ACK) { // TODO + Implicit Block Ack Tid tid = qosHeader->getTid(); MacAddress originatorAddr = qosHeader->getTransmitterAddress(); - auto agreement = getAgreement(tid, originatorAddr); - if (agreement && !agreement->isInactivityExpired()) { + auto agreement = getActiveAgreement(tid, originatorAddr); + if (agreement != nullptr) { agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); } @@ -53,20 +53,21 @@ void RecipientBlockAckAgreementHandler::qosFrameReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) { - auto agreement = getAgreement(blockAckReq->getTidInfo(), blockAckReq->getTransmitterAddress()); - if (agreement != nullptr && !agreement->isInactivityExpired()) { + auto agreement = getActiveAgreement(blockAckReq->getTidInfo(), blockAckReq->getTransmitterAddress()); + if (agreement != nullptr) { agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); } } -void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) +bool RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { // When a timeout of BlockAckTimeout is detected, the STA shall send a DELBA frame to the // peer STA with the Reason Code field set to TIMEOUT and shall issue a MLME-DELBA.indication // primitive with the ReasonCode parameter having a value of TIMEOUT. // The procedure is illustrated in Figure 10-14. simtime_t now = simTime(); + bool expired = false; for (auto id : blockAckAgreements) { auto agreement = id.second; if (!agreement->isInactivityExpired() && agreement->getExpirationTime() <= now) { @@ -77,9 +78,11 @@ void RecipientBlockAckAgreementHandler::blockAckAgreementExpired(IProcedureCallb auto delbaPacket = new Packet("Delba", delba); delbaPacket->addTag()->setGenerationId(agreement->getGenerationId()); procedureCallback->processMgmtFrame(delbaPacket, delba); // 39 - TIMEOUT see: Table 8-36—Reason codes + expired = true; } } scheduleInactivityTimer(agreementHandlerCallback); + return expired; } // @@ -142,6 +145,12 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getAgreement(Tid return it != blockAckAgreements.end() ? it->second : nullptr; } +RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::getActiveAgreement(Tid tid, MacAddress originatorAddr) +{ + auto agreement = getAgreement(tid, originatorAddr); + return agreement != nullptr && !agreement->isInactivityExpired() ? agreement : nullptr; +} + RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAddbaRequest(const Ptr& addbaRequest, IRecipientBlockAckAgreementPolicy *blockAckAgreementPolicy, IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) { EV_INFO << "Processing Addba Request from " << addbaRequest->getTransmitterAddress() << endl; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index da90268c368..7c4ce21b5b9 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -50,9 +50,10 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) override; virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) override; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; + virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; + virtual RecipientBlockAckAgreement *getActiveAgreement(Tid tid, MacAddress originatorAddr) override; virtual uint64_t getPendingTeardownGenerationId(Tid tid, MacAddress originatorAddr) const override; virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const override; }; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc index ab7cb050e5f..6796539c4f5 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc @@ -21,7 +21,7 @@ void RecipientBlockAckProcedure::processReceivedBlockAckReq(Packet *blockAckPack { numReceivedBlockAckReq++; if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) { - auto agreement = blockAckAgreementHandler->getAgreement(basicBlockAckReq->getTidInfo(), basicBlockAckReq->getTransmitterAddress()); + auto agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(basicBlockAckReq->getTidInfo(), basicBlockAckReq->getTransmitterAddress()); if (ackPolicy->isBlockAckNeeded(basicBlockAckReq, agreement)) { auto blockAck = buildBlockAck(basicBlockAckReq, agreement); auto duration = ackPolicy->computeBasicBlockAckDurationField(blockAckPacketReq, basicBlockAckReq); @@ -71,4 +71,3 @@ const Ptr RecipientBlockAckProcedure::buildBlockAck(const Ptr } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h index cd2a3859699..f73bcda128e 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IBlockAckAgreementHandlerCallback.h @@ -34,6 +34,12 @@ class INET_API IBlockAckAgreementHandlerCallback virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) = 0; virtual void scheduleAddbaResponseTimer(simtime_t deadline) = 0; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) = 0; + // Marks originator data frames that were waiting for this agreement's + // Block Ack exchange as eligible for retry with an alternative + // acknowledgment policy. This is needed whenever an agreement becomes + // unavailable, including before its retained teardown object is removed. + // The return value reports whether any ACK state changed. + virtual bool releaseBlockAckAgreementFrames(MacAddress peerAddress, Tid tid) { return false; } // Removes queued siblings of a sender-local DELBA without assuming that // the frame is still removable from the active frame sequence. The // agreement owner remains responsible for rejecting stale packets. diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h index 9a1a2af753e..64a8302f9db 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h @@ -58,10 +58,14 @@ class INET_API IOriginatorBlockAckAgreementHandler // transaction and sibling packets were cancelled through the callback. virtual bool processAcknowledgedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; virtual OriginatorBlockAckAgreementAbortResult processAbortedDelba(Packet *packet, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; + virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual void addbaResponseTimeoutExpired(IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy, IBlockAckAgreementHandlerCallback *callback) = 0; virtual OriginatorBlockAckAgreement *getAgreement(MacAddress receiverAddr, Tid tid) = 0; + // Returns the installed agreement only while it can be used by the data + // plane. Lifecycle code must use getAgreement() to retain generation-safe + // teardown state after inactivity expiry. + virtual OriginatorBlockAckAgreement *getActiveAgreement(MacAddress receiverAddr, Tid tid) = 0; virtual bool isAddbaResponsePending(MacAddress receiverAddr, Tid tid) const = 0; virtual bool isAddbaRequestPending(const Packet *packet, const Ptr& addbaReq) const = 0; virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const { return true; } diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h index 8f03c2fd052..18b934badc5 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h @@ -16,6 +16,8 @@ namespace inet { namespace ieee80211 { +class IOriginatorBlockAckAgreementHandler; + class INET_API IOriginatorQoSAckPolicy { public: @@ -23,9 +25,9 @@ class INET_API IOriginatorQoSAckPolicy virtual bool isAckNeeded(const Ptr& header) const = 0; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const = 0; - virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const = 0; + virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const = 0; virtual bool isBlockAckPolicyEligibleFrame(Packet *packet, const Ptr& header) const = 0; - virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const = 0; + virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const = 0; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const = 0; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const = 0; @@ -35,4 +37,3 @@ class INET_API IOriginatorQoSAckPolicy } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 31f6b478078..8811e70f411 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -45,9 +45,13 @@ class INET_API IRecipientBlockAckAgreementHandler virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return {}; } virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; + virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) = 0; + // Returns the installed agreement only while it can be used by the data + // plane. Lifecycle code must use getAgreement() to retain generation-safe + // teardown state after inactivity expiry. + virtual RecipientBlockAckAgreement *getActiveAgreement(Tid tid, MacAddress originatorAddr) = 0; virtual uint64_t getPendingTeardownGenerationId(Tid, MacAddress) const { return 0; } virtual bool isDelbaPending(const Packet *packet, const Ptr& delba) const { return true; } }; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 0a36f69d9e8..e2f25d68921 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -338,8 +338,14 @@ void Hcf::handleMessage(cMessage *msg) } else if (msg == inactivityTimer) { if (originatorBlockAckAgreementHandler && recipientBlockAckAgreementHandler) { - originatorBlockAckAgreementHandler->blockAckAgreementExpired(this, this); - recipientBlockAckAgreementHandler->blockAckAgreementExpired(this, this); + blockAckInactivityExpiryInProgress = true; + bool changed = originatorBlockAckAgreementHandler->blockAckAgreementExpired(this, this); + changed |= recipientBlockAckAgreementHandler->blockAckAgreementExpired(this, this); + blockAckInactivityExpiryInProgress = false; + if (changed) { + rebuildPendingFrameEligibility(); + resumeEligibleChannelAccess(); + } } else throw cRuntimeError("Unknown event"); @@ -392,7 +398,7 @@ void Hcf::processUpperFrame(Packet *packet, const PtrgetEdcaf(ac)->getPendingQueue(); trackPendingFrame(packet, ac); pendingQueue->enqueuePacket(packet); - if (hasFrameToTransmit(ac)) { + if (!blockAckInactivityExpiryInProgress && hasFrameToTransmit(ac)) { auto edcaf = edca->getChannelOwner(); if (edcaf == nullptr || edcaf->getAccessCategory() != ac) { EV_DETAIL << "Requesting channel for access category " << printAccessCategory(ac) << endl; @@ -479,6 +485,20 @@ void Hcf::scheduleAddbaResponseTimer(simtime_t deadline) rescheduleAt(deadline, addbaResponseTimer); } +bool Hcf::releaseBlockAckAgreementFrames(MacAddress peerAddress, Tid tid) +{ + Enter_Method("releaseBlockAckAgreementFrames"); + if (edca == nullptr) + return false; + bool changed = false; + for (int ac = 0; ac < edca->getNumEdcafs(); ac++) { + auto edcaf = edca->getEdcaf(AccessCategory(ac)); + if (edcaf != nullptr && edcaf->getAckHandler() != nullptr) + changed |= edcaf->getAckHandler()->releaseBlockAckAgreementFrames(peerAddress, tid); + } + return changed; +} + void Hcf::cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) { Enter_Method("cancelAddbaTransaction"); @@ -993,6 +1013,9 @@ void Hcf::originatorProcessTransmittedDataFrame(Packet *packet, const PtrgetEdcaf(ac); edcaf->getAckHandler()->processTransmittedDataOrMgmtFrame(dataHeader); + if (dataHeader->getAckPolicy() == BLOCK_ACK && + (originatorBlockAckAgreementHandler == nullptr || originatorBlockAckAgreementHandler->getActiveAgreement(dataHeader->getReceiverAddress(), dataHeader->getTid()) == nullptr)) + edcaf->getAckHandler()->releaseBlockAckAgreementFrames(dataHeader->getReceiverAddress(), dataHeader->getTid()); if (dataHeader->getAckPolicy() == NO_ACK) edcaf->getInProgressFrames()->dropFrame(packet); } @@ -1219,10 +1242,13 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(header)) { EV_INFO << "BasicBlockAck has arrived" << std::endl; + if (originatorBlockAckAgreementHandler == nullptr || originatorBlockAckAgreementHandler->getActiveAgreement(blockAck->getTransmitterAddress(), blockAck->getTidInfo()) == nullptr) { + EV_INFO << "Ignoring BasicBlockAck without an active Block Ack agreement.\n"; + return; + } edcaf->getRecoveryProcedure()->blockAckFrameReceived(); auto ackedSeqAndFragNums = edcaf->getAckHandler()->processReceivedBlockAck(blockAck); - if (originatorBlockAckAgreementHandler) - originatorBlockAckAgreementHandler->processReceivedBlockAck(blockAck, this); + originatorBlockAckAgreementHandler->processReceivedBlockAck(blockAck, this); EV_TRACE << "It has acknowledged the following frames:" << std::endl; for (auto it : ackedSeqAndFragNums) EV_TRACE << " sequenceNumber = " << it.second.second.getSequenceNumber() << ", fragmentNumber = " << (int)it.second.second.getFragmentNumber() << std::endl; @@ -1295,7 +1321,7 @@ void Hcf::transmitFrame(Packet *packet, simtime_t ifs) if (auto dataFrame = dynamicPtrCast(header)) { OriginatorBlockAckAgreement *agreement = nullptr; if (originatorBlockAckAgreementHandler) - agreement = originatorBlockAckAgreementHandler->getAgreement(dataFrame->getReceiverAddress(), dataFrame->getTid()); + agreement = originatorBlockAckAgreementHandler->getActiveAgreement(dataFrame->getReceiverAddress(), dataFrame->getTid()); auto ackPolicy = originatorAckPolicy->computeAckPolicy(packet, dataFrame, agreement); auto dataHeader = packet->removeAtFront(); dataHeader->setAckPolicy(ackPolicy); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h index 052524378e3..8f2da6d6aaa 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h @@ -70,6 +70,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: // their role so one role cannot cancel the other's timeout. simtime_t originatorInactivityDeadline = SIMTIME_MAX; simtime_t recipientInactivityDeadline = SIMTIME_MAX; + bool blockAckInactivityExpiryInProgress = false; // Transmission and Reception IRx *rx = nullptr; @@ -204,6 +205,7 @@ class INET_API Hcf : public ICoordinationFunction, public IFrameSequenceHandler: virtual void scheduleInactivityTimer(BlockAckAgreementRole role, simtime_t deadline) override; virtual void scheduleAddbaResponseTimer(simtime_t deadline) override; virtual void cancelAddbaTransaction(uint64_t transactionId, Packet *excludedPacket) override; + virtual bool releaseBlockAckAgreementFrames(MacAddress peerAddress, Tid tid) override; virtual void cancelBlockAckTeardown(bool initiator, MacAddress peerAddress, Tid tid, uint64_t generationId, Packet *excludedPacket) override; std::string getFrameSequenceInfo() const; diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index 9ad68ed2969..ac5ecb84f5e 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -392,7 +392,7 @@ IFrameSequenceStep *BlockAckReqBlockAckFs::prepareStep(FrameSequenceContext *con { switch (step) { case 0: { - auto blockAckReqParams = context->getQoSContext()->ackPolicy->computeBlockAckReqParameters(context->getInProgressFrames(), context->getQoSContext()->txopProcedure); + auto blockAckReqParams = context->getQoSContext()->ackPolicy->computeBlockAckReqParameters(context->getInProgressFrames(), context->getQoSContext()->txopProcedure, context->getQoSContext()->blockAckAgreementHandler); auto receiverAddr = std::get<0>(blockAckReqParams); auto startingSequenceNumber = std::get<1>(blockAckReqParams); auto tid = std::get<2>(blockAckReqParams); @@ -434,4 +434,3 @@ bool BlockAckReqBlockAckFs::completeStep(FrameSequenceContext *context) } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc b/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc index 302ab7c32af..ca2d861615f 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/TxOpFs.cc @@ -47,7 +47,7 @@ int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceConte { auto frameToTransmit = context->getInProgressFrames()->getFrameToTransmit(); const auto& macHeader = frameToTransmit->peekAtFront(); - if (context->getQoSContext()->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), context->getQoSContext()->txopProcedure)) + if (context->getQoSContext()->ackPolicy->isBlockAckReqNeeded(context->getInProgressFrames(), context->getQoSContext()->txopProcedure, context->getQoSContext()->blockAckAgreementHandler)) return 2; if (dynamicPtrCast(macHeader)) return 3; @@ -55,7 +55,7 @@ int TxOpFs::selectTxOpSequence(AlternativesFs *frameSequence, FrameSequenceConte auto dataHeaderToTransmit = dynamicPtrCast(macHeader); OriginatorBlockAckAgreement *agreement = nullptr; if (context->getQoSContext()->blockAckAgreementHandler) - agreement = context->getQoSContext()->blockAckAgreementHandler->getAgreement(dataHeaderToTransmit->getReceiverAddress(), dataHeaderToTransmit->getTid()); + agreement = context->getQoSContext()->blockAckAgreementHandler->getActiveAgreement(dataHeaderToTransmit->getReceiverAddress(), dataHeaderToTransmit->getTid()); auto ackPolicy = context->getQoSContext()->ackPolicy->computeAckPolicy(frameToTransmit, dataHeaderToTransmit, agreement); if (ackPolicy == AckPolicy::BLOCK_ACK) return 0; @@ -79,4 +79,3 @@ bool TxOpFs::isBlockAckReqRtsCtsNeeded(OptionalFs *frameSequence, FrameSequenceC } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index 90cc7602199..229e57502ba 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -9,6 +9,8 @@ #include +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h" + namespace inet { namespace ieee80211 { @@ -31,13 +33,20 @@ bool OriginatorQosAckPolicy::isAckNeeded(const Ptr& h return !header->getReceiverAddress().isMulticast(); } -std::map> OriginatorQosAckPolicy::getOutstandingFramesPerReceiver(InProgressFrames *inProgressFrames) const +std::map, std::vector> OriginatorQosAckPolicy::getOutstandingFramesPerAgreement(InProgressFrames *inProgressFrames, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const { auto outstandingFrames = inProgressFrames->getOutstandingFrames(); - std::map> outstandingFramesPerReceiver; - for (auto frame : outstandingFrames) - outstandingFramesPerReceiver[frame->peekAtFront()->getReceiverAddress()].push_back(frame); - return outstandingFramesPerReceiver; + std::map, std::vector> outstandingFramesPerAgreement; + if (blockAckAgreementHandler == nullptr) + return outstandingFramesPerAgreement; + for (auto frame : outstandingFrames) { + auto dataHeader = frame->peekAtFront(); + auto receiverAddress = dataHeader->getReceiverAddress(); + auto tid = dataHeader->getTid(); + if (blockAckAgreementHandler->getActiveAgreement(receiverAddress, tid) != nullptr) + outstandingFramesPerAgreement[std::make_pair(receiverAddress, tid)].push_back(frame); + } + return outstandingFramesPerAgreement; } SequenceNumberCyclic OriginatorQosAckPolicy::computeStartingSequenceNumber(const std::vector& outstandingFrames) const @@ -64,10 +73,10 @@ bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector } // FIXME -bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const +bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const { - auto outstandingFramesPerReceiver = getOutstandingFramesPerReceiver(inProgressFrames); - for (auto outstandingFrames : outstandingFramesPerReceiver) { + auto outstandingFramesPerAgreement = getOutstandingFramesPerAgreement(inProgressFrames, blockAckAgreementHandler); + for (auto outstandingFrames : outstandingFramesPerAgreement) { if ((int)outstandingFrames.second.size() >= blockAckReqThreshold) return true; } @@ -75,28 +84,26 @@ bool OriginatorQosAckPolicy::isBlockAckReqNeeded(InProgressFrames *inProgressFra } // FIXME -std::tuple OriginatorQosAckPolicy::computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const +std::tuple OriginatorQosAckPolicy::computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const { - auto outstandingFramesPerReceiver = getOutstandingFramesPerReceiver(inProgressFrames); - for (auto outstandingFrames : outstandingFramesPerReceiver) { - if ((int)outstandingFrames.second.size() >= blockAckReqThreshold) { - auto largestOutstandingFrames = outstandingFramesPerReceiver.begin(); - for (auto it = outstandingFramesPerReceiver.begin(); it != outstandingFramesPerReceiver.end(); it++) { - if (it->second.size() > largestOutstandingFrames->second.size()) - largestOutstandingFrames = it; - } - MacAddress receiverAddress = largestOutstandingFrames->first; - SequenceNumberCyclic startingSequenceNumber = computeStartingSequenceNumber(largestOutstandingFrames->second); - Tid tid = largestOutstandingFrames->second.at(0)->peekAtFront()->getTid(); - return std::make_tuple(receiverAddress, startingSequenceNumber, tid); - } + auto outstandingFramesPerAgreement = getOutstandingFramesPerAgreement(inProgressFrames, blockAckAgreementHandler); + auto largestOutstandingFrames = outstandingFramesPerAgreement.end(); + for (auto it = outstandingFramesPerAgreement.begin(); it != outstandingFramesPerAgreement.end(); it++) { + if ((int)it->second.size() >= blockAckReqThreshold && (largestOutstandingFrames == outstandingFramesPerAgreement.end() || it->second.size() > largestOutstandingFrames->second.size())) + largestOutstandingFrames = it; + } + if (largestOutstandingFrames != outstandingFramesPerAgreement.end()) { + MacAddress receiverAddress = largestOutstandingFrames->first.first; + Tid tid = largestOutstandingFrames->first.second; + SequenceNumberCyclic startingSequenceNumber = computeStartingSequenceNumber(largestOutstandingFrames->second); + return std::make_tuple(receiverAddress, startingSequenceNumber, tid); } return std::make_tuple(MacAddress::UNSPECIFIED_ADDRESS, SequenceNumberCyclic(), -1); } AckPolicy OriginatorQosAckPolicy::computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const { - if (agreement == nullptr) + if (agreement == nullptr || agreement->isInactivityExpired()) return AckPolicy::NORMAL_ACK; if (agreement->getIsAddbaResponseReceived() && isBlockAckPolicyEligibleFrame(packet, header)) { if (checkAgreementPolicy(header, agreement)) @@ -140,4 +147,3 @@ simtime_t OriginatorQosAckPolicy::getBlockAckTimeout(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const; - virtual std::map> getOutstandingFramesPerReceiver(InProgressFrames *inProgressFrames) const; + virtual std::map, std::vector> getOutstandingFramesPerAgreement(InProgressFrames *inProgressFrames, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const; virtual SequenceNumberCyclic computeStartingSequenceNumber(const std::vector& outstandingFrames) const; virtual bool isCompressedBlockAckReq(const std::vector& outstandingFrames, int startingSequenceNumber) const; @@ -39,8 +39,8 @@ class INET_API OriginatorQosAckPolicy : public ModeSetListener, public IOriginat virtual bool isAckNeeded(const Ptr& header) const override; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const override; virtual bool isBlockAckPolicyEligibleFrame(Packet *packet, const Ptr& header) const override; - virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const override; - virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) const override; + virtual bool isBlockAckReqNeeded(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const override; + virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const override; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const override; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const override; @@ -50,4 +50,3 @@ class INET_API OriginatorQosAckPolicy : public ModeSetListener, public IOriginat } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index 59bdd8b4372..e0320e59c2b 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -178,6 +178,21 @@ void QosAckHandler::processFailedBlockAckReq(const Ptr& header) { if (header->getType() == ST_DATA_WITH_QOS) { @@ -276,6 +291,7 @@ std::string QosAckHandler::getStatusString(Status status) case Status::BLOCK_ACK_ARRIVED_ACKED: return "BLOCK_ACK_ARRIVED_ACKED"; case Status::WAITING_FOR_BLOCK_ACK: return "WAITING_FOR_BLOCK_ACK"; case Status::NORMAL_ACK_ARRIVED: return "NORMAL_ACK_ARRIVED"; + case Status::BLOCK_ACK_NOT_ARRIVED: return "BLOCK_ACK_NOT_ARRIVED"; default: throw cRuntimeError("Unknown status"); } } @@ -291,4 +307,3 @@ void QosAckHandler::printAckStatuses() } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h index 7c1e0bce61c..f2b7749e99d 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.h @@ -57,6 +57,10 @@ class INET_API QosAckHandler : public SimpleModule, public IAckHandler virtual void processReceivedAck(const Ptr& ack, const Ptr& ackedHeader); virtual std::set>> processReceivedBlockAck(const Ptr& blockAck); virtual void processFailedBlockAckReq(const Ptr& blockAckReq); + // Makes all outstanding BA-policy frames for this peer/TID retryable + // through the normal acknowledgment path when the agreement is no longer + // available to the data plane. + virtual bool releaseBlockAckAgreementFrames(MacAddress peerAddress, Tid tid); virtual void frameGotInProgress(const Ptr& dataOrMgmtHeader) override; virtual void processTransmittedDataOrMgmtFrame(const Ptr& header); @@ -78,4 +82,3 @@ class INET_API QosAckHandler : public SimpleModule, public IAckHandler } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index 29f793f0110..48e33da6c92 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -8,6 +8,7 @@ #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" #include "inet/common/ModuleAccess.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" namespace inet { namespace ieee80211 { @@ -62,7 +63,7 @@ bool RecipientQosAckPolicy::isAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) const { if (dynamicPtrCast(blockAckReq)) { - return agreement != nullptr; + return agreement != nullptr && !agreement->isInactivityExpired(); // TODO The Basic BlockAckReq frame shall be discarded if all MSDUs referenced by this // frame have been discarded from the transmit buffer due to expiry of their lifetime limit. } @@ -96,4 +97,3 @@ simtime_t RecipientQosAckPolicy::computeBasicBlockAckDurationField(Packet *packe } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 624c38c448e..a190318c919 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -128,6 +128,20 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data take(dataPacket); expireReceiveLifetime(); // TODO A-MPDU Deaggregation, MPDU Header+FCS Validation, Address1 Filtering, Duplicate Removal, MPDU Decryption + RecipientBlockAckAgreement *agreement = nullptr; + if (dataHeader->getAckPolicy() == AckPolicy::BLOCK_ACK) { + if (blockAckAgreementHandler != nullptr) + agreement = blockAckAgreementHandler->getActiveAgreement(dataHeader->getTid(), dataHeader->getTransmitterAddress()); + if (agreement == nullptr) { + EV_INFO << "Dropping Block Ack policy data without an active Block Ack agreement.\n"; + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, dataPacket, &details); + delete dataPacket; + scheduleReceiveLifetimeTimer(); + return std::vector(); + } + } if (duplicateRemoval && duplicateRemoval->isDuplicate(dataHeader)) { EV_WARN << "Dropping duplicate packet " << *dataPacket << ".\n"; PacketDropDetails details; @@ -142,7 +156,8 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data if (blockAckReordering && blockAckAgreementHandler) { Tid tid = dataHeader->getTid(); MacAddress originatorAddr = dataHeader->getTransmitterAddress(); - RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); + if (agreement == nullptr) + agreement = blockAckAgreementHandler->getActiveAgreement(tid, originatorAddr); if (agreement) { auto processingResult = blockAckReordering->processReceivedQoSFrameWithResult(agreement, dataPacket, dataHeader); frames = processingResult.frames; @@ -239,7 +254,7 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c if (blockAckReordering) { Tid tid = blockAckReq->getTidInfo(); MacAddress originatorAddr = blockAckReq->getTransmitterAddress(); - RecipientBlockAckAgreement *agreement = blockAckAgreementHandler->getAgreement(tid, originatorAddr); + RecipientBlockAckAgreement *agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(tid, originatorAddr); if (agreement) frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReq); else { diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index e33fd1ff2bf..b3db38f75e4 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -20,6 +20,7 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/blockack/Ieee80211BlockAckAgreementTag_m.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h" #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" @@ -33,12 +34,14 @@ Test IEEE 802.11 ADDBA transactions, reordering reset, fragmentation cleanup, DC #include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/framesequence/HcfFs.h" +#include "inet/linklayer/ieee80211/mac/framesequence/TxOpFs.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/originator/NonQosRecoveryProcedure.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientAckProcedure.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientMacDataService.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" #include "inet/linklayer/ieee80211/mac/sequencenumberassignment/QoSSequenceNumberAssignment.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" @@ -95,8 +98,10 @@ class TestRecipientPolicy : public IRecipientBlockAckAgreementPolicy class TestRecipientAckPolicy : public IRecipientAckPolicy, public IRecipientQosAckPolicy { public: + bool blockAckNeeded = false; + virtual bool isAckNeeded(const Ptr&) const override { return true; } - virtual bool isBlockAckNeeded(const Ptr&, RecipientBlockAckAgreement *) const override { return false; } + virtual bool isBlockAckNeeded(const Ptr&, RecipientBlockAckAgreement *agreement) const override { return blockAckNeeded && agreement != nullptr; } virtual simtime_t computeAckDurationField(Packet *, const Ptr&) const override { return 0; } virtual simtime_t computeBasicBlockAckDurationField(Packet *, const Ptr&) const override { return 0; } }; @@ -144,6 +149,17 @@ class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandler } }; +class TestControlResponseCallback : public TestCallback +{ + public: + int numControlResponses = 0; + + virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr&, Packet *, const Ptr&) override { + numControlResponses++; + delete responsePacket; + } +}; + class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler { public: @@ -163,6 +179,15 @@ class TestOriginatorHandler : public OriginatorBlockAckAgreementHandler void publishInactivityDeadline(IBlockAckAgreementHandlerCallback *callback) { scheduleInactivityTimer(callback); } }; +class TestOriginatorQosAckPolicy : public OriginatorQosAckPolicy +{ + public: + void configureBlockAckSelection(int maxFrameLength, int threshold) { + maxBlockAckPolicyFrameLength = maxFrameLength; + blockAckReqThreshold = threshold; + } +}; + class TestTimeoutCancellationCallback : public TestCallback { public: @@ -183,6 +208,13 @@ class TestRecipientHandler : public RecipientBlockAckAgreementHandler int getNumCachedAddbaResponses() const { return lastAddbaResponses.size(); } }; +class TestRecipientBlockAckProcedure : public RecipientBlockAckProcedure +{ + public: + int getNumReceivedBlockAckReq() const { return numReceivedBlockAckReq; } + int getNumSentBlockAck() const { return numSentBlockAck; } +}; + class TestBlockAckReordering : public BlockAckReordering { public: @@ -633,6 +665,7 @@ class TestHcf : public Hcf std::vector cancelledBlockAckTeardownGenerationIds; int numRebuildEligibilityCalls = 0; int numTransmittedControlResponses = 0; + bool managementProcessedDuringBlockAckExpiry = false; bool droppedSetupCancelled = false; bool delegateDroppedSetupHandling = false; TestCallback *managementCallback = nullptr; @@ -696,6 +729,11 @@ class TestHcf : public Hcf auto lastTransmittedHeader = lastTransmittedPacket->peekAtFront(); originatorProcessReceivedControlFrame(packet, header, lastTransmittedPacket, lastTransmittedHeader, accessCategory); } + void processInactivityTimeout() { + if (inactivityTimer == nullptr) + inactivityTimer = new cMessage("BlockAckInactivityTimer"); + handleMessage(inactivityTimer); + } protected: virtual void transmitControlResponseFrame(Packet *responsePacket, const Ptr&, Packet *, const Ptr&) override { @@ -703,6 +741,7 @@ class TestHcf : public Hcf delete responsePacket; } virtual void processMgmtFrame(Packet *packet, const Ptr& header) override { + managementProcessedDuringBlockAckExpiry |= blockAckInactivityExpiryInProgress; if (managementCallback != nullptr) managementCallback->processMgmtFrame(packet, header); else @@ -2627,28 +2666,28 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; auto agreement = agreementHandler.processReceivedAddbaRequest(request, &policy, &callback, &callback); ASSERT(agreement != nullptr); - auto makeServiceFragment = [&](const char *name, MacAddress transmitterAddress, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto makeServiceFragment = [&](const char *name, MacAddress transmitterAddress, SequenceNumberCyclic sequenceNumber, int fragmentNumber, bool moreFragments, uint8_t payloadByte, AckPolicy ackPolicy) { auto dataHeader = makeQosHeader(peer2, 6, sequenceNumber); dataHeader->setTransmitterAddress(transmitterAddress); dataHeader->setFragmentNumber(fragmentNumber); dataHeader->setMoreFragments(moreFragments); - dataHeader->setAckPolicy(BLOCK_ACK); + dataHeader->setAckPolicy(ackPolicy); auto packet = new Packet(name, dataHeader); packet->insertAtBack(makeShared(std::vector({ payloadByte }))); packet->insertAtBack(makeShared()); return packet; }; - auto reorderFirstFragment = makeServiceFragment("serviceReorderFirstFragment", peer1, SequenceNumberCyclic(300), 1, true, 0x71); + auto reorderFirstFragment = makeServiceFragment("serviceReorderFirstFragment", peer1, SequenceNumberCyclic(300), 1, true, 0x71, BLOCK_ACK); ASSERT(service->dataFrameReceived(reorderFirstFragment, reorderFirstFragment->peekAtFront(), &agreementHandler).empty()); - auto reorderFinalFragment = makeServiceFragment("serviceReorderFinalFragment", peer1, SequenceNumberCyclic(300), 2, false, 0x72); + auto reorderFinalFragment = makeServiceFragment("serviceReorderFinalFragment", peer1, SequenceNumberCyclic(300), 2, false, 0x72, BLOCK_ACK); ASSERT(service->dataFrameReceived(reorderFinalFragment, reorderFinalFragment->peekAtFront(), &agreementHandler).empty()); ASSERT(dropListener.numSignals == 0); wait(0.5e-6); // No agreement for peer3: this fragment is retained by BasicReassembly, // with a later deadline than the already retained reorder fragments. - auto basicFragment = makeServiceFragment("serviceBasicFragment", peer3, SequenceNumberCyclic(350), 0, true, 0x73); + auto basicFragment = makeServiceFragment("serviceBasicFragment", peer3, SequenceNumberCyclic(350), 0, true, 0x73, NORMAL_ACK); ASSERT(service->dataFrameReceived(basicFragment, basicFragment->peekAtFront(), &agreementHandler).empty()); wait(1.6e-6); ASSERT(dropListener.numSignals == 2); @@ -2657,7 +2696,7 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; // The expired sequence remains tombstoned until the reorder window moves // past it. A late fragment is rejected and reported once by the service, // while no hybrid or partial frame is delivered. - auto lateReorderFragment = makeServiceFragment("serviceLateReorderFragment", peer1, SequenceNumberCyclic(300), 0, true, 0x74); + auto lateReorderFragment = makeServiceFragment("serviceLateReorderFragment", peer1, SequenceNumberCyclic(300), 0, true, 0x74, BLOCK_ACK); ASSERT(service->dataFrameReceived(lateReorderFragment, lateReorderFragment->peekAtFront(), &agreementHandler).empty()); ASSERT(dropListener.numSignals == 3); ASSERT(dropListener.numOtherPacketDrops == 3); @@ -2691,7 +2730,7 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; dataHeader->setTransmitterAddress(transmitterAddress); dataHeader->setFragmentNumber(fragmentNumber); dataHeader->setMoreFragments(moreFragments); - dataHeader->setAckPolicy(BLOCK_ACK); + dataHeader->setAckPolicy(NORMAL_ACK); auto packet = new Packet(name, dataHeader); packet->insertAtBack(makeShared(std::vector({ payloadByte }))); packet->insertAtBack(makeShared()); @@ -5614,6 +5653,7 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) request->setBlockAckTimeoutValue(3); auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &setupCallback, &setupCallback); ASSERT(agreement != nullptr); + ASSERT(recipientHandler.getActiveAgreement(12, peer2) == agreement); auto creationTime = simTime(); ASSERT(creationTime > 0); ASSERT(agreement->getExpirationTime() == creationTime + 3); @@ -5633,6 +5673,7 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) TestCallback expiryCallback; recipientHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); ASSERT(recipientHandler.getAgreement(12, peer2) == agreement); + ASSERT(recipientHandler.getActiveAgreement(12, peer2) == nullptr); ASSERT(agreement->isInactivityExpired()); ASSERT(expiryCallback.managementPackets.size() == 1); ASSERT(expiryCallback.recipientInactivityDeadline == SIMTIME_MAX); @@ -5654,6 +5695,7 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) TestOriginatorHandler originatorHandler; originatorHandler.addEstablishedAgreement(peer1, 13); auto originatorAgreement = originatorHandler.getAgreement(peer1, 13); + ASSERT(originatorHandler.getActiveAgreement(peer1, 13) == originatorAgreement); originatorAgreement->setBlockAckTimeoutValue(2); originatorAgreement->calculateExpirationTime(); auto originatorCreationTime = simTime(); @@ -5663,6 +5705,7 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) wait(3); originatorHandler.blockAckAgreementExpired(&originatorCallback, &originatorCallback); ASSERT(originatorHandler.getAgreement(peer1, 13) == originatorAgreement); + ASSERT(originatorHandler.getActiveAgreement(peer1, 13) == nullptr); ASSERT(originatorAgreement->isInactivityExpired()); ASSERT(originatorCallback.managementPackets.size() == 1); ASSERT(originatorCallback.originatorInactivityDeadline == SIMTIME_MAX); @@ -5683,6 +5726,255 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) } +// Expiry quarantines the retained originator agreement from ACK/BAR selection. +// HCF also releases both kinds of outstanding Block Ack state before resuming +// channel access, so those frames can be retried with Normal Ack. +{ + auto originatorHandler = new TestOriginatorHandler(); + originatorHandler->addEstablishedAgreement(peer1, 4); + auto agreement = originatorHandler->getAgreement(peer1, 4); + agreement->setBlockAckTimeoutValue(1); + agreement->calculateExpirationTime(); + + TestOriginatorQosAckPolicy ackPolicy; + ackPolicy.configureBlockAckSelection(1000, 1); + auto makeDataPacket = [&](const char *name, int sequenceNumber, AckPolicy ackPolicyValue) { + auto header = makeQosHeader(peer1, 4, SequenceNumberCyclic(sequenceNumber)); + header->setAckPolicy(ackPolicyValue); + return new Packet(name, header); + }; + + auto waitingFrame = makeDataPacket("expiryWaitingForBlockAck", 500, BLOCK_ACK); + auto waitingHeader = waitingFrame->peekAtFront(); + auto notRequestedFrame = makeDataPacket("expiryBlockAckNotRequested", 501, BLOCK_ACK); + auto notRequestedHeader = notRequestedFrame->peekAtFront(); + auto eligibleFrame = makeDataPacket("expiryEligibleFrame", 502, NORMAL_ACK); + auto eligibleHeader = eligibleFrame->peekAtFront(); + auto completingFrame = makeDataPacket("expiryDuringTransmission", 503, BLOCK_ACK); + auto completingHeader = completingFrame->peekAtFront(); + ASSERT(ackPolicy.computeAckPolicy(eligibleFrame, eligibleHeader, agreement) == BLOCK_ACK); + + TestOriginatorQosMacDataService dataService; + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + inProgressFrames.addFrame(waitingFrame); + inProgressFrames.addFrame(notRequestedFrame); + inProgressFrames.addFrame(eligibleFrame); + inProgressFrames.addFrame(completingFrame); + ackHandler.frameGotInProgress(waitingHeader); + ackHandler.processTransmittedDataOrMgmtFrame(waitingHeader); + auto blockAckReq = makeShared(); + blockAckReq->setReceiverAddress(peer1); + blockAckReq->setTidInfo(4); + blockAckReq->setStartingSequenceNumber(SequenceNumberCyclic(500)); + ackHandler.processTransmittedBlockAckReq(blockAckReq); + ackHandler.frameGotInProgress(notRequestedHeader); + ackHandler.processTransmittedDataOrMgmtFrame(notRequestedHeader); + ackHandler.frameGotInProgress(completingHeader); + ASSERT(ackHandler.getQoSDataAckStatus(waitingHeader) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(ackHandler.getQoSDataAckStatus(notRequestedHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(ackHandler.getQoSDataAckStatus(completingHeader) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + + TestTxopProcedure txopProcedure; + FrameSequenceContext context(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &inProgressFrames, nullptr, nullptr, nullptr, + new QoSContext(&ackPolicy, nullptr, originatorHandler, &txopProcedure)); + TxOpFs txOpFs; + ASSERT(txOpFs.selectTxOpSequence(&txOpFs, &context) == 2); + + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestCallback managementCallback; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureManagementCallback(&managementCallback); + + wait(1); + hcf.processInactivityTimeout(); + ASSERT(originatorHandler->getAgreement(peer1, 4) == agreement); + ASSERT(originatorHandler->getActiveAgreement(peer1, 4) == nullptr); + ASSERT(agreement->isInactivityExpired()); + ASSERT(managementCallback.managementPackets.size() == 1); + ASSERT(hcf.managementProcessedDuringBlockAckExpiry); + ASSERT(ackHandler.getQoSDataAckStatus(waitingHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.getQoSDataAckStatus(notRequestedHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.getQoSDataAckStatus(completingHeader) == QosAckHandler::Status::FRAME_NOT_YET_TRANSMITTED); + hcf.processTransmittedData(completingFrame, completingHeader, AC_BE); + ASSERT(ackHandler.getQoSDataAckStatus(completingHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.isEligibleToTransmit(waitingHeader)); + ASSERT(ackHandler.isEligibleToTransmit(notRequestedHeader)); + ASSERT(ackHandler.isEligibleToTransmit(completingHeader)); + ASSERT(hcf.numRebuildEligibilityCalls == 1); + ASSERT(hcf.numResumedEligibleChannelAccess == 1); + ASSERT(ackPolicy.computeAckPolicy(eligibleFrame, eligibleHeader, agreement) == NORMAL_ACK); + ASSERT(txOpFs.selectTxOpSequence(&txOpFs, &context) == 1); + + auto lateBlockAck = makeShared(); + lateBlockAck->setTransmitterAddress(peer1); + lateBlockAck->setTidInfo(4); + lateBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(500)); + auto lateBlockAckPacket = new Packet("postExpiryBlockAck", lateBlockAck); + auto transmittedBlockAckReqPacket = new Packet("preExpiryBlockAckReq", blockAckReq); + hcf.processReceivedAck(lateBlockAckPacket, transmittedBlockAckReqPacket, AC_BE); + ASSERT(ackHandler.getQoSDataAckStatus(waitingHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.getQoSDataAckStatus(notRequestedHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + delete lateBlockAckPacket; + delete transmittedBlockAckReqPacket; + + auto frames = inProgressFrames.releaseFrames(); + for (auto frame : frames) + delete frame; +} + +// Any terminal originator agreement removal releases outstanding BA-policy +// frames as well. Otherwise active-agreement BAR filtering would leave those +// frames permanently ineligible after peer or local DELBA teardown. +{ + auto originatorHandler = new TestOriginatorHandler(); + auto recipientHandler = new TestRecipientHandler(); + TestOriginatorPolicy originatorPolicy; + TestRecipientPolicy recipientPolicy; + TestOriginatorQosAckPolicy ackPolicy; + ackPolicy.configureBlockAckSelection(1000, 1); + TestOriginatorQosMacDataService dataService; + TestPacketQueue pendingQueue; + TestQosAckHandler ackHandler; + TestInProgressFrames inProgressFrames; + inProgressFrames.configure(&dataService, &ackHandler, &pendingQueue); + TestEdcaf edcaf; + edcaf.pendingQueue = &pendingQueue; + edcaf.inProgressFrames = &inProgressFrames; + edcaf.qosAckHandler = &ackHandler; + TestEdca edca; + edca.edcaf = &edcaf; + TestHcf hcf; + hcf.configureBlockAckHandlers(originatorHandler, &originatorPolicy, recipientHandler, &recipientPolicy); + hcf.configureEligibilityIndex(&edca, &dataService); + hcf.configureTransmittedManagement(&edca, &ackPolicy); + + originatorHandler->addEstablishedAgreement(peer1, 6); + auto peerTeardownHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(520)); + peerTeardownHeader->setAckPolicy(BLOCK_ACK); + auto peerTeardownFrame = new Packet("peerDelbaOutstandingData", peerTeardownHeader); + inProgressFrames.addFrame(peerTeardownFrame); + ackHandler.frameGotInProgress(peerTeardownHeader); + ackHandler.processTransmittedDataOrMgmtFrame(peerTeardownHeader); + ASSERT(ackHandler.getQoSDataAckStatus(peerTeardownHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + auto peerDelba = makeShared(); + peerDelba->setTransmitterAddress(peer1); + peerDelba->setTid(6); + peerDelba->setInitiator(false); + hcf.processReceivedManagementFrame(peerDelba); + ASSERT(originatorHandler->getAgreement(peer1, 6) == nullptr); + ASSERT(ackHandler.getQoSDataAckStatus(peerTeardownHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.isEligibleToTransmit(peerTeardownHeader)); + + originatorHandler->addEstablishedAgreement(peer1, 7); + auto localTeardownAgreement = originatorHandler->getAgreement(peer1, 7); + auto localTeardownHeader = makeQosHeader(peer1, 7, SequenceNumberCyclic(530)); + localTeardownHeader->setAckPolicy(BLOCK_ACK); + auto localTeardownFrame = new Packet("localDelbaOutstandingData", localTeardownHeader); + inProgressFrames.addFrame(localTeardownFrame); + ackHandler.frameGotInProgress(localTeardownHeader); + ackHandler.processTransmittedDataOrMgmtFrame(localTeardownHeader); + ASSERT(ackHandler.getQoSDataAckStatus(localTeardownHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + auto localDelbaPacket = makeTaggedDelbaPacket("localTerminalDelba", peer1, 7, SequenceNumberCyclic(531), 0, false, localTeardownAgreement->getTransactionId()); + auto localDelba = localDelbaPacket->peekAtFront(); + hcf.processTransmittedManagement(localDelbaPacket, localDelba, AC_BE); + ASSERT(originatorHandler->getAgreement(peer1, 7) == nullptr); + ASSERT(ackHandler.getQoSDataAckStatus(localTeardownHeader) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + ASSERT(ackHandler.isEligibleToTransmit(localTeardownHeader)); + delete localDelbaPacket; + + auto frames = inProgressFrames.releaseFrames(); + for (auto frame : frames) + delete frame; +} + +// An expired recipient agreement remains available to the DELBA lifecycle, +// but BA-policy data is dropped, BAR cannot release reordering state, and no +// Block Ack response is generated. +{ + TestRecipientHandler recipientHandler; + TestRecipientPolicy recipientPolicy; + recipientPolicy.blockAckTimeoutValue = 1; + TestCallback setupCallback; + auto request = makeRequest(peer2, 5, 111, SequenceNumberCyclic(600), 32); + request->setBlockAckTimeoutValue(1); + auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &setupCallback, &setupCallback); + ASSERT(agreement != nullptr); + ASSERT(recipientHandler.getActiveAgreement(5, peer2) == agreement); + + auto serviceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("expiredAgreementDataService", this); + serviceModule->callInitialize(); + auto service = check_and_cast(serviceModule); + TestPacketDropSignalListener dropListener; + service->subscribe(packetDroppedSignal, &dropListener); + + auto bufferedHeader = makeQosHeader(peer1, 5, SequenceNumberCyclic(601)); + bufferedHeader->setTransmitterAddress(peer2); + bufferedHeader->setAckPolicy(BLOCK_ACK); + auto bufferedPacket = new Packet("preExpiryBufferedData", bufferedHeader); + bufferedPacket->insertAtBack(makeShared()); + ASSERT(service->dataFrameReceived(bufferedPacket, bufferedHeader, &recipientHandler).empty()); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(601), 0)); + + auto blockAckReq = makeShared(); + blockAckReq->setTransmitterAddress(peer2); + blockAckReq->setReceiverAddress(peer1); + blockAckReq->setTidInfo(5); + blockAckReq->setStartingSequenceNumber(SequenceNumberCyclic(601)); + auto blockAckReqPacket = new Packet("expiredAgreementBar", blockAckReq); + RecipientQosAckPolicy productionAckPolicy; + ASSERT(productionAckPolicy.isBlockAckNeeded(blockAckReq, agreement)); + TestRecipientAckPolicy ackPolicy; + ackPolicy.blockAckNeeded = true; + TestRecipientBlockAckProcedure blockAckProcedure; + TestControlResponseCallback responseCallback; + blockAckProcedure.processReceivedBlockAckReq(blockAckReqPacket, blockAckReq, &ackPolicy, &recipientHandler, &responseCallback); + ASSERT(responseCallback.numControlResponses == 1); + + wait(1); + TestCallback expiryCallback; + recipientHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); + ASSERT(recipientHandler.getAgreement(5, peer2) == agreement); + ASSERT(recipientHandler.getActiveAgreement(5, peer2) == nullptr); + ASSERT(agreement->isInactivityExpired()); + ASSERT(!productionAckPolicy.isBlockAckNeeded(blockAckReq, agreement)); + + auto expiredHeader = makeQosHeader(peer1, 5, SequenceNumberCyclic(602)); + expiredHeader->setTransmitterAddress(peer2); + expiredHeader->setAckPolicy(BLOCK_ACK); + auto expiredPacket = new Packet("postExpiryBlockAckData", expiredHeader); + expiredPacket->insertAtBack(makeShared()); + ASSERT(service->dataFrameReceived(expiredPacket, expiredHeader, &recipientHandler).empty()); + ASSERT(dropListener.numSignals == 1); + ASSERT(dropListener.numOtherPacketDrops == 1); + ASSERT(!agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(602), 0)); + + ASSERT(service->controlFrameReceived(blockAckReqPacket, blockAckReq, &recipientHandler).empty()); + ASSERT(dropListener.numSignals == 1); + blockAckProcedure.processReceivedBlockAckReq(blockAckReqPacket, blockAckReq, &ackPolicy, &recipientHandler, &responseCallback); + ASSERT(responseCallback.numControlResponses == 1); + + service->resetBlockAckReordering(5, peer2); + ASSERT(dropListener.numSignals == 2); + ASSERT(dropListener.numOtherPacketDrops == 2); + service->unsubscribe(packetDroppedSignal, &dropListener); + serviceModule->callFinish(); + serviceModule->deleteModule(); + delete blockAckReqPacket; +} + // A timeout DELBA can be aborted before its final fragment is transmitted. // The matching generation is retired locally, and the returned ownership lets // HCF release role-specific resources without rearming a timed-out agreement. From 2b7f2b132b743b19623c86550d5ae4bd4a7f3e1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:05 +0200 Subject: [PATCH 38/56] ieee80211: fix recipient BAR and ADDBA timeout handling Ignore null defragmentation results when a BAR releases buffered fragments. This prevents half-sequence-space reassembly rejections from reaching A-MSDU deaggregation and crashing the recipient data path. Resolve the recipient Block Ack timeout according to the policy contract: inherit the originator request when the recipient policy is zero, otherwise use the configured recipient override. The negotiated response value continues to drive agreement state and inactivity deadlines. Add focused production-path coverage for the exact 2048 sequence boundary, subsequent reorder progress, timeout inheritance and override, the zero/no-timeout case, expiration timing, and cached ADDBA responses. --- .../RecipientBlockAckAgreementHandler.cc | 2 +- .../recipient/RecipientQosMacDataService.cc | 4 +- tests/unit/Ieee80211AddbaTransaction_1.test | 163 ++++++++++++++++-- 3 files changed, 156 insertions(+), 13 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index e8a6cce5749..4dcad4f4bea 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -120,7 +120,7 @@ const Ptr RecipientBlockAckAgreementHandler::buildAddbaR addbaResponse->setTid(tid); addbaResponse->setBlockAckPolicy(!addbaRequest->getBlockAckPolicy() && blockAckAgreementPolicy->delayedBlockAckPolicySupported() ? false : true); addbaResponse->setBufferSize(addbaRequest->getBufferSize() <= blockAckAgreementPolicy->getMaximumAllowedBufferSize() ? addbaRequest->getBufferSize() : blockAckAgreementPolicy->getMaximumAllowedBufferSize()); - addbaResponse->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue() == 0 ? blockAckAgreementPolicy->getBlockAckTimeoutValue() : addbaRequest->getBlockAckTimeoutValue()); + addbaResponse->setBlockAckTimeoutValue(blockAckAgreementPolicy->getBlockAckTimeoutValue() == 0 ? addbaRequest->getBlockAckTimeoutValue() : blockAckAgreementPolicy->getBlockAckTimeoutValue()); addbaResponse->setAMsduSupported(blockAckAgreementPolicy->aMsduSupported()); return addbaResponse; } diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index a190318c919..ec3abe7b820 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -266,7 +266,9 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c if (basicReassembly) { // FIXME defragmentation for (auto it : frames) { auto fragments = it.second; - defragmentedFrames.push_back(defragment(fragments)); + auto frame = defragment(fragments); + if (frame != nullptr) + defragmentedFrames.push_back(frame); } } else { diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index b3db38f75e4..458eae5b8f7 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -1879,6 +1879,27 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(threw); } +// IEEE Std 802.11-2024, 10.25.2: the recipient policy may inherit the +// originator's advisory timeout when it does not configure an override. The +// response, installed agreement, and inactivity deadline must use that one +// negotiated value. +{ + RecipientBlockAckAgreementHandler recipientHandler; + TestRecipientPolicy recipientPolicy; + TestCallback responseCallback; + auto request = makeRequest(peer1, 8, 93, SequenceNumberCyclic(200), 32); + request->setBlockAckTimeoutValue(3); + auto agreementCreationTime = simTime(); + auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &responseCallback, &responseCallback); + ASSERT(agreement != nullptr); + auto response = dynamicPtrCast(responseCallback.managementHeader); + ASSERT(response != nullptr); + ASSERT(response->getBlockAckTimeoutValue() == 3); + ASSERT(agreement->getBlockAckTimeoutValue() == 3); + ASSERT(agreement->getExpirationTime() == agreementCreationTime + 3); + ASSERT(responseCallback.recipientInactivityDeadline == agreementCreationTime + 3); +} + // An initially rejected request has no agreement, so its response is never // replayable and must not consume replay-cache state. { @@ -1948,6 +1969,7 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); auto firstRequest = makeRequest(peer2, 6, 90, SequenceNumberCyclic(100), 32); firstRequest->setSequenceNumber(SequenceNumberCyclic(50)); firstRequest->setBlockAckTimeoutValue(3); + auto firstAgreementCreationTime = simTime(); auto firstReception = recipientDataService->managementFrameReceived(new Packet("firstAddbaRequest", firstRequest), firstRequest); ASSERT(!firstReception.duplicate); ASSERT(firstReception.completeFrames.empty()); @@ -1968,8 +1990,10 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(firstResponse->getTid() == 6); ASSERT(firstResponse->getBlockAckPolicy()); ASSERT(firstResponse->getBufferSize() == 32); - ASSERT(firstResponse->getBlockAckTimeoutValue() == 3); + ASSERT(firstResponse->getBlockAckTimeoutValue() == 1); ASSERT(firstResponse->getAMsduSupported()); + ASSERT(firstAgreement->getBlockAckTimeoutValue() == 1); + ASSERT(firstAgreement->getExpirationTime() == firstAgreementCreationTime + 1); auto bufferedHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(101)); bufferedHeader->setTransmitterAddress(peer2); @@ -2027,7 +2051,7 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); ASSERT(rejectedResponse->getStatusCode() != 0); ASSERT(rejectedResponse->getTid() == 6); ASSERT(rejectedResponse->getBufferSize() == 1); - ASSERT(rejectedResponse->getBlockAckTimeoutValue() == 4); + ASSERT(rejectedResponse->getBlockAckTimeoutValue() == 1); ASSERT(!rejectedResponse->getAMsduSupported()); auto duplicateRejectedRequest = makeRequest(peer2, 6, 91, SequenceNumberCyclic(200), 16); @@ -2422,8 +2446,13 @@ ASSERT(firstImmediateAgreement != nullptr); ASSERT(immediateRecipientHandler.getAgreement(6, peer1) == firstImmediateAgreement); ASSERT(firstImmediateAgreement->getStartingSequenceNumber() == SequenceNumberCyclic(0)); auto acceptedResponse = dynamicPtrCast(recipientCallback.managementHeader); +ASSERT(acceptedResponse != nullptr); ASSERT(acceptedResponse->getDialogToken() == 77); ASSERT(acceptedResponse->getStatusCode() == 0); +ASSERT(acceptedResponse->getBlockAckTimeoutValue() == 0); +ASSERT(firstImmediateAgreement->getBlockAckTimeoutValue() == 0); +ASSERT(firstImmediateAgreement->getExpirationTime() == SIMTIME_MAX); +ASSERT(recipientCallback.recipientInactivityDeadline == SIMTIME_MAX); // Renegotiation replaces the recipient agreement and the HCF reset boundary // must discard the old reorder window before frames use the new SSN. @@ -2543,6 +2572,112 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; reassembleOrderedFragments(barFragments, { 0x50, 0x51, 0x52, 0x53 }); } +// A complete fragmented MPDU released by a BAR at the exact sequence-space +// half boundary must be consumed safely when BasicReassembly rejects its +// ambiguous generation. The production QoS data service must not expose the +// resulting null packet to deaggregation or retain the released fragments. +{ + auto serviceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("halfSpaceBarDataService", this); + serviceModule->callInitialize(); + auto service = check_and_cast(serviceModule); + RecipientBlockAckAgreementHandler agreementHandler; + TestRecipientPolicy policy; + TestCallback callback; + + // Establish the BasicReassembly sequence-space high watermark at zero + // before the Block Ack receive buffer is created. + auto seedHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(0)); + seedHeader->setTransmitterAddress(peer1); + seedHeader->setAckPolicy(NORMAL_ACK); + seedHeader->setAMsduPresent(false); + auto seedPacket = new Packet("halfSpaceReassemblySeed", seedHeader); + auto seedFrames = service->dataFrameReceived(seedPacket, seedHeader, &agreementHandler); + ASSERT(seedFrames.size() == 1); + ASSERT(seedFrames.front() == seedPacket); + for (auto frame : seedFrames) { + take(frame); + drop(frame); + delete frame; + } + + auto request = makeRequest(peer1, 6, 94, SequenceNumberCyclic(2047), 64); + request->setReceiverAddress(peer2); + auto agreement = agreementHandler.processReceivedAddbaRequest(request, &policy, &callback, &callback); + ASSERT(agreement != nullptr); + ASSERT(agreement->getStartingSequenceNumber() == SequenceNumberCyclic(2047)); + + auto makeFragment = [&](const char *name, int fragmentNumber, bool moreFragments, uint8_t payloadByte) { + auto header = makeQosHeader(peer2, 6, SequenceNumberCyclic(2048)); + header->setTransmitterAddress(peer1); + header->setFragmentNumber(fragmentNumber); + header->setMoreFragments(moreFragments); + header->setAckPolicy(BLOCK_ACK); + header->setAMsduPresent(false); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(std::vector({ payloadByte }))); + packet->insertAtBack(makeShared()); + return packet; + }; + + auto firstFragment = makeFragment("halfSpaceBarFragment0", 0, true, 0x81); + auto secondFragment = makeFragment("halfSpaceBarFragment1", 1, false, 0x82); + ASSERT(service->dataFrameReceived(firstFragment, firstFragment->peekAtFront(), &agreementHandler).empty()); + ASSERT(service->dataFrameReceived(secondFragment, secondFragment->peekAtFront(), &agreementHandler).empty()); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(2048), 0)); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(2048), 1)); + + TestSignalListener defragmentedListener; + TestSignalListener deaggregatedListener; + TestPacketDropSignalListener resetDropListener; + service->subscribe(IRecipientQosMacDataService::packetDefragmentedSignal, &defragmentedListener); + service->subscribe(IRecipientQosMacDataService::packetDeaggregatedSignal, &deaggregatedListener); + service->subscribe(packetDroppedSignal, &resetDropListener); + + auto barHeader = makeShared(); + barHeader->setTransmitterAddress(peer1); + barHeader->setReceiverAddress(peer2); + barHeader->setTidInfo(6); + barHeader->setStartingSequenceNumber(SequenceNumberCyclic(2048)); + auto barPacket = new Packet("halfSpaceBar", barHeader); + auto releasedFrames = service->controlFrameReceived(barPacket, barHeader, &agreementHandler); + ASSERT(releasedFrames.empty()); + ASSERT(defragmentedListener.numSignals == 0); + ASSERT(deaggregatedListener.numSignals == 0); + delete barPacket; + + // getAckState() intentionally reports older or absent sequence controls + // as acknowledged, so it cannot expose removal of sequence 2048. A + // complete next sequence released immediately proves that the BAR did + // advance the reorder window and the new Block Ack state is recorded. + auto nextHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(2049)); + nextHeader->setTransmitterAddress(peer1); + nextHeader->setAckPolicy(BLOCK_ACK); + nextHeader->setAMsduPresent(false); + auto nextPacket = new Packet("halfSpaceBarFollowingFrame", nextHeader); + auto nextFrames = service->dataFrameReceived(nextPacket, nextHeader, &agreementHandler); + ASSERT(nextFrames.size() == 1); + ASSERT(nextFrames.front() == nextPacket); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(2049), 0)); + ASSERT(defragmentedListener.numSignals == 1); + ASSERT(deaggregatedListener.numSignals == 0); + for (auto frame : nextFrames) { + take(frame); + drop(frame); + delete frame; + } + + // resetBlockAckReordering is a public owner-side observation: no drops + // prove that the BAR already removed the complete MPDU from the reorder + // buffer before BasicReassembly consumed its rejected fragments. + service->resetBlockAckReordering(6, peer1); + ASSERT(resetDropListener.numSignals == 0); + service->unsubscribe(packetDroppedSignal, &resetDropListener); + service->unsubscribe(IRecipientQosMacDataService::packetDeaggregatedSignal, &deaggregatedListener); + service->unsubscribe(IRecipientQosMacDataService::packetDefragmentedSignal, &defragmentedListener); + serviceModule->callFinish(); + serviceModule->deleteModule(); +} + // Receive-buffer lifetime starts when the first fragment of an incomplete // fragmented MPDU is retained. Later fragments do not refresh that deadline; // complete MPDUs held only for ordering remain available until normal window @@ -5591,14 +5726,14 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) { TestRecipientHandler expiryHandler; TestRecipientPolicy expiryPolicy; - expiryPolicy.blockAckTimeoutValue = 1; + expiryPolicy.blockAckTimeoutValue = 2; TestCallback expiryCallback; auto request = makeRequest(peer2, 10, 101, SequenceNumberCyclic(310), 32); - request->setBlockAckTimeoutValue(1); + request->setBlockAckTimeoutValue(3); auto firstAgreement = expiryHandler.processReceivedAddbaRequest(request, &expiryPolicy, &expiryCallback, &expiryCallback); ASSERT(firstAgreement != nullptr); auto firstGenerationId = firstAgreement->getGenerationId(); - wait(1); + wait(2); expiryHandler.blockAckAgreementExpired(&expiryCallback, &expiryCallback); auto stalePacket = expiryCallback.managementPacket; auto staleDelba = dynamicPtrCast(expiryCallback.managementHeader); @@ -5647,17 +5782,23 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) wait(1); TestRecipientHandler recipientHandler; TestRecipientPolicy recipientPolicy; - recipientPolicy.blockAckTimeoutValue = 3; + recipientPolicy.blockAckTimeoutValue = 2; TestCallback setupCallback; auto request = makeRequest(peer2, 12, 109, SequenceNumberCyclic(330), 32); request->setBlockAckTimeoutValue(3); + auto agreementCreationTime = simTime(); auto agreement = recipientHandler.processReceivedAddbaRequest(request, &recipientPolicy, &setupCallback, &setupCallback); ASSERT(agreement != nullptr); ASSERT(recipientHandler.getActiveAgreement(12, peer2) == agreement); auto creationTime = simTime(); ASSERT(creationTime > 0); - ASSERT(agreement->getExpirationTime() == creationTime + 3); - ASSERT(setupCallback.recipientInactivityDeadline == creationTime + 3); + auto response = dynamicPtrCast(setupCallback.managementHeader); + ASSERT(response != nullptr); + ASSERT(response->getBlockAckTimeoutValue() == 2); + ASSERT(agreement->getBlockAckTimeoutValue() == 2); + ASSERT(agreement->getExpirationTime() == agreementCreationTime + 2); + ASSERT(agreement->getExpirationTime() == creationTime + 2); + ASSERT(setupCallback.recipientInactivityDeadline == creationTime + 2); auto basicBlockAckReq = makeShared(); basicBlockAckReq->setTransmitterAddress(peer2); @@ -5665,9 +5806,9 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) wait(1); auto barReceptionTime = simTime(); recipientHandler.blockAckReqReceived(basicBlockAckReq, &setupCallback); - ASSERT(agreement->getExpirationTime() == barReceptionTime + 3); - ASSERT(agreement->getExpirationTime() > creationTime + 3); - ASSERT(setupCallback.recipientInactivityDeadline == barReceptionTime + 3); + ASSERT(agreement->getExpirationTime() == barReceptionTime + 2); + ASSERT(agreement->getExpirationTime() > creationTime + 2); + ASSERT(setupCallback.recipientInactivityDeadline == barReceptionTime + 2); wait(4); TestCallback expiryCallback; From cc32039d534b84b70903961a20ab8aab54dde460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:05 +0200 Subject: [PATCH 39/56] ieee80211: enforce valid A-MSDU fragmentation and sizing Keep A-MSDUs intact in BasicFragmentationPolicy even when their MPDU length exceeds the configured fragmentation threshold. The basic policy does not implement the capability-gated HE dynamic fragmentation procedure, so splitting these aggregates would produce an invalid fragment sequence. Track the exact serialized A-MSDU body length in BasicMsduAggregationPolicy. Account for the 4-octet alignment padding added after each subframe that becomes non-final, leave the final subframe unpadded, accept an aggregate exactly at the configured maximum, and preserve -1 as the unlimited setting. Add focused regression coverage for oversized A-MSDUs versus ordinary QoS frames, one-, two-, and three-octet padding boundaries, exact-limit acceptance, final-subframe layout, and the unlimited size configuration. --- .../aggregation/BasicMsduAggregationPolicy.cc | 22 +++- .../fragmentation/BasicFragmentationPolicy.cc | 8 +- tests/unit/Ieee80211AddbaTransaction_1.test | 107 ++++++++++++++++++ 3 files changed, 131 insertions(+), 6 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc index 951f011c5ee..a71aaf58c34 100644 --- a/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/aggregation/BasicMsduAggregationPolicy.cc @@ -10,6 +10,16 @@ namespace inet { namespace ieee80211 { +namespace { + +b computeSerializedAmsduLength(b aMsduLength, const Packet *packet, const Ptr& header, const Ptr& trailer) +{ + int paddingLength = (4 - aMsduLength.get() % 4) % 4; + return aMsduLength + B(paddingLength) + packet->getDataLength() - header->getChunkLength() - trailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER); +} + +} + Define_Module(BasicMsduAggregationPolicy); void BasicMsduAggregationPolicy::initialize() @@ -32,10 +42,12 @@ bool BasicMsduAggregationPolicy::isEligible(Packet *packet, const PtrgetType() != ST_DATA_WITH_QOS) return false; - // The maximum MPDU length that can be transported using A-MPDU aggregation is 4095 octets. An - // A-MSDU cannot be fragmented. Therefore, an A-MSDU of a length that exceeds 4065 octets ( - // 4095 minus the QoS data MPDU overhead) cannot be transported in an A-MPDU. - if (aMsduLength + packet->getDataLength() - header->getChunkLength() - trailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER) > maxAMsduSize) // default value of maxAMsduSize is 4065 + // IEEE Std 802.11-2024, 9.3.2.2.1-9.3.2.2.2 and Figure 9-123: every + // non-final Basic A-MSDU subframe is padded to a 4-octet boundary. The + // final subframe is not padded. An A-MSDU cannot be fragmented by this + // policy, so its complete serialized body must fit the configured limit. + auto serializedAmsduLength = computeSerializedAmsduLength(aMsduLength, packet, header, trailer); + if (maxAMsduSize >= b(0) && serializedAmsduLength > maxAMsduSize) // -1 means infinity return false; // The value of TID present in the QoS Control field of the MPDU carrying the A-MSDU indicates the TID for @@ -91,7 +103,7 @@ std::vector *BasicMsduAggregationPolicy::computeAggregateFrames(queuei if (!isEligible(dataPacket, dataHeader, dataTrailer, firstHeader, aMsduLength)) return false; frames->push_back(dataPacket); - aMsduLength += dataPacket->getDataLength() - dataHeader->getChunkLength() - dataTrailer->getChunkLength() + b(LENGTH_A_MSDU_SUBFRAME_HEADER); + aMsduLength = computeSerializedAmsduLength(aMsduLength, dataPacket, dataHeader, dataTrailer); return true; }; if (!appendIfEligible(candidate)) { diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc index 9af560f9b3e..a2febbfe211 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.cc @@ -36,7 +36,13 @@ std::vector BasicFragmentationPolicy::computeFragmentSizes(Packet *frame) return {}; const auto& trailer = frame->peekAtBack(B(4)); int trailerLength = trailer->getChunkLength().get(); - if (dynamicPtrCast(header)) { + if (const auto& dataHeader = dynamicPtrCast(header)) { + // IEEE Std 802.11-2024, 10.2.7 Note 2, 10.4, and 10.11: A-MSDUs are + // fragmented only through the capability-gated HE dynamic + // fragmentation procedure (26.3.2), which this basic policy does + // not implement. + if (dataHeader->getAMsduPresent()) + return {}; headerLength = header->getChunkLength().get(); payloadLength = frame->getByteLength() - headerLength - trailerLength; } diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 458eae5b8f7..0043bdb1605 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -2171,6 +2171,44 @@ ASSERT(callback.excludedPackets.back() == unsentRequestPacket); fragmentationPolicyModule->callFinish(); fragmentationPolicyModule->deleteModule(); + // IEEE Std 802.11-2024, 10.2.7 Note 2, 10.4, and 10.11: A-MSDUs are fragmented + // only with capability-gated HE dynamic fragmentation (26.3.2). The + // basic policy does not implement that procedure, so an oversized + // A-MSDU must remain a single MPDU candidate. + auto makeQosDataPacket = [&](const char *name, SequenceNumberCyclic sequenceNumber, int payloadLength) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(payloadLength))); + packet->insertAtBack(makeShared()); + return packet; + }; + auto firstMsdu = makeQosDataPacket("oversizedAmsduFirst", SequenceNumberCyclic(110), 20); + auto secondMsdu = makeQosDataPacket("oversizedAmsduSecond", SequenceNumberCyclic(111), 20); + auto amsduMembers = new std::vector({ firstMsdu, secondMsdu }); + MsduAggregation msduAggregation; + auto oversizedAmsdu = msduAggregation.aggregateFrames(amsduMembers); + delete amsduMembers; + const auto& oversizedAmsduHeader = oversizedAmsdu->peekAtFront(); + const auto& oversizedAmsduTrailer = oversizedAmsdu->peekAtBack(B(4)); + ASSERT(oversizedAmsduHeader->getAMsduPresent()); + ASSERT((oversizedAmsdu->getDataLength() - oversizedAmsduHeader->getChunkLength() - oversizedAmsduTrailer->getChunkLength()).get() == 70); + ASSERT(oversizedAmsdu->getByteLength() == 100); + + auto amsduFragmentationPolicyModule = cModuleType::get("inet.linklayer.ieee80211.mac.fragmentation.BasicFragmentationPolicy")->create("oversizedAmsduFragmentationPolicy", this); + amsduFragmentationPolicyModule->par("fragmentationThreshold").setIntValue(90); + amsduFragmentationPolicyModule->callInitialize(); + auto amsduFragmentationPolicy = check_and_cast(amsduFragmentationPolicyModule); + ASSERT(oversizedAmsdu->getByteLength() > 90); + ASSERT(amsduFragmentationPolicy->computeFragmentSizes(oversizedAmsdu).empty()); + auto ordinaryQosData = makeQosDataPacket("ordinaryQosData", SequenceNumberCyclic(112), 70); + ASSERT(amsduFragmentationPolicy->computeFragmentSizes(ordinaryQosData) == std::vector({ 60, 10 })); + delete oversizedAmsdu; + delete ordinaryQosData; + amsduFragmentationPolicyModule->callFinish(); + amsduFragmentationPolicyModule->deleteModule(); + auto recipientDataServiceModule = cModuleType::get("inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService")->create("fragmentedAddbaDataService", this); recipientDataServiceModule->callInitialize(); auto recipientDataService = check_and_cast(recipientDataServiceModule); @@ -3932,6 +3970,75 @@ ASSERT(!dataService.hasEligibleFrame(&mixedQueue)); queue.packets.clear(); } +// IEEE Std 802.11-2024, 9.3.2.2.1-9.3.2.2.2 and Figure 9-123: only +// non-final Basic A-MSDU subframes carry 0-3 octets of alignment padding. +// The policy's size check must include that padding and accept the exact +// serialized limit. +{ + auto makeAggregatePacket = [&](const char *name, SequenceNumberCyclic sequenceNumber, int payloadLength) { + auto header = makeQosHeader(peer1, 1, sequenceNumber); + header->addChunkLength(QOSCONTROL_PART_LENGTH); + header->setTransmitterAddress(peer2); + auto packet = new Packet(name, header); + packet->insertAtBack(makeShared(B(payloadLength))); + packet->insertAtBack(makeShared()); + return packet; + }; + const int firstPayloadLengths[] = { 5, 4, 7 }; + const int expectedAmsduLengths[] = { 39, 39, 43 }; + for (int i = 0; i < 3; i++) { + TestPacketQueue queue; + auto first = makeAggregatePacket("paddingBoundaryFirst", SequenceNumberCyclic(15 + i * 2), firstPayloadLengths[i]); + auto second = makeAggregatePacket("paddingBoundarySecond", SequenceNumberCyclic(16 + i * 2), 5); + queue.packets = { first, second }; + TestBasicMsduAggregationPolicy policy; + policy.configure(B(expectedAmsduLengths[i] - 1)); + ASSERT(policy.computeAggregateFrames(&queue, first, [](const Packet *) { return true; }) == nullptr); + policy.configure(B(expectedAmsduLengths[i])); + auto selected = policy.computeAggregateFrames(&queue, first, [](const Packet *) { return true; }); + ASSERT(selected != nullptr); + ASSERT(selected->size() == 2); + ASSERT(selected->at(0) == first); + ASSERT(selected->at(1) == second); + MsduAggregation aggregation; + auto aggregated = aggregation.aggregateFrames(selected); + delete selected; + aggregated->popAtFront(); + aggregated->popAtBack(B(4)); + ASSERT(aggregated->getDataLength().get() == expectedAmsduLengths[i]); + // The expected length ends with the second 14-byte header and its + // 5-byte MSDU; any final padding would make this assertion fail. + const auto& finalSubframeHeader = aggregated->peekDataAt(B(expectedAmsduLengths[i] - 14 - 5), B(14)); + ASSERT(finalSubframeHeader != nullptr); + ASSERT(finalSubframeHeader->getLength() == 5); + delete aggregated; + queue.packets.clear(); + } + + // BasicMsduAggregationPolicy.ned documents -1 as an unlimited maximum. + TestPacketQueue unlimitedQueue; + auto unlimitedFirst = makeAggregatePacket("unlimitedPaddingFirst", SequenceNumberCyclic(21), 7); + auto unlimitedSecond = makeAggregatePacket("unlimitedPaddingSecond", SequenceNumberCyclic(22), 5); + unlimitedQueue.packets = { unlimitedFirst, unlimitedSecond }; + auto unlimitedPolicyModule = cModuleType::get("inet.linklayer.ieee80211.mac.aggregation.BasicMsduAggregationPolicy")->create("unlimitedAmsduPolicy", this); + unlimitedPolicyModule->par("maxAMsduSize").setIntValue(-1); + unlimitedPolicyModule->callInitialize(); + auto unlimitedPolicy = check_and_cast(unlimitedPolicyModule); + auto unlimitedSelection = unlimitedPolicy->computeAggregateFrames(&unlimitedQueue, unlimitedFirst, [](const Packet *) { return true; }); + ASSERT(unlimitedSelection != nullptr); + ASSERT(unlimitedSelection->size() == 2); + MsduAggregation unlimitedAggregation; + auto unlimitedAggregate = unlimitedAggregation.aggregateFrames(unlimitedSelection); + delete unlimitedSelection; + unlimitedAggregate->popAtFront(); + unlimitedAggregate->popAtBack(B(4)); + ASSERT(unlimitedAggregate->getDataLength().get() == 43); + delete unlimitedAggregate; + unlimitedQueue.packets.clear(); + unlimitedPolicyModule->callFinish(); + unlimitedPolicyModule->deleteModule(); +} + // The built-in policy is anchored on the provider-selected candidate. Held // enumeration members do not hide later compatible frames, while an oversized // anchor falls back to ordinary dequeue without aggregating its followers. From eba9576422e7f6d4d2903d1cd46976fb15bd8e22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:06 +0200 Subject: [PATCH 40/56] tests: record wireless fingerprints for the ADDBA rework Record the existing expected fingerprints for the combined ADDBA, Block Ack, reassembly, aggregation, and HCF changes. These wireless QoS configurations exercise several preceding changes together, so this commit records their combined expectations. The affected configurations cover adhoc and wireless QoS, Block Ack, aggregation, fragmentation, and TXOP. Preserve the previously recorded values; no new baseline values are generated during history cleanup. --- tests/fingerprint/examples.csv | 12 ++++++------ tests/fingerprint/showcases.csv | 21 ++++++++++----------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 640e3c86289..4b0e7ba21aa 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -6,8 +6,8 @@ /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping1 -r 0, 1000s, 8668-d4e7/tplx;e2b1-205b/~tNl;4083-075c/~tND;e1f1-f93c/tyf, PASS, wireless adhoc Ipv4 # /examples/adhoc/ieee80211/, -f omnetpp.ini -c Ping2 -r 0, 100s, 0000-0000/tplx;0000-0000/~tNl;0000-0000/~tND, ERROR, wireless adhoc # [Config Ping2] # interactive config, needed a *.numHosts parameter /examples/adhoc/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, 5749-0281/tplx;f38a-cb93/~tNl;fd9b-683f/~tND;0eb8-3e3b/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 1486-cbae/tplx;521b-977a/~tNl;91c2-9aa9/~tND;2c1e-66e5/tyf, PASS, wireless adhoc Ipv4 -/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, 783f-d09a/tplx;13d1-ec15/~tNl;5aaf-cc7c/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 7852-a806/tplx;6caf-ac5a/~tNl;623f-e442/~tND;2c1e-66e5/tyf, PASS, wireless adhoc Ipv4 +/examples/adhoc/qos/, -f omnetpp.ini -c MacQos -r 1, 10s, b403-1b77/tplx;5f3c-e4ec/~tNl;6acf-af46/~tND;a587-3a4d/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c Fragmentation, 10s, 9c66-e6f0/tplx;cc9e-d1a6/~tNl;53c8-e1b0/~tND;33b7-00f9/tyf, PASS, wireless adhoc Ipv4 /examples/adhoc/qos/, -f omnetpp.ini -c MsduAggregation, 10s, 85bd-85c6/tplx;22f6-085d/~tNl;dc0d-4e19/~tND;df04-18f7/tyf, PASS, wireless adhoc Ipv4 @@ -655,11 +655,11 @@ /examples/wireless/power/, -f omnetpp.ini -c General -r 0, 100s, 6fae-d558/tplx;b8ea-b2fc/~tNl;5ea8-2cea/~tND, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacNonQos -r 0, 10s, f252-8a4b/tplx;481d-4747/~tNl;7061-7f46/~tND;6097-a429/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 31b0-6212/tplx;82ec-9fde/~tNl;70c1-79a0/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQos -r 0, 10s, 9433-0c25/tplx;c766-ffd5/~tNl;2895-311c/~tND;9f3c-4512/tyf, PASS, wireless Ipv4 /examples/wireless/qos/, -f omnetpp.ini -c MacQosWithoutAggregation -r 0, 10s, acd6-0108/tplx;b339-294a/~tNl;58ed-6137/~tND;a4c3-19bf/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 9ece-fbfb/tplx;c1af-29ff/~tNl;01a6-2535/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, d094-b008/tplx;173a-e3fb/~tNl;82c5-b383/~tND;08b5-d005/tyf, PASS, wireless Ipv4 -/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithTransactionalBlockAck -r 0, 3s, c4ff-d71f/tplx;68ba-2827/~tNl;9a98-cc02/~tND, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithRtsCts -r 0, 10s, 488a-06bf/tplx;e9ff-6499/~tNl;3059-d2c3/~tND;b2d6-1329/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithBlockAck -r 0, 10s, 194e-63be/tplx;160c-9b74/~tNl;54bf-44f9/~tND;08b5-d005/tyf, PASS, wireless Ipv4 +/examples/wireless/qos/, -f omnetpp.ini -c MacQosWithTransactionalBlockAck -r 0, 3s, 25a9-9bce/tplx;ef3a-49ad/~tNl;a969-5b95/~tND, PASS, wireless Ipv4 /examples/wireless/ratecontrol/, -f omnetpp.ini -c Mac -r 0, 100s, bf30-2f13/tplx;7b2f-653d/~tNl;6e1e-3b7b/~tND;19fe-8b0e/tyf, PASS, wireless diff --git a/tests/fingerprint/showcases.csv b/tests/fingerprint/showcases.csv index 5162a8b12f2..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, 1ecd-df80/tplx;86dd-208d/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 - +/showcases/wireless/txop/, -f omnetpp.ini -c General -r 0, 5s, 1f97-be23/tplx;22a3-c268/~tNl;c87d-3f3a/tyf, PASS, wireless Ipv4 From ba89f1b12575702012cdddab57930847f3fd32a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 9 Sep 2026 21:54:06 +0200 Subject: [PATCH 41/56] ieee80211: preserve management bodies during fragmentation Generalize embedded body extraction to management headers so fragmentation repeats only the common MAC header. Keep local action context specific to action frames. Exercise a typed association response with a 32-byte fragmentation threshold and verify the reconstructed body bytes and fragment headers. --- .../mac/fragmentation/Fragmentation.cc | 14 ++--- tests/unit/Ieee80211MgmtTransactionTag_1.test | 51 +++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc index 4465c22ecf3..2f5897ebf0c 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc @@ -51,11 +51,12 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve const auto& frameHeader = frame->popAtFront(); frame->popAtBack(B(4)); const auto& actionFrame = dynamicPtrCast(frameHeader); - if (actionFrame != nullptr) { + const auto& managementHeader = dynamicPtrCast(frameHeader); + if (managementHeader != nullptr && managementHeader->getChunkLength() > makeShared()->getChunkLength()) { // IEEE Std 802.11-2024, 10.4: a fragment frame body carries only a - // portion of the MMPDU. Move the action body out of INET's combined + // portion of the MMPDU. Move the management body out of INET's combined // typed header before slicing it into fragment bodies. - Packet serializedHeader("serializedActionHeader", frameHeader); + Packet serializedHeader("serializedManagementHeader", frameHeader); const auto& headerBytes = serializedHeader.peekDataAsBytes()->getBytes(); auto bodyOffset = makeShared()->getChunkLength().get(); frame->insertAtFront(makeShared(std::vector(headerBytes.begin() + bodyOffset, headerBytes.end()))); @@ -75,15 +76,16 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve fragment->getRegionTags().copyTags(frame->getRegionTags(), frame->getFrontOffset() + offset, fragment->getFrontOffset(), length); offset += length; Ptr fragmentHeader; + if (managementHeader != nullptr) + fragmentHeader = copyManagementHeader(managementHeader); + else + fragmentHeader = staticPtrCast(frameHeader->dupShared()); if (actionFrame != nullptr) { - fragmentHeader = copyManagementHeader(actionFrame); auto actionContext = staticPtrCast(actionFrame->dupShared()); actionContext->setFragmentNumber(i); actionContext->setMoreFragments(!lastFragment); fragment->addTag()->setActionFrame(actionContext); } - else - fragmentHeader = staticPtrCast(frameHeader->dupShared()); fragmentHeader->setSequenceNumber(frameHeader->getSequenceNumber()); fragmentHeader->setFragmentNumber(i); fragmentHeader->setMoreFragments(!lastFragment); diff --git a/tests/unit/Ieee80211MgmtTransactionTag_1.test b/tests/unit/Ieee80211MgmtTransactionTag_1.test index 568716711fc..90ff486609b 100644 --- a/tests/unit/Ieee80211MgmtTransactionTag_1.test +++ b/tests/unit/Ieee80211MgmtTransactionTag_1.test @@ -9,6 +9,9 @@ generic body region tag is clipped and rebased for each fragment. #include "inet/common/TimeTag_m.h" #include "inet/common/packet/chunk/BytesChunk.h" #include "inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/BasicFragmentationPolicy.h" +#include "inet/linklayer/ieee80211/mac/fragmentation/Ieee80211FragmentedActionContextTag.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" @@ -53,6 +56,54 @@ for (auto fragment : *fragments) delete fragment; delete fragments; +// Exercise the policy and fragmentation with an actual typed non-action response. +auto policyModule = cModuleType::get("inet.linklayer.ieee80211.mac.fragmentation.BasicFragmentationPolicy")->create("fragmentationPolicy", this); +policyModule->par("fragmentationThreshold").setIntValue(32); +policyModule->callInitialize(); +auto policy = check_and_cast(policyModule); +auto response = makeShared(); +response->setStatusCode(SC_SUCCESSFUL); +response->setAid(291); +Ieee80211SupportedRatesElement rates; +rates.numRates = 1; +rates.rate[0] = 6; +response->setSupportedRates(rates); +response->setChunkLength(B(9)); +auto responsePacket = new Packet("typedAssociationResponse", response); +auto responseHeader = makeShared(); +responseHeader->setType(ST_ASSOCIATIONRESPONSE); +responseHeader->setReceiverAddress(MacAddress("02:00:00:00:00:01")); +responseHeader->setTransmitterAddress(MacAddress("02:00:00:00:00:02")); +responseHeader->setSequenceNumber(SequenceNumberCyclic(17)); +responsePacket->insertAtFront(responseHeader); +responsePacket->insertAtBack(makeShared()); +auto sizes = policy->computeFragmentSizes(responsePacket); +ASSERT(sizes == std::vector({4, 4, 1})); +auto responseFragments = fragmentation.fragmentFrame(responsePacket, sizes); +std::vector reassembledBody; +for (size_t i = 0; i < responseFragments->size(); i++) { + auto fragment = responseFragments->at(i); + ASSERT(fragment->getByteLength() <= 32); + ASSERT(fragment->findTag() == nullptr); + auto fragmentHeader = fragment->popAtFront(); + ASSERT(fragmentHeader->getChunkLength() == B(24)); + ASSERT(fragmentHeader->getType() == ST_ASSOCIATIONRESPONSE); + ASSERT(fragmentHeader->getReceiverAddress() == responseHeader->getReceiverAddress()); + ASSERT(fragmentHeader->getTransmitterAddress() == responseHeader->getTransmitterAddress()); + ASSERT(fragmentHeader->getSequenceNumber() == SequenceNumberCyclic(17)); + ASSERT(fragmentHeader->getFragmentNumber() == i); + ASSERT(fragmentHeader->getMoreFragments() == (i + 1 < responseFragments->size())); + fragment->popAtBack(B(4)); + auto bytes = fragment->peekDataAsBytes()->getBytes(); + ASSERT(bytes.size() == sizes[i]); + reassembledBody.insert(reassembledBody.end(), bytes.begin(), bytes.end()); + delete fragment; +} +ASSERT(reassembledBody == std::vector({0, 0, 0, 0, 0x23, 0xC1, 1, 1, 0x0C})); +delete responseFragments; +policyModule->callFinish(); +policyModule->deleteModule(); + EV << "Management transaction packet tag survived frame replacement.\n"; %contains: stdout From 80518e7bd60c5d36327af8977c54dbd0f0fd1ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 15 Aug 2026 20:33:01 +0200 Subject: [PATCH 42/56] feat(ieee80211): add HT compressed Block Ack Implement one-TID compressed BlockAckReq and 64-bit compressed BlockAck exchanges across the QoS MAC path. Correct BAR and BA Control and Starting Sequence Control serialization, including the Basic BAR wire length, little-endian fields, fragment-number packing, and compressed frame lengths. Extend originator and recipient agreement handling, HCF dispatch, frame sequences, rate selection, protection timing, reordering, and acknowledgment processing. Preserve immediate versus delayed ADDBA policy and return the required all-zero compressed Block Ack when no matching recipient state exists. Keep selection default-off behind the documented assumePeerSupportsCompressedBlockAck model assumption. Reject fragmented and delayed-policy exchanges, maintain the recipient acknowledgment-window boundary, and preserve leading holes including sequence-number wraparound. Add byte-exact unit coverage for Basic and compressed variants, bitmap and wraparound behavior, capability and agreement gates, plus a deterministic HT runtime exchange test. --- .../ieee80211/mac/Ieee80211Frame.msg | 7 +- .../mac/Ieee80211MacHeaderSerializer.cc | 115 ++++--- .../ieee80211/mac/blockack/BlockAckRecord.cc | 15 +- .../ieee80211/mac/blockack/BlockAckRecord.h | 5 +- .../OriginatorBlockAckAgreementHandler.cc | 9 + .../blockack/OriginatorBlockAckProcedure.cc | 13 +- .../blockack/RecipientBlockAckAgreement.cc | 2 +- .../mac/blockack/RecipientBlockAckAgreement.h | 7 + .../RecipientBlockAckAgreementHandler.cc | 15 +- .../RecipientBlockAckAgreementHandler.h | 2 +- .../blockack/RecipientBlockAckProcedure.cc | 29 +- .../blockackreordering/BlockAckReordering.cc | 2 +- .../mac/contract/IOriginatorQoSAckPolicy.h | 1 + .../IRecipientBlockAckAgreementHandler.h | 2 +- .../mac/contract/IRecipientQosAckPolicy.h | 3 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 22 +- .../framesequence/PrimitiveFrameSequences.cc | 10 +- .../mac/originator/OriginatorQosAckPolicy.cc | 31 +- .../mac/originator/OriginatorQosAckPolicy.h | 5 +- .../mac/originator/OriginatorQosAckPolicy.ned | 4 +- .../SingleProtectionMechanism.cc | 7 +- .../mac/rateselection/QosRateSelection.cc | 5 +- .../mac/recipient/RecipientQosAckPolicy.cc | 23 +- .../mac/recipient/RecipientQosAckPolicy.h | 7 +- .../mac/recipient/RecipientQosAckPolicy.ned | 4 +- .../recipient/RecipientQosMacDataService.cc | 10 +- .../Ieee80211CompressedBlockAckRuntime.test | 37 ++ tests/unit/Ieee80211CompressedBlockAck_1.test | 319 ++++++++++++++++++ 28 files changed, 608 insertions(+), 103 deletions(-) create mode 100644 tests/module/Ieee80211CompressedBlockAckRuntime.test create mode 100644 tests/unit/Ieee80211CompressedBlockAck_1.test diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg index 184c836eb30..b8301f40cfa 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211Frame.msg @@ -42,6 +42,7 @@ const b LENGTH_ADDBAREQ = LENGTH_MGMT + B(9); // mgmt length + action body lengt const b LENGTH_ADDBARESP = LENGTH_MGMT + B(9); // mgmt length + action body length const b LENGTH_DELBA = LENGTH_MGMT + B(6); // mgmt length + action body length const b LENGTH_BASIC_BLOCKACK = B(16 + 2 + (2 + 128) + 4); // header + ba control + ba information + fcs +const b LENGTH_COMPRESSED_BLOCKACK = B(16 + 2 + (2 + 8) + 4); // header + ba control + ba information + fcs const b DATAFRAME_HEADER_MINLENGTH = B(2 + 2 + 3 * 6 + 2); //bits without QoS, without Address4: 2 + 2 + 3*6(addresses) + 2 const b QOSCONTROL_PART_LENGTH = b(2 * 8); // QoS Control field length (bits) const short int MAX_NUM_FRAGMENTS = 16; @@ -345,7 +346,7 @@ class Ieee80211BlockAckReq extends Ieee80211TwoAddressHeader class Ieee80211BasicBlockAckReq extends Ieee80211BlockAckReq { - chunkLength = B(38); + chunkLength = B(20); int tidInfo; // The TID_INFO subfield of the BAR Control field of the Basic BlockAckReq frame contains the TID for which a Basic BlockAck frame is requested. @@ -362,7 +363,7 @@ class Ieee80211BasicBlockAckReq extends Ieee80211BlockAckReq class Ieee80211CompressedBlockAckReq extends Ieee80211BlockAckReq { - chunkLength = B(38); + chunkLength = B(20); int tidInfo; // The TID_INFO subfield of the BAR Control field of the Compressed BlockAckReq frame contains the TID for which a BlockAck frame is requested. // The BAR Information field of the Compressed BlockAckReq frame contains the Block Ack Starting @@ -444,7 +445,7 @@ class Ieee80211BasicBlockAck extends Ieee80211BlockAck // class Ieee80211CompressedBlockAck extends Ieee80211BlockAck { - // chunkLength TODO + chunkLength = LENGTH_COMPRESSED_BLOCKACK - B(4); multiTid = 0; compressedBitmap = 1; diff --git a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc index b9937549479..361a8c6ceec 100644 --- a/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc +++ b/src/inet/linklayer/ieee80211/mac/Ieee80211MacHeaderSerializer.cc @@ -76,6 +76,24 @@ void readSequenceControl(MemoryInputStream& stream, int& fragmentNumber, ieee802 sequenceNumber = ieee80211::SequenceNumberCyclic((sequenceControl >> 4) & 0xFFF); } +uint16_t packBlockAckControl(bool ackPolicy, bool multiTid, bool compressedBitmap, uint16_t reserved, uint8_t tidInfo) +{ + return (ackPolicy ? 0x0001 : 0) | + (multiTid ? 0x0002 : 0) | + (compressedBitmap ? 0x0004 : 0) | + ((reserved & 0x1FF) << 3) | + ((tidInfo & 0xF) << 12); +} + +void unpackBlockAckControl(uint16_t control, bool& ackPolicy, bool& multiTid, bool& compressedBitmap, uint16_t& reserved, uint8_t& tidInfo) +{ + ackPolicy = (control & 0x0001) != 0; + multiTid = (control & 0x0002) != 0; + compressedBitmap = (control & 0x0004) != 0; + reserved = (control >> 3) & 0x1FF; + tidInfo = (control >> 12) & 0xF; +} + } // namespace namespace ieee80211 { @@ -270,26 +288,20 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c stream.writeUint16Le(blockAckReq->getDurationField().inUnit(SIMTIME_US)); stream.writeMacAddress(blockAckReq->getReceiverAddress()); stream.writeMacAddress(blockAckReq->getTransmitterAddress()); - stream.writeBit(blockAckReq->getBarAckPolicy()); bool multiTid = blockAckReq->getMultiTid(); bool compressedBitmap = blockAckReq->getCompressedBitmap(); - stream.writeBit(multiTid); - stream.writeBit(compressedBitmap); - stream.writeNBitsOfUint64Be(blockAckReq->getReserved(), 9); if (!multiTid && !compressedBitmap) { auto basicBlockAckReq = dynamicPtrCast(chunk); - stream.writeUint4(basicBlockAckReq->getTidInfo()); - stream.writeUint32Be(basicBlockAckReq->getFragmentNumber()); - stream.writeUint64Be(0); - stream.writeUint64Be(basicBlockAckReq->getStartingSequenceNumber().get()); + stream.writeUint16Le(packBlockAckControl(blockAckReq->getBarAckPolicy(), multiTid, compressedBitmap, blockAckReq->getReserved(), basicBlockAckReq->getTidInfo())); + writeSequenceControl(stream, basicBlockAckReq->getFragmentNumber(), basicBlockAckReq->getStartingSequenceNumber().get()); ASSERT(stream.getLength() - startPos == basicBlockAckReq->getChunkLength()); } else if (!multiTid && compressedBitmap) { auto compressedBlockAckReq = dynamicPtrCast(chunk); - stream.writeUint4(compressedBlockAckReq->getTidInfo()); - stream.writeUint32Be(compressedBlockAckReq->getFragmentNumber()); - stream.writeUint64Be(0); - stream.writeUint64Be(compressedBlockAckReq->getStartingSequenceNumber().get()); + // IEEE Std 802.11-2024, 9.3.1.7.2: one-TID compressed BAR Control and + // Starting Sequence Control are little-endian 16-bit on-wire fields. + stream.writeUint16Le(packBlockAckControl(blockAckReq->getBarAckPolicy(), multiTid, compressedBitmap, blockAckReq->getReserved(), compressedBlockAckReq->getTidInfo())); + writeSequenceControl(stream, compressedBlockAckReq->getFragmentNumber(), compressedBlockAckReq->getStartingSequenceNumber().get()); ASSERT(stream.getLength() - startPos == compressedBlockAckReq->getChunkLength()); } else if (multiTid && compressedBitmap) { @@ -304,16 +316,12 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c stream.writeUint16Le(blockAck->getDurationField().inUnit(SIMTIME_US)); stream.writeMacAddress(blockAck->getReceiverAddress()); stream.writeMacAddress(blockAck->getTransmitterAddress()); - stream.writeBit(blockAck->getBlockAckPolicy()); bool multiTid = blockAck->getMultiTid(); bool compressedBitmap = blockAck->getCompressedBitmap(); - stream.writeBit(multiTid); - stream.writeBit(compressedBitmap); - stream.writeNBitsOfUint64Be(blockAck->getReserved(), 9); if (!multiTid && !compressedBitmap) { auto basicBlockAck = dynamicPtrCast(chunk); - stream.writeUint4(basicBlockAck->getTidInfo()); - stream.writeUint16Be(basicBlockAck->getStartingSequenceNumber().get()); + stream.writeUint16Le(packBlockAckControl(blockAck->getBlockAckPolicy(), multiTid, compressedBitmap, blockAck->getReserved(), basicBlockAck->getTidInfo())); + writeSequenceControl(stream, basicBlockAck->getFragmentNumber(), basicBlockAck->getStartingSequenceNumber().get()); for (size_t i = 0; i < 64; ++i) { stream.writeByte(basicBlockAck->getBlockAckBitmap(i).getBytes()[0]); stream.writeByte(basicBlockAck->getBlockAckBitmap(i).getBytes()[1]); @@ -322,8 +330,10 @@ void Ieee80211MacHeaderSerializer::serializeFields(MemoryOutputStream& stream, c } else if (!multiTid && compressedBitmap) { auto compressedBlockAck = dynamicPtrCast(chunk); - stream.writeUint4(compressedBlockAck->getTidInfo()); - stream.writeUint16Be(compressedBlockAck->getStartingSequenceNumber().get()); + // IEEE Std 802.11-2024, 9.3.1.8.2: the non-HE compressed BA carries + // one Starting Sequence Control field followed by a 64-bit bitmap. + stream.writeUint16Le(packBlockAckControl(blockAck->getBlockAckPolicy(), multiTid, compressedBitmap, blockAck->getReserved(), compressedBlockAck->getTidInfo())); + writeSequenceControl(stream, compressedBlockAck->getFragmentNumber(), compressedBlockAck->getStartingSequenceNumber().get()); for (size_t i = 0; i < 8; ++i) { stream.writeByte(compressedBlockAck->getBlockAckBitmap().getBytes()[i]); } @@ -523,30 +533,38 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAckReq->setDurationField(SimTime(stream.readUint16Le(), SIMTIME_US)); blockAckReq->setReceiverAddress(stream.readMacAddress()); blockAckReq->setTransmitterAddress(stream.readMacAddress()); - blockAckReq->setBarAckPolicy(stream.readBit()); - bool multiTid = stream.readBit(); - bool compressedBitmap = stream.readBit(); + bool barAckPolicy; + bool multiTid; + bool compressedBitmap; + uint16_t reserved; + uint8_t tidInfo; + unpackBlockAckControl(stream.readUint16Le(), barAckPolicy, multiTid, compressedBitmap, reserved, tidInfo); + blockAckReq->setBarAckPolicy(barAckPolicy); blockAckReq->setMultiTid(multiTid); blockAckReq->setCompressedBitmap(compressedBitmap); - blockAckReq->setReserved(stream.readNBitsToUint64Be(9)); + blockAckReq->setReserved(reserved); if (!multiTid && !compressedBitmap) { auto basicBlockAckReq = makeShared(); copyBasicFields(basicBlockAckReq, macHeader); copyBlockAckReqFrameFields(basicBlockAckReq, blockAckReq); - basicBlockAckReq->setTidInfo(stream.readUint4()); - basicBlockAckReq->setFragmentNumber(stream.readUint32Be()); - stream.readUint64Be(); - basicBlockAckReq->setStartingSequenceNumber(SequenceNumberCyclic(stream.readUint64Be())); + basicBlockAckReq->setTidInfo(tidInfo); + int fragmentNumber; + SequenceNumberCyclic sequenceNumber; + readSequenceControl(stream, fragmentNumber, sequenceNumber); + basicBlockAckReq->setFragmentNumber(fragmentNumber); + basicBlockAckReq->setStartingSequenceNumber(sequenceNumber); return basicBlockAckReq; } else if (!multiTid && compressedBitmap) { auto compressedBlockAckReq = makeShared(); copyBasicFields(compressedBlockAckReq, macHeader); copyBlockAckReqFrameFields(compressedBlockAckReq, blockAckReq); - compressedBlockAckReq->setTidInfo(stream.readUint4()); - compressedBlockAckReq->setFragmentNumber(stream.readUint32Be()); - stream.readUint64Be(); - compressedBlockAckReq->setStartingSequenceNumber(SequenceNumberCyclic(stream.readUint64Be())); + compressedBlockAckReq->setTidInfo(tidInfo); + int fragmentNumber; + SequenceNumberCyclic sequenceNumber; + readSequenceControl(stream, fragmentNumber, sequenceNumber); + compressedBlockAckReq->setFragmentNumber(fragmentNumber); + compressedBlockAckReq->setStartingSequenceNumber(sequenceNumber); return compressedBlockAckReq; } else @@ -559,24 +577,31 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre blockAck->setDurationField(SimTime(stream.readUint16Le(), SIMTIME_US)); blockAck->setReceiverAddress(stream.readMacAddress()); blockAck->setTransmitterAddress(stream.readMacAddress()); - blockAck->setBlockAckPolicy(stream.readBit()); - bool multiTid = stream.readBit(); - bool compressedBitmap = stream.readBit(); + bool blockAckPolicy; + bool multiTid; + bool compressedBitmap; + uint16_t reserved; + uint8_t tidInfo; + unpackBlockAckControl(stream.readUint16Le(), blockAckPolicy, multiTid, compressedBitmap, reserved, tidInfo); + blockAck->setBlockAckPolicy(blockAckPolicy); blockAck->setMultiTid(multiTid); blockAck->setCompressedBitmap(compressedBitmap); - blockAck->setReserved(stream.readNBitsToUint64Be(9)); + blockAck->setReserved(reserved); if (!multiTid && !compressedBitmap) { auto basicBlockAck = makeShared(); copyBasicFields(basicBlockAck, macHeader); copyBlockAckFrameFields(basicBlockAck, blockAck); - basicBlockAck->setTidInfo(stream.readUint4()); - basicBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(stream.readUint16Be())); + basicBlockAck->setTidInfo(tidInfo); + int fragmentNumber; + SequenceNumberCyclic sequenceNumber; + readSequenceControl(stream, fragmentNumber, sequenceNumber); + basicBlockAck->setFragmentNumber(fragmentNumber); + basicBlockAck->setStartingSequenceNumber(sequenceNumber); for (size_t i = 0; i < 64; ++i) { std::vector bytes; bytes.push_back(stream.readByte()); bytes.push_back(stream.readByte()); - BitVector *blockAckBitmap = new BitVector(bytes); - basicBlockAck->setBlockAckBitmap(i, *blockAckBitmap); + basicBlockAck->setBlockAckBitmap(i, BitVector(bytes)); } return basicBlockAck; } @@ -585,13 +610,17 @@ const Ptr Ieee80211MacHeaderSerializer::deserializeFields(MemoryInputStre copyBasicFields(compressedBlockAck, macHeader); copyBlockAckFrameFields(compressedBlockAck, blockAck); - compressedBlockAck->setTidInfo(stream.readUint4()); - compressedBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(stream.readUint16Be())); + compressedBlockAck->setTidInfo(tidInfo); + int fragmentNumber; + SequenceNumberCyclic sequenceNumber; + readSequenceControl(stream, fragmentNumber, sequenceNumber); + compressedBlockAck->setFragmentNumber(fragmentNumber); + compressedBlockAck->setStartingSequenceNumber(sequenceNumber); std::vector bytes; for (size_t i = 0; i < 8; ++i) { bytes.push_back(stream.readByte()); } - compressedBlockAck->setBlockAckBitmap(*(new BitVector(bytes))); + compressedBlockAck->setBlockAckBitmap(BitVector(bytes)); return compressedBlockAck; } else { diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index 919bd813a6f..7dce744fdec 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -12,9 +12,10 @@ namespace inet { namespace ieee80211 { -BlockAckRecord::BlockAckRecord(MacAddress originatorAddress, Tid tid) : +BlockAckRecord::BlockAckRecord(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber) : originatorAddress(originatorAddress), - tid(tid) + tid(tid), + startingSequenceNumber(startingSequenceNumber) { } @@ -42,6 +43,13 @@ bool BlockAckRecord::getAckState(SequenceNumberCyclic sequenceNumber, FragmentNu } } +bool BlockAckRecord::getCompressedAckState(SequenceNumberCyclic sequenceNumber) +{ + // IEEE Std 802.11-2024, 10.25.6.1: bits preceding the maintained + // receive-window range are one; missing MPDUs within the range are zero. + return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), 0)) || sequenceNumber < startingSequenceNumber; +} + void BlockAckRecord::removeAckStates(SequenceNumberCyclic sequenceNumber) { auto it = acknowledgmentState.begin(); @@ -51,8 +59,9 @@ void BlockAckRecord::removeAckStates(SequenceNumberCyclic sequenceNumber) else it++; } + if (startingSequenceNumber <= sequenceNumber) + startingSequenceNumber = sequenceNumber + 1; } } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h index 971be8a8af9..3eb8f9bec46 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h @@ -24,14 +24,16 @@ class INET_API BlockAckRecord protected: MacAddress originatorAddress = MacAddress::UNSPECIFIED_ADDRESS; Tid tid = -1; + SequenceNumberCyclic startingSequenceNumber; std::map acknowledgmentState; public: - BlockAckRecord(MacAddress originatorAddress, Tid tid); + BlockAckRecord(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber); virtual ~BlockAckRecord() {} void blockAckPolicyFrameReceived(const Ptr& header); bool getAckState(SequenceNumberCyclic sequenceNumber, FragmentNumber fragmentNumber); + bool getCompressedAckState(SequenceNumberCyclic sequenceNumber); void removeAckStates(SequenceNumberCyclic sequenceNumber); MacAddress getOriginatorAddress() { return originatorAddress; } @@ -42,4 +44,3 @@ class INET_API BlockAckRecord } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index f69625097c3..bf5ec46863e 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -156,6 +156,14 @@ void OriginatorBlockAckAgreementHandler::processReceivedBlockAck(const Ptr(blockAck)) { + auto agreement = getAgreement(compressedBlockAck->getTransmitterAddress(), compressedBlockAck->getTidInfo()); + if (agreement) { + agreement->setStartingSequenceNumber(compressedBlockAck->getStartingSequenceNumber()); + agreement->calculateExpirationTime(); + scheduleInactivityTimer(callback); + } + } else throw cRuntimeError("Unsupported BlockAck"); } @@ -304,6 +312,7 @@ OriginatorBlockAckAgreementResponse OriginatorBlockAckAgreementHandler::processR void OriginatorBlockAckAgreementHandler::updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp) { agreement->setIsAddbaResponseReceived(true); + agreement->setIsDelayedBlockAckPolicySupported(addbaResp->getBlockAckPolicy() == 0); agreement->setBufferSize(addbaResp->getBufferSize()); agreement->setBlockAckTimeoutValue(addbaResp->getBlockAckTimeoutValue()); agreement->calculateExpirationTime(); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc index d02e412b644..6a468e94e02 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.cc @@ -12,13 +12,11 @@ namespace ieee80211 { const Ptr OriginatorBlockAckProcedure::buildCompressedBlockAckReqFrame(const MacAddress& receiverAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber) const { - throw cRuntimeError("Unsupported feature"); - // TODO implement - // auto blockAckReq = makeShared(); - // blockAckReq->setReceiverAddress(receiverAddress); - // blockAckReq->setStartingSequenceNumber(startingSequenceNumber); - // blockAckReq->setTidInfo(tid); - // return blockAckReq; + auto blockAckReq = makeShared(); + blockAckReq->setReceiverAddress(receiverAddress); + blockAckReq->setStartingSequenceNumber(startingSequenceNumber); + blockAckReq->setTidInfo(tid); + return blockAckReq; } const Ptr OriginatorBlockAckProcedure::buildBasicBlockAckReqFrame(const MacAddress& receiverAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber) const @@ -32,4 +30,3 @@ const Ptr OriginatorBlockAckProcedure::buildBasicBlockAckR } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc index becc47e3d70..893b43f3217 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc @@ -19,7 +19,7 @@ RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddr generationId(generationId) { calculateExpirationTime(); - blockAckRecord = new BlockAckRecord(originatorAddress, tid); + blockAckRecord = new BlockAckRecord(originatorAddress, tid, startingSequenceNumber); } void RecipientBlockAckAgreement::blockAckPolicyFrameReceived(const Ptr& header) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 85ef1f9e613..d2da01ea240 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -21,6 +21,8 @@ class INET_API RecipientBlockAckAgreement : public cObject SequenceNumberCyclic startingSequenceNumber; int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; + bool isAddbaResponseSent = false; + bool isDelayedBlockAckPolicySupported = 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. @@ -43,6 +45,11 @@ class INET_API RecipientBlockAckAgreement : public cObject if (!inactivityExpired) expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } + virtual bool getIsAddbaResponseSent() const { return isAddbaResponseSent; } + virtual bool getIsDelayedBlockAckPolicySupported() const { return isDelayedBlockAckPolicySupported; } + + virtual void addbaResposneSent() { isAddbaResponseSent = true; } + virtual void setIsDelayedBlockAckPolicySupported(bool isDelayedBlockAckPolicySupported) { this->isDelayedBlockAckPolicySupported = isDelayedBlockAckPolicySupported; } virtual simtime_t getExpirationTime() { return expirationTime; } virtual bool isInactivityExpired() const { return inactivityExpired; } virtual void markInactivityExpired() { inactivityExpired = true; expirationTime = SIMTIME_MAX; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index 4dcad4f4bea..cadcea969f2 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -49,11 +49,18 @@ void RecipientBlockAckAgreementHandler::qosFrameReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) +void RecipientBlockAckAgreementHandler::blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) { - auto agreement = getActiveAgreement(blockAckReq->getTidInfo(), blockAckReq->getTransmitterAddress()); + Tid tid = -1; + if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) + tid = basicBlockAckReq->getTidInfo(); + else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) + tid = compressedBlockAckReq->getTidInfo(); + else + throw cRuntimeError("Unsupported BlockAckReq"); + auto agreement = getActiveAgreement(tid, blockAckReq->getTransmitterAddress()); if (agreement != nullptr) { agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); @@ -189,6 +196,8 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAd } else blockAckAgreements[id] = agreement; + agreement->addbaResposneSent(); + agreement->setIsDelayedBlockAckPolicySupported(addbaResponse->getBlockAckPolicy() == 0); scheduleInactivityTimer(agreementHandlerCallback); } procedureCallback->processMgmtFrame(addbaResponsePacket, addbaResponse); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h index 7c4ce21b5b9..7f14ce9fa2d 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h @@ -49,7 +49,7 @@ class INET_API RecipientBlockAckAgreementHandler : public IRecipientBlockAckAgre 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 void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) override; virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) override; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) override; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc index 6796539c4f5..c56ddc06dec 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.cc @@ -24,13 +24,24 @@ void RecipientBlockAckProcedure::processReceivedBlockAckReq(Packet *blockAckPack 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); + auto duration = ackPolicy->computeBlockAckDurationField(blockAckPacketReq, basicBlockAckReq); blockAck->setDurationField(duration); auto blockAckPacket = new Packet("BasicBlockAck", blockAck); EV_DEBUG << "Duration for " << blockAckPacket->getName() << " is set to " << duration << " s.\n"; callback->transmitControlResponseFrame(blockAckPacket, blockAck, blockAckPacketReq, basicBlockAckReq); } } + else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) { + auto agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(compressedBlockAckReq->getTidInfo(), compressedBlockAckReq->getTransmitterAddress()); + if (ackPolicy->isBlockAckNeeded(compressedBlockAckReq, agreement)) { + auto blockAck = buildBlockAck(compressedBlockAckReq, agreement); + auto duration = ackPolicy->computeBlockAckDurationField(blockAckPacketReq, compressedBlockAckReq); + blockAck->setDurationField(duration); + auto blockAckPacket = new Packet("CompressedBlockAck", blockAck); + EV_DEBUG << "Duration for " << blockAckPacket->getName() << " is set to " << duration << " s.\n"; + callback->transmitControlResponseFrame(blockAckPacket, blockAck, blockAckPacketReq, compressedBlockAckReq); + } + } else throw cRuntimeError("Unsupported BlockAckReq"); } @@ -65,6 +76,22 @@ const Ptr RecipientBlockAckProcedure::buildBlockAck(const Ptr blockAck->setTidInfo(basicBlockAckReq->getTidInfo()); return blockAck; } + else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) { + auto blockAck = makeShared(); + auto startingSequenceNumber = compressedBlockAckReq->getStartingSequenceNumber(); + BitVector bitmap(std::vector(8, 0)); + if (agreement != nullptr) { + // IEEE Std 802.11-2024, 10.25.6.1 and 10.25.6.5: a non-HE + // compressed BA reports 64 consecutive, unfragmented MPDUs. + for (int i = 0; i < 64; i++) + bitmap.setBit(i, agreement->getBlockAckRecord()->getCompressedAckState(startingSequenceNumber + i)); + } + blockAck->setReceiverAddress(blockAckReq->getTransmitterAddress()); + blockAck->setStartingSequenceNumber(startingSequenceNumber); + blockAck->setTidInfo(compressedBlockAckReq->getTidInfo()); + blockAck->setBlockAckBitmap(bitmap); + return blockAck; + } else throw cRuntimeError("Unsupported Block Ack Request"); } diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 3f4801ab76f..6daf0e217bc 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -97,7 +97,7 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedBlockAckReq tid = basicReq->getTidInfo(); startingSequenceNumber = basicReq->getStartingSequenceNumber(); } - else if (auto compressedReq = dynamicPtrCast(blockAckReq)) { + else if (auto compressedReq = dynamicPtrCast(blockAckReq)) { tid = compressedReq->getTidInfo(); startingSequenceNumber = compressedReq->getStartingSequenceNumber(); } diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h index 18b934badc5..1ccdffb8a8f 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h @@ -28,6 +28,7 @@ class INET_API IOriginatorQoSAckPolicy 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, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const = 0; + virtual bool isCompressedBlockAckReq(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) const = 0; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const = 0; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h index 8811e70f411..6e9b8b29dc8 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementHandler.h @@ -44,7 +44,7 @@ class INET_API IRecipientBlockAckAgreementHandler virtual bool processAcknowledgedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return false; } virtual RecipientBlockAckAgreementAbortResult processAbortedDelba(Packet *, IBlockAckAgreementHandlerCallback *) { return {}; } virtual void qosFrameReceived(const Ptr& qosHeader, IBlockAckAgreementHandlerCallback *callback) = 0; - virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) = 0; + virtual void blockAckReqReceived(const Ptr& blockAckReq, IBlockAckAgreementHandlerCallback *callback) = 0; virtual bool blockAckAgreementExpired(IProcedureCallback *procedureCallback, IBlockAckAgreementHandlerCallback *agreementHandlerCallback) = 0; virtual RecipientBlockAckAgreement *getAgreement(Tid tid, MacAddress originatorAddr) = 0; diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h index ccf47cc5610..b0525470502 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientQosAckPolicy.h @@ -25,11 +25,10 @@ class INET_API IRecipientQosAckPolicy virtual bool isBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) const = 0; virtual simtime_t computeAckDurationField(Packet *packet, const Ptr& header) const = 0; - virtual simtime_t computeBasicBlockAckDurationField(Packet *packet, const Ptr& basicBlockAckReq) const = 0; + virtual simtime_t computeBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const = 0; }; } // namespace ieee80211 } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index e2f25d68921..d3f061639f4 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -798,7 +798,7 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr(header)) ctsProcedure->processReceivedRts(packet, rtsFrame, ctsPolicy, this); - else if (auto blockAckRequest = dynamicPtrCast(header)) { + else if (auto blockAckRequest = dynamicPtrCast(header)) { if (recipientBlockAckAgreementHandler) recipientBlockAckAgreementHandler->blockAckReqReceived(blockAckRequest, this); if (recipientBlockAckProcedure) @@ -1240,10 +1240,18 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(header)) { - EV_INFO << "BasicBlockAck has arrived" << std::endl; - if (originatorBlockAckAgreementHandler == nullptr || originatorBlockAckAgreementHandler->getActiveAgreement(blockAck->getTransmitterAddress(), blockAck->getTidInfo()) == nullptr) { - EV_INFO << "Ignoring BasicBlockAck without an active Block Ack agreement.\n"; + else if (auto blockAck = dynamicPtrCast(header)) { + EV_INFO << blockAck->getClassName() << " has arrived" << std::endl; + Tid tid = -1; + MacAddress transmitterAddress = blockAck->getTransmitterAddress(); + if (auto basicBlockAck = dynamicPtrCast(blockAck)) + tid = basicBlockAck->getTidInfo(); + else if (auto compressedBlockAck = dynamicPtrCast(blockAck)) + tid = compressedBlockAck->getTidInfo(); + else + throw cRuntimeError("Unknown BlockAck frame"); + if (originatorBlockAckAgreementHandler == nullptr || originatorBlockAckAgreementHandler->getActiveAgreement(transmitterAddress, tid) == nullptr) { + EV_INFO << "Ignoring BlockAck without an active Block Ack agreement.\n"; return; } edcaf->getRecoveryProcedure()->blockAckFrameReceived(); @@ -1261,7 +1269,7 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const PtrgetRecoveryProcedure()->ctsFrameReceived(); else if (header->getType() == ST_DATA_WITH_QOS) ; // void - else if (dynamicPtrCast(header)) + else if (dynamicPtrCast(header)) ; // void else throw cRuntimeError("Unknown control frame"); @@ -1357,7 +1365,7 @@ void Hcf::transmitControlResponseFrame(Packet *responsePacket, const Ptr(receivedHeader)) responseMode = rateSelection->computeResponseCtsFrameMode(receivedPacket, rtsFrame); - else if (auto blockAckReq = dynamicPtrCast(receivedHeader)) + else if (auto blockAckReq = dynamicPtrCast(receivedHeader)) responseMode = rateSelection->computeResponseBlockAckFrameMode(receivedPacket, blockAckReq); else if (auto dataOrMgmtHeader = dynamicPtrCast(receivedHeader)) responseMode = rateSelection->computeResponseAckFrameMode(receivedPacket, dataOrMgmtHeader); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index ac5ecb84f5e..00264aa2e7f 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -396,8 +396,14 @@ IFrameSequenceStep *BlockAckReqBlockAckFs::prepareStep(FrameSequenceContext *con auto receiverAddr = std::get<0>(blockAckReqParams); auto startingSequenceNumber = std::get<1>(blockAckReqParams); auto tid = std::get<2>(blockAckReqParams); - auto blockAckReq = context->getQoSContext()->blockAckProcedure->buildBasicBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber); - auto blockAckPacket = new Packet("BasicBlockAckReq", blockAckReq); + auto agreementHandler = context->getQoSContext()->blockAckAgreementHandler; + auto agreement = agreementHandler == nullptr ? nullptr : agreementHandler->getAgreement(receiverAddr, tid); + auto outstandingFrames = context->getInProgressFrames()->getOutstandingFrames(); + bool useCompressedBlockAck = context->getQoSContext()->ackPolicy->isCompressedBlockAckReq(outstandingFrames, agreement); + auto blockAckReq = useCompressedBlockAck ? + context->getQoSContext()->blockAckProcedure->buildCompressedBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber) : + context->getQoSContext()->blockAckProcedure->buildBasicBlockAckReqFrame(receiverAddr, tid, startingSequenceNumber); + auto blockAckPacket = new Packet(useCompressedBlockAck ? "CompressedBlockAckReq" : "BasicBlockAckReq", blockAckReq); blockAckPacket->insertAtBack(makeShared()); return new TransmitStep(blockAckPacket, context->getIfs(), true); } diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index 229e57502ba..656cb0d187d 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -23,6 +23,7 @@ void OriginatorQosAckPolicy::initialize(int stage) rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); maxBlockAckPolicyFrameLength = par("maxBlockAckPolicyFrameLength"); blockAckReqThreshold = par("blockAckReqThreshold"); + assumePeerSupportsCompressedBlockAck = par("assumePeerSupportsCompressedBlockAck"); blockAckTimeout = par("blockAckTimeout"); ackTimeout = par("ackTimeout"); } @@ -61,15 +62,29 @@ SequenceNumberCyclic OriginatorQosAckPolicy::computeStartingSequenceNumber(const return startingSequenceNumber; } -bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector& outstandingFrames, int startingSequenceNumber) const +bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) const { - // The Compressed Bitmap subfield of the BA Control field or BAR Control field shall be set to 1 in all - // BlockAck and BlockAckReq frames sent from one HT STA to another HT STA and shall be set to 0 otherwise. - return false; // non-HT STA -// for (auto frame : outstandingFrames) -// if (frame->getSequenceNumber() >= startingSequenceNumber && frame->getFragmentNumber() > 0) -// return false; -// return true; + return isCompressedBlockAckReqNeeded(outstandingFrames, agreement, assumePeerSupportsCompressedBlockAck); +} + +bool OriginatorQosAckPolicy::isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck) +{ + // IEEE Std 802.11-2024, Table 11-8 and 10.25.6.1: use the compressed + // variant only for an established immediate HT Block Ack agreement. + // Peer HT capability is not represented by the baseline agreement contract; + // the parameter is an explicit assumption supplied by the configuration. + if (!assumePeerSupportsCompressedBlockAck || agreement == nullptr || !agreement->getIsAddbaResponseReceived() || agreement->getIsDelayedBlockAckPolicySupported()) + return false; + bool hasMatchingOutstandingFrame = false; + for (auto frame : outstandingFrames) { + auto header = dynamicPtrCast(frame->peekAtFront()); + if (header == nullptr || header->getReceiverAddress() != agreement->getReceiverAddr() || header->getTid() != agreement->getTid()) + continue; + hasMatchingOutstandingFrame = true; + if (header->getFragmentNumber() != 0 || header->getMoreFragments()) + return false; + } + return hasMatchingOutstandingFrame; } // FIXME diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h index a421df20e82..7494db69eb1 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h @@ -22,6 +22,7 @@ class INET_API OriginatorQosAckPolicy : public ModeSetListener, public IOriginat IQosRateSelection *rateSelection = nullptr; int maxBlockAckPolicyFrameLength = -1; int blockAckReqThreshold = -1; + bool assumePeerSupportsCompressedBlockAck = false; simtime_t blockAckTimeout = -1; simtime_t ackTimeout = -1; @@ -33,14 +34,14 @@ class INET_API OriginatorQosAckPolicy : public ModeSetListener, public IOriginat virtual bool checkAgreementPolicy(const Ptr& header, OriginatorBlockAckAgreement *agreement) 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; - + static bool isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck); public: 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, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const override; virtual std::tuple computeBlockAckReqParameters(InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const override; + virtual bool isCompressedBlockAckReq(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) const override; virtual simtime_t getAckTimeout(Packet *packet, const Ptr& dataOrMgmtHeader) const override; virtual simtime_t getBlockAckTimeout(Packet *packet, const Ptr& blockAckReq) const override; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned index eb2f9236ab1..6575d5290b2 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned @@ -21,9 +21,11 @@ simple OriginatorQosAckPolicy extends SimpleModule like IOriginatorQosAckPolicy int blockAckReqThreshold = default(5); int maxBlockAckPolicyFrameLength @unit(B) = default(1000B); + // Set only for an HT-or-later local STA when peer capability management establishes that the peer supports Compressed Block Ack. + // This explicit assumption is needed because peer HT capabilities are not represented by the baseline agreement contract. + bool assumePeerSupportsCompressedBlockAck = default(false); double blockAckTimeout @unit(s) = default(-1s); double ackTimeout @unit(s) = default(-1s); @display("i=block/control"); } - diff --git a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc index 4b38b531005..3739079f674 100644 --- a/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc +++ b/src/inet/linklayer/ieee80211/mac/protectionmechanism/SingleProtectionMechanism.cc @@ -74,8 +74,12 @@ simtime_t SingleProtectionMechanism::computeBlockAckReqDurationField(Packet *pac simtime_t blockAckReqDurationPerId = blockAckFrameDuration + modeSet->getSifsTime(); return blockAckReqDurationPerId; } + else if (dynamicPtrCast(blockAckReq)) { + simtime_t blockAckFrameDuration = rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq)->getDuration(LENGTH_COMPRESSED_BLOCKACK); + return blockAckFrameDuration + modeSet->getSifsTime(); + } else - throw cRuntimeError("Compressed and Multi-Tid Block Ack Requests are not supported"); + throw cRuntimeError("Multi-Tid Block Ack Requests are not supported"); } // @@ -183,4 +187,3 @@ simtime_t SingleProtectionMechanism::computeDurationField(Packet *packet, const } /* namespace ieee80211 */ } /* namespace inet */ - diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 788a6bab198..1d7449cc0e9 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -168,7 +168,7 @@ const IIeee80211Mode *QosRateSelection::computeResponseCtsFrameMode(Packet *pack // const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet *packet, const Ptr& blockAckReq) { - if (!dynamicPtrCast(blockAckReq)) + if (!dynamicPtrCast(blockAckReq) && !dynamicPtrCast(blockAckReq)) throw cRuntimeError("Unknown BlockAckReq frame type"); const IIeee80211Mode *responseMode = nullptr; if (responseBlockAckFrameMode) @@ -180,7 +180,8 @@ const IIeee80211Mode *QosRateSelection::computeResponseBlockAckFrameMode(Packet // this model has no BSSBasicRateSet/HT Control context to select another PPDU. responseMode = modeSet->getNonHtControlResponseMode(mode, false); } - return blockAckReq ? getPeerCompatibleMode(blockAckReq->getTransmitterAddress(), responseMode) : responseMode; + auto peerAddress = mib && blockAckReq->getReceiverAddress() == mib->address ? blockAckReq->getTransmitterAddress() : blockAckReq->getReceiverAddress(); + return getPeerCompatibleMode(peerAddress, responseMode); } const IIeee80211Mode *QosRateSelection::computeDataOrMgmtFrameMode(const Ptr& dataOrMgmtHeader) diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index 48e33da6c92..056d442aae4 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -20,12 +20,14 @@ void RecipientQosAckPolicy::initialize(int stage) ModeSetListener::initialize(stage); if (stage == INITSTAGE_LOCAL) { rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); + assumePeerSupportsCompressedBlockAck = par("assumePeerSupportsCompressedBlockAck"); } } -simtime_t RecipientQosAckPolicy::computeBasicBlockAckDuration(Packet *packet, const Ptr& blockAckReq) const +simtime_t RecipientQosAckPolicy::computeBlockAckDuration(Packet *packet, const Ptr& blockAckReq) const { - return rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq)->getDuration(LENGTH_BASIC_BLOCKACK); + b length = dynamicPtrCast(blockAckReq) ? LENGTH_COMPRESSED_BLOCKACK : LENGTH_BASIC_BLOCKACK; + return rateSelection->computeResponseBlockAckFrameMode(packet, blockAckReq)->getDuration(length); } simtime_t RecipientQosAckPolicy::computeAckDuration(Packet *packet, const Ptr& dataOrMgmtHeader) const @@ -67,10 +69,23 @@ bool RecipientQosAckPolicy::isBlockAckNeeded(const Ptr(blockAckReq)) + return isCompressedBlockAckNeeded(compressedBlockAckReq, agreement, assumePeerSupportsCompressedBlockAck); else throw cRuntimeError("Unsupported BlockAckReq"); } +bool RecipientQosAckPolicy::isCompressedBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck) +{ + // IEEE Std 802.11-2024, 9.3.1.7.2 and 10.25.6.5. Both endpoint policies + // must carry the explicit peer-capability assumption because this baseline + // does not model negotiated peer HT capability. + if (!assumePeerSupportsCompressedBlockAck || blockAckReq->getFragmentNumber() != 0) + return false; + // A missing partial state still elicits the mandatory null compressed BA. + return agreement == nullptr || (agreement->getIsAddbaResponseSent() && !agreement->getIsDelayedBlockAckPolicySupported()); +} + // // 8.2.5.7 Setting for control response frames // For an ACK frame, the Duration/ID field is set to the value obtained from the Duration/ID field of the frame @@ -90,9 +105,9 @@ simtime_t RecipientQosAckPolicy::computeAckDurationField(Packet *packet, const P // the PPDU carrying the frame that elicited the response and the end of the PPDU carrying the BlockAck // frame. // -simtime_t RecipientQosAckPolicy::computeBasicBlockAckDurationField(Packet *packet, const Ptr& basicBlockAckReq) const +simtime_t RecipientQosAckPolicy::computeBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const { - return basicBlockAckReq->getDurationField() - modeSet->getSifsTime() - computeBasicBlockAckDuration(packet, basicBlockAckReq); + return blockAckReq->getDurationField() - modeSet->getSifsTime() - computeBlockAckDuration(packet, blockAckReq); } } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h index 9f4ce456784..e1ec8677c86 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h @@ -20,12 +20,14 @@ class INET_API RecipientQosAckPolicy : public ModeSetListener, public IRecipient { protected: IQosRateSelection *rateSelection = nullptr; + bool assumePeerSupportsCompressedBlockAck = false; protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; - simtime_t computeBasicBlockAckDuration(Packet *packet, const Ptr& blockAckReq) const; + static bool isCompressedBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck); + simtime_t computeBlockAckDuration(Packet *packet, const Ptr& blockAckReq) const; simtime_t computeAckDuration(Packet *packet, const Ptr& dataOrMgmtHeader) const; public: @@ -33,11 +35,10 @@ class INET_API RecipientQosAckPolicy : public ModeSetListener, public IRecipient virtual bool isBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) const override; virtual simtime_t computeAckDurationField(Packet *packet, const Ptr& header) const override; - virtual simtime_t computeBasicBlockAckDurationField(Packet *packet, const Ptr& basicBlockAckReq) const override; + virtual simtime_t computeBlockAckDurationField(Packet *packet, const Ptr& blockAckReq) const override; }; } /* namespace ieee80211 */ } /* namespace inet */ #endif - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned index 50971923a80..02369ccfd24 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned @@ -18,6 +18,8 @@ simple RecipientQosAckPolicy extends SimpleModule like IRecipientQosAckPolicy parameters: @class(RecipientQosAckPolicy); string rateSelectionModule; + // Set only for an HT-or-later local STA when peer capability management establishes that the peer supports Compressed Block Ack. + // This explicit assumption is needed because peer HT capabilities are not represented by the baseline agreement contract. + bool assumePeerSupportsCompressedBlockAck = default(false); @display("i=block/control"); } - diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index ec3abe7b820..ec315ddf66a 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -249,10 +249,16 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c { Enter_Method("controlFrameReceived"); expireReceiveLifetime(); - if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + if (auto blockAckReq = dynamicPtrCast(controlHeader)) { BlockAckReordering::ReorderBuffer frames; if (blockAckReordering) { - Tid tid = blockAckReq->getTidInfo(); + Tid tid = -1; + if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) + tid = basicBlockAckReq->getTidInfo(); + else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) + tid = compressedBlockAckReq->getTidInfo(); + else + return std::vector(); MacAddress originatorAddr = blockAckReq->getTransmitterAddress(); RecipientBlockAckAgreement *agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(tid, originatorAddr); if (agreement) diff --git a/tests/module/Ieee80211CompressedBlockAckRuntime.test b/tests/module/Ieee80211CompressedBlockAckRuntime.test new file mode 100644 index 00000000000..ea7c4dbdc24 --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckRuntime.test @@ -0,0 +1,37 @@ +%description: + +Checks that an explicitly enabled one-TID HT-immediate Block Ack agreement uses +Compressed BlockAckReq and receives the SIFS Compressed BlockAck response. + +%extraargs: -c CompressedBlockAckRuntime -r 0 + +%inifile: omnetpp.ini + +include ../../../../examples/wireless/qos/omnetpp.ini + +[General] +ned-path = .;../../../../src;../../../../examples;../../lib +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false +seed-set = 0 + +[Config CompressedBlockAckRuntime] +extends = MacQosWithBlockAck +abstract = false +sim-time-limit = 1.05s + +**.opMode = "n(mixed-2.4Ghz)" +# Baseline capability management is absent, so both endpoint policies explicitly +# opt in to compressed Block Ack; the production default remains false. +**.assumePeerSupportsCompressedBlockAck = true +**.cmdenv-log-level = info + +%contains: stdout +Processing Addba Request from + +%contains: stdout +Processing transmitted frame CompressedBlockAckReq as originator in frame sequence. + +%contains: stdout +Ieee80211CompressedBlockAck has arrived diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test new file mode 100644 index 00000000000..eeb7f6fb503 --- /dev/null +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -0,0 +1,319 @@ +%description: +Validate one-TID HT Compressed Block Ack wire encoding and recipient bitmap construction. +IEEE Std 802.11-2024, 9.3.1.7.2, 9.3.1.8.2, 10.25.6.1, and 10.25.6.5. + +%includes: +#include "inet/common/packet/Packet.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; + +class TestRecipientBlockAckProcedure : public RecipientBlockAckProcedure +{ + public: + using RecipientBlockAckProcedure::buildBlockAck; +}; + +class TestOriginatorBlockAckAgreementHandler : public OriginatorBlockAckAgreementHandler +{ + public: + using OriginatorBlockAckAgreementHandler::updateAgreement; +}; + +class TestOriginatorQosAckPolicy : public OriginatorQosAckPolicy +{ + public: + static bool isCompressedRequestNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement, bool assumePeerSupport) + { + return isCompressedBlockAckReqNeeded(outstandingFrames, agreement, assumePeerSupport); + } +}; + +class TestRecipientQosAckPolicy : public RecipientQosAckPolicy +{ + public: + static bool isCompressedResponseNeeded(const Ptr& request, RecipientBlockAckAgreement *agreement, bool enabled) + { + return isCompressedBlockAckNeeded(request, agreement, enabled); + } +}; + +template +Ptr roundTrip(const Ptr& original) +{ + Packet packet("original", original); + auto bytes = packet.peekAllAsBytes(); + Packet decodedPacket("decoded", bytes); + auto decoded = decodedPacket.popAtFront(); + return dynamicPtrCast(decoded); +} + +static Ptr makeCompressedBlockAckReq() +{ + auto request = makeShared(); + request->setDurationField(SIMTIME_ZERO); + request->setReceiverAddress(MacAddress("10:20:30:40:50:60")); + request->setTransmitterAddress(MacAddress("11:22:33:44:55:66")); + request->setBarAckPolicy(true); + request->setTidInfo(5); + request->setFragmentNumber(0); + request->setStartingSequenceNumber(SequenceNumberCyclic(0xABC)); + return request; +} + +static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, FragmentNumber fragmentNumber = 0, bool moreFragments = false) +{ + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(receiverAddress); + header->setTid(tid); + header->setFragmentNumber(fragmentNumber); + header->setMoreFragments(moreFragments); + return new Packet("outstanding", header); +} + +%activity: +{ + auto request = makeCompressedBlockAckReq(); + Packet packet("compressedBar", request); + auto bytes = packet.peekAllAsBytes(); + ASSERT(bytes->getChunkLength() == B(20)); + ASSERT(bytes->getByte(16) == 0x05 && bytes->getByte(17) == 0x50); // BAR Control: policy=1, compressed=1, TID=5 + ASSERT(bytes->getByte(18) == 0xC0 && bytes->getByte(19) == 0xAB); // SSC: fragment=0, sequence=0xABC + auto decoded = roundTrip(request); + ASSERT(decoded != nullptr); + ASSERT(decoded->getBarAckPolicy() && decoded->getCompressedBitmap() && !decoded->getMultiTid()); + ASSERT(decoded->getTidInfo() == 5 && decoded->getFragmentNumber() == 0); + ASSERT(decoded->getStartingSequenceNumber() == SequenceNumberCyclic(0xABC)); + EV << "Compressed BAR encoding and round-trip passed.\n"; +} + +{ + auto blockAck = makeShared(); + blockAck->setDurationField(SIMTIME_ZERO); + blockAck->setReceiverAddress(MacAddress("11:22:33:44:55:66")); + blockAck->setTransmitterAddress(MacAddress("10:20:30:40:50:60")); + blockAck->setTidInfo(5); + blockAck->setFragmentNumber(0); + blockAck->setStartingSequenceNumber(SequenceNumberCyclic(0xABC)); + BitVector bitmap(std::vector({0x05, 0, 0, 0, 0, 0, 0, 0})); + blockAck->setBlockAckBitmap(bitmap); + Packet packet("compressedBa", blockAck); + auto bytes = packet.peekAllAsBytes(); + ASSERT(bytes->getChunkLength() == B(28)); + ASSERT(bytes->getByte(16) == 0x04 && bytes->getByte(17) == 0x50); // BA Control: compressed=1, TID=5 + ASSERT(bytes->getByte(18) == 0xC0 && bytes->getByte(19) == 0xAB); + ASSERT(bytes->getByte(20) == 0x05); + auto decoded = roundTrip(blockAck); + ASSERT(decoded != nullptr); + ASSERT(decoded->getBlockAckBitmap().getBit(0)); + ASSERT(!decoded->getBlockAckBitmap().getBit(1)); + ASSERT(decoded->getBlockAckBitmap().getBit(2)); + EV << "Compressed BA encoding and round-trip passed.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, nullptr)); + ASSERT(response != nullptr && response->getChunkLength() == B(28)); + for (int i = 0; i < 64; i++) + ASSERT(!response->getBlockAckBitmap().getBit(i)); + EV << "Missing recipient state produces a null compressed BA.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + RecipientBlockAckAgreement emptyAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &emptyAgreement)); + for (int i = 0; i < 64; i++) + ASSERT(!response->getBlockAckBitmap().getBit(i)); + EV << "Empty recipient record produces a null compressed BA.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(101), 64, SIMTIME_ZERO); + for (int sequenceNumber : {101, 103}) { + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->setAckPolicy(BLOCK_ACK); + data->setTid(5); + data->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + data->setFragmentNumber(0); + agreement.blockAckPolicyFrameReceived(data); + } + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(101)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + ASSERT(response->getBlockAckBitmap().getBit(0)); + ASSERT(!response->getBlockAckBitmap().getBit(1)); + ASSERT(response->getBlockAckBitmap().getBit(2)); + EV << "Established agreement produces the expected 64-bit bitmap.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->setAckPolicy(BLOCK_ACK); + data->setTid(5); + data->setSequenceNumber(SequenceNumberCyclic(102)); + data->setFragmentNumber(0); + agreement.blockAckPolicyFrameReceived(data); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + ASSERT(!response->getBlockAckBitmap().getBit(0)); + ASSERT(!response->getBlockAckBitmap().getBit(1)); + ASSERT(response->getBlockAckBitmap().getBit(2)); + EV << "Compressed bitmap preserves leading receive-window holes.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(4095), 64, SIMTIME_ZERO); + for (int sequenceNumber : {4095, 0}) { + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->setAckPolicy(BLOCK_ACK); + data->setTid(5); + data->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + data->setFragmentNumber(0); + agreement.blockAckPolicyFrameReceived(data); + } + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(4095)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + ASSERT(response->getBlockAckBitmap().getBit(0)); + ASSERT(response->getBlockAckBitmap().getBit(1)); + ASSERT(!response->getBlockAckBitmap().getBit(2)); + EV << "Compressed bitmap wraps from sequence 4095 to 0.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(4095), 64, SIMTIME_ZERO); + auto data = makeShared(); + data->setType(ST_DATA_WITH_QOS); + data->setAckPolicy(BLOCK_ACK); + data->setTid(5); + data->setSequenceNumber(SequenceNumberCyclic(1)); + data->setFragmentNumber(0); + agreement.blockAckPolicyFrameReceived(data); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(4095)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + ASSERT(!response->getBlockAckBitmap().getBit(0)); + ASSERT(!response->getBlockAckBitmap().getBit(1)); + ASSERT(response->getBlockAckBitmap().getBit(2)); + EV << "Compressed bitmap preserves leading holes across sequence wrap.\n"; +} + +{ + auto request = makeCompressedBlockAckReq(); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr, false)); + ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr, true)); + request->setFragmentNumber(1); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr, true)); + request->setFragmentNumber(0); + RecipientBlockAckAgreement delayedAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + delayedAgreement.addbaResposneSent(); + delayedAgreement.setIsDelayedBlockAckPolicySupported(true); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement, true)); + delayedAgreement.setIsDelayedBlockAckPolicySupported(false); + ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement, true)); + EV << "Recipient capability, fragment, and agreement-policy gates passed.\n"; +} + +{ + MacAddress receiverAddress("10:20:30:40:50:60"); + OriginatorBlockAckAgreement immediateAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, false); + immediateAgreement.setIsAddbaResponseReceived(true); + std::vector outstandingFrames { makeOutstandingQosFrame(receiverAddress, 5) }; + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, false)); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, nullptr, true)); + OriginatorBlockAckAgreement delayedAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, true); + delayedAgreement.setIsAddbaResponseReceived(true); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &delayedAgreement, true)); + ASSERT(TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, true)); + delete outstandingFrames[0]; + outstandingFrames = { makeOutstandingQosFrame(receiverAddress, 5, 1) }; + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, true)); + delete outstandingFrames[0]; + outstandingFrames = { makeOutstandingQosFrame(receiverAddress, 5, 0, true) }; + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, true)); + delete outstandingFrames[0]; + EV << "Originator capability, agreement, and fragmentation gates passed.\n"; +} + +{ + auto basicRequest = makeShared(); + basicRequest->setDurationField(SIMTIME_ZERO); + basicRequest->setReceiverAddress(MacAddress("10:20:30:40:50:60")); + basicRequest->setTransmitterAddress(MacAddress("11:22:33:44:55:66")); + basicRequest->setTidInfo(5); + basicRequest->setStartingSequenceNumber(SequenceNumberCyclic(0xABC)); + Packet packet("basicBar", basicRequest); + auto bytes = packet.peekAllAsBytes(); + ASSERT(bytes->getChunkLength() == B(20)); + ASSERT(bytes->getByte(16) == 0x00 && bytes->getByte(17) == 0x50); + ASSERT(roundTrip(basicRequest) != nullptr); + auto basicBlockAck = makeShared(); + basicBlockAck->setDurationField(SIMTIME_ZERO); + basicBlockAck->setReceiverAddress(MacAddress("11:22:33:44:55:66")); + basicBlockAck->setTransmitterAddress(MacAddress("10:20:30:40:50:60")); + basicBlockAck->setTidInfo(5); + basicBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(0xABC)); + for (int i = 0; i < 64; i++) + basicBlockAck->setBlockAckBitmap(i, BitVector(std::vector(2, 0))); + Packet blockAckPacket("basicBa", basicBlockAck); + auto blockAckBytes = blockAckPacket.peekAllAsBytes(); + ASSERT(blockAckBytes->getChunkLength() == B(148)); + ASSERT(blockAckBytes->getByte(16) == 0x00 && blockAckBytes->getByte(17) == 0x50); + ASSERT(blockAckBytes->getByte(18) == 0xC0 && blockAckBytes->getByte(19) == 0xAB); + ASSERT(roundTrip(basicBlockAck) != nullptr); + EV << "Legacy Basic BAR and BA remain byte-exact.\n"; +} + +{ + TestOriginatorBlockAckAgreementHandler handler; + OriginatorBlockAckAgreement agreement(MacAddress("10:20:30:40:50:60"), 5, SequenceNumberCyclic(100), 64, false, false); + auto delayedResponse = makeShared(); + delayedResponse->setBlockAckPolicy(false); + delayedResponse->setBufferSize(64); + delayedResponse->setBlockAckTimeoutValue(SIMTIME_ZERO); + handler.updateAgreement(&agreement, delayedResponse); + ASSERT(agreement.getIsAddbaResponseReceived()); + ASSERT(agreement.getIsDelayedBlockAckPolicySupported()); + EV << "Accepted delayed agreement is retained for Basic BAR fallback.\n"; +} + +EV << ".\n"; + +%contains: stdout +Compressed BAR encoding and round-trip passed. +Compressed BA encoding and round-trip passed. +Missing recipient state produces a null compressed BA. +Empty recipient record produces a null compressed BA. +Established agreement produces the expected 64-bit bitmap. +Compressed bitmap preserves leading receive-window holes. +Compressed bitmap wraps from sequence 4095 to 0. +Compressed bitmap preserves leading holes across sequence wrap. +Recipient capability, fragment, and agreement-policy gates passed. +Originator capability, agreement, and fragmentation gates passed. +Legacy Basic BAR and BA remain byte-exact. +Accepted delayed agreement is retained for Basic BAR fallback. +. From 6c6a191ddcf77a8ab9b46b0725dc4a74c3365632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sat, 15 Aug 2026 21:51:34 +0200 Subject: [PATCH 43/56] fix(ieee80211): select Block Ack rates by base variant Classify BlockAck and BlockAckReq frames through their base classes in QosRateSelection so compressed requests follow the same control-rate policy as Basic requests. This prevents an originated Compressed BlockAckReq from falling through to the ordinary control-frame path and reusing the last transmitted data mode when the mandatory Block Ack fallback should be selected. Add focused coverage with distinct mandatory and last-transmitted modes, verifying Basic and Compressed BAR symmetry while preserving RTS behavior. --- .../mac/rateselection/QosRateSelection.cc | 9 ++--- tests/unit/Ieee80211CompressedBlockAck_1.test | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc index 1d7449cc0e9..2506372a4c4 100644 --- a/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc +++ b/src/inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.cc @@ -215,17 +215,18 @@ const IIeee80211Mode *QosRateSelection::computeControlFrameMode(const Ptr(header) != nullptr || dynamicPtrCast(header) != nullptr; // This subclause describes the rate selection rules for control frames that initiate a TXOP and that are not carried // in an A-MPDU. if (txopProcedure->isTxopInitiator(header)) { - // If a control frame other than a Basic BlockAckReq or Basic BlockAck is carried in a non-HT PPDU, the + // If a control frame other than a BlockAckReq or BlockAck is carried in a non-HT PPDU, the // transmitting STA shall transmit the frame using one of the rates in the BSSBasicRateSet parameter or a rate // from the mandatory rate set of the attached PHY if the BSSBasicRateSet is empty. - if (!dynamicPtrCast(header) && !dynamicPtrCast(header)) { + if (!isBlockAckFrame) { // TODO BSSBasicRateSet return fastestMandatoryMode; } - // If a Basic BlockAckReq or Basic BlockAck frame is carried in a non-HT PPDU, the transmitting STA shall + // If a BlockAckReq or BlockAck frame is carried in a non-HT PPDU, the transmitting STA shall // transmit the frame using a rate supported by the receiver STA, if known (as reported in the Supported Rates // element and/or Extended Supported Rates element in frames transmitted by that STA). If the supported rate set // of the receiving STA or STAs is not known, the transmitting STA shall transmit using a rate from the @@ -247,7 +248,7 @@ const IIeee80211Mode *QosRateSelection::computeControlFrameMode(const Ptr(header) && !dynamicPtrCast(header)) { + if (!isBlockAckFrame) { // TODO frame sequence context auto it = lastTransmittedFrameMode.find(header->getReceiverAddress()); return (it != lastTransmittedFrameMode.end()) ? it->second : fastestMandatoryMode; diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index eeb7f6fb503..2fd6b9732b5 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -10,7 +10,10 @@ IEEE Std 802.11-2024, 9.3.1.7.2, 9.3.1.8.2, 10.25.6.1, and 10.25.6.5. #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" +#include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" #include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h" %global: using namespace inet; @@ -46,6 +49,14 @@ class TestRecipientQosAckPolicy : public RecipientQosAckPolicy } }; +class TestQosRateSelection : public QosRateSelection +{ + public: + void setFastestMandatoryMode(const physicallayer::IIeee80211Mode *mode) { fastestMandatoryMode = mode; } + void setLastTransmittedFrameMode(const MacAddress& receiverAddress, const physicallayer::IIeee80211Mode *mode) { lastTransmittedFrameMode[receiverAddress] = mode; } + using QosRateSelection::computeControlFrameMode; +}; + template Ptr roundTrip(const Ptr& original) { @@ -301,6 +312,29 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag EV << "Accepted delayed agreement is retained for Basic BAR fallback.\n"; } +{ + // IEEE Std 802.11-2024, 10.6.6.4 applies the control-frame rate rule to BlockAckReq and BlockAck variants. + const auto mandatoryMode = &physicallayer::Ieee80211OfdmCompliantModes::ofdmMode6MbpsCS20MHz; + const auto lastTransmittedMode = &physicallayer::Ieee80211OfdmCompliantModes::ofdmMode54Mbps; + MacAddress receiverAddress("10:20:30:40:50:60"); + TestQosRateSelection rateSelection; + TxopProcedure txopProcedure; + rateSelection.setFastestMandatoryMode(mandatoryMode); + rateSelection.setLastTransmittedFrameMode(receiverAddress, lastTransmittedMode); + + auto basicRequest = makeShared(); + basicRequest->setReceiverAddress(receiverAddress); + auto compressedRequest = makeCompressedBlockAckReq(); + compressedRequest->setReceiverAddress(receiverAddress); + ASSERT(rateSelection.computeControlFrameMode(basicRequest, &txopProcedure) == mandatoryMode); + ASSERT(rateSelection.computeControlFrameMode(compressedRequest, &txopProcedure) == mandatoryMode); + + auto rtsFrame = makeShared(); + rtsFrame->setReceiverAddress(receiverAddress); + ASSERT(rateSelection.computeControlFrameMode(rtsFrame, &txopProcedure) == lastTransmittedMode); + EV << "Basic and Compressed BAR rate selection is variant-agnostic.\n"; +} + EV << ".\n"; %contains: stdout @@ -316,4 +350,5 @@ Recipient capability, fragment, and agreement-policy gates passed. Originator capability, agreement, and fragmentation gates passed. Legacy Basic BAR and BA remain byte-exact. Accepted delayed agreement is retained for Basic BAR fallback. +Basic and Compressed BAR rate selection is variant-agnostic. . From 26f65fd83d819a01717de69b7665bfd30f6a7fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 16 Aug 2026 01:25:44 +0200 Subject: [PATCH 44/56] fix(ieee80211): answer valid compressed block ack requests Remove the recipient-side peer capability assumption from compressed Block Ack response selection. An addressed, syntactically valid one-TID compressed BlockAckReq must receive a compressed BlockAck after SIFS, including the all-zero response when no matching partial state exists, as required by IEEE 802.11-2024 sections 9.3.1.7.2 and 10.25.6.5. Keep the originator-side capability assumption as the explicit opt-in for selecting compressed BlockAckReq frames until per-peer HT capability management is modeled. Continue rejecting nonzero fragment numbers and suppressing immediate responses for unaccepted or delayed agreements. Update the focused unit and runtime coverage to verify null-state responses, malformed fragment rejection, agreement-policy gates, and asymmetric endpoint configuration. --- .../mac/recipient/RecipientQosAckPolicy.cc | 15 ++++++--------- .../mac/recipient/RecipientQosAckPolicy.h | 3 +-- .../mac/recipient/RecipientQosAckPolicy.ned | 3 --- .../Ieee80211CompressedBlockAckRuntime.test | 6 +++--- tests/unit/Ieee80211CompressedBlockAck_1.test | 18 +++++++++--------- 5 files changed, 19 insertions(+), 26 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index 056d442aae4..51cbb532c64 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -18,10 +18,8 @@ Define_Module(RecipientQosAckPolicy); void RecipientQosAckPolicy::initialize(int stage) { ModeSetListener::initialize(stage); - if (stage == INITSTAGE_LOCAL) { + if (stage == INITSTAGE_LOCAL) rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); - assumePeerSupportsCompressedBlockAck = par("assumePeerSupportsCompressedBlockAck"); - } } simtime_t RecipientQosAckPolicy::computeBlockAckDuration(Packet *packet, const Ptr& blockAckReq) const @@ -70,17 +68,16 @@ bool RecipientQosAckPolicy::isBlockAckNeeded(const Ptr(blockAckReq)) - return isCompressedBlockAckNeeded(compressedBlockAckReq, agreement, assumePeerSupportsCompressedBlockAck); + return isCompressedBlockAckNeeded(compressedBlockAckReq, agreement); else throw cRuntimeError("Unsupported BlockAckReq"); } -bool RecipientQosAckPolicy::isCompressedBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck) +bool RecipientQosAckPolicy::isCompressedBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) { - // IEEE Std 802.11-2024, 9.3.1.7.2 and 10.25.6.5. Both endpoint policies - // must carry the explicit peer-capability assumption because this baseline - // does not model negotiated peer HT capability. - if (!assumePeerSupportsCompressedBlockAck || blockAckReq->getFragmentNumber() != 0) + // IEEE Std 802.11-2024, 9.3.1.7.2 and 10.25.6.5: an addressed, syntactically + // valid Compressed BlockAckReq elicits a Compressed BlockAck, including a null response. + if (blockAckReq->getFragmentNumber() != 0) return false; // A missing partial state still elicits the mandatory null compressed BA. return agreement == nullptr || (agreement->getIsAddbaResponseSent() && !agreement->getIsDelayedBlockAckPolicySupported()); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h index e1ec8677c86..d74e0ecc962 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h @@ -20,13 +20,12 @@ class INET_API RecipientQosAckPolicy : public ModeSetListener, public IRecipient { protected: IQosRateSelection *rateSelection = nullptr; - bool assumePeerSupportsCompressedBlockAck = false; protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int stage) override; - static bool isCompressedBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck); + static bool isCompressedBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement); simtime_t computeBlockAckDuration(Packet *packet, const Ptr& blockAckReq) const; simtime_t computeAckDuration(Packet *packet, const Ptr& dataOrMgmtHeader) const; diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned index 02369ccfd24..6fb9639b69e 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.ned @@ -18,8 +18,5 @@ simple RecipientQosAckPolicy extends SimpleModule like IRecipientQosAckPolicy parameters: @class(RecipientQosAckPolicy); string rateSelectionModule; - // Set only for an HT-or-later local STA when peer capability management establishes that the peer supports Compressed Block Ack. - // This explicit assumption is needed because peer HT capabilities are not represented by the baseline agreement contract. - bool assumePeerSupportsCompressedBlockAck = default(false); @display("i=block/control"); } diff --git a/tests/module/Ieee80211CompressedBlockAckRuntime.test b/tests/module/Ieee80211CompressedBlockAckRuntime.test index ea7c4dbdc24..8dde5158a5c 100644 --- a/tests/module/Ieee80211CompressedBlockAckRuntime.test +++ b/tests/module/Ieee80211CompressedBlockAckRuntime.test @@ -22,9 +22,9 @@ abstract = false sim-time-limit = 1.05s **.opMode = "n(mixed-2.4Ghz)" -# Baseline capability management is absent, so both endpoint policies explicitly -# opt in to compressed Block Ack; the production default remains false. -**.assumePeerSupportsCompressedBlockAck = true +# The originator explicitly opts in based on its peer-capability assumption; +# the recipient requires no additional station-wide capability setting. +**.originatorAckPolicy.assumePeerSupportsCompressedBlockAck = true **.cmdenv-log-level = info %contains: stdout diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index 2fd6b9732b5..8d34b512d95 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -43,9 +43,9 @@ class TestOriginatorQosAckPolicy : public OriginatorQosAckPolicy class TestRecipientQosAckPolicy : public RecipientQosAckPolicy { public: - static bool isCompressedResponseNeeded(const Ptr& request, RecipientBlockAckAgreement *agreement, bool enabled) + static bool isCompressedResponseNeeded(const Ptr& request, RecipientBlockAckAgreement *agreement) { - return isCompressedBlockAckNeeded(request, agreement, enabled); + return isCompressedBlockAckNeeded(request, agreement); } }; @@ -235,18 +235,18 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag { auto request = makeCompressedBlockAckReq(); - ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr, false)); - ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr, true)); + ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr)); request->setFragmentNumber(1); - ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr, true)); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr)); request->setFragmentNumber(0); RecipientBlockAckAgreement delayedAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement)); delayedAgreement.addbaResposneSent(); delayedAgreement.setIsDelayedBlockAckPolicySupported(true); - ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement, true)); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement)); delayedAgreement.setIsDelayedBlockAckPolicySupported(false); - ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement, true)); - EV << "Recipient capability, fragment, and agreement-policy gates passed.\n"; + ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement)); + EV << "Recipient fragment and agreement-policy gates passed.\n"; } { @@ -346,7 +346,7 @@ Established agreement produces the expected 64-bit bitmap. Compressed bitmap preserves leading receive-window holes. Compressed bitmap wraps from sequence 4095 to 0. Compressed bitmap preserves leading holes across sequence wrap. -Recipient capability, fragment, and agreement-policy gates passed. +Recipient fragment and agreement-policy gates passed. Originator capability, agreement, and fragmentation gates passed. Legacy Basic BAR and BA remain byte-exact. Accepted delayed agreement is retained for Basic BAR fallback. From 63e860eadc3cba6ab8bf2c66b8dc521721b826f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 16 Aug 2026 11:18:20 +0200 Subject: [PATCH 45/56] fix(ieee80211): correct Block Ack receive-window handling Make BlockAckRecord the authoritative owner of WinStartR and update the scoreboard for every successfully received related Data MPDU. Advance it from newer Data frames and BlockAckReq starting sequence numbers instead of coupling acknowledgment state to upward packet delivery. Fix empty Basic Block Ack records so current-window sequence and fragment entries are reported as unacknowledged while entries older than WinStartR remain acknowledged. Keep the reordering window independent from the acknowledgment window. When a future MPDU or BAR advances the window, deliver complete displaced MSDUs, discard incomplete stale entries, retain packets beyond gaps, and preserve cyclic delivery order across the 4095-to-0 boundary. Detach returned packets before deleting stale receive-buffer state. Replace the station-wide compressed Block Ack assumption with capability state stored per agreement. Derive it from explicit local support and a configured peer-address list until HT Capabilities elements are modeled. Use compressed BAR only for established immediate agreements that support it, and suppress compressed Block Ack responses when no agreement exists. Add focused unit and runtime coverage for Basic and Compressed bitmaps, Data- and BAR-driven window movement, wraparound, gaps, fragments, duplicates, ownership, Normal Ack reception, capability gating, null responses, serialization, and the BAR-to-BA exchange. Multi-TID Block Ack remains unsupported. BA Control bit 0 remains clear because it is reserved by IEEE 802.11-2024, rather than a BA Ack Policy bit. Fingerprint validation found five expected maintained-ingredient changes in QoS and Block Ack scenarios. The three previously retained tyf values were also rechecked and found stale. Fingerprint CSV updates are intentionally not included pending separate approval. --- .../ieee80211/mac/blockack/BlockAckRecord.cc | 28 +- .../ieee80211/mac/blockack/BlockAckRecord.h | 5 +- .../blockack/OriginatorBlockAckAgreement.h | 3 + .../OriginatorBlockAckAgreementHandler.cc | 10 +- .../OriginatorBlockAckAgreementHandler.h | 4 +- .../OriginatorBlockAckAgreementPolicy.cc | 3 + .../OriginatorBlockAckAgreementPolicy.h | 3 + .../OriginatorBlockAckAgreementPolicy.ned | 2 + .../blockack/RecipientBlockAckAgreement.cc | 8 +- .../mac/blockack/RecipientBlockAckAgreement.h | 5 +- .../blockackreordering/BlockAckReordering.cc | 86 ++-- .../blockackreordering/BlockAckReordering.h | 6 +- .../IOriginatorBlockAckAgreementPolicy.h | 1 + .../mac/originator/OriginatorQosAckPolicy.cc | 10 +- .../mac/originator/OriginatorQosAckPolicy.h | 3 +- .../mac/originator/OriginatorQosAckPolicy.ned | 3 - .../mac/recipient/RecipientQosAckPolicy.cc | 7 +- .../recipient/RecipientQosMacDataService.cc | 2 +- .../Ieee80211CompressedBlockAckRuntime.test | 9 +- tests/unit/Ieee80211AddbaTransaction_1.test | 9 +- tests/unit/Ieee80211CompressedBlockAck_1.test | 386 +++++++++++++++++- 21 files changed, 493 insertions(+), 100 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index 7dce744fdec..14a21493581 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -19,10 +19,14 @@ BlockAckRecord::BlockAckRecord(MacAddress originatorAddress, Tid tid, SequenceNu { } -void BlockAckRecord::blockAckPolicyFrameReceived(const Ptr& header) +void BlockAckRecord::dataFrameReceived(const Ptr& header, int windowSize) { SequenceNumberCyclic sequenceNumber = header->getSequenceNumber(); FragmentNumber fragmentNumber = header->getFragmentNumber(); + // IEEE Std 802.11-2024, 10.25.6.3(b) and 10.25.6.4(c): a related + // MPDU beyond WinEndR advances the receive window before its bit is set. + if (startingSequenceNumber + windowSize <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048) + advanceStartingSequenceNumber(sequenceNumber - windowSize + 1); acknowledgmentState[SequenceControlField(sequenceNumber.get(), fragmentNumber)] = true; } @@ -31,16 +35,7 @@ bool BlockAckRecord::getAckState(SequenceNumberCyclic sequenceNumber, FragmentNu // The status of MPDUs that are considered “old” and prior to the sequence number // range for which the receiver maintains status shall be reported as successfully // received (i.e., the corresponding bit in the bitmap shall be set to 1). - if (containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), fragmentNumber))) { - return true; - } - else if (acknowledgmentState.size() == 0) { - return true; // TODO old? - } - else { - auto earliest = acknowledgmentState.begin(); - return SequenceNumberCyclic(earliest->first.getSequenceNumber()) > sequenceNumber; // old = true - } + return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), fragmentNumber)) || sequenceNumber < startingSequenceNumber; } bool BlockAckRecord::getCompressedAckState(SequenceNumberCyclic sequenceNumber) @@ -50,17 +45,20 @@ bool BlockAckRecord::getCompressedAckState(SequenceNumberCyclic sequenceNumber) return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), 0)) || sequenceNumber < startingSequenceNumber; } -void BlockAckRecord::removeAckStates(SequenceNumberCyclic sequenceNumber) +void BlockAckRecord::advanceStartingSequenceNumber(SequenceNumberCyclic newStartingSequenceNumber) { + // IEEE Std 802.11-2024, 10.25.6.3 and 10.25.6.4: advance WinStartR + // for a newer related MPDU or BAR SSN, using the 12-bit sequence space. + if (!(startingSequenceNumber < newStartingSequenceNumber)) + return; auto it = acknowledgmentState.begin(); while (it != acknowledgmentState.end()) { - if (SequenceNumberCyclic(it->first.getSequenceNumber()) < sequenceNumber) + if (SequenceNumberCyclic(it->first.getSequenceNumber()) < newStartingSequenceNumber) it = acknowledgmentState.erase(it); else it++; } - if (startingSequenceNumber <= sequenceNumber) - startingSequenceNumber = sequenceNumber + 1; + startingSequenceNumber = newStartingSequenceNumber; } } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h index 3eb8f9bec46..c4bb3783ceb 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h @@ -31,13 +31,14 @@ class INET_API BlockAckRecord BlockAckRecord(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber); virtual ~BlockAckRecord() {} - void blockAckPolicyFrameReceived(const Ptr& header); + void dataFrameReceived(const Ptr& header, int windowSize); bool getAckState(SequenceNumberCyclic sequenceNumber, FragmentNumber fragmentNumber); bool getCompressedAckState(SequenceNumberCyclic sequenceNumber); - void removeAckStates(SequenceNumberCyclic sequenceNumber); + void advanceStartingSequenceNumber(SequenceNumberCyclic startingSequenceNumber); MacAddress getOriginatorAddress() { return originatorAddress; } Tid getTid() { return tid; } + SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } }; } /* namespace ieee80211 */ diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h index 25bad8d77a2..e2babf37b45 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h @@ -31,6 +31,7 @@ class INET_API OriginatorBlockAckAgreement : public cObject int bufferSize = -1; bool isAMsduSupported = false; bool isDelayedBlockAckPolicySupported = false; + bool isCompressedBlockAckSupported = false; OriginatorBlockAckAgreementState state = OriginatorBlockAckAgreementState::PENDING; bool isAddbaRequestSent = false; uint8_t dialogToken = 0; @@ -65,6 +66,7 @@ class INET_API OriginatorBlockAckAgreement : public cObject virtual bool getIsAddbaRequestSent() const { return isAddbaRequestSent; } virtual bool getIsAMsduSupported() const { return isAMsduSupported; } virtual bool getIsDelayedBlockAckPolicySupported() const { return isDelayedBlockAckPolicySupported; } + virtual bool getIsCompressedBlockAckSupported() const { return isCompressedBlockAckSupported; } virtual MacAddress getReceiverAddr() const { return receiverAddr; } virtual Tid getTid() const { return tid; } virtual const simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } @@ -78,6 +80,7 @@ class INET_API OriginatorBlockAckAgreement : public cObject 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 setIsCompressedBlockAckSupported(bool isCompressedBlockAckSupported) { this->isCompressedBlockAckSupported = isCompressedBlockAckSupported; } virtual void setBlockAckTimeoutValue(const simtime_t blockAckTimeoutValue) { this->blockAckTimeoutValue = blockAckTimeoutValue; } virtual void setAddbaResponseDeadline(simtime_t addbaResponseDeadline) { this->addbaResponseDeadline = addbaResponseDeadline; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc index bf5ec46863e..90c63a73e28 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.cc @@ -18,10 +18,11 @@ namespace inet { namespace ieee80211 { -void OriginatorBlockAckAgreementHandler::createAgreement(const Ptr& addbaRequest, uint64_t transactionId) +void OriginatorBlockAckAgreementHandler::createAgreement(const Ptr& addbaRequest, uint64_t transactionId, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) { ASSERT(addbaRequest->getDialogToken() != 0); OriginatorBlockAckAgreement *blockAckAgreement = new OriginatorBlockAckAgreement(addbaRequest->getReceiverAddress(), addbaRequest->getTid(), addbaRequest->getStartingSequenceNumber(), addbaRequest->getBufferSize(), addbaRequest->getAMsduSupported(), addbaRequest->getBlockAckPolicy() == 0, addbaRequest->getDialogToken(), transactionId); + blockAckAgreement->setIsCompressedBlockAckSupported(blockAckAgreementPolicy->isPeerCompressedBlockAckSupported(addbaRequest->getReceiverAddress())); auto agreementId = std::make_pair(addbaRequest->getReceiverAddress(), addbaRequest->getTid()); blockAckAgreements[agreementId] = blockAckAgreement; } @@ -257,7 +258,7 @@ uint64_t OriginatorBlockAckAgreementHandler::processAcknowledgedDataFrame(Packet // after the acknowledged trigger MPDU. auto addbaReq = buildAddbaRequest(receiverAddr, tid, dataHeader->getSequenceNumber() + 1, blockAckAgreementPolicy); auto transactionId = nextTransactionId++; - createAgreement(addbaReq, transactionId); + createAgreement(addbaReq, transactionId, blockAckAgreementPolicy); auto addbaPacket = new Packet("AddbaReq", addbaReq); addbaPacket->addTag()->setTransactionId(transactionId); procedureCallback->processMgmtFrame(addbaPacket, addbaReq); @@ -276,7 +277,7 @@ OriginatorBlockAckAgreementResponse OriginatorBlockAckAgreementHandler::processR bool acceptedByLocalPolicy = addbaResp->getStatusCode() == 0 && blockAckAgreementPolicy->isAddbaReqAccepted(addbaResp, agreement); if (addbaResp->getStatusCode() == 0) { auto transactionId = agreement->getTransactionId(); - updateAgreement(agreement, addbaResp); + updateAgreement(agreement, addbaResp, blockAckAgreementPolicy); if (acceptedByLocalPolicy) addbaRetryDeadlines.erase(std::make_pair(addbaResp->getTransmitterAddress(), addbaResp->getTid())); else @@ -309,12 +310,13 @@ OriginatorBlockAckAgreementResponse OriginatorBlockAckAgreementHandler::processR } } -void OriginatorBlockAckAgreementHandler::updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp) +void OriginatorBlockAckAgreementHandler::updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy) { agreement->setIsAddbaResponseReceived(true); agreement->setIsDelayedBlockAckPolicySupported(addbaResp->getBlockAckPolicy() == 0); agreement->setBufferSize(addbaResp->getBufferSize()); agreement->setBlockAckTimeoutValue(addbaResp->getBlockAckTimeoutValue()); + agreement->setIsCompressedBlockAckSupported(blockAckAgreementPolicy->isPeerCompressedBlockAckSupported(addbaResp->getTransmitterAddress())); agreement->calculateExpirationTime(); } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h index 96e6ca12721..9c80eee3994 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h @@ -31,8 +31,8 @@ class INET_API OriginatorBlockAckAgreementHandler : public IOriginatorBlockAckAg protected: virtual const Ptr buildAddbaRequest(MacAddress receiverAddr, Tid tid, SequenceNumberCyclic startingSequenceNumber, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); virtual uint8_t allocateDialogToken(); - virtual void createAgreement(const Ptr& addbaRequest, uint64_t transactionId); - virtual void updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp); + virtual void createAgreement(const Ptr& addbaRequest, uint64_t transactionId, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); + virtual void updateAgreement(OriginatorBlockAckAgreement *agreement, const Ptr& addbaResp, IOriginatorBlockAckAgreementPolicy *blockAckAgreementPolicy); virtual OriginatorBlockAckAgreement *removeAgreement(MacAddress originatorAddr, Tid tid); virtual void terminateAgreement(MacAddress originatorAddr, Tid tid); virtual const Ptr buildDelba(MacAddress receiverAddr, Tid tid, int reasonCode); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc index 0382f53e378..b8ce3ae768d 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.cc @@ -26,6 +26,9 @@ void OriginatorBlockAckAgreementPolicy::initialize(int stage) blockAckTimeoutValue = par("blockAckTimeoutValue"); addbaResponseTimeout = par("addbaResponseTimeout"); addbaRetryBackoff = par("addbaRetryBackoff"); + localCompressedBlockAckSupported = par("localCompressedBlockAckSupported"); + for (const auto& address : cStringTokenizer(par("compressedBlockAckPeerAddresses")).asVector()) + compressedBlockAckPeerAddresses.insert(MacAddress(address.c_str())); if (addbaResponseTimeout <= 0) throw cRuntimeError("addbaResponseTimeout must be greater than zero"); if (addbaRetryBackoff < 0) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h index a65fd75c037..10cccd96635 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h @@ -27,6 +27,8 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetListener, publi simtime_t blockAckTimeoutValue = -1; simtime_t addbaResponseTimeout = -1; simtime_t addbaRetryBackoff = -1; + bool localCompressedBlockAckSupported = false; + std::set compressedBlockAckPeerAddresses; protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } @@ -43,6 +45,7 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetListener, publi virtual bool isMsduSupported() const override { return aMsduSupported; } virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } virtual bool isDelayedAckPolicySupported() const override { return delayedAckPolicySupported; } + virtual bool isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const override { return localCompressedBlockAckSupported && compressedBlockAckPeerAddresses.find(peerAddress) != compressedBlockAckPeerAddresses.end(); } virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } }; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned index 89e42723736..6f9ec90d2f5 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.ned @@ -25,5 +25,7 @@ simple OriginatorBlockAckAgreementPolicy extends SimpleModule like IOriginatorBl 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 + bool localCompressedBlockAckSupported = default(false); + string compressedBlockAckPeerAddresses = default(""); @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 893b43f3217..872e1918d96 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.cc @@ -13,7 +13,6 @@ namespace inet { namespace ieee80211 { RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber, int bufferSize, simtime_t lastUsedTime, uint64_t generationId) : - startingSequenceNumber(startingSequenceNumber), bufferSize(bufferSize), blockAckTimeoutValue(lastUsedTime), generationId(generationId) @@ -22,17 +21,16 @@ RecipientBlockAckAgreement::RecipientBlockAckAgreement(MacAddress originatorAddr blockAckRecord = new BlockAckRecord(originatorAddress, tid, startingSequenceNumber); } -void RecipientBlockAckAgreement::blockAckPolicyFrameReceived(const Ptr& header) +void RecipientBlockAckAgreement::dataFrameReceived(const Ptr& header) { - ASSERT(header->getAckPolicy() == BLOCK_ACK); - blockAckRecord->blockAckPolicyFrameReceived(header); + blockAckRecord->dataFrameReceived(header, bufferSize); } std::ostream& operator<<(std::ostream& os, const RecipientBlockAckAgreement& agreement) { os << "originator address = " << agreement.blockAckRecord->getOriginatorAddress() << ", " << "tid = " << agreement.blockAckRecord->getTid() << ", " - << "starting sequence number = " << agreement.startingSequenceNumber << ", " + << "starting sequence number = " << agreement.getStartingSequenceNumber() << ", " << "buffer size = " << agreement.bufferSize << ", " << "block ack timeout value = " << agreement.blockAckTimeoutValue; return os; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index d2da01ea240..02a7a705f30 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -18,7 +18,6 @@ class INET_API RecipientBlockAckAgreement : public cObject protected: BlockAckRecord *blockAckRecord = nullptr; - SequenceNumberCyclic startingSequenceNumber; int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; bool isAddbaResponseSent = false; @@ -33,12 +32,12 @@ class INET_API RecipientBlockAckAgreement : public cObject 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); + virtual void dataFrameReceived(const Ptr& header); virtual BlockAckRecord *getBlockAckRecord() const { return blockAckRecord; } virtual simtime_t getBlockAckTimeoutValue() const { return blockAckTimeoutValue; } virtual int getBufferSize() const { return bufferSize; } - virtual SequenceNumberCyclic getStartingSequenceNumber() const { return startingSequenceNumber; } + virtual SequenceNumberCyclic getStartingSequenceNumber() const { return blockAckRecord->getStartingSequenceNumber(); } virtual uint64_t getGenerationId() const { return generationId; } virtual void calculateExpirationTime() { diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 6daf0e217bc..708efac89b8 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -32,21 +32,42 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived { QosFrameProcessingResult result; ReceiveBuffer *receiveBuffer = createReceiveBufferIfNecessary(agreement); + ReorderBuffer framesToPassUp; + auto sequenceNumber = dataHeader->getSequenceNumber(); + auto startingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); + bool advancesWindow = startingSequenceNumber + receiveBuffer->getBufferSize() <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048; + if (advancesWindow) { + // IEEE Std 802.11-2024, 10.25.6.6.2.1(b): move WinStartB so the + // future MPDU fits, preserving complete displaced MSDUs for delivery. + auto newStartingSequenceNumber = sequenceNumber - receiveBuffer->getBufferSize() + 1; + framesToPassUp = collectCompletePrecedingMpdus(receiveBuffer, newStartingSequenceNumber); + for (const auto& entry : framesToPassUp) + receiveBuffer->removeFrame(SequenceNumberCyclic(entry.first)); + // Any remaining displaced entries are incomplete and cannot be delivered. + receiveBuffer->dropFramesUntil(newStartingSequenceNumber); + receiveBuffer->setNextExpectedSequenceNumber(newStartingSequenceNumber); + } // 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. auto insertionResult = receiveBuffer->insertFrameWithResult(dataPacket, dataHeader); if (insertionResult == ReceiveBuffer::FrameInsertionResult::INSERTED) { - if (dataHeader->getAckPolicy() == BLOCK_ACK) - agreement->blockAckPolicyFrameReceived(dataHeader); + agreement->dataFrameReceived(dataHeader); + if (advancesWindow) { + auto consecutiveCompleteMpdus = collectConsecutiveCompleteFollowingMpdus(receiveBuffer, receiveBuffer->getNextExpectedSequenceNumber()); + releaseReceiveBuffer(receiveBuffer, consecutiveCompleteMpdus); + framesToPassUp.insert(framesToPassUp.end(), consecutiveCompleteMpdus.begin(), consecutiveCompleteMpdus.end()); + result.frames = framesToPassUp; + return result; + } auto earliestCompleteMsduOrAMsdu = getEarliestCompleteMsduOrAMsduIfExists(receiveBuffer); if (earliestCompleteMsduOrAMsdu.size() > 0) { auto earliestSequenceNumber = earliestCompleteMsduOrAMsdu.at(0)->peekAtFront()->getSequenceNumber(); // If, after an MPDU is received, the receive buffer is full, the complete MSDU or A-MSDU with the earliest // sequence number shall be passed up to the next MAC process. if (receiveBuffer->isFull()) { - passedUp(agreement, receiveBuffer, earliestSequenceNumber); + passedUp(receiveBuffer, earliestSequenceNumber); result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); return result; } @@ -54,7 +75,7 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived // 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); + passedUp(receiveBuffer, earliestSequenceNumber); result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); return result; } @@ -66,8 +87,7 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived // 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); + agreement->dataFrameReceived(dataHeader); result.tombstonedFragments.push_back(dataPacket); return result; } @@ -104,6 +124,9 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedBlockAckReq else { throw cRuntimeError("Multi-Tid BlockAckReq is currently an unimplemented feature"); } + // IEEE Std 802.11-2024, 10.25.6.3-10.25.6.5: adjust WinStartR + // from the BAR before generating the response, even without a receive buffer. + agreement->getBlockAckRecord()->advanceStartingSequenceNumber(startingSequenceNumber); auto id = std::make_pair(tid, blockAckReq->getTransmitterAddress()); auto it = receiveBuffers.find(id); if (it != receiveBuffers.end()) { @@ -117,19 +140,17 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedBlockAckReq // the starting sequence number sequentially until there is an incomplete or missing MSDU // or A-MSDU in the buffer. auto consecutiveCompleteFollowingMpdus = collectConsecutiveCompleteFollowingMpdus(receiveBuffer, startingSequenceNumber); - // If no MSDUs or A-MSDUs are passed up to the next MAC process after the receipt - // of the BlockAckReq frame and the starting sequence number of the BlockAckReq frame is newer than the - // NextExpectedSequenceNumber for that Block Ack agreement, then the NextExpectedSequenceNumber for - // that Block Ack agreement is set to the sequence number of the BlockAckReq frame. - int numOfMsdusToPassUp = completePrecedingMpdus.size() + consecutiveCompleteFollowingMpdus.size(); - if (numOfMsdusToPassUp == 0 && receiveBuffer->getNextExpectedSequenceNumber() < startingSequenceNumber) - receiveBuffer->setNextExpectedSequenceNumber(startingSequenceNumber); - // The recipient shall then release any buffers held by preceding MPDUs. - releaseReceiveBuffer(agreement, receiveBuffer, completePrecedingMpdus); - releaseReceiveBuffer(agreement, receiveBuffer, consecutiveCompleteFollowingMpdus); // The recipient shall pass MSDUs and A-MSDUs up to the next MAC process in order of increasing sequence // number. - completePrecedingMpdus.insert(consecutiveCompleteFollowingMpdus.begin(), consecutiveCompleteFollowingMpdus.end()); + completePrecedingMpdus.insert(completePrecedingMpdus.end(), consecutiveCompleteFollowingMpdus.begin(), consecutiveCompleteFollowingMpdus.end()); + // Detach all packets being returned before releasing stale buffered entries. + releaseReceiveBuffer(receiveBuffer, completePrecedingMpdus); + // Release any remaining buffers held by incomplete preceding MPDUs, then + // advance NextExpectedSequenceNumber to at least the BAR SSN without + // regressing it past consecutively released MSDUs. + receiveBuffer->dropFramesUntil(startingSequenceNumber); + if (receiveBuffer->getNextExpectedSequenceNumber() < startingSequenceNumber) + receiveBuffer->setNextExpectedSequenceNumber(startingSequenceNumber); return completePrecedingMpdus; } return ReorderBuffer(); @@ -144,12 +165,14 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::collectCompletePrecedingMp { ReorderBuffer completePrecedingMpdus; const auto& buffer = receiveBuffer->getBuffer(); - for (auto it : buffer) { // collects complete preceding MPDUs - auto sequenceNumber = it.first; - auto fragments = it.second; - if (SequenceNumberCyclic(sequenceNumber) < startingSequenceNumber) - if (ReceiveBuffer::isComplete(fragments)) - completePrecedingMpdus[sequenceNumber] = sortFragmentsByFragmentNumber(fragments); + auto currentStartingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); + for (int i = 0; i < receiveBuffer->getBufferSize(); i++) { + auto sequenceNumber = currentStartingSequenceNumber + i; + if (!(sequenceNumber < startingSequenceNumber)) + break; + auto it = buffer.find(sequenceNumber.get()); + if (it != buffer.end() && ReceiveBuffer::isComplete(it->second)) + completePrecedingMpdus.push_back(std::make_pair(sequenceNumber.get(), sortFragmentsByFragmentNumber(it->second))); } return completePrecedingMpdus; } @@ -176,18 +199,22 @@ bool BlockAckReordering::addMsduIfComplete(ReceiveBuffer *receiveBuffer, Reorder if (it != buffer.end()) { auto fragments = it->second; if (ReceiveBuffer::isComplete(fragments)) { - reorderBuffer[seqNum.get()] = sortFragmentsByFragmentNumber(fragments); + reorderBuffer.push_back(std::make_pair(seqNum.get(), sortFragmentsByFragmentNumber(fragments))); return true; } } return false; } -void BlockAckReordering::releaseReceiveBuffer(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, const ReorderBuffer& reorderBuffer) +void BlockAckReordering::releaseReceiveBuffer(ReceiveBuffer *receiveBuffer, const ReorderBuffer& reorderBuffer) { - for (auto it : reorderBuffer) { - auto sequenceNumber = it.first; - passedUp(agreement, receiveBuffer, SequenceNumberCyclic(sequenceNumber)); + // Detach all packets whose ownership is returned before stale-buffer cleanup. + for (const auto& entry : reorderBuffer) + receiveBuffer->removeFrame(SequenceNumberCyclic(entry.first)); + for (const auto& entry : reorderBuffer) { + auto sequenceNumber = entry.first; + receiveBuffer->setNextExpectedSequenceNumber(SequenceNumberCyclic(sequenceNumber) + 1); + receiveBuffer->dropFramesUntil(SequenceNumberCyclic(sequenceNumber)); } } @@ -244,7 +271,7 @@ std::vector BlockAckReordering::resetReceiveBuffer(Tid tid, MacAddress return frames; } -void BlockAckReordering::passedUp(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber) +void BlockAckReordering::passedUp(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber) { // Each time that the recipient passes an MSDU or A-MSDU for a Block Ack agreement up to the next MAC // process, the NextExpectedSequenceNumber for that Block Ack agreement is set to the sequence number of the @@ -252,7 +279,6 @@ void BlockAckReordering::passedUp(RecipientBlockAckAgreement *agreement, Receive receiveBuffer->setNextExpectedSequenceNumber(sequenceNumber + 1); receiveBuffer->dropFramesUntil(sequenceNumber); receiveBuffer->removeFrame(sequenceNumber); - agreement->getBlockAckRecord()->removeAckStates(sequenceNumber); } std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists(ReceiveBuffer *receiveBuffer) diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h index 8dd641c7439..ba2914f259b 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h @@ -24,7 +24,7 @@ class INET_API BlockAckReordering { public: typedef std::vector Fragments; - typedef std::map ReorderBuffer; + typedef std::vector> ReorderBuffer; // Values in ReorderBuffer results from processReceivedQoSFrame() and // processReceivedBlockAckReq() contain complete fragment vectors ordered @@ -41,8 +41,8 @@ class INET_API BlockAckReordering ReorderBuffer collectConsecutiveCompleteFollowingMpdus(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic startingSequenceNumber); std::vector getEarliestCompleteMsduOrAMsduIfExists(ReceiveBuffer *receiveBuffer); - void passedUp(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber); - void releaseReceiveBuffer(RecipientBlockAckAgreement *agreement, ReceiveBuffer *receiveBuffer, const ReorderBuffer& reorderBuffer); + void passedUp(ReceiveBuffer *receiveBuffer, SequenceNumberCyclic sequenceNumber); + void releaseReceiveBuffer(ReceiveBuffer *receiveBuffer, const ReorderBuffer& reorderBuffer); ReceiveBuffer *createReceiveBufferIfNecessary(RecipientBlockAckAgreement *agreement); bool addMsduIfComplete(ReceiveBuffer *receiveBuffer, ReorderBuffer& reorderBuffer, SequenceNumberCyclic seqNum); diff --git a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h index 11a5f26dfe6..237fc879d99 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementPolicy.h @@ -30,6 +30,7 @@ class INET_API IOriginatorBlockAckAgreementPolicy virtual simtime_t computeAddbaRetryBackoff() const = 0; virtual simtime_t getBlockAckTimeoutValue() const = 0; virtual bool isDelayedAckPolicySupported() const = 0; + virtual bool isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const = 0; virtual int getMaximumAllowedBufferSize() const = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index 656cb0d187d..db92c01cb73 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -23,7 +23,6 @@ void OriginatorQosAckPolicy::initialize(int stage) rateSelection = check_and_cast(getModuleByPath(par("rateSelectionModule"))); maxBlockAckPolicyFrameLength = par("maxBlockAckPolicyFrameLength"); blockAckReqThreshold = par("blockAckReqThreshold"); - assumePeerSupportsCompressedBlockAck = par("assumePeerSupportsCompressedBlockAck"); blockAckTimeout = par("blockAckTimeout"); ackTimeout = par("ackTimeout"); } @@ -64,16 +63,15 @@ SequenceNumberCyclic OriginatorQosAckPolicy::computeStartingSequenceNumber(const bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) const { - return isCompressedBlockAckReqNeeded(outstandingFrames, agreement, assumePeerSupportsCompressedBlockAck); + return isCompressedBlockAckReqNeeded(outstandingFrames, agreement); } -bool OriginatorQosAckPolicy::isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck) +bool OriginatorQosAckPolicy::isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) { // IEEE Std 802.11-2024, Table 11-8 and 10.25.6.1: use the compressed // variant only for an established immediate HT Block Ack agreement. - // Peer HT capability is not represented by the baseline agreement contract; - // the parameter is an explicit assumption supplied by the configuration. - if (!assumePeerSupportsCompressedBlockAck || agreement == nullptr || !agreement->getIsAddbaResponseReceived() || agreement->getIsDelayedBlockAckPolicySupported()) + // The agreement snapshots peer capability state when it is established. + if (agreement == nullptr || !agreement->getIsCompressedBlockAckSupported() || !agreement->getIsAddbaResponseReceived() || agreement->getIsDelayedBlockAckPolicySupported()) return false; bool hasMatchingOutstandingFrame = false; for (auto frame : outstandingFrames) { diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h index 7494db69eb1..b7ae8e918d1 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h @@ -22,7 +22,6 @@ class INET_API OriginatorQosAckPolicy : public ModeSetListener, public IOriginat IQosRateSelection *rateSelection = nullptr; int maxBlockAckPolicyFrameLength = -1; int blockAckReqThreshold = -1; - bool assumePeerSupportsCompressedBlockAck = false; simtime_t blockAckTimeout = -1; simtime_t ackTimeout = -1; @@ -34,7 +33,7 @@ class INET_API OriginatorQosAckPolicy : public ModeSetListener, public IOriginat virtual bool checkAgreementPolicy(const Ptr& header, OriginatorBlockAckAgreement *agreement) const; virtual std::map, std::vector> getOutstandingFramesPerAgreement(InProgressFrames *inProgressFrames, IOriginatorBlockAckAgreementHandler *blockAckAgreementHandler) const; virtual SequenceNumberCyclic computeStartingSequenceNumber(const std::vector& outstandingFrames) const; - static bool isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement, bool assumePeerSupportsCompressedBlockAck); + static bool isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement); public: virtual bool isAckNeeded(const Ptr& header) const override; virtual AckPolicy computeAckPolicy(Packet *packet, const Ptr& header, OriginatorBlockAckAgreement *agreement) const override; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned index 6575d5290b2..ea9835be26a 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.ned @@ -21,9 +21,6 @@ simple OriginatorQosAckPolicy extends SimpleModule like IOriginatorQosAckPolicy int blockAckReqThreshold = default(5); int maxBlockAckPolicyFrameLength @unit(B) = default(1000B); - // Set only for an HT-or-later local STA when peer capability management establishes that the peer supports Compressed Block Ack. - // This explicit assumption is needed because peer HT capabilities are not represented by the baseline agreement contract. - bool assumePeerSupportsCompressedBlockAck = default(false); double blockAckTimeout @unit(s) = default(-1s); double ackTimeout @unit(s) = default(-1s); diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index 51cbb532c64..2be55a927b4 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc @@ -75,12 +75,11 @@ bool RecipientQosAckPolicy::isBlockAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) { - // IEEE Std 802.11-2024, 9.3.1.7.2 and 10.25.6.5: an addressed, syntactically - // valid Compressed BlockAckReq elicits a Compressed BlockAck, including a null response. + // IEEE Std 802.11-2024, 10.25.6.4 and 10.25.6.5: a null response + // requires an established HT-immediate agreement whose partial state is absent. if (blockAckReq->getFragmentNumber() != 0) return false; - // A missing partial state still elicits the mandatory null compressed BA. - return agreement == nullptr || (agreement->getIsAddbaResponseSent() && !agreement->getIsDelayedBlockAckPolicySupported()); + return agreement != nullptr && agreement->getIsAddbaResponseSent() && !agreement->getIsDelayedBlockAckPolicySupported(); } // diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index ec315ddf66a..03792975517 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -152,7 +152,7 @@ std::vector RecipientQosMacDataService::dataFrameReceived(Packet *data return std::vector(); } BlockAckReordering::ReorderBuffer frames; - frames[dataHeader->getSequenceNumber().get()].push_back(dataPacket); + frames.push_back(std::make_pair(dataHeader->getSequenceNumber().get(), BlockAckReordering::Fragments({dataPacket}))); if (blockAckReordering && blockAckAgreementHandler) { Tid tid = dataHeader->getTid(); MacAddress originatorAddr = dataHeader->getTransmitterAddress(); diff --git a/tests/module/Ieee80211CompressedBlockAckRuntime.test b/tests/module/Ieee80211CompressedBlockAckRuntime.test index 8dde5158a5c..039df324eb4 100644 --- a/tests/module/Ieee80211CompressedBlockAckRuntime.test +++ b/tests/module/Ieee80211CompressedBlockAckRuntime.test @@ -22,9 +22,12 @@ abstract = false sim-time-limit = 1.05s **.opMode = "n(mixed-2.4Ghz)" -# The originator explicitly opts in based on its peer-capability assumption; -# the recipient requires no additional station-wide capability setting. -**.originatorAckPolicy.assumePeerSupportsCompressedBlockAck = true +**.originatorBlockAckAgreementPolicy.localCompressedBlockAckSupported = true +*.cliHost.wlan[*].address = "10:00:00:00:00:01" +*.srvHost.wlan[*].address = "10:00:00:00:00:02" +*.cliHost.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:00" +*.ap.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:01 10:00:00:00:00:02" +*.srvHost.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:00" **.cmdenv-log-level = info %contains: stdout diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index 0043bdb1605..b55dd363f54 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -74,6 +74,7 @@ class TestOriginatorPolicy : public IOriginatorBlockAckAgreementPolicy virtual simtime_t computeAddbaRetryBackoff() const override { return addbaRetryBackoff; } virtual simtime_t getBlockAckTimeoutValue() const override { return 0; } virtual bool isDelayedAckPolicySupported() const override { return false; } + virtual bool isPeerCompressedBlockAckSupported(const MacAddress&) const override { return false; } virtual int getMaximumAllowedBufferSize() const override { return 64; } }; @@ -103,7 +104,7 @@ class TestRecipientAckPolicy : public IRecipientAckPolicy, public IRecipientQosA 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; } + virtual simtime_t computeBlockAckDurationField(Packet *, const Ptr&) const override { return 0; } }; class TestCallback : public IProcedureCallback, public IBlockAckAgreementHandlerCallback @@ -2587,7 +2588,8 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; ASSERT(frames.empty()); } ASSERT(immediateFrames.size() == 1); - auto immediateFragments = immediateFrames.at(100); + ASSERT(immediateFrames.front().first == 100); + auto immediateFragments = immediateFrames.front().second; assertFragmentOrder(immediateFragments); reassembleOrderedFragments(immediateFragments, { 0x40, 0x41, 0x42, 0x43 }); @@ -2605,7 +2607,8 @@ RecipientBlockAckAgreement *replacementImmediateAgreement = nullptr; barRequest->setStartingSequenceNumber(SequenceNumberCyclic(201)); auto barFrames = barReordering.processReceivedBlockAckReq(&barAgreement, barRequest); ASSERT(barFrames.size() == 1); - auto barFragments = barFrames.at(201); + ASSERT(barFrames.front().first == 201); + auto barFragments = barFrames.front().second; assertFragmentOrder(barFragments); reassembleOrderedFragments(barFragments, { 0x50, 0x51, 0x52, 0x53 }); } diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index 8d34b512d95..e0bbc85340e 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -5,10 +5,13 @@ IEEE Std 802.11-2024, 9.3.1.7.2, 9.3.1.8.2, 10.25.6.1, and 10.25.6.5. %includes: #include "inet/common/packet/Packet.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" #include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" @@ -28,15 +31,50 @@ class TestRecipientBlockAckProcedure : public RecipientBlockAckProcedure class TestOriginatorBlockAckAgreementHandler : public OriginatorBlockAckAgreementHandler { public: + using OriginatorBlockAckAgreementHandler::createAgreement; using OriginatorBlockAckAgreementHandler::updateAgreement; }; +class TestOriginatorBlockAckAgreementPolicy : public OriginatorBlockAckAgreementPolicy +{ + public: + void configureCompressedBlockAckCapability(bool localSupport, std::initializer_list peerAddresses) + { + localCompressedBlockAckSupported = localSupport; + compressedBlockAckPeerAddresses = std::set(peerAddresses); + } +}; + +class TestBlockAckRecord : public BlockAckRecord +{ + public: + TestBlockAckRecord(MacAddress originatorAddress, Tid tid, SequenceNumberCyclic startingSequenceNumber) : + BlockAckRecord(originatorAddress, tid, startingSequenceNumber) + { + } + + bool hasRecordedAckState(SequenceNumberCyclic sequenceNumber, FragmentNumber fragmentNumber) const + { + return acknowledgmentState.find(SequenceControlField(sequenceNumber.get(), fragmentNumber)) != acknowledgmentState.end(); + } +}; + +class TestBlockAckReordering : public BlockAckReordering +{ + public: + ReceiveBuffer *getReceiveBuffer(Tid tid, const MacAddress& originatorAddress) const + { + auto it = receiveBuffers.find(std::make_pair(tid, originatorAddress)); + return it == receiveBuffers.end() ? nullptr : it->second; + } +}; + class TestOriginatorQosAckPolicy : public OriginatorQosAckPolicy { public: - static bool isCompressedRequestNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement, bool assumePeerSupport) + static bool isCompressedRequestNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) { - return isCompressedBlockAckReqNeeded(outstandingFrames, agreement, assumePeerSupport); + return isCompressedBlockAckReqNeeded(outstandingFrames, agreement); } }; @@ -91,6 +129,18 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag return new Packet("outstanding", header); } +static std::pair> makeReceivedQosFrame(SequenceNumber sequenceNumber, FragmentNumber fragmentNumber = 0, bool moreFragments = false, AckPolicy ackPolicy = BLOCK_ACK) +{ + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setAckPolicy(ackPolicy); + header->setTid(5); + header->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + header->setFragmentNumber(fragmentNumber); + header->setMoreFragments(moreFragments); + return std::make_pair(new Packet("received", header), header); +} + %activity: { auto request = makeCompressedBlockAckReq(); @@ -145,14 +195,81 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag { TestRecipientBlockAckProcedure procedure; RecipientBlockAckAgreement emptyAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + emptyAgreement.addbaResposneSent(); auto request = makeCompressedBlockAckReq(); request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &emptyAgreement)); auto response = dynamicPtrCast(procedure.buildBlockAck(request, &emptyAgreement)); for (int i = 0; i < 64; i++) ASSERT(!response->getBlockAckBitmap().getBit(i)); EV << "Empty recipient record produces a null compressed BA.\n"; } +{ + TestRecipientBlockAckProcedure procedure; + RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto request = makeShared(); + request->setTransmitterAddress(MacAddress("11:22:33:44:55:66")); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + for (int i = 0; i < 64; i++) + for (FragmentNumber fragmentNumber = 0; fragmentNumber < 16; fragmentNumber++) + ASSERT(!response->getBlockAckBitmap(i).getBit(fragmentNumber)); + agreement.getBlockAckRecord()->advanceStartingSequenceNumber(SequenceNumberCyclic(101)); + response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + for (FragmentNumber fragmentNumber = 0; fragmentNumber < 16; fragmentNumber++) + ASSERT(response->getBlockAckBitmap(0).getBit(fragmentNumber)); + for (int i = 1; i < 64; i++) + for (FragmentNumber fragmentNumber = 0; fragmentNumber < 16; fragmentNumber++) + ASSERT(!response->getBlockAckBitmap(i).getBit(fragmentNumber)); + EV << "Empty Basic BA distinguishes old and current-window MPDUs.\n"; +} + +{ + TestRecipientBlockAckProcedure procedure; + for (int startingSequenceNumber : {1985, 1986}) { + BlockAckReordering reordering; + RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(0), 64, SIMTIME_ZERO); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(startingSequenceNumber)); + reordering.processReceivedBlockAckReq(&agreement, request); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(startingSequenceNumber)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + for (int i = 0; i < 64; i++) + ASSERT(!response->getBlockAckBitmap().getBit(i)); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + reordering.processReceivedBlockAckReq(&agreement, request); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(startingSequenceNumber)); + } + + BlockAckReordering reordering; + RecipientBlockAckAgreement wrappedAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(4090), 64, SIMTIME_ZERO); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(5)); + reordering.processReceivedBlockAckReq(&wrappedAgreement, request); + ASSERT(wrappedAgreement.getStartingSequenceNumber() == SequenceNumberCyclic(5)); + ASSERT(wrappedAgreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(4095))); + ASSERT(!wrappedAgreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(5))); + EV << "BAR advances the authoritative receive window across boundaries and wrap.\n"; +} + +{ + TestBlockAckRecord record(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100)); + auto received102 = makeReceivedQosFrame(102); + auto received105 = makeReceivedQosFrame(105); + record.dataFrameReceived(received102.second, 64); + record.dataFrameReceived(received105.second, 64); + ASSERT(record.hasRecordedAckState(SequenceNumberCyclic(102), 0)); + ASSERT(record.hasRecordedAckState(SequenceNumberCyclic(105), 0)); + record.advanceStartingSequenceNumber(SequenceNumberCyclic(104)); + ASSERT(!record.hasRecordedAckState(SequenceNumberCyclic(102), 0)); + ASSERT(record.hasRecordedAckState(SequenceNumberCyclic(105), 0)); + delete received102.first; + delete received105.first; + EV << "Receive-window advancement erases old state and retains overlap.\n"; +} + { TestRecipientBlockAckProcedure procedure; RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(101), 64, SIMTIME_ZERO); @@ -163,7 +280,7 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag data->setTid(5); data->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); data->setFragmentNumber(0); - agreement.blockAckPolicyFrameReceived(data); + agreement.dataFrameReceived(data); } auto request = makeCompressedBlockAckReq(); request->setStartingSequenceNumber(SequenceNumberCyclic(101)); @@ -183,7 +300,7 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag data->setTid(5); data->setSequenceNumber(SequenceNumberCyclic(102)); data->setFragmentNumber(0); - agreement.blockAckPolicyFrameReceived(data); + agreement.dataFrameReceived(data); auto request = makeCompressedBlockAckReq(); request->setStartingSequenceNumber(SequenceNumberCyclic(100)); auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); @@ -193,6 +310,184 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag EV << "Compressed bitmap preserves leading receive-window holes.\n"; } +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestRecipientBlockAckProcedure procedure; + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(0), 64, SIMTIME_ZERO); + auto incomplete0 = makeReceivedQosFrame(0, 0, true); + ASSERT(reordering.processReceivedQoSFrame(&agreement, incomplete0.first, incomplete0.second).empty()); + auto received64 = makeReceivedQosFrame(64); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received64.first, received64.second).empty()); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + ASSERT(receiveBuffer != nullptr); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(1)); + ASSERT(receiveBuffer->getBuffer().find(0) == receiveBuffer->getBuffer().end()); + ASSERT(receiveBuffer->getBuffer().find(64) != receiveBuffer->getBuffer().end()); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(1)); + + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(1)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + for (int i = 0; i < 63; i++) + ASSERT(!response->getBlockAckBitmap().getBit(i)); + ASSERT(response->getBlockAckBitmap().getBit(63)); + EV << "SN64 advances a size-64 scoreboard and remains buffered across a gap.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(0), 64, SIMTIME_ZERO); + auto received1 = makeReceivedQosFrame(1); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received1.first, received1.second).empty()); + auto received2 = makeReceivedQosFrame(2); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received2.first, received2.second).empty()); + auto received65 = makeReceivedQosFrame(65); + auto framesToPassUp = reordering.processReceivedQoSFrame(&agreement, received65.first, received65.second); + ASSERT(framesToPassUp.size() == 2); + ASSERT(framesToPassUp[0].first == 1); + ASSERT(framesToPassUp[1].first == 2); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(3)); + ASSERT(receiveBuffer->getBuffer().find(65) != receiveBuffer->getBuffer().end()); + for (const auto& entry : framesToPassUp) + for (auto packet : entry.second) + delete packet; + EV << "Future MPDU returns displaced and consecutive complete MSDUs without losing gaps.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestRecipientBlockAckProcedure procedure; + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(0), 64, SIMTIME_ZERO); + auto normalAck64 = makeReceivedQosFrame(64, 0, false, NORMAL_ACK); + ASSERT(reordering.processReceivedQoSFrame(&agreement, normalAck64.first, normalAck64.second).empty()); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(1)); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(1)); + auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); + ASSERT(response->getBlockAckBitmap().getBit(63)); + EV << "Normal-Ack data advances and updates the Block Ack scoreboard.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto normalAck100 = makeReceivedQosFrame(100, 0, false, NORMAL_ACK); + auto framesToPassUp = reordering.processReceivedQoSFrame(&agreement, normalAck100.first, normalAck100.second); + ASSERT(framesToPassUp.size() == 1 && framesToPassUp[0].first == 100); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(100)); + ASSERT(agreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(100))); + delete framesToPassUp[0].second[0]; + EV << "Upward delivery advances only the reorder cursor, not WinStartR.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(4090), 64, SIMTIME_ZERO); + auto received4095 = makeReceivedQosFrame(4095); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received4095.first, received4095.second).empty()); + auto received0 = makeReceivedQosFrame(0); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received0.first, received0.second).empty()); + auto received62 = makeReceivedQosFrame(62); + auto framesToPassUp = reordering.processReceivedQoSFrame(&agreement, received62.first, received62.second); + ASSERT(framesToPassUp.size() == 2); + ASSERT(framesToPassUp[0].first == 4095); + ASSERT(framesToPassUp[1].first == 0); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(1)); + ASSERT(receiveBuffer->getBuffer().find(62) != receiveBuffer->getBuffer().end()); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(4095)); + for (const auto& entry : framesToPassUp) + for (auto packet : entry.second) + delete packet; + EV << "Data-window slide preserves cyclic 4095-to-0 delivery order.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(4090), 64, SIMTIME_ZERO); + auto received4095 = makeReceivedQosFrame(4095); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received4095.first, received4095.second).empty()); + auto received0 = makeReceivedQosFrame(0); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received0.first, received0.second).empty()); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(1)); + auto framesToPassUp = reordering.processReceivedBlockAckReq(&agreement, request); + ASSERT(framesToPassUp.size() == 2); + ASSERT(framesToPassUp[0].first == 4095); + ASSERT(framesToPassUp[1].first == 0); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(1)); + for (const auto& entry : framesToPassUp) + for (auto packet : entry.second) + delete packet; + EV << "BAR release preserves cyclic order without advancing WinStartR past its SSN.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto received101 = makeReceivedQosFrame(101); + auto firstResult = reordering.processReceivedQoSFrame(&agreement, received101.first, received101.second); + ASSERT(firstResult.empty()); + auto duplicate101 = makeReceivedQosFrame(101); + auto duplicateResult = reordering.processReceivedQoSFrame(&agreement, duplicate101.first, duplicate101.second); + ASSERT(duplicateResult.empty()); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + ASSERT(receiveBuffer->getBuffer().at(101).size() == 1); + ASSERT(receiveBuffer->getBuffer().at(101)[0] == received101.first); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(102)); + auto framesToPassUp = reordering.processReceivedBlockAckReq(&agreement, request); + ASSERT(framesToPassUp.size() == 1 && framesToPassUp[0].first == 101); + ASSERT(framesToPassUp[0].second[0] == received101.first); + delete framesToPassUp[0].second[0]; + EV << "Duplicate insertion preserves ownership of the original buffered MPDU.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto incomplete101 = makeReceivedQosFrame(101, 0, true); + auto insertionResult = reordering.processReceivedQoSFrame(&agreement, incomplete101.first, incomplete101.second); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(102)); + auto framesToPassUp = reordering.processReceivedBlockAckReq(&agreement, request); + if (!insertionResult.empty() || !framesToPassUp.empty() || !receiveBuffer->getBuffer().empty() || receiveBuffer->getLength() != 0 || + receiveBuffer->getNextExpectedSequenceNumber() != SequenceNumberCyclic(102) || agreement.getStartingSequenceNumber() != SequenceNumberCyclic(102)) + throw cRuntimeError("BAR did not discard an incomplete preceding MPDU or advance both receive-window starts to its SSN"); + EV << "BAR discards incomplete preceding MPDUs and advances the reorder cursor.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); + auto complete101 = makeReceivedQosFrame(101); + auto completeInsertionResult = reordering.processReceivedQoSFrame(&agreement, complete101.first, complete101.second); + auto incomplete102 = makeReceivedQosFrame(102, 0, true); + auto incompleteInsertionResult = reordering.processReceivedQoSFrame(&agreement, incomplete102.first, incomplete102.second); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + auto request = makeCompressedBlockAckReq(); + request->setStartingSequenceNumber(SequenceNumberCyclic(103)); + auto framesToPassUp = reordering.processReceivedBlockAckReq(&agreement, request); + if (!completeInsertionResult.empty() || !incompleteInsertionResult.empty() || framesToPassUp.size() != 1 || framesToPassUp[0].first != 101 || + framesToPassUp[0].second.size() != 1 || framesToPassUp[0].second[0] != complete101.first || !receiveBuffer->getBuffer().empty() || + receiveBuffer->getLength() != 0 || receiveBuffer->getNextExpectedSequenceNumber() != SequenceNumberCyclic(103) || + agreement.getStartingSequenceNumber() != SequenceNumberCyclic(103)) + throw cRuntimeError("BAR did not return a complete preceding MPDU and discard the incomplete gap that followed it"); + delete framesToPassUp[0].second[0]; + EV << "BAR releases complete preceding MPDUs and discards a following incomplete gap.\n"; +} + { TestRecipientBlockAckProcedure procedure; RecipientBlockAckAgreement agreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(4095), 64, SIMTIME_ZERO); @@ -203,7 +498,7 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag data->setTid(5); data->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); data->setFragmentNumber(0); - agreement.blockAckPolicyFrameReceived(data); + agreement.dataFrameReceived(data); } auto request = makeCompressedBlockAckReq(); request->setStartingSequenceNumber(SequenceNumberCyclic(4095)); @@ -223,7 +518,7 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag data->setTid(5); data->setSequenceNumber(SequenceNumberCyclic(1)); data->setFragmentNumber(0); - agreement.blockAckPolicyFrameReceived(data); + agreement.dataFrameReceived(data); auto request = makeCompressedBlockAckReq(); request->setStartingSequenceNumber(SequenceNumberCyclic(4095)); auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); @@ -235,7 +530,7 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag { auto request = makeCompressedBlockAckReq(); - ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr)); + ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr)); request->setFragmentNumber(1); ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, nullptr)); request->setFragmentNumber(0); @@ -249,23 +544,70 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag EV << "Recipient fragment and agreement-policy gates passed.\n"; } +{ + MacAddress listedPeer("10:20:30:40:50:60"); + MacAddress unlistedPeer("10:20:30:40:50:61"); + MacAddress positivePeer("10:20:30:40:50:62"); + TestOriginatorBlockAckAgreementPolicy localDisabledPolicy; + localDisabledPolicy.configureCompressedBlockAckCapability(false, {listedPeer}); + TestOriginatorBlockAckAgreementPolicy localEnabledPolicy; + localEnabledPolicy.configureCompressedBlockAckCapability(true, {listedPeer, positivePeer}); + ASSERT(!localDisabledPolicy.isPeerCompressedBlockAckSupported(listedPeer)); + ASSERT(!localEnabledPolicy.isPeerCompressedBlockAckSupported(unlistedPeer)); + ASSERT(localEnabledPolicy.isPeerCompressedBlockAckSupported(listedPeer)); + + TestOriginatorBlockAckAgreementHandler handler; + auto makeAddbaRequest = [](const MacAddress& receiverAddress) { + auto request = makeShared(); + request->setReceiverAddress(receiverAddress); + request->setTid(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(100)); + request->setBufferSize(64); + request->setBlockAckPolicy(true); + return request; + }; + handler.createAgreement(makeAddbaRequest(listedPeer), &localDisabledPolicy); + handler.createAgreement(makeAddbaRequest(unlistedPeer), &localEnabledPolicy); + handler.createAgreement(makeAddbaRequest(positivePeer), &localEnabledPolicy); + ASSERT(!handler.getAgreement(listedPeer, 5)->getIsCompressedBlockAckSupported()); + ASSERT(!handler.getAgreement(unlistedPeer, 5)->getIsCompressedBlockAckSupported()); + ASSERT(handler.getAgreement(positivePeer, 5)->getIsCompressedBlockAckSupported()); + + auto response = makeShared(); + response->setTransmitterAddress(listedPeer); + response->setBlockAckPolicy(true); + response->setBufferSize(64); + response->setBlockAckTimeoutValue(SIMTIME_ZERO); + handler.updateAgreement(handler.getAgreement(listedPeer, 5), response, &localEnabledPolicy); + ASSERT(handler.getAgreement(listedPeer, 5)->getIsCompressedBlockAckSupported()); + EV << "Local and per-peer capability gates are snapshotted by agreements.\n"; +} + { MacAddress receiverAddress("10:20:30:40:50:60"); OriginatorBlockAckAgreement immediateAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, false); immediateAgreement.setIsAddbaResponseReceived(true); std::vector outstandingFrames { makeOutstandingQosFrame(receiverAddress, 5) }; - ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, false)); - ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, nullptr, true)); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement)); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, nullptr)); + immediateAgreement.setIsCompressedBlockAckSupported(true); OriginatorBlockAckAgreement delayedAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, true); delayedAgreement.setIsAddbaResponseReceived(true); - ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &delayedAgreement, true)); - ASSERT(TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, true)); + delayedAgreement.setIsCompressedBlockAckSupported(true); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &delayedAgreement)); + ASSERT(TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement)); + MacAddress legacyReceiverAddress("10:20:30:40:50:61"); + OriginatorBlockAckAgreement legacyAgreement(legacyReceiverAddress, 5, SequenceNumberCyclic(100), 64, false, false); + legacyAgreement.setIsAddbaResponseReceived(true); + std::vector legacyOutstandingFrames { makeOutstandingQosFrame(legacyReceiverAddress, 5) }; + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(legacyOutstandingFrames, &legacyAgreement)); + delete legacyOutstandingFrames[0]; delete outstandingFrames[0]; outstandingFrames = { makeOutstandingQosFrame(receiverAddress, 5, 1) }; - ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, true)); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement)); delete outstandingFrames[0]; outstandingFrames = { makeOutstandingQosFrame(receiverAddress, 5, 0, true) }; - ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement, true)); + ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement)); delete outstandingFrames[0]; EV << "Originator capability, agreement, and fragmentation gates passed.\n"; } @@ -301,12 +643,15 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag { TestOriginatorBlockAckAgreementHandler handler; + TestOriginatorBlockAckAgreementPolicy policy; + policy.configureCompressedBlockAckCapability(false, {}); OriginatorBlockAckAgreement agreement(MacAddress("10:20:30:40:50:60"), 5, SequenceNumberCyclic(100), 64, false, false); auto delayedResponse = makeShared(); delayedResponse->setBlockAckPolicy(false); delayedResponse->setBufferSize(64); delayedResponse->setBlockAckTimeoutValue(SIMTIME_ZERO); - handler.updateAgreement(&agreement, delayedResponse); + delayedResponse->setTransmitterAddress(MacAddress("10:20:30:40:50:60")); + handler.updateAgreement(&agreement, delayedResponse, &policy); ASSERT(agreement.getIsAddbaResponseReceived()); ASSERT(agreement.getIsDelayedBlockAckPolicySupported()); EV << "Accepted delayed agreement is retained for Basic BAR fallback.\n"; @@ -342,11 +687,24 @@ Compressed BAR encoding and round-trip passed. Compressed BA encoding and round-trip passed. Missing recipient state produces a null compressed BA. Empty recipient record produces a null compressed BA. +Empty Basic BA distinguishes old and current-window MPDUs. +BAR advances the authoritative receive window across boundaries and wrap. +Receive-window advancement erases old state and retains overlap. Established agreement produces the expected 64-bit bitmap. Compressed bitmap preserves leading receive-window holes. +SN64 advances a size-64 scoreboard and remains buffered across a gap. +Future MPDU returns displaced and consecutive complete MSDUs without losing gaps. +Normal-Ack data advances and updates the Block Ack scoreboard. +Upward delivery advances only the reorder cursor, not WinStartR. +Data-window slide preserves cyclic 4095-to-0 delivery order. +BAR release preserves cyclic order without advancing WinStartR past its SSN. +Duplicate insertion preserves ownership of the original buffered MPDU. +BAR discards incomplete preceding MPDUs and advances the reorder cursor. +BAR releases complete preceding MPDUs and discards a following incomplete gap. Compressed bitmap wraps from sequence 4095 to 0. Compressed bitmap preserves leading holes across sequence wrap. Recipient fragment and agreement-policy gates passed. +Local and per-peer capability gates are snapshotted by agreements. Originator capability, agreement, and fragmentation gates passed. Legacy Basic BAR and BA remain byte-exact. Accepted delayed agreement is retained for Basic BAR fallback. From ef729f422b7da976f65dcb71e88af41d71aa6c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 16 Aug 2026 13:03:04 +0200 Subject: [PATCH 46/56] fix(ieee80211): make Block Ack receive processing transactional Update the Block Ack scoreboard for every related received QoS Data MPDU, including frames using Normal Ack, independently of reorder-buffer admission. Handle the receive-window cases explicitly: ignore old sequence numbers, record in-window MPDUs, and advance WinStartR before recording MPDUs beyond WinEndR. This behavior deliberately applies to both Basic and Compressed Block Ack agreements, as required by IEEE 802.11-2024 sections 10.25.6.3 and 10.25.6.4. Make far-ahead reorder-window movement transactional. Validate and insert the incoming MPDU against the proposed WinStartB before releasing displaced MSDUs or changing NextExpectedSequenceNumber. If admission fails, discard only the incoming packet and preserve the reorder window and its buffered frames, while retaining the independently required scoreboard update. Calculate receive-buffer capacity after accounting for entries displaced by the proposed window. Count each fragment as one buffer slot, allow an advancing MPDU to reuse slots that will be reclaimed, and remove incomplete stale entries only after successful admission. Preserve delivery of complete displaced and consecutive MSDUs, including across the 4095-to-0 sequence number boundary. Centralize supported one-TID BlockAckReq classification and extraction of the variant, TID, and starting sequence number. Use the shared classifier in HCF, RecipientQosMacDataService, and BlockAckReordering so only Basic and Compressed requests reach the implemented paths. Multi-TID requests remain unsupported and follow the existing rejection behavior. Extend the focused Block Ack tests to cover old scoreboard inputs, Normal-Ack behavior with Basic Block Ack, fragment-full admission failure, successful slot reclamation across sequence wrap, real HCF dispatch through a procedure spy, and consistent Basic, Compressed, and Multi-TID request classification. --- .../ieee80211/mac/blockack/BlockAckRecord.cc | 8 +- .../mac/blockack/OneTidBlockAckReqVariant.h | 46 ++++++ .../blockackreordering/BlockAckReordering.cc | 108 +++++++------ .../mac/blockackreordering/ReceiveBuffer.cc | 89 +++++++---- .../mac/blockackreordering/ReceiveBuffer.h | 3 + .../ieee80211/mac/coordinationfunction/Hcf.cc | 7 +- .../recipient/RecipientQosMacDataService.cc | 16 +- tests/unit/Ieee80211CompressedBlockAck_1.test | 144 +++++++++++++++++- 8 files changed, 313 insertions(+), 108 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index 14a21493581..e212ed19537 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -23,8 +23,12 @@ void BlockAckRecord::dataFrameReceived(const Ptr& hea { SequenceNumberCyclic sequenceNumber = header->getSequenceNumber(); FragmentNumber fragmentNumber = header->getFragmentNumber(); - // IEEE Std 802.11-2024, 10.25.6.3(b) and 10.25.6.4(c): a related - // MPDU beyond WinEndR advances the receive window before its bit is set. + // IEEE Std 802.11-2024, 10.25.6.3(b), case 3: an old related MPDU + // does not change the Block Ack record. + if (!(startingSequenceNumber <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048)) + return; + // Cases 1 and 2: record an in-window MPDU, or advance WinStartR + // before recording an MPDU beyond WinEndR (also see 10.25.6.4(c)). if (startingSequenceNumber + windowSize <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048) advanceStartingSequenceNumber(sequenceNumber - windowSize + 1); acknowledgmentState[SequenceControlField(sequenceNumber.get(), fragmentNumber)] = true; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h new file mode 100644 index 00000000000..0d158b365cf --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h @@ -0,0 +1,46 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_ONETIDBLOCKACKREQVARIANT_H +#define __INET_ONETIDBLOCKACKREQVARIANT_H + +#include + +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" + +namespace inet { +namespace ieee80211 { + +enum class OneTidBlockAckReqVariant +{ + BASIC, + COMPRESSED, +}; + +struct OneTidBlockAckReqDetails +{ + Ptr blockAckReq; + OneTidBlockAckReqVariant variant; + Tid tid; + SequenceNumberCyclic startingSequenceNumber; +}; + +inline std::optional getOneTidBlockAckReqDetails(const Ptr& header) +{ + if (auto basicBlockAckReq = dynamicPtrCast(header)) + return OneTidBlockAckReqDetails { basicBlockAckReq, OneTidBlockAckReqVariant::BASIC, + static_cast(basicBlockAckReq->getTidInfo()), basicBlockAckReq->getStartingSequenceNumber() }; + else if (auto compressedBlockAckReq = dynamicPtrCast(header)) + return OneTidBlockAckReqDetails { compressedBlockAckReq, OneTidBlockAckReqVariant::COMPRESSED, + static_cast(compressedBlockAckReq->getTidInfo()), compressedBlockAckReq->getStartingSequenceNumber() }; + else + return std::nullopt; +} + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index 708efac89b8..3728fe3cb89 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -9,6 +9,7 @@ #include +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" namespace inet { @@ -36,10 +37,31 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived auto sequenceNumber = dataHeader->getSequenceNumber(); auto startingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); bool advancesWindow = startingSequenceNumber + receiveBuffer->getBufferSize() <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048; + // IEEE Std 802.11-2024, 10.25.6.3 and 10.25.6.4: update the + // scoreboard for every related Data frame, independently of reorder storage. + agreement->dataFrameReceived(dataHeader); + SequenceNumberCyclic newStartingSequenceNumber; + if (advancesWindow) { + // IEEE Std 802.11-2024, 10.25.6.6.2.1(b): store the future MPDU + // before moving WinStartB and releasing complete displaced MSDUs. + newStartingSequenceNumber = sequenceNumber - receiveBuffer->getBufferSize() + 1; + } + // 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. Use the proposed window for a future MPDU so admission + // can reclaim entries that will be displaced. + auto insertionResult = advancesWindow ? + receiveBuffer->insertFrameWithResult(dataPacket, dataHeader, newStartingSequenceNumber) : + receiveBuffer->insertFrameWithResult(dataPacket, dataHeader); + if (insertionResult != ReceiveBuffer::FrameInsertionResult::INSERTED) { + if (insertionResult == ReceiveBuffer::FrameInsertionResult::REJECTED_EXPIRED) + result.tombstonedFragments.push_back(dataPacket); + else + delete dataPacket; + return result; + } if (advancesWindow) { - // IEEE Std 802.11-2024, 10.25.6.6.2.1(b): move WinStartB so the - // future MPDU fits, preserving complete displaced MSDUs for delivery. - auto newStartingSequenceNumber = sequenceNumber - receiveBuffer->getBufferSize() + 1; framesToPassUp = collectCompletePrecedingMpdus(receiveBuffer, newStartingSequenceNumber); for (const auto& entry : framesToPassUp) receiveBuffer->removeFrame(SequenceNumberCyclic(entry.first)); @@ -47,52 +69,33 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived receiveBuffer->dropFramesUntil(newStartingSequenceNumber); receiveBuffer->setNextExpectedSequenceNumber(newStartingSequenceNumber); } - // 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. - auto insertionResult = receiveBuffer->insertFrameWithResult(dataPacket, dataHeader); - if (insertionResult == ReceiveBuffer::FrameInsertionResult::INSERTED) { - agreement->dataFrameReceived(dataHeader); - if (advancesWindow) { - auto consecutiveCompleteMpdus = collectConsecutiveCompleteFollowingMpdus(receiveBuffer, receiveBuffer->getNextExpectedSequenceNumber()); - releaseReceiveBuffer(receiveBuffer, consecutiveCompleteMpdus); - framesToPassUp.insert(framesToPassUp.end(), consecutiveCompleteMpdus.begin(), consecutiveCompleteMpdus.end()); - result.frames = framesToPassUp; + if (advancesWindow) { + auto consecutiveCompleteMpdus = collectConsecutiveCompleteFollowingMpdus(receiveBuffer, receiveBuffer->getNextExpectedSequenceNumber()); + releaseReceiveBuffer(receiveBuffer, consecutiveCompleteMpdus); + framesToPassUp.insert(framesToPassUp.end(), consecutiveCompleteMpdus.begin(), consecutiveCompleteMpdus.end()); + result.frames = framesToPassUp; + return result; + } + auto earliestCompleteMsduOrAMsdu = getEarliestCompleteMsduOrAMsduIfExists(receiveBuffer); + if (earliestCompleteMsduOrAMsdu.size() > 0) { + auto earliestSequenceNumber = earliestCompleteMsduOrAMsdu.at(0)->peekAtFront()->getSequenceNumber(); + // If, after an MPDU is received, the receive buffer is full, the complete MSDU or A-MSDU with the earliest + // sequence number shall be passed up to the next MAC process. + if (receiveBuffer->isFull()) { + passedUp(receiveBuffer, earliestSequenceNumber); + result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); return result; } - auto earliestCompleteMsduOrAMsdu = getEarliestCompleteMsduOrAMsduIfExists(receiveBuffer); - if (earliestCompleteMsduOrAMsdu.size() > 0) { - auto earliestSequenceNumber = earliestCompleteMsduOrAMsdu.at(0)->peekAtFront()->getSequenceNumber(); - // If, after an MPDU is received, the receive buffer is full, the complete MSDU or A-MSDU with the earliest - // sequence number shall be passed up to the next MAC process. - if (receiveBuffer->isFull()) { - passedUp(receiveBuffer, earliestSequenceNumber); - 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(receiveBuffer, earliestSequenceNumber); - result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); - return result; - } - } - } - 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. - agreement->dataFrameReceived(dataHeader); - result.tombstonedFragments.push_back(dataPacket); + // 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(receiveBuffer, earliestSequenceNumber); + result.frames = ReorderBuffer({ std::make_pair(earliestSequenceNumber.get(), Fragments(earliestCompleteMsduOrAMsdu)) }); return result; } - delete dataPacket; } + result.frames = framesToPassUp; return result; } @@ -111,23 +114,14 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedBlockAckReq { // The originator shall use the Block Ack starting sequence control to signal the first MPDU in the block for // which an acknowledgment is expected. - SequenceNumberCyclic startingSequenceNumber; - Tid tid = -1; - if (auto basicReq = dynamicPtrCast(blockAckReq)) { - tid = basicReq->getTidInfo(); - startingSequenceNumber = basicReq->getStartingSequenceNumber(); - } - else if (auto compressedReq = dynamicPtrCast(blockAckReq)) { - tid = compressedReq->getTidInfo(); - startingSequenceNumber = compressedReq->getStartingSequenceNumber(); - } - else { + auto blockAckReqDetails = getOneTidBlockAckReqDetails(blockAckReq); + if (!blockAckReqDetails) throw cRuntimeError("Multi-Tid BlockAckReq is currently an unimplemented feature"); - } + auto startingSequenceNumber = blockAckReqDetails->startingSequenceNumber; // IEEE Std 802.11-2024, 10.25.6.3-10.25.6.5: adjust WinStartR // from the BAR before generating the response, even without a receive buffer. agreement->getBlockAckRecord()->advanceStartingSequenceNumber(startingSequenceNumber); - auto id = std::make_pair(tid, blockAckReq->getTransmitterAddress()); + auto id = std::make_pair(blockAckReqDetails->tid, blockAckReq->getTransmitterAddress()); auto it = receiveBuffers.find(id); if (it != receiveBuffers.end()) { ReceiveBuffer *receiveBuffer = it->second; diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc index f886058cf5c..1eb73de5725 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc @@ -59,7 +59,38 @@ void ReceiveBuffer::pruneExpiredFragmentSequences() // 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::canInsertFrame(const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber) const +{ + auto sequenceNumber = dataHeader->getSequenceNumber(); + auto fragmentNumber = dataHeader->getFragmentNumber(); + if (!(nextExpectedSequenceNumber <= sequenceNumber && sequenceNumber < nextExpectedSequenceNumber + bufferSize)) + return false; + int retainedLength = length; + for (const auto& entry : buffer) { + if (SequenceNumberCyclic(entry.first) < nextExpectedSequenceNumber) + retainedLength -= entry.second.size(); + } + // IEEE Std 802.11-2024, 9.4.1.13, footnote 26: each fragment + // occupies one receive-buffer slot. + if (retainedLength >= bufferSize) + return false; + auto it = buffer.find(sequenceNumber.get()); + if (it != buffer.end()) { + for (auto fragment : it->second) { + const auto& fragmentHeader = fragment->peekAtFront(); + if (fragmentHeader->getSequenceNumber() == sequenceNumber && fragmentHeader->getFragmentNumber() == fragmentNumber) + return false; + } + } + return true; +} + ReceiveBuffer::FrameInsertionResult ReceiveBuffer::insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader) +{ + return insertFrameWithResult(dataPacket, dataHeader, nextExpectedSequenceNumber); +} + +ReceiveBuffer::FrameInsertionResult ReceiveBuffer::insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber) { auto sequenceNumber = dataHeader->getSequenceNumber(); auto fragmentNumber = dataHeader->getFragmentNumber(); @@ -67,36 +98,38 @@ ReceiveBuffer::FrameInsertionResult ReceiveBuffer::insertFrameWithResult(Packet 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) { - auto it = buffer.find(sequenceNumber.get()); - if (it != buffer.end()) { - auto& fragments = it->second; - // TODO efficiency - for (auto fragment : fragments) { - const auto& fragmentHeader = fragment->peekAtFront(); - if (fragmentHeader->getSequenceNumber() == sequenceNumber && fragmentHeader->getFragmentNumber() == fragmentNumber) - return FrameInsertionResult::REJECTED; - } - fragments.push_back(dataPacket); - } - else { - buffer[sequenceNumber.get()].push_back(dataPacket); - bufferEntries[sequenceNumber.get()] = { simTime(), isFragmented, false }; + if (!canInsertFrame(dataHeader, nextExpectedSequenceNumber)) + return FrameInsertionResult::REJECTED; + auto it = buffer.find(sequenceNumber.get()); + if (it != buffer.end()) { + auto& fragments = it->second; + // TODO efficiency + for (auto fragment : fragments) { + const auto& fragmentHeader = fragment->peekAtFront(); + if (fragmentHeader->getSequenceNumber() == sequenceNumber && fragmentHeader->getFragmentNumber() == fragmentNumber) + return FrameInsertionResult::REJECTED; } - // The total number of frames that can be sent depends on the total - // number of MPDUs in all the outstanding MSDUs. - length++; - // 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; + fragments.push_back(dataPacket); + } + else { + buffer[sequenceNumber.get()].push_back(dataPacket); + bufferEntries[sequenceNumber.get()] = { simTime(), isFragmented, false }; } - return FrameInsertionResult::REJECTED; + // The total number of frames that can be sent depends on the total + // number of MPDUs in all the outstanding MSDUs. + length++; + // 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; +} + +bool ReceiveBuffer::insertFrame(Packet *dataPacket, const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber) +{ + return insertFrameWithResult(dataPacket, dataHeader, nextExpectedSequenceNumber) == FrameInsertionResult::INSERTED; } void ReceiveBuffer::dropFramesUntil(SequenceNumberCyclic sequenceNumber) diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h index f6af968c519..f9db251efcf 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.h @@ -48,13 +48,16 @@ class INET_API ReceiveBuffer SequenceNumberCyclic nextExpectedSequenceNumber; void pruneExpiredFragmentSequences(); + bool canInsertFrame(const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber) const; public: ReceiveBuffer(int bufferSize, SequenceNumberCyclic nextExpectedSequenceNumber); virtual ~ReceiveBuffer(); FrameInsertionResult insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader); + FrameInsertionResult insertFrameWithResult(Packet *dataPacket, const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber); bool insertFrame(Packet *dataPacket, const Ptr& dataHeader) { return insertFrameWithResult(dataPacket, dataHeader) == FrameInsertionResult::INSERTED; } + bool insertFrame(Packet *dataPacket, const Ptr& dataHeader, SequenceNumberCyclic nextExpectedSequenceNumber); void dropFramesUntil(SequenceNumberCyclic sequenceNumber); void removeFrame(SequenceNumberCyclic sequenceNumber); Fragments extractFrames(); diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index d3f061639f4..9cad5821b04 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -17,6 +17,7 @@ #include "inet/linklayer/ieee80211/mac/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/OneTidBlockAckReqVariant.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" @@ -798,11 +799,11 @@ void Hcf::recipientProcessReceivedControlFrame(Packet *packet, const Ptr(header)) ctsProcedure->processReceivedRts(packet, rtsFrame, ctsPolicy, this); - else if (auto blockAckRequest = dynamicPtrCast(header)) { + else if (auto blockAckReqDetails = getOneTidBlockAckReqDetails(header)) { if (recipientBlockAckAgreementHandler) - recipientBlockAckAgreementHandler->blockAckReqReceived(blockAckRequest, this); + recipientBlockAckAgreementHandler->blockAckReqReceived(blockAckReqDetails->blockAckReq, this); if (recipientBlockAckProcedure) - recipientBlockAckProcedure->processReceivedBlockAckReq(packet, blockAckRequest, recipientAckPolicy, recipientBlockAckAgreementHandler, this); + recipientBlockAckProcedure->processReceivedBlockAckReq(packet, blockAckReqDetails->blockAckReq, recipientAckPolicy, recipientBlockAckAgreementHandler, this); } else if (dynamicPtrCast(header)) EV_WARN << "ACK frame received after timeout, ignoring it.\n"; // drop it, it is an ACK frame that is received after the ACKTimeout diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 03792975517..6cef6428f18 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -10,6 +10,7 @@ #include "inet/common/Simsignals.h" #include "inet/linklayer/ieee80211/mac/aggregation/MpduDeaggregation.h" #include "inet/linklayer/ieee80211/mac/aggregation/MsduDeaggregation.h" +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/duplicateremoval/QosDuplicateRemoval.h" #include "inet/linklayer/ieee80211/mac/fragmentation/BasicReassembly.h" @@ -249,20 +250,13 @@ std::vector RecipientQosMacDataService::controlFrameReceived(Packet *c { Enter_Method("controlFrameReceived"); expireReceiveLifetime(); - if (auto blockAckReq = dynamicPtrCast(controlHeader)) { + if (auto blockAckReqDetails = getOneTidBlockAckReqDetails(controlHeader)) { BlockAckReordering::ReorderBuffer frames; if (blockAckReordering) { - Tid tid = -1; - if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) - tid = basicBlockAckReq->getTidInfo(); - else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) - tid = compressedBlockAckReq->getTidInfo(); - else - return std::vector(); - MacAddress originatorAddr = blockAckReq->getTransmitterAddress(); - RecipientBlockAckAgreement *agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(tid, originatorAddr); + MacAddress originatorAddr = blockAckReqDetails->blockAckReq->getTransmitterAddress(); + RecipientBlockAckAgreement *agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(blockAckReqDetails->tid, originatorAddr); if (agreement) - frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReq); + frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReqDetails->blockAckReq); else { scheduleReceiveLifetimeTimer(); return std::vector(); diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index e0bbc85340e..d43376db21e 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -6,12 +6,14 @@ IEEE Std 802.11-2024, 9.3.1.7.2, 9.3.1.8.2, 10.25.6.1, and 10.25.6.5. #include "inet/common/packet/Packet.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h" +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" #include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" @@ -28,6 +30,37 @@ class TestRecipientBlockAckProcedure : public RecipientBlockAckProcedure using RecipientBlockAckProcedure::buildBlockAck; }; +class SpyRecipientBlockAckProcedure : public IRecipientBlockAckProcedure +{ + public: + std::vector variants; + std::vector tids; + + virtual void processReceivedBlockAckReq(Packet *packet, const Ptr& blockAckReq, IRecipientQosAckPolicy *ackPolicy, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler, IProcedureCallback *callback) override + { + auto blockAckReqDetails = getOneTidBlockAckReqDetails(blockAckReq); + ASSERT(blockAckReqDetails.has_value()); + variants.push_back(blockAckReqDetails->variant); + tids.push_back(blockAckReqDetails->tid); + } + + virtual void processTransmittedBlockAck(const Ptr& blockAck) override {} +}; + +class TestHcf : public Hcf +{ + public: + void setRecipientBlockAckProcedure(IRecipientBlockAckProcedure *procedure) + { + recipientBlockAckProcedure = procedure; + } + + void processControlHeader(const Ptr& header) + { + recipientProcessReceivedControlFrame(nullptr, header); + } +}; + class TestOriginatorBlockAckAgreementHandler : public OriginatorBlockAckAgreementHandler { public: @@ -181,6 +214,39 @@ static std::pair> makeReceivedQosFrame(Sequen EV << "Compressed BA encoding and round-trip passed.\n"; } +{ + TestHcf hcf; + auto spy = new SpyRecipientBlockAckProcedure(); + hcf.setRecipientBlockAckProcedure(spy); + auto basicRequest = makeShared(); + basicRequest->setTidInfo(3); + basicRequest->setStartingSequenceNumber(SequenceNumberCyclic(4095)); + auto compressedRequest = makeShared(); + compressedRequest->setTidInfo(5); + compressedRequest->setStartingSequenceNumber(SequenceNumberCyclic(7)); + auto basicDetails = getOneTidBlockAckReqDetails(basicRequest); + auto compressedDetails = getOneTidBlockAckReqDetails(compressedRequest); + ASSERT(basicDetails && basicDetails->variant == OneTidBlockAckReqVariant::BASIC && basicDetails->tid == 3 && basicDetails->startingSequenceNumber == SequenceNumberCyclic(4095)); + ASSERT(compressedDetails && compressedDetails->variant == OneTidBlockAckReqVariant::COMPRESSED && compressedDetails->tid == 5 && compressedDetails->startingSequenceNumber == SequenceNumberCyclic(7)); + auto multiTidRequest = makeShared(); + ASSERT(!getOneTidBlockAckReqDetails(multiTidRequest)); + ASSERT(!getOneTidBlockAckReqDetails(makeShared())); + hcf.processControlHeader(basicRequest); + hcf.processControlHeader(compressedRequest); + ASSERT(spy->variants == std::vector({ OneTidBlockAckReqVariant::BASIC, OneTidBlockAckReqVariant::COMPRESSED })); + ASSERT(spy->tids == std::vector({ 3, 5 })); + bool multiTidRejectedAsUnknown = false; + try { + hcf.processControlHeader(multiTidRequest); + } + catch (const cRuntimeError& error) { + multiTidRejectedAsUnknown = std::string(error.what()).find("Unknown control frame") != std::string::npos; + } + ASSERT(multiTidRejectedAsUnknown); + ASSERT(spy->variants.size() == 2); + EV << "HCF accepts one-TID BAR variants and rejects Multi-TID as unknown.\n"; +} + { TestRecipientBlockAckProcedure procedure; auto request = makeCompressedBlockAckReq(); @@ -265,6 +331,8 @@ static std::pair> makeReceivedQosFrame(Sequen record.advanceStartingSequenceNumber(SequenceNumberCyclic(104)); ASSERT(!record.hasRecordedAckState(SequenceNumberCyclic(102), 0)); ASSERT(record.hasRecordedAckState(SequenceNumberCyclic(105), 0)); + record.dataFrameReceived(received102.second, 64); + ASSERT(!record.hasRecordedAckState(SequenceNumberCyclic(102), 0)); delete received102.first; delete received105.first; EV << "Receive-window advancement erases old state and retains overlap.\n"; @@ -357,6 +425,56 @@ static std::pair> makeReceivedQosFrame(Sequen EV << "Future MPDU returns displaced and consecutive complete MSDUs without losing gaps.\n"; } +{ + // IEEE Std 802.11-2024, 9.4.1.13 footnote 26 and 10.25.6.6.2.1(b): + // fragments consume individual slots, and a rejected future MPDU must not move WinStartB. + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(1), 2, SIMTIME_ZERO); + auto incomplete2Fragment0 = makeReceivedQosFrame(2, 0, true); + auto incomplete2Fragment1 = makeReceivedQosFrame(2, 1, true); + ASSERT(reordering.processReceivedQoSFrame(&agreement, incomplete2Fragment0.first, incomplete2Fragment0.second).empty()); + ASSERT(reordering.processReceivedQoSFrame(&agreement, incomplete2Fragment1.first, incomplete2Fragment1.second).empty()); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + ASSERT(receiveBuffer->getLength() == 2); + ASSERT(receiveBuffer->getBuffer().at(2)[0] == incomplete2Fragment0.first); + ASSERT(receiveBuffer->getBuffer().at(2)[1] == incomplete2Fragment1.first); + + auto received3 = makeReceivedQosFrame(3); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received3.first, received3.second).empty()); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(1)); + ASSERT(receiveBuffer->getLength() == 2); + ASSERT(receiveBuffer->getBuffer().size() == 1); + ASSERT(receiveBuffer->getBuffer().at(2)[0] == incomplete2Fragment0.first); + ASSERT(receiveBuffer->getBuffer().at(2)[1] == incomplete2Fragment1.first); + ASSERT(receiveBuffer->getBuffer().find(3) == receiveBuffer->getBuffer().end()); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(2)); + ASSERT(agreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(3))); + EV << "Rejected future MPDU preserves a fragment-full reorder window but updates the scoreboard.\n"; +} + +{ + MacAddress originatorAddress("11:22:33:44:55:66"); + TestBlockAckReordering reordering; + RecipientBlockAckAgreement agreement(originatorAddress, 5, SequenceNumberCyclic(4095), 2, SIMTIME_ZERO); + auto incomplete4095 = makeReceivedQosFrame(4095, 0, true); + auto incomplete0 = makeReceivedQosFrame(0, 0, true); + ASSERT(reordering.processReceivedQoSFrame(&agreement, incomplete4095.first, incomplete4095.second).empty()); + ASSERT(reordering.processReceivedQoSFrame(&agreement, incomplete0.first, incomplete0.second).empty()); + auto receiveBuffer = reordering.getReceiveBuffer(5, originatorAddress); + ASSERT(receiveBuffer->getLength() == 2); + + auto received1 = makeReceivedQosFrame(1); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received1.first, received1.second).empty()); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(receiveBuffer->getLength() == 2); + ASSERT(receiveBuffer->getBuffer().find(4095) == receiveBuffer->getBuffer().end()); + ASSERT(receiveBuffer->getBuffer().at(0).size() == 1 && receiveBuffer->getBuffer().at(0)[0] == incomplete0.first); + ASSERT(receiveBuffer->getBuffer().at(1).size() == 1 && receiveBuffer->getBuffer().at(1)[0] == received1.first); + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(0)); + EV << "Future MPDU reclaims a displaced slot across sequence wrap before advancing WinStartB.\n"; +} + { MacAddress originatorAddress("11:22:33:44:55:66"); TestRecipientBlockAckProcedure procedure; @@ -369,6 +487,15 @@ static std::pair> makeReceivedQosFrame(Sequen request->setStartingSequenceNumber(SequenceNumberCyclic(1)); auto response = dynamicPtrCast(procedure.buildBlockAck(request, &agreement)); ASSERT(response->getBlockAckBitmap().getBit(63)); + auto basicRequest = makeShared(); + basicRequest->setTidInfo(5); + basicRequest->setStartingSequenceNumber(SequenceNumberCyclic(0)); + auto basicResponse = dynamicPtrCast(procedure.buildBlockAck(basicRequest, &agreement)); + for (FragmentNumber fragmentNumber = 0; fragmentNumber < 16; fragmentNumber++) + ASSERT(basicResponse->getBlockAckBitmap(0).getBit(fragmentNumber)); + for (int sequenceIndex = 1; sequenceIndex < 64; sequenceIndex++) + for (FragmentNumber fragmentNumber = 0; fragmentNumber < 16; fragmentNumber++) + ASSERT(!basicResponse->getBlockAckBitmap(sequenceIndex).getBit(fragmentNumber)); EV << "Normal-Ack data advances and updates the Block Ack scoreboard.\n"; } @@ -566,9 +693,9 @@ static std::pair> makeReceivedQosFrame(Sequen request->setBlockAckPolicy(true); return request; }; - handler.createAgreement(makeAddbaRequest(listedPeer), &localDisabledPolicy); - handler.createAgreement(makeAddbaRequest(unlistedPeer), &localEnabledPolicy); - handler.createAgreement(makeAddbaRequest(positivePeer), &localEnabledPolicy); + handler.createAgreement(makeAddbaRequest(listedPeer), 1, &localDisabledPolicy); + handler.createAgreement(makeAddbaRequest(unlistedPeer), 2, &localEnabledPolicy); + handler.createAgreement(makeAddbaRequest(positivePeer), 3, &localEnabledPolicy); ASSERT(!handler.getAgreement(listedPeer, 5)->getIsCompressedBlockAckSupported()); ASSERT(!handler.getAgreement(unlistedPeer, 5)->getIsCompressedBlockAckSupported()); ASSERT(handler.getAgreement(positivePeer, 5)->getIsCompressedBlockAckSupported()); @@ -585,19 +712,19 @@ static std::pair> makeReceivedQosFrame(Sequen { MacAddress receiverAddress("10:20:30:40:50:60"); - OriginatorBlockAckAgreement immediateAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, false); + OriginatorBlockAckAgreement immediateAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, false, 0, 0); immediateAgreement.setIsAddbaResponseReceived(true); std::vector outstandingFrames { makeOutstandingQosFrame(receiverAddress, 5) }; ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement)); ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, nullptr)); immediateAgreement.setIsCompressedBlockAckSupported(true); - OriginatorBlockAckAgreement delayedAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, true); + OriginatorBlockAckAgreement delayedAgreement(receiverAddress, 5, SequenceNumberCyclic(100), 64, false, true, 0, 0); delayedAgreement.setIsAddbaResponseReceived(true); delayedAgreement.setIsCompressedBlockAckSupported(true); ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &delayedAgreement)); ASSERT(TestOriginatorQosAckPolicy::isCompressedRequestNeeded(outstandingFrames, &immediateAgreement)); MacAddress legacyReceiverAddress("10:20:30:40:50:61"); - OriginatorBlockAckAgreement legacyAgreement(legacyReceiverAddress, 5, SequenceNumberCyclic(100), 64, false, false); + OriginatorBlockAckAgreement legacyAgreement(legacyReceiverAddress, 5, SequenceNumberCyclic(100), 64, false, false, 0, 0); legacyAgreement.setIsAddbaResponseReceived(true); std::vector legacyOutstandingFrames { makeOutstandingQosFrame(legacyReceiverAddress, 5) }; ASSERT(!TestOriginatorQosAckPolicy::isCompressedRequestNeeded(legacyOutstandingFrames, &legacyAgreement)); @@ -645,7 +772,7 @@ static std::pair> makeReceivedQosFrame(Sequen TestOriginatorBlockAckAgreementHandler handler; TestOriginatorBlockAckAgreementPolicy policy; policy.configureCompressedBlockAckCapability(false, {}); - OriginatorBlockAckAgreement agreement(MacAddress("10:20:30:40:50:60"), 5, SequenceNumberCyclic(100), 64, false, false); + OriginatorBlockAckAgreement agreement(MacAddress("10:20:30:40:50:60"), 5, SequenceNumberCyclic(100), 64, false, false, 0, 0); auto delayedResponse = makeShared(); delayedResponse->setBlockAckPolicy(false); delayedResponse->setBufferSize(64); @@ -685,6 +812,7 @@ EV << ".\n"; %contains: stdout Compressed BAR encoding and round-trip passed. Compressed BA encoding and round-trip passed. +HCF accepts one-TID BAR variants and rejects Multi-TID as unknown. Missing recipient state produces a null compressed BA. Empty recipient record produces a null compressed BA. Empty Basic BA distinguishes old and current-window MPDUs. @@ -694,6 +822,8 @@ Established agreement produces the expected 64-bit bitmap. Compressed bitmap preserves leading receive-window holes. SN64 advances a size-64 scoreboard and remains buffered across a gap. Future MPDU returns displaced and consecutive complete MSDUs without losing gaps. +Rejected future MPDU preserves a fragment-full reorder window but updates the scoreboard. +Future MPDU reclaims a displaced slot across sequence wrap before advancing WinStartB. Normal-Ack data advances and updates the Block Ack scoreboard. Upward delivery advances only the reorder cursor, not WinStartR. Data-window slide preserves cyclic 4095-to-0 delivery order. From 4e9663b79f1490e1e2b93571412e56a24dde0fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 16 Aug 2026 13:37:51 +0200 Subject: [PATCH 47/56] fix(ieee80211): record frames at Block Ack window start Accept WinStartR in the receiver Block Ack bookkeeping and add focused Basic and Compressed Block Ack coverage for the boundary. --- .../ieee80211/mac/blockack/BlockAckRecord.cc | 5 ++- tests/unit/Ieee80211BlockAckRecord_1.test | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 tests/unit/Ieee80211BlockAckRecord_1.test diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index e212ed19537..54d478446b1 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -24,8 +24,9 @@ void BlockAckRecord::dataFrameReceived(const Ptr& hea SequenceNumberCyclic sequenceNumber = header->getSequenceNumber(); FragmentNumber fragmentNumber = header->getFragmentNumber(); // IEEE Std 802.11-2024, 10.25.6.3(b), case 3: an old related MPDU - // does not change the Block Ack record. - if (!(startingSequenceNumber <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048)) + // does not change the Block Ack record. The cyclic comparison below already + // restricts the accepted range to [WinStartR, WinStartR + 2047]. + if (!(startingSequenceNumber <= sequenceNumber)) return; // Cases 1 and 2: record an in-window MPDU, or advance WinStartR // before recording an MPDU beyond WinEndR (also see 10.25.6.4(c)). diff --git a/tests/unit/Ieee80211BlockAckRecord_1.test b/tests/unit/Ieee80211BlockAckRecord_1.test new file mode 100644 index 00000000000..99b9e16a7c4 --- /dev/null +++ b/tests/unit/Ieee80211BlockAckRecord_1.test @@ -0,0 +1,40 @@ +%description: +Validate that a Block Ack record accepts the MPDU at WinStartR and reports Basic fragment and Compressed state. +IEEE Std 802.11-2024, 10.25.6.3(b), case 1. + +%includes: +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; + +static Ptr makeReceivedQosHeader(SequenceNumber sequenceNumber) +{ + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setTid(5); + header->setSequenceNumber(SequenceNumberCyclic(sequenceNumber)); + header->setFragmentNumber(0); + return header; +} + +%activity: +{ + BlockAckRecord record(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(101)); + record.dataFrameReceived(makeReceivedQosHeader(101), 64); + record.dataFrameReceived(makeReceivedQosHeader(103), 64); + ASSERT(record.getAckState(SequenceNumberCyclic(101), 0)); + ASSERT(!record.getAckState(SequenceNumberCyclic(102), 0)); + ASSERT(record.getAckState(SequenceNumberCyclic(103), 0)); + ASSERT(record.getCompressedAckState(SequenceNumberCyclic(101))); + ASSERT(!record.getCompressedAckState(SequenceNumberCyclic(102))); + ASSERT(record.getCompressedAckState(SequenceNumberCyclic(103))); + EV << "IEEE Std 802.11-2024 10.25.6.3(b) WinStartR boundary is recorded in Basic and Compressed Block Ack state.\n"; +} + +EV << ".\n"; + +%contains: stdout +IEEE Std 802.11-2024 10.25.6.3(b) WinStartR boundary is recorded in Basic and Compressed Block Ack state. +. From de757fde89348a0bb9d74fe8a93a1fb9c31b3234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 6 Sep 2026 01:38:06 +0200 Subject: [PATCH 48/56] ieee80211: preserve Block Ack transaction state Scope Basic and Compressed BAR status transitions by receiver, and match incoming Block Ack responses against the active request before completing the receive step or changing recovery state. Unrelated responses leave the wait and timer active. Update the recipient scoreboard after successful buffer admission so capacity-rejected MPDUs remain unacknowledged and failed window advances preserve both cursors. Allow valid negative intermediate values in cyclic sequence arithmetic for wraparound admission. Add multi-peer, response-correlation, buffer-capacity, and sequence-wrap regressions. Correct the rejected-frame test expectation to require an unchanged scoreboard. Validation: debug build, four focused unit tests, two module tests, architecture checks, and independent review passed. Both selected legacy fingerprint mismatches reproduce identically at the unchanged parent; recorded fingerprint baselines remain untouched. --- .../mac/blockack/OneTidBlockAckReqVariant.h | 16 + .../blockackreordering/BlockAckReordering.cc | 13 +- .../ieee80211/mac/common/Ieee80211Defs.h | 2 +- .../mac/contract/IFrameSequenceHandler.h | 4 +- .../ieee80211/mac/coordinationfunction/Hcf.cc | 16 +- .../mac/framesequence/FrameSequenceHandler.cc | 26 +- .../mac/framesequence/FrameSequenceHandler.h | 2 +- .../framesequence/PrimitiveFrameSequences.cc | 10 +- .../ieee80211/mac/originator/QosAckHandler.cc | 5 +- ...eee80211BlockAckResponseCorrelation_1.test | 308 ++++++++++++++++++ tests/unit/Ieee80211AddbaTransaction_1.test | 67 +++- tests/unit/Ieee80211CompressedBlockAck_1.test | 27 +- 12 files changed, 476 insertions(+), 20 deletions(-) create mode 100644 tests/module/Ieee80211BlockAckResponseCorrelation_1.test diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h index 0d158b365cf..cc340c652a7 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h @@ -40,6 +40,22 @@ inline std::optional getOneTidBlockAckReqDetails(const return std::nullopt; } +// IEEE Std 802.11-2024, 9.3.1.7, 9.3.1.8, and 10.25.5: correlate the BAR RA +// with the BA TA, TID, and selected BlockAck variant before accepting a response. +inline bool isMatchingOneTidBlockAckResponse(const OneTidBlockAckReqDetails& blockAckReqDetails, const Ptr& blockAck) +{ + if (blockAckReqDetails.blockAckReq->getReceiverAddress() != blockAck->getTransmitterAddress()) + return false; + if (blockAckReqDetails.variant == OneTidBlockAckReqVariant::BASIC) { + auto basicBlockAck = dynamicPtrCast(blockAck); + return basicBlockAck != nullptr && basicBlockAck->getTidInfo() == blockAckReqDetails.tid; + } + else { + auto compressedBlockAck = dynamicPtrCast(blockAck); + return compressedBlockAck != nullptr && compressedBlockAck->getTidInfo() == blockAckReqDetails.tid; + } +} + } // 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 3728fe3cb89..d9e6dd69967 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -37,9 +37,6 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived auto sequenceNumber = dataHeader->getSequenceNumber(); auto startingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); bool advancesWindow = startingSequenceNumber + receiveBuffer->getBufferSize() <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048; - // IEEE Std 802.11-2024, 10.25.6.3 and 10.25.6.4: update the - // scoreboard for every related Data frame, independently of reorder storage. - agreement->dataFrameReceived(dataHeader); SequenceNumberCyclic newStartingSequenceNumber; if (advancesWindow) { // IEEE Std 802.11-2024, 10.25.6.6.2.1(b): store the future MPDU @@ -55,12 +52,20 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived receiveBuffer->insertFrameWithResult(dataPacket, dataHeader, newStartingSequenceNumber) : receiveBuffer->insertFrameWithResult(dataPacket, dataHeader); if (insertionResult != ReceiveBuffer::FrameInsertionResult::INSERTED) { - if (insertionResult == ReceiveBuffer::FrameInsertionResult::REJECTED_EXPIRED) + if (insertionResult == ReceiveBuffer::FrameInsertionResult::REJECTED_EXPIRED) { + // Preserve the receive-lifetime tombstone policy for late Block Ack fragments. + if (dataHeader->getAckPolicy() == BLOCK_ACK) + agreement->dataFrameReceived(dataHeader); result.tombstonedFragments.push_back(dataPacket); + } else delete dataPacket; return result; } + // IEEE Std 802.11-2024, 10.25.6.3 and 10.25.6.6.2.1: acknowledge an + // MPDU after it has been admitted to the receive buffer, so a capacity- + // rejected MPDU cannot be reported as received. + agreement->dataFrameReceived(dataHeader); if (advancesWindow) { framesToPassUp = collectCompletePrecedingMpdus(receiveBuffer, newStartingSequenceNumber); for (const auto& entry : framesToPassUp) diff --git a/src/inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h b/src/inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h index 11df39f2a79..a8f6fdcca2e 100644 --- a/src/inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h +++ b/src/inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h @@ -23,7 +23,7 @@ struct SequenceNumberCyclic SequenceNumber value; private: - SequenceNumber modulo4096(SequenceNumber value) const { ASSERT(value != -1); return (value % 4096 + 4096) % 4096; } // always returns positive result + SequenceNumber modulo4096(SequenceNumber value) const { return (value % 4096 + 4096) % 4096; } // always returns positive result SequenceNumber distance4096(SequenceNumber other) const { ASSERT(0 <= other && other < 4096); return (value - other + 4096) % 4096; } public: diff --git a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h index d16cf826483..3d2640db8fa 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h @@ -42,7 +42,9 @@ class INET_API IFrameSequenceHandler virtual const FrameSequenceContext *getContext() const = 0; virtual const IFrameSequence *getFrameSequence() const = 0; virtual void startFrameSequence(IFrameSequence *frameSequence, FrameSequenceContext *context, ICallback *callback) = 0; - virtual void processResponse(Packet *frame) = 0; + // Returns true when the handler consumes the frame; false leaves ownership + // with the caller because the active receive step remains unchanged. + virtual bool processResponse(Packet *frame) = 0; virtual void transmissionComplete() = 0; virtual bool isSequenceRunning() = 0; virtual void handleStartRxTimeout() = 0; diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc index 9cad5821b04..842bc4e349e 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.cc @@ -615,12 +615,19 @@ void Hcf::processLowerFrame(Packet *packet, const Ptr& if (edcaf && frameSequenceHandler->isSequenceRunning()) { // TODO always call processResponse? if ((!isForUs(header) && !startRxTimer->isScheduled()) || isForUs(header)) { - frameSequenceHandler->processResponse(packet); // Only cancel RxTimer when the current running sequence has been handled by frameSequenceHandler->processResponse(). // If the received frame is not for us, we are still waiting to receive our ACK. In that case, don't cancel the timer. // Otherwise, current frame sequence stucks in RX step and runs longer than intendeed, preventing sequence from // another access category (AC) to start running (RuntimeError("Channel access granted while a frame sequence is running")). - cancelEvent(startRxTimer); + if (frameSequenceHandler->processResponse(packet)) + cancelEvent(startRxTimer); + else { + EV_INFO << "Ignoring a response that does not match the active frame sequence step." << endl; + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } } else { EV_INFO << "This frame is not for us" << std::endl; @@ -1243,6 +1250,11 @@ void Hcf::originatorProcessReceivedControlFrame(Packet *packet, const Ptr(header)) { EV_INFO << blockAck->getClassName() << " has arrived" << std::endl; + auto blockAckReqDetails = getOneTidBlockAckReqDetails(lastTransmittedHeader); + if (!blockAckReqDetails || !isMatchingOneTidBlockAckResponse(*blockAckReqDetails, blockAck)) { + EV_INFO << "Ignoring BlockAck that does not match the transmitted BlockAckReq.\n"; + return; + } Tid tid = -1; MacAddress transmitterAddress = blockAck->getTransmitterAddress(); if (auto basicBlockAck = dynamicPtrCast(blockAck)) diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc index a2dda4b570f..b63215e0c76 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc @@ -8,12 +8,30 @@ #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h" #include "inet/common/INETUtils.h" +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" namespace inet { namespace ieee80211 { +static bool isUnexpectedBlockAckResponse(const FrameSequenceContext *context, Packet *frame) +{ + if (context == nullptr || frame == nullptr) + return false; + auto receiveStep = dynamic_cast(context->getLastStep()); + auto transmitStep = dynamic_cast(context->getStepBeforeLast()); + if (receiveStep == nullptr || transmitStep == nullptr || transmitStep->getFrameToTransmit() == nullptr) + return false; + auto receivedHeader = frame->peekAtFront(); + auto blockAck = dynamicPtrCast(receivedHeader); + if (blockAck == nullptr) + return false; + auto blockAckReqHeader = transmitStep->getFrameToTransmit()->peekAtFront(); + auto blockAckReqDetails = getOneTidBlockAckReqDetails(blockAckReqHeader); + return blockAckReqDetails && !isMatchingOneTidBlockAckResponse(*blockAckReqDetails, blockAck); +} + void FrameSequenceHandler::handleStartRxTimeout() { auto lastStep = context->getLastStep(); @@ -28,24 +46,26 @@ void FrameSequenceHandler::handleStartRxTimeout() } } -void FrameSequenceHandler::processResponse(Packet *frame) +bool FrameSequenceHandler::processResponse(Packet *frame) { ASSERT(callback != nullptr); if (frameSequenceCancellationRequested) { delete frame; abortFrameSequence(); - return; + return true; } auto lastStep = context->getLastStep(); switch (lastStep->getType()) { case IFrameSequenceStep::Type::RECEIVE: { + if (isUnexpectedBlockAckResponse(context, frame)) + return false; // TODO check if not for us and abort auto receiveStep = check_and_cast(context->getLastStep()); receiveStep->setFrameToReceive(frame); finishFrameSequenceStep(); if (isSequenceRunning()) startFrameSequenceStep(); - break; + return true; } case IFrameSequenceStep::Type::TRANSMIT: throw cRuntimeError("Received frame while current step is transmit"); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h index 9b423609170..8f2f68fb321 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h @@ -32,7 +32,7 @@ class INET_API FrameSequenceHandler : public IFrameSequenceHandler virtual const FrameSequenceContext *getContext() const override { return context; } virtual const IFrameSequence *getFrameSequence() const override { return frameSequence; } virtual void startFrameSequence(IFrameSequence *frameSequence, FrameSequenceContext *context, IFrameSequenceHandler::ICallback *callback) override; - virtual void processResponse(Packet *frame) override; + virtual bool processResponse(Packet *frame) override; virtual void transmissionComplete() override; virtual void handleStartRxTimeout() override; virtual bool isSequenceRunning() override { return frameSequence != nullptr; } diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index 00264aa2e7f..aac4bc1f314 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h" +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" + namespace inet { namespace ieee80211 { @@ -431,7 +433,13 @@ bool BlockAckReqBlockAckFs::completeStep(FrameSequenceContext *context) step++; auto receivedPacket = receiveStep->getReceivedFrame(); const auto& receivedHeader = receivedPacket->peekAtFront(); - return context->isForUs(receivedHeader) && receivedHeader->getType() == ST_BLOCKACK; + auto transmitStep = dynamic_cast(context->getStep(firstStep)); + if (transmitStep == nullptr || transmitStep->getFrameToTransmit() == nullptr) + return false; + auto blockAckReqHeader = transmitStep->getFrameToTransmit()->peekAtFront(); + auto blockAckReqDetails = getOneTidBlockAckReqDetails(blockAckReqHeader); + auto blockAck = dynamicPtrCast(receivedHeader); + return context->isForUs(receivedHeader) && receivedHeader->getType() == ST_BLOCKACK && blockAckReqDetails && blockAck != nullptr && isMatchingOneTidBlockAckResponse(*blockAckReqDetails, blockAck); } default: throw cRuntimeError("Unknown step"); diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index e0320e59c2b..33c148a99ad 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -213,18 +213,19 @@ void QosAckHandler::processTransmittedDataOrMgmtFrame(const Ptr& blockAckReq) { for (auto& ackStatus : ackStatuses) { + auto receiverAddress = ackStatus.first.first; auto tid = ackStatus.first.second.first; auto seqCtrlField = ackStatus.first.second.second; auto& status = ackStatus.second; if (auto basicBlockAckReq = dynamicPtrCast(blockAckReq)) { - if (basicBlockAckReq->getTidInfo() == tid) { + if (receiverAddress == blockAckReq->getReceiverAddress() && basicBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = basicBlockAckReq->getStartingSequenceNumber(); if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && SequenceNumberCyclic(seqCtrlField.getSequenceNumber()) >= startingSeqNum) status = Status::WAITING_FOR_BLOCK_ACK; } } else if (auto compressedBlockAckReq = dynamicPtrCast(blockAckReq)) { - if (compressedBlockAckReq->getTidInfo() == tid) { + if (receiverAddress == blockAckReq->getReceiverAddress() && compressedBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = compressedBlockAckReq->getStartingSequenceNumber(); if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && SequenceNumberCyclic(seqCtrlField.getSequenceNumber()) >= startingSeqNum && seqCtrlField.getFragmentNumber() == 0) // TODO ASSERT(seqCtrlField.second == 0)? status = Status::WAITING_FOR_BLOCK_ACK; diff --git a/tests/module/Ieee80211BlockAckResponseCorrelation_1.test b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test new file mode 100644 index 00000000000..cd7367a61b8 --- /dev/null +++ b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test @@ -0,0 +1,308 @@ +%description: +Exercise the live HCF lower-frame entry while a Block Ack response step is +waiting. A Block Ack for another active peer, for the wrong TID, or with the +wrong Basic/Compressed variant must leave the current response wait and its +timeout armed. The matching response then completes that same frame sequence. + +%file: TestIeee80211BlockAckResponseCorrelation.cc + +#include +#include + +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/channelaccess/Edca.h" +#include "inet/linklayer/ieee80211/mac/channelaccess/Edcaf.h" +#include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/contract/IOriginatorQoSAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" +#include "inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h" +#include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" +#include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" + +namespace inet { +namespace ieee80211 { + +static const MacAddress LOCAL_ADDRESS("02:00:00:00:00:01"); +static const MacAddress PEER_ONE("02:00:00:00:00:11"); +static const MacAddress PEER_TWO("02:00:00:00:00:12"); +static constexpr Tid CURRENT_TID = 6; +static constexpr SequenceNumber CURRENT_SSN = 700; + +class TestOriginatorBlockAckAgreementHandler : public OriginatorBlockAckAgreementHandler +{ + public: + void addEstablishedAgreement(const MacAddress& receiverAddress, Tid tid) + { + auto transactionId = receiverAddress == PEER_ONE ? 1 : 2; + auto agreement = new OriginatorBlockAckAgreement(receiverAddress, tid, SequenceNumberCyclic(CURRENT_SSN), 64, true, false, 1, transactionId); + agreement->setIsAddbaResponseReceived(true); + agreement->setBlockAckTimeoutValue(0); + agreement->calculateExpirationTime(); + blockAckAgreements[std::make_pair(receiverAddress, tid)] = agreement; + } +}; + +class TestOriginatorQosAckPolicy : public IOriginatorQoSAckPolicy +{ + public: + bool compressed = true; + + virtual bool isAckNeeded(const Ptr&) const override { return true; } + virtual AckPolicy computeAckPolicy(Packet *, const Ptr&, OriginatorBlockAckAgreement *) const override { return BLOCK_ACK; } + virtual bool isBlockAckReqNeeded(InProgressFrames *, TxopProcedure *, IOriginatorBlockAckAgreementHandler *) const override { return true; } + virtual bool isBlockAckPolicyEligibleFrame(Packet *, const Ptr&) const override { return true; } + virtual std::tuple computeBlockAckReqParameters(InProgressFrames *, TxopProcedure *, IOriginatorBlockAckAgreementHandler *) const override + { + return std::make_tuple(PEER_ONE, SequenceNumberCyclic(CURRENT_SSN), CURRENT_TID); + } + virtual bool isCompressedBlockAckReq(const std::vector&, OriginatorBlockAckAgreement *) const override { return compressed; } + virtual simtime_t getAckTimeout(Packet *, const Ptr&) const override { return SimTime(1, SIMTIME_MS); } + virtual simtime_t getBlockAckTimeout(Packet *, const Ptr&) const override { return SimTime(1, SIMTIME_MS); } +}; + +class TestInProgressFrames : public InProgressFrames +{ + public: + virtual std::vector getOutstandingFrames() override { return {}; } +}; + +class TestEdcaf : public Edcaf +{ +}; + +class TestEdca : public Edca +{ + protected: + TestEdcaf *channelOwner = nullptr; + + public: + TestEdca(TestEdcaf *channelOwner) : channelOwner(channelOwner) {} + virtual Edcaf *getChannelOwner() override { return channelOwner; } +}; + +class TestTxopProcedure : public TxopProcedure +{ + public: + virtual simtime_t getRemaining() const override { return SimTime(1, SIMTIME_MS); } +}; + +class TestFrameSequenceContext : public FrameSequenceContext +{ + public: + TestFrameSequenceContext(InProgressFrames *inProgressFrames, QoSContext *qosContext) : + FrameSequenceContext(LOCAL_ADDRESS, nullptr, inProgressFrames, nullptr, nullptr, nullptr, qosContext) + { + } + + virtual simtime_t getIfs() const override { return SIMTIME_ZERO; } +}; + +class TestHcfBlockAckResponseCorrelation : public Hcf +{ + protected: + TestEdcaf channelOwner; + TestEdca testEdca; + MacAddress localAddress = LOCAL_ADDRESS; + + virtual int numInitStages() const override { return 1; } + + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_LOCAL) { + startRxTimer = new cMessage("startRxTimeout"); + inactivityTimer = new cMessage("inactivityTimeout"); + addbaResponseTimer = new cMessage("addbaResponseTimeout"); + frameSequenceHandler = new FrameSequenceHandler(); + edca = &testEdca; + } + } + + virtual bool isForUs(const Ptr& header) const override + { + return header->getReceiverAddress() == localAddress; + } + + virtual void transmitFrame(Packet *, simtime_t) override + { + transmittedFrames++; + // The synthetic test frame is handed to the live FrameSequenceHandler + // at the same boundary where Tx would report PHY-TXEND.confirm. + frameSequenceHandler->transmissionComplete(); + } + + virtual void originatorProcessTransmittedFrame(Packet *) override {} + virtual void originatorProcessReceivedFrame(Packet *, Packet *) override { acceptedResponses++; } + virtual void originatorProcessFailedFrame(Packet *) override {} + virtual void frameSequenceFinished() override { finishedSequences++; } + + virtual void scheduleStartRxTimer(simtime_t timeout) override + { + scheduleAfter(timeout, startRxTimer); + } + + public: + int transmittedFrames = 0; + int acceptedResponses = 0; + int finishedSequences = 0; + + TestHcfBlockAckResponseCorrelation() : testEdca(&channelOwner) {} + + void startBlockAckResponseWait(IOriginatorQoSAckPolicy *ackPolicy, IOriginatorBlockAckProcedure *blockAckProcedure, + IOriginatorBlockAckAgreementHandler *agreementHandler, InProgressFrames *inProgressFrames, TxopProcedure *txopProcedure) + { + Enter_Method("startBlockAckResponseWait"); + auto qosContext = new QoSContext(ackPolicy, blockAckProcedure, agreementHandler, txopProcedure); + auto context = new TestFrameSequenceContext(inProgressFrames, qosContext); + frameSequenceHandler->startFrameSequence(new BlockAckReqBlockAckFs(), context, this); + } + + void injectBlockAck(const MacAddress& transmitterAddress, Tid tid, bool compressed) + { + Enter_Method("injectBlockAck"); + Ptr blockAck; + if (compressed) { + auto compressedBlockAck = makeShared(); + compressedBlockAck->setTidInfo(tid); + compressedBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(CURRENT_SSN)); + blockAck = compressedBlockAck; + } + else { + auto basicBlockAck = makeShared(); + basicBlockAck->setTidInfo(tid); + basicBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(CURRENT_SSN)); + blockAck = basicBlockAck; + } + blockAck->setReceiverAddress(localAddress); + blockAck->setTransmitterAddress(transmitterAddress); + auto packet = new Packet("injectedBlockAck", blockAck); + Hcf::processLowerFrame(packet, blockAck); + } + + bool isResponseWaitActive() const { return frameSequenceHandler->isSequenceRunning(); } + bool isResponseTimerScheduled() const { return startRxTimer->isScheduled(); } +}; + +Define_Module(TestHcfBlockAckResponseCorrelation); + +class Ieee80211BlockAckResponseCorrelationTest : public cSimpleModule +{ + public: + Ieee80211BlockAckResponseCorrelationTest() : cSimpleModule(65536) {} + + protected: + virtual void activity() override + { + auto hcf = check_and_cast(getModuleByPath("^.ap.wlan[0].mac.hcf")); + for (bool compressed : { false, true }) { + TestOriginatorBlockAckAgreementHandler agreementHandler; + agreementHandler.addEstablishedAgreement(PEER_ONE, CURRENT_TID); + agreementHandler.addEstablishedAgreement(PEER_TWO, CURRENT_TID); + ASSERT(agreementHandler.getActiveAgreement(PEER_ONE, CURRENT_TID) != nullptr); + ASSERT(agreementHandler.getActiveAgreement(PEER_TWO, CURRENT_TID) != nullptr); + TestOriginatorQosAckPolicy ackPolicy; + ackPolicy.compressed = compressed; + OriginatorBlockAckProcedure blockAckProcedure; + TestInProgressFrames inProgressFrames; + TestTxopProcedure txopProcedure; + + auto expectedTransmittedFrames = hcf->transmittedFrames + 1; + auto expectedAcceptedResponses = hcf->acceptedResponses + 1; + auto expectedFinishedSequences = hcf->finishedSequences + 1; + hcf->startBlockAckResponseWait(&ackPolicy, &blockAckProcedure, &agreementHandler, &inProgressFrames, &txopProcedure); + ASSERT(hcf->transmittedFrames == expectedTransmittedFrames); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + + // All three frames are addressed to this STA, so HCF must inspect + // the active BAR request before letting any one end the wait. + hcf->injectBlockAck(PEER_TWO, CURRENT_TID, compressed); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + + hcf->injectBlockAck(PEER_ONE, CURRENT_TID + 1, compressed); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + + hcf->injectBlockAck(PEER_ONE, CURRENT_TID, !compressed); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + + hcf->injectBlockAck(PEER_ONE, CURRENT_TID, compressed); + ASSERT(!hcf->isResponseWaitActive()); + ASSERT(!hcf->isResponseTimerScheduled()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses); + ASSERT(hcf->finishedSequences == expectedFinishedSequences); + } + std::cout << "HCF keeps the active Block Ack response wait scoped by peer, TID, and variant.\n"; + endSimulation(); + } +}; + +Define_Module(Ieee80211BlockAckResponseCorrelationTest); + +} // namespace ieee80211 +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.coordinationfunction.Hcf; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +module TestHcfBlockAckResponseCorrelation extends Hcf +{ + parameters: + @class(::inet::ieee80211::TestHcfBlockAckResponseCorrelation); +} + +simple Ieee80211BlockAckResponseCorrelationTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211BlockAckResponseCorrelationTest); +} + +network Ieee80211BlockAckResponseCorrelationTestNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint { + parameters: + wlan[*].mac.qosStation = true; + wlan[*].mac.hcf.typename = "TestHcfBlockAckResponseCorrelation"; + wlan[*].mgmt.beaconInterval = 10s; + } + test: Ieee80211BlockAckResponseCorrelationTest; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211BlockAckResponseCorrelationTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 1ms +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +HCF keeps the active Block Ack response wait scoped by peer, TID, and variant. diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index b55dd363f54..ebecf13555f 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -910,7 +910,7 @@ class TestFrameSequenceHandler : public IFrameSequenceHandler running = true; numStartedSequences++; } - virtual void processResponse(Packet *) override {} + virtual bool processResponse(Packet *) override { return true; } virtual void transmissionComplete() override {} virtual bool isSequenceRunning() override { return running; } virtual void handleStartRxTimeout() override {} @@ -6086,6 +6086,71 @@ for (auto terminalOrder : { std::pair(1, 2), std::pair(2, 1) delete frame; } +// A transmitted BAR selects one originator agreement by receiver and TID. +// IEEE Std 802.11-2024, 10.25.6.1: identical TIDs and overlapping sequence +// ranges at different peers remain independent Block Ack state machines. +for (bool compressed : { false, true }) { + TestQosAckHandler ackHandler; + auto peer1DataHeader = makeQosHeader(peer1, 6, SequenceNumberCyclic(700)); + auto peer2DataHeader = makeQosHeader(peer2, 6, SequenceNumberCyclic(700)); + peer1DataHeader->setAckPolicy(BLOCK_ACK); + peer2DataHeader->setAckPolicy(BLOCK_ACK); + auto peer1Frame = new Packet("peer1OverlappingData", peer1DataHeader); + auto peer2Frame = new Packet("peer2OverlappingData", peer2DataHeader); + auto peer1Header = peer1Frame->peekAtFront(); + auto peer2Header = peer2Frame->peekAtFront(); + ackHandler.frameGotInProgress(peer1Header); + ackHandler.processTransmittedDataOrMgmtFrame(peer1Header); + ackHandler.frameGotInProgress(peer2Header); + ackHandler.processTransmittedDataOrMgmtFrame(peer2Header); + + Ptr peer1BasicBar; + Ptr peer2BasicBar; + Ptr peer1CompressedBar; + Ptr peer2CompressedBar; + if (compressed) { + peer1CompressedBar = makeShared(); + peer2CompressedBar = makeShared(); + peer1CompressedBar->setReceiverAddress(peer1); + peer1CompressedBar->setTidInfo(6); + peer1CompressedBar->setStartingSequenceNumber(SequenceNumberCyclic(700)); + peer2CompressedBar->setReceiverAddress(peer2); + peer2CompressedBar->setTidInfo(6); + peer2CompressedBar->setStartingSequenceNumber(SequenceNumberCyclic(700)); + } + else { + peer1BasicBar = makeShared(); + peer2BasicBar = makeShared(); + peer1BasicBar->setReceiverAddress(peer1); + peer1BasicBar->setTidInfo(6); + peer1BasicBar->setStartingSequenceNumber(SequenceNumberCyclic(700)); + peer2BasicBar->setReceiverAddress(peer2); + peer2BasicBar->setTidInfo(6); + peer2BasicBar->setStartingSequenceNumber(SequenceNumberCyclic(700)); + } + Ptr peer1Bar; + Ptr peer2Bar; + if (compressed) { + peer1Bar = peer1CompressedBar; + peer2Bar = peer2CompressedBar; + } + else { + peer1Bar = peer1BasicBar; + peer2Bar = peer2BasicBar; + } + + ackHandler.processTransmittedBlockAckReq(peer1Bar); + ASSERT(ackHandler.getQoSDataAckStatus(peer1Header) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(ackHandler.getQoSDataAckStatus(peer2Header) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + + // The second peer remains eligible until its own BAR is transmitted. + ackHandler.processTransmittedBlockAckReq(peer2Bar); + ASSERT(ackHandler.getQoSDataAckStatus(peer1Header) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(ackHandler.getQoSDataAckStatus(peer2Header) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + delete peer1Frame; + delete peer2Frame; +} + // 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. diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index d43376db21e..411bf47b060 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -175,6 +175,11 @@ static std::pair> makeReceivedQosFrame(Sequen } %activity: +ASSERT(SequenceNumberCyclic(0) - 1 == SequenceNumberCyclic(4095)); +ASSERT(SequenceNumberCyclic(1) - 2 + 1 == SequenceNumberCyclic(0)); +ASSERT(SequenceNumberCyclic(4095) + 1 == SequenceNumberCyclic(0)); +EV << "Cyclic sequence arithmetic wraps through zero without invalid intermediate values.\n"; + { auto request = makeCompressedBlockAckReq(); Packet packet("compressedBar", request); @@ -440,6 +445,15 @@ static std::pair> makeReceivedQosFrame(Sequen ASSERT(receiveBuffer->getBuffer().at(2)[0] == incomplete2Fragment0.first); ASSERT(receiveBuffer->getBuffer().at(2)[1] == incomplete2Fragment1.first); + // An in-window MPDU is also rejected when incomplete fragments already + // occupy every receive-buffer slot; it must remain absent from the BA + // scoreboard and leave WinStartB unchanged. + auto received1 = makeReceivedQosFrame(1); + ASSERT(reordering.processReceivedQoSFrame(&agreement, received1.first, received1.second).empty()); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(1)); + ASSERT(!agreement.getBlockAckRecord()->getAckState(SequenceNumberCyclic(1), 0)); + ASSERT(!agreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(1))); + auto received3 = makeReceivedQosFrame(3); ASSERT(reordering.processReceivedQoSFrame(&agreement, received3.first, received3.second).empty()); ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == SequenceNumberCyclic(1)); @@ -448,9 +462,10 @@ static std::pair> makeReceivedQosFrame(Sequen ASSERT(receiveBuffer->getBuffer().at(2)[0] == incomplete2Fragment0.first); ASSERT(receiveBuffer->getBuffer().at(2)[1] == incomplete2Fragment1.first); ASSERT(receiveBuffer->getBuffer().find(3) == receiveBuffer->getBuffer().end()); - ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(2)); - ASSERT(agreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(3))); - EV << "Rejected future MPDU preserves a fragment-full reorder window but updates the scoreboard.\n"; + ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(1)); + ASSERT(!agreement.getBlockAckRecord()->getAckState(SequenceNumberCyclic(3), 0)); + ASSERT(!agreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(3))); + EV << "Rejected future MPDU preserves a fragment-full reorder window and leaves the scoreboard unchanged.\n"; } { @@ -472,6 +487,9 @@ static std::pair> makeReceivedQosFrame(Sequen ASSERT(receiveBuffer->getBuffer().at(0).size() == 1 && receiveBuffer->getBuffer().at(0)[0] == incomplete0.first); ASSERT(receiveBuffer->getBuffer().at(1).size() == 1 && receiveBuffer->getBuffer().at(1)[0] == received1.first); ASSERT(agreement.getStartingSequenceNumber() == SequenceNumberCyclic(0)); + ASSERT(receiveBuffer->getNextExpectedSequenceNumber() == agreement.getStartingSequenceNumber()); + ASSERT(agreement.getBlockAckRecord()->getAckState(SequenceNumberCyclic(1), 0)); + ASSERT(agreement.getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(1))); EV << "Future MPDU reclaims a displaced slot across sequence wrap before advancing WinStartB.\n"; } @@ -810,6 +828,7 @@ static std::pair> makeReceivedQosFrame(Sequen EV << ".\n"; %contains: stdout +Cyclic sequence arithmetic wraps through zero without invalid intermediate values. Compressed BAR encoding and round-trip passed. Compressed BA encoding and round-trip passed. HCF accepts one-TID BAR variants and rejects Multi-TID as unknown. @@ -822,7 +841,7 @@ Established agreement produces the expected 64-bit bitmap. Compressed bitmap preserves leading receive-window holes. SN64 advances a size-64 scoreboard and remains buffered across a gap. Future MPDU returns displaced and consecutive complete MSDUs without losing gaps. -Rejected future MPDU preserves a fragment-full reorder window but updates the scoreboard. +Rejected future MPDU preserves a fragment-full reorder window and leaves the scoreboard unchanged. Future MPDU reclaims a displaced slot across sequence wrap before advancing WinStartB. Normal-Ack data advances and updates the Block Ack scoreboard. Upward delivery advances only the reorder cursor, not WinStartR. From f742ca74000fea46a687dba9710891aded225f32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 6 Sep 2026 11:10:06 +0200 Subject: [PATCH 49/56] ieee80211: correlate responses with the active receive step An ACK, CTS, data frame, or unrelated control frame addressed to the station could abort a BAR exchange while it awaited a Block Ack. Reject frames outside the expected response type before transferring ownership or handling cancellation, while retaining peer, TID, and variant matching for Block Ack responses. Have DCF honor rejected responses without cancelling the receive timer. Cover Basic and Compressed BAR waits through matching completion and timeout, including an unrelated frame arriving halfway through the wait. --- .../ieee80211/mac/contract/IFrameSequence.h | 1 + .../ieee80211/mac/coordinationfunction/Dcf.cc | 11 +- .../mac/framesequence/FrameSequenceHandler.cc | 10 +- .../mac/framesequence/FrameSequenceStep.h | 9 +- .../framesequence/PrimitiveFrameSequences.cc | 30 +++- ...eee80211BlockAckResponseCorrelation_1.test | 167 ++++++++++++++++-- 6 files changed, 203 insertions(+), 25 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequence.h b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequence.h index af56920ed87..8678b0d067d 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IFrameSequence.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IFrameSequence.h @@ -55,6 +55,7 @@ class INET_API IReceiveStep : public IFrameSequenceStep virtual simtime_t getTimeout() = 0; virtual Packet *getReceivedFrame() = 0; virtual void setFrameToReceive(Packet *frame) = 0; + virtual bool isExpectedResponse(Packet *frame, FrameSequenceContext *context) const = 0; }; class INET_API IFrameSequence diff --git a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc index 350e551ee30..2f5bfc2f975 100644 --- a/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc +++ b/src/inet/linklayer/ieee80211/mac/coordinationfunction/Dcf.cc @@ -314,7 +314,15 @@ void Dcf::processLowerFrame(Packet *packet, const Ptr& if (frameSequenceHandler->isSequenceRunning()) { // TODO always call processResponses if ((!isForUs(header) && !startRxTimer->isScheduled()) || isForUs(header)) { - frameSequenceHandler->processResponse(packet); + if (frameSequenceHandler->processResponse(packet)) + cancelEvent(startRxTimer); + else { + EV_INFO << "Ignoring a response that does not match the active frame sequence step." << std::endl; + PacketDropDetails details; + details.setReason(OTHER_PACKET_DROP); + emit(packetDroppedSignal, packet, &details); + delete packet; + } } else { EV_INFO << "This frame is not for us" << std::endl; @@ -323,7 +331,6 @@ void Dcf::processLowerFrame(Packet *packet, const Ptr& emit(packetDroppedSignal, packet, &details); delete packet; } - cancelEvent(startRxTimer); } else if (isForUs(header)) recipientProcessReceivedFrame(packet, header); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc index b63215e0c76..350d557ef4f 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceHandler.cc @@ -49,17 +49,19 @@ void FrameSequenceHandler::handleStartRxTimeout() bool FrameSequenceHandler::processResponse(Packet *frame) { ASSERT(callback != nullptr); + auto lastStep = context->getLastStep(); + if (lastStep->getType() == IFrameSequenceStep::Type::RECEIVE) { + auto receiveStep = check_and_cast(lastStep); + if (!receiveStep->isExpectedResponse(frame, context) || isUnexpectedBlockAckResponse(context, frame)) + return false; + } if (frameSequenceCancellationRequested) { delete frame; abortFrameSequence(); return true; } - auto lastStep = context->getLastStep(); switch (lastStep->getType()) { case IFrameSequenceStep::Type::RECEIVE: { - if (isUnexpectedBlockAckResponse(context, frame)) - return false; - // TODO check if not for us and abort auto receiveStep = check_and_cast(context->getLastStep()); receiveStep->setFrameToReceive(frame); finishFrameSequenceStep(); diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h index bef73d418da..9c329a6fc5a 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h +++ b/src/inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h @@ -8,6 +8,8 @@ #ifndef __INET_FRAMESEQUENCESTEP_H #define __INET_FRAMESEQUENCESTEP_H +#include + #include "inet/linklayer/ieee80211/mac/contract/IFrameSequence.h" namespace inet { @@ -56,10 +58,12 @@ class INET_API ReceiveStep : public IReceiveStep Completion completion = Completion::UNDEFINED; simtime_t timeout = -1; Packet *receivedFrame = nullptr; + std::function responseValidator; public: - ReceiveStep(simtime_t timeout = -1) : - timeout(timeout) + ReceiveStep(simtime_t timeout = -1, std::function responseValidator = nullptr) : + timeout(timeout), + responseValidator(responseValidator) {} virtual ~ReceiveStep() { delete receivedFrame; } @@ -68,6 +72,7 @@ class INET_API ReceiveStep : public IReceiveStep virtual simtime_t getTimeout() override { return timeout; } virtual Packet *getReceivedFrame() override { return receivedFrame; } virtual void setFrameToReceive(Packet *frame) override { this->receivedFrame = frame; } + virtual bool isExpectedResponse(Packet *frame, FrameSequenceContext *context) const override { return responseValidator == nullptr || responseValidator(frame, context); } }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc index aac4bc1f314..a3738e260b8 100644 --- a/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc +++ b/src/inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.cc @@ -12,6 +12,22 @@ namespace inet { namespace ieee80211 { +namespace { + +// IEEE Std 802.11-2024, Annex G.4, 10.3.2.9, and 10.25.5: a frame exchange +// accepts the specified response type addressed to this station. +auto expectedResponse(Ieee80211FrameType type) +{ + return [type](Packet *packet, FrameSequenceContext *context) { + if (packet == nullptr || context == nullptr) + return false; + auto header = packet->peekAtFront(); + return context->isForUs(header) && header->getType() == type; + }; +} + +} // namespace + // TODO remove isForUs checks it's already done in framesequencehandler void SelfCtsFs::startSequence(FrameSequenceContext *context, int firstStep) @@ -79,7 +95,7 @@ IFrameSequenceStep *CtsFs::prepareStep(FrameSequenceContext *context) case 0: { auto txStep = check_and_cast(context->getLastStep()); auto rtsPacket = txStep->getFrameToTransmit(); - return new ReceiveStep(context->getCtsTimeout(rtsPacket, rtsPacket->peekAtFront())); + return new ReceiveStep(context->getCtsTimeout(rtsPacket, rtsPacket->peekAtFront()), expectedResponse(ST_CTS)); } case 1: return nullptr; @@ -151,7 +167,7 @@ IFrameSequenceStep *ManagementAckFs::prepareStep(FrameSequenceContext *context) auto txStep = check_and_cast(context->getLastStep()); auto packet = txStep->getFrameToTransmit(); auto mgmtHeader = packet->peekAtFront(); - return new ReceiveStep(context->getAckTimeout(packet, mgmtHeader)); + return new ReceiveStep(context->getAckTimeout(packet, mgmtHeader), expectedResponse(ST_ACK)); } case 2: return nullptr; @@ -223,7 +239,7 @@ IFrameSequenceStep *AckFs::prepareStep(FrameSequenceContext *context) auto txStep = check_and_cast(context->getLastStep()); auto packet = txStep->getFrameToTransmit(); auto dataOrMgmtHeader = packet->peekAtFront(); - return new ReceiveStep(context->getAckTimeout(packet, dataOrMgmtHeader)); + return new ReceiveStep(context->getAckTimeout(packet, dataOrMgmtHeader), expectedResponse(ST_ACK)); } case 1: return nullptr; @@ -269,7 +285,7 @@ IFrameSequenceStep *RtsCtsFs::prepareStep(FrameSequenceContext *context) auto txStep = check_and_cast(context->getLastStep()); auto packet = txStep->getFrameToTransmit(); auto rtsFrame = packet->peekAtFront(); - return new ReceiveStep(context->getCtsTimeout(packet, rtsFrame)); + return new ReceiveStep(context->getCtsTimeout(packet, rtsFrame), expectedResponse(ST_CTS)); } case 2: return nullptr; @@ -313,7 +329,7 @@ IFrameSequenceStep *FragFrameAckFs::prepareStep(FrameSequenceContext *context) auto txStep = check_and_cast(context->getLastStep()); auto packet = txStep->getFrameToTransmit(); auto dataOrMgmtHeader = packet->peekAtFront(); - return new ReceiveStep(context->getAckTimeout(packet, dataOrMgmtHeader)); + return new ReceiveStep(context->getAckTimeout(packet, dataOrMgmtHeader), expectedResponse(ST_ACK)); } case 2: return nullptr; @@ -357,7 +373,7 @@ IFrameSequenceStep *LastFrameAckFs::prepareStep(FrameSequenceContext *context) auto txStep = check_and_cast(context->getLastStep()); auto packet = txStep->getFrameToTransmit(); auto dataOrMgmtHeader = packet->peekAtFront(); - return new ReceiveStep(context->getAckTimeout(packet, dataOrMgmtHeader)); + return new ReceiveStep(context->getAckTimeout(packet, dataOrMgmtHeader), expectedResponse(ST_ACK)); } case 2: return nullptr; @@ -413,7 +429,7 @@ IFrameSequenceStep *BlockAckReqBlockAckFs::prepareStep(FrameSequenceContext *con auto txStep = check_and_cast(context->getLastStep()); auto packet = txStep->getFrameToTransmit(); auto blockAckReq = packet->peekAtFront(); - return new ReceiveStep(context->getQoSContext()->ackPolicy->getBlockAckTimeout(packet, blockAckReq)); + return new ReceiveStep(context->getQoSContext()->ackPolicy->getBlockAckTimeout(packet, blockAckReq), expectedResponse(ST_BLOCKACK)); } case 2: return nullptr; diff --git a/tests/module/Ieee80211BlockAckResponseCorrelation_1.test b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test index cd7367a61b8..64255a2e979 100644 --- a/tests/module/Ieee80211BlockAckResponseCorrelation_1.test +++ b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test @@ -1,8 +1,10 @@ %description: Exercise the live HCF lower-frame entry while a Block Ack response step is -waiting. A Block Ack for another active peer, for the wrong TID, or with the -wrong Basic/Compressed variant must leave the current response wait and its -timeout armed. The matching response then completes that same frame sequence. +waiting. An ACK, CTS, data frame, or other control frame addressed to this +station, as well as a Block Ack for another active peer, the wrong TID, or the +wrong Basic/Compressed variant, must leave the current response wait and its +timeout armed. The matching response then completes that same frame sequence, +while an unmatched wait ends only when its response timer expires. %file: TestIeee80211BlockAckResponseCorrelation.cc @@ -127,6 +129,8 @@ class TestHcfBlockAckResponseCorrelation : public Hcf return header->getReceiverAddress() == localAddress; } + virtual bool isReceptionInProgress() override { return false; } + virtual void transmitFrame(Packet *, simtime_t) override { transmittedFrames++; @@ -137,8 +141,12 @@ class TestHcfBlockAckResponseCorrelation : public Hcf virtual void originatorProcessTransmittedFrame(Packet *) override {} virtual void originatorProcessReceivedFrame(Packet *, Packet *) override { acceptedResponses++; } - virtual void originatorProcessFailedFrame(Packet *) override {} - virtual void frameSequenceFinished() override { finishedSequences++; } + virtual void originatorProcessFailedFrame(Packet *) override { failedFrames++; } + virtual void frameSequenceFinished() override + { + finishedSequences++; + lastFinishedAt = simTime(); + } virtual void scheduleStartRxTimer(simtime_t timeout) override { @@ -148,7 +156,9 @@ class TestHcfBlockAckResponseCorrelation : public Hcf public: int transmittedFrames = 0; int acceptedResponses = 0; + int failedFrames = 0; int finishedSequences = 0; + simtime_t lastFinishedAt = SIMTIME_ZERO; TestHcfBlockAckResponseCorrelation() : testEdca(&channelOwner) {} @@ -183,8 +193,57 @@ class TestHcfBlockAckResponseCorrelation : public Hcf Hcf::processLowerFrame(packet, blockAck); } + void injectAck() + { + Enter_Method("injectAck"); + auto ack = makeShared(); + ack->setReceiverAddress(localAddress); + auto packet = new Packet("injectedAck", ack); + Hcf::processLowerFrame(packet, ack); + } + + void injectCts() + { + Enter_Method("injectCts"); + auto cts = makeShared(); + cts->setReceiverAddress(localAddress); + auto packet = new Packet("injectedCts", cts); + Hcf::processLowerFrame(packet, cts); + } + + void injectData() + { + Enter_Method("injectData"); + auto data = makeShared(); + data->setReceiverAddress(localAddress); + data->setTransmitterAddress(PEER_ONE); + data->setAddress3(LOCAL_ADDRESS); + data->setSequenceNumber(SequenceNumberCyclic(CURRENT_SSN)); + auto packet = new Packet("injectedData", data); + Hcf::processLowerFrame(packet, data); + } + + void injectRts() + { + Enter_Method("injectRts"); + auto rts = makeShared(); + rts->setReceiverAddress(localAddress); + rts->setTransmitterAddress(PEER_ONE); + auto packet = new Packet("injectedRts", rts); + Hcf::processLowerFrame(packet, rts); + } + bool isResponseWaitActive() const { return frameSequenceHandler->isSequenceRunning(); } bool isResponseTimerScheduled() const { return startRxTimer->isScheduled(); } + simtime_t getResponseTimerArrival() const { return startRxTimer->getArrivalTime(); } + bool hasReceivedResponse() const + { + auto context = frameSequenceHandler->getContext(); + if (context == nullptr) + return false; + auto receiveStep = dynamic_cast(context->getLastStep()); + return receiveStep != nullptr && receiveStep->getReceivedFrame() != nullptr; + } }; Define_Module(TestHcfBlockAckResponseCorrelation); @@ -212,29 +271,73 @@ class Ieee80211BlockAckResponseCorrelationTest : public cSimpleModule auto expectedTransmittedFrames = hcf->transmittedFrames + 1; auto expectedAcceptedResponses = hcf->acceptedResponses + 1; + auto expectedFailedFrames = hcf->failedFrames; auto expectedFinishedSequences = hcf->finishedSequences + 1; hcf->startBlockAckResponseWait(&ackPolicy, &blockAckProcedure, &agreementHandler, &inProgressFrames, &txopProcedure); ASSERT(hcf->transmittedFrames == expectedTransmittedFrames); ASSERT(hcf->isResponseWaitActive()); ASSERT(hcf->isResponseTimerScheduled()); + const auto responseDeadline = simTime() + SimTime(1, SIMTIME_MS); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + + // Every frame below is addressed to this STA. HCF must leave each + // one outside the active BAR response step and retain its exact + // response deadline. + hcf->injectAck(); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + + hcf->injectCts(); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); - // All three frames are addressed to this STA, so HCF must inspect - // the active BAR request before letting any one end the wait. + hcf->injectData(); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + + hcf->injectRts(); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + + // Block Ack frames for the wrong peer, TID, or variant must obey + // the same ownership and timer boundary. hcf->injectBlockAck(PEER_TWO, CURRENT_TID, compressed); ASSERT(hcf->isResponseWaitActive()); ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); hcf->injectBlockAck(PEER_ONE, CURRENT_TID + 1, compressed); ASSERT(hcf->isResponseWaitActive()); ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); hcf->injectBlockAck(PEER_ONE, CURRENT_TID, !compressed); ASSERT(hcf->isResponseWaitActive()); ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == responseDeadline); + ASSERT(!hcf->hasReceivedResponse()); ASSERT(hcf->acceptedResponses == expectedAcceptedResponses - 1); ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); @@ -242,9 +345,53 @@ class Ieee80211BlockAckResponseCorrelationTest : public cSimpleModule ASSERT(!hcf->isResponseWaitActive()); ASSERT(!hcf->isResponseTimerScheduled()); ASSERT(hcf->acceptedResponses == expectedAcceptedResponses); + ASSERT(hcf->failedFrames == expectedFailedFrames); + ASSERT(hcf->finishedSequences == expectedFinishedSequences); + + // A fresh BAR wait with no matching response must stay alive + // through the same unrelated traffic and finish only at timeout. + expectedTransmittedFrames = hcf->transmittedFrames + 1; + expectedAcceptedResponses = hcf->acceptedResponses; + expectedFailedFrames = hcf->failedFrames + 1; + expectedFinishedSequences = hcf->finishedSequences + 1; + hcf->startBlockAckResponseWait(&ackPolicy, &blockAckProcedure, &agreementHandler, &inProgressFrames, &txopProcedure); + ASSERT(hcf->transmittedFrames == expectedTransmittedFrames); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + const auto timeoutDeadline = simTime() + SimTime(1, SIMTIME_MS); + ASSERT(hcf->getResponseTimerArrival() == timeoutDeadline); + + hcf->injectAck(); + hcf->injectCts(); + hcf->injectData(); + hcf->injectRts(); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == timeoutDeadline); + ASSERT(!hcf->hasReceivedResponse()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + ASSERT(hcf->failedFrames == expectedFailedFrames - 1); + + wait(SimTime(500, SIMTIME_US)); + hcf->injectAck(); + ASSERT(hcf->isResponseWaitActive()); + ASSERT(hcf->isResponseTimerScheduled()); + ASSERT(hcf->getResponseTimerArrival() == timeoutDeadline); + ASSERT(!hcf->hasReceivedResponse()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses); + ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); + ASSERT(hcf->failedFrames == expectedFailedFrames - 1); + + wait(SimTime(500, SIMTIME_US) + SimTime(1, SIMTIME_US)); + ASSERT(!hcf->isResponseWaitActive()); + ASSERT(!hcf->isResponseTimerScheduled()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses); ASSERT(hcf->finishedSequences == expectedFinishedSequences); + ASSERT(hcf->failedFrames == expectedFailedFrames); + ASSERT(hcf->lastFinishedAt == timeoutDeadline); } - std::cout << "HCF keeps the active Block Ack response wait scoped by peer, TID, and variant.\n"; + std::cout << "HCF keeps BAR response waits scoped by expected type, peer, TID, and variant until match or timeout.\n"; endSimulation(); } }; @@ -291,7 +438,7 @@ network Ieee80211BlockAckResponseCorrelationTestNetwork [General] network = Ieee80211BlockAckResponseCorrelationTestNetwork ned-path = .;../../../../src;../../lib -sim-time-limit = 1ms +sim-time-limit = 3ms seed-set = 0 cmdenv-express-mode = true record-vector-results = false @@ -305,4 +452,4 @@ record-scalar-results = false **.mobility.constraintAreaMaxZ = 0m %contains: stdout -HCF keeps the active Block Ack response wait scoped by peer, TID, and variant. +HCF keeps BAR response waits scoped by expected type, peer, TID, and variant until match or timeout. From deff17ac871e5930a46291f219f8d7ee764d6df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 6 Sep 2026 11:10:26 +0200 Subject: [PATCH 50/56] ieee80211: centralize Block Ack receive-window arithmetic Block Ack records, receive buffers, and reordering repeated cyclic window comparisons and advancement calculations. Route these decisions through a stateless BlockAckWindow service so their wraparound and half-sequence-space semantics stay consistent while each consumer retains ownership of its state. Preserve the existing comparisons and add focused unit coverage for window edges, sequence wraparound, candidate starts, and the unordered antipodal boundary. --- .../ieee80211/mac/blockack/BlockAckRecord.cc | 15 ++--- .../ieee80211/mac/blockack/BlockAckWindow.h | 61 +++++++++++++++++++ .../blockackreordering/BlockAckReordering.cc | 11 ++-- .../mac/blockackreordering/ReceiveBuffer.cc | 10 +-- tests/unit/Ieee80211BlockAckWindow_1.test | 47 ++++++++++++++ 5 files changed, 128 insertions(+), 16 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h create mode 100644 tests/unit/Ieee80211BlockAckWindow_1.test diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc index 54d478446b1..41c813a8a6c 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.cc @@ -8,6 +8,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/BlockAckRecord.h" #include "inet/common/stlutils.h" +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h" namespace inet { namespace ieee80211 { @@ -26,12 +27,12 @@ void BlockAckRecord::dataFrameReceived(const Ptr& hea // IEEE Std 802.11-2024, 10.25.6.3(b), case 3: an old related MPDU // does not change the Block Ack record. The cyclic comparison below already // restricts the accepted range to [WinStartR, WinStartR + 2047]. - if (!(startingSequenceNumber <= sequenceNumber)) + if (!BlockAckWindow::isAtOrAfter(sequenceNumber, startingSequenceNumber)) return; // Cases 1 and 2: record an in-window MPDU, or advance WinStartR // before recording an MPDU beyond WinEndR (also see 10.25.6.4(c)). - if (startingSequenceNumber + windowSize <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048) - advanceStartingSequenceNumber(sequenceNumber - windowSize + 1); + if (BlockAckWindow::isBeyond(startingSequenceNumber, windowSize, sequenceNumber)) + advanceStartingSequenceNumber(BlockAckWindow::getStartingSequenceNumber(sequenceNumber, windowSize)); acknowledgmentState[SequenceControlField(sequenceNumber.get(), fragmentNumber)] = true; } @@ -40,25 +41,25 @@ bool BlockAckRecord::getAckState(SequenceNumberCyclic sequenceNumber, FragmentNu // The status of MPDUs that are considered “old” and prior to the sequence number // range for which the receiver maintains status shall be reported as successfully // received (i.e., the corresponding bit in the bitmap shall be set to 1). - return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), fragmentNumber)) || sequenceNumber < startingSequenceNumber; + return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), fragmentNumber)) || BlockAckWindow::isBefore(sequenceNumber, startingSequenceNumber); } bool BlockAckRecord::getCompressedAckState(SequenceNumberCyclic sequenceNumber) { // IEEE Std 802.11-2024, 10.25.6.1: bits preceding the maintained // receive-window range are one; missing MPDUs within the range are zero. - return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), 0)) || sequenceNumber < startingSequenceNumber; + return containsKey(acknowledgmentState, SequenceControlField(sequenceNumber.get(), 0)) || BlockAckWindow::isBefore(sequenceNumber, startingSequenceNumber); } void BlockAckRecord::advanceStartingSequenceNumber(SequenceNumberCyclic newStartingSequenceNumber) { // IEEE Std 802.11-2024, 10.25.6.3 and 10.25.6.4: advance WinStartR // for a newer related MPDU or BAR SSN, using the 12-bit sequence space. - if (!(startingSequenceNumber < newStartingSequenceNumber)) + if (!BlockAckWindow::isAfter(newStartingSequenceNumber, startingSequenceNumber)) return; auto it = acknowledgmentState.begin(); while (it != acknowledgmentState.end()) { - if (SequenceNumberCyclic(it->first.getSequenceNumber()) < newStartingSequenceNumber) + if (BlockAckWindow::isBefore(SequenceNumberCyclic(it->first.getSequenceNumber()), newStartingSequenceNumber)) it = acknowledgmentState.erase(it); else it++; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h new file mode 100644 index 00000000000..db93d3af02d --- /dev/null +++ b/src/inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h @@ -0,0 +1,61 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + + +#ifndef __INET_BLOCKACKWINDOW_H +#define __INET_BLOCKACKWINDOW_H + +#include "inet/linklayer/ieee80211/mac/common/Ieee80211Defs.h" + +namespace inet { +namespace ieee80211 { + +/** + * Shared cyclic-window decisions for Block Ack receive state. + * + * The start and size remain owned by the Block Ack record or receive buffer; + * this class only keeps the sequence-space predicates in one place. + */ +class INET_API BlockAckWindow +{ + public: + static constexpr int HALF_SEQUENCE_SPACE = 2048; + + static bool isBefore(SequenceNumberCyclic sequenceNumber, SequenceNumberCyclic reference) + { + return sequenceNumber < reference; + } + + static bool isAtOrAfter(SequenceNumberCyclic sequenceNumber, SequenceNumberCyclic reference) + { + return reference <= sequenceNumber; + } + + static bool isAfter(SequenceNumberCyclic sequenceNumber, SequenceNumberCyclic reference) + { + return reference < sequenceNumber; + } + + static bool isWithin(SequenceNumberCyclic startingSequenceNumber, int windowSize, SequenceNumberCyclic sequenceNumber) + { + return isAtOrAfter(sequenceNumber, startingSequenceNumber) && isBefore(sequenceNumber, startingSequenceNumber + windowSize); + } + + static bool isBeyond(SequenceNumberCyclic startingSequenceNumber, int windowSize, SequenceNumberCyclic sequenceNumber) + { + return isAtOrAfter(sequenceNumber, startingSequenceNumber + windowSize) && isBefore(sequenceNumber, startingSequenceNumber + HALF_SEQUENCE_SPACE); + } + + static SequenceNumberCyclic getStartingSequenceNumber(SequenceNumberCyclic sequenceNumber, int windowSize) + { + return sequenceNumber - windowSize + 1; + } +}; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc index d9e6dd69967..24ed883e11a 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.cc @@ -9,6 +9,7 @@ #include +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h" #include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" @@ -36,12 +37,12 @@ BlockAckReordering::QosFrameProcessingResult BlockAckReordering::processReceived ReorderBuffer framesToPassUp; auto sequenceNumber = dataHeader->getSequenceNumber(); auto startingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); - bool advancesWindow = startingSequenceNumber + receiveBuffer->getBufferSize() <= sequenceNumber && sequenceNumber < startingSequenceNumber + 2048; + bool advancesWindow = BlockAckWindow::isBeyond(startingSequenceNumber, receiveBuffer->getBufferSize(), sequenceNumber); SequenceNumberCyclic newStartingSequenceNumber; if (advancesWindow) { // IEEE Std 802.11-2024, 10.25.6.6.2.1(b): store the future MPDU // before moving WinStartB and releasing complete displaced MSDUs. - newStartingSequenceNumber = sequenceNumber - receiveBuffer->getBufferSize() + 1; + newStartingSequenceNumber = BlockAckWindow::getStartingSequenceNumber(sequenceNumber, receiveBuffer->getBufferSize()); } // 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). @@ -148,7 +149,7 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::processReceivedBlockAckReq // advance NextExpectedSequenceNumber to at least the BAR SSN without // regressing it past consecutively released MSDUs. receiveBuffer->dropFramesUntil(startingSequenceNumber); - if (receiveBuffer->getNextExpectedSequenceNumber() < startingSequenceNumber) + if (BlockAckWindow::isBefore(receiveBuffer->getNextExpectedSequenceNumber(), startingSequenceNumber)) receiveBuffer->setNextExpectedSequenceNumber(startingSequenceNumber); return completePrecedingMpdus; } @@ -167,7 +168,7 @@ BlockAckReordering::ReorderBuffer BlockAckReordering::collectCompletePrecedingMp auto currentStartingSequenceNumber = receiveBuffer->getNextExpectedSequenceNumber(); for (int i = 0; i < receiveBuffer->getBufferSize(); i++) { auto sequenceNumber = currentStartingSequenceNumber + i; - if (!(sequenceNumber < startingSequenceNumber)) + if (!BlockAckWindow::isBefore(sequenceNumber, startingSequenceNumber)) break; auto it = buffer.find(sequenceNumber.get()); if (it != buffer.end() && ReceiveBuffer::isComplete(it->second)) @@ -295,7 +296,7 @@ std::vector BlockAckReordering::getEarliestCompleteMsduOrAMsduIfExists if (earliestFragments.size() > 0) { for (auto it : buffer) { SequenceNumberCyclic currentSeqNum = it.second.at(0)->peekAtFront()->getSequenceNumber(); - if (currentSeqNum < earliestSeqNum) { + if (BlockAckWindow::isBefore(currentSeqNum, earliestSeqNum)) { if (ReceiveBuffer::isComplete(it.second)) { earliestFragments = it.second; earliestSeqNum = currentSeqNum; diff --git a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc index 1eb73de5725..5a096c6ded6 100644 --- a/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc +++ b/src/inet/linklayer/ieee80211/mac/blockackreordering/ReceiveBuffer.cc @@ -9,6 +9,8 @@ #include +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h" + namespace inet { namespace ieee80211 { @@ -46,7 +48,7 @@ bool ReceiveBuffer::isComplete(const Fragments& fragments) void ReceiveBuffer::pruneExpiredFragmentSequences() { for (auto it = expiredFragmentSequences.begin(); it != expiredFragmentSequences.end();) { - if (SequenceNumberCyclic(*it) < nextExpectedSequenceNumber) + if (BlockAckWindow::isBefore(SequenceNumberCyclic(*it), nextExpectedSequenceNumber)) it = expiredFragmentSequences.erase(it); else ++it; @@ -63,11 +65,11 @@ bool ReceiveBuffer::canInsertFrame(const Ptr& dataHea { auto sequenceNumber = dataHeader->getSequenceNumber(); auto fragmentNumber = dataHeader->getFragmentNumber(); - if (!(nextExpectedSequenceNumber <= sequenceNumber && sequenceNumber < nextExpectedSequenceNumber + bufferSize)) + if (!BlockAckWindow::isWithin(nextExpectedSequenceNumber, bufferSize, sequenceNumber)) return false; int retainedLength = length; for (const auto& entry : buffer) { - if (SequenceNumberCyclic(entry.first) < nextExpectedSequenceNumber) + if (BlockAckWindow::isBefore(SequenceNumberCyclic(entry.first), nextExpectedSequenceNumber)) retainedLength -= entry.second.size(); } // IEEE Std 802.11-2024, 9.4.1.13, footnote 26: each fragment @@ -136,7 +138,7 @@ void ReceiveBuffer::dropFramesUntil(SequenceNumberCyclic sequenceNumber) { auto it = buffer.begin(); while (it != buffer.end()) { - if (SequenceNumberCyclic(it->first) < sequenceNumber) { + if (BlockAckWindow::isBefore(SequenceNumberCyclic(it->first), sequenceNumber)) { length -= it->second.size(); for (auto fragment : it->second) delete fragment; diff --git a/tests/unit/Ieee80211BlockAckWindow_1.test b/tests/unit/Ieee80211BlockAckWindow_1.test new file mode 100644 index 00000000000..52984bae589 --- /dev/null +++ b/tests/unit/Ieee80211BlockAckWindow_1.test @@ -0,0 +1,47 @@ +%description: +Validate the shared cyclic receive-window decisions used by Block Ack records, +receive buffers, and reordering. +IEEE Std 802.11-2024, 10.25.6.3, 10.25.6.4, and 10.25.6.6.2.1. + +%includes: +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h" + +%global: +using namespace inet; +using namespace inet::ieee80211; + +%activity: +{ + const auto startingSequenceNumber = SequenceNumberCyclic(4094); + const int windowSize = 4; + + ASSERT(BlockAckWindow::isWithin(startingSequenceNumber, windowSize, SequenceNumberCyclic(4094))); + ASSERT(BlockAckWindow::isWithin(startingSequenceNumber, windowSize, SequenceNumberCyclic(4095))); + ASSERT(BlockAckWindow::isWithin(startingSequenceNumber, windowSize, SequenceNumberCyclic(0))); + ASSERT(BlockAckWindow::isWithin(startingSequenceNumber, windowSize, SequenceNumberCyclic(1))); + ASSERT(!BlockAckWindow::isWithin(startingSequenceNumber, windowSize, SequenceNumberCyclic(2))); + ASSERT(!BlockAckWindow::isWithin(startingSequenceNumber, windowSize, SequenceNumberCyclic(2046))); + + ASSERT(BlockAckWindow::isBefore(SequenceNumberCyclic(4093), startingSequenceNumber)); + ASSERT(!BlockAckWindow::isBefore(startingSequenceNumber, startingSequenceNumber)); + ASSERT(BlockAckWindow::isAtOrAfter(SequenceNumberCyclic(4094), startingSequenceNumber)); + ASSERT(BlockAckWindow::isAtOrAfter(SequenceNumberCyclic(0), startingSequenceNumber)); + ASSERT(!BlockAckWindow::isAtOrAfter(SequenceNumberCyclic(4093), startingSequenceNumber)); + + ASSERT(BlockAckWindow::isBeyond(startingSequenceNumber, windowSize, SequenceNumberCyclic(2))); + ASSERT(BlockAckWindow::isBeyond(startingSequenceNumber, windowSize, SequenceNumberCyclic(2045))); + ASSERT(!BlockAckWindow::isBeyond(startingSequenceNumber, windowSize, SequenceNumberCyclic(2046))); + ASSERT(!BlockAckWindow::isAfter(SequenceNumberCyclic(2046), startingSequenceNumber)); + ASSERT(!BlockAckWindow::isBefore(SequenceNumberCyclic(2046), startingSequenceNumber)); + ASSERT(!BlockAckWindow::isAtOrAfter(SequenceNumberCyclic(2046), startingSequenceNumber)); + + ASSERT(BlockAckWindow::getStartingSequenceNumber(SequenceNumberCyclic(2), windowSize) == SequenceNumberCyclic(4095)); + ASSERT(BlockAckWindow::getStartingSequenceNumber(SequenceNumberCyclic(0), windowSize) == SequenceNumberCyclic(4093)); + EV << "Shared Block Ack cyclic receive-window predicates preserve wraparound and the antipodal boundary.\n"; +} + +EV << ".\n"; + +%contains: stdout +Shared Block Ack cyclic receive-window predicates preserve wraparound and the antipodal boundary. +. From f6d5a5917eca0b03c8354b9b7cb4d7f774d4d021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 6 Sep 2026 11:41:51 +0200 Subject: [PATCH 51/56] ieee80211: Enforce compressed BAR eligibility before state changes A compressed BAR with a nonzero fragment number could advance the receive window before the acknowledgment policy rejected its response. Share recipient acceptance checks before reorder processing and response selection, requiring an established immediate agreement with snapshotted local HT operation and configured peer compressed Block Ack support. Sparse outstanding sequences such as 0 and 100 could leave sequence 100 waiting for a response that cannot represent it. Reject compressed selection when the matching set exceeds the 64-position bitmap and limit transmitted-BAR acknowledgment transitions to that window. Add module regressions for unchanged receive state and no response after rejected BARs, capability gates, sparse and wrapping sequences, timeout, and subsequent requests for frames outside the first window. Validation: debug build, two focused unit tests, four focused module tests, and scoped architecture, naming, and seal checks pass. The legacy MacQosWithBlockAck fingerprint mismatch is identical on original HEAD; no fingerprint baseline changes are included. --- .../mac/blockack/OneTidBlockAckReqVariant.h | 16 ++ .../mac/blockack/RecipientBlockAckAgreement.h | 3 + .../RecipientBlockAckAgreementHandler.cc | 1 + .../RecipientBlockAckAgreementPolicy.cc | 9 + .../RecipientBlockAckAgreementPolicy.h | 7 +- .../RecipientBlockAckAgreementPolicy.ned | 2 + .../IRecipientBlockAckAgreementPolicy.h | 1 + .../mac/originator/OriginatorQosAckPolicy.cc | 12 + .../ieee80211/mac/originator/QosAckHandler.cc | 5 +- .../mac/recipient/RecipientQosAckPolicy.cc | 7 +- .../recipient/RecipientQosMacDataService.cc | 22 +- .../Ieee80211CompressedBlockAckRuntime.test | 4 + ...Ieee80211CompressedBlockAckValidation.test | 256 ++++++++++++++++++ tests/unit/Ieee80211AddbaTransaction_1.test | 1 + tests/unit/Ieee80211CompressedBlockAck_1.test | 3 + 15 files changed, 331 insertions(+), 18 deletions(-) create mode 100644 tests/module/Ieee80211CompressedBlockAckValidation.test diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h index cc340c652a7..f0ed03e382a 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h @@ -10,6 +10,7 @@ #include #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" namespace inet { namespace ieee80211 { @@ -40,6 +41,21 @@ inline std::optional getOneTidBlockAckReqDetails(const return std::nullopt; } +// Validate before either receive-window mutation or response generation. +// IEEE Std 802.11-2024, 9.3.1.7.2 and 10.25.6: compressed BARs have +// fragment number zero and require an established HT-immediate agreement. +inline bool isAcceptedOneTidBlockAckReq(const Ptr& request, const RecipientBlockAckAgreement *agreement) +{ + if (agreement == nullptr) + return false; + if (dynamicPtrCast(request)) + return !agreement->isInactivityExpired(); + if (auto compressed = dynamicPtrCast(request)) + return compressed->getFragmentNumber() == 0 && agreement->getIsCompressedBlockAckSupported() && + agreement->getIsAddbaResponseSent() && !agreement->getIsDelayedBlockAckPolicySupported(); + return false; +} + // IEEE Std 802.11-2024, 9.3.1.7, 9.3.1.8, and 10.25.5: correlate the BAR RA // with the BA TA, TID, and selected BlockAck variant before accepting a response. inline bool isMatchingOneTidBlockAckResponse(const OneTidBlockAckReqDetails& blockAckReqDetails, const Ptr& blockAck) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h index 02a7a705f30..b79796c4359 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h @@ -21,6 +21,7 @@ class INET_API RecipientBlockAckAgreement : public cObject int bufferSize = -1; simtime_t blockAckTimeoutValue = 0; bool isAddbaResponseSent = false; + bool isCompressedBlockAckSupported = false; bool isDelayedBlockAckPolicySupported = false; simtime_t expirationTime = -1; // The agreement stays installed until the timeout DELBA is transmitted; @@ -44,6 +45,8 @@ class INET_API RecipientBlockAckAgreement : public cObject if (!inactivityExpired) expirationTime = blockAckTimeoutValue == 0 ? SIMTIME_MAX : simTime() + blockAckTimeoutValue; } + virtual bool getIsCompressedBlockAckSupported() const { return isCompressedBlockAckSupported; } + virtual void setIsCompressedBlockAckSupported(bool supported) { isCompressedBlockAckSupported = supported; } virtual bool getIsAddbaResponseSent() const { return isAddbaResponseSent; } virtual bool getIsDelayedBlockAckPolicySupported() const { return isDelayedBlockAckPolicySupported; } diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index cadcea969f2..e2143a548e5 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -196,6 +196,7 @@ RecipientBlockAckAgreement *RecipientBlockAckAgreementHandler::processReceivedAd } else blockAckAgreements[id] = agreement; + agreement->setIsCompressedBlockAckSupported(blockAckAgreementPolicy->isPeerCompressedBlockAckSupported(addbaRequest->getTransmitterAddress())); agreement->addbaResposneSent(); agreement->setIsDelayedBlockAckPolicySupported(addbaResponse->getBlockAckPolicy() == 0); scheduleInactivityTimer(agreementHandlerCallback); diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc index 73813feaa5d..062c5c519f4 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.cc @@ -14,7 +14,11 @@ Define_Module(RecipientBlockAckAgreementPolicy); void RecipientBlockAckAgreementPolicy::initialize(int stage) { + ModeSetListener::initialize(stage); if (stage == INITSTAGE_LOCAL) { + localCompressedBlockAckSupported = par("localCompressedBlockAckSupported"); + for (const auto& address : cStringTokenizer(par("compressedBlockAckPeerAddresses")).asVector()) + compressedBlockAckPeerAddresses.insert(MacAddress(address.c_str())); isDelayedBlockAckPolicySupported = par("delayedAckPolicySupported"); isAMsduSupported = par("aMsduSupported"); maximumAllowedBufferSize = par("maximumAllowedBufferSize"); @@ -22,6 +26,11 @@ void RecipientBlockAckAgreementPolicy::initialize(int stage) } } +bool RecipientBlockAckAgreementPolicy::isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const +{ + return modeSet != nullptr && modeSet->isHtOperationSupported() && localCompressedBlockAckSupported && compressedBlockAckPeerAddresses.count(peerAddress) != 0; +} + bool RecipientBlockAckAgreementPolicy::isAddbaReqAccepted(const Ptr& addbaReq) { return true; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h index 995fcc29fdb..16b4da33f8e 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h @@ -8,17 +8,19 @@ #ifndef __INET_RECIPIENTBLOCKACKAGREEMENTPOLICY_H #define __INET_RECIPIENTBLOCKACKAGREEMENTPOLICY_H -#include "inet/common/SimpleModule.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/common/ModeSetListener.h" #include "inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h" namespace inet { namespace ieee80211 { -class INET_API RecipientBlockAckAgreementPolicy : public SimpleModule, public IRecipientBlockAckAgreementPolicy +class INET_API RecipientBlockAckAgreementPolicy : public ModeSetListener, public IRecipientBlockAckAgreementPolicy { protected: int maximumAllowedBufferSize = -1; + bool localCompressedBlockAckSupported = false; + std::set compressedBlockAckPeerAddresses; bool isAMsduSupported = false; bool isDelayedBlockAckPolicySupported = false; simtime_t blockAckTimeoutValue = -1; @@ -34,6 +36,7 @@ class INET_API RecipientBlockAckAgreementPolicy : public SimpleModule, public IR virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } virtual bool aMsduSupported() const override { return isAMsduSupported; } virtual bool delayedBlockAckPolicySupported() const override { return isDelayedBlockAckPolicySupported; } + virtual bool isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const override; virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } }; diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned index 8c41285b009..ff84122b1d3 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.ned @@ -17,6 +17,8 @@ simple RecipientBlockAckAgreementPolicy extends SimpleModule like IRecipientBloc { parameters: @class(RecipientBlockAckAgreementPolicy); + bool localCompressedBlockAckSupported = default(false); // Requires HT operation in the active mode set + string compressedBlockAckPeerAddresses = default(""); // Configured peer capability input, snapshotted at ADDBA acceptance bool delayedAckPolicySupported = default(false); bool aMsduSupported = default(true); int maximumAllowedBufferSize = default(64); diff --git a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h index 48009a67161..3f03c6532dc 100644 --- a/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/contract/IRecipientBlockAckAgreementPolicy.h @@ -24,6 +24,7 @@ class INET_API IRecipientBlockAckAgreementPolicy virtual simtime_t getBlockAckTimeoutValue() const = 0; virtual bool aMsduSupported() const = 0; virtual bool delayedBlockAckPolicySupported() const = 0; + virtual bool isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const = 0; virtual int getMaximumAllowedBufferSize() const = 0; }; diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index db92c01cb73..3df9f756595 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h" + #include #include "inet/linklayer/ieee80211/mac/contract/IOriginatorBlockAckAgreementHandler.h" @@ -74,14 +76,24 @@ bool OriginatorQosAckPolicy::isCompressedBlockAckReqNeeded(const std::vectorgetIsCompressedBlockAckSupported() || !agreement->getIsAddbaResponseReceived() || agreement->getIsDelayedBlockAckPolicySupported()) return false; bool hasMatchingOutstandingFrame = false; + SequenceNumberCyclic startingSequenceNumber; for (auto frame : outstandingFrames) { auto header = dynamicPtrCast(frame->peekAtFront()); if (header == nullptr || header->getReceiverAddress() != agreement->getReceiverAddr() || header->getTid() != agreement->getTid()) continue; + if (!hasMatchingOutstandingFrame || header->getSequenceNumber() < startingSequenceNumber) + startingSequenceNumber = header->getSequenceNumber(); hasMatchingOutstandingFrame = true; if (header->getFragmentNumber() != 0 || header->getMoreFragments()) return false; } + for (auto frame : outstandingFrames) { + auto header = dynamicPtrCast(frame->peekAtFront()); + if (header != nullptr && header->getReceiverAddress() == agreement->getReceiverAddr() && header->getTid() == agreement->getTid()) { + if (!BlockAckWindow::isWithin(startingSequenceNumber, 64, header->getSequenceNumber())) + return false; + } + } return hasMatchingOutstandingFrame; } diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index 33c148a99ad..da2d2d05239 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/BlockAckWindow.h" + namespace inet { namespace ieee80211 { @@ -227,7 +229,8 @@ void QosAckHandler::processTransmittedBlockAckReq(const Ptr(blockAckReq)) { if (receiverAddress == blockAckReq->getReceiverAddress() && compressedBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = compressedBlockAckReq->getStartingSequenceNumber(); - if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && SequenceNumberCyclic(seqCtrlField.getSequenceNumber()) >= startingSeqNum && seqCtrlField.getFragmentNumber() == 0) // TODO ASSERT(seqCtrlField.second == 0)? + // IEEE Std 802.11-2024, 10.25.6.1: the non-HE bitmap covers 64 sequence numbers. + if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && BlockAckWindow::isWithin(startingSeqNum, 64, SequenceNumberCyclic(seqCtrlField.getSequenceNumber())) && seqCtrlField.getFragmentNumber() == 0) status = Status::WAITING_FOR_BLOCK_ACK; } } diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.cc index 2be55a927b4..022d52611b8 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/OneTidBlockAckReqVariant.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" namespace inet { @@ -63,7 +64,7 @@ bool RecipientQosAckPolicy::isAckNeeded(const Ptr& blockAckReq, RecipientBlockAckAgreement *agreement) const { if (dynamicPtrCast(blockAckReq)) { - return agreement != nullptr && !agreement->isInactivityExpired(); + return isAcceptedOneTidBlockAckReq(blockAckReq, agreement); // 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. } @@ -77,9 +78,7 @@ bool RecipientQosAckPolicy::isCompressedBlockAckNeeded(const PtrgetFragmentNumber() != 0) - return false; - return agreement != nullptr && agreement->getIsAddbaResponseSent() && !agreement->getIsDelayedBlockAckPolicySupported(); + return isAcceptedOneTidBlockAckReq(blockAckReq, agreement); } // diff --git a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc index 6cef6428f18..8c657646d86 100644 --- a/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc +++ b/src/inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.cc @@ -249,19 +249,19 @@ IRecipientQosMacDataService::ManagementFrameReceptionResult RecipientQosMacDataS std::vector RecipientQosMacDataService::controlFrameReceived(Packet *controlPacket, const Ptr& controlHeader, IRecipientBlockAckAgreementHandler *blockAckAgreementHandler) { Enter_Method("controlFrameReceived"); + auto blockAckReqDetails = getOneTidBlockAckReqDetails(controlHeader); + RecipientBlockAckAgreement *agreement = nullptr; + if (blockAckReqDetails) { + auto originatorAddr = blockAckReqDetails->blockAckReq->getTransmitterAddress(); + agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(blockAckReqDetails->tid, originatorAddr); + if (!isAcceptedOneTidBlockAckReq(blockAckReqDetails->blockAckReq, agreement)) + return {}; + } expireReceiveLifetime(); - if (auto blockAckReqDetails = getOneTidBlockAckReqDetails(controlHeader)) { + if (blockAckReqDetails) { BlockAckReordering::ReorderBuffer frames; - if (blockAckReordering) { - MacAddress originatorAddr = blockAckReqDetails->blockAckReq->getTransmitterAddress(); - RecipientBlockAckAgreement *agreement = blockAckAgreementHandler == nullptr ? nullptr : blockAckAgreementHandler->getActiveAgreement(blockAckReqDetails->tid, originatorAddr); - if (agreement) - frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReqDetails->blockAckReq); - else { - scheduleReceiveLifetimeTimer(); - return std::vector(); - } - } + if (blockAckReordering) + frames = blockAckReordering->processReceivedBlockAckReq(agreement, blockAckReqDetails->blockAckReq); std::vector defragmentedFrames; if (basicReassembly) { // FIXME defragmentation for (auto it : frames) { diff --git a/tests/module/Ieee80211CompressedBlockAckRuntime.test b/tests/module/Ieee80211CompressedBlockAckRuntime.test index 039df324eb4..7383d8ed01f 100644 --- a/tests/module/Ieee80211CompressedBlockAckRuntime.test +++ b/tests/module/Ieee80211CompressedBlockAckRuntime.test @@ -23,11 +23,15 @@ sim-time-limit = 1.05s **.opMode = "n(mixed-2.4Ghz)" **.originatorBlockAckAgreementPolicy.localCompressedBlockAckSupported = true +**.recipientBlockAckAgreementPolicy.localCompressedBlockAckSupported = true *.cliHost.wlan[*].address = "10:00:00:00:00:01" *.srvHost.wlan[*].address = "10:00:00:00:00:02" *.cliHost.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:00" +*.cliHost.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:00" *.ap.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:01 10:00:00:00:00:02" +*.ap.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:01 10:00:00:00:00:02" *.srvHost.wlan[*].mac.hcf.originatorBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:00" +*.srvHost.wlan[*].mac.hcf.recipientBlockAckAgreementPolicy.compressedBlockAckPeerAddresses = "10:00:00:00:00:00" **.cmdenv-log-level = info %contains: stdout diff --git a/tests/module/Ieee80211CompressedBlockAckValidation.test b/tests/module/Ieee80211CompressedBlockAckValidation.test new file mode 100644 index 00000000000..b8eb4b6e752 --- /dev/null +++ b/tests/module/Ieee80211CompressedBlockAckValidation.test @@ -0,0 +1,256 @@ +%description: +Validate compressed BARs before receive-window mutation and response generation, +with configured peer capability and HT operation snapshotted at ADDBA acceptance. +Sparse and wrapping transmit sets must never strand frames beyond bitmap offset 63. +IEEE Std 802.11-2024, 9.3.1.7.2 and 10.25.6.1 (non-HE bitmap length 64). + +%file: TestCompressedBlockAckValidation.cc +#include + +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" +#include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosAckPolicy.h" +#include "inet/linklayer/ieee80211/mac/recipient/RecipientQosMacDataService.h" + +using namespace inet; +using namespace inet::ieee80211; + +static const MacAddress PEER("02:00:00:00:00:01"); +static const MacAddress LOCAL("02:00:00:00:00:02"); + +class InspectReordering : public BlockAckReordering +{ + public: + ReceiveBuffer *getBuffer() { return receiveBuffers.at(std::make_pair(5, PEER)); } +}; + +class InspectDataService : public RecipientQosMacDataService +{ + protected: + virtual void initialize() override { + RecipientQosMacDataService::initialize(); + delete blockAckReordering; + blockAckReordering = new InspectReordering(); + } + public: + ReceiveBuffer *getBuffer() { return static_cast(blockAckReordering)->getBuffer(); } +}; +Define_Module(InspectDataService); + +class RecipientPolicy : public RecipientBlockAckAgreementPolicy +{ + public: + void configure(bool localSupport, bool peerSupport, bool ht) { + maximumAllowedBufferSize = 64; + blockAckTimeoutValue = 0; + localCompressedBlockAckSupported = localSupport; + compressedBlockAckPeerAddresses.clear(); + if (peerSupport) + compressedBlockAckPeerAddresses.insert(PEER); + modeSet = const_cast(physicallayer::Ieee80211ModeSet::getModeSet(ht ? "n(mixed-2.4Ghz)" : "g(mixed)")); + } +}; + +class TestRecipientAckPolicy : public RecipientQosAckPolicy +{ + public: + virtual simtime_t computeBlockAckDurationField(Packet *, const Ptr&) const override { return 0; } +}; + +class Callback : public IProcedureCallback +{ + public: + int responses = 0; + virtual void processMgmtFrame(Packet *packet, const Ptr&) override { delete packet; } + virtual void transmitControlResponseFrame(Packet *packet, const Ptr& header, Packet *, const Ptr&) override { + ASSERT(dynamicPtrCast(header) != nullptr); + responses++; + delete packet; + } +}; + +class CompressedBlockAckValidation : public SimpleModule +{ + protected: + Ptr dataHeader(int sequence, int fragment = 0, bool more = false) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setTransmitterAddress(PEER); + header->setReceiverAddress(LOCAL); + header->setTid(5); + header->setAckPolicy(BLOCK_ACK); + header->setSequenceNumber(SequenceNumberCyclic(sequence)); + header->setFragmentNumber(fragment); + header->setMoreFragments(more); + return header; + } + + void checkRecipient(bool localSupport, bool peerSupport, bool ht, int fragment) { + auto service = check_and_cast(cModuleType::get("InspectDataService")->create("service", this)); + service->callInitialize(); + RecipientBlockAckAgreementHandler handler; + RecipientPolicy policy; + policy.configure(localSupport, peerSupport, ht); + Callback callback; + auto addba = makeShared(); + addba->setTransmitterAddress(PEER); + addba->setReceiverAddress(LOCAL); + addba->setTid(5); + addba->setDialogToken(1); + addba->setBufferSize(64); + addba->setStartingSequenceNumber(SequenceNumberCyclic(0)); + addba->setBlockAckPolicy(true); + addba->setBlockAckTimeoutValue(0); + auto agreement = handler.processReceivedAddbaRequest(addba, &policy, &callback, nullptr); + ASSERT(agreement != nullptr); + ASSERT(agreement->getIsCompressedBlockAckSupported() == (localSupport && peerSupport && ht)); + // Subsequent policy changes do not retroactively change this agreement. + policy.configure(!localSupport, !peerSupport, !ht); + ASSERT(agreement->getIsCompressedBlockAckSupported() == (localSupport && peerSupport && ht)); + + auto completeHeader = dataHeader(1); + auto incompleteHeader = dataHeader(2, 0, true); + auto complete = new Packet("complete", completeHeader); + auto incomplete = new Packet("incomplete", incompleteHeader); + ASSERT(service->dataFrameReceived(complete, completeHeader, &handler).empty()); + ASSERT(service->dataFrameReceived(incomplete, incompleteHeader, &handler).empty()); + auto buffer = service->getBuffer(); + auto savedBuffer = buffer->getBuffer(); + auto savedCursor = buffer->getNextExpectedSequenceNumber(); + auto savedStart = agreement->getStartingSequenceNumber(); + auto request = makeShared(); + request->setTransmitterAddress(PEER); + request->setReceiverAddress(LOCAL); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(3)); + request->setFragmentNumber(fragment); + auto packet = new Packet("rejectedBar", request); + ASSERT(service->controlFrameReceived(packet, request, &handler).empty()); + TestRecipientAckPolicy ackPolicy; + RecipientBlockAckProcedure procedure; + procedure.processReceivedBlockAckReq(packet, request, &ackPolicy, &handler, &callback); + ASSERT(callback.responses == 0); + ASSERT(buffer->getBuffer() == savedBuffer); + ASSERT(buffer->getLength() == 2); + ASSERT(buffer->getNextExpectedSequenceNumber() == savedCursor); + ASSERT(agreement->getStartingSequenceNumber() == savedStart); + ASSERT(agreement->getBlockAckRecord()->getCompressedAckState(SequenceNumberCyclic(1))); + ASSERT(agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(2), 0)); + ASSERT(!agreement->getBlockAckRecord()->getAckState(SequenceNumberCyclic(2), 1)); + delete packet; + + if (localSupport && peerSupport && ht) { + auto valid = makeShared(*request); + valid->setFragmentNumber(0); + auto validPacket = new Packet("validBar", valid); + auto released = service->controlFrameReceived(validPacket, valid, &handler); + ASSERT(released.size() == 1 && released.front() == complete); + ASSERT(buffer->getLength() == 0); + ASSERT(agreement->getStartingSequenceNumber() == SequenceNumberCyclic(3)); + procedure.processReceivedBlockAckReq(validPacket, valid, &ackPolicy, &handler, &callback); + ASSERT(callback.responses == 1); + for (auto frame : released) { take(frame); delete frame; } + delete validPacket; + } + service->deleteModule(); + } + + void checkSparse(int start, bool timeout) { + OriginatorBlockAckAgreement agreement(LOCAL, 5, SequenceNumberCyclic(start), 64, false, false, 0, 1); + agreement.setIsAddbaResponseReceived(true); + agreement.setIsCompressedBlockAckSupported(true); + OriginatorQosAckPolicy policy; + QosAckHandler handler; + std::vector frames; + for (int offset : {0, 63, 64, 100}) { + auto header = dataHeader((start + offset) % 4096); + frames.push_back(new Packet("outstanding", header)); + handler.frameGotInProgress(header); + handler.processTransmittedDataOrMgmtFrame(header); + } + ASSERT(policy.isCompressedBlockAckReq({frames[0], frames[1]}, &agreement)); + ASSERT(!policy.isCompressedBlockAckReq({frames[0], frames[2]}, &agreement)); + ASSERT(!policy.isCompressedBlockAckReq({frames[0], frames[3]}, &agreement)); + ASSERT(!policy.isCompressedBlockAckReq(frames, &agreement)); + auto bar = makeShared(); + bar->setReceiverAddress(LOCAL); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(start)); + handler.processTransmittedBlockAckReq(bar); + for (int i = 0; i < 4; i++) + ASSERT(handler.getQoSDataAckStatus(frames[i]->peekAtFront()) == + (i < 2 ? QosAckHandler::Status::WAITING_FOR_BLOCK_ACK : QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED)); + if (timeout) + handler.processFailedBlockAckReq(bar); + else { + auto ba = makeShared(); + ba->setTransmitterAddress(LOCAL); + ba->setTidInfo(5); + ba->setStartingSequenceNumber(SequenceNumberCyclic(start)); + ba->setBlockAckBitmap(BitVector(std::vector(8, 255))); + ASSERT(handler.processReceivedBlockAck(ba).size() == 2); + } + for (int i = 0; i < 4; i++) { + auto header = frames[i]->peekAtFront(); + ASSERT(handler.getQoSDataAckStatus(header) == (i >= 2 ? QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED : + timeout ? QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED : QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED)); + if (i >= 2) + ASSERT(handler.isOutstandingFrame(header)); + } + // A later BAR can request both residual frames. + auto nextBar = makeShared(*bar); + nextBar->setStartingSequenceNumber(SequenceNumberCyclic((start + 64) % 4096)); + handler.processTransmittedBlockAckReq(nextBar); + for (int i : {2, 3}) + ASSERT(handler.getQoSDataAckStatus(frames[i]->peekAtFront()) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + for (auto frame : frames) + delete frame; + } + + virtual void initialize() override { + checkRecipient(true, true, true, 1); + checkRecipient(true, false, true, 0); + checkRecipient(true, true, false, 0); + checkRecipient(false, true, true, 0); + for (int start : {0, 4090}) + for (bool timeout : {false, true}) + checkSparse(start, timeout); + std::cout << "Compressed BAR validation preserves receive state and limits transmit state to 64 positions.\n"; + } +}; +Define_Module(CompressedBlockAckValidation); + +%file: test.ned +import inet.common.SimpleModule; +import inet.linklayer.ieee80211.mac.recipient.RecipientQosMacDataService; +module InspectDataService extends RecipientQosMacDataService +{ + parameters: + @class(::InspectDataService); +} +simple CompressedBlockAckValidation extends SimpleModule +{ + parameters: + @class(::CompressedBlockAckValidation); +} +network TestNetwork +{ + submodules: + test: CompressedBlockAckValidation; +} + +%inifile: omnetpp.ini +[General] +network = TestNetwork +ned-path = .;../../../../src;../../lib +seed-set = 0 +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +%contains: stdout +Compressed BAR validation preserves receive state and limits transmit state to 64 positions. diff --git a/tests/unit/Ieee80211AddbaTransaction_1.test b/tests/unit/Ieee80211AddbaTransaction_1.test index ebecf13555f..c0a83da6e03 100644 --- a/tests/unit/Ieee80211AddbaTransaction_1.test +++ b/tests/unit/Ieee80211AddbaTransaction_1.test @@ -93,6 +93,7 @@ class TestRecipientPolicy : public IRecipientBlockAckAgreementPolicy virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } virtual bool aMsduSupported() const override { return aMsduSupportedValue; } virtual bool delayedBlockAckPolicySupported() const override { return delayedBlockAckPolicySupportedValue; } + virtual bool isPeerCompressedBlockAckSupported(const MacAddress&) const override { return false; } virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } }; diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index 411bf47b060..d584316d8e5 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -157,6 +157,7 @@ static Packet *makeOutstandingQosFrame(MacAddress receiverAddress, Tid tid, Frag header->setType(ST_DATA_WITH_QOS); header->setReceiverAddress(receiverAddress); header->setTid(tid); + header->setSequenceNumber(SequenceNumberCyclic(100)); header->setFragmentNumber(fragmentNumber); header->setMoreFragments(moreFragments); return new Packet("outstanding", header); @@ -267,6 +268,7 @@ EV << "Cyclic sequence arithmetic wraps through zero without invalid intermediat TestRecipientBlockAckProcedure procedure; RecipientBlockAckAgreement emptyAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); emptyAgreement.addbaResposneSent(); + emptyAgreement.setIsCompressedBlockAckSupported(true); auto request = makeCompressedBlockAckReq(); request->setStartingSequenceNumber(SequenceNumberCyclic(100)); ASSERT(TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &emptyAgreement)); @@ -682,6 +684,7 @@ EV << "Cyclic sequence arithmetic wraps through zero without invalid intermediat RecipientBlockAckAgreement delayedAgreement(MacAddress("11:22:33:44:55:66"), 5, SequenceNumberCyclic(100), 64, SIMTIME_ZERO); ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement)); delayedAgreement.addbaResposneSent(); + delayedAgreement.setIsCompressedBlockAckSupported(true); delayedAgreement.setIsDelayedBlockAckPolicySupported(true); ASSERT(!TestRecipientQosAckPolicy::isCompressedResponseNeeded(request, &delayedAgreement)); delayedAgreement.setIsDelayedBlockAckPolicySupported(false); From dd0d34178e3b727e5b9410b79d5bb31c185ecabf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 6 Sep 2026 12:01:06 +0200 Subject: [PATCH 52/56] ieee80211: Reject ineligible BARs before renewing agreement lifetime Malformed compressed BARs were rejected by receive-state and response processing but still renewed the recipient inactivity deadline through blockAckReqReceived. Apply the shared eligibility check in the agreement handler before updating the deadline or scheduling the inactivity timer. Extend the module regression with elapsed simulation time to verify that malformed and capability-ineligible BARs preserve the existing deadline, while valid compressed and Basic BARs still renew it. The new assertion fails before the fix and passes afterward. Validation: debug build, compressed BAR validation and inactivity timer module tests, ADDBA transaction unit test, scoped architecture check, and diff whitespace check pass. --- .../RecipientBlockAckAgreementHandler.cc | 3 +- ...Ieee80211CompressedBlockAckValidation.test | 59 +++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc index e2143a548e5..a2d9f4e47c3 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.cc @@ -7,6 +7,7 @@ #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreementHandler.h" +#include "inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.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" @@ -61,7 +62,7 @@ void RecipientBlockAckAgreementHandler::blockAckReqReceived(const PtrgetTransmitterAddress()); - if (agreement != nullptr) { + if (isAcceptedOneTidBlockAckReq(blockAckReq, agreement)) { agreement->calculateExpirationTime(); scheduleInactivityTimer(callback); } diff --git a/tests/module/Ieee80211CompressedBlockAckValidation.test b/tests/module/Ieee80211CompressedBlockAckValidation.test index b8eb4b6e752..f48145f2844 100644 --- a/tests/module/Ieee80211CompressedBlockAckValidation.test +++ b/tests/module/Ieee80211CompressedBlockAckValidation.test @@ -76,6 +76,8 @@ class Callback : public IProcedureCallback class CompressedBlockAckValidation : public SimpleModule { protected: + RecipientBlockAckAgreementHandler lifetimeHandler; + RecipientBlockAckAgreement *lifetimeAgreement = nullptr; Ptr dataHeader(int sequence, int fragment = 0, bool more = false) { auto header = makeShared(); header->setType(ST_DATA_WITH_QOS); @@ -211,7 +213,61 @@ class CompressedBlockAckValidation : public SimpleModule delete frame; } + virtual void handleMessage(cMessage *message) override { + auto savedDeadline = lifetimeAgreement->getExpirationTime(); + ASSERT(savedDeadline > simTime()); + if (message->getKind() == 0) { + auto request = makeShared(); + request->setTransmitterAddress(PEER); + request->setReceiverAddress(LOCAL); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request->setFragmentNumber(1); + lifetimeHandler.blockAckReqReceived(request, nullptr); + ASSERT(lifetimeAgreement->getExpirationTime() == savedDeadline); + + request->setFragmentNumber(0); + lifetimeAgreement->setIsCompressedBlockAckSupported(false); + lifetimeHandler.blockAckReqReceived(request, nullptr); + ASSERT(lifetimeAgreement->getExpirationTime() == savedDeadline); + + lifetimeAgreement->setIsCompressedBlockAckSupported(true); + lifetimeHandler.blockAckReqReceived(request, nullptr); + ASSERT(lifetimeAgreement->getExpirationTime() == simTime() + SimTime(1)); + ASSERT(lifetimeAgreement->getExpirationTime() > savedDeadline); + message->setKind(1); + scheduleAt(simTime() + SimTime(1, SIMTIME_MS), message); + } + else { + auto request = makeShared(); + request->setTransmitterAddress(PEER); + request->setReceiverAddress(LOCAL); + request->setTidInfo(5); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + lifetimeHandler.blockAckReqReceived(request, nullptr); + ASSERT(lifetimeAgreement->getExpirationTime() == simTime() + SimTime(1)); + ASSERT(lifetimeAgreement->getExpirationTime() > savedDeadline); + delete message; + std::cout << "Rejected BARs preserve inactivity deadlines; valid BARs renew them.\n"; + } + } + virtual void initialize() override { + RecipientPolicy policy; + policy.configure(true, true, true); + Callback callback; + auto request = makeShared(); + request->setTransmitterAddress(PEER); + request->setReceiverAddress(LOCAL); + request->setTid(5); + request->setDialogToken(1); + request->setBufferSize(64); + request->setStartingSequenceNumber(SequenceNumberCyclic(0)); + request->setBlockAckPolicy(true); + request->setBlockAckTimeoutValue(SimTime(1)); + lifetimeAgreement = lifetimeHandler.processReceivedAddbaRequest(request, &policy, &callback, nullptr); + ASSERT(lifetimeAgreement->getExpirationTime() == SimTime(1)); + scheduleAt(SimTime(1, SIMTIME_MS), new cMessage("checkBarLifetime")); checkRecipient(true, true, true, 1); checkRecipient(true, false, true, 0); checkRecipient(true, true, false, 0); @@ -254,3 +310,6 @@ record-scalar-results = false %contains: stdout Compressed BAR validation preserves receive state and limits transmit state to 64 positions. + +%contains: stdout +Rejected BARs preserve inactivity deadlines; valid BARs renew them. From 36d851d54a0a26ba7a382202c07d74ca45c406d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 7 Sep 2026 00:29:34 +0200 Subject: [PATCH 53/56] ieee80211: correlate Block Ack responses by starting sequence A delayed Block Ack from an earlier BAR to the same peer and TID can complete a later receive step and acknowledge its outstanding frames. Require the response starting sequence number to match the active Basic or Compressed BAR. Extend the HCF response correlation test with successive BARs whose bitmap windows overlap. Assert that the stale response preserves the timeout and acknowledgment statuses, and that a matching response still succeeds. --- .../mac/blockack/OneTidBlockAckReqVariant.h | 8 +- ...eee80211BlockAckResponseCorrelation_1.test | 73 ++++++++++++++++--- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h index f0ed03e382a..b46c18cbb36 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OneTidBlockAckReqVariant.h @@ -57,18 +57,20 @@ inline bool isAcceptedOneTidBlockAckReq(const Ptr& r } // IEEE Std 802.11-2024, 9.3.1.7, 9.3.1.8, and 10.25.5: correlate the BAR RA -// with the BA TA, TID, and selected BlockAck variant before accepting a response. +// with the BA TA, TID, starting sequence number, and selected BlockAck variant. inline bool isMatchingOneTidBlockAckResponse(const OneTidBlockAckReqDetails& blockAckReqDetails, const Ptr& blockAck) { if (blockAckReqDetails.blockAckReq->getReceiverAddress() != blockAck->getTransmitterAddress()) return false; if (blockAckReqDetails.variant == OneTidBlockAckReqVariant::BASIC) { auto basicBlockAck = dynamicPtrCast(blockAck); - return basicBlockAck != nullptr && basicBlockAck->getTidInfo() == blockAckReqDetails.tid; + return basicBlockAck != nullptr && basicBlockAck->getTidInfo() == blockAckReqDetails.tid && + basicBlockAck->getStartingSequenceNumber() == blockAckReqDetails.startingSequenceNumber; } else { auto compressedBlockAck = dynamicPtrCast(blockAck); - return compressedBlockAck != nullptr && compressedBlockAck->getTidInfo() == blockAckReqDetails.tid; + return compressedBlockAck != nullptr && compressedBlockAck->getTidInfo() == blockAckReqDetails.tid && + compressedBlockAck->getStartingSequenceNumber() == blockAckReqDetails.startingSequenceNumber; } } diff --git a/tests/module/Ieee80211BlockAckResponseCorrelation_1.test b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test index 64255a2e979..9f991fe50c7 100644 --- a/tests/module/Ieee80211BlockAckResponseCorrelation_1.test +++ b/tests/module/Ieee80211BlockAckResponseCorrelation_1.test @@ -2,7 +2,7 @@ Exercise the live HCF lower-frame entry while a Block Ack response step is waiting. An ACK, CTS, data frame, or other control frame addressed to this station, as well as a Block Ack for another active peer, the wrong TID, or the -wrong Basic/Compressed variant, must leave the current response wait and its +wrong Basic/Compressed variant or starting sequence number, must leave the current response wait and its timeout armed. The matching response then completes that same frame sequence, while an unmatched wait ends only when its response timer expires. @@ -25,6 +25,7 @@ while an unmatched wait ends only when its response timer expires. #include "inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h" #include "inet/linklayer/ieee80211/mac/queue/InProgressFrames.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" +#include "inet/linklayer/ieee80211/mac/originator/QosAckHandler.h" namespace inet { namespace ieee80211 { @@ -53,6 +54,7 @@ class TestOriginatorQosAckPolicy : public IOriginatorQoSAckPolicy { public: bool compressed = true; + SequenceNumber startingSequenceNumber = CURRENT_SSN; virtual bool isAckNeeded(const Ptr&) const override { return true; } virtual AckPolicy computeAckPolicy(Packet *, const Ptr&, OriginatorBlockAckAgreement *) const override { return BLOCK_ACK; } @@ -60,7 +62,7 @@ class TestOriginatorQosAckPolicy : public IOriginatorQoSAckPolicy virtual bool isBlockAckPolicyEligibleFrame(Packet *, const Ptr&) const override { return true; } virtual std::tuple computeBlockAckReqParameters(InProgressFrames *, TxopProcedure *, IOriginatorBlockAckAgreementHandler *) const override { - return std::make_tuple(PEER_ONE, SequenceNumberCyclic(CURRENT_SSN), CURRENT_TID); + return std::make_tuple(PEER_ONE, SequenceNumberCyclic(startingSequenceNumber), CURRENT_TID); } virtual bool isCompressedBlockAckReq(const std::vector&, OriginatorBlockAckAgreement *) const override { return compressed; } virtual simtime_t getAckTimeout(Packet *, const Ptr&) const override { return SimTime(1, SIMTIME_MS); } @@ -139,9 +141,20 @@ class TestHcfBlockAckResponseCorrelation : public Hcf frameSequenceHandler->transmissionComplete(); } - virtual void originatorProcessTransmittedFrame(Packet *) override {} - virtual void originatorProcessReceivedFrame(Packet *, Packet *) override { acceptedResponses++; } - virtual void originatorProcessFailedFrame(Packet *) override { failedFrames++; } + virtual void originatorProcessTransmittedFrame(Packet *packet) override + { + ackHandler.processTransmittedBlockAckReq(packet->peekAtFront()); + } + virtual void originatorProcessReceivedFrame(Packet *packet, Packet *) override + { + acceptedResponses++; + ackHandler.processReceivedBlockAck(packet->peekAtFront()); + } + virtual void originatorProcessFailedFrame(Packet *packet) override + { + failedFrames++; + ackHandler.processFailedBlockAckReq(packet->peekAtFront()); + } virtual void frameSequenceFinished() override { finishedSequences++; @@ -154,6 +167,7 @@ class TestHcfBlockAckResponseCorrelation : public Hcf } public: + QosAckHandler ackHandler; int transmittedFrames = 0; int acceptedResponses = 0; int failedFrames = 0; @@ -171,20 +185,23 @@ class TestHcfBlockAckResponseCorrelation : public Hcf frameSequenceHandler->startFrameSequence(new BlockAckReqBlockAckFs(), context, this); } - void injectBlockAck(const MacAddress& transmitterAddress, Tid tid, bool compressed) + void injectBlockAck(const MacAddress& transmitterAddress, Tid tid, bool compressed, SequenceNumber startingSequenceNumber = CURRENT_SSN) { Enter_Method("injectBlockAck"); Ptr blockAck; if (compressed) { auto compressedBlockAck = makeShared(); compressedBlockAck->setTidInfo(tid); - compressedBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(CURRENT_SSN)); + compressedBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(startingSequenceNumber)); + compressedBlockAck->setBlockAckBitmap(BitVector(std::vector(8, 255))); blockAck = compressedBlockAck; } else { auto basicBlockAck = makeShared(); basicBlockAck->setTidInfo(tid); - basicBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(CURRENT_SSN)); + basicBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(startingSequenceNumber)); + for (int i = 0; i < 64; i++) + basicBlockAck->setBlockAckBitmap(i, BitVector(std::vector(2, 255))); blockAck = basicBlockAck; } blockAck->setReceiverAddress(localAddress); @@ -348,6 +365,21 @@ class Ieee80211BlockAckResponseCorrelationTest : public cSimpleModule ASSERT(hcf->failedFrames == expectedFailedFrames); ASSERT(hcf->finishedSequences == expectedFinishedSequences); + // The next BAR advances by one, so the old response bitmap still + // covers these frames and would incorrectly acknowledge them. + ackPolicy.startingSequenceNumber = CURRENT_SSN + 1; + std::vector> waitingHeaders; + for (int offset : {1, 2}) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(PEER_ONE); + header->setTid(CURRENT_TID); + header->setSequenceNumber(SequenceNumberCyclic(CURRENT_SSN + offset)); + header->setAckPolicy(BLOCK_ACK); + hcf->ackHandler.frameGotInProgress(header); + hcf->ackHandler.processTransmittedDataOrMgmtFrame(header); + waitingHeaders.push_back(header); + } // A fresh BAR wait with no matching response must stay alive // through the same unrelated traffic and finish only at timeout. expectedTransmittedFrames = hcf->transmittedFrames + 1; @@ -373,7 +405,13 @@ class Ieee80211BlockAckResponseCorrelationTest : public cSimpleModule ASSERT(hcf->finishedSequences == expectedFinishedSequences - 1); ASSERT(hcf->failedFrames == expectedFailedFrames - 1); + for (auto header : waitingHeaders) + ASSERT(hcf->ackHandler.getQoSDataAckStatus(header) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); wait(SimTime(500, SIMTIME_US)); + // A delayed duplicate of the first BA arrives during the second wait. + hcf->injectBlockAck(PEER_ONE, CURRENT_TID, compressed, CURRENT_SSN); + for (auto header : waitingHeaders) + ASSERT(hcf->ackHandler.getQoSDataAckStatus(header) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); hcf->injectAck(); ASSERT(hcf->isResponseWaitActive()); ASSERT(hcf->isResponseTimerScheduled()); @@ -390,8 +428,23 @@ class Ieee80211BlockAckResponseCorrelationTest : public cSimpleModule ASSERT(hcf->finishedSequences == expectedFinishedSequences); ASSERT(hcf->failedFrames == expectedFailedFrames); ASSERT(hcf->lastFinishedAt == timeoutDeadline); + for (auto header : waitingHeaders) { + ASSERT(hcf->ackHandler.getQoSDataAckStatus(header) == QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED); + hcf->ackHandler.processTransmittedDataOrMgmtFrame(header); + } + // A response with the current SSN must still complete the receive + // step and update the same acknowledgment statuses. + hcf->startBlockAckResponseWait(&ackPolicy, &blockAckProcedure, &agreementHandler, &inProgressFrames, &txopProcedure); + hcf->injectBlockAck(PEER_ONE, CURRENT_TID, compressed, ackPolicy.startingSequenceNumber); + ASSERT(!hcf->isResponseWaitActive()); + ASSERT(!hcf->isResponseTimerScheduled()); + ASSERT(hcf->acceptedResponses == expectedAcceptedResponses + 1); + for (auto header : waitingHeaders) { + ASSERT(hcf->ackHandler.getQoSDataAckStatus(header) == QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED); + hcf->ackHandler.dropFrame(header); + } } - std::cout << "HCF keeps BAR response waits scoped by expected type, peer, TID, and variant until match or timeout.\n"; + std::cout << "HCF keeps BAR response waits scoped by expected type, peer, TID, SSN, and variant until match or timeout.\n"; endSimulation(); } }; @@ -452,4 +505,4 @@ record-scalar-results = false **.mobility.constraintAreaMaxZ = 0m %contains: stdout -HCF keeps BAR response waits scoped by expected type, peer, TID, and variant until match or timeout. +HCF keeps BAR response waits scoped by expected type, peer, TID, SSN, and variant until match or timeout. From 4418f5d7432e668058499382c0a3816371101911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 7 Sep 2026 00:29:38 +0200 Subject: [PATCH 54/56] ieee80211: require HT operation for compressed Block Ack Local and per-peer configuration can enable compressed Block Ack agreements even when the active mode set is legacy. Require a non-null mode set supporting HT operation before advertising peer compressed Block Ack support. Cover capability checks and agreement creation with null, legacy a/b/g, and HT mode sets while retaining local and per-peer gating coverage. --- .../mac/blockack/OriginatorBlockAckAgreementPolicy.h | 2 +- tests/unit/Ieee80211CompressedBlockAck_1.test | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h index 10cccd96635..142ed226336 100644 --- a/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h +++ b/src/inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h @@ -45,7 +45,7 @@ class INET_API OriginatorBlockAckAgreementPolicy : public ModeSetListener, publi virtual bool isMsduSupported() const override { return aMsduSupported; } virtual simtime_t getBlockAckTimeoutValue() const override { return blockAckTimeoutValue; } virtual bool isDelayedAckPolicySupported() const override { return delayedAckPolicySupported; } - virtual bool isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const override { return localCompressedBlockAckSupported && compressedBlockAckPeerAddresses.find(peerAddress) != compressedBlockAckPeerAddresses.end(); } + virtual bool isPeerCompressedBlockAckSupported(const MacAddress& peerAddress) const override { return modeSet != nullptr && modeSet->isHtOperationSupported() && localCompressedBlockAckSupported && compressedBlockAckPeerAddresses.find(peerAddress) != compressedBlockAckPeerAddresses.end(); } virtual int getMaximumAllowedBufferSize() const override { return maximumAllowedBufferSize; } }; diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index d584316d8e5..c8c5cb7c469 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -73,6 +73,7 @@ class TestOriginatorBlockAckAgreementPolicy : public OriginatorBlockAckAgreement public: void configureCompressedBlockAckCapability(bool localSupport, std::initializer_list peerAddresses) { + applyModeSet(physicallayer::Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")); localCompressedBlockAckSupported = localSupport; compressedBlockAckPeerAddresses = std::set(peerAddresses); } @@ -721,6 +722,17 @@ EV << "Cyclic sequence arithmetic wraps through zero without invalid intermediat ASSERT(!handler.getAgreement(unlistedPeer, 5)->getIsCompressedBlockAckSupported()); ASSERT(handler.getAgreement(positivePeer, 5)->getIsCompressedBlockAckSupported()); + // Configuration alone must not activate compressed BA in a legacy mode + // or before the active mode set has been supplied. + for (const char *mode : std::initializer_list {nullptr, "a", "b", "g(mixed)", "n(mixed-2.4Ghz)"}) { + localEnabledPolicy.applyModeSet(mode == nullptr ? nullptr : physicallayer::Ieee80211ModeSet::getModeSet(mode)); + bool expected = mode != nullptr && localEnabledPolicy.getModeSet()->isHtOperationSupported(); + ASSERT(localEnabledPolicy.isPeerCompressedBlockAckSupported(listedPeer) == expected); + TestOriginatorBlockAckAgreementHandler modeHandler; + modeHandler.createAgreement(makeAddbaRequest(listedPeer), 1, &localEnabledPolicy); + ASSERT(modeHandler.getAgreement(listedPeer, 5)->getIsCompressedBlockAckSupported() == expected); + } + auto response = makeShared(); response->setTransmitterAddress(listedPeer); response->setBlockAckPolicy(true); From 17b47de4706fa803111cdaf254ae0f6a9c778236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 7 Sep 2026 00:38:42 +0200 Subject: [PATCH 55/56] ieee80211: gate compressed BAR selection on the active mode Established Block Ack agreements retain their compressed-support flag when the mode set changes. Check the current HT capability at request selection so a switch to a legacy mode immediately selects Basic BAR. Exercise the production frame-sequence selector across HT-to-a/b/g transitions with the same agreement, including return to HT and an absent mode set. The transition assertion fails before the fix. The debug build, focused compressed Block Ack unit and runtime module tests, and scoped architecture checks pass. --- .../mac/originator/OriginatorQosAckPolicy.cc | 4 +- tests/unit/Ieee80211CompressedBlockAck_1.test | 45 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc index 3df9f756595..73ec29d509f 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.cc @@ -65,7 +65,9 @@ SequenceNumberCyclic OriginatorQosAckPolicy::computeStartingSequenceNumber(const bool OriginatorQosAckPolicy::isCompressedBlockAckReq(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) const { - return isCompressedBlockAckReqNeeded(outstandingFrames, agreement); + // Agreements retain their negotiated capability across mode-set changes. + // Recheck the active mode whenever selecting a new BAR variant. + return modeSet != nullptr && modeSet->isHtOperationSupported() && isCompressedBlockAckReqNeeded(outstandingFrames, agreement); } bool OriginatorQosAckPolicy::isCompressedBlockAckReqNeeded(const std::vector& outstandingFrames, OriginatorBlockAckAgreement *agreement) diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index c8c5cb7c469..32a611c526a 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -10,10 +10,13 @@ IEEE Std 802.11-2024, 9.3.1.7.2, 9.3.1.8.2, 10.25.6.1, and 10.25.6.5. #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementHandler.h" #include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckAgreementPolicy.h" +#include "inet/linklayer/ieee80211/mac/blockack/OriginatorBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckAgreement.h" #include "inet/linklayer/ieee80211/mac/blockack/RecipientBlockAckProcedure.h" #include "inet/linklayer/ieee80211/mac/blockackreordering/BlockAckReordering.h" #include "inet/linklayer/ieee80211/mac/coordinationfunction/Hcf.h" +#include "inet/linklayer/ieee80211/mac/framesequence/PrimitiveFrameSequences.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/originator/OriginatorQosAckPolicy.h" #include "inet/linklayer/ieee80211/mac/originator/TxopProcedure.h" #include "inet/linklayer/ieee80211/mac/rateselection/QosRateSelection.h" @@ -112,6 +115,14 @@ class TestOriginatorQosAckPolicy : public OriginatorQosAckPolicy } }; +class OutstandingFramesFixture : public InProgressFrames +{ + public: + std::vector frames; + virtual std::vector getOutstandingFrames() override { return frames; } + virtual ~OutstandingFramesFixture() { for (auto frame : frames) delete frame; } +}; + class TestRecipientQosAckPolicy : public RecipientQosAckPolicy { public: @@ -741,6 +752,39 @@ EV << "Cyclic sequence arithmetic wraps through zero without invalid intermediat handler.updateAgreement(handler.getAgreement(listedPeer, 5), response, &localEnabledPolicy); ASSERT(handler.getAgreement(listedPeer, 5)->getIsCompressedBlockAckSupported()); EV << "Local and per-peer capability gates are snapshotted by agreements.\n"; + + // Exercise the production BAR selector with the same established agreement + // throughout runtime mode transitions, without refreshing its capability flag. + auto agreement = handler.getActiveAgreement(listedPeer, 5); + ASSERT(agreement != nullptr && agreement->getIsCompressedBlockAckSupported()); + OriginatorQosAckPolicy ackPolicy; + OriginatorBlockAckProcedure procedure; + OutstandingFramesFixture frames; + frames.frames.push_back(makeOutstandingQosFrame(listedPeer, 5)); + for (const char *mode : {"n(mixed-2.4Ghz)", "a", "n(mixed-2.4Ghz)", "b", "n(mixed-2.4Ghz)", "g(mixed)", "n(mixed-2.4Ghz)"}) { + auto modeSet = physicallayer::Ieee80211ModeSet::getModeSet(mode); + localEnabledPolicy.applyModeSet(modeSet); + ackPolicy.applyModeSet(modeSet); + ASSERT(handler.getActiveAgreement(listedPeer, 5) == agreement); + ASSERT(agreement->getIsCompressedBlockAckSupported()); + FrameSequenceContext context(MacAddress::UNSPECIFIED_ADDRESS, nullptr, &frames, nullptr, nullptr, nullptr, + new QoSContext(&ackPolicy, &procedure, &handler, nullptr)); + BlockAckReqBlockAckFs sequence; + sequence.startSequence(&context, 0); + auto step = sequence.prepareStep(&context); + context.addStep(step); + auto packet = check_and_cast(step)->getFrameToTransmit(); + auto details = getOneTidBlockAckReqDetails(packet->peekAtFront()); + ASSERT(details.has_value()); + ASSERT(details->tid == 5); + if (modeSet->isHtOperationSupported()) + ASSERT(dynamicPtrCast(packet->peekAtFront()) != nullptr); + else + ASSERT(dynamicPtrCast(packet->peekAtFront()) != nullptr); + } + ackPolicy.applyModeSet(nullptr); + ASSERT(!ackPolicy.isCompressedBlockAckReq(frames.frames, agreement)); + EV << "Existing HT agreements use Basic BAR immediately after legacy mode transitions.\n"; } { @@ -869,6 +913,7 @@ Compressed bitmap wraps from sequence 4095 to 0. Compressed bitmap preserves leading holes across sequence wrap. Recipient fragment and agreement-policy gates passed. Local and per-peer capability gates are snapshotted by agreements. +Existing HT agreements use Basic BAR immediately after legacy mode transitions. Originator capability, agreement, and fragmentation gates passed. Legacy Basic BAR and BA remain byte-exact. Accepted delayed agreement is retained for Basic BAR fallback. From 596ea06f7125ba74deed546f0d78bdb186be3017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 10 Sep 2026 01:29:14 +0200 Subject: [PATCH 56/56] ieee80211: limit Basic BAR acknowledgment transitions to 64 positions When a Basic BAR preceded sparse frames, processTransmittedBlockAckReq marked every frame with a sequence number at or above the starting sequence number as awaiting its response. Because processReceivedBlockAck and timeout handling update only the 64-sequence-number window of the Basic Block Ack bitmap, frames beyond offset 63 were never updated, becoming neither retransmittable nor eligible for subsequent requests. Limit the acknowledgment status transition for transmitted Basic BARs to the 64-sequence-number window using BlockAckWindow::isWithin, matching the Basic BlockAck bitmap coverage. Add unit and module regressions for sparse and wrapping sequence sets under Basic BAR, verifying that out-of-window frames remain outstanding and can be requested by subsequent BAR transmissions. Apply the active mode set to originator policy checks in the validation test. Validation: debug and release builds, focused unit and module tests, and scoped architecture, naming, and source-seal gates pass. --- .../ieee80211/mac/originator/QosAckHandler.cc | 3 +- ...Ieee80211CompressedBlockAckValidation.test | 53 ++++++++++++++++++- tests/unit/Ieee80211CompressedBlockAck_1.test | 52 ++++++++++++++++++ 3 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc index da2d2d05239..0d3183f7708 100644 --- a/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc +++ b/src/inet/linklayer/ieee80211/mac/originator/QosAckHandler.cc @@ -222,7 +222,8 @@ void QosAckHandler::processTransmittedBlockAckReq(const Ptr(blockAckReq)) { if (receiverAddress == blockAckReq->getReceiverAddress() && basicBlockAckReq->getTidInfo() == tid) { auto startingSeqNum = basicBlockAckReq->getStartingSequenceNumber(); - if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && SequenceNumberCyclic(seqCtrlField.getSequenceNumber()) >= startingSeqNum) + // IEEE Std 802.11-2024, 10.25.6.1: the Basic bitmap covers 64 sequence numbers. + if (status == Status::BLOCK_ACK_NOT_YET_REQUESTED && BlockAckWindow::isWithin(startingSeqNum, 64, SequenceNumberCyclic(seqCtrlField.getSequenceNumber()))) status = Status::WAITING_FOR_BLOCK_ACK; } } diff --git a/tests/module/Ieee80211CompressedBlockAckValidation.test b/tests/module/Ieee80211CompressedBlockAckValidation.test index f48145f2844..a136a9fb4e1 100644 --- a/tests/module/Ieee80211CompressedBlockAckValidation.test +++ b/tests/module/Ieee80211CompressedBlockAckValidation.test @@ -166,6 +166,7 @@ class CompressedBlockAckValidation : public SimpleModule agreement.setIsAddbaResponseReceived(true); agreement.setIsCompressedBlockAckSupported(true); OriginatorQosAckPolicy policy; + policy.applyModeSet(physicallayer::Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)")); QosAckHandler handler; std::vector frames; for (int offset : {0, 63, 64, 100}) { @@ -213,6 +214,51 @@ class CompressedBlockAckValidation : public SimpleModule delete frame; } + void checkSparseBasic(int start, bool timeout) { + QosAckHandler handler; + std::vector frames; + for (int offset : {0, 63, 64, 100}) { + auto header = dataHeader((start + offset) % 4096); + frames.push_back(new Packet("outstanding", header)); + handler.frameGotInProgress(header); + handler.processTransmittedDataOrMgmtFrame(header); + } + auto bar = makeShared(); + bar->setReceiverAddress(LOCAL); + bar->setTidInfo(5); + bar->setStartingSequenceNumber(SequenceNumberCyclic(start)); + handler.processTransmittedBlockAckReq(bar); + for (int i = 0; i < 4; i++) + ASSERT(handler.getQoSDataAckStatus(frames[i]->peekAtFront()) == + (i < 2 ? QosAckHandler::Status::WAITING_FOR_BLOCK_ACK : QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED)); + if (timeout) + handler.processFailedBlockAckReq(bar); + else { + auto ba = makeShared(); + ba->setTransmitterAddress(LOCAL); + ba->setTidInfo(5); + ba->setStartingSequenceNumber(SequenceNumberCyclic(start)); + for (int i = 0; i < 64; i++) + ba->setBlockAckBitmap(i, BitVector(std::vector(2, 255))); + ASSERT(handler.processReceivedBlockAck(ba).size() == 2); + } + for (int i = 0; i < 4; i++) { + auto header = frames[i]->peekAtFront(); + ASSERT(handler.getQoSDataAckStatus(header) == (i >= 2 ? QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED : + timeout ? QosAckHandler::Status::BLOCK_ACK_NOT_ARRIVED : QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED)); + if (i >= 2) + ASSERT(handler.isOutstandingFrame(header)); + } + // A later BAR can request both residual frames. + auto nextBar = makeShared(*bar); + nextBar->setStartingSequenceNumber(SequenceNumberCyclic((start + 64) % 4096)); + handler.processTransmittedBlockAckReq(nextBar); + for (int i : {2, 3}) + ASSERT(handler.getQoSDataAckStatus(frames[i]->peekAtFront()) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + for (auto frame : frames) + delete frame; + } + virtual void handleMessage(cMessage *message) override { auto savedDeadline = lifetimeAgreement->getExpirationTime(); ASSERT(savedDeadline > simTime()); @@ -272,9 +318,12 @@ class CompressedBlockAckValidation : public SimpleModule checkRecipient(true, false, true, 0); checkRecipient(true, true, false, 0); checkRecipient(false, true, true, 0); - for (int start : {0, 4090}) - for (bool timeout : {false, true}) + for (int start : {0, 4090}) { + for (bool timeout : {false, true}) { checkSparse(start, timeout); + checkSparseBasic(start, timeout); + } + } std::cout << "Compressed BAR validation preserves receive state and limits transmit state to 64 positions.\n"; } }; diff --git a/tests/unit/Ieee80211CompressedBlockAck_1.test b/tests/unit/Ieee80211CompressedBlockAck_1.test index 32a611c526a..182118732dd 100644 --- a/tests/unit/Ieee80211CompressedBlockAck_1.test +++ b/tests/unit/Ieee80211CompressedBlockAck_1.test @@ -884,6 +884,57 @@ EV << "Cyclic sequence arithmetic wraps through zero without invalid intermediat EV << "Basic and Compressed BAR rate selection is variant-agnostic.\n"; } +{ + // Sparse Basic Block Ack request limits transmitted-BAR transitions to the 64-sequence window. + QosAckHandler handler; + MacAddress receiver("10:20:30:40:50:60"); + std::vector> headers; + for (int offset : {0, 63, 64, 100}) { + auto header = makeShared(); + header->setType(ST_DATA_WITH_QOS); + header->setReceiverAddress(receiver); + header->setTid(5); + header->setAckPolicy(BLOCK_ACK); + header->setSequenceNumber(SequenceNumberCyclic((4090 + offset) % 4096)); + header->setFragmentNumber(0); + headers.push_back(header); + handler.frameGotInProgress(header); + handler.processTransmittedDataOrMgmtFrame(header); + } + auto basicBar = makeShared(); + basicBar->setReceiverAddress(receiver); + basicBar->setTidInfo(5); + basicBar->setStartingSequenceNumber(SequenceNumberCyclic(4090)); + handler.processTransmittedBlockAckReq(basicBar); + + ASSERT(handler.getQoSDataAckStatus(headers[0]) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(handler.getQoSDataAckStatus(headers[1]) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(handler.getQoSDataAckStatus(headers[2]) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(handler.getQoSDataAckStatus(headers[3]) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(handler.isOutstandingFrame(headers[2])); + ASSERT(handler.isOutstandingFrame(headers[3])); + + auto basicBlockAck = makeShared(); + basicBlockAck->setTransmitterAddress(receiver); + basicBlockAck->setTidInfo(5); + basicBlockAck->setStartingSequenceNumber(SequenceNumberCyclic(4090)); + for (int i = 0; i < 64; i++) + basicBlockAck->setBlockAckBitmap(i, BitVector(std::vector(2, 0xFF))); + auto acked = handler.processReceivedBlockAck(basicBlockAck); + ASSERT(acked.size() == 2); + ASSERT(handler.getQoSDataAckStatus(headers[0]) == QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED); + ASSERT(handler.getQoSDataAckStatus(headers[1]) == QosAckHandler::Status::BLOCK_ACK_ARRIVED_ACKED); + ASSERT(handler.getQoSDataAckStatus(headers[2]) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + ASSERT(handler.getQoSDataAckStatus(headers[3]) == QosAckHandler::Status::BLOCK_ACK_NOT_YET_REQUESTED); + + auto nextBar = makeShared(*basicBar); + nextBar->setStartingSequenceNumber(SequenceNumberCyclic((4090 + 64) % 4096)); + handler.processTransmittedBlockAckReq(nextBar); + ASSERT(handler.getQoSDataAckStatus(headers[2]) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + ASSERT(handler.getQoSDataAckStatus(headers[3]) == QosAckHandler::Status::WAITING_FOR_BLOCK_ACK); + EV << "Sparse Basic BAR limits waiting statuses to 64 positions across sequence wrap.\n"; +} + EV << ".\n"; %contains: stdout @@ -918,4 +969,5 @@ Originator capability, agreement, and fragmentation gates passed. Legacy Basic BAR and BA remain byte-exact. Accepted delayed agreement is retained for Basic BAR fallback. Basic and Compressed BAR rate selection is variant-agnostic. +Sparse Basic BAR limits waiting statuses to 64 positions across sequence wrap. .