From acef126fc4feb9f9e922908750833daba26dd70e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 19:57:59 +0200 Subject: [PATCH 1/5] recorder: fix: honor finite PCAPng capture length limits A finite snaplen previously had no effect on PCAPng output. Advertise and enforce it while retaining the selected frame's original length. Zero continues to mean unlimited. PcapngWriterSnaplen_1 covers selected ranges, finite limits, unlimited capture and block padding. Change: src.common.packet.recorder.PcapngWriter | behavior.change.fix | test whatsnew migration --- WHATSNEW | 5 ++ doc/src/migration-guide/index.rst | 8 +++ .../common/packet/recorder/PcapngWriter.cc | 8 +-- .../common/packet/recorder/PcapngWriter.h | 1 + tests/unit/PcapngWriterSnaplen_1.test | 50 +++++++++++++++++++ 5 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 tests/unit/PcapngWriterSnaplen_1.test diff --git a/WHATSNEW b/WHATSNEW index 463638fb012..60be86c6707 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -64,6 +64,11 @@ Notable backward incompatible changes are the following: counted arrivals or summed bytes reported twice the true value and now reports the true one. +4. PCAPng capture limits + + PcapngWriter now obeys finite snaplen limits while retaining the original + record length. A zero PCAPng snaplen remains unlimited. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 698474469b2..55804a784b6 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -4,6 +4,14 @@ Migrating Code from INET 3.x ============================ Release: |release| +PCAPng Capture Limits +--------------------- + +``PcapngWriter`` now honors the configured ``snaplen``. Captures exceeding a +finite limit are truncated while retaining their original length; zero means +unlimited for PCAPng. Set a sufficiently large limit or zero if existing +PCAPng consumers require complete records. + IEEE 802.11 Beacon and Probe Response Fields ------------------------------------------ diff --git a/src/inet/common/packet/recorder/PcapngWriter.cc b/src/inet/common/packet/recorder/PcapngWriter.cc index d399cccbbb8..e73e1f8546a 100644 --- a/src/inet/common/packet/recorder/PcapngWriter.cc +++ b/src/inet/common/packet/recorder/PcapngWriter.cc @@ -96,6 +96,7 @@ void PcapngWriter::open(const char *filename, unsigned int snaplen, int timePrec throw cRuntimeError("Cannot open pcap file [%s] for writing: %s", filename, strerror(errno)); flush = false; + this->snaplen = snaplen; // TODO check validity of timePrecision this->timePrecision = timePrecision; @@ -135,7 +136,7 @@ void PcapngWriter::writeInterface(NetworkInterface *networkInterface, PcapLinkTy ibh.blockTotalLength = blockTotalLength; ibh.linkType = linkType; ibh.reserved = 0; - ibh.snaplen = 0; + ibh.snaplen = snaplen; fwrite(&ibh, sizeof(ibh), 1, dumpfile); // interface name option @@ -215,7 +216,8 @@ void PcapngWriter::writePacket(simtime_t stime, const Packet *packet, b frontOff if (networkInterface == nullptr) throw cRuntimeError("The interface entry not found for packet"); - b capturedLength = packet->getDataLength() - frontOffset - backOffset; + b originalLength = packet->getDataLength() - frontOffset - backOffset; + b capturedLength = snaplen == 0 ? originalLength : std::min(originalLength, b(B(snaplen))); uint32_t optionsLength = (4 + 4) + 4; uint32_t blockTotalLength = 32 + roundUp(capturedLength.get()) + optionsLength; ASSERT(blockTotalLength % 4 == 0); @@ -229,7 +231,7 @@ void PcapngWriter::writePacket(simtime_t stime, const Packet *packet, b frontOff pbh.timestampHigh = static_cast((timestamp >> 32) & 0xFFFFFFFFLLU); pbh.timestampLow = static_cast(timestamp & 0xFFFFFFFFLLU); pbh.capturedPacketLength = capturedLength.get(); - pbh.originalPacketLength = capturedLength.get(); + pbh.originalPacketLength = originalLength.get(); fwrite(&pbh, sizeof(pbh), 1, dumpfile); if (capturedLength != b(0)) { diff --git a/src/inet/common/packet/recorder/PcapngWriter.h b/src/inet/common/packet/recorder/PcapngWriter.h index 358d0deee15..45edf52519e 100644 --- a/src/inet/common/packet/recorder/PcapngWriter.h +++ b/src/inet/common/packet/recorder/PcapngWriter.h @@ -23,6 +23,7 @@ class INET_API PcapngWriter : public IPcapWriter protected: std::string fileName; FILE *dumpfile = nullptr; // pcap file + unsigned int snaplen = 0; bool flush = false; int nextPcapngInterfaceId = 0; int timePrecision = 6; diff --git a/tests/unit/PcapngWriterSnaplen_1.test b/tests/unit/PcapngWriterSnaplen_1.test new file mode 100644 index 00000000000..197e66229fa --- /dev/null +++ b/tests/unit/PcapngWriterSnaplen_1.test @@ -0,0 +1,50 @@ +%description: +PCAPng honors finite snaplen while preserving original length, and treats zero +as unlimited. The limit applies to the selected packet range. + +%includes: +#include +#include +#include +#include "inet/common/packet/chunk/BytesChunk.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 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", makeShared(std::vector{0, 1, 2, 3, 4, 5, 6, 7})); +NetworkInterface networkInterface; +for (unsigned int snaplen : {0U, 1U, 4U, 6U, 10U}) { + PcapngWriter writer; + writer.open("snaplen.pcapng", snaplen, 6); + writer.writePacket(SIMTIME_ZERO, &packet, B(1), B(1), DIRECTION_INBOUND, &networkInterface, LINKTYPE_ETHERNET); + writer.close(); + std::ifstream file("snaplen.pcapng", std::ios::binary); + std::vector bytes(std::istreambuf_iterator(file), {}); + size_t interfaceBlock = 28; + REQUIRE(readUint32(bytes, interfaceBlock + 12) == snaplen); + size_t packetBlock = interfaceBlock + readUint32(bytes, interfaceBlock + 4); + uint32_t capturedLength = snaplen == 0 ? 6 : std::min(snaplen, 6U); + REQUIRE(readUint32(bytes, packetBlock + 20) == capturedLength); + REQUIRE(readUint32(bytes, packetBlock + 24) == 6); + for (unsigned int i = 0; i < capturedLength; i++) + REQUIRE(bytes.at(packetBlock + 28 + i) == i + 1); + for (unsigned int i = capturedLength; i % 4 != 0; i++) + REQUIRE(bytes.at(packetBlock + 28 + i) == 0); +} +std::remove("snaplen.pcapng"); +EV << "PCAPng finite and unlimited snaplen verified.\n"; + +%contains: stdout +PCAPng finite and unlimited snaplen verified. From 3b21e6ce45491803012e9521572266aa5e5fd346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 19:57:59 +0200 Subject: [PATCH 2/5] recorder: add: support metadata prefixes in capture records Capture formats may carry metadata outside the selected packet bytes. Write that prefix directly, including it in original and captured lengths, without constructing another Packet. Require every writer to implement the operation. PCAPng associates each interface and link type with its own ID and resets those IDs when reopening a file. Prefix tests cover truncation, multiple formats on one interface and reopening. Change: src.common.packet.recorder | behavior.add | test whatsnew migration | radiotap-capture --- WHATSNEW | 6 +- doc/src/migration-guide/index.rst | 18 +++ src/inet/common/packet/recorder/IPcapWriter.h | 12 +- src/inet/common/packet/recorder/PcapWriter.cc | 33 ++--- src/inet/common/packet/recorder/PcapWriter.h | 3 +- .../common/packet/recorder/PcapngWriter.cc | 53 +++++--- .../common/packet/recorder/PcapngWriter.h | 5 +- tests/unit/PcapWriterPrefix_1.test | 120 ++++++++++++++++++ 8 files changed, 213 insertions(+), 37 deletions(-) create mode 100644 tests/unit/PcapWriterPrefix_1.test diff --git a/WHATSNEW b/WHATSNEW index 60be86c6707..3f06cf4bc06 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -64,7 +64,11 @@ Notable backward incompatible changes are the following: counted arrivals or summed bytes reported twice the true value and now reports the true one. -4. PCAPng capture limits +4. PCAP writer contract and PCAPng capture limits + + Custom IPcapWriter implementations must implement writePacketWithPrefix(), + which writes capture metadata followed by the selected packet bytes. See + the migration guide for the signature and length semantics. PcapngWriter now obeys finite snaplen limits while retaining the original record length. A zero PCAPng snaplen remains unlimited. diff --git a/doc/src/migration-guide/index.rst b/doc/src/migration-guide/index.rst index 55804a784b6..87b13bbfdc2 100644 --- a/doc/src/migration-guide/index.rst +++ b/doc/src/migration-guide/index.rst @@ -4,6 +4,24 @@ Migrating Code from INET 3.x ============================ Release: |release| +Migrating PCAP Writers +---------------------- + +``IPcapWriter`` now requires ``writePacketWithPrefix()``. Custom implementations +must implement this pure virtual method, or derive from a built-in writer that +implements it. Its signature is: + +.. code-block:: c++ + + void writePacketWithPrefix(simtime_t time, const std::vector& prefix, + const Packet *packet, b frontOffset, b backOffset, Direction direction, + NetworkInterface *ie, PcapLinkType linkType) override; + +Write the prefix followed by the selected packet range. Count both parts in +the original record length and apply the capture length limit to their combined +length. Do not modify or take ownership of the packet. The existing +``writePacket()`` entry point remains available for records without a prefix. + PCAPng Capture Limits --------------------- diff --git a/src/inet/common/packet/recorder/IPcapWriter.h b/src/inet/common/packet/recorder/IPcapWriter.h index 7a4e81dfd2c..0d44ac53ae3 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,17 @@ 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. Implementations must include the prefix in the original record length + * and apply the capture length limit to the prefix and packet bytes together. + */ + virtual void writePacketWithPrefix(simtime_t time, const std::vector& prefix, const Packet *packet, b frontOffset, b backOffset, + Direction direction, NetworkInterface *ie, PcapLinkType linkType) = 0; }; } // namespace inet #endif - 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 e73e1f8546a..1a307b3cf5c 100644 --- a/src/inet/common/packet/recorder/PcapngWriter.cc +++ b/src/inet/common/packet/recorder/PcapngWriter.cc @@ -96,6 +96,8 @@ void PcapngWriter::open(const char *filename, unsigned int snaplen, int timePrec throw cRuntimeError("Cannot open pcap file [%s] for writing: %s", filename, strerror(errno)); flush = false; + nextPcapngInterfaceId = 0; + interfaceModuleIdAndLinkTypeToPcapngInterfaceId.clear(); this->snaplen = snaplen; // TODO check validity of timePrecision @@ -198,28 +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 originalLength = packet->getDataLength() - frontOffset - backOffset; - b capturedLength = snaplen == 0 ? originalLength : std::min(originalLength, b(B(snaplen))); + 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 @@ -230,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 = originalLength.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); } @@ -286,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 45edf52519e..59596ba7e68 100644 --- a/src/inet/common/packet/recorder/PcapngWriter.h +++ b/src/inet/common/packet/recorder/PcapngWriter.h @@ -27,7 +27,7 @@ class INET_API PcapngWriter : public IPcapWriter bool flush = false; int nextPcapngInterfaceId = 0; int timePrecision = 6; - std::map interfaceModuleIdToPcapngInterfaceId; + std::map, int> interfaceModuleIdAndLinkTypeToPcapngInterfaceId; public: /** @@ -61,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. @@ -76,4 +78,3 @@ class INET_API PcapngWriter : public IPcapWriter } // namespace inet #endif - 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. From 5414ca59c32e1b25ab12b063293d9b1d562e6b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 19:58:00 +0200 Subject: [PATCH 3/5] recorder: add: register capture formats and wireless observations Allow optional protocol adapters to select a link type and packet ranges through generic registration points. Preserve the recorder's existing subclass hooks and conversion-disabled behavior. Move direct Signal, transmission and reception handling into the wireless feature so the recorder no longer implements that translation. Registry and observation tests accompany the infrastructure; adapters remain disabled by default. Keep the resolved adapter with the protocol and offsets so production and tests share one resolution API. Change: src.common.packet.recorder | behavior.add | test whatsnew | radiotap-capture --- .oppfeatures | 1 + .../packet/recorder/IPcapCaptureAdapter.h | 65 +++++ .../recorder/PcapCaptureAdapterRegistry.cc | 82 ++++++ .../recorder/PcapCaptureAdapterRegistry.h | 43 +++ .../common/packet/recorder/PcapRecorder.cc | 258 +++++++++++++----- .../common/packet/recorder/PcapRecorder.h | 22 +- .../common/packet/recorder/PcapRecorder.ned | 7 +- .../WirelessPcapCaptureObservationAdapter.cc | 36 +++ .../WirelessPcapCaptureObservationAdapter.h | 24 ++ tests/unit/PcapCaptureAdapterRegistry_1.test | 83 ++++++ ...relessPcapCaptureObservationAdapter_1.test | 108 ++++++++ 11 files changed, 663 insertions(+), 66 deletions(-) create mode 100644 src/inet/common/packet/recorder/IPcapCaptureAdapter.h create mode 100644 src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc create mode 100644 src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.h create mode 100644 src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.cc create mode 100644 src/inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.h create mode 100644 tests/unit/PcapCaptureAdapterRegistry_1.test create mode 100644 tests/unit/WirelessPcapCaptureObservationAdapter_1.test diff --git a/.oppfeatures b/.oppfeatures index c6eaa90f7b3..34615ed1714 100644 --- a/.oppfeatures +++ b/.oppfeatures @@ -1171,6 +1171,7 @@ inet.physicallayer.wireless.common.neighborcache inet.physicallayer.wireless.common.obstacleloss inet.physicallayer.wireless.common.pathloss + inet.physicallayer.wireless.common.pcap inet.physicallayer.wireless.common.propagation inet.physicallayer.wireless.common.radio inet.physicallayer.wireless.common.signal diff --git a/src/inet/common/packet/recorder/IPcapCaptureAdapter.h b/src/inet/common/packet/recorder/IPcapCaptureAdapter.h new file mode 100644 index 00000000000..53103e6d168 --- /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 = 0; + 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/PcapCaptureAdapterRegistry.cc b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc new file mode 100644 index 00000000000..adb81b176e9 --- /dev/null +++ b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.cc @@ -0,0 +1,82 @@ +// +// 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::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..03011cc7b13 --- /dev/null +++ b/src/inet/common/packet/recorder/PcapCaptureAdapterRegistry.h @@ -0,0 +1,43 @@ +// +// Copyright (C) 2026 OpenSim Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later +// + +#ifndef __INET_PCAPCAPTUREADAPTERREGISTRY_H +#define __INET_PCAPCAPTUREADAPTERREGISTRY_H + +#include +#include + +#include "inet/common/packet/recorder/IPcapCaptureAdapter.h" + +namespace inet { + +#define Register_Pcap_Capture_Adapter(PROTOCOL, CLASSNAME) EXECUTE_PRE_NETWORK_SETUP(::inet::PcapCaptureAdapterRegistry::getInstance().registerProtocolAdapter(PROTOCOL, new CLASSNAME())); +#define Register_Pcap_Capture_Protocol_Resolver(OUTER_PROTOCOL, CAPTURE_PROTOCOL) EXECUTE_PRE_NETWORK_SETUP(::inet::PcapCaptureAdapterRegistry::getInstance().registerProtocolResolver(OUTER_PROTOCOL, CAPTURE_PROTOCOL)); +#define Register_Pcap_Capture_Observation_Adapter(KEY, CLASSNAME) EXECUTE_PRE_NETWORK_SETUP(::inet::PcapCaptureAdapterRegistry::getInstance().registerObservationAdapter(KEY, new CLASSNAME())); + +class INET_API PcapCaptureAdapterRegistry +{ + protected: + std::map protocolAdapters; + std::map protocolResolvers; + std::map observationAdapters; + + public: + ~PcapCaptureAdapterRegistry(); + + void registerProtocolAdapter(const Protocol *protocol, const IPcapCaptureAdapter *adapter); + void registerProtocolResolver(const Protocol *outerProtocol, const Protocol *captureProtocol); + void registerObservationAdapter(const char *key, const IPcapCaptureObservationAdapter *adapter); + const IPcapCaptureAdapter *findProtocolAdapter(const Protocol *protocol) const; + std::optional> 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/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..85fd31b636f --- /dev/null +++ b/tests/unit/PcapCaptureAdapterRegistry_1.test @@ -0,0 +1,83 @@ +%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.tryResolveProtocolWithAdapter(&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)); +REQUIRE(std::get<3>(*resolution) == 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/WirelessPcapCaptureObservationAdapter_1.test b/tests/unit/WirelessPcapCaptureObservationAdapter_1.test new file mode 100644 index 00000000000..2877efcc79a --- /dev/null +++ b/tests/unit/WirelessPcapCaptureObservationAdapter_1.test @@ -0,0 +1,108 @@ +%description: +Test wireless PCAP observation translation for signals, transmissions, receptions, and unsupported objects. + +%includes: +#include "inet/physicallayer/common/Signal.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/IReception.h" +#include "inet/physicallayer/wireless/common/contract/packetlevel/ITransmission.h" +#include "inet/physicallayer/wireless/common/pcap/WirelessPcapCaptureObservationAdapter.h" + +%global: +using namespace inet; +using namespace inet::physicallayer; +#define REQUIRE(...) do { if (!(__VA_ARGS__)) throw cRuntimeError("REQUIRE failed at line %d: %s", __LINE__, #__VA_ARGS__); } while (false) + +class FakeTransmission : public cObject, public ITransmission +{ + public: + const Packet *packet; + FakeTransmission(const Packet *packet) : packet(packet) {} + virtual std::ostream& printToStream(std::ostream& stream, int, int = 0) const override { return stream; } + virtual int getId() const override { return 1; } + virtual const IRadio *getTransmitterRadio() const override { return nullptr; } + virtual int getTransmitterRadioId() const override { return -1; } + virtual const IAntennaGain *getTransmitterAntennaGain() const override { return nullptr; } + virtual const IRadioMedium *getMedium() const override { return nullptr; } + virtual const Packet *getPacket() const override { return packet; } + virtual const simtime_t getStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getStartTime(IRadioSignal::SignalPart) const override { return SIMTIME_ZERO; } + virtual const simtime_t getEndTime(IRadioSignal::SignalPart) const override { return SIMTIME_ZERO; } + virtual const simtime_t getPreambleStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getPreambleEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getHeaderStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getHeaderEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDataStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDataEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDuration() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDuration(IRadioSignal::SignalPart) const override { return SIMTIME_ZERO; } + virtual const simtime_t getPreambleDuration() const override { return SIMTIME_ZERO; } + virtual const simtime_t getHeaderDuration() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDataDuration() const override { return SIMTIME_ZERO; } + virtual const Coord& getStartPosition() const override { static Coord value; return value; } + virtual const Coord& getEndPosition() const override { static Coord value; return value; } + virtual const Quaternion& getStartOrientation() const override { static Quaternion value; return value; } + virtual const Quaternion& getEndOrientation() const override { static Quaternion value; return value; } + virtual const ITransmissionPacketModel *getPacketModel() const override { return nullptr; } + virtual const ITransmissionBitModel *getBitModel() const override { return nullptr; } + virtual const ITransmissionSymbolModel *getSymbolModel() const override { return nullptr; } + virtual const ITransmissionSampleModel *getSampleModel() const override { return nullptr; } + virtual const ITransmissionAnalogModel *getAnalogModel() const override { return nullptr; } +}; + +class FakeReception : public cObject, public IReception +{ + public: + const ITransmission *transmission; + FakeReception(const ITransmission *transmission) : transmission(transmission) {} + virtual std::ostream& printToStream(std::ostream& stream, int, int = 0) const override { return stream; } + virtual const IRadio *getReceiverRadio() const override { return nullptr; } + virtual const ITransmission *getTransmission() const override { return transmission; } + virtual const simtime_t getStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getStartTime(IRadioSignal::SignalPart) const override { return SIMTIME_ZERO; } + virtual const simtime_t getEndTime(IRadioSignal::SignalPart) const override { return SIMTIME_ZERO; } + virtual const simtime_t getPreambleStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getPreambleEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getHeaderStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getHeaderEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDataStartTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDataEndTime() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDuration() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDuration(IRadioSignal::SignalPart) const override { return SIMTIME_ZERO; } + virtual const simtime_t getPreambleDuration() const override { return SIMTIME_ZERO; } + virtual const simtime_t getHeaderDuration() const override { return SIMTIME_ZERO; } + virtual const simtime_t getDataDuration() const override { return SIMTIME_ZERO; } + virtual const Coord& getStartPosition() const override { static Coord value; return value; } + virtual const Coord& getEndPosition() const override { static Coord value; return value; } + virtual const Quaternion& getStartOrientation() const override { static Quaternion value; return value; } + virtual const Quaternion& getEndOrientation() const override { static Quaternion value; return value; } + virtual const IReceptionAnalogModel *getAnalogModel() const override { return nullptr; } +}; + +%activity: +WirelessPcapCaptureObservationAdapter adapter; +Packet packet("packet"); +Signal signal("signal"); +signal.encapsulate(packet.dup()); +auto signalObservation = adapter.tryCreateObservation(&signal, DIRECTION_OUTBOUND); +REQUIRE(signalObservation && signalObservation->packet == signal.getEncapsulatedPacket()); +REQUIRE(signalObservation->direction == DIRECTION_OUTBOUND && signalObservation->transmission == nullptr && signalObservation->reception == nullptr); + +FakeTransmission transmission(&packet); +auto transmissionObservation = adapter.tryCreateObservation(&transmission, DIRECTION_OUTBOUND); +REQUIRE(transmissionObservation && transmissionObservation->packet == &packet); +REQUIRE(transmissionObservation->transmission == &transmission && transmissionObservation->reception == nullptr); + +FakeReception reception(&transmission); +auto receptionObservation = adapter.tryCreateObservation(&reception, DIRECTION_INBOUND); +REQUIRE(receptionObservation && receptionObservation->packet == &packet); +REQUIRE(receptionObservation->direction == DIRECTION_INBOUND); +REQUIRE(receptionObservation->transmission == &transmission && receptionObservation->reception == &reception); + +cObject unsupported; +REQUIRE(!adapter.tryCreateObservation(&unsupported, DIRECTION_UNDEFINED)); +EV << "Wireless PCAP observation translation tested successfully.\n"; + +%contains: stdout +Wireless PCAP observation translation tested successfully. From 418979395547432b32ec9a3b557885271eb6299b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 19:58:01 +0200 Subject: [PATCH 4/5] ieee80211: add: opt-in Radiotap capture Expose available legacy, HT and VHT SU metadata in capture files while retaining bare MAC output by default. Split typed A-MPDUs into MPDUs and preserve malformed aggregates as whole PSDUs. Honor FcsInd only for intact imported MAC frames; selected ranges retain trailer-based FCS detection. Field and aggregate tests accompany a signal-driven recorder/reader/replay test, which preserves FCS presence even when imported frames are raw bytes. Preserve the selected HR-DSSS short preamble in Radiotap Flags independently of FCS. Cover request and indication tags and transmission precedence. Change: src.ieee80211 | behavior.add | test whatsnew | radiotap-capture --- WHATSNEW | 8 + doc/src/users-guide/ch-collecting-results.rst | 33 ++ .../Ieee80211RadiotapPcapCaptureAdapter.cc | 526 ++++++++++++++++++ .../Ieee80211RadiotapPcapCaptureAdapter.h | 26 + .../PcapRecorderRadiotapRoundTrip_1.test | 131 +++++ tests/unit/PcapRecorderFcsInd_1.test | 73 +++ tests/unit/PcapRecorderIeee80211Ampdu_1.test | 267 +++++++++ tests/unit/PcapRecorderRadiotapHtVht_1.test | 218 ++++++++ 8 files changed, 1282 insertions(+) create mode 100644 src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc create mode 100644 src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.h create mode 100644 tests/module/PcapRecorderRadiotapRoundTrip_1.test create mode 100644 tests/unit/PcapRecorderFcsInd_1.test create mode 100644 tests/unit/PcapRecorderIeee80211Ampdu_1.test create mode 100644 tests/unit/PcapRecorderRadiotapHtVht_1.test diff --git a/WHATSNEW b/WHATSNEW index 3f06cf4bc06..4688b0d0242 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -128,6 +128,14 @@ Notable backward compatible changes are the following: Those two tags now survive the measurement. +6. Opt-in IEEE 802.11 Radiotap capture + + PcapRecorder can use registered protocol-specific capture adapters to write + Radiotap metadata for legacy, HT and VHT SU frames. Enable + enableProtocolSpecificCaptureAdapters together with enableConvertingPackets. + Bare IEEE 802.11 (link type 105) remains the default. Typed A-MPDUs are split + into MPDU records, and intact imported frames retain their FCS indication. + INET-4.7 (July 2026) — feature release -------------------------------------- diff --git a/doc/src/users-guide/ch-collecting-results.rst b/doc/src/users-guide/ch-collecting-results.rst index b806a29d501..d101cc7e394 100644 --- a/doc/src/users-guide/ch-collecting-results.rst +++ b/doc/src/users-guide/ch-collecting-results.rst @@ -152,6 +152,39 @@ protocol work and verification rather than merely changing recorder output, so they are part of the simulation configuration and should be held constant when comparing runs. +IEEE 802.11 Radiotap captures +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +IEEE 802.11 captures use bare MAC frames (link type 105) by default. To include +Radiotap metadata (link type 127), enable protocol-specific capture adapters: + +.. code-block:: ini + + *.host.numPcapRecorders = 1 + *.host.pcapRecorder[0].pcapFile = "wlan.pcap" + *.host.pcapRecorder[0].fileFormat = "pcap" + *.host.pcapRecorder[0].dumpProtocols = "ieee80211mac" + *.host.pcapRecorder[0].enableProtocolSpecificCaptureAdapters = true + *.host.pcapRecorder[0].enableConvertingPackets = true + *.host.wlan[*].mac.fcsMode = "computed" + +Both adapter and conversion options must be enabled. PCAPng also supports +Radiotap; use it when a file contains multiple interfaces or link types. +The capture length limit includes the Radiotap header. + +The adapter records available legacy rate and short preamble, HT MCS, VHT SU, +channel, power and direction information. Metadata depends on the observed signal and PHY model; +unavailable fields are omitted. HE and EHT metadata are not supported. +FCS presence is taken from the selected frame's typed trailer or, for an intact +MAC frame imported from a capture, its FCS indication. An explicit indication +of absence takes precedence over a trailer. FCS indications are not applied to +selected subranges or individual members of an aggregate. + +Recognized typed A-MPDUs produce one record per MPDU, without delimiters and +padding. Inbound records include A-MPDU status. Unparseable aggregates are +preserved as one whole-PSDU record; an analyzer may not decode that fallback as +a MAC frame. Raw bytes are not guessed to be an aggregate. + .. _ug:sec:results:recording-routing-tables: Recording Routing Tables diff --git a/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc new file mode 100644 index 00000000000..9cc4cab66a4 --- /dev/null +++ b/src/inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.cc @@ -0,0 +1,526 @@ +// +// 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/FcsInd_m.h" +#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/Ieee80211HrDsssMode.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_SHORTPRE = 0x02, + 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 hasShortPreamble = false; + 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; + // FcsInd describes the imported MAC frame, not arbitrary ranges or aggregate members. + // Popped data and protocol-tag offsets also restrict the view of that original frame. + auto protocolTag = packet->findTag(); + if (frontOffset == b(0) && backOffset == b(0) && packet->getDataLength() == packet->getTotalLength() && + protocolTag != nullptr && protocolTag->getProtocol() == &Protocol::ieee80211Mac && + protocolTag->getFrontOffset() == b(0) && protocolTag->getBackOffset() == b(0)) { + if (auto fcsInd = packet->findTag()) { + metadata.isPresent = fcsInd->getHasFcs(); + if (!metadata.isPresent) + return 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) { + // Radiotap Flags 0x02 describes the selected legacy preamble, independently of FCS. + // https://www.radiotap.org/fields/Flags.html + if (auto preambleMode = dynamic_cast(mode->getPreambleMode())) + fields.hasShortPreamble = preambleMode->getPreambleType() == physicallayer::IEEE80211_HRDSSS_PREAMBLE_TYPE_SHORT; + 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((fields.hasShortPreamble ? RADIOTAP_F_SHORTPRE : 0) | + (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/tests/module/PcapRecorderRadiotapRoundTrip_1.test b/tests/module/PcapRecorderRadiotapRoundTrip_1.test new file mode 100644 index 00000000000..d32ba41c61c --- /dev/null +++ b/tests/module/PcapRecorderRadiotapRoundTrip_1.test @@ -0,0 +1,131 @@ +%description: +Exercise real PcapRecorder signal subscriptions, configuration, adapter dispatch, +prefix writing and PcapReader. A typed ACK with FCS and raw ACKs without FCS are +recorded in legacy, HT and VHT modes. Re-record imported BytesChunks to prove that +FcsInd preserves presence. Default and conversion-disabled recorders retain DLT 105. + +%includes: +#include +#include +#include "inet/common/FcsInd_m.h" +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/checksum/Checksum.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/common/packet/recorder/PcapReader.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.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" + +%file: TestNetwork.ned +import inet.common.packet.recorder.PcapRecorder; + +network TestNetwork +{ + submodules: + test: Test; + radiotap: PcapRecorder; + replay: PcapRecorder; + plain: PcapRecorder; + conversionDisabled: PcapRecorder; +} + +%inifile: omnetpp.ini +[General] +network = TestNetwork +cmdenv-express-mode = false +**.verbose = false +**.moduleNamePatterns = "test" +**.sendingSignalNames = "capture" +**.receivingSignalNames = "" +**.fileFormat = "pcap" +**.alwaysFlush = true +*.radiotap.pcapFile = "radiotap.pcap" +*.radiotap.enableProtocolSpecificCaptureAdapters = true +*.replay.pcapFile = "replay.pcap" +*.replay.sendingSignalNames = "replay" +*.replay.enableProtocolSpecificCaptureAdapters = true +*.plain.pcapFile = "plain.pcap" +*.conversionDisabled.pcapFile = "conversion-disabled.pcap" +*.conversionDisabled.enableProtocolSpecificCaptureAdapters = true +*.conversionDisabled.enableConvertingPackets = false + +%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 uint32_t linkType(const char *filename) +{ + std::ifstream file(filename, std::ios::binary); + pcap_hdr header; + file.read(reinterpret_cast(&header), sizeof(header)); + REQUIRE(file.good()); + return header.network; +} + +%activity: +auto capture = registerSignal("capture"); +auto replay = registerSignal("replay"); +Packet packet("ack"); +auto ack = makeShared(); +ack->setDurationField(SIMTIME_ZERO); +ack->setReceiverAddress(MacAddress("02:00:00:00:00:01")); +packet.insertAtBack(ack); +auto noFcsBytes = packet.peekDataAsBytes()->getBytes(); +auto trailer = makeShared(); +trailer->setFcsMode(FCS_COMPUTED); +trailer->setFcs(ethernetFcs(noFcsBytes)); +packet.insertAtBack(trailer); +packet.addTag()->setProtocol(&Protocol::ieee80211Mac); +packet.addTag()->setMode(&Ieee80211OfdmCompliantModes::getCompliantMode(13, MHz(20))); +auto withFcsBytes = packet.peekDataAsBytes()->getBytes(); +emit(capture, &packet); + +Packet raw("ack-no-fcs", makeShared(noFcsBytes)); +raw.addTag()->setProtocol(&Protocol::ieee80211Mac); +raw.addTag()->setHasFcs(false); +raw.addTag()->setMode(Ieee80211HtCompliantModes::getCompliantMode( + &Ieee80211HtmcsTable::htMcs7BW40MHz, Ieee80211HtMode::BAND_5GHZ, + Ieee80211HtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211HtModeBase::HT_GUARD_INTERVAL_SHORT)); +emit(capture, &raw); +raw.getTagForUpdate()->setMode(Ieee80211VhtCompliantModes::getCompliantMode( + &Ieee80211VhtmcsTable::vhtMcs0BW20MHzNss1, Ieee80211VhtMode::BAND_5GHZ, + Ieee80211VhtPreambleMode::HT_PREAMBLE_MIXED, Ieee80211VhtModeBase::HT_GUARD_INTERVAL_LONG)); +emit(capture, &raw); +REQUIRE(linkType("radiotap.pcap") == 127); +REQUIRE(linkType("plain.pcap") == 105); +REQUIRE(linkType("conversion-disabled.pcap") == 105); + +PcapReader reader; +reader.openPcap("radiotap.pcap", nullptr); +for (int i = 0; i < 3; i++) { + std::unique_ptr imported(reader.readPacket().second); + REQUIRE(imported != nullptr); + REQUIRE(imported->getTag()->getProtocol() == &Protocol::ieee80211Mac); + REQUIRE(imported->getTag()->getHasFcs() == (i == 0)); + REQUIRE(imported->peekDataAsBytes()->getBytes() == (i == 0 ? withFcsBytes : noFcsBytes)); + REQUIRE(imported->peekAtFront(B(10))->getReceiverAddress() == ack->getReceiverAddress()); + emit(replay, imported.get()); +} +REQUIRE(reader.readPacket().second == nullptr); +reader.closePcap(); +reader.openPcap("replay.pcap", nullptr); +for (int i = 0; i < 3; i++) { + std::unique_ptr imported(reader.readPacket().second); + REQUIRE(imported != nullptr); + REQUIRE(imported->getTag()->getHasFcs() == (i == 0)); + REQUIRE(imported->peekDataAsBytes()->getBytes() == (i == 0 ? withFcsBytes : noFcsBytes)); +} +REQUIRE(reader.readPacket().second == nullptr); +reader.closePcap(); +EV << "Recorder Radiotap round trip and DLT 105 compatibility verified.\n"; + +%contains: stdout +Recorder Radiotap round trip and DLT 105 compatibility verified. + +%not-contains: stdout +undisposed object: diff --git a/tests/unit/PcapRecorderFcsInd_1.test b/tests/unit/PcapRecorderFcsInd_1.test new file mode 100644 index 00000000000..4cc6b917cb3 --- /dev/null +++ b/tests/unit/PcapRecorderFcsInd_1.test @@ -0,0 +1,73 @@ +%description: +Capture FCS indications apply only to the complete imported MAC frame. Selected +ranges and aggregate members use their own trailers; explicit absence wins over +a conflicting trailer, while explicit presence may use its BADFCS information. + +%includes: +#include "inet/common/FcsInd_m.h" +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/linklayer/ieee80211/mac/Ieee80211Frame_m.h" +#include "inet/linklayer/ieee80211/pcap/Ieee80211RadiotapPcapCaptureAdapter.h" + +%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) + +static uint8_t captureFlags(Packet& packet, b front = b(0), b back = b(0)) +{ + Ieee80211RadiotapPcapCaptureAdapter adapter; + auto records = adapter.createRecords(PcapCaptureObservation(&packet, DIRECTION_INBOUND), front, back); + REQUIRE(records.size() == 1); + return records.front().getPrefix().at(8); +} + +%activity: +Packet raw("raw"); +raw.insertAtBack(makeShared(std::vector(12, 0))); +raw.addTag()->setProtocol(&Protocol::ieee80211Mac); +REQUIRE(captureFlags(raw) == 0); +raw.addTag()->setHasFcs(true); +REQUIRE(captureFlags(raw) == 0x10); +REQUIRE(captureFlags(raw, B(1), b(0)) == 0); +REQUIRE(captureFlags(raw, b(0), B(4)) == 0); +raw.getTagForUpdate()->setFrontOffset(B(1)); +REQUIRE(captureFlags(raw) == 0); +raw.getTagForUpdate()->setFrontOffset(b(0)); +raw.setBackOffset(raw.getTotalLength() - B(4)); +REQUIRE(captureFlags(raw) == 0); +raw.setBackOffset(raw.getTotalLength()); +raw.setFrontOffset(B(1)); +REQUIRE(captureFlags(raw) == 0); +raw.setFrontOffset(b(0)); +raw.getTagForUpdate()->setHasFcs(false); +REQUIRE(captureFlags(raw) == 0); + +Packet typed("typed"); +typed.insertAtBack(makeShared(std::vector(8, 0))); +auto trailer = makeShared(); +trailer->setFcsMode(FCS_DECLARED_INCORRECT); +typed.insertAtBack(trailer); +typed.addTag()->setProtocol(&Protocol::ieee80211Mac); +REQUIRE(captureFlags(typed) == 0x50); +typed.addTag()->setHasFcs(false); +REQUIRE(captureFlags(typed) == 0); +typed.getTagForUpdate()->setHasFcs(true); +REQUIRE(captureFlags(typed) == 0x50); +REQUIRE(captureFlags(typed, b(0), B(4)) == 0); + +Packet aggregate("aggregate"); +auto delimiter = makeShared(); +delimiter->setLength(12); +aggregate.insertAtBack(delimiter); +aggregate.insertAtBack(typed.peekData()); +aggregate.addTag()->setProtocol(&Protocol::ieee80211Mac); +aggregate.addTag()->setHasFcs(false); +REQUIRE(captureFlags(aggregate) == 0x50); // packet-level absence must not override the member trailer + +EV << "FcsInd scope and trailer precedence verified.\n"; + +%contains: stdout +FcsInd scope and trailer precedence verified. 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..1ea84a8cf72 --- /dev/null +++ b/tests/unit/PcapRecorderRadiotapHtVht_1.test @@ -0,0 +1,218 @@ +%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/common/antenna/IsotropicAntenna.h" +#include "inet/physicallayer/wireless/ieee80211/mode/Ieee80211HrDsssMode.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" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Radio.h" +#include "inet/physicallayer/wireless/ieee80211/packetlevel/Ieee80211Transmission.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) + +class CaptureTestRadio : public Ieee80211Radio +{ + protected: + IsotropicAntenna captureAntenna; + + public: + virtual const IAntenna *getAntenna() const override { return &captureAntenna; } + virtual const IRadioMedium *getMedium() const override { return nullptr; } +}; + +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 + +CaptureTestRadio captureRadio; +const IIeee80211Mode *hrDsssModes[] = { + &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckLongPreamble, + &Ieee80211HrDsssCompliantModes::hrDsssMode11MbpsCckShortPreamble +}; +for (int shortPreamble = 0; shortPreamble < 2; shortPreamble++) { + Packet hrDsss("hrDsss"); + hrDsss.insertAtBack(makeShared(std::vector{1, 2, 3, 4})); + auto trailer = makeShared(); + trailer->setFcsMode(FCS_DECLARED_INCORRECT); + hrDsss.insertAtBack(trailer); + const auto expectedFlags = 0x50 | (shortPreamble ? 0x02 : 0); // FCS, BADFCS, optional short preamble + hrDsss.addTag()->setMode(hrDsssModes[shortPreamble]); + auto requestHeader = createRadiotapHeader(adapter, hrDsss); + REQUIRE(requestHeader.at(8) == expectedFlags && requestHeader.at(9) == 22); + + hrDsss.removeTag(); + hrDsss.addTag()->setMode(hrDsssModes[shortPreamble]); + auto indicationHeader = createRadiotapHeader(adapter, hrDsss); + REQUIRE(indicationHeader.at(8) == expectedFlags && indicationHeader.at(9) == 22); + + // The actual transmission is authoritative even when a packet tag disagrees. + hrDsss.getTagForUpdate()->setMode(hrDsssModes[1 - shortPreamble]); + Ieee80211Transmission transmission(&captureRadio, &hrDsss, SIMTIME_ZERO, SIMTIME_ZERO, + SIMTIME_ZERO, SIMTIME_ZERO, SIMTIME_ZERO, Coord(), Coord(), Quaternion(), Quaternion(), + nullptr, nullptr, nullptr, nullptr, nullptr, hrDsssModes[shortPreamble], nullptr); + auto transmissionRecords = adapter.createRecords(PcapCaptureObservation(&hrDsss, DIRECTION_OUTBOUND, &transmission), b(0), b(0)); + REQUIRE(transmissionRecords.size() == 1); + const auto& transmissionHeader = transmissionRecords.front().getPrefix(); + REQUIRE(transmissionHeader.at(8) == expectedFlags && transmissionHeader.at(9) == 22); +} + +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. From 6433d9e442f5abe7950c6d750f8c45516fcb4059 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Gonz=C3=A1lez=20L=C3=B3pez?= Date: Tue, 15 Sep 2026 22:39:51 +0200 Subject: [PATCH 5/5] recorder: fix: preserve legacy write hooks for capture adapters Enabling Radiotap bypassed subclasses overriding the packet-only writePacket overload, silently skipping their filtering or replacement logic. Invoke that override before adapter record expansion and retain borrowed PHY context only when the override forwards the original packet. Restore the observation context after normal returns and exceptions. Add a signal-driven module test that suppresses and replaces packets in plain and Radiotap captures. Before the fix it fails because the Radiotap recorder never invokes the legacy override. Validation: debug build, seven capture unit tests, both recorder module tests, and ten scoped legacy fingerprint cases passed. An isolated debug build with Ieee80211 disabled and its feature smoke simulation also passed. No fingerprint baselines changed. Change: src.common.packet.recorder.PcapRecorder | behavior.change.fix | test | radiotap-capture --- .../common/packet/recorder/PcapRecorder.cc | 33 ++++--- .../common/packet/recorder/PcapRecorder.h | 2 +- .../module/PcapRecorderLegacyWriteHook_1.test | 98 +++++++++++++++++++ 3 files changed, 120 insertions(+), 13 deletions(-) create mode 100644 tests/module/PcapRecorderLegacyWriteHook_1.test diff --git a/src/inet/common/packet/recorder/PcapRecorder.cc b/src/inet/common/packet/recorder/PcapRecorder.cc index 08c618d4790..21d3e47b413 100644 --- a/src/inet/common/packet/recorder/PcapRecorder.cc +++ b/src/inet/common/packet/recorder/PcapRecorder.cc @@ -223,9 +223,29 @@ void PcapRecorder::receiveSignal(cComponent *source, simsignal_t signalID, cObje void PcapRecorder::writePacket(const Protocol *protocol, const PcapCaptureObservation& observation, b frontOffset, b backOffset, NetworkInterface *networkInterface) { - auto packet = observation.packet; + // Preserve the established packet-only override before expanding adapter records. As with + // recordPacket(), nested calls and exceptions must restore the borrowed observation. + auto previousObservation = activeCaptureObservation; + activeCaptureObservation = &observation; + try { + writePacket(protocol, observation.packet, frontOffset, backOffset, observation.direction, networkInterface); + activeCaptureObservation = previousObservation; + } + catch (...) { + activeCaptureObservation = previousObservation; + throw; + } +} + +void PcapRecorder::writePacket(const Protocol *protocol, const Packet *packet, b frontOffset, b backOffset, Direction direction, NetworkInterface *networkInterface) +{ auto adapter = findProtocolCaptureAdapter(protocol); if (adapter != nullptr) { + // A legacy override may replace the packet. Only the original packet can retain the + // borrowed PHY context; use the direction forwarded by the override in either case. + const PcapCaptureObservation observation = activeCaptureObservation != nullptr && activeCaptureObservation->packet == packet ? + PcapCaptureObservation(packet, direction, activeCaptureObservation->transmission, activeCaptureObservation->reception) : + PcapCaptureObservation(packet, direction); // 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); @@ -245,17 +265,6 @@ void PcapRecorder::writePacket(const Protocol *protocol, const PcapCaptureObserv 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 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()); diff --git a/src/inet/common/packet/recorder/PcapRecorder.h b/src/inet/common/packet/recorder/PcapRecorder.h index 5e6d214ab2b..dd1f1df3450 100644 --- a/src/inet/common/packet/recorder/PcapRecorder.h +++ b/src/inet/common/packet/recorder/PcapRecorder.h @@ -56,7 +56,7 @@ class INET_API PcapRecorder : public SimpleModule, protected cListener, public P bool enableProtocolSpecificCaptureAdapters = false; bool recordPcap = false; PcapCaptureAdapterRegistry *captureAdapterRegistry = nullptr; - // Transiently carries enriched capture data through the legacy virtual recordPacket(cPacket *) hook. + // Transiently carries enriched capture data through the legacy recordPacket() and writePacket() hooks. const PcapCaptureObservation *activeCaptureObservation = nullptr; // Transiently carries one resolved protocol adapter through the legacy virtual writePacket() hooks. bool captureAdapterResolutionActive = false; diff --git a/tests/module/PcapRecorderLegacyWriteHook_1.test b/tests/module/PcapRecorderLegacyWriteHook_1.test new file mode 100644 index 00000000000..86d69ca06f2 --- /dev/null +++ b/tests/module/PcapRecorderLegacyWriteHook_1.test @@ -0,0 +1,98 @@ +%description: +Legacy packet-only write overrides filter and replace packets in both plain and +Radiotap captures reached through the production signal path. + +%includes: +#include +#include "inet/common/ProtocolTag_m.h" +#include "inet/common/packet/chunk/BytesChunk.h" +#include "inet/common/packet/recorder/PcapReader.h" +#include "inet/common/packet/recorder/PcapRecorder.h" + +%file: TestNetwork.ned +import inet.common.packet.recorder.PcapRecorder; + +simple LegacyWriteRecorder extends PcapRecorder +{ + parameters: + @class(LegacyWriteRecorder); +} + +network TestNetwork +{ + submodules: + test: Test; + plain: LegacyWriteRecorder; + radiotap: LegacyWriteRecorder; +} + +%inifile: omnetpp.ini +[General] +network = TestNetwork +**.verbose = false +**.moduleNamePatterns = "test" +**.sendingSignalNames = "capture" +**.receivingSignalNames = "" +**.fileFormat = "pcap" +**.alwaysFlush = true +*.plain.pcapFile = "plain.pcap" +*.radiotap.pcapFile = "radiotap.pcap" +*.radiotap.enableProtocolSpecificCaptureAdapters = true + +%global: +using namespace inet; + +#define REQUIRE(...) do { if (!(__VA_ARGS__)) throw cRuntimeError("REQUIRE failed at line %d: %s", __LINE__, #__VA_ARGS__); } while (false) + +class LegacyWriteRecorder : public PcapRecorder +{ + public: + int calls = 0; + bool suppress = true; + + protected: + virtual void writePacket(const Protocol *protocol, const Packet *packet, b frontOffset, b backOffset, + Direction direction, NetworkInterface *networkInterface) override + { + calls++; + if (suppress) + return; + auto bytes = packet->peekDataAsBytes()->getBytes(); + bytes.back() = 0x55; + Packet replacement("redacted", makeShared(bytes)); + PcapRecorder::writePacket(protocol, &replacement, frontOffset, backOffset, direction, networkInterface); + } +}; + +Define_Module(LegacyWriteRecorder); + +%activity: +auto plain = check_and_cast(getModuleByPath("plain")); +auto radiotap = check_and_cast(getModuleByPath("radiotap")); +auto capture = registerSignal("capture"); +std::vector bytes = {0xd4, 0, 0, 0, 2, 0, 0, 0, 0, 0x11}; // ACK without FCS +Packet packet("original", makeShared(bytes)); +packet.addTag()->setProtocol(&Protocol::ieee80211Mac); +emit(capture, &packet); +REQUIRE(plain->calls == 1); +REQUIRE(radiotap->calls == 1); +plain->suppress = false; +radiotap->suppress = false; +emit(capture, &packet); +REQUIRE(plain->calls == 2); +REQUIRE(radiotap->calls == 2); +REQUIRE(packet.peekDataAsBytes()->getBytes() == bytes); +bytes.back() = 0x55; +for (auto filename : {"plain.pcap", "radiotap.pcap"}) { + PcapReader reader; + reader.openPcap(filename, nullptr); + std::unique_ptr recorded(reader.readPacket().second); + REQUIRE(recorded != nullptr); + REQUIRE(recorded->peekDataAsBytes()->getBytes() == bytes); + REQUIRE(reader.readPacket().second == nullptr); + reader.closePcap(); +} +EV << "Legacy write hooks suppress and redact plain and Radiotap records.\n"; + +%contains: stdout +Legacy write hooks suppress and redact plain and Radiotap records.