Skip to content

Commit

Permalink
Add CIDR and IP lookup utilities
Browse files Browse the repository at this point in the history
Closes #494
Closes #495
  • Loading branch information
chrisrohr committed Jan 21, 2021
1 parent 53ce71f commit 8e1a75c
Show file tree
Hide file tree
Showing 4 changed files with 331 additions and 0 deletions.
126 changes: 126 additions & 0 deletions src/main/java/org/kiwiproject/net/KiwiCidrs.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package org.kiwiproject.net;

import com.google.common.net.InetAddresses;
import lombok.extern.slf4j.Slf4j;

import java.math.BigInteger;
import java.net.InetAddress;
import java.nio.ByteBuffer;

/**
* Small utility to analyze CIDR information. Supports both IPv4 and IPv6.
*
* This utility was copied and enhanced from https://github.com/edazdarevic/CIDRUtils which has not been updated since
* 2019 and seems to be unmaintained.
*/
@SuppressWarnings("UnstableApiUsage")
@Slf4j
public class KiwiCidrs {

private final InetAddress inetAddress;
private final int prefixLength;

private InetAddress startAddress;
private InetAddress endAddress;
private BigInteger startAddressBigInt;
private BigInteger endAddressBigInt;

/**
* Creates a new instance of KiwiCidrs parsing the given CIDR.
*
* @param cidr the CIDR to use for this instance.
*/
public KiwiCidrs(String cidr) {

/* split CIDR to address and prefix part */
if (cidr.contains("/")) {
var index = cidr.indexOf("/");
var addressPart = cidr.substring(0, index);
var networkPart = cidr.substring(index + 1);

inetAddress = InetAddresses.forString(addressPart);
prefixLength = Integer.parseInt(networkPart);

calculate();
} else {
throw new IllegalArgumentException("not a valid CIDR format!");
}
}

private void calculate() {

ByteBuffer maskBuffer;
int targetSize;
if (inetAddress.getAddress().length == 4) {
maskBuffer = ByteBuffer.allocate(4)
.putInt(-1);

targetSize = 4;
} else {
maskBuffer = ByteBuffer.allocate(16)
.putLong(-1L)
.putLong(-1L);
targetSize = 16;
}

var mask = (new BigInteger(1, maskBuffer.array())).not().shiftRight(prefixLength);

var buffer = ByteBuffer.wrap(inetAddress.getAddress());
var ipVal = new BigInteger(1, buffer.array());

var startIp = ipVal.and(mask);
var endIp = startIp.add(mask.not());

this.startAddress = targetSize == 4 ? InetAddresses.fromIPv4BigInteger(startIp) : InetAddresses.fromIPv6BigInteger(startIp);
this.startAddressBigInt = new BigInteger(1, this.startAddress.getAddress());

this.endAddress = targetSize == 4 ? InetAddresses.fromIPv4BigInteger(endIp) : InetAddresses.fromIPv6BigInteger(endIp);
this.endAddressBigInt = new BigInteger(1, this.endAddress.getAddress());

}

/**
* Returns the network address for the CIDR. For example: 192.168.100.15/24 will return 192.168.100.0.
*
* @return The network address for the CIDR
*/
public String getNetworkAddress() {
return this.startAddress.getHostAddress();
}

/**
* Returns the broadcast address for the CIDR. For example: 192.168.100.15/24 will return 192.168.100.255.
*
* @return The broadcast address for the CIDR
*/
public String getBroadcastAddress() {
return this.endAddress.getHostAddress();
}

/**
* Checks if a given IP address (as a string) is in the CIDR range.
*
* @param ipAddress the IP address to check
* @return true if the IP address is in range, false otherwise
*/
public boolean isInRange(String ipAddress) {
var address = InetAddresses.forString(ipAddress);
return isInRange(address);
}

/**
* Checks if a given IP address (as an {@link InetAddress}) is in the CIDR range.
*
* @param address the IP address to check
* @return true if the IP address is in range, false otherwise
*/
public boolean isInRange(InetAddress address) {
var target = new BigInteger(1, address.getAddress());

if (startAddressBigInt.compareTo(target) > 0){
return false; //start is higher than address -> is not in range
}

return endAddressBigInt.compareTo(target) >= 0; // end is higher or equal -> is in range
}
}
78 changes: 78 additions & 0 deletions src/main/java/org/kiwiproject/net/KiwiInternetAddresses.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.kiwiproject.net;

import static com.google.common.base.Strings.nullToEmpty;
import static java.util.stream.Collectors.toList;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.net.HostAndPort;
Expand All @@ -13,10 +14,17 @@
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;

import java.io.UncheckedIOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;

Expand Down Expand Up @@ -175,6 +183,62 @@ public static HostAndPort hostAndPortFrom(URL url) {
return HostAndPort.fromParts(url.getHost(), url.getPort());
}

/**
* Finds the first IP Address on the machine that matches one of the given subnet CIDRs. The {@link IpScheme} is used
* to filter the IP addresses by IPv4 or IPv6.
*
* @param subnetCidrs A list of CIDRs used to match against the machine's IP addresses.
* @param ipScheme Whether to filter by IPv4 or IPv6
* @return the first found matching IP address
* @throws IllegalStateException if a matching IP address can not be found.
*/
public static String findFirstMatchingAddress(List<String> subnetCidrs, IpScheme ipScheme) {
var ipAddresses = getEnumeratedNetworkAddresses(ipScheme);
return findFirstMatchingAddress(subnetCidrs, ipAddresses);
}

@VisibleForTesting
static List<String> getEnumeratedNetworkAddresses(IpScheme ipScheme) {
try {
var interfaces = NetworkInterface.getNetworkInterfaces();
return Collections.list(interfaces)
.stream()
.map(networkInterface -> getInterfaceIps(networkInterface, ipScheme))
.flatMap(List::stream)
.collect(toList());
} catch (SocketException e) {
throw new UncheckedIOException("Error getting enumeration of network interfaces.", e);
}
}

private static List<String> getInterfaceIps(NetworkInterface networkInterface, IpScheme ipScheme) {
var addresses = networkInterface.getInetAddresses();

return Collections.list(addresses)
.stream()
.filter(address -> ipScheme.getInetAddressClass().isAssignableFrom(address.getClass()))
.map(InetAddress::getHostAddress)
.collect(toList());
}

/**
* Finds the first IP Address from a given list of ip addresses that matches one of the given subnet CIDRs.
*
* @param subnetCidrs A list of CIDRs used to match against the machine's IP addresses.
* @param ipAddresses A list of IP addresses to search for a match.
* @return the first found matching IP address
* @throws IllegalStateException if a matching IP address can not be found.
*/
public static String findFirstMatchingAddress(List<String> subnetCidrs, List<String> ipAddresses) {
return subnetCidrs.stream()
.map(KiwiCidrs::new)
.map(cidr -> ipAddresses.stream().filter(cidr::isInRange).findFirst())
.flatMap(Optional::stream)
.findFirst()
.orElseThrow(() ->
new IllegalStateException("Unable to find IP address matching a valid subnet CIDR in: " + subnetCidrs));
}

/**
* Simple value class encapsulating a host name and IP address
*/
Expand Down Expand Up @@ -217,4 +281,18 @@ InetAddress getLocalHost() throws UnknownHostException {
return InetAddress.getLocalHost();
}
}

/**
* Enum that defines the IP scheme to use when looking up a machine's IP addresses.
*/
public enum IpScheme {
IPV6(Inet6Address.class), IPV4(Inet4Address.class);

@Getter
private final Class<? extends InetAddress> inetAddressClass;

IpScheme(Class<? extends InetAddress> inetAddressClass) {
this.inetAddressClass = inetAddressClass;
}
}
}
69 changes: 69 additions & 0 deletions src/test/java/org/kiwiproject/net/KiwiCidrsTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.kiwiproject.net;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

@DisplayName("KiwiCidrs")
class KiwiCidrsTest {

@Test
void shouldThrowIllegalArgumentExceptionIfNotValidCidr() {
assertThatThrownBy(() -> new KiwiCidrs("1.2.3.4"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("not a valid CIDR format!");
}

@ParameterizedTest
@CsvSource({
"192.168.200.5/32, 192.168.200.5, 192.168.200.5",
"192.168.200.5/24, 192.168.200.0, 192.168.200.255",
"192.168.200.5/16, 192.168.0.0, 192.168.255.255",
"192.168.200.5/8, 192.0.0.0, 192.255.255.255",
"192.168.200.5/0, 0.0.0.0, 255.255.255.255",
"0.0.0.0/0, 0.0.0.0, 255.255.255.255",
"::/0, 0:0:0:0:0:0:0:0, ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
"2000::/16, 2000:0:0:0:0:0:0:0, 2000:ffff:ffff:ffff:ffff:ffff:ffff:ffff"
})
void shouldParseCidr(String cidr, String networkAddress, String broadcastAddress) {
var cidrs = new KiwiCidrs(cidr);

assertThat(cidrs.getNetworkAddress()).isEqualTo(networkAddress);
assertThat(cidrs.getBroadcastAddress()).isEqualTo(broadcastAddress);
}

@ParameterizedTest
@CsvSource({
"192.168.200.5/32, 192.168.200.5",
"192.168.200.5/24, 192.168.200.25",
"192.168.200.5/16, 192.168.150.50",
"192.168.200.5/8, 192.100.150.5",
"192.168.200.5/0, 10.10.1.1",
"0.0.0.0/0, 10.10.1.1",
"::/0, 2001:db8:0000:0000:0000:0000:0000:003f",
"2000::/16, 2000:db8:0000:0000:0000:0000:0000:003f"
})
void shouldReturnTrueWhenAddressIsInRangeOfCidr(String cidr, String address) {
var cidrs = new KiwiCidrs(cidr);

assertThat(cidrs.isInRange(address)).isTrue();
}

@ParameterizedTest
@CsvSource({
"192.168.200.5/32, 192.168.200.10",
"192.168.200.5/24, 192.168.201.25",
"192.168.200.5/16, 192.200.150.50",
"192.168.200.5/8, 10.100.150.5",
"2000::/16, 2001:db8:0000:0000:0000:0000:0000:003f"
})
void shouldReturnFalseWhenAddressIsNotInRangeOfCidr(String cidr, String address) {
var cidrs = new KiwiCidrs(cidr);

assertThat(cidrs.isInRange(address)).isFalse();
}
}
58 changes: 58 additions & 0 deletions src/test/java/org/kiwiproject/net/KiwiInternetAddressesTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.kiwiproject.net;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
Expand All @@ -10,14 +11,17 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.kiwiproject.net.KiwiInternetAddresses.InetAddressFinder;
import org.kiwiproject.net.KiwiInternetAddresses.IpScheme;
import org.kiwiproject.net.KiwiInternetAddresses.SimpleHostInfo;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.List;
import java.util.function.Supplier;

@SuppressWarnings("UnstableApiUsage")
@DisplayName("KiwiInternetAddresses")
class KiwiInternetAddressesTest {

Expand Down Expand Up @@ -233,4 +237,58 @@ private InetAddress dummyInetAddress() {
throw new RuntimeException(e);
}
}

@Test
void shouldGetEnumeratedNetworkAddresses() {
var ipv4Addresses = KiwiInternetAddresses.getEnumeratedNetworkAddresses(IpScheme.IPV4);
assertThat(ipv4Addresses).isNotEmpty();

var ipv6Addresses = KiwiInternetAddresses.getEnumeratedNetworkAddresses(IpScheme.IPV6);
assertThat(ipv6Addresses).isNotEmpty();
}

@Test
void shouldThrowIllegalStateWhenIpAddressesNotInSubnetCidrs() {
var subnetCidrs = List.of("192.168.50.0/24", "192.168.100.0/24", "192.168.150.0/24");
var ipAddresses = List.of("192.168.200.5");

assertThatIllegalStateException()
.isThrownBy(() -> KiwiInternetAddresses.findFirstMatchingAddress(subnetCidrs, ipAddresses))
.withMessageStartingWith("Unable to find IP address matching a valid subnet CIDR in: ");
}

@Test
void shouldReturnFoundAddressThatMatchesAGivenCidrFromGivenListOfAddresses() {
var subnetCidrs = List.of("192.168.50.0/24", "192.168.100.0/24", "192.168.150.0/24");
var ipAddresses = List.of("192.168.100.5", "192.168.200.5", "192.168.10.5");

var address = KiwiInternetAddresses.findFirstMatchingAddress(subnetCidrs, ipAddresses);

assertThat(address).isEqualTo("192.168.100.5");
}

@Test
void shouldReturnFoundAddressThatMatchesAGivenIpv4CidrByLookingUpAddresses() {
var subnetCidrs = List.of("0.0.0.0/0");

var address = KiwiInternetAddresses.findFirstMatchingAddress(subnetCidrs, IpScheme.IPV4);

assertThat(address).isNotBlank();
}

@Test
void shouldReturnFoundAddressThatMatchesAGivenIpv6CidrByLookingUpAddresses() {
var subnetCidrs = List.of("::/0");

var address = KiwiInternetAddresses.findFirstMatchingAddress(subnetCidrs, IpScheme.IPV6);

assertThat(address).isNotBlank();
}

@Test
void shouldThrowIllegalStateWhenRequestedIPDoesNotMatchCidrScheme() {
assertThatIllegalStateException()
.isThrownBy(() -> KiwiInternetAddresses.findFirstMatchingAddress(List.of("127.0.0.1/8"), IpScheme.IPV6))
.withMessageStartingWith("Unable to find IP address matching a valid subnet CIDR in: ");
}
}

0 comments on commit 8e1a75c

Please sign in to comment.