[VOL-3610] implementing a fake DHCP server within BBSim

Change-Id: If291a0ca7f78909c3713ef0e6831e381304fc2c9
diff --git a/internal/bbsim/devices/nni.go b/internal/bbsim/devices/nni.go
index fe092e8..32fffaa 100644
--- a/internal/bbsim/devices/nni.go
+++ b/internal/bbsim/devices/nni.go
@@ -17,68 +17,42 @@
 package devices
 
 import (
-	"bytes"
 	"encoding/hex"
-	"os/exec"
-
 	"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"
+	"github.com/opencord/voltha-protos/v4/go/openolt"
 	log "github.com/sirupsen/logrus"
 )
 
-var (
-	nniLogger    = log.WithFields(log.Fields{"module": "NNI"})
-	dhcpServerIp = "192.168.254.1"
-)
-
-type Executor interface {
-	Command(name string, arg ...string) Runnable
-}
-
-type DefaultExecutor struct{}
-
-func (d DefaultExecutor) Command(name string, arg ...string) Runnable {
-	return exec.Command(name, arg...)
-}
-
-type Runnable interface {
-	Run() error
-}
-
-var executor = DefaultExecutor{}
+var nniLogger = log.WithFields(log.Fields{"module": "NNI"})
 
 type NniPort struct {
 	// BBSIM Internals
-	ID           uint32
-	nniVeth      string
-	upstreamVeth string
-	PacketCount  uint64
+	ID  uint32
+	Olt *OltDevice
 
 	// PON Attributes
-	OperState *fsm.FSM
-	Type      string
+	OperState   *fsm.FSM
+	Type        string
+	PacketCount uint64 // dummy value for the stats
 }
 
 func CreateNNI(olt *OltDevice) (NniPort, error) {
 	nniPort := NniPort{
-		ID:           uint32(0),
-		nniVeth:      "nni",
-		upstreamVeth: "upstream",
+		ID: uint32(0),
 		OperState: getOperStateFSM(func(e *fsm.Event) {
 			oltLogger.Debugf("Changing NNI OperState from %s to %s", e.Src, e.Dst)
 		}),
 		Type: "nni",
+		Olt:  olt,
 	}
-	_ = createNNIPair(executor, olt, &nniPort)
+
 	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 (n *NniPort) sendNniPacket(packet gopacket.Packet) error {
+// handleNniPacket will send a packet to a fake DHCP server implementation
+func (n *NniPort) handleNniPacket(packet gopacket.Packet) error {
 	isDhcp := packetHandlers.IsDhcpPacket(packet)
 	isLldp := packetHandlers.IsLldpPacket(packet)
 
@@ -90,152 +64,33 @@
 	}
 
 	if isDhcp {
-		packet, err := packetHandlers.PopDoubleTag(packet)
+
+		// get a response packet from the DHCP server
+		pkt, err := n.Olt.dhcpServer.HandleServerPacket(packet)
 		if err != nil {
 			nniLogger.WithFields(log.Fields{
-				"packet": packet,
-			}).Errorf("Can't remove double tags from packet: %v", err)
+				"SourcePkt": hex.EncodeToString(packet.Data()),
+				"Err":       err,
+			}).Error("DHCP Server can't handle packet")
 			return err
 		}
 
-		handle, err := getVethHandler(n.nniVeth)
-		if err != nil {
+		// send packetIndication to VOLTHA
+		data := &openolt.Indication_PktInd{PktInd: &openolt.PacketIndication{
+			IntfType: "nni",
+			IntfId:   n.ID,
+			Pkt:      pkt.Data()}}
+		if err := n.Olt.OpenoltStream.Send(&openolt.Indication{Data: data}); err != nil {
+			oltLogger.WithFields(log.Fields{
+				"IntfType": data.PktInd.IntfType,
+				"IntfId":   n.ID,
+				"Pkt":      hex.EncodeToString(pkt.Data()),
+			}).Errorf("Fail to send PktInd indication: %v", err)
 			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.WithFields(log.Fields{
-			"packet": hex.EncodeToString(packet.Data()),
-		}).Trace("Sent packet out of NNI")
 	} else if isLldp {
 		// TODO rework this when BBSim supports data-plane packets
 		nniLogger.Trace("Received LLDP Packet, ignoring it")
 	}
 	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(executor Executor, olt *OltDevice, nniPort *NniPort) error {
-
-	if err := executor.Command("ip", "link", "add", nniPort.nniVeth, "type", "veth", "peer", "name", nniPort.upstreamVeth).Run(); err != nil {
-		nniLogger.Errorf("Couldn't create veth pair between %s and %s", nniPort.nniVeth, nniPort.upstreamVeth)
-		return err
-	}
-
-	if err := setVethUp(executor, nniPort.nniVeth); err != nil {
-		return err
-	}
-
-	if err := setVethUp(executor, nniPort.upstreamVeth); err != nil {
-		return err
-	}
-
-	// TODO should be moved out of this function in case there are multiple NNI interfaces.
-	// Only one DHCP server should be running and listening on all NNI interfaces
-	if err := startDHCPServer(nniPort.upstreamVeth, dhcpServerIp); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// NewVethChan returns a new channel for receiving packets over the NNI interface
-func (n *NniPort) NewVethChan() (chan *types.PacketMsg, *pcap.Handle, error) {
-	ch, handle, err := listenOnVeth(n.nniVeth)
-	if err != nil {
-		return nil, nil, err
-	}
-	return ch, handle, err
-}
-
-// setVethUp is responsible to activate a virtual interface
-func setVethUp(executor Executor, vethName string) error {
-	if err := executor.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
-}
-
-var startDHCPServer = func(upstreamVeth string, dhcpServerIp string) error {
-	// TODO the DHCP server should support multiple interfaces
-	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(executor, 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, "-4")
-	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
-}
-
-var listenOnVeth = func(vethName string) (chan *types.PacketMsg, *pcap.Handle, error) {
-
-	handle, err := getVethHandler(vethName)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	channel := make(chan *types.PacketMsg, 1024)
-
-	go func() {
-		nniLogger.Info("Start listening on NNI for packets")
-		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
-		}
-		nniLogger.Info("Stop listening on NNI for packets")
-	}()
-
-	return channel, handle, nil
-}
diff --git a/internal/bbsim/devices/nni_test.go b/internal/bbsim/devices/nni_test.go
index 9ee945e..4f93bb8 100644
--- a/internal/bbsim/devices/nni_test.go
+++ b/internal/bbsim/devices/nni_test.go
@@ -19,88 +19,82 @@
 
 import (
 	"errors"
-	"github.com/google/gopacket/pcap"
+	"github.com/google/gopacket"
+	"github.com/google/gopacket/layers"
+	"github.com/opencord/voltha-protos/v4/go/openolt"
+	"github.com/stretchr/testify/assert"
 	"testing"
-
-	"github.com/opencord/bbsim/internal/bbsim/types"
-	"gotest.tools/assert"
 )
 
-func TestSetVethUpSuccess(t *testing.T) {
-	spy := &ExecutorSpy{
-		Calls: make(map[int][]string),
+func TestCreateNNI(t *testing.T) {
+	olt := OltDevice{
+		ID: 0,
 	}
-	err := setVethUp(spy, "test_veth")
-	assert.Equal(t, spy.CommandCallCount, 1)
-	assert.Equal(t, spy.Calls[1][2], "test_veth")
-	assert.Equal(t, err, nil)
+	nni, err := CreateNNI(&olt)
+
+	assert.Nil(t, err)
+	assert.Equal(t, "nni", nni.Type)
+	assert.Equal(t, uint32(0), nni.ID)
+	assert.Equal(t, "down", nni.OperState.Current())
 }
 
-func TestSetVethUpFail(t *testing.T) {
-	spy := &ExecutorSpy{
-		failRun: true,
-		Calls:   make(map[int][]string),
+func TestSendNniPacket(t *testing.T) {
+
+	stream := &mockStream{
+		CallCount: 0,
+		Calls:     make(map[int]*openolt.Indication),
+		fail:      false,
+		channel:   make(chan int, 10),
 	}
-	err := setVethUp(spy, "test_veth")
-	assert.Equal(t, spy.CommandCallCount, 1)
-	assert.Equal(t, err.Error(), "fake-error")
+
+	dhcpServer := &mockDhcpServer{
+		callCount: 0,
+		fail:      false,
+	}
+
+	nni := NniPort{
+		Olt: &OltDevice{
+			OpenoltStream: stream,
+			dhcpServer:    dhcpServer,
+		},
+		ID: 12,
+	}
+
+	// the DHCP server is mocked, so we don't really care about the packet we send in
+	pkt := createTestDhcpPacket(t)
+	err := nni.handleNniPacket(pkt)
+	assert.Nil(t, err)
+	assert.Equal(t, stream.CallCount, 1)
+	indication := stream.Calls[1].GetPktInd()
+	assert.Equal(t, "nni", indication.IntfType)
+	assert.Equal(t, nni.ID, indication.IntfId)
+	assert.Equal(t, pkt.Data(), indication.Pkt)
 }
 
-func TestCreateNNIPair(t *testing.T) {
-
-	startDHCPServerCalled := false
-	_startDHCPServer := startDHCPServer
-	defer func() { startDHCPServer = _startDHCPServer }()
-	startDHCPServer = func(upstreamVeth string, dhcpServerIp string) error {
-		startDHCPServerCalled = true
-		return nil
-	}
-
-	listenOnVethCalled := false
-	_listenOnVeth := listenOnVeth
-	defer func() { listenOnVeth = _listenOnVeth }()
-	listenOnVeth = func(vethName string) (chan *types.PacketMsg, *pcap.Handle, error) {
-		listenOnVethCalled = true
-		return make(chan *types.PacketMsg, 1), nil, nil
-	}
-	spy := &ExecutorSpy{
-		failRun: false,
-		Calls:   make(map[int][]string),
-	}
-
-	olt := OltDevice{}
-	nni := NniPort{}
-
-	err := createNNIPair(spy, &olt, &nni)
-	olt.nniPktInChannel, olt.nniHandle, _ = nni.NewVethChan()
-
-	assert.Equal(t, spy.CommandCallCount, 3)
-	assert.Equal(t, startDHCPServerCalled, true)
-	assert.Equal(t, listenOnVethCalled, true)
-	assert.Equal(t, err, nil)
-	assert.Assert(t, olt.nniPktInChannel != nil)
+type mockDhcpServer struct {
+	callCount int
+	fail      bool
 }
 
-type ExecutorSpy struct {
-	failRun bool
-
-	CommandCallCount int
-	RunCallCount     int
-	Calls            map[int][]string
-}
-
-func (s *ExecutorSpy) Command(name string, arg ...string) Runnable {
-	s.CommandCallCount++
-
-	s.Calls[s.CommandCallCount] = arg
-
-	return s
-}
-
-func (s *ExecutorSpy) Run() error {
-	s.RunCallCount++
-	if s.failRun {
-		return errors.New("fake-error")
+// being a Mock I just return the same packet I got
+func (s mockDhcpServer) HandleServerPacket(pkt gopacket.Packet) (gopacket.Packet, error) {
+	if s.fail {
+		return nil, errors.New("mocked-error")
 	}
-	return nil
+	return pkt, nil
+}
+
+func createTestDhcpPacket(t *testing.T) gopacket.Packet {
+	dhcp := &layers.DHCPv4{
+		Operation: layers.DHCPOpRequest,
+	}
+
+	buffer := gopacket.NewSerializeBuffer()
+	opts := gopacket.SerializeOptions{FixLengths: true}
+	err := gopacket.SerializeLayers(buffer, opts, dhcp)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return gopacket.NewPacket(buffer.Bytes(), layers.LayerTypeDHCPv4, gopacket.DecodeOptions{})
 }
diff --git a/internal/bbsim/devices/olt.go b/internal/bbsim/devices/olt.go
index c3e0866..beeffce 100644
--- a/internal/bbsim/devices/olt.go
+++ b/internal/bbsim/devices/olt.go
@@ -20,6 +20,7 @@
 	"context"
 	"encoding/hex"
 	"fmt"
+	"github.com/opencord/bbsim/internal/bbsim/responders/dhcp"
 	"github.com/opencord/voltha-protos/v4/go/ext/config"
 	"net"
 	"sync"
@@ -27,10 +28,8 @@
 
 	"github.com/google/gopacket"
 	"github.com/google/gopacket/layers"
-	"github.com/google/gopacket/pcap"
 	"github.com/looplab/fsm"
 	"github.com/opencord/bbsim/internal/bbsim/packetHandlers"
-	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
 	"github.com/opencord/bbsim/internal/common"
 	omcisim "github.com/opencord/omci-sim"
 	common_protos "github.com/opencord/voltha-protos/v4/go/common"
@@ -58,8 +57,7 @@
 	NumOnuPerPon         int
 	InternalState        *fsm.FSM
 	channel              chan Message
-	nniPktInChannel      chan *bbsim.PacketMsg // packets coming in from the NNI and going to VOLTHA
-	nniHandle            *pcap.Handle          // handle on the NNI interface, close it when shutting down the NNI channel
+	dhcpServer           dhcp.DHCPServerIf
 	Flows                map[FlowKey]openolt.Flow
 	Delay                int
 	ControlledActivation mode
@@ -111,6 +109,7 @@
 		enablePerf:        options.BBSim.EnablePerf,
 		PublishEvents:     options.BBSim.Events,
 		PortStatsInterval: options.Olt.PortStatsInterval,
+		dhcpServer:        dhcp.NewDHCPServer(),
 	}
 
 	if val, ok := ControlledActivationModes[options.BBSim.ControlledActivation]; ok {
@@ -235,25 +234,12 @@
 	// create new channel for processOltMessages Go routine
 	o.channel = make(chan Message)
 
-	o.nniPktInChannel = make(chan *bbsim.PacketMsg, 1024)
 	// FIXME we are assuming we have only one NNI
 	if o.Nnis[0] != nil {
 		// NOTE we want to make sure the state is down when we initialize the OLT,
 		// the NNI may be in a bad state after a disable/reboot as we are not disabling it for
 		// in-band management
 		o.Nnis[0].OperState.SetState("down")
-		ch, handle, err := o.Nnis[0].NewVethChan()
-		if err == nil {
-			oltLogger.WithFields(log.Fields{
-				"Type":      o.Nnis[0].Type,
-				"IntfId":    o.Nnis[0].ID,
-				"OperState": o.Nnis[0].OperState.Current(),
-			}).Info("NNI Channel created")
-			o.nniPktInChannel = ch
-			o.nniHandle = handle
-		} else {
-			oltLogger.Errorf("Error getting NNI channel: %v", err)
-		}
 	}
 }
 
@@ -319,9 +305,7 @@
 
 	// terminate the OLT's processOltMessages go routine
 	close(o.channel)
-	// terminate the OLT's processNniPacketIns go routine
-	go o.nniHandle.Close()
-	close(o.nniPktInChannel)
+
 	o.enableContextCancel()
 
 	time.Sleep(time.Duration(rebootDelay) * time.Second)
@@ -398,7 +382,6 @@
 
 	// create Go routine to process all OLT events
 	go o.processOltMessages(o.enableContext, stream, &wg)
-	go o.processNniPacketIns(o.enableContext, stream, &wg)
 
 	// enable the OLT
 	oltMsg := Message{
@@ -753,82 +736,6 @@
 	oltLogger.Warn("Stopped handling OLT Indication Channel")
 }
 
-// processNniPacketIns handles messages received over the NNI interface
-func (o *OltDevice) processNniPacketIns(ctx context.Context, stream openolt.Openolt_EnableIndicationServer, wg *sync.WaitGroup) {
-	oltLogger.WithFields(log.Fields{
-		"nniChannel": o.nniPktInChannel,
-	}).Debug("Started Processing Packets arriving from the NNI")
-	nniId := o.Nnis[0].ID // FIXME we are assuming we have only one NNI
-
-	ch := o.nniPktInChannel
-
-loop:
-	for {
-		select {
-		case <-ctx.Done():
-			oltLogger.Debug("NNI Indication processing canceled via context")
-			break loop
-		case message, ok := <-ch:
-			if !ok || ctx.Err() != nil {
-				oltLogger.Debug("NNI Indication processing canceled via channel closed")
-				break loop
-			}
-			oltLogger.Tracef("Received packets on NNI Channel")
-
-			onuMac, err := packetHandlers.GetDstMacAddressFromPacket(message.Pkt)
-
-			if err != nil {
-				log.WithFields(log.Fields{
-					"IntfType": "nni",
-					"IntfId":   nniId,
-					"Pkt":      message.Pkt.Data(),
-				}).Error("Can't find Dst MacAddress in packet")
-				return
-			}
-
-			s, err := o.FindServiceByMacAddress(onuMac)
-			if err != nil {
-				log.WithFields(log.Fields{
-					"IntfType":   "nni",
-					"IntfId":     nniId,
-					"Pkt":        message.Pkt.Data(),
-					"MacAddress": onuMac.String(),
-				}).Error("Can't find ONU with MacAddress")
-				return
-			}
-
-			service := s.(*Service)
-
-			doubleTaggedPkt, err := packetHandlers.PushDoubleTag(service.STag, service.CTag, message.Pkt, service.UsPonCTagPriority)
-			if err != nil {
-				log.Error("Fail to add double tag to packet")
-			}
-
-			data := &openolt.Indication_PktInd{PktInd: &openolt.PacketIndication{
-				IntfType: "nni",
-				IntfId:   nniId,
-				Pkt:      doubleTaggedPkt.Data()}}
-			if err := stream.Send(&openolt.Indication{Data: data}); err != nil {
-				oltLogger.WithFields(log.Fields{
-					"IntfType": data.PktInd.IntfType,
-					"IntfId":   nniId,
-					"Pkt":      doubleTaggedPkt.Data(),
-				}).Errorf("Fail to send PktInd indication: %v", err)
-			}
-			oltLogger.WithFields(log.Fields{
-				"IntfType": data.PktInd.IntfType,
-				"IntfId":   nniId,
-				"Pkt":      hex.EncodeToString(doubleTaggedPkt.Data()),
-				"OnuSn":    service.Onu.Sn(),
-			}).Trace("Sent PktInd indication (from NNI to VOLTHA)")
-		}
-	}
-	wg.Done()
-	oltLogger.WithFields(log.Fields{
-		"nniChannel": o.nniPktInChannel,
-	}).Warn("Stopped handling NNI Channel")
-}
-
 // returns an ONU with a given Serial Number
 func (o *OltDevice) FindOnuBySn(serialNumber string) (*Onu, error) {
 	// TODO this function can be a performance bottleneck when we have many ONUs,
@@ -1405,8 +1312,11 @@
 func (o *OltDevice) UplinkPacketOut(context context.Context, packet *openolt.UplinkPacket) (*openolt.Empty, error) {
 	pkt := gopacket.NewPacket(packet.Pkt, layers.LayerTypeEthernet, gopacket.Default)
 
-	_ = o.Nnis[0].sendNniPacket(pkt) // FIXME we are assuming we have only one NNI
-	// NOTE should we return an error if sendNniPakcet fails?
+	err := o.Nnis[0].handleNniPacket(pkt) // FIXME we are assuming we have only one NNI
+
+	if err != nil {
+		return nil, err
+	}
 	return new(openolt.Empty), nil
 }
 
diff --git a/internal/bbsim/packetHandlers/filters.go b/internal/bbsim/packetHandlers/filters.go
index c46f87d..a930b1e 100644
--- a/internal/bbsim/packetHandlers/filters.go
+++ b/internal/bbsim/packetHandlers/filters.go
@@ -44,16 +44,14 @@
 	return false
 }
 
-// return true if the packet is coming in the OLT from the NNI port
-// it uses the ack to check if the source is the one we assigned to the
-// dhcp server
+// return true if the packet is coming in the OLT from the DHCP Server
+// given that we only check DHCP packets we can use the Operation
+// Request are outgoing (toward the server)
+// Replies are incoming (toward the OLT)
 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("192.168.254.1")) {
+	layerDHCP := packet.Layer(layers.LayerTypeDHCPv4)
+	if dhcp, ok := layerDHCP.(*layers.DHCPv4); ok {
+		if dhcp.Operation == layers.DHCPOpReply {
 			return true
 		}
 	}
diff --git a/internal/bbsim/packetHandlers/filters_test.go b/internal/bbsim/packetHandlers/filters_test.go
index c182e82..5be7db2 100644
--- a/internal/bbsim/packetHandlers/filters_test.go
+++ b/internal/bbsim/packetHandlers/filters_test.go
@@ -131,9 +131,8 @@
 }
 
 func Test_IsIncomingPacket_True(t *testing.T) {
-	eth := &layers.IPv4{
-		SrcIP: net.ParseIP("192.168.254.1"),
-		DstIP: net.ParseIP("182.21.0.122"),
+	eth := &layers.DHCPv4{
+		Operation: layers.DHCPOpReply,
 	}
 
 	buffer := gopacket.NewSerializeBuffer()
@@ -143,16 +142,15 @@
 		t.Fatal(err)
 	}
 
-	ethernetPkt := gopacket.NewPacket(buffer.Bytes(), layers.LayerTypeIPv4, gopacket.DecodeOptions{})
+	ethernetPkt := gopacket.NewPacket(buffer.Bytes(), layers.LayerTypeDHCPv4, gopacket.DecodeOptions{})
 
 	res := packetHandlers.IsIncomingPacket(ethernetPkt)
 	assert.Equal(t, res, true)
 }
 
 func Test_IsIncomingPacket_False(t *testing.T) {
-	eth := &layers.IPv4{
-		SrcIP: net.ParseIP("182.21.0.122"),
-		DstIP: net.ParseIP("192.168.254.1"),
+	eth := &layers.DHCPv4{
+		Operation: layers.DHCPOpRequest,
 	}
 
 	buffer := gopacket.NewSerializeBuffer()
@@ -162,7 +160,7 @@
 		t.Fatal(err)
 	}
 
-	ethernetPkt := gopacket.NewPacket(buffer.Bytes(), layers.LayerTypeIPv4, gopacket.DecodeOptions{})
+	ethernetPkt := gopacket.NewPacket(buffer.Bytes(), layers.LayerTypeDHCPv4, gopacket.DecodeOptions{})
 
 	res := packetHandlers.IsIncomingPacket(ethernetPkt)
 	assert.Equal(t, res, false)
diff --git a/internal/bbsim/packetHandlers/packet_tags.go b/internal/bbsim/packetHandlers/packet_tags.go
index 1ea7be7..90186a8 100644
--- a/internal/bbsim/packetHandlers/packet_tags.go
+++ b/internal/bbsim/packetHandlers/packet_tags.go
@@ -144,3 +144,21 @@
 	}
 	return dot1q.Priority, nil
 }
+
+// godet inner and outer tag from a packet
+// TODO unit test
+func GetTagsFromPacket(pkt gopacket.Packet) (uint16, uint16, error) {
+	sTag, err := GetVlanTag(pkt)
+	if err != nil {
+		return 0, 0, err
+	}
+	singleTagPkt, err := PopSingleTag(pkt)
+	if err != nil {
+		return 0, 0, err
+	}
+	cTag, err := GetVlanTag(singleTagPkt)
+	if err != nil {
+		return 0, 0, err
+	}
+	return sTag, cTag, nil
+}
diff --git a/internal/bbsim/responders/dhcp/dhcp.go b/internal/bbsim/responders/dhcp/dhcp.go
index f3c7e72..850d160 100644
--- a/internal/bbsim/responders/dhcp/dhcp.go
+++ b/internal/bbsim/responders/dhcp/dhcp.go
@@ -158,7 +158,7 @@
 	return &dhcpLayer
 }
 
-func serializeDHCPPacket(intfId uint32, onuId uint32, cTag int, srcMac net.HardwareAddr, dhcp *layers.DHCPv4, pbit uint8) ([]byte, error) {
+func serializeDHCPPacket(cTag int, srcMac net.HardwareAddr, dhcp *layers.DHCPv4, pbit uint8) (gopacket.Packet, error) {
 	buffer := gopacket.NewSerializeBuffer()
 
 	options := gopacket.SerializeOptions{
@@ -200,7 +200,7 @@
 		return nil, err
 	}
 
-	return gopacket.Payload(taggedPkt.Data()), nil
+	return taggedPkt, nil
 }
 
 func GetDhcpLayer(pkt gopacket.Packet) (*layers.DHCPv4, error) {
@@ -277,7 +277,7 @@
 	cTag int, gemPortId uint32, onuStateMachine *fsm.FSM, onuHwAddress net.HardwareAddr,
 	offeredIp net.IP, pbit uint8, stream bbsim.Stream) error {
 	dhcp := createDHCPReq(ponPortId, onuId, onuHwAddress, offeredIp, gemPortId)
-	pkt, err := serializeDHCPPacket(ponPortId, onuId, cTag, onuHwAddress, dhcp, pbit)
+	pkt, err := serializeDHCPPacket(cTag, onuHwAddress, dhcp, pbit)
 
 	if err != nil {
 		dhcpLogger.WithFields(log.Fields{
@@ -297,7 +297,7 @@
 	msg := bbsim.ByteMsg{
 		IntfId: ponPortId,
 		OnuId:  onuId,
-		Bytes:  pkt,
+		Bytes:  pkt.Data(),
 	}
 
 	if err := sendDHCPPktIn(msg, portNo, gemPortId, stream); err != nil {
@@ -339,7 +339,7 @@
 	pbit uint8, stream bbsim.Stream) error {
 
 	dhcp := createDHCPDisc(ponPortId, onuId, gemPortId, onuHwAddress)
-	pkt, err := serializeDHCPPacket(ponPortId, onuId, cTag, onuHwAddress, dhcp, pbit)
+	pkt, err := serializeDHCPPacket(cTag, onuHwAddress, dhcp, pbit)
 
 	if err != nil {
 		dhcpLogger.WithFields(log.Fields{
@@ -359,7 +359,7 @@
 	msg := bbsim.ByteMsg{
 		IntfId: ponPortId,
 		OnuId:  onuId,
-		Bytes:  pkt,
+		Bytes:  pkt.Data(),
 	}
 
 	if err := sendDHCPPktIn(msg, portNo, gemPortId, stream); err != nil {
@@ -574,6 +574,7 @@
 				"Type":   dhcpType,
 				"error":  err,
 			}).Error("Failed to send DHCP packet out of the NNI Port")
+			return err
 		}
 		dhcpLogger.WithFields(log.Fields{
 			"OnuId":  onuId,
diff --git a/internal/bbsim/responders/dhcp/dhcp_server.go b/internal/bbsim/responders/dhcp/dhcp_server.go
new file mode 100644
index 0000000..7e7f361
--- /dev/null
+++ b/internal/bbsim/responders/dhcp/dhcp_server.go
@@ -0,0 +1,322 @@
+/*
+ * 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 (
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"github.com/google/gopacket"
+	"github.com/google/gopacket/layers"
+	"github.com/opencord/bbsim/internal/bbsim/packetHandlers"
+	log "github.com/sirupsen/logrus"
+	"net"
+)
+
+type DHCPServerIf interface {
+	HandleServerPacket(pkt gopacket.Packet) (gopacket.Packet, error)
+}
+
+type DHCPServer struct {
+	DHCPServerMacAddress net.HardwareAddr
+}
+
+func NewDHCPServer() *DHCPServer {
+	return &DHCPServer{
+		// NOTE we may need to make this configurable in case we'll need multiple servers
+		DHCPServerMacAddress: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff},
+	}
+}
+
+func (s *DHCPServer) getClientMacAddress(pkt gopacket.Packet) (net.HardwareAddr, error) {
+	dhcpLayer, err := GetDhcpLayer(pkt)
+	if err != nil {
+		return nil, err
+	}
+	return dhcpLayer.ClientHWAddr, nil
+}
+
+func (s *DHCPServer) getTxId(pkt gopacket.Packet) (uint32, error) {
+	dhcpLayer, err := GetDhcpLayer(pkt)
+	if err != nil {
+		return 0, err
+	}
+	return dhcpLayer.Xid, nil
+}
+
+func (s *DHCPServer) getPacketHostName(pkt gopacket.Packet) ([]byte, error) {
+	dhcpLayer, err := GetDhcpLayer(pkt)
+	if err != nil {
+		return nil, err
+	}
+	for _, option := range dhcpLayer.Options {
+		if option.Type == layers.DHCPOptHostname {
+			return option.Data, nil
+		}
+	}
+	return nil, errors.New("hostname-not-found")
+}
+
+func (s *DHCPServer) getOption82(pkt gopacket.Packet) ([]byte, error) {
+	dhcpLayer, err := GetDhcpLayer(pkt)
+	if err != nil {
+		return nil, err
+	}
+	for _, option := range dhcpLayer.Options {
+		if option.Type == 82 {
+			return option.Data, nil
+		}
+	}
+	log.WithFields(log.Fields{
+		"pkt": hex.EncodeToString(pkt.Data()),
+	}).Debug("option82-not-found")
+	return []byte{}, nil
+}
+
+func (s *DHCPServer) createDefaultDhcpReply(xid uint32, mac net.HardwareAddr) layers.DHCPv4 {
+	clientIp := s.createIpFromMacAddress(mac)
+	return layers.DHCPv4{
+		Operation:    layers.DHCPOpReply,
+		HardwareType: layers.LinkTypeEthernet,
+		HardwareLen:  6,
+		HardwareOpts: 0,
+		Xid:          xid,
+		ClientHWAddr: mac,
+		ClientIP:     clientIp,
+		YourClientIP: clientIp,
+	}
+}
+
+func (s *DHCPServer) createIpFromMacAddress(mac net.HardwareAddr) net.IP {
+	ip := []byte{}
+	for i := 2; i < 6; i++ {
+		ip = append(ip, mac[i])
+	}
+	return net.IPv4(10+ip[0], ip[1], ip[2], ip[3])
+}
+
+func (s *DHCPServer) serializeServerDHCPPacket(clientMac net.HardwareAddr, dhcpLayer *layers.DHCPv4) (gopacket.Packet, error) {
+	buffer := gopacket.NewSerializeBuffer()
+
+	options := gopacket.SerializeOptions{
+		ComputeChecksums: true,
+		FixLengths:       true,
+	}
+
+	ethernetLayer := &layers.Ethernet{
+		SrcMAC:       s.DHCPServerMacAddress,
+		DstMAC:       clientMac,
+		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: 67,
+		DstPort: 68,
+	}
+
+	_ = udpLayer.SetNetworkLayerForChecksum(ipLayer)
+	if err := gopacket.SerializeLayers(buffer, options, ethernetLayer, ipLayer, udpLayer, dhcpLayer); err != nil {
+		dhcpLogger.Error("SerializeLayers")
+		return nil, err
+	}
+
+	return gopacket.NewPacket(buffer.Bytes(), layers.LayerTypeEthernet, gopacket.Default), nil
+
+}
+
+func (s *DHCPServer) getDefaultDhcpServerOptions(hostname []byte, option82 []byte) []layers.DHCPOption {
+	defaultOpts := []layers.DHCPOption{}
+	defaultOpts = append(defaultOpts, layers.DHCPOption{
+		Type:   layers.DHCPOptHostname,
+		Data:   hostname,
+		Length: uint8(len(hostname)),
+	})
+
+	defaultOpts = append(defaultOpts, layers.DHCPOption{
+		Type:   82,
+		Data:   option82,
+		Length: uint8(len(option82)),
+	})
+
+	return defaultOpts
+}
+
+// get a Discover packet an return a valid Offer
+func (s *DHCPServer) handleDiscover(pkt gopacket.Packet) (gopacket.Packet, error) {
+
+	sTag, cTag, err := packetHandlers.GetTagsFromPacket(pkt)
+	if err != nil {
+		return nil, err
+	}
+
+	clientMac, err := s.getClientMacAddress(pkt)
+	if err != nil {
+		return nil, err
+	}
+
+	txId, err := s.getTxId(pkt)
+	if err != nil {
+		return nil, err
+	}
+
+	hostname, err := s.getPacketHostName(pkt)
+	if err != nil {
+		return nil, err
+	}
+
+	option82, err := s.getOption82(pkt)
+	if err != nil {
+		return nil, err
+	}
+
+	dhcpLogger.WithFields(log.Fields{
+		"sTag":      sTag,
+		"cTag":      cTag,
+		"clientMac": clientMac,
+		"txId":      txId,
+		"hostname":  string(hostname),
+		"option82":  string(option82),
+	}).Debug("Handling DHCP Discovery packet")
+
+	dhcpLayer := s.createDefaultDhcpReply(txId, clientMac)
+	defaultOpts := s.getDefaultDhcpServerOptions(hostname, option82)
+
+	dhcpLayer.Options = append([]layers.DHCPOption{{
+		Type:   layers.DHCPOptMessageType,
+		Data:   []byte{byte(layers.DHCPMsgTypeOffer)},
+		Length: 1,
+	}}, defaultOpts...)
+
+	data := []byte{01}
+	data = append(data, clientMac...)
+	dhcpLayer.Options = append(dhcpLayer.Options, layers.DHCPOption{
+		Type:   layers.DHCPOptClientID,
+		Data:   data,
+		Length: uint8(len(data)),
+	})
+
+	// serialize the packet
+	responsePkt, err := s.serializeServerDHCPPacket(clientMac, &dhcpLayer)
+	if err != nil {
+		return nil, err
+	}
+
+	taggedResponsePkt, err := packetHandlers.PushDoubleTag(int(sTag), int(cTag), responsePkt, 0)
+	if err != nil {
+		return nil, err
+	}
+	return taggedResponsePkt, nil
+}
+
+func (s *DHCPServer) handleRequest(pkt gopacket.Packet) (gopacket.Packet, error) {
+	sTag, cTag, err := packetHandlers.GetTagsFromPacket(pkt)
+	if err != nil {
+		return nil, err
+	}
+
+	clientMac, err := s.getClientMacAddress(pkt)
+	if err != nil {
+		return nil, err
+	}
+
+	txId, err := s.getTxId(pkt)
+	if err != nil {
+		return nil, err
+	}
+
+	hostname, err := s.getPacketHostName(pkt)
+	if err != nil {
+		return nil, err
+	}
+
+	option82, err := s.getOption82(pkt)
+	if err != nil {
+		return nil, err
+	}
+
+	dhcpLogger.WithFields(log.Fields{
+		"sTag":      sTag,
+		"cTag":      cTag,
+		"clientMac": clientMac,
+		"txId":      txId,
+		"hostname":  string(hostname),
+		"option82":  string(option82),
+	}).Debug("Handling DHCP Request packet")
+
+	dhcpLayer := s.createDefaultDhcpReply(txId, clientMac)
+	defaultOpts := s.getDefaultDhcpServerOptions(hostname, option82)
+
+	dhcpLayer.Options = append([]layers.DHCPOption{{
+		Type:   layers.DHCPOptMessageType,
+		Data:   []byte{byte(layers.DHCPMsgTypeAck)},
+		Length: 1,
+	}}, defaultOpts...)
+
+	// TODO can we move this in getDefaultDhcpServerOptions?
+	data := []byte{01}
+	data = append(data, clientMac...)
+	dhcpLayer.Options = append(dhcpLayer.Options, layers.DHCPOption{
+		Type:   layers.DHCPOptClientID,
+		Data:   data,
+		Length: uint8(len(data)),
+	})
+
+	// serialize the packet
+	responsePkt, err := s.serializeServerDHCPPacket(clientMac, &dhcpLayer)
+	if err != nil {
+		return nil, err
+	}
+
+	taggedResponsePkt, err := packetHandlers.PushDoubleTag(int(sTag), int(cTag), responsePkt, 0)
+	if err != nil {
+		return nil, err
+	}
+	return taggedResponsePkt, nil
+}
+
+// HandleServerPacket is a very simple implementation of a DHCP server
+// that only replies to DHCPDiscover and DHCPRequest packets
+func (s DHCPServer) HandleServerPacket(pkt gopacket.Packet) (gopacket.Packet, error) {
+	dhcpLayer, _ := GetDhcpLayer(pkt)
+
+	if dhcpLayer.Operation == layers.DHCPOpReply {
+		dhcpLogger.WithFields(log.Fields{
+			"pkt": hex.EncodeToString(pkt.Data()),
+		}).Error("Received DHCP Reply on the server. Ignoring the packet but this is a serious error.")
+	}
+
+	dhcpMessageType, _ := GetDhcpMessageType(dhcpLayer)
+
+	switch dhcpMessageType {
+	case layers.DHCPMsgTypeDiscover:
+		dhcpLogger.Info("Received DHCP Discover")
+		return s.handleDiscover(pkt)
+	case layers.DHCPMsgTypeRequest:
+		dhcpLogger.Info("Received DHCP Request")
+		return s.handleRequest(pkt)
+	}
+	return nil, fmt.Errorf("cannot-handle-dhcp-packet-of-type-%s", dhcpMessageType.String())
+}
diff --git a/internal/bbsim/responders/dhcp/dhcp_server_test.go b/internal/bbsim/responders/dhcp/dhcp_server_test.go
new file mode 100644
index 0000000..fa4a58d
--- /dev/null
+++ b/internal/bbsim/responders/dhcp/dhcp_server_test.go
@@ -0,0 +1,35 @@
+/*
+ * 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 (
+	"gotest.tools/assert"
+	"net"
+	"testing"
+)
+
+func TestCreateIpFromMacAddress(t *testing.T) {
+	dhcpServer := NewDHCPServer()
+
+	mac1 := net.HardwareAddr{0x2e, 0x60, 0x00, 0x0c, 0x0f, 0x02}
+	ip1 := dhcpServer.createIpFromMacAddress(mac1)
+	assert.Equal(t, "10.12.15.2", ip1.String())
+
+	mac2 := net.HardwareAddr{0x2e, 0x60, 0x00, 0x00, 0x00, 0x00}
+	ip2 := dhcpServer.createIpFromMacAddress(mac2)
+	assert.Equal(t, "10.0.0.0", ip2.String())
+}