From ecfcc47ecaeeccebaa156a92a7ab7f687d62ab82 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 30 Sep 2026 14:30:21 +0200 Subject: [PATCH 1/3] ipv6: refactor: extract findOnLinkInterface() from isOnLinkAddress() isOnLinkAddress() scans the interfaces for one whose prefix advertisement list covers the address, then discards which interface that was. The Mobile IPv6 home agent needs that interface to intercept packets on the home link: it joins a multicast group and sends a Neighbor Advertisement there. Return the interface from findOnLinkInterface() and answer the boolean question from it. The scan itself is unchanged. Change: src.networklayer.ipv6.Ipv6RoutingTable | refactor | - | home-agent-proxy-nd --- src/inet/networklayer/ipv6/Ipv6RoutingTable.cc | 11 ++++++++--- src/inet/networklayer/ipv6/Ipv6RoutingTable.h | 6 ++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/inet/networklayer/ipv6/Ipv6RoutingTable.cc b/src/inet/networklayer/ipv6/Ipv6RoutingTable.cc index 0f1996eb21d..ad0cdb3422d 100644 --- a/src/inet/networklayer/ipv6/Ipv6RoutingTable.cc +++ b/src/inet/networklayer/ipv6/Ipv6RoutingTable.cc @@ -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()->getNumAdvPrefixes(); i++) if (address.matches(ie->getProtocolData()->getAdvPrefix(i).prefix, ie->getProtocolData()->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) diff --git a/src/inet/networklayer/ipv6/Ipv6RoutingTable.h b/src/inet/networklayer/ipv6/Ipv6RoutingTable.h index f6010abe1bb..0444f9595c6 100644 --- a/src/inet/networklayer/ipv6/Ipv6RoutingTable.h +++ b/src/inet/networklayer/ipv6/Ipv6RoutingTable.h @@ -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. From 64e8dccb49b5e4d6929f3f7d859481d9fc1e55c6 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 30 Sep 2026 14:30:21 +0200 Subject: [PATCH 2/3] icmpv6: add: answer Neighbor Solicitations for a proxied address A node that answers Neighbor Solicitations for an address it does not hold -- a proxy, in the sense of RFC 4861 Section 7.2.4 -- had no way to say so: processNsPacket() discards every solicitation whose target is not assigned to the receiving interface. A Mobile IPv6 home agent needs a proxy to intercept packets for an absent mobile node (RFC 6275 Section 10.4.1). For a proxied target, the existing unspecified-source path of processNsPacket() also answers another node's Duplicate Address Detection probe for the address. Solicitations for the address go to its solicited-node multicast address. A node that does not hold the address is not a member of that group, so Ipv6::routeMulticastPacket() would discard them before Neighbor Discovery sees them. addProxyAddress() therefore joins the group, and removeProxyAddress() leaves the group that addProxyAddress() recorded, not one the caller names: leaving a group that was never joined trips an assertion in Ipv6InterfaceData, and the caller may no longer know which interface it named. Two advertisement fields depend on who owns the target: - The Router flag describes the target, not the sender, so for a proxied address it follows what the caller says the proxied node is. A home agent is a router, but the mobile node it proxies for is a host. - The Override flag of a solicited advertisement is cleared for a proxied target, as RFC 4861 Section 7.2.4 says it SHOULD be, so that the owner's own advertisement wins over the proxy's. The code quoted that sentence above a condition that did not implement it. An unsolicited advertisement keeps its set Override flag. sendUnsolicitedNa() uses the target address as the IPv6 source address. For a proxied target that is an address the node does not hold, so it now sources those advertisements from the interface's preferred address, which RFC 6275 Section 10.4.1 requires for the home agent's advertisement. Each unsolicited advertisement is now logged at info level, so that a proxied one can be told apart in the log; nothing logged it before. When a node stops, Ipv6RoutingTable removes only its routes; the interfaces keep their Ipv6InterfaceData and its multicast memberships. So stop() withdraws the proxy records too: one that survived would leave the restarted node answering for an address nothing stands behind. No module calls addProxyAddress() yet, so no simulation changes: the full fingerprint suite (without the graphical tyf ingredient) and the module suite give the same results as before this commit. The first caller, the Mobile IPv6 home agent, is tested with its own change. That module test checks the service end to end: solicitations for the proxied address are answered, and neighbors that cached another link-layer address switch to the proxy's. No test asserts the individual field values (the Router flag, the cleared Override flag of a solicited advertisement, the source address of an unsolicited one), the answer to a Duplicate Address Detection probe, or the withdrawal in stop(). Change: src.networklayer.icmpv6.Ipv6NeighbourDiscovery | behavior.add | test whatsnew | home-agent-proxy-nd --- WHATSNEW | 9 ++ .../icmpv6/Ipv6NeighbourDiscovery.cc | 95 ++++++++++++++++++- .../icmpv6/Ipv6NeighbourDiscovery.h | 43 +++++++++ 3 files changed, 142 insertions(+), 5 deletions(-) diff --git a/WHATSNEW b/WHATSNEW index 03828369a2a..a0bb2125da1 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -347,6 +347,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 -------------------------------------- diff --git a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc index 9785d3f5522..d3e44e78c10 100644 --- a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc +++ b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc @@ -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()->hasAddress(nsTargetAddr) == false) + || (ie->getProtocolData()->hasAddress(nsTargetAddr) == false + && !isProxyAddress(ie, nsTargetAddr))) { bubble("NS validation failed\n"); delete packet; @@ -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 @@ -2005,7 +2012,7 @@ void Ipv6NeighbourDiscovery::sendSolicitedNa(Packet *packet, const Ipv6Neighbour if (auto sla = check_and_cast_nullable(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 @@ -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() == 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()->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()) + 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 @@ -2073,6 +2140,7 @@ void Ipv6NeighbourDiscovery::sendUnsolicitedNa(NetworkInterface *ie, const Ipv6A // least RetransTimer seconds. auto na = makeShared(); Ipv6Address myIPv6Addr = forAddress.isUnspecified() ? ie->getProtocolData()->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 @@ -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 @@ -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()->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) @@ -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 diff --git a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.h b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.h index f122b6e9399..9ff48d6f4af 100644 --- a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.h +++ b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.h @@ -9,6 +9,7 @@ #define __INET_IPV6NEIGHBOURDISCOVERY_H #include +#include #include #include "inet/common/ModuleRefByPar.h" @@ -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, bool> proxyAddresses; + typedef std::vector RaTimerList; // stores information about a pending Duplicate Address Detection for @@ -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); From 34840204052684f847c4a0885e994f38faf4e1a7 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Wed, 30 Sep 2026 14:30:21 +0200 Subject: [PATCH 3/3] mipv6: fix: intercept packets for an absent mobile node on its home link A host on the home link could not reach a mobile node that was away, although the home agent one hop away held a valid binding for it. The host solicited the home address, nobody answered, and its address resolution failed every 3 s. Only off-link traffic reached the mobile node, because it passes through the home agent as the home link's router anyway. No shipped example puts a host on the home link, which hid the defect. RFC 6275 Section 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. The same section requires the home agent, when it begins serving, to multicast a Neighbor Advertisement onto the home link on the mobile node's behalf, with its own link-layer address, the Router flag cleared and the Override flag set. Mipv6 did neither. Register the home address for proxy service when a home registration is accepted, next to the tunnel that carries the traffic already reaching the home agent, and withdraw it when the binding is de-registered or expires. Only a home registration: the standard keys the proxy check on a Binding Cache entry marked as a home registration. The home link is the interface on which the home address is on-link; a home agent that serves no prefix covering the home address logs a warning and does not proxy. The multicast advertisement goes out only for a binding that did not exist before. A refreshed binding, or one moved to a new care-of address, changes nothing on the home link. The advertisement's set Override flag replaces the mobile node's link-layer address in the home link's neighbor caches at once. Reproduce with the new module test against the unfixed source (check out this commit's parent's src/ with the test file in place, and rebuild): cd tests/module && inet_run_module_tests -m release -f MIPv6_home_agent_proxy_nd It puts two hosts on the home link of tests/module/lib/Mipv6Network and counts the ping replies for requests sent while the mobile node is away and registered (from t=36 s or, for the later host, t=40 s, to t=75.5 s). EarlyHost starts before the mobile node leaves and so caches its link-layer address; LateHost starts afterwards with an empty cache. Unfixed: EarlyHost 0 of 80 replies, LateHost 0 of 72, and LateHost reports "Address Resolution has failed". Fixed: 80 of 80 and 72 of 72. The test also asserts the multicast advertisement and the withdrawal when the mobile node comes home. The three Mobile IPv6 example fingerprint rows (Handover, RouteOptimizationTwoCNs, Roaming) move on every packet-level ingredient (tplx, ~tNl and ~tND in examples.csv, ~tNlb in mipv6-refactoring.csv). The cause is the multicast advertisement, one per new binding: with only the sendUnsolicitedNa() call removed, all four ingredients return to their recorded values. The home access point bridges the advertisement onto the wireless medium, so IEEE 802.11 timing moves and everything after it shifts. The graphical tyf values of these rows were already stale on master: a run there gives 04da-4143, 2f8e-79f9 and a8a6-63b4 against the recorded 44ef-1a45, ed3e-17fa and afae-2b3c. With the sendUnsolicitedNa() call removed, tyf also returns to exactly these master values, so this commit moves tyf from them to the new values. tyf is not maintained on master (the suites run with -F tyf), so the stale values get no commit of their own. The Proxy Mobile IPv6 example row does not move, so its stale tyf is left as it is. Not repaired here: RFC 6275 Section 10.3.1 also requires Duplicate Address Detection for the home address before the Binding Acknowledgement (#1194), and Section 10.4.1 requires proxying for the mobile node's link-local address when the Binding Update sets the Link-Layer Address Compatibility flag. Change: src.networklayer.mipv6.Mipv6 | behavior.change.fix | fingerprint test whatsnew | home-agent-proxy-nd --- WHATSNEW | 11 + src/inet/networklayer/mipv6/Mipv6.cc | 50 +++++ src/inet/networklayer/mipv6/Mipv6.h | 21 ++ tests/fingerprint/examples.csv | 6 +- tests/fingerprint/mipv6-refactoring.csv | 6 +- tests/module/MIPv6_home_agent_proxy_nd.test | 220 ++++++++++++++++++++ 6 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 tests/module/MIPv6_home_agent_proxy_nd.test diff --git a/WHATSNEW b/WHATSNEW index a0bb2125da1..74be7224d57 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -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 diff --git a/src/inet/networklayer/mipv6/Mipv6.cc b/src/inet/networklayer/mipv6/Mipv6.cc index c70d39b322b..7a0a43b3796 100644 --- a/src/inet/networklayer/mipv6/Mipv6.cc +++ b/src/inet/networklayer/mipv6/Mipv6.cc @@ -725,6 +725,11 @@ void Mipv6::processBUMessage(Packet *inPacket, const Ptr& 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 @@ -884,6 +889,13 @@ void Mipv6::processBUMessage(Packet *inPacket, const Ptr& 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 @@ -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); + + /*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); +} + +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); @@ -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); diff --git a/src/inet/networklayer/mipv6/Mipv6.h b/src/inet/networklayer/mipv6/Mipv6.h index bb871ae9114..2339ecde9a7 100644 --- a/src/inet/networklayer/mipv6/Mipv6.h +++ b/src/inet/networklayer/mipv6/Mipv6.h @@ -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 // diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 9fc76d91942..ed93005bae4 100644 --- a/tests/fingerprint/examples.csv +++ b/tests/fingerprint/examples.csv @@ -380,9 +380,9 @@ /examples/manetrouting/multiradio/, -f omnetpp.ini -c MultiRadio -r 0, 20s, ec17-5cc2/tplx;55f5-0894/~tNl, PASS, wireless adhoc Ipv4 /examples/manetrouting/multiradio/, -f omnetpp.ini -c SingleRadio -r 0, 20s, 85a0-51b8/tplx;c07a-44e1/~tNl;3aa0-49ed/tyf, PASS, wireless adhoc Ipv4 -/examples/ipv6/mipv6/, -f omnetpp.ini -c Handover -r 0, 70s, dca6-3751/tplx;a44c-17bf/~tNl;96ff-ed7c/~tND;44ef-1a45/tyf, PASS, wireless EthernetMac -/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, 1560-028f/tplx;e6db-28c0/~tNl;805b-8a25/~tND;ed3e-17fa/tyf, PASS, wireless EthernetMac -/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 342e-5e65/tplx;28ce-974e/~tNl;dcec-ae59/~tND;afae-2b3c/tyf, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c Handover -r 0, 70s, e680-ceeb/tplx;4d22-348a/~tNl;2a3d-46d8/~tND;1ccb-c62a/tyf, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, f9da-6a70/tplx;caf3-4533/~tNl;08fd-0ab2/~tND;38b5-3e5d/tyf, PASS, wireless EthernetMac +/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 0b2e-373b/tplx;caff-f124/~tNl;e555-1e1b/~tND;0da4-da9b/tyf, PASS, wireless EthernetMac /examples/ipv6/pmipv6/, -f omnetpp.ini -c General -r 0, 60s, 081b-769c/tplx;1064-9bd4/~tNl;bd46-d85c/~tND;0277-d784/tyf, PASS, wireless EthernetMac /examples/mobility/, -f omnetpp.ini -c AnsimMobility -r 0, 10000s, 72f8-5c0b/tplx;0000-0000/~tNl;0000-0000/~tND;7dd1-18eb/tyf, PASS, diff --git a/tests/fingerprint/mipv6-refactoring.csv b/tests/fingerprint/mipv6-refactoring.csv index 4e8ebad4ab4..d3b07090ebe 100644 --- a/tests/fingerprint/mipv6-refactoring.csv +++ b/tests/fingerprint/mipv6-refactoring.csv @@ -8,9 +8,9 @@ # empty-shim step, which kept these fingerprints bit-for-bit identical. # MIPv6 example — the primary test -/examples/ipv6/mipv6/, -f omnetpp.ini -c Handover -r 0, 70s, 8a43-2e1a/~tNlb, PASS, wireless EthernetMac -/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, 8a20-d2eb/~tNlb, PASS, wireless EthernetMac -/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, bc7d-f706/~tNlb, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c Handover -r 0, 70s, 531a-cbc4/~tNlb, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, e358-2c5e/~tNlb, PASS, wireless EthernetMac +/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 0494-7142/~tNlb, PASS, wireless EthernetMac # IPv6 examples # MLD example — new PASS row; ~tNl locks the MLD Report/Query/Done/MAS-Query packet exchange (traffic+lengths); diff --git a/tests/module/MIPv6_home_agent_proxy_nd.test b/tests/module/MIPv6_home_agent_proxy_nd.test new file mode 100644 index 00000000000..e225d92ed70 --- /dev/null +++ b/tests/module/MIPv6_home_agent_proxy_nd.test @@ -0,0 +1,220 @@ +%description: +Tests that a home agent intercepts packets addressed to an absent mobile node on the +mobile node's home link, as RFC 6275 Section 10.4.1 requires: while a home registration +binding exists, the home agent answers Neighbor Solicitations for the mobile node's home +address with its own link-layer address, and multicasts a Neighbor Advertisement on the +mobile node's behalf when it accepts the binding. + +Two hosts sit on the home link and ping the mobile node's home address. + + EarlyHost starts before the mobile node leaves, so it holds a neighbour cache entry + pointing at the mobile node's own link-layer address, and can only keep reaching it if + that entry is replaced or discarded. + + LateHost starts after the mobile node has left, so it has no cache entry at all and must + resolve the home address from scratch on the home link. Only a proxy reply to its + Neighbor Solicitation can answer it. + +The mobile node leaves the home link at about t=32s, the home agent accepts its Binding +Update at t=34.6s, and the mobile node is back on the home link from about t=76s (it +de-registers at t=78.1s). Replies are counted only for requests sent between t=36s and +t=75.5s, while the mobile node is away and registered, so a reply after its return cannot +satisfy the test. + + EarlyHost reaches the mobile node from t=36s on because the multicast takeover + advertisement carries a set Override flag (RFC 6275 Section 10.4.1 requires it) and + replaces its cached link-layer address with the home agent's. + + LateHost reaches it because the home agent answers its Neighbor Solicitations. + +Without the interception neither host is answered in that window: EarlyHost sends to a +link-layer address nobody answers to, and LateHost loops on address resolution. + +The withdrawal on the mobile node's return is asserted from the log. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned + +import inet.node.ipv6.StandardHost6; +import inet.test.moduletest.lib.Mipv6Network; +import inet.test.moduletest.lib.ethernetline; + +// +// The frozen Mipv6Network with two hosts added on the home link. The home link is the +// segment behind AP_Home, which bridges to the home agent, so the hosts, the home agent +// and the mobile node while it is at home all share one link. +// +network Mipv6ProxyNdNetwork extends Mipv6Network +{ + submodules: + EarlyHost: StandardHost6 { + @display("p=180,300"); + } + LateHost: StandardHost6 { + @display("p=180,380"); + } + connections: + EarlyHost.ethg++ <--> ethernetline <--> AP_Home.ethg++; + LateHost.ethg++ <--> ethernetline <--> AP_Home.ethg++; +} + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +record-vector-results = false +**.mipv6.cmdenv-log-level = trace +**.app[*].cmdenv-log-level = trace +**.Home_Agent.ipv6.neighbourDiscovery.cmdenv-log-level = info +**.*Host.ipv6.neighbourDiscovery.cmdenv-log-level = warn # so a failed address resolution is reported +**.cmdenv-log-level = off +cmdenv-event-banners = false +ned-path = .;../../../../src;../../lib +network = Mipv6ProxyNdNetwork +sim-time-limit = 100s +cmdenv-express-mode = false +cmdenv-log-prefix = "%C: " +num-rngs = 3 +seed-set = 1 +**.mobility.rng-0 = 2 + +# number of MNs and CNs +*.total_mn = 1 +*.total_cn = 1 + +**.neighbourDiscovery.minIntervalBetweenRAs = 0.03s +**.neighbourDiscovery.maxIntervalBetweenRAs = 0.07s +**.neighbourDiscovery.detectL2Movement = false # legacy: detect movement from periodic RAs only + +# channel physical parameters +*.radioMedium.mediumLimitCache.maxTransmissionPower = 2.0mW +*.radioMedium.mediumLimitCache.minReceptionPower = -82dBm +*.radioMedium.mediumLimitCache.minInterferencePower = -82dBm + +# access point +**.wlan*.mgmt.numAuthSteps = 4 + +# ALL APs common parameters +**.AP*.wlan*.mgmt.beaconInterval = 0.1s + +# Access Point parameters +**.AP_Home.wlan*.mgmt.ssid = "HOME" +**.AP_Home.wlan*.address = "10:AA:00:00:00:01" +**.AP_Home.eth[0].address = "10:AE:00:00:00:02" +**.AP_Home.eth[0].duplexMode = true + +**.AP_1.wlan*.mgmt.ssid = "AP1" +**.AP_1.wlan*.address = "10:AA:00:00:A1:01" +**.AP_1.eth[0].address = "10:AE:00:00:A1:02" +**.AP_1.eth[0].duplexMode = true + +# mobility +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxZ = 0m + +**.MN[0].mobility.typename = "RectangleMobility" +**.MN[0].mobility.constraintAreaMinX = 180m +**.MN[0].mobility.constraintAreaMinY = 170m +**.MN[0].mobility.constraintAreaMaxX = 630m +**.MN[0].mobility.constraintAreaMaxY = 180m +**.MN[0].mobility.startPos = 0 +**.MN[0].mobility.speed = 10mps +**.MN*.mobility.updateInterval = 0.1s + + +# ip settings +**.forwarding = false + +# Ethernet NIC configuration +**.eth[*].queue.typename = "EthernetQosQueue" +**.eth[*].queue.dataQueue.typename = "DropTailQueue" +**.eth[*].queue.dataQueue.packetCapacity = 10 +**.eth*.duplexMode = true + +# analog model +**.analogModel.ignorePartialInterference = true + +# wireless channels +**.AP_Home.wlan*.radio.channelNumber = 1 +**.AP_1.wlan*.radio.channelNumber = 2 +**.MN*.wlan*.radio.channelNumber = 0 + +# wireless configuration +**.wlan*.agent.activeScan = true +**.wlan*.agent.defaultSsid = "" +**.wlan*.agent.channelsToScan = "1 2" +**.wlan*.agent.probeDelay = 0.1s +**.wlan*.agent.minChannelTime = 0.15s +**.wlan*.agent.maxChannelTime = 0.3s +**.wlan*.agent.authenticationTimeout = 5s +**.wlan*.agent.associationTimeout = 5s + +# nic settings +**.wlan*.bitrate = 2Mbps +**.wlan*.mac.dcf.channelAccess.cwMin = 7 +**.radio.transmitter.power = 2.0mW + +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMaxX = 850m +**.constraintAreaMaxY = 850m + + +# The home-link hosts ping the mobile node's home address. The address is hardcoded rather +# than resolved with "MN[0](ipv6)", which returns whichever address the mobile node happens +# to hold at resolution time -- its link-local address before autoconfiguration finishes, +# and its care-of address once it has moved away -- never reliably the stable home address. +**.MN*.numApps = 0 +**.CN*.numApps = 0 +*.EarlyHost.numApps = 1 +*.LateHost.numApps = 1 +**.EarlyHost.app[0].typename = "PingApp" +**.LateHost.app[0].typename = "PingApp" +**.*Host.app[0].destAddr = "aaaa:0:66:0:8aa:ff:fe00:8" +**.*Host.app[0].packetSize = 56B +**.*Host.app[0].hopLimit = 32 +**.*Host.app[0].sendInterval = 0.5s +**.*Host.app[0].printPing = true + +# before the mobile node leaves, so the cache entry is populated while it is still at home +**.EarlyHost.app[0].startTime = 1s +# after the home agent has accepted the binding, so address resolution starts from an empty cache +**.LateHost.app[0].startTime = 40s + +%#-------------------------------------------------------------------------------------------------------------- +%subst: /omnetpp::// +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +Mipv6ProxyNdNetwork.Home_Agent.ipv6.mipv6: BU validation passed +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +Mipv6ProxyNdNetwork.Home_Agent.ipv6.neighbourDiscovery: Providing Neighbour Discovery proxy service for aaaa:0:66:0:8aa:ff:fe00:8 +%#-------------------------------------------------------------------------------------------------------------- +%# LateHost starts at t=40s, 5 s after the home agent accepted the binding, so each of its +%# solicitations is answered. (EarlyHost fails once at t=4s, before any binding exists, while +%# the mobile node is still at home; that is not asserted.) +%postrun-command: grep "LateHost.ipv6.neighbourDiscovery: Address Resolution has failed" test.out > test_addrres.out || true +%not-contains: test_addrres.out +Address Resolution has failed +%#-------------------------------------------------------------------------------------------------------------- +%# The multicast takeover advertisement RFC 6275 Section 10.4.1 requires on accepting the +%# binding, sent for an address the home agent does not hold. +%contains: stdout +Mipv6ProxyNdNetwork.Home_Agent.ipv6.neighbourDiscovery: Sending unsolicited Neighbour Advertisement for aaaa:0:66:0:8aa:ff:fe00:8 on eth1 as a proxy +%#-------------------------------------------------------------------------------------------------------------- +%# ...and the withdrawal when the mobile node comes home and de-registers. +%contains: stdout +Mipv6ProxyNdNetwork.Home_Agent.ipv6.neighbourDiscovery: No longer providing Neighbour Discovery proxy service for aaaa:0:66:0:8aa:ff:fe00:8 +%#-------------------------------------------------------------------------------------------------------------- +%# EarlyHost: requests sent from t=36s to t=75.5s are icmp_seq 70 to 149 (80 requests). +%postrun-command: awk '/EarlyHost.app\[0\]: reply of/ && match($0, /icmp_seq=[0-9]+/) { q = substr($0, RSTART+9, RLENGTH-9)+0; if (q >= 70 && q <= 149) n++ } END { print (n >= 70) ? "EARLY-REACHED-WHILE-AWAY" : "EARLY-ONLY-" n+0 }' test.out > test_early.out +%contains: test_early.out +EARLY-REACHED-WHILE-AWAY +%#-------------------------------------------------------------------------------------------------------------- +%# LateHost: requests sent from t=40s to t=75.5s are icmp_seq 0 to 71 (72 requests). +%postrun-command: awk '/LateHost.app\[0\]: reply of/ && match($0, /icmp_seq=[0-9]+/) { q = substr($0, RSTART+9, RLENGTH-9)+0; if (q <= 71) n++ } END { print (n >= 60) ? "LATE-RESOLVED-WHILE-AWAY" : "LATE-ONLY-" n+0 }' test.out > test_late.out +%contains: test_late.out +LATE-RESOLVED-WHILE-AWAY +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: (