Ctrl K
ComunidadGuías…

sharedsock/src_probe_linux_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//go:build linux && !android

package sharedsock

import (
	"net"
	"net/netip"
	"strconv"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"github.com/vishvananda/netlink"
	"golang.org/x/sys/unix"
)

// routeGetSrc is the kernel's answer through netlink, used as the reference.
func routeGetSrc(t *testing.T, dst netip.Addr) (netip.Addr, bool) {
	t.Helper()
	routes, err := netlink.RouteGet(net.IP(dst.AsSlice()))
	if err != nil {
		return netip.Addr{}, false
	}
	for _, r := range routes {
		if src, ok := netip.AddrFromSlice(r.Src); ok {
			return src.Unmap(), true
		}
	}
	return netip.Addr{}, false
}

func newTestProbe(t *testing.T, family int) *srcProbe {
	t.Helper()
	p, err := newSrcProbe(family)
	require.NoError(t, err)
	t.Cleanup(func() { _ = p.close() })
	return p
}

// A reused UDP socket keeps the source address of its first connect. Alternating
// between a loopback and an off-host destination catches a probe that forgets to
// disconnect: the second lookup would report 127.0.0.1 for the off-host address.
func TestSrcProbe_AlternatingDestinationsMatchRouteGet(t *testing.T) {
	loopback := netip.MustParseAddr("127.0.0.1")
	remote := netip.MustParseAddr("192.0.2.1")

	remoteSrc, ok := routeGetSrc(t, remote)
	if !ok {
		t.Skip("no route to an off-host IPv4 destination")
	}
	require.NotEqual(t, loopback, remoteSrc, "off-host destination must not route via loopback")

	p := newTestProbe(t, unix.AF_INET)
	for i := 0; i < 3; i++ {
		src, err := p.resolve(rawSockaddr(loopback, 0))
		require.NoError(t, err)
		assert.Equal(t, loopback, src, "source for loopback, round %d", i)

		src, err = p.resolve(rawSockaddr(remote, 0))
		require.NoError(t, err)
		assert.Equal(t, remoteSrc, src, "source for %s, round %d", remote, i)
	}
}

func TestSrcProbe_IPv6(t *testing.T) {
	loopback := netip.MustParseAddr("::1")
	if _, ok := routeGetSrc(t, loopback); !ok {
		t.Skip("no IPv6 loopback")
	}

	p := newTestProbe(t, unix.AF_INET6)
	src, err := p.resolve(rawSockaddr(loopback, 0))
	require.NoError(t, err)
	assert.Equal(t, loopback, src, "source for ::1")

	remote := netip.MustParseAddr("2001:db8::1")
	remoteSrc, ok := routeGetSrc(t, remote)
	if !ok {
		t.Skipf("no route to %s", remote)
	}
	src, err = p.resolve(rawSockaddr(remote, 0))
	require.NoError(t, err)
	assert.Equal(t, remoteSrc, src, "source for %s", remote)
}

// A route the kernel refuses is an answer, not a broken socket, so the probe keeps
// its socket and the next lookup reuses it.
func TestSrcProbe_RouteErrorKeepsSocket(t *testing.T) {
	p := newTestProbe(t, unix.AF_INET)
	conn := p.conn

	// Connecting to the limited broadcast address without SO_BROADCAST fails.
	_, err := p.resolve(rawSockaddr(netip.MustParseAddr("255.255.255.255"), 0))
	var rErr *routeError
	require.ErrorAs(t, err, &rErr, "lookup for the broadcast address should fail as a route error")
	assert.Same(t, conn, p.conn, "route error should keep the probe socket")

	src, err := p.resolve(rawSockaddr(netip.MustParseAddr("127.0.0.1"), 0))
	require.NoError(t, err)
	assert.Equal(t, netip.MustParseAddr("127.0.0.1"), src, "source after the route error")
	assert.Same(t, conn, p.conn, "lookup after a route error should reuse the socket")
}

// A socket that stops working is dropped, and the next lookup opens a fresh one.
func TestSrcProbe_ReopensAfterSocketError(t *testing.T) {
	p := newTestProbe(t, unix.AF_INET)
	require.NoError(t, p.conn.Close())

	_, err := p.resolve(rawSockaddr(netip.MustParseAddr("127.0.0.1"), 0))
	require.Error(t, err, "lookup on a closed socket should fail")
	assert.Nil(t, p.conn, "socket error should drop the probe socket")

	src, err := p.resolve(rawSockaddr(netip.MustParseAddr("127.0.0.1"), 0))
	require.NoError(t, err)
	assert.Equal(t, netip.MustParseAddr("127.0.0.1"), src, "source after reopening")
	assert.NotNil(t, p.conn, "probe socket should be open again")
}

// A link-local destination is only routable with its scope. The probe must pass the
// scope to the kernel and get the interface's own link-local address back.
func TestSrcProbe_LinkLocalWithZone(t *testing.T) {
	iface, want := linkLocalInterface(t)
	dst := netip.MustParseAddr("fe80::1")

	p := newTestProbe(t, unix.AF_INET6)
	rc, err := p.conn.SyscallConn()
	require.NoError(t, err)

	for _, zone := range []string{iface.Name, strconv.Itoa(iface.Index)} {
		scope, err := zoneIndex(rc, zone)
		require.NoError(t, err, "zone %q", zone)
		assert.Equal(t, uint32(iface.Index), scope, "index for zone %q", zone)

		src, err := p.resolve(rawSockaddr(dst.WithZone(zone), scope))
		require.NoError(t, err, "zone %q", zone)
		assert.Equal(t, want, src, "source for %s%%%s", dst, zone)
	}

	_, err = p.resolve(rawSockaddr(dst, 0))
	var rErr *routeError
	assert.ErrorAs(t, err, &rErr, "link-local destination without a scope should fail as a route error")
}

func TestZoneIndex(t *testing.T) {
	p := newTestProbe(t, unix.AF_INET6)
	rc, err := p.conn.SyscallConn()
	require.NoError(t, err)

	scope, err := zoneIndex(rc, "")
	require.NoError(t, err)
	assert.Zero(t, scope, "empty zone should be index 0")

	_, err = zoneIndex(rc, "nb-no-such-if0")
	assert.Error(t, err, "unknown interface name should fail")
}

// linkLocalInterface returns an up interface and its IPv6 link-local address.
func linkLocalInterface(t *testing.T) (net.Interface, netip.Addr) {
	t.Helper()
	ifaces, err := net.Interfaces()
	require.NoError(t, err)
	for _, iface := range ifaces {
		if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
			continue
		}
		addrs, err := iface.Addrs()
		if err != nil {
			continue
		}
		for _, a := range addrs {
			prefix, err := netip.ParsePrefix(a.String())
			if err == nil && prefix.Addr().IsLinkLocalUnicast() {
				return iface, prefix.Addr()
			}
		}
	}
	t.Skip("no interface with an IPv6 link-local address")
	return net.Interface{}, netip.Addr{}
}

func TestSrcProbe_ClosedRejectsLookups(t *testing.T) {
	p, err := newSrcProbe(unix.AF_INET)
	require.NoError(t, err)
	require.NoError(t, p.close())
	require.NoError(t, p.close(), "close must be idempotent")

	_, err = p.resolve(rawSockaddr(netip.MustParseAddr("127.0.0.1"), 0))
	assert.ErrorIs(t, err, errProbeClosed)
	assert.Nil(t, p.conn, "closed probe must not reopen")
}

func BenchmarkSrcProbe(b *testing.B) {
	p, err := newSrcProbe(unix.AF_INET)
	require.NoError(b, err)
	defer p.close()

	dst := netip.MustParseAddr("192.0.2.1")
	if _, err := p.resolve(rawSockaddr(dst, 0)); err != nil {
		b.Skipf("no route to %s: %v", dst, err)
	}

	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		if _, err := p.resolve(rawSockaddr(dst, 0)); err != nil {
			b.Fatal(err)
		}
	}
}

func BenchmarkSrcRouteGet(b *testing.B) {
	dst := net.ParseIP("192.0.2.1")
	b.ReportAllocs()
	for i := 0; i < b.N; i++ {
		if _, err := netlink.RouteGetWithOptions(dst, &netlink.RouteGetOptions{}); err != nil {
			b.Skipf("no route to %s: %v", dst, err)
		}
	}
}