First implementation of BNG application

The application offers network level APIs for the BNG data plane.

An attachment (BngAttachment) at network level is identified by an arbitrary string.
The exposed APIs are key-value like APIs.

Change-Id: If0e484f487ea16dd8c7dd99642f75686e1dbc29a
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..54e28c7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+*~
+*.class
+.classpath
+.project
+.settings
+.checkstyle
+target
+*.iml
+.idea
\ No newline at end of file
diff --git a/.gitreview b/.gitreview
new file mode 100644
index 0000000..8fdf902
--- /dev/null
+++ b/.gitreview
@@ -0,0 +1,5 @@
+[gerrit]
+host=gerrit.opencord.org
+port=29418
+project=bng.git
+defaultremote=origin
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..e65e80e
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2016 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.
\ No newline at end of file
diff --git a/api/pom.xml b/api/pom.xml
new file mode 100644
index 0000000..6ebbe67
--- /dev/null
+++ b/api/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2019-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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.opencord</groupId>
+        <artifactId>bng</artifactId>
+        <version>1.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>bng-api</artifactId>
+
+    <packaging>bundle</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-api</artifactId>
+            <version>${onos.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/api/src/main/java/org/opencord/bng/BngAttachment.java b/api/src/main/java/org/opencord/bng/BngAttachment.java
new file mode 100644
index 0000000..3fce980
--- /dev/null
+++ b/api/src/main/java/org/opencord/bng/BngAttachment.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import com.google.common.base.MoreObjects;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.behaviour.BngProgrammable;
+
+/**
+ * Abstract implementation of an attachment.
+ */
+public abstract class BngAttachment implements BngProgrammable.Attachment {
+    private final ApplicationId appId;
+    private final VlanId sTag;
+    private final VlanId cTag;
+    private final MacAddress macAddress;
+    private final IpAddress ipAddress;
+    private final boolean lineActivated;
+    private final ConnectPoint oltConnectPoint;
+    private final String onuSerial;
+    private final short qinqTpid;
+
+    /**
+     * Creates a new attachment.
+     *
+     * @param appId           The application that created this attachment
+     * @param sTag            The VLAN S-TAG
+     * @param cTag            The VLAN C-TAG
+     * @param macAddress      The MAC address of the attachment
+     * @param ipAddress       The IP address of the attachment
+     * @param lineActivated   Define if the attachment is active or not
+     * @param oltConnectPoint The connect point of the OLT (UNI port)
+     * @param onuSerial       The serial string of the ONU
+     * @param qinqTpid        The QinQ Tpid for the packets
+     */
+    BngAttachment(ApplicationId appId, VlanId sTag, VlanId cTag,
+                  MacAddress macAddress, IpAddress ipAddress,
+                  boolean lineActivated, ConnectPoint oltConnectPoint,
+                  String onuSerial, short qinqTpid) {
+        this.appId = appId;
+        this.sTag = sTag;
+        this.cTag = cTag;
+        this.macAddress = macAddress;
+        this.ipAddress = ipAddress;
+        this.lineActivated = lineActivated;
+        this.oltConnectPoint = oltConnectPoint;
+        this.onuSerial = onuSerial;
+        this.qinqTpid = qinqTpid;
+    }
+
+    /**
+     * Returns the OLT connect point (UNI port).
+     *
+     * @return The OLT connect point
+     */
+    public ConnectPoint oltConnectPoint() {
+        return this.oltConnectPoint;
+    }
+
+    /**
+     * Returns the ONU serial number.
+     *
+     * @return The ONU serial number
+     */
+    public String onuSerial() {
+        return this.onuSerial;
+    }
+
+    /**
+     * Returns the QinQ TPID.
+     *
+     * @return The QinQ TPID
+     */
+    public short qinqTpid() {
+        return this.qinqTpid;
+    }
+
+    @Override
+    public ApplicationId appId() {
+        return appId;
+    }
+
+    @Override
+    public VlanId sTag() {
+        return sTag;
+    }
+
+    @Override
+    public VlanId cTag() {
+        return cTag;
+    }
+
+    @Override
+    public MacAddress macAddress() {
+        return macAddress;
+    }
+
+    @Override
+    public IpAddress ipAddress() {
+        return ipAddress;
+    }
+
+    @Override
+    public boolean lineActive() {
+        return lineActivated;
+    }
+
+    MoreObjects.ToStringHelper toStringHelper() {
+        return MoreObjects.toStringHelper(this)
+                .add("appId", appId)
+                .add("sTag", sTag)
+                .add("cTag", cTag)
+                .add("macAddress", macAddress)
+                .add("ipAddress", ipAddress)
+                .add("lineActivated", lineActivated)
+                .add("oltConnectPoint", oltConnectPoint)
+                .add("onuSerial", onuSerial)
+                .add("qinqTpid", qinqTpid);
+    }
+
+    /**
+     * Abstract builder of attachments.
+     * <p>
+     * TODO: javadoc
+     */
+    public abstract static class BngBuilder {
+        ApplicationId appId;
+        VlanId sTag;
+        VlanId cTag;
+        MacAddress macAddress;
+        IpAddress ipAddress = IpAddress.valueOf(0);
+        boolean lineActivated;
+        ConnectPoint oltconnectPoint;
+        String onuSerial;
+        short qinqTpid;
+
+        BngBuilder() {
+            // Hide constructor
+            this.lineActivated = false;
+        }
+
+        /**
+         * Sets the ONU serial number.
+         *
+         * @param onuSerial The ONU serial number
+         * @return self
+         */
+        public BngBuilder withOnuSerial(String onuSerial) {
+            this.onuSerial = onuSerial;
+            return this;
+        }
+
+        /**
+         * Sets the OLT connect point (UNI port).
+         *
+         * @param oltconnectPoint The OLT connect point
+         * @return self
+         */
+        public BngBuilder withOltConnectPoint(ConnectPoint oltconnectPoint) {
+            this.oltconnectPoint = oltconnectPoint;
+            return this;
+        }
+
+        /**
+         * Sets the VLAN S-Tag.
+         *
+         * @param sTag The VLAN S-Tag
+         * @return self
+         */
+        public BngBuilder withSTag(VlanId sTag) {
+            this.sTag = sTag;
+            return this;
+        }
+
+        /**
+         * Sets the VLAN C-Tag.
+         *
+         * @param cTag the VLAN C-Tag
+         * @return self
+         */
+        public BngBuilder withCTag(VlanId cTag) {
+            this.cTag = cTag;
+            return this;
+        }
+
+        /**
+         * Sets the attachment activation state.
+         *
+         * @param lineActivated The attachment activation state
+         * @return self
+         */
+        public BngBuilder lineActivated(boolean lineActivated) {
+            this.lineActivated = lineActivated;
+            return this;
+        }
+
+        /**
+         * Sets the application ID.
+         *
+         * @param appId The application ID.
+         * @return self
+         */
+        public BngBuilder withApplicationId(ApplicationId appId) {
+            this.appId = appId;
+            return this;
+        }
+
+        /**
+         * Sets the attachment MAC address.
+         *
+         * @param macAddress The MAC address
+         * @return self
+         */
+        public BngBuilder withMacAddress(MacAddress macAddress) {
+            this.macAddress = macAddress;
+            return this;
+        }
+
+        /**
+         * Sets the attachment IP address.
+         *
+         * @param ipAddress The IP address
+         * @return self
+         */
+        public BngBuilder withIpAddress(IpAddress ipAddress) {
+            this.ipAddress = ipAddress;
+            return this;
+        }
+
+        /**
+         * Sets the QinQ tpid.
+         *
+         * @param qinqTpid The QinQ tpid
+         * @return self
+         */
+        public BngBuilder withQinqTpid(short qinqTpid) {
+            this.qinqTpid = qinqTpid;
+            return this;
+        }
+
+        /**
+         * Returns a new BNG attachment.
+         *
+         * @return BNG attachment
+         */
+        public abstract BngAttachment build();
+    }
+}
diff --git a/api/src/main/java/org/opencord/bng/BngService.java b/api/src/main/java/org/opencord/bng/BngService.java
new file mode 100644
index 0000000..c9936ae
--- /dev/null
+++ b/api/src/main/java/org/opencord/bng/BngService.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import org.onosproject.net.DeviceId;
+
+import java.util.Map;
+
+/**
+ * Service for managing attachments.
+ */
+public interface BngService {
+
+    String ONU_ANNOTATION = "onu";
+
+    /**
+     * Sets up the given attachment with the given attachemtn key in the BNG
+     * app. If the attachment is already registered it will be updated. It will
+     * also trigger the termination of the attachment user traffic on the ASG
+     * device.
+     *
+     * @param attachmentKey The key for the given attachment
+     * @param attachment    The attachment to be installed or updated
+     */
+    void setupAttachment(String attachmentKey, BngAttachment attachment);
+
+    /**
+     * Removes an attachment given its attachment ID. It will also trigger the
+     * removal of all the related attachment flows from the ASG device.
+     *
+     * @param attachmentKey The ID of the attachment to be removed
+     */
+    void removeAttachment(String attachmentKey);
+
+    /**
+     * Returns a map with the registered attachments.
+     *
+     * @return The map of attachemtn keys and attachments. Empty map if no
+     * attachment is registered.
+     */
+    Map<String, BngAttachment> getAttachments();
+
+    /**
+     * Returns the registered attachment given the ID.
+     *
+     * @param attachmentKey The attachment ID
+     * @return The attachment if it is present, null otherwise
+     */
+    BngAttachment getAttachment(String attachmentKey);
+
+    /**
+     * Returns the BNG device ID currently used.
+     *
+     * @return The BNG device ID.
+     */
+    DeviceId getBngDeviceId();
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/opencord/bng/BngStatsEvent.java b/api/src/main/java/org/opencord/bng/BngStatsEvent.java
new file mode 100644
index 0000000..2622b41
--- /dev/null
+++ b/api/src/main/java/org/opencord/bng/BngStatsEvent.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import org.onosproject.event.AbstractEvent;
+
+/**
+ * Represents an event related to attachment-level statistics.
+ */
+public final class BngStatsEvent extends
+        AbstractEvent<BngStatsEvent.EventType, BngStatsEventSubject> {
+
+    /**
+     * Creates an attachment-level statistics event.
+     *
+     * @param type    Event type
+     * @param subject Event subject
+     */
+    public BngStatsEvent(EventType type, BngStatsEventSubject subject) {
+        super(type, subject);
+    }
+
+    /**
+     * Type of the event.
+     */
+    public enum EventType {
+        /**
+         * Signals that the statistics has been updated.
+         */
+        STATS_UPDATED
+    }
+}
diff --git a/api/src/main/java/org/opencord/bng/BngStatsEventListener.java b/api/src/main/java/org/opencord/bng/BngStatsEventListener.java
new file mode 100644
index 0000000..6232428
--- /dev/null
+++ b/api/src/main/java/org/opencord/bng/BngStatsEventListener.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import org.onosproject.event.EventListener;
+
+/**
+ * Listener for attachment-level statistics events.
+ */
+public interface BngStatsEventListener extends EventListener<BngStatsEvent> {
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/opencord/bng/BngStatsEventSubject.java b/api/src/main/java/org/opencord/bng/BngStatsEventSubject.java
new file mode 100644
index 0000000..05ae43a
--- /dev/null
+++ b/api/src/main/java/org/opencord/bng/BngStatsEventSubject.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
+import org.onosproject.net.behaviour.BngProgrammable.BngCounterType;
+import org.onosproject.net.pi.runtime.PiCounterCellData;
+
+import java.util.Map;
+
+/**
+ * Subject for attachment-level statistics events.
+ */
+public final class BngStatsEventSubject {
+
+    private final BngAttachment bngAttachment;
+    private final String attachmentKey;
+    private final ImmutableMap<BngCounterType, PiCounterCellData> attachmentStats;
+
+    /**
+     * Creates a subject for attachment-level statistics event.
+     *
+     * @param attachmentKey   The attachment key
+     * @param bngAttachment   The attachment instance
+     * @param attachmentStats The attachments statistics
+     */
+    public BngStatsEventSubject(String attachmentKey, BngAttachment bngAttachment,
+                                Map<BngCounterType, PiCounterCellData> attachmentStats) {
+        this.attachmentKey = attachmentKey;
+        this.bngAttachment = bngAttachment;
+        this.attachmentStats = ImmutableMap.copyOf(attachmentStats);
+    }
+
+    /**
+     * Returns an immutable representation of the attachment-level statistics.
+     *
+     * @return The pair attachment instance and attachment-level statistics
+     */
+    public Map<BngCounterType, PiCounterCellData> getAttachmentStats() {
+        return attachmentStats;
+    }
+
+    /**
+     * Returns the BNG attachment instance of the attachment-level statistics.
+     *
+     * @return The BNG attachment instance
+     */
+    public BngAttachment getBngAttachment() {
+        return this.bngAttachment;
+    }
+
+    public String getAttachmentKey() {
+        return this.attachmentKey;
+    }
+
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("attachmentKey", attachmentKey)
+                .add("bngAttachment", bngAttachment)
+                .add("attachmentsStats", attachmentStats)
+                .toString();
+    }
+}
diff --git a/api/src/main/java/org/opencord/bng/BngStatsService.java b/api/src/main/java/org/opencord/bng/BngStatsService.java
new file mode 100644
index 0000000..45ab29b
--- /dev/null
+++ b/api/src/main/java/org/opencord/bng/BngStatsService.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import org.onosproject.event.ListenerService;
+import org.onosproject.net.behaviour.BngProgrammable;
+import org.onosproject.net.pi.runtime.PiCounterCellData;
+
+import java.util.Map;
+
+/**
+ * Service to retrieve attachment statistics.
+ */
+public interface BngStatsService
+        extends ListenerService<BngStatsEvent, BngStatsEventListener> {
+
+    /**
+     * Returns the statistics given an attachment Key. If the attachment is not
+     * programmed in the ASG, it returns an empty map. If one of the counter
+     * type is not supported by the ASG device, that specific counter will not
+     * be found in the map.
+     *
+     * @param bngAttachmentKey The attachment Key
+     * @return A map with the type of the counter and the associated statistics
+     * coming from the ASG device. Empty map if no BNG programmable is
+     * available.
+     */
+    Map<BngProgrammable.BngCounterType, PiCounterCellData> getStats(String bngAttachmentKey);
+
+    /**
+     * Returns the aggregate statistics related to attachments that are not
+     * known by this app, e.g., packets that are sent from an attachment while
+     * the attachment is negotiating the session.
+     *
+     * @return The statistics of not registered attachment. null if no BNG
+     * programmable device is available.
+     */
+    PiCounterCellData getControlStats();
+
+}
diff --git a/api/src/main/java/org/opencord/bng/PppoeBngAttachment.java b/api/src/main/java/org/opencord/bng/PppoeBngAttachment.java
new file mode 100644
index 0000000..7d6c8ed
--- /dev/null
+++ b/api/src/main/java/org/opencord/bng/PppoeBngAttachment.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.behaviour.BngProgrammable;
+
+/**
+ * Contains all the information about an attachment in the PPPoE BNG context.
+ */
+public final class PppoeBngAttachment extends BngAttachment {
+
+    // The PPPoE session ID
+    private final short pppoeSessionId;
+
+    private PppoeBngAttachment(ApplicationId appId, VlanId sTag, VlanId cTag,
+                               MacAddress macAddress, IpAddress ipAddress, boolean lineActivated,
+                               short pppoeSessionId, ConnectPoint oltConnectPoint, String onuSerial,
+                               short qinqTpid) {
+        super(appId, sTag, cTag, macAddress, ipAddress, lineActivated,
+              oltConnectPoint, onuSerial, qinqTpid);
+        this.pppoeSessionId = pppoeSessionId;
+    }
+
+    /**
+     * Returns a builder for PPPoE BNG Attachemnt.
+     *
+     * @return The builder
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    // TODO: remove when BngProgrammable API are updated!
+    @Override
+    public BngProgrammable.AttachmentId attachmentId() {
+        return null;
+    }
+
+    @Override
+    public BngProgrammable.Attachment.AttachmentType type() {
+        return BngProgrammable.Attachment.AttachmentType.PPPoE;
+    }
+
+    @Override
+    public short pppoeSessionId() {
+        return this.pppoeSessionId;
+    }
+
+    @Override
+    public String toString() {
+        return this.toStringHelper()
+                .add("pppoeSessionId", pppoeSessionId())
+                .toString();
+    }
+
+    /**
+     * Builder of PPPoE attachments.
+     */
+    public static final class Builder extends BngBuilder {
+        private short pppoeSessionId = 0;
+
+        private Builder() {
+            super();
+        }
+
+        /**
+         * Sets the PPPoE session ID.
+         *
+         * @param pppoeSessionId The PPPoE session ID
+         * @return self
+         */
+        public Builder withPppoeSessionId(short pppoeSessionId) {
+            this.pppoeSessionId = pppoeSessionId;
+            return this;
+        }
+
+        /**
+         * Returns a new PPPoE attachment.
+         *
+         * @return PPPoE attachment
+         */
+        public PppoeBngAttachment build() {
+            return new PppoeBngAttachment(this.appId,
+                                          this.sTag,
+                                          this.cTag,
+                                          this.macAddress,
+                                          this.ipAddress,
+                                          this.lineActivated,
+                                          this.pppoeSessionId,
+                                          this.oltconnectPoint,
+                                          this.onuSerial,
+                                          this.qinqTpid);
+        }
+    }
+}
diff --git a/api/src/main/java/org/opencord/bng/PppoeBngControlHandler.java b/api/src/main/java/org/opencord/bng/PppoeBngControlHandler.java
new file mode 100644
index 0000000..4338da4
--- /dev/null
+++ b/api/src/main/java/org/opencord/bng/PppoeBngControlHandler.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import org.onosproject.event.ListenerService;
+
+/**
+ * BNG control packet service for PPPoE protocol.
+ */
+public interface PppoeBngControlHandler
+        extends ListenerService<PppoeEvent, PppoeEventListener> {
+}
diff --git a/api/src/main/java/org/opencord/bng/PppoeEvent.java b/api/src/main/java/org/opencord/bng/PppoeEvent.java
new file mode 100644
index 0000000..7bccabf
--- /dev/null
+++ b/api/src/main/java/org/opencord/bng/PppoeEvent.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import org.onosproject.event.AbstractEvent;
+
+/**
+ * PPPoE attachment level event. This kind of events are used to advertise
+ * control level attachment events (e.g., attachment creation, successful
+ * authentication etc.)
+ */
+public final class PppoeEvent extends
+        AbstractEvent<PppoeEvent.EventType, PppoeEventSubject> {
+    /**
+     * Creates a new event for the given attachment information.
+     *
+     * @param type    type
+     * @param subject the attachment event
+     */
+    public PppoeEvent(EventType type, PppoeEventSubject subject) {
+        super(type, subject);
+    }
+
+    /**
+     * Type of the event.
+     */
+    public enum EventType {
+        /**
+         * Signals that a PPPoE session has been initiated from the client.
+         */
+        SESSION_INIT,
+
+        /**
+         * Signal that a PPPoE session has been established.
+         */
+        SESSION_CONFIRMATION,
+
+        /**
+         * Signal that the PPPoE session has been terminated.
+         */
+        SESSION_TERMINATION,
+
+        /**
+         * Signals an IPCP configuration acknowledge event.
+         */
+        IPCP_CONF_ACK,
+
+        /**
+         * Signals an IPCP configuration request event.
+         */
+        IPCP_CONF_REQUEST,
+
+        /**
+         * Authentication initiated.
+         */
+        AUTH_REQUEST,
+
+        /**
+         * Authentication confirmation.
+         */
+        AUTH_SUCCESS,
+
+        /**
+         * Failed authentication.
+         */
+        AUTH_FAILURE
+    }
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/opencord/bng/PppoeEventListener.java b/api/src/main/java/org/opencord/bng/PppoeEventListener.java
new file mode 100644
index 0000000..40d5ab1
--- /dev/null
+++ b/api/src/main/java/org/opencord/bng/PppoeEventListener.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import org.onosproject.event.EventListener;
+
+/**
+ * Listener for PPPoE attachment level events.
+ */
+public interface PppoeEventListener extends EventListener<PppoeEvent> {
+}
diff --git a/api/src/main/java/org/opencord/bng/PppoeEventSubject.java b/api/src/main/java/org/opencord/bng/PppoeEventSubject.java
new file mode 100644
index 0000000..734ac37
--- /dev/null
+++ b/api/src/main/java/org/opencord/bng/PppoeEventSubject.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import com.google.common.base.MoreObjects;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+
+/**
+ * Subject for a PPPoE attachment level events.
+ */
+public final class PppoeEventSubject {
+
+    private final ConnectPoint oltConnectPoint;
+    private final IpAddress ipAddress;
+    private final MacAddress macAddress;
+    private final String onuSerialNumber;
+    private final short sessionId;
+    private final VlanId sTag;
+    private final VlanId cTag;
+
+    /**
+     * Creates a PPPoE attachment event subject.
+     *
+     * @param oltConnectPoint The connect point of the OLT (UNI port)
+     * @param ipAddress       The IP Address that has been assigned
+     * @param macAddress      The MAC address of the attachment
+     * @param onuSerialNumber The serial number of the ONU
+     * @param sessionId       The PPPoE session ID
+     * @param sTag            The VLAN S-Tag
+     * @param cTag            The VLan C-Tag
+     */
+    public PppoeEventSubject(ConnectPoint oltConnectPoint,
+                             IpAddress ipAddress,
+                             MacAddress macAddress,
+                             String onuSerialNumber,
+                             short sessionId,
+                             VlanId sTag,
+                             VlanId cTag) {
+        this.oltConnectPoint = oltConnectPoint;
+        this.ipAddress = ipAddress;
+        this.macAddress = macAddress;
+        this.onuSerialNumber = onuSerialNumber;
+        this.sessionId = sessionId;
+        this.sTag = sTag;
+        this.cTag = cTag;
+    }
+
+    /**
+     * Returns the connect point of the OLT (UNI port).
+     *
+     * @return The connect point
+     */
+    public ConnectPoint getOltConnectPoint() {
+        return this.oltConnectPoint;
+    }
+
+
+    /**
+     * Returns the assigned IP address (if any).
+     *
+     * @return The IP address
+     */
+    public IpAddress getIpAddress() {
+        return this.ipAddress;
+    }
+
+    /**
+     * Returns the MAC address.
+     *
+     * @return The MAC address
+     */
+    public MacAddress getMacAddress() {
+        return this.macAddress;
+    }
+
+    /**
+     * Returns the ONU serial number.
+     *
+     * @return The ONU serial number
+     */
+    public String getOnuSerialNumber() {
+        return onuSerialNumber;
+    }
+
+    /**
+     * Returns the session ID.
+     *
+     * @return The session ID.
+     */
+    public short getSessionId() {
+        return this.sessionId;
+    }
+
+    /**
+     * Returns the VLAN S-Tag.
+     *
+     * @return The VLAN S-Tag.
+     */
+    public VlanId getsTag() {
+        return sTag;
+    }
+
+    /**
+     * Returns the VLAN C-Tag.
+     *
+     * @return The VLAN C-Tag.
+     */
+    public VlanId getcTag() {
+        return cTag;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+                .add("oltConnectPoint", oltConnectPoint)
+                .add("ipAddress", ipAddress)
+                .add("macAddress", macAddress)
+                .add("onuSerialNumber", onuSerialNumber)
+                .add("sessionId", sessionId)
+                .add("STag", sTag)
+                .add("CTag", cTag)
+                .toString();
+    }
+}
diff --git a/api/src/main/java/org/opencord/bng/package-info.java b/api/src/main/java/org/opencord/bng/package-info.java
new file mode 100644
index 0000000..170eb9c
--- /dev/null
+++ b/api/src/main/java/org/opencord/bng/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019-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.
+ */
+
+/**
+ * BNG application.
+ */
+package org.opencord.bng;
diff --git a/app/pom.xml b/app/pom.xml
new file mode 100644
index 0000000..9bf4f15
--- /dev/null
+++ b/app/pom.xml
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2019-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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.opencord</groupId>
+        <artifactId>bng</artifactId>
+        <version>1.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>bng-app</artifactId>
+
+    <packaging>bundle</packaging>
+    <description>BNG Control app</description>
+
+    <properties>
+        <onos.app.name>org.opencord.bng</onos.app.name>
+        <onos.app.title>BNG Control app</onos.app.title>
+        <onos.app.category>Traffic Steering</onos.app.category>
+        <onos.app.url>http://opencord.org</onos.app.url>
+        <onos.app.readme>
+            BNG app for controlling the BNG programmable device
+        </onos.app.readme>
+        <onos.app.requires>
+            org.opencord.sadis,
+            org.opencord.olt
+        </onos.app.requires>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-api</artifactId>
+            <version>${onos.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.opencord</groupId>
+            <artifactId>sadis-api</artifactId>
+            <version>${sadis.api.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.opencord</groupId>
+            <artifactId>bng-api</artifactId>
+            <version>${project.version}</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onlab-osgi</artifactId>
+            <version>${onos.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onlab-misc</artifactId>
+            <version>${onos.version}</version>
+            <scope>test</scope>
+            <classifier>tests</classifier>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-api</artifactId>
+            <version>${onos.version}</version>
+            <scope>test</scope>
+            <classifier>tests</classifier>
+        </dependency>
+
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onos-cli</artifactId>
+            <version>${onos.version}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.karaf.shell</groupId>
+            <artifactId>org.apache.karaf.shell.console</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.opencord</groupId>
+            <artifactId>olt-api</artifactId>
+            <version>${olt.api.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.onosproject</groupId>
+                <artifactId>onos-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        <Karaf-Commands>org.opencord.bng.cli</Karaf-Commands>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/bng/BngManager.java b/app/src/main/java/org/opencord/bng/BngManager.java
new file mode 100644
index 0000000..4cdd558
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/BngManager.java
@@ -0,0 +1,536 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import org.apache.commons.lang3.tuple.Pair;
+import org.onlab.packet.EthType;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultAnnotations;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.behaviour.BngProgrammable;
+import org.onosproject.net.behaviour.BngProgrammable.BngProgrammableException;
+import org.onosproject.net.config.ConfigFactory;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.config.NetworkConfigListener;
+import org.onosproject.net.config.NetworkConfigRegistry;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.host.DefaultHostDescription;
+import org.onosproject.net.host.HostDescription;
+import org.onosproject.net.host.HostProvider;
+import org.onosproject.net.host.HostProviderRegistry;
+import org.onosproject.net.host.HostProviderService;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.net.provider.ProviderId;
+import org.opencord.bng.config.BngConfig;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
+
+import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
+
+/**
+ * Implements the network level BNG service API to manage attachments.
+ */
+@Component(immediate = true, service = BngService.class)
+public class BngManager implements HostProvider, BngService {
+    public static final String BNG_APP = "org.opencord.bng";
+
+    private static final ProviderId PROVIDER_ID = new ProviderId("bngapp", BngManager.BNG_APP);
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private final AtomicBoolean bnguInitialized = new AtomicBoolean(false);
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected LinkService linkService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected NetworkConfigRegistry cfgService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected HostProviderRegistry providerRegistry;
+
+    private ConfigFactory<ApplicationId, BngConfig> cfgFactory = new ConfigFactory<>(
+            APP_SUBJECT_FACTORY,
+            BngConfig.class,
+            BngConfig.KEY) {
+        @Override
+        public BngConfig createConfig() {
+            return new BngConfig();
+        }
+    };
+    private BngProgrammable bngProgrammable;
+    private DeviceId bngDeviceId;
+    private InternalDeviceListener deviceListener;
+    private InternalConfigListener cfgListener;
+    private HostProviderService hostProviderService;
+    // TODO: add support for other attachment type
+    private Map<String, Pair<BngAttachment, HostId>> registeredAttachment;
+    private ApplicationId appId;
+
+    @Activate
+    protected void activate() {
+        appId = coreService.registerApplication(BNG_APP);
+        hostProviderService = providerRegistry.register(this);
+        registeredAttachment = Maps.newHashMap();
+        bngProgrammable = null;
+        bngDeviceId = null;
+        deviceListener = new InternalDeviceListener();
+        cfgListener = new InternalConfigListener();
+        cfgService.addListener(cfgListener);
+        cfgService.registerConfigFactory(cfgFactory);
+
+        // Update the BNG relay configuration
+        updateConfig();
+
+        deviceService.addListener(deviceListener);
+
+        log.info("BNG app activated");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        providerRegistry.unregister(this);
+        if (bngProgrammableAvailable()) {
+            try {
+                bngProgrammable.cleanUp(appId);
+            } catch (BngProgrammableException e) {
+                log.error("Error cleaning-up the BNG pipeline, {}", e.getMessage());
+            }
+        }
+        deviceService.removeListener(deviceListener);
+        cfgService.removeListener(cfgListener);
+        cfgService.unregisterConfigFactory(cfgFactory);
+        registeredAttachment = null;
+        bnguInitialized.set(false);
+        log.info("BNG app deactivated");
+    }
+
+    @Override
+    public void setupAttachment(String attachmentKey, BngAttachment attachment) {
+        // FIXME: the update case is not completely clear. Should the programAttachment method clean up the counters?
+        assert attachment.type().equals(BngProgrammable.Attachment.AttachmentType.PPPoE);
+        boolean updating = false;
+        var alreadyRegAttachment = registeredAttachment.get(attachmentKey);
+        if (alreadyRegAttachment == null) {
+            log.info("Registering a new attachment: {}", attachment.toString());
+        } else if (!attachment.equals(alreadyRegAttachment.getLeft())) {
+            log.info("Updating the attachment: {}", attachment.toString());
+            updating = true;
+        } else {
+            log.info("Attachment already registered: {}", attachment.toString());
+            return;
+        }
+        // FIXME: it could register anyway the attachment but do not program it on the BNG data-plane device.
+        if (attachment.type() != BngProgrammable.Attachment.AttachmentType.PPPoE) {
+            log.warn("Attachment type not supported, rejecting attachment: {}", attachmentKey);
+            return;
+        }
+        var pppoeAttachment = (PppoeBngAttachment) attachment;
+        // Retrieve the connect point on the ASG device
+        var asgConnectPoint = getAsgConnectPoint(pppoeAttachment.oltConnectPoint()).orElseThrow();
+        final HostId hostId = HostId.hostId(attachment.macAddress(), attachment.sTag());
+        final HostDescription hostDescription = createHostDescription(
+                attachment.cTag(), attachment.sTag(),
+                attachment.macAddress(), attachment.ipAddress(),
+                asgConnectPoint, pppoeAttachment.oltConnectPoint(), pppoeAttachment.onuSerial());
+
+        // Make sure that bngProgrammable is available and if so that the attachment is connected to the bngProgrammable
+        if (bngProgrammableAvailable() && isCorrectlyConnected(asgConnectPoint)) {
+            try {
+                programAttachment(attachment, hostId, hostDescription, updating);
+            } catch (BngProgrammableException ex) {
+                log.error("Attachment not created: " + ex.getMessage());
+            }
+        } else {
+            // If the BNG user plane is not available, or the attachment is not connected to
+            // the correct BNG user planee, accept anyway the attachment.
+            // Check if the attachment is correctly connected to the BNG device when that device will show up.
+            log.info("BNG user planee not available, attachment accepted but not programmed");
+        }
+        log.info("PPPoE Attachment created/updated: {}", pppoeAttachment);
+        registeredAttachment.put(attachmentKey, Pair.of(pppoeAttachment, hostId));
+    }
+
+    private Optional<ConnectPoint> getAsgConnectPoint(ConnectPoint oltConnectPoint) {
+        try {
+            // Here I suppose that each OLT can be connected to a SINGLE ASG that is BNG U capable
+            return Optional.of(linkService.getDeviceEgressLinks(oltConnectPoint.deviceId()).stream()
+                                       .filter(link -> isBngProgrammable(link.dst().deviceId()))
+                                       .map(link -> link.dst())
+                                       .collect(Collectors.toList())
+                                       .get(0));
+
+        } catch (IndexOutOfBoundsException ex) {
+            return Optional.empty();
+        }
+    }
+
+    /**
+     * Setup of an attachment. Before calling this method, make sure that BNG
+     * programmable is available.
+     *
+     * @param attachment
+     * @param hostId
+     * @param hostDescription
+     * @param update
+     * @throws BngProgrammableException
+     */
+    private void programAttachment(BngAttachment attachment, HostId hostId,
+                                   HostDescription hostDescription, boolean update)
+            throws BngProgrammableException {
+        assert bngProgrammableAvailable();
+        bngProgrammable.setupAttachment(attachment);
+        if (!update) {
+            bngProgrammable.resetCounters(attachment);
+        }
+        // Trigger host creation in ONOS
+        hostProviderService.hostDetected(hostId, hostDescription, true);
+    }
+
+    /**
+     * Create an host description from the attachment information.
+     *
+     * @param cTag            Vlan C-TAG.
+     * @param sTag            Vlan S-TAG.
+     * @param hostMac         MAC address of the attachment.
+     * @param hostIp          IP address of the attachment.
+     * @param asgConnectPoint Attachment connect point from the ASG switch
+     *                        perspective.
+     * @param oltConnectPoint Attachment connect point from the OLT
+     *                        perspective.
+     * @param onuSerialNumber ONU Serial Number.
+     * @return Host description of the attachment
+     */
+    private HostDescription createHostDescription(VlanId cTag, VlanId sTag,
+                                                  MacAddress hostMac,
+                                                  IpAddress hostIp,
+                                                  ConnectPoint asgConnectPoint,
+                                                  ConnectPoint oltConnectPoint,
+                                                  String onuSerialNumber) {
+//        Set<HostLocation> hostLocation = Set.of(new HostLocation(oltConnectPoint.deviceId(),
+//                oltConnectPoint.port(),
+//                System.currentTimeMillis()));
+//        var auxLocation = String.join(",", asgConnectPoint.toString());
+        // FIXME: remove this hostLocation and decomment the above rows when aux-location patch will be merged
+        Set<HostLocation> hostLocation = Set.of(new HostLocation(asgConnectPoint.deviceId(),
+                                                                 asgConnectPoint.port(),
+                                                                 System.currentTimeMillis()));
+        var annotations = DefaultAnnotations.builder()
+//                .set(AUX_LOCATIONS_ANNOTATION, auxLocation)
+                .set(ONU_ANNOTATION, onuSerialNumber)
+                .build();
+        Set<IpAddress> ips = hostIp != null
+                ? ImmutableSet.of(hostIp) : ImmutableSet.of();
+        return new DefaultHostDescription(hostMac, sTag,
+                                          hostLocation,
+                                          ips, cTag, EthType.EtherType.QINQ.ethType(),
+                                          false, annotations);
+    }
+
+    @Override
+    public void removeAttachment(String attachmentKey) {
+        assert attachmentKey != null;
+        if (!registeredAttachment.containsKey(attachmentKey)) {
+            log.info("Attachment cannot be removed if it wasn't registered");
+            return;
+        }
+        var regAttachment = registeredAttachment.get(attachmentKey).getLeft();
+
+        final HostId hostToBeRemoved = HostId.hostId(regAttachment.macAddress(), regAttachment.sTag());
+        registeredAttachment.remove(attachmentKey);
+        // Try to remove host even if the BNG user plane device is not available
+        hostProviderService.hostVanished(hostToBeRemoved);
+        if (bngProgrammableAvailable()) {
+            bngProgrammable.removeAttachment(regAttachment);
+        } else {
+            log.info("BNG user plane not available!");
+        }
+        log.info("Attachment {} removed successfully!", regAttachment);
+    }
+
+
+    @Override
+    public Map<String, BngAttachment> getAttachments() {
+        return Maps.asMap(registeredAttachment.keySet(),
+                          key -> registeredAttachment.get(key).getLeft());
+    }
+
+    @Override
+    public BngAttachment getAttachment(String attachmentKey) {
+        return registeredAttachment.getOrDefault(attachmentKey, Pair.of(null, null))
+                .getLeft();
+    }
+
+    /**
+     * Check if the given connect point is part of the BNG user plane device.
+     * Before calling this method, make sure that bngProgrammable is available.
+     *
+     * @param asgConnectPoint The connect point to check
+     * @return
+     */
+    private boolean isCorrectlyConnected(ConnectPoint asgConnectPoint) {
+        assert bngProgrammableAvailable();
+        return asgConnectPoint.deviceId().equals(bngProgrammable.data().deviceId());
+    }
+
+    /**
+     * Setup of the BNG user plane device. This method will cleanup the BNG
+     * pipeline, initialize it and then submit all the attachment already
+     * registered.
+     *
+     * @param deviceId BNG user plane device ID
+     */
+    private void setBngDevice(DeviceId deviceId) {
+        synchronized (bnguInitialized) {
+            if (bnguInitialized.get()) {
+                log.debug("BNG device {} already initialized", deviceId);
+                return;
+            }
+            if (!isBngProgrammable(deviceId)) {
+                log.warn("{} is not BNG-U", deviceId);
+                return;
+            }
+            if (bngProgrammable != null && !bngProgrammable.data().deviceId().equals(deviceId)) {
+                log.error("Change of the BNG-U while BNG-U device is available is not supported!");
+                return;
+            }
+
+            bngProgrammable = deviceService.getDevice(deviceId).as(BngProgrammable.class);
+            log.info("Program BNG-U device {}", deviceId);
+
+            // Initialize behavior
+            try {
+                bngProgrammable.cleanUp(appId);
+                bngProgrammable.init(appId);
+                // FIXME: we can improve this re-registration, keeping track of which attachment
+                //  already has the flow rules submitted in the flow rule subsystem.
+                //  In this way we do not need to cleanUp the bngProgrammable every time it come back online.
+                //  If there is any already registered attachment, try to re-setup their attachment.
+                resubmitRegisteredAttachment();
+
+                bnguInitialized.set(true);
+            } catch (BngProgrammableException e) {
+                log.error("Error in BNG user plane, {}", e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * Resubmit all the attachments to the BNG user plane device. Before calling
+     * this method, make sure that bngProgrammable is available
+     *
+     * @throws BngProgrammableException when error in BNG user plane device.
+     */
+    private void resubmitRegisteredAttachment() throws BngProgrammableException {
+        assert bngProgrammableAvailable();
+        for (var registeredAttachemnt : registeredAttachment.entrySet()) {
+            var attachment = registeredAttachemnt.getValue().getLeft();
+            var host = registeredAttachemnt.getValue().getRight();
+            var attachentKey = registeredAttachemnt.getKey();
+            var asgConnectPoint = getAsgConnectPoint(attachment.oltConnectPoint());
+            if (attachment.type() != BngProgrammable.Attachment.AttachmentType.PPPoE) {
+                log.info("Unsupported attachment: {}", attachentKey);
+                continue;
+            }
+            if (asgConnectPoint.isPresent() && isCorrectlyConnected(asgConnectPoint.orElseThrow())) {
+                HostDescription hostDescription = createHostDescription(
+                        attachment.cTag(), attachment.sTag(),
+                        attachment.macAddress(), attachment.ipAddress(),
+                        asgConnectPoint.orElseThrow(), attachment.oltConnectPoint(),
+                        attachment.onuSerial());
+                // When resubmitting registered attachment act as the attachment is being setting up.
+                programAttachment(attachment, host, hostDescription, false);
+            } else {
+                log.info("Attachment is not connected to a valid BNG user plane: {}", attachment);
+            }
+        }
+    }
+
+    /**
+     * Unset the BNG user plane device. If available it will be cleaned-up.
+     */
+    private void unsetBngDevice() {
+        synchronized (bnguInitialized) {
+            if (bngProgrammable != null) {
+                try {
+                    bngProgrammable.cleanUp(appId);
+                } catch (BngProgrammableException e) {
+                    log.error("Error in BNG user plane, {}", e.getMessage());
+                }
+                bngProgrammable = null;
+                bnguInitialized.set(false);
+            }
+        }
+    }
+
+    /**
+     * Check if the device is registered and is BNG user plane.
+     *
+     * @param deviceId
+     * @return
+     */
+    private boolean isBngProgrammable(DeviceId deviceId) {
+        final Device device = deviceService.getDevice(deviceId);
+        return device != null && device.is(BngProgrammable.class);
+    }
+
+    /**
+     * Check if the BNG user plane is available.
+     *
+     * @return
+     * @throws BngProgrammableException
+     */
+    private boolean bngProgrammableAvailable() {
+        return bngProgrammable != null;
+    }
+
+    private void bngUpdateConfig(BngConfig config) {
+        if (config.isValid()) {
+            bngDeviceId = config.getBnguDeviceId();
+            setBngDevice(bngDeviceId);
+        }
+    }
+
+    @Override
+    public DeviceId getBngDeviceId() {
+        return bngDeviceId;
+    }
+
+    /**
+     * Updates BNG app configuration.
+     */
+    private void updateConfig() {
+        BngConfig bngConfig = cfgService.getConfig(appId, BngConfig.class);
+        if (bngConfig != null) {
+            bngUpdateConfig(bngConfig);
+        }
+    }
+
+    @Override
+    public void triggerProbe(Host host) {
+        // Do nothing here
+    }
+
+    @Override
+    public ProviderId id() {
+        return PROVIDER_ID;
+    }
+
+    /**
+     * React to new devices. The first device recognized to have BNG-U
+     * functionality is taken as BNG-U device.
+     */
+    private class InternalDeviceListener implements DeviceListener {
+        @Override
+        public void event(DeviceEvent event) {
+            DeviceId deviceId = event.subject().id();
+            if (deviceId.equals(bngDeviceId)) {
+                switch (event.type()) {
+                    case DEVICE_ADDED:
+                    case DEVICE_UPDATED:
+                    case DEVICE_AVAILABILITY_CHANGED:
+                        // FIXME: do I need the IF?
+                        //if (deviceService.isAvailable(deviceId)) {
+                        log.warn("Event: {}, SETTING BNG device", event.type());
+                        setBngDevice(deviceId);
+                        //}
+                        break;
+                    case DEVICE_REMOVED:
+                    case DEVICE_SUSPENDED:
+                        unsetBngDevice();
+                        break;
+                    case PORT_ADDED:
+                    case PORT_UPDATED:
+                    case PORT_REMOVED:
+                    case PORT_STATS_UPDATED:
+                        break;
+                    default:
+                        log.warn("Unknown device event type {}", event.type());
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Listener for network config events.
+     */
+    private class InternalConfigListener implements NetworkConfigListener {
+        @Override
+        public void event(NetworkConfigEvent event) {
+            switch (event.type()) {
+                case CONFIG_UPDATED:
+                case CONFIG_ADDED:
+                    event.config().ifPresent(config -> {
+                        bngUpdateConfig((BngConfig) config);
+                        log.info("{} updated", config.getClass().getSimpleName());
+                    });
+                    break;
+                case CONFIG_REMOVED:
+                    event.prevConfig().ifPresent(config -> {
+                        unsetBngDevice();
+                        log.info("{} removed", config.getClass().getSimpleName());
+                    });
+                    break;
+                case CONFIG_REGISTERED:
+                case CONFIG_UNREGISTERED:
+                    break;
+                default:
+                    log.warn("Unsupported event type {}", event.type());
+                    break;
+            }
+        }
+
+        @Override
+        public boolean isRelevant(NetworkConfigEvent event) {
+            if (event.configClass().equals(BngConfig.class)) {
+                return true;
+            }
+            log.debug("Ignore irrelevant event class {}", event.configClass().getName());
+            return false;
+        }
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/BngStatsManager.java b/app/src/main/java/org/opencord/bng/BngStatsManager.java
new file mode 100644
index 0000000..827b552
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/BngStatsManager.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import com.google.common.collect.Maps;
+import org.onlab.util.SharedScheduledExecutors;
+import org.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.event.AbstractListenerManager;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.behaviour.BngProgrammable;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.pi.runtime.PiCounterCellData;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Dictionary;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import static org.opencord.bng.OsgiPropertyConstants.BNG_STATISTICS_PROBE_RATE;
+import static org.opencord.bng.OsgiPropertyConstants.BNG_STATISTICS_PROBE_RATE_DEFAULT;
+
+@Component(immediate = true,
+        service = BngStatsService.class,
+        property = {
+                BNG_STATISTICS_PROBE_RATE + ":Long=" + BNG_STATISTICS_PROBE_RATE_DEFAULT,
+        }
+)
+public class BngStatsManager
+        extends AbstractListenerManager<BngStatsEvent, BngStatsEventListener> implements BngStatsService {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private final BngStatisticsMonitor bngStatsMonitor = new BngStatisticsMonitor();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ComponentConfigService componentConfigService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected BngService bngService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    private ApplicationId appId;
+    /**
+     * The BNG statistics probe rate.
+     */
+    private long bngStatisticsProbeRate = BNG_STATISTICS_PROBE_RATE_DEFAULT;
+    private ScheduledFuture<?> timeout;
+
+    @Activate
+    protected void activate() {
+        eventDispatcher.addSink(BngStatsEvent.class, listenerRegistry);
+        componentConfigService.registerProperties(getClass());
+        appId = coreService.getAppId(BngManager.BNG_APP);
+        start();
+        log.info("BNG Statistics manager activated");
+    }
+
+    @Modified
+    protected void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context != null ? context.getProperties() : new Properties();
+        Long probeRate = Tools.getLongProperty(properties, BNG_STATISTICS_PROBE_RATE);
+        if (probeRate != null) {
+            bngStatisticsProbeRate = probeRate;
+        }
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        shutdown();
+        componentConfigService.unregisterProperties(getClass(), false);
+        eventDispatcher.removeSink(BngStatsEvent.class);
+        log.info("BNG Statistics manager deactivated");
+
+    }
+
+    /**
+     * Starts the BNG statistics monitor. Does nothing if the monitor is already
+     * running.
+     */
+    private void start() {
+        synchronized (bngStatsMonitor) {
+            if (timeout == null) {
+                timeout = SharedScheduledExecutors.newTimeout(bngStatsMonitor, 0, TimeUnit.MILLISECONDS);
+            }
+        }
+    }
+
+    /**
+     * Stops the BNG statistics monitor.
+     */
+    private void shutdown() {
+        synchronized (bngStatsMonitor) {
+            if (timeout != null) {
+                timeout.cancel(true);
+                timeout = null;
+            }
+        }
+    }
+
+    private Map<String, Map<BngProgrammable.BngCounterType, PiCounterCellData>> getStats(
+            Map<String, BngAttachment> attachments) {
+        Map<String, Map<BngProgrammable.BngCounterType, PiCounterCellData>>
+                stats = Maps.newHashMap();
+        attachments.forEach((key, value) -> stats.put(key, getStats(key)));
+        return stats;
+    }
+
+    @Override
+    public Map<BngProgrammable.BngCounterType, PiCounterCellData> getStats(
+            String bngAttachmentKey) {
+        BngProgrammable bngProgrammable = getBngProgrammable(bngService.getBngDeviceId());
+        BngAttachment attachment = bngService.getAttachment(bngAttachmentKey);
+        if (bngProgrammable != null && attachment != null) {
+            try {
+                return bngProgrammable.readCounters(attachment);
+            } catch (BngProgrammable.BngProgrammableException e) {
+                log.error("Error getting statistics of {}", bngAttachmentKey);
+            }
+        }
+        return Maps.newHashMap();
+    }
+
+    @Override
+    public PiCounterCellData getControlStats() {
+        BngProgrammable bngProgrammable = getBngProgrammable(bngService.getBngDeviceId());
+        if (bngProgrammable != null) {
+            try {
+                return bngProgrammable.readControlTrafficCounter();
+            } catch (BngProgrammable.BngProgrammableException e) {
+                log.error("Error control plane packets statistics");
+            }
+        }
+        return null;
+    }
+
+    private BngProgrammable getBngProgrammable(DeviceId deviceId) {
+        if (deviceId != null && deviceService.isAvailable(deviceId)) {
+            return deviceService.getDevice(deviceId).as(BngProgrammable.class);
+        }
+        return null;
+    }
+
+    private class BngStatisticsMonitor implements Runnable {
+        @Override
+        public void run() {
+            BngProgrammable bngProgrammable = getBngProgrammable(bngService.getBngDeviceId());
+            if (bngProgrammable != null) {
+                var attachments = bngService.getAttachments();
+                Map<String, Map<BngProgrammable.BngCounterType, PiCounterCellData>>
+                        attachmentsStats = getStats(attachments);
+                // Create an event for each attachment statistics
+                attachmentsStats.forEach((attachmentKey, stats) -> {
+                    BngStatsEventSubject evInfo =
+                            new BngStatsEventSubject(attachmentKey,
+                                                     attachments.get(attachmentKey),
+                                                     stats);
+                    post(new BngStatsEvent(BngStatsEvent.EventType.STATS_UPDATED, evInfo));
+                });
+            } else {
+                log.debug("BngProgrammable not available");
+            }
+            synchronized (this) {
+                if (timeout != null) {
+                    timeout = SharedScheduledExecutors.newTimeout(this, bngStatisticsProbeRate, TimeUnit.MILLISECONDS);
+                }
+            }
+        }
+    }
+}
+
+
diff --git a/app/src/main/java/org/opencord/bng/BngUtils.java b/app/src/main/java/org/opencord/bng/BngUtils.java
new file mode 100644
index 0000000..c4cbe64
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/BngUtils.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.net.ConnectPoint;
+
+public final class BngUtils {
+
+    private BngUtils() {
+
+    }
+
+    /**
+     * Extract the BNG attachment key given an event subject.
+     *
+     * @param eventInfo The event subsject
+     * @return BNG attachment ID
+     */
+    public static String calculateBngAttachmentKey(PppoeEventSubject eventInfo) {
+        return calculateBngAttachmentKey(eventInfo.getOnuSerialNumber(),
+                                         eventInfo.getcTag(), eventInfo.getsTag(),
+                                         eventInfo.getOltConnectPoint(), eventInfo.getIpAddress(),
+                                         eventInfo.getMacAddress());
+    }
+
+    /**
+     * Extract the BNG attachment key given some of the attachment fields.
+     *
+     * @param onuSerialNumber Serial number of the ONU
+     * @param cTag            VLAN C-Tag
+     * @param sTag            VLAN S-Tag
+     * @param oltConnectPoint The OLT-level connect point
+     * @param ipAddress       The attachment IP address
+     * @param macAddress      The attachment MAC address
+     * @return The built attachment ID
+     */
+    public static String calculateBngAttachmentKey(String onuSerialNumber,
+                                                   VlanId cTag, VlanId sTag,
+                                                   ConnectPoint oltConnectPoint,
+                                                   IpAddress ipAddress,
+                                                   MacAddress macAddress) {
+        return String.join("/", onuSerialNumber, cTag.toString(),
+                           sTag.toString(), oltConnectPoint.toString(),
+                           macAddress.toString()
+        );
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/OsgiPropertyConstants.java b/app/src/main/java/org/opencord/bng/OsgiPropertyConstants.java
new file mode 100644
index 0000000..962d1e1
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/OsgiPropertyConstants.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+public final class OsgiPropertyConstants {
+
+    public static final String ENABLE_LOCAL_EVENT_HANDLER = "enableLocalEventHandler";
+    public static final boolean ENABLE_LOCAL_EVENT_HANDLER_DEFAULT = true;
+    public static final String BNG_STATISTICS_PROBE_RATE = "bngStatisticsProbeRate";
+    public static final long BNG_STATISTICS_PROBE_RATE_DEFAULT = 5000;
+
+    private OsgiPropertyConstants() {
+    }
+
+}
diff --git a/app/src/main/java/org/opencord/bng/PppoeHandlerRelay.java b/app/src/main/java/org/opencord/bng/PppoeHandlerRelay.java
new file mode 100644
index 0000000..e01db20
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/PppoeHandlerRelay.java
@@ -0,0 +1,615 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import com.google.common.collect.Maps;
+import org.onlab.packet.Data;
+import org.onlab.packet.DeserializationException;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onlab.util.ItemNotFoundException;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.event.AbstractListenerManager;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.config.ConfigFactory;
+import org.onosproject.net.config.NetworkConfigEvent;
+import org.onosproject.net.config.NetworkConfigListener;
+import org.onosproject.net.config.NetworkConfigRegistry;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.driver.DriverService;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.intf.Interface;
+import org.onosproject.net.intf.InterfaceService;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.net.packet.DefaultOutboundPacket;
+import org.onosproject.net.packet.OutboundPacket;
+import org.onosproject.net.packet.PacketContext;
+import org.onosproject.net.packet.PacketProcessor;
+import org.onosproject.net.packet.PacketService;
+import org.opencord.bng.config.PppoeRelayConfig;
+import org.opencord.bng.packets.GenericPpp;
+import org.opencord.bng.packets.Ipcp;
+import org.opencord.bng.packets.PppProtocolType;
+import org.opencord.bng.packets.Pppoe;
+import org.opencord.sadis.SadisService;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.onosproject.net.config.basics.SubjectFactories.APP_SUBJECT_FACTORY;
+
+@Component(immediate = true, service = PppoeBngControlHandler.class)
+public class PppoeHandlerRelay
+        extends AbstractListenerManager<PppoeEvent, PppoeEventListener>
+        implements PppoeBngControlHandler {
+
+    private static final IpAddress IP_ADDRESS_ZERO = IpAddress.valueOf(0);
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private final InternalConfigListener cfgListener = new InternalConfigListener();
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected NetworkConfigRegistry cfgService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected InterfaceService interfaceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected PacketService packetService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected SadisService sadisService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected DeviceService deviceService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected LinkService linkService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected DriverService driverService;
+
+    private ConfigFactory<ApplicationId, PppoeRelayConfig> cfgFactory = new ConfigFactory<>(
+            APP_SUBJECT_FACTORY,
+            PppoeRelayConfig.class,
+            PppoeRelayConfig.KEY) {
+        @Override
+        public PppoeRelayConfig createConfig() {
+            return new PppoeRelayConfig();
+        }
+    };
+    private ApplicationId appId;
+    private InternalPacketProcessor internalPacketProcessor;
+    private PppoeRelayConfig pppoeRelayConfig;
+
+    /**
+     * Ephemeral internal map to trace the attachment information. This map is
+     * mainly used to modify the packet towards the PPPoE server or towards the
+     * attachment. FIXME: this map should be cleaned after some time.
+     */
+    private Map<MacAddress, BngAttachment> mapSrcMacToAttInfo;
+
+    @Activate
+    protected void activate() {
+        mapSrcMacToAttInfo = Maps.newHashMap();
+        appId = coreService.getAppId(BngManager.BNG_APP);
+        cfgService.addListener(cfgListener);
+        cfgService.registerConfigFactory(cfgFactory);
+
+        eventDispatcher.addSink(PppoeEvent.class, listenerRegistry);
+
+        updateConfig();
+
+        internalPacketProcessor = new InternalPacketProcessor();
+        packetService.addProcessor(internalPacketProcessor, PacketProcessor.director(0));
+
+        log.info("PPPoE Handler Relay activated");
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        eventDispatcher.removeSink(PppoeEvent.class);
+        packetService.removeProcessor(internalPacketProcessor);
+        cfgService.unregisterConfigFactory(cfgFactory);
+        pppoeRelayConfig = null;
+        mapSrcMacToAttInfo = null;
+        internalPacketProcessor = null;
+
+        log.info("PPPoE Handler Relay deactivated");
+    }
+
+
+    private void updateConfig() {
+        PppoeRelayConfig newPppoeRelayConfig = cfgService.getConfig(appId, PppoeRelayConfig.class);
+        log.info("{}", newPppoeRelayConfig);
+        if (this.pppoeRelayConfig == null &&
+                newPppoeRelayConfig != null &&
+                newPppoeRelayConfig.isValid()) {
+            // TODO: what happens if this is triggered in the middle of a session auth/packet relay?
+            this.pppoeRelayConfig = newPppoeRelayConfig;
+        }
+    }
+
+    private void processPppoePacket(PacketContext context) {
+        if (!isConfigured()) {
+            log.warn("Missing BNG PPPoE handler relay config. Abort packet processing");
+            return;
+        }
+        Ethernet eth = context.inPacket().parsed();
+        log.debug("Parsing the PPPoE header");
+        //FIXME: PPPoE and above headers are extracted from the ethernet
+        // payload. In case we want to modify the PPPoE/upper-layer header, remember to
+        // serialize it back on the Ethernet payload.
+        Pppoe pppoe = parsePppoeHeader(eth);
+        if (pppoe == null) {
+            return;
+        }
+
+        log.debug("Processing PPPoE header");
+
+        // Check from where the packet is received and if the interface is configured
+        ConnectPoint heardOn = context.inPacket().receivedFrom();
+        if (!heardOn.equals(pppoeRelayConfig.getPppoeServerConnectPoint()) &&
+                !heardOn.equals(pppoeRelayConfig.getAsgToOltConnectPoint()) &&
+                !interfaceService.getInterfacesByPort(heardOn).isEmpty()) {
+            log.info("PPPoE packet from unregistered port {}", heardOn);
+            return;
+        }
+
+        // Retrieve the MAC address of the device that intercepted the packet.
+        // This MAC address is the actual PPPoE server MAC address seen by the attachment
+        MacAddress bnguMac = interfaceService.getInterfacesByPort(heardOn).iterator().next().mac();
+
+        VlanId cTag = VlanId.vlanId(eth.getVlanID());
+        VlanId sTag = VlanId.vlanId(eth.getQinQVID());
+
+        // --------------------------------------- DEBUG ----------------------------------------------
+        if (Pppoe.isPPPoED(eth)) {
+            log.info("Received {} packet from {}",
+                     pppoe.getPacketType(),
+                     heardOn);
+        }
+
+        StringBuilder logPacketPppoes = new StringBuilder();
+        if (Pppoe.isPPPoES(eth)) {
+            logPacketPppoes.append("Received PPPoES ")
+                    .append(PppProtocolType.lookup(pppoe.getPppProtocol()).type())
+                    .append(" packet from ").append(heardOn).append(".");
+        }
+        if (logPacketPppoes.length() > 0) {
+            log.debug(logPacketPppoes.toString());
+        }
+        log.debug(eth.toString());
+        // --------------------------------------------------------------------------------------------
+
+        if (heardOn.equals(pppoeRelayConfig.getPppoeServerConnectPoint())) {
+            // DOWNSTREAM PACKET: from the PPPoE server to the attachment.
+            MacAddress dstMac = eth.getDestinationMAC();
+            log.debug("Packet to the attachment: {}", eth);
+            if (!mapSrcMacToAttInfo.containsKey(dstMac)) {
+                BngAttachment newAttInfo = PppoeBngAttachment.builder()
+                        .withMacAddress(dstMac)
+                        .withSTag(sTag)
+                        .withCTag(cTag)
+                        .withQinqTpid(eth.getQinQTPID())
+                        .build();
+                mapSrcMacToAttInfo.put(dstMac, newAttInfo);
+            }
+            // Retrieve the information about the attachment from the internal MAP
+            BngAttachment attInfo = mapSrcMacToAttInfo.get(dstMac);
+
+            // Generate the events for this attachment
+            manageAttachmentStateDownstream(eth, pppoe, attInfo);
+
+            modPacketForAttachment(eth, attInfo, bnguMac);
+
+            log.debug("Packet modified as: {}", eth);
+            // Send out the packet towards the OLT
+            forwardPacket(pppoeRelayConfig.getAsgToOltConnectPoint(), eth);
+        } else {
+            // UPSTREAM DIRECTION: from the attachment to the PPPoE server
+            MacAddress srcMac = eth.getSourceMAC();
+            if (!mapSrcMacToAttInfo.containsKey(srcMac)) {
+                BngAttachment newAttInfo = PppoeBngAttachment.builder()
+                        .withMacAddress(srcMac)
+                        .withSTag(sTag)
+                        .withCTag(cTag)
+                        .withQinqTpid(eth.getQinQTPID())
+                        .build();
+                mapSrcMacToAttInfo.put(srcMac, newAttInfo);
+            }
+
+            manageAttachmentStateUpstream(eth, pppoe);
+
+            modPacketForPPPoEServer(eth);
+            log.debug("Packet modified as: {}", eth);
+            // Forward packet to the PPPoE server connect point
+            forwardPacket(pppoeRelayConfig.getPppoeServerConnectPoint(), eth);
+        }
+    }
+
+    /**
+     * Generate an event related to PPPoE or IPCP state change.
+     *
+     * @param bngAppEventType Event type
+     * @param ip              IP Address if it has been assigned, otherwise
+     *                        0.0.0.0
+     * @param attInfo         Local attachment information
+     */
+    private void generateEventPppoe(PppoeEvent.EventType bngAppEventType,
+                                    BngAttachment attInfo, short pppoeSessionId,
+                                    IpAddress ip) {
+        // Retrive the NNI connect point
+        var oltConnectPoint = getOltConnectPoint(attInfo.sTag(), attInfo.cTag(),
+                                                 pppoeRelayConfig.getAsgToOltConnectPoint());
+        assert oltConnectPoint.orElse(null) != null;
+        log.info("Generating event of type {}", bngAppEventType);
+        post(new PppoeEvent(
+                     bngAppEventType,
+                     new PppoeEventSubject(
+                             oltConnectPoint.orElseThrow(),
+                             ip,
+                             attInfo.macAddress(),
+                             getPortNameAnnotation(oltConnectPoint.orElse(null)),
+                             pppoeSessionId,
+                             attInfo.sTag(),
+                             attInfo.cTag())
+             )
+        );
+    }
+
+    /**
+     * Generate attachment related state for the upstream direction.
+     *
+     * @param eth   The ethernet packet
+     * @param pppoe PPPoE header
+     */
+    private void manageAttachmentStateUpstream(Ethernet eth, Pppoe pppoe) {
+        PppoeEvent.EventType eventType = null;
+        MacAddress srcMac = eth.getSourceMAC();
+        VlanId cTag = VlanId.vlanId(eth.getVlanID());
+        VlanId sTag = VlanId.vlanId(eth.getQinQVID());
+        BngAttachment attInfo = mapSrcMacToAttInfo.get(srcMac);
+        switch (PppProtocolType.lookup(pppoe.getPppProtocol())) {
+            case IPCP:
+                // Attachment information should be already present
+                Ipcp ipcp = (Ipcp) pppoe.getPayload();
+                if (ipcp.getCode() == Ipcp.CONF_REQ) {
+                    log.debug("IPCP configuration request from attachment");
+                    eventType = PppoeEvent.EventType.IPCP_CONF_REQUEST;
+                }
+                break;
+            case NO_PROTOCOL:
+                if (Pppoe.isPPPoED(eth) &&
+                        pppoe.getPacketType() == Pppoe.PppoeType.PADI) {
+                    log.info("PADI received from attachment {}/{}. Saved in internal store",
+                             srcMac, sTag);
+                    eventType = PppoeEvent.EventType.SESSION_INIT;
+                }
+                break;
+            default:
+        }
+        if (eventType != null) {
+            generateEventPppoe(eventType, attInfo, pppoe.getSessionId(), IP_ADDRESS_ZERO);
+        }
+    }
+
+    private String getPortNameAnnotation(ConnectPoint oltConnectPoint) {
+        return deviceService.getPort(oltConnectPoint.deviceId(),
+                                     oltConnectPoint.port()).annotations().value("portName");
+    }
+
+    /**
+     * Generate attachment related state for the downstream direction.
+     *
+     * @param eth     The ethernet packet
+     * @param pppoe   PPPoE header
+     * @param attInfo Attachment info stored in the internal store
+     */
+    private void manageAttachmentStateDownstream(Ethernet eth, Pppoe pppoe,
+                                                 BngAttachment attInfo) {
+        PppoeEvent.EventType eventType = null;
+        IpAddress assignedIpAddress = IP_ADDRESS_ZERO;
+        switch (PppProtocolType.lookup(pppoe.getPppProtocol())) {
+            case IPCP:
+                Ipcp ipcp = (Ipcp) pppoe.getPayload();
+                if (ipcp.getCode() == Ipcp.ACK) {
+                    log.info("Received a IPCP ACK from Server. Assigned IP Address {}",
+                             ipcp.getIpAddress());
+                    assignedIpAddress = ipcp.getIpAddress();
+                    eventType = PppoeEvent.EventType.IPCP_CONF_ACK;
+                }
+                break;
+
+            case CHAP:
+                // Check if server has correctly authenticated the attachment
+                GenericPpp chap = (GenericPpp) pppoe.getPayload();
+                if (chap.getCode() == GenericPpp.CHAP_CODE_SUCCESS) {
+                    log.info("CHAP authentication success: {}", attInfo.macAddress());
+                    eventType = PppoeEvent.EventType.AUTH_SUCCESS;
+                }
+                if (chap.getCode() == GenericPpp.CHAP_CODE_FAILURE) {
+                    log.info("CHAP authentication failed: {}", attInfo.macAddress());
+                    eventType = PppoeEvent.EventType.AUTH_FAILURE;
+                }
+                break;
+
+            case PAP:
+                // Check if server has correctly authenticated the attachment
+                GenericPpp pap = (GenericPpp) pppoe.getPayload();
+                if (pap.getCode() == GenericPpp.PAP_AUTH_ACK) {
+                    log.info("PAP authentication success: {}", attInfo.macAddress());
+                    eventType = PppoeEvent.EventType.AUTH_SUCCESS;
+                }
+                if (pap.getCode() == GenericPpp.PAP_AUTH_NACK) {
+                    log.info("PAP authentication failed: {}", attInfo.macAddress());
+                    eventType = PppoeEvent.EventType.AUTH_FAILURE;
+                }
+                break;
+
+            case LCP:
+                GenericPpp lcp = (GenericPpp) pppoe.getPayload();
+                if (lcp.getCode() == GenericPpp.CODE_TERM_REQ) {
+                    log.info("LCP Termination request from PPPoE server");
+                    eventType = PppoeEvent.EventType.SESSION_TERMINATION;
+                }
+                break;
+
+            case NO_PROTOCOL:
+                if (Pppoe.isPPPoED(eth)) {
+                    switch (pppoe.getPacketType()) {
+                        case PADS:
+                            // Set the current PPPoE session ID
+                            eventType = PppoeEvent.EventType.SESSION_CONFIRMATION;
+                            break;
+                        case PADT:
+                            log.info("PADT received from PPPoE server");
+                            eventType = PppoeEvent.EventType.SESSION_TERMINATION;
+                            break;
+                        default:
+                    }
+                }
+                break;
+            default:
+        }
+        // Generate and event if needed
+        if (eventType != null) {
+            generateEventPppoe(eventType, attInfo, pppoe.getSessionId(), assignedIpAddress);
+        }
+    }
+
+    private Pppoe parsePppoeHeader(Ethernet eth) {
+        try {
+            return Pppoe.deserializer().deserialize(((Data) eth.getPayload()).getData(),
+                                                    0,
+                                                    ((Data) eth.getPayload()).getData().length);
+        } catch (DeserializationException e) {
+            log.error("Error parsing the PPPoE Headers, packet skipped. \n" + e.getMessage());
+            return null;
+        }
+    }
+
+
+    /**
+     * Apply the modification to the packet to send it to the attachment.
+     *
+     * @param eth     Packet to be modified
+     * @param attInfo Attachment information store in the internal map
+     */
+    private void modPacketForAttachment(Ethernet eth,
+                                        BngAttachment attInfo,
+                                        MacAddress newSourceMac) {
+        eth.setVlanID(attInfo.cTag().toShort());
+        eth.setQinQVID(attInfo.sTag().toShort());
+        eth.setQinQTPID(attInfo.qinqTpid());
+        eth.setSourceMACAddress(newSourceMac);
+    }
+
+    /**
+     * Apply the modification to the packet to send it to the PPPoE Server.
+     *
+     * @param eth Packet to be modified
+     */
+    private void modPacketForPPPoEServer(Ethernet eth) {
+        // TODO: rewrite it. Retrieve information about the interface where
+        //  PPPoE Server is connected and apply them to the packet
+        Set<Interface> interfaces = interfaceService.getInterfacesByPort(pppoeRelayConfig.getPppoeServerConnectPoint());
+        if (interfaces != null &&
+                interfaces.iterator().hasNext() &&
+                interfaces.iterator().next().vlanTagged() != null &&
+                interfaces.iterator().next().vlanTagged().iterator().hasNext()) {
+            VlanId vlanId = interfaces.iterator().next().vlanTagged().iterator().next();
+            if (vlanId != null && vlanId != VlanId.NONE) {
+                eth.setVlanID(vlanId.toShort());
+                eth.setQinQVID(Ethernet.VLAN_UNTAGGED);
+            } else {
+                eth.setVlanID(Ethernet.VLAN_UNTAGGED);
+                eth.setQinQVID(Ethernet.VLAN_UNTAGGED);
+            }
+        } else {
+            eth.setVlanID(Ethernet.VLAN_UNTAGGED);
+            eth.setQinQVID(Ethernet.VLAN_UNTAGGED);
+        }
+        // Modify DST Mac Address with the one of the PPPoE Server
+        if (!eth.getDestinationMAC().isBroadcast()) {
+            eth.setDestinationMACAddress(pppoeRelayConfig.getPppoeMacAddress());
+        }
+    }
+
+    /**
+     * Retrieve the NNI Connect Point given the S-Tag, C-Tag and the OLT facing
+     * ASG connect point.
+     *
+     * @param sTag                 The S-Tag VLAN tag
+     * @param cTag                 The C-Tag VLAN tag
+     * @param asgToOltConnectPoint Connect point from ASG to OLT.
+     * @return
+     */
+    private Optional<ConnectPoint> getOltConnectPoint(
+            VlanId sTag, VlanId cTag, ConnectPoint asgToOltConnectPoint) {
+        // Retrieve the UNI port where this attachment is attached to. We assume
+        // an attachment is uniquely identified by its c-tag and s-tag in the
+        // scope of an OLT. In lack of a better API in SADIS, we retrieve info
+        // for all OLT ports and match those that have same c-tag and s-tag as
+        // the given attachemnt info.
+
+        var oltDeviceIds = linkService.getIngressLinks(asgToOltConnectPoint)
+                .stream()
+                .map(link -> link.src().deviceId())
+                .filter(deviceId -> {
+                    try {
+                        return driverService.getDriver(deviceId)
+                                .name().contains("voltha");
+                    } catch (ItemNotFoundException e) {
+                        log.warn("Unable to find driver for {}", deviceId);
+                        return false;
+                    }
+                })
+                .collect(Collectors.toSet());
+
+        var oltConnectPoints = oltDeviceIds.stream()
+                .flatMap(deviceId -> deviceService.getPorts(deviceId).stream())
+                .filter(port -> {
+                    var portName = port.annotations().value("portName");
+                    if (portName == null) {
+                        return false;
+                    }
+                    var info = sadisService.getSubscriberInfoService()
+                            .get(portName);
+                    return info != null &&
+                            Objects.equals(cTag, info.cTag()) &&
+                            Objects.equals(sTag, info.sTag());
+                })
+                .map(port -> new ConnectPoint(port.element().id(), port.number()))
+                .collect(Collectors.toSet());
+
+        if (oltConnectPoints.isEmpty()) {
+            log.error("Unable to find a connect point for attachment with S-Tag {} C-Tag {} on OLTs {}",
+                      sTag, cTag, oltDeviceIds);
+            return Optional.empty();
+        } else if (oltConnectPoints.size() > 1) {
+            log.error("Multiple OLT connect points found for attachment S-Tag {} C-Tag {}," +
+                              "aborting discovery as this is NOT supported (yet)..." +
+                              "oltConnectPoints={}",
+                      sTag, cTag, oltConnectPoints);
+            return Optional.empty();
+        }
+
+        return Optional.of(oltConnectPoints.iterator().next());
+    }
+
+    /**
+     * Send the specified packet, out to the specified connect point.
+     *
+     * @param toPort Output port to send the packet
+     * @param packet Packet to be sent
+     */
+    private void forwardPacket(ConnectPoint toPort, Ethernet packet) {
+        TrafficTreatment toPortTreatment = DefaultTrafficTreatment.builder()
+                .setOutput(toPort.port()).build();
+        OutboundPacket outboundPacket = new DefaultOutboundPacket(
+                toPort.deviceId(), toPortTreatment, ByteBuffer.wrap(packet.serialize()));
+        packetService.emit(outboundPacket);
+    }
+
+    /**
+     * Check if the handler is correctly configured.
+     *
+     * @return True if it is correctly configure, False otherwise
+     */
+    private boolean isConfigured() {
+        return pppoeRelayConfig != null;
+    }
+
+    /**
+     * The internal packet processor for PPPoE packets.
+     */
+    private class InternalPacketProcessor implements PacketProcessor {
+
+        @Override
+        public void process(PacketContext context) {
+            processPacketInternal(context);
+        }
+
+        private void processPacketInternal(PacketContext context) {
+            if (context == null || context.isHandled()) {
+                return;
+            }
+            Ethernet eth = context.inPacket().parsed();
+            if (eth == null) {
+                return;
+            }
+            if (!Pppoe.isPPPoES(eth) && !Pppoe.isPPPoED(eth)) {
+                return;
+            }
+            processPppoePacket(context);
+        }
+    }
+
+    /**
+     * Listener for network config events.
+     */
+    private class InternalConfigListener implements NetworkConfigListener {
+        @Override
+        public void event(NetworkConfigEvent event) {
+            switch (event.type()) {
+                case CONFIG_ADDED:
+                    log.info("CONFIG_ADDED");
+                    event.config().ifPresent(config -> {
+                        pppoeRelayConfig = ((PppoeRelayConfig) config);
+                        log.info("{} added", config.getClass().getSimpleName());
+                    });
+                    break;
+                // TODO: support at least updated and removed events
+                case CONFIG_UPDATED:
+                case CONFIG_REGISTERED:
+                case CONFIG_UNREGISTERED:
+                case CONFIG_REMOVED:
+                default:
+                    log.warn("Unsupported event type {}", event.type());
+                    break;
+            }
+        }
+
+        @Override
+        public boolean isRelevant(NetworkConfigEvent event) {
+            if (event.configClass().equals(PppoeRelayConfig.class)) {
+                return true;
+            }
+            log.debug("Ignore irrelevant event class {}", event.configClass().getName());
+            return false;
+        }
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/SimpleAttachmentEventHandler.java b/app/src/main/java/org/opencord/bng/SimpleAttachmentEventHandler.java
new file mode 100644
index 0000000..eb4ecd6
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/SimpleAttachmentEventHandler.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2019-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.opencord.bng;
+
+import org.glassfish.jersey.internal.guava.Sets;
+import org.onlab.util.Tools;
+import org.onosproject.cfg.ComponentConfigService;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.store.service.StorageService;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Modified;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Dictionary;
+import java.util.Properties;
+import java.util.Set;
+
+import static org.opencord.bng.OsgiPropertyConstants.ENABLE_LOCAL_EVENT_HANDLER;
+import static org.opencord.bng.OsgiPropertyConstants.ENABLE_LOCAL_EVENT_HANDLER_DEFAULT;
+
+/**
+ * Service to intercept the PPPoE Handler events and trigger the creation of a
+ * new attachment in BNG service.
+ */
+@Component(immediate = true,
+        service = SimpleAttachmentEventHandler.class,
+        property = {
+                ENABLE_LOCAL_EVENT_HANDLER + ":Boolean=" + ENABLE_LOCAL_EVENT_HANDLER_DEFAULT,
+        }
+)
+public class SimpleAttachmentEventHandler {
+
+    private static final String ATTACHMENT_ID_GENERATOR_NAME = "SIMPLE_ATTACHMENT_EVENT_HANDLER_ATTACHMENT_ID";
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected ComponentConfigService componentConfigService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected PppoeBngControlHandler pppoEHandlerRelay;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected BngService bngService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected CoreService coreService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY)
+    protected StorageService storageService;
+
+    /**
+     * Whether to enable of not the local attachment event handler, for debugging/development.
+     */
+    private boolean enableLocalEventHandler = ENABLE_LOCAL_EVENT_HANDLER_DEFAULT;
+    private InternalPppoeEvent pppoeEventListener = new InternalPppoeEvent();
+
+    // Map to store the attachment that this component has submitted through the BNG Service
+    private Set<String> addedAttachmentKeys;
+
+    private ApplicationId appId;
+
+    @Activate
+    protected void activate() {
+        appId = coreService.getAppId(BngManager.BNG_APP);
+        addedAttachmentKeys = Sets.newHashSet();
+        componentConfigService.registerProperties(getClass());
+        pppoEHandlerRelay.addListener(pppoeEventListener);
+        log.info("Simple Attachment Event Handler STARTED");
+    }
+
+    @Modified
+    public void modified(ComponentContext context) {
+        Dictionary<?, ?> properties = context != null ? context.getProperties() : new Properties();
+
+        Boolean localEvent = Tools.isPropertyEnabled(properties, ENABLE_LOCAL_EVENT_HANDLER);
+        if (localEvent != null) {
+            enableLocalEventHandler = localEvent;
+        }
+    }
+
+    @Deactivate
+    protected void deactivate() {
+        pppoEHandlerRelay.removeListener(pppoeEventListener);
+        addedAttachmentKeys = null;
+        componentConfigService.unregisterProperties(getClass(), false);
+        log.info("Simple Attachment Event Handler STOPPED");
+    }
+
+    /**
+     * Listener for BNG Attachment event for PPPoE attachments.
+     */
+    class InternalPppoeEvent implements PppoeEventListener {
+        @Override
+        public void event(PppoeEvent event) {
+            PppoeEventSubject eventInfo = event.subject();
+            String attachmentKey = BngUtils.calculateBngAttachmentKey(eventInfo);
+            switch (event.type()) {
+                case IPCP_CONF_ACK:
+                    log.debug("Received IPCP_CONF_ACK event, submit a new attachment");
+                    log.debug(eventInfo.toString());
+                    BngAttachment newAttachment = PppoeBngAttachment.builder()
+                            .withPppoeSessionId(eventInfo.getSessionId())
+                            .withApplicationId(appId)
+                            .withCTag(eventInfo.getcTag())
+                            .withSTag(eventInfo.getsTag())
+                            .withIpAddress(eventInfo.getIpAddress())
+                            .withMacAddress(eventInfo.getMacAddress())
+                            .withOnuSerial(eventInfo.getOnuSerialNumber())
+                            .withOltConnectPoint(eventInfo.getOltConnectPoint())
+                            .lineActivated(true)
+                            .build();
+                    if (!addedAttachmentKeys.add(attachmentKey)) {
+                        log.warn("Attachment ID already present. Re-submit the attachment");
+                    }
+                    bngService.setupAttachment(attachmentKey, newAttachment);
+                    break;
+
+                case SESSION_TERMINATION:
+                    attachmentKey =  BngUtils.calculateBngAttachmentKey(eventInfo);
+                    log.debug("Received SESSION_TERMINATION event, remove the attachment {}",
+                              attachmentKey);
+                    if (!addedAttachmentKeys.remove(attachmentKey)) {
+                        log.debug("Received SESSION_TERMINATION event, for attachment {} " +
+                                          "but attachment not present in local store", attachmentKey);
+                    } else {
+                        log.debug("Received SESSION_TERMINATION event, remove the attachment {}",
+                                  attachmentKey);
+                        bngService.removeAttachment(attachmentKey);
+                    }
+                    break;
+                case AUTH_FAILURE:
+                case AUTH_REQUEST:
+                case AUTH_SUCCESS:
+                case SESSION_INIT:
+                case IPCP_CONF_REQUEST:
+                case SESSION_CONFIRMATION:
+                    log.debug("Received event {}, nothing to do here.", event.type().toString());
+                    break;
+                default:
+                    throw new IllegalStateException("Unexpected value: " + event.type() +
+                                                            ", for attachment: " + attachmentKey);
+            }
+        }
+
+        @Override
+        public boolean isRelevant(PppoeEvent event) {
+            return enableLocalEventHandler &&
+                    event.subject().getClass().equals(PppoeEventSubject.class);
+        }
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/cli/AddAttachment.java b/app/src/main/java/org/opencord/bng/cli/AddAttachment.java
new file mode 100644
index 0000000..bea0c1f
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/AddAttachment.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2019-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.opencord.bng.cli;
+
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.cli.net.DeviceIdCompleter;
+import org.onosproject.cli.net.PortNumberCompleter;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.net.ConnectPoint;
+import org.opencord.bng.BngAttachment;
+import org.opencord.bng.BngManager;
+import org.opencord.bng.BngService;
+import org.opencord.bng.BngUtils;
+import org.opencord.bng.PppoeBngAttachment;
+
+@Service
+@Command(scope = "bng", name = "attachment-add",
+        description = "Add an attachment on the BNG in disabled state")
+public class AddAttachment extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "macAddress", description = "Mac Address of the attachment", required = true)
+    String macAddressString = null;
+
+    @Argument(index = 1, name = "ipAddress", description = "IP Address of the attachment", required = true)
+    String ipAddressString = null;
+
+    @Argument(index = 2, name = "cTag", description = "VLAN C-TAG of the attachment", required = true)
+    short cTag = 0;
+
+    @Argument(index = 3, name = "sTag", description = "VLAN S-TAG of the attachment", required = true)
+    short sTag = 0;
+
+    @Argument(index = 4, name = "pppoeSessionId",
+            description = "PPPoE session ID of the attachment", required = true)
+    short pppoeSessionId = 0;
+
+    @Argument(index = 5, name = "oltDeviceID",
+            description = "OLT device ID the attachment is connected to", required = true)
+    @Completion(DeviceIdCompleter.class)
+    String oltDeviceId = null;
+
+    @Argument(index = 6, name = "portNumber",
+            description = "Port number on the OLT device", required = true)
+    @Completion(PortNumberCompleter.class)
+    String oltPortNumber = null;
+
+    @Argument(index = 7, name = "onuSerial",
+            description = "Serial number for the ONU of the attachment", required = true)
+    @Completion(OnuSerialCompleter.class)
+    String onuSerial = null;
+
+    @Option(name = "-d", aliases = "--disable", description = "Disable the specified attachment",
+            required = false, multiValued = false)
+    boolean disable = false;
+
+
+    @Override
+    protected void doExecute() throws Exception {
+        CoreService coreService = get(CoreService.class);
+
+        ApplicationId appId = coreService.getAppId(BngManager.BNG_APP);
+        ConnectPoint uniCp = ConnectPoint.fromString(oltDeviceId + "/" + oltPortNumber);
+        MacAddress macAddress = MacAddress.valueOf(macAddressString);
+        IpAddress ipAddress = IpAddress.valueOf(ipAddressString);
+
+        String attachmentKey = "CLI" + "/" +
+                BngUtils.calculateBngAttachmentKey(onuSerial, VlanId.vlanId(cTag),
+                                                   VlanId.vlanId(sTag), uniCp, ipAddress,
+                                                   macAddress);
+
+        BngAttachment newAttachment = PppoeBngAttachment.builder()
+                .withPppoeSessionId(pppoeSessionId)
+                .withApplicationId(appId)
+                .withMacAddress(macAddress)
+                .withCTag(VlanId.vlanId(cTag))
+                .withSTag(VlanId.vlanId(sTag))
+                .withIpAddress(ipAddress)
+                .withOltConnectPoint(uniCp)
+                .withOnuSerial(onuSerial)
+                .lineActivated(!disable)
+                .build();
+        BngService bngService = get(BngService.class);
+        bngService.setupAttachment(attachmentKey, newAttachment);
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/cli/AttachmentKeyCompleter.java b/app/src/main/java/org/opencord/bng/cli/AttachmentKeyCompleter.java
new file mode 100644
index 0000000..4a6cdff
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/AttachmentKeyCompleter.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019-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.opencord.bng.cli;
+
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.onosproject.cli.AbstractShellCommand;
+import org.opencord.bng.BngService;
+
+import java.util.List;
+import java.util.SortedSet;
+
+/**
+ * Attachment Key completer.
+ */
+@Service
+public class AttachmentKeyCompleter implements Completer {
+
+    @Override
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        // Delegate string completer
+        StringsCompleter delegate = new StringsCompleter();
+
+        BngService service = AbstractShellCommand.get(BngService.class);
+        SortedSet<String> strings = delegate.getStrings();
+        service.getAttachments().keySet().forEach(strings::add);
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(session, commandLine, candidates);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/bng/cli/AttachmentStats.java b/app/src/main/java/org/opencord/bng/cli/AttachmentStats.java
new file mode 100644
index 0000000..346f554
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/AttachmentStats.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2019-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.opencord.bng.cli;
+
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.opencord.bng.BngAttachment;
+import org.opencord.bng.BngService;
+import org.opencord.bng.BngStatsService;
+
+import static java.util.Map.Entry.comparingByKey;
+
+@Service
+@Command(scope = "bng", name = "attachment-stats",
+        description = "Get the stats (registers) of the attachments")
+public class AttachmentStats extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "attachmentKey", description = "Attachment Key. No ID or 0 means ALL")
+    @Completion(AttachmentKeyCompleter.class)
+    String attachmentKey = null;
+
+    @Override
+    protected void doExecute() throws Exception {
+        BngService bngService = AbstractShellCommand.get(BngService.class);
+
+        print("STATISTICS");
+        if (attachmentKey == null) {
+            // Print the statistics for all the registered attachments
+            bngService.getAttachments().forEach(this::printAttachmentStats);
+        } else {
+            printAttachmentStats(attachmentKey, bngService.getAttachment(attachmentKey));
+        }
+    }
+
+    private void printAttachmentStats(String attachmentKey, BngAttachment attachment) {
+        if (attachment != null) {
+            BngStatsService bngStatsService = AbstractShellCommand.get(BngStatsService.class);
+            print("MAC: " + attachment.macAddress().toString()
+                          + "\nC_TAG: " + attachment.cTag().toShort()
+                          + "\nS_TAG: " + attachment.sTag().toString()
+                          + "\nIP: " + attachment.ipAddress());
+            bngStatsService.getStats(attachmentKey).entrySet().stream().sorted(comparingByKey())
+                    .forEach(
+                            (entry) -> {
+                                print(BngCliUtils.niceCounterName(entry.getKey()));
+                                print("\tPackets:" + entry.getValue().packets());
+                                print("\tBytes:\t" + entry.getValue().bytes());
+                            }
+                    );
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/bng/cli/Attachments.java b/app/src/main/java/org/opencord/bng/cli/Attachments.java
new file mode 100644
index 0000000..52203d4
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/Attachments.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019-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.opencord.bng.cli;
+
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.opencord.bng.BngService;
+
+@Service
+@Command(scope = "bng", name = "attachments",
+        description = "Get the list of registered attachment")
+public class Attachments extends AbstractShellCommand {
+
+    @Override
+    protected void doExecute() throws Exception {
+        BngService bngService = AbstractShellCommand.get(BngService.class);
+        var attachments = bngService.getAttachments();
+        print("Registered attachments (size: " + attachments.size() + "):");
+        print(attachments.toString());
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/cli/BngCliUtils.java b/app/src/main/java/org/opencord/bng/cli/BngCliUtils.java
new file mode 100644
index 0000000..3776117
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/BngCliUtils.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2019-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.opencord.bng.cli;
+
+import org.onosproject.net.behaviour.BngProgrammable.BngCounterType;
+
+/**
+ * Utilities to print counter names in CLI output.
+ */
+public final class BngCliUtils {
+
+    private BngCliUtils() {
+    }
+
+    /**
+     * Prints a nicer counter name on CLI output.
+     *
+     * @param counterName The counter type to be converted
+     * @return The string representing the counter
+     */
+    static String niceCounterName(BngCounterType counterName) {
+        switch (counterName) {
+            case CONTROL_PLANE:
+                return "Upstream Control";
+            case DOWNSTREAM_RX:
+                return "Downstream Received";
+            case DOWNSTREAM_TX:
+                return "Downstream Transmitted";
+            case UPSTREAM_DROPPED:
+                return "Upstream Dropped";
+            case UPSTREAM_TX:
+                return "Upstream Terminated";
+            default:
+                return "UNKNOWN";
+        }
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/cli/ControlPacketsStats.java b/app/src/main/java/org/opencord/bng/cli/ControlPacketsStats.java
new file mode 100644
index 0000000..a552fc4
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/ControlPacketsStats.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019-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.opencord.bng.cli;
+
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.pi.runtime.PiCounterCellData;
+import org.opencord.bng.BngStatsService;
+
+@Service
+@Command(scope = "bng", name = "control-stats",
+        description = "Retrieve statistics of control plane packets of un-registered attachments")
+public class ControlPacketsStats extends AbstractShellCommand {
+
+    @Override
+    protected void doExecute() throws Exception {
+        BngStatsService bngStatsService = AbstractShellCommand.get(BngStatsService.class);
+        PiCounterCellData stats = bngStatsService.getControlStats();
+        if (stats != null) {
+            print("Packets: " + stats.packets());
+            print("Bytes:\t" + stats.bytes());
+        } else {
+            print("No BNG user plane device configured");
+        }
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/cli/EnableAttachment.java b/app/src/main/java/org/opencord/bng/cli/EnableAttachment.java
new file mode 100644
index 0000000..d209c76
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/EnableAttachment.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2019-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.opencord.bng.cli;
+
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.behaviour.BngProgrammable;
+import org.opencord.bng.BngAttachment;
+import org.opencord.bng.BngService;
+import org.opencord.bng.PppoeBngAttachment;
+
+@Service
+@Command(scope = "bng", name = "attachment-enable",
+        description = "Enable/Disable an attachment")
+public class EnableAttachment extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "attachmentKey", description = "Attachment Key", required = true)
+    @Completion(AttachmentKeyCompleter.class)
+    String attachmentKey = null;
+
+    @Option(name = "-d", aliases = "--disable", description = "Disable the specified attachment",
+            required = false, multiValued = false)
+    boolean disable = false;
+
+    @Override
+    protected void doExecute() throws Exception {
+        BngService bngService = AbstractShellCommand.get(BngService.class);
+
+        BngAttachment attachment = bngService.getAttachment(attachmentKey);
+
+        if (attachment == null) {
+            print("Attachment " + attachmentKey.toString() + "not found!");
+            return;
+        }
+        if (attachment.lineActive() == !disable) {
+            print("Attachment is already " + (disable ? "disabled" : "enabled"));
+            return;
+        }
+        if (!attachment.type().equals(BngProgrammable.Attachment.AttachmentType.PPPoE)) {
+            print((disable ? "Disable" : "Enable") + " supported only for PPPoE attachment");
+            return;
+        }
+
+        BngAttachment newAttachment = PppoeBngAttachment.builder()
+                .withPppoeSessionId(attachment.pppoeSessionId())
+                .withApplicationId(attachment.appId())
+                .withMacAddress(attachment.macAddress())
+                .withCTag(attachment.cTag())
+                .withSTag(attachment.sTag())
+                .withIpAddress(attachment.ipAddress())
+                .withOltConnectPoint(attachment.oltConnectPoint())
+                .withOnuSerial(attachment.onuSerial())
+                .lineActivated(!disable)
+                .build();
+        print(disable ? "Disabling" : "Enabling" + " attachment: " + newAttachment.toString());
+        bngService.setupAttachment(attachmentKey, newAttachment);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/bng/cli/OnuSerialCompleter.java b/app/src/main/java/org/opencord/bng/cli/OnuSerialCompleter.java
new file mode 100644
index 0000000..6f87a3d
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/OnuSerialCompleter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019-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.opencord.bng.cli;
+
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.apache.karaf.shell.api.console.CommandLine;
+import org.apache.karaf.shell.api.console.Completer;
+import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.completers.StringsCompleter;
+import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.net.device.DeviceService;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * ONU serial number completer.
+ */
+@Service
+public class OnuSerialCompleter implements Completer {
+
+    @Override
+    public int complete(Session session, CommandLine commandLine, List<String> candidates) {
+        // Delegate string completer
+        StringsCompleter delegate = new StringsCompleter();
+
+        DeviceService deviceService = AbstractShellCommand.get(DeviceService.class);
+
+        Set<String> candidateOnuSerials = delegate.getStrings();
+        deviceService.getDevices()
+                .forEach(device -> deviceService.getPorts(
+                        device.id()).stream()
+                        .map(port -> port.annotations().value("portName"))
+                        .forEach(candidateOnuSerials::add)
+                );
+        // Now let the completer do the work for figuring out what to offer.
+        return delegate.complete(session, commandLine, candidates);
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/bng/cli/RemoveAttachment.java b/app/src/main/java/org/opencord/bng/cli/RemoveAttachment.java
new file mode 100644
index 0000000..00f0527
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/RemoveAttachment.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019-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.opencord.bng.cli;
+
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Completion;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
+import org.onosproject.cli.AbstractShellCommand;
+import org.opencord.bng.BngService;
+
+@Service
+@Command(scope = "bng", name = "attachment-remove",
+        description = "Remove an attachment from the BNG")
+public class RemoveAttachment extends AbstractShellCommand {
+
+    @Argument(index = 0, name = "attachmentKey", description = "Attachment Key. No Key or 0 means ALL")
+    @Completion(AttachmentKeyCompleter.class)
+    String attachmentKey = null;
+
+    @Override
+    protected void doExecute() throws Exception {
+        BngService bngService = AbstractShellCommand.get(BngService.class);
+        if (attachmentKey == null) {
+            bngService.getAttachments().keySet()
+                    .forEach(bngService::removeAttachment);
+            return;
+        }
+        bngService.removeAttachment(attachmentKey);
+        print("Attachment removed");
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/cli/package-info.java b/app/src/main/java/org/opencord/bng/cli/package-info.java
new file mode 100644
index 0000000..68bbf80
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/cli/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019-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.
+ */
+
+/**
+ * BNG CLI package.
+ */
+package org.opencord.bng.cli;
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/bng/config/BngConfig.java b/app/src/main/java/org/opencord/bng/config/BngConfig.java
new file mode 100644
index 0000000..472343a
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/config/BngConfig.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019-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.opencord.bng.config;
+
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.config.Config;
+
+/**
+ * Configuration of the BNG app.
+ */
+public class BngConfig extends Config<ApplicationId> {
+    public static final String KEY = "bng";
+
+    private static final String BNGU_DEVICE_ID = "bnguDeviceId";
+
+    @Override
+    public boolean isValid() {
+        return hasOnlyFields(BNGU_DEVICE_ID) && hasField(BNGU_DEVICE_ID);
+    }
+
+    /**
+     * Gets the BNG user plane device ID.
+     *
+     * @return BNG user plane device ID
+     */
+    public DeviceId getBnguDeviceId() {
+        return DeviceId.deviceId(object.path(BNGU_DEVICE_ID).asText());
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/bng/config/PppoeRelayConfig.java b/app/src/main/java/org/opencord/bng/config/PppoeRelayConfig.java
new file mode 100644
index 0000000..c094f7f
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/config/PppoeRelayConfig.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2019-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.opencord.bng.config;
+
+import org.onlab.packet.MacAddress;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.config.Config;
+
+/**
+ * Configuration for the PPPoE control packet relay service.
+ */
+public class PppoeRelayConfig extends Config<ApplicationId> {
+    public static final String KEY = "pppoerelay";
+
+    private static final String PPPOE_SERVER_CONNECT_POINT = "pppoeServerConnectPoint";
+    private static final String OLT_CONNECT_POINT = "oltConnectPoint";
+    private static final String PPPOE_MAC_ADDRESS = "pppoeMacAddress";
+
+    @Override
+    public boolean isValid() {
+        return hasOnlyFields(PPPOE_SERVER_CONNECT_POINT, OLT_CONNECT_POINT, PPPOE_MAC_ADDRESS) &&
+                hasFields(PPPOE_SERVER_CONNECT_POINT, OLT_CONNECT_POINT, PPPOE_MAC_ADDRESS);
+    }
+
+    /**
+     * Gets the PPPoE server connect point.
+     *
+     * @return PPPoE server connect point
+     */
+    public ConnectPoint getPppoeServerConnectPoint() {
+        return ConnectPoint.deviceConnectPoint(object.path(PPPOE_SERVER_CONNECT_POINT).asText());
+    }
+
+    /**
+     * Gets the connect point where the OLT is connected to the ASG.
+     *
+     * @return ASG to OLT connect point
+     */
+    public ConnectPoint getAsgToOltConnectPoint() {
+        return ConnectPoint.deviceConnectPoint(object.path(OLT_CONNECT_POINT).asText());
+    }
+
+    /**
+     * Gets the PPPoE server MAC address.
+     *
+     * @return PPPoE server MAC address
+     */
+    public MacAddress getPppoeMacAddress() {
+        return MacAddress.valueOf(object.path(PPPOE_MAC_ADDRESS).asText());
+    }
+}
+
diff --git a/app/src/main/java/org/opencord/bng/config/netcfg.json b/app/src/main/java/org/opencord/bng/config/netcfg.json
new file mode 100644
index 0000000..c33fbf1
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/config/netcfg.json
@@ -0,0 +1,14 @@
+{
+  "apps": {
+    "org.opencord.bng": {
+      "pppoerelay": {
+        "oltConnectPoint": "device:asg1/128",
+        "pppoeServerConnectPoint": "device:asg1/140",
+        "pppoeMacAddress": "3c:fd:fe:9e:6e:40"
+      },
+      "bng": {
+        "bnguDeviceId": "device:asg1"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/bng/config/package-info.java b/app/src/main/java/org/opencord/bng/config/package-info.java
new file mode 100644
index 0000000..b9f42c7
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/config/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019-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.
+ */
+
+/**
+ * Configuration utility for BNG app components.
+ */
+package org.opencord.bng.config;
\ No newline at end of file
diff --git a/app/src/main/java/org/opencord/bng/package-info.java b/app/src/main/java/org/opencord/bng/package-info.java
new file mode 100644
index 0000000..170eb9c
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019-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.
+ */
+
+/**
+ * BNG application.
+ */
+package org.opencord.bng;
diff --git a/app/src/main/java/org/opencord/bng/packets/GenericPpp.java b/app/src/main/java/org/opencord/bng/packets/GenericPpp.java
new file mode 100644
index 0000000..464bf2d
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/packets/GenericPpp.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2019-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.opencord.bng.packets;
+
+import com.google.common.base.Objects;
+import org.onlab.packet.Data;
+import org.onlab.packet.Deserializer;
+
+import java.nio.ByteBuffer;
+
+import static org.onlab.packet.PacketUtils.checkInput;
+
+/**
+ * Implements the Link Control Protocol (LCP) header.
+ */
+
+public class GenericPpp extends Ppp {
+
+    // TODO: Add support for TLV options.
+
+    public static final byte CHAP_CODE_CHALLENGE = 0x01;
+    public static final byte CHAP_CODE_RESPONSE = 0x02;
+    public static final byte CHAP_CODE_SUCCESS = 0x03;
+    public static final byte CHAP_CODE_FAILURE = 0x04;
+
+    public static final byte PAP_AUTH_REQ = 0x01;
+    public static final byte PAP_AUTH_ACK = 0x02;
+    public static final byte PAP_AUTH_NACK = 0x03;
+
+    public static final byte CODE_TERM_REQ = 0x05;
+    public static final byte CODE_TERM_ACK = 0x06;
+
+
+    /**
+     * Deserializer function for LCP packets.
+     *
+     * @return deserializer function
+     */
+    public static Deserializer<GenericPpp> deserializer() {
+        return (data, offset, length) -> {
+            checkInput(data, offset, length, MIN_HEADER_LENGTH);
+            GenericPpp ppp = new GenericPpp();
+            final ByteBuffer bb = ByteBuffer.wrap(data, offset, length);
+            ppp.code = bb.get();
+            ppp.identifier = bb.get();
+            ppp.length = bb.getShort();
+            if (ppp.length > MIN_HEADER_LENGTH) {
+                ppp.payload = Data.deserializer()
+                        .deserialize(data, bb.position(),
+                                     Math.min(bb.limit() - bb.position(),
+                                              ppp.length - MIN_HEADER_LENGTH));
+                ppp.payload.setParent(ppp);
+            }
+            return ppp;
+        };
+    }
+
+    @Override
+    public byte[] serialize() {
+        byte[] payloadData = null;
+        int payloadLength = 0;
+        if (this.payload != null) {
+            this.payload.setParent(this);
+            payloadData = this.payload.serialize();
+            payloadLength = payloadData.length + MIN_HEADER_LENGTH;
+        }
+        int realLength = this.length > payloadLength ? this.length : payloadLength;
+        final byte[] data = new byte[realLength];
+        final ByteBuffer bb = ByteBuffer.wrap(data);
+        bb.put(this.code);
+        bb.put(this.identifier);
+        bb.putShort(this.length);
+        if (payloadData != null) {
+            bb.put(payloadData);
+        }
+        return data;
+    }
+
+    @Override
+    public int hashCode() {
+        return 31 * super.hashCode() + Objects.hashCode(code, identifier, length);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        final GenericPpp other = (GenericPpp) obj;
+        return Objects.equal(this.code, other.code)
+                && Objects.equal(this.identifier, other.identifier)
+                && Objects.equal(this.length, other.length);
+    }
+}
+
diff --git a/app/src/main/java/org/opencord/bng/packets/Ipcp.java b/app/src/main/java/org/opencord/bng/packets/Ipcp.java
new file mode 100644
index 0000000..d550005
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/packets/Ipcp.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2019-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.opencord.bng.packets;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
+import org.onlab.packet.DeserializationException;
+import org.onlab.packet.Deserializer;
+import org.onlab.packet.IpAddress;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static org.onlab.packet.PacketUtils.checkHeaderLength;
+import static org.onlab.packet.PacketUtils.checkInput;
+
+/**
+ * Implements the IP Control Protocol (IPCP) packet format.
+ */
+
+public class Ipcp extends Ppp {
+
+    public static final byte CONF_REQ = 0x01;
+    public static final byte ACK = 0x02;
+    public static final byte NAK = 0x03;
+
+    private PppTlv ipaddresstlv = null;
+
+    private List<PppTlv> pppTlvList;
+
+    Ipcp() {
+        super();
+        pppTlvList = Lists.newLinkedList();
+    }
+
+    /**
+     * Gets the IP Address TLV field.
+     *
+     * @return the IP Address TLV field if present, null otherwise
+     */
+    public PppTlv getIpAddressTlv() {
+        return this.ipaddresstlv;
+    }
+
+    /**
+     * Gets the IP address TLV field as an IpAddress object.
+     *
+     * @return The IP address TLV field as IpAddress object, it will be address
+     * 0 if no TLV address is present in the packet
+     */
+    public IpAddress getIpAddress() {
+        return IpAddress.valueOf(IpAddress.Version.INET,
+                                 this.getIpAddressTlv().getValue());
+    }
+
+    /**
+     * Sets the IP address TLV field.
+     *
+     * @param ipaddresstlv the IP address TLV to set
+     * @return this
+     */
+    public Ipcp setIpAddressTlv(PppTlv ipaddresstlv) {
+        this.ipaddresstlv = ipaddresstlv;
+        return this;
+    }
+
+    /**
+     * Gets the TLV field list.
+     *
+     * @return the IPCP TLV field list
+     */
+    public List<PppTlv> getIpcpTlvList() {
+        return this.pppTlvList;
+    }
+
+    @Override
+    public byte[] serialize() {
+        // TODO: Can it have any payload?
+        final byte[] data = new byte[this.length];
+        final ByteBuffer bb = ByteBuffer.wrap(data);
+        bb.put(this.code);
+        bb.put(this.identifier);
+        bb.putShort(this.length);
+        if (ipaddresstlv != null) {
+            bb.put(ipaddresstlv.serialize());
+        }
+        if (this.pppTlvList != null) {
+            for (final PppTlv tlv : this.pppTlvList) {
+                bb.put(tlv.serialize());
+            }
+        }
+        return data;
+    }
+
+    /**
+     * Deserializer function for IPCP packets.
+     *
+     * @return deserializer function
+     */
+    public static Deserializer<Ipcp> deserializer() {
+        return (data, offset, length) -> {
+            checkInput(data, offset, length, MIN_HEADER_LENGTH);
+            Ipcp ipcp = new Ipcp();
+            ByteBuffer bb = ByteBuffer.wrap(data, offset, length);
+            ipcp.code = bb.get();
+            ipcp.identifier = bb.get();
+            ipcp.length = bb.getShort();
+            short currentIndex = MIN_HEADER_LENGTH;
+            PppTlv tlv;
+            while (currentIndex < ipcp.length) {
+                // Each new TLV IPCP must be a minimum of 2 bytes
+                // (containing the type and length fields).
+                currentIndex += 2;
+                checkHeaderLength(length, currentIndex);
+
+                tlv = (new PppTlv()).deserialize(bb);
+                // if there was a failure to deserialize stop processing TLVs
+                if (tlv == null) {
+                    break;
+                }
+                if (tlv.getType() == PppTlv.IPCPTLV_IP_ADDRESS) {
+                    ipcp.ipaddresstlv = tlv;
+                } else {
+                    ipcp.pppTlvList.add(tlv);
+                }
+                currentIndex += tlv.getLength() - 2;
+            }
+            if (currentIndex != ipcp.length) {
+                throw new DeserializationException("Length of packet do not correspond to IPCP TLVs options");
+            }
+            return ipcp;
+        };
+    }
+
+    @Override
+    public int hashCode() {
+        return 31 * super.hashCode() + Objects.hashCode(ipaddresstlv);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        final Ipcp other = (Ipcp) obj;
+        return Objects.equal(this.ipaddresstlv, other.ipaddresstlv);
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("code", code)
+                .add("identifier", identifier)
+                .add("length", length)
+                .add("pppTlvList", pppTlvList)
+                .add("ipaddresstlv", ipaddresstlv)
+                .toString();
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/packets/Ppp.java b/app/src/main/java/org/opencord/bng/packets/Ppp.java
new file mode 100644
index 0000000..3785d51
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/packets/Ppp.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2019-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.opencord.bng.packets;
+
+import com.google.common.base.Objects;
+import org.onlab.packet.BasePacket;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Implements a generic PPP header.
+ */
+
+public abstract class Ppp extends BasePacket {
+
+    static final int MIN_HEADER_LENGTH = 4;
+
+    byte code;
+    byte identifier;
+    short length; // Length includes the code, identifier, length and data fields
+
+    /**
+     * Gets the LCP code.
+     *
+     * @return the LCP code
+     */
+    public byte getCode() {
+        return code;
+    }
+
+    /**
+     * Sets the LCP code.
+     *
+     * @param code the LCP code to set
+     */
+    public void setCode(byte code) {
+        this.code = code;
+    }
+
+    /**
+     * Gets the LCP identifier.
+     *
+     * @return the LCP identifier
+     */
+    public byte getIdentifier() {
+        return identifier;
+    }
+
+    /**
+     * Sets the LCP identifier.
+     *
+     * @param identifier the LCP identifier to set
+     */
+    public void setIdentifier(byte identifier) {
+        this.identifier = identifier;
+    }
+
+    /**
+     * Gets the length.
+     *
+     * @return the length
+     */
+    public short getLength() {
+        return length;
+    }
+
+    /**
+     * Sets the length.
+     *
+     * @param length the length to set
+     */
+    public void setLength(short length) {
+        this.length = length;
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(getClass())
+//                .add("PPPType", pppProtocol)
+                .add("code", code)
+                .add("identifier", identifier)
+                .add("length", length)
+                .toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return 31 * super.hashCode() + Objects.hashCode(code, identifier, length);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        final Ppp other = (Ppp) obj;
+        return Objects.equal(this.code, other.code)
+                && Objects.equal(this.identifier, other.identifier)
+                && Objects.equal(this.length, other.length);
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/packets/PppProtocolType.java b/app/src/main/java/org/opencord/bng/packets/PppProtocolType.java
new file mode 100644
index 0000000..b3f102d
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/packets/PppProtocolType.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2019-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.opencord.bng.packets;
+
+/**
+ * PPP Protocol type enumerator.
+ */
+public enum PppProtocolType {
+    LCP(0xc021, "lcp", true),
+    IPCP(0x8021, "ipcp", true),
+    PAP(0xc023, "pap", true),
+    CHAP(0xc223, "chap", true),
+    IPv4(0x0021, "ipv4", false),
+    IPv6(0x0057, "ipv6", false),
+    NO_PROTOCOL(0, "no_proto", true);
+
+    private final short code;
+    private final String type;
+    private final boolean control;
+
+    /**
+     * Constructs new PPP Protocol types.
+     *
+     * @param code         The PPP Protocol type.
+     * @param type         Textual representation of the PPP Protocol type.
+     * @param control      True if is control plane packet, false otherwise.
+     */
+    PppProtocolType(int code, String type, boolean control) {
+        this.code = (short) (code & 0xFFFF);
+        this.type = type;
+        this.control = control;
+    }
+
+    /**
+     * Lookups for a PPP Protocol type.
+     *
+     * @param code The code for PPP protocol.
+     * @return The PPPProtocol type
+     */
+    public static PppProtocolType lookup(short code) {
+        for (PppProtocolType type : PppProtocolType.values()) {
+            if (code == type.code()) {
+                return type;
+            }
+        }
+        return NO_PROTOCOL;
+    }
+
+    /**
+     * Returns code associated to the PPP protocol.
+     *
+     * @return The code for PPP protocol
+     */
+    public short code() {
+        return this.code;
+    }
+
+    /**
+     * Returns the string representation of the PPP protocol.
+     *
+     * @return The PPP protocol string representation
+     */
+    public String type() {
+        return this.type;
+    }
+
+    /**
+     * Checks if the PPP protocol is carrying control plane packets.
+     *
+     * @return True if the PPP protocol is for control plane packets, false
+     * otherwise
+     */
+    public boolean control() {
+        return this.control;
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/packets/PppTlv.java b/app/src/main/java/org/opencord/bng/packets/PppTlv.java
new file mode 100644
index 0000000..366ef7b
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/packets/PppTlv.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2019-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.opencord.bng.packets;
+
+import com.google.common.base.Objects;
+import org.onlab.packet.DeserializationException;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Implements IPCP Type-Length-Value options.
+ */
+public class PppTlv {
+    public static final byte IPCPTLV_IP_ADDRESS = 0x03;
+    private byte type;
+    private byte length; // Including the 2 byte of Minimum header
+    private byte[] value;
+
+    /**
+     * Get the TLV type.
+     *
+     * @return the TLV type
+     */
+    public byte getType() {
+        return type;
+    }
+
+    /**
+     * Set the TLV type.
+     *
+     * @param type the TLV type to set
+     * @return this
+     */
+    public PppTlv setType(byte type) {
+        this.type = type;
+        return this;
+    }
+
+    /**
+     * Get the TLV length. The length include the 2 bytes of minimum header
+     * length.
+     *
+     * @return the TLV length
+     */
+    public byte getLength() {
+        return length;
+    }
+
+    /**
+     * Set the TLV length. Length must include the 2 bytes of minimum header
+     * length.
+     *
+     * @param length the TLV length to set.
+     * @return this
+     */
+    public PppTlv setLength(byte length) {
+        this.length = length;
+        return this;
+    }
+
+    /**
+     * Get the TLV value field as byte array.
+     *
+     * @return the TLV value field
+     */
+    public byte[] getValue() {
+        return value;
+    }
+
+    /**
+     * Set the TLV valued field.
+     *
+     * @param value the TLV value to set
+     * @return this
+     */
+    public PppTlv setValue(byte[] value) {
+        this.value = value;
+        return this;
+    }
+
+    public byte[] serialize() {
+        final byte[] data = new byte[this.length];
+        final ByteBuffer bb = ByteBuffer.wrap(data);
+        bb.put(type);
+        bb.put(length);
+        if (this.value != null) {
+            bb.put(this.value);
+        }
+        return data;
+    }
+
+    public PppTlv deserialize(final ByteBuffer bb) throws DeserializationException {
+        if (bb.remaining() < 2) {
+            throw new DeserializationException(
+                    "Not enough bytes to deserialize PPP TLV options");
+        }
+        this.type = bb.get();
+        this.length = bb.get();
+        if (this.length > 2) {
+            this.value = new byte[this.length - 2];
+
+            // if there is an underrun just toss the TLV
+            // Length include the length of the TLV header itself
+            if (bb.remaining() < this.length - 2) {
+                throw new DeserializationException(
+                        "Remaining bytes are less then the length of the PPP TLV tag");
+            }
+            bb.get(this.value);
+            return this;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("[");
+        sb.append("type= ");
+        sb.append(this.type);
+        sb.append("length= ");
+        sb.append(this.length);
+        sb.append("value= ");
+        sb.append(Arrays.toString(this.value));
+        sb.append("]");
+        return sb.toString();
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof PppTlv)) {
+            return false;
+        }
+        final PppTlv other = (PppTlv) obj;
+        if (this.length != other.length) {
+            return false;
+        }
+        if (this.type != other.type) {
+            return false;
+        }
+        if (!Arrays.equals(this.value, other.value)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(type, length, Arrays.hashCode(value));
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/packets/Pppoe.java b/app/src/main/java/org/opencord/bng/packets/Pppoe.java
new file mode 100644
index 0000000..01945c8
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/packets/Pppoe.java
@@ -0,0 +1,429 @@
+/*
+ * Copyright 2019-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.opencord.bng.packets;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
+import org.onlab.packet.BasePacket;
+import org.onlab.packet.Data;
+import org.onlab.packet.Deserializer;
+import org.onlab.packet.Ethernet;
+import org.onlab.packet.IPacket;
+import org.onlab.packet.IPv4;
+import org.onlab.packet.IPv6;
+
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.List;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static org.onlab.packet.PacketUtils.checkHeaderLength;
+import static org.onlab.packet.PacketUtils.checkInput;
+
+/**
+ * Implements PPPoE packet format.
+ */
+public class Pppoe extends BasePacket {
+
+    public static final short TYPE_PPPOED = (short) 0x8863;
+    public static final short TYPE_PPPOES = (short) 0x8864;
+
+    static final ImmutableMap<Short, Deserializer<? extends IPacket>> PROTOCOL_DESERIALIZER_MAP =
+            ImmutableMap.<Short, Deserializer<? extends IPacket>>builder()
+                    //FIXME: write the correct parser for LCP, PAP, and CHAP
+                    .put(PppProtocolType.LCP.code(), GenericPpp.deserializer())
+                    .put(PppProtocolType.PAP.code(), GenericPpp.deserializer())
+                    .put(PppProtocolType.CHAP.code(), GenericPpp.deserializer())
+                    .put(PppProtocolType.IPCP.code(), Ipcp.deserializer())
+                    .put(PppProtocolType.IPv4.code(), IPv4.deserializer())
+                    .put(PppProtocolType.IPv6.code(), IPv6.deserializer())
+                    .build();
+    // Account of PPPoE standard header
+    static final int HEADER_LENGTH = 6;
+    // Fields part of PPPoE
+    private byte version; // 4bit
+    private byte typeId;  // 4bit
+    private PppoeType packetType; // 8bit => code in PPPoE header
+    private short sessionId; // 16bit
+    private short payloadLength; // 16bit
+    // FIXME: use PPPProtocol enum type
+    private short pppProtocol = 0;
+    //TODO: TLV TAGs (ref. to RFC2516)
+    private PppoeTlvTag acName = null;
+    private PppoeTlvTag serviceName = null;
+    private List<PppoeTlvTag> optionalTagTlvList;
+
+    public Pppoe() {
+        super();
+        optionalTagTlvList = new LinkedList<>();
+    }
+
+    /**
+     * Get the packet type.
+     *
+     * @return the packet type
+     */
+    public PppoeType getPacketType() {
+        return packetType;
+    }
+
+    /**
+     * Set the Packet Type.
+     *
+     * @param type The packet type to set
+     * @return this
+     */
+    public Pppoe setPacketType(PppoeType type) {
+        this.packetType = type;
+        return this;
+    }
+
+    /**
+     * Get the session ID.
+     *
+     * @return the session ID
+     */
+    public short getSessionId() {
+        return this.sessionId;
+    }
+
+    /**
+     * Set the session ID.
+     *
+     * @param sessionId the session ID to set
+     * @return this
+     */
+    public Pppoe setSessionId(short sessionId) {
+        this.sessionId = sessionId;
+        return this;
+    }
+
+    /**
+     * Get the Point-to-Point Protocol.
+     *
+     * @return the Point-to-Point Protocol
+     */
+    public short getPppProtocol() {
+        return this.pppProtocol;
+    }
+
+    /**
+     * Get the AC-Name.
+     *
+     * @return the AC-Name
+     */
+    public PppoeTlvTag getAcName() {
+        return this.acName;
+    }
+
+    /**
+     * Set the AC-Name.
+     *
+     * @param acname AC-Name to set
+     * @return this
+     */
+    public Pppoe setAcName(final PppoeTlvTag acname) {
+        this.acName = acname;
+        return this;
+    }
+
+    /**
+     * Get the PPPoE version field.
+     *
+     * @return The version field
+     */
+    public byte getVersion() {
+        return this.version;
+    }
+
+    /**
+     * Set the version field.
+     *
+     * @param version version to set
+     * @return this
+     */
+    public Pppoe setVersion(byte version) {
+        this.version = (byte) (version & 0xF);
+        return this;
+    }
+
+    /**
+     * Get the PPPoE type ID.
+     *
+     * @return The type ID
+     */
+    public short getTypeId() {
+        return this.typeId;
+    }
+
+    /**
+     * Set the type ID.
+     *
+     * @param typeId Type ID to set
+     * @return this
+     */
+    public Pppoe setTypeId(byte typeId) {
+        this.typeId = (byte) (typeId & 0xF);
+        return this;
+    }
+
+    /**
+     * Get the PPPoE payload length header field.
+     *
+     * @return The payload length
+     */
+    public short getPayloadLength() {
+        return this.payloadLength;
+    }
+
+    /**
+     * Set the payload length.
+     *
+     * @param payloadLength the payload length to set.
+     * @return this
+     */
+    public Pppoe setPayloadLength(short payloadLength) {
+        this.payloadLength = payloadLength;
+        return this;
+    }
+
+    @Override
+    public byte[] serialize() {
+        byte[] payloadData = null;
+        int payloadLength = 0;
+        if (this.payload != null) {
+            this.payload.setParent(this);
+            payloadData = this.payload.serialize();
+            payloadLength = payloadData.length + HEADER_LENGTH +
+                    (this.packetType == PppoeType.SESSION ? 2 : 0);
+        }
+        // PayloadLength account for PPP header field
+        int realLength = Math.max(this.payloadLength + HEADER_LENGTH, payloadLength);
+        final byte[] data = new byte[realLength];
+        final ByteBuffer bb = ByteBuffer.wrap(data);
+        bb.put((byte) (this.version << 4 | this.typeId & 0xf));
+        bb.put(this.packetType.code);
+        bb.putShort(this.sessionId);
+        bb.putShort(this.payloadLength);
+        if (this.packetType != PppoeType.SESSION) {
+            // Only session packet have PPP header
+            bb.putShort(this.pppProtocol);
+        } else {
+            // Only NON session packet have options
+            // TLV Tags
+            if (acName != null) {
+                bb.put(acName.serialize());
+            }
+            if (serviceName != null) {
+                bb.put(serviceName.serialize());
+            }
+            if (this.optionalTagTlvList != null) {
+                for (final PppoeTlvTag tlv : this.optionalTagTlvList) {
+                    bb.put(tlv.serialize());
+                }
+            }
+        }
+        if (payloadData != null) {
+            bb.put(payloadData);
+        }
+        return data;
+    }
+
+    /**
+     * Deserializer function for PPPoE packets.
+     *
+     * @return deserializer function
+     */
+    public static Deserializer<Pppoe> deserializer() {
+        return (data, offset, length) -> {
+            checkInput(data, offset, length, HEADER_LENGTH);
+
+            Pppoe pppoe = new Pppoe();
+            final ByteBuffer bb = ByteBuffer.wrap(data, offset, length);
+            byte versionAndType = bb.get();
+            pppoe.version = (byte) (versionAndType >> 4 & 0xF);
+            pppoe.typeId = (byte) (versionAndType & 0xF);
+            byte code = bb.get();
+            pppoe.sessionId = bb.getShort();
+            pppoe.payloadLength = bb.getShort();
+            pppoe.packetType = PppoeType.lookup(code);
+            // Check if the PPPoE packet is a SESSION packet
+            if (pppoe.packetType == PppoeType.SESSION) {
+                // Parse inner protocols
+                pppoe.pppProtocol = bb.getShort();
+                Deserializer<? extends IPacket> deserializer;
+                if (Pppoe.PROTOCOL_DESERIALIZER_MAP.containsKey(pppoe.pppProtocol)) {
+                    deserializer = PROTOCOL_DESERIALIZER_MAP.get(pppoe.pppProtocol);
+                } else {
+                    deserializer = Data.deserializer();
+                }
+                int remainingLength = bb.limit() - bb.position();
+                int bytesToRead = Math.min(pppoe.payloadLength - 2, remainingLength);
+                if (bytesToRead > 0) {
+                    pppoe.payload = deserializer.deserialize(data, bb.position(), bytesToRead);
+                    pppoe.payload.setParent(pppoe);
+                }
+            } else {
+                // PPPoE Packet is of Discovery type
+                // Parse TLV PPPoED Tags
+                int currentIndex = HEADER_LENGTH;
+                PppoeTlvTag tlv;
+                do {
+                    // Each new TLV PPPoE TAG must be a minimum of 4 bytes
+                    // (containing the type and length fields).
+                    if (length - currentIndex < 4) {
+                        // Probably the packet was zero-padded to reach the Ethernet minimum length.
+                        // Let's skip and accept the packet
+                        // FIXME: is there a "smarter" way to identify a padded packet?
+                        break;
+                    }
+                    currentIndex += 4;
+                    checkHeaderLength(length, currentIndex);
+                    tlv = new PppoeTlvTag().deserialize(bb);
+                    // if there was a failure to deserialize stop processing TLVs
+                    if (tlv == null) {
+                        break;
+                    }
+                    switch (tlv.getTagType()) {
+                        case PppoeTlvTag.PPPOED_TAGTYPE_EOL:
+                            // end delimiter
+                            break;
+                        case PppoeTlvTag.PPPOED_TAGTYPE_SERVICENAME:
+                            // Service Name
+                            pppoe.serviceName = tlv;
+                            break;
+                        case PppoeTlvTag.PPPOED_TAGTYPE_ACNAME:
+                            // AC-Name
+                            pppoe.acName = tlv;
+                            break;
+                        default:
+                            pppoe.optionalTagTlvList.add(tlv);
+                            break;
+                    }
+                    currentIndex += tlv.getLength();
+                } while (tlv.getTagType() != 0 && currentIndex < length);
+            }
+            return pppoe;
+        };
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(getClass())
+                .add("version", Byte.toString(version))
+                .add("typeId", Byte.toString(typeId))
+                .add("code", Byte.toString(packetType.code))
+                .add("sessionId", Short.toString(sessionId))
+                .add("payloadLength", Short.toString(payloadLength))
+                .toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return 31 * super.hashCode() +
+                Objects.hashCode(version, typeId, packetType,
+                                 sessionId, payloadLength, pppProtocol,
+                                 acName, serviceName, optionalTagTlvList);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        final Pppoe other = (Pppoe) obj;
+        return Objects.equal(this.version, other.version)
+                && Objects.equal(this.typeId, other.typeId)
+                && Objects.equal(this.packetType, other.packetType)
+                && Objects.equal(this.sessionId, other.sessionId)
+                && Objects.equal(this.payloadLength, other.payloadLength)
+                && Objects.equal(this.pppProtocol, other.pppProtocol)
+                && Objects.equal(this.acName, other.acName)
+                && Objects.equal(this.serviceName, other.serviceName)
+                && Objects.equal(this.optionalTagTlvList, other.optionalTagTlvList);
+    }
+
+    /**
+     * PPPoE Discovery types.
+     */
+    public enum PppoeType {
+        PADI(0x9, "padi"),
+        PADO(0x7, "pado"),
+        PADR(0x19, "padr"),
+        PADS(0x65, "pads"),
+        PADT(0xa7, "padt"),
+        SESSION(0x0, "session"),
+        UNKNOWN(0xFF, "unknown");
+
+        private final byte code;
+        private final String type;
+
+        /**
+         * Constructs new PPPoE Discovery types.
+         *
+         * @param code The PPPoED type.
+         * @param type Textual representation of the PPPoED type.
+         */
+        PppoeType(int code, String type) {
+            this.code = (byte) (code & 0xFF);
+            this.type = type;
+        }
+
+        public static PppoeType lookup(byte code) {
+            for (PppoeType type : PppoeType.values()) {
+                if (code == type.code()) {
+                    return type;
+                }
+            }
+            return UNKNOWN;
+        }
+
+        public byte code() {
+            return code;
+        }
+
+        public String type() {
+            return type;
+        }
+    }
+
+    /**
+     * Checks if the passed Ethernet packet is PPPoE Service.
+     *
+     * @param eth Packet to check
+     * @return True if the packet contains PPPoE Service header
+     */
+    public static boolean isPPPoES(Ethernet eth) {
+        return eth.getEtherType() == TYPE_PPPOES;
+    }
+
+    /**
+     * Checks if the passed Ethernet packet is PPPoE Discovery.
+     *
+     * @param eth Packet to check
+     * @return True if the packet contains PPPoE Discovery header
+     */
+    public static boolean isPPPoED(Ethernet eth) {
+        return eth.getEtherType() == TYPE_PPPOED;
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/packets/PppoeTlvTag.java b/app/src/main/java/org/opencord/bng/packets/PppoeTlvTag.java
new file mode 100644
index 0000000..d315e7f
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/packets/PppoeTlvTag.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2019-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.opencord.bng.packets;
+
+import com.google.common.base.Objects;
+import org.onlab.packet.DeserializationException;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Implements the PPPoE Type-Length-Value TAGs.
+ */
+public class PppoeTlvTag {
+
+    public static final short PPPOED_TAGTYPE_EOL = 0;
+    public static final short PPPOED_TAGTYPE_SERVICENAME = 0x0101;
+    public static final short PPPOED_TAGTYPE_ACNAME = 0x0102;
+
+    private short tagType;
+    private short length; // Length excluded the header (4 bytes minimum header)
+    private byte[] value;
+
+    /**
+     * Gets the TLV TAG type.
+     *
+     * @return the TLV TAG type
+     */
+    public short getTagType() {
+        return tagType;
+    }
+
+    /**
+     * Sets the TLV TAG type.
+     *
+     * @param tagType the type to set
+     * @return this
+     */
+    public PppoeTlvTag setTagType(short tagType) {
+        this.tagType = tagType;
+        return this;
+    }
+
+    /**
+     * Gets the length, number of bytes of value field.
+     *
+     * @return the length
+     */
+    public short getLength() {
+        return length;
+    }
+
+    /**
+     * Sets the length.
+     *
+     * @param length the length to set excluded the header length
+     * @return this
+     */
+    public PppoeTlvTag setLength(final short length) {
+        this.length = length;
+        return this;
+    }
+
+    /**
+     * The TLV value.
+     *
+     * @return the value
+     */
+    public byte[] getValue() {
+        return this.value;
+    }
+
+    /**
+     * Set the TLV value.
+     *
+     * @param value the value to set
+     * @return this
+     */
+    public PppoeTlvTag setValue(final byte[] value) {
+        this.value = value;
+        return this;
+    }
+
+    public byte[] serialize() {
+        final byte[] data = new byte[4 + this.length];
+        final ByteBuffer bb = ByteBuffer.wrap(data);
+        bb.putShort(tagType);
+        bb.putShort(length);
+        if (this.value != null) {
+            bb.put(this.value);
+        }
+        return data;
+    }
+
+    public PppoeTlvTag deserialize(final ByteBuffer bb) throws DeserializationException {
+        if (bb.remaining() < 4) {
+            throw new DeserializationException(
+                    "Not enough bytes to deserialize PPPoE TLV tag type and length");
+        }
+        this.tagType = bb.getShort();
+        this.length = bb.getShort();
+
+        if (this.length > 0) {
+            this.value = new byte[this.length];
+
+            // if there is an underrun just toss the TLV
+            if (bb.remaining() < this.length) {
+                throw new DeserializationException(
+                        "Remaining bytes are less then the length of the PPPoE TLV tag");
+            }
+            bb.get(this.value);
+        }
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("[");
+        sb.append("type= ");
+        sb.append(this.tagType);
+        sb.append("length= ");
+        sb.append(this.length);
+        sb.append("value= ");
+        sb.append(Arrays.toString(this.value));
+        sb.append("]");
+        return sb.toString();
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.lang.Object#equals(java.lang.Object)
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (!(obj instanceof PppoeTlvTag)) {
+            return false;
+        }
+        final PppoeTlvTag other = (PppoeTlvTag) obj;
+        if (this.length != other.length) {
+            return false;
+        }
+        if (this.tagType != other.tagType) {
+            return false;
+        }
+        if (!Arrays.equals(this.value, other.value)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(tagType, length, Arrays.hashCode(value));
+    }
+}
diff --git a/app/src/main/java/org/opencord/bng/packets/package-info.java b/app/src/main/java/org/opencord/bng/packets/package-info.java
new file mode 100644
index 0000000..accf4a2
--- /dev/null
+++ b/app/src/main/java/org/opencord/bng/packets/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2019-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.
+ */
+
+/**
+ * Packet parser for BNG application.
+ */
+package org.opencord.bng.packets;
\ No newline at end of file
diff --git a/app/src/test/java/org/opencord/bng/packets/GenericPppTest.java b/app/src/test/java/org/opencord/bng/packets/GenericPppTest.java
new file mode 100644
index 0000000..42d0193
--- /dev/null
+++ b/app/src/test/java/org/opencord/bng/packets/GenericPppTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2019-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.opencord.bng.packets;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.Deserializer;
+import org.onlab.packet.PacketTestUtils;
+
+import java.nio.ByteBuffer;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+public class GenericPppTest {
+    private Deserializer<GenericPpp> deserializer;
+
+    private byte code = 0x1;
+    private byte identifier = 0x1;
+    private short length = 0x08;
+    private byte[] payload = new byte[]{0x1, 0x4, 0x3, 0x4};
+    private byte[] padding = new byte[]{0x0, 0x0, 0x0};
+
+    private String packetToString = "";
+
+    private byte[] bytes;
+    private byte[] bytesPadded;
+
+    @Before
+    public void setUp() throws Exception {
+        deserializer = GenericPpp.deserializer();
+        ByteBuffer bb = ByteBuffer.allocate(Ppp.MIN_HEADER_LENGTH + 4);
+
+        bb.put(code);
+        bb.put(identifier);
+        bb.putShort(length);
+        bb.put(payload);
+
+        bytes = bb.array();
+
+        ByteBuffer bbPadded = ByteBuffer.allocate(Ppp.MIN_HEADER_LENGTH + 4 + 3);
+
+        bbPadded.put(code);
+        bbPadded.put(identifier);
+        bbPadded.putShort(length);
+        bbPadded.put(payload);
+        bbPadded.put(padding);
+
+        bytesPadded = bbPadded.array();
+    }
+
+    private short getTypeLength(byte type, short length) {
+        return (short) ((0x7f & type) << 9 | 0x1ff & length);
+    }
+
+    @Test
+    public void testDeserializeBadInput() throws Exception {
+        PacketTestUtils.testDeserializeBadInput(deserializer);
+    }
+
+    @Test
+    public void testDeserializeTruncated() throws Exception {
+//        PacketTestUtils.testDeserializeTruncated(deserializer, bytes);
+    }
+
+    /**
+     * Tests deserialize and getters.
+     */
+    @Test
+    public void testDeserialize() throws Exception {
+        GenericPpp ppp = deserializer.deserialize(bytes, 0, bytes.length);
+
+        assertEquals(code, ppp.getCode());
+        assertEquals(identifier, ppp.getIdentifier());
+        assertEquals(length, ppp.getLength());
+        assertArrayEquals(payload, ppp.getPayload().serialize());
+    }
+
+    /**
+     * Tests deserialize with padded packet.
+     */
+    @Test
+    public void testDeserializePadded() throws Exception {
+        GenericPpp ppp = deserializer.deserialize(bytesPadded, 0, bytesPadded.length);
+
+        assertEquals(code, ppp.getCode());
+        assertEquals(identifier, ppp.getIdentifier());
+        assertEquals(length, ppp.getLength());
+        assertArrayEquals(payload, ppp.getPayload().serialize());
+    }
+
+}
diff --git a/app/src/test/java/org/opencord/bng/packets/IpcpTest.java b/app/src/test/java/org/opencord/bng/packets/IpcpTest.java
new file mode 100644
index 0000000..eeaa341
--- /dev/null
+++ b/app/src/test/java/org/opencord/bng/packets/IpcpTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2019-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.opencord.bng.packets;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.Deserializer;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.PacketTestUtils;
+
+import java.nio.ByteBuffer;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+public class IpcpTest {
+
+    private Deserializer<Ipcp> deserializer;
+
+    private byte ipAddressTlvSize = 0x6;
+    private IpAddress ipAddress = IpAddress.valueOf("10.0.0.1");
+
+    private byte optionalTlvSize = 0x6;
+    private byte optionalTlvType = 0x01;
+    private byte[] optionalTlvValue = new byte[]{0x4, 0x3, 0x2, 0x1};
+
+    private byte code = Ipcp.CONF_REQ;
+    private byte identifier = 0x1;
+    private short length = (short) (Ipcp.MIN_HEADER_LENGTH + ipAddressTlvSize + optionalTlvSize);
+
+
+    private byte[] bytes;
+
+    @Before
+    public void setUp() throws Exception {
+        deserializer = Ipcp.deserializer();
+        ByteBuffer bb = ByteBuffer.allocate(Ipcp.MIN_HEADER_LENGTH + ipAddressTlvSize + optionalTlvSize);
+
+        bb.put(code);
+        bb.put(identifier);
+        bb.putShort(length);
+
+        bb.put(PppTlv.IPCPTLV_IP_ADDRESS);
+        bb.put(ipAddressTlvSize);
+        bb.put(ipAddress.toOctets());
+
+        bb.put(optionalTlvType);
+        bb.put(optionalTlvSize);
+        bb.put(optionalTlvValue);
+
+        bytes = bb.array();
+    }
+
+    @Test
+    public void testDeserializeBadInput() throws Exception {
+        PacketTestUtils.testDeserializeBadInput(deserializer);
+    }
+
+    @Test
+    public void testDeserializeTruncated() throws Exception {
+        PacketTestUtils.testDeserializeTruncated(deserializer, bytes);
+    }
+
+    /**
+     * Tests deserialize and getters.
+     */
+    @Test
+    public void testDeserialize() throws Exception {
+        Ipcp ipcp = deserializer.deserialize(bytes, 0, bytes.length);
+        PppTlv optionalTlv = ipcp.getIpcpTlvList().get(0);
+
+        assertEquals(code, ipcp.getCode());
+        assertEquals(identifier, ipcp.getIdentifier());
+        assertEquals(length, ipcp.getLength());
+
+        assertEquals(optionalTlvType, optionalTlv.getType());
+        assertEquals(optionalTlvSize, optionalTlv.getLength());
+        assertArrayEquals(optionalTlvValue, optionalTlv.getValue());
+
+        assertEquals(PppTlv.IPCPTLV_IP_ADDRESS, ipcp.getIpAddressTlv().getType());
+        assertEquals(ipAddressTlvSize, ipcp.getIpAddressTlv().getLength());
+        assertEquals(ipAddress, IpAddress.valueOf(IpAddress.Version.INET,
+                                                  ipcp.getIpAddressTlv().getValue()));
+        assertEquals(ipAddress, ipcp.getIpAddress());
+    }
+}
diff --git a/app/src/test/java/org/opencord/bng/packets/PppoeTest.java b/app/src/test/java/org/opencord/bng/packets/PppoeTest.java
new file mode 100644
index 0000000..aa1e24a
--- /dev/null
+++ b/app/src/test/java/org/opencord/bng/packets/PppoeTest.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2019-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.opencord.bng.packets;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.packet.Deserializer;
+import org.onlab.packet.PacketTestUtils;
+
+import java.nio.ByteBuffer;
+
+import static org.junit.Assert.assertEquals;
+
+public class PppoeTest {
+    private Deserializer<Pppoe> deserializer;
+
+    private byte versionAndType = 0x11;
+    private byte code = 0x9;
+    private byte codeSession = 0x0;
+    private short sessionId = 0x0102;
+    private short payloadLength = 0;
+
+    private byte[] padding = new byte[]{0x0, 0x0, 0x0};
+
+    private byte[] bytesDisc;
+    private byte[] bytesPadded;
+    private byte[] bytesSession;
+
+    @Before
+    public void setUp() throws Exception {
+        deserializer = Pppoe.deserializer();
+        ByteBuffer bb = ByteBuffer.allocate(Pppoe.HEADER_LENGTH);
+
+        bb.put(versionAndType);
+        bb.put(code);
+        bb.putShort(sessionId);
+        bb.putShort(payloadLength);
+
+        bytesDisc = bb.array();
+        ByteBuffer bbPadded = ByteBuffer.allocate(Pppoe.HEADER_LENGTH + 3);
+
+        bbPadded.put(versionAndType);
+        bbPadded.put(code);
+        bbPadded.putShort(sessionId);
+        bbPadded.putShort(payloadLength);
+        bbPadded.put(padding);
+
+        bytesPadded = bbPadded.array();
+
+        ByteBuffer bbSession = ByteBuffer.allocate(Pppoe.HEADER_LENGTH + 2);
+
+        bbSession.put(versionAndType);
+        bbSession.put(codeSession);
+        bbSession.putShort(sessionId);
+        bbSession.putShort((short) (payloadLength + 2));
+        bbSession.putShort(PppProtocolType.LCP.code());
+
+        bytesSession = bbSession.array();
+    }
+
+    @Test
+    public void testDeserializeBadInput() throws Exception {
+        PacketTestUtils.testDeserializeBadInput(deserializer);
+    }
+
+    @Test
+    public void testDeserializeTruncated() throws Exception {
+        PacketTestUtils.testDeserializeTruncated(deserializer, bytesDisc);
+    }
+
+    /**
+     * Tests deserialize and getters.
+     */
+    @Test
+    public void testDeserializeDiscovery() throws Exception {
+        Pppoe pppoe = deserializer.deserialize(bytesDisc, 0, bytesDisc.length);
+
+        assertEquals(versionAndType, (pppoe.getVersion() << 4) | pppoe.getTypeId());
+        assertEquals(code, pppoe.getPacketType().code());
+        assertEquals(sessionId, pppoe.getSessionId());
+        assertEquals(payloadLength, pppoe.getPayloadLength());
+    }
+
+    /**
+     * Tests deserialize with padded packet.
+     */
+    @Test
+    public void testDeserializePadded() throws Exception {
+        Pppoe pppoe = deserializer.deserialize(bytesPadded, 0, bytesPadded.length);
+
+        assertEquals(versionAndType, (pppoe.getVersion() << 4) | pppoe.getTypeId());
+        assertEquals(code, pppoe.getPacketType().code());
+        assertEquals(sessionId, pppoe.getSessionId());
+        assertEquals(payloadLength, pppoe.getPayloadLength());
+    }
+
+    /**
+     * Tests deserialize of PPPoE session packets.
+     */
+    @Test
+    public void testDeserializeSession() throws Exception {
+        Pppoe pppoe = deserializer.deserialize(bytesSession, 0, bytesSession.length);
+
+        assertEquals(versionAndType, (pppoe.getVersion() << 4) | pppoe.getTypeId());
+        assertEquals(codeSession, pppoe.getPacketType().code());
+        assertEquals(sessionId, pppoe.getSessionId());
+        assertEquals(payloadLength + 2, pppoe.getPayloadLength());
+        assertEquals(PppProtocolType.LCP.code(), pppoe.getPppProtocol());
+    }
+}
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..f5e604f
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2019-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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.onosproject</groupId>
+        <artifactId>onos-dependencies</artifactId>
+        <version>2.2.1-b3</version>
+    </parent>
+
+    <groupId>org.opencord</groupId>
+    <artifactId>bng</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>pom</packaging>
+
+    <description>BNG Control app</description>
+    <url>http://opencord.org</url>
+
+    <properties>
+<!--     TODO:   Update Sadis to 4.0.1-SNAPSHOT-->
+        <sadis.api.version>4.0.0-SNAPSHOT</sadis.api.version>
+        <olt.api.version>4.0.0-SNAPSHOT</olt.api.version>
+    </properties>
+
+    <modules>
+        <module>app</module>
+        <module>api</module>
+    </modules>
+
+    <repositories>
+        <repository>
+            <id>central</id>
+            <name>Central Repository</name>
+            <url>http://repo.maven.apache.org/maven2</url>
+            <layout>default</layout>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+            <releases>
+                <enabled>true</enabled>
+                <updatePolicy>always</updatePolicy>
+                <checksumPolicy>fail</checksumPolicy>
+            </releases>
+        </repository>
+
+        <repository>
+            <id>snapshots</id>
+            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+            <snapshots>
+                <enabled>true</enabled>
+                <updatePolicy>always</updatePolicy>
+                <checksumPolicy>fail</checksumPolicy>
+            </snapshots>
+        </repository>
+    </repositories>
+
+</project>