blob: ad77953b608ca0933bc3542914f9dd950ccb10f6 [file] [log] [blame]
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.routing;
import java.net.InetAddress;
import java.util.HashSet;
import java.util.Set;
import org.apache.hc.client5.http.HttpRoute;
import org.apache.hc.client5.http.RouteInfo.LayerType;
import org.apache.hc.client5.http.RouteInfo.TunnelType;
import org.apache.hc.client5.http.RouteTracker;
import org.apache.hc.client5.http.routing.HttpRouteDirector;
import org.apache.hc.core5.http.HttpHost;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link RouteTracker}.
*/
@SuppressWarnings("boxing") // test code
public class TestRouteTracker {
// a selection of constants for generating routes
public final static
HttpHost TARGET1 = new HttpHost("target1.test.invalid", 80);
public final static
HttpHost TARGET2 = new HttpHost("target2.test.invalid", 8080);
// It is not necessary to have extra targets for https.
// The 'layered' and 'secure' flags are specified explicitly
// for routes, they will not be determined from the scheme.
public final static
HttpHost PROXY1 = new HttpHost("proxy1.test.invalid", 80);
public final static
HttpHost PROXY2 = new HttpHost("proxy2.test.invalid", 1080);
public final static
HttpHost PROXY3 = new HttpHost("proxy3.test.invalid", 88);
public final static InetAddress LOCAL41;
public final static InetAddress LOCAL42;
public final static InetAddress LOCAL61;
public final static InetAddress LOCAL62;
// need static initializer to deal with exceptions
static {
try {
LOCAL41 = InetAddress.getByAddress(new byte[]{ 127, 0, 0, 1 });
LOCAL42 = InetAddress.getByAddress(new byte[]{ 127, 0, 0, 2 });
LOCAL61 = InetAddress.getByAddress(new byte[]{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
});
LOCAL62 = InetAddress.getByAddress(new byte[]{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2
});
} catch (final Exception x) {
throw new ExceptionInInitializerError(x);
}
}
@SuppressWarnings("unused")
@Test
public void testCstrTargetLocal() {
RouteTracker rt = new RouteTracker(TARGET1, null);
Assertions.assertEquals(TARGET1, rt.getTargetHost(), "wrong target (target,null)");
Assertions.assertNull(rt.getLocalAddress(), "wrong local address (target,null)");
Assertions.assertEquals(0, rt.getHopCount(), "wrong hop count (target,null)");
Assertions.assertNull(rt.getProxyHost(), "wrong proxy (target,null)");
Assertions.assertNull(rt.toRoute(), "wrong route (target,null)");
checkCTLS(rt, false, false, false, false);
rt = new RouteTracker(TARGET2, LOCAL61);
Assertions.assertEquals(TARGET2, rt.getTargetHost(), "wrong target (target,local)");
Assertions.assertEquals(LOCAL61, rt.getLocalAddress(), "wrong local address (target,local)");
Assertions.assertEquals(0, rt.getHopCount(), "wrong hop count (target,local)");
Assertions.assertNull(rt.getProxyHost(), "wrong proxy (target,local)");
Assertions.assertNull(rt.toRoute(), "wrong route (target,local)");
checkCTLS(rt, false, false, false, false);
Assertions.assertThrows(NullPointerException.class, () -> new RouteTracker(null, LOCAL41));
}
@SuppressWarnings("unused")
@Test
public void testCstrRoute() {
HttpRoute r = new HttpRoute(TARGET1);
RouteTracker rt = new RouteTracker(r);
Assertions.assertEquals(TARGET1, rt.getTargetHost(), "wrong target (r1)");
Assertions.assertNull(rt.getLocalAddress(), "wrong local address (r1)");
Assertions.assertEquals(0, rt.getHopCount(), "wrong hop count (r1)");
Assertions.assertNull(rt.getProxyHost(), "wrong proxy (r1)");
Assertions.assertNull(rt.toRoute(), "wrong route (r1)");
checkCTLS(rt, false, false, false, false);
r = new HttpRoute(TARGET2, LOCAL61, true);
rt = new RouteTracker(r);
Assertions.assertEquals(TARGET2, rt.getTargetHost(), "wrong target (r2)");
Assertions.assertEquals(LOCAL61, rt.getLocalAddress(), "wrong local address (r2)");
Assertions.assertEquals(0, rt.getHopCount(), "wrong hop count (r2)");
Assertions.assertNull(rt.getProxyHost(), "wrong proxy (r2)");
Assertions.assertNull(rt.toRoute(), "wrong route (r2)");
checkCTLS(rt, false, false, false, false);
r = new HttpRoute(TARGET1, LOCAL42, PROXY3, true);
rt = new RouteTracker(r);
Assertions.assertEquals(TARGET1, rt.getTargetHost(), "wrong target (r3)");
Assertions.assertEquals(LOCAL42, rt.getLocalAddress(), "wrong local address (r3)");
Assertions.assertEquals(0, rt.getHopCount(), "wrong hop count (r3)");
Assertions.assertNull(rt.getProxyHost(), "wrong proxy (r3)");
Assertions.assertNull(rt.toRoute(), "wrong route (r3)");
checkCTLS(rt, false, false, false, false);
Assertions.assertThrows(NullPointerException.class, () -> new RouteTracker(null));
}
@Test
public void testIllegalArgs() {
final RouteTracker rt = new RouteTracker(TARGET2, null);
Assertions.assertThrows(NullPointerException.class, () -> rt.connectProxy(null, true));
Assertions.assertThrows(NullPointerException.class, () -> rt.connectProxy(null, false));
rt.connectProxy(PROXY1, false);
Assertions.assertThrows(NullPointerException.class, () -> rt.tunnelProxy(null, false));
Assertions.assertThrows(NullPointerException.class, () -> rt.tunnelProxy(null, true));
Assertions.assertThrows(IllegalArgumentException.class, () -> rt.getHopTarget(-1));
Assertions.assertThrows(IllegalArgumentException.class, () -> rt.getHopTarget(2));
}
@Test
public void testIllegalStates() {
final RouteTracker rt = new RouteTracker(TARGET1, null);
Assertions.assertThrows(IllegalStateException.class, () -> rt.tunnelTarget(false));
Assertions.assertThrows(IllegalStateException.class, () -> rt.tunnelProxy(PROXY1, false));
Assertions.assertThrows(IllegalStateException.class, () -> rt.layerProtocol(true));
// connect directly
rt.connectTarget(false);
Assertions.assertThrows(IllegalStateException.class, () -> rt.connectTarget(false));
Assertions.assertThrows(IllegalStateException.class, () -> rt.connectProxy(PROXY2, false));
Assertions.assertThrows(IllegalStateException.class, () -> rt.tunnelTarget(false));
Assertions.assertThrows(IllegalStateException.class, () -> rt.tunnelProxy(PROXY1, false));
}
@Test
public void testDirectRoutes() {
final HttpRouteDirector rd = BasicRouteDirector.INSTANCE;
HttpRoute r = new HttpRoute(TARGET1, LOCAL41, false);
RouteTracker rt = new RouteTracker(r);
boolean complete = checkVia(rt, r, rd, 2);
Assertions.assertTrue(complete, "incomplete route 1");
r = new HttpRoute(TARGET2, LOCAL62, true);
rt = new RouteTracker(r);
complete = checkVia(rt, r, rd, 2);
Assertions.assertTrue(complete, "incomplete route 2");
}
@Test
public void testProxyRoutes() {
final HttpRouteDirector rd = BasicRouteDirector.INSTANCE;
HttpRoute r = new HttpRoute(TARGET2, null, PROXY1, false);
RouteTracker rt = new RouteTracker(r);
boolean complete = checkVia(rt, r, rd, 2);
Assertions.assertTrue(complete, "incomplete route 1");
// tunnelled, but neither secure nor layered
r = new HttpRoute(TARGET1, LOCAL61, PROXY3, false,
TunnelType.TUNNELLED, LayerType.PLAIN);
rt = new RouteTracker(r);
complete = checkVia(rt, r, rd, 3);
Assertions.assertTrue(complete, "incomplete route 2");
// tunnelled, layered, but not secure
r = new HttpRoute(TARGET1, LOCAL61, PROXY3, false,
TunnelType.TUNNELLED, LayerType.LAYERED);
rt = new RouteTracker(r);
complete = checkVia(rt, r, rd, 4);
Assertions.assertTrue(complete, "incomplete route 3");
// tunnelled, layered, secure
r = new HttpRoute(TARGET1, LOCAL61, PROXY3, true);
rt = new RouteTracker(r);
complete = checkVia(rt, r, rd, 4);
Assertions.assertTrue(complete, "incomplete route 4");
}
@Test
public void testProxyChainRoutes() {
final HttpRouteDirector rd = BasicRouteDirector.INSTANCE;
HttpHost[] proxies = { PROXY1, PROXY2 };
HttpRoute r = new HttpRoute(TARGET2, LOCAL42, proxies, false,
TunnelType.PLAIN, LayerType.PLAIN);
RouteTracker rt = new RouteTracker(r);
boolean complete = checkVia(rt, r, rd, 3);
Assertions.assertTrue(complete, "incomplete route 1");
// tunnelled, but neither secure nor layered
proxies = new HttpHost[]{ PROXY3, PROXY2 };
r = new HttpRoute(TARGET1, null, proxies, false,
TunnelType.TUNNELLED, LayerType.PLAIN);
rt = new RouteTracker(r);
complete = checkVia(rt, r, rd, 4);
Assertions.assertTrue(complete, "incomplete route 2");
// tunnelled, layered, but not secure
proxies = new HttpHost[]{ PROXY3, PROXY2, PROXY1 };
r = new HttpRoute(TARGET2, LOCAL61, proxies, false,
TunnelType.TUNNELLED, LayerType.LAYERED);
rt = new RouteTracker(r);
complete = checkVia(rt, r, rd, 6);
Assertions.assertTrue(complete, "incomplete route 3");
// tunnelled, layered, secure
proxies = new HttpHost[]{ PROXY1, PROXY3 };
r = new HttpRoute(TARGET1, LOCAL61, proxies, true,
TunnelType.TUNNELLED, LayerType.LAYERED);
rt = new RouteTracker(r);
complete = checkVia(rt, r, rd, 5);
Assertions.assertTrue(complete, "incomplete route 4");
}
@Test
public void testEqualsHashcodeCloneToString()
throws CloneNotSupportedException {
final RouteTracker rt0 = new RouteTracker(TARGET1, null);
final RouteTracker rt1 = new RouteTracker(TARGET2, null);
final RouteTracker rt2 = new RouteTracker(TARGET1, null);
final RouteTracker rt3 = new RouteTracker(TARGET1, null);
final RouteTracker rt4 = new RouteTracker(TARGET1, LOCAL41);
final RouteTracker rt6 = new RouteTracker(TARGET1, LOCAL62);
Assertions.assertNotEquals(null, rt0, "rt0");
Assertions.assertEquals(rt0, rt0, "rt0");
Assertions.assertNotEquals("rt0", rt0, rt0.toString());
Assertions.assertNotEquals(rt0, rt4, "rt0 == rt4");
Assertions.assertNotEquals(rt0, rt1, "rt0 == rt1"); // Check host takes part in equals
// Check that connection takes part in equals
Assertions.assertEquals(rt0, rt2, "rt0 != rt2");
rt2.connectTarget(false);
Assertions.assertNotEquals(rt0, rt2, "rt0 == rt2");
Assertions.assertEquals(rt0, rt3, "rt0 != rt3");
rt3.connectTarget(true);
Assertions.assertNotEquals(rt0, rt3, "rt0 == rt3");
Assertions.assertNotEquals(rt2, rt3, "rt2 == rt3"); // Test secure takes part
// TODO needs tests for tunnel and layered
Assertions.assertNotEquals(rt4, rt0, "rt4 == rt0");
Assertions.assertNotEquals(rt0, rt6, "rt0 == rt6");
Assertions.assertNotEquals(rt6, rt0, "rt6 == rt0");
Assertions.assertNotEquals(rt4, rt6, "rt4 == rt6");
Assertions.assertNotEquals(rt6, rt4, "rt6 == rt4");
// it is likely but not guaranteed that the hashcodes are different
Assertions.assertNotEquals(rt0.hashCode(), rt4.hashCode(), "rt0 == rt4 (hashcode)");
Assertions.assertNotEquals(rt0.hashCode(), rt6.hashCode(), "rt0 == rt6 (hashcode)");
Assertions.assertNotEquals(rt6.hashCode(), rt4.hashCode(), "rt6 == rt4 (hashcode)");
Assertions.assertEquals(rt0, rt0.clone(), "rt0 (clone)");
Assertions.assertEquals(rt4, rt4.clone(), "rt4 (clone)");
Assertions.assertEquals(rt6, rt6.clone(), "rt6 (clone)");
// we collect (clones of) the different tracked routes along the way
// rt0 -> direct connection
// rt1 -> via single proxy
// rt2 -> via proxy chain
final Set<RouteTracker> hs = new HashSet<>();
// we also collect hashcodes for the different paths
// since we can't guarantee what influence the HttpHost hashcodes have,
// we keep separate sets here
final Set<Integer> hc0 = new HashSet<>();
final Set<Integer> hc4 = new HashSet<>();
final Set<Integer> hc6 = new HashSet<>();
RouteTracker rt = null;
Assertions.assertTrue(hs.add(rt0));
Assertions.assertTrue(hs.add(rt4));
Assertions.assertTrue(hs.add(rt6));
Assertions.assertTrue(hc0.add(rt0.hashCode()));
Assertions.assertTrue(hc4.add(rt4.hashCode()));
Assertions.assertTrue(hc6.add(rt6.hashCode()));
rt = (RouteTracker) rt0.clone();
rt.connectTarget(false);
Assertions.assertTrue(hs.add(rt));
Assertions.assertTrue(hc0.add(rt.hashCode()));
rt = (RouteTracker) rt0.clone();
rt.connectTarget(true);
Assertions.assertTrue(hs.add(rt));
Assertions.assertTrue(hc0.add(rt.hashCode()));
// proxy (insecure) -> tunnel (insecure) -> layer (secure)
rt = (RouteTracker) rt4.clone();
rt.connectProxy(PROXY1, false);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
// this is not guaranteed to be unique...
Assertions.assertTrue(hc4.add(rt.hashCode()));
rt.tunnelTarget(false);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
Assertions.assertTrue(hc4.add(rt.hashCode()));
rt.layerProtocol(true);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
Assertions.assertTrue(hc4.add(rt.hashCode()));
// proxy (secure) -> tunnel (secure) -> layer (insecure)
rt = (RouteTracker) rt4.clone();
rt.connectProxy(PROXY1, true);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
// this is not guaranteed to be unique...
Assertions.assertTrue(hc4.add(rt.hashCode()));
rt.tunnelTarget(true);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
Assertions.assertTrue(hc4.add(rt.hashCode()));
rt.layerProtocol(false);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
Assertions.assertTrue(hc4.add(rt.hashCode()));
// PROXY1/i -> PROXY2/i -> tunnel/i -> layer/s
rt = (RouteTracker) rt6.clone();
rt.connectProxy(PROXY1, false);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
// this is not guaranteed to be unique...
Assertions.assertTrue(hc6.add(rt.hashCode()));
rt.tunnelProxy(PROXY2, false);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
// this is not guaranteed to be unique...
Assertions.assertTrue(hc6.add(rt.hashCode()));
rt.tunnelTarget(false);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
Assertions.assertTrue(hc6.add(rt.hashCode()));
rt.layerProtocol(true);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
Assertions.assertTrue(hc6.add(rt.hashCode()));
// PROXY1/s -> PROXY2/s -> tunnel/s -> layer/i
rt = (RouteTracker) rt6.clone();
rt.connectProxy(PROXY1, true);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
// this is not guaranteed to be unique...
Assertions.assertTrue(hc6.add(rt.hashCode()));
rt.tunnelProxy(PROXY2, true);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
// this is not guaranteed to be unique...
Assertions.assertTrue(hc6.add(rt.hashCode()));
rt.tunnelTarget(true);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
Assertions.assertTrue(hc6.add(rt.hashCode()));
rt.layerProtocol(false);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
Assertions.assertTrue(hc6.add(rt.hashCode()));
// PROXY2/i -> PROXY1/i -> tunnel/i -> layer/s
rt = (RouteTracker) rt6.clone();
rt.connectProxy(PROXY2, false);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
// this is not guaranteed to be unique...
Assertions.assertTrue(hc6.add(rt.hashCode()));
rt.tunnelProxy(PROXY1, false);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
// proxy chain sequence does not affect hashcode, so duplicate:
// Assertions.assertTrue(hc6.add(Integer.valueOf(rt.hashCode())));
rt.tunnelTarget(false);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
// proxy chain sequence does not affect hashcode, so duplicate:
// Assertions.assertTrue(hc6.add(Integer.valueOf(rt.hashCode())));
rt.layerProtocol(true);
Assertions.assertTrue(hs.add((RouteTracker) rt.clone()));
// proxy chain sequence does not affect hashcode, so duplicate:
// Assertions.assertTrue(hc6.add(Integer.valueOf(rt.hashCode())));
// check that all toString are OK and different
final Set<String> rtstrings = new HashSet<>();
for (final RouteTracker current: hs) {
final String rts = checkToString(current);
Assertions.assertTrue(rtstrings.add(rts), "duplicate toString: " + rts);
}
}
/** Helper to check the status of the four flags. */
public final static void checkCTLS(final RouteTracker rt,
final boolean c, final boolean t,
final boolean l, final boolean s) {
final String rts = rt.toString();
Assertions.assertEquals(c, rt.isConnected(), "wrong flag connected: " + rts);
Assertions.assertEquals(t, rt.isTunnelled(), "wrong flag tunnelled: " + rts);
Assertions.assertEquals(t ? TunnelType.TUNNELLED : TunnelType.PLAIN,
rt.getTunnelType(), "wrong enum tunnelled: " + rts);
Assertions.assertEquals(l, rt.isLayered(), "wrong flag layered: " + rts);
Assertions.assertEquals(l ? LayerType.LAYERED : LayerType.PLAIN,
rt.getLayerType(), "wrong enum layered: " + rts);
Assertions.assertEquals(s, rt.isSecure(), "wrong flag secure: " + rts);
}
/**
* Helper to check tracking of a route.
* This uses a {@link HttpRouteDirector} to fake establishing the route,
* checking the intermediate steps.
*
* @param rt the tracker to check with
* @param r the route to establish
* @param rd the director to check with
* @param steps the step count for this invocation
*
* @return {@code true} iff the route is complete
*/
public final static boolean checkVia(final RouteTracker rt, final HttpRoute r,
final HttpRouteDirector rd, final int steps) {
final String msg = r + " @ " + rt;
boolean complete = false;
int n = steps;
while (!complete && (n > 0)) {
final int action = rd.nextStep(r, rt.toRoute());
switch (action) {
case HttpRouteDirector.COMPLETE:
complete = true;
Assertions.assertEquals(r, rt.toRoute());
break;
case HttpRouteDirector.CONNECT_TARGET: {
final boolean sec = r.isSecure();
rt.connectTarget(sec);
checkCTLS(rt, true, false, false, sec);
Assertions.assertEquals(1, rt.getHopCount(), "wrong hop count "+msg);
Assertions.assertEquals(r.getTargetHost(), rt.getHopTarget(0), "wrong hop0 "+msg);
} break;
case HttpRouteDirector.CONNECT_PROXY: {
// we assume an insecure proxy connection
final boolean sec = false;
rt.connectProxy(r.getProxyHost(), sec);
checkCTLS(rt, true, false, false, sec);
Assertions.assertEquals(2, rt.getHopCount(), "wrong hop count "+msg);
Assertions.assertEquals(r.getProxyHost(), rt.getHopTarget(0), "wrong hop0 "+msg);
Assertions.assertEquals(r.getTargetHost(), rt.getHopTarget(1), "wrong hop1 "+msg);
} break;
case HttpRouteDirector.TUNNEL_TARGET: {
final int hops = rt.getHopCount();
// we assume an insecure tunnel
final boolean sec = false;
rt.tunnelTarget(sec);
checkCTLS(rt, true, true, false, sec);
Assertions.assertEquals(hops, rt.getHopCount(), "wrong hop count "+msg);
Assertions.assertEquals(r.getProxyHost(), rt.getHopTarget(0), "wrong hop0 "+msg);
Assertions.assertEquals(r.getTargetHost(), rt.getHopTarget(hops-1), "wrong hopN "+msg);
} break;
case HttpRouteDirector.TUNNEL_PROXY: {
final int hops = rt.getHopCount(); // before tunnelling
// we assume an insecure tunnel
final boolean sec = false;
final HttpHost pxy = r.getHopTarget(hops-1);
rt.tunnelProxy(pxy, sec);
// Since we're tunnelling to a proxy and not the target,
// the 'tunelling' flag is false: no end-to-end tunnel.
checkCTLS(rt, true, false, false, sec);
Assertions.assertEquals(hops+1, rt.getHopCount(), "wrong hop count "+msg);
Assertions.assertEquals(r.getProxyHost(), rt.getHopTarget(0), "wrong hop0 "+msg);
Assertions.assertEquals(pxy, rt.getHopTarget(hops-1), "wrong hop"+hops+" "+msg);
Assertions.assertEquals(r.getTargetHost(), rt.getHopTarget(hops), "wrong hopN "+msg);
} break;
case HttpRouteDirector.LAYER_PROTOCOL: {
final int hops = rt.getHopCount();
final boolean tun = rt.isTunnelled();
final boolean sec = r.isSecure();
rt.layerProtocol(sec);
checkCTLS(rt, true, tun, true, sec);
Assertions.assertEquals(hops, rt.getHopCount(), "wrong hop count "+msg);
Assertions.assertEquals(r.getProxyHost(), rt.getProxyHost(), "wrong proxy "+msg);
Assertions.assertEquals(r.getTargetHost(), rt.getTargetHost(), "wrong target "+msg);
} break;
// UNREACHABLE
default:
Assertions.fail("unexpected action " + action + " from director, "+msg);
break;
} // switch
n--;
}
return complete;
} // checkVia
/**
* Checks the output of {@code toString}.
*
* @param rt the tracker for which to check the output
*
* @return the result of {@code rt.toString()}
*/
public final static String checkToString(final RouteTracker rt) {
if (rt == null) {
return null;
}
final String rts = rt.toString();
if (rt.getLocalAddress() != null) {
final String las = rt.getLocalAddress().toString();
Assertions.assertTrue(rts.contains(las), "no local address in toString(): " + rts);
}
for (int i=0; i<rt.getHopCount(); i++) {
final String hts = rt.getHopTarget(i).toString();
Assertions.assertTrue(rts.contains(hts), "hop " + i + " (" + hts + ") missing in toString(): " + rts);
}
return rts;
}
}