mirror of https://github.com/jetkvm/kvm.git
Compare commits
8 Commits
09bbd9d780
...
35d8c3d9d1
| Author | SHA1 | Date |
|---|---|---|
|
|
35d8c3d9d1 | |
|
|
03f781f7e1 | |
|
|
36f06a064a | |
|
|
9f4acce279 | |
|
|
3e39361aa7 | |
|
|
fd44ff49fd | |
|
|
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$
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
3
go.mod
3
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
|
||||||
|
|
@ -62,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
|
||||||
|
|
|
||||||
29
go.sum
29
go.sum
|
|
@ -57,9 +57,13 @@ 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=
|
||||||
|
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,8 +74,11 @@ 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.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=
|
||||||
|
|
@ -99,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=
|
||||||
|
|
@ -206,14 +217,28 @@ 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-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.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-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-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=
|
||||||
|
|
@ -224,8 +249,12 @@ 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=
|
||||||
|
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=
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
targetSizeMb int,
|
||||||
|
snapLen int,
|
||||||
|
pageSize int,
|
||||||
|
) (
|
||||||
|
frameSize int,
|
||||||
|
blockSize int,
|
||||||
|
numBlocks int,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
if snapLen < pageSize {
|
||||||
|
// 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 {
|
||||||
|
// 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
|
||||||
|
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,165 @@
|
||||||
|
package lldp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"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 {
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
l *zerolog.Logger
|
||||||
|
tPacketRx *afpacket.TPacket
|
||||||
|
tPacketTx *afpacket.TPacket
|
||||||
|
pktSourceRx *gopacket.PacketSource
|
||||||
|
|
||||||
|
enableRx bool
|
||||||
|
enableTx bool
|
||||||
|
|
||||||
|
interfaceName string
|
||||||
|
advertiseOptions *AdvertiseOptions
|
||||||
|
onChange func(neighbors []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 AdvertiseOptions struct {
|
||||||
|
SysName string
|
||||||
|
SysDescription string
|
||||||
|
PortDescription string
|
||||||
|
SysCapabilities []string
|
||||||
|
EnabledCapabilities []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
InterfaceName string
|
||||||
|
AdvertiseOptions *AdvertiseOptions
|
||||||
|
EnableRx bool
|
||||||
|
EnableTx bool
|
||||||
|
OnChange func(neighbors []Neighbor)
|
||||||
|
Logger *zerolog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLLDP(opts *Options) *LLDP {
|
||||||
|
if opts.Logger == nil {
|
||||||
|
opts.Logger = defaultLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.InterfaceName == "" {
|
||||||
|
opts.Logger.Fatal().Msg("InterfaceName is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LLDP{
|
||||||
|
interfaceName: opts.InterfaceName,
|
||||||
|
advertiseOptions: opts.AdvertiseOptions,
|
||||||
|
enableRx: opts.EnableRx,
|
||||||
|
enableTx: opts.EnableTx,
|
||||||
|
l: opts.Logger,
|
||||||
|
neighbors: ttlcache.New(ttlcache.WithTTL[string, Neighbor](1 * time.Hour)),
|
||||||
|
onChange: opts.OnChange,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LLDP) Start() error {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LLDP) SetRxAndTx(rx, tx bool) error {
|
||||||
|
l.mu.Lock()
|
||||||
|
l.enableRx = rx
|
||||||
|
l.enableTx = tx
|
||||||
|
l.mu.Unlock()
|
||||||
|
|
||||||
|
// if rx is enabled, start the RX
|
||||||
|
if rx {
|
||||||
|
if err := l.startRx(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start RX: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := l.stopRx(); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop RX: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if tx is enabled, start the TX
|
||||||
|
if tx {
|
||||||
|
if err := l.startTx(); err != nil {
|
||||||
|
return fmt.Errorf("failed to start TX: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := l.stopTx(); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop TX: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
package lldp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"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 {
|
||||||
|
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 *ManagementAddress `json:"management_address,omitempty"`
|
||||||
|
Capabilities []string `json:"capabilities"`
|
||||||
|
Values map[string]string `json:"values"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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().
|
||||||
|
Str("mac", neighbor.Mac).
|
||||||
|
Interface("neighbor", neighbor).
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
key := neighbor.cacheKey()
|
||||||
|
|
||||||
|
current_neigh := l.neighbors.Get(key)
|
||||||
|
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.Trace().Msg("adding neighbor")
|
||||||
|
l.neighbors.Set(key, *neighbor, ttl)
|
||||||
|
|
||||||
|
l.onChange(l.GetNeighbors())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LLDP) deleteNeighbor(neighbor *Neighbor) {
|
||||||
|
logger := l.l.With().
|
||||||
|
Str("mac", neighbor.Mac).
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
logger.Info().Msg("deleting neighbor")
|
||||||
|
l.neighbors.Delete(neighbor.cacheKey())
|
||||||
|
|
||||||
|
l.onChange(l.GetNeighbors())
|
||||||
|
}
|
||||||
|
|
||||||
|
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,399 @@
|
||||||
|
package lldp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"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 {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
if l.tPacketRx != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := l.l.With().Str("interface", l.interfaceName).Logger()
|
||||||
|
tPacketRx, err := afPacketNewTPacket(l.interfaceName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info().Msg("created TPacketRx")
|
||||||
|
|
||||||
|
// 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().
|
||||||
|
Str("mac", mac).
|
||||||
|
MACAddr("hwaddr", hwAddr).
|
||||||
|
Err(err).
|
||||||
|
Msg("unable to parse MAC address")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := addMulticastAddr(l.interfaceName, hwAddr); err != nil {
|
||||||
|
logger.Error().
|
||||||
|
MACAddr("hwaddr", hwAddr).
|
||||||
|
Err(err).
|
||||||
|
Msg("unable to add multicast address")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info().
|
||||||
|
MACAddr("hwaddr", hwAddr).
|
||||||
|
Msg("added multicast address")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tPacketRx.SetBPF(bpfFilter); err != nil {
|
||||||
|
logger.Error().
|
||||||
|
Err(err).
|
||||||
|
Msg("unable to set BPF filter")
|
||||||
|
tPacketRx.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info().Msg("BPF filter set")
|
||||||
|
|
||||||
|
l.pktSourceRx = gopacket.NewPacketSource(tPacketRx, layers.LayerTypeEthernet)
|
||||||
|
l.tPacketRx = tPacketRx
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LLDP) doCapture(logger *zerolog.Logger, rxCtx context.Context) {
|
||||||
|
defer func() {
|
||||||
|
l.mu.Lock()
|
||||||
|
l.rxRunning = false
|
||||||
|
l.mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// TODO: use a channel to handle the packets
|
||||||
|
// PacketSource.Packets() is not reliable and can cause panics and the upstream hasn't fixed it yet
|
||||||
|
for rxCtx.Err() == nil {
|
||||||
|
if l.pktSourceRx == nil || l.tPacketRx == nil {
|
||||||
|
logger.Error().Msg("packet source or TPacketRx not initialized")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
packet, err := l.pktSourceRx.NextPacket()
|
||||||
|
if err == nil {
|
||||||
|
if handleErr := l.handlePacket(packet, logger); handleErr != nil {
|
||||||
|
logger.Error().
|
||||||
|
Err(handleErr).
|
||||||
|
Msg("error handling packet")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immediately retry for temporary network errors and EAGAIN
|
||||||
|
// temporary has been deprecated and most cases are timeouts
|
||||||
|
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err == syscall.EAGAIN {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Immediately break for known unrecoverable errors
|
||||||
|
if err == io.EOF || err == io.ErrUnexpectedEOF ||
|
||||||
|
err == io.ErrNoProgress || err == io.ErrClosedPipe || err == io.ErrShortBuffer ||
|
||||||
|
err == syscall.EBADF ||
|
||||||
|
strings.Contains(err.Error(), "use of closed file") {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Error().
|
||||||
|
Err(err).
|
||||||
|
Msg("error receiving LLDP packet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LLDP) startCapture() error {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
if l.rxRunning {
|
||||||
|
return nil // Already running
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.tPacketRx == nil {
|
||||||
|
return fmt.Errorf("AFPacket not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.pktSourceRx == nil {
|
||||||
|
return fmt.Errorf("packet source not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := l.l.With().Str("interface", l.interfaceName).Logger()
|
||||||
|
logger.Info().Msg("starting capture LLDP ethernet frames")
|
||||||
|
|
||||||
|
// Create a new context for this instance
|
||||||
|
l.rxCtx, l.rxCancel = context.WithCancel(context.Background())
|
||||||
|
l.rxRunning = true
|
||||||
|
|
||||||
|
// Capture context in closure
|
||||||
|
rxCtx := l.rxCtx
|
||||||
|
go l.doCapture(&logger, rxCtx)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
l.l.Trace().Hex("packet", packet.Data()).Msg("received 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 {
|
||||||
|
l.l.Trace().Hex("packet", packet.Data()).Msg("received 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 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 {
|
||||||
|
n := &Neighbor{
|
||||||
|
Values: make(map[string]string),
|
||||||
|
Source: "lldp",
|
||||||
|
Mac: mac,
|
||||||
|
}
|
||||||
|
|
||||||
|
ttl := lldpDefaultTTL
|
||||||
|
|
||||||
|
for _, v := range raw.Values {
|
||||||
|
switch v.Type {
|
||||||
|
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 = &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:
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete the neighbor if the TTL is less than 1 second
|
||||||
|
// LLDP shutdown frame should have a TTL of 0 and contains mandatory TLVs only
|
||||||
|
// but we will simply ignore the TLVs check for now
|
||||||
|
if ttl < 1*time.Second {
|
||||||
|
l.deleteNeighbor(n)
|
||||||
|
} else {
|
||||||
|
l.addNeighbor(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 {
|
||||||
|
ip := info.MgmtAddresses[0]
|
||||||
|
ipFamily := "ipv4"
|
||||||
|
if ip.To4() == nil {
|
||||||
|
ipFamily = "ipv6"
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LLDP) stopCapture() error {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
if !l.rxRunning {
|
||||||
|
return nil // Already stopped
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := l.l.With().Str("interface", l.interfaceName).Logger()
|
||||||
|
logger.Info().Msg("stopping LLDP receiver")
|
||||||
|
|
||||||
|
// Cancel context to signal stop
|
||||||
|
rxCancel := l.rxCancel
|
||||||
|
if rxCancel != nil {
|
||||||
|
rxCancel()
|
||||||
|
l.rxCancel = nil
|
||||||
|
|
||||||
|
logger.Info().Msg("cancelled RX context, waiting for goroutine to finish")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a bit for goroutine to finish
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
|
if l.tPacketRx != nil {
|
||||||
|
logger.Info().Msg("closing TPacketRx")
|
||||||
|
l.tPacketRx.Close()
|
||||||
|
l.tPacketRx = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if l.pktSourceRx != nil {
|
||||||
|
logger.Info().Msg("closing packet source")
|
||||||
|
l.pktSourceRx = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LLDP) stopRx() error {
|
||||||
|
if err := l.stopCapture(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up the neighbors table
|
||||||
|
l.neighbors.DeleteAll()
|
||||||
|
l.onChange([]Neighbor{})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
}, ðFrame, &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
|
||||||
|
}
|
||||||
|
|
@ -42,7 +42,7 @@ type NetworkConfig struct {
|
||||||
IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
||||||
IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
|
IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
|
||||||
|
|
||||||
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"`
|
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,rx_only,tx_only,rx_and_tx,basic,all" default:"rx_and_tx"`
|
||||||
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||||
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||||
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||||
|
|
@ -53,6 +53,15 @@ type NetworkConfig struct {
|
||||||
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
|
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *NetworkConfig) ShouldEnableLLDPTransmit() bool {
|
||||||
|
// backwards compatibility: `basic` mode will be `rx_only` due to privacy concerns
|
||||||
|
return c.LLDPMode.String != "rx_only" && c.LLDPMode.String != "disabled" && c.LLDPMode.String != "basic"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *NetworkConfig) ShouldEnableLLDPReceive() bool {
|
||||||
|
return c.LLDPMode.String != "tx_only" && c.LLDPMode.String != "disabled"
|
||||||
|
}
|
||||||
|
|
||||||
// GetMDNSMode returns the MDNS mode configuration
|
// GetMDNSMode returns the MDNS mode configuration
|
||||||
func (c *NetworkConfig) GetMDNSMode() *MDNSListenOptions {
|
func (c *NetworkConfig) GetMDNSMode() *MDNSListenOptions {
|
||||||
mode := c.MDNSMode.String
|
mode := c.MDNSMode.String
|
||||||
|
|
|
||||||
|
|
@ -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},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
87
network.go
87
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 {
|
||||||
|
|
@ -142,6 +144,15 @@ func validateNetworkConfig() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getLLDPAdvertiseOptions(nm *nmlite.NetworkManager) *lldp.AdvertiseOptions {
|
||||||
|
return &lldp.AdvertiseOptions{
|
||||||
|
SysName: nm.Hostname(),
|
||||||
|
SysDescription: toLLDPSysDescription(),
|
||||||
|
SysCapabilities: []string{"other", "router", "wlanap"},
|
||||||
|
EnabledCapabilities: []string{"other"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func initNetwork() error {
|
func initNetwork() error {
|
||||||
ensureConfigLoaded()
|
ensureConfigLoaded()
|
||||||
|
|
||||||
|
|
@ -161,9 +172,49 @@ func initNetwork() error {
|
||||||
|
|
||||||
networkManager = nm
|
networkManager = nm
|
||||||
|
|
||||||
|
advertiseOptions := getLLDPAdvertiseOptions(nm)
|
||||||
|
lldpService = lldp.NewLLDP(&lldp.Options{
|
||||||
|
InterfaceName: NetIfName,
|
||||||
|
EnableRx: nc.ShouldEnableLLDPReceive(),
|
||||||
|
EnableTx: nc.ShouldEnableLLDPTransmit(),
|
||||||
|
AdvertiseOptions: advertiseOptions,
|
||||||
|
OnChange: func(neighbors []lldp.Neighbor) {
|
||||||
|
// TODO: send deltas instead of the whole list
|
||||||
|
writeJSONRPCEvent("lldpNeighbors", neighbors, currentSession)
|
||||||
|
},
|
||||||
|
Logger: networkLogger,
|
||||||
|
})
|
||||||
|
if err := lldpService.Start(); err != nil {
|
||||||
|
networkLogger.Error().Err(err).Msg("failed to start LLDP service")
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toLLDPSysDescription() 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 updateLLDPOptions(nc *types.NetworkConfig) {
|
||||||
|
if lldpService == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := lldpService.SetRxAndTx(nc.ShouldEnableLLDPReceive(), nc.ShouldEnableLLDPTransmit()); err != nil {
|
||||||
|
networkLogger.Error().Err(err).Msg("failed to set LLDP RX and TX")
|
||||||
|
}
|
||||||
|
|
||||||
|
advertiseOptions := getLLDPAdvertiseOptions(networkManager)
|
||||||
|
if err := lldpService.SetAdvertiseOptions(advertiseOptions); err != nil {
|
||||||
|
networkLogger.Error().Err(err).Msg("failed to set LLDP advertise options")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -177,6 +228,12 @@ func setHostname(nm *nmlite.NetworkManager, hostname, domain string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (rebootRequired bool, postRebootAction *PostRebootAction) {
|
func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (rebootRequired bool, postRebootAction *PostRebootAction) {
|
||||||
|
rebootReasons := []string{}
|
||||||
|
defer func() {
|
||||||
|
if len(rebootReasons) > 0 {
|
||||||
|
networkLogger.Info().Strs("reasons", rebootReasons).Msg("reboot required")
|
||||||
|
}
|
||||||
|
}()
|
||||||
oldDhcpClient := oldConfig.DHCPClient.String
|
oldDhcpClient := oldConfig.DHCPClient.String
|
||||||
|
|
||||||
l := networkLogger.With().
|
l := networkLogger.With().
|
||||||
|
|
@ -185,9 +242,10 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re
|
||||||
Logger()
|
Logger()
|
||||||
|
|
||||||
// DHCP client change always requires reboot
|
// DHCP client change always requires reboot
|
||||||
if newConfig.DHCPClient.String != oldDhcpClient {
|
newDhcpClient := newConfig.DHCPClient.String
|
||||||
|
if newDhcpClient != oldDhcpClient {
|
||||||
rebootRequired = true
|
rebootRequired = true
|
||||||
l.Info().Msg("DHCP client changed, reboot required")
|
rebootReasons = append(rebootReasons, fmt.Sprintf("DHCP client changed from %s to %s", oldDhcpClient, newDhcpClient))
|
||||||
return rebootRequired, postRebootAction
|
return rebootRequired, postRebootAction
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,7 +255,7 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re
|
||||||
// IPv4 mode change requires reboot
|
// IPv4 mode change requires reboot
|
||||||
if newIPv4Mode != oldIPv4Mode {
|
if newIPv4Mode != oldIPv4Mode {
|
||||||
rebootRequired = true
|
rebootRequired = true
|
||||||
l.Info().Msg("IPv4 mode changed with udhcpc, reboot required")
|
rebootReasons = append(rebootReasons, fmt.Sprintf("IPv4 mode changed from %s to %s", oldIPv4Mode, newIPv4Mode))
|
||||||
|
|
||||||
if newIPv4Mode == "static" && oldIPv4Mode != "static" {
|
if newIPv4Mode == "static" && oldIPv4Mode != "static" {
|
||||||
postRebootAction = &PostRebootAction{
|
postRebootAction = &PostRebootAction{
|
||||||
|
|
@ -211,8 +269,11 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPv4 static config changes require reboot
|
// IPv4 static config changes require reboot
|
||||||
if !reflect.DeepEqual(oldConfig.IPv4Static, newConfig.IPv4Static) {
|
// but if it's not activated, don't care about the changes
|
||||||
|
if !reflect.DeepEqual(oldConfig.IPv4Static, newConfig.IPv4Static) && newIPv4Mode == "static" {
|
||||||
rebootRequired = true
|
rebootRequired = true
|
||||||
|
// TODO: do not restart if it's just the DNS servers that changed
|
||||||
|
rebootReasons = append(rebootReasons, "IPv4 static config changed")
|
||||||
|
|
||||||
// Handle IP change for redirect (only if both are not nil and IP changed)
|
// Handle IP change for redirect (only if both are not nil and IP changed)
|
||||||
if newConfig.IPv4Static != nil && oldConfig.IPv4Static != nil &&
|
if newConfig.IPv4Static != nil && oldConfig.IPv4Static != nil &&
|
||||||
|
|
@ -221,17 +282,17 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re
|
||||||
HealthCheck: fmt.Sprintf("//%s/device/status", newConfig.IPv4Static.Address.String),
|
HealthCheck: fmt.Sprintf("//%s/device/status", newConfig.IPv4Static.Address.String),
|
||||||
RedirectTo: fmt.Sprintf("//%s", newConfig.IPv4Static.Address.String),
|
RedirectTo: fmt.Sprintf("//%s", newConfig.IPv4Static.Address.String),
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Info().Interface("postRebootAction", postRebootAction).Msg("IPv4 static config changed, reboot required")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rebootRequired, postRebootAction
|
return rebootRequired, postRebootAction
|
||||||
}
|
}
|
||||||
|
|
||||||
// IPv6 mode change requires reboot when using udhcpc
|
// IPv6 mode change requires reboot when using udhcpc
|
||||||
|
oldIPv6Mode := oldConfig.IPv6Mode.String
|
||||||
|
newIPv6Mode := newConfig.IPv6Mode.String
|
||||||
if newConfig.IPv6Mode.String != oldConfig.IPv6Mode.String && oldDhcpClient == "udhcpc" {
|
if newConfig.IPv6Mode.String != oldConfig.IPv6Mode.String && oldDhcpClient == "udhcpc" {
|
||||||
rebootRequired = true
|
rebootRequired = true
|
||||||
l.Info().Msg("IPv6 mode changed with udhcpc, reboot required")
|
rebootReasons = append(rebootReasons, fmt.Sprintf("IPv6 mode changed from %s to %s when using udhcpc", oldIPv6Mode, newIPv6Mode))
|
||||||
}
|
}
|
||||||
|
|
||||||
return rebootRequired, postRebootAction
|
return rebootRequired, postRebootAction
|
||||||
|
|
@ -256,6 +317,8 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er
|
||||||
|
|
||||||
l.Debug().Msg("setting new config")
|
l.Debug().Msg("setting new config")
|
||||||
|
|
||||||
|
// TODO: do not restart everything if it's just the LLDP mode that changed
|
||||||
|
|
||||||
// Check if reboot is needed
|
// Check if reboot is needed
|
||||||
rebootRequired, postRebootAction := shouldRebootForNetworkChange(config.NetworkConfig, netConfig)
|
rebootRequired, postRebootAction := shouldRebootForNetworkChange(config.NetworkConfig, netConfig)
|
||||||
|
|
||||||
|
|
@ -279,6 +342,9 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er
|
||||||
}
|
}
|
||||||
config.NetworkConfig = newConfig
|
config.NetworkConfig = newConfig
|
||||||
|
|
||||||
|
// update the LLDP advertise options
|
||||||
|
updateLLDPOptions(newConfig)
|
||||||
|
|
||||||
l.Debug().Msg("saving new config")
|
l.Debug().Msg("saving new config")
|
||||||
if err := SaveConfig(); err != nil {
|
if err := SaveConfig(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -312,3 +378,10 @@ func rpcToggleDHCPClient() error {
|
||||||
|
|
||||||
return rpcReboot(true)
|
return rpcReboot(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcGetLLDPNeighbors() []lldp.Neighbor {
|
||||||
|
if lldpService == nil {
|
||||||
|
return []lldp.Neighbor{}
|
||||||
|
}
|
||||||
|
return lldpService.GetNeighbors()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -634,10 +634,11 @@
|
||||||
"network_ipv6_mode_title": "IPv6 Mode",
|
"network_ipv6_mode_title": "IPv6 Mode",
|
||||||
"network_ipv6_prefix": "IP Prefix",
|
"network_ipv6_prefix": "IP Prefix",
|
||||||
"network_ipv6_prefix_invalid": "Prefix must be between 0 and 128",
|
"network_ipv6_prefix_invalid": "Prefix must be between 0 and 128",
|
||||||
"network_ll_dp_all": "All",
|
|
||||||
"network_ll_dp_basic": "Basic",
|
|
||||||
"network_ll_dp_description": "Control which TLVs will be sent over Link Layer Discovery Protocol",
|
"network_ll_dp_description": "Control which TLVs will be sent over Link Layer Discovery Protocol",
|
||||||
"network_ll_dp_disabled": "Disabled",
|
"network_ll_dp_disabled": "Disabled",
|
||||||
|
"network_ll_dp_rx_only": "Receive only",
|
||||||
|
"network_ll_dp_tx_only": "Transmit only",
|
||||||
|
"network_ll_dp_rx_and_tx": "Receive and transmit",
|
||||||
"network_ll_dp_title": "LLDP",
|
"network_ll_dp_title": "LLDP",
|
||||||
"network_mac_address_copy_error": "Failed to copy MAC address",
|
"network_mac_address_copy_error": "Failed to copy MAC address",
|
||||||
"network_mac_address_copy_success": "MAC address { mac } copied to clipboard",
|
"network_mac_address_copy_success": "MAC address { mac } copied to clipboard",
|
||||||
|
|
|
||||||
|
|
@ -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 LLDPNeighborsCard({
|
||||||
|
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} className="col-span-2" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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.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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -778,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" | "rx_only" | "tx_only" | "rx_and_tx" | "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"
|
||||||
|
|
@ -800,6 +800,28 @@ 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 {
|
||||||
|
mac: string;
|
||||||
|
source: string;
|
||||||
|
chassis_id: string;
|
||||||
|
port_id: string;
|
||||||
|
port_description: string;
|
||||||
|
system_name: string;
|
||||||
|
system_description: string;
|
||||||
|
capabilities: string[];
|
||||||
|
ttl: number | null;
|
||||||
|
management_address: LLDPManagementAddress | null;
|
||||||
|
values: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface NetworkSettings {
|
export interface NetworkSettings {
|
||||||
dhcp_client: string;
|
dhcp_client: string;
|
||||||
hostname: string | null;
|
hostname: string | null;
|
||||||
|
|
@ -830,6 +852,18 @@ export const useNetworkStateStore = create<NetworkState>((set, get) => ({
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export interface LLDPNeighborsState {
|
||||||
|
neighbors: LLDPNeighbor[];
|
||||||
|
setNeighbors: (neighbors: LLDPNeighbor[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLLDPNeighborsStore = create<LLDPNeighborsState>((set) => ({
|
||||||
|
neighbors: [],
|
||||||
|
setNeighbors: (neighbors: LLDPNeighbor[]) => set({ neighbors }),
|
||||||
|
}));
|
||||||
|
|
||||||
export interface KeySequenceStep {
|
export interface KeySequenceStep {
|
||||||
keys: string[];
|
keys: string[];
|
||||||
modifiers: string[];
|
modifiers: string[];
|
||||||
|
|
|
||||||
|
|
@ -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 { NetworkSettings, NetworkState, useLLDPNeighborsStore, 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,13 +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 { 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 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 => {
|
||||||
|
|
@ -97,6 +98,17 @@ export default function SettingsNetworkRoute() {
|
||||||
{ label: string; from: string; to: string }[]
|
{ label: string; from: string; to: string }[]
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
|
const setLLDPNeighbors = useLLDPNeighborsStore(state => state.setNeighbors);
|
||||||
|
const lldpNeighbors = useLLDPNeighborsStore(state => state.neighbors);
|
||||||
|
const fetchLLDPNeighbors = useCallback(async () => {
|
||||||
|
const neighbors = await getLLDPNeighbors();
|
||||||
|
setLLDPNeighbors(neighbors);
|
||||||
|
}, [setLLDPNeighbors]);
|
||||||
|
|
||||||
|
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 +472,7 @@ export default function SettingsNetworkRoute() {
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<AutoHeight>
|
<AutoHeight>
|
||||||
{formState.isLoading ? (
|
{formState.isLoading ? (
|
||||||
|
|
@ -540,9 +553,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()}
|
||||||
|
|
@ -551,13 +565,18 @@ export default function SettingsNetworkRoute() {
|
||||||
size="SM"
|
size="SM"
|
||||||
options={[
|
options={[
|
||||||
{ value: "disabled", label: m.network_ll_dp_disabled() },
|
{ value: "disabled", label: m.network_ll_dp_disabled() },
|
||||||
{ value: "basic", label: m.network_ll_dp_basic() },
|
{ value: "rx_only", label: m.network_ll_dp_rx_only() },
|
||||||
{ value: "all", label: m.network_ll_dp_all() },
|
{ value: "tx_only", label: m.network_ll_dp_tx_only() },
|
||||||
|
{ value: "rx_and_tx", label: m.network_ll_dp_rx_and_tx() },
|
||||||
]}
|
]}
|
||||||
{...register("lldp_mode")}
|
{...register("lldp_mode")}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
</div>
|
||||||
|
{lldpNeighbors.length > 0 && <AutoHeight>
|
||||||
|
<LLDPNeighborsCard neighbors={lldpNeighbors} />
|
||||||
|
</AutoHeight>}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,13 @@ import { checkAuth, isInCloud, isOnDevice } from "@/main";
|
||||||
import {
|
import {
|
||||||
KeyboardLedState,
|
KeyboardLedState,
|
||||||
KeysDownState,
|
KeysDownState,
|
||||||
|
LLDPNeighbor,
|
||||||
NetworkState,
|
NetworkState,
|
||||||
OtaState,
|
OtaState,
|
||||||
PostRebootAction,
|
PostRebootAction,
|
||||||
USBStates,
|
USBStates,
|
||||||
useHidStore,
|
useHidStore,
|
||||||
|
useLLDPNeighborsStore,
|
||||||
useNetworkStateStore,
|
useNetworkStateStore,
|
||||||
User,
|
User,
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
|
|
@ -613,6 +615,7 @@ export default function KvmIdRoute() {
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
const { setNetworkState } = useNetworkStateStore();
|
const { setNetworkState } = useNetworkStateStore();
|
||||||
|
const { setNeighbors } = useLLDPNeighborsStore();
|
||||||
const { setHdmiState } = useVideoStore();
|
const { setHdmiState } = useVideoStore();
|
||||||
const {
|
const {
|
||||||
keyboardLedState, setKeyboardLedState,
|
keyboardLedState, setKeyboardLedState,
|
||||||
|
|
@ -635,6 +638,12 @@ export default function KvmIdRoute() {
|
||||||
setUsbState(usbState);
|
setUsbState(usbState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resp.method === "lldpNeighbors") {
|
||||||
|
const neighbors = resp.params as LLDPNeighbor[];
|
||||||
|
console.debug("Setting LLDP neighbors", neighbors);
|
||||||
|
setNeighbors(neighbors);
|
||||||
|
}
|
||||||
|
|
||||||
if (resp.method === "videoInputState") {
|
if (resp.method === "videoInputState") {
|
||||||
const hdmiState = resp.params as Parameters<VideoState["setHdmiState"]>[0];
|
const hdmiState = resp.params as Parameters<VideoState["setHdmiState"]>[0];
|
||||||
console.debug("Setting HDMI state", hdmiState);
|
console.debug("Setting HDMI state", hdmiState);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -203,6 +203,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) {
|
||||||
|
|
|
||||||
24
web.go
24
web.go
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -184,6 +185,8 @@ func setupRouter() *gin.Engine {
|
||||||
protected.PUT("/auth/password-local", handleUpdatePassword)
|
protected.PUT("/auth/password-local", handleUpdatePassword)
|
||||||
protected.DELETE("/auth/local-password", handleDeletePassword)
|
protected.DELETE("/auth/local-password", handleDeletePassword)
|
||||||
protected.POST("/storage/upload", handleUploadHttp)
|
protected.POST("/storage/upload", handleUploadHttp)
|
||||||
|
|
||||||
|
protected.POST("/device/send-wol/:mac-addr", handleSendWOLMagicPacket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Catch-all route for SPA
|
// Catch-all route for SPA
|
||||||
|
|
@ -341,7 +344,6 @@ func handleWebRTCSignalWsMessages(
|
||||||
|
|
||||||
l.Trace().Msg("sending ping frame")
|
l.Trace().Msg("sending ping frame")
|
||||||
err := wsCon.Ping(runCtx)
|
err := wsCon.Ping(runCtx)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warn().Str("error", err.Error()).Msg("websocket ping error")
|
l.Warn().Str("error", err.Error()).Msg("websocket ping error")
|
||||||
cancelRun()
|
cancelRun()
|
||||||
|
|
@ -807,3 +809,23 @@ func handleSetup(c *gin.Context) {
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Device setup completed successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": "Device setup completed successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleSendWOLMagicPacket(c *gin.Context) {
|
||||||
|
inputMacAddr := c.Param("mac-addr")
|
||||||
|
macAddr, err := net.ParseMAC(inputMacAddr)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Str("sendWol", inputMacAddr).Msg("Invalid mac address provided")
|
||||||
|
c.String(http.StatusBadRequest, "Invalid mac address provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
macAddrString := macAddr.String()
|
||||||
|
err = rpcSendWOLMagicPacket(macAddrString)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Str("sendWOL", macAddrString).Msg("Failed to send WOL magic packet")
|
||||||
|
c.String(http.StatusInternalServerError, "Failed to send WOL to %s: %v", macAddrString, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.String(http.StatusOK, "WOL sent to %s ", macAddr)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue