001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * https://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.commons.net.util; 018 019import java.math.BigInteger; 020import java.net.Inet6Address; 021import java.net.InetAddress; 022import java.net.UnknownHostException; 023 024/** 025 * Performs subnet calculations given an IPv6 network address and a prefix length. 026 * <p> 027 * This is the IPv6 equivalent of {@link SubnetUtils}. Addresses are parsed and formatted 028 * using {@link InetAddress}, which accepts the text representations described in 029 * <a href="https://datatracker.ietf.org/doc/html/rfc5952">RFC 5952</a>. 030 * </p> 031 * 032 * @see SubnetUtils 033 * @see <a href="https://datatracker.ietf.org/doc/html/rfc5952">RFC 5952 - A Recommendation for IPv6 Address Text Representation</a> 034 * @since 3.13.0 035 */ 036public class SubnetUtils6 { 037 038 /** 039 * Contains IPv6 subnet summary information. 040 */ 041 public final class SubnetInfo { 042 043 private SubnetInfo() { } 044 045 /** 046 * Gets the address used to initialize this subnet. 047 * 048 * @return the address as a string in standard IPv6 format. 049 */ 050 public String getAddress() { 051 return format(address); 052 } 053 054 /** 055 * Gets the count of available addresses in this subnet. 056 * <p> 057 * For IPv6, this can be astronomically large. A /64 subnet has 2^64 addresses. 058 * </p> 059 * 060 * @return the count of addresses as a BigInteger. 061 */ 062 public BigInteger getAddressCount() { 063 // 2^(128 - prefixLength) 064 return TWO.pow(NBITS - prefixLength); 065 } 066 067 /** 068 * Gets the CIDR notation for this subnet. 069 * 070 * @return the CIDR signature (e.g., "2001:db8::1/64"). 071 */ 072 public String getCidrSignature() { 073 return format(address) + "/" + prefixLength; 074 } 075 076 /** 077 * Gets the highest address in this subnet. 078 * 079 * @return the high address as a string in standard IPv6 format. 080 */ 081 public String getHighAddress() { 082 return format(high); 083 } 084 085 /** 086 * Gets the lowest address in this subnet (the network address). 087 * 088 * @return the low address as a string in standard IPv6 format. 089 */ 090 public String getLowAddress() { 091 return format(network); 092 } 093 094 /** 095 * Gets the network address for this subnet. 096 * 097 * @return the network address as a string in standard IPv6 format. 098 */ 099 public String getNetworkAddress() { 100 return format(network); 101 } 102 103 /** 104 * Gets the prefix length for this subnet. 105 * 106 * @return the prefix length (0-128). 107 */ 108 public int getPrefixLength() { 109 return prefixLength; 110 } 111 112 /** 113 * Tests if the given address is within this subnet range. 114 * 115 * @param addr the IPv6 address to test (as a BigInteger). 116 * @return true if the address is in range. 117 */ 118 public boolean isInRange(final BigInteger addr) { 119 if (addr == null) { 120 return false; 121 } 122 return addr.compareTo(network) >= 0 && addr.compareTo(high) <= 0; 123 } 124 125 /** 126 * Tests if the given address is within this subnet range. 127 * 128 * @param addr the IPv6 address to test as a byte array (16 bytes). 129 * @return true if the address is in range. 130 */ 131 public boolean isInRange(final byte[] addr) { 132 if (addr == null || addr.length != 16) { 133 return false; 134 } 135 return isInRange(new BigInteger(1, addr)); 136 } 137 138 /** 139 * Tests if the given address is within this subnet range. 140 * 141 * @param addr the IPv6 address to test. 142 * @return true if the address is in range. 143 */ 144 public boolean isInRange(final Inet6Address addr) { 145 if (addr == null) { 146 return false; 147 } 148 return isInRange(addr.getAddress()); 149 } 150 151 /** 152 * Tests if the given address is within this subnet range. 153 * 154 * @param addr the IPv6 address to test as a string. 155 * @return true if the address is in range. 156 * @throws IllegalArgumentException if the address cannot be parsed. 157 */ 158 public boolean isInRange(final String addr) { 159 return isInRange(toBytes(addr)); 160 } 161 162 /** 163 * Returns a summary of this subnet for debugging. 164 * 165 * @return a multi-line debug string summarizing this subnet. 166 */ 167 @Override 168 public String toString() { 169 final StringBuilder buf = new StringBuilder(); 170 buf.append("CIDR Signature:\t[").append(getCidrSignature()).append("]\n") 171 .append(" Network: [").append(getNetworkAddress()).append("]\n") 172 .append(" First address: [").append(getLowAddress()).append("]\n") 173 .append(" Last address: [").append(getHighAddress()).append("]\n") 174 .append(" Address Count: [").append(getAddressCount()).append("]\n"); 175 return buf.toString(); 176 } 177 } 178 179 private static final int NBITS = 128; 180 private static final String PARSE_FAIL = "Could not parse [%s]"; 181 private static final BigInteger TWO = BigInteger.valueOf(2); 182 private static final BigInteger MAX_VALUE = TWO.pow(NBITS).subtract(BigInteger.ONE); 183 184 /** 185 * Formats a BigInteger as an IPv6 address string using {@link InetAddress#getHostAddress()}. 186 * 187 * @param addr the address as a BigInteger. 188 * @return the formatted IPv6 address string. 189 * @see <a href="https://datatracker.ietf.org/doc/html/rfc5952">RFC 5952</a> 190 */ 191 private static String format(final BigInteger addr) { 192 final byte[] bytes = toByteArray16(addr); 193 try { 194 return InetAddress.getByAddress(bytes).getHostAddress(); 195 } catch (final UnknownHostException e) { 196 // Should never happen with a valid 16-byte array 197 throw new IllegalStateException("Unexpected error formatting IPv6 address", e); 198 } 199 } 200 201 /** 202 * Converts a BigInteger to a 16-byte array, padding with leading zeros if necessary. 203 * 204 * @param value the BigInteger to convert. 205 * @return a 16-byte array. 206 */ 207 private static byte[] toByteArray16(final BigInteger value) { 208 final byte[] raw = value.toByteArray(); 209 if (raw.length == 16) { 210 return raw; 211 } 212 final byte[] result = new byte[16]; 213 if (raw.length > 16) { 214 // BigInteger may have a leading sign byte; skip it 215 System.arraycopy(raw, raw.length - 16, result, 0, 16); 216 } else { 217 // Pad with leading zeros 218 System.arraycopy(raw, 0, result, 16 - raw.length, raw.length); 219 } 220 return result; 221 } 222 223 /** 224 * Parses an IPv6 address string to a byte array. 225 * 226 * @param address the IPv6 address string. 227 * @return the 16-byte representation. 228 * @throws IllegalArgumentException if the address cannot be parsed. 229 */ 230 private static byte[] toBytes(final String address) { 231 try { 232 final InetAddress inetAddr = InetAddress.getByName(address); 233 if (inetAddr instanceof Inet6Address) { 234 return inetAddr.getAddress(); 235 } 236 throw new IllegalArgumentException(String.format(PARSE_FAIL, address) + " - not an IPv6 address"); 237 } catch (final UnknownHostException e) { 238 throw new IllegalArgumentException(String.format(PARSE_FAIL, address), e); 239 } 240 } 241 242 private final BigInteger address; 243 private final BigInteger high; 244 private final BigInteger network; 245 private final int prefixLength; 246 247 /** 248 * Constructs an instance from a CIDR-notation string, e.g., "2001:db8::1/64". 249 * 250 * @param cidrNotation a CIDR-notation string, e.g., "2001:db8::1/64". 251 * @throws IllegalArgumentException if the parameter is invalid. 252 */ 253 public SubnetUtils6(final String cidrNotation) { 254 if (cidrNotation == null) { 255 throw new IllegalArgumentException(String.format(PARSE_FAIL, "null") + " - null input"); 256 } 257 258 final int slashIndex = cidrNotation.indexOf('/'); 259 if (slashIndex < 0) { 260 throw new IllegalArgumentException(String.format(PARSE_FAIL, cidrNotation) + " - missing prefix length"); 261 } 262 263 final String addressPart = cidrNotation.substring(0, slashIndex); 264 final String prefixPart = cidrNotation.substring(slashIndex + 1); 265 266 // Parse and validate prefix length 267 try { 268 this.prefixLength = Integer.parseInt(prefixPart); 269 } catch (final NumberFormatException e) { 270 throw new IllegalArgumentException(String.format(PARSE_FAIL, cidrNotation) + " - invalid prefix length", e); 271 } 272 273 if (this.prefixLength < 0 || this.prefixLength > NBITS) { 274 throw new IllegalArgumentException(String.format(PARSE_FAIL, cidrNotation) + 275 " - prefix length must be between 0 and " + NBITS); 276 } 277 278 // Parse and validate IPv6 address 279 final byte[] addressBytes = toBytes(addressPart); 280 this.address = new BigInteger(1, addressBytes); 281 282 // Create netmask: prefixLength 1-bits followed by (128 - prefixLength) 0-bits 283 final BigInteger netmask; 284 if (this.prefixLength == 0) { 285 netmask = BigInteger.ZERO; 286 } else { 287 netmask = MAX_VALUE.shiftLeft(NBITS - this.prefixLength).and(MAX_VALUE); 288 } 289 290 // Calculate network address 291 this.network = this.address.and(netmask); 292 293 // Calculate the highest address in the range 294 final BigInteger hostmask = MAX_VALUE.xor(netmask); 295 this.high = this.network.or(hostmask); 296 } 297 298 /** 299 * Constructs an instance from an IPv6 address and prefix length. 300 * 301 * @param address an IPv6 address, e.g., "2001:db8::1". 302 * @param prefixLength the prefix length (0-128). 303 * @throws IllegalArgumentException if the parameters are invalid. 304 */ 305 public SubnetUtils6(final String address, final int prefixLength) { 306 this(address + "/" + prefixLength); 307 } 308 309 /** 310 * Gets a {@link SubnetInfo} instance that contains subnet-specific statistics. 311 * 312 * @return a new SubnetInfo instance. 313 */ 314 public SubnetInfo getInfo() { 315 return new SubnetInfo(); 316 } 317 318 /** 319 * Returns a summary of this subnet for debugging. 320 * <p> 321 * Delegates to {@link SubnetInfo#toString()}. This is a diagnostic format and is not suitable for parsing. 322 * Use {@link SubnetInfo#getCidrSignature()} to obtain a string that can be fed back into 323 * {@link #SubnetUtils6(String)}. 324 * </p> 325 * 326 * @return a multi-line debug string summarizing this subnet. 327 */ 328 @Override 329 public String toString() { 330 return getInfo().toString(); 331 } 332}