View Javadoc
1   /*
2    * Copyright 2020 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.ipfilter;
17  
18  import io.netty.channel.Channel;
19  import io.netty.channel.ChannelHandler.Sharable;
20  import io.netty.channel.ChannelHandlerContext;
21  import io.netty.util.internal.ObjectUtil;
22  
23  import java.net.Inet4Address;
24  import java.net.Inet6Address;
25  import java.net.InetSocketAddress;
26  import java.net.SocketAddress;
27  import java.util.ArrayList;
28  import java.util.Arrays;
29  import java.util.Collections;
30  import java.util.Comparator;
31  import java.util.Iterator;
32  import java.util.List;
33  
34  /**
35   * <p>
36   * This class allows one to filter new {@link Channel}s based on the
37   * {@link IpSubnetFilter}s passed to its constructor. If no rules are provided, all connections
38   * will be accepted since {@code acceptIfNotFound} is {@code true} by default.
39   * </p>
40   *
41   * <p>
42   * If you would like to explicitly take action on rejected {@link Channel}s, you should override
43   * {@link AbstractRemoteAddressFilter#channelRejected(ChannelHandlerContext, SocketAddress)}.
44   * </p>
45   *
46   * <p>
47   *     Few Points to keep in mind:
48   *     <ol>
49   *         <li> Since {@link IpSubnetFilter} uses Binary search algorithm, it's a good
50   *         idea to insert IP addresses in incremental order. </li>
51   *         <li> Remove any over-lapping CIDR.  </li>
52   *     </ol>
53   * </p>
54   *
55   */
56  @Sharable
57  public class IpSubnetFilter extends AbstractRemoteAddressFilter<InetSocketAddress> {
58  
59      private final boolean acceptIfNotFound;
60      private final IpSubnetFilterRule[] ipv4Rules;
61      private final IpSubnetFilterRule[] ipv6Rules;
62      private final IpFilterRuleType ipFilterRuleTypeIPv4;
63      private final IpFilterRuleType ipFilterRuleTypeIPv6;
64  
65      /**
66       * <p> Create new {@link IpSubnetFilter} Instance with specified {@link IpSubnetFilterRule} as array. </p>
67       * <p> {@code acceptIfNotFound} is set to {@code true}. </p>
68       *
69       * @param rules {@link IpSubnetFilterRule} as an array
70       */
71      public IpSubnetFilter(IpSubnetFilterRule... rules) {
72          this(true, Arrays.asList(ObjectUtil.checkNotNull(rules, "rules")));
73      }
74  
75      /**
76       * <p> Create new {@link IpSubnetFilter} Instance with specified {@link IpSubnetFilterRule} as array
77       * and specify if we'll accept a connection if we don't find it in the rule(s). </p>
78       *
79       * @param acceptIfNotFound {@code true} if we'll accept connection if not found in rule(s).
80       * @param rules            {@link IpSubnetFilterRule} as an array
81       */
82      public IpSubnetFilter(boolean acceptIfNotFound, IpSubnetFilterRule... rules) {
83          this(acceptIfNotFound, Arrays.asList(ObjectUtil.checkNotNull(rules, "rules")));
84      }
85  
86      /**
87       * <p> Create new {@link IpSubnetFilter} Instance with specified {@link IpSubnetFilterRule} as {@link List}. </p>
88       * <p> {@code acceptIfNotFound} is set to {@code true}. </p>
89       *
90       * @param rules {@link IpSubnetFilterRule} as a {@link List}
91       */
92      public IpSubnetFilter(List<IpSubnetFilterRule> rules) {
93          this(true, rules);
94      }
95  
96      /**
97       * <p> Create new {@link IpSubnetFilter} Instance with specified {@link IpSubnetFilterRule} as {@link List}
98       * and specify if we'll accept a connection if we don't find it in the rule(s). </p>
99       *
100      * @param acceptIfNotFound {@code true} if we'll accept connection if not found in rule(s).
101      * @param rules            {@link IpSubnetFilterRule} as a {@link List}
102      */
103     public IpSubnetFilter(boolean acceptIfNotFound, List<IpSubnetFilterRule> rules) {
104         ObjectUtil.checkNotNull(rules, "rules");
105         this.acceptIfNotFound = acceptIfNotFound;
106 
107         int numAcceptIPv4 = 0;
108         int numRejectIPv4 = 0;
109         int numAcceptIPv6 = 0;
110         int numRejectIPv6 = 0;
111 
112         List<IpSubnetFilterRule> unsortedIPv4Rules = new ArrayList<IpSubnetFilterRule>();
113         List<IpSubnetFilterRule> unsortedIPv6Rules = new ArrayList<IpSubnetFilterRule>();
114 
115         // Iterate over rules and check for `null` rule.
116         for (IpSubnetFilterRule ipSubnetFilterRule : rules) {
117             ObjectUtil.checkNotNull(ipSubnetFilterRule, "rule");
118 
119             if (ipSubnetFilterRule.getFilterRule() instanceof IpSubnetFilterRule.Ip4SubnetFilterRule) {
120                 unsortedIPv4Rules.add(ipSubnetFilterRule);
121 
122                 if (ipSubnetFilterRule.ruleType() == IpFilterRuleType.ACCEPT) {
123                     numAcceptIPv4++;
124                 } else {
125                     numRejectIPv4++;
126                 }
127             } else {
128                 unsortedIPv6Rules.add(ipSubnetFilterRule);
129 
130                 if (ipSubnetFilterRule.ruleType() == IpFilterRuleType.ACCEPT) {
131                     numAcceptIPv6++;
132                 } else {
133                     numRejectIPv6++;
134                 }
135             }
136         }
137 
138         /*
139          * If Number of ACCEPT rule is 0 and number of REJECT rules is more than 0,
140          * then all rules are of "REJECT" type.
141          *
142          * In this case, we'll set `ipFilterRuleTypeIPv4` to `IpFilterRuleType.REJECT`.
143          *
144          * If Number of ACCEPT rules are more than 0 and number of REJECT rules is 0,
145          * then all rules are of "ACCEPT" type.
146          *
147          * In this case, we'll set `ipFilterRuleTypeIPv4` to `IpFilterRuleType.ACCEPT`.
148          */
149         if (numAcceptIPv4 == 0 && numRejectIPv4 > 0) {
150             ipFilterRuleTypeIPv4 = IpFilterRuleType.REJECT;
151         } else if (numAcceptIPv4 > 0 && numRejectIPv4 == 0) {
152             ipFilterRuleTypeIPv4 = IpFilterRuleType.ACCEPT;
153         } else {
154             ipFilterRuleTypeIPv4 = null;
155         }
156 
157         if (numAcceptIPv6 == 0 && numRejectIPv6 > 0) {
158             ipFilterRuleTypeIPv6 = IpFilterRuleType.REJECT;
159         } else if (numAcceptIPv6 > 0 && numRejectIPv6 == 0) {
160             ipFilterRuleTypeIPv6 = IpFilterRuleType.ACCEPT;
161         } else {
162             ipFilterRuleTypeIPv6 = null;
163         }
164 
165         this.ipv4Rules = unsortedIPv4Rules.isEmpty() ? null : sortAndFilter(unsortedIPv4Rules);
166         this.ipv6Rules = unsortedIPv6Rules.isEmpty() ? null : sortAndFilter(unsortedIPv6Rules);
167     }
168 
169     @Override
170     protected boolean accept(ChannelHandlerContext ctx, InetSocketAddress remoteAddress) {
171         if (ipv4Rules != null && remoteAddress.getAddress() instanceof Inet4Address) {
172             int indexOf = Arrays.binarySearch(ipv4Rules, remoteAddress, IpSubnetFilterRuleComparator.INSTANCE);
173             if (indexOf >= 0) {
174                 if (ipFilterRuleTypeIPv4 == null) {
175                     return ipv4Rules[indexOf].ruleType() == IpFilterRuleType.ACCEPT;
176                 } else {
177                     return ipFilterRuleTypeIPv4 == IpFilterRuleType.ACCEPT;
178                 }
179             }
180         } else if (ipv6Rules != null && remoteAddress.getAddress() instanceof Inet6Address) {
181             int indexOf = Arrays.binarySearch(ipv6Rules, remoteAddress, IpSubnetFilterRuleComparator.INSTANCE);
182             if (indexOf >= 0) {
183                 if (ipFilterRuleTypeIPv6 == null) {
184                     return ipv6Rules[indexOf].ruleType() == IpFilterRuleType.ACCEPT;
185                 } else {
186                     return ipFilterRuleTypeIPv6 == IpFilterRuleType.ACCEPT;
187                 }
188             }
189         }
190 
191         return acceptIfNotFound;
192     }
193 
194     /**
195      * <ol>
196      *     <li> Sort the list </li>
197      *     <li> Remove over-lapping subnet </li>
198      *     <li> Sort the list again </li>
199      * </ol>
200      */
201     @SuppressWarnings("ZeroLengthArrayAllocation")
202     private static IpSubnetFilterRule[] sortAndFilter(List<IpSubnetFilterRule> rules) {
203         // Equal network addresses: broader subnet first, so it becomes the parent of the narrower one.
204         Collections.sort(rules, new Comparator<IpSubnetFilterRule>() {
205             @Override
206             public int compare(IpSubnetFilterRule a, IpSubnetFilterRule b) {
207                 int cmp = a.compareTo(b);
208                 if (cmp != 0) {
209                     return cmp;
210                 }
211                 return a.contains(b) ? (b.contains(a) ? 0 : -1) : 1;
212             }
213         });
214         Iterator<IpSubnetFilterRule> iterator = rules.iterator();
215         List<IpSubnetFilterRule> toKeep = new ArrayList<IpSubnetFilterRule>();
216 
217         IpSubnetFilterRule parentRule = iterator.hasNext() ? iterator.next() : null;
218         if (parentRule != null) {
219             toKeep.add(parentRule);
220         }
221 
222         while (iterator.hasNext()) {
223 
224             // Grab a potential child rule.
225             IpSubnetFilterRule childRule = iterator.next();
226 
227             // If parentRule matches childRule, then there's no need to keep the child rule.
228             // Otherwise, the rules are distinct and we need both.
229             if (parentRule.contains(childRule)) {
230                 // The child rule is a subset of the parent rule. If they don't share the same rule type
231                 // we can't safely drop the child rule, as doing so would silently change the outcome for
232                 // addresses covered by the child but not otherwise expressible with the parent's ruleType.
233                 // This binary-search based design can't represent longest-prefix-match semantics, so reject
234                 // the unsupported configuration instead of failing open.
235                 if (childRule.ruleType() != parentRule.ruleType()) {
236                     throw new IllegalArgumentException(
237                             "IpSubnetFilter does not support overlapping rules with different ruleTypes: "
238                                     + parentRule + " (" + parentRule.ruleType() + ") overlaps "
239                                     + childRule + " (" + childRule.ruleType() + "). "
240                                     + "Use RuleBasedIpFilter for mixed ACCEPT/REJECT carve-outs.");
241                 }
242                 continue;
243             }
244 
245             toKeep.add(childRule);
246             // Then we'll keep the child rule around as the parent for the next round.
247             parentRule = childRule;
248         }
249 
250         return toKeep.toArray(new IpSubnetFilterRule[0]);
251     }
252 }