    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}