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:
|
- linters:
|
||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
path: internal/logging/sse.go
|
path: internal/logging/sse.go
|
||||||
|
- linters:
|
||||||
|
- govet
|
||||||
|
path: internal/lldp/bpf.go
|
||||||
paths:
|
paths:
|
||||||
- third_party$
|
- third_party$
|
||||||
- builtin$
|
- 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/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // 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/jonboulle/clockwork v0.5.0 // indirect
|
||||||
github.com/josharian/native v1.1.0 // indirect
|
github.com/josharian/native v1.1.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // 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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
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/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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
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=
|
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/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 h1:nu5z6Kg+gMNW6tdqnVjg/QEJ8Nw71IJQqOtWj00XHEU=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20250919081422-f80a1952f48e/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo=
|
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 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
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=
|
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=
|
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 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
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 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
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 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
|
||||||
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
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 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
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 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
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-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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/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/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 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
|
||||||
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
|
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 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
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 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
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=
|
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"}},
|
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
|
||||||
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
|
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
|
||||||
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
|
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
|
||||||
|
"getLLDPNeighbors": {Func: rpcGetLLDPNeighbors},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
network.go
21
network.go
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/internal/confparser"
|
"github.com/jetkvm/kvm/internal/confparser"
|
||||||
|
"github.com/jetkvm/kvm/internal/lldp"
|
||||||
"github.com/jetkvm/kvm/internal/mdns"
|
"github.com/jetkvm/kvm/internal/mdns"
|
||||||
"github.com/jetkvm/kvm/internal/network/types"
|
"github.com/jetkvm/kvm/internal/network/types"
|
||||||
"github.com/jetkvm/kvm/pkg/nmlite"
|
"github.com/jetkvm/kvm/pkg/nmlite"
|
||||||
|
|
@ -17,6 +18,7 @@ const (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
networkManager *nmlite.NetworkManager
|
networkManager *nmlite.NetworkManager
|
||||||
|
lldpService *lldp.LLDP
|
||||||
)
|
)
|
||||||
|
|
||||||
type RpcNetworkSettings struct {
|
type RpcNetworkSettings struct {
|
||||||
|
|
@ -161,6 +163,18 @@ func initNetwork() error {
|
||||||
|
|
||||||
networkManager = nm
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,3 +326,10 @@ func rpcToggleDHCPClient() error {
|
||||||
|
|
||||||
return rpcReboot(true)
|
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;
|
peerConnection: RTCPeerConnection | null;
|
||||||
setPeerConnection: (pc: RTCState["peerConnection"]) => void;
|
setPeerConnection: (pc: RTCState["peerConnection"]) => void;
|
||||||
|
|
||||||
setRpcDataChannel: (channel: RTCDataChannel) => void;
|
setRpcDataChannel: (channel: RTCDataChannel | null) => void;
|
||||||
rpcDataChannel: RTCDataChannel | null;
|
rpcDataChannel: RTCDataChannel | null;
|
||||||
|
|
||||||
hidRpcDisabled: boolean;
|
hidRpcDisabled: boolean;
|
||||||
|
|
@ -178,41 +178,42 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||||
setPeerConnection: (pc: RTCState["peerConnection"]) => set({ peerConnection: pc }),
|
setPeerConnection: (pc: RTCState["peerConnection"]) => set({ peerConnection: pc }),
|
||||||
|
|
||||||
rpcDataChannel: null,
|
rpcDataChannel: null,
|
||||||
setRpcDataChannel: (channel: RTCDataChannel) => set({ rpcDataChannel: channel }),
|
setRpcDataChannel: channel => set({ rpcDataChannel: channel }),
|
||||||
|
|
||||||
hidRpcDisabled: false,
|
hidRpcDisabled: false,
|
||||||
setHidRpcDisabled: (disabled: boolean) => set({ hidRpcDisabled: disabled }),
|
setHidRpcDisabled: disabled => set({ hidRpcDisabled: disabled }),
|
||||||
|
|
||||||
rpcHidProtocolVersion: null,
|
rpcHidProtocolVersion: null,
|
||||||
setRpcHidProtocolVersion: (version: number | null) => set({ rpcHidProtocolVersion: version }),
|
setRpcHidProtocolVersion: version => set({ rpcHidProtocolVersion: version }),
|
||||||
|
|
||||||
rpcHidChannel: null,
|
rpcHidChannel: null,
|
||||||
setRpcHidChannel: (channel: RTCDataChannel) => set({ rpcHidChannel: channel }),
|
setRpcHidChannel: channel => set({ rpcHidChannel: channel }),
|
||||||
|
|
||||||
rpcHidUnreliableChannel: null,
|
rpcHidUnreliableChannel: null,
|
||||||
setRpcHidUnreliableChannel: (channel: RTCDataChannel) => set({ rpcHidUnreliableChannel: channel }),
|
setRpcHidUnreliableChannel: channel => set({ rpcHidUnreliableChannel: channel }),
|
||||||
|
|
||||||
rpcHidUnreliableNonOrderedChannel: null,
|
rpcHidUnreliableNonOrderedChannel: null,
|
||||||
setRpcHidUnreliableNonOrderedChannel: (channel: RTCDataChannel) => set({ rpcHidUnreliableNonOrderedChannel: channel }),
|
setRpcHidUnreliableNonOrderedChannel: channel =>
|
||||||
|
set({ rpcHidUnreliableNonOrderedChannel: channel }),
|
||||||
|
|
||||||
transceiver: null,
|
transceiver: null,
|
||||||
setTransceiver: (transceiver: RTCRtpTransceiver) => set({ transceiver }),
|
setTransceiver: transceiver => set({ transceiver }),
|
||||||
|
|
||||||
peerConnectionState: null,
|
peerConnectionState: null,
|
||||||
setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }),
|
setPeerConnectionState: state => set({ peerConnectionState: state }),
|
||||||
|
|
||||||
mediaStream: null,
|
mediaStream: null,
|
||||||
setMediaStream: (stream: MediaStream) => set({ mediaStream: stream }),
|
setMediaStream: stream => set({ mediaStream: stream }),
|
||||||
|
|
||||||
videoStreamStats: null,
|
videoStreamStats: null,
|
||||||
appendVideoStreamStats: (stats: RTCInboundRtpStreamStats) => set({ videoStreamStats: stats }),
|
appendVideoStreamStats: stats => set({ videoStreamStats: stats }),
|
||||||
videoStreamStatsHistory: new Map(),
|
videoStreamStatsHistory: new Map(),
|
||||||
|
|
||||||
isTurnServerInUse: false,
|
isTurnServerInUse: false,
|
||||||
setTurnServerInUse: (inUse: boolean) => set({ isTurnServerInUse: inUse }),
|
setTurnServerInUse: inUse => set({ isTurnServerInUse: inUse }),
|
||||||
|
|
||||||
inboundRtpStats: new Map(),
|
inboundRtpStats: new Map(),
|
||||||
appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => {
|
appendInboundRtpStats: stats => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
inboundRtpStats: appendStatToMap(stats, prevState.inboundRtpStats),
|
inboundRtpStats: appendStatToMap(stats, prevState.inboundRtpStats),
|
||||||
}));
|
}));
|
||||||
|
|
@ -220,7 +221,7 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||||
clearInboundRtpStats: () => set({ inboundRtpStats: new Map() }),
|
clearInboundRtpStats: () => set({ inboundRtpStats: new Map() }),
|
||||||
|
|
||||||
candidatePairStats: new Map(),
|
candidatePairStats: new Map(),
|
||||||
appendCandidatePairStats: (stats: RTCIceCandidatePairStats) => {
|
appendCandidatePairStats: stats => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
candidatePairStats: appendStatToMap(stats, prevState.candidatePairStats),
|
candidatePairStats: appendStatToMap(stats, prevState.candidatePairStats),
|
||||||
}));
|
}));
|
||||||
|
|
@ -228,21 +229,21 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||||
clearCandidatePairStats: () => set({ candidatePairStats: new Map() }),
|
clearCandidatePairStats: () => set({ candidatePairStats: new Map() }),
|
||||||
|
|
||||||
localCandidateStats: new Map(),
|
localCandidateStats: new Map(),
|
||||||
appendLocalCandidateStats: (stats: RTCIceCandidateStats) => {
|
appendLocalCandidateStats: stats => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
localCandidateStats: appendStatToMap(stats, prevState.localCandidateStats),
|
localCandidateStats: appendStatToMap(stats, prevState.localCandidateStats),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
remoteCandidateStats: new Map(),
|
remoteCandidateStats: new Map(),
|
||||||
appendRemoteCandidateStats: (stats: RTCIceCandidateStats) => {
|
appendRemoteCandidateStats: stats => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
remoteCandidateStats: appendStatToMap(stats, prevState.remoteCandidateStats),
|
remoteCandidateStats: appendStatToMap(stats, prevState.remoteCandidateStats),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
diskDataChannelStats: new Map(),
|
diskDataChannelStats: new Map(),
|
||||||
appendDiskDataChannelStats: (stats: RTCDataChannelStats) => {
|
appendDiskDataChannelStats: stats => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
diskDataChannelStats: appendStatToMap(stats, prevState.diskDataChannelStats),
|
diskDataChannelStats: appendStatToMap(stats, prevState.diskDataChannelStats),
|
||||||
}));
|
}));
|
||||||
|
|
@ -250,7 +251,7 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||||
|
|
||||||
// Add these new properties to the store implementation
|
// Add these new properties to the store implementation
|
||||||
terminalChannel: null,
|
terminalChannel: null,
|
||||||
setTerminalChannel: (channel: RTCDataChannel) => set({ terminalChannel: channel }),
|
setTerminalChannel: channel => set({ terminalChannel: channel }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export interface MouseMove {
|
export interface MouseMove {
|
||||||
|
|
@ -270,12 +271,20 @@ export interface MouseState {
|
||||||
export const useMouseStore = create<MouseState>(set => ({
|
export const useMouseStore = create<MouseState>(set => ({
|
||||||
mouseX: 0,
|
mouseX: 0,
|
||||||
mouseY: 0,
|
mouseY: 0,
|
||||||
setMouseMove: (move?: MouseMove) => set({ mouseMove: move }),
|
setMouseMove: move => set({ mouseMove: move }),
|
||||||
setMousePosition: (x: number, y: number) => set({ mouseX: x, mouseY: y }),
|
setMousePosition: (x, y) => set({ mouseX: x, mouseY: y }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type HdmiStates = "ready" | "no_signal" | "no_lock" | "out_of_range" | "connecting";
|
export type HdmiStates =
|
||||||
export type HdmiErrorStates = Extract<VideoState["hdmiState"], "no_signal" | "no_lock" | "out_of_range">
|
| "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 {
|
export interface HdmiState {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
|
|
@ -290,10 +299,7 @@ export interface VideoState {
|
||||||
setClientSize: (width: number, height: number) => void;
|
setClientSize: (width: number, height: number) => void;
|
||||||
setSize: (width: number, height: number) => void;
|
setSize: (width: number, height: number) => void;
|
||||||
hdmiState: HdmiStates;
|
hdmiState: HdmiStates;
|
||||||
setHdmiState: (state: {
|
setHdmiState: (state: { ready: boolean; error?: HdmiErrorStates }) => void;
|
||||||
ready: boolean;
|
|
||||||
error?: HdmiErrorStates;
|
|
||||||
}) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useVideoStore = create<VideoState>(set => ({
|
export const useVideoStore = create<VideoState>(set => ({
|
||||||
|
|
@ -304,7 +310,8 @@ export const useVideoStore = create<VideoState>(set => ({
|
||||||
clientHeight: 0,
|
clientHeight: 0,
|
||||||
|
|
||||||
// The video element's client size
|
// The video element's client size
|
||||||
setClientSize: (clientWidth: number, clientHeight: number) => set({ clientWidth, clientHeight }),
|
setClientSize: (clientWidth: number, clientHeight: number) =>
|
||||||
|
set({ clientWidth, clientHeight }),
|
||||||
|
|
||||||
// Resolution
|
// Resolution
|
||||||
setSize: (width: number, height: number) => set({ width, height }),
|
setSize: (width: number, height: number) => set({ width, height }),
|
||||||
|
|
@ -451,13 +458,15 @@ export interface MountMediaState {
|
||||||
|
|
||||||
export const useMountMediaStore = create<MountMediaState>(set => ({
|
export const useMountMediaStore = create<MountMediaState>(set => ({
|
||||||
remoteVirtualMediaState: null,
|
remoteVirtualMediaState: null,
|
||||||
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => set({ remoteVirtualMediaState: state }),
|
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) =>
|
||||||
|
set({ remoteVirtualMediaState: state }),
|
||||||
|
|
||||||
modalView: "mode",
|
modalView: "mode",
|
||||||
setModalView: (view: MountMediaState["modalView"]) => set({ modalView: view }),
|
setModalView: (view: MountMediaState["modalView"]) => set({ modalView: view }),
|
||||||
|
|
||||||
isMountMediaDialogOpen: false,
|
isMountMediaDialogOpen: false,
|
||||||
setIsMountMediaDialogOpen: (isOpen: MountMediaState["isMountMediaDialogOpen"]) => set({ isMountMediaDialogOpen: isOpen }),
|
setIsMountMediaDialogOpen: (isOpen: MountMediaState["isMountMediaDialogOpen"]) =>
|
||||||
|
set({ isMountMediaDialogOpen: isOpen }),
|
||||||
|
|
||||||
uploadedFiles: [],
|
uploadedFiles: [],
|
||||||
addUploadedFile: (file: { name: string; size: string; uploadedAt: string }) =>
|
addUploadedFile: (file: { name: string; size: string; uploadedAt: string }) =>
|
||||||
|
|
@ -474,7 +483,7 @@ export interface KeyboardLedState {
|
||||||
compose: boolean;
|
compose: boolean;
|
||||||
kana: boolean;
|
kana: boolean;
|
||||||
shift: boolean; // Optional, as not all keyboards have a shift LED
|
shift: boolean; // Optional, as not all keyboards have a shift LED
|
||||||
};
|
}
|
||||||
|
|
||||||
export const hidKeyBufferSize = 6;
|
export const hidKeyBufferSize = 6;
|
||||||
export const hidErrorRollOver = 0x01;
|
export const hidErrorRollOver = 0x01;
|
||||||
|
|
@ -509,14 +518,23 @@ export interface HidState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHidStore = create<HidState>(set => ({
|
export const useHidStore = create<HidState>(set => ({
|
||||||
keyboardLedState: { num_lock: false, caps_lock: false, scroll_lock: false, compose: false, kana: false, shift: false } as KeyboardLedState,
|
keyboardLedState: {
|
||||||
setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }),
|
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,
|
keysDownState: { modifier: 0, keys: [0, 0, 0, 0, 0, 0] } as KeysDownState,
|
||||||
setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }),
|
setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }),
|
||||||
|
|
||||||
isVirtualKeyboardEnabled: false,
|
isVirtualKeyboardEnabled: false,
|
||||||
setVirtualKeyboardEnabled: (enabled: boolean): void => set({ isVirtualKeyboardEnabled: enabled }),
|
setVirtualKeyboardEnabled: (enabled: boolean): void =>
|
||||||
|
set({ isVirtualKeyboardEnabled: enabled }),
|
||||||
|
|
||||||
isPasteInProgress: false,
|
isPasteInProgress: false,
|
||||||
setPasteModeEnabled: (enabled: boolean): void => set({ isPasteInProgress: enabled }),
|
setPasteModeEnabled: (enabled: boolean): void => set({ isPasteInProgress: enabled }),
|
||||||
|
|
@ -568,7 +586,7 @@ export interface OtaState {
|
||||||
|
|
||||||
systemUpdateProgress: number;
|
systemUpdateProgress: number;
|
||||||
systemUpdatedAt: string | null;
|
systemUpdatedAt: string | null;
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface UpdateState {
|
export interface UpdateState {
|
||||||
isUpdatePending: boolean;
|
isUpdatePending: boolean;
|
||||||
|
|
@ -580,7 +598,7 @@ export interface UpdateState {
|
||||||
otaState: OtaState;
|
otaState: OtaState;
|
||||||
setOtaState: (state: OtaState) => void;
|
setOtaState: (state: OtaState) => void;
|
||||||
|
|
||||||
modalView: UpdateModalViews
|
modalView: UpdateModalViews;
|
||||||
setModalView: (view: UpdateModalViews) => void;
|
setModalView: (view: UpdateModalViews) => void;
|
||||||
|
|
||||||
updateErrorMessage: string | null;
|
updateErrorMessage: string | null;
|
||||||
|
|
@ -620,12 +638,11 @@ export const useUpdateStore = create<UpdateState>(set => ({
|
||||||
setModalView: (view: UpdateModalViews) => set({ modalView: view }),
|
setModalView: (view: UpdateModalViews) => set({ modalView: view }),
|
||||||
|
|
||||||
updateErrorMessage: null,
|
updateErrorMessage: null,
|
||||||
setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }),
|
setUpdateErrorMessage: (errorMessage: string) =>
|
||||||
|
set({ updateErrorMessage: errorMessage }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type UsbConfigModalViews =
|
export type UsbConfigModalViews = "updateUsbConfig" | "updateUsbConfigSuccess";
|
||||||
| "updateUsbConfig"
|
|
||||||
| "updateUsbConfigSuccess";
|
|
||||||
|
|
||||||
export interface UsbConfigModalState {
|
export interface UsbConfigModalState {
|
||||||
modalView: UsbConfigModalViews;
|
modalView: UsbConfigModalViews;
|
||||||
|
|
@ -761,7 +778,7 @@ export type IPv6Mode =
|
||||||
| "link_local"
|
| "link_local"
|
||||||
| "unknown";
|
| "unknown";
|
||||||
export type IPv4Mode = "disabled" | "static" | "dhcp" | "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 mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown";
|
||||||
export type TimeSyncMode =
|
export type TimeSyncMode =
|
||||||
| "ntp_only"
|
| "ntp_only"
|
||||||
|
|
@ -783,6 +800,19 @@ export interface IPv6StaticConfig {
|
||||||
dns: string[];
|
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 {
|
export interface NetworkSettings {
|
||||||
dhcp_client: string;
|
dhcp_client: string;
|
||||||
hostname: string | null;
|
hostname: string | null;
|
||||||
|
|
@ -978,5 +1008,5 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
} finally {
|
} finally {
|
||||||
set({ loading: false });
|
set({ loading: false });
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import dayjs from "dayjs";
|
||||||
import relativeTime from "dayjs/plugin/relativeTime";
|
import relativeTime from "dayjs/plugin/relativeTime";
|
||||||
import validator from "validator";
|
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 { useJsonRpc } from "@hooks/useJsonRpc";
|
||||||
import AutoHeight from "@components/AutoHeight";
|
import AutoHeight from "@components/AutoHeight";
|
||||||
import { Button } from "@components/Button";
|
import { Button } from "@components/Button";
|
||||||
|
|
@ -23,9 +23,10 @@ import StaticIpv4Card from "@components/StaticIpv4Card";
|
||||||
import StaticIpv6Card from "@components/StaticIpv6Card";
|
import StaticIpv6Card from "@components/StaticIpv6Card";
|
||||||
import { useCopyToClipboard } from "@components/useCopyToClipBoard";
|
import { useCopyToClipboard } from "@components/useCopyToClipBoard";
|
||||||
import { netMaskFromCidr4 } from "@/utils/ip";
|
import { netMaskFromCidr4 } from "@/utils/ip";
|
||||||
import { getNetworkSettings, getNetworkState } from "@/utils/jsonrpc";
|
import { callJsonRpc, getNetworkSettings, getNetworkState } from "@/utils/jsonrpc";
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
import { m } from "@localizations/messages";
|
import { m } from "@localizations/messages";
|
||||||
|
import LLDPNeighCard from "@components/LLDPNeigh";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
|
@ -97,6 +98,20 @@ export default function SettingsNetworkRoute() {
|
||||||
{ label: string; from: string; to: string }[]
|
{ 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 () => {
|
const fetchNetworkData = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
console.log("Fetching network data...");
|
console.log("Fetching network data...");
|
||||||
|
|
@ -460,6 +475,12 @@ export default function SettingsNetworkRoute() {
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<AutoHeight>
|
||||||
|
<LLDPNeighCard neighbors={lldpNeighbors} />
|
||||||
|
</AutoHeight>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<AutoHeight>
|
<AutoHeight>
|
||||||
{formState.isLoading ? (
|
{formState.isLoading ? (
|
||||||
|
|
|
||||||
|
|
@ -557,8 +557,9 @@ export default function KvmIdRoute() {
|
||||||
clearCandidatePairStats();
|
clearCandidatePairStats();
|
||||||
setSidebarView(null);
|
setSidebarView(null);
|
||||||
setPeerConnection(null);
|
setPeerConnection(null);
|
||||||
|
setRpcDataChannel(null);
|
||||||
};
|
};
|
||||||
}, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView]);
|
}, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView, setRpcDataChannel]);
|
||||||
|
|
||||||
// TURN server usage detection
|
// TURN server usage detection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -24,17 +24,47 @@ export interface JsonRpcCallResponse<T = unknown> {
|
||||||
let rpcCallCounter = 0;
|
let rpcCallCounter = 0;
|
||||||
|
|
||||||
// Helper: wait for RTC data channel to be ready
|
// 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> {
|
async function waitForRtcReady(signal: AbortSignal): Promise<RTCDataChannel> {
|
||||||
const pollInterval = 100;
|
const pollInterval = 100;
|
||||||
|
let lastSeenChannel: RTCDataChannel | null = null;
|
||||||
|
|
||||||
while (!signal.aborted) {
|
while (!signal.aborted) {
|
||||||
const state = useRTCStore.getState();
|
const state = useRTCStore.getState();
|
||||||
if (state.rpcDataChannel?.readyState === "open") {
|
const currentChannel = state.rpcDataChannel;
|
||||||
return 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);
|
await sleep(pollInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Signal was aborted for some reason
|
||||||
|
console.debug("[waitForRtcReady] Aborted via signal");
|
||||||
throw new Error("RTC readiness check aborted");
|
throw new Error("RTC readiness check aborted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,25 +127,26 @@ export async function callJsonRpc<T = unknown>(
|
||||||
const timeout = options.attemptTimeoutMs || 5000;
|
const timeout = options.attemptTimeoutMs || 5000;
|
||||||
|
|
||||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
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
|
// 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);
|
const backoffMs = Math.min(500 * Math.pow(2, attempt), 10000);
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Wait for RTC readiness
|
// Wait for RTC readiness without timeout - this allows time for WebRTC to connect
|
||||||
const rpcDataChannel = await waitForRtcReady(abortController.signal);
|
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
|
// Send RPC request and wait for response
|
||||||
const response = await sendRpcRequest<T>(
|
const response = await sendRpcRequest<T>(
|
||||||
rpcDataChannel,
|
rpcDataChannel,
|
||||||
options,
|
options,
|
||||||
abortController.signal,
|
rpcAbortController.signal,
|
||||||
);
|
);
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
// Retry on error if attempts remain
|
// Retry on error if attempts remain
|
||||||
if (response.error && attempt < maxAttempts - 1) {
|
if (response.error && attempt < maxAttempts - 1) {
|
||||||
await sleep(backoffMs);
|
await sleep(backoffMs);
|
||||||
|
|
@ -124,8 +155,6 @@ export async function callJsonRpc<T = unknown>(
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
// Retry on timeout/error if attempts remain
|
// Retry on timeout/error if attempts remain
|
||||||
if (attempt < maxAttempts - 1) {
|
if (attempt < maxAttempts - 1) {
|
||||||
await sleep(backoffMs);
|
await sleep(backoffMs);
|
||||||
|
|
@ -135,6 +164,10 @@ export async function callJsonRpc<T = unknown>(
|
||||||
throw error instanceof Error
|
throw error instanceof Error
|
||||||
? error
|
? error
|
||||||
: new Error(`JSON-RPC call failed after ${timeout}ms`);
|
: new Error(`JSON-RPC call failed after ${timeout}ms`);
|
||||||
|
} finally {
|
||||||
|
if (timeoutId !== null) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue