diff options
30 files changed, 652 insertions, 459 deletions
diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/Docker.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/Docker.java index 36fc1446bea..6c9c456d858 100644 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/Docker.java +++ b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/Docker.java @@ -45,7 +45,7 @@ public interface Docker { Map<String, Object> getBlkioStats(); } - default boolean networkNPTed() { + default boolean networkNATed() { return false; } diff --git a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImpl.java b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImpl.java index 4beb6eea055..5be5f69f5bc 100644 --- a/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImpl.java +++ b/docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImpl.java @@ -114,7 +114,7 @@ public class DockerImpl implements Docker { } @Override - public boolean networkNPTed() { + public boolean networkNATed() { return config.networkNATed(); } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/AclSpec.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/AclSpec.java deleted file mode 100644 index e96f903d8a6..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/AclSpec.java +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin; - -import com.yahoo.vespa.hosted.dockerapi.ContainerName; - -import java.util.Objects; - -/** - * An ACL specification for a container. - * - * @author mpolden - */ -public class AclSpec { - - private final String hostname; - private final String ipAddress; - private final ContainerName trustedBy; - - public AclSpec(String hostname, String ipAddress, ContainerName trustedBy) { - this.hostname = hostname; - this.ipAddress = ipAddress; - this.trustedBy = trustedBy; - } - - public String hostname() { - return hostname; - } - - public String ipAddress() { - return ipAddress; - } - - public ContainerName trustedBy() { - return trustedBy; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - AclSpec that = (AclSpec) o; - return Objects.equals(hostname, that.hostname) && - Objects.equals(ipAddress, that.ipAddress) && - Objects.equals(trustedBy, that.trustedBy); - } - - @Override - public int hashCode() { - return Objects.hash(hostname, ipAddress, trustedBy); - } -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/DockerAdminComponent.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/DockerAdminComponent.java index 639f8989c1d..1ed032aa89d 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/DockerAdminComponent.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/DockerAdminComponent.java @@ -106,7 +106,9 @@ public class DockerAdminComponent implements AdminComponent { AclMaintainer aclMaintainer = new AclMaintainer( dockerOperations, configServerClients.nodeRepository(), - dockerHostHostName); + dockerHostHostName, + new IPAddressesImpl(), + environment.get()); Function<String, NodeAgent> nodeAgentFactory = (hostName) -> new NodeAgentImpl( hostName, diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeRepository.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeRepository.java index 6ae1ea5642f..77ab1ac2482 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeRepository.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeRepository.java @@ -2,13 +2,15 @@ package com.yahoo.vespa.hosted.node.admin.configserver.noderepository; import com.yahoo.config.provision.NodeType; -import com.yahoo.vespa.hosted.node.admin.AclSpec; import com.yahoo.vespa.hosted.node.admin.NodeSpec; +import com.yahoo.vespa.hosted.node.admin.maintenance.acl.Acl; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; import com.yahoo.vespa.hosted.provision.Node; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; /** * @author stiankri @@ -17,11 +19,11 @@ public interface NodeRepository { List<NodeSpec> getNodes(String baseHostName); - List<NodeSpec> getNodes(NodeType... nodeTypes); - Optional<NodeSpec> getNode(String hostName); - List<AclSpec> getNodesAcl(String hostName); + List<NodeSpec> getNodes(NodeType... nodeTypes); + + Map<String, Acl> getAcls(String hostname); void updateNodeAttributes(String hostName, NodeAttributes nodeAttributes); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/RealNodeRepository.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/RealNodeRepository.java index 6a9d61c6c84..a93ffee9473 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/RealNodeRepository.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/RealNodeRepository.java @@ -1,29 +1,32 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.configserver.noderepository; +import com.google.common.net.InetAddresses; import com.yahoo.config.provision.NodeType; -import com.yahoo.vespa.hosted.dockerapi.ContainerName; import com.yahoo.vespa.hosted.dockerapi.DockerImage; -import com.yahoo.vespa.hosted.node.admin.AclSpec; import com.yahoo.vespa.hosted.node.admin.NodeSpec; import com.yahoo.vespa.hosted.node.admin.configserver.ConfigServerApi; import com.yahoo.vespa.hosted.node.admin.configserver.HttpException; -import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings.GetAclResponse; -import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings.GetNodesResponse; -import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings.NodeMessageResponse; -import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings.UpdateNodeAttributesRequestBody; -import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings.UpdateNodeAttributesResponse; +import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.bindings.*; +import com.yahoo.vespa.hosted.node.admin.maintenance.acl.Acl; import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; import com.yahoo.vespa.hosted.provision.Node; import org.apache.commons.lang.StringUtils; + +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author stiankri, dybis @@ -42,14 +45,6 @@ public class RealNodeRepository implements NodeRepository { return getNodes(Optional.of(baseHostName), Collections.emptyList()); } - @Override - public List<NodeSpec> getNodes(NodeType... nodeTypes) { - if (nodeTypes.length == 0) - throw new IllegalArgumentException("Must specify at least 1 node type"); - - return getNodes(Optional.empty(), Arrays.asList(nodeTypes)); - } - private List<NodeSpec> getNodes(Optional<String> baseHostName, List<NodeType> nodeTypeList) { Optional<String> nodeTypes = Optional .of(nodeTypeList.stream().map(NodeType::name).collect(Collectors.joining(","))) @@ -65,16 +60,25 @@ public class RealNodeRepository implements NodeRepository { .collect(Collectors.toList()); } + + @Override + public List<NodeSpec> getNodes(NodeType... nodeTypes) { + if (nodeTypes.length == 0) + throw new IllegalArgumentException("Must specify at least 1 node type"); + + return getNodes(Optional.empty(), Arrays.asList(nodeTypes)); + } + @Override public Optional<NodeSpec> getNode(String hostName) { try { GetNodesResponse.Node nodeResponse = configServerApi.get("/nodes/v2/node/" + hostName, - GetNodesResponse.Node.class); + GetNodesResponse.Node.class); if (nodeResponse == null) { return Optional.empty(); } return Optional.of(createNodeRepositoryNode(nodeResponse)); - } catch (HttpException.NotFoundException|HttpException.ForbiddenException e) { + } catch (HttpException.NotFoundException | HttpException.ForbiddenException e) { // Return empty on 403 in addition to 404 as it likely means we're trying to access a node that // has been deleted. When a node is deleted, the parent-child relationship no longer exists and // authorization cannot be granted. @@ -82,18 +86,44 @@ public class RealNodeRepository implements NodeRepository { } } + /** + * Get all ACLs that belongs to a hostname. Usually this is a parent host and all + * ACLs for child nodes are returned. + */ @Override - public List<AclSpec> getNodesAcl(String hostName) { + public Map<String, Acl> getAcls(String hostName) { + Map<String, Acl> acls = new HashMap<>(); try { final String path = String.format("/nodes/v2/acl/%s?children=true", hostName); final GetAclResponse response = configServerApi.get(path, GetAclResponse.class); - return response.trustedNodes.stream() - .map(node -> new AclSpec( - node.hostname, node.ipAddress, ContainerName.fromHostname(node.trustedBy))) - .collect(Collectors.toList()); + + // Group ports by container hostname that trusts them + Map<String, List<GetAclResponse.Port>> trustedPorts = response.trustedPorts.stream() + .collect(Collectors.groupingBy(GetAclResponse.Port::getTrustedBy)); + + // Group nodes by container hostname that trusts them + Map<String, List<GetAclResponse.Node>> trustedNodes = response.trustedNodes.stream() + .collect(Collectors.groupingBy(GetAclResponse.Node::getTrustedBy)); + + // For each hostname create an ACL + Stream.of(trustedNodes.keySet(), trustedPorts.keySet()) + .flatMap(Set::stream) + .distinct() + .forEach(hostname -> acls.put(hostname, + new Acl( + trustedPorts.getOrDefault(hostname, new ArrayList<>()) + .stream().map(port -> port.port) + .collect(Collectors.toList()), + + trustedNodes.getOrDefault(hostname, new ArrayList<>()) + .stream().map(node -> InetAddresses.forString(node.ipAddress)) + .collect(Collectors.toList())))); + } catch (HttpException.NotFoundException e) { - return Collections.emptyList(); + NODE_ADMIN_LOGGER.warning("Failed to fetch ACLs for " + hostName + " No ACL will be applied"); } + + return acls; } private static NodeSpec createNodeRepositoryNode(GetNodesResponse.Node node) diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/GetAclResponse.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/GetAclResponse.java index 7000170ca4c..ea2f313f03a 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/GetAclResponse.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/GetAclResponse.java @@ -22,11 +22,16 @@ public class GetAclResponse { @JsonProperty("trustedNetworks") public final List<Network> trustedNetworks; + @JsonProperty("trustedPorts") + public final List<Port> trustedPorts; + @JsonCreator public GetAclResponse(@JsonProperty("trustedNodes") List<Node> trustedNodes, - @JsonProperty("trustedNetworks") List<Network> trustedNetworks) { + @JsonProperty("trustedNetworks") List<Network> trustedNetworks, + @JsonProperty("trustedPorts") List<Port> trustedPorts) { this.trustedNodes = trustedNodes == null ? Collections.emptyList() : trustedNodes; this.trustedNetworks = trustedNetworks == null ? Collections.emptyList() : trustedNetworks; + this.trustedPorts = trustedPorts == null ? Collections.emptyList() : trustedPorts; } @JsonIgnoreProperties(ignoreUnknown = true) @@ -48,6 +53,10 @@ public class GetAclResponse { this.ipAddress = ipAddress; this.trustedBy = trustedBy; } + + public String getTrustedBy() { + return trustedBy; + } } @JsonIgnoreProperties(ignoreUnknown = true) @@ -64,5 +73,29 @@ public class GetAclResponse { this.network = network; this.trustedBy = trustedBy; } + + public String getTrustedBy() { + return trustedBy; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Port { + + @JsonProperty("port") + public final Integer port; + + @JsonProperty("trustedBy") + public final String trustedBy; + + @JsonCreator + public Port(@JsonProperty("port") Integer port, @JsonProperty("trustedBy") String trustedBy) { + this.port = port; + this.trustedBy = trustedBy; + } + + public String getTrustedBy() { + return trustedBy; + } } } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java index c76bf1918fa..21e763c1361 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java @@ -27,7 +27,7 @@ public interface DockerOperations { ProcessResult executeCommandInContainerAsRoot(ContainerName containerName, Long timeoutSeconds, String... command); - void executeCommandInNetworkNamespace(ContainerName containerName, String... command); + ProcessResult executeCommandInNetworkNamespace(ContainerName containerName, String... command); void resumeNode(ContainerName containerName); diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java index 04d6e07a678..c7ebc5beb0c 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java @@ -94,7 +94,7 @@ public class DockerOperationsImpl implements DockerOperations { command.withVolume("/opt/yahoo/share/ssl/certs/", "/opt/yahoo/share/ssl/certs/"); } - if (!docker.networkNPTed()) { + if (!docker.networkNATed()) { command.withIpAddress(nodeInetAddress); command.withNetworkMode(DockerImpl.DOCKER_CUSTOM_MACVLAN_NETWORK_NAME); command.withVolume("/etc/hosts", "/etc/hosts"); // TODO This is probably not necessary - review later @@ -148,7 +148,7 @@ public class DockerOperationsImpl implements DockerOperations { boolean isIPv6 = nodeInetAddress instanceof Inet6Address; if (isIPv6) { - if (!docker.networkNPTed()) { + if (!docker.networkNATed()) { docker.connectContainerToNetwork(containerName, "bridge"); } @@ -213,7 +213,7 @@ public class DockerOperationsImpl implements DockerOperations { * IPv6 gateway in containers connected to more than one docker network */ private void setupContainerNetworkConnectivity(ContainerName containerName) throws IOException { - if (!docker.networkNPTed()) { + if (!docker.networkNATed()) { InetAddress hostDefaultGateway = DockerNetworkCreator.getDefaultGatewayLinux(true); executeCommandInNetworkNamespace(containerName, "route", "-A", "inet6", "add", "default", "gw", hostDefaultGateway.getHostAddress(), "dev", "eth1"); @@ -246,7 +246,7 @@ public class DockerOperationsImpl implements DockerOperations { } @Override - public void executeCommandInNetworkNamespace(ContainerName containerName, String... command) { + public ProcessResult executeCommandInNetworkNamespace(ContainerName containerName, String... command) { final PrefixLogger logger = PrefixLogger.getNodeAgentLogger(DockerOperationsImpl.class, containerName); final Integer containerPid = docker.getContainer(containerName) .filter(container -> container.state.isRunning()) @@ -270,11 +270,13 @@ public class DockerOperationsImpl implements DockerOperations { logger.error(msg); throw new RuntimeException(msg); } + return new ProcessResult(0, result.getSecond(), ""); } catch (IOException e) { logger.warning(String.format("IOException while executing %s in network namespace for %s (PID = %d)", Arrays.toString(wrappedCommand), containerName.asString(), containerPid), e); throw new RuntimeException(e); } + } @Override diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/Acl.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/Acl.java index 4be3a9f4ef7..0b9de9bc792 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/Acl.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/Acl.java @@ -3,69 +3,65 @@ package com.yahoo.vespa.hosted.node.admin.maintenance.acl; import com.google.common.collect.ImmutableList; import com.google.common.net.InetAddresses; -import com.yahoo.vespa.hosted.node.admin.AclSpec; -import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.Action; -import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.Chain; -import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.Command; -import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.FilterCommand; -import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.PolicyCommand; +import com.yahoo.vespa.hosted.node.admin.task.util.network.IPVersion; -import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.ArrayList; + +import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; /** - * This class represents an ACL for a specific container instance + * This class represents an ACL for a specific container instance. * * @author mpolden + * @author smorgrav */ public class Acl { - private final int containerPid; - private final List<AclSpec> aclSpecs; + private final List<InetAddress> trustedNodes; + private final List<Integer> trustedPorts; - public Acl(int containerPid, List<AclSpec> aclSpecs) { - this.containerPid = containerPid; - this.aclSpecs = ImmutableList.copyOf(aclSpecs); + /** + * @param trustedPorts Ports that hostname should trust + * @param trustedNodes Other hostnames that this hostname should trust + */ + public Acl(List<Integer> trustedPorts, List<InetAddress> trustedNodes) { + this.trustedNodes = trustedNodes != null ? ImmutableList.copyOf(trustedNodes) : Collections.emptyList(); + this.trustedPorts = trustedPorts != null ? ImmutableList.copyOf(trustedPorts) : Collections.emptyList(); } - public List<Command> toCommands() { - final ImmutableList.Builder<Command> commands = ImmutableList.builder(); - commands.add( - // Default policies. Packets that do not match any rules will be processed according to policy. - new PolicyCommand(Chain.INPUT, Action.DROP), - new PolicyCommand(Chain.FORWARD, Action.DROP), - new PolicyCommand(Chain.OUTPUT, Action.ACCEPT), + public String toRules(IPVersion ipVersion) { + String basics = String.join("\n" + // We reject with rules instead of using policies + , "-P INPUT ACCEPT" + , "-P FORWARD ACCEPT" + , "-P OUTPUT ACCEPT" // Allow packets belonging to established connections - new FilterCommand(Chain.INPUT, Action.ACCEPT) - .withOption("-m", "state") - .withOption("--state", "RELATED,ESTABLISHED"), - + , "-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT" // Allow any loopback traffic - new FilterCommand(Chain.INPUT, Action.ACCEPT) - .withOption("-i", "lo"), + , "-A INPUT -i lo -j ACCEPT" + // Allow ICMP packets. See http://shouldiblockicmp.com/ + , "-A INPUT -p " + ipVersion.icmpProtocol() + " -j ACCEPT"); - // Allow IPv6 ICMP packets. This is required for IPv6 routing (e.g. path MTU) to work correctly. - new FilterCommand(Chain.INPUT, Action.ACCEPT) - .withOption("-p", "ipv6-icmp")); + // Allow trusted ports if any + String commaSeparatedPorts = trustedPorts.stream().map(i -> Integer.toString(i)).collect(Collectors.joining(",")); + String ports = commaSeparatedPorts.isEmpty() ? "" : "-A INPUT -p tcp -m multiport --dports " + commaSeparatedPorts + " -j ACCEPT\n"; - // Allow traffic from trusted containers - aclSpecs.stream() - .map(AclSpec::ipAddress) - .filter(Acl::isIpv6) - .map(ipAddress -> new FilterCommand(Chain.INPUT, Action.ACCEPT) - .withOption("-s", String.format("%s/128", ipAddress))) - .forEach(commands::add); + // Allow traffic from trusted nodes + String nodes = trustedNodes.stream() + .filter(ipVersion::match) + .map(ipAddress -> "-A INPUT -s " + InetAddresses.toAddrString(ipAddress) + ipVersion.singleHostCidr() + " -j ACCEPT") + .collect(Collectors.joining("\n")); - // Reject all other packets. This means that packets that would otherwise be processed according to policy, are - // matched by the following rule. - // - // Ideally, we want to set the INPUT policy to REJECT and get rid of this rule, but unfortunately REJECT is not - // a valid policy action. - commands.add(new FilterCommand(Chain.INPUT, Action.REJECT)); + // We reject instead of dropping to give us an easier time to figure out potential network issues + String rejectEverythingElse = "-A INPUT -j REJECT --reject-with " + ipVersion.icmpPortUnreachable(); - return commands.build(); + return basics + "\n" + ports + nodes + "\n" + rejectEverythingElse; } @Override @@ -73,16 +69,12 @@ public class Acl { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Acl that = (Acl) o; - return containerPid == that.containerPid && - Objects.equals(aclSpecs, that.aclSpecs); + return Objects.equals(trustedPorts, that.trustedPorts) && + Objects.equals(trustedNodes, that.trustedNodes); } @Override public int hashCode() { - return Objects.hash(containerPid, aclSpecs); - } - - private static boolean isIpv6(String ipAddress) { - return InetAddresses.forString(ipAddress) instanceof Inet6Address; + return Objects.hash(trustedPorts, trustedNodes); } } diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainer.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainer.java index 533afae2a45..1e79d61e9b5 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainer.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainer.java @@ -1,99 +1,87 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.maintenance.acl; -import com.yahoo.collections.Pair; -import com.yahoo.vespa.hosted.dockerapi.ContainerName; -import com.yahoo.vespa.hosted.node.admin.AclSpec; +import com.google.common.net.InetAddresses; +import com.yahoo.vespa.hosted.dockerapi.Container; +import com.yahoo.vespa.hosted.node.admin.component.Environment; import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; -import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.Action; -import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.Chain; -import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.Command; -import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.FlushCommand; -import com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables.PolicyCommand; import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.task.util.network.IPAddresses; +import com.yahoo.vespa.hosted.node.admin.task.util.network.IPVersion; import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; -import java.util.HashMap; -import java.util.List; +import java.net.InetAddress; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; -import java.util.stream.Stream; /** - * The responsibility of this class is to configure ACLs for all running containers. The ACLs are fetched from the Node - * repository. Based on those ACLs, iptables commands are created and then executed in each of the containers network - * namespace. + * This class maintains the iptables (ipv4 and ipv6) for all running containers. + * The filter table is synced with ACLs fetched from the Node repository while the nat table + * is synched with the proper redirect rule. * <p> - * If an ACL cannot be configured (e.g. iptables process execution fails), a rollback is attempted by setting the - * default policy to ACCEPT which will allow any traffic. The configuration will be retried the next time the - * maintainer runs. + * If an ACL cannot be configured (e.g. iptables process execution fails) we attempted to flush the rules + * rendering the firewall open. * <p> - * The ACL maintainer does not handle IPv4 addresses and is thus only intended to configure ACLs for IPv6-only - * containers (e.g. any container, except node-admin). + * This class currently assumes control over the filter and nat table. + * <p> + * The configuration will be retried the next time the maintainer runs. * * @author mpolden + * @author smorgrav */ public class AclMaintainer implements Runnable { private static final PrefixLogger log = PrefixLogger.getNodeAdminLogger(AclMaintainer.class); - private static final String IPTABLES_COMMAND = "ip6tables"; private final DockerOperations dockerOperations; private final NodeRepository nodeRepository; + private final IPAddresses ipAddresses; private final String nodeAdminHostname; - private final Map<ContainerName, Acl> containerAcls; + private final Environment environment; public AclMaintainer(DockerOperations dockerOperations, NodeRepository nodeRepository, - String nodeAdminHostname) { + String nodeAdminHostname, IPAddresses ipAddresses, Environment environment) { this.dockerOperations = dockerOperations; this.nodeRepository = nodeRepository; + this.ipAddresses = ipAddresses; this.nodeAdminHostname = nodeAdminHostname; - this.containerAcls = new HashMap<>(); + this.environment = environment; } - private boolean isAclActive(ContainerName containerName, Acl acl) { - return Optional.ofNullable(containerAcls.get(containerName)) - .map(acl::equals) - .orElse(false); + private void applyRedirect(Container container, InetAddress address) { + IPVersion ipVersion = IPVersion.get(address); + + String redirectStatements = String.join("\n" + , "-P PREROUTING ACCEPT" + , "-P INPUT ACCEPT" + , "-P OUTPUT ACCEPT" + , "-P POSTROUTING ACCEPT" + , "-A OUTPUT -d " + InetAddresses.toAddrString(address) + ipVersion.singleHostCidr() + " -j REDIRECT"); + + IPTablesRestore.syncTableLogOnError(dockerOperations, container.name, ipVersion, "nat", redirectStatements); } - private void applyAcl(ContainerName containerName, Acl acl) { - if (isAclActive(containerName, acl)) { - return; - } - final Command flush = new FlushCommand(Chain.INPUT); - final Command rollback = new PolicyCommand(Chain.INPUT, Action.ACCEPT); - try { - String commands = Stream.concat(Stream.of(flush), acl.toCommands().stream()) - .map(command -> command.asString(IPTABLES_COMMAND)) - .collect(Collectors.joining("; ")); + private void apply(Container container, Acl acl) { + // Apply acl to the filter table + IPTablesRestore.syncTableFlushOnError(dockerOperations, container.name, IPVersion.IPv6, "filter", acl.toRules(IPVersion.IPv6)); + IPTablesRestore.syncTableFlushOnError(dockerOperations, container.name, IPVersion.IPv4, "filter", acl.toRules(IPVersion.IPv4)); - log.debug("Running ACL command '" + commands + "' in " + containerName.asString()); - dockerOperations.executeCommandInNetworkNamespace(containerName, "/bin/sh", "-c", commands); - containerAcls.put(containerName, acl); - } catch (Exception e) { - log.error("Exception occurred while configuring ACLs for " + containerName.asString() + ", attempting rollback", e); - try { - dockerOperations.executeCommandInNetworkNamespace(containerName, rollback.asArray(IPTABLES_COMMAND)); - } catch (Exception ne) { - log.error("Rollback of ACLs for " + containerName.asString() + " failed, giving up", ne); - } + // Apply redirect to the nat table + if (this.environment.getCloud().equals("AWS")) { + ipAddresses.getAddress(container.hostname, IPVersion.IPv4).ifPresent(addr -> applyRedirect(container, addr)); + ipAddresses.getAddress(container.hostname, IPVersion.IPv6).ifPresent(addr -> applyRedirect(container, addr)); } } private synchronized void configureAcls() { - final Map<ContainerName, List<AclSpec>> nodeAclGroupedByContainerName = nodeRepository - .getNodesAcl(nodeAdminHostname).stream() - .collect(Collectors.groupingBy(AclSpec::trustedBy)); - - dockerOperations + Map<String, Container> runningContainers = dockerOperations .getAllManagedContainers().stream() .filter(container -> container.state.isRunning()) - .map(container -> new Pair<>(container, nodeAclGroupedByContainerName.get(container.name))) - .filter(pair -> pair.getSecond() != null) - .forEach(pair -> - applyAcl(pair.getFirst().name, new Acl(pair.getFirst().pid, pair.getSecond()))); + .collect(Collectors.toMap(container -> container.hostname, container -> container)); + + nodeRepository.getAcls(nodeAdminHostname).entrySet().stream() + .filter(entry -> runningContainers.containsKey(entry.getKey())) + .forEach(entry -> apply(runningContainers.get(entry.getKey()), entry.getValue())); } @Override diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/IPTablesRestore.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/IPTablesRestore.java new file mode 100644 index 00000000000..20bd50d0892 --- /dev/null +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/IPTablesRestore.java @@ -0,0 +1,82 @@ +package com.yahoo.vespa.hosted.node.admin.maintenance.acl; + +import com.yahoo.vespa.hosted.dockerapi.ContainerName; +import com.yahoo.vespa.hosted.dockerapi.ProcessResult; +import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; +import com.yahoo.vespa.hosted.node.admin.task.util.network.IPVersion; +import com.yahoo.vespa.hosted.node.admin.util.PrefixLogger; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +/** + * Utility class to sync rules for a given iptables table in a container. + * + * @author smorgrav + */ +public class IPTablesRestore { + + private static final PrefixLogger log = PrefixLogger.getNodeAdminLogger(AclMaintainer.class); + + public static void syncTableFlushOnError(DockerOperations dockerOperations, ContainerName containerName, IPVersion ipVersion, String table, String rules) { + syncTable(dockerOperations, containerName, ipVersion, table, rules, true); + } + + public static void syncTableLogOnError(DockerOperations dockerOperations, ContainerName containerName, IPVersion ipVersion, String table, String rules) { + syncTable(dockerOperations, containerName, ipVersion, table, rules, false); + } + + private static void syncTable(DockerOperations dockerOperations, ContainerName containerName, IPVersion ipVersion, String table, String rules, boolean flush) { + File file = null; + try { + // Get current rules for table + ProcessResult currentRulesResult = + dockerOperations.executeCommandInNetworkNamespace(containerName, ipVersion.iptablesCmd(), "-S", "-t", table); + String currentRules = currentRulesResult.getOutput(); + + // Compare and apply wanted if different + if (!equalsWhenIgnoreSpaceAndCase(rules, currentRules)) { + log.info(ipVersion.iptablesCmd() + " table: " + table + " differs. Wanted:\n" + rules + "\nGot\n" + currentRules); + file = writeTempFile(ipVersion.name(), "*" + table + "\n" + rules + "\nCOMMIT\n"); + dockerOperations.executeCommandInNetworkNamespace(containerName, ipVersion.iptablesRestore(), file.getAbsolutePath()); + } + } catch (Exception e) { + if (flush) { + log.error("Exception occurred while syncing iptable " + table + " for " + containerName.asString() + ", attempting rollback", e); + try { + dockerOperations.executeCommandInNetworkNamespace(containerName, ipVersion.iptablesCmd(), "-F", "-t", table); + } catch (Exception ne) { + log.error("Rollback of table " + table + " for " + containerName.asString() + " failed, giving up", ne); + } + } else { + log.warning("Unable to sync iptables for " + table, e); + } + } finally { + if (file != null) { + file.delete(); + } + } + } + + private static File writeTempFile(String postfix, String content) { + try { + Path path = Files.createTempFile("iptables-restore", "." + postfix); + File file = path.toFile(); + Files.write(path, content.getBytes(StandardCharsets.UTF_8)); + file.deleteOnExit(); + return file; + } catch (IOException e) { + throw new RuntimeException("Unable to write restore file for iptables.", e); + } + } + + /** + * to be agnostic to potential variances in output (and simplify test cases) + */ + private static boolean equalsWhenIgnoreSpaceAndCase(String a, String b) { + return a.trim().replaceAll("\\s+", " ").equalsIgnoreCase(b.trim().replaceAll("\\s+", " ")); + } +} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/Action.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/Action.java deleted file mode 100644 index 8ccb35f0936..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/Action.java +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables; - -/** - * @author mpolden - */ -public enum Action { - DROP("DROP"), - REJECT("REJECT"), - ACCEPT("ACCEPT"); - - public final String name; - - Action(String name) { - this.name = name; - } -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/Chain.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/Chain.java deleted file mode 100644 index 244f8340490..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/Chain.java +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables; - -/** - * @author mpolden - */ -public enum Chain { - INPUT("INPUT"), - FORWARD("FORWARD"), - OUTPUT("OUTPUT"); - - public final String name; - - Chain(String name) { - this.name = name; - } -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/Command.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/Command.java deleted file mode 100644 index 4f487cb6688..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/Command.java +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables; - -/** - * Represents a single iptables command - * - * @author mpolden - */ -public interface Command { - - String asString(); - - default String asString(String commandName) { - return commandName + " " + asString(); - } - - default String[] asArray(String commandName) { - return asString(commandName).split(" "); - } -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/FilterCommand.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/FilterCommand.java deleted file mode 100644 index 6cd1e7d87fc..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/FilterCommand.java +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -/** - * @author mpolden - */ -public class FilterCommand implements Command { - - private final Chain chain; - private final Action action; - private final List<Option> options; - - public FilterCommand(Chain chain, Action action) { - this.chain = chain; - this.action = action; - this.options = new ArrayList<>(); - } - - public FilterCommand withOption(String name, String argument) { - options.add(new Option(name, argument)); - return this; - } - - @Override - public String asString() { - final StringBuilder builder = new StringBuilder(); - builder.append("-A ").append(chain.name); - if (!options.isEmpty()) { - builder.append(" ") - .append(options.stream().map(Option::asString).collect(Collectors.joining(" "))); - } - builder.append(" -j ").append(action.name); - return builder.toString(); - } - - private static class Option { - private final String name; - private final String argument; - - public Option(String name, String argument) { - this.name = name; - this.argument = argument; - } - - public String asString() { - return String.format("%s %s", name, argument); - } - } -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/FlushCommand.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/FlushCommand.java deleted file mode 100644 index 36a9c8f72f9..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/FlushCommand.java +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables; - -/** - * @author mpolden - */ -public class FlushCommand implements Command { - - private final Chain chain; - - public FlushCommand(Chain chain) { - this.chain = chain; - } - - @Override - public String asString() { - return String.format("-F %s", chain.name); - } -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/PolicyCommand.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/PolicyCommand.java deleted file mode 100644 index d4070b7c328..00000000000 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/PolicyCommand.java +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -package com.yahoo.vespa.hosted.node.admin.maintenance.acl.iptables; - -/** - * @author mpolden - */ -public class PolicyCommand implements Command { - - private final Chain chain; - private final Action policy; - - public PolicyCommand(Chain chain, Action policy) { - this.chain = chain; - this.policy = policy; - } - - @Override - public String asString() { - return String.format("-P %s %s", chain.name, policy.name); - } -} diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPAddresses.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPAddresses.java index 4953627e99f..fe07076d6f2 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPAddresses.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPAddresses.java @@ -14,15 +14,15 @@ 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 @@ -31,6 +31,12 @@ public interface IPAddresses { 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) */ @@ -98,8 +104,8 @@ public interface IPAddresses { /** * 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 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 */ diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPVersion.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPVersion.java index 35262739f45..e614da020b2 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPVersion.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPVersion.java @@ -1,5 +1,10 @@ package com.yahoo.vespa.hosted.node.admin.task.util.network; +import com.google.common.net.InetAddresses; + +import java.net.Inet4Address; +import java.net.InetAddress; + /** * Strong type IPv4 and IPv6 with common executables for ip related commands. * @@ -7,21 +12,47 @@ package com.yahoo.vespa.hosted.node.admin.task.util.network; */ public enum IPVersion { - IPv6("ip6tables", "ip -6"), - IPv4("iptables", "ip"); + IPv6("ip6tables", "ip -6", "ipv6-icmp", "/128", "icmp6-port-unreachable", "ip6tables-restore"), + IPv4("iptables", "ip", "icmp", "/32", "icmp-port-unreachable", "iptables-restore"); - IPVersion(String iptablesCmd, String ipCmd) { + IPVersion(String iptablesCmd, String ipCmd, + String icmpProtocol, String singleHostCidr, String icmpPortUnreachable, + String iptablesRestore) { this.ipCmd = ipCmd; this.iptablesCmd = iptablesCmd; + this.icmpProtocol = icmpProtocol; + this.singleHostCidr = singleHostCidr; + this.icmpPortUnreachable = icmpPortUnreachable; + this.iptablesRestore = iptablesRestore; } - private String iptablesCmd; - private String ipCmd; + private final String iptablesCmd; + private final String ipCmd; + private final String icmpProtocol; + private final String singleHostCidr; + private final String icmpPortUnreachable; + private final String iptablesRestore; public String iptablesCmd() { return iptablesCmd; } + public String iptablesRestore() { + return iptablesRestore; + } public String ipCmd() { return ipCmd; } + public String icmpProtocol() { + return icmpProtocol; + } + public String singleHostCidr() { return singleHostCidr; } + public String icmpPortUnreachable() { return icmpPortUnreachable; } + + public boolean match(InetAddress address) { + return this == IPVersion.get(address); + } + + public static IPVersion get(InetAddress address) { + return address instanceof Inet4Address ? IPv4 : IPv6; + } } diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java index 5822a6a12ca..cf50a7d8d75 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java @@ -1,11 +1,13 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.integrationTests; + import com.yahoo.config.provision.NodeType; -import com.yahoo.vespa.hosted.node.admin.AclSpec; +import com.yahoo.vespa.hosted.dockerapi.Container; import com.yahoo.vespa.hosted.node.admin.NodeSpec; -import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.maintenance.acl.Acl; +import com.yahoo.vespa.hosted.node.admin.nodeagent.NodeAttributes; import com.yahoo.vespa.hosted.provision.Node; import java.util.ArrayList; @@ -14,6 +16,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; /** * Mock with some simple logic @@ -24,7 +27,8 @@ public class NodeRepoMock implements NodeRepository { private static final Object monitor = new Object(); private final Map<String, NodeSpec> nodeRepositoryNodesByHostname = new HashMap<>(); - private final Map<String, List<AclSpec>> acls = new HashMap<>(); + private final Map<String, Acl> acls = new HashMap<>(); + private final CallOrderVerifier callOrderVerifier; public NodeRepoMock(CallOrderVerifier callOrderVerifier) { @@ -39,11 +43,6 @@ public class NodeRepoMock implements NodeRepository { } @Override - public List<NodeSpec> getNodes(NodeType... nodeTypes) { - return Collections.emptyList(); - } - - @Override public Optional<NodeSpec> getNode(String hostName) { synchronized (monitor) { return Optional.ofNullable(nodeRepositoryNodesByHostname.get(hostName)); @@ -51,10 +50,14 @@ public class NodeRepoMock implements NodeRepository { } @Override - public List<AclSpec> getNodesAcl(String hostName) { + public List<NodeSpec> getNodes(NodeType... nodeTypes) { + return Collections.emptyList(); + } + + @Override + public Map<String, Acl> getAcls(String hostname) { synchronized (monitor) { - return Optional.ofNullable(acls.get(hostName)) - .orElseGet(Collections::emptyList); + return acls; } } diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainerTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainerTest.java index fc1321b16c6..e499d2aabb6 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainerTest.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainerTest.java @@ -1,156 +1,263 @@ // Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.hosted.node.admin.maintenance.acl; +import com.google.common.net.InetAddresses; import com.yahoo.vespa.hosted.dockerapi.Container; import com.yahoo.vespa.hosted.dockerapi.ContainerName; import com.yahoo.vespa.hosted.dockerapi.DockerImage; -import com.yahoo.vespa.hosted.node.admin.AclSpec; -import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; +import com.yahoo.vespa.hosted.dockerapi.ProcessResult; +import com.yahoo.vespa.hosted.node.admin.component.Environment; import com.yahoo.vespa.hosted.node.admin.configserver.noderepository.NodeRepository; +import com.yahoo.vespa.hosted.node.admin.docker.DockerOperations; +import com.yahoo.vespa.hosted.node.admin.task.util.network.IPAddressesMock; +import com.yahoo.vespa.hosted.node.admin.task.util.network.IPVersion; import org.junit.Before; import org.junit.Test; -import org.mockito.verification.VerificationMode; +import java.net.InetAddress; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; -import java.util.stream.IntStream; import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyVararg; import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.anyVararg; public class AclMaintainerTest { private static final String NODE_ADMIN_HOSTNAME = "node-admin.region-1.yahoo.com"; - private AclMaintainer aclMaintainer; - private DockerOperations dockerOperations; - private NodeRepository nodeRepository; - private List<Container> containers; + private final IPAddressesMock ipAddresses = new IPAddressesMock(); + private final DockerOperations dockerOperations = mock(DockerOperations.class); + private final NodeRepository nodeRepository = mock(NodeRepository.class); + private final Map<String, Container> containers = new HashMap<>(); + private final List<Container> containerList = new ArrayList<>(); + private final Environment env = mock(Environment.class); + private final AclMaintainer aclMaintainer = + new AclMaintainer(dockerOperations, nodeRepository, NODE_ADMIN_HOSTNAME, ipAddresses, env); @Before public void before() { - this.dockerOperations = mock(DockerOperations.class); - this.nodeRepository = mock(NodeRepository.class); - this.aclMaintainer = new AclMaintainer(dockerOperations, nodeRepository, NODE_ADMIN_HOSTNAME); - this.containers = new ArrayList<>(); - when(dockerOperations.getAllManagedContainers()).thenReturn(containers); + when(dockerOperations.getAllManagedContainers()).thenReturn(containerList); + when(env.getCloud()).thenReturn("AWS"); } @Test - public void configures_container_acl() { - Container container = makeContainer("container-1"); - List<AclSpec> aclSpec = makeNodeAcls(3, container.name); - when(nodeRepository.getNodesAcl(NODE_ADMIN_HOSTNAME)).thenReturn(aclSpec); + public void no_redirect_in_yahoo() { + when(env.getCloud()).thenReturn("YAHOO"); + + Container container = addContainer("container1", "container1.host.com", Container.State.RUNNING); + Map<String, Acl> acls = makeAcl(container.hostname, "4321", "2001::1"); + when(nodeRepository.getAcls(NODE_ADMIN_HOSTNAME)).thenReturn(acls); + + whenListRules(container.name, "filter", IPVersion.IPv6, ""); + whenListRules(container.name, "filter", IPVersion.IPv4, ""); + aclMaintainer.run(); - assertAclsApplied(container.name, aclSpec); + + verify(dockerOperations, never()).executeCommandInNetworkNamespace(eq(container.name), eq("iptables"), eq("-S"), eq("-t"), eq("nat")); + verify(dockerOperations, never()).executeCommandInNetworkNamespace(eq(container.name), eq("ip6tables"), eq("-S"), eq("-t"), eq("nat")); + verify(dockerOperations, times(1)).executeCommandInNetworkNamespace(eq(container.name), eq("iptables-restore"), anyVararg()); + verify(dockerOperations, times(1)).executeCommandInNetworkNamespace(eq(container.name), eq("ip6tables-restore"), anyVararg()); } @Test - public void does_not_configure_acl_if_unchanged() { - Container container = makeContainer("container-1"); - List<AclSpec> aclSpecs = makeNodeAcls(3, container.name); - when(nodeRepository.getNodesAcl(NODE_ADMIN_HOSTNAME)).thenReturn(aclSpecs); - // Run twice - aclMaintainer.run(); + public void empty_trusted_ports_are_handled() { + Container container = addContainer("container1", "container1.host.com", Container.State.RUNNING); + Map<String, Acl> acls = makeAcl(container.hostname, "4321", "2001::1"); + + when(nodeRepository.getAcls(NODE_ADMIN_HOSTNAME)).thenReturn(acls); + + whenListRules(container.name, "filter", IPVersion.IPv6, ""); + whenListRules(container.name, "filter", IPVersion.IPv4, ""); + whenListRules(container.name, "nat", IPVersion.IPv4, ""); + whenListRules(container.name, "nat", IPVersion.IPv6, ""); + aclMaintainer.run(); - assertAclsApplied(container.name, aclSpecs, times(1)); + + verify(dockerOperations, times(1)).executeCommandInNetworkNamespace(eq(container.name), eq("iptables-restore"), anyVararg()); //we don;t have a ip4 address for the container so no redirect either + verify(dockerOperations, times(2)).executeCommandInNetworkNamespace(eq(container.name), eq("ip6tables-restore"), anyVararg()); } @Test - public void reconfigures_acl_when_container_pid_changes() { - Container container = makeContainer("container-1"); - List<AclSpec> aclSpecs = makeNodeAcls(3, container.name); - when(nodeRepository.getNodesAcl(NODE_ADMIN_HOSTNAME)).thenReturn(aclSpecs); + public void configures_container_acl_when_iptables_differs() { + Container container = addContainer("container1", "container1.host.com", Container.State.RUNNING); + Map<String, Acl> acls = makeAcl(container.hostname, "4321", "2001::1"); - aclMaintainer.run(); - assertAclsApplied(container.name, aclSpecs); + when(nodeRepository.getAcls(NODE_ADMIN_HOSTNAME)).thenReturn(acls); + + whenListRules(container.name, "filter", IPVersion.IPv6, ""); + whenListRules(container.name, "filter", IPVersion.IPv4, ""); + whenListRules(container.name, "nat", IPVersion.IPv4, ""); + whenListRules(container.name, "nat", IPVersion.IPv6, ""); - // Container is restarted and PID changes - makeContainer(container.name.asString(), Container.State.RUNNING, 43); aclMaintainer.run(); - assertAclsApplied(container.name, aclSpecs, times(2)); + verify(dockerOperations, times(1)).executeCommandInNetworkNamespace(eq(container.name), eq("iptables-restore"), anyVararg()); //we don;t have a ip4 address for the container so no redirect either + verify(dockerOperations, times(2)).executeCommandInNetworkNamespace(eq(container.name), eq("ip6tables-restore"), anyVararg()); } @Test - public void does_not_configure_acl_for_stopped_container() { - Container stoppedContainer = makeContainer("container-1", Container.State.EXITED, 0); - List<AclSpec> aclSpecs = makeNodeAcls(1, stoppedContainer.name); - when(nodeRepository.getNodesAcl(NODE_ADMIN_HOSTNAME)).thenReturn(aclSpecs); + public void ignore_containers_not_running() { + Container container = addContainer("container1", "container1.host.com", Container.State.EXITED); + Map<String, Acl> acls = makeAcl(container.hostname, "4321", "2001::1"); + + when(nodeRepository.getAcls(NODE_ADMIN_HOSTNAME)).thenReturn(acls); + aclMaintainer.run(); - assertAclsApplied(stoppedContainer.name, aclSpecs, never()); + + verify(dockerOperations, never()).executeCommandInNetworkNamespace(eq(container.name), anyVararg()); } @Test - public void rollback_is_attempted_when_applying_acl_fail() { - Container container = makeContainer("container-1"); - when(nodeRepository.getNodesAcl(NODE_ADMIN_HOSTNAME)).thenReturn(makeNodeAcls(1, container.name)); + public void only_configure_iptables_for_ipversion_that_differs() { + Container container = addContainer("container1", "container1.host.com", Container.State.RUNNING); + Map<String, Acl> acls = makeAcl(container.hostname, "4321,2345,22", "2001::1", "fd01:1234::4321"); - doThrow(new RuntimeException("iptables command failed")) - .doNothing() - .when(dockerOperations) - .executeCommandInNetworkNamespace(any(), anyVararg()); + when(nodeRepository.getAcls(NODE_ADMIN_HOSTNAME)).thenReturn(acls); + + String IPV6 = "-P INPUT ACCEPT\n" + + "-P FORWARD ACCEPT\n" + + "-P OUTPUT ACCEPT\n" + + "-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT\n" + + "-A INPUT -i lo -j ACCEPT\n" + + "-A INPUT -p ipv6-icmp -j ACCEPT\n" + + "-A INPUT -p tcp -m multiport --dports 4321,2345,22 -j ACCEPT\n" + + "-A INPUT -s 2001::1/128 -j ACCEPT\n" + + "-A INPUT -s fd01:1234::4321/128 -j ACCEPT\n" + + "-A INPUT -j REJECT --reject-with icmp6-port-unreachable"; + + String NATv6 = "-P PREROUTING ACCEPT\n" + + "-P INPUT ACCEPT\n" + + "-P OUTPUT ACCEPT\n" + + "-P POSTROUTING ACCEPT\n" + + "-A OUTPUT -d 3001::1/128 -j REDIRECT"; + + whenListRules(container.name, "filter", IPVersion.IPv6, IPV6); + whenListRules(container.name, "filter", IPVersion.IPv4, ""); //IPv4 will then differ from wanted + whenListRules(container.name, "nat", IPVersion.IPv6, NATv6); aclMaintainer.run(); - verify(dockerOperations).executeCommandInNetworkNamespace( - eq(container.name), - eq("ip6tables"), - eq("-P"), - eq("INPUT"), - eq("ACCEPT") - ); + verify(dockerOperations, times(1)).executeCommandInNetworkNamespace(eq(container.name), eq("iptables-restore"), anyVararg()); + verify(dockerOperations, never()).executeCommandInNetworkNamespace(eq(container.name), eq("ip6tables-restore"), anyVararg()); } - private void assertAclsApplied(ContainerName containerName, List<AclSpec> aclSpecs) { - assertAclsApplied(containerName, aclSpecs, times(1)); + @Test + public void does_not_configure_acl_if_iptables_dualstack_are_ok() { + Container container = addContainer("container1", "container1.host.com", Container.State.RUNNING); + Map<String, Acl> acls = makeAcl(container.hostname, "22,4443,2222", "2001::1", "192.64.13.2"); + + when(nodeRepository.getAcls(NODE_ADMIN_HOSTNAME)).thenReturn(acls); + + String IPV4_FILTER = "-P INPUT ACCEPT\n" + + "-P FORWARD ACCEPT\n" + + "-P OUTPUT ACCEPT\n" + + "-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT\n" + + "-A INPUT -i lo -j ACCEPT\n" + + "-A INPUT -p icmp -j ACCEPT\n" + + "-A INPUT -p tcp -m multiport --dports 22,4443,2222 -j ACCEPT\n" + + "-A INPUT -s 192.64.13.2/32 -j ACCEPT\n" + + "-A INPUT -j REJECT --reject-with icmp-port-unreachable"; + + String IPV6_FILTER = "-P INPUT ACCEPT\n" + + "-P FORWARD ACCEPT\n" + + "-P OUTPUT ACCEPT\n" + + "-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT\n" + + "-A INPUT -i lo -j ACCEPT\n" + + "-A INPUT -p ipv6-icmp -j ACCEPT\n" + + "-A INPUT -p tcp -m multiport --dports 22,4443,2222 -j ACCEPT\n" + + "-A INPUT -s 2001::1/128 -j ACCEPT\n" + + "-A INPUT -j REJECT --reject-with icmp6-port-unreachable"; + + String IPV6_NAT = "-P PREROUTING ACCEPT\n" + + "-P INPUT ACCEPT\n" + + "-P OUTPUT ACCEPT\n" + + "-P POSTROUTING ACCEPT\n" + + "-A OUTPUT -d 3001::1/128 -j REDIRECT"; + + whenListRules(container.name, "filter", IPVersion.IPv6, IPV6_FILTER); + whenListRules(container.name, "nat", IPVersion.IPv6, IPV6_NAT); + whenListRules(container.name, "filter", IPVersion.IPv4, IPV4_FILTER); + + aclMaintainer.run(); + + verify(dockerOperations, never()).executeCommandInNetworkNamespace(any(), eq("ip6tables-restore"), anyVararg()); + verify(dockerOperations, never()).executeCommandInNetworkNamespace(any(), eq("iptables-restore"), anyVararg()); } - private void assertAclsApplied(ContainerName containerName, List<AclSpec> aclSpecs, - VerificationMode verificationMode) { - StringBuilder expectedCommand = new StringBuilder() - .append("ip6tables -F INPUT; ") - .append("ip6tables -P INPUT DROP; ") - .append("ip6tables -P FORWARD DROP; ") - .append("ip6tables -P OUTPUT ACCEPT; ") - .append("ip6tables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT; ") - .append("ip6tables -A INPUT -i lo -j ACCEPT; ") - .append("ip6tables -A INPUT -p ipv6-icmp -j ACCEPT; "); - aclSpecs.forEach(nodeAcl -> - expectedCommand.append("ip6tables -A INPUT -s " + nodeAcl.ipAddress() + "/128 -j ACCEPT; ")); + @Test + public void rollback_is_attempted_when_applying_acl_fail() { + Container container = addContainer("container1", "container1.host.com", Container.State.RUNNING); + Map<String, Acl> acls = makeAcl(container.hostname, "4321", "2001::1"); + when(nodeRepository.getAcls(NODE_ADMIN_HOSTNAME)).thenReturn(acls); + + String IPV6_NAT = "-P PREROUTING ACCEPT\n" + + "-P INPUT ACCEPT\n" + + "-P OUTPUT ACCEPT\n" + + "-P POSTROUTING ACCEPT\n" + + "-A OUTPUT -d 3001::1/128 -j REDIRECT"; + + whenListRules(container.name, "filter", IPVersion.IPv6, ""); + whenListRules(container.name, "filter", IPVersion.IPv4, ""); + whenListRules(container.name, "nat", IPVersion.IPv6, IPV6_NAT); + + when(dockerOperations.executeCommandInNetworkNamespace( + eq(container.name), + eq("ip6tables-restore"), anyVararg())).thenThrow(new RuntimeException("iptables restore failed")); - expectedCommand.append("ip6tables -A INPUT -j REJECT"); + when(dockerOperations.executeCommandInNetworkNamespace( + eq(container.name), + eq("iptables-restore"), anyVararg())).thenThrow(new RuntimeException("iptables restore failed")); + aclMaintainer.run(); - verify(dockerOperations, verificationMode).executeCommandInNetworkNamespace( - eq(containerName), eq("/bin/sh"), eq("-c"), eq(expectedCommand.toString())); + verify(dockerOperations, times(1)).executeCommandInNetworkNamespace(eq(container.name), + eq("ip6tables"), eq("-F"), eq("-t"), eq("filter")); + verify(dockerOperations, times(1)).executeCommandInNetworkNamespace(eq(container.name), + eq("iptables"), eq("-F"), eq("-t"), eq("filter")); } - private Container makeContainer(String hostname) { - return makeContainer(hostname, Container.State.RUNNING, 42); + private void whenListRules(ContainerName name, String table, IPVersion ipVersion, String result) { + when(dockerOperations.executeCommandInNetworkNamespace( + eq(name), + eq(ipVersion.iptablesCmd()), eq("-S"), eq("-t"), eq(table))) + .thenReturn(new ProcessResult(0, result, "")); } - private Container makeContainer(String hostname, Container.State state, int pid) { - final ContainerName containerName = new ContainerName(hostname); + private Container addContainer(String name, String hostname, Container.State state) { + final ContainerName containerName = new ContainerName(name); final Container container = new Container(hostname, new DockerImage("mock"), null, - containerName, state, pid); - containers.add(container); + containerName, state, 2); + containers.put(name, container); + containerList.add(container); + ipAddresses.addAddress(hostname, "3001::" + containers.size()); return container; } - private static List<AclSpec> makeNodeAcls(int count, ContainerName containerName) { - return IntStream.rangeClosed(1, count) - .mapToObj(i -> new AclSpec("node-" + i, "::" + i, containerName)) + private Map<String, Acl> makeAcl(String containerHostname, String portsCommaSeparated, String... addresses) { + Map<String, Acl> map = new HashMap<>(); + + List<Integer> ports = Arrays.stream(portsCommaSeparated.split(",")) + .map(Integer::valueOf) + .collect(Collectors.toList()); + + List<InetAddress> hosts = Arrays.stream(addresses) + .map(InetAddresses::forString) .collect(Collectors.toList()); - } + Acl acl = new Acl(ports, hosts); + map.put(containerHostname, acl); + + return map; + } } diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclTest.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclTest.java new file mode 100644 index 00000000000..d8dcb0e7c9d --- /dev/null +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclTest.java @@ -0,0 +1,80 @@ +package com.yahoo.vespa.hosted.node.admin.maintenance.acl; + +import com.google.common.net.InetAddresses; +import com.yahoo.vespa.hosted.node.admin.task.util.network.IPVersion; +import org.junit.Assert; +import org.junit.Test; + +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class AclTest { + + private final Acl aclCommon = new Acl( + createPortList(1234, 453), + createTrustedNodes("192.1.2.2", "fb00::1", "fe80::2")); + + private final Acl aclNoPorts = new Acl( + Collections.emptyList(), + createTrustedNodes("192.1.2.2", "fb00::1", "fe80::2")); + + @Test + public void no_trusted_ports() { + String listRulesIpv4 = aclNoPorts.toRules(IPVersion.IPv4); + Assert.assertEquals( + "-P INPUT ACCEPT\n" + + "-P FORWARD ACCEPT\n" + + "-P OUTPUT ACCEPT\n" + + "-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT\n" + + "-A INPUT -i lo -j ACCEPT\n" + + "-A INPUT -p icmp -j ACCEPT\n" + + "-A INPUT -s 192.1.2.2/32 -j ACCEPT\n" + + "-A INPUT -j REJECT --reject-with icmp-port-unreachable", + listRulesIpv4); + } + + @Test + public void ipv4_list_rules() { + String listRulesIpv4 = aclCommon.toRules(IPVersion.IPv4); + Assert.assertEquals( + "-P INPUT ACCEPT\n" + + "-P FORWARD ACCEPT\n" + + "-P OUTPUT ACCEPT\n" + + "-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT\n" + + "-A INPUT -i lo -j ACCEPT\n" + + "-A INPUT -p icmp -j ACCEPT\n" + + "-A INPUT -p tcp -m multiport --dports 1234,453 -j ACCEPT\n" + + "-A INPUT -s 192.1.2.2/32 -j ACCEPT\n" + + "-A INPUT -j REJECT --reject-with icmp-port-unreachable", + listRulesIpv4); + } + + @Test + public void ipv6_list_rules() { + String listRulesIpv6 = aclCommon.toRules(IPVersion.IPv6); + Assert.assertEquals( + "-P INPUT ACCEPT\n" + + "-P FORWARD ACCEPT\n" + + "-P OUTPUT ACCEPT\n" + + "-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT\n" + + "-A INPUT -i lo -j ACCEPT\n" + + "-A INPUT -p ipv6-icmp -j ACCEPT\n" + + "-A INPUT -p tcp -m multiport --dports 1234,453 -j ACCEPT\n" + + "-A INPUT -s fb00::1/128 -j ACCEPT\n" + + "-A INPUT -s fe80::2/128 -j ACCEPT\n" + + "-A INPUT -j REJECT --reject-with icmp6-port-unreachable", listRulesIpv6); + } + + private List<Integer> createPortList(Integer... ports) { + return Arrays.asList(ports); + } + + private List<InetAddress> createTrustedNodes(String... addresses) { + return Arrays.stream(addresses) + .map(InetAddresses::forString) + .collect(Collectors.toList()); + } +} diff --git a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPAddressesMock.java b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPAddressesMock.java index c799ee5eaca..4a103c89446 100644 --- a/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPAddressesMock.java +++ b/node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPAddressesMock.java @@ -14,7 +14,7 @@ public class IPAddressesMock implements IPAddresses { Map<String, List<InetAddress>> otherAddresses = new HashMap<>(); - IPAddressesMock addAddress(String hostname, String ip) { + public IPAddressesMock addAddress(String hostname, String ip) { List<InetAddress> addresses = otherAddresses.getOrDefault(hostname, new ArrayList<>()); try { addresses.add(InetAddress.getByName(ip)); @@ -28,6 +28,7 @@ public class IPAddressesMock implements IPAddresses { @Override public InetAddress[] getAddresses(String hostname) { List<InetAddress> addresses = otherAddresses.get(hostname); + if (addresses == null) return new InetAddress[0]; return addresses.toArray(new InetAddress[addresses.size()]); } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java index 2ef79ec53dd..4bf7e70d06b 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java @@ -166,11 +166,12 @@ public class NodeRepository extends AbstractComponent { public List<Node> getFailed() { return db.getNodes(Node.State.failed); } /** - * Returns a set of nodes that should be trusted by the given node. + * Returns the ACL for the node (trusted nodes, networks and ports) */ private NodeAcl getNodeAcl(Node node, NodeList candidates) { Set<Node> trustedNodes = new TreeSet<>(Comparator.comparing(Node::hostname)); Set<String> trustedNetworks = new HashSet<>(); + Set<Integer> trustedPorts = new HashSet<>(); // For all cases below, trust: // - nodes in same application @@ -198,13 +199,18 @@ public class NodeRepository extends AbstractComponent { case config: // Config servers trust all nodes trustedNodes.addAll(candidates.asList()); + + // And all connections on 4443 + trustedPorts.add(4443); break; case proxy: - // No special rules for proxies + // Accept connections from the world on 4443 + trustedPorts.add(4443); break; case host: + // This is only needed for macvlan networks - for nated networks this is handled elsewhere. // Docker bridge network trustedNetworks.add("172.17.0.0/16"); break; @@ -215,7 +221,7 @@ public class NodeRepository extends AbstractComponent { node.hostname(), node.type())); } - return new NodeAcl(node, trustedNodes, trustedNetworks); + return new NodeAcl(node, trustedNodes, trustedNetworks, trustedPorts); } /** diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/NodeAcl.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/NodeAcl.java index a6190f41c07..34a8b414ef4 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/NodeAcl.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/NodeAcl.java @@ -17,11 +17,13 @@ public class NodeAcl { private final Node node; private final Set<Node> trustedNodes; private final Set<String> trustedNetworks; + private final Set<Integer> trustedPorts; - public NodeAcl(Node node, Set<Node> trustedNodes, Set<String> trustedNetworks) { + public NodeAcl(Node node, Set<Node> trustedNodes, Set<String> trustedNetworks, Set<Integer> trustedPorts) { this.node = node; this.trustedNodes = ImmutableSet.copyOf(trustedNodes); this.trustedNetworks = ImmutableSet.copyOf(trustedNetworks); + this.trustedPorts = ImmutableSet.copyOf(trustedPorts); } public Node node() { @@ -35,4 +37,8 @@ public class NodeAcl { public Set<String> trustedNetworks() { return trustedNetworks; } + + public Set<Integer> trustedPorts() { + return trustedPorts; + } } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodeAclResponse.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodeAclResponse.java index 65b727ad0dd..e9b3ea5e726 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodeAclResponse.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodeAclResponse.java @@ -13,6 +13,7 @@ import com.yahoo.vespa.hosted.provision.node.NodeAcl; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.util.List; import java.util.Set; /** @@ -42,13 +43,16 @@ public class NodeAclResponse extends HttpResponse { .orElseGet(() -> nodeRepository.getConfigNode(hostname) .orElseThrow(() -> new NotFoundException("No node with hostname '" + hostname + "'"))); + List<NodeAcl> acls = nodeRepository.getNodeAcls(node, aclsForChildren); + Cursor trustedNodesArray = object.setArray("trustedNodes"); - nodeRepository.getNodeAcls(node, aclsForChildren).forEach(nodeAcl -> toSlime(nodeAcl, trustedNodesArray)); + acls.forEach(nodeAcl -> toSlime(nodeAcl, trustedNodesArray)); Cursor trustedNetworksArray = object.setArray("trustedNetworks"); - nodeRepository.getNodeAcls(node, aclsForChildren).forEach(nodeAcl -> toSlime(nodeAcl.trustedNetworks(), - nodeAcl.node(), - trustedNetworksArray)); + acls.forEach(nodeAcl -> toSlime(nodeAcl.trustedNetworks(), nodeAcl.node(), trustedNetworksArray)); + + Cursor trustedPortsArray = object.setArray("trustedPorts"); + acls.forEach(nodeAcl -> toSlime(nodeAcl.trustedPorts(), nodeAcl, trustedPortsArray)); } private void toSlime(NodeAcl nodeAcl, Cursor array) { @@ -61,11 +65,19 @@ public class NodeAclResponse extends HttpResponse { })); } - private void toSlime(Set<String> trustedNetworks, Node trustedBy, Cursor array) { + private void toSlime(Set<String> trustedNetworks, Node trustedby, Cursor array) { trustedNetworks.forEach(network -> { Cursor object = array.addObject(); object.setString("network", network); - object.setString("trustedBy", trustedBy.hostname()); + object.setString("trustedBy", trustedby.hostname()); + }); + } + + private void toSlime(Set<Integer> trustedPorts, NodeAcl trustedBy, Cursor array) { + trustedPorts.forEach(port -> { + Cursor object = array.addObject(); + object.setLong("port", port); + object.setString("trustedBy", trustedBy.node().hostname()); }); } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-config-server.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-config-server.json index 775d33a3a19..ca3556af805 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-config-server.json +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-config-server.json @@ -193,5 +193,11 @@ "trustedBy": "cfg1" } ], - "trustedNetworks": [] + "trustedNetworks": [], + "trustedPorts": [ + { + "port": 4443, + "trustedBy": "cfg1" + } + ] } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-docker-host.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-docker-host.json index f13730ba066..ec423ed0dc5 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-docker-host.json +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-docker-host.json @@ -84,5 +84,6 @@ "network": "172.17.0.0/16", "trustedBy": "dockerhost1.yahoo.com" } - ] + ], + "trustedPorts": [] } diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-tenant-node.json b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-tenant-node.json index b2184c9d825..2f37c1859a2 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-tenant-node.json +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-tenant-node.json @@ -139,5 +139,6 @@ "trustedBy": "foo.yahoo.com" } ], - "trustedNetworks": [] + "trustedNetworks": [], + "trustedPorts":[] } |