View Javadoc
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 }