# HG changeset patch # User alanb # Date 1574238953 0 # Node ID 455612b3161a45f5661f329fb3d86a7ae2ba0937 # Parent ea044aedc2b6520cbd55cea5b988d0834d9d5c47 8231259: (dc) DatagramChannel::disconnect re-binds socket to the wildcard address (macOS) Reviewed-by: dfuchs, chegar diff -r ea044aedc2b6 -r 455612b3161a src/java.base/macosx/classes/sun/nio/ch/KQueueSelectorImpl.java --- a/src/java.base/macosx/classes/sun/nio/ch/KQueueSelectorImpl.java Wed Nov 20 09:12:07 2019 +0100 +++ b/src/java.base/macosx/classes/sun/nio/ch/KQueueSelectorImpl.java Wed Nov 20 08:35:53 2019 +0000 @@ -156,6 +156,13 @@ int newEvents = ski.translateInterestOps(); int registeredEvents = ski.registeredEvents(); + + // DatagramChannelImpl::disconnect has reset socket + if (ski.getAndClearReset() && registeredEvents != 0) { + KQueue.register(kqfd, fd, EVFILT_READ, EV_DELETE); + registeredEvents = 0; + } + if (newEvents != registeredEvents) { // add or delete interest in read events diff -r ea044aedc2b6 -r 455612b3161a src/java.base/share/classes/java/nio/channels/spi/AbstractSelectableChannel.java --- a/src/java.base/share/classes/java/nio/channels/spi/AbstractSelectableChannel.java Wed Nov 20 09:12:07 2019 +0100 +++ b/src/java.base/share/classes/java/nio/channels/spi/AbstractSelectableChannel.java Wed Nov 20 08:35:53 2019 +0000 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -34,6 +34,8 @@ import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; +import java.util.Arrays; +import java.util.function.Consumer; /** @@ -172,6 +174,20 @@ } /** + * Invokes an action for each key. + * + * This method is invoked by DatagramChannelImpl::disconnect. + */ + private void forEach(Consumer action) { + synchronized (keyLock) { + SelectionKey[] keys = this.keys; + if (keys != null) { + Arrays.stream(keys).filter(k -> k != null).forEach(action::accept); + } + } + } + + /** * Registers this channel with the given selector, returning a selection key. * *

This method first verifies that this channel is open and that the diff -r ea044aedc2b6 -r 455612b3161a src/java.base/share/classes/sun/nio/ch/DatagramChannelImpl.java --- a/src/java.base/share/classes/sun/nio/ch/DatagramChannelImpl.java Wed Nov 20 09:12:07 2019 +0100 +++ b/src/java.base/share/classes/sun/nio/ch/DatagramChannelImpl.java Wed Nov 20 08:35:53 2019 +0000 @@ -31,6 +31,7 @@ import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.lang.ref.Cleaner.Cleanable; +import java.lang.reflect.Method; import java.net.DatagramSocket; import java.net.Inet4Address; import java.net.Inet6Address; @@ -54,12 +55,18 @@ import java.nio.channels.MembershipKey; import java.nio.channels.NotYetConnectedException; import java.nio.channels.SelectionKey; +import java.nio.channels.spi.AbstractSelectableChannel; import java.nio.channels.spi.SelectorProvider; +import java.security.AccessController; +import java.security.PrivilegedExceptionAction; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Consumer; import jdk.internal.ref.CleanerFactory; import sun.net.ResourceManager; @@ -113,10 +120,13 @@ private long readerThread; private long writerThread; - // Binding and remote address (when connected) + // Local and remote (connected) address private InetSocketAddress localAddress; private InetSocketAddress remoteAddress; + // Local address prior to connecting + private InetSocketAddress initialLocalAddress; + // Socket adaptor, created lazily private static final VarHandle SOCKET; static { @@ -1103,6 +1113,9 @@ bindInternal(null); } + // capture local address before connect + initialLocalAddress = localAddress; + int n = Net.connect(family, fd, isa.getAddress(), @@ -1160,21 +1173,19 @@ remoteAddress = null; state = ST_UNCONNECTED; - // check whether rebind is needed - InetSocketAddress isa = Net.localAddress(fd); - if (isa.getPort() == 0) { - // On Linux, if bound to ephemeral port, - // disconnect does not preserve that port. - // In this case, try to rebind to the previous port. - int port = localAddress.getPort(); - localAddress = isa; // in case Net.bind fails - Net.bind(family, fd, isa.getAddress(), port); - isa = Net.localAddress(fd); // refresh address - assert isa.getPort() == port; + // refresh localAddress, should be same as it was prior to connect + localAddress = Net.localAddress(fd); + try { + if (!localAddress.equals(initialLocalAddress)) { + // Workaround connect(2) issues on Linux and macOS + repairSocket(initialLocalAddress); + assert (localAddress != null) + && localAddress.equals(Net.localAddress(fd)) + && localAddress.equals(initialLocalAddress); + } + } finally { + initialLocalAddress = null; } - - // refresh localAddress - localAddress = isa; } } finally { writeLock.unlock(); @@ -1186,6 +1197,134 @@ } /** + * "Repair" the channel's socket after a disconnect that didn't restore the + * local address. + * + * On Linux, connect(2) dissolves the association but changes the local port + * to 0 when it was initially bound to an ephemeral port. The workaround here + * is to rebind to the original port. + * + * On macOS, connect(2) dissolves the association but rebinds the socket to + * the wildcard address when it was initially bound to a specific address. + * The workaround here is to re-create the socket. + */ + private void repairSocket(InetSocketAddress target) + throws IOException + { + assert Thread.holdsLock(stateLock); + + // Linux: try to bind the socket to the original address/port + if (localAddress.getPort() == 0) { + assert localAddress.getAddress().equals(target.getAddress()); + Net.bind(family, fd, target.getAddress(), target.getPort()); + localAddress = Net.localAddress(fd); + return; + } + + // capture the value of all existing socket options + Map, Object> map = new HashMap<>(); + for (SocketOption option : supportedOptions()) { + Object value = getOption(option); + if (value != null) { + map.put(option, value); + } + } + + // macOS: re-create the socket. + FileDescriptor newfd = Net.socket(family, false); + try { + // copy the socket options that are protocol family agnostic + for (Map.Entry, Object> e : map.entrySet()) { + SocketOption option = e.getKey(); + if (SocketOptionRegistry.findOption(option, Net.UNSPEC) != null) { + Object value = e.getValue(); + try { + Net.setSocketOption(newfd, Net.UNSPEC, option, value); + } catch (IOException ignore) { } + } + } + + // copy the blocking mode + if (!isBlocking()) { + IOUtil.configureBlocking(newfd, false); + } + + // dup this channel's socket to the new socket. If this succeeds then + // fd will reference the new socket. If it fails then it will still + // reference the old socket. + nd.dup(newfd, fd); + } finally { + // release the file descriptor + nd.close(newfd); + } + + // bind to the original local address + try { + Net.bind(family, fd, target.getAddress(), target.getPort()); + } catch (IOException ioe) { + // bind failed, socket is left unbound + localAddress = null; + throw ioe; + } + + // restore local address + localAddress = Net.localAddress(fd); + + // restore all socket options (including those set in first pass) + for (Map.Entry, Object> e : map.entrySet()) { + @SuppressWarnings("unchecked") + SocketOption option = (SocketOption) e.getKey(); + Object value = e.getValue(); + try { + setOption(option, value); + } catch (IOException ignore) { } + } + + // restore multicast group membership + MembershipRegistry registry = this.registry; + if (registry != null) { + registry.forEach(k -> { + if (k instanceof MembershipKeyImpl.Type6) { + MembershipKeyImpl.Type6 key6 = (MembershipKeyImpl.Type6) k; + Net.join6(fd, key6.groupAddress(), key6.index(), key6.source()); + } else { + MembershipKeyImpl.Type4 key4 = (MembershipKeyImpl.Type4) k; + Net.join4(fd, key4.groupAddress(), key4.interfaceAddress(), key4.source()); + } + }); + } + + // reset registration in all Selectors that this channel is registered with + AbstractSelectableChannels.forEach(this, SelectionKeyImpl::reset); + } + + /** + * Defines static methods to access AbstractSelectableChannel non-public members. + */ + private static class AbstractSelectableChannels { + private static final Method FOREACH; + static { + try { + PrivilegedExceptionAction pae = () -> { + Method m = AbstractSelectableChannel.class.getDeclaredMethod("forEach", Consumer.class); + m.setAccessible(true); + return m; + }; + FOREACH = AccessController.doPrivileged(pae); + } catch (Exception e) { + throw new InternalError(e); + } + } + static void forEach(AbstractSelectableChannel ch, Consumer action) { + try { + FOREACH.invoke(ch, action); + } catch (Exception e) { + throw new InternalError(e); + } + } + } + + /** * Joins channel's socket to the given group/interface and * optional source address. */ diff -r ea044aedc2b6 -r 455612b3161a src/java.base/share/classes/sun/nio/ch/MembershipRegistry.java --- a/src/java.base/share/classes/sun/nio/ch/MembershipRegistry.java Wed Nov 20 09:12:07 2019 +0100 +++ b/src/java.base/share/classes/sun/nio/ch/MembershipRegistry.java Wed Nov 20 08:35:53 2019 +0000 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2008, 2009, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2008, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,10 +25,14 @@ package sun.nio.ch; -import java.nio.channels.*; import java.net.InetAddress; import java.net.NetworkInterface; -import java.util.*; +import java.nio.channels.MembershipKey; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; /** * Simple registry of membership keys for a MulticastChannel. @@ -38,8 +42,8 @@ class MembershipRegistry { - // map multicast group to keys - private Map> groups = null; + // map multicast group to list of keys + private Map> groups; MembershipRegistry() { } @@ -116,16 +120,29 @@ } } + @FunctionalInterface + interface ThrowingConsumer { + void accept(T action) throws X; + } + + /** + * Invoke an action for each key in the registry + */ + + void forEach(ThrowingConsumer action) throws X { + if (groups != null) { + for (List keys : groups.values()) { + for (MembershipKeyImpl key : keys) { + action.accept(key); + } + } + } + } + /** * Invalidate all keys in the registry */ void invalidateAll() { - if (groups != null) { - for (InetAddress group: groups.keySet()) { - for (MembershipKeyImpl key: groups.get(group)) { - key.invalidate(); - } - } - } + forEach(MembershipKeyImpl::invalidate); } } diff -r ea044aedc2b6 -r 455612b3161a src/java.base/share/classes/sun/nio/ch/NativeDispatcher.java --- a/src/java.base/share/classes/sun/nio/ch/NativeDispatcher.java Wed Nov 20 09:12:07 2019 +0100 +++ b/src/java.base/share/classes/sun/nio/ch/NativeDispatcher.java Wed Nov 20 08:35:53 2019 +0000 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,15 +25,15 @@ package sun.nio.ch; -import java.io.*; +import java.io.FileDescriptor; +import java.io.IOException; /** * Allows different platforms to call different native methods * for read and write operations. */ -abstract class NativeDispatcher -{ +abstract class NativeDispatcher { abstract int read(FileDescriptor fd, long address, int len) throws IOException; @@ -77,4 +77,13 @@ // Do nothing by default; this is only needed on Unix } + /** + * Duplicates a file descriptor. + * @param fd1 the file descriptor to duplicate + * @param fd2 the new file descriptor, the socket or file that it is connected + * to will be closed by this method + */ + void dup(FileDescriptor fd1, FileDescriptor fd2) throws IOException { + throw new UnsupportedOperationException(); + } } diff -r ea044aedc2b6 -r 455612b3161a src/java.base/share/classes/sun/nio/ch/SelectionKeyImpl.java --- a/src/java.base/share/classes/sun/nio/ch/SelectionKeyImpl.java Wed Nov 20 09:12:07 2019 +0100 +++ b/src/java.base/share/classes/sun/nio/ch/SelectionKeyImpl.java Wed Nov 20 08:35:53 2019 +0000 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -58,6 +58,9 @@ // registered events in kernel, used by some Selector implementations private int registeredEvents; + // registered events need to be reset, used by some Selector implementations + private volatile boolean reset; + // index of key in pollfd array, used by some Selector implementations private int index; @@ -184,6 +187,26 @@ index = i; } + /** + * Sets the reset flag, re-queues the key, and wakeups up the Selector + */ + void reset() { + reset = true; + selector.setEventOps(this); + selector.wakeup(); + } + + /** + * Clears the reset flag, returning the previous value of the flag + */ + boolean getAndClearReset() { + assert Thread.holdsLock(selector); + boolean r = reset; + if (r) + reset = false; + return r; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); diff -r ea044aedc2b6 -r 455612b3161a src/java.base/unix/classes/sun/nio/ch/DatagramDispatcher.java --- a/src/java.base/unix/classes/sun/nio/ch/DatagramDispatcher.java Wed Nov 20 09:12:07 2019 +0100 +++ b/src/java.base/unix/classes/sun/nio/ch/DatagramDispatcher.java Wed Nov 20 08:35:53 2019 +0000 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2013, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,16 +25,16 @@ package sun.nio.ch; -import java.io.*; -import java.net.*; +import java.io.FileDescriptor; +import java.io.IOException; /** * Allows different platforms to call different native methods * for read and write operations. */ -class DatagramDispatcher extends NativeDispatcher -{ +class DatagramDispatcher extends NativeDispatcher { + static { IOUtil.load(); } @@ -63,6 +63,10 @@ FileDispatcherImpl.preClose0(fd); } + void dup(FileDescriptor fd1, FileDescriptor fd2) throws IOException { + FileDispatcherImpl.dup0(fd1, fd2); + } + static native int read0(FileDescriptor fd, long address, int len) throws IOException; diff -r ea044aedc2b6 -r 455612b3161a src/java.base/unix/classes/sun/nio/ch/FileDispatcherImpl.java --- a/src/java.base/unix/classes/sun/nio/ch/FileDispatcherImpl.java Wed Nov 20 09:12:07 2019 +0100 +++ b/src/java.base/unix/classes/sun/nio/ch/FileDispatcherImpl.java Wed Nov 20 08:35:53 2019 +0000 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -108,6 +108,10 @@ preClose0(fd); } + void dup(FileDescriptor fd1, FileDescriptor fd2) throws IOException { + dup0(fd1, fd2); + } + FileDescriptor duplicateForMapping(FileDescriptor fd) { // file descriptor not required for mapping operations; okay // to return invalid file descriptor. @@ -176,6 +180,8 @@ static native void preClose0(FileDescriptor fd) throws IOException; + static native void dup0(FileDescriptor fd1, FileDescriptor fd2) throws IOException; + static native void closeIntFD(int fd) throws IOException; static native int setDirect0(FileDescriptor fd) throws IOException; diff -r ea044aedc2b6 -r 455612b3161a src/java.base/unix/native/libnio/ch/FileDispatcherImpl.c --- a/src/java.base/unix/native/libnio/ch/FileDispatcherImpl.c Wed Nov 20 09:12:07 2019 +0100 +++ b/src/java.base/unix/native/libnio/ch/FileDispatcherImpl.c Wed Nov 20 08:35:53 2019 +0000 @@ -312,6 +312,14 @@ } JNIEXPORT void JNICALL +Java_sun_nio_ch_FileDispatcherImpl_dup0(JNIEnv *env, jobject this, jobject fdo1, jobject fdo2) +{ + if (dup2(fdval(env, fdo1), fdval(env, fdo2)) < 0) { + JNU_ThrowIOExceptionWithLastError(env, "dup2 failed"); + } +} + +JNIEXPORT void JNICALL Java_sun_nio_ch_FileDispatcherImpl_closeIntFD(JNIEnv *env, jclass clazz, jint fd) { closeFileDescriptor(env, fd); diff -r ea044aedc2b6 -r 455612b3161a test/jdk/java/nio/channels/DatagramChannel/AddressesAfterDisconnect.java --- a/test/jdk/java/nio/channels/DatagramChannel/AddressesAfterDisconnect.java Wed Nov 20 09:12:07 2019 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,109 +0,0 @@ -/* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. - * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. - * - * This code is free software; you can redistribute it and/or modify it - * under the terms of the GNU General Public License version 2 only, as - * published by the Free Software Foundation. - * - * This code is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License - * version 2 for more details (a copy is included in the LICENSE file that - * accompanied this code). - * - * You should have received a copy of the GNU General Public License version - * 2 along with this work; if not, write to the Free Software Foundation, - * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. - * - * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA - * or visit www.oracle.com if you need additional information or have any - * questions. - */ - -/* @test - * @library /test/lib - * @summary Test DatagramChannel local address after disconnect. - * @requires (os.family != "mac") - * @run testng/othervm AddressesAfterDisconnect - * @run testng/othervm -Djava.net.preferIPv6Addresses=true AddressesAfterDisconnect - * @run testng/othervm -Djava.net.preferIPv4Stack=true AddressesAfterDisconnect - */ - -import jdk.test.lib.net.IPSupport; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.net.StandardProtocolFamily; -import java.nio.channels.DatagramChannel; - -import org.testng.annotations.Test; -import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertTrue; -import static org.testng.Assert.assertFalse; - -public class AddressesAfterDisconnect { - - public static void main(String[] args) throws IOException { - new AddressesAfterDisconnect().execute(); - } - - @Test - public void execute() throws IOException { - IPSupport.throwSkippedExceptionIfNonOperational(); - boolean preferIPv6 = Boolean.getBoolean("java.net.preferIPv6Addresses"); - - // test with default protocol family - try (DatagramChannel dc = DatagramChannel.open()) { - System.out.println("Test with default"); - dc.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); - test(dc); - test(dc); - } - - if (IPSupport.hasIPv6()) { - // test with IPv6 only - System.out.println("Test with IPv6 only"); - try (DatagramChannel dc = DatagramChannel.open(StandardProtocolFamily.INET6)) { - dc.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); - test(dc); - test(dc); - } - } - - if (IPSupport.hasIPv4() && !preferIPv6) { - // test with IPv4 only - System.out.println("Test with IPv4 only"); - try (DatagramChannel dc = DatagramChannel.open(StandardProtocolFamily.INET)) { - dc.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); - test(dc); - test(dc); - } - } - } - - /** - * Connect DatagramChannel to a server, write a datagram and disconnect. Invoke - * a second or subsequent time with the same DatagramChannel instance to check - * that disconnect works as expected. - */ - static void test(DatagramChannel dc) throws IOException { - SocketAddress local = dc.getLocalAddress(); - try (DatagramChannel server = DatagramChannel.open()) { - server.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); - SocketAddress remote = server.getLocalAddress(); - dc.connect(remote); - assertTrue(dc.isConnected()); - // comment the following two lines on OS X to see JDK-8231259 - assertEquals(dc.getLocalAddress(), local, "local address after connect"); - assertEquals(dc.getRemoteAddress(), remote, "remote address after connect"); - dc.disconnect(); - assertFalse(dc.isConnected()); - assertEquals(dc.getLocalAddress(), local, "local address after disconnect"); - assertEquals(dc.getRemoteAddress(), null, "remote address after disconnect"); - } - } - -} diff -r ea044aedc2b6 -r 455612b3161a test/jdk/java/nio/channels/DatagramChannel/AfterDisconnect.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/nio/channels/DatagramChannel/AfterDisconnect.java Wed Nov 20 08:35:53 2019 +0000 @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* @test + * @bug 8231880 8231258 + * @library /test/lib + * @summary Test DatagramChannel bound to specific address/ephemeral port after disconnect + * @run testng/othervm AfterDisconnect + * @run testng/othervm -Djava.net.preferIPv4Stack=true AfterDisconnect + * @run testng/othervm -Djava.net.preferIPv6Addresses=true AfterDisconnect + */ + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.net.SocketOption; +import java.net.StandardSocketOptions; +import java.net.StandardProtocolFamily; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.MembershipKey; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.HashMap; +import java.util.Map; + +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +import jdk.test.lib.net.IPSupport; + +public class AfterDisconnect { + + @Test + public void execute() throws IOException { + IPSupport.throwSkippedExceptionIfNonOperational(); + boolean preferIPv6 = Boolean.getBoolean("java.net.preferIPv6Addresses"); + InetAddress lb = InetAddress.getLoopbackAddress(); + + // test with default protocol family + try (DatagramChannel dc = DatagramChannel.open()) { + System.out.println("Test with default"); + dc.bind(new InetSocketAddress(lb, 0)); + test(dc); + test(dc); + } + + // test with IPv6 socket + if (IPSupport.hasIPv6()) { + System.out.println("Test with IPv6 socket"); + try (DatagramChannel dc = DatagramChannel.open(StandardProtocolFamily.INET6)) { + dc.bind(new InetSocketAddress(lb, 0)); + test(dc); + test(dc); + } + } + + // test with IPv4 socket + if (IPSupport.hasIPv4() && !preferIPv6) { + System.out.println("Test with IPv4 socket"); + try (DatagramChannel dc = DatagramChannel.open(StandardProtocolFamily.INET)) { + dc.bind(new InetSocketAddress(lb, 0)); + test(dc); + test(dc); + } + } + } + + void test(DatagramChannel dc) throws IOException { + testLocalAddress(dc); + testSocketOptions(dc); + testSelectorRegistration(dc); + testMulticastGroups(dc); + } + + /** + * Test that disconnect restores local address + */ + void testLocalAddress(DatagramChannel dc) throws IOException { + try (DatagramChannel server = DatagramChannel.open()) { + server.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); + + SocketAddress local = dc.getLocalAddress(); + SocketAddress remote = server.getLocalAddress(); + + dc.connect(remote); + assertTrue(dc.isConnected()); + assertEquals(dc.getLocalAddress(), local); + assertEquals(dc.getRemoteAddress(), remote); + + dc.disconnect(); + assertFalse(dc.isConnected()); + assertEquals(dc.getLocalAddress(), local); + assertTrue(dc.getRemoteAddress() == null); + } + } + + /** + * Test that disconnect does not change socket options + */ + void testSocketOptions(DatagramChannel dc) throws IOException { + // set a few socket options + dc.setOption(StandardSocketOptions.SO_SNDBUF, 32*1024); + dc.setOption(StandardSocketOptions.SO_RCVBUF, 64*1024); + InetAddress ia = dc.socket().getLocalAddress(); + NetworkInterface ni = NetworkInterface.getByInetAddress(ia); + if (ni != null && ni.supportsMulticast()) + dc.setOption(StandardSocketOptions.IP_MULTICAST_IF, ni); + + // capture values of socket options + Map, Object> map = options(dc); + + dc.connect(dc.getLocalAddress()); + dc.disconnect(); + + // check socket options have not changed + assertEquals(map, options(dc)); + } + + /** + * Returns a map of the given channel's socket options and values. + */ + private Map, Object> options(DatagramChannel dc) throws IOException { + Map, Object> map = new HashMap<>(); + for (SocketOption option : dc.supportedOptions()) { + try { + Object value = dc.getOption(option); + if (value != null) { + map.put(option, value); + } + } catch (IOException ignore) { } + } + return map; + } + + /** + * Test that disconnect does not interfere with Selector registrations + */ + void testSelectorRegistration(DatagramChannel dc) throws IOException { + try (Selector sel = Selector.open()) { + dc.configureBlocking(false); + SelectionKey key = dc.register(sel, SelectionKey.OP_READ); + + // ensure socket is registered + sel.selectNow(); + + dc.connect(dc.getLocalAddress()); + dc.disconnect(); + + // selection key should still be valid + assertTrue(key.isValid()); + + // check blocking mode with non-blocking receive + ByteBuffer bb = ByteBuffer.allocate(100); + SocketAddress sender = dc.receive(bb); + assertTrue(sender == null); + + // send datagram and ensure that channel is selected + dc.send(ByteBuffer.wrap("Hello".getBytes("UTF-8")), dc.getLocalAddress()); + assertFalse(key.isReadable()); + while (sel.select() == 0); + assertTrue(key.isReadable()); + sender = dc.receive(bb); + assertEquals(sender, dc.getLocalAddress()); + + // cancel key, flush from Selector, and restore blocking mode + key.cancel(); + sel.selectNow(); + dc.configureBlocking(true); + } + } + + /** + * Test that disconnect does not interfere with multicast group membership + */ + void testMulticastGroups(DatagramChannel dc) throws IOException { + InetAddress localAddress = dc.socket().getLocalAddress(); + InetAddress group; + if (localAddress instanceof Inet6Address) { + group = InetAddress.getByName("ff02::a"); + } else { + group = InetAddress.getByName("225.4.5.6"); + } + NetworkInterface ni = NetworkInterface.getByInetAddress(localAddress); + if (ni != null && ni.supportsMulticast()) { + // join group + MembershipKey key = dc.join(group, ni); + + dc.connect(dc.getLocalAddress()); + dc.disconnect(); + + // membership key should still be valid + assertTrue(key.isValid()); + + // send datagram to multicast group, should be received + dc.send(ByteBuffer.wrap("Hello".getBytes("UTF-8")), dc.getLocalAddress()); + ByteBuffer bb = ByteBuffer.allocate(100); + SocketAddress sender = dc.receive(bb); + assertEquals(sender, dc.getLocalAddress()); + + // drop membership + key.drop(); + } + } +}