From a0a1b0e2af851e1a15286e5d5eb576eae5769a59 Mon Sep 17 00:00:00 2001 From: Marek Gradzki Date: Fri, 26 Aug 2016 12:37:45 +0200 Subject: HONEYCOMB-139: ietf-acl translation layer. IP6 L3 ACL support Other changes: - documentation update - eth + ip4 writer rafactoring + tests Change-Id: I1ac6a4e99dd4f12c870cbd749af6b98018294dd4 Signed-off-by: Marek Gradzki --- .../honeycomb/translate/v3po/AclWriterFactory.java | 2 +- .../fd/honeycomb/translate/v3po/acl/AclWriter.java | 105 ---------- .../v3po/interfaces/acl/AbstractAceWriter.java | 104 +++++++--- .../v3po/interfaces/acl/AceEthWriter.java | 50 ++--- .../v3po/interfaces/acl/AceIp4Writer.java | 113 +++++------ .../v3po/interfaces/acl/AceIp6Writer.java | 225 +++++++++++++++++++++ .../translate/v3po/interfaces/acl/AceWriter.java | 13 ++ .../translate/v3po/interfaces/acl/AclWriter.java | 105 ++++++++++ .../v3po/interfaces/acl/IetfAClWriter.java | 43 ++-- .../translate/v3po/interfaces/acl/Readme.adoc | 32 +++ .../v3po/interfaces/acl/AceEthWriterTest.java | 107 ++++++++++ .../v3po/interfaces/acl/AceIp4WriterTest.java | 5 +- .../v3po/interfaces/acl/AceIp6WriterTest.java | 129 ++++++++++++ 13 files changed, 779 insertions(+), 254 deletions(-) delete mode 100644 v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/acl/AclWriter.java create mode 100644 v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp6Writer.java create mode 100644 v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AclWriter.java create mode 100644 v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/Readme.adoc create mode 100644 v3po/v3po2vpp/src/test/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceEthWriterTest.java create mode 100644 v3po/v3po2vpp/src/test/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp6WriterTest.java diff --git a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/AclWriterFactory.java b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/AclWriterFactory.java index d0050ab62..21bc2e6cb 100644 --- a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/AclWriterFactory.java +++ b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/AclWriterFactory.java @@ -21,7 +21,7 @@ import static io.fd.honeycomb.translate.v3po.SubinterfaceAugmentationWriterFacto import com.google.common.collect.Sets; import io.fd.honeycomb.translate.impl.write.GenericListWriter; -import io.fd.honeycomb.translate.v3po.acl.AclWriter; +import io.fd.honeycomb.translate.v3po.interfaces.acl.AclWriter; import io.fd.honeycomb.translate.write.WriterFactory; import io.fd.honeycomb.translate.write.registry.ModifiableWriterRegistryBuilder; import javax.annotation.Nonnull; diff --git a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/acl/AclWriter.java b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/acl/AclWriter.java deleted file mode 100644 index 6c3b4efed..000000000 --- a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/acl/AclWriter.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (c) 2016 Cisco and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.fd.honeycomb.translate.v3po.acl; - -import io.fd.honeycomb.translate.spi.write.ListWriterCustomizer; -import io.fd.honeycomb.translate.write.WriteContext; -import io.fd.honeycomb.translate.write.WriteFailedException; -import java.util.Optional; -import java.util.stream.Stream; -import javax.annotation.Nonnull; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.AccessLists; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.AclBase; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.Acl; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.AclKey; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.interfaces.rev140508.Interfaces; -import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.v3po.rev150105.VppInterfaceAugmentation; -import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Writer customizer responsible for Access Control Lists management. Does not send any messages to VPP. All the config - * data are stored in HC and used when acl is assigned/unassigned to/from an interface. - * - * ACLs that are currently assigned to an interface cannot be updated/deleted. - */ -public class AclWriter implements ListWriterCustomizer { - - public static final InstanceIdentifier ACL_ID = - InstanceIdentifier.create(AccessLists.class); - - private static final Logger LOG = LoggerFactory.getLogger(AclWriter.class); - - @Override - public void writeCurrentAttributes(@Nonnull final InstanceIdentifier id, @Nonnull final Acl dataAfter, - @Nonnull final WriteContext writeContext) throws WriteFailedException { - LOG.debug("Creating ACL: iid={} dataAfter={}", id, dataAfter); - - // no vpp call, just updates DataTree - } - - @Override - public void updateCurrentAttributes(@Nonnull final InstanceIdentifier id, @Nonnull final Acl dataBefore, - @Nonnull final Acl dataAfter, @Nonnull final WriteContext writeContext) - throws WriteFailedException { - LOG.debug("Updating ACL: iid={} dataBefore={} dataAfter={}", id, dataBefore, dataAfter); - - if (isAssigned(dataAfter, writeContext)) { - throw new WriteFailedException(id, - String.format("Failed to update data at %s: acl %s is already assigned", id, dataAfter)); - } - - LOG.debug("Updating unassigned ACL: iid={} dataBefore={} dataAfter={}", id, dataBefore, dataAfter); - - // no vpp call, just updates DataTree - } - - @Override - public void deleteCurrentAttributes(@Nonnull final InstanceIdentifier id, @Nonnull final Acl dataBefore, - @Nonnull final WriteContext writeContext) throws WriteFailedException { - LOG.debug("Deleting ACL: iid={} dataBefore={}", id, dataBefore); - - if (isAssigned(dataBefore, writeContext)) { - throw new WriteFailedException(id, - String.format("Failed to delete data at %s: acl %s is already assigned", id, dataBefore)); - } - - LOG.debug("Deleting unassigned ACL: iid={} dataBefore={}", id, dataBefore); - - // no vpp call, just updates DataTree - } - - private static boolean isAssigned(@Nonnull final Acl acl, - @Nonnull final WriteContext writeContext) { - final String aclName = acl.getAclName(); - final Class aclType = acl.getAclType(); - final Interfaces interfaces = writeContext.readAfter(InstanceIdentifier.create(Interfaces.class)).get(); - - return interfaces.getInterface().stream() - .map(i -> Optional.ofNullable(i.getAugmentation(VppInterfaceAugmentation.class)) - .map(aug -> aug.getIetfAcl()) - .map(ietfAcl -> ietfAcl.getAccessLists()) - .map(accessLists -> accessLists.getAcl()) - ) - .flatMap(iacl -> iacl.isPresent() - ? iacl.get().stream() - : Stream.empty()) - .filter(assignedAcl -> aclName.equals(assignedAcl.getName()) && aclType.equals(assignedAcl.getType())) - .findFirst().isPresent(); - } -} diff --git a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AbstractAceWriter.java b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AbstractAceWriter.java index 9f27271b5..21a710701 100644 --- a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AbstractAceWriter.java +++ b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AbstractAceWriter.java @@ -27,6 +27,7 @@ import java.util.stream.Collector; import javax.annotation.Nonnull; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.Ace; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.actions.PacketHandling; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.actions.packet.handling.Permit; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.matches.AceType; import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; import org.openvpp.jvpp.VppBaseCallException; @@ -37,7 +38,21 @@ import org.openvpp.jvpp.core.dto.ClassifyAddDelTableReply; import org.openvpp.jvpp.core.dto.InputAclSetInterface; import org.openvpp.jvpp.core.future.FutureJVppCore; +/** + * Base writer for translation of ietf-acl model ACEs to VPP's classify tables and sessions. + * + * Creates one classify table with single session per ACE. + * + * @param type of access control list entry + */ abstract class AbstractAceWriter implements AceWriter { + + // TODO: minimise memory used by classify tables (we create a lot of them to make ietf-acl model + // mapping more convenient): + // according to https://wiki.fd.io/view/VPP/Introduction_To_N-tuple_Classifiers#Creating_a_classifier_table, + // classify table needs 16*(1 + match_n_vectors) bytes, but this does not quite work, so setting 8K for now + protected static final int TABLE_MEM_SIZE = 8 * 1024; + private static final Collector SINGLE_ITEM_COLLECTOR = RWUtils.singleItemCollector(); @@ -47,19 +62,36 @@ abstract class AbstractAceWriter implements AceWriter { this.futureJVppCore = checkNotNull(futureJVppCore, "futureJVppCore should not be null"); } - @Nonnull - public FutureJVppCore getFutureJVppCore() { - return futureJVppCore; - } - - protected abstract ClassifyAddDelTable getClassifyAddDelTableRequest(@Nonnull final PacketHandling action, - @Nonnull final T ace, - final int nextTableIndex); - - protected abstract ClassifyAddDelSession getClassifyAddDelSessionRequest(@Nonnull final PacketHandling action, - @Nonnull final T ace, - final int nextTableIndex); - + /** + * Creates classify table for given ACE. + * + * @param action packet handling action (permit/deny) + * @param ace ACE to be translated + * @param nextTableIndex classify table index + * @return classify table that represents given ACE + */ + protected abstract ClassifyAddDelTable createClassifyTable(@Nonnull final PacketHandling action, + @Nonnull final T ace, + final int nextTableIndex); + + /** + * Creates classify session for given ACE. + * + * @param action packet handling action (permit/deny) + * @param ace ACE to be translated + * @param tableIndex classify table index for the given session + * @return classify session that represents given ACE + */ + protected abstract ClassifyAddDelSession createClassifySession(@Nonnull final PacketHandling action, + @Nonnull final T ace, + final int tableIndex); + + /** + * Sets classify table index for input_acl_set_interface request. + * + * @param request request DTO + * @param tableIndex pointer to a chain of classify tables + */ protected abstract void setClassifyTable(@Nonnull final InputAclSetInterface request, final int tableIndex); public final void write(@Nonnull final InstanceIdentifier id, @Nonnull final List aces, @@ -68,24 +100,17 @@ abstract class AbstractAceWriter implements AceWriter { final PacketHandling action = aces.stream().map(ace -> ace.getActions().getPacketHandling()).distinct() .collect(SINGLE_ITEM_COLLECTOR); - int firstTableIndex = -1; int nextTableIndex = -1; for (final Ace ace : aces) { - // Create table + session per entry. We actually need one table for each nonempty subset of params, - // so we could decrease number of tables to 109 = 15 (eth) + 31 (ip4) + 63 (ip6) for general case. - // TODO: For special cases like many ACEs of similar kind, it could be significant optimization. + // Create table + session per entry final ClassifyAddDelTable ctRequest = - getClassifyAddDelTableRequest(action, (T) ace.getMatches().getAceType(), nextTableIndex); + createClassifyTable(action, (T) ace.getMatches().getAceType(), nextTableIndex); nextTableIndex = createClassifyTable(id, ctRequest); createClassifySession(id, - getClassifyAddDelSessionRequest(action, (T) ace.getMatches().getAceType(), nextTableIndex)); - if (firstTableIndex == -1) { - firstTableIndex = nextTableIndex; - } + createClassifySession(action, (T) ace.getMatches().getAceType(), nextTableIndex)); } - - setClassifyTable(request, firstTableIndex); + setClassifyTable(request, nextTableIndex); } private int createClassifyTable(@Nonnull final InstanceIdentifier id, @@ -104,4 +129,35 @@ abstract class AbstractAceWriter implements AceWriter { TranslateUtils.getReplyForWrite(cs.toCompletableFuture(), id); } + + protected ClassifyAddDelTable createClassifyTable(@Nonnull final PacketHandling action, final int nextTableIndex) { + final ClassifyAddDelTable request = new ClassifyAddDelTable(); + request.isAdd = 1; + request.tableIndex = -1; // value not present + + request.nbuckets = 1; // we expect exactly one session per table + + if (action instanceof Permit) { + request.missNextIndex = 0; // for list of permit rules, deny (0) should be default action + } else { // deny is default value + request.missNextIndex = -1; // for list of deny rules, permit (-1) should be default action + } + + request.nextTableIndex = nextTableIndex; + request.memorySize = TABLE_MEM_SIZE; + + return request; + } + + protected ClassifyAddDelSession createClassifySession(@Nonnull final PacketHandling action, final int tableIndex) { + final ClassifyAddDelSession request = new ClassifyAddDelSession(); + request.isAdd = 1; + request.tableIndex = tableIndex; + + if (action instanceof Permit) { + request.hitNextIndex = -1; + } // deny (0) is default value + + return request; + } } diff --git a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceEthWriter.java b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceEthWriter.java index 4744de1fe..1240a2986 100644 --- a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceEthWriter.java +++ b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceEthWriter.java @@ -16,12 +16,12 @@ package io.fd.honeycomb.translate.v3po.interfaces.acl; +import com.google.common.annotations.VisibleForTesting; import io.fd.honeycomb.translate.v3po.util.TranslateUtils; import java.util.List; import javax.annotation.Nonnull; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.actions.PacketHandling; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.actions.packet.handling.Permit; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.matches.ace.type.AceEth; import org.openvpp.jvpp.core.dto.ClassifyAddDelSession; import org.openvpp.jvpp.core.dto.ClassifyAddDelTable; @@ -30,8 +30,10 @@ import org.openvpp.jvpp.core.future.FutureJVppCore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -class AceEthWriter extends AbstractAceWriter { +final class AceEthWriter extends AbstractAceWriter { + @VisibleForTesting + static final int MATCH_N_VECTORS = 1; private static final Logger LOG = LoggerFactory.getLogger(AceEthWriter.class); public AceEthWriter(@Nonnull final FutureJVppCore futureJVppCore) { @@ -39,22 +41,10 @@ class AceEthWriter extends AbstractAceWriter { } @Override - public ClassifyAddDelTable getClassifyAddDelTableRequest(@Nonnull final PacketHandling action, - @Nonnull final AceEth aceEth, - @Nonnull final int nextTableIndex) { - final ClassifyAddDelTable request = new ClassifyAddDelTable(); - request.isAdd = 1; - request.tableIndex = -1; // value not present - - request.nbuckets = 1; // we expect exactly one session per table - - if (action instanceof Permit) { - request.missNextIndex = 0; // for list of permit rules, deny (0) should be default action - } else { // deny is default value - request.missNextIndex = -1; // for list of deny rules, permit (-1) should be default action - } - - request.nextTableIndex = nextTableIndex; + public ClassifyAddDelTable createClassifyTable(@Nonnull final PacketHandling action, + @Nonnull final AceEth aceEth, + @Nonnull final int nextTableIndex) { + final ClassifyAddDelTable request = createClassifyTable(action, nextTableIndex); request.mask = new byte[16]; boolean aceIsEmpty = true; @@ -95,17 +85,11 @@ class AceEthWriter extends AbstractAceWriter { if (aceIsEmpty) { throw new IllegalArgumentException( - String.format("Ace %s does not define packet field matches", aceEth.toString())); + String.format("Ace %s does not define packet field match values", aceEth.toString())); } request.skipNVectors = 0; - request.matchNVectors = request.mask.length / 16; - - // TODO: minimise memory used by classify tables (we create a lot of them to make ietf-acl model - // mapping more convenient): - // according to https://wiki.fd.io/view/VPP/Introduction_To_N-tuple_Classifiers#Creating_a_classifier_table, - // classify table needs 16*(1 + match_n_vectors) bytes, but this does not quite work, so setting 8K for now - request.memorySize = 8 * 1024; + request.matchNVectors = MATCH_N_VECTORS; if (LOG.isDebugEnabled()) { LOG.debug("ACE action={}, rule={} translated to table={}.", action, aceEth, @@ -115,16 +99,10 @@ class AceEthWriter extends AbstractAceWriter { } @Override - public ClassifyAddDelSession getClassifyAddDelSessionRequest(@Nonnull final PacketHandling action, - @Nonnull final AceEth aceEth, - @Nonnull final int tableIndex) { - final ClassifyAddDelSession request = new ClassifyAddDelSession(); - request.isAdd = 1; - request.tableIndex = tableIndex; - - if (action instanceof Permit) { - request.hitNextIndex = -1; - } // deny (0) is default value + public ClassifyAddDelSession createClassifySession(@Nonnull final PacketHandling action, + @Nonnull final AceEth aceEth, + @Nonnull final int tableIndex) { + final ClassifyAddDelSession request = createClassifySession(action, tableIndex); request.match = new byte[16]; boolean noMatch = true; diff --git a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp4Writer.java b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp4Writer.java index ac110f98b..b2a9613fa 100644 --- a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp4Writer.java +++ b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp4Writer.java @@ -19,11 +19,11 @@ package io.fd.honeycomb.translate.v3po.interfaces.acl; import static com.google.common.base.Preconditions.checkArgument; import static io.fd.honeycomb.translate.v3po.util.TranslateUtils.ipv4AddressNoZoneToArray; +import com.google.common.annotations.VisibleForTesting; import com.google.common.primitives.Ints; import javax.annotation.Nonnull; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.actions.PacketHandling; -import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.actions.packet.handling.Permit; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.matches.ace.type.AceIp; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.matches.ace.type.ace.ip.ace.ip.version.AceIpv4; import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Ipv4Prefix; @@ -34,52 +34,61 @@ import org.openvpp.jvpp.core.future.FutureJVppCore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -class AceIp4Writer extends AbstractAceWriter { +final class AceIp4Writer extends AbstractAceWriter { - private static final Logger LOG = LoggerFactory.getLogger(AceIp4Writer.class); - private static final int TABLE_MASK_LENGTH = 48; // number of bytes + @VisibleForTesting static final int MATCH_N_VECTORS = 3; // number of 16B vectors - static final int TABLE_MEM_SIZE = 8 * 1024; - private static final int IP_MASK_LENGTH = 32; // number of bits - private static final int IP_VERSION_OFFSET = 14; - private static final int DSCP_OFFSET = 15; - private static final int SRC_IP_OFFSET = 26; - private static final int DST_IP_OFFSET = 30; + private static final Logger LOG = LoggerFactory.getLogger(AceIp4Writer.class); + private static final int TABLE_MASK_LENGTH = 48; + private static final int IP4_MASK_BIT_LENGTH = 32; + + private static final int IP_VERSION_OFFSET = 14; // first 14 bytes represent L2 header (2x6 + etherType(2)) private static final int IP_VERSION_MASK = 0xf0; + private static final int DSCP_OFFSET = 15; private static final int DSCP_MASK = 0xfc; + private static final int IP4_LEN = 4; + private static final int SRC_IP_OFFSET = IP_VERSION_OFFSET + 12; + private static final int DST_IP_OFFSET = SRC_IP_OFFSET + IP4_LEN; public AceIp4Writer(@Nonnull final FutureJVppCore futureJVppCore) { super(futureJVppCore); } - @Override - public ClassifyAddDelTable getClassifyAddDelTableRequest(@Nonnull final PacketHandling action, - @Nonnull final AceIp aceIp, - @Nonnull final int nextTableIndex) { - checkArgument(aceIp.getAceIpVersion() instanceof AceIpv4, "Expected AceIpv4 version, but was %", aceIp); - final AceIpv4 ipVersion = (AceIpv4) aceIp.getAceIpVersion(); - - final ClassifyAddDelTable request = new ClassifyAddDelTable(); - request.isAdd = 1; - request.tableIndex = -1; // value not present + private static byte[] toByteMask(final int prefixLength) { + final long mask = ((1L << prefixLength) - 1) << (IP4_MASK_BIT_LENGTH - prefixLength); + return Ints.toByteArray((int) mask); + } - request.nbuckets = 1; // we expect exactly one session per table + private static byte[] toByteMask(final Ipv4Prefix ipv4Prefix) { + final int prefixLength = Byte.valueOf(ipv4Prefix.getValue().split("/")[1]); + return toByteMask(prefixLength); + } - if (action instanceof Permit) { - request.missNextIndex = 0; // for list of permit rules, deny (0) should be default action - } else { // deny is default value - request.missNextIndex = -1; // for list of deny rules, permit (-1) should be default action + private static byte[] toMatchValue(final Ipv4Prefix ipv4Prefix) { + final String[] split = ipv4Prefix.getValue().split("/"); + final byte[] addressBytes = ipv4AddressNoZoneToArray(split[0]); + final byte[] mask = toByteMask(Byte.valueOf(split[1])); + for (int i = 0; i < addressBytes.length; ++i) { + addressBytes[i] &= mask[i]; } + return addressBytes; + } - request.nextTableIndex = nextTableIndex; + @Override + public ClassifyAddDelTable createClassifyTable(@Nonnull final PacketHandling action, + @Nonnull final AceIp aceIp, + final int nextTableIndex) { + checkArgument(aceIp.getAceIpVersion() instanceof AceIpv4, "Expected AceIpv4 version, but was %", aceIp); + final AceIpv4 ipVersion = (AceIpv4) aceIp.getAceIpVersion(); + + final ClassifyAddDelTable request = createClassifyTable(action, nextTableIndex); request.skipNVectors = 0; // match entire L2 and L3 header request.matchNVectors = MATCH_N_VECTORS; - request.memorySize = TABLE_MEM_SIZE; boolean aceIsEmpty = true; request.mask = new byte[TABLE_MASK_LENGTH]; - // First 14 bytes represent l2 header (2x6 + etherType(2)_ + // First 14 bytes represent l2 header (2x6 + etherType(2)) if (aceIp.getProtocol() != null) { // Internet Protocol number request.mask[IP_VERSION_OFFSET] = (byte) IP_VERSION_MASK; // first 4 bits } @@ -99,17 +108,18 @@ class AceIp4Writer extends AbstractAceWriter { if (ipVersion.getSourceIpv4Network() != null) { aceIsEmpty = false; - System.arraycopy(toByteMask(ipVersion.getSourceIpv4Network()), 0, request.mask, SRC_IP_OFFSET, 4); + System.arraycopy(toByteMask(ipVersion.getSourceIpv4Network()), 0, request.mask, SRC_IP_OFFSET, IP4_LEN); } if (ipVersion.getDestinationIpv4Network() != null) { aceIsEmpty = false; - System.arraycopy(toByteMask(ipVersion.getDestinationIpv4Network()), 0, request.mask, DST_IP_OFFSET, 4); + System + .arraycopy(toByteMask(ipVersion.getDestinationIpv4Network()), 0, request.mask, DST_IP_OFFSET, IP4_LEN); } if (aceIsEmpty) { throw new IllegalArgumentException( - String.format("Ace %s does not define packet field matches", aceIp.toString())); + String.format("Ace %s does not define packet field match values", aceIp.toString())); } if (LOG.isDebugEnabled()) { @@ -120,19 +130,13 @@ class AceIp4Writer extends AbstractAceWriter { } @Override - public ClassifyAddDelSession getClassifyAddDelSessionRequest(@Nonnull final PacketHandling action, - @Nonnull final AceIp aceIp, - @Nonnull final int tableIndex) { + public ClassifyAddDelSession createClassifySession(@Nonnull final PacketHandling action, + @Nonnull final AceIp aceIp, + final int tableIndex) { checkArgument(aceIp.getAceIpVersion() instanceof AceIpv4, "Expected AceIpv4 version, but was %", aceIp); final AceIpv4 ipVersion = (AceIpv4) aceIp.getAceIpVersion(); - final ClassifyAddDelSession request = new ClassifyAddDelSession(); - request.isAdd = 1; - request.tableIndex = tableIndex; - - if (action instanceof Permit) { - request.hitNextIndex = -1; - } // deny (0) is default value + final ClassifyAddDelSession request = createClassifySession(action, tableIndex); request.match = new byte[TABLE_MASK_LENGTH]; boolean noMatch = true; @@ -156,17 +160,18 @@ class AceIp4Writer extends AbstractAceWriter { if (ipVersion.getSourceIpv4Network() != null) { noMatch = false; - System.arraycopy(toMatchValue(ipVersion.getSourceIpv4Network()), 0, request.match, SRC_IP_OFFSET, 4); + System.arraycopy(toMatchValue(ipVersion.getSourceIpv4Network()), 0, request.match, SRC_IP_OFFSET, IP4_LEN); } if (ipVersion.getDestinationIpv4Network() != null) { noMatch = false; - System.arraycopy(toMatchValue(ipVersion.getDestinationIpv4Network()), 0, request.match, DST_IP_OFFSET, 4); + System.arraycopy(toMatchValue(ipVersion.getDestinationIpv4Network()), 0, request.match, DST_IP_OFFSET, + IP4_LEN); } if (noMatch) { throw new IllegalArgumentException( - String.format("Ace %s does not define neither source nor destination MAC address", aceIp.toString())); + String.format("Ace %s does not define packet field match values", aceIp.toString())); } if (LOG.isDebugEnabled()) { @@ -180,24 +185,4 @@ class AceIp4Writer extends AbstractAceWriter { protected void setClassifyTable(@Nonnull final InputAclSetInterface request, final int tableIndex) { request.ip4TableIndex = tableIndex; } - - private static byte[] toByteMask(final int prefixLength) { - final long mask = ((1L << prefixLength) - 1) << (IP_MASK_LENGTH - prefixLength); - return Ints.toByteArray((int) mask); - } - - private static byte[] toByteMask(final Ipv4Prefix ipv4Prefix) { - final int prefixLength = Byte.valueOf(ipv4Prefix.getValue().split("/")[1]); - return toByteMask(prefixLength); - } - - private static byte[] toMatchValue(final Ipv4Prefix ipv4Prefix) { - final String[] split = ipv4Prefix.getValue().split("/"); - final byte[] addressBytes = ipv4AddressNoZoneToArray(split[0]); - final byte[] mask = toByteMask(Byte.valueOf(split[1])); - for (int i = 0; i < addressBytes.length; ++i) { - addressBytes[i] &= mask[i]; - } - return addressBytes; - } } diff --git a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp6Writer.java b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp6Writer.java new file mode 100644 index 000000000..c46f2e19d --- /dev/null +++ b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp6Writer.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.translate.v3po.interfaces.acl; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.annotations.VisibleForTesting; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.BitSet; +import javax.annotation.Nonnull; +import org.apache.commons.lang3.builder.ReflectionToStringBuilder; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.actions.PacketHandling; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.matches.ace.type.AceIp; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.matches.ace.type.ace.ip.ace.ip.version.AceIpv6; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Ipv6Prefix; +import org.openvpp.jvpp.core.dto.ClassifyAddDelSession; +import org.openvpp.jvpp.core.dto.ClassifyAddDelTable; +import org.openvpp.jvpp.core.dto.InputAclSetInterface; +import org.openvpp.jvpp.core.future.FutureJVppCore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +final class AceIp6Writer extends AbstractAceWriter { + + @VisibleForTesting + static final int MATCH_N_VECTORS = 4; // number of 16B vectors + private static final Logger LOG = LoggerFactory.getLogger(AceIp6Writer.class); + private static final int TABLE_MASK_LENGTH = 64; + private static final int IP6_MASK_BIT_LENGTH = 128; + + private static final int IP_VERSION_OFFSET = 14; // first 14 bytes represent L2 header (2x6 + etherType(2)) + private static final int IP_VERSION_MASK = 0xf0; + private static final int DSCP_MASK1 = 0x0f; + private static final int DSCP_MASK2 = 0xc0; + private static final int IP6_LEN = 16; + private static final int SRC_IP_OFFSET = IP_VERSION_OFFSET + 8; + private static final int DST_IP_OFFSET = SRC_IP_OFFSET + IP6_LEN; + + public AceIp6Writer(@Nonnull final FutureJVppCore futureJVppCore) { + super(futureJVppCore); + } + + private static byte[] toByteMask(final int prefixLength) { + final BitSet mask = new BitSet(IP6_MASK_BIT_LENGTH); + mask.set(0, prefixLength, true); + if (prefixLength < IP6_MASK_BIT_LENGTH) { + mask.set(prefixLength, IP6_MASK_BIT_LENGTH, false); + } + return mask.toByteArray(); + } + + private static byte[] toByteMask(final Ipv6Prefix ipv6Prefix) { + final int prefixLength = Short.valueOf(ipv6Prefix.getValue().split("/")[1]); + return toByteMask(prefixLength); + } + + private static byte[] toMatchValue(final Ipv6Prefix ipv6Prefix) { + final String[] split = ipv6Prefix.getValue().split("/"); + final byte[] addressBytes; + try { + addressBytes = InetAddress.getByName(split[0]).getAddress(); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("Invalid IP6 address", e); + } + final byte[] mask = toByteMask(Short.valueOf(split[1])); + int pos = 0; + for (; pos < mask.length; ++pos) { + addressBytes[pos] &= mask[pos]; + } + // mask can be shorter that address, so we need to clear rest of the address: + for (; pos < addressBytes.length; ++pos) { + addressBytes[pos] = 0; + } + return addressBytes; + } + + @Override + public ClassifyAddDelTable createClassifyTable(@Nonnull final PacketHandling action, + @Nonnull final AceIp aceIp, + final int nextTableIndex) { + checkArgument(aceIp.getAceIpVersion() instanceof AceIpv6, "Expected AceIpv6 version, but was %", aceIp); + final AceIpv6 ipVersion = (AceIpv6) aceIp.getAceIpVersion(); + + final ClassifyAddDelTable request = createClassifyTable(action, nextTableIndex); + request.skipNVectors = 0; // match entire L2 and L3 header + request.matchNVectors = MATCH_N_VECTORS; + + boolean aceIsEmpty = true; + request.mask = new byte[TABLE_MASK_LENGTH]; + if (aceIp.getProtocol() != null) { + request.mask[IP_VERSION_OFFSET] |= IP_VERSION_MASK; + } + + if (aceIp.getDscp() != null) { + aceIsEmpty = false; + // DCSP (bits 4-9 of IP6 header) + request.mask[IP_VERSION_OFFSET] |= DSCP_MASK1; + request.mask[IP_VERSION_OFFSET + 1] |= DSCP_MASK2; + } + + if (aceIp.getSourcePortRange() != null) { + LOG.warn("L4 Header fields are not supported. Ignoring {}", aceIp.getSourcePortRange()); + } + + if (aceIp.getDestinationPortRange() != null) { + LOG.warn("L4 Header fields are not supported. Ignoring {}", aceIp.getDestinationPortRange()); + } + + if (ipVersion.getFlowLabel() != null) { + aceIsEmpty = false; + // bits 12-31 + request.mask[IP_VERSION_OFFSET + 1] |= (byte) 0x0f; + request.mask[IP_VERSION_OFFSET + 2] = (byte) 0xff; + request.mask[IP_VERSION_OFFSET + 3] = (byte) 0xff; + } + + if (ipVersion.getSourceIpv6Network() != null) { + aceIsEmpty = false; + final byte[] mask = toByteMask(ipVersion.getSourceIpv6Network()); + System.arraycopy(mask, 0, request.mask, SRC_IP_OFFSET, mask.length); + } + + if (ipVersion.getDestinationIpv6Network() != null) { + aceIsEmpty = false; + final byte[] mask = toByteMask(ipVersion.getDestinationIpv6Network()); + System.arraycopy(mask, 0, request.mask, DST_IP_OFFSET, mask.length); + } + + if (aceIsEmpty) { + throw new IllegalArgumentException( + String.format("Ace %s does not define packet field match values", aceIp.toString())); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("ACE action={}, rule={} translated to table={}.", action, aceIp, + ReflectionToStringBuilder.toString(request)); + } + + return request; + } + + @Override + public ClassifyAddDelSession createClassifySession(@Nonnull final PacketHandling action, + @Nonnull final AceIp aceIp, + final int tableIndex) { + checkArgument(aceIp.getAceIpVersion() instanceof AceIpv6, "Expected AceIpv6 version, but was %", aceIp); + final AceIpv6 ipVersion = (AceIpv6) aceIp.getAceIpVersion(); + + final ClassifyAddDelSession request = createClassifySession(action, tableIndex); + request.match = new byte[TABLE_MASK_LENGTH]; + boolean noMatch = true; + + if (aceIp.getProtocol() != null) { + request.match[IP_VERSION_OFFSET] |= (byte) (IP_VERSION_MASK & (aceIp.getProtocol().intValue() << 4)); + } + + if (aceIp.getDscp() != null) { + noMatch = false; + final int dscp = aceIp.getDscp().getValue(); + // set bits 4-9 of IP6 header: + request.match[IP_VERSION_OFFSET] |= (byte) (DSCP_MASK1 & (dscp >> 2)); + request.match[IP_VERSION_OFFSET + 1] |= (byte) (DSCP_MASK2 & (dscp << 6)); + } + + if (aceIp.getSourcePortRange() != null) { + LOG.warn("L4 Header fields are not supported. Ignoring {}", aceIp.getSourcePortRange()); + } + + if (aceIp.getDestinationPortRange() != null) { + LOG.warn("L4 Header fields are not supported. Ignoring {}", aceIp.getDestinationPortRange()); + } + + if (ipVersion.getFlowLabel() != null) { + noMatch = false; + final int flowLabel = ipVersion.getFlowLabel().getValue().intValue(); + // bits 12-31 + request.match[IP_VERSION_OFFSET + 1] |= (byte) (0x0f & (flowLabel >> 16)); + request.match[IP_VERSION_OFFSET + 2] = (byte) (0xff & (flowLabel >> 8)); + request.match[IP_VERSION_OFFSET + 3] = (byte) (0xff & flowLabel); + } + + if (ipVersion.getSourceIpv6Network() != null) { + noMatch = false; + final byte[] match = toMatchValue(ipVersion.getSourceIpv6Network()); + System.arraycopy(match, 0, request.match, SRC_IP_OFFSET, IP6_LEN); + } + + if (ipVersion.getDestinationIpv6Network() != null) { + noMatch = false; + final byte[] match = toMatchValue(ipVersion.getDestinationIpv6Network()); + System.arraycopy(match, 0, request.match, DST_IP_OFFSET, IP6_LEN); + } + + if (noMatch) { + throw new IllegalArgumentException( + String.format("Ace %s does not define packet field match values", aceIp.toString())); + } + + if (LOG.isDebugEnabled()) { + LOG.debug("ACE action={}, rule={} translated to session={}.", action, aceIp, + ReflectionToStringBuilder.toString(request)); + } + return request; + } + + @Override + protected void setClassifyTable(@Nonnull final InputAclSetInterface request, final int tableIndex) { + request.ip6TableIndex = tableIndex; + } +} diff --git a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceWriter.java b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceWriter.java index be3889fb0..2d66619fd 100644 --- a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceWriter.java +++ b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceWriter.java @@ -24,7 +24,20 @@ import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; import org.openvpp.jvpp.VppBaseCallException; import org.openvpp.jvpp.core.dto.InputAclSetInterface; +/** + * Writer responsible for translation of ietf-acl model ACEs to VPP's classify tables and sessions. + */ interface AceWriter { + + /** + * Translates list of ACEs to chain of classify tables. Each ACE is translated into one classify table with single + * classify session. Also initializes input_acl_set_interface request message DTO with first classify table of the + * chain that was created. + * + * @param id uniquely identifies ietf-acl container + * @param aces list of access control entries + * @param request input_acl_set_interface request DTO + */ void write(@Nonnull final InstanceIdentifier id, @Nonnull final List aces, @Nonnull final InputAclSetInterface request) throws VppBaseCallException, WriteTimeoutException; } diff --git a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AclWriter.java b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AclWriter.java new file mode 100644 index 000000000..875e9dbae --- /dev/null +++ b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AclWriter.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.translate.v3po.interfaces.acl; + +import io.fd.honeycomb.translate.spi.write.ListWriterCustomizer; +import io.fd.honeycomb.translate.write.WriteContext; +import io.fd.honeycomb.translate.write.WriteFailedException; +import java.util.Optional; +import java.util.stream.Stream; +import javax.annotation.Nonnull; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.AccessLists; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.AclBase; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.Acl; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.AclKey; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.interfaces.rev140508.Interfaces; +import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.v3po.rev150105.VppInterfaceAugmentation; +import org.opendaylight.yangtools.yang.binding.InstanceIdentifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Writer customizer responsible for Access Control Lists management. Does not send any messages to VPP. All the config + * data are stored in HC and used when acl is assigned/unassigned to/from an interface. + * + * ACLs that are currently assigned to an interface cannot be updated/deleted. + */ +public class AclWriter implements ListWriterCustomizer { + + public static final InstanceIdentifier ACL_ID = + InstanceIdentifier.create(AccessLists.class); + + private static final Logger LOG = LoggerFactory.getLogger(AclWriter.class); + + @Override + public void writeCurrentAttributes(@Nonnull final InstanceIdentifier id, @Nonnull final Acl dataAfter, + @Nonnull final WriteContext writeContext) throws WriteFailedException { + LOG.debug("Creating ACL: iid={} dataAfter={}", id, dataAfter); + + // no vpp call, just updates DataTree + } + + @Override + public void updateCurrentAttributes(@Nonnull final InstanceIdentifier id, @Nonnull final Acl dataBefore, + @Nonnull final Acl dataAfter, @Nonnull final WriteContext writeContext) + throws WriteFailedException { + LOG.debug("Updating ACL: iid={} dataBefore={} dataAfter={}", id, dataBefore, dataAfter); + + if (isAssigned(dataAfter, writeContext)) { + throw new WriteFailedException(id, + String.format("Failed to update data at %s: acl %s is already assigned", id, dataAfter)); + } + + LOG.debug("Updating unassigned ACL: iid={} dataBefore={} dataAfter={}", id, dataBefore, dataAfter); + + // no vpp call, just updates DataTree + } + + @Override + public void deleteCurrentAttributes(@Nonnull final InstanceIdentifier id, @Nonnull final Acl dataBefore, + @Nonnull final WriteContext writeContext) throws WriteFailedException { + LOG.debug("Deleting ACL: iid={} dataBefore={}", id, dataBefore); + + if (isAssigned(dataBefore, writeContext)) { + throw new WriteFailedException(id, + String.format("Failed to delete data at %s: acl %s is already assigned", id, dataBefore)); + } + + LOG.debug("Deleting unassigned ACL: iid={} dataBefore={}", id, dataBefore); + + // no vpp call, just updates DataTree + } + + private static boolean isAssigned(@Nonnull final Acl acl, + @Nonnull final WriteContext writeContext) { + final String aclName = acl.getAclName(); + final Class aclType = acl.getAclType(); + final Interfaces interfaces = writeContext.readAfter(InstanceIdentifier.create(Interfaces.class)).get(); + + return interfaces.getInterface().stream() + .map(i -> Optional.ofNullable(i.getAugmentation(VppInterfaceAugmentation.class)) + .map(aug -> aug.getIetfAcl()) + .map(ietfAcl -> ietfAcl.getAccessLists()) + .map(accessLists -> accessLists.getAcl()) + ) + .flatMap(iacl -> iacl.isPresent() + ? iacl.get().stream() + : Stream.empty()) + .filter(assignedAcl -> aclName.equals(assignedAcl.getName()) && aclType.equals(assignedAcl.getType())) + .findFirst().isPresent(); + } +} diff --git a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/IetfAClWriter.java b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/IetfAClWriter.java index 58741cfeb..f4ba56d45 100644 --- a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/IetfAClWriter.java +++ b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/IetfAClWriter.java @@ -20,7 +20,6 @@ import static com.google.common.base.Preconditions.checkArgument; import com.google.common.base.Optional; import com.google.common.base.Preconditions; -import io.fd.honeycomb.translate.v3po.acl.AclWriter; import io.fd.honeycomb.translate.v3po.util.TranslateUtils; import io.fd.honeycomb.translate.v3po.util.WriteTimeoutException; import io.fd.honeycomb.translate.write.WriteContext; @@ -65,13 +64,33 @@ public final class IetfAClWriter { this.jvpp = Preconditions.checkNotNull(futureJVppCore, "futureJVppCore should not be null"); aceWriters.put(AclType.ETH, new AceEthWriter(futureJVppCore)); aceWriters.put(AclType.IP4, new AceIp4Writer(futureJVppCore)); + aceWriters.put(AclType.IP6, new AceIp6Writer(futureJVppCore)); + } + + private static Stream aclToAceStream(@Nonnull final Acl assignedAcl, + @Nonnull final WriteContext writeContext) { + final String aclName = assignedAcl.getName(); + final Class aclType = assignedAcl.getType(); + + // ietf-acl updates are handled first, so we use writeContext.readAfter + final Optional + aclOptional = writeContext.readAfter(AclWriter.ACL_ID.child( + org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.Acl.class, + new AclKey(aclName, aclType))); + checkArgument(aclOptional.isPresent(), "Acl lists not configured"); + final org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.Acl + acl = aclOptional.get(); + + final AccessListEntries accessListEntries = acl.getAccessListEntries(); + checkArgument(accessListEntries != null, "access list entries not configured"); + + return accessListEntries.getAce().stream(); } void deleteAcl(@Nonnull final InstanceIdentifier id, final int swIfIndex) throws WriteTimeoutException, WriteFailedException.DeleteFailedException { final ClassifyTableByInterface request = new ClassifyTableByInterface(); request.swIfIndex = swIfIndex; - jvpp.classifyTableByInterface(request); try { final CompletionStage cs = jvpp.classifyTableByInterface(request); @@ -138,26 +157,6 @@ public final class IetfAClWriter { } - private static Stream aclToAceStream(@Nonnull final Acl assignedAcl, - @Nonnull final WriteContext writeContext) { - final String aclName = assignedAcl.getName(); - final Class aclType = assignedAcl.getType(); - - // ietf-acl updates are handled first, so we use writeContext.readAfter - final Optional - aclOptional = writeContext.readAfter(AclWriter.ACL_ID.child( - org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.Acl.class, - new AclKey(aclName, aclType))); - checkArgument(aclOptional.isPresent(), "Acl lists not configured"); - final org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.Acl - acl = aclOptional.get(); - - final AccessListEntries accessListEntries = acl.getAccessListEntries(); - checkArgument(accessListEntries != null, "access list entries not configured"); - - return accessListEntries.getAce().stream(); - } - private enum AclType { ETH, IP4, IP6; diff --git a/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/Readme.adoc b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/Readme.adoc new file mode 100644 index 000000000..e59f72abe --- /dev/null +++ b/v3po/v3po2vpp/src/main/java/io/fd/honeycomb/translate/v3po/interfaces/acl/Readme.adoc @@ -0,0 +1,32 @@ += VPP to IETF-ACL model translation + +Package provides VPP translation code for draft-ietf-netmod-acl-model-08. +Access control lists are mapped to chains of classify tables, each with single classify session. + +== Available operations + +=== Configuration data +Configuration data for the model is stored in Honeycomb. Corresponding classify tables and sessions +are not created until control access list is assigned to an interface. + +Classify tables and sessions are removed from VPP when ACL assignment is deleted. + +ACLs can be shared among interfaces, but each time, new instance of classify table chain would be created in VPP. + +ACLs that are assigned to an interface have to be unassigned before update/removal. + +=== Operational state +Operational read in terms of ietf-acl model is not supported (would require storing additional metadata in vpp). +As a consequence, configuration data initialization based on operational state is not possible. + +To check how ietf-acl model was translated to classify tables/session, low-level vpp-classfier model can be used. + +== Restrictions + +VPP classfier works in form of offsets and masks of 16B units. +The offset always starts at the beginning of L2 Ethernet header +of input packet. Because IP header can have variable length, +source/destination port matching (L4 features of ietf-acl model) is not possible. + +Current implementation also assumes constant Ethernet header size +(802.1Q headers are not supported). \ No newline at end of file diff --git a/v3po/v3po2vpp/src/test/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceEthWriterTest.java b/v3po/v3po2vpp/src/test/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceEthWriterTest.java new file mode 100644 index 000000000..a32659a78 --- /dev/null +++ b/v3po/v3po2vpp/src/test/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceEthWriterTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.translate.v3po.interfaces.acl; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.mockito.MockitoAnnotations.initMocks; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.actions.PacketHandling; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.actions.packet.handling.DenyBuilder; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.matches.ace.type.AceEth; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.matches.ace.type.AceEthBuilder; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.types.rev130715.MacAddress; +import org.openvpp.jvpp.core.dto.ClassifyAddDelSession; +import org.openvpp.jvpp.core.dto.ClassifyAddDelTable; +import org.openvpp.jvpp.core.dto.InputAclSetInterface; +import org.openvpp.jvpp.core.future.FutureJVppCore; + +public class AceEthWriterTest { + + @Mock + private FutureJVppCore jvpp; + private AceEthWriter writer; + private PacketHandling action; + private AceEth aceEth; + + @Before + public void setUp() throws Exception { + initMocks(this); + writer = new AceEthWriter(jvpp); + action = new DenyBuilder().setDeny(true).build(); + aceEth = new AceEthBuilder() + .setDestinationMacAddress(new MacAddress("11:22:33:44:55:66")) + .setDestinationMacAddressMask(new MacAddress("ff:ff:ff:ff:ff:ff")) + .setSourceMacAddress(new MacAddress("aa:bb:cc:dd:ee:ff")) + .setSourceMacAddressMask(new MacAddress("ff:ff:ff:00:00:00")) + .build(); + } + + @Test + public void testGetClassifyAddDelTableRequest() throws Exception { + final int nextTableIndex = 42; + final ClassifyAddDelTable request = writer.createClassifyTable(action, aceEth, nextTableIndex); + + assertEquals(1, request.isAdd); + assertEquals(-1, request.tableIndex); + assertEquals(1, request.nbuckets); + assertEquals(-1, request.missNextIndex); + assertEquals(nextTableIndex, request.nextTableIndex); + assertEquals(0, request.skipNVectors); + assertEquals(AceEthWriter.MATCH_N_VECTORS, request.matchNVectors); + assertEquals(AceEthWriter.TABLE_MEM_SIZE, request.memorySize); + + byte[] expectedMask = new byte[] { + // destination MAC: + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // source MAC: + (byte) 0xff, (byte) 0xff, (byte) 0xff, 0, 0, 0, + 0, 0, 0, 0 + }; + assertArrayEquals(expectedMask, request.mask); + } + + @Test + public void testGetClassifyAddDelSessionRequest() throws Exception { + final int tableIndex = 123; + final ClassifyAddDelSession request = writer.createClassifySession(action, aceEth, tableIndex); + + assertEquals(1, request.isAdd); + assertEquals(tableIndex, request.tableIndex); + assertEquals(0, request.hitNextIndex); + + byte[] expectedMatch = new byte[] { + // destination MAC: + (byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, (byte) 0x55, (byte) 0x66, + // source MAC: + (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, (byte) 0xdd, (byte) 0xee, (byte) 0xff, + 0, 0, 0, 0 + }; + assertArrayEquals(expectedMatch, request.match); + } + + @Test + public void testSetClassifyTable() throws Exception { + final int tableIndex = 321; + final InputAclSetInterface request = new InputAclSetInterface(); + writer.setClassifyTable(request, tableIndex); + assertEquals(tableIndex, request.l2TableIndex); + } +} \ No newline at end of file diff --git a/v3po/v3po2vpp/src/test/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp4WriterTest.java b/v3po/v3po2vpp/src/test/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp4WriterTest.java index 5be0ea069..95b8fc591 100644 --- a/v3po/v3po2vpp/src/test/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp4WriterTest.java +++ b/v3po/v3po2vpp/src/test/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp4WriterTest.java @@ -36,6 +36,7 @@ import org.openvpp.jvpp.core.dto.InputAclSetInterface; import org.openvpp.jvpp.core.future.FutureJVppCore; public class AceIp4WriterTest { + @Mock private FutureJVppCore jvpp; private AceIp4Writer writer; @@ -60,7 +61,7 @@ public class AceIp4WriterTest { @Test public void testGetClassifyAddDelTableRequest() throws Exception { final int nextTableIndex = 42; - final ClassifyAddDelTable request = writer.getClassifyAddDelTableRequest(action, aceIp, nextTableIndex); + final ClassifyAddDelTable request = writer.createClassifyTable(action, aceIp, nextTableIndex); assertEquals(1, request.isAdd); assertEquals(-1, request.tableIndex); @@ -82,7 +83,7 @@ public class AceIp4WriterTest { @Test public void testGetClassifyAddDelSessionRequest() throws Exception { final int tableIndex = 123; - final ClassifyAddDelSession request = writer.getClassifyAddDelSessionRequest(action, aceIp, tableIndex); + final ClassifyAddDelSession request = writer.createClassifySession(action, aceIp, tableIndex); assertEquals(1, request.isAdd); assertEquals(tableIndex, request.tableIndex); diff --git a/v3po/v3po2vpp/src/test/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp6WriterTest.java b/v3po/v3po2vpp/src/test/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp6WriterTest.java new file mode 100644 index 000000000..181846830 --- /dev/null +++ b/v3po/v3po2vpp/src/test/java/io/fd/honeycomb/translate/v3po/interfaces/acl/AceIp6WriterTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2016 Cisco and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.fd.honeycomb.translate.v3po.interfaces.acl; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.mockito.MockitoAnnotations.initMocks; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.actions.PacketHandling; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.actions.packet.handling.DenyBuilder; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.matches.ace.type.AceIp; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.matches.ace.type.AceIpBuilder; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.access.control.list.rev160708.access.lists.acl.access.list.entries.ace.matches.ace.type.ace.ip.ace.ip.version.AceIpv6Builder; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Dscp; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Ipv6FlowLabel; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Ipv6Prefix; +import org.openvpp.jvpp.core.dto.ClassifyAddDelSession; +import org.openvpp.jvpp.core.dto.ClassifyAddDelTable; +import org.openvpp.jvpp.core.dto.InputAclSetInterface; +import org.openvpp.jvpp.core.future.FutureJVppCore; + +public class AceIp6WriterTest { + + @Mock + private FutureJVppCore jvpp; + private AceIp6Writer writer; + private PacketHandling action; + private AceIp aceIp; + + @Before + public void setUp() throws Exception { + initMocks(this); + writer = new AceIp6Writer(jvpp); + action = new DenyBuilder().setDeny(true).build(); + aceIp = new AceIpBuilder() + .setProtocol((short) 6) + .setDscp(new Dscp((short) 11)) + .setAceIpVersion(new AceIpv6Builder() + .setFlowLabel(new Ipv6FlowLabel(123L)) + .setSourceIpv6Network(new Ipv6Prefix("2001:db8:85a3:8d3:1319:8a2e:370:7348/128")) + .setDestinationIpv6Network(new Ipv6Prefix("fe80:1234:5678:abcd:ef01::/64")) + .build()) + .build(); + } + + @Test + public void testGetClassifyAddDelTableRequest() throws Exception { + final int nextTableIndex = 42; + final ClassifyAddDelTable request = writer.createClassifyTable(action, aceIp, nextTableIndex); + + assertEquals(1, request.isAdd); + assertEquals(-1, request.tableIndex); + assertEquals(1, request.nbuckets); + assertEquals(-1, request.missNextIndex); + assertEquals(nextTableIndex, request.nextTableIndex); + assertEquals(0, request.skipNVectors); + assertEquals(AceIp6Writer.MATCH_N_VECTORS, request.matchNVectors); + assertEquals(AceIp6Writer.TABLE_MEM_SIZE, request.memorySize); + + byte[] expectedMask = new byte[] { + // L2: + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // version, dscp, flow: + (byte) 0xff, (byte) 0xcf, (byte) 0xff, (byte) 0xff, + 0, 0, 0, 0, + // source address: + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + // destination address: + (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff, + 0, 0, 0, 0, 0, 0, 0, 0, + // padding to multiple of 16B: + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + assertArrayEquals(expectedMask, request.mask); + } + + @Test + public void testGetClassifyAddDelSessionRequest() throws Exception { + final int tableIndex = 123; + final ClassifyAddDelSession request = writer.createClassifySession(action, aceIp, tableIndex); + + assertEquals(1, request.isAdd); + assertEquals(tableIndex, request.tableIndex); + assertEquals(0, request.hitNextIndex); + + byte[] expectedMatch = new byte[] { + // L2: + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + // version(6), dscp(11), flow(123): + (byte) 0x62, (byte) 0xc0, (byte) 0x00, (byte) 0x7b, + 0, 0, 0, 0, + // source address: + (byte) 0x20, (byte) 0x01, (byte) 0x0d, (byte) 0xb8, (byte) 0x85, (byte) 0xa3, (byte) 0x08, (byte) 0xd3, + (byte) 0x13, (byte) 0x19, (byte) 0x8a, (byte) 0x2e, (byte) 0x03, (byte) 0x70, (byte) 0x73, (byte) 0x48, + // destination address: + (byte) 0xfe, (byte) 0x80, (byte) 0x12, (byte) 0x34, (byte) 0x56, (byte) 0x78, (byte) 0xab, (byte) 0xcd, + 0, 0, 0, 0, 0, 0, 0, 0, + // padding to multiple of 16B: + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + assertArrayEquals(expectedMatch, request.match); + } + + @Test + public void testSetClassifyTable() throws Exception { + final int tableIndex = 321; + final InputAclSetInterface request = new InputAclSetInterface(); + writer.setClassifyTable(request, tableIndex); + assertEquals(tableIndex, request.ip6TableIndex); + } +} \ No newline at end of file -- cgit 1.2.3-korg