diff --git a/README.md b/README.md index ec5adbf7ac..3ff6c56563 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ hwmon | chip | --collector.hwmon.chip-include | --collector.hwmon.chip-exclude hwmon | sensor | --collector.hwmon.sensor-include | --collector.hwmon.sensor-exclude interrupts | name | --collector.interrupts.name-include | --collector.interrupts.name-exclude netdev | device | --collector.netdev.device-include | --collector.netdev.device-exclude +netvf | device | --collector.netvf.device-include | --collector.netvf.device-exclude qdisk | device | --collector.qdisk.device-include | --collector.qdisk.device-exclude slabinfo | slab-names | --collector.slabinfo.slabs-include | --collector.slabinfo.slabs-exclude sysctl | all | --collector.sysctl.include | N/A @@ -202,6 +203,7 @@ logind | Exposes session counts from [logind](http://www.freedesktop.org/wiki/So meminfo\_numa | Exposes memory statistics from `/sys/devices/system/node/node[0-9]*/meminfo`, `/sys/devices/system/node/node[0-9]*/numastat`. | Linux mountstats | Exposes filesystem statistics from `/proc/self/mountstats`. Exposes detailed NFS client statistics. | Linux network_route | Exposes the routing table as metrics | Linux +netvf | Exposes SR-IOV Virtual Function statistics and configuration from netlink. | Linux pcidevice | Exposes pci devices' information including their link status and parent devices. | Linux perf | Exposes perf based metrics (Warning: Metrics are dependent on kernel configuration and settings). | Linux processes | Exposes aggregate process statistics from `/proc`. | Linux diff --git a/collector/netvf_linux.go b/collector/netvf_linux.go new file mode 100644 index 0000000000..a9b4e63835 --- /dev/null +++ b/collector/netvf_linux.go @@ -0,0 +1,278 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !nonetvf + +package collector + +import ( + "errors" + "fmt" + "log/slog" + "os" + "path/filepath" + + "github.com/alecthomas/kingpin/v2" + "github.com/jsimonetti/rtnetlink/v2" + "github.com/prometheus/client_golang/prometheus" +) + +const netvfSubsystem = "net_vf" + +var ( + netvfDeviceInclude = kingpin.Flag("collector.netvf.device-include", "Regexp of PF devices to include (mutually exclusive to device-exclude).").String() + netvfDeviceExclude = kingpin.Flag("collector.netvf.device-exclude", "Regexp of PF devices to exclude (mutually exclusive to device-include).").String() +) + +func init() { + registerCollector("netvf", defaultDisabled, NewNetVFCollector) +} + +type netvfCollector struct { + logger *slog.Logger + deviceFilter deviceFilter + + info *prometheus.Desc + receivePackets *prometheus.Desc + transmitPackets *prometheus.Desc + receiveBytes *prometheus.Desc + transmitBytes *prometheus.Desc + broadcast *prometheus.Desc + multicast *prometheus.Desc + receiveDropped *prometheus.Desc + transmitDropped *prometheus.Desc +} + +func NewNetVFCollector(logger *slog.Logger) (Collector, error) { + if *netvfDeviceExclude != "" && *netvfDeviceInclude != "" { + return nil, errors.New("device-exclude & device-include are mutually exclusive") + } + + if *netvfDeviceExclude != "" { + logger.Info("Parsed flag --collector.netvf.device-exclude", "flag", *netvfDeviceExclude) + } + + if *netvfDeviceInclude != "" { + logger.Info("Parsed flag --collector.netvf.device-include", "flag", *netvfDeviceInclude) + } + + return &netvfCollector{ + logger: logger, + deviceFilter: newDeviceFilter(*netvfDeviceExclude, *netvfDeviceInclude), + info: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "info"), + "Virtual Function configuration information.", + []string{"device", "vf", "mac", "vlan", "link_state", "spoof_check", "trust", "pci_address"}, nil, + ), + receivePackets: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "receive_packets_total"), + "Number of received packets by the VF.", + []string{"device", "vf", "pci_address"}, nil, + ), + transmitPackets: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "transmit_packets_total"), + "Number of transmitted packets by the VF.", + []string{"device", "vf", "pci_address"}, nil, + ), + receiveBytes: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "receive_bytes_total"), + "Number of received bytes by the VF.", + []string{"device", "vf", "pci_address"}, nil, + ), + transmitBytes: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "transmit_bytes_total"), + "Number of transmitted bytes by the VF.", + []string{"device", "vf", "pci_address"}, nil, + ), + broadcast: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "broadcast_packets_total"), + "Number of broadcast packets received by the VF.", + []string{"device", "vf", "pci_address"}, nil, + ), + multicast: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "multicast_packets_total"), + "Number of multicast packets received by the VF.", + []string{"device", "vf", "pci_address"}, nil, + ), + receiveDropped: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "receive_dropped_total"), + "Number of dropped received packets by the VF.", + []string{"device", "vf", "pci_address"}, nil, + ), + transmitDropped: prometheus.NewDesc( + prometheus.BuildFQName(namespace, netvfSubsystem, "transmit_dropped_total"), + "Number of dropped transmitted packets by the VF.", + []string{"device", "vf", "pci_address"}, nil, + ), + }, nil +} + +func (c *netvfCollector) Update(ch chan<- prometheus.Metric) error { + conn, err := rtnetlink.Dial(nil) + if err != nil { + return fmt.Errorf("failed to connect to rtnetlink: %w", err) + } + defer conn.Close() + + links, err := conn.Link.ListWithVFInfo() + if err != nil { + return fmt.Errorf("failed to list interfaces with VF info: %w", err) + } + + vfCount := 0 + for _, link := range links { + if link.Attributes == nil { + continue + } + + // Skip interfaces without VFs + if link.Attributes.NumVF == nil || *link.Attributes.NumVF == 0 { + continue + } + + device := link.Attributes.Name + + // Apply device filter + if c.deviceFilter.ignored(device) { + c.logger.Debug("Ignoring device", "device", device) + continue + } + + for _, vf := range link.Attributes.VFInfoList { + vfID := fmt.Sprintf("%d", vf.ID) + + // Emit info metric with VF configuration + mac := "" + if vf.MAC != nil { + mac = vf.MAC.String() + } + vlan := fmt.Sprintf("%d", vf.Vlan) + linkState := vfLinkStateString(vf.LinkState) + spoofCheck := fmt.Sprintf("%t", vf.SpoofCheck) + trust := fmt.Sprintf("%t", vf.Trust) + pciAddress := resolveVFPCIAddress(sysFilePath("class"), device, vf.ID) + + ch <- prometheus.MustNewConstMetric(c.info, prometheus.GaugeValue, 1, device, vfID, mac, vlan, linkState, spoofCheck, trust, pciAddress) + + // Emit stats metrics if available + if vf.Stats == nil { + c.logger.Debug("VF has no stats", "device", device, "vf", vf.ID) + vfCount++ + continue + } + + stats := vf.Stats + + ch <- prometheus.MustNewConstMetric(c.receivePackets, prometheus.CounterValue, float64(stats.RxPackets), device, vfID, pciAddress) + ch <- prometheus.MustNewConstMetric(c.transmitPackets, prometheus.CounterValue, float64(stats.TxPackets), device, vfID, pciAddress) + ch <- prometheus.MustNewConstMetric(c.receiveBytes, prometheus.CounterValue, float64(stats.RxBytes), device, vfID, pciAddress) + ch <- prometheus.MustNewConstMetric(c.transmitBytes, prometheus.CounterValue, float64(stats.TxBytes), device, vfID, pciAddress) + ch <- prometheus.MustNewConstMetric(c.broadcast, prometheus.CounterValue, float64(stats.Broadcast), device, vfID, pciAddress) + ch <- prometheus.MustNewConstMetric(c.multicast, prometheus.CounterValue, float64(stats.Multicast), device, vfID, pciAddress) + ch <- prometheus.MustNewConstMetric(c.receiveDropped, prometheus.CounterValue, float64(stats.RxDropped), device, vfID, pciAddress) + ch <- prometheus.MustNewConstMetric(c.transmitDropped, prometheus.CounterValue, float64(stats.TxDropped), device, vfID, pciAddress) + + vfCount++ + } + } + + if vfCount == 0 { + return ErrNoData + } + + return nil +} + +func vfLinkStateString(state rtnetlink.VFLinkState) string { + switch state { + case rtnetlink.VFLinkStateAuto: + return "auto" + case rtnetlink.VFLinkStateEnable: + return "enable" + case rtnetlink.VFLinkStateDisable: + return "disable" + default: + return "unknown" + } +} + +// resolveVFPCIAddress resolves the PCI BDF address of a VF by reading the +// sysfs virtfn symlink: /net//device/virtfn. +// Returns empty string if the symlink doesn't exist or can't be resolved. +func resolveVFPCIAddress(sysClassPath, pfDevice string, vfID uint32) string { + virtfnPath := filepath.Join(sysClassPath, "net", pfDevice, "device", fmt.Sprintf("virtfn%d", vfID)) + resolved, err := os.Readlink(virtfnPath) + if err != nil { + return "" + } + return filepath.Base(resolved) +} + +// vfMetrics holds parsed VF metrics for a single VF +type vfMetrics struct { + Device string + VFID uint32 + MAC string + Vlan uint32 + LinkState string + SpoofCheck bool + Trust bool + PCIAddress string + Stats *rtnetlink.VFStats +} + +// parseVFInfo extracts VF information from link messages for testing. +// sysClassPath is the path to the sysfs class directory used to resolve VF PCI addresses. +func parseVFInfo(links []rtnetlink.LinkMessage, filter *deviceFilter, logger *slog.Logger, sysClassPath string) []vfMetrics { + var result []vfMetrics + + for _, link := range links { + if link.Attributes == nil { + continue + } + + // Skip interfaces without VFs + if link.Attributes.NumVF == nil || *link.Attributes.NumVF == 0 { + continue + } + + device := link.Attributes.Name + + // Apply device filter + if filter.ignored(device) { + logger.Debug("Ignoring device", "device", device) + continue + } + + for _, vf := range link.Attributes.VFInfoList { + mac := "" + if vf.MAC != nil { + mac = vf.MAC.String() + } + + result = append(result, vfMetrics{ + Device: device, + VFID: vf.ID, + MAC: mac, + Vlan: vf.Vlan, + LinkState: vfLinkStateString(vf.LinkState), + SpoofCheck: vf.SpoofCheck, + Trust: vf.Trust, + PCIAddress: resolveVFPCIAddress(sysClassPath, device, vf.ID), + Stats: vf.Stats, + }) + } + } + + return result +} diff --git a/collector/netvf_linux_test.go b/collector/netvf_linux_test.go new file mode 100644 index 0000000000..d04e3d1b59 --- /dev/null +++ b/collector/netvf_linux_test.go @@ -0,0 +1,306 @@ +// Copyright The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !nonetvf + +package collector + +import ( + "io" + "log/slog" + "net" + "os" + "path/filepath" + "testing" + + "github.com/jsimonetti/rtnetlink/v2" +) + +func uint32Ptr(v uint32) *uint32 { + return &v +} + +var vfLinks = []rtnetlink.LinkMessage{ + { + // Interface without VFs + Attributes: &rtnetlink.LinkAttributes{ + Name: "eth0", + Stats64: &rtnetlink.LinkStats64{ + RXPackets: 1000, + TXPackets: 2000, + }, + }, + }, + { + // Interface with NumVF = 0 + Attributes: &rtnetlink.LinkAttributes{ + Name: "eth1", + NumVF: uint32Ptr(0), + }, + }, + { + // PF with 2 VFs + Attributes: &rtnetlink.LinkAttributes{ + Name: "enp3s0f0", + NumVF: uint32Ptr(2), + VFInfoList: []rtnetlink.VFInfo{ + { + ID: 0, + MAC: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x00}, + Vlan: 100, + LinkState: rtnetlink.VFLinkStateAuto, + SpoofCheck: true, + Trust: false, + Stats: &rtnetlink.VFStats{ + RxPackets: 1000, + TxPackets: 2000, + RxBytes: 100000, + TxBytes: 200000, + Broadcast: 10, + Multicast: 20, + RxDropped: 5, + TxDropped: 3, + }, + }, + { + ID: 1, + MAC: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0x01}, + Vlan: 200, + LinkState: rtnetlink.VFLinkStateEnable, + SpoofCheck: false, + Trust: true, + Stats: &rtnetlink.VFStats{ + RxPackets: 3000, + TxPackets: 4000, + RxBytes: 300000, + TxBytes: 400000, + Broadcast: 30, + Multicast: 40, + RxDropped: 7, + TxDropped: 9, + }, + }, + }, + }, + }, + { + // Another PF with 1 VF (no stats) + Attributes: &rtnetlink.LinkAttributes{ + Name: "enp3s0f1", + NumVF: uint32Ptr(1), + VFInfoList: []rtnetlink.VFInfo{ + { + ID: 0, + MAC: net.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}, + Vlan: 0, + LinkState: rtnetlink.VFLinkStateDisable, + SpoofCheck: true, + Trust: false, + Stats: nil, // No stats available + }, + }, + }, + }, + { + // Nil attributes (should be skipped) + Attributes: nil, + }, +} + +func TestParseVFInfo(t *testing.T) { + filter := newDeviceFilter("", "") + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + vfs := parseVFInfo(vfLinks, &filter, logger, "") + + // Should have 3 VFs total (2 from enp3s0f0, 1 from enp3s0f1) + if want, got := 3, len(vfs); want != got { + t.Errorf("want %d VFs, got %d", want, got) + } +} + +func TestParseVFInfoDeviceFilter(t *testing.T) { + // Exclude enp3s0f1 + filter := newDeviceFilter("enp3s0f1", "") + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + vfs := parseVFInfo(vfLinks, &filter, logger, "") + + // Should have 2 VFs (only from enp3s0f0) + if want, got := 2, len(vfs); want != got { + t.Errorf("want %d VFs, got %d", want, got) + } + + for _, vf := range vfs { + if vf.Device == "enp3s0f1" { + t.Error("enp3s0f1 should be filtered out") + } + } +} + +func TestParseVFInfoDeviceInclude(t *testing.T) { + // Only include enp3s0f1 + filter := newDeviceFilter("", "^enp3s0f1$") + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + vfs := parseVFInfo(vfLinks, &filter, logger, "") + + // Should have 1 VF (only from enp3s0f1) + if want, got := 1, len(vfs); want != got { + t.Errorf("want %d VFs, got %d", want, got) + } + + if len(vfs) > 0 && vfs[0].Device != "enp3s0f1" { + t.Errorf("want device enp3s0f1, got %s", vfs[0].Device) + } +} + +func TestParseVFInfoStats(t *testing.T) { + filter := newDeviceFilter("", "^enp3s0f0$") + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + vfs := parseVFInfo(vfLinks, &filter, logger, "") + + if len(vfs) != 2 { + t.Fatalf("expected 2 VFs, got %d", len(vfs)) + } + + // Check VF 0 stats + vf0 := vfs[0] + if vf0.VFID != 0 { + t.Errorf("expected VF ID 0, got %d", vf0.VFID) + } + if vf0.Stats == nil { + t.Fatal("expected stats for VF 0") + } + if want, got := uint64(1000), vf0.Stats.RxPackets; want != got { + t.Errorf("want RxPackets %d, got %d", want, got) + } + if want, got := uint64(200000), vf0.Stats.TxBytes; want != got { + t.Errorf("want TxBytes %d, got %d", want, got) + } + + // Check VF 1 stats + vf1 := vfs[1] + if vf1.VFID != 1 { + t.Errorf("expected VF ID 1, got %d", vf1.VFID) + } + if want, got := uint64(4000), vf1.Stats.TxPackets; want != got { + t.Errorf("want TxPackets %d, got %d", want, got) + } +} + +func TestParseVFInfoMetadata(t *testing.T) { + filter := newDeviceFilter("", "^enp3s0f0$") + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + vfs := parseVFInfo(vfLinks, &filter, logger, "") + + if len(vfs) != 2 { + t.Fatalf("expected 2 VFs, got %d", len(vfs)) + } + + // Check VF 0 metadata + vf0 := vfs[0] + if want, got := "aa:bb:cc:dd:ee:00", vf0.MAC; want != got { + t.Errorf("want MAC %s, got %s", want, got) + } + if want, got := uint32(100), vf0.Vlan; want != got { + t.Errorf("want VLAN %d, got %d", want, got) + } + if want, got := "auto", vf0.LinkState; want != got { + t.Errorf("want LinkState %s, got %s", want, got) + } + if !vf0.SpoofCheck { + t.Error("expected SpoofCheck to be true") + } + if vf0.Trust { + t.Error("expected Trust to be false") + } + + // Check VF 1 metadata + vf1 := vfs[1] + if want, got := "enable", vf1.LinkState; want != got { + t.Errorf("want LinkState %s, got %s", want, got) + } + if vf1.SpoofCheck { + t.Error("expected SpoofCheck to be false") + } + if !vf1.Trust { + t.Error("expected Trust to be true") + } +} + +func TestParseVFInfoNoStats(t *testing.T) { + filter := newDeviceFilter("", "^enp3s0f1$") + logger := slog.New(slog.NewTextHandler(io.Discard, nil)) + + vfs := parseVFInfo(vfLinks, &filter, logger, "") + + if len(vfs) != 1 { + t.Fatalf("expected 1 VF, got %d", len(vfs)) + } + + vf := vfs[0] + if vf.Stats != nil { + t.Error("expected Stats to be nil for this VF") + } + if want, got := "disable", vf.LinkState; want != got { + t.Errorf("want LinkState %s, got %s", want, got) + } +} + +func TestVFLinkStateString(t *testing.T) { + tests := []struct { + state rtnetlink.VFLinkState + expected string + }{ + {rtnetlink.VFLinkStateAuto, "auto"}, + {rtnetlink.VFLinkStateEnable, "enable"}, + {rtnetlink.VFLinkStateDisable, "disable"}, + {rtnetlink.VFLinkState(99), "unknown"}, + } + + for _, tt := range tests { + got := vfLinkStateString(tt.state) + if got != tt.expected { + t.Errorf("vfLinkStateString(%d) = %s, want %s", tt.state, got, tt.expected) + } + } +} + +func TestResolveVFPCIAddress(t *testing.T) { + // Create a fake sysfs tree: /net//device/virtfn0 -> ../0000:65:01.0 + tmp := t.TempDir() + deviceDir := filepath.Join(tmp, "net", "enp3s0f0", "device") + if err := os.MkdirAll(deviceDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.Symlink("../0000:65:01.0", filepath.Join(deviceDir, "virtfn0")); err != nil { + t.Fatal(err) + } + + got := resolveVFPCIAddress(tmp, "enp3s0f0", 0) + if want := "0000:65:01.0"; got != want { + t.Errorf("resolveVFPCIAddress() = %q, want %q", got, want) + } +} + +func TestResolveVFPCIAddressMissing(t *testing.T) { + tmp := t.TempDir() + + got := resolveVFPCIAddress(tmp, "enp3s0f0", 0) + if got != "" { + t.Errorf("resolveVFPCIAddress() = %q, want empty string for missing symlink", got) + } +} diff --git a/go.mod b/go.mod index 056a340030..b70e1edbf6 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/hashicorp/go-envparse v0.1.0 github.com/hodgesds/perf-utils v0.7.0 github.com/illumos/go-kstat v0.0.0-20210513183136-173c9b0a9973 - github.com/jsimonetti/rtnetlink/v2 v2.1.0 + github.com/jsimonetti/rtnetlink/v2 v2.2.0 github.com/lufia/iostat v1.2.1 github.com/mattn/go-xmlrpc v0.0.3 github.com/mdlayher/ethtool v0.5.0 diff --git a/go.sum b/go.sum index 1ef3b5a97a..20e615f5da 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,8 @@ 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cilium/ebpf v0.19.0 h1:Ro/rE64RmFBeA9FGjcTc+KmCeY6jXmryu6FfnzPRIao= -github.com/cilium/ebpf v0.19.0/go.mod h1:fLCgMo3l8tZmAdM3B2XqdFzXBpwkcSTroaVqN08OWVY= +github.com/cilium/ebpf v0.20.0 h1:atwWj9d3NffHyPZzVlx3hmw1on5CLe9eljR8VuHTwhM= +github.com/cilium/ebpf v0.20.0/go.mod h1:pzLjFymM+uZPLk/IXZUL63xdx5VXEo+enTzxkZXdycw= github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= @@ -42,8 +42,8 @@ github.com/illumos/go-kstat v0.0.0-20210513183136-173c9b0a9973/go.mod h1:PoK3ejP github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/jsimonetti/rtnetlink/v2 v2.1.0 h1:3sSPD0k+Qvia3wbv6kZXCN0Dlz6Swv7RHjvvonuOcKE= -github.com/jsimonetti/rtnetlink/v2 v2.1.0/go.mod h1:hPPUTE+ekH3HD+zCEGAGLxzFY9HrJCyD1aN7JJ3SHIY= +github.com/jsimonetti/rtnetlink/v2 v2.2.0 h1:/KfZ310gOAFrXXol5VwnFEt+ucldD/0dsSRZwpHCP9w= +github.com/jsimonetti/rtnetlink/v2 v2.2.0/go.mod h1:lbjDHxC+5RJ08lzPeA90Ls2pEoId3F08MoEMlhfHxeI= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=