Просмотр исходного кода

disco: new package for parsing & marshaling discovery messages

Updates #483
Brad Fitzpatrick 5 лет назад
Родитель
Сommit
eb4eb34f37
4 измененных файлов с 233 добавлено и 1 удалено
  1. 148 0
      disco/disco.go
  2. 82 0
      disco/disco_test.go
  3. 1 1
      go.mod
  4. 2 0
      go.sum

+ 148 - 0
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
+}

+ 82 - 0
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
+}

+ 1 - 1
go.mod

@@ -29,6 +29,6 @@ require (
 	golang.org/x/sys v0.0.0-20200501052902-10377860bb8e
 	golang.org/x/sys v0.0.0-20200501052902-10377860bb8e
 	golang.org/x/time v0.0.0-20191024005414-555d28b269f0
 	golang.org/x/time v0.0.0-20191024005414-555d28b269f0
 	honnef.co/go/tools v0.0.1-2020.1.4 // indirect
 	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
 	rsc.io/goversion v1.2.0
 )
 )

+ 2 - 0
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=
 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 h1:WjJ6wZhXEWQA3FFSwOjG8tO2q1NDFSqrUwNcTvxwMEQ=
 inet.af/netaddr v0.0.0-20200513162223-787f13e36cbe/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
 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 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
 rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
 rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=