summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/Docker.java2
-rw-r--r--docker-api/src/main/java/com/yahoo/vespa/hosted/dockerapi/DockerImpl.java2
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/AclSpec.java51
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/component/DockerAdminComponent.java4
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/NodeRepository.java10
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/RealNodeRepository.java76
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/configserver/noderepository/bindings/GetAclResponse.java35
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperations.java2
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/docker/DockerOperationsImpl.java10
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/Acl.java92
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainer.java102
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/IPTablesRestore.java82
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/Action.java17
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/Chain.java17
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/Command.java20
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/FilterCommand.java53
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/FlushCommand.java19
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/iptables/PolicyCommand.java21
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPAddresses.java18
-rw-r--r--node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPVersion.java41
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/integrationTests/NodeRepoMock.java25
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclMaintainerTest.java271
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/maintenance/acl/AclTest.java80
-rw-r--r--node-admin/src/test/java/com/yahoo/vespa/hosted/node/admin/task/util/network/IPAddressesMock.java3
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/NodeRepository.java12
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/node/NodeAcl.java8
-rw-r--r--node-repository/src/main/java/com/yahoo/vespa/hosted/provision/restapi/v2/NodeAclResponse.java24
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-config-server.json8
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-docker-host.json3
-rw-r--r--node-repository/src/test/java/com/yahoo/vespa/hosted/provision/restapi/v2/responses/acl-tenant-node.json3
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":[]
}