diff --git a/WHATSNEW b/WHATSNEW index 03828369a2a..88d8276aec8 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -293,6 +293,48 @@ 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. + +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. + +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 9785d3f5522..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) { @@ -2386,6 +2390,29 @@ 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; + } + + // 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/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_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: ( +%#-------------------------------------------------------------------------------------------------------------- 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: ( +%#-------------------------------------------------------------------------------------------------------------- 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: ( +%#--------------------------------------------------------------------------------------------------------------