diff options
Diffstat (limited to 'node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java')
-rw-r--r-- | node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java | 211 |
1 files changed, 91 insertions, 120 deletions
diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java index ca4b2c99ef0..0927ffe3ce2 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/IP.java @@ -6,6 +6,7 @@ import com.google.common.primitives.UnsignedBytes; import com.yahoo.config.provision.CloudAccount; import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.HostName; +import com.yahoo.config.provision.NodeType; import com.yahoo.config.provision.Zone; import com.yahoo.vespa.hosted.provision.LockedNodeList; import com.yahoo.vespa.hosted.provision.Node; @@ -16,6 +17,7 @@ import com.yahoo.vespa.hosted.provision.persistence.NameResolver.RecordType; import java.net.InetAddress; import java.util.Collections; import java.util.Comparator; +import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -36,43 +38,6 @@ import static com.yahoo.config.provision.NodeType.proxyhost; */ public record IP() { - /** IP version. Can be compared with ==, !=, and equals(). */ - public static class Version { - public static final Version v4 = new Version(4); - public static final Version v6 = new Version(6); - - private final int version; - - public static Version fromIpAddress(String ipAddress) { - if (ipAddress.contains(":")) return v6; - if (ipAddress.contains(".")) return v4; - throw new IllegalArgumentException("Failed to deduce the IP version from the textual representation of the IP address: " + ipAddress); - } - - public static Version fromIsIpv6(boolean isIpv6) { return isIpv6 ? v6 : v4; } - - private Version(int version) { this.version = version; } - - public boolean is4() { return version == 4; } - public boolean is6() { return version == 6; } - - public RecordType toForwardRecordType() { return is4() ? RecordType.A : RecordType.AAAA; } - - @Override - public String toString() { return "IPv" + version; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Version version1 = (Version) o; - return version == version1.version; - } - - @Override - public int hashCode() { return Objects.hash(version); } - } - /** Comparator for sorting IP addresses by their natural order */ public static final Comparator<InetAddress> NATURAL_ORDER = (ip1, ip2) -> { byte[] address1 = ip1.getAddress(); @@ -191,28 +156,28 @@ public record IP() { } /** A list of IP addresses and their protocol */ - record IpAddresses(Set<String> addresses, Stack stack) { + record IpAddresses(Set<String> addresses, Protocol protocol) { - public IpAddresses(Set<String> addresses, Stack stack) { + public IpAddresses(Set<String> addresses, Protocol protocol) { this.addresses = Collections.unmodifiableSet(new LinkedHashSet<>(Objects.requireNonNull(addresses, "addresses must be non-null"))); - this.stack = Objects.requireNonNull(stack, "type must be non-null"); + this.protocol = Objects.requireNonNull(protocol, "type must be non-null"); } /** Create addresses of the given set */ private static IpAddresses of(Set<String> addresses) { long ipv6AddrCount = addresses.stream().filter(IP::isV6).count(); if (ipv6AddrCount == addresses.size()) { // IPv6-only - return new IpAddresses(addresses, Stack.ipv6); + return new IpAddresses(addresses, Protocol.ipv6); } long ipv4AddrCount = addresses.stream().filter(IP::isV4).count(); if (ipv4AddrCount == addresses.size()) { // IPv4-only - return new IpAddresses(addresses, Stack.ipv4); + return new IpAddresses(addresses, Protocol.ipv4); } // If we're dual-stacked, we must have an equal number of addresses of each protocol. if (ipv4AddrCount == ipv6AddrCount) { - return new IpAddresses(addresses, Stack.dual); + return new IpAddresses(addresses, Protocol.dualStack); } throw new IllegalArgumentException(String.format("Dual-stacked IP address list must have an " + @@ -221,21 +186,16 @@ public record IP() { ipv6AddrCount, ipv4AddrCount)); } - public enum Stack { + public enum Protocol { - dual("dual-stack", Version.v4, Version.v6), - ipv4("IPv4-only", Version.v4), - ipv6("IPv6-only", Version.v6); + dualStack("dual-stack"), + ipv4("IPv4-only"), + ipv6("IPv6-only"); private final String description; - private final Set<Version> versions; - Stack(String description, Version... versions) { - this.description = description; - this.versions = Set.of(versions); - } + Protocol(String description) { this.description = description; } - public boolean supports(Version version) { return versions.contains(version); } } } @@ -267,34 +227,34 @@ public record IP() { /** * Find a free allocation in this pool. Note that the allocation is not final until it is assigned to a node * - * @param context allocation context - * @param nodes a locked list of all nodes in the repository + * @param nodes a locked list of all nodes in the repository * @return an allocation from the pool, if any can be made */ - public Optional<Allocation> findAllocation(Allocation.Context context, LockedNodeList nodes) { + public Optional<Allocation> findAllocation(LockedNodeList nodes, NameResolver resolver, boolean hasPtr) { if (ipAddresses.addresses.isEmpty()) { // IP addresses have not yet been resolved and should be done later. return findUnusedHostnames(nodes).map(Allocation::ofHostname) .findFirst(); } - Set<String> unusedIps = findUnusedIpAddresses(nodes); - - if (context.allocateFromUnusedHostname()) - return findUnusedHostnames(nodes).findFirst().map(hostname -> Allocation.fromHostname(context, hostname, ipAddresses.stack, unusedIps)); + if (!hasPtr) { + // Without PTR records (reverse IP mapping): Ensure only forward resolving from hostnames. + return findUnusedHostnames(nodes).findFirst().map(hostname -> Allocation.fromHostname(hostname, resolver, ipAddresses.protocol)); + } - if (ipAddresses.stack == IpAddresses.Stack.ipv4) { - return unusedIps.stream() - .findFirst() - .map(addr -> Allocation.ofIpv4(addr, context.resolver())); + if (ipAddresses.protocol == IpAddresses.Protocol.ipv4) { + return findUnusedIpAddresses(nodes).stream() + .findFirst() + .map(addr -> Allocation.ofIpv4(addr, resolver)); } - var allocation = unusedIps.stream() - .filter(IP::isV6) - .findFirst() - .map(addr -> Allocation.ofIpv6(addr, context.resolver())); + var unusedAddresses = findUnusedIpAddresses(nodes); + var allocation = unusedAddresses.stream() + .filter(IP::isV6) + .findFirst() + .map(addr -> Allocation.ofIpv6(addr, resolver)); allocation.flatMap(Allocation::ipv4Address).ifPresent(ipv4Address -> { - if (!unusedIps.contains(ipv4Address)) { + if (!unusedAddresses.contains(ipv4Address)) { throw new IllegalArgumentException("Allocation resolved " + ipv4Address + " from hostname " + allocation.get().hostname + ", but that address is not owned by this node"); @@ -339,36 +299,6 @@ public record IP() { Objects.requireNonNull(ipv6Address, "ipv6Address must be non-null"); } - public static class Context { - private final CloudName cloudName; - private final boolean exclave; - private final NameResolver resolver; - - private Context(CloudName cloudName, boolean exclave, NameResolver resolver) { - this.cloudName = cloudName; - this.exclave = exclave; - this.resolver = resolver; - } - - public static Context from(CloudName cloudName, boolean exclave, NameResolver resolver) { - return new Context(cloudName, exclave, resolver); - } - - public NameResolver resolver() { return resolver; } - - public boolean allocateFromUnusedHostname() { return exclave; } - - public boolean hasIpNotInDns(Version version) { - if (exclave && cloudName == CloudName.GCP && version.is4()) { - // Exclave nodes in GCP have IPv4, because load balancers backends are required to be IPv4, - // but it's private (10.x). The hostname only resolves to the public IPv6 address. - return true; - } - - return false; - } - } - /** * Allocate an IPv6 address. * @@ -423,30 +353,22 @@ public record IP() { return new Allocation(hostname4, Optional.of(addresses.get(0)), Optional.empty()); } - private static Allocation fromHostname(Context context, HostName hostname, IpAddresses.Stack stack, Set<String> unusedIps) { - Optional<String> ipv4Address = resolveAndVerify(context, hostname, stack, IP.Version.v4, unusedIps); - Optional<String> ipv6Address = resolveAndVerify(context, hostname, stack, IP.Version.v6, unusedIps); - return new Allocation(hostname.value(), ipv4Address, ipv6Address); - } + private static Allocation fromHostname(HostName hostname, NameResolver resolver, IpAddresses.Protocol protocol) { + // Resolve both A and AAAA to verify they match the protocol and to avoid surprises later on. - private static Optional<String> resolveAndVerify(Context context, HostName hostname, IpAddresses.Stack stack, Version version, Set<String> unusedIps) { - if (context.hasIpNotInDns(version)) { - List<String> candidates = unusedIps.stream() - .filter(a -> IP.Version.fromIpAddress(a).equals(version)) - .toList(); - if (candidates.size() != 1) { - throw new IllegalStateException("Unable to find a unique child IP address of " + hostname + ": Found " + candidates); - } - return candidates.stream().findFirst(); - } + Optional<String> ipv4Address = resolveOptional(hostname.value(), resolver, RecordType.A); + if (protocol != IpAddresses.Protocol.ipv6 && ipv4Address.isEmpty()) + throw new IllegalArgumentException(protocol.description + " hostname " + hostname.value() + " did not resolve to an IPv4 address"); + if (protocol == IpAddresses.Protocol.ipv6 && ipv4Address.isPresent()) + throw new IllegalArgumentException(protocol.description + " hostname " + hostname.value() + " has an IPv4 address: " + ipv4Address.get()); - Optional<String> address = resolveOptional(hostname.value(), context.resolver(), version.toForwardRecordType()); - if (stack.supports(version) && address.isEmpty()) - throw new IllegalArgumentException(stack.description + " hostname " + hostname.value() + " did not resolve to an " + version + " address"); - if (!stack.supports(version) && address.isPresent()) - throw new IllegalArgumentException(stack.description + " hostname " + hostname.value() + " has an " + version + " address: " + address.get()); + Optional<String> ipv6Address = resolveOptional(hostname.value(), resolver, RecordType.AAAA); + if (protocol != IpAddresses.Protocol.ipv4 && ipv6Address.isEmpty()) + throw new IllegalArgumentException(protocol.description + " hostname " + hostname.value() + " did not resolve to an IPv6 address"); + if (protocol == IpAddresses.Protocol.ipv4 && ipv6Address.isPresent()) + throw new IllegalArgumentException(protocol.description + " hostname " + hostname.value() + " has an IPv6 address: " + ipv6Address.get()); - return address; + return new Allocation(hostname.value(), ipv4Address, ipv6Address); } private static Optional<String> resolveOptional(String hostname, NameResolver resolver, RecordType recordType) { @@ -479,6 +401,55 @@ public record IP() { } } + public enum DnsRecordType { FORWARD, PUBLIC_FORWARD, REVERSE } + + /** Returns the set of DNS record types for a host and its children and the given version (ipv6), host type, etc. */ + public static Set<DnsRecordType> dnsRecordTypesFor(boolean ipv6, NodeType hostType, CloudName cloudName, boolean exclave) { + if (cloudName == CloudName.AWS) + return exclave ? + EnumSet.of(DnsRecordType.FORWARD, DnsRecordType.PUBLIC_FORWARD) : + EnumSet.of(DnsRecordType.FORWARD, DnsRecordType.PUBLIC_FORWARD, DnsRecordType.REVERSE); + + if (cloudName == CloudName.GCP) { + if (exclave) { + return ipv6 ? + EnumSet.of(DnsRecordType.FORWARD, DnsRecordType.PUBLIC_FORWARD) : + EnumSet.noneOf(DnsRecordType.class); + } else { + return hostType == confighost && ipv6 ? + EnumSet.of(DnsRecordType.FORWARD, DnsRecordType.REVERSE, DnsRecordType.PUBLIC_FORWARD) : + EnumSet.of(DnsRecordType.FORWARD, DnsRecordType.REVERSE); + } + } + + throw new IllegalArgumentException("Does not manage DNS for cloud " + cloudName); + } + + /** Verify DNS configuration of given hostname and IP address */ + public static void verifyDns(String hostname, String ipAddress, NodeType nodeType, NameResolver resolver, + CloudAccount cloudAccount, Zone zone) { + boolean ipv6 = isV6(ipAddress); + Set<DnsRecordType> recordTypes = dnsRecordTypesFor(ipv6, nodeType, zone.cloud().name(), cloudAccount.isExclave(zone)); + + if (recordTypes.contains(DnsRecordType.FORWARD)) { + RecordType recordType = ipv6 ? RecordType.AAAA : RecordType.A; + Set<String> addresses = resolver.resolve(hostname, recordType); + if (!addresses.equals(Set.of(ipAddress))) + throw new IllegalArgumentException("Expected " + hostname + " to resolve to " + ipAddress + + ", but got " + addresses); + } + + if (recordTypes.contains(DnsRecordType.REVERSE)) { + Optional<String> reverseHostname = resolver.resolveHostname(ipAddress); + if (reverseHostname.isEmpty()) + throw new IllegalArgumentException(ipAddress + " did not resolve to a hostname"); + + if (!reverseHostname.get().equals(hostname)) + throw new IllegalArgumentException(ipAddress + " resolved to " + reverseHostname.get() + + ", which does not match expected hostname " + hostname); + } + } + /** Convert IP address to string. This uses :: for zero compression in IPv6 addresses. */ public static String asString(InetAddress inetAddress) { return InetAddresses.toAddrString(inetAddress); |