mirror of https://github.com/jetkvm/kvm.git
Merge 366f7f3543 into 4090592112
This commit is contained in:
commit
fa1bb0a00d
|
|
@ -29,6 +29,9 @@ linters:
|
|||
- linters:
|
||||
- gochecknoinits
|
||||
path: internal/logging/sse.go
|
||||
- linters:
|
||||
- govet
|
||||
path: internal/lldp/bpf.go
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -55,6 +55,8 @@ require (
|
|||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/jellydator/ttlcache/v3 v3.4.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
|
|
|
|||
16
go.sum
16
go.sum
|
|
@ -60,6 +60,8 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
|
|||
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/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
|
||||
|
|
@ -70,6 +72,8 @@ github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8
|
|||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250919081422-f80a1952f48e h1:nu5z6Kg+gMNW6tdqnVjg/QEJ8Nw71IJQqOtWj00XHEU=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250919081422-f80a1952f48e/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo=
|
||||
github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY=
|
||||
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4=
|
||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
|
|
@ -206,14 +210,23 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
|||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
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.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
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-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
|
@ -224,8 +237,11 @@ golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
|||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package lldp
|
||||
|
||||
import "golang.org/x/net/bpf"
|
||||
|
||||
// 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},
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
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{}
|
||||
onChange func(neighbors []Neighbor)
|
||||
|
||||
neighbors *ttlcache.Cache[string, Neighbor]
|
||||
}
|
||||
|
||||
type LLDPOptions struct {
|
||||
InterfaceName string
|
||||
EnableRx bool
|
||||
EnableTx bool
|
||||
OnChange func(neighbors []Neighbor)
|
||||
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
|
||||
}
|
||||
|
||||
if err := l.startCapture(); err != nil {
|
||||
l.l.Error().Err(err).Msg("unable to start capture")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
go l.neighbors.Start()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
package lldp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const IFNAMSIZ = 16
|
||||
|
||||
var (
|
||||
lldpDefaultTTL = 120 * time.Second
|
||||
cdpDefaultTTL = 180 * time.Second
|
||||
)
|
||||
|
||||
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().
|
||||
MACAddr("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 = string(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
|
||||
}
|
||||
|
|
@ -1248,4 +1248,5 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
||||
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
|
||||
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
|
||||
"getLLDPNeighbors": {Func: rpcGetLLDPNeighbors},
|
||||
}
|
||||
|
|
|
|||
21
network.go
21
network.go
|
|
@ -6,6 +6,7 @@ import (
|
|||
"reflect"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/confparser"
|
||||
"github.com/jetkvm/kvm/internal/lldp"
|
||||
"github.com/jetkvm/kvm/internal/mdns"
|
||||
"github.com/jetkvm/kvm/internal/network/types"
|
||||
"github.com/jetkvm/kvm/pkg/nmlite"
|
||||
|
|
@ -17,6 +18,7 @@ const (
|
|||
|
||||
var (
|
||||
networkManager *nmlite.NetworkManager
|
||||
lldpService *lldp.LLDP
|
||||
)
|
||||
|
||||
type RpcNetworkSettings struct {
|
||||
|
|
@ -161,6 +163,18 @@ func initNetwork() error {
|
|||
|
||||
networkManager = nm
|
||||
|
||||
lldpService = lldp.NewLLDP(&lldp.LLDPOptions{
|
||||
InterfaceName: NetIfName,
|
||||
EnableRx: nc.LLDPMode.String != "disabled",
|
||||
EnableTx: nc.LLDPMode.String != "disabled",
|
||||
OnChange: func(neighbors []lldp.Neighbor) {
|
||||
writeJSONRPCEvent("lldpNeighbors", neighbors, currentSession)
|
||||
},
|
||||
})
|
||||
if err := lldpService.Start(); err != nil {
|
||||
networkLogger.Error().Err(err).Msg("failed to start LLDP service")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -312,3 +326,10 @@ func rpcToggleDHCPClient() error {
|
|||
|
||||
return rpcReboot(true)
|
||||
}
|
||||
|
||||
func rpcGetLLDPNeighbors() []lldp.Neighbor {
|
||||
if lldpService == nil {
|
||||
return []lldp.Neighbor{}
|
||||
}
|
||||
return lldpService.GetNeighbors()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
import { cx } from "@/cva.config";
|
||||
|
||||
import { LLDPNeighbor } from "../hooks/stores";
|
||||
|
||||
import { GridCard } from "./Card";
|
||||
|
||||
|
||||
interface LLDPDataLineProps {
|
||||
label: string;
|
||||
value: string;
|
||||
className?: string;
|
||||
}
|
||||
const LLDPDataLine = ({ label, value, className }: LLDPDataLineProps) => {
|
||||
return (
|
||||
<div className={cx("flex flex-col justify-between", className)}>
|
||||
<span className="text-sm text-slate-600 dark:text-slate-400">
|
||||
{label}
|
||||
</span>
|
||||
<span className="text-sm font-medium">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 => {
|
||||
const displayName = neighbor.system_name || neighbor.port_description || neighbor.mac;
|
||||
return <div className="space-y-3" key={neighbor.mac}>
|
||||
<h4 className="text-sm font-semibold font-mono">{displayName}</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">
|
||||
|
||||
{neighbor.system_name && (
|
||||
<LLDPDataLine label="System Name" value={neighbor.system_name} />
|
||||
)}
|
||||
|
||||
{neighbor.system_description && (
|
||||
<LLDPDataLine label="System Description" value={neighbor.system_description} />
|
||||
)}
|
||||
|
||||
{neighbor.chassis_id && (
|
||||
<LLDPDataLine label="Chassis ID" value={neighbor.chassis_id} />
|
||||
)}
|
||||
|
||||
{neighbor.port_id && (
|
||||
<LLDPDataLine label="Port ID" value={neighbor.port_id} />
|
||||
)}
|
||||
|
||||
{neighbor.port_description && (
|
||||
<LLDPDataLine label="Port Description" value={neighbor.port_description} />
|
||||
)}
|
||||
|
||||
{neighbor.management_address && (
|
||||
<LLDPDataLine label="Management Address" value={neighbor.management_address} />
|
||||
)}
|
||||
|
||||
{neighbor.mac && (
|
||||
<LLDPDataLine label="MAC Address" value={neighbor.mac} className="font-mono" />
|
||||
)}
|
||||
|
||||
{neighbor.source && (
|
||||
<LLDPDataLine label="Source" value={neighbor.source} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</GridCard>
|
||||
);
|
||||
}
|
||||
|
|
@ -761,7 +761,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"
|
||||
|
|
@ -783,6 +783,19 @@ export interface IPv6StaticConfig {
|
|||
dns: string[];
|
||||
}
|
||||
|
||||
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 interface NetworkSettings {
|
||||
dhcp_client: string;
|
||||
hostname: string | null;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { TextAreaWithLabel } from "@components/TextArea";
|
|||
import { isOnDevice } from "@/main";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { sleep } from "@/utils";
|
||||
|
||||
export default function SettingsAdvancedRoute() {
|
||||
const { send } = useJsonRpc();
|
||||
|
|
@ -311,7 +312,7 @@ export default function SettingsAdvancedRoute() {
|
|||
size="SM"
|
||||
theme="light"
|
||||
text={m.advanced_reset_config_button()}
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
handleResetConfig();
|
||||
// Add 2s delay between resetting the configuration and calling reload() to prevent reload from interrupting the RPC call to reset things.
|
||||
await sleep(2000);
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@ import { useNavigate } from "react-router";
|
|||
import { useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import { Button } from "@components/Button";
|
||||
import { m } from "@localizations/messages.js";
|
||||
import { sleep } from "@/utils";
|
||||
|
||||
export default function SettingsGeneralRebootRoute() {
|
||||
const navigate = useNavigate();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
const onClose = useCallback(async () => {
|
||||
navigate(".."); // back to the devices.$id.settings page
|
||||
// Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation.
|
||||
await sleep(1000);
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default function SettingsGeneralUpdateRoute() {
|
|||
const { setModalView, otaState } = useUpdateStore();
|
||||
const { send } = useJsonRpc();
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
const onClose = useCallback(async () => {
|
||||
navigate(".."); // back to the devices.$id.settings page
|
||||
// Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation.
|
||||
await sleep(1000);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import dayjs from "dayjs";
|
|||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import validator from "validator";
|
||||
|
||||
import { NetworkSettings, NetworkState, useNetworkStateStore, useRTCStore } from "@hooks/stores";
|
||||
import { LLDPNeighbor, NetworkSettings, NetworkState, useNetworkStateStore, useRTCStore } from "@hooks/stores";
|
||||
import { useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import AutoHeight from "@components/AutoHeight";
|
||||
import { Button } from "@components/Button";
|
||||
|
|
@ -23,9 +23,10 @@ import StaticIpv4Card from "@components/StaticIpv4Card";
|
|||
import StaticIpv6Card from "@components/StaticIpv6Card";
|
||||
import { useCopyToClipboard } from "@components/useCopyToClipBoard";
|
||||
import { netMaskFromCidr4 } from "@/utils/ip";
|
||||
import { getNetworkSettings, getNetworkState } from "@/utils/jsonrpc";
|
||||
import { callJsonRpc, getNetworkSettings, getNetworkState } from "@/utils/jsonrpc";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages";
|
||||
import LLDPNeighCard from "@components/LLDPNeigh";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
|
|
@ -97,6 +98,20 @@ export default function SettingsNetworkRoute() {
|
|||
{ label: string; from: string; to: string }[]
|
||||
>([]);
|
||||
|
||||
const [lldpNeighbors, setLldpNeighbors] = useState<LLDPNeighbor[]>([]);
|
||||
const fetchLLDPNeighbors = useCallback(async () => {
|
||||
send("getLLDPNeighbors", {}, (resp) => {
|
||||
if ("error" in resp) {
|
||||
// notifications.error(m.network_lldp_neighbors_fetch_failed({ error: neighbors.error.message || m.unknown_error() }));
|
||||
} else {
|
||||
setLldpNeighbors(resp.result as LLDPNeighbor[]);
|
||||
}
|
||||
});
|
||||
}, [setLldpNeighbors, send]);
|
||||
useEffect(() => {
|
||||
fetchLLDPNeighbors();
|
||||
}, [fetchLLDPNeighbors]);
|
||||
|
||||
const fetchNetworkData = useCallback(async () => {
|
||||
try {
|
||||
console.log("Fetching network data...");
|
||||
|
|
@ -460,6 +475,12 @@ export default function SettingsNetworkRoute() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<div>
|
||||
<AutoHeight>
|
||||
<LLDPNeighCard neighbors={lldpNeighbors} />
|
||||
</AutoHeight>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<AutoHeight>
|
||||
{formState.isLoading ? (
|
||||
|
|
|
|||
Loading…
Reference in New Issue