diff --git a/dataplane/dplanerc/BUILD b/dataplane/dplanerc/BUILD index 5cd515c6..7fe197a2 100644 --- a/dataplane/dplanerc/BUILD +++ b/dataplane/dplanerc/BUILD @@ -23,6 +23,7 @@ go_library( ] + select({ "@io_bazel_rules_go//go/platform:android": [ "//dataplane/kernel", + "//dataplane/protocol/lldp", "//gnmi/gnmiclient", "//gnmi/oc", "//gnmi/oc/ocpath", @@ -33,6 +34,7 @@ go_library( ], "@io_bazel_rules_go//go/platform:linux": [ "//dataplane/kernel", + "//dataplane/protocol/lldp", "//gnmi/gnmiclient", "//gnmi/oc", "//gnmi/oc/ocpath", diff --git a/dataplane/dplanerc/interface.go b/dataplane/dplanerc/interface.go index db7d0c9e..15e65922 100644 --- a/dataplane/dplanerc/interface.go +++ b/dataplane/dplanerc/interface.go @@ -33,6 +33,7 @@ import ( "google.golang.org/protobuf/proto" "github.com/openconfig/lemming/dataplane/kernel" + "github.com/openconfig/lemming/dataplane/protocol/lldp" "github.com/openconfig/lemming/gnmi/gnmiclient" "github.com/openconfig/lemming/gnmi/oc" "github.com/openconfig/lemming/gnmi/oc/ocpath" @@ -97,6 +98,10 @@ func (r routeMap) findRoute(ipPrefix string, vrfID uint64) *routeData { return r[key] } +type protocolHanlder interface { + Reconcile(context.Context, *oc.Root, *ygnmi.Client) error +} + // Reconciler handles config updates to the paths. type Reconciler struct { c *ygnmi.Client @@ -113,6 +118,7 @@ type Reconciler struct { nextHopGroupClient saipb.NextHopGroupClient lagClient saipb.LagClient stateMu sync.RWMutex + lldp protocolHanlder // state keeps track of the applied state of the device's interfaces so that we do not issue duplicate configuration commands to the device's interfaces. state map[string]*oc.Interface switchID uint64 @@ -162,6 +168,7 @@ func New(conn grpc.ClientConnInterface, switchID, cpuPortID uint64, contextID st nextHopGroupClient: saipb.NewNextHopGroupClient(conn), fwdClient: fwdpb.NewForwardingClient(conn), lagClient: saipb.NewLagClient(conn), + lldp: lldp.New(), } return r } @@ -186,17 +193,24 @@ func (ni *Reconciler) StartInterface(ctx context.Context, client *ygnmi.Client) ocpath.Root().InterfaceAny().Subinterface(0).Ipv6().AddressAny().PrefixLength().Config().PathStruct(), ocpath.Root().InterfaceAny().Aggregation().LagType().Config().PathStruct(), ocpath.Root().InterfaceAny().Ethernet().AggregateId().Config().PathStruct(), + ocpath.Root().Lldp().Enabled().Config().PathStruct(), + ocpath.Root().Lldp().InterfaceAny().Config().PathStruct(), ) cancelCtx, cancelFn := context.WithCancel(ctx) watcher := ygnmi.Watch(cancelCtx, ni.c, b.Config(), func(val *ygnmi.Value[*oc.Root]) error { log.V(2).Info("reconciling interfaces") root, ok := val.Val() - if !ok || root.Interface == nil { + if !ok || (root.Interface == nil && root.Lldp.Interface == nil) { return ygnmi.Continue } - for _, i := range root.Interface { - ni.reconcile(cancelCtx, i) + if root.Interface != nil { + for _, i := range root.Interface { + ni.reconcile(cancelCtx, i) + } + } + if root.Lldp.Interface != nil { + ni.reconcileLldp(cancelCtx, root) } return ygnmi.Continue }) @@ -501,6 +515,13 @@ func (ni *Reconciler) setMinLinks(intf ocInterface, data *interfaceData, minLink return nil } +// reconcileLldp compares the LLDP config with state and modifies state to match config. +func (ni *Reconciler) reconcileLldp(ctx context.Context, intent *oc.Root) { + if err := ni.lldp.Reconcile(ctx, intent, ni.c); err != nil { + log.Warningf("error found LLDP reconciliation: %v", err) + } +} + // reconcile compares the interface config with state and modifies state to match config. func (ni *Reconciler) reconcile(ctx context.Context, config *oc.Interface) { ni.stateMu.RLock() @@ -711,6 +732,7 @@ func (ni *Reconciler) handleDataplaneEvent(ctx context.Context, resp *saipb.Port } // handleLinkUpdate modifies the state based on changes to link state. +// This is the callback from netlink. func (ni *Reconciler) handleLinkUpdate(ctx context.Context, lu *netlink.LinkUpdate) { ni.stateMu.Lock() defer ni.stateMu.Unlock() diff --git a/dataplane/protocol/lldp/BUILD b/dataplane/protocol/lldp/BUILD new file mode 100644 index 00000000..61e75d69 --- /dev/null +++ b/dataplane/protocol/lldp/BUILD @@ -0,0 +1,29 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "lldp", + srcs = ["lldp.go"], + importpath = "github.com/openconfig/lemming/dataplane/protocol/lldp", + visibility = ["//visibility:public"], + deps = [ + "//dataplane/proto/packetio", + "//gnmi/gnmiclient", + "//gnmi/oc", + "//gnmi/oc/ocpath", + "@com_github_golang_glog//:glog", + "@com_github_google_gopacket//:gopacket", + "@com_github_google_gopacket//layers", + "@com_github_openconfig_ygnmi//ygnmi", + ], +) + +go_test( + name = "lldp_test", + srcs = ["lldp_test.go"], + embed = [":lldp"], + deps = [ + "//dataplane/proto/packetio", + "@com_github_google_go_cmp//cmp", + "@com_github_openconfig_gnmi//errdiff", + ], +) diff --git a/dataplane/protocol/lldp/lldp.go b/dataplane/protocol/lldp/lldp.go new file mode 100644 index 00000000..b3bfbe96 --- /dev/null +++ b/dataplane/protocol/lldp/lldp.go @@ -0,0 +1,279 @@ +// Copyright 2024 Google LLC +// +// 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 lldp + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/openconfig/ygnmi/ygnmi" + + "github.com/openconfig/lemming/gnmi/gnmiclient" + "github.com/openconfig/lemming/gnmi/oc" + + log "github.com/golang/glog" + + "github.com/openconfig/lemming/gnmi/oc/ocpath" + + "github.com/openconfig/lemming/dataplane/proto/packetio" +) + +// Daemon is the implementation of the LLDP protocol. +type Daemon struct { + enabled bool // whether LLDP is enabled globally + portEnabled map[string]bool // contains the enabled ports + portDaemons map[string]*portDaemon // tracks the active port daemons +} + +// New returns the main procotol handler. +func New() *Daemon { + return &Daemon{ + portEnabled: map[string]bool{}, + portDaemons: map[string]*portDaemon{}, + } +} + +// Start starts the procotol handler. +func (d *Daemon) Start() { + if d.portDaemons == nil { + d.portDaemons = map[string]*portDaemon{} + } + d.enabled = true +} + +// Stop stops the procotol handler by stopping all port daemons. +func (d *Daemon) Stop() { + for _, p := range d.portDaemons { + p.Stop() + } + d.enabled = false + d.portDaemons = nil +} + +// changePortState tries to change the port state and returns whether the state is actually changed. +func (d *Daemon) changePortState(intf *oc.Lldp_Interface) bool { + // Update the PD state. + wantActive := d.enabled && intf.GetEnabled() + pd, currActive := d.portDaemons[intf.GetName()] + switch { + case !currActive && wantActive: + d.portDaemons[intf.GetName()] = newPortDaemon(intf.GetName()) + case currActive && !wantActive: + pd.Stop() + delete(d.portDaemons, intf.GetName()) + } + + // Update the enabled state. + state, ok := d.portEnabled[intf.GetName()] + if !ok { + d.portEnabled[intf.GetName()] = false + } + if state != intf.GetEnabled() { + d.portEnabled[intf.GetName()] = intf.GetEnabled() + return true + } + return false +} + +// Reconcile reconciles LLDP for all ports. +func (d *Daemon) Reconcile(ctx context.Context, intent *oc.Root, c *ygnmi.Client) error { + sb := &ygnmi.SetBatch{} + if wantEnabled := intent.Lldp.GetEnabled(); d.enabled != wantEnabled { + d.enabled = wantEnabled + gnmiclient.BatchUpdate(sb, ocpath.Root().Lldp().Enabled().State(), d.enabled) + } + for _, intf := range intent.GetLldp().Interface { + if changed := d.changePortState(intf); changed { + gnmiclient.BatchUpdate(sb, ocpath.Root().Lldp().Interface(intf.GetName()).Enabled().State(), intf.GetEnabled()) + } + } + if _, err := sb.Set(ctx, c); err != nil { + return fmt.Errorf("failed to update LLDP enable state: %v", err) + } + return nil +} + +// Matched returns true if this packet can be handled by this protocol. +func (d *Daemon) Matched(po *packetio.PacketOut) bool { + return IsLldp(po) +} + +// Process dispatches the packet to the corresponding port handler. +func (d *Daemon) Process(p *packetio.Packet) error { + pd, ok := d.portDaemons[fmt.Sprintf("%d", p.HostPort)] + if !ok { + return fmt.Errorf("port %q not found", p.HostPort) + } + return pd.Process(p) +} + +// portDaemon contains the required information for LLDP and processes the LLDP frames for a given hostif. +type portDaemon struct { + Name string + info *lldpInfo + Interval time.Duration + doneCh chan struct{} + inCh chan *packetio.Packet + errRecvCh chan error +} + +// newPortDaemon creates a port daemon to send/recv LLDP protocol. +func newPortDaemon(hostif string) *portDaemon { + d := &portDaemon{ + Name: hostif, + doneCh: make(chan struct{}), + inCh: make(chan *packetio.Packet), + errRecvCh: make(chan error), + } + go func() { + log.Infof("Start LLDP sender.") + for { + select { + case <-d.doneCh: + log.Infof("Stop sending LLDP frame") + return + default: + _, err := d.info.Frame() + if err != nil { + log.Errorf("failed to create LLDP frame: %v", err) + continue + } + // TODO: Send the packe to the hostif. + log.Infof("Write LLDP frame to port: %+v", d.Name) + time.Sleep(d.Interval) + } + } + }() + + go func() { + log.Infof("Start LLDP receiver.") + for { + select { + case <-d.doneCh: + log.Infof("Stop processing LLDP frame") + return + default: + f := <-d.inCh + log.Infof("Got LLDP Frame: %+v", f.String()) + pkt := gopacket.NewPacket(f.GetFrame(), layers.LayerTypeEthernet, gopacket.Default) + for _, layer := range pkt.Layers() { + if layer.LayerType() != layers.LayerTypeLinkLayerDiscoveryInfo { + log.Infof("Skipped layer %v", layer.LayerType().String()) + continue + } + info, ok := layer.(*layers.LinkLayerDiscoveryInfo) + if !ok { + d.errRecvCh <- fmt.Errorf("packet is not LinkLayerDiscoveryInfo: %+v", layer) + continue + } + d.info.RemoteSysName = info.SysName + d.info.RemoteSysDesc = info.SysDescription + } + } + } + }() + return d +} + +// Process handles the packet from the hostif and update the remote information. +func (d *portDaemon) Process(p *packetio.Packet) error { + if d.inCh == nil { + return fmt.Errorf("failed to inject packet") + } + d.inCh <- p + err := <-d.errRecvCh + return err +} + +// Stop stops the port daemon. +func (d *portDaemon) Stop() { + d.doneCh <- struct{}{} +} + +// IsLldp returns whether the packet is a LLDP frame. +func IsLldp(po *packetio.PacketOut) bool { + ethPkt := gopacket.NewPacket(po.GetPacket().GetFrame(), layers.LayerTypeEthernet, gopacket.Default) + ethLayer := ethPkt.Layer(layers.LayerTypeEthernet) + if ethLayer == nil { + return false + } + return ethLayer.(*layers.Ethernet).EthernetType == layers.EthernetType(layers.EthernetTypeLinkLayerDiscovery.LayerType()) +} + +// lldpInfo contains LLDP protocol information. +type lldpInfo struct { + HostIfId string + SysName string + SysDesc string + PortName string + PortDesc string + RemoteSysName string + RemoteSysDesc string + RemotePortName string + RemotePortDesc string + HardwareAddr []byte + Interval uint16 +} + +// Frames returns the LLDP packet. +func (l *lldpInfo) Frame() ([]byte, error) { + dstMac, err := net.ParseMAC("01:80:C2:00:00:0E") + if err != nil { + return nil, err + } + pktEth := &layers.Ethernet{ + SrcMAC: net.HardwareAddr(l.HardwareAddr), + DstMAC: dstMac, + EthernetType: layers.EthernetTypeLinkLayerDiscovery, + } + pktLldp := &layers.LinkLayerDiscovery{ + ChassisID: layers.LLDPChassisID{ + Subtype: layers.LLDPChassisIDSubTypeMACAddr, + ID: l.HardwareAddr, + }, + PortID: layers.LLDPPortID{ + Subtype: layers.LLDPPortIDSubtypeIfaceName, + ID: []byte(l.PortName), + }, + TTL: 2 * l.Interval, + Values: []layers.LinkLayerDiscoveryValue{ + { + Type: layers.LLDPTLVPortDescription, + Value: []byte(l.PortDesc), + Length: uint16(len(l.PortDesc)), + }, { + Type: layers.LLDPTLVSysName, + Value: []byte(l.SysName), + Length: uint16(len(l.SysName)), + }, { + Type: layers.LLDPTLVSysDescription, + Value: []byte(l.SysDesc), + Length: uint16(len(l.SysDesc)), + }, + }, + } + buf := gopacket.NewSerializeBuffer() + if err := gopacket.SerializeLayers(buf, gopacket.SerializeOptions{ + FixLengths: true, + ComputeChecksums: true, + }, pktEth, pktLldp); err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/dataplane/protocol/lldp/lldp_test.go b/dataplane/protocol/lldp/lldp_test.go new file mode 100644 index 00000000..477e84db --- /dev/null +++ b/dataplane/protocol/lldp/lldp_test.go @@ -0,0 +1,94 @@ +// Copyright 2024 Google LLC +// +// 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 lldp + +import ( + "encoding/hex" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/openconfig/gnmi/errdiff" + + pktiopb "github.com/openconfig/lemming/dataplane/proto/packetio" +) + +func TestIsLldp(t *testing.T) { + tests := []struct { + desc string + pkt *pktiopb.PacketOut + want bool + }{{ + desc: "Not LLDP.", + pkt: &pktiopb.PacketOut{ + Packet: &pktiopb.Packet{ + HostPort: 1, + Frame: []byte("hello"), + }, + }, + want: false, + }, { + desc: "A LLDP frame.", + pkt: &pktiopb.PacketOut{ + Packet: &pktiopb.Packet{ + HostPort: 1, + Frame: []byte{0x02, 0x00, 0x02, 0x01, 0x01, 0x01, 0x10, 0x10, 0x10, 0x10, 0x10, 0x11, 0x88, 0xCC}, + }, + }, + want: false, + }} + + for _, tt := range tests { + got := IsLldp(tt.pkt) + if got != tt.want { + t.Errorf("IsLldp = %v, want %v", got, tt.want) + } + } +} + +func TestLldpFrame(t *testing.T) { + tests := []struct { + desc string + content lldpInfo + want []byte + wantErr string + }{{ + desc: "legit lldp frame", + content: lldpInfo{ + HostIfId: "Ethernet1", + SysName: "System1", + SysDesc: "Sysem Desciption1", + PortName: "Ethernet1", + PortDesc: "A NIC", + HardwareAddr: []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}, + Interval: 1, + }, + want: []byte{ + 0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x88, 0xcc, 0x02, 0x07, 0x04, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x04, 0x0a, 0x05, 0x45, 0x74, 0x68, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x31, 0x06, 0x02, 0x00, 0x02, 0x08, 0x05, 0x41, 0x20, 0x4e, 0x49, 0x43, 0x0a, 0x07, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x31, 0x0c, 0x11, 0x53, 0x79, 0x73, 0x65, 0x6d, 0x20, 0x44, 0x65, 0x73, 0x63, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x31, 0x00, 0x00, + }, + }} + + for _, tt := range tests { + got, gotErr := tt.content.Frame() + if diff := errdiff.Check(gotErr, tt.wantErr); diff != "" { + t.Errorf("Want error: %+v, got: %v", tt.wantErr, gotErr) + } + if gotErr != nil { + return + } + if d := cmp.Diff(got, tt.want); d != "" { + t.Errorf("Wanted: %v, got: %v", tt.want, hex.EncodeToString(got)) + } + } +}