mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-28 09:39:05 +02:00
215 lines
5.7 KiB
Go
215 lines
5.7 KiB
Go
//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
|
|
}
|