// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package derp import ( "bufio" "bytes" "context" "crypto/x509" "encoding/asn1" "encoding/json" "errors" "expvar" "fmt" "io" "log" "net" "os" "reflect" "strconv" "sync" "testing" "time" "go4.org/mem" "golang.org/x/time/rate" "tailscale.com/disco" "tailscale.com/net/memnet" "tailscale.com/types/key" "tailscale.com/types/logger" ) func TestClientInfoUnmarshal(t *testing.T) { for i, in := range []string{ `{"Version":5,"MeshKey":"abc"}`, `{"version":5,"meshKey":"abc"}`, } { var got clientInfo if err := json.Unmarshal([]byte(in), &got); err != nil { t.Fatalf("[%d]: %v", i, err) } want := clientInfo{Version: 5, MeshKey: "abc"} if got != want { t.Errorf("[%d]: got %+v; want %+v", i, got, want) } } } func TestSendRecv(t *testing.T) { serverPrivateKey := key.NewNode() s := NewServer(serverPrivateKey, t.Logf) defer s.Close() const numClients = 3 var clientPrivateKeys []key.NodePrivate var clientKeys []key.NodePublic for i := 0; i < numClients; i++ { priv := key.NewNode() clientPrivateKeys = append(clientPrivateKeys, priv) clientKeys = append(clientKeys, priv.Public()) } ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } defer ln.Close() var clients []*Client var connsOut []Conn var recvChs []chan []byte errCh := make(chan error, 3) for i := 0; i < numClients; i++ { t.Logf("Connecting client %d ...", i) cout, err := net.Dial("tcp", ln.Addr().String()) if err != nil { t.Fatal(err) } defer cout.Close() connsOut = append(connsOut, cout) cin, err := ln.Accept() if err != nil { t.Fatal(err) } defer cin.Close() ctx, cancel := context.WithCancel(context.Background()) defer cancel() brwServer := bufio.NewReadWriter(bufio.NewReader(cin), bufio.NewWriter(cin)) go s.Accept(ctx, cin, brwServer, fmt.Sprintf("test-client-%d", i)) key := clientPrivateKeys[i] brw := bufio.NewReadWriter(bufio.NewReader(cout), bufio.NewWriter(cout)) c, err := NewClient(key, cout, brw, t.Logf) if err != nil { t.Fatalf("client %d: %v", i, err) } waitConnect(t, c) clients = append(clients, c) recvChs = append(recvChs, make(chan []byte)) t.Logf("Connected client %d.", i) } var peerGoneCountDisconnected expvar.Int var peerGoneCountNotHere expvar.Int t.Logf("Starting read loops") for i := 0; i < numClients; i++ { go func(i int) { for { m, err := clients[i].Recv() if err != nil { errCh <- err return } switch m := m.(type) { default: t.Errorf("unexpected message type %T", m) continue case PeerGoneMessage: switch m.Reason { case PeerGoneReasonDisconnected: peerGoneCountDisconnected.Add(1) case PeerGoneReasonNotHere: peerGoneCountNotHere.Add(1) default: t.Errorf("unexpected PeerGone reason %v", m.Reason) } case ReceivedPacket: if m.Source.IsZero() { t.Errorf("zero Source address in ReceivedPacket") } recvChs[i] <- bytes.Clone(m.Data) } } }(i) } recv := func(i int, want string) { t.Helper() select { case b := <-recvChs[i]: if got := string(b); got != want { t.Errorf("client1.Recv=%q, want %q", got, want) } case <-time.After(5 * time.Second): t.Errorf("client%d.Recv, got nothing, want %q", i, want) } } recvNothing := func(i int) { t.Helper() select { case b := <-recvChs[0]: t.Errorf("client%d.Recv=%q, want nothing", i, string(b)) default: } } wantActive := func(total, home int64) { t.Helper() dl := time.Now().Add(5 * time.Second) var gotTotal, gotHome int64 for time.Now().Before(dl) { gotTotal, gotHome = s.curClients.Value(), s.curHomeClients.Value() if gotTotal == total && gotHome == home { return } time.Sleep(10 * time.Millisecond) } t.Errorf("total/home=%v/%v; want %v/%v", gotTotal, gotHome, total, home) } wantClosedPeers := func(want int64) { t.Helper() var got int64 dl := time.Now().Add(5 * time.Second) for time.Now().Before(dl) { if got = peerGoneCountDisconnected.Value(); got == want { return } } t.Errorf("peer gone count = %v; want %v", got, want) } wantUnknownPeers := func(want int64) { t.Helper() var got int64 dl := time.Now().Add(5 * time.Second) for time.Now().Before(dl) { if got = peerGoneCountNotHere.Value(); got == want { return } } t.Errorf("peer gone count = %v; want %v", got, want) } msg1 := []byte("hello 0->1\n") if err := clients[0].Send(clientKeys[1], msg1); err != nil { t.Fatal(err) } recv(1, string(msg1)) recvNothing(0) recvNothing(2) msg2 := []byte("hello 1->2\n") if err := clients[1].Send(clientKeys[2], msg2); err != nil { t.Fatal(err) } recv(2, string(msg2)) recvNothing(0) recvNothing(1) // Send messages to a non-existent node neKey := key.NewNode().Public() msg4 := []byte("not a CallMeMaybe->unknown destination\n") if err := clients[1].Send(neKey, msg4); err != nil { t.Fatal(err) } wantUnknownPeers(0) callMe := neKey.AppendTo([]byte(disco.Magic)) callMeHeader := make([]byte, disco.NonceLen) callMe = append(callMe, callMeHeader...) if err := clients[1].Send(neKey, callMe); err != nil { t.Fatal(err) } wantUnknownPeers(1) // PeerGoneNotHere is rate-limited to 3 times a second for i := 0; i < 5; i++ { if err := clients[1].Send(neKey, callMe); err != nil { t.Fatal(err) } } wantUnknownPeers(3) wantActive(3, 0) clients[0].NotePreferred(true) wantActive(3, 1) clients[0].NotePreferred(true) wantActive(3, 1) clients[0].NotePreferred(false) wantActive(3, 0) clients[0].NotePreferred(false) wantActive(3, 0) clients[1].NotePreferred(true) wantActive(3, 1) connsOut[1].Close() wantActive(2, 0) wantClosedPeers(1) clients[2].NotePreferred(true) wantActive(2, 1) clients[2].NotePreferred(false) wantActive(2, 0) connsOut[2].Close() wantActive(1, 0) wantClosedPeers(1) t.Logf("passed") s.Close() } func TestSendFreeze(t *testing.T) { serverPrivateKey := key.NewNode() s := NewServer(serverPrivateKey, t.Logf) defer s.Close() s.WriteTimeout = 100 * time.Millisecond // We send two streams of messages: // // alice --> bob // alice --> cathy // // Then cathy stops processing messages. // That should not interfere with alice talking to bob. newClient := func(ctx context.Context, name string, k key.NodePrivate) (c *Client, clientConn memnet.Conn) { t.Helper() c1, c2 := memnet.NewConn(name, 1024) go s.Accept(ctx, c1, bufio.NewReadWriter(bufio.NewReader(c1), bufio.NewWriter(c1)), name) brw := bufio.NewReadWriter(bufio.NewReader(c2), bufio.NewWriter(c2)) c, err := NewClient(k, c2, brw, t.Logf) if err != nil { t.Fatal(err) } waitConnect(t, c) return c, c2 } ctx, clientCtxCancel := context.WithCancel(context.Background()) defer clientCtxCancel() aliceKey := key.NewNode() aliceClient, aliceConn := newClient(ctx, "alice", aliceKey) bobKey := key.NewNode() bobClient, bobConn := newClient(ctx, "bob", bobKey) cathyKey := key.NewNode() cathyClient, cathyConn := newClient(ctx, "cathy", cathyKey) var ( aliceCh = make(chan struct{}, 32) bobCh = make(chan struct{}, 32) cathyCh = make(chan struct{}, 32) ) chs := func(name string) chan struct{} { switch name { case "alice": return aliceCh case "bob": return bobCh case "cathy": return cathyCh default: panic("unknown ch: " + name) } } errCh := make(chan error, 4) recv := func(name string, client *Client) { ch := chs(name) for { m, err := client.Recv() if err != nil { errCh <- fmt.Errorf("%s: %w", name, err) return } switch m := m.(type) { default: errCh <- fmt.Errorf("%s: unexpected message type %T", name, m) return case ReceivedPacket: if m.Source.IsZero() { errCh <- fmt.Errorf("%s: zero Source address in ReceivedPacket", name) return } select { case ch <- struct{}{}: default: } } } } go recv("alice", aliceClient) go recv("bob", bobClient) go recv("cathy", cathyClient) var cancel func() go func() { t := time.NewTicker(2 * time.Millisecond) defer t.Stop() var ctx context.Context ctx, cancel = context.WithCancel(context.Background()) for { select { case <-t.C: case <-ctx.Done(): errCh <- nil return } msg1 := []byte("hello alice->bob\n") if err := aliceClient.Send(bobKey.Public(), msg1); err != nil { errCh <- fmt.Errorf("alice send to bob: %w", err) return } msg2 := []byte("hello alice->cathy\n") // TODO: an error is expected here. // We ignore it, maybe we should log it somehow? aliceClient.Send(cathyKey.Public(), msg2) } }() drainAny := func(ch chan struct{}) { // We are draining potentially infinite sources, // so place some reasonable upper limit. // // The important thing here is to make sure that // if any tokens remain in the channel, they // must have been generated after drainAny was // called. for i := 0; i < cap(ch); i++ { select { case <-ch: default: return } } } drain := func(t *testing.T, name string) bool { t.Helper() timer := time.NewTimer(1 * time.Second) defer timer.Stop() // Ensure ch has at least one element. ch := chs(name) select { case <-ch: case <-timer.C: t.Errorf("no packet received by %s", name) return false } // Drain remaining. drainAny(ch) return true } isEmpty := func(t *testing.T, name string) { t.Helper() select { case <-chs(name): t.Errorf("packet received by %s, want none", name) default: } } t.Run("initial send", func(t *testing.T) { drain(t, "bob") drain(t, "cathy") isEmpty(t, "alice") }) t.Run("block cathy", func(t *testing.T) { // Block cathy. Now the cathyConn buffer will fill up quickly, // and the derp server will back up. cathyConn.SetReadBlock(true) time.Sleep(2 * s.WriteTimeout) drain(t, "bob") drainAny(chs("cathy")) isEmpty(t, "alice") // Now wait a little longer, and ensure packets still flow to bob if !drain(t, "bob") { t.Errorf("connection alice->bob frozen by alice->cathy") } }) // Cleanup, make sure we process all errors. t.Logf("TEST COMPLETE, cancelling sender") cancel() t.Logf("closing connections") // Close bob before alice. // Starting with alice can cause a PeerGoneMessage to reach // bob before bob is closed, causing a test flake (issue 2668). bobConn.Close() aliceConn.Close() cathyConn.Close() for i := 0; i < cap(errCh); i++ { err := <-errCh if err != nil { if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) { continue } t.Error(err) } } } type testServer struct { s *Server ln net.Listener logf logger.Logf mu sync.Mutex pubName map[key.NodePublic]string clients map[*testClient]bool } func (ts *testServer) addTestClient(c *testClient) { ts.mu.Lock() defer ts.mu.Unlock() ts.clients[c] = true } func (ts *testServer) addKeyName(k key.NodePublic, name string) { ts.mu.Lock() defer ts.mu.Unlock() ts.pubName[k] = name ts.logf("test adding named key %q for %x", name, k) } func (ts *testServer) keyName(k key.NodePublic) string { ts.mu.Lock() defer ts.mu.Unlock() if name, ok := ts.pubName[k]; ok { return name } return k.ShortString() } func (ts *testServer) close(t *testing.T) error { ts.ln.Close() ts.s.Close() for c := range ts.clients { c.close(t) } return nil } func newTestServer(t *testing.T, ctx context.Context) *testServer { t.Helper() logf := logger.WithPrefix(t.Logf, "derp-server: ") s := NewServer(key.NewNode(), logf) s.SetMeshKey("mesh-key") ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } go func() { i := 0 for { i++ c, err := ln.Accept() if err != nil { return } // TODO: register c in ts so Close also closes it? go func(i int) { brwServer := bufio.NewReadWriter(bufio.NewReader(c), bufio.NewWriter(c)) go s.Accept(ctx, c, brwServer, fmt.Sprintf("test-client-%d", i)) }(i) } }() return &testServer{ s: s, ln: ln, logf: logf, clients: map[*testClient]bool{}, pubName: map[key.NodePublic]string{}, } } type testClient struct { name string c *Client nc net.Conn pub key.NodePublic ts *testServer closed bool } func newTestClient(t *testing.T, ts *testServer, name string, newClient func(net.Conn, key.NodePrivate, logger.Logf) (*Client, error)) *testClient { t.Helper() nc, err := net.Dial("tcp", ts.ln.Addr().String()) if err != nil { t.Fatal(err) } k := key.NewNode() ts.addKeyName(k.Public(), name) c, err := newClient(nc, k, logger.WithPrefix(t.Logf, "client-"+name+": ")) if err != nil { t.Fatal(err) } tc := &testClient{ name: name, nc: nc, c: c, ts: ts, pub: k.Public(), } ts.addTestClient(tc) return tc } func newRegularClient(t *testing.T, ts *testServer, name string) *testClient { return newTestClient(t, ts, name, func(nc net.Conn, priv key.NodePrivate, logf logger.Logf) (*Client, error) { brw := bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc)) c, err := NewClient(priv, nc, brw, logf) if err != nil { return nil, err } waitConnect(t, c) return c, nil }) } func newTestWatcher(t *testing.T, ts *testServer, name string) *testClient { return newTestClient(t, ts, name, func(nc net.Conn, priv key.NodePrivate, logf logger.Logf) (*Client, error) { brw := bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc)) c, err := NewClient(priv, nc, brw, logf, MeshKey("mesh-key")) if err != nil { return nil, err } waitConnect(t, c) if err := c.WatchConnectionChanges(); err != nil { return nil, err } return c, nil }) } func (tc *testClient) wantPresent(t *testing.T, peers ...key.NodePublic) { t.Helper() want := map[key.NodePublic]bool{} for _, k := range peers { want[k] = true } for { m, err := tc.c.recvTimeout(time.Second) if err != nil { t.Fatal(err) } switch m := m.(type) { case PeerPresentMessage: got := key.NodePublic(m) if !want[got] { t.Fatalf("got peer present for %v; want present for %v", tc.ts.keyName(got), logger.ArgWriter(func(bw *bufio.Writer) { for _, pub := range peers { fmt.Fprintf(bw, "%s ", tc.ts.keyName(pub)) } })) } delete(want, got) if len(want) == 0 { return } default: t.Fatalf("unexpected message type %T", m) } } } func (tc *testClient) wantGone(t *testing.T, peer key.NodePublic) { t.Helper() m, err := tc.c.recvTimeout(time.Second) if err != nil { t.Fatal(err) } switch m := m.(type) { case PeerGoneMessage: got := key.NodePublic(m.Peer) if peer != got { t.Errorf("got gone message for %v; want gone for %v", tc.ts.keyName(got), tc.ts.keyName(peer)) } reason := m.Reason if reason != PeerGoneReasonDisconnected { t.Errorf("got gone message for reason %v; wanted %v", reason, PeerGoneReasonDisconnected) } default: t.Fatalf("unexpected message type %T", m) } } func (c *testClient) close(t *testing.T) { t.Helper() if c.closed { return } c.closed = true t.Logf("closing client %q (%x)", c.name, c.pub) c.nc.Close() } // TestWatch tests the connection watcher mechanism used by regional // DERP nodes to mesh up with each other. func TestWatch(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() ts := newTestServer(t, ctx) defer ts.close(t) w1 := newTestWatcher(t, ts, "w1") w1.wantPresent(t, w1.pub) c1 := newRegularClient(t, ts, "c1") w1.wantPresent(t, c1.pub) c2 := newRegularClient(t, ts, "c2") w1.wantPresent(t, c2.pub) w2 := newTestWatcher(t, ts, "w2") w1.wantPresent(t, w2.pub) w2.wantPresent(t, w1.pub, w2.pub, c1.pub, c2.pub) c3 := newRegularClient(t, ts, "c3") w1.wantPresent(t, c3.pub) w2.wantPresent(t, c3.pub) c2.close(t) w1.wantGone(t, c2.pub) w2.wantGone(t, c2.pub) w3 := newTestWatcher(t, ts, "w3") w1.wantPresent(t, w3.pub) w2.wantPresent(t, w3.pub) w3.wantPresent(t, c1.pub, c3.pub, w1.pub, w2.pub, w3.pub) c1.close(t) w1.wantGone(t, c1.pub) w2.wantGone(t, c1.pub) w3.wantGone(t, c1.pub) } type testFwd int func (testFwd) ForwardPacket(key.NodePublic, key.NodePublic, []byte) error { panic("not called in tests") } func (testFwd) String() string { panic("not called in tests") } func pubAll(b byte) (ret key.NodePublic) { var bs [32]byte for i := range bs { bs[i] = b } return key.NodePublicFromRaw32(mem.B(bs[:])) } func TestForwarderRegistration(t *testing.T) { s := &Server{ clients: make(map[key.NodePublic]clientSet), clientsMesh: map[key.NodePublic]PacketForwarder{}, } want := func(want map[key.NodePublic]PacketForwarder) { t.Helper() if got := s.clientsMesh; !reflect.DeepEqual(got, want) { t.Fatalf("mismatch\n got: %v\nwant: %v\n", got, want) } } wantCounter := func(c *expvar.Int, want int) { t.Helper() if got := c.Value(); got != int64(want) { t.Errorf("counter = %v; want %v", got, want) } } u1 := pubAll(1) u2 := pubAll(2) u3 := pubAll(3) s.AddPacketForwarder(u1, testFwd(1)) s.AddPacketForwarder(u2, testFwd(2)) want(map[key.NodePublic]PacketForwarder{ u1: testFwd(1), u2: testFwd(2), }) // Verify a remove of non-registered forwarder is no-op. s.RemovePacketForwarder(u2, testFwd(999)) want(map[key.NodePublic]PacketForwarder{ u1: testFwd(1), u2: testFwd(2), }) // Verify a remove of non-registered user is no-op. s.RemovePacketForwarder(u3, testFwd(1)) want(map[key.NodePublic]PacketForwarder{ u1: testFwd(1), u2: testFwd(2), }) // Actual removal. s.RemovePacketForwarder(u2, testFwd(2)) want(map[key.NodePublic]PacketForwarder{ u1: testFwd(1), }) // Adding a dup for a user. wantCounter(&s.multiForwarderCreated, 0) s.AddPacketForwarder(u1, testFwd(100)) s.AddPacketForwarder(u1, testFwd(100)) // dup to trigger dup path want(map[key.NodePublic]PacketForwarder{ u1: newMultiForwarder(testFwd(1), testFwd(100)), }) wantCounter(&s.multiForwarderCreated, 1) // Removing a forwarder in a multi set that doesn't exist; does nothing. s.RemovePacketForwarder(u1, testFwd(55)) want(map[key.NodePublic]PacketForwarder{ u1: newMultiForwarder(testFwd(1), testFwd(100)), }) // Removing a forwarder in a multi set that does exist should collapse it away // from being a multiForwarder. wantCounter(&s.multiForwarderDeleted, 0) s.RemovePacketForwarder(u1, testFwd(1)) want(map[key.NodePublic]PacketForwarder{ u1: testFwd(100), }) wantCounter(&s.multiForwarderDeleted, 1) // Removing an entry for a client that's still connected locally should result // in a nil forwarder. u1c := &sclient{ key: u1, logf: logger.Discard, } s.clients[u1] = singleClient{u1c} s.RemovePacketForwarder(u1, testFwd(100)) want(map[key.NodePublic]PacketForwarder{ u1: nil, }) // But once that client disconnects, it should go away. s.unregisterClient(u1c) want(map[key.NodePublic]PacketForwarder{}) // But if it already has a forwarder, it's not removed. s.AddPacketForwarder(u1, testFwd(2)) s.unregisterClient(u1c) want(map[key.NodePublic]PacketForwarder{ u1: testFwd(2), }) // Now pretend u1 was already connected locally (so clientsMesh[u1] is nil), and then we heard // that they're also connected to a peer of ours. That shouldn't transition the forwarder // from nil to the new one, not a multiForwarder. s.clients[u1] = singleClient{u1c} s.clientsMesh[u1] = nil want(map[key.NodePublic]PacketForwarder{ u1: nil, }) s.AddPacketForwarder(u1, testFwd(3)) want(map[key.NodePublic]PacketForwarder{ u1: testFwd(3), }) } type channelFwd struct { // id is to ensure that different instances that reference the // same channel are not equal, as they are used as keys in the // multiForwarder map. id int c chan []byte } func (f channelFwd) String() string { return "" } func (f channelFwd) ForwardPacket(_ key.NodePublic, _ key.NodePublic, packet []byte) error { f.c <- packet return nil } func TestMultiForwarder(t *testing.T) { received := 0 var wg sync.WaitGroup ch := make(chan []byte) ctx, cancel := context.WithCancel(context.Background()) s := &Server{ clients: make(map[key.NodePublic]clientSet), clientsMesh: map[key.NodePublic]PacketForwarder{}, } u := pubAll(1) s.AddPacketForwarder(u, channelFwd{1, ch}) wg.Add(2) go func() { defer wg.Done() for { select { case <-ch: received += 1 case <-ctx.Done(): return } } }() go func() { defer wg.Done() for { s.AddPacketForwarder(u, channelFwd{2, ch}) s.AddPacketForwarder(u, channelFwd{3, ch}) s.RemovePacketForwarder(u, channelFwd{2, ch}) s.RemovePacketForwarder(u, channelFwd{1, ch}) s.AddPacketForwarder(u, channelFwd{1, ch}) s.RemovePacketForwarder(u, channelFwd{3, ch}) if ctx.Err() != nil { return } } }() // Number of messages is chosen arbitrarily, just for this loop to // run long enough concurrently with {Add,Remove}PacketForwarder loop above. numMsgs := 5000 var fwd PacketForwarder for i := 0; i < numMsgs; i++ { s.mu.Lock() fwd = s.clientsMesh[u] s.mu.Unlock() fwd.ForwardPacket(u, u, []byte(strconv.Itoa(i))) } cancel() wg.Wait() if received != numMsgs { t.Errorf("expected %d messages to be forwarded; got %d", numMsgs, received) } } func TestMetaCert(t *testing.T) { priv := key.NewNode() pub := priv.Public() s := NewServer(priv, t.Logf) certBytes := s.MetaCert() cert, err := x509.ParseCertificate(certBytes) if err != nil { log.Fatal(err) } if fmt.Sprint(cert.SerialNumber) != fmt.Sprint(ProtocolVersion) { t.Errorf("serial = %v; want %v", cert.SerialNumber, ProtocolVersion) } if g, w := cert.Subject.CommonName, fmt.Sprintf("derpkey%s", pub.UntypedHexString()); g != w { t.Errorf("CommonName = %q; want %q", g, w) } if n := len(cert.Extensions); n != 1 { t.Fatalf("got %d extensions; want 1", n) } // oidExtensionBasicConstraints is the Basic Constraints ID copied // from the x509 package. oidExtensionBasicConstraints := asn1.ObjectIdentifier{2, 5, 29, 19} if id := cert.Extensions[0].Id; !id.Equal(oidExtensionBasicConstraints) { t.Errorf("extension ID = %v; want %v", id, oidExtensionBasicConstraints) } } 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, } 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) } }) } } 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) } } func TestServerDupClients(t *testing.T) { serverPriv := key.NewNode() var s *Server clientPriv := key.NewNode() clientPub := clientPriv.Public() var c1, c2, c3 *sclient var clientName map[*sclient]string // run starts a new test case and resets clients back to their zero values. run := func(name string, dupPolicy dupPolicy, f func(t *testing.T)) { s = NewServer(serverPriv, t.Logf) s.dupPolicy = dupPolicy c1 = &sclient{key: clientPub, logf: logger.WithPrefix(t.Logf, "c1: ")} c2 = &sclient{key: clientPub, logf: logger.WithPrefix(t.Logf, "c2: ")} c3 = &sclient{key: clientPub, logf: logger.WithPrefix(t.Logf, "c3: ")} clientName = map[*sclient]string{ c1: "c1", c2: "c2", c3: "c3", } t.Run(name, f) } runBothWays := func(name string, f func(t *testing.T)) { run(name+"_disablefighters", disableFighters, f) run(name+"_lastwriteractive", lastWriterIsActive, f) } wantSingleClient := func(t *testing.T, want *sclient) { t.Helper() switch s := s.clients[want.key].(type) { case singleClient: if s.c != want { t.Error("wrong single client") return } if want.isDup.Load() { t.Errorf("unexpected isDup on singleClient") } if want.isDisabled.Load() { t.Errorf("unexpected isDisabled on singleClient") } case nil: t.Error("no clients for key") case *dupClientSet: t.Error("unexpected multiple clients for key") } } wantNoClient := func(t *testing.T) { t.Helper() switch s := s.clients[clientPub].(type) { case nil: // Good. return default: t.Errorf("got %T; want empty", s) } } wantDupSet := func(t *testing.T) *dupClientSet { t.Helper() switch s := s.clients[clientPub].(type) { case *dupClientSet: return s default: t.Fatalf("wanted dup set; got %T", s) return nil } } wantActive := func(t *testing.T, want *sclient) { t.Helper() set, ok := s.clients[clientPub] if !ok { t.Error("no set for key") return } got := set.ActiveClient() if got != want { t.Errorf("active client = %q; want %q", clientName[got], clientName[want]) } } checkDup := func(t *testing.T, c *sclient, want bool) { t.Helper() if got := c.isDup.Load(); got != want { t.Errorf("client %q isDup = %v; want %v", clientName[c], got, want) } } checkDisabled := func(t *testing.T, c *sclient, want bool) { t.Helper() if got := c.isDisabled.Load(); got != want { t.Errorf("client %q isDisabled = %v; want %v", clientName[c], got, want) } } wantDupConns := func(t *testing.T, want int) { t.Helper() if got := s.dupClientConns.Value(); got != int64(want) { t.Errorf("dupClientConns = %v; want %v", got, want) } } wantDupKeys := func(t *testing.T, want int) { t.Helper() if got := s.dupClientKeys.Value(); got != int64(want) { t.Errorf("dupClientKeys = %v; want %v", got, want) } } // Common case: a single client comes and goes, with no dups. runBothWays("one_comes_and_goes", func(t *testing.T) { wantNoClient(t) s.registerClient(c1) wantSingleClient(t, c1) s.unregisterClient(c1) wantNoClient(t) }) // A still somewhat common case: a single client was // connected and then their wifi dies or laptop closes // or they switch networks and connect from a // different network. They have two connections but // it's not very bad. Only their new one is // active. The last one, being dead, doesn't send and // thus the new one doesn't get disabled. runBothWays("small_overlap_replacement", func(t *testing.T) { wantNoClient(t) s.registerClient(c1) wantSingleClient(t, c1) wantActive(t, c1) wantDupKeys(t, 0) wantDupKeys(t, 0) s.registerClient(c2) // wifi dies; c2 replacement connects wantDupSet(t) wantDupConns(t, 2) wantDupKeys(t, 1) checkDup(t, c1, true) checkDup(t, c2, true) checkDisabled(t, c1, false) checkDisabled(t, c2, false) wantActive(t, c2) // sends go to the replacement s.unregisterClient(c1) // c1 finally times out wantSingleClient(t, c2) checkDup(t, c2, false) // c2 is longer a dup wantActive(t, c2) wantDupConns(t, 0) wantDupKeys(t, 0) }) // Key cloning situation with concurrent clients, both trying // to write. run("concurrent_dups_get_disabled", disableFighters, func(t *testing.T) { wantNoClient(t) s.registerClient(c1) wantSingleClient(t, c1) wantActive(t, c1) s.registerClient(c2) wantDupSet(t) wantDupKeys(t, 1) wantDupConns(t, 2) wantActive(t, c2) checkDup(t, c1, true) checkDup(t, c2, true) checkDisabled(t, c1, false) checkDisabled(t, c2, false) s.noteClientActivity(c2) checkDisabled(t, c1, false) checkDisabled(t, c2, false) s.noteClientActivity(c1) checkDisabled(t, c1, true) checkDisabled(t, c2, true) wantActive(t, nil) s.registerClient(c3) wantActive(t, c3) checkDisabled(t, c3, false) wantDupKeys(t, 1) wantDupConns(t, 3) s.unregisterClient(c3) wantActive(t, nil) wantDupKeys(t, 1) wantDupConns(t, 2) s.unregisterClient(c2) wantSingleClient(t, c1) wantDupKeys(t, 0) wantDupConns(t, 0) }) // Key cloning with an A->B->C->A series instead. run("concurrent_dups_three_parties", disableFighters, func(t *testing.T) { wantNoClient(t) s.registerClient(c1) s.registerClient(c2) s.registerClient(c3) s.noteClientActivity(c1) checkDisabled(t, c1, true) checkDisabled(t, c2, true) checkDisabled(t, c3, true) wantActive(t, nil) }) run("activity_promotes_primary_when_nil", disableFighters, func(t *testing.T) { wantNoClient(t) // Last registered client is the active one... s.registerClient(c1) wantActive(t, c1) s.registerClient(c2) wantActive(t, c2) s.registerClient(c3) s.noteClientActivity(c2) wantActive(t, c3) // But if the last one goes away, the one with the // most recent activity wins. s.unregisterClient(c3) wantActive(t, c2) }) run("concurrent_dups_three_parties_last_writer", lastWriterIsActive, func(t *testing.T) { wantNoClient(t) s.registerClient(c1) wantActive(t, c1) s.registerClient(c2) wantActive(t, c2) s.noteClientActivity(c1) checkDisabled(t, c1, false) checkDisabled(t, c2, false) wantActive(t, c1) s.noteClientActivity(c2) checkDisabled(t, c1, false) checkDisabled(t, c2, false) wantActive(t, c2) s.unregisterClient(c2) checkDisabled(t, c1, false) wantActive(t, c1) }) } func TestLimiter(t *testing.T) { rl := rate.NewLimiter(rate.Every(time.Minute), 100) for i := 0; i < 200; i++ { r := rl.Reserve() d := r.Delay() t.Logf("i=%d, allow=%v, d=%v", i, r.OK(), d) } } func BenchmarkSendRecv(b *testing.B) { for _, size := range []int{10, 100, 1000, 10000} { b.Run(fmt.Sprintf("msgsize=%d", size), func(b *testing.B) { benchmarkSendRecvSize(b, size) }) } } func benchmarkSendRecvSize(b *testing.B, packetSize int) { serverPrivateKey := key.NewNode() s := NewServer(serverPrivateKey, logger.Discard) defer s.Close() k := key.NewNode() clientKey := k.Public() ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { b.Fatal(err) } defer ln.Close() connOut, err := net.Dial("tcp", ln.Addr().String()) if err != nil { b.Fatal(err) } defer connOut.Close() connIn, err := ln.Accept() if err != nil { b.Fatal(err) } defer connIn.Close() brwServer := bufio.NewReadWriter(bufio.NewReader(connIn), bufio.NewWriter(connIn)) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go s.Accept(ctx, connIn, brwServer, "test-client") brw := bufio.NewReadWriter(bufio.NewReader(connOut), bufio.NewWriter(connOut)) client, err := NewClient(k, connOut, brw, logger.Discard) if err != nil { b.Fatalf("client: %v", err) } go func() { for { _, err := client.Recv() if err != nil { return } } }() msg := make([]byte, packetSize) b.SetBytes(int64(len(msg))) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { if err := client.Send(clientKey, msg); err != nil { b.Fatal(err) } } } func BenchmarkWriteUint32(b *testing.B) { w := bufio.NewWriter(io.Discard) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { writeUint32(w, 0x0ba3a) } } type nopRead struct{} func (r nopRead) Read(p []byte) (int, error) { return len(p), nil } var sinkU32 uint32 func BenchmarkReadUint32(b *testing.B) { r := bufio.NewReader(nopRead{}) var err error b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { sinkU32, err = readUint32(r) if err != nil { b.Fatal(err) } } } func waitConnect(t testing.TB, c *Client) { t.Helper() if m, err := c.Recv(); err != nil { t.Fatalf("client first Recv: %v", err) } else if v, ok := m.(ServerInfoMessage); !ok { t.Fatalf("client first Recv was unexpected type %T", v) } } func TestParseSSOutput(t *testing.T) { contents, err := os.ReadFile("testdata/example_ss.txt") if err != nil { t.Errorf("os.ReadFile(example_ss.txt) failed: %v", err) } seen := parseSSOutput(string(contents)) if len(seen) == 0 { t.Errorf("parseSSOutput expected non-empty map") } } 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), } 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 i := 0; i < 1000; i++ { 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 i := 0; i < 1000; i++ { 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) } } func TestServerRepliesToPing(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() ts := newTestServer(t, ctx) defer ts.close(t) tc := newRegularClient(t, ts, "alice") data := [8]byte{1, 2, 3, 4, 5, 6, 7, 42} if err := tc.c.SendPing(data); err != nil { t.Fatal(err) } for { m, err := tc.c.recvTimeout(time.Second) if err != nil { t.Fatal(err) } switch m := m.(type) { case PongMessage: if ([8]byte(m)) != data { t.Fatalf("got pong %2x; want %2x", [8]byte(m), data) } return } } }