From 1fb72bec9afbf12701b157415f22c130991ad85f Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 2 Sep 2026 13:14:54 +0200 Subject: [PATCH] ipv6: fix: cancel the Router Advertisement an earlier one supersedes An advertising interface put two multicast Router Advertisements on the link a fraction of a second apart. RFC 4861 Section 6.2.6 requires consecutive advertisements sent to the all-nodes multicast address to be "rate limited to no more than one advertisement every MIN_DELAY_BETWEEN_RAS seconds", and MIN_DELAY_BETWEEN_RAS is 3 seconds. processRsPacket() answers a Router Solicitation by computing a send time and comparing it against advIfEntry->nextScheduledRATime, the time the next multicast advertisement is due. A computed time that is earlier wins and becomes the new nextScheduledRATime -- but the advertisement that the old value referred to stayed in the future event set. Both were then sent. Six hosts booting together on one link produced advertisements 0.264 s apart; the mipv6 RouteOptimizationTwoCNs example produced two 0.079 s apart. AdvIfEntry gains a handle to the solicited advertisement pending on the interface, so the one an earlier advertisement supersedes can be cancelled. The handle is released when the advertisement is sent, and cancelled with the entry when the module is destroyed or stopped. The periodic advertisement is a different message and is untouched. The cancellation is logged, so a test can assert that the case was reached and not merely that no advertisements were too close together. Removing an advertisement removes it from the hashed event stream, which moves the fingerprints of the two recorded IPv6 configurations in which two hosts solicit close enough together to produce a duplicate: inet/hierarchical99 (IPv6) and ipv6/mipv6 (RouteOptimizationTwoCNs). Every moved ingredient moves for that one reason. The advertisement's own send event and the reception events it caused leave the stream, so the hashed simulation times (t) change; they occurred in the router's neighbourDiscovery module and on the receiving nodes, so the module full paths (p) and network node paths (N) change; the RouterAdvertisement and the frame carrying it are gone, so the message bit lengths (l), packet data (D) and message contents (b) change; and the state they left behind shifts the extra data (x). In hierarchical99 four advertisements disappear from the 2.83-3.33 s startup burst and the trajectory rejoins the old one; in RouteOptimizationTwoCNs the first duplicate disappears at t = 7.580406 and the run diverges from there. The values are stable across two runs. The graphical (tyf) ingredients were excluded from the run and are left at their recorded values. Not repaired here: a periodic advertisement is still sent at its own due time without regard for MIN_DELAY_BETWEEN_RAS, so it can follow a solicited one inside the 3 s window. That is a separate defect in sendPeriodicRa(), visible in the same mipv6 example. --- .../icmpv6/Ipv6NeighbourDiscovery.cc | 15 ++++ .../icmpv6/Ipv6NeighbourDiscovery.h | 1 + tests/fingerprint/examples.csv | 4 +- tests/fingerprint/mipv6-refactoring.csv | 4 +- .../module/IPv6_RA_min_delay_between_RAs.test | 85 +++++++++++++++++++ 5 files changed, 105 insertions(+), 4 deletions(-) create mode 100644 tests/module/IPv6_RA_min_delay_between_RAs.test diff --git a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc index 9785d3f5522..f74501f9ac6 100644 --- a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc +++ b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc @@ -68,6 +68,7 @@ Ipv6NeighbourDiscovery::~Ipv6NeighbourDiscovery() for (auto *entry : advIfList) { cancelAndDelete(entry->raTimeoutMsg); + cancelAndDelete(entry->pendingSolicitedRaMsg); delete entry; } @@ -1208,7 +1209,18 @@ void Ipv6NeighbourDiscovery::processRsPacket(Packet *packet, const Ipv6RouterSol // computed time, ignore the random delay and let that advertisement serve // this solicitation (RFC 4861 Section 6.2.6). if (scheduledTime < advIfEntry->nextScheduledRATime) { + /*This advertisement becomes the next multicast RA on the interface, so it + supersedes the solicited one that was pending until now: that one was + scheduled to serve the earlier solicitations, and this one, being earlier, + serves them too. Sending both would put two multicast RAs on the link less + than MIN_DELAY_BETWEEN_RAS apart, which RFC 4861 Section 6.2.6 forbids.*/ + if (advIfEntry->pendingSolicitedRaMsg) { + EV_DETAIL << "Cancelling the solicited RA scheduled at " << advIfEntry->nextScheduledRATime + << "; this earlier one supersedes it\n"; + cancelAndDelete(advIfEntry->pendingSolicitedRaMsg); + } scheduleAt(scheduledTime, msg); + advIfEntry->pendingSolicitedRaMsg = msg; advIfEntry->nextScheduledRATime = scheduledTime; } else @@ -1727,6 +1739,8 @@ void Ipv6NeighbourDiscovery::sendSolicitedRa(cMessage *msg) Ipv6Address destAddr = Ipv6Address("FF02::1"); EV_DETAIL << "Testing condition!\n"; createAndSendRaPacket(destAddr, ie); + if (AdvIfEntry *advIfEntry = fetchAdvIfEntry(ie)) + advIfEntry->pendingSolicitedRaMsg = nullptr; delete msg; } @@ -2693,6 +2707,7 @@ void Ipv6NeighbourDiscovery::stop() // cancel and delete all advertising interface entries for (auto *entry : advIfList) { cancelAndDelete(entry->raTimeoutMsg); + cancelAndDelete(entry->pendingSolicitedRaMsg); delete entry; } advIfList.clear(); diff --git a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.h b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.h index f122b6e9399..75ea45da082 100644 --- a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.h +++ b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.h @@ -127,6 +127,7 @@ class INET_API Ipv6NeighbourDiscovery : public OperationalBase, protected cListe simtime_t nextScheduledRATime; // stores time when next RA will be sent. simtime_t lastMulticastRATime = -1; // time the last multicast RA was sent (-1 = none yet); used to rate-limit RAs to MIN_DELAY_BETWEEN_RAS (RFC 4861 Section 6.2.6) cMessage *raTimeoutMsg; // the message to cancel when resetting RA timer + cMessage *pendingSolicitedRaMsg = nullptr; // solicited RA scheduled on this interface but not yet sent (nullptr = none); cancelled when a solicitation schedules an earlier one }; typedef std::vector AdvIfList; diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 283915ab865..1e06681d865 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -165,7 +165,7 @@ # /examples/inet/hierarchical99/, -f .qtenv.ini -c General -r 0 /examples/inet/hierarchical99/, -f networklayer.ini -c IPv4 -r 0, 10000s, a94d-edae/tplx;1f32-e325/~tNl;b138-ee8b/~tND;66c9-cd73/tyf, PASS, EthernetMac Ipv4 -/examples/inet/hierarchical99/, -f networklayer.ini -c IPv6 -r 0, 10000s, 0001-0108/tplx;3960-479e/~tNl;add1-e848/~tND;2c03-abef/tyf, PASS, EthernetMac +/examples/inet/hierarchical99/, -f networklayer.ini -c IPv6 -r 0, 10000s, 6164-8ced/tplx;6b03-6322/~tNl;e7c1-b997/~tND;2c03-abef/tyf, PASS, EthernetMac /examples/inet/hierarchical99/, -f networklayer.ini -c Generic -r 0, 10000s, 6ae4-6e75/tplx;2209-80ac/~tNl;93bf-6657/tyf, PASS, EthernetMac /examples/inet/hierarchical99/, -f networklayer.ini -c Flooding -r 0, 10000s, 7ba7-5b91/tplx;b0f0-411e/~tNl;2b94-8906/tyf, PASS, EthernetMac /examples/inet/hierarchical99/, -f networklayer.ini -c ProbabilisticBroadcast -r 0, 10000s, 5a54-9779/tplx;1165-a70b/~tNl;760b-abfc/tyf, PASS, EthernetMac @@ -381,7 +381,7 @@ /examples/manetrouting/multiradio/, -f omnetpp.ini -c SingleRadio -r 0, 20s, 85a0-51b8/tplx;c07a-44e1/~tNl;3aa0-49ed/tyf, PASS, wireless adhoc Ipv4 /examples/ipv6/mipv6/, -f omnetpp.ini -c Handover -r 0, 70s, 19b8-20a4/tplx;b378-071d/~tNl;d358-e3bb/~tND;44ef-1a45/tyf, PASS, wireless EthernetMac -/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, 3f0c-078d/tplx;04e1-1f03/~tNl;4199-2b15/~tND;ed3e-17fa/tyf, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, 5115-7e10/tplx;a522-3d29/~tNl;5319-39f4/~tND;ed3e-17fa/tyf, PASS, wireless EthernetMac /examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 0798-40c2/tplx;a682-8d0e/~tNl;cdb7-1b34/~tND;afae-2b3c/tyf, PASS, wireless EthernetMac /examples/ipv6/pmipv6/, -f omnetpp.ini -c General -r 0, 60s, f614-da0d/tplx;8b55-7191/~tNl;b490-dc09/~tND;0277-d784/tyf, PASS, wireless EthernetMac diff --git a/tests/fingerprint/mipv6-refactoring.csv b/tests/fingerprint/mipv6-refactoring.csv index 4c2175bce5a..334523e2a0d 100644 --- a/tests/fingerprint/mipv6-refactoring.csv +++ b/tests/fingerprint/mipv6-refactoring.csv @@ -9,7 +9,7 @@ # MIPv6 example — the primary test /examples/ipv6/mipv6/, -f omnetpp.ini -c Handover -r 0, 70s, aa29-a8c5/~tNlb, PASS, wireless EthernetMac -/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, 068b-23aa/~tNlb, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, b108-5896/~tNlb, PASS, wireless EthernetMac /examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 7ed8-bee3/~tNlb, PASS, wireless EthernetMac # IPv6 examples @@ -23,7 +23,7 @@ /examples/ipv6/nclients/, -f omnetpp.ini -c PPP_SCTP -r 0, 100s, 06b9-ff17/~tNlb, PASS, # IPv6 networking examples -/examples/inet/hierarchical99/, -f networklayer.ini -c IPv6 -r 0, 10000s, f206-a0e2/~tNlb, PASS, EthernetMac +/examples/inet/hierarchical99/, -f networklayer.ini -c IPv6 -r 0, 10000s, 1cbb-1534/~tNlb, PASS, EthernetMac /examples/inet/udpclientserver/, -f omnetpp.ini -c udp_OK_ipv6 -r 0, 10s, 8feb-3936/~tNlb, PASS, /examples/inet/udpclientserver/, -f omnetpp.ini -c udp_Port_Unav_ipv6 -r 0, 10s, 9661-c1f5/~tNlb, PASS, /examples/inet/udpclientserver/, -f omnetpp.ini -c udp_Host_Unav_ipv6 -r 0, 10s, 6360-9ed1/~tNlb, PASS, diff --git a/tests/module/IPv6_RA_min_delay_between_RAs.test b/tests/module/IPv6_RA_min_delay_between_RAs.test new file mode 100644 index 00000000000..089705ffde6 --- /dev/null +++ b/tests/module/IPv6_RA_min_delay_between_RAs.test @@ -0,0 +1,85 @@ +%description: +Tests that consecutive multicast Router Advertisements (RA) sent by an IPv6 router to +the all-nodes address are never closer than MIN_DELAY_BETWEEN_RAS (3 s), even when +several Router Solicitations (RS) arrive while a solicited advertisement is already +pending (RFC 4861 Section 6.2.6). + +Six hosts boot at the same time on one link, so their solicitations reach router R +within a fraction of a second of each other, while the advertisement scheduled for the +first of them has not been sent yet. Each solicitation computes its own send time from +a random delay in [0, MAX_RA_DELAY_TIME], so a later solicitation can land earlier than +the pending advertisement. When it does, the router must let that one advertisement +serve them all, not send both. + +The scenario depends on the random delays the run draws, so the test also asserts that +the superseding case really occurred. Without that, a change elsewhere that shifts the +random number stream could leave the run with no overlapping solicitations at all, and +the rate-limit assertion would then pass without exercising anything. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned +import inet.networklayer.configurator.ipv6.Ipv6FlatNetworkConfigurator; +import inet.node.ethernet.EthernetSwitch; +import inet.node.ipv6.Router6; +import inet.node.ipv6.StandardHost6; +import ned.DatarateChannel; + +network RaRateLimit +{ + types: + channel ethline extends DatarateChannel + { + delay = 0.1us; + datarate = 10Mbps; + } + submodules: + configurator: Ipv6FlatNetworkConfigurator; + switch: EthernetSwitch; + R: Router6; + host[6]: StandardHost6; + connections: + R.ethg++ <--> ethline <--> switch.ethg++; + for i=0..5 { + host[i].ethg++ <--> ethline <--> switch.ethg++; + } +} +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +record-vector-results = false +ned-path = ../../../../src +network = RaRateLimit +sim-time-limit = 30s +cmdenv-express-mode = false +**.cmdenv-log-level = detail +cmdenv-log-prefix = "%t %C: " + +# RFC 4861 defaults, and also INET's own NED defaults: no periodic advertisement is due +# within the run, so every advertisement seen here is a solicited one +**.R.ipv6.neighbourDiscovery.minIntervalBetweenRAs = 200s +**.R.ipv6.neighbourDiscovery.maxIntervalBetweenRAs = 600s + +# all hosts boot together, as after a power failure +**.host[*].ipv6.neighbourDiscovery.hostBootupTime = 0.4s + +# Ethernet NIC configuration +**.eth[*].queue.typename = "EthernetQosQueue" +**.eth[*].queue.dataQueue.typename = "DropTailQueue" +**.eth[*].queue.dataQueue.packetCapacity = 10 + +%#-------------------------------------------------------------------------------------------------------------- +%subst: /omnetpp::// +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: awk '/R\.ipv6\.neighbourDiscovery: Create and send RA invoked/ { if (prev != "" && $1 - prev < 3) printf "VIOLATION: two multicast RAs %.6f s apart, at %s and %s\n", $1 - prev, prev, $1; prev = $1 }' test.out > ra_gaps.out +%not-contains: ra_gaps.out +VIOLATION +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +RaRateLimit.host[5].ipv6.neighbourDiscovery: Assigning new address to: eth0 +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +R.ipv6.neighbourDiscovery: Cancelling the solicited RA scheduled at +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: (