tailssh_integration_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build integrationtest
  4. // +build integrationtest
  5. package tailssh
  6. import (
  7. "bufio"
  8. "bytes"
  9. "context"
  10. "crypto/rand"
  11. "crypto/rsa"
  12. "crypto/x509"
  13. "encoding/pem"
  14. "errors"
  15. "fmt"
  16. "io"
  17. "log"
  18. "net"
  19. "net/http"
  20. "net/netip"
  21. "os"
  22. "os/exec"
  23. "path/filepath"
  24. "runtime"
  25. "strings"
  26. "testing"
  27. "time"
  28. "github.com/bramvdbogaerde/go-scp"
  29. "github.com/google/go-cmp/cmp"
  30. "github.com/pkg/sftp"
  31. gossh "github.com/tailscale/golang-x-crypto/ssh"
  32. "golang.org/x/crypto/ssh"
  33. "golang.org/x/crypto/ssh/agent"
  34. "tailscale.com/net/tsdial"
  35. "tailscale.com/tailcfg"
  36. glider "tailscale.com/tempfork/gliderlabs/ssh"
  37. "tailscale.com/types/key"
  38. "tailscale.com/types/netmap"
  39. "tailscale.com/util/set"
  40. )
  41. // This file contains integration tests of the SSH functionality. These tests
  42. // exercise everything except for the authentication logic.
  43. //
  44. // The tests make the following assumptions about the environment:
  45. //
  46. // - OS is one of MacOS or Linux
  47. // - Test is being run as root (e.g. go test -tags integrationtest -c . && sudo ./tailssh.test -test.run TestIntegration)
  48. // - TAILSCALED_PATH environment variable points at tailscaled binary
  49. // - User "testuser" exists
  50. // - "testuser" is in groups "groupone" and "grouptwo"
  51. func TestMain(m *testing.M) {
  52. // Create our log file.
  53. file, err := os.OpenFile("/tmp/tailscalessh.log", os.O_CREATE|os.O_WRONLY, 0666)
  54. if err != nil {
  55. log.Fatal(err)
  56. }
  57. file.Close()
  58. // Tail our log file.
  59. cmd := exec.Command("tail", "-F", "/tmp/tailscalessh.log")
  60. r, err := cmd.StdoutPipe()
  61. if err != nil {
  62. return
  63. }
  64. scanner := bufio.NewScanner(r)
  65. go func() {
  66. for scanner.Scan() {
  67. line := scanner.Text()
  68. log.Println(line)
  69. }
  70. }()
  71. err = cmd.Start()
  72. if err != nil {
  73. return
  74. }
  75. defer func() {
  76. // tail -f has a default sleep interval of 1 second, so it takes a
  77. // moment for it to finish reading our log file after we've terminated.
  78. // So, wait a bit to let it catch up.
  79. time.Sleep(2 * time.Second)
  80. }()
  81. m.Run()
  82. }
  83. func TestIntegrationSSH(t *testing.T) {
  84. debugTest.Store(true)
  85. t.Cleanup(func() {
  86. debugTest.Store(false)
  87. })
  88. homeDir := "/home/testuser"
  89. if runtime.GOOS == "darwin" {
  90. homeDir = "/Users/testuser"
  91. }
  92. tests := []struct {
  93. cmd string
  94. want []string
  95. forceV1Behavior bool
  96. skip bool
  97. allowSendEnv bool
  98. }{
  99. {
  100. cmd: "id",
  101. want: []string{"testuser", "groupone", "grouptwo"},
  102. forceV1Behavior: false,
  103. },
  104. {
  105. cmd: "id",
  106. want: []string{"testuser", "groupone", "grouptwo"},
  107. forceV1Behavior: true,
  108. },
  109. {
  110. cmd: "pwd",
  111. want: []string{homeDir},
  112. skip: os.Getenv("SKIP_FILE_OPS") == "1" || !fallbackToSUAvailable(),
  113. forceV1Behavior: false,
  114. },
  115. {
  116. cmd: "echo 'hello'",
  117. want: []string{"hello"},
  118. skip: os.Getenv("SKIP_FILE_OPS") == "1" || !fallbackToSUAvailable(),
  119. forceV1Behavior: false,
  120. },
  121. {
  122. cmd: `echo "${GIT_ENV_VAR:-unset1} ${EXACT_MATCH:-unset2} ${TESTING:-unset3} ${NOT_ALLOWED:-unset4}"`,
  123. want: []string{"working1 working2 working3 unset4"},
  124. forceV1Behavior: false,
  125. allowSendEnv: true,
  126. },
  127. {
  128. cmd: `echo "${GIT_ENV_VAR:-unset1} ${EXACT_MATCH:-unset2} ${TESTING:-unset3} ${NOT_ALLOWED:-unset4}"`,
  129. want: []string{"unset1 unset2 unset3 unset4"},
  130. forceV1Behavior: false,
  131. allowSendEnv: false,
  132. },
  133. }
  134. for _, test := range tests {
  135. if test.skip {
  136. continue
  137. }
  138. // run every test both without and with a shell
  139. for _, shell := range []bool{false, true} {
  140. shellQualifier := "no_shell"
  141. if shell {
  142. shellQualifier = "shell"
  143. }
  144. versionQualifier := "v2"
  145. if test.forceV1Behavior {
  146. versionQualifier = "v1"
  147. }
  148. t.Run(fmt.Sprintf("%s_%s_%s", test.cmd, shellQualifier, versionQualifier), func(t *testing.T) {
  149. sendEnv := map[string]string{
  150. "GIT_ENV_VAR": "working1",
  151. "EXACT_MATCH": "working2",
  152. "TESTING": "working3",
  153. "NOT_ALLOWED": "working4",
  154. }
  155. s := testSession(t, test.forceV1Behavior, test.allowSendEnv, sendEnv)
  156. if shell {
  157. err := s.RequestPty("xterm", 40, 80, ssh.TerminalModes{
  158. ssh.ECHO: 1,
  159. ssh.TTY_OP_ISPEED: 14400,
  160. ssh.TTY_OP_OSPEED: 14400,
  161. })
  162. if err != nil {
  163. t.Fatalf("unable to request PTY: %s", err)
  164. }
  165. err = s.Shell()
  166. if err != nil {
  167. t.Fatalf("unable to request shell: %s", err)
  168. }
  169. // Read the shell prompt
  170. s.read()
  171. }
  172. got := s.run(t, test.cmd, shell)
  173. for _, want := range test.want {
  174. if !strings.Contains(got, want) {
  175. t.Errorf("%q does not contain %q", got, want)
  176. }
  177. }
  178. })
  179. }
  180. }
  181. }
  182. func TestIntegrationSFTP(t *testing.T) {
  183. debugTest.Store(true)
  184. t.Cleanup(func() {
  185. debugTest.Store(false)
  186. })
  187. for _, forceV1Behavior := range []bool{false, true} {
  188. name := "v2"
  189. if forceV1Behavior {
  190. name = "v1"
  191. }
  192. t.Run(name, func(t *testing.T) {
  193. filePath := "/home/testuser/sftptest.dat"
  194. if forceV1Behavior || !fallbackToSUAvailable() {
  195. filePath = "/tmp/sftptest.dat"
  196. }
  197. wantText := "hello world"
  198. cl := testClient(t, forceV1Behavior, false)
  199. scl, err := sftp.NewClient(cl)
  200. if err != nil {
  201. t.Fatalf("can't get sftp client: %s", err)
  202. }
  203. file, err := scl.Create(filePath)
  204. if err != nil {
  205. t.Fatalf("can't create file: %s", err)
  206. }
  207. _, err = file.Write([]byte(wantText))
  208. if err != nil {
  209. t.Fatalf("can't write to file: %s", err)
  210. }
  211. err = file.Close()
  212. if err != nil {
  213. t.Fatalf("can't close file: %s", err)
  214. }
  215. file, err = scl.OpenFile(filePath, os.O_RDONLY)
  216. if err != nil {
  217. t.Fatalf("can't open file: %s", err)
  218. }
  219. defer file.Close()
  220. gotText, err := io.ReadAll(file)
  221. if err != nil {
  222. t.Fatalf("can't read file: %s", err)
  223. }
  224. if diff := cmp.Diff(string(gotText), wantText); diff != "" {
  225. t.Fatalf("unexpected file contents (-got +want):\n%s", diff)
  226. }
  227. s := testSessionFor(t, cl, nil)
  228. got := s.run(t, "ls -l "+filePath, false)
  229. if !strings.Contains(got, "testuser") {
  230. t.Fatalf("unexpected file owner user: %s", got)
  231. } else if !strings.Contains(got, "testuser") {
  232. t.Fatalf("unexpected file owner group: %s", got)
  233. }
  234. })
  235. }
  236. }
  237. func TestIntegrationSCP(t *testing.T) {
  238. debugTest.Store(true)
  239. t.Cleanup(func() {
  240. debugTest.Store(false)
  241. })
  242. for _, forceV1Behavior := range []bool{false, true} {
  243. name := "v2"
  244. if forceV1Behavior {
  245. name = "v1"
  246. }
  247. t.Run(name, func(t *testing.T) {
  248. filePath := "/home/testuser/scptest.dat"
  249. if !fallbackToSUAvailable() {
  250. filePath = "/tmp/scptest.dat"
  251. }
  252. wantText := "hello world"
  253. cl := testClient(t, forceV1Behavior, false)
  254. scl, err := scp.NewClientBySSH(cl)
  255. if err != nil {
  256. t.Fatalf("can't get sftp client: %s", err)
  257. }
  258. err = scl.Copy(context.Background(), strings.NewReader(wantText), filePath, "0644", int64(len(wantText)))
  259. if err != nil {
  260. t.Fatalf("can't create file: %s", err)
  261. }
  262. outfile, err := os.CreateTemp("", "")
  263. if err != nil {
  264. t.Fatalf("can't create temp file: %s", err)
  265. }
  266. err = scl.CopyFromRemote(context.Background(), outfile, filePath)
  267. if err != nil {
  268. t.Fatalf("can't copy file from remote: %s", err)
  269. }
  270. outfile.Close()
  271. gotText, err := os.ReadFile(outfile.Name())
  272. if err != nil {
  273. t.Fatalf("can't read file: %s", err)
  274. }
  275. if diff := cmp.Diff(string(gotText), wantText); diff != "" {
  276. t.Fatalf("unexpected file contents (-got +want):\n%s", diff)
  277. }
  278. s := testSessionFor(t, cl, nil)
  279. got := s.run(t, "ls -l "+filePath, false)
  280. if !strings.Contains(got, "testuser") {
  281. t.Fatalf("unexpected file owner user: %s", got)
  282. } else if !strings.Contains(got, "testuser") {
  283. t.Fatalf("unexpected file owner group: %s", got)
  284. }
  285. })
  286. }
  287. }
  288. func TestSSHAgentForwarding(t *testing.T) {
  289. debugTest.Store(true)
  290. t.Cleanup(func() {
  291. debugTest.Store(false)
  292. })
  293. // Create a client SSH key
  294. tmpDir, err := os.MkdirTemp("", "")
  295. if err != nil {
  296. t.Fatal(err)
  297. }
  298. t.Cleanup(func() {
  299. _ = os.RemoveAll(tmpDir)
  300. })
  301. pkFile := filepath.Join(tmpDir, "pk")
  302. clientKey, clientKeyRSA := generateClientKey(t, pkFile)
  303. // Start upstream SSH server
  304. l, err := net.Listen("tcp", "127.0.0.1:")
  305. if err != nil {
  306. t.Fatalf("unable to listen for SSH: %s", err)
  307. }
  308. t.Cleanup(func() {
  309. _ = l.Close()
  310. })
  311. // Run an SSH server that accepts connections from that client SSH key.
  312. gs := glider.Server{
  313. Handler: func(s glider.Session) {
  314. io.WriteString(s, "Hello world\n")
  315. },
  316. PublicKeyHandler: func(ctx glider.Context, key glider.PublicKey) error {
  317. // Note - this is not meant to be cryptographically secure, it's
  318. // just checking that SSH agent forwarding is forwarding the right
  319. // key.
  320. a := key.Marshal()
  321. b := clientKey.PublicKey().Marshal()
  322. if !bytes.Equal(a, b) {
  323. return errors.New("key mismatch")
  324. }
  325. return nil
  326. },
  327. }
  328. go gs.Serve(l)
  329. // Run tailscale SSH server and connect to it
  330. username := "testuser"
  331. tailscaleAddr := testServer(t, username, false, false)
  332. tcl, err := ssh.Dial("tcp", tailscaleAddr, &ssh.ClientConfig{
  333. HostKeyCallback: ssh.InsecureIgnoreHostKey(),
  334. })
  335. if err != nil {
  336. t.Fatal(err)
  337. }
  338. t.Cleanup(func() { tcl.Close() })
  339. s, err := tcl.NewSession()
  340. if err != nil {
  341. t.Fatal(err)
  342. }
  343. // Set up SSH agent forwarding on the client
  344. err = agent.RequestAgentForwarding(s)
  345. if err != nil {
  346. t.Fatal(err)
  347. }
  348. keyring := agent.NewKeyring()
  349. keyring.Add(agent.AddedKey{
  350. PrivateKey: clientKeyRSA,
  351. })
  352. err = agent.ForwardToAgent(tcl, keyring)
  353. if err != nil {
  354. t.Fatal(err)
  355. }
  356. // Attempt to SSH to the upstream test server using the forwarded SSH key
  357. // and run the "true" command.
  358. upstreamHost, upstreamPort, err := net.SplitHostPort(l.Addr().String())
  359. if err != nil {
  360. t.Fatal(err)
  361. }
  362. o, err := s.CombinedOutput(fmt.Sprintf(`ssh -T -o StrictHostKeyChecking=no -p %s upstreamuser@%s "true"`, upstreamPort, upstreamHost))
  363. if err != nil {
  364. t.Fatalf("unable to call true command: %s\n%s\n-------------------------", err, o)
  365. }
  366. }
  367. func fallbackToSUAvailable() bool {
  368. if runtime.GOOS != "linux" {
  369. return false
  370. }
  371. _, err := exec.LookPath("su")
  372. if err != nil {
  373. return false
  374. }
  375. // Some operating systems like Fedora seem to require login to be present
  376. // in order for su to work.
  377. _, err = exec.LookPath("login")
  378. return err == nil
  379. }
  380. type session struct {
  381. *ssh.Session
  382. stdin io.WriteCloser
  383. stdout io.ReadCloser
  384. stderr io.ReadCloser
  385. }
  386. func (s *session) run(t *testing.T, cmdString string, shell bool) string {
  387. t.Helper()
  388. if shell {
  389. _, err := s.stdin.Write([]byte(fmt.Sprintf("%s\n", cmdString)))
  390. if err != nil {
  391. t.Fatalf("unable to send command to shell: %s", err)
  392. }
  393. } else {
  394. err := s.Start(cmdString)
  395. if err != nil {
  396. t.Fatalf("unable to start command: %s", err)
  397. }
  398. }
  399. return s.read()
  400. }
  401. func (s *session) read() string {
  402. ch := make(chan []byte)
  403. go func() {
  404. for {
  405. b := make([]byte, 1)
  406. n, err := s.stdout.Read(b)
  407. if n > 0 {
  408. ch <- b
  409. }
  410. if err == io.EOF {
  411. return
  412. }
  413. }
  414. }()
  415. // Read first byte in blocking fashion.
  416. _got := <-ch
  417. // Read subsequent bytes in non-blocking fashion.
  418. readLoop:
  419. for {
  420. select {
  421. case b := <-ch:
  422. _got = append(_got, b...)
  423. case <-time.After(1 * time.Second):
  424. break readLoop
  425. }
  426. }
  427. return string(_got)
  428. }
  429. func testClient(t *testing.T, forceV1Behavior bool, allowSendEnv bool, authMethods ...ssh.AuthMethod) *ssh.Client {
  430. t.Helper()
  431. username := "testuser"
  432. addr := testServer(t, username, forceV1Behavior, allowSendEnv)
  433. cl, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{
  434. HostKeyCallback: ssh.InsecureIgnoreHostKey(),
  435. Auth: authMethods,
  436. })
  437. if err != nil {
  438. t.Fatal(err)
  439. }
  440. t.Cleanup(func() { cl.Close() })
  441. return cl
  442. }
  443. func testServer(t *testing.T, username string, forceV1Behavior bool, allowSendEnv bool) string {
  444. srv := &server{
  445. lb: &testBackend{localUser: username, forceV1Behavior: forceV1Behavior, allowSendEnv: allowSendEnv},
  446. logf: log.Printf,
  447. tailscaledPath: os.Getenv("TAILSCALED_PATH"),
  448. timeNow: time.Now,
  449. }
  450. l, err := net.Listen("tcp", "127.0.0.1:0")
  451. if err != nil {
  452. t.Fatal(err)
  453. }
  454. t.Cleanup(func() { l.Close() })
  455. go func() {
  456. for {
  457. conn, err := l.Accept()
  458. if err == nil {
  459. go srv.HandleSSHConn(&addressFakingConn{conn})
  460. }
  461. }
  462. }()
  463. return l.Addr().String()
  464. }
  465. func testSession(t *testing.T, forceV1Behavior bool, allowSendEnv bool, sendEnv map[string]string) *session {
  466. cl := testClient(t, forceV1Behavior, allowSendEnv)
  467. return testSessionFor(t, cl, sendEnv)
  468. }
  469. func testSessionFor(t *testing.T, cl *ssh.Client, sendEnv map[string]string) *session {
  470. s, err := cl.NewSession()
  471. if err != nil {
  472. t.Fatal(err)
  473. }
  474. for k, v := range sendEnv {
  475. s.Setenv(k, v)
  476. }
  477. t.Cleanup(func() { s.Close() })
  478. stdinReader, stdinWriter := io.Pipe()
  479. stdoutReader, stdoutWriter := io.Pipe()
  480. stderrReader, stderrWriter := io.Pipe()
  481. s.Stdin = stdinReader
  482. s.Stdout = io.MultiWriter(stdoutWriter, os.Stdout)
  483. s.Stderr = io.MultiWriter(stderrWriter, os.Stderr)
  484. return &session{
  485. Session: s,
  486. stdin: stdinWriter,
  487. stdout: stdoutReader,
  488. stderr: stderrReader,
  489. }
  490. }
  491. func generateClientKey(t *testing.T, privateKeyFile string) (ssh.Signer, *rsa.PrivateKey) {
  492. t.Helper()
  493. priv, err := rsa.GenerateKey(rand.Reader, 2048)
  494. if err != nil {
  495. t.Fatal(err)
  496. }
  497. mk, err := x509.MarshalPKCS8PrivateKey(priv)
  498. if err != nil {
  499. t.Fatal(err)
  500. }
  501. privateKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
  502. if privateKey == nil {
  503. t.Fatal("failed to encoded private key")
  504. }
  505. err = os.WriteFile(privateKeyFile, privateKey, 0600)
  506. if err != nil {
  507. t.Fatal(err)
  508. }
  509. signer, err := ssh.ParsePrivateKey(privateKey)
  510. if err != nil {
  511. t.Fatal(err)
  512. }
  513. return signer, priv
  514. }
  515. // testBackend implements ipnLocalBackend
  516. type testBackend struct {
  517. localUser string
  518. forceV1Behavior bool
  519. allowSendEnv bool
  520. }
  521. func (tb *testBackend) GetSSH_HostKeys() ([]gossh.Signer, error) {
  522. var result []gossh.Signer
  523. var priv any
  524. var err error
  525. const keySize = 2048
  526. priv, err = rsa.GenerateKey(rand.Reader, keySize)
  527. if err != nil {
  528. return nil, err
  529. }
  530. mk, err := x509.MarshalPKCS8PrivateKey(priv)
  531. if err != nil {
  532. return nil, err
  533. }
  534. hostKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
  535. signer, err := gossh.ParsePrivateKey(hostKey)
  536. if err != nil {
  537. return nil, err
  538. }
  539. result = append(result, signer)
  540. return result, nil
  541. }
  542. func (tb *testBackend) ShouldRunSSH() bool {
  543. return true
  544. }
  545. func (tb *testBackend) NetMap() *netmap.NetworkMap {
  546. capMap := make(set.Set[tailcfg.NodeCapability])
  547. if tb.forceV1Behavior {
  548. capMap[tailcfg.NodeAttrSSHBehaviorV1] = struct{}{}
  549. }
  550. if tb.allowSendEnv {
  551. capMap[tailcfg.NodeAttrSSHEnvironmentVariables] = struct{}{}
  552. }
  553. return &netmap.NetworkMap{
  554. SSHPolicy: &tailcfg.SSHPolicy{
  555. Rules: []*tailcfg.SSHRule{
  556. {
  557. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  558. Action: &tailcfg.SSHAction{Accept: true, AllowAgentForwarding: true},
  559. SSHUsers: map[string]string{"*": tb.localUser},
  560. AcceptEnv: []string{"GIT_*", "EXACT_MATCH", "TEST?NG"},
  561. },
  562. },
  563. },
  564. AllCaps: capMap,
  565. }
  566. }
  567. func (tb *testBackend) WhoIs(_ string, ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
  568. return (&tailcfg.Node{}).View(), tailcfg.UserProfile{
  569. LoginName: tb.localUser + "@example.com",
  570. }, true
  571. }
  572. func (tb *testBackend) DoNoiseRequest(req *http.Request) (*http.Response, error) {
  573. return nil, nil
  574. }
  575. func (tb *testBackend) Dialer() *tsdial.Dialer {
  576. return nil
  577. }
  578. func (tb *testBackend) TailscaleVarRoot() string {
  579. return ""
  580. }
  581. func (tb *testBackend) NodeKey() key.NodePublic {
  582. return key.NodePublic{}
  583. }
  584. type addressFakingConn struct {
  585. net.Conn
  586. }
  587. func (conn *addressFakingConn) LocalAddr() net.Addr {
  588. return &net.TCPAddr{
  589. IP: net.ParseIP("100.100.100.101"),
  590. Port: 22,
  591. }
  592. }
  593. func (conn *addressFakingConn) RemoteAddr() net.Addr {
  594. return &net.TCPAddr{
  595. IP: net.ParseIP("100.100.100.102"),
  596. Port: 10002,
  597. }
  598. }