wgengine{,/magicsock}: add DERP hooks for filtering+sending packets

Add two small APIs to support out-of-tree projects to exchange custom
signaling messages over DERP without requiring disco protocol
extensions:

- OnDERPRecv callback on magicsock.Options / wgengine.Config: called for
  every non-disco DERP packet before the peer map lookup, allowing callers
  to intercept packets from unknown peers that would otherwise be dropped.

- SendDERPPacketTo method on magicsock.Conn: sends arbitrary bytes to a
  node key via a DERP region, creating the connection if needed. Thin
  wrapper around the existing internal sendAddr.

Also allow netstack.Start to accept a nil LocalBackend for use cases
that wire up TCP/UDP handlers directly without a full LocalBackend.

Updates tailscale/corp#24454

Change-Id: I99a523ef281625b8c0024a963f5f5bf5d8792c17
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-03-11 13:29:06 +00:00
committed by Brad Fitzpatrick
parent 4c7c1091ba
commit 073a9a8c9e
4 changed files with 51 additions and 11 deletions
+13
View File
@@ -725,6 +725,10 @@ func (c *Conn) processDERPReadResult(dm derpReadResult, b []byte) (n int, ep *en
return 0, nil
}
if c.onDERPRecv != nil && c.onDERPRecv(regionID, dm.src, b[:n]) {
return 0, nil
}
var ok bool
c.mu.Lock()
ep, ok = c.peerMap.endpointForNodeKey(dm.src)
@@ -745,6 +749,15 @@ func (c *Conn) processDERPReadResult(dm derpReadResult, b []byte) (n int, ep *en
return n, ep
}
// SendDERPPacketTo sends an arbitrary packet to the given node key via
// the DERP relay for the given region. It creates the DERP connection
// to the region if one doesn't already exist.
func (c *Conn) SendDERPPacketTo(dstKey key.NodePublic, regionID int, pkt []byte) (sent bool, err error) {
return c.sendAddr(
netip.AddrPortFrom(tailcfg.DerpMagicIPAddr, uint16(regionID)),
dstKey, pkt, false, false)
}
// SetOnlyTCP443 set whether the magicsock connection is restricted
// to only using TCP port 443 outbound. If true, no UDP is allowed,
// no STUN checks are performend, etc.
+13 -4
View File
@@ -163,10 +163,11 @@ type Conn struct {
derpActiveFunc func()
idleFunc func() time.Duration // nil means unknown
testOnlyPacketListener nettype.PacketListener
noteRecvActivity func(key.NodePublic) // or nil, see Options.NoteRecvActivity
netMon *netmon.Monitor // must be non-nil
health *health.Tracker // or nil
controlKnobs *controlknobs.Knobs // or nil
noteRecvActivity func(key.NodePublic) // or nil, see Options.NoteRecvActivity
onDERPRecv func(int, key.NodePublic, []byte) bool // or nil, see Options.OnDERPRecv
netMon *netmon.Monitor // must be non-nil
health *health.Tracker // or nil
controlKnobs *controlknobs.Knobs // or nil
// ================================================================
// No locking required to access these fields, either because
@@ -502,6 +503,13 @@ type Options struct {
// leave it zero, in which case a new disco key is generated per
// Tailscale start and kept only in memory.
ForceDiscoKey key.DiscoPrivate
// OnDERPRecv, if non-nil, is called for every non-disco packet
// received from DERP before the peer map lookup. If it returns
// true, the packet is considered handled and is not passed to
// WireGuard. The pkt slice is borrowed and must be copied if
// the callee needs to retain it.
OnDERPRecv func(regionID int, src key.NodePublic, pkt []byte) bool
}
func (o *Options) logf() logger.Logf {
@@ -640,6 +648,7 @@ func NewConn(opts Options) (*Conn, error) {
c.idleFunc = opts.IdleFunc
c.testOnlyPacketListener = opts.TestOnlyPacketListener
c.noteRecvActivity = opts.NoteRecvActivity
c.onDERPRecv = opts.OnDERPRecv
// Set up publishers and subscribers. Subscribe calls must return before
// NewConn otherwise published events can be missed.