diff --git a/src/main/java/org/apache/commons/net/examples/cidr/SubnetUtils6Example.java b/src/main/java/org/apache/commons/net/examples/cidr/SubnetUtils6Example.java new file mode 100644 index 000000000..35fd25d67 --- /dev/null +++ b/src/main/java/org/apache/commons/net/examples/cidr/SubnetUtils6Example.java @@ -0,0 +1,58 @@ +/* + * 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.commons.net.examples.cidr; + +import java.nio.charset.Charset; +import java.util.Scanner; + +import org.apache.commons.net.util.SubnetUtils6; +import org.apache.commons.net.util.SubnetUtils6.SubnetInfo; + +/** + * Example class that shows how to use the {@link SubnetUtils6} class. + */ +public class SubnetUtils6Example { + + public static void main(final String[] args) { + final String subnet = "2001:db8:85a3::8a2e:370:7334/64"; + final SubnetUtils6 utils = new SubnetUtils6(subnet); + final SubnetInfo info = utils.getInfo(); + + System.out.printf("Subnet Information for %s:%n", subnet); + System.out.println("--------------------------------------"); + System.out.printf("IP Address:\t\t\t%s%n", info.getAddress()); + System.out.printf("Prefix Length:\t\t\t%d%n", info.getPrefixLength()); + System.out.printf("CIDR Representation:\t\t%s%n%n", info.getCidrSignature()); + + System.out.printf("Network Address:\t\t%s%n", info.getNetworkAddress()); + System.out.printf("Low Address:\t\t\t%s%n", info.getLowAddress()); + System.out.printf("High Address:\t\t\t%s%n", info.getHighAddress()); + + System.out.printf("Total addresses in subnet:\t%s%n%n", info.getAddressCount()); + + final String prompt = "Enter an IPv6 address (e.g., 2001:db8:85a3::1):"; + System.out.println(prompt); + try (Scanner scanner = new Scanner(System.in, Charset.defaultCharset().name())) { + while (scanner.hasNextLine()) { + final String address = scanner.nextLine(); + System.out.println("The IP address [" + address + "] is " + (info.isInRange(address) ? "" : "not ") + "within the subnet [" + subnet + "]"); + System.out.println(prompt); + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/org/apache/commons/net/util/SubnetUtils6.java b/src/main/java/org/apache/commons/net/util/SubnetUtils6.java new file mode 100644 index 000000000..8c9fec2ef --- /dev/null +++ b/src/main/java/org/apache/commons/net/util/SubnetUtils6.java @@ -0,0 +1,316 @@ +/* + * 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.commons.net.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}. + *

+ * + * @see SubnetUtils + * @since 3.13 + */ +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)); + } + + @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. + *

+ * Uses the canonical format with :: compression for the longest run of zeros. + *

+ * + * @param addr the address as a BigInteger. + * @return the formatted IPv6 address string. + */ + 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(); + } + + @Override + public String toString() { + return getInfo().toString(); + } +} diff --git a/src/test/java/org/apache/commons/net/util/SubnetUtils6Test.java b/src/test/java/org/apache/commons/net/util/SubnetUtils6Test.java new file mode 100644 index 000000000..8022d0285 --- /dev/null +++ b/src/test/java/org/apache/commons/net/util/SubnetUtils6Test.java @@ -0,0 +1,259 @@ +/* + * 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.commons.net.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.math.BigInteger; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.apache.commons.net.util.SubnetUtils6.SubnetInfo; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link SubnetUtils6}. + */ +public class SubnetUtils6Test { + + private static final BigInteger TWO = BigInteger.valueOf(2); + + @Test + public void testBasicCidr64() { + final SubnetUtils6 utils = new SubnetUtils6("2001:db8::1/64"); + final SubnetInfo info = utils.getInfo(); + + assertEquals(64, info.getPrefixLength()); + assertEquals("2001:db8:0:0:0:0:0:1", info.getAddress()); + assertEquals("2001:db8:0:0:0:0:0:0", info.getNetworkAddress()); + assertEquals("2001:db8:0:0:ffff:ffff:ffff:ffff", info.getHighAddress()); + // 2^64 addresses + assertEquals(TWO.pow(64), info.getAddressCount()); + } + + @Test + public void testBasicCidr128() { + final SubnetUtils6 utils = new SubnetUtils6("2001:db8::1/128"); + final SubnetInfo info = utils.getInfo(); + + assertEquals(128, info.getPrefixLength()); + assertEquals("2001:db8:0:0:0:0:0:1", info.getNetworkAddress()); + assertEquals("2001:db8:0:0:0:0:0:1", info.getHighAddress()); + assertEquals(BigInteger.ONE, info.getAddressCount()); + } + + @Test + public void testCidr0() { + final SubnetUtils6 utils = new SubnetUtils6("::/0"); + final SubnetInfo info = utils.getInfo(); + + assertEquals(0, info.getPrefixLength()); + assertEquals("0:0:0:0:0:0:0:0", info.getNetworkAddress()); + // 2^128 addresses + assertEquals(TWO.pow(128), info.getAddressCount()); + } + + @Test + public void testCidr48() { + final SubnetUtils6 utils = new SubnetUtils6("2001:db8:abcd::/48"); + final SubnetInfo info = utils.getInfo(); + + assertEquals(48, info.getPrefixLength()); + assertEquals("2001:db8:abcd:0:0:0:0:0", info.getNetworkAddress()); + assertEquals("2001:db8:abcd:ffff:ffff:ffff:ffff:ffff", info.getHighAddress()); + } + + @Test + public void testCompressedAddress() { + final SubnetUtils6 utils = new SubnetUtils6("fe80::1/10"); + final SubnetInfo info = utils.getInfo(); + + assertEquals(10, info.getPrefixLength()); + assertTrue(info.isInRange("fe80::1")); + assertTrue(info.isInRange("fe80::ffff")); + assertTrue(info.isInRange("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff")); + assertFalse(info.isInRange("fec0::1")); // Outside /10 range + } + + @Test + public void testConstructorWithSeparateArgs() { + final SubnetUtils6 utils = new SubnetUtils6("2001:db8::1", 64); + final SubnetInfo info = utils.getInfo(); + + assertEquals(64, info.getPrefixLength()); + assertEquals("2001:db8:0:0:0:0:0:0", info.getNetworkAddress()); + } + + @Test + public void testFullAddress() { + final SubnetUtils6 utils = new SubnetUtils6("2001:0db8:0000:0000:0000:0000:0000:0001/64"); + final SubnetInfo info = utils.getInfo(); + + assertEquals(64, info.getPrefixLength()); + assertEquals("2001:db8:0:0:0:0:0:0", info.getNetworkAddress()); + } + + @Test + public void testGetCidrSignature() { + final SubnetUtils6 utils = new SubnetUtils6("2001:db8::1/64"); + final SubnetInfo info = utils.getInfo(); + + assertTrue(info.getCidrSignature().contains("/64")); + assertTrue(info.getCidrSignature().contains("2001:db8")); + } + + @Test + public void testInvalidCidr() { + assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("2001:db8::1")); + assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("2001:db8::1/")); + assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("2001:db8::1/129")); + assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("2001:db8::1/-1")); + assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("2001:db8::1/abc")); + assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("not-an-address/64")); + } + + @Test + public void testInvalidIPv4Address() { + // IPv4 addresses should be rejected + assertThrows(IllegalArgumentException.class, () -> new SubnetUtils6("192.168.1.1/24")); + } + + @Test + public void testIsInRange() { + final SubnetUtils6 utils = new SubnetUtils6("2001:db8::/32"); + final SubnetInfo info = utils.getInfo(); + + // Addresses in range + assertTrue(info.isInRange("2001:db8::1")); + assertTrue(info.isInRange("2001:db8::")); + assertTrue(info.isInRange("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff")); + assertTrue(info.isInRange("2001:db8:1234:5678:9abc:def0:1234:5678")); + + // Addresses out of range + assertFalse(info.isInRange("2001:db9::1")); + assertFalse(info.isInRange("2001:db7::1")); + assertFalse(info.isInRange("2002:db8::1")); + assertFalse(info.isInRange("::1")); + } + + @Test + public void testIsInRangeWithBigInteger() { + final SubnetUtils6 utils = new SubnetUtils6("2001:db8::/32"); + final SubnetInfo info = utils.getInfo(); + + // Test with null + assertFalse(info.isInRange((BigInteger) null)); + } + + @Test + public void testIsInRangeWithByteArray() { + final SubnetUtils6 utils = new SubnetUtils6("2001:db8::/32"); + final SubnetInfo info = utils.getInfo(); + + // Test with null + assertFalse(info.isInRange((byte[]) null)); + + // Test with wrong length + assertFalse(info.isInRange(new byte[4])); + assertFalse(info.isInRange(new byte[15])); + assertFalse(info.isInRange(new byte[17])); + } + + @Test + public void testIsInRangeWithInet6Address() throws UnknownHostException { + final SubnetUtils6 utils = new SubnetUtils6("2001:db8::/32"); + final SubnetInfo info = utils.getInfo(); + + // Test with actual Inet6Address + final Inet6Address addr = (Inet6Address) InetAddress.getByName("2001:db8::1"); + assertTrue(info.isInRange(addr)); + + final Inet6Address addrOutside = (Inet6Address) InetAddress.getByName("2001:db9::1"); + assertFalse(info.isInRange(addrOutside)); + + // Test with null + assertFalse(info.isInRange((Inet6Address) null)); + } + + @Test + public void testLinkLocalAddress() { + final SubnetUtils6 utils = new SubnetUtils6("fe80::/10"); + final SubnetInfo info = utils.getInfo(); + + assertTrue(info.isInRange("fe80::1")); + assertTrue(info.isInRange("fe80::1:2:3:4")); + assertFalse(info.isInRange("::1")); // Loopback is not link-local + } + + @Test + public void testLoopbackAddress() { + final SubnetUtils6 utils = new SubnetUtils6("::1/128"); + final SubnetInfo info = utils.getInfo(); + + assertEquals(128, info.getPrefixLength()); + assertTrue(info.isInRange("::1")); + assertFalse(info.isInRange("::2")); + } + + @Test + public void testToString() { + final SubnetUtils6 utils = new SubnetUtils6("2001:db8::1/64"); + final String str = utils.toString(); + + assertNotNull(str); + assertTrue(str.contains("CIDR Signature")); + assertTrue(str.contains("Network")); + assertTrue(str.contains("Address Count")); + } + + @Test + public void testUniqueLocalAddress() { + // ULA range is fc00::/7 + final SubnetUtils6 utils = new SubnetUtils6("fd00::/8"); + final SubnetInfo info = utils.getInfo(); + + assertTrue(info.isInRange("fd00::1")); + assertTrue(info.isInRange("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")); + assertFalse(info.isInRange("fc00::1")); // fc00::/8 is different from fd00::/8 + } + + @Test + public void testHighBitAddress() { + final SubnetUtils6 utils = new SubnetUtils6("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"); + final SubnetInfo info = utils.getInfo(); + + assertEquals("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", info.getAddress()); + assertEquals("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", info.getNetworkAddress()); + assertEquals("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", info.getHighAddress()); + assertEquals(BigInteger.ONE, info.getAddressCount()); + } + + @Test + public void testGetLowAddress() { + final SubnetUtils6 utils = new SubnetUtils6("2001:db8::100/120"); + final SubnetInfo info = utils.getInfo(); + + // getLowAddress returns the network address (same as getNetworkAddress) + assertEquals(info.getNetworkAddress(), info.getLowAddress()); + assertEquals("2001:db8:0:0:0:0:0:100", info.getLowAddress()); + } +}