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

Extend NetUtils for network range scanning #4375

Merged
merged 8 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
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());
}
}