mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-06 04:36:15 +02:00
tstest/integration/vms: outgoing SSH test
This does two things: 1. Rewrites the tests so that we get a log of what individual tests failed at the end of a test run. 2. Adds a test that runs an SSH server via the tester tailscale node and then has the VMs connect to that over Tailscale. This tests outgoing TCP connections. Signed-off-by: Christine Dodrill <xe@tailscale.com>
This commit is contained in:
parent
ddb8726c98
commit
5f0799f935
137
tstest/integration/vms/ssh_test.go
Normal file
137
tstest/integration/vms/ssh_test.go
Normal file
@ -0,0 +1,137 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux
|
||||
|
||||
package vms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func mkSSHServer(t *testing.T, hostKey ssh.Signer, bindhost string) string {
|
||||
t.Helper()
|
||||
|
||||
config := &ssh.ServerConfig{
|
||||
NoClientAuth: true,
|
||||
AuthLogCallback: func(conn ssh.ConnMetadata, method string, err error) {
|
||||
t.Logf("ssh server connection auth: %s, %s: %v", conn.RemoteAddr(), method, err)
|
||||
},
|
||||
}
|
||||
|
||||
config.AddHostKey(hostKey)
|
||||
|
||||
lis, err := net.Listen("tcp", net.JoinHostPort(bindhost, "0"))
|
||||
if err != nil {
|
||||
t.Fatalf("can't listen on anonymous port: %v", err)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
t.Logf("closing socket")
|
||||
lis.Close()
|
||||
})
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := lis.Accept()
|
||||
if err != nil {
|
||||
t.Logf("unexpected SSH connection error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
sc, chanchan, reqchan, err := ssh.NewServerConn(conn, config)
|
||||
if err != nil {
|
||||
t.Logf("client can't register with ssh: %s: %v", conn.RemoteAddr(), err)
|
||||
return
|
||||
}
|
||||
defer sc.Close()
|
||||
|
||||
go ssh.DiscardRequests(reqchan)
|
||||
|
||||
newChannel := <-chanchan
|
||||
channel, requests, err := newChannel.Accept()
|
||||
if err != nil {
|
||||
t.Logf("can't accept channel from %s: %v", conn.RemoteAddr(), err)
|
||||
return
|
||||
}
|
||||
t.Logf("%s: %s", conn.RemoteAddr(), newChannel.ChannelType())
|
||||
|
||||
go func(in <-chan *ssh.Request) {
|
||||
for req := range in {
|
||||
req.Reply(req.Type == "shell", nil)
|
||||
}
|
||||
}(requests)
|
||||
|
||||
term := terminal.NewTerminal(channel, "> ")
|
||||
|
||||
time.Sleep(time.Second)
|
||||
fmt.Fprintln(term, "connection established")
|
||||
channel.Close()
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
return lis.Addr().String()
|
||||
}
|
||||
|
||||
func TestMkSSHServer(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
run(t, dir, "ssh-keygen", "-t", "ed25519", "-f", "machinekey", "-N", ``)
|
||||
|
||||
privateKey, err := os.ReadFile(filepath.Join(dir, "machinekey"))
|
||||
if err != nil {
|
||||
t.Fatalf("can't read ssh private key: %v", err)
|
||||
}
|
||||
|
||||
signer, err := ssh.ParsePrivateKey(privateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("can't parse private key: %v", err)
|
||||
}
|
||||
|
||||
addr := mkSSHServer(t, signer, "::1")
|
||||
t.Logf("connecting to %s", addr)
|
||||
host, port, _ := net.SplitHostPort(addr)
|
||||
|
||||
// NOTE(Xe): I tried to use go's SSH library for this but it just wouldn't work.
|
||||
// The way my SSH server works is that it just spews stuff and kills the session
|
||||
// afterwards. This is apparently in violation of how SSH servers are supposed
|
||||
// to normally work, but the goal here is to get the text spewed back so it
|
||||
// doesn't really matter.
|
||||
cmd := exec.Command("ssh",
|
||||
"-o", "StrictHostKeyChecking=no",
|
||||
"-o", "GlobalKnownHostsFile=/dev/null",
|
||||
"-o", "UserKnownHostsFile=/dev/null",
|
||||
host, "-p", port,
|
||||
)
|
||||
buf := bytes.NewBuffer(nil)
|
||||
cmd.Stdout = io.MultiWriter(buf, logger.FuncWriter(t.Logf))
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
if eerr, ok := err.(*exec.ExitError); ok {
|
||||
if eerr.ExitCode() != 255 {
|
||||
t.Fatalf("can't ssh into %s: %v", addr, err)
|
||||
}
|
||||
} else {
|
||||
t.Fatalf("can't ssh into %s: %v", addr, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !bytes.Contains(buf.Bytes(), []byte("connection established")) {
|
||||
t.Fatalf("wanted \"connection established\" from ssh server, got: %q", buf)
|
||||
}
|
||||
}
|
||||
@ -716,6 +716,75 @@ func testDistro(t *testing.T, loginServer string, d Distro, signer ssh.Signer, i
|
||||
|
||||
timeout := 30 * time.Second
|
||||
|
||||
t.Run("start-tailscale", func(t *testing.T) {
|
||||
var batch = []expect.Batcher{
|
||||
&expect.BExp{R: `(\#)`},
|
||||
}
|
||||
|
||||
switch d.initSystem {
|
||||
case "openrc":
|
||||
// NOTE(Xe): this is a sin, however openrc doesn't really have the concept
|
||||
// of service readiness. If this sleep is removed then tailscale will not be
|
||||
// ready once the `tailscale up` command is sent. This is not ideal, but I
|
||||
// am not really sure there is a good way around this without a delay of
|
||||
// some kind.
|
||||
batch = append(batch, &expect.BSnd{S: "rc-service tailscaled start && sleep 2\n"})
|
||||
case "systemd":
|
||||
batch = append(batch, &expect.BSnd{S: "systemctl start tailscaled.service\n"})
|
||||
}
|
||||
|
||||
batch = append(batch, &expect.BExp{R: `(\#)`})
|
||||
|
||||
runTestCommands(t, timeout, cli, batch)
|
||||
})
|
||||
|
||||
t.Run("login", func(t *testing.T) {
|
||||
runTestCommands(t, timeout, cli, []expect.Batcher{
|
||||
&expect.BSnd{S: fmt.Sprintf("tailscale up --login-server=%s\n", loginServer)},
|
||||
&expect.BExp{R: `Success.`},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("tailscale status", func(t *testing.T) {
|
||||
runTestCommands(t, timeout, cli, []expect.Batcher{
|
||||
&expect.BSnd{S: "sleep 5 && tailscale status\n"},
|
||||
&expect.BExp{R: `100.64.0.1`},
|
||||
&expect.BExp{R: `(\#)`},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ping-ipv4", func(t *testing.T) {
|
||||
runTestCommands(t, timeout, cli, []expect.Batcher{
|
||||
&expect.BSnd{S: "tailscale ping -c 1 100.64.0.1\n"},
|
||||
&expect.BExp{R: `pong from.*\(100.64.0.1\)`},
|
||||
&expect.BSnd{S: "ping -c 1 100.64.0.1\n"},
|
||||
&expect.BExp{R: `bytes`},
|
||||
})
|
||||
})
|
||||
|
||||
// This test spawns a test-local SSH server that requires no authentication and
|
||||
// returns "connection established" to every request fired at it. This allows us
|
||||
// to ensure that outgoing TCP works over Tailscale.
|
||||
//
|
||||
// NOTE(Xe): SSH was chosen for this test because SSH is universal across all of
|
||||
// the images that we will run this against. Admittedly it is kind of an odd
|
||||
// choice, however it more closely reflects real-world usage of Tailscale.
|
||||
t.Run("outgoing-ssh-ipv4", func(t *testing.T) {
|
||||
sshServer := mkSSHServer(t, signer, "::")
|
||||
_, port, _ := net.SplitHostPort(sshServer)
|
||||
runTestCommands(t, timeout, cli, []expect.Batcher{
|
||||
&expect.BSnd{
|
||||
S: fmt.Sprintf(
|
||||
"ssh -o StrictHostKeyChecking=no -o GlobalKnownHostsFile=/dev/null -o UserKnownHostsFile=/dev/null %s -p %s\n",
|
||||
"100.64.0.1",
|
||||
port,
|
||||
)},
|
||||
&expect.BExp{R: `connection established`},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func runTestCommands(t *testing.T, timeout time.Duration, cli *ssh.Client, batch []expect.Batcher) {
|
||||
e, _, err := expect.SpawnSSH(cli, timeout,
|
||||
expect.Verbose(true),
|
||||
expect.VerboseWriter(logger.FuncWriter(t.Logf)),
|
||||
@ -725,42 +794,10 @@ func testDistro(t *testing.T, loginServer string, d Distro, signer ssh.Signer, i
|
||||
// expect.Tee(nopWriteCloser{logger.FuncWriter(t.Logf)}),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("%d: can't register a shell session: %v", port, err)
|
||||
t.Fatalf("%s: can't register a shell session: %v", cli.RemoteAddr(), err)
|
||||
}
|
||||
defer e.Close()
|
||||
|
||||
t.Log("opened session")
|
||||
|
||||
var batch = []expect.Batcher{
|
||||
&expect.BSnd{S: "PS1='# '\n"},
|
||||
&expect.BExp{R: `(\#)`},
|
||||
}
|
||||
|
||||
switch d.initSystem {
|
||||
case "openrc":
|
||||
// NOTE(Xe): this is a sin, however openrc doesn't really have the concept
|
||||
// of service readiness. If this sleep is removed then tailscale will not be
|
||||
// ready once the `tailscale up` command is sent. This is not ideal, but I
|
||||
// am not really sure there is a good way around this without a delay of
|
||||
// some kind.
|
||||
batch = append(batch, &expect.BSnd{S: "rc-service tailscaled start && sleep 2\n"})
|
||||
case "systemd":
|
||||
batch = append(batch, &expect.BSnd{S: "systemctl start tailscaled.service\n"})
|
||||
}
|
||||
|
||||
batch = append(batch,
|
||||
&expect.BExp{R: `(\#)`},
|
||||
&expect.BSnd{S: fmt.Sprintf("tailscale up --login-server=%s\n", loginServer)},
|
||||
&expect.BExp{R: `Success.`},
|
||||
&expect.BSnd{S: "sleep 5 && tailscale status\n"},
|
||||
&expect.BExp{R: `100.64.0.1`},
|
||||
&expect.BExp{R: `(\#)`},
|
||||
&expect.BSnd{S: "tailscale ping -c 1 100.64.0.1\n"},
|
||||
&expect.BExp{R: `pong from.*\(100.64.0.1\)`},
|
||||
&expect.BSnd{S: "ping -c 1 100.64.0.1\n"},
|
||||
&expect.BExp{R: `bytes`},
|
||||
)
|
||||
|
||||
_, err = e.ExpectBatch(batch, timeout)
|
||||
if err != nil {
|
||||
sess, terr := cli.NewSession()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user