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 9ee9a54cc13..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; } @@ -473,27 +483,46 @@ 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); + + 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) @@ -553,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) @@ -838,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.