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..b6cf30ee2df 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,20 @@ 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. + * 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) + { + 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..fc26d16faab --- /dev/null +++ b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc @@ -0,0 +1,88 @@ +// +// 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; +} + +// 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()) { + 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 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()) + 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, adapter}) : 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() +{ + // 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); +} + +} // 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..0140c350358 --- /dev/null +++ b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.h @@ -0,0 +1,44 @@ +// +// 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> tryResolveProtocolWithAdapter(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..08c618d4790 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 { // ---- @@ -34,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; @@ -64,9 +92,11 @@ void PcapRecorder::visitChunk(const Ptr& chunk, const Protocol *pro void PcapRecorder::initialize() { + captureAdapterRegistry = &PcapCaptureAdapterRegistry::getInstance(); verbose = par("verbose"); recordEmptyPackets = par("recordEmptyPackets"); enableConvertingPackets = par("enableConvertingPackets"); + enableProtocolSpecificCaptureAdapters = par("enableProtocolSpecificCaptureAdapters"); snaplen = this->par("snaplen"); dumpBadFrames = par("dumpBadFrames"); signalList.clear(); @@ -181,26 +211,52 @@ 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 = captureAdapterRegistry->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); -#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; + 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; + } + + 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) { - auto pcapLinkType = protocolToLinkType(protocol); + auto adapter = findProtocolCaptureAdapter(protocol); + if (adapter != nullptr) { + writePacketWithResolvedAdapter(protocol, adapter, PcapCaptureObservation(packet, direction), frontOffset, backOffset, networkInterface); + return; + } + + 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); @@ -221,53 +277,97 @@ 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(); - } + // 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 { + 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 *packetObject, Direction direction, cComponent *source) +{ + 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); + 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)) - 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); + PcapCaptureObservation effectiveObservation(packet, direction, observation.transmission, observation.reception); + const auto& packetProtocolTag = packet->getTag(); + auto protocol = packetProtocolTag->getProtocol(); + if (contains(dumpProtocols, protocol)) { + auto adapter = findProtocolCaptureAdapter(protocol); + if (adapter != nullptr) + writePacketWithResolvedAdapter(protocol, adapter, effectiveObservation, packetProtocolTag->getFrontOffset(), packetProtocolTag->getBackOffset(), networkInterface); + else + 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 = captureAdapterRegistry->tryResolveProtocolWithAdapter(protocol, packet, + packetProtocolTag->getFrontOffset(), packetProtocolTag->getBackOffset()); + if (resolution.has_value() && contains(dumpProtocols, std::get<0>(*resolution))) { + auto resolvedProtocol = std::get<0>(*resolution); + writePacketWithResolvedAdapter(resolvedProtocol, std::get<3>(*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) { + auto adapter = findProtocolCaptureAdapter(dumpProtocol); + if (adapter != nullptr) + writePacketWithResolvedAdapter(dumpProtocol, adapter, effectiveObservation, frontOffset, backOffset, networkInterface); + else + writePacketWithResolvedAdapter(dumpProtocol, nullptr, packet, frontOffset, backOffset, direction, networkInterface); + } } } @@ -305,7 +405,10 @@ bool PcapRecorder::matchesLinkType(PcapLinkType pcapLinkType, const Protocol *pr PcapLinkType PcapRecorder::protocolToLinkType(const Protocol *protocol) const { - if (*protocol == Protocol::ethernetPhy) + auto captureAdapter = findProtocolCaptureAdapter(protocol); + if (captureAdapter != nullptr) + return captureAdapter->getLinkType(); + else if (*protocol == Protocol::ethernetPhy) return LINKTYPE_ETHERNET_MPACKET; else if (*protocol == Protocol::ethernetMac) return LINKTYPE_ETHERNET; @@ -327,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) { @@ -339,4 +472,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..5e6d214ab2b 100644 --- a/src/inet/common/packet/recorder/PcapRecorder.h +++ b/src/inet/common/packet/recorder/PcapRecorder.h @@ -14,10 +14,13 @@ #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 { +class PcapCaptureAdapterRegistry; + /** * Dumps every packet using the IPacketWriter and PacketDump classes */ @@ -50,7 +53,15 @@ 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; + 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; @@ -78,14 +89,21 @@ 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 *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; 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 #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..1a307b3cf5c 100644 --- a/src/inet/common/packet/recorder/PcapngWriter.cc +++ b/src/inet/common/packet/recorder/PcapngWriter.cc @@ -96,6 +96,9 @@ 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; + interfaceModuleIdAndLinkTypeToPcapngInterfaceId.clear(); + this->snaplen = snaplen; // TODO check validity of timePrecision this->timePrecision = timePrecision; @@ -135,7 +138,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,27 +200,40 @@ 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"); - auto it = interfaceModuleIdToPcapngInterfaceId.find(networkInterface->getId()); + // Enhanced Packet Blocks refer to an Interface Description Block, unlike classic PCAP + // records. Fail explicitly when no interface can be resolved instead of dereferencing null. + if (networkInterface == nullptr) + throw cRuntimeError("The interface entry not found for packet"); + + auto interfaceKey = std::make_pair(networkInterface->getId(), linkType); + auto it = interfaceModuleIdAndLinkTypeToPcapngInterfaceId.find(interfaceKey); int pcapngInterfaceId; - if (it != interfaceModuleIdToPcapngInterfaceId.end()) + if (it != interfaceModuleIdAndLinkTypeToPcapngInterfaceId.end()) pcapngInterfaceId = it->second; else { writeInterface(networkInterface, linkType); pcapngInterfaceId = nextPcapngInterfaceId++; - interfaceModuleIdToPcapngInterfaceId[networkInterface->getId()] = pcapngInterfaceId; + interfaceModuleIdAndLinkTypeToPcapngInterfaceId[interfaceKey] = 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(); + // 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.get()) + optionsLength; + uint32_t blockTotalLength = 32 + roundUp(capturedLength) + optionsLength; ASSERT(blockTotalLength % 4 == 0); // header @@ -228,19 +244,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 +306,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..59596ba7e68 100644 --- a/src/inet/common/packet/recorder/PcapngWriter.h +++ b/src/inet/common/packet/recorder/PcapngWriter.h @@ -23,10 +23,11 @@ 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; - std::map interfaceModuleIdToPcapngInterfaceId; + std::map, int> interfaceModuleIdAndLinkTypeToPcapngInterfaceId; public: /** @@ -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..f5065baab0e --- /dev/null +++ b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc @@ -0,0 +1,505 @@ +// +// 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/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; + + // 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)); + }; + + 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); + // 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) + 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 {}; + 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; + metadata.isPresent = true; + switch (trailer->getFcsMode()) { + case FCS_DECLARED_INCORRECT: + metadata.isBad = true; + break; + 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; + } + return metadata; + } + catch (cRuntimeError&) { + return metadata; + } +} + +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.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 (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)) { + 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) +{ + // 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); + + 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()) <= b(0)) + 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); + } + 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::VALID && !mpduRanges.empty()) { + 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 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; + 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; + } + + // 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; + 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..79b233dddb0 --- /dev/null +++ b/src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.cc @@ -0,0 +1,36 @@ +// +// 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()); + // 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)) + 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..e308085ef68 --- /dev/null +++ b/tests/unit/PcapCaptureAdapterRegistry_1.test @@ -0,0 +1,85 @@ +%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)); +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 { + 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..e11ab2ca501 --- /dev/null +++ b/tests/unit/PcapRecorderIeee80211Ampdu_1.test @@ -0,0 +1,267 @@ +%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); + } + + void writeIeee80211Packet(const Packet *packet, Direction direction) + { + enableProtocolSpecificCaptureAdapters = true; + enableConvertingPackets = true; + writePacket(&Protocol::ieee80211Mac, packet, b(0), b(0), direction, 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; +} + +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; +} + +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")); +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 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"); +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.size() == 1); +requireWholePacketRecord(writer->records.front(), onlyEofPadding); + +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.size() == 1); +requireWholePacketRecord(writer->records.front(), malformed); + +writer->records.clear(); +Packet trailingPadding("trailingPadding"); +appendMpdu(trailingPadding, firstMpdu, true); +recorder->writeIeee80211(&trailingPadding, DIRECTION_INBOUND); +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"; + +%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..fee5e792c14 --- /dev/null +++ b/tests/unit/PcapRecorderRadiotapHtVht_1.test @@ -0,0 +1,172 @@ +%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 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))); +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) == 0x10); // computed FCS is present and trusted without verification + +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..02fa88a4df9 --- /dev/null +++ b/tests/unit/PcapWriterPrefix_1.test @@ -0,0 +1,120 @@ +%description: +Test prefixed PCAP and PCAPng writes, including snap length and original length. + +%includes: +#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" + +%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})); + +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 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); +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("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"; + +%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.