feat: add LLDP transmit support

This commit is contained in:
Siyuan 2025-11-06 14:12:49 +00:00
parent 366f7f3543
commit fd44ff49fd
13 changed files with 673 additions and 117 deletions

View File

@ -10,5 +10,6 @@
] ]
}, },
"git.ignoreLimitWarning": true, "git.ignoreLimitWarning": true,
"cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo" "cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo",
"cmake.ignoreCMakeListsMissing": true
} }

1
go.mod
View File

@ -64,6 +64,7 @@ require (
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 // indirect
github.com/mdlayher/packet v1.1.2 // indirect github.com/mdlayher/packet v1.1.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect github.com/mdlayher/socket v0.4.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect

13
go.sum
View File

@ -57,6 +57,8 @@ github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAu
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
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=
@ -76,6 +78,7 @@ github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP
github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= 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.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
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=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
@ -103,10 +106,14 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 h1:2oDp6OOhLxQ9JBoUuysVz9UZ9uI6oLUbvAZu0x8o+vE=
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118/go.mod h1:ZFUnHIVchZ9lJoWoEGUg8Q3M4U8aNNWA3CVSUTkW4og=
github.com/mdlayher/ndp v1.1.0 h1:QylGKGVtH60sKZUE88+IW5ila1Z/M9/OXhWdsVKuscs= github.com/mdlayher/ndp v1.1.0 h1:QylGKGVtH60sKZUE88+IW5ila1Z/M9/OXhWdsVKuscs=
github.com/mdlayher/ndp v1.1.0/go.mod h1:FmgESgemgjl38vuOIyAHWUUL6vQKA/pQNkvXdWsdQFM= github.com/mdlayher/ndp v1.1.0/go.mod h1:FmgESgemgjl38vuOIyAHWUUL6vQKA/pQNkvXdWsdQFM=
github.com/mdlayher/packet v1.0.0/go.mod h1:eE7/ctqDhoiRhQ44ko5JZU2zxB88g+JH/6jmnjzPjOU=
github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -217,16 +224,21 @@ golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 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/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-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/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-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=
@ -242,6 +254,7 @@ 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/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= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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=

View File

@ -16,9 +16,15 @@ const (
afPacketSnaplen = 9216 afPacketSnaplen = 9216
) )
// afpacketComputeSize computes the block_size and the num_blocks in such a way that the
// allocated mmap buffer is close to but smaller than targetSizeMb.
// The restriction is that the blockSize must be divisible by both the
// frameSize and pageSize.
//
// See also: https://github.com/google/gopacket/blob/master/examples/afpacket/afpacket.go#L118
func afPacketComputeSize( func afPacketComputeSize(
targetSizeMb int, targetSizeMb int,
snaplen int, snapLen int,
pageSize int, pageSize int,
) ( ) (
frameSize int, frameSize int,
@ -26,10 +32,17 @@ func afPacketComputeSize(
numBlocks int, numBlocks int,
err error, err error,
) { ) {
if snaplen < pageSize { if snapLen < pageSize {
frameSize = pageSize / (pageSize / snaplen) // When snapLen < pageSize, find the largest value <= pageSize that
// is a multiple of snapLen and divides evenly into pageSize.
// This ensures frameSize is a divisor of pageSize.
// Example: snapLen=512, pageSize=4096 -> frameSize=512
// Example: snapLen=1000, pageSize=4096 -> frameSize=1024
frameSize = pageSize / (pageSize / snapLen)
} else { } else {
frameSize = (snaplen/pageSize + 1) * pageSize // When snapLen >= pageSize, round up to the next multiple of pageSize.
// Example: snapLen=9216, pageSize=4096 -> frameSize=12288 (3 pages)
frameSize = ((snapLen / pageSize) + 1) * pageSize
} }
// 128 is the default from the gopacket library so just use that // 128 is the default from the gopacket library so just use that
@ -37,7 +50,7 @@ func afPacketComputeSize(
numBlocks = (targetSizeMb * 1024 * 1024) / blockSize numBlocks = (targetSizeMb * 1024 * 1024) / blockSize
if numBlocks == 0 { if numBlocks == 0 {
return 0, 0, 0, fmt.Errorf("interface buffersize is too small") return 0, 0, 0, fmt.Errorf("interface bufferSize is too small")
} }
return frameSize, blockSize, numBlocks, nil return frameSize, blockSize, numBlocks, nil

View File

@ -1,6 +1,9 @@
package lldp package lldp
import ( import (
"context"
"fmt"
"sync"
"time" "time"
"github.com/google/gopacket" "github.com/google/gopacket"
@ -13,30 +16,50 @@ import (
var defaultLogger = logging.GetSubsystemLogger("lldp") var defaultLogger = logging.GetSubsystemLogger("lldp")
type LLDP struct { type LLDP struct {
mu sync.RWMutex
l *zerolog.Logger l *zerolog.Logger
tPacket *afpacket.TPacket tPacketRx *afpacket.TPacket
pktSource *gopacket.PacketSource tPacketTx *afpacket.TPacket
pktSourceRx *gopacket.PacketSource
enableRx bool enableRx bool
enableTx bool enableTx bool
packets chan gopacket.Packet packets chan gopacket.Packet
interfaceName string interfaceName string
stop chan struct{} advertiseOptions *AdvertiseOptions
onChange func(neighbors []Neighbor) onChange func(neighbors []Neighbor)
neighbors *ttlcache.Cache[string, Neighbor] neighbors *ttlcache.Cache[string, Neighbor]
// State tracking
rxRunning bool
txRunning bool
txCtx context.Context
txCancel context.CancelFunc
rxCtx context.Context
rxCancel context.CancelFunc
} }
type LLDPOptions struct { type AdvertiseOptions struct {
SysName string
SysDescription string
PortDescription string
SysCapabilities []string
EnabledCapabilities []string
}
type Options struct {
InterfaceName string InterfaceName string
AdvertiseOptions *AdvertiseOptions
EnableRx bool EnableRx bool
EnableTx bool EnableTx bool
OnChange func(neighbors []Neighbor) OnChange func(neighbors []Neighbor)
Logger *zerolog.Logger Logger *zerolog.Logger
} }
func NewLLDP(opts *LLDPOptions) *LLDP { func NewLLDP(opts *Options) *LLDP {
if opts.Logger == nil { if opts.Logger == nil {
opts.Logger = defaultLogger opts.Logger = defaultLogger
} }
@ -47,28 +70,76 @@ func NewLLDP(opts *LLDPOptions) *LLDP {
return &LLDP{ return &LLDP{
interfaceName: opts.InterfaceName, interfaceName: opts.InterfaceName,
advertiseOptions: opts.AdvertiseOptions,
enableRx: opts.EnableRx, enableRx: opts.EnableRx,
enableTx: opts.EnableTx, enableTx: opts.EnableTx,
l: opts.Logger, l: opts.Logger,
neighbors: ttlcache.New(ttlcache.WithTTL[string, Neighbor](1 * time.Hour)), neighbors: ttlcache.New(ttlcache.WithTTL[string, Neighbor](1 * time.Hour)),
onChange: opts.OnChange,
} }
} }
func (l *LLDP) Start() error { 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() go l.neighbors.Start()
if l.enableRx {
if err := l.startRx(); err != nil {
return fmt.Errorf("failed to start RX: %w", err)
}
}
// Start TX if enabled
if l.enableTx {
if err := l.startTx(); err != nil {
return fmt.Errorf("failed to start TX: %w", err)
}
}
return nil
}
// StartRx starts the LLDP receiver if not already running
func (l *LLDP) startRx() error {
l.mu.Lock()
running := l.rxRunning
enabled := l.enableRx
l.mu.Unlock()
if running || !enabled {
return nil
}
if err := l.setUpCapture(); err != nil {
return fmt.Errorf("failed to set up capture: %w", err)
}
return l.startCapture()
}
// StopRx stops the LLDP receiver if running
func (l *LLDP) StopRx() error {
return l.stopCapture()
}
// StopTx stops the LLDP transmitter if running
func (l *LLDP) StopTx() error {
return l.stopTx()
}
// SetAdvertiseOptions updates the advertise options and resends LLDP packets if TX is running
func (l *LLDP) SetAdvertiseOptions(opts *AdvertiseOptions) error {
l.mu.Lock()
txRunning := l.txRunning
l.advertiseOptions = opts
l.mu.Unlock()
if txRunning {
// Immediately resend with new options
if err := l.sendTxPackets(); err != nil {
return fmt.Errorf("failed to resend LLDP packet with new options: %w", err)
}
l.l.Info().Msg("advertise options changed, resent LLDP packet")
}
return nil return nil
} }

View File

@ -1,6 +1,19 @@
package lldp package lldp
import "time" import (
"fmt"
"sort"
"strings"
"time"
)
type ManagementAddress struct {
AddressFamily string `json:"address_family"`
Address string `json:"address"`
InterfaceSubtype string `json:"interface_subtype"`
InterfaceNumber uint32 `json:"interface_number"`
OID string `json:"oid,omitempty"`
}
type Neighbor struct { type Neighbor struct {
Mac string `json:"mac"` Mac string `json:"mac"`
@ -11,17 +24,24 @@ type Neighbor struct {
SystemName string `json:"system_name"` SystemName string `json:"system_name"`
SystemDescription string `json:"system_description"` SystemDescription string `json:"system_description"`
TTL uint16 `json:"ttl"` TTL uint16 `json:"ttl"`
ManagementAddress string `json:"management_address"` ManagementAddress *ManagementAddress `json:"management_address,omitempty"`
Capabilities []string `json:"capabilities"`
Values map[string]string `json:"values"` Values map[string]string `json:"values"`
} }
func (l *LLDP) addNeighbor(mac string, neighbor Neighbor, ttl time.Duration) { func (n *Neighbor) cacheKey() string {
return fmt.Sprintf("%s-%s", n.Mac, n.Source)
}
func (l *LLDP) addNeighbor(neighbor *Neighbor, ttl time.Duration) {
logger := l.l.With(). logger := l.l.With().
Str("mac", mac). Str("mac", neighbor.Mac).
Interface("neighbor", neighbor). Interface("neighbor", neighbor).
Logger() Logger()
current_neigh := l.neighbors.Get(mac) key := neighbor.cacheKey()
current_neigh := l.neighbors.Get(key)
if current_neigh != nil { if current_neigh != nil {
current_source := current_neigh.Value().Source current_source := current_neigh.Value().Source
if current_source == "lldp" && neighbor.Source != "lldp" { if current_source == "lldp" && neighbor.Source != "lldp" {
@ -31,16 +51,16 @@ func (l *LLDP) addNeighbor(mac string, neighbor Neighbor, ttl time.Duration) {
} }
logger.Info().Msg("adding neighbor") logger.Info().Msg("adding neighbor")
l.neighbors.Set(mac, neighbor, ttl) l.neighbors.Set(key, *neighbor, ttl)
} }
func (l *LLDP) deleteNeighbor(mac string) { func (l *LLDP) deleteNeighbor(neighbor *Neighbor) {
logger := l.l.With(). logger := l.l.With().
Str("mac", mac). Str("mac", neighbor.Mac).
Logger() Logger()
logger.Info().Msg("deleting neighbor") logger.Info().Msg("deleting neighbor")
l.neighbors.Delete(mac) l.neighbors.Delete(neighbor.cacheKey())
} }
func (l *LLDP) GetNeighbors() []Neighbor { func (l *LLDP) GetNeighbors() []Neighbor {
@ -51,5 +71,10 @@ func (l *LLDP) GetNeighbors() []Neighbor {
neighbors = append(neighbors, item.Value()) neighbors = append(neighbors, item.Value())
} }
// sort based on MAC address
sort.Slice(neighbors, func(i, j int) bool {
return strings.Compare(neighbors[i].Mac, neighbors[j].Mac) > 0
})
return neighbors return neighbors
} }

View File

@ -1,6 +1,7 @@
package lldp package lldp
import ( import (
"context"
"fmt" "fmt"
"net" "net"
"time" "time"
@ -27,12 +28,26 @@ var multicastAddrs = []string{
} }
func (l *LLDP) setUpCapture() error { func (l *LLDP) setUpCapture() error {
l.mu.Lock()
defer l.mu.Unlock()
if l.tPacketRx != nil {
return nil
}
logger := l.l.With().Str("interface", l.interfaceName).Logger() logger := l.l.With().Str("interface", l.interfaceName).Logger()
tPacket, err := afPacketNewTPacket(l.interfaceName) tPacketRx, err := afPacketNewTPacket(l.interfaceName)
if err != nil { if err != nil {
return err return err
} }
logger.Info().Msg("created TPacket") logger.Info().Msg("created TPacketRx")
// Double-check: another goroutine might have set it up while we were creating
if l.tPacketRx != nil {
// Another goroutine already set it up, close our instance
tPacketRx.Close()
return nil
}
// set up multicast addresses // set up multicast addresses
// otherwise the kernel might discard the packets // otherwise the kernel might discard the packets
@ -40,52 +55,95 @@ func (l *LLDP) setUpCapture() error {
for _, mac := range multicastAddrs { for _, mac := range multicastAddrs {
hwAddr, err := net.ParseMAC(mac) hwAddr, err := net.ParseMAC(mac)
if err != nil { if err != nil {
logger.Error().Msgf("unable to parse MAC address %s: %s", mac, err) logger.Error().
Str("mac", mac).
MACAddr("hwaddr", hwAddr).
Err(err).
Msg("unable to parse MAC address")
continue continue
} }
if err := addMulticastAddr(l.interfaceName, hwAddr); err != nil { if err := addMulticastAddr(l.interfaceName, hwAddr); err != nil {
logger.Error().Msgf("unable to add multicast address %s: %s", mac, err) logger.Error().
MACAddr("hwaddr", hwAddr).
Err(err).
Msg("unable to add multicast address")
continue continue
} }
logger.Info(). logger.Info().
MACAddr("hwaddr", hwAddr). MACAddr("hwaddr", hwAddr).
Msgf("added multicast address") Msg("added multicast address")
} }
if err = tPacket.SetBPF(bpfFilter); err != nil { if err = tPacketRx.SetBPF(bpfFilter); err != nil {
logger.Error().Msgf("unable to set BPF filter: %s", err) logger.Error().
tPacket.Close() Err(err).
Msg("unable to set BPF filter")
tPacketRx.Close()
return err return err
} }
logger.Info().Msg("BPF filter set") logger.Info().Msg("BPF filter set")
l.pktSource = gopacket.NewPacketSource(tPacket, layers.LayerTypeEthernet) l.pktSourceRx = gopacket.NewPacketSource(tPacketRx, layers.LayerTypeEthernet)
l.tPacket = tPacket l.tPacketRx = tPacketRx
return nil return nil
} }
func (l *LLDP) doCapture(logger *zerolog.Logger, rxCtx context.Context) {
defer func() {
l.mu.Lock()
l.rxRunning = false
l.mu.Unlock()
}()
packetChan := l.pktSourceRx.Packets()
for {
select {
case packet, ok := <-packetChan:
if !ok {
logger.Info().Msg("packet source closed")
return
}
if err := l.handlePacket(packet, logger); err != nil {
logger.Error().
Err(err).
Msg("error handling packet")
}
case <-rxCtx.Done():
logger.Info().Msg("LLDP receiver stopped")
return
}
}
}
func (l *LLDP) startCapture() error { func (l *LLDP) startCapture() error {
logger := l.l.With().Str("interface", l.interfaceName).Logger() l.mu.Lock()
if l.tPacket == nil { defer l.mu.Unlock()
if l.rxRunning {
return nil // Already running
}
if l.tPacketRx == nil {
return fmt.Errorf("AFPacket not initialized") return fmt.Errorf("AFPacket not initialized")
} }
if l.pktSource == nil { if l.pktSourceRx == nil {
return fmt.Errorf("packet source not initialized") return fmt.Errorf("packet source not initialized")
} }
go func() { logger := l.l.With().Str("interface", l.interfaceName).Logger()
logger.Info().Msg("starting capture LLDP ethernet frames") logger.Info().Msg("starting capture LLDP ethernet frames")
for packet := range l.pktSource.Packets() { // Create a new context for this instance
if err := l.handlePacket(packet, &logger); err != nil { l.rxCtx, l.rxCancel = context.WithCancel(context.Background())
logger.Error().Msgf("error handling packet: %s", err) l.rxRunning = true
}
} // Capture context in closure
}() rxCtx := l.rxCtx
go l.doCapture(&logger, rxCtx)
return nil return nil
} }
@ -108,7 +166,8 @@ func (l *LLDP) handlePacket(packet gopacket.Packet, logger *zerolog.Logger) erro
lldpRaw := packet.Layer(layers.LayerTypeLinkLayerDiscovery) lldpRaw := packet.Layer(layers.LayerTypeLinkLayerDiscovery)
if lldpRaw != nil { if lldpRaw != nil {
logger.Trace().Msgf("Found LLDP Frame") logger.Trace().Msg("Found LLDP Frame")
l.l.Info().Hex("packet", packet.Data()).Msg("received packet")
lldpInfo := packet.Layer(layers.LayerTypeLinkLayerDiscoveryInfo) lldpInfo := packet.Layer(layers.LayerTypeLinkLayerDiscoveryInfo)
if lldpInfo == nil { if lldpInfo == nil {
@ -124,7 +183,7 @@ func (l *LLDP) handlePacket(packet gopacket.Packet, logger *zerolog.Logger) erro
cdpRaw := packet.Layer(layers.LayerTypeCiscoDiscovery) cdpRaw := packet.Layer(layers.LayerTypeCiscoDiscovery)
if cdpRaw != nil { if cdpRaw != nil {
logger.Trace().Msgf("Found CDP Frame") logger.Trace().Msg("Found CDP Frame")
cdpInfo := packet.Layer(layers.LayerTypeCiscoDiscoveryInfo) cdpInfo := packet.Layer(layers.LayerTypeCiscoDiscoveryInfo)
if cdpInfo == nil { if cdpInfo == nil {
@ -141,6 +200,32 @@ func (l *LLDP) handlePacket(packet gopacket.Packet, logger *zerolog.Logger) erro
return nil return nil
} }
func capabilitiesToString(capabilities layers.LLDPCapabilities) []string {
capStr := []string{}
if capabilities.Other {
capStr = append(capStr, "other")
}
if capabilities.Repeater {
capStr = append(capStr, "repeater")
}
if capabilities.Bridge {
capStr = append(capStr, "bridge")
}
if capabilities.WLANAP {
capStr = append(capStr, "wlanap")
}
if capabilities.Router {
capStr = append(capStr, "router")
}
if capabilities.Phone {
capStr = append(capStr, "phone")
}
if capabilities.DocSis {
capStr = append(capStr, "docsis")
}
return capStr
}
func (l *LLDP) handlePacketLLDP(mac string, raw *layers.LinkLayerDiscovery, info *layers.LinkLayerDiscoveryInfo) error { func (l *LLDP) handlePacketLLDP(mac string, raw *layers.LinkLayerDiscovery, info *layers.LinkLayerDiscoveryInfo) error {
n := &Neighbor{ n := &Neighbor{
Values: make(map[string]string), Values: make(map[string]string),
@ -171,7 +256,15 @@ func (l *LLDP) handlePacketLLDP(mac string, raw *layers.LinkLayerDiscovery, info
n.SystemDescription = info.SysDescription n.SystemDescription = info.SysDescription
n.Values["system_description"] = n.SystemDescription n.Values["system_description"] = n.SystemDescription
case layers.LLDPTLVMgmtAddress: case layers.LLDPTLVMgmtAddress:
// n.ManagementAddress = info.MgmtAddress.Address n.ManagementAddress = &ManagementAddress{
AddressFamily: info.MgmtAddress.Subtype.String(),
Address: net.IP(info.MgmtAddress.Address).String(),
InterfaceSubtype: info.MgmtAddress.InterfaceSubtype.String(),
InterfaceNumber: info.MgmtAddress.InterfaceNumber,
OID: info.MgmtAddress.OID,
}
case layers.LLDPTLVSysCapabilities:
n.Capabilities = capabilitiesToString(info.SysCapabilities.EnabledCap)
case layers.LLDPTLVTTL: case layers.LLDPTLVTTL:
n.TTL = uint16(raw.TTL) n.TTL = uint16(raw.TTL)
ttl = time.Duration(n.TTL) * time.Second ttl = time.Duration(n.TTL) * time.Second
@ -184,9 +277,9 @@ func (l *LLDP) handlePacketLLDP(mac string, raw *layers.LinkLayerDiscovery, info
} }
if gotEnd || ttl < 1*time.Second { if gotEnd || ttl < 1*time.Second {
l.deleteNeighbor(mac) l.deleteNeighbor(n)
} else { } else {
l.addNeighbor(mac, *n, ttl) l.addNeighbor(n, ttl)
} }
return nil return nil
@ -213,23 +306,61 @@ func (l *LLDP) handlePacketCDP(mac string, raw *layers.CiscoDiscovery, info *lay
} }
if len(info.MgmtAddresses) > 0 { if len(info.MgmtAddresses) > 0 {
n.ManagementAddress = string(info.MgmtAddresses[0]) ip := info.MgmtAddresses[0]
ipFamily := "ipv4"
if ip.To4() == nil {
ipFamily = "ipv6"
} }
l.addNeighbor(mac, *n, ttl) l.l.Info().
Str("ip", ip.String()).
Str("ip_family", ipFamily).
Interface("ip", ip).
Interface("info", info).
Msg("parsed IP address")
n.ManagementAddress = &ManagementAddress{
AddressFamily: ipFamily,
Address: ip.String(),
InterfaceSubtype: "if_name",
InterfaceNumber: 0,
OID: "",
}
}
l.addNeighbor(n, ttl)
return nil return nil
} }
func (l *LLDP) shutdownCapture() error { func (l *LLDP) stopCapture() error {
if l.tPacket != nil { l.mu.Lock()
l.tPacket.Close() defer l.mu.Unlock()
l.tPacket = nil
if !l.rxRunning {
return nil // Already stopped
} }
if l.pktSource != nil { logger := l.l.With().Str("interface", l.interfaceName).Logger()
l.pktSource = nil logger.Info().Msg("stopping LLDP receiver")
// Cancel context to signal stop
rxCancel := l.rxCancel
if rxCancel != nil {
rxCancel()
l.rxCancel = nil
} }
if l.tPacketRx != nil {
l.tPacketRx.Close()
l.tPacketRx = nil
}
if l.pktSourceRx != nil {
l.pktSourceRx = nil
}
time.Sleep(100 * time.Millisecond)
return nil return nil
} }

270
internal/lldp/tx.go Normal file
View File

@ -0,0 +1,270 @@
package lldp
import (
"context"
"encoding/binary"
"fmt"
"net"
"time"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
"github.com/rs/zerolog"
)
var (
lldpDstMac = net.HardwareAddr([]byte{0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e})
lldpEtherType = layers.EthernetTypeLinkLayerDiscovery
)
// func encodeMandatoryTLV(subType byte, id []byte) []byte {
// // 1 byte: subtype
// // N bytes: ID
// b := make([]byte, 1+len(id))
// b[0] = byte(subtype)
// copy(b[1:], id)
// return b
// }
// func (l *LLDP) createLLDPPayload() ([]byte, error) {
// tlv := &layers.LinkLayerDiscoveryValue{
// Type: layers.LLDPTLVChassisID,
// }
func tlvStringValue(tlvType layers.LLDPTLVType, value string) layers.LinkLayerDiscoveryValue {
return layers.LinkLayerDiscoveryValue{
Type: tlvType,
Value: []byte(value),
Length: uint16(len(value)),
}
}
var (
capabilityMap = map[string]uint16{
"other": layers.LLDPCapsOther,
"repeater": layers.LLDPCapsRepeater,
"bridge": layers.LLDPCapsBridge,
"wlanap": layers.LLDPCapsWLANAP,
"router": layers.LLDPCapsRouter,
"phone": layers.LLDPCapsPhone,
"docsis": layers.LLDPCapsDocSis,
"station_only": layers.LLDPCapsStationOnly,
"cvlan": layers.LLDPCapsCVLAN,
"svlan": layers.LLDPCapsSVLAN,
"tmpr": layers.LLDPCapsTmpr,
}
)
func toLLDPCapabilitiesBytes(capabilities []string) uint16 {
r := uint16(0)
for _, capability := range capabilities {
if _, ok := capabilityMap[capability]; !ok {
continue
}
r |= capabilityMap[capability]
}
return r
}
func (l *LLDP) toPayloadValues() []layers.LinkLayerDiscoveryValue {
// See also: layers.LinkLayerDiscovery.SerializeTo()
r := []layers.LinkLayerDiscoveryValue{}
l.mu.RLock()
opts := l.advertiseOptions
l.mu.RUnlock()
if opts == nil {
return r
}
if opts.SysName != "" {
r = append(r, tlvStringValue(layers.LLDPTLVSysName, opts.SysName))
}
if opts.SysDescription != "" {
r = append(r, tlvStringValue(layers.LLDPTLVSysDescription, opts.SysDescription))
}
if len(opts.SysCapabilities) > 0 {
value := make([]byte, 4)
binary.BigEndian.PutUint16(value[0:2], toLLDPCapabilitiesBytes(opts.SysCapabilities))
binary.BigEndian.PutUint16(value[2:4], toLLDPCapabilitiesBytes(opts.EnabledCapabilities))
r = append(r, layers.LinkLayerDiscoveryValue{
Type: layers.LLDPTLVSysCapabilities,
Value: value,
Length: 4,
})
}
// EndTLV will be added by the serializer, we don't need to add it here
return r
}
func (l *LLDP) setUpTx() error {
l.mu.Lock()
defer l.mu.Unlock()
// Check if already set up (double-check pattern to prevent duplicate setup)
if l.tPacketTx != nil {
return nil
}
logger := l.l.With().Str("interface", l.interfaceName).Logger()
tPacketTx, err := afPacketNewTPacket(l.interfaceName)
if err != nil {
return err
}
logger.Info().Msg("created TPacket instance for sending LLDP packets")
l.tPacketTx = tPacketTx
return nil
}
func (l *LLDP) sendTxPackets() error {
l.mu.RLock()
defer l.mu.RUnlock()
logger := l.l.With().Str("interface", l.interfaceName).Logger()
iface, err := net.InterfaceByName(l.interfaceName)
if err != nil {
return err
}
if l.tPacketTx == nil {
return fmt.Errorf("AFPacket not initialized")
}
// create payload
ethFrame := layers.Ethernet{
EthernetType: lldpEtherType,
SrcMAC: iface.HardwareAddr,
DstMAC: lldpDstMac,
}
lldpFrame := layers.LinkLayerDiscovery{
ChassisID: layers.LLDPChassisID{
Subtype: layers.LLDPChassisIDSubTypeMACAddr,
ID: []byte(iface.HardwareAddr),
},
PortID: layers.LLDPPortID{
Subtype: layers.LLDPPortIDSubtypeIfaceName,
ID: []byte(iface.Name),
},
TTL: uint16(3600),
Values: l.toPayloadValues(),
}
buf := gopacket.NewSerializeBuffer()
if err := gopacket.SerializeLayers(buf, gopacket.SerializeOptions{
FixLengths: true,
ComputeChecksums: true,
}, &ethFrame, &lldpFrame); err != nil {
l.l.Error().Err(err).Msg("unable to serialize packet")
return err
}
logger.Trace().Hex("packet", buf.Bytes()).Msg("sending LLDP packet")
// send packet
if err := l.tPacketTx.WritePacketData(buf.Bytes()); err != nil {
l.l.Error().Err(err).Msg("unable to send packet")
return err
}
return nil
}
const txInterval = 30 * time.Second // Standard LLDP transmission interval
func (l *LLDP) doSendPeriodically(logger *zerolog.Logger, txCtx context.Context) {
l.mu.Lock()
l.txRunning = true
l.mu.Unlock()
defer func() {
l.mu.Lock()
l.txRunning = false
l.mu.Unlock()
}()
ticker := time.NewTicker(txInterval)
defer ticker.Stop()
// Send initial packet immediately
if err := l.sendTxPackets(); err != nil {
logger.Error().Err(err).Msg("error sending initial LLDP packet")
}
for {
select {
case <-ticker.C:
if err := l.sendTxPackets(); err != nil {
logger.Error().Err(err).Msg("error sending LLDP packet")
}
case <-txCtx.Done():
logger.Info().Msg("LLDP transmitter stopped")
return
}
}
}
func (l *LLDP) startTx() error {
l.mu.RLock()
running := l.txRunning
enabled := l.enableTx
cancel := l.txCancel
l.mu.RUnlock()
if running || !enabled {
return nil
}
if cancel != nil {
cancel()
}
l.txCtx, l.txCancel = context.WithCancel(context.Background())
if err := l.setUpTx(); err != nil {
return fmt.Errorf("failed to set up TX: %w", err)
}
logger := l.l.With().Str("interface", l.interfaceName).Logger()
logger.Info().Msg("starting LLDP transmitter")
go l.doSendPeriodically(&logger, l.txCtx)
return nil
}
func (l *LLDP) stopTx() error {
l.mu.Lock()
if !l.txRunning {
l.mu.Unlock()
return nil // Already stopped
}
logger := l.l.With().Str("interface", l.interfaceName).Logger()
logger.Info().Msg("stopping LLDP transmitter")
// Cancel context to signal stop
txCancel := l.txCancel
l.txRunning = false
l.mu.Unlock()
// Cancel context (goroutine will handle cleanup)
if txCancel != nil {
txCancel()
}
// Wait a bit for goroutine to finish
// Note: In a production system, you might want to use sync.WaitGroup
// for proper synchronization, but for now this is acceptable
time.Sleep(100 * time.Millisecond)
return nil
}

View File

@ -163,13 +163,22 @@ func initNetwork() error {
networkManager = nm networkManager = nm
lldpService = lldp.NewLLDP(&lldp.LLDPOptions{ advertiseOptions := &lldp.AdvertiseOptions{
SysName: networkManager.Hostname(),
SysDescription: toLLDPSysDescription(nc),
SysCapabilities: []string{"other", "router", "wlanap"},
EnabledCapabilities: []string{"other"},
}
lldpService = lldp.NewLLDP(&lldp.Options{
InterfaceName: NetIfName, InterfaceName: NetIfName,
EnableRx: nc.LLDPMode.String != "disabled", EnableRx: nc.LLDPMode.String != "disabled",
EnableTx: nc.LLDPMode.String != "disabled", EnableTx: nc.LLDPMode.String != "disabled",
AdvertiseOptions: advertiseOptions,
OnChange: func(neighbors []lldp.Neighbor) { OnChange: func(neighbors []lldp.Neighbor) {
writeJSONRPCEvent("lldpNeighbors", neighbors, currentSession) writeJSONRPCEvent("lldpNeighbors", neighbors, currentSession)
}, },
Logger: networkLogger,
}) })
if err := lldpService.Start(); err != nil { if err := lldpService.Start(); err != nil {
networkLogger.Error().Err(err).Msg("failed to start LLDP service") networkLogger.Error().Err(err).Msg("failed to start LLDP service")
@ -178,6 +187,15 @@ func initNetwork() error {
return nil return nil
} }
func toLLDPSysDescription(nc *types.NetworkConfig) string {
systemVersion, appVersion, err := GetLocalVersion()
if err == nil {
return fmt.Sprintf("JetKVM (app: %s)", GetBuiltAppVersion())
}
return fmt.Sprintf("JetKVM (app: %s, system: %s)", appVersion.String(), systemVersion.String())
}
func setHostname(nm *nmlite.NetworkManager, hostname, domain string) error { func setHostname(nm *nmlite.NetworkManager, hostname, domain string) error {
if nm == nil { if nm == nil {
return nil return nil

View File

@ -21,7 +21,7 @@ const LLDPDataLine = ({ label, value, className }: LLDPDataLineProps) => {
); );
} }
export default function LLDPNeighCard({ export default function LLDPNeighborsCard({
neighbors, neighbors,
}: { }: {
neighbors: LLDPNeighbor[]; neighbors: LLDPNeighbor[];
@ -49,7 +49,7 @@ export default function LLDPNeighCard({
)} )}
{neighbor.system_description && ( {neighbor.system_description && (
<LLDPDataLine label="System Description" value={neighbor.system_description} /> <LLDPDataLine label="System Description" value={neighbor.system_description} className="col-span-2" />
)} )}
{neighbor.chassis_id && ( {neighbor.chassis_id && (
@ -65,7 +65,7 @@ export default function LLDPNeighCard({
)} )}
{neighbor.management_address && ( {neighbor.management_address && (
<LLDPDataLine label="Management Address" value={neighbor.management_address} /> <LLDPDataLine label="Management Address" value={neighbor.management_address.address} />
)} )}
{neighbor.mac && ( {neighbor.mac && (

View File

@ -783,6 +783,14 @@ export interface IPv6StaticConfig {
dns: string[]; dns: string[];
} }
export interface LLDPManagementAddress {
address_family: string;
address: string;
interface_subtype: string;
interface_number: number;
oid: string;
}
export interface LLDPNeighbor { export interface LLDPNeighbor {
mac: string; mac: string;
source: string; source: string;
@ -791,8 +799,9 @@ export interface LLDPNeighbor {
port_description: string; port_description: string;
system_name: string; system_name: string;
system_description: string; system_description: string;
capabilities: string[];
ttl: number | null; ttl: number | null;
management_address: string | null; management_address: LLDPManagementAddress | null;
values: Record<string, string>; values: Record<string, string>;
} }

View File

@ -23,14 +23,14 @@ 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 { callJsonRpc, getNetworkSettings, getNetworkState } from "@/utils/jsonrpc"; import { getNetworkSettings, getNetworkState, getLLDPNeighbors } 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"; import LLDPNeighborsCard from "@components/LLDPNeighborsCard";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
const isLLDPAvailable = false; // LLDP is not supported yet const isLLDPAvailable = true; // LLDP is now supported
const resolveOnRtcReady = () => { const resolveOnRtcReady = () => {
return new Promise(resolve => { return new Promise(resolve => {
@ -100,14 +100,10 @@ export default function SettingsNetworkRoute() {
const [lldpNeighbors, setLldpNeighbors] = useState<LLDPNeighbor[]>([]); const [lldpNeighbors, setLldpNeighbors] = useState<LLDPNeighbor[]>([]);
const fetchLLDPNeighbors = useCallback(async () => { const fetchLLDPNeighbors = useCallback(async () => {
send("getLLDPNeighbors", {}, (resp) => { const neighbors = await getLLDPNeighbors();
if ("error" in resp) { setLldpNeighbors(neighbors);
// notifications.error(m.network_lldp_neighbors_fetch_failed({ error: neighbors.error.message || m.unknown_error() })); }, [setLldpNeighbors]);
} else {
setLldpNeighbors(resp.result as LLDPNeighbor[]);
}
});
}, [setLldpNeighbors, send]);
useEffect(() => { useEffect(() => {
fetchLLDPNeighbors(); fetchLLDPNeighbors();
}, [fetchLLDPNeighbors]); }, [fetchLLDPNeighbors]);
@ -475,11 +471,6 @@ export default function SettingsNetworkRoute() {
/> />
</SettingsItem> </SettingsItem>
<div>
<AutoHeight>
<LLDPNeighCard neighbors={lldpNeighbors} />
</AutoHeight>
</div>
<div> <div>
<AutoHeight> <AutoHeight>
@ -561,9 +552,10 @@ export default function SettingsNetworkRoute() {
</AutoHeight> </AutoHeight>
</div> </div>
{ isLLDPAvailable && {isLLDPAvailable &&
( (
<div className="hidden space-y-4"> <div className="space-y-4">
<div className="space-y-4">
<SettingsItem <SettingsItem
title={m.network_ll_dp_title()} title={m.network_ll_dp_title()}
description={m.network_ll_dp_description()} description={m.network_ll_dp_description()}
@ -579,6 +571,10 @@ export default function SettingsNetworkRoute() {
/> />
</SettingsItem> </SettingsItem>
</div> </div>
<AutoHeight>
<LLDPNeighborsCard neighbors={lldpNeighbors} />
</AutoHeight>
</div>
) )
} }

View File

@ -1,4 +1,4 @@
import { useRTCStore } from "@/hooks/stores"; import { LLDPNeighbor, useRTCStore } from "@/hooks/stores";
import { sleep } from "@/utils"; import { sleep } from "@/utils";
// JSON-RPC utility for use outside of React components // JSON-RPC utility for use outside of React components
@ -170,6 +170,14 @@ export async function getNetworkState() {
return response.result; return response.result;
} }
export async function getLLDPNeighbors() {
const response = await callJsonRpc<LLDPNeighbor[]>({ method: "getLLDPNeighbors" });
if (response.error) {
throw new Error(response.error.message);
}
return response.result;
}
export async function renewDHCPLease() { export async function renewDHCPLease() {
const response = await callJsonRpc({ method: "renewDHCPLease" }); const response = await callJsonRpc({ method: "renewDHCPLease" });
if (response.error) { if (response.error) {