From acd5645d8faaaa4131c9c424a98356b988091279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tilmann=20Z=C3=A4schke?= Date: Fri, 23 Feb 2024 15:26:09 +0100 Subject: [PATCH 1/4] initial --- .../org/scion/AbstractDatagramChannel.java | 15 +- src/main/java/org/scion/DatagramChannel.java | 16 +- src/main/java/org/scion/ScionService.java | 4 + .../org/scion/api/DatagramChannelApiTest.java | 23 +- .../api/DatagramChannelErrorHandlingTest.java | 214 ++++++++++++++++++ 5 files changed, 248 insertions(+), 24 deletions(-) create mode 100644 src/test/java/org/scion/api/DatagramChannelErrorHandlingTest.java diff --git a/src/main/java/org/scion/AbstractDatagramChannel.java b/src/main/java/org/scion/AbstractDatagramChannel.java index ed57b1def..753339081 100644 --- a/src/main/java/org/scion/AbstractDatagramChannel.java +++ b/src/main/java/org/scion/AbstractDatagramChannel.java @@ -37,7 +37,7 @@ abstract class AbstractDatagramChannel> imp private RequestPath path; private boolean cfgReportFailedValidation = false; private PathPolicy pathPolicy = PathPolicy.DEFAULT; - private ScionService service; + private final ScionService service; private int cfgExpirationSafetyMargin = ScionUtil.getPropertyOrEnv( Constants.PROPERTY_PATH_EXPIRY_MARGIN, @@ -46,7 +46,11 @@ abstract class AbstractDatagramChannel> imp private Consumer errorListener; protected AbstractDatagramChannel(ScionService service) throws IOException { - this.channel = java.nio.channels.DatagramChannel.open(); + this (service, DatagramChannel.open()); + } + + protected AbstractDatagramChannel(ScionService service, java.nio.channels.DatagramChannel channel) { + this.channel = channel; this.service = service; } @@ -78,16 +82,9 @@ public synchronized void setPathPolicy(PathPolicy pathPolicy) throws IOException } 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; } diff --git a/src/main/java/org/scion/DatagramChannel.java b/src/main/java/org/scion/DatagramChannel.java index 1a408f51b..16111f27f 100644 --- a/src/main/java/org/scion/DatagramChannel.java +++ b/src/main/java/org/scion/DatagramChannel.java @@ -29,22 +29,22 @@ public class DatagramChannel extends AbstractDatagramChannel private final ByteBuffer bufferReceive; private final ByteBuffer bufferSend; - protected DatagramChannel() throws IOException { - this(null); - } - - protected DatagramChannel(ScionService service) throws IOException { - super(service); + protected DatagramChannel(ScionService service, java.nio.channels.DatagramChannel channel) throws IOException { + super(service, channel); this.bufferReceive = ByteBuffer.allocateDirect(getOption(StandardSocketOptions.SO_RCVBUF)); this.bufferSend = ByteBuffer.allocateDirect(getOption(StandardSocketOptions.SO_SNDBUF)); } public static DatagramChannel open() throws IOException { - return new DatagramChannel(); + return open(Scion.defaultService()); } public static DatagramChannel open(ScionService service) throws IOException { - return new DatagramChannel(service); + return open(service, java.nio.channels.DatagramChannel.open()); + } + + public static DatagramChannel open(ScionService service, java.nio.channels.DatagramChannel channel) throws IOException { + return new DatagramChannel(service, channel); } // TODO we return `void` here. If we implement SelectableChannel diff --git a/src/main/java/org/scion/ScionService.java b/src/main/java/org/scion/ScionService.java index 5f45c9101..b76076b23 100644 --- a/src/main/java/org/scion/ScionService.java +++ b/src/main/java/org/scion/ScionService.java @@ -229,6 +229,10 @@ public DatagramChannel openChannel() throws IOException { return DatagramChannel.open(this); } + public DatagramChannel openChannel(java.nio.channels.DatagramChannel channel) throws IOException { + return DatagramChannel.open(this, channel); + } + Daemon.ASResponse getASInfo() { Daemon.ASRequest request = Daemon.ASRequest.newBuilder().setIsdAs(0).build(); Daemon.ASResponse response; diff --git a/src/test/java/org/scion/api/DatagramChannelApiTest.java b/src/test/java/org/scion/api/DatagramChannelApiTest.java index 42fa15b6c..eae1ac643 100644 --- a/src/test/java/org/scion/api/DatagramChannelApiTest.java +++ b/src/test/java/org/scion/api/DatagramChannelApiTest.java @@ -278,16 +278,25 @@ void isConnected_Path() throws IOException { } @Test - void getService() throws IOException { + void getService_default() throws IOException { + ScionService service1 = Scion.defaultService(); + ScionService service2 = Scion.newServiceWithDaemon(MockDaemon.DEFAULT_ADDRESS_STR); try (DatagramChannel channel = DatagramChannel.open()) { - assertEquals(Scion.defaultService(), channel.getService()); - ScionService service1 = channel.getService(); - ScionService service2 = Scion.newServiceWithDaemon(MockDaemon.DEFAULT_ADDRESS_STR); - channel.setService(service2); + assertEquals(service1, channel.getService()); + assertNotEquals(service2, channel.getService()); + } + service2.close(); + } + + @Test + void getService_non_default() throws IOException { + ScionService service1 = Scion.defaultService(); + ScionService service2 = Scion.newServiceWithDaemon(MockDaemon.DEFAULT_ADDRESS_STR); + try (DatagramChannel channel = DatagramChannel.open(service2)) { assertEquals(service2, channel.getService()); assertNotEquals(service1, channel.getService()); - service2.close(); } + service2.close(); } @Test @@ -516,7 +525,7 @@ void getCurrentPath() { // send should NOT set a path channel.send(buffer, addr); - // TODO assertNull(channel.getCurrentPath()); + assertNull(channel.getCurrentPath()); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/test/java/org/scion/api/DatagramChannelErrorHandlingTest.java b/src/test/java/org/scion/api/DatagramChannelErrorHandlingTest.java new file mode 100644 index 000000000..24f9972f0 --- /dev/null +++ b/src/test/java/org/scion/api/DatagramChannelErrorHandlingTest.java @@ -0,0 +1,214 @@ +// 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.api; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.net.*; +import java.net.DatagramSocket; +import java.nio.ByteBuffer; +import java.nio.channels.MembershipKey; +import java.nio.channels.spi.SelectorProvider; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; +import org.scion.*; +import org.scion.testutil.MockNetwork; +import org.scion.testutil.PingPongHelper; + +/** Test path switching (changing first hop) on DatagramChannel. */ +class DatagramChannelErrorHandlingTest { + + private final PathPolicy alternatingPolicy = + new PathPolicy() { + private int count = 0; + + @Override + public RequestPath filter(List paths) { + return paths.get(count++ % 2); + } + }; + + @AfterAll + public static void afterAll() { + // Defensive clean up + ScionService.closeDefault(); + } + + @Test + void testDummyChannel() throws IOException { +// java.nio.channels.DatagramChannel channel2 = new TestChannel(); +// DatagramChannel channel = null; +// try { +// channel = Scion.defaultService().openChannel(channel2); +// } finally { +// channel.close(); +// } + } + + @Test + void test() { +// PingPongHelper.Server serverFn = PingPongHelper::defaultServer; +// PingPongHelper.Client clientFn = this::client; +// PingPongHelper pph = new PingPongHelper(1, 2, 10); +// pph.runPingPong(serverFn, clientFn, false); +// assertEquals(2 * 10, MockNetwork.getForwardCount(0)); +// assertEquals(2 * 10, MockNetwork.getForwardCount(1)); +// assertEquals(2 * 2 * 10, MockNetwork.getAndResetForwardCount()); + } + + private void client(DatagramChannel channel, Path serverAddress, int id) throws IOException { + String message = PingPongHelper.MSG + "-" + id; + ByteBuffer sendBuf = ByteBuffer.wrap(message.getBytes()); + + // Use a path policy that alternates between 1st and 2nd path + // -> setPathPolicy() sets a new path! + channel.setPathPolicy(alternatingPolicy); + channel.write(sendBuf); + + // System.out.println("CLIENT: Receiving ... (" + channel.getLocalAddress() + ")"); + ByteBuffer response = ByteBuffer.allocate(512); + int len = channel.read(response); + assertEquals(message.length(), len); + + response.flip(); + String pong = Charset.defaultCharset().decode(response).toString(); + assertEquals(message, pong); + } + + private static class TestChannel extends java.nio.channels.DatagramChannel { + private boolean isOpen = true; + private boolean isConnected = false; + private boolean isBlocking = false; + private SocketAddress bindAddress; + private SocketAddress connectAddress; + + protected TestChannel() { + super(SelectorProvider.provider()); + } + + @Override + public java.nio.channels.DatagramChannel bind(SocketAddress socketAddress) throws IOException { + bindAddress = socketAddress; + return this; + } + + @Override + public java.nio.channels.DatagramChannel setOption(SocketOption socketOption, T t) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public T getOption(SocketOption socketOption) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public Set> supportedOptions() { + throw new UnsupportedOperationException(); + } + + @Override + public DatagramSocket socket() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isConnected() { + return isConnected; + } + + @Override + public java.nio.channels.DatagramChannel connect(SocketAddress socketAddress) throws IOException { + connectAddress = socketAddress; + isConnected = true; + return this; + } + + @Override + public java.nio.channels.DatagramChannel disconnect() throws IOException { + connectAddress = null; + isConnected = false; + return this; + } + + @Override + public SocketAddress getRemoteAddress() throws IOException { + return connectAddress; + } + + @Override + public SocketAddress receive(ByteBuffer byteBuffer) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public int send(ByteBuffer byteBuffer, SocketAddress socketAddress) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public int read(ByteBuffer byteBuffer) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public long read(ByteBuffer[] byteBuffers, int i, int i1) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public int write(ByteBuffer byteBuffer) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public long write(ByteBuffer[] byteBuffers, int i, int i1) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public SocketAddress getLocalAddress() throws IOException { + return bindAddress; + } + + @Override + public MembershipKey join(InetAddress inetAddress, NetworkInterface networkInterface) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public MembershipKey join(InetAddress inetAddress, NetworkInterface networkInterface, InetAddress inetAddress1) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + protected void implCloseSelectableChannel() throws IOException { + isConnected = false; + isOpen = false; + connectAddress = null; + bindAddress = null; + } + + @Override + protected void implConfigureBlocking(boolean b) throws IOException { + this.isBlocking = b; + } + } +} From 90dffc5b7f42392c0b273940a85e0a111649fd52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tilmann=20Z=C3=A4schke?= Date: Fri, 23 Feb 2024 15:31:13 +0100 Subject: [PATCH 2/4] initial --- .../org/scion/AbstractDatagramChannel.java | 5 ++- src/main/java/org/scion/DatagramChannel.java | 6 ++- .../api/DatagramChannelErrorHandlingTest.java | 43 ++++++++++--------- .../DatagramChannelPacketValidationTest.java | 3 +- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/scion/AbstractDatagramChannel.java b/src/main/java/org/scion/AbstractDatagramChannel.java index 753339081..159e649b5 100644 --- a/src/main/java/org/scion/AbstractDatagramChannel.java +++ b/src/main/java/org/scion/AbstractDatagramChannel.java @@ -46,10 +46,11 @@ abstract class AbstractDatagramChannel> imp private Consumer errorListener; protected AbstractDatagramChannel(ScionService service) throws IOException { - this (service, DatagramChannel.open()); + this(service, DatagramChannel.open()); } - protected AbstractDatagramChannel(ScionService service, java.nio.channels.DatagramChannel channel) { + protected AbstractDatagramChannel( + ScionService service, java.nio.channels.DatagramChannel channel) { this.channel = channel; this.service = service; } diff --git a/src/main/java/org/scion/DatagramChannel.java b/src/main/java/org/scion/DatagramChannel.java index 16111f27f..22e89e36a 100644 --- a/src/main/java/org/scion/DatagramChannel.java +++ b/src/main/java/org/scion/DatagramChannel.java @@ -29,7 +29,8 @@ public class DatagramChannel extends AbstractDatagramChannel private final ByteBuffer bufferReceive; private final ByteBuffer bufferSend; - protected DatagramChannel(ScionService service, java.nio.channels.DatagramChannel channel) throws IOException { + protected DatagramChannel(ScionService service, java.nio.channels.DatagramChannel channel) + throws IOException { super(service, channel); this.bufferReceive = ByteBuffer.allocateDirect(getOption(StandardSocketOptions.SO_RCVBUF)); this.bufferSend = ByteBuffer.allocateDirect(getOption(StandardSocketOptions.SO_SNDBUF)); @@ -43,7 +44,8 @@ public static DatagramChannel open(ScionService service) throws IOException { return open(service, java.nio.channels.DatagramChannel.open()); } - public static DatagramChannel open(ScionService service, java.nio.channels.DatagramChannel channel) throws IOException { + public static DatagramChannel open( + ScionService service, java.nio.channels.DatagramChannel channel) throws IOException { return new DatagramChannel(service, channel); } diff --git a/src/test/java/org/scion/api/DatagramChannelErrorHandlingTest.java b/src/test/java/org/scion/api/DatagramChannelErrorHandlingTest.java index 24f9972f0..3c5c1e090 100644 --- a/src/test/java/org/scion/api/DatagramChannelErrorHandlingTest.java +++ b/src/test/java/org/scion/api/DatagramChannelErrorHandlingTest.java @@ -25,11 +25,9 @@ import java.nio.charset.Charset; import java.util.List; import java.util.Set; - import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.scion.*; -import org.scion.testutil.MockNetwork; import org.scion.testutil.PingPongHelper; /** Test path switching (changing first hop) on DatagramChannel. */ @@ -53,24 +51,24 @@ public static void afterAll() { @Test void testDummyChannel() throws IOException { -// java.nio.channels.DatagramChannel channel2 = new TestChannel(); -// DatagramChannel channel = null; -// try { -// channel = Scion.defaultService().openChannel(channel2); -// } finally { -// channel.close(); -// } + // java.nio.channels.DatagramChannel channel2 = new TestChannel(); + // DatagramChannel channel = null; + // try { + // channel = Scion.defaultService().openChannel(channel2); + // } finally { + // channel.close(); + // } } @Test void test() { -// PingPongHelper.Server serverFn = PingPongHelper::defaultServer; -// PingPongHelper.Client clientFn = this::client; -// PingPongHelper pph = new PingPongHelper(1, 2, 10); -// pph.runPingPong(serverFn, clientFn, false); -// assertEquals(2 * 10, MockNetwork.getForwardCount(0)); -// assertEquals(2 * 10, MockNetwork.getForwardCount(1)); -// assertEquals(2 * 2 * 10, MockNetwork.getAndResetForwardCount()); + // PingPongHelper.Server serverFn = PingPongHelper::defaultServer; + // PingPongHelper.Client clientFn = this::client; + // PingPongHelper pph = new PingPongHelper(1, 2, 10); + // pph.runPingPong(serverFn, clientFn, false); + // assertEquals(2 * 10, MockNetwork.getForwardCount(0)); + // assertEquals(2 * 10, MockNetwork.getForwardCount(1)); + // assertEquals(2 * 2 * 10, MockNetwork.getAndResetForwardCount()); } private void client(DatagramChannel channel, Path serverAddress, int id) throws IOException { @@ -110,7 +108,8 @@ public java.nio.channels.DatagramChannel bind(SocketAddress socketAddress) throw } @Override - public java.nio.channels.DatagramChannel setOption(SocketOption socketOption, T t) throws IOException { + public java.nio.channels.DatagramChannel setOption(SocketOption socketOption, T t) + throws IOException { throw new UnsupportedOperationException(); } @@ -135,7 +134,8 @@ public boolean isConnected() { } @Override - public java.nio.channels.DatagramChannel connect(SocketAddress socketAddress) throws IOException { + public java.nio.channels.DatagramChannel connect(SocketAddress socketAddress) + throws IOException { connectAddress = socketAddress; isConnected = true; return this; @@ -189,12 +189,15 @@ public SocketAddress getLocalAddress() throws IOException { } @Override - public MembershipKey join(InetAddress inetAddress, NetworkInterface networkInterface) throws IOException { + public MembershipKey join(InetAddress inetAddress, NetworkInterface networkInterface) + throws IOException { throw new UnsupportedOperationException(); } @Override - public MembershipKey join(InetAddress inetAddress, NetworkInterface networkInterface, InetAddress inetAddress1) throws IOException { + public MembershipKey join( + InetAddress inetAddress, NetworkInterface networkInterface, InetAddress inetAddress1) + throws IOException { throw new UnsupportedOperationException(); } diff --git a/src/test/java/org/scion/api/DatagramChannelPacketValidationTest.java b/src/test/java/org/scion/api/DatagramChannelPacketValidationTest.java index 7a3e57183..68d84870c 100644 --- a/src/test/java/org/scion/api/DatagramChannelPacketValidationTest.java +++ b/src/test/java/org/scion/api/DatagramChannelPacketValidationTest.java @@ -23,6 +23,7 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.AfterAll; @@ -117,7 +118,7 @@ private void receive_validationFails_isBlocking_noThrow(boolean throwBad, boolea throws IOException, InterruptedException { barrier = new CountDownLatch(1); Thread serverThread = startServer(throwBad, isBlocking); - barrier.await(); // Wait for thread to start + barrier.await(1, TimeUnit.SECONDS); // Wait for thread to start // client - send bad message for (int i = 0; i < 10; i++) { From b87c85dbea9d0c7ef393062537a132b1d5fea323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tilmann=20Z=C3=A4schke?= Date: Mon, 26 Feb 2024 11:22:58 +0100 Subject: [PATCH 3/4] initial --- CHANGELOG.md | 4 + doc/Design.md | 29 +++ .../org/scion/AbstractDatagramChannel.java | 18 +- src/main/java/org/scion/DatagramChannel.java | 6 +- src/main/java/org/scion/ScionService.java | 5 +- .../org/scion/PackageVisibilityHelper.java | 18 +- .../api/DatagramChannelApiServerTest.java | 112 +++++++++++ .../org/scion/api/DatagramChannelApiTest.java | 12 +- .../api/DatagramChannelErrorHandlingTest.java | 129 ------------ .../scion/testutil/MockDatagramChannel.java | 184 ++++++++++++++++++ 10 files changed, 372 insertions(+), 145 deletions(-) create mode 100644 src/test/java/org/scion/api/DatagramChannelApiServerTest.java create mode 100644 src/test/java/org/scion/testutil/MockDatagramChannel.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 63de06783..2ca5d8fb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. [#14](https://github.com/tzaeschke/phtree-cpp/pull/14), [#15](https://github.com/tzaeschke/phtree-cpp/pull/15), [#17](https://github.com/tzaeschke/phtree-cpp/pull/17) +- BREAKING CHANGE:`ScionService` instances created via `Scion.newXYZ` + will not be considered by `Scion.defaultService()`. Also, `DatagramChannel.getService()` + does not create a service if none exists. + [#18](https://github.com/tzaeschke/phtree-cpp/pull/18) ### Fixed - Fixed: SCMP problem when pinging local AS. diff --git a/doc/Design.md b/doc/Design.md index be2b6efce..54c601e14 100644 --- a/doc/Design.md +++ b/doc/Design.md @@ -15,6 +15,35 @@ Also, when roaming, the provider may not actually support SCION. In this case th device would need to connect to some kind of gateway to connect to either a daemon or at least a topology server + control server. +## Server / Client + +Server functionality, such as `open()` and `receive()` -> `send(RequestPath)`, should _not_ require +any access to a daemon or control service. The main reason is that it ensures efficiency +by completely avoiding any type of interaction with daemon/CS. +Interaction with daemon/CS should not be denied but it should not occur during default operations. + +Client functionality will usually require a `ScionService`. If none is provided during `open()`, +a service will internally be requested via `Scion.defaultService()`. +While explicit initialization is usually preferable as a design choice, this implicit +initialization was implemented to allow usage of `open()` without additional arguments. +This allows being much closer to the native Java `DatagramChannel` API. + +An alternative would be to push the the delayed/implicit initialization into the `ScionService` +class. We would always have a `ScionService` instance, but it wouldn't initially connect to +a daemon or control service. The disadvantage is that creation of a `ScionService` would have +to fail "late", i.e. we can create it but only when we use it would be know whether it +is actually able to connect to something. + +## ScionService + +The `ScionService` implements all interactions with a local daemon or with a control service. +When a `ScionService` is required internally (e.g. by `DatagramChannel`) and none has been +specified, a `ScionService` will be requested (and if none has been created yet, created) via +`Scion.defaultService()`. + +A `ScionService` created via specific factory methods (`Scion.newServiceWithDNS`, etc) will _not_ be +used or returned by `Scion.defaultService()`. + ## Library dependencies etc - Use Maven (instead of Gradle or something else): Maven is still the most used framework and arguable the best ( diff --git a/src/main/java/org/scion/AbstractDatagramChannel.java b/src/main/java/org/scion/AbstractDatagramChannel.java index 159e649b5..88743f05c 100644 --- a/src/main/java/org/scion/AbstractDatagramChannel.java +++ b/src/main/java/org/scion/AbstractDatagramChannel.java @@ -37,7 +37,7 @@ abstract class AbstractDatagramChannel> imp private RequestPath path; private boolean cfgReportFailedValidation = false; private PathPolicy pathPolicy = PathPolicy.DEFAULT; - private final ScionService service; + private ScionService service; private int cfgExpirationSafetyMargin = ScionUtil.getPropertyOrEnv( Constants.PROPERTY_PATH_EXPIRY_MARGIN, @@ -59,6 +59,7 @@ protected synchronized void configureBlocking(boolean block) throws IOException channel.configureBlocking(block); } + // `protected` because it should not be visible in ScmpChannel API. protected synchronized boolean isBlocking() { return channel.isBlocking(); } @@ -82,6 +83,13 @@ public synchronized void setPathPolicy(PathPolicy pathPolicy) throws IOException } } + protected synchronized ScionService getOrCreateService() { + if (service == null) { + service = ScionService.defaultService(); + } + return this.service; + } + public synchronized ScionService getService() { return this.service; } @@ -139,7 +147,7 @@ public synchronized C connect(SocketAddress addr) throws IOException { throw new IllegalArgumentException( "connect() requires an InetSocketAddress or a ScionSocketAddress."); } - return connect(pathPolicy.filter(getService().getPaths((InetSocketAddress) addr))); + return connect(pathPolicy.filter(getOrCreateService().getPaths((InetSocketAddress) addr))); } /** @@ -174,7 +182,7 @@ public synchronized Path getCurrentPath() { return path; } - protected void setPath(RequestPath path) { + protected synchronized void setPath(RequestPath path) { this.path = path; } @@ -351,7 +359,7 @@ protected void buildHeaderNoRefresh( channel.connect(connection); } - srcIA = getService().getLocalIsdAs(); + srcIA = getOrCreateService().getLocalIsdAs(); // Get external host address. This must be done *after* refreshing the path! InetSocketAddress srcSocketAddress = (InetSocketAddress) channel.getLocalAddress(); srcAddress = srcSocketAddress.getAddress().getAddress(); @@ -381,7 +389,7 @@ protected RequestPath ensureUpToDate(RequestPath path) throws IOException { private RequestPath updatePath(RequestPath path) throws IOException { // expired, get new path - RequestPath newPath = pathPolicy.filter(getService().getPaths(path)); + RequestPath newPath = pathPolicy.filter(getOrCreateService().getPaths(path)); if (isConnected) { // equal to !isBound at this point if (!newPath.getFirstHopAddress().equals(this.connection)) { diff --git a/src/main/java/org/scion/DatagramChannel.java b/src/main/java/org/scion/DatagramChannel.java index 22e89e36a..9bc1efcc0 100644 --- a/src/main/java/org/scion/DatagramChannel.java +++ b/src/main/java/org/scion/DatagramChannel.java @@ -37,7 +37,7 @@ protected DatagramChannel(ScionService service, java.nio.channels.DatagramChanne } public static DatagramChannel open() throws IOException { - return open(Scion.defaultService()); + return open(null); } public static DatagramChannel open(ScionService service) throws IOException { @@ -87,7 +87,9 @@ public synchronized void send(ByteBuffer srcBuffer, SocketAddress destination) if (!(destination instanceof InetSocketAddress)) { throw new IllegalArgumentException("Address must be of type InetSocketAddress."); } - send(srcBuffer, getPathPolicy().filter(getService().getPaths((InetSocketAddress) destination))); + send( + srcBuffer, + getPathPolicy().filter(getOrCreateService().getPaths((InetSocketAddress) destination))); } /** diff --git a/src/main/java/org/scion/ScionService.java b/src/main/java/org/scion/ScionService.java index b76076b23..2f77139e9 100644 --- a/src/main/java/org/scion/ScionService.java +++ b/src/main/java/org/scion/ScionService.java @@ -171,10 +171,9 @@ static ScionService defaultService() { DEFAULT = new ScionService(daemonHost + ":" + daemonPort, Mode.DAEMON); return DEFAULT; } catch (ScionRuntimeException e) { - // Ignore + throw new ScionRuntimeException( + "Could not connect to daemon, DNS or bootstrap resource.", e); } - - throw new ScionRuntimeException("Could not connect to daemon, DNS or bootstrap resource."); } } diff --git a/src/test/java/org/scion/PackageVisibilityHelper.java b/src/test/java/org/scion/PackageVisibilityHelper.java index fd6c01fc3..e2c3c103e 100644 --- a/src/test/java/org/scion/PackageVisibilityHelper.java +++ b/src/test/java/org/scion/PackageVisibilityHelper.java @@ -15,10 +15,12 @@ package org.scion; import com.google.protobuf.ByteString; +import com.google.protobuf.Timestamp; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; import java.nio.ByteBuffer; +import java.time.Instant; import java.util.List; import org.scion.internal.InternalConstants; import org.scion.internal.ScionHeaderParser; @@ -69,10 +71,24 @@ public static RequestPath createDummyPath( Daemon.Interface.newBuilder() .setAddress(Daemon.Underlay.newBuilder().setAddress(firstHopString).build()) .build(); - Daemon.Path path = Daemon.Path.newBuilder().setRaw(bs).setInterface(inter).build(); + Timestamp ts = Timestamp.newBuilder().setSeconds(Instant.now().getEpochSecond() + 100).build(); + Daemon.Path path = + Daemon.Path.newBuilder().setRaw(bs).setInterface(inter).setExpiration(ts).build(); return RequestPath.create(path, dstIsdAs, dstHost, dstPort); } + public static ResponsePath createDummyResponsePath( + byte[] raw, + long srcIsdAs, + byte[] srcIP, + int srcPort, + long dstIsdAs, + byte[] dstIP, + int dstPort, + InetSocketAddress firstHop) { + return ResponsePath.create(raw, srcIsdAs, srcIP, srcPort, dstIsdAs, dstIP, dstPort, firstHop); + } + public static RequestPath createRequestPath110_112( Daemon.Path.Builder builder, long dstIsdAs, diff --git a/src/test/java/org/scion/api/DatagramChannelApiServerTest.java b/src/test/java/org/scion/api/DatagramChannelApiServerTest.java new file mode 100644 index 000000000..469eec30e --- /dev/null +++ b/src/test/java/org/scion/api/DatagramChannelApiServerTest.java @@ -0,0 +1,112 @@ +// 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.api; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.net.*; +import java.nio.ByteBuffer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.scion.*; +import org.scion.testutil.ExamplePacket; +import org.scion.testutil.MockDNS; +import org.scion.testutil.MockDaemon; +import org.scion.testutil.MockDatagramChannel; + +/** + * Test that typical "server" operations do not require a ScionService. + * + *

This is service-less operation is required for servers that should not use (or may not have) + * access to a daemon or control service. Service-less operation is mainly implemented to ensure + * good server performance by avoiding costly network calls to daemons etc. + */ +class DatagramChannelApiServerTest { + + private static final int dummyPort = 44444; + + @BeforeEach + public void beforeEach() throws IOException { + MockDaemon.closeDefault(); + MockDNS.clear(); + ScionService.closeDefault(); + } + + @AfterEach + public void afterEach() throws IOException { + MockDaemon.closeDefault(); + MockDNS.clear(); + ScionService.closeDefault(); + } + + @Test + void open_withoutService() throws IOException { + // check that open() (without service argument) does not internally require a ScionService. + try (DatagramChannel channel = DatagramChannel.open()) { + assertNull(channel.getService()); + } + } + + @Test + void bind_withoutService() throws IOException { + // check that open() (without service argument) does not internally require a ScionService. + try (DatagramChannel channel = DatagramChannel.open()) { + channel.bind(new InetSocketAddress("127.0.0.1", 12345)); + assertNull(channel.getService()); + } + } + + @Test + void send_withoutService() throws IOException { + // check that send(ResponsePath) does not internally require a ScionService. + try (DatagramChannel channel = DatagramChannel.open()) { + assertNull(channel.getService()); + ResponsePath path = + PackageVisibilityHelper.createDummyResponsePath( + new byte[0], + 1, + new byte[4], + 1, + 1, + new byte[4], + 1, + new InetSocketAddress("127.0.0.1", 1)); + Path p2 = channel.send(ByteBuffer.allocate(0), path); + assertNull(channel.getService()); + assertEquals(path, p2); + } + } + + @Test + void receive_withoutService() throws IOException { + // check that receive() does not internally require a ScionService. + SocketAddress addr = new InetSocketAddress("127.0.0.1", 12345); + MockDatagramChannel mock = MockDatagramChannel.open(); + mock.setReceiveCallback( + buf -> { + buf.put(ExamplePacket.PACKET_BYTES_SERVER_E2E_PING); + return addr; + }); + try (DatagramChannel channel = DatagramChannel.open(null, mock)) { + assertNull(channel.getService()); + ByteBuffer buffer = ByteBuffer.allocate(100); + Path path = channel.receive(buffer); + assertNull(channel.getService()); + assertEquals(addr, path.getFirstHopAddress()); + } + } +} diff --git a/src/test/java/org/scion/api/DatagramChannelApiTest.java b/src/test/java/org/scion/api/DatagramChannelApiTest.java index eae1ac643..9d24c83b6 100644 --- a/src/test/java/org/scion/api/DatagramChannelApiTest.java +++ b/src/test/java/org/scion/api/DatagramChannelApiTest.java @@ -18,10 +18,7 @@ import com.google.protobuf.Timestamp; import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.net.StandardSocketOptions; -import java.net.UnknownHostException; +import java.net.*; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.nio.channels.NotYetConnectedException; @@ -282,8 +279,13 @@ void getService_default() throws IOException { ScionService service1 = Scion.defaultService(); ScionService service2 = Scion.newServiceWithDaemon(MockDaemon.DEFAULT_ADDRESS_STR); try (DatagramChannel channel = DatagramChannel.open()) { - assertEquals(service1, channel.getService()); + assertNull(channel.getService()); + + // trigger service initialization in channel + RequestPath path = PackageVisibilityHelper.createDummyPath(); + channel.send(ByteBuffer.allocate(0), path); assertNotEquals(service2, channel.getService()); + assertEquals(service1, channel.getService()); } service2.close(); } diff --git a/src/test/java/org/scion/api/DatagramChannelErrorHandlingTest.java b/src/test/java/org/scion/api/DatagramChannelErrorHandlingTest.java index 3c5c1e090..6feb1d0a7 100644 --- a/src/test/java/org/scion/api/DatagramChannelErrorHandlingTest.java +++ b/src/test/java/org/scion/api/DatagramChannelErrorHandlingTest.java @@ -18,13 +18,9 @@ import java.io.IOException; import java.net.*; -import java.net.DatagramSocket; import java.nio.ByteBuffer; -import java.nio.channels.MembershipKey; -import java.nio.channels.spi.SelectorProvider; import java.nio.charset.Charset; import java.util.List; -import java.util.Set; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Test; import org.scion.*; @@ -89,129 +85,4 @@ private void client(DatagramChannel channel, Path serverAddress, int id) throws String pong = Charset.defaultCharset().decode(response).toString(); assertEquals(message, pong); } - - private static class TestChannel extends java.nio.channels.DatagramChannel { - private boolean isOpen = true; - private boolean isConnected = false; - private boolean isBlocking = false; - private SocketAddress bindAddress; - private SocketAddress connectAddress; - - protected TestChannel() { - super(SelectorProvider.provider()); - } - - @Override - public java.nio.channels.DatagramChannel bind(SocketAddress socketAddress) throws IOException { - bindAddress = socketAddress; - return this; - } - - @Override - public java.nio.channels.DatagramChannel setOption(SocketOption socketOption, T t) - throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public T getOption(SocketOption socketOption) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public Set> supportedOptions() { - throw new UnsupportedOperationException(); - } - - @Override - public DatagramSocket socket() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isConnected() { - return isConnected; - } - - @Override - public java.nio.channels.DatagramChannel connect(SocketAddress socketAddress) - throws IOException { - connectAddress = socketAddress; - isConnected = true; - return this; - } - - @Override - public java.nio.channels.DatagramChannel disconnect() throws IOException { - connectAddress = null; - isConnected = false; - return this; - } - - @Override - public SocketAddress getRemoteAddress() throws IOException { - return connectAddress; - } - - @Override - public SocketAddress receive(ByteBuffer byteBuffer) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public int send(ByteBuffer byteBuffer, SocketAddress socketAddress) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public int read(ByteBuffer byteBuffer) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public long read(ByteBuffer[] byteBuffers, int i, int i1) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public int write(ByteBuffer byteBuffer) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public long write(ByteBuffer[] byteBuffers, int i, int i1) throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public SocketAddress getLocalAddress() throws IOException { - return bindAddress; - } - - @Override - public MembershipKey join(InetAddress inetAddress, NetworkInterface networkInterface) - throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - public MembershipKey join( - InetAddress inetAddress, NetworkInterface networkInterface, InetAddress inetAddress1) - throws IOException { - throw new UnsupportedOperationException(); - } - - @Override - protected void implCloseSelectableChannel() throws IOException { - isConnected = false; - isOpen = false; - connectAddress = null; - bindAddress = null; - } - - @Override - protected void implConfigureBlocking(boolean b) throws IOException { - this.isBlocking = b; - } - } } diff --git a/src/test/java/org/scion/testutil/MockDatagramChannel.java b/src/test/java/org/scion/testutil/MockDatagramChannel.java new file mode 100644 index 000000000..4615b7b74 --- /dev/null +++ b/src/test/java/org/scion/testutil/MockDatagramChannel.java @@ -0,0 +1,184 @@ +// Copyright 2024 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.testutil; + +import java.io.IOException; +import java.net.*; +import java.nio.ByteBuffer; +import java.nio.channels.MembershipKey; +import java.nio.channels.spi.SelectorProvider; +import java.util.Set; +import java.util.function.Function; +import org.scion.ScionSocketOptions; + +public class MockDatagramChannel extends java.nio.channels.DatagramChannel { + private boolean isOpen = true; + private boolean isConnected = false; + private boolean isBlocking = false; + private SocketAddress bindAddress; + private SocketAddress connectAddress; + + private Function receiveCallback = + byteBuffer -> { + throw new UnsupportedOperationException(); + }; + + public static MockDatagramChannel open() throws IOException { + return new MockDatagramChannel(); + } + + protected MockDatagramChannel() { + super(SelectorProvider.provider()); + } + + public void setReceiveCallback(Function cb) { + receiveCallback = cb; + } + + @Override + public java.nio.channels.DatagramChannel bind(SocketAddress socketAddress) throws IOException { + bindAddress = socketAddress; + return this; + } + + @Override + public java.nio.channels.DatagramChannel setOption(SocketOption socketOption, T t) + throws IOException { + // 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 { + // channel.setOption(option, t); + // } + // return (C) this; + throw new UnsupportedOperationException(); + } + + @Override + public T getOption(SocketOption option) throws IOException { + if (ScionSocketOptions.SN_API_THROW_PARSER_FAILURE.equals(option)) { + return (T) (Boolean) false; // cfgReportFailedValidation; + } else if (ScionSocketOptions.SN_PATH_EXPIRY_MARGIN.equals(option)) { + return (T) (Integer) 10; // cfgExpirationSafetyMargin; + } else if (StandardSocketOptions.SO_RCVBUF.equals(option)) { + return (T) (Integer) 1000; + } else if (StandardSocketOptions.SO_SNDBUF.equals(option)) { + return (T) (Integer) 1000; + } + throw new UnsupportedOperationException(); + } + + @Override + public Set> supportedOptions() { + throw new UnsupportedOperationException(); + } + + @Override + public DatagramSocket socket() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isConnected() { + return isConnected; + } + + @Override + public java.nio.channels.DatagramChannel connect(SocketAddress socketAddress) throws IOException { + connectAddress = socketAddress; + isConnected = true; + return this; + } + + @Override + public java.nio.channels.DatagramChannel disconnect() throws IOException { + connectAddress = null; + isConnected = false; + return this; + } + + @Override + public SocketAddress getRemoteAddress() throws IOException { + return connectAddress; + } + + @Override + public SocketAddress receive(ByteBuffer byteBuffer) throws IOException { + return receiveCallback.apply(byteBuffer); + } + + @Override + public int send(ByteBuffer byteBuffer, SocketAddress socketAddress) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public int read(ByteBuffer byteBuffer) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public long read(ByteBuffer[] byteBuffers, int i, int i1) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public int write(ByteBuffer byteBuffer) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public long write(ByteBuffer[] byteBuffers, int i, int i1) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public SocketAddress getLocalAddress() throws IOException { + return bindAddress; + } + + @Override + public MembershipKey join(InetAddress inetAddress, NetworkInterface networkInterface) + throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public MembershipKey join( + InetAddress inetAddress, NetworkInterface networkInterface, InetAddress inetAddress1) + throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + protected void implCloseSelectableChannel() throws IOException { + isConnected = false; + isOpen = false; + connectAddress = null; + bindAddress = null; + } + + @Override + protected void implConfigureBlocking(boolean b) throws IOException { + this.isBlocking = b; + } +} From 8676d2dc7cdd0024a71395a892716a3f5c74061f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tilmann=20Z=C3=A4schke?= Date: Mon, 26 Feb 2024 12:11:51 +0100 Subject: [PATCH 4/4] initial --- src/main/java/org/scion/ScionService.java | 5 ----- src/test/java/org/scion/api/ScionTest.java | 2 ++ 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/scion/ScionService.java b/src/main/java/org/scion/ScionService.java index 2f77139e9..ab86840e8 100644 --- a/src/main/java/org/scion/ScionService.java +++ b/src/main/java/org/scion/ScionService.java @@ -121,11 +121,6 @@ protected ScionService(String addressOrHost, Mode mode) { } throw e; } - synchronized (LOCK) { - if (DEFAULT == null) { - DEFAULT = this; - } - } } /** diff --git a/src/test/java/org/scion/api/ScionTest.java b/src/test/java/org/scion/api/ScionTest.java index fa48620f9..b6843be79 100644 --- a/src/test/java/org/scion/api/ScionTest.java +++ b/src/test/java/org/scion/api/ScionTest.java @@ -181,6 +181,7 @@ void newServiceWithDNS() throws IOException { assertFalse(paths.isEmpty()); assertEquals(1, MockNetwork.getTopoServer().getAndResetCallCount()); assertEquals(1, MockNetwork.getControlServer().getAndResetCallCount()); + assertNotEquals(Scion.defaultService(), ss); } finally { MockNetwork.stopTiny(); } @@ -199,6 +200,7 @@ void newServiceWithBootstrapServer() throws IOException { assertFalse(paths.isEmpty()); assertEquals(1, MockNetwork.getTopoServer().getAndResetCallCount()); assertEquals(1, MockNetwork.getControlServer().getAndResetCallCount()); + assertNotEquals(Scion.defaultService(), ss); } finally { MockNetwork.stopTiny(); }