From 4bb790922feddead5cd87f4c3826b28d246f0364 Mon Sep 17 00:00:00 2001 From: Siyuan Miao Date: Fri, 13 Jun 2025 22:06:27 +0000 Subject: [PATCH] feat(lldp): implement rx --- .devcontainer/devcontainer.json | 2 +- Makefile | 27 +++- go.mod | 3 + go.sum | 7 +- internal/lldp/afpacket.go | 85 +++++++++++ internal/lldp/afpacket_arm.go | 15 ++ internal/lldp/afpacket_nonarm.go | 15 ++ internal/lldp/lldp.go | 68 +++++++++ internal/lldp/neigh.go | 55 +++++++ internal/lldp/rx.go | 254 +++++++++++++++++++++++++++++++ network.go | 11 ++ 11 files changed, 537 insertions(+), 5 deletions(-) create mode 100644 internal/lldp/afpacket.go create mode 100644 internal/lldp/afpacket_arm.go create mode 100644 internal/lldp/afpacket_nonarm.go create mode 100644 internal/lldp/lldp.go create mode 100644 internal/lldp/neigh.go create mode 100644 internal/lldp/rx.go diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index aa803f62..8f0af4b7 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "JetKVM", - "image": "mcr.microsoft.com/devcontainers/go:1-1.23-bookworm", + "image": "mcr.microsoft.com/devcontainers/go:1-1.24-bookworm", "features": { "ghcr.io/devcontainers/features/node:1": { // Should match what is defined in ui/package.json diff --git a/Makefile b/Makefile index c7789ed5..2b8a169a 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,10 @@ VERSION := 0.4.7 PROMETHEUS_TAG := github.com/prometheus/common/version KVM_PKG_NAME := github.com/jetkvm/kvm +BUILDKIT_FLAVOR := arm-rockchip830-linux-uclibcgnueabihf +BUILDKIT_PATH ?= /opt/jetkvm-native-buildkit + + GO_BUILD_ARGS := -tags netgo -tags timetzdata GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS) GO_LDFLAGS := \ @@ -17,7 +21,19 @@ GO_LDFLAGS := \ -X $(PROMETHEUS_TAG).Revision=$(REVISION) \ -X $(KVM_PKG_NAME).builtTimestamp=$(BUILDTS) -GO_CMD := GOOS=linux GOARCH=arm GOARM=7 go +GO_ARGS := GOOS=linux GOARCH=arm GOARM=7 +# if BUILDKIT_PATH exists, use buildkit to build +ifneq ($(wildcard $(BUILDKIT_PATH)),) + GO_ARGS := $(GO_ARGS) \ + CGO_CFLAGS="-I$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/include -I$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/sysroot/usr/include" \ + CGO_LDFLAGS="-L$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/lib -L$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/sysroot/usr/lib" \ + CC="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-gcc" \ + LD="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-ld" \ + CGO_ENABLED=1 +endif + +GO_CMD := $(GO_ARGS) go + BIN_DIR := $(shell pwd)/bin TEST_DIRS := $(shell find . -name "*_test.go" -type f -exec dirname {} \; | sort -u) @@ -32,6 +48,13 @@ build_dev: hash_resource $(GO_RELEASE_BUILD_ARGS) \ -o $(BIN_DIR)/jetkvm_app cmd/main.go +build_afpacket: + @echo "Building..." + $(GO_CMD) build \ + -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \ + $(GO_RELEASE_BUILD_ARGS) \ + -o $(BIN_DIR)/afpacket internal/lldp/cmd/afp.go + build_test2json: $(GO_CMD) build -o $(BIN_DIR)/test2json cmd/test2json @@ -101,4 +124,4 @@ release: @echo "Uploading release..." @shasum -a 256 bin/jetkvm_app | cut -d ' ' -f 1 > bin/jetkvm_app.sha256 rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION)/jetkvm_app - rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256 + rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256 \ No newline at end of file diff --git a/go.mod b/go.mod index d07ba239..bcca0468 100644 --- a/go.mod +++ b/go.mod @@ -55,10 +55,13 @@ require ( github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.0.0 // indirect + github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect diff --git a/go.sum b/go.sum index 57576a3a..b17b046c 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= -github.com/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqrKWc= -github.com/gin-contrib/logger v1.2.6/go.mod h1:7niPrd7F0Nscw/zvgz8RiGJxSdbKM2yfQNy8xCHcm64= +github.com/gin-contrib/logger v1.2.5 h1:qVQI4omayQecuN4zX9ZZnsOq7w9J/ZLds3J/FMn8ypM= +github.com/gin-contrib/logger v1.2.5/go.mod h1:/bj+vNMuA2xOEQ1aRHoJ1m9+uyaaXIAxQTvM2llsc6I= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= @@ -58,6 +58,8 @@ github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZat github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= @@ -83,6 +85,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= diff --git a/internal/lldp/afpacket.go b/internal/lldp/afpacket.go new file mode 100644 index 00000000..bb30d521 --- /dev/null +++ b/internal/lldp/afpacket.go @@ -0,0 +1,85 @@ +package lldp + +import ( + "fmt" + "net" + "os" + "syscall" + "unsafe" + + "github.com/google/gopacket/afpacket" + "golang.org/x/sys/unix" +) + +const ( + afPacketBufferSize = 2 // in MiB + afPacketSnaplen = 9216 +) + +func afPacketComputeSize(targetSizeMb int, snaplen int, pageSize int) ( + frameSize int, blockSize int, numBlocks int, err error) { + + if snaplen < pageSize { + frameSize = pageSize / (pageSize / snaplen) + } else { + frameSize = (snaplen/pageSize + 1) * pageSize + } + + // 128 is the default from the gopacket library so just use that + blockSize = frameSize * 128 + numBlocks = (targetSizeMb * 1024 * 1024) / blockSize + + if numBlocks == 0 { + return 0, 0, 0, fmt.Errorf("interface buffersize is too small") + } + + return frameSize, blockSize, numBlocks, nil +} + +func afPacketNewTPacket(ifName string) (*afpacket.TPacket, error) { + szFrame, szBlock, numBlocks, err := afPacketComputeSize( + afPacketBufferSize, + afPacketSnaplen, + os.Getpagesize()) + if err != nil { + return nil, err + } + + return afpacket.NewTPacket( + afpacket.OptInterface(ifName), + afpacket.OptFrameSize(szFrame), + afpacket.OptBlockSize(szBlock), + afpacket.OptNumBlocks(numBlocks), + afpacket.OptAddVLANHeader(false), + afpacket.SocketRaw, + afpacket.TPacketVersion3) +} + +type ifreq struct { + ifrName [IFNAMSIZ]byte + ifrHwaddr syscall.RawSockaddr +} + +func addMulticastAddr(ifName string, addr net.HardwareAddr) error { + fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0) + if err != nil { + return err + } + defer syscall.Close(fd) + + var name [IFNAMSIZ]byte + copy(name[:], []byte(ifName)) + + ifr := &ifreq{ + ifrName: name, + ifrHwaddr: toRawSockaddr(addr), + } + + _, _, ep := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), + unix.SIOCADDMULTI, uintptr(unsafe.Pointer(ifr))) + + if ep != 0 { + return syscall.Errno(ep) + } + return nil +} diff --git a/internal/lldp/afpacket_arm.go b/internal/lldp/afpacket_arm.go new file mode 100644 index 00000000..470d571c --- /dev/null +++ b/internal/lldp/afpacket_arm.go @@ -0,0 +1,15 @@ +//go:build arm && linux + +package lldp + +import ( + "net" + "syscall" +) + +func toRawSockaddr(mac net.HardwareAddr) (sockaddr syscall.RawSockaddr) { + for i, n := range mac { + sockaddr.Data[i] = uint8(n) + } + return +} diff --git a/internal/lldp/afpacket_nonarm.go b/internal/lldp/afpacket_nonarm.go new file mode 100644 index 00000000..2f11c823 --- /dev/null +++ b/internal/lldp/afpacket_nonarm.go @@ -0,0 +1,15 @@ +//go:build !arm && linux + +package lldp + +import ( + "net" + "syscall" +) + +func toRawSockaddr(mac net.HardwareAddr) (sockaddr syscall.RawSockaddr) { + for i, n := range mac { + sockaddr.Data[i] = int8(n) + } + return +} diff --git a/internal/lldp/lldp.go b/internal/lldp/lldp.go new file mode 100644 index 00000000..2a760048 --- /dev/null +++ b/internal/lldp/lldp.go @@ -0,0 +1,68 @@ +package lldp + +import ( + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/afpacket" + "github.com/jellydator/ttlcache/v3" + "github.com/jetkvm/kvm/internal/logging" + "github.com/rs/zerolog" +) + +var defaultLogger = logging.GetSubsystemLogger("lldp") + +type LLDP struct { + l *zerolog.Logger + tPacket *afpacket.TPacket + pktSource *gopacket.PacketSource + + enableRx bool + enableTx bool + + packets chan gopacket.Packet + interfaceName string + stop chan struct{} + + neighbors *ttlcache.Cache[string, Neighbor] +} + +type LLDPOptions struct { + InterfaceName string + EnableRx bool + EnableTx bool + Logger *zerolog.Logger +} + +func NewLLDP(opts *LLDPOptions) *LLDP { + if opts.Logger == nil { + opts.Logger = defaultLogger + } + + if opts.InterfaceName == "" { + opts.Logger.Fatal().Msg("InterfaceName is required") + } + + return &LLDP{ + interfaceName: opts.InterfaceName, + enableRx: opts.EnableRx, + enableTx: opts.EnableTx, + l: opts.Logger, + neighbors: ttlcache.New(ttlcache.WithTTL[string, Neighbor](1 * time.Hour)), + } +} + +func (l *LLDP) Start() error { + if l.enableRx { + l.l.Info().Msg("setting up AF_PACKET") + if err := l.setUpCapture(); err != nil { + l.l.Error().Err(err).Msg("unable to set up AF_PACKET") + return err + } + l.startCapture() + } + + go l.neighbors.Start() + + return nil +} diff --git a/internal/lldp/neigh.go b/internal/lldp/neigh.go new file mode 100644 index 00000000..d73c6f6c --- /dev/null +++ b/internal/lldp/neigh.go @@ -0,0 +1,55 @@ +package lldp + +import "time" + +type Neighbor struct { + Mac string `json:"mac"` + Source string `json:"source"` + ChassisID string `json:"chassis_id"` + PortID string `json:"port_id"` + PortDescription string `json:"port_description"` + SystemName string `json:"system_name"` + SystemDescription string `json:"system_description"` + TTL uint16 `json:"ttl"` + ManagementAddress string `json:"management_address"` + Values map[string]string `json:"values"` +} + +func (l *LLDP) addNeighbor(mac string, neighbor Neighbor, ttl time.Duration) { + logger := l.l.With(). + Str("mac", mac). + Interface("neighbor", neighbor). + Logger() + + current_neigh := l.neighbors.Get(mac) + if current_neigh != nil { + current_source := current_neigh.Value().Source + if current_source == "lldp" && neighbor.Source != "lldp" { + logger.Info().Msg("skip updating neighbor, as LLDP has higher priority") + return + } + } + + logger.Info().Msg("adding neighbor") + l.neighbors.Set(mac, neighbor, ttl) +} + +func (l *LLDP) deleteNeighbor(mac string) { + logger := l.l.With(). + Str("mac", mac). + Logger() + + logger.Info().Msg("deleting neighbor") + l.neighbors.Delete(mac) +} + +func (l *LLDP) GetNeighbors() []Neighbor { + items := l.neighbors.Items() + neighbors := make([]Neighbor, 0, len(items)) + + for _, item := range items { + neighbors = append(neighbors, item.Value()) + } + + return neighbors +} diff --git a/internal/lldp/rx.go b/internal/lldp/rx.go new file mode 100644 index 00000000..ccaf70ac --- /dev/null +++ b/internal/lldp/rx.go @@ -0,0 +1,254 @@ +package lldp + +import ( + "fmt" + "net" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/rs/zerolog" + "golang.org/x/net/bpf" +) + +const IFNAMSIZ = 16 + +var ( + lldpDefaultTTL = 120 * time.Second + cdpDefaultTTL = 180 * time.Second +) + +// from lldpd +// https://github.com/lldpd/lldpd/blob/9034c9332cca0c8b1a20e1287f0e5fed81f7eb2a/src/daemon/lldpd.h#L246 +var bpfFilter = []bpf.RawInstruction{ + {0x30, 0, 0, 0x00000000}, {0x54, 0, 0, 0x00000001}, {0x15, 0, 16, 0x00000001}, + {0x28, 0, 0, 0x0000000c}, {0x15, 0, 6, 0x000088cc}, + {0x20, 0, 0, 0x00000002}, {0x15, 2, 0, 0xc200000e}, + {0x15, 1, 0, 0xc2000003}, {0x15, 0, 2, 0xc2000000}, + {0x28, 0, 0, 0x00000000}, {0x15, 12, 13, 0x00000180}, + {0x20, 0, 0, 0x00000002}, {0x15, 0, 2, 0x52cccccc}, + {0x28, 0, 0, 0x00000000}, {0x15, 8, 9, 0x000001e0}, + {0x15, 1, 0, 0x0ccccccc}, {0x15, 0, 2, 0x81000100}, + {0x28, 0, 0, 0x00000000}, {0x15, 4, 5, 0x00000100}, + {0x20, 0, 0, 0x00000002}, {0x15, 0, 3, 0x2b000000}, + {0x28, 0, 0, 0x00000000}, {0x15, 0, 1, 0x000000e0}, + {0x6, 0, 0, 0x00040000}, + {0x6, 0, 0, 0x00000000}, +} + +var multicastAddrs = []string{ + // LLDP + "01:80:C2:00:00:00", + "01:80:C2:00:00:03", + "01:80:C2:00:00:0E", + // CDP + "01:00:0C:CC:CC:CC", +} + +func (l *LLDP) setUpCapture() error { + logger := l.l.With().Str("interface", l.interfaceName).Logger() + tPacket, err := afPacketNewTPacket(l.interfaceName) + if err != nil { + return err + } + logger.Info().Msg("created TPacket") + + // set up multicast addresses + // otherwise the kernel might discard the packets + // another workaround would be to enable promiscuous mode but that's too tricky + for _, mac := range multicastAddrs { + hwAddr, err := net.ParseMAC(mac) + if err != nil { + logger.Error().Msgf("unable to parse MAC address %s: %s", mac, err) + continue + } + + if err := addMulticastAddr(l.interfaceName, hwAddr); err != nil { + logger.Error().Msgf("unable to add multicast address %s: %s", mac, err) + continue + } + + logger.Info(). + Interface("hwaddr", hwAddr). + Msgf("added multicast address") + } + + if err = tPacket.SetBPF(bpfFilter); err != nil { + logger.Error().Msgf("unable to set BPF filter: %s", err) + tPacket.Close() + return err + } + logger.Info().Msg("BPF filter set") + + l.pktSource = gopacket.NewPacketSource(tPacket, layers.LayerTypeEthernet) + l.tPacket = tPacket + + return nil +} + +func (l *LLDP) startCapture() error { + logger := l.l.With().Str("interface", l.interfaceName).Logger() + if l.tPacket == nil { + return fmt.Errorf("AFPacket not initialized") + } + + if l.pktSource == nil { + return fmt.Errorf("packet source not initialized") + } + + go func() { + logger.Info().Msg("starting capture LLDP ethernet frames") + + for packet := range l.pktSource.Packets() { + if err := l.handlePacket(packet, &logger); err != nil { + logger.Error().Msgf("error handling packet: %s", err) + } + } + }() + + return nil +} + +func (l *LLDP) handlePacket(packet gopacket.Packet, logger *zerolog.Logger) error { + linkLayer := packet.LinkLayer() + if linkLayer == nil { + return fmt.Errorf("no link layer") + } + + srcMac := linkLayer.LinkFlow().Src().String() + dstMac := linkLayer.LinkFlow().Dst().String() + + logger.Trace(). + Str("src_mac", srcMac). + Str("dst_mac", dstMac). + Int("length", len(packet.Data())). + Hex("data", packet.Data()). + Msg("received packet") + + lldpRaw := packet.Layer(layers.LayerTypeLinkLayerDiscovery) + if lldpRaw != nil { + logger.Trace().Msgf("Found LLDP Frame") + + lldpInfo := packet.Layer(layers.LayerTypeLinkLayerDiscoveryInfo) + if lldpInfo == nil { + return fmt.Errorf("no LLDP info layer") + } + + return l.handlePacketLLDP( + srcMac, + lldpRaw.(*layers.LinkLayerDiscovery), + lldpInfo.(*layers.LinkLayerDiscoveryInfo), + ) + } + + cdpRaw := packet.Layer(layers.LayerTypeCiscoDiscovery) + if cdpRaw != nil { + logger.Trace().Msgf("Found CDP Frame") + + cdpInfo := packet.Layer(layers.LayerTypeCiscoDiscoveryInfo) + if cdpInfo == nil { + return fmt.Errorf("no CDP info layer") + } + + return l.handlePacketCDP( + srcMac, + cdpRaw.(*layers.CiscoDiscovery), + cdpInfo.(*layers.CiscoDiscoveryInfo), + ) + } + + return nil +} + +func (l *LLDP) handlePacketLLDP(mac string, raw *layers.LinkLayerDiscovery, info *layers.LinkLayerDiscoveryInfo) error { + n := &Neighbor{ + Values: make(map[string]string), + Source: "lldp", + Mac: mac, + } + gotEnd := false + + ttl := lldpDefaultTTL + + for _, v := range raw.Values { + switch v.Type { + case layers.LLDPTLVEnd: + gotEnd = true + case layers.LLDPTLVChassisID: + n.ChassisID = string(raw.ChassisID.ID) + n.Values["chassis_id"] = n.ChassisID + case layers.LLDPTLVPortID: + n.PortID = string(raw.PortID.ID) + n.Values["port_id"] = n.PortID + case layers.LLDPTLVPortDescription: + n.PortDescription = info.PortDescription + n.Values["port_description"] = n.PortDescription + case layers.LLDPTLVSysName: + n.SystemName = info.SysName + n.Values["system_name"] = n.SystemName + case layers.LLDPTLVSysDescription: + n.SystemDescription = info.SysDescription + n.Values["system_description"] = n.SystemDescription + case layers.LLDPTLVMgmtAddress: + // n.ManagementAddress = info.MgmtAddress.Address + case layers.LLDPTLVTTL: + n.TTL = uint16(raw.TTL) + ttl = time.Duration(n.TTL) * time.Second + n.Values["ttl"] = fmt.Sprintf("%d", n.TTL) + case layers.LLDPTLVOrgSpecific: + for _, org := range info.OrgTLVs { + n.Values[fmt.Sprintf("org_specific_%d", org.OUI)] = string(org.Info) + } + } + } + + if gotEnd || ttl < 1*time.Second { + l.deleteNeighbor(mac) + } else { + l.addNeighbor(mac, *n, ttl) + } + + return nil +} + +func (l *LLDP) handlePacketCDP(mac string, raw *layers.CiscoDiscovery, info *layers.CiscoDiscoveryInfo) error { + // TODO: implement full CDP parsing + n := &Neighbor{ + Values: make(map[string]string), + Source: "cdp", + Mac: mac, + } + + ttl := cdpDefaultTTL + + n.ChassisID = info.DeviceID + n.PortID = info.PortID + n.SystemName = info.SysName + n.SystemDescription = info.Platform + n.TTL = uint16(raw.TTL) + + if n.TTL > 1 { + ttl = time.Duration(n.TTL) * time.Second + } + + if len(info.MgmtAddresses) > 0 { + n.ManagementAddress = fmt.Sprintf("%s", info.MgmtAddresses[0]) + } + + l.addNeighbor(mac, *n, ttl) + + return nil +} + +func (l *LLDP) shutdownCapture() error { + if l.tPacket != nil { + l.tPacket.Close() + l.tPacket = nil + } + + if l.pktSource != nil { + l.pktSource = nil + } + + return nil +} diff --git a/network.go b/network.go index af8e50fb..aa231684 100644 --- a/network.go +++ b/network.go @@ -3,6 +3,7 @@ package kvm import ( "fmt" + "github.com/jetkvm/kvm/internal/lldp" "github.com/jetkvm/kvm/internal/network" "github.com/jetkvm/kvm/internal/udhcpc" ) @@ -49,6 +50,16 @@ func networkStateChanged(isOnline bool) { func initNetwork() error { ensureConfigLoaded() + lldp := lldp.NewLLDP(&lldp.LLDPOptions{ + InterfaceName: NetIfName, + EnableRx: true, + EnableTx: true, + Logger: networkLogger, + }) + if err := lldp.Start(); err != nil { + return err + } + state, err := network.NewNetworkInterfaceState(&network.NetworkInterfaceOptions{ DefaultHostname: GetDefaultHostname(), InterfaceName: NetIfName,