[VOL-2778] Introducing Service definition in order to support the TT workflow

Change-Id: Ib171502e8940b5d0b219620a4503f7095d376d7a
diff --git a/internal/bbsim/api/grpc_api_server.go b/internal/bbsim/api/grpc_api_server.go
index 72f999e..7ed8f8d 100644
--- a/internal/bbsim/api/grpc_api_server.go
+++ b/internal/bbsim/api/grpc_api_server.go
@@ -74,7 +74,7 @@
 		pons = append(pons, &p)
 	}
 
-	oltAddress := strings.Split(common.Options.BBSim.OpenOltAddress, ":")[0]
+	oltAddress := strings.Split(common.Config.BBSim.OpenOltAddress, ":")[0]
 	if oltAddress == "" {
 		oltAddress = getOltIP().String()
 	}
diff --git a/internal/bbsim/api/onus_handler.go b/internal/bbsim/api/onus_handler.go
index 3c62f48..fe42b35 100644
--- a/internal/bbsim/api/onus_handler.go
+++ b/internal/bbsim/api/onus_handler.go
@@ -41,10 +41,8 @@
 				OperState:     o.OperState.Current(),
 				InternalState: o.InternalState.Current(),
 				PonPortID:     int32(o.PonPortID),
-				STag:          int32(o.STag),
-				CTag:          int32(o.CTag),
-				HwAddress:     o.HwAddress.String(),
 				PortNo:        int32(o.PortNo),
+				Services:      convertBBsimServicesToProtoServices(o.Services),
 			}
 			onus.Items = append(onus.Items, &onu)
 		}
@@ -67,10 +65,8 @@
 		OperState:     onu.OperState.Current(),
 		InternalState: onu.InternalState.Current(),
 		PonPortID:     int32(onu.PonPortID),
-		STag:          int32(onu.STag),
-		CTag:          int32(onu.CTag),
-		HwAddress:     onu.HwAddress.String(),
 		PortNo:        int32(onu.PortNo),
+		Services:      convertBBsimServicesToProtoServices(onu.Services),
 	}
 	return &res, nil
 }
diff --git a/internal/bbsim/api/services_handler.go b/internal/bbsim/api/services_handler.go
new file mode 100644
index 0000000..d20b527
--- /dev/null
+++ b/internal/bbsim/api/services_handler.go
@@ -0,0 +1,80 @@
+/*
+ * 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 api
+
+import (
+	"context"
+	"github.com/opencord/bbsim/api/bbsim"
+	"github.com/opencord/bbsim/internal/bbsim/devices"
+)
+
+func convertBBSimServiceToProtoService(s *devices.Service) *bbsim.Service {
+	return &bbsim.Service{
+		Name:       s.Name,
+		HwAddress:  s.HwAddress.String(),
+		OnuSn:      s.Onu.Sn(),
+		CTag:       int32(s.CTag),
+		STag:       int32(s.STag),
+		NeedsEapol: s.NeedsEapol,
+		NeedsDhcp:  s.NeedsDhcp,
+		NeedsIgmp:  s.NeedsIgmp,
+		GemPort:    int32(s.GemPort),
+		EapolState: s.EapolState.Current(),
+		DhcpState:  s.DHCPState.Current(),
+	}
+}
+
+func convertBBsimServicesToProtoServices(list []devices.ServiceIf) []*bbsim.Service {
+	services := []*bbsim.Service{}
+	for _, service := range list {
+		s := service.(*devices.Service)
+		services = append(services, convertBBSimServiceToProtoService(s))
+	}
+	return services
+}
+
+func (s BBSimServer) GetServices(ctx context.Context, req *bbsim.Empty) (*bbsim.Services, error) {
+
+	services := bbsim.Services{
+		Items: []*bbsim.Service{},
+	}
+
+	olt := devices.GetOLT()
+
+	for _, pon := range olt.Pons {
+		for _, o := range pon.Onus {
+			s := convertBBsimServicesToProtoServices(o.Services)
+			services.Items = append(services.Items, s...)
+		}
+	}
+
+	return &services, nil
+}
+
+func (s BBSimServer) GetOnuServices(ctx context.Context, req *bbsim.ONURequest) (*bbsim.Services, error) {
+	onu, err := s.GetONU(ctx, req)
+
+	if err != nil {
+		return nil, err
+	}
+
+	services := bbsim.Services{
+		Items: onu.Services,
+	}
+
+	return &services, nil
+}
diff --git a/internal/bbsim/devices/messageTypes.go b/internal/bbsim/devices/messageTypes.go
index f59ea5c..d9ed620 100644
--- a/internal/bbsim/devices/messageTypes.go
+++ b/internal/bbsim/devices/messageTypes.go
@@ -20,6 +20,7 @@
 	"github.com/google/gopacket"
 	"github.com/opencord/bbsim/internal/bbsim/packetHandlers"
 	"github.com/opencord/voltha-protos/v2/go/openolt"
+	"net"
 )
 
 type MessageType int
@@ -33,8 +34,6 @@
 	OMCI              MessageType = 5
 	FlowAdd           MessageType = 6
 	FlowRemoved       MessageType = 18
-	StartEAPOL        MessageType = 7
-	StartDHCP         MessageType = 8
 	OnuPacketOut      MessageType = 9
 
 	// BBR messages
@@ -130,10 +129,12 @@
 }
 
 type OnuPacketMessage struct {
-	IntfId uint32
-	OnuId  uint32
-	Packet gopacket.Packet
-	Type   packetHandlers.PacketType
+	IntfId     uint32
+	OnuId      uint32
+	Packet     gopacket.Packet
+	Type       packetHandlers.PacketType
+	MacAddress net.HardwareAddr
+	GemPortId  uint32 // this is used by BBR
 }
 
 type OperState int
diff --git a/internal/bbsim/devices/nni.go b/internal/bbsim/devices/nni.go
index e74e97b..fe092e8 100644
--- a/internal/bbsim/devices/nni.go
+++ b/internal/bbsim/devices/nni.go
@@ -18,6 +18,7 @@
 
 import (
 	"bytes"
+	"encoding/hex"
 	"os/exec"
 
 	"github.com/google/gopacket"
@@ -110,7 +111,9 @@
 			return err
 		}
 
-		nniLogger.Infof("Sent packet out of NNI")
+		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")
diff --git a/internal/bbsim/devices/olt.go b/internal/bbsim/devices/olt.go
index 21b57f2..eff9c86 100644
--- a/internal/bbsim/devices/olt.go
+++ b/internal/bbsim/devices/olt.go
@@ -18,6 +18,7 @@
 
 import (
 	"context"
+	"encoding/hex"
 	"fmt"
 	"net"
 	"sync"
@@ -73,7 +74,7 @@
 	enableContext       context.Context
 	enableContextCancel context.CancelFunc
 
-	OpenoltStream *openolt.Openolt_EnableIndicationServer
+	OpenoltStream openolt.Openolt_EnableIndicationServer
 	enablePerf    bool
 }
 
@@ -84,7 +85,7 @@
 	return &olt
 }
 
-func CreateOLT(options common.BBSimYamlConfig, isMock bool) *OltDevice {
+func CreateOLT(options common.GlobalConfig, services []common.ServiceYaml, isMock bool) *OltDevice {
 	oltLogger.WithFields(log.Fields{
 		"ID":           options.Olt.ID,
 		"NumNni":       options.Olt.NniPorts,
@@ -146,44 +147,61 @@
 		olt.Nnis = append(olt.Nnis, &nniPort)
 	}
 
+	// Create device and Services
+
+	nextCtag := map[string]int{}
+	nextStag := map[string]int{}
+
 	// create PON ports
+	for i := 0; i < olt.NumPon; i++ {
+		p := CreatePonPort(&olt, uint32(i))
 
-	if options.BBSim.STagAllocation == common.TagAllocationShared && options.BBSim.CTagAllocation == common.TagAllocationShared {
-		oltLogger.Fatalf("This configuration will result in duplicate C/S tags combination")
-	} else if options.BBSim.STagAllocation == common.TagAllocationUnique && options.BBSim.CTagAllocation == common.TagAllocationUnique {
-		oltLogger.Fatalf("This configuration is not supported yet")
-	} else if options.BBSim.STagAllocation == common.TagAllocationShared && options.BBSim.CTagAllocation == common.TagAllocationUnique {
-		// ATT case
-		availableCTag := options.BBSim.CTag
-		for i := 0; i < olt.NumPon; i++ {
-			p := CreatePonPort(&olt, uint32(i))
+		// create ONU devices
+		for j := 0; j < olt.NumOnuPerPon; j++ {
+			delay := time.Duration(olt.Delay*j) * time.Millisecond
+			o := CreateONU(&olt, p, uint32(j+1), delay, isMock)
 
-			// create ONU devices
-			for j := 0; j < olt.NumOnuPerPon; j++ {
-				delay := time.Duration(olt.Delay*j) * time.Millisecond
-				o := CreateONU(&olt, p, uint32(j+1), options.BBSim.STag, availableCTag, options.BBSim.EnableAuth, options.BBSim.EnableDhcp, delay, isMock)
-				p.Onus = append(p.Onus, o)
-				availableCTag = availableCTag + 1
+			for k, s := range common.Services {
+
+				// find the correct cTag for this service
+				if _, ok := nextCtag[s.Name]; !ok {
+					// it's the first time we iterate over this service,
+					// so we start from the config value
+					nextCtag[s.Name] = s.CTag
+				} else {
+					// we have a previous value, so we check it
+					// if Allocation is unique, we increment,
+					// otherwise (shared) we do nothing
+					if s.CTagAllocation == common.TagAllocationUnique.String() {
+						nextCtag[s.Name] = nextCtag[s.Name] + 1
+					}
+				}
+
+				// find the correct sTag for this service
+				if _, ok := nextStag[s.Name]; !ok {
+					nextStag[s.Name] = s.STag
+				} else {
+					if s.STagAllocation == common.TagAllocationUnique.String() {
+						nextStag[s.Name] = nextStag[s.Name] + 1
+					}
+				}
+
+				mac := net.HardwareAddr{0x2e, 0x60, byte(olt.ID), byte(p.ID), byte(o.ID), byte(k)}
+				service, err := NewService(s.Name, mac, o, nextCtag[s.Name], nextStag[s.Name],
+					s.NeedsEapol, s.NeedsDchp, s.NeedsIgmp, s.TechnologyProfileID, s.UniTagMatch,
+					s.ConfigureMacAddress, s.UsPonCTagPriority, s.UsPonSTagPriority, s.DsPonCTagPriority, s.DsPonSTagPriority)
+
+				if err != nil {
+					oltLogger.WithFields(log.Fields{
+						"Err": err.Error(),
+					}).Fatal("Can't create Service")
+				}
+
+				o.Services = append(o.Services, service)
 			}
-
-			olt.Pons = append(olt.Pons, p)
+			p.Onus = append(p.Onus, o)
 		}
-	} else if options.BBSim.STagAllocation == common.TagAllocationUnique && options.BBSim.CTagAllocation == common.TagAllocationShared {
-		// DT case
-		availableSTag := options.BBSim.STag
-		for i := 0; i < olt.NumPon; i++ {
-			p := CreatePonPort(&olt, uint32(i))
-
-			// create ONU devices
-			for j := 0; j < olt.NumOnuPerPon; j++ {
-				delay := time.Duration(olt.Delay*j) * time.Millisecond
-				o := CreateONU(&olt, p, uint32(j+1), availableSTag, options.BBSim.CTag, options.BBSim.EnableAuth, options.BBSim.EnableDhcp, delay, isMock)
-				p.Onus = append(p.Onus, o)
-				availableSTag = availableSTag + 1
-			}
-
-			olt.Pons = append(olt.Pons, p)
-		}
+		olt.Pons = append(olt.Pons, p)
 	}
 
 	if !isMock {
@@ -238,7 +256,7 @@
 
 func (o *OltDevice) RestartOLT() error {
 
-	rebootDelay := common.Options.Olt.OltRebootDelay
+	rebootDelay := common.Config.Olt.OltRebootDelay
 
 	oltLogger.WithFields(log.Fields{
 		"oltId": o.ID,
@@ -298,7 +316,7 @@
 
 // newOltServer launches a new grpc server for OpenOLT
 func (o *OltDevice) newOltServer() (*grpc.Server, error) {
-	address := common.Options.BBSim.OpenOltAddress
+	address := common.Config.BBSim.OpenOltAddress
 	lis, err := net.Listen("tcp", address)
 	if err != nil {
 		oltLogger.Fatalf("OLT failed to listen: %v", err)
@@ -351,7 +369,7 @@
 	wg := sync.WaitGroup{}
 	wg.Add(3)
 
-	o.OpenoltStream = &stream
+	o.OpenoltStream = stream
 
 	// create Go routine to process all OLT events
 	go o.processOltMessages(o.enableContext, stream, &wg)
@@ -444,7 +462,7 @@
 				"messageType": message.Type,
 				"OnuId":       message.Data.OnuId,
 				"IntfId":      message.Data.IntfId,
-			}).Info("Received message on OMCI Sim channel")
+			}).Debug("Received message on OMCI Sim channel")
 
 			onuId := message.Data.OnuId
 			intfId := message.Data.IntfId
@@ -577,7 +595,7 @@
 
 func (o *OltDevice) sendPonIndication(ponPortID uint32) {
 
-	stream := *o.OpenoltStream
+	stream := o.OpenoltStream
 	pon, _ := o.GetPonById(ponPortID)
 	// Send IntfIndication for PON port
 	discoverData := &openolt.Indication_IntfInd{IntfInd: &openolt.IntfIndication{
@@ -624,7 +642,7 @@
 		data := &openolt.Indication_PortStats{
 			PortStats: stats,
 		}
-		stream := *o.OpenoltStream
+		stream := o.OpenoltStream
 		if err := stream.Send(&openolt.Indication{Data: data}); err != nil {
 			oltLogger.Errorf("Failed to send PortStats: %v", err)
 			return
@@ -743,7 +761,7 @@
 				return
 			}
 
-			onu, err := o.FindOnuByMacAddress(onuMac)
+			s, err := o.FindServiceByMacAddress(onuMac)
 			if err != nil {
 				log.WithFields(log.Fields{
 					"IntfType":   "nni",
@@ -754,7 +772,9 @@
 				return
 			}
 
-			doubleTaggedPkt, err := packetHandlers.PushDoubleTag(onu.STag, onu.CTag, message.Pkt)
+			service := s.(*Service)
+
+			doubleTaggedPkt, err := packetHandlers.PushDoubleTag(service.STag, service.CTag, message.Pkt)
 			if err != nil {
 				log.Error("Fail to add double tag to packet")
 			}
@@ -773,9 +793,9 @@
 			oltLogger.WithFields(log.Fields{
 				"IntfType": data.PktInd.IntfType,
 				"IntfId":   nniId,
-				"Pkt":      doubleTaggedPkt.Data(),
-				"OnuSn":    onu.Sn(),
-			}).Tracef("Sent PktInd indication")
+				"Pkt":      hex.EncodeToString(doubleTaggedPkt.Data()),
+				"OnuSn":    service.Onu.Sn(),
+			}).Trace("Sent PktInd indication (from NNI to VOLTHA)")
 		}
 	}
 	wg.Done()
@@ -815,19 +835,20 @@
 	return &Onu{}, fmt.Errorf("cannot-find-onu-by-id-%v-%v", intfId, onuId)
 }
 
-// returns an ONU with a given Mac Address
-func (o *OltDevice) FindOnuByMacAddress(mac net.HardwareAddr) (*Onu, error) {
+// returns a Service with a given Mac Address
+func (o *OltDevice) FindServiceByMacAddress(mac net.HardwareAddr) (ServiceIf, error) {
 	// TODO this function can be a performance bottleneck when we have many ONUs,
 	// memoizing it will remove the bottleneck
 	for _, pon := range o.Pons {
 		for _, onu := range pon.Onus {
-			if onu.HwAddress.String() == mac.String() {
-				return onu, nil
+			s, err := onu.findServiceByMacAddress(mac)
+			if err == nil {
+				return s, nil
 			}
 		}
 	}
 
-	return &Onu{}, fmt.Errorf("cannot-find-onu-by-mac-address-%s", mac)
+	return nil, fmt.Errorf("cannot-find-service-by-mac-address-%s", mac)
 }
 
 // GRPC Endpoints
@@ -963,7 +984,7 @@
 			OnuID:     onu.ID,
 			OnuSN:     onu.SerialNumber,
 		}
-		onu.sendOnuIndication(onuIndication, *o.OpenoltStream)
+		onu.sendOnuIndication(onuIndication, o.OpenoltStream)
 
 	}
 
@@ -999,7 +1020,7 @@
 			OnuID:     onu.ID,
 			OnuSN:     onu.SerialNumber,
 		}
-		onu.sendOnuIndication(onuIndication, *o.OpenoltStream)
+		onu.sendOnuIndication(onuIndication, o.OpenoltStream)
 
 	}
 
@@ -1174,11 +1195,11 @@
 		"PonPorts": o.NumPon,
 	}).Info("OLT receives GetDeviceInfo call from VOLTHA")
 	devinfo := new(openolt.DeviceInfo)
-	devinfo.Vendor = common.Options.Olt.Vendor
-	devinfo.Model = common.Options.Olt.Model
-	devinfo.HardwareVersion = common.Options.Olt.HardwareVersion
-	devinfo.FirmwareVersion = common.Options.Olt.FirmwareVersion
-	devinfo.Technology = common.Options.Olt.Technology
+	devinfo.Vendor = common.Config.Olt.Vendor
+	devinfo.Model = common.Config.Olt.Model
+	devinfo.HardwareVersion = common.Config.Olt.HardwareVersion
+	devinfo.FirmwareVersion = common.Config.Olt.FirmwareVersion
+	devinfo.Technology = common.Config.Olt.Technology
 	devinfo.PonPorts = uint32(o.NumPon)
 	devinfo.OnuIdStart = 1
 	devinfo.OnuIdEnd = 255
@@ -1189,7 +1210,7 @@
 	devinfo.FlowIdStart = 1
 	devinfo.FlowIdEnd = 16383
 	devinfo.DeviceSerialNumber = o.SerialNumber
-	devinfo.DeviceId = common.Options.Olt.DeviceId
+	devinfo.DeviceId = common.Config.Olt.DeviceId
 
 	return devinfo, nil
 }
@@ -1254,20 +1275,34 @@
 		"IntfId": onu.PonPortID,
 		"OnuId":  onu.ID,
 		"OnuSn":  onu.Sn(),
-	}).Tracef("Received OnuPacketOut")
+	}).Info("Received OnuPacketOut")
 
 	rawpkt := gopacket.NewPacket(onuPkt.Pkt, layers.LayerTypeEthernet, gopacket.Default)
 	pktType, _ := packetHandlers.IsEapolOrDhcp(rawpkt)
 
+	pktMac, err := packetHandlers.GetDstMacAddressFromPacket(rawpkt)
+
+	if err != nil {
+		log.WithFields(log.Fields{
+			"IntfId": onu.PonPortID,
+			"OnuId":  onu.ID,
+			"OnuSn":  onu.Sn(),
+			"Pkt":    rawpkt.Data(),
+		}).Error("Can't find Dst MacAddress in packet, droppint it")
+		return new(openolt.Empty), nil
+	}
+
 	msg := Message{
 		Type: OnuPacketOut,
 		Data: OnuPacketMessage{
-			IntfId: onuPkt.IntfId,
-			OnuId:  onuPkt.OnuId,
-			Packet: rawpkt,
-			Type:   pktType,
+			IntfId:     onuPkt.IntfId,
+			OnuId:      onuPkt.OnuId,
+			Packet:     rawpkt,
+			Type:       pktType,
+			MacAddress: pktMac,
 		},
 	}
+
 	onu.Channel <- msg
 
 	return new(openolt.Empty), nil
diff --git a/internal/bbsim/devices/olt_test.go b/internal/bbsim/devices/olt_test.go
index 8aa0ae6..53f9f44 100644
--- a/internal/bbsim/devices/olt_test.go
+++ b/internal/bbsim/devices/olt_test.go
@@ -17,6 +17,7 @@
 package devices
 
 import (
+	"github.com/opencord/bbsim/internal/common"
 	"net"
 	"testing"
 
@@ -24,7 +25,7 @@
 	"gotest.tools/assert"
 )
 
-func createMockOlt(numPon int, numOnu int) *OltDevice {
+func createMockOlt(numPon int, numOnu int, services []ServiceIf) *OltDevice {
 	olt := &OltDevice{
 		ID: 0,
 	}
@@ -40,8 +41,15 @@
 				ID:        onuId,
 				PonPort:   &pon,
 				PonPortID: pon.ID,
-				HwAddress: net.HardwareAddr{0x2e, 0x60, 0x70, 0x13, byte(pon.ID), byte(onuId)},
 			}
+
+			for k, s := range services {
+				service := s.(*Service)
+				service.HwAddress = net.HardwareAddr{0x2e, 0x60, byte(olt.ID), byte(pon.ID), byte(onuId), byte(k)}
+				service.Onu = &onu
+				onu.Services = append(onu.Services, service)
+			}
+
 			onu.SerialNumber = onu.NewSN(olt.ID, pon.ID, onu.ID)
 			pon.Onus = append(pon.Onus, &onu)
 		}
@@ -50,12 +58,76 @@
 	return olt
 }
 
+// check the creation of an OLT with a single Service
+func TestCreateOLT(t *testing.T) {
+
+	common.Services = []common.ServiceYaml{
+		{Name: "hsia", CTag: 900, CTagAllocation: common.TagAllocationUnique.String(), STag: 900, STagAllocation: common.TagAllocationShared.String(), NeedsEapol: true, NeedsDchp: true, NeedsIgmp: true},
+	}
+
+	common.Config = &common.GlobalConfig{
+		Olt: common.OltConfig{
+			ID:          1,
+			PonPorts:    2,
+			OnusPonPort: 2,
+		},
+	}
+
+	olt := CreateOLT(*common.Config, common.Services, true)
+
+	assert.Equal(t, len(olt.Pons), int(common.Config.Olt.PonPorts))
+
+	// count the ONUs
+	onus := 0
+	for _, p := range olt.Pons {
+		onus = onus + len(p.Onus)
+	}
+
+	assert.Equal(t, onus, int(common.Config.Olt.PonPorts*common.Config.Olt.OnusPonPort))
+
+	// count the services
+	services := 0
+	for _, p := range olt.Pons {
+		for _, o := range p.Onus {
+			services = services + len(o.Services)
+		}
+	}
+
+	assert.Equal(t, services, int(common.Config.Olt.PonPorts)*int(common.Config.Olt.OnusPonPort)*len(common.Services))
+
+	s1 := olt.Pons[0].Onus[0].Services[0].(*Service)
+
+	assert.Equal(t, s1.Name, "hsia")
+	assert.Equal(t, s1.CTag, 900)
+	assert.Equal(t, s1.STag, 900)
+	assert.Equal(t, s1.HwAddress.String(), "2e:60:01:00:01:00")
+	assert.Equal(t, olt.Pons[0].Onus[0].ID, uint32(1))
+
+	s2 := olt.Pons[0].Onus[1].Services[0].(*Service)
+	assert.Equal(t, s2.CTag, 901)
+	assert.Equal(t, s2.STag, 900)
+	assert.Equal(t, s2.HwAddress.String(), "2e:60:01:00:02:00")
+	assert.Equal(t, olt.Pons[0].Onus[1].ID, uint32(2))
+
+	s3 := olt.Pons[1].Onus[0].Services[0].(*Service)
+	assert.Equal(t, s3.CTag, 902)
+	assert.Equal(t, s3.STag, 900)
+	assert.Equal(t, s3.HwAddress.String(), "2e:60:01:01:01:00")
+	assert.Equal(t, olt.Pons[1].Onus[0].ID, uint32(1))
+
+	s4 := olt.Pons[1].Onus[1].Services[0].(*Service)
+	assert.Equal(t, s4.CTag, 903)
+	assert.Equal(t, s4.STag, 900)
+	assert.Equal(t, s4.HwAddress.String(), "2e:60:01:01:02:00")
+	assert.Equal(t, olt.Pons[1].Onus[1].ID, uint32(2))
+}
+
 func Test_Olt_FindOnuBySn_Success(t *testing.T) {
 
 	numPon := 4
 	numOnu := 4
 
-	olt := createMockOlt(numPon, numOnu)
+	olt := createMockOlt(numPon, numOnu, []ServiceIf{})
 
 	onu, err := olt.FindOnuBySn("BBSM00000303")
 
@@ -70,7 +142,7 @@
 	numPon := 1
 	numOnu := 4
 
-	olt := createMockOlt(numPon, numOnu)
+	olt := createMockOlt(numPon, numOnu, []ServiceIf{})
 
 	_, err := olt.FindOnuBySn("BBSM00000303")
 
@@ -78,20 +150,30 @@
 }
 
 func Test_Olt_FindOnuByMacAddress_Success(t *testing.T) {
-
 	numPon := 4
 	numOnu := 4
 
-	olt := createMockOlt(numPon, numOnu)
+	services := []ServiceIf{
+		&Service{Name: "hsia"},
+		&Service{Name: "voip"},
+		&Service{Name: "vod"},
+	}
 
-	mac := net.HardwareAddr{0x2e, 0x60, 0x70, 0x13, byte(3), byte(3)}
+	olt := createMockOlt(numPon, numOnu, services)
 
-	onu, err := olt.FindOnuByMacAddress(mac)
+	mac := net.HardwareAddr{0x2e, 0x60, byte(olt.ID), byte(3), byte(6), byte(1)}
+	s, err := olt.FindServiceByMacAddress(mac)
+
+	assert.NilError(t, err)
+
+	service := s.(*Service)
 
 	assert.Equal(t, err, nil)
-	assert.Equal(t, onu.Sn(), "BBSM00000303")
-	assert.Equal(t, onu.ID, uint32(3))
-	assert.Equal(t, onu.PonPortID, uint32(3))
+	assert.Equal(t, service.Onu.Sn(), "BBSM00000306")
+	assert.Equal(t, service.Onu.ID, uint32(6))
+	assert.Equal(t, service.Onu.PonPortID, uint32(3))
+
+	assert.Equal(t, service.Name, "voip")
 }
 
 func Test_Olt_FindOnuByMacAddress_Error(t *testing.T) {
@@ -99,20 +181,20 @@
 	numPon := 1
 	numOnu := 4
 
-	olt := createMockOlt(numPon, numOnu)
+	olt := createMockOlt(numPon, numOnu, []ServiceIf{})
 
 	mac := net.HardwareAddr{0x2e, 0x60, 0x70, 0x13, byte(3), byte(3)}
 
-	_, err := olt.FindOnuByMacAddress(mac)
+	_, err := olt.FindServiceByMacAddress(mac)
 
-	assert.Equal(t, err.Error(), "cannot-find-onu-by-mac-address-2e:60:70:13:03:03")
+	assert.Equal(t, err.Error(), "cannot-find-service-by-mac-address-2e:60:70:13:03:03")
 }
 
 func Test_Olt_GetOnuByFlowId(t *testing.T) {
 	numPon := 4
 	numOnu := 4
 
-	olt := createMockOlt(numPon, numOnu)
+	olt := createMockOlt(numPon, numOnu, []ServiceIf{})
 
 	// Add the flows to onus (to be found)
 	onu1, _ := olt.FindOnuBySn("BBSM00000303")
diff --git a/internal/bbsim/devices/onu.go b/internal/bbsim/devices/onu.go
index 6c53b68..0f78107 100644
--- a/internal/bbsim/devices/onu.go
+++ b/internal/bbsim/devices/onu.go
@@ -18,8 +18,10 @@
 
 import (
 	"context"
-	"errors"
 	"fmt"
+	"github.com/opencord/bbsim/internal/bbsim/packetHandlers"
+	"github.com/opencord/bbsim/internal/bbsim/responders/dhcp"
+	"github.com/opencord/bbsim/internal/bbsim/responders/eapol"
 	"net"
 
 	"time"
@@ -28,10 +30,6 @@
 	"github.com/google/gopacket/layers"
 	"github.com/jpillora/backoff"
 	"github.com/looplab/fsm"
-	"github.com/opencord/bbsim/internal/bbsim/packetHandlers"
-	"github.com/opencord/bbsim/internal/bbsim/responders/dhcp"
-	"github.com/opencord/bbsim/internal/bbsim/responders/eapol"
-	"github.com/opencord/bbsim/internal/bbsim/responders/igmp"
 	"github.com/opencord/bbsim/internal/common"
 	omcilib "github.com/opencord/bbsim/internal/common/omci"
 	omcisim "github.com/opencord/omci-sim"
@@ -53,31 +51,26 @@
 	ID                  uint32
 	PonPortID           uint32
 	PonPort             *PonPort
-	STag                int
-	CTag                int
-	Auth                bool // automatically start EAPOL if set to true
-	Dhcp                bool // automatically start DHCP if set to true
-	HwAddress           net.HardwareAddr
 	InternalState       *fsm.FSM
 	DiscoveryRetryDelay time.Duration // this is the time between subsequent Discovery Indication
 	DiscoveryDelay      time.Duration // this is the time to send the first Discovery Indication
-	Backoff             *backoff.Backoff
+
+	Services []ServiceIf
+
+	Backoff *backoff.Backoff
 	// ONU State
 	// PortNo comes with flows and it's used when sending packetIndications,
 	// There is one PortNo per UNI Port, for now we're only storing the first one
 	// FIXME add support for multiple UNIs (each UNI has a different PortNo)
-	PortNo            uint32
-	GemPortAdded      bool
-	EapolFlowReceived bool
-	DhcpFlowReceived  bool
-	Flows             []FlowKey
-	FlowIds           []uint32 // keep track of the flows we currently have in the ONU
+	PortNo       uint32
+	GemPortAdded bool
+	Flows        []FlowKey
+	FlowIds      []uint32 // keep track of the flows we currently have in the ONU
 
 	OperState    *fsm.FSM
 	SerialNumber *openolt.SerialNumber
 
-	Channel         chan Message // this Channel is to track state changes OMCI messages, EAPOL and DHCP packets
-	GemPortChannels []chan bool  // this channels are used to notify everyone that is interested that a GemPort has been added
+	Channel chan Message // this Channel is to track state changes OMCI messages, EAPOL and DHCP packets
 
 	// OMCI params
 	tid       uint16
@@ -92,13 +85,7 @@
 	return common.OnuSnToString(o.SerialNumber)
 }
 
-func (o *Onu) GetGemPortChan() chan bool {
-	listener := make(chan bool, 1)
-	o.GemPortChannels = append(o.GemPortChannels, listener)
-	return listener
-}
-
-func CreateONU(olt *OltDevice, pon *PonPort, id uint32, sTag int, cTag int, auth bool, dhcp bool, delay time.Duration, isMock bool) *Onu {
+func CreateONU(olt *OltDevice, pon *PonPort, id uint32, delay time.Duration, isMock bool) *Onu {
 	b := &backoff.Backoff{
 		//These are the defaults
 		Min:    5 * time.Second,
@@ -108,21 +95,14 @@
 	}
 
 	o := Onu{
-		ID:                  0,
+		ID:                  id,
 		PonPortID:           pon.ID,
 		PonPort:             pon,
-		STag:                sTag,
-		CTag:                cTag,
-		Auth:                auth,
-		Dhcp:                dhcp,
-		HwAddress:           net.HardwareAddr{0x2e, 0x60, 0x70, byte(olt.ID), byte(pon.ID), byte(id)},
 		PortNo:              0,
 		tid:                 0x1,
 		hpTid:               0x8000,
 		seqNumber:           0,
 		DoneChannel:         make(chan bool, 1),
-		DhcpFlowReceived:    false,
-		EapolFlowReceived:   false,
 		GemPortAdded:        false,
 		DiscoveryRetryDelay: 60 * time.Second, // this is used to send OnuDiscoveryIndications until an activate call is received
 		Flows:               []FlowKey{},
@@ -152,28 +132,10 @@
 			{Name: "disable", Src: []string{"enabled", "eap_response_success_received", "auth_failed", "dhcp_ack_received", "dhcp_failed", "pon_disabled"}, Dst: "disabled"},
 			// ONU state when PON port is disabled but ONU is power ON(more states should be added in src?)
 			{Name: "pon_disabled", Src: []string{"enabled", "eap_response_success_received", "auth_failed", "dhcp_ack_received", "dhcp_failed"}, Dst: "pon_disabled"},
-			// EAPOL
-			{Name: "start_auth", Src: []string{"enabled", "eap_start_sent", "eap_response_identity_sent", "eap_response_challenge_sent", "eap_response_success_received", "auth_failed", "dhcp_ack_received", "dhcp_failed", "igmp_join_started", "igmp_left", "igmp_join_error"}, 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{"enabled", "eap_response_success_received", "dhcp_discovery_sent", "dhcp_request_sent", "dhcp_ack_received", "dhcp_failed", "igmp_join_started", "igmp_left", "igmp_join_error"}, 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"},
 			// BBR States
 			// TODO add start OMCI state
 			{Name: "send_eapol_flow", Src: []string{"initialized"}, Dst: "eapol_flow_sent"},
 			{Name: "send_dhcp_flow", Src: []string{"eapol_flow_sent"}, Dst: "dhcp_flow_sent"},
-			// IGMP
-			{Name: "igmp_join_start", Src: []string{"eap_response_success_received", "dhcp_ack_received", "igmp_left", "igmp_join_error", "igmp_join_started"}, Dst: "igmp_join_started"},
-			{Name: "igmp_join_startv3", Src: []string{"eap_response_success_received", "dhcp_ack_received", "igmp_left", "igmp_join_error", "igmp_join_started"}, Dst: "igmp_join_started"},
-			{Name: "igmp_join_error", Src: []string{"igmp_join_started"}, Dst: "igmp_join_error"},
-			{Name: "igmp_leave", Src: []string{"igmp_join_started", "eap_response_success_received", "dhcp_ack_received"}, Dst: "igmp_left"},
 		},
 		fsm.Callbacks{
 			"enter_state": func(e *fsm.Event) {
@@ -193,7 +155,7 @@
 
 				if !isMock {
 					// start ProcessOnuMessages Go routine
-					go o.ProcessOnuMessages(olt.enableContext, *olt.OpenoltStream, nil)
+					go o.ProcessOnuMessages(olt.enableContext, olt.OpenoltStream, nil)
 				}
 			},
 			"enter_discovered": func(e *fsm.Event) {
@@ -220,8 +182,6 @@
 			"enter_disabled": func(event *fsm.Event) {
 
 				// clean the ONU state
-				o.DhcpFlowReceived = false
-				o.EapolFlowReceived = false
 				o.GemPortAdded = false
 				o.PortNo = 0
 				o.Flows = []FlowKey{}
@@ -252,75 +212,7 @@
 					close(o.Channel)
 				}
 			},
-			"before_start_auth": func(e *fsm.Event) {
-				if !o.EapolFlowReceived {
-					e.Cancel(errors.New("cannot-go-to-auth-started-as-eapol-flow-is-missing"))
-					return
-				}
-				if !o.GemPortAdded {
-					e.Cancel(errors.New("cannot-go-to-auth-started-as-gemport-is-missing"))
-					return
-				}
-			},
-			"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
-			},
-			"enter_eap_response_success_received": func(e *fsm.Event) {
-				publishEvent("ONU-authentication-done", int32(o.PonPortID), int32(o.ID), o.Sn())
-			},
-			"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!")
-			},
-			"before_start_dhcp": func(e *fsm.Event) {
-
-				// we allow transition from eanbled to dhcp_started only if auth was set to false
-				if o.InternalState.Current() == "enabled" && o.Auth {
-					e.Cancel(errors.New("cannot-go-to-dhcp-started-as-authentication-is-required"))
-					return
-				}
-
-				if !o.DhcpFlowReceived {
-					e.Cancel(errors.New("cannot-go-to-dhcp-started-as-dhcp-flow-is-missing"))
-					return
-				}
-
-				if !o.GemPortAdded {
-					e.Cancel(errors.New("cannot-go-to-dhcp-started-as-gemport-is-missing"))
-					return
-				}
-			},
-			"enter_dhcp_started": func(e *fsm.Event) {
-				msg := Message{
-					Type: StartDHCP,
-					Data: PacketMessage{
-						PonPortID: o.PonPortID,
-						OnuID:     o.ID,
-					},
-				}
-				o.Channel <- msg
-			},
-			"enter_dhcp_ack_received": func(e *fsm.Event) {
-				publishEvent("ONU-DHCP-ACK-received", int32(o.PonPortID), int32(o.ID), o.Sn())
-			},
-			"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!")
-			},
+			// BBR states
 			"enter_eapol_flow_sent": func(e *fsm.Event) {
 				msg := Message{
 					Type: SendEapolFlow,
@@ -413,10 +305,6 @@
 			case FlowRemoved:
 				msg, _ := message.Data.(OnuFlowUpdateMessage)
 				o.handleFlowRemove(msg)
-			case StartEAPOL:
-				o.handleEAPOLStart(stream)
-			case StartDHCP:
-				o.handleDHCPStart(stream)
 			case OnuPacketOut:
 
 				msg, _ := message.Data.(OnuPacketMessage)
@@ -427,13 +315,20 @@
 					"pktType": msg.Type,
 				}).Trace("Received OnuPacketOut Message")
 
-				if msg.Type == packetHandlers.EAPOL {
-					eapol.HandleNextPacket(msg.OnuId, msg.IntfId, o.Sn(), o.PortNo, o.InternalState, msg.Packet, stream, client)
-				} else if msg.Type == packetHandlers.DHCP {
-					// 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
-					_ = dhcp.HandleNextPacket(o.PonPort.Olt.ID, o.ID, o.PonPortID, o.Sn(), o.PortNo, o.HwAddress, o.CTag, o.InternalState, msg.Packet, stream)
+				service, err := o.findServiceByMacAddress(msg.MacAddress)
+				if err != nil {
+					onuLogger.WithFields(log.Fields{
+						"IntfId":     msg.IntfId,
+						"OnuId":      msg.OnuId,
+						"pktType":    msg.Type,
+						"MacAddress": msg.MacAddress,
+						"OnuSn":      o.Sn(),
+					}).Error("Cannot find Service associated with packet")
+					return
 				}
+
+				service.PacketCh <- msg
+
 			case OnuPacketIn:
 				// NOTE we only receive BBR packets here.
 				// Eapol.HandleNextPacket can handle both BBSim and BBr cases so the call is the same
@@ -447,9 +342,9 @@
 				}).Trace("Received OnuPacketIn Message")
 
 				if msg.Type == packetHandlers.EAPOL {
-					eapol.HandleNextPacket(msg.OnuId, msg.IntfId, o.Sn(), o.PortNo, o.InternalState, msg.Packet, stream, client)
+					eapol.HandleNextPacket(msg.OnuId, msg.IntfId, msg.GemPortId, o.Sn(), o.PortNo, o.InternalState, msg.Packet, stream, client)
 				} else if msg.Type == packetHandlers.DHCP {
-					_ = dhcp.HandleNextBbrPacket(o.ID, o.PonPortID, o.Sn(), o.STag, o.HwAddress, o.DoneChannel, msg.Packet, client)
+					_ = dhcp.HandleNextBbrPacket(o.ID, o.PonPortID, o.Sn(), o.DoneChannel, msg.Packet, client)
 				}
 			case OmciIndication:
 				msg, _ := message.Data.(OmciIndicationMessage)
@@ -458,15 +353,6 @@
 				o.sendEapolFlow(client)
 			case SendDhcpFlow:
 				o.sendDhcpFlow(client)
-			case IGMPMembershipReportV2:
-				log.Infof("Recieved IGMPMembershipReportV2 message on ONU channel")
-				_ = igmp.SendIGMPMembershipReportV2(o.PonPortID, o.ID, o.Sn(), o.PortNo, o.HwAddress, stream)
-			case IGMPLeaveGroup:
-				log.Infof("Recieved IGMPLeaveGroupV2 message on ONU channel")
-				_ = igmp.SendIGMPLeaveGroupV2(o.PonPortID, o.ID, o.Sn(), o.PortNo, o.HwAddress, stream)
-			case IGMPMembershipReportV3:
-				log.Infof("Recieved IGMPMembershipReportV3 message on ONU channel")
-				_ = igmp.SendIGMPMembershipReportV3(o.PonPortID, o.ID, o.Sn(), o.PortNo, o.HwAddress, stream)
 			default:
 				onuLogger.Warnf("Received unknown message data %v for type %v in OLT Channel", message.Data, message.Type)
 			}
@@ -485,7 +371,7 @@
 			"OnuId":  message.Data.OnuId,
 			"IntfId": message.Data.IntfId,
 			"Type":   message.Type,
-		}).Infof("UNI Link Alarm")
+		}).Debug("UNI Link Alarm")
 		// TODO send to OLT
 
 		omciInd := openolt.OmciIndication{
@@ -510,60 +396,10 @@
 			"SerialNumber": o.Sn(),
 			"Type":         message.Type,
 			"omciPacket":   omciInd.Pkt,
-		}).Info("UNI Link alarm sent")
-
-	case omcisim.GemPortAdded:
-		log.WithFields(log.Fields{
-			"OnuId":  message.Data.OnuId,
-			"IntfId": message.Data.IntfId,
-			"OnuSn":  o.Sn(),
-		}).Infof("GemPort Added")
-
-		o.GemPortAdded = true
-
-		// broadcast the change to all listeners
-		// and close the channels as once the GemPort is set
-		// it won't change anymore
-		for _, ch := range o.GemPortChannels {
-			ch <- true
-			close(ch)
-		}
-		o.GemPortChannels = []chan bool{}
+		}).Debug("UNI Link alarm sent")
 	}
 }
 
-func (o *Onu) handleEAPOLStart(stream openolt.Openolt_EnableIndicationServer) {
-	log.Infof("Receive StartEAPOL message on ONU Channel")
-	_ = eapol.SendEapStart(o.ID, o.PonPortID, o.Sn(), o.PortNo, o.HwAddress, o.InternalState, stream)
-	go func(delay time.Duration) {
-		time.Sleep(delay)
-		if (o.InternalState.Current() == "eap_start_sent" ||
-			o.InternalState.Current() == "eap_response_identity_sent" ||
-			o.InternalState.Current() == "eap_response_challenge_sent" ||
-			o.InternalState.Current() == "auth_failed") && common.Options.BBSim.AuthRetry {
-			_ = o.InternalState.Event("start_auth")
-		} else if o.InternalState.Current() == "eap_response_success_received" {
-			o.Backoff.Reset()
-		}
-	}(o.Backoff.Duration())
-}
-
-func (o *Onu) handleDHCPStart(stream openolt.Openolt_EnableIndicationServer) {
-	log.Infof("Receive StartDHCP message on ONU Channel")
-	// FIXME use id, ponId as SendEapStart
-	_ = dhcp.SendDHCPDiscovery(o.PonPort.Olt.ID, o.PonPortID, o.ID, o.Sn(), o.PortNo, o.InternalState, o.HwAddress, o.CTag, stream)
-	go func(delay time.Duration) {
-		time.Sleep(delay)
-		if (o.InternalState.Current() == "dhcp_discovery_sent" ||
-			o.InternalState.Current() == "dhcp_request_sent" ||
-			o.InternalState.Current() == "dhcp_failed") && common.Options.BBSim.DhcpRetry {
-			_ = o.InternalState.Event("start_dhcp")
-		} else if o.InternalState.Current() == "dhcp_ack_received" {
-			o.Backoff.Reset()
-		}
-	}(o.Backoff.Duration())
-}
-
 func (o Onu) NewSN(oltid int, intfid uint32, onuid uint32) *openolt.SerialNumber {
 
 	sn := new(openolt.SerialNumber)
@@ -627,6 +463,10 @@
 		"OnuSn":      o.Sn(),
 	}).Debug("Sent Indication_OnuInd")
 
+	for _, s := range o.Services {
+		go s.HandlePackets(stream)
+	}
+
 }
 
 func (o *Onu) publishOmciEvent(msg OmciMessage) {
@@ -790,83 +630,22 @@
 	}
 
 	o.FlowIds = append(o.FlowIds, msg.Flow.FlowId)
+	o.addGemPortToService(uint32(msg.Flow.GemportId), msg.Flow.Classifier.EthType, msg.Flow.Classifier.OVid, msg.Flow.Classifier.IVid)
 
 	if msg.Flow.Classifier.EthType == uint32(layers.EthernetTypeEAPOL) && msg.Flow.Classifier.OVid == 4091 {
 		// NOTE storing the PortNO, it's needed when sending PacketIndications
 		o.storePortNumber(uint32(msg.Flow.PortNo))
-		o.EapolFlowReceived = true
-		// if authentication is not enabled, do nothing
-		if o.Auth {
-			// NOTE if we receive the EAPOL flows but we don't have GemPorts
-			// wait for it before starting auth
-			if !o.GemPortAdded {
-				// wait for Gem and then start auth
-				go func() {
-					for v := range o.GetGemPortChan() {
-						if v {
-							if err := o.InternalState.Event("start_auth"); err != nil {
-								onuLogger.Warnf("Can't go to auth_started: %v", err)
-							}
-						}
-					}
-					onuLogger.Trace("GemPortChannel closed")
-				}()
-			} else {
-				// start the EAPOL state machine
-				if err := o.InternalState.Event("start_auth"); err != nil {
-					onuLogger.Warnf("Can't go to auth_started: %v", err)
-				}
-			}
-		} else {
-			onuLogger.WithFields(log.Fields{
-				"IntfId":       o.PonPortID,
-				"OnuId":        o.ID,
-				"SerialNumber": o.Sn(),
-			}).Warn("Not starting authentication as Auth bit is not set in CLI parameters")
+
+		for _, s := range o.Services {
+			s.HandleAuth(o.PonPort.Olt.OpenoltStream)
 		}
 	} else if msg.Flow.Classifier.EthType == uint32(layers.EthernetTypeIPv4) &&
 		msg.Flow.Classifier.SrcPort == uint32(68) &&
 		msg.Flow.Classifier.DstPort == uint32(67) &&
 		(msg.Flow.Classifier.OPbits == 0 || msg.Flow.Classifier.OPbits == 255) {
 
-		if o.Dhcp {
-			if !o.DhcpFlowReceived {
-				// keep track that we received the DHCP Flows
-				// so that we can transition the state to dhcp_started
-				// this is needed as a check in case someone trigger DHCP from the CLI
-				o.DhcpFlowReceived = true
-
-				if !o.GemPortAdded {
-					// wait for Gem and then start DHCP
-					go func() {
-						for v := range o.GetGemPortChan() {
-							if v {
-								if err := o.InternalState.Event("start_dhcp"); err != nil {
-									log.Errorf("Can't go to dhcp_started: %v", err)
-								}
-							}
-						}
-					}()
-				} else {
-					// start the DHCP state machine
-					if err := o.InternalState.Event("start_dhcp"); err != nil {
-						log.Errorf("Can't go to dhcp_started: %v", err)
-					}
-				}
-			} else {
-				onuLogger.WithFields(log.Fields{
-					"IntfId":           o.PonPortID,
-					"OnuId":            o.ID,
-					"SerialNumber":     o.Sn(),
-					"DhcpFlowReceived": o.DhcpFlowReceived,
-				}).Warn("DHCP already started")
-			}
-		} else {
-			onuLogger.WithFields(log.Fields{
-				"IntfId":       o.PonPortID,
-				"OnuId":        o.ID,
-				"SerialNumber": o.Sn(),
-			}).Warn("Not starting DHCP as Dhcp bit is not set in CLI parameters")
+		for _, s := range o.Services {
+			s.HandleDhcp(o.PonPort.Olt.OpenoltStream, int(msg.Flow.Classifier.OVid))
 		}
 	}
 }
@@ -896,11 +675,6 @@
 		}).Info("Resetting GemPort")
 		o.GemPortAdded = false
 
-		// TODO ideally we should keep track of the flow type (and not only the ID)
-		// so that we can properly set these two flag when the flow is removed
-		o.EapolFlowReceived = false
-		o.DhcpFlowReceived = false
-
 		// check if ONU delete is performed and
 		// terminate the ONU's ProcessOnuMessages Go routine
 		if o.InternalState.Current() == "disabled" {
@@ -1075,10 +849,15 @@
 }
 
 func (o *Onu) sendDhcpFlow(client openolt.OpenoltClient) {
+
+	// BBR only works with a single service (ATT HSIA)
+	hsia := o.Services[0].(*Service)
+
 	classifierProto := openolt.Classifier{
 		EthType: uint32(layers.EthernetTypeIPv4),
 		SrcPort: uint32(68),
 		DstPort: uint32(67),
+		OVid:    uint32(hsia.CTag),
 	}
 
 	actionProto := openolt.Action{}
@@ -1158,3 +937,36 @@
 		}).Infof("Failed to transition ONU to discovered state: %s", err.Error())
 	}
 }
+
+func (onu *Onu) addGemPortToService(gemport uint32, ethType uint32, oVlan uint32, iVlan uint32) {
+	for _, s := range onu.Services {
+		if service, ok := s.(*Service); ok {
+			// EAPOL is a strange case, as packets are untagged
+			// but we assume we will have a single service requiring EAPOL
+			if ethType == uint32(layers.EthernetTypeEAPOL) && service.NeedsEapol {
+				service.GemPort = gemport
+			}
+
+			// For DHCP services we single tag the outgoing packets,
+			// thus the flow only contains the CTag and we can use that to match the service
+			if ethType == uint32(layers.EthernetTypeIPv4) && service.NeedsDhcp && service.CTag == int(oVlan) {
+				service.GemPort = gemport
+			}
+
+			// for dataplane services match both C and S tags
+			if service.CTag == int(iVlan) && service.STag == int(oVlan) {
+				service.GemPort = gemport
+			}
+		}
+	}
+}
+
+func (onu *Onu) findServiceByMacAddress(macAddress net.HardwareAddr) (*Service, error) {
+	for _, s := range onu.Services {
+		service := s.(*Service)
+		if service.HwAddress.String() == macAddress.String() {
+			return service, nil
+		}
+	}
+	return nil, fmt.Errorf("cannot-find-service-with-mac-address-%s", macAddress.String())
+}
diff --git a/internal/bbsim/devices/onu_flow_test.go b/internal/bbsim/devices/onu_flow_test.go
index 4ff33a7..3f0d702 100644
--- a/internal/bbsim/devices/onu_flow_test.go
+++ b/internal/bbsim/devices/onu_flow_test.go
@@ -21,13 +21,12 @@
 	"github.com/looplab/fsm"
 	"github.com/opencord/voltha-protos/v2/go/openolt"
 	"gotest.tools/assert"
-	"sync"
 	"testing"
-	"time"
 )
 
+// test that BBR correctly sends the EAPOL Flow
 func Test_Onu_SendEapolFlow(t *testing.T) {
-	onu := createMockOnu(1, 1, 900, 900, false, false)
+	onu := createMockOnu(1, 1)
 
 	client := &mockClient{
 		FlowAddSpy: FlowAddSpy{
@@ -49,7 +48,7 @@
 
 // checks that the FlowId is added to the list
 func Test_HandleFlowAddFlowId(t *testing.T) {
-	onu := createMockOnu(1, 1, 900, 900, true, false)
+	onu := createMockOnu(1, 1)
 
 	flow := openolt.Flow{
 		FlowId:     64,
@@ -65,12 +64,110 @@
 	assert.Equal(t, onu.FlowIds[0], uint32(64))
 }
 
+// checks that we only remove the correct flow
+func Test_HandleFlowRemoveFlowId(t *testing.T) {
+	onu := createMockOnu(1, 1)
+
+	onu.FlowIds = []uint32{1, 2, 34, 64, 92}
+
+	flow := openolt.Flow{
+		FlowId:     64,
+		Classifier: &openolt.Classifier{},
+	}
+	msg := OnuFlowUpdateMessage{
+		OnuID:     onu.ID,
+		PonPortID: onu.PonPortID,
+		Flow:      &flow,
+	}
+	onu.handleFlowRemove(msg)
+	assert.Equal(t, len(onu.FlowIds), 4)
+	assert.Equal(t, onu.FlowIds[0], uint32(1))
+	assert.Equal(t, onu.FlowIds[1], uint32(2))
+	assert.Equal(t, onu.FlowIds[2], uint32(34))
+	assert.Equal(t, onu.FlowIds[3], uint32(92))
+}
+
+// checks that when the last flow is removed we reset the stored flags in the ONU
+func Test_HandleFlowRemoveFlowId_LastFlow(t *testing.T) {
+	onu := createMockOnu(1, 1)
+
+	onu.InternalState = fsm.NewFSM(
+		"enabled",
+		fsm.Events{
+			{Name: "disable", Src: []string{"enabled"}, Dst: "disabled"},
+		},
+		fsm.Callbacks{},
+	)
+
+	onu.GemPortAdded = true
+
+	onu.FlowIds = []uint32{64}
+
+	flow := openolt.Flow{
+		FlowId:     64,
+		Classifier: &openolt.Classifier{},
+	}
+	msg := OnuFlowUpdateMessage{
+		OnuID:     onu.ID,
+		PonPortID: onu.PonPortID,
+		Flow:      &flow,
+	}
+	onu.handleFlowRemove(msg)
+	assert.Equal(t, len(onu.FlowIds), 0)
+	assert.Equal(t, onu.GemPortAdded, false)
+}
+
+func TestOnu_HhandleEAPOLStart(t *testing.T) {
+	onu := createMockOnu(1, 1)
+	hsia := mockService{Name: "hsia"}
+	voip := mockService{Name: "voip"}
+
+	onu.Services = []ServiceIf{&hsia, &voip}
+
+	stream := mockStream{
+		Calls: make(map[int]*openolt.Indication),
+	}
+
+	onu.PonPort.Olt.OpenoltStream = &stream
+
+	flow := openolt.Flow{
+		AccessIntfId:  int32(onu.PonPortID),
+		OnuId:         int32(onu.ID),
+		UniId:         int32(0),
+		FlowId:        uint32(onu.ID),
+		FlowType:      "downstream",
+		AllocId:       int32(0),
+		NetworkIntfId: int32(0),
+		Classifier: &openolt.Classifier{
+			EthType: uint32(layers.EthernetTypeEAPOL),
+			OVid:    4091,
+		},
+		Action:   &openolt.Action{},
+		Priority: int32(100),
+		PortNo:   uint32(onu.ID), // NOTE we are using this to map an incoming packetIndication to an ONU
+	}
+
+	msg := OnuFlowUpdateMessage{
+		PonPortID: 1,
+		OnuID:     1,
+		Flow:      &flow,
+	}
+
+	onu.handleFlowAdd(msg)
+
+	// check that we call HandleAuth on all the services
+	assert.Equal(t, hsia.HandleAuthCallCount, 1)
+	assert.Equal(t, voip.HandleAuthCallCount, 1)
+}
+
+// TODO all the following tests needs to be moved in the Service model
+
 // validates that when an ONU receives an EAPOL flow for UNI 0
 // and the GemPort has already been configured
 // it transition to auth_started state
 func Test_HandleFlowAddEapolWithGem(t *testing.T) {
-
-	onu := createMockOnu(1, 1, 900, 900, true, false)
+	t.Skip("Needs to be moved in the Service struct")
+	onu := createMockOnu(1, 1)
 
 	onu.InternalState = fsm.NewFSM(
 		"enabled",
@@ -110,8 +207,8 @@
 // validates that when an ONU receives an EAPOL flow for UNI that is not 0
 // no action is taken (this is independent of GemPort status
 func Test_HandleFlowAddEapolWrongUNI(t *testing.T) {
-
-	onu := createMockOnu(1, 1, 900, 900, true, false)
+	t.Skip("Needs to be moved in the Service struct")
+	onu := createMockOnu(1, 1)
 
 	onu.InternalState = fsm.NewFSM(
 		"enabled",
@@ -148,110 +245,12 @@
 	assert.Equal(t, onu.InternalState.Current(), "enabled")
 }
 
-// validates that when an ONU receives an EAPOL flow for UNI 0
-// and the GemPort has not yet been configured
-// it transition to auth_started state
-func Test_HandleFlowAddEapolWithoutGem(t *testing.T) {
-
-	onu := createMockOnu(1, 1, 900, 900, true, false)
-	onu.GemPortAdded = false
-
-	onu.InternalState = fsm.NewFSM(
-		"enabled",
-		fsm.Events{
-			{Name: "start_auth", Src: []string{"enabled"}, Dst: "auth_started"},
-		},
-		fsm.Callbacks{},
-	)
-
-	flow := openolt.Flow{
-		AccessIntfId:  int32(onu.PonPortID),
-		OnuId:         int32(onu.ID),
-		UniId:         int32(0),
-		FlowId:        uint32(onu.ID),
-		FlowType:      "downstream",
-		AllocId:       int32(0),
-		NetworkIntfId: int32(0),
-		Classifier: &openolt.Classifier{
-			EthType: uint32(layers.EthernetTypeEAPOL),
-			OVid:    4091,
-		},
-		Action:   &openolt.Action{},
-		Priority: int32(100),
-		PortNo:   uint32(onu.ID), // NOTE we are using this to map an incoming packetIndication to an ONU
-	}
-
-	msg := OnuFlowUpdateMessage{
-		PonPortID: 1,
-		OnuID:     1,
-		Flow:      &flow,
-	}
-
-	onu.handleFlowAdd(msg)
-
-	wg := sync.WaitGroup{}
-	wg.Add(1)
-	go func(wg *sync.WaitGroup) {
-		defer wg.Done()
-		time.Sleep(100 * time.Millisecond)
-
-		// emulate the addition of a GemPort
-		for _, ch := range onu.GemPortChannels {
-			ch <- true
-		}
-
-		time.Sleep(100 * time.Millisecond)
-		assert.Equal(t, onu.InternalState.Current(), "auth_started")
-	}(&wg)
-	wg.Wait()
-
-}
-
-// validates that when an ONU receives an EAPOL flow for UNI 0
-// but the noAuth bit is set no action is taken
-func Test_HandleFlowAddEapolNoAuth(t *testing.T) {
-	onu := createMockOnu(1, 1, 900, 900, false, false)
-
-	onu.InternalState = fsm.NewFSM(
-		"enabled",
-		fsm.Events{
-			{Name: "start_auth", Src: []string{"enabled"}, Dst: "auth_started"},
-		},
-		fsm.Callbacks{},
-	)
-
-	flow := openolt.Flow{
-		AccessIntfId:  int32(onu.PonPortID),
-		OnuId:         int32(onu.ID),
-		UniId:         int32(0),
-		FlowId:        uint32(onu.ID),
-		FlowType:      "downstream",
-		AllocId:       int32(0),
-		NetworkIntfId: int32(0),
-		Classifier: &openolt.Classifier{
-			EthType: uint32(layers.EthernetTypeEAPOL),
-			OVid:    4091,
-		},
-		Action:   &openolt.Action{},
-		Priority: int32(100),
-		PortNo:   uint32(onu.ID), // NOTE we are using this to map an incoming packetIndication to an ONU
-	}
-
-	msg := OnuFlowUpdateMessage{
-		PonPortID: 1,
-		OnuID:     1,
-		Flow:      &flow,
-	}
-
-	onu.handleFlowAdd(msg)
-	assert.Equal(t, onu.InternalState.Current(), "enabled")
-}
-
 // validates that when an ONU receives a DHCP flow for UNI 0 and pbit 0
 // and the GemPort has already been configured
 // it transition to dhcp_started state
 func Test_HandleFlowAddDhcp(t *testing.T) {
-	onu := createMockOnu(1, 1, 900, 900, false, true)
+	t.Skip("Needs to be moved in the Service struct")
+	onu := createMockOnu(1, 1)
 
 	onu.InternalState = fsm.NewFSM(
 		"eap_response_success_received",
@@ -288,14 +287,14 @@
 
 	onu.handleFlowAdd(msg)
 	assert.Equal(t, onu.InternalState.Current(), "dhcp_started")
-	assert.Equal(t, onu.DhcpFlowReceived, true)
 }
 
 // validates that when an ONU receives a DHCP flow for UNI 0 and pbit 255
 // and the GemPort has already been configured
 // it transition to dhcp_started state
 func Test_HandleFlowAddDhcpPBit255(t *testing.T) {
-	onu := createMockOnu(1, 1, 900, 900, false, true)
+	t.Skip("Needs to be moved in the Service struct")
+	onu := createMockOnu(1, 1)
 
 	onu.InternalState = fsm.NewFSM(
 		"eap_response_success_received",
@@ -332,14 +331,14 @@
 
 	onu.handleFlowAdd(msg)
 	assert.Equal(t, onu.InternalState.Current(), "dhcp_started")
-	assert.Equal(t, onu.DhcpFlowReceived, true)
 }
 
 // validates that when an ONU receives a DHCP flow for UNI 0 and pbit not 0 or 255
 // and the GemPort has already been configured
 // it ignores the message
 func Test_HandleFlowAddDhcpIgnoreByPbit(t *testing.T) {
-	onu := createMockOnu(1, 1, 900, 900, false, true)
+	t.Skip("Needs to be moved in the Service struct")
+	onu := createMockOnu(1, 1)
 
 	onu.InternalState = fsm.NewFSM(
 		"eap_response_success_received",
@@ -376,13 +375,13 @@
 
 	onu.handleFlowAdd(msg)
 	assert.Equal(t, onu.InternalState.Current(), "eap_response_success_received")
-	assert.Equal(t, onu.DhcpFlowReceived, false)
 }
 
 // validates that when an ONU receives a DHCP flow for UNI 0
 // but the noDchp bit is set no action is taken
 func Test_HandleFlowAddDhcpNoDhcp(t *testing.T) {
-	onu := createMockOnu(1, 1, 900, 900, false, false)
+	t.Skip("Needs to be moved in the Service struct")
+	onu := createMockOnu(1, 1)
 
 	onu.InternalState = fsm.NewFSM(
 		"eap_response_success_received",
@@ -418,16 +417,16 @@
 
 	onu.handleFlowAdd(msg)
 	assert.Equal(t, onu.InternalState.Current(), "eap_response_success_received")
-	assert.Equal(t, onu.DhcpFlowReceived, false)
 }
 
 // validates that when an ONU receives a DHCP flow for UNI 0 and pbit not 0 or 255
 // and the GemPort has not already been configured
 // it transition to dhcp_started state
 func Test_HandleFlowAddDhcpWithoutGem(t *testing.T) {
+	t.Skip("Needs to be moved in the Service struct")
 	// NOTE that this feature is required as there is no guarantee that the gemport is the same
 	// one we received with the EAPOL flow
-	onu := createMockOnu(1, 1, 900, 900, false, true)
+	onu := createMockOnu(1, 1)
 
 	onu.GemPortAdded = false
 
@@ -465,78 +464,4 @@
 	}
 
 	onu.handleFlowAdd(msg)
-
-	wg := sync.WaitGroup{}
-	wg.Add(1)
-	go func(wg *sync.WaitGroup) {
-		defer wg.Done()
-		time.Sleep(100 * time.Millisecond)
-
-		// emulate the addition of a GemPort
-		for _, ch := range onu.GemPortChannels {
-			ch <- true
-		}
-
-		time.Sleep(100 * time.Millisecond)
-		assert.Equal(t, onu.InternalState.Current(), "dhcp_started")
-		assert.Equal(t, onu.DhcpFlowReceived, true)
-	}(&wg)
-	wg.Wait()
-}
-
-// checks that we only remove the correct flow
-func Test_HandleFlowRemoveFlowId(t *testing.T) {
-	onu := createMockOnu(1, 1, 900, 900, true, false)
-
-	onu.FlowIds = []uint32{1, 2, 34, 64, 92}
-
-	flow := openolt.Flow{
-		FlowId:     64,
-		Classifier: &openolt.Classifier{},
-	}
-	msg := OnuFlowUpdateMessage{
-		OnuID:     onu.ID,
-		PonPortID: onu.PonPortID,
-		Flow:      &flow,
-	}
-	onu.handleFlowRemove(msg)
-	assert.Equal(t, len(onu.FlowIds), 4)
-	assert.Equal(t, onu.FlowIds[0], uint32(1))
-	assert.Equal(t, onu.FlowIds[1], uint32(2))
-	assert.Equal(t, onu.FlowIds[2], uint32(34))
-	assert.Equal(t, onu.FlowIds[3], uint32(92))
-}
-
-// checks that when the last flow is removed we reset the stored flags in the ONU
-func Test_HandleFlowRemoveFlowId_LastFlow(t *testing.T) {
-	onu := createMockOnu(1, 1, 900, 900, true, false)
-
-	onu.InternalState = fsm.NewFSM(
-		"enabled",
-		fsm.Events{
-			{Name: "disable", Src: []string{"enabled"}, Dst: "disabled"},
-		},
-		fsm.Callbacks{},
-	)
-
-	onu.GemPortAdded = true
-	onu.DhcpFlowReceived = true
-	onu.EapolFlowReceived = true
-
-	onu.FlowIds = []uint32{64}
-
-	flow := openolt.Flow{
-		FlowId:     64,
-		Classifier: &openolt.Classifier{},
-	}
-	msg := OnuFlowUpdateMessage{
-		OnuID:     onu.ID,
-		PonPortID: onu.PonPortID,
-		Flow:      &flow,
-	}
-	onu.handleFlowRemove(msg)
-	assert.Equal(t, len(onu.FlowIds), 0)
-	assert.Equal(t, onu.GemPortAdded, false)
-	assert.Equal(t, onu.DhcpFlowReceived, false)
-	assert.Equal(t, onu.EapolFlowReceived, false)
 }
diff --git a/internal/bbsim/devices/onu_indications_test.go b/internal/bbsim/devices/onu_indications_test.go
index 433b776..76c7a7d 100644
--- a/internal/bbsim/devices/onu_indications_test.go
+++ b/internal/bbsim/devices/onu_indications_test.go
@@ -30,7 +30,7 @@
 type mockStream struct {
 	grpc.ServerStream
 	CallCount int
-	Calls     map[int]*openolt.OnuDiscIndication
+	Calls     map[int]*openolt.Indication
 	channel   chan int
 	fail      bool
 }
@@ -40,8 +40,10 @@
 	if s.fail {
 		return errors.New("fake-error")
 	}
-	s.Calls[s.CallCount] = ind.GetOnuDiscInd()
-	s.channel <- s.CallCount
+	s.Calls[s.CallCount] = ind
+	go func() {
+		s.channel <- s.CallCount
+	}()
 	return nil
 }
 
@@ -50,7 +52,7 @@
 	onu := createTestOnu()
 	stream := &mockStream{
 		CallCount: 0,
-		Calls:     make(map[int]*openolt.OnuDiscIndication),
+		Calls:     make(map[int]*openolt.Indication),
 		fail:      false,
 		channel:   make(chan int, 10),
 	}
@@ -62,9 +64,10 @@
 	select {
 	default:
 	case <-time.After(90 * time.Millisecond):
+		call := stream.Calls[1].GetOnuDiscInd()
 		assert.Equal(t, stream.CallCount, 1)
-		assert.Equal(t, stream.Calls[1].IntfId, onu.PonPortID)
-		assert.Equal(t, stream.Calls[1].SerialNumber, onu.SerialNumber)
+		assert.Equal(t, call.IntfId, onu.PonPortID)
+		assert.Equal(t, call.SerialNumber, onu.SerialNumber)
 	}
 	cancel()
 }
@@ -74,7 +77,7 @@
 	onu := createTestOnu()
 	stream := &mockStream{
 		CallCount: 0,
-		Calls:     make(map[int]*openolt.OnuDiscIndication),
+		Calls:     make(map[int]*openolt.Indication),
 		fail:      false,
 		channel:   make(chan int, 10),
 	}
@@ -97,7 +100,7 @@
 	onu.DiscoveryRetryDelay = 500 * time.Millisecond
 	stream := &mockStream{
 		CallCount: 0,
-		Calls:     make(map[int]*openolt.OnuDiscIndication),
+		Calls:     make(map[int]*openolt.Indication),
 		fail:      false,
 		channel:   make(chan int, 10),
 	}
diff --git a/internal/bbsim/devices/onu_state_machine_test.go b/internal/bbsim/devices/onu_state_machine_test.go
index fdecc3f..b48b286 100644
--- a/internal/bbsim/devices/onu_state_machine_test.go
+++ b/internal/bbsim/devices/onu_state_machine_test.go
@@ -37,8 +37,6 @@
 	assert.Equal(t, onu.InternalState.Current(), "enabled")
 
 	onu.PortNo = 16
-	onu.DhcpFlowReceived = true
-	onu.EapolFlowReceived = true
 	onu.GemPortAdded = true
 	onu.Flows = []FlowKey{
 		{ID: 1, Direction: "upstream"},
@@ -48,8 +46,6 @@
 	_ = onu.InternalState.Event("disable")
 	assert.Equal(t, onu.InternalState.Current(), "disabled")
 
-	assert.Equal(t, onu.DhcpFlowReceived, false)
-	assert.Equal(t, onu.EapolFlowReceived, false)
 	assert.Equal(t, onu.GemPortAdded, false)
 	assert.Equal(t, onu.PortNo, uint32(0))
 	assert.Equal(t, len(onu.Flows), 0)
@@ -59,6 +55,7 @@
 // - the GemPort is set
 // - the eapolFlow is received
 func Test_Onu_StateMachine_eapol_no_flow(t *testing.T) {
+	t.Skip("Needs to be moved in the Service struct")
 	onu := createTestOnu()
 
 	onu.InternalState.SetState("enabled")
@@ -74,13 +71,13 @@
 }
 
 func Test_Onu_StateMachine_eapol_no_gem(t *testing.T) {
+	t.Skip("Needs to be moved in the Service struct")
 	onu := createTestOnu()
 
 	onu.InternalState.SetState("enabled")
 	assert.Equal(t, onu.InternalState.Current(), "enabled")
-	// fail has no GemPort has been set
-	onu.EapolFlowReceived = true
 
+	// fail has no GemPort has been set
 	err := onu.InternalState.Event("start_auth")
 	if err == nil {
 		t.Fatal("can't start EAPOL without GemPort")
@@ -91,24 +88,23 @@
 }
 
 func Test_Onu_StateMachine_eapol_start(t *testing.T) {
-
+	t.Skip("Needs to be moved in the Service struct")
 	onu := createTestOnu()
 
 	onu.InternalState.SetState("enabled")
 	assert.Equal(t, onu.InternalState.Current(), "enabled")
 
 	// succeed
-	onu.EapolFlowReceived = true
 	onu.GemPortAdded = true
 	_ = onu.InternalState.Event("start_auth")
 	assert.Equal(t, onu.InternalState.Current(), "auth_started")
 }
 
 func Test_Onu_StateMachine_eapol_states(t *testing.T) {
+	t.Skip("Needs to be moved in the Service struct")
 	onu := createTestOnu()
 
 	onu.GemPortAdded = true
-	onu.EapolFlowReceived = true
 
 	onu.InternalState.SetState("auth_started")
 
@@ -134,13 +130,12 @@
 
 // if auth is set to true we can't go from enabled to dhcp_started
 func Test_Onu_StateMachine_dhcp_no_auth(t *testing.T) {
+	t.Skip("Needs to be moved in the Service struct")
 	onu := createTestOnu()
 
 	onu.InternalState.SetState("enabled")
 	assert.Equal(t, onu.InternalState.Current(), "enabled")
 
-	onu.Auth = true
-
 	err := onu.InternalState.Event("start_dhcp")
 	if err == nil {
 		t.Fail()
@@ -151,13 +146,12 @@
 
 // if the DHCP flow has not been received we can't start authentication
 func Test_Onu_StateMachine_dhcp_no_flow(t *testing.T) {
+	t.Skip("Needs to be moved in the Service struct")
 	onu := createTestOnu()
 
 	onu.InternalState.SetState("eap_response_success_received")
 	assert.Equal(t, onu.InternalState.Current(), "eap_response_success_received")
 
-	onu.DhcpFlowReceived = false
-
 	err := onu.InternalState.Event("start_dhcp")
 	if err == nil {
 		t.Fail()
@@ -168,12 +162,12 @@
 
 // if the ONU does not have a GemPort we can't start DHCP
 func Test_Onu_StateMachine_dhcp_no_gem(t *testing.T) {
+	t.Skip("Needs to be moved in the Service struct")
 	onu := createTestOnu()
 
 	onu.InternalState.SetState("eap_response_success_received")
 	assert.Equal(t, onu.InternalState.Current(), "eap_response_success_received")
 
-	onu.DhcpFlowReceived = true
 	onu.GemPortAdded = false
 
 	err := onu.InternalState.Event("start_dhcp")
@@ -185,10 +179,9 @@
 }
 
 func Test_Onu_StateMachine_dhcp_start(t *testing.T) {
+	t.Skip("Needs to be moved in the Service struct")
 	onu := createTestOnu()
-	onu.DhcpFlowReceived = true
 	onu.GemPortAdded = true
-	onu.Auth = true
 
 	onu.InternalState.SetState("eap_response_success_received")
 	assert.Equal(t, onu.InternalState.Current(), "eap_response_success_received")
@@ -199,9 +192,9 @@
 }
 
 func Test_Onu_StateMachine_dhcp_states(t *testing.T) {
+	t.Skip("Needs to be moved in the Service struct")
 	onu := createTestOnu()
 
-	onu.DhcpFlowReceived = true
 	onu.GemPortAdded = true
 
 	onu.InternalState.SetState("dhcp_started")
diff --git a/internal/bbsim/devices/onu_test.go b/internal/bbsim/devices/onu_test.go
index a3f1276..702b31a 100644
--- a/internal/bbsim/devices/onu_test.go
+++ b/internal/bbsim/devices/onu_test.go
@@ -17,8 +17,9 @@
 package devices
 
 import (
-	omcisim "github.com/opencord/omci-sim"
+	"github.com/google/gopacket/layers"
 	"gotest.tools/assert"
+	"net"
 	"testing"
 )
 
@@ -32,53 +33,71 @@
 		Olt: &olt,
 	}
 
-	onu := CreateONU(&olt, &pon, 1, 900, 900, true, false, 0, false)
+	onu := CreateONU(&olt, &pon, 1, 0, false)
 
 	assert.Equal(t, onu.Sn(), "BBSM00000101")
-	assert.Equal(t, onu.STag, 900)
-	assert.Equal(t, onu.CTag, 900)
-	assert.Equal(t, onu.Auth, true)
-	assert.Equal(t, onu.Dhcp, false)
-	assert.Equal(t, onu.HwAddress.String(), "2e:60:70:00:01:01")
 }
 
-func TestOnu_processOmciMessage_GemPortAdded(t *testing.T) {
+func Test_AddGemPortToService_eapol(t *testing.T) {
 
-	receivedValues := []bool{}
-
-	checker := func(ch chan bool, done chan int) {
-		for v := range ch {
-			receivedValues = append(receivedValues, v)
-		}
-		done <- 0
-	}
+	hsia := Service{Name: "hsia", NeedsEapol: true, CTag: 900}
+	voip := Service{Name: "voip", NeedsEapol: false, CTag: 55}
 
 	onu := createTestOnu()
 
-	// create two listeners on the GemPortAdded event
-	ch1 := onu.GetGemPortChan()
-	ch2 := onu.GetGemPortChan()
+	onu.Services = []ServiceIf{&hsia, &voip}
 
-	msg := omcisim.OmciChMessage{
-		Type: omcisim.GemPortAdded,
-		Data: omcisim.OmciChMessageData{
-			IntfId: 1,
-			OnuId:  1,
-		},
-	}
+	onu.addGemPortToService(1024, uint32(layers.EthernetTypeEAPOL), 0, 0)
 
-	onu.processOmciMessage(msg, nil)
+	assert.Equal(t, hsia.GemPort, uint32(1024))
+	assert.Equal(t, voip.GemPort, uint32(0))
+}
 
-	done := make(chan int)
+func Test_AddGemPortToService_dhcp(t *testing.T) {
 
-	go checker(ch1, done)
-	go checker(ch2, done)
+	hsia := Service{Name: "hsia", NeedsEapol: true}
+	voip := Service{Name: "voip", NeedsDhcp: true, CTag: 900}
+	mc := Service{Name: "mc", CTag: 900}
 
-	// wait for the messages to be received on the "done" channel
-	<-done
-	<-done
+	onu := createTestOnu()
 
-	// make sure all channel are closed and removed
-	assert.Equal(t, len(onu.GemPortChannels), 0)
-	assert.Equal(t, len(receivedValues), 2)
+	onu.Services = []ServiceIf{&hsia, &voip, &mc}
+
+	onu.addGemPortToService(1025, uint32(layers.EthernetTypeIPv4), 900, 0)
+
+	assert.Equal(t, hsia.GemPort, uint32(0))
+	assert.Equal(t, voip.GemPort, uint32(1025))
+	assert.Equal(t, mc.GemPort, uint32(0))
+}
+
+func Test_AddGemPortToService_dataplane(t *testing.T) {
+
+	hsia := Service{Name: "hsia", NeedsEapol: true, CTag: 900, STag: 500}
+	voip := Service{Name: "voip", NeedsDhcp: true, CTag: 900}
+
+	onu := createTestOnu()
+
+	onu.Services = []ServiceIf{&hsia, &voip}
+
+	onu.addGemPortToService(1024, uint32(layers.EthernetTypeLLC), 500, 900)
+
+	assert.Equal(t, hsia.GemPort, uint32(1024))
+	assert.Equal(t, voip.GemPort, uint32(0))
+}
+
+func Test_FindServiceByMacAddress(t *testing.T) {
+
+	mac := net.HardwareAddr{0x2e, 0x60, byte(1), byte(1), byte(1), byte(2)}
+
+	hsia := Service{Name: "hsia", HwAddress: net.HardwareAddr{0x2e, 0x60, byte(1), byte(1), byte(1), byte(1)}}
+	voip := Service{Name: "voip", HwAddress: mac}
+	vod := Service{Name: "vod", HwAddress: net.HardwareAddr{0x2e, 0x60, byte(1), byte(1), byte(1), byte(3)}}
+
+	onu := createTestOnu()
+
+	onu.Services = []ServiceIf{&hsia, &voip, &vod}
+
+	service, err := onu.findServiceByMacAddress(mac)
+	assert.NilError(t, err)
+	assert.Equal(t, service.HwAddress.String(), mac.String())
 }
diff --git a/internal/bbsim/devices/onu_test_helpers.go b/internal/bbsim/devices/onu_test_helpers.go
index 5e4b584..76f7917 100644
--- a/internal/bbsim/devices/onu_test_helpers.go
+++ b/internal/bbsim/devices/onu_test_helpers.go
@@ -19,7 +19,6 @@
 import (
 	"context"
 	"errors"
-	"net"
 	"time"
 
 	"github.com/opencord/voltha-protos/v2/go/openolt"
@@ -107,19 +106,18 @@
 }
 
 // this method creates a fake ONU used in the tests
-func createMockOnu(id uint32, ponPortId uint32, sTag int, cTag int, auth bool, dhcp bool) *Onu {
+func createMockOnu(id uint32, ponPortId uint32) *Onu {
 	o := Onu{
 		ID:           id,
 		PonPortID:    ponPortId,
-		STag:         sTag,
-		CTag:         cTag,
-		HwAddress:    net.HardwareAddr{0x2e, 0x60, 0x70, 0x13, byte(ponPortId), byte(id)},
 		PortNo:       0,
-		Auth:         auth,
-		Dhcp:         dhcp,
 		GemPortAdded: true,
+		PonPort: &PonPort{
+			Olt: &OltDevice{},
+		},
 	}
 	o.SerialNumber = o.NewSN(0, ponPortId, o.ID)
+	o.Channel = make(chan Message, 10)
 	return &o
 }
 
@@ -132,7 +130,7 @@
 		ID:  1,
 		Olt: &olt,
 	}
-	onu := CreateONU(&olt, &pon, 1, 900, 900, false, false, time.Duration(1*time.Millisecond), true)
+	onu := CreateONU(&olt, &pon, 1, time.Duration(1*time.Millisecond), true)
 	// NOTE we need this in order to create the OnuChannel
 	_ = onu.InternalState.Event("initialize")
 	onu.DiscoveryRetryDelay = 100 * time.Millisecond
diff --git a/internal/bbsim/devices/service_test.go b/internal/bbsim/devices/service_test.go
new file mode 100644
index 0000000..f83193a
--- /dev/null
+++ b/internal/bbsim/devices/service_test.go
@@ -0,0 +1,89 @@
+/*
+ * 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 (
+	"github.com/opencord/bbsim/internal/bbsim/types"
+	"github.com/opencord/voltha-protos/v2/go/openolt"
+	"gotest.tools/assert"
+	"net"
+	"testing"
+)
+
+type mockService struct {
+	Name                   string
+	HandleAuthCallCount    int
+	HandleDhcpCallCount    int
+	HandlePacketsCallCount int
+}
+
+func (s *mockService) HandleAuth(stream types.Stream) {
+	s.HandleAuthCallCount = s.HandleAuthCallCount + 1
+}
+
+func (s *mockService) HandleDhcp(stream types.Stream, cTag int) {
+	s.HandleDhcpCallCount = s.HandleDhcpCallCount + 1
+}
+
+func (s *mockService) HandlePackets(stream types.Stream) {
+	s.HandlePacketsCallCount = s.HandlePacketsCallCount + 1
+}
+
+func TestService_HandleAuth_noEapol(t *testing.T) {
+	mac := net.HardwareAddr{0x2e, 0x60, byte(1), byte(1), byte(1), byte(1)}
+	onu := createMockOnu(1, 1)
+	s, err := NewService("testService", mac, onu, 900, 900,
+		false, false, false, 64, 0, false,
+		0, 0, 0, 0)
+
+	assert.NilError(t, err)
+
+	stream := &mockStream{
+		Calls:   make(map[int]*openolt.Indication),
+		channel: make(chan int, 10),
+	}
+
+	s.HandleAuth(stream)
+
+	// if the service does not need EAPOL we don't expect any packet to be generated
+	assert.Equal(t, stream.CallCount, 0)
+
+	// state should not change
+	assert.Equal(t, s.EapolState.Current(), "created")
+}
+
+func TestService_HandleAuth_withEapol(t *testing.T) {
+	mac := net.HardwareAddr{0x2e, 0x60, byte(1), byte(1), byte(1), byte(1)}
+	onu := createMockOnu(1, 1)
+	s, err := NewService("testService", mac, onu, 900, 900,
+		true, false, false, 64, 0, false,
+		0, 0, 0, 0)
+
+	assert.NilError(t, err)
+
+	stream := &mockStream{
+		Calls: make(map[int]*openolt.Indication),
+	}
+
+	s.HandleAuth(stream)
+
+	// if the service does not need EAPOL we don't expect any packet to be generated
+	assert.Equal(t, stream.CallCount, 1)
+
+	// state should not change
+	assert.Equal(t, s.EapolState.Current(), "eap_start_sent")
+}
diff --git a/internal/bbsim/devices/services.go b/internal/bbsim/devices/services.go
new file mode 100644
index 0000000..c7940c9
--- /dev/null
+++ b/internal/bbsim/devices/services.go
@@ -0,0 +1,299 @@
+/*
+ * 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 (
+	"github.com/looplab/fsm"
+	"github.com/opencord/bbsim/internal/bbsim/packetHandlers"
+	"github.com/opencord/bbsim/internal/bbsim/responders/dhcp"
+	"github.com/opencord/bbsim/internal/bbsim/responders/eapol"
+	bbsimTypes "github.com/opencord/bbsim/internal/bbsim/types"
+	log "github.com/sirupsen/logrus"
+	"net"
+)
+
+var serviceLogger = log.WithFields(log.Fields{
+	"module": "SERVICE",
+})
+
+type ServiceIf interface {
+	HandlePackets(stream bbsimTypes.Stream)        // start listening on the PacketCh
+	HandleAuth(stream bbsimTypes.Stream)           // Sends the EapoStart packet
+	HandleDhcp(stream bbsimTypes.Stream, cTag int) // Sends the DHCPDiscover packet
+}
+
+type Service struct {
+	Name                string
+	HwAddress           net.HardwareAddr
+	Onu                 *Onu
+	CTag                int
+	STag                int
+	NeedsEapol          bool
+	NeedsDhcp           bool
+	NeedsIgmp           bool
+	TechnologyProfileID int
+	UniTagMatch         int
+	ConfigureMacAddress bool
+	UsPonCTagPriority   int
+	UsPonSTagPriority   int
+	DsPonCTagPriority   int
+	DsPonSTagPriority   int
+
+	// state
+	GemPort    uint32
+	EapolState *fsm.FSM
+	DHCPState  *fsm.FSM
+	PacketCh   chan OnuPacketMessage
+}
+
+func NewService(name string, hwAddress net.HardwareAddr, onu *Onu, cTag int, sTag int,
+	needsEapol bool, needsDchp bool, needsIgmp bool, tpID int, uniTagMatch int, configMacAddress bool,
+	usPonCTagPriority int, usPonSTagPriority int, dsPonCTagPriority int, dsPonSTagPriority int) (*Service, error) {
+
+	service := Service{
+		Name:                name,
+		HwAddress:           hwAddress,
+		Onu:                 onu,
+		CTag:                cTag,
+		STag:                sTag,
+		NeedsEapol:          needsEapol,
+		NeedsDhcp:           needsDchp,
+		NeedsIgmp:           needsIgmp,
+		TechnologyProfileID: tpID,
+		UniTagMatch:         uniTagMatch,
+		ConfigureMacAddress: configMacAddress,
+		UsPonCTagPriority:   usPonCTagPriority,
+		UsPonSTagPriority:   usPonSTagPriority,
+		DsPonCTagPriority:   dsPonCTagPriority,
+		DsPonSTagPriority:   dsPonSTagPriority,
+		PacketCh:            make(chan OnuPacketMessage),
+	}
+
+	service.EapolState = fsm.NewFSM(
+		"created",
+		fsm.Events{
+			{Name: "start_auth", Src: []string{"created", "eap_start_sent", "eap_response_identity_sent", "eap_response_challenge_sent", "eap_response_success_received", "auth_failed"}, 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"},
+		},
+		fsm.Callbacks{
+			"enter_state": func(e *fsm.Event) {
+				service.logStateChange("EapolState", e.Src, e.Dst)
+			},
+		},
+	)
+
+	service.DHCPState = fsm.NewFSM(
+		"created",
+		fsm.Events{
+			{Name: "start_dhcp", Src: []string{"created", "eap_response_success_received", "dhcp_discovery_sent", "dhcp_request_sent", "dhcp_ack_received", "dhcp_failed"}, 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) {
+				service.logStateChange("DHCPState", e.Src, e.Dst)
+			},
+		},
+	)
+
+	return &service, nil
+}
+
+func (s *Service) HandleAuth(stream bbsimTypes.Stream) {
+
+	if !s.NeedsEapol {
+		serviceLogger.WithFields(log.Fields{
+			"OnuId":      s.Onu.ID,
+			"IntfId":     s.Onu.PonPortID,
+			"OnuSn":      s.Onu.Sn(),
+			"Name":       s.Name,
+			"NeedsEapol": s.NeedsEapol,
+		}).Debug("Won't start authentication as EAPOL is not required")
+		return
+	}
+
+	// TODO check if the EAPOL flow was received before starting auth
+
+	if err := s.EapolState.Event("start_auth"); err != nil {
+		serviceLogger.WithFields(log.Fields{
+			"OnuId":  s.Onu.ID,
+			"IntfId": s.Onu.PonPortID,
+			"OnuSn":  s.Onu.Sn(),
+			"Name":   s.Name,
+			"err":    err.Error(),
+		}).Error("Can't start auth for this Service")
+	} else {
+		if err := s.handleEapolStart(stream); err != nil {
+			serviceLogger.WithFields(log.Fields{
+				"OnuId":  s.Onu.ID,
+				"IntfId": s.Onu.PonPortID,
+				"OnuSn":  s.Onu.Sn(),
+				"Name":   s.Name,
+				"err":    err,
+			}).Error("Error while sending EapolStart packet")
+			_ = s.EapolState.Event("auth_failed")
+		}
+	}
+}
+
+func (s *Service) HandleDhcp(stream bbsimTypes.Stream, cTag int) {
+
+	// FIXME start dhcp only for the Service that matches the tag
+	if s.CTag != cTag {
+		serviceLogger.WithFields(log.Fields{
+			"OnuId":  s.Onu.ID,
+			"IntfId": s.Onu.PonPortID,
+			"OnuSn":  s.Onu.Sn(),
+			"Name":   s.Name,
+		}).Debug("DHCP flow is not for this service, ignoring")
+		return
+	}
+
+	// NOTE since we're matching the flow tag, this may not be required
+	if !s.NeedsDhcp {
+		serviceLogger.WithFields(log.Fields{
+			"OnuId":     s.Onu.ID,
+			"IntfId":    s.Onu.PonPortID,
+			"OnuSn":     s.Onu.Sn(),
+			"Name":      s.Name,
+			"NeedsDhcp": s.NeedsDhcp,
+		}).Debug("Won't start DHCP as it is not required")
+		return
+	}
+
+	// TODO check if the EAPOL flow was received before starting auth
+
+	if err := s.DHCPState.Event("start_dhcp"); err != nil {
+		serviceLogger.WithFields(log.Fields{
+			"OnuId":  s.Onu.ID,
+			"IntfId": s.Onu.PonPortID,
+			"OnuSn":  s.Onu.Sn(),
+			"Name":   s.Name,
+			"err":    err.Error(),
+		}).Error("Can't start DHCP for this Service")
+	} else {
+		if err := s.handleDHCPStart(stream); err != nil {
+			serviceLogger.WithFields(log.Fields{
+				"OnuId":  s.Onu.ID,
+				"IntfId": s.Onu.PonPortID,
+				"OnuSn":  s.Onu.Sn(),
+				"Name":   s.Name,
+				"err":    err,
+			}).Error("Error while sending DHCPDiscovery packet")
+			_ = s.DHCPState.Event("dhcp_failed")
+		}
+	}
+}
+
+func (s *Service) handleEapolStart(stream bbsimTypes.Stream) error {
+
+	serviceLogger.WithFields(log.Fields{
+		"OnuId":   s.Onu.ID,
+		"IntfId":  s.Onu.PonPortID,
+		"OnuSn":   s.Onu.Sn(),
+		"GemPort": s.GemPort,
+		"Name":    s.Name,
+	}).Debugf("handleEapolStart")
+
+	if err := eapol.SendEapStart(s.Onu.ID, s.Onu.PonPortID, s.Onu.Sn(), s.Onu.PortNo,
+		s.HwAddress, s.GemPort, s.EapolState, stream); err != nil {
+		serviceLogger.WithFields(log.Fields{
+			"OnuId":   s.Onu.ID,
+			"IntfId":  s.Onu.PonPortID,
+			"OnuSn":   s.Onu.Sn(),
+			"GemPort": s.GemPort,
+			"Name":    s.Name,
+		}).Error("handleEapolStart")
+		return err
+	}
+	return nil
+}
+
+func (s *Service) handleDHCPStart(stream bbsimTypes.Stream) error {
+
+	serviceLogger.WithFields(log.Fields{
+		"OnuId":     s.Onu.ID,
+		"IntfId":    s.Onu.PonPortID,
+		"OnuSn":     s.Onu.Sn(),
+		"Name":      s.Name,
+		"GemPortId": s.GemPort,
+	}).Debugf("HandleDHCPStart")
+
+	if err := dhcp.SendDHCPDiscovery(s.Onu.PonPort.Olt.ID, s.Onu.PonPortID, s.Onu.ID, int(s.CTag), s.GemPort,
+		s.Onu.Sn(), s.Onu.PortNo, s.DHCPState, s.HwAddress, stream); err != nil {
+		serviceLogger.WithFields(log.Fields{
+			"OnuId":     s.Onu.ID,
+			"IntfId":    s.Onu.PonPortID,
+			"OnuSn":     s.Onu.Sn(),
+			"Name":      s.Name,
+			"GemPortId": s.GemPort,
+		}).Error("HandleDHCPStart")
+		return err
+	}
+	return nil
+}
+
+func (s *Service) HandlePackets(stream bbsimTypes.Stream) {
+	serviceLogger.WithFields(log.Fields{
+		"OnuId":     s.Onu.ID,
+		"IntfId":    s.Onu.PonPortID,
+		"OnuSn":     s.Onu.Sn(),
+		"GemPortId": s.GemPort,
+		"Name":      s.Name,
+	}).Debug("Listening on Service Packet Channel")
+
+	defer func() {
+		serviceLogger.WithFields(log.Fields{
+			"OnuId":     s.Onu.ID,
+			"IntfId":    s.Onu.PonPortID,
+			"OnuSn":     s.Onu.Sn(),
+			"GemPortId": s.GemPort,
+			"Name":      s.Name,
+		}).Debug("Done Listening on Service Packet Channel")
+	}()
+
+	for msg := range s.PacketCh {
+		serviceLogger.WithFields(log.Fields{
+			"OnuId":       s.Onu.ID,
+			"IntfId":      s.Onu.PonPortID,
+			"OnuSn":       s.Onu.Sn(),
+			"Name":        s.Name,
+			"messageType": msg.Type,
+		}).Debug("Received message on Service Packet Channel")
+
+		if msg.Type == packetHandlers.EAPOL {
+			eapol.HandleNextPacket(msg.OnuId, msg.IntfId, s.GemPort, s.Onu.Sn(), s.Onu.PortNo, s.EapolState, msg.Packet, stream, nil)
+		} else if msg.Type == packetHandlers.DHCP {
+			_ = dhcp.HandleNextPacket(s.Onu.PonPort.Olt.ID, s.Onu.ID, s.Onu.PonPortID, s.Onu.Sn(), s.Onu.PortNo, s.CTag, s.GemPort, s.HwAddress, s.DHCPState, msg.Packet, stream)
+		}
+	}
+}
+
+func (s *Service) logStateChange(stateMachine string, src string, dst string) {
+	serviceLogger.WithFields(log.Fields{
+		"OnuId":  s.Onu.ID,
+		"IntfId": s.Onu.PonPortID,
+		"OnuSn":  s.Onu.Sn(),
+		"Name":   s.Name,
+	}).Debugf("Changing Service.%s InternalState from %s to %s", stateMachine, src, dst)
+}
diff --git a/internal/bbsim/responders/dhcp/dhcp.go b/internal/bbsim/responders/dhcp/dhcp.go
index 227ce8e..a0f5fff 100644
--- a/internal/bbsim/responders/dhcp/dhcp.go
+++ b/internal/bbsim/responders/dhcp/dhcp.go
@@ -29,13 +29,10 @@
 	"github.com/looplab/fsm"
 	"github.com/opencord/bbsim/internal/bbsim/packetHandlers"
 	bbsim "github.com/opencord/bbsim/internal/bbsim/types"
-	omci "github.com/opencord/omci-sim"
 	"github.com/opencord/voltha-protos/v2/go/openolt"
 	log "github.com/sirupsen/logrus"
 )
 
-var GetGemPortId = omci.GetGemPortId
-
 var dhcpLogger = log.WithFields(log.Fields{
 	"module": "DHCP",
 })
@@ -98,7 +95,7 @@
 	return opts
 }
 
-func createDHCPDisc(oltId int, intfId uint32, onuId uint32, macAddress net.HardwareAddr) *layers.DHCPv4 {
+func createDHCPDisc(oltId int, intfId uint32, onuId uint32, gemPort uint32, macAddress net.HardwareAddr) *layers.DHCPv4 {
 	dhcpLayer := createDefaultDHCPReq(oltId, intfId, onuId, macAddress)
 	defaultOpts := createDefaultOpts(intfId, onuId)
 	dhcpLayer.Options = append([]layers.DHCPOption{{
@@ -108,7 +105,7 @@
 	}}, defaultOpts...)
 
 	data := []byte{0xcd, 0x28, 0xcb, 0xcc, 0x00, 0x01, 0x00, 0x01,
-		0x23, 0xed, 0x11, 0xec, 0x4e, 0xfc, 0xcd, 0x28, byte(intfId), byte(onuId)} //FIXME use the OLT-ID in here
+		0x23, 0xed, 0x11, 0xec, 0x4e, 0xfc, 0xcd, byte(intfId), byte(onuId), byte(gemPort)} //FIXME use the OLT-ID in here
 	dhcpLayer.Options = append(dhcpLayer.Options, layers.DHCPOption{
 		Type:   layers.DHCPOptClientID,
 		Data:   data,
@@ -242,28 +239,19 @@
 	return dhcpMessageType.String(), nil
 }
 
-func sendDHCPPktIn(msg bbsim.ByteMsg, portNo uint32, stream bbsim.Stream) error {
-	// FIXME unify sendDHCPPktIn and sendEapolPktIn methods
-	gemid, err := 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
-	}
+func sendDHCPPktIn(msg bbsim.ByteMsg, portNo uint32, gemPortId uint32, stream bbsim.Stream) error {
 
 	log.WithFields(log.Fields{
 		"OnuId":   msg.OnuId,
 		"IntfId":  msg.IntfId,
-		"GemPort": gemid,
+		"GemPort": gemPortId,
 		"Type":    "DHCP",
 	}).Trace("sending-pkt")
 
 	data := &openolt.Indication_PktInd{PktInd: &openolt.PacketIndication{
 		IntfType:  "pon",
 		IntfId:    msg.IntfId,
-		GemportId: uint32(gemid),
+		GemportId: gemPortId,
 		Pkt:       msg.Bytes,
 		PortNo:    portNo,
 	}}
@@ -275,7 +263,7 @@
 	return nil
 }
 
-func sendDHCPRequest(oltId int, ponPortId uint32, onuId uint32, serialNumber string, portNo uint32, cTag int, onuStateMachine *fsm.FSM, onuHwAddress net.HardwareAddr, offeredIp net.IP, stream openolt.Openolt_EnableIndicationServer) error {
+func sendDHCPRequest(oltId int, ponPortId uint32, onuId uint32, serialNumber string, portNo uint32, cTag int, gemPortId uint32, onuStateMachine *fsm.FSM, onuHwAddress net.HardwareAddr, offeredIp net.IP, stream bbsim.Stream) error {
 	dhcp := createDHCPReq(oltId, ponPortId, onuId, onuHwAddress, offeredIp)
 	pkt, err := serializeDHCPPacket(ponPortId, onuId, cTag, onuHwAddress, dhcp)
 
@@ -300,7 +288,7 @@
 		Bytes:  pkt,
 	}
 
-	if err := sendDHCPPktIn(msg, portNo, stream); err != nil {
+	if err := sendDHCPPktIn(msg, portNo, gemPortId, stream); err != nil {
 		dhcpLogger.WithFields(log.Fields{
 			"OnuId":  onuId,
 			"IntfId": ponPortId,
@@ -333,8 +321,8 @@
 	return nil
 }
 
-func SendDHCPDiscovery(oltId int, ponPortId uint32, onuId uint32, serialNumber string, portNo uint32, onuStateMachine *fsm.FSM, onuHwAddress net.HardwareAddr, cTag int, stream bbsim.Stream) error {
-	dhcp := createDHCPDisc(oltId, ponPortId, onuId, onuHwAddress)
+func SendDHCPDiscovery(oltId int, ponPortId uint32, onuId uint32, cTag int, gemPortId uint32, serialNumber string, portNo uint32, stateMachine *fsm.FSM, onuHwAddress net.HardwareAddr, stream bbsim.Stream) error {
+	dhcp := createDHCPDisc(oltId, ponPortId, onuId, gemPortId, onuHwAddress)
 	pkt, err := serializeDHCPPacket(ponPortId, onuId, cTag, onuHwAddress, dhcp)
 
 	if err != nil {
@@ -343,7 +331,7 @@
 			"IntfId": ponPortId,
 			"OnuSn":  serialNumber,
 		}).Errorf("Cannot serializeDHCPPacket: %s", err)
-		if err := updateDhcpFailed(onuId, ponPortId, serialNumber, onuStateMachine); err != nil {
+		if err := updateDhcpFailed(onuId, ponPortId, serialNumber, stateMachine); err != nil {
 			return err
 		}
 		return err
@@ -357,13 +345,13 @@
 		Bytes:  pkt,
 	}
 
-	if err := sendDHCPPktIn(msg, portNo, stream); err != nil {
+	if err := sendDHCPPktIn(msg, portNo, gemPortId, stream); err != nil {
 		dhcpLogger.WithFields(log.Fields{
 			"OnuId":  onuId,
 			"IntfId": ponPortId,
 			"OnuSn":  serialNumber,
 		}).Errorf("Cannot sendDHCPPktIn: %s", err)
-		if err := updateDhcpFailed(onuId, ponPortId, serialNumber, onuStateMachine); err != nil {
+		if err := updateDhcpFailed(onuId, ponPortId, serialNumber, stateMachine); err != nil {
 			return err
 		}
 		return err
@@ -374,7 +362,7 @@
 		"OnuSn":  serialNumber,
 	}).Infof("DHCPDiscovery Sent")
 
-	if err := onuStateMachine.Event("dhcp_discovery_sent"); err != nil {
+	if err := stateMachine.Event("dhcp_discovery_sent"); err != nil {
 		dhcpLogger.WithFields(log.Fields{
 			"OnuId":  onuId,
 			"IntfId": ponPortId,
@@ -385,7 +373,7 @@
 }
 
 // FIXME cTag is not used here
-func HandleNextPacket(oltId int, onuId uint32, ponPortId uint32, serialNumber string, portNo uint32, onuHwAddress net.HardwareAddr, cTag int, onuStateMachine *fsm.FSM, pkt gopacket.Packet, stream openolt.Openolt_EnableIndicationServer) error {
+func HandleNextPacket(oltId int, onuId uint32, ponPortId uint32, serialNumber string, portNo uint32, cTag int, gemPortId uint32, onuHwAddress net.HardwareAddr, onuStateMachine *fsm.FSM, pkt gopacket.Packet, stream bbsim.Stream) error {
 
 	dhcpLayer, err := GetDhcpLayer(pkt)
 	if err != nil {
@@ -415,7 +403,7 @@
 	if dhcpLayer.Operation == layers.DHCPOpReply {
 		if dhcpMessageType == layers.DHCPMsgTypeOffer {
 			offeredIp := dhcpLayer.YourClientIP
-			if err := sendDHCPRequest(oltId, ponPortId, onuId, serialNumber, portNo, cTag, onuStateMachine, onuHwAddress, offeredIp, stream); err != nil {
+			if err := sendDHCPRequest(oltId, ponPortId, onuId, serialNumber, portNo, cTag, gemPortId, onuStateMachine, onuHwAddress, offeredIp, stream); err != nil {
 				dhcpLogger.WithFields(log.Fields{
 					"OnuId":  onuId,
 					"IntfId": ponPortId,
@@ -463,7 +451,7 @@
 
 // This method handle the BBR DHCP Packets
 // BBR does not need to do anything but forward the packets in the correct direction
-func HandleNextBbrPacket(onuId uint32, ponPortId uint32, serialNumber string, sTag int, macAddress net.HardwareAddr, doneChannel chan bool, pkt gopacket.Packet, client openolt.OpenoltClient) error {
+func HandleNextBbrPacket(onuId uint32, ponPortId uint32, serialNumber string, doneChannel chan bool, pkt gopacket.Packet, client openolt.OpenoltClient) error {
 
 	// check if the packet is going:
 	// - outgouing: toward the DHCP
@@ -521,7 +509,6 @@
 			"Type":   dhcpType,
 			"DstMac": dstMac,
 			"SrcMac": srcMac,
-			"OnuMac": macAddress,
 		}).Infof("Sent DHCP packet to the ONU")
 
 		dhcpLayer, _ := GetDhcpLayer(pkt)
@@ -532,8 +519,9 @@
 
 	} else {
 		// double tag the packet and send it to the NNI
-		// NOTE do we need this in the HandleDHCP Packet?
-		doubleTaggedPkt, err := packetHandlers.PushDoubleTag(sTag, sTag, pkt)
+		// we don't really care about the tags as they are stripped before
+		// the packet is sent to the DHCP server
+		doubleTaggedPkt, err := packetHandlers.PushDoubleTag(900, 900, pkt)
 		if err != nil {
 			dhcpLogger.WithFields(log.Fields{
 				"OnuId":  onuId,
@@ -564,7 +552,6 @@
 			"Type":   dhcpType,
 			"DstMac": dstMac,
 			"SrcMac": srcMac,
-			"OnuMac": macAddress,
 		}).Infof("Sent DHCP packet out of the NNI Port")
 	}
 	return nil
diff --git a/internal/bbsim/responders/dhcp/dhcp_test.go b/internal/bbsim/responders/dhcp/dhcp_test.go
index ff9b943..cc50188 100644
--- a/internal/bbsim/responders/dhcp/dhcp_test.go
+++ b/internal/bbsim/responders/dhcp/dhcp_test.go
@@ -62,27 +62,19 @@
 	dhcpStateMachine.SetState("dhcp_started")
 
 	var onuId uint32 = 1
-	var gemPortId uint16 = 1
+	var gemPortId uint32 = 1
 	var ponPortId uint32 = 0
 	var oltId int = 1
 	var serialNumber = "BBSM00000001"
 	var mac = net.HardwareAddr{0x2e, 0x60, 0x70, 0x13, byte(ponPortId), byte(onuId)}
 	var portNo uint32 = 16
 
-	// Save current function and restore at the end:
-	old := GetGemPortId
-	defer func() { GetGemPortId = old }()
-
-	GetGemPortId = func(intfId uint32, onuId uint32) (uint16, error) {
-		return gemPortId, nil
-	}
-
 	stream := &mockStreamSuccess{
 		Calls: make(map[int]*openolt.PacketIndication),
 		fail:  false,
 	}
 
-	if err := SendDHCPDiscovery(oltId, ponPortId, onuId, serialNumber, portNo, dhcpStateMachine, mac, 1, stream); err != nil {
+	if err := SendDHCPDiscovery(oltId, ponPortId, onuId, 900, gemPortId, serialNumber, portNo, dhcpStateMachine, mac, stream); err != nil {
 		t.Errorf("SendDHCPDiscovery returned an error: %v", err)
 		t.Fail()
 	}
diff --git a/internal/bbsim/responders/eapol/eapol.go b/internal/bbsim/responders/eapol/eapol.go
index cd0606e..5595d5b 100644
--- a/internal/bbsim/responders/eapol/eapol.go
+++ b/internal/bbsim/responders/eapol/eapol.go
@@ -38,16 +38,8 @@
 var eapolVersion uint8 = 1
 var GetGemPortId = omci.GetGemPortId
 
-func sendEapolPktIn(msg bbsim.ByteMsg, portNo uint32, stream openolt.Openolt_EnableIndicationServer) {
+func sendEapolPktIn(msg bbsim.ByteMsg, portNo uint32, gemid uint32, stream bbsim.Stream) {
 	// FIXME unify sendDHCPPktIn and sendEapolPktIn methods
-	gemid, err := omci.GetGemPortId(msg.IntfId, msg.OnuId)
-	if err != nil {
-		eapolLogger.WithFields(log.Fields{
-			"OnuId":  msg.OnuId,
-			"IntfId": msg.IntfId,
-		}).Errorf("Can't retrieve GemPortId: %s", err)
-		return
-	}
 
 	log.WithFields(log.Fields{
 		"OnuId":   msg.OnuId,
@@ -137,8 +129,8 @@
 	options := gopacket.SerializeOptions{}
 
 	ethernetLayer := &layers.Ethernet{
-		SrcMAC:       net.HardwareAddr{0x2e, 0x60, 0x70, 0x13, byte(intfId), byte(onuId)},
-		DstMAC:       net.HardwareAddr{0x01, 0x80, 0xC2, 0x00, 0x00, 0x03},
+		SrcMAC:       net.HardwareAddr{0x2e, 0x60, byte(0), byte(intfId), byte(onuId), byte(0)},
+		DstMAC:       net.HardwareAddr{0x2e, 0x60, byte(0), byte(intfId), byte(onuId), byte(0)},
 		EthernetType: layers.EthernetTypeEAPOL,
 	}
 
@@ -197,22 +189,7 @@
 	return nil
 }
 
-func SendEapStart(onuId uint32, ponPortId uint32, serialNumber string, portNo uint32, macAddress net.HardwareAddr, onuStateMachine *fsm.FSM, stream bbsim.Stream) error {
-
-	// send the packet (hacked together)
-	gemId, err := GetGemPortId(ponPortId, onuId)
-	if err != nil {
-		eapolLogger.WithFields(log.Fields{
-			"OnuId":  onuId,
-			"IntfId": ponPortId,
-			"OnuSn":  serialNumber,
-		}).Errorf("Can't retrieve GemPortId: %s", err)
-
-		if err := updateAuthFailed(onuId, ponPortId, serialNumber, onuStateMachine); err != nil {
-			return err
-		}
-		return err
-	}
+func SendEapStart(onuId uint32, ponPortId uint32, serialNumber string, portNo uint32, macAddress net.HardwareAddr, gemPort uint32, stateMachine *fsm.FSM, stream bbsim.Stream) error {
 
 	// TODO use createEAPOLPkt
 	buffer := gopacket.NewSerializeBuffer()
@@ -236,13 +213,13 @@
 		PktInd: &openolt.PacketIndication{
 			IntfType:  "pon",
 			IntfId:    ponPortId,
-			GemportId: uint32(gemId),
+			GemportId: gemPort,
 			Pkt:       msg,
 			PortNo:    portNo,
 		},
 	}
 
-	err = stream.Send(&openolt.Indication{Data: data})
+	err := stream.Send(&openolt.Indication{Data: data})
 	if err != nil {
 		eapolLogger.WithFields(log.Fields{
 			"OnuId":  onuId,
@@ -250,7 +227,7 @@
 			"OnuSn":  serialNumber,
 		}).Errorf("Can't send EapStart Message: %s", err)
 
-		if err := updateAuthFailed(onuId, ponPortId, serialNumber, onuStateMachine); err != nil {
+		if err := updateAuthFailed(onuId, ponPortId, serialNumber, stateMachine); err != nil {
 			return err
 		}
 		return err
@@ -263,7 +240,7 @@
 		"PortNo": portNo,
 	}).Debugf("Sent EapStart packet")
 
-	if err := onuStateMachine.Event("eap_start_sent"); err != nil {
+	if err := stateMachine.Event("eap_start_sent"); err != nil {
 		eapolLogger.WithFields(log.Fields{
 			"OnuId":  onuId,
 			"IntfId": ponPortId,
@@ -274,7 +251,7 @@
 	return nil
 }
 
-func HandleNextPacket(onuId uint32, ponPortId uint32, serialNumber string, portNo uint32, onuStateMachine *fsm.FSM, pkt gopacket.Packet, stream openolt.Openolt_EnableIndicationServer, client openolt.OpenoltClient) {
+func HandleNextPacket(onuId uint32, ponPortId uint32, gemPortId uint32, serialNumber string, portNo uint32, stateMachine *fsm.FSM, pkt gopacket.Packet, stream bbsim.Stream, client openolt.OpenoltClient) {
 
 	eap, eapErr := extractEAP(pkt)
 
@@ -335,14 +312,14 @@
 			Bytes:  pkt,
 		}
 
-		sendEapolPktIn(msg, portNo, stream)
+		sendEapolPktIn(msg, portNo, gemPortId, stream)
 		eapolLogger.WithFields(log.Fields{
 			"OnuId":  onuId,
 			"IntfId": ponPortId,
 			"OnuSn":  serialNumber,
 			"PortNo": portNo,
 		}).Debugf("Sent EAPIdentityResponse packet")
-		if err := onuStateMachine.Event("eap_response_identity_sent"); err != nil {
+		if err := stateMachine.Event("eap_response_identity_sent"); err != nil {
 			eapolLogger.WithFields(log.Fields{
 				"OnuId":  onuId,
 				"IntfId": ponPortId,
@@ -383,14 +360,14 @@
 			Bytes:  pkt,
 		}
 
-		sendEapolPktIn(msg, portNo, stream)
+		sendEapolPktIn(msg, portNo, gemPortId, stream)
 		eapolLogger.WithFields(log.Fields{
 			"OnuId":  onuId,
 			"IntfId": ponPortId,
 			"OnuSn":  serialNumber,
 			"PortNo": portNo,
 		}).Debugf("Sent EAPChallengeResponse packet")
-		if err := onuStateMachine.Event("eap_response_challenge_sent"); err != nil {
+		if err := stateMachine.Event("eap_response_challenge_sent"); err != nil {
 			eapolLogger.WithFields(log.Fields{
 				"OnuId":  onuId,
 				"IntfId": ponPortId,
@@ -417,7 +394,7 @@
 			"OnuSn":  serialNumber,
 		}).Infof("Sent EAP Success packet")
 
-		if err := onuStateMachine.Event("send_dhcp_flow"); err != nil {
+		if err := stateMachine.Event("send_dhcp_flow"); err != nil {
 			eapolLogger.WithFields(log.Fields{
 				"OnuId":  onuId,
 				"IntfId": ponPortId,
@@ -431,7 +408,7 @@
 			"OnuSn":  serialNumber,
 			"PortNo": portNo,
 		}).Debugf("Received EAPSuccess packet")
-		if err := onuStateMachine.Event("eap_response_success_received"); err != nil {
+		if err := stateMachine.Event("eap_response_success_received"); err != nil {
 			eapolLogger.WithFields(log.Fields{
 				"OnuId":  onuId,
 				"IntfId": ponPortId,
diff --git a/internal/bbsim/responders/eapol/eapol_test.go b/internal/bbsim/responders/eapol/eapol_test.go
index 9f8ff22..824ce65 100644
--- a/internal/bbsim/responders/eapol/eapol_test.go
+++ b/internal/bbsim/responders/eapol/eapol_test.go
@@ -43,7 +43,7 @@
 
 // params for the function under test
 var onuId uint32 = 1
-var gemPortId uint16 = 1
+var gemPortId uint32 = 1
 var ponPortId uint32 = 0
 var serialNumber string = "BBSM00000001"
 var macAddress = net.HardwareAddr{0x01, 0x80, 0xC2, 0x00, 0x00, 0x03}
@@ -70,20 +70,12 @@
 func TestSendEapStartSuccess(t *testing.T) {
 	eapolStateMachine.SetState("auth_started")
 
-	// Save current function and restore at the end:
-	old := GetGemPortId
-	defer func() { GetGemPortId = old }()
-
-	GetGemPortId = func(intfId uint32, onuId uint32) (uint16, error) {
-		return gemPortId, nil
-	}
-
 	stream := &mockStream{
 		Calls: make(map[int]*openolt.PacketIndication),
 		fail:  false,
 	}
 
-	if err := SendEapStart(onuId, ponPortId, serialNumber, portNo, macAddress, eapolStateMachine, stream); err != nil {
+	if err := SendEapStart(onuId, ponPortId, serialNumber, portNo, macAddress, gemPortId, eapolStateMachine, stream); err != nil {
 		t.Errorf("SendEapStart returned an error: %v", err)
 		t.Fail()
 	}
@@ -98,35 +90,6 @@
 
 }
 
-func TestSendEapStartFailNoGemPort(t *testing.T) {
-	eapolStateMachine.SetState("auth_started")
-
-	// Save current function and restore at the end:
-	old := GetGemPortId
-	defer func() { GetGemPortId = old }()
-
-	GetGemPortId = func(intfId uint32, onuId uint32) (uint16, error) {
-		return 0, errors.New("no-gem-port")
-	}
-
-	var macAddress = net.HardwareAddr{0x01, 0x80, 0xC2, 0x00, 0x00, 0x03}
-
-	stream := &mockStream{
-		Calls: make(map[int]*openolt.PacketIndication),
-		fail:  false,
-	}
-
-	err := SendEapStart(onuId, ponPortId, serialNumber, portNo, macAddress, eapolStateMachine, stream)
-	if err == nil {
-		t.Errorf("SendEapStart did not return an error")
-		t.Fail()
-	}
-
-	assert.Equal(t, err.Error(), "no-gem-port")
-
-	assert.Equal(t, eapolStateMachine.Current(), "auth_failed")
-}
-
 func TestSendEapStartFailStreamError(t *testing.T) {
 
 	eapolStateMachine.SetState("auth_started")
@@ -144,7 +107,7 @@
 		fail:  true,
 	}
 
-	err := SendEapStart(onuId, ponPortId, serialNumber, portNo, macAddress, eapolStateMachine, stream)
+	err := SendEapStart(onuId, ponPortId, serialNumber, portNo, macAddress, gemPortId, eapolStateMachine, stream)
 	if err == nil {
 		t.Errorf("SendEapStart did not return an error")
 		t.Fail()
diff --git a/internal/bbsim/responders/sadis/sadis.go b/internal/bbsim/responders/sadis/sadis.go
index 75a6fd9..444fd57 100644
--- a/internal/bbsim/responders/sadis/sadis.go
+++ b/internal/bbsim/responders/sadis/sadis.go
@@ -76,27 +76,16 @@
 	UplinkPort         int    `json:"uplinkPort"`
 }
 
-type SadisOnuEntry struct {
-	ID                         string `json:"id"`
-	CTag                       int    `json:"cTag"`
-	STag                       int    `json:"sTag"`
-	NasPortID                  string `json:"nasPortId"`
-	CircuitID                  string `json:"circuitId"`
-	RemoteID                   string `json:"remoteId"`
-	TechnologyProfileID        int    `json:"technologyProfileId"`
-	UpstreamBandwidthProfile   string `json:"upstreamBandwidthProfile"`
-	DownstreamBandwidthProfile string `json:"downstreamBandwidthProfile"`
-}
-
 type SadisOnuEntryV2 struct {
 	ID         string        `json:"id"`
 	NasPortID  string        `json:"nasPortId"`
 	CircuitID  string        `json:"circuitId"`
 	RemoteID   string        `json:"remoteId"`
-	UniTagList []interface{} `json:"uniTagList"` // this can be SadisUniTagAtt, SadisUniTagDt
+	UniTagList []SadisUniTag `json:"uniTagList"` // this can be SadisUniTagAtt, SadisUniTagDt
 }
 
-type SadisUniTagAtt struct {
+type SadisUniTag struct {
+	UniTagMatch                int    `json:"uniTagMatch,omitempty"`
 	PonCTag                    int    `json:"ponCTag,omitempty"`
 	PonSTag                    int    `json:"ponSTag,omitempty"`
 	TechnologyProfileID        int    `json:"technologyProfileId,omitempty"`
@@ -104,15 +93,12 @@
 	DownstreamBandwidthProfile string `json:"downstreamBandwidthProfile,omitempty"`
 	IsDhcpRequired             bool   `json:"isDhcpRequired,omitempty"`
 	IsIgmpRequired             bool   `json:"isIgmpRequired,omitempty"`
-}
-
-type SadisUniTagDt struct {
-	UniTagMatch                int    `json:"uniTagMatch,omitempty"`
-	PonCTag                    int    `json:"ponCTag,omitempty"`
-	PonSTag                    int    `json:"ponSTag,omitempty"`
-	TechnologyProfileID        int    `json:"technologyProfileId,omitempty"`
-	UpstreamBandwidthProfile   string `json:"upstreamBandwidthProfile,omitempty"`
-	DownstreamBandwidthProfile string `json:"downstreamBandwidthProfile,omitempty"`
+	ConfiguredMacAddress       string `json:"configuredMacAddress,omitempty"`
+	UsPonCTagPriority          int    `json:"usPonCTagPriority,omitempty"`
+	UsPonSTagPriority          int    `json:"usPonSTagPriority,omitempty"`
+	DsPonCTagPriority          int    `json:"dsPonCTagPriority,omitempty"`
+	DsPonSTagPriority          int    `json:"dsPonSTagPriority,omitempty"`
+	ServiceName                string `json:"serviceName,omitempty"`
 }
 
 // SADIS BandwithProfile Entry
@@ -143,7 +129,7 @@
 	entries := []interface{}{}
 	entries = append(entries, solt)
 
-	a := strings.Split(common.Options.BBSim.SadisRestAddress, ":")
+	a := strings.Split(common.Config.BBSim.SadisRestAddress, ":")
 	port := a[len(a)-1]
 
 	integration := SadisIntegration{}
@@ -164,7 +150,7 @@
 	ip, _ := common.GetIPAddr("nni") // TODO verify which IP to report
 	solt := &SadisOltEntry{
 		ID:                 olt.SerialNumber,
-		HardwareIdentifier: common.Options.Olt.DeviceId,
+		HardwareIdentifier: common.Config.Olt.DeviceId,
 		IPAddress:          ip,
 		NasID:              olt.SerialNumber,
 		UplinkPort:         1048576, // TODO currently assumes we only have one NNI port
@@ -172,22 +158,6 @@
 	return solt, nil
 }
 
-func GetOnuEntryV1(olt *devices.OltDevice, onu *devices.Onu, uniId string) (*SadisOnuEntry, error) {
-	uniSuffix := "-" + uniId
-	sonu := &SadisOnuEntry{
-		ID:                         onu.Sn() + uniSuffix,
-		CTag:                       onu.CTag,
-		STag:                       onu.STag,
-		NasPortID:                  onu.Sn() + uniSuffix,
-		CircuitID:                  onu.Sn() + uniSuffix,
-		RemoteID:                   olt.SerialNumber,
-		TechnologyProfileID:        64,
-		UpstreamBandwidthProfile:   "User_Bandwidth1",
-		DownstreamBandwidthProfile: "Default",
-	}
-	return sonu, nil
-}
-
 func GetOnuEntryV2(olt *devices.OltDevice, onu *devices.Onu, uniId string) (*SadisOnuEntryV2, error) {
 	uniSuffix := "-" + uniId
 
@@ -198,42 +168,54 @@
 		RemoteID:  onu.Sn() + uniSuffix,
 	}
 
-	// base structure common to all use cases
-	var sonuUniTag interface{}
+	// createUniTagList
+	for _, s := range onu.Services {
 
-	// set workflow specific params
-	switch common.Options.BBSim.SadisFormat {
-	case common.SadisFormatAtt:
-		sonuUniTag = SadisUniTagAtt{
-			PonCTag:             onu.CTag,
-			PonSTag:             onu.STag,
-			TechnologyProfileID: 64,
-			// NOTE do we want to select a random bandwidth profile?
-			// if so use bandwidthProfiles[rand.Intn(len(bandwidthProfiles))].ID
-			UpstreamBandwidthProfile:   "Default",
-			DownstreamBandwidthProfile: "User_Bandwidth1",
-			IsDhcpRequired:             common.Options.BBSim.EnableDhcp,
-			IsIgmpRequired:             common.Options.BBSim.EnableIgmp,
+		service := s.(*devices.Service)
+
+		tag := SadisUniTag{
+			ServiceName:                service.Name,
+			IsIgmpRequired:             service.NeedsIgmp,
+			IsDhcpRequired:             service.NeedsDhcp,
+			TechnologyProfileID:        service.TechnologyProfileID,
+			UpstreamBandwidthProfile:   "User_Bandwidth1",
+			DownstreamBandwidthProfile: "User_Bandwidth2",
+			PonCTag:                    service.CTag,
+			PonSTag:                    service.STag,
 		}
-	case common.SadisFormatDt:
-		sonuUniTag = SadisUniTagDt{
-			PonCTag:             4096,
-			PonSTag:             onu.STag,
-			TechnologyProfileID: 64,
-			// NOTE do we want to select a random bandwidth profile?
-			// if so use bandwidthProfiles[rand.Intn(len(bandwidthProfiles))].ID
-			UpstreamBandwidthProfile:   "Default",
-			DownstreamBandwidthProfile: "User_Bandwidth1",
-			UniTagMatch:                4096,
+
+		if service.UniTagMatch != 0 {
+			tag.UniTagMatch = service.UniTagMatch
 		}
+
+		if service.ConfigureMacAddress {
+			tag.ConfiguredMacAddress = service.HwAddress.String()
+		}
+
+		if service.UsPonCTagPriority != 0 {
+			tag.UsPonCTagPriority = service.UsPonCTagPriority
+		}
+
+		if service.UsPonSTagPriority != 0 {
+			tag.UsPonSTagPriority = service.UsPonSTagPriority
+		}
+
+		if service.DsPonCTagPriority != 0 {
+			tag.DsPonCTagPriority = service.DsPonCTagPriority
+		}
+
+		if service.DsPonSTagPriority != 0 {
+			tag.DsPonSTagPriority = service.DsPonSTagPriority
+		}
+
+		sonuv2.UniTagList = append(sonuv2.UniTagList, tag)
 	}
 
-	sonuv2.UniTagList = append(sonuv2.UniTagList, sonuUniTag)
 	return sonuv2, nil
 }
 
 func getBWPEntries(version string) *BandwidthProfileEntries {
-	a := strings.Split(common.Options.BBSim.SadisRestAddress, ":")
+	a := strings.Split(common.Config.BBSim.SadisRestAddress, ":")
 	port := a[len(a)-1]
 
 	integration := SadisIntegration{}
@@ -278,14 +260,9 @@
 	sadisConf.Sadis.Integration.URL = ""
 	for i := range s.olt.Pons {
 		for _, onu := range s.olt.Pons[i].Onus {
-			// FIXME currently we only support one UNI per ONU
-			if vars["version"] == "v1" {
-				sonuV1, _ := GetOnuEntryV1(s.olt, onu, "1")
-				sadisConf.Sadis.Entries = append(sadisConf.Sadis.Entries, sonuV1)
-			} else if vars["version"] == "v2" {
+			if vars["version"] == "v2" {
 				sonuV2, _ := GetOnuEntryV2(s.olt, onu, "1")
 				sadisConf.Sadis.Entries = append(sadisConf.Sadis.Entries, sonuV2)
-
 			}
 		}
 	}
@@ -343,11 +320,12 @@
 		"OnuPortNo": uni,
 	}).Debug("Received SADIS request")
 
-	w.WriteHeader(http.StatusOK)
 	if vars["version"] == "v1" {
-		sadisConf, _ := GetOnuEntryV1(s.olt, onu, uni)
-		_ = json.NewEncoder(w).Encode(sadisConf)
+		// TODO format error
+		w.WriteHeader(http.StatusBadRequest)
+		_ = json.NewEncoder(w).Encode("Sadis v1 is not supported anymore, please go back to an earlier BBSim version")
 	} else if vars["version"] == "v2" {
+		w.WriteHeader(http.StatusOK)
 		sadisConf, _ := GetOnuEntryV2(s.olt, onu, uni)
 		_ = json.NewEncoder(w).Encode(sadisConf)
 	}
@@ -381,7 +359,7 @@
 
 // StartRestServer starts REST server which returns a SADIS configuration for the currently simulated OLT
 func StartRestServer(olt *devices.OltDevice, wg *sync.WaitGroup) {
-	addr := common.Options.BBSim.SadisRestAddress
+	addr := common.Config.BBSim.SadisRestAddress
 	sadisLogger.Infof("SADIS server listening on %s", addr)
 	s := &sadisServer{
 		olt: olt,
diff --git a/internal/bbsim/responders/sadis/sadis_test.go b/internal/bbsim/responders/sadis/sadis_test.go
index 04e0240..27e43bc 100644
--- a/internal/bbsim/responders/sadis/sadis_test.go
+++ b/internal/bbsim/responders/sadis/sadis_test.go
@@ -22,7 +22,6 @@
 	"testing"
 
 	"github.com/opencord/bbsim/internal/bbsim/devices"
-	"github.com/opencord/bbsim/internal/common"
 	"gotest.tools/assert"
 )
 
@@ -34,89 +33,78 @@
 	onu := &devices.Onu{
 		ID:        1,
 		PonPortID: 1,
-		STag:      900,
-		CTag:      923,
-		HwAddress: net.HardwareAddr{0x2e, 0x60, 0x70, 0x13, byte(1), byte(1)},
 		PortNo:    0,
 	}
+
+	mac := net.HardwareAddr{0x2e, 0x60, 0x01, byte(1), byte(1), byte(0)}
+
 	onu.SerialNumber = onu.NewSN(0, onu.PonPortID, onu.ID)
+	onu.Services = []devices.ServiceIf{
+		&devices.Service{Name: "hsia", CTag: 923, STag: 900, NeedsEapol: true, NeedsDhcp: true, NeedsIgmp: true, HwAddress: mac, TechnologyProfileID: 64},
+	}
 
 	return olt, onu
 }
 
-func TestSadisServer_GetOnuEntryV1(t *testing.T) {
+func TestSadisServer_GetOnuEntryV2(t *testing.T) {
 
 	olt, onu := createMockDevices()
 
 	uni := "1"
 
-	res, err := GetOnuEntryV1(olt, onu, uni)
-	if err != nil {
-		t.Fatal(err)
-	}
+	entry, err := GetOnuEntryV2(olt, onu, uni)
 
-	assert.Equal(t, res.ID, fmt.Sprintf("%s-%s", onu.Sn(), uni))
-	assert.Equal(t, res.CTag, 923)
-	assert.Equal(t, res.STag, 900)
-	assert.Equal(t, res.RemoteID, string(olt.SerialNumber))
-	assert.Equal(t, res.DownstreamBandwidthProfile, "Default")
-	assert.Equal(t, res.UpstreamBandwidthProfile, "User_Bandwidth1")
-	assert.Equal(t, res.TechnologyProfileID, 64)
+	assert.NilError(t, err)
 
+	assert.Equal(t, entry.ID, fmt.Sprintf("%s-%s", onu.Sn(), uni))
+	assert.Equal(t, entry.RemoteID, fmt.Sprintf("%s-%s", onu.Sn(), uni))
+
+	assert.Equal(t, entry.UniTagList[0].PonCTag, 923)
+	assert.Equal(t, entry.UniTagList[0].PonSTag, 900)
+	assert.Equal(t, entry.UniTagList[0].DownstreamBandwidthProfile, "User_Bandwidth2")
+	assert.Equal(t, entry.UniTagList[0].UpstreamBandwidthProfile, "User_Bandwidth1")
+	assert.Equal(t, entry.UniTagList[0].TechnologyProfileID, 64)
+	assert.Equal(t, entry.UniTagList[0].IsDhcpRequired, true)
+	assert.Equal(t, entry.UniTagList[0].IsIgmpRequired, true)
 }
 
-func TestSadisServer_GetOnuEntryV2_Att(t *testing.T) {
+func TestSadisServer_GetOnuEntryV2_multi_service(t *testing.T) {
+
+	mac := net.HardwareAddr{0x2e, 0x60, byte(1), byte(1), byte(1), byte(2)}
+
+	hsia := devices.Service{Name: "hsia", HwAddress: net.HardwareAddr{0x2e, 0x60, byte(1), byte(1), byte(1), byte(1)},
+		CTag: 900, STag: 900, TechnologyProfileID: 64}
+
+	voip := devices.Service{Name: "voip", HwAddress: mac,
+		CTag: 901, STag: 900, TechnologyProfileID: 65}
+
+	vod := devices.Service{Name: "vod", HwAddress: net.HardwareAddr{0x2e, 0x60, byte(1), byte(1), byte(1), byte(3)},
+		CTag: 902, STag: 900, TechnologyProfileID: 66}
+
 	olt, onu := createMockDevices()
 
+	onu.Services = []devices.ServiceIf{&hsia, &voip, &vod}
+
 	uni := "1"
 
-	res, err := GetOnuEntryV2(olt, onu, uni)
-	if err != nil {
-		t.Fatal(err)
-	}
+	entry, err := GetOnuEntryV2(olt, onu, uni)
 
-	assert.Equal(t, res.ID, fmt.Sprintf("%s-%s", onu.Sn(), uni))
-	assert.Equal(t, res.RemoteID, fmt.Sprintf("%s-%s", onu.Sn(), uni))
+	assert.NilError(t, err)
 
-	// assert the correct type
-	uniTagList, ok := res.UniTagList[0].(SadisUniTagAtt)
-	if !ok {
-		t.Fatal("UniTagList has the wrong type")
-	}
+	assert.Equal(t, entry.ID, fmt.Sprintf("%s-%s", onu.Sn(), uni))
+	assert.Equal(t, entry.RemoteID, fmt.Sprintf("%s-%s", onu.Sn(), uni))
 
-	assert.Equal(t, uniTagList.PonCTag, 923)
-	assert.Equal(t, uniTagList.PonSTag, 900)
-	assert.Equal(t, uniTagList.DownstreamBandwidthProfile, "User_Bandwidth1")
-	assert.Equal(t, uniTagList.UpstreamBandwidthProfile, "Default")
-	assert.Equal(t, uniTagList.TechnologyProfileID, 64)
-	assert.Equal(t, uniTagList.IsDhcpRequired, false)
-	assert.Equal(t, uniTagList.IsIgmpRequired, false)
-}
+	assert.Equal(t, len(entry.UniTagList), 3)
 
-func TestSadisServer_GetOnuEntryV2_Dt(t *testing.T) {
-	common.Options.BBSim.SadisFormat = common.SadisFormatDt
-	olt, onu := createMockDevices()
+	assert.Equal(t, entry.UniTagList[0].PonCTag, 900)
+	assert.Equal(t, entry.UniTagList[0].PonSTag, 900)
+	assert.Equal(t, entry.UniTagList[0].TechnologyProfileID, 64)
 
-	uni := "1"
+	assert.Equal(t, entry.UniTagList[1].PonCTag, 901)
+	assert.Equal(t, entry.UniTagList[1].PonSTag, 900)
+	assert.Equal(t, entry.UniTagList[1].TechnologyProfileID, 65)
 
-	res, err := GetOnuEntryV2(olt, onu, uni)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	assert.Equal(t, res.ID, fmt.Sprintf("%s-%s", onu.Sn(), uni))
-	assert.Equal(t, res.RemoteID, fmt.Sprintf("%s-%s", onu.Sn(), uni))
-
-	// assert the correct type
-	uniTagList, ok := res.UniTagList[0].(SadisUniTagDt)
-	if !ok {
-		t.Fatal("UniTagList has the wrong type")
-	}
-
-	assert.Equal(t, uniTagList.PonCTag, 4096)
-	assert.Equal(t, uniTagList.PonSTag, 900)
-	assert.Equal(t, uniTagList.DownstreamBandwidthProfile, "User_Bandwidth1")
-	assert.Equal(t, uniTagList.UpstreamBandwidthProfile, "Default")
-	assert.Equal(t, uniTagList.TechnologyProfileID, 64)
-	assert.Equal(t, uniTagList.UniTagMatch, 4096)
+	assert.Equal(t, entry.UniTagList[2].PonCTag, 902)
+	assert.Equal(t, entry.UniTagList[2].PonSTag, 900)
+	assert.Equal(t, entry.UniTagList[2].TechnologyProfileID, 66)
 }