Ctrl K
ComunidadGuías…

sharedsock/src_probe_linux.go

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

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

package sharedsock

import (
	"errors"
	"fmt"
	"net/netip"
	"strconv"
	"sync"
	"syscall"
	"unsafe"

	"github.com/mdlayher/socket"
	log "github.com/sirupsen/logrus"
	"golang.org/x/sys/unix"

	nbnet "github.com/netbirdio/netbird/client/net"
)

var errProbeClosed = errors.New("source probe closed")

// srcProbe finds the source address the kernel picks for a destination by connecting
// a UDP socket that carries the raw sockets' fwmark and reading back its local address.
// Connecting a UDP socket runs the output route lookup without sending anything.
type srcProbe struct {
	family int

	mu sync.Mutex
	// conn is nil while no socket is open. A failed route lookup keeps the socket,
	// any other failure closes it and the next lookup opens a fresh one, so a socket
	// in an unknown state is never reused.
	conn   *socket.Conn
	closed bool
}

// newSrcProbe opens a probe socket for the given address family.
func newSrcProbe(family int) (*srcProbe, error) {
	conn, err := openProbeSocket(family)
	if err != nil {
		return nil, err
	}
	return &srcProbe{family: family, conn: conn}, nil
}

// resolve returns the source address the kernel would use for a packet to sa, a
// sockaddr of the probe's family. It is safe for concurrent use.
func (p *srcProbe) resolve(sa unix.Sockaddr) (netip.Addr, error) {
	p.mu.Lock()
	defer p.mu.Unlock()

	if p.closed {
		return netip.Addr{}, errProbeClosed
	}

	if p.conn == nil {
		conn, err := openProbeSocket(p.family)
		if err != nil {
			return netip.Addr{}, err
		}
		p.conn = conn
	}

	src, err := p.lookup(sa)
	if err != nil {
		var rErr *routeError
		if !errors.As(err, &rErr) {
			if closeErr := p.closeSocket(); closeErr != nil {
				log.Debugf("failed to close source probe socket: %v", closeErr)
			}
		}
		return netip.Addr{}, err
	}
	return src, nil
}

// close releases the socket. Later lookups fail with errProbeClosed.
func (p *srcProbe) close() error {
	p.mu.Lock()
	defer p.mu.Unlock()

	p.closed = true
	return p.closeSocket()
}

// closeSocket closes the socket if one is open. Callers must hold p.mu.
func (p *srcProbe) closeSocket() error {
	conn := p.conn
	p.conn = nil
	if conn == nil {
		return nil
	}
	return conn.Close()
}

// lookup runs one route lookup on the socket. Callers must hold p.mu.
func (p *srcProbe) lookup(sa unix.Sockaddr) (netip.Addr, error) {
	rc, err := p.conn.SyscallConn()
	if err != nil {
		return netip.Addr{}, fmt.Errorf("probe socket: %w", err)
	}

	var src netip.Addr
	var lookupErr error
	if err := rc.Control(func(fd uintptr) {
		src, lookupErr = lookupFD(int(fd), sa)
	}); err != nil {
		return netip.Addr{}, fmt.Errorf("probe socket: %w", err)
	}
	return src, lookupErr
}

// routeError is a route lookup the kernel refused. The socket is still usable after it.
type routeError struct {
	err error
}

func (e *routeError) Error() string {
	return fmt.Sprintf("route lookup: %v", e.err)
}

func (e *routeError) Unwrap() error {
	return e.err
}

func lookupFD(fd int, sa unix.Sockaddr) (netip.Addr, error) {
	// A connected socket keeps the source address of its first connect and reuses
	// it for later route lookups, so dissolve the association first.
	if err := disconnect(fd); err != nil {
		return netip.Addr{}, fmt.Errorf("disconnect probe socket: %w", err)
	}

	if err := unix.Connect(fd, sa); err != nil {
		return netip.Addr{}, &routeError{err: err}
	}

	local, err := unix.Getsockname(fd)
	if err != nil {
		return netip.Addr{}, fmt.Errorf("read probe socket address: %w", err)
	}

	var src netip.Addr
	switch a := local.(type) {
	case *unix.SockaddrInet4:
		src = netip.AddrFrom4(a.Addr)
	case *unix.SockaddrInet6:
		src = netip.AddrFrom16(a.Addr)
	}
	if !src.IsValid() || src.IsUnspecified() {
		return netip.Addr{}, &routeError{err: errors.New("no source address")}
	}
	return src, nil
}

func openProbeSocket(family int) (*socket.Conn, error) {
	conn, err := socket.Socket(family, unix.SOCK_DGRAM, unix.IPPROTO_UDP, "udp_src_probe", nil)
	if err != nil {
		return nil, fmt.Errorf("create source probe socket: %w", err)
	}

	if err := nbnet.SetSocketMark(conn); err != nil {
		_ = conn.Close()
		return nil, fmt.Errorf("set SO_MARK on source probe socket: %w", err)
	}
	return conn, nil
}

// disconnect dissolves a UDP socket's association by connecting to AF_UNSPEC, which
// also clears the source address the kernel pinned on the previous connect.
func disconnect(fd int) error {
	sa := unix.RawSockaddr{Family: unix.AF_UNSPEC}
	_, _, errno := unix.Syscall(unix.SYS_CONNECT, uintptr(fd), uintptr(unsafe.Pointer(&sa)), unsafe.Sizeof(sa))
	if errno != 0 {
		return errno
	}
	return nil
}

// rawSockaddr returns the sockaddr for dst with port 0 and the given scope. Port 0
// matches a raw send, whose route lookup carries no ports. A UDP probe connected
// to it still gets an ephemeral source port before its lookup.
func rawSockaddr(dst netip.Addr, scope uint32) unix.Sockaddr {
	if dst.Is4() {
		return &unix.SockaddrInet4{Addr: dst.As4()}
	}
	return &unix.SockaddrInet6{Addr: dst.As16(), ZoneId: scope}
}

// zoneIndex returns the interface index for an IPv6 zone, which is either an
// interface name or a numeric index. An empty zone is index 0. A name costs one
// SIOCGIFINDEX ioctl on rc, which may be any socket.
func zoneIndex(rc syscall.RawConn, zone string) (uint32, error) {
	if zone == "" {
		return 0, nil
	}
	if idx, err := strconv.ParseUint(zone, 10, 32); err == nil {
		return uint32(idx), nil
	}

	ifr, err := unix.NewIfreq(zone)
	if err != nil {
		return 0, fmt.Errorf("zone %q: %w", zone, err)
	}
	var ioctlErr error
	if err := rc.Control(func(fd uintptr) {
		ioctlErr = unix.IoctlIfreq(int(fd), unix.SIOCGIFINDEX, ifr)
	}); err != nil {
		return 0, fmt.Errorf("zone %q: %w", zone, err)
	}
	if ioctlErr != nil {
		return 0, fmt.Errorf("resolve zone %q: %w", zone, ioctlErr)
	}
	return ifr.Uint32(), nil
}