From b19f9d98fd10fbdac0413feedee4532fd062dfe0 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 26 Aug 2026 15:08:55 +0200 Subject: [PATCH 1/2] ipv6: run Duplicate Address Detection on a care-of address formed at a handover Ipv6NeighbourDiscovery::processRaPrefixInfoForAddrAutoConf() has two paths for an autoconfigured global address. When the interface holds only a link-local address it assigns the new address as tentative and performs Duplicate Address Detection (DAD) on it, as RFC 4862 Section 5.4 requires. When it holds more -- which is exactly what a Mobile IPv6 handover looks like, the mobile node having a link-local address plus its home address -- it marks the existing addresses tentative, runs DAD on the link-local address alone, and defers the new address to dadGlobalList. makeTentativeAddressPermanent() then assigned that address with tentative=false and cleared the tentative flag on every address of the interface, so the "start DAD for a tentative global address" loop at the end of the same function found nothing left to do. The care-of address was therefore put into service without ever being probed, and the Binding Update registered it with the home agent. RFC 4862 Section 5.4 names this exact shortcut and rules it out: Each individual unicast address SHOULD be tested for uniqueness. Note that there are implementations deployed that only perform Duplicate Address Detection for the link-local address and skip the test for the global address that uses the same interface identifier as that of the link-local address. [...] this kind of "optimization" is NOT RECOMMENDED, and new implementations MUST NOT do that optimization. Assign the address formed from the new prefix as tentative, and clear the tentative flag on the addresses that were already on the interface before it is added, so the existing loop starts DAD for it. Performing the DAD is not enough on its own: the registration used to be started as soon as the link-local DAD completed, so the Binding Update still left before the care-of address was verified (1.67 s early in examples/ipv6/mipv6roaming). Mobile IPv6 sets the source address of that datagram explicitly, and the RFC 4862 guard in Ipv6::fragmentPostRouting() only covers datagrams whose source address Ipv6 chooses itself, so nothing held it back. Start the MIPv6 protocol when the care-of address passes its own DAD instead, keeping the dadGlobalList entry until then. dadHasFailed() already drops that entry, so a care-of address that is already in use on the visited link is now never registered. Fingerprints re-recorded for the three affected examples (ipv6/mipv6 Handover and RouteOptimizationTwoCNs, ipv6/mipv6roaming Roaming): each handover now carries the extra DAD Neighbor Solicitations for the care-of address (~tNl, ~tND, ~tNlb) and delays the Binding Update by one DAD interval (tplx). Verified identical across two independent runs; graphical (tyf) fingerprints left as-is. The ~tNlb value of ipv6/mipv6roaming in mipv6-refactoring.csv was already stale on unmodified master (expected c8dc-27c2, actual 7ed8-bee3), so this re-recording also absorbs that. ipv6/pmipv6 is unaffected. --- .../icmpv6/Ipv6NeighbourDiscovery.cc | 50 +++++++++++-------- tests/fingerprint/examples.csv | 6 +-- tests/fingerprint/mipv6-refactoring.csv | 6 +-- 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc index 9785d3f5522..6e7b55dca8a 100644 --- a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc +++ b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc @@ -897,7 +897,18 @@ void Ipv6NeighbourDiscovery::makeTentativeAddressPermanent(const Ipv6Address& te ie->getProtocolDataForUpdate()->setDadInProgress(false); if (!tentativeAddr.isLinkLocal()) { - // DAD completed for a global address -- nothing else to do + // DAD completed for a global address. When it is the care-of address formed at a + // handover, the mobile node may now register it with its home agent: until this + // point its uniqueness on the visited link was unverified (RFC 4862 Section 5.4). + auto git = dadGlobalList.find(ie->getInterfaceId()); + if (git != dadGlobalList.end() && git->second.addr == tentativeAddr) { + [[maybe_unused]] bool homeNetwork = git->second.hFlag; + dadGlobalList.erase(git); +#ifdef INET_WITH_MIPV6 + if (rt6->isMobileNode() && !homeNetwork) // if we are not in the home network, send BUs + mipv6->initiateMipv6Protocol(ie, tentativeAddr); +#endif + } return; } @@ -908,7 +919,21 @@ void Ipv6NeighbourDiscovery::makeTentativeAddressPermanent(const Ipv6Address& te if (it != dadGlobalList.end()) { DadGlobalEntry& entry = it->second; - ie->getProtocolDataForUpdate()->assignAddress(entry.addr, false, simTime() + entry.validLifetime, + // Clear the tentative flag on the addresses that were already on the interface when + // the Router Advertisement arrived: the link-local address whose DAD just completed, + // and the home address. This has to happen before the address formed from the new + // prefix is added below, because that address has not been verified yet. + for (int i = 0; i < ie->getProtocolData()->getNumAddresses(); i++) { + Ipv6Address addr = ie->getProtocolData()->getAddress(i); + ie->getProtocolDataForUpdate()->permanentlyAssign(addr); + } + + // RFC 4862 Section 5.4 requires DAD on every unicast address, so the care-of address + // formed from the new prefix is assigned as tentative; the loop at the end of this + // function starts DAD for it. The MIPv6 registration is not started here but when + // that DAD completes (see above), so no Binding Update can advertise an address + // whose uniqueness on the visited link is still unverified. + ie->getProtocolDataForUpdate()->assignAddress(entry.addr, true, simTime() + entry.validLifetime, simTime() + entry.preferredLifetime, entry.hFlag); // moved from processRAPrefixInfoForAddrAutoConf() @@ -916,24 +941,9 @@ void Ipv6NeighbourDiscovery::makeTentativeAddressPermanent(const Ipv6Address& te if (!entry.CoA.isUnspecified()) ie->getProtocolDataForUpdate()->removeAddress(entry.CoA); - // set addresses on this interface to tentative=false - for (int i = 0; i < ie->getProtocolData()->getNumAddresses(); i++) { - // TODO improve this code so that only addresses are permanently assigned - // which are formed based on the new prefix from the RA - Ipv6Address addr = ie->getProtocolData()->getAddress(i); - ie->getProtocolDataForUpdate()->permanentlyAssign(addr); - } - -#ifdef INET_WITH_MIPV6 - // if we have MIPv6 protocols on this node we will eventually have to - // call some appropriate methods - if (rt6->isMobileNode()) { - if (entry.hFlag == false) // if we are not in the home network, send BUs - mipv6->initiateMipv6Protocol(ie, tentativeAddr); - } -#endif - - dadGlobalList.erase(it->first); + // The MIPv6 protocol is initiated once the care-of address assigned above has + // passed DAD; the dadGlobalList entry is kept until then. dadHasFailed() drops + // it, so a care-of address that is already in use is never registered. } // Assign global scope addresses to routers. The Ipv6FlatNetworkConfigurator assigns diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 283915ab865..06da0dbb820 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -380,9 +380,9 @@ /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 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/mipv6/, -f omnetpp.ini -c Handover -r 0, 70s, 6204-6190/tplx;68f4-68c6/~tNl;76af-99d1/~tND;44ef-1a45/tyf, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, 9faf-b8a8/tplx;f076-4454/~tNl;582f-fb5e/~tND;ed3e-17fa/tyf, PASS, wireless EthernetMac +/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 4ab1-dea8/tplx;9d55-36d4/~tNl;1f90-d7bf/~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 /examples/mobility/, -f omnetpp.ini -c AnsimMobility -r 0, 10000s, 72f8-5c0b/tplx;0000-0000/~tNl;0000-0000/~tND;7dd1-18eb/tyf, PASS, diff --git a/tests/fingerprint/mipv6-refactoring.csv b/tests/fingerprint/mipv6-refactoring.csv index e7a648648c0..321cdf1f869 100644 --- a/tests/fingerprint/mipv6-refactoring.csv +++ b/tests/fingerprint/mipv6-refactoring.csv @@ -8,9 +8,9 @@ # 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 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 +/examples/ipv6/mipv6/, -f omnetpp.ini -c Handover -r 0, 70s, dfed-b54a/~tNlb, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, ab29-ae3f/~tNlb, PASS, wireless EthernetMac +/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 2b0b-fa1a/~tNlb, PASS, wireless EthernetMac # IPv6 examples # MLD example — new PASS row; ~tNl locks the MLD Report/Query/Done/MAS-Query packet exchange (traffic+lengths); From 43d6ce88a2b9cad8fc2952be8520656548082b6d Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 26 Aug 2026 15:09:03 +0200 Subject: [PATCH 2/2] ipv6: tests: add a module test for care-of address Duplicate Address Detection Reuses the Mipv6Network scenario of MIPv6_handover.test: the mobile node boots on its home link and then moves to the foreign network, where its interface already holds a link-local address and the home address, so address autoconfiguration takes the multi-address path. The test asserts that Duplicate Address Detection (DAD) is started and completes for the care-of address aaaa:1:66:0:8aa:ff:fe00:8, and that the mobile node only then registers with its home agent. On unmodified master the run logs a DAD completion for the link-local address and for the home address, but none for the care-of address, and sends the Binding Update anyway. Neighbour discovery logging is enabled for this test, as the module test harness turns module logging off by default. --- tests/module/MIPv6_care_of_address_DAD.test | 134 ++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 tests/module/MIPv6_care_of_address_DAD.test diff --git a/tests/module/MIPv6_care_of_address_DAD.test b/tests/module/MIPv6_care_of_address_DAD.test new file mode 100644 index 00000000000..3b2d6f6edd9 --- /dev/null +++ b/tests/module/MIPv6_care_of_address_DAD.test @@ -0,0 +1,134 @@ +%description: +Tests that a care-of address formed at a handover undergoes Duplicate Address +Detection (DAD) before it is used, as required by RFC 4862 Section 5.4, and that +the Mobile Node does not register it with its Home Agent until that DAD passes. + +The Mobile Node boots on its home link (where it has only a link-local address, +so the home address is verified through the single-address path), then moves to +the foreign network. On arrival its interface already holds a link-local address +and the home address, so address autoconfiguration takes the multi-address path. +Before this test was added, that path assigned the care-of address as permanent +and started no DAD for it, and the Binding Update advertised an address whose +uniqueness on the visited link had never been tested. + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +record-vector-results = false +**.neighbourDiscovery.cmdenv-log-level = trace +**.mipv6.cmdenv-log-level = trace +**.app[*].cmdenv-log-level = trace +**.cmdenv-log-level = off +cmdenv-event-banners = false +ned-path = .;../../../../src;../../lib +network = inet.test.moduletest.lib.Mipv6Network +sim-time-limit = 60s +cmdenv-express-mode = false +cmdenv-log-prefix = "%C: " +num-rngs = 3 +seed-set = 1 +**.mobility.rng-0 = 2 + +# number of MNs and CNs +*.total_mn = 1 +*.total_cn = 1 + +**.neighbourDiscovery.minIntervalBetweenRAs = 0.03s +**.neighbourDiscovery.maxIntervalBetweenRAs = 0.07s +**.neighbourDiscovery.detectL2Movement = false # legacy: detect movement from periodic RAs only + +# channel physical parameters +*.radioMedium.mediumLimitCache.maxTransmissionPower = 2.0mW +*.radioMedium.mediumLimitCache.minReceptionPower = -82dBm +*.radioMedium.mediumLimitCache.minInterferencePower = -82dBm + +# access point +**.wlan*.mgmt.numAuthSteps = 4 + +# ALL APs common parameters +**.AP*.wlan*.mgmt.beaconInterval = 0.1s + +# Access Point parameters +**.AP_Home.wlan*.mgmt.ssid = "HOME" +**.AP_Home.wlan*.address = "10:AA:00:00:00:01" +**.AP_Home.eth[0].address = "10:AE:00:00:00:02" +**.AP_Home.eth[0].duplexMode = true + +**.AP_1.wlan*.mgmt.ssid = "AP1" +**.AP_1.wlan*.address = "10:AA:00:00:A1:01" +**.AP_1.eth[0].address = "10:AE:00:00:A1:02" +**.AP_1.eth[0].duplexMode = true + +# mobility +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxZ = 0m + +**.MN[0].mobility.typename = "RectangleMobility" +**.MN[0].mobility.constraintAreaMinX = 180m +**.MN[0].mobility.constraintAreaMinY = 170m +**.MN[0].mobility.constraintAreaMaxX = 630m +**.MN[0].mobility.constraintAreaMaxY = 180m +**.MN[0].mobility.startPos = 0 +**.MN[0].mobility.speed = 10mps +**.MN*.mobility.updateInterval = 0.1s + +# No apps needed - we just test the handover signaling +**.MN*.numApps = 0 +**.CN*.numApps = 0 + +# ip settings +**.forwarding = false + +# Ethernet NIC configuration +**.eth[*].queue.typename = "EthernetQosQueue" +**.eth[*].queue.dataQueue.typename = "DropTailQueue" +**.eth[*].queue.dataQueue.packetCapacity = 10 +**.eth*.duplexMode = true + +# analog model +**.analogModel.ignorePartialInterference = true + +# wireless channels +**.AP_Home.wlan*.radio.channelNumber = 1 +**.AP_1.wlan*.radio.channelNumber = 2 +**.MN*.wlan*.radio.channelNumber = 0 + +# wireless configuration +**.wlan*.agent.activeScan = true +**.wlan*.agent.defaultSsid = "" +**.wlan*.agent.channelsToScan = "1 2" +**.wlan*.agent.probeDelay = 0.1s +**.wlan*.agent.minChannelTime = 0.15s +**.wlan*.agent.maxChannelTime = 0.3s +**.wlan*.agent.authenticationTimeout = 5s +**.wlan*.agent.associationTimeout = 5s + +# nic settings +**.wlan*.bitrate = 2Mbps +**.wlan*.mac.dcf.channelAccess.cwMin = 7 +**.radio.transmitter.power = 2.0mW + +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMaxX = 850m +**.constraintAreaMaxY = 850m + +%#-------------------------------------------------------------------------------------------------------------- +%subst: /omnetpp::// +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +Mipv6Network.MN[0].ipv6.neighbourDiscovery: Starting DAD for tentative global address aaaa:1:66:0:8aa:ff:fe00:8 +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +Mipv6Network.MN[0].ipv6.neighbourDiscovery: DAD completed for address aaaa:1:66:0:8aa:ff:fe00:8 on wlan0, address is unique +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +Mipv6Network.MN[0].ipv6.mipv6: Initiating Mobile Ipv6 protocol... +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +Mipv6Network.Home_Agent.ipv6.mipv6: BU validation passed +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: ( +%#--------------------------------------------------------------------------------------------------------------