Summary
Mipv6::datagramLocalOutHook() rewrites the IPv6 header of an outbound packet and
re-inserts it with a plain insertAtFront(), bypassing insertNetworkProtocolHeader().
The NetworkProtocolInd tag therefore keeps pointing at the header as it was before
the rewrite, and every later hook that reads the tag sees the old addresses.
Route optimization has two such paths, both in
src/inet/networklayer/mipv6/Mipv6.cc. The Type 2 Routing Header path
(Mipv6.cc:2019-2039) rewrites the destination:
auto header = datagram->removeAtFront<Ipv6Header>();
...
header->setDestAddress(ro.exit);
...
datagram->insertAtFront(t2RH);
datagram->insertAtFront(header); // NetworkProtocolInd still holds the old header
The Home Address Option path (Mipv6.cc:2048-2070) rewrites the source the same way
(header->setSrcAddress(ro.entry); followed by datagram->insertAtFront(header);).
The same file gets it right on the receive side: Mipv6.cc:1894 and Mipv6.cc:1959 both
call insertNetworkProtocolHeader(), which updates the tag.
What the contract says
insertNetworkProtocolHeader() (src/inet/networklayer/common/L3Tools.cc:70-76) exists
precisely to keep the tag and the packet in step:
void insertNetworkProtocolHeader(Packet *packet, const Protocol& protocol, const Ptr<NetworkHeaderBase>& header)
{
auto networkProtocolInd = packet->addTagIfAbsent<NetworkProtocolInd>();
networkProtocolInd->setProtocol(&protocol);
networkProtocolInd->setNetworkProtocolHeader(header);
insertProtocolHeader(packet, protocol, header);
}
Four outbound netfilter hooks in the tree depend on that tag being current, all of them to
build a transport pseudo-header from the network-layer addresses:
src/inet/transportlayer/tcp_common/TcpChecksumInsertionHook.cc:32
src/inet/transportlayer/sctp/SctpChecksumInsertionHook.cc:32
src/inet/transportlayer/sctp/SctpUdpHook.cc:33
src/inet/transportlayer/udp/Udp.cc:1398 (UdpChecksumInsertionHook::datagramPostRoutingHook())
Mipv6 registers its hook on the same IPv6 netfilter (Mipv6.cc:128,
ipv6->registerHook(0, this)), and datagramLocalOutHook runs before
datagramPostRoutingHook. So the rewrite always happens before the checksum hook reads the
tag.
The User Datagram Protocol (UDP) checksum is defined over a pseudo-header containing the
source and destination addresses (RFC 768, and RFC 8200 Section 8.1 for IPv6), so a
checksum built from superseded addresses is wrong by construction.
Why it matters
The damage is not latent. Measured on origin/master 7aef79d5c0, with
examples/ipv6/mipv6, configuration RouteOptimizationTwoCNs, two correspondent nodes
sending User Datagram Protocol (UDP) traffic to the mobile node:
| Route optimization |
udp.checksumMode |
datagrams delivered to the application |
| on |
declared |
28 |
| on |
computed |
20 |
| off |
declared |
28 |
| off |
computed |
28 |
The loss appears only when route optimization and computed checksums are both on. At the
receiver, the User Datagram Protocol module logs the reason:
30.001205 MN[0].udp: Packet UdpBasicAppData-0 received from network, dest port 5000
30.001205 MN[0].udp: Sending payload up to socket sockId=0
32.001087 MN[0].udp: Packet has bit error, discarding
32.002440 MN[0].udp: Packet has bit error, discarding
28 datagrams reach the module and 20 are delivered; the 8 discarded ones start at t = 32 s,
which is when the return-routability procedure completes and the binding is installed. The
receiver rebuilds NetworkProtocolInd from the header it actually received, so its
verification uses the correct addresses and correctly rejects a checksum the sender computed
from the pre-rewrite ones.
Reproduction:
cd examples/ipv6/mipv6
opp_run -m -u Cmdenv -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0 --sim-time-limit=60s \
--*.MN[0].numApps=1 --*.MN[0].app[0].typename='"UdpSink"' --*.MN[0].app[0].localPort=5000 \
--*.CN[*].app[0].typename='"UdpBasicApp"' \
--*.CN[*].app[0].destAddresses='"2001:db8:0:2:8aa:ff:fe00:8"' --*.CN[*].app[0].destPort=5000 \
--*.CN[*].app[0].messageLength=100B --*.CN[*].app[0].sendInterval=2s --*.CN[*].app[0].startTime=30s \
--**.udp.checksumMode='"computed"'
Transmission Control Protocol (TCP) and Stream Control Transmission Protocol (SCTP) traffic
over a route-optimized path is affected the same way, through the other three hooks listed
above.
The fix is to route both rewrites through insertNetworkProtocolHeader(), as the receive-side
paths in the same file already do.
Relationship to #1169
#1169 moves the Internet Control Message Protocol version 6 (ICMPv6) checksum into an
outbound hook that reads the same tag, so it meets this defect as well. The two are
independent: this one is observable on master today with User Datagram Protocol (UDP)
traffic and no new code, and #1169's fix is not required to reproduce it.
Summary
Mipv6::datagramLocalOutHook()rewrites the IPv6 header of an outbound packet andre-inserts it with a plain
insertAtFront(), bypassinginsertNetworkProtocolHeader().The
NetworkProtocolIndtag therefore keeps pointing at the header as it was beforethe rewrite, and every later hook that reads the tag sees the old addresses.
Route optimization has two such paths, both in
src/inet/networklayer/mipv6/Mipv6.cc. The Type 2 Routing Header path(
Mipv6.cc:2019-2039) rewrites the destination:The Home Address Option path (
Mipv6.cc:2048-2070) rewrites the source the same way(
header->setSrcAddress(ro.entry);followed bydatagram->insertAtFront(header);).The same file gets it right on the receive side:
Mipv6.cc:1894andMipv6.cc:1959bothcall
insertNetworkProtocolHeader(), which updates the tag.What the contract says
insertNetworkProtocolHeader()(src/inet/networklayer/common/L3Tools.cc:70-76) existsprecisely to keep the tag and the packet in step:
Four outbound netfilter hooks in the tree depend on that tag being current, all of them to
build a transport pseudo-header from the network-layer addresses:
src/inet/transportlayer/tcp_common/TcpChecksumInsertionHook.cc:32src/inet/transportlayer/sctp/SctpChecksumInsertionHook.cc:32src/inet/transportlayer/sctp/SctpUdpHook.cc:33src/inet/transportlayer/udp/Udp.cc:1398(UdpChecksumInsertionHook::datagramPostRoutingHook())Mipv6registers its hook on the same IPv6 netfilter (Mipv6.cc:128,ipv6->registerHook(0, this)), anddatagramLocalOutHookruns beforedatagramPostRoutingHook. So the rewrite always happens before the checksum hook reads thetag.
The User Datagram Protocol (UDP) checksum is defined over a pseudo-header containing the
source and destination addresses (RFC 768, and RFC 8200 Section 8.1 for IPv6), so a
checksum built from superseded addresses is wrong by construction.
Why it matters
The damage is not latent. Measured on
origin/master7aef79d5c0, withexamples/ipv6/mipv6, configurationRouteOptimizationTwoCNs, two correspondent nodessending User Datagram Protocol (UDP) traffic to the mobile node:
udp.checksumModedeclaredcomputeddeclaredcomputedThe loss appears only when route optimization and computed checksums are both on. At the
receiver, the User Datagram Protocol module logs the reason:
28 datagrams reach the module and 20 are delivered; the 8 discarded ones start at t = 32 s,
which is when the return-routability procedure completes and the binding is installed. The
receiver rebuilds
NetworkProtocolIndfrom the header it actually received, so itsverification uses the correct addresses and correctly rejects a checksum the sender computed
from the pre-rewrite ones.
Reproduction:
Transmission Control Protocol (TCP) and Stream Control Transmission Protocol (SCTP) traffic
over a route-optimized path is affected the same way, through the other three hooks listed
above.
The fix is to route both rewrites through
insertNetworkProtocolHeader(), as the receive-sidepaths in the same file already do.
Relationship to #1169
#1169 moves the Internet Control Message Protocol version 6 (ICMPv6) checksum into an
outbound hook that reads the same tag, so it meets this defect as well. The two are
independent: this one is observable on master today with User Datagram Protocol (UDP)
traffic and no new code, and #1169's fix is not required to reproduce it.