diff --git a/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilter.java b/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilter.java new file mode 100644 index 0000000000..9774f4b646 --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilter.java @@ -0,0 +1,226 @@ +/* + * Copyright 2020 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.ChannelHandler.Sharable; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.internal.ObjectUtil; + +import java.net.Inet4Address; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + *

+ * This class allows one to filter new {@link Channel}s based on the + * {@link IpSubnetFilter}s passed to its constructor. If no rules are provided, all connections + * will be accepted since {@code acceptIfNotFound} is {@code true} by default. + *

+ * + *

+ * If you would like to explicitly take action on rejected {@link Channel}s, you should override + * {@link AbstractRemoteAddressFilter#channelRejected(ChannelHandlerContext, SocketAddress)}. + *

+ * + *

+ * Few Points to keep in mind: + *

    + *
  1. Since {@link IpSubnetFilter} uses Binary search algorithm, it's a good + * idea to insert IP addresses in incremental order.
  2. + *
  3. Remove any over-lapping CIDR.
  4. + *
+ *

+ * + */ +@Sharable +public class IpSubnetFilter extends AbstractRemoteAddressFilter { + + private final boolean acceptIfNotFound; + private final List ipv4Rules; + private final List ipv6Rules; + private final IpFilterRuleType ipFilterRuleTypeIPv4; + private final IpFilterRuleType ipFilterRuleTypeIPv6; + + /** + *

Create new {@link IpSubnetFilter} Instance with specified {@link IpSubnetFilterRule} as array.

+ *

{@code acceptIfNotFound} is set to {@code true}.

+ * + * @param rules {@link IpSubnetFilterRule} as an array + */ + public IpSubnetFilter(IpSubnetFilterRule... rules) { + this(true, Arrays.asList(ObjectUtil.checkNotNull(rules, "rules"))); + } + + /** + *

Create new {@link IpSubnetFilter} Instance with specified {@link IpSubnetFilterRule} as array + * and specify if we'll accept a connection if we don't find it in the rule(s).

+ * + * @param acceptIfNotFound {@code true} if we'll accept connection if not found in rule(s). + * @param rules {@link IpSubnetFilterRule} as an array + */ + public IpSubnetFilter(boolean acceptIfNotFound, IpSubnetFilterRule... rules) { + this(acceptIfNotFound, Arrays.asList(ObjectUtil.checkNotNull(rules, "rules"))); + } + + /** + *

Create new {@link IpSubnetFilter} Instance with specified {@link IpSubnetFilterRule} as {@link List}.

+ *

{@code acceptIfNotFound} is set to {@code true}.

+ * + * @param rules {@link IpSubnetFilterRule} as a {@link List} + */ + public IpSubnetFilter(List rules) { + this(true, rules); + } + + /** + *

Create new {@link IpSubnetFilter} Instance with specified {@link IpSubnetFilterRule} as {@link List} + * and specify if we'll accept a connection if we don't find it in the rule(s).

+ * + * @param acceptIfNotFound {@code true} if we'll accept connection if not found in rule(s). + * @param rules {@link IpSubnetFilterRule} as a {@link List} + */ + public IpSubnetFilter(boolean acceptIfNotFound, List rules) { + ObjectUtil.checkNotNull(rules, "rules"); + this.acceptIfNotFound = acceptIfNotFound; + + int numAcceptIPv4 = 0; + int numRejectIPv4 = 0; + int numAcceptIPv6 = 0; + int numRejectIPv6 = 0; + + List unsortedIPv4Rules = new ArrayList(); + List unsortedIPv6Rules = new ArrayList(); + + // Iterate over rules and check for `null` rule. + for (IpSubnetFilterRule ipSubnetFilterRule : rules) { + ObjectUtil.checkNotNull(ipSubnetFilterRule, "rule"); + + if (ipSubnetFilterRule.getFilterRule() instanceof IpSubnetFilterRule.Ip4SubnetFilterRule) { + unsortedIPv4Rules.add(ipSubnetFilterRule); + + if (ipSubnetFilterRule.ruleType() == IpFilterRuleType.ACCEPT) { + numAcceptIPv4++; + } else { + numRejectIPv4++; + } + } else { + unsortedIPv6Rules.add(ipSubnetFilterRule); + + if (ipSubnetFilterRule.ruleType() == IpFilterRuleType.ACCEPT) { + numAcceptIPv6++; + } else { + numRejectIPv6++; + } + } + } + + /* + * If Number of ACCEPT rule is 0 and number of REJECT rules is more than 0, + * then all rules are of "REJECT" type. + * + * In this case, we'll set `ipFilterRuleTypeIPv4` to `IpFilterRuleType.REJECT`. + * + * If Number of ACCEPT rules are more than 0 and number of REJECT rules is 0, + * then all rules are of "ACCEPT" type. + * + * In this case, we'll set `ipFilterRuleTypeIPv4` to `IpFilterRuleType.ACCEPT`. + */ + if (numAcceptIPv4 == 0 && numRejectIPv4 > 0) { + ipFilterRuleTypeIPv4 = IpFilterRuleType.REJECT; + } else if (numAcceptIPv4 > 0 && numRejectIPv4 == 0) { + ipFilterRuleTypeIPv4 = IpFilterRuleType.ACCEPT; + } else { + ipFilterRuleTypeIPv4 = null; + } + + if (numAcceptIPv6 == 0 && numRejectIPv6 > 0) { + ipFilterRuleTypeIPv6 = IpFilterRuleType.REJECT; + } else if (numAcceptIPv6 > 0 && numRejectIPv6 == 0) { + ipFilterRuleTypeIPv6 = IpFilterRuleType.ACCEPT; + } else { + ipFilterRuleTypeIPv6 = null; + } + + this.ipv4Rules = sortAndFilter(unsortedIPv4Rules); + this.ipv6Rules = sortAndFilter(unsortedIPv6Rules); + } + + @Override + protected boolean accept(ChannelHandlerContext ctx, InetSocketAddress remoteAddress) { + if (remoteAddress.getAddress() instanceof Inet4Address) { + int indexOf = Collections.binarySearch(ipv4Rules, remoteAddress, IpSubnetFilterRuleComparator.INSTANCE); + if (indexOf >= 0) { + if (ipFilterRuleTypeIPv4 == null) { + return ipv4Rules.get(indexOf).ruleType() == IpFilterRuleType.ACCEPT; + } else { + return ipFilterRuleTypeIPv4 == IpFilterRuleType.ACCEPT; + } + } + } else { + int indexOf = Collections.binarySearch(ipv6Rules, remoteAddress, IpSubnetFilterRuleComparator.INSTANCE); + if (indexOf >= 0) { + if (ipFilterRuleTypeIPv6 == null) { + return ipv6Rules.get(indexOf).ruleType() == IpFilterRuleType.ACCEPT; + } else { + return ipFilterRuleTypeIPv6 == IpFilterRuleType.ACCEPT; + } + } + } + + return acceptIfNotFound; + } + + /** + *
    + *
  1. Sort the list
  2. + *
  3. Remove over-lapping subnet
  4. + *
  5. Sort the list again
  6. + *
+ */ + @SuppressWarnings("ConstantConditions") + private static List sortAndFilter(List rules) { + Collections.sort(rules); + Iterator iterator = rules.iterator(); + List toKeep = new ArrayList(); + + IpSubnetFilterRule parentRule = iterator.hasNext() ? iterator.next() : null; + if (parentRule != null) { + toKeep.add(parentRule); + } + + while (iterator.hasNext()) { + + // Grab a potential child rule. + IpSubnetFilterRule childRule = iterator.next(); + + // If parentRule matches childRule, then there's no need to keep the child rule. + // Otherwise, the rules are distinct and we need both. + if (!parentRule.matches(new InetSocketAddress(childRule.getIpAddress(), 1))) { + toKeep.add(childRule); + // Then we'll keep the child rule around as the parent for the next round. + parentRule = childRule; + } + } + + return toKeep; + } +} diff --git a/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java b/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java index bc6b1ffaca..e85e0713f0 100644 --- a/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java +++ b/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRule.java @@ -15,6 +15,7 @@ */ package io.netty.handler.ipfilter; +import io.netty.util.NetUtil; import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.SocketUtils; @@ -29,12 +30,14 @@ import java.net.UnknownHostException; * Use this class to create rules for {@link RuleBasedIpFilter} that group IP addresses into subnets. * Supports both, IPv4 and IPv6. */ -public final class IpSubnetFilterRule implements IpFilterRule { +public final class IpSubnetFilterRule implements IpFilterRule, Comparable { private final IpFilterRule filterRule; + private final String ipAddress; public IpSubnetFilterRule(String ipAddress, int cidrPrefix, IpFilterRuleType ruleType) { try { + this.ipAddress = ipAddress; filterRule = selectFilterRule(SocketUtils.addressByName(ipAddress), cidrPrefix, ruleType); } catch (UnknownHostException e) { throw new IllegalArgumentException("ipAddress", e); @@ -42,6 +45,7 @@ public final class IpSubnetFilterRule implements IpFilterRule { } public IpSubnetFilterRule(InetAddress ipAddress, int cidrPrefix, IpFilterRuleType ruleType) { + this.ipAddress = ipAddress.getHostAddress(); filterRule = selectFilterRule(ipAddress, cidrPrefix, ruleType); } @@ -68,7 +72,59 @@ public final class IpSubnetFilterRule implements IpFilterRule { return filterRule.ruleType(); } - private static final class Ip4SubnetFilterRule implements IpFilterRule { + /** + * Get IP Address of this rule + */ + String getIpAddress() { + return ipAddress; + } + + /** + * {@link Ip4SubnetFilterRule} or {@link Ip6SubnetFilterRule} + */ + IpFilterRule getFilterRule() { + return filterRule; + } + + @Override + public int compareTo(IpSubnetFilterRule ipSubnetFilterRule) { + if (filterRule instanceof Ip4SubnetFilterRule) { + return compareInt(((Ip4SubnetFilterRule) filterRule).networkAddress, + ((Ip4SubnetFilterRule) ipSubnetFilterRule.filterRule).networkAddress); + } else { + return ((Ip6SubnetFilterRule) filterRule).networkAddress + .compareTo(((Ip6SubnetFilterRule) ipSubnetFilterRule.filterRule).networkAddress); + } + } + + /** + * It'll compare IP address with {@link Ip4SubnetFilterRule#networkAddress} or + * {@link Ip6SubnetFilterRule#networkAddress}. + * + * @param inetSocketAddress {@link InetSocketAddress} to match + * @return 0 if IP Address match else difference index. + */ + int compareTo(InetSocketAddress inetSocketAddress) { + if (filterRule instanceof Ip4SubnetFilterRule) { + Ip4SubnetFilterRule ip4SubnetFilterRule = (Ip4SubnetFilterRule) filterRule; + return compareInt(ip4SubnetFilterRule.networkAddress, NetUtil.ipv4AddressToInt((Inet4Address) + inetSocketAddress.getAddress()) & ip4SubnetFilterRule.subnetMask); + } else { + Ip6SubnetFilterRule ip6SubnetFilterRule = (Ip6SubnetFilterRule) filterRule; + return ip6SubnetFilterRule.networkAddress + .compareTo(Ip6SubnetFilterRule.ipToInt((Inet6Address) inetSocketAddress.getAddress()) + .and(ip6SubnetFilterRule.networkAddress)); + } + } + + /** + * Equivalent to {@link Integer#compare(int, int)} + */ + private static int compareInt(int x, int y) { + return (x < y) ? -1 : ((x == y) ? 0 : 1); + } + + static final class Ip4SubnetFilterRule implements IpFilterRule { private final int networkAddress; private final int subnetMask; @@ -76,12 +132,12 @@ public final class IpSubnetFilterRule implements IpFilterRule { 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)); + 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; + networkAddress = NetUtil.ipv4AddressToInt(ipAddress) & subnetMask; this.ruleType = ruleType; } @@ -89,7 +145,7 @@ public final class IpSubnetFilterRule implements IpFilterRule { public boolean matches(InetSocketAddress remoteAddress) { final InetAddress inetAddress = remoteAddress.getAddress(); if (inetAddress instanceof Inet4Address) { - int ipAddress = ipToInt((Inet4Address) inetAddress); + int ipAddress = NetUtil.ipv4AddressToInt((Inet4Address) inetAddress); return (ipAddress & subnetMask) == networkAddress; } return false; @@ -100,18 +156,8 @@ public final class IpSubnetFilterRule implements IpFilterRule { 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) { - /** + /* * Perform the shift on a long and downcast it to int afterwards. * This is necessary to handle a cidrPrefix of zero correctly. * The left shift operator on an int only uses the five least @@ -125,7 +171,7 @@ public final class IpSubnetFilterRule implements IpFilterRule { } } - private static final class Ip6SubnetFilterRule implements IpFilterRule { + static final class Ip6SubnetFilterRule implements IpFilterRule { private static final BigInteger MINUS_ONE = BigInteger.valueOf(-1); @@ -135,8 +181,8 @@ public final class IpSubnetFilterRule implements IpFilterRule { 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)); + 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); diff --git a/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRuleComparator.java b/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRuleComparator.java new file mode 100644 index 0000000000..aaf2695ddf --- /dev/null +++ b/handler/src/main/java/io/netty/handler/ipfilter/IpSubnetFilterRuleComparator.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020 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; +import java.util.Comparator; + +/** + * This comparator is only used for searching. + */ +final class IpSubnetFilterRuleComparator implements Comparator { + + static final IpSubnetFilterRuleComparator INSTANCE = new IpSubnetFilterRuleComparator(); + + private IpSubnetFilterRuleComparator() { + // Prevent outside initialization + } + + @Override + public int compare(Object o1, Object o2) { + return ((IpSubnetFilterRule) o1).compareTo((InetSocketAddress) o2); + } +} diff --git a/handler/src/test/java/io/netty/handler/ipfilter/IpSubnetFilterTest.java b/handler/src/test/java/io/netty/handler/ipfilter/IpSubnetFilterTest.java index 0be4cab797..752f2947ce 100644 --- a/handler/src/test/java/io/netty/handler/ipfilter/IpSubnetFilterTest.java +++ b/handler/src/test/java/io/netty/handler/ipfilter/IpSubnetFilterTest.java @@ -27,6 +27,9 @@ import org.junit.Test; import java.net.InetSocketAddress; import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; public class IpSubnetFilterTest { @@ -141,6 +144,57 @@ public class IpSubnetFilterTest { Assert.assertTrue(ch4.isActive()); } + @Test + public void testBinarySearch() { + List ipSubnetFilterRuleList = new ArrayList(); + ipSubnetFilterRuleList.add(buildRejectIP("1.2.3.4", 32)); + ipSubnetFilterRuleList.add(buildRejectIP("1.1.1.1", 8)); + ipSubnetFilterRuleList.add(buildRejectIP("200.200.200.200", 32)); + ipSubnetFilterRuleList.add(buildRejectIP("108.0.0.0", 4)); + ipSubnetFilterRuleList.add(buildRejectIP("10.10.10.10", 8)); + ipSubnetFilterRuleList.add(buildRejectIP("2001:db8:abcd:0000::", 52)); + + // 1.0.0.0/8 + EmbeddedChannel ch1 = newEmbeddedInetChannel("1.1.1.1", new IpSubnetFilter(ipSubnetFilterRuleList)); + Assert.assertFalse(ch1.isActive()); + Assert.assertTrue(ch1.close().isSuccess()); + + // Nothing applies here + EmbeddedChannel ch2 = newEmbeddedInetChannel("2.2.2.2", new IpSubnetFilter(ipSubnetFilterRuleList)); + Assert.assertTrue(ch2.isActive()); + Assert.assertTrue(ch2.close().isSuccess()); + + // 108.0.0.0/4 + EmbeddedChannel ch3 = newEmbeddedInetChannel("97.100.100.100", new IpSubnetFilter(ipSubnetFilterRuleList)); + Assert.assertFalse(ch3.isActive()); + Assert.assertTrue(ch3.close().isSuccess()); + + // 200.200.200.200/32 + EmbeddedChannel ch4 = newEmbeddedInetChannel("200.200.200.200", new IpSubnetFilter(ipSubnetFilterRuleList)); + Assert.assertFalse(ch4.isActive()); + Assert.assertTrue(ch4.close().isSuccess()); + + // Nothing applies here + EmbeddedChannel ch5 = newEmbeddedInetChannel("127.0.0.1", new IpSubnetFilter(ipSubnetFilterRuleList)); + Assert.assertTrue(ch5.isActive()); + Assert.assertTrue(ch5.close().isSuccess()); + + // 10.0.0.0/8 + EmbeddedChannel ch6 = newEmbeddedInetChannel("10.1.1.2", new IpSubnetFilter(ipSubnetFilterRuleList)); + Assert.assertFalse(ch6.isActive()); + Assert.assertTrue(ch6.close().isSuccess()); + + //2001:db8:abcd:0000::/52 + EmbeddedChannel ch7 = newEmbeddedInetChannel("2001:db8:abcd:1000::", + new IpSubnetFilter(ipSubnetFilterRuleList)); + Assert.assertFalse(ch7.isActive()); + Assert.assertTrue(ch7.close().isSuccess()); + } + + private static IpSubnetFilterRule buildRejectIP(String ipAddress, int mask) { + return new IpSubnetFilterRule(ipAddress, mask, IpFilterRuleType.REJECT); + } + private static EmbeddedChannel newEmbeddedInetChannel(final String ipAddress, ChannelHandler... handlers) { return new EmbeddedChannel(handlers) { @Override