tailssh_integration_test.go 17 KB

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