├── CODEOWNERS ├── .gitignore ├── lldpd.service ├── .github ├── workflows │ ├── release-drafter.yml │ └── master.yml └── release-drafter.yml ├── README.md ├── go.mod ├── Makefile ├── LICENSE ├── pkg └── lldp │ ├── client.go │ └── server.go ├── go.sum └── main.go /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @metal-stack/go-lldpd-maintainers -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | rel 3 | .idea 4 | .vscode 5 | *.tgz 6 | -------------------------------------------------------------------------------- /lldpd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=metal-stack LLDP daemon 3 | After=network.target 4 | 5 | [Service] 6 | ExecStart=/usr/local/bin/lldpd 7 | Restart=always 8 | RestartSec=30 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release Drafter Action 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: release-drafter/release-drafter@v6 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-lldpd 2 | 3 | go-lldpd is a lldpd server written in go which sends machineUUID and installation timestamp of a bare metal server to connected switches. 4 | It is configured from a yaml file `/etc/metal/install.yaml`: 5 | 6 | ```yaml 7 | --- 8 | machineuuid: 3be6c846-57de-432a-b50e-61c6c559b6bb 9 | timestamp: 2006-01-02T15:04:05Z07:00 10 | ``` 11 | 12 | The config file location cannot be modified. 13 | go-lldpd also expects 2 distinct uplinks to the switch, otherwise it will die. 14 | 15 | Example systemd service is also bundled. 16 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | 4 | template: | 5 | ## General Changes 6 | 7 | $CHANGES 8 | 9 | categories: 10 | - title: '🚀 Features' 11 | labels: 12 | - 'feature' 13 | - 'enhancement' 14 | - title: '🐛 Bug Fixes' 15 | labels: 16 | - 'fix' 17 | - 'bugfix' 18 | - 'bug' 19 | 20 | version-resolver: 21 | major: 22 | labels: 23 | - 'major' 24 | minor: 25 | labels: 26 | - 'minor' 27 | patch: 28 | labels: 29 | - 'patch' 30 | default: patch 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/metal-stack/go-lldpd 2 | 3 | go 1.25 4 | 5 | require ( 6 | github.com/google/gopacket v1.1.19 7 | github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 8 | github.com/mdlayher/lldp v0.0.0-20150915211757-afd9f83164c5 9 | github.com/metal-stack/v v1.0.3 10 | github.com/vishvananda/netlink v1.3.1 11 | golang.org/x/sys v0.38.0 12 | gopkg.in/yaml.v3 v3.0.1 13 | ) 14 | 15 | require ( 16 | github.com/kr/text v0.2.0 // indirect 17 | github.com/vishvananda/netns v0.0.5 // indirect 18 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .ONESHELL: 2 | SHA := $(shell git rev-parse --short=8 HEAD) 3 | GITVERSION := $(shell git describe --long --all) 4 | BUILDDATE := $(shell date -Iseconds) 5 | VERSION := $(or ${TAG_NAME},$(shell git describe --tags --exact-match 2> /dev/null || git symbolic-ref -q --short HEAD || git rev-parse --short HEAD)) 6 | 7 | GO111MODULE := on 8 | 9 | .PHONY: all 10 | all: 11 | go build \ 12 | -trimpath \ 13 | -tags netgo \ 14 | -ldflags "-X 'github.com/metal-stack/v.Version=$(VERSION)' \ 15 | -X 'github.com/metal-stack/v.Revision=$(GITVERSION)' \ 16 | -X 'github.com/metal-stack/v.GitSHA1=$(SHA)' \ 17 | -X 'github.com/metal-stacj/v.BuildDate=$(BUILDDATE)'" \ 18 | -o bin/lldpd main.go 19 | strip bin/lldpd 20 | 21 | .PHONY: release 22 | release: 23 | rm -rf rel 24 | mkdir -p rel/usr/local/bin rel/etc/systemd/system 25 | cp bin/lldpd rel/usr/local/bin 26 | cp lldpd.service rel/etc/systemd/system 27 | cd rel \ 28 | && tar --owner=root --group=root -cvzf go-lldpd.tgz usr/local/bin/lldpd etc/systemd/system/lldpd.service \ 29 | && mv go-lldpd.tgz .. \ 30 | && cd - 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 The metal-stack Authors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: master 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | release: 11 | types: 12 | - published 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v5 21 | 22 | - name: Set up Go 1.25 23 | uses: actions/setup-go@v6 24 | with: 25 | go-version: '1.25.x' 26 | 27 | - name: Install libpcap-dev 28 | run: sudo apt-get install -y libpcap-dev 29 | 30 | - name: Lint 31 | uses: golangci/golangci-lint-action@v8 32 | 33 | - name: Make tag 34 | run: | 35 | [ "${GITHUB_EVENT_NAME}" == 'pull_request' ] && echo "TAG_NAME=$(echo $GITHUB_REF | awk -F / '{print $3}')-${GITHUB_HEAD_REF##*/}" >> $GITHUB_ENV || true 36 | [ "${GITHUB_EVENT_NAME}" == 'release' ] && echo "TAG_NAME=${GITHUB_REF##*/}" >> $GITHUB_ENV || true 37 | [ "${GITHUB_EVENT_NAME}" == 'push' ] && echo "TAG_NAME=latest" >> $GITHUB_ENV || true 38 | 39 | - name: Build project 40 | run: | 41 | make 42 | 43 | - name: Generate SBOM for lldpd binary 44 | uses: anchore/sbom-action@v0 45 | env: 46 | SBOM_NAME: sbom.json 47 | with: 48 | file: ./bin/lldpd 49 | format: spdx-json 50 | artifact-name: ${{ env.SBOM_NAME }} 51 | output-file: ./${{ env.SBOM_NAME }} 52 | 53 | - name: Prepare release 54 | run: | 55 | make release 56 | if: ${{ github.event_name == 'release' }} 57 | 58 | - name: Upload Release Asset 59 | id: upload-release-asset 60 | uses: softprops/action-gh-release@v1 61 | with: 62 | token: ${{ secrets.GITHUB_TOKEN }} 63 | files: | 64 | go-lldpd.tgz 65 | sbom.json 66 | if: ${{ github.event_name == 'release' }} 67 | -------------------------------------------------------------------------------- /pkg/lldp/client.go: -------------------------------------------------------------------------------- 1 | //go:build client 2 | 3 | package lldp 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "log/slog" 9 | "net" 10 | "time" 11 | 12 | "github.com/google/gopacket" 13 | "github.com/google/gopacket/layers" 14 | "github.com/google/gopacket/pcap" 15 | ) 16 | 17 | // Client consumes lldp messages. 18 | type Client struct { 19 | interfaceName string 20 | handle *pcap.Handle 21 | ctx context.Context 22 | } 23 | 24 | // DiscoveryResult holds optional TLV SysName and SysDescription fields of a real lldp frame. 25 | type DiscoveryResult struct { 26 | SysName string 27 | SysDescription string 28 | } 29 | 30 | // NewClient creates a new lldp client. 31 | func NewClient(ctx context.Context, iface net.Interface) *Client { 32 | return &Client{ 33 | interfaceName: iface.Name, 34 | ctx: ctx, 35 | } 36 | } 37 | 38 | // Start searches on the configured interface for lldp packages and 39 | // pushes the optional TLV SysName and SysDescription fields of each 40 | // found lldp package into the given channel. 41 | func (l *Client) Start(log *slog.Logger, resultChan chan<- DiscoveryResult) error { 42 | defer func() { 43 | log.Warn("terminating lldp discovery for interface", "interface", l.interfaceName) 44 | l.Close() 45 | }() 46 | 47 | var packetSource *gopacket.PacketSource 48 | for { 49 | // Recreate interface handle if not exists 50 | if l.handle == nil { 51 | var err error 52 | l.handle, err = pcap.OpenLive(l.interfaceName, 65536, true, 5*time.Second) 53 | if err != nil { 54 | return fmt.Errorf("unable to open interface:%s in promiscuous mode: %w", l.interfaceName, err) 55 | } 56 | 57 | // filter only lldp packages 58 | bpfFilter := fmt.Sprintf("ether proto %#x", etherType) 59 | err = l.handle.SetBPFFilter(bpfFilter) 60 | if err != nil { 61 | return fmt.Errorf("unable to filter lldp ethernet traffic %#x on interface:%s %w", etherType, l.interfaceName, err) 62 | } 63 | 64 | packetSource = gopacket.NewPacketSource(l.handle, l.handle.LinkType()) 65 | } 66 | 67 | select { 68 | case packet, ok := <-packetSource.Packets(): 69 | if !ok { 70 | l.handle.Close() 71 | l.handle = nil 72 | log.Debug("EOF error for the handle") 73 | continue 74 | } 75 | 76 | if packet.LinkLayer().LayerType() != layers.LayerTypeEthernet { 77 | continue 78 | } 79 | for _, layer := range packet.Layers() { 80 | if layer.LayerType() != layers.LayerTypeLinkLayerDiscoveryInfo { 81 | continue 82 | } 83 | info, ok := layer.(*layers.LinkLayerDiscoveryInfo) 84 | if !ok { 85 | log.Warn("packet is not LinkLayerDiscoveryInfo", "layer", layer) 86 | continue 87 | } 88 | dr := DiscoveryResult{ 89 | SysName: info.SysName, 90 | SysDescription: info.SysDescription, 91 | } 92 | resultChan <- dr 93 | } 94 | case <-l.ctx.Done(): 95 | log.Debug("context done, terminating lldp discovery") 96 | return nil 97 | } 98 | } 99 | } 100 | 101 | // Close the LLDP client 102 | func (l *Client) Close() { 103 | if l.handle != nil { 104 | l.handle.Close() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 3 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 4 | github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 5 | github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 6 | github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 7 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 8 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 9 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 10 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 11 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 12 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 13 | github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 h1:2oDp6OOhLxQ9JBoUuysVz9UZ9uI6oLUbvAZu0x8o+vE= 14 | github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118/go.mod h1:ZFUnHIVchZ9lJoWoEGUg8Q3M4U8aNNWA3CVSUTkW4og= 15 | github.com/mdlayher/lldp v0.0.0-20150915211757-afd9f83164c5 h1:i4JJtLb5iDVsncU7splD9ZCQXvxN13tGDUWihfKOq18= 16 | github.com/mdlayher/lldp v0.0.0-20150915211757-afd9f83164c5/go.mod h1:IZAsRpRUv/4B6NhGzofHK/+I+N31NTUz/hrEm4ssUwA= 17 | github.com/mdlayher/packet v1.0.0/go.mod h1:eE7/ctqDhoiRhQ44ko5JZU2zxB88g+JH/6jmnjzPjOU= 18 | github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E= 19 | github.com/metal-stack/v v1.0.3 h1:Sh2oBlnxrCUD+mVpzfC8HiqL045YWkxs0gpTvkjppqs= 20 | github.com/metal-stack/v v1.0.3/go.mod h1:YTahEu7/ishwpYKnp/VaW/7nf8+PInogkfGwLcGPdXg= 21 | github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= 22 | github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= 23 | github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= 24 | github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 26 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 27 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 28 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 29 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 30 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 31 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 32 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 33 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 34 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 35 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 36 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 37 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 38 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 40 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 42 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 43 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 44 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 45 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 46 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 47 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 48 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 49 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 50 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 51 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 52 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020 The metal-stack Authors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | // go-lldpd acts as a send only lldp daemon which is installed on every bare metal machine 26 | // to send required information to the networking backplane like uuid of the machine 27 | // and installation timestamp 28 | package main 29 | 30 | import ( 31 | "fmt" 32 | "log/slog" 33 | "os" 34 | "path" 35 | "path/filepath" 36 | "syscall" 37 | "time" 38 | 39 | "gopkg.in/yaml.v3" 40 | 41 | "github.com/metal-stack/go-lldpd/pkg/lldp" 42 | "github.com/metal-stack/v" 43 | "github.com/vishvananda/netlink" 44 | "golang.org/x/sys/unix" 45 | ) 46 | 47 | type installerConfig struct { 48 | // MachineUUID is the unique UUID for this machine, usually the board serial. 49 | MachineUUID string `yaml:"machineuuid"` 50 | // Timestamp is the the timestamp of installer config creation. 51 | Timestamp string `yaml:"timestamp"` 52 | } 53 | 54 | const ( 55 | debugFs = "/sys/kernel/debug" 56 | installYaml = "/etc/metal/install.yaml" 57 | ) 58 | 59 | // Starts lldp on every ethernet nic that is up 60 | func main() { 61 | jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{}) 62 | log := slog.New(jsonHandler) 63 | log.Info("lldpd", "version", v.V) 64 | b, err := os.ReadFile(installYaml) 65 | if err != nil { 66 | log.Error("lldpd", "unable to open config", err) 67 | os.Exit(1) 68 | } 69 | i := &installerConfig{} 70 | err = yaml.Unmarshal(b, &i) 71 | if err != nil { 72 | log.Error("lldpd", "unable to parse config", err) 73 | os.Exit(1) 74 | } 75 | 76 | stopFirmwareLLDP(log) 77 | 78 | var interfaces []string 79 | links, _ := netlink.LinkList() 80 | for _, nic := range links { 81 | if nic.Type() == "device" && nic.Attrs().EncapType == "ether" { 82 | name := nic.Attrs().Name 83 | if nic.Attrs().OperState == netlink.OperUp { 84 | interfaces = append(interfaces, name) 85 | } else { 86 | log.Info("interface is not up, will ignore it", "interface", name) 87 | } 88 | } 89 | } 90 | 91 | if len(interfaces) < 2 { 92 | log.Info("exiting, because not enough interfaces are up - we need at least two") 93 | return 94 | } 95 | log.Info("will start lldp on interfaces", "interfaces", interfaces) 96 | 97 | desc := fmt.Sprintf("provisioned since %s", i.Timestamp) 98 | for _, iface := range interfaces { 99 | lldpd, err := lldp.NewDaemon(log, i.MachineUUID, desc, iface, 2*time.Second) 100 | if err != nil { 101 | log.Error("could not start lldp for interface", "interface", iface) 102 | os.Exit(-1) 103 | } 104 | lldpd.Start() 105 | } 106 | select {} 107 | } 108 | 109 | func unmountDebugFs(log *slog.Logger) { 110 | log.Info("unmounting debugfs") 111 | err := syscall.Unmount(debugFs, syscall.MNT_FORCE) 112 | if err != nil { 113 | log.Error("unable to unmount debugfs", "error", err) 114 | } 115 | } 116 | 117 | // stopFirmwareLLDP stop Firmware LLDP not persistent over reboots, only during runtime. 118 | // mount -t debugfs none /sys/kernel/debug 119 | // echo lldp stop > /sys/kernel/debug/i40e/0000:01:00.2/command 120 | // where <0000:01:00.2> is the pci address of the ethernet nic, this can be inspected by lspci, 121 | // or a loop over all directories in /sys/kernel/debug/i40e/*/command 122 | func stopFirmwareLLDP(log *slog.Logger) { 123 | var stat syscall.Statfs_t 124 | err := syscall.Statfs(debugFs, &stat) 125 | if err != nil { 126 | log.Error("could not check whether debugfs is mounted", "error", err) 127 | return 128 | } 129 | 130 | if stat.Type != unix.DEBUGFS_MAGIC { 131 | log.Info("mounting debugfs") 132 | err := syscall.Mount("debugfs", debugFs, "debugfs", 0, "") 133 | if err != nil { 134 | log.Error("mounting debugfs failed", "error", err) 135 | return 136 | } 137 | defer unmountDebugFs(log) 138 | } 139 | 140 | var buggyIntelNicDriverNames = []string{"i40e"} 141 | for _, driver := range buggyIntelNicDriverNames { 142 | debugFSPath := path.Join(debugFs, driver) 143 | log.Info("check whether lldp needs to be deactivated", "path", debugFSPath) 144 | 145 | if _, err := os.Stat(debugFSPath); os.IsNotExist(err) { 146 | log.Info("nothing to do here, because directory for driver does not exist", "path", debugFSPath) 147 | continue 148 | } 149 | 150 | err := filepath.Walk(debugFSPath, func(path string, info os.FileInfo, err error) error { 151 | if err != nil { 152 | log.Warn("opening/reading debugfs failed", "path", path, "error", err) 153 | return err 154 | } 155 | if !info.IsDir() && info.Name() == "command" { 156 | log.Info("execute echo lldp stop > ", "path", path) 157 | stopCommand := []byte("lldp stop") 158 | err := os.WriteFile(path, stopCommand, os.ModePerm) // nolint:gosec 159 | if err != nil { 160 | log.Error("stop lldp > command failed", "path", path, "error", err) 161 | } 162 | } 163 | return nil 164 | }) 165 | if err != nil { 166 | log.Error("unable to walk through debugfs", "path", debugFSPath, "error", err) 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /pkg/lldp/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License 3 | 4 | Copyright (c) 2020 The Metal-Stack Authors. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | // Package lldp implements sending lldp packets. 26 | package lldp 27 | 28 | import ( 29 | "fmt" 30 | "net" 31 | "syscall" 32 | "time" 33 | 34 | "log/slog" 35 | 36 | "github.com/mdlayher/ethernet" 37 | "github.com/mdlayher/lldp" 38 | ) 39 | 40 | const ( 41 | // Make use of an LLDP EtherType. 42 | // https://www.iana.org/assignments/ieee-802-numbers/ieee-802-numbers.xhtml 43 | etherType = 0x88cc 44 | 45 | // TC_PRIO_CONTROL is required to be set as socket option, 46 | // otherwise newer intel nics will not forward packets from this socket 47 | // defined here: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/pkt_sched.h#n26 48 | TC_PRIO_CONTROL = 7 49 | ) 50 | 51 | var ( 52 | // See https://en.wikipedia.org/wiki/Link_Layer_Discovery_Protocol#Frame_structure 53 | // for explanation why this destination mac. 54 | destinationMac = net.HardwareAddr{0x01, 0x80, 0xc2, 0x00, 0x00, 0x0e} 55 | ) 56 | 57 | // Daemon is a lldp daemon 58 | type Daemon struct { 59 | systemName string 60 | systemDescription string 61 | ifi *net.Interface 62 | interval time.Duration 63 | lldpMessage []byte 64 | socket int 65 | log *slog.Logger 66 | } 67 | 68 | // NewDaemon create a new LLDPD instance for the given interface 69 | func NewDaemon(log *slog.Logger, systemName, systemDescription, interfaceName string, interval time.Duration) (*Daemon, error) { 70 | // Open a raw socket on the specified interface, and configure it to accept 71 | // traffic with etherecho's EtherType. 72 | ifi, err := net.InterfaceByName(interfaceName) 73 | if err != nil { 74 | return nil, fmt.Errorf("lldpd failed to find interface %q error: %w", interfaceName, err) 75 | } 76 | 77 | log.Info("lldpd", "listen on", ifi.Name) 78 | 79 | l := &Daemon{ 80 | systemName: systemName, 81 | systemDescription: systemDescription, 82 | ifi: ifi, 83 | interval: interval, 84 | log: log, 85 | } 86 | err = l.bindTo(ethernet.Broadcast) 87 | if err != nil { 88 | return nil, fmt.Errorf("lldpd failed to bind to socket: %w", err) 89 | } 90 | 91 | lldp, err := createLLDPMessage(l) 92 | if err != nil { 93 | return nil, fmt.Errorf("lldpd failed to create lldp message: %w", err) 94 | } 95 | l.lldpMessage = lldp 96 | return l, nil 97 | } 98 | 99 | // Start spawn a goroutine which sends LLDP PDU's every interval given. 100 | func (l *Daemon) Start() { 101 | go l.sendMessages() 102 | l.log.Info("lldpd", "interface", l.ifi.Name, "interval", l.interval.String()) 103 | } 104 | 105 | // create LLDPMessage as byte array 106 | func createLLDPMessage(lldpd *Daemon) ([]byte, error) { 107 | lf := lldp.Frame{ 108 | ChassisID: &lldp.ChassisID{ 109 | Subtype: lldp.ChassisIDSubtypeMACAddress, 110 | ID: []byte(lldpd.ifi.HardwareAddr), 111 | }, 112 | PortID: &lldp.PortID{ 113 | Subtype: lldp.PortIDSubtypeInterfaceName, 114 | ID: []byte(lldpd.ifi.Name), 115 | }, 116 | TTL: 2 * lldpd.interval, 117 | Optional: []*lldp.TLV{ 118 | { 119 | Type: lldp.TLVTypePortDescription, 120 | Value: []byte(lldpd.ifi.Name), 121 | Length: uint16(len(lldpd.ifi.Name)), // nolint:gosec 122 | }, 123 | { 124 | Type: lldp.TLVTypeSystemName, 125 | Value: []byte(lldpd.systemName), 126 | Length: uint16(len(lldpd.systemName)), // nolint:gosec 127 | }, 128 | { 129 | Type: lldp.TLVTypeSystemDescription, 130 | Value: []byte(lldpd.systemDescription), 131 | Length: uint16(len(lldpd.systemDescription)), // nolint:gosec 132 | }, 133 | }, 134 | } 135 | return lf.MarshalBinary() 136 | } 137 | 138 | // sendMessages continuously sends a message over a connection at regular intervals, 139 | // sourced from specified hardware address. 140 | func (l *Daemon) sendMessages() { 141 | // Message is LLDP destination. 142 | f := ðernet.Frame{ 143 | Destination: destinationMac, 144 | Source: l.ifi.HardwareAddr, 145 | EtherType: etherType, 146 | Payload: l.lldpMessage, 147 | } 148 | 149 | b, err := f.MarshalBinary() 150 | if err != nil { 151 | l.log.Error("lldpd", "failed to marshal ethernet frame", err) 152 | } 153 | 154 | // Send message forever. 155 | t := time.NewTicker(l.interval) 156 | for range t.C { 157 | if err := l.writeTo(b); err != nil { 158 | l.log.Error("lldpd", "failed to send message", err) 159 | } 160 | } 161 | } 162 | 163 | // htons converts a short (uint16) from host-to-network byte order. 164 | // Thanks to mikioh for this neat trick: 165 | // https://github.com/mikioh/-stdyng/blob/master/afpacket.go 166 | func htons(i uint16) uint16 { 167 | return (i<<8)&0xff00 | i>>8 168 | } 169 | 170 | // bindTo the specified address an store the raw socket with appropriate priority in the daemon. 171 | func (l *Daemon) bindTo(address net.HardwareAddr) error { 172 | fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, syscall.ETH_P_ALL) 173 | if err != nil { 174 | return fmt.Errorf("error creating raw packet socket:%w", err) 175 | 176 | } 177 | err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_PRIORITY, TC_PRIO_CONTROL) 178 | if err != nil { 179 | return fmt.Errorf("error in setting priority option on socket:%w", err) 180 | } 181 | 182 | var baddr [8]byte 183 | copy(baddr[:], address) 184 | addr := syscall.SockaddrLinklayer{ 185 | Protocol: htons(etherType), 186 | Ifindex: l.ifi.Index, 187 | Halen: uint8(len(address)), // nolint:gosec 188 | Addr: baddr, 189 | } 190 | 191 | err = syscall.Bind(fd, &addr) 192 | if err != nil { 193 | return fmt.Errorf("error binding to socket:%w", err) 194 | } 195 | l.socket = fd 196 | return nil 197 | } 198 | 199 | // writeTo the given byte array to the socket 200 | func (l *Daemon) writeTo(pkt []byte) error { 201 | if l.socket == 0 { 202 | return fmt.Errorf("socket is not bound") 203 | } 204 | _, err := syscall.Write(l.socket, pkt) 205 | if err != nil { 206 | return fmt.Errorf("unable to write to socket:%w", err) 207 | } 208 | return nil 209 | } 210 | --------------------------------------------------------------------------------