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/netty5/channel/epoll/EpollDatagramChannel.java b/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/EpollDatagramChannel.java
index 70200462201..b702a2c8a10 100644
--- a/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/EpollDatagramChannel.java
+++ b/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/EpollDatagramChannel.java
@@ -98,9 +98,7 @@ public EpollDatagramChannel(EventLoop eventLoop) {
      * on the Operation Systems default which will be chosen.
      */
     public EpollDatagramChannel(EventLoop eventLoop, InternetProtocolFamily family) {
-        this(eventLoop, family == null ?
-            newSocketDgram(Socket.isIPv6Preferred()) :
-            newSocketDgram(family == InternetProtocolFamily.IPv6),
+        this(eventLoop, newSocketDgram(family),
         false);
     }
 
diff --git a/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/EpollServerSocketChannel.java b/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/EpollServerSocketChannel.java
index a982c32a858..f22305f1fdb 100644
--- a/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/EpollServerSocketChannel.java
+++ b/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/EpollServerSocketChannel.java
@@ -18,6 +18,7 @@
 import io.netty5.channel.Channel;
 import io.netty5.channel.EventLoop;
 import io.netty5.channel.EventLoopGroup;
+import io.netty5.channel.socket.InternetProtocolFamily;
 import io.netty5.channel.socket.ServerSocketChannel;
 
 import java.io.IOException;
@@ -42,7 +43,12 @@ public final class EpollServerSocketChannel extends AbstractEpollServerChannel i
     private volatile Collection<InetAddress> tcpMd5SigAddresses = Collections.emptyList();
 
     public EpollServerSocketChannel(EventLoop eventLoop, EventLoopGroup childEventLoopGroup) {
-        super(eventLoop, childEventLoopGroup, newSocketStream(), false);
+        this(eventLoop, childEventLoopGroup, (InternetProtocolFamily) null);
+    }
+
+    public EpollServerSocketChannel(EventLoop eventLoop, EventLoopGroup childEventLoopGroup,
+                                    InternetProtocolFamily protocolFamily) {
+        super(eventLoop, childEventLoopGroup, newSocketStream(protocolFamily), false);
         config = new EpollServerSocketChannelConfig(this);
     }
 
diff --git a/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/EpollSocketChannel.java b/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/EpollSocketChannel.java
index cc58f984afc..2c6b07260e6 100644
--- a/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/EpollSocketChannel.java
+++ b/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/EpollSocketChannel.java
@@ -22,6 +22,7 @@
 import io.netty5.channel.ChannelException;
 import io.netty5.channel.ChannelOutboundBuffer;
 import io.netty5.channel.EventLoop;
+import io.netty5.channel.socket.InternetProtocolFamily;
 import io.netty5.channel.socket.ServerSocketChannel;
 import io.netty5.channel.socket.SocketChannel;
 import io.netty5.util.concurrent.GlobalEventExecutor;
@@ -49,7 +50,11 @@ public final class EpollSocketChannel extends AbstractEpollStreamChannel impleme
     private volatile Collection<InetAddress> tcpMd5SigAddresses = Collections.emptyList();
 
     public EpollSocketChannel(EventLoop eventLoop) {
-        super(eventLoop, newSocketStream(), false);
+        this(eventLoop, null);
+    }
+
+    public EpollSocketChannel(EventLoop eventLoop, InternetProtocolFamily protocolFamily) {
+        super(eventLoop, newSocketStream(protocolFamily), false);
         config = new EpollSocketChannelConfig(this);
     }
 
diff --git a/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/LinuxSocket.java b/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/LinuxSocket.java
index ed74ec651f6..32916a72432 100644
--- a/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/LinuxSocket.java
+++ b/transport-classes-epoll/src/main/java/io/netty5/channel/epoll/LinuxSocket.java
@@ -328,6 +328,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());
     }
@@ -336,6 +340,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/netty5/channel/kqueue/BsdSocket.java b/transport-classes-kqueue/src/main/java/io/netty5/channel/kqueue/BsdSocket.java
index ba9563b34ae..70bfd22b011 100644
--- a/transport-classes-kqueue/src/main/java/io/netty5/channel/kqueue/BsdSocket.java
+++ b/transport-classes-kqueue/src/main/java/io/netty5/channel/kqueue/BsdSocket.java
@@ -16,6 +16,8 @@
 package io.netty5.channel.kqueue;
 
 import io.netty5.channel.DefaultFileRegion;
+import io.netty5.channel.socket.InternetProtocolFamily;
+
 import io.netty5.channel.unix.IovArray;
 import io.netty5.channel.unix.PeerCredentials;
 import io.netty5.channel.unix.Socket;
@@ -196,10 +198,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/netty5/channel/kqueue/KQueueDatagramChannel.java b/transport-classes-kqueue/src/main/java/io/netty5/channel/kqueue/KQueueDatagramChannel.java
index cde38c730f7..4c9a7657f3d 100644
--- a/transport-classes-kqueue/src/main/java/io/netty5/channel/kqueue/KQueueDatagramChannel.java
+++ b/transport-classes-kqueue/src/main/java/io/netty5/channel/kqueue/KQueueDatagramChannel.java
@@ -28,6 +28,7 @@
 import io.netty5.channel.socket.DatagramChannel;
 import io.netty5.channel.socket.DatagramChannelConfig;
 import io.netty5.channel.socket.DatagramPacket;
+import io.netty5.channel.socket.InternetProtocolFamily;
 import io.netty5.channel.unix.DatagramSocketAddress;
 import io.netty5.channel.unix.Errors;
 import io.netty5.channel.unix.IovArray;
@@ -64,7 +65,11 @@ public final class KQueueDatagramChannel extends AbstractKQueueDatagramChannel i
     private final KQueueDatagramChannelConfig config;
 
     public KQueueDatagramChannel(EventLoop eventLoop) {
-        super(null, eventLoop, newSocketDgram(), false);
+        this(eventLoop, null);
+    }
+
+    public KQueueDatagramChannel(EventLoop eventLoop, InternetProtocolFamily protocolFamily) {
+        super(null, eventLoop, newSocketDgram(protocolFamily), false);
         config = new KQueueDatagramChannelConfig(this);
     }
 
diff --git a/transport-classes-kqueue/src/main/java/io/netty5/channel/kqueue/KQueueSocketChannel.java b/transport-classes-kqueue/src/main/java/io/netty5/channel/kqueue/KQueueSocketChannel.java
index 0c1e6b589c5..2aa1f32a105 100644
--- a/transport-classes-kqueue/src/main/java/io/netty5/channel/kqueue/KQueueSocketChannel.java
+++ b/transport-classes-kqueue/src/main/java/io/netty5/channel/kqueue/KQueueSocketChannel.java
@@ -19,6 +19,7 @@
 import io.netty5.channel.Channel;
 import io.netty5.channel.ChannelOutboundBuffer;
 import io.netty5.channel.EventLoop;
+import io.netty5.channel.socket.InternetProtocolFamily;
 import io.netty5.channel.socket.ServerSocketChannel;
 import io.netty5.channel.socket.SocketChannel;
 import io.netty5.channel.unix.IovArray;
@@ -34,7 +35,11 @@ public final class KQueueSocketChannel extends AbstractKQueueStreamChannel imple
     private final KQueueSocketChannelConfig config;
 
     public KQueueSocketChannel(EventLoop eventLoop) {
-        super(null, eventLoop, BsdSocket.newSocketStream(), false);
+        this(eventLoop, null);
+    }
+
+    public KQueueSocketChannel(EventLoop eventLoop, InternetProtocolFamily protocol) {
+        super(null, eventLoop, BsdSocket.newSocketStream(protocol), false);
         config = new KQueueSocketChannelConfig(this);
     }
 
diff --git a/transport-native-unix-common/src/main/java/io/netty5/channel/unix/Socket.java b/transport-native-unix-common/src/main/java/io/netty5/channel/unix/Socket.java
index 3325f206243..1253fd15446 100644
--- a/transport-native-unix-common/src/main/java/io/netty5/channel/unix/Socket.java
+++ b/transport-native-unix-common/src/main/java/io/netty5/channel/unix/Socket.java
@@ -16,6 +16,7 @@
 package io.netty5.channel.unix;
 
 import io.netty5.channel.ChannelException;
+import io.netty5.channel.socket.InternetProtocolFamily;
 import io.netty5.util.CharsetUtil;
 import io.netty5.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/netty5/channel/socket/nio/NioServerSocketChannel.java b/transport/src/main/java/io/netty5/channel/socket/nio/NioServerSocketChannel.java
index 93d98274bfb..b9adc172308 100644
--- a/transport/src/main/java/io/netty5/channel/socket/nio/NioServerSocketChannel.java
+++ b/transport/src/main/java/io/netty5/channel/socket/nio/NioServerSocketChannel.java
@@ -23,6 +23,7 @@
 import io.netty5.channel.ChannelOutboundBuffer;
 import io.netty5.channel.EventLoop;
 import io.netty5.channel.EventLoopGroup;
+import io.netty5.channel.socket.InternetProtocolFamily;
 import io.netty5.util.internal.SocketUtils;
 import io.netty5.channel.nio.AbstractNioMessageChannel;
 import io.netty5.channel.socket.DefaultServerSocketChannelConfig;
@@ -31,7 +32,10 @@
 import io.netty5.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;
@@ -53,12 +57,35 @@ 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;
+    }
+
+    private static ServerSocketChannel 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 ServerSocketChannel.open() otherwise.
-            //
-            //  See <a href="https://github.com/netty/netty/issues/2308">#2308</a>.
+            /**
+             *  Use the {@link SelectorProvider} to open {@link SocketChannel} and so remove condition in
+             *  {@link SelectorProvider#provider()} which is called by each ServerSocketChannel.open() otherwise.
+             *
+             *  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 | IllegalAccessException e) {
+                    throw new IOException(e);
+                }
+            }
             return provider.openServerSocketChannel();
         } catch (IOException e) {
             throw new ChannelException(
@@ -73,14 +100,22 @@ private static ServerSocketChannel newSocket(SelectorProvider provider) {
      * Create a new instance
      */
     public NioServerSocketChannel(EventLoop eventLoop, EventLoopGroup childEventLoopGroup) {
-        this(eventLoop, childEventLoopGroup, newSocket(DEFAULT_SELECTOR_PROVIDER));
+        this(eventLoop, childEventLoopGroup, DEFAULT_SELECTOR_PROVIDER);
     }
 
     /**
      * Create a new instance using the given {@link SelectorProvider}.
      */
     public NioServerSocketChannel(EventLoop eventLoop, EventLoopGroup childEventLoopGroup, SelectorProvider provider) {
-        this(eventLoop, childEventLoopGroup, newSocket(provider));
+        this(eventLoop, childEventLoopGroup, provider, null);
+    }
+
+    /**
+     * Create a new instance using the given {@link SelectorProvider} and protocol family (supported only since JDK 15).
+     */
+    public NioServerSocketChannel(EventLoop eventLoop, EventLoopGroup childEventLoopGroup,
+                                  SelectorProvider provider, InternetProtocolFamily protocolFamily) {
+        this(eventLoop, childEventLoopGroup, newSocket(provider, protocolFamily));
     }
 
     /**
diff --git a/transport/src/main/java/io/netty5/channel/socket/nio/NioSocketChannel.java b/transport/src/main/java/io/netty5/channel/socket/nio/NioSocketChannel.java
index a48254a28f4..c1f99967d74 100644
--- a/transport/src/main/java/io/netty5/channel/socket/nio/NioSocketChannel.java
+++ b/transport/src/main/java/io/netty5/channel/socket/nio/NioSocketChannel.java
@@ -26,6 +26,7 @@
 import io.netty5.channel.RecvBufferAllocator;
 import io.netty5.channel.nio.AbstractNioByteChannel;
 import io.netty5.channel.socket.DefaultSocketChannelConfig;
+import io.netty5.channel.socket.InternetProtocolFamily;
 import io.netty5.channel.socket.ServerSocketChannel;
 import io.netty5.channel.socket.SocketChannelConfig;
 import io.netty5.util.concurrent.Future;
@@ -37,7 +38,10 @@
 import io.netty5.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;
@@ -56,6 +60,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 {
             //  Use the {@link SelectorProvider} to open {@link SocketChannel} and so remove condition in
@@ -68,6 +85,28 @@ private static SocketChannel newSocket(SelectorProvider provider) {
         }
     }
 
+    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 | 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;
 
     /**
@@ -84,6 +123,13 @@ public NioSocketChannel(EventLoop eventLoop, SelectorProvider provider) {
         this(eventLoop, newSocket(provider));
     }
 
+    /**
+     * Create a new instance using the given {@link SelectorProvider} and protocol family (supported only since JDK 15).
+     */
+    public NioSocketChannel(EventLoop eventLoop, SelectorProvider provider, InternetProtocolFamily family) {
+        this(eventLoop, newSocket(provider, family));
+    }
+
     /**
      * Create a new instance using the given {@link SocketChannel}.
      */


Test patch: