mirror of
https://github.com/tailscale/tailscale.git
synced 2026-09-13 14:29:49 -04:00
Clients can advertise an opaque app name in their ClientInfo but the server previously did nothing with it. Constrain app names to at most 32 bytes of printable ASCII, enforced both in derp.NewClient and by the server when it parses the ClientInfo. Extend the peerPresent frame, following its existing pattern of appending optional fields, with a length-prefixed app name after the flags byte, so trusted mesh watchers (other DERP nodes and stats tools) can attribute connections by app. Old clients ignore the extra bytes; old servers send frames without them. Also add a derper --disallow-app-names flag taking a comma-separated list of app names whose connections are refused, except for trusted mesh peers. Updates tailscale/corp#24454 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Change-Id: I6e721258675145833aafa1355fabf7fc05a5a204
300 lines
7.2 KiB
Go
300 lines
7.2 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package derp
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"net"
|
|
"net/netip"
|
|
"reflect"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"go4.org/mem"
|
|
"tailscale.com/tstest"
|
|
"tailscale.com/types/key"
|
|
)
|
|
|
|
type dummyNetConn struct {
|
|
net.Conn
|
|
}
|
|
|
|
func (dummyNetConn) SetReadDeadline(time.Time) error { return nil }
|
|
|
|
func TestClientRecv(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input []byte
|
|
want any
|
|
}{
|
|
{
|
|
name: "ping",
|
|
input: []byte{
|
|
byte(FramePing), 0, 0, 0, 8,
|
|
1, 2, 3, 4, 5, 6, 7, 8,
|
|
},
|
|
want: PingMessage{1, 2, 3, 4, 5, 6, 7, 8},
|
|
},
|
|
{
|
|
name: "pong",
|
|
input: []byte{
|
|
byte(FramePong), 0, 0, 0, 8,
|
|
1, 2, 3, 4, 5, 6, 7, 8,
|
|
},
|
|
want: PongMessage{1, 2, 3, 4, 5, 6, 7, 8},
|
|
},
|
|
{
|
|
name: "health_bad",
|
|
input: []byte{
|
|
byte(FrameHealth), 0, 0, 0, 3,
|
|
byte('B'), byte('A'), byte('D'),
|
|
},
|
|
want: HealthMessage{Problem: "BAD"},
|
|
},
|
|
{
|
|
name: "health_ok",
|
|
input: []byte{
|
|
byte(FrameHealth), 0, 0, 0, 0,
|
|
},
|
|
want: HealthMessage{},
|
|
},
|
|
{
|
|
name: "server_restarting",
|
|
input: []byte{
|
|
byte(FrameRestarting), 0, 0, 0, 8,
|
|
0, 0, 0, 1,
|
|
0, 0, 0, 2,
|
|
},
|
|
want: ServerRestartingMessage{
|
|
ReconnectIn: 1 * time.Millisecond,
|
|
TryFor: 2 * time.Millisecond,
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Client{
|
|
nc: dummyNetConn{},
|
|
br: bufio.NewReader(bytes.NewReader(tt.input)),
|
|
logf: t.Logf,
|
|
clock: &tstest.Clock{},
|
|
}
|
|
got, err := c.Recv()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("got %#v; want %#v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestClientRecvPeerPresent tests that the client can parse peerPresent
|
|
// frames from servers of various eras: old servers that send fewer fields
|
|
// than the client knows about, and newer servers that send trailing fields
|
|
// the client doesn't know about, which it must ignore. This matters during
|
|
// rollouts of new DERP servers, when a region's meshed nodes and watchers
|
|
// run a mix of versions.
|
|
func TestClientRecvPeerPresent(t *testing.T) {
|
|
keyb := bytes.Repeat([]byte{1}, KeyLen)
|
|
k := key.NodePublicFromRaw32(mem.B(keyb))
|
|
ipPort := []byte{
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 1, 2, 3, 4, // ::ffff:1.2.3.4
|
|
0x12, 0x34, // port 4660
|
|
}
|
|
wantIPPort := netip.MustParseAddrPort("1.2.3.4:4660")
|
|
|
|
frame := func(fields ...[]byte) []byte {
|
|
b := []byte{byte(FramePeerPresent), 0, 0, 0, 0}
|
|
for _, f := range fields {
|
|
b = append(b, f...)
|
|
}
|
|
b[4] = byte(len(b) - FrameHeaderLen)
|
|
return b
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
input []byte
|
|
want PeerPresentMessage
|
|
}{
|
|
{
|
|
name: "key_only_from_ancient_server",
|
|
input: frame(keyb),
|
|
want: PeerPresentMessage{Key: k},
|
|
},
|
|
{
|
|
name: "ip_port_from_old_server",
|
|
input: frame(keyb, ipPort),
|
|
want: PeerPresentMessage{Key: k, IPPort: wantIPPort},
|
|
},
|
|
{
|
|
name: "flags_from_current_server",
|
|
input: frame(keyb, ipPort, []byte{PeerPresentIsRegular}),
|
|
want: PeerPresentMessage{Key: k, IPPort: wantIPPort, Flags: PeerPresentIsRegular},
|
|
},
|
|
{
|
|
name: "app_name_from_current_server",
|
|
input: frame(keyb, ipPort, []byte{PeerPresentIsRegular}, []byte{3, 'a', 'b', 'c'}),
|
|
want: PeerPresentMessage{Key: k, IPPort: wantIPPort, Flags: PeerPresentIsRegular, AppName: "abc"},
|
|
},
|
|
{
|
|
name: "extra_fields_from_newer_server",
|
|
// A hypothetical newer server sending fields this client
|
|
// doesn't know about. They must be ignored.
|
|
input: frame(keyb, ipPort, []byte{PeerPresentIsRegular},
|
|
[]byte{3, 'a', 'b', 'c'}, []byte{0xde, 0xad}),
|
|
want: PeerPresentMessage{Key: k, IPPort: wantIPPort, Flags: PeerPresentIsRegular, AppName: "abc"},
|
|
},
|
|
{
|
|
name: "truncated_app_name_ignored",
|
|
// A buggy or malicious server sending an app name length
|
|
// that exceeds the frame.
|
|
input: frame(keyb, ipPort, []byte{PeerPresentIsRegular}, []byte{200, 'a', 'b', 'c'}),
|
|
want: PeerPresentMessage{Key: k, IPPort: wantIPPort, Flags: PeerPresentIsRegular},
|
|
},
|
|
{
|
|
name: "invalid_app_name_ignored",
|
|
input: frame(keyb, ipPort, []byte{PeerPresentIsRegular}, []byte{3, 0x01, 0x02, 0x03}),
|
|
want: PeerPresentMessage{Key: k, IPPort: wantIPPort, Flags: PeerPresentIsRegular},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
c := &Client{
|
|
nc: dummyNetConn{},
|
|
br: bufio.NewReader(bytes.NewReader(tt.input)),
|
|
logf: t.Logf,
|
|
clock: &tstest.Clock{},
|
|
}
|
|
m, err := c.Recv()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, ok := m.(PeerPresentMessage)
|
|
if !ok {
|
|
t.Fatalf("message type = %T; want PeerPresentMessage", m)
|
|
}
|
|
if got != tt.want {
|
|
t.Errorf("got %+v; want %+v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestClientSendPing(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
c := &Client{
|
|
bw: bufio.NewWriter(&buf),
|
|
}
|
|
if err := c.SendPing([8]byte{1, 2, 3, 4, 5, 6, 7, 8}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []byte{
|
|
byte(FramePing), 0, 0, 0, 8,
|
|
1, 2, 3, 4, 5, 6, 7, 8,
|
|
}
|
|
if !bytes.Equal(buf.Bytes(), want) {
|
|
t.Errorf("unexpected output\nwrote: % 02x\n want: % 02x", buf.Bytes(), want)
|
|
}
|
|
}
|
|
|
|
func TestClientSendPong(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
c := &Client{
|
|
bw: bufio.NewWriter(&buf),
|
|
}
|
|
if err := c.SendPong([8]byte{1, 2, 3, 4, 5, 6, 7, 8}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []byte{
|
|
byte(FramePong), 0, 0, 0, 8,
|
|
1, 2, 3, 4, 5, 6, 7, 8,
|
|
}
|
|
if !bytes.Equal(buf.Bytes(), want) {
|
|
t.Errorf("unexpected output\nwrote: % 02x\n want: % 02x", buf.Bytes(), want)
|
|
}
|
|
}
|
|
|
|
type countWriter struct {
|
|
mu sync.Mutex
|
|
writes int
|
|
bytes int64
|
|
}
|
|
|
|
func (w *countWriter) Write(p []byte) (n int, err error) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
w.writes++
|
|
w.bytes += int64(len(p))
|
|
return len(p), nil
|
|
}
|
|
|
|
func (w *countWriter) Stats() (writes int, bytes int64) {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
return w.writes, w.bytes
|
|
}
|
|
|
|
func (w *countWriter) ResetStats() {
|
|
w.mu.Lock()
|
|
defer w.mu.Unlock()
|
|
w.writes, w.bytes = 0, 0
|
|
}
|
|
|
|
func TestClientSendRateLimiting(t *testing.T) {
|
|
cw := new(countWriter)
|
|
c := &Client{
|
|
bw: bufio.NewWriter(cw),
|
|
clock: &tstest.Clock{},
|
|
}
|
|
c.setSendRateLimiter(ServerInfoMessage{})
|
|
|
|
pkt := make([]byte, 1000)
|
|
if err := c.send(key.NodePublic{}, pkt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writes1, bytes1 := cw.Stats()
|
|
if writes1 != 1 {
|
|
t.Errorf("writes = %v, want 1", writes1)
|
|
}
|
|
|
|
// Flood should all succeed.
|
|
cw.ResetStats()
|
|
for range 1000 {
|
|
if err := c.send(key.NodePublic{}, pkt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
writes1K, bytes1K := cw.Stats()
|
|
if writes1K != 1000 {
|
|
t.Logf("writes = %v; want 1000", writes1K)
|
|
}
|
|
if got, want := bytes1K, bytes1*1000; got != want {
|
|
t.Logf("bytes = %v; want %v", got, want)
|
|
}
|
|
|
|
// Set a rate limiter
|
|
cw.ResetStats()
|
|
c.setSendRateLimiter(ServerInfoMessage{
|
|
TokenBucketBytesPerSecond: 1,
|
|
TokenBucketBytesBurst: int(bytes1 * 2),
|
|
})
|
|
for range 1000 {
|
|
if err := c.send(key.NodePublic{}, pkt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
writesLimited, bytesLimited := cw.Stats()
|
|
if writesLimited == 0 || writesLimited == writes1K {
|
|
t.Errorf("limited conn's write count = %v; want non-zero, less than 1k", writesLimited)
|
|
}
|
|
if bytesLimited < bytes1*2 || bytesLimited >= bytes1K {
|
|
t.Errorf("limited conn's bytes count = %v; want >=%v, <%v", bytesLimited, bytes1K*2, bytes1K)
|
|
}
|
|
}
|