Skip to content

mipv6: send mobility signaling to the next hop instead of to the whole link - #1262

Open
adamgeorge309 wants to merge 2 commits into
masterfrom
topic/gy/mipv6-unicast-signaling-v2
Open

adamgeorge309 wants to merge 2 commits into
masterfrom
topic/gy/mipv6-unicast-signaling-v2

Conversation

@adamgeorge309

@adamgeorge309 adamgeorge309 commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Mobile Internet Protocol version 6 (Mobile IPv6, MIPv6) signaling sent on a pinned output interface
now carries the link-layer address of its next hop instead of FF-FF-FF-FF-FF-FF. The branch is built
on #1152, which makes the Ipv6 module take the link-layer address from the Neighbour Cache when the
sender states a next hop; this pull request makes Mipv6 state it. Merge #1152 first. Until then this
pull request also shows #1152's commit 222173a, and only the last commit is new.

Closes #1261

The problem

Mipv6::sendMobilityMessageToIPv6Module() pins the output interface of the Binding Update (BU), the
Care-of Test Init (CoTI), the Binding Acknowledgement (BA) and the Binding Refresh Request, and
states no next hop, so Ipv6::datagramLocalOut() sends them to the broadcast address. On 802.11 the
access point relays each such broadcast back into its cell, so the mobile node receives its own
frames again.

examples/ipv6/mipv6 -c Handover with **.MN[*].numPcapRecorders = 1,
**.checksumMode = "computed" and **.fcsMode = "computed", captured at MN[0] (before:
222173a, the head of #1152; after: this pull request):

Message Before: time (s), link-layer destination After: time (s), link-layer destination
BU to the home agent, from the foreign link 21.028, FF-FF-FF-FF-FF-FF; AP_1 relays it back at 21.029 21.028, 0A-AA-00-00-00-04 (foreign router)
BU to the home agent after returning home 41.160, FF-FF-FF-FF-FF-FF; AP_Home relays it back at 41.161 40.066, 0A-AA-00-00-00-02 (home agent)
the home agent's BA, relayed by AP_Home into the home cell 41.162, FF-FF-FF-FF-FF-FF 40.067, 0A-AA-00-00-00-08 (mobile node)

The return home is 1.1 s earlier with the change because the two runs diverge after the first
changed frame.

The fix

Pinning an output interface skips next-hop determination, so the Ipv6 module has no address to look
up in the Neighbour Cache. #1152 explains why it must not resolve the destination instead: a mobile
node keeps its cache entry for the home agent after it has roamed.

Mipv6 now states the next hop (Request for Comments (RFC) 4861 Section 5.2): the next hop of the
longest matching route through the pinned interface, or the destination itself when that route is
on-link or the destination is link-local. Ipv6RoutingTable::doLongestPrefixMatch() is not used
because it may return a route through another interface. For a mobile node away from home the
result is the default router of the foreign link: the handover deletes the previous link's routes,
and the Router Advertisement that formed the care-of address installs the new default route.

Architectural surface

  • Mipv6 sets the NextHopAddressReq tag on the datagrams whose output interface it pins; ipv6: address a pinned unicast datagram from the Neighbour Cache #1152
    gives that tag its meaning for a pinned interface.
  • One new protected, non-virtual member, Mipv6::getNextHopOnInterface(); no other class calls it.
  • The link-layer destination address of the Binding Update (BU), Care-of Test Init (CoTI), Binding
    Acknowledgement (BA) and Binding Refresh Request frames changes. No packet format, Network
    Description (NED) parameter, signal, module interface or .oppfeatures entry changes, and no
    sealed path is touched.
  • Tests: one new module test and six fingerprint rows.
  • WHATSNEW: item 17 of the backward incompatible changes, because MIPv6 simulation trajectories
    change.
  • doc/project/enforcement/check-architecture.sh src/inet/networklayer/mipv6 passes.
    check-naming.sh src/inet/networklayer/mipv6 reports the gate names fromIPv6 and toIPv6 in
    Mipv6.ned, which this pull request does not touch.

Verification

Base: #1152 (222173a) on the merge base 49e1fa0. "origin/master" below means 49e1fa0;
origin/master has not changed src/ since then (it is now 4eb3bb4). Each set of runs follows
make -j4 MODE=release (or MODE=debug for the debug runs) at the repository root; module tests
run in tests/module, protocol tests in tests/protocol/ipv6, fingerprints in tests/fingerprint.

  • tests/module/MIPv6_signaling_unicast_mac.test (new): the mobile node moves home, abroad and home
    again; no BU, BA, Home Test Init (HoTI), CoTI, Home Test (HoT) or Care-of Test (CoT) to a global
    address may leave with a broadcast destination address, and the BU from the foreign link must go
    to the foreign router. It fails on the base and passes with this change.

  • inet_run_module_tests -m release --no-build -l ERROR (all module tests): 347 tests and 46
    failures on the base; 348 tests and the same 46 failures with this change: MIPv6_tcp_handover,
    IPv6_packet_too_big, 34 Transmission Control Protocol (TCP) tests, Ieee80211BitDomain,
    Ieee80211SymbolDomain, ConvolutionalCoder12, ConvolutionalCoder34, Ieee8021d-Stp
    (Spanning Tree Protocol), Ieee8021d-Rstp (Rapid Spanning Tree Protocol), PacketGate_1,
    UDPSocket_1, ExternalProcess_3, EtherHost_lifecycle. The same 46 fail on origin/master.

  • inet_run_module_tests -m debug --no-build -l ERROR -f 'IPv6|PIM|Pim|pim': 42 of 42 pass. The
    filter selects the Internet Protocol version 6 (IPv6) and Mobile IPv6 tests, and the Protocol
    Independent Multicast (PIM) tests, because PIM also sends on a pinned output interface
    (PimBase.cc:160, PimSm.cc:1692) through the Ipv6 path that ipv6: address a pinned unicast datagram from the Neighbour Cache #1152 changes; the two Internet
    Protocol version 4 (IPv4) PIM tests match the filter as well.

  • inet_run_protocol_tests -m release in tests/protocol/ipv6: 26 of 27 pass on the base and with
    this change; Rfc8200OverlappingFragments fails in both, as it does on origin/master. In debug,
    27 of 27 pass.

  • ./fingerprinttest -s -F tyf (no CSV argument, all rows): on the base 0 failures and 62 errors;
    with this change 6 failures and the same 62 errors; after the re-record 0 failures and the same
    62 errors. The 62 errors are rows of disabled features: VoipStream (40, including the
    Differentiated Services (diffserv) Voice over IP configurations), TcpLwip (14), the OpenSceneGraph
    (OSG) visualizer (7) and Z3 gate scheduling (1). The 6 re-recorded rows:

    • examples.csv: examples/ipv6/mipv6 Handover and RouteOptimizationTwoCNs,
      examples/ipv6/mipv6roaming Roaming, ingredients tplx (event times, module paths, lengths
      and extra data), ~tNl (inter-node times, node paths and lengths) and ~tND (inter-node times,
      node paths and packet bytes)
    • mipv6-refactoring.csv: the same three configurations, ~tNlb (with message contents)

    The mobility frames go to one neighbour: the access point no longer relays a copy into the cell,
    the other nodes lose those reception events, and each frame the access point delivers into the
    cell becomes a unicast frame and gains an 802.11 acknowledgement. This moves the times, paths,
    lengths and extra data. The packet bytes and message contents move with the destination address in
    the Medium Access Control (MAC) header. tyf (display strings and figures) was excluded from the
    run and is carried over. This change moves it too in the three examples.csv rows, but the
    recorded values did not match before it either: ./fingerprinttest -s -m ipv6/mipv6 examples.csv
    gives

    Configuration Recorded tyf Base With this change
    mipv6 Handover 44ef-1a45 e6fe-746f 53af-420e
    mipv6 RouteOptimizationTwoCNs ed3e-17fa fd98-4698 2ad8-5c7d
    mipv6roaming Roaming afae-2b3c 3588-8594 bc16-30b9
  • Statistical baselines: not run. The statistics repository (inet-framework/statistics) holds
    results for the three configurations whose fingerprints move, examples/ipv6/mipv6 Handover
    and RouteOptimizationTwoCNs and examples/ipv6/mipv6roaming Roaming; they are not re-recorded
    here.

  • doc/project/enforcement/check-commits.sh and check-classification.sh on the new commit pass.
    check-source-seals.sh --base origin/master passes. check-naming.sh --base origin/master and
    check-interfaces.sh report the same findings as on origin/master, none in a file this change
    touches.

Not addressed here

  • When the stated next hop has no Neighbour Cache entry, the Ipv6 module still uses the broadcast
    address instead of resolving it (the fallback ipv6: address a pinned unicast datagram from the Neighbour Cache #1152 keeps). In MIPv6_route_optimization.test the
    home agent's Binding Acknowledgement (BA) leaves this way, because the home agent has not yet
    resolved its neighbouring router.
  • In the module test network the mobile node starts return routability toward the home agent's
    link-local address and sends Care-of Test Init (CoTI) messages to it from the foreign link. They
    have no neighbour to address and stay broadcasts, so the new test excludes link-local
    destinations. Starting return routability toward a link-local address is a separate defect of
    Mipv6: the address is valid only on the home link, so the CoTI cannot reach it from the foreign
    link.

Devin Review

A locally-originated unicast datagram that carries a pinned output interface
went to the link layer with dest = FF-FF-FF-FF-FF-FF. Every Neighbour
Discovery message pins its output interface, so a solicited Neighbour
Advertisement and a Neighbour Unreachability Detection probe both left the
node as an Ethernet broadcast, although the neighbour was in the Neighbour
Cache. Every node on the link then received them, and a router forwarded the
advertisement and answered it with a spurious ICMPv6 Redirect.

RFC 4861 Section 7.2.4 requires the solicited advertisement to be unicast to
the soliciting node, and Section 5.2 says where the link-layer address comes
from: "Once the IP address of the next-hop node is known, the sender examines
the Neighbor Cache for link-layer information about that neighbor." The
multicast half of the same branch was corrected in 3be7618; the unicast
half was left on the broadcast address.

Pinning an output interface makes the Ipv6 module skip next-hop
determination, so it has no next-hop address to look up. Neighbour Discovery
therefore states the next hop it already knows -- an ND message is by
definition addressed to a neighbour on the link it goes out on -- and
datagramLocalOut() takes the link-layer address from the cache when a next
hop is stated.

Resolving the destination instead would be wrong. A pinned interface does not
imply an on-link destination: PIM-SM pins one for the unicast it sends toward
the rendezvous point, and a mobile node keeps a cache entry for its home agent
after it has roamed, so the entry addresses a node that is no longer on the
link. Both keep the previous behaviour, because neither states a next hop.

Not repaired here: an advertisement answering a unicast solicitation that
carried no Source Link-Layer Address option still goes out as a broadcast,
because the responder has no cache entry to use. RFC 4861 Section 7.2.4 says
such a node has to resolve the neighbour first, and the TODO in
sendSolicitedNa() records that this is not implemented.

This commit re-records 18 fingerprint rows: 14 in examples.csv, one per
IPv6 configuration that performs address resolution, and 4 in
mipv6-refactoring.csv, the second row that file keeps for four of those
configurations. Their Neighbour Discovery frames are now addressed to one
neighbour instead of to the whole link: a switch forwards them to one port,
and on 802.11 the other stations discard them. The nodes that no longer
receive them lose their reception events, so the event times, module
paths, lengths and extra data (tplx) and the inter-node times, node paths
and lengths (~tNl) move in the 14 examples.csv rows. The inter-node packet
bytes (~tND, in 9 of those rows) and the message contents (~tNlb, the 4
mipv6-refactoring.csv rows) move with the destination address in the
Medium Access Control (MAC) header. The display
string and figure ingredients (tyf) were excluded from the run and are
carried over unchanged. On origin/master 49e1fa0 the whole suite
(./fingerprinttest -s -F tyf) has 0 failures; with this change it fails
exactly these 18 rows before the re-record, so no configuration without
IPv6 address resolution moves.

Change: src.networklayer.ipv6 | behavior.change.fix | fingerprint test
Mobile Internet Protocol version 6 (Mobile IPv6, MIPv6) pins the output
interface of the Binding Update (BU), the Care-of Test Init (CoTI), the
Binding Acknowledgement (BA) and the Binding Refresh Request, but states
no next hop. A pinned interface makes the Ipv6 module skip next-hop
determination, and with no next hop Ipv6::datagramLocalOut() addresses
the frame to FF-FF-FF-FF-FF-FF. On an 802.11 link the access point
relays such a frame back into its cell, so every node on the link
receives a mobile node's BU to its home agent and to its correspondent
node, and its CoTI. The home agent's and the correspondent node's BA go
out as Ethernet broadcasts.

Request for Comments (RFC) 4861 Section 5.2: "Once the IP address of
the next-hop node is known, the sender examines the Neighbor Cache for
link-layer information about that neighbor." The next hop of an on-link
destination is the destination; otherwise it is a router.

Mipv6 now states the next hop, which the Ipv6 module looks up in the
Neighbour Cache since #1152 ("ipv6: fix: address a pinned unicast
datagram from the Neighbour Cache"). Neighbour Discovery passes its
destination as the next hop, because a Neighbour Discovery message
always goes to a neighbour; a mobility message's destination need not
be on-link, so Mipv6 takes the next hop of the longest matching route
through the pinned interface, or the destination itself when that route
is on-link. A link-local destination is its own next hop (RFC 4861
Section 5.1). Ipv6RoutingTable::doLongestPrefixMatch() is not used
because it may return a route through another interface, whose next hop
is not a neighbour on the pinned one.

For a mobile node away from home the result is the default router on
the foreign link: the Router Advertisement that formed the care-of
address installed its default route, and the handover deleted the
routes of the previous link. It is not the destination's Neighbour
Cache entry, which a mobile node keeps for its home agent after it has
roamed.

To reproduce, run examples/ipv6/mipv6 -c Handover with a PcapRecorder
on MN[0] (**.MN[*].numPcapRecorders = 1, **.checksumMode = "computed",
**.fcsMode = "computed"): the Binding Update (BU) to the home agent
leaves with destination address FF-FF-FF-FF-FF-FF, and the access point
sends it back into the cell 1 ms later.

Not repaired here: when the next hop has no Neighbour Cache entry, the
Ipv6 module still falls back to the broadcast address instead of
resolving it. In MIPv6_route_optimization.test the home agent's Binding
Acknowledgement (BA) leaves this way, because the home agent has not yet
resolved its neighbouring router. In the module test network the mobile
node also sends Care-of Test Init (CoTI) messages to the home agent's
link-local address from the foreign link; that address is not a
neighbour there, so they stay broadcasts, and the new test excludes
link-local destinations. Starting return routability toward a link-local
address is a separate defect of Mipv6: the address is valid only on the
home link, so the CoTI cannot reach it from the foreign link.

tests/module/MIPv6_signaling_unicast_mac.test covers registration,
return routability and de-registration, and fails without this change.

This commit re-records six fingerprint rows, one in examples.csv and one
in mipv6-refactoring.csv for each of the three MIPv6 configurations:
examples/ipv6/mipv6 Handover and RouteOptimizationTwoCNs, and
examples/ipv6/mipv6roaming Roaming. Their mobility frames are addressed
to one neighbour instead of the whole link: the access point no longer
relays a copy into the cell, the other nodes lose those reception
events, and each frame the access point delivers into the cell becomes
a unicast frame and gains an 802.11 acknowledgement. That
moves the event times, module paths, lengths and extra data (tplx) and
the inter-node times, node paths and lengths (~tNl); the inter-node
packet bytes (~tND) and message contents (~tNlb) also move with the
destination address in the Medium Access Control (MAC) header. The whole
suite (./fingerprinttest -s -F tyf) fails exactly these six rows before
the re-record, so no other configuration that ran moves in those
ingredients; the 62 rows of disabled features did not run.

The display string and figure ingredients (tyf) were excluded from the
run and are carried over. This change moves them too, in the three
examples.csv rows, but the recorded values did not match before it
either. ./fingerprinttest -s -m ipv6/mipv6 examples.csv gives, for
Handover, e6fe-746f on the parent commit and 53af-420e with this change
against the recorded 44ef-1a45; for RouteOptimizationTwoCNs, fd98-4698
and 2ad8-5c7d against ed3e-17fa; for Roaming, 3588-8594 and bc16-30b9
against afae-2b3c.

The statistical baselines of these three configurations were not run.

Change: src.mipv6.Mipv6 | behavior.change.fix | fingerprint test whatsnew

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment on lines +670 to +679
for (int i = 0; i < rt6->getNumRoutes(); i++) {
const Ipv6Route *route = rt6->getRoute(i);
if (route->getInterface() == nullptr || route->getInterface()->getInterfaceId() != interfaceId)
continue;
if (!destAddr.matches(route->getDestPrefix(), route->getPrefixLength()))
continue;
if (route->getExpiryTime() != 0 && simTime() > route->getExpiryTime()) // 0 means infinity
continue;
// an on-link destination is its own next hop
return route->getNextHop().isUnspecified() ? destAddr : route->getNextHop();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mobility signaling ignores redirected next hops

After an IPv6 Redirect, getNextHopOnInterface still selects the original route's router. processRedirectPacket updates the destination cache, which this lookup ignores. Mobility messages can miss their destination when the former router stops forwarding.

Learn more

An IPv6 Redirect changes the next hop for one destination by updating the destination cache, not the routing table. The normal IPv6 output path reads that cache in determineOutputInterface. Mobility messages pin an interface, bypassing that path, and this lookup selects the old route instead. Thus a mobility packet can take a different first hop from ordinary packets to the same destination.

Example: A correspondent node reaches a mobile node's care-of address through router A. Router A redirects that destination to router B on the same link. Ordinary packets use B, but a later Binding Acknowledgement still uses A; if A stops forwarding, the acknowledgement does not arrive.

Recommended fix: In getNextHopOnInterface, consult lookupDestCache before the route scan and accept its next hop only if its cached interface matches interfaceId. Fall back to the longest matching route on the pinned interface otherwise, and test a redirected destination with a subsequent pinned mobility message.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

This branch has not been deployed

No deployments
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.

mipv6: mobility messages sent on a pinned interface go to the link-layer broadcast address

1 participant