mirror of https://github.com/jetkvm/kvm.git
Compare commits
4 Commits
7b859e44d9
...
09bbd9d780
| Author | SHA1 | Date |
|---|---|---|
|
|
09bbd9d780 | |
|
|
5f15d8b2f6 | |
|
|
366f7f3543 | |
|
|
f0595fff40 |
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -116,7 +116,7 @@ export interface RTCState {
|
|||
peerConnection: RTCPeerConnection | null;
|
||||
setPeerConnection: (pc: RTCState["peerConnection"]) => void;
|
||||
|
||||
setRpcDataChannel: (channel: RTCDataChannel) => void;
|
||||
setRpcDataChannel: (channel: RTCDataChannel | null) => void;
|
||||
rpcDataChannel: RTCDataChannel | null;
|
||||
|
||||
hidRpcDisabled: boolean;
|
||||
|
|
@ -178,41 +178,42 @@ export const useRTCStore = create<RTCState>(set => ({
|
|||
setPeerConnection: (pc: RTCState["peerConnection"]) => set({ peerConnection: pc }),
|
||||
|
||||
rpcDataChannel: null,
|
||||
setRpcDataChannel: (channel: RTCDataChannel) => set({ rpcDataChannel: channel }),
|
||||
setRpcDataChannel: channel => set({ rpcDataChannel: channel }),
|
||||
|
||||
hidRpcDisabled: false,
|
||||
setHidRpcDisabled: (disabled: boolean) => set({ hidRpcDisabled: disabled }),
|
||||
setHidRpcDisabled: disabled => set({ hidRpcDisabled: disabled }),
|
||||
|
||||
rpcHidProtocolVersion: null,
|
||||
setRpcHidProtocolVersion: (version: number | null) => set({ rpcHidProtocolVersion: version }),
|
||||
setRpcHidProtocolVersion: version => set({ rpcHidProtocolVersion: version }),
|
||||
|
||||
rpcHidChannel: null,
|
||||
setRpcHidChannel: (channel: RTCDataChannel) => set({ rpcHidChannel: channel }),
|
||||
setRpcHidChannel: channel => set({ rpcHidChannel: channel }),
|
||||
|
||||
rpcHidUnreliableChannel: null,
|
||||
setRpcHidUnreliableChannel: (channel: RTCDataChannel) => set({ rpcHidUnreliableChannel: channel }),
|
||||
setRpcHidUnreliableChannel: channel => set({ rpcHidUnreliableChannel: channel }),
|
||||
|
||||
rpcHidUnreliableNonOrderedChannel: null,
|
||||
setRpcHidUnreliableNonOrderedChannel: (channel: RTCDataChannel) => set({ rpcHidUnreliableNonOrderedChannel: channel }),
|
||||
setRpcHidUnreliableNonOrderedChannel: channel =>
|
||||
set({ rpcHidUnreliableNonOrderedChannel: channel }),
|
||||
|
||||
transceiver: null,
|
||||
setTransceiver: (transceiver: RTCRtpTransceiver) => set({ transceiver }),
|
||||
setTransceiver: transceiver => set({ transceiver }),
|
||||
|
||||
peerConnectionState: null,
|
||||
setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }),
|
||||
setPeerConnectionState: state => set({ peerConnectionState: state }),
|
||||
|
||||
mediaStream: null,
|
||||
setMediaStream: (stream: MediaStream) => set({ mediaStream: stream }),
|
||||
setMediaStream: stream => set({ mediaStream: stream }),
|
||||
|
||||
videoStreamStats: null,
|
||||
appendVideoStreamStats: (stats: RTCInboundRtpStreamStats) => set({ videoStreamStats: stats }),
|
||||
appendVideoStreamStats: stats => set({ videoStreamStats: stats }),
|
||||
videoStreamStatsHistory: new Map(),
|
||||
|
||||
isTurnServerInUse: false,
|
||||
setTurnServerInUse: (inUse: boolean) => set({ isTurnServerInUse: inUse }),
|
||||
setTurnServerInUse: inUse => set({ isTurnServerInUse: inUse }),
|
||||
|
||||
inboundRtpStats: new Map(),
|
||||
appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => {
|
||||
appendInboundRtpStats: stats => {
|
||||
set(prevState => ({
|
||||
inboundRtpStats: appendStatToMap(stats, prevState.inboundRtpStats),
|
||||
}));
|
||||
|
|
@ -220,7 +221,7 @@ export const useRTCStore = create<RTCState>(set => ({
|
|||
clearInboundRtpStats: () => set({ inboundRtpStats: new Map() }),
|
||||
|
||||
candidatePairStats: new Map(),
|
||||
appendCandidatePairStats: (stats: RTCIceCandidatePairStats) => {
|
||||
appendCandidatePairStats: stats => {
|
||||
set(prevState => ({
|
||||
candidatePairStats: appendStatToMap(stats, prevState.candidatePairStats),
|
||||
}));
|
||||
|
|
@ -228,21 +229,21 @@ export const useRTCStore = create<RTCState>(set => ({
|
|||
clearCandidatePairStats: () => set({ candidatePairStats: new Map() }),
|
||||
|
||||
localCandidateStats: new Map(),
|
||||
appendLocalCandidateStats: (stats: RTCIceCandidateStats) => {
|
||||
appendLocalCandidateStats: stats => {
|
||||
set(prevState => ({
|
||||
localCandidateStats: appendStatToMap(stats, prevState.localCandidateStats),
|
||||
}));
|
||||
},
|
||||
|
||||
remoteCandidateStats: new Map(),
|
||||
appendRemoteCandidateStats: (stats: RTCIceCandidateStats) => {
|
||||
appendRemoteCandidateStats: stats => {
|
||||
set(prevState => ({
|
||||
remoteCandidateStats: appendStatToMap(stats, prevState.remoteCandidateStats),
|
||||
}));
|
||||
},
|
||||
|
||||
diskDataChannelStats: new Map(),
|
||||
appendDiskDataChannelStats: (stats: RTCDataChannelStats) => {
|
||||
appendDiskDataChannelStats: stats => {
|
||||
set(prevState => ({
|
||||
diskDataChannelStats: appendStatToMap(stats, prevState.diskDataChannelStats),
|
||||
}));
|
||||
|
|
@ -250,7 +251,7 @@ export const useRTCStore = create<RTCState>(set => ({
|
|||
|
||||
// Add these new properties to the store implementation
|
||||
terminalChannel: null,
|
||||
setTerminalChannel: (channel: RTCDataChannel) => set({ terminalChannel: channel }),
|
||||
setTerminalChannel: channel => set({ terminalChannel: channel }),
|
||||
}));
|
||||
|
||||
export interface MouseMove {
|
||||
|
|
@ -270,12 +271,20 @@ export interface MouseState {
|
|||
export const useMouseStore = create<MouseState>(set => ({
|
||||
mouseX: 0,
|
||||
mouseY: 0,
|
||||
setMouseMove: (move?: MouseMove) => set({ mouseMove: move }),
|
||||
setMousePosition: (x: number, y: number) => set({ mouseX: x, mouseY: y }),
|
||||
setMouseMove: move => set({ mouseMove: move }),
|
||||
setMousePosition: (x, y) => set({ mouseX: x, mouseY: y }),
|
||||
}));
|
||||
|
||||
export type HdmiStates = "ready" | "no_signal" | "no_lock" | "out_of_range" | "connecting";
|
||||
export type HdmiErrorStates = Extract<VideoState["hdmiState"], "no_signal" | "no_lock" | "out_of_range">
|
||||
export type HdmiStates =
|
||||
| "ready"
|
||||
| "no_signal"
|
||||
| "no_lock"
|
||||
| "out_of_range"
|
||||
| "connecting";
|
||||
export type HdmiErrorStates = Extract<
|
||||
VideoState["hdmiState"],
|
||||
"no_signal" | "no_lock" | "out_of_range"
|
||||
>;
|
||||
|
||||
export interface HdmiState {
|
||||
ready: boolean;
|
||||
|
|
@ -290,10 +299,7 @@ export interface VideoState {
|
|||
setClientSize: (width: number, height: number) => void;
|
||||
setSize: (width: number, height: number) => void;
|
||||
hdmiState: HdmiStates;
|
||||
setHdmiState: (state: {
|
||||
ready: boolean;
|
||||
error?: HdmiErrorStates;
|
||||
}) => void;
|
||||
setHdmiState: (state: { ready: boolean; error?: HdmiErrorStates }) => void;
|
||||
}
|
||||
|
||||
export const useVideoStore = create<VideoState>(set => ({
|
||||
|
|
@ -304,7 +310,8 @@ export const useVideoStore = create<VideoState>(set => ({
|
|||
clientHeight: 0,
|
||||
|
||||
// The video element's client size
|
||||
setClientSize: (clientWidth: number, clientHeight: number) => set({ clientWidth, clientHeight }),
|
||||
setClientSize: (clientWidth: number, clientHeight: number) =>
|
||||
set({ clientWidth, clientHeight }),
|
||||
|
||||
// Resolution
|
||||
setSize: (width: number, height: number) => set({ width, height }),
|
||||
|
|
@ -451,13 +458,15 @@ export interface MountMediaState {
|
|||
|
||||
export const useMountMediaStore = create<MountMediaState>(set => ({
|
||||
remoteVirtualMediaState: null,
|
||||
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => set({ remoteVirtualMediaState: state }),
|
||||
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) =>
|
||||
set({ remoteVirtualMediaState: state }),
|
||||
|
||||
modalView: "mode",
|
||||
setModalView: (view: MountMediaState["modalView"]) => set({ modalView: view }),
|
||||
|
||||
isMountMediaDialogOpen: false,
|
||||
setIsMountMediaDialogOpen: (isOpen: MountMediaState["isMountMediaDialogOpen"]) => set({ isMountMediaDialogOpen: isOpen }),
|
||||
setIsMountMediaDialogOpen: (isOpen: MountMediaState["isMountMediaDialogOpen"]) =>
|
||||
set({ isMountMediaDialogOpen: isOpen }),
|
||||
|
||||
uploadedFiles: [],
|
||||
addUploadedFile: (file: { name: string; size: string; uploadedAt: string }) =>
|
||||
|
|
@ -474,7 +483,7 @@ export interface KeyboardLedState {
|
|||
compose: boolean;
|
||||
kana: boolean;
|
||||
shift: boolean; // Optional, as not all keyboards have a shift LED
|
||||
};
|
||||
}
|
||||
|
||||
export const hidKeyBufferSize = 6;
|
||||
export const hidErrorRollOver = 0x01;
|
||||
|
|
@ -509,14 +518,23 @@ export interface HidState {
|
|||
}
|
||||
|
||||
export const useHidStore = create<HidState>(set => ({
|
||||
keyboardLedState: { num_lock: false, caps_lock: false, scroll_lock: false, compose: false, kana: false, shift: false } as KeyboardLedState,
|
||||
setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }),
|
||||
keyboardLedState: {
|
||||
num_lock: false,
|
||||
caps_lock: false,
|
||||
scroll_lock: false,
|
||||
compose: false,
|
||||
kana: false,
|
||||
shift: false,
|
||||
} as KeyboardLedState,
|
||||
setKeyboardLedState: (ledState: KeyboardLedState): void =>
|
||||
set({ keyboardLedState: ledState }),
|
||||
|
||||
keysDownState: { modifier: 0, keys: [0, 0, 0, 0, 0, 0] } as KeysDownState,
|
||||
setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }),
|
||||
|
||||
isVirtualKeyboardEnabled: false,
|
||||
setVirtualKeyboardEnabled: (enabled: boolean): void => set({ isVirtualKeyboardEnabled: enabled }),
|
||||
setVirtualKeyboardEnabled: (enabled: boolean): void =>
|
||||
set({ isVirtualKeyboardEnabled: enabled }),
|
||||
|
||||
isPasteInProgress: false,
|
||||
setPasteModeEnabled: (enabled: boolean): void => set({ isPasteInProgress: enabled }),
|
||||
|
|
@ -568,7 +586,7 @@ export interface OtaState {
|
|||
|
||||
systemUpdateProgress: number;
|
||||
systemUpdatedAt: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateState {
|
||||
isUpdatePending: boolean;
|
||||
|
|
@ -580,7 +598,7 @@ export interface UpdateState {
|
|||
otaState: OtaState;
|
||||
setOtaState: (state: OtaState) => void;
|
||||
|
||||
modalView: UpdateModalViews
|
||||
modalView: UpdateModalViews;
|
||||
setModalView: (view: UpdateModalViews) => void;
|
||||
|
||||
updateErrorMessage: string | null;
|
||||
|
|
@ -620,12 +638,11 @@ export const useUpdateStore = create<UpdateState>(set => ({
|
|||
setModalView: (view: UpdateModalViews) => set({ modalView: view }),
|
||||
|
||||
updateErrorMessage: null,
|
||||
setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }),
|
||||
setUpdateErrorMessage: (errorMessage: string) =>
|
||||
set({ updateErrorMessage: errorMessage }),
|
||||
}));
|
||||
|
||||
export type UsbConfigModalViews =
|
||||
| "updateUsbConfig"
|
||||
| "updateUsbConfigSuccess";
|
||||
export type UsbConfigModalViews = "updateUsbConfig" | "updateUsbConfigSuccess";
|
||||
|
||||
export interface UsbConfigModalState {
|
||||
modalView: UsbConfigModalViews;
|
||||
|
|
@ -761,7 +778,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 +800,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;
|
||||
|
|
@ -833,12 +863,12 @@ export interface MacrosState {
|
|||
loadMacros: () => Promise<void>;
|
||||
saveMacros: (macros: KeySequence[]) => Promise<void>;
|
||||
sendFn:
|
||||
| ((
|
||||
method: string,
|
||||
params: unknown,
|
||||
callback?: ((resp: JsonRpcResponse) => void) | undefined,
|
||||
) => void)
|
||||
| null;
|
||||
| ((
|
||||
method: string,
|
||||
params: unknown,
|
||||
callback?: ((resp: JsonRpcResponse) => void) | undefined,
|
||||
) => void)
|
||||
| null;
|
||||
setSendFn: (
|
||||
sendFn: (
|
||||
method: string,
|
||||
|
|
@ -978,5 +1008,5 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
|||
} finally {
|
||||
set({ loading: false });
|
||||
}
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
|
|
|
|||
|
|
@ -557,8 +557,9 @@ export default function KvmIdRoute() {
|
|||
clearCandidatePairStats();
|
||||
setSidebarView(null);
|
||||
setPeerConnection(null);
|
||||
setRpcDataChannel(null);
|
||||
};
|
||||
}, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView]);
|
||||
}, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView, setRpcDataChannel]);
|
||||
|
||||
// TURN server usage detection
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -24,17 +24,47 @@ export interface JsonRpcCallResponse<T = unknown> {
|
|||
let rpcCallCounter = 0;
|
||||
|
||||
// Helper: wait for RTC data channel to be ready
|
||||
// This waits indefinitely for the channel to be ready, only aborting via the signal
|
||||
// Throws if the channel instance changed while waiting (stale connection detected)
|
||||
async function waitForRtcReady(signal: AbortSignal): Promise<RTCDataChannel> {
|
||||
const pollInterval = 100;
|
||||
let lastSeenChannel: RTCDataChannel | null = null;
|
||||
|
||||
while (!signal.aborted) {
|
||||
const state = useRTCStore.getState();
|
||||
if (state.rpcDataChannel?.readyState === "open") {
|
||||
return state.rpcDataChannel;
|
||||
const currentChannel = state.rpcDataChannel;
|
||||
|
||||
// Channel instance changed (new connection replaced old one)
|
||||
if (lastSeenChannel && currentChannel && lastSeenChannel !== currentChannel) {
|
||||
console.debug("[waitForRtcReady] Channel instance changed, aborting wait");
|
||||
throw new Error("RTC connection changed while waiting for readiness");
|
||||
}
|
||||
|
||||
// Channel was removed from store (connection closed)
|
||||
if (lastSeenChannel && !currentChannel) {
|
||||
console.debug("[waitForRtcReady] Channel was removed from store, aborting wait");
|
||||
throw new Error("RTC connection was closed while waiting for readiness");
|
||||
}
|
||||
|
||||
// No channel yet, keep waiting
|
||||
if (!currentChannel) {
|
||||
await sleep(pollInterval);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track this channel instance
|
||||
lastSeenChannel = currentChannel;
|
||||
|
||||
// Channel is ready!
|
||||
if (currentChannel.readyState === "open") {
|
||||
return currentChannel;
|
||||
}
|
||||
|
||||
await sleep(pollInterval);
|
||||
}
|
||||
|
||||
// Signal was aborted for some reason
|
||||
console.debug("[waitForRtcReady] Aborted via signal");
|
||||
throw new Error("RTC readiness check aborted");
|
||||
}
|
||||
|
||||
|
|
@ -97,25 +127,26 @@ export async function callJsonRpc<T = unknown>(
|
|||
const timeout = options.attemptTimeoutMs || 5000;
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
const abortController = new AbortController();
|
||||
const timeoutId = setTimeout(() => abortController.abort(), timeout);
|
||||
|
||||
// Exponential backoff for retries that starts at 500ms up to a maximum of 10 seconds
|
||||
const backoffMs = Math.min(500 * Math.pow(2, attempt), 10000);
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
try {
|
||||
// Wait for RTC readiness
|
||||
const rpcDataChannel = await waitForRtcReady(abortController.signal);
|
||||
// Wait for RTC readiness without timeout - this allows time for WebRTC to connect
|
||||
const readyAbortController = new AbortController();
|
||||
const rpcDataChannel = await waitForRtcReady(readyAbortController.signal);
|
||||
|
||||
// Now apply timeout only to the actual RPC request/response
|
||||
const rpcAbortController = new AbortController();
|
||||
timeoutId = setTimeout(() => rpcAbortController.abort(), timeout);
|
||||
|
||||
// Send RPC request and wait for response
|
||||
const response = await sendRpcRequest<T>(
|
||||
rpcDataChannel,
|
||||
options,
|
||||
abortController.signal,
|
||||
rpcAbortController.signal,
|
||||
);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Retry on error if attempts remain
|
||||
if (response.error && attempt < maxAttempts - 1) {
|
||||
await sleep(backoffMs);
|
||||
|
|
@ -124,8 +155,6 @@ export async function callJsonRpc<T = unknown>(
|
|||
|
||||
return response;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Retry on timeout/error if attempts remain
|
||||
if (attempt < maxAttempts - 1) {
|
||||
await sleep(backoffMs);
|
||||
|
|
@ -135,6 +164,10 @@ export async function callJsonRpc<T = unknown>(
|
|||
throw error instanceof Error
|
||||
? error
|
||||
: new Error(`JSON-RPC call failed after ${timeout}ms`);
|
||||
} finally {
|
||||
if (timeoutId !== null) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue