incubator.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // This file contains the code for the incubator process. Tailscaled
  4. // launches the incubator as the same user as it was launched as. The
  5. // incubator then registers a new session with the OS, sets its UID
  6. // and groups to the specified `--uid`, `--gid` and `--groups`, and
  7. // then launches the requested `--cmd`.
  8. //go:build linux || (darwin && !ios) || freebsd || openbsd
  9. package tailssh
  10. import (
  11. "errors"
  12. "flag"
  13. "fmt"
  14. "io"
  15. "log"
  16. "log/syslog"
  17. "os"
  18. "os/exec"
  19. "path/filepath"
  20. "runtime"
  21. "slices"
  22. "sort"
  23. "strconv"
  24. "strings"
  25. "syscall"
  26. "github.com/creack/pty"
  27. "github.com/pkg/sftp"
  28. "github.com/u-root/u-root/pkg/termios"
  29. gossh "golang.org/x/crypto/ssh"
  30. "golang.org/x/sys/unix"
  31. "tailscale.com/cmd/tailscaled/childproc"
  32. "tailscale.com/hostinfo"
  33. "tailscale.com/tempfork/gliderlabs/ssh"
  34. "tailscale.com/types/logger"
  35. "tailscale.com/version/distro"
  36. )
  37. func init() {
  38. childproc.Add("ssh", beIncubator)
  39. }
  40. var ptyName = func(f *os.File) (string, error) {
  41. return "", fmt.Errorf("unimplemented")
  42. }
  43. // maybeStartLoginSession starts a new login session for the specified UID.
  44. // On success, it may return a non-nil close func which must be closed to
  45. // release the session.
  46. // See maybeStartLoginSessionLinux.
  47. var maybeStartLoginSession = func(logf logger.Logf, ia incubatorArgs) (close func() error, err error) {
  48. return nil, nil
  49. }
  50. // newIncubatorCommand returns a new exec.Cmd configured with
  51. // `tailscaled be-child ssh` as the entrypoint.
  52. //
  53. // If ss.srv.tailscaledPath is empty, this method is equivalent to
  54. // exec.CommandContext.
  55. //
  56. // The returned Cmd.Env is guaranteed to be nil; the caller populates it.
  57. func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) {
  58. defer func() {
  59. if cmd.Env != nil {
  60. panic("internal error")
  61. }
  62. }()
  63. var (
  64. name string
  65. args []string
  66. isSFTP bool
  67. isShell bool
  68. )
  69. switch ss.Subsystem() {
  70. case "sftp":
  71. isSFTP = true
  72. case "":
  73. name = ss.conn.localUser.LoginShell()
  74. if rawCmd := ss.RawCommand(); rawCmd != "" {
  75. args = append(args, "-c", rawCmd)
  76. } else {
  77. isShell = true
  78. args = append(args, "-l") // login shell
  79. }
  80. default:
  81. panic(fmt.Sprintf("unexpected subsystem: %v", ss.Subsystem()))
  82. }
  83. if ss.conn.srv.tailscaledPath == "" {
  84. // TODO(maisem): this doesn't work with sftp
  85. return exec.CommandContext(ss.ctx, name, args...)
  86. }
  87. lu := ss.conn.localUser
  88. ci := ss.conn.info
  89. gids := strings.Join(ss.conn.userGroupIDs, ",")
  90. remoteUser := ci.uprof.LoginName
  91. if ci.node.IsTagged() {
  92. remoteUser = strings.Join(ci.node.Tags().AsSlice(), ",")
  93. }
  94. incubatorArgs := []string{
  95. "be-child",
  96. "ssh",
  97. "--uid=" + lu.Uid,
  98. "--gid=" + lu.Gid,
  99. "--groups=" + gids,
  100. "--local-user=" + lu.Username,
  101. "--remote-user=" + remoteUser,
  102. "--remote-ip=" + ci.src.Addr().String(),
  103. "--has-tty=false", // updated in-place by startWithPTY
  104. "--tty-name=", // updated in-place by startWithPTY
  105. }
  106. if isSFTP {
  107. incubatorArgs = append(incubatorArgs, "--sftp")
  108. } else {
  109. if isShell {
  110. incubatorArgs = append(incubatorArgs, "--shell")
  111. }
  112. // Only the macOS version of the login command supports executing a
  113. // command, all other versions only support launching a shell
  114. // without taking any arguments.
  115. shouldUseLoginCmd := isShell || runtime.GOOS == "darwin"
  116. if hostinfo.IsSELinuxEnforcing() {
  117. // If we're running on a SELinux-enabled system, the login
  118. // command will be unable to set the correct context for the
  119. // shell. Fall back to using the incubator to launch the shell.
  120. // See http://github.com/tailscale/tailscale/issues/4908.
  121. shouldUseLoginCmd = false
  122. }
  123. if shouldUseLoginCmd {
  124. if lp, err := exec.LookPath("login"); err == nil {
  125. incubatorArgs = append(incubatorArgs, "--login-cmd="+lp)
  126. }
  127. }
  128. incubatorArgs = append(incubatorArgs, "--cmd="+name)
  129. if len(args) > 0 {
  130. incubatorArgs = append(incubatorArgs, "--")
  131. incubatorArgs = append(incubatorArgs, args...)
  132. }
  133. }
  134. return exec.CommandContext(ss.ctx, ss.conn.srv.tailscaledPath, incubatorArgs...)
  135. }
  136. const debugIncubator = false
  137. type stdRWC struct{}
  138. func (stdRWC) Read(p []byte) (n int, err error) {
  139. return os.Stdin.Read(p)
  140. }
  141. func (stdRWC) Write(b []byte) (n int, err error) {
  142. return os.Stdout.Write(b)
  143. }
  144. func (stdRWC) Close() error {
  145. os.Exit(0)
  146. return nil
  147. }
  148. type incubatorArgs struct {
  149. uid int
  150. gid int
  151. groups string
  152. localUser string
  153. remoteUser string
  154. remoteIP string
  155. ttyName string
  156. hasTTY bool
  157. cmdName string
  158. isSFTP bool
  159. isShell bool
  160. loginCmdPath string
  161. cmdArgs []string
  162. }
  163. func parseIncubatorArgs(args []string) (a incubatorArgs) {
  164. flags := flag.NewFlagSet("", flag.ExitOnError)
  165. flags.IntVar(&a.uid, "uid", 0, "the uid of local-user")
  166. flags.IntVar(&a.gid, "gid", 0, "the gid of local-user")
  167. flags.StringVar(&a.groups, "groups", "", "comma-separated list of gids of local-user")
  168. flags.StringVar(&a.localUser, "local-user", "", "the user to run as")
  169. flags.StringVar(&a.remoteUser, "remote-user", "", "the remote user/tags")
  170. flags.StringVar(&a.remoteIP, "remote-ip", "", "the remote Tailscale IP")
  171. flags.StringVar(&a.ttyName, "tty-name", "", "the tty name (pts/3)")
  172. flags.BoolVar(&a.hasTTY, "has-tty", false, "is the output attached to a tty")
  173. flags.StringVar(&a.cmdName, "cmd", "", "the cmd to launch (ignored in sftp mode)")
  174. flags.BoolVar(&a.isShell, "shell", false, "is launching a shell (with no cmds)")
  175. flags.BoolVar(&a.isSFTP, "sftp", false, "run sftp server (cmd is ignored)")
  176. flags.StringVar(&a.loginCmdPath, "login-cmd", "", "the path to `login` cmd")
  177. flags.Parse(args)
  178. a.cmdArgs = flags.Args()
  179. return a
  180. }
  181. // beIncubator is the entrypoint to the `tailscaled be-child ssh` subcommand.
  182. // It is responsible for informing the system of a new login session for the user.
  183. // This is sometimes necessary for mounting home directories and decrypting file
  184. // systems.
  185. //
  186. // Tailscaled launches the incubator as the same user as it was
  187. // launched as. The incubator then registers a new session with the
  188. // OS, sets its UID and groups to the specified `--uid`, `--gid` and
  189. // `--groups` and then launches the requested `--cmd`.
  190. func beIncubator(args []string) error {
  191. // To defend against issues like https://golang.org/issue/1435,
  192. // defensively lock our current goroutine's thread to the current
  193. // system thread before we start making any UID/GID/group changes.
  194. //
  195. // This shouldn't matter on Linux because syscall.AllThreadsSyscall is
  196. // used to invoke syscalls on all OS threads, but (as of 2023-03-23)
  197. // that function is not implemented on all platforms.
  198. runtime.LockOSThread()
  199. defer runtime.UnlockOSThread()
  200. ia := parseIncubatorArgs(args)
  201. if ia.isSFTP && ia.isShell {
  202. return fmt.Errorf("--sftp and --shell are mutually exclusive")
  203. }
  204. logf := logger.Discard
  205. if debugIncubator {
  206. // We don't own stdout or stderr, so the only place we can log is syslog.
  207. if sl, err := syslog.New(syslog.LOG_INFO|syslog.LOG_DAEMON, "tailscaled-ssh"); err == nil {
  208. logf = log.New(sl, "", 0).Printf
  209. }
  210. }
  211. euid := os.Geteuid()
  212. runningAsRoot := euid == 0
  213. if runningAsRoot && ia.loginCmdPath != "" {
  214. // Check if we can exec into the login command instead of trying to
  215. // incubate ourselves.
  216. if la := ia.loginArgs(); la != nil {
  217. return unix.Exec(ia.loginCmdPath, la, os.Environ())
  218. }
  219. }
  220. // Inform the system that we are about to log someone in.
  221. // We can only do this if we are running as root.
  222. // This is best effort to still allow running on machines where
  223. // we don't support starting sessions, e.g. darwin.
  224. sessionCloser, err := maybeStartLoginSession(logf, ia)
  225. if err == nil && sessionCloser != nil {
  226. defer sessionCloser()
  227. }
  228. var groupIDs []int
  229. for _, g := range strings.Split(ia.groups, ",") {
  230. gid, err := strconv.ParseInt(g, 10, 32)
  231. if err != nil {
  232. return err
  233. }
  234. groupIDs = append(groupIDs, int(gid))
  235. }
  236. if err := dropPrivileges(logf, ia.uid, ia.gid, groupIDs); err != nil {
  237. return err
  238. }
  239. if ia.isSFTP {
  240. logf("handling sftp")
  241. server, err := sftp.NewServer(stdRWC{})
  242. if err != nil {
  243. return err
  244. }
  245. // TODO(https://github.com/pkg/sftp/pull/554): Revert the check for io.EOF,
  246. // when sftp is patched to report clean termination.
  247. if err := server.Serve(); err != nil && err != io.EOF {
  248. return err
  249. }
  250. return nil
  251. }
  252. cmd := exec.Command(ia.cmdName, ia.cmdArgs...)
  253. cmd.Stdin = os.Stdin
  254. cmd.Stdout = os.Stdout
  255. cmd.Stderr = os.Stderr
  256. cmd.Env = os.Environ()
  257. if ia.hasTTY {
  258. // If we were launched with a tty then we should
  259. // mark that as the ctty of the child. However,
  260. // as the ctty is being passed from the parent
  261. // we set the child to foreground instead which
  262. // also passes the ctty.
  263. // However, we can not do this if never had a tty to
  264. // begin with.
  265. cmd.SysProcAttr = &syscall.SysProcAttr{
  266. Foreground: true,
  267. }
  268. }
  269. err = cmd.Run()
  270. if ee, ok := err.(*exec.ExitError); ok {
  271. ps := ee.ProcessState
  272. code := ps.ExitCode()
  273. if code < 0 {
  274. // TODO(bradfitz): do we need to also check the syscall.WaitStatus
  275. // and make our process look like it also died by signal/same signal
  276. // as our child process? For now we just do the exit code.
  277. fmt.Fprintf(os.Stderr, "[tailscale-ssh: process died: %v]\n", ps.String())
  278. code = 1 // for now. so we don't exit with negative
  279. }
  280. os.Exit(code)
  281. }
  282. return err
  283. }
  284. const (
  285. // This controls whether we assert that our privileges were dropped
  286. // using geteuid/getegid; it's a const and not an envknob because the
  287. // incubator doesn't see the parent's environment.
  288. //
  289. // TODO(andrew): remove this const and always do this after sufficient
  290. // testing, e.g. the 1.40 release
  291. assertPrivilegesWereDropped = true
  292. // TODO(andrew-d): verify that this works in more configurations before
  293. // enabling by default.
  294. assertPrivilegesWereDroppedByAttemptingToUnDrop = false
  295. )
  296. // dropPrivileges contains all the logic for dropping privileges to a different
  297. // UID, GID, and set of supplementary groups. This function is
  298. // security-sensitive and ordering-dependent; please be very cautious if/when
  299. // refactoring.
  300. //
  301. // WARNING: if you change this function, you *MUST* run the TestDropPrivileges
  302. // test in this package as root on at least Linux, FreeBSD and Darwin. This can
  303. // be done by running:
  304. //
  305. // go test -c ./ssh/tailssh/ && sudo ./tailssh.test -test.v -test.run TestDropPrivileges
  306. func dropPrivileges(logf logger.Logf, wantUid, wantGid int, supplementaryGroups []int) error {
  307. fatalf := func(format string, args ...any) {
  308. logf("[unexpected] error dropping privileges: "+format, args...)
  309. os.Exit(1)
  310. }
  311. euid := os.Geteuid()
  312. egid := os.Getegid()
  313. if runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" {
  314. // On FreeBSD and Darwin, the first entry returned from the
  315. // getgroups(2) syscall is the egid, and changing it with
  316. // setgroups(2) changes the egid of the process. This is
  317. // technically a violation of the POSIX standard; see the
  318. // following article for more detail:
  319. // https://www.usenix.org/system/files/login/articles/325-tsafrir.pdf
  320. //
  321. // In this case, we add an entry at the beginning of the
  322. // groupIDs list containing the expected gid if it's not
  323. // already there, which modifies the egid and additional groups
  324. // as one unit.
  325. if len(supplementaryGroups) == 0 || supplementaryGroups[0] != wantGid {
  326. supplementaryGroups = append([]int{wantGid}, supplementaryGroups...)
  327. }
  328. }
  329. if err := setGroups(supplementaryGroups); err != nil {
  330. return err
  331. }
  332. if egid != wantGid {
  333. // On FreeBSD and Darwin, we may have already called the
  334. // equivalent of setegid(wantGid) via the call to setGroups,
  335. // above. However, per the manpage, setgid(getegid()) is an
  336. // allowed operation regardless of privilege level.
  337. //
  338. // FreeBSD:
  339. // The setgid() system call is permitted if the specified ID
  340. // is equal to the real group ID or the effective group ID
  341. // of the process, or if the effective user ID is that of
  342. // the super user.
  343. //
  344. // Darwin:
  345. // The setgid() function is permitted if the effective
  346. // user ID is that of the super user, or if the specified
  347. // group ID is the same as the effective group ID. If
  348. // not, but the specified group ID is the same as the real
  349. // group ID, setgid() will set the effective group ID to
  350. // the real group ID.
  351. if err := syscall.Setgid(wantGid); err != nil {
  352. fatalf("Setgid(%d): %v", wantGid, err)
  353. }
  354. }
  355. if euid != wantUid {
  356. // Switch users if required before starting the desired process.
  357. if err := syscall.Setuid(wantUid); err != nil {
  358. fatalf("Setuid(%d): %v", wantUid, err)
  359. }
  360. }
  361. // If we changed either the UID or GID, defensively assert that we
  362. // cannot reset the it back to our original values, and that the
  363. // current egid/euid are the expected values after we change
  364. // everything; if not, we exit the process.
  365. if assertPrivilegesWereDroppedByAttemptingToUnDrop {
  366. if egid != wantGid {
  367. if err := syscall.Setegid(egid); err == nil {
  368. fatalf("able to set egid back to %d", egid)
  369. }
  370. }
  371. if euid != wantUid {
  372. if err := syscall.Seteuid(euid); err == nil {
  373. fatalf("able to set euid back to %d", euid)
  374. }
  375. }
  376. }
  377. if assertPrivilegesWereDropped {
  378. if got := os.Getegid(); got != wantGid {
  379. fatalf("got egid=%d, want %d", got, wantGid)
  380. }
  381. if got := os.Geteuid(); got != wantUid {
  382. fatalf("got euid=%d, want %d", got, wantUid)
  383. }
  384. // TODO(andrew-d): assert that our supplementary groups are correct
  385. }
  386. return nil
  387. }
  388. // launchProcess launches an incubator process for the provided session.
  389. // It is responsible for configuring the process execution environment.
  390. // The caller can wait for the process to exit by calling cmd.Wait().
  391. //
  392. // It sets ss.cmd, stdin, stdout, and stderr.
  393. func (ss *sshSession) launchProcess() error {
  394. ss.cmd = ss.newIncubatorCommand()
  395. cmd := ss.cmd
  396. homeDir := ss.conn.localUser.HomeDir
  397. if _, err := os.Stat(homeDir); err == nil {
  398. cmd.Dir = homeDir
  399. } else if os.IsNotExist(err) {
  400. // If the home directory doesn't exist, we can't chdir to it.
  401. // Instead, we'll chdir to the root directory.
  402. cmd.Dir = "/"
  403. } else {
  404. return err
  405. }
  406. cmd.Env = envForUser(ss.conn.localUser)
  407. for _, kv := range ss.Environ() {
  408. if acceptEnvPair(kv) {
  409. cmd.Env = append(cmd.Env, kv)
  410. }
  411. }
  412. ci := ss.conn.info
  413. cmd.Env = append(cmd.Env,
  414. fmt.Sprintf("SSH_CLIENT=%s %d %d", ci.src.Addr(), ci.src.Port(), ci.dst.Port()),
  415. fmt.Sprintf("SSH_CONNECTION=%s %d %s %d", ci.src.Addr(), ci.src.Port(), ci.dst.Addr(), ci.dst.Port()),
  416. )
  417. if ss.agentListener != nil {
  418. cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_AUTH_SOCK=%s", ss.agentListener.Addr()))
  419. }
  420. ptyReq, winCh, isPty := ss.Pty()
  421. if !isPty {
  422. ss.logf("starting non-pty command: %+v", cmd.Args)
  423. return ss.startWithStdPipes()
  424. }
  425. if sshDisablePTY() {
  426. ss.logf("pty support disabled by envknob")
  427. return errors.New("pty support disabled by envknob")
  428. }
  429. ss.ptyReq = &ptyReq
  430. pty, tty, err := ss.startWithPTY()
  431. if err != nil {
  432. return err
  433. }
  434. // We need to be able to close stdin and stdout separately later so make a
  435. // dup.
  436. ptyDup, err := syscall.Dup(int(pty.Fd()))
  437. if err != nil {
  438. pty.Close()
  439. tty.Close()
  440. return err
  441. }
  442. go resizeWindow(ptyDup /* arbitrary fd */, winCh)
  443. ss.wrStdin = pty
  444. ss.rdStdout = os.NewFile(uintptr(ptyDup), pty.Name())
  445. ss.rdStderr = nil // not available for pty
  446. ss.childPipes = []io.Closer{tty}
  447. return nil
  448. }
  449. func resizeWindow(fd int, winCh <-chan ssh.Window) {
  450. for win := range winCh {
  451. unix.IoctlSetWinsize(fd, syscall.TIOCSWINSZ, &unix.Winsize{
  452. Row: uint16(win.Height),
  453. Col: uint16(win.Width),
  454. })
  455. }
  456. }
  457. // opcodeShortName is a mapping of SSH opcode
  458. // to mnemonic names expected by the termios package.
  459. // These are meant to be platform independent.
  460. var opcodeShortName = map[uint8]string{
  461. gossh.VINTR: "intr",
  462. gossh.VQUIT: "quit",
  463. gossh.VERASE: "erase",
  464. gossh.VKILL: "kill",
  465. gossh.VEOF: "eof",
  466. gossh.VEOL: "eol",
  467. gossh.VEOL2: "eol2",
  468. gossh.VSTART: "start",
  469. gossh.VSTOP: "stop",
  470. gossh.VSUSP: "susp",
  471. gossh.VDSUSP: "dsusp",
  472. gossh.VREPRINT: "rprnt",
  473. gossh.VWERASE: "werase",
  474. gossh.VLNEXT: "lnext",
  475. gossh.VFLUSH: "flush",
  476. gossh.VSWTCH: "swtch",
  477. gossh.VSTATUS: "status",
  478. gossh.VDISCARD: "discard",
  479. gossh.IGNPAR: "ignpar",
  480. gossh.PARMRK: "parmrk",
  481. gossh.INPCK: "inpck",
  482. gossh.ISTRIP: "istrip",
  483. gossh.INLCR: "inlcr",
  484. gossh.IGNCR: "igncr",
  485. gossh.ICRNL: "icrnl",
  486. gossh.IUCLC: "iuclc",
  487. gossh.IXON: "ixon",
  488. gossh.IXANY: "ixany",
  489. gossh.IXOFF: "ixoff",
  490. gossh.IMAXBEL: "imaxbel",
  491. gossh.IUTF8: "iutf8",
  492. gossh.ISIG: "isig",
  493. gossh.ICANON: "icanon",
  494. gossh.XCASE: "xcase",
  495. gossh.ECHO: "echo",
  496. gossh.ECHOE: "echoe",
  497. gossh.ECHOK: "echok",
  498. gossh.ECHONL: "echonl",
  499. gossh.NOFLSH: "noflsh",
  500. gossh.TOSTOP: "tostop",
  501. gossh.IEXTEN: "iexten",
  502. gossh.ECHOCTL: "echoctl",
  503. gossh.ECHOKE: "echoke",
  504. gossh.PENDIN: "pendin",
  505. gossh.OPOST: "opost",
  506. gossh.OLCUC: "olcuc",
  507. gossh.ONLCR: "onlcr",
  508. gossh.OCRNL: "ocrnl",
  509. gossh.ONOCR: "onocr",
  510. gossh.ONLRET: "onlret",
  511. gossh.CS7: "cs7",
  512. gossh.CS8: "cs8",
  513. gossh.PARENB: "parenb",
  514. gossh.PARODD: "parodd",
  515. gossh.TTY_OP_ISPEED: "tty_op_ispeed",
  516. gossh.TTY_OP_OSPEED: "tty_op_ospeed",
  517. }
  518. // startWithPTY starts cmd with a pseudo-terminal attached to Stdin, Stdout and Stderr.
  519. func (ss *sshSession) startWithPTY() (ptyFile, tty *os.File, err error) {
  520. ptyReq := ss.ptyReq
  521. cmd := ss.cmd
  522. if cmd == nil {
  523. return nil, nil, errors.New("nil ss.cmd")
  524. }
  525. if ptyReq == nil {
  526. return nil, nil, errors.New("nil ss.ptyReq")
  527. }
  528. ptyFile, tty, err = pty.Open()
  529. if err != nil {
  530. err = fmt.Errorf("pty.Open: %w", err)
  531. return
  532. }
  533. defer func() {
  534. if err != nil {
  535. ptyFile.Close()
  536. tty.Close()
  537. }
  538. }()
  539. ptyRawConn, err := tty.SyscallConn()
  540. if err != nil {
  541. return nil, nil, fmt.Errorf("SyscallConn: %w", err)
  542. }
  543. var ctlErr error
  544. if err := ptyRawConn.Control(func(fd uintptr) {
  545. // Load existing PTY settings to modify them & save them back.
  546. tios, err := termios.GTTY(int(fd))
  547. if err != nil {
  548. ctlErr = fmt.Errorf("GTTY: %w", err)
  549. return
  550. }
  551. // Set the rows & cols to those advertised from the ptyReq frame
  552. // received over SSH.
  553. tios.Row = int(ptyReq.Window.Height)
  554. tios.Col = int(ptyReq.Window.Width)
  555. for c, v := range ptyReq.Modes {
  556. if c == gossh.TTY_OP_ISPEED {
  557. tios.Ispeed = int(v)
  558. continue
  559. }
  560. if c == gossh.TTY_OP_OSPEED {
  561. tios.Ospeed = int(v)
  562. continue
  563. }
  564. k, ok := opcodeShortName[c]
  565. if !ok {
  566. ss.vlogf("unknown opcode: %d", c)
  567. continue
  568. }
  569. if _, ok := tios.CC[k]; ok {
  570. tios.CC[k] = uint8(v)
  571. continue
  572. }
  573. if _, ok := tios.Opts[k]; ok {
  574. tios.Opts[k] = v > 0
  575. continue
  576. }
  577. ss.vlogf("unsupported opcode: %v(%d)=%v", k, c, v)
  578. }
  579. // Save PTY settings.
  580. if _, err := tios.STTY(int(fd)); err != nil {
  581. ctlErr = fmt.Errorf("STTY: %w", err)
  582. return
  583. }
  584. }); err != nil {
  585. return nil, nil, fmt.Errorf("ptyRawConn.Control: %w", err)
  586. }
  587. if ctlErr != nil {
  588. return nil, nil, fmt.Errorf("ptyRawConn.Control func: %w", ctlErr)
  589. }
  590. cmd.SysProcAttr = &syscall.SysProcAttr{
  591. Setctty: true,
  592. Setsid: true,
  593. }
  594. updateStringInSlice(cmd.Args, "--has-tty=false", "--has-tty=true")
  595. if ptyName, err := ptyName(ptyFile); err == nil {
  596. updateStringInSlice(cmd.Args, "--tty-name=", "--tty-name="+ptyName)
  597. fullPath := filepath.Join("/dev", ptyName)
  598. cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_TTY=%s", fullPath))
  599. }
  600. if ptyReq.Term != "" {
  601. cmd.Env = append(cmd.Env, fmt.Sprintf("TERM=%s", ptyReq.Term))
  602. }
  603. cmd.Stdin = tty
  604. cmd.Stdout = tty
  605. cmd.Stderr = tty
  606. ss.logf("starting pty command: %+v", cmd.Args)
  607. if err = cmd.Start(); err != nil {
  608. return
  609. }
  610. return ptyFile, tty, nil
  611. }
  612. // startWithStdPipes starts cmd with os.Pipe for Stdin, Stdout and Stderr.
  613. func (ss *sshSession) startWithStdPipes() (err error) {
  614. var rdStdin, wrStdout, wrStderr io.ReadWriteCloser
  615. defer func() {
  616. if err != nil {
  617. closeAll(rdStdin, ss.wrStdin, ss.rdStdout, wrStdout, ss.rdStderr, wrStderr)
  618. }
  619. }()
  620. if ss.cmd == nil {
  621. return errors.New("nil cmd")
  622. }
  623. if rdStdin, ss.wrStdin, err = os.Pipe(); err != nil {
  624. return err
  625. }
  626. if ss.rdStdout, wrStdout, err = os.Pipe(); err != nil {
  627. return err
  628. }
  629. if ss.rdStderr, wrStderr, err = os.Pipe(); err != nil {
  630. return err
  631. }
  632. ss.cmd.Stdin = rdStdin
  633. ss.cmd.Stdout = wrStdout
  634. ss.cmd.Stderr = wrStderr
  635. ss.childPipes = []io.Closer{rdStdin, wrStdout, wrStderr}
  636. return ss.cmd.Start()
  637. }
  638. func envForUser(u *userMeta) []string {
  639. return []string{
  640. fmt.Sprintf("SHELL=" + u.LoginShell()),
  641. fmt.Sprintf("USER=" + u.Username),
  642. fmt.Sprintf("HOME=" + u.HomeDir),
  643. fmt.Sprintf("PATH=" + defaultPathForUser(&u.User)),
  644. }
  645. }
  646. // updateStringInSlice mutates ss to change the first occurrence of a
  647. // to b.
  648. func updateStringInSlice(ss []string, a, b string) {
  649. for i, s := range ss {
  650. if s == a {
  651. ss[i] = b
  652. return
  653. }
  654. }
  655. }
  656. // acceptEnvPair reports whether the environment variable key=value pair
  657. // should be accepted from the client. It uses the same default as OpenSSH
  658. // AcceptEnv.
  659. func acceptEnvPair(kv string) bool {
  660. k, _, ok := strings.Cut(kv, "=")
  661. if !ok {
  662. return false
  663. }
  664. return k == "TERM" || k == "LANG" || strings.HasPrefix(k, "LC_")
  665. }
  666. func fileExists(path string) bool {
  667. _, err := os.Stat(path)
  668. return err == nil
  669. }
  670. // loginArgs returns the arguments to use to exec the login binary.
  671. // It returns nil if the login binary should not be used.
  672. // The login binary is only used:
  673. // - on darwin, if the client is requesting a shell or a command.
  674. // - on linux and BSD, if the client is requesting a shell with a TTY.
  675. func (ia *incubatorArgs) loginArgs() []string {
  676. if ia.isSFTP {
  677. return nil
  678. }
  679. switch runtime.GOOS {
  680. case "darwin":
  681. args := []string{
  682. ia.loginCmdPath,
  683. "-f", // already authenticated
  684. // login typically discards the previous environment, but we want to
  685. // preserve any environment variables that we currently have.
  686. "-p",
  687. "-h", ia.remoteIP, // -h is "remote host"
  688. ia.localUser,
  689. }
  690. if !ia.hasTTY {
  691. args[2] = "-pq" // -q is "quiet" which suppresses the login banner
  692. }
  693. if ia.cmdName != "" {
  694. args = append(args, ia.cmdName)
  695. args = append(args, ia.cmdArgs...)
  696. }
  697. return args
  698. case "linux":
  699. if !ia.isShell || !ia.hasTTY {
  700. // We can only use login command if a shell was requested with a TTY. If
  701. // there is no TTY, login exits immediately, which breaks things likes
  702. // mosh and VSCode.
  703. return nil
  704. }
  705. if distro.Get() == distro.Arch && !fileExists("/etc/pam.d/remote") {
  706. // See https://github.com/tailscale/tailscale/issues/4924
  707. //
  708. // Arch uses a different login binary that makes the -h flag set the PAM
  709. // service to "remote". So if they don't have that configured, don't
  710. // pass -h.
  711. return []string{ia.loginCmdPath, "-f", ia.localUser, "-p"}
  712. }
  713. return []string{ia.loginCmdPath, "-f", ia.localUser, "-h", ia.remoteIP, "-p"}
  714. case "freebsd", "openbsd":
  715. if !ia.isShell || !ia.hasTTY {
  716. // We can only use login command if a shell was requested with a TTY. If
  717. // there is no TTY, login exits immediately, which breaks things likes
  718. // mosh and VSCode.
  719. return nil
  720. }
  721. return []string{ia.loginCmdPath, "-fp", "-h", ia.remoteIP, ia.localUser}
  722. }
  723. panic("unimplemented")
  724. }
  725. func setGroups(groupIDs []int) error {
  726. if runtime.GOOS == "darwin" && len(groupIDs) > 16 {
  727. // darwin returns "invalid argument" if more than 16 groups are passed to syscall.Setgroups
  728. // some info can be found here:
  729. // https://opensource.apple.com/source/samba/samba-187.8/patches/support-darwin-initgroups-syscall.auto.html
  730. // this fix isn't great, as anyone reading this has probably just wasted hours figuring out why
  731. // some permissions thing isn't working, due to some arbitrary group ordering, but it at least allows
  732. // this to work for more things than it previously did.
  733. groupIDs = groupIDs[:16]
  734. }
  735. err := syscall.Setgroups(groupIDs)
  736. if err != nil && os.Geteuid() != 0 && groupsMatchCurrent(groupIDs) {
  737. // If we're not root, ignore a Setgroups failure if all groups are the same.
  738. return nil
  739. }
  740. return err
  741. }
  742. func groupsMatchCurrent(groupIDs []int) bool {
  743. existing, err := syscall.Getgroups()
  744. if err != nil {
  745. return false
  746. }
  747. if len(existing) != len(groupIDs) {
  748. return false
  749. }
  750. groupIDs = slices.Clone(groupIDs)
  751. sort.Ints(groupIDs)
  752. sort.Ints(existing)
  753. return slices.Equal(groupIDs, existing)
  754. }