ssh/tailssh: harden macOS exit status tests

Use the existing macOS runner account for focused exit-status coverage instead of provisioning a synthetic Directory Services user.

Also drain Go SSH client output and add a per-command timeout so stderr output cannot block the test process until the global test timeout expires.

Updates #18256

Change-Id: Ic4a0f391c56210023ece20c13d8627b0f5ad68e7
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
Kristoffer Dalby 2026-05-04 12:10:53 +00:00
parent c4a0f391c5
commit 5ea2dedd45
2 changed files with 52 additions and 37 deletions

View File

@ -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$'

View File

@ -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