Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions WHATSNEW
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,18 @@ Notable backward incompatible changes are the following:
negotiation. The MacQosWithBlockAck example fingerprints are updated; the
QoS and NonQos showcase fingerprint controls remain unchanged.

17. IPv6 takes a global source when the outgoing interface is link-local only

A datagram an IPv6 node originated into an Ipv6TunnelInterface, with no
source address bound, carried the tunnel interface's link-local address
fe80:: as its source, even to a global destination. A receiver beyond the
tunnel exit could not answer it. For a global unicast destination, IPv6 now
takes the source address from another interface when the outgoing interface
has only a link-local one, as Linux also does. This changes the packets, and
so the fingerprints, of simulations that send traffic with no bound source
to a global destination through an interface that has only a link-local
address, such as a tunnel interface.

Notable backward compatible changes are the following:

1. IEEE 802.11 per-station rate statistics
Expand Down
26 changes: 25 additions & 1 deletion src/inet/networklayer/ipv6/Ipv6.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1006,9 +1006,33 @@ void Ipv6::fragmentPostRouting(Packet *packet, const NetworkInterface *ie, const
!ipv6Header->getDestAddress().isSolicitedNodeMulticastAddress())
{
// source address can be unspecified during DAD
const Ipv6Address& srcAddr = ie->getProtocolData<Ipv6InterfaceData>()->getPreferredAddress();
Ipv6Address srcAddr = ie->getProtocolData<Ipv6InterfaceData>()->getPreferredAddress();
ASSERT(!srcAddr.isUnspecified()); // FIXME what if we don't have an address yet?

// A link-local source must not leave its link (RFC 4291 Section 2.5.6), but an
// outgoing interface with only a link-local address, such as an
// Ipv6TunnelInterface, has no other address. For a global destination, take the
// source from another interface, as Linux also does: RFC 6724 Section 4 only
// recommends the outgoing interface's addresses as the candidates, and its Rule
// 2 prefers a source whose scope is at least the destination's. A tentative
// address is not assigned yet (RFC 4862 Section 5.4), unless Optimistic
// Duplicate Address Detection (RFC 4429) allows its use.
const Ipv6Address& destAddr = ipv6Header->getDestAddress();
if (srcAddr.isLinkLocal() && destAddr.getScope() == Ipv6Address::GLOBAL) {
for (int i = 0; i < ift->getNumInterfaces(); i++) {
const NetworkInterface *candidateIE = ift->getInterface(i);
auto candidateData = candidateIE->findProtocolData<Ipv6InterfaceData>();
if (candidateIE == ie || !candidateIE->isUp() || candidateData == nullptr)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Disconnected interface supplies tunnel source

When an alternative interface loses carrier but remains up, isUp() still accepts its global address. The fallback can choose it before a connected interface, sending replies toward a disconnected link.

Learn more

Interface administrative state and carrier state are separate. NetworkInterface exposes both isUp() and hasCarrier(), and a carrier loss does not clear its addresses. The fallback chooses the first candidate in interface order, so a carrierless interface can displace a connected one with a working global address.

Example: A host has tun0 with fe80::, eth0 with a global address but no carrier, and eth1 with a reachable global address. A packet sent through tun0 chooses eth0's address because eth0 remains administratively up. Replies routed to eth0's disconnected network fail rather than reaching eth1.

Recommended fix: Require both isUp() and hasCarrier() for cross-interface source candidates, consistent with interface availability checks in Ipv6RoutingTable. Add a test with a carrierless first candidate and a connected second candidate.

Suggested change
if (candidateIE == ie || !candidateIE->isUp() || candidateData == nullptr)
if (candidateIE == ie || !candidateIE->isUp() || !candidateIE->hasCarrier() || candidateData == nullptr)

Devin Review


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

continue;
const Ipv6Address& candidate = candidateData->getPreferredAddress();
bool tentative = candidateData->isTentativeAddress(candidate) && !candidateData->isOptimisticDad();
if (candidate.getScope() == Ipv6Address::GLOBAL && !tentative) {
Comment on lines +1027 to +1029

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Tentative preference hides usable global source

When an alternative interface prefers a tentative global address, getPreferredAddress() hides its assigned global addresses. The fallback skips that interface, leaving off-link tunnel packets sourced from fe80:: and replies unroutable.

Learn more

An interface can have several IPv6 addresses. choosePreferredAddress selects a tentative global address before a non-tentative link-local address, but an existing non-tentative global address can also be present with different preference ordering. If getPreferredAddress() returns a tentative global address, the new fallback rejects it without scanning the interface's remaining addresses. When there is no other qualifying interface, the datagram retains the outgoing tunnel's link-local source even though a usable global source exists.

Example: Let eth0 hold an assigned global address with a shorter lifetime and a tentative global address with a longer lifetime, while tun0 holds only fe80::. A source-less UDP datagram sent to a global destination through tun0 rejects eth0's preferred tentative address and leaves with fe80::, rather than eth0's assigned address.

Recommended fix: Search each candidate interface's address list for an assigned, unexpired global unicast address before falling back to its tentative preferred address. Add a tunnel test with both tentative and assigned global addresses on the same candidate interface.

Devin Review


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

srcAddr = candidate;
Comment on lines +1027 to +1030

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Expired global address becomes tunnel source

After a candidate's valid lifetime expires, getPreferredAddress() can still return its cached global address. The fallback sends tunnel packets from that expired address, so peers reply to an invalid source.

Learn more

An IPv6 address assigned with a finite valid lifetime remains cached in preferredAddr until choosePreferredAddress runs again. getPreferredAddress does not check lifetime, and the fallback adds a new path that can use the stale address as the source on a different outgoing interface. When the expired address is no longer valid, a packet sent from it cannot reliably receive a reply.

Example: eth0 has a global address valid until simulation time 10s; tun0 has only fe80::. If no interface address mutation happens between 10s and a tunnel send at 11s, getPreferredAddress() still returns eth0's expired global address and the inner packet uses it.

Recommended fix: Validate candidate address expiry against simTime() when selecting a cross-interface source. Recompute the preferred address, or inspect address records with their valid lifetimes, before accepting one; cover expiry without intervening interface updates in a test.

Devin Review


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

break;
}
}
}

// TODO factor out
ipv6Header = nullptr;
auto newIpv6Header = removeNetworkProtocolHeader<Ipv6Header>(packet);
Expand Down
9 changes: 8 additions & 1 deletion tests/module/IPv6_tunnel.test
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ so packets A->H are encapsulated (outer A->B), forwarded to B, decapsulated ther

We verify that B receives an IPv6-in-IPv6 ("Tunnelled IP datagram") packet,
i.e. the new tunnel interface encapsulated correctly, and that the UDP traffic
reaches H.
reaches H. The application binds no source address, and the tunnel interface
has only its link-local address fe80::, so A must take its global address on
eth0 as the inner source: a link-local source must not leave its link
(RFC 4291 Section 2.5.6).

%#--------------------------------------------------------------------------------------------------------------
%file: test.ned
Expand Down Expand Up @@ -85,6 +88,10 @@ Tunnelled IP datagram
%contains: stdout
received 8 packets
%#--------------------------------------------------------------------------------------------------------------
%# the inner source is A's global address, not the tunnel interface's fe80::
%contains: stdout
2001:db8:1:0:8aa:ff:fe00:1:1025 --> 2001:db8:2:0:8aa:ff:fe00:4:5000
%#--------------------------------------------------------------------------------------------------------------
%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true
%not-contains: test_undisposed.out
undisposed object: (
Expand Down
Loading