Files
statuspanel/internal/probe/route_linux.go
2026-07-05 14:26:03 -03:00

152 lines
3.6 KiB
Go

//go:build linux
package probe
import (
"bufio"
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"net"
"os"
"strconv"
"strings"
)
const routePath = "/proc/net/route"
const (
routeFlagUp = 0x1
routeFlagGateway = 0x2
routeFlagHost = 0x4
)
type linuxRoute struct {
iface string
destination net.IP
gateway net.IP
flags int64
mask net.IP
}
// GatewayForInterface returns the default IPv4 gateway for an interface.
func GatewayForInterface(ctx context.Context, iface string) (net.IP, error) {
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("checking context before reading routes: %w", err)
}
data, err := os.ReadFile(routePath)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", routePath, err)
}
if err := ctx.Err(); err != nil {
return nil, fmt.Errorf("checking context after reading routes: %w", err)
}
ip, err := parseLinuxDefaultGateway(data, iface)
if err != nil {
return nil, err
}
return ip, nil
}
func parseLinuxDefaultGateway(data []byte, iface string) (net.IP, error) {
routes, err := parseLinuxRoutes(data, iface)
if err != nil {
return nil, err
}
hasDefaultRoute := false
for _, route := range routes {
if !route.isDefault() {
continue
}
hasDefaultRoute = true
if route.hasFlag(routeFlagUp) && route.hasFlag(routeFlagGateway) && !route.gateway.Equal(net.IPv4zero) {
return route.gateway, nil
}
}
if hasDefaultRoute {
for _, route := range routes {
if route.isPointToPointPeer() {
return route.destination, nil
}
}
}
return nil, errors.New("default gateway not found")
}
func parseLinuxRoutes(data []byte, iface string) ([]linuxRoute, error) {
var routes []linuxRoute
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < 8 || fields[0] != iface {
continue
}
route, err := parseLinuxRoute(fields)
if err != nil {
return nil, err
}
routes = append(routes, route)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("scanning %s contents: %w", routePath, err)
}
return routes, nil
}
func parseLinuxRoute(fields []string) (linuxRoute, error) {
destination, err := littleEndianHexIPv4(fields[1])
if err != nil {
return linuxRoute{}, fmt.Errorf("parsing route destination for %s: %w", fields[0], err)
}
gateway, err := littleEndianHexIPv4(fields[2])
if err != nil {
return linuxRoute{}, fmt.Errorf("parsing route gateway for %s: %w", fields[0], err)
}
flags, err := strconv.ParseInt(fields[3], 16, 64)
if err != nil {
return linuxRoute{}, fmt.Errorf("parsing route flags for %s: %w", fields[0], err)
}
mask, err := littleEndianHexIPv4(fields[7])
if err != nil {
return linuxRoute{}, fmt.Errorf("parsing route mask for %s: %w", fields[0], err)
}
return linuxRoute{
iface: fields[0],
destination: destination,
gateway: gateway,
flags: flags,
mask: mask,
}, nil
}
func (r linuxRoute) isDefault() bool {
return r.destination.Equal(net.IPv4zero) && r.mask.Equal(net.IPv4zero)
}
func (r linuxRoute) isPointToPointPeer() bool {
return r.hasFlag(routeFlagUp) &&
r.hasFlag(routeFlagHost) &&
r.gateway.Equal(net.IPv4zero) &&
!r.destination.Equal(net.IPv4zero)
}
func (r linuxRoute) hasFlag(flag int64) bool {
return r.flags&flag != 0
}
func littleEndianHexIPv4(value string) (net.IP, error) {
decoded, err := hex.DecodeString(value)
if err != nil {
return nil, err
}
if len(decoded) != net.IPv4len {
return nil, fmt.Errorf("expected 4 bytes, got %d", len(decoded))
}
return net.IPv4(decoded[3], decoded[2], decoded[1], decoded[0]), nil
}