From 9e75f3191bac87b575179a7398e105316253ca02 Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Mon, 1 Jun 2026 18:14:15 +0200 Subject: [PATCH 1/2] rewrite o.a.m.filter.firewall.Subnet from commons-net --- .../apache/mina/filter/firewall/Subnet.java | 205 +++---- .../apache/mina/filter/util/SubnetUtils.java | 528 ++++++++++++++++++ .../apache/mina/filter/util/SubnetUtils6.java | 333 +++++++++++ .../mina/filter/firewall/SubnetIPv6Test.java | 15 +- 4 files changed, 951 insertions(+), 130 deletions(-) create mode 100644 mina-core/src/main/java/org/apache/mina/filter/util/SubnetUtils.java create mode 100644 mina-core/src/main/java/org/apache/mina/filter/util/SubnetUtils6.java diff --git a/mina-core/src/main/java/org/apache/mina/filter/firewall/Subnet.java b/mina-core/src/main/java/org/apache/mina/filter/firewall/Subnet.java index bbf933fbe..377f298bf 100644 --- a/mina-core/src/main/java/org/apache/mina/filter/firewall/Subnet.java +++ b/mina-core/src/main/java/org/apache/mina/filter/firewall/Subnet.java @@ -1,22 +1,22 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF 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. - * - */ +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF 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 org.apache.mina.filter.firewall; @@ -24,39 +24,30 @@ import java.net.Inet6Address; import java.net.InetAddress; +import org.apache.mina.filter.util.SubnetUtils; +import org.apache.mina.filter.util.SubnetUtils6; + /** - * A IP subnet using the CIDR notation. Currently, only IP version 4 - * address are supported. - * - * @author Apache MINA Project - */ +* A IP subnet using the CIDR notation. Currently, only IP version 4 +* address are supported. +* +* @author Apache MINA Project +*/ public class Subnet { - private static final int IP_MASK_V4 = 0x80000000; - - private static final long IP_MASK_V6 = 0x8000000000000000L; - - private static final int BYTE_MASK = 0xFF; - - private InetAddress subnet; - - /** An int representation of a subnet for IPV4 addresses */ - private int subnetInt; - - /** An long representation of a subnet for IPV6 addresses */ - private long subnetLong; + private SubnetUtils subnetUtils; + private SubnetUtils6 subnetUtils6; - private long subnetMask; - - private int suffix; + boolean isIpv6; /** - * Creates a subnet from CIDR notation. For example, the subnet - * 192.168.0.0/24 would be created using the {@link InetAddress} - * 192.168.0.0 and the mask 24. - * @param subnet The {@link InetAddress} of the subnet - * @param mask The mask - */ + * Creates a subnet from CIDR notation. For example, the subnet + * 192.168.0.0/24 would be created using the {@link InetAddress} + * 192.168.0.0 and the mask 24. + * + * @param subnet The {@link InetAddress} of the subnet + * @param mask The mask + */ public Subnet(InetAddress subnet, int mask) { if (subnet == null) { throw new IllegalArgumentException("Subnet address can not be null"); @@ -68,110 +59,80 @@ public Subnet(InetAddress subnet, int mask) { if (subnet instanceof Inet4Address) { // IPV4 address - if ((mask < 0) || (mask > 32)) { - throw new IllegalArgumentException("Mask has to be an integer between 0 and 32 for an IPV4 address"); - } else { - this.subnet = subnet; - subnetInt = toInt(subnet); - this.suffix = mask; - - // binary mask for this subnet - this.subnetMask = IP_MASK_V4 >> (mask - 1); - } - } else { - // IPV6 address - if ((mask < 0) || (mask > 128)) { - throw new IllegalArgumentException("Mask has to be an integer between 0 and 128 for an IPV6 address"); - } else { - this.subnet = subnet; - subnetLong = toLong(subnet); - this.suffix = mask; - - // binary mask for this subnet - this.subnetMask = IP_MASK_V6 >> (mask - 1); - } - } - } - - /** - * Converts an IP address into an integer - */ - private int toInt(InetAddress inetAddress) { - byte[] address = inetAddress.getAddress(); - int result = 0; - - for (int i = 0; i < address.length; i++) { - result <<= 8; - result |= address[i] & BYTE_MASK; - } - - return result; - } - - /** - * Converts an IP address into a long - */ - private long toLong(InetAddress inetAddress) { - byte[] address = inetAddress.getAddress(); - long result = 0; - - for (int i = 0; i < address.length; i++) { - result <<= 8; - result |= address[i] & BYTE_MASK; - } - - return result; - } - - /** - * Converts an IP address to a subnet using the provided mask - * - * @param address - * The address to convert into a subnet - * @return The subnet as an integer - */ - private long toSubnet(InetAddress address) { - if (address instanceof Inet4Address) { - return toInt(address) & (int) subnetMask; + this.subnetUtils = new SubnetUtils(subnet.getHostAddress() + "/" + mask); + this.subnetUtils.setInclusiveHostCount(true); + isIpv6 = false; } else { - return toLong(address) & subnetMask; + this.subnetUtils6 = new SubnetUtils6(subnet.getHostAddress(), mask); + isIpv6 = true; } } /** - * Checks if the {@link InetAddress} is within this subnet - * @param address The {@link InetAddress} to check - * @return True if the address is within this subnet, false otherwise - */ + * Checks if the {@link InetAddress} is within this subnet + * @param address The {@link InetAddress} to check + * @return True if the address is within this subnet, false otherwise + */ public boolean inSubnet(InetAddress address) { if (address.isAnyLocalAddress()) { return true; } - if (address instanceof Inet4Address) { - return (int) toSubnet(address) == subnetInt; + if (this.isIpv6 ) { + if (address instanceof Inet6Address) { + return subnetUtils6.getInfo().isInRange( (Inet6Address) address); + } else { + return false; + } } else { - return toSubnet(address) == subnetLong; + if (address instanceof Inet4Address) { + byte[] bytes = address.getAddress(); + int value = ((bytes[0] & 0xFF) << 24) | + ((bytes[1] & 0xFF) << 16) | + ((bytes[2] & 0xFF) << 8) | + (bytes[3] & 0xFF); + return subnetUtils.getInfo().isInRange(value); + } else { + return false; + } } } /** - * @see Object#toString() - */ + * @see Object#toString() + */ @Override public String toString() { - return subnet.getHostAddress() + "/" + suffix; + if (this.isIpv6 ) { + return subnetUtils6.getInfo().getCidrSignature(); + } else { + return subnetUtils.getInfo().getCidrSignature(); + } + } @Override public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + if (!(obj instanceof Subnet)) { return false; } Subnet other = (Subnet) obj; - return other.subnetInt == subnetInt && other.suffix == suffix; + if (this.isIpv6 != other.isIpv6) { + return false; + } + + if (this.isIpv6 ) { + return this.subnetUtils6.getInfo().getCidrSignature().equals(other.subnetUtils6.getInfo().getCidrSignature()); + } else { + return this.subnetUtils.getInfo().getCidrSignature().equals(other.subnetUtils.getInfo().getCidrSignature()); + } } } diff --git a/mina-core/src/main/java/org/apache/mina/filter/util/SubnetUtils.java b/mina-core/src/main/java/org/apache/mina/filter/util/SubnetUtils.java new file mode 100644 index 000000000..04fd588b6 --- /dev/null +++ b/mina-core/src/main/java/org/apache/mina/filter/util/SubnetUtils.java @@ -0,0 +1,528 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF 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 +* +* https://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 org.apache.mina.filter.util; + +import java.util.Iterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** +* Performs subnet calculations given a network address and a subnet mask. +* +* This class is extracted from Apache commons-net project +* @see Classless Inter-Domain Routing (CIDR): an Address Assignment and Aggregation Strategy +* @see SubnetUtils6 +* @since 2.0 +*/ +public class SubnetUtils { + + /** + * Allows an object to be the target of the "for-each loop" statement for a SubnetInfo. + */ + private static final class SubnetAddressStringIterable implements Iterable { + + private final SubnetInfo subnetInfo; + + /** + * Constructs a new instance. + * + * @param subnetInfo the SubnetInfo to iterate. + */ + private SubnetAddressStringIterable(final SubnetInfo subnetInfo) { + this.subnetInfo = subnetInfo; + } + + @Override + public Iterator iterator() { + return new SubnetAddressStringIterator(subnetInfo); + } + } + + /** + * Iterates over a SubnetInfo. + */ + private static final class SubnetAddressStringIterator implements Iterator { + + private int currentAddress; + + private final SubnetInfo subnetInfo; + + /** + * Constructs a new instance. + * + * @param subnetInfo the SubnetInfo to iterate. + */ + private SubnetAddressStringIterator(final SubnetInfo subnetInfo) { + this.subnetInfo = subnetInfo; + currentAddress = subnetInfo.low(); + } + + @Override + public boolean hasNext() { + return subnetInfo.getAddressCountLong() > 0 && currentAddress <= subnetInfo.high(); + } + + @Override + public String next() { + return format(toArray4(currentAddress++)); + } + } + + /** + * Contains subnet summary information. + */ + public final class SubnetInfo { + + /** Mask to convert unsigned int to a long (i.e. keep 32 bits). */ + private static final long UNSIGNED_INT_MASK = 0x0FFFFFFFFL; + + private SubnetInfo() { + } + + /** + * Converts a dotted decimal format address to a packed integer format. + * + * @param address a dotted decimal format address. + * @return packed integer formatted int. + */ + public int asInteger(final String address) { + return toInteger(address); + } + + private long broadcastLong() { + return broadcast & UNSIGNED_INT_MASK; + } + + /** + * Gets this instance's address into a dotted decimal String. + * + * @return a dotted decimal String. + */ + public String getAddress() { + return format(toArray4(address)); + } + + /** + * Gets the count of available addresses. Will be zero for CIDR/31 and CIDR/32 if the inclusive flag is false. + * + * @return the count of addresses, may be zero. + * @throws RuntimeException if the correct count is greater than {@code Integer.MAX_VALUE} + * @deprecated (3.4) use {@link #getAddressCountLong()} instead + */ + @Deprecated + public int getAddressCount() { + final long countLong = getAddressCountLong(); + if (countLong > Integer.MAX_VALUE) { + throw new IllegalStateException("Count is larger than an integer: " + countLong); + } + // Cannot be negative here + return (int) countLong; + } + + /** + * Gets the count of available addresses. Will be zero for CIDR/31 and CIDR/32 if the inclusive flag is false. + * + * @return the count of addresses, may be zero. + * @since 3.4 + */ + public long getAddressCountLong() { + final long b = broadcastLong(); + final long n = networkLong(); + final long count = b - n + (isInclusiveHostCount() ? 1 : -1); + return count < 0 ? 0 : count; + } + + /** + * Gets all addresses in this subnet, the return array could be huge. + *

+ * For large ranges, you can iterate or stream over the addresses instead using {@link #iterableAddressStrings()} or {@link #streamAddressStrings()}. + *

+ * + * @return all addresses in this subnet. + * @see #iterableAddressStrings() + * @see #streamAddressStrings() + */ + public String[] getAllAddresses() { + final int ct = getAddressCount(); + final String[] addresses = new String[ct]; + if (ct == 0) { + return addresses; + } + final int high = high(); + for (int add = low(), j = 0; add <= high; ++add, ++j) { + addresses[j] = format(toArray4(add)); + } + return addresses; + } + + /** + * Gets the broadcast address for this subnet. + * + * @return the broadcast address for this subnet. + */ + public String getBroadcastAddress() { + return format(toArray4(broadcast)); + } + + /** + * Gets the CIDR signature for this subnet. + * + * @return the CIDR signature for this subnet. + */ + public String getCidrSignature() { + return format(toArray4(address)) + "/" + Integer.bitCount(netmask); + } + + /** + * Gets the high address as a dotted IP address. Will be zero for CIDR/31 and CIDR/32 if the inclusive flag is false. + * + * @return the IP address in dotted format, may be "0.0.0.0" if there is no valid address + */ + public String getHighAddress() { + return format(toArray4(high())); + } + + /** + * Gets the low address as a dotted IP address. Will be zero for CIDR/31 and CIDR/32 if the inclusive flag is false. + * + * @return the IP address in dotted format, may be "0.0.0.0" if there is no valid address + */ + public String getLowAddress() { + return format(toArray4(low())); + } + + /** + * Gets the network mask for this subnet. + * + * @return the network mask for this subnet. + */ + public String getNetmask() { + return format(toArray4(netmask)); + } + + /** + * Gets the network address for this subnet. + * + * @return the network address for this subnet. + */ + public String getNetworkAddress() { + return format(toArray4(network)); + } + + /** + * Gets the next address for this subnet. + * + * @return the next address for this subnet. + */ + public String getNextAddress() { + return format(toArray4(address + 1)); + } + + /** + * Gets the previous address for this subnet. + * + * @return the previous address for this subnet. + */ + public String getPreviousAddress() { + return format(toArray4(address - 1)); + } + + private int high() { + return isInclusiveHostCount() ? broadcast : broadcastLong() - networkLong() > 1 ? broadcast - 1 : 0; + } + + /** + * Tests if the parameter {@code address} is in the range of usable endpoint addresses for this subnet. This excludes the network and broadcast + * addresses by default. Use {@link SubnetUtils#setInclusiveHostCount(boolean)} to change this. + * + * @param address the address to check + * @return true if it is in range + * @since 3.4 (made public) + */ + public boolean isInRange(final int address) { + if (address == 0) { // cannot ever be in range; rejecting now avoids problems with CIDR/31,32 + return false; + } + final long addLong = address & UNSIGNED_INT_MASK; + final long lowLong = low() & UNSIGNED_INT_MASK; + final long highLong = high() & UNSIGNED_INT_MASK; + return addLong >= lowLong && addLong <= highLong; + } + + /** + * Tests if the parameter {@code address} is in the range of usable endpoint addresses for this subnet. This excludes the network and broadcast + * addresses. Use {@link SubnetUtils#setInclusiveHostCount(boolean)} to change this. + * + * @param address A dot-delimited IPv4 address, e.g. "192.168.0.1" + * @return True if in range, false otherwise + */ + public boolean isInRange(final String address) { + return isInRange(toInteger(address)); + } + + /** + * Creates a new Iterable of address Strings. + * + * @return a new Iterable of address Strings + * @see #getAllAddresses() + * @see #streamAddressStrings() + * @since 3.12.0 + */ + public Iterable iterableAddressStrings() { + return new SubnetAddressStringIterable(this); + } + + private int low() { + return isInclusiveHostCount() ? network : broadcastLong() - networkLong() > 1 ? network + 1 : 0; + } + + /** Long versions of the values (as unsigned int) which are more suitable for range checking. */ + private long networkLong() { + return network & UNSIGNED_INT_MASK; + } + + /** + * Creates a new Stream of address Strings. + * + * @return a new Stream of address Strings. + * @see #getAllAddresses() + * @see #iterableAddressStrings() + * @since 3.12.0 + */ + public Stream streamAddressStrings() { + return StreamSupport.stream(iterableAddressStrings().spliterator(), false); + } + + /** + * {@inheritDoc} + * + * @since 2.2 + */ + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + // @formatter:off + buf.append("CIDR Signature:\t[").append(getCidrSignature()).append("]\n") + .append(" Netmask: [").append(getNetmask()).append("]\n") + .append(" Network: [").append(getNetworkAddress()).append("]\n") + .append(" Broadcast: [").append(getBroadcastAddress()).append("]\n") + .append(" First address: [").append(getLowAddress()).append("]\n") + .append(" Last address: [").append(getHighAddress()).append("]\n") + .append(" Address Count: [").append(getAddressCountLong()).append("]\n"); + // @formatter:on + return buf.toString(); + } + } + + private static final String IP_ADDRESS = "(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})"; + + private static final String SLASH_FORMAT = IP_ADDRESS + "/(\\d{1,2})"; // 0 -> 32 + + private static final Pattern ADDRESS_PATTERN = Pattern.compile(IP_ADDRESS); + private static final Pattern CIDR_PATTERN = Pattern.compile(SLASH_FORMAT); + private static final int NBITS = 32; + private static final String PARSE_FAIL = "Could not parse [%s]"; + + /** + * Converts a 4-element array into dotted decimal format. + */ + private static String format(final int[] octets) { + final int last = octets.length - 1; + final StringBuilder builder = new StringBuilder(); + for (int i = 0;; i++) { + builder.append(octets[i]); + if (i == last) { + return builder.toString(); + } + builder.append('.'); + } + } + + /** + * Extracts the components of a dotted decimal address and pack into an integer using a regex match + */ + private static int matchAddress(final Matcher matcher) { + int addr = 0; + for (int i = 1; i <= 4; ++i) { + final int n = rangeCheck(Integer.parseInt(matcher.group(i)), 0, 255); + addr |= (n & 0xff) << 8 * (4 - i); + } + return addr; + } + + /** + * Checks integer boundaries. Checks if a value x is in the range [begin,end]. Returns x if it is in range, throws an exception otherwise. + */ + private static int rangeCheck(final int value, final int begin, final int end) { + if (value >= begin && value <= end) { // (begin,end] + return value; + } + throw new IllegalArgumentException("Value [" + value + "] not in range [" + begin + "," + end + "]"); + } + + /** + * Converts a packed integer address into a 4-element array + */ + private static int[] toArray4(final int val) { + final int[] ret = new int[4]; + for (int j = 3; j >= 0; --j) { + ret[j] |= val >>> 8 * (3 - j) & 0xff; + } + return ret; + } + + /** + * Converts a dotted decimal format address to a packed integer format. + */ + private static int toInteger(final String address) { + final Matcher matcher = ADDRESS_PATTERN.matcher(address); + if (matcher.matches()) { + return matchAddress(matcher); + } + throw new IllegalArgumentException(String.format(PARSE_FAIL, address)); + } + + private final int address; + + private final int broadcast; + + /** Whether the broadcast/network address are included in host count */ + private boolean inclusiveHostCount; + + private final int netmask; + + private final int network; + + /** + * Constructs an instance from a CIDR-notation string, e.g. "192.168.0.1/16" + * + * @param cidrNotation A CIDR-notation string, e.g. "192.168.0.1/16" + * @throws IllegalArgumentException if the parameter is invalid, i.e. does not match n.n.n.n/m where n=1-3 decimal digits, m = 1-2 decimal digits in range + * 0-32 + */ + public SubnetUtils(final String cidrNotation) { + final Matcher matcher = CIDR_PATTERN.matcher(cidrNotation); + + if (!matcher.matches()) { + throw new IllegalArgumentException(String.format(PARSE_FAIL, cidrNotation)); + } + this.address = matchAddress(matcher); + + // Create a binary netmask from the number of bits specification /x + + final int trailingZeroes = NBITS - rangeCheck(Integer.parseInt(matcher.group(5)), 0, NBITS); + + // + // An IPv4 netmask consists of 32 bits, a contiguous sequence + // of the specified number of ones followed by all zeros. + // So, it can be obtained by shifting an unsigned integer (32 bits) to the left by + // the number of trailing zeros which is (32 - the # bits specification). + // Note that there is no unsigned left shift operator, so we have to use + // a long to ensure that the left-most bit is shifted out correctly. + // + this.netmask = (int) (0x0FFFFFFFFL << trailingZeroes); + + // Calculate base network address + this.network = address & netmask; + + // Calculate broadcast address + this.broadcast = network | ~netmask; + } + + /** + * Constructs an instance from a dotted decimal address and a dotted decimal mask. + * + * @param address An IP address, e.g. "192.168.0.1" + * @param mask A dotted decimal netmask e.g. "255.255.0.0" + * @throws IllegalArgumentException if the address or mask is invalid, i.e. does not match n.n.n.n where n=1-3 decimal digits and the mask is not all zeros + */ + public SubnetUtils(final String address, final String mask) { + this.address = toInteger(address); + this.netmask = toInteger(mask); + + if ((this.netmask & -this.netmask) - 1 != ~this.netmask) { + throw new IllegalArgumentException(String.format(PARSE_FAIL, mask)); + } + + // Calculate base network address + this.network = this.address & this.netmask; + + // Calculate broadcast address + this.broadcast = this.network | ~this.netmask; + } + + /** + * Gets a {@link SubnetInfo} instance that contains subnet-specific statistics + * + * @return new instance + */ + public final SubnetInfo getInfo() { + return new SubnetInfo(); + } + + /** + * Gets the next subnet for this instance. + * + * @return the next subnet for this instance. + */ + public SubnetUtils getNext() { + return new SubnetUtils(getInfo().getNextAddress(), getInfo().getNetmask()); + } + + /** + * Gets the previous subnet for this instance. + * + * @return the next previous for this instance. + */ + public SubnetUtils getPrevious() { + return new SubnetUtils(getInfo().getPreviousAddress(), getInfo().getNetmask()); + } + + /** + * Tests if the return value of {@link SubnetInfo#getAddressCount()} includes the network and broadcast addresses. + * + * @return true if the host count includes the network and broadcast addresses + * @since 2.2 + */ + public boolean isInclusiveHostCount() { + return inclusiveHostCount; + } + + /** + * Sets to {@code true} if you want the return value of {@link SubnetInfo#getAddressCount()} to include the network and broadcast addresses. This also + * applies to {@link SubnetInfo#isInRange(int)} + * + * @param inclusiveHostCount true if network and broadcast addresses are to be included + * @since 2.2 + */ + public void setInclusiveHostCount(final boolean inclusiveHostCount) { + this.inclusiveHostCount = inclusiveHostCount; + } + + /** + * Converts this instance to a debug String. + * + * @return {@code this} instance to a debug String. + * @since 3.11.0 + */ + @Override + public String toString() { + return getInfo().toString(); + } +} diff --git a/mina-core/src/main/java/org/apache/mina/filter/util/SubnetUtils6.java b/mina-core/src/main/java/org/apache/mina/filter/util/SubnetUtils6.java new file mode 100644 index 000000000..c47a158c8 --- /dev/null +++ b/mina-core/src/main/java/org/apache/mina/filter/util/SubnetUtils6.java @@ -0,0 +1,333 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one or more +* contributor license agreements. See the NOTICE file distributed with +* this work for additional information regarding copyright ownership. +* The ASF 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 +* +* https://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 org.apache.mina.filter.util; + +import java.math.BigInteger; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; + +/** +* Performs subnet calculations given an IPv6 network address and a prefix length. +*

+* This is the IPv6 equivalent of {@link SubnetUtils}. Addresses are parsed and formatted +* using {@link InetAddress}, which accepts the text representations described in +* RFC 5952. +*

+* +* This class is extracted from Apache commons-net project +* @see SubnetUtils +* @see RFC 5952 - A Recommendation for IPv6 Address Text Representation +* @since 3.13.0 +*/ +public class SubnetUtils6 { + + /** + * Contains IPv6 subnet summary information. + */ + public final class SubnetInfo { + + private SubnetInfo() { } + + /** + * Gets the address used to initialize this subnet. + * + * @return the address as a string in standard IPv6 format. + */ + public String getAddress() { + return format(address); + } + + /** + * Gets the count of available addresses in this subnet. + *

+ * For IPv6, this can be astronomically large. A /64 subnet has 2^64 addresses. + *

+ * + * @return the count of addresses as a BigInteger. + */ + public BigInteger getAddressCount() { + // 2^(128 - prefixLength) + return TWO.pow(NBITS - prefixLength); + } + + /** + * Gets the CIDR notation for this subnet. + * + * @return the CIDR signature (e.g., "2001:db8::1/64"). + */ + public String getCidrSignature() { + return format(address) + "/" + prefixLength; + } + + /** + * Gets the highest address in this subnet. + * + * @return the high address as a string in standard IPv6 format. + */ + public String getHighAddress() { + return format(high); + } + + /** + * Gets the lowest address in this subnet (the network address). + * + * @return the low address as a string in standard IPv6 format. + */ + public String getLowAddress() { + return format(network); + } + + /** + * Gets the network address for this subnet. + * + * @return the network address as a string in standard IPv6 format. + */ + public String getNetworkAddress() { + return format(network); + } + + /** + * Gets the prefix length for this subnet. + * + * @return the prefix length (0-128). + */ + public int getPrefixLength() { + return prefixLength; + } + + /** + * Tests if the given address is within this subnet range. + * + * @param addr the IPv6 address to test (as a BigInteger). + * @return true if the address is in range. + */ + public boolean isInRange(final BigInteger addr) { + if (addr == null) { + return false; + } + return addr.compareTo(network) >= 0 && addr.compareTo(high) <= 0; + } + + /** + * Tests if the given address is within this subnet range. + * + * @param addr the IPv6 address to test as a byte array (16 bytes). + * @return true if the address is in range. + */ + public boolean isInRange(final byte[] addr) { + if (addr == null || addr.length != 16) { + return false; + } + return isInRange(new BigInteger(1, addr)); + } + + /** + * Tests if the given address is within this subnet range. + * + * @param addr the IPv6 address to test. + * @return true if the address is in range. + */ + public boolean isInRange(final Inet6Address addr) { + if (addr == null) { + return false; + } + return isInRange(addr.getAddress()); + } + + /** + * Tests if the given address is within this subnet range. + * + * @param addr the IPv6 address to test as a string. + * @return true if the address is in range. + * @throws IllegalArgumentException if the address cannot be parsed. + */ + public boolean isInRange(final String addr) { + return isInRange(toBytes(addr)); + } + + /** + * Returns a summary of this subnet for debugging. + * + * @return a multi-line debug string summarizing this subnet. + */ + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("CIDR Signature:\t[").append(getCidrSignature()).append("]\n") + .append(" Network: [").append(getNetworkAddress()).append("]\n") + .append(" First address: [").append(getLowAddress()).append("]\n") + .append(" Last address: [").append(getHighAddress()).append("]\n") + .append(" Address Count: [").append(getAddressCount()).append("]\n"); + return buf.toString(); + } + } + + private static final int NBITS = 128; + private static final String PARSE_FAIL = "Could not parse [%s]"; + private static final BigInteger TWO = BigInteger.valueOf(2); + private static final BigInteger MAX_VALUE = TWO.pow(NBITS).subtract(BigInteger.ONE); + + /** + * Formats a BigInteger as an IPv6 address string using {@link InetAddress#getHostAddress()}. + * + * @param addr the address as a BigInteger. + * @return the formatted IPv6 address string. + * @see RFC 5952 + */ + private static String format(final BigInteger addr) { + final byte[] bytes = toByteArray16(addr); + try { + return InetAddress.getByAddress(bytes).getHostAddress(); + } catch (final UnknownHostException e) { + // Should never happen with a valid 16-byte array + throw new IllegalStateException("Unexpected error formatting IPv6 address", e); + } + } + + /** + * Converts a BigInteger to a 16-byte array, padding with leading zeros if necessary. + * + * @param value the BigInteger to convert. + * @return a 16-byte array. + */ + private static byte[] toByteArray16(final BigInteger value) { + final byte[] raw = value.toByteArray(); + if (raw.length == 16) { + return raw; + } + final byte[] result = new byte[16]; + if (raw.length > 16) { + // BigInteger may have a leading sign byte; skip it + System.arraycopy(raw, raw.length - 16, result, 0, 16); + } else { + // Pad with leading zeros + System.arraycopy(raw, 0, result, 16 - raw.length, raw.length); + } + return result; + } + + /** + * Parses an IPv6 address string to a byte array. + * + * @param address the IPv6 address string. + * @return the 16-byte representation. + * @throws IllegalArgumentException if the address cannot be parsed. + */ + private static byte[] toBytes(final String address) { + try { + final InetAddress inetAddr = InetAddress.getByName(address); + if (inetAddr instanceof Inet6Address) { + return inetAddr.getAddress(); + } + throw new IllegalArgumentException(String.format(PARSE_FAIL, address) + " - not an IPv6 address"); + } catch (final UnknownHostException e) { + throw new IllegalArgumentException(String.format(PARSE_FAIL, address), e); + } + } + + private final BigInteger address; + private final BigInteger high; + private final BigInteger network; + private final int prefixLength; + + /** + * Constructs an instance from a CIDR-notation string, e.g., "2001:db8::1/64". + * + * @param cidrNotation a CIDR-notation string, e.g., "2001:db8::1/64". + * @throws IllegalArgumentException if the parameter is invalid. + */ + public SubnetUtils6(final String cidrNotation) { + if (cidrNotation == null) { + throw new IllegalArgumentException(String.format(PARSE_FAIL, "null") + " - null input"); + } + + final int slashIndex = cidrNotation.indexOf('/'); + if (slashIndex < 0) { + throw new IllegalArgumentException(String.format(PARSE_FAIL, cidrNotation) + " - missing prefix length"); + } + + final String addressPart = cidrNotation.substring(0, slashIndex); + final String prefixPart = cidrNotation.substring(slashIndex + 1); + + // Parse and validate prefix length + try { + this.prefixLength = Integer.parseInt(prefixPart); + } catch (final NumberFormatException e) { + throw new IllegalArgumentException(String.format(PARSE_FAIL, cidrNotation) + " - invalid prefix length", e); + } + + if (this.prefixLength < 0 || this.prefixLength > NBITS) { + throw new IllegalArgumentException(String.format(PARSE_FAIL, cidrNotation) + + " - prefix length must be between 0 and " + NBITS); + } + + // Parse and validate IPv6 address + final byte[] addressBytes = toBytes(addressPart); + this.address = new BigInteger(1, addressBytes); + + // Create netmask: prefixLength 1-bits followed by (128 - prefixLength) 0-bits + final BigInteger netmask; + if (this.prefixLength == 0) { + netmask = BigInteger.ZERO; + } else { + netmask = MAX_VALUE.shiftLeft(NBITS - this.prefixLength).and(MAX_VALUE); + } + + // Calculate network address + this.network = this.address.and(netmask); + + // Calculate the highest address in the range + final BigInteger hostmask = MAX_VALUE.xor(netmask); + this.high = this.network.or(hostmask); + } + + /** + * Constructs an instance from an IPv6 address and prefix length. + * + * @param address an IPv6 address, e.g., "2001:db8::1". + * @param prefixLength the prefix length (0-128). + * @throws IllegalArgumentException if the parameters are invalid. + */ + public SubnetUtils6(final String address, final int prefixLength) { + this(address + "/" + prefixLength); + } + + /** + * Gets a {@link SubnetInfo} instance that contains subnet-specific statistics. + * + * @return a new SubnetInfo instance. + */ + public SubnetInfo getInfo() { + return new SubnetInfo(); + } + + /** + * Returns a summary of this subnet for debugging. + *

+ * Delegates to {@link SubnetInfo#toString()}. This is a diagnostic format and is not suitable for parsing. + * Use {@link SubnetInfo#getCidrSignature()} to obtain a string that can be fed back into + * {@link #SubnetUtils6(String)}. + *

+ * + * @return a multi-line debug string summarizing this subnet. + */ + @Override + public String toString() { + return getInfo().toString(); + } +} diff --git a/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv6Test.java b/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv6Test.java index 5d06601af..288c5e0d5 100644 --- a/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv6Test.java +++ b/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv6Test.java @@ -35,15 +35,14 @@ */ public class SubnetIPv6Test { - // Test Data - private static final String TEST_V6ADDRESS = "1080:0:0:0:8:800:200C:417A"; - @Test public void testIPv6() throws UnknownHostException { - InetAddress a = InetAddress.getByName(TEST_V6ADDRESS); - - assertTrue(a instanceof Inet6Address); - - new Subnet(a, 24); + + Subnet subnet = new Subnet(InetAddress.getByName("2001:db8::"), 32); + assertTrue(!subnet.inSubnet(InetAddress.getByName("2001:db7:ffff:ffff:ffff:ffff:ffff:ffff"))); + assertTrue(!subnet.inSubnet(InetAddress.getByName("2001:db9::"))); + assertTrue(subnet.inSubnet(InetAddress.getByName("2001:db8::1"))); + assertTrue(subnet.inSubnet(InetAddress.getByName("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"))); + } } From 13d1b8142409ca1db095ff8f100b9b86f47473ca Mon Sep 17 00:00:00 2001 From: Maxime Besson Date: Tue, 2 Jun 2026 14:29:39 +0200 Subject: [PATCH 2/2] ipv6 unit tests for o.a.m.filter.firewall.Subnet --- .../mina/filter/firewall/SubnetIPv4Test.java | 1 - .../mina/filter/firewall/SubnetIPv6Test.java | 122 ++++++++++++++---- 2 files changed, 97 insertions(+), 26 deletions(-) diff --git a/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv4Test.java b/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv4Test.java index db3e31429..9ed6612bf 100644 --- a/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv4Test.java +++ b/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv4Test.java @@ -30,7 +30,6 @@ import org.junit.Test; /** - * TODO Add documentation * * @author Apache MINA Project */ diff --git a/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv6Test.java b/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv6Test.java index 288c5e0d5..76b1ca2d1 100644 --- a/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv6Test.java +++ b/mina-core/src/test/java/org/apache/mina/filter/firewall/SubnetIPv6Test.java @@ -1,48 +1,120 @@ /* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF 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. - * - */ +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF 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 org.apache.mina.filter.firewall; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; import org.junit.Test; /** - * TODO Add documentation - * - * @author Apache MINA Project - */ +* +* @author Apache MINA Project +*/ public class SubnetIPv6Test { @Test public void testIPv6() throws UnknownHostException { - + Subnet subnet = new Subnet(InetAddress.getByName("2001:db8::"), 32); assertTrue(!subnet.inSubnet(InetAddress.getByName("2001:db7:ffff:ffff:ffff:ffff:ffff:ffff"))); assertTrue(!subnet.inSubnet(InetAddress.getByName("2001:db9::"))); assertTrue(subnet.inSubnet(InetAddress.getByName("2001:db8::1"))); assertTrue(subnet.inSubnet(InetAddress.getByName("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"))); - + + } + + @Test + public void test32() throws UnknownHostException { + InetAddress a = InetAddress.getByName("2001:db8::"); + InetAddress b = InetAddress.getByName("2001:db8::1"); + InetAddress c = InetAddress.getByName("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"); + InetAddress d = InetAddress.getByName("2001:db7:ffff:ffff:ffff:ffff:ffff:ffff"); + InetAddress e = InetAddress.getByName("2001:db9::"); + + Subnet mask = new Subnet(a, 32); + + assertTrue(mask.inSubnet(a)); + assertTrue(mask.inSubnet(b)); + assertTrue(mask.inSubnet(c)); + assertFalse(mask.inSubnet(d)); + assertFalse(mask.inSubnet(e)); + } + + @Test + public void test96() throws UnknownHostException { + InetAddress a = InetAddress.getByName("2001:db8:dead:beef:abcd:abcd::"); + InetAddress b = InetAddress.getByName("2001:db8:dead:beef:abcd:abcd::"); + InetAddress c = InetAddress.getByName("2001:db8:dead:beef:abcd:abcd:ffff:ffff"); + InetAddress d = InetAddress.getByName("2001:db8:dead:beef:abcd:abce::"); + InetAddress e = InetAddress.getByName("2001:db8:dead:beef:abcd:abcc:ffff:ffff"); + + Subnet mask = new Subnet(a, 96); + + assertTrue(mask.inSubnet(a)); + assertTrue(mask.inSubnet(b)); + assertTrue(mask.inSubnet(c)); + assertFalse(mask.inSubnet(d)); + assertFalse(mask.inSubnet(e)); + } + + @Test + public void testSingleIp() throws UnknownHostException { + InetAddress a = InetAddress.getByName("2001:db8:dead:beef:f0ca:cc1a:ac1d:ba5e"); + InetAddress b = InetAddress.getByName("2001:db8::"); + InetAddress c = InetAddress.getByName("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff"); + InetAddress d = InetAddress.getByName("2001:db8:dead:beef:f0ca:cc1a:ac1d:ba5f"); + InetAddress e = InetAddress.getByName("2001:db8:dead:beef:f0ca:cc1a:ac1d:ba5d"); + + Subnet mask = new Subnet(a, 128); + + assertTrue(mask.inSubnet(a)); + assertFalse(mask.inSubnet(b)); + assertFalse(mask.inSubnet(c)); + assertFalse(mask.inSubnet(d)); + assertFalse(mask.inSubnet(e)); + } + + @Test + public void testToString() throws UnknownHostException { + InetAddress a = InetAddress.getByName("2001:db8::"); + Subnet mask = new Subnet(a, 32); + + assertEquals("2001:db8:0:0:0:0:0:0/32", mask.toString()); + } + + @Test + public void testEquals() throws UnknownHostException { + Subnet a = new Subnet(InetAddress.getByName("2001:db8::"), 32); + Subnet b = new Subnet(InetAddress.getByName("2001:db8::"), 32); + Subnet c = new Subnet(InetAddress.getByName("2001:db8:dead:beef::"), 64); + Subnet d = new Subnet(InetAddress.getByName("2001:db8:dead:beef::"), 64); + + assertTrue(a.equals(b)); + assertFalse(a.equals(c)); + assertFalse(a.equals(d)); + assertFalse(a.equals(null)); } }