152 lines
3.6 KiB
Go
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
|
|
}
|