diff --git a/.github/workflows/ssh-integrationtest.yml b/.github/workflows/ssh-integrationtest.yml index 708d16b22..0c51648f4 100644 --- a/.github/workflows/ssh-integrationtest.yml +++ b/.github/workflows/ssh-integrationtest.yml @@ -43,36 +43,13 @@ jobs: steps: - name: Check out code uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Create test user - run: | - set -euxo pipefail - - sudo dscl . -create /Groups/groupone - sudo dscl . -create /Groups/groupone PrimaryGroupID 10000 - sudo dscl . -create /Groups/groupone Password '*' - - sudo dscl . -create /Groups/grouptwo - sudo dscl . -create /Groups/grouptwo PrimaryGroupID 10001 - sudo dscl . -create /Groups/grouptwo Password '*' - - sudo dscl . -create /Users/testuser - sudo dscl . -create /Users/testuser UserShell /bin/bash - sudo dscl . -create /Users/testuser RealName 'Test User' - sudo dscl . -create /Users/testuser UniqueID 10002 - sudo dscl . -create /Users/testuser PrimaryGroupID 10000 - sudo dscl . -create /Users/testuser NFSHomeDirectory /Users/testuser - - sudo mkdir -p /Users/testuser - sudo chown -R testuser:groupone /Users/testuser - sudo chmod 0755 /Users/testuser - - sudo dseditgroup -o edit -a testuser -t user groupone - sudo dseditgroup -o edit -a testuser -t user grouptwo - id testuser - name: Build test binaries run: | ./tool/go test -tags integrationtest -c ./ssh/tailssh -o /tmp/tailssh.test ./tool/go build -o /tmp/tailscaled ./cmd/tailscaled - - name: Run macOS SSH integration tests + - name: Run macOS OpenSSH exit status integration tests run: | - sudo env "PATH=$PATH" TAILSCALED_PATH=/tmp/tailscaled /tmp/tailssh.test -test.v -test.timeout=2m -test.run '^(TestOpenSSHExitCodes|TestIntegrationExitCodes)$' + sudo env "PATH=$PATH" TAILSCALED_PATH=/tmp/tailscaled TS_SSH_INTEGRATION_TEST_USER=runner /tmp/tailssh.test -test.v -test.timeout=3m -test.run '^TestOpenSSHExitCodes$' + - name: Run macOS Go SSH exit status integration tests + run: | + sudo env "PATH=$PATH" TAILSCALED_PATH=/tmp/tailscaled TS_SSH_INTEGRATION_TEST_USER=runner /tmp/tailssh.test -test.v -test.timeout=3m -test.run '^TestIntegrationExitCodes$' diff --git a/ssh/tailssh/tailssh_integration_test.go b/ssh/tailssh/tailssh_integration_test.go index 14d4b4a27..e4d2fc83a 100644 --- a/ssh/tailssh/tailssh_integration_test.go +++ b/ssh/tailssh/tailssh_integration_test.go @@ -731,8 +731,11 @@ readLoop: func testClient(t *testing.T, forceV1Behavior bool, allowSendEnv bool, authMethods ...ssh.AuthMethod) *ssh.Client { t.Helper() + return testClientForUser(t, "testuser", forceV1Behavior, allowSendEnv, authMethods...) +} - username := "testuser" +func testClientForUser(t *testing.T, username string, forceV1Behavior bool, allowSendEnv bool, authMethods ...ssh.AuthMethod) *ssh.Client { + t.Helper() addr := testServer(t, username, forceV1Behavior, allowSendEnv) cl, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{ @@ -957,6 +960,7 @@ func (conn *addressFakingConn) RemoteAddr() net.Addr { func TestIntegrationExitCodes(t *testing.T) { debugTest.Store(true) t.Cleanup(func() { debugTest.Store(false) }) + username := exitCodeTestUser() tests := []struct { name string @@ -984,23 +988,49 @@ func TestIntegrationExitCodes(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := testSession(t, false, false, nil) - err := s.Run(tt.cmd) + cl := testClientForUser(t, username, false, false) + s, err := cl.NewSession() + if err != nil { + t.Fatal(err) + } + defer s.Close() + + type result struct { + out []byte + err error + } + done := make(chan result, 1) + go func() { + out, err := s.CombinedOutput(tt.cmd) + done <- result{out: out, err: err} + }() + + var out []byte + select { + case res := <-done: + out = res.out + err = res.err + case <-time.After(20 * time.Second): + s.Close() + cl.Close() + t.Fatalf("ssh command %q timed out", tt.cmd) + } + if tt.wantCode == 0 { if err != nil { - t.Fatalf("expected exit code 0, got error: %v", err) + t.Fatalf("expected exit code 0, got error: %v; output:\n%s", err, out) } return } if err == nil { - t.Fatalf("expected exit code %d, got nil error", tt.wantCode) + t.Fatalf("expected exit code %d, got nil error; output:\n%s", tt.wantCode, out) } var exitErr *ssh.ExitError if !errors.As(err, &exitErr) { - t.Fatalf("expected *ssh.ExitError, got %T: %v", err, err) + t.Fatalf("expected *ssh.ExitError, got %T: %v; output:\n%s", err, err, out) } if exitErr.ExitStatus() != tt.wantCode { - t.Errorf("exit code = %d, want %d", exitErr.ExitStatus(), tt.wantCode) + t.Errorf("exit code = %d, want %d; output:\n%s", exitErr.ExitStatus(), tt.wantCode, out) } }) } @@ -1015,8 +1045,9 @@ func TestOpenSSHExitCodes(t *testing.T) { debugTest.Store(true) t.Cleanup(func() { debugTest.Store(false) }) + username := exitCodeTestUser() - addr := testServer(t, "testuser", false, false) + addr := testServer(t, username, false, false) host, port, err := net.SplitHostPort(addr) if err != nil { t.Fatal(err) @@ -1072,7 +1103,7 @@ func TestOpenSSHExitCodes(t *testing.T) { "-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null", "-p", port, - "testuser@"+host, + username+"@"+host, tt.cmd, ) out, err := cmd.CombinedOutput() @@ -1086,6 +1117,13 @@ func TestOpenSSHExitCodes(t *testing.T) { } } +func exitCodeTestUser() string { + if username := os.Getenv("TS_SSH_INTEGRATION_TEST_USER"); username != "" { + return username + } + return "testuser" +} + // TestLocalUnixForwardingHalfClose verifies that the bidirectional copy // in Unix socket forwarding uses half-close correctly: when one direction // finishes, the other direction's data is not lost. This tests the bicopy