|
|
@@ -294,13 +294,14 @@ func completeLogin(t *testing.T, control *testcontrol.Server, counter *atomic.In
|
|
|
// This handler receives device approval URLs, and approves the device.
|
|
|
//
|
|
|
// It counts how many URLs it sees, and will fail the test if it
|
|
|
-// sees multiple device approval URLs.
|
|
|
+// sees multiple device approval URLs, or if you try to approve a device
|
|
|
+// with the wrong control server.
|
|
|
func completeDeviceApproval(t *testing.T, node *TestNode, counter *atomic.Int32) func(string) error {
|
|
|
return func(urlStr string) error {
|
|
|
control := node.env.Control
|
|
|
nodeKey := node.MustStatus().Self.PublicKey
|
|
|
t.Logf("saw device approval URL %q", urlStr)
|
|
|
- if control.CompleteDeviceApproval(&nodeKey) {
|
|
|
+ if control.CompleteDeviceApproval(node.env.ControlURL(), urlStr, &nodeKey) {
|
|
|
if counter.Add(1) > 1 {
|
|
|
err := errors.New("completed multiple device approval URLs")
|
|
|
t.Error(err)
|
|
|
@@ -513,6 +514,182 @@ func TestOneNodeUpAuth(t *testing.T) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// Returns true if the error returned by [exec.Run] fails with a non-zero
|
|
|
+// exit code, false otherwise.
|
|
|
+func isNonZeroExitCode(err error) bool {
|
|
|
+ if err == nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ exitError, ok := err.(*exec.ExitError)
|
|
|
+ if !ok {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return exitError.ExitCode() != 0
|
|
|
+}
|
|
|
+
|
|
|
+// If we interrupt `tailscale up` and then run it again, we should only
|
|
|
+// print a single auth URL.
|
|
|
+func TestOneNodeUpInterruptedAuth(t *testing.T) {
|
|
|
+ tstest.Shard(t)
|
|
|
+ tstest.Parallel(t)
|
|
|
+
|
|
|
+ env := NewTestEnv(t, ConfigureControl(
|
|
|
+ func(control *testcontrol.Server) {
|
|
|
+ control.RequireAuth = true
|
|
|
+ control.AllNodesSameUser = true
|
|
|
+ },
|
|
|
+ ))
|
|
|
+
|
|
|
+ n := NewTestNode(t, env)
|
|
|
+ d := n.StartDaemon()
|
|
|
+ defer d.MustCleanShutdown(t)
|
|
|
+
|
|
|
+ cmdArgs := []string{"up", "--login-server=" + env.ControlURL()}
|
|
|
+
|
|
|
+ // The first time we run the command, we wait for an auth URL to be
|
|
|
+ // printed, and then we cancel the command -- equivalent to ^C.
|
|
|
+ //
|
|
|
+ // At this point, we've connected to control to get an auth URL,
|
|
|
+ // and printed it in the CLI, but not clicked it.
|
|
|
+ t.Logf("Running command for the first time: %s", strings.Join(cmdArgs, " "))
|
|
|
+ cmd1 := n.Tailscale(cmdArgs...)
|
|
|
+
|
|
|
+ // This handler watches for auth URLs in stdout, then cancels the
|
|
|
+ // running `tailscale up` CLI command.
|
|
|
+ cmd1.Stdout = &authURLParserWriter{t: t, authURLFn: func(urlStr string) error {
|
|
|
+ t.Logf("saw auth URL %q", urlStr)
|
|
|
+ cmd1.Process.Kill()
|
|
|
+ return nil
|
|
|
+ }}
|
|
|
+ cmd1.Stderr = cmd1.Stdout
|
|
|
+
|
|
|
+ if err := cmd1.Run(); !isNonZeroExitCode(err) {
|
|
|
+ t.Fatalf("Command did not fail with non-zero exit code: %q", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Because we didn't click the auth URL, we should still be in NeedsLogin.
|
|
|
+ n.AwaitBackendState("NeedsLogin")
|
|
|
+
|
|
|
+ // The second time we run the command, we click the first auth URL we see
|
|
|
+ // and check that we log in correctly.
|
|
|
+ //
|
|
|
+ // In #17361, there was a bug where we'd print two auth URLs, and you could
|
|
|
+ // click either auth URL and log in to control, but logging in through the
|
|
|
+ // first URL would leave `tailscale up` hanging.
|
|
|
+ //
|
|
|
+ // Using `authURLHandler` ensures we only print the new, correct auth URL.
|
|
|
+ //
|
|
|
+ // If we print both URLs, it will throw an error because it only expects
|
|
|
+ // to log in with one auth URL.
|
|
|
+ //
|
|
|
+ // If we only print the stale auth URL, the test will timeout because
|
|
|
+ // `tailscale up` will never return.
|
|
|
+ t.Logf("Running command for the second time: %s", strings.Join(cmdArgs, " "))
|
|
|
+
|
|
|
+ var authURLCount atomic.Int32
|
|
|
+
|
|
|
+ cmd2 := n.Tailscale(cmdArgs...)
|
|
|
+ cmd2.Stdout = &authURLParserWriter{
|
|
|
+ t: t, authURLFn: completeLogin(t, env.Control, &authURLCount),
|
|
|
+ }
|
|
|
+ cmd2.Stderr = cmd2.Stdout
|
|
|
+
|
|
|
+ if err := cmd2.Run(); err != nil {
|
|
|
+ t.Fatalf("up: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ if urls := authURLCount.Load(); urls != 1 {
|
|
|
+ t.Errorf("Auth URLs completed = %d; want %d", urls, 1)
|
|
|
+ }
|
|
|
+
|
|
|
+ n.AwaitRunning()
|
|
|
+}
|
|
|
+
|
|
|
+// If we interrupt `tailscale up` and login successfully, but don't
|
|
|
+// complete the device approval, we should see the device approval URL
|
|
|
+// when we run `tailscale up` a second time.
|
|
|
+func TestOneNodeUpInterruptedDeviceApproval(t *testing.T) {
|
|
|
+ tstest.Shard(t)
|
|
|
+ tstest.Parallel(t)
|
|
|
+
|
|
|
+ env := NewTestEnv(t, ConfigureControl(
|
|
|
+ func(control *testcontrol.Server) {
|
|
|
+ control.RequireAuth = true
|
|
|
+ control.RequireMachineAuth = true
|
|
|
+ control.AllNodesSameUser = true
|
|
|
+ },
|
|
|
+ ))
|
|
|
+
|
|
|
+ n := NewTestNode(t, env)
|
|
|
+ d := n.StartDaemon()
|
|
|
+ defer d.MustCleanShutdown(t)
|
|
|
+
|
|
|
+ // The first time we run the command, we:
|
|
|
+ //
|
|
|
+ // * set a custom login URL
|
|
|
+ // * wait for an auth URL to be printed
|
|
|
+ // * click it to complete the login process
|
|
|
+ // * wait for a device approval URL to be printed
|
|
|
+ // * cancel the command, equivalent to ^C
|
|
|
+ //
|
|
|
+ // At this point, we've logged in to control, but our node isn't
|
|
|
+ // approved to connect to the tailnet.
|
|
|
+ cmd1Args := []string{"up", "--login-server=" + env.ControlURL()}
|
|
|
+ t.Logf("Running command: %s", strings.Join(cmd1Args, " "))
|
|
|
+ cmd1 := n.Tailscale(cmd1Args...)
|
|
|
+
|
|
|
+ handler1 := &authURLParserWriter{t: t,
|
|
|
+ authURLFn: completeLogin(t, env.Control, &atomic.Int32{}),
|
|
|
+ deviceApprovalURLFn: func(urlStr string) error {
|
|
|
+ t.Logf("saw device approval URL %q", urlStr)
|
|
|
+ cmd1.Process.Kill()
|
|
|
+ return nil
|
|
|
+ },
|
|
|
+ }
|
|
|
+ cmd1.Stdout = handler1
|
|
|
+ cmd1.Stderr = cmd1.Stdout
|
|
|
+
|
|
|
+ if err := cmd1.Run(); !isNonZeroExitCode(err) {
|
|
|
+ t.Fatalf("Command did not fail with non-zero exit code: %q", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ // Because we logged in but we didn't complete the device approval, we
|
|
|
+ // should be in state NeedsMachineAuth.
|
|
|
+ n.AwaitBackendState("NeedsMachineAuth")
|
|
|
+
|
|
|
+ // The second time we run the command, we expect not to get an auth URL
|
|
|
+ // and go straight to the device approval URL. We don't need to pass the
|
|
|
+ // login server, because `tailscale up` should remember our control URL.
|
|
|
+ cmd2Args := []string{"up"}
|
|
|
+ t.Logf("Running command: %s", strings.Join(cmd2Args, " "))
|
|
|
+
|
|
|
+ var deviceApprovalURLCount atomic.Int32
|
|
|
+
|
|
|
+ cmd2 := n.Tailscale(cmd2Args...)
|
|
|
+ cmd2.Stdout = &authURLParserWriter{t: t,
|
|
|
+ authURLFn: func(urlStr string) error {
|
|
|
+ t.Fatalf("got unexpected auth URL: %q", urlStr)
|
|
|
+ cmd2.Process.Kill()
|
|
|
+ return nil
|
|
|
+ },
|
|
|
+ deviceApprovalURLFn: completeDeviceApproval(t, n, &deviceApprovalURLCount),
|
|
|
+ }
|
|
|
+ cmd2.Stderr = cmd2.Stdout
|
|
|
+
|
|
|
+ if err := cmd2.Run(); err != nil {
|
|
|
+ t.Fatalf("up: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ wantDeviceApprovalURLCount := int32(1)
|
|
|
+ if n := deviceApprovalURLCount.Load(); n != wantDeviceApprovalURLCount {
|
|
|
+ t.Errorf("Device approval URLs completed = %d; want %d", n, wantDeviceApprovalURLCount)
|
|
|
+ }
|
|
|
+
|
|
|
+ n.AwaitRunning()
|
|
|
+}
|
|
|
+
|
|
|
func TestConfigFileAuthKey(t *testing.T) {
|
|
|
tstest.SkipOnUnshardedCI(t)
|
|
|
tstest.Shard(t)
|