From fce5b9002a944213e23be883d29c94ee08b99a1b Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 7 Jul 2021 16:01:06 -0400 Subject: [PATCH] tstest/integration/vms: add incoming SSH test This dials the VM over Tailscale and ensures that it answers SSH requests. This patch also refactors the testing framework to allow dialing connections into the Tailscale network. Signed-off-by: Christine Dodrill --- tstest/integration/vms/vms_test.go | 101 +++++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/tstest/integration/vms/vms_test.go b/tstest/integration/vms/vms_test.go index 7541a8c8a..c4668d873 100644 --- a/tstest/integration/vms/vms_test.go +++ b/tstest/integration/vms/vms_test.go @@ -7,6 +7,7 @@ package vms import ( + "bytes" "context" "crypto/sha256" "encoding/hex" @@ -37,6 +38,7 @@ import ( expect "github.com/google/goexpect" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" + "golang.org/x/net/proxy" "golang.org/x/sync/semaphore" "inet.af/netaddr" "tailscale.com/net/interfaces" @@ -63,6 +65,15 @@ var ( }() ) +type Harness struct { + testerDialer proxy.Dialer + testerDir string + bins *integration.Binaries + signer ssh.Signer + cs *testcontrol.Server + loginServerURL string +} + type Distro struct { name string // amazon-linux url string // URL to a qcow2 image @@ -627,7 +638,14 @@ func TestVMIntegrationEndToEnd(t *testing.T) { ramsem := semaphore.NewWeighted(int64(*vmRamLimit)) bins := integration.BuildTestBinaries(t) - makeTestNode(t, bins, loginServer) + h := &Harness{ + bins: bins, + signer: signer, + loginServerURL: loginServer, + cs: cs, + } + + h.makeTestNode(t, bins, loginServer) t.Run("do", func(t *testing.T) { for n, distro := range distros { @@ -670,13 +688,17 @@ func TestVMIntegrationEndToEnd(t *testing.T) { } }) - testDistro(t, loginServer, distro, signer, ipm, bins) + h.testDistro(t, distro, ipm) }) } }) } -func testDistro(t *testing.T, loginServer string, d Distro, signer ssh.Signer, ipm ipMapping, bins *integration.Binaries) { +func (h Harness) testDistro(t *testing.T, d Distro, ipm ipMapping) { + signer := h.signer + bins := h.bins + loginServer := h.loginServerURL + t.Helper() port := ipm.port hostport := fmt.Sprintf("127.0.0.1:%d", port) @@ -782,6 +804,49 @@ func testDistro(t *testing.T, loginServer string, d Distro, signer ssh.Signer, i &expect.BExp{R: `connection established`}, }) }) + + t.Run("incoming-ssh-ipv4", func(t *testing.T) { + sess, err := cli.NewSession() + if err != nil { + t.Fatalf("can't make incoming session: %v", err) + } + defer sess.Close() + ipBytes, err := sess.Output("tailscale ip -4") + if err != nil { + t.Fatalf("can't run `tailscale ip -4`: %v", err) + } + ip := string(bytes.TrimSpace(ipBytes)) + + conn, err := h.testerDialer.Dial("tcp", net.JoinHostPort(ip, "22")) + if err != nil { + t.Fatalf("can't dial connection to vm: %v", err) + } + defer conn.Close() + + sshConn, chanchan, reqchan, err := ssh.NewClientConn(conn, net.JoinHostPort(ip, "22"), ccfg) + if err != nil { + t.Fatalf("can't negotiate connection over tailscale: %v", err) + } + defer sshConn.Close() + + cli := ssh.NewClient(sshConn, chanchan, reqchan) + defer cli.Close() + + sess, err = cli.NewSession() + if err != nil { + t.Fatalf("can't make SSH session with VM: %v", err) + } + defer sess.Close() + + testIPBytes, err := sess.Output("tailscale ip -4") + if err != nil { + t.Fatalf("can't run command on remote VM: %v", err) + } + + if !bytes.Equal(testIPBytes, ipBytes) { + t.Fatalf("wanted reported ip to be %q, got: %q", string(ipBytes), string(testIPBytes)) + } + }) } func runTestCommands(t *testing.T, timeout time.Duration, cli *ssh.Client, batch []expect.Batcher) { @@ -926,19 +991,37 @@ func TestDeriveBindhost(t *testing.T) { t.Log(deriveBindhost(t)) } -func makeTestNode(t *testing.T, bins *integration.Binaries, controlURL string) { +func (h *Harness) Tailscale(t *testing.T, args ...string) { + t.Helper() + + args = append([]string{"--socket=" + filepath.Join(h.testerDir, "sock")}, args...) + run(t, h.testerDir, h.bins.CLI, args...) +} + +// makeTestNode creates a userspace tailscaled running in netstack mode that +// enables us to make connections to and from the tailscale network being +// tested. This mutates the Harness to allow tests to dial into the tailscale +// network as well as control the tester's tailscaled. +func (h *Harness) makeTestNode(t *testing.T, bins *integration.Binaries, controlURL string) { dir := t.TempDir() + h.testerDir = dir + + port, err := getProbablyFreePortNumber() + if err != nil { + t.Fatalf("can't get free port: %v", err) + } + cmd := exec.Command( bins.Daemon, "--tun=userspace-networking", "--state="+filepath.Join(dir, "state.json"), "--socket="+filepath.Join(dir, "sock"), - "--socks5-server=localhost:0", + fmt.Sprintf("--socks5-server=localhost:%d", port), ) cmd.Env = append(os.Environ(), "NOTIFY_SOCKET="+filepath.Join(dir, "notify_socket")) - err := cmd.Start() + err = cmd.Start() if err != nil { t.Fatalf("can't start tailscaled: %v", err) } @@ -974,6 +1057,12 @@ outer: "--login-server="+controlURL, "--hostname=tester", ) + + dialer, err := proxy.SOCKS5("tcp", net.JoinHostPort("127.0.0.1", fmt.Sprint(port)), nil, &net.Dialer{}) + if err != nil { + t.Fatalf("can't make netstack proxy dialer: %v", err) + } + h.testerDialer = dialer } type nopWriteCloser struct {