1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.net.util;
18
19 import java.math.BigInteger;
20 import java.net.Inet6Address;
21 import java.net.InetAddress;
22 import java.net.UnknownHostException;
23
24 /**
25 * Performs subnet calculations given an IPv6 network address and a prefix length.
26 * <p>
27 * This is the IPv6 equivalent of {@link SubnetUtils}. Addresses are parsed and formatted
28 * using {@link InetAddress}, which accepts the text representations described in
29 * <a href="https://datatracker.ietf.org/doc/html/rfc5952">RFC 5952</a>.
30 * </p>
31 *
32 * @see SubnetUtils
33 * @see <a href="https://datatracker.ietf.org/doc/html/rfc5952">RFC 5952 - A Recommendation for IPv6 Address Text Representation</a>
34 * @since 3.13.0
35 */
36 public class SubnetUtils6 {
37
38 /**
39 * Contains IPv6 subnet summary information.
40 */
41 public final class SubnetInfo {
42
43 private SubnetInfo() { }
44
45 /**
46 * Gets the address used to initialize this subnet.
47 *
48 * @return the address as a string in standard IPv6 format.
49 */
50 public String getAddress() {
51 return format(address);
52 }
53
54 /**
55 * Gets the count of available addresses in this subnet.
56 * <p>
57 * For IPv6, this can be astronomically large. A /64 subnet has 2^64 addresses.
58 * </p>
59 *
60 * @return the count of addresses as a BigInteger.
61 */
62 public BigInteger getAddressCount() {
63 // 2^(128 - prefixLength)
64 return TWO.pow(NBITS - prefixLength);
65 }
66
67 /**
68 * Gets the CIDR notation for this subnet.
69 *
70 * @return the CIDR signature (e.g., "2001:db8::1/64").
71 */
72 public String getCidrSignature() {
73 return format(address) + "/" + prefixLength;
74 }
75
76 /**
77 * Gets the highest address in this subnet.
78 *
79 * @return the high address as a string in standard IPv6 format.
80 */
81 public String getHighAddress() {
82 return format(high);
83 }
84
85 /**
86 * Gets the lowest address in this subnet (the network address).
87 *
88 * @return the low address as a string in standard IPv6 format.
89 */
90 public String getLowAddress() {
91 return format(network);
92 }
93
94 /**
95 * Gets the network address for this subnet.
96 *
97 * @return the network address as a string in standard IPv6 format.
98 */
99 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 }