ipfilter implementation for netty 4/5 [#2129]

This commit is contained in:
Jakob Buchgraber 2014-03-05 00:36:54 +01:00 committed by Norman Maurer
parent d63c9f2bdb
commit 386cc2cb73
9 changed files with 621 additions and 0 deletions

View File

@ -0,0 +1,99 @@
/*
* Copyright 2014 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.handler.ipfilter;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import java.net.InetSocketAddress;
/**
* This class provides the functionality to either accept or reject new {@link Channel}s
* based on their IP address.
* <p>
* You should inherit from this class if you would like to implement your own IP-based filter. Basically you have to
* implement {@link #accept(InetSocketAddress)} to decided whether you want to allow or deny a connection from an IP
* address. Furthermore overriding {@link #rejected(ChannelHandlerContext, InetSocketAddress)} gives you the flexibility
* to respond to rejected (denied) connections. If you do not want to send a response, just have it return null.
* Take a look at {@link IpFilterRuleHandler} for details.
*/
public abstract class AbstractIpFilterHandler extends ChannelInboundHandlerAdapter {
private final AttributeKey<IpFilterDecision> decisionKey = AttributeKey.valueOf(getClass().getName());
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
final InetSocketAddress ipAddress = (InetSocketAddress) ctx.channel().remoteAddress();
if (!accept(ipAddress)) {
// the channel might be active already
if (ctx.channel().isActive()) {
handleRejected(ctx);
} else {
// if the channel is not active yet, store the decision for later use
// in #channelActive(ChannelHandlerContext ctx)
Attribute<IpFilterDecision> decision = ctx.attr(decisionKey);
decision.set(IpFilterDecision.REJECTED);
super.channelRegistered(ctx);
}
} else {
super.channelRegistered(ctx);
}
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
final IpFilterDecision decision = ctx.attr(decisionKey).get();
if (decision == IpFilterDecision.REJECTED) {
handleRejected(ctx);
} else {
super.channelActive(ctx);
}
}
private void handleRejected(ChannelHandlerContext ctx) {
final InetSocketAddress ipAddress = (InetSocketAddress) ctx.channel().remoteAddress();
ChannelFuture rejectedFuture = rejected(ctx, ipAddress);
if (rejectedFuture != null) {
rejectedFuture.addListener(ChannelFutureListener.CLOSE);
} else {
ctx.close();
}
}
/**
* This method is called immediately after a {@link io.netty.channel.Channel} gets registered.
*
* @return Return true if connections from this IP address and port should be accepted. False otherwise.
*/
protected abstract boolean accept(InetSocketAddress ipAndPort) throws Exception;
/**
* This method is called if ipAndPort gets rejected by {@link #accept(InetSocketAddress)}.
* You should override it if you would like to handle (e.g. respond to) rejected addresses.
*
* @return A {@link ChannelFuture} if you perform I/O operations, so that
* the {@link Channel} can be closed once it completes. Null otherwise.
*/
protected abstract ChannelFuture rejected(ChannelHandlerContext ctx, InetSocketAddress ipAndPort);
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2014 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.handler.ipfilter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandler;
/**
* This enum is used in {@link AbstractIpFilterHandler} to keep the state between the calls to
* {@link ChannelInboundHandler#channelRegistered(ChannelHandlerContext)} and
* {@link ChannelInboundHandler#channelActive(ChannelHandlerContext)}.
*/
public enum IpFilterDecision {
REJECTED
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2014 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.handler.ipfilter;
import java.net.InetSocketAddress;
/**
* Implement this interface to create new rules.
*/
public interface IpFilterRule {
/**
* @return This method should return true if ipAndPort is valid according to your criteria. False otherwise.
*/
boolean matches(InetSocketAddress ipAndPort);
/**
* @return This method should return {@link IpFilterRuleType#ALLOW} if all
* {@link IpFilterRule#matches(InetSocketAddress)} for which {@link #matches(InetSocketAddress)}
* returns true should the accepted. If you want to exclude all of those IP addresses then
* {@link IpFilterRuleType#DENY} should be returned.
*/
IpFilterRuleType ruleType();
}

View File

@ -0,0 +1,62 @@
/*
* Copyright 2014 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.handler.ipfilter;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.Channel;
import java.net.InetSocketAddress;
/**
* This class allows one to filter new {@link Channel}s based on the
* {@link IpFilterRule}s passed to its constructor. If no rules are provided, all connections
* will be accepted.
*
* If you would like to explicitly take action on rejected {@link Channel}s, you should override
* {@link #rejected(ChannelHandlerContext, InetSocketAddress)}.
*/
@Sharable
public class IpFilterRuleHandler extends AbstractIpFilterHandler {
private final IpFilterRule[] rules;
public IpFilterRuleHandler(IpFilterRule... rules) {
if (rules == null) {
throw new NullPointerException("rules");
}
if (rules.length == 0) {
throw new IllegalArgumentException("You have to provide at least one rule.");
}
this.rules = rules;
}
@Override
protected boolean accept(InetSocketAddress ipAndPort) throws Exception {
for (IpFilterRule rule : rules) {
if (rule.matches(ipAndPort)) {
return rule.ruleType() == IpFilterRuleType.ALLOW;
}
}
return true;
}
@Override
protected ChannelFuture rejected(ChannelHandlerContext ctx, InetSocketAddress ipAndPort) {
return null;
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright 2014 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.handler.ipfilter;
/**
* Used in {@link IpFilterRule} to decide if a matching IP Address should be allowed or denied to connect.
*/
public enum IpFilterRuleType {
ALLOW,
DENY
}

View File

@ -0,0 +1,158 @@
/*
* Copyright 2014 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.handler.ipfilter;
import java.math.BigInteger;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
/**
* Use this class to create rules for {@link IpFilterRuleHandler} that group IP addresses into subnets.
* Supports both, IPv4 and IPv6.
*/
public final class IpSubnetFilterRule implements IpFilterRule {
private final IpFilterRule filterRule;
public IpSubnetFilterRule(String ipAddress, int cidrPrefix, IpFilterRuleType ruleType) {
try {
filterRule = selectFilterRule(InetAddress.getByName(ipAddress), cidrPrefix, ruleType);
} catch (UnknownHostException e) {
throw new IllegalArgumentException("ipAddress", e);
}
}
public IpSubnetFilterRule(InetAddress ipAddress, int cidrPrefix, IpFilterRuleType ruleType) {
filterRule = selectFilterRule(ipAddress, cidrPrefix, ruleType);
}
private static IpFilterRule selectFilterRule(InetAddress ipAddress, int cidrPrefix, IpFilterRuleType ruleType) {
if (ipAddress == null) {
throw new NullPointerException("ipAddress");
}
if (ruleType == null) {
throw new NullPointerException("ruleType");
}
if (ipAddress instanceof Inet4Address) {
return new Ip4SubnetFilterRule((Inet4Address) ipAddress, cidrPrefix, ruleType);
} else if (ipAddress instanceof Inet6Address) {
return new Ip6SubnetFilterRule((Inet6Address) ipAddress, cidrPrefix, ruleType);
} else {
throw new IllegalArgumentException("Only IPv4 and IPv6 addresses are supported");
}
}
@Override
public boolean matches(InetSocketAddress ipAndPort) {
return filterRule.matches(ipAndPort);
}
@Override
public IpFilterRuleType ruleType() {
return filterRule.ruleType();
}
private static final class Ip4SubnetFilterRule implements IpFilterRule {
private final int networkAddress;
private final int subnetMask;
private final IpFilterRuleType ruleType;
private Ip4SubnetFilterRule(Inet4Address ipAddress, int cidrPrefix, IpFilterRuleType ruleType) {
if (cidrPrefix < 0 || cidrPrefix > 32) {
throw new IllegalArgumentException(String.format("IPv4 requires the subnet prefix to be in range of " +
"[0,32]. The prefix was: %d", cidrPrefix));
}
subnetMask = prefixToSubnetMask(cidrPrefix);
networkAddress = ipToInt(ipAddress) & subnetMask;
this.ruleType = ruleType;
}
@Override
public boolean matches(InetSocketAddress ipAndPort) {
int ipAddress = ipToInt((Inet4Address) ipAndPort.getAddress());
return (ipAddress & subnetMask) == networkAddress;
}
@Override
public IpFilterRuleType ruleType() {
return ruleType;
}
private static int ipToInt(Inet4Address ipAddress) {
byte[] octets = ipAddress.getAddress();
assert octets.length == 4;
return (octets[0] & 0xff) << 24 |
(octets[1] & 0xff) << 16 |
(octets[2] & 0xff) << 8 |
octets[3] & 0xff;
}
private static int prefixToSubnetMask(int cidrPrefix) {
return -1 << 32 - cidrPrefix;
}
}
private static final class Ip6SubnetFilterRule implements IpFilterRule {
private static final BigInteger MINUS_ONE = BigInteger.valueOf(-1);
private final BigInteger networkAddress;
private final BigInteger subnetMask;
private final IpFilterRuleType ruleType;
private Ip6SubnetFilterRule(Inet6Address ipAddress, int cidrPrefix, IpFilterRuleType ruleType) {
if (cidrPrefix < 0 || cidrPrefix > 128) {
throw new IllegalArgumentException(String.format("IPv6 requires the subnet prefix to be in range of " +
"[0,128]. The prefix was: %d", cidrPrefix));
}
subnetMask = prefixToSubnetMask(cidrPrefix);
networkAddress = ipToInt(ipAddress).and(subnetMask);
this.ruleType = ruleType;
}
@Override
public boolean matches(InetSocketAddress ipAndPort) {
BigInteger ipAddress = ipToInt((Inet6Address) ipAndPort.getAddress());
return ipAddress.and(subnetMask).equals(networkAddress);
}
@Override
public IpFilterRuleType ruleType() {
return ruleType;
}
private static BigInteger ipToInt(Inet6Address ipAddress) {
byte[] octets = ipAddress.getAddress();
assert octets.length == 16;
return new BigInteger(octets);
}
private static BigInteger prefixToSubnetMask(int cidrPrefix) {
return MINUS_ONE.shiftLeft(128 - cidrPrefix);
}
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2014 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.handler.ipfilter;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.internal.ConcurrentSet;
import io.netty.channel.Channel;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Set;
/**
* This class allows one to ensure that at all times for every IP address there is at most one
* {@link Channel} connected to the server.
*/
@ChannelHandler.Sharable
public class UniqueIpFilterHandler extends AbstractIpFilterHandler {
private final Set<InetAddress> connected = new ConcurrentSet<InetAddress>();
@Override
protected boolean accept(InetSocketAddress ipAndPort) throws Exception {
InetAddress ipAddress = ipAndPort.getAddress();
if (connected.contains(ipAddress)) {
return false;
} else {
connected.add(ipAddress);
return true;
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
InetAddress ipAddress = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress();
connected.remove(ipAddress);
super.channelInactive(ctx);
}
@Override
protected ChannelFuture rejected(ChannelHandlerContext ctx, InetSocketAddress ipAndPort) {
return null;
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2014 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 to filter IP addresses (allow/deny).
*/
package io.netty.handler.ipfilter;

View File

@ -0,0 +1,135 @@
/*
* Copyright 2014 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.handler.ipfilter;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.embedded.EmbeddedChannel;
import org.junit.Assert;
import org.junit.Test;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
public class IpSubnetFilterTest {
@Test
public void testIp4SubnetFilterRule() throws Exception {
IpSubnetFilterRule rule = new IpSubnetFilterRule("192.168.56.1", 24, IpFilterRuleType.ALLOW);
for (int i = 0; i <= 255; i++) {
Assert.assertTrue(rule.matches(newSockAddress(String.format("192.168.56.%d", i))));
}
Assert.assertFalse(rule.matches(newSockAddress("192.168.57.1")));
rule = new IpSubnetFilterRule("91.114.240.1", 23, IpFilterRuleType.ALLOW);
Assert.assertTrue(rule.matches(newSockAddress("91.114.240.43")));
Assert.assertTrue(rule.matches(newSockAddress("91.114.240.255")));
Assert.assertTrue(rule.matches(newSockAddress("91.114.241.193")));
Assert.assertTrue(rule.matches(newSockAddress("91.114.241.254")));
Assert.assertFalse(rule.matches(newSockAddress("91.115.241.2")));
}
@Test
public void testIp6SubnetFilterRule() {
IpSubnetFilterRule rule;
rule = new IpSubnetFilterRule("2001:db8:abcd:0000::", 52, IpFilterRuleType.ALLOW);
Assert.assertTrue(rule.matches(newSockAddress("2001:db8:abcd:0000::1")));
Assert.assertTrue(rule.matches(newSockAddress("2001:db8:abcd:0fff:ffff:ffff:ffff:ffff")));
Assert.assertFalse(rule.matches(newSockAddress("2001:db8:abcd:1000::")));
}
@Test
public void testIpFilterRuleHandler() throws Exception {
IpFilterRule filter0 = new IpFilterRule() {
@Override
public boolean matches(InetSocketAddress ipAndPort) {
return "192.168.57.1".equals(ipAndPort.getHostName());
}
@Override
public IpFilterRuleType ruleType() {
return IpFilterRuleType.DENY;
}
};
IpFilterRuleHandler denyHandler = new IpFilterRuleHandler(filter0) {
private final byte[] message = {1, 2, 3, 4, 5, 6, 7};
@Override
protected ChannelFuture rejected(ChannelHandlerContext ctx, InetSocketAddress ipAndPort) {
Assert.assertTrue(ctx.channel().isActive());
Assert.assertTrue(ctx.channel().isWritable());
Assert.assertEquals("192.168.57.1", ipAndPort.getHostName());
return ctx.writeAndFlush(Unpooled.wrappedBuffer(message));
}
};
EmbeddedChannel chDeny = newEmbeddedInetChannel("192.168.57.1", denyHandler);
ByteBuf out = chDeny.readOutbound();
Assert.assertEquals(7, out.readableBytes());
for (byte i = 1; i <= 7; i++) {
Assert.assertEquals(i, out.readByte());
}
Assert.assertFalse(chDeny.isActive());
Assert.assertFalse(chDeny.isOpen());
IpFilterRuleHandler allowHandler = new IpFilterRuleHandler(filter0) {
@Override
protected ChannelFuture rejected(ChannelHandlerContext ctx, InetSocketAddress ipAndPort) {
Assert.fail();
return null;
}
};
EmbeddedChannel chAllow = newEmbeddedInetChannel("192.168.57.2", allowHandler);
Assert.assertTrue(chAllow.isActive());
Assert.assertTrue(chAllow.isOpen());
}
@Test
public void testUniqueIpFilterHandler() {
UniqueIpFilterHandler handler = new UniqueIpFilterHandler();
EmbeddedChannel ch1 = newEmbeddedInetChannel("91.92.93.1", handler);
Assert.assertTrue(ch1.isActive());
EmbeddedChannel ch2 = newEmbeddedInetChannel("91.92.93.2", handler);
Assert.assertTrue(ch2.isActive());
EmbeddedChannel ch3 = newEmbeddedInetChannel("91.92.93.1", handler);
Assert.assertFalse(ch3.isActive());
// false means that no data is left to read/write
Assert.assertFalse(ch1.finish());
EmbeddedChannel ch4 = newEmbeddedInetChannel("91.92.93.1", handler);
Assert.assertTrue(ch4.isActive());
}
private static EmbeddedChannel newEmbeddedInetChannel(final String ipAddress, ChannelHandler... handlers) {
return new EmbeddedChannel(handlers) {
@Override
protected SocketAddress remoteAddress0() {
return isActive()? new InetSocketAddress(ipAddress, 5421) : null;
}
};
}
private static InetSocketAddress newSockAddress(String ipAddress) {
return new InetSocketAddress(ipAddress, 1234);
}
}