// Copyright 2016 The Domain Registry Authors. All Rights Reserved. // // Licensed 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 // // http://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 com.google.domain.registry.util; import static com.google.common.base.Preconditions.checkArgument; import com.google.common.collect.AbstractSequentialIterator; import com.google.common.net.InetAddresses; import java.io.Serializable; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Arrays; import java.util.Iterator; import java.util.logging.Logger; import javax.annotation.Nullable; /** * Class representing an RFC 1519 CIDR IP address block. * *

When creating a CidrAddressBlock from an IP string literal * without a specified CIDR netmask (i.e. no trailing "/16" or "/64") * or an InetAddress with an accompanying integer netmask, then the * maximum length netmask for the address famiy of the specified * address is used (i.e. 32 for IPv4, 128 for IPv6). I.e. "1.2.3.4" * is automatically treated as "1.2.3.4/32" and, similarly, * "2001:db8::1" is automatically treated as "2001:db8::1/128". * */ // TODO(b/21870796): Migrate to Guava version when this is open-sourced. public class CidrAddressBlock implements Iterable, Serializable { private static final Logger logger = Logger.getLogger(CidrAddressBlock.class.getName()); private final InetAddress ip; /** * The number of block or mask bits needed to create the address block * (starting from the most-significant side of the address). */ private final int netmask; /** * Attempts to parse the given String into a CIDR block. * *

If the string is an IP string literal without a specified * CIDR netmask (i.e. no trailing "/16" or "/64") then the maximum * length netmask for the address famiy of the specified address is * used (i.e. 32 for IPv4, 128 for IPv6). * *

The specified IP address portion must be properly truncated * (i.e. all the host bits must be zero) or the input is considered * malformed. For example, "1.2.3.0/24" is accepted but "1.2.3.4/24" * is not. Similarly, for IPv6, "2001:db8::/32" is accepted whereas * "2001:db8::1/32" is not. * *

If inputs might not be properly truncated but would be acceptable * to the application consider constructing a {@code CidrAddressBlock} * via {@code create()}. * * @param s a String of the form "217.68.0.0/16" or "2001:db8::/32". * * @throws IllegalArgumentException if s is malformed or does not * represent a valid CIDR block. */ public CidrAddressBlock(String s) { this(parseInetAddress(s), parseNetmask(s), false); } /** * Attempts to parse the given String and int into a CIDR block. * *

The specified IP address portion must be properly truncated * (i.e. all the host bits must be zero) or the input is considered * malformed. For example, "1.2.3.0/24" is accepted but "1.2.3.4/24" * is not. Similarly, for IPv6, "2001:db8::/32" is accepted whereas * "2001:db8::1/32" is not. * *

An IP address without a netmask will automatically have the * maximum applicable netmask for its address family. I.e. "1.2.3.4" * is automatically treated as "1.2.3.4/32", and "2001:db8::1" is * automatically treated as "2001:db8::1/128". * *

If inputs might not be properly truncated but would be acceptable * to the application consider constructing a {@code CidrAddressBlock} * via {@code create()}. * * @param ip a String of the form "217.68.0.0" or "2001:db8::". * @param netmask an int between 0 and 32 (for IPv4) or 128 (for IPv6). * This is the number of bits, starting from the big end of the IP, * that will be used for network bits (as opposed to host bits) * in this CIDR block. * * @throws IllegalArgumentException if the params are malformed or do not * represent a valid CIDR block. */ public CidrAddressBlock(String ip, int netmask) { this(InetAddresses.forString(ip), checkNotNegative(netmask), false); } public CidrAddressBlock(InetAddress ip) { this(ip, AUTO_NETMASK, false); } public CidrAddressBlock(InetAddress ip, int netmask) { this(ip, checkNotNegative(netmask), false); } /** * Attempts to construct a CIDR block from the IP address and netmask, * truncating the IP address as required. * *

The specified IP address portion need not be properly truncated * (i.e. all the host bits need not be zero); truncation will be silently * performed. For example, "1.2.3.4/24" is accepted and returns the * same {@code CidrAddressBlock} as "1.2.3.0/24". Similarly, for IPv6, * "2001:db8::1/32" is accepted and returns the same * {@code CidrAddressBlock} as "2001:db8::/32". * * @param ip {@link InetAddress}, possibly requiring truncation. * @param netmask an int between 0 and 32 (for IPv4) or 128 (for IPv6). * This is the number of bits, starting from the big end of the IP, * that will be used for network bits (as opposed to host bits) * when truncating the supplied {@link InetAddress}. * * @throws IllegalArgumentException if the params are malformed or do not * represent a valid CIDR block. * @throws NullPointerException if a parameter is null. */ public static CidrAddressBlock create(InetAddress ip, int netmask) { return new CidrAddressBlock(ip, checkNotNegative(netmask), true); } /** * Attempts to construct a CIDR block from the IP address and netmask * expressed as a String, truncating the IP address as required. * *

The specified IP address portion need not be properly truncated * (i.e. all the host bits need not be zero); truncation will be silently * performed. For example, "1.2.3.4/24" is accepted and returns the * same {@code CidrAddressBlock} as "1.2.3.0/24". Similarly, for IPv6, * "2001:db8::1/32" is accepted and returns the same * {@code CidrAddressBlock} as "2001:db8::/32". * * @param s {@code String} representing either a single IP address or * a CIDR netblock, possibly requiring truncation. * * @throws IllegalArgumentException if the params are malformed or do not * represent a valid CIDR block. * @throws NullPointerException if a parameter is null. */ public static CidrAddressBlock create(String s) { return new CidrAddressBlock(parseInetAddress(s), parseNetmask(s), true); } private static final int AUTO_NETMASK = -1; /** * The universal constructor. All public constructors should lead here. * * @param ip {@link InetAddress}, possibly requiring truncation. * @param netmask the number of prefix bits to include in the netmask. * This is between 0 and 32 (for IPv4) or 128 (for IPv6). * The special value {@code AUTO_NETMASK} indicates that the CIDR block * should cover exactly one IP address. * @param truncate controls the behavior when an address has extra trailing * bits. If true, these bits are silently truncated, otherwise this * triggers an exception. * * @throws IllegalArgumentException if netmask is out of range, or ip has * unexpected trailing bits. * @throws NullPointerException if a parameter is null. */ private CidrAddressBlock(InetAddress ip, int netmask, boolean truncate) { // A single IP address is always truncated, by definition. if (netmask == AUTO_NETMASK) { this.ip = ip; this.netmask = ip.getAddress().length * 8; return; } // Determine the truncated form of this CIDR block. InetAddress truncatedIp = applyNetmask(ip, netmask); // If we're not truncating silently, then check for trailing bits. if (!truncate && !truncatedIp.equals(ip)) { throw new IllegalArgumentException( "CIDR block: " + getCidrString(ip, netmask) + " is not properly truncated, should have been: " + getCidrString(truncatedIp, netmask)); } this.ip = truncatedIp; this.netmask = netmask; } private static String getCidrString(InetAddress ip, int netmask) { return ip.getHostAddress() + "/" + netmask; } /** * Attempts to parse an {@link InetAddress} prefix from the given String. * * @param s a String of the form "217.68.0.0/16" or "2001:db8::/32". * * @throws IllegalArgumentException if s does not begin with an IP address. */ private static InetAddress parseInetAddress(String s) { int slash = s.indexOf('/'); return InetAddresses.forString((slash < 0) ? s : s.substring(0, slash)); } /** * Attempts to parse a netmask from the given String. * *

If the string does not end with a "/xx" suffix, then return AUTO_NETMASK * and let the constructor handle it. Otherwise, we only verify that the * suffix is a well-formed nonnegative integer. * * @param s a String of the form "217.68.0.0/16" or "2001:db8::/32". * * @throws IllegalArgumentException if s is malformed or does not end with a * valid nonnegative integer. */ private static int parseNetmask(String s) { int slash = s.indexOf('/'); if (slash < 0) { return AUTO_NETMASK; } try { return checkNotNegative(Integer.parseInt(s.substring(slash + 1))); } catch (NumberFormatException nfe) { throw new IllegalArgumentException("Invalid netmask: " + s.substring(slash + 1)); } } private static int checkNotNegative(int netmask) { checkArgument(netmask >= 0, "CIDR netmask '%s' must not be negative.", netmask); return netmask; } private static InetAddress applyNetmask(InetAddress ip, int netmask) { byte[] bytes = ip.getAddress(); checkArgument( (netmask >= 0) && (netmask <= (bytes.length * 8)), "CIDR netmask '%s' is out of range: 0 <= netmask <= %s.", netmask, (bytes.length * 8)); // The byte in which the CIDR boundary falls. int cidrByte = (netmask == 0) ? 0 : ((netmask - 1) / 8); // The number of mask bits within this byte. int numBits = netmask - (cidrByte * 8); // The bitmask for this byte. int bitMask = (-1 << (8 - numBits)); // Truncate the byte in which the CIDR boundary falls. bytes[cidrByte] = (byte) (bytes[cidrByte] & bitMask); // All bytes following the cidrByte get zeroed. for (int i = cidrByte + 1; i < bytes.length; ++i) { bytes[i] = 0; } try { return InetAddress.getByAddress(bytes); } catch (UnknownHostException uhe) { throw new IllegalArgumentException( String.format("Error creating InetAddress from byte array '%s'.", Arrays.toString(bytes)), uhe); } } /** * @return the standard {@code String} representation of the IP portion * of this CIDR block (a.b.c.d, or a:b:c::d) * *

NOTE: This is not reliable for comparison operations. It is * more reliable to normalize strings into {@link InetAddress}s and * then compare. * *

Consider: *

*/ public String getIp() { return ip.getHostAddress(); } public InetAddress getInetAddress() { return ip; } /** * Returns the number of leading bits (prefix size) of the routing prefix. */ public int getNetmask() { return netmask; } /** * Returns {@code true} if the supplied {@link InetAddress} is within * this {@code CidrAddressBlock}, {@code false} otherwise. * *

This can be used to test if the argument falls within a well-known * network range, a la GoogleIp's isGoogleIp(), isChinaIp(), et alia. * * @param ipAddr {@link InetAddress} to evaluate. * @return {@code true} if {@code ipAddr} is logically within this block, * {@code false} otherwise. */ public boolean contains(@Nullable InetAddress ipAddr) { if (ipAddr == null) { return false; } // IPv4 CIDR netblocks can never contain IPv6 addresses, and vice versa. // Calling getClass() is safe because the Inet4Address and Inet6Address // classes are final. if (ipAddr.getClass() != ip.getClass()) { return false; } try { return ip.equals(applyNetmask(ipAddr, netmask)); } catch (IllegalArgumentException iae) { // Something has gone very wrong. This CidrAddressBlock should // not have been created with an invalid netmask and a valid // netmask should have been successfully applied to "ipAddr" as long // as it represents an address of the same family as "this.ip". logger.warning(iae.getMessage()); return false; } } /** * Returns {@code true} if the supplied {@code CidrAddressBlock} is within * this {@code CidrAddressBlock}, {@code false} otherwise. * *

This can be used to test if the argument falls within a well-known * network range, a la GoogleIp's isGoogleIp(), isChinaIp(), et alia. * * @param cidr {@code CidrAddressBlock} to evaluate. * @return {@code true} if {@code cidr} is logically within this block, * {@code false} otherwise. */ public boolean contains(@Nullable CidrAddressBlock cidr) { if (cidr == null) { return false; } if (cidr.netmask < netmask) { // No block can contain a network larger than it // (in CIDR larger blocks have smaller netmasks). return false; } return contains(cidr.getInetAddress()); } /** * Returns {@code true} if the supplied {@code String} is within * this {@code CidrAddressBlock}, {@code false} otherwise. * *

This can be used to test if the argument falls within a well-known * network range, a la GoogleIp's isGoogleIp(), isChinaIp(), et alia. * * @param s {@code String} to evaluate. * @return {@code true} if {@code s} is logically within this block, * {@code false} otherwise. */ public boolean contains(@Nullable String s) { if (s == null) { return false; } try { return contains(create(s)); } catch (IllegalArgumentException iae) { return false; } } /** * Returns the address that is contained in this {@code CidrAddressBlock} * with the most bits set. * *

This can be used to calculate the upper bound address of the address * range for this {@code CidrAddressBlock}. */ public InetAddress getAllOnesAddress() { byte[] bytes = ip.getAddress(); // The byte in which the CIDR boundary falls. int cidrByte = (netmask == 0) ? 0 : ((netmask - 1) / 8); // The number of mask bits within this byte. int numBits = netmask - (cidrByte * 8); // The bitmask for this byte. int bitMask = ~(-1 << (8 - numBits)); // Set all non-prefix bits where the CIDR boundary falls. bytes[cidrByte] = (byte) (bytes[cidrByte] | bitMask); // All bytes following the cidrByte get set to all ones. for (int i = cidrByte + 1; i < bytes.length; ++i) { bytes[i] = (byte) 0xff; } try { return InetAddress.getByAddress(bytes); } catch (UnknownHostException uhe) { throw new IllegalArgumentException( String.format("Error creating InetAddress from byte array '%s'.", Arrays.toString(bytes)), uhe); } } @Override public Iterator iterator() { return new AbstractSequentialIterator(ip) { @Override protected InetAddress computeNext(InetAddress previous) { if (InetAddresses.isMaximum(previous)) { return null; } InetAddress next = InetAddresses.increment(previous); return (contains(next)) ? next : null; } }; } @Override public int hashCode() { return InetAddresses.coerceToInteger(ip); } @Override public boolean equals(@Nullable Object o) { if (!(o instanceof CidrAddressBlock)) { return false; } CidrAddressBlock cidr = (CidrAddressBlock) o; return ip.equals(cidr.ip) && (netmask == cidr.netmask); } @Override public String toString() { return getCidrString(ip, netmask); } }