ssh/tailssh: add exit code tests for session shutdown behavior

Add tests verifying SSH exit codes are correctly delivered to the
client:
- exit code 0 for successful commands
- exit code passthrough for failed commands (e.g. exit 42)
- exit code 127 for command not found (POSIX convention)
- exit code 254 for recording upload denial (assert in existing test)

Updates #18256

Change-Id: I5ab09bd45557c3af1fab15519e61f5f4d2898f40
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2026-04-08 12:00:44 +00:00
parent 5e2e75a007
commit 0c786f62aa

View File

@ -523,6 +523,7 @@ func TestSSHRecordingCancelsSessionsOnUploadFailure(t *testing.T) {
handler func(w http.ResponseWriter, r *http.Request)
sshCommand string
wantClientOutput string
wantExitCode int // expected SSH exit code; 0 means don't check
clientOutputMustNotContain []string
}{
@ -533,6 +534,7 @@ func TestSSHRecordingCancelsSessionsOnUploadFailure(t *testing.T) {
},
sshCommand: "echo hello",
wantClientOutput: "session rejected\r\n",
wantExitCode: 254,
clientOutputMustNotContain: []string{"hello"},
},
@ -580,6 +582,16 @@ func TestSSHRecordingCancelsSessionsOnUploadFailure(t *testing.T) {
} else {
t.Errorf("client did not get kicked out: %q", got)
}
if tt.wantExitCode != 0 {
var exitErr *testssh.ExitError
if errors.As(err, &exitErr) {
if exitErr.ExitStatus() != tt.wantExitCode {
t.Errorf("exit code = %d, want %d", exitErr.ExitStatus(), tt.wantExitCode)
}
} else {
t.Errorf("expected *ssh.ExitError, got %T: %v", err, err)
}
}
gotStr := string(got)
if !strings.HasSuffix(gotStr, tt.wantClientOutput) {
t.Errorf("client got %q, want %q", got, tt.wantClientOutput)
@ -1218,6 +1230,45 @@ func TestSSH(t *testing.T) {
t.Errorf("got %q; want %q", got, str)
}
})
t.Run("exit_code_zero", func(t *testing.T) {
cmd := execSSH("true")
if err := cmd.Run(); err != nil {
t.Fatalf("expected exit code 0, got error: %v", err)
}
})
t.Run("exit_code_passthrough", func(t *testing.T) {
cmd := execSSH("exit 42")
err := cmd.Run()
if err == nil {
t.Fatal("expected non-zero exit code")
}
var ee *exec.ExitError
if !errors.As(err, &ee) {
t.Fatalf("expected *exec.ExitError, got %T: %v", err, err)
}
if got := ee.ExitCode(); got != 42 {
t.Errorf("exit code = %d, want 42", got)
}
})
t.Run("exit_code_127_command_not_found", func(t *testing.T) {
cmd := execSSH("/nonexistent/binary")
err := cmd.Run()
if err == nil {
t.Fatal("expected non-zero exit code")
}
var ee *exec.ExitError
if !errors.As(err, &ee) {
t.Fatalf("expected *exec.ExitError, got %T: %v", err, err)
}
// Exit code 127 for command not found, per POSIX shell convention.
// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02
if got := ee.ExitCode(); got != 127 {
t.Errorf("exit code = %d, want 127", got)
}
})
}
func parseEnv(out []byte) map[string]string {