From 1ae3c0c329ee253609e3c8b710ea6401dd957be0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tilmann=20Z=C3=A4schke?= Date: Tue, 23 Apr 2024 17:34:46 +0200 Subject: [PATCH] Improved ScmpChannel --- CHANGELOG.md | 2 + .../scion/jpan/AbstractDatagramChannel.java | 9 +- src/main/java/org/scion/jpan/ScionUtil.java | 54 ++++ src/main/java/org/scion/jpan/ScmpChannel.java | 12 + .../org/scion/jpan/internal/ByteUtil.java | 6 +- .../scion/jpan/internal/PathRawParser.java | 240 ++++++++++++++++++ .../scion/jpan/PackageVisibilityHelper.java | 6 + .../org/scion/jpan/api/ScionUtilTest.java | 47 +++- .../org/scion/jpan/demo/ScmpEchoDemo.java | 46 ++-- 9 files changed, 398 insertions(+), 24 deletions(-) create mode 100644 src/main/java/org/scion/jpan/internal/PathRawParser.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 7546765f7..2bda74f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. [#46](https://github.com/netsec-ethz/scion-java-client/pull/46) - Support for comments, multiple spaces and tabs in `/etc/scion/hosts`. [#47](https://github.com/netsec-ethz/scion-java-client/pull/47) +- Added helper methods for `ScmpChannel` and `ScionUtil.toStringPath()`. + [#48](https://github.com/netsec-ethz/scion-java-client/pull/48) ### Changed - BREAKING CHANGE: Changed maven artifactId to "client" diff --git a/src/main/java/org/scion/jpan/AbstractDatagramChannel.java b/src/main/java/org/scion/jpan/AbstractDatagramChannel.java index fad4496dc..85a86825e 100644 --- a/src/main/java/org/scion/jpan/AbstractDatagramChannel.java +++ b/src/main/java/org/scion/jpan/AbstractDatagramChannel.java @@ -166,7 +166,14 @@ public InetSocketAddress getLocalAddress() throws IOException { } } - public SocketAddress getRemoteAddress() { + /** + * Returns the remote address. + * + * @see DatagramChannel#getRemoteAddress() + * @return The remote address. + * @throws IOException If an I/O error occurs + */ + public InetSocketAddress getRemoteAddress() throws IOException { Path path = getConnectionPath(); if (path != null) { return new InetSocketAddress(path.getDestinationAddress(), path.getDestinationPort()); diff --git a/src/main/java/org/scion/jpan/ScionUtil.java b/src/main/java/org/scion/jpan/ScionUtil.java index c67dcba8e..c7ebcad17 100644 --- a/src/main/java/org/scion/jpan/ScionUtil.java +++ b/src/main/java/org/scion/jpan/ScionUtil.java @@ -14,6 +14,9 @@ package org.scion.jpan; +import java.nio.ByteBuffer; +import org.scion.jpan.internal.PathRawParser; + /** Scion utility functions. */ public class ScionUtil { @@ -95,6 +98,57 @@ public static String toStringIA(int isd, long as) { return s; } + public static String toStringPath(byte[] raw) { + if (raw.length == 0) { + return "[]"; + } + PathRawParser ph = new PathRawParser(); + ph.read(ByteBuffer.wrap(raw)); + StringBuilder sb = new StringBuilder(); + sb.append("["); + int[] segLen = {ph.getSegLen(0), ph.getSegLen(1), ph.getSegLen(2)}; + int offset = 0; + for (int j = 0; j < segLen.length; j++) { + boolean flagC = ph.getInfoField(j).getFlagC(); + for (int i = offset; i < offset + segLen[j] - 1; i++) { + PathRawParser.HopField hfE = ph.getHopField(i); + PathRawParser.HopField hfI = ph.getHopField(i + 1); + if (flagC) { + sb.append(hfE.getEgress()).append(">").append(hfI.getIngress()); + } else { + sb.append(hfE.getIngress()).append(">").append(hfI.getEgress()); + } + if (i < ph.getHopCount() - 2) { + sb.append(" "); + } + } + offset += segLen[j]; + } + sb.append("]"); + return sb.toString(); + } + + public static String toStringPath(RequestPath path) { + if (path.getInterfacesList().isEmpty()) { + return "[]"; + } + StringBuilder sb = new StringBuilder(); + sb.append("["); + int nInterfcaces = path.getInterfacesList().size(); + for (int i = 0; i < nInterfcaces; i++) { + RequestPath.PathInterface pIf = path.getInterfacesList().get(i); + if (i % 2 == 0) { + sb.append(ScionUtil.toStringIA(pIf.getIsdAs())).append(" "); + sb.append(pIf.getId()).append(">"); + } else { + sb.append(pIf.getId()).append(" "); + } + } + sb.append(ScionUtil.toStringIA(path.getInterfacesList().get(nInterfcaces - 1).getIsdAs())); + sb.append("]"); + return sb.toString(); + } + private static void checkLimits(int isd, long as) { if (isd < 0 || isd > MaxISD) { throw new IllegalArgumentException("ISD out of range: " + isd); diff --git a/src/main/java/org/scion/jpan/ScmpChannel.java b/src/main/java/org/scion/jpan/ScmpChannel.java index 81d54ac64..04f15ed8e 100644 --- a/src/main/java/org/scion/jpan/ScmpChannel.java +++ b/src/main/java/org/scion/jpan/ScmpChannel.java @@ -244,4 +244,16 @@ public void close() throws IOException { selector.close(); } } + + public RequestPath getConnectionPath() { + return (RequestPath) channel.getConnectionPath(); + } + + public InetSocketAddress getLocalAddress() throws IOException { + return channel.getLocalAddress(); + } + + public InetSocketAddress getRemoteAddress() throws IOException { + return channel.getRemoteAddress(); + } } diff --git a/src/main/java/org/scion/jpan/internal/ByteUtil.java b/src/main/java/org/scion/jpan/internal/ByteUtil.java index 48f1d9a2d..6929ce57e 100644 --- a/src/main/java/org/scion/jpan/internal/ByteUtil.java +++ b/src/main/java/org/scion/jpan/internal/ByteUtil.java @@ -43,19 +43,19 @@ public void set(long l) { * @param bitCount number of bits to read * @return extracted bits as int. */ - static int readInt(int input, int bitOffset, int bitCount) { + public static int readInt(int input, int bitOffset, int bitCount) { int mask = (-1) >>> (32 - bitCount); int shift = 32 - bitOffset - bitCount; return (input >>> shift) & mask; } - static long readLong(long input, int bitOffset, int bitCount) { + public static long readLong(long input, int bitOffset, int bitCount) { long mask = (-1L) >>> (64 - bitCount); int shift = 64 - bitOffset - bitCount; return (input >>> shift) & mask; } - static boolean readBoolean(int input, int bitOffset) { + public static boolean readBoolean(int input, int bitOffset) { int mask = 1; int shift = 32 - bitOffset - 1; return ((input >>> shift) & mask) != 0; diff --git a/src/main/java/org/scion/jpan/internal/PathRawParser.java b/src/main/java/org/scion/jpan/internal/PathRawParser.java new file mode 100644 index 000000000..9d01f64fc --- /dev/null +++ b/src/main/java/org/scion/jpan/internal/PathRawParser.java @@ -0,0 +1,240 @@ +// Copyright 2023 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.internal; + +import static org.scion.jpan.internal.ByteUtil.readBoolean; +import static org.scion.jpan.internal.ByteUtil.readInt; +import static org.scion.jpan.internal.ByteUtil.readLong; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +public class PathRawParser { + private static final String NL = System.lineSeparator(); + + // 2 bit : (C)urrINF : 2-bits index (0-based) pointing to the current info field (see offset + // calculations below). + private int currINF; + // 6 bit : CurrHF : 6-bits index (0-based) pointing to the current hop field (see offset + // calculations below). + private int currHF; + // 6 bit : RSV + private int reserved; + // Up to 3 Info fields and up to 64 Hop fields + // The number of hop fields in a given segment. Seg,Len > 0 implies the existence of info field i. + // 6 bit : Seg0Len + // 6 bit : Seg1Len + // 6 bit : Seg2Len + private final int[] segLen = new int[3]; + private final InfoField[] info = new InfoField[3]; + + private final HopField[] hops = new HopField[64]; + private int nHops; + + private int len; + + public PathRawParser() { + this.info[0] = new InfoField(); + this.info[1] = new InfoField(); + this.info[2] = new InfoField(); + Arrays.setAll(hops, value -> new HopField()); + } + + public void read(ByteBuffer data) { + int start = data.position(); + // 2 bit : (C)urrINF : 2-bits index (0-based) pointing to the current info field (see offset + // calculations below). + // 6 bit : CurrHF : 6-bits index (0-based) pointing to the current hop field (see offset + // calculations below). + // 6 bit : RSV + // Up to 3 Info fields and up to 64 Hop fields + // The number of hop fields in a segment. SegLen > 0 implies the existence of info field i. + // 6 bit : Seg0Len + // 6 bit : Seg1Len + // 6 bit : Seg2Len + + int i0 = data.getInt(); + currINF = readInt(i0, 0, 2); + currHF = readInt(i0, 2, 6); + reserved = readInt(i0, 8, 6); + segLen[0] = readInt(i0, 14, 6); + segLen[1] = readInt(i0, 20, 6); + segLen[2] = readInt(i0, 26, 6); + + for (int i = 0; i < segLen.length && segLen[i] > 0; i++) { + info[i].read(data); + } + + nHops = segLen[0] + segLen[1] + segLen[2]; + for (int i = 0; i < nHops; i++) { + hops[i].read(data); + } + + len = data.position() - start; + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(); + s.append("currINF=").append(currINF).append(" currHP=").append(currHF); + s.append(" reserved=").append(reserved); + for (int i = 0; i < segLen.length; i++) { + s.append(" seg").append(i).append("Len=").append(segLen[i]); + } + for (int i = 0; i < segLen.length; i++) { + if (segLen[i] > 0) { + s.append(NL).append(" info").append(i).append("=").append(info[i]); + } + } + for (int i = 0; i < nHops; i++) { + s.append(NL).append(" hop=").append(hops[i]); + } + return s.toString(); + } + + public int length() { + return len; + } + + public InfoField getInfoField(int i) { + return info[i]; + } + + public int getHopCount() { + return nHops; + } + + public HopField getHopField(int i) { + return hops[i]; + } + + public int getSegLen(int i) { + return segLen[i]; + } + + public static class HopField { + + /** + * 1 bit : ConsIngress Router Alert. If the ConsIngress Router Alert is set, the ingress router + * (in construction direction) will process the L4 payload in the packet. + */ + private boolean flagI; + + /** + * 1 bit : ConsEgress Router Alert. If the ConsEgress Router Alert is set, the egress router (in + * construction direction) will process the L4 payload in the packet. + */ + private boolean flagE; + + /** + * 8 bits : Expiry time of a hop field. The expiration time expressed is relative. An absolute + * expiration time in seconds is computed in combination with the timestamp field (from the + * corresponding info field) as follows: abs_time = timestamp + (1+expiryTime)*24*60*60/256 + */ + private int expiryTime; + + // 16 bits : consIngress : The 16-bits ingress interface IDs in construction direction. + private int consIngress; + // 16 bits : consEgress : The 16-bits egress interface IDs in construction direction. + private int consEgress; + // 48 bits : MAC : 6-byte Message Authentication Code to authenticate the hop field. + // For details on how this MAC is calculated refer to Hop Field MAC Computation: + // https://scion.docs.anapaya.net/en/latest/protocols/scion-header.html#hop-field-mac-computation + private long mac; + + HopField() {} + + public void read(ByteBuffer data) { + int i0 = data.getInt(); + long l1 = data.getLong(); + flagI = readBoolean(i0, 6); + flagE = readBoolean(i0, 7); + expiryTime = readInt(i0, 8, 8); + consIngress = readInt(i0, 16, 16); + consEgress = (int) readLong(l1, 0, 16); + mac = readLong(l1, 16, 48); + } + + @Override + public String toString() { + String s = "I=" + flagI + ", E=" + flagE + ", expiryTime=" + expiryTime; + s += ", consIngress=" + consIngress + ", consEgress=" + consEgress + ", mac=" + mac; + return s; + } + + public int length() { + return 12; + } + + public int getIngress() { + return consIngress; + } + + public int getEgress() { + return consEgress; + } + + public boolean hasIngressAlert() { + return flagI; + } + + public boolean hasEgressAlert() { + return flagE; + } + } + + public static class InfoField { + + private boolean p; + private boolean c; + // 16 bits : segID + private int segID; + // 32 bits : timestamp (unsigned int) + private int timestampRaw; // "raw" because field type is "signed int" + + InfoField() {} + + public void read(ByteBuffer data) { + int i0 = data.getInt(); + int i1 = data.getInt(); + p = readBoolean(i0, 6); + c = readBoolean(i0, 7); + segID = readInt(i0, 16, 16); + timestampRaw = i1; + } + + public int length() { + return 8; + } + + @Override + public String toString() { + long ts = Integer.toUnsignedLong(timestampRaw); + return "P=" + p + ", C=" + c + ", segID=" + segID + ", timestamp=" + ts; + } + + public long getTimestamp() { + return Integer.toUnsignedLong(timestampRaw); + } + + public boolean hasConstructionDirection() { + return c; + } + + public boolean getFlagC() { + return c; + } + } +} diff --git a/src/test/java/org/scion/jpan/PackageVisibilityHelper.java b/src/test/java/org/scion/jpan/PackageVisibilityHelper.java index e4ce89abc..ea0ec85c0 100644 --- a/src/test/java/org/scion/jpan/PackageVisibilityHelper.java +++ b/src/test/java/org/scion/jpan/PackageVisibilityHelper.java @@ -109,6 +109,12 @@ public static ResponsePath createDummyResponsePath( } } + public static RequestPath createRequestPath110_110( + Daemon.Path.Builder builder, long isdAs, InetAddress dstHost, int dstPort) { + Daemon.Path path = builder.build(); + return RequestPath.create(path, isdAs, dstHost, dstPort); + } + public static RequestPath createRequestPath110_112( Daemon.Path.Builder builder, long dstIsdAs, diff --git a/src/test/java/org/scion/jpan/api/ScionUtilTest.java b/src/test/java/org/scion/jpan/api/ScionUtilTest.java index a11251b12..1d883f639 100644 --- a/src/test/java/org/scion/jpan/api/ScionUtilTest.java +++ b/src/test/java/org/scion/jpan/api/ScionUtilTest.java @@ -16,9 +16,15 @@ import static org.junit.jupiter.api.Assertions.*; +import java.net.InetAddress; +import java.net.UnknownHostException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.scion.jpan.PackageVisibilityHelper; +import org.scion.jpan.RequestPath; import org.scion.jpan.ScionUtil; +import org.scion.jpan.proto.daemon.Daemon; +import org.scion.jpan.testutil.ExamplePacket; class ScionUtilTest { @@ -35,7 +41,7 @@ void testParseIA() { } @Test - void testToStringIA() { + void toStringIA() { assertEquals("0-0:0:0", ScionUtil.toStringIA(0)); assertEquals("42-0:0:0", ScionUtil.toStringIA(42L << 48)); assertEquals("0-fedc:0:0", ScionUtil.toStringIA(0xfedcL << 32)); @@ -46,7 +52,7 @@ void testToStringIA() { } @Test - void testToStringIA2() { + void toStringIA2() { assertEquals("0-0:0:0", ScionUtil.toStringIA(0, 0)); assertEquals("42-0:0:0", ScionUtil.toStringIA(42, 0)); assertEquals("0-fedc:0:0", ScionUtil.toStringIA(0, 0xfedcL << 32)); @@ -57,7 +63,7 @@ void testToStringIA2() { } @Test - void testToStringIA2_fails() { + void toStringIA2_fails() { Exception exception; exception = assertThrows(IllegalArgumentException.class, () -> ScionUtil.toStringIA(-1, 0)); assertTrue(exception.getMessage().contains("ISD out of range")); @@ -72,7 +78,7 @@ void testToStringIA2_fails() { } @Test - void testExtractAS() { + void extractAs() { assertEquals(0L, ScionUtil.extractAs(0)); assertEquals(0L, ScionUtil.extractAs((42L << 48))); assertEquals(0xfedcba987654L, ScionUtil.extractAs((42L << 48) + 0xfedcba987654L)); @@ -80,10 +86,41 @@ void testExtractAS() { } @Test - void testExtractISD() { + void extractIsd() { assertEquals(0, ScionUtil.extractIsd(0)); assertEquals(42, ScionUtil.extractIsd((42L << 48))); assertEquals(42, ScionUtil.extractIsd((42L << 48) | 0xfedcba987654L)); assertEquals(65000, ScionUtil.extractIsd((65000L << 48) | 0xfedcba987654L)); } + + @Test + void toStringPath_requestPath() throws UnknownHostException { + RequestPath pathLocal = createRequestPathLocal(); + assertEquals("[]", ScionUtil.toStringPath(pathLocal)); + RequestPath pathRemote = createRequestPathRemote(); + assertEquals("[1-ff00:0:110 2>1 1-ff00:0:112]", ScionUtil.toStringPath(pathRemote)); + } + + private RequestPath createRequestPathLocal() throws UnknownHostException { + return PackageVisibilityHelper.createRequestPath110_110( + Daemon.Path.newBuilder(), + ExamplePacket.SRC_IA, + InetAddress.getByAddress(ExamplePacket.SRC_HOST), + 12345); + } + + private RequestPath createRequestPathRemote() throws UnknownHostException { + return PackageVisibilityHelper.createRequestPath110_112( + Daemon.Path.newBuilder(), + ExamplePacket.DST_IA, + InetAddress.getByAddress(ExamplePacket.DST_HOST), + 12345, + ExamplePacket.FIRST_HOP); + } + + @Test + void toStringPath_raw() { + assertEquals("[]", ScionUtil.toStringPath(new byte[] {})); + assertEquals("[2>1]", ScionUtil.toStringPath(ExamplePacket.PATH_RAW_TINY_110_112)); + } } diff --git a/src/test/java/org/scion/jpan/demo/ScmpEchoDemo.java b/src/test/java/org/scion/jpan/demo/ScmpEchoDemo.java index 1ec3aed0e..936690558 100644 --- a/src/test/java/org/scion/jpan/demo/ScmpEchoDemo.java +++ b/src/test/java/org/scion/jpan/demo/ScmpEchoDemo.java @@ -104,15 +104,21 @@ private void runDemo(long dstIA, InetSocketAddress dstAddress) throws IOExceptio println("Listening at port " + localPort + " ..."); + try (DatagramChannel channel = DatagramChannel.open()) { + channel.connect(path); + println("Resolved local address: "); + println(" " + channel.getLocalAddress().getAddress().getHostAddress()); + } + try (ScmpChannel scmpChannel = Scmp.createChannel(path, localPort)) { for (int i = 0; i < 10; i++) { Scmp.EchoMessage msg = scmpChannel.sendEchoRequest(i, data); if (i == 0) { - printHeader(dstIA, dstAddress, data, msg); + printHeader(scmpChannel, dstIA, dstAddress, data, msg); } String millis = String.format("%.3f", msg.getNanoSeconds() / (double) 1_000_000); String echoMsgStr = msg.getSizeReceived() + " bytes from "; - // TODO proper address + // TODO get actual address from response InetAddress addr = msg.getPath().getDestinationAddress(); echoMsgStr += ScionUtil.toStringIA(dstIA) + "," + addr.getHostAddress(); echoMsgStr += ": scmp_seq=" + msg.getSequenceNumber(); @@ -131,19 +137,29 @@ private void runDemo(long dstIA, InetSocketAddress dstAddress) throws IOExceptio } private void printHeader( - long dstIA, InetSocketAddress dstAddress, ByteBuffer data, Scmp.EchoMessage msg) { - println( - "PING " - + ScionUtil.toStringIA(dstIA) - + "," - + dstAddress.getHostString() - + ":" - + dstAddress.getPort() - + " pld=" - + data.remaining() - + "B scion_pkt=" - + msg.getSizeSent() - + "B"); + ScmpChannel channel, + long dstIA, + InetSocketAddress dstAddress, + ByteBuffer data, + Scmp.EchoMessage msg) + throws IOException { + String nl = System.lineSeparator(); + StringBuilder sb = new StringBuilder(); + sb.append("Actual local address:").append(nl); + sb.append(" ").append(channel.getLocalAddress().getAddress().getHostAddress()).append(nl); + + RequestPath path = channel.getConnectionPath(); + sb.append("Using path:").append(nl); + sb.append(" Hops:").append(ScionUtil.toStringPath(path)); + sb.append(" MTU: ").append(path.getMtu()); + sb.append(" NextHop: ").append(path.getInterface().getAddress()).append(nl); + + sb.append("PING ").append(ScionUtil.toStringIA(dstIA)).append(","); + sb.append(dstAddress.getHostString()).append(":").append(dstAddress.getPort()); + sb.append(" pld=").append(data.remaining()); + sb.append("B scion_pkt=").append(msg.getSizeSent()).append("B"); + + println(sb.toString()); } private static void println(String msg) {