diff options
| -rw-r--r-- | disco/disco.go | 9 | ||||
| -rw-r--r-- | tsd/tsd.go | 4 | ||||
| -rw-r--r-- | types/key/disco.go | 10 | ||||
| -rw-r--r-- | types/key/node.go | 3 | ||||
| -rw-r--r-- | wgengine/magicsock/magicsock.go | 56 | ||||
| -rw-r--r-- | wgengine/netstack/netstack.go | 15 | ||||
| -rw-r--r-- | wgengine/userspace.go | 35 |
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 |
