From 02ea2cc7e09ae76021e4563622e341f1a44e3b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Sun, 16 Aug 2026 22:17:25 +0200 Subject: [PATCH 1/8] pcap: add opt-in IEEE 802.11 Radiotap capture --- .oppfeatures | 1 + .../packet/recorder/IPcapCaptureAdapter.h | 65 +++ src/inet/common/packet/recorder/IPcapWriter.h | 13 +- .../recorder/PcapCaptureAdapterRegistry.cc | 78 +++ .../recorder/PcapCaptureAdapterRegistry.h | 43 ++ .../common/packet/recorder/PcapRecorder.cc | 172 +++--- .../common/packet/recorder/PcapRecorder.h | 8 +- .../common/packet/recorder/PcapRecorder.ned | 7 +- src/inet/common/packet/recorder/PcapWriter.cc | 33 +- src/inet/common/packet/recorder/PcapWriter.h | 3 +- .../common/packet/recorder/PcapngWriter.cc | 42 +- .../common/packet/recorder/PcapngWriter.h | 4 +- .../Ieee80211RadiotapPcapCaptureAdapter.cc | 493 ++++++++++++++++++ .../Ieee80211RadiotapPcapCaptureAdapter.h | 26 + .../WirelessPcapCaptureObservationAdapter.cc | 34 ++ .../WirelessPcapCaptureObservationAdapter.h | 24 + tests/unit/PcapCaptureAdapterRegistry_1.test | 82 +++ tests/unit/PcapRecorderIeee80211Ampdu_1.test | 222 ++++++++ tests/unit/PcapRecorderRadiotapHtVht_1.test | 147 ++++++ tests/unit/PcapWriterPrefix_1.test | 65 +++ ...relessPcapCaptureObservationAdapter_1.test | 108 ++++ 21 files changed, 1574 insertions(+), 96 deletions(-) create mode 100644 src/inet/common/packet/recorder/IPcapCaptureAdapter.h create mode 100644 src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc create mode 100644 src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.h create mode 100644 src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc create mode 100644 src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.h create mode 100644 src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.cc create mode 100644 src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.h create mode 100644 tests/unit/PcapCaptureAdapterRegistry_1.test create mode 100644 tests/unit/PcapRecorderIeee80211Ampdu_1.test create mode 100644 tests/unit/PcapRecorderRadiotapHtVht_1.test create mode 100644 tests/unit/PcapWriterPrefix_1.test create mode 100644 tests/unit/WirelessPcapCaptureObservationAdapter_1.test diff --git a/.oppfeatures b/.oppfeatures index c6eaa90f7b3..34615ed1714 100644 --- a/.oppfeatures +++ b/.oppfeatures @@ -1171,6 +1171,7 @@ inet.physicallayer.wireless.common.neighborcache inet.physicallayer.wireless.common.obstacleloss inet.physicallayer.wireless.common.pathloss + inet.physicallayer.wireless.common.pcap inet.physicallayer.wireless.common.propagation inet.physicallayer.wireless.common.radio inet.physicallayer.wireless.common.signal diff --git a/src/inet/common/packet/recorder/IPcapCaptureAdapter.h b/src/inet/common/packet/recorder/IPcapCaptureAdapter.h new file mode 100644 index 00000000000..dd26701194a --- /dev/null +++ b/src/inet/common/packet/recorder/IPcapCaptureAdapter.h @@ -0,0 +1,65 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IPCAPCAPTUREADAPTER_H +#define __INET_IPCAPCAPTUREADAPTER_H + +#include +#include + +#include "inet/common/DirectionTag_m.h" +#include "inet/common/IPrintableObject.h" +#include "inet/common/packet/Packet.h" +#include "inet/common/packet/recorder/IPcapWriter.h" + +namespace inet { + +class INET_API PcapCaptureObservation +{ + public: + const Packet *const packet; + const Direction direction; + const IPrintableObject *const transmission; + const IPrintableObject *const reception; + + PcapCaptureObservation(const Packet *packet, Direction direction, const IPrintableObject *transmission = nullptr, const IPrintableObject *reception = nullptr) : + packet(packet), direction(direction), transmission(transmission), reception(reception) {} +}; + +class INET_API PcapCaptureRecord +{ + protected: + std::vector prefix; + + public: + const b frontOffset; + const b backOffset; + + PcapCaptureRecord(b frontOffset, b backOffset, std::vector prefix = {}) : + prefix(std::move(prefix)), frontOffset(frontOffset), backOffset(backOffset) {} + + const std::vector& getPrefix() const { return prefix; } +}; + +class INET_API IPcapCaptureAdapter +{ + public: + virtual ~IPcapCaptureAdapter() {} + virtual PcapLinkType getLinkType() const = 0; + virtual std::optional> tryResolvePacket(const Packet *, b, b) const { return std::nullopt; } + virtual std::vector createRecords(const PcapCaptureObservation& observation, b frontOffset, b backOffset) const = 0; +}; + +class INET_API IPcapCaptureObservationAdapter +{ + public: + virtual ~IPcapCaptureObservationAdapter() {} + virtual std::optional tryCreateObservation(const cObject *object, Direction direction) const = 0; +}; + +} // namespace inet + +#endif diff --git a/src/inet/common/packet/recorder/IPcapWriter.h b/src/inet/common/packet/recorder/IPcapWriter.h index 7a4e81dfd2c..679282215ba 100644 --- a/src/inet/common/packet/recorder/IPcapWriter.h +++ b/src/inet/common/packet/recorder/IPcapWriter.h @@ -8,6 +8,8 @@ #ifndef __INET_IPCAPWRITER_H #define __INET_IPCAPWRITER_H +#include + #include "inet/common/DirectionTag_m.h" #include "inet/common/packet/Packet.h" #include "inet/networklayer/common/NetworkInterface.h" @@ -212,9 +214,18 @@ class INET_API IPcapWriter virtual void setFlush(bool flush) = 0; virtual void writePacket(simtime_t time, const Packet *packet, b frontOffset, b backOffset, Direction direction, NetworkInterface *ie, PcapLinkType linkType) = 0; + + /** + * Writes an octet prefix followed by the selected range of the original packet. + * Implementations which don't support scatter/gather capture fail explicitly. + */ + virtual void writePacketWithPrefix(simtime_t time, const std::vector& prefix, const Packet *packet, b frontOffset, b backOffset, + Direction direction, NetworkInterface *ie, PcapLinkType linkType) + { + throw cRuntimeError("This PCAP writer does not support prefixed packet records"); + } }; } // namespace inet #endif - diff --git a/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc new file mode 100644 index 00000000000..aca2f9d3f0e --- /dev/null +++ b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc @@ -0,0 +1,78 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#include "inet/common/packet/recorder/PcapCaptureAdapterRegistry.h" + +namespace inet { + +PcapCaptureAdapterRegistry::~PcapCaptureAdapterRegistry() +{ + for (auto entry : protocolAdapters) + delete entry.second; + for (auto entry : observationAdapters) + delete entry.second; +} + +void PcapCaptureAdapterRegistry::registerProtocolAdapter(const Protocol *protocol, const IPcapCaptureAdapter *adapter) +{ + if (protocol == nullptr || adapter == nullptr || protocolAdapters.find(protocol) != protocolAdapters.end()) { + delete adapter; + throw cRuntimeError("Duplicate or invalid PCAP capture protocol adapter registration"); + } + protocolAdapters.emplace(protocol, adapter); +} + +void PcapCaptureAdapterRegistry::registerProtocolResolver(const Protocol *outerProtocol, const Protocol *captureProtocol) +{ + if (outerProtocol == nullptr || captureProtocol == nullptr || protocolResolvers.find(outerProtocol) != protocolResolvers.end()) + throw cRuntimeError("Duplicate or invalid PCAP capture protocol resolver registration"); + protocolResolvers.emplace(outerProtocol, captureProtocol); +} + +void PcapCaptureAdapterRegistry::registerObservationAdapter(const char *key, const IPcapCaptureObservationAdapter *adapter) +{ + if (opp_isempty(key) || adapter == nullptr || observationAdapters.find(key) != observationAdapters.end()) { + delete adapter; + throw cRuntimeError("Duplicate or invalid PCAP capture observation adapter registration for '%s'", key == nullptr ? "" : key); + } + observationAdapters.emplace(key, adapter); +} + +const IPcapCaptureAdapter *PcapCaptureAdapterRegistry::findProtocolAdapter(const Protocol *protocol) const +{ + auto iterator = protocolAdapters.find(protocol); + return iterator == protocolAdapters.end() ? nullptr : iterator->second; +} + +std::optional> PcapCaptureAdapterRegistry::tryResolveProtocol(const Protocol *outerProtocol, const Packet *packet, b frontOffset, b backOffset) const +{ + auto resolver = protocolResolvers.find(outerProtocol); + if (resolver == protocolResolvers.end()) + return std::nullopt; + auto adapter = findProtocolAdapter(resolver->second); + if (adapter == nullptr) + return std::nullopt; + auto offsets = adapter->tryResolvePacket(packet, frontOffset, backOffset); + return offsets.has_value() ? std::optional>({resolver->second, offsets->first, offsets->second}) : std::nullopt; +} + +std::optional PcapCaptureAdapterRegistry::tryCreateObservation(const cObject *object, Direction direction) const +{ + for (const auto& entry : observationAdapters) { + auto observation = entry.second->tryCreateObservation(object, direction); + if (observation.has_value()) + return observation; + } + return std::nullopt; +} + +PcapCaptureAdapterRegistry& PcapCaptureAdapterRegistry::getInstance() +{ + static int handle = cSimulationOrSharedDataManager::registerSharedVariableName("inet::PcapCaptureAdapterRegistry::instance"); + return getSimulationOrSharedDataManager()->getSharedVariable(handle); +} + +} // namespace inet diff --git a/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.h b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.h new file mode 100644 index 00000000000..e2a92e35475 --- /dev/null +++ b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.h @@ -0,0 +1,43 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_PCAPCAPTUREADAPTERREGISTRY_H +#define __INET_PCAPCAPTUREADAPTERREGISTRY_H + +#include +#include + +#include "inet/common/packet/recorder/IPcapCaptureAdapter.h" + +namespace inet { + +#define Register_Pcap_Capture_Adapter(PROTOCOL, CLASSNAME) EXECUTE_PRE_NETWORK_SETUP(::inet::PcapCaptureAdapterRegistry::getInstance().registerProtocolAdapter(PROTOCOL, new CLASSNAME())); +#define Register_Pcap_Capture_Protocol_Resolver(OUTER_PROTOCOL, CAPTURE_PROTOCOL) EXECUTE_PRE_NETWORK_SETUP(::inet::PcapCaptureAdapterRegistry::getInstance().registerProtocolResolver(OUTER_PROTOCOL, CAPTURE_PROTOCOL)); +#define Register_Pcap_Capture_Observation_Adapter(KEY, CLASSNAME) EXECUTE_PRE_NETWORK_SETUP(::inet::PcapCaptureAdapterRegistry::getInstance().registerObservationAdapter(KEY, new CLASSNAME())); + +class INET_API PcapCaptureAdapterRegistry +{ + protected: + std::map protocolAdapters; + std::map protocolResolvers; + std::map observationAdapters; + + public: + ~PcapCaptureAdapterRegistry(); + + void registerProtocolAdapter(const Protocol *protocol, const IPcapCaptureAdapter *adapter); + void registerProtocolResolver(const Protocol *outerProtocol, const Protocol *captureProtocol); + void registerObservationAdapter(const char *key, const IPcapCaptureObservationAdapter *adapter); + const IPcapCaptureAdapter *findProtocolAdapter(const Protocol *protocol) const; + std::optional> tryResolveProtocol(const Protocol *outerProtocol, const Packet *packet, b frontOffset, b backOffset) const; + std::optional tryCreateObservation(const cObject *object, Direction direction) const; + + static PcapCaptureAdapterRegistry& getInstance(); +}; + +} // namespace inet + +#endif diff --git a/src/inet/common/packet/recorder/PcapRecorder.cc b/src/inet/common/packet/recorder/PcapRecorder.cc index bd3f673d703..16becbbe245 100644 --- a/src/inet/common/packet/recorder/PcapRecorder.cc +++ b/src/inet/common/packet/recorder/PcapRecorder.cc @@ -12,6 +12,7 @@ #include "inet/common/DirectionTag_m.h" #include "inet/common/ModuleAccess.h" +#include "inet/common/packet/recorder/PcapCaptureAdapterRegistry.h" #include "inet/common/packet/recorder/PcapngWriter.h" #include "inet/common/packet/recorder/PcapWriter.h" #include "inet/common/ProtocolTag_m.h" @@ -20,12 +21,6 @@ #include "inet/linklayer/common/InterfaceTag_m.h" #include "inet/networklayer/common/InterfaceTable.h" -#ifdef INET_WITH_PHYSICALLAYERWIRELESSCOMMON -#include "inet/physicallayer/common/Signal.h" -#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" -#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmission.h" -#endif - namespace inet { // ---- @@ -67,6 +62,7 @@ void PcapRecorder::initialize() verbose = par("verbose"); recordEmptyPackets = par("recordEmptyPackets"); enableConvertingPackets = par("enableConvertingPackets"); + enableProtocolSpecificCaptureAdapters = par("enableProtocolSpecificCaptureAdapters"); snaplen = this->par("snaplen"); dumpBadFrames = par("dumpBadFrames"); signalList.clear(); @@ -181,25 +177,40 @@ void PcapRecorder::receiveSignal(cComponent *source, simsignal_t signalID, cObje auto i = signalList.find(signalID); ASSERT(i != signalList.end()); Direction direction = i->second; - if (false) - ; -#ifdef INET_WITH_PHYSICALLAYERWIRELESSCOMMON - else if (auto signal = dynamic_cast(obj)) - recordPacket(signal->getEncapsulatedPacket(), direction, source); -#endif - else if (auto packet = dynamic_cast(obj)) + auto observation = PcapCaptureAdapterRegistry::getInstance().tryCreateObservation(obj, direction); + if (observation.has_value()) + recordPacket(*observation, source); + else if (auto packet = dynamic_cast(obj)) recordPacket(packet, direction, source); -#ifdef INET_WITH_PHYSICALLAYERWIRELESSCOMMON - else if (auto transmission = dynamic_cast(obj)) - recordPacket(transmission->getPacket(), direction, source); - else if (auto reception = dynamic_cast(obj)) - recordPacket(reception->getTransmission()->getPacket(), direction, source); -#endif } } +void PcapRecorder::writePacket(const Protocol *protocol, const PcapCaptureObservation& observation, b frontOffset, b backOffset, NetworkInterface *networkInterface) +{ + auto packet = observation.packet; + if (enableProtocolSpecificCaptureAdapters && enableConvertingPackets) { + auto adapter = PcapCaptureAdapterRegistry::getInstance().findProtocolAdapter(protocol); + if (adapter != nullptr) { + auto records = adapter->createRecords(observation, frontOffset, backOffset); + for (const auto& record : records) { + auto dataLength = packet->getDataLength() - record.frontOffset - record.backOffset; + if (recordEmptyPackets || !record.getPrefix().empty() || dataLength != b(0)) { + pcapWriter->writePacketWithPrefix(simTime(), record.getPrefix(), packet, record.frontOffset, record.backOffset, + observation.direction, networkInterface, adapter->getLinkType()); + numRecorded++; + emit(packetRecordedSignal, packet); + } + } + return; + } + } + + writePacket(protocol, packet, frontOffset, backOffset, observation.direction, networkInterface); +} + void PcapRecorder::writePacket(const Protocol *protocol, const Packet *packet, b frontOffset, b backOffset, Direction direction, NetworkInterface *networkInterface) { + auto pcapLinkType = protocolToLinkType(protocol); if (pcapLinkType == LINKTYPE_INVALID) throw cRuntimeError("Cannot determine the PCAP link type from protocol '%s'", protocol->getName()); @@ -221,53 +232,89 @@ void PcapRecorder::writePacket(const Protocol *protocol, const Packet *packet, b delete packet; } -void PcapRecorder::recordPacket(const cPacket *cpacket, Direction direction, cComponent *source) +void PcapRecorder::recordPacket(const PcapCaptureObservation& observation, cComponent *source) { - if (auto packet = dynamic_cast(cpacket)) { - EV_INFO << "Recording packet" << EV_FIELD(source, source->getFullPath()) << EV_FIELD(direction, direction) << EV_FIELD(packet) << EV_ENDL; - if (verbose) - EV_DEBUG << "Dumping packet" << EV_FIELD(packet, packetPrinter.printPacketToString(const_cast(packet), "%i")) << EV_ENDL; - if (recordPcap && packetFilter.matches(packet) && (dumpBadFrames || !packet->hasBitError())) { - // get Direction - if (direction == DIRECTION_UNDEFINED) { - if (auto directionTag = packet->findTag()) - direction = directionTag->getDirection(); - } + auto previousObservation = activeCaptureObservation; + activeCaptureObservation = &observation; + try { + recordPacket(observation.packet, observation.direction, source); + activeCaptureObservation = previousObservation; + } + catch (...) { + activeCaptureObservation = previousObservation; + throw; + } +} - // get NetworkInterface - auto srcModule = check_and_cast(source); - auto networkInterface = findContainingNicModule(srcModule); - if (networkInterface == nullptr) { - int ifaceId = -1; - if (direction == DIRECTION_OUTBOUND) { - if (auto ifaceTag = packet->findTag()) - ifaceId = ifaceTag->getInterfaceId(); - } - else if (direction == DIRECTION_INBOUND) { - if (auto ifaceTag = packet->findTag()) - ifaceId = ifaceTag->getInterfaceId(); - } - if (ifaceId != -1) { - auto ift = check_and_cast_nullable(getContainingNode(srcModule)->getSubmodule("interfaceTable")); - networkInterface = ift->getInterfaceById(ifaceId); - } +void PcapRecorder::recordPacket(const cPacket *cPacket, Direction direction, cComponent *source) +{ + auto packet = dynamic_cast(cPacket); + if (packet == nullptr) + return; + const PcapCaptureObservation observation = activeCaptureObservation != nullptr && activeCaptureObservation->packet == packet ? + PcapCaptureObservation(packet, direction, activeCaptureObservation->transmission, activeCaptureObservation->reception) : + PcapCaptureObservation(packet, direction); + EV_INFO << "Recording packet" << EV_FIELD(source, source->getFullPath()) << EV_FIELD(direction, direction) << EV_FIELD(packet) << EV_ENDL; + if (verbose) + EV_DEBUG << "Dumping packet" << EV_FIELD(packet, packetPrinter.printPacketToString(const_cast(packet), "%i")) << EV_ENDL; + if (recordPcap && packetFilter.matches(packet) && (dumpBadFrames || !packet->hasBitError())) { + // get Direction + if (direction == DIRECTION_UNDEFINED) { + if (auto directionTag = packet->findTag()) + direction = directionTag->getDirection(); + } + + // get NetworkInterface + auto srcModule = check_and_cast(source); + auto networkInterface = findContainingNicModule(srcModule); + if (networkInterface == nullptr) { + int ifaceId = -1; + if (direction == DIRECTION_OUTBOUND) { + if (auto ifaceTag = packet->findTag()) + ifaceId = ifaceTag->getInterfaceId(); + } + else if (direction == DIRECTION_INBOUND) { + if (auto ifaceTag = packet->findTag()) + ifaceId = ifaceTag->getInterfaceId(); + } + if (ifaceId != -1) { + auto ift = check_and_cast_nullable(getContainingNode(srcModule)->getSubmodule("interfaceTable")); + networkInterface = ift->getInterfaceById(ifaceId); } + } - const auto& packetProtocolTag = packet->getTag(); - auto protocol = packetProtocolTag->getProtocol(); - if (contains(dumpProtocols, protocol)) + PcapCaptureObservation effectiveObservation(packet, direction, observation.transmission, observation.reception); + const auto& packetProtocolTag = packet->getTag(); + auto protocol = packetProtocolTag->getProtocol(); + if (contains(dumpProtocols, protocol)) { + if (enableProtocolSpecificCaptureAdapters && enableConvertingPackets && + PcapCaptureAdapterRegistry::getInstance().findProtocolAdapter(protocol) != nullptr) + writePacket(protocol, effectiveObservation, packetProtocolTag->getFrontOffset(), packetProtocolTag->getBackOffset(), networkInterface); + else writePacket(protocol, packet, packetProtocolTag->getFrontOffset(), packetProtocolTag->getBackOffset(), direction, networkInterface); - else { - frontOffset = b(0); - backOffset = b(0); - dumpProtocol = nullptr; - Packet dissectedPacket(*packet); - PacketDissector packetDissector(ProtocolDissectorRegistry::getInstance(), *this); - packetDissector.dissectPacket(&dissectedPacket); - if (dumpProtocol != nullptr) - writePacket(dumpProtocol, packet, frontOffset, backOffset, direction, networkInterface); + return; + } + if (enableProtocolSpecificCaptureAdapters && enableConvertingPackets) { + auto resolution = PcapCaptureAdapterRegistry::getInstance().tryResolveProtocol(protocol, packet, + packetProtocolTag->getFrontOffset(), packetProtocolTag->getBackOffset()); + if (resolution.has_value() && contains(dumpProtocols, std::get<0>(*resolution))) { + writePacket(std::get<0>(*resolution), effectiveObservation, std::get<1>(*resolution), std::get<2>(*resolution), networkInterface); + return; } } + frontOffset = b(0); + backOffset = b(0); + dumpProtocol = nullptr; + Packet dissectedPacket(*packet); + PacketDissector packetDissector(ProtocolDissectorRegistry::getInstance(), *this); + packetDissector.dissectPacket(&dissectedPacket); + if (dumpProtocol != nullptr) { + if (enableProtocolSpecificCaptureAdapters && enableConvertingPackets && + PcapCaptureAdapterRegistry::getInstance().findProtocolAdapter(dumpProtocol) != nullptr) + writePacket(dumpProtocol, effectiveObservation, frontOffset, backOffset, networkInterface); + else + writePacket(dumpProtocol, packet, frontOffset, backOffset, direction, networkInterface); + } } } @@ -305,7 +352,11 @@ bool PcapRecorder::matchesLinkType(PcapLinkType pcapLinkType, const Protocol *pr PcapLinkType PcapRecorder::protocolToLinkType(const Protocol *protocol) const { - if (*protocol == Protocol::ethernetPhy) + auto captureAdapter = enableProtocolSpecificCaptureAdapters && enableConvertingPackets ? + PcapCaptureAdapterRegistry::getInstance().findProtocolAdapter(protocol) : nullptr; + if (captureAdapter != nullptr) + return captureAdapter->getLinkType(); + else if (*protocol == Protocol::ethernetPhy) return LINKTYPE_ETHERNET_MPACKET; else if (*protocol == Protocol::ethernetMac) return LINKTYPE_ETHERNET; @@ -339,4 +390,3 @@ Packet *PcapRecorder::tryConvertToLinkType(const Packet *packet, b frontOffset, } } // namespace inet - diff --git a/src/inet/common/packet/recorder/PcapRecorder.h b/src/inet/common/packet/recorder/PcapRecorder.h index df5bfbe4b35..fab237f414d 100644 --- a/src/inet/common/packet/recorder/PcapRecorder.h +++ b/src/inet/common/packet/recorder/PcapRecorder.h @@ -14,6 +14,7 @@ #include "inet/common/packet/dissector/PacketDissector.h" #include "inet/common/packet/PacketFilter.h" #include "inet/common/packet/printer/PacketPrinter.h" +#include "inet/common/packet/recorder/IPcapCaptureAdapter.h" #include "inet/common/packet/recorder/IPcapWriter.h" namespace inet { @@ -50,7 +51,9 @@ class INET_API PcapRecorder : public SimpleModule, protected cListener, public P bool verbose = false; bool recordEmptyPackets = false; bool enableConvertingPackets = true; + bool enableProtocolSpecificCaptureAdapters = false; bool recordPcap = false; + const PcapCaptureObservation *activeCaptureObservation = nullptr; std::vector helpers; PacketPrinter packetPrinter; @@ -78,14 +81,15 @@ class INET_API PcapRecorder : public SimpleModule, protected cListener, public P virtual void handleMessage(cMessage *msg) override; virtual void finish() override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; - virtual void recordPacket(const cPacket *msg, Direction direction, cComponent *source); + virtual void recordPacket(const cPacket *packet, Direction direction, cComponent *source); + virtual void recordPacket(const PcapCaptureObservation& observation, cComponent *source); virtual bool matchesLinkType(PcapLinkType pcapLinkType, const Protocol *protocol) const; virtual Packet *tryConvertToLinkType(const Packet *packet, b frontOffset, b backOffset, PcapLinkType pcapLinkType, const Protocol *protocol) const; virtual PcapLinkType protocolToLinkType(const Protocol *protocol) const; virtual void writePacket(const Protocol *protocol, const Packet *packet, b frontOffset, b backOffset, Direction direction, NetworkInterface *networkInterface); + virtual void writePacket(const Protocol *protocol, const PcapCaptureObservation& observation, b frontOffset, b backOffset, NetworkInterface *networkInterface); }; } // namespace inet #endif - diff --git a/src/inet/common/packet/recorder/PcapRecorder.ned b/src/inet/common/packet/recorder/PcapRecorder.ned index 772f39bd2bc..663fba55575 100644 --- a/src/inet/common/packet/recorder/PcapRecorder.ned +++ b/src/inet/common/packet/recorder/PcapRecorder.ned @@ -31,6 +31,11 @@ import inet.common.SimpleModule; // `sendingSignalNames` and `receivingSignalNames` parameters. The packets // themselves are expected as `cPacket*` signal values. // +// Registered protocol-specific capture adapters are opt-in. In particular, +// IEEE 802.11 uses the bare IEEE 802.11 link type (105) by default and the +// Radiotap link type (127) only when both adapter and packet conversion are +// enabled. +// simple PcapRecorder extends SimpleModule { parameters: @@ -38,6 +43,7 @@ simple PcapRecorder extends SimpleModule bool verbose = default(true); // Whether to log packets on the module output bool recordEmptyPackets = default(true); // Specifies if zero length packets are recorded or not bool enableConvertingPackets = default(true); // Specifies if converting packets to link type is allowed or not + bool enableProtocolSpecificCaptureAdapters = default(false); // Enable registered protocol-specific capture formats; requires enableConvertingPackets string pcapFile = default(""); // The PCAP file to be written, suggested value: pcapFile = "${resultdir}/${configname}-#${runnumber}" + fullpath() + ".pcap" string fileFormat @enum("pcap", "pcapng") = default("pcapng"); int snaplen = default(65535); // Maximum number of bytes to record per packet @@ -55,4 +61,3 @@ simple PcapRecorder extends SimpleModule @display("i=block/blackboard"); @signal[packetRecorded](type=Packet); } - diff --git a/src/inet/common/packet/recorder/PcapWriter.cc b/src/inet/common/packet/recorder/PcapWriter.cc index 80fb8f672ba..4debc385433 100644 --- a/src/inet/common/packet/recorder/PcapWriter.cc +++ b/src/inet/common/packet/recorder/PcapWriter.cc @@ -93,6 +93,12 @@ void PcapWriter::writeHeader(PcapLinkType linkType) } void PcapWriter::writePacket(simtime_t stime, const Packet *packet, b frontOffset, b backOffset, Direction direction, NetworkInterface *ie, PcapLinkType linkTypePar) +{ + writePacketWithPrefix(stime, {}, packet, frontOffset, backOffset, direction, ie, linkTypePar); +} + +void PcapWriter::writePacketWithPrefix(simtime_t stime, const std::vector& prefix, const Packet *packet, b frontOffset, b backOffset, + Direction direction, NetworkInterface *ie, PcapLinkType linkTypePar) { if (!dumpfile) throw cRuntimeError("Cannot write frame: pcap output file is not open"); @@ -113,9 +119,6 @@ void PcapWriter::writePacket(simtime_t stime, const Packet *packet, b frontOffse (void)ie; // unused EV_INFO << "Writing packet" << EV_FIELD(packet) << EV_FIELD(fileName) << EV_ENDL; - uint8_t buf[MAXBUFLENGTH]; - memset(buf, 0, sizeof(buf)); - struct pcaprec_hdr ph; ph.ts_sec = (int32_t)stime.inUnit(SIMTIME_S); switch(timePrecision) { @@ -123,19 +126,20 @@ void PcapWriter::writePacket(simtime_t stime, const Packet *packet, b frontOffse case 9: ph.ts_usec = (uint32_t)(stime.inUnit(SIMTIME_NS) - (uint32_t)1000000000 * stime.inUnit(SIMTIME_S)); break; default: throw cRuntimeError("Unsupported time precision (%d) in PcapWriter.", timePrecision); } - b capturedLength = packet->getDataLength() - frontOffset - backOffset; - if (capturedLength != b(0)) { - auto data = packet->peekDataAt(frontOffset, capturedLength); - auto bytes = data->getBytes(); - for (size_t i = 0; i < bytes.size(); i++) { - buf[i] = bytes[i]; - } - } - ph.orig_len = capturedLength.get(); - + b packetLength = packet->getDataLength() - frontOffset - backOffset; + size_t packetLengthBytes = packetLength.get(); + ph.orig_len = prefix.size() + packetLengthBytes; ph.incl_len = ph.orig_len > snaplen ? snaplen : ph.orig_len; fwrite(&ph, sizeof(ph), 1, dumpfile); - fwrite(buf, ph.incl_len, 1, dumpfile); + auto capturedPrefixLength = std::min(prefix.size(), ph.incl_len); + if (capturedPrefixLength != 0) + fwrite(prefix.data(), capturedPrefixLength, 1, dumpfile); + auto capturedPacketLength = ph.incl_len - capturedPrefixLength; + if (capturedPacketLength != 0) { + auto data = packet->peekDataAt(frontOffset, B(capturedPacketLength)); + const auto& bytes = data->getBytes(); + fwrite(bytes.data(), bytes.size(), 1, dumpfile); + } if (flush) fflush(dumpfile); } @@ -149,4 +153,3 @@ void PcapWriter::close() } } // namespace inet - diff --git a/src/inet/common/packet/recorder/PcapWriter.h b/src/inet/common/packet/recorder/PcapWriter.h index 19f2644f245..49c1fe0af92 100644 --- a/src/inet/common/packet/recorder/PcapWriter.h +++ b/src/inet/common/packet/recorder/PcapWriter.h @@ -64,6 +64,8 @@ class INET_API PcapWriter : public IPcapWriter * and throws an exception otherwise. */ void writePacket(simtime_t time, const Packet *packet, b frontOffset, b backOffset, Direction direction, NetworkInterface *ie, PcapLinkType linkType) override; + void writePacketWithPrefix(simtime_t time, const std::vector& prefix, const Packet *packet, b frontOffset, b backOffset, + Direction direction, NetworkInterface *ie, PcapLinkType linkType) override; /** * Closes the output file if it is open. @@ -79,4 +81,3 @@ class INET_API PcapWriter : public IPcapWriter } // namespace inet #endif - diff --git a/src/inet/common/packet/recorder/PcapngWriter.cc b/src/inet/common/packet/recorder/PcapngWriter.cc index d399cccbbb8..3085a25dffe 100644 --- a/src/inet/common/packet/recorder/PcapngWriter.cc +++ b/src/inet/common/packet/recorder/PcapngWriter.cc @@ -96,6 +96,7 @@ void PcapngWriter::open(const char *filename, unsigned int snaplen, int timePrec throw cRuntimeError("Cannot open pcap file [%s] for writing: %s", filename, strerror(errno)); flush = false; + this->snaplen = snaplen; // TODO check validity of timePrecision this->timePrecision = timePrecision; @@ -135,7 +136,7 @@ void PcapngWriter::writeInterface(NetworkInterface *networkInterface, PcapLinkTy ibh.blockTotalLength = blockTotalLength; ibh.linkType = linkType; ibh.reserved = 0; - ibh.snaplen = 0; + ibh.snaplen = snaplen; fwrite(&ibh, sizeof(ibh), 1, dumpfile); // interface name option @@ -197,11 +198,20 @@ void PcapngWriter::writeInterface(NetworkInterface *networkInterface, PcapLinkTy } void PcapngWriter::writePacket(simtime_t stime, const Packet *packet, b frontOffset, b backOffset, Direction direction, NetworkInterface *networkInterface, PcapLinkType linkType) +{ + writePacketWithPrefix(stime, {}, packet, frontOffset, backOffset, direction, networkInterface, linkType); +} + +void PcapngWriter::writePacketWithPrefix(simtime_t stime, const std::vector& prefix, const Packet *packet, b frontOffset, b backOffset, + Direction direction, NetworkInterface *networkInterface, PcapLinkType linkType) { EV_INFO << "Writing packet to file" << EV_FIELD(fileName) << EV_FIELD(packet) << EV_ENDL; if (!dumpfile) throw cRuntimeError("Cannot write frame: pcap output file is not open"); + if (networkInterface == nullptr) + throw cRuntimeError("The interface entry not found for packet"); + auto it = interfaceModuleIdToPcapngInterfaceId.find(networkInterface->getId()); int pcapngInterfaceId; if (it != interfaceModuleIdToPcapngInterfaceId.end()) @@ -212,12 +222,11 @@ void PcapngWriter::writePacket(simtime_t stime, const Packet *packet, b frontOff interfaceModuleIdToPcapngInterfaceId[networkInterface->getId()] = pcapngInterfaceId; } - if (networkInterface == nullptr) - throw cRuntimeError("The interface entry not found for packet"); - - b capturedLength = packet->getDataLength() - frontOffset - backOffset; + b packetLength = packet->getDataLength() - frontOffset - backOffset; + size_t originalLength = prefix.size() + packetLength.get(); + size_t capturedLength = std::min(originalLength, snaplen); uint32_t optionsLength = (4 + 4) + 4; - uint32_t blockTotalLength = 32 + roundUp(capturedLength.get()) + optionsLength; + uint32_t blockTotalLength = 32 + roundUp(capturedLength) + optionsLength; ASSERT(blockTotalLength % 4 == 0); // header @@ -228,19 +237,25 @@ void PcapngWriter::writePacket(simtime_t stime, const Packet *packet, b frontOff uint64_t timestamp = stime.inUnit(static_cast(-timePrecision)); pbh.timestampHigh = static_cast((timestamp >> 32) & 0xFFFFFFFFLLU); pbh.timestampLow = static_cast(timestamp & 0xFFFFFFFFLLU); - pbh.capturedPacketLength = capturedLength.get(); - pbh.originalPacketLength = capturedLength.get(); + pbh.capturedPacketLength = capturedLength; + pbh.originalPacketLength = originalLength; fwrite(&pbh, sizeof(pbh), 1, dumpfile); - if (capturedLength != b(0)) { + if (capturedLength != 0) { // packet data - auto data = packet->peekDataAt(frontOffset, capturedLength); - auto bytes = data->getBytes(); - fwrite(bytes.data(), bytes.size(), 1, dumpfile); + auto capturedPrefixLength = std::min(prefix.size(), capturedLength); + if (capturedPrefixLength != 0) + fwrite(prefix.data(), capturedPrefixLength, 1, dumpfile); + auto capturedPacketLength = capturedLength - capturedPrefixLength; + if (capturedPacketLength != 0) { + auto data = packet->peekDataAt(frontOffset, B(capturedPacketLength)); + const auto& bytes = data->getBytes(); + fwrite(bytes.data(), bytes.size(), 1, dumpfile); + } // packet padding char padding[] = { 0, 0, 0, 0 }; - int paddingLength = pad(capturedLength.get()); + int paddingLength = pad(capturedLength); fwrite(padding, paddingLength, 1, dumpfile); } @@ -284,4 +299,3 @@ void PcapngWriter::close() } } // namespace inet - diff --git a/src/inet/common/packet/recorder/PcapngWriter.h b/src/inet/common/packet/recorder/PcapngWriter.h index 358d0deee15..ccc206b32fe 100644 --- a/src/inet/common/packet/recorder/PcapngWriter.h +++ b/src/inet/common/packet/recorder/PcapngWriter.h @@ -23,6 +23,7 @@ class INET_API PcapngWriter : public IPcapWriter protected: std::string fileName; FILE *dumpfile = nullptr; // pcap file + unsigned int snaplen = 0; bool flush = false; int nextPcapngInterfaceId = 0; int timePrecision = 6; @@ -60,6 +61,8 @@ class INET_API PcapngWriter : public IPcapWriter * and throws an exception otherwise. */ void writePacket(simtime_t time, const Packet *packet, b frontOffset, b backOffset, Direction direction, NetworkInterface *ie, PcapLinkType linkType) override; + void writePacketWithPrefix(simtime_t time, const std::vector& prefix, const Packet *packet, b frontOffset, b backOffset, + Direction direction, NetworkInterface *ie, PcapLinkType linkType) override; /** * Closes the output file if it is open. @@ -75,4 +78,3 @@ class INET_API PcapngWriter : public IPcapWriter } // namespace inet #endif - diff --git a/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc new file mode 100644 index 00000000000..4b19db61554 --- /dev/null +++ b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc @@ -0,0 +1,493 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#include "inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.h" + +#include +#include +#include +#include +#include + +#include "inet/common/INETMath.h" +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/checksum/Checksum.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/common/packet/recorder/PcapCaptureAdapterRegistry.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/INarrowbandSignalAnalogModel.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmission.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.h" + +namespace inet { + +namespace { + +// Field layouts are defined by the official radiotap specifications: +// https://www.radiotap.org/fields/Flags.html +// https://www.radiotap.org/fields/MCS.html +// https://www.radiotap.org/fields/A-MPDU%20status.html +// https://www.radiotap.org/fields/VHT.html +enum RadiotapPresentBit { + RADIOTAP_FLAGS = 1, + RADIOTAP_RATE = 2, + RADIOTAP_CHANNEL = 3, + RADIOTAP_ANTENNA_SIGNAL = 5, + RADIOTAP_DBM_TX_POWER = 10, + RADIOTAP_RX_FLAGS = 14, + RADIOTAP_TX_FLAGS = 15, + RADIOTAP_MCS = 19, + RADIOTAP_AMPDU = 20, + RADIOTAP_VHT = 21, +}; + +enum RadiotapFlags { + RADIOTAP_F_FCS = 0x10, + RADIOTAP_F_BADFCS = 0x40, +}; + +enum RadiotapChannelFlags { + RADIOTAP_CHANNEL_2GHZ = 0x0080, + RADIOTAP_CHANNEL_5GHZ = 0x0100, +}; + +enum RadiotapVhtKnown { + RADIOTAP_VHT_GI_KNOWN = 1U << 2, + RADIOTAP_VHT_BANDWIDTH_KNOWN = 1U << 6, +}; + +struct MpduRange +{ + b offset; + b length; +}; + +enum class AmpduParseResult { + NOT_AGGREGATE, + VALID, + INVALID, +}; + +struct RadiotapRecordMetadata +{ + bool isAmpdu = false; + bool isLastSubframe = false; + uint32_t ampduReference = 0; + bool hasFcs = false; + bool hasBadFcs = false; +}; + +struct RadiotapPpduFields +{ + Direction direction = DIRECTION_UNDEFINED; + bool hasRate = false; + uint8_t rate = 0; + bool hasChannel = false; + uint16_t channelFrequency = 0; + uint16_t channelFlags = 0; + bool hasPower = false; + int8_t power = 0; + bool isHt = false; + std::array mcs = {}; + bool isVht = false; + uint16_t vhtKnown = 0; + uint8_t vhtFlags = 0; + uint8_t vhtBandwidth = 0; + std::array vhtMcsNss = {}; + uint8_t vhtCoding = 0; + uint8_t vhtGroupId = 0; + uint16_t vhtPartialAid = 0; +}; + +void appendPadding(std::vector& bytes, size_t alignment) +{ + bytes.resize(bytes.size() + (alignment - bytes.size() % alignment) % alignment, 0); +} + +void appendUint16(std::vector& bytes, uint16_t value) +{ + bytes.push_back(value & 0xff); + bytes.push_back(value >> 8); +} + +void appendUint32(std::vector& bytes, uint32_t value) +{ + for (int i = 0; i < 4; i++) + bytes.push_back((value >> (8 * i)) & 0xff); +} + +void setUint16(std::vector& bytes, size_t offset, uint16_t value) +{ + bytes.at(offset) = value & 0xff; + bytes.at(offset + 1) = value >> 8; +} + +void setUint32(std::vector& bytes, size_t offset, uint32_t value) +{ + for (size_t i = 0; i < 4; i++) + bytes.at(offset + i) = value >> (8 * i); +} + +uint8_t getRadiotapVhtBandwidth(Hz bandwidth) +{ + auto value = bandwidth.get(); + if (value < 30e6) + return 0; + if (value < 60e6) + return 1; + if (value < 100e6) + return 4; + if (value < 180e6) + return 11; + throw cRuntimeError("Unsupported VHT radiotap channel width: %g Hz", value); +} + +uint32_t makeAmpduReference(const Packet *packet) +{ + auto treeId = static_cast(packet->getTreeId()); + return static_cast(treeId) ^ static_cast(treeId >> 32); +} + +AmpduParseResult getIeee80211AmpduMpduRanges(const Packet *packet, b frontOffset, b backOffset, std::vector& mpduRanges) +{ + // IEEE 802.11-2024, 9.7.1, Figures 9-1326, 9-1328, 9-1329 and Table 9-659. + const int parsingFlags = Chunk::PF_ALLOW_INCORRECT | Chunk::PF_ALLOW_INCOMPLETE | Chunk::PF_ALLOW_IMPROPERLY_REPRESENTED; + auto endOffset = packet->getDataLength() - backOffset; + if (frontOffset + ieee80211::LENGTH_A_MPDU_SUBFRAME_HEADER > endOffset) + return AmpduParseResult::NOT_AGGREGATE; + + auto peekDelimiter = [&] (b offset) { + return dynamicPtrCast(packet->peekDataAt(offset, b(-1), parsingFlags)); + }; + + try { + if (peekDelimiter(frontOffset) == nullptr) + return AmpduParseResult::NOT_AGGREGATE; + auto offset = frontOffset; + while (offset < endOffset) { + if (offset + ieee80211::LENGTH_A_MPDU_SUBFRAME_HEADER > endOffset) + return AmpduParseResult::INVALID; + const auto& delimiter = peekDelimiter(offset); + if (delimiter == nullptr || delimiter->getLength() < 0) + return AmpduParseResult::INVALID; + auto mpduOffset = offset + delimiter->getChunkLength(); + auto mpduLength = B(delimiter->getLength()); + // A zero-length delimiter is representable as VHT EOF/padding and + // does not itself produce a captured MPDU record. + if (mpduLength == b(0)) { + offset = mpduOffset; + continue; + } + if (mpduOffset + mpduLength > endOffset) + return AmpduParseResult::INVALID; + mpduRanges.push_back({mpduOffset, mpduLength}); + offset = mpduOffset + mpduLength; + if (offset == endOffset) + return AmpduParseResult::VALID; + auto paddingLength = B((4 - (delimiter->getChunkLength() + mpduLength).get() % 4) % 4); + if (offset + paddingLength >= endOffset) + return AmpduParseResult::INVALID; + offset += paddingLength; + } + } + catch (cRuntimeError&) { + return AmpduParseResult::INVALID; + } + return AmpduParseResult::VALID; +} + +struct FcsMetadata +{ + bool isPresent = false; + bool isBad = false; +}; + +FcsMetadata getIeee80211FcsMetadata(const Packet *packet, b frontOffset, b backOffset) +{ + auto endOffset = packet->getDataLength() - backOffset; + if (endOffset - frontOffset < B(4)) + return {}; + try { + auto trailer = dynamicPtrCast(packet->peekDataAt(endOffset - B(4), B(4))); + if (trailer == nullptr) + return {}; + FcsMetadata metadata; + metadata.isPresent = true; + switch (trailer->getFcsMode()) { + case FCS_DECLARED_INCORRECT: + metadata.isBad = true; + break; + case FCS_COMPUTED: { + auto data = packet->peekDataAt(frontOffset, endOffset - frontOffset - trailer->getChunkLength()); + metadata.isBad = ethernetFcs(data->getBytes()) != trailer->getFcs(); + break; + } + case FCS_DECLARED_CORRECT: + default: + break; + } + return metadata; + } + catch (cRuntimeError&) { + return {}; + } +} + +const physicallayer::IIeee80211Mode *findIeee80211Mode(const Packet *packet, const physicallayer::ITransmission *transmission) +{ + if (auto ieee80211Transmission = dynamic_cast(transmission)) { + if (auto mode = ieee80211Transmission->getMode()) + return mode; + } + if (auto modeReq = packet->findTag()) + return modeReq->getMode(); + if (auto modeInd = packet->findTag()) + return modeInd->getMode(); + return nullptr; +} + +RadiotapPpduFields extractRadiotapPpduFields(const Packet *packet, Direction direction, const physicallayer::ITransmission *transmission, + const physicallayer::IReception *reception) +{ + RadiotapPpduFields fields; + fields.direction = direction; + + auto mode = findIeee80211Mode(packet, transmission); + if (mode != nullptr) { + auto dataMode = mode->getDataMode(); + if (dynamic_cast(mode) != nullptr) { + fields.isHt = true; + if (auto htDataMode = dynamic_cast(dataMode)) { + // IEEE 802.11-2024, Table 19-11; radiotap MCS known/flags/mcs fields. + fields.mcs[0] = 0x01 | 0x02 | 0x04 | 0x10; // bandwidth, MCS, GI, and BCC FEC are known + if (htDataMode->getBandwidth().get() > 30e6) + fields.mcs[1] |= 1; + if (htDataMode->getGuardIntervalType() == physicallayer::Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) + fields.mcs[1] |= 1 << 2; + fields.mcs[2] = htDataMode->getMcsIndex(); + } + } + else if (dynamic_cast(mode) != nullptr) { + fields.isVht = true; + if (auto vhtDataMode = dynamic_cast(dataMode)) { + // IEEE 802.11-2024, Table 21-12; radiotap VHT known/flags fields. + fields.vhtKnown = RADIOTAP_VHT_GI_KNOWN | RADIOTAP_VHT_BANDWIDTH_KNOWN; + if (vhtDataMode->getGuardIntervalType() == physicallayer::Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT) + fields.vhtFlags |= 0x04; + fields.vhtBandwidth = getRadiotapVhtBandwidth(vhtDataMode->getBandwidth()); + auto mcs = vhtDataMode->getMcsIndex(); + auto numberOfSpatialStreams = vhtDataMode->getNumberOfSpatialStreams(); + if (mcs <= 9 && numberOfSpatialStreams >= 1 && numberOfSpatialStreams <= 8) + fields.vhtMcsNss[0] = (mcs << 4) | numberOfSpatialStreams; + fields.vhtCoding = 0; // BCC + } + } + else if (dataMode != nullptr) { + double rateValue = dataMode->getNetBitrate().get() / 500000.0; + if (std::isfinite(rateValue) && rateValue >= 1 && rateValue <= 255 && rateValue == std::trunc(rateValue)) { + fields.hasRate = true; + fields.rate = static_cast(rateValue); + } + } + } + + const physicallayer::ISignalAnalogModel *analogModel = nullptr; + simtime_t startTime; + simtime_t endTime; + if (reception != nullptr) { + analogModel = reception->getAnalogModel(); + startTime = reception->getStartTime(); + endTime = reception->getEndTime(); + } + else if (transmission != nullptr) { + analogModel = transmission->getAnalogModel(); + startTime = transmission->getStartTime(); + endTime = transmission->getEndTime(); + } + + auto narrowbandAnalogModel = dynamic_cast(analogModel); + if (narrowbandAnalogModel != nullptr) { + double frequencyMHz = narrowbandAnalogModel->getCenterFrequency().get() / 1e6; + if (std::isfinite(frequencyMHz) && frequencyMHz > 0 && frequencyMHz <= UINT16_MAX) { + fields.hasChannel = true; + fields.channelFrequency = static_cast(std::round(frequencyMHz)); + fields.channelFlags = frequencyMHz < 3000 ? RADIOTAP_CHANNEL_2GHZ : frequencyMHz < 6000 ? RADIOTAP_CHANNEL_5GHZ : 0; + } + + auto power = narrowbandAnalogModel->computeMinPower(startTime, endTime); + double powerMilliwatts = power.get(); + if (std::isfinite(powerMilliwatts) && powerMilliwatts > 0 && + (direction == DIRECTION_INBOUND || direction == DIRECTION_OUTBOUND)) { + int powerDbm = static_cast(std::round(math::mW2dBmW(powerMilliwatts))); + fields.hasPower = true; + fields.power = static_cast(std::clamp(powerDbm, -128, 127)); + } + } + return fields; +} + +std::vector serializeRadiotapHeader(const RadiotapPpduFields& fields, const RadiotapRecordMetadata& metadata) +{ + uint32_t present = 0; + auto setPresentBit = [&] (RadiotapPresentBit bit) { present |= 1U << bit; }; + std::vector bytes(8, 0); + + setPresentBit(RADIOTAP_FLAGS); + bytes.push_back((metadata.hasFcs ? RADIOTAP_F_FCS : 0) | (metadata.hasBadFcs ? RADIOTAP_F_BADFCS : 0)); + if (fields.hasRate) { + setPresentBit(RADIOTAP_RATE); + bytes.push_back(fields.rate); + } + if (fields.hasChannel) { + setPresentBit(RADIOTAP_CHANNEL); + appendPadding(bytes, 2); + appendUint16(bytes, fields.channelFrequency); + appendUint16(bytes, fields.channelFlags); + } + if (fields.hasPower) { + setPresentBit(fields.direction == DIRECTION_INBOUND ? RADIOTAP_ANTENNA_SIGNAL : RADIOTAP_DBM_TX_POWER); + bytes.push_back(static_cast(fields.power)); + } + if (fields.direction == DIRECTION_INBOUND) { + setPresentBit(RADIOTAP_RX_FLAGS); + appendPadding(bytes, 2); + appendUint16(bytes, 0); + } + else if (fields.direction == DIRECTION_OUTBOUND) { + setPresentBit(RADIOTAP_TX_FLAGS); + appendPadding(bytes, 2); + appendUint16(bytes, 0); + } + if (fields.isHt) { + setPresentBit(RADIOTAP_MCS); + bytes.insert(bytes.end(), fields.mcs.begin(), fields.mcs.end()); + } + if (metadata.isAmpdu) { + setPresentBit(RADIOTAP_AMPDU); + appendPadding(bytes, 4); + appendUint32(bytes, metadata.ampduReference); + // Radiotap A-MPDU status: LAST_KNOWN and, for the terminal MPDU, IS_LAST. + // Delimiter CRC and EOF are intentionally left unknown. + appendUint16(bytes, 0x0004 | (metadata.isLastSubframe ? 0x0008 : 0)); + bytes.push_back(0); + bytes.push_back(0); + } + if (fields.isVht) { + setPresentBit(RADIOTAP_VHT); + appendPadding(bytes, 2); + appendUint16(bytes, fields.vhtKnown); + bytes.push_back(fields.vhtFlags); + bytes.push_back(fields.vhtBandwidth); + bytes.insert(bytes.end(), fields.vhtMcsNss.begin(), fields.vhtMcsNss.end()); + bytes.push_back(fields.vhtCoding); + bytes.push_back(fields.vhtGroupId); + appendUint16(bytes, fields.vhtPartialAid); + } + setUint16(bytes, 2, bytes.size()); + setUint32(bytes, 4, present); + return bytes; +} + +} // namespace + +namespace ieee80211 { + +Register_Pcap_Capture_Adapter(&Protocol::ieee80211Mac, Ieee80211RadiotapPcapCaptureAdapter); +Register_Pcap_Capture_Protocol_Resolver(&Protocol::ieee80211FhssPhy, &Protocol::ieee80211Mac); +Register_Pcap_Capture_Protocol_Resolver(&Protocol::ieee80211IrPhy, &Protocol::ieee80211Mac); +Register_Pcap_Capture_Protocol_Resolver(&Protocol::ieee80211DsssPhy, &Protocol::ieee80211Mac); +Register_Pcap_Capture_Protocol_Resolver(&Protocol::ieee80211HrDsssPhy, &Protocol::ieee80211Mac); +Register_Pcap_Capture_Protocol_Resolver(&Protocol::ieee80211OfdmPhy, &Protocol::ieee80211Mac); +Register_Pcap_Capture_Protocol_Resolver(&Protocol::ieee80211ErpOfdmPhy, &Protocol::ieee80211Mac); +Register_Pcap_Capture_Protocol_Resolver(&Protocol::ieee80211HtPhy, &Protocol::ieee80211Mac); +Register_Pcap_Capture_Protocol_Resolver(&Protocol::ieee80211VhtPhy, &Protocol::ieee80211Mac); + +std::optional> Ieee80211RadiotapPcapCaptureAdapter::tryResolvePacket(const Packet *packet, b frontOffset, b backOffset) const +{ + const int parsingFlags = Chunk::PF_ALLOW_INCORRECT | Chunk::PF_ALLOW_INCOMPLETE | Chunk::PF_ALLOW_IMPROPERLY_REPRESENTED; + try { + const auto protocol = packet->getTag()->getProtocol(); + Ptr header; + if (*protocol == Protocol::ieee80211FhssPhy) + header = packet->peekDataAt(frontOffset, b(-1), parsingFlags); + else if (*protocol == Protocol::ieee80211IrPhy) + header = packet->peekDataAt(frontOffset, b(-1), parsingFlags); + else if (*protocol == Protocol::ieee80211DsssPhy) + header = packet->peekDataAt(frontOffset, b(-1), parsingFlags); + else if (*protocol == Protocol::ieee80211HrDsssPhy) + header = packet->peekDataAt(frontOffset, b(-1), parsingFlags); + else if (*protocol == Protocol::ieee80211OfdmPhy) + header = packet->peekDataAt(frontOffset, b(-1), parsingFlags); + else if (*protocol == Protocol::ieee80211ErpOfdmPhy) + header = packet->peekDataAt(frontOffset, b(-1), parsingFlags); + else if (*protocol == Protocol::ieee80211HtPhy) + header = packet->peekDataAt(frontOffset, b(-1), parsingFlags); + else if (*protocol == Protocol::ieee80211VhtPhy) + header = packet->peekDataAt(frontOffset, b(-1), parsingFlags); + else + return std::nullopt; + if (header->isIncorrect() || header->isIncomplete() || header->isImproperlyRepresented() || + b(header->getLengthField()) < header->getChunkLength()) + return std::nullopt; + auto resolvedFrontOffset = frontOffset + header->getChunkLength(); + auto payloadLength = b(header->getLengthField()); + auto availablePayloadLength = packet->getDataLength() - resolvedFrontOffset - backOffset; + if (payloadLength > availablePayloadLength) + return std::nullopt; + auto resolvedBackOffset = packet->getDataLength() - resolvedFrontOffset - payloadLength; + return std::pair(resolvedFrontOffset, resolvedBackOffset); + } + catch (cRuntimeError&) { + return std::nullopt; + } +} + +std::vector Ieee80211RadiotapPcapCaptureAdapter::createRecords(const PcapCaptureObservation& observation, b frontOffset, b backOffset) const +{ + auto packet = observation.packet; + auto transmission = dynamic_cast(observation.transmission); + auto reception = dynamic_cast(observation.reception); + const auto ppduFields = extractRadiotapPpduFields(packet, observation.direction, transmission, reception); + + std::vector mpduRanges; + auto ampduParseResult = getIeee80211AmpduMpduRanges(packet, frontOffset, backOffset, mpduRanges); + if (ampduParseResult == AmpduParseResult::INVALID) + return {}; + if (ampduParseResult == AmpduParseResult::VALID) { + std::vector records; + records.reserve(mpduRanges.size()); + auto ampduReference = makeAmpduReference(packet); + for (size_t i = 0; i < mpduRanges.size(); i++) { + const auto& mpduRange = mpduRanges[i]; + auto recordBackOffset = packet->getDataLength() - mpduRange.offset - mpduRange.length; + RadiotapRecordMetadata metadata; + // Radiotap defines A-MPDU status for received frames. Outbound + // aggregates are still split, but carry no A-MPDU status field. + metadata.isAmpdu = observation.direction == DIRECTION_INBOUND; + metadata.isLastSubframe = i == mpduRanges.size() - 1; + metadata.ampduReference = ampduReference; + auto fcsMetadata = getIeee80211FcsMetadata(packet, mpduRange.offset, recordBackOffset); + metadata.hasFcs = fcsMetadata.isPresent; + metadata.hasBadFcs = fcsMetadata.isBad; + records.emplace_back(mpduRange.offset, recordBackOffset, serializeRadiotapHeader(ppduFields, metadata)); + } + return records; + } + + RadiotapRecordMetadata metadata; + auto fcsMetadata = getIeee80211FcsMetadata(packet, frontOffset, backOffset); + metadata.hasFcs = fcsMetadata.isPresent; + metadata.hasBadFcs = fcsMetadata.isBad; + return {PcapCaptureRecord(frontOffset, backOffset, serializeRadiotapHeader(ppduFields, metadata))}; +} + +} // namespace ieee80211 +} // namespace inet diff --git a/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.h b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.h new file mode 100644 index 00000000000..7c34d455365 --- /dev/null +++ b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.h @@ -0,0 +1,26 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_IEEE80211RADIOTAPPCAPCAPTUREADAPTER_H +#define __INET_IEEE80211RADIOTAPPCAPCAPTUREADAPTER_H + +#include "inet/common/packet/recorder/IPcapCaptureAdapter.h" + +namespace inet { +namespace ieee80211 { + +class INET_API Ieee80211RadiotapPcapCaptureAdapter : public IPcapCaptureAdapter +{ + public: + virtual PcapLinkType getLinkType() const override { return LINKTYPE_IEEE802_11_RADIOTAP; } + virtual std::optional> tryResolvePacket(const Packet *packet, b frontOffset, b backOffset) const override; + virtual std::vector createRecords(const PcapCaptureObservation& observation, b frontOffset, b backOffset) const override; +}; + +} // namespace ieee80211 +} // namespace inet + +#endif diff --git a/src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.cc b/src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.cc new file mode 100644 index 00000000000..e7289f55c47 --- /dev/null +++ b/src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.cc @@ -0,0 +1,34 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#include "inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.h" + +#include "inet/common/packet/recorder/PcapCaptureAdapterRegistry.h" +#include "inet/physicallayer/common/Signal.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmission.h" + +namespace inet { +namespace physicallayer { + +Register_Pcap_Capture_Observation_Adapter("wireless", WirelessPcapCaptureObservationAdapter); + +std::optional WirelessPcapCaptureObservationAdapter::tryCreateObservation(const cObject *object, Direction direction) const +{ + if (auto signal = dynamic_cast(object)) { + auto packet = dynamic_cast(signal->getEncapsulatedPacket()); + return packet != nullptr ? std::optional(PcapCaptureObservation(packet, direction)) : std::nullopt; + } + else if (auto transmission = dynamic_cast(object)) + return PcapCaptureObservation(transmission->getPacket(), direction, transmission); + else if (auto reception = dynamic_cast(object)) + return PcapCaptureObservation(reception->getTransmission()->getPacket(), direction, reception->getTransmission(), reception); + else + return std::nullopt; +} + +} // namespace physicallayer +} // namespace inet diff --git a/src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.h b/src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.h new file mode 100644 index 00000000000..bc19b51a6d7 --- /dev/null +++ b/src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.h @@ -0,0 +1,24 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_WIRELESSPCAPCAPTUREOBSERVATIONADAPTER_H +#define __INET_WIRELESSPCAPCAPTUREOBSERVATIONADAPTER_H + +#include "inet/common/packet/recorder/IPcapCaptureAdapter.h" + +namespace inet { +namespace physicallayer { + +class INET_API WirelessPcapCaptureObservationAdapter : public IPcapCaptureObservationAdapter +{ + public: + virtual std::optional tryCreateObservation(const cObject *object, Direction direction) const override; +}; + +} // namespace physicallayer +} // namespace inet + +#endif diff --git a/tests/unit/PcapCaptureAdapterRegistry_1.test b/tests/unit/PcapCaptureAdapterRegistry_1.test new file mode 100644 index 00000000000..ce3afacf20d --- /dev/null +++ b/tests/unit/PcapCaptureAdapterRegistry_1.test @@ -0,0 +1,82 @@ +%description: +Test deterministic PCAP capture adapter registry lookup, protocol resolution, and duplicate rejection. + +%includes: +#include "inet/common/packet/recorder/PcapCaptureAdapterRegistry.h" + +%global: + +using namespace inet; + +#define REQUIRE(...) do { if (!(__VA_ARGS__)) throw cRuntimeError("REQUIRE failed at line %d: %s", __LINE__, #__VA_ARGS__); } while (false) + +class TestCaptureAdapter : public IPcapCaptureAdapter +{ + public: + virtual PcapLinkType getLinkType() const override { return LINKTYPE_RAW; } + virtual std::optional> tryResolvePacket(const Packet *, b frontOffset, b backOffset) const override + { + return std::pair(frontOffset + B(1), backOffset + B(2)); + } + virtual std::vector createRecords(const PcapCaptureObservation&, b, b) const override { return {}; } +}; + +class TestObservationAdapter : public IPcapCaptureObservationAdapter +{ + protected: + const Packet *packet; + + public: + TestObservationAdapter(const Packet *packet) : packet(packet) {} + virtual std::optional tryCreateObservation(const cObject *, Direction direction) const override + { + return PcapCaptureObservation(packet, direction); + } +}; + +%activity: + +PcapCaptureAdapterRegistry registry; +auto protocolAdapter = new TestCaptureAdapter(); +registry.registerProtocolAdapter(&Protocol::udp, protocolAdapter); +REQUIRE(registry.findProtocolAdapter(&Protocol::udp) == protocolAdapter); +REQUIRE(registry.findProtocolAdapter(&Protocol::tcp) == nullptr); + +registry.registerProtocolResolver(&Protocol::ipv4, &Protocol::udp); +Packet packet("packet"); +auto resolution = registry.tryResolveProtocol(&Protocol::ipv4, &packet, B(3), B(4)); +REQUIRE(resolution.has_value()); +REQUIRE(std::get<0>(*resolution) == &Protocol::udp); +REQUIRE(std::get<1>(*resolution) == B(4) && std::get<2>(*resolution) == B(6)); + +bool duplicateProtocolRejected = false; +try { + registry.registerProtocolAdapter(&Protocol::udp, new TestCaptureAdapter()); +} +catch (cRuntimeError&) { + duplicateProtocolRejected = true; +} +REQUIRE(duplicateProtocolRejected); + +Packet first("first"); +Packet second("second"); +registry.registerObservationAdapter("z-last", new TestObservationAdapter(&second)); +registry.registerObservationAdapter("a-first", new TestObservationAdapter(&first)); +auto observation = registry.tryCreateObservation(&second, DIRECTION_INBOUND); +REQUIRE(observation.has_value()); +REQUIRE(observation->packet == &first); +REQUIRE(observation->direction == DIRECTION_INBOUND); + +bool duplicateObservationRejected = false; +try { + registry.registerObservationAdapter("a-first", new TestObservationAdapter(&second)); +} +catch (cRuntimeError&) { + duplicateObservationRejected = true; +} +REQUIRE(duplicateObservationRejected); + +EV << "PCAP capture adapter registry tested successfully.\n"; + +%contains: stdout +PCAP capture adapter registry tested successfully. diff --git a/tests/unit/PcapRecorderIeee80211Ampdu_1.test b/tests/unit/PcapRecorderIeee80211Ampdu_1.test new file mode 100644 index 00000000000..f7fe31e6784 --- /dev/null +++ b/tests/unit/PcapRecorderIeee80211Ampdu_1.test @@ -0,0 +1,222 @@ +%description: +Test opt-in Radiotap link selection and one delimiter-free PCAP record per IEEE 802.11 A-MPDU MPDU. + +%includes: +#include +#include +#include "inet/common/checksum/Checksum.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/common/packet/recorder/PcapRecorder.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" + +%file: TestNetwork.ned + +import inet.common.packet.recorder.PcapRecorder; + +simple TestablePcapRecorder extends PcapRecorder +{ + parameters: + @class(TestablePcapRecorder); +} + +network TestNetwork +{ + submodules: + test: Test; + recorder: TestablePcapRecorder { + pcapFile = ""; + verbose = false; + } +} + +%inifile: omnetpp.ini +[General] +network = TestNetwork + +%global: + +using namespace inet; +using namespace inet::ieee80211; + +#define REQUIRE(...) do { if (!(__VA_ARGS__)) throw cRuntimeError("REQUIRE failed at line %d: %s", __LINE__, #__VA_ARGS__); } while (false) + +class RecordingPcapWriter : public IPcapWriter +{ + public: + std::vector> records; + + virtual void open(const char *, unsigned int, int) override { } + virtual void close() override { } + virtual bool isOpen() const override { return true; } + virtual void setFlush(bool) override { } + + virtual void writePacket(simtime_t, const Packet *packet, b frontOffset, b backOffset, + Direction, NetworkInterface *, PcapLinkType linkType) override + { + REQUIRE(linkType == LINKTYPE_IEEE802_11_RADIOTAP); + auto length = packet->getDataLength() - frontOffset - backOffset; + records.push_back(packet->peekDataAt(frontOffset, length)->getBytes()); + } + + virtual void writePacketWithPrefix(simtime_t, const std::vector& prefix, const Packet *packet, b frontOffset, b backOffset, + Direction, NetworkInterface *, PcapLinkType linkType) override + { + REQUIRE(linkType == LINKTYPE_IEEE802_11_RADIOTAP); + auto bytes = prefix; + auto length = packet->getDataLength() - frontOffset - backOffset; + if (length != b(0)) { + auto packetBytes = packet->peekDataAt(frontOffset, length)->getBytes(); + bytes.insert(bytes.end(), packetBytes.begin(), packetBytes.end()); + } + records.push_back(bytes); + } +}; + +// Compiling these exact overrides protects the pre-existing extension points. +class LegacyOverridePcapRecorder : public PcapRecorder +{ + protected: + virtual void recordPacket(const cPacket *, Direction, cComponent *) override { } + virtual void writePacket(const Protocol *, const Packet *, b, b, Direction, NetworkInterface *) override { } +}; + +class TestablePcapRecorder : public PcapRecorder +{ + public: + void setWriter(IPcapWriter *writer) + { + delete pcapWriter; + pcapWriter = writer; + } + + PcapLinkType getIeee80211LinkType(bool enableAdapters, bool enableConversion) + { + enableProtocolSpecificCaptureAdapters = enableAdapters; + enableConvertingPackets = enableConversion; + return protocolToLinkType(&Protocol::ieee80211Mac); + } + + void writeIeee80211(const Packet *packet, Direction direction) + { + enableProtocolSpecificCaptureAdapters = true; + enableConvertingPackets = true; + writePacket(&Protocol::ieee80211Mac, PcapCaptureObservation(packet, direction), b(0), b(0), nullptr); + } +}; + +Define_Module(TestablePcapRecorder); + +static void appendMpdu(Packet& aggregate, const std::vector& bytes, bool appendPadding) +{ + auto delimiter = makeShared(); + delimiter->setLength(bytes.size() + 4); + aggregate.insertAtBack(delimiter); + aggregate.insertAtBack(makeShared(bytes)); + auto trailer = makeShared(); + trailer->setFcsMode(FCS_COMPUTED); + trailer->setFcs(ethernetFcs(bytes)); + aggregate.insertAtBack(trailer); + auto paddingLength = (4 - (4 + bytes.size() + 4) % 4) % 4; + if (appendPadding && paddingLength != 0) + aggregate.insertAtBack(makeShared(std::vector(paddingLength))); +} + +static void appendZeroLengthDelimiter(Packet& aggregate) +{ + auto delimiter = makeShared(); + delimiter->setLength(0); + aggregate.insertAtBack(delimiter); +} + +static uint16_t readUint16(const std::vector& bytes, size_t offset) +{ + return bytes.at(offset) | bytes.at(offset + 1) << 8; +} + +static uint32_t readUint32(const std::vector& bytes, size_t offset) +{ + uint32_t value = 0; + for (size_t i = 0; i < 4; i++) + value |= static_cast(bytes.at(offset + i)) << (8 * i); + return value; +} + +%activity: + +auto recorder = check_and_cast(getModuleByPath("recorder")); +REQUIRE(recorder->getIeee80211LinkType(false, true) == LINKTYPE_IEEE802_11); +REQUIRE(recorder->getIeee80211LinkType(true, false) == LINKTYPE_IEEE802_11); +REQUIRE(recorder->getIeee80211LinkType(true, true) == LINKTYPE_IEEE802_11_RADIOTAP); + +auto writer = new RecordingPcapWriter(); +recorder->setWriter(writer); + +const std::vector firstMpdu = {0x08, 0x01, 0x02, 0x03, 0x04}; +const std::vector secondMpdu = {0x88, 0x11, 0x12, 0x13, 0x14, 0x15}; +Packet aggregate("ampdu"); +appendMpdu(aggregate, firstMpdu, true); +appendMpdu(aggregate, secondMpdu, false); + +recorder->writeIeee80211(&aggregate, DIRECTION_INBOUND); +REQUIRE(writer->records.size() == 2); +auto expectedReference = static_cast(aggregate.getTreeId()) ^ static_cast(static_cast(aggregate.getTreeId()) >> 32); +for (size_t i = 0; i < writer->records.size(); i++) { + const auto& record = writer->records[i]; + REQUIRE(readUint16(record, 2) == 20); + REQUIRE(record.at(8) == 0x10); // selected MPDU contains the four FCS octets + REQUIRE(readUint32(record, 12) == expectedReference); + REQUIRE(readUint16(record, 16) == (i == 0 ? 0x0004 : 0x000c)); + const auto& expectedMpdu = i == 0 ? firstMpdu : secondMpdu; + REQUIRE(record.size() == 20 + expectedMpdu.size() + 4); + REQUIRE(std::equal(expectedMpdu.begin(), expectedMpdu.end(), record.begin() + 20)); +} + +writer->records.clear(); +recorder->writeIeee80211(&aggregate, DIRECTION_OUTBOUND); +REQUIRE(writer->records.size() == 2); +for (const auto& record : writer->records) { + REQUIRE(readUint16(record, 2) == 12); + REQUIRE((readUint32(record, 4) & (1U << 20)) == 0); + REQUIRE(record.at(8) == 0x10); +} + + +Packet before("before"); +recorder->writeIeee80211(&aggregate, DIRECTION_INBOUND); +Packet after("after"); +REQUIRE(after.getId() == before.getId() + 1); +REQUIRE(after.getTreeId() == before.getTreeId() + 1); + +writer->records.clear(); +Packet eofPadded("eofPadded"); +appendMpdu(eofPadded, {0x08, 0x21, 0x22, 0x23}, false); +appendZeroLengthDelimiter(eofPadded); +recorder->writeIeee80211(&eofPadded, DIRECTION_INBOUND); +REQUIRE(writer->records.size() == 1); + +writer->records.clear(); +Packet onlyEofPadding("onlyEofPadding"); +appendZeroLengthDelimiter(onlyEofPadding); +appendZeroLengthDelimiter(onlyEofPadding); +recorder->writeIeee80211(&onlyEofPadding, DIRECTION_INBOUND); +REQUIRE(writer->records.empty()); + +writer->records.clear(); +Packet malformed("malformed"); +appendMpdu(malformed, {0x08, 0x31, 0x32, 0x33}, false); +auto malformedDelimiter = makeShared(); +malformedDelimiter->setLength(100); +malformed.insertAtBack(malformedDelimiter); +recorder->writeIeee80211(&malformed, DIRECTION_INBOUND); +REQUIRE(writer->records.empty()); + +writer->records.clear(); +Packet trailingPadding("trailingPadding"); +appendMpdu(trailingPadding, firstMpdu, true); +recorder->writeIeee80211(&trailingPadding, DIRECTION_INBOUND); +REQUIRE(writer->records.empty()); + +EV << "PcapRecorder preserved compatibility, neutrality, and A-MPDU boundaries.\n"; + +%contains: stdout +PcapRecorder preserved compatibility, neutrality, and A-MPDU boundaries. diff --git a/tests/unit/PcapRecorderRadiotapHtVht_1.test b/tests/unit/PcapRecorderRadiotapHtVht_1.test new file mode 100644 index 00000000000..47e884a6f47 --- /dev/null +++ b/tests/unit/PcapRecorderRadiotapHtVht_1.test @@ -0,0 +1,147 @@ +%description: +Test legacy Rate, HT MCS, VHT, and FCS Radiotap fields without fabricating unsupported metadata. + +%includes: +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/checksum/Checksum.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HtMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211OfdmMode.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211VhtMode.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Tag_m.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211PhyHeader_m.h" + +%global: + +using namespace inet; +using namespace inet::ieee80211; +using namespace inet::physicallayer; + +#define REQUIRE(...) do { if (!(__VA_ARGS__)) throw cRuntimeError("REQUIRE failed at line %d: %s", __LINE__, #__VA_ARGS__); } while (false) + +static uint16_t readUint16(const std::vector& bytes, size_t offset) +{ + return bytes.at(offset) | bytes.at(offset + 1) << 8; +} + +static uint32_t readUint32(const std::vector& bytes, size_t offset) +{ + uint32_t value = 0; + for (size_t i = 0; i < 4; i++) + value |= static_cast(bytes.at(offset + i)) << (8 * i); + return value; +} + +static std::vector createRadiotapHeader(Ieee80211RadiotapPcapCaptureAdapter& adapter, Packet& packet) +{ + auto records = adapter.createRecords(PcapCaptureObservation(&packet, DIRECTION_UNDEFINED), b(0), b(0)); + REQUIRE(records.size() == 1); + return records.front().getPrefix(); +} + +%activity: + +Ieee80211RadiotapPcapCaptureAdapter adapter; + +Packet phyPacket("phyPacket"); +phyPacket.insertAtBack(makeShared(std::vector{0xfe, 0xed})); +auto phyHeader = makeShared(); +phyHeader->setLengthField(B(6)); +phyPacket.insertAtBack(phyHeader); +phyPacket.insertAtBack(makeShared(std::vector{1, 2, 3, 4, 5, 6})); +phyPacket.insertAtBack(makeShared(std::vector{0xaa, 0xbb, 0xcc})); +phyPacket.addTag()->setProtocol(&Protocol::ieee80211OfdmPhy); +Packet beforeResolve("beforeResolve"); +auto resolved = adapter.tryResolvePacket(&phyPacket, B(2), B(3)); +Packet afterResolve("afterResolve"); +REQUIRE(resolved.has_value()); +REQUIRE(resolved->first == B(7) && resolved->second == B(3)); +REQUIRE(afterResolve.getId() == beforeResolve.getId() + 1); +REQUIRE(afterResolve.getTreeId() == beforeResolve.getTreeId() + 1); + +Packet legacy("legacy"); +legacy.insertAtBack(makeShared(std::vector{1, 2, 3, 4})); +legacy.addTag()->setMode(&Ieee80211OfdmCompliantModes::getCompliantMode(13, MHz(20))); +auto legacyHeader = createRadiotapHeader(adapter, legacy); +REQUIRE(readUint16(legacyHeader, 2) == 10); +REQUIRE(readUint32(legacyHeader, 4) == 0x00000006); // Flags and legacy Rate +REQUIRE(legacyHeader.at(8) == 0 && legacyHeader.at(9) == 12); // no FCS; 6 Mbit/s + +Packet ht("ht"); +ht.insertAtBack(makeShared(std::vector{1, 2, 3, 4})); +ht.addTag()->setMode(Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs0BW20MHz, Ieee80211HtMode::BAND_5GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_LONG)); +auto htHeader = createRadiotapHeader(adapter, ht); +REQUIRE(readUint16(htHeader, 2) == 12); +REQUIRE(readUint32(htHeader, 4) == 0x00080002); // Flags and MCS, no legacy Rate +REQUIRE(htHeader.at(8) == 0); +REQUIRE(htHeader.at(9) == 0x17 && htHeader.at(10) == 0 && htHeader.at(11) == 0); // BW/MCS/GI/BCC, 20 MHz, MCS 0 + +Packet ht40Short("ht40Short"); +ht40Short.insertAtBack(makeShared(std::vector{1})); +ht40Short.addTag()->setMode(Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs7BW40MHz, Ieee80211HtMode::BAND_5GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +auto ht40ShortHeader = createRadiotapHeader(adapter, ht40Short); +REQUIRE(ht40ShortHeader.at(10) == 0x05 && ht40ShortHeader.at(11) == 7); + +Packet vht("vht"); +vht.insertAtBack(makeShared(std::vector{1, 2, 3, 4})); +vht.addTag()->setMode(Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG)); +auto vhtHeader = createRadiotapHeader(adapter, vht); +REQUIRE(readUint16(vhtHeader, 2) == 22); +REQUIRE(readUint32(vhtHeader, 4) == 0x00200002); // Flags and VHT, no legacy Rate +REQUIRE(vhtHeader.at(8) == 0); +REQUIRE(readUint16(vhtHeader, 10) == 0x0044); // GI and bandwidth known +REQUIRE(vhtHeader.at(12) == 0 && vhtHeader.at(13) == 0); // long GI, 20 MHz +REQUIRE(vhtHeader.at(14) == 0x01); // MCS 0, NSS 1 +REQUIRE(vhtHeader.at(18) == 0); // BCC +REQUIRE(vhtHeader.at(19) == 0 && readUint16(vhtHeader, 20) == 0); // unknown Group ID and Partial AID + +Packet vht160ShortNss8("vht160ShortNss8"); +vht160ShortNss8.insertAtBack(makeShared(std::vector{1})); +vht160ShortNss8.addTag()->setMode(Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs9BW160MHzNss8, Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_SHORT)); +auto vht160ShortNss8Header = createRadiotapHeader(adapter, vht160ShortNss8); +REQUIRE(vht160ShortNss8Header.at(12) == 0x04); +REQUIRE(vht160ShortNss8Header.at(13) == 11); +REQUIRE(vht160ShortNss8Header.at(14) == 0x98); + +const std::vector fcsPayload = {1, 2, 3, 4}; +Packet computedMismatch("computedMismatch"); +computedMismatch.insertAtBack(makeShared(fcsPayload)); +auto mismatchedTrailer = makeShared(); +mismatchedTrailer->setFcsMode(FCS_COMPUTED); +mismatchedTrailer->setFcs(ethernetFcs(fcsPayload) ^ 1); +computedMismatch.insertAtBack(mismatchedTrailer); +auto computedMismatchHeader = createRadiotapHeader(adapter, computedMismatch); +REQUIRE(computedMismatchHeader.at(8) == 0x50); // typed FCS is present and authoritatively mismatches + +Packet genericBitError("genericBitError"); +genericBitError.insertAtBack(makeShared(fcsPayload)); +auto correctTrailer = makeShared(); +correctTrailer->setFcsMode(FCS_DECLARED_CORRECT); +genericBitError.insertAtBack(correctTrailer); +genericBitError.setBitError(true); +auto genericBitErrorHeader = createRadiotapHeader(adapter, genericBitError); +REQUIRE(readUint16(genericBitErrorHeader, 2) == 9); +REQUIRE(genericBitErrorHeader.at(8) == 0x10); // generic packet error is not BADFCS + +Packet declaredIncorrect("declaredIncorrect"); +declaredIncorrect.insertAtBack(makeShared(fcsPayload)); +auto incorrectTrailer = makeShared(); +incorrectTrailer->setFcsMode(FCS_DECLARED_INCORRECT); +declaredIncorrect.insertAtBack(incorrectTrailer); +auto declaredIncorrectHeader = createRadiotapHeader(adapter, declaredIncorrect); +REQUIRE(declaredIncorrectHeader.at(8) == 0x50); + +EV << "Legacy, HT, VHT, and FCS Radiotap fields tested successfully.\n"; + +%contains: stdout +Legacy, HT, VHT, and FCS Radiotap fields tested successfully. diff --git a/tests/unit/PcapWriterPrefix_1.test b/tests/unit/PcapWriterPrefix_1.test new file mode 100644 index 00000000000..09e62f513fb --- /dev/null +++ b/tests/unit/PcapWriterPrefix_1.test @@ -0,0 +1,65 @@ +%description: +Test prefixed PCAP and PCAPng writes, including snap length and original length. + +%includes: +#include +#include +#include +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/common/packet/recorder/PcapWriter.h" +#include "inet/common/packet/recorder/PcapngWriter.h" + +%global: + +using namespace inet; + +#define REQUIRE(...) do { if (!(__VA_ARGS__)) throw cRuntimeError("REQUIRE failed at line %d: %s", __LINE__, #__VA_ARGS__); } while (false) + +static std::vector readFile(const char *name) +{ + std::ifstream stream(name, std::ios::binary); + return std::vector(std::istreambuf_iterator(stream), {}); +} + +static uint32_t readUint32(const std::vector& bytes, size_t offset) +{ + uint32_t value = 0; + for (size_t i = 0; i < 4; i++) + value |= static_cast(bytes.at(offset + i)) << (8 * i); + return value; +} + +%activity: + +Packet packet("packet"); +packet.insertAtBack(makeShared(std::vector{0x10, 0x11, 0x12, 0x13})); +const std::vector prefix = {0xaa, 0xbb, 0xcc}; + +PcapWriter pcapWriter; +pcapWriter.open("prefix.pcap", 4, 6); +pcapWriter.writePacketWithPrefix(SIMTIME_ZERO, prefix, &packet, B(1), B(1), DIRECTION_INBOUND, nullptr, LINKTYPE_IEEE802_11_RADIOTAP); +pcapWriter.close(); +auto pcap = readFile("prefix.pcap"); +REQUIRE(readUint32(pcap, 24 + 8) == 4); +REQUIRE(readUint32(pcap, 24 + 12) == 5); +REQUIRE(std::vector(pcap.begin() + 40, pcap.begin() + 44) == std::vector({0xaa, 0xbb, 0xcc, 0x11})); + +NetworkInterface networkInterface; +PcapngWriter pcapngWriter; +pcapngWriter.open("prefix.pcapng", 4, 6); +pcapngWriter.writePacketWithPrefix(SIMTIME_ZERO, prefix, &packet, B(1), B(1), DIRECTION_INBOUND, &networkInterface, LINKTYPE_IEEE802_11_RADIOTAP); +pcapngWriter.close(); +auto pcapng = readFile("prefix.pcapng"); +size_t interfaceBlock = 28; +REQUIRE(readUint32(pcapng, interfaceBlock + 12) == 4); +size_t packetBlock = interfaceBlock + readUint32(pcapng, interfaceBlock + 4); +REQUIRE(readUint32(pcapng, packetBlock + 20) == 4); +REQUIRE(readUint32(pcapng, packetBlock + 24) == 5); +REQUIRE(std::vector(pcapng.begin() + packetBlock + 28, pcapng.begin() + packetBlock + 32) == std::vector({0xaa, 0xbb, 0xcc, 0x11})); + +std::remove("prefix.pcap"); +std::remove("prefix.pcapng"); +EV << "Prefixed PCAP writers honor prefix ordering, snaplen, and original length.\n"; + +%contains: stdout +Prefixed PCAP writers honor prefix ordering, snaplen, and original length. diff --git a/tests/unit/WirelessPcapCaptureObservationAdapter_1.test b/tests/unit/WirelessPcapCaptureObservationAdapter_1.test new file mode 100644 index 00000000000..2877efcc79a --- /dev/null +++ b/tests/unit/WirelessPcapCaptureObservationAdapter_1.test @@ -0,0 +1,108 @@ +%description: +Test wireless PCAP observation translation for signals, transmissions, receptions, and unsupported objects. + +%includes: +#include "inet/physicallayer/common/Signal.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmission.h" +#include "inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; +#define REQUIRE(...) do { if (!(__VA_ARGS__)) throw cRuntimeError("REQUIRE failed at line %d: %s", __LINE__, #__VA_ARGS__); } while (false) + +class FakeTransmission : public cObject, public ITransmission +{ + public: + const Packet *packet; + FakeTransmission(const Packet *packet) : packet(packet) {} + virtual std::ostream& printToStream(std::ostream& stream, int, int = 0) const override { return stream; } + virtual int getId() const override { return 1; } + virtual const IRadio *getTransmitterRadio() const override { return nullptr; } + virtual int getTransmitterRadioId() const override { return -1; } + virtual const IAntennaGain *getTransmitterAntennaGain() const override { return nullptr; } + virtual const IRadioMedium *getMedium() const override { return nullptr; } + virtual const Packet *getPacket() const override { return packet; } + virtual const simtime_t getStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getStartTime(IRadioSignal::SignalPart) const override { return SIMTIME_ZERO; } + virtual const simtime_t getEndTime(IRadioSignal::SignalPart) const override { return SIMTIME_ZERO; } + virtual const simtime_t getPreambleStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getPreambleEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getHeaderStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getHeaderEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDataStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDataEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDuration() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDuration(IRadioSignal::SignalPart) const override { return SIMTIME_ZERO; } + virtual const simtime_t getPreambleDuration() const override { return SIMTIME_ZERO; } + virtual const simtime_t getHeaderDuration() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDataDuration() const override { return SIMTIME_ZERO; } + virtual const Coord& getStartPosition() const override { static Coord value; return value; } + virtual const Coord& getEndPosition() const override { static Coord value; return value; } + virtual const Quaternion& getStartOrientation() const override { static Quaternion value; return value; } + virtual const Quaternion& getEndOrientation() const override { static Quaternion value; return value; } + virtual const ITransmissionPacketModel *getPacketModel() const override { return nullptr; } + virtual const ITransmissionBitModel *getBitModel() const override { return nullptr; } + virtual const ITransmissionSymbolModel *getSymbolModel() const override { return nullptr; } + virtual const ITransmissionSampleModel *getSampleModel() const override { return nullptr; } + virtual const ITransmissionAnalogModel *getAnalogModel() const override { return nullptr; } +}; + +class FakeReception : public cObject, public IReception +{ + public: + const ITransmission *transmission; + FakeReception(const ITransmission *transmission) : transmission(transmission) {} + virtual std::ostream& printToStream(std::ostream& stream, int, int = 0) const override { return stream; } + virtual const IRadio *getReceiverRadio() const override { return nullptr; } + virtual const ITransmission *getTransmission() const override { return transmission; } + virtual const simtime_t getStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getStartTime(IRadioSignal::SignalPart) const override { return SIMTIME_ZERO; } + virtual const simtime_t getEndTime(IRadioSignal::SignalPart) const override { return SIMTIME_ZERO; } + virtual const simtime_t getPreambleStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getPreambleEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getHeaderStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getHeaderEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDataStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDataEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDuration() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDuration(IRadioSignal::SignalPart) const override { return SIMTIME_ZERO; } + virtual const simtime_t getPreambleDuration() const override { return SIMTIME_ZERO; } + virtual const simtime_t getHeaderDuration() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDataDuration() const override { return SIMTIME_ZERO; } + virtual const Coord& getStartPosition() const override { static Coord value; return value; } + virtual const Coord& getEndPosition() const override { static Coord value; return value; } + virtual const Quaternion& getStartOrientation() const override { static Quaternion value; return value; } + virtual const Quaternion& getEndOrientation() const override { static Quaternion value; return value; } + virtual const IReceptionAnalogModel *getAnalogModel() const override { return nullptr; } +}; + +%activity: +WirelessPcapCaptureObservationAdapter adapter; +Packet packet("packet"); +Signal signal("signal"); +signal.encapsulate(packet.dup()); +auto signalObservation = adapter.tryCreateObservation(&signal, DIRECTION_OUTBOUND); +REQUIRE(signalObservation && signalObservation->packet == signal.getEncapsulatedPacket()); +REQUIRE(signalObservation->direction == DIRECTION_OUTBOUND && signalObservation->transmission == nullptr && signalObservation->reception == nullptr); + +FakeTransmission transmission(&packet); +auto transmissionObservation = adapter.tryCreateObservation(&transmission, DIRECTION_OUTBOUND); +REQUIRE(transmissionObservation && transmissionObservation->packet == &packet); +REQUIRE(transmissionObservation->transmission == &transmission && transmissionObservation->reception == nullptr); + +FakeReception reception(&transmission); +auto receptionObservation = adapter.tryCreateObservation(&reception, DIRECTION_INBOUND); +REQUIRE(receptionObservation && receptionObservation->packet == &packet); +REQUIRE(receptionObservation->direction == DIRECTION_INBOUND); +REQUIRE(receptionObservation->transmission == &transmission && receptionObservation->reception == &reception); + +cObject unsupported; +REQUIRE(!adapter.tryCreateObservation(&unsupported, DIRECTION_UNDEFINED)); +EV << "Wireless PCAP observation translation tested successfully.\n"; + +%contains: stdout +Wireless PCAP observation translation tested successfully. From 60b667c5eea318fd1a0c69041a13444040c09882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 17 Aug 2026 00:09:43 +0200 Subject: [PATCH 2/8] pcap: handle padded A-MPDUs and short PSDUs Accept an A-MPDU whose final MPDU is followed by exactly the required alignment padding. The equality case is structurally complete for VHT/HE-family aggregates and must not discard every MPDU from the capture. Replace the unrelated PHY payload-length versus header-length comparison with a positive-length check. This permits short nonempty PSDUs while keeping zero-length VHT NDPs and negative lengths out of MAC-frame resolution. Add focused regression coverage for terminal padding, emitted MPDU data and FCS, short OFDM payload resolution, and zero or negative PHY length rejection. --- .../Ieee80211RadiotapPcapCaptureAdapter.cc | 6 +++-- tests/unit/PcapRecorderIeee80211Ampdu_1.test | 16 +++++++++++- tests/unit/PcapRecorderRadiotapHtVht_1.test | 25 +++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc index 4b19db61554..0b064c978c5 100644 --- a/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc +++ b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc @@ -193,7 +193,9 @@ AmpduParseResult getIeee80211AmpduMpduRanges(const Packet *packet, b frontOffset if (offset == endOffset) return AmpduParseResult::VALID; auto paddingLength = B((4 - (delimiter->getChunkLength() + mpduLength).get() % 4) % 4); - if (offset + paddingLength >= endOffset) + // IEEE 802.11-2024, 9.7.1 and 10.12.6 permit exact final-subframe alignment padding for VHT/HE-family PPDUs. + // Without PHY-mode provenance, accept the structurally complete equality case instead of discarding its MPDUs. + if (offset + paddingLength > endOffset) return AmpduParseResult::INVALID; offset += paddingLength; } @@ -435,7 +437,7 @@ std::optional> Ieee80211RadiotapPcapCaptureAdapter::tryResolvePa else return std::nullopt; if (header->isIncorrect() || header->isIncomplete() || header->isImproperlyRepresented() || - b(header->getLengthField()) < header->getChunkLength()) + b(header->getLengthField()) <= b(0)) return std::nullopt; auto resolvedFrontOffset = frontOffset + header->getChunkLength(); auto payloadLength = b(header->getLengthField()); diff --git a/tests/unit/PcapRecorderIeee80211Ampdu_1.test b/tests/unit/PcapRecorderIeee80211Ampdu_1.test index f7fe31e6784..d0547d3684c 100644 --- a/tests/unit/PcapRecorderIeee80211Ampdu_1.test +++ b/tests/unit/PcapRecorderIeee80211Ampdu_1.test @@ -141,6 +141,14 @@ static uint32_t readUint32(const std::vector& bytes, size_t offset) return value; } +static uint32_t readBigEndianUint32(const std::vector& bytes, size_t offset) +{ + uint32_t value = 0; + for (size_t i = 0; i < 4; i++) + value = (value << 8) | bytes.at(offset + i); + return value; +} + %activity: auto recorder = check_and_cast(getModuleByPath("recorder")); @@ -214,7 +222,13 @@ writer->records.clear(); Packet trailingPadding("trailingPadding"); appendMpdu(trailingPadding, firstMpdu, true); recorder->writeIeee80211(&trailingPadding, DIRECTION_INBOUND); -REQUIRE(writer->records.empty()); +REQUIRE(writer->records.size() == 1); +const auto& trailingPaddingRecord = writer->records.front(); +REQUIRE(readUint16(trailingPaddingRecord, 2) == 20); +REQUIRE(trailingPaddingRecord.at(8) == 0x10); +REQUIRE(trailingPaddingRecord.size() == 20 + firstMpdu.size() + 4); +REQUIRE(std::equal(firstMpdu.begin(), firstMpdu.end(), trailingPaddingRecord.begin() + 20)); +REQUIRE(readBigEndianUint32(trailingPaddingRecord, 20 + firstMpdu.size()) == ethernetFcs(firstMpdu)); EV << "PcapRecorder preserved compatibility, neutrality, and A-MPDU boundaries.\n"; diff --git a/tests/unit/PcapRecorderRadiotapHtVht_1.test b/tests/unit/PcapRecorderRadiotapHtVht_1.test index 47e884a6f47..28e5ba402e4 100644 --- a/tests/unit/PcapRecorderRadiotapHtVht_1.test +++ b/tests/unit/PcapRecorderRadiotapHtVht_1.test @@ -61,6 +61,31 @@ REQUIRE(resolved->first == B(7) && resolved->second == B(3)); REQUIRE(afterResolve.getId() == beforeResolve.getId() + 1); REQUIRE(afterResolve.getTreeId() == beforeResolve.getTreeId() + 1); +Packet shortPhyPacket("shortPhyPacket"); +auto shortPhyHeader = makeShared(); +shortPhyHeader->setLengthField(B(1)); +shortPhyPacket.insertAtBack(shortPhyHeader); +shortPhyPacket.insertAtBack(makeShared(std::vector{0xaa})); +shortPhyPacket.addTag()->setProtocol(&Protocol::ieee80211OfdmPhy); +auto shortResolved = adapter.tryResolvePacket(&shortPhyPacket, b(0), b(0)); +REQUIRE(shortResolved.has_value()); +REQUIRE(shortResolved->first == B(5) && shortResolved->second == b(0)); + +Packet zeroLengthPhyPacket("zeroLengthPhyPacket"); +auto zeroLengthPhyHeader = makeShared(); +zeroLengthPhyHeader->setChunkLength(B(5)); +zeroLengthPhyHeader->setLengthField(B(0)); +zeroLengthPhyPacket.insertAtBack(zeroLengthPhyHeader); +zeroLengthPhyPacket.addTag()->setProtocol(&Protocol::ieee80211VhtPhy); +REQUIRE(!adapter.tryResolvePacket(&zeroLengthPhyPacket, b(0), b(0)).has_value()); + +Packet negativeLengthPhyPacket("negativeLengthPhyPacket"); +auto negativeLengthPhyHeader = makeShared(); +negativeLengthPhyHeader->setLengthField(B(-1)); +negativeLengthPhyPacket.insertAtBack(negativeLengthPhyHeader); +negativeLengthPhyPacket.addTag()->setProtocol(&Protocol::ieee80211OfdmPhy); +REQUIRE(!adapter.tryResolvePacket(&negativeLengthPhyPacket, b(0), b(0)).has_value()); + Packet legacy("legacy"); legacy.insertAtBack(makeShared(std::vector{1, 2, 3, 4})); legacy.addTag()->setMode(&Ieee80211OfdmCompliantModes::getCompliantMode(13, MHz(20))); From 266eecdfd43949bcda1ba3b5052bb8b4b02d05ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 17 Aug 2026 00:37:27 +0200 Subject: [PATCH 3/8] pcap: preserve malformed 802.11 captures Fall back to a single whole-PSDU Radiotap record when A-MPDU parsing fails or produces no MPDU ranges. This prevents damaged, unusual, and delimiter-only PSDUs from disappearing from capture output and keeps recorder counters aligned with written observations. Preserve established FCS presence when computed-FCS validation cannot serialize the preceding payload, without guessing FCS provenance from untyped byte chunks or fabricating a BADFCS result. Extend the focused recorder tests to verify complete PSDU byte retention for malformed and empty aggregates, retain normal aggregate splitting, and cover the FCS serialization-failure path. --- .../Ieee80211RadiotapPcapCaptureAdapter.cc | 10 ++++------ tests/unit/PcapRecorderIeee80211Ampdu_1.test | 14 ++++++++++++-- tests/unit/PcapRecorderRadiotapHtVht_1.test | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc index 0b064c978c5..dbaea25b1bf 100644 --- a/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc +++ b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc @@ -217,11 +217,11 @@ FcsMetadata getIeee80211FcsMetadata(const Packet *packet, b frontOffset, b backO auto endOffset = packet->getDataLength() - backOffset; if (endOffset - frontOffset < B(4)) return {}; + FcsMetadata metadata; try { auto trailer = dynamicPtrCast(packet->peekDataAt(endOffset - B(4), B(4))); if (trailer == nullptr) - return {}; - FcsMetadata metadata; + return metadata; metadata.isPresent = true; switch (trailer->getFcsMode()) { case FCS_DECLARED_INCORRECT: @@ -239,7 +239,7 @@ FcsMetadata getIeee80211FcsMetadata(const Packet *packet, b frontOffset, b backO return metadata; } catch (cRuntimeError&) { - return {}; + return metadata; } } @@ -461,9 +461,7 @@ std::vector Ieee80211RadiotapPcapCaptureAdapter::createRecord std::vector mpduRanges; auto ampduParseResult = getIeee80211AmpduMpduRanges(packet, frontOffset, backOffset, mpduRanges); - if (ampduParseResult == AmpduParseResult::INVALID) - return {}; - if (ampduParseResult == AmpduParseResult::VALID) { + if (ampduParseResult == AmpduParseResult::VALID && !mpduRanges.empty()) { std::vector records; records.reserve(mpduRanges.size()); auto ampduReference = makeAmpduReference(packet); diff --git a/tests/unit/PcapRecorderIeee80211Ampdu_1.test b/tests/unit/PcapRecorderIeee80211Ampdu_1.test index d0547d3684c..78d526312e3 100644 --- a/tests/unit/PcapRecorderIeee80211Ampdu_1.test +++ b/tests/unit/PcapRecorderIeee80211Ampdu_1.test @@ -149,6 +149,14 @@ static uint32_t readBigEndianUint32(const std::vector& bytes, size_t of return value; } +static void requireWholePacketRecord(const std::vector& record, const Packet& packet) +{ + auto radiotapLength = readUint16(record, 2); + auto packetBytes = packet.peekData()->getBytes(); + REQUIRE(record.size() == radiotapLength + packetBytes.size()); + REQUIRE(std::equal(packetBytes.begin(), packetBytes.end(), record.begin() + radiotapLength)); +} + %activity: auto recorder = check_and_cast(getModuleByPath("recorder")); @@ -207,7 +215,8 @@ Packet onlyEofPadding("onlyEofPadding"); appendZeroLengthDelimiter(onlyEofPadding); appendZeroLengthDelimiter(onlyEofPadding); recorder->writeIeee80211(&onlyEofPadding, DIRECTION_INBOUND); -REQUIRE(writer->records.empty()); +REQUIRE(writer->records.size() == 1); +requireWholePacketRecord(writer->records.front(), onlyEofPadding); writer->records.clear(); Packet malformed("malformed"); @@ -216,7 +225,8 @@ auto malformedDelimiter = makeShared(); malformedDelimiter->setLength(100); malformed.insertAtBack(malformedDelimiter); recorder->writeIeee80211(&malformed, DIRECTION_INBOUND); -REQUIRE(writer->records.empty()); +REQUIRE(writer->records.size() == 1); +requireWholePacketRecord(writer->records.front(), malformed); writer->records.clear(); Packet trailingPadding("trailingPadding"); diff --git a/tests/unit/PcapRecorderRadiotapHtVht_1.test b/tests/unit/PcapRecorderRadiotapHtVht_1.test index 28e5ba402e4..93d361d731a 100644 --- a/tests/unit/PcapRecorderRadiotapHtVht_1.test +++ b/tests/unit/PcapRecorderRadiotapHtVht_1.test @@ -3,6 +3,7 @@ Test legacy Rate, HT MCS, VHT, and FCS Radiotap fields without fabricating unsup %includes: #include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/common/packet/chunk/FieldsChunk.h" #include "inet/common/ProtocolTag_m.h" #include "inet/common/checksum/Checksum.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" @@ -21,6 +22,16 @@ using namespace inet::physicallayer; #define REQUIRE(...) do { if (!(__VA_ARGS__)) throw cRuntimeError("REQUIRE failed at line %d: %s", __LINE__, #__VA_ARGS__); } while (false) +class UnserializableFieldsChunk : public FieldsChunk +{ + public: + UnserializableFieldsChunk() { setChunkLength(B(4)); } + UnserializableFieldsChunk(const UnserializableFieldsChunk& other) = default; + + virtual UnserializableFieldsChunk *dup() const override { return new UnserializableFieldsChunk(*this); } + virtual const Ptr dupShared() const override { return makeShared(*this); } +}; + static uint16_t readUint16(const std::vector& bytes, size_t offset) { return bytes.at(offset) | bytes.at(offset + 1) << 8; @@ -166,6 +177,14 @@ declaredIncorrect.insertAtBack(incorrectTrailer); auto declaredIncorrectHeader = createRadiotapHeader(adapter, declaredIncorrect); REQUIRE(declaredIncorrectHeader.at(8) == 0x50); +Packet unserializablePayload("unserializablePayload"); +unserializablePayload.insertAtBack(makeShared()); +auto unserializablePayloadTrailer = makeShared(); +unserializablePayloadTrailer->setFcsMode(FCS_COMPUTED); +unserializablePayload.insertAtBack(unserializablePayloadTrailer); +auto unserializablePayloadHeader = createRadiotapHeader(adapter, unserializablePayload); +REQUIRE(unserializablePayloadHeader.at(8) == 0x10); // typed FCS remains present when payload comparison fails + EV << "Legacy, HT, VHT, and FCS Radiotap fields tested successfully.\n"; %contains: stdout From 2cca845aaed655423e8894bbb92b8f3543ad998e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 17 Aug 2026 11:01:16 +0200 Subject: [PATCH 4/8] pcap: trust computed IEEE 802.11 FCS metadata Avoid serializing every captured MPDU and recomputing its FCS when the typed trailer is marked FCS_COMPUTED. Standard INET capture paths calculate that trailer after the final MAC fields are set, so the adapter now treats it as present and trusted while continuing to map FCS_DECLARED_INCORRECT to Radiotap BADFCS. Document the capture design decisions raised during review: typed-chunk requirements for FCS and A-MPDU recognition, aggregate padding and whole-PSDU fallback, Radiotap field layout, adapter registry uniqueness and lifecycle, protocol-adapter record semantics, wireless observation fallback, and PCAPng interface and snaplen handling. Update the Radiotap FCS unit coverage so a deliberately mismatching computed value remains trusted, and remove the obsolete serialization-failure fixture. Verified with the release build and the focused PCAP adapter, A-MPDU, registry, and writer-prefix unit tests. --- .../recorder/PcapCaptureAdapterRegistry.cc | 4 ++++ .../common/packet/recorder/PcapRecorder.cc | 8 +++++++ .../common/packet/recorder/PcapngWriter.cc | 4 ++++ .../Ieee80211RadiotapPcapCaptureAdapter.cc | 19 +++++++++++------ .../WirelessPcapCaptureObservationAdapter.cc | 2 ++ tests/unit/PcapRecorderRadiotapHtVht_1.test | 21 +------------------ 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc index aca2f9d3f0e..fc6c75f6792 100644 --- a/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc +++ b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc @@ -16,6 +16,8 @@ PcapCaptureAdapterRegistry::~PcapCaptureAdapterRegistry() delete entry.second; } +// Each protocol, resolver key, and observation key has one owner per network setup. Silently +// replacing an entry would make capture behavior depend on registration order, so conflicts fail fast. void PcapCaptureAdapterRegistry::registerProtocolAdapter(const Protocol *protocol, const IPcapCaptureAdapter *adapter) { if (protocol == nullptr || adapter == nullptr || protocolAdapters.find(protocol) != protocolAdapters.end()) { @@ -71,6 +73,8 @@ std::optional PcapCaptureAdapterRegistry::tryCreateObser PcapCaptureAdapterRegistry& PcapCaptureAdapterRegistry::getInstance() { + // SharedDataManager scopes the registry to the current network lifecycle, allowing the + // pre-network registration fragments to run again after the previous network is deleted. static int handle = cSimulationOrSharedDataManager::registerSharedVariableName("inet::PcapCaptureAdapterRegistry::instance"); return getSimulationOrSharedDataManager()->getSharedVariable(handle); } diff --git a/src/inet/common/packet/recorder/PcapRecorder.cc b/src/inet/common/packet/recorder/PcapRecorder.cc index 16becbbe245..326d08b3317 100644 --- a/src/inet/common/packet/recorder/PcapRecorder.cc +++ b/src/inet/common/packet/recorder/PcapRecorder.cc @@ -180,6 +180,8 @@ void PcapRecorder::receiveSignal(cComponent *source, simsignal_t signalID, cObje auto observation = PcapCaptureAdapterRegistry::getInstance().tryCreateObservation(obj, direction); if (observation.has_value()) recordPacket(*observation, source); + // Observation adapters are optional enrichers. If none accepts the object, retain the + // generic cPacket path; non-INET packet payloads eventually remain unrecorded as before. else if (auto packet = dynamic_cast(obj)) recordPacket(packet, direction, source); } @@ -191,13 +193,19 @@ void PcapRecorder::writePacket(const Protocol *protocol, const PcapCaptureObserv if (enableProtocolSpecificCaptureAdapters && enableConvertingPackets) { auto adapter = PcapCaptureAdapterRegistry::getInstance().findProtocolAdapter(protocol); if (adapter != nullptr) { + // A protocol adapter owns its output link type and complete record layout, so its + // records bypass the generic link-type matching and packet-conversion helpers below. auto records = adapter->createRecords(observation, frontOffset, backOffset); for (const auto& record : records) { auto dataLength = packet->getDataLength() - record.frontOffset - record.backOffset; + // A protocol-specific prefix is meaningful capture data, so a prefix-only record + // is not considered empty even when recordEmptyPackets is false. if (recordEmptyPackets || !record.getPrefix().empty() || dataLength != b(0)) { pcapWriter->writePacketWithPrefix(simTime(), record.getPrefix(), packet, record.frontOffset, record.backOffset, observation.direction, networkInterface, adapter->getLinkType()); numRecorded++; + // Emit once per written record, but retain the original observed packet as the + // signal value; split records such as A-MPDU MPDUs therefore share that value. emit(packetRecordedSignal, packet); } } diff --git a/src/inet/common/packet/recorder/PcapngWriter.cc b/src/inet/common/packet/recorder/PcapngWriter.cc index 3085a25dffe..7aead3e32c9 100644 --- a/src/inet/common/packet/recorder/PcapngWriter.cc +++ b/src/inet/common/packet/recorder/PcapngWriter.cc @@ -209,6 +209,8 @@ void PcapngWriter::writePacketWithPrefix(simtime_t stime, const std::vectorgetDataLength() - frontOffset - backOffset; size_t originalLength = prefix.size() + packetLength.get(); + // Advertise and enforce the configured snaplen for PCAPng too. The captured length is + // truncated, while originalPacketLength below retains the complete untruncated record length. size_t capturedLength = std::min(originalLength, snaplen); uint32_t optionsLength = (4 + 4) + 4; uint32_t blockTotalLength = 32 + roundUp(capturedLength) + optionsLength; diff --git a/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc index dbaea25b1bf..46ce531577b 100644 --- a/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc +++ b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc @@ -14,8 +14,6 @@ #include "inet/common/INETMath.h" #include "inet/common/ProtocolTag_m.h" -#include "inet/common/checksum/Checksum.h" -#include "inet/common/packet/chunk/BytesChunk.h" #include "inet/common/packet/recorder/PcapCaptureAdapterRegistry.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" #include "inet/physicallayer/wireless/common/contract/packetlevel/INarrowbandSignalAnalogModel.h" @@ -164,6 +162,8 @@ AmpduParseResult getIeee80211AmpduMpduRanges(const Packet *packet, b frontOffset if (frontOffset + ieee80211::LENGTH_A_MPDU_SUBFRAME_HEADER > endOffset) return AmpduParseResult::NOT_AGGREGATE; + // Delimiters are recognized only as typed chunks at their exact boundaries. A serialized + // BytesChunk is deliberately not guessed to be an aggregate; it is captured as one PSDU below. auto peekDelimiter = [&] (b offset) { return dynamicPtrCast(packet->peekDataAt(offset, b(-1), parsingFlags)); }; @@ -193,6 +193,7 @@ AmpduParseResult getIeee80211AmpduMpduRanges(const Packet *packet, b frontOffset if (offset == endOffset) return AmpduParseResult::VALID; auto paddingLength = B((4 - (delimiter->getChunkLength() + mpduLength).get() % 4) % 4); + // This mirrors MpduAggregation::aggregateFrames(): pad between MPDUs, but not after the last one. // IEEE 802.11-2024, 9.7.1 and 10.12.6 permit exact final-subframe alignment padding for VHT/HE-family PPDUs. // Without PHY-mode provenance, accept the structurally complete equality case instead of discarding its MPDUs. if (offset + paddingLength > endOffset) @@ -219,6 +220,8 @@ FcsMetadata getIeee80211FcsMetadata(const Packet *packet, b frontOffset, b backO return {}; FcsMetadata metadata; try { + // A typed trailer is authoritative evidence that the final four octets are an FCS. For a + // raw BytesChunk they may instead be payload, so the adapter does not infer FCS presence. auto trailer = dynamicPtrCast(packet->peekDataAt(endOffset - B(4), B(4))); if (trailer == nullptr) return metadata; @@ -227,11 +230,11 @@ FcsMetadata getIeee80211FcsMetadata(const Packet *packet, b frontOffset, b backO case FCS_DECLARED_INCORRECT: metadata.isBad = true; break; - case FCS_COMPUTED: { - auto data = packet->peekDataAt(frontOffset, endOffset - frontOffset - trailer->getChunkLength()); - metadata.isBad = ethernetFcs(data->getBytes()) != trailer->getFcs(); + case FCS_COMPUTED: + // On standard INET capture paths, a typed FCS_COMPUTED trailer was produced by INET + // after the final MAC fields were set. Trust it instead of serializing the MPDU and + // repeating the linear-time FCS calculation solely for packet capture. break; - } case FCS_DECLARED_CORRECT: default: break; @@ -338,6 +341,8 @@ RadiotapPpduFields extractRadiotapPpduFields(const Packet *packet, Direction dir std::vector serializeRadiotapHeader(const RadiotapPpduFields& fields, const RadiotapRecordMetadata& metadata) { + // Fields are appended in increasing present-bit order. Padding is relative to the beginning + // of this buffer, which already contains the fixed eight-octet Radiotap header. uint32_t present = 0; auto setPresentBit = [&] (RadiotapPresentBit bit) { present |= 1U << bit; }; std::vector bytes(8, 0); @@ -482,6 +487,8 @@ std::vector Ieee80211RadiotapPcapCaptureAdapter::createRecord return records; } + // Malformed aggregates and delimiter-only input still represent an observed wireless frame. + // Preserve it as one whole-PSDU record instead of silently producing no capture records. RadiotapRecordMetadata metadata; auto fcsMetadata = getIeee80211FcsMetadata(packet, frontOffset, backOffset); metadata.hasFcs = fcsMetadata.isPresent; diff --git a/src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.cc b/src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.cc index e7289f55c47..79b233dddb0 100644 --- a/src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.cc +++ b/src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.cc @@ -20,6 +20,8 @@ std::optional WirelessPcapCaptureObservationAdapter::try { if (auto signal = dynamic_cast(object)) { auto packet = dynamic_cast(signal->getEncapsulatedPacket()); + // Returning nullopt keeps the recorder's generic cPacket fallback available. If the + // encapsulated object is not an INET Packet, that fallback ignores it as before. return packet != nullptr ? std::optional(PcapCaptureObservation(packet, direction)) : std::nullopt; } else if (auto transmission = dynamic_cast(object)) diff --git a/tests/unit/PcapRecorderRadiotapHtVht_1.test b/tests/unit/PcapRecorderRadiotapHtVht_1.test index 93d361d731a..fee5e792c14 100644 --- a/tests/unit/PcapRecorderRadiotapHtVht_1.test +++ b/tests/unit/PcapRecorderRadiotapHtVht_1.test @@ -3,7 +3,6 @@ Test legacy Rate, HT MCS, VHT, and FCS Radiotap fields without fabricating unsup %includes: #include "inet/common/packet/chunk/BytesChunk.h" -#include "inet/common/packet/chunk/FieldsChunk.h" #include "inet/common/ProtocolTag_m.h" #include "inet/common/checksum/Checksum.h" #include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" @@ -22,16 +21,6 @@ using namespace inet::physicallayer; #define REQUIRE(...) do { if (!(__VA_ARGS__)) throw cRuntimeError("REQUIRE failed at line %d: %s", __LINE__, #__VA_ARGS__); } while (false) -class UnserializableFieldsChunk : public FieldsChunk -{ - public: - UnserializableFieldsChunk() { setChunkLength(B(4)); } - UnserializableFieldsChunk(const UnserializableFieldsChunk& other) = default; - - virtual UnserializableFieldsChunk *dup() const override { return new UnserializableFieldsChunk(*this); } - virtual const Ptr dupShared() const override { return makeShared(*this); } -}; - static uint16_t readUint16(const std::vector& bytes, size_t offset) { return bytes.at(offset) | bytes.at(offset + 1) << 8; @@ -157,7 +146,7 @@ mismatchedTrailer->setFcsMode(FCS_COMPUTED); mismatchedTrailer->setFcs(ethernetFcs(fcsPayload) ^ 1); computedMismatch.insertAtBack(mismatchedTrailer); auto computedMismatchHeader = createRadiotapHeader(adapter, computedMismatch); -REQUIRE(computedMismatchHeader.at(8) == 0x50); // typed FCS is present and authoritatively mismatches +REQUIRE(computedMismatchHeader.at(8) == 0x10); // computed FCS is present and trusted without verification Packet genericBitError("genericBitError"); genericBitError.insertAtBack(makeShared(fcsPayload)); @@ -177,14 +166,6 @@ declaredIncorrect.insertAtBack(incorrectTrailer); auto declaredIncorrectHeader = createRadiotapHeader(adapter, declaredIncorrect); REQUIRE(declaredIncorrectHeader.at(8) == 0x50); -Packet unserializablePayload("unserializablePayload"); -unserializablePayload.insertAtBack(makeShared()); -auto unserializablePayloadTrailer = makeShared(); -unserializablePayloadTrailer->setFcsMode(FCS_COMPUTED); -unserializablePayload.insertAtBack(unserializablePayloadTrailer); -auto unserializablePayloadHeader = createRadiotapHeader(adapter, unserializablePayload); -REQUIRE(unserializablePayloadHeader.at(8) == 0x10); // typed FCS remains present when payload comparison fails - EV << "Legacy, HT, VHT, and FCS Radiotap fields tested successfully.\n"; %contains: stdout From 0037dba9da5e907174ed1642de43c0ed86fd52f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 17 Aug 2026 11:46:32 +0200 Subject: [PATCH 5/8] Fix adapter-backed PCAP writes through generic path When protocol-specific capture adapters were enabled, protocolToLinkType() selected the adapter link type but the generic packet overload still used the legacy match-and-convert path. IEEE 802.11 therefore selected Radiotap, failed the link-type match, and aborted because no generic Radiotap converter exists. Delegate adapter-backed packet writes to the existing observation overload so the adapter creates complete prefixed records. Preserve the legacy matching and conversion behavior when either feature flag is disabled or no adapter is registered. Extend PcapRecorderIeee80211Ampdu_1 with a direct packet-overload regression that verifies the Radiotap header fields and exact payload suffix. This catches both the original abort and invalid prefix-less link-type-127 output. Validation: release and debug builds pass; the focused PcapRecorder regression passes. Both PcapRecorder tests also pass in the broader unit run, whose remaining failures match pre-existing unrelated failures. --- .../common/packet/recorder/PcapRecorder.cc | 5 +++++ tests/unit/PcapRecorderIeee80211Ampdu_1.test | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/inet/common/packet/recorder/PcapRecorder.cc b/src/inet/common/packet/recorder/PcapRecorder.cc index 326d08b3317..fae68b2ee8b 100644 --- a/src/inet/common/packet/recorder/PcapRecorder.cc +++ b/src/inet/common/packet/recorder/PcapRecorder.cc @@ -218,6 +218,11 @@ void PcapRecorder::writePacket(const Protocol *protocol, const PcapCaptureObserv void PcapRecorder::writePacket(const Protocol *protocol, const Packet *packet, b frontOffset, b backOffset, Direction direction, NetworkInterface *networkInterface) { + if (enableProtocolSpecificCaptureAdapters && enableConvertingPackets && + PcapCaptureAdapterRegistry::getInstance().findProtocolAdapter(protocol) != nullptr) { + writePacket(protocol, PcapCaptureObservation(packet, direction), frontOffset, backOffset, networkInterface); + return; + } auto pcapLinkType = protocolToLinkType(protocol); if (pcapLinkType == LINKTYPE_INVALID) diff --git a/tests/unit/PcapRecorderIeee80211Ampdu_1.test b/tests/unit/PcapRecorderIeee80211Ampdu_1.test index 78d526312e3..e11ab2ca501 100644 --- a/tests/unit/PcapRecorderIeee80211Ampdu_1.test +++ b/tests/unit/PcapRecorderIeee80211Ampdu_1.test @@ -102,6 +102,13 @@ class TestablePcapRecorder : public PcapRecorder enableConvertingPackets = true; writePacket(&Protocol::ieee80211Mac, PcapCaptureObservation(packet, direction), b(0), b(0), nullptr); } + + void writeIeee80211Packet(const Packet *packet, Direction direction) + { + enableProtocolSpecificCaptureAdapters = true; + enableConvertingPackets = true; + writePacket(&Protocol::ieee80211Mac, packet, b(0), b(0), direction, nullptr); + } }; Define_Module(TestablePcapRecorder); @@ -167,6 +174,20 @@ REQUIRE(recorder->getIeee80211LinkType(true, true) == LINKTYPE_IEEE802_11_RADIOT auto writer = new RecordingPcapWriter(); recorder->setWriter(writer); +const std::vector packetBytes = {0x08, 0x41, 0x42, 0x43, 0x44}; +Packet packetOnly("packetOnly"); +packetOnly.insertAtBack(makeShared(packetBytes)); +recorder->writeIeee80211Packet(&packetOnly, DIRECTION_INBOUND); +REQUIRE(writer->records.size() == 1); +const auto& packetOnlyRecord = writer->records.front(); +REQUIRE(packetOnlyRecord.at(0) == 0); // Radiotap version +REQUIRE(packetOnlyRecord.at(1) == 0); // Radiotap padding +REQUIRE(readUint16(packetOnlyRecord, 2) == 12); +REQUIRE(readUint32(packetOnlyRecord, 4) == ((1U << 1) | (1U << 14))); +REQUIRE(packetOnlyRecord.size() == 12 + packetBytes.size()); +REQUIRE(std::equal(packetBytes.begin(), packetBytes.end(), packetOnlyRecord.begin() + 12)); + +writer->records.clear(); const std::vector firstMpdu = {0x08, 0x01, 0x02, 0x03, 0x04}; const std::vector secondMpdu = {0x88, 0x11, 0x12, 0x13, 0x14, 0x15}; Packet aggregate("ampdu"); From 12788db20d2e1e4d04d99da903d5f9e480c9d570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 17 Aug 2026 13:54:05 +0200 Subject: [PATCH 6/8] pcap: harden radiotap capture handling Classify VHT modes before HT modes so a future inheritance relationship cannot cause VHT packets to emit HT MCS metadata. Rename the legacy cPacket argument to packetObject in both declaration and definition, avoiding type-name shadowing without changing the virtual API. Document the capture contracts that are easy to misread: PHY payload resolution remains within caller-provided offsets and falls back to generic dissection on malformed headers; outbound A-MPDUs stay split into decodable MPDUs without receive-only status metadata; adapter-backed formats require writer prefix support; and enriched PHY context crosses the legacy virtual hook only for the identical packet. Validation: release build passed; PcapRecorderRadiotapHtVht_1.test and PcapRecorderIeee80211Ampdu_1.test passed; git diff --check passed. Full unit and fingerprint suites were intentionally not run. --- src/inet/common/packet/recorder/IPcapWriter.h | 4 ++- .../common/packet/recorder/PcapRecorder.cc | 10 ++++-- .../common/packet/recorder/PcapRecorder.h | 3 +- .../Ieee80211RadiotapPcapCaptureAdapter.cc | 35 +++++++++++-------- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/inet/common/packet/recorder/IPcapWriter.h b/src/inet/common/packet/recorder/IPcapWriter.h index 679282215ba..b6cf30ee2df 100644 --- a/src/inet/common/packet/recorder/IPcapWriter.h +++ b/src/inet/common/packet/recorder/IPcapWriter.h @@ -217,7 +217,9 @@ class INET_API IPcapWriter /** * Writes an octet prefix followed by the selected range of the original packet. - * Implementations which don't support scatter/gather capture fail explicitly. + * Protocol-specific capture adapters use the prefix as part of the selected link-layer + * record format. The default fails explicitly instead of silently writing a malformed + * prefix-less record. Third-party writers must override this method before adapters are enabled. */ virtual void writePacketWithPrefix(simtime_t time, const std::vector& prefix, const Packet *packet, b frontOffset, b backOffset, Direction direction, NetworkInterface *ie, PcapLinkType linkType) diff --git a/src/inet/common/packet/recorder/PcapRecorder.cc b/src/inet/common/packet/recorder/PcapRecorder.cc index fae68b2ee8b..271edafd25d 100644 --- a/src/inet/common/packet/recorder/PcapRecorder.cc +++ b/src/inet/common/packet/recorder/PcapRecorder.cc @@ -247,6 +247,8 @@ void PcapRecorder::writePacket(const Protocol *protocol, const Packet *packet, b void PcapRecorder::recordPacket(const PcapCaptureObservation& observation, cComponent *source) { + // Keep the established cPacket overload as the subclass extension point. Save and restore the + // observation so nested calls cannot leave another recording operation's PHY context active. auto previousObservation = activeCaptureObservation; activeCaptureObservation = &observation; try { @@ -259,11 +261,13 @@ void PcapRecorder::recordPacket(const PcapCaptureObservation& observation, cComp } } -void PcapRecorder::recordPacket(const cPacket *cPacket, Direction direction, cComponent *source) +void PcapRecorder::recordPacket(const cPacket *packetObject, Direction direction, cComponent *source) { - auto packet = dynamic_cast(cPacket); + auto packet = dynamic_cast(packetObject); if (packet == nullptr) return; + // A legacy override may forward a replacement packet. Apply PHY metadata only when it forwards + // the exact observed packet; otherwise construct an ordinary packet-only observation. const PcapCaptureObservation observation = activeCaptureObservation != nullptr && activeCaptureObservation->packet == packet ? PcapCaptureObservation(packet, direction, activeCaptureObservation->transmission, activeCaptureObservation->reception) : PcapCaptureObservation(packet, direction); @@ -308,6 +312,8 @@ void PcapRecorder::recordPacket(const cPacket *cPacket, Direction direction, cCo return; } if (enableProtocolSpecificCaptureAdapters && enableConvertingPackets) { + // Resolution is best-effort. Unsupported or malformed outer headers return no result, + // allowing the generic dissector below to preserve the legacy capture behavior. auto resolution = PcapCaptureAdapterRegistry::getInstance().tryResolveProtocol(protocol, packet, packetProtocolTag->getFrontOffset(), packetProtocolTag->getBackOffset()); if (resolution.has_value() && contains(dumpProtocols, std::get<0>(*resolution))) { diff --git a/src/inet/common/packet/recorder/PcapRecorder.h b/src/inet/common/packet/recorder/PcapRecorder.h index fab237f414d..a6c2e223138 100644 --- a/src/inet/common/packet/recorder/PcapRecorder.h +++ b/src/inet/common/packet/recorder/PcapRecorder.h @@ -53,6 +53,7 @@ class INET_API PcapRecorder : public SimpleModule, protected cListener, public P bool enableConvertingPackets = true; bool enableProtocolSpecificCaptureAdapters = false; bool recordPcap = false; + // Transiently carries enriched capture data through the legacy virtual recordPacket(cPacket *) hook. const PcapCaptureObservation *activeCaptureObservation = nullptr; std::vector helpers; PacketPrinter packetPrinter; @@ -81,7 +82,7 @@ class INET_API PcapRecorder : public SimpleModule, protected cListener, public P virtual void handleMessage(cMessage *msg) override; virtual void finish() override; virtual void receiveSignal(cComponent *source, simsignal_t signalID, cObject *obj, cObject *details) override; - virtual void recordPacket(const cPacket *packet, Direction direction, cComponent *source); + virtual void recordPacket(const cPacket *packetObject, Direction direction, cComponent *source); virtual void recordPacket(const PcapCaptureObservation& observation, cComponent *source); virtual bool matchesLinkType(PcapLinkType pcapLinkType, const Protocol *protocol) const; virtual Packet *tryConvertToLinkType(const Packet *packet, b frontOffset, b backOffset, PcapLinkType pcapLinkType, const Protocol *protocol) const; diff --git a/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc index 46ce531577b..f5065baab0e 100644 --- a/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc +++ b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc @@ -268,19 +268,7 @@ RadiotapPpduFields extractRadiotapPpduFields(const Packet *packet, Direction dir auto mode = findIeee80211Mode(packet, transmission); if (mode != nullptr) { auto dataMode = mode->getDataMode(); - if (dynamic_cast(mode) != nullptr) { - fields.isHt = true; - if (auto htDataMode = dynamic_cast(dataMode)) { - // IEEE 802.11-2024, Table 19-11; radiotap MCS known/flags/mcs fields. - fields.mcs[0] = 0x01 | 0x02 | 0x04 | 0x10; // bandwidth, MCS, GI, and BCC FEC are known - if (htDataMode->getBandwidth().get() > 30e6) - fields.mcs[1] |= 1; - if (htDataMode->getGuardIntervalType() == physicallayer::Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) - fields.mcs[1] |= 1 << 2; - fields.mcs[2] = htDataMode->getMcsIndex(); - } - } - else if (dynamic_cast(mode) != nullptr) { + if (dynamic_cast(mode) != nullptr) { fields.isVht = true; if (auto vhtDataMode = dynamic_cast(dataMode)) { // IEEE 802.11-2024, Table 21-12; radiotap VHT known/flags fields. @@ -295,6 +283,18 @@ RadiotapPpduFields extractRadiotapPpduFields(const Packet *packet, Direction dir fields.vhtCoding = 0; // BCC } } + else if (dynamic_cast(mode) != nullptr) { + fields.isHt = true; + if (auto htDataMode = dynamic_cast(dataMode)) { + // IEEE 802.11-2024, Table 19-11; radiotap MCS known/flags/mcs fields. + fields.mcs[0] = 0x01 | 0x02 | 0x04 | 0x10; // bandwidth, MCS, GI, and BCC FEC are known + if (htDataMode->getBandwidth().get() > 30e6) + fields.mcs[1] |= 1; + if (htDataMode->getGuardIntervalType() == physicallayer::Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT) + fields.mcs[1] |= 1 << 2; + fields.mcs[2] = htDataMode->getMcsIndex(); + } + } else if (dataMode != nullptr) { double rateValue = dataMode->getNetBitrate().get() / 500000.0; if (std::isfinite(rateValue) && rateValue >= 1 && rateValue <= 255 && rateValue == std::trunc(rateValue)) { @@ -446,9 +446,13 @@ std::optional> Ieee80211RadiotapPcapCaptureAdapter::tryResolvePa return std::nullopt; auto resolvedFrontOffset = frontOffset + header->getChunkLength(); auto payloadLength = b(header->getLengthField()); + // The caller's back offset already excludes trailing data from the candidate range. + // Checking against that range guarantees that the resolved payload cannot extend into it. auto availablePayloadLength = packet->getDataLength() - resolvedFrontOffset - backOffset; if (payloadLength > availablePayloadLength) return std::nullopt; + // Convert the declared payload length back to Packet's offset representation. This retains + // the caller's excluded suffix and also excludes any PHY tail or padding after the payload. auto resolvedBackOffset = packet->getDataLength() - resolvedFrontOffset - payloadLength; return std::pair(resolvedFrontOffset, resolvedBackOffset); } @@ -474,8 +478,9 @@ std::vector Ieee80211RadiotapPcapCaptureAdapter::createRecord const auto& mpduRange = mpduRanges[i]; auto recordBackOffset = packet->getDataLength() - mpduRange.offset - mpduRange.length; RadiotapRecordMetadata metadata; - // Radiotap defines A-MPDU status for received frames. Outbound - // aggregates are still split, but carry no A-MPDU status field. + // Radiotap defines A-MPDU status for received frames only. Outbound aggregates are + // still split so analyzers can decode each MPDU, but omitting the status avoids + // inventing nonstandard transmit-side grouping metadata. metadata.isAmpdu = observation.direction == DIRECTION_INBOUND; metadata.isLastSubframe = i == mpduRanges.size() - 1; metadata.ampduReference = ampduReference; From 847b6f5fbb62a784274ca022aff28cc13d86ca45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 17 Aug 2026 15:58:38 +0200 Subject: [PATCH 7/8] pcapng: honor unlimited snaplen and validate link types Treat a zero PCAPng snap length as unlimited when calculating captured packet data while preserving the zero value advertised in the interface description block. Track the first link type associated with each network interface and reject later mismatches before writing an enhanced packet block, preventing silently misdecoded captures. Reset cached interface identifiers and link types whenever the writer opens a new file so reused writer instances emit self-contained interface descriptions. Extend the focused writer test with unlimited-snaplen, link-type mismatch, and close/reopen regression coverage. --- .../common/packet/recorder/PcapngWriter.cc | 19 +++++--- .../common/packet/recorder/PcapngWriter.h | 2 +- tests/unit/PcapWriterPrefix_1.test | 46 +++++++++++++++++++ 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/src/inet/common/packet/recorder/PcapngWriter.cc b/src/inet/common/packet/recorder/PcapngWriter.cc index 7aead3e32c9..b6816bf4243 100644 --- a/src/inet/common/packet/recorder/PcapngWriter.cc +++ b/src/inet/common/packet/recorder/PcapngWriter.cc @@ -96,6 +96,8 @@ void PcapngWriter::open(const char *filename, unsigned int snaplen, int timePrec throw cRuntimeError("Cannot open pcap file [%s] for writing: %s", filename, strerror(errno)); flush = false; + nextPcapngInterfaceId = 0; + interfaceModuleIdToPcapngInterface.clear(); this->snaplen = snaplen; // TODO check validity of timePrecision @@ -214,21 +216,24 @@ void PcapngWriter::writePacketWithPrefix(simtime_t stime, const std::vectorgetId()); + auto it = interfaceModuleIdToPcapngInterface.find(networkInterface->getId()); int pcapngInterfaceId; - if (it != interfaceModuleIdToPcapngInterfaceId.end()) - pcapngInterfaceId = it->second; + if (it != interfaceModuleIdToPcapngInterface.end()) { + if (it->second.second != linkType) + throw cRuntimeError("linktype mismatch error: required linktype = %d, arrived linktype = %d", it->second.second, linkType); + pcapngInterfaceId = it->second.first; + } else { writeInterface(networkInterface, linkType); pcapngInterfaceId = nextPcapngInterfaceId++; - interfaceModuleIdToPcapngInterfaceId[networkInterface->getId()] = pcapngInterfaceId; + interfaceModuleIdToPcapngInterface[networkInterface->getId()] = {pcapngInterfaceId, linkType}; } b packetLength = packet->getDataLength() - frontOffset - backOffset; size_t originalLength = prefix.size() + packetLength.get(); - // Advertise and enforce the configured snaplen for PCAPng too. The captured length is - // truncated, while originalPacketLength below retains the complete untruncated record length. - size_t capturedLength = std::min(originalLength, snaplen); + // Advertise and enforce the configured snaplen for PCAPng too. A zero snaplen means unlimited; + // otherwise the captured length is truncated while the original length remains unchanged. + size_t capturedLength = snaplen == 0 ? originalLength : std::min(originalLength, snaplen); uint32_t optionsLength = (4 + 4) + 4; uint32_t blockTotalLength = 32 + roundUp(capturedLength) + optionsLength; ASSERT(blockTotalLength % 4 == 0); diff --git a/src/inet/common/packet/recorder/PcapngWriter.h b/src/inet/common/packet/recorder/PcapngWriter.h index ccc206b32fe..c8eb95e32bf 100644 --- a/src/inet/common/packet/recorder/PcapngWriter.h +++ b/src/inet/common/packet/recorder/PcapngWriter.h @@ -27,7 +27,7 @@ class INET_API PcapngWriter : public IPcapWriter bool flush = false; int nextPcapngInterfaceId = 0; int timePrecision = 6; - std::map interfaceModuleIdToPcapngInterfaceId; + std::map> interfaceModuleIdToPcapngInterface; public: /** diff --git a/tests/unit/PcapWriterPrefix_1.test b/tests/unit/PcapWriterPrefix_1.test index 09e62f513fb..01331a5985f 100644 --- a/tests/unit/PcapWriterPrefix_1.test +++ b/tests/unit/PcapWriterPrefix_1.test @@ -5,6 +5,7 @@ Test prefixed PCAP and PCAPng writes, including snap length and original length. #include #include #include +#include #include "inet/common/packet/chunk/BytesChunk.h" #include "inet/common/packet/recorder/PcapWriter.h" #include "inet/common/packet/recorder/PcapngWriter.h" @@ -57,8 +58,53 @@ REQUIRE(readUint32(pcapng, packetBlock + 20) == 4); REQUIRE(readUint32(pcapng, packetBlock + 24) == 5); REQUIRE(std::vector(pcapng.begin() + packetBlock + 28, pcapng.begin() + packetBlock + 32) == std::vector({0xaa, 0xbb, 0xcc, 0x11})); +PcapngWriter unlimitedPcapngWriter; +unlimitedPcapngWriter.open("prefix-unlimited.pcapng", 0, 6); +unlimitedPcapngWriter.writePacketWithPrefix(SIMTIME_ZERO, prefix, &packet, B(1), B(1), DIRECTION_INBOUND, &networkInterface, LINKTYPE_IEEE802_11_RADIOTAP); +unlimitedPcapngWriter.close(); +auto unlimitedPcapng = readFile("prefix-unlimited.pcapng"); +size_t unlimitedInterfaceBlock = 28; +REQUIRE(readUint32(unlimitedPcapng, unlimitedInterfaceBlock + 12) == 0); +size_t unlimitedPacketBlock = unlimitedInterfaceBlock + readUint32(unlimitedPcapng, unlimitedInterfaceBlock + 4); +REQUIRE(readUint32(unlimitedPcapng, unlimitedPacketBlock + 20) == 5); +REQUIRE(readUint32(unlimitedPcapng, unlimitedPacketBlock + 24) == 5); +REQUIRE(std::vector(unlimitedPcapng.begin() + unlimitedPacketBlock + 28, unlimitedPcapng.begin() + unlimitedPacketBlock + 33) == std::vector({0xaa, 0xbb, 0xcc, 0x11, 0x12})); + +PcapngWriter mismatchPcapngWriter; +mismatchPcapngWriter.open("linktype-mismatch.pcapng", 64, 6); +mismatchPcapngWriter.writePacket(SIMTIME_ZERO, &packet, b(0), b(0), DIRECTION_INBOUND, &networkInterface, LINKTYPE_ETHERNET); +bool linkTypeMismatchThrown = false; +try { + mismatchPcapngWriter.writePacket(SIMTIME_ZERO, &packet, b(0), b(0), DIRECTION_INBOUND, &networkInterface, LINKTYPE_IEEE802_11_RADIOTAP); +} +catch (const cRuntimeError& error) { + REQUIRE(std::string(error.what()).find("linktype mismatch error") != std::string::npos); + linkTypeMismatchThrown = true; +} +mismatchPcapngWriter.close(); +REQUIRE(linkTypeMismatchThrown); + +PcapngWriter reopenedPcapngWriter; +reopenedPcapngWriter.open("reopen-first.pcapng", 64, 6); +reopenedPcapngWriter.writePacket(SIMTIME_ZERO, &packet, b(0), b(0), DIRECTION_INBOUND, &networkInterface, LINKTYPE_ETHERNET); +reopenedPcapngWriter.close(); +reopenedPcapngWriter.open("reopen-second.pcapng", 64, 6); +reopenedPcapngWriter.writePacket(SIMTIME_ZERO, &packet, b(0), b(0), DIRECTION_INBOUND, &networkInterface, LINKTYPE_IEEE802_11_RADIOTAP); +reopenedPcapngWriter.close(); +auto reopenedPcapng = readFile("reopen-second.pcapng"); +size_t reopenedInterfaceBlock = 28; +REQUIRE(readUint32(reopenedPcapng, reopenedInterfaceBlock) == 1); +REQUIRE(readUint32(reopenedPcapng, reopenedInterfaceBlock + 8) == LINKTYPE_IEEE802_11_RADIOTAP); +size_t reopenedPacketBlock = reopenedInterfaceBlock + readUint32(reopenedPcapng, reopenedInterfaceBlock + 4); +REQUIRE(readUint32(reopenedPcapng, reopenedPacketBlock) == 6); +REQUIRE(readUint32(reopenedPcapng, reopenedPacketBlock + 8) == 0); + std::remove("prefix.pcap"); std::remove("prefix.pcapng"); +std::remove("prefix-unlimited.pcapng"); +std::remove("linktype-mismatch.pcapng"); +std::remove("reopen-first.pcapng"); +std::remove("reopen-second.pcapng"); EV << "Prefixed PCAP writers honor prefix ordering, snaplen, and original length.\n"; %contains: stdout From cbab48f5030245e1a0d29ececde5f5112f9c3647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Mon, 17 Aug 2026 22:58:05 +0200 Subject: [PATCH 8/8] pcap: fix multi-format capture and adapter lookups --- .../recorder/PcapCaptureAdapterRegistry.cc | 8 +- .../recorder/PcapCaptureAdapterRegistry.h | 1 + .../common/packet/recorder/PcapRecorder.cc | 137 +++++++++++++----- .../common/packet/recorder/PcapRecorder.h | 13 ++ .../common/packet/recorder/PcapngWriter.cc | 14 +- .../common/packet/recorder/PcapngWriter.h | 2 +- tests/unit/PcapCaptureAdapterRegistry_1.test | 3 + tests/unit/PcapWriterPrefix_1.test | 37 +++-- 8 files changed, 154 insertions(+), 61 deletions(-) diff --git a/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc index fc6c75f6792..fc26d16faab 100644 --- a/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc +++ b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc @@ -50,6 +50,12 @@ const IPcapCaptureAdapter *PcapCaptureAdapterRegistry::findProtocolAdapter(const } std::optional> PcapCaptureAdapterRegistry::tryResolveProtocol(const Protocol *outerProtocol, const Packet *packet, b frontOffset, b backOffset) const +{ + auto resolution = tryResolveProtocolWithAdapter(outerProtocol, packet, frontOffset, backOffset); + return resolution.has_value() ? std::optional>({std::get<0>(*resolution), std::get<1>(*resolution), std::get<2>(*resolution)}) : std::nullopt; +} + +std::optional> PcapCaptureAdapterRegistry::tryResolveProtocolWithAdapter(const Protocol *outerProtocol, const Packet *packet, b frontOffset, b backOffset) const { auto resolver = protocolResolvers.find(outerProtocol); if (resolver == protocolResolvers.end()) @@ -58,7 +64,7 @@ std::optional> PcapCaptureAdapterRegistry::tr if (adapter == nullptr) return std::nullopt; auto offsets = adapter->tryResolvePacket(packet, frontOffset, backOffset); - return offsets.has_value() ? std::optional>({resolver->second, offsets->first, offsets->second}) : std::nullopt; + return offsets.has_value() ? std::optional>({resolver->second, offsets->first, offsets->second, adapter}) : std::nullopt; } std::optional PcapCaptureAdapterRegistry::tryCreateObservation(const cObject *object, Direction direction) const diff --git a/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.h b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.h index e2a92e35475..0140c350358 100644 --- a/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.h +++ b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.h @@ -33,6 +33,7 @@ class INET_API PcapCaptureAdapterRegistry void registerObservationAdapter(const char *key, const IPcapCaptureObservationAdapter *adapter); const IPcapCaptureAdapter *findProtocolAdapter(const Protocol *protocol) const; std::optional> tryResolveProtocol(const Protocol *outerProtocol, const Packet *packet, b frontOffset, b backOffset) const; + std::optional> tryResolveProtocolWithAdapter(const Protocol *outerProtocol, const Packet *packet, b frontOffset, b backOffset) const; std::optional tryCreateObservation(const cObject *object, Direction direction) const; static PcapCaptureAdapterRegistry& getInstance(); diff --git a/src/inet/common/packet/recorder/PcapRecorder.cc b/src/inet/common/packet/recorder/PcapRecorder.cc index 271edafd25d..08c618d4790 100644 --- a/src/inet/common/packet/recorder/PcapRecorder.cc +++ b/src/inet/common/packet/recorder/PcapRecorder.cc @@ -29,6 +29,39 @@ Define_Module(PcapRecorder); simsignal_t PcapRecorder::packetRecordedSignal = registerSignal("packetRecorded"); +namespace { + +class CaptureAdapterResolutionGuard +{ + protected: + bool& active; + const Protocol *&activeProtocol; + const IPcapCaptureAdapter *&activeAdapter; + bool previousActive; + const Protocol *previousProtocol; + const IPcapCaptureAdapter *previousAdapter; + + public: + CaptureAdapterResolutionGuard(bool& active, const Protocol *&activeProtocol, const IPcapCaptureAdapter *&activeAdapter, + const Protocol *protocol, const IPcapCaptureAdapter *adapter) : + active(active), activeProtocol(activeProtocol), activeAdapter(activeAdapter), previousActive(active), + previousProtocol(activeProtocol), previousAdapter(activeAdapter) + { + active = true; + activeProtocol = protocol; + activeAdapter = adapter; + } + + ~CaptureAdapterResolutionGuard() + { + active = previousActive; + activeProtocol = previousProtocol; + activeAdapter = previousAdapter; + } +}; + +} // namespace + PcapRecorder::~PcapRecorder() { delete pcapWriter; @@ -59,6 +92,7 @@ void PcapRecorder::visitChunk(const Ptr& chunk, const Protocol *pro void PcapRecorder::initialize() { + captureAdapterRegistry = &PcapCaptureAdapterRegistry::getInstance(); verbose = par("verbose"); recordEmptyPackets = par("recordEmptyPackets"); enableConvertingPackets = par("enableConvertingPackets"); @@ -177,7 +211,7 @@ void PcapRecorder::receiveSignal(cComponent *source, simsignal_t signalID, cObje auto i = signalList.find(signalID); ASSERT(i != signalList.end()); Direction direction = i->second; - auto observation = PcapCaptureAdapterRegistry::getInstance().tryCreateObservation(obj, direction); + auto observation = captureAdapterRegistry->tryCreateObservation(obj, direction); if (observation.has_value()) recordPacket(*observation, source); // Observation adapters are optional enrichers. If none accepts the object, retain the @@ -190,41 +224,39 @@ void PcapRecorder::receiveSignal(cComponent *source, simsignal_t signalID, cObje void PcapRecorder::writePacket(const Protocol *protocol, const PcapCaptureObservation& observation, b frontOffset, b backOffset, NetworkInterface *networkInterface) { auto packet = observation.packet; - if (enableProtocolSpecificCaptureAdapters && enableConvertingPackets) { - auto adapter = PcapCaptureAdapterRegistry::getInstance().findProtocolAdapter(protocol); - if (adapter != nullptr) { - // A protocol adapter owns its output link type and complete record layout, so its - // records bypass the generic link-type matching and packet-conversion helpers below. - auto records = adapter->createRecords(observation, frontOffset, backOffset); - for (const auto& record : records) { - auto dataLength = packet->getDataLength() - record.frontOffset - record.backOffset; - // A protocol-specific prefix is meaningful capture data, so a prefix-only record - // is not considered empty even when recordEmptyPackets is false. - if (recordEmptyPackets || !record.getPrefix().empty() || dataLength != b(0)) { - pcapWriter->writePacketWithPrefix(simTime(), record.getPrefix(), packet, record.frontOffset, record.backOffset, - observation.direction, networkInterface, adapter->getLinkType()); - numRecorded++; - // Emit once per written record, but retain the original observed packet as the - // signal value; split records such as A-MPDU MPDUs therefore share that value. - emit(packetRecordedSignal, packet); - } + auto adapter = findProtocolCaptureAdapter(protocol); + if (adapter != nullptr) { + // A protocol adapter owns its output link type and complete record layout, so its + // records bypass the generic link-type matching and packet-conversion helpers below. + auto records = adapter->createRecords(observation, frontOffset, backOffset); + for (const auto& record : records) { + auto dataLength = packet->getDataLength() - record.frontOffset - record.backOffset; + // A protocol-specific prefix is meaningful capture data, so a prefix-only record + // is not considered empty even when recordEmptyPackets is false. + if (recordEmptyPackets || !record.getPrefix().empty() || dataLength != b(0)) { + pcapWriter->writePacketWithPrefix(simTime(), record.getPrefix(), packet, record.frontOffset, record.backOffset, + observation.direction, networkInterface, adapter->getLinkType()); + numRecorded++; + // Emit once per written record, but retain the original observed packet as the + // signal value; split records such as A-MPDU MPDUs therefore share that value. + emit(packetRecordedSignal, packet); } - return; } + return; } - writePacket(protocol, packet, frontOffset, backOffset, observation.direction, networkInterface); + writePacketWithResolvedAdapter(protocol, nullptr, packet, frontOffset, backOffset, observation.direction, networkInterface); } void PcapRecorder::writePacket(const Protocol *protocol, const Packet *packet, b frontOffset, b backOffset, Direction direction, NetworkInterface *networkInterface) { - if (enableProtocolSpecificCaptureAdapters && enableConvertingPackets && - PcapCaptureAdapterRegistry::getInstance().findProtocolAdapter(protocol) != nullptr) { - writePacket(protocol, PcapCaptureObservation(packet, direction), frontOffset, backOffset, networkInterface); + auto adapter = findProtocolCaptureAdapter(protocol); + if (adapter != nullptr) { + writePacketWithResolvedAdapter(protocol, adapter, PcapCaptureObservation(packet, direction), frontOffset, backOffset, networkInterface); return; } - auto pcapLinkType = protocolToLinkType(protocol); + auto pcapLinkType = protocolToLinkTypeWithResolvedAdapter(protocol, nullptr); if (pcapLinkType == LINKTYPE_INVALID) throw cRuntimeError("Cannot determine the PCAP link type from protocol '%s'", protocol->getName()); bool convertPacket = !matchesLinkType(pcapLinkType, protocol); @@ -304,20 +336,22 @@ void PcapRecorder::recordPacket(const cPacket *packetObject, Direction direction const auto& packetProtocolTag = packet->getTag(); auto protocol = packetProtocolTag->getProtocol(); if (contains(dumpProtocols, protocol)) { - if (enableProtocolSpecificCaptureAdapters && enableConvertingPackets && - PcapCaptureAdapterRegistry::getInstance().findProtocolAdapter(protocol) != nullptr) - writePacket(protocol, effectiveObservation, packetProtocolTag->getFrontOffset(), packetProtocolTag->getBackOffset(), networkInterface); + auto adapter = findProtocolCaptureAdapter(protocol); + if (adapter != nullptr) + writePacketWithResolvedAdapter(protocol, adapter, effectiveObservation, packetProtocolTag->getFrontOffset(), packetProtocolTag->getBackOffset(), networkInterface); else - writePacket(protocol, packet, packetProtocolTag->getFrontOffset(), packetProtocolTag->getBackOffset(), direction, networkInterface); + writePacketWithResolvedAdapter(protocol, nullptr, packet, packetProtocolTag->getFrontOffset(), packetProtocolTag->getBackOffset(), direction, networkInterface); return; } if (enableProtocolSpecificCaptureAdapters && enableConvertingPackets) { // Resolution is best-effort. Unsupported or malformed outer headers return no result, // allowing the generic dissector below to preserve the legacy capture behavior. - auto resolution = PcapCaptureAdapterRegistry::getInstance().tryResolveProtocol(protocol, packet, + auto resolution = captureAdapterRegistry->tryResolveProtocolWithAdapter(protocol, packet, packetProtocolTag->getFrontOffset(), packetProtocolTag->getBackOffset()); if (resolution.has_value() && contains(dumpProtocols, std::get<0>(*resolution))) { - writePacket(std::get<0>(*resolution), effectiveObservation, std::get<1>(*resolution), std::get<2>(*resolution), networkInterface); + auto resolvedProtocol = std::get<0>(*resolution); + writePacketWithResolvedAdapter(resolvedProtocol, std::get<3>(*resolution), effectiveObservation, + std::get<1>(*resolution), std::get<2>(*resolution), networkInterface); return; } } @@ -328,11 +362,11 @@ void PcapRecorder::recordPacket(const cPacket *packetObject, Direction direction PacketDissector packetDissector(ProtocolDissectorRegistry::getInstance(), *this); packetDissector.dissectPacket(&dissectedPacket); if (dumpProtocol != nullptr) { - if (enableProtocolSpecificCaptureAdapters && enableConvertingPackets && - PcapCaptureAdapterRegistry::getInstance().findProtocolAdapter(dumpProtocol) != nullptr) - writePacket(dumpProtocol, effectiveObservation, frontOffset, backOffset, networkInterface); + auto adapter = findProtocolCaptureAdapter(dumpProtocol); + if (adapter != nullptr) + writePacketWithResolvedAdapter(dumpProtocol, adapter, effectiveObservation, frontOffset, backOffset, networkInterface); else - writePacket(dumpProtocol, packet, frontOffset, backOffset, direction, networkInterface); + writePacketWithResolvedAdapter(dumpProtocol, nullptr, packet, frontOffset, backOffset, direction, networkInterface); } } } @@ -371,8 +405,7 @@ bool PcapRecorder::matchesLinkType(PcapLinkType pcapLinkType, const Protocol *pr PcapLinkType PcapRecorder::protocolToLinkType(const Protocol *protocol) const { - auto captureAdapter = enableProtocolSpecificCaptureAdapters && enableConvertingPackets ? - PcapCaptureAdapterRegistry::getInstance().findProtocolAdapter(protocol) : nullptr; + auto captureAdapter = findProtocolCaptureAdapter(protocol); if (captureAdapter != nullptr) return captureAdapter->getLinkType(); else if (*protocol == Protocol::ethernetPhy) @@ -397,6 +430,36 @@ PcapLinkType PcapRecorder::protocolToLinkType(const Protocol *protocol) const return LINKTYPE_INVALID; } +const IPcapCaptureAdapter *PcapRecorder::findProtocolCaptureAdapter(const Protocol *protocol) const +{ + if (!enableProtocolSpecificCaptureAdapters || !enableConvertingPackets) + return nullptr; + else if (captureAdapterResolutionActive && activeCaptureAdapterProtocol == protocol) + return activeCaptureAdapter; + else + return captureAdapterRegistry->findProtocolAdapter(protocol); +} + +PcapLinkType PcapRecorder::protocolToLinkTypeWithResolvedAdapter(const Protocol *protocol, const IPcapCaptureAdapter *adapter) +{ + CaptureAdapterResolutionGuard guard(captureAdapterResolutionActive, activeCaptureAdapterProtocol, activeCaptureAdapter, protocol, adapter); + return protocolToLinkType(protocol); +} + +void PcapRecorder::writePacketWithResolvedAdapter(const Protocol *protocol, const IPcapCaptureAdapter *adapter, const Packet *packet, + b frontOffset, b backOffset, Direction direction, NetworkInterface *networkInterface) +{ + CaptureAdapterResolutionGuard guard(captureAdapterResolutionActive, activeCaptureAdapterProtocol, activeCaptureAdapter, protocol, adapter); + writePacket(protocol, packet, frontOffset, backOffset, direction, networkInterface); +} + +void PcapRecorder::writePacketWithResolvedAdapter(const Protocol *protocol, const IPcapCaptureAdapter *adapter, const PcapCaptureObservation& observation, + b frontOffset, b backOffset, NetworkInterface *networkInterface) +{ + CaptureAdapterResolutionGuard guard(captureAdapterResolutionActive, activeCaptureAdapterProtocol, activeCaptureAdapter, protocol, adapter); + writePacket(protocol, observation, frontOffset, backOffset, networkInterface); +} + Packet *PcapRecorder::tryConvertToLinkType(const Packet *packet, b frontOffset, b backOffset, PcapLinkType pcapLinkType, const Protocol *protocol) const { if (enableConvertingPackets) { diff --git a/src/inet/common/packet/recorder/PcapRecorder.h b/src/inet/common/packet/recorder/PcapRecorder.h index a6c2e223138..5e6d214ab2b 100644 --- a/src/inet/common/packet/recorder/PcapRecorder.h +++ b/src/inet/common/packet/recorder/PcapRecorder.h @@ -19,6 +19,8 @@ namespace inet { +class PcapCaptureAdapterRegistry; + /** * Dumps every packet using the IPacketWriter and PacketDump classes */ @@ -53,8 +55,13 @@ class INET_API PcapRecorder : public SimpleModule, protected cListener, public P bool enableConvertingPackets = true; bool enableProtocolSpecificCaptureAdapters = false; bool recordPcap = false; + PcapCaptureAdapterRegistry *captureAdapterRegistry = nullptr; // Transiently carries enriched capture data through the legacy virtual recordPacket(cPacket *) hook. const PcapCaptureObservation *activeCaptureObservation = nullptr; + // Transiently carries one resolved protocol adapter through the legacy virtual writePacket() hooks. + bool captureAdapterResolutionActive = false; + const Protocol *activeCaptureAdapterProtocol = nullptr; + const IPcapCaptureAdapter *activeCaptureAdapter = nullptr; std::vector helpers; PacketPrinter packetPrinter; @@ -89,6 +96,12 @@ class INET_API PcapRecorder : public SimpleModule, protected cListener, public P virtual PcapLinkType protocolToLinkType(const Protocol *protocol) const; virtual void writePacket(const Protocol *protocol, const Packet *packet, b frontOffset, b backOffset, Direction direction, NetworkInterface *networkInterface); virtual void writePacket(const Protocol *protocol, const PcapCaptureObservation& observation, b frontOffset, b backOffset, NetworkInterface *networkInterface); + const IPcapCaptureAdapter *findProtocolCaptureAdapter(const Protocol *protocol) const; + PcapLinkType protocolToLinkTypeWithResolvedAdapter(const Protocol *protocol, const IPcapCaptureAdapter *adapter); + void writePacketWithResolvedAdapter(const Protocol *protocol, const IPcapCaptureAdapter *adapter, const Packet *packet, + b frontOffset, b backOffset, Direction direction, NetworkInterface *networkInterface); + void writePacketWithResolvedAdapter(const Protocol *protocol, const IPcapCaptureAdapter *adapter, const PcapCaptureObservation& observation, + b frontOffset, b backOffset, NetworkInterface *networkInterface); }; } // namespace inet diff --git a/src/inet/common/packet/recorder/PcapngWriter.cc b/src/inet/common/packet/recorder/PcapngWriter.cc index b6816bf4243..1a307b3cf5c 100644 --- a/src/inet/common/packet/recorder/PcapngWriter.cc +++ b/src/inet/common/packet/recorder/PcapngWriter.cc @@ -97,7 +97,7 @@ void PcapngWriter::open(const char *filename, unsigned int snaplen, int timePrec flush = false; nextPcapngInterfaceId = 0; - interfaceModuleIdToPcapngInterface.clear(); + interfaceModuleIdAndLinkTypeToPcapngInterfaceId.clear(); this->snaplen = snaplen; // TODO check validity of timePrecision @@ -216,17 +216,15 @@ void PcapngWriter::writePacketWithPrefix(simtime_t stime, const std::vectorgetId()); + auto interfaceKey = std::make_pair(networkInterface->getId(), linkType); + auto it = interfaceModuleIdAndLinkTypeToPcapngInterfaceId.find(interfaceKey); int pcapngInterfaceId; - if (it != interfaceModuleIdToPcapngInterface.end()) { - if (it->second.second != linkType) - throw cRuntimeError("linktype mismatch error: required linktype = %d, arrived linktype = %d", it->second.second, linkType); - pcapngInterfaceId = it->second.first; - } + if (it != interfaceModuleIdAndLinkTypeToPcapngInterfaceId.end()) + pcapngInterfaceId = it->second; else { writeInterface(networkInterface, linkType); pcapngInterfaceId = nextPcapngInterfaceId++; - interfaceModuleIdToPcapngInterface[networkInterface->getId()] = {pcapngInterfaceId, linkType}; + interfaceModuleIdAndLinkTypeToPcapngInterfaceId[interfaceKey] = pcapngInterfaceId; } b packetLength = packet->getDataLength() - frontOffset - backOffset; diff --git a/src/inet/common/packet/recorder/PcapngWriter.h b/src/inet/common/packet/recorder/PcapngWriter.h index c8eb95e32bf..59596ba7e68 100644 --- a/src/inet/common/packet/recorder/PcapngWriter.h +++ b/src/inet/common/packet/recorder/PcapngWriter.h @@ -27,7 +27,7 @@ class INET_API PcapngWriter : public IPcapWriter bool flush = false; int nextPcapngInterfaceId = 0; int timePrecision = 6; - std::map> interfaceModuleIdToPcapngInterface; + std::map, int> interfaceModuleIdAndLinkTypeToPcapngInterfaceId; public: /** diff --git a/tests/unit/PcapCaptureAdapterRegistry_1.test b/tests/unit/PcapCaptureAdapterRegistry_1.test index ce3afacf20d..e308085ef68 100644 --- a/tests/unit/PcapCaptureAdapterRegistry_1.test +++ b/tests/unit/PcapCaptureAdapterRegistry_1.test @@ -48,6 +48,9 @@ auto resolution = registry.tryResolveProtocol(&Protocol::ipv4, &packet, B(3), B( REQUIRE(resolution.has_value()); REQUIRE(std::get<0>(*resolution) == &Protocol::udp); REQUIRE(std::get<1>(*resolution) == B(4) && std::get<2>(*resolution) == B(6)); +auto resolutionWithAdapter = registry.tryResolveProtocolWithAdapter(&Protocol::ipv4, &packet, B(3), B(4)); +REQUIRE(resolutionWithAdapter.has_value()); +REQUIRE(std::get<3>(*resolutionWithAdapter) == protocolAdapter); bool duplicateProtocolRejected = false; try { diff --git a/tests/unit/PcapWriterPrefix_1.test b/tests/unit/PcapWriterPrefix_1.test index 01331a5985f..02fa88a4df9 100644 --- a/tests/unit/PcapWriterPrefix_1.test +++ b/tests/unit/PcapWriterPrefix_1.test @@ -70,19 +70,28 @@ REQUIRE(readUint32(unlimitedPcapng, unlimitedPacketBlock + 20) == 5); REQUIRE(readUint32(unlimitedPcapng, unlimitedPacketBlock + 24) == 5); REQUIRE(std::vector(unlimitedPcapng.begin() + unlimitedPacketBlock + 28, unlimitedPcapng.begin() + unlimitedPacketBlock + 33) == std::vector({0xaa, 0xbb, 0xcc, 0x11, 0x12})); -PcapngWriter mismatchPcapngWriter; -mismatchPcapngWriter.open("linktype-mismatch.pcapng", 64, 6); -mismatchPcapngWriter.writePacket(SIMTIME_ZERO, &packet, b(0), b(0), DIRECTION_INBOUND, &networkInterface, LINKTYPE_ETHERNET); -bool linkTypeMismatchThrown = false; -try { - mismatchPcapngWriter.writePacket(SIMTIME_ZERO, &packet, b(0), b(0), DIRECTION_INBOUND, &networkInterface, LINKTYPE_IEEE802_11_RADIOTAP); -} -catch (const cRuntimeError& error) { - REQUIRE(std::string(error.what()).find("linktype mismatch error") != std::string::npos); - linkTypeMismatchThrown = true; -} -mismatchPcapngWriter.close(); -REQUIRE(linkTypeMismatchThrown); +PcapngWriter multipleLinkTypesPcapngWriter; +multipleLinkTypesPcapngWriter.open("multiple-linktypes.pcapng", 64, 6); +multipleLinkTypesPcapngWriter.writePacket(SIMTIME_ZERO, &packet, b(0), b(0), DIRECTION_INBOUND, &networkInterface, LINKTYPE_ETHERNET_MPACKET); +multipleLinkTypesPcapngWriter.writePacket(SIMTIME_ZERO, &packet, b(0), b(0), DIRECTION_INBOUND, &networkInterface, LINKTYPE_ETHERNET); +multipleLinkTypesPcapngWriter.writePacket(SIMTIME_ZERO, &packet, b(0), b(0), DIRECTION_INBOUND, &networkInterface, LINKTYPE_ETHERNET_MPACKET); +multipleLinkTypesPcapngWriter.close(); +auto multipleLinkTypesPcapng = readFile("multiple-linktypes.pcapng"); +size_t firstInterfaceBlock = 28; +REQUIRE(readUint32(multipleLinkTypesPcapng, firstInterfaceBlock) == 1); +REQUIRE(readUint32(multipleLinkTypesPcapng, firstInterfaceBlock + 8) == LINKTYPE_ETHERNET_MPACKET); +size_t firstPacketBlock = firstInterfaceBlock + readUint32(multipleLinkTypesPcapng, firstInterfaceBlock + 4); +REQUIRE(readUint32(multipleLinkTypesPcapng, firstPacketBlock) == 6); +REQUIRE(readUint32(multipleLinkTypesPcapng, firstPacketBlock + 8) == 0); +size_t secondInterfaceBlock = firstPacketBlock + readUint32(multipleLinkTypesPcapng, firstPacketBlock + 4); +REQUIRE(readUint32(multipleLinkTypesPcapng, secondInterfaceBlock) == 1); +REQUIRE(readUint32(multipleLinkTypesPcapng, secondInterfaceBlock + 8) == LINKTYPE_ETHERNET); +size_t secondPacketBlock = secondInterfaceBlock + readUint32(multipleLinkTypesPcapng, secondInterfaceBlock + 4); +REQUIRE(readUint32(multipleLinkTypesPcapng, secondPacketBlock) == 6); +REQUIRE(readUint32(multipleLinkTypesPcapng, secondPacketBlock + 8) == 1); +size_t thirdPacketBlock = secondPacketBlock + readUint32(multipleLinkTypesPcapng, secondPacketBlock + 4); +REQUIRE(readUint32(multipleLinkTypesPcapng, thirdPacketBlock) == 6); +REQUIRE(readUint32(multipleLinkTypesPcapng, thirdPacketBlock + 8) == 0); PcapngWriter reopenedPcapngWriter; reopenedPcapngWriter.open("reopen-first.pcapng", 64, 6); @@ -102,7 +111,7 @@ REQUIRE(readUint32(reopenedPcapng, reopenedPacketBlock + 8) == 0); std::remove("prefix.pcap"); std::remove("prefix.pcapng"); std::remove("prefix-unlimited.pcapng"); -std::remove("linktype-mismatch.pcapng"); +std::remove("multiple-linktypes.pcapng"); std::remove("reopen-first.pcapng"); std::remove("reopen-second.pcapng"); EV << "Prefixed PCAP writers honor prefix ordering, snaplen, and original length.\n";