aboutsummaryrefslogtreecommitdiffstats
path: root/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPAddresses.java
blob: 965cd9942d67fad69db8fb4c64ed7ad8d8ec6231 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
// 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.
 * <p>
 * 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.
 * <p>
 * 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)).
 * <p>
 * For IPv6 we disregard any site-local addresses (these are normally not in DNS anyway).
 * <p>
 * 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<InetAddress> 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<String> 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<Inet6Address> getIPv6Address(String hostname) {
        List<Inet6Address> 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<Inet4Address> getIPv4Address(String hostname) {
        List<Inet4Address> 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<Inet4Address> 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);
        }
    }
}