Skip to content
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

Add CIDR and IP lookup utilities #496

Merged
merged 3 commits into from
Jan 21, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
*
chrisrohr marked this conversation as resolved.
Show resolved Hide resolved
* This utility was copied and enhanced from https://github.com/edazdarevic/CIDRUtils which has not been updated since
chrisrohr marked this conversation as resolved.
Show resolved Hide resolved
* 2019 and seems to be unmaintained.
*/
@SuppressWarnings("UnstableApiUsage")
@Slf4j
public class KiwiCidrs {
sleberknight marked this conversation as resolved.
Show resolved Hide resolved

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) {
chrisrohr marked this conversation as resolved.
Show resolved Hide resolved
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) {
chrisrohr marked this conversation as resolved.
Show resolved Hide resolved
chrisrohr marked this conversation as resolved.
Show resolved Hide resolved
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: ");
}
}