mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-02 19:49:07 +02:00
Moving discovery to first use regressed the privileged suites on the three platforms that always build an ICE bind: Darwin, FreeBSD and Windows time out in TestWGIface_UpdateAddr, TestRecreation, TestEngine_SSH and TestEngine_MultiplePeers, while Linux stays green because a host with the WireGuard kernel module takes the kernel-device branch and never drives the mux that asks for interfaces. interfaceFilter probes with wgctrl every interface the disallow list does not already exclude. Discovering at construction ran that probe before the caller had an overlay interface of its own; discovering at first use runs it after, so on a userspace WireGuard platform the probe reaches the UAPI socket of the same process. The tests reach it because they construct with a nil disallow list, where the client passes DefaultInterfaceBlacklist and its own interface is excluded by prefix. Restore the original timing with an explicit warm-up. The constructors stay infallible and the error is still reported by the accessor that needs the interfaces, so the contract this branch is about is unchanged.
266 lines
6.6 KiB
Go
266 lines
6.6 KiB
Go
// Package stdnet is an extension of the pion's stdnet.
|
|
// With it the list of the interface can come from external source.
|
|
// More info: https://github.com/golang/go/issues/40569
|
|
package stdnet
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"slices"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/pion/transport/v3"
|
|
"github.com/pion/transport/v3/stdnet"
|
|
|
|
"github.com/netbirdio/netbird/client/iface/netstack"
|
|
)
|
|
|
|
const (
|
|
updateInterval = 30 * time.Second
|
|
dnsResolveTimeout = 30 * time.Second
|
|
)
|
|
|
|
var errNoSuitableAddress = errors.New("no suitable address found")
|
|
|
|
// Net is an implementation of the net.Net interface
|
|
// based on functions of the standard net package.
|
|
type Net struct {
|
|
stdnet.Net
|
|
interfaces []*transport.Interface
|
|
iFaceDiscover iFaceDiscover
|
|
// interfaceFilter should return true if the given interfaceName is allowed
|
|
interfaceFilter func(interfaceName string) bool
|
|
lastUpdate time.Time
|
|
|
|
// mu is shared between interfaces and lastUpdate
|
|
mu sync.Mutex
|
|
|
|
// ctx is the context for network operations that supports cancellation
|
|
ctx context.Context
|
|
}
|
|
|
|
// NewNetWithDiscover creates a new StdNet instance.
|
|
func NewNetWithDiscover(ctx context.Context, iFaceDiscover ExternalIFaceDiscover, disallowList []string) *Net {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
n := &Net{
|
|
interfaceFilter: InterfaceFilter(disallowList),
|
|
ctx: ctx,
|
|
}
|
|
// current ExternalIFaceDiscover implement in android-client https://github.dev/netbirdio/android-client
|
|
// so in android cli use pionDiscover
|
|
if netstack.IsEnabled() {
|
|
n.iFaceDiscover = pionDiscover{}
|
|
} else {
|
|
n.iFaceDiscover = newMobileIFaceDiscover(iFaceDiscover)
|
|
}
|
|
n.warmCache()
|
|
return n
|
|
}
|
|
|
|
// NewNet creates a new StdNet instance.
|
|
func NewNet(ctx context.Context, disallowList []string) *Net {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
n := &Net{
|
|
iFaceDiscover: pionDiscover{},
|
|
interfaceFilter: InterfaceFilter(disallowList),
|
|
ctx: ctx,
|
|
}
|
|
n.warmCache()
|
|
return n
|
|
}
|
|
|
|
// warmCache performs the first discovery while the caller is still setting up,
|
|
// before an overlay interface of its own exists. interfaceFilter probes every
|
|
// interface it is not told to skip with wgctrl, and on a userspace WireGuard
|
|
// platform that probe reaches the UAPI socket of this same process, so running
|
|
// it later means asking our own device about itself. The result is discarded:
|
|
// a failure here is reported by the accessor that needs the interfaces.
|
|
func (n *Net) warmCache() {
|
|
_, _ = n.Interfaces()
|
|
}
|
|
|
|
// resolveAddr performs DNS resolution with context support and timeout.
|
|
func (n *Net) resolveAddr(network, address string) (netip.AddrPort, error) {
|
|
host, portStr, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return netip.AddrPort{}, err
|
|
}
|
|
|
|
port, err := strconv.Atoi(portStr)
|
|
if err != nil {
|
|
return netip.AddrPort{}, fmt.Errorf("invalid port: %w", err)
|
|
}
|
|
if port < 0 || port > 65535 {
|
|
return netip.AddrPort{}, fmt.Errorf("invalid port: %d", port)
|
|
}
|
|
|
|
ipNet := "ip"
|
|
switch network {
|
|
case "tcp4", "udp4":
|
|
ipNet = "ip4"
|
|
case "tcp6", "udp6":
|
|
ipNet = "ip6"
|
|
}
|
|
|
|
if host == "" {
|
|
addr := netip.IPv4Unspecified()
|
|
if ipNet == "ip6" {
|
|
addr = netip.IPv6Unspecified()
|
|
}
|
|
return netip.AddrPortFrom(addr, uint16(port)), nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(n.ctx, dnsResolveTimeout)
|
|
defer cancel()
|
|
|
|
addrs, err := net.DefaultResolver.LookupNetIP(ctx, ipNet, host)
|
|
if err != nil {
|
|
return netip.AddrPort{}, err
|
|
}
|
|
|
|
if len(addrs) == 0 {
|
|
return netip.AddrPort{}, errNoSuitableAddress
|
|
}
|
|
|
|
return netip.AddrPortFrom(addrs[0], uint16(port)), nil
|
|
}
|
|
|
|
// Interfaces returns a slice of interfaces which are available on the
|
|
// system
|
|
func (n *Net) Interfaces() ([]*transport.Interface, error) {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
iFaces, err := n.freshInterfacesLocked()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return slices.Clone(iFaces), nil
|
|
}
|
|
|
|
// InterfaceByIndex returns the interface specified by index.
|
|
//
|
|
// On Solaris, it returns one of the logical network interfaces
|
|
// sharing the logical data link; for more precision use
|
|
// InterfaceByName.
|
|
func (n *Net) InterfaceByIndex(index int) (*transport.Interface, error) {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
iFaces, err := n.freshInterfacesLocked()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, ifc := range iFaces {
|
|
if ifc.Index == index {
|
|
return ifc, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("%w: index=%d", transport.ErrInterfaceNotFound, index)
|
|
}
|
|
|
|
// InterfaceByName returns the interface specified by name.
|
|
func (n *Net) InterfaceByName(name string) (*transport.Interface, error) {
|
|
n.mu.Lock()
|
|
defer n.mu.Unlock()
|
|
|
|
iFaces, err := n.freshInterfacesLocked()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, ifc := range iFaces {
|
|
if ifc.Name == name {
|
|
return ifc, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("%w: %s", transport.ErrInterfaceNotFound, name)
|
|
}
|
|
|
|
func (n *Net) freshInterfacesLocked() ([]*transport.Interface, error) {
|
|
if time.Since(n.lastUpdate) < updateInterval {
|
|
return n.interfaces, nil
|
|
}
|
|
|
|
if err := n.updateInterfacesLocked(); err != nil {
|
|
return nil, fmt.Errorf("update interfaces: %w", err)
|
|
}
|
|
|
|
return n.interfaces, nil
|
|
}
|
|
|
|
func (n *Net) updateInterfacesLocked() error {
|
|
allIFaces, err := n.iFaceDiscover.iFaces()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
n.interfaces = n.filterInterfaces(allIFaces)
|
|
|
|
n.lastUpdate = time.Now()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (n *Net) filterInterfaces(interfaces []*transport.Interface) []*transport.Interface {
|
|
if n.interfaceFilter == nil {
|
|
return interfaces
|
|
}
|
|
var result []*transport.Interface
|
|
for _, iface := range interfaces {
|
|
if n.interfaceFilter(iface.Name) {
|
|
result = append(result, iface)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ResolveUDPAddr resolves UDP addresses with context support and timeout.
|
|
func (n *Net) ResolveUDPAddr(network, address string) (*net.UDPAddr, error) {
|
|
switch network {
|
|
case "udp", "udp4", "udp6":
|
|
case "":
|
|
network = "udp"
|
|
default:
|
|
return nil, &net.OpError{Op: "resolve", Net: network, Err: net.UnknownNetworkError(network)}
|
|
}
|
|
|
|
addrPort, err := n.resolveAddr(network, address)
|
|
if err != nil {
|
|
return nil, &net.OpError{Op: "resolve", Net: network, Addr: &net.UDPAddr{IP: nil}, Err: err}
|
|
}
|
|
|
|
return net.UDPAddrFromAddrPort(addrPort), nil
|
|
}
|
|
|
|
// ResolveTCPAddr resolves TCP addresses with context support and timeout.
|
|
func (n *Net) ResolveTCPAddr(network, address string) (*net.TCPAddr, error) {
|
|
switch network {
|
|
case "tcp", "tcp4", "tcp6":
|
|
case "":
|
|
network = "tcp"
|
|
default:
|
|
return nil, &net.OpError{Op: "resolve", Net: network, Err: net.UnknownNetworkError(network)}
|
|
}
|
|
|
|
addrPort, err := n.resolveAddr(network, address)
|
|
if err != nil {
|
|
return nil, &net.OpError{Op: "resolve", Net: network, Addr: &net.TCPAddr{IP: nil}, Err: err}
|
|
}
|
|
|
|
return net.TCPAddrFromAddrPort(addrPort), nil
|
|
}
|