From f30548f2f55a6746668068045515352ad7b3ea04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 17 Aug 2026 11:43:00 +0200 Subject: [PATCH 01/10] ieee80211: add HT capability and operation signaling Add a model-backed subset of the IEEE 802.11 HT Capabilities and HT Operation state to the MIB. Derive local capability limits from the authoritative mode set, preserve the standard Tx/Rx MCS representation, negotiate directional peer capabilities conservatively, and retain exact bitmap holes for equal Tx/Rx sets. Carry typed HT Capabilities and HT Operation elements in beacon, probe, association, and reassociation management frames. Serialize and deserialize the standard element layouts with subtype presence validation, malformed-element rejection, and concise protocol-printer output. Install peer HT state during detailed and simplified management flows. At the AP, reserve association IDs while a response is pending and commit the AID, station state, and peer capabilities only after the response is acknowledged. Preserve state across retries, clean up final failures, and downgrade an associated station after an acknowledged refusal when management-frame protection is not modeled. Add focused unit coverage for directional negotiation, unequal and undefined Tx MCS advertisements, non-contiguous MCS bitmaps, byte-level management element encoding, malformed inputs, and subtype policy. Add a detailed 802.11n association module test that verifies the request/response element matrix and peer-state installation. --- .../ieee80211/mgmt/Ieee80211HtMgmtElements.h | 126 +++++++++ .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 140 ++++++++-- .../ieee80211/mgmt/Ieee80211MgmtAp.h | 11 +- .../ieee80211/mgmt/Ieee80211MgmtBase.cc | 16 +- .../ieee80211/mgmt/Ieee80211MgmtBase.h | 5 +- .../ieee80211/mgmt/Ieee80211MgmtFrame.msg | 30 +++ .../mgmt/Ieee80211MgmtFrameSerializer.cc | 244 ++++++++++++++++- .../mgmt/Ieee80211MgmtFrameSerializer.h | 4 +- .../mgmt/Ieee80211MgmtProtocolPrinter.cc | 21 +- .../ieee80211/mgmt/Ieee80211MgmtSta.cc | 103 +++++-- .../ieee80211/mgmt/Ieee80211MgmtSta.h | 9 +- .../mgmt/Ieee80211MgmtStaSimplified.cc | 9 +- .../ieee80211/mib/Ieee80211HtCapabilities.h | 152 +++++++++++ .../linklayer/ieee80211/mib/Ieee80211Mib.cc | 89 +++++++ .../linklayer/ieee80211/mib/Ieee80211Mib.h | 27 ++ .../linklayer/ieee80211/mib/Ieee80211Mib.ned | 5 +- .../ieee80211/mode/Ieee80211ModeSet.cc | 26 +- .../ieee80211/mode/Ieee80211ModeSet.h | 11 +- tests/module/Ieee80211HtAssociation_1.test | 75 ++++++ tests/unit/Ieee80211HtCapabilities_1.test | 109 ++++++++ tests/unit/Ieee80211HtMgmtElements_1.test | 252 ++++++++++++++++++ 21 files changed, 1396 insertions(+), 68 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h create mode 100644 src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h create mode 100644 tests/module/Ieee80211HtAssociation_1.test create mode 100644 tests/unit/Ieee80211HtCapabilities_1.test create mode 100644 tests/unit/Ieee80211HtMgmtElements_1.test diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h new file mode 100644 index 00000000000..8bf4af4641d --- /dev/null +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h @@ -0,0 +1,126 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IEEE80211HTMGMTELEMENTS_H +#define __INET_IEEE80211HTMGMTELEMENTS_H + +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h" + +namespace inet { +namespace ieee80211 { + +inline Ieee80211HtCapabilitiesElement makeHtCapabilitiesElement(const Ieee80211HtCapabilities& capabilities) +{ + Ieee80211HtCapabilitiesElement element; + element.ldpc = capabilities.ldpc; + element.supportedChannelWidth40Mhz = capabilities.supportedChannelWidths.count(MHz(40)) != 0; + element.greenfield = capabilities.greenfield; + element.shortGi20 = capabilities.shortGi20; + element.shortGi40 = capabilities.shortGi40; + element.maxAmpduLengthExponent = capabilities.maxAmpduLengthExponent; + for (int i = 0; i < 77; i++) + element.rxMcsSupported[i] = capabilities.rxMcsSupported[i]; + element.txMcsSetDefined = capabilities.txMcsSetDefined; + element.txRxMcsSetNotEqual = capabilities.txRxMcsSetNotEqual; + element.txMaxNss = capabilities.txMaxNss; + element.txUnequalModulation = capabilities.txUnequalModulation; + if (element.txMcsSetDefined && !element.txRxMcsSetNotEqual) { + for (int nss = 0; nss < 4; nss++) { + int rxMaximum = -1; + for (int mcs = 0; mcs < 8; mcs++) + if (capabilities.rxMcsSupported[nss * 8 + mcs]) + rxMaximum = mcs; + if (capabilities.txMcsNss.maxMcsPerNss[nss] != rxMaximum) + throw cRuntimeError("Equal HT Tx/Rx MCS Set does not match the Rx MCS bitmap"); + } + } + return element; +} + +inline Ieee80211HtCapabilities makeHtCapabilities(const Ieee80211HtCapabilitiesElement& element) +{ + if (element.maxAmpduLengthExponent < 0 || element.maxAmpduLengthExponent > 3) + throw cRuntimeError("Invalid Maximum A-MPDU Length Exponent: %d", element.maxAmpduLengthExponent); + Ieee80211HtCapabilities capabilities; + capabilities.supportedChannelWidths.insert(MHz(20)); + if (element.supportedChannelWidth40Mhz) + capabilities.supportedChannelWidths.insert(MHz(40)); + capabilities.ldpc = element.ldpc; + capabilities.greenfield = element.greenfield; + capabilities.shortGi20 = element.shortGi20; + capabilities.shortGi40 = element.shortGi40; + capabilities.maxAmpduLengthExponent = element.maxAmpduLengthExponent; + capabilities.txMcsSetDefined = element.txMcsSetDefined; + capabilities.txRxMcsSetNotEqual = element.txRxMcsSetNotEqual; + capabilities.txMaxNss = element.txMaxNss; + capabilities.txUnequalModulation = element.txUnequalModulation; + for (int i = 0; i < 77; i++) + capabilities.rxMcsSupported[i] = element.rxMcsSupported[i]; + for (int nss = 0; nss < 4; nss++) { + int rxMaximum = -1; + for (int mcs = 0; mcs < 8; mcs++) + if (element.rxMcsSupported[nss * 8 + mcs]) + rxMaximum = mcs; + capabilities.txMcsNss.maxMcsPerNss[nss] = element.txMcsSetDefined && !element.txRxMcsSetNotEqual ? rxMaximum : -1; + } + return capabilities; +} + +inline Ieee80211HtOperationElement makeHtOperationElement(const Ieee80211HtOperation& operation) +{ + Ieee80211HtOperationElement element; + element.primaryChannel = operation.primaryChannel; + element.secondaryChannelOffset = operation.secondaryChannelOffset; + element.staChannelWidth40Mhz = operation.operatingChannelWidth == MHz(40); + element.protectionMode = static_cast(operation.protectionMode); + for (int i = 0; i < 77; i++) + element.basicMcsSupported[i] = operation.basicMcsSupported[i]; + return element; +} + +inline Ieee80211HtOperation makeHtOperation(const Ieee80211HtOperationElement& element) +{ + if (element.secondaryChannelOffset == 2 || element.secondaryChannelOffset < 0 || element.secondaryChannelOffset > 3) + throw cRuntimeError("Invalid HT Secondary Channel Offset: %d", element.secondaryChannelOffset); + if (element.protectionMode < 0 || element.protectionMode > 3) + throw cRuntimeError("Invalid HT Protection field: %d", element.protectionMode); + Ieee80211HtOperation operation; + operation.primaryChannel = element.primaryChannel; + operation.secondaryChannelOffset = element.secondaryChannelOffset; + operation.operatingChannelWidth = element.staChannelWidth40Mhz ? MHz(40) : MHz(20); + operation.protectionMode = static_cast(element.protectionMode); + for (int i = 0; i < 77; i++) + operation.basicMcsSupported[i] = element.basicMcsSupported[i]; + return operation; +} + +inline B getHtMgmtElementsLength(const Ptr& frame) +{ + B length(0); + if (frame->getHtCapabilitiesPresent()) + length += B(28); + if (frame->getHtOperationPresent()) + length += B(24); + return length; +} + +inline void setHtCapabilities(const Ptr& frame, const Ieee80211HtCapabilities& capabilities) +{ + frame->setHtCapabilitiesPresent(true); + frame->setHtCapabilities(makeHtCapabilitiesElement(capabilities)); +} + +inline void setHtOperation(const Ptr& frame, const Ieee80211HtOperation& operation) +{ + frame->setHtOperationPresent(true); + frame->setHtOperation(makeHtOperationElement(operation)); +} + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index 8c6aa27cb8a..09c703d8bc2 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -17,6 +17,7 @@ #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/Ieee80211SubtypeTag_m.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h" #include "inet/networklayer/common/NetworkInterface.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" @@ -93,6 +94,7 @@ void Ieee80211MgmtAp::receiveSignal(cComponent *source, simsignal_t signalID, in if (signalID == Ieee80211Radio::radioChannelChangedSignal) { EV << "updating channel number\n"; channelNumber = value; + mib->htOperation.primaryChannel = channelNumber; } } @@ -105,16 +107,50 @@ void Ieee80211MgmtAp::receiveSignal(cComponent *source, simsignal_t signalID, cO if (context->getNumSteps() >= 2) { auto transmitStep = dynamic_cast(context->getStepBeforeLast()); auto receiveStep = dynamic_cast(context->getLastStep()); - if (transmitStep && receiveStep && - transmitStep->getCompletion() == IFrameSequenceStep::Completion::ACCEPTED && - receiveStep->getCompletion() == IFrameSequenceStep::Completion::ACCEPTED) { + if (transmitStep && receiveStep) { auto responseHeader = dynamicPtrCast(transmitStep->getFrameToTransmit()->peekAtFront()); if (responseHeader != nullptr && (responseHeader->getType() == ST_ASSOCIATIONRESPONSE || responseHeader->getType() == ST_REASSOCIATIONRESPONSE)) { - auto ackHeader = receiveStep->getReceivedFrame()->peekAtFront(); - if (ackHeader->getType() == ST_ACK) { - if (responseHeader->getType() == ST_ASSOCIATIONRESPONSE && mib->bssAccessPointData.stations[responseHeader->getReceiverAddress()] != Ieee80211Mib::ASSOCIATED) - sendAssocNotification(responseHeader->getReceiverAddress()); - mib->bssAccessPointData.stations[responseHeader->getReceiverAddress()] = Ieee80211Mib::ASSOCIATED; + const auto& address = responseHeader->getReceiverAddress(); + auto sta = staList.find(address); + bool exchangeSucceeded = transmitStep->getCompletion() == IFrameSequenceStep::Completion::ACCEPTED && + receiveStep->getCompletion() == IFrameSequenceStep::Completion::ACCEPTED && + receiveStep->getReceivedFrame()->peekAtFront()->getType() == ST_ACK; + bool isPendingResponse = sta != staList.end() && sta->second.pendingAssociationResponse == transmitStep->getFrameToTransmit(); + if (isPendingResponse && sta->second.pendingAssociationSuccessful && exchangeSucceeded) { + bool wasAssociated = mib->bssAccessPointData.stations[address] == Ieee80211Mib::ASSOCIATED; + mib->bssAccessPointData.associationIds[address] = sta->second.pendingAssociationId; + mib->bssAccessPointData.stations[address] = Ieee80211Mib::ASSOCIATED; + if (sta->second.pendingHtStateAvailable) { + // IEEE Std 802.11-2024 association state becomes effective only after the successful response exchange. + if (sta->second.pendingHtCapabilitiesValid) + mib->setPeerHtCapabilities(address, sta->second.pendingHtCapabilities, mib->htOperation); + else + mib->removePeerHtCapabilities(address); + } + // Signal delivery is synchronous; observers must see committed station and peer state. + if (responseHeader->getType() == ST_ASSOCIATIONRESPONSE && !wasAssociated) + sendAssocNotification(address); + } + else if (isPendingResponse && !sta->second.pendingAssociationSuccessful && exchangeSucceeded && + mib->bssAccessPointData.stations[address] == Ieee80211Mib::ASSOCIATED) + { + // This model does not implement negotiated management-frame protection. + // IEEE Std 802.11-2024, 11.3.5.3(p): a refused (re)association + // therefore moves the STA from State 4 back to State 3. + mib->releaseAssociationId(address); + mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + // Signal delivery is synchronous; observers must see the downgraded state. + sendDisAssocNotification(address); + } + if (isPendingResponse) { + bool retryPending = false; + if (!exchangeSucceeded) { + auto inProgressFrames = context->getInProgressFrames(); + for (int i = 0; i < inProgressFrames->getLength(); i++) + retryPending |= inProgressFrames->getFrames(i) == transmitStep->getFrameToTransmit(); + } + if (exchangeSucceeded || !retryPending) + clearPendingAssociation(&sta->second); } } } @@ -130,13 +166,42 @@ Ieee80211MgmtAp::StaInfo *Ieee80211MgmtAp::lookupSenderSTA(const Ptrsecond); } -void Ieee80211MgmtAp::sendManagementFrame(const char *name, const Ptr& body, int subtype, const MacAddress& destAddr) +Packet *Ieee80211MgmtAp::sendManagementFrame(const char *name, const Ptr& body, int subtype, const MacAddress& destAddr) { auto packet = new Packet(name); packet->addTag()->setDestAddress(destAddr); packet->addTag()->setSubtype(subtype); packet->insertAtBack(body); sendDown(packet); + return packet; +} + +short Ieee80211MgmtAp::reserveAssociationId(StaInfo *sta) const +{ + auto existing = mib->bssAccessPointData.associationIds.find(sta->address); + if (existing != mib->bssAccessPointData.associationIds.end()) + return existing->second; + if (sta->pendingAssociationId != 0) + return sta->pendingAssociationId; + for (short aid = 1; aid <= 2007; aid++) { + bool used = false; + for (const auto& entry : mib->bssAccessPointData.associationIds) + used |= entry.second == aid; + for (const auto& entry : staList) + used |= entry.second.pendingAssociationId == aid; + if (!used) + return aid; + } + throw cRuntimeError("No IEEE 802.11 association ID is available"); +} + +void Ieee80211MgmtAp::clearPendingAssociation(StaInfo *sta) +{ + sta->pendingAssociationSuccessful = false; + sta->pendingAssociationId = 0; + sta->pendingAssociationResponse = nullptr; + sta->pendingHtStateAvailable = false; + sta->pendingHtCapabilitiesValid = false; } void Ieee80211MgmtAp::sendBeacon() @@ -147,7 +212,9 @@ void Ieee80211MgmtAp::sendBeacon() body->setSupportedRates(supportedRates); body->setBeaconInterval(beaconInterval); body->setChannelNumber(channelNumber); - body->setChunkLength(B(8 + 2 + 2 + (2 + ssid.length()) + (2 + supportedRates.numRates))); + addHtCapabilities(body); + addHtOperation(body); + body->setChunkLength(B(8 + 2 + 2 + (2 + ssid.length()) + (2 + supportedRates.numRates)) + getHtMgmtElementsLength(body)); sendManagementFrame("Beacon", body, ST_BEACON, MacAddress::BROADCAST_ADDRESS); } @@ -165,6 +232,7 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const Ptraddress = staAddress; mib->bssAccessPointData.stations[staAddress] = Ieee80211Mib::NOT_AUTHENTICATED; sta->authSeqExpected = 1; + clearPendingAssociation(sta); } // reset authentication status, when starting a new auth sequence @@ -180,6 +248,8 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrreleaseAssociationId(sta->address); } mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::NOT_AUTHENTICATED; + clearPendingAssociation(sta); + mib->removePeerHtCapabilities(sta->address); sta->authSeqExpected = 1; } @@ -240,6 +310,8 @@ void Ieee80211MgmtAp::handleDeauthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] = Ieee80211Mib::NOT_AUTHENTICATED; sta->authSeqExpected = 1; + clearPendingAssociation(sta); + mib->removePeerHtCapabilities(sta->address); } } @@ -258,15 +330,27 @@ void Ieee80211MgmtAp::handleAssociationRequestFrame(Packet *packet, const PtrpeekData(); + sta->pendingAssociationSuccessful = false; + sta->pendingHtStateAvailable = true; + sta->pendingHtCapabilitiesValid = mib->isHtOperationSupported() && requestBody->getHtCapabilitiesPresent(); + if (sta->pendingHtCapabilitiesValid) + sta->pendingHtCapabilities = makeHtCapabilities(requestBody->getHtCapabilities()); + bool basicHtMcsSupported = !sta->pendingHtCapabilitiesValid || + supportsBasicHtMcsSet(sta->pendingHtCapabilities, mib->htOperation); delete packet; - // send OK response + // IEEE Std 802.11-2024, 11.3.5.3 g): an HT STA must support every Basic HT-MCS. const auto& body = makeShared(); - body->setStatusCode(SC_SUCCESSFUL); - body->setAid(mib->allocateAssociationId(sta->address)); + body->setStatusCode(basicHtMcsSupported ? SC_SUCCESSFUL : SC_DATARATE_UNSUP); + sta->pendingAssociationId = basicHtMcsSupported ? reserveAssociationId(sta) : 0; + body->setAid(sta->pendingAssociationId); + sta->pendingAssociationSuccessful = basicHtMcsSupported; body->setSupportedRates(supportedRates); - body->setChunkLength(B(2 + 2 + 2 + body->getSupportedRates().numRates + 2)); - sendManagementFrame("AssocResp-OK", body, ST_ASSOCIATIONRESPONSE, sta->address); + addHtCapabilities(body); + addHtOperation(body); + body->setChunkLength(B(2 + 2 + 2 + body->getSupportedRates().numRates + 2) + getHtMgmtElementsLength(body)); + sta->pendingAssociationResponse = sendManagementFrame(basicHtMcsSupported ? "AssocResp-OK" : "AssocResp-UnsupportedHtMcs", body, ST_ASSOCIATIONRESPONSE, sta->address); } void Ieee80211MgmtAp::handleAssociationResponseFrame(Packet *packet, const Ptr& header) @@ -289,15 +373,27 @@ void Ieee80211MgmtAp::handleReassociationRequestFrame(Packet *packet, const Ptr< return; } + const auto& requestBody = packet->peekData(); + sta->pendingAssociationSuccessful = false; + sta->pendingHtStateAvailable = true; + sta->pendingHtCapabilitiesValid = mib->isHtOperationSupported() && requestBody->getHtCapabilitiesPresent(); + if (sta->pendingHtCapabilitiesValid) + sta->pendingHtCapabilities = makeHtCapabilities(requestBody->getHtCapabilities()); + bool basicHtMcsSupported = !sta->pendingHtCapabilitiesValid || + supportsBasicHtMcsSet(sta->pendingHtCapabilities, mib->htOperation); delete packet; // send OK response const auto& body = makeShared(); - body->setStatusCode(SC_SUCCESSFUL); - body->setAid(mib->allocateAssociationId(sta->address)); + body->setStatusCode(basicHtMcsSupported ? SC_SUCCESSFUL : SC_DATARATE_UNSUP); + sta->pendingAssociationId = basicHtMcsSupported ? reserveAssociationId(sta) : 0; + body->setAid(sta->pendingAssociationId); + sta->pendingAssociationSuccessful = basicHtMcsSupported; body->setSupportedRates(supportedRates); - body->setChunkLength(B(2 + (2 + ssid.length()) + (2 + supportedRates.numRates) + 6)); - sendManagementFrame("ReassocResp-OK", body, ST_REASSOCIATIONRESPONSE, sta->address); + addHtCapabilities(body); + addHtOperation(body); + body->setChunkLength(B(2 + 2 + 2 + (2 + supportedRates.numRates)) + getHtMgmtElementsLength(body)); + sta->pendingAssociationResponse = sendManagementFrame(basicHtMcsSupported ? "ReassocResp-OK" : "ReassocResp-UnsupportedHtMcs", body, ST_REASSOCIATIONRESPONSE, sta->address); } void Ieee80211MgmtAp::handleReassociationResponseFrame(Packet *packet, const Ptr& header) @@ -316,6 +412,8 @@ void Ieee80211MgmtAp::handleDisassociationFrame(Packet *packet, const PtrreleaseAssociationId(sta->address); } mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::AUTHENTICATED; + clearPendingAssociation(sta); + mib->removePeerHtCapabilities(sta->address); } } @@ -344,7 +442,9 @@ void Ieee80211MgmtAp::handleProbeRequestFrame(Packet *packet, const PtrsetSupportedRates(supportedRates); body->setBeaconInterval(beaconInterval); body->setChannelNumber(channelNumber); - body->setChunkLength(B(8 + 2 + 2 + (2 + ssid.length()) + (2 + supportedRates.numRates))); + addHtCapabilities(body); + addHtOperation(body); + body->setChunkLength(B(8 + 2 + 2 + (2 + ssid.length()) + (2 + supportedRates.numRates)) + getHtMgmtElementsLength(body)); sendManagementFrame("ProbeResp", body, ST_PROBERESPONSE, staAddress); } diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h index 393df9e9104..96e3f39e99f 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h @@ -27,6 +27,12 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase struct StaInfo { MacAddress address; int authSeqExpected; // when NOT_AUTHENTICATED: transaction sequence number of next expected auth frame + bool pendingAssociationSuccessful = false; + short pendingAssociationId = 0; + const Packet *pendingAssociationResponse = nullptr; + bool pendingHtStateAvailable = false; + bool pendingHtCapabilitiesValid = false; + Ieee80211HtCapabilities pendingHtCapabilities; // int consecFailedTrans; // TODO // double expiry; // TODO association should expire after a while if STA is silent? }; @@ -81,7 +87,10 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase virtual StaInfo *lookupSenderSTA(const Ptr& header); /** Utility function: set fields in the given frame and send it out to the address */ - virtual void sendManagementFrame(const char *name, const Ptr& body, int subtype, const MacAddress& destAddr); + virtual Packet *sendManagementFrame(const char *name, const Ptr& body, int subtype, const MacAddress& destAddr); + + virtual short reserveAssociationId(StaInfo *sta) const; + virtual void clearPendingAssociation(StaInfo *sta); /** Utility function: creates and sends a beacon frame */ virtual void sendBeacon(); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc index 2cd699becc4..514e3686985 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc @@ -14,6 +14,7 @@ #include "inet/common/lifecycle/ModuleOperations.h" #include "inet/common/lifecycle/NodeStatus.h" #include "inet/linklayer/common/InterfaceTag_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h" #include "inet/networklayer/common/NetworkInterface.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" @@ -45,6 +46,7 @@ void Ieee80211MgmtBase::receiveSignal(cComponent *source, simsignal_t signalID, if (signalID == modesetChangedSignal) { modeSet = check_and_cast(obj); + mib->updateLocalHtCapabilities(modeSet); supportedRates.numRates = std::min(8, modeSet->getNumModes()); int rateIndex = 0; for (int i = 0; i < supportedRates.numRates; i++) @@ -53,6 +55,18 @@ void Ieee80211MgmtBase::receiveSignal(cComponent *source, simsignal_t signalID, } } +void Ieee80211MgmtBase::addHtCapabilities(const Ptr& frame) const +{ + if (mib->isHtOperationSupported()) + setHtCapabilities(frame, mib->localHtCapabilities); +} + +void Ieee80211MgmtBase::addHtOperation(const Ptr& frame) const +{ + if (mib->isHtOperationSupported()) + setHtOperation(frame, mib->htOperation); +} + void Ieee80211MgmtBase::handleMessageWhenUp(cMessage *msg) { if (msg->isSelfMessage()) { @@ -158,9 +172,9 @@ void Ieee80211MgmtBase::start() void Ieee80211MgmtBase::stop() { + mib->clearPeerHtCapabilities(); } } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h index c2db31e1e52..aeb57d81b83 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.h @@ -61,6 +61,10 @@ class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener /** Utility method to dispose of an unhandled frame */ virtual void dropManagementFrame(Packet *frame); + /** Adds the local HT advertisement to a frame when the authoritative PHY profile supports HT operation. */ + virtual void addHtCapabilities(const Ptr& frame) const; + virtual void addHtOperation(const Ptr& frame) const; + /** Dispatch to frame processing methods according to frame type */ virtual void processFrame(Packet *packet, const Ptr& header); @@ -99,4 +103,3 @@ class INET_API Ieee80211MgmtBase : public OperationalBase, public cListener } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg index a469125efef..bc83f445535 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame.msg @@ -118,11 +118,41 @@ struct Ieee80211SupportedRatesElement double rate[8]; // in Mbit/s; should be multiple of 500 kbit/s } +// IEEE Std 802.11-2024, 9.4.2.54: modeled fields in the fixed 26-octet HT Capabilities body. +struct Ieee80211HtCapabilitiesElement +{ + bool ldpc; + bool supportedChannelWidth40Mhz; + bool greenfield; + bool shortGi20; + bool shortGi40; + short maxAmpduLengthExponent; + bool rxMcsSupported[77]; + bool txMcsSetDefined; + bool txRxMcsSetNotEqual; + short txMaxNss; + bool txUnequalModulation; +} + +// IEEE Std 802.11-2024, 9.4.2.55: modeled fields in the fixed 22-octet HT Operation body. +struct Ieee80211HtOperationElement +{ + short primaryChannel; + short secondaryChannelOffset; + bool staChannelWidth40Mhz; + short protectionMode; + bool basicMcsSupported[77]; +} + // // Frame body base class used to hide various frame body types // class Ieee80211MgmtFrame extends FieldsChunk { + bool htCapabilitiesPresent; + Ieee80211HtCapabilitiesElement htCapabilities; + bool htOperationPresent; + Ieee80211HtOperationElement htOperation; } // diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc index 7cc2abedb52..07b1b05fa5e 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc @@ -7,6 +7,9 @@ #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h" +#include +#include + #include "inet/common/packet/serializer/ChunkSerializerRegistry.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" @@ -25,6 +28,195 @@ Register_Serializer(Ieee80211ProbeResponseFrame, Ieee80211MgmtFrameSerializer); Register_Serializer(Ieee80211ReassociationRequestFrame, Ieee80211MgmtFrameSerializer); Register_Serializer(Ieee80211ReassociationResponseFrame, Ieee80211MgmtFrameSerializer); +static constexpr uint8_t HT_CAPABILITIES_ELEMENT_ID = 45; +static constexpr uint8_t HT_OPERATION_ELEMENT_ID = 61; + +static bool getBit(const std::vector& bytes, int bit) +{ + return (bytes[bit / 8] & (1 << (bit % 8))) != 0; +} + +static void setBit(std::vector& bytes, int bit) +{ + bytes[bit / 8] |= 1 << (bit % 8); +} + +static void writeHtCapabilitiesElement(MemoryOutputStream& stream, const Ieee80211HtCapabilitiesElement& capabilities) +{ + // IEEE Std 802.11-2024, 9.4.2.54 and Tables 9-224 to 9-226. + if (capabilities.maxAmpduLengthExponent < 0 || capabilities.maxAmpduLengthExponent > 3) + throw cRuntimeError("Malformed Maximum A-MPDU Length Exponent: %d", capabilities.maxAmpduLengthExponent); + if (!capabilities.txMcsSetDefined && (capabilities.txRxMcsSetNotEqual || capabilities.txMaxNss != 0 || capabilities.txUnequalModulation)) + throw cRuntimeError("Malformed undefined HT Tx MCS Set"); + if (capabilities.txMcsSetDefined && !capabilities.txRxMcsSetNotEqual && + (capabilities.txMaxNss != 0 || capabilities.txUnequalModulation)) + throw cRuntimeError("Malformed equal HT Tx/Rx MCS Set"); + if (capabilities.txMcsSetDefined && capabilities.txRxMcsSetNotEqual && + (capabilities.txMaxNss < 1 || capabilities.txMaxNss > 4)) + throw cRuntimeError("Malformed HT Tx Maximum Number of Spatial Streams: %d", capabilities.txMaxNss); + + stream.writeByte(HT_CAPABILITIES_ELEMENT_ID); + stream.writeByte(26); + // SM Power Save is unmodeled: Table 9-224 requires value 3 for disabled/not supported. + uint16_t information = (capabilities.ldpc ? 1 : 0) | + (capabilities.supportedChannelWidth40Mhz ? 1 << 1 : 0) | + (3 << 2) | + (capabilities.greenfield ? 1 << 4 : 0) | + (capabilities.shortGi20 ? 1 << 5 : 0) | + (capabilities.shortGi40 ? 1 << 6 : 0); + stream.writeUint16Le(information); + stream.writeByte(capabilities.maxAmpduLengthExponent); + + std::vector mcs(16, 0); + for (int mcsIndex = 0; mcsIndex < 77; mcsIndex++) + if (capabilities.rxMcsSupported[mcsIndex]) + setBit(mcs, mcsIndex); + if (capabilities.txMcsSetDefined) + setBit(mcs, 96); + if (capabilities.txMcsSetDefined && capabilities.txRxMcsSetNotEqual) { + setBit(mcs, 97); + if ((capabilities.txMaxNss - 1) & 1) + setBit(mcs, 98); + if ((capabilities.txMaxNss - 1) & 2) + setBit(mcs, 99); + if (capabilities.txUnequalModulation) + setBit(mcs, 100); + } + for (auto byte : mcs) + stream.writeByte(byte); + stream.writeUint16Le(0); // HT Extended Capabilities: modeled subset advertises none. + stream.writeUint32Le(0); // Transmit Beamforming Capabilities: unmodeled. + stream.writeByte(0); // ASEL Capabilities: unmodeled. +} + +static void writeHtOperationElement(MemoryOutputStream& stream, const Ieee80211HtOperationElement& operation) +{ + // IEEE Std 802.11-2024, 9.4.2.55 and Table 9-230. + if (operation.primaryChannel < 0 || operation.primaryChannel > 255 || + operation.secondaryChannelOffset < 0 || operation.secondaryChannelOffset > 3 || operation.secondaryChannelOffset == 2 || + operation.protectionMode < 0 || operation.protectionMode > 3) + throw cRuntimeError("Malformed HT Operation element fields"); + stream.writeByte(HT_OPERATION_ELEMENT_ID); + stream.writeByte(22); + stream.writeByte(operation.primaryChannel); + uint64_t information = (operation.secondaryChannelOffset & 3) | + (operation.staChannelWidth40Mhz ? uint64_t(1) << 2 : 0) | + (uint64_t(operation.protectionMode) << 8); + for (int i = 0; i < 5; i++) + stream.writeByte((information >> (i * 8)) & 0xff); + std::vector basicMcs(16, 0); + for (int mcsIndex = 0; mcsIndex < 77; mcsIndex++) + if (operation.basicMcsSupported[mcsIndex]) + setBit(basicMcs, mcsIndex); + for (auto byte : basicMcs) + stream.writeByte(byte); +} + +enum HtElementPresence : unsigned int { + HT_ELEMENT_NONE = 0, + HT_CAPABILITIES_ALLOWED = 1, + HT_OPERATION_ALLOWED = 2, +}; + +static void writeHtElements(MemoryOutputStream& stream, const Ptr& frame, unsigned int allowedElements) +{ + if (!(allowedElements & HT_CAPABILITIES_ALLOWED) && frame->getHtCapabilitiesPresent()) + throw cRuntimeError("HT Capabilities element is not allowed in this management frame subtype"); + if (!(allowedElements & HT_OPERATION_ALLOWED) && frame->getHtOperationPresent()) + throw cRuntimeError("HT Operation element is not allowed in this management frame subtype"); + if (frame->getHtCapabilitiesPresent()) + writeHtCapabilitiesElement(stream, frame->getHtCapabilities()); + if (frame->getHtOperationPresent()) + writeHtOperationElement(stream, frame->getHtOperation()); +} + +static void readHtCapabilitiesElement(MemoryInputStream& stream, int length, const Ptr& frame) +{ + if (length != 26) + throw cRuntimeError("Malformed HT Capabilities element length: %d", length); + if (frame->getHtCapabilitiesPresent()) + throw cRuntimeError("Duplicate HT Capabilities element"); + Ieee80211HtCapabilitiesElement capabilities; + uint16_t information = stream.readUint16Le(); + capabilities.ldpc = information & 1; + capabilities.supportedChannelWidth40Mhz = information & (1 << 1); + capabilities.greenfield = information & (1 << 4); + capabilities.shortGi20 = information & (1 << 5); + capabilities.shortGi40 = information & (1 << 6); + capabilities.maxAmpduLengthExponent = stream.readByte() & 3; + std::vector mcs(16); + for (auto& byte : mcs) + byte = stream.readByte(); + for (int mcsIndex = 0; mcsIndex < 77; mcsIndex++) + capabilities.rxMcsSupported[mcsIndex] = getBit(mcs, mcsIndex); + capabilities.txMcsSetDefined = getBit(mcs, 96); + capabilities.txRxMcsSetNotEqual = getBit(mcs, 97); + bool txNssBitsSet = getBit(mcs, 98) || getBit(mcs, 99); + bool txUnequalModulation = getBit(mcs, 100); + if (!capabilities.txMcsSetDefined && (capabilities.txRxMcsSetNotEqual || txNssBitsSet || txUnequalModulation)) + throw cRuntimeError("Malformed undefined HT Tx MCS Set"); + if (capabilities.txMcsSetDefined && !capabilities.txRxMcsSetNotEqual && (txNssBitsSet || txUnequalModulation)) + throw cRuntimeError("Malformed equal HT Tx/Rx MCS Set"); + capabilities.txMaxNss = capabilities.txRxMcsSetNotEqual ? + (getBit(mcs, 98) ? 1 : 0) + (getBit(mcs, 99) ? 2 : 0) + 1 : 0; + capabilities.txUnequalModulation = txUnequalModulation; + for (int i = 0; i < 7; i++) + stream.readByte(); + frame->setHtCapabilitiesPresent(true); + frame->setHtCapabilities(capabilities); +} + +static void readHtOperationElement(MemoryInputStream& stream, int length, const Ptr& frame) +{ + if (length != 22) + throw cRuntimeError("Malformed HT Operation element length: %d", length); + if (frame->getHtOperationPresent()) + throw cRuntimeError("Duplicate HT Operation element"); + Ieee80211HtOperationElement operation; + operation.primaryChannel = stream.readByte(); + uint64_t information = 0; + for (int i = 0; i < 5; i++) + information |= uint64_t(stream.readByte()) << (i * 8); + operation.secondaryChannelOffset = information & 3; + operation.staChannelWidth40Mhz = information & (1 << 2); + operation.protectionMode = (information >> 8) & 3; + std::vector basicMcs(16); + for (auto& byte : basicMcs) + byte = stream.readByte(); + for (int mcsIndex = 0; mcsIndex < 77; mcsIndex++) + operation.basicMcsSupported[mcsIndex] = getBit(basicMcs, mcsIndex); + if (operation.secondaryChannelOffset == 2) + throw cRuntimeError("Malformed HT Operation element: reserved Secondary Channel Offset"); + frame->setHtOperationPresent(true); + frame->setHtOperation(operation); +} + +static void readHtElements(MemoryInputStream& stream, const Ptr& frame, unsigned int allowedElements) +{ + while (stream.getRemainingLength() != b(0)) { + if (stream.getRemainingLength() < B(2)) + throw cRuntimeError("Malformed IEEE 802.11 management element header"); + int elementId = stream.readByte(); + int length = stream.readByte(); + if (stream.getRemainingLength() < B(length)) + throw cRuntimeError("Malformed IEEE 802.11 management element: id=%d length=%d remaining=%" PRId64, + elementId, length, stream.getRemainingLength().get()); + if (elementId == HT_CAPABILITIES_ELEMENT_ID) { + if (!(allowedElements & HT_CAPABILITIES_ALLOWED)) + throw cRuntimeError("HT Capabilities element is not allowed in this management frame subtype"); + readHtCapabilitiesElement(stream, length, frame); + } + else if (elementId == HT_OPERATION_ELEMENT_ID) { + if (!(allowedElements & HT_OPERATION_ALLOWED)) + throw cRuntimeError("HT Operation element is not allowed in this management frame subtype"); + readHtOperationElement(stream, length, frame); + } + else + for (int i = 0; i < length; i++) + stream.readByte(); + } +} + void Ieee80211MgmtFrameSerializer::serialize(MemoryOutputStream& stream, const Ptr& chunk) const { if (auto authenticationFrame = dynamicPtrCast(chunk)) { @@ -37,14 +229,17 @@ void Ieee80211MgmtFrameSerializer::serialize(MemoryOutputStream& stream, const P stream.writeUint16Be(authenticationFrame->getStatusCode()); // 4 Challenge text The challenge text information is present only in certain Authentication frames as defined in Table 7-17. // Last Vendor Specific One or more vendor-specific information elements may appear in this frame. This information element follows all other information elements. + writeHtElements(stream, authenticationFrame, HT_ELEMENT_NONE); } else if (auto deauthenticationFrame = dynamicPtrCast(chunk)) { // type = ST_DEAUTHENTICATION; stream.writeUint16Be(deauthenticationFrame->getReasonCode()); + writeHtElements(stream, deauthenticationFrame, HT_ELEMENT_NONE); } else if (auto disassociationFrame = dynamicPtrCast(chunk)) { // type = ST_DISASSOCIATION; stream.writeUint16Be(disassociationFrame->getReasonCode()); + writeHtElements(stream, disassociationFrame, HT_ELEMENT_NONE); } else if (auto probeRequestFrame = dynamicPtrCast(chunk)) { // type = ST_PROBEREQUEST; @@ -63,11 +258,12 @@ void Ieee80211MgmtFrameSerializer::serialize(MemoryOutputStream& stream, const P // rate |= 0x80 if rate contained in the BSSBasicRateSet parameter stream.writeByte(rate); } + writeHtElements(stream, probeRequestFrame, HT_CAPABILITIES_ALLOWED); // 3 Request information May be included if dot11MultiDomainCapabilityEnabled is true. // 4 Extended Supported Rates The Extended Supported Rates element is present whenever there are more than eight supported rates, and it is optional otherwise. // Last Vendor Specific One or more vendor-specific information elements may appear in this frame. This information element follows all other information elements. } - else if (auto associationRequestFrame = dynamicPtrCast(chunk)) { + else if (auto associationRequestFrame = dynamicPtrCast(chunk); associationRequestFrame && !dynamicPtrCast(chunk)) { // type = ST_ASSOCIATIONREQUEST; // 1 Capability stream.writeUint16Be(0); // FIXME @@ -88,6 +284,7 @@ void Ieee80211MgmtFrameSerializer::serialize(MemoryOutputStream& stream, const P // rate |= 0x80 if rate contained in the BSSBasicRateSet parameter stream.writeByte(rate); } + writeHtElements(stream, associationRequestFrame, HT_CAPABILITIES_ALLOWED); // 5 Extended Supported Rates The Extended Supported Rates element is present whenever there are more than eight supported rates, and it is optional otherwise. // 6 Power Capability The Power Capability element shall be present if dot11SpectrumManagementRequired is true. // 7 Supported Channel The Supported Channels element shall be present if dot11SpectrumManagementRequired is true. @@ -119,6 +316,7 @@ void Ieee80211MgmtFrameSerializer::serialize(MemoryOutputStream& stream, const P // rate |= 0x80 if rate contained in the BSSBasicRateSet parameter stream.writeByte(rate); } + writeHtElements(stream, reassociationRequestFrame, HT_CAPABILITIES_ALLOWED); // 6 Extended Supported Rates The Extended Supported Rates element is present whenever there are more than eight supported rates, and it is optional otherwise. // 7 Power Capability The Power Capability element shall be present if dot11SpectrumManagementRequired is true. // 8 Supported Channels The Supported Channels element shall be present if dot11SpectrumManagementRequired is true. @@ -126,7 +324,7 @@ void Ieee80211MgmtFrameSerializer::serialize(MemoryOutputStream& stream, const P // 10 QoS Capability The QoS Capability element is present when dot11QosOption- Implemented is true. // Last Vendor Specific One or more vendor-specific information elements may appear in this frame. This information element follows all other information elements. } - else if (auto associationResponseFrame = dynamicPtrCast(chunk)) { + else if (auto associationResponseFrame = dynamicPtrCast(chunk); associationResponseFrame && !dynamicPtrCast(chunk)) { // type = ST_ASSOCIATIONRESPONSE; // 1 Capability stream.writeUint16Be(0); // FIXME @@ -142,6 +340,7 @@ void Ieee80211MgmtFrameSerializer::serialize(MemoryOutputStream& stream, const P // rate |= 0x80 if rate contained in the BSSBasicRateSet parameter stream.writeByte(rate); } + writeHtElements(stream, associationResponseFrame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED); // 5 Extended Supported Rates The Extended Supported Rates element is present whenever there are more than eight supported rates, and it is optional otherwise. // 6 EDCA Parameter Set // Last Vendor Specific One or more vendor-specific information elements may appear in this frame. This information element follows all other information elements. @@ -162,11 +361,12 @@ void Ieee80211MgmtFrameSerializer::serialize(MemoryOutputStream& stream, const P // rate |= 0x80 if rate contained in the BSSBasicRateSet parameter stream.writeByte(rate); } + writeHtElements(stream, reassociationResponseFrame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED); // 5 Extended Supported Rates The Extended Supported Rates element is present whenever there are more than eight supported rates, and it is optional otherwise. // 6 EDCA Parameter Set // Last Vendor Specific One or more vendor-specific information elements may appear in this frame. This information element follows all other information elements. } - else if (auto beaconFrame = dynamicPtrCast(chunk)) { + else if (auto beaconFrame = dynamicPtrCast(chunk); beaconFrame && !dynamicPtrCast(chunk)) { // type = ST_BEACON; // 1 Timestamp stream.writeUint64Be(simTime().raw()); // FIXME @@ -188,6 +388,7 @@ void Ieee80211MgmtFrameSerializer::serialize(MemoryOutputStream& stream, const P // rate |= 0x80 if rate contained in the BSSBasicRateSet parameter stream.writeByte(rate); } + writeHtElements(stream, beaconFrame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED); // 6 Frequency-Hopping (FH) Parameter Set The FH Parameter Set information element is present within Beacon frames generated by STAs using FH PHYs. // 7 DS Parameter Set The DS Parameter Set information element is present within Beacon frames generated by STAs using Clause 15, Clause 18, and Clause 19 PHYs. // 8 CF Parameter Set The CF Parameter Set information element is present only within Beacon frames generated by APs supporting a PCF. @@ -231,6 +432,7 @@ void Ieee80211MgmtFrameSerializer::serialize(MemoryOutputStream& stream, const P // rate |= 0x80 if rate contained in the BSSBasicRateSet parameter stream.writeByte(rate); } + writeHtElements(stream, probeResponseFrame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED); // 6 FH Parameter Set The FH Parameter Set information element is present within Probe Response frames generated by STAs using FH PHYs. // 7 DS Parameter Set The DS Parameter Set information element is present within Probe Response frames generated by STAs using Clause 15, Clause 18, and Clause 19 PHYs. // 8 CF Parameter Set The CF Parameter Set information element is present only within Probe Response frames generated by APs supporting a PCF. @@ -255,15 +457,29 @@ void Ieee80211MgmtFrameSerializer::serialize(MemoryOutputStream& stream, const P throw cRuntimeError("Cannot serialize frame"); } -const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& stream) const +const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& stream, const std::type_info& typeInfo) const { - switch (0) { // TODO receive and dispatch on type_info parameter + int frameType = -1; + if (typeInfo == typeid(Ieee80211AuthenticationFrame)) frameType = 0xB0; + else if (typeInfo == typeid(Ieee80211DeauthenticationFrame)) frameType = 0xC0; + else if (typeInfo == typeid(Ieee80211DisassociationFrame)) frameType = 0xA0; + else if (typeInfo == typeid(Ieee80211ProbeRequestFrame)) frameType = 0x40; + else if (typeInfo == typeid(Ieee80211AssociationRequestFrame)) frameType = 0x00; + else if (typeInfo == typeid(Ieee80211ReassociationRequestFrame)) frameType = 0x02; + else if (typeInfo == typeid(Ieee80211AssociationResponseFrame)) frameType = 0x01; + else if (typeInfo == typeid(Ieee80211ReassociationResponseFrame)) frameType = 0x03; + else if (typeInfo == typeid(Ieee80211BeaconFrame)) frameType = 0x80; + else if (typeInfo == typeid(Ieee80211ProbeResponseFrame)) frameType = 0x50; + else throw cRuntimeError("Unsupported IEEE 802.11 management frame type: %s", typeInfo.name()); + + switch (frameType) { case 0xB0: // ST_AUTHENTICATION { auto frame = makeShared(); stream.readUint16Be(); frame->setSequenceNumber(stream.readUint16Be()); frame->setStatusCode((Ieee80211StatusCode)stream.readUint16Be()); + readHtElements(stream, frame, HT_ELEMENT_NONE); return frame; } @@ -271,6 +487,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& st { auto frame = makeShared(); frame->setReasonCode((Ieee80211ReasonCode)stream.readUint16Be()); + readHtElements(stream, frame, HT_ELEMENT_NONE); return frame; } @@ -278,6 +495,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& st { auto frame = makeShared(); frame->setReasonCode((Ieee80211ReasonCode)stream.readUint16Be()); + readHtElements(stream, frame, HT_ELEMENT_NONE); return frame; } @@ -298,6 +516,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& st for (int i = 0; i < supRat.numRates; i++) supRat.rate[i] = (double)(stream.readByte() & 0x7F) * 0.5; frame->setSupportedRates(supRat); + readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED); return frame; } @@ -305,6 +524,9 @@ const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& st { auto frame = makeShared(); + stream.readUint16Be(); + stream.readUint16Be(); + char SSID[256]; stream.readByte(); unsigned int length = stream.readByte(); @@ -318,6 +540,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& st for (int i = 0; i < supRat.numRates; i++) supRat.rate[i] = (double)(stream.readByte() & 0x7F) * 0.5; frame->setSupportedRates(supRat); + readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED); return frame; } @@ -342,6 +565,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& st for (int i = 0; i < supRat.numRates; i++) supRat.rate[i] = (double)(stream.readByte() & 0x7F) * 0.5; frame->setSupportedRates(supRat); + readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED); return frame; } @@ -358,6 +582,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& st for (int i = 0; i < supRat.numRates; i++) supRat.rate[i] = (double)(stream.readByte() & 0x7F) * 0.5; frame->setSupportedRates(supRat); + readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED); return frame; } @@ -374,6 +599,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& st for (int i = 0; i < supRat.numRates; i++) supRat.rate[i] = (double)(stream.readByte() & 0x7F) * 0.5; frame->setSupportedRates(supRat); + readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED); return frame; } @@ -400,6 +626,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& st for (int i = 0; i < supRat.numRates; i++) supRat.rate[i] = (double)(stream.readByte() & 0x7F) * 0.5; frame->setSupportedRates(supRat); + readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED); return frame; } @@ -426,6 +653,7 @@ const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& st for (int i = 0; i < supRat.numRates; i++) supRat.rate[i] = (double)(stream.readByte() & 0x7F) * 0.5; frame->setSupportedRates(supRat); + readHtElements(stream, frame, HT_CAPABILITIES_ALLOWED | HT_OPERATION_ALLOWED); return frame; } @@ -434,7 +662,11 @@ const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& st } } +const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& stream) const +{ + throw cRuntimeError("Ieee80211MgmtFrameSerializer requires the target management frame type"); +} + } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h index b23b021c642..2915910062b 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h @@ -8,6 +8,8 @@ #ifndef __INET_IEEE80211MGMTFRAMESERIALIZER_H #define __INET_IEEE80211MGMTFRAMESERIALIZER_H +#include + #include "inet/common/packet/serializer/FieldsChunkSerializer.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" @@ -23,6 +25,7 @@ class INET_API Ieee80211MgmtFrameSerializer : public FieldsChunkSerializer protected: virtual void serialize(MemoryOutputStream& stream, const Ptr& chunk) const override; virtual const Ptr deserialize(MemoryInputStream& stream) const override; + virtual const Ptr deserialize(MemoryInputStream& stream, const std::type_info& typeInfo) const override; public: Ieee80211MgmtFrameSerializer() : FieldsChunkSerializer() {} @@ -33,4 +36,3 @@ class INET_API Ieee80211MgmtFrameSerializer : public FieldsChunkSerializer } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtProtocolPrinter.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtProtocolPrinter.cc index 3e98ebf3472..8455d17ec61 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtProtocolPrinter.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtProtocolPrinter.cc @@ -19,8 +19,27 @@ Register_Protocol_Printer(&Protocol::ieee80211Mgmt, Ieee80211MgmtProtocolPrinter void Ieee80211MgmtProtocolPrinter::print(const Ptr& chunk, const Protocol *protocol, const cMessagePrinter::Options *options, Context& context) const { context.infoColumn << "(IEEE 802.11 Mgmt) " << chunk; + auto frame = dynamicPtrCast(chunk); + if (frame != nullptr && frame->getHtCapabilitiesPresent()) { + const auto& capabilities = frame->getHtCapabilities(); + int maximumRxMcs = -1; + for (int mcs = 0; mcs < 77; mcs++) + if (capabilities.rxMcsSupported[mcs]) + maximumRxMcs = mcs; + context.infoColumn << " HT-Cap(width=" << (capabilities.supportedChannelWidth40Mhz ? "20/40" : "20") + << "MHz,rxMcs=0.." << maximumRxMcs << ",tx=" + << (capabilities.txMcsSetDefined ? "defined" : "undefined") << ")"; + } + if (frame != nullptr && frame->getHtOperationPresent()) { + const auto& operation = frame->getHtOperation(); + int maximumBasicMcs = -1; + for (int mcs = 0; mcs < 77; mcs++) + if (operation.basicMcsSupported[mcs]) + maximumBasicMcs = mcs; + context.infoColumn << " HT-Op(primary=" << operation.primaryChannel << ",width=" + << (operation.staChannelWidth40Mhz ? 40 : 20) << "MHz,basicMcs=0.." << maximumBasicMcs << ")"; + } } } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc index 0b9ad622375..f1dbd065a3f 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc @@ -6,6 +6,7 @@ #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h" #include "inet/common/INETUtils.h" #include "inet/common/ModuleAccess.h" @@ -267,12 +268,10 @@ void Ieee80211MgmtSta::startAssociation(ApInfo *ap, simtime_t timeout) // create and send association request const auto& body = makeShared(); - - // TODO set the following too? - // string SSID -// Ieee80211SupportedRatesElement supportedRates; - - body->setChunkLength(B(2 + 2 + strlen(body->getSSID()) + 2 + body->getSupportedRates().numRates + 2)); + body->setSSID(ap->ssid.c_str()); + body->setSupportedRates(supportedRates); + addHtCapabilities(body); + body->setChunkLength(B(2 + 2 + (2 + strlen(body->getSSID())) + (2 + body->getSupportedRates().numRates)) + getHtMgmtElementsLength(body)); sendManagementFrame("Assoc", body, ST_ASSOCIATIONREQUEST, ap->address); // schedule timeout @@ -282,6 +281,25 @@ void Ieee80211MgmtSta::startAssociation(ApInfo *ap, simtime_t timeout) scheduleAfter(timeout, assocTimeoutMsg); } +void Ieee80211MgmtSta::startReassociation(ApInfo *ap, simtime_t timeout) +{ + if (!mib->bssStationData.isAssociated || assocTimeoutMsg) + throw cRuntimeError("startReassociation: not associated or association currently in progress"); + if (!ap->isAuthenticated) + throw cRuntimeError("startReassociation: not authenticated with AP address='%s'", ap->address.str().c_str()); + changeChannel(ap->channel); + const auto& body = makeShared(); + body->setCurrentAP(assocAP.address); + body->setSSID(ap->ssid.c_str()); + body->setSupportedRates(supportedRates); + addHtCapabilities(body); + body->setChunkLength(B(2 + 2 + 6 + (2 + strlen(body->getSSID())) + (2 + body->getSupportedRates().numRates)) + getHtMgmtElementsLength(body)); + sendManagementFrame("Reassoc", body, ST_REASSOCIATIONREQUEST, ap->address); + assocTimeoutMsg = new cMessage("assocTimeout", MK_ASSOC_TIMEOUT); + assocTimeoutMsg->setContextPointer(ap); + scheduleAfter(timeout, assocTimeoutMsg); +} + void Ieee80211MgmtSta::receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) { Enter_Method("%s", cComponent::getSignalName(signalID)); @@ -376,7 +394,8 @@ void Ieee80211MgmtSta::sendProbeRequest() const auto& body = makeShared(); body->setSSID(scanning.ssid.c_str()); body->setSupportedRates(supportedRates); - body->setChunkLength(B((2 + scanning.ssid.length()) + (2 + body->getSupportedRates().numRates))); + addHtCapabilities(body); + body->setChunkLength(B((2 + scanning.ssid.length()) + (2 + body->getSupportedRates().numRates)) + getHtMgmtElementsLength(body)); sendManagementFrame("ProbeReq", body, ST_PROBEREQUEST, scanning.bssid); } @@ -448,9 +467,11 @@ void Ieee80211MgmtSta::processAssociateCommand(Ieee80211Prim_AssociateRequest *c void Ieee80211MgmtSta::processReassociateCommand(Ieee80211Prim_ReassociateRequest *ctrl) { - // treat the same way as association - // TODO refine - processAssociateCommand(ctrl); + const MacAddress& address = ctrl->getAddress(); + ApInfo *ap = lookupAP(address); + if (!ap) + throw cRuntimeError("processReassociateCommand: AP not known: address = %s", address.str().c_str()); + startReassociation(ap, ctrl->getTimeout()); } void Ieee80211MgmtSta::processDisassociateCommand(Ieee80211Prim_DisassociateRequest *ctrl) @@ -477,6 +498,7 @@ void Ieee80211MgmtSta::disassociate() EV << "Disassociating from AP address=" << assocAP.address << "\n"; ASSERT(mib->bssStationData.isAssociated); mib->bssStationData.isAssociated = false; + mib->removePeerHtCapabilities(assocAP.address); cancelAndDelete(assocAP.beaconTimeoutMsg); assocAP.beaconTimeoutMsg = nullptr; assocAP = AssociatedApInfo(); // clear it @@ -601,6 +623,7 @@ void Ieee80211MgmtSta::handleDeauthenticationFrame(Packet *packet, const PtrisAuthenticated = false; + mib->removePeerHtCapabilities(address); delete packet; } @@ -611,7 +634,12 @@ void Ieee80211MgmtSta::handleAssociationRequestFrame(Packet *packet, const Ptr& header) { - EV << "Received Association Response frame\n"; + processAssociationResponse(packet, header); +} + +void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const Ptr& header) +{ + EV << "Received Association or Reassociation Response frame\n"; if (!assocTimeoutMsg) { EV << "No association in progress, ignoring frame\n"; @@ -623,37 +651,59 @@ void Ieee80211MgmtSta::handleAssociationResponseFrame(Packet *packet, const Ptr< const auto& responseBody = packet->peekData(); MacAddress address = header->getTransmitterAddress(); int statusCode = responseBody->getStatusCode(); - // TODO short aid; - // TODO Ieee80211SupportedRatesElement supportedRates; - delete packet; - // look up AP data structure ApInfo *ap = lookupAP(address); if (!ap) throw cRuntimeError("handleAssociationResponseFrame: AP not known: address=%s", address.str().c_str()); - if (mib->bssStationData.isAssociated) { - EV << "Breaking existing association with AP address=" << assocAP.address << "\n"; - mib->bssStationData.isAssociated = false; - cancelAndDelete(assocAP.beaconTimeoutMsg); - assocAP.beaconTimeoutMsg = nullptr; - assocAP = AssociatedApInfo(); + bool responseHtValid = false; + Ieee80211HtCapabilities responseHtCapabilities; + Ieee80211HtOperation responseHtOperation; + if (statusCode == SC_SUCCESSFUL && mib->isHtOperationSupported()) { + bool responseHtCapabilitiesPresent = responseBody->getHtCapabilitiesPresent(); + bool responseHtOperationPresent = responseBody->getHtOperationPresent(); + if (responseHtCapabilitiesPresent && responseHtOperationPresent) { + responseHtCapabilities = makeHtCapabilities(responseBody->getHtCapabilities()); + responseHtOperation = makeHtOperation(responseBody->getHtOperation()); + auto negotiated = negotiateHtCapabilities(mib->localHtCapabilities, responseHtCapabilities, responseHtOperation); + responseHtValid = negotiated.localTxPeerRx.valid && negotiated.localRxPeerTx.valid; + } + // The response has already been acknowledged by the MAC. Never rewrite + // an on-air SUCCESS into local failure: doing so would leave the AP and + // STA in different association states. Invalid/absent peer HT data is + // retained conservatively as no negotiated HT state. } + delete packet; cancelAndDelete(assocTimeoutMsg); assocTimeoutMsg = nullptr; if (statusCode != SC_SUCCESSFUL) { EV << "Association failed with AP address=" << ap->address << "\n"; + if (!mib->bssStationData.isAssociated || assocAP.address != ap->address) + mib->removePeerHtCapabilities(ap->address); } else { EV << "Association successful, AP address=" << ap->address << "\n"; + if (mib->bssStationData.isAssociated) { + EV << "Breaking existing association with AP address=" << assocAP.address << "\n"; + mib->bssStationData.isAssociated = false; + mib->removePeerHtCapabilities(assocAP.address); + cancelAndDelete(assocAP.beaconTimeoutMsg); + assocAP.beaconTimeoutMsg = nullptr; + assocAP = AssociatedApInfo(); + } + // change our state to "associated" mib->bssData.ssid = ap->ssid; mib->bssData.bssid = ap->address; mib->bssStationData.isAssociated = true; (ApInfo&)assocAP = (*ap); + if (responseHtValid) + mib->setPeerHtCapabilities(ap->address, responseHtCapabilities, responseHtOperation); + else + mib->removePeerHtCapabilities(ap->address); emit(l2AssociatedSignal, myIface, ap); @@ -672,8 +722,7 @@ void Ieee80211MgmtSta::handleReassociationRequestFrame(Packet *packet, const Ptr void Ieee80211MgmtSta::handleReassociationResponseFrame(Packet *packet, const Ptr& header) { - EV << "Received Reassociation Response frame\n"; - // TODO handle with the same code as Association Response? + processAssociationResponse(packet, header); } void Ieee80211MgmtSta::handleDisassociationFrame(Packet *packet, const Ptr& header) @@ -694,6 +743,7 @@ void Ieee80211MgmtSta::handleDisassociationFrame(Packet *packet, const PtrbssStationData.isAssociated = false; + mib->removePeerHtCapabilities(address); cancelAndDelete(assocAP.beaconTimeoutMsg); assocAP.beaconTimeoutMsg = nullptr; } @@ -747,6 +797,12 @@ void Ieee80211MgmtSta::storeAPInfo(Packet *packet, const Ptraddress = address; ap->ssid = body->getSSID(); ap->supportedRates = body->getSupportedRates(); + ap->htCapabilitiesPresent = body->getHtCapabilitiesPresent(); + if (ap->htCapabilitiesPresent) + ap->htCapabilities = makeHtCapabilities(body->getHtCapabilities()); + ap->htOperationPresent = body->getHtOperationPresent(); + if (ap->htOperationPresent) + ap->htOperation = makeHtOperation(body->getHtOperation()); ap->beaconInterval = body->getBeaconInterval(); auto signalPowerInd = packet->getTag(); if (signalPowerInd != nullptr) { @@ -758,4 +814,3 @@ void Ieee80211MgmtSta::storeAPInfo(Packet *packet, const Ptr& header, const Ptr& body); + /** Processes Association and Reassociation Responses without using cached Beacon capabilities. */ + virtual void processAssociationResponse(Packet *packet, const Ptr& header); + /** Switches to the next channel to scan; returns true if done (there wasn't any more channel to scan). */ virtual bool scanNextChannel(); @@ -190,4 +198,3 @@ class INET_API Ieee80211MgmtSta : public Ieee80211MgmtBase } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc index 7c36bfb8b84..238e776c92a 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc @@ -23,7 +23,7 @@ void Ieee80211MgmtStaSimplified::initialize(int stage) mib->bssStationData.stationType = Ieee80211Mib::STATION; mib->bssStationData.isAssociated = true; } - else if (stage == INITSTAGE_LINK_LAYER) { + else if (stage == INITSTAGE_LAST) { L3AddressResolver addressResolver; auto accessPointAddress = addressResolver.resolve(par("accessPointAddress"), L3AddressResolver::ADDR_MAC).toMac(); mib->bssData.bssid = accessPointAddress; @@ -35,6 +35,12 @@ void Ieee80211MgmtStaSimplified::initialize(int stage) auto apMib = dynamic_cast(networkInterface->getSubmodule("mib")); apMib->bssAccessPointData.stations[mib->address] = Ieee80211Mib::ASSOCIATED; mib->bssData.ssid = apMib->bssData.ssid; + // Simplified management is an explicit no-air abstraction: install the state that the + // Association Request/Response exchange would have committed in detailed management. + if (mib->isHtOperationSupported() && apMib->isHtOperationSupported()) { + mib->setPeerHtCapabilities(apMib->address, apMib->localHtCapabilities, apMib->htOperation); + apMib->setPeerHtCapabilities(mib->address, mib->localHtCapabilities, apMib->htOperation); + } } } @@ -101,4 +107,3 @@ void Ieee80211MgmtStaSimplified::handleProbeResponseFrame(Packet *packet, const } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h new file mode 100644 index 00000000000..2174ce5c387 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h @@ -0,0 +1,152 @@ +// +// Copyright (C) 2026 INET Framework contributors +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IEEE80211HTCAPABILITIES_H +#define __INET_IEEE80211HTCAPABILITIES_H + +#include +#include +#include + +#include "inet/common/Units.h" + +namespace inet { +namespace ieee80211 { + +using namespace units::values; + +// IEEE Std 802.11-2024, Table 9-230. +enum class Ieee80211HtProtectionMode : uint8_t { + NO_PROTECTION = 0, + NONMEMBER_PROTECTION = 1, + TWENTY_MHZ_PROTECTION = 2, + NON_HT_MIXED = 3, +}; + +struct Ieee80211HtMcsNssMap +{ + std::array maxMcsPerNss; + + Ieee80211HtMcsNssMap() { maxMcsPerNss.fill(-1); } +}; + +/** + * Model-backed subset of the HT Capabilities element (IEEE Std 802.11-2024, 9.4.2.54). + * Unrepresented optional capabilities are encoded as unsupported or reserved. + */ +struct Ieee80211HtCapabilities +{ + std::set supportedChannelWidths; + std::array rxMcsSupported = {}; + bool txMcsSetDefined = true; + bool txRxMcsSetNotEqual = false; + int txMaxNss = 0; + bool txUnequalModulation = false; + // Modeled per-NSS local Tx ceiling. It is deliberately left unknown when a + // received unequal Tx/Rx advertisement supplies only Table 9-226's summary fields. + Ieee80211HtMcsNssMap txMcsNss; + bool ldpc = false; + bool greenfield = false; + bool shortGi20 = false; + bool shortGi40 = false; + int maxAmpduLengthExponent = 0; +}; + +/** Model-backed subset of the HT Operation element (IEEE Std 802.11-2024, 9.4.2.55). */ +struct Ieee80211HtOperation +{ + Hz operatingChannelWidth = MHz(20); + int primaryChannel = 0; + int secondaryChannelOffset = 0; + Ieee80211HtProtectionMode protectionMode = Ieee80211HtProtectionMode::NO_PROTECTION; + std::array basicMcsSupported = {}; +}; + +struct Ieee80211HtDirectionalCapabilities +{ + bool valid = false; + std::set supportedChannelWidths; + Ieee80211HtMcsNssMap mcsNss; + std::array supportedMcs = {}; + bool receiverLdpc = false; + bool receiverShortGi20 = false; + bool receiverShortGi40 = false; + int receiverMaxAmpduLengthExponent = 0; +}; + +struct Ieee80211NegotiatedHtCapabilities +{ + Ieee80211HtCapabilities localAdvertisement; + Ieee80211HtCapabilities peerAdvertisement; + Ieee80211HtDirectionalCapabilities localTxPeerRx; + Ieee80211HtDirectionalCapabilities localRxPeerTx; + Ieee80211HtOperation operation; +}; + +inline Ieee80211NegotiatedHtCapabilities negotiateHtCapabilities(const Ieee80211HtCapabilities& local, + const Ieee80211HtCapabilities& peer, const Ieee80211HtOperation& operation) +{ + Ieee80211NegotiatedHtCapabilities negotiated; + negotiated.localAdvertisement = local; + negotiated.peerAdvertisement = peer; + negotiated.operation = operation; + for (const auto& width : local.supportedChannelWidths) + if (peer.supportedChannelWidths.count(width)) { + negotiated.localTxPeerRx.supportedChannelWidths.insert(width); + negotiated.localRxPeerTx.supportedChannelWidths.insert(width); + } + for (int mcs = 0; mcs < 77; mcs++) { + int nss = mcs / 8; + bool localTx = local.txMcsSetDefined && !local.txRxMcsSetNotEqual ? local.rxMcsSupported[mcs] : + nss < 4 && local.txMcsNss.maxMcsPerNss[nss] >= mcs % 8; + // Table 9-226 defines the equal case as the exact Rx MCS bitmap, not + // a contiguous range ending at the highest advertised MCS. + bool peerTx = peer.txMcsSetDefined && !peer.txRxMcsSetNotEqual && peer.rxMcsSupported[mcs]; + negotiated.localTxPeerRx.supportedMcs[mcs] = localTx && peer.rxMcsSupported[mcs]; + negotiated.localRxPeerTx.supportedMcs[mcs] = local.rxMcsSupported[mcs] && peerTx; + } + for (int nss = 0; nss < 4; nss++) { + for (int mcs = 0; mcs < 8; mcs++) { + if (negotiated.localTxPeerRx.supportedMcs[nss * 8 + mcs]) + negotiated.localTxPeerRx.mcsNss.maxMcsPerNss[nss] = mcs; + if (negotiated.localRxPeerTx.supportedMcs[nss * 8 + mcs]) + negotiated.localRxPeerTx.mcsNss.maxMcsPerNss[nss] = mcs; + } + } + // LDPC and short-GI bits advertise receiver capability, so they are directional. + negotiated.localTxPeerRx.receiverLdpc = peer.ldpc; + negotiated.localRxPeerTx.receiverLdpc = local.ldpc; + negotiated.localTxPeerRx.receiverShortGi20 = peer.shortGi20; + negotiated.localRxPeerTx.receiverShortGi20 = local.shortGi20; + negotiated.localTxPeerRx.receiverShortGi40 = peer.shortGi40; + negotiated.localRxPeerTx.receiverShortGi40 = local.shortGi40; + negotiated.localTxPeerRx.receiverMaxAmpduLengthExponent = peer.maxAmpduLengthExponent; + negotiated.localRxPeerTx.receiverMaxAmpduLengthExponent = local.maxAmpduLengthExponent; + // The HT Operation width is the BSS maximum. A 20 MHz-only STA may join a + // 20/40 MHz BSS, so validity requires a common usable width, not equality + // with the advertised BSS width (IEEE Std 802.11-2024, 11.15.2). + negotiated.localTxPeerRx.valid = !negotiated.localTxPeerRx.supportedChannelWidths.empty() && + negotiated.localTxPeerRx.supportedMcs[0]; + // An undefined peer Tx MCS set is permitted by Table 9-226. It is unknown, + // rather than an advertisement that the peer cannot transmit. + bool peerTxMcsUnknown = !peer.txMcsSetDefined || peer.txRxMcsSetNotEqual; + negotiated.localRxPeerTx.valid = !negotiated.localRxPeerTx.supportedChannelWidths.empty() && + (peerTxMcsUnknown || negotiated.localRxPeerTx.supportedMcs[0]); + return negotiated; +} + +inline bool supportsBasicHtMcsSet(const Ieee80211HtCapabilities& capabilities, const Ieee80211HtOperation& operation) +{ + for (int mcs = 0; mcs < 77; mcs++) + if (operation.basicMcsSupported[mcs] && !capabilities.rxMcsSupported[mcs]) + return false; + return true; +} + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index 3d55b8ce6c7..328d2cfb61c 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" + namespace inet { namespace ieee80211 { @@ -19,6 +21,7 @@ void Ieee80211Mib::initialize(int stage) WATCH(address); WATCH(mode); WATCH(qos); + WATCH(localHtCapabilitiesValid); WATCH(bssData.bssid); WATCH(bssStationData.stationType); WATCH(bssStationData.isAssociated); @@ -33,6 +36,91 @@ void Ieee80211Mib::initialize(int stage) } } +void Ieee80211Mib::updateLocalHtCapabilities(const physicallayer::Ieee80211ModeSet *modeSet) +{ + // The radio publishes its initial channel at PHYSICAL_LAYER before the MAC + // publishes its mode set at LINK_LAYER. Preserve that independent BSS + // operation input when rebuilding the mode-derived capability subset. + int primaryChannel = htOperation.primaryChannel; + localHtCapabilities = Ieee80211HtCapabilities(); + htOperation = Ieee80211HtOperation(); + htOperation.primaryChannel = primaryChannel; + localHtCapabilitiesValid = modeSet != nullptr && modeSet->isHtOperationSupported(); + if (!localHtCapabilitiesValid) { + clearPeerHtCapabilities(); + return; + } + + // IEEE Std 802.11-2024, 9.4.2.54.4: MCS 0-7 are the conservative single-stream HT baseline. + int maxNss = std::min(4, modeSet->getMaximumNumberOfSpatialStreams()); + if (maxNss < 1) + throw cRuntimeError("HT operation mode does not provide a spatial stream"); + for (int i = 0; i < maxNss; i++) { + localHtCapabilities.txMcsNss.maxMcsPerNss[i] = 7; + for (int mcs = 0; mcs <= 7; mcs++) + localHtCapabilities.rxMcsSupported[i * 8 + mcs] = true; + } + localHtCapabilities.supportedChannelWidths.insert(MHz(20)); + + if (modeSet->getMaximumChannelWidth() >= MHz(40)) + localHtCapabilities.supportedChannelWidths.insert(MHz(40)); + localHtCapabilities.maxAmpduLengthExponent = par("htMaxAmpduLengthExponent"); + 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) + throw cRuntimeError("htSecondaryChannelOffset must be 0, 1, or 3"); + bool use40Mhz = htOperation.secondaryChannelOffset != 0; + if (use40Mhz && localHtCapabilities.supportedChannelWidths.count(MHz(40)) == 0) + throw cRuntimeError("40 MHz HT operation requires a 40 MHz-capable mode set"); + htOperation.operatingChannelWidth = use40Mhz ? MHz(40) : MHz(20); + int protectionMode = par("htProtectionMode"); + if (protectionMode < 0 || protectionMode > 3) + throw cRuntimeError("htProtectionMode must be between 0 and 3"); + htOperation.protectionMode = static_cast(protectionMode); + // Clause 19 HT devices support the mandatory single-stream MCS 0-7 set. + for (int mcs = 0; mcs <= 7; mcs++) + htOperation.basicMcsSupported[mcs] = true; + + for (auto& entry : peerHtStates) + if (entry.second.valid) + entry.second.negotiatedCapabilities = negotiateHtCapabilities(localHtCapabilities, + entry.second.advertisedCapabilities, entry.second.negotiatedCapabilities.operation); +} + +const Ieee80211Mib::PeerHtState *Ieee80211Mib::findPeerHtState(const MacAddress& address) const +{ + auto it = peerHtStates.find(address); + return it == peerHtStates.end() || !it->second.valid ? nullptr : &it->second; +} + +void Ieee80211Mib::setPeerHtCapabilities(const MacAddress& address, const Ieee80211HtCapabilities& capabilities, + const Ieee80211HtOperation& operation) +{ + if (!localHtCapabilitiesValid) + throw cRuntimeError("Cannot install peer HT capabilities when local HT operation is disabled"); + auto& state = peerHtStates[address]; + state.valid = true; + state.advertisedCapabilities = capabilities; + state.negotiatedCapabilities = negotiateHtCapabilities(localHtCapabilities, capabilities, operation); + if (++state.generation == 0) + state.generation = 1; + EV_INFO << "Installed peer HT state, peer = " << address + << ", txValid = " << state.negotiatedCapabilities.localTxPeerRx.valid + << ", rxValid = " << state.negotiatedCapabilities.localRxPeerTx.valid << endl; +} + +void Ieee80211Mib::removePeerHtCapabilities(const MacAddress& address) +{ + peerHtStates.erase(address); +} + +void Ieee80211Mib::clearPeerHtCapabilities() +{ + peerHtStates.clear(); +} + std::string Ieee80211Mib::getSsidStr() const { if (mode == INFRASTRUCTURE) @@ -82,6 +170,7 @@ short Ieee80211Mib::allocateAssociationId(const MacAddress& address) void Ieee80211Mib::releaseAssociationId(const MacAddress& address) { bssAccessPointData.associationIds.erase(address); + removePeerHtCapabilities(address); } } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h index 2183415fdd5..d01e6e59e0c 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h @@ -10,9 +10,14 @@ #include "inet/common/SimpleModule.h" #include "inet/linklayer/common/MacAddress.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h" namespace inet { +namespace physicallayer { +class Ieee80211ModeSet; +} + namespace ieee80211 { class INET_API Ieee80211Mib : public SimpleModule @@ -53,6 +58,14 @@ class INET_API Ieee80211Mib : public SimpleModule std::map associationIds; }; + class INET_API PeerHtState { + public: + bool valid = false; + Ieee80211HtCapabilities advertisedCapabilities; + Ieee80211NegotiatedHtCapabilities negotiatedCapabilities; + uint64_t generation = 0; + }; + public: MacAddress address; Mode mode = static_cast(-1); @@ -62,6 +75,14 @@ class INET_API Ieee80211Mib : public SimpleModule BssStationData bssStationData; BssAccessPointData bssAccessPointData; + // This is a deliberately model-backed subset, not a full Annex C HT MIB implementation. + bool localHtCapabilitiesValid = false; + Ieee80211HtCapabilities localHtCapabilities; + Ieee80211HtOperation htOperation; + + private: + std::map peerHtStates; + protected: virtual void initialize(int stage) override; @@ -71,6 +92,12 @@ class INET_API Ieee80211Mib : public SimpleModule std::string getSsidStr() const; short allocateAssociationId(const MacAddress& address); void releaseAssociationId(const MacAddress& address); + void updateLocalHtCapabilities(const physicallayer::Ieee80211ModeSet *modeSet); + bool isHtOperationSupported() const { return localHtCapabilitiesValid; } + const PeerHtState *findPeerHtState(const MacAddress& address) const; + void setPeerHtCapabilities(const MacAddress& address, const Ieee80211HtCapabilities& capabilities, const Ieee80211HtOperation& operation); + void removePeerHtCapabilities(const MacAddress& address); + void clearPeerHtCapabilities(); }; } // namespace ieee80211 diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned index 81204d26cb7..bb75ffab705 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.ned @@ -20,7 +20,10 @@ simple Ieee80211Mib extends SimpleModule { parameters: @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 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"); } - diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index 929d3e8cecc..f0be04b94d8 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -141,7 +141,7 @@ const DelayedInitializer> Ieee80211ModeSet::modeSe { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs29BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs30BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) }, { false, Ieee80211HtCompliantModes::getCompliantMode(&Ieee80211HtmcsTable::htMcs31BW40MHz, Ieee80211HtMode::BAND_2_4GHZ, Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) } - }), + }, true), 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) }, @@ -453,11 +453,12 @@ const DelayedInitializer> Ieee80211ModeSet::modeSe { false, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs7BW160MHzNss8, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT) }, { false, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs8BW160MHzNss8, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT) }, { false, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs9BW160MHzNss8, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT) }, -}),}; }); +}, true),}; }); -Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector entries) : +Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector entries, bool htOperationSupported) : name(name), - entries(entries) + entries(entries), + htOperationSupported(htOperationSupported) { std::vector *nonConstEntries = const_cast *>(&this->entries); std::stable_sort(nonConstEntries->begin(), nonConstEntries->end(), EntryNetBitrateComparator()); @@ -473,6 +474,22 @@ Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector en } } +Hz Ieee80211ModeSet::getMaximumChannelWidth() const +{ + Hz maximum(0); + for (const auto& entry : entries) + maximum = std::max(maximum, entry.mode->getDataMode()->getBandwidth()); + return maximum; +} + +int Ieee80211ModeSet::getMaximumNumberOfSpatialStreams() const +{ + int maximum = 0; + for (const auto& entry : entries) + maximum = std::max(maximum, entry.mode->getDataMode()->getNumberOfSpatialStreams()); + return maximum; +} + int Ieee80211ModeSet::findModeIndex(const IIeee80211Mode *mode) const { for (size_t index = 0; index < entries.size(); index++) @@ -626,4 +643,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 98155bf77f6..2f37d7816a8 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h @@ -30,6 +30,7 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject protected: std::string name; const std::vector entries; + bool htOperationSupported = false; public: static const DelayedInitializer> modeSets; @@ -39,15 +40,18 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject int getModeIndex(const IIeee80211Mode *mode) const; public: - Ieee80211ModeSet(const char *name, const std::vector entries); + Ieee80211ModeSet(const char *name, const std::vector entries, bool htOperationSupported = false); virtual std::ostream& printToStream(std::ostream& stream, int level, int evFlags = 0) const override { return stream << "Ieee80211ModeSet, name = " << name; } const char *getName() const override { return name.c_str(); } int getNumModes() const { return entries.size(); } - const IIeee80211Mode *getMode(int index) { return entries[index].mode; } - bool isMandatory(int index) { return entries[index].isMandatory; } + const IIeee80211Mode *getMode(int index) const { return entries[index].mode; } + bool isMandatory(int index) const { return entries[index].isMandatory; } + bool isHtOperationSupported() const { return htOperationSupported; } + Hz getMaximumChannelWidth() const; + int getMaximumNumberOfSpatialStreams() const; bool containsMode(const IIeee80211Mode *mode) const { return findModeIndex(mode) != -1; } bool getIsMandatory(const IIeee80211Mode *mode) const; @@ -84,4 +88,3 @@ class INET_API Ieee80211ModeSet : public IPrintableObject, public cObject } // namespace inet #endif - diff --git a/tests/module/Ieee80211HtAssociation_1.test b/tests/module/Ieee80211HtAssociation_1.test new file mode 100644 index 00000000000..846c374d90d --- /dev/null +++ b/tests/module/Ieee80211HtAssociation_1.test @@ -0,0 +1,75 @@ +%description: +Verify the detailed IEEE 802.11 management exchange carries the standard HT +element presence matrix and commits mutually usable peer state at both ends. + +%file: test.ned + +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +network Ieee80211HtAssociationTest +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + sta: WirelessHost { + parameters: + wlan[*].mgmt.typename = "Ieee80211MgmtSta"; + wlan[*].agent.typename = "Ieee80211AgentSta"; + } + ap: AccessPoint { + parameters: + wlan[*].mgmt.typename = "Ieee80211MgmtAp"; + } +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211HtAssociationTest +ned-path = .;../../../../src;../../lib +sim-time-limit = 50ms +cmdenv-express-mode = false +record-vector-results = false +record-scalar-results = false + +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMinZ = 0m +**.constraintAreaMaxX = 100m +**.constraintAreaMaxY = 100m +**.constraintAreaMaxZ = 0m +*.sta.mobility.initialX = 10m +*.sta.mobility.initialY = 10m +*.ap.mobility.initialX = 20m +*.ap.mobility.initialY = 10m +**.mobility.initialZ = 0m + +**.wlan[*].opMode = "n(mixed-2.4Ghz)" +**.wlan[*].bitrate = 65Mbps +**.wlan[*].radio.bandName = "2.4 GHz" +**.wlan[*].radio.centerFrequency = 2.4GHz +**.wlan[*].radio.antenna.numAntennas = 1 +**.wlan[*].radio.transmitter.power = 100mW +**.wlan[*].radio.receiver.sensitivity = -85dBm +**.wlan[*].radio.receiver.snirThreshold = 4dB + +**.mgmt.numChannels = 1 +*.sta.wlan[*].agent.startingTime = 0s +*.sta.wlan[*].agent.probeDelay = 1ms +*.sta.wlan[*].agent.minChannelTime = 5ms +*.sta.wlan[*].agent.maxChannelTime = 5ms +*.sta.wlan[*].agent.authenticationTimeout = 20ms +*.sta.wlan[*].agent.associationTimeout = 20ms + +%contains: stdout +Association successful, AP address= + +%contains-regex: stdout +Assoc .*?Ieee80211AssociationRequestFrame.*?htCapabilitiesPresent.*?true.*?htOperationPresent.*?false + +%contains-regex: stdout +AssocResp-OK .*?Ieee80211AssociationResponseFrame.*?htCapabilitiesPresent.*?true.*?htOperationPresent.*?true + +%contains: stdout +Installed peer HT state, peer = diff --git a/tests/unit/Ieee80211HtCapabilities_1.test b/tests/unit/Ieee80211HtCapabilities_1.test new file mode 100644 index 00000000000..2c11c043ee1 --- /dev/null +++ b/tests/unit/Ieee80211HtCapabilities_1.test @@ -0,0 +1,109 @@ +%description: +Verify directional HT negotiation and association-response classification, +including acceptance of a genuinely legacy AP. + +%includes: +#include "inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211HtCapabilities.h" + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::units::values; + +%activity: + +Ieee80211HtCapabilities local; +local.supportedChannelWidths.insert(MHz(20)); +local.rxMcsSupported[0] = true; +local.rxMcsSupported[1] = true; +local.rxMcsSupported[2] = true; +local.rxMcsSupported[3] = true; +local.txMcsNss.maxMcsPerNss[0] = 3; +local.ldpc = true; +local.shortGi20 = true; +local.maxAmpduLengthExponent = 1; + +Ieee80211HtCapabilities peer; +peer.supportedChannelWidths.insert(MHz(20)); +peer.rxMcsSupported[0] = true; +peer.rxMcsSupported[2] = true; +peer.rxMcsSupported[3] = true; +peer.txMcsNss.maxMcsPerNss[0] = 3; +peer.maxAmpduLengthExponent = 3; + +Ieee80211HtOperation operation; +operation.operatingChannelWidth = MHz(20); +operation.basicMcsSupported[0] = true; + +auto negotiated = negotiateHtCapabilities(local, peer, operation); +ASSERT(negotiated.localTxPeerRx.valid); +ASSERT(negotiated.localRxPeerTx.valid); +ASSERT(negotiated.localTxPeerRx.supportedMcs[0]); +ASSERT(!negotiated.localTxPeerRx.supportedMcs[1]); +ASSERT(negotiated.localTxPeerRx.supportedMcs[2]); +ASSERT(negotiated.localTxPeerRx.supportedMcs[3]); +ASSERT(negotiated.localRxPeerTx.supportedMcs[0]); +ASSERT(!negotiated.localRxPeerTx.supportedMcs[1]); +ASSERT(negotiated.localRxPeerTx.supportedMcs[2]); +ASSERT(!negotiated.localTxPeerRx.receiverLdpc); +ASSERT(negotiated.localRxPeerTx.receiverLdpc); +ASSERT(negotiated.localTxPeerRx.receiverMaxAmpduLengthExponent == 3); +ASSERT(negotiated.localRxPeerTx.receiverMaxAmpduLengthExponent == 1); + +// A 20 MHz-only peer may join a 20/40 MHz BSS. +local.supportedChannelWidths.insert(MHz(40)); +operation.operatingChannelWidth = MHz(40); +negotiated = negotiateHtCapabilities(local, peer, operation); +ASSERT(negotiated.localTxPeerRx.valid); +ASSERT(negotiated.localTxPeerRx.supportedChannelWidths.count(MHz(20)) == 1); + +// Table 9-226 permits an undefined Tx MCS set; unknown is not "cannot transmit". +peer.txMcsSetDefined = false; +peer.txMcsNss = Ieee80211HtMcsNssMap(); +negotiated = negotiateHtCapabilities(local, peer, operation); +ASSERT(negotiated.localRxPeerTx.valid); + +operation.basicMcsSupported[1] = true; +ASSERT(!supportsBasicHtMcsSet(peer, operation)); +peer.rxMcsSupported[1] = true; +ASSERT(supportsBasicHtMcsSet(peer, operation)); + +Ieee80211HtCapabilities rxOnly; +rxOnly.supportedChannelWidths.insert(MHz(20)); +rxOnly.rxMcsSupported[0] = true; +rxOnly.txMcsSetDefined = false; +auto rxOnlyElement = makeHtCapabilitiesElement(rxOnly); +ASSERT(!rxOnlyElement.txMcsSetDefined); +ASSERT(!rxOnlyElement.txRxMcsSetNotEqual); +ASSERT(rxOnlyElement.txMaxNss == 0); + +// Table 9-226 does not carry an exact Tx bitmap for unequal Tx/Rx sets. +for (bool unequalModulation : {false, true}) { + Ieee80211HtCapabilitiesElement unequalElement; + unequalElement.supportedChannelWidth40Mhz = true; + unequalElement.rxMcsSupported[0] = true; + unequalElement.rxMcsSupported[7] = true; + unequalElement.rxMcsSupported[8] = true; + unequalElement.txMcsSetDefined = true; + unequalElement.txRxMcsSetNotEqual = true; + unequalElement.txMaxNss = 2; + unequalElement.txUnequalModulation = unequalModulation; + auto unequal = makeHtCapabilities(unequalElement); + ASSERT(unequal.txRxMcsSetNotEqual); + ASSERT(unequal.txMaxNss == 2); + ASSERT(unequal.txUnequalModulation == unequalModulation); + ASSERT(unequal.txMcsNss.maxMcsPerNss[0] == -1); + ASSERT(unequal.txMcsNss.maxMcsPerNss[1] == -1); + auto unequalRoundTrip = makeHtCapabilitiesElement(unequal); + ASSERT(unequalRoundTrip.txRxMcsSetNotEqual); + ASSERT(unequalRoundTrip.txMaxNss == 2); + ASSERT(unequalRoundTrip.txUnequalModulation == unequalModulation); + auto unequalNegotiated = negotiateHtCapabilities(local, unequal, operation); + ASSERT(unequalNegotiated.localRxPeerTx.valid); + ASSERT(!unequalNegotiated.localRxPeerTx.supportedMcs[0]); +} + +EV << "HT directional and legacy association checks passed.\n"; + +%contains: stdout +HT directional and legacy association checks passed. diff --git a/tests/unit/Ieee80211HtMgmtElements_1.test b/tests/unit/Ieee80211HtMgmtElements_1.test new file mode 100644 index 00000000000..0d5a92cb6cd --- /dev/null +++ b/tests/unit/Ieee80211HtMgmtElements_1.test @@ -0,0 +1,252 @@ +%description: +Verify byte-exact HT Capabilities and HT Operation management elements, including +non-contiguous MCS bitmaps, typed deserialization, and malformed-length rejection. + +%includes: +#include "inet/common/packet/Packet.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrame_m.h" + +using namespace inet; +using namespace inet::ieee80211; + +%global: + +static Ptr makeHtResponse() +{ + auto frame = makeShared(); + frame->setStatusCode(SC_SUCCESSFUL); + frame->setAid(1); + Ieee80211SupportedRatesElement rates; + rates.numRates = 1; + rates.rate[0] = 6; + frame->setSupportedRates(rates); + + Ieee80211HtCapabilitiesElement capabilities; + capabilities.ldpc = true; + capabilities.supportedChannelWidth40Mhz = true; + capabilities.greenfield = true; + capabilities.shortGi20 = true; + capabilities.shortGi40 = true; + capabilities.maxAmpduLengthExponent = 2; + capabilities.rxMcsSupported[0] = true; + capabilities.rxMcsSupported[2] = true; + capabilities.rxMcsSupported[7] = true; + capabilities.rxMcsSupported[15] = true; + capabilities.rxMcsSupported[76] = true; + capabilities.txMcsSetDefined = true; + frame->setHtCapabilitiesPresent(true); + frame->setHtCapabilities(capabilities); + + Ieee80211HtOperationElement operation; + operation.primaryChannel = 36; + operation.secondaryChannelOffset = 1; + operation.staChannelWidth40Mhz = true; + operation.protectionMode = 2; + operation.basicMcsSupported[0] = true; + operation.basicMcsSupported[3] = true; + operation.basicMcsSupported[7] = true; + operation.basicMcsSupported[76] = true; + frame->setHtOperationPresent(true); + frame->setHtOperation(operation); + + // Fixed fields/rates (9) + HT Capabilities IE (28) + HT Operation IE (24). + frame->setChunkLength(B(61)); + return frame; +} + +static std::vector serializeResponse(const Ptr& frame) +{ + Packet packet("ht-response", frame); + return packet.peekAllAsBytes()->getBytes(); +} + +%activity: + +auto original = makeHtResponse(); +auto bytes = serializeResponse(original); +ASSERT(bytes.size() == 61); + +// IEEE Std 802.11-2024, 9.4.2.54: element ID 45 and fixed 26-octet body. +ASSERT(bytes[9] == 45 && bytes[10] == 26); +ASSERT(bytes[11] == 0x7f && bytes[12] == 0x00); // modeled flags plus SMPS=3 (disabled) +ASSERT(bytes[13] == 0x02); +ASSERT(bytes[14] == 0x85); // non-contiguous Rx MCS 0, 2, and 7 +ASSERT(bytes[15] == 0x80); // Rx MCS 15 +ASSERT(bytes[23] == 0x10); // Rx MCS 76 +ASSERT(bytes[26] == 0x01); // Tx MCS Set Defined (bit 96) + +// IEEE Std 802.11-2024, 9.4.2.55: element ID 61 and fixed 22-octet body. +ASSERT(bytes[37] == 61 && bytes[38] == 22); +ASSERT(bytes[39] == 36); +ASSERT(bytes[40] == 0x05 && bytes[41] == 0x02); +ASSERT(bytes[45] == 0x89); // non-contiguous Basic MCS 0, 3, and 7 +ASSERT(bytes[54] == 0x10); // Basic MCS 76 + +Packet encoded("encoded", makeShared(bytes)); +auto decoded = encoded.popAtFront(B(bytes.size())); +ASSERT(decoded->getHtCapabilitiesPresent()); +ASSERT(decoded->getHtOperationPresent()); +ASSERT(decoded->getHtCapabilities().rxMcsSupported[0]); +ASSERT(!decoded->getHtCapabilities().rxMcsSupported[1]); +ASSERT(decoded->getHtCapabilities().rxMcsSupported[2]); +ASSERT(decoded->getHtCapabilities().rxMcsSupported[76]); +ASSERT(decoded->getHtOperation().basicMcsSupported[0]); +ASSERT(!decoded->getHtOperation().basicMcsSupported[1]); +ASSERT(decoded->getHtOperation().basicMcsSupported[3]); +ASSERT(decoded->getHtOperation().basicMcsSupported[76]); + +auto unequalResponse = makeHtResponse(); +auto unequalCapabilities = unequalResponse->getHtCapabilities(); +unequalCapabilities.txRxMcsSetNotEqual = true; +unequalCapabilities.txMaxNss = 2; +unequalCapabilities.txUnequalModulation = true; +unequalResponse->setHtCapabilities(unequalCapabilities); +auto unequalBytes = serializeResponse(unequalResponse); +ASSERT(unequalBytes[26] == 0x17); +Packet unequalEncoded("unequal-encoded", makeShared(unequalBytes)); +auto unequalDecoded = unequalEncoded.popAtFront(B(unequalBytes.size())); +ASSERT(unequalDecoded->getHtCapabilities().txRxMcsSetNotEqual); +ASSERT(unequalDecoded->getHtCapabilities().txMaxNss == 2); +ASSERT(unequalDecoded->getHtCapabilities().txUnequalModulation); + +// Copy decoded fields into a fresh chunk so serialization cannot reuse cached bytes. +auto reencoded = makeShared(); +reencoded->setStatusCode(decoded->getStatusCode()); +reencoded->setAid(decoded->getAid()); +reencoded->setSupportedRates(decoded->getSupportedRates()); +reencoded->setHtCapabilitiesPresent(decoded->getHtCapabilitiesPresent()); +reencoded->setHtCapabilities(decoded->getHtCapabilities()); +reencoded->setHtOperationPresent(decoded->getHtOperationPresent()); +reencoded->setHtOperation(decoded->getHtOperation()); +reencoded->setChunkLength(B(61)); +ASSERT(serializeResponse(reencoded) == bytes); + +auto malformed = bytes; +malformed[10] = 25; +bool rejected = false; +try { + Packet packet("malformed", makeShared(malformed)); + packet.popAtFront(B(malformed.size())); +} +catch (const cRuntimeError&) { + rejected = true; +} +ASSERT(rejected); + +auto expectRejected = [](const std::vector& invalid) { + bool rejected = false; + try { + Packet packet("invalid", makeShared(invalid)); + packet.popAtFront(B(invalid.size())); + } + catch (const cRuntimeError&) { + rejected = true; + } + ASSERT(rejected); +}; + +auto malformedOperation = bytes; +malformedOperation[38] = 21; +expectRejected(malformedOperation); +auto truncated = bytes; +truncated.pop_back(); +expectRejected(truncated); +auto duplicate = bytes; +duplicate.insert(duplicate.end(), bytes.begin() + 9, bytes.begin() + 37); +expectRejected(duplicate); +auto reservedOffset = bytes; +reservedOffset[40] = (reservedOffset[40] & ~3) | 2; +expectRejected(reservedOffset); +auto invalidTxMcs = bytes; +invalidTxMcs[26] = 0x02; // Tx undefined while Tx/Rx unequal is set +expectRejected(invalidTxMcs); + +auto invalidRequest = makeShared(); +invalidRequest->setSSID("h"); +invalidRequest->setSupportedRates(original->getSupportedRates()); +invalidRequest->setHtOperationPresent(true); +invalidRequest->setHtOperation(original->getHtOperation()); +invalidRequest->setChunkLength(B(34)); +bool invalidSubtypeRejected = false; +try { + Packet packet("invalid-subtype", invalidRequest); + packet.peekAllAsBytes(); +} +catch (const cRuntimeError&) { + invalidSubtypeRejected = true; +} +ASSERT(invalidSubtypeRejected); + +auto expectSerializationRejected = [](const auto& frame) { + bool rejected = false; + try { + Packet packet("forbidden-elements", frame); + packet.peekAllAsBytes(); + } + catch (const cRuntimeError&) { + rejected = true; + } + ASSERT(rejected); +}; + +auto authentication = makeShared(); +authentication->setHtCapabilitiesPresent(true); +authentication->setHtCapabilities(original->getHtCapabilities()); +authentication->setChunkLength(B(34)); +expectSerializationRejected(authentication); + +auto deauthentication = makeShared(); +deauthentication->setHtCapabilitiesPresent(true); +deauthentication->setHtCapabilities(original->getHtCapabilities()); +deauthentication->setChunkLength(B(30)); +expectSerializationRejected(deauthentication); + +auto disassociation = makeShared(); +disassociation->setHtOperationPresent(true); +disassociation->setHtOperation(original->getHtOperation()); +disassociation->setChunkLength(B(26)); +expectSerializationRejected(disassociation); + +auto probeRequest = makeShared(); +probeRequest->setSSID("h"); +probeRequest->setSupportedRates(original->getSupportedRates()); +probeRequest->setHtOperationPresent(true); +probeRequest->setHtOperation(original->getHtOperation()); +probeRequest->setChunkLength(B(30)); +expectSerializationRejected(probeRequest); + +auto reassociationRequest = makeShared(); +reassociationRequest->setSSID("h"); +reassociationRequest->setSupportedRates(original->getSupportedRates()); +reassociationRequest->setHtOperationPresent(true); +reassociationRequest->setHtOperation(original->getHtOperation()); +reassociationRequest->setChunkLength(B(40)); +expectSerializationRejected(reassociationRequest); + +auto allowedProbeRequest = makeShared(); +allowedProbeRequest->setSSID("h"); +allowedProbeRequest->setSupportedRates(original->getSupportedRates()); +allowedProbeRequest->setHtCapabilitiesPresent(true); +allowedProbeRequest->setHtCapabilities(original->getHtCapabilities()); +allowedProbeRequest->setChunkLength(B(34)); +Packet allowedProbeRequestPacket("allowed-probe-request", allowedProbeRequest); +ASSERT(allowedProbeRequestPacket.peekAllAsBytes()->getChunkLength() == B(34)); + +for (bool probeResponse : {false, true}) { + Ptr discovery = probeResponse ? staticPtrCast(makeShared()) : makeShared(); + discovery->setSSID(""); + discovery->setSupportedRates(original->getSupportedRates()); + discovery->setHtCapabilitiesPresent(true); + discovery->setHtCapabilities(original->getHtCapabilities()); + discovery->setHtOperationPresent(true); + discovery->setHtOperation(original->getHtOperation()); + discovery->setChunkLength(B(69)); + Packet discoveryPacket("allowed-discovery", discovery); + ASSERT(discoveryPacket.peekAllAsBytes()->getChunkLength() == B(69)); +} + +EV << "HT management element byte and validation checks passed.\n"; + +%contains: stdout +HT management element byte and validation checks passed. From c688664231c0e377a1a904cce601eb73bd3e1f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 17 Aug 2026 15:01:19 +0200 Subject: [PATCH 02/10] Fix IEEE 802.11 HT association state handling Restore FieldsChunkSerializer bookkeeping for typed management frame deserialization so decoded chunks receive their consumed length and retain their serialized-data cache. Replace borrowed association-response packet pointers with deterministic sender-local transaction tags that survive fragmentation and RTS protection. Copy packet tags and correctly slice region tags when fragments are created, and commit or cancel pending association state only after authoritative exchange completion. Centralize association ID reservation, commit, cancellation, release, and cleanup in Ieee80211Mib using deterministic linear allocation. Mark the VHT-only ac mode profile as not advertising modeled HT operation. Add focused unit coverage for serializer length and cache preservation, bounded management-body decoding, transaction disposition and fragmentation propagation, association ID lifecycle, and HT mode-set gating. Validation: debug build; 5 focused unit tests; detailed HT association module test; lan80211ac Ping1 and DCF fragmentation fingerprints unchanged. --- .../mac/fragmentation/Fragmentation.cc | 4 +- .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 143 ++++++++++-------- .../ieee80211/mgmt/Ieee80211MgmtAp.h | 19 ++- .../mgmt/Ieee80211MgmtFrameSerializer.cc | 27 ++-- .../mgmt/Ieee80211MgmtFrameSerializer.h | 13 +- .../mgmt/Ieee80211MgmtTransactionTag.msg | 19 +++ .../linklayer/ieee80211/mib/Ieee80211Mib.cc | 71 +++++++-- .../linklayer/ieee80211/mib/Ieee80211Mib.h | 5 + .../ieee80211/mode/Ieee80211ModeSet.cc | 4 +- tests/unit/Ieee80211HtMgmtElements_1.test | 15 ++ tests/unit/Ieee80211HtModeSet_1.test | 21 +++ tests/unit/Ieee80211MgmtApTransaction_1.test | 72 +++++++++ tests/unit/Ieee80211MgmtTransactionTag_1.test | 59 ++++++++ tests/unit/Ieee80211MibAssociationId_1.test | 44 ++++++ 14 files changed, 417 insertions(+), 99 deletions(-) create mode 100644 src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag.msg create mode 100644 tests/unit/Ieee80211HtModeSet_1.test create mode 100644 tests/unit/Ieee80211MgmtApTransaction_1.test create mode 100644 tests/unit/Ieee80211MgmtTransactionTag_1.test create mode 100644 tests/unit/Ieee80211MibAssociationId_1.test diff --git a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc index 183f87d8b66..4a27316c6b3 100644 --- a/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc +++ b/src/inet/linklayer/ieee80211/mac/fragmentation/Fragmentation.cc @@ -27,7 +27,8 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve auto fragment = new Packet(name.c_str()); B length = B(fragmentSizes.at(i)); fragment->insertAtBack(frame->peekDataAt(offset, length)); - fragment->getRegionTags().copyTags(frame->getRegionTags(), offset, frame->getFrontOffset(), frame->getDataLength()); + fragment->copyTags(*frame); + fragment->getRegionTags().copyTags(frame->getRegionTags(), frame->getFrontOffset() + offset, fragment->getFrontOffset(), length); offset += length; const auto& fragmentHeader = staticPtrCast(frameHeader->dupShared()); fragmentHeader->setSequenceNumber(frameHeader->getSequenceNumber()); @@ -45,4 +46,3 @@ std::vector *Fragmentation::fragmentFrame(Packet *frame, const std::ve } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index 09c703d8bc2..dfd8d30d94f 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -14,10 +14,12 @@ #include "inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h" #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mac/Ieee80211SubtypeTag_m.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211HtMgmtElements.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" #include "inet/networklayer/common/NetworkInterface.h" #include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" @@ -36,6 +38,31 @@ static std::ostream& operator<<(std::ostream& os, const Ieee80211MgmtAp::StaInfo return os; } +const Packet *Ieee80211MgmtAp::getAssociationResponseFrame(ITransmitStep *transmitStep) +{ + if (transmitStep == nullptr) + return nullptr; + if (auto rtsTransmitStep = dynamic_cast(transmitStep)) + return rtsTransmitStep->getProtectedFrame(); + return transmitStep->getFrameToTransmit(); +} + +Ieee80211MgmtAp::AssociationResponseDisposition Ieee80211MgmtAp::getAssociationResponseDisposition(const Packet *responseFrame, + uint64_t pendingTransactionId, bool exchangeSucceeded, bool retryPending) +{ + if (responseFrame == nullptr || pendingTransactionId == 0) + return AssociationResponseDisposition::IGNORE; + auto responseHeader = dynamicPtrCast(responseFrame->peekAtFront()); + if (responseHeader == nullptr || (responseHeader->getType() != ST_ASSOCIATIONRESPONSE && responseHeader->getType() != ST_REASSOCIATIONRESPONSE)) + return AssociationResponseDisposition::IGNORE; + const auto& transactionTag = responseFrame->findTag(); + if (transactionTag == nullptr || transactionTag->getTransactionId() != pendingTransactionId) + return AssociationResponseDisposition::IGNORE; + if ((exchangeSucceeded && responseHeader->getMoreFragments()) || (!exchangeSucceeded && retryPending)) + return AssociationResponseDisposition::RETAIN; + return AssociationResponseDisposition::COMPLETE; +} + Ieee80211MgmtAp::~Ieee80211MgmtAp() { cancelAndDelete(beaconTimer); @@ -108,49 +135,48 @@ void Ieee80211MgmtAp::receiveSignal(cComponent *source, simsignal_t signalID, cO auto transmitStep = dynamic_cast(context->getStepBeforeLast()); auto receiveStep = dynamic_cast(context->getLastStep()); if (transmitStep && receiveStep) { - auto responseHeader = dynamicPtrCast(transmitStep->getFrameToTransmit()->peekAtFront()); + const Packet *responseFrame = getAssociationResponseFrame(transmitStep); + auto responseHeader = dynamicPtrCast(responseFrame->peekAtFront()); if (responseHeader != nullptr && (responseHeader->getType() == ST_ASSOCIATIONRESPONSE || responseHeader->getType() == ST_REASSOCIATIONRESPONSE)) { const auto& address = responseHeader->getReceiverAddress(); auto sta = staList.find(address); bool exchangeSucceeded = transmitStep->getCompletion() == IFrameSequenceStep::Completion::ACCEPTED && receiveStep->getCompletion() == IFrameSequenceStep::Completion::ACCEPTED && receiveStep->getReceivedFrame()->peekAtFront()->getType() == ST_ACK; - bool isPendingResponse = sta != staList.end() && sta->second.pendingAssociationResponse == transmitStep->getFrameToTransmit(); - if (isPendingResponse && sta->second.pendingAssociationSuccessful && exchangeSucceeded) { - bool wasAssociated = mib->bssAccessPointData.stations[address] == Ieee80211Mib::ASSOCIATED; - mib->bssAccessPointData.associationIds[address] = sta->second.pendingAssociationId; - mib->bssAccessPointData.stations[address] = Ieee80211Mib::ASSOCIATED; - if (sta->second.pendingHtStateAvailable) { - // IEEE Std 802.11-2024 association state becomes effective only after the successful response exchange. - if (sta->second.pendingHtCapabilitiesValid) - mib->setPeerHtCapabilities(address, sta->second.pendingHtCapabilities, mib->htOperation); - else - mib->removePeerHtCapabilities(address); - } - // Signal delivery is synchronous; observers must see committed station and peer state. - if (responseHeader->getType() == ST_ASSOCIATIONRESPONSE && !wasAssociated) - sendAssocNotification(address); - } - else if (isPendingResponse && !sta->second.pendingAssociationSuccessful && exchangeSucceeded && - mib->bssAccessPointData.stations[address] == Ieee80211Mib::ASSOCIATED) - { - // This model does not implement negotiated management-frame protection. - // IEEE Std 802.11-2024, 11.3.5.3(p): a refused (re)association - // therefore moves the STA from State 4 back to State 3. - mib->releaseAssociationId(address); - mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; - // Signal delivery is synchronous; observers must see the downgraded state. - sendDisAssocNotification(address); + bool retryPending = false; + if (!exchangeSucceeded) { + auto inProgressFrames = context->getInProgressFrames(); + for (int i = 0; i < inProgressFrames->getLength(); i++) + retryPending |= inProgressFrames->getFrames(i) == responseFrame; } - if (isPendingResponse) { - bool retryPending = false; - if (!exchangeSucceeded) { - auto inProgressFrames = context->getInProgressFrames(); - for (int i = 0; i < inProgressFrames->getLength(); i++) - retryPending |= inProgressFrames->getFrames(i) == transmitStep->getFrameToTransmit(); + uint64_t pendingTransactionId = sta == staList.end() ? 0 : sta->second.pendingAssociationTransactionId; + auto disposition = getAssociationResponseDisposition(responseFrame, pendingTransactionId, exchangeSucceeded, retryPending); + if (disposition == AssociationResponseDisposition::COMPLETE) { + if (exchangeSucceeded && sta->second.pendingAssociationSuccessful) { + bool wasAssociated = mib->bssAccessPointData.stations[address] == Ieee80211Mib::ASSOCIATED; + mib->commitAssociationId(address); + 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) + mib->setPeerHtCapabilities(address, sta->second.pendingHtCapabilities, mib->htOperation); + else + mib->removePeerHtCapabilities(address); + } + // Signal delivery is synchronous; observers must see committed station and peer state. + if (responseHeader->getType() == ST_ASSOCIATIONRESPONSE && !wasAssociated) + sendAssocNotification(address); } - if (exchangeSucceeded || !retryPending) - clearPendingAssociation(&sta->second); + else if (exchangeSucceeded && mib->bssAccessPointData.stations[address] == Ieee80211Mib::ASSOCIATED) { + // This model does not implement negotiated management-frame protection. + // IEEE Std 802.11-2024, 11.3.5.3(p): a refused (re)association + // therefore moves the STA from State 4 back to State 3. + mib->releaseAssociationId(address); + mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; + // Signal delivery is synchronous; observers must see the downgraded state. + sendDisAssocNotification(address); + } + clearPendingAssociation(&sta->second); } } } @@ -166,40 +192,29 @@ Ieee80211MgmtAp::StaInfo *Ieee80211MgmtAp::lookupSenderSTA(const Ptrsecond); } -Packet *Ieee80211MgmtAp::sendManagementFrame(const char *name, const Ptr& body, int subtype, const MacAddress& destAddr) +void Ieee80211MgmtAp::sendManagementFrame(const char *name, const Ptr& body, int subtype, const MacAddress& destAddr, uint64_t transactionId) { auto packet = new Packet(name); packet->addTag()->setDestAddress(destAddr); packet->addTag()->setSubtype(subtype); packet->insertAtBack(body); + if (transactionId != 0) + packet->addTag()->setTransactionId(transactionId); sendDown(packet); - return packet; } -short Ieee80211MgmtAp::reserveAssociationId(StaInfo *sta) const +uint64_t Ieee80211MgmtAp::createAssociationTransactionId() { - auto existing = mib->bssAccessPointData.associationIds.find(sta->address); - if (existing != mib->bssAccessPointData.associationIds.end()) - return existing->second; - if (sta->pendingAssociationId != 0) - return sta->pendingAssociationId; - for (short aid = 1; aid <= 2007; aid++) { - bool used = false; - for (const auto& entry : mib->bssAccessPointData.associationIds) - used |= entry.second == aid; - for (const auto& entry : staList) - used |= entry.second.pendingAssociationId == aid; - if (!used) - return aid; - } - throw cRuntimeError("No IEEE 802.11 association ID is available"); + if (++nextAssociationTransactionId == 0) + ++nextAssociationTransactionId; + return nextAssociationTransactionId; } void Ieee80211MgmtAp::clearPendingAssociation(StaInfo *sta) { + mib->cancelAssociationIdReservation(sta->address); sta->pendingAssociationSuccessful = false; - sta->pendingAssociationId = 0; - sta->pendingAssociationResponse = nullptr; + sta->pendingAssociationTransactionId = 0; sta->pendingHtStateAvailable = false; sta->pendingHtCapabilitiesValid = false; } @@ -331,6 +346,7 @@ void Ieee80211MgmtAp::handleAssociationRequestFrame(Packet *packet, const PtrpeekData(); + clearPendingAssociation(sta); sta->pendingAssociationSuccessful = false; sta->pendingHtStateAvailable = true; sta->pendingHtCapabilitiesValid = mib->isHtOperationSupported() && requestBody->getHtCapabilitiesPresent(); @@ -343,14 +359,15 @@ void Ieee80211MgmtAp::handleAssociationRequestFrame(Packet *packet, const Ptr(); body->setStatusCode(basicHtMcsSupported ? SC_SUCCESSFUL : SC_DATARATE_UNSUP); - sta->pendingAssociationId = basicHtMcsSupported ? reserveAssociationId(sta) : 0; - body->setAid(sta->pendingAssociationId); + short associationId = basicHtMcsSupported ? mib->reserveAssociationId(sta->address) : 0; + body->setAid(associationId); sta->pendingAssociationSuccessful = basicHtMcsSupported; + sta->pendingAssociationTransactionId = createAssociationTransactionId(); body->setSupportedRates(supportedRates); addHtCapabilities(body); addHtOperation(body); body->setChunkLength(B(2 + 2 + 2 + body->getSupportedRates().numRates + 2) + getHtMgmtElementsLength(body)); - sta->pendingAssociationResponse = sendManagementFrame(basicHtMcsSupported ? "AssocResp-OK" : "AssocResp-UnsupportedHtMcs", body, ST_ASSOCIATIONRESPONSE, sta->address); + sendManagementFrame(basicHtMcsSupported ? "AssocResp-OK" : "AssocResp-UnsupportedHtMcs", body, ST_ASSOCIATIONRESPONSE, sta->address, sta->pendingAssociationTransactionId); } void Ieee80211MgmtAp::handleAssociationResponseFrame(Packet *packet, const Ptr& header) @@ -374,6 +391,7 @@ void Ieee80211MgmtAp::handleReassociationRequestFrame(Packet *packet, const Ptr< } const auto& requestBody = packet->peekData(); + clearPendingAssociation(sta); sta->pendingAssociationSuccessful = false; sta->pendingHtStateAvailable = true; sta->pendingHtCapabilitiesValid = mib->isHtOperationSupported() && requestBody->getHtCapabilitiesPresent(); @@ -386,14 +404,15 @@ void Ieee80211MgmtAp::handleReassociationRequestFrame(Packet *packet, const Ptr< // send OK response const auto& body = makeShared(); body->setStatusCode(basicHtMcsSupported ? SC_SUCCESSFUL : SC_DATARATE_UNSUP); - sta->pendingAssociationId = basicHtMcsSupported ? reserveAssociationId(sta) : 0; - body->setAid(sta->pendingAssociationId); + short associationId = basicHtMcsSupported ? mib->reserveAssociationId(sta->address) : 0; + body->setAid(associationId); sta->pendingAssociationSuccessful = basicHtMcsSupported; + sta->pendingAssociationTransactionId = createAssociationTransactionId(); body->setSupportedRates(supportedRates); addHtCapabilities(body); addHtOperation(body); body->setChunkLength(B(2 + 2 + 2 + (2 + supportedRates.numRates)) + getHtMgmtElementsLength(body)); - sta->pendingAssociationResponse = sendManagementFrame(basicHtMcsSupported ? "ReassocResp-OK" : "ReassocResp-UnsupportedHtMcs", body, ST_REASSOCIATIONRESPONSE, sta->address); + sendManagementFrame(basicHtMcsSupported ? "ReassocResp-OK" : "ReassocResp-UnsupportedHtMcs", body, ST_REASSOCIATIONRESPONSE, sta->address, sta->pendingAssociationTransactionId); } void Ieee80211MgmtAp::handleReassociationResponseFrame(Packet *packet, const Ptr& header) @@ -479,7 +498,7 @@ void Ieee80211MgmtAp::stop() { cancelEvent(beaconTimer); staList.clear(); - mib->bssAccessPointData.associationIds.clear(); + mib->clearAssociationIds(); Ieee80211MgmtApBase::stop(); } diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h index 96e3f39e99f..b9202a244f3 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h @@ -16,20 +16,28 @@ namespace inet { namespace ieee80211 { +class ITransmitStep; + /** * Used in 802.11 infrastructure mode: handles management frames for * an access point (AP). See corresponding NED file for a detailed description. */ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase { + protected: + enum class AssociationResponseDisposition { + IGNORE, + RETAIN, + COMPLETE, + }; + public: /** Describes a STA */ struct StaInfo { MacAddress address; int authSeqExpected; // when NOT_AUTHENTICATED: transaction sequence number of next expected auth frame bool pendingAssociationSuccessful = false; - short pendingAssociationId = 0; - const Packet *pendingAssociationResponse = nullptr; + uint64_t pendingAssociationTransactionId = 0; bool pendingHtStateAvailable = false; bool pendingHtCapabilitiesValid = false; Ieee80211HtCapabilities pendingHtCapabilities; @@ -64,6 +72,7 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase // state StaList staList; ///< list of STAs cMessage *beaconTimer = nullptr; + uint64_t nextAssociationTransactionId = 0; public: Ieee80211MgmtAp() {} @@ -87,9 +96,11 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase virtual StaInfo *lookupSenderSTA(const Ptr& header); /** Utility function: set fields in the given frame and send it out to the address */ - virtual Packet *sendManagementFrame(const char *name, const Ptr& body, int subtype, const MacAddress& destAddr); + virtual void sendManagementFrame(const char *name, const Ptr& body, int subtype, const MacAddress& destAddr, uint64_t transactionId = 0); - virtual short reserveAssociationId(StaInfo *sta) const; + static const Packet *getAssociationResponseFrame(ITransmitStep *transmitStep); + static AssociationResponseDisposition getAssociationResponseDisposition(const Packet *responseFrame, uint64_t pendingTransactionId, bool exchangeSucceeded, bool retryPending); + virtual uint64_t createAssociationTransactionId(); virtual void clearPendingAssociation(StaInfo *sta); /** Utility function: creates and sends a beacon frame */ diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc index 07b1b05fa5e..80930fd830b 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.cc @@ -17,16 +17,16 @@ namespace inet { namespace ieee80211 { -Register_Serializer(Ieee80211AssociationRequestFrame, Ieee80211MgmtFrameSerializer); -Register_Serializer(Ieee80211AssociationResponseFrame, Ieee80211MgmtFrameSerializer); -Register_Serializer(Ieee80211AuthenticationFrame, Ieee80211MgmtFrameSerializer); -Register_Serializer(Ieee80211BeaconFrame, Ieee80211MgmtFrameSerializer); -Register_Serializer(Ieee80211DeauthenticationFrame, Ieee80211MgmtFrameSerializer); -Register_Serializer(Ieee80211DisassociationFrame, Ieee80211MgmtFrameSerializer); -Register_Serializer(Ieee80211ProbeRequestFrame, Ieee80211MgmtFrameSerializer); -Register_Serializer(Ieee80211ProbeResponseFrame, Ieee80211MgmtFrameSerializer); -Register_Serializer(Ieee80211ReassociationRequestFrame, Ieee80211MgmtFrameSerializer); -Register_Serializer(Ieee80211ReassociationResponseFrame, Ieee80211MgmtFrameSerializer); +Register_Serializer(Ieee80211AssociationRequestFrame, Ieee80211TypedMgmtFrameSerializer); +Register_Serializer(Ieee80211AssociationResponseFrame, Ieee80211TypedMgmtFrameSerializer); +Register_Serializer(Ieee80211AuthenticationFrame, Ieee80211TypedMgmtFrameSerializer); +Register_Serializer(Ieee80211BeaconFrame, Ieee80211TypedMgmtFrameSerializer); +Register_Serializer(Ieee80211DeauthenticationFrame, Ieee80211TypedMgmtFrameSerializer); +Register_Serializer(Ieee80211DisassociationFrame, Ieee80211TypedMgmtFrameSerializer); +Register_Serializer(Ieee80211ProbeRequestFrame, Ieee80211TypedMgmtFrameSerializer); +Register_Serializer(Ieee80211ProbeResponseFrame, Ieee80211TypedMgmtFrameSerializer); +Register_Serializer(Ieee80211ReassociationRequestFrame, Ieee80211TypedMgmtFrameSerializer); +Register_Serializer(Ieee80211ReassociationResponseFrame, Ieee80211TypedMgmtFrameSerializer); static constexpr uint8_t HT_CAPABILITIES_ELEMENT_ID = 45; static constexpr uint8_t HT_OPERATION_ELEMENT_ID = 61; @@ -457,7 +457,7 @@ void Ieee80211MgmtFrameSerializer::serialize(MemoryOutputStream& stream, const P throw cRuntimeError("Cannot serialize frame"); } -const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& stream, const std::type_info& typeInfo) const +const Ptr Ieee80211MgmtFrameSerializer::deserializeFrame(MemoryInputStream& stream, const std::type_info& typeInfo) { int frameType = -1; if (typeInfo == typeid(Ieee80211AuthenticationFrame)) frameType = 0xB0; @@ -662,11 +662,6 @@ const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& st } } -const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& stream) const -{ - throw cRuntimeError("Ieee80211MgmtFrameSerializer requires the target management frame type"); -} - } // namespace ieee80211 } // namespace inet diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h index 2915910062b..2fd2ad19166 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h @@ -24,13 +24,22 @@ class INET_API Ieee80211MgmtFrameSerializer : public FieldsChunkSerializer { protected: virtual void serialize(MemoryOutputStream& stream, const Ptr& chunk) const override; - virtual const Ptr deserialize(MemoryInputStream& stream) const override; - virtual const Ptr deserialize(MemoryInputStream& stream, const std::type_info& typeInfo) const override; + static const Ptr deserializeFrame(MemoryInputStream& stream, const std::type_info& typeInfo); public: Ieee80211MgmtFrameSerializer() : FieldsChunkSerializer() {} }; +template +class Ieee80211TypedMgmtFrameSerializer : public Ieee80211MgmtFrameSerializer +{ + protected: + virtual const Ptr deserialize(MemoryInputStream& stream) const override + { + return deserializeFrame(stream, typeid(Frame)); + } +}; + } // namespace ieee80211 } // namespace inet diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag.msg b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag.msg new file mode 100644 index 00000000000..c4db517aa59 --- /dev/null +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag.msg @@ -0,0 +1,19 @@ +// +// 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 a local management transaction across packet replacement and +// fragmentation. The physical layer removes this sender-local metadata. +// +class Ieee80211MgmtTransactionTag extends TagBase +{ + uint64_t transactionId = 0; +} diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc index 328d2cfb61c..f48febff64a 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -7,6 +7,8 @@ #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include + #include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" namespace inet { @@ -27,6 +29,7 @@ void Ieee80211Mib::initialize(int stage) WATCH(bssStationData.isAssociated); WATCH(bssAccessPointData.stations); WATCH(bssAccessPointData.associationIds); + WATCH(associationIdReservations); WATCH_EXPR("modeStr", getModeStr(mode)); WATCH_EXPR("stationTypeStr", getStationTypeStr(bssStationData.stationType)); WATCH_EXPR("qosStr", qos ? ", QoS" : ", Non-QoS"); @@ -147,32 +150,76 @@ const char *Ieee80211Mib::getStationTypeStr(Ieee80211Mib::BssStationType station } } -short Ieee80211Mib::allocateAssociationId(const MacAddress& address) +short Ieee80211Mib::reserveAssociationId(const MacAddress& address) { - auto existing = bssAccessPointData.associationIds.find(address); - if (existing != bssAccessPointData.associationIds.end()) - return existing->second; + // IEEE Std 802.11-2024, 9.4.1.8: an AP assigns AID values in the range 1 through 2007. + auto committed = bssAccessPointData.associationIds.find(address); + if (committed != bssAccessPointData.associationIds.end()) + return committed->second; + auto reserved = associationIdReservations.find(address); + if (reserved != associationIdReservations.end()) + return reserved->second; + + std::array used = {}; + for (const auto& entry : bssAccessPointData.associationIds) + if (entry.second >= 1 && entry.second <= 2007) + used[entry.second] = true; + for (const auto& entry : associationIdReservations) + if (entry.second >= 1 && entry.second <= 2007) + used[entry.second] = true; for (short aid = 1; aid <= 2007; aid++) { - bool used = false; - for (const auto& entry : bssAccessPointData.associationIds) - if (entry.second == aid) { - used = true; - break; - } - if (!used) { - bssAccessPointData.associationIds[address] = aid; + if (!used[aid]) { + associationIdReservations[address] = aid; return aid; } } throw cRuntimeError("No IEEE 802.11 association ID is available"); } +short Ieee80211Mib::commitAssociationId(const MacAddress& address) +{ + auto committed = bssAccessPointData.associationIds.find(address); + if (committed != bssAccessPointData.associationIds.end()) { + associationIdReservations.erase(address); + return committed->second; + } + auto reserved = associationIdReservations.find(address); + if (reserved == associationIdReservations.end()) + throw cRuntimeError("No IEEE 802.11 association ID is reserved for %s", address.str().c_str()); + short aid = reserved->second; + for (const auto& entry : bssAccessPointData.associationIds) + if (entry.second == aid) + throw cRuntimeError("Reserved IEEE 802.11 association ID %d is already committed", aid); + bssAccessPointData.associationIds[address] = aid; + associationIdReservations.erase(reserved); + return aid; +} + +void Ieee80211Mib::cancelAssociationIdReservation(const MacAddress& address) +{ + associationIdReservations.erase(address); +} + +short Ieee80211Mib::allocateAssociationId(const MacAddress& address) +{ + reserveAssociationId(address); + return commitAssociationId(address); +} + void Ieee80211Mib::releaseAssociationId(const MacAddress& address) { + associationIdReservations.erase(address); bssAccessPointData.associationIds.erase(address); removePeerHtCapabilities(address); } +void Ieee80211Mib::clearAssociationIds() +{ + associationIdReservations.clear(); + bssAccessPointData.associationIds.clear(); + clearPeerHtCapabilities(); +} + } // namespace ieee80211 } // namespace inet diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h index d01e6e59e0c..194a21a87a4 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h @@ -81,6 +81,7 @@ class INET_API Ieee80211Mib : public SimpleModule Ieee80211HtOperation htOperation; private: + std::map associationIdReservations; std::map peerHtStates; protected: @@ -90,8 +91,12 @@ class INET_API Ieee80211Mib : public SimpleModule static const char *getModeStr(Ieee80211Mib::Mode mode); static const char *getStationTypeStr(Ieee80211Mib::BssStationType stationType); std::string getSsidStr() const; + short reserveAssociationId(const MacAddress& address); + short commitAssociationId(const MacAddress& address); + void cancelAssociationIdReservation(const MacAddress& address); short allocateAssociationId(const MacAddress& address); void releaseAssociationId(const MacAddress& address); + void clearAssociationIds(); void updateLocalHtCapabilities(const physicallayer::Ieee80211ModeSet *modeSet); bool isHtOperationSupported() const { return localHtCapabilitiesValid; } const PeerHtState *findPeerHtState(const MacAddress& address) const; diff --git a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index f0be04b94d8..b6da11a110e 100644 --- a/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc +++ b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc @@ -453,7 +453,9 @@ const DelayedInitializer> Ieee80211ModeSet::modeSe { false, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs7BW160MHzNss8, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT) }, { false, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs8BW160MHzNss8, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT) }, { false, Ieee80211VhtCompliantModes::getCompliantMode(&Ieee80211VhtmcsTable::vhtMcs9BW160MHzNss8, Ieee80211VhtMode::BAND_5GHZ, Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT) }, -}, true),}; }); + // Intentional model limitation: unlike IEEE Std 802.11-2024, 11.38.1, + // this VHT-only profile has no selectable HT modes. +}, false),}; }); Ieee80211ModeSet::Ieee80211ModeSet(const char *name, const std::vector entries, bool htOperationSupported) : name(name), diff --git a/tests/unit/Ieee80211HtMgmtElements_1.test b/tests/unit/Ieee80211HtMgmtElements_1.test index 0d5a92cb6cd..fe58051f371 100644 --- a/tests/unit/Ieee80211HtMgmtElements_1.test +++ b/tests/unit/Ieee80211HtMgmtElements_1.test @@ -85,6 +85,7 @@ ASSERT(bytes[54] == 0x10); // Basic MCS 76 Packet encoded("encoded", makeShared(bytes)); auto decoded = encoded.popAtFront(B(bytes.size())); +ASSERT(decoded->getChunkLength() == B(bytes.size())); ASSERT(decoded->getHtCapabilitiesPresent()); ASSERT(decoded->getHtOperationPresent()); ASSERT(decoded->getHtCapabilities().rxMcsSupported[0]); @@ -96,6 +97,20 @@ ASSERT(!decoded->getHtOperation().basicMcsSupported[1]); ASSERT(decoded->getHtOperation().basicMcsSupported[3]); ASSERT(decoded->getHtOperation().basicMcsSupported[76]); +// The caller supplies the exact management-body region. A legal vendor IE is +// ignored by the field parser but retained by FieldsChunkSerializer's raw-data +// cache, while bytes beyond the requested body remain in the packet. +auto vendorBytes = bytes; +vendorBytes.insert(vendorBytes.end(), {221, 3, 0x01, 0x02, 0x03}); +auto framedBytes = vendorBytes; +std::vector trailerBytes = {0xde, 0xad, 0xbe, 0xef}; +framedBytes.insert(framedBytes.end(), trailerBytes.begin(), trailerBytes.end()); +Packet framed("framed", makeShared(framedBytes)); +auto vendorDecoded = framed.popAtFront(B(vendorBytes.size())); +ASSERT(vendorDecoded->getChunkLength() == B(vendorBytes.size())); +ASSERT(serializeResponse(vendorDecoded) == vendorBytes); +ASSERT(framed.peekDataAsBytes()->getBytes() == trailerBytes); + auto unequalResponse = makeHtResponse(); auto unequalCapabilities = unequalResponse->getHtCapabilities(); unequalCapabilities.txRxMcsSetNotEqual = true; diff --git a/tests/unit/Ieee80211HtModeSet_1.test b/tests/unit/Ieee80211HtModeSet_1.test new file mode 100644 index 00000000000..096bdf611ae --- /dev/null +++ b/tests/unit/Ieee80211HtModeSet_1.test @@ -0,0 +1,21 @@ +%description: +Verify that HT operation is enabled only for a mode set with selectable HT +modes. The current VHT-only ac profile intentionally does not advertise HT. + +%includes: +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" + +using namespace inet::physicallayer; + +%activity: + +const auto *htModeSet = Ieee80211ModeSet::getModeSet("n(mixed-2.4Ghz)"); +const auto *vhtOnlyModeSet = Ieee80211ModeSet::getModeSet("ac"); + +ASSERT(htModeSet->isHtOperationSupported()); +ASSERT(!vhtOnlyModeSet->isHtOperationSupported()); + +EV << "HT mode-set capability gates verified.\n"; + +%contains: stdout +HT mode-set capability gates verified. diff --git a/tests/unit/Ieee80211MgmtApTransaction_1.test b/tests/unit/Ieee80211MgmtApTransaction_1.test new file mode 100644 index 00000000000..ad87f2e1d47 --- /dev/null +++ b/tests/unit/Ieee80211MgmtApTransaction_1.test @@ -0,0 +1,72 @@ +%description: +Verify deterministic AP association-response transaction matching and +completion decisions, including RTS-protected responses. + +%includes: +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" + +using namespace inet; +using namespace inet::ieee80211; + +%global: + +class TestIeee80211MgmtAp : public Ieee80211MgmtAp +{ + public: + using Ieee80211MgmtAp::AssociationResponseDisposition; + using Ieee80211MgmtAp::getAssociationResponseDisposition; + using Ieee80211MgmtAp::getAssociationResponseFrame; +}; + +static Packet *makeAssociationResponse(uint64_t transactionId, bool moreFragments, bool includeTag = true) +{ + auto packet = new Packet("AssocResp-OK"); + auto header = makeShared(); + header->setType(ST_ASSOCIATIONRESPONSE); + header->setMoreFragments(moreFragments); + packet->insertAtBack(header); + if (includeTag) + packet->addTag()->setTransactionId(transactionId); + return packet; +} + +%activity: + +using Disposition = TestIeee80211MgmtAp::AssociationResponseDisposition; + +auto finalResponse = makeAssociationResponse(42, false); +TransmitStep ordinaryStep(finalResponse, SIMTIME_ZERO); +ASSERT(TestIeee80211MgmtAp::getAssociationResponseFrame(&ordinaryStep) == finalResponse); + +{ + auto rtsPacket = new Packet("RTS", makeShared()); + RtsTransmitStep rtsStep(finalResponse, rtsPacket, SIMTIME_ZERO); + ASSERT(TestIeee80211MgmtAp::getAssociationResponseFrame(&rtsStep) == finalResponse); +} + +ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 42, true, false) == Disposition::COMPLETE); +ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 41, true, false) == Disposition::IGNORE); +ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 0, true, false) == Disposition::IGNORE); + +auto zeroTokenResponse = makeAssociationResponse(0, false); +ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(zeroTokenResponse, 42, true, false) == Disposition::IGNORE); +auto untaggedResponse = makeAssociationResponse(42, false, false); +ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(untaggedResponse, 42, true, false) == Disposition::IGNORE); + +auto nonFinalResponse = makeAssociationResponse(42, true); +ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(nonFinalResponse, 42, true, false) == Disposition::RETAIN); +ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 42, false, true) == Disposition::RETAIN); +ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 42, false, false) == Disposition::COMPLETE); + +delete nonFinalResponse; +delete untaggedResponse; +delete zeroTokenResponse; +delete finalResponse; + +EV << "AP association transaction decisions verified.\n"; + +%contains: stdout +AP association transaction decisions verified. diff --git a/tests/unit/Ieee80211MgmtTransactionTag_1.test b/tests/unit/Ieee80211MgmtTransactionTag_1.test new file mode 100644 index 00000000000..568716711fc --- /dev/null +++ b/tests/unit/Ieee80211MgmtTransactionTag_1.test @@ -0,0 +1,59 @@ +%description: +Verify that the local IEEE 802.11 management transaction identity is retained +when a management frame is replaced by fragmentation packets, and that a +generic body region tag is clipped and rebased for each fragment. + +%includes: +#include + +#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/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" + +using namespace inet; +using namespace inet::ieee80211; + +%activity: + +auto frame = new Packet("AssocResp-OK"); +frame->insertAtBack(makeShared(std::vector(9, 0))); +frame->addTag()->setTransactionId(42); +frame->addRegionTag(B(2), B(5)); + +auto header = makeShared(); +header->setType(ST_ASSOCIATIONRESPONSE); +header->setReceiverAddress(MacAddress("02:00:00:00:00:01")); +frame->insertAtFront(header); +frame->insertAtBack(makeShared()); +auto headerLength = header->getChunkLength(); + +Fragmentation fragmentation; +auto fragments = fragmentation.fragmentFrame(frame, {4, 5}); +ASSERT(fragments->size() == 2); +for (auto fragment : *fragments) { + auto tag = fragment->findTag(); + ASSERT(tag != nullptr); + ASSERT(tag->getTransactionId() == 42); +} +ASSERT(fragments->front()->peekAtFront()->getMoreFragments()); +ASSERT(!fragments->back()->peekAtFront()->getMoreFragments()); + +auto firstRegionTags = fragments->front()->getAllRegionTags(); +ASSERT(firstRegionTags.size() == 1); +ASSERT(firstRegionTags.front().getStartOffset() == headerLength + B(2)); +ASSERT(firstRegionTags.front().getLength() == B(2)); +auto secondRegionTags = fragments->back()->getAllRegionTags(); +ASSERT(secondRegionTags.size() == 1); +ASSERT(secondRegionTags.front().getStartOffset() == headerLength); +ASSERT(secondRegionTags.front().getLength() == B(3)); + +for (auto fragment : *fragments) + delete fragment; +delete fragments; + +EV << "Management transaction packet tag survived frame replacement.\n"; + +%contains: stdout +Management transaction packet tag survived frame replacement. diff --git a/tests/unit/Ieee80211MibAssociationId_1.test b/tests/unit/Ieee80211MibAssociationId_1.test new file mode 100644 index 00000000000..c4135458c84 --- /dev/null +++ b/tests/unit/Ieee80211MibAssociationId_1.test @@ -0,0 +1,44 @@ +%description: +Verify deterministic IEEE 802.11 association ID reservation, commit, +cancellation, reuse, and lifecycle clearing. + +%includes: +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" + +using namespace inet; +using namespace inet::ieee80211; + +%activity: + +Ieee80211Mib mib; +MacAddress first("02:00:00:00:00:01"); +MacAddress second("02:00:00:00:00:02"); +MacAddress third("02:00:00:00:00:03"); +MacAddress fourth("02:00:00:00:00:04"); + +ASSERT(mib.reserveAssociationId(first) == 1); +ASSERT(mib.reserveAssociationId(second) == 2); +ASSERT(mib.reserveAssociationId(first) == 1); + +mib.cancelAssociationIdReservation(first); +ASSERT(mib.reserveAssociationId(third) == 1); + +ASSERT(mib.commitAssociationId(second) == 2); +ASSERT(mib.bssAccessPointData.associationIds.at(second) == 2); +mib.cancelAssociationIdReservation(second); +ASSERT(mib.bssAccessPointData.associationIds.at(second) == 2); + +ASSERT(mib.commitAssociationId(third) == 1); +mib.releaseAssociationId(second); +ASSERT(mib.bssAccessPointData.associationIds.count(second) == 0); +ASSERT(mib.allocateAssociationId(fourth) == 2); + +mib.reserveAssociationId(first); +mib.clearAssociationIds(); +ASSERT(mib.bssAccessPointData.associationIds.empty()); +ASSERT(mib.reserveAssociationId(second) == 1); + +EV << "Association ID ownership checks passed.\n"; + +%contains: stdout +Association ID ownership checks passed. From a8b2362056b3cf140df254e7063adfe1acdcfaff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 17 Aug 2026 22:48:29 +0200 Subject: [PATCH 03/10] Fix IEEE 802.11 management initialization and AID cleanup Restore simplified station BSS identity setup to the link-layer initialization stage so automatic network configuration sees the AP SSID, while keeping HT peer capability installation at the final stage after mode-set initialization. Add an AP-owned association-response timeout with per-station deadlines and transaction matching. Expired pending responses now release only uncommitted AID reservations and discard pending HT state without disturbing replacement transactions or committed reassociation IDs. Document the management serializer's exact-body stream contract and extend the HT element regression to cover vendor elements followed by trailing FCS-like bytes. Add deterministic unit and module coverage for timeout scheduling, AID reuse, stale transaction protection, simplified-station wireless identity during network configuration, and final-stage HT peer state. Tests: release and debug builds; 3 focused unit tests; 2 focused module tests. --- .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 59 ++++++- .../ieee80211/mgmt/Ieee80211MgmtAp.h | 9 + .../ieee80211/mgmt/Ieee80211MgmtAp.ned | 1 + .../mgmt/Ieee80211MgmtFrameSerializer.h | 2 + .../mgmt/Ieee80211MgmtStaSimplified.cc | 25 ++- tests/module/Ieee80211MgmtApTimeout_1.test | 154 ++++++++++++++++++ ...0211MgmtStaSimplifiedInitialization_1.test | 129 +++++++++++++++ tests/unit/Ieee80211HtMgmtElements_1.test | 9 +- tests/unit/Ieee80211MgmtApTransaction_1.test | 18 +- 9 files changed, 388 insertions(+), 18 deletions(-) create mode 100644 tests/module/Ieee80211MgmtApTimeout_1.test create mode 100644 tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index dfd8d30d94f..f10f4ec00d7 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -66,6 +66,7 @@ Ieee80211MgmtAp::AssociationResponseDisposition Ieee80211MgmtAp::getAssociationR Ieee80211MgmtAp::~Ieee80211MgmtAp() { cancelAndDelete(beaconTimer); + cancelAndDelete(associationResponseTimeoutTimer); } void Ieee80211MgmtAp::initialize(int stage) @@ -76,6 +77,9 @@ void Ieee80211MgmtAp::initialize(int stage) // read params and init vars ssid = par("ssid").stdstringValue(); beaconInterval = par("beaconInterval"); + associationResponseTimeout = par("associationResponseTimeout"); + if (associationResponseTimeout < SIMTIME_ZERO) + throw cRuntimeError("parameter 'associationResponseTimeout' must not be negative"); numAuthSteps = par("numAuthSteps"); if (numAuthSteps != 2 && numAuthSteps != 4) throw cRuntimeError("parameter 'numAuthSteps' (number of frames exchanged during authentication) must be 2 or 4, not %d", numAuthSteps); @@ -83,6 +87,7 @@ void Ieee80211MgmtAp::initialize(int stage) WATCH(ssid); WATCH(channelNumber); WATCH(beaconInterval); + WATCH(associationResponseTimeout); WATCH(numAuthSteps); WATCH(staList); @@ -95,6 +100,7 @@ void Ieee80211MgmtAp::initialize(int stage) // start beacon timer (randomize startup time) beaconTimer = new cMessage("beaconTimer"); + associationResponseTimeoutTimer = new cMessage("associationResponseTimeoutTimer"); } } @@ -104,6 +110,14 @@ void Ieee80211MgmtAp::handleTimer(cMessage *msg) sendBeacon(); scheduleAfter(beaconInterval, beaconTimer); } + else if (msg == associationResponseTimeoutTimer) { + auto sta = staList.find(scheduledAssociationResponseTimeoutAddress); + if (sta != staList.end() && isAssociationResponseTimeoutDue(sta->second, + scheduledAssociationResponseTimeoutTransactionId, scheduledAssociationResponseTimeoutDeadline, simTime())) + clearPendingAssociation(&sta->second); + else + scheduleAssociationResponseTimeout(); + } else { throw cRuntimeError("internal error: unrecognized timer '%s'", msg->getName()); } @@ -210,13 +224,46 @@ uint64_t Ieee80211MgmtAp::createAssociationTransactionId() return nextAssociationTransactionId; } +bool Ieee80211MgmtAp::isAssociationResponseTimeoutDue(const StaInfo& sta, uint64_t transactionId, simtime_t deadline, simtime_t currentTime) +{ + return transactionId != 0 && sta.pendingAssociationTransactionId == transactionId && + sta.pendingAssociationDeadline == deadline && deadline <= currentTime; +} + void Ieee80211MgmtAp::clearPendingAssociation(StaInfo *sta) { mib->cancelAssociationIdReservation(sta->address); sta->pendingAssociationSuccessful = false; sta->pendingAssociationTransactionId = 0; + sta->pendingAssociationDeadline = SIMTIME_MAX; sta->pendingHtStateAvailable = false; sta->pendingHtCapabilitiesValid = false; + sta->pendingHtCapabilities = Ieee80211HtCapabilities(); + scheduleAssociationResponseTimeout(); +} + +void Ieee80211MgmtAp::scheduleAssociationResponseTimeout() +{ + cancelEvent(associationResponseTimeoutTimer); + scheduledAssociationResponseTimeoutTransactionId = 0; + scheduledAssociationResponseTimeoutDeadline = SIMTIME_MAX; + for (const auto& entry : staList) { + const auto& sta = entry.second; + if (sta.pendingAssociationTransactionId != 0 && sta.pendingAssociationDeadline < scheduledAssociationResponseTimeoutDeadline) { + scheduledAssociationResponseTimeoutAddress = entry.first; + scheduledAssociationResponseTimeoutTransactionId = sta.pendingAssociationTransactionId; + scheduledAssociationResponseTimeoutDeadline = sta.pendingAssociationDeadline; + } + } + if (scheduledAssociationResponseTimeoutTransactionId != 0) + scheduleAt(scheduledAssociationResponseTimeoutDeadline, associationResponseTimeoutTimer); +} + +void Ieee80211MgmtAp::startAssociationResponseTimeout(StaInfo *sta) +{ + ASSERT(sta->pendingAssociationTransactionId != 0); + sta->pendingAssociationDeadline = simTime() + associationResponseTimeout; + scheduleAssociationResponseTimeout(); } void Ieee80211MgmtAp::sendBeacon() @@ -247,8 +294,8 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const Ptraddress = staAddress; mib->bssAccessPointData.stations[staAddress] = Ieee80211Mib::NOT_AUTHENTICATED; sta->authSeqExpected = 1; - clearPendingAssociation(sta); } + clearPendingAssociation(sta); // reset authentication status, when starting a new auth sequence // The statements below are added because the L2 handover time was greater than before when @@ -263,7 +310,6 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrreleaseAssociationId(sta->address); } mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::NOT_AUTHENTICATED; - clearPendingAssociation(sta); mib->removePeerHtCapabilities(sta->address); sta->authSeqExpected = 1; } @@ -336,6 +382,8 @@ void Ieee80211MgmtAp::handleAssociationRequestFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::NOT_AUTHENTICATED) { // STA not authenticated: send error and return const auto& body = makeShared(); @@ -346,7 +394,6 @@ void Ieee80211MgmtAp::handleAssociationRequestFrame(Packet *packet, const PtrpeekData(); - clearPendingAssociation(sta); sta->pendingAssociationSuccessful = false; sta->pendingHtStateAvailable = true; sta->pendingHtCapabilitiesValid = mib->isHtOperationSupported() && requestBody->getHtCapabilitiesPresent(); @@ -367,6 +414,7 @@ void Ieee80211MgmtAp::handleAssociationRequestFrame(Packet *packet, const PtrsetChunkLength(B(2 + 2 + 2 + body->getSupportedRates().numRates + 2) + getHtMgmtElementsLength(body)); + startAssociationResponseTimeout(sta); sendManagementFrame(basicHtMcsSupported ? "AssocResp-OK" : "AssocResp-UnsupportedHtMcs", body, ST_ASSOCIATIONRESPONSE, sta->address, sta->pendingAssociationTransactionId); } @@ -381,6 +429,8 @@ void Ieee80211MgmtAp::handleReassociationRequestFrame(Packet *packet, const Ptr< // "11.3.4 AP reassociation procedures" -- almost the same as AssociationRequest processing StaInfo *sta = lookupSenderSTA(header); + if (sta != nullptr) + clearPendingAssociation(sta); if (!sta || mib->bssAccessPointData.stations[sta->address] == Ieee80211Mib::NOT_AUTHENTICATED) { // STA not authenticated: send error and return const auto& body = makeShared(); @@ -391,7 +441,6 @@ void Ieee80211MgmtAp::handleReassociationRequestFrame(Packet *packet, const Ptr< } const auto& requestBody = packet->peekData(); - clearPendingAssociation(sta); sta->pendingAssociationSuccessful = false; sta->pendingHtStateAvailable = true; sta->pendingHtCapabilitiesValid = mib->isHtOperationSupported() && requestBody->getHtCapabilitiesPresent(); @@ -412,6 +461,7 @@ void Ieee80211MgmtAp::handleReassociationRequestFrame(Packet *packet, const Ptr< addHtCapabilities(body); addHtOperation(body); body->setChunkLength(B(2 + 2 + 2 + (2 + supportedRates.numRates)) + getHtMgmtElementsLength(body)); + startAssociationResponseTimeout(sta); sendManagementFrame(basicHtMcsSupported ? "ReassocResp-OK" : "ReassocResp-UnsupportedHtMcs", body, ST_REASSOCIATIONRESPONSE, sta->address, sta->pendingAssociationTransactionId); } @@ -497,6 +547,7 @@ void Ieee80211MgmtAp::start() void Ieee80211MgmtAp::stop() { cancelEvent(beaconTimer); + cancelEvent(associationResponseTimeoutTimer); staList.clear(); mib->clearAssociationIds(); Ieee80211MgmtApBase::stop(); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h index b9202a244f3..b7d6a56e622 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h @@ -38,6 +38,7 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase int authSeqExpected; // when NOT_AUTHENTICATED: transaction sequence number of next expected auth frame bool pendingAssociationSuccessful = false; uint64_t pendingAssociationTransactionId = 0; + simtime_t pendingAssociationDeadline = SIMTIME_MAX; bool pendingHtStateAvailable = false; bool pendingHtCapabilitiesValid = false; Ieee80211HtCapabilities pendingHtCapabilities; @@ -67,12 +68,17 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase std::string ssid; int channelNumber = -1; simtime_t beaconInterval; + simtime_t associationResponseTimeout; int numAuthSteps = 0; // state StaList staList; ///< list of STAs cMessage *beaconTimer = nullptr; + cMessage *associationResponseTimeoutTimer = nullptr; uint64_t nextAssociationTransactionId = 0; + MacAddress scheduledAssociationResponseTimeoutAddress; + uint64_t scheduledAssociationResponseTimeoutTransactionId = 0; + simtime_t scheduledAssociationResponseTimeoutDeadline = SIMTIME_MAX; public: Ieee80211MgmtAp() {} @@ -100,8 +106,11 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase static const Packet *getAssociationResponseFrame(ITransmitStep *transmitStep); static AssociationResponseDisposition getAssociationResponseDisposition(const Packet *responseFrame, uint64_t pendingTransactionId, bool exchangeSucceeded, bool retryPending); + static bool isAssociationResponseTimeoutDue(const StaInfo& sta, uint64_t transactionId, simtime_t deadline, simtime_t currentTime); virtual uint64_t createAssociationTransactionId(); virtual void clearPendingAssociation(StaInfo *sta); + virtual void scheduleAssociationResponseTimeout(); + virtual void startAssociationResponseTimeout(StaInfo *sta); /** Utility function: creates and sends a beacon frame */ virtual void sendBeacon(); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.ned b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.ned index f6801661aee..0d9550232f6 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.ned +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.ned @@ -26,6 +26,7 @@ simple Ieee80211MgmtAp extends SimpleModule like IIeee80211Mgmt @class(Ieee80211MgmtAp); string ssid = default("SSID"); double beaconInterval @unit(s) = default(100ms); + double associationResponseTimeout @unit(s) = default(5s); // Maximum time to retain an incomplete (re)association response transaction int numAuthSteps = default(4); // Use 2 for Open System auth, 4 for WEP string interfaceTableModule; string radioModule = default("^.radio"); // The path to the Radio module //FIXME remove default value diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h index 2fd2ad19166..22b05f08cb0 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h @@ -19,6 +19,8 @@ namespace ieee80211 { /** * Converts between Ieee80211MgmtFrame and binary network byte order IEEE 802.11 mgmt frame. + * The input stream passed to deserialize() must be bounded to the exact management-frame body; + * all bytes remaining after the fixed fields are interpreted as management elements. */ class INET_API Ieee80211MgmtFrameSerializer : public FieldsChunkSerializer { diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc index 238e776c92a..4aa8db6523a 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc @@ -15,6 +15,19 @@ namespace ieee80211 { Define_Module(Ieee80211MgmtStaSimplified); +static Ieee80211Mib *findAccessPointMib(const MacAddress& accessPointAddress) +{ + L3AddressResolver addressResolver; + auto host = addressResolver.findHostWithAddress(accessPointAddress); + if (host == nullptr) + throw cRuntimeError("Access point with address %s not found", accessPointAddress.str().c_str()); + auto interfaceTable = addressResolver.findInterfaceTableOf(host); + auto networkInterface = interfaceTable->findInterfaceByAddress(accessPointAddress); + if (networkInterface == nullptr) + throw cRuntimeError("Access point interface with address %s not found", accessPointAddress.str().c_str()); + return check_and_cast(networkInterface->getSubmodule("mib")); +} + void Ieee80211MgmtStaSimplified::initialize(int stage) { Ieee80211MgmtBase::initialize(stage); @@ -23,18 +36,16 @@ void Ieee80211MgmtStaSimplified::initialize(int stage) mib->bssStationData.stationType = Ieee80211Mib::STATION; mib->bssStationData.isAssociated = true; } - else if (stage == INITSTAGE_LAST) { + else if (stage == INITSTAGE_LINK_LAYER) { L3AddressResolver addressResolver; auto accessPointAddress = addressResolver.resolve(par("accessPointAddress"), L3AddressResolver::ADDR_MAC).toMac(); mib->bssData.bssid = accessPointAddress; - auto host = addressResolver.findHostWithAddress(mib->bssData.bssid); - if (host == nullptr) - throw cRuntimeError("Access point with address %s not found", mib->bssData.bssid.str().c_str()); - auto interfaceTable = addressResolver.findInterfaceTableOf(host); - auto networkInterface = interfaceTable->findInterfaceByAddress(mib->bssData.bssid); - auto apMib = dynamic_cast(networkInterface->getSubmodule("mib")); + auto apMib = findAccessPointMib(accessPointAddress); apMib->bssAccessPointData.stations[mib->address] = Ieee80211Mib::ASSOCIATED; mib->bssData.ssid = apMib->bssData.ssid; + } + else if (stage == INITSTAGE_LAST) { + auto apMib = findAccessPointMib(mib->bssData.bssid); // Simplified management is an explicit no-air abstraction: install the state that the // Association Request/Response exchange would have committed in detailed management. if (mib->isHtOperationSupported() && apMib->isHtOperationSupported()) { diff --git a/tests/module/Ieee80211MgmtApTimeout_1.test b/tests/module/Ieee80211MgmtApTimeout_1.test new file mode 100644 index 00000000000..7418a123cb7 --- /dev/null +++ b/tests/module/Ieee80211MgmtApTimeout_1.test @@ -0,0 +1,154 @@ +%description: +Verify the AP's association-response timer releases only expired uncommitted +association IDs, preserves later replacement transactions, and never releases +a committed association ID during a reassociation timeout. + +%file: TestIeee80211MgmtAp.cc + +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" + +namespace inet { +namespace ieee80211 { + +class TestIeee80211MgmtAp : public Ieee80211MgmtAp +{ + public: + short startPendingAssociation(const MacAddress& address) + { + auto& sta = staList[address]; + sta.address = address; + clearPendingAssociation(&sta); + short aid = mib->reserveAssociationId(address); + sta.pendingAssociationSuccessful = true; + sta.pendingAssociationTransactionId = createAssociationTransactionId(); + startAssociationResponseTimeout(&sta); + return aid; + } + + short startPendingReassociation(const MacAddress& address) + { + auto& sta = staList[address]; + sta.address = address; + clearPendingAssociation(&sta); + short aid = mib->allocateAssociationId(address); + ASSERT(mib->reserveAssociationId(address) == aid); + sta.pendingAssociationSuccessful = true; + sta.pendingAssociationTransactionId = createAssociationTransactionId(); + startAssociationResponseTimeout(&sta); + return aid; + } + + bool hasPendingAssociation(const MacAddress& address) const + { + auto it = staList.find(address); + return it != staList.end() && it->second.pendingAssociationTransactionId != 0; + } + + short reserveAssociationId(const MacAddress& address) { return mib->reserveAssociationId(address); } + void cancelAssociationIdReservation(const MacAddress& address) { mib->cancelAssociationIdReservation(address); } + short getCommittedAssociationId(const MacAddress& address) const { return mib->bssAccessPointData.associationIds.at(address); } + simtime_t getScheduledTimeout() const { return associationResponseTimeoutTimer->getArrivalTime(); } +}; + +Define_Module(TestIeee80211MgmtAp); + +class Ieee80211MgmtApTimeoutTest : public cSimpleModule +{ + public: + Ieee80211MgmtApTimeoutTest() : cSimpleModule(65536) {} + + protected: + virtual void activity() override + { + auto mgmt = check_and_cast(getModuleByPath("^.ap.wlan[0].mgmt")); + MacAddress replacement("02:00:00:00:00:01"); + MacAddress expiring("02:00:00:00:00:02"); + MacAddress committed("02:00:00:00:00:03"); + MacAddress reused("02:00:00:00:00:04"); + MacAddress finallyReused("02:00:00:00:00:05"); + + ASSERT(mgmt->startPendingAssociation(replacement) == 1); + ASSERT(mgmt->startPendingAssociation(expiring) == 2); + short committedAid = mgmt->startPendingReassociation(committed); + ASSERT(committedAid == 3); + ASSERT(mgmt->getScheduledTimeout() == SimTime(100, SIMTIME_MS)); + + wait(SimTime(50, SIMTIME_MS)); + ASSERT(mgmt->startPendingAssociation(replacement) == 1); + ASSERT(mgmt->getScheduledTimeout() == SimTime(100, SIMTIME_MS)); + + wait(SimTime(51, SIMTIME_MS)); + ASSERT(mgmt->hasPendingAssociation(replacement)); + ASSERT(!mgmt->hasPendingAssociation(expiring)); + ASSERT(!mgmt->hasPendingAssociation(committed)); + ASSERT(mgmt->reserveAssociationId(replacement) == 1); + ASSERT(mgmt->reserveAssociationId(reused) == 2); + mgmt->cancelAssociationIdReservation(reused); + ASSERT(mgmt->getCommittedAssociationId(committed) == committedAid); + ASSERT(mgmt->getScheduledTimeout() == SimTime(150, SIMTIME_MS)); + + wait(SimTime(50, SIMTIME_MS)); + ASSERT(!mgmt->hasPendingAssociation(replacement)); + ASSERT(mgmt->reserveAssociationId(finallyReused) == 1); + + std::cout << "AP association response timeout lifecycle verified.\n"; + } +}; + +Define_Module(Ieee80211MgmtApTimeoutTest); + +} // 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 TestIeee80211MgmtAp extends Ieee80211MgmtAp +{ + parameters: + @class(::inet::ieee80211::TestIeee80211MgmtAp); +} + +simple Ieee80211MgmtApTimeoutTest extends SimpleModule +{ + parameters: + @class(::inet::ieee80211::Ieee80211MgmtApTimeoutTest); +} + +network Ieee80211MgmtApTimeoutTestNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + ap: AccessPoint { + parameters: + wlan[*].mgmt.typename = "TestIeee80211MgmtAp"; + } + test: Ieee80211MgmtApTimeoutTest; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211MgmtApTimeoutTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 200ms +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +*.ap.wlan[0].mgmt.associationResponseTimeout = 100ms +*.ap.wlan[0].mgmt.beaconInterval = 10s +**.mobility.initFromDisplayString = false +**.mobility.constraintAreaMinX = 0m +**.mobility.constraintAreaMinY = 0m +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxX = 100m +**.mobility.constraintAreaMaxY = 100m +**.mobility.constraintAreaMaxZ = 0m + +%contains: stdout +AP association response timeout lifecycle verified. diff --git a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test new file mode 100644 index 00000000000..3912c2cba72 --- /dev/null +++ b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test @@ -0,0 +1,129 @@ +%description: +Verify simplified station BSS identity is visible to automatic network +configuration while negotiated HT peer state is installed only at the last +initialization stage. + +%file: TestInitializationObserver.cc + +#include "inet/common/InitStages.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/networklayer/common/NetworkInterface.h" +#include "inet/networklayer/configurator/ipv4/Ipv4NetworkConfigurator.h" + +namespace inet { + +class TestIpv4NetworkConfigurator : public Ipv4NetworkConfigurator +{ + public: + std::string getTestWirelessId(NetworkInterface *networkInterface) { return getWirelessId(networkInterface); } +}; + +Define_Module(TestIpv4NetworkConfigurator); + +class TestInitializationObserver : public cSimpleModule +{ + protected: + virtual int numInitStages() const override { return NUM_INIT_STAGES; } + + virtual void initialize(int stage) override + { + if (stage == INITSTAGE_NETWORK_CONFIGURATION || stage == INITSTAGE_LAST) { + auto configurator = check_and_cast(getModuleByPath("^.configurator")); + auto apInterface = check_and_cast(getModuleByPath("^.ap.wlan[0]")); + auto staInterface = check_and_cast(getModuleByPath("^.sta.wlan[0]")); + auto apMib = check_and_cast(apInterface->getSubmodule("mib")); + auto staMib = check_and_cast(staInterface->getSubmodule("mib")); + + ASSERT(staInterface->getSubmodule("agent") == nullptr); + if (stage == INITSTAGE_NETWORK_CONFIGURATION) { + ASSERT(apMib->bssData.ssid == "review-ssid"); + ASSERT(staMib->bssData.ssid == apMib->bssData.ssid); + ASSERT(configurator->getTestWirelessId(apInterface) == configurator->getTestWirelessId(staInterface)); + ASSERT(apMib->findPeerHtState(staMib->address) == nullptr); + ASSERT(staMib->findPeerHtState(apMib->address) == nullptr); + std::cout << "Simplified STA wireless identity available during network configuration.\n"; + } + else { + ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); + ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); + std::cout << "Simplified STA HT peer state installed at last initialization stage.\n"; + } + } + } + + virtual void handleMessage(cMessage *message) override { throw cRuntimeError("Unexpected message"); } +}; + +Define_Module(TestInitializationObserver); + +} // namespace inet + +%file: test.ned + +import inet.common.SimpleModule; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.WirelessHost; +import inet.node.wireless.AccessPoint; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; + +simple TestIpv4NetworkConfigurator extends Ipv4NetworkConfigurator +{ + parameters: + @class(::inet::TestIpv4NetworkConfigurator); +} + +simple TestInitializationObserver extends SimpleModule +{ + parameters: + @class(::inet::TestInitializationObserver); +} + +network Ieee80211MgmtStaSimplifiedInitializationTestNetwork +{ + submodules: + radioMedium: Ieee80211ScalarRadioMedium; + configurator: TestIpv4NetworkConfigurator; + ap: AccessPoint { + parameters: + wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified"; + wlan[*].agent.typename = ""; + } + sta: WirelessHost { + parameters: + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + observer: TestInitializationObserver; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211MgmtStaSimplifiedInitializationTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 1us +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +*.ap.wlan[0].address = "02:00:00:00:00:01" +*.sta.wlan[0].address = "02:00:00:00:00:02" +*.ap.wlan[0].mgmt.ssid = "review-ssid" +*.sta.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:01" +**.wlan[0].opMode = "n(mixed-2.4Ghz)" +**.wlan[0].bitrate = 65Mbps +**.wlan[0].radio.bandName = "2.4 GHz" +**.wlan[0].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 + +%contains: stdout +Simplified STA wireless identity available during network configuration. + +%contains: stdout +Simplified STA HT peer state installed at last initialization stage. diff --git a/tests/unit/Ieee80211HtMgmtElements_1.test b/tests/unit/Ieee80211HtMgmtElements_1.test index fe58051f371..beddc042fb8 100644 --- a/tests/unit/Ieee80211HtMgmtElements_1.test +++ b/tests/unit/Ieee80211HtMgmtElements_1.test @@ -97,11 +97,12 @@ ASSERT(!decoded->getHtOperation().basicMcsSupported[1]); ASSERT(decoded->getHtOperation().basicMcsSupported[3]); ASSERT(decoded->getHtOperation().basicMcsSupported[76]); -// The caller supplies the exact management-body region. A legal vendor IE is -// ignored by the field parser but retained by FieldsChunkSerializer's raw-data -// cache, while bytes beyond the requested body remain in the packet. +// The caller supplies the exact management-body region. A legal vendor IE before +// the HT elements is ignored by the field parser but retained by +// FieldsChunkSerializer's raw-data cache, while bytes beyond the requested body +// remain in the packet. auto vendorBytes = bytes; -vendorBytes.insert(vendorBytes.end(), {221, 3, 0x01, 0x02, 0x03}); +vendorBytes.insert(vendorBytes.begin() + 9, {221, 3, 0x01, 0x02, 0x03}); auto framedBytes = vendorBytes; std::vector trailerBytes = {0xde, 0xad, 0xbe, 0xef}; framedBytes.insert(framedBytes.end(), trailerBytes.begin(), trailerBytes.end()); diff --git a/tests/unit/Ieee80211MgmtApTransaction_1.test b/tests/unit/Ieee80211MgmtApTransaction_1.test index ad87f2e1d47..158b158199b 100644 --- a/tests/unit/Ieee80211MgmtApTransaction_1.test +++ b/tests/unit/Ieee80211MgmtApTransaction_1.test @@ -1,6 +1,6 @@ %description: Verify deterministic AP association-response transaction matching and -completion decisions, including RTS-protected responses. +completion and timeout decisions, including RTS-protected responses. %includes: #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" @@ -19,6 +19,7 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp using Ieee80211MgmtAp::AssociationResponseDisposition; using Ieee80211MgmtAp::getAssociationResponseDisposition; using Ieee80211MgmtAp::getAssociationResponseFrame; + using Ieee80211MgmtAp::isAssociationResponseTimeoutDue; }; static Packet *makeAssociationResponse(uint64_t transactionId, bool moreFragments, bool includeTag = true) @@ -61,12 +62,23 @@ ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(nonFinalResponse, ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 42, false, true) == Disposition::RETAIN); ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 42, false, false) == Disposition::COMPLETE); +Ieee80211MgmtAp::StaInfo sta; +sta.pendingAssociationTransactionId = 42; +sta.pendingAssociationDeadline = SimTime(5); +ASSERT(!TestIeee80211MgmtAp::isAssociationResponseTimeoutDue(sta, 42, SimTime(5), SimTime(4.999))); +ASSERT(TestIeee80211MgmtAp::isAssociationResponseTimeoutDue(sta, 42, SimTime(5), SimTime(5))); +ASSERT(TestIeee80211MgmtAp::isAssociationResponseTimeoutDue(sta, 42, SimTime(5), SimTime(6))); +ASSERT(!TestIeee80211MgmtAp::isAssociationResponseTimeoutDue(sta, 41, SimTime(5), SimTime(5))); +ASSERT(!TestIeee80211MgmtAp::isAssociationResponseTimeoutDue(sta, 42, SimTime(4), SimTime(5))); +sta.pendingAssociationTransactionId = 0; +ASSERT(!TestIeee80211MgmtAp::isAssociationResponseTimeoutDue(sta, 0, SimTime(5), SimTime(5))); + delete nonFinalResponse; delete untaggedResponse; delete zeroTokenResponse; delete finalResponse; -EV << "AP association transaction decisions verified.\n"; +EV << "AP association transaction and timeout decisions verified.\n"; %contains: stdout -AP association transaction decisions verified. +AP association transaction and timeout decisions verified. From 3086d2ed0fd7ff1979e46fb2d12c179a7ccefef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 18 Aug 2026 14:02:52 +0200 Subject: [PATCH 04/10] Fix 802.11 management reassociation lifecycle handling Restore the simplified station association bootstrap whenever its lifecycle starts so AP membership, BSS identity, association state, and bilateral negotiated HT peer state are recreated after shutdown or crash recovery. Report reassociation outcomes through Ieee80211Prim_ReassociateConfirm, populate the AP address on association confirms, return a refused reassociation confirm before initial association, and preserve the reassociation primitive on timeout. Scan complete frame sequences for acknowledged association responses instead of assuming the response and ACK are the final two steps. Also advertise only initialized mandatory supported-rate entries. Extend the simplified-station module test with a deterministic stop/start cycle that verifies restored association and HT peer state. Validated with the release build, focused HT serializer and AP transaction unit tests, and the simplified-station lifecycle module test. --- .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 6 +-- .../ieee80211/mgmt/Ieee80211MgmtBase.cc | 4 +- .../ieee80211/mgmt/Ieee80211MgmtSta.cc | 37 +++++++++++++++--- .../ieee80211/mgmt/Ieee80211MgmtSta.h | 4 +- .../mgmt/Ieee80211MgmtStaSimplified.cc | 39 ++++++++++++------- .../mgmt/Ieee80211MgmtStaSimplified.h | 3 +- ...0211MgmtStaSimplifiedInitialization_1.test | 36 ++++++++++++++++- 7 files changed, 100 insertions(+), 29 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index f10f4ec00d7..c8670f84072 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -145,9 +145,9 @@ void Ieee80211MgmtAp::receiveSignal(cComponent *source, simsignal_t signalID, cO if (signalID == IFrameSequenceHandler::frameSequenceFinishedSignal) { auto context = check_and_cast(obj); - if (context->getNumSteps() >= 2) { - auto transmitStep = dynamic_cast(context->getStepBeforeLast()); - auto receiveStep = dynamic_cast(context->getLastStep()); + for (int stepIndex = 0; stepIndex + 1 < context->getNumSteps(); stepIndex++) { + auto transmitStep = dynamic_cast(context->getStep(stepIndex)); + auto receiveStep = dynamic_cast(context->getStep(stepIndex + 1)); if (transmitStep && receiveStep) { const Packet *responseFrame = getAssociationResponseFrame(transmitStep); auto responseHeader = dynamicPtrCast(responseFrame->peekAtFront()); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc index 514e3686985..d6abb64108a 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc @@ -47,11 +47,11 @@ void Ieee80211MgmtBase::receiveSignal(cComponent *source, simsignal_t signalID, if (signalID == modesetChangedSignal) { modeSet = check_and_cast(obj); mib->updateLocalHtCapabilities(modeSet); - supportedRates.numRates = std::min(8, modeSet->getNumModes()); int rateIndex = 0; - for (int i = 0; i < supportedRates.numRates; i++) + for (int i = 0; i < modeSet->getNumModes() && rateIndex < 8; i++) if (modeSet->isMandatory(i)) supportedRates.rate[rateIndex++] = modeSet->getMode(i)->getDataMode()->getNetBitrate().get(); + supportedRates.numRates = rateIndex; } } diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc index f1dbd065a3f..f29673caf94 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc @@ -125,7 +125,11 @@ void Ieee80211MgmtSta::handleTimer(cMessage *msg) EV << "Association timed out, AP address = " << ap->address << "\n"; // send back failure report to agent - sendAssociationConfirm(ap, PRC_TIMEOUT); + if (reassociationInProgress) + sendReassociationConfirm(ap, PRC_TIMEOUT); + else + sendAssociationConfirm(ap, PRC_TIMEOUT); + reassociationInProgress = false; } else if (msg->getKind() == MK_SCAN_MAXCHANNELTIME) { // go to next channel during scanning @@ -273,6 +277,7 @@ void Ieee80211MgmtSta::startAssociation(ApInfo *ap, simtime_t timeout) addHtCapabilities(body); body->setChunkLength(B(2 + 2 + (2 + strlen(body->getSSID())) + (2 + body->getSupportedRates().numRates)) + getHtMgmtElementsLength(body)); sendManagementFrame("Assoc", body, ST_ASSOCIATIONREQUEST, ap->address); + reassociationInProgress = false; // schedule timeout ASSERT(assocTimeoutMsg == nullptr); @@ -295,6 +300,7 @@ void Ieee80211MgmtSta::startReassociation(ApInfo *ap, simtime_t timeout) addHtCapabilities(body); body->setChunkLength(B(2 + 2 + 6 + (2 + strlen(body->getSSID())) + (2 + body->getSupportedRates().numRates)) + getHtMgmtElementsLength(body)); sendManagementFrame("Reassoc", body, ST_REASSOCIATIONREQUEST, ap->address); + reassociationInProgress = true; assocTimeoutMsg = new cMessage("assocTimeout", MK_ASSOC_TIMEOUT); assocTimeoutMsg->setContextPointer(ap); scheduleAfter(timeout, assocTimeoutMsg); @@ -468,6 +474,12 @@ void Ieee80211MgmtSta::processAssociateCommand(Ieee80211Prim_AssociateRequest *c void Ieee80211MgmtSta::processReassociateCommand(Ieee80211Prim_ReassociateRequest *ctrl) { const MacAddress& address = ctrl->getAddress(); + if (!mib->bssStationData.isAssociated) { + auto confirm = new Ieee80211Prim_ReassociateConfirm(); + confirm->setAddress(address); + sendConfirm(confirm, PRC_REFUSED); + return; + } ApInfo *ap = lookupAP(address); if (!ap) throw cRuntimeError("processReassociateCommand: AP not known: address = %s", address.str().c_str()); @@ -513,7 +525,16 @@ void Ieee80211MgmtSta::sendAuthenticationConfirm(ApInfo *ap, Ieee80211PrimResult void Ieee80211MgmtSta::sendAssociationConfirm(ApInfo *ap, Ieee80211PrimResultCode resultCode) { - sendConfirm(new Ieee80211Prim_AssociateConfirm(), resultCode); + auto confirm = new Ieee80211Prim_AssociateConfirm(); + confirm->setAddress(ap->address); + sendConfirm(confirm, resultCode); +} + +void Ieee80211MgmtSta::sendReassociationConfirm(ApInfo *ap, Ieee80211PrimResultCode resultCode) +{ + auto confirm = new Ieee80211Prim_ReassociateConfirm(); + confirm->setAddress(ap->address); + sendConfirm(confirm, resultCode); } void Ieee80211MgmtSta::sendConfirm(Ieee80211PrimConfirm *confirm, Ieee80211PrimResultCode resultCode) @@ -634,10 +655,10 @@ void Ieee80211MgmtSta::handleAssociationRequestFrame(Packet *packet, const Ptr& header) { - processAssociationResponse(packet, header); + processAssociationResponse(packet, header, false); } -void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const Ptr& header) +void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const Ptr& header, bool reassociation) { EV << "Received Association or Reassociation Response frame\n"; @@ -677,6 +698,7 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const Ptr& header) @@ -722,7 +747,7 @@ void Ieee80211MgmtSta::handleReassociationRequestFrame(Packet *packet, const Ptr void Ieee80211MgmtSta::handleReassociationResponseFrame(Packet *packet, const Ptr& header) { - processAssociationResponse(packet, header); + processAssociationResponse(packet, header, true); } void Ieee80211MgmtSta::handleDisassociationFrame(Packet *packet, const Ptr& header) diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h index 2b7f2c72c55..227fdc7144f 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h @@ -96,6 +96,7 @@ class INET_API Ieee80211MgmtSta : public Ieee80211MgmtBase // associated Access Point cMessage *assocTimeoutMsg; // if non-nullptr: association is in progress + bool reassociationInProgress = false; AssociatedApInfo assocAP; public: @@ -133,7 +134,7 @@ class INET_API Ieee80211MgmtSta : public Ieee80211MgmtBase virtual void storeAPInfo(Packet *packet, const Ptr& header, const Ptr& body); /** Processes Association and Reassociation Responses without using cached Beacon capabilities. */ - virtual void processAssociationResponse(Packet *packet, const Ptr& header); + virtual void processAssociationResponse(Packet *packet, const Ptr& header, bool reassociation); /** Switches to the next channel to scan; returns true if done (there wasn't any more channel to scan). */ virtual bool scanNextChannel(); @@ -152,6 +153,7 @@ class INET_API Ieee80211MgmtSta : public Ieee80211MgmtBase /** Sends back result of association to the agent */ virtual void sendAssociationConfirm(ApInfo *ap, Ieee80211PrimResultCode resultCode); + virtual void sendReassociationConfirm(ApInfo *ap, Ieee80211PrimResultCode resultCode); /** Utility function: Cancel the existing association */ virtual void disassociate(); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc index 4aa8db6523a..600253c56cc 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc @@ -37,21 +37,32 @@ void Ieee80211MgmtStaSimplified::initialize(int stage) mib->bssStationData.isAssociated = true; } else if (stage == INITSTAGE_LINK_LAYER) { - L3AddressResolver addressResolver; - auto accessPointAddress = addressResolver.resolve(par("accessPointAddress"), L3AddressResolver::ADDR_MAC).toMac(); - mib->bssData.bssid = accessPointAddress; - auto apMib = findAccessPointMib(accessPointAddress); - apMib->bssAccessPointData.stations[mib->address] = Ieee80211Mib::ASSOCIATED; - mib->bssData.ssid = apMib->bssData.ssid; + configureAssociation(); } - else if (stage == INITSTAGE_LAST) { - auto apMib = findAccessPointMib(mib->bssData.bssid); - // Simplified management is an explicit no-air abstraction: install the state that the - // Association Request/Response exchange would have committed in detailed management. - if (mib->isHtOperationSupported() && apMib->isHtOperationSupported()) { - mib->setPeerHtCapabilities(apMib->address, apMib->localHtCapabilities, apMib->htOperation); - apMib->setPeerHtCapabilities(mib->address, mib->localHtCapabilities, apMib->htOperation); - } + else if (stage == INITSTAGE_LAST) + configureAssociation(); +} + +void Ieee80211MgmtStaSimplified::start() +{ + Ieee80211MgmtBase::start(); + configureAssociation(); +} + +void Ieee80211MgmtStaSimplified::configureAssociation() +{ + L3AddressResolver addressResolver; + auto accessPointAddress = addressResolver.resolve(par("accessPointAddress"), L3AddressResolver::ADDR_MAC).toMac(); + mib->bssData.bssid = accessPointAddress; + auto apMib = findAccessPointMib(accessPointAddress); + apMib->bssAccessPointData.stations[mib->address] = Ieee80211Mib::ASSOCIATED; + mib->bssData.ssid = apMib->bssData.ssid; + mib->bssStationData.isAssociated = true; + // Simplified management is an explicit no-air abstraction: install the state that the + // Association Request/Response exchange would have committed in detailed management. + if (mib->isHtOperationSupported() && apMib->isHtOperationSupported()) { + mib->setPeerHtCapabilities(apMib->address, apMib->localHtCapabilities, apMib->htOperation); + apMib->setPeerHtCapabilities(mib->address, mib->localHtCapabilities, apMib->htOperation); } } diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h index 90a69af2af2..4306f65798d 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h @@ -25,6 +25,8 @@ class INET_API Ieee80211MgmtStaSimplified : public Ieee80211MgmtBase protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int) override; + virtual void start() override; + virtual void configureAssociation(); /** Implements abstract Ieee80211MgmtBase method */ virtual void handleTimer(cMessage *msg) override; @@ -52,4 +54,3 @@ class INET_API Ieee80211MgmtStaSimplified : public Ieee80211MgmtBase } // namespace inet #endif - diff --git a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test index 3912c2cba72..b7a227860a1 100644 --- a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test +++ b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test @@ -47,11 +47,25 @@ class TestInitializationObserver : public cSimpleModule ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); std::cout << "Simplified STA HT peer state installed at last initialization stage.\n"; + scheduleAt(SimTime(3, SIMTIME_US), new cMessage("checkRestart")); } } } - virtual void handleMessage(cMessage *message) override { throw cRuntimeError("Unexpected message"); } + virtual void handleMessage(cMessage *message) override + { + auto apInterface = check_and_cast(getModuleByPath("^.ap.wlan[0]")); + auto staInterface = check_and_cast(getModuleByPath("^.sta.wlan[0]")); + auto apMib = check_and_cast(apInterface->getSubmodule("mib")); + auto staMib = check_and_cast(staInterface->getSubmodule("mib")); + ASSERT(staMib->bssStationData.isAssociated); + ASSERT(staMib->bssData.ssid == apMib->bssData.ssid); + ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); + ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); + ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); + std::cout << "Simplified STA association and HT peer state restored after restart.\n"; + delete message; + } }; Define_Module(TestInitializationObserver); @@ -61,6 +75,7 @@ Define_Module(TestInitializationObserver); %file: test.ned import inet.common.SimpleModule; +import inet.common.scenario.ScenarioManager; import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; import inet.node.inet.WirelessHost; import inet.node.wireless.AccessPoint; @@ -83,6 +98,7 @@ network Ieee80211MgmtStaSimplifiedInitializationTestNetwork submodules: radioMedium: Ieee80211ScalarRadioMedium; configurator: TestIpv4NetworkConfigurator; + scenarioManager: ScenarioManager; ap: AccessPoint { parameters: wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified"; @@ -101,7 +117,7 @@ network Ieee80211MgmtStaSimplifiedInitializationTestNetwork [General] network = Ieee80211MgmtStaSimplifiedInitializationTestNetwork ned-path = .;../../../../src;../../lib -sim-time-limit = 1us +sim-time-limit = 4us cmdenv-express-mode = true record-vector-results = false record-scalar-results = false @@ -111,6 +127,8 @@ record-scalar-results = false *.ap.wlan[0].mgmt.ssid = "review-ssid" *.sta.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:01" **.wlan[0].opMode = "n(mixed-2.4Ghz)" +**.hasStatus = true +**.scenarioManager.script = xmldoc("scenario.xml") **.wlan[0].bitrate = 65Mbps **.wlan[0].radio.bandName = "2.4 GHz" **.wlan[0].radio.centerFrequency = 2.4GHz @@ -122,8 +140,22 @@ record-scalar-results = false **.mobility.constraintAreaMaxY = 100m **.mobility.constraintAreaMaxZ = 0m +%file: scenario.xml + + + + + + + + + + %contains: stdout Simplified STA wireless identity available during network configuration. %contains: stdout Simplified STA HT peer state installed at last initialization stage. + +%contains: stdout +Simplified STA association and HT peer state restored after restart. From 9f7ffae627cb082a6b9f78c3636b283b3c364da0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 19 Aug 2026 08:43:21 +0200 Subject: [PATCH 05/10] Fix 802.11 reassociation dispatch and startup ordering Dispatch reassociation requests and confirms before their association base classes so the dedicated handlers remain reachable and reassociation results emit PR_REASSOCIATE_CONFIRM. Avoid installing simplified-station cross-node association state during the initialization-time null lifecycle callback. Keep normal link-layer and last-stage setup, while restoring AP membership and bilateral HT peer state for real runtime startup operations. Add focused coverage for ordinary association, successful and refused reassociation confirms, rescan recovery, and request-side dispatch. Strengthen the simplified-station lifecycle test with STA-before-AP ordering, automatic station addressing, rejection of unspecified station-map keys, and shutdown/start restoration. Validated with a debug build and focused IEEE 802.11 unit and module tests; no full suites or fingerprint baselines were run. --- .../ieee80211/mgmt/Ieee80211AgentSta.cc | 4 +- .../ieee80211/mgmt/Ieee80211MgmtSta.cc | 4 +- .../mgmt/Ieee80211MgmtStaSimplified.cc | 7 +- .../mgmt/Ieee80211MgmtStaSimplified.h | 2 +- ...0211MgmtStaSimplifiedInitialization_1.test | 13 +- .../Ieee80211MgmtStaPrimitiveDispatch_1.test | 118 ++++++++++++++++++ 6 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.cc index 39082f77669..9737e8c4356 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.cc @@ -98,10 +98,10 @@ void Ieee80211AgentSta::handleResponse(cMessage *msg) processScanConfirm(ptr); else if (auto ptr = dynamic_cast(ctrl)) processAuthenticateConfirm(ptr); - else if (auto ptr = dynamic_cast(ctrl)) - processAssociateConfirm(ptr); else if (auto ptr = dynamic_cast(ctrl)) processReassociateConfirm(ptr); + else if (auto ptr = dynamic_cast(ctrl)) + processAssociateConfirm(ptr); else if (ctrl) throw cRuntimeError("handleResponse(): unrecognized control info class `%s'", ctrl->getClassName()); else diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc index f29673caf94..173796c8282 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc @@ -177,10 +177,10 @@ void Ieee80211MgmtSta::handleCommand(int msgkind, cObject *ctrl) processAuthenticateCommand(cmd); else if (auto cmd = dynamic_cast(ctrl)) processDeauthenticateCommand(cmd); - else if (auto cmd = dynamic_cast(ctrl)) - processAssociateCommand(cmd); else if (auto cmd = dynamic_cast(ctrl)) processReassociateCommand(cmd); + else if (auto cmd = dynamic_cast(ctrl)) + processAssociateCommand(cmd); else if (auto cmd = dynamic_cast(ctrl)) processDisassociateCommand(cmd); else if (ctrl) diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc index 600253c56cc..d7fd10214b7 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc @@ -43,10 +43,11 @@ void Ieee80211MgmtStaSimplified::initialize(int stage) configureAssociation(); } -void Ieee80211MgmtStaSimplified::start() +void Ieee80211MgmtStaSimplified::handleStartOperation(LifecycleOperation *operation) { - Ieee80211MgmtBase::start(); - configureAssociation(); + Ieee80211MgmtBase::handleStartOperation(operation); + if (operation != nullptr) + configureAssociation(); } void Ieee80211MgmtStaSimplified::configureAssociation() diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h index 4306f65798d..7341ab162a1 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h @@ -25,7 +25,7 @@ class INET_API Ieee80211MgmtStaSimplified : public Ieee80211MgmtBase protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } virtual void initialize(int) override; - virtual void start() override; + virtual void handleStartOperation(LifecycleOperation *operation) override; virtual void configureAssociation(); /** Implements abstract Ieee80211MgmtBase method */ diff --git a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test index b7a227860a1..2c2cc55ec37 100644 --- a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test +++ b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test @@ -39,6 +39,9 @@ class TestInitializationObserver : public cSimpleModule ASSERT(apMib->bssData.ssid == "review-ssid"); ASSERT(staMib->bssData.ssid == apMib->bssData.ssid); ASSERT(configurator->getTestWirelessId(apInterface) == configurator->getTestWirelessId(staInterface)); + ASSERT(!staMib->address.isUnspecified()); + ASSERT(apMib->bssAccessPointData.stations.find(MacAddress::UNSPECIFIED_ADDRESS) == apMib->bssAccessPointData.stations.end()); + ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); ASSERT(apMib->findPeerHtState(staMib->address) == nullptr); ASSERT(staMib->findPeerHtState(apMib->address) == nullptr); std::cout << "Simplified STA wireless identity available during network configuration.\n"; @@ -99,14 +102,14 @@ network Ieee80211MgmtStaSimplifiedInitializationTestNetwork radioMedium: Ieee80211ScalarRadioMedium; configurator: TestIpv4NetworkConfigurator; scenarioManager: ScenarioManager; - ap: AccessPoint { + sta: WirelessHost { parameters: - wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified"; + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; wlan[*].agent.typename = ""; } - sta: WirelessHost { + ap: AccessPoint { parameters: - wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified"; wlan[*].agent.typename = ""; } observer: TestInitializationObserver; @@ -123,7 +126,7 @@ record-vector-results = false record-scalar-results = false *.ap.wlan[0].address = "02:00:00:00:00:01" -*.sta.wlan[0].address = "02:00:00:00:00:02" +*.sta.wlan[0].address = "auto" *.ap.wlan[0].mgmt.ssid = "review-ssid" *.sta.wlan[0].mgmt.accessPointAddress = "02:00:00:00:00:01" **.wlan[0].opMode = "n(mixed-2.4Ghz)" diff --git a/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test b/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test new file mode 100644 index 00000000000..ff9e64082fa --- /dev/null +++ b/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test @@ -0,0 +1,118 @@ +%description: +Verify that association and reassociation management primitives are dispatched +to their most-derived handlers, including the distinct reassociation confirm +primitive emitted by the station agent. + +%includes: +#include "inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h" + +using namespace inet; +using namespace inet::ieee80211; + +%global: + +class TestIeee80211AgentSta : public Ieee80211AgentSta +{ + public: + using Ieee80211AgentSta::handleResponse; + + int associateConfirms = 0; + int reassociateConfirms = 0; + int scanRequests = 0; + + protected: + virtual void processAssociateConfirm(Ieee80211Prim_AssociateConfirm *resp) override + { + associateConfirms++; + } + + virtual void processReassociateConfirm(Ieee80211Prim_ReassociateConfirm *resp) override + { + reassociateConfirms++; + Ieee80211AgentSta::processReassociateConfirm(resp); + } + + virtual void sendScanRequest() override + { + scanRequests++; + } +}; + +class ConfirmListener : public cListener +{ + public: + int lastCode = 0; + + virtual void receiveSignal(cComponent *source, simsignal_t signalID, intval_t value, cObject *details) override + { + lastCode = value; + } +}; + +class TestIeee80211MgmtSta : public Ieee80211MgmtSta +{ + public: + using Ieee80211MgmtSta::handleCommand; + + int associateRequests = 0; + int reassociateRequests = 0; + + protected: + virtual void processAssociateCommand(Ieee80211Prim_AssociateRequest *ctrl) override + { + associateRequests++; + } + + virtual void processReassociateCommand(Ieee80211Prim_ReassociateRequest *ctrl) override + { + reassociateRequests++; + } +}; + +static void deliverConfirm(TestIeee80211AgentSta& agent, Ieee80211PrimConfirm *confirm) +{ + auto message = new cMessage("confirm"); + message->setControlInfo(confirm); + agent.handleResponse(message); +} + +%activity: + +TestIeee80211AgentSta agent; +ConfirmListener listener; +agent.subscribe("acceptConfirm", &listener); +deliverConfirm(agent, new Ieee80211Prim_AssociateConfirm()); +ASSERT(agent.associateConfirms == 1); +ASSERT(agent.reassociateConfirms == 0); + +auto reassociateConfirm = new Ieee80211Prim_ReassociateConfirm(); +reassociateConfirm->setResultCode(PRC_SUCCESS); +deliverConfirm(agent, reassociateConfirm); +ASSERT(agent.associateConfirms == 1); +ASSERT(agent.reassociateConfirms == 1); +ASSERT(listener.lastCode == PR_REASSOCIATE_CONFIRM); + +ConfirmListener dropListener; +agent.subscribe("dropConfirm", &dropListener); +auto refusedReassociateConfirm = new Ieee80211Prim_ReassociateConfirm(); +refusedReassociateConfirm->setResultCode(PRC_REFUSED); +deliverConfirm(agent, refusedReassociateConfirm); +ASSERT(agent.associateConfirms == 1); +ASSERT(agent.reassociateConfirms == 2); +ASSERT(agent.scanRequests == 1); +ASSERT(dropListener.lastCode == PR_REASSOCIATE_CONFIRM); + +TestIeee80211MgmtSta mgmt; +mgmt.handleCommand(PR_ASSOCIATE_REQUEST, new Ieee80211Prim_AssociateRequest()); +ASSERT(mgmt.associateRequests == 1); +ASSERT(mgmt.reassociateRequests == 0); + +mgmt.handleCommand(PR_REASSOCIATE_REQUEST, new Ieee80211Prim_ReassociateRequest()); +ASSERT(mgmt.associateRequests == 1); +ASSERT(mgmt.reassociateRequests == 1); + +EV << "Association and reassociation primitive dispatch verified.\n"; + +%contains: stdout +Association and reassociation primitive dispatch verified. From dbd36e8a4305090b7e3762d0ef4f3dbcac913465 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 19 Aug 2026 14:13:14 +0200 Subject: [PATCH 06/10] Fix 802.11 association exchange completion and failure state Treat an accepted RTS/CTS handshake as setup for a protected association response rather than a terminal response decision. Evaluate the actual response/ACK pair, retain pending state while either RTS or response retries remain, and complete only terminal failures. Prefilter association transactions by tag and inspect packet front chunks without forced conversion so unrelated or aggregate-shaped transmissions cannot abort AP management processing. Match station-side responses to the AP stored in the pending transaction. Clear and delete association timeout messages before confirmation dispatch, reject late or mismatched responses, and apply IEEE 802.11 same-AP versus different-AP reassociation failure semantics consistently for refused responses and timeouts. Extend focused unit coverage for RTS/CTS decision points, retry disposition, non-MAC chunks, timeout ownership, pending-AP matching, late responses, and reassociation failure selection. Force RTS protection in the detailed HT association module test and verify committed AID, AP/STA state, bilateral peer HT state, and exactly one AP association notification. Validated with the debug build, two focused debug unit tests, the forced-RTS association module test, two RTS-on fingerprint cases, and independent architecture/WLAN review. --- .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 45 +++++++-- .../ieee80211/mgmt/Ieee80211MgmtAp.h | 4 + .../ieee80211/mgmt/Ieee80211MgmtSta.cc | 44 +++++++-- .../ieee80211/mgmt/Ieee80211MgmtSta.h | 5 + tests/module/Ieee80211HtAssociation_1.test | 67 ++++++++++++- tests/unit/Ieee80211MgmtApTransaction_1.test | 30 ++++++ .../Ieee80211MgmtStaPrimitiveDispatch_1.test | 97 ++++++++++++++++++- 7 files changed, 272 insertions(+), 20 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index c8670f84072..3f0e92c9086 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -47,13 +47,40 @@ const Packet *Ieee80211MgmtAp::getAssociationResponseFrame(ITransmitStep *transm return transmitStep->getFrameToTransmit(); } +Ptr Ieee80211MgmtAp::getMacHeader(const Packet *frame) +{ + if (frame == nullptr) + return nullptr; + const auto& frontChunk = frame->peekAtFront(b(-1), Chunk::PF_ALLOW_NULLPTR); + return dynamicPtrCast(frontChunk); +} + +Ptr Ieee80211MgmtAp::getAssociationResponseHeader(const Packet *responseFrame) +{ + if (responseFrame == nullptr || responseFrame->findTag() == nullptr) + return nullptr; + const auto& responseHeader = dynamicPtrCast(getMacHeader(responseFrame)); + if (responseHeader == nullptr || (responseHeader->getType() != ST_ASSOCIATIONRESPONSE && responseHeader->getType() != ST_REASSOCIATIONRESPONSE)) + return nullptr; + return responseHeader; +} + +bool Ieee80211MgmtAp::isAssociationResponseDecisionPoint(ITransmitStep *transmitStep, IReceiveStep *receiveStep) +{ + const auto& receivedHeader = getMacHeader(receiveStep->getReceivedFrame()); + return dynamic_cast(transmitStep) == nullptr || + transmitStep->getCompletion() != IFrameSequenceStep::Completion::ACCEPTED || + receiveStep->getCompletion() != IFrameSequenceStep::Completion::ACCEPTED || + receivedHeader == nullptr || receivedHeader->getType() != ST_CTS; +} + Ieee80211MgmtAp::AssociationResponseDisposition Ieee80211MgmtAp::getAssociationResponseDisposition(const Packet *responseFrame, uint64_t pendingTransactionId, bool exchangeSucceeded, bool retryPending) { if (responseFrame == nullptr || pendingTransactionId == 0) return AssociationResponseDisposition::IGNORE; - auto responseHeader = dynamicPtrCast(responseFrame->peekAtFront()); - if (responseHeader == nullptr || (responseHeader->getType() != ST_ASSOCIATIONRESPONSE && responseHeader->getType() != ST_REASSOCIATIONRESPONSE)) + const auto& responseHeader = getAssociationResponseHeader(responseFrame); + if (responseHeader == nullptr) return AssociationResponseDisposition::IGNORE; const auto& transactionTag = responseFrame->findTag(); if (transactionTag == nullptr || transactionTag->getTransactionId() != pendingTransactionId) @@ -150,13 +177,16 @@ void Ieee80211MgmtAp::receiveSignal(cComponent *source, simsignal_t signalID, cO auto receiveStep = dynamic_cast(context->getStep(stepIndex + 1)); if (transmitStep && receiveStep) { const Packet *responseFrame = getAssociationResponseFrame(transmitStep); - auto responseHeader = dynamicPtrCast(responseFrame->peekAtFront()); - if (responseHeader != nullptr && (responseHeader->getType() == ST_ASSOCIATIONRESPONSE || responseHeader->getType() == ST_REASSOCIATIONRESPONSE)) { + const auto& responseHeader = getAssociationResponseHeader(responseFrame); + if (responseHeader != nullptr) { + if (!isAssociationResponseDecisionPoint(transmitStep, receiveStep)) + continue; + const auto& receivedHeader = getMacHeader(receiveStep->getReceivedFrame()); const auto& address = responseHeader->getReceiverAddress(); auto sta = staList.find(address); bool exchangeSucceeded = transmitStep->getCompletion() == IFrameSequenceStep::Completion::ACCEPTED && receiveStep->getCompletion() == IFrameSequenceStep::Completion::ACCEPTED && - receiveStep->getReceivedFrame()->peekAtFront()->getType() == ST_ACK; + receivedHeader != nullptr && receivedHeader->getType() == ST_ACK; bool retryPending = false; if (!exchangeSucceeded) { auto inProgressFrames = context->getInProgressFrames(); @@ -183,8 +213,9 @@ void Ieee80211MgmtAp::receiveSignal(cComponent *source, simsignal_t signalID, cO } else if (exchangeSucceeded && mib->bssAccessPointData.stations[address] == Ieee80211Mib::ASSOCIATED) { // This model does not implement negotiated management-frame protection. - // IEEE Std 802.11-2024, 11.3.5.3(p): a refused (re)association - // therefore moves the STA from State 4 back to State 3. + // IEEE Std 802.11-2024, 11.3.5.3(p) for association and 11.3.5.5(n) + // for same-AP reassociation therefore require the existing association + // state to be cleared after this acknowledged refusal. mib->releaseAssociationId(address); mib->bssAccessPointData.stations[address] = Ieee80211Mib::AUTHENTICATED; // Signal delivery is synchronous; observers must see the downgraded state. diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h index b7d6a56e622..3c3402c2cc5 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h @@ -17,6 +17,7 @@ namespace inet { namespace ieee80211 { class ITransmitStep; +class IReceiveStep; /** * Used in 802.11 infrastructure mode: handles management frames for @@ -105,6 +106,9 @@ class INET_API Ieee80211MgmtAp : public Ieee80211MgmtApBase virtual void sendManagementFrame(const char *name, const Ptr& body, int subtype, const MacAddress& destAddr, uint64_t transactionId = 0); static const Packet *getAssociationResponseFrame(ITransmitStep *transmitStep); + static Ptr getMacHeader(const Packet *frame); + static Ptr getAssociationResponseHeader(const Packet *responseFrame); + static bool isAssociationResponseDecisionPoint(ITransmitStep *transmitStep, IReceiveStep *receiveStep); static AssociationResponseDisposition getAssociationResponseDisposition(const Packet *responseFrame, uint64_t pendingTransactionId, bool exchangeSucceeded, bool retryPending); static bool isAssociationResponseTimeoutDue(const StaInfo& sta, uint64_t transactionId, simtime_t deadline, simtime_t currentTime); virtual uint64_t createAssociationTransactionId(); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc index 173796c8282..f20c955b2ef 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc @@ -121,15 +121,22 @@ void Ieee80211MgmtSta::handleTimer(cMessage *msg) } else if (msg->getKind() == MK_ASSOC_TIMEOUT) { // association timed out + ASSERT(msg == assocTimeoutMsg); ApInfo *ap = (ApInfo *)msg->getContextPointer(); + bool reassociation = reassociationInProgress; EV << "Association timed out, AP address = " << ap->address << "\n"; + assocTimeoutMsg = nullptr; + reassociationInProgress = false; + delete msg; + // send back failure report to agent - if (reassociationInProgress) + if (reassociation) { + handleReassociationFailure(ap); sendReassociationConfirm(ap, PRC_TIMEOUT); + } else sendAssociationConfirm(ap, PRC_TIMEOUT); - reassociationInProgress = false; } else if (msg->getKind() == MK_SCAN_MAXCHANNELTIME) { // go to next channel during scanning @@ -668,14 +675,17 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrgetTransmitterAddress(); + ApInfo *ap = static_cast(assocTimeoutMsg->getContextPointer()); + if (ap == nullptr || ap->address != address) { + EV << "Association response is not from the pending AP, ignoring frame\n"; + delete packet; + return; + } + // extract frame contents const auto& responseBody = packet->peekData(); - MacAddress address = header->getTransmitterAddress(); int statusCode = responseBody->getStatusCode(); - // look up AP data structure - ApInfo *ap = lookupAP(address); - if (!ap) - throw cRuntimeError("handleAssociationResponseFrame: AP not known: address=%s", address.str().c_str()); bool responseHtValid = false; Ieee80211HtCapabilities responseHtCapabilities; @@ -702,7 +712,9 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrbssStationData.isAssociated || assocAP.address != ap->address) + if (reassociation) + handleReassociationFailure(ap); + else if (!mib->bssStationData.isAssociated || assocAP.address != ap->address) mib->removePeerHtCapabilities(ap->address); } else { @@ -740,6 +752,22 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrbssStationData.isAssociated, assocAP.address, ap->address)) + disassociate(); + else + mib->removePeerHtCapabilities(ap->address); +} + +bool Ieee80211MgmtSta::shouldDisassociateOnReassociationFailure(bool isAssociated, + const MacAddress& associatedApAddress, const MacAddress& targetApAddress) +{ + return isAssociated && associatedApAddress == targetApAddress; +} + void Ieee80211MgmtSta::handleReassociationRequestFrame(Packet *packet, const Ptr& header) { dropManagementFrame(packet); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h index 227fdc7144f..2447a615aa9 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h @@ -136,6 +136,11 @@ class INET_API Ieee80211MgmtSta : public Ieee80211MgmtBase /** Processes Association and Reassociation Responses without using cached Beacon capabilities. */ virtual void processAssociationResponse(Packet *packet, const Ptr& header, bool reassociation); + /** Applies the failed-reassociation state transition for the target AP. */ + virtual void handleReassociationFailure(ApInfo *ap); + static bool shouldDisassociateOnReassociationFailure(bool isAssociated, + const MacAddress& associatedApAddress, const MacAddress& targetApAddress); + /** Switches to the next channel to scan; returns true if done (there wasn't any more channel to scan). */ virtual bool scanNextChannel(); diff --git a/tests/module/Ieee80211HtAssociation_1.test b/tests/module/Ieee80211HtAssociation_1.test index 846c374d90d..e7a959979a0 100644 --- a/tests/module/Ieee80211HtAssociation_1.test +++ b/tests/module/Ieee80211HtAssociation_1.test @@ -1,13 +1,71 @@ %description: -Verify the detailed IEEE 802.11 management exchange carries the standard HT -element presence matrix and commits mutually usable peer state at both ends. +Verify the RTS/CTS-protected detailed IEEE 802.11 management exchange carries +the standard HT element presence matrix and commits mutually usable peer state +at both ends. + +%file: Ieee80211HtAssociationChecker.cc + +#include "inet/common/Simsignals.h" +#include "inet/common/SimpleModule.h" +#include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" + +using namespace inet; +using namespace inet::ieee80211; + +class Ieee80211HtAssociationChecker : public SimpleModule, public cListener +{ + using cListener::finish; + + protected: + int apAssociationNotifications = 0; + + virtual void initialize() override + { + getModuleByPath("^.ap.wlan[0].mgmt")->subscribe(l2ApAssociatedSignal, this); + } + + virtual void handleMessage(cMessage *message) override + { + throw cRuntimeError("Unexpected message"); + } + + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *value, cObject *details) override + { + apAssociationNotifications++; + } + + virtual void finish() override + { + auto apMib = check_and_cast(getModuleByPath("^.ap.wlan[0].mib")); + auto staMib = check_and_cast(getModuleByPath("^.sta.wlan[0].mib")); + const auto& staAddress = staMib->address; + ASSERT(apMib->bssAccessPointData.stations.at(staAddress) == Ieee80211Mib::ASSOCIATED); + ASSERT(apMib->bssAccessPointData.associationIds.at(staAddress) != 0); + ASSERT(apMib->findPeerHtState(staAddress) != nullptr); + ASSERT(apMib->findPeerHtState(staAddress)->valid); + ASSERT(staMib->bssStationData.isAssociated); + ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); + ASSERT(staMib->findPeerHtState(apMib->address)->valid); + ASSERT(apAssociationNotifications == 1); + EV << "RTS-protected association committed at AP and STA.\n"; + } +}; + +Define_Module(Ieee80211HtAssociationChecker); %file: test.ned +import inet.common.SimpleModule; import inet.node.inet.WirelessHost; import inet.node.wireless.AccessPoint; import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +simple Ieee80211HtAssociationChecker extends SimpleModule +{ + parameters: + @class(::Ieee80211HtAssociationChecker); +} + network Ieee80211HtAssociationTest { submodules: @@ -21,6 +79,7 @@ network Ieee80211HtAssociationTest parameters: wlan[*].mgmt.typename = "Ieee80211MgmtAp"; } + test: Ieee80211HtAssociationChecker; } %inifile: omnetpp.ini @@ -55,6 +114,7 @@ record-scalar-results = false **.wlan[*].radio.receiver.snirThreshold = 4dB **.mgmt.numChannels = 1 +*.ap.wlan[*].mac.dcf.rtsPolicy.rtsThreshold = 0B *.sta.wlan[*].agent.startingTime = 0s *.sta.wlan[*].agent.probeDelay = 1ms *.sta.wlan[*].agent.minChannelTime = 5ms @@ -73,3 +133,6 @@ AssocResp-OK .*?Ieee80211AssociationResponseFrame.*?htCapabilitiesPresent.*?true %contains: stdout Installed peer HT state, peer = + +%contains: stdout +RTS-protected association committed at AP and STA. diff --git a/tests/unit/Ieee80211MgmtApTransaction_1.test b/tests/unit/Ieee80211MgmtApTransaction_1.test index 158b158199b..3883b4026f8 100644 --- a/tests/unit/Ieee80211MgmtApTransaction_1.test +++ b/tests/unit/Ieee80211MgmtApTransaction_1.test @@ -17,9 +17,12 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp { public: using Ieee80211MgmtAp::AssociationResponseDisposition; + using Ieee80211MgmtAp::getAssociationResponseHeader; using Ieee80211MgmtAp::getAssociationResponseDisposition; using Ieee80211MgmtAp::getAssociationResponseFrame; + using Ieee80211MgmtAp::getMacHeader; using Ieee80211MgmtAp::isAssociationResponseTimeoutDue; + using Ieee80211MgmtAp::isAssociationResponseDecisionPoint; }; static Packet *makeAssociationResponse(uint64_t transactionId, bool moreFragments, bool includeTag = true) @@ -46,9 +49,28 @@ ASSERT(TestIeee80211MgmtAp::getAssociationResponseFrame(&ordinaryStep) == finalR auto rtsPacket = new Packet("RTS", makeShared()); RtsTransmitStep rtsStep(finalResponse, rtsPacket, SIMTIME_ZERO); ASSERT(TestIeee80211MgmtAp::getAssociationResponseFrame(&rtsStep) == finalResponse); + + rtsStep.setCompletion(IFrameSequenceStep::Completion::ACCEPTED); + ReceiveStep ctsStep; + ctsStep.setCompletion(IFrameSequenceStep::Completion::ACCEPTED); + ctsStep.setFrameToReceive(new Packet("CTS", makeShared())); + ASSERT(!TestIeee80211MgmtAp::isAssociationResponseDecisionPoint(&rtsStep, &ctsStep)); + + ReceiveStep expiredCtsStep; + expiredCtsStep.setCompletion(IFrameSequenceStep::Completion::EXPIRED); + ASSERT(TestIeee80211MgmtAp::isAssociationResponseDecisionPoint(&rtsStep, &expiredCtsStep)); + const Packet *protectedResponse = TestIeee80211MgmtAp::getAssociationResponseFrame(&rtsStep); + ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(protectedResponse, 42, false, true) == Disposition::RETAIN); + ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(protectedResponse, 42, false, false) == Disposition::COMPLETE); } +ordinaryStep.setCompletion(IFrameSequenceStep::Completion::ACCEPTED); +ReceiveStep ackStep; +ackStep.setCompletion(IFrameSequenceStep::Completion::ACCEPTED); +ackStep.setFrameToReceive(new Packet("ACK", makeShared())); +ASSERT(TestIeee80211MgmtAp::isAssociationResponseDecisionPoint(&ordinaryStep, &ackStep)); ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 42, true, false) == Disposition::COMPLETE); + ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 41, true, false) == Disposition::IGNORE); ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 0, true, false) == Disposition::IGNORE); @@ -57,6 +79,13 @@ ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(zeroTokenResponse, auto untaggedResponse = makeAssociationResponse(42, false, false); ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(untaggedResponse, 42, true, false) == Disposition::IGNORE); +auto aggregate = new Packet("A-MPDU-like"); +aggregate->insertAtBack(makeShared()); +aggregate->addTag()->setTransactionId(42); +ASSERT(TestIeee80211MgmtAp::getMacHeader(aggregate) == nullptr); +ASSERT(TestIeee80211MgmtAp::getAssociationResponseHeader(aggregate) == nullptr); +ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(aggregate, 42, true, false) == Disposition::IGNORE); + auto nonFinalResponse = makeAssociationResponse(42, true); ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(nonFinalResponse, 42, true, false) == Disposition::RETAIN); ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 42, false, true) == Disposition::RETAIN); @@ -76,6 +105,7 @@ ASSERT(!TestIeee80211MgmtAp::isAssociationResponseTimeoutDue(sta, 0, SimTime(5), delete nonFinalResponse; delete untaggedResponse; delete zeroTokenResponse; +delete aggregate; delete finalResponse; EV << "AP association transaction and timeout decisions verified.\n"; diff --git a/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test b/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test index ff9e64082fa..da47b7814d8 100644 --- a/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test +++ b/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test @@ -1,7 +1,8 @@ %description: Verify that association and reassociation management primitives are dispatched to their most-derived handlers, including the distinct reassociation confirm -primitive emitted by the station agent. +primitive emitted by the station agent. Verify association timeout ownership, +confirm typing, restart eligibility, and late-response rejection. %includes: #include "inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.h" @@ -54,9 +55,47 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta { public: using Ieee80211MgmtSta::handleCommand; + using Ieee80211MgmtSta::shouldDisassociateOnReassociationFailure; int associateRequests = 0; int reassociateRequests = 0; + int associateConfirms = 0; + int reassociateConfirms = 0; + int reassociationFailures = 0; + Ieee80211PrimResultCode lastAssociationResult = PRC_SUCCESS; + Ieee80211PrimResultCode lastReassociationResult = PRC_SUCCESS; + + void prepareAssociationTimeout(ApInfo *ap, bool reassociation) + { + ASSERT(assocTimeoutMsg == nullptr); + assocTimeoutMsg = new cMessage("assocTimeout", 2); + assocTimeoutMsg->setContextPointer(ap); + reassociationInProgress = reassociation; + } + + void deliverAssociationTimeout() + { + ASSERT(assocTimeoutMsg != nullptr); + handleTimer(assocTimeoutMsg); + } + + bool canStartAnotherAssociation() const { return assocTimeoutMsg == nullptr; } + bool isReassociationInProgress() const { return reassociationInProgress; } + + void deliverLateAssociationResponse(bool reassociation) + { + auto packet = new Packet("LateAssocResp"); + auto header = makeShared(); + processAssociationResponse(packet, header, reassociation); + } + + void deliverMismatchedAssociationResponse(const MacAddress& address, bool reassociation) + { + auto packet = new Packet("MismatchedAssocResp"); + auto header = makeShared(); + header->setTransmitterAddress(address); + processAssociationResponse(packet, header, reassociation); + } protected: virtual void processAssociateCommand(Ieee80211Prim_AssociateRequest *ctrl) override @@ -68,6 +107,23 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta { reassociateRequests++; } + + virtual void sendAssociationConfirm(ApInfo *ap, Ieee80211PrimResultCode resultCode) override + { + associateConfirms++; + lastAssociationResult = resultCode; + } + + virtual void sendReassociationConfirm(ApInfo *ap, Ieee80211PrimResultCode resultCode) override + { + reassociateConfirms++; + lastReassociationResult = resultCode; + } + + virtual void handleReassociationFailure(ApInfo *ap) override + { + reassociationFailures++; + } }; static void deliverConfirm(TestIeee80211AgentSta& agent, Ieee80211PrimConfirm *confirm) @@ -112,7 +168,42 @@ mgmt.handleCommand(PR_REASSOCIATE_REQUEST, new Ieee80211Prim_ReassociateRequest( ASSERT(mgmt.associateRequests == 1); ASSERT(mgmt.reassociateRequests == 1); -EV << "Association and reassociation primitive dispatch verified.\n"; +Ieee80211MgmtSta::ApInfo timeoutAp; +timeoutAp.address = MacAddress("02:00:00:00:00:01"); +MacAddress otherApAddress("02:00:00:00:00:02"); +ASSERT(TestIeee80211MgmtSta::shouldDisassociateOnReassociationFailure(true, timeoutAp.address, timeoutAp.address)); +ASSERT(!TestIeee80211MgmtSta::shouldDisassociateOnReassociationFailure(true, timeoutAp.address, otherApAddress)); +ASSERT(!TestIeee80211MgmtSta::shouldDisassociateOnReassociationFailure(false, timeoutAp.address, timeoutAp.address)); +mgmt.prepareAssociationTimeout(&timeoutAp, false); +mgmt.deliverMismatchedAssociationResponse(otherApAddress, false); +ASSERT(!mgmt.canStartAnotherAssociation()); +ASSERT(mgmt.associateConfirms == 0); +mgmt.deliverAssociationTimeout(); +ASSERT(mgmt.canStartAnotherAssociation()); +ASSERT(!mgmt.isReassociationInProgress()); +ASSERT(mgmt.associateConfirms == 1); +ASSERT(mgmt.reassociateConfirms == 0); +ASSERT(mgmt.lastAssociationResult == PRC_TIMEOUT); +ASSERT(mgmt.reassociationFailures == 0); + +mgmt.deliverLateAssociationResponse(false); +ASSERT(mgmt.associateConfirms == 1); +ASSERT(mgmt.reassociateConfirms == 0); + +mgmt.prepareAssociationTimeout(&timeoutAp, true); +mgmt.deliverAssociationTimeout(); +ASSERT(mgmt.canStartAnotherAssociation()); +ASSERT(!mgmt.isReassociationInProgress()); +ASSERT(mgmt.associateConfirms == 1); +ASSERT(mgmt.reassociateConfirms == 1); +ASSERT(mgmt.lastReassociationResult == PRC_TIMEOUT); +ASSERT(mgmt.reassociationFailures == 1); + +mgmt.deliverLateAssociationResponse(true); +ASSERT(mgmt.associateConfirms == 1); +ASSERT(mgmt.reassociateConfirms == 1); + +EV << "Association and reassociation primitive dispatch and timeout ownership verified.\n"; %contains: stdout -Association and reassociation primitive dispatch verified. +Association and reassociation primitive dispatch and timeout ownership verified. From 904933f82e02c2e3cb8d0c9c1808468ec71d9c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 19 Aug 2026 18:13:52 +0200 Subject: [PATCH 07/10] Fix IEEE 802.11 association lifecycle edge cases Reject non-positive AP association response timeouts so a zero-delay self-message cannot discard a pending transaction before the response exchange and ACK complete. Teach simplified station management to remove its AP-side station entry and negotiated HT peer state during shutdown or crash. Make AP MIB lookup optional during teardown so cleanup remains safe when the AP, interface table, interface, or MIB is no longer resolvable, while still clearing local peer state through the base lifecycle hook. Extend module coverage for the positive timeout lifecycle, zero-timeout validation, graceful shutdown and restart, resolvable crash cleanup and restart, and crash teardown with an unavailable AP lookup. Tests: debug parallel build Tests: focused IEEE 802.11 management module tests (2/2 passed) Tests: debug unit suite (96/96 passed) --- .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 4 +- .../mgmt/Ieee80211MgmtStaSimplified.cc | 36 +++++-- .../mgmt/Ieee80211MgmtStaSimplified.h | 1 + tests/module/Ieee80211MgmtApTimeout_1.test | 24 ++++- ...0211MgmtStaSimplifiedInitialization_1.test | 93 +++++++++++++++++-- 5 files changed, 139 insertions(+), 19 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index 3f0e92c9086..061c6f5ddb0 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -105,8 +105,8 @@ void Ieee80211MgmtAp::initialize(int stage) ssid = par("ssid").stdstringValue(); beaconInterval = par("beaconInterval"); associationResponseTimeout = par("associationResponseTimeout"); - if (associationResponseTimeout < SIMTIME_ZERO) - throw cRuntimeError("parameter 'associationResponseTimeout' must not be negative"); + if (associationResponseTimeout <= SIMTIME_ZERO) + throw cRuntimeError("parameter 'associationResponseTimeout' must be positive"); numAuthSteps = par("numAuthSteps"); if (numAuthSteps != 2 && numAuthSteps != 4) throw cRuntimeError("parameter 'numAuthSteps' (number of frames exchanged during authentication) must be 2 or 4, not %d", numAuthSteps); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc index d7fd10214b7..20c5f97393e 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc @@ -15,17 +15,31 @@ namespace ieee80211 { Define_Module(Ieee80211MgmtStaSimplified); -static Ieee80211Mib *findAccessPointMib(const MacAddress& accessPointAddress) +static Ieee80211Mib *findAccessPointMib(const MacAddress& accessPointAddress, bool required = true) { L3AddressResolver addressResolver; auto host = addressResolver.findHostWithAddress(accessPointAddress); - if (host == nullptr) - throw cRuntimeError("Access point with address %s not found", accessPointAddress.str().c_str()); + if (host == nullptr) { + if (required) + throw cRuntimeError("Access point with address %s not found", accessPointAddress.str().c_str()); + return nullptr; + } auto interfaceTable = addressResolver.findInterfaceTableOf(host); + if (interfaceTable == nullptr) { + if (required) + throw cRuntimeError("Access point interface table with address %s not found", accessPointAddress.str().c_str()); + return nullptr; + } auto networkInterface = interfaceTable->findInterfaceByAddress(accessPointAddress); - if (networkInterface == nullptr) - throw cRuntimeError("Access point interface with address %s not found", accessPointAddress.str().c_str()); - return check_and_cast(networkInterface->getSubmodule("mib")); + if (networkInterface == nullptr) { + if (required) + throw cRuntimeError("Access point interface with address %s not found", accessPointAddress.str().c_str()); + return nullptr; + } + auto apMib = dynamic_cast(networkInterface->getSubmodule("mib")); + if (apMib == nullptr && required) + throw cRuntimeError("Access point MIB with address %s not found", accessPointAddress.str().c_str()); + return apMib; } void Ieee80211MgmtStaSimplified::initialize(int stage) @@ -67,6 +81,16 @@ void Ieee80211MgmtStaSimplified::configureAssociation() } } +void Ieee80211MgmtStaSimplified::stop() +{ + auto apMib = findAccessPointMib(mib->bssData.bssid, false); + if (apMib != nullptr) { + apMib->bssAccessPointData.stations.erase(mib->address); + apMib->removePeerHtCapabilities(mib->address); + } + Ieee80211MgmtBase::stop(); +} + void Ieee80211MgmtStaSimplified::handleTimer(cMessage *msg) { ASSERT(false); diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h index 7341ab162a1..ce9e34c494e 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h @@ -27,6 +27,7 @@ class INET_API Ieee80211MgmtStaSimplified : public Ieee80211MgmtBase virtual void initialize(int) override; virtual void handleStartOperation(LifecycleOperation *operation) override; virtual void configureAssociation(); + virtual void stop() override; /** Implements abstract Ieee80211MgmtBase method */ virtual void handleTimer(cMessage *msg) override; diff --git a/tests/module/Ieee80211MgmtApTimeout_1.test b/tests/module/Ieee80211MgmtApTimeout_1.test index 7418a123cb7..80f4b410065 100644 --- a/tests/module/Ieee80211MgmtApTimeout_1.test +++ b/tests/module/Ieee80211MgmtApTimeout_1.test @@ -1,7 +1,9 @@ %description: Verify the AP's association-response timer releases only expired uncommitted association IDs, preserves later replacement transactions, and never releases -a committed association ID during a reassociation timeout. +a committed association ID during a reassociation timeout. Also verify a zero +association-response timeout is rejected because no response exchange can +complete before a same-time timeout event. %file: TestIeee80211MgmtAp.cc @@ -15,6 +17,7 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp public: short startPendingAssociation(const MacAddress& address) { + Enter_Method("startPendingAssociation"); auto& sta = staList[address]; sta.address = address; clearPendingAssociation(&sta); @@ -27,6 +30,7 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp short startPendingReassociation(const MacAddress& address) { + Enter_Method("startPendingReassociation"); auto& sta = staList[address]; sta.address = address; clearPendingAssociation(&sta); @@ -130,6 +134,16 @@ network Ieee80211MgmtApTimeoutTestNetwork test: Ieee80211MgmtApTimeoutTest; } +%file: checkZeroTimeout.sh + +binary="./@TESTNAME@" +if test -x "${binary}_dbg" && { test ! -x "$binary" || test "${binary}_dbg" -nt "$binary"; }; then + binary="${binary}_dbg" +fi +if "$binary" -u Cmdenv --check-signals=false -lINET -n ../../../../src:.:../../lib -c ZeroTimeout omnetpp.ini _defaults.ini; then + exit 1 +fi + %inifile: omnetpp.ini [General] @@ -150,5 +164,13 @@ record-scalar-results = false **.mobility.constraintAreaMaxY = 100m **.mobility.constraintAreaMaxZ = 0m +[Config ZeroTimeout] +*.ap.wlan[0].mgmt.associationResponseTimeout = 0s + %contains: stdout AP association response timeout lifecycle verified. + +%postrun-command: sh checkZeroTimeout.sh + +%contains: postrun-command(1).err +parameter 'associationResponseTimeout' must be positive diff --git a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test index 2c2cc55ec37..22308556a02 100644 --- a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test +++ b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test @@ -1,12 +1,15 @@ %description: Verify simplified station BSS identity is visible to automatic network configuration while negotiated HT peer state is installed only at the last -initialization stage. +initialization stage. Verify shutdown removes the AP-side association and HT +peer state and startup restores both. Verify crash performs the same cleanup, +and remains safe when the recorded AP can no longer be resolved. %file: TestInitializationObserver.cc #include "inet/common/InitStages.h" #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include "inet/networklayer/common/L3AddressResolver.h" #include "inet/networklayer/common/NetworkInterface.h" #include "inet/networklayer/configurator/ipv4/Ipv4NetworkConfigurator.h" @@ -50,7 +53,12 @@ class TestInitializationObserver : public cSimpleModule ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); std::cout << "Simplified STA HT peer state installed at last initialization stage.\n"; - scheduleAt(SimTime(3, SIMTIME_US), new cMessage("checkRestart")); + scheduleAt(SimTime(1500, SIMTIME_NS), new cMessage("checkShutdown")); + scheduleAt(SimTime(2500, SIMTIME_NS), new cMessage("checkShutdownRestart")); + scheduleAt(SimTime(3500, SIMTIME_NS), new cMessage("checkResolvableCrash")); + scheduleAt(SimTime(4500, SIMTIME_NS), new cMessage("checkCrashRestart")); + scheduleAt(SimTime(5500, SIMTIME_NS), new cMessage("prepareMissingApCrash")); + scheduleAt(SimTime(6500, SIMTIME_NS), new cMessage("checkMissingApCrash")); } } } @@ -61,12 +69,53 @@ class TestInitializationObserver : public cSimpleModule auto staInterface = check_and_cast(getModuleByPath("^.sta.wlan[0]")); auto apMib = check_and_cast(apInterface->getSubmodule("mib")); auto staMib = check_and_cast(staInterface->getSubmodule("mib")); - ASSERT(staMib->bssStationData.isAssociated); - ASSERT(staMib->bssData.ssid == apMib->bssData.ssid); - ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); - ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); - ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); - std::cout << "Simplified STA association and HT peer state restored after restart.\n"; + if (!strcmp(message->getName(), "checkShutdown")) { + ASSERT(apMib->bssAccessPointData.stations.find(staMib->address) == apMib->bssAccessPointData.stations.end()); + ASSERT(apMib->findPeerHtState(staMib->address) == nullptr); + ASSERT(staMib->findPeerHtState(apMib->address) == nullptr); + std::cout << "Simplified STA AP-side association and HT peer state removed after shutdown.\n"; + } + else if (!strcmp(message->getName(), "checkShutdownRestart")) { + ASSERT(staMib->bssStationData.isAssociated); + ASSERT(staMib->bssData.ssid == apMib->bssData.ssid); + ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); + ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); + ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); + std::cout << "Simplified STA association and HT peer state restored after shutdown restart.\n"; + } + else if (!strcmp(message->getName(), "checkResolvableCrash")) { + ASSERT(apMib->bssAccessPointData.stations.find(staMib->address) == apMib->bssAccessPointData.stations.end()); + ASSERT(apMib->findPeerHtState(staMib->address) == nullptr); + ASSERT(staMib->findPeerHtState(apMib->address) == nullptr); + std::cout << "Simplified STA AP-side association and HT peer state removed after resolvable crash.\n"; + } + else if (!strcmp(message->getName(), "checkCrashRestart")) { + ASSERT(staMib->bssStationData.isAssociated); + ASSERT(staMib->bssData.ssid == apMib->bssData.ssid); + ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); + ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); + ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); + std::cout << "Simplified STA association and HT peer state restored after crash restart.\n"; + } + else if (!strcmp(message->getName(), "prepareMissingApCrash")) { + ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); + ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); + ASSERT(staMib->findPeerHtState(apMib->address) != nullptr); + staMib->bssData.bssid = MacAddress("02:00:00:00:00:ff"); + ASSERT(staMib->bssData.bssid != apMib->address); + ASSERT(L3AddressResolver().findHostWithAddress(staMib->bssData.bssid) == nullptr); + std::cout << "Simplified STA recorded BSSID made unresolvable before crash.\n"; + } + else if (!strcmp(message->getName(), "checkMissingApCrash")) { + ASSERT(staMib->bssData.bssid == MacAddress("02:00:00:00:00:ff")); + ASSERT(L3AddressResolver().findHostWithAddress(staMib->bssData.bssid) == nullptr); + ASSERT(staMib->findPeerHtState(apMib->address) == nullptr); + ASSERT(apMib->bssAccessPointData.stations.at(staMib->address) == ieee80211::Ieee80211Mib::ASSOCIATED); + ASSERT(apMib->findPeerHtState(staMib->address) != nullptr); + std::cout << "Simplified STA crash tolerated missing AP lookup and cleared local HT peer state.\n"; + } + else + ASSERT(false); delete message; } }; @@ -120,7 +169,7 @@ network Ieee80211MgmtStaSimplifiedInitializationTestNetwork [General] network = Ieee80211MgmtStaSimplifiedInitializationTestNetwork ned-path = .;../../../../src;../../lib -sim-time-limit = 4us +sim-time-limit = 7us cmdenv-express-mode = true record-vector-results = false record-scalar-results = false @@ -152,6 +201,15 @@ record-scalar-results = false + + + + + + + + + %contains: stdout @@ -161,4 +219,19 @@ Simplified STA wireless identity available during network configuration. Simplified STA HT peer state installed at last initialization stage. %contains: stdout -Simplified STA association and HT peer state restored after restart. +Simplified STA AP-side association and HT peer state removed after shutdown. + +%contains: stdout +Simplified STA association and HT peer state restored after shutdown restart. + +%contains: stdout +Simplified STA AP-side association and HT peer state removed after resolvable crash. + +%contains: stdout +Simplified STA association and HT peer state restored after crash restart. + +%contains: stdout +Simplified STA recorded BSSID made unresolvable before crash. + +%contains: stdout +Simplified STA crash tolerated missing AP lookup and cleared local HT peer state. From 21e75822ca72697d78e2af87df52b95a613c0458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 19 Aug 2026 19:48:23 +0200 Subject: [PATCH 08/10] Fix IEEE 802.11 association lifecycle notifications Mark simplified stations locally disassociated during shutdown and crash before attempting AP-side cleanup, while retaining BSS identity for restart. Emit the AP association notification after a successful association or reassociation commit whenever the station was not already associated with that AP. This reports different-AP reassociation without duplicating same-AP notifications. Extend focused coverage for shutdown and crash state, reassociation transaction classification, committed AID and station state visibility at signal time, and same-AP notification suppression. Tests: debug build; 96/96 unit tests; 3/3 focused module tests; 2/2 focused RTS-on fingerprint tests. --- .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 2 +- .../mgmt/Ieee80211MgmtStaSimplified.cc | 1 + tests/module/Ieee80211MgmtApTimeout_1.test | 96 ++++++++++++++++++- ...0211MgmtStaSimplifiedInitialization_1.test | 3 + tests/unit/Ieee80211MgmtApTransaction_1.test | 19 ++-- 5 files changed, 111 insertions(+), 10 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index 061c6f5ddb0..b437336605a 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -208,7 +208,7 @@ void Ieee80211MgmtAp::receiveSignal(cComponent *source, simsignal_t signalID, cO mib->removePeerHtCapabilities(address); } // Signal delivery is synchronous; observers must see committed station and peer state. - if (responseHeader->getType() == ST_ASSOCIATIONRESPONSE && !wasAssociated) + if (!wasAssociated) sendAssocNotification(address); } else if (exchangeSucceeded && mib->bssAccessPointData.stations[address] == Ieee80211Mib::ASSOCIATED) { diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc index 20c5f97393e..fe0a0ac138e 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc @@ -83,6 +83,7 @@ void Ieee80211MgmtStaSimplified::configureAssociation() void Ieee80211MgmtStaSimplified::stop() { + mib->bssStationData.isAssociated = false; auto apMib = findAccessPointMib(mib->bssData.bssid, false); if (apMib != nullptr) { apMib->bssAccessPointData.stations.erase(mib->address); diff --git a/tests/module/Ieee80211MgmtApTimeout_1.test b/tests/module/Ieee80211MgmtApTimeout_1.test index 80f4b410065..b3ac44860b4 100644 --- a/tests/module/Ieee80211MgmtApTimeout_1.test +++ b/tests/module/Ieee80211MgmtApTimeout_1.test @@ -3,11 +3,19 @@ Verify the AP's association-response timer releases only expired uncommitted association IDs, preserves later replacement transactions, and never releases a committed association ID during a reassociation timeout. Also verify a zero association-response timeout is rejected because no response exchange can -complete before a same-time timeout event. +complete before a same-time timeout event. Verify an acknowledged successful +reassociation response commits an authenticated station before emitting one AP +association notification, without notifying again for same-AP reassociation. %file: TestIeee80211MgmtAp.cc +#include "inet/common/Simsignals.h" +#include "inet/linklayer/ieee80211/mac/contract/IFrameSequenceHandler.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceContext.h" +#include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" namespace inet { namespace ieee80211 { @@ -42,6 +50,42 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp return aid; } + short startSuccessfulReassociation(const MacAddress& address, Ieee80211Mib::BssMemberStatus status) + { + Enter_Method("startSuccessfulReassociation"); + auto& sta = staList[address]; + sta.address = address; + clearPendingAssociation(&sta); + mib->bssAccessPointData.stations[address] = status; + short aid = mib->reserveAssociationId(address); + sta.pendingAssociationSuccessful = true; + sta.pendingAssociationTransactionId = createAssociationTransactionId(); + startAssociationResponseTimeout(&sta); + return aid; + } + + void finishSuccessfulReassociation(const MacAddress& address) + { + auto response = new Packet("ReassocResp-OK"); + auto responseHeader = makeShared(); + responseHeader->setType(ST_REASSOCIATIONRESPONSE); + responseHeader->setReceiverAddress(address); + response->insertAtBack(responseHeader); + response->addTag()->setTransactionId(staList.at(address).pendingAssociationTransactionId); + + FrameSequenceContext context(MacAddress::UNSPECIFIED_ADDRESS, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); + auto transmitStep = new TransmitStep(response, SIMTIME_ZERO); + transmitStep->setCompletion(IFrameSequenceStep::Completion::ACCEPTED); + context.addStep(transmitStep); + auto receiveStep = new ReceiveStep(); + receiveStep->setCompletion(IFrameSequenceStep::Completion::ACCEPTED); + receiveStep->setFrameToReceive(new Packet("ACK", makeShared())); + context.addStep(receiveStep); + + receiveSignal(this, IFrameSequenceHandler::frameSequenceFinishedSignal, &context, nullptr); + delete response; + } + bool hasPendingAssociation(const MacAddress& address) const { auto it = staList.find(address); @@ -50,21 +94,47 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp 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(); } short getCommittedAssociationId(const MacAddress& address) const { return mib->bssAccessPointData.associationIds.at(address); } + Ieee80211Mib::BssMemberStatus getStationStatus(const MacAddress& address) const { return mib->bssAccessPointData.stations.at(address); } simtime_t getScheduledTimeout() const { return associationResponseTimeoutTimer->getArrivalTime(); } }; Define_Module(TestIeee80211MgmtAp); -class Ieee80211MgmtApTimeoutTest : public cSimpleModule +class Ieee80211MgmtApTimeoutTest : public cSimpleModule, public cListener { + using cListener::finish; + + protected: + TestIeee80211MgmtAp *mgmt = nullptr; + MacAddress expectedAssociatedAddress; + short expectedAssociationId = 0; + int associationNotifications = 0; + public: Ieee80211MgmtApTimeoutTest() : cSimpleModule(65536) {} protected: + virtual void initialize() override + { + mgmt = check_and_cast(getModuleByPath("^.ap.wlan[0].mgmt")); + mgmt->subscribe(l2ApAssociatedSignal, this); + } + + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *value, cObject *details) override + { + ASSERT(source == mgmt); + ASSERT(signalID == l2ApAssociatedSignal); + const auto& notification = check_and_cast(value); + ASSERT(notification->getStaAddress() == expectedAssociatedAddress); + ASSERT(mgmt->getStationStatus(expectedAssociatedAddress) == Ieee80211Mib::ASSOCIATED); + ASSERT(mgmt->getCommittedAssociationId(expectedAssociatedAddress) == expectedAssociationId); + associationNotifications++; + } + virtual void activity() override { - auto mgmt = check_and_cast(getModuleByPath("^.ap.wlan[0].mgmt")); MacAddress replacement("02:00:00:00:00:01"); MacAddress expiring("02:00:00:00:00:02"); MacAddress committed("02:00:00:00:00:03"); @@ -95,7 +165,24 @@ class Ieee80211MgmtApTimeoutTest : public cSimpleModule ASSERT(!mgmt->hasPendingAssociation(replacement)); ASSERT(mgmt->reserveAssociationId(finallyReused) == 1); + expectedAssociatedAddress = MacAddress("02:00:00:00:00:06"); + expectedAssociationId = mgmt->startSuccessfulReassociation(expectedAssociatedAddress, Ieee80211Mib::AUTHENTICATED); + ASSERT(mgmt->getStationStatus(expectedAssociatedAddress) == Ieee80211Mib::AUTHENTICATED); + ASSERT(!mgmt->hasCommittedAssociationId(expectedAssociatedAddress)); + mgmt->finishSuccessfulReassociation(expectedAssociatedAddress); + ASSERT(associationNotifications == 1); + ASSERT(!mgmt->hasPendingAssociation(expectedAssociatedAddress)); + + short sameApAid = mgmt->startSuccessfulReassociation(expectedAssociatedAddress, Ieee80211Mib::ASSOCIATED); + ASSERT(sameApAid == expectedAssociationId); + ASSERT(mgmt->getStationStatus(expectedAssociatedAddress) == Ieee80211Mib::ASSOCIATED); + ASSERT(mgmt->getCommittedAssociationId(expectedAssociatedAddress) == expectedAssociationId); + mgmt->finishSuccessfulReassociation(expectedAssociatedAddress); + ASSERT(associationNotifications == 1); + ASSERT(!mgmt->hasPendingAssociation(expectedAssociatedAddress)); + std::cout << "AP association response timeout lifecycle verified.\n"; + std::cout << "AP reassociation notification commit ordering verified.\n"; } }; @@ -170,6 +257,9 @@ record-scalar-results = false %contains: stdout AP association response timeout lifecycle verified. +%contains: stdout +AP reassociation notification commit ordering verified. + %postrun-command: sh checkZeroTimeout.sh %contains: postrun-command(1).err diff --git a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test index 22308556a02..5e749d64d47 100644 --- a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test +++ b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test @@ -70,6 +70,7 @@ class TestInitializationObserver : public cSimpleModule auto apMib = check_and_cast(apInterface->getSubmodule("mib")); auto staMib = check_and_cast(staInterface->getSubmodule("mib")); if (!strcmp(message->getName(), "checkShutdown")) { + ASSERT(!staMib->bssStationData.isAssociated); ASSERT(apMib->bssAccessPointData.stations.find(staMib->address) == apMib->bssAccessPointData.stations.end()); ASSERT(apMib->findPeerHtState(staMib->address) == nullptr); ASSERT(staMib->findPeerHtState(apMib->address) == nullptr); @@ -84,6 +85,7 @@ class TestInitializationObserver : public cSimpleModule std::cout << "Simplified STA association and HT peer state restored after shutdown restart.\n"; } else if (!strcmp(message->getName(), "checkResolvableCrash")) { + ASSERT(!staMib->bssStationData.isAssociated); ASSERT(apMib->bssAccessPointData.stations.find(staMib->address) == apMib->bssAccessPointData.stations.end()); ASSERT(apMib->findPeerHtState(staMib->address) == nullptr); ASSERT(staMib->findPeerHtState(apMib->address) == nullptr); @@ -107,6 +109,7 @@ class TestInitializationObserver : public cSimpleModule std::cout << "Simplified STA recorded BSSID made unresolvable before crash.\n"; } else if (!strcmp(message->getName(), "checkMissingApCrash")) { + ASSERT(!staMib->bssStationData.isAssociated); ASSERT(staMib->bssData.bssid == MacAddress("02:00:00:00:00:ff")); ASSERT(L3AddressResolver().findHostWithAddress(staMib->bssData.bssid) == nullptr); ASSERT(staMib->findPeerHtState(apMib->address) == nullptr); diff --git a/tests/unit/Ieee80211MgmtApTransaction_1.test b/tests/unit/Ieee80211MgmtApTransaction_1.test index 3883b4026f8..5f45c38fc1d 100644 --- a/tests/unit/Ieee80211MgmtApTransaction_1.test +++ b/tests/unit/Ieee80211MgmtApTransaction_1.test @@ -1,6 +1,7 @@ %description: -Verify deterministic AP association-response transaction matching and -completion and timeout decisions, including RTS-protected responses. +Verify deterministic AP association- and reassociation-response transaction +matching and completion and timeout decisions, including RTS-protected +responses. %includes: #include "inet/linklayer/ieee80211/mac/framesequence/FrameSequenceStep.h" @@ -25,11 +26,12 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp using Ieee80211MgmtAp::isAssociationResponseDecisionPoint; }; -static Packet *makeAssociationResponse(uint64_t transactionId, bool moreFragments, bool includeTag = true) +static Packet *makeAssociationResponse(uint64_t transactionId, bool moreFragments, bool includeTag = true, + Ieee80211FrameType subtype = ST_ASSOCIATIONRESPONSE) { auto packet = new Packet("AssocResp-OK"); auto header = makeShared(); - header->setType(ST_ASSOCIATIONRESPONSE); + header->setType(subtype); header->setMoreFragments(moreFragments); packet->insertAtBack(header); if (includeTag) @@ -71,6 +73,10 @@ ackStep.setFrameToReceive(new Packet("ACK", makeShared())); ASSERT(TestIeee80211MgmtAp::isAssociationResponseDecisionPoint(&ordinaryStep, &ackStep)); ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 42, true, false) == Disposition::COMPLETE); +auto reassociationResponse = makeAssociationResponse(42, false, true, ST_REASSOCIATIONRESPONSE); +ASSERT(TestIeee80211MgmtAp::getAssociationResponseHeader(reassociationResponse)->getType() == ST_REASSOCIATIONRESPONSE); +ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(reassociationResponse, 42, true, false) == Disposition::COMPLETE); + ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 41, true, false) == Disposition::IGNORE); ASSERT(TestIeee80211MgmtAp::getAssociationResponseDisposition(finalResponse, 0, true, false) == Disposition::IGNORE); @@ -106,9 +112,10 @@ delete nonFinalResponse; delete untaggedResponse; delete zeroTokenResponse; delete aggregate; +delete reassociationResponse; delete finalResponse; -EV << "AP association transaction and timeout decisions verified.\n"; +EV << "AP association and reassociation transaction and timeout decisions verified.\n"; %contains: stdout -AP association transaction and timeout decisions verified. +AP association and reassociation transaction and timeout decisions verified. From ee71c4acbd1022fa341da84a9b05a264b16dbc1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Wed, 19 Aug 2026 20:53:22 +0200 Subject: [PATCH 09/10] Fix reassociation transaction teardown --- .../ieee80211/mgmt/Ieee80211MgmtSta.cc | 37 +++++++++---------- .../ieee80211/mgmt/Ieee80211MgmtSta.h | 5 ++- .../Ieee80211MgmtStaPrimitiveDispatch_1.test | 29 ++++++++++++++- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc index f20c955b2ef..33e72ad800f 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc @@ -208,6 +208,8 @@ Ieee80211MgmtSta::ApInfo *Ieee80211MgmtSta::lookupAP(const MacAddress& address) void Ieee80211MgmtSta::clearAPList() { + cancelPendingAssociation(); + for (auto& elem : apList) if (elem.authTimeoutMsg) cancelAndDelete(elem.authTimeoutMsg); @@ -215,6 +217,13 @@ void Ieee80211MgmtSta::clearAPList() apList.clear(); } +void Ieee80211MgmtSta::cancelPendingAssociation() +{ + cancelAndDelete(assocTimeoutMsg); + assocTimeoutMsg = nullptr; + reassociationInProgress = false; +} + void Ieee80211MgmtSta::changeChannel(int channelNum) { EV << "Tuning to channel #" << channelNum << "\n"; @@ -333,14 +342,8 @@ void Ieee80211MgmtSta::processScanCommand(Ieee80211Prim_ScanRequest *ctrl) if (isScanning) throw cRuntimeError("processScanCommand: scanning already in progress"); - if (mib->bssStationData.isAssociated) { + if (mib->bssStationData.isAssociated) disassociate(); - } - else if (assocTimeoutMsg) { - EV << "Cancelling ongoing association process\n"; - cancelAndDelete(assocTimeoutMsg); - assocTimeoutMsg = nullptr; - } // clear existing AP list (and cancel any pending authentications) -- we want to start with a clean page clearAPList(); @@ -453,6 +456,8 @@ void Ieee80211MgmtSta::processDeauthenticateCommand(Ieee80211Prim_Deauthenticate if (mib->bssStationData.isAssociated && assocAP.address == address) disassociate(); + else if (assocTimeoutMsg && assocTimeoutMsg->getContextPointer() == ap) + cancelPendingAssociation(); if (ap->isAuthenticated) ap->isAuthenticated = false; @@ -500,11 +505,8 @@ void Ieee80211MgmtSta::processDisassociateCommand(Ieee80211Prim_DisassociateRequ if (mib->bssStationData.isAssociated && address == assocAP.address) { disassociate(); } - else if (assocTimeoutMsg) { - // pending association - cancelAndDelete(assocTimeoutMsg); - assocTimeoutMsg = nullptr; - } + else + cancelPendingAssociation(); // create and send disassociation request const auto& body = makeShared(); @@ -516,6 +518,7 @@ void Ieee80211MgmtSta::disassociate() { EV << "Disassociating from AP address=" << assocAP.address << "\n"; ASSERT(mib->bssStationData.isAssociated); + cancelPendingAssociation(); mib->bssStationData.isAssociated = false; mib->removePeerHtCapabilities(assocAP.address); cancelAndDelete(assocAP.beaconTimeoutMsg); @@ -706,9 +709,7 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrgetAddress3(); // source address - if (assocTimeoutMsg) { - // pending association - cancelAndDelete(assocTimeoutMsg); - assocTimeoutMsg = nullptr; - } + cancelPendingAssociation(); if (!mib->bssStationData.isAssociated || address != assocAP.address) { EV << "Not associated with that AP -- ignoring frame\n"; delete packet; diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h index 2447a615aa9..8fad544e23d 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.h @@ -124,9 +124,12 @@ class INET_API Ieee80211MgmtSta : public Ieee80211MgmtBase /** Utility function: looks up AP in our AP list. Returns nullptr if not found. */ virtual ApInfo *lookupAP(const MacAddress& address); - /** Utility function: clear the AP list, and cancel any pending authentications. */ + /** Utility function: clear the AP list and cancel pending association and authentication transactions. */ virtual void clearAPList(); + /** Utility function: cancel any pending association or reassociation. */ + virtual void cancelPendingAssociation(); + /** Utility function: switches to the given radio channel. */ virtual void changeChannel(int channelNum); diff --git a/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test b/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test index da47b7814d8..7928f5389fb 100644 --- a/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test +++ b/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test @@ -2,7 +2,7 @@ Verify that association and reassociation management primitives are dispatched to their most-derived handlers, including the distinct reassociation confirm primitive emitted by the station agent. Verify association timeout ownership, -confirm typing, restart eligibility, and late-response rejection. +confirm typing, AP-list cleanup, restart eligibility, and late-response rejection. %includes: #include "inet/linklayer/ieee80211/mgmt/Ieee80211AgentSta.h" @@ -73,6 +73,17 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta reassociationInProgress = reassociation; } + void prepareAssociationTimeoutInApList(const MacAddress& address, bool reassociation) + { + apList.push_back(ApInfo()); + apList.back().address = address; + prepareAssociationTimeout(&apList.back(), reassociation); + } + + void clearKnownAccessPoints() { clearAPList(); } + void abandonPendingAssociation() { cancelPendingAssociation(); } + size_t getKnownAccessPointCount() const { return apList.size(); } + void deliverAssociationTimeout() { ASSERT(assocTimeoutMsg != nullptr); @@ -203,6 +214,22 @@ mgmt.deliverLateAssociationResponse(true); ASSERT(mgmt.associateConfirms == 1); ASSERT(mgmt.reassociateConfirms == 1); +mgmt.prepareAssociationTimeoutInApList(otherApAddress, true); +ASSERT(mgmt.getKnownAccessPointCount() == 1); +ASSERT(!mgmt.canStartAnotherAssociation()); +ASSERT(mgmt.isReassociationInProgress()); +mgmt.clearKnownAccessPoints(); +ASSERT(mgmt.getKnownAccessPointCount() == 0); +ASSERT(mgmt.canStartAnotherAssociation()); +ASSERT(!mgmt.isReassociationInProgress()); + +mgmt.prepareAssociationTimeoutInApList(otherApAddress, true); +ASSERT(!mgmt.canStartAnotherAssociation()); +mgmt.abandonPendingAssociation(); +ASSERT(mgmt.canStartAnotherAssociation()); +ASSERT(!mgmt.isReassociationInProgress()); +mgmt.clearKnownAccessPoints(); + EV << "Association and reassociation primitive dispatch and timeout ownership verified.\n"; %contains: stdout From 9757d21efb2d3e4a40798996a7d2e8b754029fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Thu, 20 Aug 2026 21:02:59 +0200 Subject: [PATCH 10/10] Fix IEEE 802.11 association transaction handling --- .../ieee80211/mgmt/Ieee80211MgmtAp.cc | 4 +- .../ieee80211/mgmt/Ieee80211MgmtSta.cc | 10 + .../bitlevel/Ieee80211LayeredOfdmReceiver.cc | 9 + tests/module/Ieee80211MgmtApTimeout_1.test | 85 +++++++- .../Ieee80211PacketDomainTagBoundary_1.test | 196 ++++++++++++++++++ .../Ieee80211MgmtStaPrimitiveDispatch_1.test | 32 ++- 6 files changed, 325 insertions(+), 11 deletions(-) create mode 100644 tests/module/Ieee80211PacketDomainTagBoundary_1.test diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc index b437336605a..015d4a94996 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -395,6 +395,7 @@ void Ieee80211MgmtAp::handleDeauthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED) { sendDisAssocNotification(sta->address); @@ -402,7 +403,6 @@ void Ieee80211MgmtAp::handleDeauthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] = Ieee80211Mib::NOT_AUTHENTICATED; sta->authSeqExpected = 1; - clearPendingAssociation(sta); mib->removePeerHtCapabilities(sta->address); } } @@ -507,12 +507,12 @@ void Ieee80211MgmtAp::handleDisassociationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED) { sendDisAssocNotification(sta->address); mib->releaseAssociationId(sta->address); } mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::AUTHENTICATED; - clearPendingAssociation(sta); mib->removePeerHtCapabilities(sta->address); } } diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc index 33e72ad800f..c2ba7401517 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtSta.cc @@ -678,6 +678,16 @@ void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const PtrgetTransmitterAddress(); ApInfo *ap = static_cast(assocTimeoutMsg->getContextPointer()); if (ap == nullptr || ap->address != address) { diff --git a/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmReceiver.cc b/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmReceiver.cc index 55173ee9b5b..06cb15bab87 100644 --- a/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmReceiver.cc +++ b/src/inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmReceiver.cc @@ -7,6 +7,7 @@ #include "inet/physicallayer/wireless/ieee80211/bitlevel/Ieee80211LayeredOfdmReceiver.h" +#include "inet/common/ProtocolTag_m.h" #include "inet/common/packet/chunk/BytesChunk.h" #include "inet/physicallayer/wireless/common/analogmodel/dimensional/DimensionalReceptionAnalogModel.h" #include "inet/physicallayer/wireless/common/analogmodel/dimensional/DimensionalMediumAnalogModel.h" @@ -411,6 +412,14 @@ const IReceptionResult *Ieee80211LayeredOfdmReceiver::computeReceptionResult(con } // add indications auto packet = const_cast(packetModel->getPacket()); + // Packet-domain error models duplicate the transmitted packet, including + // sender-local tags. Reception results must expose only metadata that is + // authoritative at this boundary, just like ReceiverBase does for flat + // packet-level radios. + auto transmittedPacket = transmission->getPacket(); + auto transmittedProtocolTag = transmittedPacket->getTag(); + packet->clearTags(); + packet->addTag()->setProtocol(transmittedProtocolTag->getProtocol()); auto snirInd = packet->addTagIfAbsent(); snirInd->setMinimumSnir(snir->getMin()); snirInd->setMaximumSnir(snir->getMax()); diff --git a/tests/module/Ieee80211MgmtApTimeout_1.test b/tests/module/Ieee80211MgmtApTimeout_1.test index b3ac44860b4..390c0b19f47 100644 --- a/tests/module/Ieee80211MgmtApTimeout_1.test +++ b/tests/module/Ieee80211MgmtApTimeout_1.test @@ -64,14 +64,31 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp return aid; } - void finishSuccessfulReassociation(const MacAddress& address) + short startPendingAssociatedReassociation(const MacAddress& address) { + Enter_Method("startPendingAssociatedReassociation"); + auto& sta = staList[address]; + sta.address = address; + clearPendingAssociation(&sta); + short aid = mib->allocateAssociationId(address); + mib->bssAccessPointData.stations[address] = Ieee80211Mib::ASSOCIATED; + ASSERT(mib->reserveAssociationId(address) == aid); + sta.pendingAssociationSuccessful = true; + sta.pendingAssociationTransactionId = createAssociationTransactionId(); + startAssociationResponseTimeout(&sta); + return aid; + } + + void finishSuccessfulReassociation(const MacAddress& address, uint64_t transactionId = 0) + { + if (transactionId == 0) + transactionId = staList.at(address).pendingAssociationTransactionId; auto response = new Packet("ReassocResp-OK"); auto responseHeader = makeShared(); responseHeader->setType(ST_REASSOCIATIONRESPONSE); responseHeader->setReceiverAddress(address); response->insertAtBack(responseHeader); - response->addTag()->setTransactionId(staList.at(address).pendingAssociationTransactionId); + response->addTag()->setTransactionId(transactionId); FrameSequenceContext context(MacAddress::UNSPECIFIED_ADDRESS, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr); auto transmitStep = new TransmitStep(response, SIMTIME_ZERO); @@ -86,12 +103,32 @@ class TestIeee80211MgmtAp : public Ieee80211MgmtAp delete response; } + void deauthenticate(const MacAddress& address) + { + Enter_Method("deauthenticate"); + auto packet = new Packet("Deauth"); + auto header = makeShared(); + header->setTransmitterAddress(address); + handleDeauthenticationFrame(packet, header); + } + + void disassociate(const MacAddress& address) + { + Enter_Method("disassociate"); + auto packet = new Packet("Disassoc"); + 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(); } @@ -109,8 +146,10 @@ class Ieee80211MgmtApTimeoutTest : public cSimpleModule, public cListener protected: TestIeee80211MgmtAp *mgmt = nullptr; MacAddress expectedAssociatedAddress; + MacAddress expectedDisassociatedAddress; short expectedAssociationId = 0; int associationNotifications = 0; + int disassociationNotifications = 0; public: Ieee80211MgmtApTimeoutTest() : cSimpleModule(65536) {} @@ -120,17 +159,25 @@ class Ieee80211MgmtApTimeoutTest : public cSimpleModule, public cListener { mgmt = check_and_cast(getModuleByPath("^.ap.wlan[0].mgmt")); mgmt->subscribe(l2ApAssociatedSignal, this); + mgmt->subscribe(l2ApDisassociatedSignal, this); } virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *value, cObject *details) override { ASSERT(source == mgmt); - ASSERT(signalID == l2ApAssociatedSignal); const auto& notification = check_and_cast(value); - ASSERT(notification->getStaAddress() == expectedAssociatedAddress); - ASSERT(mgmt->getStationStatus(expectedAssociatedAddress) == Ieee80211Mib::ASSOCIATED); - ASSERT(mgmt->getCommittedAssociationId(expectedAssociatedAddress) == expectedAssociationId); - associationNotifications++; + if (signalID == l2ApAssociatedSignal) { + ASSERT(notification->getStaAddress() == expectedAssociatedAddress); + ASSERT(mgmt->getStationStatus(expectedAssociatedAddress) == Ieee80211Mib::ASSOCIATED); + ASSERT(mgmt->getCommittedAssociationId(expectedAssociatedAddress) == expectedAssociationId); + associationNotifications++; + } + else { + ASSERT(signalID == l2ApDisassociatedSignal); + ASSERT(notification->getStaAddress() == expectedDisassociatedAddress); + ASSERT(!mgmt->hasPendingAssociation(expectedDisassociatedAddress)); + disassociationNotifications++; + } } virtual void activity() override @@ -181,6 +228,30 @@ class Ieee80211MgmtApTimeoutTest : public cSimpleModule, public cListener ASSERT(associationNotifications == 1); ASSERT(!mgmt->hasPendingAssociation(expectedAssociatedAddress)); + MacAddress deauthenticated("02:00:00:00:00:07"); + mgmt->startPendingAssociatedReassociation(deauthenticated); + uint64_t staleDeauthenticationTransaction = mgmt->getPendingAssociationTransactionId(deauthenticated); + expectedDisassociatedAddress = deauthenticated; + mgmt->deauthenticate(deauthenticated); + ASSERT(disassociationNotifications == 1); + ASSERT(!mgmt->hasPendingAssociation(deauthenticated)); + ASSERT(!mgmt->hasCommittedAssociationId(deauthenticated)); + ASSERT(mgmt->getStationStatus(deauthenticated) == Ieee80211Mib::NOT_AUTHENTICATED); + mgmt->finishSuccessfulReassociation(deauthenticated, staleDeauthenticationTransaction); + ASSERT(!mgmt->hasCommittedAssociationId(deauthenticated)); + + MacAddress disassociated("02:00:00:00:00:08"); + mgmt->startPendingAssociatedReassociation(disassociated); + uint64_t staleDisassociationTransaction = mgmt->getPendingAssociationTransactionId(disassociated); + expectedDisassociatedAddress = disassociated; + mgmt->disassociate(disassociated); + ASSERT(disassociationNotifications == 2); + ASSERT(!mgmt->hasPendingAssociation(disassociated)); + ASSERT(!mgmt->hasCommittedAssociationId(disassociated)); + ASSERT(mgmt->getStationStatus(disassociated) == Ieee80211Mib::AUTHENTICATED); + mgmt->finishSuccessfulReassociation(disassociated, staleDisassociationTransaction); + ASSERT(!mgmt->hasCommittedAssociationId(disassociated)); + std::cout << "AP association response timeout lifecycle verified.\n"; std::cout << "AP reassociation notification commit ordering verified.\n"; } diff --git a/tests/module/Ieee80211PacketDomainTagBoundary_1.test b/tests/module/Ieee80211PacketDomainTagBoundary_1.test new file mode 100644 index 00000000000..aaea838c6a4 --- /dev/null +++ b/tests/module/Ieee80211PacketDomainTagBoundary_1.test @@ -0,0 +1,196 @@ +%description: +Verify that a packet-domain IEEE 802.11 OFDM receiver removes sender-local +packet tags while retaining the protocol and receiver indication tags required +by the upper layers. + +%file: TestIeee80211PacketDomainTagBoundary.cc + +#include "inet/applications/base/ApplicationPacket_m.h" +#include "inet/applications/udpapp/UdpBasicApp.h" +#include "inet/applications/udpapp/UdpSink.h" +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/Simsignals.h" +#include "inet/common/TimeTag_m.h" +#include "inet/linklayer/ieee80211/mgmt/Ieee80211MgmtTransactionTag_m.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/SignalTag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" + +using namespace inet; +using namespace inet::ieee80211; + +class TaggedUdpBasicApp : public UdpBasicApp +{ + protected: + virtual void sendPacket() override + { + std::ostringstream str; + str << packetName << "-" << numSent; + auto packet = new Packet(str.str().c_str()); + const auto& payload = makeShared(); + payload->setChunkLength(B(par("messageLength"))); + payload->setSequenceNumber(numSent); + payload->addTag()->setCreationTime(simTime()); + packet->insertAtBack(payload); + packet->addTag()->setTransactionId(0x1234); + L3Address destAddr = chooseDestAddr(); + emit(packetSentSignal, packet); + socket.sendTo(packet, destAddr, destPort); + numSent++; + } +}; + +Define_Module(TaggedUdpBasicApp); + +class CheckingUdpSink : public UdpSink +{ + protected: + virtual void processPacket(Packet *packet) override + { + ASSERT(packet->findTag() == nullptr); + UdpSink::processPacket(packet); + } +}; + +Define_Module(CheckingUdpSink); + +class Ieee80211PacketDomainTagBoundaryTest : public cSimpleModule, public cListener +{ + public: + Ieee80211PacketDomainTagBoundaryTest() : cSimpleModule(65536) {} + + protected: + cModule *sourceRadio = nullptr; + cModule *destinationRadio = nullptr; + int sourcePacketsWithTransactionTag = 0; + int destinationPackets = 0; + + virtual void initialize() override + { + sourceRadio = getModuleByPath("^.sourceHost.wlan[0].radio"); + destinationRadio = getModuleByPath("^.destinationHost.wlan[0].radio"); + sourceRadio->subscribe(packetReceivedFromUpperSignal, this); + destinationRadio->subscribe(packetSentToUpperSignal, this); + } + + virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *value, cObject *details) override + { + auto packet = check_and_cast(value); + if (source == sourceRadio && signalID == packetReceivedFromUpperSignal) { + if (packet->findTag() != nullptr) + sourcePacketsWithTransactionTag++; + } + else if (source == destinationRadio && signalID == packetSentToUpperSignal) { + ASSERT(packet->findTag() == nullptr); + ASSERT(packet->findTag() != nullptr); + ASSERT(packet->findTag() != nullptr); + ASSERT(packet->findTag() != nullptr); + ASSERT(packet->findTag() != nullptr); + ASSERT(packet->findTag() != nullptr); + destinationPackets++; + } + } + + virtual void activity() override + { + wait(SimTime(100, SIMTIME_MS)); + ASSERT(sourcePacketsWithTransactionTag > 0); + ASSERT(destinationPackets > 0); + std::cout << "Packet-domain receiver tag boundary verified.\n"; + } +}; + +Define_Module(Ieee80211PacketDomainTagBoundaryTest); + +%file: test.ned + +import inet.applications.udpapp.UdpBasicApp; +import inet.applications.udpapp.UdpSink; +import inet.common.SimpleModule; +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.AdhocHost; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211DimensionalRadioMedium; + +simple TaggedUdpBasicApp extends UdpBasicApp +{ + parameters: + @class(::TaggedUdpBasicApp); +} + +simple CheckingUdpSink extends UdpSink +{ + parameters: + @class(::CheckingUdpSink); +} + +simple Ieee80211PacketDomainTagBoundaryTest extends SimpleModule +{ + parameters: + @class(::Ieee80211PacketDomainTagBoundaryTest); +} + +network Ieee80211PacketDomainTagBoundaryTestNetwork +{ + submodules: + radioMedium: Ieee80211DimensionalRadioMedium; + configurator: Ipv4NetworkConfigurator; + sourceHost: AdhocHost; + destinationHost: AdhocHost; + test: Ieee80211PacketDomainTagBoundaryTest; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211PacketDomainTagBoundaryTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 200ms +cmdenv-express-mode = true +record-vector-results = false +record-scalar-results = false + +**.arp.typename = "GlobalArp" +**.checksumMode = "computed" +**.fcsMode = "computed" +**.mobility.initFromDisplayString = false +**.mobility.typename = "StationaryMobility" +*.sourceHost.mobility.initialX = 0m +*.sourceHost.mobility.initialY = 0m +*.destinationHost.mobility.initialX = 100m +*.destinationHost.mobility.initialY = 0m +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMinZ = 0m +**.constraintAreaMaxX = 200m +**.constraintAreaMaxY = 100m +**.constraintAreaMaxZ = 0m + +*.sourceHost.numApps = 1 +*.sourceHost.app[0].typename = "TaggedUdpBasicApp" +*.sourceHost.app[0].destAddresses = "destinationHost" +*.sourceHost.app[0].destPort = 5000 +*.sourceHost.app[0].messageLength = 100B +*.sourceHost.app[0].sendInterval = 1s +*.sourceHost.app[0].startTime = 10ms + +*.destinationHost.numApps = 1 +*.destinationHost.app[0].typename = "CheckingUdpSink" +*.destinationHost.app[0].localPort = 5000 + +*.sourceHost.wlan[*].typename = "Ieee80211Interface" +*.destinationHost.wlan[*].typename = "Ieee80211Interface" +**.wlan[*].radio.typename = "Ieee80211OfdmRadio" +**.wlan[*].radio.transmitter.power = 0.1mW +**.wlan[*].radio.receiver.sensitivity = -109dBm +**.wlan[*].radio.receiver.snirThreshold = 0.1dB +**.wlan[*].radio.receiver.energyDetection = -90dBm +**.wlan[*].radio.bandwidth = 20MHz +**.wlan[*].radio.centerFrequency = 2.412GHz +**.wlan[*].radio.receiver.channelSpacing = 5MHz +**.wlan[*].radio.receiver.levelOfDetail = "packet" +**.wlan[*].radio.receiver.errorModel.snirOffset = -2dB +**.wlan[*].bitrate = 6Mbps +**.wlan[*].mac.**.responseAckFrameBitrate = 6Mbps +**.wlan[*].mac.**.*Retry* = 0 + +%contains: stdout +Packet-domain receiver tag boundary verified. diff --git a/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test b/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test index 7928f5389fb..b03c5153b91 100644 --- a/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test +++ b/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test @@ -108,6 +108,15 @@ class TestIeee80211MgmtSta : public Ieee80211MgmtSta processAssociationResponse(packet, header, reassociation); } + void deliverPendingAssociationResponse(bool reassociation) + { + auto packet = new Packet("PendingAssocResp"); + auto header = makeShared(); + auto ap = static_cast(assocTimeoutMsg->getContextPointer()); + header->setTransmitterAddress(ap->address); + processAssociationResponse(packet, header, reassociation); + } + protected: virtual void processAssociateCommand(Ieee80211Prim_AssociateRequest *ctrl) override { @@ -201,17 +210,36 @@ mgmt.deliverLateAssociationResponse(false); ASSERT(mgmt.associateConfirms == 1); ASSERT(mgmt.reassociateConfirms == 0); +// IEEE Std 802.11-2024, 11.3.5.2 and 11.3.5.4 correlate the response +// procedure with the corresponding association or reassociation request. +mgmt.prepareAssociationTimeout(&timeoutAp, false); +mgmt.deliverPendingAssociationResponse(true); +ASSERT(!mgmt.canStartAnotherAssociation()); +ASSERT(!mgmt.isReassociationInProgress()); +ASSERT(mgmt.associateConfirms == 1); +ASSERT(mgmt.reassociateConfirms == 0); +mgmt.deliverAssociationTimeout(); +ASSERT(mgmt.canStartAnotherAssociation()); +ASSERT(mgmt.associateConfirms == 2); +ASSERT(mgmt.reassociateConfirms == 0); +ASSERT(mgmt.lastAssociationResult == PRC_TIMEOUT); + mgmt.prepareAssociationTimeout(&timeoutAp, true); +mgmt.deliverPendingAssociationResponse(false); +ASSERT(!mgmt.canStartAnotherAssociation()); +ASSERT(mgmt.isReassociationInProgress()); +ASSERT(mgmt.associateConfirms == 2); +ASSERT(mgmt.reassociateConfirms == 0); mgmt.deliverAssociationTimeout(); ASSERT(mgmt.canStartAnotherAssociation()); ASSERT(!mgmt.isReassociationInProgress()); -ASSERT(mgmt.associateConfirms == 1); +ASSERT(mgmt.associateConfirms == 2); ASSERT(mgmt.reassociateConfirms == 1); ASSERT(mgmt.lastReassociationResult == PRC_TIMEOUT); ASSERT(mgmt.reassociationFailures == 1); mgmt.deliverLateAssociationResponse(true); -ASSERT(mgmt.associateConfirms == 1); +ASSERT(mgmt.associateConfirms == 2); ASSERT(mgmt.reassociateConfirms == 1); mgmt.prepareAssociationTimeoutInApList(otherApAddress, true);