8231259: (dc) DatagramChannel::disconnect re-binds socket to the wildcard address (macOS)
authoralanb
Wed, 20 Nov 2019 08:35:53 +0000
changeset 59146 455612b3161a
parent 59145 ea044aedc2b6
child 59147 e735301d76b9
8231259: (dc) DatagramChannel::disconnect re-binds socket to the wildcard address (macOS) Reviewed-by: dfuchs, chegar
src/java.base/macosx/classes/sun/nio/ch/KQueueSelectorImpl.java
src/java.base/share/classes/java/nio/channels/spi/AbstractSelectableChannel.java
src/java.base/share/classes/sun/nio/ch/DatagramChannelImpl.java
src/java.base/share/classes/sun/nio/ch/MembershipRegistry.java
src/java.base/share/classes/sun/nio/ch/NativeDispatcher.java
src/java.base/share/classes/sun/nio/ch/SelectionKeyImpl.java
src/java.base/unix/classes/sun/nio/ch/DatagramDispatcher.java
src/java.base/unix/classes/sun/nio/ch/FileDispatcherImpl.java
src/java.base/unix/native/libnio/ch/FileDispatcherImpl.c
test/jdk/java/nio/channels/DatagramChannel/AddressesAfterDisconnect.java
test/jdk/java/nio/channels/DatagramChannel/AfterDisconnect.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
--- 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();
+        }
+    }
+}