diff --git a/CHANGELOG.md b/CHANGELOG.md index a5e5cec7e..ee282cd7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Changed - BREAKING CHANGE: Changed maven artifactId to "client" [#9](https://github.com/tzaeschke/phtree-cpp/pull/9) +- BREAKING CHANGE: SCMP refactoring, renamed several SCMP related classes. + [#14](https://github.com/tzaeschke/phtree-cpp/pull/14) ### Fixed - Fixed SCMP timeout and error handling (IOExceptions + SCMP errors). diff --git a/README.md b/README.md index c653ad73f..d6079ff45 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,10 @@ The central classes of the API are: returned by `Scion.defaultService()`. - `Scion`, `ScionUtil`, `ScionConstants`: Utility classes. - `ScionSocketOptions`: Options for the `DatagramChannel`. -- `SCMP` provides `ScmpType` and `ScmpCode` enums with text messages. It also contains - `ScmpMessage` (for SCMP errors) and `ScmpEcho`/`ScmpTraceroute` types. These can be used with the - `DatagramChannel`'s `sendXXXRequest()` and `setXXXListener()` methods. +- `Scmp`: + - `ScmpType` and `ScmpCode` enums with text messages. + - `Message` (for SCMP errors) and `EchoMessage`/`TracerouteMessage` types. + - `createChannel(...)` for sending echo and traceroute requests - **TODO** Currently residing in `test`: `ScionPacketInspector`: A packet inspector and builder. - **TODO** `DatagramSocket` and `DatagramPacket`: These work similar to the old `java.net.DatagramSocket`. This is currently deprecated because it does not work well. diff --git a/TODO.md b/TODO.md index c6d56b050..a01177bcb 100644 --- a/TODO.md +++ b/TODO.md @@ -94,6 +94,7 @@ Discuss required for 0.1.0: - Selector support - Implement interfaces from nio.DatagramChannel - Look into Selectors: https://www.baeldung.com/java-nio-selector +- Consider sublcassing DatagramChannel directly. - Consider SHIM support. SHIM is a compatibility component that supports old border-router software (requiring a fixed port on the client, unless the client is listening on this very port). When SHIM is used, we cannot diff --git a/doc/Design.md b/doc/Design.md index ea884172c..be2b6efce 100644 --- a/doc/Design.md +++ b/doc/Design.md @@ -8,6 +8,7 @@ We should look at other custom Java protocol implementations, e.g. for QUIC: ## Daemon The implementation can use the daemon. Alternatively, since daemon installation may be cumbersome on platforms such as Android, we can directly connect to a control service. + An alternative for mobile devices could be a non-local daemon that is hosted by the mobile provider. This may work but may open opportunities for side-channel attacks on the daemon. Also, when roaming, the provider may not actually support SCION. In this case the @@ -27,7 +28,7 @@ at least a topology server + control server. - Use Junit 5. - Use Google style guide for code - We use custom exceptions. However, to make the API as compatible with standard networking API - as possible, our Exceptions extend either IOException or RuntimeExcdeption. + as possible, our Exceptions extends either IOException or RuntimeException. ## Paths diff --git a/src/main/java/org/scion/AbstractDatagramChannel.java b/src/main/java/org/scion/AbstractDatagramChannel.java new file mode 100644 index 000000000..a21b328cf --- /dev/null +++ b/src/main/java/org/scion/AbstractDatagramChannel.java @@ -0,0 +1,398 @@ +// 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; + +import java.io.Closeable; +import java.io.IOException; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.AlreadyConnectedException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.DatagramChannel; +import java.nio.channels.NotYetConnectedException; +import java.time.Instant; +import java.util.function.Consumer; +import org.scion.internal.ExtensionHeader; +import org.scion.internal.InternalConstants; +import org.scion.internal.ScionHeaderParser; +import org.scion.internal.ScmpParser; + +abstract class AbstractDatagramChannel> implements Closeable { + + private final java.nio.channels.DatagramChannel channel; + private boolean isConnected = false; + private InetSocketAddress connection; + private RequestPath path; + private boolean cfgReportFailedValidation = false; + private PathPolicy pathPolicy = PathPolicy.DEFAULT; + private ScionService service; + private int cfgExpirationSafetyMargin = + ScionUtil.getPropertyOrEnv( + Constants.PROPERTY_PATH_EXPIRY_MARGIN, + Constants.ENV_PATH_EXPIRY_MARGIN, + Constants.DEFAULT_PATH_EXPIRY_MARGIN); + private Consumer errorListener; + + protected AbstractDatagramChannel(ScionService service) throws IOException { + this.channel = java.nio.channels.DatagramChannel.open(); + this.service = service; + } + + protected synchronized void configureBlocking(boolean block) throws IOException { + channel.configureBlocking(block); + } + + protected synchronized boolean isBlocking() { + return channel.isBlocking(); + } + + public synchronized PathPolicy getPathPolicy() { + return this.pathPolicy; + } + + /** + * Set the path policy. The default path policy is set in PathPolicy.DEFAULT, which currently + * means to use the first path returned by the daemon or control service. If the channel is + * connected, this method will request a new path using the new policy. + * + * @param pathPolicy the new path policy + * @see PathPolicy#DEFAULT + */ + public synchronized void setPathPolicy(PathPolicy pathPolicy) throws IOException { + this.pathPolicy = pathPolicy; + if (path != null) { + updatePath(path); + } + } + + public synchronized ScionService getService() { + if (service == null) { + service = Scion.defaultService(); + } + return this.service; + } + + public synchronized void setService(ScionService service) { + this.service = service; + } + + protected DatagramChannel channel() { + return channel; + } + + @SuppressWarnings("unchecked") + public synchronized C bind(InetSocketAddress address) throws IOException { + channel.bind(address); + return (C) this; + } + + public synchronized InetSocketAddress getLocalAddress() throws IOException { + return (InetSocketAddress) channel.getLocalAddress(); + } + + public synchronized void disconnect() throws IOException { + channel.disconnect(); + connection = null; + isConnected = false; + path = null; + } + + public synchronized boolean isOpen() { + return channel.isOpen(); + } + + @Override + // TODO not synchronized yet, think about a more fine grained lock (see JDK Channel impl) + public void close() throws IOException { + channel.disconnect(); + channel.close(); + isConnected = false; + connection = null; + path = null; + } + + /** + * Connect to a destination host. Note: - A SCION channel will internally connect to the next + * border router (first hop) instead of the remote host. + * + *

NB: This method does internally no call {@link java.nio.channels.DatagramChannel}.connect(), + * instead it calls bind(). That means this method does NOT perform any additional security checks + * associated with connect(), only those associated with bind(). + * + * @param addr Address of remote host. + * @return This channel. + * @throws IOException for example when the first hop (border router) cannot be connected. + */ + public synchronized C connect(SocketAddress addr) throws IOException { + checkConnected(false); + if (!(addr instanceof InetSocketAddress)) { + throw new IllegalArgumentException( + "connect() requires an InetSocketAddress or a ScionSocketAddress."); + } + return connect(pathPolicy.filter(getService().getPaths((InetSocketAddress) addr))); + } + + /** + * Connect to a destination host. Note: - A SCION channel will internally connect to the next + * border router (first hop) instead of the remote host. - The path will be replaced with a new + * path once it is expired. + * + *

NB: This method does internally no call {@link java.nio.channels.DatagramChannel}.connect(), + * instead it calls bind(). That means this method does NOT perform any additional security checks + * associated with connect(), only those associated with bind(). + * + * @param path Path to the remote host. + * @return This channel. + * @throws IOException for example when the first hop (border router) cannot be connected. + */ + @SuppressWarnings("unchecked") + public synchronized C connect(RequestPath path) throws IOException { + checkConnected(false); + this.path = path; + isConnected = true; + connection = path.getFirstHopAddress(); + channel.connect(connection); + return (C) this; + } + + /** + * Get the currently connected path. + * + * @return the current Path or `null` if not path is connected. + */ + public synchronized Path getCurrentPath() { + return path; + } + + protected void setPath(RequestPath path) { + this.path = path; + } + + protected ResponsePath receiveFromChannel( + ByteBuffer buffer, InternalConstants.HdrTypes expectedHdrType) throws IOException { + while (true) { + buffer.clear(); + InetSocketAddress srcAddress = (InetSocketAddress) channel.receive(buffer); + if (srcAddress == null) { + // this indicates nothing is available - non-blocking mode + return null; + } + buffer.flip(); + + String validationResult = ScionHeaderParser.validate(buffer.asReadOnlyBuffer()); + if (validationResult != null) { + if (cfgReportFailedValidation) { + throw new ScionException(validationResult); + } + continue; + } + + InternalConstants.HdrTypes hdrType = ScionHeaderParser.extractNextHeader(buffer); + + // From here on we use linear reading using the buffer's position() mechanism + buffer.position(ScionHeaderParser.extractHeaderLength(buffer)); + // Check for extension headers. + // This should be mostly unnecessary, however we sometimes saw SCMP error headers wrapped + // in extensions headers. + hdrType = receiveExtensionHeader(buffer, hdrType); + + if (hdrType == expectedHdrType) { + return ScionHeaderParser.extractResponsePath(buffer, srcAddress); + } + receiveScmp(buffer, path); + } + } + + private InternalConstants.HdrTypes receiveExtensionHeader( + ByteBuffer buffer, InternalConstants.HdrTypes hdrType) { + if (hdrType == InternalConstants.HdrTypes.END_TO_END + || hdrType == InternalConstants.HdrTypes.HOP_BY_HOP) { + ExtensionHeader extHdr = ExtensionHeader.consume(buffer); + // Currently we are not doing much here except hoping for an SCMP header + hdrType = extHdr.nextHdr(); + if (hdrType != InternalConstants.HdrTypes.SCMP) { + throw new UnsupportedOperationException("Extension header not supported: " + hdrType); + } + } + return hdrType; + } + + private void receiveScmp(ByteBuffer buffer, Path path) { + Scmp.ScmpType type = ScmpParser.extractType(buffer); + checkListeners(ScmpParser.consume(buffer, Scmp.createMessage(type, path))); + } + + protected void checkListeners(Scmp.Message scmpMsg) { + if (errorListener != null && scmpMsg.getTypeCode().isError()) { + errorListener.accept(scmpMsg); + } + } + + protected void sendRaw(ByteBuffer buffer, InetSocketAddress address) throws IOException { + channel.send(buffer, address); + } + + public synchronized Consumer setScmpErrorListener(Consumer listener) { + Consumer old = errorListener; + errorListener = listener; + return old; + } + + protected void checkOpen() throws ClosedChannelException { + if (!channel.isOpen()) { + throw new ClosedChannelException(); + } + } + + protected void checkConnected(boolean requiredState) { + if (requiredState != isConnected) { + if (isConnected) { + throw new AlreadyConnectedException(); + } else { + throw new NotYetConnectedException(); + } + } + } + + public synchronized boolean isConnected() { + return isConnected; + } + + @SuppressWarnings("unchecked") + public synchronized T getOption(SocketOption option) throws IOException { + if (option instanceof ScionSocketOptions.SciSocketOption) { + if (ScionSocketOptions.SN_API_THROW_PARSER_FAILURE.equals(option)) { + return (T) (Boolean) cfgReportFailedValidation; + } else if (ScionSocketOptions.SN_PATH_EXPIRY_MARGIN.equals(option)) { + return (T) (Integer) cfgExpirationSafetyMargin; + } else { + throw new UnsupportedOperationException(); + } + } + return channel.getOption(option); + } + + @SuppressWarnings("unchecked") + public synchronized C setOption(SocketOption option, T t) throws IOException { + if (option instanceof ScionSocketOptions.SciSocketOption) { + if (ScionSocketOptions.SN_API_THROW_PARSER_FAILURE.equals(option)) { + cfgReportFailedValidation = (Boolean) t; + } else if (ScionSocketOptions.SN_PATH_EXPIRY_MARGIN.equals(option)) { + cfgExpirationSafetyMargin = (Integer) t; + } else { + throw new UnsupportedOperationException(); + } + } else if (StandardSocketOptions.SO_RCVBUF.equals(option)) { + // TODO resize buf + channel.setOption(option, t); + } else if (StandardSocketOptions.SO_SNDBUF.equals(option)) { + // TODO resize buf + channel.setOption(option, t); + } else { + channel.setOption(option, t); + } + return (C) this; + } + + /** + * @param path path + * @param payloadLength payload length + * @return argument path or a new path if the argument path was expired + * @throws IOException in case of IOException. + */ + protected Path buildHeader( + ByteBuffer buffer, Path path, int payloadLength, InternalConstants.HdrTypes hdrType) + throws IOException { + if (path instanceof RequestPath) { + path = ensureUpToDate((RequestPath) path); + } + buildHeaderNoRefresh(buffer, path, payloadLength, hdrType); + return path; + } + + /** + * @param buffer The output buffer + * @param path path + * @param payloadLength payload length + * @param hdrType Header type e.g. SCMP + * @throws IOException in case of IOException. + */ + protected void buildHeaderNoRefresh( + ByteBuffer buffer, Path path, int payloadLength, InternalConstants.HdrTypes hdrType) + throws IOException { + buffer.clear(); + long srcIA; + byte[] srcAddress; + int srcPort; + if (path instanceof ResponsePath) { + // We could get source IA, address and port locally, but it seems cleaner + // to get these from the inverted header. + ResponsePath rPath = (ResponsePath) path; + srcIA = rPath.getSourceIsdAs(); + srcAddress = rPath.getSourceAddress(); + srcPort = rPath.getSourcePort(); + } else { + // For sending request path we need to have a valid local external address. + // For a valid local external address we need to be connected. + if (!isConnected) { + isConnected = true; + connection = path.getFirstHopAddress(); + channel.connect(connection); + } + + srcIA = getService().getLocalIsdAs(); + // Get external host address. This must be done *after* refreshing the path! + InetSocketAddress srcSocketAddress = (InetSocketAddress) channel.getLocalAddress(); + srcAddress = srcSocketAddress.getAddress().getAddress(); + srcPort = srcSocketAddress.getPort(); + } + + long dstIA = path.getDestinationIsdAs(); + byte[] dstAddress = path.getDestinationAddress(); + int dstPort = path.getDestinationPort(); + + byte[] rawPath = path.getRawPath(); + ScionHeaderParser.write( + buffer, payloadLength, rawPath.length, srcIA, srcAddress, dstIA, dstAddress, hdrType); + ScionHeaderParser.writePath(buffer, rawPath); + + if (hdrType == InternalConstants.HdrTypes.UDP) { + ScionHeaderParser.writeUdpOverlayHeader(buffer, payloadLength, srcPort, dstPort); + } + } + + protected RequestPath ensureUpToDate(RequestPath path) throws IOException { + if (Instant.now().getEpochSecond() + cfgExpirationSafetyMargin <= path.getExpiration()) { + return path; + } + return updatePath(path); + } + + private RequestPath updatePath(RequestPath path) throws IOException { + // expired, get new path + RequestPath newPath = pathPolicy.filter(getService().getPaths(path)); + + if (isConnected) { // equal to !isBound at this point + if (!newPath.getFirstHopAddress().equals(this.connection)) { + // TODO only reconnect if firstHop is on different interface....?! + channel.disconnect(); + this.connection = newPath.getFirstHopAddress(); + channel.connect(this.connection); + } + this.path = newPath; + } + return newPath; + } +} diff --git a/src/main/java/org/scion/DatagramChannel.java b/src/main/java/org/scion/DatagramChannel.java index 05392f289..1a408f51b 100644 --- a/src/main/java/org/scion/DatagramChannel.java +++ b/src/main/java/org/scion/DatagramChannel.java @@ -18,287 +18,57 @@ import java.io.IOException; import java.net.*; import java.nio.ByteBuffer; -import java.nio.channels.AlreadyConnectedException; import java.nio.channels.ByteChannel; -import java.nio.channels.ClosedChannelException; import java.nio.channels.NotYetConnectedException; -import java.time.Instant; -import java.util.function.Consumer; -import org.scion.internal.ExtensionHeader; import org.scion.internal.InternalConstants; -import org.scion.internal.PathHeaderParser; import org.scion.internal.ScionHeaderParser; -import org.scion.internal.ScmpParser; -public class DatagramChannel implements ByteChannel, Closeable { +public class DatagramChannel extends AbstractDatagramChannel + implements ByteChannel, Closeable { - private final java.nio.channels.DatagramChannel channel; - private boolean isConnected = false; - private InetSocketAddress connection; - private RequestPath path; - private final ByteBuffer buffer; - private boolean cfgReportFailedValidation = false; - private PathPolicy pathPolicy = PathPolicy.DEFAULT; - private ScionService service; - private int cfgExpirationSafetyMargin = - ScionUtil.getPropertyOrEnv( - Constants.PROPERTY_PATH_EXPIRY_MARGIN, - Constants.ENV_PATH_EXPIRY_MARGIN, - Constants.DEFAULT_PATH_EXPIRY_MARGIN); - - private Consumer pingListener; - private Consumer traceListener; - private Consumer errorListener; - - public static DatagramChannel open() throws IOException { - return new DatagramChannel(); - } - - public static DatagramChannel open(ScionService service) throws IOException { - return new DatagramChannel(service); - } + private final ByteBuffer bufferReceive; + private final ByteBuffer bufferSend; protected DatagramChannel() throws IOException { this(null); } protected DatagramChannel(ScionService service) throws IOException { - this.channel = java.nio.channels.DatagramChannel.open(); - this.service = service; - // TODO snd/rcv - int size = - Math.max( - channel.getOption(StandardSocketOptions.SO_RCVBUF), - channel.getOption(StandardSocketOptions.SO_SNDBUF)); - this.buffer = ByteBuffer.allocateDirect(size); - } - - /** - * Set the path policy. The default path policy is set in PathPolicy.DEFAULT, which currently - * means to use the first path returned by the daemon or control service. If the channel is - * connected, this method will request a new path using the new policy. - * - * @param pathPolicy the new path policy - * @see PathPolicy#DEFAULT - */ - public synchronized void setPathPolicy(PathPolicy pathPolicy) throws IOException { - this.pathPolicy = pathPolicy; - if (path != null) { - updatePath(path); - } + super(service); + this.bufferReceive = ByteBuffer.allocateDirect(getOption(StandardSocketOptions.SO_RCVBUF)); + this.bufferSend = ByteBuffer.allocateDirect(getOption(StandardSocketOptions.SO_SNDBUF)); } - public synchronized PathPolicy getPathPolicy() { - return this.pathPolicy; - } - - public synchronized void setService(ScionService service) { - this.service = service; - } - - public synchronized ScionService getService() { - if (service == null) { - service = Scion.defaultService(); - } - return this.service; + public static DatagramChannel open() throws IOException { + return new DatagramChannel(); } - public synchronized DatagramChannel bind(InetSocketAddress address) throws IOException { - channel.bind(address); - return this; + public static DatagramChannel open(ScionService service) throws IOException { + return new DatagramChannel(service); } // TODO we return `void` here. If we implement SelectableChannel // this can be changed to return SelectableChannel. - public synchronized void configureBlocking(boolean block) throws IOException { - channel.configureBlocking(block); - } - - public synchronized boolean isBlocking() { - return channel.isBlocking(); - } - - public synchronized InetSocketAddress getLocalAddress() throws IOException { - return (InetSocketAddress) channel.getLocalAddress(); - } - - public synchronized void disconnect() throws IOException { - channel.disconnect(); - connection = null; - isConnected = false; - path = null; - } - @Override - public synchronized boolean isOpen() { - return channel.isOpen(); + public synchronized void configureBlocking(boolean block) throws IOException { + super.configureBlocking(block); } @Override - // TODO not synchronized yet, think about a more fine grained lock (see JDK Channel impl) - public void close() throws IOException { - channel.disconnect(); - channel.close(); - isConnected = false; - connection = null; - path = null; - } - - /** - * Connect to a destination host. Note: - A SCION channel will internally connect to the next - * border router (first hop) instead of the remote host. - * - *

NB: This method does internally no call {@link java.nio.channels.DatagramChannel}.connect(), - * instead it calls bind(). That means this method does NOT perform any additional security checks - * associated with connect(), only those associated with bind(). - * - * @param addr Address of remote host. - * @return This channel. - * @throws IOException for example when the first hop (border router) cannot be connected. - */ - public synchronized DatagramChannel connect(SocketAddress addr) throws IOException { - checkConnected(false); - if (!(addr instanceof InetSocketAddress)) { - throw new IllegalArgumentException( - "connect() requires an InetSocketAddress or a ScionSocketAddress."); - } - return connect(pathPolicy.filter(getService().getPaths((InetSocketAddress) addr))); - } - - /** - * Connect to a destination host. Note: - A SCION channel will internally connect to the next - * border router (first hop) instead of the remote host. - The path will be replaced with a new - * path once it is expired. - * - *

NB: This method does internally no call {@link java.nio.channels.DatagramChannel}.connect(), - * instead it calls bind(). That means this method does NOT perform any additional security checks - * associated with connect(), only those associated with bind(). - * - * @param path Path to the remote host. - * @return This channel. - * @throws IOException for example when the first hop (border router) cannot be connected. - */ - public synchronized DatagramChannel connect(RequestPath path) throws IOException { - checkConnected(false); - this.path = path; - isConnected = true; - connection = path.getFirstHopAddress(); - channel.connect(connection); - return this; - } - - /** - * Get the currently connected path. - * - * @return the current Path or `null` if not path is connected. - */ - public synchronized Path getCurrentPath() { - return path; + public synchronized boolean isBlocking() { + return super.isBlocking(); } public synchronized ResponsePath receive(ByteBuffer userBuffer) throws IOException { - ResponsePath receivePath = receiveFromChannel(InternalConstants.HdrTypes.UDP); + ResponsePath receivePath = receiveFromChannel(bufferReceive, InternalConstants.HdrTypes.UDP); if (receivePath == null) { return null; // non-blocking, nothing available } - ScionHeaderParser.extractUserPayload(buffer, userBuffer); - buffer.clear(); + ScionHeaderParser.extractUserPayload(bufferReceive, userBuffer); + bufferReceive.clear(); return receivePath; } - synchronized Scmp.ScmpMessage receiveScmp() throws IOException { - ResponsePath receivePath = receiveFromChannel(InternalConstants.HdrTypes.SCMP); - if (receivePath == null) { - return null; // non-blocking, nothing available - } - return receiveScmp(receivePath); - } - - private ResponsePath receiveFromChannel(InternalConstants.HdrTypes expectedHdrType) - throws IOException { - while (true) { - buffer.clear(); - InetSocketAddress srcAddress = (InetSocketAddress) channel.receive(buffer); - if (srcAddress == null) { - // this indicates nothing is available - non-blocking mode - return null; - } - buffer.flip(); - - String validationResult = ScionHeaderParser.validate(buffer.asReadOnlyBuffer()); - if (validationResult != null && cfgReportFailedValidation) { - throw new ScionException(validationResult); - } - if (validationResult != null) { - continue; - } - - InternalConstants.HdrTypes hdrType = ScionHeaderParser.extractNextHeader(buffer); - if (hdrType == InternalConstants.HdrTypes.UDP && expectedHdrType == hdrType) { - return ScionHeaderParser.extractResponsePath(buffer, srcAddress); - } - - // From here on we use linear reading using the buffer's position() mechanism - buffer.position(ScionHeaderParser.extractHeaderLength(buffer)); - if (hdrType == InternalConstants.HdrTypes.END_TO_END - || hdrType == InternalConstants.HdrTypes.HOP_BY_HOP) { - ExtensionHeader extHdr = ExtensionHeader.consume(buffer); - // Currently we are not doing much here except hoping for an SCMP header - hdrType = extHdr.nextHdr(); - if (hdrType != InternalConstants.HdrTypes.SCMP) { - throw new UnsupportedOperationException("Extension header not supported: " + hdrType); - } - } - - if (hdrType == expectedHdrType) { - return ScionHeaderParser.extractResponsePath(buffer, srcAddress); - } - receiveScmp(path); - } - } - - private Object receiveNonDataPacket(InternalConstants.HdrTypes hdrType, ResponsePath path) - throws ScionException { - switch (hdrType) { - case HOP_BY_HOP: - case END_TO_END: - return receiveExtension(path); - // break; - case SCMP: - return receiveScmp(path); - // break; - default: - if (cfgReportFailedValidation) { - throw new ScionException("Unknown nextHdr: " + hdrType); - } - } - return null; - } - - private Object receiveExtension(ResponsePath path) throws ScionException { - ExtensionHeader extHdr = ExtensionHeader.consume(buffer); - // Currently we are not doing much here except hoping for an SCMP header - return receiveNonDataPacket(extHdr.nextHdr(), path); - } - - private Scmp.ScmpMessage receiveScmp(Path path) { - Scmp.ScmpMessage scmpMsg = ScmpParser.consume(buffer, path); - if (scmpMsg instanceof Scmp.ScmpEcho) { - if (pingListener != null) { - pingListener.accept((Scmp.ScmpEcho) scmpMsg); - } - } else if (scmpMsg instanceof Scmp.ScmpTraceroute) { - if (traceListener != null) { - traceListener.accept((Scmp.ScmpTraceroute) scmpMsg); - } - } else { - if (errorListener != null) { - errorListener.accept(scmpMsg); - } - } - return scmpMsg; - } - /** * Attempts to send the content of the buffer to the destinationAddress. This method will request * a new path for each call. @@ -315,7 +85,7 @@ public synchronized void send(ByteBuffer srcBuffer, SocketAddress destination) if (!(destination instanceof InetSocketAddress)) { throw new IllegalArgumentException("Address must be of type InetSocketAddress."); } - send(srcBuffer, pathPolicy.filter(getService().getPaths((InetSocketAddress) destination))); + send(srcBuffer, getPathPolicy().filter(getService().getPaths((InetSocketAddress) destination))); } /** @@ -332,59 +102,14 @@ public synchronized void send(ByteBuffer srcBuffer, SocketAddress destination) */ public synchronized Path send(ByteBuffer srcBuffer, Path path) throws IOException { // + 8 for UDP overlay header length - Path actualPath = buildHeader(path, srcBuffer.remaining() + 8, InternalConstants.HdrTypes.UDP); - buffer.put(srcBuffer); - buffer.flip(); - channel.send(buffer, actualPath.getFirstHopAddress()); + Path actualPath = + buildHeader(bufferSend, path, srcBuffer.remaining() + 8, InternalConstants.HdrTypes.UDP); + bufferSend.put(srcBuffer); + bufferSend.flip(); + sendRaw(bufferSend, actualPath.getFirstHopAddress()); return actualPath; } - public synchronized void sendEchoRequest(Path path, int sequenceNumber, ByteBuffer data) - throws IOException { - // EchoHeader = 8 + data - int len = 8 + data.remaining(); - Path actualPath = buildHeader(path, len, InternalConstants.HdrTypes.SCMP); - ScmpParser.buildScmpPing(buffer, getLocalAddress().getPort(), sequenceNumber, data); - buffer.flip(); - channel.send(buffer, actualPath.getFirstHopAddress()); - } - - void sendTracerouteRequest(Path path, int interfaceNumber, PathHeaderParser.Node node) - throws IOException { - // TracerouteHeader = 24 - int len = 24; - // TODO we are modifying the raw path here, this is bad! It breaks concurrent usage. - // we should only modify the outgoing packet. - byte[] raw = path.getRawPath(); - raw[node.posHopFlags] = node.hopFlags; - Path actualPath = buildHeader(path, len, InternalConstants.HdrTypes.SCMP); - ScmpParser.buildScmpTraceroute(buffer, getLocalAddress().getPort(), interfaceNumber); - buffer.flip(); - channel.send(buffer, actualPath.getFirstHopAddress()); - // Clean up! // TODO this is really bad! - raw[node.posHopFlags] = 0; - } - - public synchronized Consumer setEchoListener(Consumer listener) { - Consumer old = pingListener; - pingListener = listener; - return old; - } - - public synchronized Consumer setTracerouteListener( - Consumer listener) { - Consumer old = traceListener; - traceListener = listener; - return old; - } - - public synchronized Consumer setScmpErrorListener( - Consumer listener) { - Consumer old = errorListener; - errorListener = listener; - return old; - } - /** * Read data from the connected stream. * @@ -425,132 +150,14 @@ public synchronized int write(ByteBuffer src) throws IOException { int len = src.remaining(); // + 8 for UDP overlay header length - buildHeader(path, len + 8, InternalConstants.HdrTypes.UDP); - buffer.put(src); - buffer.flip(); + buildHeader(bufferSend, getCurrentPath(), len + 8, InternalConstants.HdrTypes.UDP); + bufferSend.put(src); + bufferSend.flip(); - int sent = channel.write(buffer); - if (sent < buffer.limit() || buffer.remaining() > 0) { + int sent = channel().write(bufferSend); + if (sent < bufferSend.limit() || bufferSend.remaining() > 0) { throw new ScionException("Failed to send all data."); } - return len - buffer.remaining(); - } - - private void checkOpen() throws ClosedChannelException { - if (!channel.isOpen()) { - throw new ClosedChannelException(); - } - } - - private void checkConnected(boolean requiredState) { - if (requiredState != isConnected) { - if (isConnected) { - throw new AlreadyConnectedException(); - } else { - throw new NotYetConnectedException(); - } - } - } - - public synchronized boolean isConnected() { - return isConnected; - } - - public synchronized DatagramChannel setOption(SocketOption option, T t) - throws IOException { - if (option instanceof ScionSocketOptions.SciSocketOption) { - if (ScionSocketOptions.SN_API_THROW_PARSER_FAILURE.equals(option)) { - cfgReportFailedValidation = (Boolean) t; - } else if (ScionSocketOptions.SN_PATH_EXPIRY_MARGIN.equals(option)) { - cfgExpirationSafetyMargin = (Integer) t; - } else if (StandardSocketOptions.SO_RCVBUF.equals(option)) { - // TODO resize buf - channel.setOption(option, t); - } else if (StandardSocketOptions.SO_SNDBUF.equals(option)) { - // TODO resize buf - channel.setOption(option, t); - } else { - throw new UnsupportedOperationException(); - } - } else { - channel.setOption(option, t); - } - return this; - } - - /** - * @param path path - * @param payloadLength payload length - * @return argument path or a new path if the argument path was expired - * @throws IOException in case of IOException. - */ - private Path buildHeader(Path path, int payloadLength, InternalConstants.HdrTypes hdrType) - throws IOException { - buffer.clear(); - long srcIA; - byte[] srcAddress; - int srcPort; - if (path instanceof ResponsePath) { - // We could get source IA, address and port locally but it seems cleaner to - // to get the from the inverted header. - ResponsePath rPath = (ResponsePath) path; - srcIA = rPath.getSourceIsdAs(); - srcAddress = rPath.getSourceAddress(); - srcPort = rPath.getSourcePort(); - } else { - // For sending request path we need to have a valid local external address. - // For a valid local external address we need to be connected. - if (!isConnected) { - isConnected = true; - connection = path.getFirstHopAddress(); - channel.connect(connection); - } - - // check path expiration - path = ensureUpToDate((RequestPath) path); - srcIA = getService().getLocalIsdAs(); - // Get external host address. This must be done *after* refreshing the path! - InetSocketAddress srcSocketAddress = (InetSocketAddress) channel.getLocalAddress(); - srcAddress = srcSocketAddress.getAddress().getAddress(); - srcPort = srcSocketAddress.getPort(); - } - - long dstIA = path.getDestinationIsdAs(); - byte[] dstAddress = path.getDestinationAddress(); - int dstPort = path.getDestinationPort(); - - byte[] rawPath = path.getRawPath(); - ScionHeaderParser.write( - buffer, payloadLength, rawPath.length, srcIA, srcAddress, dstIA, dstAddress, hdrType); - ScionHeaderParser.writePath(buffer, rawPath); - - if (hdrType == InternalConstants.HdrTypes.UDP) { - ScionHeaderParser.writeUdpOverlayHeader(buffer, payloadLength, srcPort, dstPort); - } - - return path; - } - - private Path ensureUpToDate(RequestPath path) throws IOException { - if (Instant.now().getEpochSecond() + cfgExpirationSafetyMargin <= path.getExpiration()) { - return path; - } - return updatePath(path); - } - - private Path updatePath(RequestPath path) throws IOException { - // expired, get new path - RequestPath newPath = pathPolicy.filter(getService().getPaths(path)); - - if (isConnected) { // equal to !isBound at this point - if (!newPath.getFirstHopAddress().equals(this.connection)) { - // TODO only reconnect if firstHop is on different interface....?! - channel.disconnect(); - this.connection = newPath.getFirstHopAddress(); - channel.connect(this.connection); - } - this.path = newPath; - } - return newPath; + return len - bufferSend.remaining(); } } diff --git a/src/main/java/org/scion/Scmp.java b/src/main/java/org/scion/Scmp.java index 94c0ac884..cea02b506 100644 --- a/src/main/java/org/scion/Scmp.java +++ b/src/main/java/org/scion/Scmp.java @@ -15,6 +15,7 @@ package org.scion; import java.io.IOException; +import java.nio.ByteBuffer; public class Scmp { @@ -165,20 +166,24 @@ public String getText() { return text; } + public boolean isError() { + return type >= ScmpType.ERROR_1.id && type <= ScmpType.ERROR_127.id; + } + @Override public String toString() { return type + ":" + id + ":'" + text + '\''; } } - public static class ScmpMessage { - final ScmpTypeCode typeCode; - final int identifier; - final int sequenceNumber; - final Path path; + public static class Message { + private ScmpTypeCode typeCode; + private int identifier; + private int sequenceNumber; + private Path path; /** DO NOT USE! */ - public ScmpMessage(ScmpTypeCode typeCode, int identifier, int sequenceNumber, Path path) { + public Message(ScmpTypeCode typeCode, int identifier, int sequenceNumber, Path path) { this.typeCode = typeCode; this.identifier = identifier; this.sequenceNumber = sequenceNumber; @@ -200,34 +205,92 @@ public ScmpTypeCode getTypeCode() { public Path getPath() { return path; } + + public void setPath(Path path) { + this.path = path; + } + + public void setMessageArgs(ScmpTypeCode sc, int identifier, int sequenceNumber) { + this.typeCode = sc; + this.identifier = identifier; + this.sequenceNumber = sequenceNumber; + } } - public static class ScmpEcho extends ScmpMessage { - byte[] data; + public static class EchoMessage extends Message { + private byte[] data; + private long nanoSeconds; + private boolean timedOut = false; - /** DO NOT USE! */ - public ScmpEcho( + private EchoMessage( ScmpTypeCode typeCode, int identifier, int sequenceNumber, Path path, byte[] data) { super(typeCode, identifier, sequenceNumber, path); this.data = data; } + public static EchoMessage createEmpty(Path path) { + return new EchoMessage(ScmpTypeCode.TYPE_128, -1, -1, path, null); + } + + public static EchoMessage createRequest(int sequenceNumber, Path path, ByteBuffer payload) { + byte[] data = new byte[payload.remaining()]; + payload.get(data); + return new EchoMessage(ScmpTypeCode.TYPE_128, -1, sequenceNumber, path, data); + } + public byte[] getData() { return data; } + + public void setData(byte[] data) { + this.data = data; + } + + public void setNanoSeconds(long nanoSeconds) { + this.nanoSeconds = nanoSeconds; + } + + public long getNanoSeconds() { + return nanoSeconds; + } + + public void setTimedOut(long nanoSeconds) { + this.nanoSeconds = nanoSeconds; + this.timedOut = true; + } + + public boolean isTimedOut() { + return timedOut; + } } - public static class ScmpTraceroute extends ScmpMessage { + public static class TracerouteMessage extends Message { - private final long isdAs; - private final long ifID; + private long isdAs; + private long ifID; + private long nanoSeconds; + private boolean timedOut = false; - /** DO NOT USE! */ - public ScmpTraceroute(ScmpTypeCode typeCode, int identifier, int sequenceNumber, Path path) { + private TracerouteMessage( + ScmpTypeCode typeCode, int identifier, int sequenceNumber, Path path) { this(typeCode, identifier, sequenceNumber, 0, 0, path); } - public ScmpTraceroute( + public static TracerouteMessage createEmpty(Path path) { + return new TracerouteMessage(ScmpTypeCode.TYPE_130, -1, -1, path); + } + + public static TracerouteMessage createRequest(int sequenceNumber, Path path) { + return new TracerouteMessage(ScmpTypeCode.TYPE_130, -1, sequenceNumber, path); + } + + public static TracerouteMessage createTimedOut(long nanoSeconds) { + TracerouteMessage r = new TracerouteMessage(null, -1, -1, null); + r.setNanoSeconds(nanoSeconds); + return r; + } + + public TracerouteMessage( ScmpTypeCode typeCode, int identifier, int sequenceNumber, @@ -247,6 +310,14 @@ public long getIfID() { return ifID; } + public void setNanoSeconds(long nanoSeconds) { + this.nanoSeconds = nanoSeconds; + } + + public long getNanoSeconds() { + return nanoSeconds; + } + @Override public String toString() { String echoMsgStr = getTypeCode().getText(); @@ -254,23 +325,32 @@ public String toString() { echoMsgStr += " " + ScionUtil.toStringIA(getIsdAs()) + " IfID=" + getIfID(); return echoMsgStr; } - } - public static class Result { - private final T message; - private final long nanoSeconds; + public void setTracerouteArgs(long isdAs, long ifID) { + this.isdAs = isdAs; + this.ifID = ifID; + } - public Result(T message, long nanoSeconds) { - this.message = message; + public void setTimedOut(long nanoSeconds) { this.nanoSeconds = nanoSeconds; + this.timedOut = true; } - public T getMessage() { - return message; + public boolean isTimedOut() { + return timedOut; } + } - public long getNanoSeconds() { - return nanoSeconds; + static Scmp.Message createMessage(Scmp.ScmpType type, Path path) { + switch (type) { + case INFO_128: + case INFO_129: + return Scmp.EchoMessage.createEmpty(path); + case INFO_130: + case INFO_131: + return Scmp.TracerouteMessage.createEmpty(path); + default: + return new Scmp.Message(null, -1, -1, path); } } diff --git a/src/main/java/org/scion/ScmpChannel.java b/src/main/java/org/scion/ScmpChannel.java index 61789aa37..568a77fce 100644 --- a/src/main/java/org/scion/ScmpChannel.java +++ b/src/main/java/org/scion/ScmpChannel.java @@ -16,19 +16,20 @@ import java.io.IOException; import java.net.InetSocketAddress; -import java.net.SocketOption; +import java.net.StandardSocketOptions; import java.nio.ByteBuffer; import java.util.Collection; import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; +import org.scion.internal.InternalConstants; import org.scion.internal.PathHeaderParser; +import org.scion.internal.ScmpParser; -public class ScmpChannel implements AutoCloseable { - private final DatagramChannel channel; - private final RequestPath path; - private final AtomicReference error = new AtomicReference<>(); +public class ScmpChannel extends AbstractDatagramChannel implements AutoCloseable { + private final ByteBuffer bufferReceive; + private final ByteBuffer bufferSend; + private final AtomicReference error = new AtomicReference<>(); private int timeOutMs = 1000; ScmpChannel(RequestPath path) throws IOException { @@ -36,18 +37,71 @@ public class ScmpChannel implements AutoCloseable { } ScmpChannel(RequestPath path, int port) throws IOException { - this.path = path; + super(Scion.defaultService()); + configureBlocking(true); + this.bufferReceive = ByteBuffer.allocateDirect(getOption(StandardSocketOptions.SO_RCVBUF)); + this.bufferSend = ByteBuffer.allocateDirect(getOption(StandardSocketOptions.SO_SNDBUF)); + + setPath(path); InetSocketAddress local = new InetSocketAddress("0.0.0.0", port); - ScionService service = Scion.defaultService(); - this.channel = service.openChannel().bind(local); - channel.configureBlocking(true); - channel.setScmpErrorListener(this::errorListener); + this.bind(local); } - private void errorListener(Scmp.ScmpMessage msg) { - error.set(msg); - Thread.currentThread().interrupt(); - throw new RuntimeException(); + private synchronized Scmp.EchoMessage sendEchoRequest(Scmp.EchoMessage request) + throws IOException { + // send + // EchoHeader = 8 + data + int len = 8 + request.getData().length; + Path path = request.getPath(); + buildHeaderNoRefresh(bufferSend, request.getPath(), len, InternalConstants.HdrTypes.SCMP); + ScmpParser.buildScmpPing( + bufferSend, getLocalAddress().getPort(), request.getSequenceNumber(), request.getData()); + bufferSend.flip(); + sendRaw(bufferSend, path.getFirstHopAddress()); + + // receive + Scmp.Message scmpMsg = null; + ResponsePath receivePath = null; + while (scmpMsg == null) { + receivePath = receiveFromChannel(bufferReceive, InternalConstants.HdrTypes.SCMP); + scmpMsg = ScmpParser.consume(bufferReceive, request); + } + scmpMsg.setPath(receivePath); + checkListeners(scmpMsg); + return (Scmp.EchoMessage) scmpMsg; + } + + private Scmp.TracerouteMessage sendTracerouteRequest( + Scmp.TracerouteMessage request, PathHeaderParser.Node node) throws IOException { + Path path = request.getPath(); + // send + // TracerouteHeader = 24 + int len = 24; + // TODO we are modifying the raw path here, this is bad! It breaks concurrent usage. + // we should only modify the outgoing packet. + byte[] raw = path.getRawPath(); + byte backup = raw[node.posHopFlags]; + raw[node.posHopFlags] = node.hopFlags; + + buildHeaderNoRefresh(bufferSend, path, len, InternalConstants.HdrTypes.SCMP); + int interfaceNumber = request.getSequenceNumber(); + ScmpParser.buildScmpTraceroute(bufferSend, getLocalAddress().getPort(), interfaceNumber); + bufferSend.flip(); + sendRaw(bufferSend, path.getFirstHopAddress()); + // Clean up! // TODO this is really bad! + raw[node.posHopFlags] = backup; + + // receive + Scmp.Message scmpMsg = null; + ResponsePath receivePath = null; + while (scmpMsg == null) { + receivePath = receiveFromChannel(bufferReceive, InternalConstants.HdrTypes.SCMP); + scmpMsg = ScmpParser.consume(bufferReceive, request); + } + + scmpMsg.setPath(receivePath); + checkListeners(scmpMsg); + return (Scmp.TracerouteMessage) scmpMsg; } /** @@ -60,15 +114,18 @@ private void errorListener(Scmp.ScmpMessage msg) { * and the time is equal to the time-out duration. * @throws IOException if an IO error occurs or if an SCMP error is received. */ - public Scmp.Result sendEchoRequest(int sequenceNumber, ByteBuffer data) - throws IOException { - if (!channel.isConnected()) { - channel.connect(path); + public Scmp.EchoMessage sendEchoRequest(int sequenceNumber, ByteBuffer data) throws IOException { + RequestPath path = (RequestPath) getCurrentPath(); + if (!isConnected()) { + connect(path); } - AtomicReference> result = new AtomicReference<>(); + // Hack: we do not modify the AtomicReference.It simply serves as a memory barrier + // to facilitate concurrent access to the result. + AtomicReference result = new AtomicReference<>(); AtomicReference exception = new AtomicReference<>(); - Thread t = new Thread(() -> sendEchoRequest(sequenceNumber, data, result, exception)); + result.set(Scmp.EchoMessage.createRequest(sequenceNumber, path, data)); + Thread t = new Thread(() -> sendEchoRequest(result, exception)); t.start(); try { t.join(timeOutMs); @@ -87,23 +144,21 @@ public Scmp.Result sendEchoRequest(int sequenceNumber, ByteBuffer if (t.isAlive()) { // timeout t.interrupt(); - return new Scmp.Result<>(null, timeOutMs * 1_000_000L); + Scmp.EchoMessage echo = result.get(); + echo.setTimedOut(timeOutMs * 1_000_000L); + return echo; } return result.get(); } private void sendEchoRequest( - int sequenceNumber, - ByteBuffer data, - AtomicReference> result, - AtomicReference exception) { + AtomicReference result, AtomicReference exception) { try { long sendNanos = System.nanoTime(); - channel.sendEchoRequest(path, sequenceNumber, data); - Scmp.ScmpMessage msg = channel.receiveScmp(); + Scmp.EchoMessage msg = sendEchoRequest(result.get()); long nanos = System.nanoTime() - sendNanos; - if (msg instanceof Scmp.ScmpEcho) { - result.set(new Scmp.Result<>((Scmp.ScmpEcho) msg, nanos)); + if (msg.getTypeCode() == Scmp.ScmpTypeCode.TYPE_129) { + msg.setNanoSeconds(nanos); } else { // error throw new IOException("SCMP error: " + msg.getTypeCode().getText()); @@ -122,30 +177,29 @@ private void sendEchoRequest( * If a request times out, the traceroute is aborted. * @throws IOException if an IO error occurs or if an SCMP error is received. */ - public Collection> sendTracerouteRequest() throws IOException { - ConcurrentLinkedQueue> results = new ConcurrentLinkedQueue<>(); - try { - List nodes = PathHeaderParser.getTraceNodes(path.getRawPath()); - for (int i = 0; i < nodes.size(); i++) { - if (!sendConcurrentTraceRequest(i, nodes.get(i), results)) { - // timeout: abort - break; - } + public Collection sendTracerouteRequest() throws IOException { + ConcurrentLinkedQueue results = new ConcurrentLinkedQueue<>(); + Path path = getCurrentPath(); + List nodes = PathHeaderParser.getTraceNodes(path.getRawPath()); + for (int i = 0; i < nodes.size(); i++) { + if (!sendConcurrentTraceRequest(i, path, nodes.get(i), results)) { + // timeout: abort + break; } - } finally { - channel.setTracerouteListener(null); } return results; } private boolean sendConcurrentTraceRequest( int sequenceNumber, + Path path, PathHeaderParser.Node node, - ConcurrentLinkedQueue> results) + ConcurrentLinkedQueue results) throws IOException { AtomicReference exception = new AtomicReference<>(); - Thread t = new Thread(() -> sendTracerouteRequest(sequenceNumber, node, results, exception)); + Thread t = + new Thread(() -> sendTracerouteRequest(path, sequenceNumber, node, results, exception)); t.start(); try { t.join(timeOutMs); @@ -164,48 +218,36 @@ private boolean sendConcurrentTraceRequest( if (t.isAlive()) { // timeout t.interrupt(); - results.add(new Scmp.Result<>(null, timeOutMs * 1_000_000L)); + results.add(Scmp.TracerouteMessage.createTimedOut(timeOutMs * 1_000_000L)); return false; } return true; } private void sendTracerouteRequest( + Path path, int sequenceNumber, PathHeaderParser.Node node, - ConcurrentLinkedQueue> results, + ConcurrentLinkedQueue results, AtomicReference exception) { try { + Scmp.TracerouteMessage trace = Scmp.TracerouteMessage.createRequest(sequenceNumber, path); long sendNanos = System.nanoTime(); - channel.sendTracerouteRequest(path, sequenceNumber, node); - Scmp.ScmpMessage msg = channel.receiveScmp(); + trace = sendTracerouteRequest(trace, node); long nanos = System.nanoTime() - sendNanos; - if (msg instanceof Scmp.ScmpTraceroute) { - results.add(new Scmp.Result<>((Scmp.ScmpTraceroute) msg, nanos)); + if (trace.getTypeCode() == Scmp.ScmpTypeCode.TYPE_131) { + trace.setNanoSeconds(nanos); + results.add(trace); } else { // error - throw new IOException("SCMP error: " + msg.getTypeCode().getText()); + throw new IOException("SCMP error: " + trace.getTypeCode().getText()); } } catch (IOException e) { exception.set(e); } } - public Consumer setScmpErrorListener(Consumer listener) { - return channel.setScmpErrorListener(listener); - } - - public synchronized ScmpChannel setOption(SocketOption option, T t) throws IOException { - channel.setOption(option, t); - return this; - } - public void setTimeOut(int milliSeconds) { this.timeOutMs = milliSeconds; } - - @Override - public void close() throws IOException { - channel.close(); - } } diff --git a/src/main/java/org/scion/ScmpDatagramChannel.java b/src/main/java/org/scion/ScmpDatagramChannel.java new file mode 100644 index 000000000..3a64bb50d --- /dev/null +++ b/src/main/java/org/scion/ScmpDatagramChannel.java @@ -0,0 +1,82 @@ +// 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; + +import java.io.Closeable; +import java.io.IOException; +import java.net.*; +import java.nio.ByteBuffer; +import java.util.function.Consumer; +import org.scion.internal.InternalConstants; +import org.scion.internal.ScmpParser; + +@Deprecated +public class ScmpDatagramChannel extends AbstractDatagramChannel + implements Closeable { + + private final ByteBuffer bufferReceive; + private final ByteBuffer bufferSend; + private Consumer echoListener; + + protected ScmpDatagramChannel() throws IOException { + this(null); + } + + protected ScmpDatagramChannel(ScionService service) throws IOException { + super(service); + configureBlocking(true); + this.bufferReceive = ByteBuffer.allocateDirect(getOption(StandardSocketOptions.SO_RCVBUF)); + this.bufferSend = ByteBuffer.allocateDirect(getOption(StandardSocketOptions.SO_SNDBUF)); + } + + public static ScmpDatagramChannel open() throws IOException { + return new ScmpDatagramChannel(); + } + + public static ScmpDatagramChannel open(ScionService service) throws IOException { + return new ScmpDatagramChannel(service); + } + + @Deprecated // TODO REMOVE THIS + public synchronized void receive() throws IOException { + receiveFromChannel(bufferReceive, InternalConstants.HdrTypes.UDP); + } + + @Deprecated + public synchronized Scmp.Message sendEchoRequest(Path path, int sequenceNumber, ByteBuffer data) + throws IOException { + // send + // EchoHeader = 8 + data + int len = 8 + data.remaining(); + Path actualPath = buildHeader(bufferSend, path, len, InternalConstants.HdrTypes.SCMP); + ScmpParser.buildScmpPing(bufferSend, getLocalAddress().getPort(), sequenceNumber, data); + bufferSend.flip(); + sendRaw(bufferSend, actualPath.getFirstHopAddress()); + + // receive + ResponsePath receivePath = receiveFromChannel(bufferReceive, InternalConstants.HdrTypes.SCMP); + Scmp.Message scmpMsg = + ScmpParser.consume(bufferReceive, Scmp.EchoMessage.createEmpty(receivePath)); + checkListeners(scmpMsg); + if (echoListener != null && scmpMsg instanceof Scmp.EchoMessage) { + echoListener.accept((Scmp.EchoMessage) scmpMsg); + } + return scmpMsg; + } + + public synchronized void setEchoListener(Consumer listener) { + echoListener = listener; + } +} diff --git a/src/main/java/org/scion/internal/ScionBootstrapper.java b/src/main/java/org/scion/internal/ScionBootstrapper.java index 2543a8dbd..7d82b38ee 100644 --- a/src/main/java/org/scion/internal/ScionBootstrapper.java +++ b/src/main/java/org/scion/internal/ScionBootstrapper.java @@ -51,13 +51,8 @@ public class ScionBootstrapper { private static final Logger LOG = LoggerFactory.getLogger(ScionBootstrapper.class.getName()); private static final String STR_X_SCION = "x-sciondiscovery"; private static final String STR_X_SCION_TCP = "x-sciondiscovery:tcp"; - private static final String baseURL = ""; - private static final String topologyEndpoint = "topology"; - private static final String signedTopologyEndpoint = "topology.signed"; - private static final String trcsEndpoint = "trcs"; - private static final String trcBlobEndpoint = "trcs/isd%d-b%d-s%d/blob"; - private static final String topologyJSONFileName = "topology.json"; - private static final String signedTopologyFileName = "topology.signed"; + private static final String BASE_URL = ""; + private static final String TOPOLOGY_ENDPOINT = "topology"; private static final Duration httpRequestTimeout = Duration.of(2, ChronoUnit.SECONDS); private final String topologyResource; private final List controlServices = new ArrayList<>(); @@ -181,7 +176,8 @@ private void init() { try { parseTopologyFile(getTopologyFile()); } catch (IOException e) { - throw new ScionRuntimeException("Error while getting topology file: " + e.getMessage(), e); + throw new ScionRuntimeException( + "Error while getting topology file from " + topologyResource + ": " + e.getMessage(), e); } if (controlServices.isEmpty()) { throw new ScionRuntimeException( @@ -255,7 +251,7 @@ private String getTopologyFile() throws IOException { controlServices.clear(); discoveryServices.clear(); // TODO https???? - URL url = new URL("http://" + topologyResource + "/" + baseURL + topologyEndpoint); + URL url = new URL("http://" + topologyResource + "/" + BASE_URL + TOPOLOGY_ENDPOINT); return fetchTopologyFile(url); } diff --git a/src/main/java/org/scion/internal/ScmpParser.java b/src/main/java/org/scion/internal/ScmpParser.java index 13694cdbd..aef722a87 100644 --- a/src/main/java/org/scion/internal/ScmpParser.java +++ b/src/main/java/org/scion/internal/ScmpParser.java @@ -14,14 +14,13 @@ package org.scion.internal; -import static org.scion.Scmp.ScmpEcho; -import static org.scion.Scmp.ScmpMessage; -import static org.scion.Scmp.ScmpTraceroute; +import static org.scion.Scmp.EchoMessage; +import static org.scion.Scmp.Message; import static org.scion.Scmp.ScmpType; import static org.scion.Scmp.ScmpTypeCode; +import static org.scion.Scmp.TracerouteMessage; import java.nio.ByteBuffer; -import org.scion.Path; public class ScmpParser { @@ -35,6 +34,16 @@ public static void buildExtensionHeader(ByteBuffer buffer, InternalConstants.Hdr buffer.putInt(0); } + public static void buildScmpPing( + ByteBuffer buffer, int identifier, int sequenceNumber, byte[] data) { + buffer.put(ByteUtil.toByte(ScmpType.INFO_128.id())); + buffer.put(ByteUtil.toByte(0)); + buffer.putShort((short) 0); // TODO checksum + buffer.putShort((short) identifier); // unsigned + buffer.putShort((short) sequenceNumber); // unsigned + buffer.put(data); + } + public static void buildScmpPing( ByteBuffer buffer, int identifier, int sequenceNumber, ByteBuffer data) { buffer.put(ByteUtil.toByte(ScmpType.INFO_128.id())); @@ -66,14 +75,19 @@ public static void buildScmpTraceroute(ByteBuffer buffer, int identifier, int se buffer.putLong(0); } + public static ScmpType extractType(ByteBuffer data) { + // Avoid changing the position! + return ScmpType.parse(ByteUtil.toUnsigned(data.get(data.position()))); + } + /** * Reads a SCMP message from the packet. Consumes the byte buffer. * * @param data packet data - * @param path receive path + * @param holder SCMP message holder * @return ScmpMessage object */ - public static ScmpMessage consume(ByteBuffer data, Path path) { + public static Message consume(ByteBuffer data, Message holder) { int type = ByteUtil.toUnsigned(data.get()); int code = ByteUtil.toUnsigned(data.get()); data.getShort(); // checksum @@ -83,20 +97,27 @@ public static ScmpMessage consume(ByteBuffer data, Path path) { ScmpTypeCode sc = ScmpTypeCode.parse(type, code); int short1 = ByteUtil.toUnsigned(data.getShort()); int short2 = ByteUtil.toUnsigned(data.getShort()); + holder.setMessageArgs(sc, short1, short2); switch (st) { case INFO_128: case INFO_129: - byte[] scmpData = new byte[data.remaining()]; - data.get(scmpData); - return new ScmpEcho(sc, short1, short2, path, scmpData); + EchoMessage echo = (EchoMessage) holder; + if (echo.getData() == null) { + echo.setData(new byte[data.remaining()]); + } + // If there is an array we can simply reuse it. The length of the + // package has already been validated. + data.get(echo.getData()); + return echo; case INFO_130: - return new ScmpTraceroute(sc, short1, short2, path); case INFO_131: long isdAs = data.getLong(); long ifID = data.getLong(); - return new ScmpTraceroute(sc, short1, short2, isdAs, ifID, path); + TracerouteMessage trace = (TracerouteMessage) holder; + trace.setTracerouteArgs(isdAs, ifID); + return trace; default: - return new ScmpMessage(sc, short1, short2, path); + return holder; } } } diff --git a/src/test/java/org/scion/PackageVisibilityHelper.java b/src/test/java/org/scion/PackageVisibilityHelper.java index 4d248b3dd..fd6c01fc3 100644 --- a/src/test/java/org/scion/PackageVisibilityHelper.java +++ b/src/test/java/org/scion/PackageVisibilityHelper.java @@ -44,6 +44,10 @@ public static InternalConstants.HdrTypes getNextHdr(ByteBuffer packet) { return ScionHeaderParser.extractNextHeader(packet); } + public static Scmp.Message createMessage(Scmp.ScmpType type, Path path) { + return Scmp.createMessage(type, path); + } + public static InetSocketAddress getDstAddress(ByteBuffer packet) throws UnknownHostException { return ScionHeaderParser.extractDestinationSocketAddress(packet); } diff --git a/src/test/java/org/scion/api/DatagramChannelApiTest.java b/src/test/java/org/scion/api/DatagramChannelApiTest.java index 32c68d700..42fa15b6c 100644 --- a/src/test/java/org/scion/api/DatagramChannelApiTest.java +++ b/src/test/java/org/scion/api/DatagramChannelApiTest.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.net.StandardSocketOptions; import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; @@ -501,7 +502,7 @@ private RequestPath createExpiredPath(Path basePath) throws UnknownHostException } @Test - void geCurrentPath() { + void getCurrentPath() { RequestPath addr = ExamplePacket.PATH; ByteBuffer buffer = ByteBuffer.allocate(50); try (DatagramChannel channel = DatagramChannel.open()) { @@ -520,4 +521,25 @@ void geCurrentPath() { throw new RuntimeException(e); } } + + @Test + void setOption() throws IOException { + try (DatagramChannel channel = DatagramChannel.open()) { + assertFalse(channel.getOption(ScionSocketOptions.SN_API_THROW_PARSER_FAILURE)); + DatagramChannel dc = channel.setOption(ScionSocketOptions.SN_API_THROW_PARSER_FAILURE, true); + assertEquals(channel, dc); + + int margin = channel.getOption(ScionSocketOptions.SN_PATH_EXPIRY_MARGIN); + channel.setOption(ScionSocketOptions.SN_PATH_EXPIRY_MARGIN, margin + 1000); + assertEquals(margin + 1000, channel.getOption(ScionSocketOptions.SN_PATH_EXPIRY_MARGIN)); + + int bufSizeSend = channel.getOption(StandardSocketOptions.SO_SNDBUF); + channel.setOption(StandardSocketOptions.SO_SNDBUF, bufSizeSend + 1000); + assertEquals(bufSizeSend + 1000, channel.getOption(StandardSocketOptions.SO_SNDBUF)); + + int bufSizeReceive = channel.getOption(StandardSocketOptions.SO_RCVBUF); + channel.setOption(StandardSocketOptions.SO_RCVBUF, bufSizeReceive + 1000); + assertEquals(bufSizeReceive + 1000, channel.getOption(StandardSocketOptions.SO_RCVBUF)); + } + } } diff --git a/src/test/java/org/scion/api/SCMPTest.java b/src/test/java/org/scion/api/SCMPTest.java index b9844b5d5..41410267f 100644 --- a/src/test/java/org/scion/api/SCMPTest.java +++ b/src/test/java/org/scion/api/SCMPTest.java @@ -99,12 +99,12 @@ void echo() throws IOException { channel.setScmpErrorListener(scmpMessage -> fail(scmpMessage.getTypeCode().getText())); channel.setOption(ScionSocketOptions.SN_API_THROW_PARSER_FAILURE, true); byte[] data = new byte[] {1, 2, 3, 4, 5}; - Scmp.Result result = channel.sendEchoRequest(42, ByteBuffer.wrap(data)); - assertEquals(42, result.getMessage().getSequenceNumber()); - assertEquals(Scmp.ScmpTypeCode.TYPE_129, result.getMessage().getTypeCode()); + Scmp.EchoMessage result = channel.sendEchoRequest(42, ByteBuffer.wrap(data)); + assertEquals(42, result.getSequenceNumber()); + assertEquals(Scmp.ScmpTypeCode.TYPE_129, result.getTypeCode()); assertTrue(result.getNanoSeconds() > 0); assertTrue(result.getNanoSeconds() < 10_000_000); // 10 ms - assertArrayEquals(data, result.getMessage().getData()); + assertArrayEquals(data, result.getData()); } finally { MockNetwork.stopTiny(); } @@ -118,8 +118,8 @@ void echo_timeout() throws IOException { channel.setOption(ScionSocketOptions.SN_API_THROW_PARSER_FAILURE, true); channel.setTimeOut(1_000); MockNetwork.dropNextPackets(1); - Scmp.Result result = channel.sendEchoRequest(42, ByteBuffer.allocate(0)); - assertNull(result.getMessage()); + Scmp.EchoMessage result = channel.sendEchoRequest(42, ByteBuffer.allocate(0)); + assertTrue(result.isTimedOut()); assertEquals(1_000 * 1_000_000, result.getNanoSeconds()); } finally { MockNetwork.stopTiny(); @@ -165,16 +165,25 @@ void traceroute() throws IOException { try (ScmpChannel channel = Scmp.createChannel(getPathTo112())) { channel.setScmpErrorListener(scmpMessage -> fail(scmpMessage.getTypeCode().getText())); channel.setOption(ScionSocketOptions.SN_API_THROW_PARSER_FAILURE, true); - Collection> results = channel.sendTracerouteRequest(); - channel.setTimeOut(Integer.MAX_VALUE); + Collection results = channel.sendTracerouteRequest(); + channel.setTimeOut(Integer.MAX_VALUE); // TODO ?? int n = 0; - for (Scmp.Result result : results) { - assertEquals(n++, result.getMessage().getSequenceNumber()); - assertEquals(Scmp.ScmpTypeCode.TYPE_131, result.getMessage().getTypeCode()); + for (Scmp.TracerouteMessage result : results) { + assertEquals(n++, result.getSequenceNumber()); + assertEquals(Scmp.ScmpTypeCode.TYPE_131, result.getTypeCode()); assertTrue(result.getNanoSeconds() > 0); assertTrue(result.getNanoSeconds() < 10_000_000); // 10 ms + if (n == 1) { + assertEquals(ScionUtil.parseIA("1-ff00:0:112"), result.getIsdAs()); + assertEquals(42, result.getIfID()); + } + if (n == 2) { + assertEquals(ScionUtil.parseIA("1-ff00:0:110"), result.getIsdAs()); + assertEquals(42, result.getIfID()); + } } + assertEquals(2, results.size()); } finally { MockNetwork.stopTiny(); @@ -188,11 +197,11 @@ void traceroute_timeout() throws IOException { channel.setScmpErrorListener(scmpMessage -> fail(scmpMessage.getTypeCode().getText())); channel.setOption(ScionSocketOptions.SN_API_THROW_PARSER_FAILURE, true); MockNetwork.dropNextPackets(2); - Collection> results = channel.sendTracerouteRequest(); + Collection results = channel.sendTracerouteRequest(); assertEquals(1, results.size()); - for (Scmp.Result result : results) { - assertNull(result.getMessage()); + for (Scmp.TracerouteMessage result : results) { + assertNull(result.getTypeCode()); assertEquals(1_000 * 1_000_000, result.getNanoSeconds()); } } finally { diff --git a/src/test/java/org/scion/demo/ScmpEchoDemo.java b/src/test/java/org/scion/demo/ScmpEchoDemo.java index fb63423f5..5bfd2df70 100644 --- a/src/test/java/org/scion/demo/ScmpEchoDemo.java +++ b/src/test/java/org/scion/demo/ScmpEchoDemo.java @@ -31,7 +31,7 @@ public class ScmpEchoDemo { private final AtomicLong nowNanos = new AtomicLong(); private final ByteBuffer sendBuffer = ByteBuffer.allocateDirect(8); private final int localPort; - private DatagramChannel channel; + private ScmpDatagramChannel channel; private Path path; private enum Network { @@ -95,7 +95,7 @@ public static void main(String[] args) throws IOException, InterruptedException } } - private void echoListener(Scmp.ScmpEcho msg) { + private void echoListener(Scmp.EchoMessage msg) { String echoMsgStr = msg.getTypeCode().getText(); echoMsgStr += " scmp_seq=" + msg.getSequenceNumber(); echoMsgStr += " time=" + getPassedMillies() + "ms"; @@ -103,7 +103,7 @@ private void echoListener(Scmp.ScmpEcho msg) { send(); } - private void errorListener(Scmp.ScmpMessage msg) { + private void errorListener(Scmp.Message msg) { Scmp.ScmpTypeCode code = msg.getTypeCode(); String millies = getPassedMillies(); println("SCMP error (after " + millies + "ms): " + code.getText() + " (" + code + ")"); @@ -128,10 +128,9 @@ private void runDemo(long destinationIA) throws IOException { ByteBuffer data = ByteBuffer.allocate(0); try (ScmpChannel scmpChannel = Scmp.createChannel(path, localPort)) { - for (int i = 0; i < 5; i++) { - Scmp.Result result = scmpChannel.sendEchoRequest(i, data); - Scmp.ScmpEcho msg = result.getMessage(); - String millis = String.format("%.4f", result.getNanoSeconds() / (double) 1_000_000); + for (int i = 0; i < 10; i++) { + Scmp.EchoMessage msg = scmpChannel.sendEchoRequest(i, data); + String millis = String.format("%.4f", msg.getNanoSeconds() / (double) 1_000_000); String echoMsgStr = msg.getTypeCode().getText(); echoMsgStr += " scmp_seq=" + msg.getSequenceNumber(); echoMsgStr += " time=" + millis + "ms"; @@ -148,8 +147,7 @@ private void runDemo(long destinationIA) throws IOException { private void doClientStuff(long destinationIA) throws IOException { InetSocketAddress local = new InetSocketAddress("0.0.0.0", localPort); ScionService service = Scion.defaultService(); - try (DatagramChannel channel = service.openChannel().bind(local)) { - channel.configureBlocking(true); + try (ScmpDatagramChannel channel = ScmpDatagramChannel.open(service).bind(local)) { this.channel = channel; InetSocketAddress destinationAddress = @@ -168,7 +166,7 @@ private void doClientStuff(long destinationIA) throws IOException { send(); println("Listening at " + channel.getLocalAddress() + " ..."); - channel.receive(null); + channel.receive(); channel.disconnect(); } diff --git a/src/test/java/org/scion/demo/ScmpTracerouteDemo.java b/src/test/java/org/scion/demo/ScmpTracerouteDemo.java index 44a4dd6b4..0043970d3 100644 --- a/src/test/java/org/scion/demo/ScmpTracerouteDemo.java +++ b/src/test/java/org/scion/demo/ScmpTracerouteDemo.java @@ -80,6 +80,7 @@ public static void main(String[] args) throws IOException, InterruptedException // Port must be 30041 for networks that expect a dispatcher ScmpTracerouteDemo demo = new ScmpTracerouteDemo(30041); demo.runDemo(DemoConstants.iaAnapayaHK); + // demo.runDemo(DemoConstants.iaOVGU); break; } } @@ -101,10 +102,9 @@ private void runDemo(long destinationIA) throws IOException { System.out.println("Listening at port " + localPort + " ..."); try (ScmpChannel scmpChannel = Scmp.createChannel(path, localPort)) { - Collection> results = scmpChannel.sendTracerouteRequest(); - for (Scmp.Result r : results) { - Scmp.ScmpTraceroute msg = r.getMessage(); - String millis = String.format("%.4f", r.getNanoSeconds() / (double) 1_000_000); + Collection results = scmpChannel.sendTracerouteRequest(); + for (Scmp.TracerouteMessage msg : results) { + String millis = String.format("%.4f", msg.getNanoSeconds() / (double) 1_000_000); String echoMsgStr = msg.getTypeCode().getText(); echoMsgStr += " scmp_seq=" + msg.getSequenceNumber(); echoMsgStr += " " + ScionUtil.toStringIA(msg.getIsdAs()) + " IfID=" + msg.getIfID(); diff --git a/src/test/java/org/scion/demo/inspector/HopField.java b/src/test/java/org/scion/demo/inspector/HopField.java index 9051f8ac5..5d1e6cdc9 100644 --- a/src/test/java/org/scion/demo/inspector/HopField.java +++ b/src/test/java/org/scion/demo/inspector/HopField.java @@ -26,18 +26,21 @@ public class HopField { private boolean r3; private boolean r4; private boolean r5; - private boolean r6; - // 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. + /** + * 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. + /** + * 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 + /** + * 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; @@ -55,14 +58,13 @@ public void read(ByteBuffer data) { int i0 = data.getInt(); long l1 = data.getLong(); r0 = readBoolean(i0, 0); - r1 = readBoolean(i0, 0); - r2 = readBoolean(i0, 0); - r3 = readBoolean(i0, 0); - r4 = readBoolean(i0, 0); - r5 = readBoolean(i0, 0); - r6 = readBoolean(i0, 0); - flagI = readBoolean(i0, 0); - flagE = readBoolean(i0, 0); + r1 = readBoolean(i0, 1); + r2 = readBoolean(i0, 2); + r3 = readBoolean(i0, 3); + r4 = readBoolean(i0, 4); + r5 = readBoolean(i0, 5); + flagI = readBoolean(i0, 6); + flagE = readBoolean(i0, 7); expiryTime = readInt(i0, 8, 8); consIngress = readInt(i0, 16, 16); consEgress = (int) readLong(l1, 0, 16); @@ -98,8 +100,6 @@ public String toString() { + r4 + ", r5=" + r5 - + ", r6=" - + r6 + ", I=" + flagI + ", E=" @@ -126,7 +126,6 @@ public void reset() { r3 = false; r4 = false; r5 = false; - r6 = false; flagI = false; flagE = false; expiryTime = 0; @@ -142,4 +141,12 @@ public int getIngress() { public int getEgress() { return consEgress; } + + public boolean hasIngressAlert() { + return flagI; + } + + public boolean hasEgressAlert() { + return flagE; + } } diff --git a/src/test/java/org/scion/demo/inspector/ScionPacketInspector.java b/src/test/java/org/scion/demo/inspector/ScionPacketInspector.java index d97690bb9..88073d103 100644 --- a/src/test/java/org/scion/demo/inspector/ScionPacketInspector.java +++ b/src/test/java/org/scion/demo/inspector/ScionPacketInspector.java @@ -150,7 +150,9 @@ public byte[] getPayLoad() { public void reversePath() { scionHeader.reverse(); pathHeaderScion.reverse(); - overlayHeaderUdp.reverse(); + if (scionHeader.nextHeader() == Constants.HdrTypes.UDP) { + overlayHeaderUdp.reverse(); + } } public void writePacket(ByteBuffer newData, byte[] userData) { diff --git a/src/test/java/org/scion/demo/inspector/ScmpHeader.java b/src/test/java/org/scion/demo/inspector/ScmpHeader.java index b137c94d1..12013933f 100644 --- a/src/test/java/org/scion/demo/inspector/ScmpHeader.java +++ b/src/test/java/org/scion/demo/inspector/ScmpHeader.java @@ -125,4 +125,9 @@ public void setCode(Scmp.ScmpTypeCode code) { public byte[] getUserData() { return echoUserData; } + + public void setTraceData(long isdAs, int ifID) { + this.traceIsdAs = isdAs; + this.traceIfID = ifID; + } } diff --git a/src/test/java/org/scion/testutil/MockNetwork.java b/src/test/java/org/scion/testutil/MockNetwork.java index 2e881483a..80a792ba8 100644 --- a/src/test/java/org/scion/testutil/MockNetwork.java +++ b/src/test/java/org/scion/testutil/MockNetwork.java @@ -33,7 +33,10 @@ import java.util.stream.Collectors; import org.scion.PackageVisibilityHelper; import org.scion.ResponsePath; +import org.scion.ScionUtil; import org.scion.Scmp; +import org.scion.demo.inspector.HopField; +import org.scion.demo.inspector.PathHeaderScion; import org.scion.demo.inspector.ScionPacketInspector; import org.scion.demo.inspector.ScmpHeader; import org.scion.internal.ScionHeaderParser; @@ -44,30 +47,21 @@ public class MockNetwork { public static final String BORDER_ROUTER_HOST = "127.0.0.1"; - private static final int BORDER_ROUTER_PORT1 = 30555; - private static final int BORDER_ROUTER_PORT2 = 30556; public static final String TINY_SRV_ADDR_1 = "127.0.0.112"; public static final int TINY_SRV_PORT_1 = 22233; public static final String TINY_SRV_ISD_AS = "1-ff00:0:112"; public static final String TINY_SRV_NAME_1 = "server.as112.test"; + static final AtomicInteger nForwardTotal = new AtomicInteger(); + static final AtomicIntegerArray nForwards = new AtomicIntegerArray(20); + static final AtomicInteger dropNextPackets = new AtomicInteger(); + static final AtomicReference scmpErrorOnNextPacket = new AtomicReference<>(); + private static final int BORDER_ROUTER_PORT1 = 30555; + private static final int BORDER_ROUTER_PORT2 = 30556; private static final Logger logger = LoggerFactory.getLogger(MockNetwork.class.getName()); private static ExecutorService routers = null; private static MockDaemon daemon = null; - static final AtomicInteger nForwardTotal = new AtomicInteger(); - static final AtomicIntegerArray nForwards = new AtomicIntegerArray(20); private static MockTopologyServer topoServer; private static MockControlServer controlServer; - static final AtomicInteger dropNextPackets = new AtomicInteger(); - static final AtomicReference scmpErrorOnNextPacket = new AtomicReference<>(); - - public enum Mode { - /** Start daemon */ - DAEMON, - /** Install bootstrap server with DNS NAPTR record */ - NAPTR, - /** Install bootstrap server */ - BOOTSTRAP - } /** * Start a network with one daemon and a border router. The border router connects "1-ff00:0:110" @@ -191,6 +185,15 @@ public static MockTopologyServer getTopoServer() { public static MockControlServer getControlServer() { return controlServer; } + + public enum Mode { + /** Start daemon */ + DAEMON, + /** Install bootstrap server with DNS NAPTR record */ + NAPTR, + /** Install bootstrap server */ + BOOTSTRAP + } } class MockBorderRouter implements Runnable { @@ -309,40 +312,20 @@ private void handleScmp( buffer.position(ScionHeaderParser.extractHeaderLength(buffer)); ResponsePath path = PackageVisibilityHelper.getResponsePath(buffer, (InetSocketAddress) srcAddress); - Scmp.ScmpMessage scmpMsg = ScmpParser.consume(buffer, path); + Scmp.ScmpType type = ScmpParser.extractType(buffer); + Scmp.Message scmpMsg = + ScmpParser.consume(buffer, PackageVisibilityHelper.createMessage(type, path)); logger.info( " received SCMP " + scmpMsg.getTypeCode().name() + " " + scmpMsg.getTypeCode().getText()); - if (scmpMsg instanceof Scmp.ScmpEcho) { + if (scmpMsg instanceof Scmp.EchoMessage) { // send back! // This is very basic: // - we always answer regardless of whether we are actually the destination. // - We do not invert path / addresses sendScmp(Scmp.ScmpTypeCode.TYPE_129, buffer, srcAddress, incoming); - // buffer.rewind(); - // ScionPacketInspector spi = ScionPacketInspector.readPacket(buffer); - // ScmpHeader scmpHeader = spi.getScmpHeader(); - // scmpHeader.setCode(Scmp.ScmpTypeCode.TYPE_129); - // ByteBuffer out = ByteBuffer.allocate(100); - // spi.writePacketSCMP(out); - // out.flip(); - // incoming.send(out, srcAddress); - // buffer.clear(); - } else if (scmpMsg instanceof Scmp.ScmpTraceroute) { - // send back! - // This is very basic: - // - we always answer regardless of whether we are actually the destination. - // - We do not invert path / addresses - sendScmp(Scmp.ScmpTypeCode.TYPE_131, buffer, srcAddress, incoming); - // buffer.rewind(); - // ScionPacketInspector spi = ScionPacketInspector.readPacket(buffer); - // ScmpHeader scmpHeader = spi.getScmpHeader(); - // scmpHeader.setCode(Scmp.ScmpTypeCode.TYPE_131); - // ByteBuffer out = ByteBuffer.allocate(100); - // spi.writePacketSCMP(out); - // out.flip(); - // incoming.send(out, srcAddress); - // buffer.clear(); + } else if (scmpMsg instanceof Scmp.TracerouteMessage) { + answerTraceRoute(buffer, srcAddress, incoming); } else { // forward error InetSocketAddress dstAddress = PackageVisibilityHelper.getDstAddress(buffer); @@ -374,6 +357,33 @@ private void sendScmp( buffer.clear(); } + private void answerTraceRoute( + ByteBuffer buffer, SocketAddress srcAddress, DatagramChannel incoming) throws IOException { + // This is very basic: + // - we always answer regardless of whether we are actually the destination. + buffer.rewind(); + ScionPacketInspector spi = ScionPacketInspector.readPacket(buffer); + spi.reversePath(); + ScmpHeader scmpHeader = spi.getScmpHeader(); + scmpHeader.setCode(Scmp.ScmpTypeCode.TYPE_131); + PathHeaderScion phs = spi.getPathHeaderScion(); + for (int i = 0; i < phs.getHopCount(); i++) { + HopField hf = phs.getHopField(i); + // These answers are hardcoded to work specifically with ScmpTest.traceroute() + if (hf.hasEgressAlert()) { + scmpHeader.setTraceData(ScionUtil.parseIA("1-ff00:0:112"), 42); + } + if (hf.hasIngressAlert()) { + scmpHeader.setTraceData(ScionUtil.parseIA("1-ff00:0:110"), 42); + } + } + ByteBuffer out = ByteBuffer.allocate(100); + spi.writePacketSCMP(out); + out.flip(); + incoming.send(out, srcAddress); + buffer.clear(); + } + public int getPort1() { return port1; }