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/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/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..015d4a94996 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.cc @@ -14,9 +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" @@ -35,9 +38,62 @@ 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(); +} + +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; + const auto& responseHeader = getAssociationResponseHeader(responseFrame); + if (responseHeader == nullptr) + 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); + cancelAndDelete(associationResponseTimeoutTimer); } void Ieee80211MgmtAp::initialize(int stage) @@ -48,6 +104,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 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); @@ -55,6 +114,7 @@ void Ieee80211MgmtAp::initialize(int stage) WATCH(ssid); WATCH(channelNumber); WATCH(beaconInterval); + WATCH(associationResponseTimeout); WATCH(numAuthSteps); WATCH(staList); @@ -67,6 +127,7 @@ void Ieee80211MgmtAp::initialize(int stage) // start beacon timer (randomize startup time) beaconTimer = new cMessage("beaconTimer"); + associationResponseTimeoutTimer = new cMessage("associationResponseTimeoutTimer"); } } @@ -76,6 +137,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()); } @@ -93,6 +162,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; } } @@ -102,19 +172,56 @@ 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()); - if (transmitStep && receiveStep && - transmitStep->getCompletion() == IFrameSequenceStep::Completion::ACCEPTED && - receiveStep->getCompletion() == IFrameSequenceStep::Completion::ACCEPTED) { - 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; + 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); + 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 && + receivedHeader != nullptr && receivedHeader->getType() == ST_ACK; + bool retryPending = false; + if (!exchangeSucceeded) { + auto inProgressFrames = context->getInProgressFrames(); + for (int i = 0; i < inProgressFrames->getLength(); i++) + retryPending |= inProgressFrames->getFrames(i) == responseFrame; + } + 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 (!wasAssociated) + sendAssocNotification(address); + } + 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) 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. + sendDisAssocNotification(address); + } + clearPendingAssociation(&sta->second); } } } @@ -130,15 +237,66 @@ Ieee80211MgmtAp::StaInfo *Ieee80211MgmtAp::lookupSenderSTA(const Ptrsecond); } -void 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); } +uint64_t Ieee80211MgmtAp::createAssociationTransactionId() +{ + if (++nextAssociationTransactionId == 0) + ++nextAssociationTransactionId; + 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() { EV << "Sending beacon\n"; @@ -147,7 +305,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); } @@ -166,6 +326,7 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[staAddress] = Ieee80211Mib::NOT_AUTHENTICATED; sta->authSeqExpected = 1; } + 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 @@ -180,6 +341,7 @@ void Ieee80211MgmtAp::handleAuthenticationFrame(Packet *packet, const PtrreleaseAssociationId(sta->address); } mib->bssAccessPointData.stations[sta->address] = Ieee80211Mib::NOT_AUTHENTICATED; + mib->removePeerHtCapabilities(sta->address); sta->authSeqExpected = 1; } @@ -233,6 +395,7 @@ void Ieee80211MgmtAp::handleDeauthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] == Ieee80211Mib::ASSOCIATED) { sendDisAssocNotification(sta->address); @@ -240,6 +403,7 @@ void Ieee80211MgmtAp::handleDeauthenticationFrame(Packet *packet, const PtrbssAccessPointData.stations[sta->address] = Ieee80211Mib::NOT_AUTHENTICATED; sta->authSeqExpected = 1; + mib->removePeerHtCapabilities(sta->address); } } @@ -249,6 +413,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(); @@ -258,15 +424,29 @@ 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); + short associationId = basicHtMcsSupported ? mib->reserveAssociationId(sta->address) : 0; + body->setAid(associationId); + sta->pendingAssociationSuccessful = basicHtMcsSupported; + sta->pendingAssociationTransactionId = createAssociationTransactionId(); 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)); + startAssociationResponseTimeout(sta); + sendManagementFrame(basicHtMcsSupported ? "AssocResp-OK" : "AssocResp-UnsupportedHtMcs", body, ST_ASSOCIATIONRESPONSE, sta->address, sta->pendingAssociationTransactionId); } void Ieee80211MgmtAp::handleAssociationResponseFrame(Packet *packet, const Ptr& header) @@ -280,6 +460,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(); @@ -289,15 +471,29 @@ 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); + short associationId = basicHtMcsSupported ? mib->reserveAssociationId(sta->address) : 0; + body->setAid(associationId); + sta->pendingAssociationSuccessful = basicHtMcsSupported; + sta->pendingAssociationTransactionId = createAssociationTransactionId(); 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)); + startAssociationResponseTimeout(sta); + sendManagementFrame(basicHtMcsSupported ? "ReassocResp-OK" : "ReassocResp-UnsupportedHtMcs", body, ST_REASSOCIATIONRESPONSE, sta->address, sta->pendingAssociationTransactionId); } void Ieee80211MgmtAp::handleReassociationResponseFrame(Packet *packet, const Ptr& header) @@ -311,11 +507,13 @@ 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; + mib->removePeerHtCapabilities(sta->address); } } @@ -344,7 +542,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); } @@ -378,8 +578,9 @@ void Ieee80211MgmtAp::start() void Ieee80211MgmtAp::stop() { cancelEvent(beaconTimer); + cancelEvent(associationResponseTimeoutTimer); 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 393df9e9104..3c3402c2cc5 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtAp.h @@ -16,17 +16,33 @@ namespace inet { namespace ieee80211 { +class ITransmitStep; +class IReceiveStep; + /** * 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; + uint64_t pendingAssociationTransactionId = 0; + simtime_t pendingAssociationDeadline = SIMTIME_MAX; + bool pendingHtStateAvailable = false; + bool pendingHtCapabilitiesValid = false; + Ieee80211HtCapabilities pendingHtCapabilities; // int consecFailedTrans; // TODO // double expiry; // TODO association should expire after a while if STA is silent? }; @@ -53,11 +69,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() {} @@ -81,7 +103,18 @@ 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 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(); + 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/Ieee80211MgmtBase.cc b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtBase.cc index 2cd699becc4..d6abb64108a 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,14 +46,27 @@ void Ieee80211MgmtBase::receiveSignal(cComponent *source, simsignal_t signalID, if (signalID == modesetChangedSignal) { modeSet = check_and_cast(obj); - supportedRates.numRates = std::min(8, modeSet->getNumModes()); + mib->updateLocalHtCapabilities(modeSet); 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; } } +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..80930fd830b 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" @@ -14,16 +17,205 @@ 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; + +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 { @@ -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::deserializeFrame(MemoryInputStream& stream, const std::type_info& typeInfo) { - 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; } @@ -437,4 +665,3 @@ const Ptr Ieee80211MgmtFrameSerializer::deserialize(MemoryInputStream& st } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtFrameSerializer.h index b23b021c642..22b05f08cb0 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" @@ -17,20 +19,31 @@ 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 { protected: virtual void serialize(MemoryOutputStream& stream, const Ptr& chunk) const override; - virtual const Ptr deserialize(MemoryInputStream& stream) 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 #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..c2ba7401517 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" @@ -120,11 +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 - sendAssociationConfirm(ap, PRC_TIMEOUT); + if (reassociation) { + handleReassociationFailure(ap); + sendReassociationConfirm(ap, PRC_TIMEOUT); + } + else + sendAssociationConfirm(ap, PRC_TIMEOUT); } else if (msg->getKind() == MK_SCAN_MAXCHANNELTIME) { // go to next channel during scanning @@ -172,10 +184,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) @@ -196,6 +208,8 @@ Ieee80211MgmtSta::ApInfo *Ieee80211MgmtSta::lookupAP(const MacAddress& address) void Ieee80211MgmtSta::clearAPList() { + cancelPendingAssociation(); + for (auto& elem : apList) if (elem.authTimeoutMsg) cancelAndDelete(elem.authTimeoutMsg); @@ -203,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"; @@ -267,13 +288,12 @@ 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); + reassociationInProgress = false; // schedule timeout ASSERT(assocTimeoutMsg == nullptr); @@ -282,6 +302,26 @@ 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); + reassociationInProgress = true; + 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)); @@ -302,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(); @@ -376,7 +410,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); } @@ -421,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; @@ -448,9 +485,17 @@ 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(); + 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()); + startReassociation(ap, ctrl->getTimeout()); } void Ieee80211MgmtSta::processDisassociateCommand(Ieee80211Prim_DisassociateRequest *ctrl) @@ -460,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(); @@ -476,7 +518,9 @@ 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); assocAP.beaconTimeoutMsg = nullptr; assocAP = AssociatedApInfo(); // clear it @@ -491,7 +535,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) @@ -601,6 +654,7 @@ void Ieee80211MgmtSta::handleDeauthenticationFrame(Packet *packet, const PtrisAuthenticated = false; + mib->removePeerHtCapabilities(address); delete packet; } @@ -611,7 +665,12 @@ void Ieee80211MgmtSta::handleAssociationRequestFrame(Packet *packet, const Ptr& header) { - EV << "Received Association Response frame\n"; + processAssociationResponse(packet, header, false); +} + +void Ieee80211MgmtSta::processAssociationResponse(Packet *packet, const Ptr& header, bool reassociation) +{ + EV << "Received Association or Reassociation Response frame\n"; if (!assocTimeoutMsg) { EV << "No association in progress, ignoring frame\n"; @@ -619,41 +678,77 @@ void Ieee80211MgmtSta::handleAssociationResponseFrame(Packet *packet, const Ptr< return; } + // IEEE Std 802.11-2024, 11.3.5.2 and 11.3.5.4: process only the response + // corresponding to the association or reassociation procedure in progress. + if (reassociation != reassociationInProgress) { + EV_INFO << "Association response subtype does not match the pending " + << (reassociationInProgress ? "reassociation" : "association") + << " attempt, ignoring frame\n"; + delete packet; + return; + } + + MacAddress address = header->getTransmitterAddress(); + 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(); - // 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; + cancelPendingAssociation(); if (statusCode != SC_SUCCESSFUL) { EV << "Association failed with AP address=" << ap->address << "\n"; + if (reassociation) + handleReassociationFailure(ap); + else 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); @@ -662,7 +757,26 @@ void Ieee80211MgmtSta::handleAssociationResponseFrame(Packet *packet, const Ptr< } // report back to agent - sendAssociationConfirm(ap, statusCodeToPrimResultCode(statusCode)); + if (reassociation) + sendReassociationConfirm(ap, statusCodeToPrimResultCode(statusCode)); + else + sendAssociationConfirm(ap, statusCodeToPrimResultCode(statusCode)); +} + +void Ieee80211MgmtSta::handleReassociationFailure(ApInfo *ap) +{ + // IEEE Std 802.11-2024, 11.3.5.4(f): failed or timed-out reassociation + // disassociates the STA only when the target is its current AP. + if (shouldDisassociateOnReassociationFailure(mib->bssStationData.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) @@ -672,8 +786,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, true); } void Ieee80211MgmtSta::handleDisassociationFrame(Packet *packet, const Ptr& header) @@ -681,11 +794,7 @@ void Ieee80211MgmtSta::handleDisassociationFrame(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; @@ -694,6 +803,7 @@ void Ieee80211MgmtSta::handleDisassociationFrame(Packet *packet, const PtrbssStationData.isAssociated = false; + mib->removePeerHtCapabilities(address); cancelAndDelete(assocAP.beaconTimeoutMsg); assocAP.beaconTimeoutMsg = nullptr; } @@ -747,6 +857,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 +874,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, 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(); @@ -144,6 +161,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(); @@ -190,4 +208,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..fe0a0ac138e 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.cc @@ -15,6 +15,33 @@ namespace ieee80211 { Define_Module(Ieee80211MgmtStaSimplified); +static Ieee80211Mib *findAccessPointMib(const MacAddress& accessPointAddress, bool required = true) +{ + L3AddressResolver addressResolver; + auto host = addressResolver.findHostWithAddress(accessPointAddress); + 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) { + 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) { Ieee80211MgmtBase::initialize(stage); @@ -24,20 +51,47 @@ 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 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")); - apMib->bssAccessPointData.stations[mib->address] = Ieee80211Mib::ASSOCIATED; - mib->bssData.ssid = apMib->bssData.ssid; + configureAssociation(); + } + else if (stage == INITSTAGE_LAST) + configureAssociation(); +} + +void Ieee80211MgmtStaSimplified::handleStartOperation(LifecycleOperation *operation) +{ + Ieee80211MgmtBase::handleStartOperation(operation); + if (operation != nullptr) + 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); } } +void Ieee80211MgmtStaSimplified::stop() +{ + mib->bssStationData.isAssociated = false; + 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); @@ -101,4 +155,3 @@ void Ieee80211MgmtStaSimplified::handleProbeResponseFrame(Packet *packet, const } // namespace ieee80211 } // namespace inet - diff --git a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h index 90a69af2af2..ce9e34c494e 100644 --- a/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h +++ b/src/inet/linklayer/ieee80211/mgmt/Ieee80211MgmtStaSimplified.h @@ -25,6 +25,9 @@ class INET_API Ieee80211MgmtStaSimplified : public Ieee80211MgmtBase protected: virtual int numInitStages() const override { return NUM_INIT_STAGES; } 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; @@ -52,4 +55,3 @@ class INET_API Ieee80211MgmtStaSimplified : public Ieee80211MgmtBase } // namespace inet #endif - 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/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..f48febff64a 100644 --- a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc +++ b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.cc @@ -7,6 +7,10 @@ #include "inet/linklayer/ieee80211/mib/Ieee80211Mib.h" +#include + +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.h" + namespace inet { namespace ieee80211 { @@ -19,11 +23,13 @@ void Ieee80211Mib::initialize(int stage) WATCH(address); WATCH(mode); WATCH(qos); + WATCH(localHtCapabilitiesValid); WATCH(bssData.bssid); WATCH(bssStationData.stationType); 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"); @@ -33,6 +39,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) @@ -59,29 +150,74 @@ 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 diff --git a/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h b/src/inet/linklayer/ieee80211/mib/Ieee80211Mib.h index 2183415fdd5..194a21a87a4 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,15 @@ 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 associationIdReservations; + std::map peerHtStates; + protected: virtual void initialize(int stage) override; @@ -69,8 +91,18 @@ 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; + 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/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/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc b/src/inet/physicallayer/wireless/ieee80211/mode/Ieee80211ModeSet.cc index 929d3e8cecc..b6da11a110e 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,14 @@ 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) }, -}),}; }); + // 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) : +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 +476,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 +645,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..e7a959979a0 --- /dev/null +++ b/tests/module/Ieee80211HtAssociation_1.test @@ -0,0 +1,138 @@ +%description: +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: + radioMedium: Ieee80211ScalarRadioMedium; + sta: WirelessHost { + parameters: + wlan[*].mgmt.typename = "Ieee80211MgmtSta"; + wlan[*].agent.typename = "Ieee80211AgentSta"; + } + ap: AccessPoint { + parameters: + wlan[*].mgmt.typename = "Ieee80211MgmtAp"; + } + test: Ieee80211HtAssociationChecker; +} + +%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 +*.ap.wlan[*].mac.dcf.rtsPolicy.rtsThreshold = 0B +*.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 = + +%contains: stdout +RTS-protected association committed at AP and STA. diff --git a/tests/module/Ieee80211MgmtApTimeout_1.test b/tests/module/Ieee80211MgmtApTimeout_1.test new file mode 100644 index 00000000000..390c0b19f47 --- /dev/null +++ b/tests/module/Ieee80211MgmtApTimeout_1.test @@ -0,0 +1,337 @@ +%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. Also verify a zero +association-response timeout is rejected because no response exchange can +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 { + +class TestIeee80211MgmtAp : public Ieee80211MgmtAp +{ + public: + short startPendingAssociation(const MacAddress& address) + { + Enter_Method("startPendingAssociation"); + 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) + { + Enter_Method("startPendingReassociation"); + 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; + } + + 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; + } + + 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(transactionId); + + 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; + } + + 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(); } + 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, public cListener +{ + using cListener::finish; + + protected: + TestIeee80211MgmtAp *mgmt = nullptr; + MacAddress expectedAssociatedAddress; + MacAddress expectedDisassociatedAddress; + short expectedAssociationId = 0; + int associationNotifications = 0; + int disassociationNotifications = 0; + + public: + Ieee80211MgmtApTimeoutTest() : cSimpleModule(65536) {} + + protected: + virtual void initialize() override + { + 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); + const auto& notification = check_and_cast(value); + 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 + { + 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); + + 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)); + + 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"; + } +}; + +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; +} + +%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] +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 + +[Config ZeroTimeout] +*.ap.wlan[0].mgmt.associationResponseTimeout = 0s + +%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 +parameter 'associationResponseTimeout' must be positive diff --git a/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test new file mode 100644 index 00000000000..5e749d64d47 --- /dev/null +++ b/tests/module/Ieee80211MgmtStaSimplifiedInitialization_1.test @@ -0,0 +1,240 @@ +%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. 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" + +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(!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"; + } + 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"; + 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")); + } + } + } + + 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")); + 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); + 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(!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); + 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->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); + 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; + } +}; + +Define_Module(TestInitializationObserver); + +} // namespace inet + +%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; +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; + scenarioManager: ScenarioManager; + sta: WirelessHost { + parameters: + wlan[*].mgmt.typename = "Ieee80211MgmtStaSimplified"; + wlan[*].agent.typename = ""; + } + ap: AccessPoint { + parameters: + wlan[*].mgmt.typename = "Ieee80211MgmtApSimplified"; + wlan[*].agent.typename = ""; + } + observer: TestInitializationObserver; +} + +%inifile: omnetpp.ini + +[General] +network = Ieee80211MgmtStaSimplifiedInitializationTestNetwork +ned-path = .;../../../../src;../../lib +sim-time-limit = 7us +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 = "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)" +**.hasStatus = true +**.scenarioManager.script = xmldoc("scenario.xml") +**.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 + +%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 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. 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/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..beddc042fb8 --- /dev/null +++ b/tests/unit/Ieee80211HtMgmtElements_1.test @@ -0,0 +1,268 @@ +%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->getChunkLength() == 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]); + +// 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.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()); +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; +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. 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..5f45c38fc1d --- /dev/null +++ b/tests/unit/Ieee80211MgmtApTransaction_1.test @@ -0,0 +1,121 @@ +%description: +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" +#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::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, + Ieee80211FrameType subtype = ST_ASSOCIATIONRESPONSE) +{ + auto packet = new Packet("AssocResp-OK"); + auto header = makeShared(); + header->setType(subtype); + 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); + + 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); + +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); + +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 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); +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 aggregate; +delete reassociationResponse; +delete finalResponse; + +EV << "AP association and reassociation transaction and timeout decisions verified.\n"; + +%contains: stdout +AP association and reassociation transaction and timeout decisions verified. diff --git a/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test b/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test new file mode 100644 index 00000000000..b03c5153b91 --- /dev/null +++ b/tests/unit/Ieee80211MgmtStaPrimitiveDispatch_1.test @@ -0,0 +1,264 @@ +%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. Verify association timeout ownership, +confirm typing, AP-list cleanup, restart eligibility, and late-response rejection. + +%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; + 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 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); + 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); + } + + 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 + { + associateRequests++; + } + + virtual void processReassociateCommand(Ieee80211Prim_ReassociateRequest *ctrl) override + { + 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) +{ + 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); + +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); + +// 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 == 2); +ASSERT(mgmt.reassociateConfirms == 1); +ASSERT(mgmt.lastReassociationResult == PRC_TIMEOUT); +ASSERT(mgmt.reassociationFailures == 1); + +mgmt.deliverLateAssociationResponse(true); +ASSERT(mgmt.associateConfirms == 2); +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 +Association and reassociation primitive dispatch and timeout ownership 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.