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
32 changes: 32 additions & 0 deletions WHATSNEW
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,32 @@ Notable backward incompatible changes are the following:
negotiation. The MacQosWithBlockAck example fingerprints are updated; the
QoS and NonQos showcase fingerprint controls remain unchanged.

17. A home agent routes a decapsulated datagram as arriving from the tunnel

An IPv6 node that decapsulates an IPv6-in-IPv6 datagram now routes the
inner datagram as arriving on the tunnel interface instead of the
physical interface the outer datagram came in on. A Mobile IPv6 home
agent no longer sends an ICMPv6 Redirect for each reverse-tunneled
datagram it forwards back out of the interface it arrived on; before,
Mobile IPv6 scenarios with bidirectional tunneling and the default
sendRedirects = true sent one per such datagram. Upper layers still see
the physical interface in the InterfaceInd tag. Fingerprints of the
Mobile IPv6 examples change.

18. A mobile node holds home-address traffic until its reverse tunnel exists

A Mobile IPv6 mobile node that had just moved to a foreign network
discarded the packets it sent from its home address until the home agent
acknowledged its registration ("Using HoA instead of CoA... dropping
datagram"). It now holds them, at most 100 per home agent, and sends them
through the reverse tunnel once the tunnel exists; held packets are dropped
with a packetDropped signal if the registration does not complete. A held
Home Test Init is not retransmitted while it waits, so a late
acknowledgement does not make return routability run twice. In
examples/ipv6/mipv6roaming this recovers a Home Test Init and two echo
replies in run 0. Fingerprints of the Handover and Roaming configurations
change.

Notable backward compatible changes are the following:

1. IEEE 802.11 per-station rate statistics
Expand Down Expand Up @@ -347,6 +373,12 @@ Notable backward compatible changes are the following:
ResidenceTimeTag and, with it, the FlowTag and the PacketEventTag of the packet.
Those two tags now survive the measurement.

6. Finding an IPv6 tunnel interface by its endpoints

Ipv6RoutingTable::findTunnelNetworkInterface() returns the tunnel interface
configured with a given entry and exit point, and Ipv6Tunnel now exposes
those endpoints with getSource() and getDestination().


INET-4.7 (July 2026) — feature release
--------------------------------------
Expand Down
32 changes: 32 additions & 0 deletions src/inet/networklayer/ipv6/Ipv6.cc
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,38 @@ void Ipv6::localDeliverFinish(Packet *packet)
// datagram is re-processed as if received from the network, so it is routed
// or forwarded -- and seen by the netfilter pre-routing hooks -- normally.
// The L3AddressInd left by decapsulate() carries the tunnel (outer) source.
//
// The inner datagram did not arrive on the physical interface the outer one
// came in on: it arrived through the tunnel, which RFC 2473 Section 3 models
// as a link of its own. Route it as arriving from the tunnel interface, so
// that the RFC 4861 Section 8.2 Redirect condition in routePacket() compares
// against the interface the datagram really came from. For an incoming
// datagram the tunnel's entry point is the outer destination and its exit
// point the outer source, so the endpoints are looked up the other way round.
// A node that decapsulates without having a tunnel interface for these
// endpoints (e.g. the far end of a unidirectional tunnel) keeps the physical
// interface, as before.
//
// Only the routing decision is re-attributed; the InterfaceInd tag keeps
// naming the physical interface, because upper layers read it to find the
// link the datagram came in on -- Mipv6, for instance, looks up that
// interface's Mipv6InterfaceData, which a tunnel interface does not have.
// For the same reason a pre-routing hook that QUEUEs the datagram loses the
// re-attribution: reinjectQueuedDatagram() recovers fromIE from that tag.
//
// Unicast only. A multicast inner datagram keeps the physical interface,
// because routeMulticastPacket() reads the arrival interface's
// Ipv6InterfaceData for its group membership test and matches it against the
// multicast route's incoming interface, and a tunnel interface has neither.
// Redirects are never sent for multicast (RFC 4861 Section 8.2), so nothing
// this commit repairs is lost by leaving that path alone.
const auto& inner = packet->peekAtFront<Ipv6Header>();
if (!inner->getDestAddress().isMulticast()) {
if (auto *tunnelIE = rt->findTunnelNetworkInterface(localAddress, remoteAddress)) {
fromIE = tunnelIE;
EV_INFO << "Routing the decapsulated datagram as arriving on " << fromIE->getInterfaceName() << "\n";
}
}
packet->removeTagIfPresent<InterfaceReq>();
auto verdict = datagramPreRoutingHook(packet);
if (verdict == INetfilter::IHook::ACCEPT)
Expand Down
16 changes: 16 additions & 0 deletions src/inet/networklayer/ipv6/Ipv6RoutingTable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "inet/networklayer/contract/IInterfaceTable.h"
#include "inet/networklayer/ipv6/Ipv6InterfaceData.h"
#include "inet/networklayer/ipv6/Mipv6InterfaceData.h"
#include "inet/networklayer/ipv6tunneling/Ipv6Tunnel.h"

namespace inet {

Expand Down Expand Up @@ -443,6 +444,21 @@ void Ipv6RoutingTable::deleteTunnelNetworkInterface(NetworkInterface *networkInt
ift->deleteInterface(networkInterface);
}

NetworkInterface *Ipv6RoutingTable::findTunnelNetworkInterface(const Ipv6Address& source, const Ipv6Address& destination) const
{
Enter_Method("findTunnelNetworkInterface(%s, %s)=?", source.str().c_str(), destination.str().c_str());

for (int i = 0; i < ift->getNumInterfaces(); i++) {
NetworkInterface *ie = ift->getInterface(i);
for (cModule::SubmoduleIterator it(ie); !it.end(); ++it) {
auto *tunnel = dynamic_cast<Ipv6Tunnel *>(*it);
if (tunnel != nullptr && tunnel->getSource() == source && tunnel->getDestination() == destination)
return ie;
}
}
return nullptr;
}

NetworkInterface *Ipv6RoutingTable::getInterfaceByAddress(const Ipv6Address& addr) const
{
Enter_Method("getInterfaceByAddress(%s)=?", addr.str().c_str());
Expand Down
15 changes: 15 additions & 0 deletions src/inet/networklayer/ipv6/Ipv6RoutingTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,21 @@ class INET_API Ipv6RoutingTable : public SimpleModule, public IRoutingTable, pro
*/
virtual void deleteTunnelNetworkInterface(NetworkInterface *networkInterface);

/**
* Returns the ~Ipv6TunnelInterface configured with the given tunnel entry
* and exit points, or nullptr if this node has no such tunnel. Used to
* identify the tunnel a received IPv6-in-IPv6 datagram arrived through:
* for an incoming datagram, source is its outer destination address and
* destination its outer source address.
*
* Endpoints do not identify a tunnel uniquely -- several tunnels may share
* an entry and exit pair and differ only in what is routed onto them (MIPv6
* split tunnels, for instance). They all represent the same pair of tunnel
* endpoints, so for the purpose of naming the link a datagram arrived on any
* of them will do, and the first match is returned.
*/
virtual NetworkInterface *findTunnelNetworkInterface(const Ipv6Address& source, const Ipv6Address& destination) const;

/**
* Adds a default route for a host. This method requires the RA's source
* address and the router expiry time plus the simTime().
Expand Down
6 changes: 6 additions & 0 deletions src/inet/networklayer/ipv6tunneling/Ipv6Tunnel.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ namespace inet {
*/
class INET_API Ipv6Tunnel : public LayeredProtocolBase
{
public:
/** The tunnel entry point: the outer source address this tunnel encapsulates with. */
const Ipv6Address& getSource() const { return source; }
/** The tunnel exit point: the outer destination address this tunnel encapsulates toward. */
const Ipv6Address& getDestination() const { return destination; }

protected:
opp_component_ptr<NetworkInterface> networkInterface;

Expand Down
184 changes: 184 additions & 0 deletions src/inet/networklayer/mipv6/Mipv6.cc
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ void Mipv6::initialize(int stage)
// the Home Address Option (MN->CN) on route-optimized outgoing traffic
// (replacing the old T2RH/HA_OPT pseudo-tunnels).
ipv6->registerHook(0, this);
networkProtocol = ipv6;

// Register for Protocol::mobileipv6 so the dispatcher delivers mobility messages to us
registerProtocol(Protocol::mobileipv6, gate("toIPv6"), gate("fromIPv6"));
Expand Down Expand Up @@ -1124,6 +1125,8 @@ void Mipv6::processBAMessage(Packet *inPacket, const Ptr<const BindingAcknowledg

// delete the entry from the BUL
bul->removeBinding(baSource);
// this registration is over, so nothing will release datagrams held for it
dropHeldDatagrams(baSource);
// remove all timers related to this BA address
removeTimerEntries(baSource, ifTag->getInterfaceId());
}
Expand All @@ -1142,6 +1145,9 @@ void Mipv6::processBAMessage(Packet *inPacket, const Ptr<const BindingAcknowledg

createTunnel(NORMAL, entry->careOfAddress, entry->destAddress);

// the reverse tunnel now exists: send whatever was held for it
releaseHeldDatagrams(entry->destAddress);

/**11.5.1
After updating its home registration, the mobile
node then updates associated mobility bindings in correspondent nodes
Expand Down Expand Up @@ -1430,6 +1436,20 @@ void Mipv6::sendTestInit(cMessage *msg)
MAY record the same information in multiple Binding Update List
entries.*/

// RFC 6275 Section 11.8 retransmits a Test Init only when no response arrived within
// the retransmission interval. A Home Test Init still held by
// holdUntilReverseTunnelExists() has not been sent, so no response can be missing
// yet: sending another one now would release both at the Binding Acknowledgement and
// run return routability twice. Check again one interval later;
// releaseHeldDatagrams() restarts the timer when the held one is sent.
if (dynamicPtrCast<HomeTestInit>(tiIfEntry->testInitMsg) && isHomeTestInitHeld(tiIfEntry->dest)) {
EV_INFO << "The previous HoTI to " << tiIfEntry->dest << " is still held until the reverse "
<< "tunnel exists; not retransmitting it\n";
tiIfEntry->nextScheduledTime = simTime() + tiIfEntry->ackTimeout;
scheduleAt(tiIfEntry->nextScheduledTime, msg);
return;
}

// retrieve the cookie from the Test Init message
if (auto homeTestInit = dynamicPtrCast<HomeTestInit>(tiIfEntry->testInitMsg)) {
// moved the following two lines to here
Expand Down Expand Up @@ -2076,9 +2096,161 @@ INetfilter::IHook::Result Mipv6::datagramLocalOutHook(Packet *datagram)

// a mobile node's own home-address-sourced traffic goes out the reverse tunnel
requestTunnelOutputInterface(datagram);

if (holdUntilReverseTunnelExists(datagram))
return QUEUE;

return ACCEPT;
}

bool Mipv6::holdUntilReverseTunnelExists(Packet *datagram)
{
if (!rt6->isMobileNode())
return false;

const auto& ipv6Header = datagram->peekAtFront<Ipv6Header>();
if (datagram->findTag<InterfaceReq>() != nullptr)
return false; // an output interface (the reverse tunnel, or another one) is already pinned
Comment on lines +2111 to +2113

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Local packets diverted into reverse tunnel

During registration, holdUntilReverseTunnelExists queues home-address packets even when their destination is local. handleMessageFromHL selects loopback, but release pins the tunnel instead. Local packets leave for the home agent or remain queued without acknowledgement.

Learn more

IPv6 selects a loopback output for a packet addressed to the same node before calling the LOCAL_OUT hook in handleMessageFromHL. The new hold ignores this decision because it examines only the packet's InterfaceReq tag, which is absent when IPv6 chose loopback internally. After an acknowledgement, releaseHeldDatagrams pins a tunnel and reinjects the packet, replacing local delivery. An unacknowledged registration leaves the packet queued despite the local destination.

Example: An away mobile node sends from its home address to another local IPv6 address while its home-agent BU is pending. Instead of reaching a local application, the packet waits for a BA and then enters the tunnel.

Recommended fix: Exclude locally destined datagrams from the hold, using the same local-address decision as Ipv6 before enqueuing. Also consider multicast separately: IPv6's locally originated multicast path does not pass through the unicast HoA drop guard.

Devin Review


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

Comment on lines +2111 to +2113

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Held extension-header packets still drop

When a home-address packet starts with an IPv6 extension header, holdUntilReverseTunnelExists queues it during registration. requestTunnelOutputInterface rejects extension headers both before holding and upon release. The released packet uses the foreign interface and hits IPv6's home-address drop guard.

Learn more

The LOCAL_OUT hook first tries to request a reverse-tunnel interface, then calls the new hold. The tunnel requester refuses IPv6 headers whose protocol ID is an extension header in requestTunnelOutputInterface. Holding still succeeds for these packets when the home-address and registration checks match. releaseHeldDatagrams invokes the same requester, so reinjection has no tunnel pin; resolveMACAddressAndSendPacket discards the packet if it leaves by a foreign interface.

Example: An away node emits a HoA-sourced datagram with a destination-options header while the HA BA is pending. The datagram is held, logged as released after the BA, then discarded instead of entering the tunnel.

Recommended fix: Make reverse-tunnel selection handle supported extension-header chains for both initial transmission and reinjection. Until then, avoid claiming an extension-header datagram is releasable through this hold.

Devin Review


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


/* RFC 6275 Section 11.3.1:
"If a binding exists, the mobile node SHOULD send the packets directly to
the correspondent node. Otherwise, if a binding does not exist, the mobile
node MUST use reverse tunneling."
The standard licenses no third treatment, discarding least of all. The reverse
tunnel is created only when the home agent's Binding Acknowledgement arrives,
so between sending a Binding Update for a new care-of address and receiving
that acknowledgement requestTunnelOutputInterface() finds no tunnel to pin.
The datagram would then be routed onto the foreign link, where
Ipv6::resolveMACAddressAndSendPacket() discards it because a home address is
not a topologically correct source there. Hold it until the tunnel exists. */
for (int i = 0; i < ift->getNumInterfaces(); i++) {
NetworkInterface *ie = ift->getInterface(i);
auto mipv6Data = ie->findProtocolData<Mipv6InterfaceData>();
if (mipv6Data == nullptr || mipv6Data->getMNHomeAddress() != ipv6Header->getSrcAddress())
continue;

// Hold only what the drop guard would actually discard. That guard fires on an
// interface that has a care-of address, i.e. one the node is away from home on;
// a home-address-sourced datagram leaving an interface that has none is
// topologically correct and must be sent, not held.
auto ipv6Data = ie->findProtocolData<Ipv6InterfaceData>();
if (ipv6Data == nullptr || ipv6Data->getGlobalAddress(Ipv6InterfaceData::CoA).isUnspecified())
continue;

// Hold only while a home registration is actually in flight -- otherwise
// nothing would ever release the datagram, and holding it would replace a
// visible drop with a silent leak.
const Ipv6Address& homeAgentAddress = mipv6Data->getHomeAgentAddress();
BindingUpdateList::BindingUpdateListEntry *entry = bul->lookup(homeAgentAddress);
if (entry == nullptr || entry->BAck)
continue;

// A Binding Update that is never acknowledged is retransmitted indefinitely and
// its binding update list entry never expires, so cap what one registration may
// accumulate and discard the oldest beyond it.
if (heldDatagrams.count(homeAgentAddress) >= MAX_HELD_DATAGRAMS_PER_HOME_AGENT) {
auto oldest = heldDatagrams.lower_bound(homeAgentAddress);
EV_WARN << "Already holding " << MAX_HELD_DATAGRAMS_PER_HOME_AGENT << " datagrams for home "
<< "agent " << homeAgentAddress << "; dropping the oldest\n";
dropHeldDatagram(oldest->second, QUEUE_OVERFLOW);
heldDatagrams.erase(oldest);
}

EV_INFO << "No reverse tunnel to home agent " << homeAgentAddress << " yet; holding the "
<< "datagram from " << ipv6Header->getSrcAddress() << " to "
<< ipv6Header->getDestAddress() << " until the registration completes\n";
heldDatagrams.insert({ homeAgentAddress, datagram });
return true;
}
return false;
}

std::vector<Packet *> Mipv6::takeHeldDatagrams(const Ipv6Address& homeAgentAddress)
{
// Detach the datagrams from the map before doing anything with them: reinjecting
// one re-enters the IPv6 module synchronously, and anything it holds in turn would
// otherwise be erased by a range erase performed afterwards.
auto lower = heldDatagrams.lower_bound(homeAgentAddress);
auto upper = heldDatagrams.upper_bound(homeAgentAddress);
std::vector<Packet *> datagrams;
for (auto it = lower; it != upper; ++it)
datagrams.push_back(it->second);
heldDatagrams.erase(lower, upper);
return datagrams;
}

void Mipv6::releaseHeldDatagrams(const Ipv6Address& homeAgentAddress)
{
for (Packet *datagram : takeHeldDatagrams(homeAgentAddress)) {
EV_INFO << "Reverse tunnel to home agent " << homeAgentAddress << " is up; sending the held "
<< "datagram " << datagram->getName() << "\n";
// The local-out hook is not re-run on reinjection (reinjectQueuedDatagram()
// resumes at datagramLocalOut()), so pin the tunnel interface here, exactly
// as the hook would have done had the tunnel existed when the datagram was
// first offered.
requestTunnelOutputInterface(datagram);
// A held Home Test Init is transmitted only now, so its retransmission back-off
// starts now (RFC 6275 Section 11.8), not when it was first offered.
if (isHomeTestInit(datagram))
restartHomeTestInitTimer(datagram->peekAtFront<Ipv6Header>()->getDestAddress());
networkProtocol->reinjectQueuedDatagram(datagram);
}
}

bool Mipv6::isHomeTestInit(Packet *datagram) const
{
const auto& ipv6Header = datagram->peekAtFront<Ipv6Header>();
if (ipv6Header->getProtocolId() != IP_PROT_IPv6EXT_MOB)
return false;
const auto& mobilityHeader = datagram->peekDataAt<MobilityHeader>(ipv6Header->getChunkLength());
return mobilityHeader->getMobilityHeaderType() == HOME_TEST_INIT;
}

bool Mipv6::isHomeTestInitHeld(const Ipv6Address& cnAddress) const
{
for (const auto& elem : heldDatagrams)
if (isHomeTestInit(elem.second) && elem.second->peekAtFront<Ipv6Header>()->getDestAddress() == cnAddress)
return true;
return false;
}

void Mipv6::restartHomeTestInitTimer(const Ipv6Address& cnAddress)
{
for (auto& elem : transmitIfList) {
if (elem.first.type != KEY_HI || elem.first.dest != cnAddress)
continue;
TimerIfEntry *entry = elem.second;
if (entry->timer != nullptr && entry->timer->isScheduled()) {
// the transmission happening now is the first one, so start the back-off
// from the initial interval, as sendTestInit() does after a first send
auto mipv6Data = entry->ifEntry->getProtocolData<Mipv6InterfaceData>();
entry->nextScheduledTime = simTime() + mipv6Data->_getInitialBindAckTimeout();
entry->ackTimeout = std::min(2 * mipv6Data->_getInitialBindAckTimeout(), mipv6Data->_getMaxBindAckTimeout());
rescheduleAt(entry->nextScheduledTime, entry->timer);
}
}
}

void Mipv6::dropHeldDatagrams(const Ipv6Address& homeAgentAddress)
{
for (Packet *datagram : takeHeldDatagrams(homeAgentAddress)) {
EV_WARN << "Registration with home agent " << homeAgentAddress << " will not complete; "
<< "dropping the held datagram " << datagram->getName() << "\n";
dropHeldDatagram(datagram, OTHER_PACKET_DROP);
}
}

void Mipv6::dropHeldDatagram(Packet *datagram, PacketDropReason reason)
{
// The path this replaces counted the loss (Ipv6::numDropped); report it the modern
// way instead, so a held datagram that is discarded is as visible to a statistic or
// a visualizer as one the IPv6 layer drops itself.
PacketDropDetails details;
details.setReason(reason);
emit(packetDroppedSignal, datagram, &details);
networkProtocol->dropQueuedDatagram(datagram);
}

void Mipv6::requestTunnelOutputInterface(Packet *datagram)
{
// Steer home-address-destined traffic (at the home agent, on the forwarding path)
Expand Down Expand Up @@ -2788,6 +2960,8 @@ void Mipv6::handleBULExpiry(cMessage *msg)
entry->state = BindingUpdateList::NONE;
// remove binding
bul->removeBinding(bulExpIfEntry->dest);
// the registration will not complete, so nothing will release datagrams held for it
dropHeldDatagrams(bulExpIfEntry->dest);

// remove all timers
int interfaceID = bulExpIfEntry->ifEntry->getInterfaceId();
Expand Down Expand Up @@ -2917,6 +3091,11 @@ void Mipv6::handleStopOperation(LifecycleOperation *operation)
interfaceCoAList.clear();
cnList.clear();

// Ipv6::flush() has freed (or is about to free) every datagram in its netfilter
// queue without telling the hooks, so only forget the borrowed pointers here --
// dropping them through the IPv6 module would touch memory it already owns.
heldDatagrams.clear();

tunnels.clear();
noOfNonSplitTunnels = 0;
}
Expand All @@ -2933,6 +3112,11 @@ void Mipv6::handleCrashOperation(LifecycleOperation *operation)
interfaceCoAList.clear();
cnList.clear();

// Ipv6::flush() has freed (or is about to free) every datagram in its netfilter
// queue without telling the hooks, so only forget the borrowed pointers here --
// dropping them through the IPv6 module would touch memory it already owns.
heldDatagrams.clear();

tunnels.clear();
noOfNonSplitTunnels = 0;
}
Expand Down
Loading
Loading