2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.network.internal.utils;
15 import java.io.BufferedReader;
16 import java.io.IOException;
17 import java.io.InputStreamReader;
18 import java.net.ConnectException;
19 import java.net.DatagramPacket;
20 import java.net.DatagramSocket;
21 import java.net.Inet4Address;
22 import java.net.InetAddress;
23 import java.net.InetSocketAddress;
24 import java.net.NetworkInterface;
25 import java.net.NoRouteToHostException;
26 import java.net.PortUnreachableException;
27 import java.net.Socket;
28 import java.net.SocketAddress;
29 import java.net.SocketException;
30 import java.net.SocketTimeoutException;
31 import java.net.UnknownHostException;
32 import java.time.Duration;
33 import java.util.ArrayList;
34 import java.util.Enumeration;
35 import java.util.HashSet;
36 import java.util.LinkedHashSet;
37 import java.util.List;
38 import java.util.Optional;
40 import java.util.stream.Collectors;
42 import org.eclipse.jdt.annotation.NonNullByDefault;
43 import org.eclipse.jdt.annotation.Nullable;
44 import org.openhab.core.io.net.exec.ExecUtil;
45 import org.openhab.core.net.CidrAddress;
46 import org.openhab.core.net.NetUtil;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
51 * Network utility functions for pinging and for determining all interfaces and assigned IP addresses.
53 * @author David Graeff - Initial contribution
56 public class NetworkUtils {
57 private final Logger logger = LoggerFactory.getLogger(NetworkUtils.class);
59 private LatencyParser latencyParser = new LatencyParser();
62 * Gets every IPv4 Address on each Interface except the loopback
63 * The Address format is ip/subnet
65 * @return The collected IPv4 Addresses
67 public Set<CidrAddress> getInterfaceIPs() {
68 return NetUtil.getAllInterfaceAddresses().stream().filter(a -> a.getAddress() instanceof Inet4Address)
69 .collect(Collectors.toSet());
73 * Gets every IPv4 address on the network defined by its cidr
75 * @return The collected IPv4 Addresses
77 private List<String> getIPAddresses(CidrAddress adr) {
78 List<String> result = new ArrayList<>();
79 byte[] octets = adr.getAddress().getAddress();
80 final int addressCount = (1 << (32 - adr.getPrefix())) - 2;
81 final int ipMask = 0xFFFFFFFF << (32 - adr.getPrefix());
82 octets[0] &= ipMask >> 24;
83 octets[1] &= ipMask >> 16;
84 octets[2] &= ipMask >> 8;
87 final CidrAddress baseIp = new CidrAddress(InetAddress.getByAddress(octets), (short) adr.getPrefix());
88 for (int i = 1; i <= addressCount; i++) {
89 int octet = i & ~ipMask;
90 byte[] segments = baseIp.getAddress().getAddress();
91 segments[2] += (octet >> 8);
93 result.add(InetAddress.getByAddress(segments).getHostAddress());
95 } catch (UnknownHostException e) {
96 logger.debug("Could not build net ip address.", e);
102 * Get a set of all interface names.
104 * @return Set of interface names
106 public Set<String> getInterfaceNames() {
107 Set<String> result = new HashSet<>();
110 // For each interface ...
111 for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
112 NetworkInterface networkInterface = en.nextElement();
113 if (!networkInterface.isLoopback()) {
114 result.add(networkInterface.getName());
117 } catch (SocketException ignored) {
118 // If we are not allowed to enumerate, we return an empty result set.
125 * Determines every IP which can be assigned on all available interfaces
127 * @param maximumPerInterface The maximum of IP addresses per interface or 0 to get all.
128 * @return Every single IP which can be assigned on the Networks the computer is connected to
130 public Set<String> getNetworkIPs(int maximumPerInterface) {
131 return getNetworkIPs(getInterfaceIPs(), maximumPerInterface);
135 * Takes the interfaceIPs and fetches every IP which can be assigned on their network
137 * @param interfaceIPs The IPs which are assigned to the Network Interfaces
138 * @param maximumPerInterface The maximum of IP addresses per interface or 0 to get all.
139 * @return Every single IP which can be assigned on the Networks the computer is connected to
141 private Set<String> getNetworkIPs(Set<CidrAddress> interfaceIPs, int maximumPerInterface) {
142 LinkedHashSet<String> networkIPs = new LinkedHashSet<>();
144 short minCidrPrefixLength = 8; // historic Class A network, addresses = 16777214
145 if (maximumPerInterface != 0) {
146 // calculate minimum CIDR prefix length from maximumPerInterface
147 // (equals leading unset bits (Integer has 32 bits)
148 minCidrPrefixLength = (short) Integer.numberOfLeadingZeros(maximumPerInterface);
149 if (Integer.bitCount(maximumPerInterface) == 1) {
150 // if only the highest is set, decrease prefix by 1 to cover all addresses
151 minCidrPrefixLength--;
154 logger.trace("set minCidrPrefixLength to {}, maximumPerInterface is {}", minCidrPrefixLength,
155 maximumPerInterface);
157 for (CidrAddress cidrNotation : interfaceIPs) {
158 if (cidrNotation.getPrefix() < minCidrPrefixLength) {
160 "CIDR prefix is smaller than /{} on interface with address {}, truncating to /{}, some addresses might be lost",
161 minCidrPrefixLength, cidrNotation, minCidrPrefixLength);
162 cidrNotation = new CidrAddress(cidrNotation.getAddress(), minCidrPrefixLength);
165 List<String> addresses = getIPAddresses(cidrNotation);
166 int len = addresses.size();
167 if (maximumPerInterface != 0 && maximumPerInterface < len) {
168 len = maximumPerInterface;
170 for (int i = 0; i < len; i++) {
171 networkIPs.add(addresses.get(i));
179 * Try to establish a tcp connection to the given port. Returns false if a timeout occurred
180 * or the connection was denied.
182 * @param host The IP or hostname
183 * @param port The tcp port. Must be not 0.
184 * @param timeout Timeout in ms
185 * @return Ping result information. Optional is empty if ping command was not executed.
186 * @throws IOException
188 public Optional<PingResult> servicePing(String host, int port, int timeout) throws IOException {
189 double execStartTimeInMS = System.currentTimeMillis();
191 SocketAddress socketAddress = new InetSocketAddress(host, port);
192 try (Socket socket = new Socket()) {
193 socket.connect(socketAddress, timeout);
194 return Optional.of(new PingResult(true, System.currentTimeMillis() - execStartTimeInMS));
195 } catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
196 return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
201 * Return the working method for the native system ping. If no native ping
202 * works JavaPing is returned.
204 public IpPingMethodEnum determinePingMethod() {
205 String os = System.getProperty("os.name");
206 IpPingMethodEnum method;
208 return IpPingMethodEnum.JAVA_PING;
210 os = os.toLowerCase();
211 if (os.indexOf("win") >= 0) {
212 method = IpPingMethodEnum.WINDOWS_PING;
213 } else if (os.indexOf("mac") >= 0) {
214 method = IpPingMethodEnum.MAC_OS_PING;
215 } else if (os.indexOf("nix") >= 0 || os.indexOf("nux") >= 0 || os.indexOf("aix") >= 0) {
216 method = IpPingMethodEnum.IPUTILS_LINUX_PING;
218 // We cannot estimate the command line for any other operating system and just return false
219 return IpPingMethodEnum.JAVA_PING;
224 Optional<PingResult> pingResult = nativePing(method, "127.0.0.1", 1000);
225 if (pingResult.isPresent() && pingResult.get().isSuccess()) {
228 } catch (IOException ignored) {
229 } catch (InterruptedException e) {
230 Thread.currentThread().interrupt(); // Reset interrupt flag
232 return IpPingMethodEnum.JAVA_PING;
236 * Return true if the external arp ping utility (arping) is available and executable on the given path.
238 public ArpPingUtilEnum determineNativeARPpingMethod(String arpToolPath) {
239 String result = ExecUtil.executeCommandLineAndWaitResponse(Duration.ofMillis(100), arpToolPath, "--help");
240 if (result == null || result.isBlank()) {
241 return ArpPingUtilEnum.DISABLED_UNKNOWN_TOOL;
242 } else if (result.contains("Thomas Habets")) {
243 if (result.matches("(?s)(.*)w sec Specify a timeout(.*)")) {
244 return ArpPingUtilEnum.THOMAS_HABERT_ARPING;
246 return ArpPingUtilEnum.THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT;
248 } else if (result.contains("-w timeout") || result.contains("-w <timeout>")) {
249 return ArpPingUtilEnum.IPUTILS_ARPING;
250 } else if (result.contains("Usage: arp-ping.exe")) {
251 return ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS;
253 return ArpPingUtilEnum.DISABLED_UNKNOWN_TOOL;
256 public enum IpPingMethodEnum {
264 * Use the native ping utility of the operating system to detect device presence.
266 * @param hostname The DNS name, IPv4 or IPv6 address. Must not be null.
267 * @param timeoutInMS Timeout in milliseconds. Be aware that DNS resolution is not part of this timeout.
268 * @return Ping result information. Optional is empty if ping command was not executed.
269 * @throws IOException The ping command could probably not be found
271 public Optional<PingResult> nativePing(@Nullable IpPingMethodEnum method, String hostname, int timeoutInMS)
272 throws IOException, InterruptedException {
273 double execStartTimeInMS = System.currentTimeMillis();
276 if (method == null) {
277 return Optional.empty();
279 // Yes, all supported operating systems have their own ping utility with a different command line
281 case IPUTILS_LINUX_PING:
282 proc = new ProcessBuilder("ping", "-w", String.valueOf(timeoutInMS / 1000), "-c", "1", hostname)
286 proc = new ProcessBuilder("ping", "-t", String.valueOf(timeoutInMS / 1000), "-c", "1", hostname)
290 proc = new ProcessBuilder("ping", "-w", String.valueOf(timeoutInMS), "-n", "1", hostname).start();
294 // We cannot estimate the command line for any other operating system and just return false
295 return Optional.empty();
298 // The return code is 0 for a successful ping, 1 if device didn't
299 // respond, and 2 if there is another error like network interface
301 // Exception: return code is also 0 in Windows for all requests on the local subnet.
302 // see https://superuser.com/questions/403905/ping-from-windows-7-get-no-reply-but-sets-errorlevel-to-0
304 int result = proc.waitFor();
306 return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
309 try (BufferedReader r = new BufferedReader(new InputStreamReader(proc.getInputStream()))) {
310 String line = r.readLine();
312 throw new IOException("Received no output from ping process.");
315 // Because of the Windows issue, we need to check this. We assume that the ping was successful whenever
316 // this specific string is contained in the output
317 if (line.contains("TTL=") || line.contains("ttl=")) {
318 PingResult pingResult = new PingResult(true, System.currentTimeMillis() - execStartTimeInMS);
319 latencyParser.parseLatency(line).ifPresent(pingResult::setResponseTimeInMS);
320 return Optional.of(pingResult);
323 } while (line != null);
325 return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
329 public enum ArpPingUtilEnum {
330 DISABLED("Disabled", false),
331 DISABLED_INVALID_IP("Destination is not a valid IPv4 address", false),
332 DISABLED_UNKNOWN_TOOL("Unknown arping tool", false),
333 IPUTILS_ARPING("Iputils Arping", true),
334 THOMAS_HABERT_ARPING("Arping tool by Thomas Habets", true),
335 THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT("Arping tool by Thomas Habets (old version)", true),
336 ELI_FULKERSON_ARP_PING_FOR_WINDOWS("Eli Fulkerson ARPing tool for Windows", true);
338 public final String description;
339 public final boolean canProceed;
341 ArpPingUtilEnum(String description, boolean canProceed) {
342 this.description = description;
343 this.canProceed = canProceed;
348 * Execute the arping tool to perform an ARP ping (only for IPv4 addresses).
349 * There exist two different arping utils with the same name unfortunatelly.
350 * * iputils arping which is sometimes preinstalled on fedora/ubuntu and the
351 * * https://github.com/ThomasHabets/arping which also works on Windows and MacOS.
353 * @param arpUtilPath The arping absolute path including filename. Example: "arping" or "/usr/bin/arping" or
354 * "C:\something\arping.exe" or "arp-ping.exe"
355 * @param interfaceName An interface name, on linux for example "wlp58s0", shown by ifconfig. Must not be null.
356 * @param ipV4address The ipV4 address. Must not be null.
357 * @param timeoutInMS A timeout in milliseconds
358 * @return Ping result information. Optional is empty if ping command was not executed.
359 * @throws IOException The ping command could probably not be found
361 public Optional<PingResult> nativeARPPing(@Nullable ArpPingUtilEnum arpingTool, @Nullable String arpUtilPath,
362 String interfaceName, String ipV4address, int timeoutInMS) throws IOException, InterruptedException {
363 double execStartTimeInMS = System.currentTimeMillis();
365 if (arpUtilPath == null || arpingTool == null || !arpingTool.canProceed) {
366 return Optional.empty();
369 if (arpingTool == ArpPingUtilEnum.THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT) {
370 proc = new ProcessBuilder(arpUtilPath, "-c", "1", "-i", interfaceName, ipV4address).start();
371 } else if (arpingTool == ArpPingUtilEnum.THOMAS_HABERT_ARPING) {
372 proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS / 1000), "-C", "1", "-i",
373 interfaceName, ipV4address).start();
374 } else if (arpingTool == ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS) {
375 proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS), "-x", ipV4address).start();
377 proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS / 1000), "-c", "1", "-I",
378 interfaceName, ipV4address).start();
381 // The return code is 0 for a successful ping. 1 if device didn't respond and 2 if there is another error like
382 // network interface not ready.
383 return Optional.of(new PingResult(proc.waitFor() == 0, System.currentTimeMillis() - execStartTimeInMS));
387 * Execute a Java ping.
389 * @param timeoutInMS A timeout in milliseconds
390 * @param destinationAddress The address to check
391 * @return Ping result information. Optional is empty if ping command was not executed.
393 public Optional<PingResult> javaPing(int timeoutInMS, InetAddress destinationAddress) {
394 double execStartTimeInMS = System.currentTimeMillis();
397 if (destinationAddress.isReachable(timeoutInMS)) {
398 return Optional.of(new PingResult(true, System.currentTimeMillis() - execStartTimeInMS));
400 return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
402 } catch (IOException e) {
403 return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
408 * iOS devices are in a deep sleep mode, where they only listen to UDP traffic on port 5353 (Bonjour service
409 * discovery). A packet on port 5353 will wake up the network stack to respond to ARP pings at least.
411 * @throws IOException
413 public void wakeUpIOS(InetAddress address) throws IOException {
414 try (DatagramSocket s = new DatagramSocket()) {
415 byte[] buffer = new byte[0];
416 s.send(new DatagramPacket(buffer, buffer.length, address, 5353));
417 } catch (PortUnreachableException ignored) {
418 // We ignore the port unreachable error