diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index aa803f6..8f0af4b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "JetKVM", - "image": "mcr.microsoft.com/devcontainers/go:1-1.23-bookworm", + "image": "mcr.microsoft.com/devcontainers/go:1-1.24-bookworm", "features": { "ghcr.io/devcontainers/features/node:1": { // Should match what is defined in ui/package.json diff --git a/Makefile b/Makefile index 2f3c74a..87cd2bd 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,9 @@ VERSION ?= 0.4.4 PROMETHEUS_TAG := github.com/prometheus/common/version KVM_PKG_NAME := github.com/jetkvm/kvm +BUILDKIT_FLAVOR := arm-rockchip830-linux-uclibcgnueabihf +BUILDKIT_PATH ?= /opt/jetkvm-native-buildkit + GO_BUILD_ARGS := -tags netgo GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS) GO_LDFLAGS := \ @@ -17,7 +20,19 @@ GO_LDFLAGS := \ -X $(PROMETHEUS_TAG).Revision=$(REVISION) \ -X $(KVM_PKG_NAME).builtTimestamp=$(BUILDTS) -GO_CMD := GOOS=linux GOARCH=arm GOARM=7 go +GO_ARGS := GOOS=linux GOARCH=arm GOARM=7 +# if BUILDKIT_PATH exists, use buildkit to build +ifneq ($(wildcard $(BUILDKIT_PATH)),) + GO_ARGS := $(GO_ARGS) \ + CGO_CFLAGS="-I$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/include -I$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/sysroot/usr/include" \ + CGO_LDFLAGS="-L$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/lib -L$(BUILDKIT_PATH)/$(BUILDKIT_FLAVOR)/sysroot/usr/lib" \ + CC="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-gcc" \ + LD="$(BUILDKIT_PATH)/bin/$(BUILDKIT_FLAVOR)-ld" \ + CGO_ENABLED=1 +endif + +GO_CMD := $(GO_ARGS) go + BIN_DIR := $(shell pwd)/bin TEST_DIRS := $(shell find . -name "*_test.go" -type f -exec dirname {} \; | sort -u) @@ -32,6 +47,13 @@ build_dev: hash_resource $(GO_RELEASE_BUILD_ARGS) \ -o $(BIN_DIR)/jetkvm_app cmd/main.go +build_afpacket: + @echo "Building..." + $(GO_CMD) build \ + -ldflags="$(GO_LDFLAGS) -X $(KVM_PKG_NAME).builtAppVersion=$(VERSION_DEV)" \ + $(GO_RELEASE_BUILD_ARGS) \ + -o $(BIN_DIR)/afpacket internal/lldp/cmd/afp.go + build_test2json: $(GO_CMD) build -o $(BIN_DIR)/test2json cmd/test2json @@ -86,4 +108,4 @@ release: @echo "Uploading release..." @shasum -a 256 bin/jetkvm_app | cut -d ' ' -f 1 > bin/jetkvm_app.sha256 rclone copyto bin/jetkvm_app r2://jetkvm-update/app/$(VERSION)/jetkvm_app - rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256 + rclone copyto bin/jetkvm_app.sha256 r2://jetkvm-update/app/$(VERSION)/jetkvm_app.sha256 \ No newline at end of file diff --git a/go.mod b/go.mod index 3e38ac1..cbabaa6 100644 --- a/go.mod +++ b/go.mod @@ -11,12 +11,15 @@ require ( github.com/coreos/go-oidc/v3 v3.11.0 github.com/creack/pty v1.1.23 github.com/fsnotify/fsnotify v1.9.0 - github.com/gin-contrib/logger v1.2.6 - github.com/gin-gonic/gin v1.10.1 + github.com/gin-contrib/logger v1.2.5 + github.com/gin-gonic/gin v1.10.0 + github.com/google/gopacket v1.1.19 github.com/google/uuid v1.6.0 github.com/guregu/null/v6 v6.0.0 github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf - github.com/hanwen/go-fuse/v2 v2.8.0 + github.com/hanwen/go-fuse/v2 v2.5.1 + github.com/hashicorp/go-getter/v2 v2.2.3 + github.com/jellydator/ttlcache/v3 v3.3.0 github.com/pion/logging v0.2.3 github.com/pion/mdns/v2 v2.0.7 github.com/pion/webrtc/v4 v4.0.16 @@ -30,15 +33,16 @@ require ( github.com/stretchr/testify v1.10.0 github.com/vishvananda/netlink v1.3.0 go.bug.st/serial v1.6.2 - golang.org/x/crypto v0.39.0 - golang.org/x/net v0.41.0 - golang.org/x/sys v0.33.0 + golang.org/x/crypto v0.37.0 + golang.org/x/net v0.39.0 + golang.org/x/sys v0.32.0 ) replace github.com/pojntfx/go-nbd v0.3.2 => github.com/chemhack/go-nbd v0.0.0-20241006125820-59e45f5b1e7b require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/bytedance/sonic v1.13.2 // indirect github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -52,11 +56,19 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.0 // indirect + github.com/hashicorp/go-multierror v1.1.0 // indirect + github.com/hashicorp/go-safetemp v1.0.0 // indirect + github.com/hashicorp/go-version v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.0.0 // indirect + github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -80,11 +92,13 @@ require ( github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + github.com/ulikunitz/xz v0.5.8 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/arch v0.17.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/text v0.24.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 6e7e5d5..53026da 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/beevik/ntp v1.4.3 h1:PlbTvE5NNy4QHmA4Mg57n7mcFTmr1W1j3gcK7L1lqho= github.com/beevik/ntp v1.4.3/go.mod h1:Unr8Zg+2dRn7d8bHFuehIMSvvUYssHMxW3Q5Nx4RW5Q= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ= github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -32,12 +34,12 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= -github.com/gin-contrib/logger v1.2.6 h1:EPolruKUTzNXMVBD9LuAFQmRjTs7AH7yKGuXgYqrKWc= -github.com/gin-contrib/logger v1.2.6/go.mod h1:7niPrd7F0Nscw/zvgz8RiGJxSdbKM2yfQNy8xCHcm64= +github.com/gin-contrib/logger v1.2.5 h1:qVQI4omayQecuN4zX9ZZnsOq7w9J/ZLds3J/FMn8ypM= +github.com/gin-contrib/logger v1.2.5/go.mod h1:/bj+vNMuA2xOEQ1aRHoJ1m9+uyaaXIAxQTvM2llsc6I= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ= -github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -54,14 +56,30 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ= github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ= github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf h1:JO6ISZIvEUitto5zjQ3/VEnDM5rPbqIFuOhS0U0ByeA= github.com/gwatts/rootcerts v0.0.0-20240401182218-3ab9db955caf/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g= -github.com/hanwen/go-fuse/v2 v2.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs= -github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI= +github.com/hanwen/go-fuse/v2 v2.5.1 h1:OQBE8zVemSocRxA4OaFJbjJ5hlpCmIWbGr7r0M4uoQQ= +github.com/hanwen/go-fuse/v2 v2.5.1/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0 h1:wvCrVc9TjDls6+YGAF2hAifE1E5U1+b4tH6KdvN3Gig= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-getter/v2 v2.2.3 h1:6CVzhT0KJQHqd9b0pK3xSP0CM/Cv+bVhk+jcaRJ2pGk= +github.com/hashicorp/go-getter/v2 v2.2.3/go.mod h1:hp5Yy0GMQvwWVUmwLs3ygivz1JSLI323hdIE9J9m7TY= +github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc= +github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -77,6 +95,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -88,8 +107,12 @@ 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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= -github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -167,6 +190,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQdrZk= github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= @@ -175,23 +200,41 @@ github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/arch v0.17.0 h1:4O3dfLzd+lQewptAHqjewQZQDyEdejz3VwgeYwkZneU= golang.org/x/arch v0.17.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +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-20220520151302-bc2c85ada10a/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/confparser/confparser_test.go b/internal/confparser/confparser_test.go index 07d057e..d96f2b1 100644 --- a/internal/confparser/confparser_test.go +++ b/internal/confparser/confparser_test.go @@ -39,7 +39,7 @@ type testNetworkConfig struct { IPv6Mode null.String `json:"ipv6_mode" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"` IPv6Static *testIPv6StaticConfig `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,enabled" default:"enabled"` 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"` TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` diff --git a/internal/lldp/afpacket.go b/internal/lldp/afpacket.go new file mode 100644 index 0000000..10775dd --- /dev/null +++ b/internal/lldp/afpacket.go @@ -0,0 +1,84 @@ +package lldp + +import ( + "fmt" + "net" + "os" + "syscall" + "unsafe" + + "github.com/google/gopacket/afpacket" + "golang.org/x/sys/unix" +) + +const ( + afPacketBufferSize = 2 // in MiB + afPacketSnaplen = 9216 +) + +func afPacketComputeSize(targetSizeMb int, snaplen int, pageSize int) ( + frameSize int, blockSize int, numBlocks int, err error) { + if snaplen < pageSize { + frameSize = pageSize / (pageSize / snaplen) + } else { + frameSize = (snaplen/pageSize + 1) * pageSize + } + + // 128 is the default from the gopacket library so just use that + blockSize = frameSize * 128 + numBlocks = (targetSizeMb * 1024 * 1024) / blockSize + + if numBlocks == 0 { + return 0, 0, 0, fmt.Errorf("interface buffersize is too small") + } + + return frameSize, blockSize, numBlocks, nil +} + +func afPacketNewTPacket(ifName string) (*afpacket.TPacket, error) { + szFrame, szBlock, numBlocks, err := afPacketComputeSize( + afPacketBufferSize, + afPacketSnaplen, + os.Getpagesize()) + if err != nil { + return nil, err + } + + return afpacket.NewTPacket( + afpacket.OptInterface(ifName), + afpacket.OptFrameSize(szFrame), + afpacket.OptBlockSize(szBlock), + afpacket.OptNumBlocks(numBlocks), + afpacket.OptAddVLANHeader(false), + afpacket.SocketRaw, + afpacket.TPacketVersion3) +} + +type ifreq struct { + ifrName [IFNAMSIZ]byte + ifrHwaddr syscall.RawSockaddr +} + +func addMulticastAddr(ifName string, addr net.HardwareAddr) error { + fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0) + if err != nil { + return err + } + defer syscall.Close(fd) + + var name [IFNAMSIZ]byte + copy(name[:], []byte(ifName)) + + ifr := &ifreq{ + ifrName: name, + ifrHwaddr: toRawSockaddr(addr), + } + + _, _, ep := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), + unix.SIOCADDMULTI, uintptr(unsafe.Pointer(ifr))) + + if ep != 0 { + return syscall.Errno(ep) + } + return nil +} diff --git a/internal/lldp/afpacket_arm.go b/internal/lldp/afpacket_arm.go new file mode 100644 index 0000000..470d571 --- /dev/null +++ b/internal/lldp/afpacket_arm.go @@ -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 +} diff --git a/internal/lldp/afpacket_nonarm.go b/internal/lldp/afpacket_nonarm.go new file mode 100644 index 0000000..2f11c82 --- /dev/null +++ b/internal/lldp/afpacket_nonarm.go @@ -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 +} diff --git a/internal/lldp/lldp.go b/internal/lldp/lldp.go new file mode 100644 index 0000000..85020b6 --- /dev/null +++ b/internal/lldp/lldp.go @@ -0,0 +1,106 @@ +package lldp + +import ( + "context" + "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 { + l *zerolog.Logger + tPacket *afpacket.TPacket + pktSource *gopacket.PacketSource + rxCtx context.Context + rxCancel context.CancelFunc + rxLock sync.Mutex + + enableRx bool + enableTx bool + + packets chan gopacket.Packet + interfaceName string + stop chan struct{} + + neighbors *ttlcache.Cache[string, Neighbor] +} + +type LLDPOptions struct { + InterfaceName string + EnableRx bool + EnableTx bool + Logger *zerolog.Logger +} + +func NewLLDP(opts *LLDPOptions) *LLDP { + if opts.Logger == nil { + opts.Logger = defaultLogger + } + + if opts.InterfaceName == "" { + opts.Logger.Fatal().Msg("InterfaceName is required") + } + + return &LLDP{ + interfaceName: opts.InterfaceName, + enableRx: opts.EnableRx, + enableTx: opts.EnableTx, + l: opts.Logger, + neighbors: ttlcache.New(ttlcache.WithTTL[string, Neighbor](1 * time.Hour)), + } +} + +func (l *LLDP) Start() error { + l.rxLock.Lock() + defer l.rxLock.Unlock() + + if l.rxCtx != nil { + l.l.Info().Msg("LLDP already started") + return nil + } + + l.rxCtx, l.rxCancel = context.WithCancel(context.Background()) + + if l.enableRx { + l.l.Info().Msg("setting up AF_PACKET") + if err := l.setUpCapture(); err != nil { + l.l.Error().Err(err).Msg("unable to set up AF_PACKET") + return err + } + if err := l.startCapture(); err != nil { + l.l.Error().Err(err).Msg("unable to start capture") + return err + } + } + + go l.neighbors.Start() + + return nil +} + +func (l *LLDP) Stop() error { + l.rxLock.Lock() + defer l.rxLock.Unlock() + + if l.rxCancel != nil { + l.rxCancel() + l.rxCancel = nil + l.rxCtx = nil + } + + if l.enableRx { + _ = l.shutdownCapture() + } + + l.neighbors.Stop() + l.neighbors.DeleteAll() + + return nil +} diff --git a/internal/lldp/neigh.go b/internal/lldp/neigh.go new file mode 100644 index 0000000..f144238 --- /dev/null +++ b/internal/lldp/neigh.go @@ -0,0 +1,57 @@ +package lldp + +import "time" + +type Neighbor struct { + Mac string `json:"mac"` + Source string `json:"source"` + ChassisID string `json:"chassis_id"` + PortID string `json:"port_id"` + PortDescription string `json:"port_description"` + SystemName string `json:"system_name"` + SystemDescription string `json:"system_description"` + TTL uint16 `json:"ttl"` + ManagementAddress string `json:"management_address"` + Values map[string]string `json:"values"` +} + +func (l *LLDP) addNeighbor(mac string, neighbor Neighbor, ttl time.Duration) { + logger := l.l.With(). + Str("mac", mac). + Interface("neighbor", neighbor). + Logger() + + current_neigh := l.neighbors.Get(mac) + if current_neigh != nil { + current_source := current_neigh.Value().Source + if current_source == "lldp" && neighbor.Source != "lldp" { + logger.Info().Msg("skip updating neighbor, as LLDP has higher priority") + return + } + } + + logger.Info().Msg("adding neighbor") + l.neighbors.Set(mac, neighbor, ttl) +} + +func (l *LLDP) deleteNeighbor(mac string) { + logger := l.l.With(). + Str("mac", mac). + Logger() + + logger.Info().Msg("deleting neighbor") + l.neighbors.Delete(mac) +} + +func (l *LLDP) GetNeighbors() []Neighbor { + items := l.neighbors.Items() + neighbors := make([]Neighbor, 0, len(items)) + + for _, item := range items { + neighbors = append(neighbors, item.Value()) + } + + l.l.Info().Interface("neighbors", neighbors).Msg("neighbors") + + return neighbors +} diff --git a/internal/lldp/rx.go b/internal/lldp/rx.go new file mode 100644 index 0000000..759d3ba --- /dev/null +++ b/internal/lldp/rx.go @@ -0,0 +1,264 @@ +package lldp + +import ( + "fmt" + "net" + "time" + + "github.com/google/gopacket" + "github.com/google/gopacket/layers" + "github.com/rs/zerolog" + "golang.org/x/net/bpf" +) + +const IFNAMSIZ = 16 + +var ( + lldpDefaultTTL = 120 * time.Second + cdpDefaultTTL = 180 * time.Second +) + +// from lldpd +// https://github.com/lldpd/lldpd/blob/9034c9332cca0c8b1a20e1287f0e5fed81f7eb2a/src/daemon/lldpd.h#L246 +// +//nolint:govet +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}, +} + +var multicastAddrs = []string{ + // LLDP + "01:80:C2:00:00:00", + "01:80:C2:00:00:03", + "01:80:C2:00:00:0E", + // CDP + "01:00:0C:CC:CC:CC", +} + +func (l *LLDP) setUpCapture() error { + logger := l.l.With().Str("interface", l.interfaceName).Logger() + tPacket, err := afPacketNewTPacket(l.interfaceName) + if err != nil { + return err + } + logger.Info().Msg("created TPacket") + + // set up multicast addresses + // otherwise the kernel might discard the packets + // another workaround would be to enable promiscuous mode but that's too tricky + for _, mac := range multicastAddrs { + hwAddr, err := net.ParseMAC(mac) + if err != nil { + logger.Error().Msgf("unable to parse MAC address %s: %s", mac, err) + continue + } + + if err := addMulticastAddr(l.interfaceName, hwAddr); err != nil { + logger.Error().Msgf("unable to add multicast address %s: %s", mac, err) + continue + } + + logger.Info(). + Interface("hwaddr", hwAddr). + Msgf("added multicast address") + } + + if err = tPacket.SetBPF(bpfFilter); err != nil { + logger.Error().Msgf("unable to set BPF filter: %s", err) + tPacket.Close() + return err + } + logger.Info().Msg("BPF filter set") + + l.pktSource = gopacket.NewPacketSource(tPacket, layers.LayerTypeEthernet) + l.tPacket = tPacket + + return nil +} + +func (l *LLDP) startCapture() error { + logger := l.l.With().Str("interface", l.interfaceName).Logger() + if l.tPacket == nil { + return fmt.Errorf("AFPacket not initialized") + } + + if l.pktSource == nil { + return fmt.Errorf("packet source not initialized") + } + + go func() { + logger.Info().Msg("starting capture LLDP ethernet frames") + + for { + select { + case <-l.rxCtx.Done(): + logger.Info().Msg("shutting down LLDP capture") + return + case packet := <-l.pktSource.Packets(): + if err := l.handlePacket(packet, &logger); err != nil { + logger.Error().Msgf("error handling packet: %s", err) + } + } + } + }() + + return nil +} + +func (l *LLDP) handlePacket(packet gopacket.Packet, logger *zerolog.Logger) error { + linkLayer := packet.LinkLayer() + if linkLayer == nil { + return fmt.Errorf("no link layer") + } + + srcMac := linkLayer.LinkFlow().Src().String() + dstMac := linkLayer.LinkFlow().Dst().String() + + logger.Trace(). + Str("src_mac", srcMac). + Str("dst_mac", dstMac). + Int("length", len(packet.Data())). + Hex("data", packet.Data()). + Msg("received packet") + + lldpRaw := packet.Layer(layers.LayerTypeLinkLayerDiscovery) + if lldpRaw != nil { + logger.Trace().Msgf("Found LLDP Frame") + + lldpInfo := packet.Layer(layers.LayerTypeLinkLayerDiscoveryInfo) + if lldpInfo == nil { + return fmt.Errorf("no LLDP info layer") + } + + return l.handlePacketLLDP( + srcMac, + lldpRaw.(*layers.LinkLayerDiscovery), + lldpInfo.(*layers.LinkLayerDiscoveryInfo), + ) + } + + cdpRaw := packet.Layer(layers.LayerTypeCiscoDiscovery) + if cdpRaw != nil { + logger.Trace().Msgf("Found CDP Frame") + + cdpInfo := packet.Layer(layers.LayerTypeCiscoDiscoveryInfo) + if cdpInfo == nil { + return fmt.Errorf("no CDP info layer") + } + + return l.handlePacketCDP( + srcMac, + cdpRaw.(*layers.CiscoDiscovery), + cdpInfo.(*layers.CiscoDiscoveryInfo), + ) + } + + return nil +} + +func (l *LLDP) handlePacketLLDP(mac string, raw *layers.LinkLayerDiscovery, info *layers.LinkLayerDiscoveryInfo) error { + n := &Neighbor{ + Values: make(map[string]string), + Source: "lldp", + Mac: mac, + } + gotEnd := false + + ttl := lldpDefaultTTL + + for _, v := range raw.Values { + switch v.Type { + case layers.LLDPTLVEnd: + gotEnd = true + case layers.LLDPTLVChassisID: + n.ChassisID = string(raw.ChassisID.ID) + n.Values["chassis_id"] = n.ChassisID + case layers.LLDPTLVPortID: + n.PortID = string(raw.PortID.ID) + n.Values["port_id"] = n.PortID + case layers.LLDPTLVPortDescription: + n.PortDescription = info.PortDescription + n.Values["port_description"] = n.PortDescription + case layers.LLDPTLVSysName: + n.SystemName = info.SysName + n.Values["system_name"] = n.SystemName + case layers.LLDPTLVSysDescription: + n.SystemDescription = info.SysDescription + n.Values["system_description"] = n.SystemDescription + case layers.LLDPTLVMgmtAddress: + // n.ManagementAddress = info.MgmtAddress.Address + case layers.LLDPTLVTTL: + n.TTL = uint16(raw.TTL) + ttl = time.Duration(n.TTL) * time.Second + n.Values["ttl"] = fmt.Sprintf("%d", n.TTL) + case layers.LLDPTLVOrgSpecific: + for _, org := range info.OrgTLVs { + n.Values[fmt.Sprintf("org_specific_%d", org.OUI)] = string(org.Info) + } + } + } + + if gotEnd || ttl < 1*time.Second { + l.deleteNeighbor(mac) + } else { + l.addNeighbor(mac, *n, ttl) + } + + return nil +} + +func (l *LLDP) handlePacketCDP(mac string, raw *layers.CiscoDiscovery, info *layers.CiscoDiscoveryInfo) error { + // TODO: implement full CDP parsing + n := &Neighbor{ + Values: make(map[string]string), + Source: "cdp", + Mac: mac, + } + + ttl := cdpDefaultTTL + + n.ChassisID = info.DeviceID + n.PortID = info.PortID + n.SystemName = info.SysName + n.SystemDescription = info.Platform + n.TTL = uint16(raw.TTL) + + if n.TTL > 1 { + ttl = time.Duration(n.TTL) * time.Second + } + + if len(info.MgmtAddresses) > 0 { + n.ManagementAddress = string(info.MgmtAddresses[0]) + } + + l.addNeighbor(mac, *n, ttl) + + return nil +} + +func (l *LLDP) shutdownCapture() error { + if l.tPacket != nil { + l.l.Info().Msg("closing TPacket") + l.tPacket.Close() + l.tPacket = nil + } + + if l.pktSource != nil { + l.l.Info().Msg("closing packet source") + l.pktSource = nil + } + + return nil +} diff --git a/internal/network/config.go b/internal/network/config.go index 74ddf19..69f16d5 100644 --- a/internal/network/config.go +++ b/internal/network/config.go @@ -41,7 +41,7 @@ type NetworkConfig struct { 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"` - 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,basic,all,enabled" default:"enabled"` 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"` TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` diff --git a/internal/network/lldp.go b/internal/network/lldp.go new file mode 100644 index 0000000..8a3e214 --- /dev/null +++ b/internal/network/lldp.go @@ -0,0 +1,46 @@ +package network + +import ( + "errors" + + "github.com/jetkvm/kvm/internal/lldp" +) + +func (s *NetworkInterfaceState) shouldStartLLDP() bool { + if s.lldp == nil { + s.l.Trace().Msg("LLDP not initialized") + return false + } + + s.l.Trace().Msgf("LLDP mode: %s", s.config.LLDPMode.String) + + return s.config.LLDPMode.String != "disabled" +} + +func (s *NetworkInterfaceState) startLLDP() { + if !s.shouldStartLLDP() || s.lldp == nil { + return + } + + s.l.Trace().Msg("starting LLDP") + if err := s.lldp.Start(); err != nil { + s.l.Error().Err(err).Msg("unable to start LLDP") + } +} + +func (s *NetworkInterfaceState) stopLLDP() { + if s.lldp == nil { + return + } + s.l.Trace().Msg("stopping LLDP") + if err := s.lldp.Stop(); err != nil { + s.l.Error().Err(err).Msg("unable to stop LLDP") + } +} + +func (s *NetworkInterfaceState) GetLLDPNeighbors() ([]lldp.Neighbor, error) { + if s.lldp == nil { + return nil, errors.New("lldp not initialized") + } + return s.lldp.GetNeighbors(), nil +} diff --git a/internal/network/netif.go b/internal/network/netif.go index c5db806..e20d24d 100644 --- a/internal/network/netif.go +++ b/internal/network/netif.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/jetkvm/kvm/internal/confparser" + "github.com/jetkvm/kvm/internal/lldp" "github.com/jetkvm/kvm/internal/logging" "github.com/jetkvm/kvm/internal/udhcpc" "github.com/rs/zerolog" @@ -29,6 +30,8 @@ type NetworkInterfaceState struct { config *NetworkConfig dhcpClient *udhcpc.DHCPClient + lldp *lldp.LLDP + defaultHostname string currentHostname string currentFqdn string @@ -96,8 +99,16 @@ func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceS }, }) - s.dhcpClient = dhcpClient + // create the lldp service + lldpClient := lldp.NewLLDP(&lldp.LLDPOptions{ + InterfaceName: opts.InterfaceName, + EnableRx: true, + EnableTx: true, + Logger: l, + }) + s.dhcpClient = dhcpClient + s.lldp = lldpClient return s, nil } @@ -310,14 +321,30 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) { } if initialCheck { - s.onInitialCheck(s) + s.handleInitialCheck() } else if changed { - s.onStateChange(s) + s.handleStateChange() } return dhcpTargetState, nil } +func (s *NetworkInterfaceState) handleInitialCheck() { + if s.IsUp() { + s.startLLDP() + } + s.onInitialCheck(s) +} + +func (s *NetworkInterfaceState) handleStateChange() { + if s.IsUp() { + s.startLLDP() + } else { + s.stopLLDP() + } + s.onStateChange(s) +} + func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error { dhcpTargetState, err := s.update() if err != nil { diff --git a/jsonrpc.go b/jsonrpc.go index 258828a..95474b3 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -1104,4 +1104,5 @@ var rpcHandlers = map[string]RPCHandler{ "setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}}, "getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly}, "setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}}, + "getLLDPNeighbors": {Func: rpcGetLLDPNeighbors}, } diff --git a/network.go b/network.go index 2208a47..7c2a0ae 100644 --- a/network.go +++ b/network.go @@ -3,6 +3,7 @@ package kvm import ( "fmt" + "github.com/jetkvm/kvm/internal/lldp" "github.com/jetkvm/kvm/internal/network" "github.com/jetkvm/kvm/internal/udhcpc" ) @@ -98,3 +99,7 @@ func rpcSetNetworkSettings(settings network.RpcNetworkSettings) (*network.RpcNet func rpcRenewDHCPLease() error { return networkState.RpcRenewDHCPLease() } + +func rpcGetLLDPNeighbors() ([]lldp.Neighbor, error) { + return networkState.GetLLDPNeighbors() +} diff --git a/ui/src/components/LLDPNeighCard.tsx b/ui/src/components/LLDPNeighCard.tsx new file mode 100644 index 0000000..fe95af9 --- /dev/null +++ b/ui/src/components/LLDPNeighCard.tsx @@ -0,0 +1,84 @@ +import { LLDPNeighbor } from "../hooks/stores"; +import { LifeTimeLabel } from "../routes/devices.$id.settings.network"; + +import { GridCard } from "./Card"; + +export default function LLDPNeighCard({ + neighbors, +}: { + neighbors: LLDPNeighbor[]; +}) { + return ( + +
+
+

+ LLDP Neighbors +

+ +
+ {neighbors.map(neighbor => ( +
+

{neighbor.mac}

+
+
+
+ + Interface + + {neighbor.port_description} +
+ + {neighbor.system_name && ( +
+ + System Name + + {neighbor.system_name} +
+ )} + + {neighbor.system_description && ( +
+ + System Description + + {neighbor.system_description} +
+ )} + + + {neighbor.port_id && ( +
+ + Port ID + + + {neighbor.port_id} + +
+ )} + + + {neighbor.port_description && ( +
+ + Port Description + + + {neighbor.port_description} + +
+ )} +
+
+
+ ))} +
+
+
+
+ ); +} diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 6bc7e17..625741c 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -741,7 +741,7 @@ export type IPv6Mode = | "link_local" | "unknown"; export type IPv4Mode = "disabled" | "static" | "dhcp" | "unknown"; -export type LLDPMode = "disabled" | "basic" | "all" | "unknown"; +export type LLDPMode = "disabled" | "basic" | "all" | "tx_only" | "rx_only" | "unknown"; export type mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown"; export type TimeSyncMode = | "ntp_only" @@ -761,6 +761,19 @@ export interface NetworkSettings { time_sync_mode: TimeSyncMode; } +export interface LLDPNeighbor { + mac: string; + source: string; + chassis_id: string; + port_id: string; + port_description: string; + system_name: string; + system_description: string; + ttl: number | null; + management_address: string | null; + values: Record; +} + export const useNetworkStateStore = create((set, get) => ({ setNetworkState: (state: NetworkState) => set(state), setDhcpLease: (lease: NetworkState["dhcp_lease"]) => set({ dhcp_lease: lease }), diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index 0905db5..b486084 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -7,6 +7,7 @@ import { IPv4Mode, IPv6Mode, LLDPMode, + LLDPNeighbor, mDNSMode, NetworkSettings, NetworkState, @@ -29,6 +30,7 @@ import AutoHeight from "../components/AutoHeight"; import DhcpLeaseCard from "../components/DhcpLeaseCard"; import { SettingsItem } from "./devices.$id.settings"; +import LLDPNeighCard from "../components/LLDPNeighCard"; dayjs.extend(relativeTime); @@ -88,6 +90,14 @@ export default function SettingsNetworkRoute() { const [customDomain, setCustomDomain] = useState(""); const [selectedDomainOption, setSelectedDomainOption] = useState("dhcp"); + const [lldpNeighbors, setLldpNeighbors] = useState(undefined); + useEffect(() => { + send("getLLDPNeighbors", {}, resp => { + if ("error" in resp) return; + setLldpNeighbors(resp.result as LLDPNeighbor[]); + }); + }, [send]); + useEffect(() => { if (networkSettings.domain && networkSettingsLoaded) { // Check if the domain is one of the predefined options @@ -130,7 +140,7 @@ export default function SettingsNetworkRoute() { if ("error" in resp) { notifications.error( "Failed to save network settings: " + - (resp.error.data ? resp.error.data : resp.error.message), + (resp.error.data ? resp.error.data : resp.error.message), ); setNetworkSettingsLoaded(true); return; @@ -402,7 +412,7 @@ export default function SettingsNetworkRoute() { {!networkSettingsLoaded && - !(networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0) ? ( + !(networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0) ? (
@@ -428,22 +438,49 @@ export default function SettingsNetworkRoute() { )}
-
- + +
+ handleLldpModeChange(e.target.value)} options={filterUnknown([ { value: "disabled", label: "Disabled" }, - { value: "basic", label: "Basic" }, - { value: "all", label: "All" }, + { value: "tx_only", label: "Tx only" }, + { value: "rx_only", label: "Rx only" }, + { value: "basic", label: "Tx Minimal + Rx" }, + { value: "all", label: "Tx Detailed + Rx" }, + { value: "enabled", label: "Enabled" }, ])} /> + + {lldpNeighbors === undefined ? ( + +
+
+

+ LLDP Neighbors +

+
+
+
+
+
+
+
+ + ) : lldpNeighbors.length > 0 ? ( + + ) : ( + + )} +