Compare commits

..

3 Commits

Author SHA1 Message Date
Siyuan Miao 6e25d44597 fix lint issues 2025-06-14 14:56:05 +02:00
Siyuan Miao cb7da61ab4 feat(lldp): show neighbors in UI 2025-06-14 14:50:21 +02:00
Siyuan Miao 748bfe5477 feat(lldp): implement rx 2025-06-14 09:23:56 +02:00
16 changed files with 303 additions and 272 deletions

11
go.mod
View File

@ -20,7 +20,7 @@ require (
github.com/hanwen/go-fuse/v2 v2.5.1
github.com/hashicorp/go-getter/v2 v2.2.3
github.com/jellydator/ttlcache/v3 v3.3.0
github.com/pion/logging v0.2.2
github.com/pion/logging v0.2.3
github.com/pion/mdns/v2 v2.0.7
github.com/pion/webrtc/v4 v4.0.16
github.com/pojntfx/go-nbd v0.3.2
@ -33,8 +33,8 @@ require (
github.com/stretchr/testify v1.10.0
github.com/vishvananda/netlink v1.3.0
go.bug.st/serial v1.6.2
golang.org/x/crypto v0.36.0
golang.org/x/net v0.38.0
golang.org/x/crypto v0.37.0
golang.org/x/net v0.39.0
golang.org/x/sys v0.32.0
)
@ -62,6 +62,7 @@ require (
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-version v1.1.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
@ -96,8 +97,8 @@ require (
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/arch v0.17.0 // indirect
golang.org/x/oauth2 v0.24.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

29
go.sum
View File

@ -34,12 +34,12 @@ 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=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
@ -95,6 +95,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=
@ -201,24 +202,24 @@ go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU=
golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -230,8 +231,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=

View File

@ -39,7 +39,7 @@ type testNetworkConfig struct {
IPv6Mode null.String `json:"ipv6_mode" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
IPv6Static *testIPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"`
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,rx_only,tx_only,enabled" default:"enabled"`
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`

View File

@ -18,7 +18,6 @@ const (
func afPacketComputeSize(targetSizeMb int, snaplen int, pageSize int) (
frameSize int, blockSize int, numBlocks int, err error) {
if snaplen < pageSize {
frameSize = pageSize / (pageSize / snaplen)
} else {

View File

@ -1,222 +0,0 @@
// Copyright 2018 Google, Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file in the root of the source
// tree.
// afpacket provides a simple example of using afpacket with zero-copy to read
// packet data.
package main
import (
"flag"
"fmt"
"log"
"net"
"os"
"runtime/pprof"
"syscall"
"time"
"unsafe"
"github.com/google/gopacket"
"github.com/google/gopacket/afpacket"
"github.com/google/gopacket/layers"
"golang.org/x/net/bpf"
"golang.org/x/sys/unix"
_ "github.com/google/gopacket/layers"
)
const ETH_P_LLDP = 0x88cc
var bpfFilter = []bpf.RawInstruction{
{0x28, 0, 0, 0x0000000c},
{0x15, 0, 5, 0x000088cc},
{0x20, 0, 0, 0x00000002},
{0x15, 0, 3, 0xc200000e},
{0x28, 0, 0, 0x00000000},
{0x15, 0, 1, 0x00000180},
{0x6, 0, 0, 0x00040000},
{0x6, 0, 0, 0x00000000},
}
func rawSocketaddrFromMAC(mac net.HardwareAddr) (sockaddr syscall.RawSockaddr) {
for i, n := range mac {
sockaddr.Data[i] = uint8(n)
}
return
}
var (
iface = flag.String("i", "any", "Interface to read from")
cpuprofile = flag.String("cpuprofile", "", "If non-empty, write CPU profile here")
snaplen = flag.Int("s", 0, "Snaplen, if <= 0, use 65535")
bufferSize = flag.Int("b", 8, "Interface buffersize (MB)")
filter = flag.String("f", "port not 22", "BPF filter")
count = flag.Int64("c", -1, "If >= 0, # of packets to capture before returning")
verbose = flag.Int64("log_every", 1, "Write a log every X packets")
addVLAN = flag.Bool("add_vlan", false, "If true, add VLAN header")
)
type afpacketHandle struct {
TPacket *afpacket.TPacket
}
const IFNAMSIZ = 16
type ifreq struct {
ifrName [IFNAMSIZ]byte
ifrHwaddr syscall.RawSockaddr
}
// addMulticastAddr adds a multicast address to an interface using an ioctl call
func addMulticastAddr(intf string, addr string) 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(intf))
mac, _ := net.ParseMAC(addr)
ifr := &ifreq{
ifrName: name,
ifrHwaddr: rawSocketaddrFromMAC(mac),
}
_, _, ep := unix.Syscall(unix.SYS_IOCTL, uintptr(fd),
unix.SIOCADDMULTI, uintptr(unsafe.Pointer(ifr)))
if ep != 0 {
return syscall.Errno(ep)
}
return nil
}
func newAfpacketHandle(device string, snaplen int, block_size int, num_blocks int,
useVLAN bool, timeout time.Duration) (*afpacketHandle, error) {
h := &afpacketHandle{}
var err error
if device == "any" {
h.TPacket, err = afpacket.NewTPacket(
afpacket.OptFrameSize(snaplen),
afpacket.OptBlockSize(block_size),
afpacket.OptNumBlocks(num_blocks),
afpacket.OptAddVLANHeader(useVLAN),
afpacket.OptPollTimeout(timeout),
afpacket.SocketRaw,
afpacket.TPacketVersion3)
} else {
h.TPacket, err = afpacket.NewTPacket(
afpacket.OptInterface(device),
afpacket.OptFrameSize(snaplen),
afpacket.OptBlockSize(block_size),
afpacket.OptNumBlocks(num_blocks),
afpacket.OptAddVLANHeader(useVLAN),
afpacket.OptPollTimeout(timeout),
afpacket.SocketRaw,
afpacket.TPacketVersion3)
}
return h, err
}
// ZeroCopyReadPacketData satisfies ZeroCopyPacketDataSource interface
func (h *afpacketHandle) ZeroCopyReadPacketData() (data []byte, ci gopacket.CaptureInfo, err error) {
return h.TPacket.ZeroCopyReadPacketData()
}
// LinkType returns ethernet link type.
func (h *afpacketHandle) LinkType() layers.LinkType {
return layers.LinkTypeEthernet
}
// Close will close afpacket source.
func (h *afpacketHandle) Close() {
h.TPacket.Close()
}
// SocketStats prints received, dropped, queue-freeze packet stats.
func (h *afpacketHandle) SocketStats() (as afpacket.SocketStats, asv afpacket.SocketStatsV3, err error) {
return h.TPacket.SocketStats()
}
// afpacketComputeSize computes the block_size and the num_blocks in such a way that the
// allocated mmap buffer is close to but smaller than target_size_mb.
// The restriction is that the block_size must be divisible by both the
// frame size and page size.
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 main() {
flag.Parse()
if *cpuprofile != "" {
log.Printf("Writing CPU profile to %q", *cpuprofile)
f, err := os.Create(*cpuprofile)
if err != nil {
log.Fatal(err)
}
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal(err)
}
defer pprof.StopCPUProfile()
}
log.Printf("Starting on interface %q", *iface)
if *snaplen <= 0 {
*snaplen = 65535
}
szFrame, szBlock, numBlocks, err := afpacketComputeSize(*bufferSize, *snaplen, os.Getpagesize())
if err != nil {
log.Fatal(err)
}
if *addVLAN {
log.Printf("Adding VLAN header")
}
lldpPrefix := "01:80:c2:00:00"
for _, lastByte := range []byte{0x00, 0x03, 0x0e} {
// Add multicast address so that the kernel does not discard it
if err := addMulticastAddr(*iface, fmt.Sprintf("%s:%02X", lldpPrefix, lastByte)); err != nil {
log.Fatalf("Failed to add multicast address: %s", err)
}
}
afpacketHandle, err := newAfpacketHandle(*iface, szFrame, szBlock, numBlocks, *addVLAN, -time.Millisecond*10)
if err != nil {
log.Fatal(err)
}
err = afpacketHandle.TPacket.SetBPF(bpfFilter)
if err != nil {
log.Fatal(err)
}
source := gopacket.NewPacketSource(afpacketHandle.TPacket, afpacketHandle.LinkType())
defer afpacketHandle.Close()
for packet := range source.Packets() {
fmt.Println("packet", packet)
lldpLayer := packet.Layer(layers.LayerTypeLinkLayerDiscovery)
if lldpLayer != nil {
fmt.Println("lldpLayer", lldpLayer)
}
}
}

View File

@ -1,6 +1,8 @@
package lldp
import (
"context"
"sync"
"time"
"github.com/google/gopacket"
@ -16,6 +18,9 @@ type LLDP struct {
l *zerolog.Logger
tPacket *afpacket.TPacket
pktSource *gopacket.PacketSource
rxCtx context.Context
rxCancel context.CancelFunc
rxLock sync.Mutex
enableRx bool
enableTx bool
@ -53,16 +58,49 @@ func NewLLDP(opts *LLDPOptions) *LLDP {
}
func (l *LLDP) Start() error {
l.rxLock.Lock()
defer l.rxLock.Unlock()
if l.rxCtx != nil {
l.l.Info().Msg("LLDP already started")
return nil
}
l.rxCtx, l.rxCancel = context.WithCancel(context.Background())
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()
if err := l.startCapture(); err != nil {
l.l.Error().Err(err).Msg("unable to start capture")
return err
}
}
go l.neighbors.Start()
return nil
}
func (l *LLDP) Stop() error {
l.rxLock.Lock()
defer l.rxLock.Unlock()
if l.rxCancel != nil {
l.rxCancel()
l.rxCancel = nil
l.rxCtx = nil
}
if l.enableRx {
_ = l.shutdownCapture()
}
l.neighbors.Stop()
l.neighbors.DeleteAll()
return nil
}

View File

@ -51,5 +51,7 @@ func (l *LLDP) GetNeighbors() []Neighbor {
neighbors = append(neighbors, item.Value())
}
l.l.Info().Interface("neighbors", neighbors).Msg("neighbors")
return neighbors
}

View File

@ -20,6 +20,8 @@ var (
// from lldpd
// https://github.com/lldpd/lldpd/blob/9034c9332cca0c8b1a20e1287f0e5fed81f7eb2a/src/daemon/lldpd.h#L246
//
//nolint:govet
var bpfFilter = []bpf.RawInstruction{
{0x30, 0, 0, 0x00000000}, {0x54, 0, 0, 0x00000001}, {0x15, 0, 16, 0x00000001},
{0x28, 0, 0, 0x0000000c}, {0x15, 0, 6, 0x000088cc},
@ -99,9 +101,15 @@ func (l *LLDP) startCapture() error {
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)
for {
select {
case <-l.rxCtx.Done():
logger.Info().Msg("shutting down LLDP capture")
return
case packet := <-l.pktSource.Packets():
if err := l.handlePacket(packet, &logger); err != nil {
logger.Error().Msgf("error handling packet: %s", err)
}
}
}
}()
@ -232,7 +240,7 @@ func (l *LLDP) handlePacketCDP(mac string, raw *layers.CiscoDiscovery, info *lay
}
if len(info.MgmtAddresses) > 0 {
n.ManagementAddress = fmt.Sprintf("%s", info.MgmtAddresses[0])
n.ManagementAddress = string(info.MgmtAddresses[0])
}
l.addNeighbor(mac, *n, ttl)
@ -242,11 +250,13 @@ func (l *LLDP) handlePacketCDP(mac string, raw *layers.CiscoDiscovery, info *lay
func (l *LLDP) shutdownCapture() error {
if l.tPacket != nil {
l.l.Info().Msg("closing TPacket")
l.tPacket.Close()
l.tPacket = nil
}
if l.pktSource != nil {
l.l.Info().Msg("closing packet source")
l.pktSource = nil
}

View File

@ -41,7 +41,7 @@ type NetworkConfig struct {
IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"`
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,rx_only,tx_only,basic,all,enabled" default:"enabled"`
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`

46
internal/network/lldp.go Normal file
View File

@ -0,0 +1,46 @@
package network
import (
"errors"
"github.com/jetkvm/kvm/internal/lldp"
)
func (s *NetworkInterfaceState) shouldStartLLDP() bool {
if s.lldp == nil {
s.l.Trace().Msg("LLDP not initialized")
return false
}
s.l.Trace().Msgf("LLDP mode: %s", s.config.LLDPMode.String)
return s.config.LLDPMode.String != "disabled"
}
func (s *NetworkInterfaceState) startLLDP() {
if !s.shouldStartLLDP() || s.lldp == nil {
return
}
s.l.Trace().Msg("starting LLDP")
if err := s.lldp.Start(); err != nil {
s.l.Error().Err(err).Msg("unable to start LLDP")
}
}
func (s *NetworkInterfaceState) stopLLDP() {
if s.lldp == nil {
return
}
s.l.Trace().Msg("stopping LLDP")
if err := s.lldp.Stop(); err != nil {
s.l.Error().Err(err).Msg("unable to stop LLDP")
}
}
func (s *NetworkInterfaceState) GetLLDPNeighbors() ([]lldp.Neighbor, error) {
if s.lldp == nil {
return nil, errors.New("lldp not initialized")
}
return s.lldp.GetNeighbors(), nil
}

View File

@ -6,6 +6,7 @@ import (
"sync"
"github.com/jetkvm/kvm/internal/confparser"
"github.com/jetkvm/kvm/internal/lldp"
"github.com/jetkvm/kvm/internal/logging"
"github.com/jetkvm/kvm/internal/udhcpc"
"github.com/rs/zerolog"
@ -29,6 +30,8 @@ type NetworkInterfaceState struct {
config *NetworkConfig
dhcpClient *udhcpc.DHCPClient
lldp *lldp.LLDP
defaultHostname string
currentHostname string
currentFqdn string
@ -96,8 +99,16 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS
},
})
s.dhcpClient = dhcpClient
// create the lldp service
lldpClient := lldp.NewLLDP(&lldp.LLDPOptions{
InterfaceName: opts.InterfaceName,
EnableRx: true,
EnableTx: true,
Logger: l,
})
s.dhcpClient = dhcpClient
s.lldp = lldpClient
return s, nil
}
@ -310,14 +321,30 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
}
if initialCheck {
s.onInitialCheck(s)
s.handleInitialCheck()
} else if changed {
s.onStateChange(s)
s.handleStateChange()
}
return dhcpTargetState, nil
}
func (s *NetworkInterfaceState) handleInitialCheck() {
if s.IsUp() {
s.startLLDP()
}
s.onInitialCheck(s)
}
func (s *NetworkInterfaceState) handleStateChange() {
if s.IsUp() {
s.startLLDP()
} else {
s.stopLLDP()
}
s.onStateChange(s)
}
func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
dhcpTargetState, err := s.update()
if err != nil {

View File

@ -1104,4 +1104,5 @@ var rpcHandlers = map[string]RPCHandler{
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
"getLLDPNeighbors": {Func: rpcGetLLDPNeighbors},
}

View File

@ -32,16 +32,6 @@ func networkStateChanged() {
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,
@ -116,3 +106,7 @@ func rpcSetNetworkSettings(settings network.RpcNetworkSettings) (*network.RpcNet
func rpcRenewDHCPLease() error {
return networkState.RpcRenewDHCPLease()
}
func rpcGetLLDPNeighbors() ([]lldp.Neighbor, error) {
return networkState.GetLLDPNeighbors()
}

View File

@ -0,0 +1,84 @@
import { LLDPNeighbor } from "../hooks/stores";
import { LifeTimeLabel } from "../routes/devices.$id.settings.network";
import { GridCard } from "./Card";
export default function LLDPNeighCard({
neighbors,
}: {
neighbors: LLDPNeighbor[];
}) {
return (
<GridCard>
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
<div className="space-y-4">
<h3 className="text-base font-bold text-slate-900 dark:text-white">
LLDP Neighbors
</h3>
<div className="space-y-3 pt-2">
{neighbors.map(neighbor => (
<div className="space-y-3" key={neighbor.mac}>
<h4 className="text-sm font-semibold font-mono">{neighbor.mac}</h4>
<div
className="rounded-md rounded-l-none border border-slate-500/10 border-l-blue-700/50 bg-white p-4 pl-4 backdrop-blur-sm dark:bg-transparent"
>
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<div className="col-span-2 flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">
Interface
</span>
<span className="text-sm font-medium">{neighbor.port_description}</span>
</div>
{neighbor.system_name && (
<div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">
System Name
</span>
<span className="text-sm font-medium">{neighbor.system_name}</span>
</div>
)}
{neighbor.system_description && (
<div className="col-span-2 flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">
System Description
</span>
<span className="text-sm font-medium">{neighbor.system_description}</span>
</div>
)}
{neighbor.port_id && (
<div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">
Port ID
</span>
<span className="text-sm font-medium">
{neighbor.port_id}
</span>
</div>
)}
{neighbor.port_description && (
<div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">
Port Description
</span>
<span className="text-sm font-medium">
{neighbor.port_description}
</span>
</div>
)}
</div>
</div>
</div>
))}
</div>
</div>
</div>
</GridCard>
);
}

View File

@ -741,7 +741,7 @@ export type IPv6Mode =
| "link_local"
| "unknown";
export type IPv4Mode = "disabled" | "static" | "dhcp" | "unknown";
export type LLDPMode = "disabled" | "basic" | "all" | "unknown";
export type LLDPMode = "disabled" | "basic" | "all" | "tx_only" | "rx_only" | "unknown";
export type mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown";
export type TimeSyncMode =
| "ntp_only"
@ -761,6 +761,19 @@ export interface NetworkSettings {
time_sync_mode: TimeSyncMode;
}
export interface LLDPNeighbor {
mac: string;
source: string;
chassis_id: string;
port_id: string;
port_description: string;
system_name: string;
system_description: string;
ttl: number | null;
management_address: string | null;
values: Record<string, string>;
}
export const useNetworkStateStore = create<NetworkState>((set, get) => ({
setNetworkState: (state: NetworkState) => set(state),
setDhcpLease: (lease: NetworkState["dhcp_lease"]) => set({ dhcp_lease: lease }),

View File

@ -7,6 +7,7 @@ import {
IPv4Mode,
IPv6Mode,
LLDPMode,
LLDPNeighbor,
mDNSMode,
NetworkSettings,
NetworkState,
@ -29,6 +30,7 @@ import AutoHeight from "../components/AutoHeight";
import DhcpLeaseCard from "../components/DhcpLeaseCard";
import { SettingsItem } from "./devices.$id.settings";
import LLDPNeighCard from "../components/LLDPNeighCard";
dayjs.extend(relativeTime);
@ -88,6 +90,14 @@ export default function SettingsNetworkRoute() {
const [customDomain, setCustomDomain] = useState<string>("");
const [selectedDomainOption, setSelectedDomainOption] = useState<string>("dhcp");
const [lldpNeighbors, setLldpNeighbors] = useState<LLDPNeighbor[] | undefined>(undefined);
useEffect(() => {
send("getLLDPNeighbors", {}, resp => {
if ("error" in resp) return;
setLldpNeighbors(resp.result as LLDPNeighbor[]);
});
}, [send]);
useEffect(() => {
if (networkSettings.domain && networkSettingsLoaded) {
// Check if the domain is one of the predefined options
@ -130,7 +140,7 @@ export default function SettingsNetworkRoute() {
if ("error" in resp) {
notifications.error(
"Failed to save network settings: " +
(resp.error.data ? resp.error.data : resp.error.message),
(resp.error.data ? resp.error.data : resp.error.message),
);
setNetworkSettingsLoaded(true);
return;
@ -402,7 +412,7 @@ export default function SettingsNetworkRoute() {
</SettingsItem>
<AutoHeight>
{!networkSettingsLoaded &&
!(networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0) ? (
!(networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0) ? (
<GridCard>
<div className="p-4">
<div className="space-y-4">
@ -428,22 +438,49 @@ export default function SettingsNetworkRoute() {
)}
</AutoHeight>
</div>
<div className="hidden space-y-4">
<SettingsItem
title="LLDP"
description="Control which TLVs will be sent over Link Layer Discovery Protocol"
>
<div className="space-y-4">
<SettingsItem title="LLDP" description="Configure the LLDP mode">
<SelectMenuBasic
size="SM"
value={networkSettings.lldp_mode}
onChange={e => handleLldpModeChange(e.target.value)}
options={filterUnknown([
{ value: "disabled", label: "Disabled" },
{ value: "basic", label: "Basic" },
{ value: "all", label: "All" },
{ value: "tx_only", label: "Tx only" },
{ value: "rx_only", label: "Rx only" },
{ value: "basic", label: "Tx Minimal + Rx" },
{ value: "all", label: "Tx Detailed + Rx" },
{ value: "enabled", label: "Enabled" },
])}
/>
</SettingsItem>
<AutoHeight>
{lldpNeighbors === undefined ? (
<GridCard>
<div className="p-4">
<div className="space-y-4">
<h3 className="text-base font-bold text-slate-900 dark:text-white">
LLDP Neighbors
</h3>
<div className="animate-pulse space-y-3">
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-1/2 rounded bg-slate-200 dark:bg-slate-700" />
<div className="h-4 w-1/3 rounded bg-slate-200 dark:bg-slate-700" />
</div>
</div>
</div>
</GridCard>
) : lldpNeighbors.length > 0 ? (
<LLDPNeighCard neighbors={lldpNeighbors} />
) : (
<EmptyCard
IconElm={LuEthernetPort}
headline="LLDP Neighbors"
description="No LLDP neighbors found"
/>
)}
</AutoHeight>
</div>
</Fieldset>
<ConfirmDialog