[SDFAB-187] Add UpfProgrammable interface in ONOS core

Change-Id: Icef23a14015bb0ebe33ebe57eadecaaadc8eebd3
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/ForwardingActionRule.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/ForwardingActionRule.java
new file mode 100644
index 0000000..3cd7c8c
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/ForwardingActionRule.java
@@ -0,0 +1,398 @@
+/*
+ * Copyright 2021-present Open Networking Foundation
+ *
+ * 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 org.onosproject.net.behaviour.upf;
+
+import org.onlab.packet.Ip4Address;
+import org.onlab.util.ImmutableByteSequence;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A single Forwarding Action Rule (FAR), an entity described in the 3GPP
+ * specifications (although that does not mean that this class is 3GPP
+ * compliant). An instance of this class will be generated by a logical switch
+ * write request to the database-style FAR P4 table, and the resulting instance
+ * should contain all the information needed to reproduce that logical switch
+ * FAR in the event of a client read request. The instance should also contain
+ * sufficient information (or expose the means to retrieve such information) to
+ * generate the corresponding dataplane forwarding state that implements the FAR.
+ */
+public final class ForwardingActionRule {
+    // Match Keys
+    private final ImmutableByteSequence sessionId;  // The PFCP session identifier that created this FAR
+    private final int farId;  // PFCP session-local identifier for this FAR
+    // Action parameters
+    private final boolean notifyFlag;  // Should this FAR notify the control plane when it sees a packet?
+    private final boolean dropFlag;
+    private final boolean bufferFlag;
+    private final GtpTunnel tunnel;  // The GTP tunnel that this FAR should encapsulate packets with (if downlink)
+
+    private static final int SESSION_ID_BITWIDTH = 96;
+
+    private ForwardingActionRule(ImmutableByteSequence sessionId, Integer farId,
+                                 boolean notifyFlag, GtpTunnel tunnel, boolean dropFlag, boolean bufferFlag) {
+        this.sessionId = sessionId;
+        this.farId = farId;
+        this.notifyFlag = notifyFlag;
+        this.tunnel = tunnel;
+        this.dropFlag = dropFlag;
+        this.bufferFlag = bufferFlag;
+    }
+
+    /**
+     * Return a new instance of this FAR with the action parameters stripped, leaving only the match keys.
+     *
+     * @return a new FAR with only match keys
+     */
+    public ForwardingActionRule withoutActionParams() {
+        return ForwardingActionRule.builder()
+                .setFarId(farId)
+                .withSessionId(sessionId)
+                .build();
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Return a string representing the dataplane action applied by this FAR.
+     *
+     * @return a string representing the FAR action
+     */
+    public String actionString() {
+        String actionName;
+        String actionParams = "";
+        if (dropFlag) {
+            actionName = "Drop";
+        } else if (bufferFlag) {
+            actionName = "Buffer";
+        } else if (tunnel != null) {
+            actionName = "Encap";
+            actionParams = String.format("Src=%s, SPort=%d, TEID=%s, Dst=%s",
+                    tunnel.src().toString(), tunnel.srcPort(), tunnel.teid().toString(), tunnel.dst().toString());
+        } else {
+            actionName = "Forward";
+        }
+        if (notifyFlag) {
+            actionName += "+NotifyCP";
+        }
+
+        return String.format("%s(%s)", actionName, actionParams);
+    }
+
+    @Override
+    public String toString() {
+        String matchKeys = String.format("ID=%d, SEID=%s", farId, sessionId.toString());
+        String actionString = actionString();
+
+        return String.format("FAR{Match(%s) -> %s}", matchKeys, actionString);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        ForwardingActionRule that = (ForwardingActionRule) obj;
+
+        // Safe comparisons between potentially null objects
+        return (this.dropFlag == that.dropFlag &&
+                this.bufferFlag == that.bufferFlag &&
+                this.notifyFlag == that.notifyFlag &&
+                this.farId == that.farId &&
+                Objects.equals(this.tunnel, that.tunnel) &&
+                Objects.equals(this.sessionId, that.sessionId));
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(sessionId, farId, notifyFlag, tunnel, dropFlag, bufferFlag);
+    }
+
+    /**
+     * Get the ID of the PFCP Session that produced this FAR.
+     *
+     * @return PFCP session ID
+     */
+    public ImmutableByteSequence sessionId() {
+        return sessionId;
+    }
+
+    /**
+     * Get the PFCP session-local ID of the FAR that should apply to packets that match this PDR.
+     *
+     * @return PFCP session-local FAR ID
+     */
+    public int farId() {
+        return farId;
+    }
+
+    /**
+     * True if this FAR does not drop packets.
+     *
+     * @return true if FAR is forwards
+     */
+    public boolean forwards() {
+        return !dropFlag;
+    }
+
+    /**
+     * True if this FAR encapsulates packets in a GTP tunnel, and false otherwise.
+     *
+     * @return true is FAR encapsulates
+     */
+    public boolean encaps() {
+        return tunnel != null;
+    }
+
+    /**
+     * Returns true if this FAR drops packets, and false otherwise.
+     *
+     * @return true if this FAR drops
+     */
+    public boolean drops() {
+        return dropFlag;
+    }
+
+    /**
+     * Returns true if this FAR notifies the control plane on receiving a packet, and false otherwise.
+     *
+     * @return true if this FAR notifies the cp
+     */
+    public boolean notifies() {
+        return notifyFlag;
+    }
+
+
+    /**
+     * Returns true if this FAR buffers incoming packets, and false otherwise.
+     *
+     * @return true if this FAR buffers
+     */
+    public boolean buffers() {
+        return bufferFlag;
+    }
+
+    /**
+     * A description of the tunnel that this FAR will encapsulate packets with, if it is a downlink FAR. If the FAR
+     * is uplink, there will be no such tunnel and this method wil return null.
+     *
+     * @return A GtpTunnel instance containing a tunnel sourceIP, destIP, and GTPU TEID, or null if the FAR is uplink.
+     */
+    public GtpTunnel tunnel() {
+        return tunnel;
+    }
+
+    /**
+     * Get the source UDP port of the GTP tunnel that this FAR will encapsulate packets with.
+     *
+     * @return GTP tunnel source UDP port
+     */
+    public Short tunnelSrcPort() {
+        return tunnel != null ? tunnel.srcPort() : null;
+    }
+
+    /**
+     * Get the source IP of the GTP tunnel that this FAR will encapsulate packets with.
+     *
+     * @return GTP tunnel source IP
+     */
+    public Ip4Address tunnelSrc() {
+        if (tunnel == null) {
+            return null;
+        }
+        return tunnel.src();
+    }
+
+    /**
+     * Get the destination IP of the GTP tunnel that this FAR will encapsulate packets with.
+     *
+     * @return GTP tunnel destination IP
+     */
+    public Ip4Address tunnelDst() {
+        if (tunnel == null) {
+            return null;
+        }
+        return tunnel.dst();
+    }
+
+    /**
+     * Get the identifier of the GTP tunnel that this FAR will encapsulate packets with.
+     *
+     * @return GTP tunnel ID
+     */
+    public ImmutableByteSequence teid() {
+        if (tunnel == null) {
+            return null;
+        }
+        return tunnel.teid();
+    }
+
+    public static class Builder {
+        private ImmutableByteSequence sessionId = null;
+        private Integer farId = null;
+        private GtpTunnel tunnel = null;
+        private boolean dropFlag = false;
+        private boolean bufferFlag = false;
+        private boolean notifyCp = false;
+
+        public Builder() {
+        }
+
+        /**
+         * Set the ID of the PFCP session that created this FAR.
+         *
+         * @param sessionId PFC session ID
+         * @return This builder object
+         */
+        public Builder withSessionId(ImmutableByteSequence sessionId) {
+            this.sessionId = sessionId;
+            return this;
+        }
+
+        /**
+         * Set the ID of the PFCP session that created this FAR.
+         *
+         * @param sessionId PFC session ID
+         * @return This builder object
+         */
+        public Builder withSessionId(long sessionId) {
+            try {
+                this.sessionId = ImmutableByteSequence.copyFrom(sessionId).fit(SESSION_ID_BITWIDTH);
+            } catch (ImmutableByteSequence.ByteSequenceTrimException e) {
+                // This error is literally impossible
+            }
+            return this;
+        }
+
+        /**
+         * Set the PFCP Session-local ID of this FAR.
+         *
+         * @param farId PFCP session-local FAR ID
+         * @return This builder object
+         */
+        public Builder setFarId(int farId) {
+            this.farId = farId;
+            return this;
+        }
+
+        /**
+         * Make this FAR forward incoming packets.
+         *
+         * @param flag the flag value to set
+         * @return This builder object
+         */
+        public Builder setForwardFlag(boolean flag) {
+            this.dropFlag = !flag;
+            return this;
+        }
+
+        /**
+         * Make this FAR drop incoming packets.
+         *
+         * @param flag the flag value to set
+         * @return This builder object
+         */
+        public Builder setDropFlag(boolean flag) {
+            this.dropFlag = flag;
+            return this;
+        }
+
+        /**
+         * Make this FAR buffer incoming packets.
+         *
+         * @param flag the flag value to set
+         * @return This builder object
+         */
+        public Builder setBufferFlag(boolean flag) {
+            this.bufferFlag = flag;
+            return this;
+        }
+
+        /**
+         * Set a flag specifying if the control plane should be notified when this FAR is hit.
+         *
+         * @param notifyCp true if FAR notifies control plane
+         * @return This builder object
+         */
+        public Builder setNotifyFlag(boolean notifyCp) {
+            this.notifyCp = notifyCp;
+            return this;
+        }
+
+        /**
+         * Set the GTP tunnel that this FAR should encapsulate packets with.
+         *
+         * @param tunnel GTP tunnel
+         * @return This builder object
+         */
+        public Builder setTunnel(GtpTunnel tunnel) {
+            this.tunnel = tunnel;
+            return this;
+        }
+
+        /**
+         * Set the unidirectional GTP tunnel that this FAR should encapsulate packets with.
+         *
+         * @param src  GTP tunnel source IP
+         * @param dst  GTP tunnel destination IP
+         * @param teid GTP tunnel ID
+         * @return This builder object
+         */
+        public Builder setTunnel(Ip4Address src, Ip4Address dst, ImmutableByteSequence teid) {
+            return this.setTunnel(GtpTunnel.builder()
+                    .setSrc(src)
+                    .setDst(dst)
+                    .setTeid(teid)
+                    .build());
+        }
+
+        /**
+         * Set the unidirectional GTP tunnel that this FAR should encapsulate packets with.
+         *
+         * @param src     GTP tunnel source IP
+         * @param dst     GTP tunnel destination IP
+         * @param teid    GTP tunnel ID
+         * @param srcPort GTP tunnel UDP source port (destination port is hardcoded as 2152)
+         * @return This builder object
+         */
+        public Builder setTunnel(Ip4Address src, Ip4Address dst, ImmutableByteSequence teid, short srcPort) {
+            return this.setTunnel(GtpTunnel.builder()
+                    .setSrc(src)
+                    .setDst(dst)
+                    .setTeid(teid)
+                    .setSrcPort(srcPort)
+                    .build());
+        }
+
+        public ForwardingActionRule build() {
+            // All match keys are required
+            checkNotNull(sessionId, "Session ID is required");
+            checkNotNull(farId, "FAR ID is required");
+            return new ForwardingActionRule(sessionId, farId, notifyCp, tunnel, dropFlag, bufferFlag);
+        }
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/GtpTunnel.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/GtpTunnel.java
new file mode 100644
index 0000000..bc262c7
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/GtpTunnel.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2021-present Open Networking Foundation
+ *
+ * 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 org.onosproject.net.behaviour.upf;
+
+import org.onlab.packet.Ip4Address;
+import org.onlab.util.ImmutableByteSequence;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A structure representing a unidirectional GTP tunnel.
+ */
+public final class GtpTunnel {
+    private final Ip4Address src;  // The source address of the unidirectional tunnel
+    private final Ip4Address dst;  // The destination address of the unidirectional tunnel
+    private final ImmutableByteSequence teid;  // Tunnel Endpoint Identifier
+    private final short srcPort;  // Tunnel destination port, default 2152
+
+    private GtpTunnel(Ip4Address src, Ip4Address dst, ImmutableByteSequence teid,
+                      Short srcPort) {
+        this.src = src;
+        this.dst = dst;
+        this.teid = teid;
+        this.srcPort = srcPort;
+    }
+
+    public static GtpTunnelBuilder builder() {
+        return new GtpTunnelBuilder();
+    }
+
+    @Override
+    public String toString() {
+        return String.format("GTP-Tunnel(%s -> %s, TEID:%s)",
+                             src.toString(), dst.toString(), teid.toString());
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        GtpTunnel that = (GtpTunnel) obj;
+
+        return (this.src.equals(that.src) &&
+                this.dst.equals(that.dst) &&
+                this.teid.equals(that.teid) &&
+                (this.srcPort == that.srcPort));
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(src, dst, teid, srcPort);
+    }
+
+    /**
+     * Get the source IP address of this unidirectional GTP tunnel.
+     *
+     * @return tunnel source IP
+     */
+    public Ip4Address src() {
+        return this.src;
+    }
+
+    /**
+     * Get the destination address of this unidirectional GTP tunnel.
+     *
+     * @return tunnel destination IP
+     */
+    public Ip4Address dst() {
+        return this.dst;
+    }
+
+    /**
+     * Get the ID of this unidirectional GTP tunnel.
+     *
+     * @return tunnel ID
+     */
+    public ImmutableByteSequence teid() {
+        return this.teid;
+    }
+
+
+    /**
+     * Get the source L4 port of this unidirectional GTP tunnel.
+     *
+     * @return tunnel source port
+     */
+    public Short srcPort() {
+        return this.srcPort;
+    }
+
+    public static class GtpTunnelBuilder {
+        private Ip4Address src;
+        private Ip4Address dst;
+        private ImmutableByteSequence teid;
+        private short srcPort = 2152;  // Default value is equal to GTP tunnel dst port
+
+        public GtpTunnelBuilder() {
+            this.src = null;
+            this.dst = null;
+            this.teid = null;
+        }
+
+        /**
+         * Set the source IP address of the unidirectional GTP tunnel.
+         *
+         * @param src GTP tunnel source IP
+         * @return This builder object
+         */
+        public GtpTunnelBuilder setSrc(Ip4Address src) {
+            this.src = src;
+            return this;
+        }
+
+        /**
+         * Set the destination IP address of the unidirectional GTP tunnel.
+         *
+         * @param dst GTP tunnel destination IP
+         * @return This builder object
+         */
+        public GtpTunnelBuilder setDst(Ip4Address dst) {
+            this.dst = dst;
+            return this;
+        }
+
+        /**
+         * Set the identifier of this unidirectional GTP tunnel.
+         *
+         * @param teid tunnel ID
+         * @return This builder object
+         */
+        public GtpTunnelBuilder setTeid(ImmutableByteSequence teid) {
+            this.teid = teid;
+            return this;
+        }
+
+        /**
+         * Set the identifier of this unidirectional GTP tunnel.
+         *
+         * @param teid tunnel ID
+         * @return This builder object
+         */
+        public GtpTunnelBuilder setTeid(long teid) {
+            this.teid = ImmutableByteSequence.copyFrom(teid);
+            return this;
+        }
+
+        /**
+         * Set the source port of this unidirectional GTP tunnel.
+         *
+         * @param srcPort tunnel source port
+         * @return this builder object
+         */
+        public GtpTunnelBuilder setSrcPort(short srcPort) {
+            this.srcPort = srcPort;
+            return this;
+        }
+
+        public GtpTunnel build() {
+            checkNotNull(src, "Tunnel source address cannot be null");
+            checkNotNull(dst, "Tunnel destination address cannot be null");
+            checkNotNull(teid, "Tunnel TEID cannot be null");
+            return new GtpTunnel(this.src, this.dst, this.teid, srcPort);
+        }
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/PacketDetectionRule.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/PacketDetectionRule.java
new file mode 100644
index 0000000..c2069a5
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/PacketDetectionRule.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright 2021-present Open Networking Foundation
+ *
+ * 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 org.onosproject.net.behaviour.upf;
+
+import org.onlab.packet.Ip4Address;
+import org.onlab.util.ImmutableByteSequence;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * A single Packet Detection Rule (PDR), an entity described in the 3GPP
+ * specifications (although that does not mean that this class is 3GPP
+ * compliant). An instance of this class will be generated by a logical switch
+ * write request to the database-style PDR P4 table, and the resulting instance
+ * should contain all the information needed to reproduce that logical switch
+ * PDR in the event of a client read request. The instance should also contain
+ * sufficient information (or expose the means to retrieve such information) to
+ * generate the corresponding dataplane forwarding state that implements the PDR.
+ */
+public final class PacketDetectionRule {
+    // Match keys
+    private final Ip4Address ueAddr;  // The UE IP address that this PDR matches on
+    private final ImmutableByteSequence teid;  // The Tunnel Endpoint ID that this PDR matches on
+    private final Ip4Address tunnelDst;  // The tunnel destination address that this PDR matches on
+    // Action parameters
+    private final ImmutableByteSequence sessionId;  // The ID of the PFCP session that created this PDR
+    private final Integer ctrId;  // Counter ID unique to this PDR
+    private final Integer farId;  // The PFCP session-local ID of the FAR that should apply after this PDR hits
+    private final Type type;
+    private final Integer schedulingPriority;
+
+    private static final int SESSION_ID_BITWIDTH = 96;
+
+    private PacketDetectionRule(ImmutableByteSequence sessionId, Integer ctrId,
+                                Integer farId, Integer schedulingPriority,
+                                Ip4Address ueAddr, ImmutableByteSequence teid,
+                                Ip4Address tunnelDst, Type type) {
+        this.ueAddr = ueAddr;
+        this.teid = teid;
+        this.tunnelDst = tunnelDst;
+        this.sessionId = sessionId;
+        this.ctrId = ctrId;
+        this.farId = farId;
+        this.schedulingPriority = schedulingPriority;
+        this.type = type;
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Return a string representing the match conditions of this PDR.
+     *
+     * @return a string representing the PDR match conditions
+     */
+    public String matchString() {
+        if (matchesEncapped()) {
+            return String.format("Match(Dst=%s, TEID=%s)", tunnelDest(), teid());
+        } else {
+            return String.format("Match(Dst=%s, !GTP)", ueAddress());
+        }
+    }
+
+    @Override
+    public String toString() {
+        String actionParams = "";
+        if (hasActionParameters()) {
+            actionParams = String.format("SEID=%s, FAR=%d, CtrIdx=%d, SchedulingPrio=%d",
+                                         sessionId.toString(), farId, ctrId, schedulingPriority);
+        }
+
+        return String.format("PDR{%s -> LoadParams(%s)}",
+                             matchString(), actionParams);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        PacketDetectionRule that = (PacketDetectionRule) obj;
+
+        // Safe comparisons between potentially null objects
+        return (this.type.equals(that.type) &&
+                Objects.equals(this.teid, that.teid) &&
+                Objects.equals(this.tunnelDst, that.tunnelDst) &&
+                Objects.equals(this.ueAddr, that.ueAddr) &&
+                Objects.equals(this.ctrId, that.ctrId) &&
+                Objects.equals(this.sessionId, that.sessionId) &&
+                Objects.equals(this.farId, that.farId) &&
+                Objects.equals(this.schedulingPriority, that.schedulingPriority));
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(ueAddr, teid, tunnelDst, sessionId, ctrId, farId,
+                            schedulingPriority, type);
+    }
+
+    /**
+     * Instances created as a result of DELETE write requests will not have
+     * action parameters, only match keys. This method should be used to avoid
+     * null pointer exceptions in those instances.
+     *
+     * @return true if this instance has PDR action parameters, false otherwise.
+     */
+    public boolean hasActionParameters() {
+        return type == Type.MATCH_ENCAPPED || type == Type.MATCH_UNENCAPPED;
+    }
+
+    /**
+     * Return a new instance of this PDR with the action parameters stripped,
+     * leaving only the match keys.
+     *
+     * @return a new PDR with only match keys
+     */
+    public PacketDetectionRule withoutActionParams() {
+        if (matchesEncapped()) {
+            return PacketDetectionRule.builder()
+                    .withTeid(teid)
+                    .withTunnelDst(tunnelDst)
+                    .build();
+        } else {
+            return PacketDetectionRule.builder()
+                    .withUeAddr(ueAddr).build();
+        }
+    }
+
+    /**
+     * True if this PDR matches on packets received with a GTP header, and false
+     * otherwise.
+     *
+     * @return true if the PDR matches only encapsulated packets
+     */
+    public boolean matchesEncapped() {
+        return type == Type.MATCH_ENCAPPED ||
+                type == Type.MATCH_ENCAPPED_NO_ACTION;
+    }
+
+    /**
+     * True if this PDR matches on packets received without a GTP header, and
+     * false otherwise.
+     *
+     * @return true if the PDR matches only un-encapsulated packets
+     */
+    public boolean matchesUnencapped() {
+        return type == Type.MATCH_UNENCAPPED ||
+                type == Type.MATCH_UNENCAPPED_NO_ACTION;
+    }
+
+    /**
+     * Get the ID of the PFCP session that produced this PDR.
+     *
+     * @return PFCP session ID
+     */
+    public ImmutableByteSequence sessionId() {
+        return sessionId;
+    }
+
+    /**
+     * Get the UE IP address that this PDR matches on.
+     *
+     * @return UE IP address
+     */
+    public Ip4Address ueAddress() {
+        return ueAddr;
+    }
+
+    /**
+     * Get the identifier of the GTP tunnel that this PDR matches on.
+     *
+     * @return GTP tunnel ID
+     */
+    public ImmutableByteSequence teid() {
+        return teid;
+    }
+
+    /**
+     * Get the destination IP of the GTP tunnel that this PDR matches on.
+     *
+     * @return GTP tunnel destination IP
+     */
+    public Ip4Address tunnelDest() {
+        return tunnelDst;
+    }
+
+    /**
+     * Get the dataplane PDR counter cell ID that this PDR is assigned.
+     *
+     * @return PDR counter cell ID
+     */
+    public int counterId() {
+        return ctrId;
+    }
+
+    /**
+     * Get the PFCP session-local ID of the far that should apply to packets
+     * that this PDR matches.
+     *
+     * @return PFCP session-local FAR ID
+     */
+    public int farId() {
+        return farId;
+    }
+
+    /**
+     * Get the scheduling priority that this PDR is assigned.
+     *
+     * @return scheduling priority
+     */
+    public int schedulingPriority() {
+        return schedulingPriority;
+    }
+
+    /**
+     * This method is used to differentiate between prioritized and non-prioritized
+     * flows.
+     *
+     * @return true if scheduling priority is assigned.
+     */
+    public boolean hasSchedulingPriority() {
+        return schedulingPriority > 0;
+    }
+
+    private enum Type {
+        /**
+         * Match on packets that are encapsulated in a GTP tunnel.
+         */
+        MATCH_ENCAPPED,
+        /**
+         * Match on packets that are not encapsulated in a GTP tunnel.
+         */
+        MATCH_UNENCAPPED,
+        /**
+         * For PDRs that match on encapsulated packets but do not yet have any
+         * action parameters set.
+         * These are usually built in the context of P4Runtime DELETE write requests.
+         */
+        MATCH_ENCAPPED_NO_ACTION,
+        /**
+         * For PDRs that match on unencapsulated packets but do not yet have any
+         * action parameters set.
+         * These are usually built in the context of P4Runtime DELETE write requests.
+         */
+        MATCH_UNENCAPPED_NO_ACTION
+    }
+
+    public static class Builder {
+        private ImmutableByteSequence sessionId = null;
+        private Integer ctrId = null;
+        private Integer localFarId = null;
+        private Integer schedulingPriority = null;
+        private Ip4Address ueAddr = null;
+        private ImmutableByteSequence teid = null;
+        private Ip4Address tunnelDst = null;
+
+        public Builder() {
+
+        }
+
+        /**
+         * Set the ID of the PFCP session that produced this PDR.
+         *
+         * @param sessionId PFCP session ID
+         * @return This builder object
+         */
+        public Builder withSessionId(ImmutableByteSequence sessionId) {
+            this.sessionId = sessionId;
+            return this;
+        }
+
+        /**
+         * Set the ID of the PFCP session that produced this PDR.
+         *
+         * @param sessionId PFCP session ID
+         * @return This builder object
+         */
+        public Builder withSessionId(long sessionId) {
+            try {
+                this.sessionId = ImmutableByteSequence.copyFrom(sessionId)
+                        .fit(SESSION_ID_BITWIDTH);
+            } catch (ImmutableByteSequence.ByteSequenceTrimException e) {
+                // This error is literally impossible
+            }
+            return this;
+        }
+
+        /**
+         * Set the UE IP address that this PDR matches on.
+         *
+         * @param ueAddr UE IP address
+         * @return This builder object
+         */
+        public Builder withUeAddr(Ip4Address ueAddr) {
+            this.ueAddr = ueAddr;
+            return this;
+        }
+
+        /**
+         * Set the dataplane PDR counter cell ID that this PDR is assigned.
+         *
+         * @param ctrId PDR counter cell ID
+         * @return This builder object
+         */
+        public Builder withCounterId(int ctrId) {
+            this.ctrId = ctrId;
+            return this;
+        }
+
+
+        /**
+         * Set the PFCP session-local ID of the far that should apply to packets that this PDR matches.
+         *
+         * @param localFarId PFCP session-local FAR ID
+         * @return This builder object
+         */
+        public Builder withLocalFarId(int localFarId) {
+            this.localFarId = localFarId;
+            return this;
+        }
+
+        public Builder withSchedulingPriority(int schedulingPriority) {
+            this.schedulingPriority = schedulingPriority;
+            return this;
+        }
+
+        /**
+         * Set the identifier of the GTP tunnel that this PDR matches on.
+         *
+         * @param teid GTP tunnel ID
+         * @return This builder object
+         */
+        public Builder withTeid(int teid) {
+            this.teid = ImmutableByteSequence.copyFrom(teid);
+            return this;
+        }
+
+        /**
+         * Set the identifier of the GTP tunnel that this PDR matches on.
+         *
+         * @param teid GTP tunnel ID
+         * @return This builder object
+         */
+        public Builder withTeid(ImmutableByteSequence teid) {
+            this.teid = teid;
+            return this;
+        }
+
+        /**
+         * Set the destination IP of the GTP tunnel that this PDR matches on.
+         *
+         * @param tunnelDst GTP tunnel destination IP
+         * @return This builder object
+         */
+        public Builder withTunnelDst(Ip4Address tunnelDst) {
+            this.tunnelDst = tunnelDst;
+            return this;
+        }
+
+        /**
+         * Set the tunnel ID and destination IP of the GTP tunnel that this PDR matches on.
+         *
+         * @param teid      GTP tunnel ID
+         * @param tunnelDst GTP tunnel destination IP
+         * @return This builder object
+         */
+        public Builder withTunnel(ImmutableByteSequence teid, Ip4Address tunnelDst) {
+            this.teid = teid;
+            this.tunnelDst = tunnelDst;
+            return this;
+        }
+
+        public PacketDetectionRule build() {
+            // Some match keys are required.
+            checkArgument(
+                    (ueAddr != null && teid == null && tunnelDst == null) ||
+                            (ueAddr == null && teid != null && tunnelDst != null),
+                    "Either a UE address or a TEID and Tunnel destination must be provided, but not both.");
+            // Action parameters are optional but must be all provided together if they are provided
+            checkArgument(
+                    (sessionId != null && ctrId != null && localFarId != null && schedulingPriority != null) ||
+                            (sessionId == null && ctrId == null && localFarId == null && schedulingPriority == null),
+                    "PDR action parameters must be provided together or not at all.");
+            Type type;
+            if (teid != null) {
+                if (sessionId != null) {
+                    type = Type.MATCH_ENCAPPED;
+                } else {
+                    type = Type.MATCH_ENCAPPED_NO_ACTION;
+                }
+            } else {
+                if (sessionId != null) {
+                    type = Type.MATCH_UNENCAPPED;
+                } else {
+                    type = Type.MATCH_UNENCAPPED_NO_ACTION;
+                }
+            }
+            return new PacketDetectionRule(sessionId, ctrId, localFarId, schedulingPriority,
+                                           ueAddr, teid, tunnelDst, type);
+        }
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/PdrStats.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/PdrStats.java
new file mode 100644
index 0000000..eb4ea18
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/PdrStats.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2021-present Open Networking Foundation
+ *
+ * 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 org.onosproject.net.behaviour.upf;
+
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A structure for compactly passing PDR counter values for a given counter ID.
+ * Contains four counts: Ingress Packets, Ingress Bytes, Egress Packets, Egress Bytes
+ */
+public final class PdrStats {
+    private final int cellId;
+    private final long ingressPkts;
+    private final long ingressBytes;
+    private final long egressPkts;
+    private final long egressBytes;
+
+    private PdrStats(int cellId, long ingressPkts, long ingressBytes,
+                     long egressPkts, long egressBytes) {
+        this.cellId = cellId;
+        this.ingressPkts = ingressPkts;
+        this.ingressBytes = ingressBytes;
+        this.egressPkts = egressPkts;
+        this.egressBytes = egressBytes;
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @Override
+    public String toString() {
+        return String.format("PDR-Stats:{ CellID: %d, Ingress:(%dpkts,%dbytes), Egress:(%dpkts,%dbytes) }",
+                cellId, ingressPkts, ingressBytes, egressPkts, egressBytes);
+    }
+
+    /**
+     * Get the cell ID (index) of the dataplane PDR counter that produced this set of stats.
+     *
+     * @return counter cell ID
+     */
+    public int getCellId() {
+        return cellId;
+    }
+
+    /**
+     * Get the number of packets that hit this counter in the dataplane ingress pipeline.
+     *
+     * @return ingress packet count
+     */
+    public long getIngressPkts() {
+        return ingressPkts;
+    }
+
+    /**
+     * Get the number of packets that hit this counter in the dataplane egress pipeline.
+     *
+     * @return egress packet count
+     */
+    public long getEgressPkts() {
+        return egressPkts;
+    }
+
+    /**
+     * Get the number of packet bytes that hit this counter in the dataplane ingress pipeline.
+     *
+     * @return ingress byte count
+     */
+    public long getIngressBytes() {
+        return ingressBytes;
+    }
+
+    /**
+     * Get the number of packet bytes that hit this counter in the dataplane egress pipeline.
+     *
+     * @return egress byte count
+     */
+    public long getEgressBytes() {
+        return egressBytes;
+    }
+
+    public static class Builder {
+        private Integer cellId;
+        private long ingressPkts;
+        private long ingressBytes;
+        private long egressPkts;
+        private long egressBytes;
+
+        public Builder() {
+            this.ingressPkts = 0;
+            this.ingressBytes = 0;
+            this.egressPkts = 0;
+            this.egressBytes = 0;
+        }
+
+        /**
+         * Set the Cell ID (index) of the datalane PDR counter that produced this set of stats.
+         *
+         * @param cellId the counter cell ID
+         * @return This builder
+         */
+        public Builder withCellId(int cellId) {
+            this.cellId = cellId;
+            return this;
+        }
+
+        /**
+         * Set the number of packets and bytes that hit the PDR counter in the dataplane ingress pipeline.
+         *
+         * @param ingressPkts  ingress packet count
+         * @param ingressBytes egress packet count
+         * @return This builder
+         */
+        public Builder setIngress(long ingressPkts, long ingressBytes) {
+            this.ingressPkts = ingressPkts;
+            this.ingressBytes = ingressBytes;
+            return this;
+        }
+
+        /**
+         * Set the number of packets and bytes that hit the PDR counter in the dataplane egress pipeline.
+         *
+         * @param egressPkts  egress packet count
+         * @param egressBytes egress byte count
+         * @return This builder
+         */
+        public Builder setEgress(long egressPkts, long egressBytes) {
+            this.egressPkts = egressPkts;
+            this.egressBytes = egressBytes;
+            return this;
+        }
+
+        public PdrStats build() {
+            checkNotNull(cellId, "CellID must be provided");
+            return new PdrStats(cellId, ingressPkts, ingressBytes, egressPkts, egressBytes);
+        }
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfDevice.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfDevice.java
new file mode 100644
index 0000000..ebb9f68
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfDevice.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2021-present Open Networking Foundation
+ *
+ * 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 org.onosproject.net.behaviour.upf;
+
+import com.google.common.annotations.Beta;
+import org.onlab.packet.Ip4Address;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+
+/**
+ * Provides means to update forwarding state to implement a 3GPP User Plane Function.
+ */
+@Beta
+public interface UpfDevice {
+
+    /**
+     * Remove any state previously created by this API.
+     */
+    void cleanUp();
+
+    /**
+     * Remove all interfaces currently installed on the UPF-programmable device.
+     */
+    void clearInterfaces();
+
+    /**
+     * Remove all UE flows (PDRs, FARs) currently installed on the UPF-programmable device.
+     */
+    void clearFlows();
+
+    /**
+     * Get all ForwardingActionRules currently installed on the UPF-programmable device.
+     *
+     * @return a collection of installed FARs
+     * @throws UpfProgrammableException if FARs are unable to be read
+     */
+    Collection<ForwardingActionRule> getFars() throws UpfProgrammableException;
+
+    /**
+     * Get all PacketDetectionRules currently installed on the UPF-programmable device.
+     *
+     * @return a collection of installed PDRs
+     * @throws UpfProgrammableException if PDRs are unable to be read
+     */
+    Collection<PacketDetectionRule> getPdrs() throws UpfProgrammableException;
+
+    /**
+     * Get all UPF interface lookup entries currently installed on the UPF-programmable device.
+     *
+     * @return a collection of installed interfaces
+     * @throws UpfProgrammableException if interfaces are unable to be read
+     */
+    Collection<UpfInterface> getInterfaces() throws UpfProgrammableException;
+
+    /**
+     * Add a Packet Detection Rule (PDR) to the given device.
+     *
+     * @param pdr The PDR to be added
+     * @throws UpfProgrammableException if the PDR cannot be installed, or the counter index is out
+     *                                  of bounds
+     */
+    void addPdr(PacketDetectionRule pdr) throws UpfProgrammableException;
+
+    /**
+     * Remove a previously installed Packet Detection Rule (PDR) from the target device.
+     *
+     * @param pdr The PDR to be removed
+     * @throws UpfProgrammableException if the PDR cannot be found
+     */
+    void removePdr(PacketDetectionRule pdr) throws UpfProgrammableException;
+
+    /**
+     * Add a Forwarding Action Rule (FAR) to the given device.
+     *
+     * @param far The FAR to be added
+     * @throws UpfProgrammableException if the FAR cannot be installed
+     */
+    void addFar(ForwardingActionRule far) throws UpfProgrammableException;
+
+    /**
+     * Remove a previously installed Forwarding Action Rule (FAR) from the target device.
+     *
+     * @param far The FAR to be removed
+     * @throws UpfProgrammableException if the FAR cannot be found
+     */
+    void removeFar(ForwardingActionRule far) throws UpfProgrammableException;
+
+    /**
+     * Install a new interface on the UPF device's interface lookup tables.
+     *
+     * @param upfInterface the interface to install
+     * @throws UpfProgrammableException if the interface cannot be installed
+     */
+    void addInterface(UpfInterface upfInterface) throws UpfProgrammableException;
+
+    /**
+     * Remove a previously installed UPF interface from the target device.
+     *
+     * @param upfInterface the interface to be removed
+     * @throws UpfProgrammableException if the interface cannot be found
+     */
+    void removeInterface(UpfInterface upfInterface) throws UpfProgrammableException;
+
+    /**
+     * Read the the given cell (Counter index) of the PDR counters from the given device.
+     *
+     * @param counterIdx The counter cell index from which to read
+     * @return A structure containing ingress and egress packet and byte counts for the given
+     * cellId.
+     * @throws UpfProgrammableException if the cell ID is out of bounds
+     */
+    PdrStats readCounter(int counterIdx) throws UpfProgrammableException;
+
+    /**
+     * Return the number of PDR counter cells available. The number of cells in the ingress and
+     * egress PDR counters are equivalent.
+     *
+     * @return PDR counter size
+     */
+    long pdrCounterSize();
+
+    /**
+     * Return the number of maximum number of table entries the FAR table supports.
+     *
+     * @return the number of FARs that can be installed
+     */
+    long farTableSize();
+
+    /**
+     * Return the total number of table entries the downlink and uplink PDR tables support. Both
+     * tables support an equal number of entries, so the total is twice the size of either.
+     *
+     * @return the total number of PDRs that can be installed
+     */
+    long pdrTableSize();
+
+    /**
+     * Read the counter contents for all cell indices that are valid on the hardware switch.
+     * @code maxCounterId} parameter is used to limit the number of counters
+     * retrieved from the UPF device. If the limit given is larger than the
+     * physical limit, the physical limit will be used. A limit of -1 removes
+     * limitations.
+     *
+     * @param maxCounterId Maximum counter ID to retrieve from the UPF device.
+     * @return A collection of counter values for all valid hardware counter cells
+     * @throws UpfProgrammableException if the counters are unable to be read
+     */
+    Collection<PdrStats> readAllCounters(long maxCounterId) throws UpfProgrammableException;
+
+    /**
+     * Set the source and destination of the GTPU tunnel used to send packets to a dbuf buffering
+     * device.
+     *
+     * @param switchAddr the address on the switch that sends and receives packets to and from dbuf
+     * @param dbufAddr   the dataplane address of dbuf
+     */
+    void setDbufTunnel(Ip4Address switchAddr, Ip4Address dbufAddr);
+
+    /**
+     * Removes the dbuf tunnel info if they were previously set using {@link
+     * #setDbufTunnel(Ip4Address, Ip4Address)}.
+     */
+    void unsetDbufTunnel();
+
+    /**
+     * Install a BufferDrainer reference that can be used to trigger the draining of a specific dbuf
+     * buffer back into the UPF device.
+     *
+     * @param drainer the BufferDrainer reference
+     */
+    void setBufferDrainer(UpfProgrammable.BufferDrainer drainer);
+
+    /**
+     * Removes the buffer drainer if one was set using {@link #setBufferDrainer(UpfProgrammable.BufferDrainer)}.
+     */
+    void unsetBufferDrainer();
+
+    /**
+     * Instructs the UPF-programmable device to use GTP-U extension PDU Session Container (PSC) when
+     * doing encap of downlink packets, with the given QoS Flow Identifier (QFI).
+     *
+     * @param defaultQfi QFI to be used by default for all encapped packets.
+     * @throws UpfProgrammableException if operation is not available
+     */
+    // FIXME: remove once we expose QFI in logical pipeline
+    //  QFI should be set by the SMF using PFCP
+    void enablePscEncap(int defaultQfi) throws UpfProgrammableException;
+
+    /**
+     * Disable PSC encap previously enabled with {@link #enablePscEncap(int)}.
+     *
+     * @throws UpfProgrammableException if operation is not available
+     */
+    // FIXME: remove once we expose QFI in logical pipeline
+    //  QFI should be set by the SMF using PFCP
+    void disablePscEncap() throws UpfProgrammableException;
+
+    /**
+     * Sends the given data as a data plane packet-out through this device. Data is expected to
+     * contain an Ethernet frame.
+     *
+     * The device should process the packet through the pipeline tables to select an output port
+     * and to apply eventual modifications (e.g., MAC rewrite for routing, pushing a VLAN tag,
+     * etc.).
+     *
+     * @param data Ethernet frame bytes
+     */
+    void sendPacketOut(ByteBuffer data);
+
+    /**
+     * Used by the UpfProgrammable to trigger buffer draining as needed. Install an instance using
+     * {@link UpfProgrammable#setBufferDrainer(UpfProgrammable.BufferDrainer)}
+     */
+    //TODO: remove from UpfDevice
+    interface BufferDrainer {
+        /**
+         * Drain the buffer that contains packets for the UE with the given address.
+         *
+         * @param ueAddr the address of the UE for which we should drain a buffer
+         */
+        void drain(Ip4Address ueAddr);
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfInterface.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfInterface.java
new file mode 100644
index 0000000..3fee11a
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfInterface.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright 2021-present Open Networking Foundation
+ *
+ * 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 org.onosproject.net.behaviour.upf;
+
+import org.onlab.packet.Ip4Address;
+import org.onlab.packet.Ip4Prefix;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A UPF device interface, such as a S1U or UE IP address pool.
+ */
+public final class UpfInterface {
+    private final Ip4Prefix prefix;
+    private final Type type;
+
+    private UpfInterface(Ip4Prefix prefix, Type type) {
+        this.prefix = prefix;
+        this.type = type;
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @Override
+    public String toString() {
+        String typeStr;
+        if (type.equals(Type.ACCESS)) {
+            typeStr = "Access";
+        } else if (type.equals(Type.CORE)) {
+            typeStr = "Core";
+        } else if (type.equals(Type.DBUF)) {
+            typeStr = "Dbuf-Receiver";
+        } else {
+            typeStr = "UNKNOWN";
+        }
+        return String.format("Interface{%s, %s}", typeStr, prefix);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        UpfInterface that = (UpfInterface) obj;
+        return (this.type.equals(that.type) &&
+                this.prefix.equals(that.prefix));
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(prefix, type);
+    }
+
+    /**
+     * Create a core-facing UPF Interface from the given address, which will be treated as a /32 prefix.
+     *
+     * @param address the address of the new core-facing interface
+     * @return a new UPF interface
+     */
+    public static UpfInterface createS1uFrom(Ip4Address address) {
+        return builder().setAccess().setPrefix(Ip4Prefix.valueOf(address, 32)).build();
+    }
+
+    /**
+     * Create a core-facing UPF Interface from the given IP prefix.
+     *
+     * @param prefix the prefix of the new core-facing interface
+     * @return a new UPF interface
+     */
+    public static UpfInterface createUePoolFrom(Ip4Prefix prefix) {
+        return builder().setCore().setPrefix(prefix).build();
+    }
+
+    /**
+     * Create a dbuf-receiving UPF interface from the given IP address.
+     *
+     * @param address the address of the dbuf-receiving interface
+     * @return a new UPF interface
+     */
+    public static UpfInterface createDbufReceiverFrom(Ip4Address address) {
+        return UpfInterface.builder().setDbufReceiver().setAddress(address).build();
+    }
+
+    /**
+     * Get the IP prefix of this interface.
+     *
+     * @return the interface prefix
+     */
+    public Ip4Prefix prefix() {
+        return prefix;
+    }
+
+    /**
+     * Check if this UPF interface is for packets traveling from UEs.
+     * This will be true for S1U interface table entries.
+     *
+     * @return true if interface receives from access
+     */
+    public boolean isAccess() {
+        return type == Type.ACCESS;
+    }
+
+    /**
+     * Check if this UPF interface is for packets traveling towards UEs.
+     * This will be true for UE IP address pool table entries.
+     *
+     * @return true if interface receives from core
+     */
+    public boolean isCore() {
+        return type == Type.CORE;
+    }
+
+
+    /**
+     * Check if this UPF interface is for receiving buffered packets as they are released from the dbuf
+     * buffering device.
+     *
+     * @return true if interface receives from dbuf
+     */
+    public boolean isDbufReceiver() {
+        return type == Type.DBUF;
+    }
+
+    /**
+     * Get the IPv4 prefix of this UPF interface.
+     *
+     * @return the interface prefix
+     */
+    public Ip4Prefix getPrefix() {
+        return this.prefix;
+    }
+
+    public enum Type {
+        /**
+         * Unknown UPF interface type.
+         */
+        UNKNOWN,
+
+        /**
+         * Interface that receives GTP encapsulated packets.
+         * This is the type of the S1U interface.
+         */
+        ACCESS,
+
+        /**
+         * Interface that receives unencapsulated packets from the core of the network.
+         * This is the type of UE IP address pool interfaces.
+         */
+        CORE,
+
+        /**
+         * Interface that receives buffered packets as they are drained from a dbuf device.
+         */
+        DBUF
+    }
+
+    public static class Builder {
+        private Ip4Prefix prefix;
+        private Type type;
+
+        public Builder() {
+            type = Type.UNKNOWN;
+        }
+
+        /**
+         * Set the IPv4 prefix of this interface.
+         *
+         * @param prefix the interface prefix
+         * @return this builder object
+         */
+        public Builder setPrefix(Ip4Prefix prefix) {
+            this.prefix = prefix;
+            return this;
+        }
+
+        /**
+         * Set the IPv4 prefix of this interface, by turning the given address into a /32 prefix.
+         *
+         * @param address the interface address that will become a /32 prefix
+         * @return this builder object
+         */
+        public Builder setAddress(Ip4Address address) {
+            this.prefix = Ip4Prefix.valueOf(address, 32);
+            return this;
+        }
+
+        /**
+         * Make this an access-facing interface.
+         *
+         * @return this builder object
+         */
+        public Builder setAccess() {
+            this.type = Type.ACCESS;
+            return this;
+        }
+
+        /**
+         * Make this a core-facing interface.
+         *
+         * @return this builder object
+         */
+        public Builder setCore() {
+            this.type = Type.CORE;
+            return this;
+        }
+
+        /**
+         * Make this a dbuf-facing interface.
+         *
+         * @return this builder object
+         */
+        public Builder setDbufReceiver() {
+            this.type = Type.DBUF;
+            return this;
+        }
+
+        public UpfInterface build() {
+            checkNotNull(prefix);
+            return new UpfInterface(prefix, type);
+        }
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfProgrammable.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfProgrammable.java
new file mode 100644
index 0000000..6dcce30
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfProgrammable.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2021-present Open Networking Foundation
+ *
+ * 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 org.onosproject.net.behaviour.upf;
+
+import com.google.common.annotations.Beta;
+import org.onosproject.net.driver.HandlerBehaviour;
+
+
+/**
+ * Provides means to update the device forwarding state to implement a 3GPP
+ * User Plane Function. An implementation of this API should not write state
+ * directly to the device, but instead, always rely on core ONOS subsystems
+ * (e.g., FlowRuleService, GroupService, etc).
+ */
+@Beta
+public interface UpfProgrammable extends HandlerBehaviour, UpfDevice {
+    /**
+     * Apps are expected to call this method as the first one when they are ready
+     * to install PDRs and FARs.
+     *
+     * @return True if initialized, false otherwise.
+     */
+    boolean init();
+}
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfProgrammableException.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfProgrammableException.java
new file mode 100644
index 0000000..7daa806
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/UpfProgrammableException.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021-present Open Networking Foundation
+ *
+ * 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 org.onosproject.net.behaviour.upf;
+
+/**
+ * An exception indicating a an error happened in the UPF programmable behaviour.
+ * Possible errors include the attempted insertion of a malformed flow rule, the
+ * reading or writing of an out-of-bounds counter cell, the deletion of a non-existent
+ * flow rule, and the attempted insertion of a flow rule into a full table.
+ */
+public class UpfProgrammableException extends Exception {
+    private final Type type;
+
+    public enum Type {
+        /**
+         * The UpfProgrammable did not provide a specific exception type.
+         */
+        UNKNOWN,
+        /**
+         * The target table is at capacity.
+         */
+        TABLE_EXHAUSTED,
+        /**
+         * A provided counter cell index was out of range.
+         */
+        COUNTER_INDEX_OUT_OF_RANGE,
+        /**
+         * The UpfProgrammable implementation doesn't support the operation.
+         */
+        UNSUPPORTED_OPERATION
+    }
+
+    /**
+     * Creates a new exception for the given message.
+     *
+     * @param message message
+     */
+    public UpfProgrammableException(String message) {
+        super(message);
+        this.type = Type.UNKNOWN;
+    }
+
+    /**
+     * Creates a new exception for the given message and type.
+     *
+     * @param message exception message
+     * @param type    exception type
+     */
+    public UpfProgrammableException(String message, Type type) {
+        super(message);
+        this.type = type;
+    }
+
+    /**
+     * Get the type of the exception.
+     *
+     * @return exception type
+     */
+    public Type getType() {
+        return type;
+    }
+}
diff --git a/core/api/src/main/java/org/onosproject/net/behaviour/upf/package-info.java b/core/api/src/main/java/org/onosproject/net/behaviour/upf/package-info.java
new file mode 100644
index 0000000..805e149
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/net/behaviour/upf/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2021-present Open Networking Foundation
+ *
+ * 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.
+ */
+
+/**
+ * 3GPP User Plane Function behaviors and related classes.
+ */
+package org.onosproject.net.behaviour.upf;