Skip to content
Open
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
174 changes: 141 additions & 33 deletions collector/network_route_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@
package collector

import (
"bufio"
"fmt"
"golang.org/x/sys/unix"
"net"
"os"
"path/filepath"
"strconv"
"strings"

"golang.org/x/sys/unix"

"github.com/go-kit/log"
"github.com/jsimonetti/rtnetlink"
Expand All @@ -43,7 +48,7 @@ func NewNetworkRouteCollector(logger log.Logger) (Collector, error) {

routeInfoDesc := prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "route_info"),
"network routing table information", []string{"device", "src", "dest", "gw", "priority", "proto", "weight"}, nil,
"network routing table information", []string{"device", "src", "dest", "gw", "priority", "proto", "weight", "table", "type"}, nil,
)
routesDesc := prometheus.NewDesc(
prometheus.BuildFQName(namespace, subsystem, "routes"),
Expand Down Expand Up @@ -76,49 +81,67 @@ func (n networkRouteCollector) Update(ch chan<- prometheus.Metric) error {
return fmt.Errorf("couldn't get routes: %w", err)
}

routeTableIDName, err := routeTableIDToString()
if err != nil {
return fmt.Errorf("couldn't get route table names: %w", err)
}

for _, route := range routes {
if route.Type != unix.RTA_DST {
continue
}
if len(route.Attributes.Multipath) != 0 {
for _, nextHop := range route.Attributes.Multipath {
ifName := ""
var ifName, weight, gateway string

switch route.Type {

case unix.RTN_BLACKHOLE, unix.RTN_UNREACHABLE:
labels := []string{
ifName, // if
networkRouteIPToString(route.Attributes.Src), // src
networkRouteIPWithPrefixToString(route.Attributes.Dst, route.DstLength), // dest
gateway, // gw
strconv.FormatUint(uint64(route.Attributes.Priority), 10), // priority(metrics)
networkRouteProtocolToString(route.Protocol), // proto
weight, // weight
routeTableNameFromID(routeTableIDName, int(route.Attributes.Table)), // table
routeTypeToString(route.Type), // type
}
ch <- prometheus.MustNewConstMetric(n.routeInfoDesc, prometheus.GaugeValue, 1, labels...)

case unix.RTN_UNICAST:
// Unicast route with multiple next-hops
if len(route.Attributes.Multipath) > 0 {
for _, nextHop := range route.Attributes.Multipath {
ifName = ""
for _, link := range links {
if link.Index == nextHop.Hop.IfIndex {
ifName = link.Attributes.Name
break
}
}
weight = strconv.Itoa(int(nextHop.Hop.Hops) + 1)
gateway = networkRouteIPToString(nextHop.Gateway)
}
}

// Unicast routes with a single next-hop
if len(route.Attributes.Multipath) == 0 {
for _, link := range links {
if link.Index == nextHop.Hop.IfIndex {
if link.Index == route.Attributes.OutIface {
ifName = link.Attributes.Name
break
}
}

labels := []string{
ifName, // if
networkRouteIPToString(route.Attributes.Src), // src
networkRouteIPWithPrefixToString(route.Attributes.Dst, route.DstLength), // dest
networkRouteIPToString(nextHop.Gateway), // gw
strconv.FormatUint(uint64(route.Attributes.Priority), 10), // priority(metrics)
networkRouteProtocolToString(route.Protocol), // proto
strconv.Itoa(int(nextHop.Hop.Hops) + 1), // weight
}
ch <- prometheus.MustNewConstMetric(n.routeInfoDesc, prometheus.GaugeValue, 1, labels...)
deviceRoutes[ifName]++
}
} else {
ifName := ""
for _, link := range links {
if link.Index == route.Attributes.OutIface {
ifName = link.Attributes.Name
break
}
gateway = networkRouteIPToString(route.Attributes.Gateway)
}

labels := []string{
ifName, // if
networkRouteIPToString(route.Attributes.Src), // src
networkRouteIPWithPrefixToString(route.Attributes.Dst, route.DstLength), // dest
networkRouteIPToString(route.Attributes.Gateway), // gw
strconv.FormatUint(uint64(route.Attributes.Priority), 10), // priority(metrics)
networkRouteProtocolToString(route.Protocol), // proto
"", // weight
gateway, // gw
strconv.FormatUint(uint64(route.Attributes.Priority), 10), // priority(metrics)
networkRouteProtocolToString(route.Protocol), // proto
weight, // weight
routeTableNameFromID(routeTableIDName, int(route.Attributes.Table)), // table
routeTypeToString(route.Type), // type
}
ch <- prometheus.MustNewConstMetric(n.routeInfoDesc, prometheus.GaugeValue, 1, labels...)
deviceRoutes[ifName]++
Expand Down Expand Up @@ -202,3 +225,88 @@ func networkRouteProtocolToString(protocol uint8) string {
}
return "unknown"
}

func routeTableIDToString() (map[int]string, error) {
routeTableName := make(map[int]string)
// Paths are optional and do not have to exist
rt_tablesConfigFile := "/etc/iproute2/rt_tables"
Comment thread
derrickhayashi marked this conversation as resolved.
rt_tablesConfigDir := "/etc/iproute2/rt_tables.d"
Comment on lines +232 to +233

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These paths should be set as flags to make them configurable.

This allows users to change the paths, as well as we can then setup fixtures for the end-to-end tests.

--collector.network_route.config.file
--collector.network_route.config.dir


rt_tableConfigPaths := make([]string, 0)

fileInfo, err := os.Stat(rt_tablesConfigFile)
if err == nil {
if !fileInfo.IsDir() {
rt_tableConfigPaths = append(rt_tableConfigPaths, rt_tablesConfigFile)
}
}

files, err := os.ReadDir(rt_tablesConfigDir)
if err == nil {
for _, file := range files {
// iproute2 processes all files ending in '.conf'
if filepath.Ext(file.Name()) == ".conf" {
if !file.IsDir() {
rt_tableConfigPaths = append(rt_tableConfigPaths, filepath.Join(rt_tablesConfigDir, file.Name()))
}
}
}
}

for _, configPath := range rt_tableConfigPaths {
f, err := os.Open(configPath)
if err != nil {
return nil, fmt.Errorf("could not open %s", configPath)
}
defer f.Close()

scanner := bufio.NewScanner(f)

for scanner.Scan() {
if strings.HasPrefix(scanner.Text(), "#") {
continue
}

// Split each line into <table id, table name> pairs
// Try tab as a delimiter first, then space
config := strings.Split(scanner.Text(), "\t")
if len(config) != 2 {
config = strings.Split(scanner.Text(), " ")
}
if len(config) == 2 {
tableID, err := strconv.Atoi(config[0])
if err != nil {
return nil, fmt.Errorf("invalid rt_tables config in %s", configPath)
}
tableName := config[1]
routeTableName[tableID] = tableName
}
}
}

return routeTableName, nil
}

func routeTableNameFromID(routeTableIDName map[int]string, routeTableID int) string {
// If no table name is defined, simply use the ID
name, ok := routeTableIDName[routeTableID]
if ok {
return name
} else {
return strconv.Itoa(routeTableID)
}
}

func routeTypeToString(routeType uint8) string {
// Subset of possible types defined on https://man7.org/linux/man-pages/man7/rtnetlink.7.html
switch routeType {
case unix.RTN_BLACKHOLE:
return "blackhole"
case unix.RTN_UNREACHABLE:
return "unreachable"
case unix.RTN_UNICAST:
return "unicast"
}

return "unknown"
}