Skip to content

ipv6: join the solicited-node multicast group of every assigned address - #1191

Open
adamgeorge309 wants to merge 2 commits into
masterfrom
topic/gy/ipv6-solicited-node-join
Open

ipv6: join the solicited-node multicast group of every assigned address#1191
adamgeorge309 wants to merge 2 commits into
masterfrom
topic/gy/ipv6-solicited-node-join

Conversation

@adamgeorge309

Copy link
Copy Markdown
Contributor

What this changes

An IPv6 interface now joins the solicited-node multicast group of every address assigned to
it, and leaves that group when the address goes away. Until now INET never joined such a
group at all: membership was recomputed from the address list at reception time, so a node
sent a Duplicate Address Detection probe 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 ever appeared on the wire.

Fixes #1190.

Why

RFC 4861 Section 7.2.1 makes the join mandatory:

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. [...] In such cases
the node MUST join and leave the solicited-node multicast address corresponding to the new
and old addresses, respectively. Joining the solicited-node multicast address is done
using a Multicast Listener Discovery such as [MLD] or [MLDv2] protocols. [...] a node MUST
NOT leave the solicited-node multicast group until all assigned addresses corresponding to
that multicast address have been removed.

RFC 4862 Section 5.4.2 requires it 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".

How it works

The address list shrinks in three places, and the fix covers all three:
removeAddress(const Ipv6Address&), the removeAddress(AddressType) overload that Mobile
IPv6 uses for a care-of address, and the lifetime-expiry sweep inside
choosePreferredAddress() — the quiet one, and the one stateless autoconfiguration reaches
when a prefix lifetime elapses. Ipv6InterfaceData joins in assignAddress(), the single
place the list grows.

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. changeMulticastGroupMembership() already counts joins in
numOfExcludeModeSockets, so one join per address and one leave per address satisfies RFC
4861 Section 7.2.1's "MUST NOT leave [...] until all assigned addresses [...] have been
removed" without new bookkeeping. The leave is guarded on actual membership rather than on
the interface flags, which are read once at join time and again at leave time: a leave with
no matching join would otherwise drive that count below zero and leave the interface a
permanent member of a group it never joined.

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.

Reading order

Two commits, in order.

  1. ipv6: refactor: give choosePreferredAddress() a single exit — behaviour-preserving. The
    function returned from the middle of its scan loop, which left nowhere to put work that
    has to run after the address list is settled. Verified by the whole fingerprint suite:
    three failures and 62 errors, identical to the parent commit.
  2. ipv6: fix: join the solicited-node group of each assigned address — the fix, with the
    tests and baselines that travel with it (PR-SPLIT-BASELINE).

Read the source of the second alone with git show <sha> -- . ':!tests'.

What it does not change

Reception. Ipv6RoutingTable::isLocalAddress() keeps its derived path through
matchesSolicitedNodeMulticastAddress(), which accepted a solicited-node datagram before
and still does; the new membership only makes a second disjunct in Ipv6.cc:654 true as
well. Making the joined groups the single source of truth for reception would change
behaviour for any address assigned on a path that cannot join, so it is a separate decision.

Router-side state does grow. A router with multicast forwarding now records a listener entry
per announced solicited-node group, visible through getNumOfReportedMulticastGroups(). PIM
builds nothing from them: PimSm filters on scope and source-specificity, and
PimDm::multicastReceiverAdded() is reached but cannot create a route for a scope-2 group.

RFC 4862 Section 5.4.2's SHOULD to delay the join by a random interval between 0 and
MAX_RTR_SOLICITATION_DELAY is not implemented; the join is immediate. That SHOULD is
conditional — it applies when the probe is the first message after interface
(re)initialization, or when the address came from a multicast Router Advertisement — and the
same section requires the interface to receive and process datagrams for the group during
the delay regardless, which it does. INET's existing
uniform(0, IPv6_MAX_RTR_SOLICITATION_DELAY) at Ipv6NeighbourDiscovery.cc:851 is added
to the Duplicate Address Detection timeout, the wait for an answer, rather than to either
the probe or the join; that is #1179.

Architectural surface

  • Ipv6InterfaceData gains four protected helpers —
    joinSolicitedNodeMulticastGroup(), leaveSolicitedNodeMulticastGroup(),
    leaveExpiredSolicitedNodeMulticastGroups() and the shared predicate
    isSolicitedNodeGroupOwner(). No public API change, no NED parameter, no packet format,
    no feature descriptor.
  • Behaviour on the wire changes only where Multicast Listener Discovery is enabled
    (Ipv6NetworkLayer.hasMld), because that is the only place a join becomes a packet.
  • No sealed path is touched; doc/project/audit/seal-list.md seals only common/packet/.
  • No new AV-* or NV-* exception rows.

Baselines and tests that move

File Why
tests/fingerprint/mipv6-refactoring.csvexamples/ipv6/mld MldDemo MLDv1: each first join of a solicited-node group adds an unsolicited Multicast Listener Report
tests/fingerprint/mipv6-refactoring.csvexamples/ipv6/mld MldV2Ssm MLDv2: no packet is added; the group becomes one more Mldv2MulticastAddressRecord inside reports that already existed, so those reports grow
tests/fingerprint/examples.csvexamples/pim/dm_ipv6, sm_ipv6 General MLDv1, as above
tests/fingerprint/examples.csvexamples/pim/ssm_ipv6 General MLDv2, as above
tests/module/MLDv2_host_ssm.test the <dump what="groups"/> lines now also list ff02::1:ff00:1
tests/module/MLDv2_host_query.test as above, and each Current-State Report now carries ff02::1:ff00:1=IS_EX beside ff3e::1

No configuration without Multicast Listener Discovery moved.

tests/module/IPv6_solicited_node_group_join.test is new. It asserts that the host
announces ff02::1:ff00:2, the solicited-node group of its link-local address; that the
report precedes the host's own first Duplicate Address Detection probe (RFC 4862 Section
5.4.2); and that its two addresses — the link-local one and the global one formed from the
same interface identifier — run Duplicate Address Detection twice but produce exactly one
report for the group they share, which is RFC 4861 Section 7.2.1's "multiple unicast
addresses may map into the same solicited-node multicast address". It fails on the parent
commit and passes on this one.

The leave side is not covered by a test: no shipped configuration removes an IPv6 address
while another still maps to the same group. The join-side collapse above exercises the same
reference count from the other direction, and MLDv2_host_ssm.test shows it directly: the
tester's interface holds two addresses and dumps a single ff02::1:ff00:1 E entry.

Tests

Run in a worktree at origin/master (8ac5675c9c), release mode.

Fingerprints — whole suite, both before and after:

cd tests/fingerprint && ./fingerprinttest -s -F tyf
origin/master after commit 1 commit 2, before the baselines were regenerated commit 2, as submitted
test cases 1774 1774 1774 1774
failures 3 3 8 3
errors 62 62 62 62

The three failures throughout are examples/ipv6/mipv6 Handover and
RouteOptimizationTwoCNs and examples/ipv6/mipv6roaming Roaming, whose recorded ~tNlb
values are stale on master; they are unchanged here and deliberately not absorbed. Commit 1
moves nothing, which is what makes it a refactor. The five that appear in the third column
are exactly the configurations in the table above, and they are the five whose values this
pull request regenerates. The 62 errors are disabled optional features (VoIPStream,
TcpLwip) and never move.

Module tests:

cd tests/module && inet_run_module_tests -m release --no-build -l ERROR -f 'IPv6|Ipv6|MLD|MIPv6'

57 TOTAL, 55 PASS, 2 FAIL — the two are MIPv6_tcp_handover.test and
IPv6_packet_too_big.test, which fail on pristine master in release mode and pass in debug;
they are environmental and pre-existing. The count is one higher than on master because of
the new test.

Protocol tests:

cd tests/protocol/ipv6 && inet_run_protocol_tests -m release

29 TOTAL, 21 PASS, 8 FAIL (expected) — identical to pristine master, no unexpected failure.

Gates:

doc/project/enforcement/check-commits.sh origin/master..HEAD   # PASS
doc/project/enforcement/check-seals.sh                          # PASS

check-naming.sh src/inet/networklayer/ipv6/ reports three hits, all pre-existing and in
files this change does not touch (ipv6/headers, Ipv6ExtHeaderTag.msg,
Ipv6ExtensionHeaders.msg).

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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ipv6: the solicited-node multicast group of an assigned address is never joined

1 participant