From eb4eb34f374276a8523b13015b3f30c554e2ca43 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 29 Jun 2020 21:54:34 -0700 Subject: [PATCH] disco: new package for parsing & marshaling discovery messages Updates #483 --- disco/disco.go | 148 ++++++++++++++++++++++++++++++++++++++++++++ disco/disco_test.go | 82 ++++++++++++++++++++++++ go.mod | 2 +- go.sum | 2 + 4 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 disco/disco.go create mode 100644 disco/disco_test.go diff --git a/disco/disco.go b/disco/disco.go new file mode 100644 index 000000000..e8bed16fc --- /dev/null +++ b/disco/disco.go @@ -0,0 +1,148 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// 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 secretbox) +// 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-paylod [...]byte +package disco + +import ( + "encoding/binary" + "errors" + "fmt" + "net" + + "inet.af/netaddr" +) + +type MessageType byte + +const ( + TypePing = MessageType(0x01) + TypePong = MessageType(0x02) + TypeCallMeMaybe = MessageType(0x03) +) + +const v0 = byte(0) + +var errShort = errors.New("short message") + +// Parse parses the encrypted part of the message from inside the +// nacl secretbox. +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 { + case TypePing: + return parsePing(ver, p) + case TypePong: + return parsePong(ver, p) + case TypeCallMeMaybe: + return CallMeMaybe{}, nil + 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. + AppendMarshal([]byte) []byte +} + +// 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 [12]byte +} + +func (m *Ping) AppendMarshal(b []byte) []byte { + ret, d := appendMsgHeader(b, TypePing, v0, 12) + copy(d, m.TxID[:]) + return ret +} + +func parsePing(ver uint8, p []byte) (m *Ping, err error) { + if len(p) < 12 { + return nil, errShort + } + m = new(Ping) + copy(m.TxID[:], p) + 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{} + +func (CallMeMaybe) AppendMarshal(b []byte) []byte { + ret, _ := appendMsgHeader(b, TypeCallMeMaybe, v0, 0) + return ret +} + +// 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 netaddr.IPPort // 18 bytes (16+2) on the wire; v4-mapped ipv6 for IPv4 +} + +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.IP.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:] + + m.Src.IP, _ = netaddr.FromStdIP(net.IP(p[:16])) + p = p[16:] + + m.Src.Port = binary.BigEndian.Uint16(p) + return m, nil +} diff --git a/disco/disco_test.go b/disco/disco_test.go new file mode 100644 index 000000000..723b07ab6 --- /dev/null +++ b/disco/disco_test.go @@ -0,0 +1,82 @@ +// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package disco + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "inet.af/netaddr" +) + +func TestMarshalAndParse(t *testing.T) { + tests := []struct { + name string + want string + m Message + }{ + { + name: "ping", + m: &Ping{ + TxID: [12]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + }, + want: "01 00 01 02 03 04 05 06 07 08 09 0a 0b 0c", + }, + { + name: "pong", + m: &Pong{ + TxID: [12]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + Src: mustIPPort("2.3.4.5:1234"), + }, + want: "02 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 00 00 00 00 00 00 00 00 00 00 ff ff 02 03 04 05 04 d2", + }, + { + name: "pongv6", + m: &Pong{ + TxID: [12]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + Src: mustIPPort("[fed0::12]:6666"), + }, + want: "02 00 01 02 03 04 05 06 07 08 09 0a 0b 0c fe d0 00 00 00 00 00 00 00 00 00 00 00 00 00 12 1a 0a", + }, + { + name: "call_me_maybe", + m: CallMeMaybe{}, + want: "03 00", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + foo := []byte("foo") + got := string(tt.m.AppendMarshal(foo)) + if !strings.HasPrefix(got, "foo") { + t.Fatalf("didn't start with foo: got %q", got) + } + got = strings.TrimPrefix(got, "foo") + + gotHex := fmt.Sprintf("% x", got) + if gotHex != tt.want { + t.Fatalf("wrong marshal\n got: %s\nwant: %s\n", gotHex, tt.want) + } + + back, err := Parse([]byte(got)) + if err != nil { + t.Fatalf("parse back: %v", err) + } + if !reflect.DeepEqual(back, tt.m) { + t.Errorf("message in %+v doesn't match Parse back result %+v", tt.m, back) + } + }) + } +} + +func mustIPPort(s string) netaddr.IPPort { + ipp, err := netaddr.ParseIPPort(s) + if err != nil { + panic(err) + } + return ipp +} diff --git a/go.mod b/go.mod index 0d632f8a7..1f85c8ad8 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,6 @@ require ( golang.org/x/sys v0.0.0-20200501052902-10377860bb8e golang.org/x/time v0.0.0-20191024005414-555d28b269f0 honnef.co/go/tools v0.0.1-2020.1.4 // indirect - inet.af/netaddr v0.0.0-20200513162223-787f13e36cbe + inet.af/netaddr v0.0.0-20200629220211-f44a6d25c536 rsc.io/goversion v1.2.0 ) diff --git a/go.sum b/go.sum index e65d5c0a4..3b98b35f8 100644 --- a/go.sum +++ b/go.sum @@ -160,5 +160,7 @@ honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= inet.af/netaddr v0.0.0-20200513162223-787f13e36cbe h1:WjJ6wZhXEWQA3FFSwOjG8tO2q1NDFSqrUwNcTvxwMEQ= inet.af/netaddr v0.0.0-20200513162223-787f13e36cbe/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww= +inet.af/netaddr v0.0.0-20200629220211-f44a6d25c536 h1:XFVw2MVOtmHBidx70M+I6vIw2F6f55UyXvkiKfIrE38= +inet.af/netaddr v0.0.0-20200629220211-f44a6d25c536/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww= rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w= rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=