From 01be3c55f5117302fb719a8a4060c137b12caf79 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Fri, 16 Oct 2026 13:27:19 +0200 Subject: [PATCH] ipsec: fix: emit packetDropped for the datagrams IPsec discards IPsec discarded a datagram by returning DROP from its netfilter hook after emitting only its own counter signals (outDrop, inUnprotectedDrop, inProtectedDrop), and the network layer deleted the datagram without emitting packetDropped either. PacketDropVisualizer, which subscribes to packetDropped, and the statistics and result filters built on it did not see Security Policy Database (SPD) discards or any other IPsec discard. A new member function, emitPacketDropped(), emits packetDropped with a PacketDropDetails reason, and each of the 14 sites that count a discard calls it before returning DROP: with LIFETIME_EXPIRED for the two inbound sites where the hard lifetime of the Security Association has ended or the packet would exceed its byte limit, and with OTHER_PACKET_DROP for the others, including an outbound datagram whose only Security Association has expired. At the six inbound Authentication Header (AH) and Encapsulating Security Payload (ESP) sites that have already removed the network header, the datagram is restored before the emit: as it reached AH or ESP processing where the AH or ESP header is still at hand (as received, unless AH covered ESP), and as AH or ESP processing gives it back at the two lifetime sites. The discard of an ESP dummy packet stays silent, because Request for Comments (RFC) 4303 Section 2.6 makes it normal operation. IPsec.ned declares the signal and records packetDropLifetimeExpired and packetDropUndefined, like Ipv4.ned. tests/module/IPsec_packet_dropped.test discards three datagrams by an outbound SPD DISCARD entry at the client, and at the server three unprotected datagrams that no inbound SPD entry matches and three ESP-protected datagrams whose Security Parameters Index (SPI) has no Security Association, and checks that packetDropUndefined counts each IPsec module's outDrop, inUnprotectedDrop and inProtectedDrop discards. No fingerprint changes. The statistical baselines of examples/inet/ipsec gain the new scalars. Change: src.networklayer.ipsec.IPsec | behavior.change.fix | test statistical whatsnew --- WHATSNEW | 4 ++ src/inet/networklayer/ipsec/IPsec.cc | 42 ++++++++++++++ src/inet/networklayer/ipsec/IPsec.h | 2 + src/inet/networklayer/ipsec/IPsec.ned | 3 + tests/module/IPsec_packet_dropped.test | 79 ++++++++++++++++++++++++++ 5 files changed, 130 insertions(+) create mode 100644 tests/module/IPsec_packet_dropped.test diff --git a/WHATSNEW b/WHATSNEW index 68c0405dbb7..c1cca60e545 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -490,6 +490,10 @@ Notable bug fixes are the following: The HT mode factory now includes band and preamble format in its cache key. A 5 GHz request after an equivalent 2.4 GHz request therefore receives the requested band and timing. Previously, that request could receive the earlier mode. Identical complete requests still reuse one object. +2. IPsec reports the datagrams it discards + + IPsec discarded datagrams, for example by a Security Policy Database entry with the action DISCARD, without emitting packetDropped, so PacketDropVisualizer and the statistics built on that signal did not see these drops. IPsec now emits packetDropped at each discard it counts, with the reason LIFETIME_EXPIRED when an inbound packet arrives on a Security Association whose hard lifetime has ended or whose byte limit it would exceed, and OTHER_PACKET_DROP otherwise, and records the statistics packetDropLifetimeExpired and packetDropUndefined. A discarded Encapsulating Security Payload (ESP) dummy packet is still not reported. The event trajectory does not change. + INET-4.7 (July 2026) — feature release -------------------------------------- diff --git a/src/inet/networklayer/ipsec/IPsec.cc b/src/inet/networklayer/ipsec/IPsec.cc index 4b3e8fca7fd..bbaa3a1054d 100644 --- a/src/inet/networklayer/ipsec/IPsec.cc +++ b/src/inet/networklayer/ipsec/IPsec.cc @@ -486,6 +486,14 @@ void IPsec::sendDummyPacket(SecurityAssociation *sadEntry) send(packet, "ipOut"); } +void IPsec::emitPacketDropped(Packet *packet, PacketDropReason reason) +{ + // the hook returns DROP after this, and the network layer deletes the packet + PacketDropDetails details; + details.setReason(reason); + emit(packetDroppedSignal, packet, &details); +} + INetfilter::IHook::Result IPsec::datagramPostRoutingHook(Packet *packet) { // a dummy packet is already protected @@ -525,12 +533,14 @@ INetfilter::IHook::Result IPsec::processEgressPacket(Packet *packet, const L3Add } else if (spdEntry->getAction() == Action::DISCARD) { EV_INFO << "IPsec OUT DROP rule, packet: " << egressPacketInfo.str() << std::endl; + emitPacketDropped(packet, OTHER_PACKET_DROP); emit(outDropSignal, 1L); outDrop++; return INetfilter::IHook::DROP; } } EV_INFO << "IPsec OUT BYPASS, no matching rule, packet: " << egressPacketInfo.str() << std::endl; + emitPacketDropped(packet, OTHER_PACKET_DROP); emit(outDropSignal, 1L); outDrop++; return INetfilter::IHook::DROP; @@ -882,6 +892,7 @@ INetfilter::IHook::Result IPsec::protectDatagram(Packet *packet, const PacketInf // RFC 4301 section 5.1: the entry wants protection, and no SA of it covers the packet; // with no key management to create one, the packet is discarded, not sent in clear EV_INFO << "IPsec OUT DROP, no SA of the PROTECT entry matches, packet: " << packetInfo.str() << std::endl; + emitPacketDropped(packet, OTHER_PACKET_DROP); emit(outDropSignal, 1L); outDrop++; return INetfilter::IHook::DROP; @@ -939,6 +950,10 @@ INetfilter::IHook::Result IPsec::processIngressPacket(Packet *packet) if (sadEntry != nullptr) { if (sadEntry->getProtection() != Protection::AH) { EV_INFO << "IPsec IN DROP AH, SA does not prescribe AH, packet: " << ingressPacketInfo.str() << std::endl; + // report the datagram as received + packet->insertAtFront(ah); + packet->insertAtFront(netHeader); + emitPacketDropped(packet, OTHER_PACKET_DROP); emit(inProtectedDropSignal, 1L); inDrop++; return INetfilter::IHook::DROP; @@ -956,6 +971,11 @@ INetfilter::IHook::Result IPsec::processIngressPacket(Packet *packet) // RFC 4301 section 4.4.2.1: an SA whose hard lifetime has ended accepts nothing if (!sadEntry->isUsable(simTime(), packet->getByteLength())) { EV_INFO << "IPsec IN DROP AH, the hard lifetime of the SA has ended, packet: " << ingressPacketInfo.str() << std::endl; + // report the datagram as AH processing would give it back + setNetworkHeaderProtocol(netHeader, (IpProtocolId)ah->getNextHeader()); + updateNetworkHeaderLength(netHeader, packet); + packet->insertAtFront(netHeader); + emitPacketDropped(packet, LIFETIME_EXPIRED); emit(inProtectedDropSignal, 1L); inDrop++; return INetfilter::IHook::DROP; @@ -972,6 +992,7 @@ INetfilter::IHook::Result IPsec::processIngressPacket(Packet *packet) if (ah->getNextHeader() != IP_PROT_ESP) { if (!matchesSelectorsOfSa(packet, sadEntry)) { EV_INFO << "IPsec IN DROP AH, the packet does not match the selectors of its SA, packet: " << ingressPacketInfo.str() << std::endl; + emitPacketDropped(packet, OTHER_PACKET_DROP); emit(inProtectedDropSignal, 1L); inDrop++; return INetfilter::IHook::DROP; @@ -1004,6 +1025,10 @@ INetfilter::IHook::Result IPsec::processIngressPacket(Packet *packet) if (sadEntry == nullptr) { EV_INFO << "IPsec IN DROP AH, no matching rule, packet: " << ingressPacketInfo.str() << std::endl; + // report the datagram as received + packet->insertAtFront(ah); + packet->insertAtFront(netHeader); + emitPacketDropped(packet, OTHER_PACKET_DROP); emit(inProtectedDropSignal, 1L); inDrop++; return INetfilter::IHook::DROP; @@ -1019,6 +1044,10 @@ INetfilter::IHook::Result IPsec::processIngressPacket(Packet *packet) if (sadEntry != nullptr) { if (sadEntry->getProtection() != Protection::ESP) { EV_INFO << "IPsec IN DROP ESP, SA does not prescribe ESP, packet: " << ingressPacketInfo.str() << std::endl; + // report the datagram as ESP processing received it + packet->insertAtFront(espHeader); + packet->insertAtFront(netHeader); + emitPacketDropped(packet, OTHER_PACKET_DROP); emit(inProtectedDropSignal, 1L); inDrop++; return INetfilter::IHook::DROP; @@ -1044,6 +1073,11 @@ INetfilter::IHook::Result IPsec::processIngressPacket(Packet *packet) // RFC 4301 section 4.4.2.1: an SA whose hard lifetime has ended accepts nothing if (!sadEntry->isUsable(simTime(), packet->getByteLength())) { EV_INFO << "IPsec IN DROP ESP, the hard lifetime of the SA has ended, packet: " << ingressPacketInfo.str() << std::endl; + // report the datagram as ESP processing would give it back + setNetworkHeaderProtocol(netHeader, (IpProtocolId)espTrailer->getNextHeader()); + updateNetworkHeaderLength(netHeader, packet); + packet->insertAtFront(netHeader); + emitPacketDropped(packet, LIFETIME_EXPIRED); emit(inProtectedDropSignal, 1L); inDrop++; return INetfilter::IHook::DROP; @@ -1055,6 +1089,7 @@ INetfilter::IHook::Result IPsec::processIngressPacket(Packet *packet) if (!matchesSelectorsOfSa(packet, sadEntry) || (ahSadEntry != nullptr && !matchesSelectorsOfSa(packet, ahSadEntry))) { EV_INFO << "IPsec IN DROP ESP, the packet does not match the selectors of its SA, packet: " << ingressPacketInfo.str() << std::endl; + emitPacketDropped(packet, OTHER_PACKET_DROP); emit(inProtectedDropSignal, 1L); inDrop++; return INetfilter::IHook::DROP; @@ -1086,6 +1121,10 @@ INetfilter::IHook::Result IPsec::processIngressPacket(Packet *packet) if (sadEntry == nullptr) { EV_INFO << "IPsec IN DROP ESP, no matching rule, packet: " << ingressPacketInfo.str() << std::endl; + // report the datagram as ESP processing received it + packet->insertAtFront(espHeader); + packet->insertAtFront(netHeader); + emitPacketDropped(packet, OTHER_PACKET_DROP); emit(inProtectedDropSignal, 1L); inDrop++; return INetfilter::IHook::DROP; @@ -1102,12 +1141,14 @@ INetfilter::IHook::Result IPsec::processIngressPacket(Packet *packet) } else if (spdEntry->getAction() == Action::DISCARD) { EV_INFO << "IPsec DROP rule, packet: " << ingressPacketInfo.str() << std::endl; + emitPacketDropped(packet, OTHER_PACKET_DROP); emit(inUnprotectedDropSignal, 1L); inDrop++; return INetfilter::IHook::DROP; } else { EV_INFO << "IPsec IN DROP due to wrong rule, packet: " << ingressPacketInfo.str() << std::endl; + emitPacketDropped(packet, OTHER_PACKET_DROP); emit(inUnprotectedDropSignal, 1L); inDrop++; return INetfilter::IHook::DROP; @@ -1116,6 +1157,7 @@ INetfilter::IHook::Result IPsec::processIngressPacket(Packet *packet) } EV_INFO << "IPsec IN DROP, no matching rule, packet: " << ingressPacketInfo.str() << std::endl; + emitPacketDropped(packet, OTHER_PACKET_DROP); emit(inUnprotectedDropSignal, 1L); inDrop++; return INetfilter::IHook::DROP; diff --git a/src/inet/networklayer/ipsec/IPsec.h b/src/inet/networklayer/ipsec/IPsec.h index 36846c77e77..9f5f56921dd 100644 --- a/src/inet/networklayer/ipsec/IPsec.h +++ b/src/inet/networklayer/ipsec/IPsec.h @@ -23,6 +23,7 @@ #include "inet/common/INETDefs.h" #include "inet/common/SimpleModule.h" +#include "inet/common/Simsignals.h" #include "inet/networklayer/contract/INetfilter.h" #include "inet/common/ModuleAccess.h" #include "inet/common/Protocol.h" @@ -112,6 +113,7 @@ class INET_API IPsec : public SimpleModule, NetfilterBase::HookBase virtual INetfilter::IHook::Result protectDatagram(Packet *ipv4datagram, const PacketInfo& packetInfo, SecurityPolicy *spdEntry); virtual bool matchesSelectorsOfSa(Packet *packet, SecurityAssociation *sadEntry); virtual void sendDummyPacket(SecurityAssociation *sadEntry); + virtual void emitPacketDropped(Packet *packet, PacketDropReason reason); virtual int getIntegrityCheckValueBitLength(EncryptionAlg alg); virtual int getInitializationVectorBitLength(EncryptionAlg alg); diff --git a/src/inet/networklayer/ipsec/IPsec.ned b/src/inet/networklayer/ipsec/IPsec.ned index 2c1cba693dc..ccb63addfc6 100644 --- a/src/inet/networklayer/ipsec/IPsec.ned +++ b/src/inet/networklayer/ipsec/IPsec.ned @@ -232,6 +232,7 @@ simple IPsec extends SimpleModule @signal[outDrop](type=long); @signal[inProcessDelay](type=double); @signal[outProcessDelay](type=double); + @signal[packetDropped](type=inet::Packet); @statistic[inProtectedAccept](title="Incoming Protected Packet Accepted"; record=count,vector); @statistic[inProtectedDrop](title="Incoming Protected Packet Dropped"; record=count,vector); @@ -242,6 +243,8 @@ simple IPsec extends SimpleModule @statistic[outDrop](title="Outgoing Packet Dropped"; record=count,vector); @statistic[inProcessDelay](title="Incoming Processing Delay"; record=vector,stats); @statistic[outProcessDelay](title="Outgoing Processing Delay"; record=vector,stats); + @statistic[packetDropLifetimeExpired](title="packet drop: lifetime expired"; source=packetDropReasonIsLifetimeExpired(packetDropped); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); + @statistic[packetDropUndefined](title="packet drop: undefined"; source=packetDropReasonIsUndefined(packetDropped); record=count,sum(packetBytes),vector(packetBytes); interpolationmode=none); gates: input ipIn; // unused: nothing is dispatched up to IPsec diff --git a/tests/module/IPsec_packet_dropped.test b/tests/module/IPsec_packet_dropped.test new file mode 100644 index 00000000000..a8595a25300 --- /dev/null +++ b/tests/module/IPsec_packet_dropped.test @@ -0,0 +1,79 @@ +%description: +Tests that IPsec emits packetDropped when it discards a datagram, so that the +packetDropUndefined statistic of the IPsec module counts the discards that its +outDrop, inUnprotectedDrop and inProtectedDrop statistics count. + +client sends three UDP datagrams to each of three ports of server. +- Port 2000: the client's Security Policy Database (SPD) has an OUT entry with + the action DISCARD, so the client's IPsec discards the datagrams. +- Port 2001: the client's SPD has an OUT entry with the action BYPASS, and the + server's SPD has no entry for them, so the server's IPsec discards them on + arrival. +- Port 2002: the client's SPD protects the datagrams with the Encapsulating + Security Payload (ESP) under Security Parameters Index (SPI) 7, and the + server has no Security Association (SA) and no SPD entry for them, so the server's IPsec + discards them after removing their network and ESP headers. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned +import inet.networklayer.configurator.ipv4.Ipv4NetworkConfigurator; +import inet.node.inet.StandardHost; +import ned.DatarateChannel; + +network IpsecDropNetwork +{ + submodules: + configurator: Ipv4NetworkConfigurator; + client: StandardHost; + server: StandardHost; + connections: + client.pppg++ <--> { datarate = 10Mbps; delay = 0.1us; } <--> server.pppg++; +} +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +ned-path = ../../../../src +network = IpsecDropNetwork +sim-time-limit = 5s +record-vector-results = false +cmdenv-express-mode = true + +**.hasIpsec = true +**.client.**.spdConfig = xml("\ + UDP2000OUTDISCARD\ + UDP2001OUTBYPASS\ + UDP2002OUTPROTECTESPCONFIDENTIALITYAES_CBC_1287\ + ") +**.server.**.spdConfig = xml("") + +**.client.numApps = 3 +**.client.app[*].typename = "UdpBasicApp" +**.client.app[*].destAddresses = "server" +**.client.app[0].destPort = 2000 +**.client.app[1].destPort = 2001 +**.client.app[2].destPort = 2002 +**.client.app[*].messageLength = 100B +**.client.app[*].startTime = 1s +**.client.app[*].sendInterval = 1s +**.client.app[*].stopTime = 3.5s +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: gawk '$1 == "scalar" && $2 ~ /\.ipv4\.ipsec$/ && ($3 == "outDrop:count" || $3 == "inUnprotectedDrop:count" || $3 == "inProtectedDrop:count" || $3 == "packetDropUndefined:count") { print $2, $3, $4 }' results/*.sca > drops.out +%contains: drops.out +IpsecDropNetwork.client.ipv4.ipsec outDrop:count 3 +%#-------------------------------------------------------------------------------------------------------------- +%contains: drops.out +IpsecDropNetwork.client.ipv4.ipsec packetDropUndefined:count 3 +%#-------------------------------------------------------------------------------------------------------------- +%contains: drops.out +IpsecDropNetwork.server.ipv4.ipsec inUnprotectedDrop:count 3 +%#-------------------------------------------------------------------------------------------------------------- +%contains: drops.out +IpsecDropNetwork.server.ipv4.ipsec inProtectedDrop:count 3 +%#-------------------------------------------------------------------------------------------------------------- +%contains: drops.out +IpsecDropNetwork.server.ipv4.ipsec packetDropUndefined:count 6 +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: ( +%#--------------------------------------------------------------------------------------------------------------