wgengine/magicsock: implement more relay handshake disco handling (#15856)

Conn.handleDiscoMessage() now makes a distinction between relay
handshake disco messages and peer disco messages.

Updates tailscale/corp#27502

Signed-off-by: Jordan Whited <jordan@tailscale.com>
This commit is contained in:
Jordan Whited 2025-05-02 09:04:18 -07:00 committed by GitHub
parent 383664b2f7
commit f05347a5bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -1627,13 +1627,13 @@ func (c *Conn) sendDiscoMessage(dst netip.AddrPort, geneveVNI *uint32, dstKey ke
} }
var di *discoInfo var di *discoInfo
switch { switch {
case c.peerMap.knownPeerDiscoKey(dstDisco):
di = c.discoInfoForKnownPeerLocked(dstDisco)
case isRelayHandshakeMsg: case isRelayHandshakeMsg:
// TODO(jwhited): consider caching relay server disco shared keys // TODO(jwhited): consider caching relay server disco shared keys
di = &discoInfo{ di = &discoInfo{
sharedKey: c.discoPrivate.Shared(dstDisco), sharedKey: c.discoPrivate.Shared(dstDisco),
} }
case c.peerMap.knownPeerDiscoKey(dstDisco):
di = c.discoInfoForKnownPeerLocked(dstDisco)
default: default:
// This is an attempt to send to an unknown peer that is not a relay // This is an attempt to send to an unknown peer that is not a relay
// server. This can happen when a call to the current function, which is // server. This can happen when a call to the current function, which is
@ -1768,10 +1768,22 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netip.AddrPort, derpNodeSrc ke
if !isDiscoMsg { if !isDiscoMsg {
return return
} }
var geneve packet.GeneveHeader
if isGeneveEncap { if isGeneveEncap {
// TODO(jwhited): decode Geneve header err := geneve.Decode(msg)
if err != nil {
// Decode only returns an error when 'msg' is too short, and
// 'isGeneveEncap' indicates it's a sufficient length.
c.logf("[unexpected] geneve header decoding error: %v", err)
return
}
msg = msg[packet.GeneveFixedHeaderLength:] msg = msg[packet.GeneveFixedHeaderLength:]
} }
// The control bit should only be set for relay handshake messages
// terminating on or originating from a UDP relay server. We have yet to
// open the encrypted payload to determine the [disco.MessageType], but
// we assert it should be handshake-related.
shouldBeRelayHandshakeMsg := isGeneveEncap && geneve.Control
sender := key.DiscoPublicFromRaw32(mem.B(msg[len(disco.Magic):discoHeaderLen])) sender := key.DiscoPublicFromRaw32(mem.B(msg[len(disco.Magic):discoHeaderLen]))
@ -1790,11 +1802,20 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netip.AddrPort, derpNodeSrc ke
return return
} }
if !c.peerMap.knownPeerDiscoKey(sender) { var di *discoInfo
// Geneve encapsulated disco used for udp relay handshakes are not known switch {
// "peer" keys as they are dynamically discovered by UDP relay endpoint case shouldBeRelayHandshakeMsg:
// allocation or [disco.CallMeMaybeVia] reception. var ok bool
// TODO(jwhited): handle relay handshake messsages instead of early return di, ok = c.discoInfoForRelayHandshakeLocked(sender, geneve.VNI)
if !ok {
if debugDisco() {
c.logf("magicsock: disco: ignoring disco-looking relay handshake frame, no active handshakes with key %v over VNI %d", sender.ShortString(), geneve.VNI)
}
return
}
case c.peerMap.knownPeerDiscoKey(sender):
di = c.discoInfoForKnownPeerLocked(sender)
default:
metricRecvDiscoBadPeer.Add(1) metricRecvDiscoBadPeer.Add(1)
if debugDisco() { if debugDisco() {
c.logf("magicsock: disco: ignoring disco-looking frame, don't know of key %v", sender.ShortString()) c.logf("magicsock: disco: ignoring disco-looking frame, don't know of key %v", sender.ShortString())
@ -1803,7 +1824,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netip.AddrPort, derpNodeSrc ke
} }
isDERP := src.Addr() == tailcfg.DerpMagicIPAddr isDERP := src.Addr() == tailcfg.DerpMagicIPAddr
if !isDERP { if !isDERP && !shouldBeRelayHandshakeMsg {
// Record receive time for UDP transport packets. // Record receive time for UDP transport packets.
pi, ok := c.peerMap.byIPPort[src] pi, ok := c.peerMap.byIPPort[src]
if ok { if ok {
@ -1811,17 +1832,13 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netip.AddrPort, derpNodeSrc ke
} }
} }
// We're now reasonably sure we're expecting communication from // We're now reasonably sure we're expecting communication from 'sender',
// this peer, do the heavy crypto lifting to see what they want. // do the heavy crypto lifting to see what they want.
//
// From here on, peerNode and de are non-nil.
di := c.discoInfoForKnownPeerLocked(sender)
sealedBox := msg[discoHeaderLen:] sealedBox := msg[discoHeaderLen:]
payload, ok := di.sharedKey.Open(sealedBox) payload, ok := di.sharedKey.Open(sealedBox)
if !ok { if !ok {
// This might be have been intended for a previous // This might have been intended for a previous
// disco key. When we restart we get a new disco key // disco key. When we restart we get a new disco key
// and old packets might've still been in flight (or // and old packets might've still been in flight (or
// scheduled). This is particularly the case for LANs // scheduled). This is particularly the case for LANs
@ -1864,6 +1881,19 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netip.AddrPort, derpNodeSrc ke
metricRecvDiscoUDP.Add(1) metricRecvDiscoUDP.Add(1)
} }
if shouldBeRelayHandshakeMsg {
_, ok := dm.(*disco.BindUDPRelayEndpointChallenge)
if !ok {
// We successfully parsed the disco message, but it wasn't a
// challenge. We should never receive other message types
// from a relay server with the Geneve header control bit set.
c.logf("[unexpected] %T packets should not come from a relay server with Geneve control bit set", dm)
return
}
// TODO(jwhited): handle the challenge on the associated [*endpoint]
return
}
switch dm := dm.(type) { switch dm := dm.(type) {
case *disco.Ping: case *disco.Ping:
metricRecvDiscoPing.Add(1) metricRecvDiscoPing.Add(1)
@ -2078,6 +2108,15 @@ func (c *Conn) enqueueCallMeMaybe(derpAddr netip.AddrPort, de *endpoint) {
} }
} }
// discoInfoForRelayHandshakeLocked returns a [*discoInfo] for k and vni if one
// is known, i.e. an [endpoint] has an in-progress handshake with k over vni.
//
// c.mu must be held
func (c *Conn) discoInfoForRelayHandshakeLocked(k key.DiscoPublic, vni uint32) (*discoInfo, bool) {
// TODO(jwhited): implement
return nil, false
}
// discoInfoForKnownPeerLocked returns the previous or new discoInfo for k. // discoInfoForKnownPeerLocked returns the previous or new discoInfo for k.
// //
// Callers must only pass key.DiscoPublic's that are present in and // Callers must only pass key.DiscoPublic's that are present in and