Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
//go:build (linux && !android) || (darwin && !ios) || freebsd || openbsd
package tailssh
import ( "context" "io" "os" "os/exec" "strings" "testing" "time")
// TestMonitorExecCancellationAfterDispatch delays the exec reply until after// cancellation. The session must retain the child handle and terminate it.func TestMonitorExecCancellationAfterDispatch(t *testing.T) { client, peer := rpcTestPair(t) mc := &monitorClient{ c: client.c, rpc: client, pending: make(map[int64]chan *rpcResult), done: make(chan struct{}), } go mc.readLoop() t.Cleanup(func() { client.c.Close() <-mc.done }) c := &conn{srv: &server{logf: t.Logf}} c.monitor.mon = mc mc.SetExitCallback(c.monitor.exits.dispatchExit) ctx, cancel := context.WithCancel(context.Background()) defer cancel() ss := &sshSession{conn: c, ctx: ctx, logf: t.Logf} f, err := os.OpenFile(os.DevNull, os.O_RDWR, 0) if err != nil { t.Fatal(err) } defer f.Close() launched := make(chan error, 1) cmd := exec.Command("/unused/incubator") stdio := []*os.File{f, f, f} go func() { launched <- ss.launchViaMonitor(cmd, nil, stdio, false) }() frame, fds, err := peer.readFrame() closeFds(fds) if err != nil { t.Fatal(err) } if frame.Op != opExec { t.Fatalf("op=%s; want exec", frame.Op) } cancel() child := monitorProcess{PID: 42, ID: frame.ID} if err := peer.writeFrame(rpcExecReply(frame.ID, child.PID)); err != nil { t.Fatal(err) } select { case err := <-launched: if err != nil { t.Fatal(err) } case <-time.After(5 * time.Second): t.Fatal("exec did not return its reply") } if ss.monChild != child { t.Fatalf("child=%v; want %v", ss.monChild, child) }
hungup := make(chan bool, 1) go func() { hungup <- ss.hangupMonitorProcess() }() frame, fds, err = peer.readFrame() closeFds(fds) if err != nil { t.Fatal(err) } if frame.Op != opSignal || frame.Pid != child.PID || frame.ChildID != child.ID || frame.Sig != "SIGHUP" { t.Fatalf("hangup frame=%+v", frame) } if err := peer.writeFrame(rpcOKReply(frame.ID, opSignal)); err != nil { t.Fatal(err) } if !<-hungup { t.Fatal("canceled session lost its monitor child") } if err := peer.writeFrame(rpcExitNotify(child, 129)); err != nil { t.Fatal(err) } select { case <-ss.monExit.done: case <-time.After(5 * time.Second): t.Fatal("exit notification not delivered") } // Escalation must neither consume the result nor send another signal. ss.escalateMonitorProcess(mc) for range 2 { code, ok, err := ss.waitMonitorProcess() if code != 129 || !ok || err != nil { t.Fatalf("wait=(%d,%v,%v)", code, ok, err) } }}
// A locally rejected request must not kill other sessions on the connection.func TestMonitorValidationFailureKeepsChannel(t *testing.T) { for _, tt := range []struct { name string frame rpcFrame nfds int wantErr string }{ {"line_cap", rpcExecReq(0, []string{strings.Repeat("x", rpcMaxLine)}, nil, "", false, false, 1), 1, "line cap"}, {"fd_count", rpcExecReq(0, []string{"/bin/true"}, nil, "", false, false, rpcMaxFds+1), rpcMaxFds + 1, "cannot carry"}, {"op_fd_count", rpcPingReq(0), 1, "cannot carry"}, } { t.Run(tt.name, func(t *testing.T) { mc := startTestMonitor(t, os.Getuid(), os.Getgid(), testGroups(t)) exits := collectExits(mc) child, err := mc.Exec(mustCtx(t), []string{"/bin/sleep", "30"}, nil, "/", false, nil) if err != nil { t.Fatal(err) } fds := make([]int, tt.nfds) for i := range fds { fds[i] = devNullFd(t) } _, _, err = mc.call(mustCtx(t), tt.frame, fds...) if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("call error = %v; want %q", err, tt.wantErr) } mc.mu.Lock() npending := len(mc.pending) mc.mu.Unlock() if npending != 0 { t.Fatalf("pending requests = %d; want 0", npending) } if err := mc.Ping(mustCtx(t)); err != nil { t.Fatalf("ping after validation failure: %v", err) } if err := mc.Signal(mustCtx(t), child, "SIGTERM"); err != nil { t.Fatal(err) } note := waitExitNote(t, exits, 5*time.Second) if note.pid != child || note.code != 143 { t.Fatalf("exit=%+v; want child=%+v code=143", note, child) } }) }}
func TestMonitorWriteFailureClosesChannel(t *testing.T) { for _, partial := range []bool{false, true} { name := "closed_write" if partial { name = "partial_write" } t.Run(name, func(t *testing.T) { client, peer := rpcTestPair(t) mc := &monitorClient{ c: client.c, rpc: client, pending: make(map[int64]chan *rpcResult), done: make(chan struct{}), } go mc.readLoop() t.Cleanup(func() { client.c.Close() <-mc.done }) frame := rpcPingReq(0) if partial { if err := client.c.SetWriteBuffer(1024); err != nil { t.Fatal(err) } client.writeTimeout = 100 * time.Millisecond frame = rpcExecReq(0, []string{strings.Repeat("x", 100<<10)}, nil, "", false, false, 0) } else if err := client.c.CloseWrite(); err != nil { t.Fatal(err) } if _, _, err := mc.call(mustCtx(t), frame); err == nil { t.Fatal("write succeeded unexpectedly") } select { case <-mc.done: case <-time.After(5 * time.Second): t.Fatal("write failure did not close the channel") } if err := peer.c.SetReadDeadline(time.Now().Add(time.Second)); err != nil { t.Fatal(err) } // Drain any partial frame and verify EOF triggers peer teardown. n, err := io.Copy(io.Discard, peer.c) if err != nil { t.Fatalf("drain peer: %v; want EOF", err) } if partial && (n == 0 || n >= 100<<10) { t.Fatalf("wrote %d bytes; want a partial frame", n) } }) }}
func TestMonitorReadFailureClosesChannel(t *testing.T) { client, peer := rpcTestPair(t) mc := &monitorClient{ c: client.c, rpc: client, pending: make(map[int64]chan *rpcResult), done: make(chan struct{}), } go mc.readLoop() t.Cleanup(func() { client.c.Close() <-mc.done }) if _, err := peer.c.Write([]byte("invalid JSON\n")); err != nil { t.Fatal(err) } select { case <-mc.done: case <-time.After(5 * time.Second): t.Fatal("read loop did not reject the invalid frame") } if err := peer.c.SetReadDeadline(time.Now().Add(time.Second)); err != nil { t.Fatal(err) } var b [1]byte if _, err := peer.c.Read(b[:]); err != io.EOF { t.Fatalf("peer read = %v; want EOF to trigger monitor teardown", err) }}
func TestMonitorRejectsStaleChildHandle(t *testing.T) { mc := startTestMonitor(t, os.Getuid(), os.Getgid(), testGroups(t)) exits := collectExits(mc) child, err := mc.Exec(mustCtx(t), []string{"/bin/sleep", "30"}, nil, "/", false, nil) if err != nil { t.Fatal(err) } stale := child stale.ID++ if err := mc.Signal(mustCtx(t), stale, "SIGKILL"); err == nil { t.Fatal("signal accepted a mismatched child generation") } if err := mc.Signal(mustCtx(t), child, "SIGTERM"); err != nil { t.Fatal(err) } note := waitExitNote(t, exits, 5*time.Second) if note.pid != child || note.code != 143 { t.Fatalf("exit=%+v; want child=%+v code=143", note, child) }}
func TestIncubatorRejectsIdentityAliases(t *testing.T) { m := &monitor{ args: monitorArgs{ uid: 1000, gid: 1000, gids: []uint32{1000}, localUser: "ubuntu", }, } for _, alias := range [][]string{ {"-uid=0"}, {"--uid", "0"}, {"-gid=0"}, {"--gid", "0"}, {"-groups=0"}, {"--groups", "0"}, {"-local-user=root"}, {"--local-user", "root"}, } { argv := append([]string{ "/tailscaled", "be-child", "ssh", "--uid=1", "--gid=1", "--groups=1", "--local-user=alice", }, alias...) if _, err := m.incubatorArgv(argv); err == nil { t.Errorf("accepted identity alias %q", alias) } }}