Skip to content

Commit

Permalink
Extend NetUtils for network range scanning (#4375)
Browse files Browse the repository at this point in the history
Signed-off-by: Leo Siepel <[email protected]>
  • Loading branch information
lsiepel authored Oct 7, 2024
1 parent d012d36 commit d9e5df0
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand All @@ -34,6 +35,7 @@
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
Expand All @@ -55,6 +57,7 @@
* @author Stefan Triller - Converted to OSGi service with primary ipv4 conf
* @author Gary Tse - Network address change listener
* @author Tim Roberts - Added primary address change to network address change listener
* @author Leo Siepel - Added methods to improve support for network scanning
*/
@Component(configurationPid = "org.openhab.network", property = { "service.pid=org.openhab.network",
"service.config.description.uri=system:network", "service.config.label=Network Settings",
Expand Down Expand Up @@ -386,7 +389,7 @@ public static String networkPrefixLengthToNetmask(int prefixLength) {
/**
* Get the network address a specific ip address is in
*
* @param ipAddressString ipv4 address of the device (i.e. 192.168.5.1)
* @param ipAddressString IPv4 address of the device (i.e. 192.168.5.1)
* @param netMask netmask in bits (i.e. 24)
* @return network a device is in (i.e. 192.168.5.0)
* @throws IllegalArgumentException if parameters are wrong
Expand Down Expand Up @@ -422,7 +425,7 @@ public static String getIpv4NetAddress(String ipAddressString, short netMask) {
/**
* Get the network broadcast address of the subnet a specific ip address is in
*
* @param ipAddressString ipv4 address of the device (i.e. 192.168.5.1)
* @param ipAddressString IPv4 address of the device (i.e. 192.168.5.1)
* @param prefix network prefix in bits (i.e. 24)
* @return network broadcast address of the network the device is in (i.e. 192.168.5.255)
* @throws IllegalArgumentException if parameters are wrong
Expand Down Expand Up @@ -595,4 +598,92 @@ private boolean getConfigParameter(Map<String, Object> parameters, String parame
return defaultValue;
}
}

/**
* For all network interfaces (except loopback) all IPv4 addresses are returned.
* This list can for example, be used to scan the network for available devices.
*
* @return A full list of IP {@link InetAddress} (except network and broadcast)
*/
public static List<InetAddress> getFullRangeOfAddressesToScan() {
List<InetAddress> addressesToScan = List.of();
List<CidrAddress> ipV4InterfaceAddresses = NetUtil.getAllInterfaceAddresses().stream()
.filter(a -> a.getAddress() instanceof Inet4Address).collect(Collectors.toList());

for (CidrAddress i : ipV4InterfaceAddresses) {
addressesToScan.addAll(getAddressesRangeByCidrAddress(i, i.getPrefix()));
}
return addressesToScan;
}

/**
* For the given {@link CidrAddress} all IPv4 addresses are returned.
* This list can, for example, be used to scan the network for available devices.
*
* @param iFaceAddress The {@link CidrAddress} of the network interface
* @param maxAllowedPrefixLength Control the maximum allowed prefix length of the network (e.g. 24 for class C).
* iFaceAddress's with a larger prefix are ignored and return an empty result.
* @return A full list of IP {@link InetAddress} (except network and broadcast)
*/
public static List<InetAddress> getAddressesRangeByCidrAddress(CidrAddress iFaceAddress,
int maxAllowedPrefixLength) {
if (!(iFaceAddress.getAddress() instanceof Inet4Address) || iFaceAddress.getPrefix() < maxAllowedPrefixLength) {
return List.of();
}

List<byte[]> addresses = getAddressesInSubnet(iFaceAddress.getAddress().getAddress(), iFaceAddress.getPrefix());
if (addresses.size() > 2) {
addresses.remove(0); // remove network address
addresses.remove(addresses.size() - 1); // remove broadcast address
}

return addresses.stream().map(m -> {
try {
return InetAddress.getByAddress(m);
} catch (UnknownHostException e) {
return null;
}
}).filter(f -> f != null).sorted((a, b) -> {
byte[] aOct = a.getAddress();
byte[] bOct = b.getAddress();
int r = 0;
for (int i = 0; i < aOct.length && i < bOct.length; i++) {
r = Integer.compare(aOct[i] & 0xff, bOct[i] & 0xff);
if (r != 0) {
return r;
}
}
return r;
}).collect(Collectors.toList());
}

/**
* Calculate each IP address within a subnet
*
* @param address IPv4 address in byte array form (i.e. 127.0.0.1 = 01111111 00000000 00000000 00000001)
* @param maskLength Network mask length (i.e. the number after the forward-slash, '/', in CIDR notation)
* @return A list of all possible IP addresses in byte array form
*/
private static List<byte[]> getAddressesInSubnet(byte[] address, int maskLength) {
byte[] lowestAddress = address.clone();
for (int bit = maskLength; bit < 32; bit++) {
lowestAddress[bit / 8] &= ~(1 << (bit % 8));
}
int lowestAddressAsLong = ByteBuffer.wrap(lowestAddress).getInt(); // big-endian by default

byte[] highestAddress = address.clone();
for (int bit = maskLength; bit < 32; bit++) {
highestAddress[bit / 8] |= ~(1 << (bit % 8));
}
int highestAddressAsLong = ByteBuffer.wrap(highestAddress).getInt();

List<byte[]> addresses = new ArrayList<byte[]>();
for (int i = lowestAddressAsLong; i <= highestAddressAsLong; i++) {
ByteBuffer dbuf = ByteBuffer.allocate(4);
dbuf.putInt(i);
addresses.add(dbuf.array());
}

return addresses;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -116,4 +121,35 @@ public void testBroadcastAddress() {
assertThat(iae.getMessage(), is("IP 'SOME_TEXT' is not a valid IPv4 address"));
}
}

@Test
public void checkValidRangeCountAndSort() throws UnknownHostException {
InetAddress testableAddress = InetAddress.getByName("192.168.1.4");
List<InetAddress> addresses = NetUtil
.getAddressesRangeByCidrAddress(new CidrAddress(testableAddress, (short) 24), 24);

assertEquals(254, addresses.size());
assertEquals("192.168.1.1", addresses.get(0).getHostAddress());
assertEquals("192.168.1.254", addresses.get(253).getHostAddress());
}

@Test
public void checkValidLargeRangeCountAndSort() throws UnknownHostException {
InetAddress testableAddress = InetAddress.getByName("127.0.1.12");
List<InetAddress> addresses = NetUtil
.getAddressesRangeByCidrAddress(new CidrAddress(testableAddress, (short) 16), 16);

assertEquals(65534, addresses.size());
assertEquals("127.0.0.1", addresses.get(0).getHostAddress());
assertEquals("127.0.255.254", addresses.get(65533).getHostAddress());
}

@Test
public void checkNotAllowedPrefixLength() throws UnknownHostException {
InetAddress testableAddress = InetAddress.getByName("192.168.1.0");
List<InetAddress> addresses = NetUtil
.getAddressesRangeByCidrAddress(new CidrAddress(testableAddress, (short) 16), 24);

assertEquals(0, addresses.size());
}
}

0 comments on commit d9e5df0

Please sign in to comment.