diff --git a/transport-native-epoll/src/main/c/io_netty_channel_epoll_Native.c b/transport-native-epoll/src/main/c/io_netty_channel_epoll_Native.c index 756c3f53f6..ed0204a773 100644 --- a/transport-native-epoll/src/main/c/io_netty_channel_epoll_Native.c +++ b/transport-native-epoll/src/main/c/io_netty_channel_epoll_Native.c @@ -1736,3 +1736,51 @@ JNIEXPORT jint JNICALL Java_io_netty_channel_epoll_Native_splice0(JNIEnv* env, j JNIEXPORT jlong JNICALL Java_io_netty_channel_epoll_Native_ssizeMax(JNIEnv* env, jclass clazz) { return SSIZE_MAX; } + +JNIEXPORT jint JNICALL Java_io_netty_channel_epoll_Native_tcpMd5SigMaxKeyLen(JNIEnv* env, jclass clazz) { + struct tcp_md5sig md5sig; + + // Defensive size check + if (sizeof(md5sig.tcpm_key) < TCP_MD5SIG_MAXKEYLEN) { + return sizeof(md5sig.tcpm_key); + } + + return TCP_MD5SIG_MAXKEYLEN; +} + +JNIEXPORT void JNICALL Java_io_netty_channel_epoll_Native_setTcpMd5Sig0(JNIEnv* env, jclass clazz, jint fd, jbyteArray address, jint scopeId, jbyteArray key) { + struct sockaddr_storage addr; + if (init_sockaddr(env, address, scopeId, 0, &addr) == -1) { + return; + } + + struct tcp_md5sig md5sig; + memset(&md5sig, 0, sizeof(md5sig)); + md5sig.tcpm_addr.ss_family = addr.ss_family; + + struct sockaddr_in* ipaddr; + struct sockaddr_in6* ip6addr; + + switch (addr.ss_family) { + case AF_INET: + ipaddr = (struct sockaddr_in*) &addr; + memcpy(&((struct sockaddr_in *) &md5sig.tcpm_addr)->sin_addr, &ipaddr->sin_addr, sizeof(ipaddr->sin_addr)); + break; + case AF_INET6: + ip6addr = (struct sockaddr_in6*) &addr; + memcpy(&((struct sockaddr_in6 *) &md5sig.tcpm_addr)->sin6_addr, &ip6addr->sin6_addr, sizeof(ip6addr->sin6_addr)); + break; + } + + if (key != NULL) { + md5sig.tcpm_keylen = (*env)->GetArrayLength(env, key); + (*env)->GetByteArrayRegion(env, key, 0, md5sig.tcpm_keylen, (void *) &md5sig.tcpm_key); + if ((*env)->ExceptionCheck(env) == JNI_TRUE) { + return; + } + } + + if (setsockopt(fd, IPPROTO_TCP, TCP_MD5SIG, &md5sig, sizeof(md5sig)) < 0) { + throwChannelExceptionErrorNo(env, "setsockopt() failed: ", errno); + } +} diff --git a/transport-native-epoll/src/main/c/io_netty_channel_epoll_Native.h b/transport-native-epoll/src/main/c/io_netty_channel_epoll_Native.h index 380bd3b20f..258233134e 100644 --- a/transport-native-epoll/src/main/c/io_netty_channel_epoll_Native.h +++ b/transport-native-epoll/src/main/c/io_netty_channel_epoll_Native.h @@ -129,3 +129,6 @@ jint Java_io_netty_channel_epoll_Native_offsetofEpollData(JNIEnv* env, jclass cl jlong Java_io_netty_channel_epoll_Native_pipe0(JNIEnv* env, jclass clazz); jint Java_io_netty_channel_epoll_Native_splice0(JNIEnv* env, jclass clazz, jint fd, jint offIn, jint fdOut, jint offOut, jint len); + +jint Java_io_netty_channel_epoll_Native_tcpMd5SigMaxKeyLen(JNIEnv* env, jclass clazz); +void Java_io_netty_channel_epoll_Native_setTcpMd5Sig0(JNIEnv* env, jclass clazz, jint fd, jbyteArray address, jint scopeId, jbyteArray key); diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java index 1f231f2f80..3fedfa09c0 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollChannelOption.java @@ -18,6 +18,9 @@ package io.netty.channel.epoll; import io.netty.channel.ChannelOption; import io.netty.channel.unix.DomainSocketReadMode; +import java.net.InetAddress; +import java.util.Map; + public final class EpollChannelOption extends ChannelOption { public static final ChannelOption TCP_CORK = valueOf("TCP_CORK"); @@ -35,6 +38,8 @@ public final class EpollChannelOption extends ChannelOption { public static final ChannelOption EPOLL_MODE = valueOf("EPOLL_MODE"); + public static final ChannelOption> TCP_MD5SIG = valueOf("TCP_MD5SIG"); + @SuppressWarnings({ "unused", "deprecation" }) private EpollChannelOption(String name) { super(name); diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java index b24e2632f6..dfb23272d6 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerChannelConfig.java @@ -21,11 +21,13 @@ import io.netty.channel.MessageSizeEstimator; import io.netty.channel.RecvByteBufAllocator; import io.netty.util.NetUtil; +import java.net.InetAddress; import java.util.Map; import static io.netty.channel.ChannelOption.SO_BACKLOG; import static io.netty.channel.ChannelOption.SO_RCVBUF; import static io.netty.channel.ChannelOption.SO_REUSEADDR; +import static io.netty.channel.epoll.EpollChannelOption.TCP_MD5SIG;; public class EpollServerChannelConfig extends EpollChannelConfig { protected final AbstractEpollChannel channel; @@ -66,6 +68,10 @@ public class EpollServerChannelConfig extends EpollChannelConfig { setReuseAddress((Boolean) value); } else if (option == SO_BACKLOG) { setBacklog((Integer) value); + } else if (option == TCP_MD5SIG) { + @SuppressWarnings("unchecked") + final Map m = (Map) value; + ((EpollServerSocketChannel) channel).setTcpMd5Sig(m); } else { return super.setOption(option, value); } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannel.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannel.java index 03e74be8b6..d70e9de1ba 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannel.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannel.java @@ -20,8 +20,12 @@ import io.netty.channel.EventLoop; import io.netty.channel.socket.ServerSocketChannel; import io.netty.channel.unix.FileDescriptor; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; /** * {@link ServerSocketChannel} implementation that uses linux EPOLL Edge-Triggered Mode for @@ -31,6 +35,7 @@ public final class EpollServerSocketChannel extends AbstractEpollServerChannel i private final EpollServerSocketChannelConfig config; private volatile InetSocketAddress local; + private volatile Collection tcpMd5SigAddresses = Collections.emptyList(); public EpollServerSocketChannel() { super(Native.socketStreamFd()); @@ -89,4 +94,12 @@ public final class EpollServerSocketChannel extends AbstractEpollServerChannel i protected Channel newChildChannel(int fd, byte[] address, int offset, int len) throws Exception { return new EpollSocketChannel(this, fd, Native.address(address, offset, len)); } + + Collection tcpMd5SigAddresses() { + return tcpMd5SigAddresses; + } + + void setTcpMd5Sig(Map keys) { + this.tcpMd5SigAddresses = TcpMd5Util.newTcpMd5Sigs(this, tcpMd5SigAddresses, keys); + } } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannelConfig.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannelConfig.java index e6888eb55e..558f29700e 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannelConfig.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollServerSocketChannelConfig.java @@ -21,6 +21,7 @@ import io.netty.channel.MessageSizeEstimator; import io.netty.channel.RecvByteBufAllocator; import io.netty.channel.socket.ServerSocketChannelConfig; +import java.net.InetAddress; import java.util.Map; public final class EpollServerSocketChannelConfig extends EpollServerChannelConfig @@ -60,6 +61,10 @@ public final class EpollServerSocketChannelConfig extends EpollServerChannelConf setReusePort((Boolean) value); } else if (option == EpollChannelOption.IP_FREEBIND) { setFreeBind((Boolean) value); + } else if (option == EpollChannelOption.TCP_MD5SIG) { + @SuppressWarnings("unchecked") + final Map m = (Map) value; + setTcpMd5Sig(m); } else { return super.setOption(option, value); } @@ -144,6 +149,16 @@ public final class EpollServerSocketChannelConfig extends EpollServerChannelConf return this; } + /** + * Set the {@code TCP_MD5SIG} option on the socket. See {@code linux/tcp.h} for more details. + * Keys can only be set on, not read to prevent a potential leak, as they are confidential. + * Allowing them being read would mean anyone with access to the channel could get them. + */ + public EpollServerSocketChannelConfig setTcpMd5Sig(Map keys) { + ((EpollServerSocketChannel) channel).setTcpMd5Sig(keys); + return this; + } + /** * Returns {@code true} if the SO_REUSEPORT option is set. */ diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannel.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannel.java index 3ddf6aed2a..2f16b0ab18 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannel.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannel.java @@ -25,8 +25,12 @@ import io.netty.channel.unix.FileDescriptor; import io.netty.util.concurrent.GlobalEventExecutor; import io.netty.util.internal.OneTimeTask; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; import java.util.concurrent.Executor; /** @@ -39,6 +43,7 @@ public final class EpollSocketChannel extends AbstractEpollStreamChannel impleme private volatile InetSocketAddress local; private volatile InetSocketAddress remote; + private volatile Collection tcpMd5SigAddresses = Collections.emptyList(); EpollSocketChannel(Channel parent, int fd, InetSocketAddress remote) { super(parent, fd); @@ -47,6 +52,10 @@ public final class EpollSocketChannel extends AbstractEpollStreamChannel impleme // See https://github.com/netty/netty/issues/2359 this.remote = remote; local = Native.localAddress(fd); + + if (parent instanceof EpollServerSocketChannel) { + tcpMd5SigAddresses = ((EpollServerSocketChannel) parent).tcpMd5SigAddresses(); + } } public EpollSocketChannel() { @@ -205,4 +214,8 @@ public final class EpollSocketChannel extends AbstractEpollStreamChannel impleme return null; } } + + void setTcpMd5Sig(Map keys) { + this.tcpMd5SigAddresses = TcpMd5Util.newTcpMd5Sigs(this, tcpMd5SigAddresses, keys); + } } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannelConfig.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannelConfig.java index 65337c3e23..e789563901 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannelConfig.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/EpollSocketChannelConfig.java @@ -22,6 +22,7 @@ import io.netty.channel.RecvByteBufAllocator; import io.netty.channel.socket.SocketChannelConfig; import io.netty.util.internal.PlatformDependent; +import java.net.InetAddress; import java.util.Map; import static io.netty.channel.ChannelOption.*; @@ -49,7 +50,8 @@ public final class EpollSocketChannelConfig extends EpollChannelConfig implement super.getOptions(), SO_RCVBUF, SO_SNDBUF, TCP_NODELAY, SO_KEEPALIVE, SO_REUSEADDR, SO_LINGER, IP_TOS, ALLOW_HALF_CLOSURE, EpollChannelOption.TCP_CORK, EpollChannelOption.TCP_NOTSENT_LOWAT, - EpollChannelOption.TCP_KEEPCNT, EpollChannelOption.TCP_KEEPIDLE, EpollChannelOption.TCP_KEEPINTVL); + EpollChannelOption.TCP_KEEPCNT, EpollChannelOption.TCP_KEEPIDLE, EpollChannelOption.TCP_KEEPINTVL, + EpollChannelOption.TCP_MD5SIG); } @SuppressWarnings("unchecked") @@ -132,6 +134,10 @@ public final class EpollSocketChannelConfig extends EpollChannelConfig implement setTcpKeepIntvl((Integer) value); } else if (option == EpollChannelOption.TCP_USER_TIMEOUT) { setTcpUserTimeout((Integer) value); + } else if (option == EpollChannelOption.TCP_MD5SIG) { + @SuppressWarnings("unchecked") + final Map m = (Map) value; + setTcpMd5Sig(m); } else { return super.setOption(option, value); } @@ -317,6 +323,16 @@ public final class EpollSocketChannelConfig extends EpollChannelConfig implement return this; } + /* + * Set the {@code TCP_MD5SIG} option on the socket. See {@code linux/tcp.h} for more details. + * Keys can only be set on, not read to prevent a potential leak, as they are confidential. + * Allowing them being read would mean anyone with access to the channel could get them. + */ + public EpollSocketChannelConfig setTcpMd5Sig(Map keys) { + channel.setTcpMd5Sig(keys); + return this; + } + @Override public boolean isAllowHalfClosure() { return allowHalfClosure; diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/Native.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/Native.java index 3a7136809d..8ab9cb900c 100644 --- a/transport-native-epoll/src/main/java/io/netty/channel/epoll/Native.java +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/Native.java @@ -62,6 +62,8 @@ public final class Native { public static final int UIO_MAX_IOV = uioMaxIov(); public static final boolean IS_SUPPORTING_SENDMMSG = isSupportingSendmmsg(); public static final long SSIZE_MAX = ssizeMax(); + public static final int TCP_MD5SIG_MAXKEYLEN = tcpMd5SigMaxKeyLen(); + private static final byte[] IPV4_MAPPED_IPV6_PREFIX = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xff, (byte) 0xff }; @@ -661,6 +663,13 @@ public final class Native { private static native void tcpInfo0(int fd, int[] array); + public static void setTcpMd5Sig(int fd, InetAddress address, byte[] key) { + final NativeInetAddress a = toNativeInetAddress(address); + setTcpMd5Sig0(fd, a.address, a.scopeId, key); + } + + private static native void setTcpMd5Sig0(int fd, byte[] address, int scopeId, byte[] key); + private static NativeInetAddress toNativeInetAddress(InetAddress addr) { byte[] bytes = addr.getAddress(); if (addr instanceof Inet6Address) { @@ -709,6 +718,8 @@ public final class Native { private static native int epollerr(); private static native long ssizeMax(); + private static native int tcpMd5SigMaxKeyLen(); + private Native() { // utility } diff --git a/transport-native-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java b/transport-native-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java new file mode 100644 index 0000000000..a56f7c9717 --- /dev/null +++ b/transport-native-epoll/src/main/java/io/netty/channel/epoll/TcpMd5Util.java @@ -0,0 +1,76 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.channel.epoll; + +import io.netty.util.internal.ObjectUtil; +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; + +final class TcpMd5Util { + + static Collection newTcpMd5Sigs(AbstractEpollChannel channel, Collection current, + Map newKeys) { + ObjectUtil.checkNotNull(channel, "channel"); + ObjectUtil.checkNotNull(current, "current"); + ObjectUtil.checkNotNull(newKeys, "newKeys"); + + // Validate incoming values + for (Entry e : newKeys.entrySet()) { + final byte[] key = e.getValue(); + if (e.getKey() == null) { + throw new IllegalArgumentException("newKeys contains an entry with null address: " + newKeys); + } + if (key == null) { + throw new NullPointerException("newKeys[" + e.getKey() + ']'); + } + if (key.length == 0) { + throw new IllegalArgumentException("newKeys[" + e.getKey() + "] has an empty key."); + } + if (key.length > Native.TCP_MD5SIG_MAXKEYLEN) { + throw new IllegalArgumentException("newKeys[" + e.getKey() + + "] has a key with invalid length; should not exceed the maximum length (" + + Native.TCP_MD5SIG_MAXKEYLEN + ')'); + } + } + + // Remove mappings not present in the new set. + for (InetAddress addr : current) { + if (!newKeys.containsKey(addr)) { + Native.setTcpMd5Sig(channel.fd().intValue(), addr, null); + } + } + + if (newKeys.isEmpty()) { + return Collections.emptySet(); + } + + // Set new mappings and store addresses which we set. + final Collection addresses = new ArrayList(newKeys.size()); + for (Entry e : newKeys.entrySet()) { + Native.setTcpMd5Sig(channel.fd().intValue(), e.getKey(), e.getValue()); + addresses.add(e.getKey()); + } + + return addresses; + } + + private TcpMd5Util() { + } +} diff --git a/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTcpMd5Test.java b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTcpMd5Test.java new file mode 100644 index 0000000000..fb6349ad6e --- /dev/null +++ b/transport-native-epoll/src/test/java/io/netty/channel/epoll/EpollSocketTcpMd5Test.java @@ -0,0 +1,113 @@ +/* + * Copyright 2015 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.channel.epoll; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ConnectTimeoutException; +import io.netty.channel.EventLoopGroup; +import io.netty.util.CharsetUtil; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Collections; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +public class EpollSocketTcpMd5Test { + private static final byte[] SERVER_KEY = "abc".getBytes(CharsetUtil.US_ASCII); + private static final byte[] BAD_KEY = "def".getBytes(CharsetUtil.US_ASCII); + private static EventLoopGroup GROUP; + private EpollServerSocketChannel server; + + @BeforeClass + public static void beforeClass() { + GROUP = new EpollEventLoopGroup(1); + } + + @AfterClass + public static void afterClass() { + GROUP.shutdownGracefully(); + } + + @Before + public void setup() { + Bootstrap bootstrap = new Bootstrap(); + server = (EpollServerSocketChannel) bootstrap.group(GROUP) + .channel(EpollServerSocketChannel.class) + .handler(new ChannelInboundHandlerAdapter()) + .bind(new InetSocketAddress(0)).syncUninterruptibly().channel(); + } + + @After + public void teardown() { + server.close().syncUninterruptibly(); + } + + @Test + public void testServerSocketChannelOption() throws Exception { + server.config().setOption(EpollChannelOption.TCP_MD5SIG, Collections.singletonMap(InetAddress.getLocalHost(), + SERVER_KEY)); + server.config().setOption(EpollChannelOption.TCP_MD5SIG, Collections.emptyMap()); + } + + @Test + public void testServerOption() throws Exception { + Bootstrap bootstrap = new Bootstrap(); + EpollServerSocketChannel ch = (EpollServerSocketChannel) bootstrap.group(GROUP) + .channel(EpollServerSocketChannel.class) + .handler(new ChannelInboundHandlerAdapter()) + .bind(new InetSocketAddress(0)).syncUninterruptibly().channel(); + + ch.config().setOption(EpollChannelOption.TCP_MD5SIG, Collections.singletonMap(InetAddress.getLocalHost(), + SERVER_KEY)); + ch.config().setOption(EpollChannelOption.TCP_MD5SIG, Collections.emptyMap()); + + ch.close().syncUninterruptibly(); + } + + @Test(expected = ConnectTimeoutException.class) + public void testKeyMismatch() throws Exception { + server.config().setOption(EpollChannelOption.TCP_MD5SIG, Collections.singletonMap(InetAddress.getLocalHost(), + SERVER_KEY)); + + EpollSocketChannel client = (EpollSocketChannel) new Bootstrap().group(GROUP) + .channel(EpollSocketChannel.class) + .handler(new ChannelInboundHandlerAdapter()) + .option(EpollChannelOption.TCP_MD5SIG, + Collections.singletonMap(InetAddress.getLocalHost(), BAD_KEY)) + .connect(server.localAddress()).syncUninterruptibly().channel(); + client.close().syncUninterruptibly(); + } + + @Test + public void testKeyMatch() throws Exception { + server.config().setOption(EpollChannelOption.TCP_MD5SIG, Collections.singletonMap(InetAddress.getLocalHost(), + SERVER_KEY)); + + EpollSocketChannel client = (EpollSocketChannel) new Bootstrap().group(GROUP) + .channel(EpollSocketChannel.class) + .handler(new ChannelInboundHandlerAdapter()) + .option(EpollChannelOption.TCP_MD5SIG, + Collections.singletonMap(InetAddress.getLocalHost(), SERVER_KEY)) + .connect(server.localAddress()).syncUninterruptibly().channel(); + client.close().syncUninterruptibly(); + } +}