Skip to content

Commit c029467

Browse files
committed
collector/netclass: add master and VRF info metrics
The netlink netclass path already receives IFLA_MASTER and IFLA_LINKINFO in the RTM_GETLINK dump it makes, but exposed neither the master device nor its kind, so there is no way to tell which VRF an interface is in. Add node_network_master_info, labelled with the direct master and the master's kind as reported by IFLA_INFO_SLAVE_KIND, covering VRF membership as well as bridge ports and bond members. Also add node_network_vrf_info to publish the routing table ID of VRF devices. rtnetlink has no vrf driver, so IFLA_VRF_TABLE is decoded from the raw IFLA_INFO_DATA payload. Signed-off-by: Brandon Ewing <brandon.ewing@warningg.com>
1 parent b401dcf commit c029467

2 files changed

Lines changed: 140 additions & 0 deletions

File tree

collector/netclass_rtnl_linux.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ import (
2020
"fmt"
2121
"io/fs"
2222
"path/filepath"
23+
"strconv"
2324
"strings"
2425

2526
"github.com/alecthomas/kingpin/v2"
2627
"github.com/jsimonetti/rtnetlink/v2"
2728
"github.com/mdlayher/ethtool"
29+
"github.com/mdlayher/netlink"
2830
"github.com/prometheus/client_golang/prometheus"
2931
"github.com/prometheus/procfs/sysfs"
32+
"golang.org/x/sys/unix"
3033
)
3134

3235
var (
@@ -35,6 +38,19 @@ var (
3538
"unknown", "notpresent", "down", "lowerlayerdown", "testing",
3639
"dormant", "up",
3740
}
41+
42+
netclassMasterDesc = prometheus.NewDesc(
43+
prometheus.BuildFQName(namespace, "network", "master_info"),
44+
"Direct master device of <iface> and the master's kind (bridge, bond, vrf), value is always 1.",
45+
[]string{"device", "master", "master_kind"},
46+
nil,
47+
)
48+
netclassVRFDesc = prometheus.NewDesc(
49+
prometheus.BuildFQName(namespace, "network", "vrf_info"),
50+
"Routing table ID of a VRF device, value is always 1.",
51+
[]string{"device", "table"},
52+
nil,
53+
)
3854
)
3955

4056
func (c *netClassCollector) netClassRTNLUpdate(ch chan<- prometheus.Metric) error {
@@ -64,6 +80,15 @@ func (c *netClassCollector) netClassRTNLUpdate(ch chan<- prometheus.Metric) erro
6480
return fmt.Errorf("could not get net class info: %w", err)
6581
}
6682

83+
// Map every interface index to its name so that master devices can be
84+
// resolved by name. This is built from all links, not just the relevant
85+
// ones, because a master may itself be an ignored device while the
86+
// interfaces enslaved to it are not.
87+
ifNames := make(map[uint32]string, len(lMsgs))
88+
for _, msg := range lMsgs {
89+
ifNames[msg.Index] = msg.Attributes.Name
90+
}
91+
6792
relevantLinks := make([]rtnetlink.LinkMessage, 0, len(lMsgs))
6893
for _, msg := range lMsgs {
6994
if !c.ignoredDevicesPattern.MatchString(msg.Attributes.Name) {
@@ -128,6 +153,32 @@ func (c *netClassCollector) netClassRTNLUpdate(ch chan<- prometheus.Metric) erro
128153
ch <- prometheus.MustNewConstMetric(altnameDesc, prometheus.GaugeValue, infoValue, strings.ToValidUTF8(altname, "\uFFFD"), msg.Attributes.Name)
129154
}
130155
}
156+
157+
// Only the direct master is reported. An interface enslaved to a
158+
// bridge that is itself enslaved to a VRF has master_kind="bridge".
159+
if msg.Attributes.Master != nil {
160+
if master, ok := ifNames[*msg.Attributes.Master]; ok {
161+
// IFLA_INFO_SLAVE_KIND holds the master's kind as
162+
// reported by the kernel. It may be absent, in which
163+
// case the master is still reported with an empty kind.
164+
masterKind := ""
165+
if msg.Attributes.Info != nil {
166+
masterKind = msg.Attributes.Info.SlaveKind
167+
}
168+
ch <- prometheus.MustNewConstMetric(netclassMasterDesc, prometheus.GaugeValue, infoValue, msg.Attributes.Name, master, masterKind)
169+
}
170+
}
171+
172+
if msg.Attributes.Info != nil && msg.Attributes.Info.Kind == "vrf" {
173+
// rtnetlink has no vrf driver, so IFLA_INFO_DATA is left as
174+
// raw nested attributes.
175+
if data, ok := msg.Attributes.Info.Data.(*rtnetlink.LinkData); ok {
176+
if table, ok := vrfTable(data.Data); ok {
177+
ch <- prometheus.MustNewConstMetric(netclassVRFDesc, prometheus.GaugeValue, infoValue, msg.Attributes.Name, strconv.FormatUint(uint64(table), 10))
178+
}
179+
}
180+
}
181+
131182
pushMetric(ch, c.getFieldDesc("address_assign_type"), ifaceInfo.AddrAssignType, prometheus.GaugeValue, msg.Attributes.Name)
132183
pushMetric(ch, c.getFieldDesc("carrier"), msg.Attributes.Carrier, prometheus.GaugeValue, msg.Attributes.Name)
133184
pushMetric(ch, c.getFieldDesc("carrier_changes_total"), msg.Attributes.CarrierChanges, prometheus.CounterValue, msg.Attributes.Name)
@@ -220,6 +271,24 @@ func (c *netClassCollector) getLinkModes() ([]*ethtool.LinkMode, error) {
220271
return lms, err
221272
}
222273

274+
// vrfTable decodes the routing table ID from the raw IFLA_INFO_DATA payload of
275+
// a vrf link. It reports false if the attribute is absent or malformed.
276+
func vrfTable(data []byte) (uint32, bool) {
277+
// The attribute decoder defaults to native byte order, which is what
278+
// rtnetlink uses.
279+
ad, err := netlink.NewAttributeDecoder(data)
280+
if err != nil {
281+
return 0, false
282+
}
283+
for ad.Next() {
284+
if ad.Type() == unix.IFLA_VRF_TABLE {
285+
table := ad.Uint32()
286+
return table, ad.Err() == nil
287+
}
288+
}
289+
return 0, false
290+
}
291+
223292
// getSysfsAttributes reads attributes that are absent from netlink but provided
224293
// by sysfs.
225294
func getSysfsAttributes(links []rtnetlink.LinkMessage) (sysfs.NetClass, error) {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2026 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
//go:build !nonetclass && linux
15+
16+
package collector
17+
18+
import "testing"
19+
20+
func TestVRFTable(t *testing.T) {
21+
tests := []struct {
22+
name string
23+
data []byte
24+
want uint32
25+
ok bool
26+
}{
27+
{
28+
// IFLA_INFO_DATA of a vrf device using table 100, as
29+
// returned by RTM_GETLINK.
30+
name: "table 100",
31+
data: []byte{0x08, 0x00, 0x01, 0x00, 0x64, 0x00, 0x00, 0x00},
32+
want: 100,
33+
ok: true,
34+
},
35+
{
36+
// Table IDs above 255 exercise the multi-byte decode.
37+
name: "table 1000",
38+
data: []byte{0x08, 0x00, 0x01, 0x00, 0xe8, 0x03, 0x00, 0x00},
39+
want: 1000,
40+
ok: true,
41+
},
42+
{
43+
name: "no attributes",
44+
data: nil,
45+
ok: false,
46+
},
47+
{
48+
// IFLA_VRF_TABLE absent, only an unrelated attribute.
49+
name: "missing table attribute",
50+
data: []byte{0x08, 0x00, 0x02, 0x00, 0x64, 0x00, 0x00, 0x00},
51+
ok: false,
52+
},
53+
{
54+
name: "truncated attribute",
55+
data: []byte{0x08, 0x00, 0x01, 0x00, 0x64},
56+
ok: false,
57+
},
58+
}
59+
60+
for _, test := range tests {
61+
t.Run(test.name, func(t *testing.T) {
62+
got, ok := vrfTable(test.data)
63+
if ok != test.ok {
64+
t.Fatalf("vrfTable() ok = %v, want %v", ok, test.ok)
65+
}
66+
if ok && got != test.want {
67+
t.Errorf("vrfTable() = %d, want %d", got, test.want)
68+
}
69+
})
70+
}
71+
}

0 commit comments

Comments
 (0)