Add support for RFC2385 on Linux

Motivation:

There are protocols (BGP, SXP), which are typically deployed with TCP
MD5 authentication to protect sessions from being hijacked/torn down by
third parties. This facility is not available on most operating systems,
but is typically present on Linux.

Modifications:

- add a new EpollChannelOption, which is write-only
- teach Epoll(Server)SocketChannel to track which addresses have keys
  associated
- teach Native how to set the MD5 signature keys for a socket

Result:

Users of the native-epoll transport can set MD5 signature keys and thus
leverage RFC-2385 protection on TCP connections.
This commit is contained in:
Robert Varga 2015-08-30 12:11:41 +02:00 committed by Norman Maurer
parent 34de2667c7
commit 30a7701616
11 changed files with 320 additions and 1 deletions

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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<T> extends ChannelOption<T> {
@SuppressWarnings("rawtypes")
private static final Class<EpollChannelOption> T = EpollChannelOption.class;
@ -36,6 +39,8 @@ public final class EpollChannelOption<T> extends ChannelOption<T> {
public static final ChannelOption<EpollMode> EPOLL_MODE =
ChannelOption.valueOf(T, "EPOLL_MODE");
public static final ChannelOption<Map<InetAddress, byte[]>> TCP_MD5SIG = valueOf("TCP_MD5SIG");
@SuppressWarnings({ "unused", "deprecation" })
private EpollChannelOption() {
super(null);

View File

@ -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<InetAddress, byte[]> m = (Map<InetAddress, byte[]>) value;
((EpollServerSocketChannel) channel).setTcpMd5Sig(m);
} else {
return super.setOption(option, value);
}

View File

@ -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<InetAddress> 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<InetAddress> tcpMd5SigAddresses() {
return tcpMd5SigAddresses;
}
void setTcpMd5Sig(Map<InetAddress, byte[]> keys) {
this.tcpMd5SigAddresses = TcpMd5Util.newTcpMd5Sigs(this, tcpMd5SigAddresses, keys);
}
}

View File

@ -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<InetAddress, byte[]> m = (Map<InetAddress, byte[]>) value;
setTcpMd5Sig(m);
} else {
return super.setOption(option, value);
}
@ -145,6 +150,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<InetAddress, byte[]> keys) {
((EpollServerSocketChannel) channel).setTcpMd5Sig(keys);
return this;
}
/**
* Returns {@code true} if the SO_REUSEPORT option is set.
*/

View File

@ -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<InetAddress> 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<InetAddress, byte[]> keys) {
this.tcpMd5SigAddresses = TcpMd5Util.newTcpMd5Sigs(this, tcpMd5SigAddresses, keys);
}
}

View File

@ -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<InetAddress, byte[]> m = (Map<InetAddress, byte[]>) 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<InetAddress, byte[]> keys) {
channel.setTcpMd5Sig(keys);
return this;
}
@Override
public boolean isAllowHalfClosure() {
return allowHalfClosure;

View File

@ -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
}

View File

@ -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<InetAddress> newTcpMd5Sigs(AbstractEpollChannel channel, Collection<InetAddress> current,
Map<InetAddress, byte[]> newKeys) {
ObjectUtil.checkNotNull(channel, "channel");
ObjectUtil.checkNotNull(current, "current");
ObjectUtil.checkNotNull(newKeys, "newKeys");
// Validate incoming values
for (Entry<InetAddress, byte[]> 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<InetAddress> addresses = new ArrayList<InetAddress>(newKeys.size());
for (Entry<InetAddress, byte[]> e : newKeys.entrySet()) {
Native.setTcpMd5Sig(channel.fd().intValue(), e.getKey(), e.getValue());
addresses.add(e.getKey());
}
return addresses;
}
private TcpMd5Util() {
}
}

View File

@ -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.<InetAddress, byte[]>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.<InetAddress, byte[]>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();
}
}