// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.task.util.network; import ai.vespa.net.CidrBlock; import com.google.common.net.InetAddresses; import com.yahoo.vespa.hosted.node.admin.nodeadmin.ConvergenceException; import java.io.UncheckedIOException; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; /** * IP addresses - IP utilities to retrieve and manipulate addresses for docker host and docker containers in a * multi-home environment. *

* The assumption is that DNS is the source of truth for which address are assigned to the host and which * that belongs to the containers. Only one address should be assigned to each. *

* The behavior with respect to site-local addresses are distinct for IPv4 and IPv6. For IPv4 we choose * the site-local address (assume the public is a NAT address not assigned to the host interface (the typical aws setup)). *

* For IPv6 we disregard any site-local addresses (these are normally not in DNS anyway). *

* This class also provides some utilities for prefix translation. * * @author smorgrav */ public interface IPAddresses { CidrBlock gcpInternalBlock = CidrBlock.fromString("2600:2d00::/32"); InetAddress[] getAddresses(String hostname); default Optional getAddress(String hostname, IPVersion ipVersion) { return ipVersion == IPVersion.IPv6 ? getIPv6Address(hostname).map(InetAddress.class::cast) : getIPv4Address(hostname).map(InetAddress.class::cast); } /** * Returns a list of string representation of the IP addresses (RFC 5952 compact format) */ default List getAddresses(String hostname, IPVersion ipVersion) { return Stream.of(getAddresses(hostname)) .filter(inetAddress -> isOfType(inetAddress, ipVersion)) .map(InetAddresses::toAddrString) .toList(); } /** * Get the IPv6 address for the host if any. * * @throws ConvergenceException if multiple addresses are found */ default Optional getIPv6Address(String hostname) { List ipv6addresses = Stream.of(getAddresses(hostname)) .filter(Inet6Address.class::isInstance) .filter(inetAddress -> !inetAddress.isLoopbackAddress()) .map(Inet6Address.class::cast) .filter(inetAddress -> !inetAddress.isLinkLocalAddress()) .filter(inetAddress -> !inetAddress.isSiteLocalAddress()) .filter(inet6Address -> !gcpInternalBlock.contains(inet6Address)) .toList(); if (ipv6addresses.size() <= 1) return ipv6addresses.stream().findFirst(); String addresses = ipv6addresses.stream().map(InetAddresses::toAddrString).collect(Collectors.joining(",")); throw ConvergenceException.ofError( String.format( "Multiple IPv6 addresses found: %s. Perhaps a missing DNS entry or multiple AAAA records in DNS?", addresses)); } /** Returns the hostname of given inetAddress */ default String getHostname(InetAddress inetAddress) { String hostname = inetAddress.getHostName(); if (hostname.equals(inetAddress.getHostAddress())) { throw new IllegalArgumentException("Could not find hostname for address " + inetAddress.getHostAddress()); } return hostname; } /** * Get the IPv4 address for the host if any. * * @throws ConvergenceException if multiple site-local addresses are found */ default Optional getIPv4Address(String hostname) { List ipv4Addresses = Stream.of(getAddresses(hostname)) .filter(Inet4Address.class::isInstance) .filter(inetAddress -> !inetAddress.isLoopbackAddress()) .map(Inet4Address.class::cast) .toList(); if (ipv4Addresses.size() <= 1) return ipv4Addresses.stream().findFirst(); List siteLocalIPv4Addresses = ipv4Addresses.stream() .filter(InetAddress::isSiteLocalAddress) .toList(); if (siteLocalIPv4Addresses.size() == 1) return Optional.of(siteLocalIPv4Addresses.get(0)); String addresses = ipv4Addresses.stream().map(InetAddresses::toAddrString).collect(Collectors.joining(",")); throw ConvergenceException.ofError( String.format( "Multiple IPv4 addresses found: %s. Perhaps a missing DNS entry or multiple A records in DNS?", addresses)); } static boolean isOfType(InetAddress address, IPVersion ipVersion) { if (ipVersion.equals(IPVersion.IPv4) && address instanceof Inet4Address) return true; if (ipVersion.equals(IPVersion.IPv6) && address instanceof Inet6Address) return true; return false; } /** * For NPTed networks we want to find the private address from a public. * * @param address The original address to translate * @param prefix The prefix address * @param subnetSizeInBytes in bits - e.g a /64 subnet equals 8 bytes * @return The translated address * @throws ConvergenceException if */ static InetAddress prefixTranslate(InetAddress address, InetAddress prefix, int subnetSizeInBytes) { return prefixTranslate(address.getAddress(), prefix.getAddress(), subnetSizeInBytes); } static InetAddress prefixTranslate(byte[] address, byte[] prefix, int nofBytes) { System.arraycopy(prefix, 0, address, 0, nofBytes); try { return InetAddress.getByAddress(address); } catch (UnknownHostException e) { throw new UncheckedIOException(e); } } }