From 0855b710131d083121987f380e299e2409bb2314 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Thu, 10 Sep 2026 18:51:02 +0200 Subject: [PATCH 1/2] ipv6: refactor: give choosePreferredAddress() a single exit The function picked the preferred source address by returning from the middle of its scan loop as soon as it met a routable address, and again at the end for the link-local fallback. Both exits also carried the F_IP_ADDRESS notification, so anything that has to run after the address list is settled had to be written twice. The scan now records the routable candidate and breaks, and the choice and the notification happen once, after it. Behaviour is unchanged: the first routable unicast address in sort order still wins, the first link-local one is still the fallback, and an empty address list still returns early without notifying. --- .../networklayer/ipv6/Ipv6InterfaceData.cc | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/inet/networklayer/ipv6/Ipv6InterfaceData.cc b/src/inet/networklayer/ipv6/Ipv6InterfaceData.cc index 9ee9a54cc13..203a1bbcc83 100644 --- a/src/inet/networklayer/ipv6/Ipv6InterfaceData.cc +++ b/src/inet/networklayer/ipv6/Ipv6InterfaceData.cc @@ -473,25 +473,33 @@ void Ipv6InterfaceData::choosePreferredAddress() // right away. Falling back to a link-local source for an off-link // destination would be non-routable (RFC 4291 Section 2.5.6), so link-local // is used only when no routable address exists at all. + Ipv6Address routableCandidate = Ipv6Address::UNSPECIFIED_ADDRESS; + simtime_t routableExpiry = SIMTIME_ZERO; Ipv6Address linkLocalCandidate = Ipv6Address::UNSPECIFIED_ADDRESS; simtime_t linkLocalExpiry = SIMTIME_ZERO; for (auto& elem : addresses) { if (!elem.address.isUnicast()) continue; if (!elem.address.isLinkLocal()) { - preferredAddr = elem.address; - preferredAddrExpiryTime = elem.expiryTime; - if (changed) - changed1(F_IP_ADDRESS); - return; + routableCandidate = elem.address; + routableExpiry = elem.expiryTime; + break; } if (linkLocalCandidate.isUnspecified()) { linkLocalCandidate = elem.address; linkLocalExpiry = elem.expiryTime; } } - preferredAddr = linkLocalCandidate; - preferredAddrExpiryTime = linkLocalExpiry; + + if (!routableCandidate.isUnspecified()) { + preferredAddr = routableCandidate; + preferredAddrExpiryTime = routableExpiry; + } + else { + preferredAddr = linkLocalCandidate; + preferredAddrExpiryTime = linkLocalExpiry; + } + if (changed) changed1(F_IP_ADDRESS); } From bca0998d423d0e34058658effe47549537d9678e Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Thu, 10 Sep 2026 19:12:47 +0200 Subject: [PATCH 2/2] ipv6: fix: join the solicited-node group of each assigned address An IPv6 interface never joined the solicited-node multicast group of any address it held. Membership was derived at reception time instead: Ipv6RoutingTable::isLocalAddress() recomputed the solicited-node address of every address of every interface and compared it with the destination. That makes a Duplicate Address Detection answer arrive, but nothing is ever announced, so a node probed an address while declaring no membership in the group that carries the answer, and with Multicast Listener Discovery enabled no report for a ff02::1:ffXX:XXXX group appeared on the wire at all. RFC 4861 Section 7.2.1 requires the node to join the solicited-node multicast address corresponding to each address assigned to the interface, and to join and leave as addresses are added and removed. RFC 4862 Section 5.4.2 requires the join before the first probe of a tentative address, and says what skipping it costs: with an MLD-snooping switch "no multicast reception will be available until the MLD report is sent". Ipv6InterfaceData joins in assignAddress(), the one place the address list grows, and leaves in all three places it shrinks: both removeAddress() overloads and the valid-lifetime sweep inside choosePreferredAddress(), which is the quiet one and the one stateless autoconfiguration reaches when a prefix lifetime elapses. The sweep collects the addresses it drops and leaves their groups only after the list is sorted and preferredAddr is chosen, because leaving hands control to a Multicast Listener Discovery module that builds and sends a Done message. Several unicast addresses map to one solicited-node address -- under stateless autoconfiguration a link-local and a global address formed from the same interface identifier always do -- and RFC 4861 Section 7.2.1 forbids leaving before the last of them is gone. changeMulticastGroupMembership() already counts joins in numOfExcludeModeSockets, so one join per address and one leave per address gives that for free. The leave is guarded on actual membership rather than on the interface flags, so a leave that never had a matching join cannot drive that count below zero. The loopback address is excluded: it is not a link address, and its solicited-node address would collide with that of every unicast address ending in the same 24 bits. Reception does not change. The derived path in matchesSolicitedNodeMulticastAddress() is kept, so isLocalAddress() accepts a solicited-node datagram exactly as before. Router-side state does grow: a router with multicast forwarding now records a listener entry per announced solicited-node group. PIM ignores them, because a scope-2 group is neither routable nor source-specific. Fingerprints move only where Multicast Listener Discovery is enabled, because that is the only place a join becomes a packet. Under MLDv1 -- examples/ipv6/mld MldDemo and examples/pim/{dm,sm}_ipv6 -- each first join of a group adds an unsolicited Multicast Listener Report. Under MLDv2 -- examples/ipv6/mld MldV2Ssm and examples/pim/ssm_ipv6 -- no packet is added at all; the group becomes one more multicast address record inside reports that already existed, so those reports grow. The whole suite gives three failures before and after, the five configurations above being the only ones whose recorded values move. --- .../icmpv6/Ipv6NeighbourDiscovery.cc | 6 +- .../networklayer/ipv6/Ipv6InterfaceData.cc | 64 +++++++++++ .../networklayer/ipv6/Ipv6InterfaceData.h | 4 + .../networklayer/ipv6/Ipv6RoutingTable.cc | 11 +- tests/fingerprint/examples.csv | 6 +- tests/fingerprint/mipv6-refactoring.csv | 4 +- .../IPv6_solicited_node_group_join.test | 105 ++++++++++++++++++ tests/module/MLDv2_host_query.test | 11 +- tests/module/MLDv2_host_ssm.test | 19 +++- 9 files changed, 209 insertions(+), 21 deletions(-) create mode 100644 tests/module/IPv6_solicited_node_group_join.test diff --git a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc index 9785d3f5522..ecf46f9611a 100644 --- a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc +++ b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc @@ -846,8 +846,10 @@ void Ipv6NeighbourDiscovery::initiateDad(const Ipv6Address& tentativeAddr, Netwo msg->setContextPointer(dadEntry); dadEntry->timeoutMsg = msg; - // added uniform(0, IPv6_MAX_RTR_SOLICITATION_DELAY) to account for joining the solicited-node multicast - // group which is delay up to one 1 second (RFC 4862, 5.4.2) + // TODO the uniform(0, IPv6_MAX_RTR_SOLICITATION_DELAY) term was meant for the RFC 4862 + // Section 5.4.2 delay before joining the solicited-node group, but it is added to the + // timeout -- the wait for an answer -- instead of delaying the first probe. The group + // itself is now joined when the address is assigned. See issue #1179. scheduleAfter(ie->getProtocolData()->getRetransTimer() + uniform(0, IPv6_MAX_RTR_SOLICITATION_DELAY), msg); emit(startDadSignal, 1); diff --git a/src/inet/networklayer/ipv6/Ipv6InterfaceData.cc b/src/inet/networklayer/ipv6/Ipv6InterfaceData.cc index 203a1bbcc83..dd7e099cde2 100644 --- a/src/inet/networklayer/ipv6/Ipv6InterfaceData.cc +++ b/src/inet/networklayer/ipv6/Ipv6InterfaceData.cc @@ -312,6 +312,11 @@ void Ipv6InterfaceData::assignAddress(const Ipv6Address& addr, bool tentative, Ipv6AddressInfo info(ownerp, addr); m->emit(ipv6AddressAssignedSignal, &info); } + + // choosePreferredAddress() above drops an address whose valid lifetime has already + // run out, and leaves its group again; join only for one that is still held. + if (findAddress(addr) != -1) + joinSolicitedNodeMulticastGroup(addr); } void Ipv6InterfaceData::updateMatchingAddressExpiryTimes(const Ipv6Address& prefix, int length, @@ -425,6 +430,8 @@ void Ipv6InterfaceData::removeAddress(const Ipv6Address& address) Ipv6AddressInfo info(ownerp, address); m->emit(ipv6AddressRemovedSignal, &info); } + + leaveSolicitedNodeMulticastGroup(address); } bool Ipv6InterfaceData::addrLess(const AddressData& a, const AddressData& b) @@ -448,9 +455,11 @@ void Ipv6InterfaceData::choosePreferredAddress() // remove expired addresses (expiryTime == 0 means infinite lifetime) simtime_t now = simTime(); bool changed = false; + Ipv6AddressVector expired; for (auto it = addresses.begin(); it != addresses.end(); ) { if (it->expiryTime != SIMTIME_ZERO && it->expiryTime <= now) { EV_INFO << "Address " << it->address << " has expired, removing\n"; + expired.push_back(it->address); it = addresses.erase(it); changed = true; } @@ -460,6 +469,7 @@ void Ipv6InterfaceData::choosePreferredAddress() if (addresses.empty()) { preferredAddr = Ipv6Address(); + leaveExpiredSolicitedNodeMulticastGroups(expired); return; } @@ -502,6 +512,17 @@ void Ipv6InterfaceData::choosePreferredAddress() if (changed) changed1(F_IP_ADDRESS); + + leaveExpiredSolicitedNodeMulticastGroups(expired); +} + +void Ipv6InterfaceData::leaveExpiredSolicitedNodeMulticastGroups(const Ipv6AddressVector& expired) +{ + // Leaving hands control to a Multicast Listener Discovery module, which builds and + // sends a Done message, so it must not happen until this object's own state is + // consistent again: the address list erased and sorted, and preferredAddr chosen. + for (const auto& addr : expired) + leaveSolicitedNodeMulticastGroup(addr); } void Ipv6InterfaceData::addAdvPrefix(const AdvPrefix& advPrefix) @@ -561,6 +582,48 @@ void Ipv6InterfaceData::leaveMulticastGroup(const Ipv6Address& multicastAddress) changeMulticastGroupMembership(multicastAddress, MCAST_EXCLUDE_SOURCES, empty, MCAST_INCLUDE_SOURCES, empty); } +void Ipv6InterfaceData::joinSolicitedNodeMulticastGroup(const Ipv6Address& addr) +{ + // RFC 4861 Section 7.2.1: "When a multicast-capable interface becomes enabled, the + // node MUST join the all-nodes multicast address on that interface, as well as the + // solicited-node multicast address corresponding to each of the IP addresses assigned + // to the interface." Tentative addresses are included: RFC 4862 Section 5.4.2 requires + // the group to be joined before the first Duplicate Address Detection probe is sent. + // + // The loopback address is excluded because it is not a link address: its solicited-node + // address would collide with that of every real address ending in the same 24 bits. + if (!isSolicitedNodeGroupOwner(addr)) + return; + + joinMulticastGroup(addr.formSolicitedNodeMulticastAddress()); +} + +void Ipv6InterfaceData::leaveSolicitedNodeMulticastGroup(const Ipv6Address& addr) +{ + // RFC 4861 Section 7.2.1: "a node MUST NOT leave the solicited-node multicast group + // until all assigned addresses corresponding to that multicast address have been + // removed." Several unicast addresses map to one solicited-node address -- with + // stateless autoconfiguration a link-local and a global address formed from the same + // interface identifier always do -- so this is the common case, not the exception. + // changeMulticastGroupMembership() counts the joins, so one leave per address is + // enough: the group entry only goes away with the last of them. + if (!isSolicitedNodeGroupOwner(addr)) + return; + + // Membership, not the interface flags, decides whether a leave is due. The flags are + // read once when the address is assigned and again here, and a leave that never had a + // matching join would drive the reference count in changeMulticastGroupMembership() + // below zero, leaving the interface a member of a group it never joined. + Ipv6Address solNodeAddr = addr.formSolicitedNodeMulticastAddress(); + if (isMemberOfMulticastGroup(solNodeAddr)) + leaveMulticastGroup(solNodeAddr); +} + +bool Ipv6InterfaceData::isSolicitedNodeGroupOwner(const Ipv6Address& addr) const +{ + return ownerp && ownerp->isMulticast() && addr.isUnicast() && !addr.isLoopback(); +} + void Ipv6InterfaceData::changeMulticastGroupMembership(Ipv6Address multicastAddress, McastSourceFilterMode oldFilterMode, const Ipv6AddressVector& oldSourceList, McastSourceFilterMode newFilterMode, const Ipv6AddressVector& newSourceList) @@ -846,6 +909,7 @@ Ipv6Address Ipv6InterfaceData::removeAddress(Ipv6InterfaceData::AddressType type Ipv6AddressInfo info(ownerp, addr); m->emit(ipv6AddressRemovedSignal, &info); } + leaveSolicitedNodeMulticastGroup(addr); } return addr; diff --git a/src/inet/networklayer/ipv6/Ipv6InterfaceData.h b/src/inet/networklayer/ipv6/Ipv6InterfaceData.h index e8389a19e35..11794945d28 100644 --- a/src/inet/networklayer/ipv6/Ipv6InterfaceData.h +++ b/src/inet/networklayer/ipv6/Ipv6InterfaceData.h @@ -431,6 +431,10 @@ class INET_API Ipv6InterfaceData : public InterfaceProtocolData protected: int findAddress(const Ipv6Address& addr) const; void choosePreferredAddress(); + void joinSolicitedNodeMulticastGroup(const Ipv6Address& addr); + void leaveSolicitedNodeMulticastGroup(const Ipv6Address& addr); + void leaveExpiredSolicitedNodeMulticastGroups(const Ipv6AddressVector& expired); + bool isSolicitedNodeGroupOwner(const Ipv6Address& addr) const; void changed1(int fieldId) { changed(interfaceIpv6ConfigChangedSignal, fieldId); } HostMulticastData *getHostData() { if (!hostMcastData) hostMcastData = new HostMulticastData(); return hostMcastData; } const HostMulticastData *getHostData() const { return const_cast(this)->getHostData(); } diff --git a/src/inet/networklayer/ipv6/Ipv6RoutingTable.cc b/src/inet/networklayer/ipv6/Ipv6RoutingTable.cc index 0f1996eb21d..110919abead 100644 --- a/src/inet/networklayer/ipv6/Ipv6RoutingTable.cc +++ b/src/inet/networklayer/ipv6/Ipv6RoutingTable.cc @@ -267,16 +267,17 @@ void Ipv6RoutingTable::assignRequiredNodeAddresses(NetworkInterface *ie) /*o Any additional Unicast and Anycast Addresses that have been configured for the node's interfaces (manually or automatically).*/ - // FIXME: commented out the following lines, because these addresses - // are implicitly checked for in isLocalAddress() (we don't want redundancy, - // and manually adding solicited-node mcast address for each and every address - // is very error-prone) - // // o The All-Nodes Multicast Addresses defined in section 2.7.1. + // + // Joined below, together with the all-routers group. /*o The Solicited-Node Multicast Address for each of its unicast and anycast addresses.*/ + // Not joined here: Ipv6InterfaceData::assignAddress() joins the solicited-node group + // of an address as it is assigned, and leaves it as it is removed, so the membership + // tracks the address list on its own (RFC 4861 Section 7.2.1). + // o Multicast Addresses of all other groups to which the node belongs. /*A router is required to recognize all addresses that a host is diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 696ba2cb33b..20f0553cc64 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -465,15 +465,15 @@ /examples/ospfv3/v3_square_2_areas/, -f omnetpp.ini -c General -r 0, 200s, bbee-ed95/tplx;ac7a-261c/~tNl;025f-56b5/tyf;c1f1-6687/~tND, PASS, EthernetMac /examples/pim/dm/, -f omnetpp.ini -c General -r 0, 300s, 5542-465a/tplx;320d-ec2e/~tNl;7540-425c/~tND;ed5b-ae9d/tyf, PASS, igmp EthernetMac Ipv4 -/examples/pim/dm_ipv6/, -f omnetpp.ini -c General -r 0, 120s, 3c32-3dca/~tNlb, PASS, MLD EthernetMac Ipv6 +/examples/pim/dm_ipv6/, -f omnetpp.ini -c General -r 0, 120s, 3c6e-baaf/~tNlb, PASS, MLD EthernetMac Ipv6 /examples/pim/iptv/, -f omnetpp.ini -c PIM_SM -r 0, 35s, 9fdf-a54f/tplx;c356-8a7d/~tNl;fddd-28bc/~tND;11d1-62c7/tyf, PASS, igmp ospf EthernetMac Ipv4 /examples/pim/iptv/, -f omnetpp.ini -c PIM_DM -r 0, 35s, 25e4-e1ff/tplx;280f-57ab/~tNl;6e8e-78ba/~tND;96bd-4dca/tyf, PASS, igmp ospf EthernetMac Ipv4 /examples/pim/sm/, -f omnetpp.ini -c Scenario1 -r 0, 100s, 3f92-7b84/tplx;1cf9-ee08/~tNl;7bc2-d697/~tND;ba86-0da1/tyf, PASS, igmp EthernetMac Ipv4 /examples/pim/sm/, -f omnetpp.ini -c Scenario2 -r 0, 150s, b64f-022b/tplx;5022-893e/~tNl;9384-a10d/~tND;ce2a-7241/tyf, PASS, igmp EthernetMac Ipv4 /examples/pim/sm/, -f omnetpp.ini -c Scenario3 -r 0, 300s, a4d4-39b0/tplx;2e76-520f/~tNl;eb91-fabc/~tND;4f37-e2c3/tyf, PASS, igmp EthernetMac Ipv4 /examples/pim/sm/, -f omnetpp.ini -c Scenario4 -r 0, 100s, 1bbc-ce82/tplx;90c6-efdf/~tNl;3c6f-feb9/~tND;015c-b31f/tyf, PASS, igmp EthernetMac Ipv4 -/examples/pim/sm_ipv6/, -f omnetpp.ini -c General -r 0, 120s, 0a21-bad8/~tNlb, PASS, MLD EthernetMac Ipv6 -/examples/pim/ssm_ipv6/, -f omnetpp.ini -c General -r 0, 60s, 6eba-9db0/~tNl, PASS, MLD EthernetMac Ipv6 +/examples/pim/sm_ipv6/, -f omnetpp.ini -c General -r 0, 120s, 1dcf-20a5/~tNlb, PASS, MLD EthernetMac Ipv6 +/examples/pim/ssm_ipv6/, -f omnetpp.ini -c General -r 0, 60s, 094d-904b/~tNl, PASS, MLD EthernetMac Ipv6 /examples/seaport/, -f omnetpp.ini -c General -r 0, 1000s, 32cb-156a/tplx;fcf4-00cd/~tNl;e3e9-834d/tyf, PASS, igmp EthernetMac Ipv4 diff --git a/tests/fingerprint/mipv6-refactoring.csv b/tests/fingerprint/mipv6-refactoring.csv index 4c2175bce5a..b26c686526e 100644 --- a/tests/fingerprint/mipv6-refactoring.csv +++ b/tests/fingerprint/mipv6-refactoring.csv @@ -16,8 +16,8 @@ # MLD example — new PASS row; ~tNl locks the MLD Report/Query/Done/MAS-Query packet exchange (traffic+lengths); # ~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 MldDemo -r 0, 30s, e72b-4cc1/~tNl, PASS, EthernetMac MLD +/examples/ipv6/mld/, -f omnetpp.ini -c MldV2Ssm -r 0, 30s, f947-4b1c/~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, diff --git a/tests/module/IPv6_solicited_node_group_join.test b/tests/module/IPv6_solicited_node_group_join.test new file mode 100644 index 00000000000..0383e6f05dd --- /dev/null +++ b/tests/module/IPv6_solicited_node_group_join.test @@ -0,0 +1,105 @@ +%description: +Tests that a node joins the solicited-node multicast group of every address +assigned to an interface, and announces the membership with a Multicast +Listener Report before it probes the address. + +RFC 4861 Section 7.2.1 requires a node to join the solicited-node multicast +address corresponding to each of the IP addresses assigned to an interface, +and to join and leave as addresses are added and removed. RFC 4862 Section +5.4.2 requires the join to happen before the first Duplicate Address +Detection probe of a tentative address, because with an MLD-snooping switch +"no multicast reception will be available until the MLD report is sent". + +A router and a host obtain their addresses by Stateless Address +Autoconfiguration, with Multicast Listener Discovery enabled so that each +join becomes a packet. The host's interface identifier is fixed by its MAC +address, so its link-local address is fe80::8aa:ff:fe00:2 and the +solicited-node group of that address is ff02::1:ff00:2. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned +import inet.networklayer.configurator.ipv6.Ipv6FlatNetworkConfigurator; +import inet.node.ipv6.Router6; +import inet.node.ipv6.StandardHost6; +import ned.DatarateChannel; + +network SolicitedNodeJoinNetwork +{ + types: + channel ethline extends DatarateChannel + { + delay = 0.1us; + datarate = 10Mbps; + } + submodules: + configurator: Ipv6FlatNetworkConfigurator; + router: Router6; + host: StandardHost6; + connections: + host.ethg++ <--> ethline <--> router.ethg++; +} +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +record-vector-results = false +ned-path = ../../../../src +network = SolicitedNodeJoinNetwork +sim-time-limit = 5s +cmdenv-express-mode = false +cmdenv-log-prefix = "%C: " + +# Multicast Listener Discovery makes every join visible as a packet. +**.ipv6.hasMld = true + +# One probe per address is enough, and keeps the log short. +**.neighbourDiscovery.dupAddrDetectTransmits = 1 + +# Ethernet NIC configuration +**.eth[*].queue.typename = "EthernetQosQueue" +**.eth[*].queue.dataQueue.typename = "DropTailQueue" +**.eth[*].queue.dataQueue.packetCapacity = 10 + +%#-------------------------------------------------------------------------------------------------------------- +%subst: /omnetpp::// +%#-------------------------------------------------------------------------------------------------------------- +%# +%# The host announces the solicited-node group of its link-local address. +%# +%contains: stdout +SolicitedNodeJoinNetwork.host.ipv6.mld: Mldv1: sending Multicast Listener Report for group=ff02::1:ff00:2 on iface=eth0 + +%# +%# The report precedes the host's own first Duplicate Address Detection probe +%# (RFC 4862 Section 5.4.2). Both greps are anchored on the host, so the +%# router's earlier probe cannot satisfy the ordering; grep -n keeps the file +%# order, so an ordered match over the two line numbers is enough. +%# +%postrun-command: grep -n "host\.ipv6\.mld: .*Report for group=ff02::1:ff00:2\|host\.ipv6\.neighbourDiscovery: .*INITIATING DUPLICATE ADDRESS" test.out | head -2 > order.out || true +%contains-regex: order.out +[0-9]+:.*host\.ipv6\.mld: .*Report for group=ff02::1:ff00:2.*\n[0-9]+:.*host\.ipv6\.neighbourDiscovery: .*INITIATING DUPLICATE ADDRESS + +%# +%# The host holds two addresses -- the link-local one and the global one it +%# forms from the router's prefix -- and probes both, so it runs Duplicate +%# Address Detection twice. +%# +%postrun-command: grep -c "host\.ipv6\.neighbourDiscovery: .*INITIATING DUPLICATE ADDRESS" test.out > dadcount.out || true +%contains: dadcount.out +2 + +%# +%# Both addresses carry the same interface identifier, so both map to the same +%# solicited-node group and the second join finds the group already joined. +%# RFC 4861 Section 7.2.1 requires exactly that: "multiple unicast addresses +%# may map into the same solicited-node multicast address". The second join +%# changes no state and emits no further report, so two addresses produce one +%# report, not two. +%# +%postrun-command: grep -c "host\.ipv6\.mld: .*sending Multicast Listener Report for group=ff02::1:ff00:2" test.out > joincount.out || true +%contains: joincount.out +1 +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: ( +%#-------------------------------------------------------------------------------------------------------------- diff --git a/tests/module/MLDv2_host_query.test b/tests/module/MLDv2_host_query.test index e8be7f79282..dcfa16ff4dc 100644 --- a/tests/module/MLDv2_host_query.test +++ b/tests/module/MLDv2_host_query.test @@ -61,9 +61,12 @@ network = inet.test.moduletest.lib.TestMLDNetwork %# First check that the join/set-filter were applied to the interface state %# %postrun-command: grep "TestMld: .*groups =" test.out > groups.out || true +%# ff02::1:ff00:1 is the solicited-node multicast group of the interface's own address, +%# joined when that address is assigned (RFC 4861 Section 7.2.1), so it is a member of +%# every dump and of every Current-State Report below. %contains: groups.out -TestMld: eth0: groups = ff3e::1 I 2001::1. -TestMld: eth0: groups = ff3e::1 E 2001::2. +TestMld: eth0: groups = ff02::1:ff00:1 E, ff3e::1 I 2001::1. +TestMld: eth0: groups = ff02::1:ff00:1 E, ff3e::1 E 2001::2. %# %# Now check that the Mldv2 module responded to each General Query with a Current-State @@ -75,10 +78,10 @@ TestMld: eth0: groups = ff3e::1 E 2001::2. %postrun-command: grep "TestMld: .*Received" test.out > received.out || true %contains-regex: received.out TestMld: Received: inet::Mldv2Report. -(?:.*\n)*?TestMld: Received: inet::Mldv2Report. +(?:.*\n)*?TestMld: Received: inet::Mldv2Report. (?:.*\n)*?TestMld: Received: inet::Mldv2Report. (?:.*\n)*?TestMld: Received: inet::Mldv2Report. -(?:.*\n)*?TestMld: Received: inet::Mldv2Report. +(?:.*\n)*?TestMld: Received: inet::Mldv2Report. (?:.*\n)*?TestMld: Received: inet::Mldv2Report. %# diff --git a/tests/module/MLDv2_host_ssm.test b/tests/module/MLDv2_host_ssm.test index 62643a44616..b83b47eaaca 100644 --- a/tests/module/MLDv2_host_ssm.test +++ b/tests/module/MLDv2_host_ssm.test @@ -70,12 +70,16 @@ seed-set = 0 %# First check that the set-filter commands modified the interface state %# %postrun-command: grep "TestMld: .* groups = " test.out > groups.out || true +%# ff02::1:ff00:1 is the solicited-node multicast group of the interface's own address, +%# joined when that address is assigned (RFC 4861 Section 7.2.1), so it is a member of +%# every dump. It carries no source filter, hence the bare "E" (EXCLUDE of nothing). %contains: groups.out -TestMld: eth0: groups = ff3e::1 I 2001::1 2001::2. -TestMld: eth0: groups = ff3e::1 I 2001::1. -TestMld: eth0: groups = ff3e::1 E 2001::2. -TestMld: eth0: groups = ff3e::1 E 2001::1 2001::2. -TestMld: eth0: groups = ff3e::1 I 2001::1. +TestMld: eth0: groups = ff02::1:ff00:1 E. +TestMld: eth0: groups = ff02::1:ff00:1 E, ff3e::1 I 2001::1 2001::2. +TestMld: eth0: groups = ff02::1:ff00:1 E, ff3e::1 I 2001::1. +TestMld: eth0: groups = ff02::1:ff00:1 E, ff3e::1 E 2001::2. +TestMld: eth0: groups = ff02::1:ff00:1 E, ff3e::1 E 2001::1 2001::2. +TestMld: eth0: groups = ff02::1:ff00:1 E, ff3e::1 I 2001::1. %# %# Now check the state transitions of the Mldv2 module @@ -105,7 +109,12 @@ State of group 'ff3e::1' on interface 'eth0' has changed: %# Unsolicited Report Interval, before the next scenario step. %# %postrun-command: grep "TestMld: Received" test.out > received.out || true +%# The first two reports are the State-Change Report for the solicited-node +%# group of the interface's own address, joined when that address is assigned +%# (RFC 4861 Section 7.2.1) and, like every state change, sent twice. %contains: received.out +TestMld: Received: inet::Mldv2Report. +TestMld: Received: inet::Mldv2Report. TestMld: Received: inet::Mldv2Report. TestMld: Received: inet::Mldv2Report. TestMld: Received: inet::Mldv2Report.