parent
7ca911a5c6
commit
eb4eb34f37
@ -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 |
||||
} |
||||
@ -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 |
||||
} |
||||
Loading…
Reference in new issue