|
|
@@ -1,384 +0,0 @@
|
|
|
-// 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.
|
|
|
-
|
|
|
-// +build depends_on_currently_unreleased
|
|
|
-
|
|
|
-package controlclient
|
|
|
-
|
|
|
-import (
|
|
|
- "context"
|
|
|
- "io/ioutil"
|
|
|
- "net/http"
|
|
|
- "net/http/cookiejar"
|
|
|
- "net/http/httptest"
|
|
|
- "os"
|
|
|
- "testing"
|
|
|
- "time"
|
|
|
-
|
|
|
- "github.com/klauspost/compress/zstd"
|
|
|
- "github.com/tailscale/wireguard-go/wgcfg"
|
|
|
- "tailscale.com/tailcfg"
|
|
|
- "tailscale.com/tstest"
|
|
|
- "tailscale.com/types/logger"
|
|
|
- "tailscale.io/control" // not yet released
|
|
|
-)
|
|
|
-
|
|
|
-// Test that when there are two controlclient connections using the
|
|
|
-// same credentials, the later one disconnects the earlier one.
|
|
|
-func TestDirectReusingKeys(t *testing.T) {
|
|
|
- tstest.PanicOnLog()
|
|
|
- rc := tstest.NewResourceCheck()
|
|
|
- defer rc.Assert(t)
|
|
|
-
|
|
|
- tmpdir, err := ioutil.TempDir("", "control-test-")
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- var server *control.Server
|
|
|
- httpsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
- server.ServeHTTP(w, r)
|
|
|
- }))
|
|
|
- httpsrv.Config.ErrorLog = logger.StdLogger(t.Logf)
|
|
|
- defer func() {
|
|
|
- httpsrv.CloseClientConnections()
|
|
|
- httpsrv.Close()
|
|
|
- os.RemoveAll(tmpdir)
|
|
|
- }()
|
|
|
-
|
|
|
- httpc := httpsrv.Client()
|
|
|
- httpc.Jar, err = cookiejar.New(nil)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
-
|
|
|
- server, err = control.New(tmpdir, tmpdir, tmpdir, httpsrv.URL, true, t.Logf)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- server.QuietLogging = true
|
|
|
- defer server.Shutdown()
|
|
|
-
|
|
|
- hi := NewHostinfo()
|
|
|
- hi.FrontendLogID = "go-test-only"
|
|
|
- hi.BackendLogID = "go-test-only"
|
|
|
-
|
|
|
- // Let's test some nonempty extra hostinfo fields to make sure
|
|
|
- // the server can handle them.
|
|
|
- hi.RequestTags = []string{"tag:abc"}
|
|
|
- cidr, err := wgcfg.ParseCIDR("1.2.3.4/24")
|
|
|
- if err != nil {
|
|
|
- t.Fatalf("ParseCIDR: %v", err)
|
|
|
- }
|
|
|
- hi.RoutableIPs = []wgcfg.CIDR{cidr}
|
|
|
- hi.Services = []tailcfg.Service{
|
|
|
- {
|
|
|
- Proto: tailcfg.TCP,
|
|
|
- Port: 1234,
|
|
|
- Description: "Description",
|
|
|
- },
|
|
|
- }
|
|
|
-
|
|
|
- c1, err := NewDirect(Options{
|
|
|
- ServerURL: httpsrv.URL,
|
|
|
- HTTPTestClient: httpsrv.Client(),
|
|
|
- //TimeNow: s.control.TimeNow,
|
|
|
- Logf: func(fmt string, args ...interface{}) {
|
|
|
- t.Helper()
|
|
|
- t.Logf("c1: "+fmt, args...)
|
|
|
- },
|
|
|
- Hostinfo: hi,
|
|
|
- })
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
-
|
|
|
- // Use a cancelable context so that goroutines blocking in
|
|
|
- // PollNetMap shut down when the test exits.
|
|
|
- ctx, cancel := context.WithCancel(context.Background())
|
|
|
- defer cancel()
|
|
|
-
|
|
|
- // Execute c1's login flow: TryLogin to get an auth URL,
|
|
|
- // postAuthURL to execute the (faked) OAuth segment of the flow,
|
|
|
- // and WaitLoginURL to complete the login on the client end.
|
|
|
- const user = "[email protected]"
|
|
|
- authURL, err := c1.TryLogin(ctx, nil, 0)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- postAuthURL(t, ctx, httpc, user, authURL)
|
|
|
- newURL, err := c1.WaitLoginURL(ctx, authURL)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- if newURL != "" {
|
|
|
- t.Fatalf("unexpected newURL: %s", newURL)
|
|
|
- }
|
|
|
-
|
|
|
- // Start c1's netmap poll in parallel with the rest of the
|
|
|
- // test. We're expecting it to block happily, invoking the no-op
|
|
|
- // update function periodically, then exit once c2 starts its own
|
|
|
- // poll below.
|
|
|
- gotNetmap := make(chan struct{}, 1)
|
|
|
- pollErrCh := make(chan error, 1)
|
|
|
- go func() {
|
|
|
- pollErrCh <- c1.PollNetMap(ctx, -1, func(netMap *NetworkMap) {
|
|
|
- select {
|
|
|
- case gotNetmap <- struct{}{}:
|
|
|
- default:
|
|
|
- }
|
|
|
- })
|
|
|
- }()
|
|
|
-
|
|
|
- select {
|
|
|
- case <-gotNetmap:
|
|
|
- t.Logf("c1: received initial netmap")
|
|
|
- case err := <-pollErrCh:
|
|
|
- t.Fatal(err)
|
|
|
- case <-time.After(5 * time.Second):
|
|
|
- t.Fatal("c1 did not receive an initial netmap")
|
|
|
- }
|
|
|
-
|
|
|
- // Connect c2, reusing c1's credentials. In other words, c2 *is*
|
|
|
- // c1 from the server's perspective.
|
|
|
- c2, err := NewDirect(Options{
|
|
|
- ServerURL: httpsrv.URL,
|
|
|
- HTTPTestClient: httpsrv.Client(),
|
|
|
- Logf: func(fmt string, args ...interface{}) {
|
|
|
- t.Helper()
|
|
|
- t.Logf("c2: "+fmt, args...)
|
|
|
- },
|
|
|
- Persist: c1.GetPersist(),
|
|
|
- Hostinfo: hi,
|
|
|
- NewDecompressor: func() (Decompressor, error) {
|
|
|
- return zstd.NewReader(nil,
|
|
|
- zstd.WithDecoderLowmem(true),
|
|
|
- zstd.WithDecoderConcurrency(1),
|
|
|
- zstd.WithDecoderMaxMemory(65536),
|
|
|
- )
|
|
|
- },
|
|
|
- KeepAlive: true,
|
|
|
- })
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- authURL, err = c2.TryLogin(ctx, nil, 0)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- // We don't expect to be given an authURL, our credentials from c1
|
|
|
- // should still be good.
|
|
|
- if authURL != "" {
|
|
|
- t.Errorf("unexpected authURL %s", authURL)
|
|
|
- }
|
|
|
-
|
|
|
- // Request a single netmap, so this function returns promptly
|
|
|
- // instead of blocking like c1's PollNetMap.
|
|
|
- err = c2.PollNetMap(ctx, 1, func(netMap *NetworkMap) {})
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
-
|
|
|
- // Now that c2 connected and got a netmap, we expect c1's poll to
|
|
|
- // have exited.
|
|
|
- select {
|
|
|
- case err := <-pollErrCh:
|
|
|
- t.Logf("c1: netmap poll aborted as expected (%v)", err)
|
|
|
- case <-time.After(5 * time.Second):
|
|
|
- t.Fatal("first client poll failed to close")
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-func TestDirectReusingOldKey(t *testing.T) {
|
|
|
- tstest.PanicOnLog()
|
|
|
- rc := tstest.NewResourceCheck()
|
|
|
- defer rc.Assert(t)
|
|
|
-
|
|
|
- tmpdir, err := ioutil.TempDir("", "control-test-")
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- var server *control.Server
|
|
|
- httpsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
- server.ServeHTTP(w, r)
|
|
|
- }))
|
|
|
- httpsrv.Config.ErrorLog = logger.StdLogger(t.Logf)
|
|
|
- defer func() {
|
|
|
- httpsrv.CloseClientConnections()
|
|
|
- httpsrv.Close()
|
|
|
- os.RemoveAll(tmpdir)
|
|
|
- }()
|
|
|
-
|
|
|
- httpc := httpsrv.Client()
|
|
|
- httpc.Jar, err = cookiejar.New(nil)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
-
|
|
|
- server, err = control.New(tmpdir, tmpdir, tmpdir, httpsrv.URL, true, t.Logf)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- server.QuietLogging = true
|
|
|
- defer server.Shutdown()
|
|
|
-
|
|
|
- hi := NewHostinfo()
|
|
|
- hi.FrontendLogID = "go-test-only"
|
|
|
- hi.BackendLogID = "go-test-only"
|
|
|
- genOpts := func() Options {
|
|
|
- return Options{
|
|
|
- ServerURL: httpsrv.URL,
|
|
|
- HTTPTestClient: httpc,
|
|
|
- //TimeNow: s.control.TimeNow,
|
|
|
- Logf: func(fmt string, args ...interface{}) {
|
|
|
- t.Helper()
|
|
|
- t.Logf("c1: "+fmt, args...)
|
|
|
- },
|
|
|
- Hostinfo: hi,
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Login with a new node key. This requires authorization.
|
|
|
- c1, err := NewDirect(genOpts())
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- ctx, cancel := context.WithCancel(context.Background())
|
|
|
- defer cancel()
|
|
|
- authURL, err := c1.TryLogin(ctx, nil, 0)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- const user = "[email protected]"
|
|
|
- postAuthURL(t, ctx, httpc, user, authURL)
|
|
|
- newURL, err := c1.WaitLoginURL(ctx, authURL)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- if newURL != "" {
|
|
|
- t.Fatalf("unexpected newURL: %s", newURL)
|
|
|
- }
|
|
|
-
|
|
|
- if err := c1.PollNetMap(ctx, 1, func(netMap *NetworkMap) {}); err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
-
|
|
|
- newPrivKey := func(t *testing.T) wgcfg.PrivateKey {
|
|
|
- t.Helper()
|
|
|
- k, err := wgcfg.NewPrivateKey()
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- return k
|
|
|
- }
|
|
|
-
|
|
|
- // Replace the previous key with a new key.
|
|
|
- persist1 := c1.GetPersist()
|
|
|
- persist2 := Persist{
|
|
|
- PrivateMachineKey: persist1.PrivateMachineKey,
|
|
|
- OldPrivateNodeKey: persist1.PrivateNodeKey,
|
|
|
- PrivateNodeKey: newPrivKey(t),
|
|
|
- }
|
|
|
- opts := genOpts()
|
|
|
- opts.Persist = persist2
|
|
|
-
|
|
|
- c1, err = NewDirect(opts)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- if authURL, err := c1.TryLogin(ctx, nil, 0); err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- } else if authURL == "" {
|
|
|
- t.Fatal("expected authURL for reused oldNodeKey, got none")
|
|
|
- } else {
|
|
|
- postAuthURL(t, ctx, httpc, user, authURL)
|
|
|
- if newURL, err := c1.WaitLoginURL(ctx, authURL); err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- } else if newURL != "" {
|
|
|
- t.Fatalf("unexpected newURL: %s", newURL)
|
|
|
- }
|
|
|
- }
|
|
|
- if p := c1.GetPersist(); p.PrivateNodeKey != opts.Persist.PrivateNodeKey {
|
|
|
- t.Error("unexpected node key change")
|
|
|
- } else {
|
|
|
- persist2 = p
|
|
|
- }
|
|
|
-
|
|
|
- // Here we simulate a client using using old persistent data.
|
|
|
- // We use the key we have already replaced as the old node key.
|
|
|
- // This requires the user to authenticate.
|
|
|
- persist3 := Persist{
|
|
|
- PrivateMachineKey: persist1.PrivateMachineKey,
|
|
|
- OldPrivateNodeKey: persist1.PrivateNodeKey,
|
|
|
- PrivateNodeKey: newPrivKey(t),
|
|
|
- }
|
|
|
- opts = genOpts()
|
|
|
- opts.Persist = persist3
|
|
|
-
|
|
|
- c1, err = NewDirect(opts)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- if authURL, err := c1.TryLogin(ctx, nil, 0); err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- } else if authURL == "" {
|
|
|
- t.Fatal("expected authURL for reused oldNodeKey, got none")
|
|
|
- } else {
|
|
|
- postAuthURL(t, ctx, httpc, user, authURL)
|
|
|
- if newURL, err := c1.WaitLoginURL(ctx, authURL); err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- } else if newURL != "" {
|
|
|
- t.Fatalf("unexpected newURL: %s", newURL)
|
|
|
- }
|
|
|
- }
|
|
|
- if err := c1.PollNetMap(ctx, 1, func(netMap *NetworkMap) {}); err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
-
|
|
|
- // At this point, there should only be one node for the machine key
|
|
|
- // registered as active in the server.
|
|
|
- mkey := tailcfg.MachineKey(persist1.PrivateMachineKey.Public())
|
|
|
- nodeIDs, err := server.DB().MachineNodes(mkey)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- if len(nodeIDs) != 1 {
|
|
|
- t.Logf("active nodes for machine key %v:", mkey)
|
|
|
- for i, nodeID := range nodeIDs {
|
|
|
- nodeKey := server.DB().NodeKey(nodeID)
|
|
|
- t.Logf("\tnode %d: id=%v, key=%v", i, nodeID, nodeKey)
|
|
|
- }
|
|
|
- t.Fatalf("want 1 active node for the client machine, got %d", len(nodeIDs))
|
|
|
- }
|
|
|
-
|
|
|
- // Now try the previous node key. It should fail.
|
|
|
- opts = genOpts()
|
|
|
- opts.Persist = persist2
|
|
|
- c1, err = NewDirect(opts)
|
|
|
- if err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- // TODO(crawshaw): make this return an actual error.
|
|
|
- // Have cfgdb track expired keys, and when an expired key is reused
|
|
|
- // produce an error.
|
|
|
- if authURL, err := c1.TryLogin(ctx, nil, 0); err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- } else if authURL == "" {
|
|
|
- t.Fatal("expected authURL for reused nodeKey, got none")
|
|
|
- } else {
|
|
|
- postAuthURL(t, ctx, httpc, user, authURL)
|
|
|
- if newURL, err := c1.WaitLoginURL(ctx, authURL); err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- } else if newURL != "" {
|
|
|
- t.Fatalf("unexpected newURL: %s", newURL)
|
|
|
- }
|
|
|
- }
|
|
|
- if err := c1.PollNetMap(ctx, 1, func(netMap *NetworkMap) {}); err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- }
|
|
|
- if nodeIDs, err := server.DB().MachineNodes(mkey); err != nil {
|
|
|
- t.Fatal(err)
|
|
|
- } else if len(nodeIDs) != 1 {
|
|
|
- t.Fatalf("want 1 active node for the client machine, got %d", len(nodeIDs))
|
|
|
- }
|
|
|
-}
|