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
20 changes: 20 additions & 0 deletions WHATSNEW
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,17 @@ 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 Mobile IPv6 home agent intercepts packets on the home link

While a home registration exists, a Mobile IPv6 home agent now answers
Neighbor Solicitations for the mobile node's home address with its own
link-layer address, and multicasts a Neighbor Advertisement for the home
address when it accepts a new binding, as RFC 6275 Section 10.4.1
requires. Before, a host on the home link could not reach a mobile node
that was away. The results of Mobile IPv6 simulations change, including
the fingerprints of the Mobile IPv6 examples, because of the additional
advertisement.

Notable backward compatible changes are the following:

1. IEEE 802.11 per-station rate statistics
Expand Down Expand Up @@ -347,6 +358,15 @@ 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. IPv6 Neighbor Discovery proxy service

Ipv6NeighbourDiscovery can answer Neighbor Solicitations for an address the
node does not hold, as a proxy in the sense of RFC 4861 Section 7.2.4. The
C++ methods addProxyAddress() and removeProxyAddress() start and stop the
service for an address on an interface. While it runs, the node is a member
of the address's solicited-node multicast group and answers solicitations
with its own link-layer address and a cleared Override flag.


INET-4.7 (July 2026) — feature release
--------------------------------------
Expand Down
95 changes: 90 additions & 5 deletions src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1819,8 +1819,11 @@ void Ipv6NeighbourDiscovery::processNsPacket(Packet *packet, const Ipv6Neighbour
// RFC 2461:Section 7.2.3
// If target address is not a valid "unicast" or anycast address assigned to the
// receiving interface, we should silently discard the packet.
// A node also answers for an address it provides proxy service for (RFC 4861,
// Section 7.2.4), which by definition is not assigned to the interface.
if (validateNsPacket(packet, ns) == false
|| ie->getProtocolData<Ipv6InterfaceData>()->hasAddress(nsTargetAddr) == false)
|| (ie->getProtocolData<Ipv6InterfaceData>()->hasAddress(nsTargetAddr) == false
&& !isProxyAddress(ie, nsTargetAddr)))
{
bubble("NS validation failed\n");
delete packet;
Expand Down Expand Up @@ -1994,7 +1997,11 @@ void Ipv6NeighbourDiscovery::sendSolicitedNa(Packet *packet, const Ipv6Neighbour

/*Furthermore, if the node is a router, it MUST set the Router flag to one;
otherwise it MUST set the flag to zero.*/
na->setRouterFlag(rt6->isRouter());
// The flag describes the target, not the sender, so for a proxied address it follows
// what the caller said the proxied node is -- a Mobile IPv6 home agent proxies for a
// mobile host and so clears it (RFC 6275, Section 10.4.1), even though it is a router.
bool proxy = isProxyAddress(ie, ns->getTargetAddress());
na->setRouterFlag(proxy ? isProxyTargetRouter(ie, ns->getTargetAddress()) : rt6->isRouter());

/*If the (NS)Target Address is either an anycast address or a unicast
address for which the node is providing proxy service, or the Target
Expand All @@ -2005,7 +2012,7 @@ void Ipv6NeighbourDiscovery::sendSolicitedNa(Packet *packet, const Ipv6Neighbour
if (auto sla = check_and_cast_nullable<const Ipv6NdSourceLinkLayerAddress *>(ns->getOptions().findOption(IPv6ND_SOURCE_LINK_LAYER_ADDR_OPTION)))
sourceLinkLayerAddress = sla->getLinkLayerAddress();

if (sourceLinkLayerAddress.isUnspecified())
if (proxy || sourceLinkLayerAddress.isUnspecified())
// the Override flag SHOULD be set to zero.
na->setOverrideFlag(false);
else
Expand Down Expand Up @@ -2058,6 +2065,66 @@ void Ipv6NeighbourDiscovery::sendSolicitedNa(Packet *packet, const Ipv6Neighbour
sendPacketToIpv6Module(naPacket, naDestAddr, myIPv6Addr, ie->getInterfaceId());
}

void Ipv6NeighbourDiscovery::addProxyAddress(NetworkInterface *ie, const Ipv6Address& address, bool targetIsRouter)
{
Enter_Method("addProxyAddress");

// Both preconditions come from what proxying needs to do below: hold IPv6 state on
// the interface, and join a multicast group on it.
if (ie->findProtocolData<Ipv6InterfaceData>() == nullptr)
throw cRuntimeError("addProxyAddress(): interface %s has no IPv6 data", ie->getInterfaceName());
if (!ie->isMulticast())
throw cRuntimeError("addProxyAddress(): interface %s is not multicast capable, so solicitations "
"for %s cannot reach this node", ie->getInterfaceName(), address.str().c_str());

if (!proxyAddresses.insert({ { ie->getInterfaceId(), address }, targetIsRouter }).second)
return; // already proxying for it on this interface

EV_INFO << "Providing Neighbour Discovery proxy service for " << address
<< " on " << ie->getInterfaceName() << "\n";

// Solicitations for the address are sent to its solicited-node multicast address.
// A node that does not hold the address is not a member of that group, so Ipv6
// would discard the solicitation before Neighbour Discovery ever saw it.
ie->getProtocolDataForUpdate<Ipv6InterfaceData>()->joinMulticastGroup(address.formSolicitedNodeMulticastAddress());
}

void Ipv6NeighbourDiscovery::removeProxyAddress(const Ipv6Address& address)
{
Enter_Method("removeProxyAddress");

for (auto it = proxyAddresses.begin(); it != proxyAddresses.end(); ) {
if (it->first.second != address) {
++it;
continue;
}

NetworkInterface *ie = ift->getInterfaceById(it->first.first);
EV_INFO << "No longer providing Neighbour Discovery proxy service for " << address
<< " on " << (ie ? ie->getInterfaceName() : "a deleted interface") << "\n";

// Leave the group this entry joined. Gated on the entry having existed, because
// leaving a group that was never joined trips an assertion in Ipv6InterfaceData.
if (ie != nullptr) {
if (auto ipv6Data = ie->findProtocolDataForUpdate<Ipv6InterfaceData>())
ipv6Data->leaveMulticastGroup(address.formSolicitedNodeMulticastAddress());
}

it = proxyAddresses.erase(it);
}
}

bool Ipv6NeighbourDiscovery::isProxyAddress(NetworkInterface *ie, const Ipv6Address& address) const
{
return proxyAddresses.find({ ie->getInterfaceId(), address }) != proxyAddresses.end();
}

bool Ipv6NeighbourDiscovery::isProxyTargetRouter(NetworkInterface *ie, const Ipv6Address& address) const
{
auto it = proxyAddresses.find({ ie->getInterfaceId(), address });
return it != proxyAddresses.end() && it->second;
}

void Ipv6NeighbourDiscovery::sendUnsolicitedNa(NetworkInterface *ie, const Ipv6Address& forAddress)
{
// RFC 2461
Expand All @@ -2073,6 +2140,7 @@ void Ipv6NeighbourDiscovery::sendUnsolicitedNa(NetworkInterface *ie, const Ipv6A
// least RetransTimer seconds.
auto na = makeShared<Ipv6NeighbourAdvertisement>();
Ipv6Address myIPv6Addr = forAddress.isUnspecified() ? ie->getProtocolData<Ipv6InterfaceData>()->getPreferredAddress() : forAddress;
bool proxy = isProxyAddress(ie, myIPv6Addr);

// The Target Address field in the unsolicited advertisement is set to
// an IP address of the interface, and the Target Link-Layer Address
Expand All @@ -2089,7 +2157,8 @@ void Ipv6NeighbourDiscovery::sendUnsolicitedNa(NetworkInterface *ie, const Ipv6A

// If the node is a router, it MUST set the Router flag to one;
// otherwise it MUST set it to zero.
na->setRouterFlag(rt6->isRouter());
// As in sendSolicitedNa(), the flag describes the target, not the sender.
na->setRouterFlag(proxy ? isProxyTargetRouter(ie, myIPv6Addr) : rt6->isRouter());

// The Override flag MAY be set to either zero or one. In either case,
// neighboring nodes will immediately change the state of their Neighbor
Expand Down Expand Up @@ -2125,10 +2194,21 @@ void Ipv6NeighbourDiscovery::sendUnsolicitedNa(NetworkInterface *ie, const Ipv6A
// Neighbor Unreachability Detection algorithm ensures that all nodes
// obtain a reachable link-layer address, though the delay may be
// slightly longer.
// The target address doubles as the source address, which holds as long as the node
// advertises an address of its own. It does not hold when it advertises one it only
// proxies for: RFC 6275, Section 10.4.1 requires that "The Source Address in the IPv6
// header MUST be set to the home agent's IP address on the interface used to send the
// advertisement", and a node cannot legitimately source a packet from an address it
// does not hold in any case.
Ipv6Address dgSrcAddr = proxy ? ie->getProtocolData<Ipv6InterfaceData>()->getPreferredAddress() : myIPv6Addr;

EV_INFO << "Sending unsolicited Neighbour Advertisement for " << myIPv6Addr
<< " on " << ie->getInterfaceName() << (proxy ? " as a proxy" : "") << "\n";

auto packet = new Packet("NeighbourAdvertisement");
Icmpv6::insertChecksum(checksumMode, na, packet);
packet->insertAtFront(na);
sendPacketToIpv6Module(packet, Ipv6Address::ALL_NODES_2, myIPv6Addr, ie->getInterfaceId());
sendPacketToIpv6Module(packet, Ipv6Address::ALL_NODES_2, dgSrcAddr, ie->getInterfaceId());
}

void Ipv6NeighbourDiscovery::sendUnsolicitedRa(NetworkInterface *ie)
Expand Down Expand Up @@ -2706,6 +2786,11 @@ void Ipv6NeighbourDiscovery::stop()

// clear neighbour cache
neighbourCache.clear();

// stop proxying: nothing here survives the node, and a record that did would make it
// answer for -- and defend -- an address on a live link with no binding behind it
while (!proxyAddresses.empty())
removeProxyAddress(proxyAddresses.begin()->first.second);
}
} // namespace inet

43 changes: 43 additions & 0 deletions src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#define __INET_IPV6NEIGHBOURDISCOVERY_H

#include <map>
#include <utility>
#include <vector>

#include "inet/common/ModuleRefByPar.h"
Expand Down Expand Up @@ -100,6 +101,12 @@ class INET_API Ipv6NeighbourDiscovery : public OperationalBase, protected cListe
#endif

Ipv6NeighbourCache neighbourCache;

// Addresses this node answers Neighbor Solicitations for although it does not hold
// them, keyed by (interface id, address); the value says whether the proxied target
// is itself a router. See addProxyAddress().
std::map<std::pair<int, Ipv6Address>, bool> proxyAddresses;

typedef std::vector<cMessage *> RaTimerList;

// stores information about a pending Duplicate Address Detection for
Expand Down Expand Up @@ -377,7 +384,43 @@ class INET_API Ipv6NeighbourDiscovery : public OperationalBase, protected cListe
*/
virtual void sendUnsolicitedRa(NetworkInterface *ie);

/**
* Starts providing Neighbour Discovery proxy service for the given address on the
* given interface. The node joins the address's solicited-node multicast group, so
* that solicitations for an address it does not hold reach it at all, and answers
* them with its own link-layer address (RFC 4861, Section 7.2.4). It also defends
* the address against another node's Duplicate Address Detection.
*
* A Mobile IPv6 home agent uses this to intercept packets addressed to an absent
* mobile node on the home link (RFC 6275, Section 10.4.1). Taking an address over
* from a node whose neighbours still cache its old link-layer address additionally
* requires a multicast Neighbor Advertisement, which the caller sends with
* sendUnsolicitedNa(): a solicited proxy advertisement carries a cleared Override
* flag and so cannot replace a cached entry on its own.
*/
virtual void addProxyAddress(NetworkInterface *ie, const Ipv6Address& address, bool targetIsRouter = false);

/**
* Stops providing Neighbour Discovery proxy service for the given address on every
* interface it is proxied on, and leaves its solicited-node multicast groups. Takes
* the address alone because the interface the address was on-link on may since have
* changed, and the record is what says where the group was actually joined.
* Does nothing if the address is not being proxied.
*/
virtual void removeProxyAddress(const Ipv6Address& address);

protected:
/**
* Returns true if this node provides Neighbour Discovery proxy service for the
* given address on the given interface.
*/
virtual bool isProxyAddress(NetworkInterface *ie, const Ipv6Address& address) const;

/**
* Returns true if the proxied target is itself a router, which decides the Router
* flag of an advertisement sent for it. False for an address that is not proxied.
*/
virtual bool isProxyTargetRouter(NetworkInterface *ie, const Ipv6Address& address) const;

virtual void processNaPacket(Packet *packet, const Ipv6NeighbourAdvertisement *na);
virtual bool validateNaPacket(Packet *packet, const Ipv6NeighbourAdvertisement *na);
Expand Down
11 changes: 8 additions & 3 deletions src/inet/networklayer/ipv6/Ipv6RoutingTable.cc
Original file line number Diff line number Diff line change
Expand Up @@ -992,18 +992,23 @@ void Ipv6RoutingTable::deletePrefixes(int interfaceID)
}
}

bool Ipv6RoutingTable::isOnLinkAddress(const Ipv6Address& address)
NetworkInterface *Ipv6RoutingTable::findOnLinkInterface(const Ipv6Address& address)
{
for (int j = 0; j < ift->getNumInterfaces(); j++) {
NetworkInterface *ie = ift->getInterface(j);

for (int i = 0; i < ie->getProtocolData<Ipv6InterfaceData>()->getNumAdvPrefixes(); i++)
if (address.matches(ie->getProtocolData<Ipv6InterfaceData>()->getAdvPrefix(i).prefix, ie->getProtocolData<Ipv6InterfaceData>()->getAdvPrefix(i).prefixLength))
return true;
return ie;

}

return false;
return nullptr;
}

bool Ipv6RoutingTable::isOnLinkAddress(const Ipv6Address& address)
{
return findOnLinkInterface(address) != nullptr;
}

void Ipv6RoutingTable::deleteInterfaceRoutes(const NetworkInterface *entry)
Expand Down
6 changes: 6 additions & 0 deletions src/inet/networklayer/ipv6/Ipv6RoutingTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,12 @@ class INET_API Ipv6RoutingTable : public SimpleModule, public IRoutingTable, pro
*/
void setMipv6Support(bool value) { mipv6Support = value; }

/**
* Returns the interface on which the provided address is on-link with respect
* to the prefix advertisement list, or nullptr if it is on-link on none.
*/
NetworkInterface *findOnLinkInterface(const Ipv6Address& address);

/**
* Checks whether the provided address is an on-link address
* with respect to the prefix advertisement list.
Expand Down
50 changes: 50 additions & 0 deletions src/inet/networklayer/mipv6/Mipv6.cc
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,11 @@ void Mipv6::processBUMessage(Packet *inPacket, const Ptr<const BindingUpdate>& b
// of course this is also true for CNs
destroyTunnelFromTrigger(HoA);

// ...which on the home link means giving the home address back to the
// mobile node, that being where it has just returned to
if (rt6->isHomeAgent())
stopInterceptingForHomeAddress(HoA);

// A correspondent node inserts the Type 2 Routing Header for this home
// address based on its route-optimization state (see datagramLocalOutHook),
// not on the binding cache, so that state must be dropped here too -- otherwise
Expand Down Expand Up @@ -884,6 +889,13 @@ void Mipv6::processBUMessage(Packet *inPacket, const Ptr<const BindingUpdate>& b
destroyTunnelForEntryAndTrigger(HA, HoA);

createTunnel(NORMAL, HA, CoA, HoA);

// The tunnel only carries what already reached this home agent. Traffic
// from a host on the home link is resolved by Neighbour Discovery there
// and never routed, so intercepting it takes a second step -- and only
// for a home registration, which is what the standard keys the check on.
if (homeRegistration)
startInterceptingForHomeAddress(HoA, existingBinding);
}
else {
// we first destroy the already existing RH2 path if
Expand Down Expand Up @@ -2803,6 +2815,40 @@ void Mipv6::handleBULExpiry(cMessage *msg)
}
}

void Mipv6::startInterceptingForHomeAddress(const Ipv6Address& HoA, bool existingBinding)
{
/*10.4.1
While a node is serving as a home agent for some mobile node, the home agent
uses IPv6 Neighbor Discovery [18] to intercept unicast packets on the home link
addressed to the mobile node. In order to intercept packets in this way, the
home agent MUST act as a proxy for this mobile node and reply to any received
Neighbor Solicitations for it.*/
NetworkInterface *homeLink = rt6->findOnLinkInterface(HoA);

if (homeLink == nullptr) {
// Every home agent advertises the home prefix, so this means the home address
// does not belong to a prefix this node serves.
EV_WARN << "Home address " << HoA << " is on-link on no interface; not intercepting for it\n";
return;
}

ipv6nd->addProxyAddress(homeLink, HoA);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Old home link keeps proxying moved address

When a home prefix moves between interfaces, startInterceptingForHomeAddress adds the new proxy without withdrawing the old one. The home agent keeps answering solicitations on the former home link, where its tunnel still intercepts traffic.

Learn more

Proxy registrations are keyed by interface and address in proxyAddresses. findOnLinkInterface can select a different interface after advertised prefixes are changed. A refreshed Binding Update calls addProxyAddress for the new interface, but the previous registration and multicast membership remain; the home agent answers Neighbor Solicitations on both links.

Example: The home agent initially advertises M's home prefix on eth1 and later moves it to eth2. A subsequent Binding Update adds proxying on eth2 while eth1 still answers solicitations for M's address.

Recommended fix: Before changing the home-link proxy, withdraw the previous interface's proxy registration for that address, or make the proxy update operation atomically move the registration to the selected interface. Retain the no-extra-advertisement behavior only when the existing proxy is already on the selected interface.

Devin Review


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


/*10.4.1
[...] subsequently it MUST multicast onto the home link a Neighbor Advertisement
message [18] on behalf of the mobile node.*/
// Only for a binding that did not exist before. A binding that is merely being
// refreshed, or moved to a new care-of address, changes nothing on the home link:
// its neighbours already resolve the home address to this home agent.
if (!existingBinding)
ipv6nd->sendUnsolicitedNa(homeLink, HoA);
Comment on lines +2843 to +2844

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Home-link takeover advertisement skipped after registration change

When a home registration replaces a non-home binding, existingBinding suppresses the takeover advertisement. The binding cache also stores non-home registrations. Neighbors retain the absent mobile node's link-layer address and cannot reach it until their caches recover.

Learn more

A home agent accepts a Binding Update without the Home Registration flag and stores it through addOrUpdateBC. existingBinding tests only whether that address exists in the cache, not whether its entry already represents a home registration. When a later home registration arrives for the same address, it creates the proxy but skips the advertisement that replaces the mobile node's old link-layer address in neighbors' caches.

Example: A home-link host caches mobile node M's MAC. The home agent first receives a non-home binding for M's address, then M registers that address as its home address while away. The proxy answers new solicitations, but the host continues sending to M's old MAC.

Recommended fix: Capture the previous entry's getHomeRegistration(HoA) before addOrUpdateBC, and announce when the prior entry was not a home registration. Preserve the existing refresh behavior for an already active home registration.

Devin Review


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

Comment on lines +2843 to +2844

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Takeover advertisement precedes home-address verification

For a new binding, startInterceptingForHomeAddress multicasts the override advertisement before the delayed acknowledgement and any address check. Neighbors can replace a resident node's MAC with the home agent's before it establishes that the address is free.

Learn more

A new home binding takes the existingBinding == false path. processBUMessage delays its Binding Acknowledgement by one second for the intended duplicate-address check, while the new proxy registration and unsolicited Neighbor Advertisement happen immediately. The advertisement sets Override, so a home-link neighbor with an existing cache entry can replace its entry before the address has been checked. The duplicate-address check is not implemented yet, but the new takeover action makes this pre-existing gap affect neighbors.

Example: A second node still owns the requested home address on the home link. The home agent announces its own MAC for that address immediately, and a neighbor replaces its existing MAC mapping before the delayed acknowledgement.

Recommended fix: Defer starting proxy service and sending the takeover advertisement until the new binding's home-link duplicate check has completed successfully. Keep the acceptance and failure paths consistent so a failed check does not leave a proxy or tunnel behind.

Devin Review


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

}

void Mipv6::stopInterceptingForHomeAddress(const Ipv6Address& HoA)
{
ipv6nd->removeProxyAddress(HoA);
}

void Mipv6::createBCEntryExpiryTimer(const Ipv6Address& HoA, NetworkInterface *ie, simtime_t scheduledTime)
{
cMessage *bcExpiryMsg = new cMessage("BCEntryExpiry", MK_BC_EXPIRY);
Expand Down Expand Up @@ -2844,6 +2890,10 @@ void Mipv6::handleBCExpiry(cMessage *msg)
destroyTunnelFromTrigger(bcExpIfEntry->HoA);
removeRouteOptimizationForTrigger(bcExpIfEntry->HoA);

// an expired binding is no longer a reason to answer for the home address
if (rt6->isHomeAgent())
stopInterceptingForHomeAddress(bcExpIfEntry->HoA);

// and remove entry from list
cancelTimerIfEntry(bcExpIfEntry->dest, bcExpIfEntry->ifEntry->getInterfaceId(), KEY_BC_EXP);
// deletion of the message already takes place in the cancelTimerIfEntry(.., KEY_BC_EXP);
Expand Down
21 changes: 21 additions & 0 deletions src/inet/networklayer/mipv6/Mipv6.h
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,27 @@ class INET_API Mipv6 : public OperationalBase, public IIpv6ExtensionHeaderHandle
*/
void handleBCExpiry(cMessage *msg);

//
// Helper functions for intercepting packets on the home link (RFC 6275, Section 10.4.1)
//
/**
* Makes this home agent answer Neighbor Solicitations for the mobile node's home
* address on the home link, so that on-link hosts reach the mobile node while it is
* away. For a binding that did not exist before, also multicasts a Neighbor
* Advertisement onto the home link on the mobile node's behalf, which takes the
* address over from neighbours that still cache the mobile node's own link-layer
* address. Does nothing if the home address is on-link on no interface.
*/
void startInterceptingForHomeAddress(const Ipv6Address& HoA, bool existingBinding);

/**
* Stops answering Neighbor Solicitations for the mobile node's home address. Called
* when the binding is de-registered or expires, after which the mobile node defends
* its own address again. Withdrawn by address rather than by interface, so that a
* home link whose advertised prefixes changed meanwhile still leaves nothing behind.
*/
void stopInterceptingForHomeAddress(const Ipv6Address& HoA);

//
// Helper functions for token expiry
//
Expand Down
Loading
Loading