From e28cfdd90994445f5d718530f08b742200217307 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Tue, 25 Aug 2026 11:53:14 +0200 Subject: [PATCH] ipv6: fix: answer every Router Solicitation, not only the first one An advertising interface answered the first Router Solicitation it received and then silently discarded every later one until its next periodic Router Advertisement was due. With the RFC 4861 default MinRtrAdvInterval and MaxRtrAdvInterval (200s and 600s, which are also INET's NED defaults) that window is far longer than a host's Router Discovery budget of MAX_RTR_SOLICITATIONS * RTR_SOLICITATION_INTERVAL, about 9s, so a host that solicited second gave up with "No RA messages were received. Assume no routers are on-link", never autoconfigured a global address, and stayed unreachable for the rest of the run, with no error and a zero exit code. processRsPacket() decides whether to schedule a solicited advertisement by comparing the computed send time against advIfEntry->nextScheduledRATime, which per RFC 4861 Section 6.2.6 must be the time the *next* multicast Router Advertisement is scheduled to be sent: - Upon receipt of a Router Solicitation, compute a random delay within the range 0 through MAX_RA_DELAY_TIME. If the computed value corresponds to a time later than the time the next multicast Router Advertisement is scheduled to be sent, ignore the random delay and send the advertisement at the already-scheduled time. Scheduling a solicited advertisement correctly sets nextScheduledRATime to its send time, because it then is the next multicast advertisement to go out. But sendSolicitedRa() never moved the field on once that advertisement had been sent, so it kept pointing at an advertisement already in the past. Every later solicitation computed a send time in the future, found it "later than" a past timestamp, and fell into the ignore branch -- where nothing then served the solicitation, although Section 6.2.6 also states that "a router sends advertisements in response to valid solicitations received on an advertising interface". Point nextScheduledRATime back at the pending periodic advertisement once the solicited one has been sent. Also log the ignore path, which is now only taken when a future advertisement really will serve the solicitation. The comparison itself was made dimensionally correct only recently, in 632e460559 ("IPv6: three Neighbour Discovery conformance fixes"); before that it compared a duration against an absolute time, which was wrong but almost always true, so the ignore branch was effectively unreachable and the stale field did no harm. Adds tests/module/IPv6_RA_repeated_solicitations.test, which shuts a host down and restarts it so that it has to redo Router Discovery while no periodic advertisement is due. The test fails on unmodified master. Fingerprints re-recorded for the IPv6 examples that have two or more soliciting hosts on one link: ipv6/mipv6 (Handover), ipv6/mld (MldV2Ssm), ospfv3/small, ospfv3/multiple_areas_FINAL (all three configurations) and inet/hierarchical99 (IPv6). Every ingredient of those fingerprints moves for one reason: the router now emits Router Advertisements it used to discard. That adds events (t), in modules that previously saw none at that point (p, N), carrying Router Advertisement payloads that were not in the hashed stream before (l, D, b), and everything downstream on those nodes shifts (x). The graphical (tyf) ingredients were excluded from the run and are left untouched. Verified stable across two independent runs. IPv6 examples with only one soliciting host per router interface are unaffected and were not re-recorded: ipv6/nclients (each client has its own point-to-point link) and inet/udpclientserver IPv6 configurations all still pass. ipv6/mipv6roaming (Roaming, ~tNlb) fails both before and after this change with the same actual value 7ed8-bee3, so it is pre-existing and deliberately not re-recorded here. --- .../icmpv6/Ipv6NeighbourDiscovery.cc | 16 ++- tests/fingerprint/examples.csv | 12 +- tests/fingerprint/mipv6-refactoring.csv | 6 +- .../IPv6_RA_repeated_solicitations.test | 107 ++++++++++++++++++ 4 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 tests/module/IPv6_RA_repeated_solicitations.test diff --git a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc index 9785d3f5522..14f7767e959 100644 --- a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc +++ b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc @@ -1211,8 +1211,11 @@ void Ipv6NeighbourDiscovery::processRsPacket(Packet *packet, const Ipv6RouterSol scheduleAt(scheduledTime, msg); advIfEntry->nextScheduledRATime = scheduledTime; } - else + else { + EV_DETAIL << "A multicast RA is already scheduled at " << advIfEntry->nextScheduledRATime + << "; it will serve this solicitation\n"; delete msg; + } } else { EV_INFO << "This interface is a host, discarding RA message\n"; @@ -1727,6 +1730,17 @@ void Ipv6NeighbourDiscovery::sendSolicitedRa(cMessage *msg) Ipv6Address destAddr = Ipv6Address("FF02::1"); EV_DETAIL << "Testing condition!\n"; createAndSendRaPacket(destAddr, ie); + + /*When this advertisement was scheduled it became the next multicast Router + Advertisement to be sent, so nextScheduledRATime was set to its send time + (RFC 4861 Section 6.2.6). It has now been sent, and the next multicast + advertisement is the pending periodic one again. Point nextScheduledRATime back + at it; left in the past, it would make every later solicitation compare against + an advertisement that has already gone out, and be ignored for as long as no + periodic advertisement is sent.*/ + if (AdvIfEntry *advIfEntry = fetchAdvIfEntry(ie)) + advIfEntry->nextScheduledRATime = advIfEntry->raTimeoutMsg->getArrivalTime(); + delete msg; } diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 283915ab865..17640a2083d 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, 856e-a5fa/tplx;079d-3888/~tNl;1e2a-3d3b/~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 @@ -380,7 +380,7 @@ /examples/manetrouting/multiradio/, -f omnetpp.ini -c MultiRadio -r 0, 20s, ec17-5cc2/tplx;55f5-0894/~tNl, PASS, wireless adhoc Ipv4 /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 Handover -r 0, 70s, dfc0-8b68/tplx;e31e-d2fb/~tNl;3026-672f/~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/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 @@ -453,10 +453,10 @@ /examples/ospfv2/simpletest/, -f omnetpp.ini -c ShutdownAndRestart -r 0, 500s, fbe5-93ad/tplx;bc6e-6aa5/~tNl;1700-f363/~tND;7e87-b79f/tyf, PASS, EthernetMac Ipv4 /examples/ospfv2/simpletest/, -f omnetpp.ini -c CrashAndReboot -r 0, 500s, 73cc-e6f7/tplx;bc6e-6aa5/~tNl;1700-f363/~tND;8ea5-1a49/tyf, PASS, EthernetMac Ipv4 -/examples/ospfv3/multiple_areas_FINAL/, -f omnetpp.ini -c General -r 0, 500s, 5a6c-7b26/tplx;1cb8-4b4c/~tNl;c96e-af34/tyf;2d23-dc61/~tND, PASS, EthernetMac -/examples/ospfv3/multiple_areas_FINAL/, -f omnetpp.ini -c LinkDown -r 0, 500s, 0a8e-1d2c/tplx;026a-465c/~tNl;058d-fbf3/tyf;5859-a357/~tND, PASS, EthernetMac -/examples/ospfv3/multiple_areas_FINAL/, -f omnetpp.ini -c LinkDownLinkUp -r 0, 500s, ca28-5d2b/tplx;e812-c9a5/~tNl;7d65-aa4c/tyf;8767-9c3b/~tND, PASS, EthernetMac -/examples/ospfv3/small/, -f omnetpp.ini -c General -r 0, 200s, a507-2349/tplx;78bf-31e2/~tNl;441a-9244/tyf;4b8b-fb7a/~tND, PASS, EthernetMac +/examples/ospfv3/multiple_areas_FINAL/, -f omnetpp.ini -c General -r 0, 500s, 645d-012f/tplx;98be-431d/~tNl;c96e-af34/tyf;29c8-752d/~tND, PASS, EthernetMac +/examples/ospfv3/multiple_areas_FINAL/, -f omnetpp.ini -c LinkDown -r 0, 500s, 86d0-9f63/tplx;447a-5e7d/~tNl;058d-fbf3/tyf;0b58-99bd/~tND, PASS, EthernetMac +/examples/ospfv3/multiple_areas_FINAL/, -f omnetpp.ini -c LinkDownLinkUp -r 0, 500s, 6abb-be3c/tplx;e043-4da3/~tNl;7d65-aa4c/tyf;838c-3577/~tND, PASS, EthernetMac +/examples/ospfv3/small/, -f omnetpp.ini -c General -r 0, 200s, 13b1-1693/tplx;806f-af9b/~tNl;441a-9244/tyf;d9d2-cb28/~tND, PASS, EthernetMac /examples/ospfv3/v3_2_routers/, -f omnetpp.ini -c General -r 0, 400s, b94b-8c51/tplx;536c-1525/~tNl;993e-1af3/tyf;bb52-b309/~tND, PASS, EthernetMac /examples/ospfv3/v3_2_routers/, -f omnetpp.ini -c LinkDownAndUp -r 0, 400s, 24d9-8a90/tplx;d598-192b/~tNl;c4df-4e3f/tyf;b19b-722d/~tND, PASS, EthernetMac /examples/ospfv3/v3_3_areas/, -f omnetpp.ini -c General -r 0, 3000s, 7cc4-6f54/tplx;18bf-6f31/~tNl;ddbf-753a/tyf;89ed-849f/~tND, PASS, EthernetMac diff --git a/tests/fingerprint/mipv6-refactoring.csv b/tests/fingerprint/mipv6-refactoring.csv index e7a648648c0..80c6d5c93be 100644 --- a/tests/fingerprint/mipv6-refactoring.csv +++ b/tests/fingerprint/mipv6-refactoring.csv @@ -8,7 +8,7 @@ # empty-shim step, which kept these fingerprints bit-for-bit identical. # 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 Handover -r 0, 70s, bf13-d7e0/~tNlb, PASS, wireless EthernetMac /examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, 068b-23aa/~tNlb, PASS, wireless EthernetMac /examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, c8dc-27c2/~tNlb, PASS, wireless EthernetMac @@ -17,13 +17,13 @@ # ~tNlb excluded: EthernetHub (WireJunction) adds a pointer-typed 'originalSender' cPar to the signal which # causes parsimPack() to abort — use ~tNl (no parsim serialization) instead; tyf excluded as unreliable /examples/ipv6/mld/, -f omnetpp.ini -c MldDemo -r 0, 30s, f1b1-b523/~tNl, PASS, EthernetMac MLD -/examples/ipv6/mld/, -f omnetpp.ini -c MldV2Ssm -r 0, 30s, a22a-81c9/~tNl, PASS, EthernetMac MLD +/examples/ipv6/mld/, -f omnetpp.ini -c MldV2Ssm -r 0, 30s, e849-1de6/~tNl, PASS, EthernetMac MLD /examples/ipv6/nclients/, -f omnetpp.ini -c ETH -r 0, 1000s, 50c9-9bb3/~tNlb, PASS, EthernetMac /examples/ipv6/nclients/, -f omnetpp.ini -c PPP -r 0, 1000s, 0e0c-b800/~tNlb, PASS, /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, aa68-afc5/~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_repeated_solicitations.test b/tests/module/IPv6_RA_repeated_solicitations.test new file mode 100644 index 00000000000..aa002b75f84 --- /dev/null +++ b/tests/module/IPv6_RA_repeated_solicitations.test @@ -0,0 +1,107 @@ +%description: +Tests that an IPv6 router answers every valid Router Solicitation, not just the +first one on an advertising interface (RFC 4861 Section 6.2.6). + +hostA and hostB share a link with router R. hostA solicits first and is answered. +hostB is then shut down and restarted at t=20s, so it loses its addresses and has +to perform Router Discovery again, at a time when no periodic Router Advertisement +is due (the RFC 4861 default MinRtrAdvInterval/MaxRtrAdvInterval of 200s/600s are +far longer than hostB's MAX_RTR_SOLICITATIONS budget of about 9s). + +The router must answer hostB's solicitation. If it does not, hostB gives up with +"No RA messages were received", never autoconfigures a global address, and its +traffic to srv is silently lost while the run still exits with status 0. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned +import inet.common.scenario.ScenarioManager; +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 RepeatedSolicitations +{ + types: + channel ethline extends DatarateChannel + { + delay = 0.1us; + datarate = 10Mbps; + } + submodules: + scenarioManager: ScenarioManager; + configurator: Ipv6FlatNetworkConfigurator; + switch: EthernetSwitch; + R: Router6; + hostA: StandardHost6; + hostB: StandardHost6; + srv: StandardHost6; + connections: + hostA.ethg++ <--> ethline <--> switch.ethg++; + hostB.ethg++ <--> ethline <--> switch.ethg++; + R.ethg++ <--> ethline <--> switch.ethg++; + R.ethg++ <--> ethline <--> srv.ethg++; +} +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +record-vector-results = false +ned-path = ../../../../src +network = RepeatedSolicitations +sim-time-limit = 60s +cmdenv-express-mode = false +cmdenv-log-prefix = "%C: " + +**.hasStatus = true +**.scenarioManager.script = xmldoc("scenario.xml") + +# RFC 4861 defaults, and also INET's own NED defaults +**.R.ipv6.neighbourDiscovery.minIntervalBetweenRAs = 200s +**.R.ipv6.neighbourDiscovery.maxIntervalBetweenRAs = 600s + +# udp apps +**.hostB.numApps = 1 +**.hostB.app[0].typename = "UdpBasicApp" +**.hostB.app[0].destAddresses = "srv" +**.hostB.app[0].destPort = 1000 +**.hostB.app[0].messageLength = 64B +**.hostB.app[0].startTime = 40s +**.hostB.app[0].sendInterval = 1s + +**.srv.numApps = 1 +**.srv.app[0].typename = "UdpSink" +**.srv.app[0].localPort = 1000 + +# Ethernet NIC configuration +**.eth[*].queue.typename = "EthernetQosQueue" +**.eth[*].queue.dataQueue.typename = "DropTailQueue" +**.eth[*].queue.dataQueue.packetCapacity = 10 + +%#-------------------------------------------------------------------------------------------------------------- +%file: scenario.xml + + + + + + + + + + +%#-------------------------------------------------------------------------------------------------------------- +%subst: /omnetpp::// +%#-------------------------------------------------------------------------------------------------------------- +%not-contains: stdout +No RA messages were received +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +RepeatedSolicitations.hostB.ipv6.neighbourDiscovery: Assigning new address to: eth0 +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +RepeatedSolicitations.srv.app[0]: received 20 packets +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: (