Files
Zoltan Papp 28657607c1 [client] Keep interface discovery out of the bind open path (#8226)
* [client] Keep interface discovery out of the bind open path

Since 0.81.0 a userspace-mode agent whose interface name is not covered
by the interface blacklist deadlocks while bringing the device up, and
the daemon, the GUI and the service stop all hang. wireguard-go holds
the device net lock across bind.Open, our bind builds the UDP mux there,
the mux enumerated interfaces in its constructor, and the interface
filter probed every unlisted name through wgctrl. For the agent's own
interface that probe connected to the daemon's own UAPI endpoint and
waited, with no deadline, for a reply the handler could only give after
taking the same lock. #7346 moved the first enumeration from the stdnet
constructor, which ran before the device existed, to this call site.

The mux no longer discovers interfaces when it is constructed. It
records whether its socket is bound to an unspecified address and
GetConn uses that instead of the length of the discovered list, while
GetListenAddresses keeps refreshing on demand, which pion calls during
candidate gathering outside the device locks. The WireGuard detector
also answers for the agent's own interface without probing, so no
filter asks the daemon about itself.

* [client] Take the agent's own interface name in the detector constructor

Every device implementation reports the configured name, so there is no
later name to follow and the setter was state without a reader that
needed it. The detector takes the name once and compares without a lock.

* [client] Create the default mux Net in the constructor

With discovery moved to GetListenAddresses, the nil Net fallback ran
there as well, and the first concurrent callers raced on writing it.
stdnet.NewNet does no discovery, so the constructor can create it before
the mux is shared and the address refresh only reads it.

* [client] Follow the created interface name in the detector

Android ignores the configured interface name and keeps the TUN name
returned by CreateUnmonitoredTUNFromFD, so the detector built from the
configuration would still probe the agent's real interface. The engine
updates the detector with wgInterface.Name() once the interface exists.

* [client] Keep the detector's own interface name immutable

On Android the kernel names the TUN and the name can change again on
renew, while the UAPI socket cannot be opened there at all, so neither
the probe nor a name set after creation can identify the agent's own
interface reliably. Excluding that interface on Android belongs to the
interface discovery the app provides. Everywhere else the configured
name is the created name, so the constructor argument is enough and the
detector needs no setter and no lock on the hot path.
2026-10-16 02:16:45 +02:00

131 lines
3.5 KiB
Go

package stdnet
import (
"sync"
"time"
log "github.com/sirupsen/logrus"
"golang.org/x/sync/singleflight"
"golang.zx2c4.com/wireguard/wgctrl"
)
// wgDetectorTTL bounds how long a cached answer is trusted. An interface rarely
// becomes, or stops being, a WireGuard device, and the window only has to be short
// enough that ICE does not keep gathering candidates on one that just appeared.
const wgDetectorTTL = 1 * time.Second
type wgDetectorEntry struct {
isWireGuard bool
expireAt time.Time
}
// WGDetector answers whether an interface is a WireGuard device, remembering the
// answer for a short while.
//
// The question is asked once per interface for every ICE agent, and an agent is
// created per peer connection attempt, so on a large network the uncached form runs
// constantly. Answering it means opening a wgctrl client, which builds both a kernel
// and a userspace client and resolves the netlink family, and then a round trip that
// usually just reports the device does not exist.
//
// A detector is safe for concurrent use and is meant to be shared by every agent.
type WGDetector struct {
ttl time.Duration
// probe is replaced in tests; it is the call this type exists to avoid repeating.
probe func(string) bool
ownIface string
mu sync.RWMutex
cache map[string]wgDetectorEntry
sf singleflight.Group
}
// NewWGDetector returns a detector with the default time to live.
func NewWGDetector(ownIface string) *WGDetector {
return &WGDetector{
ttl: wgDetectorTTL,
probe: probeWireGuard,
ownIface: ownIface,
cache: make(map[string]wgDetectorEntry),
}
}
// IsWireGuard reports whether the named interface is a WireGuard device. An interface
// it cannot ask about is reported as not being one, which leaves it available to ICE
// exactly as an uncached probe would.
func (d *WGDetector) IsWireGuard(iFace string) bool {
if d == nil {
return probeWireGuard(iFace)
}
if d.isOwnInterface(iFace) {
return true
}
if isWireGuard, ok := d.cached(iFace); ok {
return isWireGuard
}
result, _, _ := d.sf.Do(iFace, func() (interface{}, error) {
// A caller that saw the entry expire may get here after another caller already
// refreshed it and left the singleflight group.
if isWireGuard, ok := d.cached(iFace); ok {
return isWireGuard, nil
}
isWireGuard := d.probe(iFace)
d.store(iFace, isWireGuard)
return isWireGuard, nil
})
return result.(bool)
}
func (d *WGDetector) cached(iFace string) (isWireGuard, ok bool) {
d.mu.RLock()
defer d.mu.RUnlock()
entry, found := d.cache[iFace]
if !found || !time.Now().Before(entry.expireAt) {
return false, false
}
return entry.isWireGuard, true
}
// store records an answer and drops the expired ones, so names of interfaces that came
// and went, such as container veths, do not pile up for the life of the engine.
func (d *WGDetector) store(iFace string, isWireGuard bool) {
now := time.Now()
d.mu.Lock()
defer d.mu.Unlock()
for name, entry := range d.cache {
if !now.Before(entry.expireAt) {
delete(d.cache, name)
}
}
d.cache[iFace] = wgDetectorEntry{isWireGuard: isWireGuard, expireAt: now.Add(d.ttl)}
}
func (d *WGDetector) isOwnInterface(iFace string) bool {
return d.ownIface != "" && iFace == d.ownIface
}
func probeWireGuard(iFace string) bool {
wg, err := wgctrl.New()
if err != nil {
log.Debugf("trying to create a wgctrl client failed with: %v", err)
return false
}
defer func() {
_ = wg.Close()
}()
_, err = wg.Device(iFace)
return err == nil
}