device: add API for on-demand configuration of peers
Updates tailscale/tailscale#17858 Signed-off-by: Brad Fitzpatrick <brad@danga.com>
This commit is contained in:
parent
70b09a6edd
commit
e924a91e99
5 changed files with 151 additions and 10 deletions
|
|
@ -257,17 +257,17 @@ func (node *trieEntry) remove() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (table *AllowedIPs) Remove(prefix netip.Prefix, peer *Peer) {
|
func (table *AllowedIPs) Remove(prefix netip.Prefix, peer *Peer) {
|
||||||
table.mutex.Lock()
|
table.mu.Lock()
|
||||||
defer table.mutex.Unlock()
|
defer table.mu.Unlock()
|
||||||
var node *trieEntry
|
var node *trieEntry
|
||||||
var exact bool
|
var exact bool
|
||||||
|
|
||||||
if prefix.Addr().Is6() {
|
if prefix.Addr().Is6() {
|
||||||
ip := prefix.Addr().As16()
|
ip := prefix.Addr().As16()
|
||||||
node, exact = table.IPv6.nodePlacement(ip[:], uint8(prefix.Bits()))
|
node, exact = table.ipv6.nodePlacement(ip[:], uint8(prefix.Bits()))
|
||||||
} else if prefix.Addr().Is4() {
|
} else if prefix.Addr().Is4() {
|
||||||
ip := prefix.Addr().As4()
|
ip := prefix.Addr().As4()
|
||||||
node, exact = table.IPv4.nodePlacement(ip[:], uint8(prefix.Bits()))
|
node, exact = table.ipv4.nodePlacement(ip[:], uint8(prefix.Bits()))
|
||||||
} else {
|
} else {
|
||||||
panic(errors.New("removing unknown address type"))
|
panic(errors.New("removing unknown address type"))
|
||||||
}
|
}
|
||||||
|
|
@ -277,10 +277,26 @@ func (table *AllowedIPs) Remove(prefix netip.Prefix, peer *Peer) {
|
||||||
node.remove()
|
node.remove()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (table *AllowedIPs) RemoveByPeer(peer *Peer) {
|
|
||||||
|
// setPeerPrefixes atomically removes all of peer's existing prefixes and adds
|
||||||
|
// the provided ones.
|
||||||
|
func (table *AllowedIPs) setPeerPrefixes(peer *Peer, prefixes []netip.Prefix) {
|
||||||
table.mu.Lock()
|
table.mu.Lock()
|
||||||
defer table.mu.Unlock()
|
defer table.mu.Unlock()
|
||||||
|
|
||||||
|
table.removeByPeerLocked(peer)
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
table.insertLocked(prefix, peer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *AllowedIPs) RemoveByPeer(peer *Peer) {
|
||||||
|
table.mu.Lock()
|
||||||
|
defer table.mu.Unlock()
|
||||||
|
table.removeByPeerLocked(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *AllowedIPs) removeByPeerLocked(peer *Peer) {
|
||||||
var next *list.Element
|
var next *list.Element
|
||||||
for elem := peer.trieEntries.Front(); elem != nil; elem = next {
|
for elem := peer.trieEntries.Front(); elem != nil; elem = next {
|
||||||
next = elem.Next()
|
next = elem.Next()
|
||||||
|
|
@ -291,7 +307,10 @@ func (table *AllowedIPs) RemoveByPeer(peer *Peer) {
|
||||||
func (table *AllowedIPs) Insert(prefix netip.Prefix, peer *Peer) {
|
func (table *AllowedIPs) Insert(prefix netip.Prefix, peer *Peer) {
|
||||||
table.mu.Lock()
|
table.mu.Lock()
|
||||||
defer table.mu.Unlock()
|
defer table.mu.Unlock()
|
||||||
|
table.insertLocked(prefix, peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (table *AllowedIPs) insertLocked(prefix netip.Prefix, peer *Peer) {
|
||||||
if prefix.Addr().Is6() {
|
if prefix.Addr().Is6() {
|
||||||
ip := prefix.Addr().As16()
|
ip := prefix.Addr().As16()
|
||||||
parentIndirection{&table.ipv6, 2}.insert(ip[:], uint8(prefix.Bits()), peer)
|
parentIndirection{&table.ipv6, 2}.insert(ip[:], uint8(prefix.Bits()), peer)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ package device
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/netip"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
@ -59,6 +61,7 @@ type Device struct {
|
||||||
peers struct {
|
peers struct {
|
||||||
sync.RWMutex // protects keyMap
|
sync.RWMutex // protects keyMap
|
||||||
keyMap map[NoisePublicKey]*Peer
|
keyMap map[NoisePublicKey]*Peer
|
||||||
|
lookupFunc PeerLookupFunc // or nil if unused
|
||||||
}
|
}
|
||||||
|
|
||||||
rate struct {
|
rate struct {
|
||||||
|
|
@ -345,13 +348,63 @@ func (device *Device) BatchSize() int {
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LookupPeer looks up a peer by its public key.
|
||||||
|
//
|
||||||
|
// If the peer does not exist and a [PeerLookupFunc] is set (via
|
||||||
|
// [Device.SetPeerLookupFunc]), then that function is used to create the peer
|
||||||
|
// before returning it. Peers created via this mechanism exist only until their
|
||||||
|
// state machine reaches idle, and then the peers are removed.
|
||||||
|
//
|
||||||
|
// If the peer does not exist and no [PeerLookupFunc] is set, nil is returned.
|
||||||
|
//
|
||||||
|
// Use [Device.LookupActivePeer] to only return already-existing peers, without
|
||||||
|
// using a [PeerLookupFunc].
|
||||||
func (device *Device) LookupPeer(pk NoisePublicKey) *Peer {
|
func (device *Device) LookupPeer(pk NoisePublicKey) *Peer {
|
||||||
device.peers.RLock()
|
device.peers.RLock()
|
||||||
defer device.peers.RUnlock()
|
p, ok := device.peers.keyMap[pk]
|
||||||
|
lookupFunc := device.peers.lookupFunc
|
||||||
|
device.peers.RUnlock()
|
||||||
|
if ok || lookupFunc == nil {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
allowedIPs := lookupFunc(pk)
|
||||||
|
if allowedIPs == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := device.NewPeer(pk)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errAddExistingPeer) {
|
||||||
|
device.peers.RLock()
|
||||||
|
defer device.peers.RUnlock()
|
||||||
return device.peers.keyMap[pk]
|
return device.peers.keyMap[pk]
|
||||||
|
}
|
||||||
|
device.log.Errorf("Failed to create peer: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p.SetAllowedIPs(allowedIPs)
|
||||||
|
p.deleteOnIdle = true
|
||||||
|
p.Start()
|
||||||
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LookupActivePeer looks up a peer by its public key.
|
||||||
|
//
|
||||||
|
// Unlike [Device.LookupPeer], this function does not use a [PeerLookupFunc] to
|
||||||
|
// create the peer if it does not already exist.
|
||||||
|
//
|
||||||
|
// If the peer does not exist or was created lazily via [PeerLookupFunc]
|
||||||
|
// and has subsequently idled away, it returns (nil, false).
|
||||||
|
func (device *Device) LookupActivePeer(pk NoisePublicKey) (_ *Peer, ok bool) {
|
||||||
|
device.peers.RLock()
|
||||||
|
defer device.peers.RUnlock()
|
||||||
|
p, ok := device.peers.keyMap[pk]
|
||||||
|
return p, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
var errAddExistingPeer = errors.New("adding existing peer")
|
||||||
|
|
||||||
func (device *Device) RemovePeer(key NoisePublicKey) {
|
func (device *Device) RemovePeer(key NoisePublicKey) {
|
||||||
device.peers.Lock()
|
device.peers.Lock()
|
||||||
defer device.peers.Unlock()
|
defer device.peers.Unlock()
|
||||||
|
|
@ -374,6 +427,41 @@ func (device *Device) RemoveAllPeers() {
|
||||||
device.peers.keyMap = make(map[NoisePublicKey]*Peer)
|
device.peers.keyMap = make(map[NoisePublicKey]*Peer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveMatchingPeers removes all peers for which shouldRemove returns true.
|
||||||
|
//
|
||||||
|
// It returns the number of peers removed.
|
||||||
|
func (device *Device) RemoveMatchingPeers(shouldRemove func(NoisePublicKey) bool) (numRemoved int) {
|
||||||
|
device.peers.Lock()
|
||||||
|
defer device.peers.Unlock()
|
||||||
|
|
||||||
|
for key, peer := range device.peers.keyMap {
|
||||||
|
if shouldRemove(key) {
|
||||||
|
removePeerLocked(device, peer, key)
|
||||||
|
numRemoved++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return numRemoved
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeerLookupFunc is the type of function used to look up peers by public key
|
||||||
|
// when receiving packets for unknown peers.
|
||||||
|
//
|
||||||
|
// If it returns nil, the peer is not known.
|
||||||
|
//
|
||||||
|
// Otherwise, returning non-nil signals that wireguard-go should create the peer
|
||||||
|
// with the provided allowed IPs.
|
||||||
|
//
|
||||||
|
// See [Device.SetPeerLookupFunc] and [Device.LookupPeer].
|
||||||
|
type PeerLookupFunc func(NoisePublicKey) (allowedIPs []netip.Prefix)
|
||||||
|
|
||||||
|
// SetPeerLookupFunc sets the function used to look up peers by public key
|
||||||
|
// when receiving packets for unknown peers.
|
||||||
|
func (device *Device) SetPeerLookupFunc(f PeerLookupFunc) {
|
||||||
|
device.peers.Lock()
|
||||||
|
defer device.peers.Unlock()
|
||||||
|
device.peers.lookupFunc = f
|
||||||
|
}
|
||||||
|
|
||||||
func (device *Device) Close() {
|
func (device *Device) Close() {
|
||||||
device.state.Lock()
|
device.state.Lock()
|
||||||
defer device.state.Unlock()
|
defer device.state.Unlock()
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ package device
|
||||||
import (
|
import (
|
||||||
"container/list"
|
"container/list"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -27,6 +29,12 @@ type Peer struct {
|
||||||
|
|
||||||
queuedOutboundPackets atomic.Int32 // packets in staged+outbound queues, for input backpressure
|
queuedOutboundPackets atomic.Int32 // packets in staged+outbound queues, for input backpressure
|
||||||
|
|
||||||
|
// deleteOnIdle indicates whether the peer should be deleted when idle
|
||||||
|
// because it was auto-created via a Device.PeerLookupFunc.
|
||||||
|
//
|
||||||
|
// This field should only be set once, before the peer is started.
|
||||||
|
deleteOnIdle bool
|
||||||
|
|
||||||
endpoint struct {
|
endpoint struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
val conn.Endpoint
|
val conn.Endpoint
|
||||||
|
|
@ -46,7 +54,9 @@ type Peer struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
state struct {
|
state struct {
|
||||||
sync.Mutex // protects against concurrent Start/Stop
|
sync.Mutex // protects against concurrent Start/Stop, and fields below
|
||||||
|
|
||||||
|
allowedIPs []netip.Prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
queue struct {
|
queue struct {
|
||||||
|
|
@ -89,7 +99,7 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) {
|
||||||
// map public key
|
// map public key
|
||||||
_, ok := device.peers.keyMap[pk]
|
_, ok := device.peers.keyMap[pk]
|
||||||
if ok {
|
if ok {
|
||||||
return nil, errors.New("adding existing peer")
|
return nil, errAddExistingPeer
|
||||||
}
|
}
|
||||||
|
|
||||||
// pre-compute DH
|
// pre-compute DH
|
||||||
|
|
@ -115,6 +125,22 @@ func (device *Device) NewPeer(pk NoisePublicKey) (*Peer, error) {
|
||||||
return peer, nil
|
return peer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetAllowedIPs sets the allowed IP prefixes for this peer.
|
||||||
|
//
|
||||||
|
// If the allowedIPs are unchanged since the last call, this method is a no-op.
|
||||||
|
// It's the caller's responsibility to ensure that no two peers have duplicate
|
||||||
|
// allowed IPs. If so, the last writer wins.
|
||||||
|
func (p *Peer) SetAllowedIPs(allowedIPs []netip.Prefix) {
|
||||||
|
p.state.Lock()
|
||||||
|
defer p.state.Unlock()
|
||||||
|
|
||||||
|
if slices.Equal(p.state.allowedIPs, allowedIPs) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.device.allowedips.setPeerPrefixes(p, allowedIPs)
|
||||||
|
p.state.allowedIPs = slices.Clone(allowedIPs) // avoid retaining caller's slice
|
||||||
|
}
|
||||||
|
|
||||||
// SendBuffers sends buffers to peer. WireGuard packet data in each element of
|
// SendBuffers sends buffers to peer. WireGuard packet data in each element of
|
||||||
// buffers must be preceded by MessageEncapsulatingTransportSize number of
|
// buffers must be preceded by MessageEncapsulatingTransportSize number of
|
||||||
// bytes.
|
// bytes.
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,14 @@ func expiredNewHandshake(peer *Peer) {
|
||||||
func expiredZeroKeyMaterial(peer *Peer) {
|
func expiredZeroKeyMaterial(peer *Peer) {
|
||||||
peer.device.log.Verbosef("%s - Removing all keys, since we haven't received a new one in %d seconds", peer, int((RejectAfterTime * 3).Seconds()))
|
peer.device.log.Verbosef("%s - Removing all keys, since we haven't received a new one in %d seconds", peer, int((RejectAfterTime * 3).Seconds()))
|
||||||
peer.ZeroAndFlushAll()
|
peer.ZeroAndFlushAll()
|
||||||
|
if peer.deleteOnIdle {
|
||||||
|
peer.device.log.Verbosef("%s - Removing idle lazy peer", peer)
|
||||||
|
// Remove the peer from the device in a new goroutine as we're currently
|
||||||
|
// holding timer locks which RemovePeer also needs. This is TOCTOU, but
|
||||||
|
// acceptable since the worst case is we remove the peer and the lazy
|
||||||
|
// peerfunc created it again after. We might lose some packets.
|
||||||
|
go peer.device.RemovePeer(peer.handshake.remoteStatic)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func expiredPersistentKeepalive(peer *Peer) {
|
func expiredPersistentKeepalive(peer *Peer) {
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -1,6 +1,6 @@
|
||||||
module github.com/sagernet/wireguard-go
|
module github.com/sagernet/wireguard-go
|
||||||
|
|
||||||
go 1.20
|
go 1.25
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/sagernet/sing v0.7.10
|
github.com/sagernet/sing v0.7.10
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue