tailssh_integration_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652
  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. }{
  98. {
  99. cmd: "id",
  100. want: []string{"testuser", "groupone", "grouptwo"},
  101. forceV1Behavior: false,
  102. },
  103. {
  104. cmd: "id",
  105. want: []string{"testuser", "groupone", "grouptwo"},
  106. forceV1Behavior: true,
  107. },
  108. {
  109. cmd: "pwd",
  110. want: []string{homeDir},
  111. skip: !fallbackToSUAvailable(),
  112. forceV1Behavior: false,
  113. },
  114. {
  115. cmd: "echo 'hello'",
  116. want: []string{"hello"},
  117. skip: !fallbackToSUAvailable(),
  118. forceV1Behavior: false,
  119. },
  120. }
  121. for _, test := range tests {
  122. if test.skip {
  123. continue
  124. }
  125. // run every test both without and with a shell
  126. for _, shell := range []bool{false, true} {
  127. shellQualifier := "no_shell"
  128. if shell {
  129. shellQualifier = "shell"
  130. }
  131. versionQualifier := "v2"
  132. if test.forceV1Behavior {
  133. versionQualifier = "v1"
  134. }
  135. t.Run(fmt.Sprintf("%s_%s_%s", test.cmd, shellQualifier, versionQualifier), func(t *testing.T) {
  136. s := testSession(t, test.forceV1Behavior)
  137. if shell {
  138. err := s.RequestPty("xterm", 40, 80, ssh.TerminalModes{
  139. ssh.ECHO: 1,
  140. ssh.TTY_OP_ISPEED: 14400,
  141. ssh.TTY_OP_OSPEED: 14400,
  142. })
  143. if err != nil {
  144. t.Fatalf("unable to request PTY: %s", err)
  145. }
  146. err = s.Shell()
  147. if err != nil {
  148. t.Fatalf("unable to request shell: %s", err)
  149. }
  150. // Read the shell prompt
  151. s.read()
  152. }
  153. got := s.run(t, test.cmd, shell)
  154. for _, want := range test.want {
  155. if !strings.Contains(got, want) {
  156. t.Errorf("%q does not contain %q", got, want)
  157. }
  158. }
  159. })
  160. }
  161. }
  162. }
  163. func TestIntegrationSFTP(t *testing.T) {
  164. debugTest.Store(true)
  165. t.Cleanup(func() {
  166. debugTest.Store(false)
  167. })
  168. for _, forceV1Behavior := range []bool{false, true} {
  169. name := "v2"
  170. if forceV1Behavior {
  171. name = "v1"
  172. }
  173. t.Run(name, func(t *testing.T) {
  174. filePath := "/home/testuser/sftptest.dat"
  175. if forceV1Behavior || !fallbackToSUAvailable() {
  176. filePath = "/tmp/sftptest.dat"
  177. }
  178. wantText := "hello world"
  179. cl := testClient(t, forceV1Behavior)
  180. scl, err := sftp.NewClient(cl)
  181. if err != nil {
  182. t.Fatalf("can't get sftp client: %s", err)
  183. }
  184. file, err := scl.Create(filePath)
  185. if err != nil {
  186. t.Fatalf("can't create file: %s", err)
  187. }
  188. _, err = file.Write([]byte(wantText))
  189. if err != nil {
  190. t.Fatalf("can't write to file: %s", err)
  191. }
  192. err = file.Close()
  193. if err != nil {
  194. t.Fatalf("can't close file: %s", err)
  195. }
  196. file, err = scl.OpenFile(filePath, os.O_RDONLY)
  197. if err != nil {
  198. t.Fatalf("can't open file: %s", err)
  199. }
  200. defer file.Close()
  201. gotText, err := io.ReadAll(file)
  202. if err != nil {
  203. t.Fatalf("can't read file: %s", err)
  204. }
  205. if diff := cmp.Diff(string(gotText), wantText); diff != "" {
  206. t.Fatalf("unexpected file contents (-got +want):\n%s", diff)
  207. }
  208. s := testSessionFor(t, cl)
  209. got := s.run(t, "ls -l "+filePath, false)
  210. if !strings.Contains(got, "testuser") {
  211. t.Fatalf("unexpected file owner user: %s", got)
  212. } else if !strings.Contains(got, "testuser") {
  213. t.Fatalf("unexpected file owner group: %s", got)
  214. }
  215. })
  216. }
  217. }
  218. func TestIntegrationSCP(t *testing.T) {
  219. debugTest.Store(true)
  220. t.Cleanup(func() {
  221. debugTest.Store(false)
  222. })
  223. for _, forceV1Behavior := range []bool{false, true} {
  224. name := "v2"
  225. if forceV1Behavior {
  226. name = "v1"
  227. }
  228. t.Run(name, func(t *testing.T) {
  229. filePath := "/home/testuser/scptest.dat"
  230. if !fallbackToSUAvailable() {
  231. filePath = "/tmp/scptest.dat"
  232. }
  233. wantText := "hello world"
  234. cl := testClient(t, forceV1Behavior)
  235. scl, err := scp.NewClientBySSH(cl)
  236. if err != nil {
  237. t.Fatalf("can't get sftp client: %s", err)
  238. }
  239. err = scl.Copy(context.Background(), strings.NewReader(wantText), filePath, "0644", int64(len(wantText)))
  240. if err != nil {
  241. t.Fatalf("can't create file: %s", err)
  242. }
  243. outfile, err := os.CreateTemp("", "")
  244. if err != nil {
  245. t.Fatalf("can't create temp file: %s", err)
  246. }
  247. err = scl.CopyFromRemote(context.Background(), outfile, filePath)
  248. if err != nil {
  249. t.Fatalf("can't copy file from remote: %s", err)
  250. }
  251. outfile.Close()
  252. gotText, err := os.ReadFile(outfile.Name())
  253. if err != nil {
  254. t.Fatalf("can't read file: %s", err)
  255. }
  256. if diff := cmp.Diff(string(gotText), wantText); diff != "" {
  257. t.Fatalf("unexpected file contents (-got +want):\n%s", diff)
  258. }
  259. s := testSessionFor(t, cl)
  260. got := s.run(t, "ls -l "+filePath, false)
  261. if !strings.Contains(got, "testuser") {
  262. t.Fatalf("unexpected file owner user: %s", got)
  263. } else if !strings.Contains(got, "testuser") {
  264. t.Fatalf("unexpected file owner group: %s", got)
  265. }
  266. })
  267. }
  268. }
  269. func TestSSHAgentForwarding(t *testing.T) {
  270. debugTest.Store(true)
  271. t.Cleanup(func() {
  272. debugTest.Store(false)
  273. })
  274. // Create a client SSH key
  275. tmpDir, err := os.MkdirTemp("", "")
  276. if err != nil {
  277. t.Fatal(err)
  278. }
  279. t.Cleanup(func() {
  280. _ = os.RemoveAll(tmpDir)
  281. })
  282. pkFile := filepath.Join(tmpDir, "pk")
  283. clientKey, clientKeyRSA := generateClientKey(t, pkFile)
  284. // Start upstream SSH server
  285. l, err := net.Listen("tcp", "127.0.0.1:")
  286. if err != nil {
  287. t.Fatalf("unable to listen for SSH: %s", err)
  288. }
  289. t.Cleanup(func() {
  290. _ = l.Close()
  291. })
  292. // Run an SSH server that accepts connections from that client SSH key.
  293. gs := glider.Server{
  294. Handler: func(s glider.Session) {
  295. io.WriteString(s, "Hello world\n")
  296. },
  297. PublicKeyHandler: func(ctx glider.Context, key glider.PublicKey) error {
  298. // Note - this is not meant to be cryptographically secure, it's
  299. // just checking that SSH agent forwarding is forwarding the right
  300. // key.
  301. a := key.Marshal()
  302. b := clientKey.PublicKey().Marshal()
  303. if !bytes.Equal(a, b) {
  304. return errors.New("key mismatch")
  305. }
  306. return nil
  307. },
  308. }
  309. go gs.Serve(l)
  310. // Run tailscale SSH server and connect to it
  311. username := "testuser"
  312. tailscaleAddr := testServer(t, username, false) // TODO: make this false to use V2 behavior
  313. tcl, err := ssh.Dial("tcp", tailscaleAddr, &ssh.ClientConfig{
  314. HostKeyCallback: ssh.InsecureIgnoreHostKey(),
  315. })
  316. if err != nil {
  317. t.Fatal(err)
  318. }
  319. t.Cleanup(func() { tcl.Close() })
  320. s, err := tcl.NewSession()
  321. if err != nil {
  322. t.Fatal(err)
  323. }
  324. // Set up SSH agent forwarding on the client
  325. err = agent.RequestAgentForwarding(s)
  326. if err != nil {
  327. t.Fatal(err)
  328. }
  329. keyring := agent.NewKeyring()
  330. keyring.Add(agent.AddedKey{
  331. PrivateKey: clientKeyRSA,
  332. })
  333. err = agent.ForwardToAgent(tcl, keyring)
  334. if err != nil {
  335. t.Fatal(err)
  336. }
  337. // Attempt to SSH to the upstream test server using the forwarded SSH key
  338. // and run the "true" command.
  339. upstreamHost, upstreamPort, err := net.SplitHostPort(l.Addr().String())
  340. if err != nil {
  341. t.Fatal(err)
  342. }
  343. o, err := s.CombinedOutput(fmt.Sprintf(`ssh -T -o StrictHostKeyChecking=no -p %s upstreamuser@%s "true"`, upstreamPort, upstreamHost))
  344. if err != nil {
  345. t.Fatalf("unable to call true command: %s\n%s", err, o)
  346. }
  347. }
  348. func fallbackToSUAvailable() bool {
  349. if runtime.GOOS != "linux" {
  350. return false
  351. }
  352. _, err := exec.LookPath("su")
  353. if err != nil {
  354. return false
  355. }
  356. // Some operating systems like Fedora seem to require login to be present
  357. // in order for su to work.
  358. _, err = exec.LookPath("login")
  359. return err == nil
  360. }
  361. type session struct {
  362. *ssh.Session
  363. stdin io.WriteCloser
  364. stdout io.ReadCloser
  365. stderr io.ReadCloser
  366. }
  367. func (s *session) run(t *testing.T, cmdString string, shell bool) string {
  368. t.Helper()
  369. if shell {
  370. _, err := s.stdin.Write([]byte(fmt.Sprintf("%s\n", cmdString)))
  371. if err != nil {
  372. t.Fatalf("unable to send command to shell: %s", err)
  373. }
  374. } else {
  375. err := s.Start(cmdString)
  376. if err != nil {
  377. t.Fatalf("unable to start command: %s", err)
  378. }
  379. }
  380. return s.read()
  381. }
  382. func (s *session) read() string {
  383. ch := make(chan []byte)
  384. go func() {
  385. for {
  386. b := make([]byte, 1)
  387. n, err := s.stdout.Read(b)
  388. if n > 0 {
  389. ch <- b
  390. }
  391. if err == io.EOF {
  392. return
  393. }
  394. }
  395. }()
  396. // Read first byte in blocking fashion.
  397. _got := <-ch
  398. // Read subsequent bytes in non-blocking fashion.
  399. readLoop:
  400. for {
  401. select {
  402. case b := <-ch:
  403. _got = append(_got, b...)
  404. case <-time.After(1 * time.Second):
  405. break readLoop
  406. }
  407. }
  408. return string(_got)
  409. }
  410. func testClient(t *testing.T, forceV1Behavior bool, authMethods ...ssh.AuthMethod) *ssh.Client {
  411. t.Helper()
  412. username := "testuser"
  413. addr := testServer(t, username, forceV1Behavior)
  414. cl, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{
  415. HostKeyCallback: ssh.InsecureIgnoreHostKey(),
  416. Auth: authMethods,
  417. })
  418. if err != nil {
  419. t.Fatal(err)
  420. }
  421. t.Cleanup(func() { cl.Close() })
  422. return cl
  423. }
  424. func testServer(t *testing.T, username string, forceV1Behavior bool) string {
  425. srv := &server{
  426. lb: &testBackend{localUser: username, forceV1Behavior: forceV1Behavior},
  427. logf: log.Printf,
  428. tailscaledPath: os.Getenv("TAILSCALED_PATH"),
  429. timeNow: time.Now,
  430. }
  431. l, err := net.Listen("tcp", "127.0.0.1:0")
  432. if err != nil {
  433. t.Fatal(err)
  434. }
  435. t.Cleanup(func() { l.Close() })
  436. go func() {
  437. for {
  438. conn, err := l.Accept()
  439. if err == nil {
  440. go srv.HandleSSHConn(&addressFakingConn{conn})
  441. }
  442. }
  443. }()
  444. return l.Addr().String()
  445. }
  446. func testSession(t *testing.T, forceV1Behavior bool) *session {
  447. cl := testClient(t, forceV1Behavior)
  448. return testSessionFor(t, cl)
  449. }
  450. func testSessionFor(t *testing.T, cl *ssh.Client) *session {
  451. s, err := cl.NewSession()
  452. if err != nil {
  453. t.Fatal(err)
  454. }
  455. t.Cleanup(func() { s.Close() })
  456. stdinReader, stdinWriter := io.Pipe()
  457. stdoutReader, stdoutWriter := io.Pipe()
  458. stderrReader, stderrWriter := io.Pipe()
  459. s.Stdin = stdinReader
  460. s.Stdout = io.MultiWriter(stdoutWriter, os.Stdout)
  461. s.Stderr = io.MultiWriter(stderrWriter, os.Stderr)
  462. return &session{
  463. Session: s,
  464. stdin: stdinWriter,
  465. stdout: stdoutReader,
  466. stderr: stderrReader,
  467. }
  468. }
  469. func generateClientKey(t *testing.T, privateKeyFile string) (ssh.Signer, *rsa.PrivateKey) {
  470. t.Helper()
  471. priv, err := rsa.GenerateKey(rand.Reader, 2048)
  472. if err != nil {
  473. t.Fatal(err)
  474. }
  475. mk, err := x509.MarshalPKCS8PrivateKey(priv)
  476. if err != nil {
  477. t.Fatal(err)
  478. }
  479. privateKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
  480. if privateKey == nil {
  481. t.Fatal("failed to encoded private key")
  482. }
  483. err = os.WriteFile(privateKeyFile, privateKey, 0600)
  484. if err != nil {
  485. t.Fatal(err)
  486. }
  487. signer, err := ssh.ParsePrivateKey(privateKey)
  488. if err != nil {
  489. t.Fatal(err)
  490. }
  491. return signer, priv
  492. }
  493. // testBackend implements ipnLocalBackend
  494. type testBackend struct {
  495. localUser string
  496. forceV1Behavior bool
  497. }
  498. func (tb *testBackend) GetSSH_HostKeys() ([]gossh.Signer, error) {
  499. var result []gossh.Signer
  500. var priv any
  501. var err error
  502. const keySize = 2048
  503. priv, err = rsa.GenerateKey(rand.Reader, keySize)
  504. if err != nil {
  505. return nil, err
  506. }
  507. mk, err := x509.MarshalPKCS8PrivateKey(priv)
  508. if err != nil {
  509. return nil, err
  510. }
  511. hostKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
  512. signer, err := gossh.ParsePrivateKey(hostKey)
  513. if err != nil {
  514. return nil, err
  515. }
  516. result = append(result, signer)
  517. return result, nil
  518. }
  519. func (tb *testBackend) ShouldRunSSH() bool {
  520. return true
  521. }
  522. func (tb *testBackend) NetMap() *netmap.NetworkMap {
  523. capMap := make(set.Set[tailcfg.NodeCapability])
  524. if tb.forceV1Behavior {
  525. capMap[tailcfg.NodeAttrSSHBehaviorV1] = struct{}{}
  526. }
  527. return &netmap.NetworkMap{
  528. SSHPolicy: &tailcfg.SSHPolicy{
  529. Rules: []*tailcfg.SSHRule{
  530. {
  531. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  532. Action: &tailcfg.SSHAction{Accept: true, AllowAgentForwarding: true},
  533. SSHUsers: map[string]string{"*": tb.localUser},
  534. },
  535. },
  536. },
  537. AllCaps: capMap,
  538. }
  539. }
  540. func (tb *testBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
  541. return (&tailcfg.Node{}).View(), tailcfg.UserProfile{
  542. LoginName: tb.localUser + "@example.com",
  543. }, true
  544. }
  545. func (tb *testBackend) DoNoiseRequest(req *http.Request) (*http.Response, error) {
  546. return nil, nil
  547. }
  548. func (tb *testBackend) Dialer() *tsdial.Dialer {
  549. return nil
  550. }
  551. func (tb *testBackend) TailscaleVarRoot() string {
  552. return ""
  553. }
  554. func (tb *testBackend) NodeKey() key.NodePublic {
  555. return key.NodePublic{}
  556. }
  557. type addressFakingConn struct {
  558. net.Conn
  559. }
  560. func (conn *addressFakingConn) LocalAddr() net.Addr {
  561. return &net.TCPAddr{
  562. IP: net.ParseIP("100.100.100.101"),
  563. Port: 22,
  564. }
  565. }
  566. func (conn *addressFakingConn) RemoteAddr() net.Addr {
  567. return &net.TCPAddr{
  568. IP: net.ParseIP("100.100.100.102"),
  569. Port: 10002,
  570. }
  571. }