primeiro commit
This commit is contained in:
52
internal/probe/dns.go
Normal file
52
internal/probe/dns.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DNSResult contains the outcome of one DNS lookup.
|
||||
type DNSResult struct {
|
||||
Reachable bool
|
||||
Duration time.Duration
|
||||
IPs []net.IP
|
||||
Err error
|
||||
}
|
||||
|
||||
// Resolve checks whether a domain can be resolved against a specific DNS server.
|
||||
func Resolve(ctx context.Context, server string, domain string, timeout time.Duration) DNSResult {
|
||||
start := time.Now()
|
||||
lookupCtx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
dialer := net.Dialer{Timeout: timeout}
|
||||
resolver := net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network string, address string) (net.Conn, error) {
|
||||
conn, err := dialer.DialContext(ctx, network, dnsServerAddress(server))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing dns server %s: %w", server, err)
|
||||
}
|
||||
return conn, nil
|
||||
},
|
||||
}
|
||||
ips, err := resolver.LookupIP(lookupCtx, "ip", domain)
|
||||
duration := time.Since(start)
|
||||
if err != nil {
|
||||
return DNSResult{Duration: duration, Err: fmt.Errorf("resolving %s with %s: %w", domain, server, err)}
|
||||
}
|
||||
return DNSResult{Reachable: len(ips) > 0, Duration: duration, IPs: ips}
|
||||
}
|
||||
|
||||
func dnsServerAddress(server string) string {
|
||||
if _, err := netip.ParseAddrPort(server); err == nil {
|
||||
return server
|
||||
}
|
||||
if addr, err := netip.ParseAddr(server); err == nil {
|
||||
return net.JoinHostPort(addr.String(), "53")
|
||||
}
|
||||
return net.JoinHostPort(server, "53")
|
||||
}
|
||||
2
internal/probe/doc.go
Normal file
2
internal/probe/doc.go
Normal file
@@ -0,0 +1,2 @@
|
||||
// Package probe provides network, DNS, route, and interface probes for statuspanel.
|
||||
package probe
|
||||
23
internal/probe/humanize.go
Normal file
23
internal/probe/humanize.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package probe
|
||||
|
||||
import "fmt"
|
||||
|
||||
// HumanizeBitsPerSecond formats a bit rate as kilobit, megabit, or gigabit.
|
||||
func HumanizeBitsPerSecond(bitsPerSecond float64) string {
|
||||
const (
|
||||
kilobit = 1000
|
||||
megabit = 1000 * kilobit
|
||||
gigabit = 1000 * megabit
|
||||
)
|
||||
|
||||
switch {
|
||||
case bitsPerSecond >= gigabit:
|
||||
return fmt.Sprintf("%.2f Gbit/s", bitsPerSecond/gigabit)
|
||||
case bitsPerSecond >= megabit:
|
||||
return fmt.Sprintf("%.2f Mbit/s", bitsPerSecond/megabit)
|
||||
case bitsPerSecond >= kilobit:
|
||||
return fmt.Sprintf("%.2f kbit/s", bitsPerSecond/kilobit)
|
||||
default:
|
||||
return fmt.Sprintf("%.0f bit/s", bitsPerSecond)
|
||||
}
|
||||
}
|
||||
71
internal/probe/interface.go
Normal file
71
internal/probe/interface.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const netDevPath = "/proc/net/dev"
|
||||
|
||||
// InterfaceStats contains received and transmitted byte counters.
|
||||
type InterfaceStats struct {
|
||||
Name string
|
||||
RXBytes uint64
|
||||
TXBytes uint64
|
||||
}
|
||||
|
||||
// ReadInterfaceStats reads byte counters for a network interface.
|
||||
func ReadInterfaceStats(ctx context.Context, name string) (InterfaceStats, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return InterfaceStats{}, fmt.Errorf("checking context before reading interface stats: %w", err)
|
||||
}
|
||||
data, err := os.ReadFile(netDevPath)
|
||||
if err != nil {
|
||||
return InterfaceStats{}, fmt.Errorf("reading %s: %w", netDevPath, err)
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return InterfaceStats{}, fmt.Errorf("checking context after reading interface stats: %w", err)
|
||||
}
|
||||
stats, err := parseNetDev(data, name)
|
||||
if err != nil {
|
||||
return InterfaceStats{}, err
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func parseNetDev(data []byte, name string) (InterfaceStats, error) {
|
||||
scanner := bufio.NewScanner(bytes.NewReader(data))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if !strings.Contains(line, ":") {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if strings.TrimSpace(parts[0]) != name {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(parts[1])
|
||||
if len(fields) < 16 {
|
||||
return InterfaceStats{}, fmt.Errorf("parsing interface %s stats: expected at least 16 fields, got %d", name, len(fields))
|
||||
}
|
||||
rxBytes, err := strconv.ParseUint(fields[0], 10, 64)
|
||||
if err != nil {
|
||||
return InterfaceStats{}, fmt.Errorf("parsing interface %s rx bytes: %w", name, err)
|
||||
}
|
||||
txBytes, err := strconv.ParseUint(fields[8], 10, 64)
|
||||
if err != nil {
|
||||
return InterfaceStats{}, fmt.Errorf("parsing interface %s tx bytes: %w", name, err)
|
||||
}
|
||||
return InterfaceStats{Name: name, RXBytes: rxBytes, TXBytes: txBytes}, nil
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return InterfaceStats{}, fmt.Errorf("scanning %s contents: %w", netDevPath, err)
|
||||
}
|
||||
return InterfaceStats{}, errors.New("interface not found")
|
||||
}
|
||||
153
internal/probe/ping.go
Normal file
153
internal/probe/ping.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
icmpEchoReply = 0
|
||||
icmpEcho = 8
|
||||
)
|
||||
|
||||
// PingResult contains the outcome of one ICMP echo request.
|
||||
type PingResult struct {
|
||||
Reachable bool
|
||||
RTT time.Duration
|
||||
Err error
|
||||
}
|
||||
|
||||
// Ping sends one ICMP echo request to an IPv4 host.
|
||||
func Ping(ctx context.Context, host string, timeout time.Duration) (result PingResult) {
|
||||
start := time.Now()
|
||||
ip, err := resolveIPv4(ctx, host)
|
||||
if err != nil {
|
||||
return PingResult{Err: err}
|
||||
}
|
||||
|
||||
conn, err := net.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||
if err != nil {
|
||||
return PingResult{Err: fmt.Errorf("opening icmp socket: %w", err)}
|
||||
}
|
||||
defer func() {
|
||||
if closeErr := conn.Close(); closeErr != nil && result.Err == nil {
|
||||
result = PingResult{Err: fmt.Errorf("closing icmp socket: %w", closeErr)}
|
||||
}
|
||||
}()
|
||||
|
||||
deadline := time.Now().Add(timeout)
|
||||
if ctxDeadline, ok := ctx.Deadline(); ok && ctxDeadline.Before(deadline) {
|
||||
deadline = ctxDeadline
|
||||
}
|
||||
if err := conn.SetDeadline(deadline); err != nil {
|
||||
return PingResult{Err: fmt.Errorf("setting icmp deadline: %w", err)}
|
||||
}
|
||||
|
||||
id, seq, err := echoIdentifiers()
|
||||
if err != nil {
|
||||
return PingResult{Err: err}
|
||||
}
|
||||
packet := echoPacket(id, seq)
|
||||
if _, err := conn.WriteTo(packet, &net.IPAddr{IP: ip}); err != nil {
|
||||
return PingResult{Err: fmt.Errorf("sending icmp echo to %s: %w", host, err)}
|
||||
}
|
||||
|
||||
buffer := make([]byte, 1500)
|
||||
for {
|
||||
n, addr, err := conn.ReadFrom(buffer)
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return PingResult{Err: fmt.Errorf("ping canceled: %w", ctxErr)}
|
||||
}
|
||||
return PingResult{Err: fmt.Errorf("reading icmp reply from %s: %w", host, err)}
|
||||
}
|
||||
if !sameIP(addr, ip) {
|
||||
continue
|
||||
}
|
||||
if echoReplyMatches(buffer[:n], id, seq) {
|
||||
return PingResult{Reachable: true, RTT: time.Since(start)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resolveIPv4(ctx context.Context, host string) (net.IP, error) {
|
||||
if parsed := net.ParseIP(host); parsed != nil {
|
||||
if ipv4 := parsed.To4(); ipv4 != nil {
|
||||
return ipv4, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%s is not an IPv4 address", host)
|
||||
}
|
||||
addrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolving ping host %s: %w", host, err)
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ipv4 := addr.IP.To4(); ipv4 != nil {
|
||||
return ipv4, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("resolving ping host %s: no IPv4 address found", host)
|
||||
}
|
||||
|
||||
func echoIdentifiers() (uint16, uint16, error) {
|
||||
var data [4]byte
|
||||
if _, err := rand.Read(data[:]); err != nil {
|
||||
return 0, 0, fmt.Errorf("generating icmp identifiers: %w", err)
|
||||
}
|
||||
return binary.BigEndian.Uint16(data[0:2]), binary.BigEndian.Uint16(data[2:4]), nil
|
||||
}
|
||||
|
||||
func echoPacket(id uint16, seq uint16) []byte {
|
||||
payload := []byte("statuspanel")
|
||||
packet := make([]byte, 8+len(payload))
|
||||
packet[0] = icmpEcho
|
||||
binary.BigEndian.PutUint16(packet[4:6], id)
|
||||
binary.BigEndian.PutUint16(packet[6:8], seq)
|
||||
copy(packet[8:], payload)
|
||||
checksum := icmpChecksum(packet)
|
||||
binary.BigEndian.PutUint16(packet[2:4], checksum)
|
||||
return packet
|
||||
}
|
||||
|
||||
func echoReplyMatches(packet []byte, id uint16, seq uint16) bool {
|
||||
if len(packet) < 8 {
|
||||
return false
|
||||
}
|
||||
if packet[0] == 69 && len(packet) >= 28 {
|
||||
packet = packet[20:]
|
||||
}
|
||||
return len(packet) >= 8 &&
|
||||
packet[0] == icmpEchoReply &&
|
||||
binary.BigEndian.Uint16(packet[4:6]) == id &&
|
||||
binary.BigEndian.Uint16(packet[6:8]) == seq
|
||||
}
|
||||
|
||||
func sameIP(addr net.Addr, ip net.IP) bool {
|
||||
ipAddr, ok := addr.(*net.IPAddr)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return ipAddr.IP.Equal(ip)
|
||||
}
|
||||
|
||||
func icmpChecksum(data []byte) uint16 {
|
||||
var sum uint32
|
||||
for len(data) > 1 {
|
||||
sum += uint32(binary.BigEndian.Uint16(data[:2]))
|
||||
data = data[2:]
|
||||
}
|
||||
if len(data) == 1 {
|
||||
sum += uint32(data[0]) << 8
|
||||
}
|
||||
for sum>>16 != 0 {
|
||||
sum = (sum & 0xffff) + (sum >> 16)
|
||||
}
|
||||
return ^uint16(sum)
|
||||
}
|
||||
|
||||
var errUnsupportedGateway = errors.New("gateway discovery is unsupported on this platform")
|
||||
99
internal/probe/probe_test.go
Normal file
99
internal/probe/probe_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHumanizeBitsPerSecond(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
rate float64
|
||||
want string
|
||||
}{
|
||||
{name: "bits", rate: 999, want: "999 bit/s"},
|
||||
{name: "kilobits", rate: 1500, want: "1.50 kbit/s"},
|
||||
{name: "megabits", rate: 2500000, want: "2.50 Mbit/s"},
|
||||
{name: "gigabits", rate: 4200000000, want: "4.20 Gbit/s"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := HumanizeBitsPerSecond(tt.rate); got != tt.want {
|
||||
t.Fatalf("HumanizeBitsPerSecond(%v) = %q, want %q", tt.rate, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNetDev(t *testing.T) {
|
||||
data := []byte(`Inter-| Receive | Transmit
|
||||
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
|
||||
lo: 1000 0 0 0 0 0 0 0 2000 0 0 0 0 0 0 0
|
||||
eth0: 12345 1 0 0 0 0 0 0 67890 2 0 0 0 0 0 0
|
||||
`)
|
||||
|
||||
stats, err := parseNetDev(data, "eth0")
|
||||
if err != nil {
|
||||
t.Fatalf("parseNetDev() error = %v", err)
|
||||
}
|
||||
if stats.RXBytes != 12345 || stats.TXBytes != 67890 {
|
||||
t.Fatalf("stats = %+v, want rx 12345 tx 67890", stats)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLittleEndianHexIPv4(t *testing.T) {
|
||||
ip, err := littleEndianHexIPv4("0102A8C0")
|
||||
if err != nil {
|
||||
t.Fatalf("littleEndianHexIPv4() error = %v", err)
|
||||
}
|
||||
if !ip.Equal(net.IPv4(192, 168, 2, 1)) {
|
||||
t.Fatalf("ip = %v, want 192.168.2.1", ip)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLinuxDefaultGateway(t *testing.T) {
|
||||
t.Run("explicit gateway default route", func(t *testing.T) {
|
||||
data := []byte(`Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
|
||||
eth0 00000000 0102A8C0 0003 0 0 100 00000000 0 0 0
|
||||
`)
|
||||
|
||||
ip, err := parseLinuxDefaultGateway(data, "eth0")
|
||||
if err != nil {
|
||||
t.Fatalf("parseLinuxDefaultGateway() error = %v", err)
|
||||
}
|
||||
if !ip.Equal(net.IPv4(192, 168, 2, 1)) {
|
||||
t.Fatalf("ip = %v, want 192.168.2.1", ip)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("point to point peer route", func(t *testing.T) {
|
||||
data := []byte(`Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
|
||||
ppp0 00000000 00000000 0001 0 0 0 00000000 0 0 0
|
||||
ppp0 0100400A 00000000 0005 0 0 0 FFFFFFFF 0 0 0
|
||||
`)
|
||||
|
||||
ip, err := parseLinuxDefaultGateway(data, "ppp0")
|
||||
if err != nil {
|
||||
t.Fatalf("parseLinuxDefaultGateway() error = %v", err)
|
||||
}
|
||||
if !ip.Equal(net.IPv4(10, 64, 0, 1)) {
|
||||
t.Fatalf("ip = %v, want 10.64.0.1", ip)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("prefers explicit gateway over peer route", func(t *testing.T) {
|
||||
data := []byte(`Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
|
||||
ppp0 00000000 FE00400A 0003 0 0 0 00000000 0 0 0
|
||||
ppp0 0100400A 00000000 0005 0 0 0 FFFFFFFF 0 0 0
|
||||
`)
|
||||
|
||||
ip, err := parseLinuxDefaultGateway(data, "ppp0")
|
||||
if err != nil {
|
||||
t.Fatalf("parseLinuxDefaultGateway() error = %v", err)
|
||||
}
|
||||
if !ip.Equal(net.IPv4(10, 64, 0, 254)) {
|
||||
t.Fatalf("ip = %v, want 10.64.0.254", ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
151
internal/probe/route_linux.go
Normal file
151
internal/probe/route_linux.go
Normal file
@@ -0,0 +1,151 @@
|
||||
//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
|
||||
}
|
||||
16
internal/probe/route_other.go
Normal file
16
internal/probe/route_other.go
Normal file
@@ -0,0 +1,16 @@
|
||||
//go:build !linux
|
||||
|
||||
package probe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
)
|
||||
|
||||
// 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, err
|
||||
}
|
||||
return nil, errUnsupportedGateway
|
||||
}
|
||||
Reference in New Issue
Block a user