summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--disco/disco.go9
-rw-r--r--tsd/tsd.go4
-rw-r--r--types/key/disco.go10
-rw-r--r--types/key/node.go3
-rw-r--r--wgengine/magicsock/magicsock.go56
-rw-r--r--wgengine/netstack/netstack.go15
-rw-r--r--wgengine/userspace.go35
7 files changed, 109 insertions, 23 deletions
diff --git a/disco/disco.go b/disco/disco.go
index 8f667b262..fef30f3e3 100644
--- a/disco/disco.go
+++ b/disco/disco.go
@@ -57,6 +57,10 @@ const v0 = byte(0)
var errShort = errors.New("short message")
+// ParseHook, if non-nil, is called for unknown message types.
+// If it returns (nil, nil), Parse returns the default "unknown type" error.
+var ParseHook func(t MessageType, ver uint8, p []byte) (Message, error)
+
// LooksLikeDiscoWrapper reports whether p looks like it's a packet
// containing an encrypted disco message.
func LooksLikeDiscoWrapper(p []byte) bool {
@@ -104,6 +108,11 @@ func Parse(p []byte) (Message, error) {
case TypeAllocateUDPRelayEndpointResponse:
return parseAllocateUDPRelayEndpointResponse(ver, p)
default:
+ if ParseHook != nil {
+ if m, err := ParseHook(t, ver, p); m != nil || err != nil {
+ return m, err
+ }
+ }
return nil, fmt.Errorf("unknown message type 0x%02x", byte(t))
}
}
diff --git a/tsd/tsd.go b/tsd/tsd.go
index 57437ddcc..51aa0c762 100644
--- a/tsd/tsd.go
+++ b/tsd/tsd.go
@@ -18,7 +18,10 @@
package tsd
import (
+ "context"
"fmt"
+ "net"
+ "net/netip"
"reflect"
"tailscale.com/control/controlknobs"
@@ -114,6 +117,7 @@ type NetstackImpl interface {
UpdateNetstackIPs(*netmap.NetworkMap)
UpdateIPServiceMappings(netmap.IPServiceMappings)
UpdateActiveVIPServices(views.Slice[string])
+ DialContextTCP(ctx context.Context, ipp netip.AddrPort) (net.Conn, error)
}
// Set is a convenience method to set a subsystem value.
diff --git a/types/key/disco.go b/types/key/disco.go
index f46347c91..7fa476dc3 100644
--- a/types/key/disco.go
+++ b/types/key/disco.go
@@ -42,6 +42,16 @@ func NewDisco() DiscoPrivate {
return ret
}
+// DiscoPrivateFromRaw32 parses a 32-byte raw value as a DiscoPrivate.
+func DiscoPrivateFromRaw32(raw mem.RO) DiscoPrivate {
+ if raw.Len() != 32 {
+ panic("input has wrong size")
+ }
+ var ret DiscoPrivate
+ raw.Copy(ret.k[:])
+ return ret
+}
+
// IsZero reports whether k is the zero value.
func (k DiscoPrivate) IsZero() bool {
return k.Equal(DiscoPrivate{})
diff --git a/types/key/node.go b/types/key/node.go
index 1402aad36..83be593af 100644
--- a/types/key/node.go
+++ b/types/key/node.go
@@ -61,6 +61,9 @@ func NewNode() NodePrivate {
return ret
}
+// Raw32 returns k as 32 raw bytes.
+func (k NodePrivate) Raw32() [32]byte { return k.k }
+
// NodePrivateFromRaw32 parses a 32-byte raw value as a NodePrivate.
//
// Deprecated: only needed to cast from legacy node private key types,
diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go
index 78ffd0cd0..a868cdb75 100644
--- a/wgengine/magicsock/magicsock.go
+++ b/wgengine/magicsock/magicsock.go
@@ -163,10 +163,12 @@ 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
+ netMon *netmon.Monitor // must be non-nil
+ health *health.Tracker // or nil
+ controlKnobs *controlknobs.Knobs // or nil
+ discoMessageHook func(disco.Message, key.DiscoPublic, key.NodePublic) bool
+ acceptDiscoFromUnknownPeer func(key.DiscoPublic) bool
// ================================================================
// No locking required to access these fields, either because
@@ -495,6 +497,16 @@ type Options struct {
// DisablePortMapper, if true, disables the portmapper.
// This is primarily useful in tests.
DisablePortMapper bool
+
+ // DiscoMessageHook, if non-nil, is called when a disco message is
+ // received from a peer. If it returns true, the message is considered
+ // handled and no further processing occurs.
+ DiscoMessageHook func(dm disco.Message, sender key.DiscoPublic, derpNodeSrc key.NodePublic) (handled bool)
+
+ // AcceptDiscoFromUnknownPeer, if non-nil, is called when a disco
+ // message arrives from an unknown peer. If it returns true, the
+ // message is accepted and a discoInfo is created for the sender.
+ AcceptDiscoFromUnknownPeer func(sender key.DiscoPublic) bool
}
func (o *Options) logf() logger.Logf {
@@ -630,6 +642,8 @@ func NewConn(opts Options) (*Conn, error) {
c.idleFunc = opts.IdleFunc
c.testOnlyPacketListener = opts.TestOnlyPacketListener
c.noteRecvActivity = opts.NoteRecvActivity
+ c.discoMessageHook = opts.DiscoMessageHook
+ c.acceptDiscoFromUnknownPeer = opts.AcceptDiscoFromUnknownPeer
// Set up publishers and subscribers. Subscribe calls must return before
// NewConn otherwise published events can be missed.
@@ -2151,6 +2165,8 @@ func (c *Conn) handleDiscoMessage(msg []byte, src epAddr, shouldBeRelayHandshake
}
case c.peerMap.knownPeerDiscoKey(sender):
di = c.discoInfoForKnownPeerLocked(sender)
+ case c.acceptDiscoFromUnknownPeer != nil && c.acceptDiscoFromUnknownPeer(sender):
+ di = c.discoInfoForKnownPeerLocked(sender)
default:
metricRecvDiscoBadPeer.Add(1)
if debugDisco() {
@@ -2233,6 +2249,10 @@ func (c *Conn) handleDiscoMessage(msg []byte, src epAddr, shouldBeRelayHandshake
return
}
+ if c.discoMessageHook != nil && c.discoMessageHook(dm, sender, derpNodeSrc) {
+ return
+ }
+
switch dm := dm.(type) {
case *disco.Ping:
metricRecvDiscoPing.Add(1)
@@ -2635,6 +2655,34 @@ func (c *Conn) discoInfoForKnownPeerLocked(k key.DiscoPublic) *discoInfo {
return di
}
+// SetDiscoKey sets the disco private key used by this Conn.
+func (c *Conn) SetDiscoKey(priv key.DiscoPrivate) {
+ c.discoAtomic.Set(priv)
+}
+
+// SendDiscoMessageOverDERP sends a disco message to a peer identified by
+// its disco and node public keys via the specified DERP region.
+func (c *Conn) SendDiscoMessageOverDERP(dstDisco key.DiscoPublic, dstNode key.NodePublic, derpRegion int, m disco.Message) (sent bool, err error) {
+ dstAddr := netip.AddrPortFrom(tailcfg.DerpMagicIPAddr, uint16(derpRegion))
+
+ c.mu.Lock()
+ if c.closed {
+ c.mu.Unlock()
+ return false, errConnClosed
+ }
+ pkt := make([]byte, 0, 512)
+ pkt = append(pkt, disco.Magic...)
+ pkt = c.discoAtomic.Public().AppendTo(pkt)
+ di := c.discoInfoForKnownPeerLocked(dstDisco)
+ c.mu.Unlock()
+
+ box := di.sharedKey.Seal(m.AppendMarshal(nil))
+ pkt = append(pkt, box...)
+ const isDisco = true
+ const isGeneveEncap = false
+ return c.sendAddr(dstAddr, dstNode, pkt, isDisco, isGeneveEncap)
+}
+
func (c *Conn) SetNetworkUp(up bool) {
c.mu.Lock()
defer c.mu.Unlock()
diff --git a/wgengine/netstack/netstack.go b/wgengine/netstack/netstack.go
index 59c261345..1db9fb26f 100644
--- a/wgengine/netstack/netstack.go
+++ b/wgengine/netstack/netstack.go
@@ -604,14 +604,13 @@ type LocalBackend = any
// Start sets up all the handlers so netstack can start working. Implements
// wgengine.FakeImpl.
func (ns *Impl) Start(b LocalBackend) error {
- if b == nil {
- panic("nil LocalBackend interface")
- }
- lb := b.(*ipnlocal.LocalBackend)
- if lb == nil {
- panic("nil LocalBackend")
+ if b != nil {
+ lb, ok := b.(*ipnlocal.LocalBackend)
+ if !ok || lb == nil {
+ panic("non-nil LocalBackend is not *ipnlocal.LocalBackend")
+ }
+ ns.lb = lb
}
- ns.lb = lb
tcpFwd := tcp.NewForwarder(ns.ipstack, tcpRXBufDefSize, maxInFlightConnectionAttempts(), ns.acceptTCP)
udpFwd := udp.NewForwarder(ns.ipstack, ns.acceptUDPNoICMP)
ns.ipstack.SetTransportProtocolHandler(tcp.ProtocolNumber, ns.wrapTCPProtocolHandler(tcpFwd.HandlePacket))
@@ -906,7 +905,7 @@ func (ns *Impl) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper, gro *gro.
return filter.DropSilently, gro
}
-func (ns *Impl) DialContextTCP(ctx context.Context, ipp netip.AddrPort) (*gonet.TCPConn, error) {
+func (ns *Impl) DialContextTCP(ctx context.Context, ipp netip.AddrPort) (net.Conn, error) {
remoteAddress := tcpip.FullAddress{
NIC: nicID,
Addr: tcpip.AddrFromSlice(ipp.Addr().AsSlice()),
diff --git a/wgengine/userspace.go b/wgengine/userspace.go
index 245ce421f..bf325721f 100644
--- a/wgengine/userspace.go
+++ b/wgengine/userspace.go
@@ -23,6 +23,7 @@ import (
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/control/controlknobs"
+ "tailscale.com/disco"
"tailscale.com/drive"
"tailscale.com/envknob"
"tailscale.com/feature"
@@ -265,6 +266,16 @@ type Config struct {
// Conn25PacketHooks, if non-nil, is used to hook packets for Connectors 2025
// app connector handling logic.
Conn25PacketHooks Conn25PacketHooks
+
+ // DiscoMessageHook, if non-nil, is called when a disco message is
+ // received from a peer. If it returns true, the message is considered
+ // handled and no further processing occurs.
+ DiscoMessageHook func(disco.Message, key.DiscoPublic, key.NodePublic) bool
+
+ // AcceptDiscoFromUnknownPeer, if non-nil, is called when a disco
+ // message arrives from an unknown peer. If it returns true, the
+ // message is accepted.
+ AcceptDiscoFromUnknownPeer func(key.DiscoPublic) bool
}
// NewFakeUserspaceEngine returns a new userspace engine for testing.
@@ -422,17 +433,19 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
e.RequestStatus()
}
magicsockOpts := magicsock.Options{
- EventBus: e.eventBus,
- Logf: logf,
- Port: conf.ListenPort,
- EndpointsFunc: endpointsFn,
- DERPActiveFunc: e.RequestStatus,
- IdleFunc: e.tundev.IdleDuration,
- NetMon: e.netMon,
- HealthTracker: e.health,
- Metrics: conf.Metrics,
- ControlKnobs: conf.ControlKnobs,
- PeerByKeyFunc: e.PeerByKey,
+ EventBus: e.eventBus,
+ Logf: logf,
+ Port: conf.ListenPort,
+ EndpointsFunc: endpointsFn,
+ DERPActiveFunc: e.RequestStatus,
+ IdleFunc: e.tundev.IdleDuration,
+ NetMon: e.netMon,
+ HealthTracker: e.health,
+ Metrics: conf.Metrics,
+ ControlKnobs: conf.ControlKnobs,
+ PeerByKeyFunc: e.PeerByKey,
+ DiscoMessageHook: conf.DiscoMessageHook,
+ AcceptDiscoFromUnknownPeer: conf.AcceptDiscoFromUnknownPeer,
}
if buildfeatures.HasLazyWG {
magicsockOpts.NoteRecvActivity = e.noteRecvActivity