From 0628fe8579dbfb91a6e219fca2253221e3af7ef1 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 2 Sep 2026 12:18:13 +0200 Subject: [PATCH] ipv6: fix: source a Router Solicitation from the link-local address createAndSendRsPacket() chose the source address with getPreferredAddress(), which is the default source address for off-link destinations. Ipv6InterfaceData::choosePreferredAddress() deliberately returns a tentative global address in preference to a permanent link-local one, because a link-local source is not routable off-link. A Router Solicitation goes to ff02::2, the link-local all-routers multicast address, and never needs a routable source, so that preference is the wrong one for it. The link-local fallback was reached only when there was no preferred address at all, never when the preferred address was tentative. While Duplicate Address Detection (DAD) ran for an autoconfigured global address, the function therefore gave up and used the unspecified address -- and correctly omitted the Source Link-Layer Address option for it -- although a permanent link-local address was present on the same interface. The solicitation did not leave with an unspecified source, though. Ipv6::fragmentPostRouting() fills in getPreferredAddress() for a datagram from the higher layer whose source address is unspecified, unless the destination is a solicited-node multicast address. That carve-out covers a DAD Neighbour Solicitation but not a Router Solicitation, so the solicitation was held on pendingDadQueue and released when DAD completed, carrying the global address and no Source Link-Layer Address option. A router that reads such a solicitation extracts an all-zero link-layer address. RFC 4861 Section 6.3.7: "The Source Link-Layer Address option SHOULD be set to the host's link-layer address, if the IP source address is not the unspecified address." Section 4.1 makes the omission mandatory only when the source is the unspecified address. Falling back to the link-local address keeps the source address and the option decision in one place, and sends the solicitation at once instead of waiting for a DAD it does not depend on. The router discovery timer is armed in makeTentativeAddressPermanent() only after link-local DAD has completed, so a usable link-local address is always present when a solicitation is built. In showcases/general/ipv6autoconfiguration the solicitation of host[0] used to be built at 2.380967 s and reach the router at 3.157715 s with an 8-byte ICMPv6 payload; it now reaches the router at 2.380980 s with a 16-byte payload, and the router reads 0A-AA-00-00-00-09 instead of 00-00-00-00-00-00. Ipv6RouterSolicitation_SourceLinkLayerAddressOption.test pins the behaviour: two hosts and a router on a switched LAN, addresses from Stateless Address Autoconfiguration, and the router advertising often enough that both hosts hold a tentative global address when their solicitations are due. Three fingerprint configurations move, and only three: the MIPv6 handover and roaming examples, where a mobile node autoconfigures an address and solicits a router while Duplicate Address Detection is still running for it. Their tplx, ~tNl and ~tND values in examples.csv and their ~tNlb values in mipv6-refactoring.csv are re-recorded here. Per ingredient: l (message bit length) and b / D (message contents, packet data) move because the solicitation now carries the 8-byte Source Link-Layer Address option, so its ICMPv6 payload grows from 8 to 16 bytes and its source address is the link-local one. t (simulation time) moves because the solicitation is no longer held until Duplicate Address Detection completes, and everything that follows from it shifts with it. p and N (module and network node paths) move because the earlier solicitation reorders the events around it. x (extra data) follows the trajectory. The tyf values of those rows are deliberately not re-recorded. They were already stale before this change -- all four IPv6 mobility configurations failed on tyf against unmodified master -- and re-recording them here would repair a pre-existing failure this change did not investigate. The set of failing fingerprints is therefore identical before and after. /examples/ipv6/pmipv6/ does not move: its tplx, ~tNl and ~tND values are unchanged, so no Router Solicitation in it was deferred. Every other IPv6 configuration -- nclients, hierarchical99, MLD, udpclientserver, DYMO, EIGRP -- uses Ipv6FlatNetworkConfigurator, which assigns permanent addresses, so a solicitation is never built from a tentative one. --- .../icmpv6/Ipv6NeighbourDiscovery.cc | 15 +++- tests/fingerprint/examples.csv | 6 +- tests/fingerprint/mipv6-refactoring.csv | 6 +- ...citation_SourceLinkLayerAddressOption.test | 75 +++++++++++++++++++ 4 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 tests/module/Ipv6RouterSolicitation_SourceLinkLayerAddressOption.test diff --git a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc index 9785d3f5522..3fe03ee0449 100644 --- a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc +++ b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc @@ -1010,10 +1010,17 @@ void Ipv6NeighbourDiscovery::createAndSendRsPacket(NetworkInterface *ie) // or the unspecified address. Ipv6Address myIPv6Address = ie->getProtocolData()->getPreferredAddress(); - if (myIPv6Address.isUnspecified()) - myIPv6Address = ie->getProtocolData()->getLinkLocalAddress(); // so we use the link local address instead - - if (ie->getProtocolData()->isTentativeAddress(myIPv6Address)) + // getPreferredAddress() returns the default source address for off-link destinations, + // and deliberately prefers a global address even while Duplicate Address Detection (DAD) + // is still running for it. A solicitation only has to reach the link-local all-routers + // multicast address, so fall back to the link-local address rather than straight to the + // unspecified address: a tentative address is not assigned to the interface yet + // (RFC 4862 Section 5.4), and Ipv6 would substitute it on transmission, releasing the + // solicitation with a source address that contradicts the option chosen below. + if (myIPv6Address.isUnspecified() || ie->getProtocolData()->isTentativeAddress(myIPv6Address)) + myIPv6Address = ie->getProtocolData()->getLinkLocalAddress(); + + if (myIPv6Address.isUnspecified() || ie->getProtocolData()->isTentativeAddress(myIPv6Address)) myIPv6Address = Ipv6Address::UNSPECIFIED_ADDRESS; Ipv6Address destAddr = Ipv6Address::ALL_ROUTERS_2; // all_routers multicast diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 283915ab865..0e7a11c157b 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, 43f0-699e/tplx;ff9a-8dbc/~tNl;9304-d5d5/~tND;44ef-1a45/tyf, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, b457-5c37/tplx;4038-4ba8/~tNl;d2a0-90db/~tND;ed3e-17fa/tyf, PASS, wireless EthernetMac +/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 5272-c850/tplx;1d99-c152/~tNl;9ca1-b5ff/~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 4c2175bce5a..361ba8afe04 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, 7ed8-bee3/~tNlb, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c Handover -r 0, 70s, 9489-e81a/~tNlb, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, f670-714f/~tNlb, PASS, wireless EthernetMac +/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 05e6-93d0/~tNlb, PASS, wireless EthernetMac # IPv6 examples # MLD example — new PASS row; ~tNl locks the MLD Report/Query/Done/MAS-Query packet exchange (traffic+lengths); diff --git a/tests/module/Ipv6RouterSolicitation_SourceLinkLayerAddressOption.test b/tests/module/Ipv6RouterSolicitation_SourceLinkLayerAddressOption.test new file mode 100644 index 00000000000..0841427daec --- /dev/null +++ b/tests/module/Ipv6RouterSolicitation_SourceLinkLayerAddressOption.test @@ -0,0 +1,75 @@ +%description: +Tests that a Router Solicitation whose IP source address is a real (not +unspecified) address carries the Source Link-Layer Address option, even when +Duplicate Address Detection (DAD) is still running for the host's global +address at the moment the solicitation is sent. + +RFC 4861 Section 6.3.7: "A host sends Router Solicitations to the all-routers +multicast address. The IP source address is set to either one of the +interface's unicast addresses or the unspecified address. The Source +Link-Layer Address option SHOULD be set to the host's link-layer address, if +the IP source address is not the unspecified address." + +The router logs the link-layer address it extracts from each solicitation, so +a missing option shows up as an all-zero MAC address. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned +import inet.networklayer.configurator.ipv6.Ipv6NetworkConfigurator; +import inet.node.ethernet.EthernetSwitch; +import inet.node.ipv6.Router6; +import inet.node.ipv6.StandardHost6; +import ned.DatarateChannel; + +network RsSourceLinkLayerOptionNetwork +{ + types: + channel ethline extends DatarateChannel + { + delay = 0.1us; + datarate = 100Mbps; + } + submodules: + configurator: Ipv6NetworkConfigurator; + router: Router6; + switch: EthernetSwitch; + host[2]: StandardHost6; + connections: + router.ethg++ <--> ethline <--> switch.ethg++; + for i=0..1 { + host[i].ethg++ <--> ethline <--> switch.ethg++; + } +} +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +record-vector-results = false +ned-path = ../../../../src +network = RsSourceLinkLayerOptionNetwork +sim-time-limit = 10s +cmdenv-express-mode = false +cmdenv-log-prefix = "%C: " + +# Stateless address autoconfiguration: the configurator only hands out the prefix +**.ipv6.configurator.networkConfiguratorModule = "configurator" +*.configurator.assignAddressesToHosts = false +*.configurator.config = xml("") + +# Advertise often, so that a host holds a tentative global address by the time +# its own Router Solicitation is due +*.router.ipv6.neighbourDiscovery.minIntervalBetweenRAs = 0.1s +*.router.ipv6.neighbourDiscovery.maxIntervalBetweenRAs = 0.2s + +%#-------------------------------------------------------------------------------------------------------------- +%subst: /omnetpp::// +%#-------------------------------------------------------------------------------------------------------------- +%not-contains: stdout +MAC Address '00-00-00-00-00-00' extracted +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +MAC Address '0A-AA- +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: ( +%#--------------------------------------------------------------------------------------------------------------