-
Notifications
You must be signed in to change notification settings - Fork 203
[NET-741] Add subnet IPv6 handling #391
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+801
−0
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
062a334
Add SubnetUtils6
see-quick 2972763
also add example of IPv6 subnet
see-quick a1bc084
address review + add RFC
see-quick 7aec2bb
Fix Javadoc sicne tag and one grammar issue.
garydgregory e25db9a
Fix missing newline at end of SubnetUtils6Example.java
garydgregory File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
58 changes: 58 additions & 0 deletions
58
src/main/java/org/apache/commons/net/examples/cidr/SubnetUtils6Example.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /* | ||
| * 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.examples.cidr; | ||
|
|
||
| import java.nio.charset.Charset; | ||
| import java.util.Scanner; | ||
|
|
||
| import org.apache.commons.net.util.SubnetUtils6; | ||
| import org.apache.commons.net.util.SubnetUtils6.SubnetInfo; | ||
|
|
||
| /** | ||
| * Example class that shows how to use the {@link SubnetUtils6} class. | ||
| */ | ||
| public class SubnetUtils6Example { | ||
|
|
||
| public static void main(final String[] args) { | ||
| final String subnet = "2001:db8:85a3::8a2e:370:7334/64"; | ||
| final SubnetUtils6 utils = new SubnetUtils6(subnet); | ||
| final SubnetInfo info = utils.getInfo(); | ||
|
|
||
| System.out.printf("Subnet Information for %s:%n", subnet); | ||
| System.out.println("--------------------------------------"); | ||
| System.out.printf("IP Address:\t\t\t%s%n", info.getAddress()); | ||
| System.out.printf("Prefix Length:\t\t\t%d%n", info.getPrefixLength()); | ||
| System.out.printf("CIDR Representation:\t\t%s%n%n", info.getCidrSignature()); | ||
|
|
||
| System.out.printf("Network Address:\t\t%s%n", info.getNetworkAddress()); | ||
| System.out.printf("Low Address:\t\t\t%s%n", info.getLowAddress()); | ||
| System.out.printf("High Address:\t\t\t%s%n", info.getHighAddress()); | ||
|
|
||
| System.out.printf("Total addresses in subnet:\t%s%n%n", info.getAddressCount()); | ||
|
|
||
| final String prompt = "Enter an IPv6 address (e.g., 2001:db8:85a3::1):"; | ||
| System.out.println(prompt); | ||
| try (Scanner scanner = new Scanner(System.in, Charset.defaultCharset().name())) { | ||
| while (scanner.hasNextLine()) { | ||
| final String address = scanner.nextLine(); | ||
| System.out.println("The IP address [" + address + "] is " + (info.isInRange(address) ? "" : "not ") + "within the subnet [" + subnet + "]"); | ||
| System.out.println(prompt); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| } |
332 changes: 332 additions & 0 deletions
332
src/main/java/org/apache/commons/net/util/SubnetUtils6.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,332 @@ | ||
| /* | ||
| * 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(); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a Javadoc to describe the format. Is there an expectation that this string can be fed back in the constructor? Either way,
testToStringshould test that use case.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is purely for debugging purposes same as IPv4 util. I have added a Javadoc to also reference
getCidrSignatureas such method can be used for that use case (i.e., string that can be fed back in the constructor). Also, I have extendedtestToStringtest case.