[SEBA-817][SEBA-821]
Adding c/s tags and hw address in the onu struct
DHCP State machine completed
Cleaned up logs

Change-Id: Iadb1d3967befe1c402e302a552b67faa2701f5c5
diff --git a/Makefile b/Makefile
index 04f7f15..b2595bd 100644
--- a/Makefile
+++ b/Makefile
@@ -20,6 +20,7 @@
 DOCKER_TAG  			?= ${VERSION}
 DOCKER_REPOSITORY  		?= voltha/
 DOCKER_REGISTRY 		?= ""
+DOCKER_RUN_ARGS			?= ""
 
 # Public targets
 
@@ -43,6 +44,9 @@
 docker-push: # @HELP Push a docker container to a registry
 	docker push ${DOCKER_REGISTRY}${DOCKER_REPOSITORY}bbsim:${DOCKER_TAG}
 
+docker-run: # @HELP Run the container locally (intended for development purposes: DOCKER_RUN_ARGS="-pon 2 -onu 2" make docker-run)
+	docker run -p 50070:50070 -p 50060:50060 --privileged --rm --name bbsim ${DOCKER_REGISTRY}${DOCKER_REPOSITORY}bbsim:${DOCKER_TAG} /app/bbsim ${DOCKER_RUN_ARGS}
+
 help: # @HELP Print the command options
 	@echo
 	@echo "\033[0;31m    BroadBand Simulator (BBSim) \033[0m"
diff --git a/README.md b/README.md
index 63e37cd..6a5dcbb 100644
--- a/README.md
+++ b/README.md
@@ -27,15 +27,16 @@
 When you install the `BBSim` helm chart you'll notice that the last line of the output
 prints the service name and port:
 
-```
+```bash
+...
 NOTES:
 BBSim deployed with release name: bbsim
 
 OLT ID: 0
 # of NNI Ports: 1
-# of PON Ports: 1
-# of ONU Ports: 1
-Total ONUs: (total: 1)
+# of PON Ports: 2
+# of ONU Ports: 2
+Total ONUs: (total: 4)
 
 OLT is listening on: "voltha.svc.bbsim-olt-id-0:50060"
 ```
@@ -44,7 +45,7 @@
 
 Connect to the voltha CLI and execute this commands:
 
-```
+```bash
 preprovision_olt -t openolt -H voltha.svc.bbsim-olt-id-0:50060
 enable
 ```
@@ -53,95 +54,94 @@
 
 _This assumes `voltctl` is installed an configured_
 
-```
-voltctl device create -t openolt -H $(kubectl get -n voltha service/bbsim -o go-template='{{.spec.clusterIP}}'):50060
+```bash
+voltctl device create -t openolt -H bbsim-olt-id-0:50060
 voltctl device enable $(voltctl device list --filter Type~openolt -q)
 ```
 
 ## Control API
 
 BBSim comes with a gRPC interface to control the internal state.
-We plan to provide a `bbsimctl` at certain point, meanwhile you can use `grpcurl`:
-
-```
-$ export BBSIM_IP="$(kubectl get svc -n voltha bbsim-olt-id-0 -o go-template='{{.spec.clusterIP}}')"
-$ grpcurl -plaintext $BBSIM_IP:50070 bbsim.BBSim/Version
-{
-  "version": "0.0.1-alpha",
-  "buildTime": "”2019.08.09.084157”",
-  "commitHash": "9ef7241b07a83c326ef152320428f204f7eff43d"
-}
-
-
-$ grpcurl -plaintext $BBSIM_IP:50070 bbsim.BBSim/GetOlt
-{
-  "ID": 22,
-  "OperState": "up",
-  "NNIPorts": [
-    {
-      "OperState": "down"
-    }
-  ],
-  "PONPorts": [
-    {
-      "OperState": "down"
-    }
-  ]
-}
-
-$ grpcurl -plaintext 127.0.0.1:50070 bbsim.BBSim/GetONUs
-{
-  "items": [
-    {
-      "ID": 1,
-      "SerialNumber": "vendor_id:\"BBSM\" vendor_specific:\"\\000\\000\\000\\001\" ",
-      "OperState": "up",
-      "InternalState": "auth_started"
-    },
-    {
-      "ID": 2,
-      "SerialNumber": "vendor_id:\"BBSM\" vendor_specific:\"\\000\\000\\000\\002\" ",
-      "OperState": "up",
-      "InternalState": "auth_started"
-    },
-    {
-      "ID": 1,
-      "SerialNumber": "vendor_id:\"BBSM\" vendor_specific:\"\\000\\000\\001\\001\" ",
-      "OperState": "up",
-      "InternalState": "auth_started",
-      "PonPortID": 1
-    },
-    {
-      "ID": 2,
-      "SerialNumber": "vendor_id:\"BBSM\" vendor_specific:\"\\000\\000\\001\\002\" ",
-      "OperState": "up",
-      "InternalState": "auth_started",
-      "PonPortID": 1
-    }
-  ]
-}
-```
-
-## Development
-
-To use a patched version of the `omci-sim` library:
+This interface can be queried using `bbsimctl` (the tool can be build with `make build`):
 
 ```bash
-make dep
-cd vendor/github.com/opencord/
-rm -rf omci-sim/
-git clone https://gerrit.opencord.org/omci-sim
-cd omci-sim
+$ ./bbsimctl --help
+Usage:
+  bbsimctl [OPTIONS] <config | olt | onus>
+
+Global Options:
+  -c, --config=FILE           Location of client config file [$BBSIMCTL_CONFIG]
+  -s, --server=SERVER:PORT    IP/Host and port of XOS
+  -d, --debug                 Enable debug mode
+
+Help Options:
+  -h, --help                  Show this help message
+
+Available commands:
+  config  generate bbsimctl configuration
+  olt     OLT Commands
+  onus    List ONU Devices
 ```
 
-Once done, go to `gerrit.opencord.org` and locate the patch you want to get. Click on the download URL and copy the `Checkout` command.
+`bbsimctl` can be configured via a config file such as:
 
-It should look something like:
-
-```
-git fetch ssh://teone@gerrit.opencord.org:29418/omci-sim refs/changes/67/15067/1 && git checkout FETCH_HEAD
+``` bash
+$ cat ~/.bbsim/config
+apiVersion: v1
+server: 127.0.0.1:50070
+grpc:
+  timeout: 10s
 ```
 
-Then just execute that command in the `omci-sim` folder inside the vendored dependencies.
+Some example commands:
+
+```bash
+$ ./bbsimctl olt get
+ID    SERIALNUMBER    OPERSTATE    INTERNALSTATE
+0     BBSIM_OLT_0     up           enabled
+
+
+$ ./bbsimctl olt pons
+PON Ports for : BBSIM_OLT_0
+
+ID    OPERSTATE
+0     up
+1     up
+2     up
+3     up
+
+
+$ ./bbsimctl onus
+PONPORTID    ID    SERIALNUMBER    STAG    CTAG    OPERSTATE    INTERNALSTATE
+0            1     BBSM00000001    900     900     up           eap_response_identity_sent
+0            2     BBSM00000002    900     901     up           eap_start_sent
+0            3     BBSM00000003    900     902     up           auth_failed
+0            4     BBSM00000004    900     903     up           auth_failed
+1            1     BBSM00000101    900     904     up           eap_response_success_received
+1            2     BBSM00000102    900     905     up           eap_response_success_received
+1            3     BBSM00000103    900     906     up           eap_response_challenge_sent
+1            4     BBSM00000104    900     907     up           auth_failed
+2            1     BBSM00000201    900     908     up           auth_failed
+2            2     BBSM00000202    900     909     up           eap_start_sent
+2            3     BBSM00000203    900     910     up           eap_response_identity_sent
+2            4     BBSM00000204    900     911     up           eap_start_sent
+3            1     BBSM00000301    900     912     up           eap_response_identity_sent
+3            2     BBSM00000302    900     913     up           auth_failed
+3            3     BBSM00000303    900     914     up           auth_failed
+3            4     BBSM00000304    900     915     up           auth_failed
+```
+
+## Documentation
+
+More advanced documentation lives in the [here](./docs/README.md)
+
+## Know Issues
+
+In some runs, EAPOL fails with:
+```
+time="2019-09-20T21:24:31Z" level=error msg="Can't send EapStart Message: ONU {intfid:1, onuid:2} - Not DONE (GemportID is not set)" IntfId=1 OnuId=2 OnuSn=BBSM00000102 module=EAPOL
+time="2019-09-20T21:24:31Z" level=error msg="ONU failed to authenticate!" IntfId=1 OnuId=2 OnuSn=BBSM00000102 module=ONU
+```
+Investigate why this happens (we believe the source to be in the OMCI library)
 
 > This project structure is based on [golang-standards/project-layout](https://github.com/golang-standards/project-layout).
diff --git a/api/bbsim/bbsim.proto b/api/bbsim/bbsim.proto
index 1a0134b..81a25f1 100644
--- a/api/bbsim/bbsim.proto
+++ b/api/bbsim/bbsim.proto
@@ -40,6 +40,9 @@
     string OperState = 3;
     string InternalState = 4;
     int32 PonPortID = 5;
+    int32 STag = 6;
+    int32 CTag = 7;
+    string HwAddress = 8;
 }
 
 message ONUs {
diff --git a/build/package/Dockerfile b/build/package/Dockerfile
index 93a48d2..2b1484e 100644
--- a/build/package/Dockerfile
+++ b/build/package/Dockerfile
@@ -63,7 +63,7 @@
 # debian symlinks it to 0.8 for historical reasons:
 # https://packages.debian.org/stretch/libpcap0.8-dev
 RUN apt-get update \
- && apt-get install -y libpcap-dev isc-dhcp-server network-manager\
+ && apt-get install -y libpcap-dev isc-dhcp-server network-manager tcpdump\
  && ln -s /usr/lib/libpcap.so.1.8.1 /usr/lib/libpcap.so.0.8
 
 COPY ./configs/isc-dhcp-server /etc/default/
@@ -75,4 +75,4 @@
 WORKDIR /app
 COPY --from=builder /go/src/github.com/opencord/bbsim/cmd/bbsim /app/bbsim
 RUN chmod a+x /app/bbsim
-ENTRYPOINT [ '/app/bbsim' ]
+CMD [ '/app/bbsim' ]
\ No newline at end of file
diff --git a/docs/README.md b/docs/README.md
index fe4d5d3..710182d 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1 +1,4 @@
-Coming soon
\ No newline at end of file
+# BBSim Documentation
+
+- [ONU State Machine](./onu-state-machine.md)
+- [Using development dependencies](./development-dependencies.md)
\ No newline at end of file
diff --git a/docs/development-dependencies.md b/docs/development-dependencies.md
new file mode 100644
index 0000000..fa1a474
--- /dev/null
+++ b/docs/development-dependencies.md
@@ -0,0 +1,21 @@
+# Development dependencies
+
+To use a patched version of the `omci-sim` library:
+
+```bash
+make dep
+cd vendor/github.com/opencord/
+rm -rf omci-sim/
+git clone https://gerrit.opencord.org/omci-sim
+cd omci-sim
+```
+
+Once done, go to `gerrit.opencord.org` and locate the patch you want to get. Click on the download URL and copy the `Checkout` command.
+
+It should look something like:
+
+```
+git fetch ssh://teone@gerrit.opencord.org:29418/omci-sim refs/changes/67/15067/1 && git checkout FETCH_HEAD
+```
+
+Then just execute that command in the `omci-sim` folder inside the vendored dependencies.
\ No newline at end of file
diff --git a/docs/onu-state-machine.md b/docs/onu-state-machine.md
new file mode 100644
index 0000000..af33db1
--- /dev/null
+++ b/docs/onu-state-machine.md
@@ -0,0 +1,27 @@
+# ONU State Machine
+
+In `BBSim` the device state is createdtained using a state machine library: [fsm](https://github.com/looplab/fsm).
+
+Here is a list of possible state transitions in BBSim:
+
+
+
+|Transition|Starting States|End State| Notes |
+| --- | --- | --- | --- |
+| - | - | created |
+| discover | created | discovered |
+| enable | discovered | enabled |
+| receive_eapol_flow | enabled, gem_port_added | eapol_flow_received |
+| add_gem_port | enabled, eapol_flow_received | gem_port_added | We need to wait for both the flow and the gem port to come before moving to `auth_started` |
+| start_auth | eapol_flow_received, gem_port_added | auth_started |
+| eap_start_sent | auth_started | eap_start_sent |
+| eap_response_identity_sent | eap_start_sent | eap_response_identity_sent |
+| eap_response_challenge_sent | eap_response_identity_sent | eap_response_challenge_sent |
+| eap_response_success_received | eap_response_challenge_sent | eap_response_success_received |
+| auth_failed | auth_started, eap_start_sent, eap_response_identity_sent, eap_response_challenge_sent | auth_failed |
+| start_dhcp | eap_response_success_received | dhcp_started |
+| dhcp_discovery_sent | dhcp_started | dhcp_discovery_sent |
+| dhcp_request_sent | dhcp_discovery_sent | dhcp_request_sent |
+| dhcp_ack_received | dhcp_request_sent | dhcp_ack_received |
+| dhcp_failed | dhcp_started, dhcp_discovery_sent, dhcp_request_sent | dhcp_failed |
+ 
\ No newline at end of file
diff --git a/examples/onos-dhcp.json b/examples/onos-dhcp.json
new file mode 100644
index 0000000..4e71440
--- /dev/null
+++ b/examples/onos-dhcp.json
@@ -0,0 +1,7 @@
+{
+  "org.opencord.dhcpl2relay": {
+    "dhcpl2relay": {
+      "useOltUplinkForServerPktInOut": true
+    }
+  }
+}
\ No newline at end of file
diff --git a/examples/sadis-minimal.json b/examples/sadis-minimal.json
index a37c3fe..259d3c4 100644
--- a/examples/sadis-minimal.json
+++ b/examples/sadis-minimal.json
@@ -11,8 +11,8 @@
       "entries": [
         {
           "id": "BBSM00000001",
-          "cTag": 101,
-          "sTag": 99,
+          "cTag": 900,
+          "sTag": 900,
           "nasPortId": "BBSM00000001",
           "technologyProfileId": 64,
           "upstreamBandwidthProfile": "High-Speed-Internet",
@@ -20,8 +20,8 @@
         },
         {
           "id": "BBSM00000002",
-          "cTag": 102,
-          "sTag": 99,
+          "cTag": 901,
+          "sTag": 900,
           "nasPortId": "BBSM00000002",
           "technologyProfileId": 64,
           "upstreamBandwidthProfile": "High-Speed-Internet",
@@ -29,8 +29,8 @@
         },
         {
           "id": "BBSM00000101",
-          "cTag": 111,
-          "sTag": 99,
+          "cTag": 902,
+          "sTag": 900,
           "nasPortId": "BBSM00000101",
           "technologyProfileId": 64,
           "upstreamBandwidthProfile": "High-Speed-Internet",
@@ -38,8 +38,8 @@
         },
         {
           "id": "BBSM00000102",
-          "cTag": 112,
-          "sTag": 99,
+          "cTag": 903,
+          "sTag": 900,
           "nasPortId": "BBSM00000102",
           "technologyProfileId": 64,
           "upstreamBandwidthProfile": "High-Speed-Internet",
diff --git a/internal/bbsim/bbsim.go b/internal/bbsim/bbsim.go
index 943c021..7c47e91 100644
--- a/internal/bbsim/bbsim.go
+++ b/internal/bbsim/bbsim.go
@@ -31,11 +31,13 @@
 
 func getOpts() *CliOptions {
 
-	olt_id 		:= flag.Int("olt_id", 0, "Number of OLT devices to be emulated (default is 1)")
-	nni 		:= flag.Int("nni", 1, "Number of NNI ports per OLT device to be emulated (default is 1)")
-	pon 		:= flag.Int("pon", 1, "Number of PON ports per OLT device to be emulated (default is 1)")
-	onu 		:= flag.Int("onu", 1, "Number of ONU devices per PON port to be emulated (default is 1)")
-	profileCpu 	:= flag.String("cpuprofile", "", "write cpu profile to file")
+	olt_id := flag.Int("olt_id", 0, "Number of OLT devices to be emulated (default is 1)")
+	nni := flag.Int("nni", 1, "Number of NNI ports per OLT device to be emulated (default is 1)")
+	pon := flag.Int("pon", 1, "Number of PON ports per OLT device to be emulated (default is 1)")
+	onu := flag.Int("onu", 1, "Number of ONU devices per PON port to be emulated (default is 1)")
+	s_tag := flag.Int("s_tag", 900, "S-Tag value (default is 900)")
+	c_tag_init := flag.Int("c_tag", 900, "C-Tag starting value (default is 900), each ONU will get a sequentail one (targeting 1024 ONUs per BBSim instance the range is bug enough)")
+	profileCpu := flag.String("cpuprofile", "", "write cpu profile to file")
 
 	flag.Parse()
 
@@ -45,14 +47,16 @@
 	o.NumNniPerOlt = int(*nni)
 	o.NumPonPerOlt = int(*pon)
 	o.NumOnuPerPon = int(*onu)
+	o.STag = int(*s_tag)
+	o.CTagInit = int(*c_tag_init)
 	o.profileCpu = profileCpu
 
 	return o
 }
 
-func startApiServer(channel chan bool, group *sync.WaitGroup)  {
+func startApiServer(channel chan bool, group *sync.WaitGroup) {
 	// TODO make configurable
-	address :=  "0.0.0.0:50070"
+	address := "0.0.0.0:50070"
 	log.Debugf("APIServer Listening on: %v", address)
 	lis, err := net.Listen("tcp", address)
 	if err != nil {
@@ -69,7 +73,7 @@
 	go grpcServer.Serve(lis)
 
 	for {
-		_, ok := <- channel
+		_, ok := <-channel
 		if !ok {
 			// if the olt channel is closed, stop the gRPC server
 			log.Warnf("Stopping API gRPC server")
@@ -105,7 +109,7 @@
 	}
 
 	log.WithFields(log.Fields{
-		"OltID": options.OltID,
+		"OltID":        options.OltID,
 		"NumNniPerOlt": options.NumNniPerOlt,
 		"NumPonPerOlt": options.NumPonPerOlt,
 		"NumOnuPerPon": options.NumOnuPerPon,
@@ -118,8 +122,7 @@
 	wg := sync.WaitGroup{}
 	wg.Add(2)
 
-
-	go devices.CreateOLT(options.OltID, options.NumNniPerOlt, options.NumPonPerOlt, options.NumOnuPerPon, &oltDoneChannel, &apiDoneChannel, &wg)
+	go devices.CreateOLT(options.OltID, options.NumNniPerOlt, options.NumPonPerOlt, options.NumOnuPerPon, options.STag, options.CTagInit, &oltDoneChannel, &apiDoneChannel, &wg)
 	log.Debugf("Created OLT with id: %d", options.OltID)
 	go startApiServer(apiDoneChannel, &wg)
 	log.Debugf("Started APIService")
@@ -133,4 +136,4 @@
 			pprof.StopCPUProfile()
 		}
 	}()
-}
\ No newline at end of file
+}
diff --git a/internal/bbsim/devices/helpers.go b/internal/bbsim/devices/helpers.go
index 920b1d4..5a56825 100644
--- a/internal/bbsim/devices/helpers.go
+++ b/internal/bbsim/devices/helpers.go
@@ -16,7 +16,11 @@
 
 package devices
 
-import "github.com/looplab/fsm"
+import (
+	"github.com/looplab/fsm"
+	"github.com/opencord/voltha-protos/go/openolt"
+	"strconv"
+)
 
 var newFSM = fsm.NewFSM
 
@@ -34,3 +38,11 @@
 		},
 	)
 }
+
+func onuSnToString(sn *openolt.SerialNumber) string {
+	s := string(sn.VendorId)
+	for _, i := range sn.VendorSpecific {
+		s = s + strconv.FormatInt(int64(i/16), 16) + strconv.FormatInt(int64(i%16), 16)
+	}
+	return s
+}
diff --git a/internal/bbsim/devices/nni.go b/internal/bbsim/devices/nni.go
new file mode 100644
index 0000000..814c825
--- /dev/null
+++ b/internal/bbsim/devices/nni.go
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2018-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 devices
+
+import (
+	"bytes"
+	"github.com/google/gopacket"
+	"github.com/google/gopacket/pcap"
+	"github.com/looplab/fsm"
+	"github.com/opencord/bbsim/internal/bbsim/packetHandlers"
+	"github.com/opencord/bbsim/internal/bbsim/types"
+	log "github.com/sirupsen/logrus"
+	"os/exec"
+)
+
+var (
+	nniLogger    = log.WithFields(log.Fields{"module": "NNI"})
+	nniVeth      = "nni"
+	upstreamVeth = "upstream"
+	dhcpServerIp = "182.21.0.128"
+)
+
+func CreateNNI(olt *OltDevice) (NniPort, error) {
+	nniPort := NniPort{
+		ID: uint32(0),
+		OperState: getOperStateFSM(func(e *fsm.Event) {
+			oltLogger.Debugf("Changing NNI OperState from %s to %s", e.Src, e.Dst)
+		}),
+		Type: "nni",
+	}
+	createNNIPair(olt)
+	return nniPort, nil
+}
+
+// sendNniPacket will send a packet out of the NNI interface.
+// We will send upstream only DHCP packets and drop anything else
+func sendNniPacket(packet gopacket.Packet) error {
+	if isDhcp := packetHandlers.IsDhcpPacket(packet); !isDhcp {
+		nniLogger.WithFields(log.Fields{
+			"packet": packet,
+		}).Trace("Dropping NNI packet as it's not DHCP")
+	}
+
+	packet, err := packetHandlers.PopDoubleTag(packet)
+	if err != nil {
+		nniLogger.WithFields(log.Fields{
+			"packet": packet,
+		}).Errorf("Can't remove double tags from packet: %v", err)
+		return err
+	}
+
+	handle, err := getVethHandler(nniVeth)
+	if err != nil {
+		return err
+	}
+
+	err = handle.WritePacketData(packet.Data())
+	if err != nil {
+		nniLogger.WithFields(log.Fields{
+			"packet": packet,
+		}).Errorf("Failed to send packet out of the NNI: %s", err)
+		return err
+	}
+
+	nniLogger.Infof("Sent packet out of NNI")
+	return nil
+}
+
+// createNNIBridge will create a veth bridge to fake the connection between the NNI port
+// and something upstream, in this case a DHCP server.
+// It is also responsible to start the DHCP server itself
+func createNNIPair(olt *OltDevice) error {
+
+	if err := exec.Command("ip", "link", "add", nniVeth, "type", "veth", "peer", "name", upstreamVeth).Run(); err != nil {
+		nniLogger.Errorf("Couldn't create veth pair between %s and %s", nniVeth, upstreamVeth)
+		return err
+	}
+
+	if err := setVethUp(nniVeth); err != nil {
+		return err
+	}
+
+	if err := setVethUp(upstreamVeth); err != nil {
+		return err
+	}
+
+	if err := startDHCPServer(); err != nil {
+		return err
+	}
+
+	ch, err := listenOnVeth(nniVeth)
+	if err != nil {
+		return err
+	}
+	olt.nniPktInChannel = ch
+	return nil
+}
+
+// setVethUp is responsible to activate a virtual interface
+func setVethUp(vethName string) error {
+	if err := exec.Command("ip", "link", "set", vethName, "up").Run(); err != nil {
+		nniLogger.Errorf("Couldn't change interface %s state to up: %v", vethName, err)
+		return err
+	}
+	return nil
+}
+
+func startDHCPServer() error {
+	if err := exec.Command("ip", "addr", "add", dhcpServerIp, "dev", upstreamVeth).Run(); err != nil {
+		nniLogger.Errorf("Couldn't assing ip %s to interface %s: %v", dhcpServerIp, upstreamVeth, err)
+		return err
+	}
+
+	if err := setVethUp(upstreamVeth); err != nil {
+		return err
+	}
+
+	dhcp := "/usr/local/bin/dhcpd"
+	conf := "/etc/dhcp/dhcpd.conf" // copied in the container from configs/dhcpd.conf
+	logfile := "/tmp/dhcplog"
+	var stderr bytes.Buffer
+	cmd := exec.Command(dhcp, "-cf", conf, upstreamVeth, "-tf", logfile)
+	cmd.Stderr = &stderr
+	err := cmd.Run()
+	if err != nil {
+		nniLogger.Errorf("Fail to start DHCP Server: %s, %s", err, stderr.String())
+		return err
+	}
+	nniLogger.Info("Successfully activated DHCP Server")
+	return nil
+}
+
+func getVethHandler(vethName string) (*pcap.Handle, error) {
+	var (
+		device            = vethName
+		snapshotLen int32 = 1518
+		promiscuous       = false
+		timeout           = pcap.BlockForever
+	)
+	handle, err := pcap.OpenLive(device, snapshotLen, promiscuous, timeout)
+	if err != nil {
+		nniLogger.Errorf("Can't retrieve handler for interface %s", vethName)
+		return nil, err
+	}
+	return handle, nil
+}
+
+func listenOnVeth(vethName string) (chan *types.PacketMsg, error) {
+
+	handle, err := getVethHandler(vethName)
+	if err != nil {
+		return nil, err
+	}
+
+	channel := make(chan *types.PacketMsg, 32)
+
+	go func() {
+		packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
+		for packet := range packetSource.Packets() {
+
+			if !packetHandlers.IsIncomingPacket(packet) {
+				nniLogger.Tracef("Ignoring packet as it's going out")
+				continue
+			}
+
+			nniLogger.WithFields(log.Fields{
+				"packet": packet.Dump(),
+			}).Tracef("Received packet on NNI Port")
+			pkt := types.PacketMsg{
+				Pkt: packet,
+			}
+			channel <- &pkt
+		}
+	}()
+
+	return channel, nil
+}
diff --git a/internal/bbsim/devices/olt.go b/internal/bbsim/devices/olt.go
index 6ce09a1..c75f2d1 100644
--- a/internal/bbsim/devices/olt.go
+++ b/internal/bbsim/devices/olt.go
@@ -20,10 +20,10 @@
 	"context"
 	"errors"
 	"fmt"
-	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
 	"github.com/google/gopacket"
 	"github.com/google/gopacket/layers"
 	"github.com/looplab/fsm"
+	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
 	"github.com/opencord/voltha-protos/go/openolt"
 	"github.com/opencord/voltha-protos/go/tech_profile"
 	log "github.com/sirupsen/logrus"
@@ -43,32 +43,33 @@
 
 var olt = OltDevice{}
 
-func GetOLT() OltDevice  {
+func GetOLT() OltDevice {
 	return olt
 }
 
-func CreateOLT(seq int, nni int, pon int, onuPerPon int, oltDoneChannel *chan bool, apiDoneChannel *chan bool, group *sync.WaitGroup) OltDevice {
+func CreateOLT(seq int, nni int, pon int, onuPerPon int, sTag int, cTagInit int, oltDoneChannel *chan bool, apiDoneChannel *chan bool, group *sync.WaitGroup) OltDevice {
 	oltLogger.WithFields(log.Fields{
-		"ID": seq,
-		"NumNni":nni,
-		"NumPon":pon,
-		"NumOnuPerPon":onuPerPon,
+		"ID":           seq,
+		"NumNni":       nni,
+		"NumPon":       pon,
+		"NumOnuPerPon": onuPerPon,
 	}).Debug("CreateOLT")
 
 	olt = OltDevice{
-		ID: seq,
+		ID:           seq,
 		SerialNumber: fmt.Sprintf("BBSIM_OLT_%d", seq),
 		OperState: getOperStateFSM(func(e *fsm.Event) {
 			oltLogger.Debugf("Changing OLT OperState from %s to %s", e.Src, e.Dst)
 		}),
-		NumNni:nni,
-		NumPon:pon,
-		NumOnuPerPon:onuPerPon,
-		Pons: []PonPort{},
-		Nnis: []NniPort{},
-		channel: make(chan Message),
-		oltDoneChannel: oltDoneChannel,
-		apiDoneChannel: apiDoneChannel,
+		NumNni:          nni,
+		NumPon:          pon,
+		NumOnuPerPon:    onuPerPon,
+		Pons:            []PonPort{},
+		Nnis:            []NniPort{},
+		channel:         make(chan Message),
+		oltDoneChannel:  oltDoneChannel,
+		apiDoneChannel:  apiDoneChannel,
+		nniPktInChannel: make(chan *bbsim.PacketMsg, 1024), // packets coming in from the NNI and going to VOLTHA
 	}
 
 	// OLT State machine
@@ -87,22 +88,21 @@
 	)
 
 	// create NNI Port
-	nniPort := NniPort{
-		ID: uint32(0),
-		OperState: getOperStateFSM(func(e *fsm.Event) {
-			oltLogger.Debugf("Changing NNI OperState from %s to %s", e.Src, e.Dst)
-		}),
-		Type: "nni",
+	nniPort, err := CreateNNI(&olt)
+
+	if err != nil {
+		oltLogger.Fatalf("Couldn't create NNI Port: %v", err)
 	}
+
 	olt.Nnis = append(olt.Nnis, nniPort)
 
 	// create PON ports
-	//onuId := 1
+	availableCTag := cTagInit
 	for i := 0; i < pon; i++ {
 		p := PonPort{
 			NumOnu: olt.NumOnuPerPon,
-			ID: uint32(i),
-			Type: "pon",
+			ID:     uint32(i),
+			Type:   "pon",
 		}
 		p.OperState = getOperStateFSM(func(e *fsm.Event) {
 			oltLogger.WithFields(log.Fields{
@@ -113,9 +113,9 @@
 		// create ONU devices
 		for j := 0; j < onuPerPon; j++ {
 			//o := CreateONU(olt, p, uint32(onuId))
-			o := CreateONU(olt, p, uint32(j + 1))
+			o := CreateONU(olt, p, uint32(j+1), sTag, availableCTag)
 			p.Onus = append(p.Onus, o)
-			//onuId = onuId + 1
+			availableCTag = availableCTag + 1
 		}
 
 		olt.Pons = append(olt.Pons, p)
@@ -129,7 +129,7 @@
 
 func newOltServer(o OltDevice) error {
 	// TODO make configurable
-	address :=  "0.0.0.0:50060"
+	address := "0.0.0.0:50060"
 	lis, err := net.Listen("tcp", address)
 	if err != nil {
 		oltLogger.Fatalf("OLT failed to listen: %v", err)
@@ -144,7 +144,7 @@
 	oltLogger.Debugf("OLT Listening on: %v", address)
 
 	for {
-		_, ok := <- *o.oltDoneChannel
+		_, ok := <-*o.oltDoneChannel
 		if !ok {
 			// if the olt channel is closed, stop the gRPC server
 			log.Warnf("Stopping OLT gRPC server")
@@ -161,15 +161,16 @@
 
 // Device Methods
 
-func (o OltDevice) Enable (stream openolt.Openolt_EnableIndicationServer) error {
+func (o OltDevice) Enable(stream openolt.Openolt_EnableIndicationServer) error {
 
 	oltLogger.Debug("Enable OLT called")
 
 	wg := sync.WaitGroup{}
-	wg.Add(1)
+	wg.Add(2)
 
 	// create a channel for all the OLT events
 	go o.processOltMessages(stream)
+	go o.processNniPacketIns(stream)
 
 	// enable the OLT
 	olt_msg := Message{
@@ -207,9 +208,9 @@
 			go onu.processOnuMessages(stream)
 			go onu.processOmciMessages(stream)
 			msg := Message{
-				Type:      OnuDiscIndication,
+				Type: OnuDiscIndication,
 				Data: OnuDiscIndicationMessage{
-					Onu:     onu,
+					Onu:       onu,
 					OperState: UP,
 				},
 			}
@@ -257,8 +258,8 @@
 	nni.OperState.Event("enable")
 	// NOTE Operstate may need to be an integer
 	operData := &openolt.Indication_IntfOperInd{IntfOperInd: &openolt.IntfOperIndication{
-		Type: nni.Type,
-		IntfId: nni.ID,
+		Type:      nni.Type,
+		IntfId:    nni.ID,
 		OperState: nni.OperState.Current(),
 	}}
 
@@ -267,8 +268,8 @@
 	}
 
 	oltLogger.WithFields(log.Fields{
-		"Type": nni.Type,
-		"IntfId": nni.ID,
+		"Type":      nni.Type,
+		"IntfId":    nni.ID,
 		"OperState": nni.OperState.Current(),
 	}).Debug("Sent Indication_IntfOperInd for NNI")
 }
@@ -277,7 +278,7 @@
 	pon, _ := o.getPonById(msg.PonPortID)
 	pon.OperState.Event("enable")
 	discoverData := &openolt.Indication_IntfInd{IntfInd: &openolt.IntfIndication{
-		IntfId: pon.ID,
+		IntfId:    pon.ID,
 		OperState: pon.OperState.Current(),
 	}}
 
@@ -286,13 +287,13 @@
 	}
 
 	oltLogger.WithFields(log.Fields{
-		"IntfId": pon.ID,
+		"IntfId":    pon.ID,
 		"OperState": pon.OperState.Current(),
 	}).Debug("Sent Indication_IntfInd")
 
 	operData := &openolt.Indication_IntfOperInd{IntfOperInd: &openolt.IntfOperIndication{
-		Type: pon.Type,
-		IntfId: pon.ID,
+		Type:      pon.Type,
+		IntfId:    pon.ID,
 		OperState: pon.OperState.Current(),
 	}}
 
@@ -301,8 +302,8 @@
 	}
 
 	oltLogger.WithFields(log.Fields{
-		"Type": pon.Type,
-		"IntfId": pon.ID,
+		"Type":      pon.Type,
+		"IntfId":    pon.ID,
 		"OperState": pon.OperState.Current(),
 	}).Debug("Sent Indication_IntfOperInd for PON")
 }
@@ -311,9 +312,8 @@
 	oltLogger.Debug("Started OLT Indication Channel")
 	for message := range o.channel {
 
-
 		oltLogger.WithFields(log.Fields{
-			"oltId": o.ID,
+			"oltId":       o.ID,
 			"messageType": message.Type,
 		}).Trace("Received message")
 
@@ -341,11 +341,32 @@
 	}
 }
 
+func (o OltDevice) processNniPacketIns(stream openolt.Openolt_EnableIndicationServer) {
+	oltLogger.WithFields(log.Fields{
+		"nniChannel": o.nniPktInChannel,
+	}).Debug("Started NNI Channel")
+	nniId := o.Nnis[0].ID // FIXME we are assuming we have only one NNI
+	for message := range o.nniPktInChannel {
+		oltLogger.Debug("Received packets on NNI Channel")
+		data := &openolt.Indication_PktInd{PktInd: &openolt.PacketIndication{
+			IntfType: "nni",
+			IntfId:   nniId,
+			Pkt:      message.Pkt.Data()}}
+		if err := stream.Send(&openolt.Indication{Data: data}); err != nil {
+			oltLogger.WithFields(log.Fields{
+				"IntfType": data.PktInd.IntfType,
+				"IntfId":   nniId,
+				"Pkt":      message.Pkt.Data(),
+			}).Errorf("Fail to send PktInd indication: %v", err)
+		}
+	}
+}
+
 // GRPC Endpoints
 
-func (o OltDevice) ActivateOnu(context context.Context, onu *openolt.Onu) (*openolt.Empty, error)  {
+func (o OltDevice) ActivateOnu(context context.Context, onu *openolt.Onu) (*openolt.Empty, error) {
 	oltLogger.WithFields(log.Fields{
-		"onuSerialNumber": onu.SerialNumber,
+		"OnuSn": onuSnToString(onu.SerialNumber),
 	}).Info("Received ActivateOnu call from VOLTHA")
 
 	pon, _ := o.getPonById(onu.IntfId)
@@ -353,28 +374,28 @@
 
 	// NOTE we need to immediately activate the ONU or the OMCI state machine won't start
 	msg := Message{
-		Type:      OnuIndication,
-		Data:      OnuIndicationMessage{
+		Type: OnuIndication,
+		Data: OnuIndicationMessage{
 			OnuSN:     onu.SerialNumber,
 			PonPortID: onu.IntfId,
 			OperState: UP,
 		},
 	}
 	_onu.channel <- msg
-	return new(openolt.Empty) , nil
+	return new(openolt.Empty), nil
 }
 
-func (o OltDevice) DeactivateOnu(context.Context, *openolt.Onu) (*openolt.Empty, error)  {
+func (o OltDevice) DeactivateOnu(context.Context, *openolt.Onu) (*openolt.Empty, error) {
 	oltLogger.Error("DeactivateOnu not implemented")
-	return new(openolt.Empty) , nil
+	return new(openolt.Empty), nil
 }
 
-func (o OltDevice) DeleteOnu(context.Context, *openolt.Onu) (*openolt.Empty, error)  {
+func (o OltDevice) DeleteOnu(context.Context, *openolt.Onu) (*openolt.Empty, error) {
 	oltLogger.Error("DeleteOnu not implemented")
-	return new(openolt.Empty) , nil
+	return new(openolt.Empty), nil
 }
 
-func (o OltDevice) DisableOlt(context.Context, *openolt.Empty) (*openolt.Empty, error)  {
+func (o OltDevice) DisableOlt(context.Context, *openolt.Empty) (*openolt.Empty, error) {
 	// NOTE when we disable the OLT should we disable NNI, PONs and ONUs altogether?
 	olt_msg := Message{
 		Type: OltIndication,
@@ -383,37 +404,37 @@
 		},
 	}
 	o.channel <- olt_msg
-	return new(openolt.Empty) , nil
+	return new(openolt.Empty), nil
 }
 
-func (o OltDevice) DisablePonIf(context.Context, *openolt.Interface) (*openolt.Empty, error)  {
+func (o OltDevice) DisablePonIf(context.Context, *openolt.Interface) (*openolt.Empty, error) {
 	oltLogger.Error("DisablePonIf not implemented")
-	return new(openolt.Empty) , nil
+	return new(openolt.Empty), nil
 }
 
-func (o OltDevice) EnableIndication(_ *openolt.Empty, stream openolt.Openolt_EnableIndicationServer) error  {
+func (o OltDevice) EnableIndication(_ *openolt.Empty, stream openolt.Openolt_EnableIndicationServer) error {
 	oltLogger.WithField("oltId", o.ID).Info("OLT receives EnableIndication call from VOLTHA")
 	o.Enable(stream)
 	return nil
 }
 
-func (o OltDevice) EnablePonIf(context.Context, *openolt.Interface) (*openolt.Empty, error)  {
+func (o OltDevice) EnablePonIf(context.Context, *openolt.Interface) (*openolt.Empty, error) {
 	oltLogger.Error("EnablePonIf not implemented")
-	return new(openolt.Empty) , nil
+	return new(openolt.Empty), nil
 }
 
-func (o OltDevice) FlowAdd(ctx context.Context, flow *openolt.Flow) (*openolt.Empty, error)  {
+func (o OltDevice) FlowAdd(ctx context.Context, flow *openolt.Flow) (*openolt.Empty, error) {
 	oltLogger.WithFields(log.Fields{
-		"IntfId": flow.AccessIntfId,
-		"OnuId": flow.OnuId,
-		"EthType": fmt.Sprintf("%x", flow.Classifier.EthType),
+		"IntfId":    flow.AccessIntfId,
+		"OnuId":     flow.OnuId,
+		"EthType":   fmt.Sprintf("%x", flow.Classifier.EthType),
 		"InnerVlan": flow.Classifier.IVid,
 		"OuterVlan": flow.Classifier.OVid,
-		"FlowType": flow.FlowType,
-		"FlowId": flow.FlowId,
-		"UniID": flow.UniId,
-		"PortNo": flow.PortNo,
-	}).Infof("OLT receives Flow")
+		"FlowType":  flow.FlowType,
+		"FlowId":    flow.FlowId,
+		"UniID":     flow.UniId,
+		"PortNo":    flow.PortNo,
+	}).Tracef("OLT receives Flow")
 	// TODO optionally store flows somewhere
 
 	if flow.AccessIntfId == -1 {
@@ -425,34 +446,34 @@
 		onu, _ := pon.getOnuById(uint32(flow.OnuId))
 
 		msg := Message{
-			Type:      FlowUpdate,
-			Data:      OnuFlowUpdateMessage{
-				PonPortID:  pon.ID,
-				OnuID: 	onu.ID,
-				Flow: 	flow,
+			Type: FlowUpdate,
+			Data: OnuFlowUpdateMessage{
+				PonPortID: pon.ID,
+				OnuID:     onu.ID,
+				Flow:      flow,
 			},
 		}
 		onu.channel <- msg
 	}
 
-	return new(openolt.Empty) , nil
+	return new(openolt.Empty), nil
 }
 
-func (o OltDevice) FlowRemove(context.Context, *openolt.Flow) (*openolt.Empty, error)  {
-	oltLogger.Info("received FlowRemove")
+func (o OltDevice) FlowRemove(context.Context, *openolt.Flow) (*openolt.Empty, error) {
+	oltLogger.Tracef("received FlowRemove")
 	// TODO store flows somewhere
-	return new(openolt.Empty) , nil
+	return new(openolt.Empty), nil
 }
 
-func (o OltDevice) HeartbeatCheck(context.Context, *openolt.Empty) (*openolt.Heartbeat, error)  {
+func (o OltDevice) HeartbeatCheck(context.Context, *openolt.Empty) (*openolt.Heartbeat, error) {
 	oltLogger.Error("HeartbeatCheck not implemented")
-	return new(openolt.Heartbeat) , nil
+	return new(openolt.Heartbeat), nil
 }
 
-func (o OltDevice) GetDeviceInfo(context.Context, *openolt.Empty) (*openolt.DeviceInfo, error)  {
+func (o OltDevice) GetDeviceInfo(context.Context, *openolt.Empty) (*openolt.DeviceInfo, error) {
 
 	oltLogger.WithFields(log.Fields{
-		"oltId": o.ID,
+		"oltId":    o.ID,
 		"PonPorts": o.NumPon,
 	}).Info("OLT receives GetDeviceInfo call from VOLTHA")
 	devinfo := new(openolt.DeviceInfo)
@@ -475,67 +496,76 @@
 	return devinfo, nil
 }
 
-func (o OltDevice) OmciMsgOut(ctx context.Context, omci_msg *openolt.OmciMsg) (*openolt.Empty, error)  {
+func (o OltDevice) OmciMsgOut(ctx context.Context, omci_msg *openolt.OmciMsg) (*openolt.Empty, error) {
 	pon, _ := o.getPonById(omci_msg.IntfId)
 	onu, _ := pon.getOnuById(omci_msg.OnuId)
 	msg := Message{
-		Type:      OMCI,
-		Data:      OmciMessage{
-			OnuSN:  onu.SerialNumber,
-			OnuID: 	onu.ID,
-			omciMsg: 	omci_msg,
+		Type: OMCI,
+		Data: OmciMessage{
+			OnuSN:   onu.SerialNumber,
+			OnuID:   onu.ID,
+			omciMsg: omci_msg,
 		},
 	}
 	onu.channel <- msg
-	return new(openolt.Empty) , nil
+	return new(openolt.Empty), nil
 }
 
-func (o OltDevice) OnuPacketOut(ctx context.Context, onuPkt *openolt.OnuPacket) (*openolt.Empty, error)  {
+func (o OltDevice) OnuPacketOut(ctx context.Context, onuPkt *openolt.OnuPacket) (*openolt.Empty, error) {
 	pon, _ := o.getPonById(onuPkt.IntfId)
 	onu, _ := pon.getOnuById(onuPkt.OnuId)
 
 	rawpkt := gopacket.NewPacket(onuPkt.Pkt, layers.LayerTypeEthernet, gopacket.Default)
 
-	// NOTE is this the best way to the to the ethertype?
 	etherType := rawpkt.Layer(layers.LayerTypeEthernet).(*layers.Ethernet).EthernetType
 
 	if etherType == layers.EthernetTypeEAPOL {
 		eapolPkt := bbsim.ByteMsg{IntfId: onuPkt.IntfId, OnuId: onuPkt.OnuId, Bytes: rawpkt.Data()}
 		onu.eapolPktOutCh <- &eapolPkt
+	} else if layerDHCP := rawpkt.Layer(layers.LayerTypeDHCPv4); layerDHCP != nil {
+		// TODO use IsDhcpPacket
+		// TODO we need to untag the packets
+		// NOTE here we receive packets going from the DHCP Server to the ONU
+		// for now we expect them to be double-tagged, but ideally the should be single tagged
+		dhcpPkt := bbsim.ByteMsg{IntfId: onuPkt.IntfId, OnuId: onuPkt.OnuId, Bytes: rawpkt.Data()}
+		onu.dhcpPktOutCh <- &dhcpPkt
 	}
-	return new(openolt.Empty) , nil
+	return new(openolt.Empty), nil
 }
 
-func (o OltDevice) Reboot(context.Context, *openolt.Empty) (*openolt.Empty, error)  {
+func (o OltDevice) Reboot(context.Context, *openolt.Empty) (*openolt.Empty, error) {
 	oltLogger.Info("Shutting Down")
 	close(*o.oltDoneChannel)
 	close(*o.apiDoneChannel)
-	return new(openolt.Empty) , nil
+	return new(openolt.Empty), nil
 }
 
 func (o OltDevice) ReenableOlt(context.Context, *openolt.Empty) (*openolt.Empty, error) {
 	oltLogger.Error("ReenableOlt not implemented")
-	return new(openolt.Empty) , nil
+	return new(openolt.Empty), nil
 }
 
 func (o OltDevice) UplinkPacketOut(context context.Context, packet *openolt.UplinkPacket) (*openolt.Empty, error) {
-	oltLogger.Warn("UplinkPacketOut not implemented")
-	return new(openolt.Empty) , nil
+	pkt := gopacket.NewPacket(packet.Pkt, layers.LayerTypeEthernet, gopacket.Default)
+
+	sendNniPacket(pkt)
+	// NOTE should we return an error if sendNniPakcet fails?
+	return new(openolt.Empty), nil
 }
 
-func (o OltDevice) CollectStatistics(context.Context, *openolt.Empty) (*openolt.Empty, error)  {
+func (o OltDevice) CollectStatistics(context.Context, *openolt.Empty) (*openolt.Empty, error) {
 	oltLogger.Error("CollectStatistics not implemented")
-	return new(openolt.Empty) , nil
+	return new(openolt.Empty), nil
 }
 
 func (o OltDevice) GetOnuInfo(context context.Context, packet *openolt.Onu) (*openolt.OnuIndication, error) {
 	oltLogger.Error("GetOnuInfo not implemented")
-	return new(openolt.OnuIndication) , nil
+	return new(openolt.OnuIndication), nil
 }
 
 func (o OltDevice) GetPonIf(context context.Context, packet *openolt.Interface) (*openolt.IntfIndication, error) {
 	oltLogger.Error("GetPonIf not implemented")
-	return new(openolt.IntfIndication) , nil
+	return new(openolt.IntfIndication), nil
 }
 
 func (s OltDevice) CreateTrafficQueues(context.Context, *tech_profile.TrafficQueues) (*openolt.Empty, error) {
@@ -556,4 +586,4 @@
 func (s OltDevice) RemoveTrafficSchedulers(context.Context, *tech_profile.TrafficSchedulers) (*openolt.Empty, error) {
 	oltLogger.Info("received RemoveTrafficSchedulers")
 	return new(openolt.Empty), nil
-}
\ No newline at end of file
+}
diff --git a/internal/bbsim/devices/onu.go b/internal/bbsim/devices/onu.go
index fac7ce1..77e912c 100644
--- a/internal/bbsim/devices/onu.go
+++ b/internal/bbsim/devices/onu.go
@@ -18,129 +18,130 @@
 
 import (
 	"fmt"
-	"github.com/opencord/bbsim/internal/bbsim/responders/eapol"
 	"github.com/google/gopacket/layers"
 	"github.com/looplab/fsm"
+	"github.com/opencord/bbsim/internal/bbsim/responders/dhcp"
+	"github.com/opencord/bbsim/internal/bbsim/responders/eapol"
+	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
 	omci "github.com/opencord/omci-sim"
 	"github.com/opencord/voltha-protos/go/openolt"
 	log "github.com/sirupsen/logrus"
-	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
+	"net"
 )
 
 var onuLogger = log.WithFields(log.Fields{
 	"module": "ONU",
 })
 
-func CreateONU(olt OltDevice, pon PonPort, id uint32) Onu {
-		o := Onu{
-			ID: id,
-			PonPortID: pon.ID,
-			PonPort: pon,
-			// NOTE can we combine everything in a single channel?
-			channel: make(chan Message),
-			eapolPktOutCh: make(chan *bbsim.ByteMsg, 1024),
-		}
-		o.SerialNumber = o.NewSN(olt.ID, pon.ID, o.ID)
+func CreateONU(olt OltDevice, pon PonPort, id uint32, sTag int, cTag int) Onu {
 
-		// NOTE this state machine is used to track the operational
-		// state as requested by VOLTHA
-		o.OperState = getOperStateFSM(func(e *fsm.Event) {
-			onuLogger.WithFields(log.Fields{
-				"ID": o.ID,
-			}).Debugf("Changing ONU OperState from %s to %s", e.Src, e.Dst)
-		})
+	o := Onu{
+		ID:        id,
+		PonPortID: pon.ID,
+		PonPort:   pon,
+		STag:      sTag,
+		CTag:      cTag,
+		HwAddress: net.HardwareAddr{0x2e, 0x60, 0x70, 0x13, byte(pon.ID), byte(id)},
+		// NOTE can we combine everything in a single channel?
+		channel:       make(chan Message, 2048),
+		eapolPktOutCh: make(chan *bbsim.ByteMsg, 1024),
+		dhcpPktOutCh:  make(chan *bbsim.ByteMsg, 1024),
+	}
+	o.SerialNumber = o.NewSN(olt.ID, pon.ID, o.ID)
 
-		// NOTE this state machine is used to activate the OMCI, EAPOL and DHCP clients
-		o.InternalState = fsm.NewFSM(
-			"created",
-			fsm.Events{
-				{Name: "discover", Src: []string{"created"}, Dst: "discovered"},
-				{Name: "enable", Src: []string{"discovered"}, Dst: "enabled"},
-				{Name: "receive_eapol_flow", Src: []string{"enabled", "gem_port_added"}, Dst: "eapol_flow_received"},
-				{Name: "add_gem_port", Src: []string{"enabled", "eapol_flow_received"}, Dst: "gem_port_added"},
-				{Name: "start_auth", Src: []string{"eapol_flow_received", "gem_port_added"}, Dst: "auth_started"},
-				{Name: "eap_start_sent", Src: []string{"auth_started"}, Dst: "eap_start_sent"},
-				{Name: "eap_resonse_identity_sent", Src: []string{"eap_start_sent"}, Dst: "eap_resonse_identity_sent"},
-				{Name: "eap_resonse_challenge_sent", Src: []string{"eap_resonse_identity_sent"}, Dst: "eap_resonse_challenge_sent"},
-				{Name: "eap_resonse_success_received", Src: []string{"eap_resonse_challenge_sent"}, Dst: "eap_resonse_success_received"},
-				{Name: "auth_failed", Src: []string{"auth_started", "eap_start_sent", "eap_resonse_identity_sent", "eap_resonse_challenge_sent"}, Dst: "auth_failed"},
+	// NOTE this state machine is used to track the operational
+	// state as requested by VOLTHA
+	o.OperState = getOperStateFSM(func(e *fsm.Event) {
+		onuLogger.WithFields(log.Fields{
+			"ID": o.ID,
+		}).Debugf("Changing ONU OperState from %s to %s", e.Src, e.Dst)
+	})
+
+	// NOTE this state machine is used to activate the OMCI, EAPOL and DHCP clients
+	o.InternalState = fsm.NewFSM(
+		"created",
+		fsm.Events{
+			// DEVICE Activation
+			{Name: "discover", Src: []string{"created"}, Dst: "discovered"},
+			{Name: "enable", Src: []string{"discovered"}, Dst: "enabled"},
+			{Name: "receive_eapol_flow", Src: []string{"enabled", "gem_port_added"}, Dst: "eapol_flow_received"},
+			{Name: "add_gem_port", Src: []string{"enabled", "eapol_flow_received"}, Dst: "gem_port_added"},
+			// EAPOL
+			{Name: "start_auth", Src: []string{"eapol_flow_received", "gem_port_added"}, Dst: "auth_started"},
+			{Name: "eap_start_sent", Src: []string{"auth_started"}, Dst: "eap_start_sent"},
+			{Name: "eap_response_identity_sent", Src: []string{"eap_start_sent"}, Dst: "eap_response_identity_sent"},
+			{Name: "eap_response_challenge_sent", Src: []string{"eap_response_identity_sent"}, Dst: "eap_response_challenge_sent"},
+			{Name: "eap_response_success_received", Src: []string{"eap_response_challenge_sent"}, Dst: "eap_response_success_received"},
+			{Name: "auth_failed", Src: []string{"auth_started", "eap_start_sent", "eap_response_identity_sent", "eap_response_challenge_sent"}, Dst: "auth_failed"},
+			// DHCP
+			{Name: "start_dhcp", Src: []string{"eap_response_success_received"}, Dst: "dhcp_started"},
+			{Name: "dhcp_discovery_sent", Src: []string{"dhcp_started"}, Dst: "dhcp_discovery_sent"},
+			{Name: "dhcp_request_sent", Src: []string{"dhcp_discovery_sent"}, Dst: "dhcp_request_sent"},
+			{Name: "dhcp_ack_received", Src: []string{"dhcp_request_sent"}, Dst: "dhcp_ack_received"},
+			{Name: "dhcp_failed", Src: []string{"dhcp_started", "dhcp_discovery_sent", "dhcp_request_sent"}, Dst: "dhcp_failed"},
+		},
+		fsm.Callbacks{
+			"enter_state": func(e *fsm.Event) {
+				o.logStateChange(e.Src, e.Dst)
 			},
-			fsm.Callbacks{
-				"enter_state": func(e *fsm.Event) {
-					o.logStateChange(e.Src, e.Dst)
-				},
-				"enter_eapol_flow_received": func(e *fsm.Event) {
-					o.logStateChange(e.Src, e.Dst)
-					if e.Src == "enter_gem_port_added" {
-						if err := o.InternalState.Event("start_auth"); err != nil {
-							log.Infof("Transitioning to StartAuth")
-							onuLogger.WithFields(log.Fields{
-								"OnuId":  o.ID,
-								"IntfId": o.PonPortID,
-								"OnuSn":  o.SerialNumber,
-							}).Errorf("Error while transitioning ONU State")
-						}
-					}
-				},
-				"enter_gem_port_added": func(e *fsm.Event) {
-					o.logStateChange(e.Src, e.Dst)
-					if e.Src == "eapol_flow_received" {
-						log.Infof("Transitioning to StartAuth")
-						if err := o.InternalState.Event("start_auth"); err != nil {
-							onuLogger.WithFields(log.Fields{
-								"OnuId": o.ID,
-								"IntfId": o.PonPortID,
-								"OnuSn": o.SerialNumber,
-							}).Errorf("Error while transitioning ONU State")
-						}
-					}
-				},
-				"enter_auth_started": func(e *fsm.Event) {
-					o.logStateChange(e.Src, e.Dst)
-					msg := Message{
-						Type:      StartEAPOL,
-						Data: EapStartMessage{
-							PonPortID: o.PonPortID,
-							OnuID: o.ID,
-						},
-					}
-					go func(msg Message){
-						// you can only send a value on an unbuffered channel without blocking
-						o.channel <- msg
-					}(msg)
-
-				},
-				"enter_eap_resonse_success_received": func(e *fsm.Event) {
-					o.logStateChange(e.Src, e.Dst)
-					onuLogger.WithFields(log.Fields{
-						"OnuId": o.ID,
-						"IntfId": o.PonPortID,
-						"OnuSn": o.SerialNumber,
-					}).Warnf("TODO start DHCP request")
-				},
+			"enter_auth_started": func(e *fsm.Event) {
+				o.logStateChange(e.Src, e.Dst)
+				msg := Message{
+					Type: StartEAPOL,
+					Data: PacketMessage{
+						PonPortID: o.PonPortID,
+						OnuID:     o.ID,
+					},
+				}
+				o.channel <- msg
 			},
-		)
-		return o
+			"enter_auth_failed": func(e *fsm.Event) {
+				onuLogger.WithFields(log.Fields{
+					"OnuId":  o.ID,
+					"IntfId": o.PonPortID,
+					"OnuSn":  o.Sn(),
+				}).Errorf("ONU failed to authenticate!")
+			},
+			"enter_dhcp_started": func(e *fsm.Event) {
+				msg := Message{
+					Type: StartDHCP,
+					Data: PacketMessage{
+						PonPortID: o.PonPortID,
+						OnuID:     o.ID,
+					},
+				}
+				o.channel <- msg
+			},
+			"enter_dhcp_failed": func(e *fsm.Event) {
+				onuLogger.WithFields(log.Fields{
+					"OnuId":  o.ID,
+					"IntfId": o.PonPortID,
+					"OnuSn":  o.Sn(),
+				}).Errorf("ONU failed to DHCP!")
+			},
+		},
+	)
+	return o
 }
 
 func (o Onu) logStateChange(src string, dst string) {
 	onuLogger.WithFields(log.Fields{
-		"OnuId": o.ID,
+		"OnuId":  o.ID,
 		"IntfId": o.PonPortID,
-		"OnuSn": o.SerialNumber,
+		"OnuSn":  o.Sn(),
 	}).Debugf("Changing ONU InternalState from %s to %s", src, dst)
 }
 
-func (o Onu) processOnuMessages(stream openolt.Openolt_EnableIndicationServer)  {
+func (o Onu) processOnuMessages(stream openolt.Openolt_EnableIndicationServer) {
 	onuLogger.WithFields(log.Fields{
 		"onuID": o.ID,
-		"onuSN": o.SerialNumber,
+		"onuSN": o.Sn(),
 	}).Debug("Started ONU Indication Channel")
 
 	for message := range o.channel {
 		onuLogger.WithFields(log.Fields{
-			"onuID": o.ID,
-			"onuSN": o.SerialNumber,
+			"onuID":       o.ID,
+			"onuSN":       o.SerialNumber,
 			"messageType": message.Type,
 		}).Tracef("Received message on ONU Channel")
 
@@ -161,7 +162,29 @@
 			log.Infof("Receive StartEAPOL message on ONU channel")
 			go func() {
 				// TODO kill this thread
-				eapol.CreateWPASupplicant(o.ID, o.PonPortID, o.SerialNumber, o.InternalState, stream, o.eapolPktOutCh)
+				eapol.CreateWPASupplicant(
+					o.ID,
+					o.PonPortID,
+					o.Sn(),
+					o.InternalState,
+					stream,
+					o.eapolPktOutCh,
+				)
+			}()
+		case StartDHCP:
+			log.Infof("Receive StartDHCP message on ONU channel")
+			go func() {
+				// TODO kill this thread
+				dhcp.CreateDHCPClient(
+					o.ID,
+					o.PonPortID,
+					o.Sn(),
+					o.HwAddress,
+					o.CTag,
+					o.InternalState,
+					stream,
+					o.dhcpPktOutCh,
+				)
 			}()
 		default:
 			onuLogger.Warnf("Received unknown message data %v for type %v in OLT channel", message.Data, message.Type)
@@ -169,19 +192,19 @@
 	}
 }
 
-func (o Onu) processOmciMessages(stream openolt.Openolt_EnableIndicationServer)  {
+func (o Onu) processOmciMessages(stream openolt.Openolt_EnableIndicationServer) {
 	ch := omci.GetChannel()
 
 	onuLogger.WithFields(log.Fields{
 		"onuID": o.ID,
-		"onuSN": o.SerialNumber,
+		"onuSN": o.Sn(),
 	}).Debug("Started OMCI Indication Channel")
 
 	for message := range ch {
 		switch message.Type {
 		case omci.GemPortAdded:
 			log.WithFields(log.Fields{
-				"OnuId": message.Data.OnuId,
+				"OnuId":  message.Data.OnuId,
 				"IntfId": message.Data.IntfId,
 			}).Infof("GemPort Added")
 
@@ -191,7 +214,7 @@
 				if err := o.InternalState.Event("add_gem_port"); err != nil {
 					log.Errorf("Can't go to gem_port_added: %v", err)
 				}
-			} else if  o.InternalState.Is("eapol_flow_received"){
+			} else if o.InternalState.Is("eapol_flow_received") {
 				if err := o.InternalState.Event("start_auth"); err != nil {
 					log.Errorf("Can't go to auth_started: %v", err)
 				}
@@ -213,7 +236,7 @@
 
 func (o Onu) sendOnuDiscIndication(msg OnuDiscIndicationMessage, stream openolt.Openolt_EnableIndicationServer) {
 	discoverData := &openolt.Indication_OnuDiscInd{OnuDiscInd: &openolt.OnuDiscIndication{
-		IntfId: msg.Onu.PonPortID,
+		IntfId:       msg.Onu.PonPortID,
 		SerialNumber: msg.Onu.SerialNumber,
 	}}
 	if err := stream.Send(&openolt.Indication{Data: discoverData}); err != nil {
@@ -222,8 +245,8 @@
 	o.InternalState.Event("discover")
 	onuLogger.WithFields(log.Fields{
 		"IntfId": msg.Onu.PonPortID,
-		"SerialNumber": msg.Onu.SerialNumber,
-		"OnuId": o.ID,
+		"OnuSn":  msg.Onu.Sn(),
+		"OnuId":  o.ID,
 	}).Debug("Sent Indication_OnuDiscInd")
 }
 
@@ -235,10 +258,10 @@
 	o.OperState.Event("enable")
 
 	indData := &openolt.Indication_OnuInd{OnuInd: &openolt.OnuIndication{
-		IntfId: o.PonPortID,
-		OnuId: o.ID,
-		OperState: o.OperState.Current(),
-		AdminState: o.OperState.Current(),
+		IntfId:       o.PonPortID,
+		OnuId:        o.ID,
+		OperState:    o.OperState.Current(),
+		AdminState:   o.OperState.Current(),
 		SerialNumber: o.SerialNumber,
 	}}
 	if err := stream.Send(&openolt.Indication{Data: indData}); err != nil {
@@ -246,20 +269,20 @@
 	}
 	o.InternalState.Event("enable")
 	onuLogger.WithFields(log.Fields{
-		"IntfId": o.PonPortID,
-		"OnuId": o.ID,
-		"OperState": msg.OperState.String(),
+		"IntfId":     o.PonPortID,
+		"OnuId":      o.ID,
+		"OperState":  msg.OperState.String(),
 		"AdminState": msg.OperState.String(),
-		"SerialNumber": o.SerialNumber,
+		"OnuSn":      o.Sn(),
 	}).Debug("Sent Indication_OnuInd")
 }
 
 func (o Onu) handleOmciMessage(msg OmciMessage, stream openolt.Openolt_EnableIndicationServer) {
 
 	onuLogger.WithFields(log.Fields{
-		"IntfId": o.PonPortID,
+		"IntfId":       o.PonPortID,
 		"SerialNumber": o.SerialNumber,
-		"omciPacket": msg.omciMsg.Pkt,
+		"omciPacket":   msg.omciMsg.Pkt,
 	}).Tracef("Received OMCI message")
 
 	var omciInd openolt.OmciIndication
@@ -277,36 +300,48 @@
 		onuLogger.Errorf("send omci indication failed: %v", err)
 	}
 	onuLogger.WithFields(log.Fields{
-		"IntfId": o.PonPortID,
+		"IntfId":       o.PonPortID,
 		"SerialNumber": o.SerialNumber,
-		"omciPacket": omciInd.Pkt,
+		"omciPacket":   omciInd.Pkt,
 	}).Tracef("Sent OMCI message")
 }
 
 func (o Onu) handleFlowUpdate(msg OnuFlowUpdateMessage, stream openolt.Openolt_EnableIndicationServer) {
 	onuLogger.WithFields(log.Fields{
-		"IntfId": msg.Flow.AccessIntfId,
-		"OnuId": msg.Flow.OnuId,
-		"EthType": fmt.Sprintf("%x", msg.Flow.Classifier.EthType),
+		"DstPort":   msg.Flow.Classifier.DstPort,
+		"EthType":   fmt.Sprintf("%x", msg.Flow.Classifier.EthType),
+		"FlowId":    msg.Flow.FlowId,
+		"FlowType":  msg.Flow.FlowType,
 		"InnerVlan": msg.Flow.Classifier.IVid,
+		"IntfId":    msg.Flow.AccessIntfId,
+		"IpProto":   msg.Flow.Classifier.IpProto,
+		"OnuId":     msg.Flow.OnuId,
+		"OnuSn":     o.Sn(),
 		"OuterVlan": msg.Flow.Classifier.OVid,
-		"FlowType": msg.Flow.FlowType,
-		"FlowId": msg.Flow.FlowId,
-		"UniID": msg.Flow.UniId,
-		"PortNo": msg.Flow.PortNo,
-	}).Infof("ONU receives Flow")
+		"PortNo":    msg.Flow.PortNo,
+		"SrcPort":   msg.Flow.Classifier.SrcPort,
+		"UniID":     msg.Flow.UniId,
+	}).Debug("ONU receives Flow")
+
 	if msg.Flow.Classifier.EthType == uint32(layers.EthernetTypeEAPOL) && msg.Flow.Classifier.OVid == 4091 {
 		// NOTE if we receive the EAPOL flows but we don't have GemPorts
 		// go an intermediate state, otherwise start auth
 		if o.InternalState.Is("enabled") {
 			if err := o.InternalState.Event("receive_eapol_flow"); err != nil {
-				log.Errorf("Can't go to eapol_flow_received: %v", err)
+				log.Warnf("Can't go to eapol_flow_received: %v", err)
 			}
-		} else if  o.InternalState.Is("gem_port_added"){
+		} else if o.InternalState.Is("gem_port_added") {
 			if err := o.InternalState.Event("start_auth"); err != nil {
-				log.Errorf("Can't go to auth_started: %v", err)
+				log.Warnf("Can't go to auth_started: %v", err)
 			}
 		}
+	} else if msg.Flow.Classifier.EthType == uint32(layers.EthernetTypeIPv4) &&
+		msg.Flow.Classifier.SrcPort == uint32(68) &&
+		msg.Flow.Classifier.DstPort == uint32(67) {
+		// NOTE we are receiving mulitple DHCP flows but we shouldn't call the transition multiple times
+		if err := o.InternalState.Event("start_dhcp"); err != nil {
+			log.Warnf("Can't go to dhcp_started: %v", err)
+		}
 	}
 }
 
@@ -321,4 +356,4 @@
 	}
 	onuLogger.Tracef("Omci decoded: %x.", p)
 	return p
-}
\ No newline at end of file
+}
diff --git a/internal/bbsim/devices/types.go b/internal/bbsim/devices/types.go
index 65aa09e..e012858 100644
--- a/internal/bbsim/devices/types.go
+++ b/internal/bbsim/devices/types.go
@@ -20,9 +20,10 @@
 	"bytes"
 	"errors"
 	"fmt"
-	"github.com/opencord/voltha-protos/go/openolt"
 	"github.com/looplab/fsm"
 	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
+	"github.com/opencord/voltha-protos/go/openolt"
+	"net"
 )
 
 // TODO get rid of this file
@@ -32,19 +33,25 @@
 
 // Devices
 type Onu struct {
-	ID uint32
-	PonPortID uint32
-	PonPort PonPort
+	ID            uint32
+	PonPortID     uint32
+	PonPort       PonPort
+	STag          int
+	CTag          int
+	HwAddress     net.HardwareAddr
 	InternalState *fsm.FSM
 
-	OperState *fsm.FSM
+	OperState    *fsm.FSM
 	SerialNumber *openolt.SerialNumber
 
-	channel chan Message
-	eapolPktOutCh chan *bbsim.ByteMsg
+	channel       chan Message        // this channel is to track state changes and OMCI messages
+	eapolPktOutCh chan *bbsim.ByteMsg // this channel is for EAPOL Packet Outs (coming from the controller)
+	dhcpPktOutCh  chan *bbsim.ByteMsg // this channel is for DHCP Packet Outs (coming from the controller)
 }
 
-
+func (o Onu) Sn() string {
+	return onuSnToString(o.SerialNumber)
+}
 
 type NniPort struct {
 	// BBSIM Internals
@@ -52,18 +59,18 @@
 
 	// PON Attributes
 	OperState *fsm.FSM
-	Type string
+	Type      string
 }
 
 type PonPort struct {
 	// BBSIM Internals
-	ID uint32
+	ID     uint32
 	NumOnu int
-	Onus []Onu
+	Onus   []Onu
 
 	// PON Attributes
 	OperState *fsm.FSM
-	Type string
+	Type      string
 
 	// NOTE do we need a state machine for the PON Ports?
 }
@@ -88,15 +95,16 @@
 
 type OltDevice struct {
 	// BBSIM Internals
-	ID int
-	SerialNumber string
-	NumNni int
-	NumPon int
-	NumOnuPerPon int
-	InternalState *fsm.FSM
-	channel chan Message
-	oltDoneChannel *chan bool
-	apiDoneChannel *chan bool
+	ID              int
+	SerialNumber    string
+	NumNni          int
+	NumPon          int
+	NumOnuPerPon    int
+	InternalState   *fsm.FSM
+	channel         chan Message
+	oltDoneChannel  *chan bool
+	apiDoneChannel  *chan bool
+	nniPktInChannel chan *bbsim.PacketMsg
 
 	Pons []PonPort
 	Nnis []NniPort
@@ -116,8 +124,10 @@
 	OnuDiscIndication MessageType = 3
 	OnuIndication     MessageType = 4
 	OMCI              MessageType = 5
-	FlowUpdate		  MessageType = 6
-	StartEAPOL		  MessageType = 7
+	FlowUpdate        MessageType = 6
+	StartEAPOL        MessageType = 7
+	DoneEAPOL         MessageType = 8
+	StartDHCP         MessageType = 9
 )
 
 func (m MessageType) String() string {
@@ -130,13 +140,15 @@
 		"OMCI",
 		"FlowUpdate",
 		"StartEAPOL",
+		"DoneEAPOL",
+		"StartDHCP",
 	}
 	return names[m]
 }
 
 type Message struct {
-	Type      MessageType
-	Data 	  interface{}
+	Type MessageType
+	Data interface{}
 }
 
 type OltIndicationMessage struct {
@@ -166,9 +178,9 @@
 }
 
 type OmciMessage struct {
-	OnuSN     *openolt.SerialNumber
-	OnuID 	  uint32
-	omciMsg   *openolt.OmciMsg
+	OnuSN   *openolt.SerialNumber
+	OnuID   uint32
+	omciMsg *openolt.OmciMsg
 }
 
 type OnuFlowUpdateMessage struct {
@@ -177,17 +189,16 @@
 	Flow      *openolt.Flow
 }
 
-type EapStartMessage struct {
+type PacketMessage struct {
 	PonPortID uint32
 	OnuID     uint32
 }
 
-
 type OperState int
 
 const (
-	UP OperState = iota
-	DOWN // The device has been discovered, but not yet activated
+	UP   OperState = iota
+	DOWN           // The device has been discovered, but not yet activated
 )
 
 func (m OperState) String() string {
@@ -196,4 +207,4 @@
 		"down",
 	}
 	return names[m]
-}
\ No newline at end of file
+}
diff --git a/internal/bbsim/grpc_api_server.go b/internal/bbsim/grpc_api_server.go
index 304d3fa..20b6164 100644
--- a/internal/bbsim/grpc_api_server.go
+++ b/internal/bbsim/grpc_api_server.go
@@ -89,10 +89,13 @@
 		for _, o := range pon.Onus {
 			onu := bbsim.ONU{
 				ID:            int32(o.ID),
-				SerialNumber:  o.SerialNumber.String(),
+				SerialNumber:  o.Sn(),
 				OperState:     o.OperState.Current(),
 				InternalState: o.InternalState.Current(),
 				PonPortID:     int32(o.PonPortID),
+				STag:          int32(o.STag),
+				CTag:          int32(o.CTag),
+				HwAddress:     o.HwAddress.String(),
 			}
 			onus.Items = append(onus.Items, &onu)
 		}
diff --git a/internal/bbsim/packetHandlers/filters.go b/internal/bbsim/packetHandlers/filters.go
new file mode 100644
index 0000000..3233c8c
--- /dev/null
+++ b/internal/bbsim/packetHandlers/filters.go
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018-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 packetHandlers
+
+import (
+	"github.com/google/gopacket"
+	"github.com/google/gopacket/layers"
+	"net"
+)
+
+func IsDhcpPacket(pkt gopacket.Packet) bool {
+	if layerDHCP := pkt.Layer(layers.LayerTypeDHCPv4); layerDHCP != nil {
+		return true
+	}
+	return false
+}
+
+func IsIncomingPacket(packet gopacket.Packet) bool {
+	if ipLayer := packet.Layer(layers.LayerTypeIPv4); ipLayer != nil {
+
+		ip, _ := ipLayer.(*layers.IPv4)
+
+		// FIXME find a better way to filter outgoing packets
+		if ip.SrcIP.Equal(net.ParseIP("182.21.0.128")) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/internal/bbsim/packetHandlers/packet_tags.go b/internal/bbsim/packetHandlers/packet_tags.go
new file mode 100644
index 0000000..3e31e3c
--- /dev/null
+++ b/internal/bbsim/packetHandlers/packet_tags.go
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2018-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 packetHandlers
+
+import (
+	"errors"
+	"github.com/google/gopacket"
+	"github.com/google/gopacket/layers"
+)
+
+func PushSingleTag(tag int, pkt gopacket.Packet) (gopacket.Packet, error) {
+	// TODO can this method be semplified?
+	if eth := getEthernetLayer(pkt); eth != nil {
+		ethernetLayer := &layers.Ethernet{
+			SrcMAC:       eth.SrcMAC,
+			DstMAC:       eth.DstMAC,
+			EthernetType: 0x8100,
+		}
+
+		dot1qLayer := &layers.Dot1Q{
+			Type:           eth.EthernetType,
+			VLANIdentifier: uint16(tag),
+		}
+
+		buffer := gopacket.NewSerializeBuffer()
+		gopacket.SerializeLayers(
+			buffer,
+			gopacket.SerializeOptions{
+				FixLengths: false,
+			},
+			ethernetLayer,
+			dot1qLayer,
+			gopacket.Payload(eth.Payload),
+		)
+		ret := gopacket.NewPacket(
+			buffer.Bytes(),
+			layers.LayerTypeEthernet,
+			gopacket.Default,
+		)
+
+		return ret, nil
+	}
+	return nil, errors.New("Couldn't extract LayerTypeEthernet from packet")
+}
+
+func PopSingleTag(pkt gopacket.Packet) (gopacket.Packet, error) {
+	layer, err := getDot1QLayer(pkt)
+	if err != nil {
+		return nil, err
+	}
+
+	if eth := getEthernetLayer(pkt); eth != nil {
+		ethernetLayer := &layers.Ethernet{
+			SrcMAC:       eth.SrcMAC,
+			DstMAC:       eth.DstMAC,
+			EthernetType: layer.Type,
+		}
+		buffer := gopacket.NewSerializeBuffer()
+		gopacket.SerializeLayers(buffer, gopacket.SerializeOptions{},
+			ethernetLayer,
+			gopacket.Payload(layer.Payload),
+		)
+		retpkt := gopacket.NewPacket(
+			buffer.Bytes(),
+			layers.LayerTypeEthernet,
+			gopacket.Default,
+		)
+
+		return retpkt, nil
+	}
+	return nil, errors.New("no-ethernet-layer")
+}
+
+func PopDoubleTag(pkt gopacket.Packet) (gopacket.Packet, error) {
+	packet, err := PopSingleTag(pkt)
+	if err != nil {
+		return nil, err
+	}
+	packet, err = PopSingleTag(packet)
+	if err != nil {
+		return nil, err
+	}
+	return packet, nil
+}
+
+func getEthernetLayer(pkt gopacket.Packet) *layers.Ethernet {
+	eth := &layers.Ethernet{}
+	if ethLayer := pkt.Layer(layers.LayerTypeEthernet); ethLayer != nil {
+		eth, _ = ethLayer.(*layers.Ethernet)
+	}
+	return eth
+}
+
+func getDot1QLayer(pkt gopacket.Packet) (*layers.Dot1Q, error) {
+	dot1q := &layers.Dot1Q{}
+	if dot1qLayer := pkt.Layer(layers.LayerTypeDot1Q); dot1qLayer != nil {
+		dot1q = dot1qLayer.(*layers.Dot1Q)
+		return dot1q, nil
+	}
+	return nil, errors.New("no-dot1q-layer-in-packet")
+}
+
+func getVlanTag(pkt gopacket.Packet) (uint16, error) {
+	dot1q, err := getDot1QLayer(pkt)
+	if err != nil {
+		return 0, err
+	}
+	return dot1q.VLANIdentifier, nil
+}
diff --git a/internal/bbsim/packetHandlers/packet_tags_test.go b/internal/bbsim/packetHandlers/packet_tags_test.go
new file mode 100644
index 0000000..91c944e
--- /dev/null
+++ b/internal/bbsim/packetHandlers/packet_tags_test.go
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2018-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 packetHandlers
+
+import (
+	"fmt"
+	"github.com/google/gopacket"
+	"github.com/google/gopacket/layers"
+	"gotest.tools/assert"
+	"net"
+	"os"
+	"testing"
+)
+
+func setUp() {
+	fmt.Println("Test Setup")
+}
+
+func tearDown() {
+	fmt.Println("Test Teardown")
+}
+
+func TestMain(m *testing.M) {
+	setUp()
+	code := m.Run()
+	tearDown()
+	os.Exit(code)
+}
+
+// GO111MODULE=on go test -v -mod vendor ./internal/bbsim/... -run TestPushSingleTag
+func TestPushSingleTag(t *testing.T) {
+	rawBytes := []byte{10, 20, 30}
+	srcMac := net.HardwareAddr{0xff, 0xff, 0xff, 0xff, byte(1), byte(1)}
+	dstMac := net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
+
+	ethernetLayer := &layers.Ethernet{
+		SrcMAC:       srcMac,
+		DstMAC:       dstMac,
+		EthernetType: 0x8100,
+	}
+
+	buffer := gopacket.NewSerializeBuffer()
+	gopacket.SerializeLayers(
+		buffer,
+		gopacket.SerializeOptions{
+			FixLengths: false,
+		},
+		ethernetLayer,
+		gopacket.Payload(rawBytes),
+	)
+
+	untaggedPkt := gopacket.NewPacket(buffer.Bytes(), layers.LayerTypeEthernet, gopacket.Default)
+	taggedPkt, err := PushSingleTag(111, untaggedPkt)
+	if err != nil {
+		t.Fail()
+		t.Logf("Error in PushSingleTag: %v", err)
+	}
+
+	vlan, _ := getVlanTag(taggedPkt)
+	assert.Equal(t, vlan, uint16(111))
+}
+
+func TestPopSingleTag(t *testing.T) {
+	rawBytes := []byte{10, 20, 30}
+	srcMac := net.HardwareAddr{0xff, 0xff, 0xff, 0xff, byte(1), byte(1)}
+	dstMac := net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
+
+	ethernetLayer := &layers.Ethernet{
+		SrcMAC:       srcMac,
+		DstMAC:       dstMac,
+		EthernetType: 0x8100,
+	}
+
+	dot1qLayer := &layers.Dot1Q{
+		Type:           0x8100,
+		VLANIdentifier: uint16(111),
+	}
+
+	buffer := gopacket.NewSerializeBuffer()
+	gopacket.SerializeLayers(
+		buffer,
+		gopacket.SerializeOptions{
+			FixLengths: false,
+		},
+		ethernetLayer,
+		dot1qLayer,
+		gopacket.Payload(rawBytes),
+	)
+
+	untaggedPkt := gopacket.NewPacket(buffer.Bytes(), layers.LayerTypeEthernet, gopacket.Default)
+	taggedPkt, err := PopSingleTag(untaggedPkt)
+	if err != nil {
+		t.Fail()
+		t.Logf("Error in PushSingleTag: %v", err)
+	}
+
+	vlan, err := getVlanTag(taggedPkt)
+	assert.Equal(t, vlan, uint16(2580)) // FIXME where dows 2056 comes from??
+}
+
+func TestPopDoubleTag(t *testing.T) {
+	rawBytes := []byte{10, 20, 30}
+	srcMac := net.HardwareAddr{0xff, 0xff, 0xff, 0xff, byte(1), byte(1)}
+	dstMac := net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
+
+	ethernetLayer := &layers.Ethernet{
+		SrcMAC:       srcMac,
+		DstMAC:       dstMac,
+		EthernetType: 0x8100,
+	}
+
+	dot1qLayer := &layers.Dot1Q{
+		Type:           0x8100,
+		VLANIdentifier: uint16(111),
+	}
+
+	buffer := gopacket.NewSerializeBuffer()
+	gopacket.SerializeLayers(
+		buffer,
+		gopacket.SerializeOptions{
+			FixLengths: false,
+		},
+		ethernetLayer,
+		dot1qLayer,
+		gopacket.Payload(rawBytes),
+	)
+
+	untaggedPkt := gopacket.NewPacket(buffer.Bytes(), layers.LayerTypeEthernet, gopacket.Default)
+	taggedPkt, err := PopDoubleTag(untaggedPkt)
+	if err != nil {
+		t.Fail()
+		t.Logf("Error in PushSingleTag: %v", err)
+	}
+
+	vlan, err := getVlanTag(taggedPkt)
+	assert.Equal(t, vlan, uint16(0))
+	assert.Equal(t, err.Error(), "no-dot1q-layer-in-packet")
+}
diff --git a/internal/bbsim/responders/dhcp/dhcp.go b/internal/bbsim/responders/dhcp/dhcp.go
new file mode 100644
index 0000000..d2a3db7
--- /dev/null
+++ b/internal/bbsim/responders/dhcp/dhcp.go
@@ -0,0 +1,387 @@
+/*
+ * Copyright 2018-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 dhcp
+
+import (
+	"errors"
+	"fmt"
+	"github.com/google/gopacket"
+	"github.com/google/gopacket/layers"
+	"github.com/looplab/fsm"
+	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
+	omci "github.com/opencord/omci-sim"
+	"github.com/opencord/voltha-protos/go/openolt"
+	log "github.com/sirupsen/logrus"
+	"net"
+	"reflect"
+)
+
+var dhcpLogger = log.WithFields(log.Fields{
+	"module": "DHCP",
+})
+
+var defaultParamsRequestList = []layers.DHCPOpt{
+	layers.DHCPOptSubnetMask,
+	layers.DHCPOptBroadcastAddr,
+	layers.DHCPOptTimeOffset,
+	layers.DHCPOptRouter,
+	layers.DHCPOptDomainName,
+	layers.DHCPOptDNS,
+	layers.DHCPOptDomainSearch,
+	layers.DHCPOptHostname,
+	layers.DHCPOptNetBIOSTCPNS,
+	layers.DHCPOptNetBIOSTCPScope,
+	layers.DHCPOptInterfaceMTU,
+	layers.DHCPOptClasslessStaticRoute,
+	layers.DHCPOptNTPServers,
+}
+
+func createDefaultDHCPReq(intfId uint32, onuId uint32) layers.DHCPv4 {
+	return layers.DHCPv4{
+		Operation:    layers.DHCPOpRequest,
+		HardwareType: layers.LinkTypeEthernet,
+		HardwareLen:  6,
+		HardwareOpts: 0,
+		Xid:          onuId,
+		ClientHWAddr: net.HardwareAddr{0x2e, 0x60, 0x70, 0x13, byte(intfId), byte(onuId)},
+	}
+}
+
+func createDefaultOpts() []layers.DHCPOption {
+	hostname := []byte("bbsim.onf.org")
+	opts := []layers.DHCPOption{}
+	opts = append(opts, layers.DHCPOption{
+		Type:   layers.DHCPOptHostname,
+		Data:   hostname,
+		Length: uint8(len(hostname)),
+	})
+
+	bytes := []byte{}
+	for _, option := range defaultParamsRequestList {
+		bytes = append(bytes, byte(option))
+	}
+
+	opts = append(opts, layers.DHCPOption{
+		Type:   layers.DHCPOptParamsRequest,
+		Data:   bytes,
+		Length: uint8(len(bytes)),
+	})
+	return opts
+}
+
+func createDHCPDisc(intfId uint32, onuId uint32) *layers.DHCPv4 {
+	dhcpLayer := createDefaultDHCPReq(intfId, onuId)
+	defaultOpts := createDefaultOpts()
+	dhcpLayer.Options = append([]layers.DHCPOption{layers.DHCPOption{
+		Type:   layers.DHCPOptMessageType,
+		Data:   []byte{byte(layers.DHCPMsgTypeDiscover)},
+		Length: 1,
+	}}, defaultOpts...)
+
+	return &dhcpLayer
+}
+
+func createDHCPReq(intfId uint32, onuId uint32) *layers.DHCPv4 {
+	dhcpLayer := createDefaultDHCPReq(intfId, onuId)
+	defaultOpts := createDefaultOpts()
+
+	dhcpLayer.Options = append(defaultOpts, layers.DHCPOption{
+		Type:   layers.DHCPOptMessageType,
+		Data:   []byte{byte(layers.DHCPMsgTypeRequest)},
+		Length: 1,
+	})
+
+	data := []byte{182, 21, 0, 128}
+	dhcpLayer.Options = append(dhcpLayer.Options, layers.DHCPOption{
+		Type:   layers.DHCPOptServerID,
+		Data:   data,
+		Length: uint8(len(data)),
+	})
+
+	data = []byte{0xcd, 0x28, 0xcb, 0xcc, 0x00, 0x01, 0x00, 0x01,
+		0x23, 0xed, 0x11, 0xec, 0x4e, 0xfc, 0xcd, 0x28, 0xcb, 0xcc}
+	dhcpLayer.Options = append(dhcpLayer.Options, layers.DHCPOption{
+		Type:   layers.DHCPOptClientID,
+		Data:   data,
+		Length: uint8(len(data)),
+	})
+
+	data = []byte{182, 21, 0, byte(onuId)}
+	dhcpLayer.Options = append(dhcpLayer.Options, layers.DHCPOption{
+		Type:   layers.DHCPOptRequestIP,
+		Data:   data,
+		Length: uint8(len(data)),
+	})
+	return &dhcpLayer
+}
+
+func serializeDHCPPacket(intfId uint32, onuId uint32, srcMac net.HardwareAddr, dhcp *layers.DHCPv4) ([]byte, error) {
+	buffer := gopacket.NewSerializeBuffer()
+	options := gopacket.SerializeOptions{
+		ComputeChecksums: true,
+		FixLengths:       true,
+	}
+
+	ethernetLayer := &layers.Ethernet{
+		SrcMAC:       srcMac,
+		DstMAC:       net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
+		EthernetType: layers.EthernetTypeIPv4,
+	}
+
+	ipLayer := &layers.IPv4{
+		Version:  4,
+		TOS:      0x10,
+		TTL:      128,
+		SrcIP:    []byte{0, 0, 0, 0},
+		DstIP:    []byte{255, 255, 255, 255},
+		Protocol: layers.IPProtocolUDP,
+	}
+
+	udpLayer := &layers.UDP{
+		SrcPort: 68,
+		DstPort: 67,
+	}
+
+	udpLayer.SetNetworkLayerForChecksum(ipLayer)
+	if err := gopacket.SerializeLayers(buffer, options, ethernetLayer, ipLayer, udpLayer, dhcp); err != nil {
+		return nil, err
+	}
+
+	bytes := buffer.Bytes()
+	return bytes, nil
+}
+
+func getDhcpLayer(pkt gopacket.Packet) (*layers.DHCPv4, error) {
+	layerDHCP := pkt.Layer(layers.LayerTypeDHCPv4)
+	dhcp, _ := layerDHCP.(*layers.DHCPv4)
+	if dhcp == nil {
+		return nil, errors.New("Failed-to-extract-DHCP-layer")
+	}
+	return dhcp, nil
+}
+
+func getDhcpMessageType(dhcp *layers.DHCPv4) (layers.DHCPMsgType, error) {
+	for _, option := range dhcp.Options {
+		if option.Type == layers.DHCPOptMessageType {
+			if reflect.DeepEqual(option.Data, []byte{byte(layers.DHCPMsgTypeOffer)}) {
+				return layers.DHCPMsgTypeOffer, nil
+			} else if reflect.DeepEqual(option.Data, []byte{byte(layers.DHCPMsgTypeAck)}) {
+				return layers.DHCPMsgTypeAck, nil
+			} else if reflect.DeepEqual(option.Data, []byte{byte(layers.DHCPMsgTypeRelease)}) {
+				return layers.DHCPMsgTypeRelease, nil
+			} else {
+				msg := fmt.Sprintf("This type %x is not supported", option.Data)
+				return 0, errors.New(msg)
+			}
+		}
+	}
+	return 0, errors.New("Failed to extract MsgType from dhcp")
+}
+
+func sendDHCPPktIn(msg bbsim.ByteMsg, stream openolt.Openolt_EnableIndicationServer) error {
+	// FIXME unify sendDHCPPktIn and sendEapolPktIn methods
+	gemid, err := omci.GetGemPortId(msg.IntfId, msg.OnuId)
+	if err != nil {
+		dhcpLogger.WithFields(log.Fields{
+			"OnuId":  msg.OnuId,
+			"IntfId": msg.IntfId,
+		}).Errorf("Can't retrieve GemPortId: %s", err)
+		return err
+	}
+	data := &openolt.Indication_PktInd{PktInd: &openolt.PacketIndication{
+		IntfType: "pon", IntfId: msg.IntfId, GemportId: uint32(gemid), Pkt: msg.Bytes,
+	}}
+
+	if err := stream.Send(&openolt.Indication{Data: data}); err != nil {
+		dhcpLogger.Errorf("Fail to send DHCP PktInd indication. %v", err)
+		return err
+	}
+	return nil
+}
+
+func sendDHCPDiscovery(ponPortId uint32, onuId uint32, serialNumber string, onuHwAddress net.HardwareAddr, cTag int, stream openolt.Openolt_EnableIndicationServer) error {
+	dhcp := createDHCPDisc(ponPortId, onuId)
+	pkt, err := serializeDHCPPacket(ponPortId, onuId, onuHwAddress, dhcp)
+	if err != nil {
+		dhcpLogger.Errorf("Cannot serializeDHCPPacket: %s", err)
+		return err
+	}
+	// NOTE I don't think we need to tag the packet
+	//taggedPkt, err := packetHandlers.PushSingleTag(cTag, pkt)
+
+	msg := bbsim.ByteMsg{
+		IntfId: ponPortId,
+		OnuId:  onuId,
+		Bytes:  pkt,
+	}
+
+	if err := sendDHCPPktIn(msg, stream); err != nil {
+		return err
+	}
+	dhcpLogger.WithFields(log.Fields{
+		"OnuId":  onuId,
+		"IntfId": ponPortId,
+		"OnuSn":  serialNumber,
+	}).Infof("DHCPDiscovery Sent")
+	return nil
+}
+
+func sendDHCPRequest(ponPortId uint32, onuId uint32, serialNumber string, onuHwAddress net.HardwareAddr, cTag int, stream openolt.Openolt_EnableIndicationServer) error {
+	dhcp := createDHCPReq(ponPortId, onuId)
+	pkt, err := serializeDHCPPacket(ponPortId, onuId, onuHwAddress, dhcp)
+
+	if err != nil {
+		dhcpLogger.Errorf("Cannot serializeDHCPPacket: %s", err)
+		return err
+	}
+	// NOTE I don't think we need to tag the packet
+	//taggedPkt, err := packetHandlers.PushSingleTag(cTag, pkt)
+
+	msg := bbsim.ByteMsg{
+		IntfId: ponPortId,
+		OnuId:  onuId,
+		Bytes:  pkt,
+	}
+
+	if err := sendDHCPPktIn(msg, stream); err != nil {
+		return err
+	}
+	dhcpLogger.WithFields(log.Fields{
+		"OnuId":  onuId,
+		"IntfId": ponPortId,
+		"OnuSn":  serialNumber,
+	}).Infof("DHCPDiscovery Sent")
+	return nil
+}
+
+func CreateDHCPClient(onuId uint32, ponPortId uint32, serialNumber string, onuHwAddress net.HardwareAddr, cTag int, onuStateMachine *fsm.FSM, stream openolt.Openolt_EnableIndicationServer, pktOutCh chan *bbsim.ByteMsg) {
+	// NOTE pckOutCh is channel to listen on for packets received by VOLTHA
+	// the OLT device will publish messages on that channel
+
+	dhcpLogger.WithFields(log.Fields{
+		"OnuId":  onuId,
+		"IntfId": ponPortId,
+		"OnuSn":  serialNumber,
+	}).Infof("DHCP State Machine starting")
+
+	defer dhcpLogger.WithFields(log.Fields{
+		"OnuId":  onuId,
+		"IntfId": ponPortId,
+		"OnuSn":  serialNumber,
+	}).Infof("DHCP State machine completed")
+
+	// Send DHCP Discovery packet
+	if err := sendDHCPDiscovery(ponPortId, onuId, serialNumber, onuHwAddress, cTag, stream); err != nil {
+		dhcpLogger.WithFields(log.Fields{
+			"OnuId":  onuId,
+			"IntfId": ponPortId,
+			"OnuSn":  serialNumber,
+		}).Errorf("Can't send DHCP Discovery: %s", err)
+		if err := onuStateMachine.Event("dhcp_failed"); err != nil {
+			dhcpLogger.WithFields(log.Fields{
+				"OnuId":  onuId,
+				"IntfId": ponPortId,
+				"OnuSn":  serialNumber,
+			}).Errorf("Error while transitioning ONU State %v", err)
+		}
+		return
+	}
+
+	if err := onuStateMachine.Event("dhcp_discovery_sent"); err != nil {
+		dhcpLogger.WithFields(log.Fields{
+			"OnuId":  onuId,
+			"IntfId": ponPortId,
+			"OnuSn":  serialNumber,
+		}).Errorf("Error while transitioning ONU State %v", err)
+	}
+
+	dhcpLogger.WithFields(log.Fields{
+		"OnuId":  onuId,
+		"IntfId": ponPortId,
+		"OnuSn":  serialNumber,
+	}).Infof("Listening on dhcpPktOutCh")
+
+	for msg := range pktOutCh {
+		dhcpLogger.Tracef("Received DHCP message %v", msg)
+
+		pkt := gopacket.NewPacket(msg.Bytes, layers.LayerTypeEthernet, gopacket.Default)
+		dhcpLayer, err := getDhcpLayer(pkt)
+		if err != nil {
+			dhcpLogger.WithFields(log.Fields{
+				"OnuId":  onuId,
+				"IntfId": ponPortId,
+				"OnuSn":  serialNumber,
+			}).Errorf("Can't get DHCP Layer from Packet: %v", err)
+			continue
+		}
+		dhcpMessageType, err := getDhcpMessageType(dhcpLayer)
+		if err != nil {
+			dhcpLogger.WithFields(log.Fields{
+				"OnuId":  onuId,
+				"IntfId": ponPortId,
+				"OnuSn":  serialNumber,
+			}).Errorf("Can't get DHCP Message Type from DHCP Layer: %v", err)
+			continue
+		}
+
+		if dhcpLayer.Operation == layers.DHCPOpReply {
+			if dhcpMessageType == layers.DHCPMsgTypeOffer {
+				if err := sendDHCPRequest(ponPortId, onuId, serialNumber, onuHwAddress, cTag, stream); err != nil {
+					dhcpLogger.WithFields(log.Fields{
+						"OnuId":  onuId,
+						"IntfId": ponPortId,
+						"OnuSn":  serialNumber,
+					}).Errorf("Can't send DHCP Request: %s", err)
+					if err := onuStateMachine.Event("dhcp_failed"); err != nil {
+						dhcpLogger.WithFields(log.Fields{
+							"OnuId":  onuId,
+							"IntfId": ponPortId,
+							"OnuSn":  serialNumber,
+						}).Errorf("Error while transitioning ONU State %v", err)
+					}
+					return
+				}
+				if err := onuStateMachine.Event("dhcp_request_sent"); err != nil {
+					dhcpLogger.WithFields(log.Fields{
+						"OnuId":  onuId,
+						"IntfId": ponPortId,
+						"OnuSn":  serialNumber,
+					}).Errorf("Error while transitioning ONU State %v", err)
+				}
+
+			} else if dhcpMessageType == layers.DHCPMsgTypeAck {
+				// NOTE once the ack is received we don't need to do anything but change the state
+				if err := onuStateMachine.Event("dhcp_ack_received"); err != nil {
+					dhcpLogger.WithFields(log.Fields{
+						"OnuId":  onuId,
+						"IntfId": ponPortId,
+						"OnuSn":  serialNumber,
+					}).Errorf("Error while transitioning ONU State %v", err)
+				}
+				return
+			}
+			// NOTE do we need to care about DHCPMsgTypeRelease??
+		} else {
+			dhcpLogger.WithFields(log.Fields{
+				"OnuId":  onuId,
+				"IntfId": ponPortId,
+				"OnuSn":  serialNumber,
+			}).Warnf("Unsupported DHCP Operation: %s", dhcpLayer.Operation.String())
+			continue
+		}
+	}
+}
diff --git a/internal/bbsim/responders/eapol/eapol.go b/internal/bbsim/responders/eapol/eapol.go
index d32999a..602a78e 100644
--- a/internal/bbsim/responders/eapol/eapol.go
+++ b/internal/bbsim/responders/eapol/eapol.go
@@ -19,15 +19,14 @@
 import (
 	"crypto/md5"
 	"errors"
-	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
 	"github.com/google/gopacket"
 	"github.com/google/gopacket/layers"
 	"github.com/looplab/fsm"
+	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
 	omci "github.com/opencord/omci-sim"
 	"github.com/opencord/voltha-protos/go/openolt"
 	log "github.com/sirupsen/logrus"
 	"net"
-	"sync"
 )
 
 var eapolLogger = log.WithFields(log.Fields{
@@ -36,25 +35,28 @@
 
 var eapolVersion uint8 = 1
 
-func CreateWPASupplicant(onuId uint32, ponPortId uint32, serialNumber *openolt.SerialNumber, onuStateMachine *fsm.FSM, stream openolt.Openolt_EnableIndicationServer, pktOutCh chan *bbsim.ByteMsg) {
+func CreateWPASupplicant(onuId uint32, ponPortId uint32, serialNumber string, onuStateMachine *fsm.FSM, stream openolt.Openolt_EnableIndicationServer, pktOutCh chan *bbsim.ByteMsg) {
 	// NOTE pckOutCh is channel to listen on for packets received by VOLTHA
 	// the OLT device will publish messages on that channel
 
 	eapolLogger.WithFields(log.Fields{
-		"OnuId": onuId,
+		"OnuId":  onuId,
 		"IntfId": ponPortId,
-		"OnuSn": serialNumber,
+		"OnuSn":  serialNumber,
 	}).Infof("EAPOL State Machine starting")
 
-	var wg sync.WaitGroup
-	wg.Add(1)
+	defer eapolLogger.WithFields(log.Fields{
+		"OnuId":  onuId,
+		"IntfId": ponPortId,
+		"OnuSn":  serialNumber,
+	}).Infof("EAPOL State machine completed")
 
 	if err := sendEapStart(onuId, ponPortId, serialNumber, stream); err != nil {
 		eapolLogger.WithFields(log.Fields{
-			"OnuId": onuId,
+			"OnuId":  onuId,
 			"IntfId": ponPortId,
-			"OnuSn": serialNumber,
-		}).Errorf("Can't retrieve GemPortId: %s", err)
+			"OnuSn":  serialNumber,
+		}).Errorf("Can't send EapStart Message: %s", err)
 		if err := onuStateMachine.Event("auth_failed"); err != nil {
 			eapolLogger.WithFields(log.Fields{
 				"OnuId":  onuId,
@@ -72,104 +74,92 @@
 		}).Errorf("Error while transitioning ONU State %v", err)
 	}
 
-	// NOTE do we really need a thread here?
-	go func() {
-		eapolLogger.WithFields(log.Fields{
-			"OnuId": onuId,
-			"IntfId": ponPortId,
-			"OnuSn": serialNumber,
-		}).Infof("Listening on eapolPktOutCh")
-		for msg := range pktOutCh {
-			recvpkt := gopacket.NewPacket(msg.Bytes, layers.LayerTypeEthernet, gopacket.Default)
-			eap, err := extractEAP(recvpkt)
-			if err != nil {
-				eapolLogger.Errorf("%s", err)
-			}
+	eapolLogger.WithFields(log.Fields{
+		"OnuId":  onuId,
+		"IntfId": ponPortId,
+		"OnuSn":  serialNumber,
+	}).Infof("Listening on eapolPktOutCh")
 
-			if eap.Code == layers.EAPCodeRequest && eap.Type == layers.EAPTypeIdentity {
-				reseap := createEAPIdentityResponse(eap.Id)
-				pkt := createEAPOLPkt(reseap, onuId, ponPortId)
-
-				msg := bbsim.ByteMsg{
-					IntfId: ponPortId,
-					OnuId: onuId,
-					Bytes:  pkt,
-				}
-
-				sendEapolPktIn(msg, stream)
-				eapolLogger.WithFields(log.Fields{
-					"OnuId": onuId,
-					"IntfId": ponPortId,
-					"OnuSn": serialNumber,
-				}).Infof("Sent EAPIdentityResponse packet")
-				if err := onuStateMachine.Event("eap_resonse_identity_sent"); err != nil {
-					eapolLogger.WithFields(log.Fields{
-						"OnuId":  onuId,
-						"IntfId": ponPortId,
-						"OnuSn":  serialNumber,
-					}).Errorf("Error while transitioning ONU State %v", err)
-				}
-
-			} else if eap.Code == layers.EAPCodeRequest && eap.Type == layers.EAPTypeOTP {
-				senddata := getMD5Data(eap)
-				senddata = append([]byte{0x10}, senddata...)
-				sendeap := createEAPChallengeResponse(eap.Id, senddata)
-				pkt := createEAPOLPkt(sendeap, onuId, ponPortId)
-
-				msg := bbsim.ByteMsg{
-					IntfId: ponPortId,
-					OnuId: onuId,
-					Bytes:  pkt,
-				}
-
-				sendEapolPktIn(msg, stream)
-				eapolLogger.WithFields(log.Fields{
-					"OnuId": onuId,
-					"IntfId": ponPortId,
-					"OnuSn": serialNumber,
-				}).Infof("Sent EAPChallengeResponse packet")
-				if err := onuStateMachine.Event("eap_resonse_challenge_sent"); err != nil {
-					eapolLogger.WithFields(log.Fields{
-						"OnuId":  onuId,
-						"IntfId": ponPortId,
-						"OnuSn":  serialNumber,
-					}).Errorf("Error while transitioning ONU State %v", err)
-				}
-			} else if eap.Code == layers.EAPCodeSuccess && eap.Type == layers.EAPTypeNone {
-				eapolLogger.WithFields(log.Fields{
-					"OnuId": onuId,
-					"IntfId": ponPortId,
-					"OnuSn": serialNumber,
-				}).Infof("Received EAPSuccess packet")
-				if err := onuStateMachine.Event("eap_resonse_success_received"); err != nil {
-					eapolLogger.WithFields(log.Fields{
-						"OnuId":  onuId,
-						"IntfId": ponPortId,
-						"OnuSn":  serialNumber,
-					}).Errorf("Error while transitioning ONU State %v", err)
-				}
-				wg.Done()
-			}
-
+	for msg := range pktOutCh {
+		recvpkt := gopacket.NewPacket(msg.Bytes, layers.LayerTypeEthernet, gopacket.Default)
+		eap, err := extractEAP(recvpkt)
+		if err != nil {
+			eapolLogger.Errorf("%s", err)
 		}
 
+		if eap.Code == layers.EAPCodeRequest && eap.Type == layers.EAPTypeIdentity {
+			reseap := createEAPIdentityResponse(eap.Id)
+			pkt := createEAPOLPkt(reseap, onuId, ponPortId)
 
-	}()
+			msg := bbsim.ByteMsg{
+				IntfId: ponPortId,
+				OnuId:  onuId,
+				Bytes:  pkt,
+			}
 
-	defer eapolLogger.WithFields(log.Fields{
-		"OnuId": onuId,
-		"IntfId": ponPortId,
-		"OnuSn": serialNumber,
-	}).Infof("EAPOL State machine completed")
+			sendEapolPktIn(msg, stream)
+			eapolLogger.WithFields(log.Fields{
+				"OnuId":  onuId,
+				"IntfId": ponPortId,
+				"OnuSn":  serialNumber,
+			}).Infof("Sent EAPIdentityResponse packet")
+			if err := onuStateMachine.Event("eap_response_identity_sent"); err != nil {
+				eapolLogger.WithFields(log.Fields{
+					"OnuId":  onuId,
+					"IntfId": ponPortId,
+					"OnuSn":  serialNumber,
+				}).Errorf("Error while transitioning ONU State %v", err)
+			}
 
-	wg.Wait()
+		} else if eap.Code == layers.EAPCodeRequest && eap.Type == layers.EAPTypeOTP {
+			senddata := getMD5Data(eap)
+			senddata = append([]byte{0x10}, senddata...)
+			sendeap := createEAPChallengeResponse(eap.Id, senddata)
+			pkt := createEAPOLPkt(sendeap, onuId, ponPortId)
+
+			msg := bbsim.ByteMsg{
+				IntfId: ponPortId,
+				OnuId:  onuId,
+				Bytes:  pkt,
+			}
+
+			sendEapolPktIn(msg, stream)
+			eapolLogger.WithFields(log.Fields{
+				"OnuId":  onuId,
+				"IntfId": ponPortId,
+				"OnuSn":  serialNumber,
+			}).Infof("Sent EAPChallengeResponse packet")
+			if err := onuStateMachine.Event("eap_response_challenge_sent"); err != nil {
+				eapolLogger.WithFields(log.Fields{
+					"OnuId":  onuId,
+					"IntfId": ponPortId,
+					"OnuSn":  serialNumber,
+				}).Errorf("Error while transitioning ONU State %v", err)
+			}
+		} else if eap.Code == layers.EAPCodeSuccess && eap.Type == layers.EAPTypeNone {
+			eapolLogger.WithFields(log.Fields{
+				"OnuId":  onuId,
+				"IntfId": ponPortId,
+				"OnuSn":  serialNumber,
+			}).Infof("Received EAPSuccess packet")
+			if err := onuStateMachine.Event("eap_response_success_received"); err != nil {
+				eapolLogger.WithFields(log.Fields{
+					"OnuId":  onuId,
+					"IntfId": ponPortId,
+					"OnuSn":  serialNumber,
+				}).Errorf("Error while transitioning ONU State %v", err)
+			}
+			return
+		}
+	}
 }
 
 func sendEapolPktIn(msg bbsim.ByteMsg, stream openolt.Openolt_EnableIndicationServer) {
+	// FIXME unify sendDHCPPktIn and sendEapolPktIn methods
 	gemid, err := omci.GetGemPortId(msg.IntfId, msg.OnuId)
 	if err != nil {
 		eapolLogger.WithFields(log.Fields{
-			"OnuId": msg.OnuId,
+			"OnuId":  msg.OnuId,
 			"IntfId": msg.IntfId,
 		}).Errorf("Can't retrieve GemPortId: %s", err)
 		return
@@ -198,9 +188,9 @@
 
 func createEAPChallengeResponse(eapId uint8, payload []byte) *layers.EAP {
 	eap := layers.EAP{
-		Code: layers.EAPCodeResponse,
-		Id: eapId,
-		Length: 22,
+		Code:     layers.EAPCodeResponse,
+		Id:       eapId,
+		Length:   22,
 		Type:     layers.EAPTypeOTP,
 		TypeData: payload,
 	}
@@ -226,7 +216,6 @@
 		EthernetType: layers.EthernetTypeEAPOL,
 	}
 
-
 	gopacket.SerializeLayers(buffer, options,
 		ethernetLayer,
 		&layers.EAPOL{Version: eapolVersion, Type: 0, Length: eap.Length},
@@ -246,7 +235,7 @@
 	return eap, nil
 }
 
-var sendEapStart = func(onuId uint32, ponPortId uint32, serialNumber *openolt.SerialNumber, stream openolt.Openolt_EnableIndicationServer) error  {
+var sendEapStart = func(onuId uint32, ponPortId uint32, serialNumber string, stream openolt.Openolt_EnableIndicationServer) error {
 
 	// send the packet (hacked together)
 	gemid, err := omci.GetGemPortId(ponPortId, onuId)
@@ -254,6 +243,7 @@
 		return err
 	}
 
+	// TODO use createEAPOLPkt
 	buffer := gopacket.NewSerializeBuffer()
 	options := gopacket.SerializeOptions{}
 
@@ -269,13 +259,14 @@
 	)
 
 	msg := buffer.Bytes()
+	// TODO end createEAPOLPkt
 
 	data := &openolt.Indication_PktInd{
 		PktInd: &openolt.PacketIndication{
-			IntfType: "pon",
-			IntfId: ponPortId,
+			IntfType:  "pon",
+			IntfId:    ponPortId,
 			GemportId: uint32(gemid),
-			Pkt: msg,
+			Pkt:       msg,
 		},
 	}
 	// end of hacked (move in an EAPOL state machine)
@@ -284,4 +275,4 @@
 		return err
 	}
 	return nil
-}
\ No newline at end of file
+}
diff --git a/internal/bbsim/responders/eapol/eapol_test.go b/internal/bbsim/responders/eapol/eapol_test.go
index 2d8a06c..9e37dae 100644
--- a/internal/bbsim/responders/eapol/eapol_test.go
+++ b/internal/bbsim/responders/eapol/eapol_test.go
@@ -17,8 +17,8 @@
 package eapol
 
 import (
-	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
 	"github.com/looplab/fsm"
+	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
 	"github.com/opencord/voltha-protos/go/openolt"
 	"google.golang.org/grpc"
 	"gotest.tools/assert"
@@ -29,7 +29,7 @@
 )
 
 var (
-	originalSendEapStart func(onuId uint32, ponPortId uint32, serialNumber *openolt.SerialNumber, stream openolt.Openolt_EnableIndicationServer) error
+	originalSendEapStart func(onuId uint32, ponPortId uint32, serialNumber string, stream openolt.Openolt_EnableIndicationServer) error
 )
 
 type fakeStream struct {
@@ -42,12 +42,11 @@
 	return nil
 }
 
-func setUp()  {
+func setUp() {
 	originalSendEapStart = sendEapStart
 }
 
-
-func tearDown()  {
+func tearDown() {
 	sendEapStart = originalSendEapStart
 }
 
@@ -63,12 +62,12 @@
 	// mocks
 	mockSendEapStartCalled := 0
 	mockSendEapStartArgs := struct {
-		onuId uint32
-		ponPortId uint32
+		onuId        uint32
+		ponPortId    uint32
 		serialNumber *openolt.SerialNumber
-		stream openolt.Openolt_EnableIndicationServer
+		stream       openolt.Openolt_EnableIndicationServer
 	}{}
-	mockSendEapStart := func(onuId uint32, ponPortId uint32, serialNumber *openolt.SerialNumber, stream openolt.Openolt_EnableIndicationServer) error {
+	mockSendEapStart := func(onuId uint32, ponPortId uint32, serialNumber string, stream openolt.Openolt_EnableIndicationServer) error {
 		mockSendEapStartCalled++
 		mockSendEapStartArgs.onuId = onuId
 		mockSendEapStartArgs.ponPortId = ponPortId
@@ -79,20 +78,17 @@
 	// params for the function under test
 	var onuId uint32 = 1
 	var ponPortId uint32 = 0
-	var serialNumber = new(openolt.SerialNumber)
-
-	serialNumber.VendorId = []byte("BBSM")
-	serialNumber.VendorSpecific = []byte{0, byte(0 % 256), byte(ponPortId), byte(onuId)}
+	var serialNumber string = "BBSM00000001"
 
 	eapolStateMachine := fsm.NewFSM(
-	"auth_started",
-	fsm.Events{
-		{Name: "eap_start_sent", Src: []string{"auth_started"}, Dst: "eap_start_sent"},
-		{Name: "eap_resonse_identity_sent", Src: []string{"eap_start_sent"}, Dst: "eap_resonse_identity_sent"},
-		{Name: "eap_resonse_challenge_sent", Src: []string{"eap_resonse_identity_sent"}, Dst: "eap_resonse_challenge_sent"},
-		{Name: "eap_resonse_success_received", Src: []string{"eap_resonse_challenge_sent"}, Dst: "eap_resonse_success_received"},
-	},
-	fsm.Callbacks{},
+		"auth_started",
+		fsm.Events{
+			{Name: "eap_start_sent", Src: []string{"auth_started"}, Dst: "eap_start_sent"},
+			{Name: "eap_response_identity_sent", Src: []string{"eap_start_sent"}, Dst: "eap_response_identity_sent"},
+			{Name: "eap_response_challenge_sent", Src: []string{"eap_response_identity_sent"}, Dst: "eap_response_challenge_sent"},
+			{Name: "eap_response_success_received", Src: []string{"eap_response_challenge_sent"}, Dst: "eap_response_success_received"},
+		},
+		fsm.Callbacks{},
 	)
 
 	pktOutCh := make(chan *bbsim.ByteMsg, 1024)
@@ -103,7 +99,7 @@
 	wg.Add(1)
 
 	go CreateWPASupplicant(onuId, ponPortId, serialNumber, eapolStateMachine, stream, pktOutCh)
-	go func(){
+	go func() {
 		time.Sleep(1 * time.Second)
 		close(pktOutCh)
 		wg.Done()
@@ -114,4 +110,4 @@
 	assert.Equal(t, mockSendEapStartCalled, 1)
 	assert.Equal(t, mockSendEapStartArgs.onuId, onuId)
 	assert.Equal(t, mockSendEapStartArgs.ponPortId, ponPortId)
-}
\ No newline at end of file
+}
diff --git a/internal/bbsim/types.go b/internal/bbsim/types.go
index 0da944a..76c23cd 100644
--- a/internal/bbsim/types.go
+++ b/internal/bbsim/types.go
@@ -19,10 +19,11 @@
 // General
 
 type CliOptions struct {
-	OltID 	 	 	int
-	NumNniPerOlt 	int
-	NumPonPerOlt 	int
-	NumOnuPerPon 	int
-	profileCpu		*string
+	OltID        int
+	NumNniPerOlt int
+	NumPonPerOlt int
+	NumOnuPerPon int
+	STag         int
+	CTagInit     int
+	profileCpu   *string
 }
-
diff --git a/internal/bbsim/types/types.go b/internal/bbsim/types/types.go
index 1a1788c..731ca58 100644
--- a/internal/bbsim/types/types.go
+++ b/internal/bbsim/types/types.go
@@ -16,8 +16,14 @@
 
 package types
 
+import "github.com/google/gopacket"
+
 type ByteMsg struct {
 	IntfId uint32
 	OnuId  uint32
-	Bytes   []byte
-}
\ No newline at end of file
+	Bytes  []byte
+}
+
+type PacketMsg struct {
+	Pkt gopacket.Packet
+}
diff --git a/internal/bbsimctl/commands/onu.go b/internal/bbsimctl/commands/onu.go
index 7d6da8e..6422f0a 100644
--- a/internal/bbsimctl/commands/onu.go
+++ b/internal/bbsimctl/commands/onu.go
@@ -29,7 +29,7 @@
 )
 
 const (
-	DEFAULT_ONU_DEVICE_HEADER_FORMAT = "table{{ .PonPortID }}\t{{ .ID }}\t{{ .SerialNumber }}\t{{ .OperState }}\t{{ .InternalState }}"
+	DEFAULT_ONU_DEVICE_HEADER_FORMAT = "table{{ .PonPortID }}\t{{ .ID }}\t{{ .SerialNumber }}\t{{ .STag }}\t{{ .CTag }}\t{{ .OperState }}\t{{ .InternalState }}"
 )
 
 type ONUOptions struct{}