//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 }