blob: 68aa14fad09a03bff2a614685d3f467aa8f2f1b2 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.core.network.utils;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.core.network.utils.IpAddressUtils.IpTypePreference;
import org.openide.util.RequestProcessor;
/**
* Methods for determining the local host's own address. The methods provide
* two benefits over the core JDK classes.:
* <p>
* <ul>
* <li><i>Caching</i>. Results from the methods are cached and can therefore
* be returned without blocking. An application should call
* {@link #warmUp()} during the application's startup phase
* so that results are readily available when needed.
* </li>
* <li><i>IP protocol preference</i>. All methods allow to state
* an explicit preference for IPv4 vs IPv6. (<i>without</i> relation
* to the JVM's overall protocol preference settings).
* </li>
* </ul>
*
* <p>
* The main method is
* {@link #getMostLikelyLocalInetAddress(IpAddressUtils.IpTypePreference) getMostLikelyLocalInetAddress()}.
* The other methods essentially exist to provide input to this method but are
* exposed nevertheless if anyone wants them.
*
* <p>
* These utility methods are in particular relevant in relation to
* the PAC helper method {@code myIpAddress()}. However, the class may indeed
* be used for any use case.
*
* <p>
* Note, that there is no single correct answer to the question about
* determining the local host's IP address in an environment with multiple
* network interfaces or multiple addresses on each network interface.
*
* @author lbruun
*/
public class LocalAddressUtils {
private static final Logger LOG = Logger.getLogger(LocalAddressUtils.class.getName());
private static final RequestProcessor RP = new RequestProcessor("LocalNetworkAddressFinder", 3);
private static final byte[] LOOPBACK_IPV4_RAW = new byte[]{0x7f,0x00,0x00,0x01};
private static final byte[] LOOPBACK_IPV6_RAW = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01};
private static InetAddress LOOPBACK_IPV4;
private static InetAddress LOOPBACK_IPV6;
static {
try {
LOOPBACK_IPV4 = InetAddress.getByAddress("local-ipv4-dummy", LOOPBACK_IPV4_RAW);
LOOPBACK_IPV6 = InetAddress.getByAddress("local-ipv6-dummy", LOOPBACK_IPV6_RAW);
} catch (UnknownHostException ex) {
}
}
private static final Object LOCK = new Object();
private static Future<InetAddress> fut1;
private static Future<InetAddress[]> fut2;
private static Future<List<InetAddress>> fut3;
private static final Callable<InetAddress> C1 = () -> InetAddress.getLocalHost();
private static final Callable<InetAddress[]> C2 = () -> {
try {
String hostname = HostnameUtils.getNetworkHostname();
return InetAddress.getAllByName(hostname);
} catch (NativeException ex) {
throw new UnknownHostException(ex.getMessage() + ", error code : " + ex.getErrorCode());
}
};
private static final Callable<List<InetAddress>> C3 = () -> getLocalNetworkInterfaceAddr();
static {
refreshNetworkInfo(false);
}
private LocalAddressUtils() {
}
/**
* Starts collecting network information in a background task. This method
* is ideal for calling once during application startup phase. Calling this
* method during startup means that later calls to methods in this class
* will perform very fast, because they will then return a cached result and
* thus will never block.
*/
public static void warmUp() {
// Does nothing. The refreshNetworkInfo task will be fired when the class
// is loaded by the classloaded.
}
/**
* Refreshes the cached network information. Normally there is no reason
* to call this method as a computer's network information is unlikely
* to change during the lifetime of the application, unless the computer
* connects to a different network than was the case when the information
* was first cached.
*
* @param await if true, waits for the refresh to complete
*/
public static void refreshNetworkInfo(boolean await) {
synchronized (LOCK) {
fut1 = RP.submit(C1);
fut2 = RP.submit(C2);
fut3 = RP.submit(C3);
if (await) {
try {
fut1.get();
fut2.get();
fut3.get();
} catch (InterruptedException | ExecutionException ex) {
}
}
}
}
/**
* Returns the address of the local host.
*
* <p>This method returns a cached result of calling
* {@link InetAddress#getLocalHost()} and is therefore likely not to
* block (unlike the underlying method) unless this is the first time
* this class is being referenced.
*
* <p>Note that {@code InetAddress#getLocalHost()} is known to return
* unpredictable results for hosts with multiple network adapters. The
* {@link #getMostLikelyLocalInetAddresses(IpAddressUtils.IpTypePreference) getMostLikelyLocalInetAddresses()}
* method is much more likely to return an acceptable result.
*
* @see #getMostLikelyLocalInetAddresses(IpAddressUtils.IpTypePreference)
* @return
* @throws UnknownHostException
*/
public static @NonNull InetAddress getLocalHost() throws UnknownHostException {
synchronized (LOCK) {
if (fut1 == null) {
refreshNetworkInfo(false);
}
try {
return fut1.get();
} catch (ExecutionException ex) {
if (ex.getCause() instanceof UnknownHostException) {
throw (UnknownHostException) ex.getCause();
}
throw new RuntimeException(ex.getCause());
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}
}
/**
* Returns the addresses of the local host.
*
* <p>This is achieved by retrieving the
* {@link org.netbeans.network.hname.HostnameUtils#getNetworkHostname() name-of-the-host}
* from the system, then resolving that name into a list of {@code InetAddress}es.
*
* <p>This method returns a cached result and is therefore likely not to
* block unless this is the first time this class is being referenced.
*
* @see org.netbeans.network.hname.HostnameUtils#getNetworkHostname()
* @see InetAddress#getAllByName(java.lang.String)
* @param ipTypePref filter
* @return
* @throws UnknownHostException if no IP address for the host name could be found
*/
public static @NonNull InetAddress[] getLocalHostAddresses(IpTypePreference ipTypePref) throws UnknownHostException {
synchronized (LOCK) {
if (fut2 == null) {
refreshNetworkInfo(false);
}
try {
return fut2.get();
} catch (ExecutionException ex) {
if (ex.getCause() instanceof UnknownHostException) {
throw (UnknownHostException) ex.getCause();
}
if (ex.getCause() instanceof SecurityException) {
throw (SecurityException) ex.getCause();
}
throw new RuntimeException(ex.getCause());
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}
}
/**
* Returns a prioritized list of local host addresses. The further up
* on this list, the more likely it is that the address is the host's IP
* address.
*
* <p>
* Prioritization is done on the following basis:
* <ul>
* <li>IPv4 addresses are prioritized higher than IPv6 addresses.</li>
* <li>Addresses belonging to a non-virtual interface are prioritized higher
* than addresses belonging to a virtual interface.</li>
* <li>Addresses belonging to an interface which supports multicast are
* prioritized higher than addresses belonging to an interface which doesn't
* support multicast.</li>
* <li>Addresses belonging to an interface which
* {@link #isSoftwareVirtualAdapter(java.net.NetworkInterface) look
* like a software virtual adapters} are prioritized lower than addresses
* belonging to interfaces which don't look software virtual adapters.</li>
* <li>Addresses with a broadcast address are prioritized higher than
* addresses with no broadcast address.</li>
* </ul>
*
* <p>
* The method returns a cached result and is therefore likely not to block,
* unless this is the first time this class is being referenced.
*
* @param ipTypePref filter
* @return prioritized list of addresses
*/
public static @NonNull List<InetAddress> getPrioritizedLocalHostAddresses(IpAddressUtils.IpTypePreference ipTypePref) {
synchronized (LOCK) {
if (fut3 == null) {
refreshNetworkInfo(false);
}
try {
return IpAddressUtilsFilter.filterInetAddresses(fut3.get(), ipTypePref);
} catch (ExecutionException ex) {
throw new RuntimeException(ex.getCause());
} catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
}
}
/**
* Returns the host's IP addresses. Or rather the IP addresses most likely
* to be the ones the host is known by. This method is much more likely
* to return a correct result than the JDKs {@link InetAddress#getLocalHost()},
* in particular on hosts with multiple network interfaces or hosts
* that are virtualized or operating in a PaaS environment.
*
* <p>
* The method uses the following prioritization for determining what
* to return, by continously moving to the next step if the previous
* step yielded an empty result:
* <ol>
* <li>
* The list from {@link #getLocalHostAddresses(IpAddressUtils.IpTypePreference) getLocalHostAddresses() }
* (List A) is compared to
* {@link #getPrioritizedLocalHostAddresses(IpAddressUtils.IpTypePreference) getPrioritizedLocalHostAddresses() }
* (List B),
* picking the ones from List B list which is also on List A.
* </li>
*
* <li>
* Use List B.
* </li>
*
* <li>
* Use List A.
* </li>
*
* <li>
* Use the result from {@link #getLocalHost()} if it matches the
* {@code ipTypePref} filter.
* </li>
*
* <li>
* Finally, if everything else fails, return the result of
* {@link #getLoopbackAddress(IpAddressUtils.IpTypePreference)
* getLoopbackAddress()}.
* </li>
* </ol>
*
* <p>
* The method uses the other methods in the class and is therefore
* likely not to block, unless this is the first time this class is being
* referenced.
*
* @see #getMostLikelyLocalInetAddress(IpAddressUtils.IpTypePreference)
* @param ipTypePref IP protocol filter
* @return IP addresses, never null
*/
public static @NonNull InetAddress[] getMostLikelyLocalInetAddresses(IpAddressUtils.IpTypePreference ipTypePref) {
List<InetAddress> filteredList = getPrioritizedLocalHostAddresses(ipTypePref);
try {
List<InetAddress> localHostAddresses = Arrays.asList(getLocalHostAddresses(ipTypePref));
if (localHostAddresses != null && !localHostAddresses.isEmpty()) {
List<InetAddress> tmpList = new ArrayList<>(5);
for (InetAddress addr : filteredList) {
if (localHostAddresses.contains(addr)) {
tmpList.add(addr);
}
}
// #1
if (!tmpList.isEmpty()) {
return tmpList.toArray(new InetAddress[tmpList.size()]);
}
// #2
return localHostAddresses.toArray(new InetAddress[localHostAddresses.size()]);
}
} catch (UnknownHostException ex) {
}
// #3
if (!filteredList.isEmpty()) {
return filteredList.toArray(new InetAddress[filteredList.size()]);
}
// #4
try {
InetAddress addr = IpAddressUtilsFilter.pickInetAddress(Collections.singletonList(getLocalHost()), ipTypePref);
if (addr != null) {
return new InetAddress[]{addr};
}
} catch (UnknownHostException ex) {
}
// #5 - last resort
return new InetAddress[]{getLoopbackAddress(ipTypePref)};
}
/**
* Returns the host's IP address. Same as
* {@link #getMostLikelyLocalInetAddresses(IpAddressUtils.IpTypePreference) getMostLikelyLocalInetAddresses()}
* but only returns a single IP address.
*
*
* @see #getMostLikelyLocalInetAddresses(IpAddressUtils.IpTypePreference)
* @param ipTypePref IP protocol filter
* @return IP address, never null
*/
public static @NonNull InetAddress getMostLikelyLocalInetAddress(IpAddressUtils.IpTypePreference ipTypePref) {
InetAddress[] ipAddresses = getMostLikelyLocalInetAddresses(ipTypePref);
// We're guaranteed the array will have length > 0 and never null.
return ipAddresses[0];
}
/**
* Returns the loopback address.
*
* <p>This method is similar to {@link InetAddress#getLoopbackAddress()}
* except that the preference for IPv4 vs IPv6 can be explicitly
* stated.
*
* <p>For IPv4 the returned address is always {@code 127.0.0.1} and for
* IPv6 it is {@code ::1}.
*
* @param ipTypePref IPv4 vs IP4v6 preference
* @return
*/
public static @NonNull InetAddress getLoopbackAddress(IpAddressUtils.IpTypePreference ipTypePref) {
switch(ipTypePref) {
case IPV4_ONLY:
case ANY_IPV4_PREF:
return LOOPBACK_IPV4;
case IPV6_ONLY:
case ANY_IPV6_PREF:
return LOOPBACK_IPV6;
default:
return LOOPBACK_IPV4;
}
}
private static @NonNull List<InetAddress> getLocalNetworkInterfaceAddr() {
final Map<InetAddress, Integer> mapWithScores = new HashMap<>();
// Looping through all network interfaces on the host.
// WARNING: On Windows this is quite slow. On my Intel Core i7 it takes
// approximately 1200 msecs and I only have 3 network interfaces defined.
// The reason is that Windows creates a lot of virtual/bogus network
// interfaces. (nope, you don't see them with 'ipconfig /all' command)
// On my Win10 host there are 46 entries returned from
// NetworkInterface.getNetworkInterfaces() !! (all but a few are really
// to be ignored). It is the actual call to NetworkInterface.getNetworkInterfaces()
// which consumes 95% of the total time of this method.
// For a GUI oriented system a call to NetworkInterface.getNetworkInterfaces()
// can make the application seem slow if it is done on a critical thread.
Enumeration<NetworkInterface> interfaces;
try {
interfaces = NetworkInterface.getNetworkInterfaces(); // expensive call
} catch (SocketException ex) {
LOG.log(Level.WARNING, "Cannot get host's network interfaces", ex);
return Collections.emptyList();
}
while (interfaces.hasMoreElements()) {
int ifScore = 0; // the score for the interface, higher is better
NetworkInterface netIf = interfaces.nextElement(); // inexpensive call
try {
if (!netIf.isUp() || netIf.isLoopback()) { // inexpensive call
continue; // discard
}
// Solaris note: When inside a non-global zone a network interface which
// is really virtual as seen from the global zone is seen as non-virtual
// from within the non-global zone. So, it is safe to give virtual
// interfaces a lower score.
if (netIf.isVirtual()) {
ifScore -= 1;
}
if (!netIf.supportsMulticast()) {
ifScore -= 1;
}
if (isSoftwareVirtualAdapter(netIf)) {
ifScore -= 1;
}
} catch (SocketException ex) {
// isUp() and isLoopback() may throw exception. Discard
// the interface if that's the case.
continue;
}
List<InterfaceAddress> interfaceAddresses = netIf.getInterfaceAddresses(); // inexpensive call
for(InterfaceAddress ifAddr : interfaceAddresses) {
int addrScore = 0; // the score for the address, higher is better
InetAddress address = ifAddr.getAddress();
if (ifAddr.getBroadcast() == null) {
addrScore -= 1;
}
if (address instanceof Inet6Address) {
addrScore -= 1;
}
mapWithScores.put(address, ifScore + addrScore);
}
}
List<InetAddress> list = new ArrayList<>(mapWithScores.keySet());
// Sort descending according to the scores
Collections.sort( list, new Comparator<InetAddress>(){
@Override
public int compare(InetAddress o1, InetAddress o2) {
return mapWithScores.get(o2).compareTo(mapWithScores.get(o1));
}
});
return list; // returns a prioritized list
}
/**
* Tries to guess if the network interface is a virtual adapter
* by one of the makers of virtualization solutions (e.g. VirtualBox,
* VMware, etc). This is by no means a bullet proof method which is
* why it errs on the side of caution.
*
* @param nif network interface
* @return
*/
public static boolean isSoftwareVirtualAdapter(NetworkInterface nif) {
try {
// VirtualBox uses a semi-random MAC address for their adapter
// where the first 3 bytes are always the same:
// Windows, Mac OS X, Linux : begins with 0A-00-27
// Solaris: begins with 10-00-27
// (above from VirtualBox source code)
//
byte[] macAddress = nif.getHardwareAddress();
if (macAddress != null && macAddress.length >= 3) {
if ((macAddress[0] == 0x0A || macAddress[0] == 0x08) &&
(macAddress[1] == 0x00) &&
(macAddress[2] == 0x27)) {
return true;
}
}
return false;
} catch (SocketException ex) {
return false;
}
}
}