SubnetUtils6.java
/*
* 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.
* <p>
* This is the IPv6 equivalent of {@link SubnetUtils}. Addresses are parsed and formatted
* using {@link InetAddress}, which accepts the text representations described in
* <a href="https://datatracker.ietf.org/doc/html/rfc5952">RFC 5952</a>.
* </p>
*
* @see SubnetUtils
* @see <a href="https://datatracker.ietf.org/doc/html/rfc5952">RFC 5952 - A Recommendation for IPv6 Address Text Representation</a>
* @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.
* <p>
* For IPv6, this can be astronomically large. A /64 subnet has 2^64 addresses.
* </p>
*
* @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 <a href="https://datatracker.ietf.org/doc/html/rfc5952">RFC 5952</a>
*/
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.
* <p>
* 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)}.
* </p>
*
* @return a multi-line debug string summarizing this subnet.
*/
@Override
public String toString() {
return getInfo().toString();
}
}