From ad4f6432d34ff53eaded4619819ea3e505050fb8 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Fri, 4 Sep 2026 17:34:18 +0200 Subject: [PATCH 1/3] ipv6: add: find a tunnel interface by its endpoints Ipv6RoutingTable creates and deletes the Ipv6TunnelInterface modules that perform Internet Protocol version 6 (IPv6) in IPv6 encapsulation (Request for Comments (RFC) 2473), but nothing could ask it which of them a given pair of tunnel endpoints belongs to. A node that receives a tunneled datagram needs exactly that. The outer header names the tunnel's exit point as its destination and the entry point as its source, and the local interface configured with those endpoints is the one the inner datagram arrived on: RFC 2473 Section 3 models a tunnel as a link of its own, so the arrival interface of the inner datagram is the tunnel, not the physical interface the outer datagram came in on. Ipv6Tunnel now exposes the endpoints it was configured with, for findTunnelNetworkInterface() only. 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. They all denote the same pair of tunnel endpoints, so for naming the link a datagram arrived on any of them will do, and the first match is returned. The function has no caller yet, so behavior is unchanged: the fingerprint rows of the IPv6 configurations in examples.csv and mipv6-refactoring.csv pass unmodified at this commit. The commit "ipv6: fix: route a decapsulated datagram as arriving from the tunnel" is the first caller, and its module test covers the function. Change: src.networklayer.ipv6.Ipv6RoutingTable | behavior.add | test whatsnew | mipv6-forwarding-path --- WHATSNEW | 6 ++++++ src/inet/networklayer/ipv6/Ipv6RoutingTable.cc | 16 ++++++++++++++++ src/inet/networklayer/ipv6/Ipv6RoutingTable.h | 15 +++++++++++++++ src/inet/networklayer/ipv6tunneling/Ipv6Tunnel.h | 6 ++++++ 4 files changed, 43 insertions(+) diff --git a/WHATSNEW b/WHATSNEW index 03828369a2a..ec40efd33a7 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -347,6 +347,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 -------------------------------------- diff --git a/src/inet/networklayer/ipv6/Ipv6RoutingTable.cc b/src/inet/networklayer/ipv6/Ipv6RoutingTable.cc index 0f1996eb21d..b51d98b7052 100644 --- a/src/inet/networklayer/ipv6/Ipv6RoutingTable.cc +++ b/src/inet/networklayer/ipv6/Ipv6RoutingTable.cc @@ -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 { @@ -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(*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()); diff --git a/src/inet/networklayer/ipv6/Ipv6RoutingTable.h b/src/inet/networklayer/ipv6/Ipv6RoutingTable.h index f6010abe1bb..7b951f3236c 100644 --- a/src/inet/networklayer/ipv6/Ipv6RoutingTable.h +++ b/src/inet/networklayer/ipv6/Ipv6RoutingTable.h @@ -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(). diff --git a/src/inet/networklayer/ipv6tunneling/Ipv6Tunnel.h b/src/inet/networklayer/ipv6tunneling/Ipv6Tunnel.h index 75f19bb28fa..316c51138eb 100644 --- a/src/inet/networklayer/ipv6tunneling/Ipv6Tunnel.h +++ b/src/inet/networklayer/ipv6tunneling/Ipv6Tunnel.h @@ -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; From de9cfbf29571f0f34d17e3d56900bf1ed96173d5 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Fri, 4 Sep 2026 17:35:17 +0200 Subject: [PATCH 2/3] ipv6: fix: route a decapsulated datagram as arriving from the tunnel A home agent that decapsulated a reverse-tunneled datagram and forwarded the inner one back out of the interface the outer datagram had arrived on sent an Internet Control Message Protocol for Internet Protocol version 6 (ICMPv6) Redirect for every one of them, addressed to the mobile node's home address -- a node that is not on that link, and to which the home agent then had to tunnel the Redirect. To reproduce, count the Redirects in the Mobile IPv6 (MIPv6) roaming example: cd examples/ipv6/mipv6roaming inet -s -u Cmdenv -c Roaming -r 0 --cmdenv-express-mode=false \ --cmdenv-log-prefix="%t %C: " | grep "Sending ICMPv6 Redirect" Before this commit (on 49e1fa0945) the home agent sent 2, at t = 23.502 s and t = 23.505 s, one for each reverse-tunneled datagram it forwarded. It sends none now. Request for Comments (RFC) 4861 Section 8.2 lets a router send a Redirect only when, among other conditions, "the Source Address field of the packet identifies a neighbor". routePacket() approximates that by comparing the output interface with the arrival interface, which is sound for a datagram that arrived on a link. A decapsulated datagram did not arrive on one: RFC 2473 Section 3 models the tunnel as a link of its own. localDeliverFinish() passed the physical arrival interface on regardless, so the comparison matched and the Redirect went out. The inner datagram is now routed as arriving on the tunnel interface found from the outer addresses. The arrival interface is the input that was wrong: the comparison in routePacket() is right for a datagram that arrived on a link, and a tunnel is a link of its own. Correcting the input also covers a decapsulated datagram whose inner source is on-link on the physical interface, which a neighbour test on the source address alone would still redirect. The module test MIPv6_no_redirect_for_tunneled_traffic.test checks the arrival interface through the new log line (ip6tun1 at the home agent in this run; the test matches the ip6tun prefix) and the absence of Redirects. Three limits are deliberate. Only the routing decision is re-attributed: the InterfaceInd tag still names the physical interface, because upper layers read it to find the link the datagram came in on -- Mipv6 looks up that interface's Mipv6InterfaceData, which a tunnel interface does not carry -- and for the same reason a pre-routing hook that QUEUEs the datagram loses the re-attribution, since reinjectQueuedDatagram() recovers the interface from that tag. Only unicast is re-attributed: 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, while RFC 4861 Section 8.2 excludes multicast destinations from Redirects. And a node that decapsulates without having a tunnel interface for these endpoints, for instance the far end of a unidirectional tunnel, keeps the physical interface as before. Ipv6.ned defaults sendRedirects to true, so a MIPv6 home agent did this without any configuration whenever it forwarded reverse-tunneled traffic back out of its arrival interface. Fingerprints: the Handover and RouteOptimizationTwoCNs rows of examples/ipv6/mipv6 and the Roaming row of examples/ipv6/mipv6roaming move, in examples.csv (tplx, ~tNl and ~tND) and in mipv6-refactoring.csv (~tNlb), because the Redirects are no longer transmitted: '~' because the set of inter-node packet events loses them; 't' because the events after them shift; 'N' and 'p' because the removed events had their own node and module paths; 'l', 'b' and 'D' because the Redirects' length, contents and bytes leave the hash; and 'x' because it hashes per-event extra data for those same events. No other row of the full suite moves. The graphical tyf values of these rows (t simulation time, y display strings, f canvas figures) were not checked, because every run used -F tyf, and are left as recorded. Change: src.networklayer.ipv6.Ipv6 | behavior.change.fix | fingerprint test whatsnew | mipv6-forwarding-path --- WHATSNEW | 12 ++ src/inet/networklayer/ipv6/Ipv6.cc | 32 ++++ tests/fingerprint/examples.csv | 6 +- tests/fingerprint/mipv6-refactoring.csv | 6 +- ...IPv6_no_redirect_for_tunneled_traffic.test | 138 ++++++++++++++++++ 5 files changed, 188 insertions(+), 6 deletions(-) create mode 100644 tests/module/MIPv6_no_redirect_for_tunneled_traffic.test diff --git a/WHATSNEW b/WHATSNEW index ec40efd33a7..a71f186d05c 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -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. 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. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/src/inet/networklayer/ipv6/Ipv6.cc b/src/inet/networklayer/ipv6/Ipv6.cc index f3f518f1616..2b4c0e8d83c 100644 --- a/src/inet/networklayer/ipv6/Ipv6.cc +++ b/src/inet/networklayer/ipv6/Ipv6.cc @@ -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(); + 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(); auto verdict = datagramPreRoutingHook(packet); if (verdict == INetfilter::IHook::ACCEPT) diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index 9fc76d91942..b47d62a148c 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, 6de1-de47/tplx;cf0f-5aba/~tNl;10e9-9eb9/~tND;44ef-1a45/tyf, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, f2bf-98a6/tplx;e791-2cf4/~tNl;b4c1-7888/~tND;ed3e-17fa/tyf, PASS, wireless EthernetMac +/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 144f-e03b/tplx;0c46-2b33/~tNl;3e46-e152/~tND;afae-2b3c/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..bb1bce01b19 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, c4ad-103d/~tNlb, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, 2c04-f051/~tNlb, PASS, wireless EthernetMac +/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 6449-5bf1/~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_no_redirect_for_tunneled_traffic.test b/tests/module/MIPv6_no_redirect_for_tunneled_traffic.test new file mode 100644 index 00000000000..9e5b82b4248 --- /dev/null +++ b/tests/module/MIPv6_no_redirect_for_tunneled_traffic.test @@ -0,0 +1,138 @@ +%description: +A mobile node away from home reverse-tunnels its traffic through its home agent, which +decapsulates each datagram and forwards the inner one toward the correspondent node. + +This is a regression test for the arrival interface of a decapsulated datagram. RFC 4861 +Section 8.2 lets a router send a Redirect only when, among other conditions, "the Source +Address field of the packet identifies a neighbor". A reverse-tunneled datagram does not +arrive on a link at all -- it arrives through the tunnel, which RFC 2473 Section 3 models +as a link of its own -- and its source, the mobile node's home address, is not a neighbor +on the physical interface the outer datagram came in on. The inner datagram used to keep +that physical interface, so the home agent, forwarding it back out of the same interface, +believed it was redirecting an on-link neighbor and sent a Redirect to a node that was not +on the link. Because Ipv6.ned defaults sendRedirects to true, a home agent did this without +any configuration whenever it forwarded reverse-tunneled traffic back out of its arrival +interface. + +This configuration leaves sendRedirects at its default, so the home agent would send those +Redirects if the defect were reintroduced. The test asserts that it sends none, and -- so +that the assertion cannot pass merely because no datagram was ever reverse-tunneled -- that +the home agent really does decapsulate tunneled traffic during the run. + +The Redirect check also passes when the Redirect is suppressed for another reason, such as +a check that the source is a neighbor. The test therefore also asserts the arrival interface +itself: the home agent routes the decapsulated datagram as arriving on its tunnel interface +(ip6tun...), not on the physical interface. + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +record-vector-results = false +**.mipv6.cmdenv-log-level = trace +**.ipv6.ipv6.cmdenv-log-level = trace +**.ipv6.neighbourDiscovery.cmdenv-log-level = trace +**.app[*].cmdenv-log-level = trace +**.cmdenv-log-level = off +cmdenv-event-banners = false +ned-path = .;../../../../src;../../lib +network = inet.test.moduletest.lib.Mipv6RoamingNetwork +sim-time-limit = 40s +cmdenv-express-mode = false +cmdenv-log-prefix = "%C: " +num-rngs = 3 +seed-set = 1 +**.mobility.rng-0 = 2 + +*.total_mn = 1 +*.total_cn = 1 + +**.neighbourDiscovery.minIntervalBetweenRAs = 0.03s +**.neighbourDiscovery.maxIntervalBetweenRAs = 0.07s +**.neighbourDiscovery.detectL2Movement = false + +*.radioMedium.mediumLimitCache.maxTransmissionPower = 2.0mW +*.radioMedium.mediumLimitCache.minReceptionPower = -82dBm +*.radioMedium.mediumLimitCache.minInterferencePower = -82dBm + +**.wlan*.mgmt.numAuthSteps = 4 +**.AP*.wlan*.mgmt.beaconInterval = 0.1s +**.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 +**.AP_2.wlan*.mgmt.ssid = "AP2" +**.AP_2.wlan*.address = "10:AA:00:00:A2:01" +**.AP_2.eth[0].address = "10:AE:00:00:A2:02" +**.AP_2.eth[0].duplexMode = true + +**.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 = 950m +**.MN[0].mobility.constraintAreaMaxY = 180m +**.MN[0].mobility.startPos = 0 +**.MN[0].mobility.speed = 10mps +**.MN*.mobility.updateInterval = 0.1s + +# CN pings the MN's home address throughout; the replies must follow the MN across both handovers +**.MN*.numApps = 0 +**.CN*.numApps = 1 +**.CN*.app[*].typename = "PingApp" +**.CN[0].app[0].destAddr = "aaaa:0:66:0:8aa:ff:fe00:b" +**.CN*.app[0].packetSize = 56B +**.CN*.app[0].sendInterval = 0.5s +**.CN[0].app[0].startTime = 1s +**.CN*.app[0].hopLimit = 32 +**.CN*.app[0].printPing = true + +**.forwarding = false +**.eth[*].queue.typename = "EthernetQosQueue" +**.eth[*].queue.dataQueue.typename = "DropTailQueue" +**.eth[*].queue.dataQueue.packetCapacity = 10 +**.eth*.duplexMode = true +**.analogModel.ignorePartialInterference = true +**.AP_Home.wlan*.radio.channelNumber = 1 +**.AP_1.wlan*.radio.channelNumber = 2 +**.AP_2.wlan*.radio.channelNumber = 3 +**.MN*.wlan*.radio.channelNumber = 0 +**.wlan*.agent.activeScan = true +**.wlan*.agent.defaultSsid = "" +**.wlan*.agent.channelsToScan = "1 2 3" +**.wlan*.agent.probeDelay = 0.1s +**.wlan*.agent.minChannelTime = 0.15s +**.wlan*.agent.maxChannelTime = 0.3s +**.wlan*.agent.authenticationTimeout = 5s +**.wlan*.agent.associationTimeout = 5s +**.wlan*.bitrate = 2Mbps +**.wlan*.mac.dcf.channelAccess.cwMin = 7 +**.radio.transmitter.power = 2.0mW +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMaxX = 1050m +**.constraintAreaMaxY = 850m + +%#-------------------------------------------------------------------------------------------------------------- +%subst: /omnetpp::// +%#-------------------------------------------------------------------------------------------------------------- +%# the home agent really does decapsulate reverse-tunneled traffic in this run +%contains: stdout +Mipv6RoamingNetwork.Home_Agent.ipv6.ipv6: Tunnelled IP datagram +%#-------------------------------------------------------------------------------------------------------------- +%# the decapsulated datagram is routed as arriving on the tunnel interface +%contains: stdout +Mipv6RoamingNetwork.Home_Agent.ipv6.ipv6: Routing the decapsulated datagram as arriving on ip6tun +%#-------------------------------------------------------------------------------------------------------------- +%# the defect itself: forwarding a decapsulated datagram must not trigger a Redirect +%not-contains: stdout +Mipv6RoamingNetwork.Home_Agent.ipv6.neighbourDiscovery: Sending ICMPv6 Redirect +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: ( +%#-------------------------------------------------------------------------------------------------------------- From 09822c1a183c3bdc53ce4bce72e702d4ff0c13d7 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Fri, 4 Sep 2026 17:36:53 +0200 Subject: [PATCH 3/3] mipv6: fix: hold home-address traffic until the reverse tunnel exists A Mobile Internet Protocol version 6 (Mobile IPv6, MIPv6) mobile node that had just moved to a foreign network discarded the packets it sourced from its home address until its home registration completed, logging "Using HoA instead of CoA... dropping datagram" (HoA is the home address, CoA the care-of address). To reproduce: cd examples/ipv6/mipv6roaming inet -s -u Cmdenv -c Roaming -r 0 --cmdenv-express-mode=false \ --cmdenv-log-prefix="%t %C: " | grep "Using HoA instead of CoA" Before this commit (on 49e1fa0945) three datagrams were lost this way during the first registration: the first Home Test Init and an Internet Control Message Protocol for IPv6 (ICMPv6) echo reply to the correspondent node at t = 22.503 s, and a second echo reply at t = 23.001 s. Losing the Home Test Init also delayed return routability by a retransmission timeout. None is lost now. Request for Comments (RFC) 6275 Section 11.3.1 says: "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 reverse tunnel is created only when the home agent's Binding Acknowledgement arrives, whereas the home agent creates its own tunnel and starts forwarding as soon as it accepts the Binding Update. In that window the mobile node receives tunneled traffic it cannot answer: datagramLocalOutHook() finds no tunnel to pin as the output interface, the datagram is routed onto the foreign link, and Ipv6::resolveMACAddressAndSendPacket() discards it. That guard is right in itself -- a home address is not a topologically valid source on a foreign link -- but traffic that a binding is about to make deliverable should not reach it. The hook now returns QUEUE for such a datagram. When the Binding Acknowledgement creates the reverse tunnel, processBAMessage() calls releaseHeldDatagrams(), which reinjects the held datagrams onto the tunnel. Without the commit "ipv6: fix: route a decapsulated datagram as arriving from the tunnel" this fix makes the Redirect defect worse: the released datagrams reach the home agent through the reverse tunnel, and in mipv6roaming the home agent then sends 3 Redirects instead of 2. The alternative is to create the reverse tunnel when the Binding Update is sent rather than when it is acknowledged, which would send these datagrams at once instead of after a registration round trip. It is rejected because RFC 6275 Section 11.3.1 defines the reverse tunnel's outer source as "the primary care-of address as registered with the home agent": before the acknowledgement nothing is registered, and the datagrams would be tunneled to a home agent that has no binding for them. Holding is bounded from three directions, because the registration alone does not bound it: Mipv6 retransmits a Binding Update that is never acknowledged without a limit (RFC 6275 Section 11.8), and the binding update list entry of such a registration does not expire. A datagram is held only while a Binding Update to that home agent is unacknowledged and only when the interface it is sourced from has a care-of address, the same condition the drop guard tests; at most 100 datagrams are held per home agent, the oldest being dropped beyond that; and the whole set is dropped when the binding update list entry expires or the registration is torn down. A dropped held datagram is reported with a packetDropped signal. The map holds pointers borrowed from the IPv6 module's netfilter queue, which Ipv6::flush() frees on stop and crash without notifying its hooks, so the map is cleared in the same lifecycle operations. A held Home Test Init is not retransmitted. Its retransmission timer runs from the moment Mipv6 hands it to the IPv6 module, so a Binding Acknowledgement that arrives more than INITIAL_BINDACK_TIMEOUT (1 s) later would find a second Home Test Init held beside the first. Both would be released together, and return routability would run twice: two Home Test Init, two Home Test, two Binding Updates to the correspondent node and two acknowledgements. RFC 6275 Section 11.8 calls for a retransmission "if the mobile node fails to receive a valid matching response within the selected initial retransmission interval"; a held message has not been sent, so no response is missing yet. sendTestInit() therefore skips the retransmission while a Home Test Init to that correspondent node is held, and releaseHeldDatagrams() restarts the timer from the initial interval when the held message is sent. The timer keeps running while the message is held rather than stopping, because a held datagram can still be dropped (overflow, binding update list expiry, registration teardown); the next check then sends a fresh Home Test Init. Mipv6::processBUMessage() delays the acknowledgement of a first registration by 1 s (sendTime = existingBinding ? 0 : 1), a stand-in for Duplicate Address Detection (RFC 6275 Section 10.3.1), and the home agent tunnels from acceptance. Both intervals are 1 s, so the timer fires first whenever the Binding Acknowledgement spends longer in transit from the home agent than the tunneled packet that starts return routability. The new module test MIPv6_reverse_tunnel_pending.test requires that a datagram is held and later sent, and that none is discarded with the "Using HoA instead of CoA" message. MIPv6_held_home_test_init_not_retransmitted.test delays the Binding Acknowledgement by 0.4 s on the link between Home_Agent and R_2, on top of that 1 s: the Home Test Init is held at t = 34.004 s, its retransmission timer fires at 35.004 s and the acknowledgement arrives at 35.231 s. The test requires one Home Test Init and one Binding Update at the correspondent node; without the retransmission check it sees two of each. Fingerprints: the Handover row of examples/ipv6/mipv6 and the Roaming row of examples/ipv6/mipv6roaming move, in examples.csv (tplx, ~tNl and ~tND) and in mipv6-refactoring.csv (~tNlb), because datagrams that were discarded are now transmitted through the reverse tunnel, and the recovered Home Test Init advances the return-routability exchange that follows it: '~' because the set of inter-node packet events gains the recovered datagrams; 't' because the exchange after them shifts; 'N' and 'p' because the added events have their own node and module paths; 'l', 'b' and 'D' because the datagrams now carry an outer IPv6 header; and 'x' because it hashes per-event extra data for those same events. RouteOptimizationTwoCNs does not move, and no other row of the full suite moves. The graphical tyf values of these rows (t simulation time, y display strings, f canvas figures) were not checked, because every run used -F tyf, and are left as recorded. Change: src.networklayer.mipv6.Mipv6 | behavior.change.fix | fingerprint test whatsnew | mipv6-forwarding-path --- WHATSNEW | 14 ++ src/inet/networklayer/mipv6/Mipv6.cc | 184 +++++++++++++++++ src/inet/networklayer/mipv6/Mipv6.h | 80 ++++++++ tests/fingerprint/examples.csv | 4 +- tests/fingerprint/mipv6-refactoring.csv | 4 +- ...held_home_test_init_not_retransmitted.test | 191 ++++++++++++++++++ .../module/MIPv6_reverse_tunnel_pending.test | 134 ++++++++++++ 7 files changed, 607 insertions(+), 4 deletions(-) create mode 100644 tests/module/MIPv6_held_home_test_init_not_retransmitted.test create mode 100644 tests/module/MIPv6_reverse_tunnel_pending.test diff --git a/WHATSNEW b/WHATSNEW index a71f186d05c..c566b77986f 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -305,6 +305,20 @@ Notable backward incompatible changes are the following: 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 diff --git a/src/inet/networklayer/mipv6/Mipv6.cc b/src/inet/networklayer/mipv6/Mipv6.cc index c70d39b322b..d04cabb01dc 100644 --- a/src/inet/networklayer/mipv6/Mipv6.cc +++ b/src/inet/networklayer/mipv6/Mipv6.cc @@ -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")); @@ -1124,6 +1125,8 @@ void Mipv6::processBAMessage(Packet *inPacket, const PtrremoveBinding(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()); } @@ -1142,6 +1145,9 @@ void Mipv6::processBAMessage(Packet *inPacket, const PtrcareOfAddress, 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 @@ -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(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(tiIfEntry->testInitMsg)) { // moved the following two lines to here @@ -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(); + if (datagram->findTag() != nullptr) + return false; // an output interface (the reverse tunnel, or another one) is already pinned + + /* 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(); + 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(); + 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 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 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()->getDestAddress()); + networkProtocol->reinjectQueuedDatagram(datagram); + } +} + +bool Mipv6::isHomeTestInit(Packet *datagram) const +{ + const auto& ipv6Header = datagram->peekAtFront(); + if (ipv6Header->getProtocolId() != IP_PROT_IPv6EXT_MOB) + return false; + const auto& mobilityHeader = datagram->peekDataAt(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()->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(); + 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) @@ -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(); @@ -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; } @@ -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; } diff --git a/src/inet/networklayer/mipv6/Mipv6.h b/src/inet/networklayer/mipv6/Mipv6.h index bb871ae9114..ef1c5089ca5 100644 --- a/src/inet/networklayer/mipv6/Mipv6.h +++ b/src/inet/networklayer/mipv6/Mipv6.h @@ -16,6 +16,7 @@ #include #include "inet/common/ModuleRefByPar.h" +#include "inet/common/Simsignals_m.h" #include "inet/common/lifecycle/OperationalBase.h" #include "inet/common/lifecycle/ModuleOperations.h" @@ -71,6 +72,32 @@ class INET_API Mipv6 : public OperationalBase, public IIpv6ExtensionHeaderHandle ModuleRefByPar bc; ModuleRefByPar ipv6nd; + // The IPv6 module this one hooks into (set in initialize()), used to reinject or + // drop the datagrams held by holdUntilReverseTunnelExists(). A plain pointer is + // enough: it is a sibling submodule of the same Ipv6NetworkLayer, so it is created + // before and destroyed after this module, and it is never replaced at run time. + INetfilter *networkProtocol = nullptr; + + // Home-address-sourced datagrams held while the reverse tunnel to the home agent + // does not exist yet, keyed by the home agent whose Binding Acknowledgement they + // are waiting for. + // + // Ownership: the datagrams belong to the IPv6 module's netfilter queue, and these + // are borrowed pointers. The IPv6 module frees that queue in flush() on stop and + // crash without telling the hooks, so this map must be cleared in the same + // lifecycle operations (handleStopOperation/handleCrashOperation) -- a pointer + // left here across a restart would be dereferenced after free by + // releaseHeldDatagrams(). + std::multimap heldDatagrams; + + // Upper bound on the datagrams held for one home agent. A Binding Update that is + // never acknowledged is retransmitted indefinitely (RFC 6275 Section 11.8) and its + // binding update list entry never expires, so the registration alone does not bound + // the hold; without a cap a home agent that stopped answering would make a mobile + // node accumulate every packet it sends for the rest of the run. On overflow the + // oldest held datagram is dropped, with a packetDropped signal. + static const unsigned int MAX_HELD_DATAGRAMS_PER_HOME_AGENT = 100; + // // IP tunnel management (RFC 2473), moved here from the former Ipv6Tunneling // module. A "tunnel" is a dynamically created Ipv6TunnelInterface (built by @@ -488,6 +515,59 @@ class INET_API Mipv6 : public OperationalBase, public IIpv6ExtensionHeaderHandle */ void requestTunnelOutputInterface(Packet *datagram); + /** + * RFC 6275 Section 11.3.1: while away from home and without a binding at the + * correspondent node, a mobile node MUST reverse-tunnel packets that it sources + * from its home address. If such a datagram is offered while the reverse tunnel + * to the home agent does not exist yet -- between sending a Binding Update for a + * new care-of address and receiving the Binding Acknowledgement -- hold it and + * return true, so the local-out hook can QUEUE it instead of letting it be routed + * onto the foreign link (where Ipv6 discards it as topologically incorrect). + * Returns false if the datagram can be sent as it is. + */ + bool holdUntilReverseTunnelExists(Packet *datagram); + + /** + * Reinject the datagrams held by holdUntilReverseTunnelExists() for the given home + * agent, now that the reverse tunnel to it exists. + */ + void releaseHeldDatagrams(const Ipv6Address& homeAgentAddress); + + /** + * Discard the datagrams held by holdUntilReverseTunnelExists() for the given home + * agent, because the registration they were waiting for will not complete. + */ + void dropHeldDatagrams(const Ipv6Address& homeAgentAddress); + + /** + * Detach the datagrams held for the given home agent from heldDatagrams and return + * them, so that a caller can reinject or drop them without the map being mutated + * underneath it by the re-entrant call that reinjection makes. + */ + std::vector takeHeldDatagrams(const Ipv6Address& homeAgentAddress); + + /** + * Discard one held datagram, reporting it with a packetDropped signal first. + */ + void dropHeldDatagram(Packet *datagram, PacketDropReason reason); + + /** + * Returns true if the datagram carries a Home Test Init message. + */ + bool isHomeTestInit(Packet *datagram) const; + + /** + * Returns true if a Home Test Init to the given correspondent node is among the + * datagrams held by holdUntilReverseTunnelExists(), i.e. it has not been sent yet. + */ + bool isHomeTestInitHeld(const Ipv6Address& cnAddress) const; + + /** + * Restart the pending Home Test Init retransmission timer for the given + * correspondent node from the initial retransmission interval, counted from now. + */ + void restartHomeTestInitTimer(const Ipv6Address& cnAddress); + /** * A route-optimization extension-header insertion that the local-out hook * applies to outgoing traffic, replacing a T2RH/HA_OPT pseudo-tunnel. diff --git a/tests/fingerprint/examples.csv b/tests/fingerprint/examples.csv index b47d62a148c..2fce5b54b71 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, 6de1-de47/tplx;cf0f-5aba/~tNl;10e9-9eb9/~tND;44ef-1a45/tyf, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c Handover -r 0, 70s, 21f1-ebd1/tplx;18c3-f600/~tNl;1cd9-872d/~tND;44ef-1a45/tyf, PASS, wireless EthernetMac /examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, f2bf-98a6/tplx;e791-2cf4/~tNl;b4c1-7888/~tND;ed3e-17fa/tyf, PASS, wireless EthernetMac -/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 144f-e03b/tplx;0c46-2b33/~tNl;3e46-e152/~tND;afae-2b3c/tyf, PASS, wireless EthernetMac +/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 5a79-c7e1/tplx;d557-f31d/~tNl;331a-4535/~tND;afae-2b3c/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 bb1bce01b19..04adfb1b5ae 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, c4ad-103d/~tNlb, PASS, wireless EthernetMac +/examples/ipv6/mipv6/, -f omnetpp.ini -c Handover -r 0, 70s, 23c1-5f4a/~tNlb, PASS, wireless EthernetMac /examples/ipv6/mipv6/, -f omnetpp.ini -c RouteOptimizationTwoCNs -r 0, 60s, 2c04-f051/~tNlb, PASS, wireless EthernetMac -/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, 6449-5bf1/~tNlb, PASS, wireless EthernetMac +/examples/ipv6/mipv6roaming/, -f omnetpp.ini -c Roaming -r 0, 70s, bf23-11dd/~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_held_home_test_init_not_retransmitted.test b/tests/module/MIPv6_held_home_test_init_not_retransmitted.test new file mode 100644 index 00000000000..ecb8f453dec --- /dev/null +++ b/tests/module/MIPv6_held_home_test_init_not_retransmitted.test @@ -0,0 +1,191 @@ +%description: +A mobile node moves to a foreign network while the correspondent node pings its home +address. The home agent starts tunneling the pings as soon as it accepts the Binding +Update, and the first tunneled ping makes the mobile node start return routability. Its +Home Test Init is sourced from the home address, so it is held until the Binding +Acknowledgement creates the reverse tunnel. + +The home agent sends the acknowledgement of a first registration 1 s after it accepts the +Binding Update (its stand-in for Duplicate Address Detection), and it starts tunneling at +acceptance. The link between Home_Agent and R_2 delays frames that carry a Binding +Acknowledgement by another 0.4 s, so the acknowledgement arrives more than INITIAL_BINDACK_TIMEOUT (1 s) +after the Home Test Init was held: the Home Test Init is held at t = 34.004 s, its +retransmission timer fires at 35.004 s, and the acknowledgement arrives at 35.231 s. + +RFC 6275 Section 11.8 retransmits a Home Test Init only if no response arrives within the +retransmission interval. A held Home Test Init has not been sent, so the test requires that +the retransmission timer does not send a second one: the correspondent node receives one +Home Test Init and one Binding Update, not two of each. + +%#-------------------------------------------------------------------------------------------------------------- +%file: BindingAcknowledgementDelayChannel.cc +#include "inet/common/INETDefs.h" + +namespace inet { + +class BindingAcknowledgementDelayChannel : public cDatarateChannel +{ + public: + explicit BindingAcknowledgementDelayChannel(const char *name = nullptr) : cDatarateChannel(name) {} + + virtual Result processMessage(cMessage *msg, const SendOptions& options, simtime_t t) override + { + Result result = cDatarateChannel::processMessage(msg, options, t); + if (strstr(msg->getName(), "Binding Acknowledgement") != nullptr) + result.delay += par("extraDelay").doubleValue(); + return result; + } +}; + +Define_Channel(BindingAcknowledgementDelayChannel); + +} // namespace inet + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned +import inet.networklayer.configurator.ipv6.Ipv6FlatNetworkConfigurator; +import inet.node.ethernet.EthernetSwitch; +import inet.node.ipv6.Router6; +import inet.node.wireless.AccessPoint; +import inet.node.mipv6.CorrespondentNode6; +import inet.node.mipv6.HomeAgent6; +import inet.node.mipv6.WirelessHost6; +import inet.physicallayer.wireless.ieee80211.packetlevel.Ieee80211ScalarRadioMedium; +import inet.test.moduletest.lib.ethernetline; + +channel BindingAcknowledgementDelayChannel extends ned.DatarateChannel +{ + @class(::inet::BindingAcknowledgementDelayChannel); + double extraDelay @unit(s) = default(0s); + delay = 0.1us; + datarate = 100Mbps; +} + +network Mipv6DelayedAckNetwork +{ + parameters: + **.mgmt.numChannels = 5; + submodules: + configurator: Ipv6FlatNetworkConfigurator; + radioMedium: Ieee80211ScalarRadioMedium; + Home_Agent: HomeAgent6; + R_1: Router6; + R_2: Router6; + MN: WirelessHost6; + CN: CorrespondentNode6; + AP_Home: AccessPoint { + @display("p=249,172"); + } + AP_1: AccessPoint { + @display("p=566,172"); + } + switch: EthernetSwitch; + connections allowunconnected: + R_1.ethg++ <--> ethernetline <--> R_2.ethg++; + Home_Agent.ethg++ <--> BindingAcknowledgementDelayChannel { extraDelay = 0.4s; } <--> R_2.ethg++; + CN.ethg++ <--> ethernetline <--> switch.ethg++; + switch.ethg++ <--> ethernetline <--> R_2.ethg++; + AP_Home.ethg++ <--> ethernetline <--> Home_Agent.ethg++; + AP_1.ethg++ <--> ethernetline <--> R_1.ethg++; +} + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +record-vector-results = false +**.mipv6.cmdenv-log-level = trace +**.cmdenv-log-level = off +cmdenv-event-banners = false +ned-path = .;../../../../src;../../lib +network = Mipv6DelayedAckNetwork +sim-time-limit = 60s +cmdenv-express-mode = false +cmdenv-log-prefix = "%C: " +num-rngs = 3 +seed-set = 1 +**.mobility.rng-0 = 2 + +**.neighbourDiscovery.minIntervalBetweenRAs = 0.03s +**.neighbourDiscovery.maxIntervalBetweenRAs = 0.07s +**.neighbourDiscovery.detectL2Movement = false + +*.radioMedium.mediumLimitCache.maxTransmissionPower = 2.0mW +*.radioMedium.mediumLimitCache.minReceptionPower = -82dBm +*.radioMedium.mediumLimitCache.minInterferencePower = -82dBm + +**.wlan*.mgmt.numAuthSteps = 4 +**.AP*.wlan*.mgmt.beaconInterval = 0.1s +**.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_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" + +**.mobility.constraintAreaMinZ = 0m +**.mobility.constraintAreaMaxZ = 0m +**.MN.mobility.typename = "RectangleMobility" +**.MN.mobility.constraintAreaMinX = 180m +**.MN.mobility.constraintAreaMinY = 170m +**.MN.mobility.constraintAreaMaxX = 630m +**.MN.mobility.constraintAreaMaxY = 180m +**.MN.mobility.startPos = 0 +**.MN.mobility.speed = 10mps +**.MN.mobility.updateInterval = 0.1s + +**.MN.numApps = 0 +**.CN.numApps = 1 +**.CN.app[0].typename = "PingApp" +**.CN.app[0].destAddr = "aaaa:0:66:0:8aa:ff:fe00:8" +**.CN.app[0].packetSize = 56B +**.CN.app[0].sendInterval = 0.2s +**.CN.app[0].startTime = 1s +**.CN.app[0].hopLimit = 32 + +**.forwarding = false +**.eth*.duplexMode = true +**.analogModel.ignorePartialInterference = true +**.AP_Home.wlan*.radio.channelNumber = 1 +**.AP_1.wlan*.radio.channelNumber = 2 +**.MN.wlan*.radio.channelNumber = 0 +**.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 +**.wlan*.bitrate = 2Mbps +**.wlan*.mac.dcf.channelAccess.cwMin = 7 +**.radio.transmitter.power = 2.0mW +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMaxX = 850m +**.constraintAreaMaxY = 850m + +%#-------------------------------------------------------------------------------------------------------------- +%subst: /omnetpp::// +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep -c "CN.ipv6.mipv6: Message recognised as HOME TEST INIT" test.out > hoti_count.out || true +%postrun-command: grep -c "CN.ipv6.mipv6: Message recognised as BINDING UPDATE" test.out > bu_count.out || true +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%#-------------------------------------------------------------------------------------------------------------- +%# the retransmission timer of the Home Test Init fired while the Home Test Init was held... +%contains: stdout +is still held until the reverse tunnel exists; not retransmitting it +%#-------------------------------------------------------------------------------------------------------------- +%# ...and the held Home Test Init was sent when the reverse tunnel came up +%contains: stdout +is up; sending the held datagram HoTI +%#-------------------------------------------------------------------------------------------------------------- +%# one Home Test Init and one Binding Update reach the correspondent node +%contains-regex: hoti_count.out +^1$ +%#-------------------------------------------------------------------------------------------------------------- +%contains-regex: bu_count.out +^1$ +%#-------------------------------------------------------------------------------------------------------------- +%not-contains: test_undisposed.out +undisposed object: ( +%#-------------------------------------------------------------------------------------------------------------- diff --git a/tests/module/MIPv6_reverse_tunnel_pending.test b/tests/module/MIPv6_reverse_tunnel_pending.test new file mode 100644 index 00000000000..3cdaf1b8a4a --- /dev/null +++ b/tests/module/MIPv6_reverse_tunnel_pending.test @@ -0,0 +1,134 @@ +%description: +A mobile node moves to a foreign network and registers its new care-of address with its +home agent. RFC 6275 Section 11.3.1 requires that, without a binding at the correspondent +node, packets the mobile node sources from its home address be reverse-tunneled through +the home agent -- it licenses no other treatment, and discarding least of all. + +This is a regression test for the window between sending the Binding Update and receiving +the Binding Acknowledgement. The reverse tunnel is created only when the acknowledgement +arrives, so during that window nothing pins it as the output interface, and the datagram +used to be routed onto the foreign link, where Ipv6 discarded it as topologically +incorrect ("Using HoA instead of CoA... dropping datagram"). The correspondent node's +echo replies and the first Home Test Init were lost that way once per handover. + +The test asserts that no such datagram is discarded, and -- so that the assertion cannot +pass merely because the window was never entered -- that the hold-and-release path is +actually exercised: at least one datagram is held for the missing reverse tunnel and sent +once the tunnel comes up. + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +record-vector-results = false +**.mipv6.cmdenv-log-level = trace +**.ipv6.ipv6.cmdenv-log-level = trace +**.ipv6.neighbourDiscovery.cmdenv-log-level = trace +**.app[*].cmdenv-log-level = trace +**.cmdenv-log-level = off +cmdenv-event-banners = false +ned-path = .;../../../../src;../../lib +network = inet.test.moduletest.lib.Mipv6RoamingNetwork +sim-time-limit = 40s +cmdenv-express-mode = false +cmdenv-log-prefix = "%C: " +num-rngs = 3 +seed-set = 1 +**.mobility.rng-0 = 2 + +*.total_mn = 1 +*.total_cn = 1 + +**.neighbourDiscovery.minIntervalBetweenRAs = 0.03s +**.neighbourDiscovery.maxIntervalBetweenRAs = 0.07s +**.neighbourDiscovery.detectL2Movement = false + +*.radioMedium.mediumLimitCache.maxTransmissionPower = 2.0mW +*.radioMedium.mediumLimitCache.minReceptionPower = -82dBm +*.radioMedium.mediumLimitCache.minInterferencePower = -82dBm + +**.wlan*.mgmt.numAuthSteps = 4 +**.AP*.wlan*.mgmt.beaconInterval = 0.1s +**.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 +**.AP_2.wlan*.mgmt.ssid = "AP2" +**.AP_2.wlan*.address = "10:AA:00:00:A2:01" +**.AP_2.eth[0].address = "10:AE:00:00:A2:02" +**.AP_2.eth[0].duplexMode = true + +**.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 = 950m +**.MN[0].mobility.constraintAreaMaxY = 180m +**.MN[0].mobility.startPos = 0 +**.MN[0].mobility.speed = 10mps +**.MN*.mobility.updateInterval = 0.1s + +# CN pings the MN's home address throughout; the replies must follow the MN across both handovers +**.MN*.numApps = 0 +**.CN*.numApps = 1 +**.CN*.app[*].typename = "PingApp" +**.CN[0].app[0].destAddr = "aaaa:0:66:0:8aa:ff:fe00:b" +**.CN*.app[0].packetSize = 56B +**.CN*.app[0].sendInterval = 0.5s +**.CN[0].app[0].startTime = 1s +**.CN*.app[0].hopLimit = 32 +**.CN*.app[0].printPing = true + +**.forwarding = false +**.eth[*].queue.typename = "EthernetQosQueue" +**.eth[*].queue.dataQueue.typename = "DropTailQueue" +**.eth[*].queue.dataQueue.packetCapacity = 10 +**.eth*.duplexMode = true +**.analogModel.ignorePartialInterference = true +**.AP_Home.wlan*.radio.channelNumber = 1 +**.AP_1.wlan*.radio.channelNumber = 2 +**.AP_2.wlan*.radio.channelNumber = 3 +**.MN*.wlan*.radio.channelNumber = 0 +**.wlan*.agent.activeScan = true +**.wlan*.agent.defaultSsid = "" +**.wlan*.agent.channelsToScan = "1 2 3" +**.wlan*.agent.probeDelay = 0.1s +**.wlan*.agent.minChannelTime = 0.15s +**.wlan*.agent.maxChannelTime = 0.3s +**.wlan*.agent.authenticationTimeout = 5s +**.wlan*.agent.associationTimeout = 5s +**.wlan*.bitrate = 2Mbps +**.wlan*.mac.dcf.channelAccess.cwMin = 7 +**.radio.transmitter.power = 2.0mW +**.constraintAreaMinX = 0m +**.constraintAreaMinY = 0m +**.constraintAreaMaxX = 1050m +**.constraintAreaMaxY = 850m + +%#-------------------------------------------------------------------------------------------------------------- +%subst: /omnetpp::// +%#-------------------------------------------------------------------------------------------------------------- +%# the mobile node does register, so the window this test is about is really entered +%contains: stdout +Mipv6RoamingNetwork.MN[0].ipv6.mipv6: Binding was accepted. +%#-------------------------------------------------------------------------------------------------------------- +%# a home-address-sourced datagram was offered before the reverse tunnel existed... +%contains: stdout +yet; holding the datagram from +%#-------------------------------------------------------------------------------------------------------------- +%# ...and was sent, not discarded, once the tunnel came up +%contains: stdout +is up; sending the held datagram +%#-------------------------------------------------------------------------------------------------------------- +%# the defect itself: nothing sourced from the home address may be discarded +%not-contains: stdout +Using HoA instead of CoA... dropping datagram +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: ( +%#--------------------------------------------------------------------------------------------------------------