mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-05 20:26:47 +02:00
Renaming reachability->probe
Correctly handle interface probes for 0.0.0.0 and :: Made all hooks sync.atomic so we're not blowing up the tests.
This commit is contained in:
parent
5e37be0fb6
commit
7e771b0c9e
@ -22,6 +22,9 @@ import (
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
const VERBOSE_LOGS = true
|
||||
const CHECK_PROBE_RESULTS_WITH_RIB = true
|
||||
|
||||
func control(logf logger.Logf, netMon *netmon.Monitor) func(network, address string, c syscall.RawConn) error {
|
||||
return func(network, address string, c syscall.RawConn) error {
|
||||
return controlLogf(logf, netMon, network, address, c)
|
||||
@ -47,23 +50,42 @@ func controlLogf(logf logger.Logf, netMon *netmon.Monitor, network, address stri
|
||||
return fmt.Errorf("netns: control: SplitHostPort %q: %w", address, err)
|
||||
}
|
||||
|
||||
hpn := NewProbeTarget(network, host, port)
|
||||
logf("netns: probing for interface to reach %s/%s:%s", network, hpn.Host, hpn.Port)
|
||||
|
||||
opts := probeOpts{
|
||||
logf: logf,
|
||||
hpn: HostPortNetwork{Network: network, Host: host, Port: port},
|
||||
filterf: filterInvalidIntefaces,
|
||||
race: true,
|
||||
cache: cache(),
|
||||
logf: logf,
|
||||
pt: hpn,
|
||||
filterf: nil,
|
||||
race: true,
|
||||
cache: cache(),
|
||||
debugLogs: VERBOSE_LOGS,
|
||||
}
|
||||
|
||||
// No netmon and no routing table.
|
||||
iface, err := findInterfaceThatCanReach(opts)
|
||||
|
||||
if err != nil || iface == nil {
|
||||
if CHECK_PROBE_RESULTS_WITH_RIB {
|
||||
ribIdx, berr := getInterfaceIndex(logf, netMon, address)
|
||||
probeIdx := 0
|
||||
if iface != nil {
|
||||
probeIdx = iface.Index
|
||||
}
|
||||
if berr == nil && iface != nil && iface.Index != ribIdx {
|
||||
logf("netns: [unexpected] probe chose ifindex %d but routing table chose ifindex %d", probeIdx, ribIdx)
|
||||
}
|
||||
if berr != nil && iface != nil {
|
||||
logf("netns: [unexpected] probe chose ifindex %d but routing table lookup failed: %v", probeIdx, berr)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logf("netns: probe found no interface to reach %s/%s", network, address)
|
||||
return err
|
||||
}
|
||||
|
||||
bindFn := getBindFn(network, address)
|
||||
logf("netns: post-probe binding to interface %q (index %d) for %s/%s", iface.Name, iface.Index, network, address)
|
||||
bindFn := bindFnByAddrType(network, address)
|
||||
return bindFn(c, uint32(iface.Index))
|
||||
}
|
||||
|
||||
@ -73,8 +95,11 @@ func controlLogf(logf logger.Logf, netMon *netmon.Monitor, network, address stri
|
||||
}
|
||||
|
||||
// Bind using the legacy RIB / netmon method.
|
||||
idx, _ := getInterfaceIndex(logf, netMon, address)
|
||||
bindFn := getBindFn(network, address)
|
||||
idx, err := getInterfaceIndex(logf, netMon, address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bindFn := bindFnByAddrType(network, address)
|
||||
return bindFn(c, uint32(idx))
|
||||
}
|
||||
|
||||
@ -99,7 +124,7 @@ func SetListenConfigInterfaceIndex(lc *net.ListenConfig, ifIndex int) error {
|
||||
return errors.New("ListenConfig.Control already set")
|
||||
}
|
||||
lc.Control = func(network, address string, c syscall.RawConn) error {
|
||||
bindFn := getBindFn(network, address)
|
||||
bindFn := bindFnByAddrType(network, address)
|
||||
return bindFn(c, uint32(ifIndex))
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
package netns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
@ -131,3 +132,12 @@ func bindToDevice(fd uintptr) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func bindSocket6(c syscall.RawConn, idx uint32) error {
|
||||
return errors.New("bindSocket6 not implemented on Linux. Use BindToDevice or SocketMark instead")
|
||||
}
|
||||
|
||||
func bindSocket4(c syscall.RawConn, idx uint32) error {
|
||||
return errors.New("bindSocket4 not implemented on Linux. Use BindToDevice or SocketMark instead")
|
||||
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@ -66,9 +67,9 @@ func tailscaleInterface() (*net.Interface, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// inetReachability describes an interface and whether it was able to reach
|
||||
// probeResult describes an interface and whether it was able to reach
|
||||
// the provided address.
|
||||
type inetReachability struct {
|
||||
type probeResult struct {
|
||||
iface net.Interface
|
||||
// TODO (barnstar): These are invariant. reachable should be true if err==nil.
|
||||
reachable bool
|
||||
@ -77,35 +78,49 @@ type inetReachability struct {
|
||||
|
||||
// Tuple of the destination host, port, and network.
|
||||
// ie: "tcp4", "example.com", "80"
|
||||
type HostPortNetwork struct {
|
||||
type ProbeTarget struct {
|
||||
Host string
|
||||
Port string
|
||||
Network string
|
||||
}
|
||||
|
||||
func (hpn HostPortNetwork) String() string {
|
||||
func NewProbeTarget(network, host, port string) ProbeTarget {
|
||||
// Probe scans require the explicit zero host not an empty string
|
||||
if host == "" {
|
||||
isV6 := strings.Contains(network, "6")
|
||||
if !isV6 {
|
||||
host = "0.0.0.0"
|
||||
} else {
|
||||
host = "::"
|
||||
}
|
||||
}
|
||||
|
||||
return ProbeTarget{
|
||||
Host: host,
|
||||
Port: port,
|
||||
Network: network,
|
||||
}
|
||||
}
|
||||
|
||||
func (hpn ProbeTarget) String() string {
|
||||
return fmt.Sprintf("%s/%s:%s", hpn.Network, hpn.Host, hpn.Port)
|
||||
}
|
||||
|
||||
type probeOpts struct {
|
||||
logf logger.Logf
|
||||
hpn HostPortNetwork
|
||||
race bool // if true, we'll pick the first interface that responds. sortf is ignored.
|
||||
filterf interfaceFilter // optional pre-filter for interfaces
|
||||
cache *routeCache // must be non-nil
|
||||
logf logger.Logf
|
||||
pt ProbeTarget
|
||||
race bool // if true, we'll pick the first interface that responds. sortf is ignored.
|
||||
filterf interfaceFilter // optional pre-filter for interfaces
|
||||
cache *routeCache // must be non-nil
|
||||
debugLogs bool // if true, log verbose output
|
||||
}
|
||||
|
||||
type DefaultIfaceHintFn func() int
|
||||
|
||||
var defaultIfaceHintFn DefaultIfaceHintFn
|
||||
|
||||
// Platforms may set defaultIFQueryFn to a function that returns the platforms's high
|
||||
// level view of the default interface index.
|
||||
func SetDefaultIFQueryFn(fn DefaultIfaceHintFn) {
|
||||
defaultIfaceHintFn = fn
|
||||
func (p *probeOpts) logDebug(format string, args ...any) {
|
||||
if p.debugLogs && p.logf != nil {
|
||||
p.logf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// uint
|
||||
type bindFn func(c syscall.RawConn, ifidx uint32) error
|
||||
|
||||
// Returns the proper bind function for the given network and address.
|
||||
@ -118,59 +133,77 @@ func bindFnByAddrType(network, address string) bindFn {
|
||||
return bindSocket4
|
||||
}
|
||||
|
||||
type bindFunctionHook func(network, address string) bindFn
|
||||
|
||||
var getBindFn bindFunctionHook = bindFnByAddrType
|
||||
|
||||
// For testing
|
||||
var interfacesHookFn func() ([]net.Interface, error)
|
||||
|
||||
var interfacesHook = net.Interfaces
|
||||
type probeHookFn func(iface *net.Interface, pt ProbeTarget) error
|
||||
|
||||
var (
|
||||
interfacesHook atomic.Value // of interfacesHookFn
|
||||
probeHook atomic.Value // of probeHookFn
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Set default hooks
|
||||
probeHook.Store(probe)
|
||||
interfacesHook.Store(net.Interfaces)
|
||||
}
|
||||
|
||||
func ifaceListDesc(ifaces []net.Interface) string {
|
||||
names := ""
|
||||
for i, iface := range ifaces {
|
||||
if i > 0 {
|
||||
names += ", "
|
||||
}
|
||||
names += iface.Name
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
// ProbeInterfacesReachability probes all non-loopback, up interfaces
|
||||
// concurrently to determine which can reach the given address. It returns
|
||||
// a slice with one entry per probed interface in the same order as
|
||||
// net.Interfaces() filtered by the probe criteria.
|
||||
func probeInterfacesReachability(opts probeOpts) ([]inetReachability, error) {
|
||||
ifaces, err := interfacesHook()
|
||||
func probeInterfacesReachability(opts probeOpts) ([]probeResult, error) {
|
||||
ifaces, err := interfacesHook.Load().(func() ([]net.Interface, error))()
|
||||
if err != nil {
|
||||
opts.logf("netns: ProbeInterfacesReachability: net.Interfaces: %v", err)
|
||||
opts.logf("netns: probe failed to find net.Interfaces: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make(chan inetReachability, len(ifaces))
|
||||
results := make(chan probeResult, len(ifaces))
|
||||
|
||||
tsiface, _ := tailscaleInterface()
|
||||
|
||||
var candidates []net.Interface
|
||||
|
||||
for _, iface := range ifaces {
|
||||
// Individual platforms can exclude potential intefaces based on platorm-specific logic.
|
||||
// For example, on Darwin, we skip "utun" interfaces.
|
||||
if opts.filterf != nil && !opts.filterf(iface) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only consider up, non-loopback interfaces.
|
||||
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagRunning == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip the Tailscale interface.
|
||||
if tsiface != nil && iface.Index == tsiface.Index {
|
||||
continue
|
||||
}
|
||||
|
||||
// require an IPv4 or IPv6 global unicast address
|
||||
if !ifaceHasV4OrGlobalV6(&iface) {
|
||||
continue
|
||||
}
|
||||
|
||||
candidates = append(candidates, iface)
|
||||
}
|
||||
|
||||
if len(candidates) == 0 {
|
||||
opts.logf("netns: ProbeInterfacesReachability: no candidate interfaces found")
|
||||
opts.logDebug("netns: ProbeInterfacesReachability: no candidate interfaces found")
|
||||
return nil, errors.New("no candidate interfaces")
|
||||
}
|
||||
opts.logDebug("netns: using candidate interfaces: %s", ifaceListDesc(candidates))
|
||||
|
||||
// Close this channel to abort ongoing probes if we're racing and are only interested
|
||||
// in the first result.
|
||||
@ -184,13 +217,15 @@ func probeInterfacesReachability(opts probeOpts) ([]inetReachability, error) {
|
||||
default:
|
||||
}
|
||||
|
||||
// Per-probe timeout.
|
||||
err := reachabilityHook(&iface, opts.hpn)
|
||||
results <- inetReachability{iface: iface, reachable: err == nil, err: err}
|
||||
opts.logDebug("netns: probing %s for reachability to %s via %s", iface.Name, opts.pt.Host, opts.pt.Network)
|
||||
probeFn := probeHook.Load().(func(*net.Interface, ProbeTarget) error)
|
||||
|
||||
err := probeFn(&iface, opts.pt)
|
||||
results <- probeResult{iface: iface, reachable: err == nil, err: err}
|
||||
}()
|
||||
}
|
||||
|
||||
out := make([]inetReachability, 0, len(candidates))
|
||||
out := make([]probeResult, 0, len(candidates))
|
||||
timeout := time.After(600 * time.Millisecond)
|
||||
received := 0
|
||||
|
||||
@ -202,7 +237,7 @@ func probeInterfacesReachability(opts probeOpts) ([]inetReachability, error) {
|
||||
// can't get the conn up and running later but signal early if we're racing.
|
||||
if opts.race && r.reachable {
|
||||
close(done)
|
||||
return []inetReachability{r}, nil
|
||||
return []probeResult{r}, nil
|
||||
}
|
||||
// .. otherwise, collect all results including the unreachable ones.
|
||||
out = append(out, r)
|
||||
@ -215,12 +250,15 @@ func probeInterfacesReachability(opts probeOpts) ([]inetReachability, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// For testing
|
||||
type reachabilityHookFn func(iface *net.Interface, hpn HostPortNetwork) error
|
||||
func probe(iface *net.Interface, pt ProbeTarget) error {
|
||||
// For unspecified hosts, we need to listen rather than dial.
|
||||
if pt.Host == "0.0.0.0" || pt.Host == "::" {
|
||||
return probeBindListen(iface, pt)
|
||||
}
|
||||
return probeBindDial(iface, pt)
|
||||
}
|
||||
|
||||
var reachabilityHook reachabilityHookFn = reachabilityCheck
|
||||
|
||||
func reachabilityCheck(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
func probeBindDial(iface *net.Interface, pt ProbeTarget) error {
|
||||
// Per-probe timeout.
|
||||
dialCtx, dialCancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
|
||||
defer dialCancel()
|
||||
@ -228,19 +266,37 @@ func reachabilityCheck(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
d := net.Dialer{
|
||||
Control: func(network, address string, c syscall.RawConn) error {
|
||||
// (barnstar) TODO: The bind step here is still platform specific
|
||||
bindFn := getBindFn(network, address)
|
||||
bindFn := bindFnByAddrType(network, address)
|
||||
return bindFn(c, uint32(iface.Index))
|
||||
},
|
||||
}
|
||||
|
||||
dst := net.JoinHostPort(hpn.Host, hpn.Port)
|
||||
conn, err := d.DialContext(dialCtx, hpn.Network, dst)
|
||||
dst := net.JoinHostPort(pt.Host, pt.Port)
|
||||
conn, err := d.DialContext(dialCtx, pt.Network, dst)
|
||||
if err == nil {
|
||||
defer conn.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func probeBindListen(iface *net.Interface, pt ProbeTarget) error {
|
||||
lc := net.ListenConfig{
|
||||
Control: func(network, address string, c syscall.RawConn) error {
|
||||
bindFn := bindFnByAddrType(network, address)
|
||||
return bindFn(c, uint32(iface.Index))
|
||||
},
|
||||
}
|
||||
|
||||
dst := net.JoinHostPort(pt.Host, pt.Port)
|
||||
// Bind to this interface on any available port
|
||||
listener, err := lc.ListenPacket(context.Background(), pt.Network, dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
listener.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pre-filter for interfaces. Platform-specific code can provide a filter
|
||||
// to exclude certain interfaces from consideration. For example, on Darwin,
|
||||
// we exclude "utun" interfaces and various other types which will never provie
|
||||
@ -259,6 +315,7 @@ func filterInPlace[T any](s []T, keep func(T) bool) []T {
|
||||
}
|
||||
|
||||
var errUnspecifiedHost = errors.New("unspecified host")
|
||||
var errNoAvailableInterface = errors.New("no available interface")
|
||||
|
||||
func parseAddress(address string) (addr netip.Addr, err error) {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
@ -289,12 +346,12 @@ func parseAddress(address string) (addr netip.Addr, err error) {
|
||||
// nil is returned if no interface can reach the destination.
|
||||
func findInterfaceThatCanReach(opts probeOpts) (iface *net.Interface, err error) {
|
||||
// Try to parse the host as an IP address for cache lookup
|
||||
addr, err := parseAddress(opts.hpn.Host)
|
||||
if err == nil && addr.IsValid() {
|
||||
addr, err := parseAddress(opts.pt.Host)
|
||||
if opts.cache != nil && err == nil && addr.IsValid() {
|
||||
// Check cache first
|
||||
if cached := opts.cache.lookupCachedRoute(addr); cached != nil {
|
||||
hits, misses, total := opts.cache.stats()
|
||||
opts.logf("netns: cachHit for %v cache stats: hits=%d misses=%d total=%d", addr, hits, misses, total)
|
||||
opts.logDebug("netns: cachHit for %v cache stats: hits=%d misses=%d total=%d", addr, hits, misses, total)
|
||||
return cached, nil
|
||||
}
|
||||
}
|
||||
@ -305,10 +362,10 @@ func findInterfaceThatCanReach(opts probeOpts) (iface *net.Interface, err error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res = filterInPlace(res, func(r inetReachability) bool { return r.reachable })
|
||||
res = filterInPlace(res, func(r probeResult) bool { return r.reachable })
|
||||
if len(res) == 0 {
|
||||
opts.logf("netns: could not find interface on network %v to reach %q:%q on %q: %v", opts.hpn.Network, opts.hpn.Host, opts.hpn.Port, opts.hpn.Network, err)
|
||||
return nil, nil
|
||||
opts.logf("netns: could not find interface on network %v to reach %s:%s on %s: %v", opts.pt.Network, opts.pt.Host, opts.pt.Port, opts.pt.Network, err)
|
||||
return nil, errNoAvailableInterface
|
||||
}
|
||||
|
||||
candidatesNames := make([]string, 0, len(res))
|
||||
@ -317,19 +374,8 @@ func findInterfaceThatCanReach(opts probeOpts) (iface *net.Interface, err error)
|
||||
}
|
||||
iface = &res[0].iface
|
||||
|
||||
if defaultIfaceHintFn != nil {
|
||||
defIdx := defaultIfaceHintFn()
|
||||
for _, r := range res {
|
||||
if r.iface.Index == defIdx {
|
||||
opts.logf("netns: using default iface hint")
|
||||
iface = &r.iface
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the result if we have a valid IP address
|
||||
if addr.IsValid() {
|
||||
if opts.cache != nil && addr.IsValid() {
|
||||
opts.cache.setCachedRoute(addr, iface)
|
||||
}
|
||||
|
||||
|
||||
@ -298,11 +298,11 @@ func TestGlobalRouteCache(t *testing.T) {
|
||||
}
|
||||
|
||||
func hookInterfaces(t *testing.T, ifaces []net.Interface) {
|
||||
interfacesHook = func() ([]net.Interface, error) {
|
||||
interfacesHook.Store(func() ([]net.Interface, error) {
|
||||
return ifaces, nil
|
||||
}
|
||||
})
|
||||
t.Cleanup(func() {
|
||||
interfacesHook = net.Interfaces
|
||||
interfacesHook.Store(net.Interfaces)
|
||||
})
|
||||
}
|
||||
|
||||
@ -336,10 +336,10 @@ var (
|
||||
)
|
||||
|
||||
func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
origReachabilityHook := reachabilityHook
|
||||
origProbeHook := probeHook.Load().(func(*net.Interface, ProbeTarget) error)
|
||||
t.Cleanup(func() {
|
||||
ifaceHasV4AndGlobalV6Hook = nil
|
||||
reachabilityHook = origReachabilityHook
|
||||
probeHook.Store(origProbeHook)
|
||||
})
|
||||
|
||||
ifaceHasV4AndGlobalV6Hook = func(iface *net.Interface) bool {
|
||||
@ -355,14 +355,14 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
cache.setCachedRoute(addr, &interfaceWlan0)
|
||||
|
||||
// Hook should never be called when cache hits
|
||||
reachabilityHook = func(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
probeHook.Store(func(iface *net.Interface, hpn ProbeTarget) error {
|
||||
t.Error("reachabilityHookFn should not be called when cache hits")
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
opts := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: "8.8.8.8", Port: "53", Network: "udp"},
|
||||
pt: ProbeTarget{Host: "8.8.8.8", Port: "53", Network: "udp"},
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
@ -385,13 +385,13 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
hookDefaultInterfaces(t)
|
||||
|
||||
// All interfaces succeed
|
||||
reachabilityHook = func(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
probeHook.Store(func(iface *net.Interface, hpn ProbeTarget) error {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
opts := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: "1.1.1.1", Port: "53", Network: "udp"},
|
||||
pt: ProbeTarget{Host: "1.1.1.1", Port: "53", Network: "udp"},
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
@ -419,13 +419,13 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
hookDefaultInterfaces(t)
|
||||
|
||||
// All interfaces fail
|
||||
reachabilityHook = func(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
probeHook.Store(func(iface *net.Interface, hpn ProbeTarget) error {
|
||||
return errors.New("unreachable")
|
||||
}
|
||||
})
|
||||
|
||||
opts := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: "192.0.2.1", Port: "53", Network: "udp"},
|
||||
pt: ProbeTarget{Host: "192.0.2.1", Port: "53", Network: "udp"},
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
@ -451,15 +451,15 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
prefix2 := netip.MustParsePrefix("10.0.1.0/24")
|
||||
cache.setCachedRoutePrefix(prefix2, &interfaceWlan0)
|
||||
|
||||
reachabilityHook = func(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
probeHook.Store(func(iface *net.Interface, hpn ProbeTarget) error {
|
||||
t.Error("should use cache, not probe")
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
// Test 10.0.1.5 -> should match more specific /24
|
||||
opts1 := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: "10.0.1.5", Port: "53", Network: "udp"},
|
||||
pt: ProbeTarget{Host: "10.0.1.5", Port: "53", Network: "udp"},
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
@ -471,7 +471,7 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
// Test 10.0.2.5 -> should match broader /8
|
||||
opts2 := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: "10.0.2.5", Port: "53", Network: "udp"},
|
||||
pt: ProbeTarget{Host: "10.0.2.5", Port: "53", Network: "udp"},
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
@ -492,7 +492,7 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
wlan0Done := make(chan struct{})
|
||||
eth1Done := make(chan struct{})
|
||||
|
||||
reachabilityHook = func(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
probeHook.Store(func(iface *net.Interface, hpn ProbeTarget) error {
|
||||
switch iface.Index {
|
||||
case interfaceEth0.Index: // eth0 - returns immediately
|
||||
return nil
|
||||
@ -504,7 +504,7 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
return nil
|
||||
}
|
||||
return errors.New("unknown interface")
|
||||
}
|
||||
})
|
||||
defer func() {
|
||||
close(wlan0Done)
|
||||
close(eth1Done)
|
||||
@ -512,7 +512,7 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
|
||||
opts := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: "8.8.8.8", Port: "53", Network: "udp"},
|
||||
pt: ProbeTarget{Host: "8.8.8.8", Port: "53", Network: "udp"},
|
||||
race: true,
|
||||
cache: cache,
|
||||
}
|
||||
@ -535,14 +535,14 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
hookDefaultInterfaces(t)
|
||||
|
||||
probeCount := atomic.Int32{}
|
||||
reachabilityHook = func(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
probeHook.Store(func(iface *net.Interface, hpn ProbeTarget) error {
|
||||
probeCount.Add(1)
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
opts := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: "8.8.8.8", Port: "53", Network: "udp"},
|
||||
pt: ProbeTarget{Host: "8.8.8.8", Port: "53", Network: "udp"},
|
||||
cache: cache,
|
||||
filterf: func(iface net.Interface) bool {
|
||||
// Exclude wlan0 and eth1
|
||||
@ -569,14 +569,14 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
cache := NewRouteCache()
|
||||
hookDefaultInterfaces(t)
|
||||
|
||||
reachabilityHook = func(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
probeHook.Store(func(iface *net.Interface, hpn ProbeTarget) error {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
// Use a hostname that can't be parsed as an IP
|
||||
opts := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: "example.com", Port: "443", Network: "tcp"},
|
||||
pt: ProbeTarget{Host: "example.com", Port: "443", Network: "tcp"},
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
@ -596,43 +596,6 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("default interface hint is respected", func(t *testing.T) {
|
||||
cache := NewRouteCache()
|
||||
hookDefaultInterfaces(t)
|
||||
|
||||
// All interfaces are reachable
|
||||
reachabilityHook = func(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set hint to prefer iface2 (index 2)
|
||||
origHintFn := defaultIfaceHintFn
|
||||
defer func() { defaultIfaceHintFn = origHintFn }()
|
||||
|
||||
defaultIfaceHintFn = func() int {
|
||||
return 2 // iface2 / wlan0
|
||||
}
|
||||
|
||||
opts := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: "1.1.1.1", Port: "53", Network: "udp"},
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
result, err := findInterfaceThatCanReach(opts)
|
||||
if err != nil {
|
||||
t.Fatalf("findInterfaceThatCanReach failed: %v", err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("expected non-nil result")
|
||||
}
|
||||
|
||||
if result.Index != 2 {
|
||||
t.Errorf("expected default hint interface (index 2), got index %d (%s)", result.Index, result.Name)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("IPv6 address uses IPv6 cache table", func(t *testing.T) {
|
||||
cache := NewRouteCache()
|
||||
hookDefaultInterfaces(t)
|
||||
@ -641,14 +604,14 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
addr6 := netip.MustParseAddr("2001:4860:4860::8888")
|
||||
cache.setCachedRoute(addr6, &interfaceEth1)
|
||||
|
||||
reachabilityHook = func(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
probeHook.Store(func(iface *net.Interface, hpn ProbeTarget) error {
|
||||
t.Error("should use cache for IPv6")
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
opts := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: "2001:4860:4860::8888", Port: "53", Network: "udp6"},
|
||||
pt: ProbeTarget{Host: "2001:4860:4860::8888", Port: "53", Network: "udp6"},
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
@ -672,15 +635,15 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
cache.setCachedRoute(addr4, &interfaceEth0)
|
||||
cache.setCachedRoute(addr6, &interfaceWlan0)
|
||||
|
||||
reachabilityHook = func(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
probeHook.Store(func(iface *net.Interface, hpn ProbeTarget) error {
|
||||
t.Error("should use cache")
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
// Test IPv4
|
||||
opts4 := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: "8.8.8.8", Port: "53", Network: "udp"},
|
||||
pt: ProbeTarget{Host: "8.8.8.8", Port: "53", Network: "udp"},
|
||||
cache: cache,
|
||||
}
|
||||
result4, _ := findInterfaceThatCanReach(opts4)
|
||||
@ -691,7 +654,7 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
// Test IPv6
|
||||
opts6 := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: "2001:4860:4860::8888", Port: "53", Network: "udp6"},
|
||||
pt: ProbeTarget{Host: "2001:4860:4860::8888", Port: "53", Network: "udp6"},
|
||||
cache: cache,
|
||||
}
|
||||
result6, _ := findInterfaceThatCanReach(opts6)
|
||||
@ -704,13 +667,13 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
cache := NewRouteCache()
|
||||
hookDefaultInterfaces(t)
|
||||
|
||||
reachabilityHook = func(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
probeHook.Store(func(iface *net.Interface, hpn ProbeTarget) error {
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
opts := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: "", Port: "53", Network: "udp"},
|
||||
pt: ProbeTarget{Host: "", Port: "53", Network: "udp"},
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
@ -730,10 +693,10 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
prefix := netip.MustParsePrefix("192.168.0.0/16")
|
||||
cache.setCachedRoutePrefix(prefix, &interfaceEth0)
|
||||
|
||||
reachabilityHook = func(iface *net.Interface, hpn HostPortNetwork) error {
|
||||
probeHook.Store(func(iface *net.Interface, hpn ProbeTarget) error {
|
||||
t.Error("should use cached subnet")
|
||||
return nil
|
||||
}
|
||||
})
|
||||
|
||||
// Test various IPs in the subnet
|
||||
testIPs := []string{
|
||||
@ -745,7 +708,7 @@ func TestFindInterfaceThatCanReach(t *testing.T) {
|
||||
for _, ip := range testIPs {
|
||||
opts := probeOpts{
|
||||
logf: t.Logf,
|
||||
hpn: HostPortNetwork{Host: ip, Port: "53", Network: "udp"},
|
||||
pt: ProbeTarget{Host: ip, Port: "53", Network: "udp"},
|
||||
cache: cache,
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user