From dae2319e119cfe55b7c76888ee4be7f750c5150b Mon Sep 17 00:00:00 2001 From: Jordan Whited Date: Fri, 25 Apr 2025 13:00:00 -0700 Subject: [PATCH] disco: implement CallMeMaybeVia serialization (#15779) This message type is currently unused and considered experimental. Updates tailscale/corp#27502 Signed-off-by: Jordan Whited --- disco/disco.go | 95 +++++++++++++++++++++++++++++++++++++++++++++ disco/disco_test.go | 16 ++++++++ 2 files changed, 111 insertions(+) diff --git a/disco/disco.go b/disco/disco.go index c5aa4ace2..1219a604d 100644 --- a/disco/disco.go +++ b/disco/disco.go @@ -25,6 +25,7 @@ import ( "fmt" "net" "net/netip" + "time" "go4.org/mem" "tailscale.com/types/key" @@ -47,6 +48,7 @@ const ( TypeBindUDPRelayEndpoint = MessageType(0x04) TypeBindUDPRelayEndpointChallenge = MessageType(0x05) TypeBindUDPRelayEndpointAnswer = MessageType(0x06) + TypeCallMeMaybeVia = MessageType(0x07) ) const v0 = byte(0) @@ -93,6 +95,8 @@ func Parse(p []byte) (Message, error) { return parseBindUDPRelayEndpointChallenge(ver, p) case TypeBindUDPRelayEndpointAnswer: return parseBindUDPRelayEndpointAnswer(ver, p) + case TypeCallMeMaybeVia: + return parseCallMeMaybeVia(ver, p) default: return nil, fmt.Errorf("unknown message type 0x%02x", byte(t)) } @@ -392,3 +396,94 @@ func parseBindUDPRelayEndpointAnswer(ver uint8, p []byte) (m *BindUDPRelayEndpoi copy(m.Answer[:], p[:]) return m, nil } + +// CallMeMaybeVia is a message sent only over DERP to request that the recipient +// try to open up a magicsock path back to the sender. The 'Via' in +// CallMeMaybeVia highlights that candidate paths are served through an +// intermediate relay, likely a [tailscale.com/net/udprelay.Server]. +// +// Usage of the candidate paths in magicsock requires a 3-way handshake +// involving [BindUDPRelayEndpoint], [BindUDPRelayEndpointChallenge], and +// [BindUDPRelayEndpointAnswer]. +// +// CallMeMaybeVia mirrors [tailscale.com/net/udprelay.ServerEndpoint], which +// contains field documentation. +// +// The recipient may choose to not open a path back if it's already happy with +// its path. Direct connections, e.g. [CallMeMaybe]-signaled, take priority over +// CallMeMaybeVia paths. +// +// This message type is currently considered experimental and is not yet tied to +// a [tailscale.com/tailcfg.CapabilityVersion]. +type CallMeMaybeVia struct { + // ServerDisco is [tailscale.com/net/udprelay.ServerEndpoint.ServerDisco] + ServerDisco key.DiscoPublic + // LamportID is [tailscale.com/net/udprelay.ServerEndpoint.LamportID] + LamportID uint64 + // VNI is [tailscale.com/net/udprelay.ServerEndpoint.VNI] + VNI uint32 + // BindLifetime is [tailscale.com/net/udprelay.ServerEndpoint.BindLifetime] + BindLifetime time.Duration + // SteadyStateLifetime is [tailscale.com/net/udprelay.ServerEndpoint.SteadyStateLifetime] + SteadyStateLifetime time.Duration + // AddrPorts is [tailscale.com/net/udprelay.ServerEndpoint.AddrPorts] + AddrPorts []netip.AddrPort +} + +const cmmvDataLenMinusEndpoints = key.DiscoPublicRawLen + // ServerDisco + 8 + // LamportID + 4 + // VNI + 8 + // BindLifetime + 8 // SteadyStateLifetime + +func (m *CallMeMaybeVia) AppendMarshal(b []byte) []byte { + endpointsLen := epLength * len(m.AddrPorts) + ret, p := appendMsgHeader(b, TypeCallMeMaybeVia, v0, cmmvDataLenMinusEndpoints+endpointsLen) + disco := m.ServerDisco.AppendTo(nil) + copy(p, disco) + p = p[key.DiscoPublicRawLen:] + binary.BigEndian.PutUint64(p[:8], m.LamportID) + p = p[8:] + binary.BigEndian.PutUint32(p[:4], m.VNI) + p = p[4:] + binary.BigEndian.PutUint64(p[:8], uint64(m.BindLifetime)) + p = p[8:] + binary.BigEndian.PutUint64(p[:8], uint64(m.SteadyStateLifetime)) + p = p[8:] + for _, ipp := range m.AddrPorts { + a := ipp.Addr().As16() + copy(p, a[:]) + binary.BigEndian.PutUint16(p[16:18], ipp.Port()) + p = p[epLength:] + } + return ret +} + +func parseCallMeMaybeVia(ver uint8, p []byte) (m *CallMeMaybeVia, err error) { + m = new(CallMeMaybeVia) + if len(p) < cmmvDataLenMinusEndpoints+epLength || + (len(p)-cmmvDataLenMinusEndpoints)%epLength != 0 || + ver != 0 { + return m, nil + } + m.ServerDisco = key.DiscoPublicFromRaw32(mem.B(p[:key.DiscoPublicRawLen])) + p = p[key.DiscoPublicRawLen:] + m.LamportID = binary.BigEndian.Uint64(p[:8]) + p = p[8:] + m.VNI = binary.BigEndian.Uint32(p[:4]) + p = p[4:] + m.BindLifetime = time.Duration(binary.BigEndian.Uint64(p[:8])) + p = p[8:] + m.SteadyStateLifetime = time.Duration(binary.BigEndian.Uint64(p[:8])) + p = p[8:] + m.AddrPorts = make([]netip.AddrPort, 0, len(p)-cmmvDataLenMinusEndpoints/epLength) + for len(p) > 0 { + var a [16]byte + copy(a[:], p) + m.AddrPorts = append(m.AddrPorts, netip.AddrPortFrom( + netip.AddrFrom16(a).Unmap(), + binary.BigEndian.Uint16(p[16:18]))) + p = p[epLength:] + } + return m, nil +} diff --git a/disco/disco_test.go b/disco/disco_test.go index 751190445..f2a29a744 100644 --- a/disco/disco_test.go +++ b/disco/disco_test.go @@ -9,6 +9,7 @@ import ( "reflect" "strings" "testing" + "time" "go4.org/mem" "tailscale.com/types/key" @@ -106,6 +107,21 @@ func TestMarshalAndParse(t *testing.T) { }, want: "06 00 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f", }, + { + name: "call_me_maybe_via", + m: &CallMeMaybeVia{ + ServerDisco: key.DiscoPublicFromRaw32(mem.B([]byte{1: 1, 2: 2, 30: 30, 31: 31})), + LamportID: 123, + VNI: 456, + BindLifetime: time.Second, + SteadyStateLifetime: time.Minute, + AddrPorts: []netip.AddrPort{ + netip.MustParseAddrPort("1.2.3.4:567"), + netip.MustParseAddrPort("[2001::3456]:789"), + }, + }, + want: "07 00 00 01 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1e 1f 00 00 00 00 00 00 00 7b 00 00 01 c8 00 00 00 00 3b 9a ca 00 00 00 00 0d f8 47 58 00 00 00 00 00 00 00 00 00 00 00 ff ff 01 02 03 04 02 37 20 01 00 00 00 00 00 00 00 00 00 00 00 00 34 56 03 15", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {