From 95a98c49ae6713f7dd6c44ee2addb0953e77245b Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Mon, 28 Sep 2026 23:24:56 +0200 Subject: [PATCH 1/3] ipv6: fix: send a Redirect only when the source is a neighbour An Internet Protocol version 6 (IPv6) router sent an Internet Control Message Protocol for IPv6 (ICMPv6) Redirect from Ipv6::routePacket() for every datagram it forwarded back out of the interface the datagram had arrived on. Request for Comments (RFC) 4861 Section 8.2 also requires that "the Source Address field of the packet identifies a neighbor", and that the better first hop "resides on the same link as the sending node". The interface comparison also holds for a source two hops away, and then the Redirect names a next hop the source cannot reach. The Redirect is now sent only when the source is a link-local address or a longest prefix match on it gives an on-link route out of the arrival interface. This is how next-hop determination (RFC 4861 Section 5.2) decides on-link. To reproduce: hostA -- r0 -- [switch: r0, r1, r2] -- r2 -- hostB, where r0 routes hostB's prefix via r1, and r1 routes it via r2's link-local address. hostA pings hostB for 10 s at 20 echo requests per second. On master, r1 sends hostA 200 Redirects, one per echo request, although hostA is not on r1's link. r2 forwards each of them back out of the same interface and sends r1 a Redirect for it: 200 more. With this change neither router sends them, and 8 Redirects are left in the run instead of 408. The new module test IPv6_redirect_source_not_neighbour.test is this topology. The same change stops the home agent Redirects of issue #1197. The source of a decapsulated, reverse-tunneled datagram is the mobile node's home address, whose prefix is on-link on the home link, not on the interface the tunneled datagram arrived on. On master the home agent sends 2 such Redirects in examples/ipv6/mipv6roaming -c Roaming, 43 in examples/ipv6/mipv6 -c Handover and 4 in -c RouteOptimizationTwoCNs. It sends none now. Baselines: the whole fingerprint suite (1774 rows, run with -F tyf, which leaves out the graphical ingredients) was run before and after. Three configurations move, in both examples.csv and mipv6-refactoring.csv: the three Mobile IPv6 (MIPv6) configurations above. Their ingredients move because the Redirects leave the wire: '~' (the inter-node packet events), 't' (the events after them shift), 'N' and 'p' (the paths of the removed events), 'l', 'b' and 'D' (their length and bytes) and 'x' (their per-event data). The graphical 'tyf' values are left as recorded: run separately, all three already fail on master (Handover, recorded 44ef-1a45, gives 04da-4143 on master), so this commit's effect on them cannot be told from the stale baseline. Change: src.networklayer.ipv6 | behavior.change.fix | fingerprint test whatsnew --- WHATSNEW | 16 ++++ src/inet/networklayer/ipv6/Ipv6.cc | 10 +- tests/fingerprint/examples.csv | 6 +- tests/fingerprint/mipv6-refactoring.csv | 6 +- .../IPv6_redirect_source_not_neighbour.test | 92 +++++++++++++++++++ 5 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 tests/module/IPv6_redirect_source_not_neighbour.test diff --git a/WHATSNEW b/WHATSNEW index 03828369a2a..20c496f5bfe 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -293,6 +293,22 @@ Notable backward incompatible changes are the following: negotiation. The MacQosWithBlockAck example fingerprints are updated; the QoS and NonQos showcase fingerprint controls remain unchanged. +17. An IPv6 router sends a Redirect only to a neighbour + + Ipv6::routePacket sent an Internet Control Message Protocol for IPv6 + (ICMPv6) Redirect for every datagram it forwarded back out of the + interface it had arrived on, whatever its source. RFC 4861 + section 8.2 also requires that the source of the datagram is a neighbour, so + that the better first hop is on the sender's link. A router now sends the + Redirect only when the source is a link-local address or matches an on-link + prefix of the arrival interface. A home agent that forwards a decapsulated, + reverse-tunneled datagram from a mobile node away from home is one of the + routers that no longer sends one. + + Simulations where a router forwards datagrams from a distant source back out + of their arrival interface send fewer Redirects, so their fingerprints may + 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..2c5e72e1682 100644 --- a/src/inet/networklayer/ipv6/Ipv6.cc +++ b/src/inet/networklayer/ipv6/Ipv6.cc @@ -531,7 +531,15 @@ void Ipv6::routePacket(Packet *packet, const NetworkInterface *destIE, const Net // next hop is not directly reachable by the source and redirects cause a storm. if (sendRedirects && rt->isRouter() && !fromHL && fromIE && interfaceId == fromIE->getInterfaceId()) { Ipv6Address pktSrcAddr = ipv6Header->getSrcAddress(); - if (nextHop != pktSrcAddr && !pktSrcAddr.isUnspecified() && !pktSrcAddr.isMulticast()) { + // The same section requires that "the Source Address field of the packet + // identifies a neighbor": a source reached through another router on this link + // cannot use a better first hop on it. On-link is decided as next-hop + // determination decides it (RFC 4861 Section 5.2): a link-local source, or a + // longest prefix match that gives an on-link route out of the arrival interface. + const Ipv6Route *srcRoute = pktSrcAddr.isLinkLocal() ? nullptr : rt->doLongestPrefixMatch(pktSrcAddr); + bool srcIsNeighbour = pktSrcAddr.isLinkLocal() || + (srcRoute && srcRoute->getNextHop().isUnspecified() && srcRoute->getInterface() == fromIE); + if (nextHop != pktSrcAddr && !pktSrcAddr.isUnspecified() && !pktSrcAddr.isMulticast() && srcIsNeighbour) { NetworkInterface *outIe = ift->getInterfaceById(interfaceId); nd->sendRedirect(packet, nextHop, destAddress, outIe); } 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/IPv6_redirect_source_not_neighbour.test b/tests/module/IPv6_redirect_source_not_neighbour.test new file mode 100644 index 00000000000..0574b38653f --- /dev/null +++ b/tests/module/IPv6_redirect_source_not_neighbour.test @@ -0,0 +1,92 @@ +%description: +Tests that a router sends no ICMPv6 Redirect to a source that is not a +neighbour (RFC 4861 Section 8.2: "the Source Address field of the packet +identifies a neighbor"). + +Topology: hostA -- r0 -- [switch: r0, r1, r2] -- r2 -- hostB. +hostA pings hostB. r0 forwards the echo requests to r1, whose route sends +them back out the same interface to r2. The source of each forwarded echo +request, hostA, is two hops away from r1, so r1 must not send it a Redirect. +r2 still forwards the echo requests, and hostB answers them. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned +import inet.networklayer.configurator.ipv6.Ipv6NetworkConfigurator; +import inet.node.ethernet.Eth100M; +import inet.node.ethernet.EthernetSwitch; +import inet.node.inet.Router; +import inet.node.inet.StandardHost; + +network RedirectSourceNotNeighbour +{ + submodules: + configurator: Ipv6NetworkConfigurator; + hostA: StandardHost; + r0: Router; + sw: EthernetSwitch; + r1: Router; + r2: Router; + hostB: StandardHost; + connections: + hostA.ethg++ <--> Eth100M <--> r0.ethg++; + r0.ethg++ <--> Eth100M <--> sw.ethg++; + r1.ethg++ <--> Eth100M <--> sw.ethg++; + r2.ethg++ <--> Eth100M <--> sw.ethg++; + r2.ethg++ <--> Eth100M <--> hostB.ethg++; +} +%#-------------------------------------------------------------------------------------------------------------- +%file: redirect.xml + + + + + + + + + + + + + + + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +record-vector-results = false +ned-path = ../../../../src;. +network = RedirectSourceNotNeighbour +sim-time-limit = 3s +cmdenv-express-mode = false +cmdenv-log-prefix = "%C: " + +**.hasIpv4 = false +**.hasIpv6 = true +**.ipv6.configurator.networkConfiguratorModule = "configurator" +**.ipv6.neighbourDiscovery.dupAddrDetectTransmits = 0 +*.configurator.addStaticRoutes = true +*.configurator.addRemoteRoutes = false +*.configurator.addDefaultRoutes = false +*.configurator.config = xmldoc("redirect.xml") + +*.hostA.numApps = 1 +*.hostA.app[0].typename = "PingApp" +*.hostA.app[0].destAddr = "2001:db8:4::2" +*.hostA.app[0].startTime = 2s +*.hostA.app[0].stopTime = 2.5s +*.hostA.app[0].sendInterval = 50ms + +%#-------------------------------------------------------------------------------------------------------------- +%subst: /omnetpp::// +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +RedirectSourceNotNeighbour.hostB.ipv6.ipv6: ICMPv6 packet: passing it to ICMPv6 module +%#-------------------------------------------------------------------------------------------------------------- +%not-contains: stdout +Sending ICMPv6 Redirect to 2001:db8:1::1 +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: ( +%#-------------------------------------------------------------------------------------------------------------- From f8ab8525999910890d53d3961c602fd8d91ca214 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Mon, 28 Sep 2026 23:24:56 +0200 Subject: [PATCH 2/3] icmpv6: fix: send no Redirect whose target is a global router address Ipv6NeighbourDiscovery::sendRedirect() copied the next hop of the route unchanged into the Target Address of the Redirect, a message of the Internet Control Message Protocol for Internet Protocol version 6 (ICMPv6). Request for Comments (RFC) 4861 Sections 4.5 and 8.2 allow two values there: the Destination Address, when the target is the destination itself, or "that router's link-local address", when the target is a router. A route whose gateway is configured as a global address, as Ipv6NetworkConfigurator writes it from , produced a Redirect with a global Target Address. processRedirectPacket() discards such a Redirect, as RFC 4861 Section 8.1 requires, so no receiver could act on one. The router now sends no Redirect whose target is neither the destination nor a link-local address. A global next hop does not give the router's link-local address, and Section 8.2 makes sending a Redirect a SHOULD, so declining conforms and a global Target Address does not. To reproduce: hostA, r1 and r2 on one switch, hostB behind r2. hostA routes hostB's prefix via r1, and r1 routes it via r2 named as 2001:db8:2::3. hostA pings hostB for 10 s at 20 echo requests per second. On master r1 sends hostA 200 Redirects with Target Address 2001:db8:2::3, and hostA discards all of them: "Redirect target is neither link-local nor equal to destination, discarding". With this change r1 sends none of them. When both routes name the next router by its link-local address instead, r1 sends one Redirect, and hostA updates its destination cache and sends straight to r2, before and after this change. The new module test IPv6_redirect_target_not_link_local.test is the configuration with global gateways. Baselines: the whole fingerprint suite (1774 rows, run with -F tyf, which leaves out the graphical ingredients) moves no row with this commit; the graphical 'tyf' values were not checked. Change: src.networklayer.icmpv6 | behavior.change.fix | test whatsnew --- WHATSNEW | 13 +++ .../icmpv6/Ipv6NeighbourDiscovery.cc | 11 +++ .../IPv6_redirect_target_not_link_local.test | 90 +++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 tests/module/IPv6_redirect_target_not_link_local.test diff --git a/WHATSNEW b/WHATSNEW index 20c496f5bfe..b65dda99e2a 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -309,6 +309,19 @@ Notable backward incompatible changes are the following: of their arrival interface send fewer Redirects, so their fingerprints may change. +18. An IPv6 Redirect names a router by its link-local address + + Ipv6NeighbourDiscovery::sendRedirect copied the next hop of the route into + the Target Address field as it was, so a route whose gateway was configured + as a global address produced a Redirect with a global Target Address. RFC + 4861 sections 4.5 and 8.2 require the router's link-local address there, and + the receiving side of INET discarded every such Redirect. A router now sends + no Redirect in that case, and the Target Address is either the destination + itself or a link-local address. + + Simulations with configured routes through a global gateway on a shared + link send fewer Redirects, so their fingerprints may change. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc index 9785d3f5522..b4c55b8382e 100644 --- a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc +++ b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc @@ -2386,6 +2386,17 @@ void Ipv6NeighbourDiscovery::sendRedirect(Packet *redirectedPacket, const Ipv6Ad Ipv6Address pktSrcAddr = ipv6Header->getSrcAddress(); Ipv6Address srcAddr = ie->getProtocolData()->getLinkLocalAddress(); + // RFC 4861 Section 8.2: "If the target is a router, that router's link-local + // address MUST be used." A target that is neither the destination itself nor a + // link-local address is a router known only by another address, typically the + // gateway of a configured route; its link-local address is not known here, so + // no Redirect is sent (sending one is a SHOULD in the same section). + if (targetAddr != destAddr && !targetAddr.isLinkLocal()) { + EV_INFO << "Not sending ICMPv6 Redirect to " << pktSrcAddr << ": the target " << targetAddr + << " is neither the destination " << destAddr << " nor a link-local address\n"; + return; + } + EV_INFO << "Sending ICMPv6 Redirect to " << pktSrcAddr << ": use " << targetAddr << " as next hop for " << destAddr << "\n"; diff --git a/tests/module/IPv6_redirect_target_not_link_local.test b/tests/module/IPv6_redirect_target_not_link_local.test new file mode 100644 index 00000000000..4bba2f0e552 --- /dev/null +++ b/tests/module/IPv6_redirect_target_not_link_local.test @@ -0,0 +1,90 @@ +%description: +Tests that a router does not put a global address in the Target Address of an +ICMPv6 Redirect (RFC 4861 Section 4.5 and 8.2: when the target is a router, +"that router's link-local address MUST be used"). + +Topology: hostA, r1 and r2 on one switch; hostB behind r2. hostA's route to +hostB goes via r1, and r1's route goes via r2, named by its global address +2001:db8:2::3, back out the interface the echo requests arrived on. The +link-local address of r2 is not known to r1, so r1 sends no Redirect and +keeps forwarding. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned +import inet.networklayer.configurator.ipv6.Ipv6NetworkConfigurator; +import inet.node.ethernet.Eth100M; +import inet.node.ethernet.EthernetSwitch; +import inet.node.inet.Router; +import inet.node.inet.StandardHost; + +network RedirectTargetNotLinkLocal +{ + submodules: + configurator: Ipv6NetworkConfigurator; + hostA: StandardHost; + sw: EthernetSwitch; + r1: Router; + r2: Router; + hostB: StandardHost; + connections: + hostA.ethg++ <--> Eth100M <--> sw.ethg++; + r1.ethg++ <--> Eth100M <--> sw.ethg++; + r2.ethg++ <--> Eth100M <--> sw.ethg++; + r2.ethg++ <--> Eth100M <--> hostB.ethg++; +} +%#-------------------------------------------------------------------------------------------------------------- +%file: redirect.xml + + + + + + + + + + + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +record-vector-results = false +ned-path = ../../../../src;. +network = RedirectTargetNotLinkLocal +sim-time-limit = 3s +cmdenv-express-mode = false +cmdenv-log-prefix = "%C: " + +**.hasIpv4 = false +**.hasIpv6 = true +**.ipv6.configurator.networkConfiguratorModule = "configurator" +**.ipv6.neighbourDiscovery.dupAddrDetectTransmits = 0 +*.configurator.addStaticRoutes = true +*.configurator.addRemoteRoutes = false +*.configurator.addDefaultRoutes = false +*.configurator.config = xmldoc("redirect.xml") +*.r1.ipv6.routingTable.routes = xml("") +*.r2.ipv6.routingTable.routes = xml("") + +*.hostA.numApps = 1 +*.hostA.app[0].typename = "PingApp" +*.hostA.app[0].destAddr = "2001:db8:4::2" +*.hostA.app[0].startTime = 2s +*.hostA.app[0].stopTime = 2.5s +*.hostA.app[0].sendInterval = 50ms +%#-------------------------------------------------------------------------------------------------------------- +%subst: /omnetpp::// +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +RedirectTargetNotLinkLocal.r1.ipv6.neighbourDiscovery: Not sending ICMPv6 Redirect to 2001:db8:2::10: the target 2001:db8:2::3 is neither the destination 2001:db8:4::2 nor a link-local address +%#-------------------------------------------------------------------------------------------------------------- +%not-contains: stdout +use 2001:db8:2::3 as next hop for 2001:db8:4::2 +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +RedirectTargetNotLinkLocal.hostB.ipv6.ipv6: ICMPv6 packet: passing it to ICMPv6 module +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: ( +%#-------------------------------------------------------------------------------------------------------------- From 988805fdbf3893b5c2ca1ee26c6e13acad842db3 Mon Sep 17 00:00:00 2001 From: Gyorgy Szaszko Date: Mon, 28 Sep 2026 23:24:56 +0200 Subject: [PATCH 3/3] icmpv6: add+fix: limit the rate of Redirects An Internet Protocol version 6 (IPv6) router sent one Internet Control Message Protocol for IPv6 (ICMPv6) Redirect for every datagram that qualified. Request for Comments (RFC) 4861 Section 8.2: "A router MUST limit the rate at which Redirect messages are sent, in order to limit the bandwidth and processing costs incurred by the Redirect messages when the source does not correctly respond to the Redirects". Neither Ipv6NeighbourDiscovery nor Ipv6 kept any state for this. Ipv6NeighbourDiscovery now meters Redirects through one token bucket per node, the method RFC 4443 Section 2.4 (f) recommends; RFC 4861 cites it for the details. One bucket per node follows that section, which sets the limit on what "an IPv6 node" originates; a bucket per source or per destination, which would stop one misdirected flow from using up the Redirects of another, is not modelled. Two new parameters set the bucket: redirectRate, the average rate N, and redirectBurst, the burst size B. Their defaults, 10Hz and 10, are the example values of that section for "a small/mid-size device". A Redirect over the limit is not sent, and the datagram is forwarded as before. To reproduce: hostA, r1 and r2 on one switch, hostB behind r2. hostA routes hostB's prefix via r1 named by its global address, and r1 routes it via r2's link-local address. hostA discards every Redirect from r1, because their source, r1's link-local address, is not the next hop of hostA's route, so hostA keeps sending through r1. hostA pings hostB for 10 s at 20 echo requests per second. On master r1 sends 200 Redirects, one per echo request. With this change it sends 108: a burst of 10, then 10 per second. The new module test IPv6_redirect_rate_limit.test sets a burst of 5 and a rate of 2 per second, and counts 8 Redirects for 40 echo requests over 1.95 s. Baselines: the whole fingerprint suite (1774 rows, run with -F tyf, which leaves out the graphical ingredients) moves no row with this commit; the graphical 'tyf' values were not checked. Change: src.networklayer.icmpv6 | behavior.add+change.fix | test whatsnew --- WHATSNEW | 13 +++ .../icmpv6/Ipv6NeighbourDiscovery.cc | 16 ++++ .../icmpv6/Ipv6NeighbourDiscovery.h | 6 ++ .../icmpv6/Ipv6NeighbourDiscovery.ned | 2 + tests/module/IPv6_redirect_rate_limit.test | 96 +++++++++++++++++++ 5 files changed, 133 insertions(+) create mode 100644 tests/module/IPv6_redirect_rate_limit.test diff --git a/WHATSNEW b/WHATSNEW index b65dda99e2a..88d8276aec8 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -322,6 +322,19 @@ Notable backward incompatible changes are the following: Simulations with configured routes through a global gateway on a shared link send fewer Redirects, so their fingerprints may change. +19. IPv6 Redirects are rate limited + + A router sent one Internet Control Message Protocol for IPv6 (ICMPv6) + Redirect for every datagram that qualified, with no limit, although RFC + 4861 section 8.2 requires one. Ipv6NeighbourDiscovery now + meters them with the token bucket that RFC 4443 section 2.4 (f) recommends. + The two new parameters redirectRate (default 10Hz) and redirectBurst (default + 10) are the rate N and the burst size B of that bucket, and the defaults are + the example values of that section. + + Simulations where a source keeps sending through a router that redirects it + send fewer Redirects, so their fingerprints may change. + Notable backward compatible changes are the following: 1. IEEE 802.11 per-station rate statistics diff --git a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc index b4c55b8382e..7d5533f0f66 100644 --- a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc +++ b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.cc @@ -97,6 +97,10 @@ void Ipv6NeighbourDiscovery::initialize(int stage) sendGratuitousNa = par("sendGratuitousNa"); + redirectRate = par("redirectRate"); + redirectBurst = par("redirectBurst"); + redirectTokens = redirectBurst; + pendingQueue.setName("pendingQueue"); } else if (stage == INITSTAGE_NETWORK_CONFIGURATION) { @@ -2397,6 +2401,18 @@ void Ipv6NeighbourDiscovery::sendRedirect(Packet *redirectedPacket, const Ipv6Ad return; } + // RFC 4861 Section 8.2: "A router MUST limit the rate at which Redirect messages + // are sent". RFC 4443 Section 2.4 (f), which it refers to for the details, + // recommends a token bucket of average rate N and burst size B. + simtime_t now = simTime(); + redirectTokens = std::min((double)redirectBurst, redirectTokens + (now - redirectTokensUpdated).dbl() * redirectRate); + redirectTokensUpdated = now; + if (redirectTokens < 1) { + EV_INFO << "Not sending ICMPv6 Redirect to " << pktSrcAddr << " for " << destAddr << ": rate limit reached\n"; + return; + } + redirectTokens -= 1; + EV_INFO << "Sending ICMPv6 Redirect to " << pktSrcAddr << ": use " << targetAddr << " as next hop for " << destAddr << "\n"; diff --git a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.h b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.h index f122b6e9399..3ee7934a5a1 100644 --- a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.h +++ b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.h @@ -95,6 +95,12 @@ class INET_API Ipv6NeighbourDiscovery : public OperationalBase, protected cListe ModuleRefByPar icmpv6; ChecksumMode checksumMode = CHECKSUM_MODE_UNDEFINED; + // token bucket that limits the rate of sent Redirects (RFC 4861 Section 8.2) + double redirectRate = NaN; // tokens added per second + int redirectBurst = -1; // bucket size + double redirectTokens = NaN; + simtime_t redirectTokensUpdated; + #ifdef INET_WITH_MIPV6 ModuleRefByPar mipv6; // in case the node has MIP support #endif diff --git a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.ned b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.ned index eea1942cfee..ad23fd6779e 100644 --- a/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.ned +++ b/src/inet/networklayer/icmpv6/Ipv6NeighbourDiscovery.ned @@ -37,6 +37,8 @@ simple Ipv6NeighbourDiscovery extends SimpleModule double baseReachableTime @unit(s) = default(30s); // RFC 4861 Section 10: base value for computing random ReachableTime volatile double routerBootupTime @unit(s) = default(uniform(0s, 0.3s)); // random startup delay before a router assigns its link-local address; routers boot faster than hosts so that RAs are available by the time hosts solicit volatile double hostBootupTime @unit(s) = default(uniform(0.4s, 1s)); // random startup delay before a host assigns its link-local address + double redirectRate @unit(Hz) = default(10Hz); // RFC 4861 Section 8.2 requires a router to limit the rate of the Redirects it sends; this is the average rate N of the token bucket RFC 4443 Section 2.4 (f) recommends, whose example defaults are N=10/s and B=10 + int redirectBurst = default(10); // the burst size B of that token bucket: the number of Redirects that may be sent back to back @display("i=block/network"); @signal[startDad](type=long); // Emits value=1 @signal[dadCompleted](type=long); // Emits value=1 diff --git a/tests/module/IPv6_redirect_rate_limit.test b/tests/module/IPv6_redirect_rate_limit.test new file mode 100644 index 00000000000..79815966f1b --- /dev/null +++ b/tests/module/IPv6_redirect_rate_limit.test @@ -0,0 +1,96 @@ +%description: +Tests that a router limits the rate of the ICMPv6 Redirects it sends +(RFC 4861 Section 8.2: "A router MUST limit the rate at which Redirect +messages are sent"), with the token bucket of RFC 4443 Section 2.4 (f). + +Topology: hostA, r1 and r2 on one switch; hostB behind r2. hostA's route to +hostB goes via r1, and r1's route goes via the link-local address of r2, back +out the interface the echo requests arrived on, so every echo request makes +r1 send a Redirect. hostA discards each one, because its route names r1 by +its global address, not by the link-local source of the Redirect, and keeps +sending through r1. + +hostA sends 40 echo requests, one per 50 ms, from t=2s to t=3.95s. With a +burst of 5 and a rate of 2 per second, the bucket allows 5 + 2 * 1.95 = 8.9 +Redirects over that time, so r1 sends 8 in all. The postrun command counts +every Redirect r1 sends. + +%#-------------------------------------------------------------------------------------------------------------- +%file: test.ned +import inet.networklayer.configurator.ipv6.Ipv6NetworkConfigurator; +import inet.node.ethernet.Eth100M; +import inet.node.ethernet.EthernetSwitch; +import inet.node.inet.Router; +import inet.node.inet.StandardHost; + +network RedirectRateLimit +{ + submodules: + configurator: Ipv6NetworkConfigurator; + hostA: StandardHost; + sw: EthernetSwitch; + r1: Router; + r2: Router; + hostB: StandardHost; + connections: + hostA.ethg++ <--> Eth100M <--> sw.ethg++; + r1.ethg++ <--> Eth100M <--> sw.ethg++; + r2.ethg++ <--> Eth100M <--> sw.ethg++; + r2.ethg++ <--> Eth100M <--> hostB.ethg++; +} +%#-------------------------------------------------------------------------------------------------------------- +%file: redirect.xml + + + + + + + + + + + +%#-------------------------------------------------------------------------------------------------------------- +%inifile: omnetpp.ini +[General] +record-vector-results = false +ned-path = ../../../../src;. +network = RedirectRateLimit +sim-time-limit = 4.1s +cmdenv-express-mode = false +cmdenv-log-prefix = "%C: " + +**.hasIpv4 = false +**.hasIpv6 = true +**.ipv6.configurator.networkConfiguratorModule = "configurator" +**.ipv6.neighbourDiscovery.dupAddrDetectTransmits = 0 +*.configurator.addStaticRoutes = true +*.configurator.addRemoteRoutes = false +*.configurator.addDefaultRoutes = false +*.configurator.config = xmldoc("redirect.xml") +*.r1.ipv6.routingTable.routes = xml("") +*.r2.ipv6.routingTable.routes = xml("") + +*.hostA.numApps = 1 +*.hostA.app[0].typename = "PingApp" +*.hostA.app[0].destAddr = "2001:db8:4::2" +*.hostA.app[0].startTime = 2s +*.hostA.app[0].stopTime = 3.99s +*.hostA.app[0].sendInterval = 50ms +*.r1.ipv6.neighbourDiscovery.redirectRate = 2Hz +*.r1.ipv6.neighbourDiscovery.redirectBurst = 5 +%#-------------------------------------------------------------------------------------------------------------- +%subst: /omnetpp::// +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep -c "RedirectRateLimit.r1.ipv6.neighbourDiscovery: Sending ICMPv6 Redirect" test.out > test_redirects.out || true +%contains-regex: test_redirects.out +^8$ +%#-------------------------------------------------------------------------------------------------------------- +%contains: stdout +RedirectRateLimit.r1.ipv6.neighbourDiscovery: Not sending ICMPv6 Redirect to 2001:db8:2::10 for 2001:db8:4::2: rate limit reached +%#-------------------------------------------------------------------------------------------------------------- +%postrun-command: grep "undisposed object:" test.out > test_undisposed.out || true +%not-contains: test_undisposed.out +undisposed object: ( +%#--------------------------------------------------------------------------------------------------------------