package org.dcache.util; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.Collections2; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterators; import com.google.common.collect.Lists; import com.google.common.collect.Ordering; import com.google.common.net.InetAddresses; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.DatagramSocket; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.NetworkInterface; import java.net.ProtocolFamily; import java.net.SocketException; import java.net.StandardProtocolFamily; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.UnknownHostException; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.TimeUnit; import static com.google.common.base.Predicates.and; import static com.google.common.base.Strings.nullToEmpty; import static com.google.common.collect.Iterables.filter; import static com.google.common.collect.Iterators.*; /** * Various network related utility functions. */ public abstract class NetworkUtils { private static final Logger logger = LoggerFactory.getLogger(NetworkUtils.class); public static final String LOCAL_HOST_ADDRESS_PROPERTY = "org.dcache.net.localaddresses"; private static String canonicalHostName; private static final int RANDOM_PORT = 23241; private static final List<InetAddress> FAKED_ADDRESSES; private static final Supplier<List<InetAddress>> LOCAL_ADDRESS_SUPPLIER = Suppliers.memoizeWithExpiration(new LocalAddressSupplier(), 5, TimeUnit.SECONDS); static { String value = nullToEmpty(System.getProperty(LOCAL_HOST_ADDRESS_PROPERTY)); ImmutableList.Builder<InetAddress> fakedAddresses = ImmutableList.builder(); for (String address: Splitter.on(',').omitEmptyStrings().trimResults().split(value)) { fakedAddresses.add(InetAddresses.forString(address)); } FAKED_ADDRESSES = fakedAddresses.build(); } public static synchronized String getCanonicalHostName() { if (canonicalHostName == null) { canonicalHostName = getPreferredHostName(); } return canonicalHostName; } /** * Returns the list of IP addresses of this host. * * @return * @throws SocketException */ public static Collection<InetAddress> getLocalAddresses() { if (!FAKED_ADDRESSES.isEmpty()) { return FAKED_ADDRESSES; } return Collections2.filter(LOCAL_ADDRESS_SUPPLIER.get(), isNotMulticast()); } /** * Like URI.toURL, but translates exceptions to URISyntaxException * with a descriptive error message. */ public static URL toURL(URI uri) throws URISyntaxException { try { return uri.toURL(); } catch (IllegalArgumentException | MalformedURLException e) { URISyntaxException exception = new URISyntaxException(uri.toString(), e.getMessage()); exception.initCause(e); throw exception; } } /** * Return a local address that is likely reachable from {@code expectedSource}. */ public static InetAddress getLocalAddress(InetAddress expectedSource) throws SocketException { InetAddress localAddress = getLocalAddress(expectedSource, getProtocolFamily(expectedSource)); if (localAddress == null) { if (!FAKED_ADDRESSES.isEmpty()) { localAddress = FAKED_ADDRESSES.get(0); } else { try (DatagramSocket socket = new DatagramSocket()) { socket.connect(expectedSource, RANDOM_PORT); localAddress = socket.getLocalAddress(); /* DatagramSocket#getLocalAddress reports errors by returning the * wildcard address. There are several cases in which it does this, * such as when the host it is unable to serve the protocol family, * has no route to the address, or in case of Max OS X and Windows XP * due to bugs (see http://goo.gl/ENXkD). * * We fall back to enumerating all local network addresses and choose * the one with the smallest scope not smaller than the scope of the * expected source. */ if (localAddress.isAnyLocalAddress()) { InetAddressScope minScope = InetAddressScope.of(expectedSource); try { return Ordering.natural().onResultOf(InetAddressScope.OF).min( filter(LOCAL_ADDRESS_SUPPLIER.get(), and(greaterThanOrEquals(minScope), isNotMulticast()))); } catch (NoSuchElementException e) { throw new SocketException("Unable to find address that faces " + expectedSource); } } } } } return localAddress; } /** * Like getLocalAddress(InetAddress), but returns an addresses from the given protocolFamily * that is likely reachable from {@code expectedSource}. Returns null if such an address * could not be determined. */ public static InetAddress getLocalAddress(InetAddress expectedSource, ProtocolFamily protocolFamily) throws SocketException { if (!FAKED_ADDRESSES.isEmpty()) { for (InetAddress address : FAKED_ADDRESSES) { if (getProtocolFamily(address) == protocolFamily) { return address; } } return null; } try (DatagramSocket socket = new DatagramSocket()) { socket.connect(expectedSource, RANDOM_PORT); InetAddress localAddress = socket.getLocalAddress(); /* DatagramSocket#getLocalAddress reports errors by returning the * wildcard address. There are several cases in which it does this, * such as when the host it is unable to serve the protocol family, * has no route to the address, or in case of Max OS X and Windows XP * due to bugs (see http://goo.gl/ENXkD). * * We fall back to enumerating all local network addresses and choose * the one with the smallest scope which matches the desired protocol * family and has a scope at least as big as the expected source. */ if (localAddress.isAnyLocalAddress()) { InetAddressScope minScope = InetAddressScope.of(expectedSource); try { return Ordering.natural().onResultOf(InetAddressScope.OF).min( filter(LOCAL_ADDRESS_SUPPLIER.get(), and(greaterThanOrEquals(minScope), hasProtocolFamily(protocolFamily), isNotMulticast()))); } catch (NoSuchElementException e) { return null; } } /* It is quite possible that the expected source has a different protocol * family than the one we are expected to serve. In that case we try to * find a matching address from the same network interface (which we know * faces the expected source). */ if (getProtocolFamily(localAddress) != protocolFamily) { InetAddressScope intendedScope = InetAddressScope.of(expectedSource); NetworkInterface byInetAddress = NetworkInterface.getByInetAddress(localAddress); try { return Ordering.natural().onResultOf(InetAddressScope.OF).min( Iterators.filter(forEnumeration(byInetAddress.getInetAddresses()), and(greaterThanOrEquals(intendedScope), hasProtocolFamily(protocolFamily), isNotMulticast()))); } catch (NoSuchElementException e) { return null; } } return localAddress; } } private static Predicate<InetAddress> isNotMulticast() { return new Predicate<InetAddress>() { @Override public boolean apply(InetAddress address) { return !address.isMulticastAddress(); } }; } private static Predicate<InetAddress> hasProtocolFamily(final ProtocolFamily protocolFamily) { return new Predicate<InetAddress>() { @Override public boolean apply(InetAddress address) { return getProtocolFamily(address) == protocolFamily; } }; } private static Predicate<InetAddress> greaterThanOrEquals(final InetAddressScope scope) { return new Predicate<InetAddress>() { @Override public boolean apply(InetAddress address) { return InetAddressScope.of(address).ordinal() >= scope.ordinal(); } }; } public static ProtocolFamily getProtocolFamily(InetAddress address) { if (address instanceof Inet4Address) { return StandardProtocolFamily.INET; } if (address instanceof Inet6Address) { return StandardProtocolFamily.INET6; } throw new IllegalArgumentException("Unknown protocol family: " + address); } public static String toString(InetAddress a) { String name = a.getHostName(); if (InetAddresses.isInetAddress(name)) { return InetAddresses.toAddrString(a); } else { return name + "/" + InetAddresses.toUriString(a); } } private static String getPreferredHostName() { List<InetAddress> addresses = Ordering.natural().onResultOf(InetAddressScope.OF).reverse().sortedCopy(getLocalAddresses()); if (addresses.isEmpty()) { return "localhost"; } /* For legibility, we prefer to see a traditional * DNS host name; but if there is no mapping, * use the first address. */ for (InetAddress a: addresses) { String hostName = stripScope(a.getCanonicalHostName()); if (!InetAddresses.isInetAddress(hostName)) { return hostName; } } return addresses.get(0).getCanonicalHostName(); } /* * Workaround for bug in Guava, which should not * return the scoping portion of the address. There * is a patch for this, but it has not yet been * applied to InetAddresses in our current library version. */ private static String stripScope(String hostName) { int i = hostName.indexOf('%'); if (i > 0) { return hostName.substring(0, i); } return hostName; } public static boolean isInetAddress(String hostname) { return InetAddresses.isInetAddress(stripScope(hostname)); } /** * Returns an InetAddress with the result of InetAddress#getCanonicalHostName * filled in as the hostname. Subsequent calls to InetAddress#getHostName * will return the canonical name without further lookups. */ public static InetAddress withCanonicalAddress(InetAddress address) { try { String name = address.getCanonicalHostName(); // Java uses an extension to IPv6 addressing // [draft-ietf-ipngwg-scoping-arch-04.txt] where a '%' is appended // to the String representation of an IPv6 link-local and // site-local address to disambiguate addresses that are potentially // not globally unique. // // For dCache, this makes no sense: the zone identifiers are local // to the door (e.g., "eth0", "eth1", etc). There is no guarantee // the client machine will share the same mapping; e.g., the link- // local address #1 accessible via eth0 on the door may be accessible // via eth1 on the client machine. // // Therefore we strip off any zone identifiers, if no canonical name // is provided. This makes a tacit assumption that any site-local // or link-local address is unique to clients that can connect over // those addresses. // // Note that, due to a bug in Guava[1], we can't detect when the // canonical is an IP address; however, as '%' is not a character // for a DNS entry, we can apply the work-around for all IPv6 // addresses. // // [1] https://code.google.com/p/guava-libraries/issues/detail?id=1557 // if (address instanceof Inet6Address) { name = stripScope(name); } return InetAddress.getByAddress(name, address.getAddress()); } catch (UnknownHostException e) { return address; } } /** * The scope of an address captures the extend of the validity of * an internet address. */ public enum InetAddressScope { LOOPBACK, LINK, SITE, GLOBAL; public static InetAddressScope of(InetAddress address) { if (address.isLoopbackAddress()) { return LOOPBACK; } if (address.isLinkLocalAddress()) { return LINK; } if (address.isSiteLocalAddress()) { return SITE; } return GLOBAL; } public static final Function<InetAddress,InetAddressScope> OF = new Function<InetAddress, InetAddressScope>() { @Override public InetAddressScope apply(InetAddress address) { return of(address); } }; } /** * A supplier that returns all internet addresses of network interfaces that are up. */ private static class LocalAddressSupplier implements Supplier<List<InetAddress>> { @Override public List<InetAddress> get() { try { return Lists.newArrayList( /* * Get IP addresses from all interfaces. As InetAddress objects returned by * etworkInterface contain interface names, deerialization of them will * trigger interface re-discovery. Re-create InetAddress objects with address * information only. */ transform( concat(transform(forEnumeration(NetworkInterface.getNetworkInterfaces()), new Function<NetworkInterface, Iterator<InetAddress>>() { @Override public Iterator<InetAddress> apply(NetworkInterface i) { try { if (i.isUp()) { return forEnumeration(i.getInetAddresses()); } } catch (SocketException ignored) { } return Collections.emptyIterator(); } })), new Function<InetAddress, InetAddress>() { @Override public InetAddress apply(InetAddress input) { try { return InetAddress.getByAddress(input.getAddress()); }catch(UnknownHostException e) { // must never happen throw new RuntimeException("Failed to create new instance of InetAddress", e); } } }) ); } catch (SocketException e) { logger.error("Failed to resolve local network addresses: {}", e.toString()); return Collections.emptyList(); } } } }