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/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