// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause // Package disco contains the discovery message types. // // A discovery message is: // // Header: // // magic [6]byte // “TS💬” (0x54 53 f0 9f 92 ac) // senderDiscoPub [32]byte // nacl public key // nonce [24]byte // // The recipient then decrypts the bytes following (the nacl box) // and then the inner payload structure is: // // messageType byte (the MessageType constants below) // messageVersion byte (0 for now; but always ignore bytes at the end) // message-payload [...]byte package disco import ( "encoding/binary" "errors" "fmt" "net" "net/netip" "time" "go4.org/mem" "tailscale.com/types/key" ) // Magic is the 6 byte header of all discovery messages. const Magic = "TS💬" // 6 bytes: 0x54 53 f0 9f 92 ac const keyLen = 32 // NonceLen is the length of the nonces used by nacl box. const NonceLen = 24 type MessageType byte const ( TypePing = MessageType(0x01) TypePong = MessageType(0x02) TypeCallMeMaybe = MessageType(0x03) TypeBindUDPRelayEndpoint = MessageType(0x04) TypeBindUDPRelayEndpointChallenge = MessageType(0x05) TypeBindUDPRelayEndpointAnswer = MessageType(0x06) TypeCallMeMaybeVia = MessageType(0x07) TypeAllocateUDPRelayEndpointRequest = MessageType(0x08) TypeAllocateUDPRelayEndpointResponse = MessageType(0x09) ) const v0 = byte(0) var errShort = errors.New("short message") // LooksLikeDiscoWrapper reports whether p looks like it's a packet // containing an encrypted disco message. func LooksLikeDiscoWrapper(p []byte) bool { if len(p) < len(Magic)+keyLen+NonceLen { return false } return string(p[:len(Magic)]) == Magic } // Source returns the slice of p that represents the // disco public key source, and whether p looks like // a disco message. func Source(p []byte) (src []byte, ok bool) { if !LooksLikeDiscoWrapper(p) { return nil, false } return p[len(Magic):][:keyLen], true } // Parse parses the encrypted part of the message from inside the // nacl box. func Parse(p []byte) (Message, error) { if len(p) < 2 { return nil, errShort } t, ver, p := MessageType(p[0]), p[1], p[2:] switch t { // TODO(jwhited): consider using a signature matching encoding.BinaryUnmarshaler case TypePing: return parsePing(ver, p) case TypePong: return parsePong(ver, p) case TypeCallMeMaybe: return parseCallMeMaybe(ver, p) case TypeBindUDPRelayEndpoint: return parseBindUDPRelayEndpoint(ver, p) case TypeBindUDPRelayEndpointChallenge: return parseBindUDPRelayEndpointChallenge(ver, p) case TypeBindUDPRelayEndpointAnswer: return parseBindUDPRelayEndpointAnswer(ver, p) case TypeCallMeMaybeVia: return parseCallMeMaybeVia(ver, p) case TypeAllocateUDPRelayEndpointRequest: return parseAllocateUDPRelayEndpointRequest(ver, p) case TypeAllocateUDPRelayEndpointResponse: return parseAllocateUDPRelayEndpointResponse(ver, p) default: return nil, fmt.Errorf("unknown message type 0x%02x", byte(t)) } } // Message a discovery message. type Message interface { // AppendMarshal appends the message's marshaled representation. // TODO(jwhited): consider using a signature matching encoding.BinaryAppender AppendMarshal([]byte) []byte } // MessageHeaderLen is the length of a message header, 2 bytes for type and version. const MessageHeaderLen = 2 // appendMsgHeader appends two bytes (for t and ver) and then also // dataLen bytes to b, returning the appended slice in all. The // returned data slice is a subslice of all with just dataLen bytes of // where the caller will fill in the data. func appendMsgHeader(b []byte, t MessageType, ver uint8, dataLen int) (all, data []byte) { // TODO: optimize this? all = append(b, make([]byte, dataLen+2)...) all[len(b)] = byte(t) all[len(b)+1] = ver data = all[len(b)+2:] return } type Ping struct { // TxID is a random client-generated per-ping transaction ID. TxID [12]byte // NodeKey is allegedly the ping sender's wireguard public key. // Old clients (~1.16.0 and earlier) don't send this field. // It shouldn't be trusted by itself, but can be combined with // netmap data to reduce the discokey:nodekey relation from 1:N to // 1:1. NodeKey key.NodePublic // Padding is the number of 0 bytes at the end of the // message. (It's used to probe path MTU.) Padding int } // PingLen is the length of a marshalled ping message, without the message // header or padding. const PingLen = 12 + key.NodePublicRawLen func (m *Ping) AppendMarshal(b []byte) []byte { dataLen := 12 hasKey := !m.NodeKey.IsZero() if hasKey { dataLen += key.NodePublicRawLen } ret, d := appendMsgHeader(b, TypePing, v0, dataLen+m.Padding) n := copy(d, m.TxID[:]) if hasKey { m.NodeKey.AppendTo(d[:n]) } return ret } func parsePing(ver uint8, p []byte) (m *Ping, err error) { if len(p) < 12 { return nil, errShort } m = new(Ping) m.Padding = len(p) p = p[copy(m.TxID[:], p):] m.Padding -= 12 // Deliberately lax on longer-than-expected messages, for future // compatibility. if len(p) >= key.NodePublicRawLen { m.NodeKey = key.NodePublicFromRaw32(mem.B(p[:key.NodePublicRawLen])) m.Padding -= key.NodePublicRawLen } return m, nil } // CallMeMaybe is a message sent only over DERP to request that the recipient try // to open up a magicsock path back to the sender. // // The sender should've already sent UDP packets to the peer to open // up the stateful firewall mappings inbound. // // The recipient may choose to not open a path back, if it's already // happy with its path. But usually it will. type CallMeMaybe struct { // MyNumber is what the peer believes its endpoints are. // // Prior to Tailscale 1.4, the endpoints were exchanged purely // between nodes and the control server. // // Starting with Tailscale 1.4, clients advertise their endpoints. // Older clients won't use this, but newer clients should // use any endpoints in here that aren't included from control. // // Control might have sent stale endpoints if the client was idle // before contacting us. In that case, the client likely did a STUN // request immediately before sending the CallMeMaybe to recreate // their NAT port mapping, and that new good endpoint is included // in this field, but might not yet be in control's endpoints. // (And in the future, control will stop distributing endpoints // when clients are suitably new.) MyNumber []netip.AddrPort } const epLength = 16 + 2 // 16 byte IP address + 2 byte port func (m *CallMeMaybe) AppendMarshal(b []byte) []byte { ret, p := appendMsgHeader(b, TypeCallMeMaybe, v0, epLength*len(m.MyNumber)) for _, ipp := range m.MyNumber { a := ipp.Addr().As16() copy(p[:], a[:]) binary.BigEndian.PutUint16(p[16:], ipp.Port()) p = p[epLength:] } return ret } func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) { m = new(CallMeMaybe) if len(p)%epLength != 0 || ver != 0 || len(p) == 0 { return m, nil } m.MyNumber = make([]netip.AddrPort, 0, len(p)/epLength) for len(p) > 0 { var a [16]byte copy(a[:], p) m.MyNumber = append(m.MyNumber, netip.AddrPortFrom( netip.AddrFrom16(a).Unmap(), binary.BigEndian.Uint16(p[16:18]))) p = p[epLength:] } return m, nil } // Pong is a response a Ping. // // It includes the sender's source IP + port, so it's effectively a // STUN response. type Pong struct { TxID [12]byte Src netip.AddrPort // 18 bytes (16+2) on the wire; v4-mapped ipv6 for IPv4 } // pongLen is the length of a marshalled pong message, without the message // header or padding. const pongLen = 12 + 16 + 2 func (m *Pong) AppendMarshal(b []byte) []byte { ret, d := appendMsgHeader(b, TypePong, v0, pongLen) d = d[copy(d, m.TxID[:]):] ip16 := m.Src.Addr().As16() d = d[copy(d, ip16[:]):] binary.BigEndian.PutUint16(d, m.Src.Port()) return ret } func parsePong(ver uint8, p []byte) (m *Pong, err error) { if len(p) < pongLen { return nil, errShort } m = new(Pong) copy(m.TxID[:], p) p = p[12:] srcIP, _ := netip.AddrFromSlice(net.IP(p[:16])) p = p[16:] port := binary.BigEndian.Uint16(p) m.Src = netip.AddrPortFrom(srcIP.Unmap(), port) return m, nil } // MessageSummary returns a short summary of m for logging purposes. func MessageSummary(m Message) string { switch m := m.(type) { case *Ping: return fmt.Sprintf("ping tx=%x padding=%v", m.TxID[:6], m.Padding) case *Pong: return fmt.Sprintf("pong tx=%x", m.TxID[:6]) case *CallMeMaybe: return "call-me-maybe" case *BindUDPRelayEndpoint: return "bind-udp-relay-endpoint" case *BindUDPRelayEndpointChallenge: return "bind-udp-relay-endpoint-challenge" case *BindUDPRelayEndpointAnswer: return "bind-udp-relay-endpoint-answer" default: return fmt.Sprintf("%#v", m) } } // BindUDPRelayHandshakeState represents the state of the 3-way bind handshake // between UDP relay client and UDP relay server. Its potential values include // those for both participants, UDP relay client and UDP relay server. A UDP // relay server implementation can be found in net/udprelay. This is currently // considered experimental. type BindUDPRelayHandshakeState int const ( // BindUDPRelayHandshakeStateInit represents the initial state prior to any // message being transmitted. BindUDPRelayHandshakeStateInit BindUDPRelayHandshakeState = iota // BindUDPRelayHandshakeStateBindSent is the first client state after // transmitting a BindUDPRelayEndpoint message to a UDP relay server. BindUDPRelayHandshakeStateBindSent // BindUDPRelayHandshakeStateChallengeSent is the first server state after // receiving a BindUDPRelayEndpoint message from a UDP relay client and // replying with a BindUDPRelayEndpointChallenge. BindUDPRelayHandshakeStateChallengeSent // BindUDPRelayHandshakeStateAnswerSent is a client state that is entered // after transmitting a BindUDPRelayEndpointAnswer message towards a UDP // relay server in response to a BindUDPRelayEndpointChallenge message. BindUDPRelayHandshakeStateAnswerSent // BindUDPRelayHandshakeStateAnswerReceived is a server state that is // entered after it has received a correct BindUDPRelayEndpointAnswer // message from a UDP relay client in response to a // BindUDPRelayEndpointChallenge message. BindUDPRelayHandshakeStateAnswerReceived ) // bindUDPRelayEndpointCommonLen is the length of a marshalled // [BindUDPRelayEndpointCommon], without the message header. const bindUDPRelayEndpointCommonLen = 72 // BindUDPRelayChallengeLen is the length of the Challenge field carried in // [BindUDPRelayEndpointChallenge] & [BindUDPRelayEndpointAnswer] messages. const BindUDPRelayChallengeLen = 32 // BindUDPRelayEndpointCommon contains fields that are common across all 3 // UDP relay handshake message types. All 4 field values are expected to be // consistent for the lifetime of a handshake besides Challenge, which is // irrelevant in a [BindUDPRelayEndpoint] message. type BindUDPRelayEndpointCommon struct { // VNI is the Geneve header Virtual Network Identifier field value, which // must match this disco-sealed value upon reception. If they are // non-matching it indicates the cleartext Geneve header was tampered with // and/or mangled. VNI uint32 // Generation represents the handshake generation. Clients must set a new, // nonzero value at the start of every handshake. Generation uint32 // RemoteKey is the disco key of the remote peer participating over this // relay endpoint. RemoteKey key.DiscoPublic // Challenge is set by the server in a [BindUDPRelayEndpointChallenge] // message, and expected to be echoed back by the client in a // [BindUDPRelayEndpointAnswer] message. Its value is irrelevant in a // [BindUDPRelayEndpoint] message, where it simply serves a padding purpose // ensuring all handshake messages are equal in size. Challenge [BindUDPRelayChallengeLen]byte } // encode encodes m in b. b must be at least bindUDPRelayEndpointCommonLen bytes // long. func (m *BindUDPRelayEndpointCommon) encode(b []byte) { binary.BigEndian.PutUint32(b, m.VNI) b = b[4:] binary.BigEndian.PutUint32(b, m.Generation) b = b[4:] m.RemoteKey.AppendTo(b[:0]) b = b[key.DiscoPublicRawLen:] copy(b, m.Challenge[:]) } // decode decodes m from b. func (m *BindUDPRelayEndpointCommon) decode(b []byte) error { if len(b) < bindUDPRelayEndpointCommonLen { return errShort } m.VNI = binary.BigEndian.Uint32(b) b = b[4:] m.Generation = binary.BigEndian.Uint32(b) b = b[4:] m.RemoteKey = key.DiscoPublicFromRaw32(mem.B(b[:key.DiscoPublicRawLen])) b = b[key.DiscoPublicRawLen:] copy(m.Challenge[:], b[:BindUDPRelayChallengeLen]) return nil } // BindUDPRelayEndpoint is the first messaged transmitted from UDP relay client // towards UDP relay server as part of the 3-way bind handshake. type BindUDPRelayEndpoint struct { BindUDPRelayEndpointCommon } func (m *BindUDPRelayEndpoint) AppendMarshal(b []byte) []byte { ret, d := appendMsgHeader(b, TypeBindUDPRelayEndpoint, v0, bindUDPRelayEndpointCommonLen) m.BindUDPRelayEndpointCommon.encode(d) return ret } func parseBindUDPRelayEndpoint(ver uint8, p []byte) (m *BindUDPRelayEndpoint, err error) { m = new(BindUDPRelayEndpoint) err = m.BindUDPRelayEndpointCommon.decode(p) if err != nil { return nil, err } return m, nil } // BindUDPRelayEndpointChallenge is transmitted from UDP relay server towards // UDP relay client in response to a BindUDPRelayEndpoint message as part of the // 3-way bind handshake. type BindUDPRelayEndpointChallenge struct { BindUDPRelayEndpointCommon } func (m *BindUDPRelayEndpointChallenge) AppendMarshal(b []byte) []byte { ret, d := appendMsgHeader(b, TypeBindUDPRelayEndpointChallenge, v0, bindUDPRelayEndpointCommonLen) m.BindUDPRelayEndpointCommon.encode(d) return ret } func parseBindUDPRelayEndpointChallenge(ver uint8, p []byte) (m *BindUDPRelayEndpointChallenge, err error) { m = new(BindUDPRelayEndpointChallenge) err = m.BindUDPRelayEndpointCommon.decode(p) if err != nil { return nil, err } return m, nil } // BindUDPRelayEndpointAnswer is transmitted from UDP relay client to UDP relay // server in response to a BindUDPRelayEndpointChallenge message. type BindUDPRelayEndpointAnswer struct { BindUDPRelayEndpointCommon } func (m *BindUDPRelayEndpointAnswer) AppendMarshal(b []byte) []byte { ret, d := appendMsgHeader(b, TypeBindUDPRelayEndpointAnswer, v0, bindUDPRelayEndpointCommonLen) m.BindUDPRelayEndpointCommon.encode(d) return ret } func parseBindUDPRelayEndpointAnswer(ver uint8, p []byte) (m *BindUDPRelayEndpointAnswer, err error) { m = new(BindUDPRelayEndpointAnswer) err = m.BindUDPRelayEndpointCommon.decode(p) if err != nil { return nil, err } return m, nil } // AllocateUDPRelayEndpointRequest is a message sent only over DERP to request // allocation of a relay endpoint on a [tailscale.com/net/udprelay.Server] type AllocateUDPRelayEndpointRequest struct { // ClientDisco are the Disco public keys of the clients that should be // permitted to handshake with the endpoint. ClientDisco [2]key.DiscoPublic // Generation represents the allocation request generation. The server must // echo it back in the [AllocateUDPRelayEndpointResponse] to enable request // and response alignment client-side. Generation uint32 } // allocateUDPRelayEndpointRequestLen is the length of a marshaled // [AllocateUDPRelayEndpointRequest] message without the message header. const allocateUDPRelayEndpointRequestLen = key.DiscoPublicRawLen*2 + // ClientDisco 4 // Generation func (m *AllocateUDPRelayEndpointRequest) AppendMarshal(b []byte) []byte { ret, p := appendMsgHeader(b, TypeAllocateUDPRelayEndpointRequest, v0, allocateUDPRelayEndpointRequestLen) for i := 0; i < len(m.ClientDisco); i++ { disco := m.ClientDisco[i].AppendTo(nil) copy(p, disco) p = p[key.DiscoPublicRawLen:] } binary.BigEndian.PutUint32(p, m.Generation) return ret } func parseAllocateUDPRelayEndpointRequest(ver uint8, p []byte) (m *AllocateUDPRelayEndpointRequest, err error) { m = new(AllocateUDPRelayEndpointRequest) if ver != 0 { return } if len(p) < allocateUDPRelayEndpointRequestLen { return m, errShort } for i := 0; i < len(m.ClientDisco); i++ { m.ClientDisco[i] = key.DiscoPublicFromRaw32(mem.B(p[:key.DiscoPublicRawLen])) p = p[key.DiscoPublicRawLen:] } m.Generation = binary.BigEndian.Uint32(p) return m, nil } // AllocateUDPRelayEndpointResponse is a message sent only over DERP in response // to a [AllocateUDPRelayEndpointRequest]. type AllocateUDPRelayEndpointResponse struct { // Generation represents the allocation request generation. The server must // echo back the [AllocateUDPRelayEndpointRequest.Generation] here to enable // request and response alignment client-side. Generation uint32 UDPRelayEndpoint } func (m *AllocateUDPRelayEndpointResponse) AppendMarshal(b []byte) []byte { endpointsLen := epLength * len(m.AddrPorts) generationLen := 4 ret, d := appendMsgHeader(b, TypeAllocateUDPRelayEndpointResponse, v0, generationLen+udpRelayEndpointLenMinusAddrPorts+endpointsLen) binary.BigEndian.PutUint32(d, m.Generation) m.encode(d[4:]) return ret } func parseAllocateUDPRelayEndpointResponse(ver uint8, p []byte) (m *AllocateUDPRelayEndpointResponse, err error) { m = new(AllocateUDPRelayEndpointResponse) if ver != 0 { return m, nil } if len(p) < 4 { return m, errShort } m.Generation = binary.BigEndian.Uint32(p) err = m.decode(p[4:]) return m, err } const udpRelayEndpointLenMinusAddrPorts = key.DiscoPublicRawLen + // ServerDisco (key.DiscoPublicRawLen * 2) + // ClientDisco 8 + // LamportID 4 + // VNI 8 + // BindLifetime 8 // SteadyStateLifetime // UDPRelayEndpoint is a mirror of [tailscale.com/net/udprelay/endpoint.ServerEndpoint], // refer to it for field documentation. [UDPRelayEndpoint] is carried in both // [CallMeMaybeVia] and [AllocateUDPRelayEndpointResponse] messages. type UDPRelayEndpoint struct { // ServerDisco is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.ServerDisco] ServerDisco key.DiscoPublic // ClientDisco is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.ClientDisco] ClientDisco [2]key.DiscoPublic // LamportID is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.LamportID] LamportID uint64 // VNI is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.VNI] VNI uint32 // BindLifetime is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.BindLifetime] BindLifetime time.Duration // SteadyStateLifetime is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.SteadyStateLifetime] SteadyStateLifetime time.Duration // AddrPorts is [tailscale.com/net/udprelay/endpoint.ServerEndpoint.AddrPorts] AddrPorts []netip.AddrPort } // encode encodes m in b. b must be at least [udpRelayEndpointLenMinusAddrPorts] // + [epLength] * len(m.AddrPorts) bytes long. func (m *UDPRelayEndpoint) encode(b []byte) { disco := m.ServerDisco.AppendTo(nil) copy(b, disco) b = b[key.DiscoPublicRawLen:] for i := 0; i < len(m.ClientDisco); i++ { disco = m.ClientDisco[i].AppendTo(nil) copy(b, disco) b = b[key.DiscoPublicRawLen:] } binary.BigEndian.PutUint64(b[:8], m.LamportID) b = b[8:] binary.BigEndian.PutUint32(b[:4], m.VNI) b = b[4:] binary.BigEndian.PutUint64(b[:8], uint64(m.BindLifetime)) b = b[8:] binary.BigEndian.PutUint64(b[:8], uint64(m.SteadyStateLifetime)) b = b[8:] for _, ipp := range m.AddrPorts { a := ipp.Addr().As16() copy(b, a[:]) binary.BigEndian.PutUint16(b[16:18], ipp.Port()) b = b[epLength:] } } // decode decodes m from b. func (m *UDPRelayEndpoint) decode(b []byte) error { if len(b) < udpRelayEndpointLenMinusAddrPorts+epLength || (len(b)-udpRelayEndpointLenMinusAddrPorts)%epLength != 0 { return errShort } m.ServerDisco = key.DiscoPublicFromRaw32(mem.B(b[:key.DiscoPublicRawLen])) b = b[key.DiscoPublicRawLen:] for i := 0; i < len(m.ClientDisco); i++ { m.ClientDisco[i] = key.DiscoPublicFromRaw32(mem.B(b[:key.DiscoPublicRawLen])) b = b[key.DiscoPublicRawLen:] } m.LamportID = binary.BigEndian.Uint64(b[:8]) b = b[8:] m.VNI = binary.BigEndian.Uint32(b[:4]) b = b[4:] m.BindLifetime = time.Duration(binary.BigEndian.Uint64(b[:8])) b = b[8:] m.SteadyStateLifetime = time.Duration(binary.BigEndian.Uint64(b[:8])) b = b[8:] m.AddrPorts = make([]netip.AddrPort, 0, len(b)-udpRelayEndpointLenMinusAddrPorts/epLength) for len(b) > 0 { var a [16]byte copy(a[:], b) m.AddrPorts = append(m.AddrPorts, netip.AddrPortFrom( netip.AddrFrom16(a).Unmap(), binary.BigEndian.Uint16(b[16:18]))) b = b[epLength:] } return 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/endpoint.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. type CallMeMaybeVia struct { UDPRelayEndpoint } func (m *CallMeMaybeVia) AppendMarshal(b []byte) []byte { endpointsLen := epLength * len(m.AddrPorts) ret, p := appendMsgHeader(b, TypeCallMeMaybeVia, v0, udpRelayEndpointLenMinusAddrPorts+endpointsLen) m.encode(p) return ret } func parseCallMeMaybeVia(ver uint8, p []byte) (m *CallMeMaybeVia, err error) { m = new(CallMeMaybeVia) if ver != 0 { return m, nil } err = m.decode(p) return m, err }