2 * Copyright (c) 2010-2020 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;
20 import java.util.stream.Collectors;
22 import org.apache.commons.lang.StringUtils;
23 import org.apache.commons.lang.SystemUtils;
24 import org.apache.commons.net.util.SubnetUtils;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.core.io.net.exec.ExecUtil;
28 import org.openhab.core.net.CidrAddress;
29 import org.openhab.core.net.NetUtil;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
34 * Network utility functions for pinging and for determining all interfaces and assigned IP addresses.
36 * @author David Graeff - Initial contribution
39 public class NetworkUtils {
40 private final Logger logger = LoggerFactory.getLogger(NetworkUtils.class);
42 private LatencyParser latencyParser = new LatencyParser();
45 * Gets every IPv4 Address on each Interface except the loopback
46 * The Address format is ip/subnet
48 * @return The collected IPv4 Addresses
50 public Set<CidrAddress> getInterfaceIPs() {
51 return NetUtil.getAllInterfaceAddresses().stream().filter(a -> a.getAddress() instanceof Inet4Address)
52 .collect(Collectors.toSet());
56 * Get a set of all interface names.
58 * @return Set of interface names
60 public Set<String> getInterfaceNames() {
61 Set<String> result = new HashSet<>();
64 // For each interface ...
65 for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
66 NetworkInterface networkInterface = en.nextElement();
67 if (!networkInterface.isLoopback()) {
68 result.add(networkInterface.getName());
71 } catch (SocketException ignored) {
72 // If we are not allowed to enumerate, we return an empty result set.
79 * Determines every IP which can be assigned on all available interfaces
81 * @param maximumPerInterface The maximum of IP addresses per interface or 0 to get all.
82 * @return Every single IP which can be assigned on the Networks the computer is connected to
84 public Set<String> getNetworkIPs(int maximumPerInterface) {
85 return getNetworkIPs(getInterfaceIPs(), maximumPerInterface);
89 * Takes the interfaceIPs and fetches every IP which can be assigned on their network
91 * @param interfaceIPs The IPs which are assigned to the Network Interfaces
92 * @param maximumPerInterface The maximum of IP addresses per interface or 0 to get all.
93 * @return Every single IP which can be assigned on the Networks the computer is connected to
95 public Set<String> getNetworkIPs(Set<CidrAddress> interfaceIPs, int maximumPerInterface) {
96 LinkedHashSet<String> networkIPs = new LinkedHashSet<>();
98 short minCidrPrefixLength = 8; // historic Class A network, addresses = 16777214
99 if (maximumPerInterface != 0) {
100 // calculate minimum CIDR prefix length from maximumPerInterface
101 // (equals leading unset bits (Integer has 32 bits)
102 minCidrPrefixLength = (short) Integer.numberOfLeadingZeros(maximumPerInterface);
103 if (Integer.bitCount(maximumPerInterface) == 1) {
104 // if only the highest is set, decrease prefix by 1 to cover all addresses
105 minCidrPrefixLength--;
108 logger.trace("set minCidrPrefixLength to {}, maximumPerInterface is {}", minCidrPrefixLength,
109 maximumPerInterface);
111 for (CidrAddress cidrNotation : interfaceIPs) {
112 if (cidrNotation.getPrefix() < minCidrPrefixLength) {
114 "CIDR prefix is smaller than /{} on interface with address {}, truncating to /{}, some addresses might be lost",
115 minCidrPrefixLength, cidrNotation, minCidrPrefixLength);
116 cidrNotation = new CidrAddress(cidrNotation.getAddress(), minCidrPrefixLength);
119 SubnetUtils utils = new SubnetUtils(cidrNotation.toString());
120 String[] addresses = utils.getInfo().getAllAddresses();
121 int len = addresses.length;
122 if (maximumPerInterface != 0 && maximumPerInterface < len) {
123 len = maximumPerInterface;
125 for (int i = 0; i < len; i++) {
126 networkIPs.add(addresses[i]);
134 * Try to establish a tcp connection to the given port. Returns false if a timeout occurred
135 * or the connection was denied.
137 * @param host The IP or hostname
138 * @param port The tcp port. Must be not 0.
139 * @param timeout Timeout in ms
140 * @return Ping result information. Optional is empty if ping command was not executed.
141 * @throws IOException
143 public Optional<PingResult> servicePing(String host, int port, int timeout) throws IOException {
144 double execStartTimeInMS = System.currentTimeMillis();
146 SocketAddress socketAddress = new InetSocketAddress(host, port);
147 try (Socket socket = new Socket()) {
148 socket.connect(socketAddress, timeout);
149 return Optional.of(new PingResult(true, System.currentTimeMillis() - execStartTimeInMS));
150 } catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
151 return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
156 * Return the working method for the native system ping. If no native ping
157 * works JavaPing is returned.
159 public IpPingMethodEnum determinePingMethod() {
160 IpPingMethodEnum method;
161 if (SystemUtils.IS_OS_WINDOWS) {
162 method = IpPingMethodEnum.WINDOWS_PING;
163 } else if (SystemUtils.IS_OS_MAC) {
164 method = IpPingMethodEnum.MAC_OS_PING;
165 } else if (SystemUtils.IS_OS_UNIX) {
166 method = IpPingMethodEnum.IPUTILS_LINUX_PING;
168 // We cannot estimate the command line for any other operating system and just return false
169 return IpPingMethodEnum.JAVA_PING;
173 Optional<PingResult> pingResult = nativePing(method, "127.0.0.1", 1000);
174 if (pingResult.isPresent() && pingResult.get().isSuccess()) {
177 } catch (IOException ignored) {
178 } catch (InterruptedException e) {
179 Thread.currentThread().interrupt(); // Reset interrupt flag
181 return IpPingMethodEnum.JAVA_PING;
185 * Return true if the external arp ping utility (arping) is available and executable on the given path.
187 public ArpPingUtilEnum determineNativeARPpingMethod(String arpToolPath) {
188 String result = ExecUtil.executeCommandLineAndWaitResponse(arpToolPath + " --help", 100);
189 if (StringUtils.isBlank(result)) {
190 return ArpPingUtilEnum.UNKNOWN_TOOL;
191 } else if (result.contains("Thomas Habets")) {
192 if (result.matches("(?s)(.*)w sec Specify a timeout(.*)")) {
193 return ArpPingUtilEnum.THOMAS_HABERT_ARPING;
195 return ArpPingUtilEnum.THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT;
197 } else if (result.contains("-w timeout")) {
198 return ArpPingUtilEnum.IPUTILS_ARPING;
199 } else if (result.contains("Usage: arp-ping.exe")) {
200 return ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS;
202 return ArpPingUtilEnum.UNKNOWN_TOOL;
205 public enum IpPingMethodEnum {
213 * Use the native ping utility of the operating system to detect device presence.
215 * @param hostname The DNS name, IPv4 or IPv6 address. Must not be null.
216 * @param timeoutInMS Timeout in milliseconds. Be aware that DNS resolution is not part of this timeout.
217 * @return Ping result information. Optional is empty if ping command was not executed.
218 * @throws IOException The ping command could probably not be found
220 public Optional<PingResult> nativePing(@Nullable IpPingMethodEnum method, String hostname, int timeoutInMS)
221 throws IOException, InterruptedException {
222 double execStartTimeInMS = System.currentTimeMillis();
225 if (method == null) {
226 return Optional.empty();
228 // Yes, all supported operating systems have their own ping utility with a different command line
230 case IPUTILS_LINUX_PING:
231 proc = new ProcessBuilder("ping", "-w", String.valueOf(timeoutInMS / 1000), "-c", "1", hostname)
235 proc = new ProcessBuilder("ping", "-t", String.valueOf(timeoutInMS / 1000), "-c", "1", hostname)
239 proc = new ProcessBuilder("ping", "-w", String.valueOf(timeoutInMS), "-n", "1", hostname).start();
243 // We cannot estimate the command line for any other operating system and just return false
244 return Optional.empty();
247 // The return code is 0 for a successful ping, 1 if device didn't
248 // respond, and 2 if there is another error like network interface
250 // Exception: return code is also 0 in Windows for all requests on the local subnet.
251 // see https://superuser.com/questions/403905/ping-from-windows-7-get-no-reply-but-sets-errorlevel-to-0
253 int result = proc.waitFor();
255 return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
258 try (BufferedReader r = new BufferedReader(new InputStreamReader(proc.getInputStream()))) {
259 String line = r.readLine();
261 throw new IOException("Received no output from ping process.");
264 // Because of the Windows issue, we need to check this. We assume that the ping was successful whenever
265 // this specific string is contained in the output
266 if (line.contains("TTL=") || line.contains("ttl=")) {
267 PingResult pingResult = new PingResult(true, System.currentTimeMillis() - execStartTimeInMS);
268 latencyParser.parseLatency(line).ifPresent(pingResult::setResponseTimeInMS);
269 return Optional.of(pingResult);
272 } while (line != null);
274 return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
278 public enum ArpPingUtilEnum {
281 THOMAS_HABERT_ARPING,
282 THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT,
283 ELI_FULKERSON_ARP_PING_FOR_WINDOWS
287 * Execute the arping tool to perform an ARP ping (only for IPv4 addresses).
288 * There exist two different arping utils with the same name unfortunatelly.
289 * * iputils arping which is sometimes preinstalled on fedora/ubuntu and the
290 * * https://github.com/ThomasHabets/arping which also works on Windows and MacOS.
292 * @param arpUtilPath The arping absolute path including filename. Example: "arping" or "/usr/bin/arping" or
293 * "C:\something\arping.exe" or "arp-ping.exe"
294 * @param interfaceName An interface name, on linux for example "wlp58s0", shown by ifconfig. Must not be null.
295 * @param ipV4address The ipV4 address. Must not be null.
296 * @param timeoutInMS A timeout in milliseconds
297 * @return Ping result information. Optional is empty if ping command was not executed.
298 * @throws IOException The ping command could probably not be found
300 public Optional<PingResult> nativeARPPing(@Nullable ArpPingUtilEnum arpingTool, @Nullable String arpUtilPath,
301 String interfaceName, String ipV4address, int timeoutInMS) throws IOException, InterruptedException {
302 double execStartTimeInMS = System.currentTimeMillis();
304 if (arpUtilPath == null || arpingTool == null || arpingTool == ArpPingUtilEnum.UNKNOWN_TOOL) {
305 return Optional.empty();
308 if (arpingTool == ArpPingUtilEnum.THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT) {
309 proc = new ProcessBuilder(arpUtilPath, "-c", "1", "-i", interfaceName, ipV4address).start();
310 } else if (arpingTool == ArpPingUtilEnum.THOMAS_HABERT_ARPING) {
311 proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS / 1000), "-C", "1", "-i",
312 interfaceName, ipV4address).start();
313 } else if (arpingTool == ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS) {
314 proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS), "-x", ipV4address).start();
316 proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS / 1000), "-c", "1", "-I",
317 interfaceName, ipV4address).start();
320 // The return code is 0 for a successful ping. 1 if device didn't respond and 2 if there is another error like
321 // network interface not ready.
322 return Optional.of(new PingResult(proc.waitFor() == 0, System.currentTimeMillis() - execStartTimeInMS));
326 * Execute a Java ping.
328 * @param timeoutInMS A timeout in milliseconds
329 * @param destinationAddress The address to check
330 * @return Ping result information. Optional is empty if ping command was not executed.
332 public Optional<PingResult> javaPing(int timeoutInMS, InetAddress destinationAddress) {
333 double execStartTimeInMS = System.currentTimeMillis();
336 if (destinationAddress.isReachable(timeoutInMS)) {
337 return Optional.of(new PingResult(true, System.currentTimeMillis() - execStartTimeInMS));
339 return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
341 } catch (IOException e) {
342 return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
347 * iOS devices are in a deep sleep mode, where they only listen to UDP traffic on port 5353 (Bonjour service
348 * discovery). A packet on port 5353 will wake up the network stack to respond to ARP pings at least.
350 * @throws IOException
352 public void wakeUpIOS(InetAddress address) throws IOException {
353 try (DatagramSocket s = new DatagramSocket()) {
354 byte[] buffer = new byte[0];
355 s.send(new DatagramPacket(buffer, buffer.length, address, 5353));
356 } catch (PortUnreachableException ignored) {
357 // We ignore the port unreachable error