diff --git a/CHANGELOG.md b/CHANGELOG.md index 91d38bd..453e737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. [#13](https://github.com/netsec-ethz/scion-java-multiping/pull/13) - Fixed calculation of average and median values [#17](https://github.com/netsec-ethz/scion-java-multiping/pull/17) +- Added proper tests, refactored PingAll and fixed minor issues + [#18](https://github.com/netsec-ethz/scion-java-multiping/pull/18) ## [0.4.0] - 2025-04-04 diff --git a/src/main/java/org/scion/multiping/PingAll.java b/src/main/java/org/scion/multiping/PingAll.java index 19c012b..741321e 100644 --- a/src/main/java/org/scion/multiping/PingAll.java +++ b/src/main/java/org/scion/multiping/PingAll.java @@ -24,9 +24,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.ToDoubleFunction; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import org.scion.jpan.*; import org.scion.jpan.internal.PathRawParser; @@ -54,7 +52,7 @@ public class PingAll { private static final boolean SHOW_ONLY_ICMP = false; private static final Config config = new Config(); - private static final int localPort = 30041; + private static final int LOCAL_PORT = 30041; private static final boolean STOP_SHIM = true; static { @@ -64,25 +62,17 @@ public class PingAll { } } - private int nAsTried = 0; - private int nAsSuccess = 0; - private int nAsError = 0; - private int nAsTimeout = 0; - private int nAsNoPathFound = 0; + private final Set listedAs = new HashSet<>(); + private final Set seenAs = new HashSet<>(); + private final ResultSummary summary = new ResultSummary(); - private int nPathTried = 0; - private int nPathSuccess = 0; - private int nPathTimeout = 0; + private final ScionProvider service; + private final Policy policy; - private static final Set listedAs = new HashSet<>(); - private static final Set seenAs = new HashSet<>(); - private static final List results = new ArrayList<>(); - - private enum Policy { + enum Policy { /** Fastest path using SCMP traceroute */ FASTEST_TR, /** Fastest path using SCMP async traceroute */ - FASTEST_ECHO, FASTEST_TR_ASYNC, /** Shortest path using SCMP traceroute */ SHORTEST_TR, @@ -90,92 +80,57 @@ private enum Policy { SHORTEST_ECHO } - private static final Policy POLICY = Policy.FASTEST_TR_ASYNC; + private static final Policy DEFAULT_POLICY = Policy.FASTEST_TR_ASYNC; private static final boolean SHOW_PATH = false; + PingAll(Policy policy, ScionProvider service) { + this.policy = policy; + this.service = service; + } + public static void main(String[] args) throws IOException { PRINT = true; // System.setProperty(Constants.PROPERTY_DNS_SEARCH_DOMAINS, "ethz.ch."); System.setProperty(Constants.PROPERTY_SHIM, STOP_SHIM ? "false" : "true"); // disable SHIM println("Settings:"); - println(" Path policy = " + POLICY); + println(" Path policy = " + DEFAULT_POLICY); println(" ICMP=" + config.tryICMP); println(" printOnlyICMP=" + SHOW_ONLY_ICMP); - PingAll demo = new PingAll(); - List allASes = DownloadAssignmentsFromWeb.getList(); + PingAll pingAll = new PingAll(DEFAULT_POLICY, ScionProvider.defaultProvider(LOCAL_PORT)); + pingAll.run(); + pingAll.summary.prettyPrint(); + } + + ResultSummary run() throws IOException { + List allASes = service.getIsdAsEntries(); // remove entry for local AS - long localAS = Scion.defaultService().getLocalIsdAs(); + long localAS = service.getLocalIsdAs(); allASes = allASes.stream().filter(e -> e.getIsdAs() != localAS).collect(Collectors.toList()); // Process all ASes for (ParseAssignments.HostEntry e : allASes) { print(ScionUtil.toStringIA(e.getIsdAs()) + " \"" + e.getName() + "\" "); - demo.runDemo(e); + runAS(e); listedAs.add(e.getIsdAs()); } // Try to identify ASes that occur in any paths but that are not on the public list. - int nSeenButNotListed = 0; for (Long isdAs : seenAs) { if (!listedAs.contains(isdAs)) { - nSeenButNotListed++; + summary.incSeenButNotListed(); } } - - // max: - Result maxPing = max(Result::isSuccess, (o1, o2) -> (int) (o1.getPingMs() - o2.getPingMs())); - Result maxHops = max(r -> r.getHopCount() > 0, Comparator.comparingInt(Result::getHopCount)); - Result maxPaths = max(r -> r.getPathCount() > 0, Comparator.comparingInt(Result::getPathCount)); - - // avg/median: - double avgPing = avg(Result::isSuccess, Result::getPingMs); - double avgHops = avg(r -> r.getHopCount() > 0, Result::getHopCount); - double avgPaths = avg(r -> r.getPathCount() > 0, Result::getPathCount); - double medianPing = median(Result::isSuccess, Result::getPingMs); - double medianHops = median(r -> r.getHopCount() > 0, Result::getHopCount); - double medianPaths = median(r -> r.getPathCount() > 0, Result::getPathCount); - - println(""); - println("Max hops = " + maxHops.getHopCount() + ": " + maxHops); - println("Max ping [ms] = " + round(maxPing.getPingMs(), 2) + ": " + maxPing); - println("Max paths = " + maxPaths.getPathCount() + ": " + maxPaths); - - println("Median hops = " + (int) medianHops); - println("Median ping [ms] = " + round(medianPing, 2)); - println("Median paths = " + (int) medianPaths); - - println("Avg hops = " + round(avgHops, 1)); - println("Avg ping [ms] = " + round(avgPing, 2)); - println("Avg paths = " + (int) round(avgPaths, 0)); - - println(""); - println("AS Stats:"); - println(" all = " + demo.nAsTried); - println(" success = " + demo.nAsSuccess); - println(" no path = " + demo.nAsNoPathFound); - println(" timeout = " + demo.nAsTimeout); - println(" error = " + demo.nAsError); - println(" not listed = " + nSeenButNotListed); - println("Path Stats:"); - println(" all = " + demo.nPathTried); - println(" success = " + demo.nPathSuccess); - println(" timeout = " + demo.nPathTimeout); - println("ICMP Stats:"); - println(" all = " + ICMP.nIcmpTried); - println(" success = " + ICMP.nIcmpSuccess); - println(" timeout = " + ICMP.nIcmpTimeout); - println(" error = " + ICMP.nIcmpError); + return summary; } - private void runDemo(ParseAssignments.HostEntry remote) throws IOException { - nAsTried++; - ScionService service = Scion.defaultService(); + private void runAS(ParseAssignments.HostEntry remote) throws IOException { + summary.incAsTried(); // Dummy address. The traceroute will contact the control service IP instead. InetSocketAddress destinationAddress = new InetSocketAddress(InetAddress.getByAddress(new byte[] {0, 0, 0, 0}), 30041); int nPaths; - Scmp.TimedMessage[] msg = new Scmp.TimedMessage[REPEAT]; + Scmp.TimedMessage[] msgs = new Scmp.TimedMessage[REPEAT]; Ref bestPath = Ref.empty(); try { List paths = service.getPaths(remote.getIsdAs(), destinationAddress); @@ -185,37 +140,41 @@ private void runDemo(ParseAssignments.HostEntry remote) throws IOException { if (!SHOW_ONLY_ICMP) { println("WARNING: No path found from " + src + " to " + dst); } - nAsNoPathFound++; - results.add(new Result(remote, Result.State.NO_PATH)); + summary.incAsNoPathFound(); + summary.add(new Result(remote, Result.State.NO_PATH)); return; } nPaths = paths.size(); - msg[0] = findPaths(paths, bestPath); - if (msg[0] != null && REPEAT > 1) { - try (ScmpSender sender = Scmp.newSenderBuilder().setLocalPort(localPort).build()) { - for (int i = 1; i < msg.length; i++) { + msgs[0] = findPaths(paths, bestPath); + // bestPath is null if all paths have timed out + if (msgs[0] != null && bestPath.get() != null && REPEAT > 1) { + try (ScionProvider.Sync sender = service.getSync()) { + for (int i = 1; i < msgs.length; i++) { + if (bestPath.get() == null) { + break; + } List messages = sender.sendTracerouteRequest(bestPath.get()); - msg[i] = messages.get(messages.size() - 1); + msgs[i] = messages.get(messages.size() - 1); } } } } catch (ScionRuntimeException e) { println("ERROR: " + e.getMessage()); - nAsError++; - results.add(new Result(remote, Result.State.ERROR)); + summary.incAsError(); + summary.add(new Result(remote, Result.State.ERROR)); return; } - Result result = new Result(remote, msg[0], bestPath.get(), nPaths); - results.add(result); + Result result = new Result(remote, msgs[0], bestPath.get(), nPaths); + summary.add(result); - if (msg[0] == null) { + if (msgs[0] == null) { return; } // ICMP ping StringBuilder icmpMs = new StringBuilder(); for (int i = 0; i < REPEAT; i++) { - String icmpMsStr = ICMP.pingICMP(msg[0].getPath().getRemoteAddress(), config); + String icmpMsStr = ICMP.pingICMP(msgs[0].getPath().getRemoteAddress(), config); icmpMs.append(icmpMsStr).append(" "); if (icmpMsStr.startsWith("TIMEOUT") || icmpMsStr.startsWith("N/A")) { break; @@ -224,10 +183,14 @@ private void runDemo(ParseAssignments.HostEntry remote) throws IOException { result.setICMP(icmpMs.toString()); // output - int nHops = PathRawParser.create(msg[0].getPath().getRawPath()).getHopCount(); - String addr = msg[0].getPath().getRemoteAddress().getHostAddress(); + int nHops = PathRawParser.create(msgs[0].getPath().getRawPath()).getHopCount(); + String addr = msgs[0].getPath().getRemoteAddress().getHostAddress(); print(addr + " nPaths=" + nPaths + " nHops=" + nHops + " time= "); - for (Scmp.TimedMessage m : msg) { + for (Scmp.TimedMessage m : msgs) { + if (m == null) { + print("N/A "); + continue; + } double millis = round(m.getNanoSeconds() / (double) 1_000_000, 2); print(millis + "ms "); } @@ -241,15 +204,15 @@ private void runDemo(ParseAssignments.HostEntry remote) throws IOException { } else { println(); } - if (msg[0].isTimedOut()) { - nAsTimeout++; + if (msgs[0].isTimedOut()) { + summary.incAsTimeout(); } else { - nAsSuccess++; + summary.incAsSuccess(); } } private Scmp.TimedMessage findPaths(List paths, Ref bestOut) { - switch (POLICY) { + switch (policy) { case FASTEST_TR: return findFastestTR(paths, bestOut); case FASTEST_TR_ASYNC: @@ -267,26 +230,20 @@ private Scmp.EchoMessage findShortestEcho(List paths, Ref refBest) { Path path = PathPolicy.MIN_HOPS.filter(paths).get(0); refBest.set(path); ByteBuffer bb = ByteBuffer.allocate(0); - try (ScmpSender sender = Scmp.newSenderBuilder().setLocalPort(localPort).build()) { - nPathTried++; + try (ScionProvider.Sync sender = service.getSync()) { + summary.incPathTried(); Scmp.EchoMessage msg = sender.sendEchoRequest(path, bb); - if (msg == null) { - println(" -> local AS, no timing available"); - nPathSuccess++; - nAsSuccess++; - return null; - } if (msg.isTimedOut()) { - nPathTimeout++; + summary.incPathTimeout(); return msg; } - nPathSuccess++; + summary.incPathSuccess(); return msg; } catch (IOException e) { println("ERROR: " + e.getMessage()); - nAsError++; + summary.incAsError(); return null; } } @@ -294,15 +251,9 @@ private Scmp.EchoMessage findShortestEcho(List paths, Ref refBest) { private Scmp.TracerouteMessage findShortestTR(List paths, Ref refBest) { Path path = PathPolicy.MIN_HOPS.filter(paths).get(0); refBest.set(path); - try (ScmpSender sender = Scmp.newSenderBuilder().setLocalPort(localPort).build()) { - nPathTried++; + try (ScionProvider.Sync sender = service.getSync()) { + summary.incPathTried(); List messages = sender.sendTracerouteRequest(path); - if (messages.isEmpty()) { - println(" -> local AS, no timing available"); - nPathSuccess++; - nAsSuccess++; - return null; - } for (Scmp.TracerouteMessage msg : messages) { seenAs.add(msg.getIsdAs()); @@ -310,31 +261,25 @@ private Scmp.TracerouteMessage findShortestTR(List paths, Ref refBes Scmp.TracerouteMessage msg = messages.get(messages.size() - 1); if (msg.isTimedOut()) { - nPathTimeout++; + summary.incPathTimeout(); return msg; } - nPathSuccess++; + summary.incPathSuccess(); return msg; } catch (IOException e) { println("ERROR: " + e.getMessage()); - nAsError++; + summary.incAsError(); return null; } } private Scmp.TracerouteMessage findFastestTR(List paths, Ref refBest) { Scmp.TracerouteMessage best = null; - try (ScmpSender sender = Scmp.newSenderBuilder().setLocalPort(localPort).build()) { + try (ScionProvider.Sync sender = service.getSync()) { for (Path path : paths) { - nPathTried++; + summary.incPathTried(); List messages = sender.sendTracerouteRequest(path); - if (messages.isEmpty()) { - println(" -> local AS, no timing available"); - nPathSuccess++; - nAsSuccess++; - return null; - } for (Scmp.TracerouteMessage msg : messages) { seenAs.add(msg.getIsdAs()); @@ -342,11 +287,11 @@ private Scmp.TracerouteMessage findFastestTR(List paths, Ref refBest Scmp.TracerouteMessage msg = messages.get(messages.size() - 1); if (msg.isTimedOut()) { - nPathTimeout++; + summary.incPathTimeout(); return msg; } - nPathSuccess++; + summary.incPathSuccess(); if (best == null || msg.getNanoSeconds() < best.getNanoSeconds()) { best = msg; @@ -356,7 +301,7 @@ private Scmp.TracerouteMessage findFastestTR(List paths, Ref refBest return best; } catch (IOException e) { println("ERROR: " + e.getMessage()); - nAsError++; + summary.incAsError(); return null; } } @@ -364,6 +309,7 @@ private Scmp.TracerouteMessage findFastestTR(List paths, Ref refBest private Scmp.TracerouteMessage findFastestTRasync(List paths, Ref refBest) { ConcurrentHashMap messages = new ConcurrentHashMap<>(); CountDownLatch barrier = new CountDownLatch(paths.size()); + AtomicInteger errors = new AtomicInteger(); ScmpSenderAsync.ResponseHandler handler = new ScmpSenderAsync.ResponseHandler() { @Override @@ -380,59 +326,57 @@ public void onTimeout(Scmp.TimedMessage msg) { @Override public void onError(Scmp.ErrorMessage msg) { + errors.incrementAndGet(); barrier.countDown(); } @Override public void onException(Throwable t) { + errors.incrementAndGet(); barrier.countDown(); } }; - Scmp.TracerouteMessage best = null; - try (ScmpSenderAsync sender = - Scmp.newSenderAsyncBuilder(handler).setLocalPort(localPort).build()) { + // Send all requests + try (ScionProvider.Async sender = service.getAsync(handler)) { for (Path path : paths) { - nPathTried++; - int id = sender.sendTracerouteLast(path); - if (id == -1) { - println(" -> local AS, no timing available"); - nPathSuccess++; - nAsSuccess++; - return null; - } + summary.incPathTried(); + sender.sendTracerouteLast(path); } - // Wait for all messages to be received - try { - if (!barrier.await(1100, TimeUnit.MILLISECONDS)) { - throw new IllegalStateException( - "Missing messages: " + barrier.getCount() + "/" + paths.size()); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new IllegalStateException(e); + // Wait for all messages to be received, BEFORE closing the "sender". + if (!barrier.await(1100, TimeUnit.MILLISECONDS)) { + throw new IllegalStateException( + "Missing messages: " + barrier.getCount() + "/" + paths.size()); } - } catch (IOException e) { println("ERROR: " + e.getMessage()); - nAsError++; + summary.incAsError(); + return null; + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + + if (errors.get() > 0 && messages.isEmpty()) { + summary.incAsError(); return null; } + Scmp.TracerouteMessage best = null; for (Scmp.TimedMessage tm : messages.values()) { Scmp.TracerouteMessage msg = (Scmp.TracerouteMessage) tm; seenAs.add(msg.getIsdAs()); if (msg.isTimedOut()) { - nPathTimeout++; + summary.incPathTimeout(); if (best == null) { best = msg; } continue; } - nPathSuccess++; + summary.incPathSuccess(); if (best == null || msg.getNanoSeconds() < best.getNanoSeconds()) { best = msg; @@ -441,24 +385,4 @@ public void onException(Throwable t) { } return best; } - - private static double avg(Predicate filter, ToDoubleFunction mapper) { - return results.stream().filter(filter).mapToDouble(mapper).average().orElse(-1); - } - - private static Result max(Predicate filter, Comparator comparator) { - return results.stream().filter(filter).max(comparator).orElseThrow(NoSuchElementException::new); - } - - private static double median(Predicate filter, Function mapper) { - List list = - results.stream().filter(filter).map(mapper).sorted().collect(Collectors.toList()); - if (list.isEmpty()) { - return -1; - } - if (list.get(0) instanceof Double) { - return (Double) list.get(list.size() / 2); - } - return (Integer) list.get(list.size() / 2); - } } diff --git a/src/main/java/org/scion/multiping/util/ParseAssignments.java b/src/main/java/org/scion/multiping/util/ParseAssignments.java index a6086d5..dddb712 100644 --- a/src/main/java/org/scion/multiping/util/ParseAssignments.java +++ b/src/main/java/org/scion/multiping/util/ParseAssignments.java @@ -36,7 +36,7 @@ public static class HostEntry { private final String name; private String ip; - HostEntry(long isdAs, String name) { + public HostEntry(long isdAs, String name) { this.isdAs = isdAs; this.name = name; } diff --git a/src/main/java/org/scion/multiping/util/Result.java b/src/main/java/org/scion/multiping/util/Result.java index 6d0aa7a..23017bb 100644 --- a/src/main/java/org/scion/multiping/util/Result.java +++ b/src/main/java/org/scion/multiping/util/Result.java @@ -57,7 +57,7 @@ public Result(ParseAssignments.HostEntry e, Scmp.TimedMessage msg, Path request, } this.nPaths = nPaths; this.path = request; - nHops = PathRawParser.create(request.getRawPath()).getHopCount(); + nHops = request != null ? PathRawParser.create(request.getRawPath()).getHopCount() : -1; remoteIP = msg.getPath().getRemoteAddress().getHostAddress(); if (msg.isTimedOut()) { state = State.TIMEOUT; @@ -98,7 +98,7 @@ public boolean isSuccess() { @Override public String toString() { String out = ScionUtil.toStringIA(isdAs) + " " + name; - out += " " + ScionUtil.toStringPath(path.getMetadata()); + out += " " + (path != null ? ScionUtil.toStringPath(path.getMetadata()) : "N/A"); out += " " + remoteIP + " nPaths=" + nPaths + " nHops=" + nHops; return out + " time=" + Util.round(pingMs, 2) + "ms" + " ICMP=" + icmp; } diff --git a/src/main/java/org/scion/multiping/util/ResultSummary.java b/src/main/java/org/scion/multiping/util/ResultSummary.java new file mode 100644 index 0000000..a45a078 --- /dev/null +++ b/src/main/java/org/scion/multiping/util/ResultSummary.java @@ -0,0 +1,168 @@ +// Copyright 2025 ETH Zurich +// +// Licensed 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. + +package org.scion.multiping.util; + +import static org.scion.multiping.util.Util.println; +import static org.scion.multiping.util.Util.round; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.ToDoubleFunction; +import java.util.stream.Collectors; + +public class ResultSummary { + + private final List results = new ArrayList<>(); + private int nAsTried = 0; + private int nAsSuccess = 0; + private int nAsError = 0; + private int nAsTimeout = 0; + private int nAsNoPathFound = 0; + + private int nPathTried = 0; + private int nPathSuccess = 0; + private int nPathTimeout = 0; + + private int nSeenButNotListed = 0; + + public void incAsTried() { + nAsTried++; + } + + public void incAsSuccess() { + nAsSuccess++; + } + + public void incAsError() { + nAsError++; + } + + public void incAsTimeout() { + nAsTimeout++; + } + + public void incAsNoPathFound() { + nAsNoPathFound++; + } + + public void incPathTried() { + nPathTried++; + } + + public void incPathSuccess() { + nPathSuccess++; + } + + public void incPathTimeout() { + nPathTimeout++; + } + + public void incSeenButNotListed() { + nSeenButNotListed++; + } + + public void add(Result r) { + results.add(r); + } + + public Result getMaxPaths() { + return max(r -> r.getPathCount() > 0, Comparator.comparingInt(Result::getPathCount)); + } + + public int getAsTimeouts() { + return nAsTimeout; + } + + public int getAsErrors() { + return nAsError; + } + + public int getPathTimeouts() { + return nPathTimeout; + } + + public void prettyPrint() { + // max: + Result maxPing = max(Result::isSuccess, (o1, o2) -> (int) (o1.getPingMs() - o2.getPingMs())); + Result maxHops = max(r -> r.getHopCount() > 0, Comparator.comparingInt(Result::getHopCount)); + Result maxPaths = max(r -> r.getPathCount() > 0, Comparator.comparingInt(Result::getPathCount)); + + // avg/median: + double avgPing = avg(Result::isSuccess, Result::getPingMs); + double avgHops = avg(r -> r.getHopCount() > 0, Result::getHopCount); + double avgPaths = avg(r -> r.getPathCount() > 0, Result::getPathCount); + double medianPing = median(Result::isSuccess, Result::getPingMs); + double medianHops = median(r -> r.getHopCount() > 0, Result::getHopCount); + double medianPaths = median(r -> r.getPathCount() > 0, Result::getPathCount); + + println(""); + println("Max hops = " + maxHops.getHopCount() + ": " + maxHops); + println("Max ping [ms] = " + round(maxPing.getPingMs(), 2) + ": " + maxPing); + println("Max paths = " + maxPaths.getPathCount() + ": " + maxPaths); + + println("Median hops = " + (int) medianHops); + println("Median ping [ms] = " + round(medianPing, 2)); + println("Median paths = " + (int) medianPaths); + + println("Avg hops = " + round(avgHops, 1)); + println("Avg ping [ms] = " + round(avgPing, 2)); + println("Avg paths = " + (int) round(avgPaths, 0)); + + println(""); + println("AS Stats:"); + println(" all = " + (nAsTried + 1)); // +1 for local AS + println(" success = " + nAsSuccess); + println(" no path = " + (nAsNoPathFound + 1)); // +1 for local AS + println(" timeout = " + nAsTimeout); + println(" error = " + nAsError); + println(" not listed = " + nSeenButNotListed); + println("Path Stats:"); + println(" all = " + nPathTried); + println(" success = " + nPathSuccess); + println(" timeout = " + nPathTimeout); + println("ICMP Stats:"); + println(" all = " + ICMP.nIcmpTried); + println(" success = " + ICMP.nIcmpSuccess); + println(" timeout = " + ICMP.nIcmpTimeout); + println(" error = " + ICMP.nIcmpError); + } + + private double avg(Predicate filter, ToDoubleFunction mapper) { + return results.stream().filter(filter).mapToDouble(mapper).average().orElse(-1); + } + + private Result max(Predicate filter, Comparator comparator) { + Result empty = new Result(new ParseAssignments.HostEntry(0, "dummy"), Result.State.ERROR); + return results.stream() + .filter(filter) + .max(comparator) + .orElse(empty); // orElseThrow(NoSuchElementException::new); + } + + private double median(Predicate filter, Function mapper) { + List list = + results.stream().filter(filter).map(mapper).sorted().collect(Collectors.toList()); + if (list.isEmpty()) { + return -1; + } + if (list.get(0) instanceof Double) { + return (Double) list.get(list.size() / 2); + } + return (Integer) list.get(list.size() / 2); + } +} diff --git a/src/main/java/org/scion/multiping/util/ScionProvider.java b/src/main/java/org/scion/multiping/util/ScionProvider.java new file mode 100644 index 0000000..2353bca --- /dev/null +++ b/src/main/java/org/scion/multiping/util/ScionProvider.java @@ -0,0 +1,145 @@ +// Copyright 2025 ETH Zurich +// +// Licensed 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. + +package org.scion.multiping.util; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import org.scion.jpan.*; + +public class ScionProvider { + + public interface Async extends AutoCloseable { + int sendTracerouteLast(Path path) throws IOException; + + void close() throws IOException; + } + + public interface Sync extends AutoCloseable { + Scmp.EchoMessage sendEchoRequest(Path path, ByteBuffer bb) throws IOException; + + List sendTracerouteRequest(Path path) throws IOException; + + void close() throws IOException; + } + + private static class AsyncDefault implements Async { + private final ScmpSenderAsync sender; + + AsyncDefault(ScmpSenderAsync sender) { + this.sender = sender; + } + + @Override + public int sendTracerouteLast(Path path) throws IOException { + return sender.sendTracerouteLast(path); + } + + @Override + public void close() throws IOException { + sender.close(); + } + } + + private static class SyncDefault implements Sync { + private final ScmpSender sender; + + SyncDefault(ScmpSender sender) { + this.sender = sender; + } + + @Override + public Scmp.EchoMessage sendEchoRequest(Path path, ByteBuffer bb) throws IOException { + return sender.sendEchoRequest(path, bb); + } + + @Override + public List sendTracerouteRequest(Path path) throws IOException { + return sender.sendTracerouteRequest(path); + } + + @Override + public void close() throws IOException { + sender.close(); + } + } + + private final Supplier senderSupplier; + private final Function senderAsyncSupplier; + private final Supplier> assignmentSupplier; + private final Supplier localIsdAsSupplier; + private final BiFunction> localDefaultPathsSupplier; + + public static ScionProvider defaultProvider(int localPort) { + return new ScionProvider( + () -> new SyncDefault(Scmp.newSenderBuilder().setLocalPort(localPort).build()), + handler -> + new AsyncDefault(Scmp.newSenderAsyncBuilder(handler).setLocalPort(localPort).build()), + DownloadAssignmentsFromWeb::getList, + () -> Scion.defaultService().getLocalIsdAs(), + (isdAs, address) -> Scion.defaultService().getPaths(isdAs, address)); + } + + public static ScionProvider createSync( + Supplier senderSupplier, + Function senderAsyncSupplier, + Supplier> assignmentSupplier, + Supplier localIsdAsSupplier, + BiFunction> localDefaultPathsSupplier) { + return new ScionProvider( + senderSupplier, + senderAsyncSupplier, + assignmentSupplier, + localIsdAsSupplier, + localDefaultPathsSupplier); + } + + private ScionProvider( + Supplier senderSupplier, + Function senderAsyncSupplier, + Supplier> assignmentSupplier, + Supplier localIsdAsSupplier, + BiFunction> localDefaultPathsSupplier) { + this.assignmentSupplier = assignmentSupplier; + this.senderSupplier = senderSupplier; + this.senderAsyncSupplier = senderAsyncSupplier; + this.localIsdAsSupplier = localIsdAsSupplier; + this.localDefaultPathsSupplier = localDefaultPathsSupplier; + } + + public List getIsdAsEntries() { + return assignmentSupplier.get(); + } + + public Async getAsync(ScmpSenderAsync.ResponseHandler handler) { + return senderAsyncSupplier.apply(handler); + } + + public Sync getSync() { + return senderSupplier.get(); + } + + public List getPaths(long isdAs, InetSocketAddress destinationAddress) { + return localDefaultPathsSupplier.apply(isdAs, destinationAddress); + } + + public long getLocalIsdAs() { + return localIsdAsSupplier.get(); + } +} diff --git a/src/test/java/org/scion/jpan/PathHelper.java b/src/test/java/org/scion/jpan/PathHelper.java new file mode 100644 index 0000000..d80ab14 --- /dev/null +++ b/src/test/java/org/scion/jpan/PathHelper.java @@ -0,0 +1,42 @@ +// Copyright 2025 ETH Zurich +// +// Licensed 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. + +package org.scion.jpan; + +import com.google.protobuf.ByteString; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.List; +import org.scion.jpan.proto.daemon.Daemon; + +public class PathHelper { + + private PathHelper() {} + + public static List createPaths(int n) { + List paths = new ArrayList<>(); + for (int i = 0; i < n; i++) { + Daemon.Path.Builder builder = Daemon.Path.newBuilder(); + builder.setRaw(ByteString.copyFrom(new byte[] {})); + try { + InetAddress dst = InetAddress.getByAddress(new byte[] {123, 123, 123, 123}); + paths.add(RequestPath.create(builder.build(), n + 1, dst, 12345)); + } catch (UnknownHostException e) { + throw new ScionRuntimeException("Unable to create test path", e); + } + } + return paths; + } +} diff --git a/src/test/java/org/scion/multiping/PingAllTest.java b/src/test/java/org/scion/multiping/PingAllTest.java new file mode 100644 index 0000000..18c61f0 --- /dev/null +++ b/src/test/java/org/scion/multiping/PingAllTest.java @@ -0,0 +1,187 @@ +// Copyright 2025 ETH Zurich +// +// Licensed 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. + +package org.scion.multiping; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; +import org.scion.jpan.*; +import org.scion.multiping.util.Helper; +import org.scion.multiping.util.ResultSummary; +import org.scion.multiping.util.ScionProvider; + +class PingAllTest { + + @FunctionalInterface + interface AsyncNoClose extends ScionProvider.Async { + @Override + default void close() {} + } + + static class WithHandler implements AsyncNoClose { + private int sequenceId = 0; + ScmpSenderAsync.ResponseHandler handler; + Consumer handlerConsumer; + + WithHandler( + ScmpSenderAsync.ResponseHandler handler, + Consumer handlerConsumer) { + this.handler = handler; + this.handlerConsumer = handlerConsumer; + // trigger handler + handlerConsumer.accept(handler); + } + + @Override + public int sendTracerouteLast(Path path) throws IOException { + return sequenceId++; + } + } + + static class WithIOError extends WithHandler { + WithIOError( + ScmpSenderAsync.ResponseHandler handler, + Consumer handlerConsumer) { + super(handler, handlerConsumer); + } + + @Override + public int sendTracerouteLast(Path path) throws IOException { + throw new IOException("SCMP error"); + } + } + + static class MySync implements ScionProvider.Sync { + private final int sent; + private int seqId = 0; + + MySync(int sent) { + this.sent = sent; + } + + @Override + public Scmp.EchoMessage sendEchoRequest(Path path, ByteBuffer bb) throws IOException { + Scmp.EchoMessage msg = Scmp.EchoMessage.createEmpty(path); + msg.setMessageArgs(Scmp.TypeCode.TYPE_129, seqId++, seqId); + msg.assignRequest(msg, 1_000_000); // Hack: assign to itself + return msg; + } + + @Override + public List sendTracerouteRequest(Path path) throws IOException { + List msgs = new ArrayList<>(); + int sID = seqId++; + for (int i = 0; i < sent; i++) { + Scmp.TracerouteMessage msg = + new Scmp.TracerouteMessage(Scmp.TypeCode.TYPE_131, sID, sID, 1, 1, path); + msg.assignRequest(msg, 1_000_000); // Hack: assign to itself + msgs.add(msg); + } + return msgs; + } + + @Override + public void close() {} + } + + @Test + void testPingTimeout() throws IOException { + List paths = PathHelper.createPaths(3); + class MyWithHandler extends WithHandler { + MyWithHandler(ScmpSenderAsync.ResponseHandler handler) { + super( + handler, + hdl -> { + for (int i = 0; i < 3; i++) { + Scmp.TimedMessage req = Scmp.TracerouteMessage.createRequest(i, paths.get(i)); + Scmp.TimedMessage msg = Scmp.TracerouteMessage.createEmpty(paths.get(i)); + msg.setMessageArgs(Scmp.TypeCode.TYPE_131, i, i); + msg.assignRequest(req, 1_000_000); // Hack: assign to itself + msg.setTimedOut(1000 * 1000 * 1000); + hdl.onTimeout(msg); + } + }); + } + } + + ScionProvider p = + ScionProvider.createSync( + () -> new MySync(3), + h -> new MyWithHandler(h), + Helper::isdAsList, + () -> Long.valueOf(0), + (ia, addr) -> PathHelper.createPaths(3)); + PingAll ping = new PingAll(PingAll.Policy.FASTEST_TR_ASYNC, p); + ResultSummary summary = ping.run(); + assertEquals(3, summary.getAsTimeouts()); + assertEquals(9, summary.getPathTimeouts()); + } + + @Test + void testPingErrorCode() throws IOException { + List paths = PathHelper.createPaths(3); + class MyWithHandler extends WithHandler { + MyWithHandler(ScmpSenderAsync.ResponseHandler handler) { + super( + handler, + hdl -> { + for (int i = 0; i < 3; i++) { + Scmp.ErrorMessage msg = + Scmp.ErrorMessage.createEmpty(Scmp.TypeCode.TYPE_5, paths.get(i)); + hdl.onError(msg); + } + }); + } + } + + ScionProvider p = + ScionProvider.createSync( + () -> new MySync(3), + h -> new MyWithHandler(h), + Helper::isdAsList, + () -> Long.valueOf(0), + (ia, addr) -> PathHelper.createPaths(3)); + PingAll ping = new PingAll(PingAll.Policy.FASTEST_TR_ASYNC, p); + ResultSummary summary = ping.run(); + assertEquals(0, summary.getMaxPaths().getPathCount()); + assertEquals(3, summary.getAsErrors()); + } + + @Test + void testPingSendingError() throws IOException { + class MyWithHandler extends WithIOError { + MyWithHandler(ScmpSenderAsync.ResponseHandler handler) { + super(handler, hdl -> {}); + } + } + + ScionProvider p = + ScionProvider.createSync( + () -> new MySync(3), + h -> new MyWithHandler(h), + Helper::isdAsList, + () -> Long.valueOf(0), + (ia, addr) -> PathHelper.createPaths(3)); + PingAll ping = new PingAll(PingAll.Policy.FASTEST_TR_ASYNC, p); + ResultSummary summary = ping.run(); + assertEquals(0, summary.getMaxPaths().getPathCount()); + assertEquals(3, summary.getAsErrors()); + } +} diff --git a/src/test/java/org/scion/multiping/util/Helper.java b/src/test/java/org/scion/multiping/util/Helper.java new file mode 100644 index 0000000..343be49 --- /dev/null +++ b/src/test/java/org/scion/multiping/util/Helper.java @@ -0,0 +1,30 @@ +// Copyright 2025 ETH Zurich +// +// Licensed 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. + +package org.scion.multiping.util; + +import java.util.ArrayList; +import java.util.List; +import org.scion.jpan.ScionUtil; + +public class Helper { + + public static List isdAsList() { + List result = new ArrayList<>(); + result.add(new ParseAssignments.HostEntry(ScionUtil.parseIA("1-123"), "AS-1-123")); + result.add(new ParseAssignments.HostEntry(ScionUtil.parseIA("1-234"), "AS-1-234")); + result.add(new ParseAssignments.HostEntry(ScionUtil.parseIA("2-123"), "AS-2-123")); + return result; + } +}