Repository: netty/netty
Issue: Allow explicitly choosing protocol family

### Expected behavior

Netty should be able to bind to specific protocol family only (IPv4, IPv6) if requested by the caller.

### Actual behavior

Netty uses `java.net.preferIPv4Stack` JVM property to determine whether to bind to IPv4 or IPv6 (possibly IPv6+IPv4 given by settings of IPV6_V6ONLY socket option).

### Steps to reproduce

```
					ServerBootstrap b = new ServerBootstrap();
					b.group(bossGroup, workerGroup)
							.channelFactory(configAdapter.getServerChannel(address))
							.childHandler(channelInitializer).option(ChannelOption.SO_BACKLOG, Integer.MAX_VALUE)
							.childOption(ChannelOption.SO_KEEPALIVE, true);

					bindFuture = b.bind(address);
```

### Minimal yet complete reproducer code (or URL to code)

https://github.com/kvr000/netty-specific-family

### Netty version

4.1.75.Final

### JVM version (e.g. `java -version`)

openjdk version "18-ea" 2022-03-15
OpenJDK Runtime Environment (build 18-ea+15-Ubuntu-4)
OpenJDK 64-Bit Server VM (build 18-ea+15-Ubuntu-4, mixed mode, sharing)

### OS version (e.g. `uname -a`)

Linux ratteburg 5.13.0-37-generic #42-Ubuntu SMP Tue Mar 15 14:34:06 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

### Solution

Add `InternetProtocolFamily` parameter to Netty `SocketChannel` classes, so it can construct appropriate socket handler with correct family.  JDK supports specifying family since JDK 15, for Epoll and Kqueue, those need to be added.  Suggestion provided in Pull Request and confirmed as working (so far without automated tests though).

Pull request: https://github.com/netty/netty/pull/12270


Issue comments:

Pull request review comments:

Patch that resolved the issue:
diff --git a/transport-classes-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java b/transport-classes-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java
index 79e1fbfddc3..0ca449b6654 100644
--- a/transport-classes-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java
+++ b/transport-classes-epoll/src/main/java/io/netty/channel/epoll/EpollDatagramChannel.java
@@ -89,9 +89,7 @@ public EpollDatagramChannel() {
      * on the Operation Systems default which will be chosen.
      */
     public EpollDatagramChannel(InternetProtocolFamily family) {
-        this(family == null ?
-                newSocketDgram(Socket.isIPv6Preferred()) : newSocketDgram(family == InternetProtocolFamily.IPv6),
-                false);
+        this(newSocketDgram(family), false);
     }
 
     /**
diff --git a/transport-classes-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannel.java b/transport-classes-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannel.java
index c794a8f3ec7..a2f188d2211 100644
--- a/transport-classes-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannel.java
+++ b/transport-classes-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannel.java
@@ -17,6 +17,7 @@
 
 import io.netty.channel.Channel;
 import io.netty.channel.EventLoop;
+import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.channel.socket.ServerSocketChannel;
 
 import java.io.IOException;
@@ -41,7 +42,11 @@ public final class EpollServerSocketChannel extends AbstractEpollServerChannel i
     private volatile Collection<InetAddress> tcpMd5SigAddresses = Collections.emptyList();
 
     public EpollServerSocketChannel() {
-        super(newSocketStream(), false);
+        this((InternetProtocolFamily) null);
+    }
+
+    public EpollServerSocketChannel(InternetProtocolFamily protocol) {
+        super(newSocketStream(protocol), false);
         config = new EpollServerSocketChannelConfig(this);
     }
 
diff --git a/transport-classes-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannel.java b/transport-classes-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannel.java
index 48f8a931a3f..ea06b4a1fd3 100644
--- a/transport-classes-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannel.java
+++ b/transport-classes-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannel.java
@@ -19,6 +19,7 @@
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelException;
 import io.netty.channel.ChannelOutboundBuffer;
+import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.channel.socket.ServerSocketChannel;
 import io.netty.channel.socket.SocketChannel;
 import io.netty.util.concurrent.GlobalEventExecutor;
@@ -50,6 +51,11 @@ public EpollSocketChannel() {
         config = new EpollSocketChannelConfig(this);
     }
 
+    public EpollSocketChannel(InternetProtocolFamily protocol) {
+        super(newSocketStream(protocol), false);
+        config = new EpollSocketChannelConfig(this);
+    }
+
     public EpollSocketChannel(int fd) {
         super(fd);
         config = new EpollSocketChannelConfig(this);
diff --git a/transport-classes-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java b/transport-classes-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java
index 29271c1290c..d12e60c66f9 100644
--- a/transport-classes-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java
+++ b/transport-classes-epoll/src/main/java/io/netty/channel/epoll/LinuxSocket.java
@@ -331,6 +331,10 @@ public static LinuxSocket newSocketStream(boolean ipv6) {
         return new LinuxSocket(newSocketStream0(ipv6));
     }
 
+    public static LinuxSocket newSocketStream(InternetProtocolFamily protocol) {
+        return new LinuxSocket(newSocketStream0(protocol));
+    }
+
     public static LinuxSocket newSocketStream() {
         return newSocketStream(isIPv6Preferred());
     }
@@ -339,6 +343,10 @@ public static LinuxSocket newSocketDgram(boolean ipv6) {
         return new LinuxSocket(newSocketDgram0(ipv6));
     }
 
+    public static LinuxSocket newSocketDgram(InternetProtocolFamily family) {
+        return new LinuxSocket(newSocketDgram0(family));
+    }
+
     public static LinuxSocket newSocketDgram() {
         return newSocketDgram(isIPv6Preferred());
     }
diff --git a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/BsdSocket.java b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/BsdSocket.java
index ad2cd5f4396..97fe4a43c5e 100644
--- a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/BsdSocket.java
+++ b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/BsdSocket.java
@@ -16,6 +16,7 @@
 package io.netty.channel.kqueue;
 
 import io.netty.channel.DefaultFileRegion;
+import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.channel.unix.IovArray;
 import io.netty.channel.unix.PeerCredentials;
 import io.netty.channel.unix.Socket;
@@ -196,10 +197,18 @@ public static BsdSocket newSocketStream() {
         return new BsdSocket(newSocketStream0());
     }
 
+    public static BsdSocket newSocketStream(InternetProtocolFamily protocol) {
+        return new BsdSocket(newSocketStream0(protocol));
+    }
+
     public static BsdSocket newSocketDgram() {
         return new BsdSocket(newSocketDgram0());
     }
 
+    public static BsdSocket newSocketDgram(InternetProtocolFamily protocol) {
+        return new BsdSocket(newSocketDgram0(protocol));
+    }
+
     public static BsdSocket newSocketDomain() {
         return new BsdSocket(newSocketDomain0());
     }
diff --git a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDatagramChannel.java b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDatagramChannel.java
index af99bcc9bdd..7c8dfc9b1ab 100644
--- a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDatagramChannel.java
+++ b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueDatagramChannel.java
@@ -25,6 +25,7 @@
 import io.netty.channel.socket.DatagramChannel;
 import io.netty.channel.socket.DatagramChannelConfig;
 import io.netty.channel.socket.DatagramPacket;
+import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.channel.unix.DatagramSocketAddress;
 import io.netty.channel.unix.Errors;
 import io.netty.channel.unix.IovArray;
@@ -61,6 +62,11 @@ public KQueueDatagramChannel() {
         config = new KQueueDatagramChannelConfig(this);
     }
 
+    public KQueueDatagramChannel(InternetProtocolFamily protocol) {
+        super(null, newSocketDgram(protocol), false);
+        config = new KQueueDatagramChannelConfig(this);
+    }
+
     public KQueueDatagramChannel(int fd) {
         this(new BsdSocket(fd), true);
     }
diff --git a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueSocketChannel.java b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueSocketChannel.java
index e024a6eaf51..486a895f7ea 100644
--- a/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueSocketChannel.java
+++ b/transport-classes-kqueue/src/main/java/io/netty/channel/kqueue/KQueueSocketChannel.java
@@ -18,6 +18,7 @@
 import io.netty.buffer.ByteBuf;
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelOutboundBuffer;
+import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.channel.socket.ServerSocketChannel;
 import io.netty.channel.socket.SocketChannel;
 import io.netty.channel.unix.IovArray;
@@ -37,6 +38,11 @@ public KQueueSocketChannel() {
         config = new KQueueSocketChannelConfig(this);
     }
 
+    public KQueueSocketChannel(InternetProtocolFamily protocol) {
+        super(null, BsdSocket.newSocketStream(protocol), false);
+        config = new KQueueSocketChannelConfig(this);
+    }
+
     public KQueueSocketChannel(int fd) {
         super(new BsdSocket(fd));
         config = new KQueueSocketChannelConfig(this);
diff --git a/transport-native-unix-common/src/main/java/io/netty/channel/unix/Socket.java b/transport-native-unix-common/src/main/java/io/netty/channel/unix/Socket.java
index e58d6342b69..4fb894dc566 100644
--- a/transport-native-unix-common/src/main/java/io/netty/channel/unix/Socket.java
+++ b/transport-native-unix-common/src/main/java/io/netty/channel/unix/Socket.java
@@ -16,6 +16,7 @@
 package io.netty.channel.unix;
 
 import io.netty.channel.ChannelException;
+import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.util.CharsetUtil;
 import io.netty.util.NetUtil;
 
@@ -499,6 +500,11 @@ public static boolean isIPv6Preferred() {
         return isIpv6Preferred;
     }
 
+    public static boolean shouldUseIpv6(InternetProtocolFamily family) {
+        return family == null ? isIPv6Preferred() :
+                        family == InternetProtocolFamily.IPv6;
+    }
+
     private static native boolean isIPv6Preferred0(boolean ipv4Preferred);
 
     private static native boolean isIPv6(int fd);
@@ -534,6 +540,10 @@ protected static int newSocketStream0() {
         return newSocketStream0(isIPv6Preferred());
     }
 
+    protected static int newSocketStream0(InternetProtocolFamily protocol) {
+        return newSocketStream0(shouldUseIpv6(protocol));
+    }
+
     protected static int newSocketStream0(boolean ipv6) {
         int res = newSocketStreamFd(ipv6);
         if (res < 0) {
@@ -546,6 +556,10 @@ protected static int newSocketDgram0() {
         return newSocketDgram0(isIPv6Preferred());
     }
 
+    protected static int newSocketDgram0(InternetProtocolFamily family) {
+        return newSocketDgram0(shouldUseIpv6(family));
+    }
+
     protected static int newSocketDgram0(boolean ipv6) {
         int res = newSocketDgramFd(ipv6);
         if (res < 0) {
diff --git a/transport/src/main/java/io/netty/channel/socket/nio/NioServerSocketChannel.java b/transport/src/main/java/io/netty/channel/socket/nio/NioServerSocketChannel.java
index a90a4277165..e9304a89098 100644
--- a/transport/src/main/java/io/netty/channel/socket/nio/NioServerSocketChannel.java
+++ b/transport/src/main/java/io/netty/channel/socket/nio/NioServerSocketChannel.java
@@ -19,6 +19,7 @@
 import io.netty.channel.ChannelMetadata;
 import io.netty.channel.ChannelOption;
 import io.netty.channel.ChannelOutboundBuffer;
+import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.util.internal.SocketUtils;
 import io.netty.channel.nio.AbstractNioMessageChannel;
 import io.netty.channel.socket.DefaultServerSocketChannelConfig;
@@ -29,7 +30,10 @@
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
 import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.net.InetSocketAddress;
+import java.net.ProtocolFamily;
 import java.net.ServerSocket;
 import java.net.SocketAddress;
 import java.nio.channels.SelectionKey;
@@ -51,7 +55,21 @@ public class NioServerSocketChannel extends AbstractNioMessageChannel
 
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(NioServerSocketChannel.class);
 
-    private static ServerSocketChannel newSocket(SelectorProvider provider) {
+    private static final Method OPEN_SERVER_SOCKET_CHANNEL_WITH_FAMILY;
+
+    static {
+        Method found = null;
+        try {
+            found = SelectorProvider.class.getMethod(
+                            "openServerSocketChannel", ProtocolFamily.class);
+        } catch (Throwable e) {
+            logger.info("openServerSocketChannel(ProtocolFamily) not available, will use default method", e);
+        }
+        OPEN_SERVER_SOCKET_CHANNEL_WITH_FAMILY = found;
+    }
+
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
+    private static ServerSocketChannel newSocket(SelectorProvider provider, InternetProtocolFamily family) {
         try {
             /**
              *  Use the {@link SelectorProvider} to open {@link SocketChannel} and so remove condition in
@@ -59,6 +77,16 @@ private static ServerSocketChannel newSocket(SelectorProvider provider) {
              *
              *  See <a href="https://github.com/netty/netty/issues/2308">#2308</a>.
              */
+            if (family != null && OPEN_SERVER_SOCKET_CHANNEL_WITH_FAMILY != null) {
+                try {
+                    return (ServerSocketChannel) OPEN_SERVER_SOCKET_CHANNEL_WITH_FAMILY.invoke(
+                                    provider, ProtocolFamilyConverter.convert(family));
+                } catch (InvocationTargetException e) {
+                    throw new IOException(e);
+                } catch (IllegalAccessException e) {
+                    throw new IOException(e);
+                }
+            }
             return provider.openServerSocketChannel();
         } catch (IOException e) {
             throw new ChannelException(
@@ -72,14 +100,21 @@ private static ServerSocketChannel newSocket(SelectorProvider provider) {
      * Create a new instance
      */
     public NioServerSocketChannel() {
-        this(newSocket(DEFAULT_SELECTOR_PROVIDER));
+        this(DEFAULT_SELECTOR_PROVIDER);
     }
 
     /**
      * Create a new instance using the given {@link SelectorProvider}.
      */
     public NioServerSocketChannel(SelectorProvider provider) {
-        this(newSocket(provider));
+        this(provider, null);
+    }
+
+    /**
+     * Create a new instance using the given {@link SelectorProvider} and protocol family (supported only since JDK 15).
+     */
+    public NioServerSocketChannel(SelectorProvider provider, InternetProtocolFamily family) {
+        this(newSocket(provider, family));
     }
 
     /**
diff --git a/transport/src/main/java/io/netty/channel/socket/nio/NioSocketChannel.java b/transport/src/main/java/io/netty/channel/socket/nio/NioSocketChannel.java
index b1570a9fa6a..15a745eafbf 100644
--- a/transport/src/main/java/io/netty/channel/socket/nio/NioSocketChannel.java
+++ b/transport/src/main/java/io/netty/channel/socket/nio/NioSocketChannel.java
@@ -28,6 +28,7 @@
 import io.netty.channel.RecvByteBufAllocator;
 import io.netty.channel.nio.AbstractNioByteChannel;
 import io.netty.channel.socket.DefaultSocketChannelConfig;
+import io.netty.channel.socket.InternetProtocolFamily;
 import io.netty.channel.socket.ServerSocketChannel;
 import io.netty.channel.socket.SocketChannelConfig;
 import io.netty.util.concurrent.GlobalEventExecutor;
@@ -39,7 +40,10 @@
 import io.netty.util.internal.logging.InternalLoggerFactory;
 
 import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.net.InetSocketAddress;
+import java.net.ProtocolFamily;
 import java.net.Socket;
 import java.net.SocketAddress;
 import java.nio.ByteBuffer;
@@ -58,6 +62,19 @@ public class NioSocketChannel extends AbstractNioByteChannel implements io.netty
     private static final InternalLogger logger = InternalLoggerFactory.getInstance(NioSocketChannel.class);
     private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();
 
+    private static final Method OPEN_SOCKET_CHANNEL_WITH_FAMILY;
+
+    static {
+        Method found = null;
+        try {
+            found = SelectorProvider.class.getMethod(
+                            "openSocketChannel", ProtocolFamily.class);
+        } catch (Throwable e) {
+            logger.warn("openSocketChannel(ProtocolFamily) not available, will use default", e);
+        }
+        OPEN_SOCKET_CHANNEL_WITH_FAMILY = found;
+    }
+
     private static SocketChannel newSocket(SelectorProvider provider) {
         try {
             /**
@@ -72,6 +89,31 @@ private static SocketChannel newSocket(SelectorProvider provider) {
         }
     }
 
+    @SuppressJava6Requirement(reason = "Usage guarded by java version check")
+    private static SocketChannel newSocket(SelectorProvider provider, InternetProtocolFamily family) {
+        try {
+            /**
+             *  Use the {@link SelectorProvider} to open {@link SocketChannel} and so remove condition in
+             *  {@link SelectorProvider#provider()} which is called by each SocketChannel.open() otherwise.
+             *
+             *  See <a href="https://github.com/netty/netty/issues/2308">#2308</a>.
+             */
+            if (family != null && OPEN_SOCKET_CHANNEL_WITH_FAMILY != null) {
+                try {
+                    return (SocketChannel) OPEN_SOCKET_CHANNEL_WITH_FAMILY.invoke(
+                                    provider, ProtocolFamilyConverter.convert(family));
+                } catch (InvocationTargetException e) {
+                    throw new IOException(e);
+                } catch (IllegalAccessException e) {
+                    throw new IOException(e);
+                }
+            }
+            return provider.openSocketChannel();
+        } catch (IOException e) {
+            throw new ChannelException("Failed to open a socket.", e);
+        }
+    }
+
     private final SocketChannelConfig config;
 
     /**
@@ -85,7 +127,14 @@ public NioSocketChannel() {
      * Create a new instance using the given {@link SelectorProvider}.
      */
     public NioSocketChannel(SelectorProvider provider) {
-        this(newSocket(provider));
+        this(provider, null);
+    }
+
+    /**
+     * Create a new instance using the given {@link SelectorProvider} and protocol family (supported only since JDK 15).
+     */
+    public NioSocketChannel(SelectorProvider provider, InternetProtocolFamily family) {
+        this(newSocket(provider, family));
     }
 
     /**


Test patch: