diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e060ea..08564418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] -## Added +### Added - SCMP echo responder [#78](https://github.com/scionproto-contrib/jpan/pull/78) - Maven Java executor [#80](https://github.com/scionproto-contrib/jpan/pull/80) - Dev environment setup hints doc [#82](https://github.com/scionproto-contrib/jpan/pull/82) @@ -19,6 +19,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Rename `DatagramSocket` to `ScionDatagramSopcketl` and move it to main package - Rename `ScionSocketOptions` starting with `SN_` to `SCION_` - SCMP API changes. [#71](https://github.com/scionproto-contrib/jpan/pull/71) +- Make `Path` inherit `InetSocketAddress` [#69](https://github.com/scionproto-contrib/jpan/pull/69) + - **BREAKING CHANGE**: Access to path details such as metadata has been moved to + `Path.getDetails()`. + - **BREAKING CHANGE**: ScionDatagramChannel.send(buffer, path) returns 'int'. +TODO +- Move expiryMargin to ScionService? +- remove ScionAddress? +- Remove getPaths(long dstIsdAs, InetSocketAddress dstScionAddress) -< ISD + Scion address!!! +- Remove use of getHostName() -> InetAddress! ### Fixed - Fixed locking and resizing of buffers. [#68](https://github.com/scionproto-contrib/jpan/pull/68) diff --git a/doc/Design.md b/doc/Design.md index fdca7330..80114515 100644 --- a/doc/Design.md +++ b/doc/Design.md @@ -64,15 +64,35 @@ used or returned by `Scion.defaultService()`. ## Paths -There are two types of paths (both inherit `Path`: +`Path` contains a destination IA:IP:port, a raw path and a first hop IP:port. +There are two subclasses of paths (both inherit `Path`): + - `RequestPath` are used to send initial request. They are retrieved from a path service and contain meta information. -- `ResponsePath` are used to respond to a client request. They are extracted from SCION packets. +- `ResponsePath` are used to respond to a client request. They are extracted from SCION packets. + They also contains origin ISD/AS:IP:port. -`Path` contains a destination IA:IP:port, a raw path and a first hop IP:port. -`RequestPath` also contains path meta information. -`ResponsePath` also contains origin IA:IP:port. +### Design choices - Subclassing InetSocketAddress + +`Path` is a subclass of `InetSocketAddress`. This is useful for `DatagramChannel`'s `receive()` +(which returns an `InetScoketAddress`) and `send()` which requires an `InetSocketAddress` as +argument. + +The catch here is that `Path` has `getAddress()` and `getPort()` which are now contracted to +return the *remote* IP/port. Ideally they would be called `getRemoteAddress()` and`getRemotePort()` +but that is not possible. + +### Design choices - Mutable & separate "details" + +For concurrent usage, `Path`s should be thread safe. THis would ideally be achieved by making them +immutable. +The problem is that paths expire or can otherwise be invalidated. We could make this explicit and +force API users to either manage expiry/invalidation manually. + +The alternative is to move the path of `Path` that is mutable to a new class `PathDetails`. +`PathDetails` is itself immutable and is referenced atomically from `Path`, so thread safe +behaviour is still easily achievable. ## DatagramSocket diff --git a/src/main/java/org/scion/jpan/AbstractDatagramChannel.java b/src/main/java/org/scion/jpan/AbstractDatagramChannel.java index 3c3147bc..5b61d062 100644 --- a/src/main/java/org/scion/jpan/AbstractDatagramChannel.java +++ b/src/main/java/org/scion/jpan/AbstractDatagramChannel.java @@ -22,7 +22,6 @@ import java.nio.channels.ClosedChannelException; import java.nio.channels.DatagramChannel; import java.nio.channels.NotYetConnectedException; -import java.time.Instant; import java.util.Objects; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; @@ -179,21 +178,18 @@ public InetSocketAddress getLocalAddress() throws IOException { /** * Returns the remote address. * + * @return The remote address or 'null' if this channel is not connected. * @see DatagramChannel#getRemoteAddress() - * @return The remote address. + * @see #connect(SocketAddress) + * @see #connect(RequestPath) * @throws IOException If an I/O error occurs */ public InetSocketAddress getRemoteAddress() throws IOException { - Path path = getConnectionPath(); - if (path != null) { - return new InetSocketAddress(path.getRemoteAddress(), path.getRemotePort()); - } - return null; + return (InetSocketAddress) connectionPath; } public void disconnect() throws IOException { synchronized (stateLock) { - channel.disconnect(); // TODO Why ? We shouldn´t do that...? connectionPath = null; } } @@ -214,12 +210,15 @@ public void close() throws IOException { } /** - * Connect to a destination host. Note: - A SCION channel will internally connect to the next - * border router (first hop) instead of the remote host. + * Connect to a destination host. + * + *

NB: 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(). + *

NB: This method does internally not 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. @@ -232,7 +231,10 @@ public C connect(SocketAddress addr) throws IOException { throw new IllegalArgumentException( "connect() requires an InetSocketAddress or a ScionSocketAddress."); } - return connect(pathPolicy.filter(getOrCreateService().getPaths((InetSocketAddress) addr))); + if (addr instanceof RequestPath) { + return connect(((RequestPath) addr)); + } + return connect(getOrCreateService().lookupAndGetPath((InetSocketAddress) addr, pathPolicy)); } } @@ -301,7 +303,7 @@ public C connect(RequestPath path) throws IOException { * * @return the current Path or `null` if not path is connected. */ - public Path getConnectionPath() { + public RequestPath getConnectionPath() { synchronized (stateLock) { return connectionPath; } @@ -398,17 +400,12 @@ public void setOverrideSourceAddress(InetSocketAddress address) { this.overrideExternalAddress = address; } - protected int sendRaw(ByteBuffer buffer, InetSocketAddress address, Path path) - throws IOException { - if (cfgRemoteDispatcher && path != null && path.getRawPath().length == 0) { - return channel.send( - buffer, new InetSocketAddress(address.getAddress(), Constants.DISPATCHER_PORT)); + protected int sendRaw(ByteBuffer buffer, Path path) throws IOException { + if (cfgRemoteDispatcher && path.getRawPath().length == 0) { + InetAddress remoteHostIP = path.getFirstHopAddress().getAddress(); + return channel.send(buffer, new InetSocketAddress(remoteHostIP, Constants.DISPATCHER_PORT)); } - return channel.send(buffer, address); - } - - protected int sendRaw(ByteBuffer buffer, InetSocketAddress address) throws IOException { - return sendRaw(buffer, address, null); + return channel.send(buffer, path.getFirstHopAddress()); } public Consumer setScmpErrorListener(Consumer listener) { @@ -536,18 +533,21 @@ protected final ByteBuffer getBufferReceive(int requiredSize) { /** * @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 checkPathAndBuildHeader( + protected void checkPathAndBuildHeader( ByteBuffer buffer, Path path, int payloadLength, InternalConstants.HdrTypes hdrType) throws IOException { synchronized (stateLock) { if (path instanceof RequestPath) { - path = ensureUpToDate((RequestPath) path); + RequestPath requestPath = (RequestPath) path; + ScionService svc = getOrCreateService(); + if (svc.refreshPath(requestPath, getPathPolicy(), cfgExpirationSafetyMargin) + && isConnected()) { + updateConnection(requestPath, true); + } } buildHeader(buffer, path, payloadLength, hdrType); - return path; } } @@ -574,6 +574,7 @@ protected void buildHeader( srcIA = rPath.getLocalIsdAs(); srcAddress = rPath.getLocalAddress(); srcPort = rPath.getLocalPort(); + // TODO do we need to verify that the srcAddress is equal to the used local address? } else { srcIA = getOrCreateService().getLocalIsdAs(); if (overrideExternalAddress != null) { @@ -623,20 +624,6 @@ protected void buildHeader( } } - protected RequestPath ensureUpToDate(RequestPath path) throws IOException { - synchronized (stateLock) { - if (Instant.now().getEpochSecond() + cfgExpirationSafetyMargin <= path.getExpiration()) { - return path; - } - // expired, get new path - RequestPath newPath = pathPolicy.filter(getOrCreateService().getPaths(path)); - if (isConnected()) { - updateConnection(newPath, true); - } - return newPath; - } - } - private void updateConnection(RequestPath newPath, boolean mustBeConnected) throws IOException { if (mustBeConnected && !isConnected()) { throw new IllegalStateException(); diff --git a/src/main/java/org/scion/jpan/Path.java b/src/main/java/org/scion/jpan/Path.java index 898619ce..6a981c0c 100644 --- a/src/main/java/org/scion/jpan/Path.java +++ b/src/main/java/org/scion/jpan/Path.java @@ -15,55 +15,24 @@ package org.scion.jpan; import java.net.*; -import java.util.Arrays; /** * A Path is an InetSocketAddress/ISD/AS of a destination host plus a path to that host. * *

This class is thread safe. */ -public abstract class Path { - private final byte[] pathRaw; - private final long dstIsdAs; - private final InetAddress dstAddress; - private final int dstPort; +public interface Path { - protected Path(byte[] rawPath, long dstIsdAs, InetAddress dstIP, int dstPort) { - this.pathRaw = rawPath; - this.dstIsdAs = dstIsdAs; - this.dstAddress = dstIP; - this.dstPort = dstPort; - } + byte[] getRawPath(); - public byte[] getRawPath() { - return pathRaw; - } + InetSocketAddress getFirstHopAddress() throws UnknownHostException; - public abstract InetSocketAddress getFirstHopAddress() throws UnknownHostException; + int getRemotePort(); - public int getRemotePort() { - return dstPort; - } + InetAddress getRemoteAddress(); - public InetAddress getRemoteAddress() { - return dstAddress; - } - - public long getRemoteIsdAs() { - return dstIsdAs; - } + long getRemoteIsdAs(); @Override - public String toString() { - return "Path{" - + "rmtIsdAs=" - + ScionUtil.toStringIA(dstIsdAs) - + ", rmtAddress=" - + dstAddress - + ", rmtPort=" - + dstPort - + ", pathRaw=" - + Arrays.toString(pathRaw) - + '}'; - } + String toString(); } diff --git a/src/main/java/org/scion/jpan/PathDetails.java b/src/main/java/org/scion/jpan/PathDetails.java new file mode 100644 index 00000000..2ff6ef8f --- /dev/null +++ b/src/main/java/org/scion/jpan/PathDetails.java @@ -0,0 +1,300 @@ +// 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; + +import java.io.UncheckedIOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.scion.jpan.internal.IPHelper; +import org.scion.jpan.proto.daemon.Daemon; + +/** + * PathDetails contains the raw path and meta information such as bandwidth, latency or geo + * coordinates. PathDetails is available from RequestPaths that are created/returned by the + * ScionService when requesting a new path from the control service. + */ +public class PathDetails { + + private final Daemon.Path pathProtoc; + private final byte[] pathRaw; + // We store the first hop separately to void creating unnecessary objects. + private final InetSocketAddress firstHop; + + static PathDetails create(Daemon.Path path, InetAddress dstIP, int dstPort) { + return new PathDetails(path, dstIP, dstPort); + } + + private PathDetails(Daemon.Path path, InetAddress dstIP, int dstPort) { + this.pathProtoc = path; + this.pathRaw = path.getRaw().toByteArray(); + if (getRawPath().length == 0) { + // local AS has path length 0 + firstHop = new InetSocketAddress(dstIP, dstPort); + } else { + firstHop = getFirstHopAddress(path); + } + } + + private InetSocketAddress getFirstHopAddress(Daemon.Path internalPath) { + try { + String underlayAddressString = internalPath.getInterface().getAddress().getAddress(); + int splitIndex = underlayAddressString.indexOf(':'); + InetAddress ip = IPHelper.toInetAddress(underlayAddressString.substring(0, splitIndex)); + int port = Integer.parseUnsignedInt(underlayAddressString.substring(splitIndex + 1)); + return new InetSocketAddress(ip, port); + } catch (UnknownHostException e) { + // This really should never happen, the first hop is a literal IP address. + throw new UncheckedIOException(e); + } + } + + private Daemon.Path protoPath() { + if (pathProtoc == null) { + throw new IllegalStateException( + "Information is only available for paths that" + + " were retrieved directly from a path server."); + } + return pathProtoc; + } + + public InetSocketAddress getFirstHopAddress() throws UnknownHostException { + return firstHop; + } + + public byte[] getRawPath() { + return pathRaw; + } + + /** + * @return Interface for exiting the local AS using this path. + * @throws IllegalStateException if this is path is only a raw path + */ + public Interface getInterface() { + return new Interface(protoPath().getInterface()); + } + + /** + * @return The list of interfaces the path is composed of. + * @throws IllegalStateException if this is path is only a raw path + */ + public List getInterfacesList() { + return Collections.unmodifiableList( + protoPath().getInterfacesList().stream() + .map(PathInterface::new) + .collect(Collectors.toList())); + } + + /** + * @return The maximum transmission unit (MTU) on the path. + * @throws IllegalStateException if this is path is only a raw path + */ + public int getMtu() { + return protoPath().getMtu(); + } + + /** + * @return The point in time when this path expires. In seconds since UNIX epoch. + * @throws IllegalStateException if this is path is only a raw path + */ + public long getExpiration() { + return protoPath().getExpiration().getSeconds(); + } + + /** + * @return Latency lists the latencies between any two consecutive interfaces. Entry i describes + * the latency between interface i and i+1. Consequently, there are N-1 entries for N + * interfaces. A 0-value indicates that the AS did not announce a latency for this hop. + * @throws IllegalStateException if this is path is only a raw path + */ + public List getLatencyList() { + return Collections.unmodifiableList( + protoPath().getLatencyList().stream() + .map(time -> (int) (time.getSeconds() * 1_000 + time.getNanos() / 1_000_000)) + .collect(Collectors.toList())); + } + + /** + * @return Bandwidth lists the bandwidth between any two consecutive interfaces, in Kbit/s. Entry + * i describes the bandwidth between interfaces i and i+1. A 0-value indicates that the AS did + * not announce a bandwidth for this hop. + * @throws IllegalStateException if this is path is only a raw path + */ + public List getBandwidthList() { + return protoPath().getBandwidthList(); + } + + /** + * @return Geo lists the geographical position of the border routers along the path. Entry i + * describes the position of the router for interface i. A 0-value indicates that the AS did + * not announce a position for this router. + * @throws IllegalStateException if this is path is only a raw path + */ + public List getGeoList() { + return Collections.unmodifiableList( + protoPath().getGeoList().stream().map(GeoCoordinates::new).collect(Collectors.toList())); + } + + /** + * @return LinkType contains the announced link type of inter-domain links. Entry i describes the + * link between interfaces 2*i and 2*i+1. + * @throws IllegalStateException if this is path is only a raw path + */ + public List getLinkTypeList() { + return Collections.unmodifiableList( + protoPath().getLinkTypeList().stream() + .map(linkType -> LinkType.values()[linkType.getNumber()]) + .collect(Collectors.toList())); + } + + /** + * @return InternalHops lists the number of AS internal hops for the ASes on path. Entry i + * describes the hop between interfaces 2*i+1 and 2*i+2 in the same AS. Consequently, there + * are no entries for the first and last ASes, as these are not traversed completely by the + * path. + * @throws IllegalStateException if this is path is only a raw path + */ + public List getInternalHopsList() { + return protoPath().getInternalHopsList(); + } + + /** + * @return Notes contains the notes added by ASes on the path, in the order of occurrence. Entry i + * is the note of AS i on the path. + * @throws IllegalStateException if this is path is only a raw path + */ + public List getNotesList() { + return protoPath().getNotesList(); + } + + /** + * @return EpicAuths contains the EPIC authenticators used to calculate the PHVF and LHVF. + * @throws IllegalStateException if this is path is only a raw path + */ + public EpicAuths getEpicAuths() { + return new EpicAuths(protoPath().getEpicAuths()); + } + + public enum LinkType { + /** Unspecified link type. */ + LINK_TYPE_UNSPECIFIED, // = 0 + /** Direct physical connection. */ + LINK_TYPE_DIRECT, // = 1 + /** Connection with local routing/switching. */ + LINK_TYPE_MULTI_HOP, // = 2 + /** Connection overlayed over publicly routed Internet. */ + LINK_TYPE_OPEN_NET, // = 3 + } + + public static class EpicAuths { + private final byte[] authPhvf; + private final byte[] authLhvf; + + private EpicAuths(Daemon.EpicAuths epicAuths) { + this.authPhvf = epicAuths.getAuthPhvf().toByteArray(); + this.authLhvf = epicAuths.getAuthLhvf().toByteArray(); + } + + /** + * @return AuthPHVF is the authenticator use to calculate the PHVF. + */ + public byte[] getAuthPhvf() { + return authPhvf; + } + + /** + * @return AuthLHVF is the authenticator use to calculate the LHVF. + */ + public byte[] getAuthLhvf() { + return authLhvf; + } + } + + public static class Interface { + private final String address; + + private Interface(Daemon.Interface inter) { + this.address = inter.getAddress().getAddress(); + } + + /** + * @return Underlay address to exit through the interface. + */ + public String getAddress() { + return address; + } + } + + public static class PathInterface { + private final long isdAs; + + private final long id; + + private PathInterface(Daemon.PathInterface pathInterface) { + this.isdAs = pathInterface.getIsdAs(); + this.id = pathInterface.getId(); + } + + /** + * @return ISD-AS the interface belongs to. + */ + public long getIsdAs() { + return isdAs; + } + + /** + * @return ID of the interface in the AS. + */ + public long getId() { + return id; + } + } + + public static class GeoCoordinates { + private final float latitude; + private final float longitude; + private final String address; + + private GeoCoordinates(Daemon.GeoCoordinates geoCoordinates) { + this.latitude = geoCoordinates.getLatitude(); + this.longitude = geoCoordinates.getLongitude(); + this.address = geoCoordinates.getAddress(); + } + + /** + * @return Latitude of the geographic coordinate, in the WGS 84 datum. + */ + public float getLatitude() { + return latitude; + } + + /** + * @return Longitude of the geographic coordinate, in the WGS 84 datum. + */ + public float getLongitude() { + return longitude; + } + + /** + * @return Civic address of the location. + */ + public String getAddress() { + return address; + } + } +} diff --git a/src/main/java/org/scion/jpan/PathImpl.java b/src/main/java/org/scion/jpan/PathImpl.java new file mode 100644 index 00000000..a56ad2ae --- /dev/null +++ b/src/main/java/org/scion/jpan/PathImpl.java @@ -0,0 +1,70 @@ +// 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; + +import java.net.*; +import java.util.Arrays; + +/** + * A Path is an InetSocketAddress/ISD/AS of a destination host plus a path to that host. + * + *

This class is thread safe. + */ +abstract class PathImpl extends InetSocketAddress implements Path { + private final long dstIsdAs; + + protected PathImpl(long dstIsdAs, InetAddress dstIP, int dstPort) { + super(dstIP, dstPort); + this.dstIsdAs = dstIsdAs; + } + + public abstract byte[] getRawPath(); + + public abstract InetSocketAddress getFirstHopAddress() throws UnknownHostException; + + @Deprecated // Use getPort() instead + public int getRemotePort() { + return getPort(); + } + + @Deprecated // Use getAddress() instead + public InetAddress getRemoteAddress() { + return getAddress(); + } + + @Deprecated // Use getIsdAs() instead + public long getRemoteIsdAs() { + return dstIsdAs; + } + + /** + * @return ISD/AS of the remote host. + */ + public long getIsdAs() { + return dstIsdAs; + } + + @Override + public String toString() { + return "Path{" + + "ISD/AS=" + + ScionUtil.toStringIA(dstIsdAs) + + ", address=" + + super.toString() + + ", pathRaw=" + + Arrays.toString(getRawPath()) + + '}'; + } +} diff --git a/src/main/java/org/scion/jpan/PathPolicy.java b/src/main/java/org/scion/jpan/PathPolicy.java index 454d5a83..29feb5e4 100644 --- a/src/main/java/org/scion/jpan/PathPolicy.java +++ b/src/main/java/org/scion/jpan/PathPolicy.java @@ -32,7 +32,7 @@ public RequestPath filter(List paths) { class MaxBandwith implements PathPolicy { public RequestPath filter(List paths) { return paths.stream() - .max(Comparator.comparing(path -> Collections.min(path.getBandwidthList()))) + .max(Comparator.comparing(path -> Collections.min(path.getDetails().getBandwidthList()))) .orElseThrow(NoSuchElementException::new); } } @@ -45,7 +45,7 @@ public RequestPath filter(List paths) { .min( Comparator.comparing( path -> - path.getLatencyList().stream() + path.getDetails().getLatencyList().stream() .mapToLong(l -> l > 0 ? l : Integer.MAX_VALUE) .reduce(0, Long::sum))) .orElseThrow(NoSuchElementException::new); @@ -55,7 +55,7 @@ public RequestPath filter(List paths) { class MinHopCount implements PathPolicy { public RequestPath filter(List paths) { return paths.stream() - .min(Comparator.comparing(path -> path.getInternalHopsList().size())) + .min(Comparator.comparing(path -> path.getDetails().getInternalHopsList().size())) .orElseThrow(NoSuchElementException::new); } } @@ -76,7 +76,7 @@ public RequestPath filter(List paths) { } private boolean checkPath(RequestPath path) { - for (RequestPath.PathInterface pif : path.getInterfacesList()) { + for (PathDetails.PathInterface pif : path.getDetails().getInterfacesList()) { int isd = (int) (pif.getIsdAs() >>> 48); if (!allowedIsds.contains(isd)) { return false; @@ -102,7 +102,7 @@ public RequestPath filter(List paths) { } private boolean checkPath(RequestPath path) { - for (RequestPath.PathInterface pif : path.getInterfacesList()) { + for (PathDetails.PathInterface pif : path.getDetails().getInterfacesList()) { int isd = (int) (pif.getIsdAs() >>> 48); if (disallowedIsds.contains(isd)) { return false; diff --git a/src/main/java/org/scion/jpan/RequestPath.java b/src/main/java/org/scion/jpan/RequestPath.java index 9e6de75e..3cb4ef75 100644 --- a/src/main/java/org/scion/jpan/RequestPath.java +++ b/src/main/java/org/scion/jpan/RequestPath.java @@ -14,283 +14,22 @@ package org.scion.jpan; -import java.io.UncheckedIOException; import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.UnknownHostException; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; -import org.scion.jpan.internal.IPHelper; import org.scion.jpan.proto.daemon.Daemon; /** * A RequestPath is a Path with additional meta information such as bandwidth, latency or geo * coordinates. RequestPaths are created/returned by the ScionService when requesting a new path * from the control service. + * + *

A RequestPath is immutable except for an atomic reference to PathDetails (PathDetails are + * immutable). The reference may be updated, for example when a path expires. */ -public class RequestPath extends Path { - - private final Daemon.Path pathProtoc; - // We store the first hop separately to void creating unnecessary objects. - private final InetSocketAddress firstHop; +public interface RequestPath extends Path { static RequestPath create(Daemon.Path path, long dstIsdAs, InetAddress dstIP, int dstPort) { - return new RequestPath(path, dstIsdAs, dstIP, dstPort); - } - - private RequestPath(Daemon.Path path, long dstIsdAs, InetAddress dstIP, int dstPort) { - super(path.getRaw().toByteArray(), dstIsdAs, dstIP, dstPort); - this.pathProtoc = path; - if (getRawPath().length == 0) { - // local AS has path length 0 - firstHop = new InetSocketAddress(getRemoteAddress(), getRemotePort()); - } else { - firstHop = getFirstHopAddress(pathProtoc); - } - } - - private Daemon.Path protoPath() { - if (pathProtoc == null) { - throw new IllegalStateException( - "Information is only available for paths that" - + " were retrieved directly from a path server."); - } - return pathProtoc; - } - - @Override - public InetSocketAddress getFirstHopAddress() throws UnknownHostException { - return firstHop; - } - - private InetSocketAddress getFirstHopAddress(Daemon.Path internalPath) { - try { - String underlayAddressString = internalPath.getInterface().getAddress().getAddress(); - int splitIndex = underlayAddressString.indexOf(':'); - InetAddress ip = IPHelper.toInetAddress(underlayAddressString.substring(0, splitIndex)); - int port = Integer.parseUnsignedInt(underlayAddressString.substring(splitIndex + 1)); - return new InetSocketAddress(ip, port); - } catch (UnknownHostException e) { - // This really should never happen, the first hop is a literal IP address. - throw new UncheckedIOException(e); - } - } - - /** - * @return Interface for exiting the local AS using this path. - * @throws IllegalStateException if this is path is only a raw path - */ - public Interface getInterface() { - return new Interface(protoPath().getInterface()); - } - - /** - * @return The list of interfaces the path is composed of. - * @throws IllegalStateException if this is path is only a raw path - */ - public List getInterfacesList() { - return Collections.unmodifiableList( - protoPath().getInterfacesList().stream() - .map(PathInterface::new) - .collect(Collectors.toList())); - } - - /** - * @return The maximum transmission unit (MTU) on the path. - * @throws IllegalStateException if this is path is only a raw path - */ - public int getMtu() { - return protoPath().getMtu(); - } - - /** - * @return The point in time when this path expires. In seconds since UNIX epoch. - * @throws IllegalStateException if this is path is only a raw path - */ - public long getExpiration() { - return protoPath().getExpiration().getSeconds(); - } - - /** - * @return Latency lists the latencies between any two consecutive interfaces. Entry i describes - * the latency between interface i and i+1. Consequently, there are N-1 entries for N - * interfaces. A 0-value indicates that the AS did not announce a latency for this hop. - * @throws IllegalStateException if this is path is only a raw path - */ - public List getLatencyList() { - return Collections.unmodifiableList( - protoPath().getLatencyList().stream() - .map(time -> (int) (time.getSeconds() * 1_000 + time.getNanos() / 1_000_000)) - .collect(Collectors.toList())); - } - - /** - * @return Bandwidth lists the bandwidth between any two consecutive interfaces, in Kbit/s. Entry - * i describes the bandwidth between interfaces i and i+1. A 0-value indicates that the AS did - * not announce a bandwidth for this hop. - * @throws IllegalStateException if this is path is only a raw path - */ - public List getBandwidthList() { - return protoPath().getBandwidthList(); - } - - /** - * @return Geo lists the geographical position of the border routers along the path. Entry i - * describes the position of the router for interface i. A 0-value indicates that the AS did - * not announce a position for this router. - * @throws IllegalStateException if this is path is only a raw path - */ - public List getGeoList() { - return Collections.unmodifiableList( - protoPath().getGeoList().stream().map(GeoCoordinates::new).collect(Collectors.toList())); - } - - /** - * @return LinkType contains the announced link type of inter-domain links. Entry i describes the - * link between interfaces 2*i and 2*i+1. - * @throws IllegalStateException if this is path is only a raw path - */ - public List getLinkTypeList() { - return Collections.unmodifiableList( - protoPath().getLinkTypeList().stream() - .map(linkType -> LinkType.values()[linkType.getNumber()]) - .collect(Collectors.toList())); - } - - /** - * @return InternalHops lists the number of AS internal hops for the ASes on path. Entry i - * describes the hop between interfaces 2*i+1 and 2*i+2 in the same AS. Consequently, there - * are no entries for the first and last ASes, as these are not traversed completely by the - * path. - * @throws IllegalStateException if this is path is only a raw path - */ - public List getInternalHopsList() { - return protoPath().getInternalHopsList(); + return RequestPathImpl.create(path, dstIsdAs, dstIP, dstPort); } - /** - * @return Notes contains the notes added by ASes on the path, in the order of occurrence. Entry i - * is the note of AS i on the path. - * @throws IllegalStateException if this is path is only a raw path - */ - public List getNotesList() { - return protoPath().getNotesList(); - } - - /** - * @return EpicAuths contains the EPIC authenticators used to calculate the PHVF and LHVF. - * @throws IllegalStateException if this is path is only a raw path - */ - public EpicAuths getEpicAuths() { - return new EpicAuths(protoPath().getEpicAuths()); - } - - public enum LinkType { - /** Unspecified link type. */ - LINK_TYPE_UNSPECIFIED, // = 0 - /** Direct physical connection. */ - LINK_TYPE_DIRECT, // = 1 - /** Connection with local routing/switching. */ - LINK_TYPE_MULTI_HOP, // = 2 - /** Connection overlayed over publicly routed Internet. */ - LINK_TYPE_OPEN_NET, // = 3 - } - - public static class EpicAuths { - private final byte[] authPhvf; - private final byte[] authLhvf; - - private EpicAuths(Daemon.EpicAuths epicAuths) { - this.authPhvf = epicAuths.getAuthPhvf().toByteArray(); - this.authLhvf = epicAuths.getAuthLhvf().toByteArray(); - } - - /** - * @return AuthPHVF is the authenticator use to calculate the PHVF. - */ - public byte[] getAuthPhvf() { - return authPhvf; - } - - /** - * @return AuthLHVF is the authenticator use to calculate the LHVF. - */ - public byte[] getAuthLhvf() { - return authLhvf; - } - } - - public static class Interface { - private final String address; - - private Interface(Daemon.Interface inter) { - this.address = inter.getAddress().getAddress(); - } - - /** - * @return Underlay address to exit through the interface. - */ - public String getAddress() { - return address; - } - } - - public static class PathInterface { - private final long isdAs; - - private final long id; - - private PathInterface(Daemon.PathInterface pathInterface) { - this.isdAs = pathInterface.getIsdAs(); - this.id = pathInterface.getId(); - } - - /** - * @return ISD-AS the interface belongs to. - */ - public long getIsdAs() { - return isdAs; - } - - /** - * @return ID of the interface in the AS. - */ - public long getId() { - return id; - } - } - - public static class GeoCoordinates { - private final float latitude; - private final float longitude; - private final String address; - - private GeoCoordinates(Daemon.GeoCoordinates geoCoordinates) { - this.latitude = geoCoordinates.getLatitude(); - this.longitude = geoCoordinates.getLongitude(); - this.address = geoCoordinates.getAddress(); - } - - /** - * @return Latitude of the geographic coordinate, in the WGS 84 datum. - */ - public float getLatitude() { - return latitude; - } - - /** - * @return Longitude of the geographic coordinate, in the WGS 84 datum. - */ - public float getLongitude() { - return longitude; - } - - /** - * @return Civic address of the location. - */ - public String getAddress() { - return address; - } - } + PathDetails getDetails(); } diff --git a/src/main/java/org/scion/jpan/RequestPathImpl.java b/src/main/java/org/scion/jpan/RequestPathImpl.java new file mode 100644 index 00000000..cd0dc28b --- /dev/null +++ b/src/main/java/org/scion/jpan/RequestPathImpl.java @@ -0,0 +1,61 @@ +// 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; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.concurrent.atomic.AtomicReference; +import org.scion.jpan.proto.daemon.Daemon; + +/** + * A RequestPath is a Path with additional meta information such as bandwidth, latency or geo + * coordinates. RequestPaths are created/returned by the ScionService when requesting a new path + * from the control service. + * + *

A RequestPath is immutable except for an atomic reference to PathDetails (PathDetails are + * immutable). The reference may be updated, for example when a path expires. + */ +class RequestPathImpl extends PathImpl implements RequestPath { + + private final AtomicReference details = new AtomicReference<>(); + + static RequestPathImpl create(Daemon.Path path, long dstIsdAs, InetAddress dstIP, int dstPort) { + return new RequestPathImpl(path, dstIsdAs, dstIP, dstPort); + } + + private RequestPathImpl(Daemon.Path path, long dstIsdAs, InetAddress dstIP, int dstPort) { + super(dstIsdAs, dstIP, dstPort); + this.details.set(PathDetails.create(path, dstIP, dstPort)); + } + + @Override + public InetSocketAddress getFirstHopAddress() throws UnknownHostException { + return getDetails().getFirstHopAddress(); + } + + @Override + public byte[] getRawPath() { + return getDetails().getRawPath(); + } + + public PathDetails getDetails() { + return details.get(); + } + + void setDetails(PathDetails details) { + this.details.set(details); + } +} diff --git a/src/main/java/org/scion/jpan/ResponsePath.java b/src/main/java/org/scion/jpan/ResponsePath.java index b53b4cc7..8a3ffb45 100644 --- a/src/main/java/org/scion/jpan/ResponsePath.java +++ b/src/main/java/org/scion/jpan/ResponsePath.java @@ -22,16 +22,12 @@ * ISD/AS, IP and port of the local host. This is mostly for convenience to avoid looking up this * information, but it also ensures that the return packet header contains the exact information * sent/expected by the client. + * + *

A ResponsePath is immutable and thus thread safe. */ -public class ResponsePath extends Path { +public interface ResponsePath extends Path { - private final InetSocketAddress firstHopAddress; - // The ResponsePath gets source information from the incoming packet. - private final long srcIsdAs; - private final InetAddress srcAddress; - private final int srcPort; - - public static ResponsePath create( + static ResponsePath create( byte[] rawPath, long srcIsdAs, InetAddress srcIP, @@ -40,55 +36,13 @@ public static ResponsePath create( InetAddress dstIP, int dstPort, InetSocketAddress firstHopAddress) { - return new ResponsePath( + return ResponsePathImpl.create( rawPath, srcIsdAs, srcIP, srcPort, dstIsdAs, dstIP, dstPort, firstHopAddress); } - private ResponsePath( - byte[] rawPath, - long srcIsdAs, - InetAddress srcIP, - int srcPort, - long dstIsdAs, - InetAddress dstIP, - int dstPort, - InetSocketAddress firstHopAddress) { - super(rawPath, dstIsdAs, dstIP, dstPort); - this.firstHopAddress = firstHopAddress; - this.srcIsdAs = srcIsdAs; - this.srcAddress = srcIP; - this.srcPort = srcPort; - } - - @Override - public InetSocketAddress getFirstHopAddress() { - return firstHopAddress; - } + long getLocalIsdAs(); - public long getLocalIsdAs() { - return srcIsdAs; - } + InetAddress getLocalAddress(); - public InetAddress getLocalAddress() { - return srcAddress; - } - - public int getLocalPort() { - return srcPort; - } - - @Override - public String toString() { - return "ResponsePath{" - + super.toString() - + ", firstHopAddress=" - + firstHopAddress - + ", localIsdAs=" - + srcIsdAs - + ", localAddress=" - + srcAddress - + ", localPort=" - + srcPort - + '}'; - } + int getLocalPort(); } diff --git a/src/main/java/org/scion/jpan/ResponsePathImpl.java b/src/main/java/org/scion/jpan/ResponsePathImpl.java new file mode 100644 index 00000000..1983efe6 --- /dev/null +++ b/src/main/java/org/scion/jpan/ResponsePathImpl.java @@ -0,0 +1,103 @@ +// 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; + +import java.net.InetAddress; +import java.net.InetSocketAddress; + +/** + * A ResponsePath is created/returned when receiving a packet. Besides being a Path, it contains + * ISD/AS, IP and port of the local host. This is mostly for convenience to avoid looking up this + * information, but it also ensures that the return packet header contains the exact information + * sent/expected by the client. + * + *

A ResponsePath is immutable and thus thread safe. + */ +class ResponsePathImpl extends PathImpl implements ResponsePath { + + private final InetSocketAddress firstHopAddress; + // The ResponsePath gets source information from the incoming packet. + private final long srcIsdAs; + private final InetAddress srcAddress; + private final int srcPort; + private final byte[] pathRaw; + + public static ResponsePathImpl create( + byte[] rawPath, + long srcIsdAs, + InetAddress srcIP, + int srcPort, + long dstIsdAs, + InetAddress dstIP, + int dstPort, + InetSocketAddress firstHopAddress) { + return new ResponsePathImpl( + rawPath, srcIsdAs, srcIP, srcPort, dstIsdAs, dstIP, dstPort, firstHopAddress); + } + + private ResponsePathImpl( + byte[] rawPath, + long srcIsdAs, + InetAddress srcIP, + int srcPort, + long dstIsdAs, + InetAddress dstIP, + int dstPort, + InetSocketAddress firstHopAddress) { + super(dstIsdAs, dstIP, dstPort); + this.firstHopAddress = firstHopAddress; + this.srcIsdAs = srcIsdAs; + this.srcAddress = srcIP; + this.srcPort = srcPort; + this.pathRaw = rawPath; + } + + @Override + public InetSocketAddress getFirstHopAddress() { + return firstHopAddress; + } + + public long getLocalIsdAs() { + return srcIsdAs; + } + + public InetAddress getLocalAddress() { + return srcAddress; + } + + public int getLocalPort() { + return srcPort; + } + + @Override + public byte[] getRawPath() { + return pathRaw; + } + + @Override + public String toString() { + return "ResponsePath{" + + super.toString() + + ", firstHopAddress=" + + firstHopAddress + + ", localIsdAs=" + + srcIsdAs + + ", localAddress=" + + srcAddress + + ", localPort=" + + srcPort + + '}'; + } +} diff --git a/src/main/java/org/scion/jpan/ScionDatagramChannel.java b/src/main/java/org/scion/jpan/ScionDatagramChannel.java index 8156d4c2..e9f80ee8 100644 --- a/src/main/java/org/scion/jpan/ScionDatagramChannel.java +++ b/src/main/java/org/scion/jpan/ScionDatagramChannel.java @@ -84,20 +84,23 @@ public ResponsePath receive(ByteBuffer userBuffer) throws IOException { * cannot be resolved to an ISD/AS. * @see java.nio.channels.DatagramChannel#send(ByteBuffer, SocketAddress) */ - public void send(ByteBuffer srcBuffer, SocketAddress destination) throws IOException { + public int send(ByteBuffer srcBuffer, SocketAddress destination) throws IOException { if (!(destination instanceof InetSocketAddress)) { throw new IllegalArgumentException("Address must be of type InetSocketAddress."); } + if (destination instanceof Path) { + return send(srcBuffer, (Path) destination); + } InetSocketAddress dst = (InetSocketAddress) destination; - Path path = getPathPolicy().filter(getOrCreateService().getPaths(dst)); - send(srcBuffer, path); + Path path = getOrCreateService().lookupAndGetPath(dst, getPathPolicy()); + return send(srcBuffer, path); } /** * Attempts to send the content of the buffer to the destinationAddress. * * @param srcBuffer Data to send - * @param path Path to destination. If this is a Request path and it is expired then it will + * @param path Path to destination. If this is a RequestPath, and it is expired, then it will * automatically be replaced with a new path. Expiration of ResponsePaths is not checked * @return either the path argument or a new path if the path was an expired RequestPath. Note * that ResponsePaths are not checked for expiration. @@ -105,22 +108,22 @@ public void send(ByteBuffer srcBuffer, SocketAddress destination) throws IOExcep * cannot be resolved to an ISD/AS. * @see java.nio.channels.DatagramChannel#send(ByteBuffer, SocketAddress) */ - public Path send(ByteBuffer srcBuffer, Path path) throws IOException { + public int send(ByteBuffer srcBuffer, Path path) throws IOException { writeLock().lock(); try { ByteBuffer buffer = getBufferSend(srcBuffer.remaining()); // + 8 for UDP overlay header length - Path actualPath = - checkPathAndBuildHeader( - buffer, path, srcBuffer.remaining() + 8, InternalConstants.HdrTypes.UDP); + checkPathAndBuildHeader( + buffer, path, srcBuffer.remaining() + 8, InternalConstants.HdrTypes.UDP); + int headerSize = buffer.position(); try { buffer.put(srcBuffer); } catch (BufferOverflowException e) { throw new IOException("Packet is larger than max send buffer size."); } buffer.flip(); - sendRaw(buffer, actualPath.getFirstHopAddress(), actualPath); - return actualPath; + int size = sendRaw(buffer, path); + return size - headerSize; } finally { writeLock().unlock(); } @@ -165,15 +168,16 @@ public int write(ByteBuffer src) throws IOException { try { checkOpen(); checkConnected(true); + Path path = getConnectionPath(); ByteBuffer buffer = getBufferSend(src.remaining()); int len = src.remaining(); // + 8 for UDP overlay header length - checkPathAndBuildHeader(buffer, getConnectionPath(), len + 8, InternalConstants.HdrTypes.UDP); + checkPathAndBuildHeader(buffer, path, len + 8, InternalConstants.HdrTypes.UDP); buffer.put(src); buffer.flip(); - int sent = sendRaw(buffer, getConnectionPath().getFirstHopAddress(), getConnectionPath()); + int sent = sendRaw(buffer, path); if (sent < buffer.limit() || buffer.remaining() > 0) { throw new ScionException("Failed to send all data."); } diff --git a/src/main/java/org/scion/jpan/ScionDatagramSocket.java b/src/main/java/org/scion/jpan/ScionDatagramSocket.java index b4ccca3f..6c2894ba 100644 --- a/src/main/java/org/scion/jpan/ScionDatagramSocket.java +++ b/src/main/java/org/scion/jpan/ScionDatagramSocket.java @@ -296,9 +296,10 @@ public void send(DatagramPacket packet) throws IOException { synchronized (pathCache) { path = pathCache.get(addr); if (path == null) { - path = channel.getPathPolicy().filter(channel.getOrCreateService2().getPaths(addr)); + path = channel.getOrCreateService().lookupAndGetPath(addr, channel.getPathPolicy()); } else if (path instanceof RequestPath - && ((RequestPath) path).getExpiration() > Instant.now().getEpochSecond()) { + && ((RequestPath) path).getDetails().getExpiration() + > Instant.now().getEpochSecond()) { // check expiration only for RequestPaths RequestPath request = (RequestPath) path; path = channel.getPathPolicy().filter(channel.getOrCreateService2().getPaths(request)); @@ -532,7 +533,9 @@ public void leaveGroup(SocketAddress mcastaddr, NetworkInterface netIf) { * * @return the current Path or `null` if not path is connected. * @see ScionDatagramChannel#getConnectionPath() + * @deprecated Please use getRemoteAddress() instead */ + @Deprecated public RequestPath getConnectionPath() { return (RequestPath) channel.getConnectionPath(); } diff --git a/src/main/java/org/scion/jpan/ScionService.java b/src/main/java/org/scion/jpan/ScionService.java index f65e8329..84c7598c 100644 --- a/src/main/java/org/scion/jpan/ScionService.java +++ b/src/main/java/org/scion/jpan/ScionService.java @@ -33,6 +33,7 @@ import java.net.InetSocketAddress; import java.net.SocketAddress; import java.nio.file.Paths; +import java.time.Instant; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @@ -306,8 +307,8 @@ List getPathListDaemon(long srcIsdAs, long dstIsdAs) { * @return All paths returned by the path service. * @throws IOException if an errors occurs while querying paths. */ + @Deprecated // Please use lookup() instead public List getPaths(InetSocketAddress dstAddress) throws IOException { - // Use getHostString() to avoid DNS reverse lookup. ScionAddress sa = getScionAddress(dstAddress.getHostString()); return getPaths(sa.getIsdAs(), sa.getInetAddress(), dstAddress.getPort()); } @@ -320,7 +321,6 @@ public List getPaths(InetSocketAddress dstAddress) throws IOExcepti * @return All paths returned by the path service. */ public List getPaths(long dstIsdAs, InetSocketAddress dstScionAddress) { - // TODO change method API name to make clear that this requires a SCION IP. return getPaths(dstIsdAs, dstScionAddress.getAddress(), dstScionAddress.getPort()); } @@ -338,19 +338,44 @@ public List getPaths(RequestPath path) { * Request paths from the local ISD/AS to the destination. * * @param dstIsdAs Destination ISD/AS - * @param dstAddress Destination IP address + * @param dstAddress A SCION-enabled Destination IP address * @param dstPort Destination port - * @return All paths returned by the path service. + * @return All paths returned by the path service. Returns an empty list if no paths are found. */ public List getPaths(long dstIsdAs, InetAddress dstAddress, int dstPort) { - long srcIsdAs = getLocalIsdAs(); - List paths = getPathList(srcIsdAs, dstIsdAs); - if (paths.isEmpty()) { - return Collections.emptyList(); + return getPaths(ScionAddress.create(dstIsdAs, dstAddress), dstPort); + } + + /** + * Resolves the address to a SCION address, request paths, and selects a path using the policy. + * + * @param dstAddr Destination address + * @param policy path policy + * @return All paths returned by the path service. + * @throws ScionException if the DNS/TXT lookup did not return a (valid) SCION address. + */ + public RequestPath lookupAndGetPath(InetSocketAddress dstAddr, PathPolicy policy) + throws ScionException { + if (policy == null) { + policy = PathPolicy.DEFAULT; } + return policy.filter(getPaths(lookupAddress(dstAddr.getHostString()), dstAddr.getPort())); + } + + /** + * Request paths from the local ISD/AS to the destination. + * + * @param dstAddress Destination SCION address + * @return All paths returned by the path service. + */ + public List getPaths(ScionAddress dstAddress, int dstPort) { + long srcIsdAs = getLocalIsdAs(); + List paths = getPathList(srcIsdAs, dstAddress.getIsdAs()); List scionPaths = new ArrayList<>(paths.size()); for (int i = 0; i < paths.size(); i++) { - scionPaths.add(RequestPath.create(paths.get(i), dstIsdAs, dstAddress, dstPort)); + scionPaths.add( + RequestPath.create( + paths.get(i), dstAddress.getIsdAs(), dstAddress.getInetAddress(), dstPort)); } return scionPaths; } @@ -377,7 +402,7 @@ public long getLocalIsdAs() { /** * @param hostName hostName of the host to resolve - * @return A ScionAddress + * @return The ISD/AS code for a hostname * @throws ScionException if the DNS/TXT lookup did not return a (valid) SCION address. */ public long getIsdAs(String hostName) throws ScionException { @@ -416,12 +441,20 @@ public long getIsdAs(String hostName) throws ScionException { throw new ScionException("No DNS TXT entry \"scion\" found for host: " + hostName); } + @Deprecated // Please use lookupScionAddress() instead. + public ScionAddress getScionAddress(String hostName) throws ScionException { + return lookupAddress(hostName); + } + /** + * Uses DNS and hostfiles to look up a SCION enabled IP address for a give host string. + * * @param hostName hostName of the host to resolve * @return A ScionAddress * @throws ScionException if the DNS/TXT lookup did not return a (valid) SCION address. */ - public ScionAddress getScionAddress(String hostName) throws ScionException { + @Deprecated // TODO NOW: remove this lookupAddress() method in favor of lookupSocketAddress() + ScionAddress lookupAddress(String hostName) throws ScionException { ScionAddress scionAddress = scionAddressCache.get(hostName); if (scionAddress != null) { return scionAddress; @@ -555,4 +588,21 @@ InetAddress getExternalIP(InetSocketAddress firstHopAddress) { } } } + + /** + * @param path RequestPath that may need refreshing + * @param pathPolicy PathPolicy for selecting a new path when required + * @param expiryMargin Expiry margin, i.e. a path is considered "expired" if expiry is less than + * expiryMargin seconds away. + * @return `true` if the path was updated, otherwise `false`. + */ + boolean refreshPath(RequestPath path, PathPolicy pathPolicy, int expiryMargin) { + if (Instant.now().getEpochSecond() + expiryMargin <= path.getDetails().getExpiration()) { + return false; + } + // expired, get new path + RequestPath newPath = pathPolicy.filter(getPaths(path)); + ((RequestPathImpl) path).setDetails(newPath.getDetails()); + return true; + } } diff --git a/src/main/java/org/scion/jpan/ScionUtil.java b/src/main/java/org/scion/jpan/ScionUtil.java index 7d65f42f..637306a5 100644 --- a/src/main/java/org/scion/jpan/ScionUtil.java +++ b/src/main/java/org/scion/jpan/ScionUtil.java @@ -132,14 +132,15 @@ public static String toStringPath(byte[] raw) { * @return ISD/AS codes and border outer interface IDs along the path. */ public static String toStringPath(RequestPath path) { - if (path.getInterfacesList().isEmpty()) { + PathDetails details = path.getDetails(); + if (details.getInterfacesList().isEmpty()) { return "[]"; } StringBuilder sb = new StringBuilder(); sb.append("["); - int nInterfcaces = path.getInterfacesList().size(); + int nInterfcaces = details.getInterfacesList().size(); for (int i = 0; i < nInterfcaces; i++) { - RequestPath.PathInterface pIf = path.getInterfacesList().get(i); + PathDetails.PathInterface pIf = details.getInterfacesList().get(i); if (i % 2 == 0) { sb.append(ScionUtil.toStringIA(pIf.getIsdAs())).append(" "); sb.append(pIf.getId()).append(">"); @@ -147,7 +148,7 @@ public static String toStringPath(RequestPath path) { sb.append(pIf.getId()).append(" "); } } - sb.append(ScionUtil.toStringIA(path.getInterfacesList().get(nInterfcaces - 1).getIsdAs())); + sb.append(ScionUtil.toStringIA(details.getInterfacesList().get(nInterfcaces - 1).getIsdAs())); sb.append("]"); return sb.toString(); } diff --git a/src/main/java/org/scion/jpan/ScmpChannel.java b/src/main/java/org/scion/jpan/ScmpChannel.java index 458c63fa..b03d44fa 100644 --- a/src/main/java/org/scion/jpan/ScmpChannel.java +++ b/src/main/java/org/scion/jpan/ScmpChannel.java @@ -229,7 +229,7 @@ Scmp.TimedMessage sendEchoRequest(Scmp.EchoMessage request) throws IOException { buffer, Scmp.Type.INFO_128, localPort, request.getSequenceNumber(), request.getData()); buffer.flip(); request.setSizeSent(buffer.remaining()); - sendRaw(buffer, path.getFirstHopAddress()); + sendRaw(buffer, path); int sizeReceived = receive(request); request.setSizeReceived(sizeReceived); @@ -261,7 +261,7 @@ Scmp.TimedMessage sendTracerouteRequest( int posPath = ScionHeaderParser.extractPathHeaderPosition(buffer); buffer.put(posPath + node.posHopFlags, node.hopFlags); - sendRaw(buffer, path.getFirstHopAddress()); + sendRaw(buffer, path); receive(request); return request; @@ -356,7 +356,7 @@ void sendEchoResponses() throws IOException { buffer, Scmp.Type.INFO_129, port, msg.getSequenceNumber(), msg.getData()); buffer.flip(); msg.setSizeSent(buffer.remaining()); - sendRaw(buffer, path.getFirstHopAddress()); + sendRaw(buffer, path); log.info("Responded to SCMP {} from {}", type, path.getRemoteAddress()); } else { log.info("Dropped SCMP message with type {} from {}", type, path.getRemoteAddress()); @@ -398,7 +398,9 @@ public void close() throws IOException { * * @return the current Path * @see ScionDatagramChannel#getConnectionPath() + * @deprecated Please use getRemoteAddress() instead */ + @Deprecated public RequestPath getConnectionPath() { return (RequestPath) channel.getConnectionPath(); } diff --git a/src/test/java/org/scion/jpan/api/DatagramChannelApiServerTest.java b/src/test/java/org/scion/jpan/api/DatagramChannelApiServerTest.java index 50d00e1e..8d0f0e53 100644 --- a/src/test/java/org/scion/jpan/api/DatagramChannelApiServerTest.java +++ b/src/test/java/org/scion/jpan/api/DatagramChannelApiServerTest.java @@ -85,9 +85,8 @@ void send_withoutService() throws IOException { new byte[4], 1, new InetSocketAddress("127.0.0.1", 1)); - Path p2 = channel.send(ByteBuffer.allocate(0), path); + channel.send(ByteBuffer.allocate(0), path); assertNull(channel.getService()); - assertEquals(path, p2); } } diff --git a/src/test/java/org/scion/jpan/api/DatagramChannelApiTest.java b/src/test/java/org/scion/jpan/api/DatagramChannelApiTest.java index e174854e..4893f6e4 100644 --- a/src/test/java/org/scion/jpan/api/DatagramChannelApiTest.java +++ b/src/test/java/org/scion/jpan/api/DatagramChannelApiTest.java @@ -133,7 +133,7 @@ void getLocalAddress_withSendResponsePath() throws IOException { InetSocketAddress local = channel.getLocalAddress(); assertTrue(local.getAddress().isAnyLocalAddress()); // double check that we used a responsePath - assertNull(channel.getConnectionPath()); + assertNull(channel.getRemoteAddress()); } } @@ -402,6 +402,17 @@ void getPathPolicy() throws IOException { } } + @Test + void send_bufferSize() throws IOException { + try (ScionDatagramChannel channel = ScionDatagramChannel.open()) { + int size0 = channel.send(ByteBuffer.allocate(0), ExamplePacket.PATH); + assertEquals(0, size0); + + int size100 = channel.send(ByteBuffer.wrap(new byte[100]), ExamplePacket.PATH); + assertEquals(100, size100); + } + } + @Test void send_bufferTooLarge() { RequestPath addr = ExamplePacket.PATH; @@ -469,13 +480,16 @@ void write_ChannelClosedFails() throws IOException { void send_disconnected_expiredRequestPath() throws IOException { // Expected behavior: expired paths should be replaced transparently. testExpired( - (channel, expiredPath) -> { + (channel, expiringPath) -> { ByteBuffer sendBuf = ByteBuffer.wrap(PingPongChannelHelper.MSG.getBytes()); try { - RequestPath newPath = (RequestPath) channel.send(sendBuf, expiredPath); - assertTrue(newPath.getExpiration() > expiredPath.getExpiration()); - assertTrue(Instant.now().getEpochSecond() < newPath.getExpiration()); - assertNull(channel.getConnectionPath()); + long oldExpiration = expiringPath.getDetails().getExpiration(); + assertTrue(Instant.now().getEpochSecond() > oldExpiration); + channel.send(sendBuf, expiringPath); + long newExpiration = expiringPath.getDetails().getExpiration(); + assertTrue(newExpiration > oldExpiration); + assertTrue(Instant.now().getEpochSecond() < newExpiration); + assertNull(channel.getRemoteAddress()); } catch (IOException e) { throw new RuntimeException(e); } @@ -487,13 +501,16 @@ void send_disconnected_expiredRequestPath() throws IOException { void send_connected_expiredRequestPath() throws IOException { // Expected behavior: expired paths should be replaced transparently. testExpired( - (channel, expiredPath) -> { + (channel, expiringPath) -> { ByteBuffer sendBuf = ByteBuffer.wrap(PingPongChannelHelper.MSG.getBytes()); try { - RequestPath newPath = (RequestPath) channel.send(sendBuf, expiredPath); - assertTrue(newPath.getExpiration() > expiredPath.getExpiration()); - assertTrue(Instant.now().getEpochSecond() < newPath.getExpiration()); - assertEquals(newPath, channel.getConnectionPath()); + long oldExpiration = expiringPath.getDetails().getExpiration(); + assertTrue(Instant.now().getEpochSecond() > oldExpiration); + channel.send(sendBuf, expiringPath); + long newExpiration = expiringPath.getDetails().getExpiration(); + assertTrue(newExpiration > oldExpiration); + assertTrue(Instant.now().getEpochSecond() < newExpiration); + assertEquals(expiringPath, channel.getRemoteAddress()); } catch (IOException e) { throw new RuntimeException(e); } @@ -509,9 +526,10 @@ void write_expiredRequestPath() throws IOException { ByteBuffer sendBuf = ByteBuffer.wrap(PingPongChannelHelper.MSG.getBytes()); try { channel.write(sendBuf); - RequestPath newPath = (RequestPath) channel.getConnectionPath(); - assertTrue(newPath.getExpiration() > expiredPath.getExpiration()); - assertTrue(Instant.now().getEpochSecond() < newPath.getExpiration()); + RequestPath newPath = channel.getConnectionPath(); + assertTrue( + newPath.getDetails().getExpiration() > expiredPath.getDetails().getExpiration()); + assertTrue(Instant.now().getEpochSecond() < newPath.getDetails().getExpiration()); } catch (IOException e) { throw new RuntimeException(e); } @@ -552,7 +570,7 @@ private RequestPath createExpiredPath(Path basePath) throws UnknownHostException basePath.getRemoteAddress(), basePath.getRemotePort(), basePath.getFirstHopAddress()); - assertTrue(Instant.now().getEpochSecond() > expiredPath.getExpiration()); + assertTrue(Instant.now().getEpochSecond() > expiredPath.getDetails().getExpiration()); return expiredPath; } @@ -561,22 +579,22 @@ void getConnectionPath() throws IOException { RequestPath addr = ExamplePacket.PATH; ByteBuffer buffer = ByteBuffer.allocate(50); try (ScionDatagramChannel channel = ScionDatagramChannel.open()) { - assertNull(channel.getConnectionPath()); + assertNull(channel.getRemoteAddress()); // send should NOT set a path channel.send(buffer, addr); - assertNull(channel.getConnectionPath()); + assertNull(channel.getRemoteAddress()); // connect should set a path channel.connect(addr); - assertNotNull(channel.getConnectionPath()); + assertNotNull(channel.getRemoteAddress()); channel.disconnect(); - assertNull(channel.getConnectionPath()); + assertNull(channel.getRemoteAddress()); // send should NOT set a path if (Util.getJavaMajorVersion() >= 14) { // This fails because of disconnect(), see https://bugs.openjdk.org/browse/JDK-8231880 channel.send(buffer, addr); - assertNull(channel.getConnectionPath()); + assertNull(channel.getRemoteAddress()); } } } diff --git a/src/test/java/org/scion/jpan/api/DatagramChannelErrorHandlingTest.java b/src/test/java/org/scion/jpan/api/DatagramChannelErrorHandlingTest.java index 3e2f8505..4c606407 100644 --- a/src/test/java/org/scion/jpan/api/DatagramChannelErrorHandlingTest.java +++ b/src/test/java/org/scion/jpan/api/DatagramChannelErrorHandlingTest.java @@ -58,12 +58,12 @@ void testErrorHandling() throws IOException { RequestPath path1 = paths.get(0); channel.connect(path0); channel.write(ByteBuffer.allocate(0)); - assertEquals(path0, channel.getConnectionPath()); + assertEquals(path0, channel.getRemoteAddress()); // TODO Use mock instead of daemon? MockNetwork.returnScmpErrorOnNextPacket(Scmp.TypeCode.TYPE_5); channel.write(ByteBuffer.allocate(0)); - assertEquals(path0, channel.getConnectionPath()); + assertEquals(path0, channel.getRemoteAddress()); assertEquals(1, scmpReceived.get()); // mock.setSendCallback((byteBuffer,path) -> {}); diff --git a/src/test/java/org/scion/jpan/api/DatagramSocketApiTest.java b/src/test/java/org/scion/jpan/api/DatagramSocketApiTest.java index f4356497..11661e0d 100644 --- a/src/test/java/org/scion/jpan/api/DatagramSocketApiTest.java +++ b/src/test/java/org/scion/jpan/api/DatagramSocketApiTest.java @@ -618,8 +618,9 @@ void send_connected_expiredRequestPath() throws IOException { try { socket.send(packet); RequestPath newPath = socket.getConnectionPath(); - assertTrue(newPath.getExpiration() > expiredPath.getExpiration()); - assertTrue(Instant.now().getEpochSecond() < newPath.getExpiration()); + assertTrue( + newPath.getDetails().getExpiration() > expiredPath.getDetails().getExpiration()); + assertTrue(Instant.now().getEpochSecond() < newPath.getDetails().getExpiration()); // assertNull(channel.getCurrentPath()); } catch (IOException e) { throw new RuntimeException(e); @@ -660,7 +661,7 @@ private RequestPath createExpiredPath(Path basePath) throws UnknownHostException basePath.getRemoteAddress(), basePath.getRemotePort(), basePath.getFirstHopAddress()); - assertTrue(Instant.now().getEpochSecond() > expiredPath.getExpiration()); + assertTrue(Instant.now().getEpochSecond() > expiredPath.getDetails().getExpiration()); return expiredPath; } diff --git a/src/test/java/org/scion/jpan/api/ScionServiceTest.java b/src/test/java/org/scion/jpan/api/ScionServiceTest.java index 011127ab..1c7cb0eb 100644 --- a/src/test/java/org/scion/jpan/api/ScionServiceTest.java +++ b/src/test/java/org/scion/jpan/api/ScionServiceTest.java @@ -125,8 +125,8 @@ void getPath() throws IOException { assertEquals(dstIA, path.getRemoteIsdAs()); assertEquals(36, path.getRawPath().length); - assertEquals("127.0.0.10:31004", path.getInterface().getAddress()); - assertEquals(2, path.getInterfacesList().size()); + assertEquals("127.0.0.10:31004", path.getDetails().getInterface().getAddress()); + assertEquals(2, path.getDetails().getInterfacesList().size()); // assertEquals(1, viewer.getInternalHopsList().size()); // assertEquals(0, viewer.getMtu()); // assertEquals(0, viewer.getLinkTypeList().size()); diff --git a/src/test/java/org/scion/jpan/demo/PingPongChannelClient.java b/src/test/java/org/scion/jpan/demo/PingPongChannelClient.java index 765986ef..904dde08 100644 --- a/src/test/java/org/scion/jpan/demo/PingPongChannelClient.java +++ b/src/test/java/org/scion/jpan/demo/PingPongChannelClient.java @@ -53,7 +53,7 @@ private static void run() throws IOException { channel.write(sendBuf); println( "Sent via " - + ScionUtil.toStringPath((RequestPath) channel.getConnectionPath()) + + ScionUtil.toStringPath((RequestPath) channel.getRemoteAddress()) + ": " + msg); diff --git a/src/test/java/org/scion/jpan/demo/ScmpEchoDemo.java b/src/test/java/org/scion/jpan/demo/ScmpEchoDemo.java index b85d2dce..b79ccf58 100644 --- a/src/test/java/org/scion/jpan/demo/ScmpEchoDemo.java +++ b/src/test/java/org/scion/jpan/demo/ScmpEchoDemo.java @@ -149,8 +149,8 @@ private void printPath(RequestPath path) { // ").append(channel.getLocalAddress().getAddress().getHostAddress()).append(nl); 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(" MTU: ").append(path.getDetails().getMtu()); + sb.append(" NextHop: ").append(path.getDetails().getInterface().getAddress()).append(nl); println(sb.toString()); } diff --git a/src/test/java/org/scion/jpan/demo/ScmpTracerouteDemo.java b/src/test/java/org/scion/jpan/demo/ScmpTracerouteDemo.java index db8d230d..911f489f 100644 --- a/src/test/java/org/scion/jpan/demo/ScmpTracerouteDemo.java +++ b/src/test/java/org/scion/jpan/demo/ScmpTracerouteDemo.java @@ -129,8 +129,8 @@ private void printPath(RequestPath path) { // sb.append(" ").append(channel.getLocalAddress().getAddress().getHostAddress()).append(nl); 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(" MTU: ").append(path.getDetails().getMtu()); + sb.append(" NextHop: ").append(path.getDetails().getInterface().getAddress()).append(nl); println(sb.toString()); } diff --git a/src/test/java/org/scion/jpan/demo/ShowpathsDemo.java b/src/test/java/org/scion/jpan/demo/ShowpathsDemo.java index 9510b54c..3ad8ccbc 100644 --- a/src/test/java/org/scion/jpan/demo/ShowpathsDemo.java +++ b/src/test/java/org/scion/jpan/demo/ShowpathsDemo.java @@ -114,9 +114,9 @@ private void runDemo(long destinationIA) throws IOException { + "] Hops: " + ScionUtil.toStringPath(path) + " MTU: " - + path.getMtu() + + path.getDetails().getMtu() + " NextHop: " - + path.getInterface().getAddress() + + path.getDetails().getInterface().getAddress() + " LocalIP: " + localIP; println(sb); diff --git a/src/test/java/org/scion/jpan/testutil/PingPongHelperBase.java b/src/test/java/org/scion/jpan/testutil/PingPongHelperBase.java index 73448d32..1b7b6255 100644 --- a/src/test/java/org/scion/jpan/testutil/PingPongHelperBase.java +++ b/src/test/java/org/scion/jpan/testutil/PingPongHelperBase.java @@ -107,11 +107,11 @@ void runPingPong(ServerFactory serverFactory, ClientFactory clientFactory, boole } InetSocketAddress serverAddress = servers[0].getLocalAddress(); MockDNS.install(MockNetwork.TINY_SRV_ISD_AS, serverAddress.getAddress()); - RequestPath scionAddress = Scion.defaultService().getPaths(serverAddress).get(0); + RequestPath requestPath = Scion.defaultService().lookupAndGetPath(serverAddress, null); Thread[] clients = new Thread[nClients]; for (int i = 0; i < clients.length; i++) { - clients[i] = clientFactory.create(i, scionAddress, nRounds); + clients[i] = clientFactory.create(i, requestPath, nRounds); clients[i].setName("Client-thread-" + i); clients[i].start(); }