8231259: (dc) DatagramChannel::disconnect re-binds socket to the wildcard address (macOS)
Reviewed-by: dfuchs, chegar
--- 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
--- 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<SelectionKey> 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.
*
* <p> This method first verifies that this channel is open and that the
--- 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<SocketOption<?>, 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<SocketOption<?>, 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<SocketOption<?>, Object> e : map.entrySet()) {
+ @SuppressWarnings("unchecked")
+ SocketOption<Object> option = (SocketOption<Object>) 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<Method> 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<SelectionKeyImpl> 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.
*/
--- 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<InetAddress,List<MembershipKeyImpl>> groups = null;
+ // map multicast group to list of keys
+ private Map<InetAddress, List<MembershipKeyImpl>> groups;
MembershipRegistry() {
}
@@ -116,16 +120,29 @@
}
}
+ @FunctionalInterface
+ interface ThrowingConsumer<T, X extends Throwable> {
+ void accept(T action) throws X;
+ }
+
+ /**
+ * Invoke an action for each key in the registry
+ */
+ <X extends Throwable>
+ void forEach(ThrowingConsumer<MembershipKeyImpl, X> action) throws X {
+ if (groups != null) {
+ for (List<MembershipKeyImpl> 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);
}
}
--- 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();
+ }
}
--- 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();
--- 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;
--- 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;
--- 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);
--- 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");
- }
- }
-
-}
--- /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<SocketOption<?>, 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<SocketOption<?>, Object> options(DatagramChannel dc) throws IOException {
+ Map<SocketOption<?>, 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();
+ }
+ }
+}