diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 7d25abd2a..f707c6778 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -405,7 +405,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+ tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+ tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal - 💣 tailscale.com/wgengine/wgint from tailscale.com/wgengine + 💣 tailscale.com/wgengine/wgint from tailscale.com/wgengine+ tailscale.com/wgengine/wglog from tailscale.com/wgengine W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router golang.org/x/crypto/argon2 from tailscale.com/tka diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index db8e36d52..94f1c4c79 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -203,11 +203,6 @@ type PeerStatusLite struct { // since this peer was last known to WireGuard. (Tailscale removes peers // from the wireguard peer that are idle.) LastHandshake time.Time - - // HandshakeAttempts is how many failed attempts there have been at - // completing the current WireGuard handshake. This resets to zero on every - // successful handshake. - HandshakeAttempts uint32 } // PeerStatus describes a peer node and its current state. diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 42ffae2e1..db95ec453 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -60,6 +60,7 @@ import ( "tailscale.com/util/testenv" "tailscale.com/util/uniq" "tailscale.com/wgengine/capture" + "tailscale.com/wgengine/wgint" ) const ( @@ -298,6 +299,10 @@ type Conn struct { // onPortUpdate is called with the new port when magicsock rebinds to // a new port. onPortUpdate func(port uint16, network string) + + // getPeerByKey optionally specifies a function to look up a peer's + // wireguard state by its public key. If nil, it's not used. + getPeerByKey func(key.NodePublic) (_ wgint.Peer, ok bool) } // SetDebugLoggingEnabled controls whether spammy debug logging is enabled. @@ -367,6 +372,11 @@ type Options struct { // OnPortUpdate is called with the new port when magicsock rebinds to // a new port. OnPortUpdate func(port uint16, network string) + + // PeerByKeyFunc optionally specifies a function to look up a peer's + // WireGuard state by its public key. If nil, it's not used. + // In regular use, this will be wgengine.(*userspaceEngine).PeerByKey. + PeerByKeyFunc func(key.NodePublic) (_ wgint.Peer, ok bool) } func (o *Options) logf() logger.Logf { @@ -440,6 +450,7 @@ func NewConn(opts Options) (*Conn, error) { } c.netMon = opts.NetMon c.onPortUpdate = opts.OnPortUpdate + c.getPeerByKey = opts.PeerByKeyFunc if err := c.rebind(keepCurrentPort); err != nil { return nil, err diff --git a/wgengine/userspace.go b/wgengine/userspace.go index c5be79517..ae6f71806 100644 --- a/wgengine/userspace.go +++ b/wgengine/userspace.go @@ -1011,25 +1011,30 @@ func (e *userspaceEngine) getStatusCallback() StatusCallback { var ErrEngineClosing = errors.New("engine closing; no status") -func (e *userspaceEngine) getPeerStatusLite(pk key.NodePublic) (status ipnstate.PeerStatusLite, ok bool) { +func (e *userspaceEngine) PeerByKey(pubKey key.NodePublic) (_ wgint.Peer, ok bool) { e.wgLock.Lock() dev := e.wgdev e.wgLock.Unlock() if dev == nil { - return status, false + return wgint.Peer{}, false } - peer := dev.LookupPeer(pk.Raw32()) + peer := dev.LookupPeer(pubKey.Raw32()) if peer == nil { + return wgint.Peer{}, false + } + return wgint.PeerOf(peer), true +} + +func (e *userspaceEngine) getPeerStatusLite(pk key.NodePublic) (status ipnstate.PeerStatusLite, ok bool) { + peer, ok := e.PeerByKey(pk) + if !ok { return status, false } status.NodeKey = pk - status.RxBytes = int64(wgint.PeerRxBytes(peer)) - status.TxBytes = int64(wgint.PeerTxBytes(peer)) - status.HandshakeAttempts = wgint.PeerHandshakeAttempts(peer) - if nano := wgint.PeerLastHandshakeNano(peer); nano != 0 { - status.LastHandshake = time.Unix(0, nano) - } + status.RxBytes = int64(peer.RxBytes()) + status.TxBytes = int64(peer.TxBytes()) + status.LastHandshake = peer.LastHandshake() return status, true } diff --git a/wgengine/watchdog.go b/wgengine/watchdog.go index fd8a2372c..9ff342bcd 100644 --- a/wgengine/watchdog.go +++ b/wgengine/watchdog.go @@ -18,11 +18,13 @@ import ( "tailscale.com/ipn/ipnstate" "tailscale.com/net/dns" "tailscale.com/tailcfg" + "tailscale.com/types/key" "tailscale.com/types/netmap" "tailscale.com/wgengine/capture" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/router" "tailscale.com/wgengine/wgcfg" + "tailscale.com/wgengine/wgint" ) // NewWatchdog wraps an Engine and makes sure that all methods complete @@ -157,3 +159,7 @@ func (e *watchdogEngine) Done() <-chan struct{} { func (e *watchdogEngine) InstallCaptureHook(cb capture.Callback) { e.wrap.InstallCaptureHook(cb) } + +func (e *watchdogEngine) PeerByKey(pubKey key.NodePublic) (_ wgint.Peer, ok bool) { + return e.wrap.PeerByKey(pubKey) +} diff --git a/wgengine/wgengine.go b/wgengine/wgengine.go index d96df05fc..a27db96d9 100644 --- a/wgengine/wgengine.go +++ b/wgengine/wgengine.go @@ -11,11 +11,13 @@ import ( "tailscale.com/ipn/ipnstate" "tailscale.com/net/dns" "tailscale.com/tailcfg" + "tailscale.com/types/key" "tailscale.com/types/netmap" "tailscale.com/wgengine/capture" "tailscale.com/wgengine/filter" "tailscale.com/wgengine/router" "tailscale.com/wgengine/wgcfg" + "tailscale.com/wgengine/wgint" ) // Status is the Engine status. @@ -84,6 +86,10 @@ type Engine interface { // away, sent to the callback registered via SetStatusCallback. RequestStatus() + // PeerByKey returns the WireGuard status of the provided peer. + // If the peer is not found, ok is false. + PeerByKey(key.NodePublic) (_ wgint.Peer, ok bool) + // Close shuts down this wireguard instance, remove any routes // it added, etc. To bring it up again later, you'll need a // new Engine. diff --git a/wgengine/wgint/wgint.go b/wgengine/wgint/wgint.go index ce8109d5f..309113df7 100644 --- a/wgengine/wgint/wgint.go +++ b/wgengine/wgint/wgint.go @@ -8,6 +8,7 @@ package wgint import ( "reflect" "sync/atomic" + "time" "unsafe" "github.com/tailscale/wireguard-go/device" @@ -49,24 +50,59 @@ func getPeerHandshakeAttemptsOffset() uintptr { return field.Offset + field2.Offset } -// PeerLastHandshakeNano returns the last handshake time in nanoseconds since the +// peerLastHandshakeNano returns the last handshake time in nanoseconds since the // unix epoch. -func PeerLastHandshakeNano(peer *device.Peer) int64 { +func peerLastHandshakeNano(peer *device.Peer) int64 { return (*atomic.Int64)(unsafe.Add(unsafe.Pointer(peer), offHandshake)).Load() } -// PeerRxBytes returns the number of bytes received from this peer. -func PeerRxBytes(peer *device.Peer) uint64 { +// peerRxBytes returns the number of bytes received from this peer. +func peerRxBytes(peer *device.Peer) uint64 { return (*atomic.Uint64)(unsafe.Add(unsafe.Pointer(peer), offRxBytes)).Load() } -// PeerTxBytes returns the number of bytes sent to this peer. -func PeerTxBytes(peer *device.Peer) uint64 { +// peerTxBytes returns the number of bytes sent to this peer. +func peerTxBytes(peer *device.Peer) uint64 { return (*atomic.Uint64)(unsafe.Add(unsafe.Pointer(peer), offTxBytes)).Load() } -// PeerHandshakeAttempts returns the number of WireGuard handshake attempts +// peerHandshakeAttempts returns the number of WireGuard handshake attempts // made for the current handshake. It resets to zero before every new handshake. -func PeerHandshakeAttempts(peer *device.Peer) uint32 { +func peerHandshakeAttempts(peer *device.Peer) uint32 { return (*atomic.Uint32)(unsafe.Add(unsafe.Pointer(peer), offHandshakeAttempts)).Load() } + +// Peer is a wrapper around a wireguard-go device.Peer pointer. +type Peer struct { + p *device.Peer +} + +// PeerOf returns a Peer wrapper around a wireguard-go device.Peer. +func PeerOf(p *device.Peer) Peer { + return Peer{p} +} + +// LastHandshake returns the last handshake time. +// +// If the handshake has never happened, it returns the zero value. +func (p Peer) LastHandshake() time.Time { + if n := peerLastHandshakeNano(p.p); n != 0 { + return time.Unix(0, n) + } + return time.Time{} +} + +func (p Peer) IsValid() bool { return p.p != nil } + +// TxBytes returns the number of bytes sent to this peer. +func (p Peer) TxBytes() uint64 { return peerTxBytes(p.p) } + +// RxBytes returns the number of bytes received from this peer. +func (p Peer) RxBytes() uint64 { return peerRxBytes(p.p) } + +// HandshakeAttempts returns the number of failed WireGuard handshake attempts +// made for the current handshake. It resets to zero before every new handshake +// and after a successful handshake. +func (p Peer) HandshakeAttempts() uint32 { + return peerHandshakeAttempts(p.p) +} diff --git a/wgengine/wgint/wgint_test.go b/wgengine/wgint/wgint_test.go index 3e9e85e92..714d2044b 100644 --- a/wgengine/wgint/wgint_test.go +++ b/wgengine/wgint/wgint_test.go @@ -11,16 +11,16 @@ import ( func TestInternalOffsets(t *testing.T) { peer := new(device.Peer) - if got := PeerLastHandshakeNano(peer); got != 0 { + if got := peerLastHandshakeNano(peer); got != 0 { t.Errorf("PeerLastHandshakeNano = %v, want 0", got) } - if got := PeerRxBytes(peer); got != 0 { + if got := peerRxBytes(peer); got != 0 { t.Errorf("PeerRxBytes = %v, want 0", got) } - if got := PeerTxBytes(peer); got != 0 { + if got := peerTxBytes(peer); got != 0 { t.Errorf("PeerTxBytes = %v, want 0", got) } - if got := PeerHandshakeAttempts(peer); got != 0 { + if got := peerHandshakeAttempts(peer); got != 0 { t.Errorf("PeerHandshakeAttempts = %v, want 0", got) } }