Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## master / unreleased

* [FEATURE] netlink: Add node_network_master_info and node_network_vrf_info metrics #3765

## 1.12.1 / 2026-07-14

* [BUGFIX] perf: Fix perf profiler flag handling #3731
Expand Down
65 changes: 65 additions & 0 deletions collector/netclass_rtnl_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ import (
"fmt"
"io/fs"
"path/filepath"
"strconv"
"strings"

"github.com/alecthomas/kingpin/v2"
"github.com/jsimonetti/rtnetlink/v2"
"github.com/mdlayher/ethtool"
"github.com/mdlayher/netlink"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/procfs/sysfs"
"golang.org/x/sys/unix"
)

var (
Expand All @@ -35,6 +38,19 @@ var (
"unknown", "notpresent", "down", "lowerlayerdown", "testing",
"dormant", "up",
}

netclassMasterDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "network", "master_info"),
"Direct master device of <iface> and the master's kind (bridge, bond, vrf), value is always 1.",
[]string{"device", "master", "master_kind"},
nil,
)
netclassVRFDesc = prometheus.NewDesc(
prometheus.BuildFQName(namespace, "network", "vrf_info"),
"Routing table ID of a VRF device, value is always 1.",
[]string{"device", "table"},
nil,
)
)

func (c *netClassCollector) netClassRTNLUpdate(ch chan<- prometheus.Metric) error {
Expand Down Expand Up @@ -64,8 +80,13 @@ func (c *netClassCollector) netClassRTNLUpdate(ch chan<- prometheus.Metric) erro
return fmt.Errorf("could not get net class info: %w", err)
}

// ifNames maps every interface index to its name, so that master devices
// can be resolved by name. Ignored devices are kept, because an ignored
// device may be the master of a device that is not ignored.
ifNames := make(map[uint32]string, len(lMsgs))
relevantLinks := make([]rtnetlink.LinkMessage, 0, len(lMsgs))
for _, msg := range lMsgs {
ifNames[msg.Index] = msg.Attributes.Name
if !c.ignoredDevicesPattern.MatchString(msg.Attributes.Name) {
relevantLinks = append(relevantLinks, msg)
}
Expand Down Expand Up @@ -128,6 +149,32 @@ func (c *netClassCollector) netClassRTNLUpdate(ch chan<- prometheus.Metric) erro
ch <- prometheus.MustNewConstMetric(altnameDesc, prometheus.GaugeValue, infoValue, strings.ToValidUTF8(altname, "\uFFFD"), msg.Attributes.Name)
}
}

// Only the direct master is reported. An interface enslaved to a
// bridge that is itself enslaved to a VRF has master_kind="bridge".
if msg.Attributes.Master != nil {
if master, ok := ifNames[*msg.Attributes.Master]; ok {
// IFLA_INFO_SLAVE_KIND holds the master's kind as
// reported by the kernel. It may be absent, in which
// case the master is still reported with an empty kind.
masterKind := ""
if msg.Attributes.Info != nil {
masterKind = msg.Attributes.Info.SlaveKind
}
ch <- prometheus.MustNewConstMetric(netclassMasterDesc, prometheus.GaugeValue, infoValue, msg.Attributes.Name, master, masterKind)
}
}

if msg.Attributes.Info != nil && msg.Attributes.Info.Kind == "vrf" {
// rtnetlink has no vrf driver, so IFLA_INFO_DATA is left as
// raw nested attributes.
if data, ok := msg.Attributes.Info.Data.(*rtnetlink.LinkData); ok {
if table, ok := vrfTable(data.Data); ok {
ch <- prometheus.MustNewConstMetric(netclassVRFDesc, prometheus.GaugeValue, infoValue, msg.Attributes.Name, strconv.FormatUint(uint64(table), 10))
}
}
}

pushMetric(ch, c.getFieldDesc("address_assign_type"), ifaceInfo.AddrAssignType, prometheus.GaugeValue, msg.Attributes.Name)
pushMetric(ch, c.getFieldDesc("carrier"), msg.Attributes.Carrier, prometheus.GaugeValue, msg.Attributes.Name)
pushMetric(ch, c.getFieldDesc("carrier_changes_total"), msg.Attributes.CarrierChanges, prometheus.CounterValue, msg.Attributes.Name)
Expand Down Expand Up @@ -220,6 +267,24 @@ func (c *netClassCollector) getLinkModes() ([]*ethtool.LinkMode, error) {
return lms, err
}

// vrfTable decodes the routing table ID from the raw IFLA_INFO_DATA payload of
// a vrf link. It reports false if the attribute is absent or malformed.
func vrfTable(data []byte) (uint32, bool) {
// The attribute decoder defaults to native byte order, which is what
// rtnetlink uses.
ad, err := netlink.NewAttributeDecoder(data)
if err != nil {
return 0, false
}
for ad.Next() {
if ad.Type() == unix.IFLA_VRF_TABLE {
table := ad.Uint32()
return table, ad.Err() == nil
}
}
return 0, false
}

// getSysfsAttributes reads attributes that are absent from netlink but provided
// by sysfs.
func getSysfsAttributes(links []rtnetlink.LinkMessage) (sysfs.NetClass, error) {
Expand Down
71 changes: 71 additions & 0 deletions collector/netclass_rtnl_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 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 !nonetclass && linux

package collector

import "testing"

func TestVRFTable(t *testing.T) {
tests := []struct {
name string
data []byte
want uint32
ok bool
}{
{
// IFLA_INFO_DATA of a vrf device using table 100, as
// returned by RTM_GETLINK.
name: "table 100",
data: []byte{0x08, 0x00, 0x01, 0x00, 0x64, 0x00, 0x00, 0x00},
want: 100,
ok: true,
},
{
// Table IDs above 255 exercise the multi-byte decode.
name: "table 1000",
data: []byte{0x08, 0x00, 0x01, 0x00, 0xe8, 0x03, 0x00, 0x00},
want: 1000,
ok: true,
},
{
name: "no attributes",
data: nil,
ok: false,
},
{
// IFLA_VRF_TABLE absent, only an unrelated attribute.
name: "missing table attribute",
data: []byte{0x08, 0x00, 0x02, 0x00, 0x64, 0x00, 0x00, 0x00},
ok: false,
},
{
name: "truncated attribute",
data: []byte{0x08, 0x00, 0x01, 0x00, 0x64},
ok: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, ok := vrfTable(test.data)
if ok != test.ok {
t.Fatalf("vrfTable() ok = %v, want %v", ok, test.ok)
}
if ok && got != test.want {
t.Errorf("vrfTable() = %d, want %d", got, test.want)
}
})
}
}