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 {